@packvium/engine 0.1.1 → 0.1.2
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.
- package/README.md +16 -0
- package/examples/objectives.mjs +114 -0
- package/index.js +1 -1
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -120,6 +120,22 @@ node examples/basic.mjs
|
|
|
120
120
|
The native addon is optional. `npm install` works on unsupported platforms too; call
|
|
121
121
|
`backend()` if your application needs to know which implementation handled a request.
|
|
122
122
|
|
|
123
|
+
## The Packvium family
|
|
124
|
+
|
|
125
|
+
One request and result contract, implemented independently in four engines (Rust,
|
|
126
|
+
Python, PHP, JavaScript) and held to identical placements on a shared fixture set.
|
|
127
|
+
Pick the package for your stack; mixing them in one system is safe.
|
|
128
|
+
|
|
129
|
+
| Package | Install | Source |
|
|
130
|
+
| --- | --- | --- |
|
|
131
|
+
| Python — [`packvium`](https://pypi.org/project/packvium/) | `pip install packvium` | [packvium-python](https://github.com/toxakara/packvium-python) |
|
|
132
|
+
| PHP — [`packvium/packvium`](https://packagist.org/packages/packvium/packvium) | `composer require packvium/packvium` | [packvium-php](https://github.com/toxakara/packvium-php) |
|
|
133
|
+
| Rust — [`packvium`](https://crates.io/crates/packvium) | `packvium = "0.1"` | [packvium-rust](https://github.com/toxakara/packvium-rust) |
|
|
134
|
+
| Node.js — [`@packvium/engine`](https://www.npmjs.com/package/@packvium/engine) | `npm install @packvium/engine` | [packvium-node](https://github.com/toxakara/packvium-node) |
|
|
135
|
+
| Browser / WebAssembly — [`@packvium/browser`](https://www.npmjs.com/package/@packvium/browser) | `npm install @packvium/browser` | [packvium-wasm](https://github.com/toxakara/packvium-wasm) |
|
|
136
|
+
| 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) |
|
|
137
|
+
| Python native selector — `packvium-native` | from source until the native wheels ship | [packvium-python-adapter](https://github.com/toxakara/packvium-python-adapter) |
|
|
138
|
+
|
|
123
139
|
## API and support
|
|
124
140
|
|
|
125
141
|
TypeScript declarations are included. See the package's `index.d.ts` for the complete
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Objectives: six ways to be "best", and the scenes where they disagree.
|
|
3
|
+
*
|
|
4
|
+
* Run it:
|
|
5
|
+
*
|
|
6
|
+
* node examples/objectives.mjs
|
|
7
|
+
*
|
|
8
|
+
* Every solve returns the arrangement that scores best -- but "best" is a choice, and it
|
|
9
|
+
* is the one setting most likely to make the library look wrong when it is merely
|
|
10
|
+
* answering a different question than you meant to ask. This example builds scenes where
|
|
11
|
+
* two objectives genuinely pick different containers, so the difference is visible
|
|
12
|
+
* rather than asserted.
|
|
13
|
+
*
|
|
14
|
+
* The score is always a lexicographic array of exact integers, never a float, and its
|
|
15
|
+
* first key is always the unpacked count: no objective will ever leave an item behind to
|
|
16
|
+
* save money. The same request handed to the Python, PHP or Rust engine prints the same
|
|
17
|
+
* vector.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { pack } from '../index.js';
|
|
21
|
+
|
|
22
|
+
const widgets = [{
|
|
23
|
+
id: 'widget', quantity: 8,
|
|
24
|
+
dimensions: { length: '100', width: '100', height: '100' },
|
|
25
|
+
weight: '500 g',
|
|
26
|
+
}];
|
|
27
|
+
|
|
28
|
+
const solve = (configuration, containers) => {
|
|
29
|
+
const result = pack({ units: { length: 'mm' }, configuration, items: widgets, containers });
|
|
30
|
+
const chosen = result.containers.length > 0 ? result.containers[0].container_type : 'none';
|
|
31
|
+
return `${chosen.padEnd(6)} score=${JSON.stringify(result.score)}`;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const box = (id, side, extra = {}) => ({
|
|
35
|
+
id,
|
|
36
|
+
inner_dimensions: { length: side, width: side, height: side },
|
|
37
|
+
max_payload: '20 kg',
|
|
38
|
+
...extra,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const weightPricing = {
|
|
42
|
+
dimensional_weight_divisor: 5000,
|
|
43
|
+
dimensional_weight_length_unit: 'cm',
|
|
44
|
+
dimensional_weight_weight_unit: 'kg',
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const snug = box('snug', '300', { cost_minor: 500 });
|
|
48
|
+
const roomy = box('roomy', '400', { cost_minor: 150 });
|
|
49
|
+
|
|
50
|
+
// `default` -- fewest containers, then tightest fit. What you want when the boxes are
|
|
51
|
+
// interchangeable and you are simply trying not to open another one.
|
|
52
|
+
console.log('default ', solve({ seed: 42 }, [snug, roomy]));
|
|
53
|
+
|
|
54
|
+
// `lowest_cost` -- the cheapest *packaging*. `cost_minor` is what the box costs you, so
|
|
55
|
+
// this is the objective for a warehouse buying cartons, not a shipper paying a carrier.
|
|
56
|
+
console.log('lowest_cost ', solve({ seed: 42, objective: 'lowest_cost' }, [snug, roomy]));
|
|
57
|
+
|
|
58
|
+
// `shipping_cost` -- carrier-billable *weight*: the greater of actual gross weight and
|
|
59
|
+
// dimensional weight. A big light box can bill more than a small heavy one, which is why
|
|
60
|
+
// this is not the same objective as `lowest_cost`. It needs a divisor and refuses rather
|
|
61
|
+
// than guessing one, because a wrong divisor silently misprices every shipment.
|
|
62
|
+
console.log('shipping_cost ',
|
|
63
|
+
solve({ seed: 42, objective: 'shipping_cost', ...weightPricing }, [snug, roomy]));
|
|
64
|
+
|
|
65
|
+
// `lowest_landed_cost` -- carrier-billable *money*, with the rate card arriving as
|
|
66
|
+
// request data. Weight and money do not always agree: a bracket step, or a minimum
|
|
67
|
+
// charge, can make the cheaper shipment the heavier one. Below the roomy box bills
|
|
68
|
+
// heavier (12,800 g of dimensional weight against the snug box's 5,400) and still costs
|
|
69
|
+
// less, because the snug box's carrier charges a steep first bracket.
|
|
70
|
+
const dearPerGram = box('snug', '300', {
|
|
71
|
+
rate_table: { weight_brackets_g: [6000, 20000], prices_minor: [2400, 3100] },
|
|
72
|
+
});
|
|
73
|
+
const cheapPerGram = box('roomy', '400', {
|
|
74
|
+
rate_table: { weight_brackets_g: [6000, 20000], prices_minor: [900, 1500] },
|
|
75
|
+
});
|
|
76
|
+
const byMoney = { seed: 42, objective: 'lowest_landed_cost', ...weightPricing };
|
|
77
|
+
console.log('lowest_landed_cost ', solve(byMoney, [dearPerGram, cheapPerGram]));
|
|
78
|
+
|
|
79
|
+
// A rate card that stops short of the shipment is a refusal, never a silent clamp to the
|
|
80
|
+
// top bracket -- you would otherwise be quoted a price the carrier never published.
|
|
81
|
+
const tooNarrow = box('roomy', '400', {
|
|
82
|
+
rate_table: { weight_brackets_g: [2000], prices_minor: [900] },
|
|
83
|
+
});
|
|
84
|
+
try {
|
|
85
|
+
solve(byMoney, [tooNarrow]);
|
|
86
|
+
} catch (refusal) {
|
|
87
|
+
console.log('lowest_landed_cost* ', `refused: ${refusal.message}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// `open_dimension_height` -- pack into the shortest stack, for a lidless container or a
|
|
91
|
+
// pallet that has to clear a doorway.
|
|
92
|
+
console.log('open_dimension_height',
|
|
93
|
+
solve({ seed: 42, objective: 'open_dimension_height' }, [snug, roomy]));
|
|
94
|
+
|
|
95
|
+
// `maximum_value` -- when not everything fits, leave the *cheap* things behind. It orders
|
|
96
|
+
// by value; it does not solve the knapsack problem to optimality. `quantity: 1` on the
|
|
97
|
+
// container is what makes it a choice at all -- with an unlimited supply the packer
|
|
98
|
+
// simply opens another box.
|
|
99
|
+
const scarce = pack({
|
|
100
|
+
units: { length: 'mm' },
|
|
101
|
+
configuration: { seed: 42, objective: 'maximum_value' },
|
|
102
|
+
items: [
|
|
103
|
+
{ id: 'gold', quantity: 2, dimensions: { length: '100', width: '100', height: '100' }, weight: '500 g', value: 90000 },
|
|
104
|
+
{ id: 'gravel', quantity: 2, dimensions: { length: '100', width: '100', height: '100' }, weight: '500 g', value: 10 },
|
|
105
|
+
],
|
|
106
|
+
containers: [{
|
|
107
|
+
id: 'tiny', quantity: 1,
|
|
108
|
+
inner_dimensions: { length: '200', width: '100', height: '100' },
|
|
109
|
+
max_payload: '20 kg',
|
|
110
|
+
}],
|
|
111
|
+
});
|
|
112
|
+
const kept = scarce.containers.flatMap((c) => c.placements.map((p) => p.item_type)).sort();
|
|
113
|
+
const left = (scarce.unpacked_items ?? []).map((u) => u.item_type ?? u.item_id).sort();
|
|
114
|
+
console.log('maximum_value ', `packed=${JSON.stringify(kept)} left behind=${JSON.stringify(left)}`);
|
package/index.js
CHANGED
|
@@ -29,7 +29,7 @@ export function rebalanceWeight(request,result,{maxMoves=64}={}){
|
|
|
29
29
|
}
|
|
30
30
|
return rebalanceFallback(request,result,{maxMoves});
|
|
31
31
|
}
|
|
32
|
-
export const version=()=>native?.version?.()??'0.1.
|
|
32
|
+
export const version=()=>native?.version?.()??'0.1.2-js-fallback';
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
35
|
* The exported commercial and control-plane API: a quote, a policy decision and catalog
|
package/package.json
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name":"@packvium/engine",
|
|
3
|
-
"version":"0.1.
|
|
3
|
+
"version":"0.1.2",
|
|
4
4
|
"description":"Native-first 3D cartonization with deterministic JS fallback",
|
|
5
|
+
"keywords":["3d-bin-packing","bin-packing","cartonization","packing","container-loading","logistics","shipping","deterministic"],
|
|
6
|
+
"homepage":"https://github.com/toxakara/packvium-node#readme",
|
|
7
|
+
"repository":{"type":"git","url":"git+https://github.com/toxakara/packvium-node.git"},
|
|
8
|
+
"bugs":{"url":"https://github.com/toxakara/packvium-node/issues"},
|
|
5
9
|
"type":"module",
|
|
6
10
|
"main":"index.js",
|
|
7
11
|
"types":"index.d.ts",
|
|
8
12
|
"exports":{".":{"types":"./index.d.ts","import":"./index.js"}},
|
|
9
13
|
"files":["index.js","fallback.js","contact-graph.js","policy.js","commerce.js","commerce-model.js","examples","index.d.ts","README.md","SECURITY.md"],
|
|
10
14
|
"engines":{"node":">=16"},
|
|
11
|
-
"optionalDependencies":{"@packvium/native":"0.1.
|
|
15
|
+
"optionalDependencies":{"@packvium/native":"0.1.2"},
|
|
12
16
|
"scripts":{
|
|
13
17
|
"test":"node --test \"test/*.test.mjs\"",
|
|
14
18
|
"test:legacy":"node test/legacy-conformance.mjs"
|