Draft of histories operator concept - #1111
Closed
tturocy wants to merge 13 commits into
Closed
Conversation
First working slice of the H expression-engine design: a game-neutral Selector built by H.path(*steps)/H.plays, evaluated only when handed to Game.get_nodes (internal, returns Node) or Game.get_histories (public, materializes plain History tuples). Reuses Node's existing navigation (.children, .plays) rather than new C++ traversal code. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Seed form searches the whole game (via game.nodes) for any node whose own trailing labels match; chained form is a pure filter over whatever is already selected. Evaluator now tracks whether a selection has been seeded yet, since .after's seed candidates differ from .path/.plays's (root-anchored) default. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
GroupedSelector wraps a base Selector plus a key function, game-neutral until Game.get_groups evaluates it into dict[key, list[History]]. HistoryView is what the key callable actually receives: plain sequence indexing/slicing like a History tuple, plus .last_action(player) -- built by walking Node.parent/.player/.prior_action -- but never exposes the Node or game it's privately backed by (verified via hasattr checks inside a probing callback). get_histories refactored to share the new _history_of helper with get_groups instead of duplicating the walk. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Chained-only (no bare H.filter(...) seed -- unlike .after(...), a predicate has no natural whole-game starting domain). Keeps elements where predicate(HistoryView) is truthy; complements .after(...)'s label-pattern matching for anything needing richer navigation like .last_action(player). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
_resolve_nodes now resolves a Selector via get_nodes before its usual node resolution, so both append_move and append_event accept an H expression directly wherever they took Node/NodeReferenceSet before -- no changes needed to either method's own body for the flat case. append_move additionally accepts a GroupedSelector (from .by(...)), dispatching to one append_move call per group via the new Game._group_nodes helper (shares get_groups's logic, keeping Node objects instead of materializing Histories, avoiding a round trip). Verified end-to-end: building Kuhn poker's deal via append_event(H.path(...)) and Alice's three per-card infosets via one append_move(H.path(...).plays.by(lambda h: h[0]), ...) call, confirming is_perfect_recall and each infoset's membership. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
_resolve_node now also accepts a Selector (must resolve to exactly one
node) and a bare History tuple (resolved via Selector().path(*history),
the always-available manual fallback), and _resolve_nodes now excludes
tuple from its "treat as a collection" check the same way it already
excluded str -- so a single History isn't misread as several
single-label node references. append_infoset gets Selector support in
both its nodes and infoset arguments for free through this; make_outcome
gets it through location's existing _resolve_nodes path.
Verified end-to-end: the Absent-Minded Driver (append_move(H.path(),...),
append_infoset(H.path("S"), H.path()), make_outcome at H.path("S","S")
etc.) reproduces is_perfect_recall == False and the expected two-member
root infoset; and passing a get_groups(...) group (a list[tuple]) straight
to make_outcome works, matching the checkpointed Kuhn poker outcomes
pattern.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
GroupedSelector gains .plays/.after(...) (per-group expand/filter, key untouched) and .with_recall(player), which -- from that point on -- makes every subsequent .plays also refine each group's key by folding in player's last action at that point (via the new shared _last_action helper). Scoped to .plays specifically for now, not every expand-style op. Game._group_nodes applies a GroupedSelector's post_ops in order, per-group, doing the recall refinement when with_recall set a player. Surfaced and fixed a real edge case along the way: filtering after a recall-refined .plays can produce empty groups (e.g. a card's "bet first" branch has no "...,Check,Bet" suffix) -- append_move's per-group dispatch now skips empty groups rather than erroring, settling an earlier open design question in favor of "dropped". Verified end-to-end: the full Kuhn poker betting tree, including Alice's second decision reusing a with_recall-tagged partition, reproduces is_perfect_recall == True and the correct 3-infosets-of-2 structure -- the same result the tuple-pivot phase needed a manual get_last_action+get_infoset dance to achieve, now automatic on reuse. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
doc/tutorials/advanced_tutorials/h_selector_prototype.ipynb -- a design prototype, not a released feature, demonstrating the H selector algebra sketched on this branch: Selten's Horse, Kuhn poker (construction + outcomes), bayes2a (a regular two-stage Bayesian game needing neither with_recall nor append_infoset), a new minimal example of imperfect recall via forgetting a past observation (distinct in shape from absent-mindedness and untimeability), the Jakobsen et al. (2016) untimeable game, and an extended Absent-Minded Driver showing append_infoset composes normally with further construction. Every cell actually executed (via jupyter nbconvert --execute) against the real built module, not hand-traced -- outputs are real, including cross-checks against the original .efg/catalog files for Selten's Horse, bayes2a, and the AM-driver-subgame fixture (infoset partitions and outcome maps verified to match exactly before being folded into the notebook's own diagnostics). Also: pip install -e . to make src/pygambit importable directly by the notebook's Jupyter kernel, which doesn't inherit PYTHONPATH the way a shell subprocess does. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Check out this pull request on See visual diffs & provide feedback on Jupyter Notebooks. Powered by ReviewNB |
test_execute_notebook builds nbclient.NotebookClient with kernel_name=nb.metadata["kernelspec"]["name"] -- our notebook never had that key set (built via raw nbformat.v4.new_notebook(), which doesn't populate it the way Jupyter's own UI or an already-kernelspec'd source notebook would), so kernel_name came through as None, which traitlets rejects outright before any cell runs. Verified against the actual CI entry point, not just nbconvert --execute succeeding: ran pytest tests/test_tutorials.py -k h_selector_prototype -m tutorials directly, matching CI's own NotebookClient construction. Passes, and the rest of the tutorials suite (all 10 notebooks) still passes too. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…me_null _resolve_outcome_location already delegated to _resolve_nodes for tree games, which handles Node, History (tuple), Selector, and iterables of these -- so make_outcome/make_outcome_null already worked with H-built selectors and materialized histories, just undocumented and untested. Updates the docstrings and adds coverage for both call sites. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
….members Mirrors Node.members but returns each member's History (a plain tuple) rather than a Node, so a .by(...)/.filter(...) callable can reason about infoset/event membership -- e.g. build a grouping key from it -- without ever touching Node, Infoset, or Event. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Game.root and Game.nodes are removed from the public API -- H selectors and materialized History tuples are now the only entry points into a tree. Game.nodes's only real job was letting calling code build ad hoc selections by hand, which H does directly, so it has no replacement; Game.root is superseded by H.path()/(). Internally, get_nodes()'s H.after(...) whole-game seed and _resolve_node()'s label lookup now route through new private Game._root_node()/_all_nodes() helpers instead of the public properties. GameNodes (which only ever backed Game.nodes) is deleted outright. layout_tree()'s own Node-keyed dict return type is untouched, so its known external consumer (gtdraw) is not affected by this specific change -- though see below. MixedBehaviorProfile.__getitem__/__setitem__/set_mixed_action and MixedBehavior.__getitem__ now take a History (tuple of action labels) instead of a Node, like-for-like -- same resolution semantics, routed through Game._resolve_infoset (already History-capable). Node gains a new `.history` property as the bridge for code that still holds a Node (e.g. from Game.get_infosets/get_events) and needs to index a profile with it. BehaviorSupportProfile's indexing already accepted a History tuple-shaped resolution path once _resolve_infoset_arg was widened, so it picked this up the same way. Fixed three production call sites that broke under their own change: Game._fill_behavior_profile, Game.random_behavior_profile, and src/pygambit/cli/common.py's starting-profile reader all still indexed a MixedBehaviorProfile by Node; now use node.history. Added Game._num_nodes() (an O(1) count via the C++ layer) so catalog.py's n_nodes filter didn't regress into materializing every node just to count them. Updated every test file that constructed or indexed via game.root/ game.nodes (games.py gained root_node()/all_nodes()/history_of() test helpers backing this across the suite) or indexed a MixedBehaviorProfile by Node. Deleted test_strategic_game_root/test_strategic_game_nodes (tested the removed properties' error behavior on strategic games, no longer applicable) and test_nodes_iteration_order (tested Game.nodes's own DFS ordering guarantee, which no longer exists). Full suite green (1898 passed). Not yet done, flagged rather than silently skipped: several tutorial notebooks (02_extensive_form, 03_stripped_down_poker, h_selector_prototype, agent_versus_non_agent_regret, openspiel) call game.root/game.nodes directly and now fail to execute; 04_creating_images additionally fails via gtdraw (an external package), which calls game.root directly in its own layout code -- a real break for that consumer, not just a theoretical risk, and needs a decision on how to handle before touching notebooks. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
02_extensive_form.ipynb, 03_stripped_down_poker.ipynb, and interoperability_tutorials/openspiel.ipynb all built games via game.root/game.root.children[...] chains, and the poker notebook also indexed a MixedBehaviorProfile by Node (eqm[node]/eqm[bob_node]). Switched construction to H.path(...)/H.path(...)-with-wildcards and profile indexing to node.history, matching the API changes landed in 07146c3. Verified two ways: (1) built each fixed game programmatically and diffed its outcome/infoset structure against a reconstruction using the old Node-based navigation (via a get_nodes(H.path())[0] stand-in for the removed game.root) -- identical in every case; (2) executed full copies of the notebooks end-to-end (--allow-errors, outside the repo) and confirmed every remaining error is a `draw(...)` cell failing inside gtdraw itself (an external package -- flagged separately, not fixed here), not a pygambit-API cell. Three other notebooks (h_selector_prototype, agent_versus_non_agent_ regret, 04_creating_images) have no in-repo API usage to fix -- they fail solely through gtdraw's own game.root call in its layout code, so there's nothing to change here; they'll pass once gtdraw is fixed upstream. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Member
Author
|
This has been superseded by a different line of organising this work. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.