Spaces:
Sleeping
Sleeping
| import glob | |
| import hashlib | |
| import os | |
| import re | |
| import numpy as np | |
| import pddl | |
| import pddl.logic.base | |
| import pddl.logic.predicates | |
| from tqdm import tqdm | |
| # Regex to parse "(on a b)" -> "on a b" | |
| PREDICATE_REGEX = re.compile(r"\(([\w-]+(?: [\w-]+)*)\)") | |
| class WLEncoder: | |
| def __init__(self, domain_pddl_path, iterations=2): | |
| self.domain_pddl_path = domain_pddl_path | |
| self.iterations = iterations | |
| # Vocabulary: Map specific WL hash strings to integer indices | |
| self.vocab = {} | |
| self.is_collected = False | |
| # Parse domain using the 'pddl' library (robust) | |
| print(f" [GC-WL] Parsing Domain: {domain_pddl_path}") | |
| self.pddl_domain = pddl.parse_domain(domain_pddl_path) | |
| # Cache domain predicates to know arity (unary vs binary) | |
| # p.name is string, p.arity is int | |
| self.domain_info = { | |
| p.name.lower(): p.arity for p in self.pddl_domain.predicates | |
| } | |
| def _get_initial_graph(self, objects, state_atoms, goal_atoms): | |
| """ | |
| Builds a graph where nodes = objects. | |
| Features include current state AND goal state info. | |
| """ | |
| # 1. Initialize Nodes | |
| # Graph structure: {obj_name: {'attributes': [], 'neighbors': []}} | |
| graph = {obj: {"attributes": [], "neighbors": []} for obj in objects} | |
| # 2. Process State Atoms | |
| for atom in state_atoms: | |
| parts = atom.replace("(", "").replace(")", "").lower().split() | |
| if not parts: | |
| continue | |
| pred = parts[0] | |
| args = parts[1:] | |
| if pred not in self.domain_info: | |
| continue # Skip unknown predicates (e.g. equality) | |
| arity = self.domain_info[pred] | |
| if arity == 1 and len(args) == 1: | |
| # Unary State Feature: clear(a) -> a has attr "state-clear" | |
| if args[0] in graph: | |
| graph[args[0]]["attributes"].append(f"state-{pred}") | |
| elif arity == 2 and len(args) == 2: | |
| # Binary State Edge: on(a, b) -> a -[state-on]-> b | |
| u, v = args | |
| if u in graph and v in graph: | |
| graph[u]["neighbors"].append((f"state-{pred}", v)) | |
| # We treat edges as directed. For WL, we can add inverse if needed, | |
| # but standard directed WL is usually fine for planning. | |
| # 3. Process Goal Atoms (The "Goal-Aware" part) | |
| for atom in goal_atoms: | |
| parts = atom.replace("(", "").replace(")", "").lower().split() | |
| if not parts: | |
| continue | |
| pred = parts[0] | |
| args = parts[1:] | |
| if pred not in self.domain_info: | |
| continue | |
| arity = self.domain_info[pred] | |
| if arity == 1 and len(args) == 1: | |
| # Unary Goal Feature: goal-clear(a) | |
| if args[0] in graph: | |
| graph[args[0]]["attributes"].append(f"goal-{pred}") | |
| elif arity == 2 and len(args) == 2: | |
| # Binary Goal Edge: goal-on(a, b) | |
| u, v = args | |
| if u in graph and v in graph: | |
| graph[u]["neighbors"].append((f"goal-{pred}", v)) | |
| # 4. Sort attributes for determinism | |
| for obj in graph: | |
| graph[obj]["attributes"].sort() | |
| graph[obj]["neighbors"].sort() | |
| return graph | |
| def _compute_wl_hashes(self, graph): | |
| """ | |
| Runs k-iterations of Weisfeiler-Leman. | |
| Returns a list of all colors found in the final graph. | |
| """ | |
| # Initial Coloring: Hash of attributes | |
| # current_colors: {obj_name: hash_string} | |
| current_colors = {} | |
| for obj, data in graph.items(): | |
| # Hash the sorted list of attributes | |
| attr_str = "|".join(data["attributes"]) | |
| current_colors[obj] = hashlib.md5(attr_str.encode()).hexdigest() | |
| # Iterations | |
| for _ in range(self.iterations): | |
| new_colors = {} | |
| for obj in graph: | |
| # Collect neighbor colors | |
| # neighbor_desc = list of (edge_label, neighbor_color) | |
| neighbors = graph[obj]["neighbors"] | |
| neighbor_descriptors = [] | |
| for label, neighbor in neighbors: | |
| neighbor_descriptors.append(f"{label}:{current_colors[neighbor]}") | |
| # Sort to ensure invariance to neighbor order | |
| neighbor_descriptors.sort() | |
| # Aggregate: (SelfColor, Neighbors) | |
| aggregate_str = ( | |
| current_colors[obj] + "||" + ",".join(neighbor_descriptors) | |
| ) | |
| new_hash = hashlib.md5(aggregate_str.encode()).hexdigest() | |
| new_colors[obj] = new_hash | |
| current_colors = new_colors | |
| # Return all node colors (multiset) | |
| return list(current_colors.values()) | |
| def parse_state_string_to_atoms(self, state_str_or_list): | |
| if isinstance(state_str_or_list, str): | |
| return PREDICATE_REGEX.findall(state_str_or_list) | |
| return state_str_or_list | |
| def parse_pddl_goal(self, problem_path): | |
| """Extracts goal atoms and objects from PDDL using the 'pddl' library.""" | |
| # Parse problem | |
| problem = pddl.parse_problem(problem_path) | |
| # Collect objects (problem objects + domain constants) | |
| objects = set() | |
| for o in problem.objects: | |
| objects.add(o.name) | |
| for o in self.pddl_domain.constants: | |
| objects.add(o.name) | |
| # Extract Goal Atoms recursively | |
| goals = [] | |
| def visit(node): | |
| if isinstance(node, pddl.logic.predicates.Predicate): | |
| # node.name is predicate name, node.terms are arguments | |
| # Handle terms that might be objects or just strings | |
| args = [t.name if hasattr(t, "name") else str(t) for t in node.terms] | |
| s = f"({node.name} {' '.join(args)})" | |
| goals.append(s) | |
| elif hasattr(node, "operands"): # Handle And, Or, etc. | |
| for op in node.operands: | |
| visit(op) | |
| elif hasattr(node, "_operands"): # Fallback for older pddl versions | |
| for op in node._operands: | |
| visit(op) | |
| # Note: We ignore 'Not' for graph edges usually, or can be handled it if needed. | |
| visit(problem.goal) | |
| return sorted(list(objects)), goals | |
| def collect_vocabulary(self, train_states_dir): | |
| print(f" [GC-WL] Collecting vocabulary from {train_states_dir}...") | |
| self.vocab = {} | |
| unique_hashes = set() | |
| train_files = sorted(glob.glob(os.path.join(train_states_dir, "*.traj"))) | |
| pddl_train_dir = train_states_dir.replace("states", "pddl") | |
| for traj_file in tqdm(train_files, desc=" Parsing Traces"): | |
| prob_name = os.path.splitext(os.path.basename(traj_file))[0] | |
| prob_pddl = os.path.join(pddl_train_dir, f"{prob_name}.pddl") | |
| if not os.path.exists(prob_pddl): | |
| continue | |
| try: | |
| # 1. Get Objects and Goal | |
| objects, goal_atoms = self.parse_pddl_goal(prob_pddl) | |
| # 2. Read Trajectory | |
| with open(traj_file, "r") as f: | |
| lines = f.read().strip().split("\n") | |
| # 3. Process states | |
| for line in lines: | |
| state_atoms = self.parse_state_string_to_atoms(line) | |
| graph = self._get_initial_graph(objects, state_atoms, goal_atoms) | |
| colors = self._compute_wl_hashes(graph) | |
| unique_hashes.update(colors) | |
| except Exception: | |
| # print(f"Error reading {prob_name}: {e}") | |
| pass | |
| # Build Vocab Map | |
| sorted_hashes = sorted(list(unique_hashes)) | |
| self.vocab = {h: i for i, h in enumerate(sorted_hashes)} | |
| self.is_collected = True | |
| print(f" [GC-WL] Vocabulary collected. Size: {len(self.vocab)}") | |
| def embed_state(self, state_atoms_or_obj, problem_pddl_path): | |
| if not self.is_collected: | |
| raise RuntimeError("Vocab not collected") | |
| # Handle input types (if legacy code passes State objects) | |
| if hasattr(state_atoms_or_obj, "atoms"): | |
| # Extract string representation from wlplan State object if passed | |
| state_atoms = [] | |
| for atom in state_atoms_or_obj.atoms: | |
| args = " ".join(atom.objects) | |
| state_atoms.append(f"({atom.predicate.name} {args})") | |
| else: | |
| state_atoms = self.parse_state_string_to_atoms(state_atoms_or_obj) | |
| # We need objects and goals again | |
| objects, goal_atoms = self.parse_pddl_goal(problem_pddl_path) | |
| # Build Graph | |
| graph = self._get_initial_graph(objects, state_atoms, goal_atoms) | |
| # Run WL | |
| colors = self._compute_wl_hashes(graph) | |
| # Vectorize (Histogram) | |
| vec = np.zeros(len(self.vocab), dtype=np.float32) | |
| for c in colors: | |
| if c in self.vocab: | |
| vec[self.vocab[c]] += 1.0 | |
| return vec | |
| # Adapter methods to match existing interface | |
| def parse_state_string_to_wl_state(self, s): | |
| return s | |
| def parse_pddl_goal_to_wl_state(self, p): | |
| _, goals = self.parse_pddl_goal(p) | |
| return goals | |