diff --git a/ChangeLog b/ChangeLog index b12b97ddca..3ed43f880a 100644 --- a/ChangeLog +++ b/ChangeLog @@ -5,12 +5,19 @@ recently opened games. (#1108) - Details on bifurcations found and actions taken to resolve them (by perturbation) are now reported by `nash.logit_solve` (and related functions) (#186) +- `-D`/`--detail` is now available on every `gambit-*` command-line tool that reports equilibria. +- The command-line tools' `-D` report for behavior profiles now includes the probability + distribution over full histories (discussion #1094) ### Changed - Refined the perturbation approach in `path.cc` (logit and hp solvers) to better handle paths on games with bifurcations. (#492) - Nash solver algorithms now return a method-specific `Result` class, rather than a generic class, with potentially richer information about the performance of the algorithm. +- The command-line tools' `-D` (detail) report for behavior profiles now identifies each node by its + `History` (the actions from the root, comma-joined). +- The command-line tools' `-D` report now identifies each player by their own label, rather than by + their position among the game's players. ## [17.0.0-beta.1] - 2026-09-08 diff --git a/doc/tools.convert.rst b/doc/tools.convert.rst deleted file mode 100644 index 2273bddc8d..0000000000 --- a/doc/tools.convert.rst +++ /dev/null @@ -1,65 +0,0 @@ -:program:`gambit-convert` -========================= - -:program:`gambit-convert` reads a game on standard input in any supported format -and converts it to another text representation. Currently, this tool supports -outputting the strategic form of the game in one of these formats: - -* A standard HTML table. -* A LaTeX fragment in the format of Martin Osborne's `sgame` macros - (see http://www.economics.utoronto.ca/osborne/latex/index.html). - - -.. program:: gambit-convert - -.. cmdoption:: -O FORMAT - - Required. Specifies the output format. Supported options for - `FORMAT` are `html` or `sgame`. - -.. cmdoption:: -r PLAYER - - Specifies the player number to place on the rows of the tables. - The default if not specified is to place player 1 on the rows. - -.. cmdoption:: -c PLAYER - - Specifies the player number to place on the columns of the tables. - The default if not specified is to place player 2 on the columns. - -.. cmdoption:: -h - - Prints a help message listing the available options. - -.. cmdoption:: -q - - Suppresses printing of the banner at program launch. - - -Example invocation for HTML output:: - - $ gambit-convert -O html 2x2.nfg - Convert games among various file formats - Gambit version |release|, Copyright (C) 1994-2026, The Gambit Project - This is free software, distributed under the GNU GPL - -

Two person 2 x 2 game with unique mixed equilibrium

-
12
12,00,1
20,11,0
- - -Example invocation for LaTeX output:: - - $ gambit-convert -O sgame 2x2.nfg - Convert games among various file formats - Gambit version |release|, Copyright (C) 1994-2026, The Gambit Project - This is free software, distributed under the GNU GPL - - \begin{game}{2}{2}[Player 1][Player 2] - &1 & 2\\ - 1 & $2,0$ & $0,1$ \\ - 2 & $0,1$ & $1,0$ - \end{game} diff --git a/doc/tools.enummixed.rst b/doc/tools.enummixed.rst index 76d87cdfeb..2319c134b8 100644 --- a/doc/tools.enummixed.rst +++ b/doc/tools.enummixed.rst @@ -29,6 +29,17 @@ See the :ref:`algorithm description ` for full details. each convex set, prefixed by convex-N , where N indexes the set. The set of all equilibria, then, is the union of these convex sets. +.. cmdoption:: -D + + .. versionadded:: 17.0.0 + + The default output format for computed equilibria is a + comma-separated list of strategy probabilities, suitable + for postprocessing by automated tools. Specifying `-D` instead + causes the program to output greater detail on each equilibrium + profile computed (and, with :option:`-c`, on each member of every + convex set reported). + .. cmdoption:: -h Prints a help message listing the available options. diff --git a/doc/tools.enumpoly.rst b/doc/tools.enumpoly.rst index d7d27fbf6a..b425a380c0 100644 --- a/doc/tools.enumpoly.rst +++ b/doc/tools.enumpoly.rst @@ -50,6 +50,16 @@ and (in verbose mode) the support is identified with the label strategies for extensive games. (This has no effect for strategic games, since a strategic game is its own reduced strategic game.) +.. cmdoption:: -D + + .. versionadded:: 17.0.0 + + The default output format for computed equilibria is a + comma-separated list of strategy or action probabilities, suitable + for postprocessing by automated tools. Specifying `-D` instead + causes the program to output greater detail on each equilibrium + profile computed. + .. cmdoption:: -m .. versionadded:: 16.3.0 diff --git a/doc/tools.gnm.rst b/doc/tools.gnm.rst index c92fd2534d..1716a22919 100644 --- a/doc/tools.gnm.rst +++ b/doc/tools.gnm.rst @@ -19,6 +19,16 @@ subsets of equilibria being found. Express all output using decimal representations with the specified number of digits. +.. cmdoption:: -D + + .. versionadded:: 17.0.0 + + The default output format for computed equilibria is a + comma-separated list of strategy probabilities, suitable + for postprocessing by automated tools. Specifying `-D` instead + causes the program to output greater detail on each equilibrium + profile computed. + .. cmdoption:: -h Prints a help message listing the available options. diff --git a/doc/tools.hp.rst b/doc/tools.hp.rst index b3a00b7d7a..d730357e9e 100644 --- a/doc/tools.hp.rst +++ b/doc/tools.hp.rst @@ -20,6 +20,16 @@ different equilibria being found. Express all output using decimal representations with the specified number of digits. +.. cmdoption:: -D + + .. versionadded:: 17.0.0 + + The default output format for computed equilibria is a + comma-separated list of strategy probabilities, suitable + for postprocessing by automated tools. Specifying `-D` instead + causes the program to output greater detail on each equilibrium + profile computed. + .. cmdoption:: -h Prints a help message listing the available options. diff --git a/doc/tools.ipa.rst b/doc/tools.ipa.rst index d2f83f391f..8fb6a9168f 100644 --- a/doc/tools.ipa.rst +++ b/doc/tools.ipa.rst @@ -19,6 +19,16 @@ equilibria being found. Express all output using decimal representations with the specified number of digits. +.. cmdoption:: -D + + .. versionadded:: 17.0.0 + + The default output format for computed equilibria is a + comma-separated list of strategy probabilities, suitable + for postprocessing by automated tools. Specifying `-D` instead + causes the program to output greater detail on each equilibrium + profile computed. + .. cmdoption:: -h Prints a help message listing the available options. diff --git a/doc/tools.liap.rst b/doc/tools.liap.rst index 78ade88c4b..62575ca6d0 100644 --- a/doc/tools.liap.rst +++ b/doc/tools.liap.rst @@ -47,6 +47,16 @@ See the :ref:`algorithm description ` for full details. Express all output using decimal representations with the specified number of digits. +.. cmdoption:: -D + + .. versionadded:: 17.0.0 + + The default output format for computed equilibria is a + comma-separated list of strategy or action probabilities, suitable + for postprocessing by automated tools. Specifying `-D` instead + causes the program to output greater detail on each equilibrium + profile computed. + .. cmdoption:: -n Specify the number of starting points to randomly generate. @@ -88,13 +98,17 @@ See the :ref:`algorithm description ` for full details. output of equilibria (excluding the initial NE tag). Mutually exclusive with :option:`-n`. -.. cmdoption:: -v +.. cmdoption:: -V, --verbose Sets verbose mode. In verbose mode, initial points, as well as points at which the minimization fails at a constrained local minimum that is not a Nash equilibrium, are all output, in addition to any equilibria found. +.. cmdoption:: -v, --version + + Prints version information and exits. + Computing an equilibrium in mixed strategies of the example in Figure 2 of :cite:p:`Sel75`:: $ gambit-liap catalog/games/journals/ijgt/selten1975/fig2.efg diff --git a/doc/tools.logit.rst b/doc/tools.logit.rst index 2b709b7c39..521b7bef4f 100644 --- a/doc/tools.logit.rst +++ b/doc/tools.logit.rst @@ -74,6 +74,18 @@ See the :ref:`algorithm description ` for full details. this switch is specified, only the approximation to the Nash equilibrium at the end of the branch is output. +.. cmdoption:: -D + + .. versionadded:: 17.0.0 + + If tracing reaches a point accepted as a Nash equilibrium (per + :option:`-m`), print detailed, human-readable information about that + terminal profile instead of the usual comma-separated line -- whether + or not :option:`-e` was also given, since both end with the same + accepted-equilibrium line. This has no effect on the points printed + along the way there, or on the output of :option:`-l` or of maximum-likelihood + estimation, neither of which is itself an accepted equilibrium. + .. cmdoption:: -b .. versionadded:: 17.0.0 diff --git a/doc/tools.rst b/doc/tools.rst index 99445adf14..856a67c32c 100644 --- a/doc/tools.rst +++ b/doc/tools.rst @@ -30,9 +30,12 @@ strategy. For behavior strategy profiles, the probabilities are sorted by player, then information set, then action number, where the information sets for a player are sorted by the order in which they are encountered in a depth-first traversal of the game tree. -Many programs take an option `-D`, which, if specified, instead prints +Every program takes an option `-D`, which, if specified, instead prints a more verbose, human-friendly description of each strategy profile -computed. +computed. (`gambit-logit` is a partial exception: since it traces a whole +branch rather than reporting discrete equilibria, `-D` there affects only +the terminal point, if tracing reaches one accepted as a Nash equilibrium -- +see its own documentation.) Many of the programs optionally output additional information about the operation of the algorithm. These outputs have other, diff --git a/doc/tools.simpdiv.rst b/doc/tools.simpdiv.rst index 14a7a1a33b..bd8bc2b224 100644 --- a/doc/tools.simpdiv.rst +++ b/doc/tools.simpdiv.rst @@ -83,6 +83,16 @@ options to specify additional starting points for the algorithm. this option sacrifices some precision in reporting the output of the method, in exchange for probabilities which are more human-readable. +.. cmdoption:: -D + + .. versionadded:: 17.0.0 + + The default output format for computed equilibria is a + comma-separated list of strategy probabilities, suitable + for postprocessing by automated tools. Specifying `-D` instead + causes the program to output greater detail on each equilibrium + profile computed, respecting :option:`-d` the same way. + .. cmdoption:: -V, --verbose Sets verbose mode. In verbose mode, initial points, as well as diff --git a/src/pygambit/cli/common.py b/src/pygambit/cli/common.py index e667e56de2..329d60ae7b 100644 --- a/src/pygambit/cli/common.py +++ b/src/pygambit/cli/common.py @@ -23,6 +23,7 @@ from __future__ import annotations +import csv import functools import io import pathlib @@ -32,7 +33,6 @@ import numpy as np import pygambit as gbt -from pygambit.gambit import _history_of _GAME_FORMATS = ( ("NFG", gbt.read_nfg), @@ -266,52 +266,60 @@ def render_support_csv( def render_profile_detail( profile: gbt.MixedStrategyProfile | gbt.MixedBehaviorProfile, decimals: int, + fixed: bool = True, + as_float: bool = False, ) -> str: """Render a strategy or behavior profile as a human-readable description, matching `MixedStrategyProfileDetailRenderer` and - `MixedBehaviorProfileDetailRenderer` in the C++ tools. + `MixedBehaviorProfileDetailRenderer` in the C++ tools. See `format_value` for + the meaning of `fixed` and `as_float`. """ if _is_behavior_profile(profile): - return _render_behavior_detail(profile, decimals) - return _render_strategy_detail(profile, decimals) + return _render_behavior_detail(profile, decimals, fixed, as_float) + return _render_strategy_detail(profile, decimals, fixed, as_float) -def _name_or_number(node: gbt.Node) -> str: - # Gambit's Python API numbers nodes from 0; the C++ tools display the - # underlying (1-based) engine numbering. `Node._label`/`._number` are private - # (not part of the public API) but still Python-accessible, same as - # `Game._get_infosets` just below -- rendering needs the node's own label/engine - # number, which a History alone cannot provide. - return node._label if node._label else str(node._number() + 1) +def _render_history(actions: tuple[str, ...]) -> str: + # Comma-joining alone is ambiguous: action labels may themselves contain commas + # (or quotes), so e.g. actions "A,B" then "C" and "A" then "B,C" would both render + # as "A,B,C". CSV quote-when-needed is a standard, already-correct fix for exactly + # this problem. + buf = io.StringIO() + csv.writer(buf, lineterminator="").writerow(actions) + return buf.getvalue() or "(root)" -def _render_strategy_detail(profile: gbt.MixedStrategyProfile, decimals: int) -> str: +def _render_strategy_detail( + profile: gbt.MixedStrategyProfile, decimals: int, fixed: bool = True, as_float: bool = False +) -> str: lines = [] - for number, player in enumerate(profile.game.players, start=1): - lines.append(f"Strategy profile for player {number}:") + for player in profile.game.players: + lines.append(f"Strategy profile for {player}:") lines.append("Strategy Prob Value") lines.append("-------- ----------- -----------") probs = profile[player] values = profile.strategy_values[player] for strategy in profile.game.get_strategies(player): - prob = format_value(probs[strategy], decimals) - value = format_value(values[strategy], decimals) + prob = format_value(probs[strategy], decimals, fixed, as_float) + value = format_value(values[strategy], decimals, fixed, as_float) lines.append(f"{strategy:>8} {prob:>10} {value:>11}") return "\n".join(lines) -def _render_behavior_detail(profile: gbt.MixedBehaviorProfile, decimals: int) -> str: +def _render_behavior_detail( + profile: gbt.MixedBehaviorProfile, decimals: int, fixed: bool = True, as_float: bool = False +) -> str: lines = [] action_values = profile.action_values beliefs = profile.beliefs realiz_probs = profile.realiz_probs - for number, player in enumerate(profile.game.players, start=1): - lines.append(f"Behavior profile for player {number}:") + for player in profile.game.players: + lines.append(f"Behavior profile for {player}:") lines.append("Infoset Action Prob Value") lines.append("------- ------- ----------- -----------") - # Numbered by position among the player's information sets: Infoset no - # longer exists as an object, so there is currently no infoset-level label - # to prefer over this, unlike _name_or_number's use for players/nodes/actions. + # Numbered by position among the player's information sets -- a compact key + # matched against the same numbering in the History table below, rather than + # a description; see that table's own comment. for infoset_number, (history, (_, mixed_action)) in enumerate( zip(profile.game.get_infosets(player), profile[player], strict=True), start=1 ): @@ -320,32 +328,52 @@ def _render_behavior_detail(profile: gbt.MixedBehaviorProfile, decimals: int) -> for action in profile.game.get_actions(selector): prob = mixed_action[action] value = values[action] - value_text = format_value(value, decimals) if value is not None else "" + value_text = ( + format_value(value, decimals, fixed, as_float) if value is not None else "" + ) lines.append( f"{infoset_number:>7} {action:>7} " - f"{format_value(prob, decimals):>11} {value_text:>11}" + f"{format_value(prob, decimals, fixed, as_float):>11} {value_text:>11}" ) lines.append("") - lines.append("Infoset Node Belief Prob") - lines.append("------- ------- ----------- -----------") - # Uses the private, Node-returning `_get_infosets` (rather than the public - # History-returning `get_infosets`) because rendering needs each member's own - # `.label`/`.number` for display, which a History alone cannot provide; - # `Node.members` (still public) then gives the other members directly, and - # `_history_of` recovers the History each one needs to index - # `beliefs`/`realiz_probs` with. - for infoset_number, node in enumerate(profile.game._get_infosets(player), start=1): - for member in node.members: - node_name = _name_or_number(member) - member_history = _history_of(member) - belief = beliefs[member_history] - belief_text = format_value(belief, decimals) if belief is not None else "" - realiz_text = format_value(realiz_probs[member_history], decimals) - lines.append( - f"{infoset_number:>7} {node_name:>7} " - f"{belief_text:>11} {realiz_text:>11}" - ) + # One row per member of each of the player's information sets, identified by + # its History (see `_render_history`) rather than an engine-assigned node + # number: the `Infoset` column is still the same encounter-order index as the + # table above (a compact key for matching rows across the two tables, not a + # description -- the first member listed under each infoset number is always + # its canonical one, so its own History is already visible there for anyone + # who wants it). + rows = [ + (infoset_number, _render_history(member.actions), beliefs[member], + realiz_probs[member]) + for infoset_number, history in enumerate(profile.game.get_infosets(player), start=1) + for member in profile.game.get_members(gbt.H.path(*history.actions)) + ] + history_width = max([len("History")] + [len(row[1]) for row in rows]) + lines.append(f"Infoset {'History':<{history_width}} Belief Prob") + lines.append(f"------- {'-' * history_width} ----------- -----------") + for infoset_number, history_text, belief, realiz in rows: + belief_text = ( + format_value(belief, decimals, fixed, as_float) if belief is not None else "" + ) + lines.append( + f"{infoset_number:>7} {history_text:<{history_width}} " + f"{belief_text:>11} {format_value(realiz, decimals, fixed, as_float):>11}" + ) lines.append("") + # The probability distribution over plays: not player-specific (unlike the tables + # above), so printed once for the whole profile rather than once per player. + plays = [ + (_render_history(play.actions), realiz_probs[play]) + for play in profile.game.get_histories(gbt.H.plays) + ] + play_width = max([len("Play")] + [len(text) for text, _ in plays]) + lines.append("Probability distribution over full histories:") + lines.append(f"{'Play':<{play_width}} Prob") + lines.append(f"{'-' * play_width} -----------") + for play_text, prob in plays: + prob_text = format_value(prob, decimals, fixed, as_float) + lines.append(f"{play_text:<{play_width}} {prob_text:>11}") return "\n".join(lines) diff --git a/src/pygambit/cli/enummixed.py b/src/pygambit/cli/enummixed.py index 989d6b6e90..642ba80eae 100644 --- a/src/pygambit/cli/enummixed.py +++ b/src/pygambit/cli/enummixed.py @@ -31,6 +31,7 @@ handle_errors, load_game, render_profile_csv, + render_profile_detail, version_option, ) @@ -55,27 +56,33 @@ help="compute using floating-point arithmetic; display results with DECIMALS digits", ) @click.option("-c", "--cliques", is_flag=True, help="output connectedness information") +@click.option("-D", "--detail", is_flag=True, help="print detailed information about equilibria") @click.option("-q", "--quiet", is_flag=True, help="quiet mode (suppresses banner)") @version_option(DESCRIPTION) @handle_errors -def main(file: str | None, decimals: int | None, cliques: bool, quiet: bool) -> None: +def main( + file: str | None, decimals: int | None, cliques: bool, detail: bool, quiet: bool +) -> None: game = load_game(quiet, DESCRIPTION, file, PROG_NAME) rational = decimals is None - def render(profile) -> None: - click.echo(render_profile_csv(profile, "NE", decimals or 0)) + def echo_profile(profile, label: str) -> None: + if detail: + click.echo(render_profile_detail(profile, decimals or 0)) + else: + click.echo(render_profile_csv(profile, label, decimals or 0)) result = gbt.nash.enummixed_solve( game, rational=rational, - nash_callback=render, + nash_callback=lambda profile: echo_profile(profile, "NE"), cliques=cliques, ) if cliques: for index, clique in enumerate(result.cliques, start=1): label = f"convex-{index}" for profile in clique: - click.echo(render_profile_csv(profile, label, decimals or 0)) + echo_profile(profile, label) if __name__ == "__main__": diff --git a/src/pygambit/cli/enumpoly.py b/src/pygambit/cli/enumpoly.py index 9675c1d8ea..aef54b1d3c 100644 --- a/src/pygambit/cli/enumpoly.py +++ b/src/pygambit/cli/enumpoly.py @@ -40,6 +40,7 @@ handle_errors, load_game, render_profile_csv, + render_profile_detail, render_support_csv, validate_stop_after, version_option, @@ -92,6 +93,7 @@ help="maximum number of rectangles to examine when searching for roots on a " "single support, before giving up on that support", ) +@click.option("-D", "--detail", is_flag=True, help="print detailed information about equilibria") @click.option("-q", "--quiet", is_flag=True, help="quiet mode (suppresses banner)") @click.option( "-V", @@ -108,6 +110,7 @@ def main( maxregret: float, stop_after: int | None, max_rectangles: int, + detail: bool, quiet: bool, verbose: bool, ) -> None: @@ -116,7 +119,10 @@ def main( raise ValueError("Computing equilibria of games with imperfect recall is not supported.") def render(profile) -> None: - click.echo(render_profile_csv(profile, "NE", decimals)) + if detail: + click.echo(render_profile_detail(profile, decimals)) + else: + click.echo(render_profile_csv(profile, "NE", decimals)) def render_event(event) -> None: if not verbose: diff --git a/src/pygambit/cli/gnm.py b/src/pygambit/cli/gnm.py index 5ca1746ef6..a6d2a5fc08 100644 --- a/src/pygambit/cli/gnm.py +++ b/src/pygambit/cli/gnm.py @@ -36,6 +36,7 @@ print_banner, read_game, render_profile_csv, + render_profile_detail, resolve_strategy_starts, version_option, ) @@ -115,6 +116,7 @@ type=int, help="number of steps in each support cell", ) +@click.option("-D", "--detail", is_flag=True, help="print detailed information about equilibria") @click.option("-q", "--quiet", is_flag=True, help="quiet mode (suppresses banner)") @click.option( "-V", @@ -134,6 +136,7 @@ def main( local_newton_interval: int, local_newton_maxits: int, steps: int, + detail: bool, quiet: bool, verbose: bool, ) -> None: @@ -151,7 +154,10 @@ def main( perturbations = resolve_strategy_starts(game, n_vectors, seed, start_file) def render(profile, label: str = "NE") -> None: - click.echo(render_profile_csv(profile, label, decimals)) + if detail: + click.echo(render_profile_detail(profile, decimals)) + else: + click.echo(render_profile_csv(profile, label, decimals)) def render_event(event) -> None: if not verbose: diff --git a/src/pygambit/cli/hp.py b/src/pygambit/cli/hp.py index add1c43656..202d9ac382 100644 --- a/src/pygambit/cli/hp.py +++ b/src/pygambit/cli/hp.py @@ -34,6 +34,7 @@ handle_errors, load_game, render_profile_csv, + render_profile_detail, resolve_strategy_starts, version_option, ) @@ -88,6 +89,7 @@ type=float, help="maximum regret acceptable as a proportion of the range of payoffs in the game", ) +@click.option("-D", "--detail", is_flag=True, help="print detailed information about equilibria") @click.option("-q", "--quiet", is_flag=True, help="quiet mode (suppresses banner)") @click.option( "-V", @@ -104,23 +106,30 @@ def main( seed: int | None, start_file: str | None, maxregret: float, + detail: bool, quiet: bool, verbose: bool, ) -> None: game = load_game(quiet, DESCRIPTION, file, PROG_NAME) priors = resolve_strategy_starts(game, n_priors, seed, start_file) + def render(profile, label: str) -> None: + if detail: + click.echo(render_profile_detail(profile, decimals)) + else: + click.echo(render_profile_csv(profile, label, decimals)) + def render_event(event: gbt.HPStepEvent) -> None: if verbose: - click.echo(render_profile_csv(event.profile, f"{event.t:.6g}", decimals)) + render(event.profile, f"{event.t:.6g}") for prior in priors: prior = prior.as_float() if verbose: - click.echo(render_profile_csv(prior, "prior", decimals)) + render(prior, "prior") result = gbt.nash.hp_solve(prior, maxregret=maxregret, event_callback=render_event) if result.equilibrium is not None: - click.echo(render_profile_csv(result.equilibrium, "NE", decimals)) + render(result.equilibrium, "NE") if __name__ == "__main__": diff --git a/src/pygambit/cli/ipa.py b/src/pygambit/cli/ipa.py index d12ed82352..6110a8029d 100644 --- a/src/pygambit/cli/ipa.py +++ b/src/pygambit/cli/ipa.py @@ -34,6 +34,7 @@ handle_errors, load_game, render_profile_csv, + render_profile_detail, resolve_strategy_starts, version_option, ) @@ -81,6 +82,7 @@ default=None, help="file containing perturbation vectors (mutually exclusive with -n)", ) +@click.option("-D", "--detail", is_flag=True, help="print detailed information about equilibria") @click.option("-q", "--quiet", is_flag=True, help="quiet mode (suppresses banner)") @click.option( "-V", @@ -96,6 +98,7 @@ def main( n_vectors: int | None, seed: int | None, start_file: str | None, + detail: bool, quiet: bool, verbose: bool, ) -> None: @@ -103,7 +106,10 @@ def main( perturbations = resolve_strategy_starts(game, n_vectors, seed, start_file) def render(profile, label: str = "NE") -> None: - click.echo(render_profile_csv(profile, label, decimals)) + if detail: + click.echo(render_profile_detail(profile, decimals)) + else: + click.echo(render_profile_csv(profile, label, decimals)) def render_event(event) -> None: if not verbose: diff --git a/src/pygambit/cli/liap.py b/src/pygambit/cli/liap.py index 4609de9702..23cefb6abd 100644 --- a/src/pygambit/cli/liap.py +++ b/src/pygambit/cli/liap.py @@ -34,6 +34,7 @@ handle_errors, load_game, render_profile_csv, + render_profile_detail, resolve_behavior_starts, resolve_strategy_starts, version_option, @@ -102,6 +103,7 @@ default=None, help="file containing starting points (mutually exclusive with -n)", ) +@click.option("-D", "--detail", is_flag=True, help="print detailed information about equilibria") @click.option("-q", "--quiet", is_flag=True, help="quiet mode (suppresses banner)") @click.option( "-V", @@ -120,6 +122,7 @@ def main( maxiter: int, maxregret: float, start_file: str | None, + detail: bool, quiet: bool, verbose: bool, ) -> None: @@ -127,7 +130,10 @@ def main( use_agent = agent and game.is_tree def render(profile, label: str = "NE") -> None: - click.echo(render_profile_csv(profile, label, decimals)) + if detail: + click.echo(render_profile_detail(profile, decimals)) + else: + click.echo(render_profile_csv(profile, label, decimals)) def render_event(event) -> None: if not verbose: diff --git a/src/pygambit/cli/logit.py b/src/pygambit/cli/logit.py index 37f1bd5c65..791fbfd74a 100644 --- a/src/pygambit/cli/logit.py +++ b/src/pygambit/cli/logit.py @@ -33,6 +33,7 @@ handle_errors, load_game, render_profile_csv, + render_profile_detail, version_option, ) @@ -159,6 +160,16 @@ def _read_frequencies(path: str, game: gbt.Game) -> gbt.MixedStrategyProfileDoub "bifurcations are already reported as part of the traced branch otherwise" ), ) +@click.option( + "-D", + "--detail", + is_flag=True, + help=( + "print detailed information about the terminal equilibrium, if tracing reaches " + "one accepted as a Nash equilibrium (has no effect on points printed along the " + "way, or on -L/-l output, which are not themselves accepted equilibria)" + ), +) @click.option("-q", "--quiet", is_flag=True, help="quiet mode (suppresses banner)") @version_option(DESCRIPTION) @handle_errors @@ -173,12 +184,19 @@ def main( mle_file: str | None, terminal_only: bool, report_bifurcations: bool, + detail: 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 render_equilibrium(profile) -> None: + if detail: + click.echo(render_profile_detail(profile, decimals, fixed=False)) + else: + click.echo(render_profile_csv(profile, "NE", decimals, fixed=False)) + def stream(event) -> None: if isinstance(event, gbt.LogitPathEvent): click.echo(_render_qre_row(event.qre, decimals)) @@ -231,7 +249,8 @@ def stream_bifurcations_only(event) -> None: max_accel=max_accel, event_callback=event_callback, ) - click.echo(render_profile_csv(result.equilibrium, "NE", decimals, fixed=False)) + if result.equilibrium is not None: + render_equilibrium(result.equilibrium) return result = gbt.nash.logit_solve( @@ -242,7 +261,8 @@ def stream_bifurcations_only(event) -> None: max_accel=max_accel, event_callback=event_callback, ) - click.echo(render_profile_csv(result.equilibrium, "NE", decimals, fixed=False)) + if result.equilibrium is not None: + render_equilibrium(result.equilibrium) if __name__ == "__main__": diff --git a/src/pygambit/cli/simpdiv.py b/src/pygambit/cli/simpdiv.py index 77939ec153..11881775a0 100644 --- a/src/pygambit/cli/simpdiv.py +++ b/src/pygambit/cli/simpdiv.py @@ -38,6 +38,7 @@ read_game, read_strategy_profiles_csv, render_profile_csv, + render_profile_detail, version_option, ) @@ -117,6 +118,7 @@ def _default_start(game: gbt.Game) -> gbt.MixedStrategyProfileRational: help="maximum regret acceptable as a proportion of range of payoffs in the game " "(default is 1/10000000)", ) +@click.option("-D", "--detail", is_flag=True, help="print detailed information about equilibria") @click.option("-q", "--quiet", is_flag=True, help="quiet mode (suppresses banner)") @click.option( "-V", @@ -135,6 +137,7 @@ def main( start_file: str | None, decimals: int, maxregret: str | None, + detail: bool, quiet: bool, verbose: bool, ) -> None: @@ -161,7 +164,10 @@ def main( starts = [_default_start(game)] def render(profile, label: str = "NE") -> None: - click.echo(render_profile_csv(profile, label, decimals, as_float=as_float)) + if detail: + click.echo(render_profile_detail(profile, decimals, as_float=as_float)) + else: + click.echo(render_profile_csv(profile, label, decimals, as_float=as_float)) def render_event(event) -> None: if not verbose: diff --git a/tests/cli/test_enumpure.py b/tests/cli/test_enumpure.py index 448111bf24..5e707abbee 100644 --- a/tests/cli/test_enumpure.py +++ b/tests/cli/test_enumpure.py @@ -27,8 +27,8 @@ def test_detail_flag_switches_to_multiline_report(cli_runner, nfg_coordination_t detail_result = cli_runner.invoke(enumpure.main, ["-q", "-D"], input=nfg_coordination_text) assert detail_result.exit_code == 0 assert "NE," not in detail_result.stdout - assert "Strategy profile for player 1:" in detail_result.stdout - assert "Strategy profile for player 2:" in detail_result.stdout + assert "Strategy profile for 1:" in detail_result.stdout + assert "Strategy profile for 2:" in detail_result.stdout assert len(detail_result.stdout.splitlines()) > len(csv_result.stdout.splitlines()) diff --git a/tests/cli/test_lcp.py b/tests/cli/test_lcp.py index fa1a578cb8..ae279963a7 100644 --- a/tests/cli/test_lcp.py +++ b/tests/cli/test_lcp.py @@ -19,7 +19,7 @@ def test_detail_flag_switches_to_multiline_report(cli_runner, nfg_coordination_t result = cli_runner.invoke(lcp.main, ["-q", "-D"], input=nfg_coordination_text) assert result.exit_code == 0 assert "NE," not in result.stdout - assert "Strategy profile for player 1:" in result.stdout + assert "Strategy profile for 1:" in result.stdout def test_strategic_flag_switches_tree_game_to_strategy_representation( diff --git a/tests/cli/test_lp.py b/tests/cli/test_lp.py index 1d3767e101..3292644cbe 100644 --- a/tests/cli/test_lp.py +++ b/tests/cli/test_lp.py @@ -19,7 +19,7 @@ def test_detail_flag_switches_to_multiline_report(cli_runner, nfg_matching_penni result = cli_runner.invoke(lp.main, ["-q", "-D"], input=nfg_matching_pennies_text) assert result.exit_code == 0 assert "NE," not in result.stdout - assert "Strategy profile for player 1:" in result.stdout + assert "Strategy profile for 1:" in result.stdout def test_strategic_flag_switches_tree_game_to_strategy_representation( @@ -33,8 +33,8 @@ def test_strategic_flag_switches_tree_game_to_strategy_representation( strategic_result = cli_runner.invoke(lp.main, ["-q", "-D", "-S"], input=efg_small_tree_text) assert behavior_result.exit_code == 0 assert strategic_result.exit_code == 0 - assert "Behavior profile for player 1:" in behavior_result.stdout - assert "Strategy profile for player 1:" in strategic_result.stdout + assert "Behavior profile for 1:" in behavior_result.stdout + assert "Strategy profile for 1:" in strategic_result.stdout def test_non_constant_sum_game_is_a_clean_error(cli_runner, nfg_coordination_text):