Spaces:
Running
Running
File size: 5,867 Bytes
bc52096 | 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 | """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)
|