sissississi's picture
Add RL training environment with OpenEnv backend
bc52096
"""FOLD JSON parsing and validation.
Validates LLM-generated FOLD crease patterns before simulation.
FOLD spec: https://github.com/edemaine/fold
"""
from typing import Any
import numpy as np
def validate_fold(fold_data: dict[str, Any]) -> tuple[bool, str]:
"""Validate a FOLD JSON object. Returns (is_valid, error_message)."""
# Required fields
for key in ("vertices_coords", "edges_vertices", "edges_assignment"):
if key not in fold_data:
return False, f"Missing required field: {key}"
verts = fold_data["vertices_coords"]
edges = fold_data["edges_vertices"]
assignments = fold_data["edges_assignment"]
# Must have at least 3 vertices (a triangle)
if len(verts) < 3:
return False, f"Need at least 3 vertices, got {len(verts)}"
# Must have at least 3 edges
if len(edges) < 3:
return False, f"Need at least 3 edges, got {len(edges)}"
# Edges and assignments must match length
if len(edges) != len(assignments):
return False, (
f"edges_vertices ({len(edges)}) and "
f"edges_assignment ({len(assignments)}) must match length"
)
# Fold angles must match if present
if "edges_foldAngle" in fold_data:
angles = fold_data["edges_foldAngle"]
if len(angles) != len(edges):
return False, (
f"edges_foldAngle ({len(angles)}) must match "
f"edges_vertices ({len(edges)})"
)
# Validate vertex coordinates (2D or 3D)
num_verts = len(verts)
for i, v in enumerate(verts):
if not isinstance(v, (list, tuple)) or len(v) < 2:
return False, f"Vertex {i} must be [x, y] or [x, y, z], got {v}"
# Validate edge indices
for i, e in enumerate(edges):
if not isinstance(e, (list, tuple)) or len(e) != 2:
return False, f"Edge {i} must be [v1, v2], got {e}"
v1, v2 = e
if v1 < 0 or v1 >= num_verts or v2 < 0 or v2 >= num_verts:
return False, f"Edge {i} references invalid vertex: {e}"
if v1 == v2:
return False, f"Edge {i} is degenerate (same vertex): {e}"
# Validate assignments
valid_assignments = {"M", "V", "B", "F", "U", "C"}
for i, a in enumerate(assignments):
if a not in valid_assignments:
return False, f"Edge {i} has invalid assignment '{a}'"
# Must have at least one fold crease (M or V)
has_fold = any(a in ("M", "V") for a in assignments)
if not has_fold:
return False, "No fold creases (M or V) found"
# Must have boundary edges
has_boundary = any(a == "B" for a in assignments)
if not has_boundary:
return False, "No boundary edges (B) found"
return True, ""
def parse_fold(fold_data: dict[str, Any]) -> dict[str, np.ndarray]:
"""Parse validated FOLD JSON into numpy arrays for simulation.
Returns dict with:
vertices: (N, 3) float64 — vertex positions (z=0 for 2D input)
edges: (E, 2) int — edge vertex indices
assignments: list[str] — edge type per edge
fold_angles: (E,) float64 — target fold angle per edge (radians)
faces: (F, 3) int — triangulated face vertex indices
"""
verts = fold_data["vertices_coords"]
# Ensure 3D (add z=0 if 2D)
vertices = np.zeros((len(verts), 3), dtype=np.float64)
for i, v in enumerate(verts):
vertices[i, 0] = v[0]
vertices[i, 1] = v[1]
if len(v) > 2:
vertices[i, 2] = v[2]
edges = np.array(fold_data["edges_vertices"], dtype=np.int32)
assignments = list(fold_data["edges_assignment"])
# Fold angles: default based on assignment if not provided
if "edges_foldAngle" in fold_data:
fold_angles = np.array(fold_data["edges_foldAngle"], dtype=np.float64)
else:
fold_angles = np.zeros(len(edges), dtype=np.float64)
for i, a in enumerate(assignments):
if a == "V":
fold_angles[i] = 180.0
elif a == "M":
fold_angles[i] = -180.0
# Convert degrees to radians for simulation
fold_angles_rad = np.radians(fold_angles)
# Triangulate faces
if "faces_vertices" in fold_data:
raw_faces = fold_data["faces_vertices"]
faces = _triangulate_faces(raw_faces)
else:
faces = _compute_faces(vertices, edges)
return {
"vertices": vertices,
"edges": edges,
"assignments": assignments,
"fold_angles": fold_angles_rad,
"faces": faces,
}
def _triangulate_faces(raw_faces: list[list[int]]) -> np.ndarray:
"""Fan-triangulate polygon faces into triangles."""
triangles = []
for face in raw_faces:
if len(face) < 3:
continue
for i in range(1, len(face) - 1):
triangles.append([face[0], face[i], face[i + 1]])
if not triangles:
return np.zeros((0, 3), dtype=np.int32)
return np.array(triangles, dtype=np.int32)
def _compute_faces(vertices: np.ndarray, edges: np.ndarray) -> np.ndarray:
"""Compute triangulated faces from vertices and edges using adjacency."""
from collections import defaultdict
adj = defaultdict(set)
for v1, v2 in edges:
adj[v1].add(v2)
adj[v2].add(v1)
triangles = set()
for v1, v2 in edges:
common = adj[v1] & adj[v2]
for v3 in common:
tri = tuple(sorted([v1, v2, v3]))
triangles.add(tri)
if not triangles:
# Fallback: create faces using Delaunay on 2D projection
from scipy.spatial import Delaunay
pts_2d = vertices[:, :2]
try:
tri = Delaunay(pts_2d)
return tri.simplices.astype(np.int32)
except Exception:
return np.zeros((0, 3), dtype=np.int32)
return np.array(list(triangles), dtype=np.int32)