diff --git a/core/include/bertini2/nag_algorithms/common/config.hpp b/core/include/bertini2/nag_algorithms/common/config.hpp index 478eef0fa..2e5967acd 100644 --- a/core/include/bertini2/nag_algorithms/common/config.hpp +++ b/core/include/bertini2/nag_algorithms/common/config.hpp @@ -194,6 +194,13 @@ struct ZeroDimConfig /// Under MPI the per-rank thread count comes from OMP_NUM_THREADS, not this field. unsigned num_threads = 0; + /// Whether an identical ask already present in the records directory may be RECALLED instead of + /// re-tracked (default true). Set false to force a fresh track even when the paths are recorded -- + /// e.g. to run path observers, benchmark the solve, or re-verify reproducibility. Like num_threads + /// this is transient (it changes only WHETHER the work runs, not WHAT is computed), so it is + /// deliberately excluded from the configuration's identity/digest. No effect when nothing is recorded. + bool recall = true; + mpq_rational start_time{1}; ///< Homotopy start time (t=1). mpq_rational endgame_boundary{1, 10}; ///< Time at which tracking hands off to the endgame (t=1/10). mpq_rational target_time{0}; ///< Homotopy target time (t=0). diff --git a/core/include/bertini2/nag_algorithms/zero_dim_solve.hpp b/core/include/bertini2/nag_algorithms/zero_dim_solve.hpp index db8bb1577..297ebb8c8 100644 --- a/core/include/bertini2/nag_algorithms/zero_dim_solve.hpp +++ b/core/include/bertini2/nag_algorithms/zero_dim_solve.hpp @@ -1130,7 +1130,11 @@ run the endgame, classify the endpoints, report. See the forward-declare doc ab if (records_) { EnsureRunRecorded(); - indices_to_run = RecallRecordedPaths(all_indices); + // recall==false forces a fresh track of every path even when identical results are + // already recorded (e.g. to run path observers or benchmark); the fresh run is still + // recorded. See ZeroDimConfig::recall. + if (this->template Get().recall) + indices_to_run = RecallRecordedPaths(all_indices); } // num_threads: 0 = auto (hardware_concurrency), 1 = serial, N = N threads; diff --git a/core/include/bertini2/records/config_encoding.hpp b/core/include/bertini2/records/config_encoding.hpp index 25d38e4f8..62583e56b 100644 --- a/core/include/bertini2/records/config_encoding.hpp +++ b/core/include/bertini2/records/config_encoding.hpp @@ -43,8 +43,9 @@ System encoding (ADR-0042): encoding changes without the bump. Deliberately excluded from encoding: `algorithm::RandomConfig` (the seed is its own slot -in the ask identity, beside the config digest -- never inside it) and -`ZeroDimConfig::num_threads` (thread count must not change what was computed). +in the ask identity, beside the config digest -- never inside it) and the transient +`ZeroDimConfig::num_threads` / `ZeroDimConfig::recall` (thread count and recall-vs-retrack +change only whether/how the work runs, not what is computed). */ #pragma once @@ -104,7 +105,7 @@ std::string CanonicalEncoding(algorithm::RegenerationConfig const& c); /// \brief Canonical encoding of post-processing (classification) settings. std::string CanonicalEncoding(algorithm::PostProcessingConfig const& c); /// \brief Canonical encoding of top-level zero-dim solve settings. Excludes -/// num_threads (transient: thread count must not change identity). +/// num_threads and recall (transient: they must not change identity). std::string CanonicalEncoding(algorithm::ZeroDimConfig const& c); /// \brief Canonical encoding of the algorithm (track-type) selection. std::string CanonicalEncoding(algorithm::MetaConfig const& c); diff --git a/core/src/records/config_encoding.cpp b/core/src/records/config_encoding.cpp index 2d2317c33..d382af926 100644 --- a/core/src/records/config_encoding.cpp +++ b/core/src/records/config_encoding.cpp @@ -311,7 +311,8 @@ std::string CanonicalEncoding(algorithm::PostProcessingConfig const& c) std::string CanonicalEncoding(algorithm::ZeroDimConfig const& c) { - // num_threads deliberately excluded: transient (must not change what was computed). + // num_threads and recall deliberately excluded: transient (they change only whether/how the work + // runs, not what is computed), so they must not affect the config's identity. std::ostringstream out; out << "(cfg ZeroDim" << " initial_ambient_precision=" << c.initial_ambient_precision diff --git a/core/test/nag_algorithms/zero_dim_records.cpp b/core/test/nag_algorithms/zero_dim_records.cpp index 978df65be..c3733e0bf 100644 --- a/core/test/nag_algorithms/zero_dim_records.cpp +++ b/core/test/nag_algorithms/zero_dim_records.cpp @@ -194,6 +194,44 @@ BOOST_AUTO_TEST_CASE(recording_solve_then_full_recall) BOOST_CHECK_SMALL((first[ii] - again[ii]).norm(), 1e-14); } +// ZeroDimConfig::recall == false forces a fresh re-track of an identical ask that would otherwise be +// recalled -- the escape hatch for path observers / benchmarking / re-verification. +BOOST_AUTO_TEST_CASE(recall_false_forces_a_fresh_retrack) +{ + auto const dir = FreshDir("norecall"); + + // first solve records all four paths (nothing to recall yet) + SetGlobalSeed(42); + auto sys_a = TwoQuadrics(); + ZD a(sys_a); + a.DefaultSetup(); + a.RecordTo(std::make_shared(dir)); + a.Solve(); + BOOST_CHECK_EQUAL(a.NumPathsRecalled(), 0u); + + // identical ask, recall = true (the default): recalls all four, tracks nothing + SetGlobalSeed(42); + auto sys_b = TwoQuadrics(); + ZD b(sys_b); + b.DefaultSetup(); + b.RecordTo(std::make_shared(dir)); + b.Solve(); + BOOST_CHECK_EQUAL(b.NumPathsRecalled(), 4u); + + // identical ask, recall = false: re-tracks everything (recalls nothing) and still finds the roots + SetGlobalSeed(42); + auto sys_c = TwoQuadrics(); + ZD c(sys_c); + c.DefaultSetup(); + auto cfg = c.Get(); + cfg.recall = false; + c.Set(cfg); + c.RecordTo(std::make_shared(dir)); + c.Solve(); + BOOST_CHECK_EQUAL(c.NumPathsRecalled(), 0u); + BOOST_CHECK_EQUAL(c.Report().num_finite_solutions, 4u); +} + BOOST_AUTO_TEST_CASE(partial_directory_resumes_computing_only_the_missing) { auto const full_dir = FreshDir("resume_full"); diff --git a/python/test/zero_dim/recall_test.py b/python/test/zero_dim/recall_test.py new file mode 100644 index 000000000..39b067fbd --- /dev/null +++ b/python/test/zero_dim/recall_test.py @@ -0,0 +1,46 @@ +"""ZeroDimConfig.recall (default True): an identical ask already in the records directory is recalled +rather than re-tracked. recall=False forces a fresh track even when recorded -- the escape hatch for +path observers / benchmarking / re-verification. (Bug context: a SolutionPathCollector silently +collected nothing on a re-solve, because the paths were recalled, not tracked.) +""" + +import bertini as pb +from bertini import ZeroDimSolver +from bertini.nag_algorithm import ZeroDimConfig, observers as nobs + + +def _two_quadrics(): + x, y = pb.variables(['x', 'y']) + sys = pb.System() + sys.add_variable_group(x, y) + sys.add_functions([x * x - 1, y * y - 1]) # 4 well-separated roots -> 4 total-degree paths + return sys + + +def _paths_collected(rec_dir, recall): + pb.random.set_random_seed(42) # SAME ask every call (system + settings + seed) + solver = ZeroDimSolver(_two_quadrics()) + solver.record_to(str(rec_dir)) # isolate the records dir (no CWD pollution) + cfg = solver.get_config(ZeroDimConfig) + cfg.recall = recall + solver.set_config(cfg) + collector = nobs.SolutionPathCollector() + solver.add_observer(collector) + solver.solve() + return len(collector.series) + + +def test_recall_default_true_then_false(tmp_path): + rec = tmp_path / "records" + assert _paths_collected(rec, recall=True) == 4 # 1st: fresh, tracks + records 4 paths + assert _paths_collected(rec, recall=True) == 0 # 2nd: identical ask -> RECALLED, observer empty + assert _paths_collected(rec, recall=False) == 4 # 3rd: recall=False -> forced fresh re-track + + +def test_recall_config_roundtrips(): + solver = ZeroDimSolver(_two_quadrics()) + assert solver.get_config(ZeroDimConfig).recall is True # default + cfg = solver.get_config(ZeroDimConfig) + cfg.recall = False + solver.set_config(cfg) + assert solver.get_config(ZeroDimConfig).recall is False diff --git a/python_bindings/src/zero_dim_configs_export.cpp b/python_bindings/src/zero_dim_configs_export.cpp index dcdbd2549..43674a8ab 100644 --- a/python_bindings/src/zero_dim_configs_export.cpp +++ b/python_bindings/src/zero_dim_configs_export.cpp @@ -107,6 +107,12 @@ namespace bertini{ "(all available cores), 1 = serial (no thread pool), N = N threads. The " "OMP_NUM_THREADS environment variable overrides this. Threading needs no MPI and " "no free-threaded Python: the heavy tracking runs in C++ with the GIL released.") + .def_readwrite("recall", &ZeroDimConfig::recall, + "Whether an identical ask already in the records directory may be RECALLED instead of " + "re-tracked (default True). Set False to force a fresh track even when the paths are " + "recorded -- e.g. to run path observers, benchmark the solve, or re-verify a run; the " + "fresh track is still recorded. No effect when nothing is recorded. Transient: it does " + "not affect the run's identity/digest.") ; // metadata types