@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/src/strategy-space.ts
CHANGED
|
@@ -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";
|
|
@@ -45,6 +46,18 @@ export interface MemberRule {
|
|
|
45
46
|
readonly min: number;
|
|
46
47
|
readonly max: number;
|
|
47
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[])[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The inputs that an option supports. A missing field supports each value. */
|
|
58
|
+
export interface OptionSupports {
|
|
59
|
+
/** The shape kinds of the boxes (`docs/SHAPES.md`). Without it, the option supports each kind. */
|
|
60
|
+
readonly shape?: readonly ShapeKind[];
|
|
48
61
|
}
|
|
49
62
|
|
|
50
63
|
/** One implementation of a stage. An option with `members` is a meta option: it chooses between whole configs, or wraps them. */
|
|
@@ -52,6 +65,14 @@ export interface StageOption {
|
|
|
52
65
|
readonly id: string;
|
|
53
66
|
readonly params: readonly SpaceParam[];
|
|
54
67
|
readonly members?: MemberRule;
|
|
68
|
+
/** The inputs that the option supports. It describes the option, so it is not part of a config or of its id. */
|
|
69
|
+
readonly supports?: OptionSupports;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** The shape kinds of the inputs of a check, a count or an enumeration. Without `shapes`, each option counts. */
|
|
73
|
+
export interface ShapeFilter {
|
|
74
|
+
/** The shape kinds of the input. Only the options that support each of these kinds count. */
|
|
75
|
+
readonly shapes?: readonly ShapeKind[];
|
|
55
76
|
}
|
|
56
77
|
|
|
57
78
|
/** One stage of the pipeline of an engine. A config of an `optional` stage can have no option. */
|
|
@@ -113,7 +134,7 @@ export interface SpaceSize {
|
|
|
113
134
|
}
|
|
114
135
|
|
|
115
136
|
/** The options of `spaceSize`. */
|
|
116
|
-
export interface SpaceSizeOptions {
|
|
137
|
+
export interface SpaceSizeOptions extends ShapeFilter {
|
|
117
138
|
/** The count of the levels of each numeric param, a whole number of 1 or more. */
|
|
118
139
|
readonly levels: number;
|
|
119
140
|
/** The largest meta count that `spaceSize` gives. The default is `10n ** 18n`. */
|
|
@@ -121,7 +142,7 @@ export interface SpaceSizeOptions {
|
|
|
121
142
|
}
|
|
122
143
|
|
|
123
144
|
/** The options of a grid. The grid gives only the configs without a meta option. */
|
|
124
|
-
export interface GridOptions {
|
|
145
|
+
export interface GridOptions extends ShapeFilter {
|
|
125
146
|
readonly mode: "grid";
|
|
126
147
|
readonly levels: number;
|
|
127
148
|
/** The largest count of configs. A larger grid causes a `RangeError`. The default is 100000. */
|
|
@@ -129,12 +150,12 @@ export interface GridOptions {
|
|
|
129
150
|
}
|
|
130
151
|
|
|
131
152
|
/** The options of a sample. */
|
|
132
|
-
export interface SampleOptions {
|
|
153
|
+
export interface SampleOptions extends ShapeFilter {
|
|
133
154
|
readonly mode: "sample";
|
|
134
155
|
/** The count of the configs, a whole number of 0 or more. */
|
|
135
156
|
readonly n: number;
|
|
136
157
|
readonly seed: number;
|
|
137
|
-
/** 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`. */
|
|
138
159
|
readonly meta?: "skip" | "sample";
|
|
139
160
|
/** The largest count of option draws. The default is the larger value of 1000 and `100 * n`. */
|
|
140
161
|
readonly maxTries?: number;
|
|
@@ -189,6 +210,21 @@ function isActive(p: SpaceParam, values: Values): boolean {
|
|
|
189
210
|
return p.when.in.some((x) => x === v);
|
|
190
211
|
}
|
|
191
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
|
+
|
|
192
228
|
/** This function gives the part of a space that has a stage, and the prefix of the names of its stages. */
|
|
193
229
|
function partOf(space: StrategySpace, stage: string): { readonly space: StrategySpace; readonly prefix: string } {
|
|
194
230
|
for (const part of space.parts ?? []) {
|
|
@@ -271,14 +307,36 @@ function optionProblems(stage: SpaceStage, o: StageOption): Problem[] {
|
|
|
271
307
|
if (problem !== undefined) out.push(["when", `${pat} has a when value that is not a value of ${p.when.param}: ${problem}`]);
|
|
272
308
|
}
|
|
273
309
|
});
|
|
310
|
+
if (o.supports !== undefined) out.push(...supportsProblems(o.supports, at));
|
|
274
311
|
if (o.members !== undefined) {
|
|
275
312
|
const { min, max, ordered } = o.members;
|
|
276
313
|
if (!Number.isInteger(min) || !Number.isInteger(max) || min < 1 || min > max || typeof ordered !== "boolean")
|
|
277
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`]);
|
|
278
318
|
}
|
|
279
319
|
return out;
|
|
280
320
|
}
|
|
281
321
|
|
|
322
|
+
/** This function gives the problems of the `supports` of an option. */
|
|
323
|
+
function supportsProblems(supports: unknown, at: string): Problem[] {
|
|
324
|
+
if (!isObject(supports)) return [["shape", `the supports of ${at} are not an object`]];
|
|
325
|
+
const shape = supports["shape"];
|
|
326
|
+
if (shape === undefined) return [];
|
|
327
|
+
if (!Array.isArray(shape) || shape.length === 0) return [["shape", `${at} supports no shape kind. Remove shape to support each kind`]];
|
|
328
|
+
const out: Problem[] = [];
|
|
329
|
+
shape.forEach((k: unknown, i) => {
|
|
330
|
+
if (!isShapeKind(k)) out.push(["shape", `${at} supports the shape kind ${show(k)}, which is not one of ${SHAPE_KIND_IDS.join(", ")}`]);
|
|
331
|
+
else if (shape.indexOf(k) !== i) out.push(["duplicate", `${at} has the shape kind ${k} two times`]);
|
|
332
|
+
});
|
|
333
|
+
return out;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** This function tells if an option supports each shape kind of the list. */
|
|
337
|
+
const supportsShapes = (o: StageOption, shapes: readonly ShapeKind[] | undefined): boolean =>
|
|
338
|
+
shapes === undefined || o.supports?.shape === undefined || shapes.every((k) => o.supports!.shape!.includes(k));
|
|
339
|
+
|
|
282
340
|
/** This function gives the problems of the params of one stage choice. */
|
|
283
341
|
function choiceParamProblems(o: StageOption, params: unknown, at: string): Problem[] {
|
|
284
342
|
if (params !== undefined && !isObject(params)) return [["config", `the params of ${at} are not an object`]];
|
|
@@ -301,7 +359,7 @@ function choiceParamProblems(o: StageOption, params: unknown, at: string): Probl
|
|
|
301
359
|
}
|
|
302
360
|
|
|
303
361
|
/** 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[] {
|
|
362
|
+
function configProblems(space: StrategySpace, config: unknown, member: boolean, shapes?: readonly ShapeKind[]): Problem[] {
|
|
305
363
|
if (!isObject(config) || !isObject(config["stages"])) return [["config", "the config is not an object with stages"]];
|
|
306
364
|
const out: Problem[] = [];
|
|
307
365
|
const stages = config["stages"];
|
|
@@ -339,6 +397,10 @@ function configProblems(space: StrategySpace, config: unknown, member: boolean):
|
|
|
339
397
|
}
|
|
340
398
|
chosen.set(s.name, o.id);
|
|
341
399
|
const at = `the option ${s.name}.${o.id}`;
|
|
400
|
+
if (!supportsShapes(o, shapes)) {
|
|
401
|
+
const missing = shapes!.filter((k) => !o.supports!.shape!.includes(k));
|
|
402
|
+
out.push(["shape", `${at} supports the shape kinds ${o.supports!.shape!.join(", ")}, not ${missing.join(", ")}`]);
|
|
403
|
+
}
|
|
342
404
|
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
405
|
out.push(...choiceParamProblems(o, c["params"], at));
|
|
344
406
|
if (o.members === undefined) {
|
|
@@ -349,42 +411,77 @@ function configProblems(space: StrategySpace, config: unknown, member: boolean):
|
|
|
349
411
|
out.push(["depth", `a member has the meta option ${s.name}.${o.id}. The depth of a meta config is 1`]);
|
|
350
412
|
continue;
|
|
351
413
|
}
|
|
352
|
-
out.push(...memberProblems(partOf(space, s.name).space, o.members, c["members"], at));
|
|
414
|
+
out.push(...memberProblems(partOf(space, s.name).space, o.members, c["members"], at, shapes));
|
|
353
415
|
}
|
|
354
416
|
for (const c of space.constraints ?? []) if (!holds(c, chosen)) out.push(["constraint", `the config breaks the constraint ${describe(c)}`]);
|
|
355
417
|
return out;
|
|
356
418
|
}
|
|
357
419
|
|
|
358
420
|
/** 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[] {
|
|
421
|
+
function memberProblems(part: StrategySpace, rule: MemberRule, members: unknown, at: string, shapes?: readonly ShapeKind[]): Problem[] {
|
|
360
422
|
if (!Array.isArray(members)) return [["members", `${at} is a meta option without a list of members`]];
|
|
361
423
|
const out: Problem[] = [];
|
|
362
424
|
if (members.length < rule.min || members.length > rule.max) out.push(["members", `${at} has ${members.length} members, not ${rule.min} to ${rule.max}`]);
|
|
363
425
|
const ids = new Map<string, number>();
|
|
364
426
|
members.forEach((m: unknown, k) => {
|
|
365
|
-
const inner = configProblems(part, m, true);
|
|
427
|
+
const inner = configProblems(part, m, true, shapes);
|
|
366
428
|
for (const [claim, detail] of inner) out.push([claim, `the member ${k} of ${at}: ${detail}`]);
|
|
367
429
|
if (inner.length > 0) return;
|
|
368
430
|
const id = idOf(canonicalOf(part, m as StrategyConfig, true));
|
|
369
431
|
if (ids.has(id)) out.push(["duplicate", `the members ${ids.get(id)} and ${k} of ${at} are the same config ${id}`]);
|
|
370
432
|
else ids.set(id, k);
|
|
371
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
|
+
});
|
|
372
462
|
return out;
|
|
373
463
|
}
|
|
374
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
|
+
|
|
375
468
|
const toBreaks = (problems: readonly Problem[], inputs: readonly string[] = []): ClaimBreak[] => problems.map(([claim, detail]) => ({ claim, detail, inputs }));
|
|
376
469
|
|
|
377
470
|
/**
|
|
378
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.
|
|
379
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`.
|
|
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.
|
|
380
476
|
*/
|
|
381
|
-
export function checkConfig(space: StrategySpace, config: StrategyConfig): ClaimBreak[] {
|
|
382
|
-
return toBreaks(configProblems(space, config, false));
|
|
477
|
+
export function checkConfig(space: StrategySpace, config: StrategyConfig, options: ShapeFilter = {}): ClaimBreak[] {
|
|
478
|
+
return toBreaks(configProblems(space, config, false, options.shapes));
|
|
383
479
|
}
|
|
384
480
|
|
|
385
481
|
/**
|
|
386
482
|
* This function checks a space. It gives one break for each problem. An empty list tells that the space is good.
|
|
387
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.
|
|
388
485
|
* The `inputs` of a break of a named config have the name of the config.
|
|
389
486
|
*/
|
|
390
487
|
export function checkSpace(space: StrategySpace): ClaimBreak[] {
|
|
@@ -423,6 +520,9 @@ export function checkSpace(space: StrategySpace): ClaimBreak[] {
|
|
|
423
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`]);
|
|
424
521
|
const breaks = toBreaks(out);
|
|
425
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;
|
|
426
526
|
for (const [name, config] of Object.entries(space.named ?? {}))
|
|
427
527
|
breaks.push(...toBreaks(configProblems(space, config, false).map(([c, d]) => [c, `the named config ${name}: ${d}`] as const), [name]));
|
|
428
528
|
return breaks;
|
|
@@ -569,7 +669,7 @@ interface Combo {
|
|
|
569
669
|
const optionIds = (choices: Choices): Map<string, string | null> => new Map([...choices].map(([k, o]) => [k, o?.id ?? null]));
|
|
570
670
|
const keeps = (space: StrategySpace, choices: Choices): boolean => (space.constraints ?? []).every((c) => holds(c, optionIds(choices)));
|
|
571
671
|
|
|
572
|
-
/** 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`. */
|
|
573
673
|
function memberLists(base: bigint, rule: MemberRule, limit: bigint): bigint | null {
|
|
574
674
|
let sum = 0n;
|
|
575
675
|
for (let m = rule.min; m <= rule.max; m++) {
|
|
@@ -638,7 +738,8 @@ function combos(space: StrategySpace, levels: number, meta: boolean, limit: bigi
|
|
|
638
738
|
for (const o of s.options) {
|
|
639
739
|
if (o.members === undefined) continue;
|
|
640
740
|
const choices = new Map<string, StageOption | null>(stages.map((x) => [x.name, x === s ? o : null]));
|
|
641
|
-
|
|
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) });
|
|
642
743
|
}
|
|
643
744
|
return out;
|
|
644
745
|
}
|
|
@@ -646,14 +747,49 @@ function combos(space: StrategySpace, levels: number, meta: boolean, limit: bigi
|
|
|
646
747
|
/**
|
|
647
748
|
* This function gives the count of the grid points of a space, with `levels` levels for each numeric param.
|
|
648
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.
|
|
751
|
+
* With `shapes`, it counts only the options that support each kind of the list.
|
|
649
752
|
*/
|
|
650
753
|
export function spaceSize(space: StrategySpace, options: SpaceSizeOptions): SpaceSize {
|
|
651
754
|
assertSpace(space);
|
|
652
755
|
assertWhole(options.levels, "levels", 1);
|
|
653
|
-
|
|
756
|
+
assertShapes(options.shapes);
|
|
757
|
+
return sizeOf(restrict(space, options.shapes), options.levels, options.maxMeta ?? 10n ** 18n);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
/**
|
|
761
|
+
* This function gives the space with only the options that support each shape kind of `shapes`. Without `shapes`, it gives the same space.
|
|
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.
|
|
764
|
+
*/
|
|
765
|
+
function restrict(space: StrategySpace, shapes: readonly ShapeKind[] | undefined): StrategySpace {
|
|
766
|
+
if (shapes === undefined) return space;
|
|
767
|
+
const kept: StrategySpace = {
|
|
768
|
+
...space,
|
|
769
|
+
stages: space.stages.map((s) => ({ ...s, options: s.options.filter((o) => supportsShapes(o, shapes)) })),
|
|
770
|
+
...(space.parts === undefined ? {} : { parts: space.parts.map((p) => restrict(p, shapes)) }),
|
|
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)) })) };
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/** This function throws a `RangeError` for a list of shape kinds with a kind that is not declared. */
|
|
782
|
+
function assertShapes(shapes: readonly ShapeKind[] | undefined): void {
|
|
783
|
+
if (shapes === undefined) return;
|
|
784
|
+
if (!Array.isArray(shapes)) throw new RangeError("The option shapes is not a list of shape kinds.");
|
|
785
|
+
for (const k of shapes) if (!isShapeKind(k)) throw new RangeError(`The shape kind ${show(k)} is not one of ${SHAPE_KIND_IDS.join(", ")}.`);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/** This function gives the count of the grid points of a space that is known to be good. */
|
|
789
|
+
function sizeOf(space: StrategySpace, levels: number, limit: bigint): SpaceSize {
|
|
654
790
|
let base = 0n;
|
|
655
791
|
let meta: bigint | null = 0n;
|
|
656
|
-
for (const c of combos(space,
|
|
792
|
+
for (const c of combos(space, levels, true, limit)) {
|
|
657
793
|
if (c.base) base += c.weight!;
|
|
658
794
|
else meta = meta === null || c.weight === null || meta + c.weight > limit ? null : meta + c.weight;
|
|
659
795
|
}
|
|
@@ -674,11 +810,12 @@ function* product<T>(lists: readonly (readonly T[])[], i = 0, acc: T[] = []): Ge
|
|
|
674
810
|
}
|
|
675
811
|
|
|
676
812
|
/** 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
|
|
813
|
+
function gridConfigs(space: StrategySpace, levels: number, limit: number, shapes: readonly ShapeKind[] | undefined): StrategyConfig[] {
|
|
814
|
+
const kept = restrict(space, shapes);
|
|
815
|
+
const size = sizeOf(kept, levels, 0n).base;
|
|
679
816
|
if (size > BigInt(limit)) throw new RangeError(`The grid of the space ${space.engine} has ${size} configs, more than the limit ${limit}.`);
|
|
680
817
|
const out: StrategyConfig[] = [];
|
|
681
|
-
for (const c of combos(
|
|
818
|
+
for (const c of combos(kept, levels, false, 0n)) {
|
|
682
819
|
const names = [...c.choices.keys()];
|
|
683
820
|
const lists = names.map((k) => {
|
|
684
821
|
const o = c.choices.get(k)!;
|
|
@@ -724,6 +861,7 @@ function sampleConfigs(space: StrategySpace, n: number, seed: number, meta: bool
|
|
|
724
861
|
const lists = stages.map((s) => {
|
|
725
862
|
const list: (StageOption | null)[] = s.options.filter((o) => meta || o.members === undefined);
|
|
726
863
|
if (s.optional) list.push(null);
|
|
864
|
+
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
865
|
return list;
|
|
728
866
|
});
|
|
729
867
|
const points: Map<string, StageOption | null>[] = [];
|
|
@@ -761,6 +899,14 @@ function sampleConfigs(space: StrategySpace, n: number, seed: number, meta: bool
|
|
|
761
899
|
});
|
|
762
900
|
}
|
|
763
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
|
+
}
|
|
764
910
|
const part = partOf(space, s.name).space;
|
|
765
911
|
const poolSize = Math.max(n, 4 * o.members.max, 8);
|
|
766
912
|
const seen = new Set<string>();
|
|
@@ -794,12 +940,15 @@ function sampleConfigs(space: StrategySpace, n: number, seed: number, meta: bool
|
|
|
794
940
|
* - 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
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.
|
|
796
942
|
* - A sample draws again when a point breaks a constraint. After `maxTries` draws, it throws an `Error`. Two sampled configs can be equal.
|
|
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.
|
|
797
945
|
*/
|
|
798
946
|
export function enumerateSpace(space: StrategySpace, options: EnumerateOptions): StrategyConfig[] {
|
|
799
947
|
assertSpace(space);
|
|
948
|
+
assertShapes(options.shapes);
|
|
800
949
|
if (options.mode === "grid") {
|
|
801
950
|
assertWhole(options.levels, "levels", 1);
|
|
802
|
-
return gridConfigs(space, options.levels, options.limit ?? 100_000);
|
|
951
|
+
return gridConfigs(space, options.levels, options.limit ?? 100_000, options.shapes);
|
|
803
952
|
}
|
|
804
953
|
if (options.mode !== "sample") throw new RangeError(`The mode ${String((options as { mode?: unknown }).mode)} is not grid or sample.`);
|
|
805
954
|
assertWhole(options.n, "n", 0);
|
|
@@ -807,7 +956,16 @@ export function enumerateSpace(space: StrategySpace, options: EnumerateOptions):
|
|
|
807
956
|
const meta = options.meta ?? "skip";
|
|
808
957
|
if (meta !== "skip" && meta !== "sample") throw new RangeError(`The option meta is ${String(meta)}, not skip or sample.`);
|
|
809
958
|
if (options.n === 0) return [];
|
|
810
|
-
return sampleConfigs(space, options.n, options.seed, meta === "sample", options.maxTries ?? Math.max(1000, 100 * options.n));
|
|
959
|
+
return sampleConfigs(restrict(space, options.shapes), options.n, options.seed, meta === "sample", options.maxTries ?? Math.max(1000, 100 * options.n));
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
/**
|
|
963
|
+
* 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.
|
|
964
|
+
* A space accepts a kind when it has at least one config without a meta option on that kind.
|
|
965
|
+
*/
|
|
966
|
+
export function spaceShapeKinds(space: StrategySpace): ShapeKind[] {
|
|
967
|
+
assertSpace(space);
|
|
968
|
+
return SHAPE_KIND_IDS.filter((k) => sizeOf(restrict(space, [k]), 1, 0n).base > 0n);
|
|
811
969
|
}
|
|
812
970
|
|
|
813
971
|
/**
|