import argparse
import json
import random
import sys
from collections import Counter, defaultdict
from pathlib import Path
ACTIONS = ["Up", "Down", "Left", "Right"]
DEFAULT_OUTPUT = "docs/experiments/gridworld-prolepsis/gridworld_instances.json"
def dominant_action(agent, goal):
dx = goal[0] - agent[0]
dy = goal[1] - agent[1]
if abs(dx) > abs(dy):
return "Right" if dx > 0 else "Left"
if abs(dy) > abs(dx):
return "Up" if dy > 0 else "Down"
return None
def build_pool(grid_size):
by_action = defaultdict(list)
for ax in range(grid_size):
for ay in range(grid_size):
for gx in range(grid_size):
for gy in range(grid_size):
agent = (ax, ay)
goal = (gx, gy)
action = dominant_action(agent, goal)
if action is not None:
by_action[action].append((agent, goal))
return by_action
def balanced_sample(by_action, num_instances, rng):
base, remainder = divmod(num_instances, len(ACTIONS))
chosen = []
for i, action in enumerate(ACTIONS):
want = base + (1 if i < remainder else 0)
pool = by_action.get(action, [])
if want > len(pool):
raise ValueError(
f"action {action!r} has only {len(pool)} unique-dominant "
f"instances but {want} were requested (raise --grid-size or "
f"lower --num-instances)"
)
for agent, goal in rng.sample(pool, want):
chosen.append((agent, goal, action))
rng.shuffle(chosen)
return chosen
def unbalanced_sample(by_action, num_instances, rng):
flat = [
(agent, goal, action)
for action, pairs in by_action.items()
for agent, goal in pairs
]
if num_instances > len(flat):
raise ValueError(
f"requested {num_instances} instances but only {len(flat)} "
f"unique-dominant pairs exist on a {len(by_action)}-action grid"
)
return rng.sample(flat, num_instances)
def main():
sys.stdout.reconfigure(encoding="utf-8")
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
parser.add_argument("--grid-size", type=int, default=5)
parser.add_argument("--num-instances", type=int, default=100)
parser.add_argument("--seed", type=int, default=0)
parser.add_argument("--output", type=Path, default=Path(DEFAULT_OUTPUT))
parser.add_argument(
"--balanced",
action=argparse.BooleanOptionalAction,
default=True,
help="equal instances per cardinal action (default: balanced)",
)
args = parser.parse_args()
if args.grid_size < 2:
parser.error("--grid-size must be >= 2")
if args.num_instances < 1:
parser.error("--num-instances must be >= 1")
rng = random.Random(args.seed)
by_action = build_pool(args.grid_size)
if args.balanced:
chosen = balanced_sample(by_action, args.num_instances, rng)
else:
chosen = unbalanced_sample(by_action, args.num_instances, rng)
instances = [
{
"agent": [agent[0], agent[1]],
"goal": [goal[0], goal[1]],
"correct_action": action,
"instance_id": instance_id,
}
for instance_id, (agent, goal, action) in enumerate(chosen)
]
args.output.parent.mkdir(parents=True, exist_ok=True)
with open(args.output, "w", encoding="utf-8") as f:
json.dump(instances, f, indent=2)
f.write("\n")
counts = Counter(inst["correct_action"] for inst in instances)
summary = ", ".join(f"{a}={counts.get(a, 0)}" for a in ACTIONS)
print(
f"Wrote {len(instances)} instances "
f"(grid {args.grid_size}x{args.grid_size}, seed {args.seed}, "
f"{'balanced' if args.balanced else 'unbalanced'}) to {args.output}",
file=sys.stderr,
)
print(f"Per-action counts: {summary}", file=sys.stderr)
if __name__ == "__main__":
main()