diff --git a/README.md b/README.md index e285761..55949bc 100644 --- a/README.md +++ b/README.md @@ -240,7 +240,18 @@ pddlpy validate domain.pddl problem.pddl # diagnostics (see below) ``` `solve` defaults to `astar`; `--planner` accepts any registered planner -(`bfs`, `astar`, `gbfs`, `ucs`). Exit codes: `0` success, `1` search +(`bfs`, `astar`, `gbfs`, `ucs`, `temporal`). With `--planner temporal` the +JSON carries the schedule (#119): each step gains `start`, `duration` and +`end`, and the result a top-level `makespan`: + +```bash +$ pddlpy solve weld-domain.pddl weld-problem.pddl --planner temporal +{"planner": "temporal", "solved": true, "cost": 6.0, "makespan": 6.0, + "length": 1, "steps": [{"action": "steady-weld", "args": {"?p": "p1"}, + "start": 0.0, "duration": 6.0, "end": 6.0}]} +``` + +Exit codes: `0` success, `1` search completed without finding a plan / `validate` found issues, `2` bad input (missing file, unknown operator, unsupported `:requirements`). diff --git a/docs/object-model.md b/docs/object-model.md index 237c8f0..cb63f3a 100644 --- a/docs/object-model.md +++ b/docs/object-model.md @@ -164,7 +164,9 @@ the `:durative-actions` capability and returns a `TemporalPlan`. 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`. + `cost`. The CLI and MCP `solve` render the schedule too (#119): each JSON + step carries `start`/`duration`/`end` and the result a top-level + `makespan` alongside `cost`. ```python from pddlpy import DomainProblem diff --git a/pddlpy/serialize.py b/pddlpy/serialize.py index 99d4b73..c17823c 100644 --- a/pddlpy/serialize.py +++ b/pddlpy/serialize.py @@ -11,7 +11,7 @@ from typing import Any, Dict, Iterable, List, Optional from pddlpy.pddl import DomainProblem, Operator -from pddlpy.planning import Plan, atom_tuple +from pddlpy.planning import Plan, TemporalPlan, atom_tuple def atoms_list(atoms: Iterable[Any]) -> List[List[str]]: @@ -59,9 +59,31 @@ def operator_dict(op: Operator) -> Dict[str, Any]: def plan_dict(plan: Optional[Plan]) -> Dict[str, Any]: - """A plan (or the absence of one, when ``plan`` is ``None``).""" + """A plan (or the absence of one, when ``plan`` is ``None``). + + A :class:`TemporalPlan` (#84) additionally carries its schedule (#119): + each step gains ``start``, ``duration`` and ``end``, and the dict gains a + top-level ``makespan`` (which also remains the ``cost``). + """ if plan is None: return {"solved": False, "cost": None, "length": None, "steps": None} + if isinstance(plan, TemporalPlan): + return { + "solved": True, + "cost": plan.cost, + "makespan": plan.makespan, + "length": len(plan), + "steps": [ + { + "action": s.action.operator_name, + "args": dict(s.action.variable_list), + "start": s.start, + "duration": s.duration, + "end": s.end, + } + for s in plan.steps + ], + } return { "solved": True, "cost": plan.cost, diff --git a/tests/test_cli.py b/tests/test_cli.py index 57f243f..d4a0a30 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -108,6 +108,20 @@ def test_solve_cost_aware(capsys): assert out["solved"] is True and out["cost"] > 0 +def test_solve_temporal_schedule(capsys): + # #119: TemporalPlan steps carry the schedule, plus a top-level makespan. + code, out, _ = _run( + capsys, "solve", *_files("temporal-weld"), "--planner", "temporal" + ) + assert code == 0 + assert out["solved"] is True + assert out["makespan"] == out["cost"] == 6.0 + step = out["steps"][0] + assert set(step) == {"action", "args", "start", "duration", "end"} + assert step["action"] == "steady-weld" and step["args"] == {"?p": "p1"} + assert step["start"] == 0.0 and step["duration"] == 6.0 and step["end"] == 6.0 + + def test_solve_no_plan(capsys, tmp_path): domain = tmp_path / "d.pddl" problem = tmp_path / "p.pddl" diff --git a/tests/test_mcpserver.py b/tests/test_mcpserver.py index 522332f..e0275f5 100644 --- a/tests/test_mcpserver.py +++ b/tests/test_mcpserver.py @@ -44,6 +44,14 @@ def test_solve_default_planner(): assert out["length"] == len(out["steps"]) > 0 +def test_solve_temporal_schedule(): + # #119: the solve tool surfaces TemporalPlan schedules. + out = solve(*_files("temporal-weld"), planner="temporal") + assert out["solved"] is True and out["makespan"] == out["cost"] + step = out["steps"][0] + assert step["end"] == step["start"] + step["duration"] + + def test_solve_no_plan(tmp_path): domain = tmp_path / "d.pddl" problem = tmp_path / "p.pddl"