Spaces:
Running
Running
| """ | |
| Quality metrics for folded origami. | |
| Computes bounding box, deployment ratio, fold count, and aggregated | |
| metric dictionaries for the trainer reward functions. | |
| """ | |
| from __future__ import annotations | |
| import numpy as np | |
| from .paper import Paper | |
| def compute_bounding_box(paper: Paper) -> np.ndarray: | |
| """Axis-aligned bounding-box dimensions (dx, dy, dz). | |
| Returns shape (3,) array. Minimum z-thickness accounts for | |
| material thickness times estimated layer count. | |
| """ | |
| if len(paper.vertices) == 0: | |
| return np.zeros(3) | |
| ptp = np.ptp(paper.vertices, axis=0) | |
| ptp = np.where(np.abs(ptp) < 1e-12, 0.0, ptp) | |
| # Minimum z from material thickness * layers | |
| t = paper.material.thickness_mm / 1000.0 | |
| ptp[2] = max(ptp[2], t * paper.num_layers) | |
| return ptp | |
| def compute_deployment_ratio(paper: Paper) -> float: | |
| """Ratio of folded XY footprint area to original sheet area. | |
| A fully flat unfolded sheet has ratio 1.0; a tightly folded sheet | |
| approaches 0.0. | |
| """ | |
| if paper.original_area <= 0: | |
| return 1.0 | |
| bb = compute_bounding_box(paper) | |
| folded_area = bb[0] * bb[1] | |
| ratio = folded_area / paper.original_area | |
| return float(np.clip(ratio, 0.0, 1.0)) | |
| def compute_fold_count(paper: Paper) -> int: | |
| """Number of mountain (M) and valley (V) edges.""" | |
| return sum(1 for a in paper.assignments if a in ("M", "V")) | |
| def compute_compactness(paper: Paper) -> float: | |
| """1 - deployment_ratio. Higher is more compact.""" | |
| return 1.0 - compute_deployment_ratio(paper) | |
| def compute_volume(paper: Paper) -> float: | |
| """Bounding-box volume in cubic meters.""" | |
| bb = compute_bounding_box(paper) | |
| return float(bb[0] * bb[1] * bb[2]) | |
| def compute_metrics(paper: Paper, original_paper: Paper | None = None) -> dict: | |
| """Compute all quality metrics and return as a dict. | |
| Parameters | |
| ---------- | |
| paper : Paper | |
| The current (folded) paper state. | |
| original_paper : Paper or None | |
| The original (unfolded) paper, used for strain comparison. | |
| If None, strain is computed against the current paper's rest lengths. | |
| Returns | |
| ------- | |
| dict with keys: | |
| bounding_box, deployment_ratio, fold_count, compactness, | |
| volume, max_strain, mean_strain, num_vertices, num_faces, | |
| num_layers. | |
| """ | |
| from .physics import compute_strain # local import to avoid circular | |
| bb = compute_bounding_box(paper) | |
| strain = compute_strain(paper) | |
| return { | |
| "bounding_box": { | |
| "x": float(bb[0]), | |
| "y": float(bb[1]), | |
| "z": float(bb[2]), | |
| }, | |
| "deployment_ratio": compute_deployment_ratio(paper), | |
| "fold_count": compute_fold_count(paper), | |
| "compactness": compute_compactness(paper), | |
| "volume": compute_volume(paper), | |
| "max_strain": float(np.max(strain)) if len(strain) > 0 else 0.0, | |
| "mean_strain": float(np.mean(strain)) if len(strain) > 0 else 0.0, | |
| "num_vertices": len(paper.vertices), | |
| "num_faces": len(paper.faces), | |
| "num_layers": paper.num_layers, | |
| } | |
| def compute_all_metrics(paper, task: dict, validation: dict) -> dict: | |
| """Compute every metric and return a flat dict. | |
| Called after physics + validation. Combines validity, compactness, | |
| structural, efficiency, and deployability metrics. | |
| Parameters | |
| ---------- | |
| paper : Paper | |
| Current paper state (after simulate()). | |
| task : dict | |
| Task definition with keys: width, height, target_ratio, target_box, must_deploy. | |
| validation : dict | |
| Output of validate_state(paper). | |
| """ | |
| import numpy as np | |
| bb = paper.bounding_box # (3,) array | |
| original_area = paper.original_area if paper.original_area > 0 else (paper.material.thickness_mm / 1000.0) | |
| t = paper.material.thickness_mm / 1000.0 | |
| original_bbox_vol = original_area * t | |
| folded_bbox_vol = float(bb[0] * bb[1] * bb[2]) if bb[2] > 0 else float(bb[0] * bb[1] * t) | |
| # ββ Folded area (XY footprint) ββββββββββββββββββββββββββββββββ | |
| if len(paper.vertices) >= 3: | |
| try: | |
| from scipy.spatial import ConvexHull | |
| hull = ConvexHull(paper.vertices[:, :2]) | |
| folded_area = float(hull.volume) | |
| except Exception: | |
| ptp = np.ptp(paper.vertices[:, :2], axis=0) | |
| folded_area = float(ptp[0] * ptp[1]) | |
| else: | |
| folded_area = original_area | |
| deployment_ratio = folded_area / original_area if original_area > 0 else 1.0 | |
| compactness = 1.0 - deployment_ratio | |
| volume_compaction = folded_bbox_vol / original_bbox_vol if original_bbox_vol > 0 else 1.0 | |
| material_volume = original_area * t | |
| packing_efficiency = material_volume / folded_bbox_vol if folded_bbox_vol > 0 else 0.0 | |
| # ββ Target box check βββββββββββββββββββββββββββββββββββββββββ | |
| target_box = task.get("target_box") | |
| fits_target_box = False | |
| if target_box and len(target_box) == 3: | |
| fits_target_box = bool( | |
| bb[0] <= target_box[0] + 1e-6 and | |
| bb[1] <= target_box[1] + 1e-6 and | |
| bb[2] <= target_box[2] + 1e-6 | |
| ) | |
| # ββ Strain βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| strain = paper.strain_per_vertex | |
| max_strain = float(np.max(strain)) if len(strain) > 0 else 0.0 | |
| mean_strain = float(np.mean(strain)) if len(strain) > 0 else 0.0 | |
| # ββ Energy βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| energy = paper.energy | |
| # ββ Efficiency βββββββββββββββββββββββββββββββββββββββββββββββ | |
| fold_count = paper.fold_count | |
| # Crease complexity: entropy of M/V assignment distribution | |
| mv_assignments = [a for a in paper.assignments if a in ("M", "V")] | |
| if mv_assignments: | |
| total = len(mv_assignments) | |
| m_count = mv_assignments.count("M") | |
| v_count = mv_assignments.count("V") | |
| p_m = m_count / total if total > 0 else 0 | |
| p_v = v_count / total if total > 0 else 0 | |
| crease_complexity = 0.0 | |
| if p_m > 0: | |
| crease_complexity -= p_m * np.log2(p_m) | |
| if p_v > 0: | |
| crease_complexity -= p_v * np.log2(p_v) | |
| else: | |
| crease_complexity = 0.0 | |
| folding_efficiency = compactness / max(fold_count, 1) | |
| # ββ Deployability βββββββββββββββββββββββββββββββββββββββββββββ | |
| must_deploy = task.get("must_deploy", False) | |
| # Simple deployability heuristic: if valid and compactness > 0, assume deployable | |
| is_deployable = bool(validation.get("is_valid", False) and compactness > 0.01) if must_deploy else None | |
| # Deployment force estimate from total energy gradient (rough) | |
| deployment_force_estimate = float(energy.get("fold", 0.0)) / max(paper.original_area, 1e-6) | |
| return { | |
| # Validity (from validation dict) | |
| "is_valid": validation.get("is_valid", False), | |
| "kawasaki_violations": validation.get("kawasaki_violations", 0), | |
| "kawasaki_total_error": validation.get("kawasaki_total_error", 0.0), | |
| "maekawa_violations": validation.get("maekawa_violations", 0), | |
| "self_intersections": validation.get("self_intersections", 0), | |
| "strain_exceeded": validation.get("strain_exceeded", False), | |
| # Compactness | |
| "deployment_ratio": float(deployment_ratio), | |
| "compactness": float(compactness), | |
| "volume_compaction": float(volume_compaction), | |
| "packing_efficiency": float(packing_efficiency), | |
| "fits_target_box": fits_target_box, | |
| "bounding_box": bb.tolist(), | |
| # Structural | |
| "max_strain": max_strain, | |
| "mean_strain": mean_strain, | |
| "total_energy": float(energy.get("total", 0.0)), | |
| "energy_bar": float(energy.get("bar", 0.0)), | |
| "energy_facet": float(energy.get("facet", 0.0)), | |
| "energy_fold": float(energy.get("fold", 0.0)), | |
| # Efficiency | |
| "fold_count": fold_count, | |
| "folding_efficiency": float(folding_efficiency), | |
| "crease_complexity": float(crease_complexity), | |
| # Deployability | |
| "is_deployable": is_deployable, | |
| "deployment_force_estimate": float(deployment_force_estimate), | |
| # Shape similarity placeholders | |
| "chamfer_distance": None, | |
| "hausdorff_distance": None, | |
| } | |