Spaces:
Sleeping
Sleeping
File size: 9,455 Bytes
dbc6675 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 | 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
|