import re import numpy as np import pddl # Special Values VAL_PAD = -99.0 # Slot is unused (outside problem size) VAL_DONTCARE = -10.0 # Goal value when variable is not specified class FSFEncoder: def __init__(self, domain_name, domain_pddl_path, max_objects): """ max_objects: The count of distinct objects in the largest problem. The actual vector size will be max_objects + 1 (for Global Slot 0). """ self.domain_name = domain_name self.domain_pddl = pddl.parse_domain(domain_pddl_path) # Vector Size = Max Objects + 1 (Index 0 is reserved for Global/Robot) self.vector_size = max_objects + 1 print( f" [FSF] Initialized with {self.vector_size} slots (Max Objects: {max_objects} + 1 Global)" ) # Regex to correctly parse "(pred arg1 arg2)" groups self.predicate_regex = re.compile(r"\(([\w-]+(?: [\w-]+)*)\)") def _get_sorted_objects(self, problem_path): """ Parses problem to get all objects, including domain constants. Returns sorted list of object names. """ problem = pddl.parse_problem(problem_path) objs = set() for o in problem.objects: objs.add(o.name) for o in self.domain_pddl.constants: objs.add(o.name) return sorted(list(objs)) def _get_object_indices(self, objects): # Index 0 is Global. Objects start at 1. return {o: i + 1 for i, o in enumerate(objects)} def parse_state_atoms(self, state_lines): """ Parses list of strings using Regex to separate predicates. Input: ['(on a b) (clear c)'] Output: [('on', 'a', 'b'), ('clear', 'c')] """ atoms = [] for line in state_lines: # Find all matches of (pred arg1 arg2 ...) matches = self.predicate_regex.findall(line) for m in matches: parts = m.split() if parts: atoms.append(tuple([p.lower() for p in parts])) return atoms def embed_trajectory(self, problem_path, trajectory_file, verbose=False): """ Reads a .traj file and returns [T, MAX_OBJECTS] matrix. """ objects = self._get_sorted_objects(problem_path) obj_map = self._get_object_indices(objects) if verbose: print(f"\nDEBUG: {problem_path}") print(f"Objects ({len(objects)}): {objects[:5]} ...") print(f"Map: {list(obj_map.items())[:5]} ...") with open(trajectory_file, "r") as f: lines = f.readlines() vectors = [] for i, line in enumerate(lines): atoms = self.parse_state_atoms([line]) # Debug first and last step is_debug_step = verbose and (i == 0 or i == len(lines) - 1) vec = self._state_to_vector(atoms, objects, obj_map, debug=is_debug_step) vectors.append(vec) return np.array(vectors, dtype=np.float32) def embed_goal(self, problem_path): """ Parses problem goal and returns [MAX_OBJECTS] vector. Unspecified variables are set to VAL_DONTCARE. """ objects = self._get_sorted_objects(problem_path) obj_map = self._get_object_indices(objects) problem = pddl.parse_problem(problem_path) # Extract goal atoms goal_atoms = [] def visit(node): if hasattr(node, "name") and hasattr(node, "terms"): # Predicate args = [t.name if hasattr(t, "name") else str(t) for t in node.terms] goal_atoms.append(tuple([node.name] + args)) elif hasattr(node, "operands"): for op in node.operands: visit(op) visit(problem.goal) # Generate vector with DONTCARE as default return self._state_to_vector(goal_atoms, objects, obj_map, is_goal=True) def _state_to_vector(self, atoms, objects, obj_map, is_goal=False, debug=False): """ Core Logic: Maps atoms to vector based on domain rules. """ # 1. Initialize # If it's a goal, default is DONTCARE. # If it's a state, default is PAD (we fill valid slots below). default_fill = VAL_DONTCARE if is_goal else VAL_PAD # Create vector of size N+1 vec = np.full(self.vector_size, default_fill, dtype=np.float32) # Initialize Valid Slots to 0.0 for states if not is_goal: # Global Slot (0) defaults to 0 vec[0] = 0.0 # Object Slots (1..N) default to 0.0 (e.g. Table/Unvisited/Free) for i in range(len(objects)): slot = i + 1 if slot < self.vector_size: vec[slot] = 0.0 # Helper to get value (index) of an object def get_val(name): return float(obj_map.get(name, 0)) # Helper to get slot (index) of an object def get_slot(name): s = obj_map.get(name, -1) if s >= self.vector_size: # This should theoretically not happen if we scanned correctly return -1 return s if debug: print(f"Processing Atoms: {atoms}") # DOMAIN SPECIFIC LOGIC if "blocks" in self.domain_name: # Slot i = Block i # Values: 0 (Table), -1 (Held), k (On block k) # First pass: Set held for pred in atoms: name = pred[0] args = pred[1:] if name == "holding": # (holding a) -> V[a] = -1 slot = get_slot(args[0]) if slot != -1: vec[slot] = -1.0 if debug: print(f" Set {args[0]} (slot {slot}) = -1.0 (Held)") elif name == "on": # (on a b) -> V[a] = Index(b) slot = get_slot(args[0]) if slot != -1: vec[slot] = get_val(args[1]) if debug: print( f" Set {args[0]} (slot {slot}) = {get_val(args[1])} (On {args[1]})" ) elif name == "ontable": # (ontable a) -> V[a] = 0 slot = get_slot(args[0]) if slot != -1: vec[slot] = 0.0 elif "gripper" in self.domain_name: # Slot 0: Robot Location (Room Index) # Slot i (Ball): Room Index OR -1 * Gripper Index # Slot i (Gripper): 0 (Free) or Ball Index (Holding) for pred in atoms: name = pred[0] args = pred[1:] if name == "at-robby": vec[0] = get_val(args[0]) if debug: print( f" Set Global (slot 0) = {get_val(args[0])} (Robby at {args[0]})" ) elif name == "at": # (at ball room) -> V[ball] = Index(room) slot = get_slot(args[0]) if slot != -1: vec[slot] = get_val(args[1]) if debug: print( f" Set {args[0]} (slot {slot}) = {get_val(args[1])} (At {args[1]})" ) elif name == "carry": # (carry ball gripper) ball, gripper = args b_slot = get_slot(ball) g_slot = get_slot(gripper) if b_slot != -1: vec[b_slot] = -1.0 * get_val(gripper) if debug: print( f" Set {ball} (slot {b_slot}) = {-1.0 * get_val(gripper)} (Carried)" ) if g_slot != -1: vec[g_slot] = get_val(ball) if debug: print( f" Set {gripper} (slot {g_slot}) = {get_val(ball)} (Holding)" ) elif "logistics" in self.domain_name: # Slot i (Pkg/Truck/Plane): Location Index # Slot i (Pkg in vehicle): -1 * Vehicle Index for pred in atoms: name = pred[0] args = pred[1:] if name == "at": # (at obj loc) slot = get_slot(args[0]) if slot != -1: vec[slot] = get_val(args[1]) if debug: print( f" Set {args[0]} (slot {slot}) = {get_val(args[1])} (At {args[1]})" ) elif name == "in": # (in pkg vehicle) pkg, veh = args slot = get_slot(pkg) if slot != -1: vec[slot] = -1.0 * get_val(veh) if debug: print( f" Set {pkg} (slot {slot}) = {-1.0 * get_val(veh)} (In {veh})" ) elif "visit" in self.domain_name: # visitall # Slot 0: Robot Location (Cell Index) # Slot i (Cell): 0 (Unvisited), 1 (Visited) for pred in atoms: name = pred[0] args = pred[1:] if name == "at-robot": vec[0] = get_val(args[0]) if debug: print( f" Set Global (slot 0) = {get_val(args[0])} (Robot at {args[0]})" ) elif name == "visited": # (visited cell) -> V[cell] = 1 slot = get_slot(args[0]) if slot != -1: vec[slot] = 1.0 if debug: print(f" Set {args[0]} (slot {slot}) = 1.0 (Visited)") return vec