@mark1russell7/layout-contract 0.2.12 → 0.2.14
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 +14 -5
- package/build/index.d.ts +475 -150
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1022 -40
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/families.ts +618 -0
- package/src/hash.ts +4 -2
- package/src/index.ts +2 -0
- package/src/input.ts +41 -0
- package/src/measures.ts +21 -10
- package/src/record.ts +11 -2
- package/src/rules.ts +84 -9
- package/src/shapes.ts +119 -0
- package/src/strategy-space.ts +177 -19
package/build/index.js
CHANGED
|
@@ -110,6 +110,134 @@ function overlapOf(a, b) {
|
|
|
110
110
|
y: Math.min(a.y + a.h, b.y + b.h) - Math.max(a.y, b.y)
|
|
111
111
|
};
|
|
112
112
|
}
|
|
113
|
+
// src/shapes.ts
|
|
114
|
+
/** The declared shape kinds, in their order. Only `rect` has all of its geometry. The other kinds have their bounding box only. */
|
|
115
|
+
const SHAPE_KINDS = Object.freeze([
|
|
116
|
+
{
|
|
117
|
+
id: "rect",
|
|
118
|
+
fields: ["w", "h"],
|
|
119
|
+
regions: ["rect"],
|
|
120
|
+
geometry: {
|
|
121
|
+
bounds: true,
|
|
122
|
+
area: true,
|
|
123
|
+
contains: true,
|
|
124
|
+
overlap: true
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
id: "polygon",
|
|
129
|
+
fields: ["points", "holes?"],
|
|
130
|
+
regions: ["polygon"],
|
|
131
|
+
geometry: {
|
|
132
|
+
bounds: true,
|
|
133
|
+
area: false,
|
|
134
|
+
contains: false,
|
|
135
|
+
overlap: false
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
id: "circle",
|
|
140
|
+
fields: [
|
|
141
|
+
"cx",
|
|
142
|
+
"cy",
|
|
143
|
+
"r"
|
|
144
|
+
],
|
|
145
|
+
regions: ["circle"],
|
|
146
|
+
geometry: {
|
|
147
|
+
bounds: true,
|
|
148
|
+
area: false,
|
|
149
|
+
contains: false,
|
|
150
|
+
overlap: false
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
id: "free",
|
|
155
|
+
fields: [
|
|
156
|
+
"w",
|
|
157
|
+
"h",
|
|
158
|
+
"shape?"
|
|
159
|
+
],
|
|
160
|
+
regions: [
|
|
161
|
+
"rect",
|
|
162
|
+
"polygon",
|
|
163
|
+
"circle",
|
|
164
|
+
"ellipse"
|
|
165
|
+
],
|
|
166
|
+
geometry: {
|
|
167
|
+
bounds: true,
|
|
168
|
+
area: false,
|
|
169
|
+
contains: false,
|
|
170
|
+
overlap: false
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
]);
|
|
174
|
+
/** The ids of `SHAPE_KINDS`, in their order. */
|
|
175
|
+
const SHAPE_KIND_IDS = Object.freeze(SHAPE_KINDS.map((k) => k.id));
|
|
176
|
+
/** The shape kinds of a measure or a rule without `shapes`. The contract measured only rectangles before L46. */
|
|
177
|
+
const DEFAULT_SHAPES = Object.freeze(["rect"]);
|
|
178
|
+
const REGION_KIND = {
|
|
179
|
+
rect: "rect",
|
|
180
|
+
polygon: "polygon",
|
|
181
|
+
circle: "circle",
|
|
182
|
+
ellipse: "free"
|
|
183
|
+
};
|
|
184
|
+
/** This function tells if a value is a declared shape kind. */
|
|
185
|
+
function isShapeKind(value) {
|
|
186
|
+
return typeof value === "string" && SHAPE_KIND_IDS.includes(value);
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* This function gives the shape kind of a box. A box with `shapeKind` has that kind.
|
|
190
|
+
* Without it, the kind comes from `shape`: no shape is a `rect`, and an ellipse is `free`.
|
|
191
|
+
*/
|
|
192
|
+
function shapeKindOf(box) {
|
|
193
|
+
if (box.shapeKind !== void 0) return box.shapeKind;
|
|
194
|
+
return box.shape === void 0 ? "rect" : REGION_KIND[box.shape.kind];
|
|
195
|
+
}
|
|
196
|
+
/** This function gives the shape kinds of the boxes of an input, in the order of `SHAPE_KINDS`. An input without boxes gives an empty list. */
|
|
197
|
+
function shapeKindsOf(input) {
|
|
198
|
+
const seen = new Set(input.boxes.map(shapeKindOf));
|
|
199
|
+
return SHAPE_KIND_IDS.filter((k) => seen.has(k));
|
|
200
|
+
}
|
|
201
|
+
/** This function gives the value of the cell dimension `itemShape` of an input: its one kind, `mixed` for more kinds, or `rect` without boxes. */
|
|
202
|
+
function itemShapeOf(input) {
|
|
203
|
+
const kinds = shapeKindsOf(input);
|
|
204
|
+
return kinds.length === 0 ? "rect" : kinds.length === 1 ? kinds[0] : "mixed";
|
|
205
|
+
}
|
|
206
|
+
/** This function gives the problem of the shape kind of a box, or `undefined` when `shapeKind` and `shape` agree. */
|
|
207
|
+
function shapeKindProblem(box) {
|
|
208
|
+
const k = box.shapeKind;
|
|
209
|
+
if (k === void 0) return void 0;
|
|
210
|
+
const spec = SHAPE_KINDS.find((s) => s.id === k);
|
|
211
|
+
if (spec === void 0) return `box ${box.id}: the shape kind ${String(k)} is not a declared kind`;
|
|
212
|
+
if (box.shape === void 0) return k === "rect" || k === "free" ? void 0 : `box ${box.id}: the shape kind ${k} has no shape`;
|
|
213
|
+
if (!spec.regions.includes(box.shape.kind)) return `box ${box.id}: the shape kind ${k} does not agree with a shape of the kind ${box.shape.kind}`;
|
|
214
|
+
}
|
|
215
|
+
/** This function gives the shape kinds that a measure or a rule supports. */
|
|
216
|
+
function shapesOf(use) {
|
|
217
|
+
return use.shapes ?? DEFAULT_SHAPES;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* This function checks that each measure and each rule of a run supports each shape kind of the input. An empty list tells that the run is good.
|
|
221
|
+
* Each break has the claim `shape`, the id of the input, and the first 5 ids of the boxes of the kind.
|
|
222
|
+
*/
|
|
223
|
+
function checkShapes(input, uses) {
|
|
224
|
+
const kinds = shapeKindsOf(input);
|
|
225
|
+
const out = [];
|
|
226
|
+
for (const use of uses) {
|
|
227
|
+
const shapes = shapesOf(use);
|
|
228
|
+
for (const kind of kinds) {
|
|
229
|
+
if (shapes.includes(kind)) continue;
|
|
230
|
+
const ids = input.boxes.filter((b) => shapeKindOf(b) === kind).map((b) => b.id);
|
|
231
|
+
const shown = ids.slice(0, 5).join(", ") + (ids.length > 5 ? `, and ${ids.length - 5} more` : "");
|
|
232
|
+
out.push({
|
|
233
|
+
claim: "shape",
|
|
234
|
+
detail: `${use.id} supports only the shape kinds ${shapes.join(", ") || "(none)"}, but the input ${input.id} has ${kind} boxes: ${shown}`,
|
|
235
|
+
inputs: [input.id]
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return out;
|
|
240
|
+
}
|
|
113
241
|
// src/tolerance.ts
|
|
114
242
|
/** The epsilon for the float compares in an engine. */
|
|
115
243
|
const EPS = 1e-6;
|
|
@@ -138,7 +266,27 @@ function toPackInput(input) {
|
|
|
138
266
|
container: input.container ?? { fixed: "none" },
|
|
139
267
|
items: input.boxes,
|
|
140
268
|
...input.obstacles === void 0 ? {} : { obstacles: input.obstacles },
|
|
141
|
-
...input.gap === void 0 ? {} : { gap: input.gap }
|
|
269
|
+
...input.gap === void 0 ? {} : { gap: input.gap },
|
|
270
|
+
...input.pairGaps === void 0 ? {} : { pairGaps: input.pairGaps }
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
/** This function compares two strings by their UTF-16 code units, as `Array.prototype.sort` does without a compare function. */
|
|
274
|
+
const byCode = (x, y) => x < y ? -1 : x > y ? 1 : 0;
|
|
275
|
+
/**
|
|
276
|
+
* This function gives the canonical form of an input. `documentHash` uses it for each `layout-input/1` document.
|
|
277
|
+
* Each pair of `pairGaps` gets `a` before `b`, and the list is sorted by `a` and then by `b`.
|
|
278
|
+
* An input without `pairGaps` is the same object, so its hash does not change.
|
|
279
|
+
*/
|
|
280
|
+
function canonicalInput(input) {
|
|
281
|
+
if (!Array.isArray(input.pairGaps)) return input;
|
|
282
|
+
const pairGaps = input.pairGaps.map((p) => byCode(p.a, p.b) > 0 ? {
|
|
283
|
+
...p,
|
|
284
|
+
a: p.b,
|
|
285
|
+
b: p.a
|
|
286
|
+
} : p).sort((x, y) => byCode(x.a, y.a) || byCode(x.b, y.b));
|
|
287
|
+
return {
|
|
288
|
+
...input,
|
|
289
|
+
pairGaps
|
|
142
290
|
};
|
|
143
291
|
}
|
|
144
292
|
// src/turn.ts
|
|
@@ -256,8 +404,13 @@ function applyPack(input, output) {
|
|
|
256
404
|
};
|
|
257
405
|
}
|
|
258
406
|
// src/record.ts
|
|
259
|
-
/**
|
|
407
|
+
/**
|
|
408
|
+
* This function gives the record of a run. The input gives its id, its hash, its set and the counts of the nodes and the edges.
|
|
409
|
+
* The input also gives `shapes`. An input with only `rect` boxes gets no `shapes`, so its record does not change.
|
|
410
|
+
*/
|
|
260
411
|
function toLayoutRecord(input, run) {
|
|
412
|
+
const shapes = shapeKindsOf(input);
|
|
413
|
+
const onlyRect = shapes.length === 0 || shapes.length === 1 && shapes[0] === "rect";
|
|
261
414
|
return {
|
|
262
415
|
schema: "layout-record/1",
|
|
263
416
|
...run,
|
|
@@ -265,7 +418,8 @@ function toLayoutRecord(input, run) {
|
|
|
265
418
|
inputHash: input.hash ?? "",
|
|
266
419
|
set: input.set ?? "",
|
|
267
420
|
nodes: input.boxes.length,
|
|
268
|
-
edges: input.edges?.length ?? 0
|
|
421
|
+
edges: input.edges?.length ?? 0,
|
|
422
|
+
...onlyRect ? {} : { shapes }
|
|
269
423
|
};
|
|
270
424
|
}
|
|
271
425
|
// src/rules.ts
|
|
@@ -289,20 +443,53 @@ function placedRect(box) {
|
|
|
289
443
|
/**
|
|
290
444
|
* This function counts the pairs of placed boxes whose insides, with their margins, overlap by more than `tolerance` on each axis.
|
|
291
445
|
* A box with a shape counts as its bounding box. That area is larger, thus the count is safe.
|
|
446
|
+
*
|
|
447
|
+
* 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.
|
|
448
|
+
* Thus the pair breaks the rule only when the boxes are nearer than the gap on each axis. The other pairs do not change.
|
|
292
449
|
*/
|
|
293
|
-
function countOverlaps(boxes, tolerance = DEFAULT_TOLERANCE.overlap) {
|
|
450
|
+
function countOverlaps(boxes, tolerance = DEFAULT_TOLERANCE.overlap, pairGaps) {
|
|
451
|
+
if (pairGaps === void 0 || pairGaps.length === 0) return marginOverlaps(boxes, tolerance, void 0);
|
|
452
|
+
const gaps = /* @__PURE__ */ new Map();
|
|
453
|
+
for (const p of pairGaps) {
|
|
454
|
+
const key = pairKey(p.a, p.b);
|
|
455
|
+
if (p.a !== p.b && !gaps.has(key)) gaps.set(key, p.gap);
|
|
456
|
+
}
|
|
457
|
+
const raw = /* @__PURE__ */ new Map();
|
|
458
|
+
for (const box of boxes) {
|
|
459
|
+
const r = placedRect(box);
|
|
460
|
+
if (r !== void 0 && !raw.has(box.id)) raw.set(box.id, r);
|
|
461
|
+
}
|
|
462
|
+
let count = marginOverlaps(boxes, tolerance, gaps);
|
|
463
|
+
for (const [key, gap] of gaps) {
|
|
464
|
+
const [a, b] = JSON.parse(key);
|
|
465
|
+
const ra = raw.get(a);
|
|
466
|
+
const rb = raw.get(b);
|
|
467
|
+
if (ra === void 0 || rb === void 0) continue;
|
|
468
|
+
const o = overlapOf(grow(ra, gap), rb);
|
|
469
|
+
if (o.x > tolerance && o.y > tolerance) count++;
|
|
470
|
+
}
|
|
471
|
+
return count;
|
|
472
|
+
}
|
|
473
|
+
/** This function gives the key of a pair of box ids, with the same key for each order. */
|
|
474
|
+
const pairKey = (a, b) => JSON.stringify(a < b ? [a, b] : [b, a]);
|
|
475
|
+
/** 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. */
|
|
476
|
+
function marginOverlaps(boxes, tolerance, skip) {
|
|
294
477
|
const rects = [];
|
|
295
478
|
for (const box of boxes) {
|
|
296
479
|
const r = placedRect(box);
|
|
297
|
-
if (r !== void 0) rects.push(
|
|
480
|
+
if (r !== void 0) rects.push({
|
|
481
|
+
id: box.id,
|
|
482
|
+
r: grow(r, box.margin)
|
|
483
|
+
});
|
|
298
484
|
}
|
|
299
|
-
rects.sort((a, b) => a.x - b.x);
|
|
485
|
+
rects.sort((a, b) => a.r.x - b.r.x);
|
|
300
486
|
let count = 0;
|
|
301
487
|
for (let i = 0; i < rects.length; i++) {
|
|
302
|
-
const a = rects[i];
|
|
488
|
+
const a = rects[i].r;
|
|
303
489
|
for (let j = i + 1; j < rects.length; j++) {
|
|
304
|
-
const b = rects[j];
|
|
490
|
+
const b = rects[j].r;
|
|
305
491
|
if (b.x >= a.x + a.w - tolerance) break;
|
|
492
|
+
if (skip !== void 0 && skip.has(pairKey(rects[i].id, rects[j].id))) continue;
|
|
306
493
|
const o = overlapOf(a, b);
|
|
307
494
|
if (o.x > tolerance && o.y > tolerance) count++;
|
|
308
495
|
}
|
|
@@ -359,6 +546,29 @@ function countPortsOffBorder(boxes, eps = RECORD_EPS) {
|
|
|
359
546
|
}
|
|
360
547
|
return count;
|
|
361
548
|
}
|
|
549
|
+
/**
|
|
550
|
+
* The hard rules of a pack, with the shape kinds that each rule supports.
|
|
551
|
+
* `no-overlap` and `in-container` test the bounding box of a shape. That count is exact only for a `rect`.
|
|
552
|
+
* `locked-kept` and `complete` read only the positions and the ids, so they support each kind.
|
|
553
|
+
*/
|
|
554
|
+
const PACK_HARD_RULES = Object.freeze([
|
|
555
|
+
{
|
|
556
|
+
id: "no-overlap",
|
|
557
|
+
shapes: ["rect"]
|
|
558
|
+
},
|
|
559
|
+
{
|
|
560
|
+
id: "in-container",
|
|
561
|
+
shapes: ["rect"]
|
|
562
|
+
},
|
|
563
|
+
{
|
|
564
|
+
id: "locked-kept",
|
|
565
|
+
shapes: SHAPE_KIND_IDS
|
|
566
|
+
},
|
|
567
|
+
{
|
|
568
|
+
id: "complete",
|
|
569
|
+
shapes: SHAPE_KIND_IDS
|
|
570
|
+
}
|
|
571
|
+
]);
|
|
362
572
|
/** This function tells if the inside of `region` and the inside of `rect` have a common point. */
|
|
363
573
|
function regionHitsRect(region, rect) {
|
|
364
574
|
if (rect.w <= 0 || rect.h <= 0) return false;
|
|
@@ -514,7 +724,8 @@ function countObstacleHits(boxes, obstacles, tolerance = DEFAULT_TOLERANCE.overl
|
|
|
514
724
|
/**
|
|
515
725
|
* This function gives the count of each hard rule of the output of a packer.
|
|
516
726
|
* It applies the output with `applyPack`. Only the placed boxes and the locked boxes count for the overlaps and the container.
|
|
517
|
-
* `no-overlap` counts the pairs of boxes and the pairs of a box and an obstacle.
|
|
727
|
+
* `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.
|
|
728
|
+
*
|
|
518
729
|
* A locked box without a placement, or with a move of more than `RECORD_EPS`, breaks `locked-kept`.
|
|
519
730
|
* A locked box without a position in the input is a bad input, not a break. `checkInput` finds it.
|
|
520
731
|
* Another box without a placement that is not in `overflow` breaks `complete`.
|
|
@@ -540,7 +751,7 @@ function packHard(input, output, tolerance = DEFAULT_TOLERANCE.overlap, options
|
|
|
540
751
|
else if (!out.has(box.id)) incomplete++;
|
|
541
752
|
}
|
|
542
753
|
return {
|
|
543
|
-
"no-overlap": countOverlaps(kept, tolerance) + countObstacleHits(kept, after.obstacles, tolerance),
|
|
754
|
+
"no-overlap": countOverlaps(kept, tolerance, input.pairGaps) + countObstacleHits(kept, after.obstacles, tolerance),
|
|
544
755
|
"in-container": countOutside({
|
|
545
756
|
...after,
|
|
546
757
|
boxes: kept
|
|
@@ -552,6 +763,7 @@ function packHard(input, output, tolerance = DEFAULT_TOLERANCE.overlap, options
|
|
|
552
763
|
/**
|
|
553
764
|
* This function gives the problems of an input file. An empty list tells that the file is good.
|
|
554
765
|
* A problem is a bad input, not a break of a hard rule. Thus an engine refuses the input.
|
|
766
|
+
* In `pairGaps`, an unknown box, a pair of one box, a repeated pair in either order, a bad `gap` and bad `lanes` are problems.
|
|
555
767
|
*/
|
|
556
768
|
function checkInput(input) {
|
|
557
769
|
const problems = [];
|
|
@@ -561,13 +773,46 @@ function checkInput(input) {
|
|
|
561
773
|
ids.add(box.id);
|
|
562
774
|
if (!(box.w > 0 && box.h > 0 && Number.isFinite(box.w) && Number.isFinite(box.h))) problems.push(`box ${box.id}: the size is not positive and finite`);
|
|
563
775
|
if (box.locked === true && (box.x === void 0 || box.y === void 0)) problems.push(`box ${box.id}: a locked box has no position`);
|
|
776
|
+
const shape = shapeKindProblem(box);
|
|
777
|
+
if (shape !== void 0) problems.push(shape);
|
|
564
778
|
}
|
|
565
779
|
if (countPortsOffBorder(input.boxes) > 0) problems.push("a fixed port is not on the border of its box");
|
|
566
780
|
for (const edge of input.edges ?? []) if (!ids.has(edge.source) || !ids.has(edge.target)) problems.push(`edge ${edge.id}: a box of the edge is not in the input`);
|
|
781
|
+
problems.push(...pairGapProblems(input.pairGaps, ids));
|
|
782
|
+
return problems;
|
|
783
|
+
}
|
|
784
|
+
/** This function gives the problems of the `pairGaps` of an input. `ids` has the ids of the boxes. */
|
|
785
|
+
function pairGapProblems(pairGaps, ids) {
|
|
786
|
+
if (pairGaps === void 0) return [];
|
|
787
|
+
if (!Array.isArray(pairGaps)) return ["pairGaps: the value is not a list"];
|
|
788
|
+
const problems = [];
|
|
789
|
+
const seen = /* @__PURE__ */ new Map();
|
|
790
|
+
pairGaps.forEach((p, i) => {
|
|
791
|
+
const at = `pair gap ${i} (${String(p?.a)}, ${String(p?.b)})`;
|
|
792
|
+
if (typeof p !== "object" || p === null || typeof p.a !== "string" || typeof p.b !== "string") {
|
|
793
|
+
problems.push(`pair gap ${i}: the entry has no string ids a and b`);
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
for (const id of [p.a, p.b]) if (!ids.has(id)) problems.push(`${at}: the box ${id} is not in the input`);
|
|
797
|
+
if (p.a === p.b) problems.push(`${at}: the two ids are the same box`);
|
|
798
|
+
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`);
|
|
799
|
+
if (p.lanes !== void 0 && (typeof p.lanes !== "number" || !Number.isInteger(p.lanes) || p.lanes < 0)) problems.push(`${at}: the lanes value ${String(p.lanes)} is not a whole number of 0 or more`);
|
|
800
|
+
const key = JSON.stringify(p.a < p.b ? [p.a, p.b] : [p.b, p.a]);
|
|
801
|
+
const first = seen.get(key);
|
|
802
|
+
if (first !== void 0) problems.push(`${at}: the pair is also pair gap ${first}`);
|
|
803
|
+
else seen.set(key, i);
|
|
804
|
+
});
|
|
567
805
|
return problems;
|
|
568
806
|
}
|
|
569
807
|
// src/measures.ts
|
|
570
|
-
/** The
|
|
808
|
+
/** The shape kinds of a joint measure that reads only the bounding boxes, the centers and the positions of the boxes. */
|
|
809
|
+
const BOUNDS_ONLY = SHAPE_KIND_IDS;
|
|
810
|
+
/** The shape kinds of a joint measure that reads the area `w` by `h` of each box. */
|
|
811
|
+
const AREA = ["rect"];
|
|
812
|
+
/**
|
|
813
|
+
* The specs of the joint measures, in the order of `docs/JOINT-MEASURES.md`.
|
|
814
|
+
* `joint.fill` and `joint.straight.length` read the area of each box, so they support only `rect`. `docs/SHAPES.md` tells the reasons.
|
|
815
|
+
*/
|
|
571
816
|
const JOINT_MEASURES = Object.freeze([
|
|
572
817
|
{
|
|
573
818
|
id: "joint.fill",
|
|
@@ -577,7 +822,8 @@ const JOINT_MEASURES = Object.freeze([
|
|
|
577
822
|
min: 0,
|
|
578
823
|
max: 1,
|
|
579
824
|
floor: 1,
|
|
580
|
-
role: "guard"
|
|
825
|
+
role: "guard",
|
|
826
|
+
shapes: AREA
|
|
581
827
|
},
|
|
582
828
|
{
|
|
583
829
|
id: "joint.aspectError",
|
|
@@ -588,7 +834,8 @@ const JOINT_MEASURES = Object.freeze([
|
|
|
588
834
|
max: Infinity,
|
|
589
835
|
floor: 0,
|
|
590
836
|
tol: .01,
|
|
591
|
-
role: "guard"
|
|
837
|
+
role: "guard",
|
|
838
|
+
shapes: BOUNDS_ONLY
|
|
592
839
|
},
|
|
593
840
|
{
|
|
594
841
|
id: "joint.spill",
|
|
@@ -599,7 +846,8 @@ const JOINT_MEASURES = Object.freeze([
|
|
|
599
846
|
max: 1,
|
|
600
847
|
floor: 0,
|
|
601
848
|
tol: .001,
|
|
602
|
-
role: "guard"
|
|
849
|
+
role: "guard",
|
|
850
|
+
shapes: BOUNDS_ONLY
|
|
603
851
|
},
|
|
604
852
|
{
|
|
605
853
|
id: "joint.straight.crossings",
|
|
@@ -610,7 +858,8 @@ const JOINT_MEASURES = Object.freeze([
|
|
|
610
858
|
max: Infinity,
|
|
611
859
|
floor: 0,
|
|
612
860
|
tol: 0,
|
|
613
|
-
role: "guard"
|
|
861
|
+
role: "guard",
|
|
862
|
+
shapes: BOUNDS_ONLY
|
|
614
863
|
},
|
|
615
864
|
{
|
|
616
865
|
id: "joint.straight.length",
|
|
@@ -621,7 +870,8 @@ const JOINT_MEASURES = Object.freeze([
|
|
|
621
870
|
max: Infinity,
|
|
622
871
|
floor: 0,
|
|
623
872
|
tol: .01,
|
|
624
|
-
role: "guard"
|
|
873
|
+
role: "guard",
|
|
874
|
+
shapes: AREA
|
|
625
875
|
},
|
|
626
876
|
{
|
|
627
877
|
id: "joint.stability.mean",
|
|
@@ -632,7 +882,8 @@ const JOINT_MEASURES = Object.freeze([
|
|
|
632
882
|
max: Infinity,
|
|
633
883
|
floor: 0,
|
|
634
884
|
tol: .005,
|
|
635
|
-
role: "change-guard"
|
|
885
|
+
role: "change-guard",
|
|
886
|
+
shapes: BOUNDS_ONLY
|
|
636
887
|
},
|
|
637
888
|
{
|
|
638
889
|
id: "joint.stability.moved",
|
|
@@ -643,7 +894,8 @@ const JOINT_MEASURES = Object.freeze([
|
|
|
643
894
|
max: 1,
|
|
644
895
|
floor: 0,
|
|
645
896
|
tol: .02,
|
|
646
|
-
role: "change-guard"
|
|
897
|
+
role: "change-guard",
|
|
898
|
+
shapes: BOUNDS_ONLY
|
|
647
899
|
},
|
|
648
900
|
{
|
|
649
901
|
id: "joint.proxy.rank",
|
|
@@ -651,7 +903,8 @@ const JOINT_MEASURES = Object.freeze([
|
|
|
651
903
|
direction: "higher",
|
|
652
904
|
min: -1,
|
|
653
905
|
max: 1,
|
|
654
|
-
role: "diagnostic"
|
|
906
|
+
role: "diagnostic",
|
|
907
|
+
shapes: BOUNDS_ONLY
|
|
655
908
|
},
|
|
656
909
|
{
|
|
657
910
|
id: "joint.ms",
|
|
@@ -659,7 +912,8 @@ const JOINT_MEASURES = Object.freeze([
|
|
|
659
912
|
direction: "lower",
|
|
660
913
|
min: 0,
|
|
661
914
|
max: Infinity,
|
|
662
|
-
role: "time"
|
|
915
|
+
role: "time",
|
|
916
|
+
shapes: BOUNDS_ONLY
|
|
663
917
|
}
|
|
664
918
|
]);
|
|
665
919
|
/** The threshold of `joint.stability.moved`, in widths `W` of the base. */
|
|
@@ -1112,10 +1366,11 @@ function outputHash(value, options = {}) {
|
|
|
1112
1366
|
/**
|
|
1113
1367
|
* 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.
|
|
1114
1368
|
* With the default grid, it is the input hash of `docs/FORMAT.md` of `packing`.
|
|
1369
|
+
* A document with the schema `layout-input/1` goes through `canonicalInput` first. Thus the order of its `pairGaps` does not change the hash.
|
|
1115
1370
|
*/
|
|
1116
1371
|
function documentHash(doc, options = {}) {
|
|
1117
1372
|
const { hash: _hash, ...rest } = doc;
|
|
1118
|
-
return outputHash(rest, options);
|
|
1373
|
+
return outputHash(rest.schema === "layout-input/1" ? canonicalInput(rest) : rest, options);
|
|
1119
1374
|
}
|
|
1120
1375
|
/**
|
|
1121
1376
|
* This function gives the output groups of the candidates. The key of `hashes` is the id of a candidate.
|
|
@@ -1683,6 +1938,18 @@ function isActive(p, values) {
|
|
|
1683
1938
|
const v = values[p.when.param];
|
|
1684
1939
|
return p.when.in.some((x) => x === v);
|
|
1685
1940
|
}
|
|
1941
|
+
/** 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. */
|
|
1942
|
+
function paramProblem(param, value) {
|
|
1943
|
+
return valueProblem(param, value);
|
|
1944
|
+
}
|
|
1945
|
+
/** This function tells if a param is active for the values of the earlier params. A param without `when` is always active. */
|
|
1946
|
+
function paramActive(param, values) {
|
|
1947
|
+
return isActive(param, values);
|
|
1948
|
+
}
|
|
1949
|
+
/** This function gives the stable form of a value of a param: a real number gets 12 significant digits, and `-0` becomes `0`. */
|
|
1950
|
+
function paramValue(param, value) {
|
|
1951
|
+
return normalize(param, value);
|
|
1952
|
+
}
|
|
1686
1953
|
/** This function gives the part of a space that has a stage, and the prefix of the names of its stages. */
|
|
1687
1954
|
function partOf(space, stage) {
|
|
1688
1955
|
for (const part of space.parts ?? []) {
|
|
@@ -1764,12 +2031,30 @@ function optionProblems(stage, o) {
|
|
|
1764
2031
|
if (problem !== void 0) out.push(["when", `${pat} has a when value that is not a value of ${p.when.param}: ${problem}`]);
|
|
1765
2032
|
}
|
|
1766
2033
|
});
|
|
2034
|
+
if (o.supports !== void 0) out.push(...supportsProblems(o.supports, at));
|
|
1767
2035
|
if (o.members !== void 0) {
|
|
1768
2036
|
const { min, max, ordered } = o.members;
|
|
1769
2037
|
if (!Number.isInteger(min) || !Number.isInteger(max) || min < 1 || min > max || typeof ordered !== "boolean") out.push(["members", `${at} has a member rule without whole counts 1 <= min <= max and a boolean ordered`]);
|
|
2038
|
+
const pool = o.members.pool;
|
|
2039
|
+
if (pool !== void 0 && (!Array.isArray(pool) || pool.length === 0 || !pool.every((entry) => Array.isArray(entry)))) out.push(["pool", `${at} has a pool that is not a list of one or more member lists`]);
|
|
1770
2040
|
}
|
|
1771
2041
|
return out;
|
|
1772
2042
|
}
|
|
2043
|
+
/** This function gives the problems of the `supports` of an option. */
|
|
2044
|
+
function supportsProblems(supports, at) {
|
|
2045
|
+
if (!isObject(supports)) return [["shape", `the supports of ${at} are not an object`]];
|
|
2046
|
+
const shape = supports["shape"];
|
|
2047
|
+
if (shape === void 0) return [];
|
|
2048
|
+
if (!Array.isArray(shape) || shape.length === 0) return [["shape", `${at} supports no shape kind. Remove shape to support each kind`]];
|
|
2049
|
+
const out = [];
|
|
2050
|
+
shape.forEach((k, i) => {
|
|
2051
|
+
if (!isShapeKind(k)) out.push(["shape", `${at} supports the shape kind ${show(k)}, which is not one of ${SHAPE_KIND_IDS.join(", ")}`]);
|
|
2052
|
+
else if (shape.indexOf(k) !== i) out.push(["duplicate", `${at} has the shape kind ${k} two times`]);
|
|
2053
|
+
});
|
|
2054
|
+
return out;
|
|
2055
|
+
}
|
|
2056
|
+
/** This function tells if an option supports each shape kind of the list. */
|
|
2057
|
+
const supportsShapes = (o, shapes) => shapes === void 0 || o.supports?.shape === void 0 || shapes.every((k) => o.supports.shape.includes(k));
|
|
1773
2058
|
/** This function gives the problems of the params of one stage choice. */
|
|
1774
2059
|
function choiceParamProblems(o, params, at) {
|
|
1775
2060
|
if (params !== void 0 && !isObject(params)) return [["config", `the params of ${at} are not an object`]];
|
|
@@ -1791,7 +2076,7 @@ function choiceParamProblems(o, params, at) {
|
|
|
1791
2076
|
return out;
|
|
1792
2077
|
}
|
|
1793
2078
|
/** This function gives the problems of a config of a space. A member of a meta option has `member: true`. */
|
|
1794
|
-
function configProblems(space, config, member) {
|
|
2079
|
+
function configProblems(space, config, member, shapes) {
|
|
1795
2080
|
if (!isObject(config) || !isObject(config["stages"])) return [["config", "the config is not an object with stages"]];
|
|
1796
2081
|
const out = [];
|
|
1797
2082
|
const stages = config["stages"];
|
|
@@ -1829,6 +2114,10 @@ function configProblems(space, config, member) {
|
|
|
1829
2114
|
}
|
|
1830
2115
|
chosen.set(s.name, o.id);
|
|
1831
2116
|
const at = `the option ${s.name}.${o.id}`;
|
|
2117
|
+
if (!supportsShapes(o, shapes)) {
|
|
2118
|
+
const missing = shapes.filter((k) => !o.supports.shape.includes(k));
|
|
2119
|
+
out.push(["shape", `${at} supports the shape kinds ${o.supports.shape.join(", ")}, not ${missing.join(", ")}`]);
|
|
2120
|
+
}
|
|
1832
2121
|
if (meta !== void 0 && meta !== s.name) out.push(["meta", `the stage ${s.name} is set, but the meta option of ${meta} replaces the other stages of its part`]);
|
|
1833
2122
|
out.push(...choiceParamProblems(o, c["params"], at));
|
|
1834
2123
|
if (o.members === void 0) {
|
|
@@ -1839,27 +2128,58 @@ function configProblems(space, config, member) {
|
|
|
1839
2128
|
out.push(["depth", `a member has the meta option ${s.name}.${o.id}. The depth of a meta config is 1`]);
|
|
1840
2129
|
continue;
|
|
1841
2130
|
}
|
|
1842
|
-
out.push(...memberProblems(partOf(space, s.name).space, o.members, c["members"], at));
|
|
2131
|
+
out.push(...memberProblems(partOf(space, s.name).space, o.members, c["members"], at, shapes));
|
|
1843
2132
|
}
|
|
1844
2133
|
for (const c of space.constraints ?? []) if (!holds(c, chosen)) out.push(["constraint", `the config breaks the constraint ${describe(c)}`]);
|
|
1845
2134
|
return out;
|
|
1846
2135
|
}
|
|
1847
2136
|
/** This function gives the problems of the members of a meta option. `part` is the space of the members. */
|
|
1848
|
-
function memberProblems(part, rule, members, at) {
|
|
2137
|
+
function memberProblems(part, rule, members, at, shapes) {
|
|
1849
2138
|
if (!Array.isArray(members)) return [["members", `${at} is a meta option without a list of members`]];
|
|
1850
2139
|
const out = [];
|
|
1851
2140
|
if (members.length < rule.min || members.length > rule.max) out.push(["members", `${at} has ${members.length} members, not ${rule.min} to ${rule.max}`]);
|
|
1852
2141
|
const ids = /* @__PURE__ */ new Map();
|
|
1853
2142
|
members.forEach((m, k) => {
|
|
1854
|
-
const inner = configProblems(part, m, true);
|
|
2143
|
+
const inner = configProblems(part, m, true, shapes);
|
|
1855
2144
|
for (const [claim, detail] of inner) out.push([claim, `the member ${k} of ${at}: ${detail}`]);
|
|
1856
2145
|
if (inner.length > 0) return;
|
|
1857
2146
|
const id = idOf(canonicalOf(part, m, true));
|
|
1858
2147
|
if (ids.has(id)) out.push(["duplicate", `the members ${ids.get(id)} and ${k} of ${at} are the same config ${id}`]);
|
|
1859
2148
|
else ids.set(id, k);
|
|
1860
2149
|
});
|
|
2150
|
+
if (rule.pool !== void 0 && out.length === 0) {
|
|
2151
|
+
const key = memberKey(part, rule, members);
|
|
2152
|
+
if (!rule.pool.some((entry) => memberKey(part, rule, entry) === key)) out.push(["pool", `${at} has a member list that is not in its pool of ${rule.pool.length} ${rule.pool.length === 1 ? "list" : "lists"}`]);
|
|
2153
|
+
}
|
|
1861
2154
|
return out;
|
|
1862
2155
|
}
|
|
2156
|
+
/** This function gives the key of a good member list: the `configId` of each member, sorted when the rule is not ordered. */
|
|
2157
|
+
function memberKey(part, rule, members) {
|
|
2158
|
+
const ids = members.map((m) => idOf(canonicalOf(part, m, true)));
|
|
2159
|
+
if (!rule.ordered) ids.sort();
|
|
2160
|
+
return ids.join(",");
|
|
2161
|
+
}
|
|
2162
|
+
/** This function gives the problems of the pool of a meta option of a good space. `part` is the space of the members. */
|
|
2163
|
+
function poolProblems(part, rule, at) {
|
|
2164
|
+
const out = [];
|
|
2165
|
+
const keys = /* @__PURE__ */ new Map();
|
|
2166
|
+
const plain = {
|
|
2167
|
+
min: rule.min,
|
|
2168
|
+
max: rule.max,
|
|
2169
|
+
ordered: rule.ordered
|
|
2170
|
+
};
|
|
2171
|
+
(rule.pool ?? []).forEach((entry, j) => {
|
|
2172
|
+
const inner = memberProblems(part, plain, entry, `the pool entry ${j} of ${at}`);
|
|
2173
|
+
out.push(...inner);
|
|
2174
|
+
if (inner.length > 0) return;
|
|
2175
|
+
const key = memberKey(part, rule, entry);
|
|
2176
|
+
if (keys.has(key)) out.push(["duplicate", `the pool entries ${keys.get(key)} and ${j} of ${at} have the same members`]);
|
|
2177
|
+
else keys.set(key, j);
|
|
2178
|
+
});
|
|
2179
|
+
return out;
|
|
2180
|
+
}
|
|
2181
|
+
/** 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. */
|
|
2182
|
+
const fits = (part, entry) => entry.every((m) => configProblems(part, m, true).length === 0);
|
|
1863
2183
|
const toBreaks = (problems, inputs = []) => problems.map(([claim, detail]) => ({
|
|
1864
2184
|
claim,
|
|
1865
2185
|
detail,
|
|
@@ -1868,13 +2188,17 @@ const toBreaks = (problems, inputs = []) => problems.map(([claim, detail]) => ({
|
|
|
1868
2188
|
/**
|
|
1869
2189
|
* This function checks one config of a space. It gives one break for each problem. An empty list tells that the config is good.
|
|
1870
2190
|
* The `claim` of a break is the kind of the problem, for example `missing`, `unknown`, `inactive`, `param`, `constraint`, `members` or `depth`.
|
|
2191
|
+
*
|
|
2192
|
+
* A member list that is not an entry of the `pool` of its option gives the claim `pool`.
|
|
2193
|
+
* 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.
|
|
1871
2194
|
*/
|
|
1872
|
-
function checkConfig(space, config) {
|
|
1873
|
-
return toBreaks(configProblems(space, config, false));
|
|
2195
|
+
function checkConfig(space, config, options = {}) {
|
|
2196
|
+
return toBreaks(configProblems(space, config, false, options.shapes));
|
|
1874
2197
|
}
|
|
1875
2198
|
/**
|
|
1876
2199
|
* This function checks a space. It gives one break for each problem. An empty list tells that the space is good.
|
|
1877
2200
|
* It finds duplicate names, empty ranges, defaults out of range, enums without values, bad log scales, unknown names and bad named configs.
|
|
2201
|
+
* It also checks each entry of a `pool`: the count of its members, each member and a repeated entry.
|
|
1878
2202
|
* The `inputs` of a break of a named config have the name of the config.
|
|
1879
2203
|
*/
|
|
1880
2204
|
function checkSpace(space) {
|
|
@@ -1913,6 +2237,8 @@ function checkSpace(space) {
|
|
|
1913
2237
|
}
|
|
1914
2238
|
const breaks = toBreaks(out);
|
|
1915
2239
|
if (breaks.length > 0) return breaks;
|
|
2240
|
+
for (const s of space.stages) for (const o of s.options) if (o.members?.pool !== void 0) breaks.push(...toBreaks(poolProblems(partOf(space, s.name).space, o.members, `the option ${s.name}.${o.id}`)));
|
|
2241
|
+
if (breaks.length > 0) return breaks;
|
|
1916
2242
|
for (const [name, config] of Object.entries(space.named ?? {})) breaks.push(...toBreaks(configProblems(space, config, false).map(([c, d]) => [c, `the named config ${name}: ${d}`]), [name]));
|
|
1917
2243
|
return breaks;
|
|
1918
2244
|
}
|
|
@@ -2048,7 +2374,7 @@ function* gridParams(params, levels, i = 0, values = {}) {
|
|
|
2048
2374
|
}
|
|
2049
2375
|
const optionIds = (choices) => new Map([...choices].map(([k, o]) => [k, o?.id ?? null]));
|
|
2050
2376
|
const keeps = (space, choices) => (space.constraints ?? []).every((c) => holds(c, optionIds(choices)));
|
|
2051
|
-
/** This function gives the count of the member lists of a meta option from `base` base configs, or `null` above `limit`. */
|
|
2377
|
+
/** This function gives the count of the member lists of a meta option without a pool from `base` base configs, or `null` above `limit`. */
|
|
2052
2378
|
function memberLists(base, rule, limit) {
|
|
2053
2379
|
let sum = 0n;
|
|
2054
2380
|
for (let m = rule.min; m <= rule.max; m++) {
|
|
@@ -2125,10 +2451,11 @@ function combos(space, levels, meta, limit) {
|
|
|
2125
2451
|
for (const s of stages) for (const o of s.options) {
|
|
2126
2452
|
if (o.members === void 0) continue;
|
|
2127
2453
|
const choices = new Map(stages.map((x) => [x.name, x === s ? o : null]));
|
|
2454
|
+
const lists = o.members.pool === void 0 ? memberLists(base, o.members, limit) : BigInt(o.members.pool.length);
|
|
2128
2455
|
if (keeps(space, choices)) out.push({
|
|
2129
2456
|
choices,
|
|
2130
2457
|
base: false,
|
|
2131
|
-
weight: times(countOf(o),
|
|
2458
|
+
weight: times(countOf(o), lists, limit)
|
|
2132
2459
|
});
|
|
2133
2460
|
}
|
|
2134
2461
|
return out;
|
|
@@ -2136,14 +2463,61 @@ function combos(space, levels, meta, limit) {
|
|
|
2136
2463
|
/**
|
|
2137
2464
|
* This function gives the count of the grid points of a space, with `levels` levels for each numeric param.
|
|
2138
2465
|
* It counts only the active params and the combinations that keep the constraints. It gives the meta configs apart.
|
|
2466
|
+
* A meta option with a `pool` counts each entry of the pool one time.
|
|
2467
|
+
* With `shapes`, it counts only the options that support each kind of the list.
|
|
2139
2468
|
*/
|
|
2140
2469
|
function spaceSize(space, options) {
|
|
2141
2470
|
assertSpace(space);
|
|
2142
2471
|
assertWhole(options.levels, "levels", 1);
|
|
2143
|
-
|
|
2472
|
+
assertShapes(options.shapes);
|
|
2473
|
+
return sizeOf(restrict(space, options.shapes), options.levels, options.maxMeta ?? 10n ** 18n);
|
|
2474
|
+
}
|
|
2475
|
+
/**
|
|
2476
|
+
* This function gives the space with only the options that support each shape kind of `shapes`. Without `shapes`, it gives the same space.
|
|
2477
|
+
* A stage can lose all of its options. Then the space has no config with that stage.
|
|
2478
|
+
* A pool keeps only the entries whose members fit the smaller space. A meta option without such an entry goes out.
|
|
2479
|
+
*/
|
|
2480
|
+
function restrict(space, shapes) {
|
|
2481
|
+
if (shapes === void 0) return space;
|
|
2482
|
+
const kept = {
|
|
2483
|
+
...space,
|
|
2484
|
+
stages: space.stages.map((s) => ({
|
|
2485
|
+
...s,
|
|
2486
|
+
options: s.options.filter((o) => supportsShapes(o, shapes))
|
|
2487
|
+
})),
|
|
2488
|
+
...space.parts === void 0 ? {} : { parts: space.parts.map((p) => restrict(p, shapes)) }
|
|
2489
|
+
};
|
|
2490
|
+
const withPool = (s, o) => {
|
|
2491
|
+
if (o.members?.pool === void 0) return [o];
|
|
2492
|
+
const part = partOf(kept, s.name).space;
|
|
2493
|
+
const pool = o.members.pool.filter((entry) => fits(part, entry));
|
|
2494
|
+
return pool.length === 0 ? [] : [{
|
|
2495
|
+
...o,
|
|
2496
|
+
members: {
|
|
2497
|
+
...o.members,
|
|
2498
|
+
pool
|
|
2499
|
+
}
|
|
2500
|
+
}];
|
|
2501
|
+
};
|
|
2502
|
+
return {
|
|
2503
|
+
...kept,
|
|
2504
|
+
stages: kept.stages.map((s) => ({
|
|
2505
|
+
...s,
|
|
2506
|
+
options: s.options.flatMap((o) => withPool(s, o))
|
|
2507
|
+
}))
|
|
2508
|
+
};
|
|
2509
|
+
}
|
|
2510
|
+
/** This function throws a `RangeError` for a list of shape kinds with a kind that is not declared. */
|
|
2511
|
+
function assertShapes(shapes) {
|
|
2512
|
+
if (shapes === void 0) return;
|
|
2513
|
+
if (!Array.isArray(shapes)) throw new RangeError("The option shapes is not a list of shape kinds.");
|
|
2514
|
+
for (const k of shapes) if (!isShapeKind(k)) throw new RangeError(`The shape kind ${show(k)} is not one of ${SHAPE_KIND_IDS.join(", ")}.`);
|
|
2515
|
+
}
|
|
2516
|
+
/** This function gives the count of the grid points of a space that is known to be good. */
|
|
2517
|
+
function sizeOf(space, levels, limit) {
|
|
2144
2518
|
let base = 0n;
|
|
2145
2519
|
let meta = 0n;
|
|
2146
|
-
for (const c of combos(space,
|
|
2520
|
+
for (const c of combos(space, levels, true, limit)) if (c.base) base += c.weight;
|
|
2147
2521
|
else meta = meta === null || c.weight === null || meta + c.weight > limit ? null : meta + c.weight;
|
|
2148
2522
|
return {
|
|
2149
2523
|
base,
|
|
@@ -2163,14 +2537,12 @@ function* product(lists, i = 0, acc = []) {
|
|
|
2163
2537
|
}
|
|
2164
2538
|
}
|
|
2165
2539
|
/** This function gives the configs of the grid of a space, without meta options. */
|
|
2166
|
-
function gridConfigs(space, levels, limit) {
|
|
2167
|
-
const
|
|
2168
|
-
|
|
2169
|
-
maxMeta: 0n
|
|
2170
|
-
}).base;
|
|
2540
|
+
function gridConfigs(space, levels, limit, shapes) {
|
|
2541
|
+
const kept = restrict(space, shapes);
|
|
2542
|
+
const size = sizeOf(kept, levels, 0n).base;
|
|
2171
2543
|
if (size > BigInt(limit)) throw new RangeError(`The grid of the space ${space.engine} has ${size} configs, more than the limit ${limit}.`);
|
|
2172
2544
|
const out = [];
|
|
2173
|
-
for (const c of combos(
|
|
2545
|
+
for (const c of combos(kept, levels, false, 0n)) {
|
|
2174
2546
|
const names = [...c.choices.keys()];
|
|
2175
2547
|
const lists = names.map((k) => {
|
|
2176
2548
|
const o = c.choices.get(k);
|
|
@@ -2218,6 +2590,7 @@ function sampleConfigs(space, n, seed, meta, maxTries) {
|
|
|
2218
2590
|
const lists = stages.map((s) => {
|
|
2219
2591
|
const list = s.options.filter((o) => meta || o.members === void 0);
|
|
2220
2592
|
if (s.optional) list.push(null);
|
|
2593
|
+
if (list.length === 0) throw new Error(`The stage ${s.name} of the space ${space.engine} has no option for a sample. A shape filter can remove all of its options.`);
|
|
2221
2594
|
return list;
|
|
2222
2595
|
});
|
|
2223
2596
|
const points = [];
|
|
@@ -2252,6 +2625,14 @@ function sampleConfigs(space, n, seed, meta, maxTries) {
|
|
|
2252
2625
|
});
|
|
2253
2626
|
}
|
|
2254
2627
|
if (o.members === void 0) continue;
|
|
2628
|
+
const fixed = o.members.pool;
|
|
2629
|
+
if (fixed !== void 0) {
|
|
2630
|
+
const r = stepRandom(seed, `pool-entry:${s.name}:${o.id}`);
|
|
2631
|
+
permutation(users.length, r).forEach((stratum, j) => {
|
|
2632
|
+
members[users[j]][k] = [...fixed[Math.min(fixed.length - 1, Math.floor((stratum + r()) / users.length * fixed.length))]];
|
|
2633
|
+
});
|
|
2634
|
+
continue;
|
|
2635
|
+
}
|
|
2255
2636
|
const part = partOf(space, s.name).space;
|
|
2256
2637
|
const poolSize = Math.max(n, 4 * o.members.max, 8);
|
|
2257
2638
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -2294,12 +2675,15 @@ function sampleConfigs(space, n, seed, meta, maxTries) {
|
|
|
2294
2675
|
* - The mode `grid` gives each grid point without a meta option, in a stable order. Stages, options and values go in the order of the space.
|
|
2295
2676
|
* - 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.
|
|
2296
2677
|
* - A sample draws again when a point breaks a constraint. After `maxTries` draws, it throws an `Error`. Two sampled configs can be equal.
|
|
2678
|
+
* - With `shapes`, only the options that support each kind of the list count. Without `shapes`, the result does not change.
|
|
2679
|
+
* - A meta option with a `pool` gives only the entries of its pool. With `shapes`, only the entries whose members support each kind count.
|
|
2297
2680
|
*/
|
|
2298
2681
|
function enumerateSpace(space, options) {
|
|
2299
2682
|
assertSpace(space);
|
|
2683
|
+
assertShapes(options.shapes);
|
|
2300
2684
|
if (options.mode === "grid") {
|
|
2301
2685
|
assertWhole(options.levels, "levels", 1);
|
|
2302
|
-
return gridConfigs(space, options.levels, options.limit ?? 1e5);
|
|
2686
|
+
return gridConfigs(space, options.levels, options.limit ?? 1e5, options.shapes);
|
|
2303
2687
|
}
|
|
2304
2688
|
if (options.mode !== "sample") throw new RangeError(`The mode ${String(options.mode)} is not grid or sample.`);
|
|
2305
2689
|
assertWhole(options.n, "n", 0);
|
|
@@ -2307,7 +2691,15 @@ function enumerateSpace(space, options) {
|
|
|
2307
2691
|
const meta = options.meta ?? "skip";
|
|
2308
2692
|
if (meta !== "skip" && meta !== "sample") throw new RangeError(`The option meta is ${String(meta)}, not skip or sample.`);
|
|
2309
2693
|
if (options.n === 0) return [];
|
|
2310
|
-
return sampleConfigs(space, options.n, options.seed, meta === "sample", options.maxTries ?? Math.max(1e3, 100 * options.n));
|
|
2694
|
+
return sampleConfigs(restrict(space, options.shapes), options.n, options.seed, meta === "sample", options.maxTries ?? Math.max(1e3, 100 * options.n));
|
|
2695
|
+
}
|
|
2696
|
+
/**
|
|
2697
|
+
* This function gives the shape kinds that a space accepts, in the order of `SHAPE_KINDS`. The value comes from the `supports` of the options.
|
|
2698
|
+
* A space accepts a kind when it has at least one config without a meta option on that kind.
|
|
2699
|
+
*/
|
|
2700
|
+
function spaceShapeKinds(space) {
|
|
2701
|
+
assertSpace(space);
|
|
2702
|
+
return SHAPE_KIND_IDS.filter((k) => sizeOf(restrict(space, [k]), 1, 0n).base > 0n);
|
|
2311
2703
|
}
|
|
2312
2704
|
/**
|
|
2313
2705
|
* This function joins configs of the parts of a product space into one config of the space `space`.
|
|
@@ -2600,6 +2992,596 @@ function verdict(records, options) {
|
|
|
2600
2992
|
dropped
|
|
2601
2993
|
};
|
|
2602
2994
|
}
|
|
2603
|
-
|
|
2995
|
+
// src/families.ts
|
|
2996
|
+
/** The random numbers of the default factory: `mulberry32` of `random.ts`. Its stream is pinned in `families.test.ts`. */
|
|
2997
|
+
const DEFAULT_RNG = mulberry32;
|
|
2998
|
+
/** This function gives the `kind` of the cells of a version of a family: `<name>@<version>`, for example `rect-grid@2`. Thus two versions do not share a family key. */
|
|
2999
|
+
function familyKind(name, version) {
|
|
3000
|
+
return `${name}@${version}`;
|
|
3001
|
+
}
|
|
3002
|
+
/** The default key of a family in a manifest: the kind of the cell alone. It is the map form of `FamilyBy` with the entry `"*"`. */
|
|
3003
|
+
const DEFAULT_FAMILY_BY = Object.freeze({ "*": Object.freeze([]) });
|
|
3004
|
+
/** This function gives an `enum` dimension. */
|
|
3005
|
+
const choice = (name, values, value, meaning, engines) => Object.freeze({
|
|
3006
|
+
name,
|
|
3007
|
+
kind: "enum",
|
|
3008
|
+
values: Object.freeze([...values]),
|
|
3009
|
+
default: value,
|
|
3010
|
+
meaning,
|
|
3011
|
+
engines: Object.freeze([...engines])
|
|
3012
|
+
});
|
|
3013
|
+
/** This function gives an `int` or a `real` dimension. */
|
|
3014
|
+
const range = (name, kind, limits, meaning, engines) => Object.freeze({
|
|
3015
|
+
name,
|
|
3016
|
+
kind,
|
|
3017
|
+
min: limits.min,
|
|
3018
|
+
max: limits.max,
|
|
3019
|
+
default: limits.value,
|
|
3020
|
+
scale: limits.scale ?? "linear",
|
|
3021
|
+
...limits.levels === void 0 ? {} : { levels: Object.freeze([...limits.levels]) },
|
|
3022
|
+
meaning,
|
|
3023
|
+
engines: Object.freeze([...engines])
|
|
3024
|
+
});
|
|
3025
|
+
const P = "packing";
|
|
3026
|
+
const H = "highways";
|
|
3027
|
+
const J = "joint";
|
|
3028
|
+
/**
|
|
3029
|
+
* The shared dimension table. A family takes a dimension of it with `sharedDimension`, or declares its own dimension.
|
|
3030
|
+
* The coverage table of each dimension and each engine is in `docs/CORPUS.md`.
|
|
3031
|
+
*/
|
|
3032
|
+
const CORPUS_DIMENSIONS = Object.freeze({
|
|
3033
|
+
count: range("count", "int", {
|
|
3034
|
+
min: 0,
|
|
3035
|
+
max: 1e5,
|
|
3036
|
+
value: 100,
|
|
3037
|
+
scale: "log",
|
|
3038
|
+
levels: [
|
|
3039
|
+
1,
|
|
3040
|
+
10,
|
|
3041
|
+
100,
|
|
3042
|
+
1e3,
|
|
3043
|
+
1e4
|
|
3044
|
+
]
|
|
3045
|
+
}, "The count of the items (n) of an input.", [
|
|
3046
|
+
P,
|
|
3047
|
+
H,
|
|
3048
|
+
J
|
|
3049
|
+
]),
|
|
3050
|
+
sizes: choice("sizes", [
|
|
3051
|
+
"identical",
|
|
3052
|
+
"nearIdentical",
|
|
3053
|
+
"uniform",
|
|
3054
|
+
"heavyTail",
|
|
3055
|
+
"bimodal",
|
|
3056
|
+
"extremeAspect"
|
|
3057
|
+
], "uniform", "The distribution of the sizes of the items.", [P, J]),
|
|
3058
|
+
aspect: range("aspect", "real", {
|
|
3059
|
+
min: .25,
|
|
3060
|
+
max: 4,
|
|
3061
|
+
value: 1,
|
|
3062
|
+
scale: "log"
|
|
3063
|
+
}, "The width over the height of a container with no fixed size.", [P]),
|
|
3064
|
+
density: range("density", "real", {
|
|
3065
|
+
min: .05,
|
|
3066
|
+
max: 1,
|
|
3067
|
+
value: .4,
|
|
3068
|
+
scale: "linear"
|
|
3069
|
+
}, "The area of the items over the area of the frame of the container.", [P]),
|
|
3070
|
+
obstacles: choice("obstacles", [
|
|
3071
|
+
"none",
|
|
3072
|
+
"center",
|
|
3073
|
+
"scattered",
|
|
3074
|
+
"wall",
|
|
3075
|
+
"corridors",
|
|
3076
|
+
"touching",
|
|
3077
|
+
"edge"
|
|
3078
|
+
], "none", "The layout of the fixed obstacles.", [P, J]),
|
|
3079
|
+
containerSize: choice("containerSize", [
|
|
3080
|
+
"fixed",
|
|
3081
|
+
"freeHeight",
|
|
3082
|
+
"freeWidth",
|
|
3083
|
+
"freeAspect",
|
|
3084
|
+
"freeArea"
|
|
3085
|
+
], "fixed", "The sides of the container that the engine can change.", [P, J]),
|
|
3086
|
+
containerShape: choice("containerShape", [
|
|
3087
|
+
"rect",
|
|
3088
|
+
"convex",
|
|
3089
|
+
"polygon",
|
|
3090
|
+
"circle",
|
|
3091
|
+
"ellipse",
|
|
3092
|
+
"holes"
|
|
3093
|
+
], "rect", "The kind of the shape of the container.", [P, J]),
|
|
3094
|
+
shape: choice("shape", SHAPE_KIND_IDS, "rect", "The shape kind of each item: a `ShapeKind` of `shapes.ts`.", [P]),
|
|
3095
|
+
order: choice("order", ["free", "soft"], "free", "The reading order of the items: no order, or a rank on each item.", [P]),
|
|
3096
|
+
groups: choice("groups", [
|
|
3097
|
+
"none",
|
|
3098
|
+
"flat",
|
|
3099
|
+
"nested"
|
|
3100
|
+
], "none", "The groups of the items: no groups, one level, or groups in groups.", [H, J]),
|
|
3101
|
+
edgeDensity: choice("edgeDensity", [
|
|
3102
|
+
"sparse",
|
|
3103
|
+
"medium",
|
|
3104
|
+
"dense"
|
|
3105
|
+
], "medium", "The mean count of the edges of each item: 0.5, 1.5 or 4.", [H, J]),
|
|
3106
|
+
edgeShape: choice("edgeShape", [
|
|
3107
|
+
"random",
|
|
3108
|
+
"clustered",
|
|
3109
|
+
"hubChain",
|
|
3110
|
+
"hubs",
|
|
3111
|
+
"longRange",
|
|
3112
|
+
"tree",
|
|
3113
|
+
"bundled"
|
|
3114
|
+
], "random", "The topology of the edges.", [H, J]),
|
|
3115
|
+
ports: choice("ports", [
|
|
3116
|
+
"free",
|
|
3117
|
+
"sides",
|
|
3118
|
+
"fixed"
|
|
3119
|
+
], "free", "The ports of the edges: free, a side for each end, or fixed ports.", [H, J]),
|
|
3120
|
+
hubBy: Object.freeze({
|
|
3121
|
+
...choice("hubBy", ["area", "random"], "area", "The order of the hubs of the shapes hubs and tree: the largest item first, or random.", [J]),
|
|
3122
|
+
when: Object.freeze({
|
|
3123
|
+
param: "edgeShape",
|
|
3124
|
+
in: Object.freeze(["hubs", "tree"])
|
|
3125
|
+
})
|
|
3126
|
+
}),
|
|
3127
|
+
spacing: choice("spacing", [
|
|
3128
|
+
"stress",
|
|
3129
|
+
"gap",
|
|
3130
|
+
"margin",
|
|
3131
|
+
"both"
|
|
3132
|
+
], "both", "The split of the route clearance between the margins and the gap. `stress` has neither.", [J]),
|
|
3133
|
+
margin: range("margin", "real", {
|
|
3134
|
+
min: 0,
|
|
3135
|
+
max: 32,
|
|
3136
|
+
value: 0
|
|
3137
|
+
}, "The margin of each item, in units.", [P, J]),
|
|
3138
|
+
gap: range("gap", "real", {
|
|
3139
|
+
min: 0,
|
|
3140
|
+
max: 32,
|
|
3141
|
+
value: 0
|
|
3142
|
+
}, "The free space between two items, in units.", [P, J])
|
|
3143
|
+
});
|
|
3144
|
+
/** This function gives a dimension of `CORPUS_DIMENSIONS`, with the fields of `patch` over it. It throws a `RangeError` for an unknown name. */
|
|
3145
|
+
function sharedDimension(name, patch = {}) {
|
|
3146
|
+
const base = Object.hasOwn(CORPUS_DIMENSIONS, name) ? CORPUS_DIMENSIONS[name] : void 0;
|
|
3147
|
+
if (base === void 0) throw new RangeError(`The shared dimension table has no dimension "${name}".`);
|
|
3148
|
+
return Object.freeze({
|
|
3149
|
+
...base,
|
|
3150
|
+
...patch,
|
|
3151
|
+
name
|
|
3152
|
+
});
|
|
3153
|
+
}
|
|
3154
|
+
/** This function tells if a log scale of a range starts at 0. Then the scale is the log of `1 + x - min`. */
|
|
3155
|
+
const fromZero = (d) => d.scale === "log" && (d.min ?? 0) <= 0;
|
|
3156
|
+
/**
|
|
3157
|
+
* This function gives the value at the fraction `t` of the range of a numeric dimension, on its scale. A slider of a site uses it.
|
|
3158
|
+
* A log scale with a `min` of 0 or less is the log of `1 + x - min`. An `int` dimension gives a whole number.
|
|
3159
|
+
*/
|
|
3160
|
+
function dimensionAt(dim, t) {
|
|
3161
|
+
const min = dim.min ?? 0;
|
|
3162
|
+
const max = dim.max ?? min;
|
|
3163
|
+
const u = Math.min(1, Math.max(0, t));
|
|
3164
|
+
let v;
|
|
3165
|
+
if (dim.scale !== "log") v = min + u * (max - min);
|
|
3166
|
+
else if (fromZero(dim)) v = min + Math.expm1(u * Math.log1p(max - min));
|
|
3167
|
+
else v = Math.exp(Math.log(min) + u * (Math.log(max) - Math.log(min)));
|
|
3168
|
+
v = Math.min(max, Math.max(min, v));
|
|
3169
|
+
return dim.kind === "int" ? Math.round(v) : paramValue(dim, v);
|
|
3170
|
+
}
|
|
3171
|
+
/** This function gives the fraction of the range of a numeric dimension at a value, on its scale. It is the inverse of `dimensionAt`. */
|
|
3172
|
+
function dimensionUnit(dim, value) {
|
|
3173
|
+
const min = dim.min ?? 0;
|
|
3174
|
+
const max = dim.max ?? min;
|
|
3175
|
+
if (max === min) return 0;
|
|
3176
|
+
const v = Math.min(max, Math.max(min, value));
|
|
3177
|
+
if (dim.scale !== "log") return (v - min) / (max - min);
|
|
3178
|
+
if (fromZero(dim)) return Math.log1p(v - min) / Math.log1p(max - min);
|
|
3179
|
+
return (Math.log(v) - Math.log(min)) / (Math.log(max) - Math.log(min));
|
|
3180
|
+
}
|
|
3181
|
+
/**
|
|
3182
|
+
* This function gives the values of a sweep of a dimension. `levels` of the dimension come first. Then an `enum` gives its values, and a `bool` gives `false` and `true`.
|
|
3183
|
+
* With `steps`, a numeric dimension gives `steps` points from `min` to `max` on its scale. Else it gives its default.
|
|
3184
|
+
*/
|
|
3185
|
+
function levelsOf(dim, steps) {
|
|
3186
|
+
if (dim.levels !== void 0) return [...dim.levels];
|
|
3187
|
+
if (dim.kind === "enum" || dim.kind === "bool" || steps === void 0) return paramLevels(dim, 1);
|
|
3188
|
+
if (!fromZero(dim)) return paramLevels(dim, steps);
|
|
3189
|
+
if (!Number.isInteger(steps) || steps < 1) throw new RangeError(`The steps of a sweep are a whole number of 1 or more, not ${steps}.`);
|
|
3190
|
+
if (steps === 1) return [paramValue(dim, dim.default)];
|
|
3191
|
+
return [...new Set(Array.from({ length: steps }, (_, j) => dimensionAt(dim, j / (steps - 1))))];
|
|
3192
|
+
}
|
|
3193
|
+
/** This function gives the problems of the dimensions of a family: a repeated name, a bad default, a bad level or a bad `when`. */
|
|
3194
|
+
function dimensionProblems(dims) {
|
|
3195
|
+
const out = [];
|
|
3196
|
+
const names = dims.map((d) => d.name);
|
|
3197
|
+
dims.forEach((d, i) => {
|
|
3198
|
+
if (names.indexOf(d.name) !== i) out.push(`the dimension "${d.name}" is in the list two times`);
|
|
3199
|
+
const bad = paramProblem(d, d.default);
|
|
3200
|
+
if (bad !== void 0) out.push(`the default of "${d.name}" is bad: ${bad}`);
|
|
3201
|
+
for (const v of d.levels ?? []) {
|
|
3202
|
+
const p = paramProblem(d, v);
|
|
3203
|
+
if (p !== void 0) out.push(`a level of "${d.name}" is bad: ${p}`);
|
|
3204
|
+
}
|
|
3205
|
+
if (d.when === void 0) return;
|
|
3206
|
+
const j = names.indexOf(d.when.param);
|
|
3207
|
+
if (j < 0 || j >= i) out.push(`the when of "${d.name}" names "${d.when.param}", which is not an earlier dimension`);
|
|
3208
|
+
else if (d.when.in.length === 0) out.push(`the when of "${d.name}" has no values`);
|
|
3209
|
+
else for (const v of d.when.in) if (paramProblem(dims[j], v) !== void 0) out.push(`the when of "${d.name}" has a value that "${d.when.param}" does not have`);
|
|
3210
|
+
});
|
|
3211
|
+
return out;
|
|
3212
|
+
}
|
|
3213
|
+
/**
|
|
3214
|
+
* This function gives the full dimension values of an input. A missing dimension gets its default. An inactive dimension of `when` also gets its default.
|
|
3215
|
+
* Thus a default that the caller gives, a missing value and an inactive value give the same values. A real number gets its stable form.
|
|
3216
|
+
* The function throws a `RangeError` for an unknown dimension or a bad value, also for a bad value of an inactive dimension.
|
|
3217
|
+
*/
|
|
3218
|
+
function resolveDimensions(dimensions, dims = {}, where = "The family") {
|
|
3219
|
+
const known = new Set(dimensions.map((d) => d.name));
|
|
3220
|
+
for (const key of Object.keys(dims)) if (!known.has(key)) throw new RangeError(`${where} has no dimension "${key}".`);
|
|
3221
|
+
const active = {};
|
|
3222
|
+
const out = {};
|
|
3223
|
+
for (const d of dimensions) {
|
|
3224
|
+
const given = Object.hasOwn(dims, d.name) ? dims[d.name] : void 0;
|
|
3225
|
+
if (given !== void 0) {
|
|
3226
|
+
const bad = paramProblem(d, given);
|
|
3227
|
+
if (bad !== void 0) throw new RangeError(`${where}: the dimension "${d.name}" is bad: ${bad}.`);
|
|
3228
|
+
}
|
|
3229
|
+
const on = paramActive(d, active);
|
|
3230
|
+
const value = paramValue(d, on && given !== void 0 ? given : d.default);
|
|
3231
|
+
if (on) active[d.name] = value;
|
|
3232
|
+
out[d.name] = value;
|
|
3233
|
+
}
|
|
3234
|
+
return out;
|
|
3235
|
+
}
|
|
3236
|
+
/** This function gives the values of the active dimensions only. */
|
|
3237
|
+
function activeValues(dimensions, full) {
|
|
3238
|
+
const active = {};
|
|
3239
|
+
for (const d of dimensions) if (paramActive(d, active)) active[d.name] = full[d.name];
|
|
3240
|
+
return active;
|
|
3241
|
+
}
|
|
3242
|
+
/** This function gives the active values of each cell of a sweep, in the order of the dimensions. The last dimension changes first. */
|
|
3243
|
+
function* sweepOf(dimensions, levels, i = 0, values = {}) {
|
|
3244
|
+
if (i === dimensions.length) {
|
|
3245
|
+
yield { ...values };
|
|
3246
|
+
return;
|
|
3247
|
+
}
|
|
3248
|
+
const d = dimensions[i];
|
|
3249
|
+
const lv = levels[i];
|
|
3250
|
+
if (lv === void 0 || !paramActive(d, values)) {
|
|
3251
|
+
yield* sweepOf(dimensions, levels, i + 1, paramActive(d, values) ? {
|
|
3252
|
+
...values,
|
|
3253
|
+
[d.name]: paramValue(d, d.default)
|
|
3254
|
+
} : values);
|
|
3255
|
+
return;
|
|
3256
|
+
}
|
|
3257
|
+
for (const v of lv) yield* sweepOf(dimensions, levels, i + 1, {
|
|
3258
|
+
...values,
|
|
3259
|
+
[d.name]: paramValue(d, v)
|
|
3260
|
+
});
|
|
3261
|
+
}
|
|
3262
|
+
/** This function counts the cells of a sweep with no list of the cells. A dimension that a later `when` reads splits the count. */
|
|
3263
|
+
function countOf(dimensions, levels, i = 0, values = {}) {
|
|
3264
|
+
if (i === dimensions.length) return 1;
|
|
3265
|
+
const d = dimensions[i];
|
|
3266
|
+
if (!paramActive(d, values)) return countOf(dimensions, levels, i + 1, values);
|
|
3267
|
+
const lv = levels[i] ?? [d.default];
|
|
3268
|
+
if (!dimensions.slice(i + 1).some((q) => q.when?.param === d.name)) return lv.length * countOf(dimensions, levels, i + 1, values);
|
|
3269
|
+
let sum = 0;
|
|
3270
|
+
for (const v of lv) sum += countOf(dimensions, levels, i + 1, {
|
|
3271
|
+
...values,
|
|
3272
|
+
[d.name]: paramValue(d, v)
|
|
3273
|
+
});
|
|
3274
|
+
return sum;
|
|
3275
|
+
}
|
|
3276
|
+
/** This function gives the levels of each dimension of a sweep. A dimension outside the sweep has `undefined`, so it keeps its default. */
|
|
3277
|
+
function sweepLevels(dimensions, options, where) {
|
|
3278
|
+
const names = new Set(dimensions.map((d) => d.name));
|
|
3279
|
+
for (const name of [...options.sweep ?? [], ...Object.keys(options.levels ?? {})]) if (!names.has(name)) throw new RangeError(`${where} has no dimension "${name}".`);
|
|
3280
|
+
return dimensions.map((d) => {
|
|
3281
|
+
if (options.sweep !== void 0 && !options.sweep.includes(d.name)) return void 0;
|
|
3282
|
+
const lv = options.levels !== void 0 && Object.hasOwn(options.levels, d.name) ? options.levels[d.name] : levelsOf(d);
|
|
3283
|
+
for (const v of lv) {
|
|
3284
|
+
const bad = paramProblem(d, v);
|
|
3285
|
+
if (bad !== void 0) throw new RangeError(`${where}: a level of "${d.name}" is bad: ${bad}.`);
|
|
3286
|
+
}
|
|
3287
|
+
return lv;
|
|
3288
|
+
});
|
|
3289
|
+
}
|
|
3290
|
+
/** This function gives the values that make a dimension active: the first value of its `when`, and the values that make that dimension active. */
|
|
3291
|
+
function activation(dimensions, dim) {
|
|
3292
|
+
const out = {};
|
|
3293
|
+
let d = dim;
|
|
3294
|
+
while (d?.when !== void 0) {
|
|
3295
|
+
const param = d.when.param;
|
|
3296
|
+
out[param] = d.when.in[0];
|
|
3297
|
+
d = dimensions.find((q) => q.name === param);
|
|
3298
|
+
}
|
|
3299
|
+
return out;
|
|
3300
|
+
}
|
|
3301
|
+
/** This function throws a `RangeError` when a seed is not a whole number of 0 or more. */
|
|
3302
|
+
function checkSeed(seed) {
|
|
3303
|
+
if (!Number.isSafeInteger(seed) || seed < 0) throw new RangeError(`The seed is a whole number of 0 or more, not ${seed}.`);
|
|
3304
|
+
}
|
|
3305
|
+
/** This function gives a new registry with the families of the list. Each engine gives the list of its families. An import registers nothing. */
|
|
3306
|
+
function createFamilyRegistry(families = []) {
|
|
3307
|
+
const list = [];
|
|
3308
|
+
const label = (f) => `The family "${f.name}" version ${f.version}`;
|
|
3309
|
+
const find = (name, version) => {
|
|
3310
|
+
const named = list.filter((f) => f.name === name);
|
|
3311
|
+
if (named.length === 0) throw new RangeError(`The registry has no family "${name}".`);
|
|
3312
|
+
if (version === void 0) return named[0];
|
|
3313
|
+
const found = named.find((f) => f.version === version);
|
|
3314
|
+
if (found === void 0) throw new RangeError(`The family "${name}" has the versions ${named.map((f) => f.version).join(", ")}, not ${version}.`);
|
|
3315
|
+
return found;
|
|
3316
|
+
};
|
|
3317
|
+
/** This function removes the `kind` of a cell of `cellOf` when it is the `familyKind` of `f`. It throws a `RangeError` for another kind. */
|
|
3318
|
+
const ownDims = (f, dims) => {
|
|
3319
|
+
if (dims === void 0 || !Object.hasOwn(dims, "kind")) return dims;
|
|
3320
|
+
const kind = familyKind(f.name, f.version);
|
|
3321
|
+
if (dims["kind"] !== kind) throw new RangeError(`${label(f)} has the kind "${kind}", not the kind ${JSON.stringify(dims["kind"])} of the cell.`);
|
|
3322
|
+
const { kind: _kind, ...rest } = dims;
|
|
3323
|
+
return rest;
|
|
3324
|
+
};
|
|
3325
|
+
const streamOf = (f, seed, full) => {
|
|
3326
|
+
const rng = f.rng ?? DEFAULT_RNG;
|
|
3327
|
+
return {
|
|
3328
|
+
rng,
|
|
3329
|
+
random: rng(f.rngSeed === void 0 ? seed : f.rngSeed(seed, full))
|
|
3330
|
+
};
|
|
3331
|
+
};
|
|
3332
|
+
const registry = {
|
|
3333
|
+
register(family) {
|
|
3334
|
+
if (!Number.isInteger(family.version) || family.version < 1) throw new RangeError(`${label(family)} has a version that is not a whole number of 1 or more.`);
|
|
3335
|
+
if (list.some((f) => f.name === family.name && f.version === family.version)) throw new RangeError(`${label(family)} is in the registry already.`);
|
|
3336
|
+
const problems = dimensionProblems(family.dimensions);
|
|
3337
|
+
if (family.dimensions.map((d) => d.name).includes("kind")) problems.push("the dimension name \"kind\" is reserved: the cell of `cellOf` has the `familyKind` of the family in `kind`");
|
|
3338
|
+
for (const key of family.familyBy ?? []) {
|
|
3339
|
+
const d = family.dimensions.find((q) => q.name === key);
|
|
3340
|
+
if (d === void 0) problems.push(`the familyBy dimension "${key}" is not a dimension of the family`);
|
|
3341
|
+
else if (d.when !== void 0) problems.push(`the familyBy dimension "${key}" has a when, so a cell without it has no family key`);
|
|
3342
|
+
}
|
|
3343
|
+
if (problems.length > 0) throw new RangeError(`${label(family)} is not good: ${problems.join("; ")}.`);
|
|
3344
|
+
list.push(family);
|
|
3345
|
+
return registry;
|
|
3346
|
+
},
|
|
3347
|
+
family: find,
|
|
3348
|
+
families: () => [...list],
|
|
3349
|
+
resolve(name, dims, version) {
|
|
3350
|
+
const f = find(name, version);
|
|
3351
|
+
return resolveDimensions(f.dimensions, ownDims(f, dims), label(f));
|
|
3352
|
+
},
|
|
3353
|
+
generate(name, seed, dims, options = {}) {
|
|
3354
|
+
checkSeed(seed);
|
|
3355
|
+
const f = find(name, options.version);
|
|
3356
|
+
const full = resolveDimensions(f.dimensions, ownDims(f, dims), label(f));
|
|
3357
|
+
const context = {
|
|
3358
|
+
...streamOf(f, seed, full),
|
|
3359
|
+
...options.set === void 0 ? {} : { set: options.set }
|
|
3360
|
+
};
|
|
3361
|
+
return f.generate(seed, full, context);
|
|
3362
|
+
},
|
|
3363
|
+
cellOf(name, dims, version) {
|
|
3364
|
+
const f = find(name, version);
|
|
3365
|
+
return {
|
|
3366
|
+
kind: familyKind(f.name, f.version),
|
|
3367
|
+
...activeValues(f.dimensions, resolveDimensions(f.dimensions, ownDims(f, dims), label(f)))
|
|
3368
|
+
};
|
|
3369
|
+
},
|
|
3370
|
+
*cells(name, options = {}) {
|
|
3371
|
+
const f = find(name, options.version);
|
|
3372
|
+
for (const values of sweepOf(f.dimensions, sweepLevels(f.dimensions, options, label(f)))) yield resolveDimensions(f.dimensions, values, label(f));
|
|
3373
|
+
},
|
|
3374
|
+
cellCount(name, options = {}) {
|
|
3375
|
+
const f = find(name, options.version);
|
|
3376
|
+
return countOf(f.dimensions, sweepLevels(f.dimensions, options, label(f)));
|
|
3377
|
+
},
|
|
3378
|
+
boundaryCells(name, version) {
|
|
3379
|
+
const f = find(name, version);
|
|
3380
|
+
const base = resolveDimensions(f.dimensions, {}, label(f));
|
|
3381
|
+
const out = [base];
|
|
3382
|
+
const seen = /* @__PURE__ */ new Set([JSON.stringify(base)]);
|
|
3383
|
+
for (const d of f.dimensions) {
|
|
3384
|
+
if (d.kind !== "int" && d.kind !== "real") continue;
|
|
3385
|
+
for (const end of [d.min, d.max]) {
|
|
3386
|
+
if (end === void 0) continue;
|
|
3387
|
+
const cell = resolveDimensions(f.dimensions, {
|
|
3388
|
+
...activation(f.dimensions, d),
|
|
3389
|
+
[d.name]: end
|
|
3390
|
+
}, label(f));
|
|
3391
|
+
const key = JSON.stringify(cell);
|
|
3392
|
+
if (!seen.has(key)) {
|
|
3393
|
+
seen.add(key);
|
|
3394
|
+
out.push(cell);
|
|
3395
|
+
}
|
|
3396
|
+
}
|
|
3397
|
+
}
|
|
3398
|
+
return out;
|
|
3399
|
+
},
|
|
3400
|
+
rngOf(name, version) {
|
|
3401
|
+
const f = find(name, version);
|
|
3402
|
+
return (seed, dims) => {
|
|
3403
|
+
checkSeed(seed);
|
|
3404
|
+
return streamOf(f, seed, resolveDimensions(f.dimensions, ownDims(f, dims), label(f))).random;
|
|
3405
|
+
};
|
|
3406
|
+
},
|
|
3407
|
+
familyBy() {
|
|
3408
|
+
const out = {};
|
|
3409
|
+
for (const f of list) out[familyKind(f.name, f.version)] = [...f.familyBy ?? []];
|
|
3410
|
+
return out;
|
|
3411
|
+
}
|
|
3412
|
+
};
|
|
3413
|
+
for (const f of families) registry.register(f);
|
|
3414
|
+
return registry;
|
|
3415
|
+
}
|
|
3416
|
+
/** This function gives the values of a list of dimensions from the full values. */
|
|
3417
|
+
const pick = (dims, full) => Object.fromEntries(dims.map((d) => [d.name, full[d.name]]));
|
|
3418
|
+
/**
|
|
3419
|
+
* This function gives a family that runs the base family and then each step, with the same seed.
|
|
3420
|
+
* The family of the input gets the part of each step after a dot. The id becomes `<set>/<family>/s<seed>`, and the input gets the hash of `options.hash`.
|
|
3421
|
+
* The function throws a `RangeError` when a step has a dimension with the name of an earlier dimension.
|
|
3422
|
+
*/
|
|
3423
|
+
function composeFamily(base, steps, options = {}) {
|
|
3424
|
+
const dimensions = [...base.dimensions, ...steps.flatMap((s) => s.dimensions)];
|
|
3425
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3426
|
+
for (const d of dimensions) {
|
|
3427
|
+
if (seen.has(d.name)) throw new RangeError(`composeFamily: the dimension "${d.name}" is in the base family or in an earlier step already.`);
|
|
3428
|
+
seen.add(d.name);
|
|
3429
|
+
}
|
|
3430
|
+
const hash = options.hash ?? documentHash;
|
|
3431
|
+
const libm = base.crossEngine === "libm-dependent" || steps.some((s) => s.crossEngine === "libm-dependent");
|
|
3432
|
+
return Object.freeze({
|
|
3433
|
+
name: options.name ?? [base.name, ...steps.map((s) => s.name)].join("+"),
|
|
3434
|
+
version: base.version,
|
|
3435
|
+
owner: options.owner ?? base.owner,
|
|
3436
|
+
meaning: options.meaning ?? `${base.meaning} Then the steps ${steps.map((s) => s.name).join(", ")}.`,
|
|
3437
|
+
dimensions,
|
|
3438
|
+
seeding: [base.seeding, ...steps.map((s) => `${s.name}: ${s.seeding}`)].join(" "),
|
|
3439
|
+
crossEngine: libm ? "libm-dependent" : "exact",
|
|
3440
|
+
...base.rng === void 0 ? {} : { rng: base.rng },
|
|
3441
|
+
...base.rngSeed === void 0 ? {} : { rngSeed: (seed, full) => base.rngSeed(seed, pick(base.dimensions, full)) },
|
|
3442
|
+
...base.familyBy === void 0 ? {} : { familyBy: base.familyBy },
|
|
3443
|
+
generate(seed, full, context) {
|
|
3444
|
+
let input = base.generate(seed, pick(base.dimensions, full), context);
|
|
3445
|
+
const parts = [];
|
|
3446
|
+
for (const step of steps) {
|
|
3447
|
+
const values = pick(step.dimensions, full);
|
|
3448
|
+
input = step.apply(input, seed, values);
|
|
3449
|
+
if (step.familyPart !== void 0) parts.push(step.familyPart(values));
|
|
3450
|
+
}
|
|
3451
|
+
const family = [input.family ?? base.name, ...parts].join(".");
|
|
3452
|
+
const set = input.set ?? context.set;
|
|
3453
|
+
const id = `${set === void 0 ? "" : `${set}/`}${family}/s${seed}`;
|
|
3454
|
+
const stepped = {
|
|
3455
|
+
...input,
|
|
3456
|
+
id,
|
|
3457
|
+
family
|
|
3458
|
+
};
|
|
3459
|
+
return {
|
|
3460
|
+
...stepped,
|
|
3461
|
+
hash: hash(stepped)
|
|
3462
|
+
};
|
|
3463
|
+
}
|
|
3464
|
+
});
|
|
3465
|
+
}
|
|
3466
|
+
/** The dimensions of `EDGES_STEP`: the parameters of `edgeStep`, from the shared table. */
|
|
3467
|
+
const EDGE_DIMENSIONS = Object.freeze([
|
|
3468
|
+
"edgeDensity",
|
|
3469
|
+
"edgeShape",
|
|
3470
|
+
"groups",
|
|
3471
|
+
"ports",
|
|
3472
|
+
"hubBy"
|
|
3473
|
+
].map((n) => CORPUS_DIMENSIONS[n]));
|
|
3474
|
+
/** This function gives the parameters of `edgeStep` from the values of the dimensions. It throws a `RangeError` for a missing or a bad value. */
|
|
3475
|
+
function edgeParams(d) {
|
|
3476
|
+
const text = (name) => {
|
|
3477
|
+
const dim = EDGE_DIMENSIONS.find((q) => q.name === name);
|
|
3478
|
+
const value = Object.hasOwn(d, name) ? d[name] : void 0;
|
|
3479
|
+
const bad = value === void 0 ? "the value is missing" : paramProblem(dim, value);
|
|
3480
|
+
if (bad !== void 0 || typeof value !== "string") throw new RangeError(`EDGES_STEP: the dimension "${name}" is bad: ${bad ?? "it is not text"}.`);
|
|
3481
|
+
return value;
|
|
3482
|
+
};
|
|
3483
|
+
return {
|
|
3484
|
+
edgeDensity: text("edgeDensity"),
|
|
3485
|
+
edgeShape: text("edgeShape"),
|
|
3486
|
+
groups: text("groups"),
|
|
3487
|
+
ports: text("ports"),
|
|
3488
|
+
hubBy: text("hubBy")
|
|
3489
|
+
};
|
|
3490
|
+
}
|
|
3491
|
+
/**
|
|
3492
|
+
* The step `edges` of `edgeStep` (version 1), as a corpus step. Its family part is `e-<edgeDensity>.<edgeShape>.<groups>.<ports>`.
|
|
3493
|
+
* The value `random` of `hubBy` adds the part `.hub-random`.
|
|
3494
|
+
*/
|
|
3495
|
+
const EDGES_STEP = Object.freeze({
|
|
3496
|
+
name: "edges",
|
|
3497
|
+
version: 1,
|
|
3498
|
+
owner: "layout-contract",
|
|
3499
|
+
meaning: "The edges, the groups and the ports of an input, from its topology alone.",
|
|
3500
|
+
dimensions: EDGE_DIMENSIONS,
|
|
3501
|
+
seeding: "stepRandom(seed, step) for the steps edges and ports.",
|
|
3502
|
+
crossEngine: "libm-dependent",
|
|
3503
|
+
familyPart: (d) => `e-${d["edgeDensity"]}.${d["edgeShape"]}.${d["groups"]}.${d["ports"]}${d["hubBy"] === "random" ? ".hub-random" : ""}`,
|
|
3504
|
+
apply: (input, seed, d) => edgeStep(input, edgeParams(d), seed)
|
|
3505
|
+
});
|
|
3506
|
+
/** The set of `RECT_SCATTERED` when the caller gives no set. */
|
|
3507
|
+
const XCHECK_SET = "xcheck-v1";
|
|
3508
|
+
/**
|
|
3509
|
+
* The shared family `rect-scattered`, version 1: a fixed rectangle container, rectangle obstacles at random places and boxes without positions.
|
|
3510
|
+
* It is the one copy of the generator of `docs/FORMAT.md` of `packing`. Its input is the input of `packing`, byte for byte.
|
|
3511
|
+
* The steps `container`, `obstacles` and `boxes` each have a stream of `stepRandom`. Each value is a whole number, so the family is `exact`.
|
|
3512
|
+
*/
|
|
3513
|
+
const RECT_SCATTERED = Object.freeze({
|
|
3514
|
+
name: "rect-scattered",
|
|
3515
|
+
version: 1,
|
|
3516
|
+
owner: "layout-contract",
|
|
3517
|
+
meaning: "Rectangle obstacles at random places in a fixed rectangle container, and boxes of random sizes.",
|
|
3518
|
+
dimensions: Object.freeze([sharedDimension("count", { default: 40 })]),
|
|
3519
|
+
seeding: "stepRandom(seed, container) gives w and h. stepRandom(seed, obstacles) gives n, then ow, oh, x and y of each obstacle. stepRandom(seed, boxes) gives bw and bh of each box.",
|
|
3520
|
+
crossEngine: "exact",
|
|
3521
|
+
generate(seed, dims, context) {
|
|
3522
|
+
const count = dims["count"];
|
|
3523
|
+
if (typeof count !== "number" || !Number.isInteger(count) || count < 0) throw new RangeError(`rect-scattered: the count is a whole number of 0 or more, not ${String(count)}.`);
|
|
3524
|
+
const set = context.set ?? XCHECK_SET;
|
|
3525
|
+
let r = stepRandom(seed, "container");
|
|
3526
|
+
const w = randomInt(r, 600, 1e3);
|
|
3527
|
+
const h = randomInt(r, 400, 800);
|
|
3528
|
+
r = stepRandom(seed, "obstacles");
|
|
3529
|
+
const n = randomInt(r, 3, 8);
|
|
3530
|
+
const obstacles = [];
|
|
3531
|
+
for (let i = 0; i < n; i++) {
|
|
3532
|
+
const ow = randomInt(r, 40, 160);
|
|
3533
|
+
const oh = randomInt(r, 40, 160);
|
|
3534
|
+
const x = randomInt(r, 0, w - ow);
|
|
3535
|
+
const y = randomInt(r, 0, h - oh);
|
|
3536
|
+
obstacles.push({
|
|
3537
|
+
id: `o${i}`,
|
|
3538
|
+
region: {
|
|
3539
|
+
kind: "rect",
|
|
3540
|
+
x,
|
|
3541
|
+
y,
|
|
3542
|
+
w: ow,
|
|
3543
|
+
h: oh
|
|
3544
|
+
}
|
|
3545
|
+
});
|
|
3546
|
+
}
|
|
3547
|
+
r = stepRandom(seed, "boxes");
|
|
3548
|
+
const boxes = Array.from({ length: count }, (_, i) => {
|
|
3549
|
+
const bw = randomInt(r, 20, 120);
|
|
3550
|
+
const bh = randomInt(r, 20, 80);
|
|
3551
|
+
return {
|
|
3552
|
+
id: `b${i}`,
|
|
3553
|
+
w: bw,
|
|
3554
|
+
h: bh
|
|
3555
|
+
};
|
|
3556
|
+
});
|
|
3557
|
+
const body = {
|
|
3558
|
+
schema: "layout-input/1",
|
|
3559
|
+
id: `${set}/rect-scattered/s${seed}`,
|
|
3560
|
+
set,
|
|
3561
|
+
family: "rect-scattered",
|
|
3562
|
+
seed,
|
|
3563
|
+
source: "made-up",
|
|
3564
|
+
generator: {
|
|
3565
|
+
name: "rect-scattered",
|
|
3566
|
+
version: 1,
|
|
3567
|
+
params: { count },
|
|
3568
|
+
seed
|
|
3569
|
+
},
|
|
3570
|
+
container: {
|
|
3571
|
+
w,
|
|
3572
|
+
h,
|
|
3573
|
+
fixed: "size"
|
|
3574
|
+
},
|
|
3575
|
+
obstacles,
|
|
3576
|
+
boxes,
|
|
3577
|
+
edges: []
|
|
3578
|
+
};
|
|
3579
|
+
return {
|
|
3580
|
+
...body,
|
|
3581
|
+
hash: documentHash(body)
|
|
3582
|
+
};
|
|
3583
|
+
}
|
|
3584
|
+
});
|
|
3585
|
+
export { CORPUS_DIMENSIONS, DEFAULT_FAMILY_BY, DEFAULT_RNG, DEFAULT_SHAPES, DEFAULT_TOLERANCE, EDGES_STEP, EDGE_DENSITY, EDGE_STEP_VERSION, EPS, FAMILY_ANY_KIND, JOINT_MEASURES, JOINT_MOVED_MIN, PACK_HARD_RULES, RECORD_EPS, RECORD_SCALE, RECT_SCATTERED, SHAPE_KINDS, SHAPE_KIND_IDS, SIDES, SPLIT_SCHEMA, STRATEGY_SPACE_SCHEMA, VERDICT_DRAWS, VERDICT_EXACT_CAP, VERDICT_EXACT_MAX, VERDICT_RUNS, VERDICT_SEED, applyPack, boundsOf, canonicalConfig, canonicalInput, canonicalJson, checkConfig, checkDeclared, checkInput, checkShapes, checkSpace, checkSplit, compareMeasure, composeFamily, configId, corpusHashOf, countObstacleHits, countOutside, countOverlaps, countPortsOffBorder, createFamilyRegistry, declaredClaimOf, declaredCounts, dimensionAt, dimensionUnit, documentHash, edgeStep, enumerateSpace, exercised, familyKind, familyOf, fnv1a32, grow, isShapeKind, itemShapeOf, joinConfigs, leaveOneSeedOut, levelsOf, marginSides, minEffectOf, mulberry32, outputGroups, outputHash, overlapOf, packHard, paramActive, paramLevels, paramProblem, paramValue, pickMoves, placedRect, portfolio, productSpace, randomInt, rectInRegion, regionHitsRect, resolveDimensions, selectBest, sha256Hex, shapeKindOf, shapeKindProblem, shapeKindsOf, shapesOf, sharedDimension, spaceShapeKinds, spaceSize, splitConfig, splitFileName, splitFolds, stepRandom, toLayoutRecord, toPackInput, translate, turnBox, turnMargin, turnPoint, turnPort, turnRegion, turnSide, verdict };
|
|
2604
3586
|
|
|
2605
3587
|
//# sourceMappingURL=index.js.map
|