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
4 changes: 4 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
## [17.0.0-beta.2] - unreleased

### Added
- Details on bifurcations found and actions taken to resolve them (by perturbation) are now reported
by `nash.logit_solve` (and related functions) (#186)

### Changed
- Refined the perturbation approach in `path.cc` (logit and hp solvers) to better handle paths on games
with bifurcations. (#492)
Expand Down
10 changes: 10 additions & 0 deletions doc/tools.logit.rst
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,16 @@ See the :ref:`algorithm description <logit>` for full details.
this switch is specified, only the approximation to the Nash
equilibrium at the end of the branch is output.

.. cmdoption:: -b

.. versionadded:: 17.0.0

Report any bifurcations detected while tracing, as comment lines giving the
bracketing values of lambda. Bifurcations are reported inline, interleaved
with the trace, as soon as each is detected. This switch has no additional
effect unless :option:`-e` is also given, since bifurcations are already
reported as part of the full trace otherwise.

Computing the principal branch, in mixed strategies, of
the reduced strategic form of the example
in Figure 2 of :cite:p:`Sel75`::
Expand Down
6 changes: 5 additions & 1 deletion src/gui/dllogit.h
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,11 @@ template <class Traits> class LogitThreadRunner final : public wxThread {
Traits::Solve(
m_game,
[this](const LogitEvent<typename Traits::QREType> &p_event) {
PostPoint(std::get<LogitPathEvent<typename Traits::QREType>>(p_event).qre);
if (const auto *pathEvent =
std::get_if<LogitPathEvent<typename Traits::QREType>>(&p_event)) {
PostPoint(pathEvent->qre);
}
// Bifurcation/perturbation events are not yet visualized in the GUI's logit plot.
},
m_cancel);
}
Expand Down
81 changes: 65 additions & 16 deletions src/pygambit/callback.h
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,23 @@ std::string InvokeBehaviorCallbackRational(
PyObject *p_callback,
std::shared_ptr<Gambit::MixedBehaviorProfile<Gambit::Rational>> p_profile);
std::string
InvokeLogitStrategyEventCallback(PyObject *p_callback,
std::shared_ptr<Gambit::LogitQREMixedStrategyProfile> p_qre);
InvokeLogitStrategyPathEventCallback(PyObject *p_callback,
std::shared_ptr<Gambit::LogitQREMixedStrategyProfile> p_qre);
std::string
InvokeLogitBehaviorEventCallback(PyObject *p_callback,
std::shared_ptr<Gambit::LogitQREMixedBehaviorProfile> p_qre);
InvokeLogitBehaviorPathEventCallback(PyObject *p_callback,
std::shared_ptr<Gambit::LogitQREMixedBehaviorProfile> p_qre);
std::string InvokeLogitStrategyBifurcationEventCallback(
PyObject *p_callback, std::shared_ptr<Gambit::LogitQREMixedStrategyProfile> p_before,
std::shared_ptr<Gambit::LogitQREMixedStrategyProfile> p_after);
std::string InvokeLogitBehaviorBifurcationEventCallback(
PyObject *p_callback, std::shared_ptr<Gambit::LogitQREMixedBehaviorProfile> p_before,
std::shared_ptr<Gambit::LogitQREMixedBehaviorProfile> p_after);
std::string InvokeLogitStrategyPerturbationEventCallback(
PyObject *p_callback, bool p_active,
std::shared_ptr<Gambit::LogitQREMixedStrategyProfile> p_qre);
std::string InvokeLogitBehaviorPerturbationEventCallback(
PyObject *p_callback, bool p_active,
std::shared_ptr<Gambit::LogitQREMixedBehaviorProfile> p_qre);
std::string
InvokeHPStrategyEventCallback(PyObject *p_callback,
std::shared_ptr<Gambit::MixedStrategyProfile<double>> p_profile,
Expand Down Expand Up @@ -195,10 +207,11 @@ MakeBehaviorCallback<Gambit::Rational>(PyObject *p_callback)
}

///
/// Builds a LogitEventCallbackType<QRE> which, when invoked with a
/// path-tracing event, calls a Python callable with the QRE point traced so
/// far. A null callback (Python `None`) yields the solver's own no-op
/// default.
/// Builds a LogitEventCallbackType<QRE> which, when invoked, dispatches to whichever
/// Invoke*EventCallback trampoline matches the alternative held by the event (an ordinary
/// traced point, a detected bifurcation, or a perturbation switching on/off), calling a
/// Python callable with the corresponding pygambit event object. A null callback (Python
/// `None`) yields the solver's own no-op default.
///
template <class QRE>
Gambit::LogitEventCallbackType<QRE> MakeLogitEventCallback(PyObject *p_callback);
Expand All @@ -211,10 +224,28 @@ MakeLogitEventCallback<Gambit::LogitQREMixedStrategyProfile>(PyObject *p_callbac
return Gambit::NullLogitEventCallback<Gambit::LogitQREMixedStrategyProfile>;
}
return [p_callback](const Gambit::LogitEvent<Gambit::LogitQREMixedStrategyProfile> &p_event) {
const auto &qre =
std::get<Gambit::LogitPathEvent<Gambit::LogitQREMixedStrategyProfile>>(p_event).qre;
Gambit::ThrowIfPythonError(InvokeLogitStrategyEventCallback(
p_callback, std::make_shared<Gambit::LogitQREMixedStrategyProfile>(qre)));
using Gambit::LogitQREMixedStrategyProfile;
std::visit(
[p_callback]<typename Event>(const Event &event) {
if constexpr (std::is_same_v<Event,
Gambit::LogitPathEvent<LogitQREMixedStrategyProfile>>) {
Gambit::ThrowIfPythonError(InvokeLogitStrategyPathEventCallback(
p_callback, std::make_shared<LogitQREMixedStrategyProfile>(event.qre)));
}
else if constexpr (std::is_same_v<Event, Gambit::LogitBifurcationEvent<
LogitQREMixedStrategyProfile>>) {
Gambit::ThrowIfPythonError(InvokeLogitStrategyBifurcationEventCallback(
p_callback, std::make_shared<LogitQREMixedStrategyProfile>(event.before),
std::make_shared<LogitQREMixedStrategyProfile>(event.after)));
}
else if constexpr (std::is_same_v<Event, Gambit::LogitPerturbationEvent<
LogitQREMixedStrategyProfile>>) {
Gambit::ThrowIfPythonError(InvokeLogitStrategyPerturbationEventCallback(
p_callback, event.active,
std::make_shared<LogitQREMixedStrategyProfile>(event.qre)));
}
},
p_event);
};
}

Expand All @@ -226,10 +257,28 @@ MakeLogitEventCallback<Gambit::LogitQREMixedBehaviorProfile>(PyObject *p_callbac
return Gambit::NullLogitEventCallback<Gambit::LogitQREMixedBehaviorProfile>;
}
return [p_callback](const Gambit::LogitEvent<Gambit::LogitQREMixedBehaviorProfile> &p_event) {
const auto &qre =
std::get<Gambit::LogitPathEvent<Gambit::LogitQREMixedBehaviorProfile>>(p_event).qre;
Gambit::ThrowIfPythonError(InvokeLogitBehaviorEventCallback(
p_callback, std::make_shared<Gambit::LogitQREMixedBehaviorProfile>(qre)));
using Gambit::LogitQREMixedBehaviorProfile;
std::visit(
[p_callback]<typename Event>(const Event &event) {
if constexpr (std::is_same_v<Event,
Gambit::LogitPathEvent<LogitQREMixedBehaviorProfile>>) {
Gambit::ThrowIfPythonError(InvokeLogitBehaviorPathEventCallback(
p_callback, std::make_shared<LogitQREMixedBehaviorProfile>(event.qre)));
}
else if constexpr (std::is_same_v<Event, Gambit::LogitBifurcationEvent<
LogitQREMixedBehaviorProfile>>) {
Gambit::ThrowIfPythonError(InvokeLogitBehaviorBifurcationEventCallback(
p_callback, std::make_shared<LogitQREMixedBehaviorProfile>(event.before),
std::make_shared<LogitQREMixedBehaviorProfile>(event.after)));
}
else if constexpr (std::is_same_v<Event, Gambit::LogitPerturbationEvent<
LogitQREMixedBehaviorProfile>>) {
Gambit::ThrowIfPythonError(InvokeLogitBehaviorPerturbationEventCallback(
p_callback, event.active,
std::make_shared<LogitQREMixedBehaviorProfile>(event.qre)));
}
},
p_event);
};
}

Expand Down
46 changes: 42 additions & 4 deletions src/pygambit/cli/logit.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,20 @@ def _render_qre_row(qre, decimals: int) -> str:
return text


def _render_bifurcation_comment(event, decimals: int) -> str:
"""Render a `LogitBifurcationEvent` as a comment line reporting the lambda
interval bracketing the detected bifurcation.
"""
lo, hi = event.before.lam, event.after.lam
return f"# bifurcation: lambda in [{lo:.{decimals}f}, {hi:.{decimals}f}]"


def _render_perturbation_comment(event, decimals: int) -> str:
"""Render a `LogitPerturbationEvent` as a comment line."""
state = "started" if event.active else "ended"
return f"# perturbation {state} near lambda={event.qre.lam:.{decimals}f}"


def _read_frequencies(path: str, game: gbt.Game) -> gbt.MixedStrategyProfileDouble:
"""Read observed strategy frequencies for maximum-likelihood estimation: a flat,
comma-separated list of counts, one per strategy, in the same order as a profile's
Expand Down Expand Up @@ -135,6 +149,16 @@ def _read_frequencies(path: str, game: gbt.Game) -> gbt.MixedStrategyProfileDoub
is_flag=True,
help="print only the terminal equilibrium (default is to print the entire branch)",
)
@click.option(
"-b",
"--report-bifurcations",
is_flag=True,
help=(
"report any bifurcations detected while tracing, as comment lines; "
"has no additional effect unless --terminal-only is also given, since "
"bifurcations are already reported as part of the traced branch otherwise"
),
)
@click.option("-q", "--quiet", is_flag=True, help="quiet mode (suppresses banner)")
@version_option(DESCRIPTION)
@handle_errors
Expand All @@ -148,16 +172,29 @@ def main(
target_lambda: tuple[float, ...],
mle_file: str | None,
terminal_only: bool,
report_bifurcations: bool,
quiet: bool,
) -> None:
game = load_game(quiet, DESCRIPTION, file, PROG_NAME)
if not game.is_perfect_recall:
raise ValueError("Computing equilibria of games with imperfect recall is not supported.")

def stream(qre) -> None:
click.echo(_render_qre_row(qre, decimals))
def stream(event) -> None:
if isinstance(event, gbt.LogitPathEvent):
click.echo(_render_qre_row(event.qre, decimals))
elif isinstance(event, gbt.LogitBifurcationEvent):
click.echo(_render_bifurcation_comment(event, decimals))
elif isinstance(event, gbt.LogitPerturbationEvent):
click.echo(_render_perturbation_comment(event, decimals))

event_callback = None if terminal_only else stream
def stream_bifurcations_only(event) -> None:
if isinstance(event, gbt.LogitBifurcationEvent):
click.echo(_render_bifurcation_comment(event, decimals))

if terminal_only:
event_callback = stream_bifurcations_only if report_bifurcations else None
else:
event_callback = stream

# Maximum-likelihood estimation, like the C++ tool, is only defined over the
# strategic representation, since the observed frequencies are read as a flat
Expand Down Expand Up @@ -192,6 +229,7 @@ def stream(qre) -> None:
maxregret=maxregret,
first_step=first_step,
max_accel=max_accel,
event_callback=event_callback,
)
click.echo(render_profile_csv(result.equilibrium, "NE", decimals, fixed=False))
return
Expand All @@ -202,7 +240,7 @@ def stream(qre) -> None:
maxregret=maxregret,
first_step=first_step,
max_accel=max_accel,
event_callback=stream,
event_callback=event_callback,
)
click.echo(render_profile_csv(result.equilibrium, "NE", decimals, fixed=False))

Expand Down
9 changes: 9 additions & 0 deletions src/pygambit/gambit.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -729,14 +729,23 @@ cdef extern from "solvers/logit/logit.h":
Converged
RegretTargetNotReached

cdef cppclass c_LogitBifurcationStrategy "LogitBifurcation<LogitQREMixedStrategyProfile>":
c_LogitQREMixedStrategyProfile before
c_LogitQREMixedStrategyProfile after
cdef cppclass c_LogitBifurcationBehavior "LogitBifurcation<LogitQREMixedBehaviorProfile>":
c_LogitQREMixedBehaviorProfile before
c_LogitQREMixedBehaviorProfile after

cdef cppclass c_LogitStrategyResult "Gambit::LogitStrategyResult":
optional[c_MixedStrategyProfile[double]] equilibrium
bool success
c_LogitTerminationReason reason
stdvector[c_LogitBifurcationStrategy] bifurcations
cdef cppclass c_LogitBehaviorResult "Gambit::LogitBehaviorResult":
optional[c_MixedBehaviorProfile[double]] equilibrium
bool success
c_LogitTerminationReason reason
stdvector[c_LogitBifurcationBehavior] bifurcations

c_LogitStrategyResult LogitStrategySolveEquilibrium(
c_LogitQREMixedStrategyProfile, double, double, double,
Expand Down
6 changes: 4 additions & 2 deletions src/pygambit/nash.h
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ LogitBehaviorPrincipalBranchWrapper(const Game &p_game, double p_regret, double
double p_maxAccel)
{
return LogitBehaviorSolve(LogitQREMixedBehaviorProfile(p_game), p_regret,
PathTracer::TraceDirection::Positive, p_firstStep, p_maxAccel);
PathTracer::TraceDirection::Positive, p_firstStep, p_maxAccel)
.profiles;
}

std::shared_ptr<LogitQREMixedBehaviorProfile>
Expand Down Expand Up @@ -82,7 +83,8 @@ LogitStrategyPrincipalBranchWrapper(const Game &p_game, double p_regret, double
double p_maxAccel)
{
return LogitStrategySolve(LogitQREMixedStrategyProfile(p_game), p_regret,
PathTracer::TraceDirection::Positive, p_firstStep, p_maxAccel);
PathTracer::TraceDirection::Positive, p_firstStep, p_maxAccel)
.profiles;
}

std::list<std::shared_ptr<LogitQREMixedStrategyProfile>>
Expand Down
Loading
Loading