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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,16 @@ _______________________________________________________________________________
-->


_______________________________________________________________________________

## [4.0.1] - unreleased

Bug fixes for 4.0.

### Fixed

- A `HomotopySolver` solve's records ask now includes its start points (#498). It named the homotopy, target, settings and seed but not the given start points, so with recording on (the default) a second solve of the same homotopy from different start points was the same ask, and was answered from the record of the first: it silently returned the first solve's endpoints. The start system now reports the identity of start points it was given (`StartSystem::GivenStartIdentity`, a digest of their exact values), and the solve adds it to its ask. A solve from given start points therefore has a different run id than in 4.0.0, so records written by 4.0.0 for such solves are asked again rather than recalled.

_______________________________________________________________________________

## [4.0.0] - 2026-10-06
Expand Down
10 changes: 8 additions & 2 deletions core/include/bertini2/nag_algorithms/zero_dim_solve.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -2716,8 +2716,14 @@ run the endgame, classify the endpoints, report. See the forward-declare doc ab
// redundant with (target, seed) but harmless; for USER homotopies (the
// engine driven directly) it is the only thing telling asks apart
ask["homotopy"] = Homotopy().ContentDigest().Hex();
if (!records_start_identity_.empty())
ask["start"] = records_start_identity_; // external start data is identity
// external start data is identity: the caller's (a chain or a given), else the
// start system's own when it holds given points. Without the second, two solves
// of one homotopy from different given points were one ask, and the second was
// answered with the first's endpoints (bertiniteam/b2#498)
auto const start_identity = !records_start_identity_.empty()
? records_start_identity_ : StartSystem().GivenStartIdentity();
if (!start_identity.empty())
ask["start"] = start_identity;
ask["config"] = settings.Hex();
ask["seed"] = static_cast<std::int64_t>(GetGlobalSeed());
return ask;
Expand Down
8 changes: 8 additions & 0 deletions core/include/bertini2/system/start/user.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,14 @@ namespace bertini
*/
unsigned long long NumStartPoints() const override;

/**
\brief A digest of the given start points: their exact values in order, at the
precision they were given in.

\return The SHA-256 digest, as hex, of the points' exact encoding.
*/
std::string GivenStartIdentity() const override;

User& operator*=(Nd const& n) = delete;

User& operator+=(System const& sys) = delete;
Expand Down
15 changes: 15 additions & 0 deletions core/include/bertini2/system/start_base.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@

#include <functional>
#include <memory>
#include <string>


namespace bertini
Expand Down Expand Up @@ -98,6 +99,20 @@ namespace bertini
return GenerateStartPoint(T(),index);
}

/**
\brief The identity of start points this system was GIVEN, as a digest; empty for a
start system whose points follow from its target system.

A solve's records ask names its target, homotopy, settings and seed. Start points
derived from the target are determined by those; given start points are not, so a
start system that holds given points reports their identity here and the solve adds it
to its ask. The same homotopy from different given points is then a different ask,
and the same points are still recalled.

\return A digest of the given start points, or the empty string.
*/
virtual std::string GivenStartIdentity() const { return {}; }

virtual ~StartSystem() = default;

private:
Expand Down
72 changes: 72 additions & 0 deletions core/src/system/start/user.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,39 @@
// additional terms in the b2/licenses/ directory.

#include "bertini2/system/start/user.hpp"
#include "bertini2/detail/sha256.hpp"

#include <cstdint>
#include <cstring>
#include <iomanip>
#include <sstream>


BOOST_CLASS_EXPORT(bertini::start_system::User);


namespace {

// a double as its exact IEEE-754 bit pattern, the convention of every b2 digest preimage
void EmitExact(std::ostream& out, double v)
{
std::uint64_t bits;
static_assert(sizeof bits == sizeof v, "a double is 64 bits");
std::memcpy(&bits, &v, sizeof bits);
out << "d64:" << std::hex << std::setw(16) << std::setfill('0') << bits << std::dec;
}

// an mpfr value at its own precision with every stored digit, as the system canonical
// encoding writes its multiprecision constants
template<typename RealT>
void EmitExactMp(std::ostream& out, RealT const& v)
{
out << v.str(0, std::ios::scientific);
}

} // namespace


namespace bertini {

namespace start_system {
Expand All @@ -50,6 +78,50 @@ namespace bertini {
}


std::string User::GivenStartIdentity() const
{
// the exact start data, in order: version, kind, point count, then each point's
// length and coordinates. Versioned so a later change of encoding is a new identity.
std::ostringstream text;
text << "b2start/1 ";
if (solns_in_dbl_)
{
auto const& pts = std::get<SampCont<complex_dbl>>(solns_);
text << "dbl " << pts.size() << '\n';
for (auto const& p : pts)
{
text << p.size();
for (Eigen::Index ii = 0; ii < p.size(); ++ii)
{
text << ' ';
EmitExact(text, p(ii).real());
text << ' ';
EmitExact(text, p(ii).imag());
}
text << '\n';
}
}
else
{
auto const& pts = std::get<SampCont<complex_mp>>(solns_);
text << "mp " << pts.size() << '\n';
for (auto const& p : pts)
{
text << p.size();
for (Eigen::Index ii = 0; ii < p.size(); ++ii)
{
text << ' ' << p(ii).precision() << ' ';
EmitExactMp(text, p(ii).real());
text << ' ';
EmitExactMp(text, p(ii).imag());
}
text << '\n';
}
}
return detail::Sha256(text.str()).Hex();
}



Vec<complex_dbl> User::GenerateStartPoint(complex_dbl,unsigned long long index) const
{
Expand Down
99 changes: 99 additions & 0 deletions core/test/nag_algorithms/zero_dim.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1584,6 +1584,105 @@ BOOST_AUTO_TEST_CASE(a_recalled_path_remembers_that_its_crossing_was_never_resol
}


namespace {

// One path of the parameter homotopy H(x,t) = x^2 - (9 - 5t) -- t = 1 roots +/-2, t = 0 roots
// +/-3 -- from a GIVEN start point, recording into `dir`.
struct GivenStartSolve
{
using TrackerT = bertini::tracking::DoublePrecisionTracker;
using SolverT = bertini::algorithm::HomotopySolver<TrackerT,
bertini::endgame::EndgameSelector<TrackerT>::PSEG, bertini::System>;

bertini::System H, target;
bertini::SampCont<bertini::complex_mp> start_points;
std::unique_ptr<bertini::start_system::User> start;
std::unique_ptr<SolverT> solver;

GivenStartSolve(bertini::complex_mp const& from, std::filesystem::path const& dir)
{
using namespace bertini;
auto x = Variable::Make("x");
auto t = Variable::Make("t");
H.AddVariableGroup(VariableGroup{x});
H.AddFunction(x*x - (9 - 5*t));
H.AddPathVariable(t);
target.AddVariableGroup(VariableGroup{x});
target.AddFunction(x*x - 9);

Vec<complex_mp> p(1); p(0) = from;
start_points.push_back(p);
start = std::make_unique<start_system::User>(target, start_points);
solver = std::make_unique<SolverT>(target, *start, H);
solver->DefaultSetup();
solver->RecordTo(std::make_shared<records::OutputDirectory>(dir));
solver->Solve();
}

bertini::complex_dbl Endpoint() const
{
return bertini::complex_dbl(solver->SolutionsUserCoords().at(0)(0));
}
};

} // namespace


// The same homotopy tracked from DIFFERENT given start points is a different computation, so a
// records directory holding the first must not answer the second with the first's endpoints.
// Before the start system's own identity joined the ask, the second solve was the same ask and was
// recalled: tracked from -2 it came back at +3.
BOOST_AUTO_TEST_CASE(different_given_start_points_are_different_asks)
{
auto const dir = FreshCrossingRecords("given_starts");
GivenStartSolve from_plus(bertini::complex_mp(2), dir);
GivenStartSolve from_minus(bertini::complex_mp(-2), dir);

BOOST_CHECK_NE(from_plus.solver->RecordsRunId(), from_minus.solver->RecordsRunId());
BOOST_CHECK_EQUAL(from_minus.solver->NumPathsRecalled(), 0u);
BOOST_CHECK_SMALL(std::abs(from_plus.Endpoint() - bertini::complex_dbl( 3, 0)), 1e-8);
BOOST_CHECK_SMALL(std::abs(from_minus.Endpoint() - bertini::complex_dbl(-3, 0)), 1e-8);
}


// ... and the same given start points are still the same ask, answered from the record.
BOOST_AUTO_TEST_CASE(the_same_given_start_points_are_still_recalled)
{
auto const dir = FreshCrossingRecords("given_starts_again");
GivenStartSolve first(bertini::complex_mp(2), dir);
GivenStartSolve again(bertini::complex_mp(2), dir);

BOOST_CHECK_EQUAL(again.solver->RecordsRunId(), first.solver->RecordsRunId());
BOOST_CHECK_EQUAL(again.solver->NumPathsRecalled(), 1u);
BOOST_CHECK_SMALL(std::abs(again.Endpoint() - first.Endpoint()), 1e-14);
}


// The identity is of the points' exact values: two start points that differ by a hair are
// different start data, and the identity does not depend on anything but the points.
BOOST_AUTO_TEST_CASE(given_start_identity_is_exact)
{
using namespace bertini;
System target;
auto x = Variable::Make("x");
target.AddVariableGroup(VariableGroup{x});
target.AddFunction(x*x - 9);

auto identity_of = [&](complex_mp const& v) {
SampCont<complex_mp> pts;
Vec<complex_mp> p(1); p(0) = v;
pts.push_back(p);
return start_system::User(target, pts).GivenStartIdentity();
};
// 2 and 2 + 2^-40: a small difference, exactly representable at any working precision
complex_mp a(2), b(complex_dbl(2.0 + std::ldexp(1.0, -40), 0.0));

BOOST_CHECK(!identity_of(a).empty());
BOOST_CHECK_EQUAL(identity_of(a), identity_of(complex_mp(2)));
BOOST_CHECK_NE(identity_of(a), identity_of(b));
}


// A run whose check passed says so in its own record: absence of a midpath record has to keep
// meaning "the check did not run", or a solve cut short reads as a clean bill of health.
BOOST_AUTO_TEST_CASE(a_clean_solve_records_a_crossing_check_that_passed)
Expand Down
28 changes: 28 additions & 0 deletions python/test/records/records_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,34 @@ def test_homotopy_solver_also_produces_a_zerodim_result():
assert len(hr.answer.finite) == 2 and len(hr.answer.real) == 2 # +-sqrt(3)


def test_homotopy_solver_from_different_starts_is_never_recalled(tmp_path):
"""The same homotopy from DIFFERENT start points is a different solve: recording the first
must not answer the second with the first's endpoints, and the same start points still
recall. H(x, t) = x^2 - (9 - 5t) carries +2 to +3 and -2 to -3."""
import bertini.nag_algorithm as nag
x, t = Variable('x'), Variable('t')
H = System(); H.add_variable_group(VariableGroup([x])); H.add_function(x * x - (9 - 5 * t))
H.add_path_variable(t)
tgt = System(); tgt.add_variable_group(VariableGroup([x])); tgt.add_function(x * x - 9)
d = str(tmp_path / 'records')

def from_(value):
# one seed for every solve, so the start points are all that differs between asks
pb.random.set_random_seed(11)
solver = nag.HomotopySolver(H, [np.array([pb.multiprec.complex_mp(value)])], tgt)
solver.record_to(d)
result = solver.solve()
return complex(result[0][0]), result.num_recalled, result.run_id

plus, recalled_plus, run_plus = from_(2)
minus, recalled_minus, run_minus = from_(-2)
assert recalled_minus == 0 and run_minus != run_plus
assert abs(plus - 3) < 1e-8 and abs(minus + 3) < 1e-8

again, recalled_again, run_again = from_(2)
assert recalled_again == 1 and run_again == run_plus and abs(again - plus) < 1e-14


def test_zerodim_result_categories_for_a_multiple_root():
# {x^2, y^2}: one distinct finite solution (0,0), singular (multiplicity 4)
x, y = Variable('x'), Variable('y')
Expand Down
Loading