diff --git a/CHANGELOG.md b/CHANGELOG.md index 91bf501a9..ad173cdd0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/core/include/bertini2/nag_algorithms/output.hpp b/core/include/bertini2/nag_algorithms/output.hpp index 9b17e0ce1..1044589d0 100644 --- a/core/include/bertini2/nag_algorithms/output.hpp +++ b/core/include/bertini2/nag_algorithms/output.hpp @@ -272,7 +272,7 @@ struct Classic > 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' @@ -296,7 +296,7 @@ struct Classic > << 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' diff --git a/core/include/bertini2/nag_algorithms/zero_dim_solve.hpp b/core/include/bertini2/nag_algorithms/zero_dim_solve.hpp index 268723a37..07a889f87 100644 --- a/core/include/bertini2/nag_algorithms/zero_dim_solve.hpp +++ b/core/include/bertini2/nag_algorithms/zero_dim_solve.hpp @@ -213,14 +213,20 @@ struct SolutionMetaData Vec 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. @@ -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 @@ -310,7 +316,7 @@ std::ostream& operator<<(std::ostream & out, const SolutionMetaData & 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; @@ -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( (ctx.target_sys.DehomogenizePoint(solutions_post_endgame_[soln_ind]) - ctx.target_sys.DehomogenizePoint(ctx.endgame.template PreviousApproximation())).template lpNorm() ); @@ -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(Precision(solutions_post_endgame_[soln_ind])); { using std::log10; using std::floor; using std::min; using std::max; - double err = static_cast(smd.accuracy_estimate); + double err = static_cast(smd.accuracy_estimate_internal_coords); unsigned acc = (err > 0.0) ? static_cast(max(0.0, floor(-log10(err)))) : smd.precision_digits; @@ -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; @@ -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; diff --git a/core/include/bertini2/parallel/path_result.hpp b/core/include/bertini2/parallel/path_result.hpp index 6607bd197..a78c0c58f 100644 --- a/core/include/bertini2/parallel/path_result.hpp +++ b/core/include/bertini2/parallel/path_result.hpp @@ -118,7 +118,7 @@ struct FullPathResult Vec 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). @@ -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; diff --git a/core/include/bertini2/records/solver_recording.hpp b/core/include/bertini2/records/solver_recording.hpp index 46be236e7..ed0eb0038 100644 --- a/core/include/bertini2/records/solver_recording.hpp +++ b/core/include/bertini2/records/solver_recording.hpp @@ -210,7 +210,7 @@ boost::json::object EncodeFullPathResult(parallel::FullPathResult 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(r.cycle_num); out["precision_digits"] = static_cast(r.precision_digits); @@ -267,7 +267,13 @@ parallel::FullPathResult DecodeFullPathResult(boost::json::object cons } r.newton_residual = DoubleFromText(std::string(rec.at("newton_residual").as_string())); r.final_time_used = DecodeComplexScalar(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(rec.at("cycle_num").as_int64()); diff --git a/core/test/nag_algorithms/zero_dim_records.cpp b/core/test/nag_algorithms/zero_dim_records.cpp index 177123a7d..a0e44d8f6 100644 --- a/core/test/nag_algorithms/zero_dim_records.cpp +++ b/core/test/nag_algorithms/zero_dim_records.cpp @@ -937,4 +937,33 @@ BOOST_AUTO_TEST_CASE(the_crossing_verdict_round_trips_through_a_path_record) BOOST_CHECK(!records::DecodeFullPathResult(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 result; + result.path_index = 3; + result.solution = Vec::Zero(1); + result.boundary_point = Vec::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(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(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() diff --git a/docs/adr/0009-amp-endgame-internal-double-state.md b/docs/adr/0009-amp-endgame-internal-double-state.md index 0ac3fcb4c..dff99eaa2 100644 --- a/docs/adr/0009-amp-endgame-internal-double-state.md +++ b/docs/adr/0009-amp-endgame-internal-double-state.md @@ -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. diff --git a/docs/adr/0069-an-accuracy-estimate-names-its-coordinates.md b/docs/adr/0069-an-accuracy-estimate-names-its-coordinates.md new file mode 100644 index 000000000..577fc8961 --- /dev/null +++ b/docs/adr/0069-an-accuracy-estimate-names-its-coordinates.md @@ -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`. diff --git a/docs/adr/README.md b/docs/adr/README.md index 3f8730d19..8d5cd598b 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -85,3 +85,4 @@ Each ADR follows the template: | [0066](0066-a-records-session-is-the-process.md) | A records session is the process: `OutputDirectory::Shared()` holds each instance for the life of the process (replacing it only for a deleted directory or a forked child), so a parameter sweep writes one history file instead of claiming one per solve and exhausting the 25 names a second allows; an idle instance holds no open files, and results files are capped at 16 open | Records | | [0067](0067-the-endgames-differ-in-root-estimation-only.md) | The endgames differ in root estimation ONLY: the security ceiling (two consecutive endpoint approximations above `max_norm`, at security level 0) is asked BEFORE acceptance in the Cauchy endgame as it is in the power series one, and an approximation above the ceiling is never accepted -- Cauchy had returned `Success` for finite roots above the ceiling that power series truncated; Cauchy's count still runs over operating-zone rounds only, since an out-of-zone mean is not an endpoint approximation (that condition keeps cyclic-6 at 156); don't move acceptance back above security, don't drop the `!above_ceiling` clause | Core / endgames | | [0068](0068-combining-systems-compares-the-variables.md) | Combining systems compares THE VARIABLES, and a loaded system holds the live ones: `System` and `Slice` re-intern as the last step of loading from an archive (deep copies, pickles and the MPI broadcast came back over fresh variables of the same names and could not be recombined with their original); `Concatenate`, `MakeHomotopy`, `MakeMovingHomotopy` and `+=` make ONE check, variable by variable, group by group, in order -- the builders had counted, and blended `(x, y)` with `(y, x)` by position; a system that declares no variables takes the other's; in Python a node compares by identity, not by wrapper; don't go back to counting, don't remove the load hooks, don't build a node outside `Make` | Core / system, function tree; Python bindings | +| [0069](0069-an-accuracy-estimate-names-its-coordinates.md) | An accuracy estimate NAMES ITS COORDINATES: `accuracy_estimate_internal_coords` (what `final_tolerance` is compared with; reads as digits) and `accuracy_estimate_user_coords` (the absolute error in the user's units); the bare `accuracy_estimate` held the internal one and read as the user's, so it is RETIRED -- reading it raises a `RuntimeError` naming both, not an `AttributeError`, which `getattr` with a default would swallow; retired and not handed to the other estimate, because the same name returning a different number breaks every reader silently; the records reader accepts the earlier key; don't re-add the bare name as an alias | Core / nag_algorithms, records; Python bindings | diff --git a/python/bertini/nag_algorithm/__init__.py b/python/bertini/nag_algorithm/__init__.py index c9e4c7161..84a419c3a 100644 --- a/python/bertini/nag_algorithm/__init__.py +++ b/python/bertini/nag_algorithm/__init__.py @@ -243,7 +243,7 @@ def _witness_repr(self): 'is_finite', 'is_real', 'is_singular', 'is_nonsolution', 'crossing_unresolved', 'multiplicity', 'multiplicity_representative', 'condition_number', 'function_residual', 'newton_residual', - 'accuracy_estimate', 'accuracy_estimate_user_coords', + 'accuracy_estimate_internal_coords', 'accuracy_estimate_user_coords', 'precision_digits', 'accuracy_digits', 'cycle_num', 'endgame_success_code', 'pre_endgame_success_code', 'final_time_used', 'precision_changed', 'max_precision_used', 'time_of_first_prec_increase', diff --git a/python/test/zero_dim/accuracy_estimate_names_test.py b/python/test/zero_dim/accuracy_estimate_names_test.py new file mode 100644 index 000000000..32c3f9d60 --- /dev/null +++ b/python/test/zero_dim/accuracy_estimate_names_test.py @@ -0,0 +1,103 @@ +"""Each accuracy estimate says in its name which coordinates it is in (ADR-0069). + +``accuracy_estimate_internal_coords`` is the distance between the endgame's last two +approximations in the solver's internal coordinates (homogenized, on the patch): what +``final_tolerance`` is compared with, and a number that does not grow with the solution. +``accuracy_estimate_user_coords`` is the same distance after dehomogenizing: the absolute error in +the units of the caller's variables. The bare name ``accuracy_estimate`` held the internal one and +read as the user's; it is retired, and says so when read. +""" + +import pytest + +import bertini as pb +from bertini import ZeroDimSolver + +ENDGAMES = ['powerseries', 'cauchy'] + + +def _solve(scale, endgame='powerseries', seed=1): + """``x^2 = scale^2, y = 2x``: roots at ``(+-scale, +-2 scale)``.""" + pb.random.set_random_seed(seed) + x, y = pb.variables(['x', 'y']) + system = pb.System() + system.add_variable_group([x, y]) + system.add_functions([x ** 2 - pb.coefficient(repr(float(scale * scale))), + y - pb.coefficient(2) * x]) + solver = ZeroDimSolver(system, endgame=endgame) + solver.update(max_norm=1e8) # the roots are finite; keep the security ceiling above them + solver.solve(show_progress=False) + metadata = list(solver.solution_metadata()) + assert [str(m.endgame_success_code) for m in metadata] == ['Success', 'Success'] + return solver, metadata + + +def test_both_estimates_are_there_under_their_names(): + _, metadata = _solve(3.0) + for m in metadata: + assert 0 < float(m.accuracy_estimate_internal_coords) < 1e-8 + assert 0 < float(m.accuracy_estimate_user_coords) < 1e-8 + + +def test_the_bare_name_is_retired_and_says_what_to_read(): + _, metadata = _solve(3.0) + with pytest.raises(RuntimeError) as refusal: + metadata[0].accuracy_estimate + said = str(refusal.value) + assert 'accuracy_estimate_internal_coords' in said + assert 'accuracy_estimate_user_coords' in said + + +def test_the_retirement_is_not_swallowed_by_a_default(): + # getattr with a default swallows an AttributeError and carries on with the default, which + # is how a reader of the old name would have gone on computing with None + _, metadata = _solve(3.0) + with pytest.raises(RuntimeError): + getattr(metadata[0], 'accuracy_estimate', None) + + +def test_the_retired_name_is_not_a_field(): + # it is not pickled, printed or tabulated: only the two named estimates are data + from bertini.config import writable_fields + _, metadata = _solve(3.0) + fields = writable_fields(type(metadata[0])) + assert 'accuracy_estimate_internal_coords' in fields + assert 'accuracy_estimate_user_coords' in fields + assert 'accuracy_estimate' not in fields + + +def test_metadata_round_trips_with_both_estimates(): + import copy + import pickle + _, metadata = _solve(3.0) + m = metadata[0] + for other in (copy.deepcopy(m), pickle.loads(pickle.dumps(m))): + assert float(other.accuracy_estimate_internal_coords) == float(m.accuracy_estimate_internal_coords) + assert float(other.accuracy_estimate_user_coords) == float(m.accuracy_estimate_user_coords) + + +def test_the_dataframe_has_the_named_columns(): + pytest.importorskip('pandas') + solver, _ = _solve(3.0) + columns = list(solver.to_dataframe().columns) + assert 'accuracy_estimate_internal_coords' in columns + assert 'accuracy_estimate_user_coords' in columns + assert 'accuracy_estimate' not in columns + + +@pytest.mark.parametrize('endgame', ENDGAMES) +def test_the_internal_estimate_does_not_grow_with_the_solution(endgame): + internal = {scale: max(float(m.accuracy_estimate_internal_coords) + for m in _solve(scale, endgame)[1]) + for scale in (1.0, 1e2, 1e4)} + assert 0.1 < internal[1e4] / internal[1.0] < 10.0, internal + assert 0.1 < internal[1e4] / internal[1e2] < 10.0, internal + + +@pytest.mark.parametrize('endgame', ENDGAMES) +def test_the_user_estimate_is_in_the_units_of_the_variables(endgame): + # ONE problem at two scales under ONE seed: how the two estimates relate to each other + # depends on the patch that was drawn, so different draws are not comparable + small = max(float(m.accuracy_estimate_user_coords) for m in _solve(1e2, endgame)[1]) + big = max(float(m.accuracy_estimate_user_coords) for m in _solve(1e4, endgame)[1]) + assert 0.5 < (big / small) / 100.0 < 2.0, (small, big) diff --git a/python_bindings/include/zero_dim_export.hpp b/python_bindings/include/zero_dim_export.hpp index dc008a99e..195091992 100644 --- a/python_bindings/include/zero_dim_export.hpp +++ b/python_bindings/include/zero_dim_export.hpp @@ -130,10 +130,36 @@ void ExposeSolutionMetaData(std::string const& class_name){ "The last time value this path reached. For a successful path, the endgame's latest time " "(at or near the target). For a path that did not succeed, in either stage, the time the " "tracker was at when it stopped -- read with latest_path_point, the point it was at.") - .def_readwrite("accuracy_estimate",&MDT::accuracy_estimate, - "Accuracy estimate from the endgame, the difference between successive extrapolations.") + .def_readwrite("accuracy_estimate_internal_coords",&MDT::accuracy_estimate_internal_coords, + "The distance between the endgame's last two approximations of the root, in the solver's " + "INTERNAL coordinates (homogenized, on the patch). This is the quantity the endgame " + "compares with final_tolerance to decide it has converged. It does not grow with the " + "scale of the solution, so it reads as a number of correct digits: see accuracy_digits. " + "For the error in your own variables, read accuracy_estimate_user_coords.") .def_readwrite("accuracy_estimate_user_coords",&MDT::accuracy_estimate_user_coords, - "Accuracy estimate in natural (dehomogenized) coordinates.") + "The distance between the endgame's last two approximations of the root, in YOUR " + "coordinates (dehomogenized): an absolute error, in the units of your variables. A " + "solution of order 1000 with six correct digits has about 1e-3 here. It is measured " + "in the infinity norm, so the largest coordinate sets it and a small coordinate of the " + "same point has fewer correct digits of its own.") + // RETIRED, loudly. The bare name read as "the accuracy of my solution", in the reader's own + // coordinates, and it held the INTERNAL estimate. It is not an alias for either successor: + // the same name giving a different number is the failure a rename exists to prevent. The + // error is not an AttributeError, because getattr(md, 'accuracy_estimate', None) would + // swallow that and carry on with None. See ADR-0069. + .add_property("accuracy_estimate", + +[](MDT const&) -> double + { + PyErr_SetString(PyExc_RuntimeError, + "SolutionMetaData.accuracy_estimate was retired in bertini 4.0, because its name " + "did not say which coordinates it was in. For the value it held, read " + "accuracy_estimate_internal_coords (the solver's homogenized, patched " + "coordinates; what final_tolerance is compared with). For the absolute error in " + "your own variables, read accuracy_estimate_user_coords."); + boost::python::throw_error_already_set(); + return 0.0; + }, + "Retired in 4.0: read accuracy_estimate_internal_coords or accuracy_estimate_user_coords.") .def_readwrite("cycle_num",&MDT::cycle_num, "The cycle number used by the endgame's extrapolation.") .def_readwrite("precision_digits",&MDT::precision_digits, @@ -142,7 +168,7 @@ void ExposeSolutionMetaData(std::string const& class_name){ "and the higher mpfr precision for a path that had to escalate.") .def_readwrite("accuracy_digits",&MDT::accuracy_digits, "How many digits of this solution are trustworthy (a digit count), from the convergence " - "agreement: floor(-log10(accuracy_estimate)), clamped to [0, precision_digits]. Read with " + "agreement: floor(-log10(accuracy_estimate_internal_coords)), clamped to [0, precision_digits]. Read with " "precision_digits as 'computed in N digits, good to M of them'.") .def_readwrite("endgame_success_code",&MDT::endgame_success_code, "The SuccessCode from the endgame. 0 means Success; anything else means the path did not " diff --git a/python_bindings/src/endgame_config_export.cpp b/python_bindings/src/endgame_config_export.cpp index 2cb84d841..48f170986 100644 --- a/python_bindings/src/endgame_config_export.cpp +++ b/python_bindings/src/endgame_config_export.cpp @@ -17,7 +17,7 @@ namespace bertini{ .def_readwrite("min_track_time", &endgame::EndgameConfig::min_track_time,"The minimum distance from the target time to track to. Decreasing this may help failing runs succeed, or maybe not, because you are, after all, tracking toward a singularity.") .def_readwrite("sample_factor", &endgame::EndgameConfig::sample_factor,"The factor by which to space the geometrically spaced 'distance' between sample points, or sample circles for Cauchy.") .def_readwrite("max_num_refinements", &endgame::EndgameConfig::max_num_refinements,"the maximum number of Newton refinements to be taken during sample point sharpening. Increasing this can help speed convergence, at the risk of path jumping.") - .def_readwrite("final_tolerance", &endgame::EndgameConfig::final_tolerance, "The tolerance to which to track the path, using the endgame. Endgames require two consecutive estimates to be this close to each other under the relative infinity norm. Default value is 1e-11.") + .def_readwrite("final_tolerance", &endgame::EndgameConfig::final_tolerance, "The tolerance to which to track the path, using the endgame: in effect, how many digits to get right. An endgame stops when two consecutive approximations of the root are this close to each other in the infinity norm, measured in the solver's INTERNAL coordinates (homogenized, on the patch), where a point has size about one. In your own variables that is an absolute error of about this tolerance times the size of the solution: a solution of order 1000 found to 1e-6 is known to about 1e-3. The distance reached is reported per solution as accuracy_estimate_internal_coords, and in your variables as accuracy_estimate_user_coords. Default value is 1e-11.") .def_readwrite("refine_when_increasing_precision", &endgame::EndgameConfig::refine_when_increasing_precision, "Whether to re-refine the samples the endgame is keeping when it moves to a higher " "working precision (default False). Off, the retained samples carry the accuracy they "