packvium 0.1.0__tar.gz
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.
- packvium-0.1.0/LICENSE +21 -0
- packvium-0.1.0/PKG-INFO +108 -0
- packvium-0.1.0/README.md +85 -0
- packvium-0.1.0/pyproject.toml +37 -0
- packvium-0.1.0/setup.cfg +4 -0
- packvium-0.1.0/src/packvium/__init__.py +70 -0
- packvium-0.1.0/src/packvium/__main__.py +18 -0
- packvium-0.1.0/src/packvium/_compat.py +42 -0
- packvium-0.1.0/src/packvium/axle_load.py +128 -0
- packvium-0.1.0/src/packvium/centre_of_mass.py +41 -0
- packvium-0.1.0/src/packvium/config.py +85 -0
- packvium-0.1.0/src/packvium/constraints.py +648 -0
- packvium-0.1.0/src/packvium/contact.py +124 -0
- packvium-0.1.0/src/packvium/effort.py +31 -0
- packvium-0.1.0/src/packvium/explain.py +127 -0
- packvium-0.1.0/src/packvium/extensions.py +285 -0
- packvium-0.1.0/src/packvium/geometry.py +159 -0
- packvium-0.1.0/src/packvium/lattice_summary.py +133 -0
- packvium-0.1.0/src/packvium/models.py +385 -0
- packvium-0.1.0/src/packvium/nested.py +30 -0
- packvium-0.1.0/src/packvium/nesting.py +77 -0
- packvium-0.1.0/src/packvium/packer.py +146 -0
- packvium-0.1.0/src/packvium/packing_sequence.py +675 -0
- packvium-0.1.0/src/packvium/policy.py +302 -0
- packvium-0.1.0/src/packvium/py.typed +0 -0
- packvium-0.1.0/src/packvium/rebalance.py +214 -0
- packvium-0.1.0/src/packvium/result.py +284 -0
- packvium-0.1.0/src/packvium/serialization.py +202 -0
- packvium-0.1.0/src/packvium/solvers.py +2297 -0
- packvium-0.1.0/src/packvium/spatial_index.py +93 -0
- packvium-0.1.0/src/packvium/support_polygon.py +77 -0
- packvium-0.1.0/src/packvium/trace.py +52 -0
- packvium-0.1.0/src/packvium/units.py +182 -0
- packvium-0.1.0/src/packvium/validation.py +234 -0
- packvium-0.1.0/src/packvium.egg-info/PKG-INFO +108 -0
- packvium-0.1.0/src/packvium.egg-info/SOURCES.txt +62 -0
- packvium-0.1.0/src/packvium.egg-info/dependency_links.txt +1 -0
- packvium-0.1.0/src/packvium.egg-info/entry_points.txt +2 -0
- packvium-0.1.0/src/packvium.egg-info/top_level.txt +1 -0
- packvium-0.1.0/tests/test_axle_load.py +85 -0
- packvium-0.1.0/tests/test_centre_of_mass.py +69 -0
- packvium-0.1.0/tests/test_cli.py +100 -0
- packvium-0.1.0/tests/test_compat.py +52 -0
- packvium-0.1.0/tests/test_constraints.py +977 -0
- packvium-0.1.0/tests/test_decision_trace.py +120 -0
- packvium-0.1.0/tests/test_explain.py +102 -0
- packvium-0.1.0/tests/test_extensions.py +260 -0
- packvium-0.1.0/tests/test_geometry.py +202 -0
- packvium-0.1.0/tests/test_invariants.py +525 -0
- packvium-0.1.0/tests/test_models.py +274 -0
- packvium-0.1.0/tests/test_nested.py +90 -0
- packvium-0.1.0/tests/test_nesting.py +64 -0
- packvium-0.1.0/tests/test_objective.py +541 -0
- packvium-0.1.0/tests/test_packing.py +795 -0
- packvium-0.1.0/tests/test_packing_sequence.py +776 -0
- packvium-0.1.0/tests/test_policy.py +335 -0
- packvium-0.1.0/tests/test_rebalance.py +236 -0
- packvium-0.1.0/tests/test_serialization.py +450 -0
- packvium-0.1.0/tests/test_solvers.py +1306 -0
- packvium-0.1.0/tests/test_spatial_index.py +115 -0
- packvium-0.1.0/tests/test_support_polygon.py +104 -0
- packvium-0.1.0/tests/test_units.py +193 -0
- packvium-0.1.0/tests/test_unsupported_fields.py +77 -0
- packvium-0.1.0/tests/test_validation.py +680 -0
packvium-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Packvium contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
packvium-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: packvium
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Deterministic, extensible 3D cartonization and rectangular bin-packing library
|
|
5
|
+
Author: Packvium contributors
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: 3d-bin-packing,cartonization,packaging,container-loading
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Python: >=3.9
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# Packvium for Python
|
|
25
|
+
|
|
26
|
+
Deterministic 3D cartonization and rectangular bin packing. Pure Python, **no runtime
|
|
27
|
+
dependencies**, exact integer geometry.
|
|
28
|
+
|
|
29
|
+
> **Version 0.1.0 — early release.** The public API is not frozen; pin an exact version.
|
|
30
|
+
> Read [docs/GUARANTEES.md](docs/GUARANTEES.md) before relying on a result.
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install packvium
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Quick start
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from packvium import Container, Dimensions, Item, Packer, PackingConfig
|
|
40
|
+
|
|
41
|
+
result = Packer(PackingConfig.balanced()).pack(
|
|
42
|
+
items=[Item.create("book", Dimensions.mm("210", "140", "30"), quantity=4)],
|
|
43
|
+
containers=[Container.create("box", Dimensions.mm("400", "300", "250"))],
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
print(result.status) # feasible
|
|
47
|
+
for container in result.containers:
|
|
48
|
+
for placement in container.placements:
|
|
49
|
+
print(placement.item_id, placement.position, placement.orientation)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Fractional inches are exact, not approximated:
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
Dimensions.inches("12 3/8", "8 1/2", "3/4")
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
There is also a CLI that reads a JSON request on standard input:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
echo '{"items":[{"id":"box","quantity":8,"dimensions":{"length":"50","width":"50","height":"50"}}],
|
|
62
|
+
"containers":[{"id":"carton","inner_dimensions":{"length":"100","width":"100","height":"100"}}]}' \
|
|
63
|
+
| python -m packvium
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## What it does
|
|
67
|
+
|
|
68
|
+
- **Exact arithmetic.** Length is measured in ticks of 1/16000 mm and weight in 1/8 µg.
|
|
69
|
+
No coordinate is ever a float, so no placement decision depends on rounding.
|
|
70
|
+
- **Real constraints.** Weight and payload limits, permitted rotations, keep-upright,
|
|
71
|
+
floor-only, non-stackable, top-load limits, minimum support ratio, tag incompatibility,
|
|
72
|
+
clearance and rectangular obstacles.
|
|
73
|
+
- **A solver portfolio, not one algorithm.** Regular-grid, layer, extreme-point,
|
|
74
|
+
maximal-space and bounded exact search, selected by problem shape and profile.
|
|
75
|
+
- **Answers you can check.** Every solution is re-validated by logic independent of the
|
|
76
|
+
search. Unplaced items come back with a reason code, not silently missing.
|
|
77
|
+
- **Deterministic.** The same input and seed produce the same result, always.
|
|
78
|
+
- **Multi-container and nested.** Split across containers, or pack containers into
|
|
79
|
+
containers.
|
|
80
|
+
- **Extensible.** Register your own constraints, item orderings, candidate scorers,
|
|
81
|
+
container selectors or complete solvers.
|
|
82
|
+
|
|
83
|
+
## Documentation
|
|
84
|
+
|
|
85
|
+
| Document | Covers |
|
|
86
|
+
| --- | --- |
|
|
87
|
+
| [docs/GUARANTEES.md](docs/GUARANTEES.md) | What is promised and what is not. Start here. |
|
|
88
|
+
| [docs/PUBLIC-API.md](docs/PUBLIC-API.md) | Inputs, outputs and status semantics. |
|
|
89
|
+
| [docs/UNITS-AND-NUMERICS.md](docs/UNITS-AND-NUMERICS.md) | Units, accepted input forms, rounding policy. |
|
|
90
|
+
|
|
91
|
+
## Requirements
|
|
92
|
+
|
|
93
|
+
Python 3.10 or newer. No dependencies.
|
|
94
|
+
|
|
95
|
+
## Other ports exist
|
|
96
|
+
|
|
97
|
+
The same request and result contract is implemented independently in PHP and Rust, and
|
|
98
|
+
all three are held to producing identical placements on a shared fixture set. If your
|
|
99
|
+
stack spans languages, you can compute a packing on any of them and get the same answer.
|
|
100
|
+
|
|
101
|
+
## Contributing
|
|
102
|
+
|
|
103
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md). Security reports go through the process in
|
|
104
|
+
[SECURITY.md](SECURITY.md), not public issues.
|
|
105
|
+
|
|
106
|
+
## License
|
|
107
|
+
|
|
108
|
+
MIT. See [LICENSE](LICENSE).
|
packvium-0.1.0/README.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Packvium for Python
|
|
2
|
+
|
|
3
|
+
Deterministic 3D cartonization and rectangular bin packing. Pure Python, **no runtime
|
|
4
|
+
dependencies**, exact integer geometry.
|
|
5
|
+
|
|
6
|
+
> **Version 0.1.0 — early release.** The public API is not frozen; pin an exact version.
|
|
7
|
+
> Read [docs/GUARANTEES.md](docs/GUARANTEES.md) before relying on a result.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install packvium
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick start
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from packvium import Container, Dimensions, Item, Packer, PackingConfig
|
|
17
|
+
|
|
18
|
+
result = Packer(PackingConfig.balanced()).pack(
|
|
19
|
+
items=[Item.create("book", Dimensions.mm("210", "140", "30"), quantity=4)],
|
|
20
|
+
containers=[Container.create("box", Dimensions.mm("400", "300", "250"))],
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
print(result.status) # feasible
|
|
24
|
+
for container in result.containers:
|
|
25
|
+
for placement in container.placements:
|
|
26
|
+
print(placement.item_id, placement.position, placement.orientation)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Fractional inches are exact, not approximated:
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
Dimensions.inches("12 3/8", "8 1/2", "3/4")
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
There is also a CLI that reads a JSON request on standard input:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
echo '{"items":[{"id":"box","quantity":8,"dimensions":{"length":"50","width":"50","height":"50"}}],
|
|
39
|
+
"containers":[{"id":"carton","inner_dimensions":{"length":"100","width":"100","height":"100"}}]}' \
|
|
40
|
+
| python -m packvium
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## What it does
|
|
44
|
+
|
|
45
|
+
- **Exact arithmetic.** Length is measured in ticks of 1/16000 mm and weight in 1/8 µg.
|
|
46
|
+
No coordinate is ever a float, so no placement decision depends on rounding.
|
|
47
|
+
- **Real constraints.** Weight and payload limits, permitted rotations, keep-upright,
|
|
48
|
+
floor-only, non-stackable, top-load limits, minimum support ratio, tag incompatibility,
|
|
49
|
+
clearance and rectangular obstacles.
|
|
50
|
+
- **A solver portfolio, not one algorithm.** Regular-grid, layer, extreme-point,
|
|
51
|
+
maximal-space and bounded exact search, selected by problem shape and profile.
|
|
52
|
+
- **Answers you can check.** Every solution is re-validated by logic independent of the
|
|
53
|
+
search. Unplaced items come back with a reason code, not silently missing.
|
|
54
|
+
- **Deterministic.** The same input and seed produce the same result, always.
|
|
55
|
+
- **Multi-container and nested.** Split across containers, or pack containers into
|
|
56
|
+
containers.
|
|
57
|
+
- **Extensible.** Register your own constraints, item orderings, candidate scorers,
|
|
58
|
+
container selectors or complete solvers.
|
|
59
|
+
|
|
60
|
+
## Documentation
|
|
61
|
+
|
|
62
|
+
| Document | Covers |
|
|
63
|
+
| --- | --- |
|
|
64
|
+
| [docs/GUARANTEES.md](docs/GUARANTEES.md) | What is promised and what is not. Start here. |
|
|
65
|
+
| [docs/PUBLIC-API.md](docs/PUBLIC-API.md) | Inputs, outputs and status semantics. |
|
|
66
|
+
| [docs/UNITS-AND-NUMERICS.md](docs/UNITS-AND-NUMERICS.md) | Units, accepted input forms, rounding policy. |
|
|
67
|
+
|
|
68
|
+
## Requirements
|
|
69
|
+
|
|
70
|
+
Python 3.10 or newer. No dependencies.
|
|
71
|
+
|
|
72
|
+
## Other ports exist
|
|
73
|
+
|
|
74
|
+
The same request and result contract is implemented independently in PHP and Rust, and
|
|
75
|
+
all three are held to producing identical placements on a shared fixture set. If your
|
|
76
|
+
stack spans languages, you can compute a packing on any of them and get the same answer.
|
|
77
|
+
|
|
78
|
+
## Contributing
|
|
79
|
+
|
|
80
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md). Security reports go through the process in
|
|
81
|
+
[SECURITY.md](SECURITY.md), not public issues.
|
|
82
|
+
|
|
83
|
+
## License
|
|
84
|
+
|
|
85
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "packvium"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Deterministic, extensible 3D cartonization and rectangular bin-packing library"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [{name = "Packvium contributors"}]
|
|
13
|
+
keywords = ["3d-bin-packing", "cartonization", "packaging", "container-loading"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
18
|
+
"Programming Language :: Python :: 3.9",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
"Programming Language :: Python :: 3.14",
|
|
24
|
+
"Operating System :: OS Independent",
|
|
25
|
+
"Typing :: Typed",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.scripts]
|
|
29
|
+
packvium = "packvium.__main__:main"
|
|
30
|
+
|
|
31
|
+
[tool.setuptools.packages.find]
|
|
32
|
+
where = ["src"]
|
|
33
|
+
|
|
34
|
+
[tool.pytest.ini_options]
|
|
35
|
+
addopts = "-q"
|
|
36
|
+
testpaths = ["tests"]
|
|
37
|
+
pythonpath = ["src"]
|
packvium-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from .config import PackingConfig, SolverProfile
|
|
2
|
+
from .effort import EffortBudget
|
|
3
|
+
from .explain import (
|
|
4
|
+
LEVEL_PREFIXES,
|
|
5
|
+
REASON_MESSAGES,
|
|
6
|
+
UnknownReasonError,
|
|
7
|
+
explain_reason,
|
|
8
|
+
explain_unpacked_item,
|
|
9
|
+
explain_unpacked_items,
|
|
10
|
+
)
|
|
11
|
+
from .extensions import ContainerSelector, DefaultSolutionScorer, ExtensionRegistry, ItemOrderStrategy, SolutionScorer
|
|
12
|
+
from .geometry import AxisAlignedBox, Dimensions, Point, Rotation, dimensional_weight
|
|
13
|
+
from .models import (Axle, Container, Item, ItemInstance, Obstacle, PackedContainer, PackingRequest,
|
|
14
|
+
Placement, ReasonProof, RejectionObservation, UnpackedItem)
|
|
15
|
+
from .nested import NestedPacker, NestedPackingResult, PackingLevel
|
|
16
|
+
from .packer import Packer
|
|
17
|
+
from .packing_sequence import (
|
|
18
|
+
ALL_DIRECTIONS,
|
|
19
|
+
InvalidDirectionError,
|
|
20
|
+
LoadingDependencyGraph,
|
|
21
|
+
Reachability,
|
|
22
|
+
RouteSequenceError,
|
|
23
|
+
SequenceError,
|
|
24
|
+
SequenceReplayError,
|
|
25
|
+
SequenceStep,
|
|
26
|
+
SequenceWarning,
|
|
27
|
+
UnloadingDependencyGraph,
|
|
28
|
+
placement_reachability,
|
|
29
|
+
replay_loading_order,
|
|
30
|
+
replay_removal_order,
|
|
31
|
+
safe_loading_order,
|
|
32
|
+
safe_loading_order_with_evidence,
|
|
33
|
+
safe_removal_order,
|
|
34
|
+
safe_removal_order_with_evidence,
|
|
35
|
+
safe_route_removal_order,
|
|
36
|
+
)
|
|
37
|
+
from .rebalance import RebalanceResult, WeightMove, rebalance_weight
|
|
38
|
+
from .result import (
|
|
39
|
+
AlgorithmReport,
|
|
40
|
+
PackingResult,
|
|
41
|
+
PackingStatus,
|
|
42
|
+
ResultFact,
|
|
43
|
+
SolverMetrics,
|
|
44
|
+
StartRecord,
|
|
45
|
+
aggregate_termination,
|
|
46
|
+
)
|
|
47
|
+
from .serialization import pack_from_dict
|
|
48
|
+
from .trace import TraceSink, use_trace
|
|
49
|
+
from .units import Length, Rounding, Weight
|
|
50
|
+
from .validation import IndependentSolutionValidator, ValidationIssue, ValidationReport
|
|
51
|
+
|
|
52
|
+
__all__ = [
|
|
53
|
+
"ALL_DIRECTIONS", "AlgorithmReport", "Axle", "AxisAlignedBox", "Container", "DefaultSolutionScorer", "Dimensions",
|
|
54
|
+
"EffortBudget", "ExtensionRegistry",
|
|
55
|
+
"IndependentSolutionValidator", "InvalidDirectionError", "Item", "ItemInstance", "LEVEL_PREFIXES", "Length",
|
|
56
|
+
"LoadingDependencyGraph", "NestedPacker",
|
|
57
|
+
"NestedPackingResult", "Obstacle", "PackedContainer", "Packer", "PackingConfig",
|
|
58
|
+
"PackingLevel", "PackingRequest", "PackingResult", "PackingStatus", "Placement", "Point",
|
|
59
|
+
"REASON_MESSAGES", "Reachability", "ReasonProof", "RebalanceResult", "RejectionObservation", "ResultFact", "Rotation",
|
|
60
|
+
"Rounding", "RouteSequenceError",
|
|
61
|
+
"SequenceError",
|
|
62
|
+
"SequenceReplayError", "SequenceStep", "SequenceWarning", "SolutionScorer",
|
|
63
|
+
"SolverMetrics", "StartRecord", "TraceSink", "UnknownReasonError", "UnloadingDependencyGraph",
|
|
64
|
+
"aggregate_termination",
|
|
65
|
+
"SolverProfile", "UnpackedItem", "ValidationIssue", "ValidationReport", "WeightMove",
|
|
66
|
+
"Weight", "dimensional_weight", "explain_reason", "explain_unpacked_item", "explain_unpacked_items",
|
|
67
|
+
"pack_from_dict", "placement_reachability", "rebalance_weight", "replay_loading_order",
|
|
68
|
+
"replay_removal_order", "safe_loading_order", "safe_loading_order_with_evidence", "safe_removal_order",
|
|
69
|
+
"safe_removal_order_with_evidence", "safe_route_removal_order", "use_trace",
|
|
70
|
+
]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import argparse, json, sys
|
|
3
|
+
from .serialization import pack_from_dict
|
|
4
|
+
|
|
5
|
+
def main() -> int:
|
|
6
|
+
parser = argparse.ArgumentParser(description="Pack rigid cuboids into rectangular containers")
|
|
7
|
+
parser.add_argument("input", nargs="?", help="JSON input file; stdin when omitted")
|
|
8
|
+
parser.add_argument("-o", "--output")
|
|
9
|
+
args = parser.parse_args()
|
|
10
|
+
with (open(args.input, encoding="utf-8") if args.input else sys.stdin) as source:
|
|
11
|
+
result = pack_from_dict(json.load(source))
|
|
12
|
+
encoded = json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True)
|
|
13
|
+
if args.output:
|
|
14
|
+
with open(args.output, "w", encoding="utf-8") as target: target.write(encoded + "\n")
|
|
15
|
+
else: print(encoded)
|
|
16
|
+
return 0
|
|
17
|
+
|
|
18
|
+
if __name__ == "__main__": raise SystemExit(main())
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Small runtime compatibility helpers for the public Python package.
|
|
2
|
+
|
|
3
|
+
The canonical models use ``dataclass(slots=True)`` on Python 3.10+ to reduce
|
|
4
|
+
per-instance memory. Python 3.9 has the same dataclass semantics but does not
|
|
5
|
+
accept the ``slots`` keyword. Keeping the version branch in one lower-layer
|
|
6
|
+
module lets every model retain one definition and one public API.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import sys
|
|
12
|
+
from dataclasses import dataclass as _stdlib_dataclass
|
|
13
|
+
from typing import Any, Callable, TypeVar, overload
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
_T = TypeVar("_T")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _dataclass_options(options: dict[str, Any], version: tuple[int, int]) -> dict[str, Any]:
|
|
20
|
+
compatible = dict(options)
|
|
21
|
+
if version < (3, 10):
|
|
22
|
+
compatible.pop("slots", None)
|
|
23
|
+
return compatible
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@overload
|
|
27
|
+
def dataclass(cls: type[_T]) -> type[_T]: ...
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@overload
|
|
31
|
+
def dataclass(**options: Any) -> Callable[[type[_T]], type[_T]]: ...
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def dataclass(cls: type[_T] | None = None, **options: Any) -> Any:
|
|
35
|
+
"""Delegate to stdlib dataclass, omitting only unsupported 3.10 keywords."""
|
|
36
|
+
|
|
37
|
+
compatible = _dataclass_options(options, sys.version_info[:2])
|
|
38
|
+
|
|
39
|
+
def decorate(target: type[_T]) -> type[_T]:
|
|
40
|
+
return _stdlib_dataclass(target, **compatible)
|
|
41
|
+
|
|
42
|
+
return decorate if cls is None else decorate(cls)
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Sequence
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from .constraints import LoadUnit
|
|
7
|
+
from .models import Axle
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def axle_reactions(
|
|
11
|
+
axles: tuple["Axle", "Axle"],
|
|
12
|
+
units: Sequence["LoadUnit"],
|
|
13
|
+
tare_weight_ticks: int = 0,
|
|
14
|
+
tare_doubled_center_x: int = 0,
|
|
15
|
+
) -> tuple[int, int, int]:
|
|
16
|
+
"""Return ``(denominator, front_numerator, rear_numerator)`` exactly.
|
|
17
|
+
|
|
18
|
+
Axle limits are gross-load limits. Payload contributes at each item's physical
|
|
19
|
+
centre; tare contributes at the container's geometric longitudinal centre (the
|
|
20
|
+
caller passes its doubled coordinate, preserving an odd half-tick).
|
|
21
|
+
"""
|
|
22
|
+
front, rear = axles
|
|
23
|
+
total_weight = tare_weight_ticks + sum(unit.weight_ticks for unit in units)
|
|
24
|
+
doubled_weighted_x = tare_weight_ticks * tare_doubled_center_x + sum(
|
|
25
|
+
unit.weight_ticks * (2 * unit.box.origin.x + unit.box.dimensions.length.ticks)
|
|
26
|
+
for unit in units
|
|
27
|
+
)
|
|
28
|
+
denominator = 2 * (rear.position.ticks - front.position.ticks)
|
|
29
|
+
return (
|
|
30
|
+
denominator,
|
|
31
|
+
2 * total_weight * rear.position.ticks - doubled_weighted_x,
|
|
32
|
+
doubled_weighted_x - 2 * total_weight * front.position.ticks,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def axle_load_exceeded(
|
|
37
|
+
axles: tuple["Axle", "Axle"],
|
|
38
|
+
units: Sequence["LoadUnit"],
|
|
39
|
+
tare_weight_ticks: int = 0,
|
|
40
|
+
tare_doubled_center_x: int = 0,
|
|
41
|
+
) -> tuple[str, str] | None:
|
|
42
|
+
"""Whether either axle bears more than its own limit, as (code, detail).
|
|
43
|
+
|
|
44
|
+
Two-point beam statics: taking moments about each axle in turn gives an exact
|
|
45
|
+
fraction for what the other axle carries, with no assumption weaker than "the
|
|
46
|
+
container behaves like a rigid beam resting on exactly two supports" -- true by
|
|
47
|
+
construction since `Container.axles` only ever has two entries.
|
|
48
|
+
|
|
49
|
+
Takes `LoadUnit`s, the same "box + weight, real or hypothetical" abstraction
|
|
50
|
+
`top_loads`/`stack_density_exceeded` already share, rather than requiring a real
|
|
51
|
+
`Placement` -- a placement-time candidate does not have one yet. A unit's own box
|
|
52
|
+
is already its envelope; the envelope's centre coincides exactly with the
|
|
53
|
+
physical item's centre because clearance pads both sides of every axis equally,
|
|
54
|
+
so no separate physical/envelope distinction is needed here.
|
|
55
|
+
|
|
56
|
+
Never rounds an intermediate axle load. Both loads are compared to their limits
|
|
57
|
+
by cross-multiplying the same exact fraction (numerator over `2 * (rear - front)`
|
|
58
|
+
as the shared denominator), the same discipline `stack_density_exceeded` and
|
|
59
|
+
`SupportConstraint` already use -- a rounded intermediate could hide a real
|
|
60
|
+
overload or manufacture one that was not there.
|
|
61
|
+
"""
|
|
62
|
+
front, rear = axles
|
|
63
|
+
denominator, numerator_front, numerator_rear = axle_reactions(
|
|
64
|
+
axles, units, tare_weight_ticks, tare_doubled_center_x
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
if front.max_load is not None and numerator_front > front.max_load.ticks * denominator:
|
|
68
|
+
return ("axle_overloaded", "front")
|
|
69
|
+
if rear.max_load is not None and numerator_rear > rear.max_load.ticks * denominator:
|
|
70
|
+
return ("axle_overloaded", "rear")
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def axle_balanced_origins(
|
|
75
|
+
axles: tuple["Axle", "Axle"],
|
|
76
|
+
other_units: Sequence["LoadUnit"],
|
|
77
|
+
tare_weight_ticks: int,
|
|
78
|
+
tare_doubled_center_x: int,
|
|
79
|
+
item_weight_ticks: int,
|
|
80
|
+
item_length_ticks: int,
|
|
81
|
+
) -> list[int]:
|
|
82
|
+
"""The tightest x-origins that seat this item exactly on either axle's limit.
|
|
83
|
+
|
|
84
|
+
Candidate-point generation (`find_candidates`) only ever proposes positions
|
|
85
|
+
flush against the container wall or another placed box's own corner -- correct
|
|
86
|
+
for plain volume packing, where nothing is ever gained by leaving a gap, but
|
|
87
|
+
incomplete once axle limits are in play: sometimes the only feasible spot for
|
|
88
|
+
an item is floating away from every wall and every other box, specifically to
|
|
89
|
+
keep this item's own moment from tipping one axle over its limit. A floor-level
|
|
90
|
+
item needs no lateral contact for support (see `_support_ratio`), so that
|
|
91
|
+
position is otherwise unreachable by any point this module already generates.
|
|
92
|
+
|
|
93
|
+
Solving `axle_reactions`'s own boundary equation for this item's centre --
|
|
94
|
+
"where would the front/rear reaction land exactly on its limit if this item's
|
|
95
|
+
centre were here" -- turns the two axle limits into two extra x-origins worth
|
|
96
|
+
trying, on top of the ordinary extreme points. Both are exact integer ticks,
|
|
97
|
+
biased toward the safe side of their own limit (never past it) since a
|
|
98
|
+
placement that lands exactly on the boundary is still allowed by
|
|
99
|
+
`axle_load_exceeded`'s own strict `>` comparison; the caller runs every
|
|
100
|
+
candidate through the same collision and constraint checks regardless, so a
|
|
101
|
+
boundary that turns out unreachable (blocked, out of bounds, or infeasible for
|
|
102
|
+
the other axle) is simply rejected same as any other candidate.
|
|
103
|
+
|
|
104
|
+
Never assumes there is room: the caller clamps and discards out-of-range
|
|
105
|
+
results.
|
|
106
|
+
"""
|
|
107
|
+
if item_weight_ticks <= 0:
|
|
108
|
+
return []
|
|
109
|
+
front, rear = axles
|
|
110
|
+
denominator = 2 * (rear.position.ticks - front.position.ticks)
|
|
111
|
+
other_total = tare_weight_ticks + sum(unit.weight_ticks for unit in other_units)
|
|
112
|
+
other_doubled_x = tare_weight_ticks * tare_doubled_center_x + sum(
|
|
113
|
+
unit.weight_ticks * (2 * unit.box.origin.x + unit.box.dimensions.length.ticks)
|
|
114
|
+
for unit in other_units
|
|
115
|
+
)
|
|
116
|
+
total = other_total + item_weight_ticks
|
|
117
|
+
origins: list[int] = []
|
|
118
|
+
if front.max_load is not None:
|
|
119
|
+
# Smallest doubled centre for which numerator_front <= front limit * denominator.
|
|
120
|
+
required = 2 * total * rear.position.ticks - front.max_load.ticks * denominator - other_doubled_x
|
|
121
|
+
doubled_centre = -(-required // item_weight_ticks) # ceil: never understate what front needs
|
|
122
|
+
origins.append(-(-(doubled_centre - item_length_ticks) // 2)) # ceil: stay on the safe side
|
|
123
|
+
if rear.max_load is not None:
|
|
124
|
+
# Largest doubled centre for which numerator_rear <= rear limit * denominator.
|
|
125
|
+
allowed = rear.max_load.ticks * denominator + 2 * total * front.position.ticks - other_doubled_x
|
|
126
|
+
doubled_centre = allowed // item_weight_ticks # floor: never overstate what rear allows
|
|
127
|
+
origins.append((doubled_centre - item_length_ticks) // 2) # floor: stay on the safe side
|
|
128
|
+
return origins
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Sequence
|
|
4
|
+
|
|
5
|
+
from .geometry import Dimensions
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from .models import Placement
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def centre_of_mass_offset_ppm(inner_dimensions: Dimensions, placements: Sequence[Placement]) -> int:
|
|
12
|
+
"""How far the weighted centre of mass sits from the container's own centre.
|
|
13
|
+
|
|
14
|
+
Reported as parts per million of the half-extent along whichever of the two
|
|
15
|
+
horizontal axes (length, width) is worse -- the Chebyshev, not Euclidean, offset,
|
|
16
|
+
so the result stays exact: a Euclidean distance would need a square root, and
|
|
17
|
+
this library's whole premise is exact fixed-point arithmetic. 0% means
|
|
18
|
+
centred; 100% means the mass sits at the very edge along that axis. Needed for
|
|
19
|
+
axle load and side-to-side balance, both of which care about the worst axis, not
|
|
20
|
+
a single blended number that could hide either one.
|
|
21
|
+
|
|
22
|
+
Every item contributes its own physical centre, weighted by its own mass -- an
|
|
23
|
+
item's clearance envelope is packing buffer, not part of what it weighs, so
|
|
24
|
+
`position`/`dimensions` are used rather than the (possibly larger) envelope.
|
|
25
|
+
|
|
26
|
+
Kept exact throughout via one combined numerator/denominator per axis and a
|
|
27
|
+
single final floor division, rather than rounding an intermediate centroid --
|
|
28
|
+
the same discipline `required_area`/`unused_volume_ppm` already use elsewhere.
|
|
29
|
+
"""
|
|
30
|
+
total_weight = sum(p.instance.weight.ticks for p in placements)
|
|
31
|
+
if total_weight == 0:
|
|
32
|
+
return 0
|
|
33
|
+
length = inner_dimensions.length.ticks
|
|
34
|
+
width = inner_dimensions.width.ticks
|
|
35
|
+
doubled_weighted_x = sum(p.instance.weight.ticks * (2 * p.position.x + p.dimensions.length.ticks) for p in placements)
|
|
36
|
+
doubled_weighted_y = sum(p.instance.weight.ticks * (2 * p.position.y + p.dimensions.width.ticks) for p in placements)
|
|
37
|
+
numerator_x = doubled_weighted_x - total_weight * length
|
|
38
|
+
numerator_y = doubled_weighted_y - total_weight * width
|
|
39
|
+
offset_x_ppm = abs(numerator_x) * 1_000_000 // (total_weight * length)
|
|
40
|
+
offset_y_ppm = abs(numerator_y) * 1_000_000 // (total_weight * width)
|
|
41
|
+
return max(offset_x_ppm, offset_y_ppm)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from ._compat import dataclass
|
|
4
|
+
from enum import Enum
|
|
5
|
+
|
|
6
|
+
from .effort import EffortBudget
|
|
7
|
+
from .units import Length
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SolverProfile(str, Enum):
|
|
11
|
+
FAST = "fast"
|
|
12
|
+
BALANCED = "balanced"
|
|
13
|
+
QUALITY = "quality"
|
|
14
|
+
EXACT_SMALL = "exact_small"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True, slots=True)
|
|
18
|
+
class PackingConfig:
|
|
19
|
+
profile: SolverProfile = SolverProfile.BALANCED
|
|
20
|
+
time_limit_ms: int = 1000
|
|
21
|
+
top_k: int = 3
|
|
22
|
+
seed: int = 42
|
|
23
|
+
max_containers: int | None = None
|
|
24
|
+
clearance: Length = Length(0)
|
|
25
|
+
minimum_support_ratio: float = 0.0
|
|
26
|
+
exact_item_limit: int = 7
|
|
27
|
+
multi_start_orders: int = 8
|
|
28
|
+
validate_result: bool = True
|
|
29
|
+
max_candidates_per_item: int = 1
|
|
30
|
+
max_candidate_points: int = 4096
|
|
31
|
+
solvers: tuple[str, ...] = ()
|
|
32
|
+
objective: str = "default"
|
|
33
|
+
effort_budget: EffortBudget | None = None
|
|
34
|
+
#: A carrier's published dimensional-weight divisor. Required only when
|
|
35
|
+
#: `objective` is `"shipping_cost"` -- see `extensions.ShippingCostSolutionScorer`.
|
|
36
|
+
dimensional_weight_divisor: int | None = None
|
|
37
|
+
dimensional_weight_length_unit: str = "in"
|
|
38
|
+
dimensional_weight_weight_unit: str = "lb"
|
|
39
|
+
# Default True reproduces every existing result byte-for-byte. False lets
|
|
40
|
+
# GridSolver's regular-lattice fast path skip materializing one `Placement`
|
|
41
|
+
# object per instance -- see `lattice_summary.py` -- when a caller
|
|
42
|
+
# only needs to know the packing fits, not each item's own coordinates.
|
|
43
|
+
require_placement_coordinates: bool = True
|
|
44
|
+
#: Opt-in worker-process ceiling for the multi-start portfolio. `1`
|
|
45
|
+
#: (the default) reproduces the original fully sequential orchestration
|
|
46
|
+
#: exactly, with zero behavioural change. A value above `1` lets
|
|
47
|
+
#: `SolverOrchestrator` run starts after the first concurrently in separate
|
|
48
|
+
#: processes -- see docs/ALGORITHMS-AND-COMPLEXITY.md's "Multi-start
|
|
49
|
+
#: orchestration" section for the eligibility rules and determinism argument.
|
|
50
|
+
parallel_starts: int = 1
|
|
51
|
+
#: Width of the deterministic beam over partial container plans. The default
|
|
52
|
+
#: keeps the historical greedy path; ``quality()`` opts into the bounded search.
|
|
53
|
+
container_plan_beam_width: int = 1
|
|
54
|
+
#: Hard counted-work ceiling for container-plan nodes, independent of wall time.
|
|
55
|
+
container_plan_node_limit: int = 1
|
|
56
|
+
|
|
57
|
+
def __post_init__(self) -> None:
|
|
58
|
+
if (self.time_limit_ms <= 0 or self.top_k <= 0 or self.exact_item_limit <= 0
|
|
59
|
+
or self.multi_start_orders <= 0 or self.max_candidates_per_item <= 0
|
|
60
|
+
or self.parallel_starts <= 0 or self.container_plan_beam_width <= 0
|
|
61
|
+
or self.container_plan_node_limit <= 0):
|
|
62
|
+
raise ValueError("positive configuration values required")
|
|
63
|
+
if self.max_candidate_points < 16:
|
|
64
|
+
raise ValueError("max_candidate_points must be at least 16")
|
|
65
|
+
if not 0 <= self.minimum_support_ratio <= 1: raise ValueError("minimum_support_ratio must be between 0 and 1")
|
|
66
|
+
if self.dimensional_weight_divisor is not None and self.dimensional_weight_divisor <= 0:
|
|
67
|
+
raise ValueError("dimensional_weight_divisor must be positive")
|
|
68
|
+
|
|
69
|
+
@classmethod
|
|
70
|
+
def fast(cls, time_limit_ms: int = 200, **kwargs) -> "PackingConfig":
|
|
71
|
+
return cls(profile=SolverProfile.FAST, time_limit_ms=time_limit_ms, top_k=1, multi_start_orders=1, **kwargs)
|
|
72
|
+
|
|
73
|
+
@classmethod
|
|
74
|
+
def balanced(cls, time_limit_ms: int = 1000, top_k: int = 3, **kwargs) -> "PackingConfig":
|
|
75
|
+
return cls(profile=SolverProfile.BALANCED, time_limit_ms=time_limit_ms, top_k=top_k, **kwargs)
|
|
76
|
+
|
|
77
|
+
@classmethod
|
|
78
|
+
def quality(cls, time_limit_ms: int = 5000, top_k: int = 5, **kwargs) -> "PackingConfig":
|
|
79
|
+
kwargs.setdefault("container_plan_beam_width", 16)
|
|
80
|
+
kwargs.setdefault("container_plan_node_limit", 100_000)
|
|
81
|
+
return cls(profile=SolverProfile.QUALITY, time_limit_ms=time_limit_ms, top_k=top_k, multi_start_orders=24, max_candidates_per_item=16, **kwargs)
|
|
82
|
+
|
|
83
|
+
@classmethod
|
|
84
|
+
def exact_small(cls, time_limit_ms: int = 10000, **kwargs) -> "PackingConfig":
|
|
85
|
+
return cls(profile=SolverProfile.EXACT_SMALL, time_limit_ms=time_limit_ms, **kwargs)
|