Source code for up_SMT_engine.ProblemBuilder.BaseProblemBuilder
from z3 import And, Not, Or
[docs]class BaseProblemBuilder:
"""Class used to handle constructing the SMT problem for Z3 to solve
Suitable for sequential planning
"""
def __init__(self, actions, fluents, is_incremental, initial_values):
self.actions = actions
self.fluents = fluents
self.incremental = is_incremental
self.initial_values = initial_values
self.num_mutexes = 0
[docs] def get_mutex_count(self):
return self.num_mutexes
def __generate_parallelism_mutexes(self, plan_len):
"""Generate mutexes for sequential parallelism
Use basic encoding requiring O(n^2) constraints per timestep
Assert that, for every pair of actions at time t, only one can be true
Assert that, for every action at time t at least one can be true (using OR)
Args:
plan_len (int): Plan length
Returns:
Array of constraint clauses: Array of constraint clauses. If using incremental solving this only covers the penultimate timestep. Otherwise the array covers the first to penultimate timesteps
"""
constraints = []
if self.incremental:
t = plan_len - 1
actions_at_t = []
for i in range(0, len(self.actions)):
action_a = self.actions[i].get_action_at_t(t)
actions_at_t.append(action_a)
for j in range(0, i):
action_b = self.actions[j].get_action_at_t(t)
constraints.append(Not(And(action_a, action_b)))
constraints.append(Or(actions_at_t))
else:
for t in range(0, plan_len):
timestep_constraints = []
actions_at_t = []
for i in range(0, len(self.actions)):
action_a = self.actions[i].get_action_at_t(t)
actions_at_t.append(action_a)
for j in range(0, i):
action_b = self.actions[j].get_action_at_t(t)
timestep_constraints.append(Not(And(action_a, action_b)))
timestep_constraints.append(Or(actions_at_t))
constraints.append(timestep_constraints)
return constraints
[docs] def add_mutexes(self, instance, mutex_array):
"""Add mutex clauses to the solver while counting each mutex
Args:
instance (z3.Solver): Current Solver to which clauses can be added
mutex_array (Array of constraint clauses): Array of mutex clauses to add
"""
if self.incremental:
# Expect a 1d array of constraints
self.num_mutexes += len(mutex_array)
for constraint in mutex_array:
instance.add(constraint)
else:
# Expect a 2d array of constraints
for constraint_set in mutex_array:
self.num_mutexes += len(constraint_set)
for constraint in constraint_set:
instance.add(constraint)
[docs] def add_action_constraints(self, problem_instance, plan_len):
"""For each action object generate all relevant constraints
Args:
problem_instance (z3.Solver): Current Solver to which clauses can be added
plan_len (int): Plan length
"""
# Generate all causal constraints over all actions
for action in self.actions:
if self.incremental:
problem_instance.add(action.get_causal_axioms_at_t(plan_len))
else:
axioms = action.get_causal_axioms_up_to_t(plan_len)
for axiom in axioms:
problem_instance.add(axiom)
# Generate all precondition constraints over all actions
for action in self.actions:
if self.incremental:
problem_instance.add(action.get_precondition_constraints_at_t(plan_len))
else:
constraints = action.get_precondition_constraints_up_to_t(plan_len)
for constraint in constraints:
problem_instance.add(constraint)
[docs] def add_fluent_constraints(self, problem_instance, plan_len):
"""For each fluent object generate all relevant constraints
Args:
problem_instance (z3.Solver): Current Solver to which clauses can be added
plan_len (int): Plan length
"""
# Generate bound constraints over all fluents
for fluent in self.fluents:
if self.incremental:
problem_instance.add(fluent.get_bound_constraints_at_t(plan_len))
else:
bound_constraints = fluent.get_bound_constraints_up_to_t(plan_len)
if bound_constraints is not None:
for constraint in bound_constraints:
problem_instance.add(constraint)
if plan_len > 0:
# Generate frame axiom constraints over all fluents
for fluent in self.fluents:
if self.incremental:
problem_instance.add(
fluent.generate_frame_axiom_constraints_at_t(plan_len)
)
else:
problem_instance.add(
fluent.generate_frame_axiom_constraints_up_to_t(plan_len)
)
[docs] def add_init(self, problem_instance, plan_len):
"""Add initial values if not already present. If using incremental pop the previous goal clause
Args:
problem_instance (z3.Solver): The current solver to which clauses are added
plan_len (int): The plan length
"""
if plan_len == 0 or not self.incremental:
# Reset mutex count
self.num_mutexes = 0
# Add initial state constraints
for init_value in self.initial_values:
problem_instance.add(init_value)
elif problem_instance is not None and self.incremental:
# Pop previous goal clause
problem_instance.pop()
[docs] def add_goal(self, problem_instance, goal_clause):
"""Add the goal value to the solver, and create a checkpoint if using incremental solving
Args:
problem_instance (z3.Solver): The current solver to which clauses are added
goal_clause (Clause): The clause representing all goal conditions
"""
if self.incremental:
# Create a breakpoint, allowing the current goals to be removed if they are unsatisfiable
# Using push automatically turns on incremental mode for the solver
problem_instance.push()
problem_instance.add(goal_clause)
[docs] def build(self, problem_instance, plan_len, goal_clause):
"""Using clauses generated by actions and fluents build the problem in the z3 Solver
Args:
problem_instance (z3.Solver): The current solver to which clauses are added
plan_len (int): The plan length
goal_clause (Clause): The clause representing all goal conditions
"""
self.add_init(problem_instance, plan_len)
self.add_fluent_constraints(problem_instance, plan_len)
# Don't consider actions, frame axiom constraints and chained variables until after timestep 0
if plan_len > 0:
self.add_action_constraints(problem_instance, plan_len)
# Generate action parallelism constraints over all actions
mutexes = self.__generate_parallelism_mutexes(plan_len)
self.add_mutexes(problem_instance, mutexes)
self.add_goal(problem_instance, goal_clause)