Draft of histories operator concept - #1111
Draft
tturocy wants to merge 9 commits into
Draft
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>
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.