Skip to content

compas_nest.nfp

compas_nest.nfp

opennest — the NFP + genetic-algorithm nesting engine (the OpenNest component).

nfp_solve

Handle to an NFP solve running on a background thread.

Returned by :meth:opennest.start. Poll :meth:snapshot for the current best layout (e.g. from a viewer animation callback), check :meth:is_running, and call :meth:wait for the final result.

Source code in compas_nest/nfp.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
class nfp_solve:
    """Handle to an NFP solve running on a background thread.

    Returned by :meth:`opennest.start`. Poll :meth:`snapshot` for the current best layout
    (e.g. from a viewer animation callback), check :meth:`is_running`, and call :meth:`wait` for the
    final result.
    """

    def __init__(self, geo, sheet_origins, n_instances, thread, box):
        self.geo = geo
        self.sheet_origins = sheet_origins
        self.n_instances = n_instances
        self._thread = thread
        self._box = box

    def progress(self):
        """int : GA generation reached so far."""
        return _nfp_nest.progress()

    def fitness(self):
        """float : Best fitness so far."""
        return _nfp_nest.fitness()

    def is_running(self):
        """bool : Whether the solve thread is still running."""
        return self._thread.is_alive()

    def cancel(self):
        """Ask the solve to stop and return its best layout so far."""
        _nfp_nest.cancel()

    def _build(self, tx, ty, angle, sheet_id, part_index, n_sheets, fitness=None):
        raw = [
            (part_index[i], sheet_id[i], angle[i], tx[i], ty[i])
            for i in range(self.n_instances)
        ]
        return nest_result._from_engine(raw, self.geo, self.sheet_origins, n_sheets, fitness=fitness, degrees=True)

    def snapshot(self):
        """Build a :class:`compas_nest.nest_result` from the current mid-solve layout."""
        _placed, tx, ty, angle, sheet_id, part_index, n_sheets = _nfp_nest.poll_layout(self.n_instances)
        return self._build(tx, ty, angle, sheet_id, part_index, n_sheets)

    def wait(self):
        """Block until the solve finishes and return the final :class:`compas_nest.nest_result`."""
        self._thread.join()
        _placed, tx, ty, angle, sheet_id, part_index, n_sheets, fitness = self._box["ret"]
        return self._build(tx, ty, angle, sheet_id, part_index, n_sheets, fitness)

progress()

int : GA generation reached so far.

Source code in compas_nest/nfp.py
27
28
29
def progress(self):
    """int : GA generation reached so far."""
    return _nfp_nest.progress()

fitness()

float : Best fitness so far.

Source code in compas_nest/nfp.py
31
32
33
def fitness(self):
    """float : Best fitness so far."""
    return _nfp_nest.fitness()

is_running()

bool : Whether the solve thread is still running.

Source code in compas_nest/nfp.py
35
36
37
def is_running(self):
    """bool : Whether the solve thread is still running."""
    return self._thread.is_alive()

cancel()

Ask the solve to stop and return its best layout so far.

Source code in compas_nest/nfp.py
39
40
41
def cancel(self):
    """Ask the solve to stop and return its best layout so far."""
    _nfp_nest.cancel()

snapshot()

Build a :class:compas_nest.nest_result from the current mid-solve layout.

Source code in compas_nest/nfp.py
50
51
52
53
def snapshot(self):
    """Build a :class:`compas_nest.nest_result` from the current mid-solve layout."""
    _placed, tx, ty, angle, sheet_id, part_index, n_sheets = _nfp_nest.poll_layout(self.n_instances)
    return self._build(tx, ty, angle, sheet_id, part_index, n_sheets)

wait()

Block until the solve finishes and return the final :class:compas_nest.nest_result.

Source code in compas_nest/nfp.py
55
56
57
58
59
def wait(self):
    """Block until the solve finishes and return the final :class:`compas_nest.nest_result`."""
    self._thread.join()
    _placed, tx, ty, angle, sheet_id, part_index, n_sheets, fitness = self._box["ret"]
    return self._build(tx, ty, angle, sheet_id, part_index, n_sheets, fitness)

opennest

Nest polylines (with holes) into sheets (with holes) using the NFP + genetic-algorithm engine.

Replicates the OpenNest grasshopper component, including carrying attributes geometry through placement. The solve runs on a background thread while the calling thread prints live progress (GA generation + fitness); Ctrl-C cancels and returns the best layout so far.

Parameters:

Name Type Description Default
generations int

GA generations to evolve (the component "Iterations").

10
rotations int

Discrete rotation count (360 / n orientations).

8
placement_type int

0 = box, 1 = gravity, 2 = squeeze.

1
spacing float

Gap between parts.

0.0
seed int

RNG seed (-1 = time-based, non-deterministic).

30
mutation_rate int

GA mutation rate (applied as 0.01 * rate).

10
population_size int

GA population size.

10
use_holes bool

Allow nesting into holes.

True
try_all_rotations bool

Evaluate every rotation per placement (slower, tighter). Defaults to False.

.. warning:: The upstream NFP engine's tryAllRotations path can crash (segfault) on some mixes of part shapes (e.g. a triangle together with a holed rectangle). Leave this False unless you know your geometry is safe.

False
exact_nfp bool

Full-resolution exact NFP (no gap, slower).

False
mode int

0 = faithful (single-thread parity), 1 = default, 2 = turbo (multi-seed).

1
num_seeds int

Turbo: parallel independent seeds.

4
use_parallel bool

Parallel NFP / population evaluation.

True
curve_tolerance float

Simplification tolerance.

0.3
clipper_scale float

Clipper integer scale.

10000000.0
time_budget_secs float

If > 0, run until elapsed (overrides generations).

0.0
max_sheets int

0 = use all provided sheets.

0
verbose bool

Print progress to the terminal.

True
Source code in compas_nest/nfp.py
 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
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
class opennest:
    """Nest polylines (with holes) into sheets (with holes) using the NFP + genetic-algorithm engine.

    Replicates the OpenNest grasshopper component, including carrying ``attributes``
    geometry through placement. The solve runs on a background thread while the calling thread prints
    live progress (GA generation + fitness); ``Ctrl-C`` cancels and returns the best layout so far.

    Parameters
    ----------
    generations : int, optional
        GA generations to evolve (the component "Iterations").
    rotations : int, optional
        Discrete rotation count (360 / n orientations).
    placement_type : int, optional
        0 = box, 1 = gravity, 2 = squeeze.
    spacing : float, optional
        Gap between parts.
    seed : int, optional
        RNG seed (-1 = time-based, non-deterministic).
    mutation_rate : int, optional
        GA mutation rate (applied as 0.01 * rate).
    population_size : int, optional
        GA population size.
    use_holes : bool, optional
        Allow nesting into holes.
    try_all_rotations : bool, optional
        Evaluate every rotation per placement (slower, tighter). Defaults to ``False``.

        .. warning::
            The upstream NFP engine's ``tryAllRotations`` path can **crash (segfault)** on some
            mixes of part shapes (e.g. a triangle together with a holed rectangle). Leave this
            ``False`` unless you know your geometry is safe.
    exact_nfp : bool, optional
        Full-resolution exact NFP (no gap, slower).
    mode : int, optional
        0 = faithful (single-thread parity), 1 = default, 2 = turbo (multi-seed).
    num_seeds : int, optional
        Turbo: parallel independent seeds.
    use_parallel : bool, optional
        Parallel NFP / population evaluation.
    curve_tolerance : float, optional
        Simplification tolerance.
    clipper_scale : float, optional
        Clipper integer scale.
    time_budget_secs : float, optional
        If > 0, run until elapsed (overrides ``generations``).
    max_sheets : int, optional
        0 = use all provided sheets.
    verbose : bool, optional
        Print progress to the terminal.
    """

    def __init__(
        self,
        generations=10,
        rotations=8,
        placement_type=1,
        spacing=0.0,
        seed=30,
        mutation_rate=10,
        population_size=10,
        use_holes=True,
        try_all_rotations=False,
        exact_nfp=False,
        mode=1,
        num_seeds=4,
        use_parallel=True,
        curve_tolerance=0.3,
        clipper_scale=1e7,
        sheet_spacing=0.0,
        rotation_limit=360.0,
        time_budget_secs=0.0,
        max_sheets=0,
        verbose=True,
    ):
        self.generations = generations
        self.rotations = rotations
        self.placement_type = placement_type
        self.spacing = spacing
        self.seed = seed
        self.mutation_rate = mutation_rate
        self.population_size = population_size
        self.use_holes = use_holes
        self.try_all_rotations = try_all_rotations
        self.exact_nfp = exact_nfp
        self.mode = mode
        self.num_seeds = num_seeds
        self.use_parallel = use_parallel
        self.curve_tolerance = curve_tolerance
        self.clipper_scale = clipper_scale
        self.sheet_spacing = sheet_spacing
        self.rotation_limit = rotation_limit
        self.time_budget_secs = time_budget_secs
        self.max_sheets = max_sheets
        self.verbose = verbose

    def _params(self):
        p = _nfp_nest.NfpParams()
        p.placementType = int(self.placement_type)
        p.rotations = max(1, int(self.rotations))
        p.mutationRate = int(self.mutation_rate)
        p.populationSize = max(1, int(self.population_size))
        p.seed = int(self.seed)
        p.curveTolerance = float(self.curve_tolerance)
        p.clipperScale = float(self.clipper_scale)
        p.spacing = float(self.spacing)
        p.sheetSpacing = float(self.sheet_spacing)
        p.rotationLimit = float(self.rotation_limit)
        p.useHoles = 1 if self.use_holes else 0
        p.exploreConcave = 0
        p.clipByHull = 0
        p.clipByRects = 0
        p.simplify = 0
        p.mode = int(self.mode)
        p.generations = int(self.generations)
        p.numSeeds = int(self.num_seeds)
        p.useParallel = 1 if self.use_parallel else 0
        p.timeBudgetSecs = float(self.time_budget_secs)
        p.maxSheets = int(self.max_sheets)
        p.edgeSamples = 0
        p.compactionPasses = 0
        p.tryAllRotations = 1 if self.try_all_rotations else 0
        p.exactNfp = 1 if self.exact_nfp else 0
        return p

    def start(self, geo, sheets):
        """Launch the solve on a background thread and return immediately.

        Use this for live/animated UIs: poll :meth:`nfp_solve.snapshot` while
        :meth:`nfp_solve.is_running` is true, then call :meth:`nfp_solve.wait`.

        Parameters
        ----------
        geo : :class:`compas_nest.nest_geo`
            Parts to nest (``copies`` handled natively as quantities; instance order is
            part0 x q0, part1 x q1, ...).
        sheets : :class:`compas_nest.nest_sheets`
            Sheets to nest into.

        Returns
        -------
        :class:`nfp_solve`
        """
        parts = geo._flatten_parts(expand_copies=False)
        sh = sheets.to_arrays()
        params = self._params()
        n_instances = sum(parts["quantities"])

        box = {}

        def work():
            box["ret"] = _nfp_nest.nest(
                parts["vertex_counts"], parts["xy"], parts["quantities"],
                parts["hole_counts"], parts["hole_vertex_counts"], parts["hole_xy"],
                sh["vertex_counts"], sh["xy"],
                sh["hole_counts"], sh["hole_vertex_counts"], sh["hole_xy"],
                params,
                parts["rotations"],   # per-part rotation overrides (0 = global)
            )

        _nfp_nest.cancel_reset()
        thread = threading.Thread(target=work, daemon=True)
        thread.start()
        return nfp_solve(geo, sh["origins"], n_instances, thread, box)

    def solve(self, geo, sheets):
        """Run the nest, blocking until done (prints progress when ``verbose``).

        Parameters
        ----------
        geo : :class:`compas_nest.nest_geo`
            Parts to nest (``copies`` handled natively as quantities).
        sheets : :class:`compas_nest.nest_sheets`
            Sheets to nest into.

        Returns
        -------
        :class:`compas_nest.nest_result`
        """
        handle = self.start(geo, sheets)
        try:
            while handle.is_running():
                if self.verbose:
                    sys.stdout.write("\r[opennest] gen {} / {}   fit {:.3f}   (Ctrl-C = stop)".format(handle.progress(), self.generations, handle.fitness()))
                    sys.stdout.flush()
                time.sleep(0.1)
        except KeyboardInterrupt:
            handle.cancel()
        result = handle.wait()
        if self.verbose:
            sys.stdout.write("\n")
            print("[opennest] placed {}/{} instances on {} sheet(s), fitness {:.3f}.".format(len(result.placed), len(result.placements), result.n_sheets, result.fitness or 0.0))
        return result

start(geo, sheets)

Launch the solve on a background thread and return immediately.

Use this for live/animated UIs: poll :meth:nfp_solve.snapshot while :meth:nfp_solve.is_running is true, then call :meth:nfp_solve.wait.

Parameters:

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

Parts to nest (copies handled natively as quantities; instance order is part0 x q0, part1 x q1, ...).

required
sheets :class:`compas_nest.nest_sheets`

Sheets to nest into.

required

Returns:

Type Description
class:`nfp_solve`
Source code in compas_nest/nfp.py
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
def start(self, geo, sheets):
    """Launch the solve on a background thread and return immediately.

    Use this for live/animated UIs: poll :meth:`nfp_solve.snapshot` while
    :meth:`nfp_solve.is_running` is true, then call :meth:`nfp_solve.wait`.

    Parameters
    ----------
    geo : :class:`compas_nest.nest_geo`
        Parts to nest (``copies`` handled natively as quantities; instance order is
        part0 x q0, part1 x q1, ...).
    sheets : :class:`compas_nest.nest_sheets`
        Sheets to nest into.

    Returns
    -------
    :class:`nfp_solve`
    """
    parts = geo._flatten_parts(expand_copies=False)
    sh = sheets.to_arrays()
    params = self._params()
    n_instances = sum(parts["quantities"])

    box = {}

    def work():
        box["ret"] = _nfp_nest.nest(
            parts["vertex_counts"], parts["xy"], parts["quantities"],
            parts["hole_counts"], parts["hole_vertex_counts"], parts["hole_xy"],
            sh["vertex_counts"], sh["xy"],
            sh["hole_counts"], sh["hole_vertex_counts"], sh["hole_xy"],
            params,
            parts["rotations"],   # per-part rotation overrides (0 = global)
        )

    _nfp_nest.cancel_reset()
    thread = threading.Thread(target=work, daemon=True)
    thread.start()
    return nfp_solve(geo, sh["origins"], n_instances, thread, box)

solve(geo, sheets)

Run the nest, blocking until done (prints progress when verbose).

Parameters:

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

Parts to nest (copies handled natively as quantities).

required
sheets :class:`compas_nest.nest_sheets`

Sheets to nest into.

required

Returns:

Type Description
class:`compas_nest.nest_result`
Source code in compas_nest/nfp.py
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
def solve(self, geo, sheets):
    """Run the nest, blocking until done (prints progress when ``verbose``).

    Parameters
    ----------
    geo : :class:`compas_nest.nest_geo`
        Parts to nest (``copies`` handled natively as quantities).
    sheets : :class:`compas_nest.nest_sheets`
        Sheets to nest into.

    Returns
    -------
    :class:`compas_nest.nest_result`
    """
    handle = self.start(geo, sheets)
    try:
        while handle.is_running():
            if self.verbose:
                sys.stdout.write("\r[opennest] gen {} / {}   fit {:.3f}   (Ctrl-C = stop)".format(handle.progress(), self.generations, handle.fitness()))
                sys.stdout.flush()
            time.sleep(0.1)
    except KeyboardInterrupt:
        handle.cancel()
    result = handle.wait()
    if self.verbose:
        sys.stdout.write("\n")
        print("[opennest] placed {}/{} instances on {} sheet(s), fitness {:.3f}.".format(len(result.placed), len(result.placements), result.n_sheets, result.fitness or 0.0))
    return result