Metadata-Version: 2.4
Name: linearcutting
Version: 1.0.0
Summary: 1D cut list optimiser — cut steel, timber, tube and extrusion with the least waste, fewest bars, or fewest saw setups.
Project-URL: Homepage, https://linearcutting.com
Project-URL: Documentation, https://linearcutting.com/api-docs
Project-URL: Source, https://github.com/jack-empyreal/linearcutting-python
Author-email: LinearCutting <help@linearcutting.com>
License: MIT
License-File: LICENSE
Keywords: 1d bin packing,cut list,cutting stock,fabrication,kerf,linear cutting,nesting,optimisation,optimization,steel,timber
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Manufacturing
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Scientific/Engineering :: Mathematics
Classifier: Typing :: Typed
Requires-Python: >=3.9
Description-Content-Type: text/markdown

# 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.

```bash
pip install linearcutting
```

```python
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 public tier is
rate limited, not gated, 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 stop settings

  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:

```python
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.

```python
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.

```python
plan = lc.optimise(parts=cut_list, stock=6000, method="cutters_dream")
plan.stop_settings   # the number of measurements the operator dials in
```

On a real 960-piece job this took the stop settings 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. |
| `cutters_dream` | Saw stop settings — operator setup time. |
| `fewest_layouts` | The number of distinct cutting patterns. |

## Use the offcuts on your rack

```python
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:

```python
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:

```python
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)
```

Give prices and the optimiser minimises **money** instead of millimetres — which
is not the same plan, because a 6000 bar is rarely 1.5× the price of a 4000.

```python
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

```python
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](https://linearcutting.com/pricing).

## Cutting sheet as a PDF

```python
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.

```python
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](https://linearcutting.com/benchmarks)
· [raw dataset](https://linearcutting.com/benchmarks/data.json)

A benchmark you cannot reproduce is marketing.

## Links

- [Full API documentation](https://linearcutting.com/api-docs)
- [The free browser calculator](https://linearcutting.com)
- help@linearcutting.com

MIT licensed.
