diff --git a/astrobee/survey_manager/survey_planner/data/test_cases/README.md b/astrobee/survey_manager/survey_planner/data/test_cases/README.md new file mode 100644 index 00000000..160b9afd --- /dev/null +++ b/astrobee/survey_manager/survey_planner/data/test_cases/README.md @@ -0,0 +1,13 @@ + +This folder contains informal test cases for `problem_generator.py` and `plan_survey.py`. The +`*_config.yaml` test cases and the `generate.bash` script are precious source, and all other files +were auto-generated from the test cases and committed so they could be compared to the output of +future runs. + +To run the informal tests, call `generate.bash` in this folder, which will overwrite the committed +version of the generated files, then run `git diff` to check for differences in the new outputs +versus their committed version. + +Typically we don't expect any differences, but future code changes could change the expected output +and that might not be an error. Observed changes require informed manual review of correctness. If +the new output is correct, it should probably be committed. diff --git a/astrobee/survey_manager/survey_planner/data/test_cases/completed1_config.yaml b/astrobee/survey_manager/survey_planner/data/test_cases/completed1_config.yaml new file mode 100644 index 00000000..3d477d91 --- /dev/null +++ b/astrobee/survey_manager/survey_planner/data/test_cases/completed1_config.yaml @@ -0,0 +1,38 @@ +# Copyright (c) 2023, United States Government, as represented by the +# Administrator of the National Aeronautics and Space Administration. +# +# All rights reserved. +# +# The "ISAAC - Integrated System for Autonomous and Adaptive Caretaking +# platform" software is licensed under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with the +# License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +# Test case for marking goals completed. For goals with completed: true, we should see completed-x +# and need-x predicates asserted in the problem_generator.py output, and those goals should not +# result in any actions in the plan. This is a fairly minimal test case verifying we can mark both +# panoramas and stereos as completed. + +goals: + +- {type: panorama, robot: bumble, order: 0, location: jem_bay4, completed: true} +- {type: panorama, robot: bumble, order: 1, location: jem_bay3} +- {type: stereo, robot: bumble, order: 4, trajectory: jem_bay1_to_bay3} + +- {type: panorama, robot: honey, order: 1, location: jem_bay7, completed: true} +- {type: panorama, robot: honey, order: 2, location: jem_bay6} +- {type: stereo, robot: honey, order: 4, trajectory: jem_bay7_to_bay4, completed: true} + +init: + bumble: + location: berth1 + honey: + location: berth2 diff --git a/astrobee/survey_manager/survey_planner/data/test_cases/completed1_plan.pddl b/astrobee/survey_manager/survey_planner/data/test_cases/completed1_plan.pddl new file mode 100644 index 00000000..4b233883 --- /dev/null +++ b/astrobee/survey_manager/survey_planner/data/test_cases/completed1_plan.pddl @@ -0,0 +1,14 @@ +; Solution Found +0.000: (undock bumble berth1 jem_bay7 jem_bay6 jem_bay8) [30.000] +30.001: (move bumble jem_bay7 jem_bay6 jem_bay5) [20.000] +50.002: (move bumble jem_bay6 jem_bay5 jem_bay4) [20.000] +70.003: (move bumble jem_bay5 jem_bay4 jem_bay3) [20.000] +70.004: (undock honey berth2 jem_bay7 jem_bay6 jem_bay8) [30.000] +90.004: (move bumble jem_bay4 jem_bay3 jem_bay2) [20.000] +100.005: (move honey jem_bay7 jem_bay6 jem_bay5) [20.000] +110.005: (panorama bumble o1 jem_bay3) [780.000] +120.006: (panorama honey o2 jem_bay6) [780.000] +890.006: (move bumble jem_bay3 jem_bay2 jem_bay1) [20.000] +910.007: (move bumble jem_bay2 jem_bay1 jem_bay0) [20.000] +930.008: (stereo bumble o4 jem_bay1 jem_bay4 jem_bay3 jem_bay5) [600.000] + diff --git a/astrobee/survey_manager/survey_planner/data/test_cases/completed1_plan.yaml b/astrobee/survey_manager/survey_planner/data/test_cases/completed1_plan.yaml new file mode 100644 index 00000000..46a0bbbf --- /dev/null +++ b/astrobee/survey_manager/survey_planner/data/test_cases/completed1_plan.yaml @@ -0,0 +1,115 @@ +- start_time_seconds: '0.000' + action: + type: undock + robot: bumble + duration_seconds: '30.000' +- start_time_seconds: '30.001' + action: + type: move + robot: bumble + from_name: jem_bay7 + to_name: jem_bay6 + to_pos: + - 11.0 + - -9.0 + - 4.8 + duration_seconds: '20.000' +- start_time_seconds: '50.002' + action: + type: move + robot: bumble + from_name: jem_bay6 + to_name: jem_bay5 + to_pos: + - 11.0 + - -8.0 + - 4.8 + duration_seconds: '20.000' +- start_time_seconds: '70.003' + action: + type: move + robot: bumble + from_name: jem_bay5 + to_name: jem_bay4 + to_pos: + - 11.0 + - -7.0 + - 4.8 + duration_seconds: '20.000' +- start_time_seconds: '70.004' + action: + type: undock + robot: honey + duration_seconds: '30.000' +- start_time_seconds: '90.004' + action: + type: move + robot: bumble + from_name: jem_bay4 + to_name: jem_bay3 + to_pos: + - 11.0 + - -6.0 + - 4.8 + duration_seconds: '20.000' +- start_time_seconds: '100.005' + action: + type: move + robot: honey + from_name: jem_bay7 + to_name: jem_bay6 + to_pos: + - 11.0 + - -9.0 + - 4.8 + duration_seconds: '20.000' +- start_time_seconds: '110.005' + action: + type: panorama + robot: bumble + location_name: jem_bay3 + location_pos: + - 11.0 + - -6.0 + - 4.8 + duration_seconds: '780.000' +- start_time_seconds: '120.006' + action: + type: panorama + robot: honey + location_name: jem_bay6 + location_pos: + - 11.0 + - -9.0 + - 4.8 + duration_seconds: '780.000' +- start_time_seconds: '890.006' + action: + type: move + robot: bumble + from_name: jem_bay3 + to_name: jem_bay2 + to_pos: + - 11.0 + - -5.0 + - 4.8 + duration_seconds: '20.000' +- start_time_seconds: '910.007' + action: + type: move + robot: bumble + from_name: jem_bay2 + to_name: jem_bay1 + to_pos: + - 11.0 + - -4.0 + - 4.8 + duration_seconds: '20.000' +- start_time_seconds: '930.008' + action: + type: stereo + robot: bumble + fplan: ISAAC/jem_stereo_mapping_bay1_to_bay3 + base_name: jem_bay1 + bound_name: jem_bay4 + duration_seconds: '600.000' diff --git a/astrobee/survey_manager/survey_planner/data/test_cases/completed1_problem.pddl b/astrobee/survey_manager/survey_planner/data/test_cases/completed1_problem.pddl new file mode 100644 index 00000000..2f20ee7f --- /dev/null +++ b/astrobee/survey_manager/survey_planner/data/test_cases/completed1_problem.pddl @@ -0,0 +1,160 @@ +;; Auto-generated by problem_generator.py. Do not edit! +;; Command was: src/survey_planner/problem_generator.py --config=data/survey_static.yaml,data/test_cases/completed1_config.yaml --output=data/test_cases/completed1_problem.pddl +;; Working directory was: data/test_cases +;; Problem template: ../../pddl/jem_survey_template.pddl +;; Config 1: data/survey_static.yaml +;; Config 2: data/test_cases/completed1_config.yaml + +(define (problem jem-survey) + (:domain survey-manager) + (:metric minimize (total-time)) + (:objects + jem_bay0 jem_bay1 jem_bay2 jem_bay3 jem_bay4 jem_bay5 jem_bay6 jem_bay7 jem_bay8 berth1 berth2 - location + bumble honey - robot + o0 o1 o2 o3 o4 - order + ) + + (:goal + (and + (completed-panorama bumble o0 jem_bay4) + (completed-panorama bumble o1 jem_bay3) + (completed-stereo bumble o4 jem_bay1 jem_bay4) + (completed-panorama honey o1 jem_bay7) + (completed-panorama honey o2 jem_bay6) + (completed-stereo honey o4 jem_bay7 jem_bay4) + ) + ) + + (:init + ;; === Static predicates === + (move-connected jem_bay0 jem_bay1) + (move-connected jem_bay1 jem_bay0) + (move-connected jem_bay1 jem_bay2) + (move-connected jem_bay2 jem_bay1) + (move-connected jem_bay2 jem_bay3) + (move-connected jem_bay3 jem_bay2) + (move-connected jem_bay3 jem_bay4) + (move-connected jem_bay4 jem_bay3) + (move-connected jem_bay4 jem_bay5) + (move-connected jem_bay5 jem_bay4) + (move-connected jem_bay5 jem_bay6) + (move-connected jem_bay6 jem_bay5) + (move-connected jem_bay6 jem_bay7) + (move-connected jem_bay7 jem_bay6) + (move-connected jem_bay7 jem_bay8) + (move-connected jem_bay8 jem_bay7) + (location-real jem_bay1) + (location-real jem_bay2) + (location-real jem_bay3) + (location-real jem_bay4) + (location-real jem_bay5) + (location-real jem_bay6) + (location-real jem_bay7) + (dock-connected jem_bay7 berth1) + (dock-connected jem_bay7 berth2) + (robots-different bumble honey) + (robots-different honey bumble) + (locations-different jem_bay0 jem_bay1) + (locations-different jem_bay0 jem_bay2) + (locations-different jem_bay0 jem_bay3) + (locations-different jem_bay0 jem_bay4) + (locations-different jem_bay0 jem_bay5) + (locations-different jem_bay0 jem_bay6) + (locations-different jem_bay0 jem_bay7) + (locations-different jem_bay0 jem_bay8) + (locations-different jem_bay1 jem_bay0) + (locations-different jem_bay1 jem_bay2) + (locations-different jem_bay1 jem_bay3) + (locations-different jem_bay1 jem_bay4) + (locations-different jem_bay1 jem_bay5) + (locations-different jem_bay1 jem_bay6) + (locations-different jem_bay1 jem_bay7) + (locations-different jem_bay1 jem_bay8) + (locations-different jem_bay2 jem_bay0) + (locations-different jem_bay2 jem_bay1) + (locations-different jem_bay2 jem_bay3) + (locations-different jem_bay2 jem_bay4) + (locations-different jem_bay2 jem_bay5) + (locations-different jem_bay2 jem_bay6) + (locations-different jem_bay2 jem_bay7) + (locations-different jem_bay2 jem_bay8) + (locations-different jem_bay3 jem_bay0) + (locations-different jem_bay3 jem_bay1) + (locations-different jem_bay3 jem_bay2) + (locations-different jem_bay3 jem_bay4) + (locations-different jem_bay3 jem_bay5) + (locations-different jem_bay3 jem_bay6) + (locations-different jem_bay3 jem_bay7) + (locations-different jem_bay3 jem_bay8) + (locations-different jem_bay4 jem_bay0) + (locations-different jem_bay4 jem_bay1) + (locations-different jem_bay4 jem_bay2) + (locations-different jem_bay4 jem_bay3) + (locations-different jem_bay4 jem_bay5) + (locations-different jem_bay4 jem_bay6) + (locations-different jem_bay4 jem_bay7) + (locations-different jem_bay4 jem_bay8) + (locations-different jem_bay5 jem_bay0) + (locations-different jem_bay5 jem_bay1) + (locations-different jem_bay5 jem_bay2) + (locations-different jem_bay5 jem_bay3) + (locations-different jem_bay5 jem_bay4) + (locations-different jem_bay5 jem_bay6) + (locations-different jem_bay5 jem_bay7) + (locations-different jem_bay5 jem_bay8) + (locations-different jem_bay6 jem_bay0) + (locations-different jem_bay6 jem_bay1) + (locations-different jem_bay6 jem_bay2) + (locations-different jem_bay6 jem_bay3) + (locations-different jem_bay6 jem_bay4) + (locations-different jem_bay6 jem_bay5) + (locations-different jem_bay6 jem_bay7) + (locations-different jem_bay6 jem_bay8) + (locations-different jem_bay7 jem_bay0) + (locations-different jem_bay7 jem_bay1) + (locations-different jem_bay7 jem_bay2) + (locations-different jem_bay7 jem_bay3) + (locations-different jem_bay7 jem_bay4) + (locations-different jem_bay7 jem_bay5) + (locations-different jem_bay7 jem_bay6) + (locations-different jem_bay7 jem_bay8) + (locations-different jem_bay8 jem_bay0) + (locations-different jem_bay8 jem_bay1) + (locations-different jem_bay8 jem_bay2) + (locations-different jem_bay8 jem_bay3) + (locations-different jem_bay8 jem_bay4) + (locations-different jem_bay8 jem_bay5) + (locations-different jem_bay8 jem_bay6) + (locations-different jem_bay8 jem_bay7) + + ;; === Dynamic predicates === + (robot-available bumble) + (robot-available honey) + (robot-at bumble berth1) + (robot-at honey berth2) + (location-available jem_bay0) + (location-available jem_bay1) + (location-available jem_bay2) + (location-available jem_bay3) + (location-available jem_bay4) + (location-available jem_bay5) + (location-available jem_bay6) + (location-available jem_bay7) + (location-available jem_bay8) + (completed-panorama bumble o0 jem_bay4) + (completed-panorama honey o1 jem_bay7) + (completed-stereo honey o4 jem_bay7 jem_bay4) + (need-stereo bumble o4 jem_bay1 jem_bay4) + + ;; === Static numeric fluents === + (= (order-identity o0) 0) + (= (order-identity o1) 1) + (= (order-identity o2) 2) + (= (order-identity o3) 3) + (= (order-identity o4) 4) + + ;; === Dynamic numeric fluents === + (= (robot-order bumble) -1) + (= (robot-order honey) -1) + ) ;; end :init +) ;; end problem diff --git a/astrobee/survey_manager/survey_planner/data/test_cases/completed2_config.yaml b/astrobee/survey_manager/survey_planner/data/test_cases/completed2_config.yaml new file mode 100644 index 00000000..63fc12f6 --- /dev/null +++ b/astrobee/survey_manager/survey_planner/data/test_cases/completed2_config.yaml @@ -0,0 +1,43 @@ +# Copyright (c) 2023, United States Government, as represented by the +# Administrator of the National Aeronautics and Space Administration. +# +# All rights reserved. +# +# The "ISAAC - Integrated System for Autonomous and Adaptive Caretaking +# platform" software is licensed under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with the +# License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +# Second test case for marking goals completed. This case is a copy of the baseline problem instance +# to be used on ISS, but with the first few actions marked complete to simulate the system state if +# a replan is needed partway through execution. Let's simulate the state at t=~1700 seconds when +# each robot should have completed its first two panoramas. + +goals: + +- {type: panorama, robot: bumble, order: 0, location: jem_bay4, completed: true} +- {type: panorama, robot: bumble, order: 1, location: jem_bay3, completed: true} +- {type: panorama, robot: bumble, order: 2, location: jem_bay2} +- {type: panorama, robot: bumble, order: 3, location: jem_bay1} +- {type: stereo, robot: bumble, order: 4, trajectory: jem_bay1_to_bay3} +- {type: robot_at, robot: bumble, location: berth1} + +- {type: panorama, robot: honey, order: 1, location: jem_bay7, completed: true} +- {type: panorama, robot: honey, order: 2, location: jem_bay6, completed: true} +- {type: panorama, robot: honey, order: 3, location: jem_bay5} +- {type: stereo, robot: honey, order: 4, trajectory: jem_bay7_to_bay4} +- {type: robot_at, robot: honey, location: berth2} + +init: + bumble: + location: jem_bay3 + honey: + location: jem_bay6 diff --git a/astrobee/survey_manager/survey_planner/data/test_cases/completed2_plan.pddl b/astrobee/survey_manager/survey_planner/data/test_cases/completed2_plan.pddl new file mode 100644 index 00000000..a6f6b5a9 --- /dev/null +++ b/astrobee/survey_manager/survey_planner/data/test_cases/completed2_plan.pddl @@ -0,0 +1,20 @@ +; Solution Found +0.000: (move bumble jem_bay3 jem_bay2 jem_bay1) [20.000] +0.001: (move honey jem_bay6 jem_bay5 jem_bay4) [20.000] +20.001: (panorama bumble o2 jem_bay2) [780.000] +20.002: (panorama honey o3 jem_bay5) [780.000] +800.002: (move bumble jem_bay2 jem_bay1 jem_bay0) [20.000] +800.003: (move honey jem_bay5 jem_bay6 jem_bay7) [20.000] +820.003: (panorama bumble o3 jem_bay1) [780.000] +820.004: (move honey jem_bay6 jem_bay7 jem_bay8) [20.000] +840.005: (stereo honey o4 jem_bay7 jem_bay4 jem_bay3 jem_bay5) [600.000] +1440.006: (dock honey jem_bay7 berth2) [30.000] +1600.004: (stereo bumble o4 jem_bay1 jem_bay4 jem_bay3 jem_bay5) [600.000] +2200.005: (move bumble jem_bay1 jem_bay2 jem_bay3) [20.000] +2220.006: (move bumble jem_bay2 jem_bay3 jem_bay4) [20.000] +2240.007: (move bumble jem_bay3 jem_bay4 jem_bay5) [20.000] +2260.008: (move bumble jem_bay4 jem_bay5 jem_bay6) [20.000] +2280.009: (move bumble jem_bay5 jem_bay6 jem_bay7) [20.000] +2300.010: (move bumble jem_bay6 jem_bay7 jem_bay8) [20.000] +2320.011: (dock bumble jem_bay7 berth1) [30.000] + diff --git a/astrobee/survey_manager/survey_planner/data/test_cases/completed2_plan.yaml b/astrobee/survey_manager/survey_planner/data/test_cases/completed2_plan.yaml new file mode 100644 index 00000000..5753c7a6 --- /dev/null +++ b/astrobee/survey_manager/survey_planner/data/test_cases/completed2_plan.yaml @@ -0,0 +1,179 @@ +- start_time_seconds: '0.000' + action: + type: move + robot: bumble + from_name: jem_bay3 + to_name: jem_bay2 + to_pos: + - 11.0 + - -5.0 + - 4.8 + duration_seconds: '20.000' +- start_time_seconds: '0.001' + action: + type: move + robot: honey + from_name: jem_bay6 + to_name: jem_bay5 + to_pos: + - 11.0 + - -8.0 + - 4.8 + duration_seconds: '20.000' +- start_time_seconds: '20.001' + action: + type: panorama + robot: bumble + location_name: jem_bay2 + location_pos: + - 11.0 + - -5.0 + - 4.8 + duration_seconds: '780.000' +- start_time_seconds: '20.002' + action: + type: panorama + robot: honey + location_name: jem_bay5 + location_pos: + - 11.0 + - -8.0 + - 4.8 + duration_seconds: '780.000' +- start_time_seconds: '800.002' + action: + type: move + robot: bumble + from_name: jem_bay2 + to_name: jem_bay1 + to_pos: + - 11.0 + - -4.0 + - 4.8 + duration_seconds: '20.000' +- start_time_seconds: '800.003' + action: + type: move + robot: honey + from_name: jem_bay5 + to_name: jem_bay6 + to_pos: + - 11.0 + - -9.0 + - 4.8 + duration_seconds: '20.000' +- start_time_seconds: '820.003' + action: + type: panorama + robot: bumble + location_name: jem_bay1 + location_pos: + - 11.0 + - -4.0 + - 4.8 + duration_seconds: '780.000' +- start_time_seconds: '820.004' + action: + type: move + robot: honey + from_name: jem_bay6 + to_name: jem_bay7 + to_pos: + - 11.0 + - -9.7 + - 4.8 + duration_seconds: '20.000' +- start_time_seconds: '840.005' + action: + type: stereo + robot: honey + fplan: ISAAC/jem_stereo_mapping_bay7_to_bay4 + base_name: jem_bay7 + bound_name: jem_bay4 + duration_seconds: '600.000' +- start_time_seconds: '1440.006' + action: + type: dock + robot: honey + berth: berth2 + duration_seconds: '30.000' +- start_time_seconds: '1600.004' + action: + type: stereo + robot: bumble + fplan: ISAAC/jem_stereo_mapping_bay1_to_bay3 + base_name: jem_bay1 + bound_name: jem_bay4 + duration_seconds: '600.000' +- start_time_seconds: '2200.005' + action: + type: move + robot: bumble + from_name: jem_bay1 + to_name: jem_bay2 + to_pos: + - 11.0 + - -5.0 + - 4.8 + duration_seconds: '20.000' +- start_time_seconds: '2220.006' + action: + type: move + robot: bumble + from_name: jem_bay2 + to_name: jem_bay3 + to_pos: + - 11.0 + - -6.0 + - 4.8 + duration_seconds: '20.000' +- start_time_seconds: '2240.007' + action: + type: move + robot: bumble + from_name: jem_bay3 + to_name: jem_bay4 + to_pos: + - 11.0 + - -7.0 + - 4.8 + duration_seconds: '20.000' +- start_time_seconds: '2260.008' + action: + type: move + robot: bumble + from_name: jem_bay4 + to_name: jem_bay5 + to_pos: + - 11.0 + - -8.0 + - 4.8 + duration_seconds: '20.000' +- start_time_seconds: '2280.009' + action: + type: move + robot: bumble + from_name: jem_bay5 + to_name: jem_bay6 + to_pos: + - 11.0 + - -9.0 + - 4.8 + duration_seconds: '20.000' +- start_time_seconds: '2300.010' + action: + type: move + robot: bumble + from_name: jem_bay6 + to_name: jem_bay7 + to_pos: + - 11.0 + - -9.7 + - 4.8 + duration_seconds: '20.000' +- start_time_seconds: '2320.011' + action: + type: dock + robot: bumble + berth: berth1 + duration_seconds: '30.000' diff --git a/astrobee/survey_manager/survey_planner/data/test_cases/completed2_problem.pddl b/astrobee/survey_manager/survey_planner/data/test_cases/completed2_problem.pddl new file mode 100644 index 00000000..9e2ee20a --- /dev/null +++ b/astrobee/survey_manager/survey_planner/data/test_cases/completed2_problem.pddl @@ -0,0 +1,167 @@ +;; Auto-generated by problem_generator.py. Do not edit! +;; Command was: src/survey_planner/problem_generator.py --config=data/survey_static.yaml,data/test_cases/completed2_config.yaml --output=data/test_cases/completed2_problem.pddl +;; Working directory was: data/test_cases +;; Problem template: ../../pddl/jem_survey_template.pddl +;; Config 1: data/survey_static.yaml +;; Config 2: data/test_cases/completed2_config.yaml + +(define (problem jem-survey) + (:domain survey-manager) + (:metric minimize (total-time)) + (:objects + jem_bay0 jem_bay1 jem_bay2 jem_bay3 jem_bay4 jem_bay5 jem_bay6 jem_bay7 jem_bay8 berth1 berth2 - location + bumble honey - robot + o0 o1 o2 o3 o4 - order + ) + + (:goal + (and + (completed-panorama bumble o0 jem_bay4) + (completed-panorama bumble o1 jem_bay3) + (completed-panorama bumble o2 jem_bay2) + (completed-panorama bumble o3 jem_bay1) + (completed-stereo bumble o4 jem_bay1 jem_bay4) + (robot-at bumble berth1) + (completed-panorama honey o1 jem_bay7) + (completed-panorama honey o2 jem_bay6) + (completed-panorama honey o3 jem_bay5) + (completed-stereo honey o4 jem_bay7 jem_bay4) + (robot-at honey berth2) + ) + ) + + (:init + ;; === Static predicates === + (move-connected jem_bay0 jem_bay1) + (move-connected jem_bay1 jem_bay0) + (move-connected jem_bay1 jem_bay2) + (move-connected jem_bay2 jem_bay1) + (move-connected jem_bay2 jem_bay3) + (move-connected jem_bay3 jem_bay2) + (move-connected jem_bay3 jem_bay4) + (move-connected jem_bay4 jem_bay3) + (move-connected jem_bay4 jem_bay5) + (move-connected jem_bay5 jem_bay4) + (move-connected jem_bay5 jem_bay6) + (move-connected jem_bay6 jem_bay5) + (move-connected jem_bay6 jem_bay7) + (move-connected jem_bay7 jem_bay6) + (move-connected jem_bay7 jem_bay8) + (move-connected jem_bay8 jem_bay7) + (location-real jem_bay1) + (location-real jem_bay2) + (location-real jem_bay3) + (location-real jem_bay4) + (location-real jem_bay5) + (location-real jem_bay6) + (location-real jem_bay7) + (dock-connected jem_bay7 berth1) + (dock-connected jem_bay7 berth2) + (robots-different bumble honey) + (robots-different honey bumble) + (locations-different jem_bay0 jem_bay1) + (locations-different jem_bay0 jem_bay2) + (locations-different jem_bay0 jem_bay3) + (locations-different jem_bay0 jem_bay4) + (locations-different jem_bay0 jem_bay5) + (locations-different jem_bay0 jem_bay6) + (locations-different jem_bay0 jem_bay7) + (locations-different jem_bay0 jem_bay8) + (locations-different jem_bay1 jem_bay0) + (locations-different jem_bay1 jem_bay2) + (locations-different jem_bay1 jem_bay3) + (locations-different jem_bay1 jem_bay4) + (locations-different jem_bay1 jem_bay5) + (locations-different jem_bay1 jem_bay6) + (locations-different jem_bay1 jem_bay7) + (locations-different jem_bay1 jem_bay8) + (locations-different jem_bay2 jem_bay0) + (locations-different jem_bay2 jem_bay1) + (locations-different jem_bay2 jem_bay3) + (locations-different jem_bay2 jem_bay4) + (locations-different jem_bay2 jem_bay5) + (locations-different jem_bay2 jem_bay6) + (locations-different jem_bay2 jem_bay7) + (locations-different jem_bay2 jem_bay8) + (locations-different jem_bay3 jem_bay0) + (locations-different jem_bay3 jem_bay1) + (locations-different jem_bay3 jem_bay2) + (locations-different jem_bay3 jem_bay4) + (locations-different jem_bay3 jem_bay5) + (locations-different jem_bay3 jem_bay6) + (locations-different jem_bay3 jem_bay7) + (locations-different jem_bay3 jem_bay8) + (locations-different jem_bay4 jem_bay0) + (locations-different jem_bay4 jem_bay1) + (locations-different jem_bay4 jem_bay2) + (locations-different jem_bay4 jem_bay3) + (locations-different jem_bay4 jem_bay5) + (locations-different jem_bay4 jem_bay6) + (locations-different jem_bay4 jem_bay7) + (locations-different jem_bay4 jem_bay8) + (locations-different jem_bay5 jem_bay0) + (locations-different jem_bay5 jem_bay1) + (locations-different jem_bay5 jem_bay2) + (locations-different jem_bay5 jem_bay3) + (locations-different jem_bay5 jem_bay4) + (locations-different jem_bay5 jem_bay6) + (locations-different jem_bay5 jem_bay7) + (locations-different jem_bay5 jem_bay8) + (locations-different jem_bay6 jem_bay0) + (locations-different jem_bay6 jem_bay1) + (locations-different jem_bay6 jem_bay2) + (locations-different jem_bay6 jem_bay3) + (locations-different jem_bay6 jem_bay4) + (locations-different jem_bay6 jem_bay5) + (locations-different jem_bay6 jem_bay7) + (locations-different jem_bay6 jem_bay8) + (locations-different jem_bay7 jem_bay0) + (locations-different jem_bay7 jem_bay1) + (locations-different jem_bay7 jem_bay2) + (locations-different jem_bay7 jem_bay3) + (locations-different jem_bay7 jem_bay4) + (locations-different jem_bay7 jem_bay5) + (locations-different jem_bay7 jem_bay6) + (locations-different jem_bay7 jem_bay8) + (locations-different jem_bay8 jem_bay0) + (locations-different jem_bay8 jem_bay1) + (locations-different jem_bay8 jem_bay2) + (locations-different jem_bay8 jem_bay3) + (locations-different jem_bay8 jem_bay4) + (locations-different jem_bay8 jem_bay5) + (locations-different jem_bay8 jem_bay6) + (locations-different jem_bay8 jem_bay7) + + ;; === Dynamic predicates === + (robot-available bumble) + (robot-available honey) + (robot-at bumble jem_bay3) + (robot-at honey jem_bay6) + (location-available berth1) + (location-available berth2) + (location-available jem_bay0) + (location-available jem_bay1) + (location-available jem_bay2) + (location-available jem_bay4) + (location-available jem_bay5) + (location-available jem_bay7) + (location-available jem_bay8) + (completed-panorama bumble o0 jem_bay4) + (completed-panorama bumble o1 jem_bay3) + (completed-panorama honey o1 jem_bay7) + (completed-panorama honey o2 jem_bay6) + (need-stereo bumble o4 jem_bay1 jem_bay4) + (need-stereo honey o4 jem_bay7 jem_bay4) + + ;; === Static numeric fluents === + (= (order-identity o0) 0) + (= (order-identity o1) 1) + (= (order-identity o2) 2) + (= (order-identity o3) 3) + (= (order-identity o4) 4) + + ;; === Dynamic numeric fluents === + (= (robot-order bumble) -1) + (= (robot-order honey) -1) + ) ;; end :init +) ;; end problem diff --git a/astrobee/survey_manager/survey_planner/data/test_cases/generate.bash b/astrobee/survey_manager/survey_planner/data/test_cases/generate.bash new file mode 100755 index 00000000..f5ad8b99 --- /dev/null +++ b/astrobee/survey_manager/survey_planner/data/test_cases/generate.bash @@ -0,0 +1,46 @@ +#!/bin/bash + +# Copyright (c) 2023, United States Government, as represented by the +# Administrator of the National Aeronautics and Space Administration. +# +# All rights reserved. +# +# The "ISAAC - Integrated System for Autonomous and Adaptive Caretaking +# platform" software is licensed under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with the +# License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +# DESCRIPTION +# Regenerate outputs from *_config.yaml test cases. + +set -eu -o pipefail + +THIS_FILE=$(readlink -f "$0") +THIS_DIR=$(dirname "${THIS_FILE}") + +SURVEY_PLANNER_DIR=$(readlink -f "${THIS_DIR}/../..") +PDDL_DIR="${SURVEY_PLANNER_DIR}/pddl" +DATA_DIR="${SURVEY_PLANNER_DIR}/data" +CASES_DIR="${DATA_DIR}/test_cases" +cd "${THIS_DIR}" + +TEST_CASES=(completed1 completed2) + +set -x +for i in "${TEST_CASES[@]}"; do + { echo -e "\n=== $i ==="; } 2> /dev/null + rosrun survey_planner problem_generator.py "--config=${DATA_DIR}/survey_static.yaml,${CASES_DIR}/${i}_config.yaml" "--output=${CASES_DIR}/${i}_problem.pddl" + # Try to eliminate variation in problem.pddl due to embedding different local install paths in the output. + perl -i -ple "s:${SURVEY_PLANNER_DIR}/::g;" "${CASES_DIR}/${i}_problem.pddl" + rosrun survey_planner plan_survey.py "${PDDL_DIR}/domain_survey.pddl" "${CASES_DIR}/${i}_problem.pddl" > "${CASES_DIR}/${i}_plan.pddl" + { echo "[plan_survey.py stdout redirected to ${CASES_DIR}/${i}_plan.pddl]"; } 2> /dev/null + rosrun survey_planner plan_interpreter.py "--plan=${CASES_DIR}/${i}_plan.pddl" "--plot=${CASES_DIR}/${i}_plot.png" "--output=${CASES_DIR}/${i}_plan.yaml" +done diff --git a/astrobee/survey_manager/survey_planner/src/mypy.ini b/astrobee/survey_manager/survey_planner/src/survey_planner/mypy.ini similarity index 100% rename from astrobee/survey_manager/survey_planner/src/mypy.ini rename to astrobee/survey_manager/survey_planner/src/survey_planner/mypy.ini diff --git a/astrobee/survey_manager/survey_planner/src/survey_planner/plan_survey.py b/astrobee/survey_manager/survey_planner/src/survey_planner/plan_survey.py index 9c2879ee..ba3671a4 100755 --- a/astrobee/survey_manager/survey_planner/src/survey_planner/plan_survey.py +++ b/astrobee/survey_manager/survey_planner/src/survey_planner/plan_survey.py @@ -22,6 +22,7 @@ List, NamedTuple, Optional, + Set, Tuple, Type, TypeVar, @@ -45,6 +46,7 @@ ExecutionTrace = List["TraceEvent"] OrderName = str # Names PDDL object of type order PddlExpression = Any # Actually a pp.ParseResults. But 'Any' satisfies mypy. +PddlPredicate = Any # More specific than PddlExpression, same from mypy perspective. PddlActionName = str # Names PDDL action T = TypeVar("T") # So we can assign parametric types below PddlTypeName = str # Names PDDL object type @@ -285,7 +287,7 @@ class SimState: trace: ExecutionTrace = field(default_factory=list) "Trace of timestamped actions executed so far in the sim." - completed: Dict[Any, bool] = field(default_factory=dict) + completed: Set[str] = field(default_factory=set) "Tracks completion status for actions that subclass MarkCompleteAction." events: PriorityQueue[PendingEvent] = field(default_factory=PriorityQueue) @@ -325,29 +327,40 @@ class MarkCompleteAction(Action): order: OrderName "Propagates goal ordering constraint from problem instance. Required by PDDL model." + def get_completed_predicate(self) -> str: + "Return the completed-x predicate asserted when this action is completed." + return repr(self).replace("(", "(completed-", 1) + def end(self, sim_state: SimState) -> None: super().end(sim_state) - sim_state.completed[repr(self)] = True + sim_state.completed.add(self.get_completed_predicate()) def is_complete(self, sim_state: SimState) -> bool: """ Return True if this action has been completed in `sim_state`. """ - return sim_state.completed.get(repr(self), False) + return self.get_completed_predicate() in sim_state.completed def get_collision_check_locations( - from_pos: LocationName, to_pos: LocationName + from_pos: LocationName, to_pos: LocationName, include_to_pos: bool = True ) -> List[str]: """ - Return neighbors of `to_pos` that are distinct from `from_pos` and are flying locations. A move - is invalid if one of these collision check locations is reserved by another robot. The PDDL - models of some actions also require collision check locations to be specified as arguments. + Return locations that need to be checked for collision avoidance purposes prior to moving to + `to_pos`. Locations include `to_pos` itself and neighbors of `to_pos` that are distinct from + `from_pos` and are flying locations. If another robot has reserved any of these locations the + move is invalid. + + :param include_to_pos: If False, don't include `to_pos` in the result. This is handy + for filling in move action "check" arguments that conform to the PDDL planning model. """ to_ind = CONFIG.location_lookup[to_pos] to_neighbors_ind = CONFIG.neighbors[to_ind] to_neighbors = [CONFIG.locations[n] for n in to_neighbors_ind] - return [n for n in to_neighbors if is_location_flying(n) and n != from_pos] + result = [n for n in to_neighbors if is_location_flying(n) and n != from_pos] + if include_to_pos: + result.append(to_pos) + return result def get_collision_reason( @@ -383,11 +396,13 @@ class AbstractMoveAction(Action): to: LocationName "The location the robot should move to." - def get_collision_check_locations(self) -> List[str]: + def get_collision_check_locations(self, include_to_pos: bool = True) -> List[str]: """ Return collision check locations for this move action. """ - return get_collision_check_locations(self.from_pos, self.to) + return get_collision_check_locations( + self.from_pos, self.to, include_to_pos=include_to_pos + ) def get_pddl_args(self) -> List[Any]: return [self.robot, self.from_pos, self.to] @@ -433,7 +448,10 @@ class MoveAction(AbstractMoveAction): def get_pddl_args(self) -> List[Any]: # One check location argument is required for move - return super().get_pddl_args() + self.get_collision_check_locations()[:1] + return ( + super().get_pddl_args() + + self.get_collision_check_locations(include_to_pos=False)[:1] + ) def invalid_reason(self, sim_state: SimState) -> str: super_reason = super().invalid_reason(sim_state) @@ -465,7 +483,10 @@ class UndockAction(AbstractMoveAction): def get_pddl_args(self) -> List[Any]: # Two check location arguments are required for undock - return super().get_pddl_args() + self.get_collision_check_locations()[:2] + return ( + super().get_pddl_args() + + self.get_collision_check_locations(include_to_pos=False)[:2] + ) def invalid_reason(self, sim_state: SimState) -> str: super_reason = super().invalid_reason(sim_state) @@ -507,9 +528,20 @@ class StereoAction(MarkCompleteAction): def get_pddl_args(self) -> List[Any]: # Two collision check arguments are required for stereo action - check_locs = get_collision_check_locations(self.base, self.bound)[:2] + check_locs = get_collision_check_locations( + self.base, self.bound, include_to_pos=False + )[:2] return [self.robot, self.order, self.base, self.bound] + check_locs + def get_completed_predicate(self) -> str: + # Unlike the panorama action, where the completed-panorama predicate takes exactly the same + # arguments as the action, for the completed-stereo predicate we don't include the collision + # check arguments. + args_str = " ".join( + ["completed-stereo", self.robot, self.order, self.base, self.bound] + ) + return f"({args_str})" + def invalid_reason(self, sim_state: SimState) -> str: robot_state = sim_state.robot_states[self.robot] if robot_state.pos != self.base: @@ -573,7 +605,7 @@ class RobotExecState: status: RobotStatus = "INIT" """ - Execution states of the robot. DONE = completed all goals, BLOCKED = the next action to perform + Execution status of the robot. DONE = completed all goals, BLOCKED = the next action to perform is not (yet) valid, ACTIVE = robot is actively working on a goal. """ @@ -750,7 +782,7 @@ def get_completing_action(self) -> MarkCompleteAction: raise NotImplementedError() # Note: Can't mark as @abstractmethod due to mypy limitation def __repr__(self) -> str: - return repr(self.get_completing_action()).replace("(", "(completed-", 1) + return self.get_completing_action().get_completed_predicate() def is_complete(self, exec_state: ExecState) -> bool: return self.get_completing_action().is_complete(exec_state.sim_state) @@ -910,7 +942,10 @@ def filter_none(seq: Iterable[Optional[T]]) -> List[T]: def get_objects_by_type( problem: PddlExpression, ) -> Dict[PddlTypeName, List[PddlObjectName]]: - "Return robot names parsed from PDDL problem instance." + """ + Return mapping from PDDL type name to a list of object instances of that type, parsed from + `problem`. + """ (objects_clause,) = [e for e in problem[2:] if e[0] == ":objects"] # object_decls example: ["foo", "bar", "-", "int", "baz", "-", "float"] object_decls = list(objects_clause[1:]) @@ -943,15 +978,26 @@ def get_robot_goals(problem: PddlExpression) -> Dict[RobotName, List[Goal]]: return robot_goals +def get_init_predicates(problem: PddlExpression) -> List[PddlPredicate]: + "Return the predicates in the initial state of `problem`." + # Example problem snippet: + # (define (problem jem-survey) + # ... other clauses ... + # (:init (pred1 arg1 arg2) (pred2 arg1 arg2))) + # + # We want to return the list of predicates in the :init clause. + (init_clause,) = [e for e in problem[2:] if e[0] == ":init"] + return init_clause[1:] + + def get_robot_states(problem: PddlExpression) -> Dict[RobotName, RobotState]: """ - Return a mapping of robot name to robot state parsed from `problem`. + Return a mapping of robot name to robot state extracted from `problem`. """ robot_states = { robot: RobotState(pos="", action=None, reserved=[]) for robot in CONFIG.robots } - (init_clause,) = [e for e in problem[2:] if e[0] == ":init"] - init_predicates = init_clause[1:] + init_predicates = get_init_predicates(problem) robot_at_predicates = [p for p in init_predicates if p[0] == "robot-at"] for _, robot, pos in robot_at_predicates: robot_states[robot].pos = pos @@ -959,6 +1005,17 @@ def get_robot_states(problem: PddlExpression) -> Dict[RobotName, RobotState]: return robot_states +def get_completed_predicates(problem: PddlExpression) -> List[PddlPredicate]: + "Return the completed action predicates in the initial state of `problem`." + return [p for p in get_init_predicates(problem) if p[0].startswith("completed-")] + + +def string_from_predicate(expr: PddlPredicate) -> str: + "Return the PDDL string representation of `expr`." + space_list = " ".join((str(item) for item in expr)) + return f"({space_list})" + + def survey_planner(domain_path: pathlib.Path, problem_path: pathlib.Path): "Primary driver function for custom planning." @@ -975,8 +1032,10 @@ def survey_planner(domain_path: pathlib.Path, problem_path: pathlib.Path): robot_goals = get_robot_goals(problem_expr) robot_states = get_robot_states(problem_expr) + completed_predicates = get_completed_predicates(problem_expr) + completed_set = {string_from_predicate(p) for p in completed_predicates} - sim_state = SimState(robot_states=robot_states) + sim_state = SimState(robot_states=robot_states, completed=completed_set) robot_exec_states = { robot: RobotExecState(robot_goals[robot]) for robot in CONFIG.robots } diff --git a/astrobee/survey_manager/survey_planner/src/survey_planner/problem_generator.py b/astrobee/survey_manager/survey_planner/src/survey_planner/problem_generator.py index 4968f00c..5bfea135 100755 --- a/astrobee/survey_manager/survey_planner/src/survey_planner/problem_generator.py +++ b/astrobee/survey_manager/survey_planner/src/survey_planner/problem_generator.py @@ -99,10 +99,14 @@ def load_yaml(yaml_path: pathlib.Path) -> YamlMapping: return yaml.safe_load(yaml_stream) -def get_stereo_traj(static_config, base, bound): +def get_stereo_traj(config: YamlMapping, base: str, bound: str) -> str: + """ + Return the name of the fplan file that specifies the trajectory for the stereo action with + arguments `base` and `bound`, as looked up in `config`. + """ traj_matches = [ traj - for traj in static_config["stereo"].values() + for traj in config["stereo"].values() if traj["base_location"] == base and traj["bound_location"] == bound ] assert ( @@ -112,9 +116,7 @@ def get_stereo_traj(static_config, base, bound): return fplan -def yaml_action_from_pddl( - action: str, static_config: YamlMapping -) -> Optional[YamlMapping]: +def yaml_action_from_pddl(action: str, config: YamlMapping) -> Optional[YamlMapping]: """ Return a YamlMapping representation of `action`. This is the only place we really need domain-specific logic. @@ -143,7 +145,7 @@ def yaml_action_from_pddl( "robot": robot, "from_name": from_bay, "to_name": to_bay, - "to_pos": static_config["bays"][to_bay], + "to_pos": config["bays"][to_bay], } if action_type == "panorama": @@ -153,13 +155,13 @@ def yaml_action_from_pddl( "type": "panorama", "robot": robot, "location_name": location, - "location_pos": static_config["bays"][location], + "location_pos": config["bays"][location], } if action_type == "stereo": robot, _order, base, bound, _check1, _check2 = action_args[1:] # Use base and bound to look up trajectory. - fplan = get_stereo_traj(static_config, base, bound) + fplan = get_stereo_traj(config, base, bound) # Can discard order check1, check2. return { @@ -177,9 +179,10 @@ def yaml_action_from_pddl( return {} # Make pylint happy -def pddl_goal_from_yaml(goal: YamlMapping, config_static: YamlMapping) -> str: +def pddl_goal_from_yaml(goal: YamlMapping, config: YamlMapping) -> str: """ - Convert a YAML goal with named fields from the dynamic config into a PDDL goal predicate. + Return the result of converting `goal` from a YAML object with named fields to a PDDL goal + predicate, looking up info in `config` as needed. """ goal_type = goal["type"] assert ( @@ -196,7 +199,7 @@ def pddl_goal_from_yaml(goal: YamlMapping, config_static: YamlMapping) -> str: robot = goal["robot"] order = goal["order"] trajectory = goal["trajectory"] - traj_info = config_static["stereo"][trajectory] + traj_info = config["stereo"][trajectory] base = traj_info["base_location"] bound = traj_info["bound_location"] return f"(completed-stereo {robot} o{order} {base} {bound})" @@ -451,7 +454,7 @@ def problem_generator( robots = config["robots"] writer.declare_instances(robots, "robot") - max_order = max([goal.get("order", -1) for goal in config["goals"]]) + max_order = max((goal.get("order", -1) for goal in config["goals"])) num_orders = max_order + 1 orders = [f"o{i}" for i in range(num_orders)] writer.declare_instances(orders, "order") @@ -503,10 +506,17 @@ def problem_generator( ] writer.declare_predicates(location_available_lines, "dynamic_predicates") + completed_goals = [ + pddl_goal_from_yaml(goal, config) + for goal in yaml_goals + if goal.get("completed", False) + ] + writer.declare_predicates(completed_goals, "dynamic_predicates") + need_stereo_lines = [ - goal.replace("completed-stereo", "need-stereo") - for goal in pddl_goals - if "completed-stereo" in goal + pddl_goal_from_yaml(goal, config).replace("completed-stereo", "need-stereo") + for goal in yaml_goals + if goal["type"] == "stereo" and not goal.get("completed", False) ] writer.declare_predicates(need_stereo_lines, "dynamic_predicates") @@ -519,7 +529,7 @@ def problem_generator( output_path = pathlib.Path( str(output_path_template).replace("{ext}", writer.get_extension()) ) - output_path.write_text(writer.getvalue()) + output_path.write_text(writer.getvalue(), encoding="utf-8") print(f"Wrote to {output_path}", file=sys.stderr)