vishalp's picture
Deploy State-Centric Learning live demo
dbc6675 verified
Raw
History Blame Contribute Delete
8.19 kB
"""
WL (Weisfeiler-Leman) tokenizer wrapping the existing wlplan-based pipeline.
This does NOT reimplement WL — it delegates to the wlplan library that the
codebase already uses in generate_graph_embeddings.py.
"""
import logging
import os
import re
import sys
import numpy as np
from tqdm import tqdm
from wlplan.data import DomainDataset, ProblemDataset
from wlplan.feature_generator import init_feature_generator, load_feature_generator
from wlplan.planning import Atom, State, parse_domain, parse_problem
from code.tokenization.base import TokenizationStrategy
logger = logging.getLogger(__name__)
# Regex to parse "(on a b)" -> "on a b"
_PREDICATE_REGEX = re.compile(r"\(([\w-]+(?: [\w-]+)*)\)")
def _progress_enabled() -> bool:
return bool(sys.stdout.isatty())
def _parse_traj_line_to_state(line: str, pred_map: dict) -> State:
"""Parse a line like '(on a b) (clear c)' into a wlplan State object."""
line = line.strip()
if not line:
return State([])
matches = re.findall(r"\(([\w-]+(?: [\w-]+)*)\)", line)
atoms = []
for m in matches:
parts = m.split()
pred_name = parts[0]
objs = parts[1:]
if pred_name in pred_map:
atoms.append(Atom(pred_map[pred_name], objs))
return State(atoms)
def _atoms_strings_to_wl_atoms(atom_strings: list[str], pred_map: dict) -> list[Atom]:
"""Convert list of atom strings like '(on a b)' to wlplan Atom objects."""
atoms = []
for a_str in atom_strings:
content = a_str.replace("(", "").replace(")", "")
parts = content.split()
if not parts:
continue
p_name = parts[0]
p_args = parts[1:]
if p_name in pred_map:
atoms.append(Atom(pred_map[p_name], p_args))
return atoms
class WLTokenizer(TokenizationStrategy):
"""
Weisfeiler-Leman color refinement tokenization.
Wraps the existing wlplan library to provide a consistent interface
with other tokenization strategies. The fit/transform cycle mirrors
what generate_graph_embeddings.py does.
"""
def __init__(self, iterations: int = 2):
super().__init__(name="WL")
self.iterations = iterations
self._feature_gen = None
self._wl_domain = None
self._pred_map: dict | None = None
self._domain_pddl_path: str | None = None
def fit(
self,
domain_pddl_path: str,
train_states_dir: str,
train_pddl_dir: str,
) -> None:
"""
Build WL vocabulary from training trajectories.
Mirrors the vocabulary collection in generate_graph_embeddings.py:
parse domain → init feature generator → collect from training data.
"""
self._domain_pddl_path = domain_pddl_path
# 1. Parse domain
self._wl_domain = parse_domain(domain_pddl_path)
self._pred_map = {p.name: p for p in self._wl_domain.predicates}
# 2. Initialize feature generator (ILG + WL)
self._feature_gen = init_feature_generator(
feature_algorithm="wl",
domain=self._wl_domain,
graph_representation="ilg",
iterations=self.iterations,
pruning="none",
multiset_hash=True,
)
# 3. Load training data
train_files = sorted(
[f for f in os.listdir(train_states_dir) if f.endswith(".traj")]
)
wl_problems = []
for t_file in tqdm(
train_files,
desc=f" [{self.name}] Parsing train",
disable=(not _progress_enabled()),
):
prob_name = t_file.replace(".traj", "")
prob_pddl = os.path.join(train_pddl_dir, f"{prob_name}.pddl")
traj_path = os.path.join(train_states_dir, t_file)
if not os.path.exists(prob_pddl):
continue
try:
wl_prob = parse_problem(domain_pddl_path, prob_pddl)
with open(traj_path, "r") as f:
lines = f.readlines()
states = [
_parse_traj_line_to_state(line, self._pred_map) for line in lines
]
wl_problems.append(ProblemDataset(wl_prob, states))
except Exception:
continue
if not wl_problems:
raise RuntimeError("No valid training data found for WL vocabulary.")
# 4. Collect vocabulary
full_train_ds = DomainDataset(self._wl_domain, wl_problems)
self._feature_gen.collect(full_train_ds)
self.embedding_dim = self._feature_gen.get_n_features()
self._is_fitted = True
logger.info(f"[{self.name}] Vocabulary size: {self.embedding_dim}")
def transform_state(
self,
state_atoms: list[str],
goal_atoms: list[str],
objects: list[str],
*,
problem_pddl_path: str | None = None,
_wl_prob=None,
) -> np.ndarray:
"""
Embed a single state using the WL feature generator.
Args:
state_atoms: List of atom strings for the current state.
goal_atoms: Not used directly (wlplan reads goal from problem PDDL).
objects: Not used directly (wlplan reads objects from problem PDDL).
problem_pddl_path: Path to problem PDDL (required for wlplan).
_wl_prob: Pre-parsed wlplan problem object (optimization to avoid re-parsing).
"""
self._check_fitted()
if _wl_prob is None:
if problem_pddl_path is None:
raise ValueError("WLTokenizer requires problem_pddl_path for transform.")
_wl_prob = parse_problem(self._domain_pddl_path, problem_pddl_path)
wl_atoms = _atoms_strings_to_wl_atoms(state_atoms, self._pred_map)
state = State(wl_atoms)
ds = DomainDataset(self._wl_domain, [ProblemDataset(_wl_prob, [state])])
embs = self._feature_gen.embed(ds)
return np.array(embs[0], dtype=np.float32)
def transform_goal(
self,
goal_atoms: list[str],
objects: list[str],
*,
problem_pddl_path: str | None = None,
_wl_prob=None,
) -> np.ndarray:
"""
Embed a goal state. Creates a wlplan State from goal atoms and embeds it.
"""
self._check_fitted()
if _wl_prob is None:
if problem_pddl_path is None:
raise ValueError("WLTokenizer requires problem_pddl_path for transform_goal.")
_wl_prob = parse_problem(self._domain_pddl_path, problem_pddl_path)
wl_atoms = _atoms_strings_to_wl_atoms(goal_atoms, self._pred_map)
goal_state = State(wl_atoms)
ds = DomainDataset(self._wl_domain, [ProblemDataset(_wl_prob, [goal_state])])
embs = self._feature_gen.embed(ds)
return np.array(embs[0], dtype=np.float32)
def get_embedding_dim(self) -> int:
self._check_fitted()
return self.embedding_dim
def save_vocabulary(self, filepath: str) -> None:
"""Save the wlplan feature generator to JSON."""
self._check_fitted()
self._feature_gen.save(filepath)
logger.info(f"[{self.name}] Saved vocabulary to {filepath}")
def load_vocabulary(self, filepath: str) -> None:
"""Load a previously saved wlplan feature generator."""
self._feature_gen = load_feature_generator(filepath)
self.embedding_dim = self._feature_gen.get_n_features()
self._is_fitted = True
logger.info(
f"[{self.name}] Loaded vocabulary from {filepath}, "
f"dim={self.embedding_dim}"
)
def set_domain(self, domain_pddl_path: str) -> None:
"""Set domain info needed for transform calls after load_vocabulary."""
self._domain_pddl_path = domain_pddl_path
self._wl_domain = parse_domain(domain_pddl_path)
self._pred_map = {p.name: p for p in self._wl_domain.predicates}