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
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,19 @@
All notable changes to `pddlpy` are documented here. This project adheres to
[Semantic Versioning](https://semver.org/).

## [Unreleased]

### Added
- **Temporal planner** (#84): `get("temporal")` *solves* durative-action
domains. It enforces the full time-tagged contract — `at start`, `over all`
(invariant across the duration), and `at end` — applying start effects at the
start point and end effects at start+duration, under **sequential** semantics
(actions never overlap; instantaneous actions participate as zero-duration
steps). Returns a `TemporalPlan` whose `ScheduledAction` steps carry start
times and durations and whose `makespan` is the plan cost. Registered with
the `:durative-actions` capability. Required concurrency (mandatory overlap)
remains out of scope.

## [1.1.2] - 2026-07-12

Infrastructure release — no library changes.
Expand Down
37 changes: 28 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ The orginal grammar file was authored by Zeyn Saigol from University of Birmingh

### NOTICE ###

Durative actions are now recovered into the object model (see _Planning API_).
The reference planners, however, are non-temporal and do not solve durative
domains — that is documented as out of scope for this round.
Durative actions are recovered into the object model and now *solved*: the
`temporal` planner (#84) schedules durative-action domains sequentially. The
reference STRIPS planners remain non-temporal; see _Planning API_.

### What this project is not? ###

Expand Down Expand Up @@ -177,9 +177,27 @@ Action costs (`:action-costs`, `(increase (total-cost) ...)`, `(:metric minimize
Durative actions (`:durative-actions`) are recovered into a `DurativeAction`
type with time-tagged conditions (`at start` / `over all` / `at end`) and
effects (`at start` / `at end`) plus a duration. `DomainProblem.durative_operators()`
and `ground_durative_operator(name)` expose them. The reference planners are
non-temporal, so they do not solve durative domains — this is documented as out
of scope for now.
and `ground_durative_operator(name)` expose them.

The `temporal` planner (#84) *solves* these domains. It enforces the full
time-tagged contract — `at start` at the start point, `over all` as an
invariant across the duration, and `at end` at start+duration — under
**sequential** semantics (actions never overlap; instantaneous actions
participate as zero-duration steps). It returns a `TemporalPlan` whose `steps`
carry per-action start times and durations and whose `makespan` is the plan
cost. Required concurrency (mandatory overlap, e.g. match/cellar) is out of
scope.

```python
>>> import pddlpy
>>> from pddlpy.planning import get
>>> dp = pddlpy.DomainProblem('weld-domain.pddl', 'weld-problem.pddl')
>>> plan = get('temporal').solve(dp)
>>> plan.makespan
6.0
>>> [(s.start, s.action.operator_name, s.duration) for s in plan.steps]
[(0.0, 'steady-weld', 6.0)]
```

```python
>>> import pddlpy
Expand Down Expand Up @@ -295,15 +313,16 @@ There are wonderful material at the the University of Edinburgh:

### Future development ###

* A temporal planner that solves durative-action domains.
* Concurrent temporal planning (required action overlap, e.g. match/cellar).
* Heuristic improvements for the reference planners.
* ADL (conditional effects, quantifiers) and a full and/or/not precondition tree.

Done recently: case-insensitive keywords, `:requirements` capture/enforcement,
a planner interface with BFS/A*/GBFS/UCS reference planners, numeric fluents
(`:functions`, numeric preconditions/effects), action costs (`total-cost` +
cost-aware search), durative-action recovery into the object model, and a
measured test suite with full coverage of the object model and planning layer.
cost-aware search), durative-action recovery into the object model, a
sequential `temporal` planner that schedules durative-action domains (#84), and
a measured test suite with full coverage of the object model and planning layer.

### Advanced ###

Expand Down
45 changes: 41 additions & 4 deletions docs/object-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@ evaluates a numeric precondition; `NumericEffect.apply(valuation)` returns the
### `DurativeAction` (#23)
A durative action with time-tagged `condition_pos`/`condition_neg` keyed by
`'start'`/`'over'`/`'end'`, `effect_pos`/`effect_neg` keyed by
`'start'`/`'end'`, and a `duration`. Recovered into the model but **not**
solved by the reference planners (they are non-temporal).
`'start'`/`'end'`, and a `duration`. Recovered into the model; the reference
planners are non-temporal, but the `temporal` planner (below) schedules them.

## Planning layer (`pddlpy.planning`)

Expand Down Expand Up @@ -123,12 +123,49 @@ plan = get("astar").solve(dp)
print(plan.cost, plan.action_names())
```

### Temporal planner (#84)

The `temporal` planner *solves* durative-action domains, enforcing the full
time-tagged contract that `DurativeState` (at-start only) cannot. It declares
the `:durative-actions` capability and returns a `TemporalPlan`.

- **`apply_durative(state, action)`** — applies one grounded `DurativeAction`
under **sequential** semantics: it checks `at start` in `state`, applies the
start effects, then checks the `over all` invariant and the `at end`
conditions against that mid-state (with no concurrency it persists for the
whole duration), and finally applies the end effects at `start + duration`.
Returns the successor `State`, or `None` if any condition fails.
- **`TemporalPlanner`** (`get("temporal")`) — uniform-cost search over
accumulated duration; instantaneous actions participate as zero-duration
steps, so mixed domains work. Returns a makespan-minimal sequential schedule.
- **`TemporalPlan`** — a `Plan` whose `steps` are `ScheduledAction`s
(`action`, `start`, `duration`, `end`); its `makespan` doubles as the plan
`cost`.

```python
from pddlpy import DomainProblem
from pddlpy.planning import get

dp = DomainProblem("durative-domain.pddl", "durative-problem.pddl")
plan = get("temporal").solve(dp)
if plan is not None:
print("makespan", plan.makespan)
for step in plan.steps:
print(step.start, step.action.operator_name, step.duration)
```

Because semantics are sequential (v1), actions never overlap: `over all` and
`at end` are constrained only by the action's own start effects. **Required
concurrency** (actions that must overlap, e.g. match/cellar) is out of scope —
a follow-up to this planner.

## Known limitations

- **Union types:** the `:types` hierarchy is supported (#22) — a parameter
typed with a supertype binds objects of any transitive subtype — but
`(either ...)` union types are not handled.
- **Disjunctive/ADL preconditions:** the `or` connective is preserved but not
evaluated; the reference planners reject such domains via capability checks.
- **Durative actions:** validated and checked for `at start` applicability
(see above), but not solved — the reference planners are non-temporal.
- **Durative actions:** solved sequentially by the `temporal` planner (#84);
the reference STRIPS planners remain non-temporal. **Required concurrency**
(mandatory action overlap) and PDDL+ processes/events are out of scope.
14 changes: 14 additions & 0 deletions pddlpy/planning/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@
UniformCostPlanner,
)
from .state import Plan, State, atom_tuple
from .temporal import (
TEMPORAL_CAPABILITIES,
ScheduledAction,
TemporalPlan,
TemporalPlanner,
TemporalTask,
apply_durative,
)

__all__ = [
"State",
Expand All @@ -53,4 +61,10 @@
"action_cost",
"plan_cost",
"TOTAL_COST",
"TemporalPlanner",
"TemporalPlan",
"TemporalTask",
"ScheduledAction",
"apply_durative",
"TEMPORAL_CAPABILITIES",
]
31 changes: 23 additions & 8 deletions pddlpy/planning/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,25 @@
"""
from __future__ import annotations

from typing import TYPE_CHECKING, Any, Dict, Iterable, Iterator, List, Mapping, Optional, Tuple
from typing import (
TYPE_CHECKING,
Any,
Dict,
Iterable,
Iterator,
List,
Mapping,
Optional,
Tuple,
Union,
)

if TYPE_CHECKING:
from pddlpy.pddl import DomainProblem, Operator
from pddlpy.pddl import DomainProblem, DurativeAction, Operator

#: Any grounded action a plan step can carry — instantaneous or durative
#: (#84); both expose ``operator_name`` and ``variable_list``.
PlanAction = Union[Operator, DurativeAction]

#: A ground atom, e.g. ``("on", "a", "b")``.
GroundAtom = Tuple[str, ...]
Expand Down Expand Up @@ -133,22 +148,22 @@ def __repr__(self) -> str:
class Plan:
"""An ordered sequence of grounded actions that reaches a goal.

``actions`` is a tuple of grounded ``Operator`` instances. ``cost`` is
the plan cost (defaults to the number of actions; action-cost domains
override this in a later phase).
``actions`` is a tuple of grounded ``Operator`` (or, for temporal plans,
``DurativeAction``) instances. ``cost`` is the plan cost (defaults to the
number of actions; action-cost domains override this in a later phase).
"""

__slots__ = ("actions", "cost")

def __init__(
self,
actions: Iterable["Operator"] = (),
actions: Iterable["PlanAction"] = (),
cost: Optional[float] = None,
) -> None:
self.actions: Tuple["Operator", ...] = tuple(actions)
self.actions: Tuple["PlanAction", ...] = tuple(actions)
self.cost: float = len(self.actions) if cost is None else cost

def __iter__(self) -> Iterator["Operator"]:
def __iter__(self) -> Iterator["PlanAction"]:
return iter(self.actions)

def __len__(self) -> int:
Expand Down
Loading