@mark1russell7/highways 0.8.1 → 0.9.1
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 +112 -8
- package/build/index.d.ts +989 -187
- package/build/index.d.ts.map +1 -1
- package/build/index.js +9761 -6738
- package/build/index.js.map +1 -1
- package/package.json +3 -2
- package/src/bench/compare.ts +174 -5
- package/src/bench/room.ts +1 -1
- package/src/bench/run.ts +38 -3
- package/src/bench/screen/exercise.ts +5 -0
- package/src/bench/screen/forms.ts +5 -1
- package/src/bench/screen/generators.ts +11 -41
- package/src/bench/screen/screen.ts +16 -10
- package/src/bench/screen/set.ts +156 -49
- package/src/bench/suite.ts +110 -12
- package/src/cases.ts +64 -18
- package/src/families/cases.ts +108 -0
- package/src/families/fixtures.ts +101 -0
- package/src/families/index.ts +36 -0
- package/src/families/model.ts +55 -0
- package/src/fixtures.ts +8 -2
- package/src/highways/fixtures.test-util.ts +13 -0
- package/src/highways/follow.ts +3 -2
- package/src/highways/free-lines.ts +141 -0
- package/src/highways/gaps/index.ts +85 -0
- package/src/highways/index.ts +6 -0
- package/src/highways/interchanges/index.ts +231 -0
- package/src/highways/search/index.ts +170 -0
- package/src/highways/snap.ts +4 -2
- package/src/highways/soft-keys/index.ts +8 -4
- package/src/highways/steiner/index.ts +215 -0
- package/src/highways/streets/index.ts +78 -0
- package/src/index.ts +8 -0
- package/src/lane-order.ts +37 -0
- package/src/lane-orders/stretch/index.ts +486 -0
- package/src/lane-orders/stretch/router.ts +22 -0
- package/src/levels/depths/index.ts +57 -0
- package/src/model.ts +60 -4
- package/src/plugs/facing/index.ts +22 -0
- package/src/portfolio/router.ts +44 -0
- package/src/routers/attraction/index.ts +61 -0
- package/src/routers/corners.ts +31 -7
- package/src/routers/index.ts +2 -1
- package/src/routers/plugs.ts +3 -2
- package/src/routers/sections.ts +25 -42
- package/src/routers/shared.ts +96 -14
- package/src/routers/straight.ts +3 -2
- package/src/routers/trunks.ts +24 -5
- package/src/segments.ts +4 -25
- package/src/strategies/builtins.ts +109 -2
- package/src/strategies/model.ts +55 -10
- package/src/strategies/options.ts +36 -4
- package/src/strategies/schemas.ts +124 -5
- package/src/whole/pipeline/index.ts +66 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// The 3 case families of the screen sets: `hub-chain`, `many-groups` and `packed-grid` (CORPUS-GEN).
|
|
2
|
+
import type { CorpusDimension, DimensionValues, GenerateContext } from "@mark1russell7/layout-contract";
|
|
3
|
+
import { hubChainBoard, manyGroupsBoard, packedGridBoard } from "../cases.ts";
|
|
4
|
+
import type { Graph } from "../model.ts";
|
|
5
|
+
import { changedDimensions, numberDimension, numberOf, type HighwaysBoard, type HighwaysFamily } from "./model.ts";
|
|
6
|
+
|
|
7
|
+
/** This function gives a whole-number dimension with a linear scale. */
|
|
8
|
+
const count = (name: string, min: number, max: number, value: number, meaning: string): CorpusDimension =>
|
|
9
|
+
numberDimension({ name, kind: "int", min, max, default: value, meaning });
|
|
10
|
+
|
|
11
|
+
/** This function gives the numbers of the full dimension values, by name. */
|
|
12
|
+
function numbers(dimensions: readonly CorpusDimension[], dims: DimensionValues): Record<string, number> {
|
|
13
|
+
return Object.fromEntries(dimensions.map((d) => [d.name, numberOf(dims, d.name)]));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** The fields of a case family of `caseFamily`. */
|
|
17
|
+
interface CaseFields {
|
|
18
|
+
readonly name: string;
|
|
19
|
+
readonly meaning: string;
|
|
20
|
+
readonly dimensions: readonly CorpusDimension[];
|
|
21
|
+
/** The name of the generator record, for example `case.hub-chain`. */
|
|
22
|
+
readonly generator: string;
|
|
23
|
+
/** This function gives the parameters of the generator record from the full values. The default gives the values that differ from their defaults. */
|
|
24
|
+
readonly params?: (dims: DimensionValues) => Readonly<Record<string, unknown>>;
|
|
25
|
+
/** This function makes the board. It gets the values by name, the input seed and the source of random numbers of the registry. */
|
|
26
|
+
readonly make: (values: Readonly<Record<string, number>>, seed: number, random: () => number) => Graph;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** This function gives a case family. Its stream is mulberry32 of the input seed, and it uses only the stream, `Math.floor` and `Math.ceil`. */
|
|
30
|
+
function caseFamily(fields: CaseFields): HighwaysFamily {
|
|
31
|
+
const { name, meaning, dimensions } = fields;
|
|
32
|
+
const params = fields.params ?? ((dims: DimensionValues) => changedDimensions(dimensions, dims));
|
|
33
|
+
return Object.freeze({
|
|
34
|
+
name,
|
|
35
|
+
version: 1,
|
|
36
|
+
owner: "highways",
|
|
37
|
+
meaning,
|
|
38
|
+
dimensions,
|
|
39
|
+
seeding: "mulberry32 of the input seed. The fixed part of the board takes no number. Then one stream gives the random links.",
|
|
40
|
+
crossEngine: "exact",
|
|
41
|
+
generate(seed: number, dims: DimensionValues, context: GenerateContext): HighwaysBoard {
|
|
42
|
+
return { graph: fields.make(numbers(dimensions, dims), seed, context.random), generator: { name: fields.generator, version: 1, params: params(dims), seed } };
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The dimensions of `hub-chain`. */
|
|
48
|
+
const HUB_CHAIN_DIMENSIONS: readonly CorpusDimension[] = Object.freeze([
|
|
49
|
+
count("rows", 1, 8, 3, "The number of rows of cards."),
|
|
50
|
+
count("cols", 4, 64, 32, "The number of cards in each row."),
|
|
51
|
+
count("long", 0, 200, 40, "The number of tries for a long link between two rows."),
|
|
52
|
+
count("gap", 0, 96, 48, "The free space between two cards, in units."),
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
/** The family `hub-chain`: rows of cards in a chain, and long links between rows. At its defaults, it is `hubChainBoard`. */
|
|
56
|
+
export const HUB_CHAIN: HighwaysFamily = caseFamily({
|
|
57
|
+
name: "hub-chain",
|
|
58
|
+
meaning: "Rows of cards in a chain, and long links that climb a chain of hubs away from their targets.",
|
|
59
|
+
dimensions: HUB_CHAIN_DIMENSIONS,
|
|
60
|
+
generator: "case.hub-chain",
|
|
61
|
+
make: (v, seed, random) => hubChainBoard({ seed, rows: v["rows"]!, cols: v["cols"]!, long: v["long"]!, gap: v["gap"]! }, random),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
/** The dimensions of `many-groups`. */
|
|
65
|
+
const MANY_GROUPS_DIMENSIONS: readonly CorpusDimension[] = Object.freeze([
|
|
66
|
+
count("groupCols", 1, 16, 8, "The number of columns of groups."),
|
|
67
|
+
count("groupRows", 1, 8, 4, "The number of rows of groups."),
|
|
68
|
+
count("perGroup", 1, 16, 4, "The number of cards in each group."),
|
|
69
|
+
count("crossLinksPerGroup", 0, 8, 3, "The number of random links between groups, for each group."),
|
|
70
|
+
count("groupGap", 0, 120, 60, "The free space between two groups, in units."),
|
|
71
|
+
]);
|
|
72
|
+
|
|
73
|
+
/** The family `many-groups`: groups of cards in a grid, with links between groups. At its defaults, it is `manyGroupsBoard`. */
|
|
74
|
+
export const MANY_GROUPS: HighwaysFamily = caseFamily({
|
|
75
|
+
name: "many-groups",
|
|
76
|
+
meaning: "A grid of groups of cards, with random links between groups that go around the board.",
|
|
77
|
+
dimensions: MANY_GROUPS_DIMENSIONS,
|
|
78
|
+
generator: "case.many-groups",
|
|
79
|
+
make: (v, seed, random) =>
|
|
80
|
+
manyGroupsBoard(
|
|
81
|
+
{ seed, groupCols: v["groupCols"]!, groupRows: v["groupRows"]!, perGroup: v["perGroup"]!, crossLinksPerGroup: v["crossLinksPerGroup"]!, groupGap: v["groupGap"]! },
|
|
82
|
+
random,
|
|
83
|
+
),
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
/** The dimensions of `packed-grid`. */
|
|
87
|
+
const PACKED_GRID_DIMENSIONS: readonly CorpusDimension[] = Object.freeze([
|
|
88
|
+
count("cols", 2, 32, 12, "The number of columns of boxes."),
|
|
89
|
+
count("rows", 2, 32, 8, "The number of rows of boxes."),
|
|
90
|
+
count("long", 0, 256, 48, "The number of long links between boxes far apart."),
|
|
91
|
+
count("reach", 1, 4, 2, "The largest step, in rows and in columns, of the link of a box to a box near it."),
|
|
92
|
+
]);
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The family `packed-grid`: a grid of boxes that touch. At its defaults, it is `packedGridBoard` with 12 columns, 8 rows and 48 long links.
|
|
96
|
+
* Its generator record always has `cols`, `rows` and `long`, as the screen sets of today. It has `reach` only away from its default.
|
|
97
|
+
*/
|
|
98
|
+
export const PACKED_GRID: HighwaysFamily = caseFamily({
|
|
99
|
+
name: "packed-grid",
|
|
100
|
+
meaning: "A packed grid of boxes that touch, with no channel for the routes.",
|
|
101
|
+
dimensions: PACKED_GRID_DIMENSIONS,
|
|
102
|
+
generator: "case.packed-grid",
|
|
103
|
+
params: (dims) => {
|
|
104
|
+
const reach = numberOf(dims, "reach");
|
|
105
|
+
return { cols: numberOf(dims, "cols"), rows: numberOf(dims, "rows"), long: numberOf(dims, "long"), ...(reach === 2 ? {} : { reach }) };
|
|
106
|
+
},
|
|
107
|
+
make: (v, seed, random) => packedGridBoard({ seed, cols: v["cols"]!, rows: v["rows"]!, long: v["long"]!, reach: v["reach"]! }, random),
|
|
108
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// The 3 fixture families of the screen sets: `tiny-uniform`, `small-clustered` and `dense-groups` (CORPUS-GEN).
|
|
2
|
+
import type { CorpusDimension, DimensionValues, GenerateContext } from "@mark1russell7/layout-contract";
|
|
3
|
+
import { fixtureRngSeed, makeFixture, type FixtureKind, type FixtureSpec } from "../fixtures.ts";
|
|
4
|
+
import { enumDimension, numberDimension, numberOf, type HighwaysBoard, type HighwaysFamily } from "./model.ts";
|
|
5
|
+
|
|
6
|
+
/** The values of the dimension `layout`: each `FixtureKind`. */
|
|
7
|
+
export const FIXTURE_LAYOUTS: readonly FixtureKind[] = Object.freeze(["uniform", "clustered", "grid", "hubs", "collections"]);
|
|
8
|
+
|
|
9
|
+
/** The share of the edges near the source when a spec has no `locality`: the default of `makeFixture`. */
|
|
10
|
+
export const FIXTURE_LOCALITY = 0.65;
|
|
11
|
+
|
|
12
|
+
/** The defaults of the dimensions of one fixture family: the spec of its board in the benchmark. */
|
|
13
|
+
interface FixtureDefaults {
|
|
14
|
+
readonly layout: FixtureKind;
|
|
15
|
+
readonly nodes: number;
|
|
16
|
+
readonly edgesPerNode: number;
|
|
17
|
+
readonly fixedPorts: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* This function gives the 5 dimensions of a fixture family. The name `kind` is reserved, so the dimension of the `FixtureKind` is `layout`.
|
|
22
|
+
* No dimension has a `when`, because each kind reads `locality` and `fixedPorts`.
|
|
23
|
+
*/
|
|
24
|
+
function fixtureDimensions(d: FixtureDefaults): readonly CorpusDimension[] {
|
|
25
|
+
return Object.freeze([
|
|
26
|
+
enumDimension("layout", FIXTURE_LAYOUTS, d.layout, "The way that the board puts its nodes and picks its edges: the `kind` of the `FixtureSpec`."),
|
|
27
|
+
numberDimension({ name: "nodes", kind: "int", min: 2, max: 400, default: d.nodes, scale: "log", meaning: "The number of nodes of the board." }),
|
|
28
|
+
numberDimension({ name: "edgesPerNode", kind: "real", min: 0, max: 6, default: d.edgesPerNode, meaning: "The number of edges, divided by the number of nodes." }),
|
|
29
|
+
numberDimension({ name: "locality", kind: "real", min: 0, max: 1, default: FIXTURE_LOCALITY, meaning: "The share of the edges that join a node near the source." }),
|
|
30
|
+
numberDimension({ name: "fixedPorts", kind: "real", min: 0, max: 1, default: d.fixedPorts, meaning: "The share of the nodes with one fixed plug for their outgoing edges." }),
|
|
31
|
+
]);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** This function gives the spec of a fixture board from the full dimension values. A field at the default of `makeFixture` stays out of the spec. */
|
|
35
|
+
export function fixtureSpecOf(name: string, seed: number, dims: DimensionValues): FixtureSpec {
|
|
36
|
+
const layout = dims["layout"];
|
|
37
|
+
if (typeof layout !== "string" || !FIXTURE_LAYOUTS.includes(layout as FixtureKind)) throw new RangeError(`The dimension "layout" is ${String(layout)}, not a fixture kind.`);
|
|
38
|
+
const locality = numberOf(dims, "locality");
|
|
39
|
+
const fixedPorts = numberOf(dims, "fixedPorts");
|
|
40
|
+
return {
|
|
41
|
+
name,
|
|
42
|
+
kind: layout as FixtureKind,
|
|
43
|
+
nodes: numberOf(dims, "nodes"),
|
|
44
|
+
edgesPerNode: numberOf(dims, "edgesPerNode"),
|
|
45
|
+
seed,
|
|
46
|
+
...(locality === FIXTURE_LOCALITY ? {} : { locality }),
|
|
47
|
+
...(fixedPorts === 0 ? {} : { fixedPorts }),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* This function gives the parameters of the generator record of a fixture board: the spec without its name and its seed.
|
|
53
|
+
* Thus the record of each screen board stays the record of today, for example `{ kind, nodes, edgesPerNode, fixedPorts }` of `small-clustered`.
|
|
54
|
+
*/
|
|
55
|
+
export function fixtureParams(spec: FixtureSpec): Readonly<Record<string, unknown>> {
|
|
56
|
+
const { name: _name, seed: _seed, ...params } = spec;
|
|
57
|
+
return params;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** This function gives one fixture family with the defaults of its board in the benchmark. */
|
|
61
|
+
function fixtureFamily(name: string, meaning: string, defaults: FixtureDefaults): HighwaysFamily {
|
|
62
|
+
return Object.freeze({
|
|
63
|
+
name,
|
|
64
|
+
version: 1,
|
|
65
|
+
owner: "highways",
|
|
66
|
+
meaning,
|
|
67
|
+
dimensions: fixtureDimensions(defaults),
|
|
68
|
+
seeding: "mulberry32 of (seed × 2654435761 + nodes) >>> 0. One stream gives the boxes, the plugs and then the edges, in a fixed order.",
|
|
69
|
+
// The kinds `clustered` and `collections` use `Math.log` and `Math.cos`, and the kind `hubs` uses `**`. Each fixture family has these kinds in `layout`.
|
|
70
|
+
crossEngine: "libm-dependent",
|
|
71
|
+
rngSeed: (seed: number, dims: DimensionValues) => fixtureRngSeed(seed, numberOf(dims, "nodes")),
|
|
72
|
+
generate(seed: number, dims: DimensionValues, context: GenerateContext): HighwaysBoard {
|
|
73
|
+
const spec = fixtureSpecOf(name, seed, dims);
|
|
74
|
+
return { graph: makeFixture(spec, context.random), generator: { name: "fixture", version: 1, params: fixtureParams(spec), seed } };
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** The family `tiny-uniform`: 10 nodes at random places. It is the fixture `tiny-uniform` of `FIXTURES` at its defaults. */
|
|
80
|
+
export const TINY_UNIFORM: HighwaysFamily = fixtureFamily("tiny-uniform", "A tiny board of nodes at random places: the cost of a strategy at its start.", {
|
|
81
|
+
layout: "uniform",
|
|
82
|
+
nodes: 10,
|
|
83
|
+
edgesPerNode: 1.5,
|
|
84
|
+
fixedPorts: 0,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
/** The family `small-clustered`: 100 nodes in clusters, with a fixed plug on 20% of the nodes. It is the fixture `small-clustered` at its defaults. */
|
|
88
|
+
export const SMALL_CLUSTERED: HighwaysFamily = fixtureFamily("small-clustered", "Nodes in clusters, and fixed plugs on a share of the nodes.", {
|
|
89
|
+
layout: "clustered",
|
|
90
|
+
nodes: 100,
|
|
91
|
+
edgesPerNode: 2,
|
|
92
|
+
fixedPorts: 0.2,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
/** The family `dense-groups`: 150 nodes in tight clusters. It is `denseGroupsBoard` at its defaults. */
|
|
96
|
+
export const DENSE_GROUPS: HighwaysFamily = fixtureFamily("dense-groups", "Dense clusters whose groups overlap at the far view.", {
|
|
97
|
+
layout: "clustered",
|
|
98
|
+
nodes: 150,
|
|
99
|
+
edgesPerNode: 2,
|
|
100
|
+
fixedPorts: 0,
|
|
101
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// The corpus families of highways on the shared registry of `@mark1russell7/layout-contract` (CORPUS-GEN, L45).
|
|
2
|
+
import { createFamilyRegistry, type FamilyRegistry } from "@mark1russell7/layout-contract";
|
|
3
|
+
import { HUB_CHAIN, MANY_GROUPS, PACKED_GRID } from "./cases.ts";
|
|
4
|
+
import { DENSE_GROUPS, SMALL_CLUSTERED, TINY_UNIFORM } from "./fixtures.ts";
|
|
5
|
+
import type { HighwaysBoard, HighwaysFamily } from "./model.ts";
|
|
6
|
+
|
|
7
|
+
export * from "./model.ts";
|
|
8
|
+
export { DENSE_GROUPS, FIXTURE_LAYOUTS, FIXTURE_LOCALITY, fixtureParams, fixtureSpecOf, SMALL_CLUSTERED, TINY_UNIFORM } from "./fixtures.ts";
|
|
9
|
+
export { HUB_CHAIN, MANY_GROUPS, PACKED_GRID } from "./cases.ts";
|
|
10
|
+
// The parts of the shared registry that a host of the families uses. The site imports only `@mark1russell7/highways`.
|
|
11
|
+
export {
|
|
12
|
+
createFamilyRegistry,
|
|
13
|
+
DEFAULT_RNG,
|
|
14
|
+
dimensionAt,
|
|
15
|
+
dimensionUnit,
|
|
16
|
+
familyKind,
|
|
17
|
+
levelsOf,
|
|
18
|
+
paramProblem,
|
|
19
|
+
type CorpusDimension,
|
|
20
|
+
type CorpusFamily,
|
|
21
|
+
type DimensionValues,
|
|
22
|
+
type FamilyRegistry,
|
|
23
|
+
type GenerateContext,
|
|
24
|
+
type ParamValue,
|
|
25
|
+
} from "@mark1russell7/layout-contract";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The corpus families of highways, in the order of the screen sets. Each family at its defaults makes the board of its screen family.
|
|
29
|
+
* An import registers nothing. A host makes a registry with `createFamilyRegistry(HIGHWAYS_FAMILIES)`, or with the lists of other engines too.
|
|
30
|
+
*/
|
|
31
|
+
export const HIGHWAYS_FAMILIES: readonly HighwaysFamily[] = Object.freeze([TINY_UNIFORM, SMALL_CLUSTERED, DENSE_GROUPS, HUB_CHAIN, MANY_GROUPS, PACKED_GRID]);
|
|
32
|
+
|
|
33
|
+
/** This function gives a new registry of `HIGHWAYS_FAMILIES`. */
|
|
34
|
+
export function highwaysRegistry(): FamilyRegistry<HighwaysBoard> {
|
|
35
|
+
return createFamilyRegistry(HIGHWAYS_FAMILIES);
|
|
36
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// The form of the corpus families of highways (CORPUS-GEN) on the shared registry of `@mark1russell7/layout-contract` (L45).
|
|
2
|
+
import type { CorpusDimension, CorpusFamily, DimensionValues, ParamValue } from "@mark1russell7/layout-contract";
|
|
3
|
+
import type { LayoutGenerator } from "../bench/screen/forms.ts";
|
|
4
|
+
import type { Graph } from "../model.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The output of a family of highways: the graph of the board and its generator record.
|
|
8
|
+
* A screen set puts the board into an input file with `graphInput`. The set gives the id, the member seed and the cell.
|
|
9
|
+
*/
|
|
10
|
+
export interface HighwaysBoard {
|
|
11
|
+
readonly graph: Graph;
|
|
12
|
+
/** The generator of the board: its name, its version, its parameters and its input seed. */
|
|
13
|
+
readonly generator: LayoutGenerator;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** A corpus family of highways: a family of the shared registry whose output is a `HighwaysBoard`. */
|
|
17
|
+
export type HighwaysFamily = CorpusFamily<HighwaysBoard>;
|
|
18
|
+
|
|
19
|
+
/** The engine of each dimension of the families of highways. */
|
|
20
|
+
export const HIGHWAYS_ENGINE = "highways";
|
|
21
|
+
|
|
22
|
+
/** The fields of a numeric dimension of `numberDimension`. */
|
|
23
|
+
interface NumberFields {
|
|
24
|
+
readonly name: string;
|
|
25
|
+
readonly kind: "int" | "real";
|
|
26
|
+
readonly min: number;
|
|
27
|
+
readonly max: number;
|
|
28
|
+
readonly default: number;
|
|
29
|
+
readonly scale?: "linear" | "log";
|
|
30
|
+
readonly meaning: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** This function gives an `int` or a `real` dimension that only highways reads. */
|
|
34
|
+
export function numberDimension(fields: NumberFields): CorpusDimension {
|
|
35
|
+
return Object.freeze({ ...fields, scale: fields.scale ?? "linear", engines: Object.freeze([HIGHWAYS_ENGINE]) });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** This function gives an `enum` dimension that only highways reads. */
|
|
39
|
+
export function enumDimension(name: string, values: readonly string[], value: string, meaning: string): CorpusDimension {
|
|
40
|
+
return Object.freeze({ name, kind: "enum", values: Object.freeze([...values]), default: value, meaning, engines: Object.freeze([HIGHWAYS_ENGINE]) });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** This function gives the values of the dimensions that differ from their defaults, in the order of the dimensions. */
|
|
44
|
+
export function changedDimensions(dimensions: readonly CorpusDimension[], dims: DimensionValues): Record<string, ParamValue> {
|
|
45
|
+
const out: Record<string, ParamValue> = {};
|
|
46
|
+
for (const d of dimensions) if (!Object.is(dims[d.name], d.default)) out[d.name] = dims[d.name]!;
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** This function gives the value of a numeric dimension. It throws a `RangeError` when the value is not a number. */
|
|
51
|
+
export function numberOf(dims: DimensionValues, name: string): number {
|
|
52
|
+
const v = dims[name];
|
|
53
|
+
if (typeof v !== "number") throw new RangeError(`The dimension "${name}" is ${String(v)}, not a number.`);
|
|
54
|
+
return v;
|
|
55
|
+
}
|
package/src/fixtures.ts
CHANGED
|
@@ -52,12 +52,18 @@ const SIDES: readonly Side[] = ["north", "east", "south", "west"];
|
|
|
52
52
|
const GAP = 12;
|
|
53
53
|
const SPACING = 90;
|
|
54
54
|
|
|
55
|
+
/** This function gives the seed of the random numbers of a fixture: `(seed × 2654435761 + nodes) >>> 0`. */
|
|
56
|
+
export function fixtureRngSeed(seed: number, nodes: number): number {
|
|
57
|
+
return (seed * 2654435761 + nodes) >>> 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
55
60
|
/**
|
|
56
61
|
* This function makes the graph of a fixture. The same spec always gives the same graph.
|
|
57
62
|
* Node boxes do not overlap, and a gap of 12 units stays between them.
|
|
63
|
+
* The default source of random numbers is `rng(fixtureRngSeed(spec.seed, spec.nodes))`. A corpus family gives the same stream from its registry.
|
|
58
64
|
*/
|
|
59
|
-
export function makeFixture(spec: FixtureSpec): Graph {
|
|
60
|
-
const R =
|
|
65
|
+
export function makeFixture(spec: FixtureSpec, random: () => number = rng(fixtureRngSeed(spec.seed, spec.nodes))): Graph {
|
|
66
|
+
const R = random;
|
|
61
67
|
const gauss = (): number => Math.sqrt(-2 * Math.log(R() + 1e-12)) * Math.cos(2 * Math.PI * R());
|
|
62
68
|
const side = Math.sqrt(spec.nodes) * SPACING;
|
|
63
69
|
const grid = new Grid<Box>(SPACING);
|
|
@@ -49,6 +49,19 @@ export const parallel: Graph = {
|
|
|
49
49
|
edges: [0, 1, 2].map((i) => ({ id: `e${i}`, source: { node: `l${i}` }, target: { node: `r${i}` } })),
|
|
50
50
|
};
|
|
51
51
|
|
|
52
|
+
/** Three rows of nodes on the left and on the right, with three edges between the rows. The gaps between the rows are 40 wide. */
|
|
53
|
+
export const rows: Graph = {
|
|
54
|
+
nodes: [0, 1, 2].flatMap((i) => [
|
|
55
|
+
{ id: `l${i}`, box: box(0, 60 * i) },
|
|
56
|
+
{ id: `r${i}`, box: box(600, 60 * i) },
|
|
57
|
+
]),
|
|
58
|
+
edges: [
|
|
59
|
+
{ id: "a", source: { node: "l0" }, target: { node: "r2" } },
|
|
60
|
+
{ id: "b", source: { node: "l1" }, target: { node: "r1" } },
|
|
61
|
+
{ id: "c", source: { node: "l2" }, target: { node: "r0" } },
|
|
62
|
+
],
|
|
63
|
+
};
|
|
64
|
+
|
|
52
65
|
/**
|
|
53
66
|
* This function adds the common tests of a highway strategy. The strategy is in the registry, and it gives a correct plan the same each time.
|
|
54
67
|
* Its routes break no hard rule after `follow`.
|
package/src/highways/follow.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { center, dist, pointBoxDistance, type Point } from "../geom.ts";
|
|
2
|
-
import { fixedPortPoint, nodeIndex, type End, type Graph, type Node, type Result, type Route, type SharedRun } from "../model.ts";
|
|
2
|
+
import { fixedPortPoint, nodeIndex, resolvePorts, type End, type Graph, type Node, type Result, type Route, type SharedRun } from "../model.ts";
|
|
3
3
|
import { placePlug } from "../plugs.ts";
|
|
4
4
|
import { orderLanes, type RunOrder } from "../segments.ts";
|
|
5
5
|
import { WayFinder, type WayOptions } from "../ways.ts";
|
|
@@ -68,7 +68,8 @@ function slice(stops: Stops, on: number, off: number): Point[] {
|
|
|
68
68
|
* The result has a shared run for each stretch of a spine where the same two edges or more go. Each run holds the points of its routes in a row.
|
|
69
69
|
* The same graph, plan and options give the same result.
|
|
70
70
|
*/
|
|
71
|
-
export function follow(
|
|
71
|
+
export function follow(input: Graph, plan: HighwayPlan, options: FollowOptions = {}): Result {
|
|
72
|
+
const graph = resolvePorts(input);
|
|
72
73
|
const byId = nodeIndex(graph);
|
|
73
74
|
const ways = new WayFinder(graph.nodes, options);
|
|
74
75
|
const base = new Map((options.routes?.routes ?? []).map((r) => [r.edge, r.points]));
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { dist, polylineLength, type Point } from "../geom.ts";
|
|
2
|
+
import type { Graph } from "../model.ts";
|
|
3
|
+
import { boardGaps } from "../room/index.ts";
|
|
4
|
+
import type { WayFinder } from "../ways.ts";
|
|
5
|
+
import { firstRoutes } from "./base.ts";
|
|
6
|
+
import type { HighwayInput } from "./model.ts";
|
|
7
|
+
import { edgePlugs } from "./stretches.ts";
|
|
8
|
+
|
|
9
|
+
/** The source of the candidate streets: the middle lines of the gaps of the room step, or the lines through the corners. */
|
|
10
|
+
export type StreetSource = "room" | "corners";
|
|
11
|
+
|
|
12
|
+
/** The values of `StreetSource`. */
|
|
13
|
+
export const STREET_SOURCES: readonly StreetSource[] = Object.freeze(["room", "corners"]);
|
|
14
|
+
|
|
15
|
+
/** One axis-parallel street in the free space, from `a` to `b`. */
|
|
16
|
+
export interface FreeLine {
|
|
17
|
+
readonly a: Point;
|
|
18
|
+
readonly b: Point;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** One edge for the streets: its two points out of its plugs, and the length of its first route. */
|
|
22
|
+
export interface StreetEdge {
|
|
23
|
+
readonly edge: string;
|
|
24
|
+
readonly s: Point;
|
|
25
|
+
readonly t: Point;
|
|
26
|
+
readonly base: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The ride of one edge on one street. It has the points where the edge joins and leaves the street, and two lengths. */
|
|
30
|
+
export interface StreetRide {
|
|
31
|
+
readonly on: Point;
|
|
32
|
+
readonly off: Point;
|
|
33
|
+
/** The length on the street. */
|
|
34
|
+
readonly ride: number;
|
|
35
|
+
/** The length of the two ramps, to the street and from it. */
|
|
36
|
+
readonly ramps: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const round = (v: number): number => Math.round(v * 2) / 2;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* This function gives the free pieces of the line `x = c` (axis `x`) or `y = c` (axis `y`). A box closer than `pad` to the line blocks it.
|
|
43
|
+
* A piece shorter than `min` is not in the list. The pieces run over the board and `2 pad` past it.
|
|
44
|
+
*/
|
|
45
|
+
function freePieces(finder: WayFinder, axis: "x" | "y", c: number, pad: number, min: number, lo: number, hi: number): FreeLine[] {
|
|
46
|
+
const blocks: [number, number][] = [];
|
|
47
|
+
const strip = axis === "x" ? { x: c - pad, y: lo, w: 2 * pad, h: hi - lo } : { x: lo, y: c - pad, w: hi - lo, h: 2 * pad };
|
|
48
|
+
finder.obstacles.query(strip, (n) => {
|
|
49
|
+
const a = axis === "x" ? n.box.x : n.box.y;
|
|
50
|
+
const w = axis === "x" ? n.box.w : n.box.h;
|
|
51
|
+
if (c <= a - pad || c >= a + w + pad) return;
|
|
52
|
+
const b = axis === "x" ? n.box.y : n.box.x;
|
|
53
|
+
const h = axis === "x" ? n.box.h : n.box.w;
|
|
54
|
+
blocks.push([b - pad, b + h + pad]);
|
|
55
|
+
});
|
|
56
|
+
blocks.sort((p, q) => p[0] - q[0] || p[1] - q[1]);
|
|
57
|
+
const out: FreeLine[] = [];
|
|
58
|
+
const at = (u: number): Point => (axis === "x" ? { x: c, y: u } : { x: u, y: c });
|
|
59
|
+
let from = lo;
|
|
60
|
+
for (const [s, e] of blocks) {
|
|
61
|
+
if (s - from >= min) out.push({ a: at(from), b: at(s) });
|
|
62
|
+
from = Math.max(from, e);
|
|
63
|
+
}
|
|
64
|
+
if (hi - from >= min) out.push({ a: at(from), b: at(hi) });
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* This function gives the candidate streets of a board. Each street is an axis-parallel piece of free space.
|
|
70
|
+
* The source `room` takes the middle line of each gap of `boardGaps` that is two margins wide or more.
|
|
71
|
+
* The source `corners` takes the lines one margin out from each side of each box.
|
|
72
|
+
* A street is two margins long or more. The list has no copies, and its order is the same each time.
|
|
73
|
+
*/
|
|
74
|
+
export function freeLines(graph: Graph, finder: WayFinder, source: StreetSource): FreeLine[] {
|
|
75
|
+
const nodes = graph.nodes;
|
|
76
|
+
if (nodes.length === 0) return [];
|
|
77
|
+
const m = finder.margin;
|
|
78
|
+
const pad = m / 2;
|
|
79
|
+
let x0 = Infinity;
|
|
80
|
+
let y0 = Infinity;
|
|
81
|
+
let x1 = -Infinity;
|
|
82
|
+
let y1 = -Infinity;
|
|
83
|
+
for (const n of nodes) {
|
|
84
|
+
x0 = Math.min(x0, n.box.x);
|
|
85
|
+
y0 = Math.min(y0, n.box.y);
|
|
86
|
+
x1 = Math.max(x1, n.box.x + n.box.w);
|
|
87
|
+
y1 = Math.max(y1, n.box.y + n.box.h);
|
|
88
|
+
}
|
|
89
|
+
const lines: { axis: "x" | "y"; c: number }[] = [];
|
|
90
|
+
if (source === "room") {
|
|
91
|
+
for (const g of boardGaps(nodes)) if (g.width >= 2 * m) lines.push({ axis: g.axis, c: round(g.at - g.width / 2) });
|
|
92
|
+
} else {
|
|
93
|
+
for (const n of nodes) {
|
|
94
|
+
lines.push({ axis: "x", c: round(n.box.x - m) }, { axis: "x", c: round(n.box.x + n.box.w + m) });
|
|
95
|
+
lines.push({ axis: "y", c: round(n.box.y - m) }, { axis: "y", c: round(n.box.y + n.box.h + m) });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const seen = new Set<string>();
|
|
99
|
+
const out: FreeLine[] = [];
|
|
100
|
+
const sorted = [...lines].sort((p, q) => (p.axis === q.axis ? p.c - q.c : p.axis < q.axis ? -1 : 1));
|
|
101
|
+
for (const l of sorted) {
|
|
102
|
+
const k = `${l.axis}${l.c}`;
|
|
103
|
+
if (seen.has(k)) continue;
|
|
104
|
+
seen.add(k);
|
|
105
|
+
const [lo, hi] = l.axis === "x" ? [y0 - 2 * m, y1 + 2 * m] : [x0 - 2 * m, x1 + 2 * m];
|
|
106
|
+
out.push(...freePieces(finder, l.axis, l.c, pad, 2 * m, lo, hi));
|
|
107
|
+
}
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** This function gives the edges of the input for the streets, with the points out of their plugs and the lengths of their first routes. */
|
|
112
|
+
export function streetEdges(input: HighwayInput, finder: WayFinder): StreetEdge[] {
|
|
113
|
+
const first = firstRoutes(input);
|
|
114
|
+
const out: StreetEdge[] = [];
|
|
115
|
+
for (const p of edgePlugs(input.graph)) {
|
|
116
|
+
const route = first.get(p.edge.id);
|
|
117
|
+
const s = finder.out(p.ps);
|
|
118
|
+
const t = finder.out(p.pt);
|
|
119
|
+
out.push({ edge: p.edge.id, s, t, base: route ? polylineLength(route) : dist(s, t) });
|
|
120
|
+
}
|
|
121
|
+
return out;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** This function gives the point of the street nearest to `p`. */
|
|
125
|
+
export function streetPoint(line: FreeLine, p: Point): Point {
|
|
126
|
+
if (line.a.x === line.b.x) {
|
|
127
|
+
const lo = Math.min(line.a.y, line.b.y);
|
|
128
|
+
const hi = Math.max(line.a.y, line.b.y);
|
|
129
|
+
return { x: line.a.x, y: Math.min(hi, Math.max(lo, p.y)) };
|
|
130
|
+
}
|
|
131
|
+
const lo = Math.min(line.a.x, line.b.x);
|
|
132
|
+
const hi = Math.max(line.a.x, line.b.x);
|
|
133
|
+
return { x: Math.min(hi, Math.max(lo, p.x)), y: line.a.y };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** This function gives the ride of an edge on a street. The edge joins the street at the point nearest to `s`, and leaves it at the point nearest to `t`. */
|
|
137
|
+
export function streetRide(line: FreeLine, e: StreetEdge): StreetRide {
|
|
138
|
+
const on = streetPoint(line, e.s);
|
|
139
|
+
const off = streetPoint(line, e.t);
|
|
140
|
+
return { on, off, ride: dist(on, off), ramps: dist(e.s, on) + dist(off, e.t) };
|
|
141
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { WayFinder } from "../../ways.ts";
|
|
2
|
+
import { freeLines, streetEdges, streetRide, type FreeLine, type StreetEdge, type StreetSource } from "../free-lines.ts";
|
|
3
|
+
import type { HighwayInput, HighwayPlan } from "../model.ts";
|
|
4
|
+
import { snapPlan, type SnapLine } from "../snap.ts";
|
|
5
|
+
|
|
6
|
+
/** The options of the highway strategy `gaps` (HWY-057 of the note on highways). */
|
|
7
|
+
export interface GapOptions {
|
|
8
|
+
/** A street with fewer edges than this number (the demand `k`) is not a highway. The default is 3. */
|
|
9
|
+
readonly minSize: number;
|
|
10
|
+
/** The cost of a length on a street against a length off the streets, from 0.1 to 0.8. The default is 0.5. */
|
|
11
|
+
readonly discount: number;
|
|
12
|
+
/** An edge rides a street only when its way along the street is this many times its first route or less. The default is 1.3. */
|
|
13
|
+
readonly detour: number;
|
|
14
|
+
/** The source of the streets: the gaps of the room step, or the lines through the corners. The default is `room`. */
|
|
15
|
+
readonly source: StreetSource;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** The default options of the strategy `gaps`. */
|
|
19
|
+
export const GAP_DEFAULTS: Readonly<GapOptions> = Object.freeze({ minSize: 3, discount: 0.5, detour: 1.3, source: "room" });
|
|
20
|
+
|
|
21
|
+
/** The best street of an edge, from the streets in `open`. The ride costs `discount` times its length. The way is at most `detour` times the first route. */
|
|
22
|
+
function bestStreet(lines: readonly FreeLine[], open: ReadonlySet<number>, e: StreetEdge, o: Readonly<GapOptions>, min: number): number {
|
|
23
|
+
let best = -1;
|
|
24
|
+
let bestSaving = 1e-9;
|
|
25
|
+
for (const i of open) {
|
|
26
|
+
const r = streetRide(lines[i]!, e);
|
|
27
|
+
if (r.ride < min) continue;
|
|
28
|
+
if (r.ramps + r.ride > o.detour * e.base) continue;
|
|
29
|
+
const saving = e.base - (r.ramps + o.discount * r.ride);
|
|
30
|
+
if (saving > bestSaving || (saving === bestSaving && i < best)) [best, bestSaving] = [i, saving];
|
|
31
|
+
}
|
|
32
|
+
return best;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* This function gives the highways on the gaps of the board (HWY-057). The streets are the free middle lines of the gaps.
|
|
37
|
+
* Each edge takes the street that saves it the most cost, when a length on a street costs `discount` times its length.
|
|
38
|
+
* A street with `minSize` edges or more becomes a highway. The edges of the other streets then choose again among the highways, two times.
|
|
39
|
+
*/
|
|
40
|
+
export function gapPlan(input: HighwayInput, o: Readonly<GapOptions>): HighwayPlan {
|
|
41
|
+
const finder = new WayFinder(input.graph.nodes);
|
|
42
|
+
const lines = freeLines(input.graph, finder, o.source);
|
|
43
|
+
const edges = streetEdges(input, finder);
|
|
44
|
+
const min = 2 * finder.margin;
|
|
45
|
+
const size = Math.max(2, Math.round(o.minSize));
|
|
46
|
+
let open = new Set(lines.map((_, i) => i));
|
|
47
|
+
let choice = new Map<string, number>();
|
|
48
|
+
for (let round = 0; round < 3; round++) {
|
|
49
|
+
choice = new Map();
|
|
50
|
+
const count = new Map<number, number>();
|
|
51
|
+
for (const e of edges) {
|
|
52
|
+
const i = bestStreet(lines, open, e, o, min);
|
|
53
|
+
if (i < 0) continue;
|
|
54
|
+
choice.set(e.edge, i);
|
|
55
|
+
count.set(i, (count.get(i) ?? 0) + 1);
|
|
56
|
+
}
|
|
57
|
+
const kept = new Set([...open].filter((i) => (count.get(i) ?? 0) >= size));
|
|
58
|
+
if (kept.size === open.size) break;
|
|
59
|
+
open = kept;
|
|
60
|
+
}
|
|
61
|
+
const byLine = new Map<number, SnapLine["members"][number][]>();
|
|
62
|
+
for (const e of edges) {
|
|
63
|
+
const i = choice.get(e.edge);
|
|
64
|
+
if (i === undefined || !open.has(i)) continue;
|
|
65
|
+
const r = streetRide(lines[i]!, e);
|
|
66
|
+
const list = byLine.get(i) ?? [];
|
|
67
|
+
list.push({ edge: e.edge, from: r.on, to: r.off, at: 0 });
|
|
68
|
+
byLine.set(i, list);
|
|
69
|
+
}
|
|
70
|
+
const snaps: SnapLine[] = [...byLine.keys()]
|
|
71
|
+
.sort((p, q) => p - q)
|
|
72
|
+
.map((i) => {
|
|
73
|
+
const members = byLine.get(i)!;
|
|
74
|
+
// The spine runs only over the part of the street that its members ride.
|
|
75
|
+
const line = lines[i]!;
|
|
76
|
+
const vertical = line.a.x === line.b.x;
|
|
77
|
+
const us = members.flatMap((m) => (vertical ? [m.from.y, m.to.y] : [m.from.x, m.to.x]));
|
|
78
|
+
const lo = Math.min(...us);
|
|
79
|
+
const hi = Math.max(...us);
|
|
80
|
+
const a = vertical ? { x: line.a.x, y: lo } : { x: lo, y: line.a.y };
|
|
81
|
+
const b = vertical ? { x: line.a.x, y: hi } : { x: hi, y: line.a.y };
|
|
82
|
+
return { key: `gap${i}`, a, b, members };
|
|
83
|
+
});
|
|
84
|
+
return snapPlan(input, snaps, { minSize: size, direct: true });
|
|
85
|
+
}
|
package/src/highways/index.ts
CHANGED
|
@@ -97,5 +97,11 @@ export { LINK_COMMUNITY_DEFAULTS, linkCommunities, linkCommunityGroups, linkComm
|
|
|
97
97
|
export { cellSteps, nearestCentre, pointGroups, TERRITORY_DEFAULTS, TERRITORY_POINTS, territoryPlan, type CellStep, type TerritoryOptions, type TerritoryPoints } from "./territories/index.ts";
|
|
98
98
|
export { coAssociation, CONSENSUS_DEFAULTS, CONSENSUS_LINKAGES, consensusGroups, consensusLabels, consensusPlan, type ConsensusLinkage, type ConsensusOptions } from "./consensus/index.ts";
|
|
99
99
|
export { HUB_ALLOCATIONS, HUB_CANDIDATES, hubCandidates, P_HUB_DEFAULTS, pHubPlan, pMedian, type HubAllocation, type HubCandidates, type PHubOptions } from "./p-hub/index.ts";
|
|
100
|
+
export { freeLines, STREET_SOURCES, streetEdges, streetPoint, streetRide, type FreeLine, type StreetEdge, type StreetRide, type StreetSource } from "./free-lines.ts";
|
|
101
|
+
export { oneSteiner, spanningTree, STEINER_DEFAULTS, STEINER_GROUPS, STEINER_METHODS, STEINER_METRICS, steinerPlan, type SteinerGroup, type SteinerMethod, type SteinerMetric, type SteinerOptions } from "./steiner/index.ts";
|
|
102
|
+
export { GAP_DEFAULTS, gapPlan, type GapOptions } from "./gaps/index.ts";
|
|
103
|
+
export { STREET_DEFAULTS, streetPlan, type StreetOptions } from "./streets/index.ts";
|
|
104
|
+
export { SEARCH_DEFAULTS, SEARCH_MOVES, searchPlan, searchScore, type SearchMove, type SearchOptions } from "./search/index.ts";
|
|
105
|
+
export { INTERCHANGE_BASES, INTERCHANGE_DEFAULTS, interchangeCandidates, interchangePlan, type Interchange, type InterchangeBase, type InterchangeOptions } from "./interchanges/index.ts";
|
|
100
106
|
export { SCALE_SPACE_DEFAULTS, scaleClusters, scaleSpaceGroups, scaleSpacePlan, type ScaleCluster, type ScaleSpaceOptions } from "./scale-space/index.ts";
|
|
101
107
|
export * from "./gates/index.ts";
|