File size: 3,105 Bytes
efeed27
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""
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,
    }