Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/DISCUSSION_TEMPLATE/q-a.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ body:
id: package-version
attributes:
label: Package and version
placeholder: "packvium 1.8.0 (Python)"
placeholder: "packvium 1.9.0 (Python)"
validations:
required: true
- type: textarea
Expand Down
2 changes: 1 addition & 1 deletion .github/DISCUSSION_TEMPLATE/show-and-tell.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ body:
id: package-version
attributes:
label: Package and version
placeholder: "packvium 1.8.0 (Python)"
placeholder: "packvium 1.9.0 (Python)"
validations:
required: false
- type: textarea
Expand Down
4 changes: 2 additions & 2 deletions .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ body:
id: version
attributes:
label: Package version
description: The exact version installed, for example 1.8.0.
placeholder: "1.8.0"
description: The exact version installed, for example 1.9.0.
placeholder: "1.9.0"
validations:
required: true
- type: input
Expand Down
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/feature_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ body:
id: version
attributes:
label: Package version
placeholder: "1.8.0"
placeholder: "1.9.0"
validations:
required: true
- type: textarea
Expand Down
12 changes: 6 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ jobs:
os: [ubuntu-24.04, macos-latest, windows-latest]
python: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: ${{ matrix.python }}
- run: python -m pip install --upgrade pip
Expand All @@ -37,8 +37,8 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: "3.12"
- run: python -m pip install --upgrade pip build twine
Expand All @@ -50,7 +50,7 @@ jobs:
/tmp/clean/bin/python -m pip install --quiet pytest
/tmp/clean/bin/python -m pip install --quiet --no-index --find-links dist packvium
/tmp/clean/bin/python -m pytest tests -q
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: distributions
path: dist/
Expand All @@ -67,7 +67,7 @@ jobs:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
- uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0
with:
fail-on-severity: low
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/rc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: "3.12"
- run: python -m pip install --upgrade pip build twine
Expand All @@ -52,7 +52,7 @@ jobs:
/tmp/clean/bin/python -m pip install --quiet --no-index --find-links dist packvium
/tmp/clean/bin/python -m pytest tests -q

- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: distributions
path: dist/
Expand All @@ -69,7 +69,7 @@ jobs:
id-token: write # OIDC, for Trusted Publishing and for the attestation
attestations: write # build provenance, so a consumer can verify where a file came from
steps:
- uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
name: distributions
path: dist/
Expand Down
10 changes: 5 additions & 5 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: "3.12"
- run: python -m pip install --upgrade pip build twine
Expand All @@ -53,7 +53,7 @@ jobs:
/tmp/clean/bin/python -m pip install --quiet --no-index --find-links dist packvium
/tmp/clean/bin/python -m pytest tests -q

- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: distributions
path: dist/
Expand All @@ -66,7 +66,7 @@ jobs:
id-token: write # OIDC, for Trusted Publishing and for the attestation
attestations: write # build provenance, so a consumer can verify where a file came from
steps:
- uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
name: distributions
path: dist/
Expand All @@ -85,7 +85,7 @@ jobs:
permissions:
contents: write # the Release is the only thing this workflow writes to the repository
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
- name: Create the GitHub Release from the tag
env:
GH_TOKEN: ${{ github.token }}
Expand Down
34 changes: 34 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,40 @@ What changed in `packvium` on PyPI, release by release. The format follows
[Keep a Changelog](https://keepachangelog.com/1.1.0/) and this project adheres to
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.9.0]

No API change, but admission is stricter: some requests 1.8 accepted are now refused (see the
weight and length limits below). Answers now agree with the other engines where 1.8 found them
apart.

### Changed

- **Under a clearance a compressible item bears its load on its own footprint**, so its crush
check is stricter and `validate` measures its own compressed volume.
- **A weight is at most `2^63 - 1` ticks, and so is every item's weight times its quantity plus
the heaviest tare**: past it, `invalid_request` / `above_maximum`; a weight written as a JSON
number past `2^53 - 1` is `invalid_value` and must be a string.
- **Agrees with the other engines where 1.8 did not:** nesting under a clearance shares the
items' own overlap and a deep nesting column nests in pairs; a length past `2^53 - 1` ticks or
a box past `10^30` cubic ticks is `invalid_request` / `above_maximum`; a side that reads as zero
ticks is `invalid_value` / `must be positive`; an obstacle box outside its container is
`invalid_value` / `lies outside inner_dimensions`, each at its own pointer.
- **`validate` checks the centroid (`centre_of_gravity_unsupported`) and the whole stack's load
(`top_load_exceeded`)**, an item on several bearers shared by contact area. It no longer
holds that item's whole weight against every bearer, so it accepts some results 1.8 rejected
and names the overloaded bearer.
- **A plan beam wider than 1 compares every container type**, whatever the node limit.
- **A JSON request is checked once, not twice**, on the same objects the request check built.

### Fixed

- **A narrow nesting item under a clearance reported a negative `used_volume_ticks3`**, and a
deep nesting column on the lattice returned `invalid_result`.
- **An obstacle origin with a wrong type is refused with `invalid_request`**, rather than
being defaulted or raising `AttributeError`; an explicit null origin defaults to zero,
including in additional boxes. Null coordinates default to zero, and null
`additional_boxes` defaults to an empty list, as request admission already permits.

## [1.8.0]

Results now say how far they can be from the best possible answer, `optimal` means a proof,
Expand Down
2 changes: 1 addition & 1 deletion CITATION.cff
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ type: software
authors:
- name: "Packvium contributors"
website: "https://packvium.com"
version: "1.8.0"
version: "1.9.0"
license: MIT
url: "https://packvium.com"
repository-code: "https://github.com/toxakara/packvium-python"
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ coordinates and rotations for each item, with a reason for anything that did not
Full documentation, the constraint reference and benchmarks live at
[packvium.com](https://packvium.com).

> **Version 1.8.0 — the public API is frozen.** Field names, status codes and the
> **Version 1.9.0 — the public API is frozen.** Field names, status codes and the
> objective vector do not change without a major version, so any `1.x` is a safe upgrade
> from any earlier `1.x`.
> Read [docs/GUARANTEES.md](https://github.com/toxakara/packvium-python/blob/main/docs/GUARANTEES.md) before relying on a result.
Expand Down Expand Up @@ -310,7 +310,7 @@ Documentation, the constraint reference and the benchmarks are at
| --- | --- | --- |
| Python — [`packvium`](https://pypi.org/project/packvium/) | `pip install packvium` | [packvium-python](https://github.com/toxakara/packvium-python) |
| PHP — [`packvium/packvium`](https://packagist.org/packages/packvium/packvium) | `composer require packvium/packvium` | [packvium-php](https://github.com/toxakara/packvium-php) |
| Rust — [`packvium`](https://crates.io/crates/packvium) | `packvium = "1.8"` | [packvium-rust](https://github.com/toxakara/packvium-rust) |
| Rust — [`packvium`](https://crates.io/crates/packvium) | `packvium = "1.9"` | [packvium-rust](https://github.com/toxakara/packvium-rust) |
| Node.js — [`@packvium/engine`](https://www.npmjs.com/package/@packvium/engine) | `npm install @packvium/engine` | [packvium-node](https://github.com/toxakara/packvium-node) |
| Browser / WebAssembly — [`@packvium/browser`](https://www.npmjs.com/package/@packvium/browser) | `npm install @packvium/browser` | [packvium-wasm](https://github.com/toxakara/packvium-wasm) |
| PHP FFI bridge — [`packvium/native-bridge`](https://packagist.org/packages/packvium/native-bridge) | `composer require packvium/native-bridge` | [packvium-php-bridge](https://github.com/toxakara/packvium-php-bridge) |
Expand All @@ -332,7 +332,7 @@ CodeMeta form.
@software{packvium_python,
author = {{Packvium contributors}},
title = {Packvium for Python},
version = {1.8.0},
version = {1.9.0},
license = {MIT},
url = {https://packvium.com}
}
Expand Down
2 changes: 1 addition & 1 deletion codemeta.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"@type": "SoftwareSourceCode",
"name": "Packvium for Python",
"description": "Deterministic 3D bin-packing and cartonization engine for pallets, containers, trucks and shipping cartons. Exact integer geometry, reproducible results.",
"version": "1.8.0",
"version": "1.9.0",
"license": "https://spdx.org/licenses/MIT",
"author": [
{
Expand Down
32 changes: 32 additions & 0 deletions docs/COMMERCE-API.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,10 @@ history with a hole or a duplicated version number.
All three top-level keys are optional; a document that only needs to price a shipment
may carry only `tariffs`. Field-level rules:

- A tariff history is identified by the ordered pair `(carrier_id, service_id)`.
Separators inside either string are ordinary identifier characters: for example,
`("a/b", "c")` and `("a", "b/c")` name different histories. Duplicate detection and
both pinned and effective-dated lookup use the complete pair.
- `accessorials` is an ordered **list**, not an object, so no language has to agree
about key order; each entry sets exactly one of `flat_charge_minor` or
`permille_of_base`. Duplicate `accessorial_id` values are an input error.
Expand All @@ -165,6 +169,9 @@ may carry only `tariffs`. Field-level rules:
`published_at`, `cost_minor`, `minimum_charge_minor`, `fuel_surcharge_permille`,
`flat_charge_minor`, `permille_of_base` and each zone rate in
`cost_per_dimensional_kg_minor` may be zero but not negative.
Rollback versions follow the same date bounds as snapshot versions, and a catalog
request's `resolved_at` must also be non-negative, including when its catalog or
version does not exist. These are input errors rather than lookup rejections.

### Input errors versus rejections

Expand Down Expand Up @@ -244,6 +251,16 @@ Success:
`false` `allowed` always carries a citation — the model refuses to construct a
citation-free rejection.

`equals` and `not_equals` compare JSON values recursively: list order matters,
object key order does not, and different key sets remain different even when values
are `null`. Boolean/integer equality follows the reference model (`true == 1`,
`false == 0`), including inside lists and objects. `in` and `not_in` test list
elements, string substrings, or object keys; object values are not members. A
non-string context value cannot be a substring or an object key. A missing context
field matches neither binary operator, including its negated form. Rust's rule priority
comparison handles the entire signed 64-bit range without negating its minimum;
portable JavaScript inputs remain limited by exact `Number` representation.

### `catalog_version_info`

Request:
Expand Down Expand Up @@ -368,6 +385,16 @@ The `e log e` term is sorting the three id lists; every other bound is a linear
the relevant history. These are the bounds of the underlying models; the wrapper adds
parsing and serialization, both linear in the payload, and nothing else.

The policy rows treat predicate values as bounded. For structured values, multiply
their predicate work by the comparison/membership cost: recursive equality visits
the compared values, with an additional logarithmic key lookup per object entry in
Rust's default ordered maps. PHP and JavaScript use expected constant-time object-key
lookup; JavaScript also allocates key arrays during object comparison. Python's
effective-dated model evaluation streams version resolution and winner selection in
constant auxiliary space, excluding value-comparison recursion and the loaded
document. Each matching rule's predicates run once; the other ports retain their
existing rule-pool storage.

## Limitations

- The document is supplied by the caller. Nothing here fetches, scrapes or embeds any
Expand All @@ -380,6 +407,11 @@ parsing and serialization, both linear in the payload, and nothing else.
- Policy evaluation covers the closed `PolicyScope` / `PolicyOperator` vocabulary.
An unrecognised scope or operator fails document admission rather than being
silently ignored — the guarantee `domain/policy/model.py` already makes.
- PHP's array boundary cannot recover the object/list distinction erased by an
associative JSON decoder: `{}` and `[]`, or an object with consecutive numeric
keys, can become the same PHP array. Portable object-valued predicates should use
nonempty objects with nonnumeric string keys. This limitation does not affect
list ordering or ordinary string-key object comparisons.
- A JSON number must be written without a fractional part. `1` is an integer; `1.0` and
`1e3` are not, and Python, PHP and Rust refuse them. JavaScript cannot tell the
difference — `JSON.parse` gives the same `Number` for `1` and `1.0` — so this is the
Expand Down
2 changes: 1 addition & 1 deletion docs/GUARANTEES.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ silently — if you need them, they belong in your own layer above this library.

## Status of this release

Version `1.8.0` freezes the public API. Field names, status codes, the objective
Version `1.9.0` freezes the public API. Field names, status codes, the objective
vector, the numeric policy and the validation rules do not change without a major
version, so any `1.x` is a safe upgrade from any earlier `1.x`. A caret or tilde
constraint on `1.0` is enough; an exact pin is no longer required.
Expand Down
9 changes: 6 additions & 3 deletions docs/PUBLIC-API.md
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,9 @@ All counters are non-negative integers. A zero means the selected solver did not
that phase; it does not mean the metric is unsupported. Compatibility fields
`algorithm.placements_attempted` and `algorithm.candidates_evaluated` remain and must
equal `metrics.orientations_considered` and `metrics.feasible_candidates`.
For a multi-start portfolio these counters belong to the selected start; the complete
list of starts is recorded separately in `termination.starts`. Counters are not summed
across starts.
Metrics are diagnostics: they never participate in feasibility, ranking or the
objective vector.

Expand Down Expand Up @@ -469,9 +472,9 @@ bounds its work by counting must get the same answer on any machine, and a start
own share of the clock would lose quietly, with no flag saying the answer depended on the
machine's speed. When the clock does fire, the portfolio-wide deadline is reached and
`time_limit_reached` is true. Rust and the JavaScript fallback always run their starts against
the one portfolio deadline. Each start keeps its own counters against the whole budget in
Python, PHP and Rust; the JavaScript fallback hands each later start what the earlier ones
left of one budget, so the same number buys less search there.
the one portfolio deadline. Each start keeps its own counters against the whole budget, in all
four engines; until 1.9.0 the JavaScript fallback handed each later start what the earlier ones
left of one budget, so the same number bought less search there.

**Read `any_start_truncated` when the answer must not depend on the budget.** The selected
start can finish while another start, one that might have won, was stopped by the budget; the
Expand Down
Loading
Loading