@mark1russell7/layout-contract 0.2.11 → 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/README.md +6 -4
- package/build/index.d.ts +270 -147
- package/build/index.d.ts.map +1 -1
- package/build/index.js +285 -29
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/input.ts +8 -0
- package/src/measures.ts +21 -10
- package/src/record.ts +11 -2
- package/src/rules.ts +15 -0
- package/src/shapes.ts +119 -0
- package/src/strategy-space.ts +94 -16
- package/src/verdict.ts +47 -1
package/build/index.js
CHANGED
|
@@ -110,6 +110,134 @@ function overlapOf(a, b) {
|
|
|
110
110
|
y: Math.min(a.y + a.h, b.y + b.h) - Math.max(a.y, b.y)
|
|
111
111
|
};
|
|
112
112
|
}
|
|
113
|
+
// src/shapes.ts
|
|
114
|
+
/** The declared shape kinds, in their order. Only `rect` has all of its geometry. The other kinds have their bounding box only. */
|
|
115
|
+
const SHAPE_KINDS = Object.freeze([
|
|
116
|
+
{
|
|
117
|
+
id: "rect",
|
|
118
|
+
fields: ["w", "h"],
|
|
119
|
+
regions: ["rect"],
|
|
120
|
+
geometry: {
|
|
121
|
+
bounds: true,
|
|
122
|
+
area: true,
|
|
123
|
+
contains: true,
|
|
124
|
+
overlap: true
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
id: "polygon",
|
|
129
|
+
fields: ["points", "holes?"],
|
|
130
|
+
regions: ["polygon"],
|
|
131
|
+
geometry: {
|
|
132
|
+
bounds: true,
|
|
133
|
+
area: false,
|
|
134
|
+
contains: false,
|
|
135
|
+
overlap: false
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
id: "circle",
|
|
140
|
+
fields: [
|
|
141
|
+
"cx",
|
|
142
|
+
"cy",
|
|
143
|
+
"r"
|
|
144
|
+
],
|
|
145
|
+
regions: ["circle"],
|
|
146
|
+
geometry: {
|
|
147
|
+
bounds: true,
|
|
148
|
+
area: false,
|
|
149
|
+
contains: false,
|
|
150
|
+
overlap: false
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
id: "free",
|
|
155
|
+
fields: [
|
|
156
|
+
"w",
|
|
157
|
+
"h",
|
|
158
|
+
"shape?"
|
|
159
|
+
],
|
|
160
|
+
regions: [
|
|
161
|
+
"rect",
|
|
162
|
+
"polygon",
|
|
163
|
+
"circle",
|
|
164
|
+
"ellipse"
|
|
165
|
+
],
|
|
166
|
+
geometry: {
|
|
167
|
+
bounds: true,
|
|
168
|
+
area: false,
|
|
169
|
+
contains: false,
|
|
170
|
+
overlap: false
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
]);
|
|
174
|
+
/** The ids of `SHAPE_KINDS`, in their order. */
|
|
175
|
+
const SHAPE_KIND_IDS = Object.freeze(SHAPE_KINDS.map((k) => k.id));
|
|
176
|
+
/** The shape kinds of a measure or a rule without `shapes`. The contract measured only rectangles before L46. */
|
|
177
|
+
const DEFAULT_SHAPES = Object.freeze(["rect"]);
|
|
178
|
+
const REGION_KIND = {
|
|
179
|
+
rect: "rect",
|
|
180
|
+
polygon: "polygon",
|
|
181
|
+
circle: "circle",
|
|
182
|
+
ellipse: "free"
|
|
183
|
+
};
|
|
184
|
+
/** This function tells if a value is a declared shape kind. */
|
|
185
|
+
function isShapeKind(value) {
|
|
186
|
+
return typeof value === "string" && SHAPE_KIND_IDS.includes(value);
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* This function gives the shape kind of a box. A box with `shapeKind` has that kind.
|
|
190
|
+
* Without it, the kind comes from `shape`: no shape is a `rect`, and an ellipse is `free`.
|
|
191
|
+
*/
|
|
192
|
+
function shapeKindOf(box) {
|
|
193
|
+
if (box.shapeKind !== void 0) return box.shapeKind;
|
|
194
|
+
return box.shape === void 0 ? "rect" : REGION_KIND[box.shape.kind];
|
|
195
|
+
}
|
|
196
|
+
/** This function gives the shape kinds of the boxes of an input, in the order of `SHAPE_KINDS`. An input without boxes gives an empty list. */
|
|
197
|
+
function shapeKindsOf(input) {
|
|
198
|
+
const seen = new Set(input.boxes.map(shapeKindOf));
|
|
199
|
+
return SHAPE_KIND_IDS.filter((k) => seen.has(k));
|
|
200
|
+
}
|
|
201
|
+
/** This function gives the value of the cell dimension `itemShape` of an input: its one kind, `mixed` for more kinds, or `rect` without boxes. */
|
|
202
|
+
function itemShapeOf(input) {
|
|
203
|
+
const kinds = shapeKindsOf(input);
|
|
204
|
+
return kinds.length === 0 ? "rect" : kinds.length === 1 ? kinds[0] : "mixed";
|
|
205
|
+
}
|
|
206
|
+
/** This function gives the problem of the shape kind of a box, or `undefined` when `shapeKind` and `shape` agree. */
|
|
207
|
+
function shapeKindProblem(box) {
|
|
208
|
+
const k = box.shapeKind;
|
|
209
|
+
if (k === void 0) return void 0;
|
|
210
|
+
const spec = SHAPE_KINDS.find((s) => s.id === k);
|
|
211
|
+
if (spec === void 0) return `box ${box.id}: the shape kind ${String(k)} is not a declared kind`;
|
|
212
|
+
if (box.shape === void 0) return k === "rect" || k === "free" ? void 0 : `box ${box.id}: the shape kind ${k} has no shape`;
|
|
213
|
+
if (!spec.regions.includes(box.shape.kind)) return `box ${box.id}: the shape kind ${k} does not agree with a shape of the kind ${box.shape.kind}`;
|
|
214
|
+
}
|
|
215
|
+
/** This function gives the shape kinds that a measure or a rule supports. */
|
|
216
|
+
function shapesOf(use) {
|
|
217
|
+
return use.shapes ?? DEFAULT_SHAPES;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* This function checks that each measure and each rule of a run supports each shape kind of the input. An empty list tells that the run is good.
|
|
221
|
+
* Each break has the claim `shape`, the id of the input, and the first 5 ids of the boxes of the kind.
|
|
222
|
+
*/
|
|
223
|
+
function checkShapes(input, uses) {
|
|
224
|
+
const kinds = shapeKindsOf(input);
|
|
225
|
+
const out = [];
|
|
226
|
+
for (const use of uses) {
|
|
227
|
+
const shapes = shapesOf(use);
|
|
228
|
+
for (const kind of kinds) {
|
|
229
|
+
if (shapes.includes(kind)) continue;
|
|
230
|
+
const ids = input.boxes.filter((b) => shapeKindOf(b) === kind).map((b) => b.id);
|
|
231
|
+
const shown = ids.slice(0, 5).join(", ") + (ids.length > 5 ? `, and ${ids.length - 5} more` : "");
|
|
232
|
+
out.push({
|
|
233
|
+
claim: "shape",
|
|
234
|
+
detail: `${use.id} supports only the shape kinds ${shapes.join(", ") || "(none)"}, but the input ${input.id} has ${kind} boxes: ${shown}`,
|
|
235
|
+
inputs: [input.id]
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return out;
|
|
240
|
+
}
|
|
113
241
|
// src/tolerance.ts
|
|
114
242
|
/** The epsilon for the float compares in an engine. */
|
|
115
243
|
const EPS = 1e-6;
|
|
@@ -256,8 +384,13 @@ function applyPack(input, output) {
|
|
|
256
384
|
};
|
|
257
385
|
}
|
|
258
386
|
// src/record.ts
|
|
259
|
-
/**
|
|
387
|
+
/**
|
|
388
|
+
* 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.
|
|
389
|
+
* The input also gives `shapes`. An input with only `rect` boxes gets no `shapes`, so its record does not change.
|
|
390
|
+
*/
|
|
260
391
|
function toLayoutRecord(input, run) {
|
|
392
|
+
const shapes = shapeKindsOf(input);
|
|
393
|
+
const onlyRect = shapes.length === 0 || shapes.length === 1 && shapes[0] === "rect";
|
|
261
394
|
return {
|
|
262
395
|
schema: "layout-record/1",
|
|
263
396
|
...run,
|
|
@@ -265,7 +398,8 @@ function toLayoutRecord(input, run) {
|
|
|
265
398
|
inputHash: input.hash ?? "",
|
|
266
399
|
set: input.set ?? "",
|
|
267
400
|
nodes: input.boxes.length,
|
|
268
|
-
edges: input.edges?.length ?? 0
|
|
401
|
+
edges: input.edges?.length ?? 0,
|
|
402
|
+
...onlyRect ? {} : { shapes }
|
|
269
403
|
};
|
|
270
404
|
}
|
|
271
405
|
// src/rules.ts
|
|
@@ -359,6 +493,29 @@ function countPortsOffBorder(boxes, eps = RECORD_EPS) {
|
|
|
359
493
|
}
|
|
360
494
|
return count;
|
|
361
495
|
}
|
|
496
|
+
/**
|
|
497
|
+
* The hard rules of a pack, with the shape kinds that each rule supports.
|
|
498
|
+
* `no-overlap` and `in-container` test the bounding box of a shape. That count is exact only for a `rect`.
|
|
499
|
+
* `locked-kept` and `complete` read only the positions and the ids, so they support each kind.
|
|
500
|
+
*/
|
|
501
|
+
const PACK_HARD_RULES = Object.freeze([
|
|
502
|
+
{
|
|
503
|
+
id: "no-overlap",
|
|
504
|
+
shapes: ["rect"]
|
|
505
|
+
},
|
|
506
|
+
{
|
|
507
|
+
id: "in-container",
|
|
508
|
+
shapes: ["rect"]
|
|
509
|
+
},
|
|
510
|
+
{
|
|
511
|
+
id: "locked-kept",
|
|
512
|
+
shapes: SHAPE_KIND_IDS
|
|
513
|
+
},
|
|
514
|
+
{
|
|
515
|
+
id: "complete",
|
|
516
|
+
shapes: SHAPE_KIND_IDS
|
|
517
|
+
}
|
|
518
|
+
]);
|
|
362
519
|
/** This function tells if the inside of `region` and the inside of `rect` have a common point. */
|
|
363
520
|
function regionHitsRect(region, rect) {
|
|
364
521
|
if (rect.w <= 0 || rect.h <= 0) return false;
|
|
@@ -561,13 +718,22 @@ function checkInput(input) {
|
|
|
561
718
|
ids.add(box.id);
|
|
562
719
|
if (!(box.w > 0 && box.h > 0 && Number.isFinite(box.w) && Number.isFinite(box.h))) problems.push(`box ${box.id}: the size is not positive and finite`);
|
|
563
720
|
if (box.locked === true && (box.x === void 0 || box.y === void 0)) problems.push(`box ${box.id}: a locked box has no position`);
|
|
721
|
+
const shape = shapeKindProblem(box);
|
|
722
|
+
if (shape !== void 0) problems.push(shape);
|
|
564
723
|
}
|
|
565
724
|
if (countPortsOffBorder(input.boxes) > 0) problems.push("a fixed port is not on the border of its box");
|
|
566
725
|
for (const edge of input.edges ?? []) if (!ids.has(edge.source) || !ids.has(edge.target)) problems.push(`edge ${edge.id}: a box of the edge is not in the input`);
|
|
567
726
|
return problems;
|
|
568
727
|
}
|
|
569
728
|
// src/measures.ts
|
|
570
|
-
/** The
|
|
729
|
+
/** The shape kinds of a joint measure that reads only the bounding boxes, the centers and the positions of the boxes. */
|
|
730
|
+
const BOUNDS_ONLY = SHAPE_KIND_IDS;
|
|
731
|
+
/** The shape kinds of a joint measure that reads the area `w` by `h` of each box. */
|
|
732
|
+
const AREA = ["rect"];
|
|
733
|
+
/**
|
|
734
|
+
* The specs of the joint measures, in the order of `docs/JOINT-MEASURES.md`.
|
|
735
|
+
* `joint.fill` and `joint.straight.length` read the area of each box, so they support only `rect`. `docs/SHAPES.md` tells the reasons.
|
|
736
|
+
*/
|
|
571
737
|
const JOINT_MEASURES = Object.freeze([
|
|
572
738
|
{
|
|
573
739
|
id: "joint.fill",
|
|
@@ -577,7 +743,8 @@ const JOINT_MEASURES = Object.freeze([
|
|
|
577
743
|
min: 0,
|
|
578
744
|
max: 1,
|
|
579
745
|
floor: 1,
|
|
580
|
-
role: "guard"
|
|
746
|
+
role: "guard",
|
|
747
|
+
shapes: AREA
|
|
581
748
|
},
|
|
582
749
|
{
|
|
583
750
|
id: "joint.aspectError",
|
|
@@ -588,7 +755,8 @@ const JOINT_MEASURES = Object.freeze([
|
|
|
588
755
|
max: Infinity,
|
|
589
756
|
floor: 0,
|
|
590
757
|
tol: .01,
|
|
591
|
-
role: "guard"
|
|
758
|
+
role: "guard",
|
|
759
|
+
shapes: BOUNDS_ONLY
|
|
592
760
|
},
|
|
593
761
|
{
|
|
594
762
|
id: "joint.spill",
|
|
@@ -599,7 +767,8 @@ const JOINT_MEASURES = Object.freeze([
|
|
|
599
767
|
max: 1,
|
|
600
768
|
floor: 0,
|
|
601
769
|
tol: .001,
|
|
602
|
-
role: "guard"
|
|
770
|
+
role: "guard",
|
|
771
|
+
shapes: BOUNDS_ONLY
|
|
603
772
|
},
|
|
604
773
|
{
|
|
605
774
|
id: "joint.straight.crossings",
|
|
@@ -610,7 +779,8 @@ const JOINT_MEASURES = Object.freeze([
|
|
|
610
779
|
max: Infinity,
|
|
611
780
|
floor: 0,
|
|
612
781
|
tol: 0,
|
|
613
|
-
role: "guard"
|
|
782
|
+
role: "guard",
|
|
783
|
+
shapes: BOUNDS_ONLY
|
|
614
784
|
},
|
|
615
785
|
{
|
|
616
786
|
id: "joint.straight.length",
|
|
@@ -621,7 +791,8 @@ const JOINT_MEASURES = Object.freeze([
|
|
|
621
791
|
max: Infinity,
|
|
622
792
|
floor: 0,
|
|
623
793
|
tol: .01,
|
|
624
|
-
role: "guard"
|
|
794
|
+
role: "guard",
|
|
795
|
+
shapes: AREA
|
|
625
796
|
},
|
|
626
797
|
{
|
|
627
798
|
id: "joint.stability.mean",
|
|
@@ -632,7 +803,8 @@ const JOINT_MEASURES = Object.freeze([
|
|
|
632
803
|
max: Infinity,
|
|
633
804
|
floor: 0,
|
|
634
805
|
tol: .005,
|
|
635
|
-
role: "change-guard"
|
|
806
|
+
role: "change-guard",
|
|
807
|
+
shapes: BOUNDS_ONLY
|
|
636
808
|
},
|
|
637
809
|
{
|
|
638
810
|
id: "joint.stability.moved",
|
|
@@ -643,7 +815,8 @@ const JOINT_MEASURES = Object.freeze([
|
|
|
643
815
|
max: 1,
|
|
644
816
|
floor: 0,
|
|
645
817
|
tol: .02,
|
|
646
|
-
role: "change-guard"
|
|
818
|
+
role: "change-guard",
|
|
819
|
+
shapes: BOUNDS_ONLY
|
|
647
820
|
},
|
|
648
821
|
{
|
|
649
822
|
id: "joint.proxy.rank",
|
|
@@ -651,7 +824,8 @@ const JOINT_MEASURES = Object.freeze([
|
|
|
651
824
|
direction: "higher",
|
|
652
825
|
min: -1,
|
|
653
826
|
max: 1,
|
|
654
|
-
role: "diagnostic"
|
|
827
|
+
role: "diagnostic",
|
|
828
|
+
shapes: BOUNDS_ONLY
|
|
655
829
|
},
|
|
656
830
|
{
|
|
657
831
|
id: "joint.ms",
|
|
@@ -659,7 +833,8 @@ const JOINT_MEASURES = Object.freeze([
|
|
|
659
833
|
direction: "lower",
|
|
660
834
|
min: 0,
|
|
661
835
|
max: Infinity,
|
|
662
|
-
role: "time"
|
|
836
|
+
role: "time",
|
|
837
|
+
shapes: BOUNDS_ONLY
|
|
663
838
|
}
|
|
664
839
|
]);
|
|
665
840
|
/** The threshold of `joint.stability.moved`, in widths `W` of the base. */
|
|
@@ -1764,12 +1939,28 @@ function optionProblems(stage, o) {
|
|
|
1764
1939
|
if (problem !== void 0) out.push(["when", `${pat} has a when value that is not a value of ${p.when.param}: ${problem}`]);
|
|
1765
1940
|
}
|
|
1766
1941
|
});
|
|
1942
|
+
if (o.supports !== void 0) out.push(...supportsProblems(o.supports, at));
|
|
1767
1943
|
if (o.members !== void 0) {
|
|
1768
1944
|
const { min, max, ordered } = o.members;
|
|
1769
1945
|
if (!Number.isInteger(min) || !Number.isInteger(max) || min < 1 || min > max || typeof ordered !== "boolean") out.push(["members", `${at} has a member rule without whole counts 1 <= min <= max and a boolean ordered`]);
|
|
1770
1946
|
}
|
|
1771
1947
|
return out;
|
|
1772
1948
|
}
|
|
1949
|
+
/** This function gives the problems of the `supports` of an option. */
|
|
1950
|
+
function supportsProblems(supports, at) {
|
|
1951
|
+
if (!isObject(supports)) return [["shape", `the supports of ${at} are not an object`]];
|
|
1952
|
+
const shape = supports["shape"];
|
|
1953
|
+
if (shape === void 0) return [];
|
|
1954
|
+
if (!Array.isArray(shape) || shape.length === 0) return [["shape", `${at} supports no shape kind. Remove shape to support each kind`]];
|
|
1955
|
+
const out = [];
|
|
1956
|
+
shape.forEach((k, i) => {
|
|
1957
|
+
if (!isShapeKind(k)) out.push(["shape", `${at} supports the shape kind ${show(k)}, which is not one of ${SHAPE_KIND_IDS.join(", ")}`]);
|
|
1958
|
+
else if (shape.indexOf(k) !== i) out.push(["duplicate", `${at} has the shape kind ${k} two times`]);
|
|
1959
|
+
});
|
|
1960
|
+
return out;
|
|
1961
|
+
}
|
|
1962
|
+
/** This function tells if an option supports each shape kind of the list. */
|
|
1963
|
+
const supportsShapes = (o, shapes) => shapes === void 0 || o.supports?.shape === void 0 || shapes.every((k) => o.supports.shape.includes(k));
|
|
1773
1964
|
/** This function gives the problems of the params of one stage choice. */
|
|
1774
1965
|
function choiceParamProblems(o, params, at) {
|
|
1775
1966
|
if (params !== void 0 && !isObject(params)) return [["config", `the params of ${at} are not an object`]];
|
|
@@ -1791,7 +1982,7 @@ function choiceParamProblems(o, params, at) {
|
|
|
1791
1982
|
return out;
|
|
1792
1983
|
}
|
|
1793
1984
|
/** This function gives the problems of a config of a space. A member of a meta option has `member: true`. */
|
|
1794
|
-
function configProblems(space, config, member) {
|
|
1985
|
+
function configProblems(space, config, member, shapes) {
|
|
1795
1986
|
if (!isObject(config) || !isObject(config["stages"])) return [["config", "the config is not an object with stages"]];
|
|
1796
1987
|
const out = [];
|
|
1797
1988
|
const stages = config["stages"];
|
|
@@ -1829,6 +2020,10 @@ function configProblems(space, config, member) {
|
|
|
1829
2020
|
}
|
|
1830
2021
|
chosen.set(s.name, o.id);
|
|
1831
2022
|
const at = `the option ${s.name}.${o.id}`;
|
|
2023
|
+
if (!supportsShapes(o, shapes)) {
|
|
2024
|
+
const missing = shapes.filter((k) => !o.supports.shape.includes(k));
|
|
2025
|
+
out.push(["shape", `${at} supports the shape kinds ${o.supports.shape.join(", ")}, not ${missing.join(", ")}`]);
|
|
2026
|
+
}
|
|
1832
2027
|
if (meta !== void 0 && meta !== s.name) out.push(["meta", `the stage ${s.name} is set, but the meta option of ${meta} replaces the other stages of its part`]);
|
|
1833
2028
|
out.push(...choiceParamProblems(o, c["params"], at));
|
|
1834
2029
|
if (o.members === void 0) {
|
|
@@ -1839,19 +2034,19 @@ function configProblems(space, config, member) {
|
|
|
1839
2034
|
out.push(["depth", `a member has the meta option ${s.name}.${o.id}. The depth of a meta config is 1`]);
|
|
1840
2035
|
continue;
|
|
1841
2036
|
}
|
|
1842
|
-
out.push(...memberProblems(partOf(space, s.name).space, o.members, c["members"], at));
|
|
2037
|
+
out.push(...memberProblems(partOf(space, s.name).space, o.members, c["members"], at, shapes));
|
|
1843
2038
|
}
|
|
1844
2039
|
for (const c of space.constraints ?? []) if (!holds(c, chosen)) out.push(["constraint", `the config breaks the constraint ${describe(c)}`]);
|
|
1845
2040
|
return out;
|
|
1846
2041
|
}
|
|
1847
2042
|
/** This function gives the problems of the members of a meta option. `part` is the space of the members. */
|
|
1848
|
-
function memberProblems(part, rule, members, at) {
|
|
2043
|
+
function memberProblems(part, rule, members, at, shapes) {
|
|
1849
2044
|
if (!Array.isArray(members)) return [["members", `${at} is a meta option without a list of members`]];
|
|
1850
2045
|
const out = [];
|
|
1851
2046
|
if (members.length < rule.min || members.length > rule.max) out.push(["members", `${at} has ${members.length} members, not ${rule.min} to ${rule.max}`]);
|
|
1852
2047
|
const ids = /* @__PURE__ */ new Map();
|
|
1853
2048
|
members.forEach((m, k) => {
|
|
1854
|
-
const inner = configProblems(part, m, true);
|
|
2049
|
+
const inner = configProblems(part, m, true, shapes);
|
|
1855
2050
|
for (const [claim, detail] of inner) out.push([claim, `the member ${k} of ${at}: ${detail}`]);
|
|
1856
2051
|
if (inner.length > 0) return;
|
|
1857
2052
|
const id = idOf(canonicalOf(part, m, true));
|
|
@@ -1868,9 +2063,10 @@ const toBreaks = (problems, inputs = []) => problems.map(([claim, detail]) => ({
|
|
|
1868
2063
|
/**
|
|
1869
2064
|
* This function checks one config of a space. It gives one break for each problem. An empty list tells that the config is good.
|
|
1870
2065
|
* The `claim` of a break is the kind of the problem, for example `missing`, `unknown`, `inactive`, `param`, `constraint`, `members` or `depth`.
|
|
2066
|
+
* With `shapes`, an option that does not support a kind of the list gives the claim `shape`. A member of a meta option gets the same check.
|
|
1871
2067
|
*/
|
|
1872
|
-
function checkConfig(space, config) {
|
|
1873
|
-
return toBreaks(configProblems(space, config, false));
|
|
2068
|
+
function checkConfig(space, config, options = {}) {
|
|
2069
|
+
return toBreaks(configProblems(space, config, false, options.shapes));
|
|
1874
2070
|
}
|
|
1875
2071
|
/**
|
|
1876
2072
|
* This function checks a space. It gives one break for each problem. An empty list tells that the space is good.
|
|
@@ -2136,14 +2332,40 @@ function combos(space, levels, meta, limit) {
|
|
|
2136
2332
|
/**
|
|
2137
2333
|
* This function gives the count of the grid points of a space, with `levels` levels for each numeric param.
|
|
2138
2334
|
* It counts only the active params and the combinations that keep the constraints. It gives the meta configs apart.
|
|
2335
|
+
* With `shapes`, it counts only the options that support each kind of the list.
|
|
2139
2336
|
*/
|
|
2140
2337
|
function spaceSize(space, options) {
|
|
2141
2338
|
assertSpace(space);
|
|
2142
2339
|
assertWhole(options.levels, "levels", 1);
|
|
2143
|
-
|
|
2340
|
+
assertShapes(options.shapes);
|
|
2341
|
+
return sizeOf(restrict(space, options.shapes), options.levels, options.maxMeta ?? 10n ** 18n);
|
|
2342
|
+
}
|
|
2343
|
+
/**
|
|
2344
|
+
* This function gives the space with only the options that support each shape kind of `shapes`. Without `shapes`, it gives the same space.
|
|
2345
|
+
* A stage can lose all of its options. Then the space has no config with that stage.
|
|
2346
|
+
*/
|
|
2347
|
+
function restrict(space, shapes) {
|
|
2348
|
+
if (shapes === void 0) return space;
|
|
2349
|
+
return {
|
|
2350
|
+
...space,
|
|
2351
|
+
stages: space.stages.map((s) => ({
|
|
2352
|
+
...s,
|
|
2353
|
+
options: s.options.filter((o) => supportsShapes(o, shapes))
|
|
2354
|
+
})),
|
|
2355
|
+
...space.parts === void 0 ? {} : { parts: space.parts.map((p) => restrict(p, shapes)) }
|
|
2356
|
+
};
|
|
2357
|
+
}
|
|
2358
|
+
/** This function throws a `RangeError` for a list of shape kinds with a kind that is not declared. */
|
|
2359
|
+
function assertShapes(shapes) {
|
|
2360
|
+
if (shapes === void 0) return;
|
|
2361
|
+
if (!Array.isArray(shapes)) throw new RangeError("The option shapes is not a list of shape kinds.");
|
|
2362
|
+
for (const k of shapes) if (!isShapeKind(k)) throw new RangeError(`The shape kind ${show(k)} is not one of ${SHAPE_KIND_IDS.join(", ")}.`);
|
|
2363
|
+
}
|
|
2364
|
+
/** This function gives the count of the grid points of a space that is known to be good. */
|
|
2365
|
+
function sizeOf(space, levels, limit) {
|
|
2144
2366
|
let base = 0n;
|
|
2145
2367
|
let meta = 0n;
|
|
2146
|
-
for (const c of combos(space,
|
|
2368
|
+
for (const c of combos(space, levels, true, limit)) if (c.base) base += c.weight;
|
|
2147
2369
|
else meta = meta === null || c.weight === null || meta + c.weight > limit ? null : meta + c.weight;
|
|
2148
2370
|
return {
|
|
2149
2371
|
base,
|
|
@@ -2163,14 +2385,12 @@ function* product(lists, i = 0, acc = []) {
|
|
|
2163
2385
|
}
|
|
2164
2386
|
}
|
|
2165
2387
|
/** This function gives the configs of the grid of a space, without meta options. */
|
|
2166
|
-
function gridConfigs(space, levels, limit) {
|
|
2167
|
-
const
|
|
2168
|
-
|
|
2169
|
-
maxMeta: 0n
|
|
2170
|
-
}).base;
|
|
2388
|
+
function gridConfigs(space, levels, limit, shapes) {
|
|
2389
|
+
const kept = restrict(space, shapes);
|
|
2390
|
+
const size = sizeOf(kept, levels, 0n).base;
|
|
2171
2391
|
if (size > BigInt(limit)) throw new RangeError(`The grid of the space ${space.engine} has ${size} configs, more than the limit ${limit}.`);
|
|
2172
2392
|
const out = [];
|
|
2173
|
-
for (const c of combos(
|
|
2393
|
+
for (const c of combos(kept, levels, false, 0n)) {
|
|
2174
2394
|
const names = [...c.choices.keys()];
|
|
2175
2395
|
const lists = names.map((k) => {
|
|
2176
2396
|
const o = c.choices.get(k);
|
|
@@ -2218,6 +2438,7 @@ function sampleConfigs(space, n, seed, meta, maxTries) {
|
|
|
2218
2438
|
const lists = stages.map((s) => {
|
|
2219
2439
|
const list = s.options.filter((o) => meta || o.members === void 0);
|
|
2220
2440
|
if (s.optional) list.push(null);
|
|
2441
|
+
if (list.length === 0) throw new Error(`The stage ${s.name} of the space ${space.engine} has no option for a sample. A shape filter can remove all of its options.`);
|
|
2221
2442
|
return list;
|
|
2222
2443
|
});
|
|
2223
2444
|
const points = [];
|
|
@@ -2294,12 +2515,14 @@ function sampleConfigs(space, n, seed, meta, maxTries) {
|
|
|
2294
2515
|
* - The mode `grid` gives each grid point without a meta option, in a stable order. Stages, options and values go in the order of the space.
|
|
2295
2516
|
* - The mode `sample` gives `n` configs of a seeded Latin hypercube. The options of a stage get equal strata, and each active param gets its own strata.
|
|
2296
2517
|
* - A sample draws again when a point breaks a constraint. After `maxTries` draws, it throws an `Error`. Two sampled configs can be equal.
|
|
2518
|
+
* - With `shapes`, only the options that support each kind of the list count. Without `shapes`, the result does not change.
|
|
2297
2519
|
*/
|
|
2298
2520
|
function enumerateSpace(space, options) {
|
|
2299
2521
|
assertSpace(space);
|
|
2522
|
+
assertShapes(options.shapes);
|
|
2300
2523
|
if (options.mode === "grid") {
|
|
2301
2524
|
assertWhole(options.levels, "levels", 1);
|
|
2302
|
-
return gridConfigs(space, options.levels, options.limit ?? 1e5);
|
|
2525
|
+
return gridConfigs(space, options.levels, options.limit ?? 1e5, options.shapes);
|
|
2303
2526
|
}
|
|
2304
2527
|
if (options.mode !== "sample") throw new RangeError(`The mode ${String(options.mode)} is not grid or sample.`);
|
|
2305
2528
|
assertWhole(options.n, "n", 0);
|
|
@@ -2307,7 +2530,15 @@ function enumerateSpace(space, options) {
|
|
|
2307
2530
|
const meta = options.meta ?? "skip";
|
|
2308
2531
|
if (meta !== "skip" && meta !== "sample") throw new RangeError(`The option meta is ${String(meta)}, not skip or sample.`);
|
|
2309
2532
|
if (options.n === 0) return [];
|
|
2310
|
-
return sampleConfigs(space, options.n, options.seed, meta === "sample", options.maxTries ?? Math.max(1e3, 100 * options.n));
|
|
2533
|
+
return sampleConfigs(restrict(space, options.shapes), options.n, options.seed, meta === "sample", options.maxTries ?? Math.max(1e3, 100 * options.n));
|
|
2534
|
+
}
|
|
2535
|
+
/**
|
|
2536
|
+
* 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.
|
|
2537
|
+
* A space accepts a kind when it has at least one config without a meta option on that kind.
|
|
2538
|
+
*/
|
|
2539
|
+
function spaceShapeKinds(space) {
|
|
2540
|
+
assertSpace(space);
|
|
2541
|
+
return SHAPE_KIND_IDS.filter((k) => sizeOf(restrict(space, [k]), 1, 0n).base > 0n);
|
|
2311
2542
|
}
|
|
2312
2543
|
/**
|
|
2313
2544
|
* This function joins configs of the parts of a product space into one config of the space `space`.
|
|
@@ -2410,6 +2641,25 @@ function signP(wins, losses) {
|
|
|
2410
2641
|
}
|
|
2411
2642
|
return Math.min(1, 2 * tail / 2 ** n);
|
|
2412
2643
|
}
|
|
2644
|
+
/** This function gives the breadth of a verdict with the side `side`: 1, -1, or 0 for no side. */
|
|
2645
|
+
function breadthOf(families, side) {
|
|
2646
|
+
const ties = families.filter((f) => f.sign === 0).length;
|
|
2647
|
+
return {
|
|
2648
|
+
against: side === 0 ? 0 : families.filter((f) => f.sign === -side).length,
|
|
2649
|
+
of: families.length,
|
|
2650
|
+
ties
|
|
2651
|
+
};
|
|
2652
|
+
}
|
|
2653
|
+
/** This function gives the line of a verdict: its label, its scope and its breadth (`docs/VERDICT.md`). */
|
|
2654
|
+
function summaryOf(label, side, breadth) {
|
|
2655
|
+
const ties = breadth.ties === 0 ? "" : ` (${breadth.ties} ${breadth.ties === 1 ? "tie" : "ties"})`;
|
|
2656
|
+
const word = side > 0 ? "better" : "worse";
|
|
2657
|
+
const other = side > 0 ? "worse" : "better";
|
|
2658
|
+
const count = breadth.against === 0 ? "none" : String(breadth.against);
|
|
2659
|
+
if (label === "better" || label === "worse") return `${label} on these families, ${other} on ${count} of ${breadth.of}${ties}`;
|
|
2660
|
+
if (side === 0) return `${label}: the family mean is 0, on ${breadth.of} families${ties}`;
|
|
2661
|
+
return `${label}: ${word} on the family mean, ${other} on ${count} of ${breadth.of}${ties}`;
|
|
2662
|
+
}
|
|
2413
2663
|
/** This function tells if a value is a positive whole number. */
|
|
2414
2664
|
const isCount = (v) => Number.isInteger(v) && v > 0;
|
|
2415
2665
|
/** This function gives the test of one side. */
|
|
@@ -2468,6 +2718,7 @@ function pairsOf(records, options) {
|
|
|
2468
2718
|
* - `stratified` is the mean of the family means. Its bootstrap draws the seeds inside each family. `pooled` is the mean of the inputs.
|
|
2469
2719
|
* - The permutation test changes the sign of each difference inside its family. Its statistic is the mean of the family means.
|
|
2470
2720
|
* - The label is `better` or `worse` only when `agree` is `true`. Opposite signs of the two means give `depends on the mix`.
|
|
2721
|
+
* - `scope`, `breadth` and `summary` tell that the claim is about these families, and how many families are against it.
|
|
2471
2722
|
*
|
|
2472
2723
|
* It throws a `RangeError` for a missing `tie` or `level`, and for no pair. Two records of one side and input, or a value that is not finite, also cause a `RangeError`.
|
|
2473
2724
|
*/
|
|
@@ -2562,6 +2813,8 @@ function verdict(records, options) {
|
|
|
2562
2813
|
const sign = Math.sign(stratified.mean);
|
|
2563
2814
|
const agree = sign !== 0 && sign === Math.sign(pooled.mean) && clearOn(stratified, sign) && clearOn(pooled, sign) && permutation.p < level;
|
|
2564
2815
|
const label = agree ? sign > 0 ? "better" : "worse" : sign * Math.sign(pooled.mean) < 0 ? "depends on the mix" : "no verdict";
|
|
2816
|
+
const breadth = breadthOf(families, sign);
|
|
2817
|
+
const summary = summaryOf(label, sign, breadth);
|
|
2565
2818
|
return {
|
|
2566
2819
|
measure: measure.id,
|
|
2567
2820
|
n,
|
|
@@ -2572,9 +2825,12 @@ function verdict(records, options) {
|
|
|
2572
2825
|
signTest,
|
|
2573
2826
|
agree,
|
|
2574
2827
|
label,
|
|
2828
|
+
scope: "these families",
|
|
2829
|
+
breadth,
|
|
2830
|
+
summary,
|
|
2575
2831
|
dropped
|
|
2576
2832
|
};
|
|
2577
2833
|
}
|
|
2578
|
-
export { DEFAULT_TOLERANCE, EDGE_DENSITY, EDGE_STEP_VERSION, EPS, FAMILY_ANY_KIND, JOINT_MEASURES, JOINT_MOVED_MIN, RECORD_EPS, RECORD_SCALE, SIDES, SPLIT_SCHEMA, STRATEGY_SPACE_SCHEMA, VERDICT_DRAWS, VERDICT_EXACT_CAP, VERDICT_EXACT_MAX, VERDICT_RUNS, VERDICT_SEED, applyPack, boundsOf, canonicalConfig, canonicalJson, checkConfig, checkDeclared, checkInput, checkSpace, checkSplit, compareMeasure, configId, corpusHashOf, countObstacleHits, countOutside, countOverlaps, countPortsOffBorder, declaredClaimOf, declaredCounts, documentHash, edgeStep, enumerateSpace, exercised, familyOf, fnv1a32, grow, joinConfigs, leaveOneSeedOut, marginSides, minEffectOf, mulberry32, outputGroups, outputHash, overlapOf, packHard, paramLevels, pickMoves, placedRect, portfolio, productSpace, randomInt, rectInRegion, regionHitsRect, selectBest, sha256Hex, spaceSize, splitConfig, splitFileName, splitFolds, stepRandom, toLayoutRecord, toPackInput, translate, turnBox, turnMargin, turnPoint, turnPort, turnRegion, turnSide, verdict };
|
|
2834
|
+
export { DEFAULT_SHAPES, DEFAULT_TOLERANCE, EDGE_DENSITY, EDGE_STEP_VERSION, EPS, FAMILY_ANY_KIND, JOINT_MEASURES, JOINT_MOVED_MIN, PACK_HARD_RULES, RECORD_EPS, RECORD_SCALE, SHAPE_KINDS, SHAPE_KIND_IDS, SIDES, SPLIT_SCHEMA, STRATEGY_SPACE_SCHEMA, VERDICT_DRAWS, VERDICT_EXACT_CAP, VERDICT_EXACT_MAX, VERDICT_RUNS, VERDICT_SEED, applyPack, boundsOf, canonicalConfig, canonicalJson, checkConfig, checkDeclared, checkInput, checkShapes, checkSpace, checkSplit, compareMeasure, configId, corpusHashOf, countObstacleHits, countOutside, countOverlaps, countPortsOffBorder, declaredClaimOf, declaredCounts, documentHash, edgeStep, enumerateSpace, exercised, familyOf, fnv1a32, grow, isShapeKind, itemShapeOf, joinConfigs, leaveOneSeedOut, marginSides, minEffectOf, mulberry32, outputGroups, outputHash, overlapOf, packHard, paramLevels, pickMoves, placedRect, portfolio, productSpace, randomInt, rectInRegion, regionHitsRect, selectBest, sha256Hex, shapeKindOf, shapeKindProblem, shapeKindsOf, shapesOf, spaceShapeKinds, spaceSize, splitConfig, splitFileName, splitFolds, stepRandom, toLayoutRecord, toPackInput, translate, turnBox, turnMargin, turnPoint, turnPort, turnRegion, turnSide, verdict };
|
|
2579
2835
|
|
|
2580
2836
|
//# sourceMappingURL=index.js.map
|