From d74aa7bd7e041724b11b48973b99de5759edc9b5 Mon Sep 17 00:00:00 2001 From: HanielUlises Date: Mon, 28 Sep 2026 10:24:34 -0600 Subject: [PATCH] Repair the tracked model only when the planner does --- .../EpistemicStateNode.hpp | 8 +- .../test/epistemic_state_node_test.cpp | 28 +++ .../test/tasks/README.md | 8 + .../test/tasks/backdoor-revealed.json | 185 ++++++++++++++++++ 4 files changed, 226 insertions(+), 3 deletions(-) create mode 100644 plansys2_epistemic_planner/test/tasks/backdoor-revealed.json diff --git a/plansys2_epistemic_executor/include/plansys2_epistemic_executor/EpistemicStateNode.hpp b/plansys2_epistemic_executor/include/plansys2_epistemic_executor/EpistemicStateNode.hpp index 3db17813..5a8b1c3c 100644 --- a/plansys2_epistemic_executor/include/plansys2_epistemic_executor/EpistemicStateNode.hpp +++ b/plansys2_epistemic_executor/include/plansys2_epistemic_executor/EpistemicStateNode.hpp @@ -142,11 +142,13 @@ class EpistemicStateNode : public rclcpp_lifecycle::LifecycleNode /// state without polling a service on every change. void publish_state(); - /// How the tracked model takes an action: repaired on a KD45 task, as it - /// always was, and never refused, since what it records has already happened. + /// How the tracked model takes an action: repaired only when the task asks + /// for repair, as the planner does, and never refused, since what it records + /// has already happened. A model repaired where the planner's was not would + /// refuse an action the policy had every reason to send. Seriality tracked_seriality() const { - return task_->kd45 ? Seriality::Repair : Seriality::Ignore; + return task_->repair_seriality() ? Seriality::Repair : Seriality::Ignore; } rclcpp::Service::SharedPtr load_task_service_; diff --git a/plansys2_epistemic_executor/test/epistemic_state_node_test.cpp b/plansys2_epistemic_executor/test/epistemic_state_node_test.cpp index d385375e..4a815fa8 100644 --- a/plansys2_epistemic_executor/test/epistemic_state_node_test.cpp +++ b/plansys2_epistemic_executor/test/epistemic_state_node_test.cpp @@ -342,6 +342,17 @@ class EpistemicFleetDomainTest : public EpistemicStateNodeTest /// /// This is where a belief and a sensor reading can disagree in the ordinary /// way, which is what recovery is for. +/// backdoor-revealed: a KD45 task in which Alice hides the key where Bob cannot +/// see, and then shows it to both of them. +class BackdoorRevealedTest : public EpistemicStateNodeTest +{ +protected: + std::string task_for_test() const override + { + return std::string(EPISTEMIC_TASK_DIR) + "/backdoor-revealed.json"; + } +}; + class StalePriorTest : public EpistemicStateNodeTest { protected: @@ -492,6 +503,23 @@ TEST_F(EpistemicFleetDomainTest, TheModelItselfIsUnchangedByAskingForAView) EXPECT_EQ(before->holds, after->holds); } +TEST_F(BackdoorRevealedTest, AnAnnouncementAgainstABeliefIsTrackedAsThePlannerPredictsIt) +{ + // Bob believes the key is not at the back when Alice shows it, and is left + // with no world. The planner does not repair a KD45 task unless asked to, + // and the model that follows execution has to agree with it: repairing here + // deletes the only world there is and refuses the announcement. + for (const std::string action : {"alice-hides-key_alice", "alice-shows-key_alice"}) { + auto request = std::make_shared(); + request->epistemic_action = action; + + auto response = call( + "epistemic_state/apply_action", request); + ASSERT_NE(response, nullptr); + EXPECT_TRUE(response->success) << action << ": " << response->error; + } +} + // Recovery. // // The model is a belief about the world, and the world can contradict it. What diff --git a/plansys2_epistemic_planner/test/tasks/README.md b/plansys2_epistemic_planner/test/tasks/README.md index 8fd79390..86af189c 100644 --- a/plansys2_epistemic_planner/test/tasks/README.md +++ b/plansys2_epistemic_planner/test/tasks/README.md @@ -19,6 +19,7 @@ tests assert). | `coin-in-the-box-4.json` | del-planner `benchmarks/coin4/problem_4.json` | its shortest plan leaves B believing a contradiction; the `consistent_beliefs` test | | `active-muddy-child.json` | `Active-Muddy-Child/out/problem_1.json` | 32 initial worlds; the canonical partial-observability case | | `coin-in-the-box-multipointed.json` | derived, see below | hand-made branching fixture, kept for the parser tests | +| `backdoor-revealed.json` | derived, see below | KD45: an announcement leaves Bob no world; the executor's update test | The puzzle instances come from the `epddl-workspace/` of [Epistemic-Robotics](https://github.com/HanielUlises/epistemic-robotics); the @@ -46,3 +47,10 @@ path `EpistemicPlanSolver` flattens away when it converts a policy into a That is the textbook multi-pointed coin-in-the-box, and its solution branches. If the EPDDL instance is ever written and grounded, replace this file with the grounder's output. + +`backdoor-revealed.json` is del-planner's `benchmarks/backdoor/backdoor-problem.json` +with one action added, `alice-shows-key_alice`, a public announcement that the +key is at the back. After Alice hides the key where Bob cannot see, Bob believes +it is not there, and the announcement leaves him no accessible world. Updating +without repair keeps that one world; seriality repair deletes it and refuses +the update. diff --git a/plansys2_epistemic_planner/test/tasks/backdoor-revealed.json b/plansys2_epistemic_planner/test/tasks/backdoor-revealed.json new file mode 100644 index 00000000..86b1079b --- /dev/null +++ b/plansys2_epistemic_planner/test/tasks/backdoor-revealed.json @@ -0,0 +1,185 @@ +{ + "planning-task-info": { + "problem": "backdoor-revealed", + "domain": "backdoor", + "libraries": [ + "intermediate" + ], + "requirements": [ + ":KD45-frames", + ":disjunctive-list-formulas", + ":disjunctive-preconditions", + ":equality", + ":events-conditions", + ":list-comprehensions", + ":lists", + ":modal-goals", + ":multi-pointed-models", + ":negative-list-formulas", + ":negative-preconditions", + ":ontic-actions", + ":partial-observability" + ], + "agents-number": 2, + "atoms-number": 1, + "facts-number": 0, + "actions-number": 2, + "initial-worlds-number": 1, + "goal-modal-depth": 1, + "goal-size": 5 + }, + "language": { + "atoms": [ + "key-at-back" + ], + "agents": [ + "alice", + "bob" + ] + }, + "facts": [], + "initial-state": { + "worlds": [ + "w0" + ], + "relations": { + "alice": { + "w0": [ + "w0" + ] + }, + "bob": { + "w0": [ + "w0" + ] + } + }, + "labels": { + "w0": [] + }, + "designated": [ + "w0" + ] + }, + "actions": { + "alice-hides-key_alice": { + "action-type": "private-ontic", + "events": [ + "e-hide", + "nil" + ], + "relations": { + "Fully": { + "e-hide": [ + "e-hide" + ], + "nil": [ + "nil" + ] + }, + "Oblivious": { + "e-hide": [ + "nil" + ], + "nil": [ + "nil" + ] + } + }, + "designated": [ + "e-hide" + ], + "preconditions": { + "e-hide": { + "formula": { + "connective": "and", + "formulas": [ + "true", + { + "connective": "not", + "formula": "key-at-back" + } + ] + } + }, + "nil": { + "formula": "true" + } + }, + "effects": { + "e-hide": { + "key-at-back": { + "formula": "true" + } + }, + "nil": null + }, + "observability-conditions": { + "alice": { + "Fully": { + "formula": "true" + } + }, + "bob": { + "Oblivious": { + "formula": "true" + } + } + } + }, + "alice-shows-key_alice": { + "action-type": "public-announcement", + "events": [ + "e-show" + ], + "relations": { + "Fully": { + "e-show": [ + "e-show" + ] + } + }, + "designated": [ + "e-show" + ], + "preconditions": { + "e-show": { + "formula": "key-at-back" + } + }, + "effects": { + "e-show": null + }, + "observability-conditions": { + "alice": { + "Fully": { + "formula": "true" + } + }, + "bob": { + "Fully": { + "formula": "true" + } + } + } + } + }, + "goal": { + "formula": { + "connective": "and", + "formulas": [ + "key-at-back", + { + "modality-name": "box", + "modality-index": [ + "bob" + ], + "formula": { + "connective": "not", + "formula": "key-at-back" + } + } + ] + } + } +} \ No newline at end of file