Source code for up_SMT_engine.helper_functions.FNODEHelperFunctions

# Custom functions for conversion to Z3 format
# Helper functions for SmtFluent and SmtAction classes
from z3 import Bool, Real, Int, Implies, And, Or, Not, Exists, ForAll
from unified_planning.model import FNode


[docs]def get_fluent_name_at_state(fluent_name, t): """Generate the variable name for a Fluent, at timestep t Args: fluent_name (String): The Fluent's name t (int): timestep Returns: String: variable name for a Fluent, at timestep t """ name = fluent_name + "_@t" + str(t) return name
[docs]def create_stated_action_instance(action, t): """Creates a Bool for the fluent or action at time 't', if a bool with that name exists then returns a reference to that bool Args: action (String): Action name t (int): timestep Returns: z3.Bool: a Bool for the fluent or action at time 't', if a bool with that name exists then returns a reference to that bool """ name = action + "_@t" + str(t) return Bool(name)
def __convert_FNODE_args(args, t, r2exists_tuple=None): """Handles recursively calling convert_FNODE_to_Z3 for each FNODE argument Args: args (FNODE.args): An FNODE's arguments t (int): current timestep r2exists_tuple (Tuple, optional): Tuple of values required for handling chained variables for relaxed relaxed ThereExists parallelism. Defaults to None. Returns: Array: Array of z3 expressions """ parsed_args = [] for argument in args: parsed_args.append(convert_FNODE_to_Z3(argument, t, r2exists_tuple)) return parsed_args
[docs]def get_effected_fluent_name(fluent_name, args): """If a fluent has arguments then ground it by including arguments in name Args: fluent_name (String): Ungrounded fluent name args (FNODE.args): Parameter values for ungrounded fluent Returns: String: Grounded fluent name """ if not (args is None) and (len(args) > 0): args_string = "" for arg in args: if isinstance(arg, FNode) and arg.is_object_exp(): if arg.is_object_exp(): args_string = args_string + "_" + arg.object().name else: print("Unexpected FNODE parameter type:") print(arg.node_type) else: args_string = args_string + "_" + arg.name name = fluent_name + args_string else: name = fluent_name return name
[docs]def createR2ExistsTuple(action_index, is_effect, fluents_list, actions_list): """Function for creating a relaxed relaxed ThereExists tuple, used to convey information required to handle chained variables Args: action_index (int): Index of current action in ordered action list. is_effect (bool): If True find chained variable corresponding to the current action fluents_list (List): List of fluents actions_list (List): Ordered list of Actions Returns: Tuple: Tuple used to convey information required to handle chained variables """ return (action_index, is_effect, fluents_list, actions_list)
[docs]def getR2ExistsTupleValue(info_tuple, val_name): """Tuple used to convey information needed for converting an FNODE to z3 for relaxed relaxed ThereExists encoding Extra information is needed due to chained variables Args: info_tuple (Tuple): Tuple used to convey information needed for converting an FNODE to z3 for relaxed relaxed ThereExists encoding val_name (String): Information required from tuple Returns: int or Bool or List: action index, or is_effect, or fluents list, or actions list """ if info_tuple is None: return None if val_name == "action_index": # action_index: The calling action's index. Used for relaxed relaxed ThereExists to find the corresponding chain variable return info_tuple[0] elif val_name == "is_effect": # is_effect: A boolean value. If true we are concerned with the corresponding chain variable to current action. If false we are concerned with the previous chained variable return info_tuple[1] elif val_name == "fluents_list": # fluents_list: A list of all fluents. Used to find chained-variables return info_tuple[2] elif val_name == "actions_list": # actions_list: A list of all actions, in order. Used to find chained-variables return info_tuple[3] else: return None
def __create_stated_fluent_instance(fluent, args, t, r2exists_tuple=None): """Creates a Bool, Real or Int for the fluent at time 't', if a value with that name exists then returns a reference This effectively both grounds a fluent, and transforms it into a variable representing that fluent at time t. Args: fluent (FNODE.fluent): Fluent contained by FNODE args (FNODE.args): Fluent arguments t (int): current timestep r2exists_tuple (Tuple, optional): Tuple used to convey information needed for converting an FNODE to z3 for relaxed relaxed ThereExists encoding. Defaults to None. Returns: z3.Bool or z3.Real or z3.Int: Grounded variable corresponding to fluent at timestep t """ fluent_name = get_effected_fluent_name(fluent.name, args) if r2exists_tuple is None: name = fluent_name else: # We must first find the referenced SmtFluent object fluents_list = getR2ExistsTupleValue(r2exists_tuple, "fluents_list") matching_fluent = next( (x for x in fluents_list if (x.check_name_match(fluent_name))), None ) if matching_fluent is None: print("Error. Unrecognised fluent instance:") print(fluent_name) return None name = matching_fluent.get_chained_var(r2exists_tuple) stated_name = get_fluent_name_at_state(name, t) if fluent.type.is_bool_type(): return Bool(stated_name) elif fluent.type.is_real_type(): return Real(stated_name) elif fluent.type.is_int_type(): return Int(stated_name) # Convert FNODE to Z3, this is only done when creating a new Z3 problem instance # Parameters: # FNODE: AIPLAN4EU API, which uses a directed acyclic graph to represent logical relations between fluents and other objects # t: the time or state for which the current logical relation will be created # r2exists_tuple: a tuple containing information required for relaxed relaxed ThereExists encoding. See getR2ExistsTupleValue for more information. None if relaxed relaxed ThereExists is not used
[docs]def convert_FNODE_to_Z3(FNODE, t, r2exists_tuple=None): """Convert FNODE to z3 expression equivalent Args: FNODE (FNODE): AIPLAN4EU API object used to express logical relations between fluents and other objects t (int): current timestep r2exists_tuple (Tuple, optional): Tuple used to convey information needed for converting an FNODE to z3 for relaxed relaxed ThereExists encoding. Defaults to None. Raises: Exception: Raise exception when FNODE does not match expected logical relation structure Returns: z3 expression: Equivalent expression expressued using z3 """ # Check if FNODE is null (e.g. when argument list is empty) # Generally z3 has helpful error messages, and with python's traceback this makes bugfixing simple # so error checking code has been only added where necessary if FNODE is None: return True if FNODE.is_and(): # Z3 permits empty relations, e.g. And(), so do not throw an error return And(__convert_FNODE_args(FNODE.args, t, r2exists_tuple)) elif FNODE.is_or(): return Or(__convert_FNODE_args(FNODE.args, t, r2exists_tuple)) elif FNODE.is_not(): # Z3 does not permit Not(), but throws a helpful error so no need to add error checking code here parsed_args = __convert_FNODE_args(FNODE.args, t, r2exists_tuple) if len(parsed_args) != 1: raise Exception("Unexpected number of variables for Not relation") return Not(parsed_args[0]) elif FNODE.is_implies(): # Must supply exactly two arguments parsed_args = __convert_FNODE_args(FNODE.args, t, r2exists_tuple) if len(parsed_args) != 2: raise Exception("Unexpected number of variables for implies relation") return Implies(parsed_args[0], parsed_args[1]) elif FNODE.is_iff(): # Z3 does not have an operator for iff, but can be represented this way: parsed_args = __convert_FNODE_args(FNODE.args, t, r2exists_tuple) if len(parsed_args) != 2: raise Exception("Unexpected number of variables for iff relation") return And( Implies(parsed_args[0], parsed_args[1]), Implies(parsed_args[1], parsed_args[0]), ) elif FNODE.is_exists(): # Z3 expects a very specific format for the arguments of Exists() # TODO consider more thorough error checking # This should be grounded out in pre-processing, but worth considering ensuring pre-processing is conducted # on an API based input return Exists(__convert_FNODE_args(FNODE.args, t, r2exists_tuple)) elif FNODE.is_forall(): # Z3 expects a very specific format for the arguments of Forall() # TODO consider more thorough error checking return ForAll(__convert_FNODE_args(FNODE.args, t, r2exists_tuple)) elif FNODE.is_fluent_exp(): # Base case return __create_stated_fluent_instance( FNODE.fluent(), FNODE.args, t, r2exists_tuple ) elif FNODE.is_constant(): # Base case for is_bool_constant, is_int_constant, is_real_constant # Handle this like a fluent return FNODE.constant_value() elif FNODE.is_plus(): # Assert that plus requires exactly two parameters parsed_args = __convert_FNODE_args(FNODE.args, t, r2exists_tuple) if len(parsed_args) != 2: raise Exception("Unexpected number of variables for plus relation") return parsed_args[0] + parsed_args[1] elif FNODE.is_minus(): # Assert that minus requires exactly two parameters parsed_args = __convert_FNODE_args(FNODE.args, t, r2exists_tuple) if len(parsed_args) != 2: raise Exception("Unexpected number of variables for minus relation") return parsed_args[0] - parsed_args[1] elif FNODE.is_times(): # Assert that times requires exactly two parameters parsed_args = __convert_FNODE_args(FNODE.args, t, r2exists_tuple) if len(parsed_args) != 2: raise Exception("Unexpected number of variables for times relation") return parsed_args[0] * parsed_args[1] elif FNODE.is_div(): # Assert that dividing requires exactly two parameters parsed_args = __convert_FNODE_args(FNODE.args, t, r2exists_tuple) if len(parsed_args) != 2: raise Exception("Unexpected number of variables for div relation") return parsed_args[0] / parsed_args[1] elif FNODE.is_le(): # Assert that inequality relations requires exactly two parameters parsed_args = __convert_FNODE_args(FNODE.args, t, r2exists_tuple) if len(parsed_args) != 2: raise Exception("Unexpected number of variables for le relation") return parsed_args[0] <= parsed_args[1] elif FNODE.is_lt(): # Assert that inequality relations requires exactly two parameters parsed_args = __convert_FNODE_args(FNODE.args, t, r2exists_tuple) if len(parsed_args) != 2: raise Exception("Unexpected number of variables for lt relation") return parsed_args[0] < parsed_args[1] elif FNODE.is_equals(): # Assert that equality relations requires exactly two parameters parsed_args = __convert_FNODE_args(FNODE.args, t, r2exists_tuple) if len(parsed_args) != 2: raise Exception("Unexpected number of variables for equals relation") return parsed_args[0] == parsed_args[1] else: print(FNODE.node_type()) raise Exception("FNODE type not implemented")
[docs]def convert_effect_to_Z3(effect, t, r2exists_tuple=None): """Function used to convert an Effect object into Z3 equivalent expression for a given time t Args: effect (unified-planning.Model.effect): The effect to be converted t (int): current timestep r2exists_tuple (Tuple, optional): Tuple used to convey information needed for converting an FNODE to z3 for relaxed relaxed ThereExists encoding. Defaults to None. Returns: z3 expression: An action's effect on a fluent expressed as a z3 expression at timestep t """ effect_value = effect.value # Create a tuple allowing reference to previous chained variables. Used for new value calculation, and conditions r2exists_non_effect_tuple = None if r2exists_tuple is None: # The effected fluent is in the next state, this is not guaranteed to be a simple fluent because it is an FNODE structure fluent = convert_FNODE_to_Z3(effect.fluent, (t + 1), None) # Get old value of the fluent to be incremented/decremented if effect.is_increase() or effect.is_decrease(): old_fluent = convert_FNODE_to_Z3(effect.fluent, (t), None) # The resulting value can only refer to constants, or values in the current state value = convert_FNODE_to_Z3(effect_value, (t), None) else: r2exists_non_effect_tuple = createR2ExistsTuple( r2exists_tuple[0], False, r2exists_tuple[2], r2exists_tuple[3] ) # The effected fluent is in the current state, using the assigned chained variable fluent = convert_FNODE_to_Z3(effect.fluent, (t), r2exists_tuple) # Get old value of the fluent to be incremented/decremented if effect.is_increase() or effect.is_decrease(): old_fluent = convert_FNODE_to_Z3( effect.fluent, (t), r2exists_non_effect_tuple ) # The resulting value can only refer to constants, or values in the current state value = convert_FNODE_to_Z3(effect_value, (t), r2exists_non_effect_tuple) # Build effect operation if effect.is_assignment(): operation = fluent == value elif effect.is_increase(): operation = fluent == (old_fluent + value) else: # Effect causes a decrement in value operation = fluent == (old_fluent - value) # Handle the effect's precondition if effect.is_conditional(): # The condition is in the current state condition = convert_FNODE_to_Z3( effect.condition, (t), r2exists_non_effect_tuple ) return Implies(condition, operation) else: return operation
[docs]def get_base_fluent_name(FNODE): """Function used to return a grounded fluent name, given it is the correct FNODE type Args: FNODE (FNODE): unified-planning FNODE Raises: Exception: Raise an exception if FNODE is not a fluent Returns: String: Grounded fluent name """ if FNODE.is_fluent_exp(): return get_effected_fluent_name(FNODE.fluent().name, FNODE.args) else: raise Exception("Unexpected FNODE type")
[docs]def create_fluent_condition_tuple(effect): """Function used to extract and package useful effect information before passing to a Fluent class for generating frame axiom constraints Args: effect (unified-planning.Model.effect): Effect, changing a fluent value, with optional condition Returns: Tuple: (grounded fluent basename, effect condition as FNODE expression) """ fluent_basename = get_base_fluent_name(effect.fluent) API_condition = None if effect.is_conditional(): API_condition = effect.condition return (fluent_basename, API_condition)
[docs]def search_fluents_list(fluents_list, fluent_basename): """Return fluent object corresponding to basename, returns fluent if matched, None if no match Args: fluents_list (List(BaseFluent or BaseFluent subclass)): List of fluents fluent_basename (String): Name of fluent Returns: BaseFluent or BaseFluent subclass: Fluent corresponding to the fluent basename """ for fluent in fluents_list: if fluent.check_name_match(fluent_basename): return fluent return None