feat: UI quality-of-life batch (#293–#304) + solution group projection — 3.1.0.dev1 - #305
Merged
Merged
Conversation
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…304 + group projection) Reusable primitives land in core (+ bindings), gated by C++ tests: - #294: Slice::RandomReal orthogonal path is now genuinely real (was falling through to the complex conjugate-orthonormal matrix); friendly random_real / random_complex / random_vector factories exposed in bertini.random. - #304: IsSamePoint / IsDistinct infinity-norm tolerance util (eigen_extensions), bound as is_distinct_up_to for real_mp / complex_mp / real_dbl / complex_dbl points. - #299: merge_multiplicities (default true) on the solver's SolutionsWhere and the finite category accessors -- dedup where the clustering lives. - #302: MetadataForPoint / CoincidentMetadataForPoint (point -> metadata); the return type depends on the call (single representative, or coincident=list), never on the point's multiplicity. - #297: System::functions() / copy_functions() accessors. - #300: node.eval accepts a point array or a dict, not just kwargs (NodeEvalRaw); still returns complex_mp natively. - Group projection (Cluster G): System::CoordinatesOfGroup / FIFOIndexOfGroup + sys.coordinates_of(point, group) -- project a user-coordinate point onto one variable group (projective groups returned as-is). C++ regression tests added: seeded factory realness, IsDistinct, slice realness, merge_multiplicities + metadata_for (mult-4 fixture), copy_functions, group projection. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…293,#295,#298,#300,#301) Pure-Python layer over the new C++ primitives, plus the presentation-only fixes: - #293: variables(['x','y','z']) list form; add_variable_group accepts loose variables / a list / a single Variable / a VariableGroup; Slice.random_* gives a clear error (and unwraps a one-group sequence) instead of an opaque converter TypeError. - #294 sugar: VariableGroup @ coeffs builds the single linear-combination node. - #296: sys.clone() method (over bertini.system.clone). - #295: bertini.sympy_bridge exposed lazily; nodes auto-sympify via the _sympy_ protocol, so sympy.sympify(node) / sympy.Matrix(array) / sympy.det(J) just work. - #298/#301: bertini.real/imag/abs/conj/round/sum/norm/is_real -- vectorized, mp-native helpers over scalar/list/array (the numpy<->eigenpy dtype boundary is unpatchable; documented in known_gotchas). Builtin-shadowing names kept out of *. - #299/#302/Cluster G surface: merge_multiplicities & metadata_for reach Python via the bindings; group= keyword on every solution getter and to_dataframe projects onto a variable group (via System.coordinates_of). Interface tests added; observer_temp_system_test updated for the new merge-multiplicities-by-default (a singular double root now counts once). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This was referenced Jul 8, 2026
Closed
Contributor
Author
Contributor
Author
|
there's some competing work in here, with #306 . will deal with in a minute |
ofloveandhate
added a commit
that referenced
this pull request
Jul 8, 2026
…docs page reframed Reconciles #305's interim numpy helpers with the binding-level ufunc coverage from this branch: - bertini.real/imag/abs/conj/sum/norm/is_real keep their API but now take the native C++ ufunc loops when handed an mp-dtype array (returning proper mp-dtype arrays that keep working with sort and reductions), falling back to element-wise work for lists and mixed input. bertini.round keeps its decimal-digit Decimal semantics. - The "Known gotchas" page is retired. Its replacement, docs/source/numpy.rst ("Multiprecision numbers and NumPy"), documents the interop as the feature it now is: what works (everything), the float64 boundary as deliberate digit protection with the tolerance- comparison carve-out, the complex component accessors, and the boundaries by design. The one remaining hazard -- ndarray .real/.imag lie on complex_mp arrays, a numpy hardwiring no binding can reach -- is a warning admonition, not a page of dragons. - All cross-references updated (module docstrings, test comments). 769 tests pass (both suites merged), 169 doctests, doclint clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ofloveandhate
added a commit
that referenced
this pull request
Jul 8, 2026
…bulary `from bertini.operators import *` now gives functions that work on EVERYTHING, dispatched per argument: a symbolic Variable/expression builds a function-tree node; multiprecision scalars, mp-dtype numpy arrays, lists, and plain python numbers take the numeric path through the native precision-preserving loops. No more remembering that arg lives in bertini.multiprec while imag is at the top level. - sin/cos/tan/asin/acos/atan/exp/log/sqrt: fully polymorphic (symbolic twin exists). - sinh/cosh/tanh/asinh/acosh/atanh and abs/arg/real/imag/conj/round/ sum/norm/is_real: numeric, with a clear TypeError on symbolic input. - E/Pi/I ride along. abs/round/sum shadow the builtins only inside this opt-in star-import (they fall back to builtin behavior on plain python input); `from bertini import *` still never shadows. - the top-level elementary functions are rebound to the polymorphic versions (a strict superset: bertini.sin(x) now also accepts numbers and arrays), and bertini.arg + the hyperbolics join the top level. - new _numpy_helpers.arg rides mp.arg on complex arrays (real arrays go through the exact real->complex cast). Also fixes a latent #305 bug the new tests caught: _numpy_helpers' scalar fallbacks called bare abs()/round(), which resolve to the module's own shadowing functions at module scope -> infinite recursion on plain python input. Now explicitly builtins. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ofloveandhate
added a commit
that referenced
this pull request
Jul 9, 2026
…e comparisons for mp dtypes (#306) ## What this PR delivers The numpy gotchas for the `real_mp` / `complex_mp` dtypes are gone. The scary "Known gotchas" page is retired; its replacement, `docs/source/numpy.rst` ("Multiprecision numbers and NumPy"), documents the interop as the feature it now is. Full decision record in **ADR-0051**; includes the post-#305 reconciliation. ### Full ufunc coverage `np.abs`, `np.conj`, `np.exp`/`log`/`log2`/`log10`/`expm1`/`log1p`, all trig/hyperbolic + inverses, `power`, `sign`, `reciprocal`, `minimum`/`maximum`/`fmin`/`fmax`, `floor`/`ceil`/`trunc`/`rint`, `isnan`/`isinf`/`isfinite`/`signbit`, `arctan2`, `hypot`, `copysign`, `mod`/`fmod`/`floor_divide` — previously all `TypeError: ufunc not supported`. Every loop reads through `value_or_zero` (ADR-0006 doctrine) and calls the same boost::multiprecision function the `multiprec` scalar functions bind, so `np.f(a)[i] == mp.f(a[i])` exactly. numpy-semantics corners done deliberately: `rint` is half-to-even via direct `mpfr_rint` (boost's is half-away; registered for complex too, component-wise, so `np.round` works on complex arrays), `mod` takes the divisor's sign, `minimum`/`maximum` propagate nan while `fmin`/`fmax` ignore it. ### Sorting & reductions — and the bug hiding under them `HardenCompare`/`HardenArgMinMax` fill the dtype slots for `real_mp`: `np.sort`, `np.argsort`, `np.searchsorted`, `np.median`, `np.argmax`/`argmin`, `np.max`/`min` (complex stays unordered by design). `np.sum`/`np.prod`/`np.mean` work (verified numpy 2.3.2 & 2.4.6, regression-tested) — but investigating them exposed the real hazard: **`getitem` returned `boost::ref` into the numpy buffer** (stock eigenpy behavior), so a scalar extracted from a temporary array dangled — `np.mean` returned a silently wrong `0`, and `s = np.sum(v)` kept past the statement SIGABRT'd in `str()`. This was the root of the ADR-0031 / #259 aliasing class. `getitem` now returns an **owned copy**, killing the class at the source. ### The float64 boundary: closed for values, open for tolerance comparisons `double→mp` casts stay **unsafe** — floats must not pollute polynomial-system construction. But tolerance *orderings* against a double are registered (`np.abs(a - b) < 1e-10`, both operand orders): a comparison yields a bool, no float value enters an mp computation, and the compare is exact (1e-22 is not lost against a 1e-30 tolerance). Mirrors the C++ solvers' double `ToleranceT` and the scalar `GreatLessVisitor<T,double>` precedent. Mixed equality and arithmetic stay blocked; `np.isclose`/`allclose` still raise (they *compute* with float64 tolerances). `arr.astype(float)` / `astype(complex)` are the explicit conscious truncations (`mp→complex128` cast registered unsafe; it never had been). Registration-order note: casts must register **before** the ufunc loops, or numpy permanently ignores them. ### Component access on complex arrays — "a crash is better than incorrect values" numpy's `ndarray.real`/`.imag` are C getsets gated on `PyArray_ISCOMPLEX`, a hardwired builtin-type check with no user-dtype hook (verified against numpy 2.4.x `getset.c`; neither the legacy API nor NEP 42 offers one) — on mp-complex arrays they return silently wrong values. Defended everywhere reachable: - **`Solution` overrides `.real`/`.imag`** at the subclass level — solve results are simply correct; - **`bertini._numpy_guard`**, installed at import, wraps `np.real`/`np.imag`/`np.angle` to raise a `TypeError` naming the right tool on plain mp-complex input (pass-through otherwise; `angle` raises for every mp-complex input since it branches on dtype); - sanctioned array accessors `bertini.real/imag/arg` and `mp.real/imag/arg` (en route: fixed the scalar `mp.imag`, which had returned the **real** part since it was written — copy-paste bug); - the raw attributes on a plain self-built ndarray are the one unreachable spelling — documented, pinned by test, and tracked upstream-ward in #307. ### One namespace for the whole vocabulary `bertini.operators` is now polymorphic: `from bertini.operators import *` gives `sin`/`cos`/`exp`/... that dispatch per argument (symbolic node for a Variable/expression; numeric through the native precision-preserving loops for mp scalars, numpy containers, lists, and plain numbers), plus `abs`/`arg`/`real`/`imag`/`conj`/`round`/`sum`/`norm`/`is_real` and `E`/`Pi`/`I`. The top-level elementary functions are rebound to the polymorphic versions (a strict superset), and `bertini.arg` + the hyperbolics join the top level. No more remembering that `arg` lives in `bertini.multiprec` while `imag` is top-level. ### #305 reconciliation Develop (the UI QoL batch) is merged in. Its interim `bertini.real/imag/abs/conj/sum/norm/is_real` helpers keep their API but now ride the native ufunc loops on mp-dtype arrays, falling back to element-wise work for lists. Two latent #305 bugs fixed: the helpers' scalar fallbacks called bare `abs()`/`round()`, which resolve to the module's own shadowing names → infinite recursion on plain python input; and the docs claim that the numpy boundary "cannot be patched in the bindings" (this PR is the patch). ## Tests / verification - `numpy_ufuncs_test.py` (~120) + `operators_test.py` (17): exact agreement with the scalar functions, semantics corners, precision preservation through every loop shape (including the mixed real/complex division path — loops re-tag via `at_precision_of`), unwritten-slot safety per ADR-0006, sorting incl. nan-wins argmax, reductions, tolerance comparisons, astype, guard behavior, polymorphic dispatch, and named regressions for the dangling-scalar, `mp.imag`, and shadowed-builtin bugs. - Full suite **793 passed**; **169 sphinx doctests** green (the numpy page is executable); doclint clean; end-to-end verified on a precision-40 solve with numpy ops on the returned solutions. ## Linked issues - Closes #298 — `.real` in numpy doesn't get the real part: Solution overrides + guarded numpy functions + `bertini.real/imag`/`mp.real/imag/arg` accessors; the scalar `mp.imag` bug found under it is fixed. The unreachable plain-attribute residue is #307. - Closes #301 — `np.abs(complex_mp)` / `np.round(complex_mp)` and the rest of the ufunc family now just work. - #307 (filed from this work) tracks the numpy-side shortcoming and the watch-items for future numpy versions. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
ofloveandhate
added a commit
that referenced
this pull request
Jul 9, 2026
Bump `VERSION` from `3.1.0.dev1` to **`3.1.0rc1`** — release candidate for 3.1.0. `VERSION` is the single source of truth (scikit-build-core reads it dynamically; `publish.yml`'s `check_version` asserts a release tag matches it). This one-line change is docs-adjacent: `VERSION` is in the workflow `paths-ignore`, so this PR runs **no CI** and needs no approvals — it can merge straight in. 3.1.0.dev1 → 3.1.0rc1 gathers the 3.1.0 line so far: the UI quality-of-life batch (#305, issues #293–#304 + solution group projection), the numpy-compat overhaul (#306), and `ZeroDimConfig.recall` (#308).
ofloveandhate
added a commit
that referenced
this pull request
Jul 9, 2026
The changelog had drifted badly: the last entry was **2.0.1**, while **2.0.2**, the entire **3.0.0** modernization, and the **3.1.0** line had all shipped — with their notes living only in commit messages, PRs, and GitHub Releases. This consolidates them back into `CHANGELOG.md`, newest-first, in the existing *Keep a Changelog* format. ### New entries - **[3.1.0] – 2026-07-09** — NumPy interop for the mp dtypes (#306), the Python UI quality-of-life batch (#293–#304, #305), `ZeroDimConfig.recall` (#308), prebuilt CI deps (ADR-0049, #282), and docs-store Pages (ADR-0050, #291, #292). - **[3.0.0] – 2026-07-07** — reworked from the hand-written v3.0.0 release notes (~70 PRs; full themed index in #238) into Added / Changed / Fixed sections. - **[2.0.2] – 2026-05-22** — the packaging/CI maintenance entry that was never recorded. Older 1.0.x / 2.0.1 entries and the commented template are untouched. ### Why now (load-bearing) `publish.yml`'s `github-release` job builds the release body from the **top** `CHANGELOG.md` block. That block was the stale **[2.0.1]** — so a final `v3.1.0` tag would have published 2.0.1's notes as the 3.1.0 release. With this merged, the extraction yields exactly the **[3.1.0]** block (verified locally against the workflow's extraction logic). Docs-only (`**/*.md` → `paths-ignore`), so this runs no CI. **Merge before tagging `v3.1.0`.**
ofloveandhate
added a commit
that referenced
this pull request
Jul 9, 2026
…ns (v3.1.0 docs deploy) (#313) The **v3.1.0** versioned-docs deploy failed. `build_docs.yml` runs `sphinx-build -b html -W` (warnings-as-errors), and three docstrings wrote absolute-value / norm notation with **bare pipes**: ``` ERROR: Undefined substitution referenced: "p_i - q_i" — bertini.multiprec.is_distinct_up_to ERROR: Undefined substitution referenced: "p_i - q_i" — bertini.is_distinct_up_to (top-level re-export) ERROR: Undefined substitution referenced: "imag" — bertini.operators.is_real ``` In reStructuredText `|word|` is a **substitution reference**, so Sphinx tried to resolve undefined substitutions `p_i - q_i` and `imag` and errored out. `is_distinct_up_to` (#305) and `is_real` (#306) both landed in the 3.1.0 line. ### Fix Reword the two source strings to `abs(...)` — `max_i abs(p_i - q_i)` and `abs(imag) < tol` — which read cleanly in Sphinx HTML *and* plain `help()` and carry no RST metacharacters. The other `|…|` docstrings in the tree are already safe (wrapped in ``inline literals`` or inside code blocks) and are left untouched. ### Not a package problem Docstrings only — the released wheels are unaffected. **3.1.0 is already on PyPI and the GitHub Release is published**; only the docs-site deploy failed. ### Verified Reproduced the exact failing command locally (`sphinx-build -b html -W --keep-going`): now **`build succeeded`**, zero substitution errors. ### Root-cause note (for the post-3.1.0 CI pass) The `-b html -W` build runs **only at docs-deploy time**, not in PR CI (PR CI runs `-b doctest`, which passed). So this whole class of RST/docstring error is invisible until release. Running the `-W` html build in PR CI would have caught it — worth adding alongside #312.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

UI quality-of-life batch (issues #293–#304) + solution group projection
These rough edges surfaced while implementing the numerical real cellular decomposition algorithm in
Python. The through-line goal: get from a polynomial system to the projection values of critical
points as
real_mp, straight through numpy, without a code maze.Per the "put reusable primitives in core" doctrine, anything with a C++ home landed in core +
bindings (behind C++ tests); only presentation/interop sugar stayed pure-Python.
C++ core + bindings
Slice::RandomReal's orthogonal path was silently using the complex conjugate-orthonormalmatrix, so a "real" slice came out complex; now genuinely real. New
random_real/random_complex/random_vectorfactories (continuous, seed-reproducible — the generic-direction tool, unlike thequantized orthonormal
random_matrix).is_distinct_up_to#304 — reusableIsDistinct/IsSamePointinfinity-norm util →bertini.is_distinct_up_to(accepts
real_mp/complex_mp/real_dbl/complex_dblpoints).merge_multiplicities(default True) on the solver's ownSolutionsWhere+ finiteaccessors — dedup where the multiplicity clustering lives.
metadata_for(point): returns the cluster representative by default (one record,carrying
.multiplicity), orcoincident=Truefor the list of every copy — return type neverdepends on the point's multiplicity.
System.functions()/copy_functions().node.evalaccepts a point array or a{Variable-or-name: value}dict (not just kwargs),still returning
complex_mpnatively.System.coordinates_of(point, group)+ agroup=keyword on every solutiongetter and
to_dataframe: project solutions onto one variable group (projective groups returnedas-is). Reusable for the eventual C++ decomposition.
Python layer
variables(['x','y','z']);add_variable_groupaccepts loose variables / a list / a singleVariable / a VariableGroup;
Slice.random_*gives a clear message (and unwraps a one-group sequence)instead of an opaque converter error.
sys.clone()method.bertini.sympy_bridgeexposed lazily + nodes auto-sympify (_sympy_), sosympy.sympify(node)/sympy.Matrix(array)/sympy.det(J)just work.VariableGroup @ coeffs→ the linear-combination node.#298 / #301 (numpy interop) — note the overlap
Adds mp-native elementwise helpers
bertini.real/imag/abs/conj/round/sum/norm/is_real(thenumpy↔eigenpy dtype boundary is unpatchable for identity-seeded reductions; documented in
known_gotchas). These likely overlap with the native ufunc coverage onfeature/numpy_compat—left for the maintainer to reconcile (the helpers still stand alone for
is_real/ dedup / norm).Deferred
Verification
test_classes+test_nag_algorithmsgreen (new regression tests for every C++ change).test was updated).
curve_decomposition_in_pythonrepo,original left untouched) runs end-to-end — projection values arrive as
real_mp, plus a 3D plot ofthe real critical points.
Heads-up for review
first commit if you'd rather bump separately.
points survive) but the mhom homotopy fails every path for this bilinear structure. The notebook uses
the working affine+patch formulation and documents the projective attempt as an open question for the
start-system / endgame work.
int(real_mp)segfaults (pre-existingreal_mpbinding bug;float/strfine) — hitincidentally, out of scope here, worth a follow-up (
real_mp.__int__).doclintnot run locally (no doxygen in this env); every new public C++ entity carries a matchingDoxygen comment, so the CI gate should pass.
Closes #293, #294, #295, #296, #297, #299, #300, #302, #304.
(#298 and #301 addressed via helpers — see the overlap note above before closing.)
🤖 Generated with Claude Code