@mark1russell7/highways 0.4.1 → 0.4.2
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 +27 -1
- package/build/index.d.ts +642 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +5189 -2609
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/bench/measures/highways.ts +93 -0
- package/src/bench/measures/index.ts +1 -0
- package/src/bench/screen/measures.ts +5 -0
- package/src/bench/screen/screen.ts +14 -0
- package/src/highways/base.ts +126 -0
- package/src/highways/flow-field/index.ts +49 -0
- package/src/highways/gates/ambiguity/index.ts +110 -0
- package/src/highways/gates/breakeven/index.ts +62 -0
- package/src/highways/gates/hysteresis/index.ts +48 -0
- package/src/highways/gates/ids/index.ts +59 -0
- package/src/highways/gates/index.ts +6 -0
- package/src/highways/gates/model.ts +129 -0
- package/src/highways/gates/stretch/index.ts +39 -0
- package/src/highways/grouped.ts +123 -0
- package/src/highways/index.ts +15 -0
- package/src/highways/level-links/index.ts +114 -0
- package/src/highways/meet/index.ts +84 -0
- package/src/highways/model.ts +5 -0
- package/src/highways/od-cells/index.ts +50 -0
- package/src/highways/quickbundles/index.ts +117 -0
- package/src/highways/road-classes/index.ts +74 -0
- package/src/highways/router.ts +1 -1
- package/src/highways/select/index.ts +162 -0
- package/src/highways/stretches.ts +141 -0
- package/src/highways/stubs/index.ts +117 -0
- package/src/highways/weiszfeld/index.ts +105 -0
- package/src/highways/wspd/index.ts +130 -0
- package/src/highways/zipper/index.ts +168 -0
- package/src/strategies/builtins.ts +149 -7
- package/src/strategies/schemas.ts +120 -0
package/package.json
CHANGED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { follow } from "../../highways/follow.ts";
|
|
2
|
+
import { commonEdges, matchHighways } from "../../highways/gates/model.ts";
|
|
3
|
+
import type { HighwayPlan } from "../../highways/model.ts";
|
|
4
|
+
import { wayOptionsOf } from "../../highways/router.ts";
|
|
5
|
+
import type { Graph } from "../../model.ts";
|
|
6
|
+
import type { Strategy } from "../../strategies/model.ts";
|
|
7
|
+
import { changeGraph, routeStability } from "./stability.ts";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* This function gives the churn of the highways over one edit (Z10 for highways, from HWY-046). Two edges are a pair when they share a highway.
|
|
11
|
+
* The churn is the share of the pairs that come or go: `|P ∆ P'| / |P ∪ P'|`. Only the edges of `edges` count, for example the edges of both graphs.
|
|
12
|
+
* Two plans without pairs give 0. The scale is 0 to 1, and lower is better.
|
|
13
|
+
*/
|
|
14
|
+
export function highwayChurn(before: HighwayPlan, after: HighwayPlan, edges: ReadonlySet<string>): number {
|
|
15
|
+
const pairs = (plan: HighwayPlan): Set<string> => {
|
|
16
|
+
const out = new Set<string>();
|
|
17
|
+
for (const h of plan.highways) {
|
|
18
|
+
const list = [...new Set(h.members.map((m) => m.edge).filter((e) => edges.has(e)))].sort();
|
|
19
|
+
for (let i = 0; i < list.length; i++) for (let j = i + 1; j < list.length; j++) out.add(`${list[i]}\u0000${list[j]}`);
|
|
20
|
+
}
|
|
21
|
+
return out;
|
|
22
|
+
};
|
|
23
|
+
const a = pairs(before);
|
|
24
|
+
const b = pairs(after);
|
|
25
|
+
let common = 0;
|
|
26
|
+
for (const p of a) if (b.has(p)) common++;
|
|
27
|
+
const all = a.size + b.size - common;
|
|
28
|
+
return all === 0 ? 0 : (all - common) / all;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The match of the highways over one edit. */
|
|
32
|
+
export interface HighwayIdKeep {
|
|
33
|
+
/** The number of new highways with a match in the old plan: a Jaccard index of their edges of 0.5 or more. */
|
|
34
|
+
readonly matched: number;
|
|
35
|
+
/** The number of matched highways that keep the id of their match. */
|
|
36
|
+
readonly kept: number;
|
|
37
|
+
/** `kept / matched`, or `NaN` without a match. The scale is 0 to 1, and higher is better. */
|
|
38
|
+
readonly share: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* This function tells how many highways keep their id over one edit (HWY-054). It matches the new highways with the old ones by `matchHighways`, with the index 0.5.
|
|
43
|
+
* A matched highway keeps its id when its id is the id of its match. Only the edges of `edges` count.
|
|
44
|
+
*/
|
|
45
|
+
export function highwayIdKeep(before: HighwayPlan, after: HighwayPlan, edges: ReadonlySet<string>): HighwayIdKeep {
|
|
46
|
+
const match = matchHighways(before, after, 0.5, edges);
|
|
47
|
+
let kept = 0;
|
|
48
|
+
for (const [i, i0] of match) if (after.highways[i]!.id === before.highways[i0]!.id) kept++;
|
|
49
|
+
return { matched: match.size, kept, share: match.size === 0 ? Number.NaN : kept / match.size };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The stability of a highway strategy over a seeded sequence of edits. Each value is the mean over the edits. */
|
|
53
|
+
export interface HighwayEdits {
|
|
54
|
+
/** The mean Fréchet distance of the routes over one edit, in units of the board (Z10). */
|
|
55
|
+
readonly mean: number;
|
|
56
|
+
/** The share of the routes that move over one edit (Z10). */
|
|
57
|
+
readonly moved: number;
|
|
58
|
+
/** The churn of the highways over one edit (`highwayChurn`). */
|
|
59
|
+
readonly churn: number;
|
|
60
|
+
/** The share of the matched highways that keep their id over one edit (`highwayIdKeep`), or `NaN` without a match. */
|
|
61
|
+
readonly ids: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* This function measures a highway strategy over a sequence of `edits` seeded edits. Each edit is the small change of `changeGraph` with the seed `seed + i`.
|
|
66
|
+
* The strategy gets the graph and its plan before the edit as `previous`, so a strategy with memory can keep its highways.
|
|
67
|
+
* `follow` routes each plan with the way options of the strategy. The same graph, strategy, options and seed give the same values.
|
|
68
|
+
*/
|
|
69
|
+
export function highwayEdits(graph: Graph, strategy: Strategy<"highway", object>, options: object | undefined, edits: number, seed = 1): HighwayEdits {
|
|
70
|
+
const way = wayOptionsOf({ ...strategy.defaults, ...options });
|
|
71
|
+
let g = graph;
|
|
72
|
+
let plan = strategy.run({ graph: g }, options);
|
|
73
|
+
let routes = follow(g, plan, way);
|
|
74
|
+
const mean: number[] = [];
|
|
75
|
+
const moved: number[] = [];
|
|
76
|
+
const churn: number[] = [];
|
|
77
|
+
const ids: number[] = [];
|
|
78
|
+
for (let i = 0; i < edits; i++) {
|
|
79
|
+
const next = changeGraph(g, seed + i).graph;
|
|
80
|
+
const p = strategy.run({ graph: next, previous: { graph: g, plan } }, options);
|
|
81
|
+
const r = follow(next, p, way);
|
|
82
|
+
const s = routeStability(g, routes, r, { seed });
|
|
83
|
+
const edges = commonEdges(g, next);
|
|
84
|
+
mean.push(s.mean);
|
|
85
|
+
moved.push(s.moved);
|
|
86
|
+
churn.push(highwayChurn(plan, p, edges));
|
|
87
|
+
const keep = highwayIdKeep(plan, p, edges);
|
|
88
|
+
if (keep.matched > 0) ids.push(keep.share);
|
|
89
|
+
[g, plan, routes] = [next, p, r];
|
|
90
|
+
}
|
|
91
|
+
const avg = (v: readonly number[]): number => (v.length === 0 ? Number.NaN : v.reduce((a, b) => a + b, 0) / v.length);
|
|
92
|
+
return { mean: avg(mean), moved: avg(moved), churn: avg(churn), ids: avg(ids) };
|
|
93
|
+
}
|
|
@@ -68,6 +68,11 @@ const ROWS: readonly Row[] = [
|
|
|
68
68
|
["Z10.base.mean", "Z10", "route.stability.base.mean", "units of the board", "lower", false],
|
|
69
69
|
["Z10.base.max", "Z10", "route.stability.base.max", "units of the board", "lower", false],
|
|
70
70
|
["Z10.base.moved", "Z10", "route.stability.base.moved", "0 to 1", "lower", false],
|
|
71
|
+
// The stability of a highway strategy over a sequence of seeded edits, with its plan before each edit (the screen option `edits`).
|
|
72
|
+
["Z10.edits.mean", "Z10", "route.stability.edits.mean", "units of the board", "lower", false],
|
|
73
|
+
["Z10.edits.moved", "Z10", "route.stability.edits.moved", "0 to 1", "lower", false],
|
|
74
|
+
["Z10.churn", "Z10", "route.stability.highway-churn", "0 to 1", "lower", false],
|
|
75
|
+
["Z10.ids", "Z10", "route.stability.highway-ids", "0 to 1", "higher", false],
|
|
71
76
|
// The zoom levels and the groups. The default grouping rule reads the positions, so most of these measures depend on the placement.
|
|
72
77
|
["level-nesting", "Z6", "route.level.nesting", "count", "lower", false],
|
|
73
78
|
["Z3.through", "Z3", "route.level.through", "count", "lower", true],
|
|
@@ -6,6 +6,7 @@ import { STRATEGIES } from "../../strategies/builtins.ts";
|
|
|
6
6
|
import { asRouter, createRegistry, getStrategy, listStrategies, register, STEPS, type AnyStrategy, type Registry, type Strategy } from "../../strategies/model.ts";
|
|
7
7
|
import { compareStep, type CompareRow, type CompareStep, type StepComparison } from "../compare.ts";
|
|
8
8
|
import { bundledShare, bundleStretch, mergeAngle } from "../measures/bundles.ts";
|
|
9
|
+
import { highwayEdits } from "../measures/highways.ts";
|
|
9
10
|
import { orderFragility } from "../measures/order.ts";
|
|
10
11
|
import { paretoFront, type Objective } from "../measures/pareto.ts";
|
|
11
12
|
import { routeStability } from "../measures/stability.ts";
|
|
@@ -84,6 +85,12 @@ export interface ScreenOptions {
|
|
|
84
85
|
* The default is 0: no measure of the order fragility. The steps `group` and `curve` do not read it.
|
|
85
86
|
*/
|
|
86
87
|
readonly shuffles?: number;
|
|
88
|
+
/**
|
|
89
|
+
* The number of seeded edits of the step `highway` (BUILD-W2A). Each highway strategy runs again after each edit of `highwayEdits`, with its plan before the edit.
|
|
90
|
+
* The records then get `Z10.edits.*`, the churn of the highways and the share of the highways that keep their id.
|
|
91
|
+
* The default is 0: no edits. The other steps and the base routers do not read it.
|
|
92
|
+
*/
|
|
93
|
+
readonly edits?: number;
|
|
87
94
|
/** The commit of the code, for the records. */
|
|
88
95
|
readonly commit?: string;
|
|
89
96
|
/** The predictions of the run. Each record names the first prediction with its strategy, its set and its input. */
|
|
@@ -295,6 +302,8 @@ export function screen(step: ScreenStep, strategies: readonly (Strategy<ScreenSt
|
|
|
295
302
|
}
|
|
296
303
|
const shuffles = givesRoutes(step) ? Math.max(0, Math.floor(options.shuffles ?? 0)) : 0;
|
|
297
304
|
const seed = options.seed ?? 1;
|
|
305
|
+
const edits = step === "highway" ? Math.max(0, Math.floor(options.edits ?? 0)) : 0;
|
|
306
|
+
const plans = new Map(listStrategies(screened.registry, "highway").map((s) => [s.name, s]));
|
|
298
307
|
const log = options.log ?? ((): void => {});
|
|
299
308
|
const families = options.families;
|
|
300
309
|
const inputs = (options.inputs ?? screenInputs(set)).filter((i) => families?.includes(i.family) ?? true);
|
|
@@ -346,6 +355,11 @@ export function screen(step: ScreenStep, strategies: readonly (Strategy<ScreenSt
|
|
|
346
355
|
const f = orderFragility(graph, (g) => router.route(g), { orders: shuffles, seed, base: row.result });
|
|
347
356
|
Object.assign(values, { "W3.crossings": f.crossings, "W3.detour": f.detour, "W3.inkSaved": f.inkSaved, "W3.ink": f.ink, "W3.changed": f.changed, "W3.poa": f.poa });
|
|
348
357
|
}
|
|
358
|
+
const plan = edits > 0 && row.result ? plans.get(row.strategy) : undefined;
|
|
359
|
+
if (plan) {
|
|
360
|
+
const z = highwayEdits(graph, plan, own[row.strategy], edits, seed);
|
|
361
|
+
Object.assign(values, { "Z10.edits.mean": z.mean, "Z10.edits.moved": z.moved, "Z10.churn": z.churn, "Z10.ids": z.ids });
|
|
362
|
+
}
|
|
349
363
|
const record = { ...head, measures: named(values), hard, tolerance: { through: TOLERANCE }, deterministic, ms: row.ms, ...tail };
|
|
350
364
|
records.push(record);
|
|
351
365
|
if (givesRoutes(step) && input.base !== undefined && row.result) variants.push({ record, base: input.base, result: row.result });
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { dist, type Point } from "../geom.ts";
|
|
2
|
+
import type { Graph, Result, Route } from "../model.ts";
|
|
3
|
+
import { byCollection } from "../routers/trunks.ts";
|
|
4
|
+
import { CORRIDOR_DEFAULTS, corridorPlan } from "./corridors/index.ts";
|
|
5
|
+
import { follow } from "./follow.ts";
|
|
6
|
+
import type { Highway, HighwayInput, HighwayMember, HighwayPlan } from "./model.ts";
|
|
7
|
+
import { sharedWaysPlan } from "./shared-ways/index.ts";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The plan that a post step changes. `none` is the empty plan: `follow` then routes each edge as the router `corners` does.
|
|
11
|
+
* `shared-ways` and `corridors` are the plans of those highway strategies, with their defaults.
|
|
12
|
+
*/
|
|
13
|
+
export type HighwayBase = "none" | "shared-ways" | "corridors";
|
|
14
|
+
|
|
15
|
+
/** The values of `HighwayBase`. */
|
|
16
|
+
export const HIGHWAY_BASES: readonly HighwayBase[] = Object.freeze(["none", "shared-ways", "corridors"]);
|
|
17
|
+
|
|
18
|
+
/** This function gives the plan of a base. */
|
|
19
|
+
export function basePlan(input: HighwayInput, base: HighwayBase): HighwayPlan {
|
|
20
|
+
if (base === "shared-ways") return sharedWaysPlan(input, { rule: byCollection });
|
|
21
|
+
if (base === "corridors") return corridorPlan(input, CORRIDOR_DEFAULTS);
|
|
22
|
+
return { highways: [] };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** This function gives the routes of a plan after `follow`, with the default way finder. The plan `none` gives the first routes of the edges. */
|
|
26
|
+
export function planRoutes(graph: Graph, plan: HighwayPlan): Result {
|
|
27
|
+
return follow(graph, plan);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** This function gives the first route of each edge: the route of `input.routes`, or the route of the empty plan after `follow`. */
|
|
31
|
+
export function firstRoutes(input: HighwayInput): Map<string, readonly Point[]> {
|
|
32
|
+
const given = routeIndex(input.routes?.routes ?? []);
|
|
33
|
+
const known = new Set(input.graph.edges.map((e) => e.id));
|
|
34
|
+
if (input.graph.edges.every((e) => given.has(e.id))) return given;
|
|
35
|
+
const made = routeIndex(planRoutes(input.graph, { highways: [] }).routes);
|
|
36
|
+
for (const [edge, points] of given) if (known.has(edge)) made.set(edge, points);
|
|
37
|
+
return made;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** One stop of the trip of an edge: the index of the highway in the plan, and the member. */
|
|
41
|
+
export interface TripStop {
|
|
42
|
+
readonly h: number;
|
|
43
|
+
readonly m: HighwayMember;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** This function gives the trip of each edge of a plan. The trip is its highways in the order of `seq`, then in the order of the plan. */
|
|
47
|
+
export function tripsOf(plan: HighwayPlan): Map<string, TripStop[]> {
|
|
48
|
+
const trips = new Map<string, TripStop[]>();
|
|
49
|
+
plan.highways.forEach((h, hi) => {
|
|
50
|
+
for (const m of h.members) {
|
|
51
|
+
const list = trips.get(m.edge);
|
|
52
|
+
if (list) list.push({ h: hi, m });
|
|
53
|
+
else trips.set(m.edge, [{ h: hi, m }]);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
for (const list of trips.values()) list.sort((a, b) => a.m.seq - b.m.seq || a.h - b.h);
|
|
57
|
+
return trips;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* This function gives a plan with the same trips, and with `seq` from 0 to the number of highways of the edge less 1.
|
|
62
|
+
* A post step can give `seq` values with gaps or below 0, and this function makes them whole again.
|
|
63
|
+
* It also drops each highway with fewer than two members, and gives the highways new ids in order.
|
|
64
|
+
*/
|
|
65
|
+
export function tidyPlan(plan: HighwayPlan): HighwayPlan {
|
|
66
|
+
const kept = plan.highways.filter((h) => h.members.length >= 2);
|
|
67
|
+
const seq = new Map<string, number>();
|
|
68
|
+
for (const list of tripsOf({ highways: kept }).values()) list.forEach((stop, i) => seq.set(`${stop.h}\u0000${stop.m.edge}`, i));
|
|
69
|
+
const highways: Highway[] = kept.map((h, hi) => ({
|
|
70
|
+
...h,
|
|
71
|
+
id: `h${hi}`,
|
|
72
|
+
lanes: h.lanes.filter((e) => h.members.some((m) => m.edge === e)),
|
|
73
|
+
members: h.members.map((m) => ({ ...m, seq: seq.get(`${hi}\u0000${m.edge}`) ?? 0 })),
|
|
74
|
+
}));
|
|
75
|
+
return { highways };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** This function gives the place along a polyline (as a length from its first point) of the point of the polyline nearest to `p`. */
|
|
79
|
+
export function arcPlace(line: readonly Point[], p: Point): number {
|
|
80
|
+
let best = Infinity;
|
|
81
|
+
let at = 0;
|
|
82
|
+
let walked = 0;
|
|
83
|
+
for (let i = 1; i < line.length; i++) {
|
|
84
|
+
const a = line[i - 1]!;
|
|
85
|
+
const b = line[i]!;
|
|
86
|
+
const len = dist(a, b);
|
|
87
|
+
const u = len > 0 ? Math.max(0, Math.min(1, ((p.x - a.x) * (b.x - a.x) + (p.y - a.y) * (b.y - a.y)) / (len * len))) : 0;
|
|
88
|
+
const d = dist(p, { x: a.x + (b.x - a.x) * u, y: a.y + (b.y - a.y) * u });
|
|
89
|
+
if (d < best) [best, at] = [d, walked + u * len];
|
|
90
|
+
walked += len;
|
|
91
|
+
}
|
|
92
|
+
return at;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** This function gives the place on a spine (the index of a point plus the share of the next leg) of the point of the spine nearest to `p`. */
|
|
96
|
+
export function nearestPlace(spine: readonly Point[], p: Point): number {
|
|
97
|
+
let best = Infinity;
|
|
98
|
+
let at = 0;
|
|
99
|
+
for (let i = 1; i < spine.length; i++) {
|
|
100
|
+
const a = spine[i - 1]!;
|
|
101
|
+
const b = spine[i]!;
|
|
102
|
+
const len2 = (b.x - a.x) ** 2 + (b.y - a.y) ** 2;
|
|
103
|
+
const u = len2 > 0 ? Math.max(0, Math.min(1, ((p.x - a.x) * (b.x - a.x) + (p.y - a.y) * (b.y - a.y)) / len2)) : 0;
|
|
104
|
+
const d = dist(p, { x: a.x + (b.x - a.x) * u, y: a.y + (b.y - a.y) * u });
|
|
105
|
+
if (d < best) [best, at] = [d, i - 1 + u];
|
|
106
|
+
}
|
|
107
|
+
return at;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* This function gives a plan where the `seq` of each member follows the place of its merge point along the route of its edge.
|
|
112
|
+
* A post step that adds highways to the trips of edges uses it. Equal places keep the order of `seq`.
|
|
113
|
+
*/
|
|
114
|
+
export function orderAlongRoutes(plan: HighwayPlan, routes: ReadonlyMap<string, readonly Point[]>): HighwayPlan {
|
|
115
|
+
const rank = new Map<string, number>();
|
|
116
|
+
for (const [edge, list] of tripsOf(plan)) {
|
|
117
|
+
const route = routes.get(edge);
|
|
118
|
+
const placed = list.map((stop, i) => ({ stop, i, at: route ? arcPlace(route, stop.m.merge) : i }));
|
|
119
|
+
placed.sort((a, b) => a.at - b.at || a.i - b.i);
|
|
120
|
+
placed.forEach((p, k) => rank.set(`${p.stop.h}\u0000${edge}`, k));
|
|
121
|
+
}
|
|
122
|
+
return { highways: plan.highways.map((h, hi) => ({ ...h, members: h.members.map((m) => ({ ...m, seq: rank.get(`${hi}\u0000${m.edge}`) ?? m.seq })) })) };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** This function gives the route of each edge by its id. */
|
|
126
|
+
export const routeIndex = (routes: readonly Route[]): Map<string, readonly Point[]> => new Map(routes.map((r) => [r.edge, r.points]));
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { Point } from "../../geom.ts";
|
|
2
|
+
import { WayFinder, type WayOptions } from "../../ways.ts";
|
|
3
|
+
import type { HighwayInput, HighwayPlan } from "../model.ts";
|
|
4
|
+
import { edgePlugs, sharedStretches, type KeyedWay } from "../stretches.ts";
|
|
5
|
+
|
|
6
|
+
/** The end of the edges that gets a field: the target of a fan-in, or the source of a fan-out. */
|
|
7
|
+
export type FieldEnd = "target" | "source";
|
|
8
|
+
|
|
9
|
+
/** The options of the highway strategy `flow-field` (EDA-046 with HWY-037 of the notes). */
|
|
10
|
+
export interface FlowFieldOptions extends Pick<WayOptions, "margin" | "k"> {
|
|
11
|
+
/** An end with this number of edges or more on one side of its box gets a field. The default is 2. */
|
|
12
|
+
readonly minFanIn: number;
|
|
13
|
+
/** The end that gets a field. The default is `target`. */
|
|
14
|
+
readonly end: FieldEnd;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** The default options of the strategy `flow-field`. */
|
|
18
|
+
export const FLOW_FIELD_DEFAULTS: Readonly<Required<Omit<FlowFieldOptions, "margin" | "k">>> = Object.freeze({ minFanIn: 2, end: "target" });
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* This function gives the highways of a flow field for each target (EDA-046, HWY-037).
|
|
22
|
+
* The edges into one side of one box make a fan. The root of the fan is the point out of the median plug of the fan.
|
|
23
|
+
* Each edge of the fan goes the shortest way from the point out of its other plug to the root, through the corner graph.
|
|
24
|
+
* The shortest ways to one root make a tree, so they merge like streams and do not cross. Each stretch that two ways share is one highway.
|
|
25
|
+
* With the end `source`, the same holds for the edges out of one side of one box.
|
|
26
|
+
*/
|
|
27
|
+
export function flowFieldPlan(input: HighwayInput, o: Readonly<FlowFieldOptions>): HighwayPlan {
|
|
28
|
+
const finder = new WayFinder(input.graph.nodes, { ...(o.margin === undefined ? {} : { margin: o.margin }), ...(o.k === undefined ? {} : { k: o.k }) });
|
|
29
|
+
const fans = new Map<string, { edge: string; root: Point; far: Point; along: number }[]>();
|
|
30
|
+
for (const { edge, ps, pt } of edgePlugs(input.graph)) {
|
|
31
|
+
const [near, far, node] = o.end === "target" ? [pt, ps, edge.target.node] : [ps, pt, edge.source.node];
|
|
32
|
+
const key = `${node}\u0000${near.normal.x},${near.normal.y}`;
|
|
33
|
+
const item = { edge: edge.id, root: finder.out(near), far: finder.out(far), along: near.normal.x === 0 ? near.point.x : near.point.y };
|
|
34
|
+
const list = fans.get(key);
|
|
35
|
+
if (list) list.push(item);
|
|
36
|
+
else fans.set(key, [item]);
|
|
37
|
+
}
|
|
38
|
+
const ways: KeyedWay[] = [];
|
|
39
|
+
for (const [key, fan] of fans) {
|
|
40
|
+
if (fan.length < Math.max(2, o.minFanIn)) continue;
|
|
41
|
+
const sorted = [...fan].sort((a, b) => a.along - b.along || (a.edge < b.edge ? -1 : a.edge > b.edge ? 1 : 0));
|
|
42
|
+
const root = sorted[Math.floor((sorted.length - 1) / 2)]!.root;
|
|
43
|
+
for (const f of fan) {
|
|
44
|
+
const way = finder.corners.path(f.far, root);
|
|
45
|
+
if (way) ways.push({ edge: f.edge, key, points: o.end === "target" ? way : [...way].reverse() });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return sharedStretches(ways, { minSize: 2, pull: (p) => finder.corners.pull(p) });
|
|
49
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { Graph } from "../../../model.ts";
|
|
2
|
+
import type { HighwayInput, HighwayPlan } from "../../model.ts";
|
|
3
|
+
import { keepMembers, type Gate } from "../model.ts";
|
|
4
|
+
|
|
5
|
+
/** The options of the gate `ambiguity` (HWY-049). */
|
|
6
|
+
export interface AmbiguityGuardOptions {
|
|
7
|
+
/** Two groups of edges merge only when the merge adds this share of false pairs, or less. The default is 0.5. */
|
|
8
|
+
readonly maxAmbiguity: number;
|
|
9
|
+
/** With `node`, the default, two groups merge only when they share an end node. With `any`, only `maxAmbiguity` decides. */
|
|
10
|
+
readonly link: "node" | "any";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** The default options of the gate `ambiguity`. */
|
|
14
|
+
export const AMBIGUITY_GUARD_DEFAULTS: Readonly<AmbiguityGuardOptions> = Object.freeze({ maxAmbiguity: 0.5, link: "node" });
|
|
15
|
+
|
|
16
|
+
/** The adjacency of the nodes of a graph: the neighbours of each node, by the edges. */
|
|
17
|
+
function adjacency(graph: Graph): Map<string, Set<string>> {
|
|
18
|
+
const out = new Map<string, Set<string>>();
|
|
19
|
+
const add = (a: string, b: string): void => {
|
|
20
|
+
const set = out.get(a);
|
|
21
|
+
if (set) set.add(b);
|
|
22
|
+
else out.set(a, new Set([b]));
|
|
23
|
+
};
|
|
24
|
+
for (const e of graph.edges) {
|
|
25
|
+
add(e.source.node, e.target.node);
|
|
26
|
+
add(e.target.node, e.source.node);
|
|
27
|
+
}
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* This function gives the ambiguity of a set of nodes: the share of the pairs of distinct nodes without an edge between them.
|
|
33
|
+
* The nodes are the ends of the edges of one highway. A set with fewer than two nodes gives 0.
|
|
34
|
+
*/
|
|
35
|
+
export function nodeAmbiguity(nodes: ReadonlySet<string>, adj: ReadonlyMap<string, ReadonlySet<string>>): number {
|
|
36
|
+
const n = nodes.size;
|
|
37
|
+
if (n < 2) return 0;
|
|
38
|
+
let linked = 0;
|
|
39
|
+
for (const v of nodes) for (const w of adj.get(v) ?? []) if (w !== v && nodes.has(w)) linked++;
|
|
40
|
+
return 1 - linked / 2 / ((n * (n - 1)) / 2);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** A group of the members of one highway: the edges, their end nodes and the ambiguity of the nodes. */
|
|
44
|
+
interface Group {
|
|
45
|
+
readonly edges: string[];
|
|
46
|
+
readonly nodes: Set<string>;
|
|
47
|
+
readonly amb: number;
|
|
48
|
+
/** The first lane of the group, for a fixed order. */
|
|
49
|
+
readonly first: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* This function applies the ambiguity guard of HWY-049 to a plan. A highway joins edges that belong together.
|
|
54
|
+
*
|
|
55
|
+
* Two nodes are a false pair when their edges share a highway and no edge joins them. The ambiguity of a group of edges is the share of false pairs among the pairs of its end nodes.
|
|
56
|
+
* In each highway, each edge starts as a group. Two groups merge only when `amb(A ∪ B) - max(amb A, amb B)` is at most `maxAmbiguity`.
|
|
57
|
+
* With the link `node`, they also must share an end node. The merge with the smallest rise comes first.
|
|
58
|
+
*
|
|
59
|
+
* The largest group stays on the highway. The other edges leave it and go alone.
|
|
60
|
+
*/
|
|
61
|
+
export function ambiguityPlan(input: HighwayInput, plan: HighwayPlan, options: Readonly<AmbiguityGuardOptions>): HighwayPlan {
|
|
62
|
+
const adj = adjacency(input.graph);
|
|
63
|
+
const ends = new Map(input.graph.edges.map((e) => [e.id, [e.source.node, e.target.node] as const]));
|
|
64
|
+
const stay = new Map<string, Set<string>>();
|
|
65
|
+
for (const h of plan.highways) {
|
|
66
|
+
let groups: Group[] = h.lanes.flatMap((edge, i): Group[] => {
|
|
67
|
+
const end = ends.get(edge);
|
|
68
|
+
if (!end) return [];
|
|
69
|
+
const nodes = new Set(end);
|
|
70
|
+
return [{ edges: [edge], nodes, amb: nodeAmbiguity(nodes, adj), first: i }];
|
|
71
|
+
});
|
|
72
|
+
for (;;) {
|
|
73
|
+
let best: { a: number; b: number; rise: number; group: Group } | undefined;
|
|
74
|
+
for (let a = 0; a < groups.length; a++)
|
|
75
|
+
for (let b = a + 1; b < groups.length; b++) {
|
|
76
|
+
const ga = groups[a]!;
|
|
77
|
+
const gb = groups[b]!;
|
|
78
|
+
if (options.link === "node" && ![...gb.nodes].some((v) => ga.nodes.has(v))) continue;
|
|
79
|
+
const nodes = new Set([...ga.nodes, ...gb.nodes]);
|
|
80
|
+
const amb = nodeAmbiguity(nodes, adj);
|
|
81
|
+
const rise = amb - Math.max(ga.amb, gb.amb);
|
|
82
|
+
if (rise > options.maxAmbiguity + 1e-12) continue;
|
|
83
|
+
if (best === undefined || rise < best.rise - 1e-12)
|
|
84
|
+
best = { a, b, rise, group: { edges: [...ga.edges, ...gb.edges], nodes, amb, first: Math.min(ga.first, gb.first) } };
|
|
85
|
+
}
|
|
86
|
+
if (!best) break;
|
|
87
|
+
const merged = best.group;
|
|
88
|
+
const { a, b } = best;
|
|
89
|
+
groups = groups.filter((_, i) => i !== a && i !== b);
|
|
90
|
+
groups.push(merged);
|
|
91
|
+
groups.sort((p, q) => p.first - q.first);
|
|
92
|
+
}
|
|
93
|
+
let top: Group | undefined;
|
|
94
|
+
for (const g of groups) if (top === undefined || g.edges.length > top.edges.length) top = g;
|
|
95
|
+
stay.set(h.id, new Set(top?.edges ?? []));
|
|
96
|
+
}
|
|
97
|
+
return keepMembers(plan, (h, m) => stay.get(h.id)?.has(m.edge) ?? false);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** The gate `ambiguity` (HWY-049): the ambiguity guard. A highway does not join edges whose end nodes have no edges between them. */
|
|
101
|
+
export const ambiguityGate: Gate<AmbiguityGuardOptions> = {
|
|
102
|
+
name: "ambiguity",
|
|
103
|
+
describe: "The gate `ambiguity` (HWY-049) keeps on each highway the largest group of edges that a reader can tell apart. A merge of two groups must add few false pairs of nodes.",
|
|
104
|
+
defaults: AMBIGUITY_GUARD_DEFAULTS,
|
|
105
|
+
schema: {
|
|
106
|
+
maxAmbiguity: { type: "number", min: 0, max: 1, step: 0.05, marks: [0.3, 0.5], default: 0.5, describe: "Two groups merge only when the merge adds this share of false pairs, or less." },
|
|
107
|
+
link: { type: "enum", values: ["node", "any"], default: "node", describe: "With `node`, two groups merge only when they share an end node. With `any`, only `maxAmbiguity` decides." },
|
|
108
|
+
},
|
|
109
|
+
apply: (input, plan, options) => ambiguityPlan(input, plan, options),
|
|
110
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { WayOptions } from "../../../ways.ts";
|
|
2
|
+
import { lengthBetween, type HighwayInput, type HighwayPlan } from "../../model.ts";
|
|
3
|
+
import { edgeLengths, keepMembers, type Gate } from "../model.ts";
|
|
4
|
+
|
|
5
|
+
/** The options of the gate `breakeven` (HWY-047). */
|
|
6
|
+
export interface BreakEvenOptions {
|
|
7
|
+
/**
|
|
8
|
+
* The exponent of the cost law: a stretch with `n` edges costs `n^shareAlpha` times its length. Each edge pays the share `n^(shareAlpha - 1)`.
|
|
9
|
+
* With 0, a shared stretch is one line, as the ink of B1 counts it. The default is 0.5, as the break-even floor of `corridors`.
|
|
10
|
+
*/
|
|
11
|
+
readonly shareAlpha: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** The default options of the gate `breakeven`. */
|
|
15
|
+
export const BREAK_EVEN_DEFAULTS: Readonly<BreakEvenOptions> = Object.freeze({ shareAlpha: 0.5 });
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* This function applies the break-even law of HWY-047 to a plan. An edge stays on its highways only when sharing saves ink.
|
|
19
|
+
*
|
|
20
|
+
* The edge pays `n^(shareAlpha - 1)` of each shared stretch, with `n` the members of the highway. Thus it saves `(1 - n^(shareAlpha - 1)) L` on a stretch of the length `L`.
|
|
21
|
+
* It adds the length of its route along the plan less the length of its route alone. An edge stays when the saving is as large as the added length.
|
|
22
|
+
* For two parallel edges at the gap `d`, this rule gives the break-even overlap `d √((2 + t) / (2 - t))` with `t = 2^shareAlpha`.
|
|
23
|
+
*
|
|
24
|
+
* An edge that does not save ink leaves each of its highways. Then the other members of those highways save less, so the test repeats until no edge leaves.
|
|
25
|
+
*/
|
|
26
|
+
export function breakEvenPlan(input: HighwayInput, plan: HighwayPlan, options: Readonly<BreakEvenOptions>, way: WayOptions = {}): HighwayPlan {
|
|
27
|
+
const lengths = edgeLengths(input.graph, plan, way);
|
|
28
|
+
let now = plan;
|
|
29
|
+
for (;;) {
|
|
30
|
+
const saving = new Map<string, number>();
|
|
31
|
+
for (const h of now.highways) {
|
|
32
|
+
const share = 1 - Math.pow(h.members.length, options.shareAlpha - 1);
|
|
33
|
+
for (const m of h.members) saving.set(m.edge, (saving.get(m.edge) ?? 0) + share * lengthBetween(h.spine, Math.min(m.on, m.off), Math.max(m.on, m.off)));
|
|
34
|
+
}
|
|
35
|
+
const out = new Set<string>();
|
|
36
|
+
for (const [edge, saved] of saving) {
|
|
37
|
+
const l = lengths.get(edge);
|
|
38
|
+
if (l && saved < l.route - l.alone) out.add(edge);
|
|
39
|
+
}
|
|
40
|
+
if (out.size === 0) return now;
|
|
41
|
+
now = keepMembers(now, (_, m) => !out.has(m.edge));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The gate `breakeven` (HWY-047): an edge stays on a highway only when sharing saves ink. */
|
|
46
|
+
export const breakEvenGate: Gate<BreakEvenOptions> = {
|
|
47
|
+
name: "breakeven",
|
|
48
|
+
describe: "The gate `breakeven` (HWY-047) keeps an edge on a highway only when its share of the highway saves more ink than its detour adds.",
|
|
49
|
+
defaults: BREAK_EVEN_DEFAULTS,
|
|
50
|
+
schema: {
|
|
51
|
+
shareAlpha: {
|
|
52
|
+
type: "number",
|
|
53
|
+
min: 0,
|
|
54
|
+
max: 0.9,
|
|
55
|
+
step: 0.1,
|
|
56
|
+
marks: [0, 0.5],
|
|
57
|
+
default: 0.5,
|
|
58
|
+
describe: "The exponent of the cost law. An edge pays the share `n^(shareAlpha - 1)` of a stretch with `n` edges.",
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
apply: breakEvenPlan,
|
|
62
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { HighwayInput, HighwayPlan } from "../../model.ts";
|
|
2
|
+
import { commonEdges, matchHighways, type Gate } from "../model.ts";
|
|
3
|
+
|
|
4
|
+
/** The options of the gate `hysteresis` (HWY-046). */
|
|
5
|
+
export interface HysteresisOptions {
|
|
6
|
+
/** A new highway needs this number of edges or more. The default is 3. */
|
|
7
|
+
readonly kOn: number;
|
|
8
|
+
/** A highway of the earlier plan stays while it has this number of edges or more. The default is 2. */
|
|
9
|
+
readonly kOff: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** The default options of the gate `hysteresis`. */
|
|
13
|
+
export const HYSTERESIS_DEFAULTS: Readonly<HysteresisOptions> = Object.freeze({ kOn: 3, kOff: 2 });
|
|
14
|
+
|
|
15
|
+
/** Two highways of two plans are the same highway when the Jaccard index of their edges is this value or more. */
|
|
16
|
+
export const SAME_HIGHWAY = 0.5;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* This function applies the count threshold with hysteresis of HWY-046 to a plan.
|
|
20
|
+
* A highway with `kOn` edges or more stays. A highway with fewer than `kOff` edges goes away.
|
|
21
|
+
* A highway between the two limits stays only when it was in the earlier plan, `input.previous`.
|
|
22
|
+
* The highway of the earlier plan with the most common edges is its match, with a Jaccard index of `SAME_HIGHWAY` or more.
|
|
23
|
+
*
|
|
24
|
+
* Thus an edit that adds or removes one edge near the limit does not make a highway come and go. Without an earlier plan, the limit is `kOn`.
|
|
25
|
+
*/
|
|
26
|
+
export function hysteresisPlan(input: HighwayInput, plan: HighwayPlan, options: Readonly<HysteresisOptions>): HighwayPlan {
|
|
27
|
+
const matched = input.previous ? matchHighways(input.previous.plan, plan, SAME_HIGHWAY, commonEdges(input.previous.graph, input.graph)) : new Map<number, number>();
|
|
28
|
+
const kOff = Math.max(2, Math.min(options.kOff, options.kOn));
|
|
29
|
+
const highways = plan.highways.filter((h, i) => {
|
|
30
|
+
const n = h.members.length;
|
|
31
|
+
if (n >= options.kOn) return true;
|
|
32
|
+
if (n < kOff) return false;
|
|
33
|
+
return matched.has(i);
|
|
34
|
+
});
|
|
35
|
+
return highways.length === plan.highways.length ? plan : { highways };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The gate `hysteresis` (HWY-046): a highway forms at `kOn` edges and goes away below `kOff` edges. */
|
|
39
|
+
export const hysteresisGate: Gate<HysteresisOptions> = {
|
|
40
|
+
name: "hysteresis",
|
|
41
|
+
describe: "The gate `hysteresis` (HWY-046) keeps a new highway with `kOn` edges or more. A highway of the earlier plan stays while it has `kOff` edges or more.",
|
|
42
|
+
defaults: HYSTERESIS_DEFAULTS,
|
|
43
|
+
schema: {
|
|
44
|
+
kOn: { type: "integer", min: 2, max: 16, step: 1, default: 3, describe: "A new highway needs this number of edges or more." },
|
|
45
|
+
kOff: { type: "integer", min: 2, max: 16, step: 1, default: 2, describe: "A highway of the earlier plan stays while it has this number of edges or more." },
|
|
46
|
+
},
|
|
47
|
+
apply: (input, plan, options) => hysteresisPlan(input, plan, options),
|
|
48
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { Highway, HighwayInput, HighwayPlan } from "../../model.ts";
|
|
2
|
+
import { commonEdges, matchHighways, type Gate } from "../model.ts";
|
|
3
|
+
|
|
4
|
+
/** The options of the gate `ids` (HWY-054). */
|
|
5
|
+
export interface StableIdOptions {
|
|
6
|
+
/** A new highway takes the id of an old highway when the Jaccard index of their edges is this value or more. The default is 0.5. */
|
|
7
|
+
readonly minJaccard: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** The default options of the gate `ids`. */
|
|
11
|
+
export const STABLE_ID_DEFAULTS: Readonly<StableIdOptions> = Object.freeze({ minJaccard: 0.5 });
|
|
12
|
+
|
|
13
|
+
/** This function gives the number at the end of an id, or -1 for an id without a number at its end. */
|
|
14
|
+
function numberOf(id: string): number {
|
|
15
|
+
let start = id.length;
|
|
16
|
+
while (start > 0 && id.charCodeAt(start - 1) >= 48 && id.charCodeAt(start - 1) <= 57) start--;
|
|
17
|
+
return start === id.length ? -1 : Number(id.slice(start));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* This function gives stable ids to the highways of a plan (HWY-054). The earlier plan is `input.previous`.
|
|
22
|
+
* Each new highway takes the id of the old highway with the most common edges.
|
|
23
|
+
* The pairs with the largest Jaccard index of their edges come first.
|
|
24
|
+
*
|
|
25
|
+
* A pair needs an index of `minJaccard` or more, and only the edges of both graphs count.
|
|
26
|
+
* A new highway without a match gets a new id `h<n>` that no old highway and no new highway has. Thus the old highway of a new id ends.
|
|
27
|
+
* The gate changes only the ids, so the routes do not change. Without an earlier plan, the plan does not change.
|
|
28
|
+
*/
|
|
29
|
+
export function stableIdPlan(input: HighwayInput, plan: HighwayPlan, options: Readonly<StableIdOptions>): HighwayPlan {
|
|
30
|
+
if (!input.previous) return plan;
|
|
31
|
+
const before = input.previous.plan;
|
|
32
|
+
const matched = matchHighways(before, plan, options.minJaccard, commonEdges(input.previous.graph, input.graph));
|
|
33
|
+
const taken = new Set<string>([...before.highways.map((h) => h.id)]);
|
|
34
|
+
for (const [, i0] of matched) taken.add(before.highways[i0]!.id);
|
|
35
|
+
let next = 1 + Math.max(-1, ...before.highways.map((h) => numberOf(h.id)), ...plan.highways.map((h) => numberOf(h.id)));
|
|
36
|
+
const fresh = (): string => {
|
|
37
|
+
while (taken.has(`h${next}`)) next++;
|
|
38
|
+
const id = `h${next++}`;
|
|
39
|
+
taken.add(id);
|
|
40
|
+
return id;
|
|
41
|
+
};
|
|
42
|
+
const highways = plan.highways.map((h, i): Highway => {
|
|
43
|
+
const i0 = matched.get(i);
|
|
44
|
+
const id = i0 === undefined ? fresh() : before.highways[i0]!.id;
|
|
45
|
+
return id === h.id ? h : { ...h, id };
|
|
46
|
+
});
|
|
47
|
+
return { highways };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The gate `ids` (HWY-054): stable highway ids across edits. A highway keeps the id of its match in the earlier plan. */
|
|
51
|
+
export const stableIdGate: Gate<StableIdOptions> = {
|
|
52
|
+
name: "ids",
|
|
53
|
+
describe: "The gate `ids` (HWY-054) gives each highway the id of the highway of the earlier plan with the most common edges. The routes do not change.",
|
|
54
|
+
defaults: STABLE_ID_DEFAULTS,
|
|
55
|
+
schema: {
|
|
56
|
+
minJaccard: { type: "number", min: 0.3, max: 0.8, step: 0.05, default: 0.5, describe: "A new highway takes the id of an old highway when the Jaccard index of their edges is this value or more." },
|
|
57
|
+
},
|
|
58
|
+
apply: (input, plan, options) => stableIdPlan(input, plan, options),
|
|
59
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { commonEdges, edgeLengths, gateStrategy, jaccard, keepMembers, matchHighways, type EdgeLengths, type Gate } from "./model.ts";
|
|
2
|
+
export { BREAK_EVEN_DEFAULTS, breakEvenGate, breakEvenPlan, type BreakEvenOptions } from "./breakeven/index.ts";
|
|
3
|
+
export { STRETCH_DEFAULTS, stretchGate, stretchPlan, type StretchOptions } from "./stretch/index.ts";
|
|
4
|
+
export { AMBIGUITY_GUARD_DEFAULTS, ambiguityGate, ambiguityPlan, nodeAmbiguity, type AmbiguityGuardOptions } from "./ambiguity/index.ts";
|
|
5
|
+
export { HYSTERESIS_DEFAULTS, hysteresisGate, hysteresisPlan, SAME_HIGHWAY, type HysteresisOptions } from "./hysteresis/index.ts";
|
|
6
|
+
export { STABLE_ID_DEFAULTS, stableIdGate, stableIdPlan, type StableIdOptions } from "./ids/index.ts";
|