Spaces:
Running
Running
| """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 (degrees) | |
| 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. | |
| Finds all triangles formed by the edge connectivity. | |
| """ | |
| from collections import defaultdict | |
| n_verts = len(vertices) | |
| 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) | |