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
83 changes: 83 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,89 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). As of `1.
public API, the request and result schemas, the numeric and units policy, the validation
rules and the compatibility policy are frozen: breaking any of them costs a major version.

## [1.1.0]

An additive release on the 1.0.0 freeze. Route-aware unloading becomes a property of the
container rather than of the whole solve, and the result contract grows the reserved,
typed shape a future optimality gap needs. Every schema change is additive: a request that
omits the new field gets the answer it got before, and both frozen public API surfaces are
unchanged.

The four engines' candidate hot paths were also profiled and rewritten. That work changes
no result — all 399 corpus fixtures are byte-identical — and it is in this release because
two of its findings were correctness fixes rather than speed.

### Added

- **Doors on a container: `container.access_directions`.** Naming which walls an item can
be pulled through, so that nothing due at a later stop stands between an earlier item and
a door. Previously this could only be stated once for a whole solve; a container now
states its own doors and falls back to that setting when it states none. An empty list
leaves the rule inert — it is not read as a sealed container, and it is deliberately not
read as all six walls, which would switch a real constraint on for callers who never set
the field. Implemented in all four engines, and refused by none.
- **A reserved shape for reporting an optimality gap.** The result schema now declares the
names, types and ceilings a gap will use, together with the rule that an attained bound
carries no gap at all rather than a zero. No engine emits any of them yet. The existing
extension point on that object stays open, so a producer accepted by the 1.0 schema is
still accepted by 1.1.

### Changed

- **The solvers do less repeated work.** Byte-identical results everywhere, with the
largest gains where a scene has many contacts: on a 120-item contact-heavy request the
Rust engine goes from 10.1 s to 1.4 s and the JavaScript fallback from 1.70 s to 0.76 s;
on a 200-item adversarial free-space scene JavaScript goes from 3.25 s to 0.58 s. Some
scenes are unchanged and one is marginally slower. **This is not a speed-leadership
claim** — measured against other libraries on identical hardware, that claim is false on
latency, and no Packvium surface makes it.
- **Every published package manifest now names its author, licence, repository, issue
tracker and homepage.** Absent fields are read as an anonymous package.
- **`@packvium/engine` no longer declares `@packvium/native` as an optional dependency.**
That package is not published, so the entry named something npm could not fetch. Nothing
a caller can observe changes: the install already succeeded and answered from the
JavaScript engine, and `index.js` still loads `@packvium/native` by literal specifier, so
installing it yourself alongside the engine still selects the compiled backend.

### Fixed

- **The Rust engine accepted placements the validator refused.** Support-ratio comparison
used a floating-point epsilon wide enough to admit an area a whole square tick short of
the requirement. It is now the same exact integer rule the other three engines use.
- **The JavaScript fallback accepted door names no other engine would.** An unknown
direction was refused only on requests that took the general solving path; a request
simple enough to be answered by the compact-grid shortcut was answered instead of
refused.
- **The JavaScript fallback broke identifier ties by host locale.** That is neither stable
across machines nor equal to the code-point order the other engines use, so two hosts
could order the same items differently. Every tie-break now uses the shared code-point
order.
- **A container's doors could be answered from another container's cached corridor.** Two
containers of the same size with different doors are two different questions; the cache
key did not separate them, which could have silently accepted a placement that walls an
item in.
- **The PHP package could not be loaded on PHP 7.3 or 7.4.** The package advertises
`php: >=7.3` and carries a second, downgraded source tree for runtimes below 8.2. In
`1.0.0` that tree contained one line of PHP 8.1 syntax, so the whole of it failed to
parse on both older runtimes. `1.1.0` is the first version whose legacy tree loads. If
you are on PHP 8.2 or newer you were never affected — the canonical tree is what your
runtime selects.

- **The engine package no longer loads its native backend through a computed specifier.**
Every module the package can load can now be resolved by reading the source. Behaviour
is identical: an absent, unbuilt or incompatible native addon still means the pure
JavaScript engine answers instead.

### Not claimed

- **A reported optimality gap.** The names are reserved and typed; nothing emits them.
- **`container.pallet_overhang_limit`.** Reserved in the request schema and refused by all
four engines.
- **Identical placements across engines.** Unchanged from 1.0.0: different engines may
return different, equally valid arrangements, and that is measured and budgeted.
- **Optimal packings for arbitrary requests.** 3D packing remains NP-hard.
- **Fastest engine.** Still false on latency and still claimed nowhere.

## [1.0.0]

The stable core release. It freezes the contract that already exists rather than adding a
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ dependencies**, exact integer geometry.
Full documentation, the constraint reference and benchmarks live at
[packvium.com](https://packvium.com).

> **Version 1.0.0 — the public API is frozen.** Field names, status codes and the
> **Version 1.1.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
2 changes: 1 addition & 1 deletion docs/GUARANTEES.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ silently — if you need them, they belong in your own layer above this library.

## Status of this release

Version `1.0.0` freezes the public API. Field names, status codes, the objective
Version `1.1.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
8 changes: 7 additions & 1 deletion docs/PUBLIC-API.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,13 @@ move backwards. This is a testing seam, not a serialized request field.
- three independent result facts:
- `feasibility.code`: `feasible`, `infeasible`, or `unknown`;
- `termination.code`: `complete`, `time_limit`, `effort_limit`, or `error`;
- `optimality.code`: `proven_optimal`, `proven_infeasible`, `best_found`, or `not_proven`;
- `optimality.code`: `proven_optimal`, `proven_infeasible`, `best_found`, or `not_proven`.
The 1.1.0 freeze reserved and typed `gap_key`, `absolute_gap` and `relative_gap`
alongside it. The pre-1.1.0 extension point remains open: closing the object would
make a previously valid producer fail a 1.1.0 validator, which is a breaking change
and therefore not legal in this minor release. No engine emits the reserved fields
yet, so `code` is still the only key any Packvium result carries. See
OPTIMALITY-CERTIFICATES.md;
- legacy `status`: `optimal`, `feasible`, `best_found`, `time_limit`, `infeasible`, or `invalid_result`;
- packed containers with exact coordinates, dimensions, rotations, and a
`centre_of_mass_offset_ppm` — the weighted centre of mass's exact-integer Chebyshev
Expand Down
2 changes: 2 additions & 0 deletions docs/UNITS-AND-NUMERICS.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ Conversion supports floor, ceiling and ties-to-even nearest rounding. Applicatio

Feasibility checks are integer-only, including the support ratio: the requested fraction is converted once to parts per million and compared as `supported_area >= floor(base_area x ratio_ppm / 10^6)`, never as a float division against an epsilon. The independent validator rechecks boundaries and intersections using exact integers.

The Rust solver and validator were the exception until 2026-09-02: both compared `area / base_area + 1e-12 < ratio` in `f64`. On a real-sized base (a square centimetre is `2.56 x 10^10` square ticks) an area one square tick short of the requirement sits well inside that epsilon, so Rust admitted a placement the shared validator refused. Both now go through `support_area_sufficient`, the rule above; the `f64` ratio survives only as the reported `support_ratio` on the placement record.

Ordering keys are exact too. Two volumes that differ by one cubic tick must not collapse onto the same value, or two implementations of one algorithm can order the same items differently.

## PHP integer limits
Expand Down
12 changes: 10 additions & 2 deletions examples/serialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,14 @@
"profile": "balanced",
"seed": 42,
"top_k": 2,
# An example must not change answer merely because the machine is busy. `top_k`
# asks the portfolio for runners-up, and how many it finds is bounded by the
# *wall clock* unless a budget says otherwise -- so without this line two runs on
# a loaded host can print a different number of alternatives, which is
# exactly. gave every conformance fixture an explicit budget for this
# reason; the examples were not part of that sweep. The value is a safety fuse,
# not a target: nothing here comes close to it.
"time_limit_ms": 60_000,
},
"items": [
{
Expand Down Expand Up @@ -131,14 +139,14 @@

# And a field this engine has named as not-yet-implemented is refused explicitly, so a
# request written for a newer engine fails loudly instead of being half-honoured. The list
# below is the engine's own constant, and it is empty: implemented `convex_hull`
# below is the engine's own constant, and it is empty: implemented `convex_hull`
# and `compressible`, the last reserved names left on it, so this engine now serves every
# field and every `shape_type` value the schema defines.
print("fields this engine refuses by name:",
{scope: fields for scope, fields in UNSUPPORTED_FIELDS.items() if fields} or "none")

# Caught up is the right state and a poor demonstration, so the guard takes its lists as
# parameters -- the same hook its own tests use. Passing the value retired shows
# parameters -- the same hook its own tests use. Passing the value retired shows
# the refusal a caller still gets from an engine that is behind, and shows it naming the
# *value* rather than the field: `rigid_cuboid` is the default and is implemented, so a
# caller who spells the default out must be served, not refused.
Expand Down
4 changes: 4 additions & 0 deletions examples/shapes.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
request runs unchanged against the Python, PHP, Rust and JavaScript engines.
"""

# `list | None` in a signature is PEP 604, which needs Python 3.10 at runtime. This
# package supports 3.9, so the annotation is deferred rather than evaluated.
from __future__ import annotations

from packvium import pack_from_dict


Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "packvium"
version = "1.0.0"
version = "1.1.0"
description = "Deterministic, extensible 3D cartonization and rectangular bin-packing library"
readme = "README.md"
requires-python = ">=3.9"
Expand Down
6 changes: 3 additions & 3 deletions src/packvium/bounds.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
The mathematics is fixed by [docs/OPTIMALITY-CERTIFICATES.md](../../../docs/OPTIMALITY-CERTIFICATES.md)
and `scripts/optimality_bounds.py` is the independent oracle. This module is written from
the document and never imports the oracle, so the property tests compare two
implementations rather than one implementation with itself -- the discipline set
for irregular items and restated here.
implementations rather than one implementation with itself -- the discipline set
for irregular items and restated here.

What a bound is for. Every solver in this project is a heuristic: it returns an
arrangement and has no notion of what it did not try. A bound is the other half of that
Expand All @@ -22,7 +22,7 @@
are *dropped* rather than scaled -- a bound that is sometimes wrong is not a bound.

`convex_hull` and `compressible` have exactly the same property and the document does not
say so, because was written before the irregular shapes existed. A hull occupies
say so, because was written before the irregular shapes existed. A hull occupies
its hull rather than its bounding box, and a compressible item gives up height under load;
summing nominal box volumes over-states what a solution must carry, which over-states the
container count.
Expand Down
Loading
Loading