Skip to content

Commit

Permalink
Added support to timed_effects, timed_goals, simulated effects and si…
Browse files Browse the repository at this point in the history
…multaneous events
  • Loading branch information
Framba-Luca committed Dec 19, 2023
1 parent fdbbc0c commit d8758b4
Show file tree
Hide file tree
Showing 3 changed files with 128 additions and 65 deletions.
21 changes: 13 additions & 8 deletions unified_planning/plans/stn_plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
#


from itertools import product
from itertools import chain, product
from numbers import Real
import unified_planning as up
import unified_planning.plans as plans
Expand Down Expand Up @@ -198,17 +198,18 @@ def __init__(

# Create and populate the DeltaSTN
self._stn = DeltaSimpleTemporalNetwork()
start_plan = STNPlanNode(TimepointKind.GLOBAL_START)
end_plan = STNPlanNode(TimepointKind.GLOBAL_END)
self._stn.insert_interval(start_plan, end_plan, left_bound=Fraction(0))
if isinstance(constraints, List):
gen: Iterator[
Tuple[STNPlanNode, Optional[Real], Optional[Real], STNPlanNode]
] = iter(constraints)
else:
assert isinstance(constraints, Dict), "Typing not respected"
gen = flatten_dict_structure(constraints)
start_plan = STNPlanNode(TimepointKind.GLOBAL_START)
end_plan = STNPlanNode(TimepointKind.GLOBAL_END)
assert start_plan is not None and end_plan is not None
f0 = Fraction(0)
self._stn.insert_interval(start_plan, end_plan, left_bound=f0)
for a_node, lower_bound, upper_bound, b_node in gen:
if (
a_node.environment is not None
Expand All @@ -220,10 +221,14 @@ def __init__(
raise UPUsageError(
"Different environments given inside the same STNPlan!"
)
self._stn.insert_interval(start_plan, a_node, left_bound=f0)
self._stn.insert_interval(a_node, end_plan, left_bound=f0)
self._stn.insert_interval(start_plan, b_node, left_bound=f0)
self._stn.insert_interval(b_node, end_plan, left_bound=f0)
if a_node != start_plan:
self._stn.insert_interval(start_plan, a_node, left_bound=f0)
if a_node != end_plan:
self._stn.insert_interval(a_node, end_plan, left_bound=f0)
if b_node != start_plan:
self._stn.insert_interval(start_plan, b_node, left_bound=f0)
if b_node != end_plan:
self._stn.insert_interval(b_node, end_plan, left_bound=f0)
lb = None
if isinstance(lower_bound, Fraction):
lb = lower_bound
Expand Down
104 changes: 56 additions & 48 deletions unified_planning/plans/time_triggered_plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -243,25 +243,20 @@ def _absolute_time(


EPSILON = Fraction(1, 1000)
MAX_TIME = Fraction(5000, 1)


def _convert_to_stn(
time_triggered_plan: TimeTriggeredPlan,
problem: "up.model.AbstractProblem",
) -> "plans.stn_plan.STNPlan":
# This algorithm takes the TimeTriggeredPlan and converts it to an STNPlan, by
# removing the temporal dimension, creating a SequentialPlan, then de-ordering the
# SequentialPlan creating a PartialOrderPlan and re-adding the temporal dimension,
# getting an STNPlan in the end.
from unified_planning.plans.stn_plan import STNPlanNode, STNPlan

assert isinstance(problem, Problem), "This algorithm works only for Problem"

# TODO add support for timed_effects in problem and support for 2 events to happen in the same moment
# - timed effects
# - timed goals
# - 2 events in the same moment (merge them or keep splitted)
# - simulated effects
# - understand if the way durative conditions are handled is OK (by "sampling" only start and end as events with no effects)
# - eventually: support for task ordering

# Constraints that go in the final STNPlan
stn_constraints: Dict[
STNPlanNode, List[Tuple[Optional[Fraction], Optional[Fraction], STNPlanNode]]
Expand All @@ -273,7 +268,7 @@ def _convert_to_stn(
Dict[Timing, Tuple[List[FNode], List[Effect], Optional[SimulatedEffect]]],
] = {}

# Mapping from an ActionInstance Starting STNPlanNodes
# Mapping from an ActionInstance to it's Starting STNPlanNodes
ai_to_start_node: Dict["plans.plan.ActionInstance", STNPlanNode] = {}

# create a mockup durative action that contains the problem's timed_effects and conditions
Expand All @@ -286,7 +281,7 @@ def _convert_to_stn(
end_timepoint = Timepoint(
TimepointKind.START if interval.upper.is_from_start() else TimepointKind.END
)
end_timing = Timing(interval.lower.delay, end_timepoint)
end_timing = Timing(interval.upper.delay, end_timepoint)
relative_interval = TimeInterval(
start_timing, end_timing, interval.is_left_open(), interval.is_right_open()
)
Expand All @@ -304,14 +299,18 @@ def _convert_to_stn(

mockup_action_instance = plans.ActionInstance(mockup_action)

# Iterate over all the actions of the plan + the mockup action
# For each action create the starting node, split it into events
# and populate the events_table
for start, ai, duration in chain(
[(Fraction(0), mockup_action_instance, Fraction(-1))],
time_triggered_plan.timed_actions,
):
if ai == mockup_action_instance:
start_node, end_node = STNPlanNode(TimepointKind.GLOBAL_START, None), None
start_node = STNPlanNode(TimepointKind.GLOBAL_START, None)
else:
start_node, end_node = STNPlanNode(TimepointKind.START, ai), None
start_node = STNPlanNode(TimepointKind.START, ai)
end_node = None
action = ai.action
action_cpl = (start, ai, duration)
assert action_cpl not in event_table
Expand Down Expand Up @@ -372,7 +371,6 @@ def _convert_to_stn(
):
continue # Empty interval

# TODO understand the edge case where a condition ends with an end open interval
# Add the conditions only if they are not part of the mockup action or they are not at the end of the plan
pconditions, effects, sim_eff = timing_to_cond_effects.get(
start_timing, ([], [], None)
Expand All @@ -395,26 +393,26 @@ def _convert_to_stn(
stn_constraints[start_node] = [(duration, duration, end_node)]
ai_to_start_node[ai] = start_node

# Convert event table to a list of Instantaneous events
events: List[Tuple[Fraction, "plans.plan.ActionInstance"]] = []
# Mapping from an event to the action (and the relative time) that created it
# Convert event table to a map from time to simultaneous events
events: Dict[Fraction, List["plans.plan.ActionInstance"]] = {}

# Mapping from an event to the action and the relative time that created it
event_creating_ais: Dict[
"plans.plan.ActionInstance", List[Tuple["plans.plan.ActionInstance", Fraction]]
"plans.plan.ActionInstance", Tuple["plans.plan.ActionInstance", Fraction]
] = {}

for (start, ai, duration), time_to_cond_eff in event_table.items():
if duration is None:
assert isinstance(
ai.action, InstantaneousAction
), "Error, None duration specified for non InstantaneousAction"
events.append((start, ai))
event_creating_ais[ai] = [(ai, Fraction(0))]
events.setdefault(start, []).append(ai)
event_creating_ais[ai] = (ai, Fraction(0))
continue
for i, (time_pt, (conditions, effects, sim_eff)) in enumerate(
time_to_cond_eff.items()
):
time = _absolute_time(time_pt, start, duration)
# inst_action = InstantaneousAction(str(ai) + str(time_pt))
inst_action = InstantaneousAction(
f"{ai.action.name}_{i}",
_parameters=OrderedDict(
Expand All @@ -428,44 +426,54 @@ def _convert_to_stn(
if sim_eff is not None:
inst_action.set_simulated_effect(sim_eff)
inst_ai = plans.ActionInstance(inst_action, ai.actual_parameters)
events.append((time, inst_ai))
# TODO here is the place where if 2 events happen at the same time can/should/must be merged
event_creating_ais[inst_ai] = [(ai, time - start)]
events.setdefault(time, []).append(inst_ai)
event_creating_ais[inst_ai] = (ai, time - start)

simultaneous_events: List[Set["plans.plan.ActionInstance"]] = [
set(l) for l in events.values() if len(l) > 1
]

# sort events and create a map from action instance to it's time
events = sorted(events, key=lambda acts: acts[0])
sorted_events = sorted(events.items(), key=lambda acts: acts[0])

act_to_time_map: Dict["plans.plan.ActionInstance", Fraction] = dict(
[(value, key) for key, value in events]
)
# Create the equivalent sequential plan and then deorder it to partial order plan
list_act = [ia for _, ia in events]
list_act = [ia for _, se in sorted_events for ia in se]
seq_plan = plans.SequentialPlan(list_act)
partial_order_plan = seq_plan.convert_to(plans.PlanKind.PARTIAL_ORDER_PLAN, problem)
assert isinstance(partial_order_plan, plans.PartialOrderPlan)

for ai_current, l_next_ai in partial_order_plan.get_adjacency_list.items():
ai_current_time = act_to_time_map[
ai_current
] # TODO consider the case where you have timed_goals/effect
# Get the ActionInstance that generated this event and add the constraint between the starting of the action and the start
# of the action that generated the other event
current_generating_ai, current_skew_time = event_creating_ais[ai_current][
0
] # TODO for now it's a list of only 1 element. When 2 events in the same moment will be merged this needs to change
# of the action that generated the other event.
# If the 2 events were simultaneous and have a constraint, they are forced to happen together, otherwise the event
# that is scheduled later in the original plan is forced to happen later also in the STN (later by an EPSILON > 0)
current_generating_ai, current_skew_time = event_creating_ais[ai_current]
current_start_node = ai_to_start_node[current_generating_ai]

current_simultaneous_events = None
for se in simultaneous_events:
if ai_current in se:
current_simultaneous_events = se
break

for ai_next in l_next_ai:
# Time between two differents actions depend on epsilon
next_generating_ai, next_skew_time = event_creating_ais[ai_next][
0
] # TODO for now it's a list of only 1 element. When 2 events in the same moment will be merged this needs to change
next_generating_ai, next_skew_time = event_creating_ais[ai_next]
next_start_node = ai_to_start_node[next_generating_ai]

if current_generating_ai != next_generating_ai:
upper_bound = None
if (
current_simultaneous_events is not None
and ai_next in current_simultaneous_events
):
lower_bound = current_skew_time - next_skew_time
upper_bound = lower_bound
else:
lower_bound = current_skew_time - next_skew_time + EPSILON

stn_constraints.setdefault(current_start_node, []).append(
(
current_skew_time - next_skew_time + EPSILON,
MAX_TIME,
lower_bound,
upper_bound,
next_start_node,
)
)
Expand Down Expand Up @@ -500,10 +508,10 @@ def is_time_in_interv(
Return if the timepoint is in the interval given.
"""
time_pt = _absolute_time(timing, start=start, duration=duration)
upper_time = _absolute_time(interval._upper, start=start, duration=duration)
lower_time = _absolute_time(interval._lower, start=start, duration=duration)
if (time_pt > lower_time if interval._is_left_open else time_pt >= lower_time) and (
time_pt < upper_time if interval._is_right_open else time_pt <= upper_time
):
upper_time = _absolute_time(interval.upper, start=start, duration=duration)
lower_time = _absolute_time(interval.lower, start=start, duration=duration)
if (
time_pt > lower_time if interval.is_left_open() else time_pt >= lower_time
) and (time_pt < upper_time if interval.is_right_open() else time_pt <= upper_time):
return True
return False
68 changes: 59 additions & 9 deletions unified_planning/test/test_ttp_to_stn.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,51 @@
from unified_planning.plans.ttp_to_stn import *
from unified_planning.test.examples import get_example_problems
from unified_planning.test import unittest_TestCase
from unified_planning.test import skipIfNoPlanValidatorForProblemKind


up.shortcuts.get_environment().credits_stream = None


def simultaneity_problem() -> Problem:
problem = Problem("test_simultaneity")
x = problem.add_fluent("x", default_initial_value=True)
y = problem.add_fluent("y", default_initial_value=True)
z = problem.add_fluent("z", default_initial_value=True)
k = problem.add_fluent("k", default_initial_value=False)

problem.add_timed_effect(GlobalStartTiming(5), k, True)

a = DurativeAction("a")
a.add_condition(ClosedTimeInterval(StartTiming(), EndTiming()), y)
a.add_condition(TimePointInterval(StartTiming()), k)
a.add_effect(EndTiming(), x, False)
a.set_fixed_duration(1)
problem.add_action(a)

b = DurativeAction("b")
b.add_condition(ClosedTimeInterval(StartTiming(), EndTiming()), x)
b.add_effect(EndTiming(), y, False)
b.set_fixed_duration(1)
problem.add_action(b)

c = DurativeAction("c")
c.add_condition(ClosedTimeInterval(StartTiming(), EndTiming()), z)
c.add_effect(EndTiming(), z, False)
c.set_fixed_duration(1)
problem.add_action(c)

problem.add_goal(Not(x))
problem.add_goal(Not(y))
problem.add_goal(Not(z))
problem.add_goal(k)

return problem


sim_problem = simultaneity_problem()


class TestTTPToSTN(unittest_TestCase):
def setUp(self) -> None:
unittest_TestCase.setUp(self)
Expand Down Expand Up @@ -43,9 +84,6 @@ def test_all_valid(self):
for name, tc in self.problems.items():
for valid_plan in tc.valid_plans:
if valid_plan.kind == PlanKind.TIME_TRIGGERED_PLAN:
if not tc.problem.timed_effects:
continue
print(name)
stn_plan = valid_plan.convert_to(PlanKind.STN_PLAN, tc.problem)
tt_plan = stn_plan.convert_to(
PlanKind.TIME_TRIGGERED_PLAN, tc.problem
Expand All @@ -58,10 +96,22 @@ def test_all_valid(self):
self.assertTrue(val_res)
except up.exceptions.UPNoSuitableEngineAvailableException as e:
pass
print(tc.problem)
print(valid_plan)
print(stn_plan)
plot_stn_plan(stn_plan)
print("----------------------------------------------\n\n")

assert False
@skipIfNoPlanValidatorForProblemKind(sim_problem.kind)
def test_simultaneity(self):

a = sim_problem.action("a")
b = sim_problem.action("b")
c = sim_problem.action("c")

st = Fraction(6)
dur = Fraction(1)
action_instances = (a(), b(), c())
valid_plan = TimeTriggeredPlan([(st, act, dur) for act in action_instances])
stn_plan = valid_plan.convert_to(PlanKind.STN_PLAN, sim_problem)
tt_plan = stn_plan.convert_to(PlanKind.TIME_TRIGGERED_PLAN, sim_problem)
with PlanValidator(
problem_kind=sim_problem.kind, plan_kind=tt_plan.kind
) as validator:
val_res = validator.validate(sim_problem, tt_plan)
self.assertTrue(val_res)

0 comments on commit d8758b4

Please sign in to comment.