Skip to main content

pyckingsolver

Shapely-based Python interface for PackingSolver — 2D irregular bin packing & nesting.

PyPI version Python 3.10+ License: MIT Build

Pack irregular shapes into bins — rectangles, circles, arbitrary polygons with holes. Built for CNC laser cutting, sheet metal nesting, fabric cutting, and any 2D packing problem.

Metal cutting — plates, washers, brackets, gussets
Laser cutting layout: mounting plates with bolt holes, washers, U-brackets, discs & gussets


Install

pip install pyckingsolver

The C++ solver binary is bundled — no compilation needed on Windows x64 and Linux x64.

For other platforms, build the solver from the included submodule. See Building the Solver.


What's New in 0.8.0

  • ARC_RESOLUTION now means vertices per full circle, and a circle costs 64 of them, not 256. circle_polygon used Point.buffer(resolution=...), whose resolution counts segments per quadrant, while _sample_arc on the parse side read the same constant as segments per full circle. One name, two meanings, 4x apart. Vertex count is what the solver's runtime scales with: fourteen distinct-radius circle types x4 copies on a 2000x1000 sheet at 3 mm spacing, KNAPSACK, 56 items placed either way, went 12.23 s -> 7.89 s (1.55x). Accuracy lost is 0.010% -> 0.161% of area, a 0.06 mm sagitta at r=50 — an order of magnitude under any kerf. Packings change, so pin ==0.7.0 if you need the old layouts verbatim.

  • C++ pin moves to packingsolver 9917fcb0f (master, 2026-09-02). Two upstream fixes. A subset-row cut in the shared column-generation template charged its resource once per item copy instead of once per item type, overcharging the cut on pools with multi-copy types; column generation is auto-selected for bin packing with several bin types, so irregular reaches it. The other fix is a onedimensional MILP guard that irregular links but never triggers.

  • copies_min on item types. add_item(..., copies_min=2) forces at least that many copies to be packed. It only bites under KNAPSACK, where packing an item type is otherwise optional: a 100x100 bin offered one 90x90 item at profit 1 and twenty-five 20x20 items at profit 50 packs the smalls and drops the big one; copies_min=1 on the big item packs it instead.

  • SolverParams.not_anytime_tree_search_periodic_packing_queue_size — the last packingsolver_irregular CLI option without a wrapper field. Every flag the binary accepts is now reachable from SolverParams.

  • on_improvement= streams provisional layouts. solve() calls it with each improving certificate the poll loop catches, then once with the solution it returns. Adapted from @Cabalist's PR #4.

  • json_output= now saves the solver's certificate verbatim instead of re-serializing the parsed solution. The old path wrote only id/x/y/angle/mirror, so reading the file back gave a Solution with zero geometry. Solution.to_json() still writes that sparse form on demand — it is upstream's own editable solution format, see Debugging against the C++ solver.

  • Solution.metrics finally holds the metrics. It used to hand back the entire --output document (Parameters, IntermediaryOutputs, Output), so the documented metrics["BinCost"] raised KeyError. It is now Output.Solution (BinCost, FullWastePercentage, DensityX, ItemProfit, NumberOfBins, ...) merged with the run-level Time, IsProvenInfeasible and the per-objective bounds. IntermediaryOutputs, one entry per improving solution, is dropped.

What's New in 0.7.0

  • C++ pin moves to packingsolver 5c8dcbcde (master, 2026-09-01). The headline change is upstream's new tree_search_periodic_packing algorithm (2026-07-30, item spacing honoured since 07-31): any item type with more than 16 copies is tiled as a repeating lattice instead of being placed copy by copy. It is auto-selected; SolverParams.use_tree_search_periodic_packing exposes the CLI flag for when you want to force or disable it.
  • Measured on a laser-nesting benchmark (2000x1000 sheet, 3 mm spacing, KNAPSACK): a 465-part pool of five small parts placed fully in 1.9 s where 0.6.7 needed 87 s; a 1000-part pool reached 67.7% fill in 7.5 s where 0.6.7 placed 158 parts in 45 s; a single part at 3000 copies packed to 78.5% in 4.6 s.
  • Also picked up from upstream since 1ad3c94e9: knapsack tree search tries bin types by increasing space, proven-infeasibility detection for bin packing objectives, defects respected in the maximal-spaces tree search, copies_min on item types, and a HiGHS callback fix.

What's New in 0.6.7

  • Native item_item_minimum_spacing no longer crashes on items with holes. The C++ pin moves to packingsolver 1ad3c94e9, whose shape dependency now points at fontanf/shape abea925 (PR #41). approximate_by_line_segments was picking its arc-extras function from outer alone, but the choice also depends on the arc's orientation: a Clockwise hole arc got the wrong wedge, its boundary never cancelled during the union, and it survived as a CircularArc into output that then failed is_polygon(). Measured on a 6-case nesting benchmark, the previous binary threw on 3 of them and silently fell back to a worse packing; one case improves from 35.2% to 59.8% material efficiency once the solve actually completes.
  • nest() lost its pre_buffer argument. It existed only to dodge the crash above by inflating each item by spacing/2, which rounded corners, grew area and shrank holes. spacing now always goes to the solver natively. Breaking: passing pre_buffer= is now a TypeError.
  • Carries 8 upstream packingsolver fixes, including a crash on single-item/single-bin instances whose rotation requires mirroring.

What's New in 0.6.6

  • Solver.solve() returns None instead of raising when no solution was found. A too-tight bin or a first_solution_timeout cutoff is a normal answer ("this does not fit"), not a failure, and callers that probe with short budgets were drowning in FileNotFoundError noise for expected outcomes. Genuine solver crashes (non-zero exit) still raise RuntimeError and still save the instance to _crashes/. Breaking: check the result for None before using it. nest() forwards it.

What's New in 0.6.5

  • Adaptive stop: stall_timeout / first_solution_timeout on SolverParams. The Anytime solver writes every improving certificate, so these watch that file and end the solve once it converges — time_limit becomes a ceiling instead of a fixed cost. stall_timeout kills the run after N seconds without an improvement; first_solution_timeout kills a wedged run that never produced one. Either flag forces only_write_at_the_end=False (the streaming writes are the signal), and the best certificate found so far is kept and returned. Unset = old behaviour.
  • Bundled solver rebuilt at upstream 07682efd9. Tighter knapsack bound from the Benders decomposition MILP relaxation, plus Feasibility support in column generation.
  • Runtime crash dumps (pyckingsolver/_crashes) can no longer end up inside a wheel.

What's New in 0.6.4

  • Bundled solver rebuilt at upstream bbfe94288. Fixes a crash on empty-item instances and a circle-item crash; adds SolutionBuilder and a user feasibility callback to the irregular module. No wrapper API change.

What's New in 0.6.3

  • Bundled solver rebuilt at upstream 59f50fed3. Fixes a NotAnytimeDeterministic race across the parallel algorithms in optimize(), so deterministic solves are now genuinely reproducible. No API or JSON change — the remaining 14 upstream commits are all other-domain or tooling.

What's New in 0.6.2

  • Bundled solver rebuilt at upstream 750c7d7fd. Irregular tree search now skips bins that can't fit any item instead of dead-ending the branch; skipped bins appear in the solution as empty bins (kept for position/cost accounting), so Solution.bins may contain bins with no items.

What's New in 0.6.1

  • Native cancellation: Solver.solve(..., cancel=event). Pass any Event-like object (.is_set()); once set, the solver subprocess is killed (0.25s poll) and SolverCancelled is raised. No cancel = the old subprocess.run path, byte-for-byte unchanged.

What's New in 0.6.0

  • Bundled solver rebuilt at upstream 8ea3129e6.
  • Corner → LeftoverMode (breaking rename, mirrors upstream). Parameters.leftover_corner → leftover_mode, set_leftover_corner() → set_leftover_mode(), SolverParams.leftover_corner → leftover_mode. New edge modes: LEFT, RIGHT, BOTTOM, TOP — reserve a full-width / full-height strip instead of a corner rectangle.

What's New in 0.5.0

  • Bundled solver rebuilt at upstream c2c1f1f42. Mostly an internal branching_scheme→tree_search refactor across domains; irregular now folds sequential_feasibility into tree/local search automatically.
  • CLI sync (breaking only for tuning knobs). Upstream removed the four use_sequential_feasibility / sequential_feasibility_use_* toggles and renamed *_subproblem_queue_size → *_subproblem_tree_search_queue_size. SolverParams matches the new binary; default/auto solves are unaffected.

What's New in 0.4.1

  • Bundled solver rebuilt at upstream da2af179b. Irregular ID types widened int16→int32 and input-scale overflow guards added — pathological instances now fail with a clear error instead of an access-violation crash.
  • New memory_limit_megabytes knob (--memory-limit, MiB; None/0 = unlimited) — optional RAM cap so the solver fails cleanly rather than OOM-crashing.

What's New in 0.4.0 (Breaking)

  • Dropped the inert quality-rule surface. add_quality_rule(...), Parameters.quality_rules and ItemShape.quality_rule are removed — the bundled binary never read them from JSON, so they silently did nothing.
  • Removed the _extra forward-compat dicts from every dataclass. New upstream CLI flags are still reachable via SolverParams.extra_args.
  • New solver passthroughs: objective= (re-run one instance under another objective), log_path= / log_to_stderr= / json_search_tree_path=.
  • Fixed Solution.placed_shapes() — it double-applied the placement transform; the solver already emits absolute coordinates, so it now returns item.shapes unchanged.
  • AllowedRotation remains a (start_angle, end_angle, mirror) dataclass; the legacy (start, end) 2-tuple form and allow_mirroring=True keyword still work.
  • Every shape is serialized as type=polygon (circles/rectangles included). Feeding the solver native type:"circle" crashes it, and native type:"rectangle" makes its heuristic non-deterministic; the polygon form is identical geometry with stable output.

Hole Fill Custom Holes & Rings Metal Cutting
hole fill custom holes metal cutting
Filler placed inside frame hole Frames, rings, discs & triangles Plates, washers, brackets & gussets

All Features — Example Results

# Example Preview
1 BIN_PACKING — fewest bins (rects, triangles, circles) ex01
2 KNAPSACK — maximize profit ex02
3 OPEN_DIMENSION_X — minimize strip width, free rotation ex03
4 OPEN_DIMENSION_Y — minimize strip height ex04
6 VARIABLE_SIZED_BIN_PACKING — multi-size bins, min cost ex06
7 BIN_PACKING_WITH_LEFTOVERS — leftover tracking ex07
8 DEFECTS — avoid scratch zones ex08
9 POLYGON BIN — hexagonal bin ex09
10 HOLES & MIRRORING — frames with holes, L-shapes ex10
11 LP=Highs + ANCHOR — post-processing ex11
12 JSON ROUND-TRIP — serialize → load → solve ex12

Note: Example 5 (OPEN_DIMENSION_XY) is omitted — the C++ solver crashes on this objective in the current build.


Quick Start

from shapely.geometry import Polygon, Point
from pyckingsolver import InstanceBuilder, Objective, Solver

b = InstanceBuilder(Objective.OPEN_DIMENSION_X)
b.add_bin_type_rectangle(1200, 600)
b.add_item_type_rectangle(80, 60, copies=10)
b.add_item_type(Polygon([(0,0),(50,0),(25,40)]), copies=6)

solver = Solver()  # auto-finds bundled binary
solution = solver.solve(b.build(), time_limit=30)

print(f"{solution.total_item_count()} items in {solution.total_bins_used()} bins")

for item in solution.all_items():
    print(item.item_type_id, item.angle, item.shapes[0].bounds)

Debugging against the C++ solver

Everything the wrapper exchanges with packingsolver_irregular is JSON, so any solve can be reproduced against the upstream binary by hand.

inst = builder.build()
inst.to_json("instance.json")             # byte-identical to what solve() sends

sol = Solver().solve(inst, json_output="certificate.json")   # the solver's own output, verbatim
sol.to_json("sparse.json")                # upstream's editable solution format
packingsolver_irregular --input instance.json --certificate out.json --output metrics.json

The two solution files are different on purpose:

file contents who reads it
json_output= certificate full geometry, every placed shape Solution.from_json, your own inspection
Solution.to_json() bins[].id/copies + items[].id/x/y/angle/mirror upstream SolutionBuilder::read, and the fill_solution_file tool that rebuilds geometry from it

Solution.metrics comes from the --output file, not the certificate.

Objectives

Choose what the solver optimizes:

Objective Use Case
OPEN_DIMENSION_X Minimize strip width — items pack left-to-right (laser cutting rolls)
OPEN_DIMENSION_Y Minimize strip height
OPEN_DIMENSION_XY Minimize both dimensions (compact 2D nesting)
BIN_PACKING Use fewest bins — fixed-size sheets
KNAPSACK Maximize value of items in one bin
VARIABLE_SIZED_BIN_PACKING Multiple bin sizes with costs — minimize total cost
BIN_PACKING_WITH_LEFTOVERS Bin packing that tracks reusable scrap
DEFAULT Let the solver pick the best objective
OPEN_DIMENSION_Z Minimize the Z dimension (3D problems)

All C++ naming conventions are accepted — kebab-case, PascalCase, and abbreviations:

Objective("bin-packing")             # kebab-case (canonical)
Objective("BinPacking")              # PascalCase
Objective("BPP")                     # abbreviation
Objective("BinPackingWithLeftovers")  # PascalCase
Objective("BPPL")                    # abbreviation
from pyckingsolver import Objective

b = InstanceBuilder(Objective.BIN_PACKING)

InstanceBuilder

Bins

b = InstanceBuilder(Objective.BIN_PACKING)

# Rectangle bin
b.add_bin_type_rectangle(1200, 600, copies=10, cost=1.0)

# Circle bin (discretized to a polygon — see Shape Format below)
b.add_bin_type_circle(radius=300)

# Any Shapely polygon
b.add_bin_type(Polygon([...]))

# With edge clearance (e.g. clamp margin)
b.add_bin_type_rectangle(1200, 600, item_bin_minimum_spacing=5.0)

# Multiple bin types (variable-sized bin packing)
small_id = b.add_bin_type_rectangle(600, 400, cost=1.0, copies=5)
large_id = b.add_bin_type_rectangle(1200, 800, cost=1.8, copies=3)

Items

# Rectangle item
b.add_item_type_rectangle(80, 60, copies=4)

# Any Shapely polygon
b.add_item_type(Polygon([(0,0),(100,0),(50,80)]), copies=6)

# Polygon with interior hole (e.g. washer, frame)
washer = Point(0,0).buffer(30).difference(Point(0,0).buffer(15))
b.add_item_type(washer, copies=4)

# With profit (for knapsack)
b.add_item_type(polygon, copies=3, profit=42.0)

# Multiple shapes per item (composite/multi-part item)
b.add_item_type([shape_a, shape_b], copies=2)

# Force at least N copies to be packed (KNAPSACK only — elsewhere every copy is mandatory)
b.add_item_type(polygon, copies=5, copies_min=2)

Rotations

The allowed_rotations parameter accepts several shapes — each entry maps to one upstream AllowedRotation { start_angle, end_angle, mirror } record:

from pyckingsolver import AllowedRotation

# Fixed at 0° (default if omitted)
b.add_item_type(shape)

# Discrete angles (mirror=False each)
b.add_item_type(shape, allowed_rotations=[0, 90, 180, 270])

# 2-tuples — continuous ranges, mirror=False
b.add_item_type(shape, allowed_rotations=[(0, 0), (90, 90)])

# 3-tuples — full triple form (start, end, mirror)
b.add_item_type(shape, allowed_rotations=[(0, 0, False), (90, 90, True)])

# Free continuous rotation
b.add_item_type(shape, allowed_rotations=[(0, 360)])

# AllowedRotation dataclass directly
b.add_item_type(shape, allowed_rotations=[AllowedRotation(0, 360, False),
                                          AllowedRotation(0, 360, True)])

# Back-compat: duplicate every entry with mirror=True
b.add_item_type(shape, allow_mirroring=True)

Fixed Items

Pre-place an item that the solver must pack around. Applies to every bin of the chosen BinType:

b = InstanceBuilder(Objective.BIN_PACKING)
bin_id  = b.add_bin_type_rectangle(1200, 600)
plate_id = b.add_item_type_rectangle(200, 100)        # "plate" item type
b.add_item_type_rectangle(80, 60, copies=20)          # parts to pack

# Lock one plate at (50, 50) on every bin of this type
b.add_fixed_item(bin_id, plate_id, (50, 50), angle=0, mirror=False)

solution = Solver().solve(b.build(), time_limit=30)

# Identify the locked items in the solution (set by the wrapper post-parse)
for it in solution.all_items():
    if it.is_fixed:
        print("locked at", it.x, it.y)

Caveat: the C++ solver's JSON output does not currently emit the is_fixed flag. The wrapper reconstructs it by matching each placement against the bin's fixed_items list (Solution.mark_fixed_items()).

Spacing

# Minimum gap between all items (e.g. 2mm laser kerf)
b.set_item_item_minimum_spacing(2.0)

# Clearance from bin edges (per bin type)
b.add_bin_type_rectangle(1200, 600, item_bin_minimum_spacing=5.0)

Defects

Defects are no-go zones inside a bin (scratches, holes, clamps):

bin_id = b.add_bin_type_rectangle(1200, 600)

# Add a defect (no item may overlap it)
scratch = Polygon([(100,100),(200,100),(200,150),(100,150)])
b.add_defect(bin_id, scratch)

# With clearance around defect
b.add_defect(bin_id, scratch, item_defect_minimum_spacing=3.0)

# With defect type label
b.add_defect(bin_id, scratch, defect_type=1)

Aspect Ratio (Open Dimension XY)

b = InstanceBuilder(Objective.OPEN_DIMENSION_XY)
b.set_open_dimension_xy_aspect_ratio(1.5)  # enforce width/height <= 1.5

Leftover Mode

For BIN_PACKING_WITH_LEFTOVERS, set the reference corner/edge for scrap:

from pyckingsolver import LeftoverMode

b.set_leftover_mode(LeftoverMode.BOTTOM_LEFT)   # default
b.set_leftover_mode(LeftoverMode.TOP_RIGHT)
b.set_leftover_mode(LeftoverMode.RIGHT)         # edge: full-height strip

Modes accept all C++ naming formats:

LeftoverMode("BottomLeft")    # PascalCase (canonical)
LeftoverMode("bl")            # abbreviation
LeftoverMode("bottom-left")   # kebab-case

Use Cases

Laser Cutting / Sheet Metal Nesting

Minimize material usage from a fixed sheet with kerf spacing:

from shapely.geometry import Polygon, Point
from pyckingsolver import InstanceBuilder, Objective, Solver

b = InstanceBuilder(Objective.BIN_PACKING)
b.set_item_item_minimum_spacing(2.0)        # 2mm laser kerf
b.add_bin_type_rectangle(1200, 600, copies=100)

# Mounting plate with bolt holes
plate = Polygon([(0,0),(150,0),(150,100),(0,100)])
for cx, cy in [(25,25),(125,25),(25,75),(125,75)]:
    plate = plate.difference(Point(cx,cy).buffer(12, resolution=16))
b.add_item_type(plate, copies=8,
                allowed_rotations=[(0,0),(90,90),(180,180),(270,270)])

# Discs that nest inside the bolt holes
b.add_item_type(Point(0,0).buffer(8, resolution=16), copies=16)

# L-bracket
b.add_item_type(
    Polygon([(0,0),(80,0),(80,60),(70,60),(70,10),(10,10),(10,60),(0,60)]),
    copies=12, allowed_rotations=[(0,0),(90,90),(180,180),(270,270)])

solution = Solver().solve(b.build(), time_limit=60)
print(f"{solution.total_item_count()} parts in {solution.total_bins_used()} sheets")

Roll / Strip Cutting

Minimize roll length consumed:

b = InstanceBuilder(Objective.OPEN_DIMENSION_X)
b.set_item_item_minimum_spacing(1.5)
b.add_bin_type_rectangle(99999, 1200)   # very long, fixed width

b.add_item_type(shape_a, copies=20, allowed_rotations=[(0, 360)])
b.add_item_type(shape_b, copies=15, allowed_rotations=[(0, 360)])

solution = Solver().solve(b.build(), time_limit=30)
used_length = max(item.x + item.shapes[0].bounds[2]
                  for item in solution.all_items())
print(f"Roll used: {used_length:.1f} mm")

Knapsack / Value Maximization

Pack as much value as possible in one bin:

b = InstanceBuilder(Objective.KNAPSACK)
b.add_bin_type_rectangle(500, 300)

shapes_with_values = [
    (Polygon([...]), 10.0),
    (Polygon([...]), 25.0),
]
for shape, profit in shapes_with_values:
    b.add_item_type(shape, copies=5, profit=profit)

solution = Solver().solve(b.build(), time_limit=30)
total_profit = sum(
    instance.item_types[item.item_type_id].profit
    for item in solution.all_items()
)

Variable-Sized Bin Packing

Choose from multiple sheet sizes to minimize cost:

b = InstanceBuilder(Objective.VARIABLE_SIZED_BIN_PACKING)
b.add_bin_type_rectangle(600, 400, cost=1.0, copies=10)
b.add_bin_type_rectangle(1200, 800, cost=1.8, copies=5)

for shape in my_parts:
    b.add_item_type(shape, copies=2)

solution = Solver().solve(b.build(), time_limit=60)

Defective Sheet Handling

Avoid defective zones on material:

b = InstanceBuilder(Objective.BIN_PACKING)
bin_id = b.add_bin_type_rectangle(1200, 600)

# Scratch at center — no item within 3mm
scratch = Point(600,300).buffer(40)
b.add_defect(bin_id, scratch, item_defect_minimum_spacing=3.0)

# Clamped edges — keep items 10mm from edges
b.add_bin_type_rectangle(1200, 600, item_bin_minimum_spacing=10.0)

Arbitrary Polygon Bins

Non-rectangular cutting areas (e.g., round table, irregular offcut):

# Circular bin
b.add_bin_type_circle(radius=500)

# Hexagonal bin
import math
hex_pts = [(500*math.cos(math.pi/3*i), 500*math.sin(math.pi/3*i)) for i in range(6)]
b.add_bin_type(Polygon(hex_pts))

# Irregular offcut
offcut = Polygon([(0,0),(800,0),(800,300),(500,600),(0,600)])
b.add_bin_type(offcut)

Solver

from pyckingsolver import Solver, LeftoverMode

# Auto-discover bundled binary
solver = Solver()

# Explicit binary path
solver = Solver(binary="path/to/packingsolver_irregular")

# Different problem type (rectangle-only problems)
solver = Solver(problem_type="rectangle")

solution = solver.solve(
    instance,
    time_limit=60,              # seconds
    verbosity_level=1,          # 0=quiet, 1=summary, 2=verbose
    json_output="sol.json",     # optional: save the solver certificate as-is
)

SolverParams Dataclass

For reusable configurations and IDE autocomplete, build a SolverParams dataclass once and pass it to solver.solve(...):

from pyckingsolver import Solver, SolverParams

params = SolverParams(
    time_limit=120,
    verbosity_level=1,
    optimization_mode="Anytime",
    use_tree_search=True,
    item_item_minimum_spacing=2.0,
    anchor=True,
    anchor_x_weight=1.0,
)

for instance in batch:
    sol = Solver().solve(instance, params=params)

Keyword arguments to solve() always override fields of params.

Streaming Improvements

Pass on_improvement to see provisional layouts while the solver is still searching:

solution = solver.solve(instance, time_limit=120, on_improvement=show_layout)

The Anytime solver rewrites its certificate on every improvement, so a callback forces only_write_at_the_end=False and the same 0.25 s poll that drives the adaptive stop parses each new certificate. A file caught mid-rewrite is retried on the next poll, so improvements closer together than the poll are coalesced: there is no callback per solver write.

The callback runs on the calling thread, so keep it quick and hand real work to a queue. Provisional solutions carry no metrics and the layout can still change. solve() calls it once more with exactly the Solution it returns; raising from it kills the solve, and no certificate means no callback and a None return.

Algorithm Control

Fine-tune the solver's strategy:

solution = solver.solve(
    instance,
    time_limit=120,
    # Choose optimization mode
    optimization_mode="Anytime",           # "Anytime" | "NotAnytime" | "NotAnytimeDeterministic"
    # Enable/disable algorithm components
    use_tree_search=True,
    use_sequential_single_knapsack=True,
    use_sequential_value_correction=True,
    use_column_generation=False,
    use_dichotomic_search=False,
)

Instance-Level Overrides

Override instance parameters from the solver call — useful for batch experiments:

solution = solver.solve(
    instance,
    time_limit=60,
    item_item_minimum_spacing=3.0,          # override kerf gap
    item_bin_minimum_spacing=5.0,           # override edge clearance
    leftover_mode=LeftoverMode.TOP_RIGHT,   # override scrap corner/edge
    bin_unweighted=True,                    # set bin costs to areas
    unweighted=True,                        # set item profits to areas
)

Post-Processing

Anchor items towards a corner by sliding them as close as possible without overlapping:

solution = solver.solve(
    instance,
    time_limit=60,
    anchor=True,
    anchor_x_weight=1.0,   # positive=left, negative=right, 0=off
    anchor_y_weight=1.0,   # positive=bottom, negative=top, 0=off
)

Algorithm Tuning

Advanced parameters for algorithm performance tuning:

solution = solver.solve(
    instance,
    time_limit=120,
    initial_maximum_approximation_ratio=0.20,
    maximum_approximation_ratio_factor=0.75,
    sequential_value_correction_subproblem_tree_search_queue_size=128,
    column_generation_subproblem_tree_search_queue_size=128,
    not_anytime_tree_search_queue_size=512,
)

Forward-Compatible Extra Args

Unknown CLI flags can still be passed via extra_args=["--my-flag", "value"] (field of SolverParams).


Quick Nest

For the common "pack this list of Shapely polygons" use case, nest() wraps the builder + solver into one call and adds two quality-of-life features:

  1. Identical-shape grouping — duplicate Shapely polygons are collapsed into one ItemType with a copies count (compared via WKB).
  2. Origin anchoring — every input is translated to its bottom-left corner before being added to the instance.

spacing is forwarded to the solver's native --item-item-minimum-spacing.

from shapely.geometry import box, Polygon
from pyckingsolver import nest, Objective

shapes = [box(0, 0, 80, 60) for _ in range(20)]
shapes += [Polygon([(0,0), (50,0), (25,40)]) for _ in range(8)]

sol = nest(
    shapes,
    bins=(1200, 600),                 # or a Polygon, or a list of either
    objective=Objective.BIN_PACKING,
    spacing=2.0,                      # 2mm kerf, enforced natively by the solver
    allowed_rotations=[(0, 0), (90, 90)],
    bin_copies=10,
    time_limit=30,
)

for it in sol.all_items():
    poly = sol.placed_shapes(it)[0]   # already mirrored/rotated/translated
    print(it.item_type_id, poly.bounds)

nest() accepts the same keyword arguments as Solver.solve() (e.g. time_limit, params=SolverParams(...), json_output, on_improvement).

Pass any CLI flag directly for new solver features:

solution = solver.solve(instance, extra_args=["--some-new-flag", "value"])

Solution

solution.total_item_count()     # int: total items placed
solution.total_bins_used()      # int: total bins used
solution.all_items()            # list[SolutionItem]: flat, across all bins

for sbin in solution.bins:
    sbin.bin_type_id            # which bin type
    sbin.copies                 # copies of this bin used
    sbin.items                  # list[SolutionItem]

for item in solution.all_items():
    item.item_type_id           # which item type
    item.x, item.y              # placement position
    item.angle                  # rotation in degrees
    item.mirror                 # bool: mirrored?
    item.shapes                 # list[Polygon] — absolute coordinates, ready to use

Solver Metrics

After solving, solution.metrics contains statistics from the C++ solver:

solution = solver.solve(instance, time_limit=30)

print(solution.metrics)
# {
#     "NumberOfItems": 16,
#     "ItemArea": 48000.0,
#     "ItemProfit": 48000.0,
#     "NumberOfBins": 1,
#     "BinArea": 720000.0,
#     "BinCost": 720000.0,
#     "FullWaste": 672000.0,
#     "FullWastePercentage": 93.33,
#     "XMax": 1200.0,
#     "YMax": 600.0,
#     "DensityX": 0.067,
#     "DensityY": 0.133,
#     "LeftoverValue": 0.0,
#     ...
# }

# Access individual metrics
waste_pct = solution.metrics.get("FullWastePercentage", 0)
density_x = solution.metrics.get("DensityX", 0)

Export to DXF / SVG / other formats

import ezdxf  # pip install ezdxf

doc = ezdxf.new()
msp = doc.modelspace()
for item in solution.all_items():
    for poly in item.shapes:
        pts = list(poly.exterior.coords)
        msp.add_lwpolyline(pts, close=True)
doc.saveas("output.dxf")

JSON I/O

Compatible with the C++ solver's JSON format:

# Save/load instance
instance.to_json("problem.json")
instance = Instance.from_json("problem.json")

# Dict round-trip (for custom serialization)
d = instance.to_dict()
instance = Instance.from_dict(d)

# Load / save solution
solution = Solution.from_json("solution.json")

Geometry Helpers

from pyckingsolver import (
    shape_to_json,          # Shapely Polygon → solver JSON dict
    shape_from_json,        # solver JSON dict → Shapely Polygon
    elements_to_polygon,    # line-segment + arc elements → Shapely Polygon
    circle_polygon,         # circle → Shapely polygon
    rectangle_polygon,      # rectangle → Shapely polygon
)
from shapely.geometry import Polygon

# Export any Shapely polygon to solver JSON (CCW winding enforced automatically)
data = shape_to_json(Polygon([(0, 0), (50, 0), (25, 40)]))   # {"type": "polygon", ...}

# Parse a solver shape dict back to Shapely
poly = shape_from_json({"type": "rectangle", "width": 80, "height": 40})

# Convert arc-based C++ solution geometry to Shapely
poly = elements_to_polygon(elements, arc_resolution=64)

# Circle / rectangle → Shapely polygon (the form the solver accepts)
circle = circle_polygon(radius=50, center=(100, 100), resolution=64)
rect = rectangle_polygon(80, 40)

Building the Solver

Pre-built binaries are bundled in the pip wheel for Windows x64 and Linux x64.

For other platforms, build from the included submodule:

git clone --recurse-submodules https://github.com/HamzaYslmn/pyckingsolver.git
cd pyckingsolver/extern/packingsolver

# Ubuntu: sudo apt-get install liblapack-dev libbz2-dev
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --config Release --parallel

# Binary location:
# Linux:   extern/packingsolver/build/src/irregular/packingsolver_irregular
# Windows: extern/packingsolver/build/src/irregular/Release/packingsolver_irregular.exe

Then point the solver at it:

solver = Solver(binary="extern/packingsolver/build/src/irregular/packingsolver_irregular")

Updating the C++ Solver

git -C extern/packingsolver pull origin master
git add extern/packingsolver
git commit -m "Update solver submodule"

How It Works

Python (Shapely)  →  JSON  →  C++ Solver  →  JSON  →  Python (Shapely)
  InstanceBuilder   instance   optimize     solution    Solution
  1. Build — define bins and items as Shapely Polygons via InstanceBuilder
  2. Serialize — convert to PackingSolver JSON (CCW winding enforced, holes as interior rings)
  3. Solve — C++ solver runs branch-and-bound / heuristics
  4. Parse — placed items returned as Shapely geometries in absolute coordinates

The C++ solver (fontanf/packingsolver) also supports rectangle, box (3D), guillotine cut, and 1D packing — accessible by passing problem_type to Solver().


License

MIT — see LICENSE.

Based on PackingSolver by Florian Fontan.

Metadata

Release files for pyckingsolver 0.8.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Built distributions (wheels)

Table of built distributions (wheels) for pyckingsolver 0.8.0
File
pyckingsolver-0.8.0-py3-none-win_amd64.whl Python 3 none Windows x86-64 Details
pyckingsolver-0.8.0-py3-none-manylinux_2_35_x86_64.whl Python 3 none Linux glibc 2.35+ x86-64 Details
pyckingsolver-0.8.0-py3-none-manylinux_2_35_aarch64.whl Python 3 none Linux glibc 2.35+ ARM64 Details
pyckingsolver-0.8.0-py3-none-macosx_11_0_arm64.whl Python 3 none macOS 11.0+ ARM64 Details

Total release size: 12.9 MB

Release files / pyckingsolver-0.8.0-py3-none-win_amd64.whl

Download URL pyckingsolver-0.8.0-py3-none-win_amd64.whl
Size 2.7 MB
Tags Python 3 Windows x86-64
SHA-256 checksum
How to use checksums
231a5d39cace032de996d39f955a8a23cea23bf1f2db727f1c48828e48a53b21
BLAKE2b-256 checksum
How to use checksums
caf43e9427ac62a59204ee67159209868fa8695d5da24f185639f4f3727b4180
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 3, 2026.

Transparency log

Release files / pyckingsolver-0.8.0-py3-none-manylinux_2_35_x86_64.whl

Download URL pyckingsolver-0.8.0-py3-none-manylinux_2_35_x86_64.whl
Size 3.7 MB
Tags Linux glibc 2.35+ x86-64 Python 3
SHA-256 checksum
How to use checksums
ab436e099636a06a9da53acaa220d74f99ffbf7c2bfff064302c12c32dc36144
BLAKE2b-256 checksum
How to use checksums
2d487f4b4ff90339b2b56cb68780af271d1ec31ada0f1c56502c5c0886242dcb
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 3, 2026.

Transparency log

Release files / pyckingsolver-0.8.0-py3-none-manylinux_2_35_aarch64.whl

Download URL pyckingsolver-0.8.0-py3-none-manylinux_2_35_aarch64.whl
Size 3.4 MB
Tags Linux glibc 2.35+ ARM64 Python 3
SHA-256 checksum
How to use checksums
d8229bed28065396d24c397330e2c70734122974b67c46dbe19a07938fe80e08
BLAKE2b-256 checksum
How to use checksums
3d85ba7c87fdf3bf0916e57b3077c98831bd6bee3c15a9b216824779d42b16d7
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 3, 2026.

Transparency log

Release files / pyckingsolver-0.8.0-py3-none-macosx_11_0_arm64.whl

Download URL pyckingsolver-0.8.0-py3-none-macosx_11_0_arm64.whl
Size 3.1 MB
Tags Python 3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
81586896f37b9efab11e4b721f9f64fa34297b090b80a933a466b1885358aac0
BLAKE2b-256 checksum
How to use checksums
ef84c1d4cac5c8e593a4d0adc1f0828a70b9d5becd2d17e9a8a2387439bc0050
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 3, 2026.

Transparency log

Security for pyckingsolver 0.8.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 4 files in this release have been uploaded with provenance metadata.

Select from 4 files

Attestation for pyckingsolver-0.8.0-py3-none-win_amd64.whl

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 3, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page