Skip to content

compas_nest.result

compas_nest.result

Result of a nesting solve: placement poses and the world-placed geometry they imply.

nest_result

Bases: Data

Placements produced by a nesting engine (a serializable COMPAS Data).

The placement contract (shared by both engines) is::

world_point = Rotate(part_point, angle, about origin) + (tx, ty) + sheet_origin

where part_point is a vertex of the original part geometry and angle is in radians.

Parameters:

Name Type Description Default
placements list[dict]

Each placement: {"part_index": int, "sheet_id": int, "angle": float (rad), "tx": float, "ty": float}. Unplaced instances have sheet_id == -1.

required
geo :class:`compas_nest.nest_geo`

The parts that were nested (placements reference parts by part_index).

required
sheet_origins list[tuple[float, float]]

World origin of each sheet, added to sheet-local (tx, ty).

required
n_sheets int

Number of sheets actually used.

required
fitness float

Solver fitness (NFP engine only).

None
name str
None
Source code in compas_nest/result.py
 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
178
179
180
181
class nest_result(Data):
    """Placements produced by a nesting engine (a serializable COMPAS ``Data``).

    The placement contract (shared by both engines) is::

        world_point = Rotate(part_point, angle, about origin) + (tx, ty) + sheet_origin

    where ``part_point`` is a vertex of the *original* part geometry and ``angle`` is in radians.

    Parameters
    ----------
    placements : list[dict]
        Each placement: ``{"part_index": int, "sheet_id": int, "angle": float (rad), "tx": float, "ty": float}``.
        Unplaced instances have ``sheet_id == -1``.
    geo : :class:`compas_nest.nest_geo`
        The parts that were nested (placements reference parts by ``part_index``).
    sheet_origins : list[tuple[float, float]]
        World origin of each sheet, added to sheet-local ``(tx, ty)``.
    n_sheets : int
        Number of sheets actually used.
    fitness : float, optional
        Solver fitness (NFP engine only).
    name : str, optional
    """

    def __init__(self, placements, geo, sheet_origins, n_sheets, fitness=None, name=None):
        super().__init__(name=name)
        self.placements = placements
        self.geo = geo
        self.sheet_origins = sheet_origins
        self.n_sheets = n_sheets
        self.fitness = fitness

    @property
    def __data__(self):
        return {
            "placements": [dict(p) for p in self.placements],
            "geo": self.geo,
            "sheet_origins": [[float(o[0]), float(o[1])] for o in self.sheet_origins],
            "n_sheets": int(self.n_sheets),
            "fitness": self.fitness,
        }

    @classmethod
    def __from_data__(cls, data):
        return cls(
            data["placements"],
            data["geo"],
            [tuple(o) for o in data["sheet_origins"]],
            data["n_sheets"],
            data.get("fitness"),
        )

    @property
    def placed(self):
        """list[dict] : Placements that landed on a sheet (``sheet_id >= 0``)."""
        return [p for p in self.placements if p["sheet_id"] >= 0]

    @property
    def unplaced(self):
        """list[dict] : Placements that could not be placed."""
        return [p for p in self.placements if p["sheet_id"] < 0]

    def transformation(self, placement):
        """Return the world :class:`compas.geometry.Transformation` for a placement."""
        ox, oy = self.sheet_origins[placement["sheet_id"]]
        R = Rotation.from_axis_and_angle([0.0, 0.0, 1.0], placement["angle"])
        T = Translation.from_vector([ox + placement["tx"], oy + placement["ty"], 0.0])
        return T * R

    def placed_polylines(self, geo=None):
        """Return the placed geometry grouped per sheet.

        Parameters
        ----------
        geo : :class:`compas_nest.nest_geo`, optional
            Apply the solved poses to this geometry instead of the one that was nested. Use it to
            render the *original* parts when the solve ran on offset (clearance) geometry.

        Returns
        -------
        list[dict]
            One entry per used sheet::

                {"sheet_id": int,
                 "parts": [{"part_index": int, "transformation": Transformation,
                            "outline": Polyline, "holes": [Polyline, ...],
                            "attributes": [Geometry, ...]}, ...]}
        """
        parts_source = (geo or self.geo).parts
        groups = {}
        for placement in self.placed:
            sid = placement["sheet_id"]
            X = self.transformation(placement)
            part = parts_source[placement["part_index"]]
            entry = {
                "part_index": placement["part_index"],
                "transformation": X,
                "outline": part["outline"].transformed(X),
                "holes": [h.transformed(X) for h in part.get("holes", [])],
                "attributes": [a.transformed(X) for a in part.get("attributes", [])],
            }
            groups.setdefault(sid, []).append(entry)
        return [{"sheet_id": sid, "parts": groups[sid]} for sid in sorted(groups)]

    def to_json(self, filepath, geo=None):
        """Serialize the placed polylines (with holes) and transformations to COMPAS JSON.

        Parameters
        ----------
        filepath : str
            Output path. Parent directories are created if missing.
        geo : :class:`compas_nest.nest_geo`, optional
            Apply the solved poses to this geometry instead of the nested one (see
            :meth:`placed_polylines`).
        """
        filepath = os.fspath(filepath)
        os.makedirs(os.path.dirname(os.path.abspath(filepath)), exist_ok=True)
        json_dump({"n_sheets": self.n_sheets, "sheets": self.placed_polylines(geo=geo)}, filepath)
        return filepath

    def to_obj(self, filepath, geo=None):
        """Write the placed outlines and holes to a Wavefront OBJ as closed polyline loops.

        Parameters
        ----------
        filepath : str
            Output path. Parent directories are created if missing.
        geo : :class:`compas_nest.nest_geo`, optional
            Apply the solved poses to this geometry instead of the nested one (see
            :meth:`placed_polylines`).
        """
        filepath = os.fspath(filepath)
        os.makedirs(os.path.dirname(os.path.abspath(filepath)), exist_ok=True)
        lines = ["# compas_nest placed geometry"]
        offset = 0
        for group in self.placed_polylines(geo=geo):
            for part in group["parts"]:
                for ring in [part["outline"], *part["holes"]]:
                    pts = list(ring.points)
                    if len(pts) >= 2 and pts[0] == pts[-1]:
                        pts = pts[:-1]
                    for p in pts:
                        lines.append("v {} {} {}".format(float(p[0]), float(p[1]), float(p[2])))
                    idx = [offset + i + 1 for i in range(len(pts))]
                    lines.append("l " + " ".join(str(i) for i in idx + [idx[0]]))  # closed loop
                    offset += len(pts)
        with open(filepath, "w") as f:
            f.write("\n".join(lines) + "\n")
        return filepath

    @staticmethod
    def _from_engine(placements_raw, geo, sheet_origins, n_sheets, fitness=None, degrees=False):
        placements = []
        for part_index, sheet_id, angle, tx, ty in placements_raw:
            placements.append(
                {
                    "part_index": int(part_index),
                    "sheet_id": int(sheet_id),
                    "angle": math.radians(angle) if degrees else float(angle),
                    "tx": float(tx),
                    "ty": float(ty),
                }
            )
        return nest_result(placements, geo, sheet_origins, n_sheets, fitness)

    def __repr__(self):
        return "nest_result(placed={}/{}, n_sheets={}, fitness={})".format(
            len(self.placed), len(self.placements), self.n_sheets, self.fitness
        )

placed property

list[dict] : Placements that landed on a sheet (sheet_id >= 0).

unplaced property

list[dict] : Placements that could not be placed.

transformation(placement)

Return the world :class:compas.geometry.Transformation for a placement.

Source code in compas_nest/result.py
75
76
77
78
79
80
def transformation(self, placement):
    """Return the world :class:`compas.geometry.Transformation` for a placement."""
    ox, oy = self.sheet_origins[placement["sheet_id"]]
    R = Rotation.from_axis_and_angle([0.0, 0.0, 1.0], placement["angle"])
    T = Translation.from_vector([ox + placement["tx"], oy + placement["ty"], 0.0])
    return T * R

placed_polylines(geo=None)

Return the placed geometry grouped per sheet.

Parameters:

Name Type Description Default
geo :class:`compas_nest.nest_geo`

Apply the solved poses to this geometry instead of the one that was nested. Use it to render the original parts when the solve ran on offset (clearance) geometry.

None

Returns:

Type Description
list[dict]

One entry per used sheet::

{"sheet_id": int,
 "parts": [{"part_index": int, "transformation": Transformation,
            "outline": Polyline, "holes": [Polyline, ...],
            "attributes": [Geometry, ...]}, ...]}
Source code in compas_nest/result.py
 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
def placed_polylines(self, geo=None):
    """Return the placed geometry grouped per sheet.

    Parameters
    ----------
    geo : :class:`compas_nest.nest_geo`, optional
        Apply the solved poses to this geometry instead of the one that was nested. Use it to
        render the *original* parts when the solve ran on offset (clearance) geometry.

    Returns
    -------
    list[dict]
        One entry per used sheet::

            {"sheet_id": int,
             "parts": [{"part_index": int, "transformation": Transformation,
                        "outline": Polyline, "holes": [Polyline, ...],
                        "attributes": [Geometry, ...]}, ...]}
    """
    parts_source = (geo or self.geo).parts
    groups = {}
    for placement in self.placed:
        sid = placement["sheet_id"]
        X = self.transformation(placement)
        part = parts_source[placement["part_index"]]
        entry = {
            "part_index": placement["part_index"],
            "transformation": X,
            "outline": part["outline"].transformed(X),
            "holes": [h.transformed(X) for h in part.get("holes", [])],
            "attributes": [a.transformed(X) for a in part.get("attributes", [])],
        }
        groups.setdefault(sid, []).append(entry)
    return [{"sheet_id": sid, "parts": groups[sid]} for sid in sorted(groups)]

to_json(filepath, geo=None)

Serialize the placed polylines (with holes) and transformations to COMPAS JSON.

Parameters:

Name Type Description Default
filepath str

Output path. Parent directories are created if missing.

required
geo :class:`compas_nest.nest_geo`

Apply the solved poses to this geometry instead of the nested one (see :meth:placed_polylines).

None
Source code in compas_nest/result.py
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
def to_json(self, filepath, geo=None):
    """Serialize the placed polylines (with holes) and transformations to COMPAS JSON.

    Parameters
    ----------
    filepath : str
        Output path. Parent directories are created if missing.
    geo : :class:`compas_nest.nest_geo`, optional
        Apply the solved poses to this geometry instead of the nested one (see
        :meth:`placed_polylines`).
    """
    filepath = os.fspath(filepath)
    os.makedirs(os.path.dirname(os.path.abspath(filepath)), exist_ok=True)
    json_dump({"n_sheets": self.n_sheets, "sheets": self.placed_polylines(geo=geo)}, filepath)
    return filepath

to_obj(filepath, geo=None)

Write the placed outlines and holes to a Wavefront OBJ as closed polyline loops.

Parameters:

Name Type Description Default
filepath str

Output path. Parent directories are created if missing.

required
geo :class:`compas_nest.nest_geo`

Apply the solved poses to this geometry instead of the nested one (see :meth:placed_polylines).

None
Source code in compas_nest/result.py
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
def to_obj(self, filepath, geo=None):
    """Write the placed outlines and holes to a Wavefront OBJ as closed polyline loops.

    Parameters
    ----------
    filepath : str
        Output path. Parent directories are created if missing.
    geo : :class:`compas_nest.nest_geo`, optional
        Apply the solved poses to this geometry instead of the nested one (see
        :meth:`placed_polylines`).
    """
    filepath = os.fspath(filepath)
    os.makedirs(os.path.dirname(os.path.abspath(filepath)), exist_ok=True)
    lines = ["# compas_nest placed geometry"]
    offset = 0
    for group in self.placed_polylines(geo=geo):
        for part in group["parts"]:
            for ring in [part["outline"], *part["holes"]]:
                pts = list(ring.points)
                if len(pts) >= 2 and pts[0] == pts[-1]:
                    pts = pts[:-1]
                for p in pts:
                    lines.append("v {} {} {}".format(float(p[0]), float(p[1]), float(p[2])))
                idx = [offset + i + 1 for i in range(len(pts))]
                lines.append("l " + " ".join(str(i) for i in idx + [idx[0]]))  # closed loop
                offset += len(pts)
    with open(filepath, "w") as f:
        f.write("\n".join(lines) + "\n")
    return filepath