@mark1russell7/highways 0.9.3 → 0.9.5
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 +85 -5
- package/build/index.d.ts +954 -5
- package/build/index.d.ts.map +1 -1
- package/build/index.js +10689 -5294
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/bench/screen/exercise.ts +4 -1
- package/src/bench/screen/set.ts +58 -1
- package/src/families/cases.ts +17 -9
- package/src/families/coverage.ts +502 -0
- package/src/families/index.ts +4 -1
- package/src/highways/balance/index.ts +93 -0
- package/src/highways/bronchial/index.ts +144 -0
- package/src/highways/index.ts +5 -0
- package/src/highways/knee/index.ts +154 -0
- package/src/highways/knee/root.ts +19 -0
- package/src/highways/manifolds/index.ts +157 -0
- package/src/highways/slots/index.ts +178 -0
- package/src/index.ts +8 -0
- package/src/levels/detail/index.ts +140 -0
- package/src/levels/venation/index.ts +191 -0
- package/src/plugs/leaders/index.ts +42 -0
- package/src/plugs/spread/index.ts +39 -5
- package/src/routers/adam/index.ts +294 -0
- package/src/routers/annealing/index.ts +260 -0
- package/src/routers/canals/index.ts +191 -0
- package/src/routers/frames.ts +80 -16
- package/src/routers/gutter.ts +545 -0
- package/src/routers/index.ts +8 -2
- package/src/routers/reroute/index.ts +164 -0
- package/src/routers/shared.ts +43 -2
- package/src/routers/tight/index.ts +122 -0
- package/src/strategies/builtins.ts +158 -2
- package/src/strategies/schemas.ts +143 -0
- package/src/whole/curriculum/index.ts +243 -0
- package/src/whole/fireflies/index.ts +177 -0
- package/src/whole/portfolio/index.ts +108 -0
- package/src/whole/preferences/index.ts +247 -0
- package/src/whole/tracing/index.ts +144 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { inkSaved } from "../../bench/measures/bundles.ts";
|
|
2
|
+
import { detourSpread } from "../../bench/measures/routes.ts";
|
|
3
|
+
import { checkProperties } from "../../bench/properties.ts";
|
|
4
|
+
import { measureQuality } from "../../bench/quality.ts";
|
|
5
|
+
import type { Graph, Result } from "../../model.ts";
|
|
6
|
+
import type { WholeOutput } from "../../strategies/model.ts";
|
|
7
|
+
import { runPipeline, type PipelineOptions } from "../pipeline/index.ts";
|
|
8
|
+
|
|
9
|
+
/** The clock in milliseconds. */
|
|
10
|
+
const clock = (): number => (globalThis as { performance?: { now(): number } }).performance?.now() ?? Date.now();
|
|
11
|
+
|
|
12
|
+
/** One setting of the portfolio: a name and the options of the pipeline. */
|
|
13
|
+
export interface PortfolioSetting {
|
|
14
|
+
readonly name: string;
|
|
15
|
+
readonly pipeline: PipelineOptions;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** The options of the whole strategy `board-portfolio` (WILD-045). */
|
|
19
|
+
export interface BoardPortfolioOptions {
|
|
20
|
+
/** The number of settings that the portfolio tries, from the start of its list, from 3 to 12. The default is 4. */
|
|
21
|
+
readonly settings: number;
|
|
22
|
+
/** The weight of the crossings (R1) in the score, from 0 to 4. The default is 1. */
|
|
23
|
+
readonly crossings: number;
|
|
24
|
+
/** The weight of the median detour (R5) in the score, from 0 to 4. The default is 1. */
|
|
25
|
+
readonly detour: number;
|
|
26
|
+
/** The weight of the ink saved (B1) in the score, from 0 to 4. The default is 1. */
|
|
27
|
+
readonly ink: number;
|
|
28
|
+
/**
|
|
29
|
+
* The time budget in seconds, from 0 to 10. After the budget, no new setting starts. The first setting always runs.
|
|
30
|
+
* The value 0, the default, sets no budget, so the output does not depend on the speed of the machine.
|
|
31
|
+
*/
|
|
32
|
+
readonly budget: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The default options of the strategy `board-portfolio`. */
|
|
36
|
+
export const BOARD_PORTFOLIO_DEFAULTS: Readonly<BoardPortfolioOptions> = Object.freeze({ settings: 4, crossings: 1, detour: 1, ink: 1, budget: 0 });
|
|
37
|
+
|
|
38
|
+
/** The measures of one setting on the board, and its output. */
|
|
39
|
+
export interface PortfolioEntry {
|
|
40
|
+
readonly name: string;
|
|
41
|
+
readonly output: WholeOutput;
|
|
42
|
+
readonly crossings: number;
|
|
43
|
+
readonly detour: number;
|
|
44
|
+
readonly ink: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The result of `boardPortfolio`: the output and the name of the pick. It also holds the measures of each setting with no break of a hard rule. */
|
|
48
|
+
export interface BoardPortfolioRun {
|
|
49
|
+
readonly output: WholeOutput;
|
|
50
|
+
readonly pick: string;
|
|
51
|
+
readonly entries: readonly PortfolioEntry[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** This function gives the rank of each value from 0 (the best) to 1 (the worst). Equal values get the mean of their ranks. One value gets 0. */
|
|
55
|
+
export function rankShares(values: readonly number[], lowerIsBetter: boolean): number[] {
|
|
56
|
+
const n = values.length;
|
|
57
|
+
if (n < 2) return values.map(() => 0);
|
|
58
|
+
const order = values.map((_, i) => i).sort((a, b) => (lowerIsBetter ? values[a]! - values[b]! : values[b]! - values[a]!));
|
|
59
|
+
const rank = new Array<number>(n).fill(0);
|
|
60
|
+
for (let i = 0; i < n; ) {
|
|
61
|
+
let j = i;
|
|
62
|
+
while (j + 1 < n && Math.abs(values[order[j + 1]!]! - values[order[i]!]!) <= 1e-9) j++;
|
|
63
|
+
for (let k = i; k <= j; k++) rank[order[k]!] = (i + j) / 2 / (n - 1);
|
|
64
|
+
i = j + 1;
|
|
65
|
+
}
|
|
66
|
+
return rank;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* This function runs the portfolio for one board (WILD-045, after Rice, 1976). It reads only the board and the outputs of its own settings.
|
|
71
|
+
* A setting whose router or highway strategy has a node limit below the size of the board does not run.
|
|
72
|
+
* A setting with a break of a hard rule does not count. The score of a setting is `Σ w_i × the rank share of measure_i` over the settings.
|
|
73
|
+
* The measures are the crossings (R1), the median detour (R5) and the ink saved (B1). The least score wins, and a tie goes to the earlier setting.
|
|
74
|
+
*/
|
|
75
|
+
export function boardPortfolio(graph: Graph, list: readonly PortfolioSetting[], o: Readonly<BoardPortfolioOptions>, now: () => number = clock): BoardPortfolioRun {
|
|
76
|
+
const count = Math.max(1, Math.min(list.length, Math.floor(o.settings)));
|
|
77
|
+
const t0 = now();
|
|
78
|
+
const entries: PortfolioEntry[] = [];
|
|
79
|
+
let fallback: { name: string; output: WholeOutput } | undefined;
|
|
80
|
+
for (const s of list.slice(0, count)) {
|
|
81
|
+
if (o.budget > 0 && fallback !== undefined && now() - t0 > o.budget * 1000) break;
|
|
82
|
+
const limit = (s.pipeline.highway ?? s.pipeline.router).maxNodes;
|
|
83
|
+
if (limit !== undefined && graph.nodes.length > limit) continue;
|
|
84
|
+
const output = runPipeline(graph, s.pipeline);
|
|
85
|
+
fallback ??= { name: s.name, output };
|
|
86
|
+
if (checkProperties(output.graph, output.result, 1).length > 0) continue;
|
|
87
|
+
entries.push({ name: s.name, output, ...measuresOf(output.graph, output.result) });
|
|
88
|
+
}
|
|
89
|
+
if (entries.length === 0) {
|
|
90
|
+
const first = fallback ?? { name: list[0]!.name, output: runPipeline(graph, list[0]!.pipeline) };
|
|
91
|
+
return { output: first.output, pick: first.name, entries };
|
|
92
|
+
}
|
|
93
|
+
const r1 = rankShares(entries.map((e) => e.crossings), true);
|
|
94
|
+
const r5 = rankShares(entries.map((e) => e.detour), true);
|
|
95
|
+
const b1 = rankShares(entries.map((e) => e.ink), false);
|
|
96
|
+
let best = 0;
|
|
97
|
+
let score = Infinity;
|
|
98
|
+
entries.forEach((_, i) => {
|
|
99
|
+
const s = o.crossings * r1[i]! + o.detour * r5[i]! + o.ink * b1[i]!;
|
|
100
|
+
if (s < score - 1e-9) [best, score] = [i, s];
|
|
101
|
+
});
|
|
102
|
+
return { output: entries[best]!.output, pick: entries[best]!.name, entries };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** This function gives the three measures of the score of one output: the crossings, the median detour and the ink saved. */
|
|
106
|
+
function measuresOf(graph: Graph, result: Result): { crossings: number; detour: number; ink: number } {
|
|
107
|
+
return { crossings: measureQuality(graph, result).crossings, detour: detourSpread(result.routes).median, ink: inkSaved(result.routes) };
|
|
108
|
+
}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { checkProperties } from "../../bench/properties.ts";
|
|
2
|
+
import { rng } from "../../fixtures.ts";
|
|
3
|
+
import { dist, polylineLength, type Point } from "../../geom.ts";
|
|
4
|
+
import type { Graph, Route } from "../../model.ts";
|
|
5
|
+
import { turnsOf } from "../../routers/reroute/index.ts";
|
|
6
|
+
import type { WholeOutput } from "../../strategies/model.ts";
|
|
7
|
+
import { pairCosts } from "../curriculum/index.ts";
|
|
8
|
+
import { runPipeline } from "../pipeline/index.ts";
|
|
9
|
+
import type { PortfolioSetting } from "../portfolio/index.ts";
|
|
10
|
+
|
|
11
|
+
/** The features of one route, in order: crossings (R1), angle cost (R2), lane overlap (R12) over 20, detour minus 1 (R5), and bends over 2. */
|
|
12
|
+
export const PREFERENCE_FEATURES: readonly string[] = Object.freeze(["R1", "R2", "R12", "R5", "bends"]);
|
|
13
|
+
|
|
14
|
+
/** The scale of the lane overlap in the features, in units of length. */
|
|
15
|
+
const OVERLAP_SCALE = 20;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The weights of the made-up panel that labels the made-up pairs, in the order of `PREFERENCE_FEATURES`.
|
|
19
|
+
* The order comes from the studies of the readers of graphs: the crossings count most, then the bends and the length, then the angle (Purchase 1997, Ware, Purchase, Colpoys and McGill 2002).
|
|
20
|
+
* The numbers are made up. They do not come from the screen or from a board.
|
|
21
|
+
*/
|
|
22
|
+
export const PREFERENCE_PANEL: readonly number[] = Object.freeze([3, 0.75, 0.5, 1.5, 1]);
|
|
23
|
+
|
|
24
|
+
/** The options of the fit of the preferences. */
|
|
25
|
+
export interface PreferenceFit {
|
|
26
|
+
/** The weight of the L2 term of the fit, from 0 to 1. The default is 0.01. */
|
|
27
|
+
readonly l2: number;
|
|
28
|
+
/** The seed of the made-up pairs. The default is 1. */
|
|
29
|
+
readonly seed: number;
|
|
30
|
+
/** The number of made-up pairs, from 50 to 1000. The default is 200. */
|
|
31
|
+
readonly pairs: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The options of the whole strategy `preferences` (RULE-038). */
|
|
35
|
+
export interface PreferenceOptions {
|
|
36
|
+
/** The weight of the L2 term of the fit, from 0 to 1. The default is 0.01. */
|
|
37
|
+
readonly l2: number;
|
|
38
|
+
/** The number of passes over the edges, from 1 to 3. The default is 1. */
|
|
39
|
+
readonly passes: number;
|
|
40
|
+
/** The number of settings in the pool of routes, from the start of its list, from 2 to 3. The default is 3. */
|
|
41
|
+
readonly settings: number;
|
|
42
|
+
/** A new route has a detour of at most this value, or of its old route, from 1.5 to 4. The default is 2. */
|
|
43
|
+
readonly detour: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The default options of the strategy `preferences`. */
|
|
47
|
+
export const PREFERENCE_DEFAULTS: Readonly<PreferenceOptions> = Object.freeze({ l2: 0.01, passes: 1, settings: 3, detour: 2 });
|
|
48
|
+
|
|
49
|
+
/** One made-up pair: the features of two routes, and 1 when the panel prefers the first one. */
|
|
50
|
+
export interface PreferencePair {
|
|
51
|
+
readonly a: readonly number[];
|
|
52
|
+
readonly b: readonly number[];
|
|
53
|
+
readonly first: 0 | 1;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** The result of the fit: the weights, the agreement with held-out pairs, and the source of the pairs. */
|
|
57
|
+
export interface PreferenceModel {
|
|
58
|
+
readonly weights: readonly number[];
|
|
59
|
+
/** The share of 50 held-out made-up pairs where the weights agree with the panel. */
|
|
60
|
+
readonly agreement: number;
|
|
61
|
+
readonly source: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* This function gives a smooth step from 0 to 1, a stand-in for the logistic function with only `+`, `*`, `/` and `Math.abs`.
|
|
66
|
+
* It is `0.5 + 0.5 z / (1 + |z|)`. These operations are exact to the last bit on each platform.
|
|
67
|
+
*/
|
|
68
|
+
export function softStep(z: number): number {
|
|
69
|
+
return 0.5 + (0.5 * z) / (1 + Math.abs(z));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** This function gives a made-up feature vector of one route. */
|
|
73
|
+
function madeUpFeatures(random: () => number): number[] {
|
|
74
|
+
return [Math.floor(random() * 5), random() * 2, random() * 2, random(), Math.floor(random() * 7) / 2];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** This function gives a noise with a mean of 0 and a spread of approximately 0.6: the sum of three uniform values. */
|
|
78
|
+
function noise(random: () => number): number {
|
|
79
|
+
return (random() + random() + random() - 1.5) * 1.2;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** This function gives the utility of a feature vector for the weights `w`. A larger utility is better. */
|
|
83
|
+
function utility(w: readonly number[], f: readonly number[]): number {
|
|
84
|
+
let u = 0;
|
|
85
|
+
for (let i = 0; i < f.length; i++) u -= w[i]! * f[i]!;
|
|
86
|
+
return u;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* This function gives `count` made-up pairs, with the labels of the made-up panel `PREFERENCE_PANEL` and a noise.
|
|
91
|
+
* The pairs come from the seed alone. They do not come from a board.
|
|
92
|
+
*/
|
|
93
|
+
export function madeUpPairs(count: number, seed: number): PreferencePair[] {
|
|
94
|
+
const random = rng(seed);
|
|
95
|
+
const out: PreferencePair[] = [];
|
|
96
|
+
for (let i = 0; i < count; i++) {
|
|
97
|
+
const a = madeUpFeatures(random);
|
|
98
|
+
const b = madeUpFeatures(random);
|
|
99
|
+
out.push({ a, b, first: utility(PREFERENCE_PANEL, a) - utility(PREFERENCE_PANEL, b) + noise(random) > 0 ? 1 : 0 });
|
|
100
|
+
}
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** This function gives the share of the pairs where the weights `w` prefer the same route as the panel. */
|
|
105
|
+
export function agreementOf(w: readonly number[], pairs: readonly PreferencePair[]): number {
|
|
106
|
+
if (pairs.length === 0) return 0;
|
|
107
|
+
let n = 0;
|
|
108
|
+
for (const p of pairs) if ((utility(w, p.a) > utility(w, p.b) ? 1 : 0) === p.first) n++;
|
|
109
|
+
return n / pairs.length;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** The fits of earlier calls, by their options. */
|
|
113
|
+
const FITS = new Map<string, PreferenceModel>();
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* This function fits the weights of the rules to made-up pairs (RULE-038, a Bradley-Terry model, after Bradley and Terry 1952).
|
|
117
|
+
* The probability that a reader prefers `a` to `b` is `softStep(u(a) - u(b))`, with `u(x) = -w · f(x)`. The fit takes 400 steps of the gradient of the score.
|
|
118
|
+
* An L2 term `l2 × |w|²` keeps the weights small. A weight below 0 becomes 0. The weights are scaled so that their sum is the number of features.
|
|
119
|
+
* The held-out pairs come from the seed plus 7919. The pairs do not come from the screen or from a board.
|
|
120
|
+
*/
|
|
121
|
+
export function fitPreferences(options: Partial<PreferenceFit> = {}): PreferenceModel {
|
|
122
|
+
const o = { l2: 0.01, seed: 1, pairs: 200, ...options };
|
|
123
|
+
const key = JSON.stringify(o);
|
|
124
|
+
const known = FITS.get(key);
|
|
125
|
+
if (known) return known;
|
|
126
|
+
const pairs = madeUpPairs(Math.max(1, Math.floor(o.pairs)), o.seed);
|
|
127
|
+
const n = PREFERENCE_FEATURES.length;
|
|
128
|
+
const w = new Array<number>(n).fill(1);
|
|
129
|
+
const rate = 0.5;
|
|
130
|
+
for (let step = 0; step < 400; step++) {
|
|
131
|
+
const grad = new Array<number>(n).fill(0);
|
|
132
|
+
for (const p of pairs) {
|
|
133
|
+
const d = p.b.map((v, i) => v - p.a[i]!);
|
|
134
|
+
let z = 0;
|
|
135
|
+
for (let i = 0; i < n; i++) z += w[i]! * d[i]!;
|
|
136
|
+
const e = softStep(z) - p.first;
|
|
137
|
+
for (let i = 0; i < n; i++) grad[i]! += (e * d[i]!) / pairs.length;
|
|
138
|
+
}
|
|
139
|
+
for (let i = 0; i < n; i++) w[i] = Math.max(0, w[i]! - rate * (grad[i]! + 2 * o.l2 * w[i]!));
|
|
140
|
+
}
|
|
141
|
+
const sum = w.reduce((s, v) => s + v, 0);
|
|
142
|
+
const weights = sum > 1e-12 ? w.map((v) => (v * n) / sum) : w.map(() => 1);
|
|
143
|
+
const model: PreferenceModel = {
|
|
144
|
+
weights,
|
|
145
|
+
agreement: agreementOf(weights, madeUpPairs(50, o.seed + 7919)),
|
|
146
|
+
source: `${Math.floor(o.pairs)} made-up pairs of seed ${o.seed}, labelled by the made-up panel PREFERENCE_PANEL with a noise`,
|
|
147
|
+
};
|
|
148
|
+
FITS.set(key, model);
|
|
149
|
+
return model;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** A route with its bounding box, for the quick test of two routes. */
|
|
153
|
+
interface Shape {
|
|
154
|
+
readonly route: Route;
|
|
155
|
+
readonly x0: number;
|
|
156
|
+
readonly y0: number;
|
|
157
|
+
readonly x1: number;
|
|
158
|
+
readonly y1: number;
|
|
159
|
+
/** The weighted cost of the route alone: its detour and its bends. */
|
|
160
|
+
readonly own: number;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** This function gives the detour of a route: its length over the distance between its ends. */
|
|
164
|
+
function detourOf(points: readonly Point[]): number {
|
|
165
|
+
const span = dist(points[0]!, points[points.length - 1]!);
|
|
166
|
+
return span > 1e-9 ? polylineLength(points) / span : 1;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* This function runs the whole strategy `preferences` (RULE-038). It fits the weights of the rules to made-up pairs (`fitPreferences`).
|
|
171
|
+
* Each edge can take its route from any setting of the pool. The first answer is the first setting.
|
|
172
|
+
* A pass visits the edges in order. An edge takes the route of the pool with the least weighted cost, when that cost is lower than the cost of its route.
|
|
173
|
+
* The cost is the weighted sum of the features of the route against the routes of the other edges. A route with a break of a hard rule is not in the pool.
|
|
174
|
+
* The output has the routes alone, with no runs, bundles or levels.
|
|
175
|
+
*/
|
|
176
|
+
export function preferences(graph: Graph, list: readonly PortfolioSetting[], options: Partial<PreferenceOptions> = {}): WholeOutput {
|
|
177
|
+
const o = { ...PREFERENCE_DEFAULTS, ...options };
|
|
178
|
+
const w = fitPreferences({ l2: o.l2 }).weights;
|
|
179
|
+
const count = Math.max(1, Math.min(list.length, Math.floor(o.settings)));
|
|
180
|
+
const pool = new Map<string, Route[]>();
|
|
181
|
+
let first: WholeOutput | undefined;
|
|
182
|
+
for (const s of list.slice(0, count)) {
|
|
183
|
+
const limit = (s.pipeline.highway ?? s.pipeline.router).maxNodes;
|
|
184
|
+
if (limit !== undefined && graph.nodes.length > limit) continue;
|
|
185
|
+
const output = runPipeline(graph, s.pipeline);
|
|
186
|
+
first ??= output;
|
|
187
|
+
const faulty = new Set(checkProperties(output.graph, output.result, 1_000_000).map((v) => v.edge));
|
|
188
|
+
for (const r of output.result.routes) {
|
|
189
|
+
if (faulty.has(r.edge) || r.points.length < 2) continue;
|
|
190
|
+
const own = pool.get(r.edge);
|
|
191
|
+
if (own) own.push(r);
|
|
192
|
+
else pool.set(r.edge, [r]);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (!first) return runPipeline(graph, list[0]!.pipeline);
|
|
196
|
+
const shapeOf = (route: Route): Shape => {
|
|
197
|
+
let x0 = Infinity;
|
|
198
|
+
let y0 = Infinity;
|
|
199
|
+
let x1 = -Infinity;
|
|
200
|
+
let y1 = -Infinity;
|
|
201
|
+
for (const p of route.points) {
|
|
202
|
+
x0 = Math.min(x0, p.x);
|
|
203
|
+
y0 = Math.min(y0, p.y);
|
|
204
|
+
x1 = Math.max(x1, p.x);
|
|
205
|
+
y1 = Math.max(y1, p.y);
|
|
206
|
+
}
|
|
207
|
+
return { route, x0, y0, x1, y1, own: w[3]! * (detourOf(route.points) - 1) + (w[4]! * turnsOf(route.points)) / 2 };
|
|
208
|
+
};
|
|
209
|
+
const current = new Map<string, Shape>(first.result.routes.map((r) => [r.edge, shapeOf(r)]));
|
|
210
|
+
for (const [edge, shape] of current) {
|
|
211
|
+
const own = pool.get(edge);
|
|
212
|
+
if (own && !own.includes(shape.route)) current.set(edge, shapeOf(own[0]!));
|
|
213
|
+
}
|
|
214
|
+
const near = 4;
|
|
215
|
+
const costOf = (edge: string, s: Shape): number => {
|
|
216
|
+
let sum = s.own;
|
|
217
|
+
for (const [other, t] of current) {
|
|
218
|
+
if (other === edge || t.x0 > s.x1 + near || t.x1 < s.x0 - near || t.y0 > s.y1 + near || t.y1 < s.y0 - near) continue;
|
|
219
|
+
const c = pairCosts(s.route.points, t.route.points);
|
|
220
|
+
sum += w[0]! * c.R1 + w[1]! * c.R2 + (w[2]! * c.R12) / OVERLAP_SCALE;
|
|
221
|
+
}
|
|
222
|
+
return sum;
|
|
223
|
+
};
|
|
224
|
+
const edges = [...current.keys()];
|
|
225
|
+
for (let pass = 0; pass < Math.max(1, Math.floor(o.passes)); pass++) {
|
|
226
|
+
let moved = 0;
|
|
227
|
+
for (const edge of edges) {
|
|
228
|
+
const now = current.get(edge)!;
|
|
229
|
+
const base = costOf(edge, now);
|
|
230
|
+
const cap = Math.max(o.detour, detourOf(now.route.points));
|
|
231
|
+
let best: { shape: Shape; cost: number } | undefined;
|
|
232
|
+
for (const r of pool.get(edge) ?? []) {
|
|
233
|
+
if (r === now.route || detourOf(r.points) > cap + 1e-9) continue;
|
|
234
|
+
const shape = shapeOf(r);
|
|
235
|
+
const c = costOf(edge, shape);
|
|
236
|
+
if (c < (best?.cost ?? base) - 1e-9) best = { shape, cost: c };
|
|
237
|
+
}
|
|
238
|
+
if (best) {
|
|
239
|
+
current.set(edge, best.shape);
|
|
240
|
+
moved++;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (moved === 0) break;
|
|
244
|
+
}
|
|
245
|
+
const routes = first.result.routes.map((r) => current.get(r.edge)?.route ?? r);
|
|
246
|
+
return { graph: first.graph, result: { routes } };
|
|
247
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { checkProperties } from "../../bench/properties.ts";
|
|
2
|
+
import { segmentsCross, type Point } from "../../geom.ts";
|
|
3
|
+
import type { Graph, Result } from "../../model.ts";
|
|
4
|
+
import { turnsOf } from "../../routers/reroute/index.ts";
|
|
5
|
+
import type { WholeOutput } from "../../strategies/model.ts";
|
|
6
|
+
import { runPipeline } from "../pipeline/index.ts";
|
|
7
|
+
import type { PortfolioSetting } from "../portfolio/index.ts";
|
|
8
|
+
|
|
9
|
+
/** The value of the loads of the edges of a board that the pick reads: the median, the quantile 0.95 or the mean. */
|
|
10
|
+
export type TracingQuantile = "q50" | "q95" | "mean";
|
|
11
|
+
|
|
12
|
+
/** The values of `TracingQuantile`. */
|
|
13
|
+
export const TRACING_QUANTILES: readonly TracingQuantile[] = Object.freeze(["q50", "q95", "mean"]);
|
|
14
|
+
|
|
15
|
+
/** The weights of the places to choose on the way of one edge (WILD-057). */
|
|
16
|
+
export interface TracingWeights {
|
|
17
|
+
/** The weight of a crossing, from 0 to 4. The default is 1. */
|
|
18
|
+
readonly crossing: number;
|
|
19
|
+
/** The weight of a merge or a split where the edge is not an outer lane, from 0 to 4. The default is 2. */
|
|
20
|
+
readonly merge: number;
|
|
21
|
+
/** The weight of a bend, from 0 to 4. The default is 0.5. */
|
|
22
|
+
readonly bend: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The default weights of the tracing load, as the record gives them. */
|
|
26
|
+
export const TRACING_WEIGHTS: Readonly<TracingWeights> = Object.freeze({ crossing: 1, merge: 2, bend: 0.5 });
|
|
27
|
+
|
|
28
|
+
/** The options of the whole strategy `tracing` (WILD-057). */
|
|
29
|
+
export interface TracingOptions extends TracingWeights {
|
|
30
|
+
/** The number of settings that the strategy tries, from the start of its list, from 1 to 12. The default is 4. */
|
|
31
|
+
readonly settings: number;
|
|
32
|
+
/** The value of the loads that the pick reads. The default is `q95`. */
|
|
33
|
+
readonly quantile: TracingQuantile;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The default options of the strategy `tracing`. */
|
|
37
|
+
export const TRACING_DEFAULTS: Readonly<TracingOptions> = Object.freeze({ ...TRACING_WEIGHTS, settings: 4, quantile: "q95" });
|
|
38
|
+
|
|
39
|
+
/** The bounds of a polyline: the least x and y, and the largest x and y. */
|
|
40
|
+
function boundsOf(p: readonly Point[]): [number, number, number, number] {
|
|
41
|
+
let x0 = Infinity;
|
|
42
|
+
let y0 = Infinity;
|
|
43
|
+
let x1 = -Infinity;
|
|
44
|
+
let y1 = -Infinity;
|
|
45
|
+
for (const q of p) {
|
|
46
|
+
x0 = Math.min(x0, q.x);
|
|
47
|
+
y0 = Math.min(y0, q.y);
|
|
48
|
+
x1 = Math.max(x1, q.x);
|
|
49
|
+
y1 = Math.max(y1, q.y);
|
|
50
|
+
}
|
|
51
|
+
return [x0, y0, x1, y1];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* This function gives the tracing load of each edge of a result (WILD-057, after the tracing tasks of Ware, Purchase, Colpoys and McGill 2002).
|
|
56
|
+
* The load is `T(e) = crossing × crossings + merge × (merges and splits where e is not an outer lane) + bend × bends`.
|
|
57
|
+
* A shared run where the edge is not an outer lane gives one merge and one split. Such a bundle gives one split.
|
|
58
|
+
* A bend is a turn of more than 1 degree. The measure is W4 of MEAS3.
|
|
59
|
+
*/
|
|
60
|
+
export function tracingLoads(result: Result, weights: Partial<TracingWeights> = {}): Map<string, number> {
|
|
61
|
+
const w = { ...TRACING_WEIGHTS, ...weights };
|
|
62
|
+
const routes = result.routes;
|
|
63
|
+
const bounds = routes.map((r) => boundsOf(r.points));
|
|
64
|
+
const crossings = new Array<number>(routes.length).fill(0);
|
|
65
|
+
for (let i = 0; i < routes.length; i++) {
|
|
66
|
+
const a = routes[i]!.points;
|
|
67
|
+
const [ax0, ay0, ax1, ay1] = bounds[i]!;
|
|
68
|
+
for (let j = i + 1; j < routes.length; j++) {
|
|
69
|
+
const [bx0, by0, bx1, by1] = bounds[j]!;
|
|
70
|
+
if (bx0 > ax1 || bx1 < ax0 || by0 > ay1 || by1 < ay0) continue;
|
|
71
|
+
const b = routes[j]!.points;
|
|
72
|
+
let n = 0;
|
|
73
|
+
for (let p = 1; p < a.length; p++) for (let q = 1; q < b.length; q++) if (segmentsCross(a[p - 1]!, a[p]!, b[q - 1]!, b[q]!)) n++;
|
|
74
|
+
crossings[i]! += n;
|
|
75
|
+
crossings[j]! += n;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const choices = new Map<string, number>();
|
|
79
|
+
const inner = (edges: readonly string[], add: number): void => {
|
|
80
|
+
if (edges.length < 3) return;
|
|
81
|
+
for (const e of edges.slice(1, -1)) choices.set(e, (choices.get(e) ?? 0) + add);
|
|
82
|
+
};
|
|
83
|
+
for (const run of result.runs ?? []) inner(run.edges, 2);
|
|
84
|
+
for (const b of result.bundles ?? []) inner(b.edges, 1);
|
|
85
|
+
const out = new Map<string, number>();
|
|
86
|
+
routes.forEach((r, i) => out.set(r.edge, w.crossing * crossings[i]! + w.merge * (choices.get(r.edge) ?? 0) + w.bend * turnsOf(r.points)));
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** This function gives a value of a list of loads: the median, the quantile 0.95 by the nearest rank, or the mean. An empty list gives 0. */
|
|
91
|
+
export function tracingValue(loads: Iterable<number>, quantile: TracingQuantile): number {
|
|
92
|
+
const list = [...loads].sort((a, b) => a - b);
|
|
93
|
+
if (list.length === 0) return 0;
|
|
94
|
+
if (quantile === "mean") return list.reduce((s, v) => s + v, 0) / list.length;
|
|
95
|
+
const q = quantile === "q50" ? 0.5 : 0.95;
|
|
96
|
+
return list[Math.min(list.length - 1, Math.max(0, Math.ceil(q * list.length) - 1))]!;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** The tracing loads of one setting on the board: the value that the pick reads, and the median for a tie. */
|
|
100
|
+
export interface TracingEntry {
|
|
101
|
+
readonly name: string;
|
|
102
|
+
readonly value: number;
|
|
103
|
+
readonly median: number;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** The result of `tracing`: the output, the name of the pick and the loads of each setting with no break of a hard rule. */
|
|
107
|
+
export interface TracingRun {
|
|
108
|
+
readonly output: WholeOutput;
|
|
109
|
+
readonly pick: string;
|
|
110
|
+
readonly entries: readonly TracingEntry[];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* This function runs the whole strategy `tracing` (WILD-057). It runs the first `settings` settings of `list` on the board.
|
|
115
|
+
* A setting with a break of a hard rule, or above its node limit, does not count. The setting with the least tracing load wins.
|
|
116
|
+
* A tie goes to the least median load, then to the earlier setting. It reads only the board and the outputs of its own settings.
|
|
117
|
+
*/
|
|
118
|
+
export function tracing(graph: Graph, list: readonly PortfolioSetting[], options: Partial<TracingOptions> = {}): TracingRun {
|
|
119
|
+
const o = { ...TRACING_DEFAULTS, ...options };
|
|
120
|
+
const count = Math.max(1, Math.min(list.length, Math.floor(o.settings)));
|
|
121
|
+
const entries: TracingEntry[] = [];
|
|
122
|
+
const outputs: WholeOutput[] = [];
|
|
123
|
+
let fallback: { name: string; output: WholeOutput } | undefined;
|
|
124
|
+
for (const s of list.slice(0, count)) {
|
|
125
|
+
const limit = (s.pipeline.highway ?? s.pipeline.router).maxNodes;
|
|
126
|
+
if (limit !== undefined && graph.nodes.length > limit) continue;
|
|
127
|
+
const output = runPipeline(graph, s.pipeline);
|
|
128
|
+
fallback ??= { name: s.name, output };
|
|
129
|
+
if (checkProperties(output.graph, output.result, 1).length > 0) continue;
|
|
130
|
+
const loads = [...tracingLoads(output.result, o).values()];
|
|
131
|
+
entries.push({ name: s.name, value: tracingValue(loads, o.quantile), median: tracingValue(loads, "q50") });
|
|
132
|
+
outputs.push(output);
|
|
133
|
+
}
|
|
134
|
+
if (entries.length === 0) {
|
|
135
|
+
const first = fallback ?? { name: list[0]!.name, output: runPipeline(graph, list[0]!.pipeline) };
|
|
136
|
+
return { output: first.output, pick: first.name, entries };
|
|
137
|
+
}
|
|
138
|
+
let best = 0;
|
|
139
|
+
entries.forEach((e, i) => {
|
|
140
|
+
const b = entries[best]!;
|
|
141
|
+
if (e.value < b.value - 1e-9 || (Math.abs(e.value - b.value) <= 1e-9 && e.median < b.median - 1e-9)) best = i;
|
|
142
|
+
});
|
|
143
|
+
return { output: outputs[best]!, pick: entries[best]!.name, entries };
|
|
144
|
+
}
|