diff --git a/ChangeLog b/ChangeLog index bce4d092d..18f6e94ca 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,6 +1,8 @@ ## [17.0.0] - unreleased ### Changed +- `lcp_solve` no longer silently absorbs internal exceptions if they occur, but instead these propagate out + to match the behaviour of all other Nash equilibrium solvers. (#996) - Implemented bespoke XML parser that handles the subset in the de-facto legacy XML workbook .gbt format; removes dependency on tinyxml. (#897) diff --git a/src/games/nash.h b/src/games/nash.h index c8efdfdc7..88d7eb71e 100644 --- a/src/games/nash.h +++ b/src/games/nash.h @@ -57,20 +57,6 @@ ToMixedBehaviorProfile(const std::list> &p_list) return ret; } -template -using BehaviorSolverType = std::function>(const Game &)>; - -// -// Exception raised when maximum number of equilibria to compute -// has been reached. A convenience for unraveling a potentially -// deep recursion. -// -class EquilibriumLimitReached : public std::runtime_error { -public: - EquilibriumLimitReached() : std::runtime_error("Reached target number of equilibria") {} - ~EquilibriumLimitReached() noexcept override = default; -}; - } // namespace Gambit::Nash #endif // LIBGAMBIT_NASH_H diff --git a/src/gui/dlnashmon.cc b/src/gui/dlnashmon.cc index b83951346..ea2d9e31f 100644 --- a/src/gui/dlnashmon.cc +++ b/src/gui/dlnashmon.cc @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include "wx/sheet/sheet.h" diff --git a/src/pygambit/gambit.pxd b/src/pygambit/gambit.pxd index b6be8e913..71538a7fc 100644 --- a/src/pygambit/gambit.pxd +++ b/src/pygambit/gambit.pxd @@ -551,9 +551,7 @@ cdef extern from "solvers/lcp/lcp.h": stdlist[c_MixedStrategyProfile[T]] LcpStrategySolve[T]( c_Game, int p_stopAfter, int p_maxDepth ) except +RuntimeError - stdlist[c_MixedBehaviorProfile[T]] LcpBehaviorSolve[T]( - c_Game, int p_stopAfter, int p_maxDepth - ) except +RuntimeError + stdlist[c_MixedBehaviorProfile[T]] LcpBehaviorSolve[T](c_Game) except +RuntimeError cdef extern from "solvers/lp/lp.h": stdlist[c_MixedStrategyProfile[T]] LpStrategySolve[T](c_Game) except +RuntimeError diff --git a/src/solvers/lcp/efglcp.cc b/src/solvers/lcp/efglcp.cc index 8207c0eed..73b85d739 100644 --- a/src/solvers/lcp/efglcp.cc +++ b/src/solvers/lcp/efglcp.cc @@ -143,21 +143,16 @@ std::list> NashLcpBehaviorSolver::Solve(const Game &p linalg::LemkeTableau tab(A, b); solution.eps = tab.Epsilon(); - try { - tab.Pivot(solution.ns1 + solution.ns2 + 1, 0); - tab.SF_LCPPath(solution.ns1 + solution.ns2 + 1); - solution.AddBFS(tab); - Vector sol(tab.MinRow(), tab.MaxRow()); - tab.BasisVector(sol); - MixedBehaviorProfile profile(p_game); - GetProfile(tab, profile, sol, p_game->GetRoot(), 1, 1, solution); - profile.UndefinedToCentroid(); - solution.m_equilibria.push_back(profile); - this->m_onEquilibrium(profile, "NE"); - } - catch (std::runtime_error &e) { - std::cerr << "Error: " << e.what() << std::endl; - } + tab.Pivot(solution.ns1 + solution.ns2 + 1, 0); + tab.SF_LCPPath(solution.ns1 + solution.ns2 + 1); + solution.AddBFS(tab); + Vector sol(tab.MinRow(), tab.MaxRow()); + tab.BasisVector(sol); + MixedBehaviorProfile profile(p_game); + GetProfile(tab, profile, sol, p_game->GetRoot(), 1, 1, solution); + profile.UndefinedToCentroid(); + solution.m_equilibria.push_back(profile); + this->m_onEquilibrium(profile, "NE"); return solution.m_equilibria; } diff --git a/src/solvers/lcp/nfglcp.cc b/src/solvers/lcp/nfglcp.cc index eb1034d95..cfa502aa8 100644 --- a/src/solvers/lcp/nfglcp.cc +++ b/src/solvers/lcp/nfglcp.cc @@ -121,13 +121,15 @@ template class NashLcpStrategySolver { std::list> Solve(const Game &) const; private: + enum class SearchResult { Continue, PruneBranch, LimitReached }; + StrategyCallbackType m_onEquilibrium; int m_stopAfter, m_maxDepth; class Solution; - bool OnBFS(const Game &, linalg::LHTableau &, Solution &) const; - void AllLemke(const Game &, int j, linalg::LHTableau &, Solution &, int) const; + SearchResult OnBFS(const Game &, linalg::LHTableau &, Solution &) const; + SearchResult AllLemke(const Game &, int j, linalg::LHTableau &, Solution &, int) const; }; template class NashLcpStrategySolver::Solution { @@ -145,16 +147,17 @@ template class NashLcpStrategySolver::Solution { // Function called when a CBFS is encountered. // If it is not already in the list p_list, it is added. // The corresponding equilibrium is computed and output. -// Returns 'true' if the CBFS is new; 'false' if it already appears in the -// list. +// Returns whether to continue from this BFS, prune the current branch, +// or stop because the requested equilibrium limit has been reached. // template -bool NashLcpStrategySolver::OnBFS(const Game &p_game, linalg::LHTableau &p_tableau, - Solution &p_solution) const +typename NashLcpStrategySolver::SearchResult +NashLcpStrategySolver::OnBFS(const Game &p_game, linalg::LHTableau &p_tableau, + Solution &p_solution) const { const Gambit::linalg::BFS cbfs(p_tableau.GetBFS()); if (p_solution.Contains(cbfs)) { - return false; + return SearchResult::PruneBranch; } p_solution.push_back(cbfs); @@ -170,7 +173,7 @@ bool NashLcpStrategySolver::OnBFS(const Game &p_game, linalg::LHTableau &p } if (sum == (T)0) { // This is the trivial CBFS. - return false; + return SearchResult::PruneBranch; } for (int j = 1; j <= n1; j++) { @@ -204,10 +207,10 @@ bool NashLcpStrategySolver::OnBFS(const Game &p_game, linalg::LHTableau &p p_solution.m_equilibria.push_back(profile); if (m_stopAfter > 0 && p_solution.EquilibriumCount() >= m_stopAfter) { - throw EquilibriumLimitReached(); + return SearchResult::LimitReached; } - return true; + return SearchResult::Continue; } // @@ -218,26 +221,36 @@ bool NashLcpStrategySolver::OnBFS(const Game &p_game, linalg::LHTableau &p // all possible paths, adding any new equilibria to the List. // template -void NashLcpStrategySolver::AllLemke(const Game &p_game, int j, linalg::LHTableau &B, - Solution &p_solution, int depth) const +typename NashLcpStrategySolver::SearchResult +NashLcpStrategySolver::AllLemke(const Game &p_game, int j, linalg::LHTableau &B, + Solution &p_solution, int depth) const { if (m_maxDepth != 0 && depth > m_maxDepth) { - return; + return SearchResult::Continue; } // On the initial depth=0 call, the CBFS we are at is the extraneous // solution. - if (depth > 0 && !OnBFS(p_game, B, p_solution)) { - return; + if (depth > 0) { + const auto result = OnBFS(p_game, B, p_solution); + if (result == SearchResult::PruneBranch) { + return SearchResult::Continue; + } + if (result == SearchResult::LimitReached) { + return result; + } } for (int i = B.MinCol(); i <= B.MaxCol(); i++) { if (i != j) { linalg::LHTableau Bcopy(B); Bcopy.LemkePath(i); - AllLemke(p_game, i, Bcopy, p_solution, depth + 1); + if (AllLemke(p_game, i, Bcopy, p_solution, depth + 1) == SearchResult::LimitReached) { + return SearchResult::LimitReached; + } } } + return SearchResult::Continue; } template @@ -252,27 +265,18 @@ std::list> NashLcpStrategySolver::Solve(const Game &p } Solution solution; - try { - const Matrix A1 = Make_A1(p_game); - const Vector b1 = Make_b1(p_game); - const Matrix A2 = Make_A2(p_game); - const Vector b2 = Make_b2(p_game); - linalg::LHTableau B(A1, A2, b1, b2); + const Matrix A1 = Make_A1(p_game); + const Vector b1 = Make_b1(p_game); + const Matrix A2 = Make_A2(p_game); + const Vector b2 = Make_b2(p_game); + linalg::LHTableau B(A1, A2, b1, b2); - if (m_stopAfter != 1) { - AllLemke(p_game, 0, B, solution, 0); - } - else { - B.LemkePath(1); - OnBFS(p_game, B, solution); - } - } - catch (EquilibriumLimitReached &) { - // This pseudo-exception requires no additional action; - // solution contains details of all equilibria found + if (m_stopAfter != 1) { + AllLemke(p_game, 0, B, solution, 0); } - catch (std::runtime_error &e) { - std::cerr << "ERROR: " << e.what() << std::endl; + else { + B.LemkePath(1); + OnBFS(p_game, B, solution); } return solution.m_equilibria; }