@mark1russell7/highways 0.4.2 → 0.5.0
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 +18 -2
- package/build/index.d.ts +799 -68
- package/build/index.d.ts.map +1 -1
- package/build/index.js +6235 -2128
- package/build/index.js.map +1 -1
- package/package.json +3 -1
- package/src/bench/exact/index.ts +197 -0
- package/src/bench/measures/index.ts +1 -0
- package/src/bench/measures/lanes.ts +304 -0
- package/src/bench/measures/levels.ts +86 -2
- package/src/bench/screen/forms.ts +11 -5
- package/src/bench/screen/measures.ts +18 -0
- package/src/bench/screen/screen.ts +28 -0
- package/src/channels.ts +56 -0
- package/src/highways/diffusion/index.ts +103 -0
- package/src/highways/edge-paths/index.ts +155 -0
- package/src/highways/fixtures.test-util.ts +71 -0
- package/src/highways/flow-clusters/index.ts +143 -0
- package/src/highways/growth/index.ts +148 -0
- package/src/highways/hdbscan/index.ts +250 -0
- package/src/highways/hough/index.ts +182 -0
- package/src/highways/hub-arcs/index.ts +181 -0
- package/src/highways/index.ts +15 -0
- package/src/highways/lanes/index.ts +189 -0
- package/src/highways/mingle/index.ts +171 -0
- package/src/highways/nested/index.ts +143 -0
- package/src/highways/ridges/index.ts +328 -0
- package/src/highways/snap.ts +110 -0
- package/src/highways/soft-keys/index.ts +114 -0
- package/src/highways/spiral/index.ts +205 -0
- package/src/highways/traclus/index.ts +296 -0
- package/src/index.ts +1 -0
- package/src/routers/shared.ts +56 -3
- package/src/strategies/builtins.ts +155 -1
- package/src/strategies/schemas.ts +180 -0
- package/src/visibility.ts +5 -0
package/package.json
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"directory": "packages/core"
|
|
10
10
|
},
|
|
11
11
|
"homepage": "https://mark1russell7.github.io/highways/",
|
|
12
|
-
"version": "0.
|
|
12
|
+
"version": "0.5.0",
|
|
13
13
|
"type": "module",
|
|
14
14
|
"main": "./build/index.js",
|
|
15
15
|
"types": "./build/index.d.ts",
|
|
@@ -30,7 +30,9 @@
|
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@mark1russell7/cue": "github:mark1russell7/cue",
|
|
33
|
+
"@mark1russell7/layout-contract": "github:mark1russell7/layout-contract#69933a61b0270c5915b82ab563d24908ade708af&path:/packages/layout-contract",
|
|
33
34
|
"@types/node": "^26.6.4",
|
|
35
|
+
"highs": "^1.15.4",
|
|
34
36
|
"typescript": "^7.0.2",
|
|
35
37
|
"vitest": "^5.0.3"
|
|
36
38
|
},
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { dist, type Point } from "../../geom.ts";
|
|
2
|
+
import { edgePlugs, sharedStretches, type KeyedWay } from "../../highways/stretches.ts";
|
|
3
|
+
import type { HighwayPlan } from "../../highways/model.ts";
|
|
4
|
+
import type { Graph } from "../../model.ts";
|
|
5
|
+
import { byCollection } from "../../routers/trunks.ts";
|
|
6
|
+
import { WayFinder, type WayOptions } from "../../ways.ts";
|
|
7
|
+
|
|
8
|
+
/** The edges that can share a link: all the edges, or only the edges of one collection, as with the rule `byCollection`. */
|
|
9
|
+
export type ExactRule = "all" | "collection";
|
|
10
|
+
|
|
11
|
+
/** The options of the exact model of fixed-charge network design (HWY-040). */
|
|
12
|
+
export interface ExactOptions extends WayOptions {
|
|
13
|
+
/** The way of each edge is this value times its shortest way or shorter. */
|
|
14
|
+
readonly stretch: number;
|
|
15
|
+
/** The edges that can share a link. */
|
|
16
|
+
readonly rule: ExactRule;
|
|
17
|
+
/** The cost of each unit of length of each way, as a share of the cost of the ink. A small value removes needless loops. The default is 0.001. */
|
|
18
|
+
readonly epsilon?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** A model of fixed-charge network design as the text of a mixed integer program (CPLEX LP format), and the function that reads its solution. */
|
|
22
|
+
export interface ExactModel {
|
|
23
|
+
/** The text of the program in the CPLEX LP format. */
|
|
24
|
+
readonly lp: string;
|
|
25
|
+
/** The number of binary variables. */
|
|
26
|
+
readonly columns: number;
|
|
27
|
+
/** The number of constraints. */
|
|
28
|
+
readonly rows: number;
|
|
29
|
+
/** The number of edges in the model. An edge without a way in the stretch limit is not in the model. */
|
|
30
|
+
readonly edges: number;
|
|
31
|
+
/** This function gives the plan of a solution. `value` gives the value of a variable by its name. */
|
|
32
|
+
decode(value: (name: string) => number): HighwayPlan;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface Arc {
|
|
36
|
+
readonly from: number;
|
|
37
|
+
readonly to: number;
|
|
38
|
+
readonly link: number;
|
|
39
|
+
readonly len: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** This function gives the shortest distance from `start` to each node, with Dijkstra on an adjacency list. */
|
|
43
|
+
function distances(adj: readonly (readonly { to: number; len: number }[])[], start: number): Float64Array {
|
|
44
|
+
const d = new Float64Array(adj.length).fill(Infinity);
|
|
45
|
+
d[start] = 0;
|
|
46
|
+
const done = new Uint8Array(adj.length);
|
|
47
|
+
// The boards of the model are small, so a scan for the smallest open node is fast enough.
|
|
48
|
+
for (;;) {
|
|
49
|
+
let u = -1;
|
|
50
|
+
for (let i = 0; i < adj.length; i++) if (!done[i] && d[i]! < Infinity && (u < 0 || d[i]! < d[u]!)) u = i;
|
|
51
|
+
if (u < 0) return d;
|
|
52
|
+
done[u] = 1;
|
|
53
|
+
for (const { to, len } of adj[u]!) if (d[u]! + len < d[to]!) d[to] = d[u]! + len;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* This function gives the model of fixed-charge network design (Magnanti and Wong 1984) for the edges of a graph (HWY-040).
|
|
59
|
+
* The network is the corner graph, plus the point out of each plug. Each link costs its length once for each key that uses it.
|
|
60
|
+
* Each edge goes one way between the points out of its two plugs. The way is `stretch` times its shortest way or shorter.
|
|
61
|
+
* The optimum has the least ink for the stretch limit. The ways of the solution give highways with `sharedStretches`.
|
|
62
|
+
*/
|
|
63
|
+
export function exactModel(graph: Graph, o: Readonly<ExactOptions>): ExactModel {
|
|
64
|
+
const finder = new WayFinder(graph.nodes, { ...(o.margin === undefined ? {} : { margin: o.margin }), ...(o.k === undefined ? {} : { k: o.k }) });
|
|
65
|
+
const corners = finder.corners;
|
|
66
|
+
const k = o.k ?? 8;
|
|
67
|
+
const pad = finder.margin / 2;
|
|
68
|
+
const points: Point[] = [...corners.points];
|
|
69
|
+
const ids = new Map<string, number>();
|
|
70
|
+
const nodeOf = (p: Point): number => {
|
|
71
|
+
const key = `${p.x},${p.y}`;
|
|
72
|
+
let id = ids.get(key);
|
|
73
|
+
if (id === undefined) {
|
|
74
|
+
id = points.length;
|
|
75
|
+
points.push(p);
|
|
76
|
+
ids.set(key, id);
|
|
77
|
+
}
|
|
78
|
+
return id;
|
|
79
|
+
};
|
|
80
|
+
corners.points.forEach((p, i) => ids.set(`${p.x},${p.y}`, i));
|
|
81
|
+
const plugs = edgePlugs(graph);
|
|
82
|
+
const ends = plugs.map(({ edge, ps, pt }) => ({ edge: edge.id, key: o.rule === "all" ? "*" : (byCollection(edge) ?? `\u0000${edge.id}`), a: nodeOf(finder.out(ps)), b: nodeOf(finder.out(pt)) }));
|
|
83
|
+
|
|
84
|
+
// The links: each corner to its k nearest visible corners, as in the corner graph, and each end point to its 2k nearest visible corners.
|
|
85
|
+
const linkIds = new Map<string, number>();
|
|
86
|
+
const links: { u: number; v: number; len: number }[] = [];
|
|
87
|
+
const addLink = (u: number, v: number): void => {
|
|
88
|
+
if (u === v) return;
|
|
89
|
+
const key = u < v ? `${u},${v}` : `${v},${u}`;
|
|
90
|
+
if (linkIds.has(key)) return;
|
|
91
|
+
linkIds.set(key, links.length);
|
|
92
|
+
links.push({ u: Math.min(u, v), v: Math.max(u, v), len: dist(points[u]!, points[v]!) });
|
|
93
|
+
};
|
|
94
|
+
for (let i = 0; i < corners.points.length; i++) for (const j of corners.nearestVisible(corners.points[i]!, k, i)) addLink(i, j);
|
|
95
|
+
for (let t = corners.points.length; t < points.length; t++) for (const j of corners.nearestVisible(points[t]!, 2 * k)) addLink(t, j);
|
|
96
|
+
for (const e of ends) if (finder.obstacles.clear(points[e.a]!, points[e.b]!, pad)) addLink(e.a, e.b);
|
|
97
|
+
const adj: { to: number; len: number }[][] = points.map(() => []);
|
|
98
|
+
for (const l of links) {
|
|
99
|
+
adj[l.u]!.push({ to: l.v, len: l.len });
|
|
100
|
+
adj[l.v]!.push({ to: l.u, len: l.len });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// The arcs of each edge: the arcs on some way within the stretch limit.
|
|
104
|
+
const eps = o.epsilon ?? 0.001;
|
|
105
|
+
const objective: string[] = [];
|
|
106
|
+
const rows: string[] = [];
|
|
107
|
+
const binaries: string[] = [];
|
|
108
|
+
const keys = [...new Set(ends.map((e) => e.key))];
|
|
109
|
+
const used = new Map<string, Set<number>>();
|
|
110
|
+
const arcsOf: { e: number; arcs: Arc[] }[] = [];
|
|
111
|
+
ends.forEach((e, ei) => {
|
|
112
|
+
const da = distances(adj, e.a);
|
|
113
|
+
const db = distances(adj, e.b);
|
|
114
|
+
const best = da[e.b]!;
|
|
115
|
+
if (!Number.isFinite(best) || e.a === e.b) return;
|
|
116
|
+
const limit = o.stretch * best + 1e-9;
|
|
117
|
+
const arcs: Arc[] = [];
|
|
118
|
+
links.forEach((l, li) => {
|
|
119
|
+
if (da[l.u]! + l.len + db[l.v]! <= limit) arcs.push({ from: l.u, to: l.v, link: li, len: l.len });
|
|
120
|
+
if (da[l.v]! + l.len + db[l.u]! <= limit) arcs.push({ from: l.v, to: l.u, link: li, len: l.len });
|
|
121
|
+
});
|
|
122
|
+
arcsOf.push({ e: ei, arcs });
|
|
123
|
+
let set = used.get(e.key);
|
|
124
|
+
if (!set) used.set(e.key, (set = new Set()));
|
|
125
|
+
for (const a of arcs) set.add(a.link);
|
|
126
|
+
// Flow: one unit leaves `a` and arrives at `b`.
|
|
127
|
+
const flow = new Map<number, string[]>();
|
|
128
|
+
const term = (n: number, s: string): void => {
|
|
129
|
+
const list = flow.get(n);
|
|
130
|
+
if (list) list.push(s);
|
|
131
|
+
else flow.set(n, [s]);
|
|
132
|
+
};
|
|
133
|
+
const stretch: string[] = [];
|
|
134
|
+
for (const a of arcs) {
|
|
135
|
+
const x = `x${ei}_${a.link}_${a.from < a.to ? 0 : 1}`;
|
|
136
|
+
binaries.push(x);
|
|
137
|
+
objective.push(`${(eps * a.len).toFixed(6)} ${x}`);
|
|
138
|
+
term(a.from, `+ ${x}`);
|
|
139
|
+
term(a.to, `- ${x}`);
|
|
140
|
+
stretch.push(`${a.len.toFixed(6)} ${x}`);
|
|
141
|
+
}
|
|
142
|
+
for (const [n, list] of flow) rows.push(`f${ei}_${n}: ${list.join(" ")} = ${n === e.a ? 1 : n === e.b ? -1 : 0}`);
|
|
143
|
+
rows.push(`s${ei}: ${stretch.join(" + ")} <= ${limit.toFixed(6)}`);
|
|
144
|
+
// An arc needs its link open for the key of the edge.
|
|
145
|
+
const byLink = new Map<number, string[]>();
|
|
146
|
+
for (const a of arcs) {
|
|
147
|
+
const list = byLink.get(a.link);
|
|
148
|
+
const x = `x${ei}_${a.link}_${a.from < a.to ? 0 : 1}`;
|
|
149
|
+
if (list) list.push(x);
|
|
150
|
+
else byLink.set(a.link, [x]);
|
|
151
|
+
}
|
|
152
|
+
const ki = keys.indexOf(e.key);
|
|
153
|
+
for (const [li, xs] of byLink) rows.push(`o${ei}_${li}: ${xs.join(" + ")} - y${ki}_${li} <= 0`);
|
|
154
|
+
});
|
|
155
|
+
keys.forEach((key, ki) => {
|
|
156
|
+
for (const li of used.get(key) ?? []) {
|
|
157
|
+
const y = `y${ki}_${li}`;
|
|
158
|
+
binaries.push(y);
|
|
159
|
+
objective.push(`${links[li]!.len.toFixed(6)} ${y}`);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
const wrap = (terms: readonly string[]): string => {
|
|
163
|
+
const out: string[] = [];
|
|
164
|
+
for (let i = 0; i < terms.length; i += 8) out.push(terms.slice(i, i + 8).join(" + "));
|
|
165
|
+
return out.join("\n + ");
|
|
166
|
+
};
|
|
167
|
+
const lp = [
|
|
168
|
+
"Minimize",
|
|
169
|
+
` obj: ${objective.length > 0 ? wrap(objective) : "0 dummy"}`,
|
|
170
|
+
"Subject To",
|
|
171
|
+
...rows.map((r) => ` ${r.replace(/((?:\S+\s+){24})/g, "$1\n ")}`),
|
|
172
|
+
"Binary",
|
|
173
|
+
...Array.from({ length: Math.ceil(binaries.length / 8) }, (_, i) => ` ${binaries.slice(i * 8, i * 8 + 8).join(" ")}`),
|
|
174
|
+
"End",
|
|
175
|
+
"",
|
|
176
|
+
].join("\n");
|
|
177
|
+
|
|
178
|
+
const decode = (value: (name: string) => number): HighwayPlan => {
|
|
179
|
+
const ways: KeyedWay[] = [];
|
|
180
|
+
for (const { e: ei, arcs } of arcsOf) {
|
|
181
|
+
const e = ends[ei]!;
|
|
182
|
+
const next = new Map<number, number>();
|
|
183
|
+
for (const a of arcs) if (value(`x${ei}_${a.link}_${a.from < a.to ? 0 : 1}`) > 0.5) next.set(a.from, a.to);
|
|
184
|
+
const seq: Point[] = [points[e.a]!];
|
|
185
|
+
let at = e.a;
|
|
186
|
+
for (let guard = 0; at !== e.b && guard <= points.length; guard++) {
|
|
187
|
+
const to = next.get(at);
|
|
188
|
+
if (to === undefined) break;
|
|
189
|
+
seq.push(points[to]!);
|
|
190
|
+
at = to;
|
|
191
|
+
}
|
|
192
|
+
if (at === e.b) ways.push({ edge: e.edge, key: e.key, points: seq });
|
|
193
|
+
}
|
|
194
|
+
return sharedStretches(ways, { minSize: 2 });
|
|
195
|
+
};
|
|
196
|
+
return { lp, columns: binaries.length, rows: rows.length, edges: arcsOf.length, decode };
|
|
197
|
+
}
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import { dist, frechet, pointSegmentDistance, segmentsCross, type Point } from "../../geom.ts";
|
|
2
|
+
import { Grid } from "../../grid.ts";
|
|
3
|
+
import type { Graph, Result, Route } from "../../model.ts";
|
|
4
|
+
import { laneRoutes, runKey, segmentsOf, type SegmentGraph } from "../../segments.ts";
|
|
5
|
+
import { measureRouteFaults } from "../links.ts";
|
|
6
|
+
import { segmentKey } from "./routes.ts";
|
|
7
|
+
import { quantile } from "./stats.ts";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The lane width of the lane measures, in units of the board. The room bench opens the lanes at the same width (`spacing` of `measureBoard`).
|
|
11
|
+
* R12 also uses it as the smallest correct distance between two parallel legs.
|
|
12
|
+
*/
|
|
13
|
+
export const MEASURE_LANE_WIDTH = 4;
|
|
14
|
+
|
|
15
|
+
/** The routes of a result with each shared segment open: one lane for each edge. */
|
|
16
|
+
export interface OpenLanes {
|
|
17
|
+
/** The segments of the result. The shared segments are the runs and the trunks of the bundles. */
|
|
18
|
+
readonly graph: SegmentGraph;
|
|
19
|
+
/** The lane route of each edge, from `laneRoutes`. Without a shared segment, the lane routes are the routes. */
|
|
20
|
+
readonly lanes: readonly Route[];
|
|
21
|
+
/** The distance between two lanes. */
|
|
22
|
+
readonly width: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** This function opens each shared segment of a result: each edge of a run or of a trunk gets its own lane, `width` apart. */
|
|
26
|
+
export function openLanes(result: Result, width: number = MEASURE_LANE_WIDTH): OpenLanes {
|
|
27
|
+
const graph = segmentsOf(result);
|
|
28
|
+
const open = new Set(graph.segments.filter((s) => s.count > 1).map((s) => runKey(s.edges)));
|
|
29
|
+
return { graph, lanes: laneRoutes(graph, open, width), width };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** One drawn segment and the sorted ids of the edges that draw it. */
|
|
33
|
+
interface Leg {
|
|
34
|
+
readonly a: Point;
|
|
35
|
+
readonly b: Point;
|
|
36
|
+
readonly edges: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** This function gives each drawn segment of the routes one time, with the edges that draw it. A segment shorter than the grain of `segmentKey` is not a leg. */
|
|
40
|
+
function legsOf(routes: readonly Route[]): Leg[] {
|
|
41
|
+
const byKey = new Map<string, { a: Point; b: Point; edges: Set<string> }>();
|
|
42
|
+
for (const r of routes) {
|
|
43
|
+
for (let i = 1; i < r.points.length; i++) {
|
|
44
|
+
const a = r.points[i - 1]!;
|
|
45
|
+
const b = r.points[i]!;
|
|
46
|
+
const k = segmentKey(a, b);
|
|
47
|
+
if (k === undefined) continue;
|
|
48
|
+
const leg = byKey.get(k);
|
|
49
|
+
if (leg) leg.edges.add(r.edge);
|
|
50
|
+
else byKey.set(k, { a, b, edges: new Set([r.edge]) });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return [...byKey.values()].map((l) => ({ a: l.a, b: l.b, edges: [...l.edges].sort().join("\u0000") }));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** This function gives the distance between the segments `ab` and `cd`. It is 0 when they cross or touch. */
|
|
57
|
+
function segmentDistance(a: Point, b: Point, c: Point, d: Point): number {
|
|
58
|
+
if (segmentsCross(a, b, c, d)) return 0;
|
|
59
|
+
return Math.min(pointSegmentDistance(a, c, d), pointSegmentDistance(b, c, d), pointSegmentDistance(c, a, b), pointSegmentDistance(d, a, b));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** This function gives the length of the union of intervals. */
|
|
63
|
+
function unionLength(intervals: [number, number][]): number {
|
|
64
|
+
intervals.sort((p, q) => p[0] - q[0]);
|
|
65
|
+
let sum = 0;
|
|
66
|
+
let lo = Number.NEGATIVE_INFINITY;
|
|
67
|
+
let hi = Number.NEGATIVE_INFINITY;
|
|
68
|
+
for (const [a, b] of intervals) {
|
|
69
|
+
if (a > hi) {
|
|
70
|
+
if (hi > lo) sum += hi - lo;
|
|
71
|
+
lo = a;
|
|
72
|
+
hi = b;
|
|
73
|
+
} else hi = Math.max(hi, b);
|
|
74
|
+
}
|
|
75
|
+
if (hi > lo) sum += hi - lo;
|
|
76
|
+
return sum;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* This function gives the lane separation of the routes (R12 of MEAS0): the share of the ink that runs too near a parallel leg of other edges.
|
|
81
|
+
*
|
|
82
|
+
* A leg is a drawn segment of `S`, and a leg that several routes share counts one time, as in B1. Two legs are near when four things are true.
|
|
83
|
+
* The sets of their edges are different. They are within `maxAngle` degrees of parallel. Their projections overlap. Their distance is less than `near`.
|
|
84
|
+
*
|
|
85
|
+
* `ns = Σ_s length(the part of s that the projections of its near legs cover) / ink(S)`. Each part counts one time, so the scale is 0 to 1.
|
|
86
|
+
* Lower is better. Routes without ink give 0. A grid of buckets finds the near legs, so the cost is `n log n` for an even density.
|
|
87
|
+
*/
|
|
88
|
+
export function routeSeparation(routes: readonly Route[], near: number = MEASURE_LANE_WIDTH, maxAngle = 5): number {
|
|
89
|
+
const legs = legsOf(routes);
|
|
90
|
+
let ink = 0;
|
|
91
|
+
for (const l of legs) ink += dist(l.a, l.b);
|
|
92
|
+
if (ink <= 0) return 0;
|
|
93
|
+
const grid = new Grid<number>(Math.max(16, 4 * near));
|
|
94
|
+
legs.forEach((l, i) => grid.addSegment(i, l.a, l.b));
|
|
95
|
+
const sin = Math.sin((maxAngle * Math.PI) / 180);
|
|
96
|
+
let close = 0;
|
|
97
|
+
legs.forEach((s, i) => {
|
|
98
|
+
const length = dist(s.a, s.b);
|
|
99
|
+
const ux = (s.b.x - s.a.x) / length;
|
|
100
|
+
const uy = (s.b.y - s.a.y) / length;
|
|
101
|
+
const cover: [number, number][] = [];
|
|
102
|
+
grid.querySegment(s.a, s.b, near, (j) => {
|
|
103
|
+
const t = legs[j]!;
|
|
104
|
+
if (j === i || t.edges === s.edges) return;
|
|
105
|
+
const tx = t.b.x - t.a.x;
|
|
106
|
+
const ty = t.b.y - t.a.y;
|
|
107
|
+
const tl = Math.hypot(tx, ty);
|
|
108
|
+
if (Math.abs(ux * ty - uy * tx) > sin * tl) return;
|
|
109
|
+
const p = (t.a.x - s.a.x) * ux + (t.a.y - s.a.y) * uy;
|
|
110
|
+
const q = (t.b.x - s.a.x) * ux + (t.b.y - s.a.y) * uy;
|
|
111
|
+
const lo = Math.max(0, Math.min(p, q));
|
|
112
|
+
const hi = Math.min(length, Math.max(p, q));
|
|
113
|
+
if (hi - lo <= 1e-9 || segmentDistance(s.a, s.b, t.a, t.b) >= near) return;
|
|
114
|
+
cover.push([lo, hi]);
|
|
115
|
+
});
|
|
116
|
+
close += unionLength(cover);
|
|
117
|
+
});
|
|
118
|
+
return Math.min(1, close / ink);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* This function gives the orthogonality of the routes (R14 of MEAS0): `orth = Σ L(legs within maxAngle degrees of an axis) / Σ_e L(r_e)`.
|
|
123
|
+
* Each route counts its own legs, also on a shared segment. The scale is 0 to 1, and higher is better for an orthogonal style.
|
|
124
|
+
* Routes without length give 1. The cost is linear.
|
|
125
|
+
*/
|
|
126
|
+
export function orthogonality(routes: readonly Route[], maxAngle = 1): number {
|
|
127
|
+
const tan = Math.tan((maxAngle * Math.PI) / 180);
|
|
128
|
+
let total = 0;
|
|
129
|
+
let straight = 0;
|
|
130
|
+
for (const r of routes) {
|
|
131
|
+
for (let i = 1; i < r.points.length; i++) {
|
|
132
|
+
const dx = Math.abs(r.points[i]!.x - r.points[i - 1]!.x);
|
|
133
|
+
const dy = Math.abs(r.points[i]!.y - r.points[i - 1]!.y);
|
|
134
|
+
const l = Math.hypot(dx, dy);
|
|
135
|
+
total += l;
|
|
136
|
+
if (Math.min(dx, dy) <= tan * Math.max(dx, dy)) straight += l;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return total > 0 ? straight / total : 1;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* This function counts the lane crossings of the segments of a result (B5 of MEAS0): the pairs of edges whose lane order flips.
|
|
144
|
+
* Along the route of each edge `e`, it takes the shared segments in order. On each one, another edge `f` is on the left or on the right of `e`, in the way of `e`.
|
|
145
|
+
*
|
|
146
|
+
* A pair `(e, f)` counts one time when the side of `f` changes from one shared segment of both edges to the next one.
|
|
147
|
+
* Lower is better. A result without shared segments gives 0. The cost is the sum of the squares of the edge counts of the shared segments.
|
|
148
|
+
*/
|
|
149
|
+
export function laneCrossings(graph: SegmentGraph): number {
|
|
150
|
+
const shared = new Map(graph.segments.filter((s) => s.count > 1).map((s) => [s.id, s] as const));
|
|
151
|
+
const flipped = new Set<string>();
|
|
152
|
+
for (const path of graph.paths) {
|
|
153
|
+
const side = new Map<string, number>();
|
|
154
|
+
for (const ref of path.segments) {
|
|
155
|
+
const s = shared.get(ref.segment);
|
|
156
|
+
if (!s) continue;
|
|
157
|
+
s.edges.forEach((f, lane) => {
|
|
158
|
+
if (f === path.edge) return;
|
|
159
|
+
const now = Math.sign(lane - ref.lane) * (ref.reverse ? -1 : 1);
|
|
160
|
+
const before = side.get(f);
|
|
161
|
+
if (before !== undefined && before !== now) flipped.add(path.edge < f ? `${path.edge}\u0000${f}` : `${f}\u0000${path.edge}`);
|
|
162
|
+
side.set(f, now);
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return flipped.size;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** This function adds the key of each cell of side `u` that the segment `ab` touches, with an exact walk over the cells (Amanatides and Woo, 1987). */
|
|
170
|
+
function touchCells(a: Point, b: Point, u: number, cells: Set<string>): void {
|
|
171
|
+
let i = Math.floor(a.x / u);
|
|
172
|
+
let j = Math.floor(a.y / u);
|
|
173
|
+
const iEnd = Math.floor(b.x / u);
|
|
174
|
+
const jEnd = Math.floor(b.y / u);
|
|
175
|
+
const dx = b.x - a.x;
|
|
176
|
+
const dy = b.y - a.y;
|
|
177
|
+
const sx = Math.sign(dx);
|
|
178
|
+
const sy = Math.sign(dy);
|
|
179
|
+
const deltaX = dx !== 0 ? u / Math.abs(dx) : Number.POSITIVE_INFINITY;
|
|
180
|
+
const deltaY = dy !== 0 ? u / Math.abs(dy) : Number.POSITIVE_INFINITY;
|
|
181
|
+
let maxX = dx !== 0 ? ((sx > 0 ? (i + 1) * u : i * u) - a.x) / dx : Number.POSITIVE_INFINITY;
|
|
182
|
+
let maxY = dy !== 0 ? ((sy > 0 ? (j + 1) * u : j * u) - a.y) / dy : Number.POSITIVE_INFINITY;
|
|
183
|
+
cells.add(`${i},${j}`);
|
|
184
|
+
while (i !== iEnd || j !== jEnd) {
|
|
185
|
+
// At a corner of four cells, the walk steps on both axes, so it does not touch the two side cells.
|
|
186
|
+
const moveX = j === jEnd || (i !== iEnd && maxX <= maxY);
|
|
187
|
+
const moveY = i === iEnd || (j !== jEnd && maxY <= maxX);
|
|
188
|
+
if (moveX) {
|
|
189
|
+
i += sx;
|
|
190
|
+
maxX += deltaX;
|
|
191
|
+
}
|
|
192
|
+
if (moveY) {
|
|
193
|
+
j += sy;
|
|
194
|
+
maxY += deltaY;
|
|
195
|
+
}
|
|
196
|
+
cells.add(`${i},${j}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* This function gives the occupied area of the routes (B7 of MEAS0): `moa = cells that S touches / cells that the straight lines touch`.
|
|
202
|
+
* The cells are squares of side `unit` from the origin, and `unit` is `u` of the board (`boardUnit`). The straight line of an edge joins the two ends of its route.
|
|
203
|
+
* The scale is a ratio, and lower is better. Routes without a straight line give 1. The cost is linear in the cells.
|
|
204
|
+
*/
|
|
205
|
+
export function occupiedArea(routes: readonly Route[], unit: number): number {
|
|
206
|
+
const u = unit > 0 ? unit : 1;
|
|
207
|
+
const drawn = new Set<string>();
|
|
208
|
+
const straight = new Set<string>();
|
|
209
|
+
for (const r of routes) {
|
|
210
|
+
const p = r.points;
|
|
211
|
+
if (p.length < 2) continue;
|
|
212
|
+
for (let i = 1; i < p.length; i++) touchCells(p[i - 1]!, p[i]!, u, drawn);
|
|
213
|
+
touchCells(p[0]!, p[p.length - 1]!, u, straight);
|
|
214
|
+
}
|
|
215
|
+
return straight.size > 0 ? drawn.size / straight.size : 1;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** The run profile of a result (B8 of MEAS0). */
|
|
219
|
+
export interface RunProfile {
|
|
220
|
+
/** The number of shared segments: the runs and the trunks of the bundles with two edges or more. */
|
|
221
|
+
readonly runs: number;
|
|
222
|
+
/** The median of the edges of each shared segment. It is 0 without a shared segment. */
|
|
223
|
+
readonly median: number;
|
|
224
|
+
/** The most edges of one shared segment. It is 0 without a shared segment. */
|
|
225
|
+
readonly max: number;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** This function gives the run profile of a result (B8 of MEAS0): the shared segments, and the median and the most edges of each. The cost is linear. */
|
|
229
|
+
export function runProfile(graph: SegmentGraph): RunProfile {
|
|
230
|
+
const counts = graph.segments.filter((s) => s.count > 1).map((s) => s.count);
|
|
231
|
+
if (counts.length === 0) return { runs: 0, median: 0, max: 0 };
|
|
232
|
+
return { runs: counts.length, median: quantile(counts, 0.5), max: Math.max(...counts) };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** The lane fidelity of a result (B9 of MEAS0). */
|
|
236
|
+
export interface LaneFidelity {
|
|
237
|
+
/** The median of `lf(e)` over the edges with a shared segment. */
|
|
238
|
+
readonly median: number;
|
|
239
|
+
/** The largest `lf(e)`. */
|
|
240
|
+
readonly max: number;
|
|
241
|
+
/** The number of edges with a shared segment. */
|
|
242
|
+
readonly edges: number;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* This function gives the lane fidelity (B9 of MEAS0): `lf(e) = frechet(lane(e), r_e) / (width · k / 2)` for each edge `e` with a shared segment.
|
|
247
|
+
* `lane(e)` is the lane route of `openLanes`, `r_e` is the route, and `k` is the edge count of the widest shared segment of `e`.
|
|
248
|
+
* `frechet` is the discrete Fréchet distance of the points. A value of 1 or less is correct, and lower is better.
|
|
249
|
+
* It gives `undefined` when no edge has a shared segment. The cost is quadratic in the points of each route.
|
|
250
|
+
*/
|
|
251
|
+
export function laneFidelity(routes: readonly Route[], open: OpenLanes): LaneFidelity | undefined {
|
|
252
|
+
const count = new Map(open.graph.segments.map((s) => [s.id, s.count] as const));
|
|
253
|
+
const lanes = new Map(open.lanes.map((r) => [r.edge, r.points] as const));
|
|
254
|
+
const own = new Map(routes.map((r) => [r.edge, r.points] as const));
|
|
255
|
+
const values: number[] = [];
|
|
256
|
+
for (const path of open.graph.paths) {
|
|
257
|
+
let k = 0;
|
|
258
|
+
for (const ref of path.segments) k = Math.max(k, count.get(ref.segment) ?? 0);
|
|
259
|
+
const lane = lanes.get(path.edge);
|
|
260
|
+
const route = own.get(path.edge);
|
|
261
|
+
if (k < 2 || !lane || !route || open.width <= 0) continue;
|
|
262
|
+
values.push(frechet(lane, route) / ((open.width * k) / 2));
|
|
263
|
+
}
|
|
264
|
+
if (values.length === 0) return undefined;
|
|
265
|
+
return { median: quantile(values, 0.5), max: Math.max(...values), edges: values.length };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* This function counts the lane routes that go through a box that is not one of their ends (M4 of MEAS0).
|
|
270
|
+
* It is the count `laneThrough` of the room bench (`measureBoard`), for the lanes of any result. Lower is better. The cost is `n log n`.
|
|
271
|
+
*/
|
|
272
|
+
export function lanesThrough(graph: Graph, open: OpenLanes): number {
|
|
273
|
+
return measureRouteFaults(graph, { routes: open.lanes }).through;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* This function gives the area of a result (M1 of MEAS0) for a router: the growth of the board that the open lanes need.
|
|
278
|
+
* The value is the area of the bounding box of the boxes and the lane routes. The function divides it by the area of the bounding box of the boxes.
|
|
279
|
+
*
|
|
280
|
+
* It is the room form `area(open) / area(packed)` with the boxes as they are. The scale is a ratio of 1 or more, and lower is better.
|
|
281
|
+
* A board without area gives 1. The cost is linear.
|
|
282
|
+
*/
|
|
283
|
+
export function laneArea(graph: Graph, open: OpenLanes): number {
|
|
284
|
+
let x0 = Number.POSITIVE_INFINITY;
|
|
285
|
+
let y0 = Number.POSITIVE_INFINITY;
|
|
286
|
+
let x1 = Number.NEGATIVE_INFINITY;
|
|
287
|
+
let y1 = Number.NEGATIVE_INFINITY;
|
|
288
|
+
for (const n of graph.nodes) {
|
|
289
|
+
x0 = Math.min(x0, n.box.x);
|
|
290
|
+
y0 = Math.min(y0, n.box.y);
|
|
291
|
+
x1 = Math.max(x1, n.box.x + n.box.w);
|
|
292
|
+
y1 = Math.max(y1, n.box.y + n.box.h);
|
|
293
|
+
}
|
|
294
|
+
const packed = (x1 - x0) * (y1 - y0);
|
|
295
|
+
if (!(packed > 0)) return 1;
|
|
296
|
+
for (const r of open.lanes)
|
|
297
|
+
for (const p of r.points) {
|
|
298
|
+
x0 = Math.min(x0, p.x);
|
|
299
|
+
y0 = Math.min(y0, p.y);
|
|
300
|
+
x1 = Math.max(x1, p.x);
|
|
301
|
+
y1 = Math.max(y1, p.y);
|
|
302
|
+
}
|
|
303
|
+
return ((x1 - x0) * (y1 - y0)) / packed;
|
|
304
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { center, dist } from "../../geom.ts";
|
|
2
|
-
import type { Graph, Level } from "../../model.ts";
|
|
1
|
+
import { center, dist, pointBoxDistance, polylineLength, type Box, type Point } from "../../geom.ts";
|
|
2
|
+
import type { Graph, Level, Route } from "../../model.ts";
|
|
3
3
|
import { kendallTau } from "./stats.ts";
|
|
4
4
|
|
|
5
5
|
/** This function gives the levels in the order of their depth, from the far view to the near view. */
|
|
@@ -101,3 +101,87 @@ export function orderStability(graph: Graph, levels: readonly Level[]): number {
|
|
|
101
101
|
}
|
|
102
102
|
return n ? sum / n : 1;
|
|
103
103
|
}
|
|
104
|
+
|
|
105
|
+
/** This function gives the points of a polyline at most `step` apart: each vertex, and evenly spaced points on each longer segment. */
|
|
106
|
+
function samplePolyline(p: readonly Point[], step: number): Point[] {
|
|
107
|
+
const out: Point[] = [];
|
|
108
|
+
for (let i = 0; i < p.length; i++) {
|
|
109
|
+
const b = p[i]!;
|
|
110
|
+
if (i > 0) {
|
|
111
|
+
const a = p[i - 1]!;
|
|
112
|
+
const n = Math.ceil(dist(a, b) / step);
|
|
113
|
+
for (let k = 1; k < n; k++) out.push({ x: a.x + ((b.x - a.x) * k) / n, y: a.y + ((b.y - a.y) * k) / n });
|
|
114
|
+
}
|
|
115
|
+
out.push(b);
|
|
116
|
+
}
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** This function gives the Hausdorff distance between two sets of points. Both sets have one point or more. */
|
|
121
|
+
function hausdorff(a: readonly Point[], b: readonly Point[]): number {
|
|
122
|
+
const directed = (p: readonly Point[], q: readonly Point[]): number => {
|
|
123
|
+
let worst = 0;
|
|
124
|
+
for (const x of p) {
|
|
125
|
+
let best = Number.POSITIVE_INFINITY;
|
|
126
|
+
for (const y of q) best = Math.min(best, dist(x, y));
|
|
127
|
+
worst = Math.max(worst, best);
|
|
128
|
+
}
|
|
129
|
+
return worst;
|
|
130
|
+
};
|
|
131
|
+
return Math.max(directed(a, b), directed(b, a));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** The number of points of the sample of one polyline for the link agreement, at most. */
|
|
135
|
+
const AGREEMENT_SAMPLES = 64;
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* This function gives the link agreement of the levels with groups (Z9 of MEAS0).
|
|
139
|
+
* `la = mean over links of hausdorff(link, routes of its edges outside the two items) / u`, with `u` the argument `unit`.
|
|
140
|
+
* The edges of a link are the edges between a node of one item and a node of the other item, in either direction.
|
|
141
|
+
*
|
|
142
|
+
* The function samples each polyline at points `u / 4` apart, with at most 64 points for each polyline. It keeps the points outside the boxes of the two items.
|
|
143
|
+
* A link without such points on both sides does not count. It gives `undefined` when no link counts.
|
|
144
|
+
* The value has no unit, and lower is better. The cost is quadratic in the points of each link.
|
|
145
|
+
*/
|
|
146
|
+
export function linkAgreement(graph: Graph, levels: readonly Level[], routes: readonly Route[], unit: number): number | undefined {
|
|
147
|
+
const u = unit > 0 ? unit : 1;
|
|
148
|
+
const endsOf = new Map(graph.edges.map((e) => [e.id, [e.source.node, e.target.node] as const]));
|
|
149
|
+
const sample = (p: readonly Point[], boxes: readonly Box[]): Point[] => {
|
|
150
|
+
const step = Math.max(u / 4, polylineLength(p) / AGREEMENT_SAMPLES);
|
|
151
|
+
return samplePolyline(p, step).filter((q) => boxes.every((b) => pointBoxDistance(q, b) > 0));
|
|
152
|
+
};
|
|
153
|
+
let sum = 0;
|
|
154
|
+
let n = 0;
|
|
155
|
+
for (const level of levels) {
|
|
156
|
+
if (!level.items.some((i) => i.nodes.length > 1)) continue;
|
|
157
|
+
const itemOf = itemIndex(level);
|
|
158
|
+
const indexOf = new Map(level.items.map((it, i) => [it.id, i] as const));
|
|
159
|
+
// The edges between each pair of items, by the key of the pair.
|
|
160
|
+
const between = new Map<string, Route[]>();
|
|
161
|
+
for (const r of routes) {
|
|
162
|
+
const ends = endsOf.get(r.edge);
|
|
163
|
+
if (!ends) continue;
|
|
164
|
+
const i = itemOf.get(ends[0]);
|
|
165
|
+
const j = itemOf.get(ends[1]);
|
|
166
|
+
if (i === undefined || j === undefined || i === j) continue;
|
|
167
|
+
const key = i < j ? `${i},${j}` : `${j},${i}`;
|
|
168
|
+
const list = between.get(key);
|
|
169
|
+
if (list) list.push(r);
|
|
170
|
+
else between.set(key, [r]);
|
|
171
|
+
}
|
|
172
|
+
for (const link of level.links) {
|
|
173
|
+
const i = indexOf.get(link.source);
|
|
174
|
+
const j = indexOf.get(link.target);
|
|
175
|
+
if (i === undefined || j === undefined || i === j) continue;
|
|
176
|
+
const mine = between.get(i < j ? `${i},${j}` : `${j},${i}`);
|
|
177
|
+
if (!mine) continue;
|
|
178
|
+
const boxes = [level.items[i]!.box, level.items[j]!.box];
|
|
179
|
+
const a = sample(link.points, boxes);
|
|
180
|
+
const b = mine.flatMap((r) => sample(r.points, boxes));
|
|
181
|
+
if (a.length === 0 || b.length === 0) continue;
|
|
182
|
+
sum += hausdorff(a, b) / u;
|
|
183
|
+
n++;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return n > 0 ? sum / n : undefined;
|
|
187
|
+
}
|
|
@@ -47,8 +47,8 @@ export interface LayoutBox {
|
|
|
47
47
|
readonly parent?: string;
|
|
48
48
|
/** The group of the box. */
|
|
49
49
|
readonly group?: string;
|
|
50
|
-
/** This value is `true` when the box is an obstacle: the packer keeps its `x` and `y`. The router ignores it. */
|
|
51
|
-
readonly locked?:
|
|
50
|
+
/** This value is `true` when the box is an obstacle: the packer keeps its `x` and `y`. `false` is the same as no value. The router ignores it. */
|
|
51
|
+
readonly locked?: boolean;
|
|
52
52
|
/** The reading order that the caller wants. The router ignores it. */
|
|
53
53
|
readonly rank?: number;
|
|
54
54
|
readonly ports?: readonly LayoutPort[];
|
|
@@ -87,7 +87,7 @@ export interface LayoutGenerator {
|
|
|
87
87
|
* The packer reads the first six dimensions. The router reads the last four.
|
|
88
88
|
*/
|
|
89
89
|
export interface LayoutCell {
|
|
90
|
-
readonly containerSize?: "fixed" | "freeWidth" | "freeAspect";
|
|
90
|
+
readonly containerSize?: "fixed" | "freeHeight" | "freeWidth" | "freeAspect" | "freeArea";
|
|
91
91
|
readonly containerShape?: "rect" | "convex" | "polygon" | "circle" | "ellipse" | "holes";
|
|
92
92
|
readonly obstacles?: "none" | "center" | "scattered" | "wall" | "corridors" | "touching" | "edge";
|
|
93
93
|
readonly sizes?: "identical" | "uniform" | "heavyTail" | "bimodal" | "extremeAspect" | "nearIdentical";
|
|
@@ -291,19 +291,25 @@ export function regionBounds(region: Region): { x: number; y: number; w: number;
|
|
|
291
291
|
return { x, y, w: Math.max(...xs) - x, h: Math.max(...ys) - y };
|
|
292
292
|
}
|
|
293
293
|
|
|
294
|
+
/**
|
|
295
|
+
* The fields of an input that `inputGraph` reads. A contract input, for example the result of `applyPack`, has these fields.
|
|
296
|
+
* Without `edges`, the input has no edges.
|
|
297
|
+
*/
|
|
298
|
+
export type RouteInput = Pick<LayoutInput, "id" | "boxes" | "obstacles"> & { readonly edges?: LayoutInput["edges"] };
|
|
299
|
+
|
|
294
300
|
/**
|
|
295
301
|
* This function reads the graph of the router from an input file. The router reads the positions, the ports, the edges and the data.
|
|
296
302
|
* Each obstacle becomes a node without edges, with the bounding box of its region, after the boxes.
|
|
297
303
|
* The router ignores the other fields of the packer: `container`, `gap`, `locked`, `rank`, the `shape` of a box and the `move` of an obstacle.
|
|
298
304
|
* A box without `x` and `y` is an error.
|
|
299
305
|
*/
|
|
300
|
-
export function inputGraph(input:
|
|
306
|
+
export function inputGraph(input: RouteInput): Graph {
|
|
301
307
|
const nodes = input.boxes.map((b): Node => {
|
|
302
308
|
if (b.x === undefined || b.y === undefined) throw new Error(`The box "${b.id}" of the input "${input.id}" has no position. The router needs a placed input.`);
|
|
303
309
|
return { id: b.id, box: { x: b.x, y: b.y, w: b.w, h: b.h }, ...(b.ports ? { ports: b.ports.map(portToModel) } : {}), ...(b.data ? { data: b.data } : {}) };
|
|
304
310
|
});
|
|
305
311
|
for (const o of input.obstacles ?? []) nodes.push({ id: o.id, box: regionBounds(o.region) });
|
|
306
|
-
const edges = input.edges.map(
|
|
312
|
+
const edges = (input.edges ?? []).map(
|
|
307
313
|
(e): Edge => ({
|
|
308
314
|
id: e.id,
|
|
309
315
|
source: { node: e.source, ...(e.sourcePort !== undefined ? { port: e.sourcePort } : {}) },
|