@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/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
@@ -15,3 +15,4 @@ export * from "./portfolio.ts";
15
15
  export * from "./corpus.ts";
16
16
  export * from "./strategy-space.ts";
17
17
  export * from "./verdict.ts";
18
+ export * from "./families.ts";
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
- * Without them, the router places the port on its `side`. Without a side, the router picks the side.
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
- const rects: Rect[] = [];
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
  }
@@ -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
- if (keeps(space, choices)) out.push({ choices, base: false, weight: times(countOf(o), memberLists(base, o.members, limit), limit) });
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
- return {
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
- /** 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`. */
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