Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
2 changes: 1 addition & 1 deletion .docs/PUBLISHING.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ submitting**. This list is a working copy, not a substitute.
|---|---|
| OSI-approved licence | ✅ Apache-2.0 |
| Public repository, open issue tracker, history in the open | ✅ |
| Substantial scholarly effort | ✅ Parser and grounder covering 20 of 21 PDDL requirement flags, 14 planners, LM-cut and `h^m`, PDDL 3 compiled to the classical core, learned heuristics, search tracing |
| Substantial scholarly effort | ✅ Parser and grounder covering 20 of 24 PDDL requirement flags, 14 planners, LM-cut and `h^m`, PDDL 3 compiled to the classical core, learned heuristics, search tracing |
| Installation instructions | ✅ README → Install; `pip install jupyddl` |
| Example usage | ✅ README, `demos/`, the browser workbench |
| Automated tests | ✅ CI on Python 3.10–3.14, with and without the optional extras, plus a packaging check on the built wheel |
Expand Down
14 changes: 14 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,20 @@ traps worth knowing:
the planner would simply decline to notice. Those ride on conditional effects
added to every domain action.

- **Durative actions are one sequential action** with three time points:
at-start conditions in the starting state, then at-start effects, then
over-all/at-end conditions (and `always`) checked in that in-between state,
then at-end effects, which win any conflict (`grounding._sequence`). What
cannot be expressed that way is refused by name, not approximated.
- **The cost model.** No metric: every action costs 1 (or its declared
`total-cost` increase). A metric over `(total-cost)`: declared increases,
0 otherwise; increases may read *static* functions (`road-length`), which
are evaluated at grounding. A metric without `total-cost`: domain actions
are free and only preference penalties count. `(total-time)`: an action
costs its duration. `maximize` and other metric shapes are refused.
- `compile_problem` deep-copies its inputs: `ground()` must never mutate a
parsed Domain/Problem, which callers may reuse for another problem.

### Optional task layers
`Task` carries three layers that are inert unless the domain uses them, and the
planners must go through the task rather than the operator to honour them:
Expand Down
52 changes: 52 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,58 @@ plans because it replays them through the same model. They are now pinned by
what `solve_task` actually ran, worked out by the new
`jupyddl.api.resolve_heuristic`.

Wrong answers from the PDDL front end. The parser and the PDDL 3 compilations
handed the grounder a different problem from the one written, so plans
validated — against the wrong problem. Each case is now pinned by
`tests/test_frontend_semantics.py`.

- **Durative actions merged their at-start and at-end effects**, and an add
beat a delete, so `(at start (busy)) (at end (not (busy)))` ended with
`(busy)` true. The at-end effects now apply after the at-start ones and win a
conflict, and over-all and at-end conditions are checked in the state after
the at-start effects rather than before them. `always` is checked in that
state too; the other trajectory constraints are refused when a durative
action has at-start effects, because their monitors cannot see it.
- **`sometime-after` could not be met by the final state.** An obligation
recorded by the last action's monitor could never be discharged, so the
plan `[t, r]` was reported unsolvable; the goal now accepts the response
holding at the end.
- **`forall` around a trajectory constraint moved inside it**:
`(forall (?x) (sometime (lit ?x)))` meant one moment with every `?x` lit at
once. It is now one constraint per object. A soft constraint under `forall`
crashed with an `AttributeError`; it is refused by name like the others.
- **A goal preference under `forall` kept its free variable** and could never
be satisfied; it is now one preference per object. Under `not`, `or`, `imply`
or `exists` a preference silently made the goal false; it is now an error.
- **Timed initial literals slipped past the PDDL 3 machinery.** The
`at-most-once` and `sometime-after` monitors ignored the states literals
fired in, a literal could fire after the preferences had been paid out, and
a plan could end before a literal that was already due had fired.
- **Object fluents in trajectory constraints** were read as numeric
comparisons; they are now compiled as everywhere else. Assigning one object
fluent's value to another took the function's name for an object; it is
refused.
- **The metric.** A preference the metric does not mention cost 1 to violate
rather than nothing, a metric without `(total-cost)` still charged for
actions, `maximize` was silently minimised, and `(total-time)` charged an
instantaneous action 1. `maximize`, any metric other than `(total-cost)` or
`(total-time)` plus weighted `(is-violated p)` terms, and a metric pricing a
preference the goal does not declare are now refused.
- **`ground()` edited the domain it was given**, so a second problem grounded
against the same parsed domain inherited the first one's synthetic actions
and lost the domain's own constraints.
- **Misspelt PDDL was accepted.** `:preconditon` gave an action with no
precondition and `(:goals ...)` a problem with an empty goal; unknown keys
and sections and a missing `:goal` are now errors that name them. Malformed
input — `(?x -)`, `(when (q))`, `(:goal)` — raised a bare `IndexError`; it is
a `PDDLError`, and an unbalanced parenthesis names its line.
- **Objects called `nan`, `inf` or `infinity` were read as numbers.**
- **`(increase (total-cost) (road-length ?a ?b))` was refused** although
`:action-costs` is listed as native. A cost over static functions is now
evaluated when grounding, so the IPC `transport` domains load. `:fluents`
now implies `:object-fluents`, and `:ucpop`, `:domain-axioms` and
`:safety-constraints` are refused by name rather than as unknown flags.

## [2.4.0]

### Added
Expand Down
23 changes: 13 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ says where and what is left.
- 🧩 **Hand-written PDDL parser** and grounder covering **STRIPS, full ADL,
derived predicates, numeric fluents, durative actions, trajectory
constraints, preferences, timed initial literals and object fluents** — 20 of
the 21 requirement flags, with the last one refused by name rather than
the 24 requirement flags, with the other four refused by name rather than
ignored. ([the full matrix](#pddl-support))
- 🔎 **14 planners**: BFS, DFS, Iterative Deepening, Dijkstra, Greedy Best-First,
A*, Weighted A*, IDA*, Enforced Hill Climbing, hill climbing, beam search,
Expand Down Expand Up @@ -507,10 +507,10 @@ no heuristic at all — it prunes by *novelty* instead.

## PDDL support

**20 of the 21 requirement flags are supported.**
**20 of the 24 requirement flags are supported.**
6 are modelled natively, 7 are compiled into the core
representation, and 7 are supported with a documented restriction.
Exactly 1 is **refused at parse time with an explanation** — never
The other 4 are **refused at parse time with an explanation** — never
silently ignored, because a silently ignored requirement produces plans that are
wrong rather than absent.

Expand All @@ -521,27 +521,30 @@ jupyddl requirements --verbose # with the full compilation notes

| Requirement | PDDL | Support | What jupyddl does |
|---|---|---|---|
| `:action-costs` | 3.0 | **native** | `(increase (total-cost) k)` and metric minimisation. Operator costs feed straight into g, so A*/Dijkstra optimise cost. Under a metric over `(total-cost)` an action with no cost effect is free, as in IPC planners; without a metric every action costs 1. A cost inside `when` and a negative cost are refused. |
| `:action-costs` | 3.0 | **native** | `(increase (total-cost) k)` and metric minimisation. Operator costs feed straight into g, so A*/Dijkstra optimise cost. `k` is a constant or an expression over static functions, such as `(road-length ?from ?to)`, evaluated from `:init` when the action is grounded; one that reads a function some effect changes is refused. Under a metric over `(total-cost)` an action with no cost effect is free, as in IPC planners; under one over `(total-time)` an action costs its duration; under one that mentions neither, every action is free; without a metric every action costs 1 (a durative one, its duration). The metric must be `minimize` over `(total-cost)` or `(total-time)`, plus constant multiples of `(is-violated p)` and constants; `maximize` and any other shape are refused. A cost inside `when` and a negative cost are refused. |
| `:conditional-effects` | 1.2 | **native** | `when` and `forall` inside effects. Kept explicitly in the grounded operator and evaluated against the state at application time. |
| `:derived-predicates` | 2.2 | **native** | `(:derived ...)` axioms. Rules are grounded and re-evaluated from scratch to a least fixpoint after every state change, so planners and heuristics always see closed states and a derived fact whose support goes away is retracted. Negating a derived predicate in a precondition or goal is supported; negating one inside another rule, which needs stratification, is refused. Derived predicates may not appear in action effects. |
| `:equality` | 1.2 | **native** | `=` between terms. Evaluated during grounding; infeasible instances are dropped. |
| `:strips` | 1.2 | **native** | Add/delete effects over positive literals. The core representation. Everything else compiles down to this. |
| `:typing` | 1.2 | **native** | Typed parameters and objects, with type hierarchies. Subtypes are resolved transitively when building the object pools. |
| `:typing` | 1.2 | **native** | Typed parameters and objects, with type hierarchies. Subtypes are resolved transitively when building the object pools. `(either a b)` types are refused; declare a common supertype instead. |
| `:adl` | 1.2 | **compiled** | The full ADL feature set. |
| `:disjunctive-preconditions` | 1.2 | **compiled** | `or` in preconditions and goals. Preconditions are converted to DNF; each disjunct becomes its own grounded operator. A disjunctive goal becomes a single artificial goal fact achieved by one zero-cost operator per disjunct. |
| `:duration-inequalities` | 2.1 | **compiled** | Durations bounded by inequalities rather than fixed. Bounds are collected and the shortest feasible duration is chosen. With no concurrency and no continuous change nothing in the model prefers a longer action, so the tightest lower bound is makespan-optimal. Strict `<`/`>` are refused: they have no shortest feasible value. |
| `:existential-preconditions` | 1.2 | **compiled** | `exists` in preconditions and goals. Expanded over the typed object pool into a disjunction, then handled like any other disjunction. |
| `:negative-preconditions` | 1.2 | **compiled** | `not` in preconditions and goals. Compiled to positive normal form: each negated fluent gets a complement fact that every operator maintains. |
| `:quantified-preconditions` | 1.2 | **compiled** | Both `exists` and `forall` in preconditions. |
| `:universal-preconditions` | 1.2 | **compiled** | `forall` in preconditions and goals. Expanded over the typed object pool into a conjunction. |
| `:constraints` | 3.0 | **partial** | State trajectory constraints. `always`, `at-end`, `sometime`, `sometime-before`, `sometime-after` and `at-most-once` are compiled into invariants on every action, monitor facts and extra goal conjuncts. The metric-time forms (`within`, `always-within`, `hold-after`, `hold-during`) are refused by name. |
| `:durative-actions` | 2.1 | **partial** | `(:durative-action ...)` with timed conditions and effects. Compiled to sequential actions: at-start and over-all conditions become the precondition, at-start and at-end effects are merged, and the duration is carried through so plans report a makespan. Actions therefore never overlap — this models sequential temporal planning, not true concurrency, so a plan needing two actions to run at the same time will not be found. |
| `:constraints` | 3.0 | **partial** | State trajectory constraints. `always`, `at-end`, `sometime`, `sometime-before`, `sometime-after` and `at-most-once` are compiled into invariants on every action, monitor facts and extra goal conjuncts; the monitors also watch the states timed literals fire in. A constraint under `forall` becomes one constraint per object. The metric-time forms (`within`, `always-within`, `hold-after`, `hold-during`) are refused by name, and so is any form but `always` and `at-end` when a durative action has at-start effects: its monitor cannot see the state between that action's start and end. |
| `:durative-actions` | 2.1 | **partial** | `(:durative-action ...)` with timed conditions and effects. Compiled to one sequential action each. At-start conditions are checked in the state the action starts in; over-all and at-end conditions in the state after its at-start effects (they are regressed through those effects when grounding). The at-start effects apply first and the at-end effects after them, so an at-end effect wins a conflict. The duration is carried through so plans report a makespan, and a `(total-time)` metric minimises it. Actions therefore never overlap — this models sequential temporal planning, not true concurrency, so a plan needing two actions to run at the same time will not be found. Refused: every effect and condition needs a time specifier (put a `when` inside one: `(at end (when c e))`); an at-start numeric effect that a later condition or effect of the same action reads; an at-end `when` that deletes what the at-start effects add, or reads a fact an at-start `when` may change. |
| `:fluents` | 2.1 | **partial** | Numeric plus object fluents. Both halves are implemented; see the two rows above for what each one covers. |
| `:numeric-fluents` | 2.1 | **partial** | Numeric state variables, comparisons and assignments. Supported: ground numeric fluents, comparisons (< <= = >= >) in preconditions and goals, and assign/increase/decrease/scale-up/scale-down effects over arithmetic expressions. Numeric values are part of the state, so the state space can become infinite — the delete-relaxation heuristics ignore numeric conditions, which keeps them admissible but uninformative about them. |
| `:object-fluents` | 3.1 | **partial** | Functions returning objects rather than numbers. Compiled to a predicate plus a uniqueness rule: `(= (location ?p) ?x)` becomes a fact and `assign` clears the old value first. Using an object fluent as a *nested term* — `(at ?t (location ?p))` — is refused; write the equality form instead. |
| `:preferences` | 3.0 | **partial** | Soft goals scored by a metric. Goal preferences are compiled into a priced choice: a closing action freezes the state, then each preference is resolved either for free (if it holds) or at its `(is-violated p)` weight, so cost-optimal search minimises the metric. Preferences over trajectory constraints or inside action preconditions are refused. |
| `:timed-initial-literals` | 2.2 | **partial** | Facts that become true (or false) at a given absolute time. Elapsed time becomes a numeric fluent advanced by action durations. Each literal gets a firing action guarded on the clock plus a wait action that advances it, and every domain action is blocked while a due literal has not fired. Because actions never overlap, a literal scheduled strictly inside an action's duration fires immediately after that action rather than during it. |
| `:object-fluents` | 3.1 | **partial** | Functions returning objects rather than numbers. Compiled to a predicate plus a uniqueness rule: `(= (location ?p) ?x)` becomes a fact and `assign` clears the old value first, in preconditions, effects, goals and trajectory constraints alike. Using an object fluent as a *nested term* — `(at ?t (location ?p))` — or as the value of another, as in `(assign (location ?p) (location ?q))`, is refused; write the equality form with a variable instead. |
| `:preferences` | 3.0 | **partial** | Soft goals scored by a metric. Goal preferences are compiled into a priced choice: a closing action freezes the state, then each preference is resolved either for free (if it holds) or at its `(is-violated p)` weight, so cost-optimal search minimises the metric. Timed literals cannot fire after the closing action. A preference under `forall` is one preference per object, each priced at the name's weight. Under a metric, a preference it does not mention costs nothing to violate; without one, each costs 1. Preferences over trajectory constraints, inside action preconditions, or under `or`, `not`, `imply` or `exists` are refused. |
| `:timed-initial-literals` | 2.2 | **partial** | Facts that become true (or false) at a given absolute time. Elapsed time becomes a numeric fluent advanced by action durations. Each literal gets a firing action guarded on the clock plus a wait action that advances it, and every domain action — and the end of the plan — is blocked while a due literal has not fired. Because actions never overlap, a literal scheduled strictly inside an action's duration fires immediately after that action rather than during it. A `(total-time)` metric is refused here: waiting takes time no action is charged for. |
| `:continuous-effects` | 2.1 | **rejected** | Effects that change continuously over an action's duration. Requires continuous-time reasoning, which this planner does not do. |
| `:domain-axioms` | 1.2 | **rejected** | PDDL 1.2 `(:axiom ...)` blocks. Superseded by `:derived-predicates`, which is supported: write each axiom as a `(:derived ...)` rule. |
| `:safety-constraints` | 1.2 | **rejected** | PDDL 1.2 `(:safety ...)` constraints. Superseded by PDDL 3 trajectory constraints: `(:constraints (always ...))` says the same thing and is supported. |
| `:ucpop` | 1.2 | **rejected** | The PDDL 1.2 UCPOP bundle: `:adl` plus `:domain-axioms` and `:safety-constraints`. Refused because two of its parts are; declare `:adl` (and `:derived-predicates` for axioms) instead. |

The table is generated from `jupyddl.requirements`, which is the single source of
truth: the parser, the CLI and the web workbench all read the same registry.
Expand Down
Loading
Loading