From 9bc34b19e3b78e8e1fede6cddaea765bbcfbf465 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Fern=C3=A1ndez=20Cervell?= Date: Sat, 8 Aug 2026 19:16:48 +0200 Subject: [PATCH 01/11] Polish method --- src/solvers/hp/hp.cc | 39 ++++++++++++++++++-- src/solvers/logit/path.cc | 78 +++++++++++++++++++++++++++++++++++++-- src/solvers/logit/path.h | 11 ++++++ 3 files changed, 121 insertions(+), 7 deletions(-) diff --git a/src/solvers/hp/hp.cc b/src/solvers/hp/hp.cc index 9b71fdd07..386adb06a 100644 --- a/src/solvers/hp/hp.cc +++ b/src/solvers/hp/hp.cc @@ -38,10 +38,20 @@ HPStrategySolve(const MixedStrategyProfile &p_prior) const PathTracer tracer; double omega = 1.0; + const double t_target = 1.0; + const double tol = 1e-8; - auto termination_condition = [](const Vector &point) { return point[1] >= 1.5; }; - auto criterion_function = [](const Vector &point, - const Vector &tangent) -> double { return point[1] - 1.0; }; + auto termination_condition = [t_target](const Vector &point) { + return point[1] >= t_target; + }; + auto criterion_function = [t_target](const Vector &point, + const Vector &tangent) -> double { + return point[1] - t_target; + }; + + auto polishing_termination_condition = [tol, &system](const Vector &point) -> bool { + return system.ExtractEquilibrium(point).GetMaxRegret() <= tol; + }; const TracePathResult result = tracer.TracePath( [&system](const Vector &point, Vector &lhs) { system.GetValue(point, lhs); }, @@ -66,7 +76,30 @@ HPStrategySolve(const MixedStrategyProfile &p_prior) }, criterion_function); + int polish_step = 1; + const TracePathResult polishing_result = PolishPoint( + [&system](const Vector &point, Vector &lhs) { system.GetValue(point, lhs); }, + [&system](const Vector &point, Matrix &jac) { + system.GetJacobian(point, jac); + }, + x, t_target, 1, polishing_termination_condition, 100, + [&system, &polish_step](const Vector &point) { + const MixedStrategyProfile profile = system.ExtractEquilibrium(point); + const double regret = profile.GetMaxRegret(); + + std::cout << "[Polish Step " << polish_step++ << "] "; + std::cout << "Regret: " << regret << " | Probabilities: "; + + Vector prob_vector = profile.GetProbVector(); + std::cout << std::fixed << std::setprecision(5); + for (size_t i = 1; i <= prob_vector.size(); ++i) { + std::cout << prob_vector[i] << " "; + } + std::cout << std::endl; + }); + equilibria.push_back(system.ExtractEquilibrium(x)); + return equilibria; } } // namespace Gambit diff --git a/src/solvers/logit/path.cc b/src/solvers/logit/path.cc index 267d8f914..c43a645dd 100644 --- a/src/solvers/logit/path.cc +++ b/src/solvers/logit/path.cc @@ -158,11 +158,13 @@ PathTracer::TracePath(std::function &, Vector q.GetRow(q.NumRows(), t); p_callback(x); + int steps = 0; + while (!p_terminate(x)) { bool accept = true; if (fabs(h) <= c_hmin) { - return {x, false, "Stepsize fell below minimum threshold."}; + return {x, false, "Stepsize fell below minimum threshold.", steps}; } // Predictor step @@ -205,7 +207,7 @@ PathTracer::TracePath(std::function &, Vector disto = dist; iter++; if (iter > c_maxIter) { - return {x, false, "Maximum iterations exceeded."}; + return {x, false, "Maximum iterations exceeded.", steps}; } } @@ -227,7 +229,7 @@ PathTracer::TracePath(std::function &, Vector if (!accept) { h /= m_maxDecel; // PC not accepted; change stepsize and retry if (fabs(h) <= c_hmin) { - return {x, false, "Stepsize fell below minimum threshold."}; + return {x, false, "Stepsize fell below minimum threshold.", steps}; } continue; } @@ -258,6 +260,7 @@ PathTracer::TracePath(std::function &, Vector x = u; t = newT; p_callback(x); + steps++; if (pert_countdown > 0.0) { // If we are currently perturbing in the neighborhood of a bifurcation, check to see @@ -269,7 +272,74 @@ PathTracer::TracePath(std::function &, Vector } } } - return {x, true, "Path tracing terminated successfully."}; + return {x, true, "Path tracing terminated successfully.", steps}; } +TracePathResult +PolishPoint(std::function &, Vector &)> p_function, + std::function &, Matrix &)> p_jacobian, + Vector &x, double fixed_value, size_t fixed_index, + TerminationFunctionType p_terminate, int max_iter, CallbackFunctionType p_callback) +{ + x[fixed_index] = fixed_value; + + const size_t N = x.size() - 1; + Vector y(N); // Equations results + Matrix jac_full(N + 1, N); // Full Jacobian matrix (N+1 unknowns, N equations) + Matrix jac_square(N, N); // Jacobian matrix with fixed_index row removed + Matrix Q(N, N); // Orthogonal matrix from QR decomposition + Vector x_reduced(N); // Reduced x vector with fixed_index removed + + int steps = 0; + double dist = 0.0; + + while (!p_terminate(x)) { + if (steps >= max_iter) { + return {x, false, "Polishing exceeded maximum iterations.", steps}; + } + + p_function(x, y); + p_jacobian(x, jac_full); + + // Square Matrix removing fixed_index column + size_t row_index = 1; + for (size_t i = 1; i <= N + 1; ++i) { // Newton step expects the transposed Jacobian + if (i != fixed_index) { + for (size_t j = 1; j <= N; ++j) { + jac_square(row_index, j) = jac_full(i, j); + } + row_index++; + } + } + + // Reduced x vector removing fixed_index + size_t temp_idx = 1; + for (size_t i = 1; i <= N + 1; ++i) { + if (i != fixed_index) { + x_reduced[temp_idx++] = x[i]; + } + } + + QRDecomp(jac_square, Q); + + // Solve jac_square * x_reduced = -y + NewtonStep(Q, jac_square, x_reduced, y, dist); + + // Update x, keeping fixed_index constant + temp_idx = 1; + for (size_t i = 1; i <= N + 1; ++i) { + if (i != fixed_index) { + x[i] = x_reduced[temp_idx++]; + } + } + + steps++; + + if (p_callback) { + p_callback(x); + } + } + + return {x, true, "Polishing terminated successfully.", steps}; +} } // end namespace Gambit diff --git a/src/solvers/logit/path.h b/src/solvers/logit/path.h index 655194e42..b5cd11bb9 100644 --- a/src/solvers/logit/path.h +++ b/src/solvers/logit/path.h @@ -62,6 +62,7 @@ struct TracePathResult { Vector final_point; bool status; // true if path tracing terminated successfully, false if it terminated due to error std::string message; // error message if status is false + int step; // Step at which the tracing terminated }; // @@ -92,6 +93,16 @@ class PathTracer { double m_maxDecel{1.1}, m_hStart{0.03}; }; +// This function reduces the regret of a point that is close to an equilibrium that has been found +// by the path-following algorithm. Fixing the value of a component of the point, it uses a +// Newton-type method to find a nearby point with lower regret. +TracePathResult +PolishPoint(std::function &, Vector &)> p_function, + std::function &, Matrix &)> p_jacobian, + Vector &p_x, double fixed_value, size_t fixed_index, + TerminationFunctionType p_terminate, int max_iter = 100, + CallbackFunctionType p_callback = NullCallbackFunction); + } // end namespace Gambit #endif // PATH_H From bbc4b4e35e0dd8acc196a9d7caa6fb88b4005a8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Fern=C3=A1ndez=20Cervell?= Date: Tue, 11 Aug 2026 14:19:38 +0200 Subject: [PATCH 02/11] Adjustments in polish method --- src/solvers/hp/hp.cc | 43 +++++++++++++++++++++++++++++++++++---- src/solvers/logit/path.cc | 11 +++++----- src/solvers/logit/path.h | 20 +++++++++++------- 3 files changed, 57 insertions(+), 17 deletions(-) diff --git a/src/solvers/hp/hp.cc b/src/solvers/hp/hp.cc index 386adb06a..44c2798ca 100644 --- a/src/solvers/hp/hp.cc +++ b/src/solvers/hp/hp.cc @@ -40,10 +40,38 @@ HPStrategySolve(const MixedStrategyProfile &p_prior) double omega = 1.0; const double t_target = 1.0; const double tol = 1e-8; + double last_t = 0.0; + bool has_crossed = false; - auto termination_condition = [t_target](const Vector &point) { - return point[1] >= t_target; + auto termination_condition = [t_target, &last_t, &has_crossed, + tol](const Vector &point) { + const double t = point[1]; + + // Path tracer reaches tol + if (t >= t_target && t - t_target < tol) { + return true; + } + + if (t > t_target) { + if (!has_crossed) { + has_crossed = true; + } + else { + // Criterion function is not working; polish will do the job + if (t > last_t + tol) { + return true; + } + } + } + else { + // Criterion function might take t back to being minor than t_target + has_crossed = false; + } + + last_t = t; + return false; }; + auto criterion_function = [t_target](const Vector &point, const Vector &tangent) -> double { return point[1] - t_target; @@ -60,6 +88,7 @@ HPStrategySolve(const MixedStrategyProfile &p_prior) }, x, omega, termination_condition, [&system](const Vector &point) { + std::cout << std::fixed << std::setprecision(8); std::cout << "[Path Tracer Step] t = " << point[1]; std::cout << " | Alfas: "; for (size_t i = 2; i <= 5; ++i) { @@ -77,13 +106,13 @@ HPStrategySolve(const MixedStrategyProfile &p_prior) criterion_function); int polish_step = 1; - const TracePathResult polishing_result = PolishPoint( + const PolishResult polishing_result = PolishPoint( [&system](const Vector &point, Vector &lhs) { system.GetValue(point, lhs); }, [&system](const Vector &point, Matrix &jac) { system.GetJacobian(point, jac); }, x, t_target, 1, polishing_termination_condition, 100, - [&system, &polish_step](const Vector &point) { + [&system, &polish_step, tol](const Vector &point) { const MixedStrategyProfile profile = system.ExtractEquilibrium(point); const double regret = profile.GetMaxRegret(); @@ -95,6 +124,12 @@ HPStrategySolve(const MixedStrategyProfile &p_prior) for (size_t i = 1; i <= prob_vector.size(); ++i) { std::cout << prob_vector[i] << " "; } + if (regret <= tol) { + std::cout << " | Polishing successful (" << regret << " <= " << tol << ")"; + } + else { + std::cout << " | Polishing failed (" << regret << " > " << tol << ")"; + } std::cout << std::endl; }); diff --git a/src/solvers/logit/path.cc b/src/solvers/logit/path.cc index c43a645dd..cfa0ff20d 100644 --- a/src/solvers/logit/path.cc +++ b/src/solvers/logit/path.cc @@ -275,11 +275,11 @@ PathTracer::TracePath(std::function &, Vector return {x, true, "Path tracing terminated successfully.", steps}; } -TracePathResult -PolishPoint(std::function &, Vector &)> p_function, - std::function &, Matrix &)> p_jacobian, - Vector &x, double fixed_value, size_t fixed_index, - TerminationFunctionType p_terminate, int max_iter, CallbackFunctionType p_callback) +PolishResult PolishPoint(std::function &, Vector &)> p_function, + std::function &, Matrix &)> p_jacobian, + Vector &x, double fixed_value, size_t fixed_index, + TerminationFunctionType p_terminate, int max_iter, + CallbackFunctionType p_callback) { x[fixed_index] = fixed_value; @@ -301,7 +301,6 @@ PolishPoint(std::function &, Vector &)> p_func p_function(x, y); p_jacobian(x, jac_full); - // Square Matrix removing fixed_index column size_t row_index = 1; for (size_t i = 1; i <= N + 1; ++i) { // Newton step expects the transposed Jacobian if (i != fixed_index) { diff --git a/src/solvers/logit/path.h b/src/solvers/logit/path.h index b5cd11bb9..446383c24 100644 --- a/src/solvers/logit/path.h +++ b/src/solvers/logit/path.h @@ -62,7 +62,14 @@ struct TracePathResult { Vector final_point; bool status; // true if path tracing terminated successfully, false if it terminated due to error std::string message; // error message if status is false - int step; // Step at which the tracing terminated + int steps; // Step at which the tracing terminated +}; + +struct PolishResult { + Vector final_point; + bool status; // true if polishing terminated successfully, false if it terminated due to error + std::string message; // error message if status is false + int steps; // Step at which the polishing terminated }; // @@ -96,12 +103,11 @@ class PathTracer { // This function reduces the regret of a point that is close to an equilibrium that has been found // by the path-following algorithm. Fixing the value of a component of the point, it uses a // Newton-type method to find a nearby point with lower regret. -TracePathResult -PolishPoint(std::function &, Vector &)> p_function, - std::function &, Matrix &)> p_jacobian, - Vector &p_x, double fixed_value, size_t fixed_index, - TerminationFunctionType p_terminate, int max_iter = 100, - CallbackFunctionType p_callback = NullCallbackFunction); +PolishResult PolishPoint(std::function &, Vector &)> p_function, + std::function &, Matrix &)> p_jacobian, + Vector &p_x, double fixed_value, size_t fixed_index, + TerminationFunctionType p_terminate, int max_iter = 100, + CallbackFunctionType p_callback = NullCallbackFunction); } // end namespace Gambit From d48a057247d6b18c190dda66105cbc9d7b3d3981 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Fern=C3=A1ndez=20Cervell?= Date: Tue, 11 Aug 2026 15:10:06 +0200 Subject: [PATCH 03/11] Newton check in tracer --- src/solvers/logit/path.cc | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/src/solvers/logit/path.cc b/src/solvers/logit/path.cc index cfa0ff20d..976898109 100644 --- a/src/solvers/logit/path.cc +++ b/src/solvers/logit/path.cc @@ -132,14 +132,15 @@ PathTracer::TracePath(std::function &, Vector CallbackFunctionType p_callback, CriterionFunctionType p_criterion, CriterionBracketFunctionType p_criterionBracket) const { - const double c_tol = 1.0e-4; // tolerance for corrector iteration - const double c_maxDist = 0.4; // maximal distance to curve - const double c_maxContr = 0.6; // maximal contraction rate in corrector - const double c_eta = 0.1; // perturbation to avoid cancellation - // in calculating contraction rate - double h = m_hStart; // initial stepsize - const double c_hmin = 1.0e-8; // minimal stepsize - const int c_maxIter = 100; // maximum iterations in corrector + const double c_tol = 1.0e-4; // tolerance for corrector iteration + const double c_maxDist = 0.4; // maximal distance to curve + const double c_maxContr = 0.6; // maximal contraction rate in corrector + const double c_eta = 0.1; // perturbation to avoid cancellation + // in calculating contraction rate + double h = m_hStart; // initial stepsize + const double c_hmin = 1.0e-8; // minimal stepsize + const int c_maxIter = 100; // maximum iterations in corrector + const double newton_tol = 1.0e-8; // tolerance for Newton convergence bool newton = false; // using Newton steplength (for zero-finding) const double c_pert = 0.0000001; // The size of perturbation to apply to avoid bifurcation traps @@ -164,7 +165,12 @@ PathTracer::TracePath(std::function &, Vector bool accept = true; if (fabs(h) <= c_hmin) { - return {x, false, "Stepsize fell below minimum threshold.", steps}; + if (newton && std::abs(p_criterion(x, t)) < newton_tol) { + return {x, true, "Path tracing terminated successfully (Newton convergence)", steps}; + } + else { + return {x, false, "Stepsize fell below minimum threshold.", steps}; + } } // Predictor step @@ -229,7 +235,12 @@ PathTracer::TracePath(std::function &, Vector if (!accept) { h /= m_maxDecel; // PC not accepted; change stepsize and retry if (fabs(h) <= c_hmin) { - return {x, false, "Stepsize fell below minimum threshold.", steps}; + if (newton && std::abs(p_criterion(x, t)) < newton_tol) { + return {x, true, "Path tracing terminated successfully (Newton convergence)", steps}; + } + else { + return {x, false, "Stepsize fell below minimum threshold.", steps}; + } } continue; } From 04882bf313fa14a13f45fe249730cad38acc881e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Fern=C3=A1ndez=20Cervell?= Date: Wed, 12 Aug 2026 23:27:21 +0200 Subject: [PATCH 04/11] Format fixes --- src/solvers/hp/hp.cc | 53 ++++++--------------------------------- src/solvers/logit/path.cc | 30 ++++++++++++---------- 2 files changed, 25 insertions(+), 58 deletions(-) diff --git a/src/solvers/hp/hp.cc b/src/solvers/hp/hp.cc index 44c2798ca..d9ae74523 100644 --- a/src/solvers/hp/hp.cc +++ b/src/solvers/hp/hp.cc @@ -56,15 +56,12 @@ HPStrategySolve(const MixedStrategyProfile &p_prior) if (!has_crossed) { has_crossed = true; } - else { - // Criterion function is not working; polish will do the job - if (t > last_t + tol) { - return true; - } + else if (t > last_t + tol) { // Criterion function is not working; polish will do the job + return true; } } else { - // Criterion function might take t back to being minor than t_target + // Criterion function might take t back to being less than t_target has_crossed = false; } @@ -86,24 +83,7 @@ HPStrategySolve(const MixedStrategyProfile &p_prior) [&system](const Vector &point, Matrix &jac) { system.GetJacobian(point, jac); }, - x, omega, termination_condition, - [&system](const Vector &point) { - std::cout << std::fixed << std::setprecision(8); - std::cout << "[Path Tracer Step] t = " << point[1]; - std::cout << " | Alfas: "; - for (size_t i = 2; i <= 5; ++i) { - std::cout << point[i] << " "; - } - std::cout << "| Mu: " << point[6] << " " << point[7] << std::endl; - - std::cout << "Full point vector in probabilities: "; - Vector prob_vector = system.ExtractEquilibrium(point).GetProbVector(); - for (size_t i = 1; i <= prob_vector.size(); ++i) { - std::cout << prob_vector[i] << " "; - } - std::cout << std::endl; - }, - criterion_function); + x, omega, termination_condition, NullCallbackFunction, criterion_function); int polish_step = 1; const PolishResult polishing_result = PolishPoint( @@ -111,28 +91,11 @@ HPStrategySolve(const MixedStrategyProfile &p_prior) [&system](const Vector &point, Matrix &jac) { system.GetJacobian(point, jac); }, - x, t_target, 1, polishing_termination_condition, 100, - [&system, &polish_step, tol](const Vector &point) { - const MixedStrategyProfile profile = system.ExtractEquilibrium(point); - const double regret = profile.GetMaxRegret(); - - std::cout << "[Polish Step " << polish_step++ << "] "; - std::cout << "Regret: " << regret << " | Probabilities: "; - - Vector prob_vector = profile.GetProbVector(); - std::cout << std::fixed << std::setprecision(5); - for (size_t i = 1; i <= prob_vector.size(); ++i) { - std::cout << prob_vector[i] << " "; - } - if (regret <= tol) { - std::cout << " | Polishing successful (" << regret << " <= " << tol << ")"; - } - else { - std::cout << " | Polishing failed (" << regret << " > " << tol << ")"; - } - std::cout << std::endl; - }); + x, t_target, 1, polishing_termination_condition, 100, NullCallbackFunction); + if (!polishing_result.status) { + return {}; + } equilibria.push_back(system.ExtractEquilibrium(x)); return equilibria; diff --git a/src/solvers/logit/path.cc b/src/solvers/logit/path.cc index 976898109..9ecbef22e 100644 --- a/src/solvers/logit/path.cc +++ b/src/solvers/logit/path.cc @@ -132,15 +132,15 @@ PathTracer::TracePath(std::function &, Vector CallbackFunctionType p_callback, CriterionFunctionType p_criterion, CriterionBracketFunctionType p_criterionBracket) const { - const double c_tol = 1.0e-4; // tolerance for corrector iteration - const double c_maxDist = 0.4; // maximal distance to curve - const double c_maxContr = 0.6; // maximal contraction rate in corrector - const double c_eta = 0.1; // perturbation to avoid cancellation - // in calculating contraction rate - double h = m_hStart; // initial stepsize - const double c_hmin = 1.0e-8; // minimal stepsize - const int c_maxIter = 100; // maximum iterations in corrector - const double newton_tol = 1.0e-8; // tolerance for Newton convergence + const double c_tol = 1.0e-4; // tolerance for corrector iteration + const double c_maxDist = 0.4; // maximal distance to curve + const double c_maxContr = 0.6; // maximal contraction rate in corrector + const double c_eta = 0.1; // perturbation to avoid cancellation + // in calculating contraction rate + double h = m_hStart; // initial stepsize + const double c_hmin = 1.0e-8; // minimal stepsize + const int c_maxIter = 100; // maximum iterations in corrector + const double c_newtonTol = 1.0e-8; // tolerance for Newton convergence bool newton = false; // using Newton steplength (for zero-finding) const double c_pert = 0.0000001; // The size of perturbation to apply to avoid bifurcation traps @@ -165,8 +165,10 @@ PathTracer::TracePath(std::function &, Vector bool accept = true; if (fabs(h) <= c_hmin) { - if (newton && std::abs(p_criterion(x, t)) < newton_tol) { - return {x, true, "Path tracing terminated successfully (Newton convergence)", steps}; + if (newton && std::abs(p_criterion(x, t)) < c_newtonTol) { + return {x, true, + "Path following terminated successfully at point satisfying criterion function.", + steps}; } else { return {x, false, "Stepsize fell below minimum threshold.", steps}; @@ -235,8 +237,10 @@ PathTracer::TracePath(std::function &, Vector if (!accept) { h /= m_maxDecel; // PC not accepted; change stepsize and retry if (fabs(h) <= c_hmin) { - if (newton && std::abs(p_criterion(x, t)) < newton_tol) { - return {x, true, "Path tracing terminated successfully (Newton convergence)", steps}; + if (newton && std::abs(p_criterion(x, t)) < c_newtonTol) { + return {x, true, + "Path following terminated successfully at point satisfying criterion function.", + steps}; } else { return {x, false, "Stepsize fell below minimum threshold.", steps}; From 5829da2d81e0caec0eede484016180be63432cf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Fern=C3=A1ndez=20Cervell?= Date: Thu, 13 Aug 2026 00:12:09 +0200 Subject: [PATCH 05/11] Format fixes, test suite, merge.. --- .../books/shohamleytonbrown2008/fig5_1.efg | 21 ++ .../books/shohamleytonbrown2008/fig5_10.efg | 16 + .../fig5_10__original_layout.ef | 12 + .../books/shohamleytonbrown2008/fig5_11.efg | 14 + .../fig5_11__original_layout.ef | 10 + .../books/shohamleytonbrown2008/fig5_12.efg | 15 + .../fig5_12__original_layout.ef | 15 + .../books/shohamleytonbrown2008/fig5_15.efg | 17 + .../fig5_15__original_layout.ef | 11 + .../fig5_1__original_layout.ef | 12 + .../books/shohamleytonbrown2008/fig5_2.efg | 16 + .../fig5_2__original_layout.ef | 11 + .../books/shohamleytonbrown2008/fig5_9.efg | 22 ++ .../fig5_9__original_layout.ef | 13 + .../books/shohamleytonbrown2008/fig6_2.efg | 38 ++ .../fig6_2__original_layout.ef | 38 ++ .../books/shohamleytonbrown2008/fig6_8.efg | 36 ++ .../fig6_8__original_layout.ef | 92 +++++ src/pygambit/nash.h | 33 +- src/solvers/hp/hp.cc | 9 +- src/solvers/logit/efglogit.cc | 19 +- src/solvers/logit/logit.h | 26 +- src/solvers/logit/nfglogit.cc | 19 +- src/solvers/logit/path.cc | 45 ++- src/solvers/logit/path.h | 4 +- src/tools/logit/logit.cc | 17 +- tests/test_hp.py | 345 +++++++++++++++++- 27 files changed, 857 insertions(+), 69 deletions(-) create mode 100644 catalog/books/shohamleytonbrown2008/fig5_1.efg create mode 100644 catalog/books/shohamleytonbrown2008/fig5_10.efg create mode 100644 catalog/books/shohamleytonbrown2008/fig5_10__original_layout.ef create mode 100644 catalog/books/shohamleytonbrown2008/fig5_11.efg create mode 100644 catalog/books/shohamleytonbrown2008/fig5_11__original_layout.ef create mode 100644 catalog/books/shohamleytonbrown2008/fig5_12.efg create mode 100644 catalog/books/shohamleytonbrown2008/fig5_12__original_layout.ef create mode 100644 catalog/books/shohamleytonbrown2008/fig5_15.efg create mode 100644 catalog/books/shohamleytonbrown2008/fig5_15__original_layout.ef create mode 100644 catalog/books/shohamleytonbrown2008/fig5_1__original_layout.ef create mode 100644 catalog/books/shohamleytonbrown2008/fig5_2.efg create mode 100644 catalog/books/shohamleytonbrown2008/fig5_2__original_layout.ef create mode 100644 catalog/books/shohamleytonbrown2008/fig5_9.efg create mode 100644 catalog/books/shohamleytonbrown2008/fig5_9__original_layout.ef create mode 100644 catalog/books/shohamleytonbrown2008/fig6_2.efg create mode 100644 catalog/books/shohamleytonbrown2008/fig6_2__original_layout.ef create mode 100644 catalog/books/shohamleytonbrown2008/fig6_8.efg create mode 100644 catalog/books/shohamleytonbrown2008/fig6_8__original_layout.ef diff --git a/catalog/books/shohamleytonbrown2008/fig5_1.efg b/catalog/books/shohamleytonbrown2008/fig5_1.efg new file mode 100644 index 000000000..49f598d28 --- /dev/null +++ b/catalog/books/shohamleytonbrown2008/fig5_1.efg @@ -0,0 +1,21 @@ +EFG 2 R "Fig 5.1 from Shoham and Leyton-Brown (2008)" { "1" "2" } + +" +Figure 5.1 from :cite:p:`ShoLeyB08`. +This is a sharing game. Imagine a brother and sister sharing two indivisible and identical presents +from their parents. First the brother suggests a split, which can be one of three: he +keeps both, she keeps both, or they each keep one. Then the sister chooses whether +to accept or reject the split. If she accepts they each get their allocated present(s), +and otherwise neither gets any gift. +" + +p "" 1 1 "" { "2-0" "1-1" "0-2" } 0 +p "" 2 2 "" { "no" "yes" } 0 +t "" 1 "" { 0 0 } +t "" 2 "" { 2 0 } +p "" 2 3 "" { "no" "yes" } 0 +t "" 3 "" { 0 0 } +t "" 4 "" { 1 1 } +p "" 2 4 "" { "no" "yes" } 0 +t "" 5 "" { 0 0 } +t "" 6 "" { 0 2 } diff --git a/catalog/books/shohamleytonbrown2008/fig5_10.efg b/catalog/books/shohamleytonbrown2008/fig5_10.efg new file mode 100644 index 000000000..94cd759ff --- /dev/null +++ b/catalog/books/shohamleytonbrown2008/fig5_10.efg @@ -0,0 +1,16 @@ +EFG 2 R "Fig 5.10 from Shoham and Leyton-Brown (2008)" { "1" "2" } + +" +Figure 5.10 from :cite:p:`ShoLeyB08`. +This is an example of an imperfect-information game. +" + +p "" 1 2 "" { "L" "R" } 0 +p "" 2 3 "" { "A" "B" } 0 +p "" 1 1 "" { "l" "r" } 0 +t "" 1 "" { 0 0 } +t "" 2 "" { 2 4 } +p "" 1 1 0 +t "" 3 "" { 4 2 } +t "" 4 "" { 0 0 } +t "" 5 "" { 0 0 } diff --git a/catalog/books/shohamleytonbrown2008/fig5_10__original_layout.ef b/catalog/books/shohamleytonbrown2008/fig5_10__original_layout.ef new file mode 100644 index 000000000..55e983222 --- /dev/null +++ b/catalog/books/shohamleytonbrown2008/fig5_10__original_layout.ef @@ -0,0 +1,12 @@ +player 1 name 1 +player 2 name 2 +level 0 node 1 player 1 +level 2 node 1 player 2 xshift -3.58 from 0,1 move L +level 2 node 2 xshift 3.58 from 0,1 move R payoffs 0 0 +level 4 node 1 xshift -2.86 from 2,1 move A +level 4 node 2 xshift 2.86 from 2,1 move B +level 6 node 1 xshift -1.43 from 4,1 move l payoffs 0 0 +level 6 node 2 xshift 1.43 from 4,1 move r payoffs 2 4 +level 6 node 3 xshift -1.43 from 4,2 move l payoffs 4 2 +level 6 node 4 xshift 1.43 from 4,2 move r payoffs 0 0 +iset 4,1 4,2 player 1 diff --git a/catalog/books/shohamleytonbrown2008/fig5_11.efg b/catalog/books/shohamleytonbrown2008/fig5_11.efg new file mode 100644 index 000000000..a972fb90b --- /dev/null +++ b/catalog/books/shohamleytonbrown2008/fig5_11.efg @@ -0,0 +1,14 @@ +EFG 2 R "Fig 5.11 from Shoham and Leyton-Brown (2008)" { "1" "2" } + +" +Figure 5.11 from :cite:p:`ShoLeyB08`. +The Prisoner's Dilemma game in extensive form. +" + +p "" 1 2 "" { "C" "D" } 0 +p "" 2 1 "" { "c" "d" } 0 +t "" 1 "" { -1 -1 } +t "" 2 "" { -4 0 } +p "" 2 1 0 +t "" 3 "" { 0 -4 } +t "" 4 "" { -3 -3 } diff --git a/catalog/books/shohamleytonbrown2008/fig5_11__original_layout.ef b/catalog/books/shohamleytonbrown2008/fig5_11__original_layout.ef new file mode 100644 index 000000000..8aa60582e --- /dev/null +++ b/catalog/books/shohamleytonbrown2008/fig5_11__original_layout.ef @@ -0,0 +1,10 @@ +player 1 name 1 +player 2 name 2 +level 0 node 1 player 1 +level 2 node 1 xshift -3.58 from 0,1 move C +level 2 node 2 xshift 3.58 from 0,1 move D +level 4 node 1 xshift -1.79 from 2,1 move c payoffs -1 -1 +level 4 node 2 xshift 1.79 from 2,1 move d payoffs -4 0 +level 4 node 3 xshift -1.79 from 2,2 move c payoffs 0 -4 +level 4 node 4 xshift 1.79 from 2,2 move d payoffs -3 -3 +iset 2,1 2,2 player 2 diff --git a/catalog/books/shohamleytonbrown2008/fig5_12.efg b/catalog/books/shohamleytonbrown2008/fig5_12.efg new file mode 100644 index 000000000..27c70afe2 --- /dev/null +++ b/catalog/books/shohamleytonbrown2008/fig5_12.efg @@ -0,0 +1,15 @@ +EFG 2 R "Fig 5.12 from Shoham and Leyton-Brown (2008)" { "1" "2" } + +" +Figure 5.12 from :cite:p:`ShoLeyB08`. +A game with imperfect recall, in particular absent-mindedness. +" + + +p "" 1 1 "" { "L" "R" } 0 +p "" 1 1 0 +t "" 1 "" { 1 0 } +t "" 2 "" { 100 100 } +p "" 2 2 "" { "U" "D" } 0 +t "" 3 "" { 5 1 } +t "" 4 "" { 2 2 } diff --git a/catalog/books/shohamleytonbrown2008/fig5_12__original_layout.ef b/catalog/books/shohamleytonbrown2008/fig5_12__original_layout.ef new file mode 100644 index 000000000..98b720cdc --- /dev/null +++ b/catalog/books/shohamleytonbrown2008/fig5_12__original_layout.ef @@ -0,0 +1,15 @@ +% Extensive-form game transcribed from the uploaded image +player 1 name 1 +player 2 name 2 + +% Player 1's two decision nodes are in the same information set. +level 0 node 1 +level 2 node 1 xshift -3 from 0,1 move L +level 2 node 2 xshift 3 from 0,1 move R player 2 + +level 4 node 1 xshift -1 from 2,1 move L payoffs 1 0 +level 4 node 2 xshift 1 from 2,1 move R payoffs 100 100 +level 4 node 3 xshift -1 from 2,2 move U payoffs 5 1 +level 4 node 4 xshift 1 from 2,2 move D payoffs 2 2 + +iset 0,1 2,1 player 1 diff --git a/catalog/books/shohamleytonbrown2008/fig5_15.efg b/catalog/books/shohamleytonbrown2008/fig5_15.efg new file mode 100644 index 000000000..ceb402d94 --- /dev/null +++ b/catalog/books/shohamleytonbrown2008/fig5_15.efg @@ -0,0 +1,17 @@ +EFG 2 R "Fig 5.15 from Shoham and Leyton-Brown (2008)" { "1" "2" } + +" +Figure 5.15 from :cite:p:`ShoLeyB08`. +A game with imperfect information. +This example shows how a requirement that a substrategy be a best response in +all subgames is too simplistic for defining SPE in games with imperfect information. +" + +p "" 1 2 "" { "L" "C" "R" } 0 +t "" 1 "" { 1 1 } +p "" 2 1 "" { "U" "D" } 0 +t "" 2 "" { 0 1000 } +t "" 3 "" { 0 0 } +p "" 2 1 0 +t "" 4 "" { 1 0 } +t "" 5 "" { 3 1 } diff --git a/catalog/books/shohamleytonbrown2008/fig5_15__original_layout.ef b/catalog/books/shohamleytonbrown2008/fig5_15__original_layout.ef new file mode 100644 index 000000000..50d5d3446 --- /dev/null +++ b/catalog/books/shohamleytonbrown2008/fig5_15__original_layout.ef @@ -0,0 +1,11 @@ +player 1 name 1 +player 2 name 2 +level 0 node 1 player 1 +level 2 node 1 xshift -5.01 from 0,1 move L payoffs 1 1 +level 2 node 2 xshift -0.72 from 0,1 move C +level 2 node 3 xshift 5.01 from 0,1 move R +level 4 node 1 xshift -1.43 from 2,2 move U payoffs 0 1000 +level 4 node 2 xshift 1.43 from 2,2 move D payoffs 0 0 +level 4 node 3 xshift -1.43 from 2,3 move U payoffs 1 0 +level 4 node 4 xshift 1.43 from 2,3 move D payoffs 3 1 +iset 2,2 2,3 player 2 diff --git a/catalog/books/shohamleytonbrown2008/fig5_1__original_layout.ef b/catalog/books/shohamleytonbrown2008/fig5_1__original_layout.ef new file mode 100644 index 000000000..492f04754 --- /dev/null +++ b/catalog/books/shohamleytonbrown2008/fig5_1__original_layout.ef @@ -0,0 +1,12 @@ +player 1 name 1 +player 2 name 2 +level 0 node 1 player 1 +level 2 node 1 player 2 xshift -4.77 from 0,1 move 2-0 +level 2 node 2 player 2 xshift 0 from 0,1 move 1-1 +level 4 node 1 xshift -1.19 from 2,1 move no payoffs 0 0 +level 4 node 2 xshift 1.19 from 2,1 move yes payoffs 2 0 +level 2 node 3 player 2 xshift 4.77 from 0,1 move 0-2 +level 4 node 3 xshift -1.19 from 2,2 move no payoffs 0 0 +level 4 node 4 xshift 1.19 from 2,2 move yes payoffs 1 1 +level 4 node 5 xshift -1.19 from 2,3 move no payoffs 0 0 +level 4 node 6 xshift 1.19 from 2,3 move yes payoffs 0 2 diff --git a/catalog/books/shohamleytonbrown2008/fig5_2.efg b/catalog/books/shohamleytonbrown2008/fig5_2.efg new file mode 100644 index 000000000..5ed1bd189 --- /dev/null +++ b/catalog/books/shohamleytonbrown2008/fig5_2.efg @@ -0,0 +1,16 @@ +EFG 2 R "Fig 5.2 from Shoham and Leyton-Brown (2008)" { "1" "2" } + +" +Figure 5.2 from :cite:p:`ShoLeyB08`. +This is an example of a perfect-information game in extensive form. +" + +p "" 1 1 "" { "A" "B" } 0 +p "" 2 2 "" { "C" "D" } 0 +t "" 1 "" { 3 8 } +t "" 2 "" { 8 3 } +p "" 2 3 "" { "E" "F" } 0 +t "" 3 "" { 5 5 } +p "" 1 4 "" { "G" "H" } 0 +t "" 4 "" { 2 10 } +t "" 5 "" { 1 0 } diff --git a/catalog/books/shohamleytonbrown2008/fig5_2__original_layout.ef b/catalog/books/shohamleytonbrown2008/fig5_2__original_layout.ef new file mode 100644 index 000000000..1d55ae617 --- /dev/null +++ b/catalog/books/shohamleytonbrown2008/fig5_2__original_layout.ef @@ -0,0 +1,11 @@ +player 1 name 1 +player 2 name 2 +level 0 node 1 player 1 +level 2 node 1 player 2 xshift -3.22 from 0,1 move A +level 2 node 2 player 2 xshift 3.22 from 0,1 move B +level 4 node 1 xshift -1.43 from 2,1 move C payoffs 3 8 +level 4 node 2 xshift 1.43 from 2,1 move D payoffs 8 3 +level 4 node 3 xshift -2.15 from 2,2 move E payoffs 5 5 +level 4 node 4 player 1 xshift 2.15 from 2,2 move F +level 6 node 1 xshift -1.43 from 4,4 move G payoffs 2 10 +level 6 node 2 xshift 1.43 from 4,4 move H payoffs 1 0 diff --git a/catalog/books/shohamleytonbrown2008/fig5_9.efg b/catalog/books/shohamleytonbrown2008/fig5_9.efg new file mode 100644 index 000000000..861c7c0b7 --- /dev/null +++ b/catalog/books/shohamleytonbrown2008/fig5_9.efg @@ -0,0 +1,22 @@ +EFG 2 R "Fig 5.9 from Shoham and Leyton-Brown (2008)" { "1" "2" } + +" +Figure 5.9 from :cite:p:`ShoLeyB08`. +This is the centipede game. In this game two players alternate in making decisions, at each +turn choosing between going down and ending the game or going across and +continuing it. +This example is used to explain the criticisms of backward induction for finding subgame-perfect equilibrium. +Note that centipede is also a parametrized game, with the parameter being the number of rounds. +" + +p "" 1 1 "" { "A" "D" } 0 +t "" 1 "" { 1 0 } +p "" 2 2 "" { "A" "D" } 0 +t "" 2 "" { 0 2 } +p "" 1 3 "" { "A" "D" } 0 +t "" 3 "" { 3 1 } +p "" 2 4 "" { "A" "D" } 0 +t "" 4 "" { 2 4 } +p "" 1 5 "" { "A" "D" } 0 +t "" 5 "" { 4 3 } +t "" 6 "" { 3 5 } diff --git a/catalog/books/shohamleytonbrown2008/fig5_9__original_layout.ef b/catalog/books/shohamleytonbrown2008/fig5_9__original_layout.ef new file mode 100644 index 000000000..9f477bc52 --- /dev/null +++ b/catalog/books/shohamleytonbrown2008/fig5_9__original_layout.ef @@ -0,0 +1,13 @@ +player 1 name 1 +player 2 name 2 +level 0 node 1 player 1 +level 2 node 1 xshift -2.31 from 0,1 move A payoffs 1 0 +level 2 node 2 player 2 xshift 2.31 from 0,1 move D +level 4 node 1 xshift -2.24 from 2,2 move A payoffs 0 2 +level 4 node 2 player 1 xshift 2.24 from 2,2 move D +level 6 node 1 xshift -2.09 from 4,2 move A payoffs 3 1 +level 6 node 2 player 2 xshift 2.09 from 4,2 move D +level 8 node 1 xshift -1.79 from 6,2 move A payoffs 2 4 +level 8 node 2 player 1 xshift 1.79 from 6,2 move D +level 10 node 1 xshift -1.19 from 8,2 move A payoffs 4 3 +level 10 node 2 xshift 1.19 from 8,2 move D payoffs 3 5 diff --git a/catalog/books/shohamleytonbrown2008/fig6_2.efg b/catalog/books/shohamleytonbrown2008/fig6_2.efg new file mode 100644 index 000000000..6a543e361 --- /dev/null +++ b/catalog/books/shohamleytonbrown2008/fig6_2.efg @@ -0,0 +1,38 @@ +EFG 2 R "Fig 6.2 from Shoham and Leyton-Brown (2008)" { "1" "2" } + +" +Figure 6.2 from :cite:p:`ShoLeyB08`. +This is a repeated game, where Prisoner's Dilemma is played twice. +" + +p "" 1 6 "" { "C" "D" } 0 +p "" 2 1 "" { "c" "d" } 0 +p "" 1 7 "" { "C" "D" } 0 +p "" 2 4 "" { "c" "d" } 0 +t "" 1 "" { -2 -2 } +t "" 2 "" { -5 -1 } +p "" 2 4 0 +t "" 3 "" { -1 -5 } +t "" 4 "" { -4 -4 } +p "" 1 8 "" { "C" "D" } 0 +p "" 2 5 "" { "c" "d" } 0 +t "" 5 "" { -5 -1 } +t "" 6 "" { -8 0 } +p "" 2 5 0 +t "" 7 "" { -4 -4 } +t "" 8 "" { -7 -3 } +p "" 2 1 0 +p "" 1 9 "" { "C" "D" } 0 +p "" 2 2 "" { "c" "d" } 0 +t "" 9 "" { -1 -5 } +t "" 10 "" { -4 -4 } +p "" 2 2 0 +t "" 11 "" { 0 -8 } +t "" 12 "" { -3 -7 } +p "" 1 10 "" { "C" "D" } 0 +p "" 2 3 "" { "c" "d" } 0 +t "" 13 "" { -4 -4 } +t "" 14 "" { -7 -3 } +p "" 2 3 0 +t "" 15 "" { -3 -7 } +t "" 16 "" { -6 -6 } diff --git a/catalog/books/shohamleytonbrown2008/fig6_2__original_layout.ef b/catalog/books/shohamleytonbrown2008/fig6_2__original_layout.ef new file mode 100644 index 000000000..563e3b0f0 --- /dev/null +++ b/catalog/books/shohamleytonbrown2008/fig6_2__original_layout.ef @@ -0,0 +1,38 @@ +player 1 name 1 +player 2 name 2 +level 0 node 1 player 1 +level 2 node 1 xshift -6.37 from 0,1 move C +level 2 node 2 xshift 6.37 from 0,1 move D +level 4 node 1 player 1 xshift -3.19 from 2,1 move c +level 4 node 2 player 1 xshift 3.19 from 2,1 move d +level 4 node 3 player 1 xshift -3.19 from 2,2 move c +level 4 node 4 player 1 xshift 3.19 from 2,2 move d +level 6 node 1 xshift -1.59 from 4,1 move C +level 6 node 2 xshift 1.59 from 4,1 move D +level 6 node 3 xshift -1.59 from 4,2 move C +level 6 node 4 xshift 1.59 from 4,2 move D +level 6 node 5 xshift -1.59 from 4,3 move C +level 6 node 6 xshift 1.59 from 4,3 move D +level 6 node 7 xshift -1.59 from 4,4 move C +level 6 node 8 xshift 1.59 from 4,4 move D +level 8 node 1 xshift -0.8 from 6,1 move c payoffs -2 -2 +level 8 node 2 xshift 0.8 from 6,1 move d payoffs -5 -1 +level 8 node 3 xshift -0.8 from 6,2 move c payoffs -1 -5 +level 8 node 4 xshift 0.8 from 6,2 move d payoffs -4 -4 +level 8 node 5 xshift -0.8 from 6,3 move c payoffs -5 -1 +level 8 node 6 xshift 0.8 from 6,3 move d payoffs -8 0 +level 8 node 7 xshift -0.8 from 6,4 move c payoffs -4 -4 +level 8 node 8 xshift 0.8 from 6,4 move d payoffs -7 -3 +level 8 node 9 xshift -0.8 from 6,5 move c payoffs -1 -5 +level 8 node 10 xshift 0.8 from 6,5 move d payoffs -4 -4 +level 8 node 11 xshift -0.8 from 6,6 move c payoffs 0 -8 +level 8 node 12 xshift 0.8 from 6,6 move d payoffs -3 -7 +level 8 node 13 xshift -0.8 from 6,7 move c payoffs -4 -4 +level 8 node 14 xshift 0.8 from 6,7 move d payoffs -7 -3 +level 8 node 15 xshift -0.8 from 6,8 move c payoffs -3 -7 +level 8 node 16 xshift 0.8 from 6,8 move d payoffs -6 -6 +iset 2,1 2,2 player 2 +iset 6,5 6,6 player 2 +iset 6,7 6,8 player 2 +iset 6,1 6,2 player 2 +iset 6,3 6,4 player 2 diff --git a/catalog/books/shohamleytonbrown2008/fig6_8.efg b/catalog/books/shohamleytonbrown2008/fig6_8.efg new file mode 100644 index 000000000..08e0e3cba --- /dev/null +++ b/catalog/books/shohamleytonbrown2008/fig6_8.efg @@ -0,0 +1,36 @@ +EFG 2 R "Fig 6.8 from Shoham and Leyton-Brown (2008)" { "1" "2" } + +" +Figure 6.8 from :cite:p:`ShoLeyB08`. +This is a Bayesian game, represented in EFG with Nature deciding the types. +" + +c "" 5 "" { "MP" 1/4 "PD" 1/4 "Coord" 1/4 "BoS" 1/4 } 0 +p "" 1 1 "" { "U" "D" } 0 +p "" 2 3 "" { "L" "R" } 0 +t "" 1 "" { 2 0 } +t "" 2 "" { 0 2 } +p "" 2 3 0 +t "" 3 "" { 0 2 } +t "" 4 "" { 2 0 } +p "" 1 1 0 +p "" 2 4 "" { "L" "R" } 0 +t "" 5 "" { 2 2 } +t "" 6 "" { 0 3 } +p "" 2 4 0 +t "" 7 "" { 3 0 } +t "" 8 "" { 1 1 } +p "" 1 2 "" { "U" "D" } 0 +p "" 2 3 0 +t "" 9 "" { 2 2 } +t "" 10 "" { 0 0 } +p "" 2 3 0 +t "" 11 "" { 0 0 } +t "" 12 "" { 1 1 } +p "" 1 2 0 +p "" 2 4 0 +t "" 13 "" { 2 1 } +t "" 14 "" { 0 0 } +p "" 2 4 0 +t "" 15 "" { 0 0 } +t "" 16 "" { 1 2 } diff --git a/catalog/books/shohamleytonbrown2008/fig6_8__original_layout.ef b/catalog/books/shohamleytonbrown2008/fig6_8__original_layout.ef new file mode 100644 index 000000000..a01dfbf0f --- /dev/null +++ b/catalog/books/shohamleytonbrown2008/fig6_8__original_layout.ef @@ -0,0 +1,92 @@ +% Four games with uniform chance and non-overlapping information sets. +% Nature chooses among MP, PD, Coord, and BoS with probability 1/4 each. +% +% The Player 2 information sets are separated vertically: +% - MP and Coord nodes are at level 5. +% - PD and BoS nodes are at level 11. +% +% The additional spacing prevents the U/D labels and the terminal +% payoffs of the upper subtrees from overlapping either information set. + +player 0 name Nature +player 1 name 1 +player 2 name 2 + +% Nature root +level 0 node 1 player 0 + +% Nature moves +% MP and PD belong to Player 1's first information set. +level 2 node 1 xshift -7.5 from 0,1 move MP~(1/4) +level 2 node 2 xshift -2.5 from 0,1 move PD~(1/4) + +% Coord and BoS belong to Player 1's second information set. +level 3 node 3 xshift 2.5 from 0,1 move Coord~(1/4) +level 3 node 4 xshift 7.5 from 0,1 move BoS~(1/4) + +% Player 1 moves U/D + +% Matching Pennies: +% These nodes belong to Player 2 information set 1. +level 5 node 1 xshift -1 from 2,1 move U +level 5 node 2 xshift 1 from 2,1 move D + +% Prisoner's Dilemma: +% These nodes belong to Player 2 information set 2. +level 11 node 3 xshift -1 from 2,2 move U +level 11 node 4 xshift 1 from 2,2 move D + +% Coordination: +% These nodes belong to Player 2 information set 1. +level 5 node 5 xshift -1 from 3,3 move U +level 5 node 6 xshift 1 from 3,3 move D + +% Battle of the Sexes: +% These nodes belong to Player 2 information set 2. +level 11 node 7 xshift -1 from 3,4 move U +level 11 node 8 xshift 1 from 3,4 move D + +% Player 2 moves L/R and terminal payoffs + +% Matching Pennies +% The terminal level is sufficiently above the second information set. +level 8 node 1 xshift -0.55 from 5,1 move L payoffs 2 0 +level 8 node 2 xshift 0.55 from 5,1 move R payoffs 0 2 +level 8 node 3 xshift -0.55 from 5,2 move L payoffs 0 2 +level 8 node 4 xshift 0.55 from 5,2 move R payoffs 2 0 + +% Prisoner's Dilemma +level 14 node 5 xshift -0.55 from 11,3 move L payoffs 2 2 +level 14 node 6 xshift 0.55 from 11,3 move R payoffs 0 3 +level 14 node 7 xshift -0.55 from 11,4 move L payoffs 3 0 +level 14 node 8 xshift 0.55 from 11,4 move R payoffs 1 1 + +% Coordination +% These payoffs are placed at level 8, leaving three levels before +% the lower Player 2 information set at level 11. +level 8 node 9 xshift -0.55 from 5,5 move L payoffs 2 2 +level 8 node 10 xshift 0.55 from 5,5 move R payoffs 0 0 +level 8 node 11 xshift -0.55 from 5,6 move L payoffs 0 0 +level 8 node 12 xshift 0.55 from 5,6 move R payoffs 1 1 + +% Battle of the Sexes +level 14 node 13 xshift -0.55 from 11,7 move L payoffs 2 1 +level 14 node 14 xshift 0.55 from 11,7 move R payoffs 0 0 +level 14 node 15 xshift -0.55 from 11,8 move L payoffs 0 0 +level 14 node 16 xshift 0.55 from 11,8 move R payoffs 1 2 + +% Player 1 information sets + +% Player 1 cannot distinguish MP from PD. +iset 2,1 2,2 player 1 + +% Player 1 cannot distinguish Coordination from Battle of the Sexes. +iset 3,3 3,4 player 1 + +% Player 2 information sets + +% Player 2 cannot distinguish MP from Coordination. +iset 5,1 5,2 5,5 5,6 player 2 + +% Player 2 cannot distinguish PD from Battle of the Sexes. +iset 11,3 11,4 11,7 11,8 player 2 diff --git a/src/pygambit/nash.h b/src/pygambit/nash.h index 4643e4e38..0707488ef 100644 --- a/src/pygambit/nash.h +++ b/src/pygambit/nash.h @@ -22,6 +22,7 @@ #include "gambit.h" #include "solvers/logit/logit.h" +#include "solvers/logit/path.h" using namespace std; using namespace Gambit; @@ -32,8 +33,8 @@ std::list> LogitBehaviorSolveWrapper(const Game &p_ double p_maxAccel) { std::list> ret; - ret.push_back(LogitBehaviorSolve(LogitQREMixedBehaviorProfile(p_game), p_regret, 1.0, - p_firstStep, p_maxAccel) + ret.push_back(LogitBehaviorSolve(LogitQREMixedBehaviorProfile(p_game), p_regret, + PathTracer::TraceDirection::Positive, p_firstStep, p_maxAccel) .back() .GetProfile()); return ret; @@ -43,16 +44,17 @@ inline std::list LogitBehaviorPrincipalBranchWrapper(const Game &p_game, double p_regret, double p_firstStep, double p_maxAccel) { - return LogitBehaviorSolve(LogitQREMixedBehaviorProfile(p_game), p_regret, 1.0, p_firstStep, - p_maxAccel); + return LogitBehaviorSolve(LogitQREMixedBehaviorProfile(p_game), p_regret, + PathTracer::TraceDirection::Positive, p_firstStep, p_maxAccel); } std::shared_ptr LogitBehaviorEstimateWrapper(std::shared_ptr> p_frequencies, bool p_stopAtLocal, double p_firstStep, double p_maxAccel) { - return make_shared(LogitBehaviorEstimate( - *p_frequencies, 1000000.0, 1.0, p_stopAtLocal, p_firstStep, p_maxAccel)); + return make_shared( + LogitBehaviorEstimate(*p_frequencies, 1000000.0, PathTracer::TraceDirection::Positive, + p_stopAtLocal, p_firstStep, p_maxAccel)); } std::list> @@ -61,7 +63,8 @@ LogitBehaviorAtLambdaWrapper(const Game &p_game, const std::list &p_targ { LogitQREMixedBehaviorProfile start(p_game); std::list> ret; - for (auto &qre : LogitBehaviorSolveLambda(start, p_targetLambda, 1.0, p_firstStep, p_maxAccel)) { + for (auto &qre : LogitBehaviorSolveLambda( + start, p_targetLambda, PathTracer::TraceDirection::Positive, p_firstStep, p_maxAccel)) { ret.push_back(std::make_shared(qre)); } return ret; @@ -73,8 +76,8 @@ std::list> LogitStrategySolveWrapper(const Game &p_ double p_maxAccel) { std::list> ret; - ret.push_back(LogitStrategySolve(LogitQREMixedStrategyProfile(p_game), p_regret, 1.0, - p_firstStep, p_maxAccel) + ret.push_back(LogitStrategySolve(LogitQREMixedStrategyProfile(p_game), p_regret, + PathTracer::TraceDirection::Positive, p_firstStep, p_maxAccel) .back() .GetProfile()); return ret; @@ -84,8 +87,8 @@ inline std::list LogitStrategyPrincipalBranchWrapper(const Game &p_game, double p_regret, double p_firstStep, double p_maxAccel) { - return LogitStrategySolve(LogitQREMixedStrategyProfile(p_game), p_regret, 1.0, p_firstStep, - p_maxAccel); + return LogitStrategySolve(LogitQREMixedStrategyProfile(p_game), p_regret, + PathTracer::TraceDirection::Positive, p_firstStep, p_maxAccel); } std::list> @@ -94,7 +97,8 @@ LogitStrategyAtLambdaWrapper(const Game &p_game, const std::list &p_targ { LogitQREMixedStrategyProfile start(p_game); std::list> ret; - for (auto &qre : LogitStrategySolveLambda(start, p_targetLambda, 1.0, p_firstStep, p_maxAccel)) { + for (auto &qre : LogitStrategySolveLambda( + start, p_targetLambda, PathTracer::TraceDirection::Positive, p_firstStep, p_maxAccel)) { ret.push_back(std::make_shared(qre)); } return ret; @@ -104,6 +108,7 @@ std::shared_ptr LogitStrategyEstimateWrapper(std::shared_ptr> p_frequencies, bool p_stopAtLocal, double p_firstStep, double p_maxAccel) { - return make_shared(LogitStrategyEstimate( - *p_frequencies, 1000000.0, 1.0, p_stopAtLocal, p_firstStep, p_maxAccel)); + return make_shared( + LogitStrategyEstimate(*p_frequencies, 1000000.0, PathTracer::TraceDirection::Positive, + p_stopAtLocal, p_firstStep, p_maxAccel)); } diff --git a/src/solvers/hp/hp.cc b/src/solvers/hp/hp.cc index d9ae74523..1f211b855 100644 --- a/src/solvers/hp/hp.cc +++ b/src/solvers/hp/hp.cc @@ -20,7 +20,6 @@ // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. // -#include #include "gambit.h" #include "solvers/hp/hp.h" #include "solvers/hp/hpsystem.h" @@ -37,7 +36,9 @@ HPStrategySolve(const MixedStrategyProfile &p_prior) Vector x = system.ComputeInitialPoint(); const PathTracer tracer; - double omega = 1.0; + const PathTracer::TraceDirection direction = PathTracer::TraceDirection::Positive; + const size_t tracking_index = 1; // Track the first variable (t) for orientation + const double t_target = 1.0; const double tol = 1e-8; double last_t = 0.0; @@ -83,9 +84,9 @@ HPStrategySolve(const MixedStrategyProfile &p_prior) [&system](const Vector &point, Matrix &jac) { system.GetJacobian(point, jac); }, - x, omega, termination_condition, NullCallbackFunction, criterion_function); + x, direction, tracking_index, termination_condition, NullCallbackFunction, + criterion_function); - int polish_step = 1; const PolishResult polishing_result = PolishPoint( [&system](const Vector &point, Vector &lhs) { system.GetValue(point, lhs); }, [&system](const Vector &point, Matrix &jac) { diff --git a/src/solvers/logit/efglogit.cc b/src/solvers/logit/efglogit.cc index cae2f769d..280bafc89 100644 --- a/src/solvers/logit/efglogit.cc +++ b/src/solvers/logit/efglogit.cc @@ -309,8 +309,8 @@ void EstimatorCallbackFunction::EvaluatePoint(const Vector &p_point) namespace Gambit { std::list -LogitBehaviorSolve(const LogitQREMixedBehaviorProfile &p_start, double p_regret, double p_omega, - double p_firstStep, double p_maxAccel, +LogitBehaviorSolve(const LogitQREMixedBehaviorProfile &p_start, double p_regret, + PathTracer::TraceDirection p_direction, double p_firstStep, double p_maxAccel, MixedBehaviorObserverFunctionType p_observer) { if (p_start.size() == 0) { @@ -335,7 +335,7 @@ LogitBehaviorSolve(const LogitQREMixedBehaviorProfile &p_start, double p_regret, [&system](const Vector &p_point, Matrix &p_jac) { system.GetJacobian(p_point, p_jac); }, - x, p_omega, + x, p_direction, x.size(), [game, p_regret](const Vector &p_point) { return RegretTerminationFunction(game, p_point, p_regret); }, @@ -345,9 +345,9 @@ LogitBehaviorSolve(const LogitQREMixedBehaviorProfile &p_start, double p_regret, std::list LogitBehaviorSolveLambda(const LogitQREMixedBehaviorProfile &p_start, - const std::list &p_targetLambda, double p_omega, - double p_firstStep, double p_maxAccel, - MixedBehaviorObserverFunctionType p_observer) + const std::list &p_targetLambda, + PathTracer::TraceDirection p_direction, double p_firstStep, + double p_maxAccel, MixedBehaviorObserverFunctionType p_observer) { if (p_start.size() == 0) { return {p_start}; @@ -369,7 +369,7 @@ LogitBehaviorSolveLambda(const LogitQREMixedBehaviorProfile &p_start, [&system](const Vector &p_point, Matrix &p_jac) { system.GetJacobian(p_point, p_jac); }, - x, p_omega, LambdaPositiveTerminationFunction, + x, p_direction, x.size(), LambdaPositiveTerminationFunction, [&callback](const Vector &p_point) -> void { callback.AppendPoint(p_point); }, [lam](const Vector &x, const Vector &) -> double { return x.back() - lam; @@ -381,7 +381,8 @@ LogitBehaviorSolveLambda(const LogitQREMixedBehaviorProfile &p_start, LogitQREMixedBehaviorProfile LogitBehaviorEstimate(const MixedBehaviorProfile &p_frequencies, double p_maxLambda, - double p_omega, double p_stopAtLocal, double p_firstStep, double p_maxAccel, + PathTracer::TraceDirection p_direction, double p_stopAtLocal, + double p_firstStep, double p_maxAccel, MixedBehaviorObserverFunctionType p_observer) { const LogitQREMixedBehaviorProfile start(p_frequencies.GetGame()); @@ -405,7 +406,7 @@ LogitBehaviorEstimate(const MixedBehaviorProfile &p_frequencies, double [&system](const Vector &p_point, Matrix &p_jac) { system.GetJacobian(p_point, p_jac); }, - x, p_omega, + x, p_direction, x.size(), [p_maxLambda](const Vector &p_point) { return LambdaRangeTerminationFunction(p_point, 0, p_maxLambda); }, diff --git a/src/solvers/logit/logit.h b/src/solvers/logit/logit.h index 8d619d44e..a2d19fcd4 100644 --- a/src/solvers/logit/logit.h +++ b/src/solvers/logit/logit.h @@ -24,6 +24,7 @@ #define SOLVERS_LOGIT_H #include +#include "solvers/logit/path.h" namespace Gambit { @@ -85,18 +86,19 @@ using MixedStrategyObserverFunctionType = inline void NullMixedStrategyObserver(const LogitQREMixedStrategyProfile &) {} std::list LogitStrategySolve( - const LogitQREMixedStrategyProfile &p_start, double p_regret, double p_omega, - double p_firstStep, double p_maxAccel, + const LogitQREMixedStrategyProfile &p_start, double p_regret, + PathTracer::TraceDirection p_direction, double p_firstStep, double p_maxAccel, const MixedStrategyObserverFunctionType &p_observer = NullMixedStrategyObserver); std::list LogitStrategySolveLambda( const LogitQREMixedStrategyProfile &p_start, const std::list &p_targetLambda, - double p_omega, double p_firstStep, double p_maxAccel, + PathTracer::TraceDirection p_direction, double p_firstStep, double p_maxAccel, const MixedStrategyObserverFunctionType &p_observer = NullMixedStrategyObserver); LogitQREMixedStrategyProfile LogitStrategyEstimate(const MixedStrategyProfile &p_frequencies, double p_maxLambda, - double p_omega, double p_stopAtLocal, double p_firstStep, double p_maxAccel, + PathTracer::TraceDirection p_direction, double p_stopAtLocal, + double p_firstStep, double p_maxAccel, MixedStrategyObserverFunctionType p_observer = NullMixedStrategyObserver); using LogitQREMixedBehaviorProfile = LogitQRE>; @@ -107,19 +109,19 @@ using MixedBehaviorObserverFunctionType = inline void NullMixedBehaviorObserver(const LogitQREMixedBehaviorProfile &) {} std::list -LogitBehaviorSolve(const LogitQREMixedBehaviorProfile &p_start, double p_regret, double p_omega, - double p_firstStep, double p_maxAccel, +LogitBehaviorSolve(const LogitQREMixedBehaviorProfile &p_start, double p_regret, + PathTracer::TraceDirection p_direction, double p_firstStep, double p_maxAccel, MixedBehaviorObserverFunctionType p_observer = NullMixedBehaviorObserver); -std::list -LogitBehaviorSolveLambda(const LogitQREMixedBehaviorProfile &p_start, - const std::list &p_targetLambda, double p_omega, - double p_firstStep, double p_maxAccel, - MixedBehaviorObserverFunctionType p_observer = NullMixedBehaviorObserver); +std::list LogitBehaviorSolveLambda( + const LogitQREMixedBehaviorProfile &p_start, const std::list &p_targetLambda, + PathTracer::TraceDirection p_direction, double p_firstStep, double p_maxAccel, + MixedBehaviorObserverFunctionType p_observer = NullMixedBehaviorObserver); LogitQREMixedBehaviorProfile LogitBehaviorEstimate(const MixedBehaviorProfile &p_frequencies, double p_maxLambda, - double p_omega, double p_stopAtLocal, double p_firstStep, double p_maxAccel, + PathTracer::TraceDirection p_direction, double p_stopAtLocal, + double p_firstStep, double p_maxAccel, MixedBehaviorObserverFunctionType p_observer = NullMixedBehaviorObserver); } // namespace Gambit diff --git a/src/solvers/logit/nfglogit.cc b/src/solvers/logit/nfglogit.cc index e8e9f8167..224e9566d 100644 --- a/src/solvers/logit/nfglogit.cc +++ b/src/solvers/logit/nfglogit.cc @@ -347,8 +347,8 @@ void EstimatorCallbackFunction::EvaluatePoint(const Vector &p_point) } // namespace std::list -LogitStrategySolve(const LogitQREMixedStrategyProfile &p_start, double p_regret, double p_omega, - double p_firstStep, double p_maxAccel, +LogitStrategySolve(const LogitQREMixedStrategyProfile &p_start, double p_regret, + PathTracer::TraceDirection p_direction, double p_firstStep, double p_maxAccel, const MixedStrategyObserverFunctionType &p_observer) { if (p_start.size() == 0) { @@ -374,7 +374,7 @@ LogitStrategySolve(const LogitQREMixedStrategyProfile &p_start, double p_regret, [&system](const Vector &p_point, Matrix &p_jac) { system.GetJacobian(p_point, p_jac); }, - x, p_omega, + x, p_direction, x.size(), [p_start, p_regret](const Vector &p_point) { return RegretTerminationFunction(p_start.GetGame(), p_point, p_regret); }, @@ -384,9 +384,9 @@ LogitStrategySolve(const LogitQREMixedStrategyProfile &p_start, double p_regret, std::list LogitStrategySolveLambda(const LogitQREMixedStrategyProfile &p_start, - const std::list &p_targetLambda, double p_omega, - double p_firstStep, double p_maxAccel, - const MixedStrategyObserverFunctionType &p_observer) + const std::list &p_targetLambda, + PathTracer::TraceDirection p_direction, double p_firstStep, + double p_maxAccel, const MixedStrategyObserverFunctionType &p_observer) { if (p_start.size() == 0) { return {p_start}; @@ -408,7 +408,7 @@ LogitStrategySolveLambda(const LogitQREMixedStrategyProfile &p_start, [&system](const Vector &p_point, Matrix &p_jac) { system.GetJacobian(p_point, p_jac); }, - x, p_omega, LambdaPositiveTerminationFunction, + x, p_direction, x.size(), LambdaPositiveTerminationFunction, [&callback](const Vector &p_point) -> void { callback.AppendPoint(p_point); }, [lam](const Vector &x, const Vector &) -> double { return x.back() - lam; @@ -420,7 +420,8 @@ LogitStrategySolveLambda(const LogitQREMixedStrategyProfile &p_start, LogitQREMixedStrategyProfile LogitStrategyEstimate(const MixedStrategyProfile &p_frequencies, double p_maxLambda, - double p_omega, double p_stopAtLocal, double p_firstStep, double p_maxAccel, + PathTracer::TraceDirection p_direction, double p_stopAtLocal, + double p_firstStep, double p_maxAccel, MixedStrategyObserverFunctionType p_observer) { const LogitQREMixedStrategyProfile start(p_frequencies.GetGame()); @@ -444,7 +445,7 @@ LogitStrategyEstimate(const MixedStrategyProfile &p_frequencies, double [&system](const Vector &p_point, Matrix &p_jac) { system.GetJacobian(p_point, p_jac); }, - x, p_omega, + x, p_direction, x.size(), [p_maxLambda](const Vector &p_point) { return LambdaRangeTerminationFunction(p_point, 0, p_maxLambda); }, diff --git a/src/solvers/logit/path.cc b/src/solvers/logit/path.cc index 9ecbef22e..e34933073 100644 --- a/src/solvers/logit/path.cc +++ b/src/solvers/logit/path.cc @@ -38,16 +38,16 @@ inline double sqr(double x) { return x * x; } void Givens(Matrix &b, Matrix &q, double &c1, double &c2, int l1, int l2, int l3) { - if (fabs(c1) + fabs(c2) == 0.0) { + if (std::abs(c1) + std::abs(c2) == 0.0) { return; } double sn; - if (fabs(c2) >= fabs(c1)) { - sn = std::sqrt(1.0 + sqr(c1 / c2)) * fabs(c2); + if (std::abs(c2) >= std::abs(c1)) { + sn = std::sqrt(1.0 + sqr(c1 / c2)) * std::abs(c2); } else { - sn = std::sqrt(1.0 + sqr(c2 / c1)) * fabs(c1); + sn = std::sqrt(1.0 + sqr(c2 / c1)) * std::abs(c1); } const double s1 = c1 / sn; const double s2 = c2 / sn; @@ -128,8 +128,9 @@ void NewtonStep(Matrix &q, Matrix &b, Vector &u, Vector< TracePathResult PathTracer::TracePath(std::function &, Vector &)> p_function, std::function &, Matrix &)> p_jacobian, - Vector &x, double &p_omega, TerminationFunctionType p_terminate, - CallbackFunctionType p_callback, CriterionFunctionType p_criterion, + Vector &x, TraceDirection p_direction, size_t p_trackingIndex, + TerminationFunctionType p_terminate, CallbackFunctionType p_callback, + CriterionFunctionType p_criterion, CriterionBracketFunctionType p_criterionBracket) const { const double c_tol = 1.0e-4; // tolerance for corrector iteration @@ -146,6 +147,7 @@ PathTracer::TracePath(std::function &, Vector const double c_pert = 0.0000001; // The size of perturbation to apply to avoid bifurcation traps double pert = 0.0; // The current version of the perturbation being applied double pert_countdown = 0.0; // How much longer (in arclength) to apply perturbation + const double c_orientTol = 1.0e-8; // tolerance for detecting change in orientation Vector u(x.size()); // t is current tangent at x; newT is tangent at u, which is the next point. @@ -158,13 +160,19 @@ PathTracer::TracePath(std::function &, Vector QRDecomp(b, q); q.GetRow(q.NumRows(), t); p_callback(x); - int steps = 0; + bool first_step = true; + double omega = (p_direction == TraceDirection::Positive) ? 1.0 : -1.0; + + if (p_trackingIndex > x.size() || p_trackingIndex < 1) { + return {x, false, "Tracking index exceeds dimension of point vector.", steps}; + } + while (!p_terminate(x)) { bool accept = true; - if (fabs(h) <= c_hmin) { + if (std::abs(h) <= c_hmin) { if (newton && std::abs(p_criterion(x, t)) < c_newtonTol) { return {x, true, "Path following terminated successfully at point satisfying criterion function.", @@ -175,9 +183,22 @@ PathTracer::TracePath(std::function &, Vector } } + if (first_step) { + if (std::abs(t[p_trackingIndex]) <= c_orientTol) { + return {x, false, "Initial tangent vector is orthogonal to path-following direction.", + steps}; + } + // Ensure that the tangent is oriented in the same direction as + // the path-following direction. + else if (t[p_trackingIndex] < -c_orientTol) { + omega *= -1.0; + } + first_step = false; + } + // Predictor step for (size_t k = 1; k <= x.size(); k++) { - u[k] = x[k] + h * p_omega * t[k]; + u[k] = x[k] + h * omega * t[k]; } double decel = 1.0 / m_maxDecel; // initialize deceleration factor @@ -229,14 +250,14 @@ PathTracer::TracePath(std::function &, Vector // is oriented in the same direction as we were originally following if (pert_countdown == 0.0) { pert = c_pert; - pert_countdown = abs(2 * h); + pert_countdown = std::abs(2 * h); } accept = false; } if (!accept) { h /= m_maxDecel; // PC not accepted; change stepsize and retry - if (fabs(h) <= c_hmin) { + if (std::abs(h) <= c_hmin) { if (newton && std::abs(p_criterion(x, t)) < c_newtonTol) { return {x, true, "Path following terminated successfully at point satisfying criterion function.", @@ -268,7 +289,7 @@ PathTracer::TracePath(std::function &, Vector } else { // Standard steplength adaptation - h = fabs(h / decel); + h = std::abs(h / decel); } // PC step was successful; update and iterate diff --git a/src/solvers/logit/path.h b/src/solvers/logit/path.h index 446383c24..dd6b2375f 100644 --- a/src/solvers/logit/path.h +++ b/src/solvers/logit/path.h @@ -79,6 +79,7 @@ struct PolishResult { // class PathTracer { public: + enum class TraceDirection { Positive = 1, Negative = -1 }; PathTracer() = default; virtual ~PathTracer() = default; @@ -91,7 +92,8 @@ class PathTracer { TracePathResult TracePath(std::function &, Vector &)> p_function, std::function &, Matrix &)> p_jacobian, - Vector &p_x, double &p_omega, TerminationFunctionType p_terminate, + Vector &p_x, TraceDirection p_direction, size_t p_trackingIndex, + TerminationFunctionType p_terminate, CallbackFunctionType p_callback = NullCallbackFunction, CriterionFunctionType p_criterion = NullCriterionFunction, CriterionBracketFunctionType p_criterionBracker = NullCriterionBracketFunction) const; diff --git a/src/tools/logit/logit.cc b/src/tools/logit/logit.cc index a10fbebd2..01f1a93ef 100644 --- a/src/tools/logit/logit.cc +++ b/src/tools/logit/logit.cc @@ -206,7 +206,8 @@ int main(int argc, char *argv[]) } }; auto result = - LogitStrategyEstimate(frequencies, maxLambda, 1.0, false, hStart, maxDecel, printer); + LogitStrategyEstimate(frequencies, maxLambda, PathTracer::TraceDirection::Positive, + false, hStart, maxDecel, printer); PrintProfile(std::cout, decimals, result); return 0; } @@ -219,14 +220,15 @@ int main(int argc, char *argv[]) }; const LogitQREMixedStrategyProfile start(game); if (!targetLambda.empty()) { - auto result = - LogitStrategySolveLambda(start, targetLambda, 1.0, hStart, maxDecel, printer); + auto result = LogitStrategySolveLambda( + start, targetLambda, PathTracer::TraceDirection::Positive, hStart, maxDecel, printer); for (auto &profile : result) { PrintProfile(std::cout, decimals, profile); } } else { - auto result = LogitStrategySolve(start, maxregret, 1.0, hStart, maxDecel, printer); + auto result = LogitStrategySolve(start, maxregret, PathTracer::TraceDirection::Positive, + hStart, maxDecel, printer); PrintProfile(std::cout, decimals, result.back(), true); } } @@ -238,14 +240,15 @@ int main(int argc, char *argv[]) }; const LogitQREMixedBehaviorProfile start(game); if (!targetLambda.empty()) { - auto result = - LogitBehaviorSolveLambda(start, targetLambda, 1.0, hStart, maxDecel, printer); + auto result = LogitBehaviorSolveLambda( + start, targetLambda, PathTracer::TraceDirection::Positive, hStart, maxDecel, printer); for (auto &profile : result) { PrintProfile(std::cout, decimals, profile); } } else { - auto result = LogitBehaviorSolve(start, maxregret, 1.0, hStart, maxDecel, printer); + auto result = LogitBehaviorSolve(start, maxregret, PathTracer::TraceDirection::Positive, + hStart, maxDecel, printer); PrintProfile(std::cout, decimals, result.back(), true); } } diff --git a/tests/test_hp.py b/tests/test_hp.py index 4b6af3cca..3aca424c5 100644 --- a/tests/test_hp.py +++ b/tests/test_hp.py @@ -1,6 +1,7 @@ """Test of calls to the Herings & Peeters (2001) homotopy solver.""" import dataclasses +import functools import typing import numpy as np @@ -8,7 +9,9 @@ import pygambit as gbt -TOL = 1e-6 +from . import games + +TOL = 1e-8 def d(*probs) -> tuple: @@ -26,6 +29,26 @@ class HPSolverTestCase: prob_tol: float = TOL +def create_prior_from_catalog(game_path: str) -> gbt.MixedStrategyProfile: + """Loads a game from the catalog and returns a prior that is uniform over all strategies.""" + game = gbt.catalog.load(game_path) + return game.mixed_strategy_profile() + + +def load_game_from_file(file_name: str) -> gbt.MixedStrategyProfile: + """Loads a game from a file in tests/test_games""" + return games.read_from_file(file_name) + + +def check_equilibrium(result, subtests) -> None: + """Checks that the result of the HP solver is a valid Nash equilibrium.""" + with subtests.test("number of equilibria found"): + assert len(result.equilibria) == 1 + + with subtests.test("valid Nash equilibrium (max_regret <= TOL)"): + assert result.equilibria[0].max_regret() <= TOL + + def create_hs_base_game() -> gbt.Game: """Creates the base 2x2 game used in all examples from Harsanyi & Selten (1988) Section 4.11 and also featured in Herings & Peeters (2001). @@ -78,6 +101,72 @@ def create_t0_degenerate_example() -> gbt.MixedStrategyProfileDouble: return prior +def create_test_hp_2x2x2_nfg_eq1() -> gbt.MixedStrategyProfileDouble: + """Creates a prior for the 2x2x2.nfg game that is exactly the first equilibrium.""" + game = load_game_from_file("../../contrib/games/2x2x2.nfg") + return game.mixed_strategy_profile(data=[(1.0, 0.0), (1.0, 0.0), (1.0, 0.0)]) + + +def create_test_hp_2x2x2_nfg_eq1_perturbed() -> gbt.MixedStrategyProfileDouble: + """Creates a prior for the 2x2x2.nfg game that is the first equilibrium perturbed.""" + game = load_game_from_file("../../contrib/games/2x2x2.nfg") + return game.mixed_strategy_profile(data=[(0.9, 0.1), (0.9, 0.1), (0.9, 0.1)]) + + +def create_test_hp_2x2x2_nfg_eq2() -> gbt.MixedStrategyProfileDouble: + """Creates a prior for the 2x2x2.nfg game that is exactly the second pure equilibrium.""" + game = load_game_from_file("../../contrib/games/2x2x2.nfg") + return game.mixed_strategy_profile(data=[(1.0, 0.0), (0.0, 1.0), (0.0, 1.0)]) + + +def create_test_hp_2x2x2_nfg_eq2_perturbed() -> gbt.MixedStrategyProfileDouble: + """Creates a prior for the 2x2x2.nfg game that is the second equilibrium perturbed.""" + game = load_game_from_file("../../contrib/games/2x2x2.nfg") + return game.mixed_strategy_profile(data=[(0.9, 0.1), (0.1, 0.9), (0.1, 0.9)]) + + +def create_test_hp_2x2x2_nfg_eq3() -> gbt.MixedStrategyProfileDouble: + """Creates a prior for the 2x2x2.nfg game that is exactly the third pure equilibrium.""" + game = load_game_from_file("../../contrib/games/2x2x2.nfg") + return game.mixed_strategy_profile(data=[(0.0, 1.0), (1.0, 0.0), (0.0, 1.0)]) + + +def create_test_hp_2x2x2_nfg_eq3_perturbed() -> gbt.MixedStrategyProfileDouble: + """Creates a prior for the 2x2x2.nfg game that is the third equilibrium perturbed.""" + game = load_game_from_file("../../contrib/games/2x2x2.nfg") + return game.mixed_strategy_profile(data=[(0.1, 0.9), (0.9, 0.1), (0.1, 0.9)]) + + +def create_test_hp_2x2x2_nfg_eq4() -> gbt.MixedStrategyProfileDouble: + """Creates a prior for the 2x2x2.nfg game that is exactly the fourth pure equilibrium.""" + game = load_game_from_file("../../contrib/games/2x2x2.nfg") + return game.mixed_strategy_profile(data=[(0.0, 1.0), (0.0, 1.0), (1.0, 0.0)]) + + +def create_test_hp_2x2x2_nfg_eq4_perturbed() -> gbt.MixedStrategyProfileDouble: + """Creates a prior for the 2x2x2.nfg game that is the fourth equilibrium perturbed.""" + game = load_game_from_file("../../contrib/games/2x2x2.nfg") + return game.mixed_strategy_profile(data=[(0.1, 0.9), (0.1, 0.9), (0.9, 0.1)]) + + +def create_test_hp_2x2x2_nfg_eq5_perturbed() -> gbt.MixedStrategyProfileDouble: + """Creates a perturbed prior near the fifth equilibrium (partially mixed).""" + game = load_game_from_file("../../contrib/games/2x2x2.nfg") + return game.mixed_strategy_profile(data=[(0.51, 0.49), (0.49, 0.51), (0.9, 0.1)]) + + +def create_test_hp_2x2x2_nfg_eq8_perturbed() -> gbt.MixedStrategyProfileDouble: + """Creates a perturbed prior near the eighth equilibrium (totally mixed).""" + game = load_game_from_file("../../contrib/games/2x2x2.nfg") + return game.mixed_strategy_profile(data=[(0.51, 0.49), (0.41, 0.59), (0.26, 0.74)]) + + +def create_test_hp_2x2x2_nfg_eq9_perturbed() -> gbt.MixedStrategyProfileDouble: + """Creates a perturbed prior near the ninth equilibrium (totally mixed).""" + game = load_game_from_file("../../contrib/games/2x2x2.nfg") + return game.mixed_strategy_profile(data=[(0.41, 0.59), (0.51, 0.49), (0.34, 0.66)]) + + HP_CASES = [ pytest.param( HPSolverTestCase( @@ -93,6 +182,94 @@ def create_t0_degenerate_example() -> gbt.MixedStrategyProfileDouble: ), id="test_hp_hs_example_1", ), + pytest.param( + HPSolverTestCase( + factory=functools.partial(create_prior_from_catalog, "books/myerson1991/fig2_1"), + expected=[d(1.0 / 3.0, 2.0 / 3.0, 0.0, 0.0), d(2.0 / 3.0, 1.0 / 3.0)], + ), + id="test_hp_myerson1991_fig2_1", + ), + pytest.param( + HPSolverTestCase( + factory=functools.partial(create_prior_from_catalog, "books/vonstengel2022/fig10.1"), + expected=[d(0.0, 0.5, 0, 0.5), d(1.0 / 4.0, 3.0 / 4.0)], + ), + id="test_hp_vonstengel2022_fig10.1", + ), + pytest.param( + HPSolverTestCase( + factory=functools.partial(create_prior_from_catalog, "journals/ijgt/nau2004/sec4"), + expected=[ + d(0.6192325794725538, 0.38076742052744617), + d(0.4798042226776052, 0.5201957773223949), + d(0.37882533606563146, 0.6211746639343685) + ], + ), + id="test_hp_nau2004_sec4", + ), + pytest.param( + HPSolverTestCase( + factory=functools.partial(create_prior_from_catalog, "journals/other/reiley2008/fig1"), + expected=[d(1.0 / 3.0, 2.0 / 3.0, 0.0, 0.0), d(2.0 / 3.0, 1.0 / 3.0)], + ), + id="test_hp_reiley2008_fig1", + ), + pytest.param( + HPSolverTestCase( + factory=create_test_hp_2x2x2_nfg_eq1, + expected=[d(1.0, 0.0), d(1.0, 0.0), d(1.0, 0.0)], + ), + id="test_hp_2x2x2_nfg_eq1", + ), + pytest.param( + HPSolverTestCase( + factory=create_test_hp_2x2x2_nfg_eq1_perturbed, + expected=[d(1.0, 0.0), d(1.0, 0.0), d(1.0, 0.0)], + ), + id="test_hp_2x2x2_nfg_eq1_perturbed", + ), + pytest.param( + HPSolverTestCase( + factory=create_test_hp_2x2x2_nfg_eq2, + expected=[d(1.0, 0.0), d(0.0, 1.0), d(0.0, 1.0)], + ), + id="test_hp_2x2x2_nfg_eq2", + ), + pytest.param( + HPSolverTestCase( + factory=create_test_hp_2x2x2_nfg_eq2_perturbed, + expected=[d(1.0, 0.0), d(0.0, 1.0), d(0.0, 1.0)], + ), + id="test_hp_2x2x2_nfg_eq2_perturbed", + ), + pytest.param( + HPSolverTestCase( + factory=create_test_hp_2x2x2_nfg_eq3, + expected=[d(0.0, 1.0), d(1.0, 0.0), d(0.0, 1.0)], + ), + id="test_hp_2x2x2_nfg_eq3", + ), + pytest.param( + HPSolverTestCase( + factory=create_test_hp_2x2x2_nfg_eq3_perturbed, + expected=[d(0.0, 1.0), d(1.0, 0.0), d(0.0, 1.0)], + ), + id="test_hp_2x2x2_nfg_eq3_perturbed", + ), + pytest.param( + HPSolverTestCase( + factory=create_test_hp_2x2x2_nfg_eq4, + expected=[d(0.0, 1.0), d(0.0, 1.0), d(1.0, 0.0)], + ), + id="test_hp_2x2x2_nfg_eq4", + ), + pytest.param( + HPSolverTestCase( + factory=create_test_hp_2x2x2_nfg_eq4_perturbed, + expected=[d(0.0, 1.0), d(0.0, 1.0), d(1.0, 0.0)], + ), + id="test_hp_2x2x2_nfg_eq4_perturbed", + ), ] @@ -130,3 +307,169 @@ def test_hp_degenerate_t0_prior_raises_error() -> None: with pytest.raises(RuntimeError, match="Multiple best responses found for player 1. " "Only one best response is allowed."): gbt.nash.hp_solve(prior=prior) + + +@pytest.mark.nash +def test_hp_degenerate_t0_prior_selten1975_fig2() -> None: + game = gbt.catalog.load("journals/ijgt/selten1975/fig2") + prior = game.mixed_strategy_profile() + with pytest.raises(RuntimeError, match="Multiple best responses found for player 1. " + "Only one best response is allowed."): + gbt.nash.hp_solve(prior=prior) + + +@pytest.mark.nash +def test_hp_2x2x2_nfg_eq5_perturbed(subtests) -> None: + """Test convergence from a perturbed prior near eq 5 using max regret.""" + prior = create_test_hp_2x2x2_nfg_eq5_perturbed() + result = gbt.nash.hp_solve(prior=prior) + check_equilibrium(result, subtests) + + +@pytest.mark.nash +def test_hp_2x2x2_nfg_eq8_perturbed(subtests) -> None: + """Test convergence from a perturbed prior near eq 8 using max regret.""" + prior = create_test_hp_2x2x2_nfg_eq8_perturbed() + result = gbt.nash.hp_solve(prior=prior) + check_equilibrium(result, subtests) + + +@pytest.mark.nash +def test_hp_2x2x2_nfg_eq9_perturbed(subtests) -> None: + """Test convergence from a perturbed prior near eq 9 using max regret.""" + prior = create_test_hp_2x2x2_nfg_eq9_perturbed() + result = gbt.nash.hp_solve(prior=prior) + check_equilibrium(result, subtests) + + +CATALOG_GAMES_TO_TEST = [ + pytest.param( + "books/myerson1991/fig2_1", + id="catalog games that converge to a NE - books/myerson1991/fig2_1" + ), + pytest.param( + "books/shohamleytonbrown2008/fig5_10", + id="catalog games that converge to a NE - books/shohamleytonbrown2008/fig5_10" + ), + pytest.param( + "books/shohamleytonbrown2008/fig5_11", + id="catalog games that converge to a NE - books/shohamleytonbrown2008/fig5_11" + ), + pytest.param( + "books/shohamleytonbrown2008/fig5_15", + id="catalog games that converge to a NE - books/shohamleytonbrown2008/fig5_15" + ), + pytest.param( + "books/shohamleytonbrown2008/fig6_2", + id="catalog games that converge to a NE - books/shohamleytonbrown2008/fig6_2" + ), + pytest.param( + "books/shohamleytonbrown2008/fig6_8", + id="catalog games that converge to a NE - books/shohamleytonbrown2008/fig6_8" + ), + pytest.param( + "books/vonstengel2022/fig10.1", + id="catalog games that converge to a NE - books/vonstengel2022/fig10.1" + ), + pytest.param( + "books/watson2013/fig29_1", + id="catalog games that converge to a NE - books/watson2013/fig29_1" + ), + pytest.param( + "conf/itcs/jakobsen2016/fig1c", + id="catalog games that converge to a NE - conf/itcs/jakobsen2016/fig1c" + ), + pytest.param( + "conf/itcs/jakobsen2016/fig3", + id="catalog games that converge to a NE - conf/itcs/jakobsen2016/fig3" + ), + pytest.param( + "journals/geb/bagwell1995", + id="catalog games that converge to a NE - journals/geb/bagwell1995" + ), + pytest.param( + "journals/ijgt/nau2004/sec3", + id="catalog games that converge to a NE - journals/ijgt/nau2004/sec3" + ), + pytest.param( + "journals/ijgt/nau2004/sec4", + id="catalog games that converge to a NE - journals/ijgt/nau2004/sec4" + ), + pytest.param( + "journals/ijgt/nau2004/sec5", + id="catalog games that converge to a NE - journals/ijgt/nau2004/sec5" + ), + pytest.param( + "journals/other/reiley2008/fig1", + id="catalog games that converge to a NE - journals/other/reiley2008/fig1" + ), +] + + +@pytest.mark.nash +@pytest.mark.parametrize("game_id", CATALOG_GAMES_TO_TEST) +def test_hp_catalog_games_max_regret(game_id, subtests) -> None: + """Some catalog games that converge to a Nash equilibrium with a 1e-8 tolerance.""" + game = gbt.catalog.load(game_id) + prior = game.mixed_strategy_profile() + result = gbt.nash.hp_solve(prior=prior) + check_equilibrium(result, subtests) + + +LOCAL_GAMES_TO_TEST = [ + pytest.param( + "8x8.nfg", + id="contrib games that converge to a NE - 8x8.nfg" + ), + pytest.param( + "5x4x3.nfg", + id="contrib games that converge to a NE - 5x4x3.nfg" + ), + pytest.param( + "3x3x3.nfg", + id="contrib games that converge to a NE - 3x3x3.nfg" + ), + pytest.param( + "2x2x2x2x2.nfg", + id="contrib games that converge to a NE - 2x2x2x2x2.nfg" + ), + pytest.param( + "8x2x2.nfg", + id="contrib games that converge to a NE - 8x2x2.nfg" + ), + pytest.param( + "pd.nfg", + id="contrib games that converge to a NE - pd.nfg" + ), + pytest.param( + "2x2x2x2.nfg", + id="contrib games that converge to a NE - 2x2x2x2.nfg" + ), + pytest.param( + "vd.nfg", + id="contrib games that converge to a NE (convex set of equilibria) - vd.nfg" + ), + pytest.param( + "wink3.nfg", + id="contrib games that converge to a NE - wink3.nfg" + ), + pytest.param( + "g1.nfg", + id="contrib games that converge to a NE - g1.nfg" + ), + pytest.param( + "g2.nfg", + id="contrib games that converge to a NE - g2.nfg (initial tangent goes negative)" + ), + +] + + +@pytest.mark.nash +@pytest.mark.parametrize("filename", LOCAL_GAMES_TO_TEST) +def test_hp_large_local_games_max_regret(filename, subtests) -> None: + """Some contrib/games that converge to a Nash equilibrium with a 1e-8 tolerance.""" + game = load_game_from_file(f"../../contrib/games/{filename}") + prior = game.mixed_strategy_profile() + result = gbt.nash.hp_solve(prior=prior) + check_equilibrium(result, subtests) From b38db1dedbf60e588249b7e280fe86b6a0d50d34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Fern=C3=A1ndez=20Cervell?= <153729439+AndresFerCervell@users.noreply.github.com> Date: Thu, 13 Aug 2026 00:19:45 +0200 Subject: [PATCH 06/11] Removing duplicated first_step logic Removed initial tangent vector checks for path-following direction. --- src/solvers/logit/path.cc | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/solvers/logit/path.cc b/src/solvers/logit/path.cc index 53fa37db3..e34933073 100644 --- a/src/solvers/logit/path.cc +++ b/src/solvers/logit/path.cc @@ -196,18 +196,6 @@ PathTracer::TracePath(std::function &, Vector first_step = false; } - if (first_step) { - if (std::abs(t[p_trackingIndex]) <= c_orientTol) { - return {x, false, "Initial tangent vector is orthogonal to path-following direction."}; - } - // Ensure that the tangent is oriented in the same direction as - // the path-following direction. - else if (t[p_trackingIndex] < -c_orientTol) { - omega *= -1.0; - } - first_step = false; - } - // Predictor step for (size_t k = 1; k <= x.size(); k++) { u[k] = x[k] + h * omega * t[k]; From 41673ad091b7311f1853d66f24de735b7d4d9bf5 Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Thu, 13 Aug 2026 18:39:10 +0100 Subject: [PATCH 07/11] Small tidying. --- src/solvers/hp/hp.cc | 3 +++ src/solvers/logit/path.cc | 29 ++++++++++++----------------- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/src/solvers/hp/hp.cc b/src/solvers/hp/hp.cc index 1f211b855..4c9156cd3 100644 --- a/src/solvers/hp/hp.cc +++ b/src/solvers/hp/hp.cc @@ -86,6 +86,9 @@ HPStrategySolve(const MixedStrategyProfile &p_prior) }, x, direction, tracking_index, termination_condition, NullCallbackFunction, criterion_function); + if (!result.status) { + return {}; + } const PolishResult polishing_result = PolishPoint( [&system](const Vector &point, Vector &lhs) { system.GetValue(point, lhs); }, diff --git a/src/solvers/logit/path.cc b/src/solvers/logit/path.cc index e34933073..588a79a12 100644 --- a/src/solvers/logit/path.cc +++ b/src/solvers/logit/path.cc @@ -162,6 +162,15 @@ PathTracer::TracePath(std::function &, Vector p_callback(x); int steps = 0; + auto stepsizeBelowMinimum = [&]() -> TracePathResult { + if (newton && std::abs(p_criterion(x, t)) < c_newtonTol) { + return {x, true, + "Path following terminated successfully at point satisfying criterion function.", + steps}; + } + return {x, false, "Stepsize fell below minimum threshold.", steps}; + }; + bool first_step = true; double omega = (p_direction == TraceDirection::Positive) ? 1.0 : -1.0; @@ -173,14 +182,7 @@ PathTracer::TracePath(std::function &, Vector bool accept = true; if (std::abs(h) <= c_hmin) { - if (newton && std::abs(p_criterion(x, t)) < c_newtonTol) { - return {x, true, - "Path following terminated successfully at point satisfying criterion function.", - steps}; - } - else { - return {x, false, "Stepsize fell below minimum threshold.", steps}; - } + return stepsizeBelowMinimum(); } if (first_step) { @@ -190,7 +192,7 @@ PathTracer::TracePath(std::function &, Vector } // Ensure that the tangent is oriented in the same direction as // the path-following direction. - else if (t[p_trackingIndex] < -c_orientTol) { + if (t[p_trackingIndex] < -c_orientTol) { omega *= -1.0; } first_step = false; @@ -258,14 +260,7 @@ PathTracer::TracePath(std::function &, Vector if (!accept) { h /= m_maxDecel; // PC not accepted; change stepsize and retry if (std::abs(h) <= c_hmin) { - if (newton && std::abs(p_criterion(x, t)) < c_newtonTol) { - return {x, true, - "Path following terminated successfully at point satisfying criterion function.", - steps}; - } - else { - return {x, false, "Stepsize fell below minimum threshold.", steps}; - } + return stepsizeBelowMinimum(); } continue; } From 815872cad9c70563622b28862711523e1fb30caf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Fern=C3=A1ndez=20Cervell?= <153729439+AndresFerCervell@users.noreply.github.com> Date: Thu, 20 Aug 2026 20:40:41 +0200 Subject: [PATCH 08/11] Refine equilibrium check and result validation --- src/solvers/hp/hp.cc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/solvers/hp/hp.cc b/src/solvers/hp/hp.cc index 4c9156cd3..1c1d99076 100644 --- a/src/solvers/hp/hp.cc +++ b/src/solvers/hp/hp.cc @@ -49,7 +49,7 @@ HPStrategySolve(const MixedStrategyProfile &p_prior) const double t = point[1]; // Path tracer reaches tol - if (t >= t_target && t - t_target < tol) { + if (system.ExtractEquilibrium(point).GetMaxRegret() <= tol && t >= t_target - tol) { return true; } @@ -86,7 +86,7 @@ HPStrategySolve(const MixedStrategyProfile &p_prior) }, x, direction, tracking_index, termination_condition, NullCallbackFunction, criterion_function); - if (!result.status) { + if (!result.status && system.ExtractEquilibrium(x).GetMaxRegret() > tol) { return {}; } From f82b57df072edb31f05ad38396f8fc843eb46d0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Fern=C3=A1ndez=20Cervell?= <153729439+AndresFerCervell@users.noreply.github.com> Date: Thu, 20 Aug 2026 20:49:11 +0200 Subject: [PATCH 09/11] Add system parameter to termination condition lambda --- src/solvers/hp/hp.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/solvers/hp/hp.cc b/src/solvers/hp/hp.cc index 1c1d99076..cb0e596f6 100644 --- a/src/solvers/hp/hp.cc +++ b/src/solvers/hp/hp.cc @@ -45,7 +45,7 @@ HPStrategySolve(const MixedStrategyProfile &p_prior) bool has_crossed = false; auto termination_condition = [t_target, &last_t, &has_crossed, - tol](const Vector &point) { + tol, system](const Vector &point) { const double t = point[1]; // Path tracer reaches tol From 34adbf9dc437af4805a9004dfcb8e9fbfd24e2c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Fern=C3=A1ndez=20Cervell?= <153729439+AndresFerCervell@users.noreply.github.com> Date: Fri, 21 Aug 2026 12:13:54 +0200 Subject: [PATCH 10/11] Format --- src/solvers/hp/hp.cc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/solvers/hp/hp.cc b/src/solvers/hp/hp.cc index cb0e596f6..bd2aed987 100644 --- a/src/solvers/hp/hp.cc +++ b/src/solvers/hp/hp.cc @@ -44,8 +44,8 @@ HPStrategySolve(const MixedStrategyProfile &p_prior) double last_t = 0.0; bool has_crossed = false; - auto termination_condition = [t_target, &last_t, &has_crossed, - tol, system](const Vector &point) { + auto termination_condition = [t_target, &last_t, &has_crossed, tol, + &system](const Vector &point) { const double t = point[1]; // Path tracer reaches tol From ded034de542065587d51ee9f1ef9913350de9758 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Fern=C3=A1ndez=20Cervell?= Date: Fri, 21 Aug 2026 22:00:39 +0200 Subject: [PATCH 11/11] Perturbation and polish fix --- src/solvers/logit/path.cc | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/solvers/logit/path.cc b/src/solvers/logit/path.cc index 588a79a12..25291b525 100644 --- a/src/solvers/logit/path.cc +++ b/src/solvers/logit/path.cc @@ -149,6 +149,9 @@ PathTracer::TracePath(std::function &, Vector double pert_countdown = 0.0; // How much longer (in arclength) to apply perturbation const double c_orientTol = 1.0e-8; // tolerance for detecting change in orientation + const double b_tol = 1.0e-10; // Tolerance for perturbing the b matrix in case of singularity + const double b_pert = 1.0e-8; // Perturbation of the b matrix in case of singularity + Vector u(x.size()); // t is current tangent at x; newT is tangent at u, which is the next point. Vector t(x.size()), newT(x.size()); @@ -207,6 +210,17 @@ PathTracer::TracePath(std::function &, Vector p_jacobian(u, b); QRDecomp(b, q); + for (size_t i = 1; i < b.NumRows(); i++) { + if (std::abs(b(i, i)) < b_tol) { + if (b(i, i) < 0) { + b(i, i) -= b_pert; + } + else { + b(i, i) += b_pert; + } + } + } + int iter = 1; double disto = 0.0; while (true) { @@ -314,6 +328,8 @@ PolishResult PolishPoint(std::function &, Vector original_x = x; + const size_t N = x.size() - 1; Vector y(N); // Equations results Matrix jac_full(N + 1, N); // Full Jacobian matrix (N+1 unknowns, N equations) @@ -321,6 +337,8 @@ PolishResult PolishPoint(std::function &, Vector Q(N, N); // Orthogonal matrix from QR decomposition Vector x_reduced(N); // Reduced x vector with fixed_index removed + double const eq_tol = 1e-2; + int steps = 0; double dist = 0.0; @@ -370,6 +388,14 @@ PolishResult PolishPoint(std::function &, Vector eq_tol) { + x = original_x; + return {x, false, "Polishing converged to an invalid mathematical state. Reverted.", steps}; + } + } + return {x, true, "Polishing terminated successfully.", steps}; } } // end namespace Gambit