@mark1russell7/layout-contract 0.2.12 → 0.2.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package",
3
3
  "name": "@mark1russell7/layout-contract",
4
- "version": "0.2.12",
4
+ "version": "0.2.13",
5
5
  "description": "The shared contract of the packing and highways layout engines: the input and record formats, the turn rule, margins, tolerances and hard rules.",
6
6
  "keywords": [
7
7
  "layout",
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from "./geometry.ts";
2
+ export * from "./shapes.ts";
2
3
  export * from "./tolerance.ts";
3
4
  export * from "./input.ts";
4
5
  export * from "./output.ts";
package/src/input.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { Margin, Region, Side } from "./geometry.ts";
2
+ import type { ShapeKind } from "./shapes.ts";
2
3
 
3
4
  /**
4
5
  * A port of a box: a place where edges go into the box.
@@ -28,6 +29,11 @@ export interface LayoutBox {
28
29
  readonly group?: string;
29
30
  /** A region relative to the origin of the box. Without it, the box is the rectangle `w` by `h`. */
30
31
  readonly shape?: Region;
32
+ /**
33
+ * The shape kind of the box (`docs/SHAPES.md`). Without it, `shapeKindOf` gives the kind of `shape`, or `rect` without a shape.
34
+ * The kind `free` tells that the contract knows only the bounding box `w` by `h`.
35
+ */
36
+ readonly shapeKind?: ShapeKind;
31
37
  readonly ports?: readonly Port[];
32
38
  /** A ring around the box. No other box enters it. Only a route from a port of the box enters it, at a right angle. */
33
39
  readonly margin?: Margin;
@@ -92,6 +98,8 @@ export interface LayoutCell {
92
98
  readonly edgeShape?: "random" | "clustered" | "hubChain" | "longRange" | "tree" | "bundled";
93
99
  readonly groups?: "none" | "flat" | "nested";
94
100
  readonly ports?: "free" | "sides" | "fixed";
101
+ /** The shape kinds of the boxes: one kind, or `mixed` for more kinds. Without it, each box is a `rect`. `itemShapeOf` gives the value. */
102
+ readonly itemShape?: ShapeKind | "mixed";
95
103
  }
96
104
 
97
105
  /** An input of the corpus: the format `layout-input/1`. */
package/src/measures.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { SHAPE_KIND_IDS, type ShapeKind } from "./shapes.ts";
1
2
  import { RECORD_EPS, RECORD_SCALE } from "./tolerance.ts";
2
3
 
3
4
  /** The direction of a measure: the side of the better values. */
@@ -29,6 +30,8 @@ export interface MeasureSpec<Id extends string = string> {
29
30
  readonly minEffect?: number | { readonly rel: number };
30
31
  /** The role of the measure in the joint selector. */
31
32
  readonly role?: "guard" | "change-guard" | "diagnostic" | "time";
33
+ /** The shape kinds of the boxes that the measure supports. Without it, the measure supports only `rect`. `checkShapes` refuses the other kinds. */
34
+ readonly shapes?: readonly ShapeKind[];
32
35
  }
33
36
 
34
37
  /** The id of a joint measure. `docs/JOINT-MEASURES.md` gives the definitions. */
@@ -43,17 +46,25 @@ export type JointMeasureId =
43
46
  | "joint.proxy.rank"
44
47
  | "joint.ms";
45
48
 
46
- /** The specs of the joint measures, in the order of `docs/JOINT-MEASURES.md`. */
49
+ /** The shape kinds of a joint measure that reads only the bounding boxes, the centers and the positions of the boxes. */
50
+ const BOUNDS_ONLY = SHAPE_KIND_IDS;
51
+ /** The shape kinds of a joint measure that reads the area `w` by `h` of each box. */
52
+ const AREA: readonly ShapeKind[] = ["rect"];
53
+
54
+ /**
55
+ * The specs of the joint measures, in the order of `docs/JOINT-MEASURES.md`.
56
+ * `joint.fill` and `joint.straight.length` read the area of each box, so they support only `rect`. `docs/SHAPES.md` tells the reasons.
57
+ */
47
58
  export const JOINT_MEASURES: readonly MeasureSpec<JointMeasureId>[] = Object.freeze([
48
- { id: "joint.fill", minEffect: 0.001, unit: "0 to 1", direction: "higher", min: 0, max: 1, floor: 1, role: "guard" },
49
- { id: "joint.aspectError", minEffect: 0.01, unit: "natural log", direction: "lower", min: 0, max: Infinity, floor: 0, tol: 0.01, role: "guard" },
50
- { id: "joint.spill", minEffect: 0.001, unit: "0 to 1", direction: "lower", min: 0, max: 1, floor: 0, tol: 0.001, role: "guard" },
51
- { id: "joint.straight.crossings", minEffect: 0, unit: "count", direction: "lower", min: 0, max: Infinity, floor: 0, tol: 0, role: "guard" },
52
- { id: "joint.straight.length", minEffect: 0.01, unit: "S for each edge", direction: "lower", min: 0, max: Infinity, floor: 0, tol: 0.01, role: "guard" },
53
- { id: "joint.stability.mean", minEffect: 0.005, unit: "W", direction: "lower", min: 0, max: Infinity, floor: 0, tol: 0.005, role: "change-guard" },
54
- { id: "joint.stability.moved", minEffect: 0.02, unit: "0 to 1", direction: "lower", min: 0, max: 1, floor: 0, tol: 0.02, role: "change-guard" },
55
- { id: "joint.proxy.rank", unit: "-1 to 1", direction: "higher", min: -1, max: 1, role: "diagnostic" },
56
- { id: "joint.ms", unit: "milliseconds", direction: "lower", min: 0, max: Infinity, role: "time" },
59
+ { id: "joint.fill", minEffect: 0.001, unit: "0 to 1", direction: "higher", min: 0, max: 1, floor: 1, role: "guard", shapes: AREA },
60
+ { id: "joint.aspectError", minEffect: 0.01, unit: "natural log", direction: "lower", min: 0, max: Infinity, floor: 0, tol: 0.01, role: "guard", shapes: BOUNDS_ONLY },
61
+ { id: "joint.spill", minEffect: 0.001, unit: "0 to 1", direction: "lower", min: 0, max: 1, floor: 0, tol: 0.001, role: "guard", shapes: BOUNDS_ONLY },
62
+ { id: "joint.straight.crossings", minEffect: 0, unit: "count", direction: "lower", min: 0, max: Infinity, floor: 0, tol: 0, role: "guard", shapes: BOUNDS_ONLY },
63
+ { id: "joint.straight.length", minEffect: 0.01, unit: "S for each edge", direction: "lower", min: 0, max: Infinity, floor: 0, tol: 0.01, role: "guard", shapes: AREA },
64
+ { id: "joint.stability.mean", minEffect: 0.005, unit: "W", direction: "lower", min: 0, max: Infinity, floor: 0, tol: 0.005, role: "change-guard", shapes: BOUNDS_ONLY },
65
+ { id: "joint.stability.moved", minEffect: 0.02, unit: "0 to 1", direction: "lower", min: 0, max: 1, floor: 0, tol: 0.02, role: "change-guard", shapes: BOUNDS_ONLY },
66
+ { id: "joint.proxy.rank", unit: "-1 to 1", direction: "higher", min: -1, max: 1, role: "diagnostic", shapes: BOUNDS_ONLY },
67
+ { id: "joint.ms", unit: "milliseconds", direction: "lower", min: 0, max: Infinity, role: "time", shapes: BOUNDS_ONLY },
57
68
  ] as const);
58
69
 
59
70
  /** The threshold of `joint.stability.moved`, in widths `W` of the base. */
package/src/record.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { Tolerance } from "./tolerance.ts";
2
2
  import type { LayoutInput } from "./input.ts";
3
+ import { shapeKindsOf, type ShapeKind } from "./shapes.ts";
3
4
 
4
5
  /** The stage of a record: the packer, the router, or both together. */
5
6
  export type Stage = "pack" | "route" | "joint";
@@ -21,6 +22,8 @@ export interface LayoutRecord {
21
22
  readonly seed: number;
22
23
  readonly nodes: number;
23
24
  readonly edges: number;
25
+ /** The shape kinds of the boxes of the input, in the order of `SHAPE_KINDS`. Without it, each box is a `rect`. */
26
+ readonly shapes?: readonly ShapeKind[];
24
27
  /** The value of each measure, by its name: `pack.*`, `route.*` or `joint.*`. */
25
28
  readonly measures: Readonly<Record<string, number>>;
26
29
  /** The count of the breaks of each hard rule. A good run has 0 for each rule. */
@@ -67,10 +70,15 @@ export interface RunEnv {
67
70
  }
68
71
 
69
72
  /** The facts of a run that the input does not give. */
70
- export type RunFacts = Omit<LayoutRecord, "schema" | "input" | "inputHash" | "set" | "nodes" | "edges">;
73
+ export type RunFacts = Omit<LayoutRecord, "schema" | "input" | "inputHash" | "set" | "nodes" | "edges" | "shapes">;
71
74
 
72
- /** 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. */
75
+ /**
76
+ * 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.
77
+ * The input also gives `shapes`. An input with only `rect` boxes gets no `shapes`, so its record does not change.
78
+ */
73
79
  export function toLayoutRecord(input: LayoutInput, run: RunFacts): LayoutRecord {
80
+ const shapes = shapeKindsOf(input);
81
+ const onlyRect = shapes.length === 0 || (shapes.length === 1 && shapes[0] === "rect");
74
82
  return {
75
83
  schema: "layout-record/1",
76
84
  ...run,
@@ -79,5 +87,6 @@ export function toLayoutRecord(input: LayoutInput, run: RunFacts): LayoutRecord
79
87
  set: input.set ?? "",
80
88
  nodes: input.boxes.length,
81
89
  edges: input.edges?.length ?? 0,
90
+ ...(onlyRect ? {} : { shapes }),
82
91
  };
83
92
  }
package/src/rules.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { boundsOf, grow, overlapOf, type Rect, type Region } from "./geometry.ts";
2
2
  import type { LayoutBox, LayoutInput, Obstacle } from "./input.ts";
3
3
  import { applyPack, type PackOutput } from "./output.ts";
4
+ import { SHAPE_KIND_IDS, shapeKindProblem, type ShapeSupport } from "./shapes.ts";
4
5
  import { DEFAULT_TOLERANCE, EPS, RECORD_EPS } from "./tolerance.ts";
5
6
 
6
7
  /** This function gives the area of a placed box: its shape or its rectangle, at its position. A box without a position gives `undefined`. */
@@ -96,6 +97,18 @@ export interface PackHard {
96
97
  readonly complete: number;
97
98
  }
98
99
 
100
+ /**
101
+ * The hard rules of a pack, with the shape kinds that each rule supports.
102
+ * `no-overlap` and `in-container` test the bounding box of a shape. That count is exact only for a `rect`.
103
+ * `locked-kept` and `complete` read only the positions and the ids, so they support each kind.
104
+ */
105
+ export const PACK_HARD_RULES: readonly ShapeSupport<keyof PackHard>[] = Object.freeze([
106
+ { id: "no-overlap", shapes: ["rect"] },
107
+ { id: "in-container", shapes: ["rect"] },
108
+ { id: "locked-kept", shapes: SHAPE_KIND_IDS },
109
+ { id: "complete", shapes: SHAPE_KIND_IDS },
110
+ ] as const);
111
+
99
112
  /** This function tells if the inside of `region` and the inside of `rect` have a common point. */
100
113
  export function regionHitsRect(region: Region, rect: Rect): boolean {
101
114
  if (rect.w <= 0 || rect.h <= 0) return false;
@@ -314,6 +327,8 @@ export function checkInput(input: LayoutInput): readonly string[] {
314
327
  ids.add(box.id);
315
328
  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`);
316
329
  if (box.locked === true && (box.x === undefined || box.y === undefined)) problems.push(`box ${box.id}: a locked box has no position`);
330
+ const shape = shapeKindProblem(box);
331
+ if (shape !== undefined) problems.push(shape);
317
332
  }
318
333
  if (countPortsOffBorder(input.boxes) > 0) problems.push("a fixed port is not on the border of its box");
319
334
  for (const edge of input.edges ?? []) {
package/src/shapes.ts ADDED
@@ -0,0 +1,119 @@
1
+ // The shape kinds of the items (`docs/SHAPES.md`): the declared set, the kind of a box and the check of the measures and rules on a run.
2
+ import type { ClaimBreak } from "./corpus.ts";
3
+ import type { Region } from "./geometry.ts";
4
+ import type { LayoutBox, LayoutInput } from "./input.ts";
5
+
6
+ /**
7
+ * The kind of the shape of an item. A box without `shape` and without `shapeKind` is a `rect`.
8
+ * The set is open: a later version can add a kind. A reader treats an unknown kind as a kind that it does not support.
9
+ */
10
+ export type ShapeKind = "rect" | "polygon" | "circle" | "free";
11
+
12
+ /** The geometry of a shape kind that this package gives. A `false` value tells that the kind declares the operation, but this package does not supply it. */
13
+ export interface ShapeGeometry {
14
+ /** The bounding box: `boundsOf` and `placedRect`. */
15
+ readonly bounds: boolean;
16
+ /** The area of the shape. */
17
+ readonly area: boolean;
18
+ /** The test that the shape is inside a region: `rectInRegion` for a `rect`. */
19
+ readonly contains: boolean;
20
+ /** The test that two shapes overlap: `overlapOf` for two `rect` shapes. */
21
+ readonly overlap: boolean;
22
+ }
23
+
24
+ /** The facts of one shape kind: its fields, the kinds of `Region` that give it, and its geometry. */
25
+ export interface ShapeKindSpec {
26
+ readonly id: ShapeKind;
27
+ /** The fields of the outline, relative to the top-left corner of the box. A name with `?` is optional. */
28
+ readonly fields: readonly string[];
29
+ /** The kinds of `LayoutBox.shape` that agree with this kind. */
30
+ readonly regions: readonly Region["kind"][];
31
+ readonly geometry: ShapeGeometry;
32
+ }
33
+
34
+ /** The declared shape kinds, in their order. Only `rect` has all of its geometry. The other kinds have their bounding box only. */
35
+ export const SHAPE_KINDS: readonly ShapeKindSpec[] = Object.freeze([
36
+ { id: "rect", fields: ["w", "h"], regions: ["rect"], geometry: { bounds: true, area: true, contains: true, overlap: true } },
37
+ { id: "polygon", fields: ["points", "holes?"], regions: ["polygon"], geometry: { bounds: true, area: false, contains: false, overlap: false } },
38
+ { id: "circle", fields: ["cx", "cy", "r"], regions: ["circle"], geometry: { bounds: true, area: false, contains: false, overlap: false } },
39
+ { id: "free", fields: ["w", "h", "shape?"], regions: ["rect", "polygon", "circle", "ellipse"], geometry: { bounds: true, area: false, contains: false, overlap: false } },
40
+ ] as const);
41
+
42
+ /** The ids of `SHAPE_KINDS`, in their order. */
43
+ export const SHAPE_KIND_IDS: readonly ShapeKind[] = Object.freeze(SHAPE_KINDS.map((k) => k.id));
44
+
45
+ /** The shape kinds of a measure or a rule without `shapes`. The contract measured only rectangles before L46. */
46
+ export const DEFAULT_SHAPES: readonly ShapeKind[] = Object.freeze(["rect"] as const);
47
+
48
+ /** A measure or a rule, with the shape kinds that it supports. Without `shapes`, it supports `DEFAULT_SHAPES`. */
49
+ export interface ShapeSupport<Id extends string = string> {
50
+ readonly id: Id;
51
+ readonly shapes?: readonly ShapeKind[];
52
+ }
53
+
54
+ const REGION_KIND: Readonly<Record<Region["kind"], ShapeKind>> = { rect: "rect", polygon: "polygon", circle: "circle", ellipse: "free" };
55
+
56
+ /** This function tells if a value is a declared shape kind. */
57
+ export function isShapeKind(value: unknown): value is ShapeKind {
58
+ return typeof value === "string" && (SHAPE_KIND_IDS as readonly string[]).includes(value);
59
+ }
60
+
61
+ /**
62
+ * This function gives the shape kind of a box. A box with `shapeKind` has that kind.
63
+ * Without it, the kind comes from `shape`: no shape is a `rect`, and an ellipse is `free`.
64
+ */
65
+ export function shapeKindOf(box: LayoutBox): ShapeKind {
66
+ if (box.shapeKind !== undefined) return box.shapeKind;
67
+ return box.shape === undefined ? "rect" : REGION_KIND[box.shape.kind];
68
+ }
69
+
70
+ /** 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. */
71
+ export function shapeKindsOf(input: Pick<LayoutInput, "boxes">): ShapeKind[] {
72
+ const seen = new Set(input.boxes.map(shapeKindOf));
73
+ return SHAPE_KIND_IDS.filter((k) => seen.has(k));
74
+ }
75
+
76
+ /** This function gives the value of the cell dimension `itemShape` of an input: its one kind, `mixed` for more kinds, or `rect` without boxes. */
77
+ export function itemShapeOf(input: Pick<LayoutInput, "boxes">): ShapeKind | "mixed" {
78
+ const kinds = shapeKindsOf(input);
79
+ return kinds.length === 0 ? "rect" : kinds.length === 1 ? kinds[0]! : "mixed";
80
+ }
81
+
82
+ /** This function gives the problem of the shape kind of a box, or `undefined` when `shapeKind` and `shape` agree. */
83
+ export function shapeKindProblem(box: LayoutBox): string | undefined {
84
+ const k: unknown = box.shapeKind;
85
+ if (k === undefined) return undefined;
86
+ const spec = SHAPE_KINDS.find((s) => s.id === k);
87
+ if (spec === undefined) return `box ${box.id}: the shape kind ${String(k)} is not a declared kind`;
88
+ if (box.shape === undefined) return k === "rect" || k === "free" ? undefined : `box ${box.id}: the shape kind ${k} has no shape`;
89
+ 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}`;
90
+ return undefined;
91
+ }
92
+
93
+ /** This function gives the shape kinds that a measure or a rule supports. */
94
+ export function shapesOf(use: Pick<ShapeSupport, "shapes">): readonly ShapeKind[] {
95
+ return use.shapes ?? DEFAULT_SHAPES;
96
+ }
97
+
98
+ /**
99
+ * 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.
100
+ * Each break has the claim `shape`, the id of the input, and the first 5 ids of the boxes of the kind.
101
+ */
102
+ export function checkShapes(input: Pick<LayoutInput, "id" | "boxes">, uses: readonly ShapeSupport[]): ClaimBreak[] {
103
+ const kinds = shapeKindsOf(input);
104
+ const out: ClaimBreak[] = [];
105
+ for (const use of uses) {
106
+ const shapes = shapesOf(use);
107
+ for (const kind of kinds) {
108
+ if (shapes.includes(kind)) continue;
109
+ const ids = input.boxes.filter((b) => shapeKindOf(b) === kind).map((b) => b.id);
110
+ const shown = ids.slice(0, 5).join(", ") + (ids.length > 5 ? `, and ${ids.length - 5} more` : "");
111
+ out.push({
112
+ claim: "shape",
113
+ detail: `${use.id} supports only the shape kinds ${shapes.join(", ") || "(none)"}, but the input ${input.id} has ${kind} boxes: ${shown}`,
114
+ inputs: [input.id],
115
+ });
116
+ }
117
+ }
118
+ return out;
119
+ }
@@ -2,6 +2,7 @@
2
2
  import type { ClaimBreak } from "./corpus.ts";
3
3
  import { canonicalJson, sha256Hex } from "./hash.ts";
4
4
  import { stepRandom } from "./random.ts";
5
+ import { isShapeKind, SHAPE_KIND_IDS, type ShapeKind } from "./shapes.ts";
5
6
 
6
7
  /** The schema of a strategy space. */
7
8
  export const STRATEGY_SPACE_SCHEMA = "strategy-space/1";
@@ -47,11 +48,25 @@ export interface MemberRule {
47
48
  readonly ordered: boolean;
48
49
  }
49
50
 
51
+ /** The inputs that an option supports. A missing field supports each value. */
52
+ export interface OptionSupports {
53
+ /** The shape kinds of the boxes (`docs/SHAPES.md`). Without it, the option supports each kind. */
54
+ readonly shape?: readonly ShapeKind[];
55
+ }
56
+
50
57
  /** One implementation of a stage. An option with `members` is a meta option: it chooses between whole configs, or wraps them. */
51
58
  export interface StageOption {
52
59
  readonly id: string;
53
60
  readonly params: readonly SpaceParam[];
54
61
  readonly members?: MemberRule;
62
+ /** The inputs that the option supports. It describes the option, so it is not part of a config or of its id. */
63
+ readonly supports?: OptionSupports;
64
+ }
65
+
66
+ /** The shape kinds of the inputs of a check, a count or an enumeration. Without `shapes`, each option counts. */
67
+ export interface ShapeFilter {
68
+ /** The shape kinds of the input. Only the options that support each of these kinds count. */
69
+ readonly shapes?: readonly ShapeKind[];
55
70
  }
56
71
 
57
72
  /** One stage of the pipeline of an engine. A config of an `optional` stage can have no option. */
@@ -113,7 +128,7 @@ export interface SpaceSize {
113
128
  }
114
129
 
115
130
  /** The options of `spaceSize`. */
116
- export interface SpaceSizeOptions {
131
+ export interface SpaceSizeOptions extends ShapeFilter {
117
132
  /** The count of the levels of each numeric param, a whole number of 1 or more. */
118
133
  readonly levels: number;
119
134
  /** The largest meta count that `spaceSize` gives. The default is `10n ** 18n`. */
@@ -121,7 +136,7 @@ export interface SpaceSizeOptions {
121
136
  }
122
137
 
123
138
  /** The options of a grid. The grid gives only the configs without a meta option. */
124
- export interface GridOptions {
139
+ export interface GridOptions extends ShapeFilter {
125
140
  readonly mode: "grid";
126
141
  readonly levels: number;
127
142
  /** The largest count of configs. A larger grid causes a `RangeError`. The default is 100000. */
@@ -129,7 +144,7 @@ export interface GridOptions {
129
144
  }
130
145
 
131
146
  /** The options of a sample. */
132
- export interface SampleOptions {
147
+ export interface SampleOptions extends ShapeFilter {
133
148
  readonly mode: "sample";
134
149
  /** The count of the configs, a whole number of 0 or more. */
135
150
  readonly n: number;
@@ -271,6 +286,7 @@ function optionProblems(stage: SpaceStage, o: StageOption): Problem[] {
271
286
  if (problem !== undefined) out.push(["when", `${pat} has a when value that is not a value of ${p.when.param}: ${problem}`]);
272
287
  }
273
288
  });
289
+ if (o.supports !== undefined) out.push(...supportsProblems(o.supports, at));
274
290
  if (o.members !== undefined) {
275
291
  const { min, max, ordered } = o.members;
276
292
  if (!Number.isInteger(min) || !Number.isInteger(max) || min < 1 || min > max || typeof ordered !== "boolean")
@@ -279,6 +295,24 @@ function optionProblems(stage: SpaceStage, o: StageOption): Problem[] {
279
295
  return out;
280
296
  }
281
297
 
298
+ /** This function gives the problems of the `supports` of an option. */
299
+ function supportsProblems(supports: unknown, at: string): Problem[] {
300
+ if (!isObject(supports)) return [["shape", `the supports of ${at} are not an object`]];
301
+ const shape = supports["shape"];
302
+ if (shape === undefined) return [];
303
+ if (!Array.isArray(shape) || shape.length === 0) return [["shape", `${at} supports no shape kind. Remove shape to support each kind`]];
304
+ const out: Problem[] = [];
305
+ shape.forEach((k: unknown, i) => {
306
+ if (!isShapeKind(k)) out.push(["shape", `${at} supports the shape kind ${show(k)}, which is not one of ${SHAPE_KIND_IDS.join(", ")}`]);
307
+ else if (shape.indexOf(k) !== i) out.push(["duplicate", `${at} has the shape kind ${k} two times`]);
308
+ });
309
+ return out;
310
+ }
311
+
312
+ /** This function tells if an option supports each shape kind of the list. */
313
+ const supportsShapes = (o: StageOption, shapes: readonly ShapeKind[] | undefined): boolean =>
314
+ shapes === undefined || o.supports?.shape === undefined || shapes.every((k) => o.supports!.shape!.includes(k));
315
+
282
316
  /** This function gives the problems of the params of one stage choice. */
283
317
  function choiceParamProblems(o: StageOption, params: unknown, at: string): Problem[] {
284
318
  if (params !== undefined && !isObject(params)) return [["config", `the params of ${at} are not an object`]];
@@ -301,7 +335,7 @@ function choiceParamProblems(o: StageOption, params: unknown, at: string): Probl
301
335
  }
302
336
 
303
337
  /** This function gives the problems of a config of a space. A member of a meta option has `member: true`. */
304
- function configProblems(space: StrategySpace, config: unknown, member: boolean): Problem[] {
338
+ function configProblems(space: StrategySpace, config: unknown, member: boolean, shapes?: readonly ShapeKind[]): Problem[] {
305
339
  if (!isObject(config) || !isObject(config["stages"])) return [["config", "the config is not an object with stages"]];
306
340
  const out: Problem[] = [];
307
341
  const stages = config["stages"];
@@ -339,6 +373,10 @@ function configProblems(space: StrategySpace, config: unknown, member: boolean):
339
373
  }
340
374
  chosen.set(s.name, o.id);
341
375
  const at = `the option ${s.name}.${o.id}`;
376
+ if (!supportsShapes(o, shapes)) {
377
+ const missing = shapes!.filter((k) => !o.supports!.shape!.includes(k));
378
+ out.push(["shape", `${at} supports the shape kinds ${o.supports!.shape!.join(", ")}, not ${missing.join(", ")}`]);
379
+ }
342
380
  if (meta !== undefined && 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`]);
343
381
  out.push(...choiceParamProblems(o, c["params"], at));
344
382
  if (o.members === undefined) {
@@ -349,20 +387,20 @@ function configProblems(space: StrategySpace, config: unknown, member: boolean):
349
387
  out.push(["depth", `a member has the meta option ${s.name}.${o.id}. The depth of a meta config is 1`]);
350
388
  continue;
351
389
  }
352
- out.push(...memberProblems(partOf(space, s.name).space, o.members, c["members"], at));
390
+ out.push(...memberProblems(partOf(space, s.name).space, o.members, c["members"], at, shapes));
353
391
  }
354
392
  for (const c of space.constraints ?? []) if (!holds(c, chosen)) out.push(["constraint", `the config breaks the constraint ${describe(c)}`]);
355
393
  return out;
356
394
  }
357
395
 
358
396
  /** This function gives the problems of the members of a meta option. `part` is the space of the members. */
359
- function memberProblems(part: StrategySpace, rule: MemberRule, members: unknown, at: string): Problem[] {
397
+ function memberProblems(part: StrategySpace, rule: MemberRule, members: unknown, at: string, shapes?: readonly ShapeKind[]): Problem[] {
360
398
  if (!Array.isArray(members)) return [["members", `${at} is a meta option without a list of members`]];
361
399
  const out: Problem[] = [];
362
400
  if (members.length < rule.min || members.length > rule.max) out.push(["members", `${at} has ${members.length} members, not ${rule.min} to ${rule.max}`]);
363
401
  const ids = new Map<string, number>();
364
402
  members.forEach((m: unknown, k) => {
365
- const inner = configProblems(part, m, true);
403
+ const inner = configProblems(part, m, true, shapes);
366
404
  for (const [claim, detail] of inner) out.push([claim, `the member ${k} of ${at}: ${detail}`]);
367
405
  if (inner.length > 0) return;
368
406
  const id = idOf(canonicalOf(part, m as StrategyConfig, true));
@@ -377,9 +415,10 @@ const toBreaks = (problems: readonly Problem[], inputs: readonly string[] = []):
377
415
  /**
378
416
  * This function checks one config of a space. It gives one break for each problem. An empty list tells that the config is good.
379
417
  * The `claim` of a break is the kind of the problem, for example `missing`, `unknown`, `inactive`, `param`, `constraint`, `members` or `depth`.
418
+ * 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.
380
419
  */
381
- export function checkConfig(space: StrategySpace, config: StrategyConfig): ClaimBreak[] {
382
- return toBreaks(configProblems(space, config, false));
420
+ export function checkConfig(space: StrategySpace, config: StrategyConfig, options: ShapeFilter = {}): ClaimBreak[] {
421
+ return toBreaks(configProblems(space, config, false, options.shapes));
383
422
  }
384
423
 
385
424
  /**
@@ -646,14 +685,40 @@ function combos(space: StrategySpace, levels: number, meta: boolean, limit: bigi
646
685
  /**
647
686
  * This function gives the count of the grid points of a space, with `levels` levels for each numeric param.
648
687
  * It counts only the active params and the combinations that keep the constraints. It gives the meta configs apart.
688
+ * With `shapes`, it counts only the options that support each kind of the list.
649
689
  */
650
690
  export function spaceSize(space: StrategySpace, options: SpaceSizeOptions): SpaceSize {
651
691
  assertSpace(space);
652
692
  assertWhole(options.levels, "levels", 1);
653
- const limit = options.maxMeta ?? 10n ** 18n;
693
+ assertShapes(options.shapes);
694
+ return sizeOf(restrict(space, options.shapes), options.levels, options.maxMeta ?? 10n ** 18n);
695
+ }
696
+
697
+ /**
698
+ * This function gives the space with only the options that support each shape kind of `shapes`. Without `shapes`, it gives the same space.
699
+ * A stage can lose all of its options. Then the space has no config with that stage.
700
+ */
701
+ function restrict(space: StrategySpace, shapes: readonly ShapeKind[] | undefined): StrategySpace {
702
+ if (shapes === undefined) return space;
703
+ return {
704
+ ...space,
705
+ stages: space.stages.map((s) => ({ ...s, options: s.options.filter((o) => supportsShapes(o, shapes)) })),
706
+ ...(space.parts === undefined ? {} : { parts: space.parts.map((p) => restrict(p, shapes)) }),
707
+ };
708
+ }
709
+
710
+ /** This function throws a `RangeError` for a list of shape kinds with a kind that is not declared. */
711
+ function assertShapes(shapes: readonly ShapeKind[] | undefined): void {
712
+ if (shapes === undefined) return;
713
+ if (!Array.isArray(shapes)) throw new RangeError("The option shapes is not a list of shape kinds.");
714
+ for (const k of shapes) if (!isShapeKind(k)) throw new RangeError(`The shape kind ${show(k)} is not one of ${SHAPE_KIND_IDS.join(", ")}.`);
715
+ }
716
+
717
+ /** This function gives the count of the grid points of a space that is known to be good. */
718
+ function sizeOf(space: StrategySpace, levels: number, limit: bigint): SpaceSize {
654
719
  let base = 0n;
655
720
  let meta: bigint | null = 0n;
656
- for (const c of combos(space, options.levels, true, limit)) {
721
+ for (const c of combos(space, levels, true, limit)) {
657
722
  if (c.base) base += c.weight!;
658
723
  else meta = meta === null || c.weight === null || meta + c.weight > limit ? null : meta + c.weight;
659
724
  }
@@ -674,11 +739,12 @@ function* product<T>(lists: readonly (readonly T[])[], i = 0, acc: T[] = []): Ge
674
739
  }
675
740
 
676
741
  /** This function gives the configs of the grid of a space, without meta options. */
677
- function gridConfigs(space: StrategySpace, levels: number, limit: number): StrategyConfig[] {
678
- const size = spaceSize(space, { levels, maxMeta: 0n }).base;
742
+ function gridConfigs(space: StrategySpace, levels: number, limit: number, shapes: readonly ShapeKind[] | undefined): StrategyConfig[] {
743
+ const kept = restrict(space, shapes);
744
+ const size = sizeOf(kept, levels, 0n).base;
679
745
  if (size > BigInt(limit)) throw new RangeError(`The grid of the space ${space.engine} has ${size} configs, more than the limit ${limit}.`);
680
746
  const out: StrategyConfig[] = [];
681
- for (const c of combos(space, levels, false, 0n)) {
747
+ for (const c of combos(kept, levels, false, 0n)) {
682
748
  const names = [...c.choices.keys()];
683
749
  const lists = names.map((k) => {
684
750
  const o = c.choices.get(k)!;
@@ -724,6 +790,7 @@ function sampleConfigs(space: StrategySpace, n: number, seed: number, meta: bool
724
790
  const lists = stages.map((s) => {
725
791
  const list: (StageOption | null)[] = s.options.filter((o) => meta || o.members === undefined);
726
792
  if (s.optional) list.push(null);
793
+ 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.`);
727
794
  return list;
728
795
  });
729
796
  const points: Map<string, StageOption | null>[] = [];
@@ -794,12 +861,14 @@ function sampleConfigs(space: StrategySpace, n: number, seed: number, meta: bool
794
861
  * - 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.
795
862
  * - 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.
796
863
  * - A sample draws again when a point breaks a constraint. After `maxTries` draws, it throws an `Error`. Two sampled configs can be equal.
864
+ * - With `shapes`, only the options that support each kind of the list count. Without `shapes`, the result does not change.
797
865
  */
798
866
  export function enumerateSpace(space: StrategySpace, options: EnumerateOptions): StrategyConfig[] {
799
867
  assertSpace(space);
868
+ assertShapes(options.shapes);
800
869
  if (options.mode === "grid") {
801
870
  assertWhole(options.levels, "levels", 1);
802
- return gridConfigs(space, options.levels, options.limit ?? 100_000);
871
+ return gridConfigs(space, options.levels, options.limit ?? 100_000, options.shapes);
803
872
  }
804
873
  if (options.mode !== "sample") throw new RangeError(`The mode ${String((options as { mode?: unknown }).mode)} is not grid or sample.`);
805
874
  assertWhole(options.n, "n", 0);
@@ -807,7 +876,16 @@ export function enumerateSpace(space: StrategySpace, options: EnumerateOptions):
807
876
  const meta = options.meta ?? "skip";
808
877
  if (meta !== "skip" && meta !== "sample") throw new RangeError(`The option meta is ${String(meta)}, not skip or sample.`);
809
878
  if (options.n === 0) return [];
810
- return sampleConfigs(space, options.n, options.seed, meta === "sample", options.maxTries ?? Math.max(1000, 100 * options.n));
879
+ return sampleConfigs(restrict(space, options.shapes), options.n, options.seed, meta === "sample", options.maxTries ?? Math.max(1000, 100 * options.n));
880
+ }
881
+
882
+ /**
883
+ * 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.
884
+ * A space accepts a kind when it has at least one config without a meta option on that kind.
885
+ */
886
+ export function spaceShapeKinds(space: StrategySpace): ShapeKind[] {
887
+ assertSpace(space);
888
+ return SHAPE_KIND_IDS.filter((k) => sizeOf(restrict(space, [k]), 1, 0n).base > 0n);
811
889
  }
812
890
 
813
891
  /**