Observable search, most of PDDL 3, and a browser research workbench - #151
Open
guilyx wants to merge 3 commits into
Open
Observable search, most of PDDL 3, and a browser research workbench#151guilyx wants to merge 3 commits into
guilyx wants to merge 3 commits into
Conversation
jupyddl could solve PDDL problems but you could never see it work. This adds an instrumentation layer and everything that falls out of it. Instrumentation (jupyddl/trace.py) - SearchObserver protocol with no-op defaults, so subclasses override only what they need; TraceRecorder accumulates a serialisable SearchTrace with JSON round-trip and bounded memory (adaptive thinning); MultiObserver fans out. - Every planner emits start/expand/generate/bound/goal/finish. Observers sit behind `if observer is not None`, so the default path keeps its zero-overhead behaviour, and tests assert an observed search returns the same plan and statistics as an unobserved one. - trace_search() in the high-level API returns (result, trace). Seeing it - jupyddl/live.py: a live terminal dashboard with Unicode sparklines, a frontier gauge and live counters. Stdlib only -- watching a search costs no dependency. Degrades to one-line progress on a non-tty, so it is safe in CI and notebooks. - jupyddl/viz/: a light/dark chart theme validated for colour-vision-deficiency separation, plus search-progress, radial-wavefront, planner-comparison, benchmark-dashboard and plan-timeline charts, a notebook LiveSearchPlot, and animate_search() for MP4/GIF replays. Charts release their figure after writing, so galleries no longer accumulate figures. Running it in a browser (web/) - The playground runs this library unmodified under Pyodide in a web worker. The core being stdlib-only means there is no wheel to resolve: the sources are handed straight to the interpreter. Live charts, an animated wavefront and a four-planner race, all computed in the tab. - web/dist is generated by tools/build_web.py and committed so a plain clone works; a test and the Pages workflow fail if it drifts from the sources. Demo instances (demos/) - Six instances chosen to stress different parts of the framework: gripper, blocksworld8, hanoi (checked against the closed form 2^5-1), logistics (action costs), sokoban (static pruning, dead ends) and elevator (conditional effects). The bundled examples topped out at 127 expansions, which was too small to show a heuristic doing anything. CLI - New: `jupyddl animate` and `jupyddl demo`; `--live/--trace/--plot/--tree/ --plan-plot/--dark` on solve, `--dashboard` on benchmark. Also - tools/make_promo.py renders the project's promo video from numbers it measures at render time by running the real planners; nothing in it is hand-typed. - Test suite grows 92 -> 193, green on Python 3.9 through 3.12. - The viz extra now requires matplotlib>=3.6: the charts use the constrained layout engine API, which 3.5 does not have. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Meb35zHKsyBkH2sbWoyKMT
The parser handled STRIPS plus a little ADL. It now covers 15 of the 21 PDDL requirement flags, and refuses the other 6 by name. Requirements registry (jupyddl/requirements.py) - One table saying what happens to every flag: native, compiled, partial or rejected, each with a reason. The parser validates against it, the CLI prints it, the README table is generated from it and the web UI renders it, so the documentation cannot drift from the behaviour. - A rejected flag fails at parse time with an explanation. Silently ignoring a requirement yields plans that are wrong rather than absent. Full ADL conditions - `or`, `imply`, `exists` and nested `not` in preconditions, goals and `when` conditions. The parser emits negation normal form; the grounder expands quantifiers over the object pool and distributes to DNF, one operator per disjunct, so the search still only ever sees conjunctions. - Disjunctive goals compile to an artificial goal fact reached by a zero-cost operator per disjunct, hidden from the printed plan. Derived predicates, numerics, time - `(:derived ...)` axioms are grounded and closed to a least fixpoint after every state change, and enter the delete relaxation as zero-cost rules so the heuristics stay admissible. - Numeric fluents: comparisons in preconditions and goals, and assign/increase/ decrease/scale-up/scale-down effects. Numeric tasks carry a value vector in the state; classical tasks keep the bare frozenset and pay nothing. - Durative actions compile to sequential actions carrying their duration, with Task.makespan. Concurrency is explicitly not modelled, and the support table says so rather than implying otherwise. Planners and budgets - Five more: hill climbing, beam search, Iterated Width, branch and bound, and anytime weighted A*. - max_expansions / time_limit on every planner, the API, the CLI and the benchmark harness. A truncated run sets stats.truncated, so "we stopped looking" is never reported as "no plan exists" -- and branch and bound, which otherwise runs for hours, carries its own default ceiling. Generators and examples - jupyddl.generator: seven parametrised domains where the same (kind, size, seed) always emits byte-identical PDDL, plus `jupyddl generate`. random-strips plants a reachable chain among distractors, because uniformly random operators are almost always unsolvable and so useless as a benchmark. - New demos: rovers (ADL), network (recursive axioms), numeric-transport, workshop (temporal), blocksworld12. Web workbench - Four views: Solve, Experiment (an instance x configuration sweep sharing one grounding per instance, streaming into a sortable table with CSV/JSON export), PDDL support, and Generate. Fixes - validate_plan replayed from task.init through raw operators, so it saw neither derived predicates nor numeric fluents; it now goes through the task. - Budget checked the clock every 256 expansions. Under LM-cut one expansion can cost tens of milliseconds, so a 0.5s limit overran to 17s. It now checks every expansion. - Operator.base_name strips compilation tags, so plans print the action the domain author wrote instead of `move(a,b)#2`. Test suite 204 -> 298, green on Python 3.9 through 3.12. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Meb35zHKsyBkH2sbWoyKMT
… object fluents Five of the six flags that were refused are now supported, taking coverage from 15 of 21 to 20. All of it is source-to-source: jupyddl/compile.py rewrites these constructs into the classical core before grounding, so the grounder and the search engine are untouched and there is still one representation to optimise. :constraints - always, at-end, sometime, sometime-before, sometime-after, at-most-once. - `always phi` becomes a precondition on every action plus a goal conjunct. Every state on a trajectory is the initial state, a state an action is taken from, or the final state, so those three checks cover all of them. - sometime / sometime-before use optional monitor actions the planner applies when convenient. sometime-after and at-most-once use forced monitors on conditional effects instead: a constraint the planner could satisfy by not looking would not be a constraint. - The metric-time forms (within, always-within, hold-after, hold-during) are refused by name. :preferences - A closing action freezes the state, then each preference is resolved either free (if it holds) or at its (is-violated p) weight. Cost-optimal search then minimises the metric without knowing what a preference is. - Freezing first is the point: otherwise a preference could be satisfied halfway through, broken, and still paid for. There is a test for exactly that. - Soft trajectory constraints are refused; goal preferences are the supported form. :timed-initial-literals - Elapsed time becomes a numeric fluent advanced by action durations. Each literal gets a firing action guarded on the clock and a wait action that advances it, and every domain action is blocked while a due literal has not fired. - Literals must also fire in time order. Without that, `(at 0 (open))` and `(at 3 (not (open)))` could be fired backwards and leave the shop open forever -- caught by a test that expected an unsolvable instance and got a plan. :object-fluents - Compiled to a predicate plus a uniqueness rule; assign clears the old value first, so the function cannot quietly become a relation. Nested-term use is refused in favour of the equality form. :duration-inequalities - Bounds are collected and the shortest feasible duration chosen, which is makespan-optimal given no concurrency and no continuous change. Strict < and > are refused: they have no shortest feasible value. Also - Task.makespan replays the clock when a task has one. Waiting for a timed literal advances time without any action taking that long, so summing durations under-reported the end time. - Synthetic actions declare a zero cost. Grounding charges 1 for an action with no cost effect, which silently inflated every metric involving a preference. - New demos: errands (preferences + constraints, where the optimal plan deliberately skips an errand and pays the penalty) and timed-market. - Tests 298 -> 358, green on Python 3.9 through 3.12. Still not supported: :continuous-effects, and true temporal concurrency. Durative actions compile to a sequential schedule and never overlap, so a plan needing two actions to run at once will not be found. That needs a mutex-aware temporal scheduler, not another compilation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Meb35zHKsyBkH2sbWoyKMT
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
Contributor
|
Tick the box to add this pull request to the merge queue (same as
|
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.
Three commits that take
jupyddlfrom a STRIPS planner you run blind to one you can watch, sweep, and point at most of PDDL 3.1. Make the search observable
One observer hook on every planner; everything else reads from it.
jupyddl.trace—SearchObserverwith no-op defaults, aTraceRecorderproducing a serialisableSearchTrace(JSON round-trip, bounded memory via adaptive thinning), andtrace_search().jupyddl.live— a live terminal dashboard with Unicode sparklines and a frontier gauge. Stdlib only, so watching a search still costs no dependency; degrades to one-line output on a non-tty.jupyddl.viz(thevizextra) — search-progress, planner-comparison, benchmark-dashboard, plan-timeline and a radial "wavefront" chart where a well-guided search reads as a narrow spike and a blind one fills the disc. Light and dark both validated for colour-vision-deficiency separation.2. Grow the PDDL fragment
jupyddl.requirementsis a single registry saying what happens to every requirement flag — native, compiled, partial or rejected — with the reason. The parser validates against it,jupyddl requirementsprints it, the README table is generated from it, and the web UI renders it, so they cannot drift apart.20 of 21 flags are now supported, up from a STRIPS-plus-a-little-ADL subset:
or,imply,exists, nestednot— parser emits NNF, grounder expands quantifiers and distributes to DNF, one operator per disjunctassign/increase/decrease/scale-*Task.makespanalways,at-end,sometime,sometime-before,sometime-after,at-most-oncePDDL 3 is handled by
jupyddl/compile.pyas source-to-source rewriting before grounding, so the search engine never learns these constructs exist.A rejected flag fails at parse time with an explanation. Silently ignoring
:preferenceswould produce plans that are wrong rather than absent — worse than refusing.Also: 5 more planners (hill climbing, beam, Iterated Width, branch-and-bound, anytime weighted A*) for 14 total, search budgets on every planner, and 7 seeded instance generators.
3. The browser workbench
Runs this library, unmodified, under Pyodide — not a JS reimplementation. Four views: Solve (live charts + wavefront), Experiment (instance × configuration sweep sharing one grounding per instance, streaming into a sortable table with CSV/JSON export), PDDL support, and Generate. Verified end-to-end in Chromium.
Bugs found and fixed along the way
validate_planwas wrong for the new features — it replayed fromtask.initthrough raw operators, so it saw neither axioms nor numeric fluents.(at 0 (open))and(at 3 (not (open)))fired backwards left the shop open forever. Caught by a test that expected an unsolvable instance and got a plan.Verification
hanoiis checked against the closed form 2⁵−1.Known limits
:continuous-effectsis refused, and true temporal concurrency is not modelled. Durative actions compile to a sequential schedule and never overlap, so a plan needing two actions to run at once will not be found. That needs a mutex-aware temporal scheduler (POPF-style), not another compilation — the one remaining gap that is a project rather than a patch..github/workflows/pages.ymlis included but GitHub Pages must be enabled in repository settings before the workbench URL goes live.web/distis generated and committed so the page works from a plain clone; a test and the Pages workflow fail if it drifts from the sources.Generated by Claude Code