Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,17 @@ the identity of the algorithm that answered it (#420).

### Changed

- **An accuracy estimate names its coordinates** (ADR-0069). A solution's metadata has two
accuracy estimates, both the distance between the endgame's last two approximations of the
root. `accuracy_estimate` is renamed **`accuracy_estimate_internal_coords`**: it is in the
solver's internal coordinates (homogenized, on the patch), it is what `final_tolerance` is
compared with, and it reads as a number of correct digits.
`accuracy_estimate_user_coords` keeps its name: it is the absolute error in the units of
your variables, so a solution of order 1000 with six correct digits has about `1e-3`
there. The plain name read as the second and held the first. It is **retired**, not
reassigned: reading `accuracy_estimate` raises an error that names both, so no code goes on
running with a number that changed meaning. The dataframe column and the records key are
renamed with it; records written under the earlier key still load.
- **Combining systems compares the variables** (ADR-0068). `concatenate`, the homotopy
builders (`straight_line_homotopy`, and `MakeHomotopy` / `MakeMovingHomotopy` beneath it) and
`+` on systems now all make one check: the two systems must be over the same variables, in
Expand Down
4 changes: 2 additions & 2 deletions core/include/bertini2/nag_algorithms/output.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ struct Classic <HomotopySolver<A,B,C>>
out << '\n' << data.max_precision_used << '\n';

generators::Classic::generate(boost::spirit::ostream_iterator(out), data.time_of_first_prec_increase);
out << '\n' << data.accuracy_estimate << '\n'
out << '\n' << data.accuracy_estimate_internal_coords << '\n'
<< data.accuracy_estimate_user_coords << '\n'
<< data.cycle_num << '\n'
<< data.multiplicity << '\n'
Expand All @@ -296,7 +296,7 @@ struct Classic <HomotopySolver<A,B,C>>
<< data.condition_number << '\n'
<< data.newton_residual << '\n';
generators::Classic::generate(boost::spirit::ostream_iterator(out), data.final_time_used);
out << '\n' << data.accuracy_estimate << '\n';
out << '\n' << data.accuracy_estimate_internal_coords << '\n';
generators::Classic::generate(boost::spirit::ostream_iterator(out), data.time_of_first_prec_increase);
out << '\n' << data.cycle_num << '\n'
<< data.endgame_success_code << '\n'
Expand Down
26 changes: 16 additions & 10 deletions core/include/bertini2/nag_algorithms/zero_dim_solve.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -213,14 +213,20 @@ struct SolutionMetaData
Vec<NumErrorT> singular_values;
NumErrorT newton_residual; ///< The latest Newton residual.
ComplexT final_time_used; ///< The last time value the path reached: the endgame's latest time for a successful path, the tracker's time when it stopped for one that did not succeed in either stage.
NumErrorT accuracy_estimate; ///< Accuracy estimate between extrapolations.
NumErrorT accuracy_estimate_user_coords; ///< Accuracy estimate between extrapolations, in natural coordinates.
// Two accuracy estimates, and each says in its NAME which coordinates it is in (ADR-0069).
// Both are the distance between the endgame's last two approximations of the root. The
// internal one is what the endgame's convergence test compares against final_tolerance;
// it reads as a number of digits. The user one is the absolute error in the units of
// the caller's own variables. There is deliberately no field named plain
// `accuracy_estimate`: a reader takes that to be in their own coordinates, and it was not.
NumErrorT accuracy_estimate_internal_coords; ///< Distance between the endgame's last two approximations, in the solver's internal coordinates (homogenized, on the patch). The quantity the convergence test compares with final_tolerance; it does not grow with the scale of the solution.
NumErrorT accuracy_estimate_user_coords; ///< Distance between the endgame's last two approximations after each is dehomogenized: the absolute error in the units of the user's variables.
unsigned cycle_num; ///< Cycle number used in extrapolations.
// Honest precision/accuracy of the computed solution, as DIGIT COUNTS. precision_digits is the
// working precision the endgame actually finished in (DoublePrecision() ~16 for a path that stayed in
// the adaptive-numeric-type endgame's hardware-double fast lane; the mpfr precision for one that
// escalated). accuracy_digits is how many of those digits are trustworthy, from the convergence
// agreement: floor(-log10(accuracy_estimate)), clamped to [0, precision_digits]. Read together:
// agreement: floor(-log10(accuracy_estimate_internal_coords)), clamped to [0, precision_digits]. Read together:
// "computed in precision_digits digits, good to accuracy_digits of them."
unsigned precision_digits = 0; ///< Working precision (digits) the endgame finished this solution in.
unsigned accuracy_digits = 0; ///< Trustworthy digit count, from the convergence agreement.
Expand Down Expand Up @@ -260,7 +266,7 @@ struct SolutionMetaData
&& this->condition_number == other.condition_number
&& this->newton_residual == other.newton_residual
&& this->final_time_used == other.final_time_used
&& this->accuracy_estimate == other.accuracy_estimate
&& this->accuracy_estimate_internal_coords == other.accuracy_estimate_internal_coords
&& this->accuracy_estimate_user_coords == other.accuracy_estimate_user_coords
&& this->cycle_num == other.cycle_num
&& this->precision_digits == other.precision_digits
Expand Down Expand Up @@ -310,7 +316,7 @@ std::ostream& operator<<(std::ostream & out, const SolutionMetaData<NumT> & meta
out << "singular_values = " << meta.singular_values.transpose() << std::endl;
out << "newton_residual = " << meta.newton_residual << std::endl;
out << "final_time_used = " << meta.final_time_used << std::endl;
out << "accuracy_estimate = " << meta.accuracy_estimate << std::endl;
out << "accuracy_estimate_internal_coords = " << meta.accuracy_estimate_internal_coords << std::endl;
out << "accuracy_estimate_user_coords = " << meta.accuracy_estimate_user_coords << std::endl;
out << "precision_digits = " << meta.precision_digits << std::endl;
out << "accuracy_digits = " << meta.accuracy_digits << std::endl;
Expand Down Expand Up @@ -2188,7 +2194,7 @@ run the endgame, classify the endpoints, report. See the forward-declare doc ab
smd.condition_number = SpectralConditionNumberOf(smd.singular_values);
smd.newton_residual = ctx.tracker.LatestNormOfStep();

smd.accuracy_estimate = ctx.endgame.ApproximateError();
smd.accuracy_estimate_internal_coords = ctx.endgame.ApproximateError();
smd.accuracy_estimate_user_coords =
static_cast<NumErrorT>( (ctx.target_sys.DehomogenizePoint(solutions_post_endgame_[soln_ind]) -
ctx.target_sys.DehomogenizePoint(ctx.endgame.template PreviousApproximation<BaseComplexT>())).template lpNorm<Eigen::Infinity>() );
Expand All @@ -2198,11 +2204,11 @@ run the endgame, classify the endpoints, report. See the forward-declare doc ab
// precision the endgame actually finished in -- which is exactly the precision of the
// stored solution point (DoublePrecision() for a fast-lane path, the mpfr precision for an
// escalated one). accuracy_digits is how many of those digits the convergence agreement
// supports: floor(-log10(accuracy_estimate)), clamped to [0, precision_digits].
// supports: floor(-log10(accuracy_estimate_internal_coords)), clamped to [0, precision_digits].
smd.precision_digits = static_cast<unsigned>(Precision(solutions_post_endgame_[soln_ind]));
{
using std::log10; using std::floor; using std::min; using std::max;
double err = static_cast<double>(smd.accuracy_estimate);
double err = static_cast<double>(smd.accuracy_estimate_internal_coords);
unsigned acc = (err > 0.0)
? static_cast<unsigned>(max(0.0, floor(-log10(err))))
: smd.precision_digits;
Expand Down Expand Up @@ -2442,7 +2448,7 @@ run the endgame, classify the endpoints, report. See the forward-declare doc ab
r.singular_values = smd.singular_values;
r.newton_residual = smd.newton_residual;
r.final_time_used = smd.final_time_used;
r.accuracy_estimate = smd.accuracy_estimate;
r.accuracy_estimate_internal_coords = smd.accuracy_estimate_internal_coords;
r.accuracy_estimate_user_coords = smd.accuracy_estimate_user_coords;
r.cycle_num = smd.cycle_num;
r.precision_digits = smd.precision_digits;
Expand Down Expand Up @@ -2484,7 +2490,7 @@ run the endgame, classify the endpoints, report. See the forward-declare doc ab
smd.singular_values = r.singular_values;
smd.newton_residual = r.newton_residual;
smd.final_time_used = r.final_time_used;
smd.accuracy_estimate = r.accuracy_estimate;
smd.accuracy_estimate_internal_coords = r.accuracy_estimate_internal_coords;
smd.accuracy_estimate_user_coords = r.accuracy_estimate_user_coords;
smd.cycle_num = r.cycle_num;
smd.precision_digits = r.precision_digits;
Expand Down
4 changes: 2 additions & 2 deletions core/include/bertini2/parallel/path_result.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ struct FullPathResult
Vec<double> singular_values; ///< Singular values of the target Jacobian at the final solution, largest first (the spectrum behind condition_number).
double newton_residual = 0; ///< Final Newton residual.
ComplexT final_time_used; ///< The time value the endgame finished at.
double accuracy_estimate = 0; ///< Estimated accuracy of the final solution.
double accuracy_estimate_internal_coords = 0; ///< Estimated accuracy in the solver's internal coordinates (homogenized, on the patch).
double accuracy_estimate_user_coords = 0; ///< Estimated accuracy in the user's coordinates.
unsigned cycle_num = 0; ///< The estimated cycle number at the endpoint.
unsigned precision_digits = 0; ///< Digits the endgame finished in (= solution point's precision).
Expand Down Expand Up @@ -159,7 +159,7 @@ struct FullPathResult
ar & singular_values;
ar & newton_residual;
ar & final_time_used;
ar & accuracy_estimate;
ar & accuracy_estimate_internal_coords;
ar & accuracy_estimate_user_coords;
ar & cycle_num;
ar & precision_digits;
Expand Down
10 changes: 8 additions & 2 deletions core/include/bertini2/records/solver_recording.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ boost::json::object EncodeFullPathResult(parallel::FullPathResult<ComplexT> cons
}
out["newton_residual"] = ExactDoubleText(r.newton_residual);
out["final_time_used"] = EncodeComplexScalar(r.final_time_used);
out["accuracy_estimate"] = ExactDoubleText(r.accuracy_estimate);
out["accuracy_estimate_internal_coords"] = ExactDoubleText(r.accuracy_estimate_internal_coords);
out["accuracy_estimate_user_coords"] = ExactDoubleText(r.accuracy_estimate_user_coords);
out["cycle_num"] = static_cast<std::int64_t>(r.cycle_num);
out["precision_digits"] = static_cast<std::int64_t>(r.precision_digits);
Expand Down Expand Up @@ -267,7 +267,13 @@ parallel::FullPathResult<ComplexT> DecodeFullPathResult(boost::json::object cons
}
r.newton_residual = DoubleFromText(std::string(rec.at("newton_residual").as_string()));
r.final_time_used = DecodeComplexScalar<ComplexT>(rec.at("final_time_used").as_array());
r.accuracy_estimate = DoubleFromText(std::string(rec.at("accuracy_estimate").as_string()));
// the internal estimate was recorded under the bare key `accuracy_estimate` until the
// field was named for its coordinates (ADR-0069); a record written before that reads the
// same, under the key it was written with
r.accuracy_estimate_internal_coords = DoubleFromText(std::string(
(rec.if_contains("accuracy_estimate_internal_coords")
? rec.at("accuracy_estimate_internal_coords")
: rec.at("accuracy_estimate")).as_string()));
r.accuracy_estimate_user_coords =
DoubleFromText(std::string(rec.at("accuracy_estimate_user_coords").as_string()));
r.cycle_num = static_cast<unsigned>(rec.at("cycle_num").as_int64());
Expand Down
29 changes: 29 additions & 0 deletions core/test/nag_algorithms/zero_dim_records.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -937,4 +937,33 @@ BOOST_AUTO_TEST_CASE(the_crossing_verdict_round_trips_through_a_path_record)
BOOST_CHECK(!records::DecodeFullPathResult<complex_dbl>(clean_encoded, 7).crossing_unresolved);
}

// ADR-0069: each accuracy estimate is recorded under a key that names its coordinates, and a
// record written while the internal one was still called plain `accuracy_estimate` reads the same.
BOOST_AUTO_TEST_CASE(the_accuracy_estimates_are_recorded_under_names_that_say_their_coordinates)
{
parallel::FullPathResult<complex_dbl> result;
result.path_index = 3;
result.solution = Vec<complex_dbl>::Zero(1);
result.boundary_point = Vec<complex_dbl>::Zero(1);
result.accuracy_estimate_internal_coords = 3.5e-12;
result.accuracy_estimate_user_coords = 7.25e-8;

auto const encoded = records::EncodeFullPathResult(result);
BOOST_REQUIRE(encoded.if_contains("accuracy_estimate_internal_coords"));
BOOST_REQUIRE(encoded.if_contains("accuracy_estimate_user_coords"));
BOOST_CHECK(!encoded.if_contains("accuracy_estimate")); // the bare key is not written

auto const decoded = records::DecodeFullPathResult<complex_dbl>(encoded, 3);
BOOST_CHECK_EQUAL(decoded.accuracy_estimate_internal_coords, 3.5e-12);
BOOST_CHECK_EQUAL(decoded.accuracy_estimate_user_coords, 7.25e-8);

// an earlier record: the same value, under the key it was written with
auto earlier = encoded;
earlier["accuracy_estimate"] = earlier.at("accuracy_estimate_internal_coords");
earlier.erase("accuracy_estimate_internal_coords");
auto const from_earlier = records::DecodeFullPathResult<complex_dbl>(earlier, 3);
BOOST_CHECK_EQUAL(from_earlier.accuracy_estimate_internal_coords, 3.5e-12);
BOOST_CHECK_EQUAL(from_earlier.accuracy_estimate_user_coords, 7.25e-8);
}

BOOST_AUTO_TEST_SUITE_END()
2 changes: 1 addition & 1 deletion docs/adr/0009-amp-endgame-internal-double-state.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ tracker, applied one level up.

**Does not address**
- **Accuracy** (how many digits are *correct*) remains orthogonal: it is carried separately in
`SolutionMetaData` (`accuracy_estimate`, `condition_number`, `newton_residual`; computed in
`SolutionMetaData` (`accuracy_estimate_internal_coords`, `condition_number`, `newton_residual`; computed in
cauchy.hpp). This internal-double change neither solves nor worsens it. A unified
precision-and-accuracy-aware solution type is a separate, larger decision.

Expand Down
62 changes: 62 additions & 0 deletions docs/adr/0069-an-accuracy-estimate-names-its-coordinates.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# ADR-0069: An accuracy estimate names its coordinates, and the bare name is retired

**Status:** Accepted
**Date:** 2026-09-29

## Context

A solution's metadata carried two accuracy estimates. Both are the distance, in the infinity
norm, between the endgame's last two approximations of the root.

| field | coordinates |
|---|---|
| `accuracy_estimate` | the solver's internal ones: homogenized, on the patch |
| `accuracy_estimate_user_coords` | the user's: dehomogenized |

The plain name went to the quantity fewer people want. A user reads `accuracy_estimate` as
the accuracy of their solution in their own coordinates, because those are the only
coordinates they think in. It was not that. The internal estimate is what the endgame
compares with `final_tolerance`, so it behaves like a number of correct digits; the absolute
error in the user's variables is that, times roughly the scale of the solution. A solution of
order 1000 with six correct digits has an absolute error near `1e-3`. The two differ by the
scale, and nothing in the name said which was which.

The cellular decomposition port found the consequence. It reads the internal estimate as a
relative error, and once multiplied a tolerance by the scale a second time.

## Decision

Both estimates are named for their coordinates, and there is no field named plain
`accuracy_estimate`.

- `accuracy_estimate_internal_coords`: the former `accuracy_estimate`.
- `accuracy_estimate_user_coords`: unchanged.
- `accuracy_estimate` is **retired**. In Python, reading it raises a `RuntimeError` that names
both successors. It is not an `AttributeError`, because `getattr(md, 'accuracy_estimate',
None)` swallows that and the caller goes on with `None`.

The bare name is retired, not given to the user-coordinates estimate. The same name
returning a different number is the failure a rename exists to prevent: every reader would
keep running, on a value off by the scale of its solution, with no error anywhere.

In the records each estimate is written under its new key. The reader accepts the earlier
key `accuracy_estimate` for the internal one, so a record written before the rename loads.

## Consequences

- Code that read `accuracy_estimate` fails loudly and is told what to read.
- **Do not re-add `accuracy_estimate` as an alias** for either estimate in the 4.x line. If the
plain name is ever to mean the user's coordinates, that is for a later major release, once
nothing is left reading it with the old meaning.
- **Do not change the retirement to an `AttributeError`**, however conventional that is for a
missing attribute.
- A new quantity that exists in both coordinate systems is named for the one it is in.
- `accuracy_digits` is derived from the internal estimate and keeps its name: a count of
digits has no coordinates.
- The estimates are distances between successive approximations. Whether they bound the true
error is a separate question and is not settled here: the triangle inequality makes the
estimate the error of the PREVIOUS approximation, give or take the error of the last, so
it bounds the true error from above only when the last round at least halved the error.
- Pinned by `python/test/zero_dim/accuracy_estimate_names_test.py` and by
`the_accuracy_estimates_are_recorded_under_names_that_say_their_coordinates` in
`core/test/nag_algorithms/zero_dim_records.cpp`.
Loading
Loading