@mark1russell7/layout-contract 0.2.13 → 0.2.15
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 +15 -2
- package/build/index.d.ts +307 -5
- package/build/index.d.ts.map +1 -1
- package/build/index.js +980 -16
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/families.ts +826 -0
- package/src/hash.ts +4 -2
- package/src/index.ts +1 -0
- package/src/input.ts +67 -2
- package/src/rules.ts +83 -9
- package/src/strategy-space.ts +84 -4
- package/src/turn.ts +8 -2
package/src/hash.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { canonicalInput, type LayoutInput } from "./input.ts";
|
|
1
2
|
import { RECORD_EPS } from "./tolerance.ts";
|
|
2
3
|
import { utf8 } from "./utf8.ts";
|
|
3
4
|
|
|
@@ -131,10 +132,11 @@ export function outputHash(value: unknown, options: HashOptions = {}): string {
|
|
|
131
132
|
/**
|
|
132
133
|
* This function gives the hash of a document. It uses the rule of `outputHash`, but the form does not include the field `hash` of the document.
|
|
133
134
|
* With the default grid, it is the input hash of `docs/FORMAT.md` of `packing`.
|
|
135
|
+
* A document with the schema `layout-input/1` goes through `canonicalInput` first. Thus the order of its `pairGaps` does not change the hash.
|
|
134
136
|
*/
|
|
135
137
|
export function documentHash(doc: object, options: HashOptions = {}): string {
|
|
136
|
-
const { hash: _hash, ...rest } = doc as { readonly hash?: unknown };
|
|
137
|
-
return outputHash(rest, options);
|
|
138
|
+
const { hash: _hash, ...rest } = doc as { readonly hash?: unknown; readonly schema?: unknown };
|
|
139
|
+
return outputHash(rest.schema === "layout-input/1" ? canonicalInput(rest as unknown as LayoutInput) : rest, options);
|
|
138
140
|
}
|
|
139
141
|
|
|
140
142
|
/** One output group: the candidates with the same output hash on each decision input. */
|
package/src/index.ts
CHANGED
package/src/input.ts
CHANGED
|
@@ -1,16 +1,43 @@
|
|
|
1
|
-
import type { Margin, Region, Side } from "./geometry.ts";
|
|
1
|
+
import type { Margin, Point, Region, Side } from "./geometry.ts";
|
|
2
2
|
import type { ShapeKind } from "./shapes.ts";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* A port of a box: a place where edges go into the box.
|
|
6
6
|
* With `x` and `y`, the port is fixed on the border of the box, relative to its top-left corner.
|
|
7
|
-
*
|
|
7
|
+
* With `side` and `along`, the port is fixed at a fraction of its side.
|
|
8
|
+
* With only a side, the router places the port on that side. Without a side, the router picks the side.
|
|
8
9
|
*/
|
|
9
10
|
export interface Port {
|
|
10
11
|
readonly id: string;
|
|
11
12
|
readonly side?: Side;
|
|
12
13
|
readonly x?: number;
|
|
13
14
|
readonly y?: number;
|
|
15
|
+
/**
|
|
16
|
+
* The place of the port on its `side`: a fraction from 0 to 1, from the start of the side.
|
|
17
|
+
* The start of `left` and `right` is the top end. The start of `top` and `bottom` is the left end.
|
|
18
|
+
* It needs a `side`. A port with `along` has no `x` and no `y`.
|
|
19
|
+
*/
|
|
20
|
+
readonly along?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* This function gives the point of a port on the border of its box, relative to the top-left corner of the box.
|
|
25
|
+
* A port with `x` and `y` gives that point. A port with `side` and `along` gives the point at that fraction of the side.
|
|
26
|
+
* Another port gives `undefined`, because the router picks its point.
|
|
27
|
+
*/
|
|
28
|
+
export function portPoint(box: Pick<LayoutBox, "w" | "h">, port: Port): Point | undefined {
|
|
29
|
+
if (port.x !== undefined && port.y !== undefined) return { x: port.x, y: port.y };
|
|
30
|
+
if (port.side === undefined || port.along === undefined) return undefined;
|
|
31
|
+
switch (port.side) {
|
|
32
|
+
case "top":
|
|
33
|
+
return { x: port.along * box.w, y: 0 };
|
|
34
|
+
case "bottom":
|
|
35
|
+
return { x: port.along * box.w, y: box.h };
|
|
36
|
+
case "left":
|
|
37
|
+
return { x: 0, y: port.along * box.h };
|
|
38
|
+
case "right":
|
|
39
|
+
return { x: box.w, y: port.along * box.h };
|
|
40
|
+
}
|
|
14
41
|
}
|
|
15
42
|
|
|
16
43
|
/** A box of an input. In the packer, its name is item. */
|
|
@@ -86,6 +113,19 @@ export interface LayoutGenerator {
|
|
|
86
113
|
readonly seed: number;
|
|
87
114
|
}
|
|
88
115
|
|
|
116
|
+
/**
|
|
117
|
+
* One minimum distance between two boxes (`a` and `b`). It replaces the two margins of the boxes for this pair, and the global `gap`.
|
|
118
|
+
* The order of `a` and `b` has no meaning. A pair without an entry keeps the margins and the `gap`.
|
|
119
|
+
*/
|
|
120
|
+
export interface PairGap {
|
|
121
|
+
readonly a: string;
|
|
122
|
+
readonly b: string;
|
|
123
|
+
/** The minimum distance between the two boxes, in units: a finite number of 0 or more. */
|
|
124
|
+
readonly gap: number;
|
|
125
|
+
/** The count of the channels that the gap has space for. It is information only: a whole number of 0 or more. */
|
|
126
|
+
readonly lanes?: number;
|
|
127
|
+
}
|
|
128
|
+
|
|
89
129
|
/** The cell of an input in the taxonomy: one value for each dimension. The dimensions are in `docs/FORMAT.md` of `packing`. */
|
|
90
130
|
export interface LayoutCell {
|
|
91
131
|
readonly containerSize?: "fixed" | "freeHeight" | "freeWidth" | "freeAspect" | "freeArea";
|
|
@@ -124,6 +164,8 @@ export interface LayoutInput {
|
|
|
124
164
|
readonly container?: LayoutContainer;
|
|
125
165
|
/** The free space between two boxes with their margins. Routes use it. */
|
|
126
166
|
readonly gap?: number;
|
|
167
|
+
/** The distances of some pairs of boxes. An entry replaces the margins and the `gap` for its pair. `canonicalInput` sorts the list. */
|
|
168
|
+
readonly pairGaps?: readonly PairGap[];
|
|
127
169
|
readonly boxes: readonly LayoutBox[];
|
|
128
170
|
readonly edges?: readonly LayoutEdge[];
|
|
129
171
|
readonly obstacles?: readonly Obstacle[];
|
|
@@ -133,6 +175,11 @@ export interface LayoutInput {
|
|
|
133
175
|
*/
|
|
134
176
|
readonly base?: string;
|
|
135
177
|
readonly change?: "add" | "remove" | "resize" | "reorder" | "packed";
|
|
178
|
+
/**
|
|
179
|
+
* The fields that a corpus registry removed, because a dimension value made them wrong (`CorpusDimension.invalidates`).
|
|
180
|
+
* The names are sorted. An input with no removed field has no `invalidated`.
|
|
181
|
+
*/
|
|
182
|
+
readonly invalidated?: readonly string[];
|
|
136
183
|
}
|
|
137
184
|
|
|
138
185
|
/** The input of a packer: a `layout-input/1` file without the edges. */
|
|
@@ -141,6 +188,7 @@ export interface PackInput {
|
|
|
141
188
|
readonly items: readonly Item[];
|
|
142
189
|
readonly obstacles?: readonly Obstacle[];
|
|
143
190
|
readonly gap?: number;
|
|
191
|
+
readonly pairGaps?: readonly PairGap[];
|
|
144
192
|
}
|
|
145
193
|
|
|
146
194
|
/** This function gives the packer input of a file. An input without a container gets `{ fixed: "none" }`, so the packer picks the size. */
|
|
@@ -150,5 +198,22 @@ export function toPackInput(input: LayoutInput): PackInput {
|
|
|
150
198
|
items: input.boxes,
|
|
151
199
|
...(input.obstacles === undefined ? {} : { obstacles: input.obstacles }),
|
|
152
200
|
...(input.gap === undefined ? {} : { gap: input.gap }),
|
|
201
|
+
...(input.pairGaps === undefined ? {} : { pairGaps: input.pairGaps }),
|
|
153
202
|
};
|
|
154
203
|
}
|
|
204
|
+
|
|
205
|
+
/** This function compares two strings by their UTF-16 code units, as `Array.prototype.sort` does without a compare function. */
|
|
206
|
+
const byCode = (x: string, y: string): number => (x < y ? -1 : x > y ? 1 : 0);
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* This function gives the canonical form of an input. `documentHash` uses it for each `layout-input/1` document.
|
|
210
|
+
* Each pair of `pairGaps` gets `a` before `b`, and the list is sorted by `a` and then by `b`.
|
|
211
|
+
* An input without `pairGaps` is the same object, so its hash does not change.
|
|
212
|
+
*/
|
|
213
|
+
export function canonicalInput<T extends LayoutInput>(input: T): T {
|
|
214
|
+
if (!Array.isArray(input.pairGaps)) return input;
|
|
215
|
+
const pairGaps = input.pairGaps
|
|
216
|
+
.map((p): PairGap => (byCode(p.a, p.b) > 0 ? { ...p, a: p.b, b: p.a } : p))
|
|
217
|
+
.sort((x, y) => byCode(x.a, y.a) || byCode(x.b, y.b));
|
|
218
|
+
return { ...input, pairGaps };
|
|
219
|
+
}
|
package/src/rules.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { boundsOf, grow, overlapOf, type Rect, type Region } from "./geometry.ts";
|
|
2
|
-
import type { LayoutBox, LayoutInput, Obstacle } from "./input.ts";
|
|
2
|
+
import type { LayoutBox, LayoutInput, Obstacle, PairGap, Port } from "./input.ts";
|
|
3
3
|
import { applyPack, type PackOutput } from "./output.ts";
|
|
4
4
|
import { SHAPE_KIND_IDS, shapeKindProblem, type ShapeSupport } from "./shapes.ts";
|
|
5
5
|
import { DEFAULT_TOLERANCE, EPS, RECORD_EPS } from "./tolerance.ts";
|
|
@@ -15,20 +15,52 @@ export function placedRect(box: LayoutBox): Rect | undefined {
|
|
|
15
15
|
/**
|
|
16
16
|
* This function counts the pairs of placed boxes whose insides, with their margins, overlap by more than `tolerance` on each axis.
|
|
17
17
|
* A box with a shape counts as its bounding box. That area is larger, thus the count is safe.
|
|
18
|
+
*
|
|
19
|
+
* A pair of `pairGaps`, in either order, uses its `gap` in place of the two margins. One box grows by the gap, and the other box does not grow.
|
|
20
|
+
* Thus the pair breaks the rule only when the boxes are nearer than the gap on each axis. The other pairs do not change.
|
|
18
21
|
*/
|
|
19
|
-
export function countOverlaps(boxes: readonly LayoutBox[], tolerance: number = DEFAULT_TOLERANCE.overlap): number {
|
|
20
|
-
|
|
22
|
+
export function countOverlaps(boxes: readonly LayoutBox[], tolerance: number = DEFAULT_TOLERANCE.overlap, pairGaps?: readonly PairGap[]): number {
|
|
23
|
+
if (pairGaps === undefined || pairGaps.length === 0) return marginOverlaps(boxes, tolerance, undefined);
|
|
24
|
+
const gaps = new Map<string, number>();
|
|
25
|
+
for (const p of pairGaps) {
|
|
26
|
+
const key = pairKey(p.a, p.b);
|
|
27
|
+
if (p.a !== p.b && !gaps.has(key)) gaps.set(key, p.gap);
|
|
28
|
+
}
|
|
29
|
+
const raw = new Map<string, Rect>();
|
|
30
|
+
for (const box of boxes) {
|
|
31
|
+
const r = placedRect(box);
|
|
32
|
+
if (r !== undefined && !raw.has(box.id)) raw.set(box.id, r);
|
|
33
|
+
}
|
|
34
|
+
let count = marginOverlaps(boxes, tolerance, gaps);
|
|
35
|
+
for (const [key, gap] of gaps) {
|
|
36
|
+
const [a, b] = JSON.parse(key) as [string, string];
|
|
37
|
+
const ra = raw.get(a);
|
|
38
|
+
const rb = raw.get(b);
|
|
39
|
+
if (ra === undefined || rb === undefined) continue;
|
|
40
|
+
const o = overlapOf(grow(ra, gap), rb);
|
|
41
|
+
if (o.x > tolerance && o.y > tolerance) count++;
|
|
42
|
+
}
|
|
43
|
+
return count;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** This function gives the key of a pair of box ids, with the same key for each order. */
|
|
47
|
+
const pairKey = (a: string, b: string): string => JSON.stringify(a < b ? [a, b] : [b, a]);
|
|
48
|
+
|
|
49
|
+
/** This function counts the overlaps of the boxes with their margins. It skips the pairs of `skip`. Without `skip`, it is the rule of 0.2.13. */
|
|
50
|
+
function marginOverlaps(boxes: readonly LayoutBox[], tolerance: number, skip: ReadonlyMap<string, number> | undefined): number {
|
|
51
|
+
const rects: { readonly id: string; readonly r: Rect }[] = [];
|
|
21
52
|
for (const box of boxes) {
|
|
22
53
|
const r = placedRect(box);
|
|
23
|
-
if (r !== undefined) rects.push(grow(r, box.margin));
|
|
54
|
+
if (r !== undefined) rects.push({ id: box.id, r: grow(r, box.margin) });
|
|
24
55
|
}
|
|
25
|
-
rects.sort((a, b) => a.x - b.x);
|
|
56
|
+
rects.sort((a, b) => a.r.x - b.r.x);
|
|
26
57
|
let count = 0;
|
|
27
58
|
for (let i = 0; i < rects.length; i++) {
|
|
28
|
-
const a = rects[i]
|
|
59
|
+
const a = rects[i]!.r;
|
|
29
60
|
for (let j = i + 1; j < rects.length; j++) {
|
|
30
|
-
const b = rects[j]
|
|
61
|
+
const b = rects[j]!.r;
|
|
31
62
|
if (b.x >= a.x + a.w - tolerance) break;
|
|
63
|
+
if (skip !== undefined && skip.has(pairKey(rects[i]!.id, rects[j]!.id))) continue;
|
|
32
64
|
const o = overlapOf(a, b);
|
|
33
65
|
if (o.x > tolerance && o.y > tolerance) count++;
|
|
34
66
|
}
|
|
@@ -279,7 +311,8 @@ export function countObstacleHits(
|
|
|
279
311
|
/**
|
|
280
312
|
* This function gives the count of each hard rule of the output of a packer.
|
|
281
313
|
* It applies the output with `applyPack`. Only the placed boxes and the locked boxes count for the overlaps and the container.
|
|
282
|
-
* `no-overlap` counts the pairs of boxes and the pairs of a box and an obstacle.
|
|
314
|
+
* `no-overlap` counts the pairs of boxes and the pairs of a box and an obstacle. A pair of `pairGaps` uses its gap in place of the margins.
|
|
315
|
+
*
|
|
283
316
|
* A locked box without a placement, or with a move of more than `RECORD_EPS`, breaks `locked-kept`.
|
|
284
317
|
* A locked box without a position in the input is a bad input, not a break. `checkInput` finds it.
|
|
285
318
|
* Another box without a placement that is not in `overflow` breaks `complete`.
|
|
@@ -308,7 +341,7 @@ export function packHard(input: LayoutInput, output: PackOutput, tolerance: numb
|
|
|
308
341
|
}
|
|
309
342
|
}
|
|
310
343
|
return {
|
|
311
|
-
"no-overlap": countOverlaps(kept, tolerance) + countObstacleHits(kept, after.obstacles, tolerance),
|
|
344
|
+
"no-overlap": countOverlaps(kept, tolerance, input.pairGaps) + countObstacleHits(kept, after.obstacles, tolerance),
|
|
312
345
|
"in-container": countOutside({ ...after, boxes: kept }, tolerance, options),
|
|
313
346
|
"locked-kept": lockedMoved,
|
|
314
347
|
complete: incomplete,
|
|
@@ -318,6 +351,8 @@ export function packHard(input: LayoutInput, output: PackOutput, tolerance: numb
|
|
|
318
351
|
/**
|
|
319
352
|
* This function gives the problems of an input file. An empty list tells that the file is good.
|
|
320
353
|
* A problem is a bad input, not a break of a hard rule. Thus an engine refuses the input.
|
|
354
|
+
* In `pairGaps`, an unknown box, a pair of one box, a repeated pair in either order, a bad `gap` and bad `lanes` are problems.
|
|
355
|
+
* A port with `along` and no `side`, with `x` or `y`, or with a value outside [0, 1] is a problem.
|
|
321
356
|
*/
|
|
322
357
|
export function checkInput(input: LayoutInput): readonly string[] {
|
|
323
358
|
const problems: string[] = [];
|
|
@@ -329,10 +364,49 @@ export function checkInput(input: LayoutInput): readonly string[] {
|
|
|
329
364
|
if (box.locked === true && (box.x === undefined || box.y === undefined)) problems.push(`box ${box.id}: a locked box has no position`);
|
|
330
365
|
const shape = shapeKindProblem(box);
|
|
331
366
|
if (shape !== undefined) problems.push(shape);
|
|
367
|
+
for (const port of box.ports ?? []) problems.push(...alongProblems(box.id, port));
|
|
332
368
|
}
|
|
333
369
|
if (countPortsOffBorder(input.boxes) > 0) problems.push("a fixed port is not on the border of its box");
|
|
334
370
|
for (const edge of input.edges ?? []) {
|
|
335
371
|
if (!ids.has(edge.source) || !ids.has(edge.target)) problems.push(`edge ${edge.id}: a box of the edge is not in the input`);
|
|
336
372
|
}
|
|
373
|
+
problems.push(...pairGapProblems(input.pairGaps, ids));
|
|
374
|
+
return problems;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/** This function gives the problems of the `along` of a port: no side, a value outside [0, 1], or `x` or `y` with it. A port without `along` has no problem. */
|
|
378
|
+
function alongProblems(boxId: string, port: Port): string[] {
|
|
379
|
+
if (port.along === undefined) return [];
|
|
380
|
+
const at = `box ${boxId} port ${port.id}`;
|
|
381
|
+
const problems: string[] = [];
|
|
382
|
+
if (port.side === undefined) problems.push(`${at}: a port with along has no side`);
|
|
383
|
+
if (typeof port.along !== "number" || !Number.isFinite(port.along) || port.along < 0 || port.along > 1)
|
|
384
|
+
problems.push(`${at}: the along value ${String(port.along)} is not a finite number from 0 to 1`);
|
|
385
|
+
if (port.x !== undefined || port.y !== undefined) problems.push(`${at}: a port with along also has x or y`);
|
|
386
|
+
return problems;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/** This function gives the problems of the `pairGaps` of an input. `ids` has the ids of the boxes. */
|
|
390
|
+
function pairGapProblems(pairGaps: LayoutInput["pairGaps"], ids: ReadonlySet<string>): string[] {
|
|
391
|
+
if (pairGaps === undefined) return [];
|
|
392
|
+
if (!Array.isArray(pairGaps)) return ["pairGaps: the value is not a list"];
|
|
393
|
+
const problems: string[] = [];
|
|
394
|
+
const seen = new Map<string, number>();
|
|
395
|
+
pairGaps.forEach((p: PairGap, i) => {
|
|
396
|
+
const at = `pair gap ${i} (${String(p?.a)}, ${String(p?.b)})`;
|
|
397
|
+
if (typeof p !== "object" || p === null || typeof p.a !== "string" || typeof p.b !== "string") {
|
|
398
|
+
problems.push(`pair gap ${i}: the entry has no string ids a and b`);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
for (const id of [p.a, p.b]) if (!ids.has(id)) problems.push(`${at}: the box ${id} is not in the input`);
|
|
402
|
+
if (p.a === p.b) problems.push(`${at}: the two ids are the same box`);
|
|
403
|
+
if (typeof p.gap !== "number" || !Number.isFinite(p.gap) || p.gap < 0) problems.push(`${at}: the gap ${String(p.gap)} is not a finite number of 0 or more`);
|
|
404
|
+
if (p.lanes !== undefined && (typeof p.lanes !== "number" || !Number.isInteger(p.lanes) || p.lanes < 0))
|
|
405
|
+
problems.push(`${at}: the lanes value ${String(p.lanes)} is not a whole number of 0 or more`);
|
|
406
|
+
const key = JSON.stringify(p.a < p.b ? [p.a, p.b] : [p.b, p.a]);
|
|
407
|
+
const first = seen.get(key);
|
|
408
|
+
if (first !== undefined) problems.push(`${at}: the pair is also pair gap ${first}`);
|
|
409
|
+
else seen.set(key, i);
|
|
410
|
+
});
|
|
337
411
|
return problems;
|
|
338
412
|
}
|
package/src/strategy-space.ts
CHANGED
|
@@ -46,6 +46,12 @@ export interface MemberRule {
|
|
|
46
46
|
readonly min: number;
|
|
47
47
|
readonly max: number;
|
|
48
48
|
readonly ordered: boolean;
|
|
49
|
+
/**
|
|
50
|
+
* The member lists that the option permits. Each entry is a list of whole configs of the space of the members.
|
|
51
|
+
* Without `pool`, each list of base configs with a good count is a member list.
|
|
52
|
+
* With `pool`, a config of the option has the members of one entry, and `spaceSize` counts each entry one time.
|
|
53
|
+
*/
|
|
54
|
+
readonly pool?: readonly (readonly StrategyConfig[])[];
|
|
49
55
|
}
|
|
50
56
|
|
|
51
57
|
/** The inputs that an option supports. A missing field supports each value. */
|
|
@@ -149,7 +155,7 @@ export interface SampleOptions extends ShapeFilter {
|
|
|
149
155
|
/** The count of the configs, a whole number of 0 or more. */
|
|
150
156
|
readonly n: number;
|
|
151
157
|
readonly seed: number;
|
|
152
|
-
/** With `skip` (the default), the sample has no meta option. With `sample`, a meta option draws its members from base samples
|
|
158
|
+
/** With `skip` (the default), the sample has no meta option. With `sample`, a meta option draws its members from base samples, or one entry of its `pool`. */
|
|
153
159
|
readonly meta?: "skip" | "sample";
|
|
154
160
|
/** The largest count of option draws. The default is the larger value of 1000 and `100 * n`. */
|
|
155
161
|
readonly maxTries?: number;
|
|
@@ -204,6 +210,21 @@ function isActive(p: SpaceParam, values: Values): boolean {
|
|
|
204
210
|
return p.when.in.some((x) => x === v);
|
|
205
211
|
}
|
|
206
212
|
|
|
213
|
+
/** This function gives the problem of a value of a param, or `undefined` for a value in its range. The corpus dimensions of `families.ts` use it. */
|
|
214
|
+
export function paramProblem(param: SpaceParam, value: unknown): string | undefined {
|
|
215
|
+
return valueProblem(param, value);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** This function tells if a param is active for the values of the earlier params. A param without `when` is always active. */
|
|
219
|
+
export function paramActive(param: SpaceParam, values: Readonly<Record<string, ParamValue>>): boolean {
|
|
220
|
+
return isActive(param, values);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** This function gives the stable form of a value of a param: a real number gets 12 significant digits, and `-0` becomes `0`. */
|
|
224
|
+
export function paramValue(param: SpaceParam, value: ParamValue): ParamValue {
|
|
225
|
+
return normalize(param, value);
|
|
226
|
+
}
|
|
227
|
+
|
|
207
228
|
/** This function gives the part of a space that has a stage, and the prefix of the names of its stages. */
|
|
208
229
|
function partOf(space: StrategySpace, stage: string): { readonly space: StrategySpace; readonly prefix: string } {
|
|
209
230
|
for (const part of space.parts ?? []) {
|
|
@@ -291,6 +312,9 @@ function optionProblems(stage: SpaceStage, o: StageOption): Problem[] {
|
|
|
291
312
|
const { min, max, ordered } = o.members;
|
|
292
313
|
if (!Number.isInteger(min) || !Number.isInteger(max) || min < 1 || min > max || typeof ordered !== "boolean")
|
|
293
314
|
out.push(["members", `${at} has a member rule without whole counts 1 <= min <= max and a boolean ordered`]);
|
|
315
|
+
const pool: unknown = o.members.pool;
|
|
316
|
+
if (pool !== undefined && (!Array.isArray(pool) || pool.length === 0 || !pool.every((entry: unknown) => Array.isArray(entry))))
|
|
317
|
+
out.push(["pool", `${at} has a pool that is not a list of one or more member lists`]);
|
|
294
318
|
}
|
|
295
319
|
return out;
|
|
296
320
|
}
|
|
@@ -407,14 +431,47 @@ function memberProblems(part: StrategySpace, rule: MemberRule, members: unknown,
|
|
|
407
431
|
if (ids.has(id)) out.push(["duplicate", `the members ${ids.get(id)} and ${k} of ${at} are the same config ${id}`]);
|
|
408
432
|
else ids.set(id, k);
|
|
409
433
|
});
|
|
434
|
+
if (rule.pool !== undefined && out.length === 0) {
|
|
435
|
+
const key = memberKey(part, rule, members as readonly StrategyConfig[]);
|
|
436
|
+
if (!rule.pool.some((entry) => memberKey(part, rule, entry) === key))
|
|
437
|
+
out.push(["pool", `${at} has a member list that is not in its pool of ${rule.pool.length} ${rule.pool.length === 1 ? "list" : "lists"}`]);
|
|
438
|
+
}
|
|
439
|
+
return out;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/** This function gives the key of a good member list: the `configId` of each member, sorted when the rule is not ordered. */
|
|
443
|
+
function memberKey(part: StrategySpace, rule: MemberRule, members: readonly StrategyConfig[]): string {
|
|
444
|
+
const ids = members.map((m) => idOf(canonicalOf(part, m, true)));
|
|
445
|
+
if (!rule.ordered) ids.sort();
|
|
446
|
+
return ids.join(",");
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/** This function gives the problems of the pool of a meta option of a good space. `part` is the space of the members. */
|
|
450
|
+
function poolProblems(part: StrategySpace, rule: MemberRule, at: string): Problem[] {
|
|
451
|
+
const out: Problem[] = [];
|
|
452
|
+
const keys = new Map<string, number>();
|
|
453
|
+
const plain: MemberRule = { min: rule.min, max: rule.max, ordered: rule.ordered };
|
|
454
|
+
(rule.pool ?? []).forEach((entry, j) => {
|
|
455
|
+
const inner = memberProblems(part, plain, entry, `the pool entry ${j} of ${at}`);
|
|
456
|
+
out.push(...inner);
|
|
457
|
+
if (inner.length > 0) return;
|
|
458
|
+
const key = memberKey(part, rule, entry);
|
|
459
|
+
if (keys.has(key)) out.push(["duplicate", `the pool entries ${keys.get(key)} and ${j} of ${at} have the same members`]);
|
|
460
|
+
else keys.set(key, j);
|
|
461
|
+
});
|
|
410
462
|
return out;
|
|
411
463
|
}
|
|
412
464
|
|
|
465
|
+
/** This function tells if a member list fits a space: each member is a good config of the space. A shape filter removes options from the space. */
|
|
466
|
+
const fits = (part: StrategySpace, entry: readonly StrategyConfig[]): boolean => entry.every((m) => configProblems(part, m, true).length === 0);
|
|
467
|
+
|
|
413
468
|
const toBreaks = (problems: readonly Problem[], inputs: readonly string[] = []): ClaimBreak[] => problems.map(([claim, detail]) => ({ claim, detail, inputs }));
|
|
414
469
|
|
|
415
470
|
/**
|
|
416
471
|
* This function checks one config of a space. It gives one break for each problem. An empty list tells that the config is good.
|
|
417
472
|
* The `claim` of a break is the kind of the problem, for example `missing`, `unknown`, `inactive`, `param`, `constraint`, `members` or `depth`.
|
|
473
|
+
*
|
|
474
|
+
* A member list that is not an entry of the `pool` of its option gives the claim `pool`.
|
|
418
475
|
* With `shapes`, an option that does not support a kind of the list gives the claim `shape`. A member of a meta option gets the same check.
|
|
419
476
|
*/
|
|
420
477
|
export function checkConfig(space: StrategySpace, config: StrategyConfig, options: ShapeFilter = {}): ClaimBreak[] {
|
|
@@ -424,6 +481,7 @@ export function checkConfig(space: StrategySpace, config: StrategyConfig, option
|
|
|
424
481
|
/**
|
|
425
482
|
* This function checks a space. It gives one break for each problem. An empty list tells that the space is good.
|
|
426
483
|
* It finds duplicate names, empty ranges, defaults out of range, enums without values, bad log scales, unknown names and bad named configs.
|
|
484
|
+
* It also checks each entry of a `pool`: the count of its members, each member and a repeated entry.
|
|
427
485
|
* The `inputs` of a break of a named config have the name of the config.
|
|
428
486
|
*/
|
|
429
487
|
export function checkSpace(space: StrategySpace): ClaimBreak[] {
|
|
@@ -462,6 +520,9 @@ export function checkSpace(space: StrategySpace): ClaimBreak[] {
|
|
|
462
520
|
for (const s of space.stages) if (partOf(space, s.name).prefix === "") out.push(["parts", `the stage ${s.name} is not a stage of a part`]);
|
|
463
521
|
const breaks = toBreaks(out);
|
|
464
522
|
if (breaks.length > 0) return breaks;
|
|
523
|
+
for (const s of space.stages)
|
|
524
|
+
for (const o of s.options) if (o.members?.pool !== undefined) breaks.push(...toBreaks(poolProblems(partOf(space, s.name).space, o.members, `the option ${s.name}.${o.id}`)));
|
|
525
|
+
if (breaks.length > 0) return breaks;
|
|
465
526
|
for (const [name, config] of Object.entries(space.named ?? {}))
|
|
466
527
|
breaks.push(...toBreaks(configProblems(space, config, false).map(([c, d]) => [c, `the named config ${name}: ${d}`] as const), [name]));
|
|
467
528
|
return breaks;
|
|
@@ -608,7 +669,7 @@ interface Combo {
|
|
|
608
669
|
const optionIds = (choices: Choices): Map<string, string | null> => new Map([...choices].map(([k, o]) => [k, o?.id ?? null]));
|
|
609
670
|
const keeps = (space: StrategySpace, choices: Choices): boolean => (space.constraints ?? []).every((c) => holds(c, optionIds(choices)));
|
|
610
671
|
|
|
611
|
-
/** This function gives the count of the member lists of a meta option from `base` base configs, or `null` above `limit`. */
|
|
672
|
+
/** This function gives the count of the member lists of a meta option without a pool from `base` base configs, or `null` above `limit`. */
|
|
612
673
|
function memberLists(base: bigint, rule: MemberRule, limit: bigint): bigint | null {
|
|
613
674
|
let sum = 0n;
|
|
614
675
|
for (let m = rule.min; m <= rule.max; m++) {
|
|
@@ -677,7 +738,8 @@ function combos(space: StrategySpace, levels: number, meta: boolean, limit: bigi
|
|
|
677
738
|
for (const o of s.options) {
|
|
678
739
|
if (o.members === undefined) continue;
|
|
679
740
|
const choices = new Map<string, StageOption | null>(stages.map((x) => [x.name, x === s ? o : null]));
|
|
680
|
-
|
|
741
|
+
const lists = o.members.pool === undefined ? memberLists(base, o.members, limit) : BigInt(o.members.pool.length);
|
|
742
|
+
if (keeps(space, choices)) out.push({ choices, base: false, weight: times(countOf(o), lists, limit) });
|
|
681
743
|
}
|
|
682
744
|
return out;
|
|
683
745
|
}
|
|
@@ -685,6 +747,7 @@ function combos(space: StrategySpace, levels: number, meta: boolean, limit: bigi
|
|
|
685
747
|
/**
|
|
686
748
|
* This function gives the count of the grid points of a space, with `levels` levels for each numeric param.
|
|
687
749
|
* It counts only the active params and the combinations that keep the constraints. It gives the meta configs apart.
|
|
750
|
+
* A meta option with a `pool` counts each entry of the pool one time.
|
|
688
751
|
* With `shapes`, it counts only the options that support each kind of the list.
|
|
689
752
|
*/
|
|
690
753
|
export function spaceSize(space: StrategySpace, options: SpaceSizeOptions): SpaceSize {
|
|
@@ -697,14 +760,22 @@ export function spaceSize(space: StrategySpace, options: SpaceSizeOptions): Spac
|
|
|
697
760
|
/**
|
|
698
761
|
* This function gives the space with only the options that support each shape kind of `shapes`. Without `shapes`, it gives the same space.
|
|
699
762
|
* A stage can lose all of its options. Then the space has no config with that stage.
|
|
763
|
+
* A pool keeps only the entries whose members fit the smaller space. A meta option without such an entry goes out.
|
|
700
764
|
*/
|
|
701
765
|
function restrict(space: StrategySpace, shapes: readonly ShapeKind[] | undefined): StrategySpace {
|
|
702
766
|
if (shapes === undefined) return space;
|
|
703
|
-
|
|
767
|
+
const kept: StrategySpace = {
|
|
704
768
|
...space,
|
|
705
769
|
stages: space.stages.map((s) => ({ ...s, options: s.options.filter((o) => supportsShapes(o, shapes)) })),
|
|
706
770
|
...(space.parts === undefined ? {} : { parts: space.parts.map((p) => restrict(p, shapes)) }),
|
|
707
771
|
};
|
|
772
|
+
const withPool = (s: SpaceStage, o: StageOption): StageOption[] => {
|
|
773
|
+
if (o.members?.pool === undefined) return [o];
|
|
774
|
+
const part = partOf(kept, s.name).space;
|
|
775
|
+
const pool = o.members.pool.filter((entry) => fits(part, entry));
|
|
776
|
+
return pool.length === 0 ? [] : [{ ...o, members: { ...o.members, pool } }];
|
|
777
|
+
};
|
|
778
|
+
return { ...kept, stages: kept.stages.map((s) => ({ ...s, options: s.options.flatMap((o) => withPool(s, o)) })) };
|
|
708
779
|
}
|
|
709
780
|
|
|
710
781
|
/** This function throws a `RangeError` for a list of shape kinds with a kind that is not declared. */
|
|
@@ -828,6 +899,14 @@ function sampleConfigs(space: StrategySpace, n: number, seed: number, meta: bool
|
|
|
828
899
|
});
|
|
829
900
|
}
|
|
830
901
|
if (o.members === undefined) continue;
|
|
902
|
+
const fixed = o.members.pool;
|
|
903
|
+
if (fixed !== undefined) {
|
|
904
|
+
const r = stepRandom(seed, `pool-entry:${s.name}:${o.id}`);
|
|
905
|
+
permutation(users.length, r).forEach((stratum, j) => {
|
|
906
|
+
members[users[j]!]![k] = [...fixed[Math.min(fixed.length - 1, Math.floor(((stratum + r()) / users.length) * fixed.length))]!];
|
|
907
|
+
});
|
|
908
|
+
continue;
|
|
909
|
+
}
|
|
831
910
|
const part = partOf(space, s.name).space;
|
|
832
911
|
const poolSize = Math.max(n, 4 * o.members.max, 8);
|
|
833
912
|
const seen = new Set<string>();
|
|
@@ -862,6 +941,7 @@ function sampleConfigs(space: StrategySpace, n: number, seed: number, meta: bool
|
|
|
862
941
|
* - The mode `sample` gives `n` configs of a seeded Latin hypercube. The options of a stage get equal strata, and each active param gets its own strata.
|
|
863
942
|
* - A sample draws again when a point breaks a constraint. After `maxTries` draws, it throws an `Error`. Two sampled configs can be equal.
|
|
864
943
|
* - With `shapes`, only the options that support each kind of the list count. Without `shapes`, the result does not change.
|
|
944
|
+
* - A meta option with a `pool` gives only the entries of its pool. With `shapes`, only the entries whose members support each kind count.
|
|
865
945
|
*/
|
|
866
946
|
export function enumerateSpace(space: StrategySpace, options: EnumerateOptions): StrategyConfig[] {
|
|
867
947
|
assertSpace(space);
|
package/src/turn.ts
CHANGED
|
@@ -18,14 +18,20 @@ export function turnPoint(x: number, y: number, h: number): [number, number] {
|
|
|
18
18
|
return [h - y, x];
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
/**
|
|
21
|
+
/**
|
|
22
|
+
* This function gives a port of a box of height `h` after the turn. Each value of the port turns, also a port with only `x` or only `y`.
|
|
23
|
+
* The start of a side is its top end or its left end. Thus `left` goes to `top` and `right` goes to `bottom` with `along` changed to `1 - along`.
|
|
24
|
+
* `top` goes to `right` and `bottom` goes to `left` with the same `along`.
|
|
25
|
+
*/
|
|
22
26
|
export function turnPort(port: Port, h: number): Port {
|
|
23
|
-
const { side, x, y, ...rest } = port;
|
|
27
|
+
const { side, x, y, along, ...rest } = port;
|
|
28
|
+
const flips = side === "left" || side === "right";
|
|
24
29
|
return {
|
|
25
30
|
...rest,
|
|
26
31
|
...(side === undefined ? {} : { side: turnSide(side) }),
|
|
27
32
|
...(y === undefined ? {} : { x: h - y }),
|
|
28
33
|
...(x === undefined ? {} : { y: x }),
|
|
34
|
+
...(along === undefined ? {} : { along: flips ? 1 - along : along }),
|
|
29
35
|
};
|
|
30
36
|
}
|
|
31
37
|
|