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
3 changes: 2 additions & 1 deletion .all-contributorsrc
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,8 @@
"avatar_url": "https://avatars.githubusercontent.com/u/83069628?v=4",
"profile": "https://github.com/ameliekleber",
"contributions": [
"test"
"test",
"code"
]
}
],
Expand Down
1 change: 1 addition & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
- Added `Game.get_strategies(player)`, `Game.get_sequences(player)`, `Game.get_min_payoff(player)`,
and `Game.get_max_payoff(player)`, replacing `Player.strategies`/`.sequences`/`.min_payoff`/
`.max_payoff` now that `Player` has been removed.
- In the GUI, computed strategy and behaviour profiles can now be sorted by probability (#1077)

### Changed
- `Infoset` is now a lazy, node-anchored view (like `Node.player`/`Node.outcome`), constructed
Expand Down
4 changes: 4 additions & 0 deletions Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,10 @@ gambit_SOURCES = \
src/gui/nfgprofile.h \
src/gui/nfgtable.cc \
src/gui/nfgtable.h \
src/gui/profilelabels.cc \
src/gui/profilelabels.h \
src/gui/profilesort.cc \
src/gui/profilesort.h \
src/gui/renratio.cc \
src/gui/renratio.h \
src/gui/style.cc \
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ pip install pygambit
<td align="center" valign="top" width="14.28%"><a href="https://github.com/skunath"><img src="https://avatars.githubusercontent.com/u/144108?v=4?s=100" width="100px;" alt="S Kunath"/><br /><sub><b>S Kunath</b></sub></a><br /><a href="https://github.com/gambitproject/gambit/commits?author=skunath" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/andrioni"><img src="https://avatars.githubusercontent.com/u/159177?v=4?s=100" width="100px;" alt="Alessandro Andrioni"/><br /><sub><b>Alessandro Andrioni</b></sub></a><br /><a href="https://github.com/gambitproject/gambit/commits?author=andrioni" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/EwoutH"><img src="https://avatars.githubusercontent.com/u/15776622?v=4?s=100" width="100px;" alt="Ewout ter Hoeven"/><br /><sub><b>Ewout ter Hoeven</b></sub></a><br /><a href="https://github.com/EwoutH" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/gambitproject/gambit/commits?author=EwoutH" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ameliekleber"><img src="https://avatars.githubusercontent.com/u/83069628?v=4?s=100" width="100px;" alt="ameliekleber"/><br /><sub><b>ameliekleber</b></sub></a><br /><a href="https://github.com/gambitproject/gambit/commits?author=ameliekleber" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ameliekleber"><img src="https://avatars.githubusercontent.com/u/83069628?v=4?s=100" width="100px;" alt="ameliekleber"/><br /><sub><b>ameliekleber</b></sub></a><br /><a href="https://github.com/gambitproject/gambit/commits?author=ameliekleber" title="Tests">⚠️</a> <a href="https://github.com/gambitproject/gambit/commits?author=ameliekleber" title="Code">💻</a></td>
</tr>
</tbody>
</table>
Expand Down
17 changes: 17 additions & 0 deletions doc/gui.nash.rst
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,23 @@ since only one computation has been run in this example, it shows
brief description of the method used to compute the equilibria is
listed across the top of the profiles panel.

By default the equilibria are listed in the order in which they were
computed. Clicking on the label of a column in the profiles listing
sorts the equilibria by the probability with which that action or
strategy is played; clicking on the same column label again reverses
the order. The label of the column sorted on is highlighted and
carries an arrow, :guilabel:`▲` or :guilabel:`▼`, showing the
direction of the sort. Equilibria which agree on that
column are ordered by
comparing their profiles entry-by-entry from the leftmost column
onwards, so sorting on the first column lists the equilibria in
lexicographic order by profile. Clicking on the :guilabel:`#` label in
the top left corner of the listing restores the order in which the
equilibria were computed. Sorting only changes the order in which the
equilibria are listed: the number shown at the left of each row always
identifies the equilibrium itself, so it does not change when the list
is re-sorted.

Some methods for computing equilibria construct good numerical
approximations to equilibrium points rather than exact values; for
these methods, the computed equilibria are stored in floating-point
Expand Down
67 changes: 67 additions & 0 deletions src/gui/analysis.cc
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,73 @@ std::string AnalysisProfileList<T>::GetStrategyValue(int p_strategy, int p_index
}
}

namespace {

/// Order two entries, either of which may be undefined; undefined entries
/// order after defined ones. Only operator< is used, so entries are ordered
/// exactly in whatever type the profiles are stored in.
template <class T>
int CompareEntries(const std::optional<T> &p_left, const std::optional<T> &p_right)
{
if (!p_left.has_value()) {
return p_right.has_value() ? 1 : 0;
}
if (!p_right.has_value()) {
return -1;
}
if (p_left.value() < p_right.value()) {
return -1;
}
if (p_right.value() < p_left.value()) {
return 1;
}
return 0;
}

} // end anonymous namespace

template <class T>
std::optional<T> AnalysisProfileList<T>::GetActionProbEntry(int p_action, int p_index) const
{
try {
const MixedBehaviorProfile<T> &profile = *m_behavProfiles[p_index];

if (!profile.IsDefinedAt(m_doc->GetAction(p_action)->GetInfoset())) {
return {};
}

return profile[p_action];
}
catch (std::out_of_range &) {
return {};
}
}

template <class T>
std::optional<T> AnalysisProfileList<T>::GetStrategyProbEntry(int p_strategy, int p_index) const
{
try {
return (*m_mixedProfiles[p_index])[p_strategy];
}
catch (std::out_of_range &) {
return {};
}
}

template <class T>
int AnalysisProfileList<T>::CompareActionProb(int p_action, int p_left, int p_right) const
{
return CompareEntries(GetActionProbEntry(p_action, p_left),
GetActionProbEntry(p_action, p_right));
}

template <class T>
int AnalysisProfileList<T>::CompareStrategyProb(int p_strategy, int p_left, int p_right) const
{
return CompareEntries(GetStrategyProbEntry(p_strategy, p_left),
GetStrategyProbEntry(p_strategy, p_right));
}

template <class T> LegacyWorkspaceFile::Analysis AnalysisProfileList<T>::Save() const
{
LegacyWorkspaceFile::Analysis result;
Expand Down
19 changes: 19 additions & 0 deletions src/gui/analysis.h
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,16 @@ class AnalysisOutput {
virtual std::string GetStrategyProb(int p_strategy, int p_index = -1) const = 0;
virtual std::string GetStrategyValue(int p_strategy, int p_index = -1) const = 0;

/// Compare the probability of an action in two profiles, for ordering
/// them; negative, zero, or positive as the probability in p_left is less
/// than, equal to, or greater than that in p_right. Probabilities are
/// compared in the type the profiles are stored in, so that exact
/// representations are ordered exactly. A profile which does not define
/// behavior at the action's information set orders after those which do.
virtual int CompareActionProb(int p_action, int p_left, int p_right) const = 0;
/// Compare the probability of a strategy in two profiles, as above
virtual int CompareStrategyProb(int p_strategy, int p_left, int p_right) const = 0;

/// Map all behavior profiles to corresponding mixed profiles
virtual void BuildNfg() = 0;

Expand Down Expand Up @@ -173,6 +183,8 @@ template <class T> class AnalysisProfileList final : public AnalysisOutput {
std::string GetActionProb(int p_action, int p_index = -1) const override;
std::string GetStrategyProb(int p_strategy, int p_index = -1) const override;
std::string GetStrategyValue(int p_strategy, int p_index = -1) const override;
int CompareActionProb(int p_action, int p_left, int p_right) const override;
int CompareStrategyProb(int p_strategy, int p_left, int p_right) const override;

/// Get the index of the currently selected profile
int GetCurrent() const override { return m_current; }
Expand Down Expand Up @@ -202,6 +214,13 @@ template <class T> class AnalysisProfileList final : public AnalysisOutput {
void Load(const LegacyWorkspaceFile::Analysis &p_analysis);
LegacyWorkspaceFile::Analysis Save() const override;
//@}

private:
/// The probability of an action in a profile, empty if the profile does
/// not define behavior at the action's information set
std::optional<T> GetActionProbEntry(int p_action, int p_index) const;
/// The probability of a strategy in a profile
std::optional<T> GetStrategyProbEntry(int p_strategy, int p_index) const;
};

} // namespace Gambit::GUI
Expand Down
65 changes: 58 additions & 7 deletions src/gui/efgprofile.cc
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ MixedBehaviorProfileList::MixedBehaviorProfileList(wxWindow *p_parent,
{
CreateGrid(0, 0);

// The table takes ownership of the provider
m_labels = new ProfileLabelProvider;
GetTable()->SetAttrProvider(m_labels);

SetRowLabelSize(40);
SetColLabelSize(25);
SetCornerLabelValue(wxT("#"));
Expand All @@ -50,6 +54,24 @@ MixedBehaviorProfileList::MixedBehaviorProfileList(wxWindow *p_parent,
SetCellHighlightPenWidth(0);
SetCellHighlightROPenWidth(0);

#if wxCHECK_VERSION(3, 3, 0)
// Suppress wxGrid's own label highlighting, which follows the grid cursor.
// The cursor is meaningless here, and cannot be moved out of the way, as
// wxGrid puts it back on the first cell whenever the grid is repainted;
// the labels worth highlighting are chosen in OnUpdate() instead.
// Versions before 3.3 do not highlight labels, so there is nothing to do.
DisableOverlaySelection();
#endif

// The column and corner labels sort the list, so show them as clickable
GetGridColLabelWindow()->SetCursor(wxCursor(wxCURSOR_HAND));
GetGridCornerLabelWindow()->SetCursor(wxCursor(wxCURSOR_HAND));

GetGridColLabelWindow()->SetToolTip(_("Click an action to sort the profiles by its probability; "
"click it again to reverse the order"));
GetGridCornerLabelWindow()->SetToolTip(
_("Click to list the profiles in the order in which they were computed"));

Bind(wxEVT_GRID_LABEL_LEFT_CLICK, &MixedBehaviorProfileList::OnLabelClick, this);
Bind(wxEVT_GRID_CELL_LEFT_CLICK, &MixedBehaviorProfileList::OnCellClick, this);
Bind(wxEVT_GRID_SELECT_CELL, &MixedBehaviorProfileList::OnSelectCell, this);
Expand All @@ -59,16 +81,28 @@ MixedBehaviorProfileList::~MixedBehaviorProfileList() = default;

void MixedBehaviorProfileList::OnLabelClick(wxGridEvent &p_event)
{
if (p_event.GetCol() == -1) {
m_doc->DoSelectProfile(p_event.GetRow() + 1);
if (p_event.GetCol() == -1 && p_event.GetRow() == -1) {
// The corner label: restore the order in which the profiles were computed
p_event.Veto();
m_sortOrder.Reset();
OnUpdate();
}
else if (p_event.GetCol() == -1) {
m_doc->DoSelectProfile(m_sortOrder.GetProfile(p_event.GetRow()));
}
else if (p_event.GetRow() == -1) {
// An action: sort the profiles on the probability it is played with
p_event.Veto();
m_sortOrder.ToggleColumn(p_event.GetCol() + 1);
OnUpdate();
}

ClearSelection();
}

void MixedBehaviorProfileList::OnCellClick(wxGridEvent &p_event)
{
m_doc->DoSelectProfile(p_event.GetRow() + 1);
m_doc->DoSelectProfile(m_sortOrder.GetProfile(p_event.GetRow()));
ClearSelection();
}

Expand Down Expand Up @@ -103,17 +137,20 @@ void MixedBehaviorProfileList::ResizeGrid(int p_rows, int p_cols)

void MixedBehaviorProfileList::UpdateLabels()
{
// Row labels identify the profile itself, so that a profile keeps its
// number however the list happens to be sorted.
for (int row = 0; row < GetNumberRows(); ++row) {
wxString label;
label << (row + 1);
label << m_sortOrder.GetProfile(row);
SetRowLabelValue(row, label);
}

for (int col = 0; col < GetNumberCols(); ++col) {
const GameAction action = m_doc->GetAction(col + 1);

wxString label;
label << action->GetInfoset()->GetNumber() << ": " << action->GetLabel();
label << action->GetInfoset()->GetNumber() << ": " << action->GetLabel()
<< wxString::FromUTF8(m_sortOrder.GetColumnMarker(col + 1));
SetColLabelValue(col, label);
}
}
Expand All @@ -126,14 +163,16 @@ void MixedBehaviorProfileList::UpdateCells()
const wxFont boldFont(10, wxFONTFAMILY_SWISS, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD);

for (int row = 0; row < GetNumberRows(); ++row) {
const int profile = m_sortOrder.GetProfile(row);

for (int col = 0; col < GetNumberCols(); ++col) {
SetCellValue(
row, col,
wxString(m_doc->GetWorkspace().GetProfiles().GetActionProb(col + 1, row + 1).c_str(),
wxString(m_doc->GetWorkspace().GetProfiles().GetActionProb(col + 1, profile).c_str(),
*wxConvCurrent));

wxGridCellAttr *attr = new wxGridCellAttr;
attr->SetFont(row + 1 == currentProfile ? boldFont : normalFont);
attr->SetFont(profile == currentProfile ? boldFont : normalFont);
attr->SetAlignment(wxALIGN_CENTER, wxALIGN_CENTER);
attr->SetReadOnly(true);

Expand All @@ -158,6 +197,13 @@ void MixedBehaviorProfileList::UpdateCells()
}
}

void MixedBehaviorProfileList::UpdateHighlights()
{
// GetColumn() numbers columns from one, and is zero when unsorted
m_labels->SetSortedColumn(m_sortOrder.GetColumn() - 1);
m_labels->SetSelectedRow(m_sortOrder.GetRow(m_doc->GetWorkspace().GetCurrentProfile()));
}

void MixedBehaviorProfileList::OnUpdate()
{
if (!m_doc->GetGame() || m_doc->GetWorkspace().NumProfileLists() == 0) {
Expand All @@ -176,6 +222,11 @@ void MixedBehaviorProfileList::OnUpdate()
BeginBatch();

ResizeGrid(profiles.NumProfiles(), profileLength);
m_sortOrder.Rebuild(profiles.NumProfiles(), profileLength,
[&](int p_col, int p_left, int p_right) {
return profiles.CompareActionProb(p_col, p_left, p_right);
});
UpdateHighlights();
UpdateLabels();
UpdateCells();

Expand Down
9 changes: 9 additions & 0 deletions src/gui/efgprofile.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,25 @@
#include <wx/grid.h>

#include "gamedoc.h"
#include "profilelabels.h"
#include "profilesort.h"

namespace Gambit::GUI {
class MixedBehaviorProfileList final : public wxGrid, public GameView {
/// The order in which the profiles are listed
ProfileSortOrder m_sortOrder;
/// Highlights the sorted column and the selected profile's row; owned by
/// the grid's table, which deletes it
ProfileLabelProvider *m_labels{nullptr};

// Event handlers
void OnLabelClick(wxGridEvent &);
void OnCellClick(wxGridEvent &);
void OnSelectCell(wxGridEvent &);

void ResizeGrid(int p_rows, int p_cols);
void UpdateLabels();
void UpdateHighlights();
void UpdateCells();

// Overriding GameView members
Expand Down
Loading
Loading