Skip to main content

1D cut list optimiser — cut steel, timber, tube and extrusion with the least waste, fewest bars, or fewest saw setups.

Project description

linearcutting

1D cut list optimiser for Python. Work out how to cut a list of pieces from stock lengths with the least waste, the fewest bars, or the fewest saw setups — steel, timber, tube, extrusion, pipe. Any material that is long and gets cut across.

pip install linearcutting
import linearcutting as lc

plan = lc.optimise(parts="2400x5, 1800x3", stock=6000)

print(plan.bars_used)   # 4
print(plan.optimal)     # True  — proved, not guessed
print(plan.summary())

That runs as written. No API key, no account, no signup. The free public tier allows 30 requests an hour and 250 a calendar month per IP, because you should be able to evaluate a thing before you pay for it.

4 bars, 6600mm waste (72.5% yield) — provably optimal, no better plan exists
3 distinct layouts, 4 saw setups

  6000 (stock): 2400 + 1800 + 1800  [waste 0]
  6000 (stock): 2400 + 2400  [waste 1200] ×2
  6000 (stock): 1800  [waste 4200]

Zero dependencies. Nothing to conflict with, nothing to keep up to date.


Why not just sort longest-first?

Because that is provably not optimal, and often not close. Cutting a list of pieces from stock is the 1D cutting stock problem, and no greedy rule (longest-first, best-fit, first-fit-decreasing) reliably finds the best plan.

LinearCutting runs a branch-and-bound search against a mathematical lower bound. That gives you something a greedy heuristic never can:

plan.optimal            # True: no better plan EXISTS. Not "none was found".
plan.bars_lower_bound   # a relaxation bound — see the warning below

When a job is too big to prove in the time budget, optimal is False. That means "not proved", not "not good" — it does not pretend either way.

optimal and bars_lower_bound disagree on purpose. The bound is roughly total length ÷ bar length, and it is often not achievable. Five 2400s and three 1800s is 17,400mm — 2.9 bars of 6000 — so the bound says 3. You cannot cut those pieces from 3 bars; 4 is the true minimum, and the solver proves it. So bars_used=4 with bars_lower_bound=3 and optimal=True is not a contradiction: the bound was loose, not the plan. Trust optimal. Treat the bound as a sanity check, never as a target.

Every plan is also verified before it is returned — re-audited against your inputs, so a plan that does not actually cut your list cannot reach you.

Mind the kerf

The blade destroys material. A 6000mm bar does not hold 2400 + 2400 + 1200 once you account for a 3mm blade, and a plan that ignores that fails at the saw.

plan = lc.optimise(parts="2400x2, 1200", stock=6000, kerf=3.2)

Get this number right. It is the single most common way a cut list goes wrong.

Optimise for the saw, not just the steel

Most optimisers minimise waste. Waste is not the only cost: every distinct layout is a fence reset, and on a manual saw the setups can cost more than the offcut ever did.

plan = lc.optimise(parts=cut_list, stock=6000, method="fewest_setups")
plan.setups   # the number of measurements the operator dials in

On a real 960-piece job this took the setups from 150 down to 97 — a 35% cut in fence resets — for three extra bars. Whether that trade is worth taking depends on whether your bottleneck is steel or setup time, which is exactly why it is a choice and not a default.

Method What it minimises
balanced Fewest bars, then least waste. The default, and right for most jobs.
least_waste Material consumed.
offcuts_first Buys new stock only after the remnant rack is used up.
fewest_setups Saw setups — the times the operator moves the stop.

Use the offcuts on your rack

plan = lc.optimise(
    parts="2400x5, 1800x3",
    stock=6000,
    offcuts=[3200, 1900, (1200, 2)],    # (length, quantity)
    method="offcuts_first",
)

for layout in plan.layouts:
    if layout.from_offcut:
        print("from the rack:", layout)

plan.unused_offcuts     # what is still on the rack afterwards
plan.usable_remnants    # new offcuts this job creates, worth keeping

Writing a cut list

All of these mean the same thing. Use whichever suits the code you are in:

parts = "2400x5, 1800x3"                    # a string
parts = ["2400x5", "1800x3"]                # a list of strings
parts = [(2400, 5), (1800, 3)]              # (length, quantity)
parts = [(2400, 5, "rail"), (1800, 3, "stile")]   # ...with labels
parts = [{"length": 2400, "quantity": 5}]   # the long form

Stock is the same, and a third element is a price rather than a label:

stock = 6000                       # this length, buy as many as needed
stock = [6000, 4000]               # choose between lengths
stock = [(6000, 20, 45.50)]        # (length, how many you have, price each)

Prices only take effect with method="cheapest", which minimises money instead of millimetres — a different plan, because a 6000 bar is rarely 1.5× the price of a 4000. Every other method ignores prices (and reports no cost), so balanced can never quietly turn into a money optimiser behind your back.

plan = client.optimise(parts=parts, stock=stock, method="cheapest")
plan.total_cost
plan.cost_lower_bound
for buy in plan.purchases:
    print(f"{buy.count} × {buy.length}mm = {buy.cost}")

With an API key

client = lc.Client(api_key="lk_live_...")
plan = client.optimise(parts="2400x5", stock=6000)

A key lifts the rate limit, raises the size caps, and gives you your own concurrency, so a busy afternoon on the public tier cannot slow you down. Plans start at $5/month for 500 requests and run to $99/month for 30,000 — about $3.30 per 1,000 requests. See linearcutting.com/pricing.

Cutting sheet as a PDF

sheet = client.pdf(parts="2400x5", stock=6000, job_name="Henderson balustrade")
open("cutting-sheet.pdf", "wb").write(sheet)

Errors

Error messages are written for people. Print them.

from linearcutting import ValidationError, RateLimited

try:
    plan = lc.optimise(parts="7000", stock=6000)
except ValidationError as error:
    print(error)      # says what was wrong and what was expected
except RateLimited as error:
    time.sleep(error.retry_after)

ValidationError, AuthError, RateLimited (with .retry_after) and ServerError all inherit from LinearCuttingError. 429s and 503s are retried automatically, honouring the server's Retry-After.

What this is not

1D only. Linear material, cut across its length. It does not do 2D sheet or panel nesting, plate, glass, or DXF and beam-saw export. If your material is flat and you cut shapes out of it, this is the wrong tool and you should use something else.

Benchmarks

Three real jobs, every method, with the input cut lists published so you can re-run them: linearcutting.com/benchmarks · raw dataset

A benchmark you cannot reproduce is marketing.

Links

MIT licensed.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

linearcutting-1.1.0.tar.gz (12.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

linearcutting-1.1.0-py3-none-any.whl (11.9 kB view details)

Uploaded Python 3

File details

Details for the file linearcutting-1.1.0.tar.gz.

File metadata

  • Download URL: linearcutting-1.1.0.tar.gz
  • Upload date:
  • Size: 12.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for linearcutting-1.1.0.tar.gz
Algorithm Hash digest
SHA256 efd37f23767aeca1112024e7c7a57661d54a2173698e24352160274ad0d30ba7
MD5 0ad613548ad6bc941ed930e603a21619
BLAKE2b-256 b917cb6e9da438993a59583b7636fb5698faff72add782c27e93d4f6a59f7ff7

See more details on using hashes here.

File details

Details for the file linearcutting-1.1.0-py3-none-any.whl.

File metadata

  • Download URL: linearcutting-1.1.0-py3-none-any.whl
  • Upload date:
  • Size: 11.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for linearcutting-1.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 0971783f11f2816ded97b493c9c9a23987ab79b815736a5567d16b6b40ab6111
MD5 4774abf3223e8fd55e2816460261dfd8
BLAKE2b-256 1768e59bfbf9327e8eaa4bcbecad7bdafdadeabd547ef37a61a0306e9ad3fd62

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page