Skip to content

Release 3.0.0: merge develop → main - #283

Merged
ofloveandhate merged 742 commits into
mainfrom
develop
Jul 7, 2026
Merged

Release 3.0.0: merge develop → main#283
ofloveandhate merged 742 commits into
mainfrom
develop

Conversation

@ofloveandhate

Copy link
Copy Markdown
Contributor

Promote develop to main for the 3.0.0 release. Everything here was validated by the rc1 publish run (green, 15 wheels live on TestPyPI) on the new fast prebuilt-deps CI.

Merge immediatelymain requires a PR but 0 approvals and no required status checks, so no need to wait on this PR's CI (it re-validates the exact code rc1 already proved; I'll cancel it after merge to spare runners). Use a merge commit to preserve history.

After this merges: bump VERSION → 3.0.0 on main (version-only PR, no CI), then tag v3.0.0 → publish.yml → real PyPI + Sigstore GitHub Release + first versioned docs deploy.

🤖 Generated with Claude Code

ofloveandhate and others added 30 commits June 29, 2026 14:36
Update the parameter-homotopy, moving-slice, and user-product-of-linears tutorials
to call the first-class nag_algorithm.HomotopySolver (the continuation primitive)
instead of the user_homotopy forwarder, and reference it consistently with :func:.
Identical signature, clearer name.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…stalls

The wheel ships the bertini2 CLI at bertini/_bin/ via a SKBUILD-gated install rule,
but a from-source EDITABLE install (`pip install -e .`) does not run install(), so
the console-script shim had no binary to hand off to and the CLI smoke test skipped.

Add a non-wheel POST_BUILD step on bertini2_exe that copies the freshly-built CLI
into the source-tree python/bertini/_bin/bertini2 (where an editable install resolves
the package), guarded on the package dir existing.  Now every build path delivers the
`bertini2` command: wheel (`pip install .`), editable (`pip install -e .`), and plain
in-tree builds.  python/bertini/_bin/ is gitignored.

CLI smoke tests now run + pass locally (2 passed).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Tag the bertini2_exe install rules COMPONENT cli, so a user who wants only the
solver -- no Python, no library/headers -- can `cmake --install build --component cli`
and get just bin/bertini2 (the exe statically links libbertini2.a, so it is
self-contained re: our code; it still needs Boost/GMP/MPFR/MPC/MPI like any
from-source build).  A plain `cmake --install` still installs everything.

Verified: --component cli installs only bin/bertini2 (runs); the default install
still ships the exe + library + headers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Document that the bertini2 CLI is pure C++ (no Python dependency) and how to get it
from each build path -- wheel (pip install bertini2), from-source pip (install . /
-e .), the Python-free CLI-only install (cmake --install --component cli), and the
full C++ dev install -- plus the shared-library expectations and threads-only/MPI note.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Follow-up on the over-determined filter, for regeneration-cascade use.

- SolutionMetaData gains `is_nonsolution`: a finite, successful endpoint that is
  NOT a solution of the target -- the junk squaring up an over-determined system
  introduces.  The filter now sets THIS (orthogonal to is_finite) instead of
  is_finite=false, so nonsolutions stay geometrically finite and no longer masquerade
  as at-infinity.  The finite / real / singular accessors exclude them; a new
  Nonsolutions() / nonsolutions() surfaces them; SolveReport gains num_nonsolutions.

- New `solutions(**flags)` getter on both ZeroDimSolver and HomotopySolver: finite
  genuine solutions by default, with category keywords -- singular / nonsingular,
  real / nonreal (a finite solution is returned iff its conditioning class AND its
  realness class are enabled), infinite=True to add at-infinity endpoints,
  nonsolution=True to add the junk.  all_solutions() stays as the raw per-path list.

is_nonsolution is bound, added to to_dataframe columns, and documented in the
ZeroDimSolver tutorial.  Tests: C++ over-determined test asserts the flag / Nonsolutions
/ report count; Python feasibility_test covers solutions() filtering by realness and
nonsolution opt-in.  C++ suites pass (OMP=1); Python suite 547 passed / 1 skipped.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…"junk" -> nonsolutions

The over-determined test asserted an exact nonsolution count (Nonsolutions == all - 2),
which assumes every extra root of the squared system stays finite.  How many of the
squaring's extra roots land finite vs. diverge to infinity is RNG/gamma-dependent, so
that passed locally but failed the Ubuntu C++ job.  Assert the ROBUST partition instead:
FiniteSolutions + Nonsolutions + InfiniteSolutions == all endpoints (every endpoint is
exactly one of genuine finite solution / finite nonsolution / at infinity), plus
solutions() == 2 (the genuine roots).  Same fix in the Python feasibility test and the
tutorial doctest.

Also rename the user-facing "junk" wording to "nonsolutions" throughout (report line,
docstrings, comments) per review.

C++ suites pass (OMP=1 + threaded); Python suite 547 passed / 1 skipped.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…perating-zone test

ComputeCOverK drew a FRESH random probe vector on every call, so the c/k operating-zone estimate was
non-deterministic: consecutive estimates disagreed by probe noise alone, which could certify (or
stall) the zone spuriously and churned an mpfr allocation each call.  It now reuses a single fixed
probe (new member c_over_k_probe_, generated once per precision, re-precisioned in place), so the
estimate is reproducible and CheckForCOverKStabilization measures actual sample stabilization.

Effect on the actual default start system (RootsOfUnity), cyclic-5 AMP, seed 1: 3.37s -> ~3.06s with
the number of escalating paths dropping from 2 to ~0 (the noisy probe had been driving spurious
precision escalation), plus the per-call mpfr churn is gone.

Also adds CauchyConfig::num_consecutive_same_cycle_number (default 2): refuse to accept a converged
Cauchy approximation until the cycle number has reported the same value that many times in a row, so a
coincidental approx_error dip on an unreliable low-precision cycle count is not mistaken for
convergence.  Exposed in the python bindings; verified not to regress the adaptive case.

Scope note: this does NOT fix the linear-product TotalDegree start system, whose Cauchy endgame still
stalls near t=0 (double and adaptive) -- that is a separate, known issue for a later sprint.  Earlier
"TotalDegree" validation through the python facade inadvertently exercised RootsOfUnity, because
ZeroDim(sys, startsystem='totaldegree') maps to the default constructor / default start system.

C++ ctest green; python suite green (1 pre-existing numpy-env failure unrelated).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…homogenization + interop

Adds core/test/classes/start_system_interop_test.cpp (9 cases) exercising the start-system layer
at the corners, to establish sanity around the linear-product TotalDegree and its multi-group
generalization MHomogeneous:

- EVERY start point of TotalDegree and MHomogeneous is verified to be a root of the start system,
  on its patch, in double AND high multiprecision (not a sampled few).  At 100 digits the patch
  residual is < 1e-90, confirming start points are correctly homogenized and patch-fitted across
  the precision range -- i.e. there is no start-point patch-normalization defect.
- MHomogeneous on a single affine variable group reproduces TotalDegree's Bezout count
  (product of the function degrees), across several degree tuples -- MHom is a generalization of
  total degree, and the two must agree on one group.
- The blend homotopy H = (1-t) target + gamma t start vanishes at every start point at t=1, for
  both TotalDegree and MHomogeneous on the same target (homotopy interoperability).
- Corners: single-variable degree-d (exactly d start points), Homogenize/Dehomogenize roundtrip,
  and the construction guards (non-square and homogeneous-group targets must throw).

These run only the start-system layer (no full ZeroDim solve), so they are fast and cannot hang.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…r hang-prone solves)

macOS has no timeout(1), and some solves can spin/churn indefinitely (notably the linear-product
TotalDegree start system, whose Cauchy endgame stalls near t=0).  This wraps any command with a
wall-clock limit and SIGKILLs the whole process group on overrun (exit 124), so a hung solve can't
peg a core forever.  The b2-mcp server bakes the same behavior into its tool runner; this is the
plain-CLI equivalent.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…_time_seconds)

Adds SolutionMetaData::path_time_seconds -- the wall-clock time to execute a whole path (pre-endgame
tracking + endgame), stamped in ExecuteOnePath around both exit points with a monotonic steady_clock.
Each path writes only its own metadata slot, so there is no cross-path race in a threaded/distributed
solve; it is plumbed through FullPathResult (Pack/Store + serialize) so the manager-worker and MPI
paths carry it too.  Excluded from SolutionMetaData::operator== (timing is not an identity field).

Exposed to Python as the read-write `path_time_seconds` attribute and added to to_dataframe()'s
column set, so per-path cost shows up in the solutions dataframe alongside condition_number and
max_precision_used (e.g. sort paths by time, or correlate time with precision escalation).

Verified: default cyclic-5 solve stamps all 120 paths (sum ~3.0s), C++ ctest 10/10 green, python
suite green (1 pre-existing numpy-env failure unrelated).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…segfaulting

Manually composing a homotopy with a structured-block start system -- e.g.
(1-t)*target + gamma*t*TotalDegree, exactly the gamma-trick in the manual-endgame tutorial --
SEGFAULTED.  System::operator+= and operator*= only touched PolyBlock().Functions(), but the
linear-product TotalDegree (and MHomogeneous) keep their functions in a ProductsOfLinearsBlock, not
the PolynomialBlock.  So:
  * operator*= (gamma*t*td) silently multiplied an EMPTY PolynomialBlock -> a wrong result (the start
    system left unscaled), no crash;
  * operator+= then indexed rhs.PolyFunctions() (size 0) up to lhs's function count -> out-of-bounds
    read -> segmentation fault.

Both operators now detect a structured block (HasStructuredBlocks) and, in that case, expand every
block to function-tree nodes via NaturalFunctionsAsNodes(), operate on those, and store the result as
a single PolynomialBlock (a function-tree system that evaluates and tracks correctly).  The
pure-polynomial fast path is unchanged, so existing systems are unaffected.

Verified: the previously-segfaulting (1-t)*gw + t*gamma*TotalDegree now builds a correct homotopy
(all 6 start points solve H at t=1 to ~5e-11), the manual_endgame_usage doctest no longer crashes,
C++ ctest 10/10 green, python suite green (1 pre-existing numpy-env failure unrelated).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…mplated names

After the de-templating (the start system is no longer baked into the ZeroDim type), the bound class
names dropped the start-system suffix: ZeroDimCauchyDoublePrecision (not ...TotalDegree), etc.  Update
the doctest assertions accordingly and note that the start system is now a construction choice, so it
is not part of the class name.

(The separate precision_matters doctest -- at-infinity paths counted as num_failed -- is being handled
elsewhere; untouched here.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…#263)

System::Function(index) dereferenced PolyBlockPtr() with no null check, so it
segfaulted on a system built from a slice (a pure LinearFormsBlock, with no
PolynomialBlock). Route Function() and GetNaturalFunctions() through
NaturalFunctionsAsNodes() so every block type (polynomial, linear forms,
products of linears, randomization, blend) is expanded to a function-tree node
on demand, in sync with NumNaturalFunctions(); out-of-range now throws
std::out_of_range (-> Python IndexError) instead of crashing.

Add System::Slices() (bound as system.slices()) to back out the linear-form
slices embedded in a system -- the inverse of Slice.as_system() -- without
exposing the Block variant to Python.

Tests: function_accessor_test.py covers function(i) across all five block types
plus slices() round-trip and node/block eval agreement; C++ system_blocks
suite gains structured-block Function()/Slices() cases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The over-determined filter flags is_nonsolution (a dedicated, orthogonal metadata
field) rather than is_finite=false, so nonsolutions stay geometrically finite and are
surfaced by Nonsolutions() / counted in num_nonsolutions; it is load-bearing for the
regeneration cascade.  Also note the additive solutions(**flags) getter.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
)

A system built from a slice printed every function as the opaque placeholder
`c.[x, y, z, 1]`, hiding the coefficients. Keep the placeholder (structure
stays legible) and print the actual coefficient matrix just below it as a
named-expression-style `c =` legend: 4 significant figures in the default /
terse describe, full working precision under describe(verbose=True). The same
applies to a randomization block's `R` matrix, which was previously gated
behind verbose -- it now shows in the default describe too.

PrintCoeff gains a significant-digits parameter (terse=4, verbose=current
precision; the master coefficient matrix is stored far above working precision,
so its full string would be thousands of digits). Each structured block
truncates its terse listing after kTerseRowCap (10) rows with a "... (k more)"
note so a large slice does not flood the terminal; verbose prints everything.

products-of-linears and blend keep their existing structural terse output
(they carry no inline coefficients to shorten).

Tests: system_printing_test (Python + C++) updated for the new default output
and extended with terse-vs-verbose digit-width and row-cap-truncation cases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…_homotopysolver

Split ZeroDim into HomotopySolver (engine) + ZeroDimSolver (algorithm)
# Conflicts:
#	python/docs/source/tutorials/precision_models.rst
…-timing

Cauchy c/k probe determinism, start-system sanity tests, per-path timing
…ystem

fix: function() on slice-derived systems + system.slices() (#263)
feat: show low-precision coefficients in system describe (#264)
The `H.eval_time_derivative(...)` block in the moving-slice tutorial failed under
`sphinx -b doctest`: the preceding adaptive `solver.solve()` leaves the homotopy H at
double precision (16), while the multiprecision evaluation point is created at the
multiprec literal default (20), so the direct eval raised "precision of input point in
SetVariables (20) must match the precision of the system (16)".

Match H to the evaluation point's precision before the direct eval. (Using
`bertini.default_precision()` does NOT work here: the AMP solve leaves the global default
at 16, which disagrees with the multiprec literal default of 20 -- a latent precision-state
inconsistency worth a separate look, but out of scope for this doctest fix.)

Found by running the full tutorial doctest suite locally (it is not in the per-push PR CI,
only the dispatched build_docs workflow): 147 tests, now 0 failures.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ecision

fix(docs): moving_slice eval_time_derivative doctest precision mismatch
Foundation for fixing and locking down the C++ API docs. The docs had
rotted (stale @param/@tparam, signature drift) and nothing caught it:
the Doxyfile runs EXTRACT_ALL=YES with all warnings off, and the doc
build only runs at publish time, never on PRs.

- tools/doclint.sh: cheap Doxygen-only lint (no compile/wheel/graphviz).
  Pass 1 is a zero-tolerance error gate for *wrong* docs (param/signature
  mismatches, broken refs); Pass 2 is a monotonic ratchet on the
  undocumented-entity count vs tools/doc_undocumented_baseline.txt.
- .github/workflows/doc_lint.yml: runs the lint on every PR with a pinned
  Doxygen (the ratchet count is version-specific). First foothold of a
  broader linting initiative.
- Doxyfile: wire CITE_BIB_FILES to the shared bibliography so \cite
  resolves; build_docs.yml installs bibtex so citations render in the
  published docs.
- Remove the orphaned legacy core/doc/ (stale bertini.doxy.config, its
  generated_documentation/, dead review .tex, dangling images_common).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drive the doc-lint error gate (tools/doclint.sh pass 1) from 115 warnings
to zero.  All changes are comment-only.  Fixes fall into a few patterns
left behind by past refactors:

- Stale @param names that no longer match the signature (Predict,
  HermiteInterpolateAndSolve, CircleAdvanced, EnsureAtUniformPrecision,
  RandomMp, the linear-forms / products-of-linears blocks, Initializing,
  TransformToSPlane, MakeHomotopy/MakeMovingHomotopy, the System
  Eval/JacobianInPlace family).
- @return documented on void in-place functions; dropped.
- Orphaned doc blocks describing removed functions (AddFunction overloads,
  a stale NumRealSlots block); removed.
- Malformed markup: \param[out] typo, an unterminated ``` fence (system.hpp),
  a stray code-span backtick closed by an apostrophe (number.hpp), <file>/<tt>
  parsing, an unresolved #factors_i link, \url, and missing \defgroup titles.
- extern-template instantiation blocks hidden from Doxygen with \cond
  (they are not API and confused the member matcher).

Seed tools/doc_undocumented_baseline.txt at 1821 -- the monotonic ratchet
floor that Phase C will drive toward zero.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Make the cheap Doxygen-only doc lint a prerequisite of every job that
compiles the C++ core, so a documentation error fails the run in ~1 minute
and no expensive build/test job ever starts.

- doc_lint.yml gains a workflow_call trigger and is invoked by
  build_and_test.yml as the `doc_lint` job; test_cpp_unix and
  test_cpp_windows (the first compilers) now `needs` it, so the wheel jobs
  that depend on them are transitively gated too.
- Scope doc_lint.yml's standalone push/pull_request triggers to the
  doc-config/tooling paths that build_and_test.yml already paths-ignores
  (Doxyfile, doc_resources, tools/doclint.sh, the baseline, the workflow),
  so doc-only changes are still linted without double-running on code PRs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Scaffold (no revisions yet) so an upcoming whitespace/indentation pass --
which will touch nearly every header -- can be kept in one isolated,
formatting-only commit and excluded from `git blame`, instead of burying
real authorship. GitHub honours this file automatically.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ofloveandhate and others added 29 commits July 6, 2026 14:18
…doctest

The doctest convention used python/docs/_doctest_build (not matched by the build/
ignore patterns, so a forgotten one shows as untracked).  Ignore _doctest_build/ and
switch the documented convention to python/docs/build/doctest, which is already under
the gitignored build/ dir.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… Reference

The docs reorg (merged #72) switched html_theme to 'furo', but nothing installed it
(CI's build_docs and the dev env both lacked it), so the HTML build failed with 'no
theme named furo found'.  Add furo to build_docs.yml.

Also, per review:
- Drop 'sphinx_rtd_theme' as a loaded extension (conf.py) and from the CI install --
  it did nothing once furo became the theme; the release-in-sidebar comment is now
  theme-agnostic.
- Lift the Full API Reference: api.rst previously autosummary'd a single 'bertini'
  entry, so the page was one table row and the real reference lived a click away at
  _autosummary/bertini.  api.rst now shows the bertini module surface inline
  (automodule) plus the recursive submodule index -- one page, no extra click.

HTML build verified clean with -W (warnings-as-errors, as CI runs it).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…he period

The module docstring's first line rendered as a de-emphasized paragraph
('bertini – Python bindings for Bertini 2.') under the automodule directive.
Formatting it as an RST section header (underline) makes it a proper <h2> on the
Full API Reference page, and the trailing period is gone.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Boost 1.90 (up from 1.87, commit 54958ef) made 'b2 install' hard-error on boost_mpi
when no MPI toolset is present ('No best alternative for .../boost_mpi'), where 1.87
silently skipped it.  215fd3e fixed this on the Linux wheel build with --without-mpi
but not the macOS one, so the macOS-14 wheel build started failing.  bertini uses raw
MPI via its own headers, not boost.mpi, so building it is pointless -- skip it, exactly
as Linux does.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Boost version was hard-coded in 13 places, which is exactly why the 1.87->1.90
bump (54958ef) went wrong: the companion --without-mpi fix (215fd3e) reached the
Linux b2 install but not macOS, because there was no single place to change.  Now
BOOST_VERSION is one env var (mirroring EIGENPY_VERSION); the underscored tarball
form (boost_1_90_0) is derived in-shell ($BOOST_VERSION in run: steps, the baked
${{ env.BOOST_VERSION }} in the cibuildwheel commands) since GitHub expressions have
no string replace(); the one cache path that needed the tarball name is now a
version-agnostic glob.  A future bump is a single, atomic edit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…the Cauchy endgame doctrine

- ADR-0045 + 0047 gained dated update notes: the two-store layout + single results.json
  evolved into three truth stores (history/ + results/ + definitions/); results.json and
  RESULTS.txt are retired.  The original design text stays for the record.
- ADR-0048 (new): the Cauchy endgame's divergence handling — watch the extrapolated
  ENDPOINT (not loop samples), truncate ONLY in the operating zone, no pole-growth
  truncation (the acceptance gate alone cures junk-success).  Records caught the cyclic-6
  regression; B1 gave ground truth; refines #70.  Restores cyclic-6's 156 solutions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Structured output directory: records, resume, provenance
…omotopy, two-level parallelism... (#238)

This PR merges the entire `ofloveandhate/b2` `develop` line into
upstream — ~70 internal PRs. It's large, so this description indexes the
work by theme, links each internal PR, and lists the upstream issues it
closes. Going forward I'll open PRs directly against
`bertiniteam/develop` so this roll-up isn't needed again.

## Highlights

- **~10× faster multiprecision** eval (tiered SLP arithmetic,
allocation-free eval, CSE, in-place LU) and **5–7×** faster
well-conditioned AMP solves by staying in double where safe.
- **Content-addressed function trees and systems** —
hash-consing/interning, symbolic Jacobian, `Seal()`.
- **Parallel-by-default solving** without requiring MPI, plus MPI
serialization & reproducibility fixes.
- **Friendlier Python surface**: flattened namespace, solutions in user
coordinates, `to_dataframe`, sympy bridge, Unicode identifiers, revamped
config model.
- The **`bertini2` CLI ships in the PyPI wheel** and emits
Bertini-1.7-compatible solution files.
- **Durable, resumable output with provenance**: every solve writes a
plain-text **structured output directory** — content-addressed inputs,
one results file per run, walkable provenance chains — and consults it
first, so a killed run *finishes* on rerun. Content-identity digests
(systems, configs, seeds) make `seed=42` mean the exact same homotopy
forever.
- Substantial **tracker/endgame** correctness (Cauchy security &
pole-zone guards, predict/correct rewrite, adaptive-numeric-type AMP
endgames).
- **Executable, verified docs** (doctest), a C++ doc-lint gate, and ADRs
for load-bearing decisions.

## Upstream issues closed

- Closes #156 — pi computed inefficiently → ofloveandhate#21
- Closes #239 — ship the (non-MPI) CLI in the wheel →
ofloveandhate#46
- Closes #253 — refresh solving-at-scale timing tables →
ofloveandhate#56
- Closes #255 — observer subsystem refactor → ofloveandhate#23
- Closes #256 — Concatenate using cloned systems → ofloveandhate#26
- Closes #258 — `ZeroDim…TotalDegree` not iterable →
ofloveandhate#30, #31, #33
- Closes #259 — nondeterministic GMP SIGABRT (uninitialized mpfr/mpc
slot) → ofloveandhate#32
- Closes #263 — segfault `.function()` on a slice-derived system →
ofloveandhate#51
- Closes #264 — printing a system converted from a generic slice →
ofloveandhate#52
- Closes #51 — system generator (classic input export) →
ofloveandhate#26
- Closes #74 — parser builds generic, not integer, power nodes →
ofloveandhate#40, #7
- Closes #87 — centralize/seed random number generation →
ofloveandhate#17
- Closes #123 — add `__str__` methods to many pybertini types →
ofloveandhate#7
- Closes #128 — expose the m-hom start system to pybertini →
ofloveandhate#18
- Closes #130 — tutorial on endgame usage → ofloveandhate#22
- Closes #131 — tutorial: solve a system start-to-finish →
ofloveandhate#22
- Closes #133 — tutorial on user-defined homotopies →
ofloveandhate#22
- Closes #148 — pybertini complexes not pickleable → ofloveandhate#21
- Closes #210 — build a System from a list of functions →
ofloveandhate#3, #65
- Closes #228 — stringification produces too many parentheses →
ofloveandhate#7
- Closes #232 — improve CI build time → ofloveandhate#15
- Closes #235 — endgames need a genuinely adaptive precision mode →
ofloveandhate#55
- Closes #236 — make observers from Python with callbacks →
ofloveandhate#23
- Closes #243 — auto-randomize-and-filter overdetermined inputs →
ofloveandhate#49, #19
- Closes #247 — reparseable export (Bertini-1 classic format) →
ofloveandhate#26, #34, #39
- Closes #249 — rewrite predictor/corrector (per-track probe, pure
kernel) → ofloveandhate#43
- Closes #251 — non-mutating (copy-on-write) Simplify →
ofloveandhate#25, #7
- Closes #71 — TotalDegree constructor wrong for a non-affine variable
group → ofloveandhate#48
- Closes #95 — Reset wrapper for generic nodes (mooted by SLP-only
evaluation) → ofloveandhate#6
- Closes #100 — variable-values-random-by-default (mooted by SLP-only
evaluation) → ofloveandhate#25
- Closes #107 — optimize resetting of nodes in the function tree (mooted
by SLP-only evaluation) → ofloveandhate#25
- Closes #116 — mark leaf core types `final` (node hierarchy
de-virtualized) → ofloveandhate#6
- Closes #129 — move the start system out from under `system` →
ofloveandhate#48, #49
- Closes #132 — user homotopy missing its link to start points →
ofloveandhate#18, #49
- Closes #154 — precision consistency when homogenizing variables →
ofloveandhate#25, #26
- Closes #198 — computational inefficiency in predict/correct FullStep →
ofloveandhate#43
- Closes #199 — rename ODE predictor stages/variables →
ofloveandhate#43
- Closes #260 — user-homotopy crash (duplicate parameter functions) →
ofloveandhate#33
- Closes #261 — `user_homotopy()` parameters excessive →
ofloveandhate#49

## Internal PRs by theme

### Multiprecision & evaluation performance
- ofloveandhate#3 — grabbag of precision & step-size improvements
(folds in the CLI/MPI/pytest work of fork #4).
- ofloveandhate#5 — run well-conditioned AMP paths in double
precision (5–7×).
- ofloveandhate#40 — tiered SLP arithmetic + allocation-free mp eval
(~10×).
- ofloveandhate#41 — mimalloc + GMP allocator, AMP start-at-double,
cheaper norms/temporaries.
- ofloveandhate#44 — AMP Criterion B uses the Newton residual, not
size-proportion (cyclic-5 11s → 3.5s).
- ofloveandhate#58 — stop over-reading the SLP instruction tape by
one word in `Eval`.
- ofloveandhate#60 — SLP CSE: product power-fold + value-numbering
(30–45% faster mp eval).
- ofloveandhate#71 — stateful in-place multiprecision LU solver
(replaces Eigen in the tracker).

<img width="2290" alt="Tiered SLP arithmetic roadmap"
src="https://github.com/user-attachments/assets/bded1a18-6abb-4bb0-acb1-36307ff01f53"
/>

### Function tree & content-addressed systems
- ofloveandhate#6 — eliminate diamond inheritance; de-virtualize the
node hierarchy.
- ofloveandhate#7 — function-tree introspection, precedence-aware
printing, simplified differentiation, Hessian.
- ofloveandhate#8 — fix `float()`/`complex()` on multiprec scalars
(numpy-recursion segfault).
- ofloveandhate#25 — hash-consing the function tree: interning,
canonical variables & ordering, functional `Simplified()`.
- ofloveandhate#37 — rename function-tree node `Float` → `Complex`.
- ofloveandhate#38 — standardize scalar numeric type names
(real/complex × dbl/mp).
- ofloveandhate#59 — symbolic Jacobian in the core + `random_matrix`
+ nullvector criticality.
- ofloveandhate#64 — hash-consing of Systems: content identity,
`Seal()`, interning.

<img width="588" alt="critical_points curve — two interlocking circles"
src="https://github.com/user-attachments/assets/1bf07625-e959-4f7e-9f31-036b5c77195c"
/>

### Trackers, endgames & adaptive precision
- ofloveandhate#42 — fix inverted Cauchy `SecurityLevel` guard
(truncate diverging paths).
- ofloveandhate#43 — collapse predict/correct to a `StepMetadata` API
+ shared per-path probe.
- ofloveandhate#50 — Cauchy c/k probe determinism, start-system
sanity tests, per-path timing.
- ofloveandhate#55 — adaptive-numeric-type AMP endgames +
well-conditioned random coefficients.
- ofloveandhate#69 — fully initialize the default `AMPConfig`
(deterministic epsilon/Phi/Psi).
- ofloveandhate#70 — Cauchy pole-component operating-zone check —
never `Success` at a non-root.

<img width="1000" alt="AMP endgame session work"
src="https://github.com/user-attachments/assets/73d46ced-fccc-4775-82cb-2223ed67ec7c"
/>
<img width="928" alt="AMP endgame numeric-type flowchart"
src="https://github.com/user-attachments/assets/e612a7a2-38f1-4e9a-8a81-d9de88e44511"
/>

### Solve pipeline, start systems & randomization
- ofloveandhate#12 — solutions in user coordinates by default;
internal coords explicit; `homogenize_point` lift (ADR-0013).
- ofloveandhate#17 — unify `RandomMp` onto the shared `ThreadEngine`
so one seed controls all types.
- ofloveandhate#18 — block-composed System: fold the polynomial path
into blocks; MHom + linear algebra on blocks; expose m-hom start system.
- ofloveandhate#19 — randomize overdetermined systems via a
first-class `RandomizationBlock`.
- ofloveandhate#48 — de-template ZeroDim on the start system; add
linear-product TotalDegree.
- ofloveandhate#49 — split ZeroDim into `HomotopySolver` (engine) +
`ZeroDimSolver` (algorithm); auto square-up & filter.

### Parallelism
- ofloveandhate#21 — ZeroDim crossed-path resolution, speculative
full-path parallelism, reproducibility, pickling & MPI-portability.
- ofloveandhate#36 — MPI-less shared-memory threading:
parallel-by-default solves.
- ofloveandhate#56 — fix parallel (MPI) multihomogeneous solve
serialization; refresh scaling tables; restore `%` comment parsing.

### Witness sets, slices & NID enablement
- ofloveandhate#34 — expose `WitnessSet` + a block-backed `Slice`
(enable NID); classic slice/witness emission.
- ofloveandhate#51 — `function()` on slice-derived systems +
`system.slices()`.
- ofloveandhate#52 — show low-precision coefficients in system
`describe`.

### Python API & user experience
- ofloveandhate#13 — sympy bridge: exact two-way conversion + solve
round trip.
- ofloveandhate#28 — config UX: string-valued fields,
`owner.update(**fields)` router, `slice_` rename, `ZeroDimConfig`
de-template.
- ofloveandhate#29 — `FixedPrecisionConfig.precision`.
- ofloveandhate#30 — ZeroDim solution access: `all_solutions`,
`infinite_solutions`, `to_dataframe`.
- ofloveandhate#32 — `to_dataframe` keeps the whole solution in one
column.
- ofloveandhate#62 — Unicode + emoji identifiers, name validation,
collision-safe path variables.
- ofloveandhate#65 — flatten the public Python API (breaking,
pre-release); `System.add_functions`.
- ofloveandhate#66 — config-model cleanup: `max_num_refinements` +
single-source `final_tolerance`.

### CLI, distribution & versioning
- ofloveandhate#39 — emit Bertini-1.7-compatible solution files +
external-solver benchmark.
- ofloveandhate#46 — ship the `bertini2` CLI in the PyPI wheel (`pip
install bertini2` → `bertini2` on PATH).
- ofloveandhate#61 — single-source `VERSION` file + full version in
C++/CLI.
- (fork #4, folded into #3 — CLI exe, MPI parallelism, seeded RNG,
pytest suite, benchmarks.)

### Bugfixes (cross-cutting)
- ofloveandhate#14 — zero-dim coordinate-test tolerances respect the
values' working precision.
- ofloveandhate#26 — post-#25 grab-bag: precision fixes, Bertini-1
emitter, Concatenate fix, gamma conditioning, CI fallout.
- ofloveandhate#31 — friendly error when a solver is passed to
`user_homotopy`.
- ofloveandhate#33 — reject equations in the wrong block of a moving
homotopy.
- ofloveandhate#47 — add pandas to test deps; document the
numpy-reduction gotcha for mp dtypes.
- ofloveandhate#53 — fix `moving_slice` `eval_time_derivative`
doctest precision mismatch.

### Documentation, ADRs & CI
- ofloveandhate#9 — single CI run per PR push; secret-gated GitLab
sync for all forks.
- ofloveandhate#10 — remove expression syntax from a workflow comment
(it broke file validation).
- ofloveandhate#11 — ADR-0010/0011/0012 for the function_tree
overhaul; index + 0006 updates.
- ofloveandhate#15 — ccache for bertini2 compiles (linux+macos
wheels, unix C++ tests).
- ofloveandhate#16 — explicit template instantiation of the
endgame/ZeroDim universe (ADR-0014).
- ofloveandhate#20, #22, #24 — documentation improvements; make
tutorial code blocks executable & verified (doctest).
- ofloveandhate#23 — observer subsystem refactor + Python
path-visualization/callback observers.
- ofloveandhate#27 — ADRs (gamma, fixed-multiple precision) +
cyclic-5 cross-software regression test.
- ofloveandhate#35 — resolve macOS compiler warnings.
- ofloveandhate#54 — add a C++ doc-lint gate + fix rotted Doxygen
docs.
- ofloveandhate#63 — one-folder-per-tutorial, complete runnable
examples, release-refresh mechanism.
- ofloveandhate#67 — one-time deterministic regeneration of committed
plot images.

### Structured output directory: records, resume, provenance
- ofloveandhate#68 — every solve writes a durable, plain-text
**structured output directory** and consults it first, so a crashed
solve *finishes* on rerun (ensure-answered `solve()`: recorded paths
recall instead of recompute). The directory is self-describing and needs
no software to read.
- **Content identity for reproducibility**: versioned canonical
encodings + SHA-256 digests for Systems (`b2sysenc`), configs
(`b2cfgenc`), and seed-rooted randomness (`b2rand`) — equal objects
digest equally *forever*, across machines, compilers, and library
versions; append-only version registries make an un-bumped encoding
change fail loudly. `seed=42` names the exact homotopy — same gamma,
patch, and start points — everywhere.
- **Three truth stores**: `history/` (what was asked, when), `results/`
(one append-only JSONL file per run — every computed path with its
endpoint and metadata), `definitions/` (content-addressed inputs:
systems, configs, givens); `README`/`INDEX` are derived, rebuildable
views.
- **Reproducible sessions**: effective per-solve seeds make every
recorded run replayable standalone; "a solve leaves the session as it
found it" (default precision + RNG streams restored on exit); `recall`
events narrate re-asks; records are **on by default** for the CLI,
`bertini.solve`, and the bare `ZeroDimSolver`/`HomotopySolver` classes.
- **Casual surface**: `bertini.solve` / `save` / `load` / `annotate`;
`Solution` = points that remember their provenance; chained solves
thread provenance across runs, walkable back to canonical start labels
or user-supplied givens; pandas + networkx navigation of the records.
- **Cauchy endgame corrections** (found via the records' own
`function_residual` and a cyclic-6 audit against Bertini 1, refining
#70): the security check watches the extrapolated **endpoint** for
divergence to infinity (not the loop samples) and truncates **only in
the operating zone**; the pole-growth truncation is dropped in favor of
the operating-zone acceptance gate alone — cyclic-6 recovers its full
156 nonsingular solutions.
  - ADR-0042..0047; record-format spec at `docs/records/b2rec-1.md`.
…pped

A prerelease tag (.dev/a/b/rc) correctly skips check_timing_freshness,
but GitHub propagates a *skipped* `needs` as a skip under the default
`if: success()`. That silently skipped build_and_test and every
downstream publish/docs job -- v3.0.0.dev9 reported success yet
distributed nothing.

Gate build_and_test on the freshness job's *result* instead of relying
on implicit success(): run unless cancelled, require check_version to
succeed, and block only when the freshness check actually ran and
failed (skipped == satisfied). Real releases still gate on stale
tutorial timings; prereleases now build and publish as intended.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…267)

The `v3.0.0.dev9` publish run reported success but distributed nothing:
every real job (build/test, TestPyPI, docs) was **skipped**.

**Cause:** a prerelease tag correctly skips `check_timing_freshness`,
but GitHub propagates a *skipped* `needs` dependency as a skip under the
default `if: success()`. So `build_and_test` skipped, and everything
downstream with it.

**Fix:** gate `build_and_test` on the freshness job's *result* rather
than implicit `success()` — run unless cancelled, require
`check_version` to succeed, and block only when the freshness check
actually **failed** (skipped == satisfied). Real releases still gate on
stale tutorial timings; prereleases now build and publish to TestPyPI as
intended.

Regression introduced in #63 when the freshness gate was added
(dev7/dev8 predate it).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Two release-blocking bugs in publish.yml, both surfaced while getting
v3.0.0.dev9 out:

A) Nothing published even after build_and_test ran. The prior fix put a
   custom `if:` on build_and_test, but it is a reusable-workflow caller
   (`uses:`), and a conditional on such a job breaks the implicit
   `success()` gating of every downstream job -- so publish-to-testpypi
   (and pypi/docs) skipped despite build_and_test succeeding. The
   working v2.0.1.dev2 config had no conditional here. Restore that:
   drop build_and_test's `if:`, and move check_timing_freshness's gate
   from a job-level `if` to a *step-level* `if`. The job now always
   SUCCEEDS for prereleases (step skipped) instead of being skipped, so
   nothing propagates a skip. Real releases still fail the job when
   timings are stale, which correctly skips the build.

B) The is_prerelease classifier used `\.(dev|a|b|rc)[0-9]+$`, which
   requires a leading dot -- only `.devN` has one. `aN`/`bN`/`rcN`
   attach directly (3.0.0rc1), so they were misread as FINAL releases
   and would route to real PyPI + a signed GitHub Release instead of
   TestPyPI. Match the validation regex: `(\.dev[0-9]+|a[0-9]+|b[0-9]+|rc[0-9]+)$`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…skips)

records_test.py and the doc-example tests importorskip("matplotlib"); it was absent from
all three test-dep lists, so those tests silently SKIPPED. Add matplotlib to the Linux
CIBW_TEST_REQUIRES and the macOS/Windows test installs, add numpy to mac/Windows for
parity, and drop a duplicated `networkx` on the Windows line.

This closes the matplotlib skips on all platforms. The remaining skip source is mpi4py
(whole of test_mpi_zerodim.py + test_doc_example_scripts.py), which needs an MPI runtime
per platform -- handled next as part of the Linux complete-env image, then macOS/Windows.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Linux wheel matrix recompiles Boost.Python + eigenpy every run (most of the ~hour),
and actions/cache can't hold them (10 GB/repo ceiling, hit before). Bake them into a
GHCR image instead -- separate storage quota, layer-cached pull.

- docker/manylinux-deps/Dockerfile: manylinux_2_34 + Boost (per-CPython Boost.Python) +
  eigenpy at /opt/deps/<tag>, mirroring build_and_test.yml's proven BEFORE_BUILD recipe.
- .github/workflows/build-ci-image.yml: build+push to ghcr.io/bertiniteam/b2-manylinux-deps,
  tagged by toolchain key (boost/eigen/eigenpy+manylinux); runs on docker/ changes or dispatch.
- README: the ABI contract (tag encodes toolchain; main CI asserts a match -- ADR-0006),
  the one-time make-package-public step, and size/mac/Windows follow-ups.

Consumer wiring (CIBW_MANYLINUX_X86_64_IMAGE + assert-match) lands separately, once the
image is published and confirmed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Beyond Boost/eigenpy, bake the CPython-independent system layer so the Linux wheel run
needs (almost) nothing at runtime and reaches 0 skipped tests:

- OpenMPI (openmpi + openmpi-devel, on PATH + ldconfig) so `pip install mpi4py` builds in
  cibuildwheel's isolated test venv -- which un-skips test_mpi_zerodim.py and
  test_doc_example_scripts.py (currently skipped for lack of mpi4py).
- ccache and a pinned patchelf 0.17.2.1 (the stock patchelf corrupts the bundled CLI's
  ELF -> SIGSEGV), both moved off CIBW_BEFORE_ALL_LINUX.

README updated: complete-env scope + how main CI consumes it. Consumer wiring
(CIBW_MANYLINUX_X86_64_IMAGE + assert-match + gutted BEFORE_ALL/BEFORE_BUILD) lands once
the image is published and confirmed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The first image build failed at eigenpy's cmake: `python.cmake:602 Failed to detect
scipy`. eigenpy's configure requires scipy; the image installed only numpy per Python,
whereas the working CIBW_BEFORE_BUILD recipe installs `numpy scipy`. Add scipy.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…, furo-correct footer

The Sphinx front page (furo sidebar header) was showing the git commit SHA as the
"version" -- and the literal 'unknown' whenever git wasn't available (wheel installs,
shallow clones). Fix conf.py to derive version/release from the actual package:
importlib.metadata.version("bertini2") (defined for wheels, matches what autodoc
documents), falling back to the top-level VERSION file. The full string is kept, so a
prerelease suffix like .dev9 / rc1 shows in full.

The commit SHA is demoted to footer-only build provenance, omitted entirely when it
can't be resolved (no more 'unknown' or broken commit links in any build path).

The footer override was also dead: it targeted sphinx_rtd_theme's `extrafooter`
block, but the theme is now furo (rtd removed). Replace _templates/footer.html with
_templates/page.html overriding furo's own `{% block footer %}` (verified rendering in
a real furo build).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ocs.py)

Folds a freshly-built single-version site/ into a persistent store as /vX.Y.Z/, mirrors
the newest release to /stable/, and regenerates the root landing + versions.json. The
version directories present ARE the truth; versions.json and the root index are derived,
rebuildable views (mirrors the records doctrine). Dependency-free stdlib; rejects
prereleases (they get no durable directory). 12 unit tests (stable-moves, date
preservation, re-release idempotency, derived-view rebuild, semver ordering).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
build_docs now folds each real release's site into the docs-store branch via
assemble_versioned_docs.py (/vX.Y.Z/ + /stable/ + root landing), pushes it, and uploads
the whole multi-version store as the Pages artifact -- so history accrues and old versions
are never rebuilt. No Pages settings change: deployment stays on actions/deploy-pages, the
docs-store branch is just the durable byte store. Deploy is gated to public releases
(X.Y.Z); prerelease/dev builds build-only, never clobbering the versioned site. Grants the
docs job contents:write (here and in publish.yml's caller) to push the store branch.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…walk

sphinx-build -b html -W (warnings-as-errors) fails when autosummary can't import a
package module: bertini.sympy_bridge needs sympy and bertini.records needs networkx,
neither of which the docs job installed. Latent because docs only deploy on real
releases and the last one (2.0.2) predates these modules; surfaced by a deploy:false
validation build. Both are already declared in the CI test deps.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
macOS has no container, so the Linux image trick doesn't apply; the analog is prebuilt
Boost+eigenpy tarballs published as GitHub Release assets (off the 10 GB Actions-cache
ceiling) and downloaded by the wheel job instead of recompiling Boost per Python.

build-macos-deps.yml builds per CPython on macos-14, mirroring CIBW_BEFORE_BUILD_MACOS
exactly (same /tmp/deps-py prefix + hardcode-dll-paths so a downloaded bundle extracts
back in place for delocate), and uploads deps-macos14-arm64-<cpXY>-<toolchain-key>.tar.gz
to the `ci-deps` prerelease. Producer only; consumer wiring (download-or-build fallback in
CIBW_BEFORE_BUILD_MACOS + open-mpi/mpi4py for 0 skips) lands once the bundles are published.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- CIBW_BEFORE_BUILD_MACOS: download the prebuilt Boost+eigenpy bundle from the ci-deps
  release (built by build-macos-deps.yml) and extract to /tmp/deps-py instead of compiling
  Boost per Python. Build-from-source FALLBACK if the asset is missing (e.g. a version bump
  before the bundle refreshes) -- the asset name encodes the toolchain, so a bump 404s and
  rebuilds, never a silent ABI mismatch. The bundle keeps hardcode-dll-paths=/tmp/deps-py/lib
  so delocate is happy.
- macOS test job: brew install open-mpi + pip install mpi4py so the MPI test modules run
  (at 1 rank, guarded by Get_size()>1) instead of skipping -- matching the Windows job, whose
  env already ships mpi4py and runs them. Added `-rs` to surface any remaining skips.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Windows wheel build already wraps clang-cl with ccache (USE_CCACHE=ON default; ccache
ships in environment-win.yml) and builds correctly -- but its cache dir was ephemeral, so
every run recompiled bertini cold. Point CCACHE_DIR at the workspace and cache it (loose
key, like the Unix jobs), and print ccache --show-stats to measure the hit rate.

Low risk: ccache is already active on this job, so persisting the dir can only warm it,
never change correctness. (The -DUSE_CCACHE=OFF added in 87bb4c6 was a precaution on the
C++ *test* job only, and is unrelated to the /WHOLEARCHIVE static-lib fix in that commit.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The single-Dockerfile producer built Boost+eigenpy for all 5 CPythons serially -- ~an hour,
and the run we cancelled was still on cp310 after 85 min. Split it:

- build-python-deps.sh: builds one CPython's Boost+eigenpy into /opt/deps/<tag> and tars it.
- build-ci-image.yml: a `build-deps` MATRIX runs that script per tag in parallel (inside the
  manylinux container via `docker run`, dodging the node-in-container issue), uploading a
  deps-<tag>.tar.gz artifact each; `assemble-and-push` downloads them and builds a thin
  assembly image that just installs the system layer (openmpi/ccache/patchelf/eigen) and
  unpacks the tarballs -> ~2 min. Dropped the registry buildcache (assembly is cheap).
- Dockerfile: reworked from the inline per-Python loop to the COPY-and-unpack assembly.

Wall-clock is now one Python's build, not five in series. `deps/` is gitignored.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add .github/ci-deps-versions.env as the ONE place the C++ dep versions live, read by all
three CI workflows:

- build_and_test.yml: a small `set_versions` job reads the file and exposes boost/eigen/
  eigenpy as outputs; the two jobs that build from source (test_cpp_unix,
  build_macos_ubuntu_wheels) consume them as job-level env. Removed the workflow-level
  BOOST_VERSION/EIGENPY_VERSION and the hardcoded eigen-3.4.0 literals.
- build-ci-image.yml / build-macos-deps.yml: load the file into $GITHUB_ENV per job, and
  add the file to their push path filters.

So a version bump in one file (a) reruns build_and_test, (b) rebuilds the Linux image, and
(c) rebuilds the macOS tarballs -- no more editing versions in four places, and no more
forgetting to rebuild the prebuilt deps after a bump.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Run CI against just one platform without editing the workflow, e.g.:
  gh workflow run build_and_test.yml -f os=macos-14   # or linux / windows / all

set_matrix narrows the unix matrix on os=linux|macos (empty on os=windows); the three
standalone Windows jobs gate on the same input. Empty/`all` (push, PR, workflow_call) keep
the full behavior. Makes single-OS validation cheap -- e.g. re-running just macOS to confirm
the tarball consumer without the whole matrix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Point CIBW_MANYLINUX_X86_64_IMAGE at ghcr.io/bertiniteam/b2-manylinux-deps pinned to the
toolchain-key tag (from the single-sourced versions). BEFORE_ALL_LINUX drops from
install-eigen/download-eigenpy to a fast image sanity assert; BEFORE_BUILD_LINUX drops from
compiling Boost+eigenpy per Python to symlinking the active Python's prebuilt prefix
(/opt/deps/<tag> -> /opt/deps/current, since CIBW_ENVIRONMENT is static). Env points at
/opt/deps/current. Add mpi4py to the test-requires (image ships OpenMPI) so the MPI modules
run at 1 rank instead of skipping; -rs surfaces any remaining skips.

Net: Linux wheels stop recompiling Boost every run, and reach 0 skiptests. If the pinned
tag isn't built (version bump w/o image rebuild), the image pull fails loudly -- the ABI
assert. Stacked on the single-source-versions branch (needs EIGEN_VERSION for the tag).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
test_wheels_linux_macos excludes ubuntu, so an ubuntu-only os matrix (a non-develop PR
base, or -f os=linux / os=windows once the os input lands) expands to zero combinations
and FAILS the run with no job to point at -- exactly what turned the Linux-consumer PR CI
red despite every job passing. Guard it with contains(os, 'macos-14') so it cleanly skips
instead. Also unblocks the os-targeting input.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ofloveandhate
ofloveandhate merged commit 94d09d2 into main Jul 7, 2026
43 of 50 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant