fastrectpack 0.1.0__cp313-cp313-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,28 @@
1
+ """fastrectpack: a drop-in accelerator for rectpack (https://github.com/secnot/rectpack).
2
+
3
+ Use it exactly like rectpack:
4
+
5
+ from fastrectpack import newPacker
6
+ packer = newPacker()
7
+ ...
8
+
9
+ Everything rectpack exports is re-exported unchanged, except the MaxRects algorithms, which run in C++ when
10
+ all sizes are plain Python ints. Any other input is handled by the original rectpack code.
11
+ """
12
+ from rectpack import * # noqa: F401,F403 (re-export the full original API)
13
+ from rectpack import SORT_AREA, PackingBin, PackingMode
14
+ from rectpack import newPacker as _newPacker
15
+
16
+ from ._maxrects import NATIVE_AVAILABLE as NATIVE_AVAILABLE
17
+ from ._maxrects import MaxRectsBaf as MaxRectsBaf
18
+ from ._maxrects import MaxRectsBl as MaxRectsBl
19
+ from ._maxrects import MaxRectsBlsf as MaxRectsBlsf
20
+ from ._maxrects import MaxRectsBssf as MaxRectsBssf
21
+
22
+ __version__ = "0.1.0"
23
+
24
+
25
+ def newPacker(mode=PackingMode.Offline, bin_algo=PackingBin.BBF, pack_algo=MaxRectsBssf,
26
+ sort_algo=SORT_AREA, rotation=True):
27
+ """Same signature and behaviour as rectpack.newPacker; the default algorithm is the accelerated one."""
28
+ return _newPacker(mode=mode, bin_algo=bin_algo, pack_algo=pack_algo, sort_algo=sort_algo, rotation=rotation)
@@ -0,0 +1,105 @@
1
+ """Native-backed drop-in replacements for rectpack's MaxRects* classes.
2
+
3
+ Each class subclasses the original (so isinstance checks and every method we do not override still work),
4
+ runs the algorithm in C++ while all dimensions are plain Python ints, and falls back to the original Python
5
+ code, permanently for that object, the first time it sees anything else (floats, Decimals, numpy ints, bools,
6
+ zero or negative sizes, huge ints). The fallback is the original code, so results and errors are the same.
7
+ """
8
+ from rectpack import maxrects as _orig
9
+ from rectpack.geometry import Rectangle
10
+ from rectpack.pack_algo import PackingAlgorithm
11
+
12
+ try:
13
+ from . import _native
14
+ except ImportError: # extension not built: everything below degrades to the original behaviour
15
+ _native = None
16
+
17
+ NATIVE_AVAILABLE = _native is not None
18
+
19
+
20
+ class _NativeMaxRects:
21
+ _variant = 0 # overridden by each concrete class
22
+
23
+ def reset(self):
24
+ PackingAlgorithm.reset(self) # self.rectangles = []
25
+ self.__dict__.pop("_max_rects", None)
26
+ core = None
27
+ if _native is not None:
28
+ try:
29
+ core = _native.Core(self.width, self.height, bool(self.rot), self._variant)
30
+ except _native.Deopt:
31
+ core = None
32
+ self._native = core
33
+ if core is None:
34
+ self._max_rects = [Rectangle(0, 0, self.width, self.height)]
35
+
36
+ def _deopt(self):
37
+ """Hand the current state over to the original Python implementation for good."""
38
+ core = self._native
39
+ self._max_rects = [Rectangle(*t) for t in core.free_rects()]
40
+ self._native = None
41
+
42
+ def __getattr__(self, name):
43
+ # Only reached when normal lookup fails: lets code that peeks at _max_rects keep working.
44
+ if name == "_max_rects":
45
+ core = self.__dict__.get("_native")
46
+ if core is not None:
47
+ return [Rectangle(*t) for t in core.free_rects()]
48
+ raise AttributeError(name)
49
+
50
+ def fitness(self, width, height):
51
+ core = self._native
52
+ if core is not None:
53
+ try:
54
+ return core.fitness(width, height)
55
+ except _native.Deopt:
56
+ self._deopt()
57
+ return super().fitness(width, height)
58
+
59
+ def add_rect(self, width, height, rid=None):
60
+ core = self._native
61
+ if core is not None:
62
+ try:
63
+ placed = core.add_rect(width, height)
64
+ except _native.Deopt:
65
+ self._deopt()
66
+ else:
67
+ if placed is None:
68
+ return None
69
+ rect = Rectangle(*placed)
70
+ rect.rid = rid
71
+ self.rectangles.append(rect)
72
+ return rect
73
+ return super().add_rect(width, height, rid)
74
+
75
+ # pickle / copy.deepcopy: the C++ object cannot be pickled, so serialise as plain Python state
76
+ def __getstate__(self):
77
+ state = dict(self.__dict__)
78
+ core = state.pop("_native", None)
79
+ if core is not None:
80
+ state["_max_rects"] = [Rectangle(*t) for t in core.free_rects()]
81
+ state["_native"] = None
82
+ return state
83
+
84
+
85
+ _BL = _native.BL if _native else 0
86
+ _BSSF = _native.BSSF if _native else 1
87
+ _BAF = _native.BAF if _native else 2
88
+ _BLSF = _native.BLSF if _native else 3
89
+
90
+
91
+ class MaxRectsBl(_NativeMaxRects, _orig.MaxRectsBl):
92
+ _variant = _BL
93
+
94
+
95
+ class MaxRectsBssf(_NativeMaxRects, _orig.MaxRectsBssf):
96
+ """Best Short Side Fit (the rectpack default)."""
97
+ _variant = _BSSF
98
+
99
+
100
+ class MaxRectsBaf(_NativeMaxRects, _orig.MaxRectsBaf):
101
+ _variant = _BAF
102
+
103
+
104
+ class MaxRectsBlsf(_NativeMaxRects, _orig.MaxRectsBlsf):
105
+ _variant = _BLSF
Binary file
@@ -0,0 +1,244 @@
1
+ // Native core for the MaxRects family of rectpack (https://github.com/secnot/rectpack, Apache-2.0).
2
+ //
3
+ // This is a port of rectpack.maxrects (MaxRectsBl / Bssf / Baf / Blsf) for plain Python int dimensions.
4
+ // It reproduces the original's behaviour exactly, including its quirks:
5
+ // * candidates are scanned in free-rectangle order, normal orientation first, then rotated, and the FIRST
6
+ // minimal candidate wins (Python's min() semantics);
7
+ // * MaxRectsBl compares only (y + height); its docstring promises an x tie-break but the code does not do it;
8
+ // * _remove_duplicates removes every free rectangle whose *value* equals that of a contained rectangle, so two
9
+ // identical free rectangles are both dropped.
10
+ //
11
+ // Anything outside the exact-int fast path raises Deopt and the Python wrapper falls back to the original code.
12
+
13
+ #include <pybind11/pybind11.h>
14
+
15
+ #include <algorithm>
16
+ #include <cstdint>
17
+ #include <memory>
18
+ #include <stdexcept>
19
+ #include <tuple>
20
+ #include <vector>
21
+
22
+ namespace py = pybind11;
23
+ using i64 = std::int64_t;
24
+
25
+ namespace {
26
+
27
+ // Keep every intermediate (sums, and products used by Baf) far from int64 overflow.
28
+ constexpr i64 kLimit = i64(1) << 30;
29
+
30
+ struct Deopt : std::runtime_error {
31
+ Deopt() : std::runtime_error("input outside the native fast path") {}
32
+ };
33
+
34
+ struct Rect {
35
+ i64 x, y, w, h;
36
+ i64 top() const { return y + h; }
37
+ i64 right() const { return x + w; }
38
+ bool operator==(const Rect& o) const { return x == o.x && y == o.y && w == o.w && h == o.h; }
39
+ bool operator<(const Rect& o) const { return std::tie(x, y, w, h) < std::tie(o.x, o.y, o.w, o.h); }
40
+ };
41
+
42
+ enum Variant : int { kBl = 0, kBssf = 1, kBaf = 2, kBlsf = 3 };
43
+
44
+ // Exact Python int only (not bool, not numpy ints, not floats). Returns false when not representable.
45
+ bool as_i64(PyObject* o, i64& out) {
46
+ if (!PyLong_CheckExact(o)) return false;
47
+ int overflow = 0;
48
+ long long v = PyLong_AsLongLongAndOverflow(o, &overflow);
49
+ if (overflow != 0) return false;
50
+ if (v == -1 && PyErr_Occurred()) {
51
+ PyErr_Clear();
52
+ return false;
53
+ }
54
+ out = static_cast<i64>(v);
55
+ return true;
56
+ }
57
+
58
+ // rectpack Rectangle.contains(): `a` contains `b`
59
+ inline bool contains(const Rect& a, const Rect& b) {
60
+ return b.y >= a.y && b.x >= a.x && b.top() <= a.top() && b.right() <= a.right();
61
+ }
62
+
63
+ class Core {
64
+ public:
65
+ Core(i64 width, i64 height, bool rot, int variant) : rot_(rot), variant_(static_cast<Variant>(variant)) {
66
+ free_.push_back(Rect{0, 0, width, height});
67
+ }
68
+
69
+ // Returns fitness (>= 0), or -1 when the rectangle cannot be placed.
70
+ i64 fitness(i64 w, i64 h) const {
71
+ Rect placed;
72
+ size_t idx;
73
+ if (!select(w, h, placed, idx)) return -1;
74
+ return rect_fitness(free_[idx], placed.w, placed.h);
75
+ }
76
+
77
+ bool add_rect(i64 w, i64 h, Rect& placed) {
78
+ size_t idx;
79
+ if (!select(w, h, placed, idx)) return false;
80
+ split(placed);
81
+ return true;
82
+ }
83
+
84
+ const std::vector<Rect>& free_rects() const { return free_; }
85
+
86
+ private:
87
+ // Mirrors MaxRects*._rect_fitness for the variant. Caller guarantees (cw, ch) fits inside m.
88
+ i64 rect_fitness(const Rect& m, i64 cw, i64 ch) const {
89
+ switch (variant_) {
90
+ case kBssf:
91
+ return std::min(m.w - cw, m.h - ch);
92
+ case kBaf:
93
+ return m.w * m.h - cw * ch;
94
+ case kBlsf:
95
+ return std::max(m.w - cw, m.h - ch);
96
+ default:
97
+ return 0; // MaxRectsBl uses the base-class fitness
98
+ }
99
+ }
100
+
101
+ // Mirrors MaxRects*._select_position: first minimal candidate, normal orientation before rotated.
102
+ bool select(i64 w, i64 h, Rect& placed, size_t& idx) const {
103
+ bool have = false;
104
+ i64 best_key = 0;
105
+ for (size_t i = 0; i < free_.size(); ++i) {
106
+ const Rect& m = free_[i];
107
+ if (w <= m.w && h <= m.h) {
108
+ i64 key = (variant_ == kBl) ? m.y + h : rect_fitness(m, w, h);
109
+ if (!have || key < best_key) {
110
+ have = true;
111
+ best_key = key;
112
+ placed = Rect{m.x, m.y, w, h};
113
+ idx = i;
114
+ }
115
+ }
116
+ }
117
+ if (rot_) {
118
+ for (size_t i = 0; i < free_.size(); ++i) {
119
+ const Rect& m = free_[i];
120
+ if (h <= m.w && w <= m.h) {
121
+ i64 key = (variant_ == kBl) ? m.y + w : rect_fitness(m, h, w);
122
+ if (!have || key < best_key) {
123
+ have = true;
124
+ best_key = key;
125
+ placed = Rect{m.x, m.y, h, w};
126
+ idx = i;
127
+ }
128
+ }
129
+ }
130
+ }
131
+ return have;
132
+ }
133
+
134
+ // Mirrors MaxRects._split followed by MaxRects._remove_duplicates.
135
+ void split(const Rect& r) {
136
+ std::vector<Rect> next;
137
+ std::vector<char> is_new;
138
+ next.reserve(free_.size() + 8);
139
+ is_new.reserve(free_.size() + 8);
140
+ for (const Rect& m : free_) {
141
+ bool intersects = !(m.y >= r.top() || m.top() <= r.y || m.x >= r.right() || m.right() <= r.x);
142
+ if (!intersects) {
143
+ next.push_back(m);
144
+ is_new.push_back(0);
145
+ continue;
146
+ }
147
+ // _generate_splits, same order: left, right, top, bottom
148
+ if (r.x > m.x) {
149
+ next.push_back(Rect{m.x, m.y, r.x - m.x, m.h});
150
+ is_new.push_back(1);
151
+ }
152
+ if (r.right() < m.right()) {
153
+ next.push_back(Rect{r.right(), m.y, m.right() - r.right(), m.h});
154
+ is_new.push_back(1);
155
+ }
156
+ if (r.top() < m.top()) {
157
+ next.push_back(Rect{m.x, r.top(), m.w, m.top() - r.top()});
158
+ is_new.push_back(1);
159
+ }
160
+ if (r.y > m.y) {
161
+ next.push_back(Rect{m.x, m.y, m.w, r.y - m.y});
162
+ is_new.push_back(1);
163
+ }
164
+ }
165
+
166
+ // _remove_duplicates. The original drops every rectangle whose VALUE equals the value of some rectangle
167
+ // that is contained by another one. Because the list it leaves behind never contains a containing pair,
168
+ // only pairs that involve a newly created piece can add to that set, so checking those is equivalent
169
+ // to the original's all-pairs scan (verified against the original by the parity tests).
170
+ std::vector<Rect> flagged;
171
+ const size_t n = next.size();
172
+ for (size_t i = 0; i < n; ++i) {
173
+ if (!is_new[i]) continue;
174
+ for (size_t j = 0; j < n; ++j) {
175
+ if (j == i) continue;
176
+ if (contains(next[i], next[j])) flagged.push_back(next[j]);
177
+ if (contains(next[j], next[i])) flagged.push_back(next[i]);
178
+ }
179
+ }
180
+ if (flagged.empty()) {
181
+ free_.swap(next);
182
+ return;
183
+ }
184
+ std::sort(flagged.begin(), flagged.end());
185
+ flagged.erase(std::unique(flagged.begin(), flagged.end()), flagged.end());
186
+ free_.clear();
187
+ for (const Rect& m : next) {
188
+ if (!std::binary_search(flagged.begin(), flagged.end(), m)) free_.push_back(m);
189
+ }
190
+ }
191
+
192
+ bool rot_;
193
+ Variant variant_;
194
+ std::vector<Rect> free_;
195
+ };
196
+
197
+ inline void need_dim(PyObject* o, i64& out) {
198
+ if (!as_i64(o, out) || out < 1 || out > kLimit) throw Deopt();
199
+ }
200
+
201
+ std::unique_ptr<Core> make_core(py::handle width, py::handle height, bool rot, int variant) {
202
+ i64 w, h;
203
+ need_dim(width.ptr(), w);
204
+ need_dim(height.ptr(), h);
205
+ return std::make_unique<Core>(w, h, rot, variant);
206
+ }
207
+
208
+ } // namespace
209
+
210
+ PYBIND11_MODULE(_native, m) {
211
+ m.doc() = "Native core for the MaxRects family of rectpack (exact-int fast path)";
212
+ py::register_exception<Deopt>(m, "Deopt");
213
+
214
+ py::class_<Core>(m, "Core")
215
+ .def(py::init(&make_core), py::arg("width"), py::arg("height"), py::arg("rot"), py::arg("variant"))
216
+ .def("fitness",
217
+ [](const Core& c, py::handle w, py::handle h) -> py::object {
218
+ i64 iw, ih;
219
+ need_dim(w.ptr(), iw);
220
+ need_dim(h.ptr(), ih);
221
+ i64 f = c.fitness(iw, ih);
222
+ if (f < 0) return py::none();
223
+ return py::int_(f);
224
+ })
225
+ .def("add_rect",
226
+ [](Core& c, py::handle w, py::handle h) -> py::object {
227
+ i64 iw, ih;
228
+ need_dim(w.ptr(), iw);
229
+ need_dim(h.ptr(), ih);
230
+ Rect placed;
231
+ if (!c.add_rect(iw, ih, placed)) return py::none();
232
+ return py::make_tuple(placed.x, placed.y, placed.w, placed.h);
233
+ })
234
+ .def("free_rects", [](const Core& c) {
235
+ py::list out;
236
+ for (const Rect& r : c.free_rects()) out.append(py::make_tuple(r.x, r.y, r.w, r.h));
237
+ return out;
238
+ });
239
+
240
+ m.attr("BL") = static_cast<int>(kBl);
241
+ m.attr("BSSF") = static_cast<int>(kBssf);
242
+ m.attr("BAF") = static_cast<int>(kBaf);
243
+ m.attr("BLSF") = static_cast<int>(kBlsf);
244
+ }
@@ -0,0 +1,164 @@
1
+ Metadata-Version: 2.4
2
+ Name: fastrectpack
3
+ Version: 0.1.0
4
+ Summary: Drop-in C++ accelerator for rectpack's MaxRects 2D packing algorithms, with identical output
5
+ Keywords: rectpack,bin packing,rectangle packing,2d packing,nesting,pybind11,performance
6
+ Author: Ramcharan
7
+ License-Expression: Apache-2.0
8
+ License-File: LICENSE
9
+ License-File: NOTICE
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: C++
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ Project-URL: Homepage, https://github.com/p-ramcharan-dev/fastrectpack
22
+ Project-URL: Source, https://github.com/p-ramcharan-dev/fastrectpack
23
+ Project-URL: Issues, https://github.com/p-ramcharan-dev/fastrectpack/issues
24
+ Project-URL: Benchmarks, https://github.com/p-ramcharan-dev/fastrectpack/blob/main/BENCHMARKS.md
25
+ Project-URL: Original library, https://github.com/secnot/rectpack
26
+ Requires-Python: >=3.9
27
+ Requires-Dist: rectpack==0.2.2
28
+ Provides-Extra: test
29
+ Requires-Dist: pytest; extra == "test"
30
+ Requires-Dist: hypothesis; extra == "test"
31
+ Description-Content-Type: text/markdown
32
+
33
+ # fastrectpack
34
+
35
+ [![ci](https://github.com/p-ramcharan-dev/fastrectpack/actions/workflows/ci.yml/badge.svg)](https://github.com/p-ramcharan-dev/fastrectpack/actions/workflows/ci.yml)
36
+ [![wheels](https://github.com/p-ramcharan-dev/fastrectpack/actions/workflows/wheels.yml/badge.svg)](https://github.com/p-ramcharan-dev/fastrectpack/actions/workflows/wheels.yml)
37
+ ![License: Apache-2.0](https://img.shields.io/badge/license-Apache--2.0-blue)
38
+ ![Python 3.9 to 3.13](https://img.shields.io/badge/python-3.9%20to%203.13-blue)
39
+
40
+ A drop-in accelerator for [rectpack](https://github.com/secnot/rectpack), the pure-Python 2D rectangle packing library. The MaxRects packing algorithms are re-implemented in C++17 and produce **exactly the same packings** as the original, 5x to 246x faster depending on the workload.
41
+
42
+ ```python
43
+ # before
44
+ from rectpack import newPacker
45
+ # after: nothing else changes
46
+ from fastrectpack import newPacker
47
+ ```
48
+
49
+ ## Headline numbers
50
+
51
+ Default `newPacker()` settings. Times are the best recorded run; full method and all 14 cases are in [BENCHMARKS.md](https://github.com/p-ramcharan-dev/fastrectpack/blob/main/BENCHMARKS.md).
52
+
53
+ | Workload | Rectangles | Original rectpack | Pure-Python fixes only | fastrectpack (C++) | Speedup |
54
+ |---|---|---|---|---|---|
55
+ | random sizes | 1,000 | 3.525 s | 0.473 s | 0.0144 s | 246x |
56
+ | random sizes | 5,000 | 21.123 s | 3.738 s | 0.1364 s | 155x |
57
+ | few large, many small | 1,000 | 1.360 s | 0.243 s | 0.0102 s | 133x |
58
+ | sprite sizes | 2,000 | 1.500 s | 0.322 s | 0.0177 s | 85x |
59
+ | sheet cutting, 100 bins | 2,000 | 0.415 s | 0.214 s | 0.0780 s | 5x |
60
+
61
+ Two things to read from this table:
62
+
63
+ - **Part of the win needs no C++.** A better algorithm in plain Python gives 1.7x to 7.5x on its own (the "Pure-Python fixes only" column). The C++ port supplies the rest.
64
+ - **The gain depends on the workload.** Jobs that spread rectangles over many bins gain about 5x, because the bin-selection loop is still Python. See "What is not faster" below.
65
+
66
+ The benchmark machine is a laptop that drops into slow phases (probably thermal throttling), so absolute seconds vary by up to about 2.3x. Best-time speedup ratios agreed to roughly 10% between full runs.
67
+
68
+ <p align="center">
69
+ <picture>
70
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/p-ramcharan-dev/fastrectpack/main/docs/benchmarks-dark.png">
71
+ <img alt="Dumbbell chart of the speedup over the original rectpack for 14 workloads, log scale. Pure-Python fixes alone give 1.7x to 7.5x; the C++ version gives 5x to 246x, with output identical in every case. Jobs that spread rectangles over many bins gain about 5x. The same numbers are in the tables in this README and in BENCHMARKS.md." src="https://raw.githubusercontent.com/p-ramcharan-dev/fastrectpack/main/docs/benchmarks-light.png" width="760">
72
+ </picture>
73
+ </p>
74
+
75
+ ## Install
76
+
77
+ ```
78
+ pip install fastrectpack
79
+ ```
80
+
81
+ Prebuilt wheels cover CPython 3.9 to 3.13 on Linux x86_64 (manylinux), Windows x86_64 and macOS arm64 (Apple silicon). Other platforms (Linux ARM, Intel Macs, Alpine/musl) and Python 3.14 are untested and build from the source distribution, which needs CMake 3.15+ and a C++17 compiler (MSVC 2019+, GCC 9+ or Clang 10+). From a checkout: `pip install .`. `rectpack==0.2.2` is installed as a dependency.
82
+
83
+ ## Usage
84
+
85
+ Identical to rectpack:
86
+
87
+ ```python
88
+ from fastrectpack import newPacker
89
+
90
+ packer = newPacker()
91
+ for width, height in [(100, 30), (40, 60), (30, 30), (70, 70), (100, 50), (30, 30)]:
92
+ packer.add_rect(width, height)
93
+ packer.add_bin(300, 450)
94
+ packer.pack()
95
+
96
+ for bin_index, x, y, w, h, rid in packer.rect_list():
97
+ print(bin_index, x, y, w, h)
98
+ ```
99
+
100
+ `fastrectpack` re-exports everything rectpack exports. `MaxRectsBl`, `MaxRectsBssf`, `MaxRectsBaf` and `MaxRectsBlsf` are replaced by accelerated subclasses of the originals; everything else is the original object.
101
+
102
+ ## How it works
103
+
104
+ The four MaxRects classes subclass the originals and run the algorithm in a small C++17 core while every size is a plain Python `int`. Anything else falls back, for good, to the original Python code for that object.
105
+
106
+ Profiling the original showed about 90% of its time in one step that compares every pair of free rectangles after each placement. The port compares only the pairs that involve newly created rectangles, which is valid because the list left by the previous step contains no rectangle that contains another. [docs/DESIGN.md](https://github.com/p-ramcharan-dev/fastrectpack/blob/main/docs/DESIGN.md) covers the design, exactly what "identical" means, and the known gaps.
107
+
108
+ ## What is accelerated
109
+
110
+ | | Accelerated |
111
+ |---|---|
112
+ | MaxRects algorithms (Bl, Bssf, Baf, Blsf), with or without rotation | Yes |
113
+ | All bin heuristics (BNF, BFF, BBF, Global), online and offline modes, all sort orders | Yes, when used with a MaxRects algorithm |
114
+ | Sizes given as plain Python `int`, up to 2^30 | Yes |
115
+ | Sizes given as `float`, `Decimal`, numpy integers or `bool` | No: handled by the original code, same result, original speed |
116
+ | Skyline and Guillotine algorithms | No: unchanged original code |
117
+
118
+ When an accelerated object meets an input outside the fast path, it hands its state to the original Python implementation and continues there, so results and error messages stay the same.
119
+
120
+ ## What is not faster
121
+
122
+ - **Many-bin workloads.** With 100 open bins, most of the time goes to the Python loop that asks every bin how well a rectangle fits. That loop was left in Python, so the gain there is about 5x.
123
+ - **Non-integer sizes.** Floats and Decimals run at the original speed.
124
+ - **Skyline and Guillotine.** Not ported.
125
+
126
+ ## How correctness was checked
127
+
128
+ - **Exact parity.** After every single placement, the tests compare the fitness value, the placement and the complete list of free rectangles (including its order) against the original. 474 tests, including two Hypothesis property tests (300 and 150 generated cases) and all 200 packer configurations that use a MaxRects algorithm.
129
+ - **Edge cases.** Floats, Decimals, NaN, infinity, zero and negative sizes, booleans, strings, `None`, numpy integers, integers beyond the native range, exact fits, pieces larger than the bin. Results and exceptions match the original.
130
+ - **Benchmark outputs.** Every benchmark run hashes the full result of all three variants; all 14 cases are identical.
131
+ - **Sanitizers.** The suite passes under AddressSanitizer and UndefinedBehaviorSanitizer, both locally (GCC 15.2 in WSL) and on a GitHub Actions Ubuntu runner.
132
+ - **Mutation check.** Six deliberate bugs were injected into the C++. The tests caught five; the sixth (treating edge-touching rectangles as overlapping) only adds free rectangles that the next step deletes, so the output cannot change. I reasoned that through rather than proving it formally.
133
+ - **Platforms and Python versions.** In GitHub Actions the test suite ran inside every built wheel: Python 3.9, 3.10, 3.11, 3.12 and 3.13 on Linux x86_64, Windows x86_64 and macOS arm64. Locally it also passes on 3.13 and 3.11 on Windows.
134
+
135
+ Run them yourself:
136
+
137
+ ```
138
+ pip install ".[test]"
139
+ pytest
140
+ python bench/run_bench.py --quick # about a minute
141
+ python bench/run_bench.py # full benchmark, several minutes
142
+ ```
143
+
144
+ ## Behaviour inherited from rectpack
145
+
146
+ These were found while porting. They are reproduced on purpose, not fixed, so that results match ([`research/original_bugs.py`](https://github.com/p-ramcharan-dev/fastrectpack/blob/main/research/original_bugs.py) demonstrates each):
147
+
148
+ - `validate_packing()` never compares the last rectangle with the others, so an overlap involving it goes undetected.
149
+ - `MaxRectsBl` documents a smallest-x tie-break but takes the first candidate.
150
+ - With the BBF bin heuristic, online `add_rect` returns `True`/`False` instead of the rectangle.
151
+
152
+ ## Limitations
153
+
154
+ - Speed was measured on one Windows laptop only. Correctness is checked in CI on Linux, Windows and macOS (arm64), but speed on those systems has not been measured.
155
+ - The package subclasses rectpack internals and pins `rectpack==0.2.2`. A new rectpack release would need re-checking.
156
+ - An unpickled or deep-copied packer continues on the original Python code path: correct, but not accelerated.
157
+
158
+ ## Contributing and security
159
+
160
+ Issues and pull requests are welcome; see [CONTRIBUTING.md](https://github.com/p-ramcharan-dev/fastrectpack/blob/main/CONTRIBUTING.md). The most useful report is a real workload where it is still slow. Please report security problems privately, as described in [SECURITY.md](https://github.com/p-ramcharan-dev/fastrectpack/blob/main/SECURITY.md).
161
+
162
+ ## Credits and licence
163
+
164
+ Apache-2.0, the same licence as rectpack, of which this is a derivative work. rectpack is by SecNot. The MaxRects algorithms come from Jukka Jylänki, "A Thousand Ways to Pack the Bin" (2010). See [NOTICE](https://github.com/p-ramcharan-dev/fastrectpack/blob/main/NOTICE).
@@ -0,0 +1,9 @@
1
+ fastrectpack/__init__.py,sha256=Vl4TSOaJuolSCu-bwAxbl1GlagsL6TuobFVOUgK3Q1U,1225
2
+ fastrectpack/_maxrects.py,sha256=-c5p8wPvp66GodwiMX24__b8Qd9q7TN9wNu6KJhtbkM,3645
3
+ fastrectpack/_native.cp313-win_amd64.pyd,sha256=Va3GgZhXkJFhXHxiMd5gzVqwrZqj5NtdnDs_xBwKWYI,215040
4
+ fastrectpack/_native.cpp,sha256=IEPtqn2ZWco1jxbU6xsqWk1ZNoHovs26Io4HIAL67n4,9118
5
+ fastrectpack-0.1.0.dist-info/METADATA,sha256=qZp0WlZLNKv2sgBu79f4wLSiOnl_oM_4JI4yJwAT_mY,10338
6
+ fastrectpack-0.1.0.dist-info/WHEEL,sha256=bzccfTXe5NE7eB86OiS4hIwvwnRsRQgKw2c3BU4To6A,105
7
+ fastrectpack-0.1.0.dist-info/licenses/LICENSE,sha256=tAkwu8-AdEyGxGoSvJ2gVmQdcicWw3j1ZZueVV74M-E,11357
8
+ fastrectpack-0.1.0.dist-info/licenses/NOTICE,sha256=abWczgp7TPLPxsrkBfJMSZzdqINIno6YzNsVtJbjgMY,708
9
+ fastrectpack-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: scikit-build-core 1.1.2
3
+ Root-Is-Purelib: false
4
+ Tag: cp313-cp313-win_amd64
5
+
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "{}"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright {yyyy} {name of copyright owner}
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
@@ -0,0 +1,13 @@
1
+ fastrectpack
2
+ Copyright 2026 Ramcharan
3
+
4
+ This product is a derivative work of rectpack (https://github.com/secnot/rectpack),
5
+ licensed under the Apache License, Version 2.0, by its author SecNot.
6
+
7
+ Changes made: the MaxRects packing algorithms (MaxRectsBl, MaxRectsBssf, MaxRectsBaf,
8
+ MaxRectsBlsf) were re-implemented in C++17 for plain Python int sizes and are exposed
9
+ through classes that subclass the originals. All other behaviour and all other algorithms
10
+ are the unmodified rectpack code, which is installed as a dependency and re-exported.
11
+
12
+ The MaxRects family itself is described in: Jukka Jylanki, "A Thousand Ways to Pack the
13
+ Bin - A Practical Approach to Two-Dimensional Rectangle Bin Packing" (2010).