@mark1russell7/highways 0.9.3 → 0.9.4

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.
@@ -0,0 +1,122 @@
1
+ import { byText } from "../../compare.ts";
2
+ import { dist, polylineLength } from "../../geom.ts";
3
+ import type { Graph, GroupRule, Result, Router } from "../../model.ts";
4
+ import { defineStrategy, type Strategy } from "../../strategies/model.ts";
5
+ import type { OptionSchema } from "../../strategies/options.ts";
6
+ import { sharedRouter, type SharedSearch, type Way } from "../shared.ts";
7
+ import { byCollection } from "../trunks.ts";
8
+
9
+ /** The edges that go first: the edges with few good ways (`tight`), or the edges with many good ways (`loose`). */
10
+ export type TightFirst = "tight" | "loose";
11
+
12
+ /** The values of `TightFirst`. */
13
+ export const TIGHT_FIRSTS: readonly TightFirst[] = Object.freeze(["tight", "loose"]);
14
+
15
+ /** The order of two edges with the same slack: by the edge id, or the longer edge first. */
16
+ export type TightTies = "id" | "length";
17
+
18
+ /** The values of `TightTies`. */
19
+ export const TIGHT_TIES: readonly TightTies[] = Object.freeze(["id", "length"]);
20
+
21
+ /** The options of the router `tight` (WILD-023: tight edges first, after list scheduling by slack). */
22
+ export interface TightOptions {
23
+ /** The cost factor of the links of the first way in the search of the second way, from 2 to 10. The default is 3. */
24
+ readonly penalty: number;
25
+ /**
26
+ * The edges that go first. With `tight`, the default, an edge whose second way costs much more than its first way goes first.
27
+ * With `loose`, the order turns around: the formula of the record, with the rising slack.
28
+ */
29
+ readonly first: TightFirst;
30
+ /** The order of two edges with the same slack. The default is `length`: the longer edge first, as in `shared`. */
31
+ readonly ties: TightTies;
32
+ }
33
+
34
+ /** The default options of the router `tight`. */
35
+ export const TIGHT_DEFAULTS: Readonly<TightOptions> = Object.freeze({ penalty: 3, first: "tight", ties: "length" });
36
+
37
+ /** The declarations of the options of the router `tight` (WILD-023). */
38
+ export const TIGHT_SCHEMA: OptionSchema<TightOptions> = {
39
+ penalty: { type: "number", min: 2, max: 10, step: 0.5, marks: [2, 3, 5], default: 3, describe: "The cost factor of the links of the first way in the search of the second way." },
40
+ first: { type: "enum", values: ["tight", "loose"], default: "tight", describe: "The edges that go first: the edges with few good ways, or the edges with many good ways." },
41
+ ties: { type: "enum", values: ["id", "length"], default: "length", describe: "The order of two edges with the same slack: by the edge id, or the longer edge first." },
42
+ };
43
+
44
+ /**
45
+ * This function gives the slack of each way (WILD-023): `L2 / L1 - 1`. `L1` is the length of the shortest way.
46
+ * `L2` is the cost of the shortest way when each link of the first way costs `penalty` times its length.
47
+ * A way with no second way has the slack `penalty - 1`. A way with a second way of near-equal length has a slack near 0.
48
+ */
49
+ export function waySlacks(ways: readonly Way[], search: SharedSearch, penalty: number): Map<Way, number> {
50
+ const out = new Map<Way, number>();
51
+ const n = search.corners;
52
+ for (const w of ways) {
53
+ const first = search.find(w);
54
+ if (!first) {
55
+ out.set(w, 0);
56
+ continue;
57
+ }
58
+ const l1 = polylineLength(first.seq);
59
+ const own = new Set<number>();
60
+ for (let i = 1; i < first.ids.length; i++) {
61
+ const a = first.ids[i - 1]!;
62
+ const b = first.ids[i]!;
63
+ if (a < n && b < n) own.add(search.link(a, b));
64
+ }
65
+ const cost = (i: number, j: number, d: number): number => (own.has(search.link(i, j)) ? d * penalty : d);
66
+ const second = search.find(w, cost, 1);
67
+ let l2 = l1;
68
+ if (second) {
69
+ l2 = 0;
70
+ for (let i = 1; i < second.ids.length; i++) {
71
+ const a = second.ids[i - 1]!;
72
+ const b = second.ids[i]!;
73
+ const d = dist(second.seq[i - 1]!, second.seq[i]!);
74
+ l2 += a < n && b < n ? cost(a, b, d) : d;
75
+ }
76
+ }
77
+ out.set(w, l1 > 1e-9 ? l2 / l1 - 1 : 0);
78
+ }
79
+ return out;
80
+ }
81
+
82
+ /**
83
+ * This function makes the router `tight` (WILD-023). It is the router `shared` with another order of the edges of each key.
84
+ * With `first: tight`, the edges with the largest slack value go first: their second way costs much more, so they have few good ways.
85
+ * The edges with many ways of near-equal cost go last. The keys keep the order of `shared`.
86
+ */
87
+ export function tightRouter(options: Partial<TightOptions> & { readonly name: string; readonly rule?: GroupRule; readonly maxNodes?: number }): Router {
88
+ const o = { ...TIGHT_DEFAULTS, ...options };
89
+ const penalty = Math.min(10, Math.max(1, o.penalty));
90
+ const sign = o.first === "loose" ? 1 : -1;
91
+ return sharedRouter(
92
+ { name: o.name, ...(o.rule === undefined ? {} : { rule: o.rule }), ...(o.maxNodes === undefined ? {} : { maxNodes: o.maxNodes }) },
93
+ undefined,
94
+ {
95
+ order: (ways, search) => {
96
+ const slack = waySlacks(ways, search, penalty);
97
+ const span = (w: Way): number => dist(w.ps.point, w.pt.point);
98
+ return [...ways].sort(
99
+ (p, q) =>
100
+ byText(p.key, q.key) ||
101
+ sign * (slack.get(p)! - slack.get(q)!) ||
102
+ (o.ties === "length" ? span(q) - span(p) : 0) ||
103
+ byText(p.edge.id, q.edge.id),
104
+ );
105
+ },
106
+ },
107
+ );
108
+ }
109
+
110
+ /**
111
+ * The router strategy `tight` (WILD-023), with the rule `byCollection` of `shared`. It is not a built-in router, so the benchmark and the hashes of `ROUTERS` do not change.
112
+ * A screen module or a consumer adds it to a registry.
113
+ */
114
+ export const tightStrategy: Strategy<"router", TightOptions> = defineStrategy({
115
+ step: "router",
116
+ name: "tight",
117
+ describe: "Tight edges first (WILD-023). The router `shared` routes first the edges whose second way costs much more than their first way.",
118
+ defaults: { ...TIGHT_DEFAULTS },
119
+ schema: TIGHT_SCHEMA,
120
+ maxNodes: 10_000,
121
+ run: (graph: Graph, o: Readonly<TightOptions>): Result => tightRouter({ ...o, name: "tight", rule: byCollection }).route(graph),
122
+ });
@@ -46,6 +46,8 @@ import { P_HUB_DEFAULTS, pHubPlan, type PHubOptions } from "../highways/p-hub/in
46
46
  import { SCALE_SPACE_DEFAULTS, scaleSpacePlan, type ScaleSpaceOptions } from "../highways/scale-space/index.ts";
47
47
  import { GAP_DEFAULTS, gapPlan, type GapOptions } from "../highways/gaps/index.ts";
48
48
  import { INTERCHANGE_DEFAULTS, interchangePlan, type InterchangeOptions } from "../highways/interchanges/index.ts";
49
+ import { KNEE_DEFAULTS, kneePlan, type KneeOptions } from "../highways/knee/index.ts";
50
+ import { BRONCHIAL_DEFAULTS, bronchialPlan, type BronchialOptions } from "../highways/bronchial/index.ts";
49
51
  import { SEARCH_DEFAULTS, searchPlan, type SearchOptions } from "../highways/search/index.ts";
50
52
  import { STEINER_DEFAULTS, steinerPlan, type SteinerOptions } from "../highways/steiner/index.ts";
51
53
  import { STREET_DEFAULTS, streetPlan, type StreetOptions } from "../highways/streets/index.ts";
@@ -72,6 +74,7 @@ import { WEISZFELD_DEFAULTS, weiszfeldPlan, type WeiszfeldOptions } from "../hig
72
74
  import { WSPD_DEFAULTS, wspdPlan, type WspdOptions } from "../highways/wspd/index.ts";
73
75
  import { ZIPPER_DEFAULTS, zipperPlan, type ZipperOptions } from "../highways/zipper/index.ts";
74
76
  import { depthLevels, type DepthLevelOptions } from "../levels/depths/index.ts";
77
+ import { VENATION_DEFAULTS, venationLevels, type VenationOptions } from "../levels/venation/index.ts";
75
78
  import type { Graph, Result, Router } from "../model.ts";
76
79
  import { facingEnds } from "../plugs/facing/index.ts";
77
80
  import { justifyEnds, type JustifyOptions } from "../plugs/justify.ts";
@@ -87,11 +90,11 @@ import { straightRouter } from "../routers/straight.ts";
87
90
  import { byCollection, trunksRouter, type TrunksOptions } from "../routers/trunks.ts";
88
91
  import { buildSections, SECTION_DEFAULTS, type SectionOptions } from "../sections.ts";
89
92
  import { runPipeline, type PipelineOptions } from "../whole/pipeline/index.ts";
93
+ import { BOARD_PORTFOLIO_DEFAULTS, boardPortfolio, type BoardPortfolioOptions, type PortfolioSetting } from "../whole/portfolio/index.ts";
90
94
  import { createRegistry, defineStrategy, type AnyStrategy, type CurveInput, type LaneBoard, type Registry, type Strategy } from "./model.ts";
91
95
  import type { OptionSchema } from "./options.ts";
92
96
  import { AREA_SCHEMA, CONFLUENT_SCHEMA, CROSSBAR_SCHEMA, FRECHET_SCHEMA, MLSEB_SCHEMA, POWER_SCHEMA, SBEB_SCHEMA } from "./schemas.ts";
93
- import { DEPTHS_SCHEMA, JUSTIFY_SCHEMA, PIPELINE_SCHEMA, SIDE_PLUG_SCHEMA, SPREAD_SCHEMA } from "./schemas.ts";
94
- import { CLOTHOID_SCHEMA, CORNERS_SCHEMA, CORRIDOR_SCHEMA, CONSENSUS_SCHEMA, CUBU_SCHEMA, CUT_SCHEMA, DIFFUSION_SCHEMA, DIVIDED_SCHEMA, EDGE_PATH_SCHEMA, FDEB_SCHEMA, FLOW_CLUSTER_SCHEMA, FLOW_FIELD_SCHEMA, GROWTH_SCHEMA, HDBSCAN_SCHEMA, HIERARCHY_SCHEMA, HIGHWAY_SCHEMA, HOUGH_SCHEMA, HUB_ARC_SCHEMA, KD_SCHEMA, KDEEB_SCHEMA, KEY_SCHEMA, LANES_SCHEMA, LEVEL_HIGHWAY_SCHEMA, LEVEL_ROOM_SCHEMA, LINK_COMMUNITY_SCHEMA, MEET_SCHEMA, MESH_SCHEMA, MINGLE_SCHEMA, NESTED_SCHEMA, OD_CELL_SCHEMA, P_HUB_SCHEMA, QUICKBUNDLES_SCHEMA, RADIUS_SCHEMA, RESISTANCE_SCHEMA, RIDGE_SCHEMA, ROAD_CLASS_SCHEMA, ROOM_SCHEMA, SCALE_SPACE_SCHEMA, SECTION_SCHEMA, SELECT_SCHEMA, SECTIONS_SCHEMA, SHARED_ALL_SCHEMA, SHARED_SCHEMA, SMOOTH_SCHEMA, SOFT_KEY_SCHEMA, SPECTRAL_SCHEMA, SPIRAL_SCHEMA, STUB_SCHEMA, TENSION_SCHEMA, TERRITORY_SCHEMA, TRACLUS_SCHEMA, TRUNKS_SCHEMA, WEISZFELD_SCHEMA, WSPD_SCHEMA, ZIPPER_SCHEMA, STEINER_SCHEMA, GAP_SCHEMA, STREET_SCHEMA, SEARCH_SCHEMA, INTERCHANGE_SCHEMA } from "./schemas.ts";
97
+ import { DEPTHS_SCHEMA, JUSTIFY_SCHEMA, PIPELINE_SCHEMA, SIDE_PLUG_SCHEMA, SPREAD_SCHEMA, CLOTHOID_SCHEMA, CORNERS_SCHEMA, CORRIDOR_SCHEMA, CONSENSUS_SCHEMA, CUBU_SCHEMA, CUT_SCHEMA, DIFFUSION_SCHEMA, DIVIDED_SCHEMA, EDGE_PATH_SCHEMA, FDEB_SCHEMA, FLOW_CLUSTER_SCHEMA, FLOW_FIELD_SCHEMA, GROWTH_SCHEMA, HDBSCAN_SCHEMA, HIERARCHY_SCHEMA, HIGHWAY_SCHEMA, HOUGH_SCHEMA, HUB_ARC_SCHEMA, KD_SCHEMA, KDEEB_SCHEMA, KEY_SCHEMA, LANES_SCHEMA, LEVEL_HIGHWAY_SCHEMA, LEVEL_ROOM_SCHEMA, LINK_COMMUNITY_SCHEMA, MEET_SCHEMA, MESH_SCHEMA, MINGLE_SCHEMA, NESTED_SCHEMA, OD_CELL_SCHEMA, P_HUB_SCHEMA, QUICKBUNDLES_SCHEMA, RADIUS_SCHEMA, RESISTANCE_SCHEMA, RIDGE_SCHEMA, ROAD_CLASS_SCHEMA, ROOM_SCHEMA, SCALE_SPACE_SCHEMA, SECTION_SCHEMA, SELECT_SCHEMA, SECTIONS_SCHEMA, SHARED_ALL_SCHEMA, SHARED_SCHEMA, SMOOTH_SCHEMA, SOFT_KEY_SCHEMA, SPECTRAL_SCHEMA, SPIRAL_SCHEMA, STUB_SCHEMA, TENSION_SCHEMA, TERRITORY_SCHEMA, TRACLUS_SCHEMA, TRUNKS_SCHEMA, WEISZFELD_SCHEMA, WSPD_SCHEMA, ZIPPER_SCHEMA, STEINER_SCHEMA, GAP_SCHEMA, STREET_SCHEMA, SEARCH_SCHEMA, INTERCHANGE_SCHEMA, KNEE_SCHEMA, BRONCHIAL_SCHEMA, BOARD_PORTFOLIO_SCHEMA, VENATION_SCHEMA } from "./schemas.ts";
95
98
 
96
99
  /** The options of a router strategy: the options of its router, without the name and the limit of the benchmark. */
97
100
  export type RouterPreset<O> = Omit<O, "name" | "maxNodes">;
@@ -475,6 +478,8 @@ export const highwayStrategies: {
475
478
  readonly streets: Strategy<"highway", StreetOptions>;
476
479
  readonly search: Strategy<"highway", SearchOptions>;
477
480
  readonly interchanges: Strategy<"highway", InterchangeOptions>;
481
+ readonly knee: Strategy<"highway", KneeOptions>;
482
+ readonly bronchial: Strategy<"highway", BronchialOptions>;
478
483
  readonly "corridors+breakeven": Strategy<"highway", CorridorOptions & BreakEvenOptions>;
479
484
  readonly "corridors+stretch": Strategy<"highway", CorridorOptions & StretchOptions>;
480
485
  readonly "corridors+ambiguity": Strategy<"highway", CorridorOptions & AmbiguityGuardOptions>;
@@ -950,6 +955,24 @@ export const highwayStrategies: {
950
955
  maxNodes: 10_000,
951
956
  run: (input: HighwayInput, o: Readonly<InterchangeOptions>) => interchangePlan(input, o),
952
957
  }),
958
+ knee: defineStrategy({
959
+ step: "highway",
960
+ name: "knee",
961
+ describe: "The knee of the plant front (NAT-018). A sweep of the balancing factor of NAT-017 gives trees, and the tree at the knee of ink against path length wins.",
962
+ defaults: { ...KNEE_DEFAULTS } as KneeOptions,
963
+ schema: KNEE_SCHEMA,
964
+ maxNodes: 10_000,
965
+ run: (input: HighwayInput, o: Readonly<KneeOptions>) => kneePlan(input, o),
966
+ }),
967
+ bronchial: defineStrategy({
968
+ step: "highway",
969
+ name: "bronchial",
970
+ describe: "Bronchial splits that fill the space (NAT-019). The tree grows toward the center of its targets, then splits the targets in two halves, again and again.",
971
+ defaults: { ...BRONCHIAL_DEFAULTS } as BronchialOptions,
972
+ schema: BRONCHIAL_SCHEMA,
973
+ maxNodes: 10_000,
974
+ run: (input: HighwayInput, o: Readonly<BronchialOptions>) => bronchialPlan(input, o),
975
+ }),
953
976
  "corridors+breakeven": gateStrategy(corridors, breakEvenGate),
954
977
  "corridors+stretch": gateStrategy(corridors, stretchGate),
955
978
  "corridors+ambiguity": gateStrategy(corridors, ambiguityGate),
@@ -1001,6 +1024,7 @@ export const plugStrategies: {
1001
1024
  /** The built-in level rules: the strategies of the step `levels`. The router `sections` makes its zoom levels with its option `level`. */
1002
1025
  export const levelStrategies: {
1003
1026
  readonly depths: Strategy<"levels", DepthLevelOptions>;
1027
+ readonly venation: Strategy<"levels", VenationOptions>;
1004
1028
  } = Object.freeze({
1005
1029
  depths: defineStrategy<"levels", DepthLevelOptions>({
1006
1030
  step: "levels",
@@ -1010,14 +1034,37 @@ export const levelStrategies: {
1010
1034
  schema: DEPTHS_SCHEMA,
1011
1035
  run: (input, o) => depthLevels(input, o),
1012
1036
  }),
1037
+ venation: defineStrategy<"levels", VenationOptions>({
1038
+ step: "levels",
1039
+ name: "venation",
1040
+ describe: "A closed venation backbone (NAT-016). The links of each level with groups take their ways along the relative neighbourhood graph of the items.",
1041
+ defaults: { ...VENATION_DEFAULTS },
1042
+ schema: VENATION_SCHEMA,
1043
+ run: (input, o) => venationLevels(input, o),
1044
+ }),
1013
1045
  });
1014
1046
 
1047
+ /**
1048
+ * The fixed list of settings of the whole strategy `board-portfolio` (WILD-045), in order. The option `settings` takes the first ones.
1049
+ * The list holds routers and highway strategies with their defaults. It comes from the catalog of the strategies, not from a fit to the screen.
1050
+ */
1051
+ export const BOARD_PORTFOLIO_SETTINGS: readonly PortfolioSetting[] = Object.freeze(
1052
+ [
1053
+ ...(["shared", "corners", "trunks", "sections", "sections-tree", "corners-smooth"] as const).map((name) => ({ name, pipeline: { router: routerStrategies[name] as Strategy<"router"> } })),
1054
+ ...(["corridors", "shared-ways", "road-classes", "steiner", "knee", "bronchial"] as const).map((name) => ({
1055
+ name: `highway:${name}`,
1056
+ pipeline: { router: routerStrategies.corners as Strategy<"router">, highway: highwayStrategies[name] as Strategy<"highway"> },
1057
+ })),
1058
+ ].map((s) => Object.freeze(s)),
1059
+ );
1060
+
1015
1061
  /**
1016
1062
  * The built-in whole strategies: the strategies of the step `whole`. The strategy `pipeline` runs one strategy of each step, as the demo of the site does.
1017
1063
  * Without options, it routes the graph with the router `corners`.
1018
1064
  */
1019
1065
  export const wholeStrategies: {
1020
1066
  readonly pipeline: Strategy<"whole", PipelineOptions>;
1067
+ readonly "board-portfolio": Strategy<"whole", BoardPortfolioOptions>;
1021
1068
  } = Object.freeze({
1022
1069
  pipeline: defineStrategy<"whole", PipelineOptions>({
1023
1070
  step: "whole",
@@ -1027,6 +1074,14 @@ export const wholeStrategies: {
1027
1074
  schema: PIPELINE_SCHEMA,
1028
1075
  run: (graph, o) => runPipeline(graph, o),
1029
1076
  }),
1077
+ "board-portfolio": defineStrategy<"whole", BoardPortfolioOptions>({
1078
+ step: "whole",
1079
+ name: "board-portfolio",
1080
+ describe: "A portfolio for each board (WILD-045). The pipeline tries a fixed list of settings, and the least weighted rank of crossings, detour and ink wins.",
1081
+ defaults: { ...BOARD_PORTFOLIO_DEFAULTS },
1082
+ schema: BOARD_PORTFOLIO_SCHEMA,
1083
+ run: (graph, o) => boardPortfolio(graph, BOARD_PORTFOLIO_SETTINGS, o).output,
1084
+ }),
1030
1085
  });
1031
1086
 
1032
1087
  /** The built-in strategies of each step, in order. A consumer adds its own: `createRegistry([...BUILT_IN_STRATEGIES, mine])`. */
@@ -44,6 +44,8 @@ import type { RoadClassOptions } from "../highways/road-classes/index.ts";
44
44
  import type { ScaleSpaceOptions } from "../highways/scale-space/index.ts";
45
45
  import type { GapOptions } from "../highways/gaps/index.ts";
46
46
  import type { InterchangeOptions } from "../highways/interchanges/index.ts";
47
+ import type { KneeOptions } from "../highways/knee/index.ts";
48
+ import type { BronchialOptions } from "../highways/bronchial/index.ts";
47
49
  import type { SearchOptions } from "../highways/search/index.ts";
48
50
  import type { SteinerOptions } from "../highways/steiner/index.ts";
49
51
  import type { StreetOptions } from "../highways/streets/index.ts";
@@ -59,6 +61,8 @@ import type { WspdOptions } from "../highways/wspd/index.ts";
59
61
  import type { ZipperOptions } from "../highways/zipper/index.ts";
60
62
  import type { LaneStretchOptions } from "../lane-orders/stretch/index.ts";
61
63
  import type { DepthLevelOptions } from "../levels/depths/index.ts";
64
+ import type { VenationOptions } from "../levels/venation/index.ts";
65
+ import type { BoardPortfolioOptions } from "../whole/portfolio/index.ts";
62
66
  import type { CornersOptions } from "../routers/corners.ts";
63
67
  import type { SectionsOptions } from "../routers/sections.ts";
64
68
  import type { SharedOptions } from "../routers/shared.ts";
@@ -141,6 +145,20 @@ export const CORNERS_SCHEMA: OptionSchema<RouterPreset<CornersOptions>> = {
141
145
  step: 1,
142
146
  describe: "The distance from the walls of a frame to the bounds of its inner routes. Without a value, the inset is the margin.",
143
147
  },
148
+ inner: {
149
+ type: "enum",
150
+ values: ["corners", "gutter"],
151
+ default: "corners",
152
+ describe: "The mode of the inner routes. With `gutter`, each inner route is orthogonal in the gaps between the nodes of the frame, with a lane in each shared gap.",
153
+ },
154
+ bendCost: {
155
+ type: "number",
156
+ min: 0,
157
+ max: 100,
158
+ step: 1,
159
+ default: 16,
160
+ describe: "The cost of one bend of an inner route of the mode `gutter`, in units of length. A larger cost gives fewer bends.",
161
+ },
144
162
  };
145
163
 
146
164
  /** The declarations of the options of the router `trunks`. */
@@ -1016,3 +1034,39 @@ export const INTERCHANGE_SCHEMA: OptionSchema<InterchangeOptions> = {
1016
1034
  discount: { type: "number", min: 0.1, max: 1, step: 0.05, marks: [0.3, 0.5, 0.7], default: 0.5, describe: "The cost of a length on a highway against a length off the highways." },
1017
1035
  detour: { type: "number", min: 1, max: 3, step: 0.05, marks: [1.25, 1.5, 2], default: 1.5, describe: "An edge rides the network only when its way is this many times its first route or less." },
1018
1036
  };
1037
+
1038
+ /** The declarations of the options of the highway strategy `knee` (NAT-018). */
1039
+ export const KNEE_SCHEMA: OptionSchema<KneeOptions> = {
1040
+ sweep: { type: "integer", min: 5, max: 21, step: 1, marks: [5, 11, 21], default: 11, describe: "The number of points of the sweep of the balancing factor." },
1041
+ rule: { type: "enum", values: ["chord", "weights"], default: "chord", describe: "The rule that picks one tree: the knee of the chord, or the least weighted sum." },
1042
+ ink: { type: "number", min: 0, max: 1, step: 0.05, marks: [0.25, 0.5, 0.75], default: 0.5, describe: "The weight of the ink in the rule `weights`. The rest of the weight goes to the path length." },
1043
+ detour: { type: "number", min: 1, max: 3, step: 0.05, marks: [1.25, 1.5, 2], default: 1.5, describe: "An edge rides the tree only when its path in the tree is this many times its first route or less." },
1044
+ minSize: { type: "integer", min: 2, max: 16, step: 1, default: 2, describe: "A stretch of a tree with fewer edges than this number is not a highway." },
1045
+ };
1046
+
1047
+ /** The declarations of the options of the highway strategy `bronchial` (NAT-019). */
1048
+ export const BRONCHIAL_SCHEMA: OptionSchema<BronchialOptions> = {
1049
+ split: { type: "enum", values: ["count", "weight", "gap"], default: "count", describe: "The rule that splits the targets of a tip in two: by count, by weight, or at the widest gap." },
1050
+ share: { type: "number", min: 0.3, max: 0.7, step: 0.05, marks: [0.3, 0.5, 0.7], default: 0.5, describe: "The share of the distance to the center of the targets that a branch grows." },
1051
+ smallest: { type: "integer", min: 1, max: 3, step: 1, default: 1, describe: "A set with this number of targets or fewer stops the splits." },
1052
+ detour: { type: "number", min: 1, max: 3, step: 0.05, marks: [1.25, 1.5, 2], default: 1.5, describe: "An edge rides the tree only when its path in the tree is this many times its first route or less." },
1053
+ minSize: { type: "integer", min: 2, max: 16, step: 1, default: 2, describe: "A stretch of a tree with fewer edges than this number is not a highway." },
1054
+ };
1055
+
1056
+ /** The declarations of the options of the level strategy `venation` (NAT-016): the options of `depths`, and the options of the backbone. */
1057
+ export const VENATION_SCHEMA: OptionSchema<VenationOptions> = {
1058
+ ...DEPTHS_SCHEMA,
1059
+ backbone: { type: "enum", values: ["rng", "gabriel"], default: "rng", describe: "The graph of the backbone: the relative neighbourhood graph, or the Gabriel graph." },
1060
+ discount: { type: "number", min: 0, max: 0.8, step: 0.05, marks: [0, 0.4, 0.8], default: 0.4, describe: "The share of the cost of a backbone link that a later level link saves when an earlier link uses it." },
1061
+ detour: { type: "number", min: 1, max: 3, step: 0.05, marks: [1.25, 1.5, 2], default: 1.5, describe: "A level link takes its way on the backbone only when that way is this many times its direct link or less." },
1062
+ maxItems: { type: "integer", min: 10, max: 2000, step: 10, marks: [100, 400, 1000], default: 400, describe: "A level with more items than this number keeps the links of `depths`." },
1063
+ };
1064
+
1065
+ /** The declarations of the options of the whole strategy `board-portfolio` (WILD-045). */
1066
+ export const BOARD_PORTFOLIO_SCHEMA: OptionSchema<BoardPortfolioOptions> = {
1067
+ settings: { type: "integer", min: 3, max: 12, step: 1, marks: [3, 4, 8, 12], default: 4, describe: "The number of settings that the portfolio tries, from the start of its list." },
1068
+ crossings: { type: "number", min: 0, max: 4, step: 0.25, marks: [0, 1, 2], default: 1, describe: "The weight of the crossings (R1) in the score." },
1069
+ detour: { type: "number", min: 0, max: 4, step: 0.25, marks: [0, 1, 2], default: 1, describe: "The weight of the median detour (R5) in the score." },
1070
+ ink: { type: "number", min: 0, max: 4, step: 0.25, marks: [0, 1, 2], default: 1, describe: "The weight of the ink saved (B1) in the score." },
1071
+ budget: { type: "number", min: 0, max: 10, step: 0.1, marks: [0, 1, 10], default: 0, describe: "The time budget in seconds. The value 0 sets no budget." },
1072
+ };
@@ -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
+ }