@mark1russell7/packing 0.2.5 → 0.2.6

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package",
3
3
  "name": "@mark1russell7/packing",
4
- "version": "0.2.5",
4
+ "version": "0.2.6",
5
5
  "description": "Rectangle packing: shelf, guillotine and skyline strategies, push cascades and measures of a layout.",
6
6
  "keywords": [
7
7
  "rectangle-packing",
@@ -49,7 +49,7 @@
49
49
  "node": ">=22.12"
50
50
  },
51
51
  "dependencies": {
52
- "@mark1russell7/layout-contract": "^0.2.4"
52
+ "@mark1russell7/layout-contract": "^0.2.6"
53
53
  },
54
54
  "devDependencies": {
55
55
  "@mark1russell7/cue": "^1.0.2",
package/src/index.ts CHANGED
@@ -80,12 +80,15 @@ export {
80
80
  } from "./adapters.ts";
81
81
  export { fromRects, toRects } from "./convert.ts";
82
82
  export { packWith, strategies, strategy } from "./registry.ts";
83
+ export { hardBreaks, packPortfolio, PORTFOLIO_LEVELS, PORTFOLIO_STAGE, type PackPortfolioOptions, type PortfolioEntry } from "./portfolio.ts";
84
+ export type { Portfolio, PortfolioPick, PortfolioTrace, MemberTrace, SelectLevel } from "@mark1russell7/layout-contract";
83
85
  export { checkHard, type ContainerTest, type PackHard } from "./check.ts";
84
86
  export {
85
87
  canonicalJson,
86
88
  countCell,
87
89
  fnv1a32,
88
90
  hashInput,
91
+ outputHash,
89
92
  round3,
90
93
  toPackInput,
91
94
  type CorpusCell,
@@ -1,7 +1,7 @@
1
1
  // The input file `layout-input/1` of `docs/FORMAT.md`: its types, its numbers, its canonical form and its hash.
2
2
 
3
3
  import type { LayoutContainer, LayoutEdge, LayoutGenerator, LayoutInput } from "@mark1russell7/layout-contract";
4
- import type { PackInput } from "./contract.ts";
4
+ import type { PackInput, PackOutput } from "./contract.ts";
5
5
  import { sha256Hex, utf8 } from "./sha256.ts";
6
6
  import type { Cell } from "./strategy.ts";
7
7
 
@@ -90,6 +90,16 @@ export function hashInput(file: LayoutInput): string {
90
90
  return sha256Hex(canonicalJson(rest)).slice(0, 16);
91
91
  }
92
92
 
93
+ /**
94
+ * This function gives the `outputHash` of an output. It is the first 16 hex characters of the SHA-256 of the canonical form of
95
+ * its placements, sorted by id. The canonical form rounds each number with `round3`. Thus the order of the list does not count.
96
+ * The records of the benchmark and the key of `packPortfolio` use it. It is not `outputHash` of the contract, which uses a grid.
97
+ */
98
+ export function outputHash(output: Pick<PackOutput, "placements">): string {
99
+ const sorted = output.placements.toSorted((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
100
+ return sha256Hex(canonicalJson(sorted)).slice(0, 16);
101
+ }
102
+
93
103
  /** This function gives the scale of a count: the nearest of 1, 10, 100, 1000 and 10000 on a log scale. The count 0 gives `1`. */
94
104
  export function countCell(count: number): CountScale {
95
105
  if (count <= 3) return "1";
@@ -0,0 +1,103 @@
1
+ // The portfolio of roadmap item k20, part 1 (`docs/PACKER.md`, "The portfolio"). A portfolio packs an input with each
2
+ // member and keeps the best output by the one pick rule `selectBest` of the contract. Nothing default uses it.
3
+
4
+ import { pickMoves, portfolio, type Member, type Portfolio, type PortfolioKey, type PortfolioPick, type SelectLevel } from "@mark1russell7/layout-contract";
5
+ import { checkHard } from "./check.ts";
6
+ import type { PackInput, PackOutput } from "./contract.ts";
7
+ import { outputHash } from "./layout-input.ts";
8
+ import { MEASURE_SPECS, measure, type LayoutMeasures } from "./measures.ts";
9
+ import { packWith, strategy } from "./registry.ts";
10
+
11
+ /** A member of `packPortfolio`: a strategy of the registry with some options. The id names the member in the trace. */
12
+ export interface PortfolioEntry {
13
+ id: string;
14
+ /** The id of a strategy of the registry */
15
+ strategy: string;
16
+ /** The options of the member over the defaults of the strategy */
17
+ options?: unknown;
18
+ }
19
+
20
+ /** The options of `packPortfolio`. The time limit and the member limit are those of `portfolio` of the contract. */
21
+ export interface PackPortfolioOptions {
22
+ /** The id of the portfolio. The default is `portfolio`. */
23
+ id?: string;
24
+ /** The seed of each member. The default is 0, as for `packWith`. */
25
+ seed?: number;
26
+ /** The time limit in milliseconds. Without it, each member runs. A cut member makes the pick not deterministic. */
27
+ budgetMs?: number;
28
+ /** The largest count of members that run */
29
+ maxMembers?: number;
30
+ /** The clock in milliseconds. The default is `performance.now`. */
31
+ now?: () => number;
32
+ }
33
+
34
+ /** The id of the trace stage of `packPortfolio`. Its measure `move.pick` is 1 when the pick is not member 0. */
35
+ export const PORTFOLIO_STAGE = "portfolio";
36
+
37
+ /**
38
+ * The levels of the pick of `packPortfolio` and of the portfolio report of the benchmark, in order. Level 0 is `hard`.
39
+ *
40
+ * - `hard`: the count of hard breaks of `checkHard` with `exactContainer`. Lower is better, with the minimum effect 0.
41
+ * - `pack.overflow.excessArea`: the overflow area above its bound. Lower is better.
42
+ * - `pack.fill`: the fill of the hull. Higher is better.
43
+ */
44
+ export const PORTFOLIO_LEVELS: readonly SelectLevel[] = Object.freeze([
45
+ { id: "hard", spec: { direction: "lower", minEffect: 0 } },
46
+ { id: "pack.overflow.excessArea", spec: MEASURE_SPECS["pack.overflow.excessArea"] },
47
+ { id: "pack.fill", spec: MEASURE_SPECS["pack.fill"] },
48
+ ] as SelectLevel[]);
49
+
50
+ /** This function gives the count of the hard breaks of an output: the sum of `checkHard` with `exactContainer`. */
51
+ export function hardBreaks(input: PackInput, output: PackOutput): number {
52
+ return Object.values(checkHard(input, output, undefined, { exactContainer: true })).reduce((sum, n) => sum + n, 0);
53
+ }
54
+
55
+ /**
56
+ * This function makes an opt-in portfolio of strategies of the registry with `portfolio` of the contract.
57
+ * Its `pick(input)` packs the input with each member and gives the output of the winner, its id and the trace.
58
+ *
59
+ * - A member is a strategy id, or a `PortfolioEntry` with options. The members run in the given order.
60
+ * - The key is the hard breaks, then `pack.overflow.excessArea`, then `pack.fill`. `PORTFOLIO_LEVELS` gives the specs.
61
+ * - The hash of the key is `outputHash`. `selectBest` of the contract picks the winner.
62
+ * - The output of the winner gets the trace stage `portfolio` with `move.pick` of `pickMoves`.
63
+ * - The function throws a `RangeError` for an unknown strategy, an empty list and two members with the same id.
64
+ */
65
+ export function packPortfolio(members: readonly (string | PortfolioEntry)[], options: PackPortfolioOptions = {}): Portfolio<PackInput, PackOutput> {
66
+ const seed = options.seed ?? 0;
67
+ const list: Member<PackInput, PackOutput>[] = members.map((m) => {
68
+ const e = typeof m === "string" ? { id: m, strategy: m } : m;
69
+ strategy(e.strategy);
70
+ return { id: e.id, run: (input: PackInput): PackOutput => packWith(e.strategy, input, e.options, seed) };
71
+ });
72
+ // Each level reads the measures of one output, so the measures of an output are computed one time
73
+ const cache = new WeakMap<PackOutput, LayoutMeasures>();
74
+ const measured = (input: PackInput, output: PackOutput): LayoutMeasures => {
75
+ let m = cache.get(output);
76
+ if (m === undefined) {
77
+ m = measure(input, output);
78
+ cache.set(output, m);
79
+ }
80
+ return m;
81
+ };
82
+ const key: PortfolioKey<PackInput, PackOutput> = {
83
+ hard: hardBreaks,
84
+ levels: PORTFOLIO_LEVELS.slice(1).map((level) => ({
85
+ id: level.id,
86
+ spec: level.spec,
87
+ value: (input: PackInput, output: PackOutput): number => measured(input, output)[level.id as "pack.fill" | "pack.overflow.excessArea"],
88
+ })),
89
+ hash: outputHash,
90
+ };
91
+ const limits = {
92
+ ...(options.budgetMs !== undefined ? { budgetMs: options.budgetMs } : {}),
93
+ ...(options.maxMembers !== undefined ? { maxMembers: options.maxMembers } : {}),
94
+ ...(options.now !== undefined ? { now: options.now } : {}),
95
+ };
96
+ const inner = portfolio(options.id ?? PORTFOLIO_STAGE, list, key, limits);
97
+ const pick = (input: PackInput): PortfolioPick<PackOutput> => {
98
+ const got = inner.pick(input);
99
+ const stage = { id: PORTFOLIO_STAGE, measures: { "move.pick": pickMoves(got.trace).pick } };
100
+ return { ...got, output: { ...got.output, trace: [...(got.output.trace ?? []), stage] } };
101
+ };
102
+ return { id: inner.id, pick, run: (input: PackInput): PackOutput => pick(input).output };
103
+ }