@mark1russell7/highways 0.4.2 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/README.md +59 -2
  2. package/build/index.d.ts +2333 -193
  3. package/build/index.d.ts.map +1 -1
  4. package/build/index.js +13042 -2334
  5. package/build/index.js.map +1 -1
  6. package/package.json +12 -1
  7. package/src/bench/exact/index.ts +258 -0
  8. package/src/bench/exact/optima-exact-v1-bounds.json +14409 -0
  9. package/src/bench/exact/optima-exact-v1.json +4809 -0
  10. package/src/bench/exact/solve.ts +129 -0
  11. package/src/bench/measures/index.ts +1 -0
  12. package/src/bench/measures/lanes.ts +304 -0
  13. package/src/bench/measures/levels.ts +86 -2
  14. package/src/bench/screen/activation.ts +3 -0
  15. package/src/bench/screen/bounds.ts +140 -0
  16. package/src/bench/screen/compare.ts +26 -0
  17. package/src/bench/screen/exact-set.ts +100 -0
  18. package/src/bench/screen/exercise.ts +290 -0
  19. package/src/bench/screen/forms.ts +25 -5
  20. package/src/bench/screen/gap.ts +316 -0
  21. package/src/bench/screen/hash.ts +7 -76
  22. package/src/bench/screen/index.ts +6 -0
  23. package/src/bench/screen/measures.ts +89 -1
  24. package/src/bench/screen/portfolio-pick.ts +20 -0
  25. package/src/bench/screen/screen.ts +78 -2
  26. package/src/bench/screen/set.ts +3 -2
  27. package/src/channels.ts +56 -0
  28. package/src/highways/areas/index.ts +160 -0
  29. package/src/highways/confluent/index.ts +154 -0
  30. package/src/highways/confluent/k23.test-util.ts +8 -0
  31. package/src/highways/consensus/index.ts +129 -0
  32. package/src/highways/crossbars/index.ts +184 -0
  33. package/src/highways/cubu/index.ts +26 -0
  34. package/src/highways/diffusion/index.ts +103 -0
  35. package/src/highways/divided/index.ts +111 -0
  36. package/src/highways/edge-paths/index.ts +155 -0
  37. package/src/highways/fdeb/index.ts +193 -0
  38. package/src/highways/fixtures.test-util.ts +71 -0
  39. package/src/highways/flow-clusters/index.ts +143 -0
  40. package/src/highways/frechet/index.ts +232 -0
  41. package/src/highways/growth/index.ts +148 -0
  42. package/src/highways/hdbscan/index.ts +250 -0
  43. package/src/highways/hierarchy/index.ts +140 -0
  44. package/src/highways/hough/index.ts +182 -0
  45. package/src/highways/hub-arcs/index.ts +181 -0
  46. package/src/highways/index.ts +79 -0
  47. package/src/highways/kdeeb/index.ts +355 -0
  48. package/src/highways/lanes/index.ts +189 -0
  49. package/src/highways/link-communities/index.ts +189 -0
  50. package/src/highways/mesh/index.ts +209 -0
  51. package/src/highways/mingle/index.ts +171 -0
  52. package/src/highways/mlseb/index.ts +204 -0
  53. package/src/highways/nested/index.ts +143 -0
  54. package/src/highways/p-hub/index.ts +248 -0
  55. package/src/highways/power/index.ts +117 -0
  56. package/src/highways/resistance/index.ts +159 -0
  57. package/src/highways/ridges/index.ts +328 -0
  58. package/src/highways/sbeb/index.ts +303 -0
  59. package/src/highways/scale-space/index.ts +135 -0
  60. package/src/highways/snap.ts +110 -0
  61. package/src/highways/soft-keys/index.ts +114 -0
  62. package/src/highways/spectral/index.ts +391 -0
  63. package/src/highways/spiral/index.ts +205 -0
  64. package/src/highways/territories/index.ts +150 -0
  65. package/src/highways/trace.ts +153 -0
  66. package/src/highways/traclus/index.ts +296 -0
  67. package/src/index.ts +2 -0
  68. package/src/portfolio/bands.ts +33 -0
  69. package/src/portfolio/fit.ts +29 -0
  70. package/src/portfolio/index.ts +6 -0
  71. package/src/portfolio/key.ts +98 -0
  72. package/src/portfolio/members.ts +171 -0
  73. package/src/portfolio/router.ts +133 -0
  74. package/src/portfolio/sets.ts +28 -0
  75. package/src/routers/shared.ts +56 -3
  76. package/src/strategies/builtins.ts +376 -1
  77. package/src/strategies/schemas.ts +460 -1
  78. package/src/visibility.ts +5 -0
@@ -0,0 +1,29 @@
1
+ // This file is generated by `scripts/portfolio-sets.ts` (`pnpm portfolio:sets`). Do not edit it. The test `sets.test.ts` checks that it is current.
2
+ // The times come from the CI run 40658558076 (the workflow `Portfolio`, `portfolio:time --members`).
3
+ import type { PortfolioCap, PortfolioRule } from "./bands.ts";
4
+
5
+ /**
6
+ * The fit of each rule, cap and size band, by the key `<rule>|<cap>|<band>`: the members of the default of the portfolio router, in their order.
7
+ * Each member has a node limit: it runs only on an input with that many nodes or fewer. The limits keep the model time at or below 40 ms.
8
+ * The base `shared` is not in a fit: the portfolio always runs it first.
9
+ */
10
+ export const PORTFOLIO_FIT: Readonly<Record<`${PortfolioRule}|${PortfolioCap}|${string}`, readonly (readonly [string, number])[]>> = {
11
+ "collection|1.1|n1-12": [["corners-side", 12], ["w2c:select+stretch", 12], ["trunks-gilbert-a05", 12], ["w3a:spiral-a15", 12], ["trunks-tree-bf08", 12], ["hw:zipper", 12], ["w2c:level-links+stretch-b1.1", 12], ["hw:stubs", 12]],
12
+ "collection|1.1|n13-24": [["w3e:crossbars+stretch", 24], ["corners-side", 24], ["shared-p2", 24], ["w2c:zipper+stretch-b1.1", 18], ["trunks-tree-bf08", 18], ["hw:weiszfeld", 18]],
13
+ "collection|1.1|n25-40": [["w2a:shared-ways+breakeven", 32], ["w2b:weiszfeld-a1", 32]],
14
+ "collection|1.25|n1-12": [["corners-side", 12], ["trunks-tree-bf06", 12], ["w3f:p-hub-a0.2", 12], ["hw:stubs", 12], ["hw:p-hub", 12], ["hw:spiral", 12], ["w2c:level-links+stretch", 12], ["w3f:spectral-c0.3", 12]],
15
+ "collection|1.25|n13-24": [["w2a:shared-ways+stretch", 24], ["shared-p3", 24], ["trunks-gilbert-a07", 24], ["w3e:crossbars+stretch", 24], ["trunks-tree-bf06", 18], ["w3c:hdbscan+stretch", 18], ["shared-wind-p2-r1-e2", 18], ["hw:stubs", 18]],
16
+ "collection|1.25|n25-40": [["w2a:shared-ways+stretch", 32], ["shared-p3", 32], ["shared-order-shortest", 32]],
17
+ "collection|1.5|n1-12": [["trunks-gilbert-a03", 12], ["w3e:areas-keys", 12], ["trunks-tree-bf02", 12], ["w3f:p-hub-a0.2", 12], ["w2b:zipper-a1", 12], ["w3a:spiral-a35", 12], ["w3f:spectral-c0.3", 12], ["w2c:level-links+stretch", 12]],
18
+ "collection|1.5|n13-24": [["shared-wind-p2-r0.5", 24], ["trunks-gilbert-a03", 24], ["w3e:crossbars+stretch", 24], ["shared-wind-p2-r1-e2", 24], ["trunks-tree-bf06", 24], ["shared-flow-a06-p3", 18], ["shared-wind-p3-r1", 18], ["trunks-gilbert-a07", 18]],
19
+ "collection|1.5|n25-40": [["shared-p3", 40], ["hw:lanes", 32], ["shared-wind-p2-r1-e2", 32]],
20
+ "all|1.1|n1-12": [["w2b:zipper-p2", 12], ["hw:cubu", 12], ["w3d:kdeeb+stretch", 12], ["w3c:hough-wide", 12], ["w3d:kdeeb-routes", 12], ["hw:stubs", 12], ["hw:spiral", 12], ["w3b:growth-s0.7", 12]],
21
+ "all|1.1|n13-24": [["w3e:frechet-e0.5", 24], ["w3a:diffusion-wide", 24], ["w2c:zipper+stretch-b1.1", 24], ["w3d:cubu+stretch", 18]],
22
+ "all|1.1|n25-40": [["w2c:zipper+stretch-b1.1", 32], ["w3a:spiral-a15", 32]],
23
+ "all|1.25|n1-12": [["w3d:kdeeb-routes", 12], ["w3e:mlseb+stretch", 12], ["hw:growth", 12], ["hw:level-links", 12], ["w3b:growth-input", 12], ["w3e:confluent+stretch", 12], ["w3d:kdeeb-nomask", 10], ["w3a:diffusion-g2", 10]],
24
+ "all|1.25|n13-24": [["w3d:kdeeb-routes", 24], ["w3e:mlseb+stretch", 18]],
25
+ "all|1.25|n25-40": [["w3d:kdeeb-routes", 32]],
26
+ "all|1.5|n1-12": [["w3b:growth-s0.3", 12], ["w3e:mlseb-a0", 12], ["w3e:mlseb-routes", 10], ["w3c:edge-paths-k1.5", 10]],
27
+ "all|1.5|n13-24": [["hw:growth", 24], ["w3b:growth-input", 24]],
28
+ "all|1.5|n25-40": [["hw:growth", 40], ["w3b:growth-s0.7", 32], ["w3b:growth-s0.3", 32]],
29
+ };
@@ -0,0 +1,6 @@
1
+ export * from "./bands.ts";
2
+ export * from "./fit.ts";
3
+ export * from "./key.ts";
4
+ export * from "./members.ts";
5
+ export * from "./router.ts";
6
+ export * from "./sets.ts";
@@ -0,0 +1,98 @@
1
+ // The key of the portfolio router (PORTFOLIO-FIT): one guard for the router, the records and the greedy choice of the sets.
2
+ // A member passes the guard when it has no more breaks of each hard rule than the base, a stretch at or below the cap, and, under `collection`, no mixed segment.
3
+ // The base `shared` is the fallback output, so it passes the guard at each cap. The script `portfolio.ts` applies the same guard to the records.
4
+ import { exactStretches } from "../bench/exact/index.ts";
5
+ import { inkSaved } from "../bench/measures/bundles.ts";
6
+ import { checkProperties } from "../bench/properties.ts";
7
+ import { resultHash } from "../bench/screen/exercise.ts";
8
+ import { mixedSegments } from "../bench/screen/gap.ts";
9
+ import type { CompareSpec } from "../bench/screen/compare.ts";
10
+ import { compareSpec, measureByKey } from "../bench/screen/measures.ts";
11
+ import type { PortfolioKey } from "../bench/screen/portfolio-pick.ts";
12
+ import type { Graph, Result } from "../model.ts";
13
+ import type { PortfolioCap, PortfolioRule } from "./bands.ts";
14
+
15
+ /** The id of the B1 level of the key of the portfolio router. */
16
+ export const PORTFOLIO_B1 = "B1";
17
+
18
+ /** This function gives the spec of the B1 level: higher is better, with the `minEffect` of the row `B1.inkSaved` of the table `MEASURES`. */
19
+ export function portfolioB1Spec(): CompareSpec {
20
+ const info = measureByKey("B1.inkSaved");
21
+ if (info === undefined) throw new Error("The table MEASURES has no row B1.inkSaved.");
22
+ return compareSpec(info);
23
+ }
24
+
25
+ /** The facts of one output that the guard reads. The router takes them from the output, and the script `portfolio.ts` from a record. */
26
+ export interface PortfolioFacts {
27
+ /** The count of the breaks of each hard rule. */
28
+ readonly hard: Readonly<Record<string, number>>;
29
+ /** The count of the edges with a stretch above each cap, by the cap. The largest stretch of a record gives 1 or 0. */
30
+ readonly over: (cap: number) => number;
31
+ /** The count of the shared segments that hold edges of two collections. */
32
+ readonly mixed: number;
33
+ }
34
+
35
+ /**
36
+ * This function gives the count of the breaks of the guard of an output against the base, with the rule `rule` and the cap `cap`.
37
+ * The count is the sum of three parts:
38
+ * - for each hard rule, the breaks above the breaks of the base
39
+ * - the edges with a stretch above the cap
40
+ * - under the rule `collection`, the mixed segments.
41
+ * An output passes the guard when the count is 0.
42
+ */
43
+ export function guardBreaks(facts: PortfolioFacts, base: PortfolioFacts | undefined, rule: PortfolioRule, cap: number): number {
44
+ let n = 0;
45
+ for (const [k, v] of Object.entries(facts.hard)) n += Math.max(0, v - (base?.hard[k] ?? 0));
46
+ n += facts.over(cap);
47
+ if (rule === "collection") n += facts.mixed;
48
+ return n;
49
+ }
50
+
51
+ /** This function counts the breaks of each hard rule in a list of breaks. */
52
+ function countRules(breaks: readonly { readonly rule: string }[]): Record<string, number> {
53
+ const out: Record<string, number> = {};
54
+ for (const b of breaks) out[b.rule] = (out[b.rule] ?? 0) + 1;
55
+ return out;
56
+ }
57
+
58
+ /**
59
+ * This function gives the function of the facts of the outputs on the graph `graph`. It builds the network of the stretch one time for the graph.
60
+ * The hard rules are the rules of `checkProperties`. The stretch is the stretch of the exact model, as the records of the gap screen give it.
61
+ */
62
+ export function portfolioFacts(graph: Graph): (result: Result) => PortfolioFacts {
63
+ // The network of the stretch is built at the first call. A portfolio of the base alone does not build it.
64
+ let stretches: ReturnType<typeof exactStretches> | undefined;
65
+ return (result) => {
66
+ stretches ??= exactStretches(graph);
67
+ const values = [...stretches(result.routes).values()];
68
+ return {
69
+ hard: countRules(checkProperties(graph, result, 1_000_000)),
70
+ over: (cap) => values.filter((v) => v > cap + 1e-9).length,
71
+ mixed: mixedSegments(graph, result),
72
+ };
73
+ };
74
+ }
75
+
76
+ /**
77
+ * This function gives the key of the portfolio router on the graph `graph`, with the rule `rule` and the cap `cap`.
78
+ * - `hard` is `guardBreaks` against the output of the base. `base` gives that output after the base runs. The output of the base has 0 breaks.
79
+ * Before the base runs, or when the base throws, a member has its own breaks of the hard rules.
80
+ * - The one level `B1` is the ink saved (higher is better), with the `minEffect` of the table `MEASURES`.
81
+ * - The hash is `resultHash`.
82
+ * The winner is the first member with the most B1 of the members with 0 breaks (`selectBest`). The base has 0 breaks, so a member that fails the guard does not win.
83
+ */
84
+ export function portfolioKey(graph: Graph, rule: PortfolioRule, cap: PortfolioCap, base: () => Result | undefined = () => undefined): PortfolioKey<Graph, Result> {
85
+ const facts = portfolioFacts(graph);
86
+ const spec = portfolioB1Spec();
87
+ let baseFacts: { readonly of: Result; readonly facts: PortfolioFacts } | undefined;
88
+ return {
89
+ hard: (_g, result) => {
90
+ const b = base();
91
+ if (b !== undefined && result === b) return 0;
92
+ if (b !== undefined && baseFacts?.of !== b) baseFacts = { of: b, facts: facts(b) };
93
+ return guardBreaks(facts(result), b === undefined ? undefined : baseFacts!.facts, rule, cap);
94
+ },
95
+ levels: [{ id: PORTFOLIO_B1, spec, value: (_g, result) => inkSaved(result.routes) }],
96
+ hash: resultHash,
97
+ };
98
+ }
@@ -0,0 +1,171 @@
1
+ // The members of the portfolio router (PORTFOLIO-API). Each member is a router strategy with the name of its records in the gap screen.
2
+ // A built-in highway strategy has the name `hw:<name>`, and an option of an idea wave has the name `<wave>:<name>`. A router option has its own name.
3
+ // The test `members.test.ts` checks that each member gives the same output hash as the strategy of the same name in the screen module of MEAS2.
4
+ import { ambiguityGate } from "../highways/gates/ambiguity/index.ts";
5
+ import { breakEvenGate } from "../highways/gates/breakeven/index.ts";
6
+ import { gateStrategy } from "../highways/gates/model.ts";
7
+ import { stretchGate } from "../highways/gates/stretch/index.ts";
8
+ import type { HighwayInput, HighwayPlan } from "../highways/model.ts";
9
+ import { highwayRouterStrategy } from "../highways/router.ts";
10
+ import type { Graph, Result } from "../model.ts";
11
+ import { cornersRouter, type CornersOptions } from "../routers/corners.ts";
12
+ import { sharedRouter, type SharedOptions } from "../routers/shared.ts";
13
+ import { byCollection, trunksRouter, type TrunksOptions } from "../routers/trunks.ts";
14
+ import { highwayStrategies } from "../strategies/builtins.ts";
15
+ import { defineStrategy, type Strategy } from "../strategies/model.ts";
16
+ import { sharedWaysAll } from "../bench/screen/gap.ts";
17
+
18
+ /** The name of a built-in highway strategy. */
19
+ type HighwayName = keyof typeof highwayStrategies;
20
+
21
+ /**
22
+ * The form of one member:
23
+ * - `["shared", options]`, `["trunks", options]` or `["corners", options]`: the router with the options. `shared` and `trunks` use the rule `byCollection`.
24
+ * - `["highway", name]`: the built-in highway strategy `name`, or `shared-ways-all`, with `follow`.
25
+ * - `["gate", name, gate, budget?]`: the built-in highway strategy with the gate `stretch`, `breakeven` or `ambiguity`. `budget` sets the budget of the gate `stretch`.
26
+ * - `["plan", name, options]`: the plan of the built-in highway strategy `name` with other options.
27
+ */
28
+ export type MemberForm =
29
+ | readonly ["shared", Omit<SharedOptions, "name" | "rule">]
30
+ | readonly ["trunks", Omit<TrunksOptions, "name" | "rule">]
31
+ | readonly ["corners", Omit<CornersOptions, "name">]
32
+ | readonly ["highway", HighwayName | "shared-ways-all"]
33
+ | readonly ["gate", HighwayName, "stretch" | "breakeven" | "ambiguity", number?]
34
+ | readonly ["plan", HighwayName, Readonly<Record<string, unknown>>];
35
+
36
+ /** The forms of the members, by the names of the records. `shared` is the base. The greedy sets of `sets.ts` name only these members. */
37
+ export const MEMBER_FORMS: Readonly<Record<string, MemberForm>> = {
38
+ shared: ["shared", {}],
39
+ "corners-side": ["corners", { plugs: "side" }],
40
+ "shared-flow-a06-p3": ["shared", { cost: "flow", alpha: 0.6, passes: 3 }],
41
+ "shared-order-shortest": ["shared", { order: "shortest" }],
42
+ "shared-p2": ["shared", { passes: 2 }],
43
+ "shared-p3": ["shared", { passes: 3 }],
44
+ "shared-wind-p2-r0.5": ["shared", { passes: 2, reuse: 0.5 }],
45
+ "shared-wind-p2-r1": ["shared", { passes: 2, reuse: 1 }],
46
+ "shared-wind-p2-r1-e2": ["shared", { passes: 2, reuse: 1, reuseExponent: 2 }],
47
+ "shared-wind-p2-r2": ["shared", { passes: 2, reuse: 2 }],
48
+ "shared-wind-p3-r1": ["shared", { passes: 3, reuse: 1 }],
49
+ "trunks-gilbert-a03": ["trunks", { splitAt: "gilbert", alpha: 0.3 }],
50
+ "trunks-gilbert-a05": ["trunks", { splitAt: "gilbert", alpha: 0.5 }],
51
+ "trunks-gilbert-a07": ["trunks", { splitAt: "gilbert", alpha: 0.7 }],
52
+ "trunks-tree-bf02": ["trunks", { fan: "tree", bf: 0.2 }],
53
+ "trunks-tree-bf06": ["trunks", { fan: "tree", bf: 0.6 }],
54
+ "trunks-tree-bf08": ["trunks", { fan: "tree", bf: 0.8 }],
55
+ "hw:crossbars": ["highway", "crossbars"],
56
+ "hw:cubu": ["highway", "cubu"],
57
+ "hw:diffusion": ["highway", "diffusion"],
58
+ "hw:growth": ["highway", "growth"],
59
+ "hw:lanes": ["highway", "lanes"],
60
+ "hw:level-links": ["highway", "level-links"],
61
+ "hw:mesh": ["highway", "mesh"],
62
+ "hw:mlseb": ["highway", "mlseb"],
63
+ "hw:p-hub": ["highway", "p-hub"],
64
+ "hw:shared-ways-all": ["highway", "shared-ways-all"],
65
+ "hw:spiral": ["highway", "spiral"],
66
+ "hw:stubs": ["highway", "stubs"],
67
+ "hw:weiszfeld": ["highway", "weiszfeld"],
68
+ "hw:zipper": ["highway", "zipper"],
69
+ "hw:areas": ["highway", "areas"],
70
+ "w2a:shared-ways+ambiguity": ["gate", "shared-ways", "ambiguity"],
71
+ "w2a:shared-ways+breakeven": ["gate", "shared-ways", "breakeven"],
72
+ "w2a:shared-ways+stretch": ["gate", "shared-ways", "stretch"],
73
+ "w2b:weiszfeld-a0": ["plan", "weiszfeld", { alpha: 0 }],
74
+ "w2b:weiszfeld-a1": ["plan", "weiszfeld", { alpha: 1 }],
75
+ "w2b:zipper-a1": ["plan", "zipper", { adhesion: 1 }],
76
+ "w2b:zipper-p2": ["plan", "zipper", { passes: 2 }],
77
+ "w2c:level-links+stretch": ["gate", "level-links", "stretch"],
78
+ "w2c:level-links+stretch-b1.1": ["gate", "level-links", "stretch", 1.1],
79
+ "w2c:road-classes-d0.3": ["plan", "road-classes", { discount: 0.3 }],
80
+ "w2c:select+stretch": ["gate", "select", "stretch"],
81
+ "w2c:zipper+stretch-b1.1": ["gate", "zipper", "stretch", 1.1],
82
+ "w3a:diffusion+stretch": ["gate", "diffusion", "stretch"],
83
+ "w3a:diffusion-g2": ["plan", "diffusion", { gamma: 2 }],
84
+ "w3a:diffusion-l3": ["plan", "diffusion", { lambda: 3 }],
85
+ "w3a:diffusion-wide": ["plan", "diffusion", { a: 0.5, steps: 6 }],
86
+ "w3a:spiral-a15": ["plan", "spiral", { alpha: 15 }],
87
+ "w3a:spiral-a35": ["plan", "spiral", { alpha: 35 }],
88
+ "w3a:spiral-n3": ["plan", "spiral", { minTargets: 3 }],
89
+ "w3b:growth-input": ["plan", "growth", { order: "input" }],
90
+ "w3b:growth-s0.3": ["plan", "growth", { share: 0.3 }],
91
+ "w3b:growth-s0.7": ["plan", "growth", { share: 0.7 }],
92
+ "w3b:growth-t1.2": ["plan", "growth", { stretch: 1.2 }],
93
+ "w3c:edge-paths-k1.5": ["plan", "edge-paths", { distortion: 1.5 }],
94
+ "w3c:hdbscan+stretch": ["gate", "hdbscan", "stretch"],
95
+ "w3c:hough-wide": ["plan", "hough", { dTheta: 12, dRho: 24 }],
96
+ "w3d:cubu+stretch": ["gate", "cubu", "stretch"],
97
+ "w3d:kdeeb+stretch": ["gate", "kdeeb", "stretch"],
98
+ "w3d:kdeeb-nomask": ["plan", "kdeeb", { mask: false }],
99
+ "w3d:kdeeb-routes": ["plan", "kdeeb", { source: "routes" }],
100
+ "w3d:mesh+stretch": ["gate", "mesh", "stretch"],
101
+ "w3e:areas-keys": ["plan", "areas", { group: "keys" }],
102
+ "w3e:confluent+stretch": ["gate", "confluent", "stretch"],
103
+ "w3e:crossbars+stretch": ["gate", "crossbars", "stretch"],
104
+ "w3e:frechet+stretch": ["gate", "frechet", "stretch"],
105
+ "w3e:frechet-e0.5": ["plan", "frechet", { eps: 0.5 }],
106
+ "w3e:mlseb+stretch": ["gate", "mlseb", "stretch"],
107
+ "w3e:mlseb-a0": ["plan", "mlseb", { angleWeight: 0 }],
108
+ "w3e:mlseb-parabola": ["plan", "mlseb", { fit: "parabola" }],
109
+ "w3e:mlseb-routes": ["plan", "mlseb", { source: "routes" }],
110
+ "w3f:p-hub-a0.2": ["plan", "p-hub", { alpha: 0.2 }],
111
+ "w3f:spectral-c0.3": ["plan", "spectral", { c0: 0.3 }],
112
+ "w3f:territories-r2": ["plan", "territories", { radius: 2 }],
113
+ };
114
+
115
+ const GATES = { stretch: stretchGate, breakeven: breakEvenGate, ambiguity: ambiguityGate } as const;
116
+
117
+ /** This function gives a highway strategy with no options: it runs `run`, with the node limit `maxNodes`. As in the screen, `follow` then gets no way options. */
118
+ function fixedHighway(name: string, run: (input: HighwayInput) => HighwayPlan, maxNodes: number | undefined): Strategy<"highway", object> {
119
+ return defineStrategy({ step: "highway", name, describe: `The member ${name} of the portfolio router.`, defaults: {}, schema: {}, maxNodes: maxNodes ?? 10_000, run });
120
+ }
121
+
122
+ /** This function gives a router strategy with no options: it runs `route`. */
123
+ function fixedRouter(name: string, route: (graph: Graph) => Result): Strategy<"router", object> {
124
+ return defineStrategy({ step: "router", name, describe: `The member ${name} of the portfolio router.`, defaults: {}, schema: {}, maxNodes: 10_000, run: (graph: Graph) => route(graph) });
125
+ }
126
+
127
+ /** This function gives the router strategy of a member form, with the name `name`. */
128
+ export function memberStrategy(name: string, form: MemberForm): Strategy<"router", object> {
129
+ const highway = (h: Strategy<"highway", object>): Strategy<"router", object> => {
130
+ const r = highwayRouterStrategy(h);
131
+ return defineStrategy({ step: "router", name, describe: r.describe, defaults: r.defaults, ...(r.schema === undefined ? {} : { schema: r.schema }), maxNodes: r.maxNodes ?? 10_000, run: r.run });
132
+ };
133
+ switch (form[0]) {
134
+ case "shared": {
135
+ const o = form[1];
136
+ return fixedRouter(name, (g) => sharedRouter({ ...o, rule: byCollection, name }).route(g));
137
+ }
138
+ case "trunks": {
139
+ const o = form[1];
140
+ return fixedRouter(name, (g) => trunksRouter({ ...o, rule: byCollection, name }).route(g));
141
+ }
142
+ case "corners": {
143
+ const o = form[1];
144
+ return fixedRouter(name, (g) => cornersRouter({ ...o, name }).route(g));
145
+ }
146
+ case "highway":
147
+ return highway(form[1] === "shared-ways-all" ? sharedWaysAll : (highwayStrategies[form[1]] as Strategy<"highway", object>));
148
+ case "gate": {
149
+ const gated = gateStrategy(highwayStrategies[form[1]] as Strategy<"highway", object>, GATES[form[2]] as Parameters<typeof gateStrategy>[1]);
150
+ const budget = form[3];
151
+ if (budget === undefined) return highway(gated);
152
+ const options = { ...gated.defaults, budget };
153
+ return highway(fixedHighway(name, (input) => gated.run(input, options), gated.maxNodes));
154
+ }
155
+ case "plan": {
156
+ const base = highwayStrategies[form[1]] as Strategy<"highway", object>;
157
+ const options = { ...base.defaults, ...form[2] };
158
+ return highway(fixedHighway(name, (input) => base.run(input, options), base.maxNodes));
159
+ }
160
+ }
161
+ }
162
+
163
+ const cache = new Map<string, Strategy<"router", object>>();
164
+
165
+ /** This function gives the member with the name `name`, or `undefined` for a name without a form in `MEMBER_FORMS`. */
166
+ export function portfolioMember(name: string): Strategy<"router", object> | undefined {
167
+ const form = MEMBER_FORMS[name];
168
+ if (form === undefined) return undefined;
169
+ if (!cache.has(name)) cache.set(name, memberStrategy(name, form));
170
+ return cache.get(name);
171
+ }
@@ -0,0 +1,133 @@
1
+ // The portfolio router (PORTFOLIO-API, PORTFOLIO-FIT): it runs the base `shared` and a small greedy set of members on an input, and gives the output with the most
2
+ // ink saved inside the rule and the stretch cap of the caller. The combinator is `portfolio` of `@mark1russell7/layout-contract`.
3
+ // The router is opt-in: it is not in the registry `STRATEGIES`, so no default output changes.
4
+ import { pickMoves, portfolio, type Member, type PortfolioTrace } from "../bench/screen/portfolio-pick.ts";
5
+ import type { Graph, Result, Router } from "../model.ts";
6
+ import { asRouter, defineStrategy, type Strategy } from "../strategies/model.ts";
7
+ import { PORTFOLIO_CAPS, PORTFOLIO_KS_API, PORTFOLIO_RULES, portfolioBand, type PortfolioCap, type PortfolioRule } from "./bands.ts";
8
+ import { PORTFOLIO_FIT } from "./fit.ts";
9
+ import { portfolioKey } from "./key.ts";
10
+ import { portfolioMember } from "./members.ts";
11
+ import { PORTFOLIO_SETS } from "./sets.ts";
12
+
13
+ /** The options of the portfolio router. */
14
+ export interface PortfolioRouterOptions {
15
+ /** The rule of the caller. Under `collection` (the default), a shared segment must hold edges of one collection only. Under `all`, collections can mix. */
16
+ readonly rule?: PortfolioRule;
17
+ /** The stretch cap of the caller: 1.1, 1.25 or 1.5 (the default). An output with an edge above the cap does not win. The base is the fallback output. */
18
+ readonly cap?: PortfolioCap;
19
+ /**
20
+ * The count of members after the base: 3, 5 or 8. Without `k` (the default), the router runs the fit of `fit.ts`.
21
+ * The fit gives a member count for each rule, cap and band, and a node limit for each member. Thus the default is deterministic.
22
+ */
23
+ readonly k?: 3 | 5 | 8;
24
+ /**
25
+ * A time limit of the pick in milliseconds, as a ceiling that the caller sets. The base always runs. The combinator cuts the members that start after the limit.
26
+ * A cut by the budget makes the output depend on the speed of the machine. Then the trace has `deterministic: false`.
27
+ */
28
+ readonly budgetMs?: number;
29
+ /** The largest count of members that run, with the base. */
30
+ readonly maxMembers?: number;
31
+ /** The clock in milliseconds. The default is `performance.now`. */
32
+ readonly now?: () => number;
33
+ }
34
+
35
+ /** The base member of each portfolio: the router `shared` with the rule `byCollection`. It is the fallback output above the cap. */
36
+ export const PORTFOLIO_BASE_MEMBER = "shared";
37
+
38
+ /** The result of the portfolio router: the output of the winner, the winner, the trace of the pick, the move counts and the band. */
39
+ export interface PortfolioRoute {
40
+ readonly result: Result;
41
+ readonly winner: string;
42
+ readonly trace: PortfolioTrace;
43
+ /** The move count of the pick (ACTIVATION): `{ "move.pick": 1 }` when the winner is not the base and has another output. */
44
+ readonly moves: Readonly<Record<string, number>>;
45
+ /** The size band of the input, from its node count. */
46
+ readonly band: string;
47
+ /** The names of the members, in their order. */
48
+ readonly members: readonly string[];
49
+ }
50
+
51
+ /** This function checks the options of the portfolio router, and gives them with their defaults. It throws a `RangeError` for a bad value. */
52
+ function checked(o: PortfolioRouterOptions): { rule: PortfolioRule; cap: PortfolioCap; k: 3 | 5 | 8 | undefined } {
53
+ const rule = o.rule ?? "collection";
54
+ const cap = o.cap ?? 1.5;
55
+ const k = o.k;
56
+ if (!PORTFOLIO_RULES.includes(rule)) throw new RangeError(`The portfolio router has the rule "${String(rule)}". It must be ${PORTFOLIO_RULES.join(" or ")}.`);
57
+ if (!PORTFOLIO_CAPS.includes(cap)) throw new RangeError(`The portfolio router has the cap ${String(cap)}. It must be ${PORTFOLIO_CAPS.join(", ")}.`);
58
+ if (k !== undefined && !PORTFOLIO_KS_API.includes(k)) throw new RangeError(`The portfolio router has the k ${String(k)}. It must be ${PORTFOLIO_KS_API.join(", ")}.`);
59
+ return { rule, cap, k };
60
+ }
61
+
62
+ /** This function gives the name of the portfolio router with the options `options`: `portfolio-<rule>-<cap>-k<k>`, or `portfolio-<rule>-<cap>-fit` without `k`. */
63
+ export function portfolioName(options: PortfolioRouterOptions = {}): string {
64
+ const { rule, cap, k } = checked(options);
65
+ return `portfolio-${rule}-${cap}-${k === undefined ? "fit" : `k${k}`}`;
66
+ }
67
+
68
+ /**
69
+ * This function gives the names of the members for an input with `nodes` nodes. The base comes first.
70
+ * - With `k`, the first `k` names of the set of the band follow.
71
+ * - Without `k`, the members of the fit of the band follow, each only when `nodes` is at or below its node limit.
72
+ */
73
+ export function portfolioMembers(nodes: number, options: PortfolioRouterOptions = {}): string[] {
74
+ const { rule, cap, k } = checked(options);
75
+ const key: `${PortfolioRule}|${PortfolioCap}|${string}` = `${rule}|${cap}|${portfolioBand(nodes).id}`;
76
+ if (k === undefined) return [PORTFOLIO_BASE_MEMBER, ...(PORTFOLIO_FIT[key] ?? []).filter(([s, limit]) => s !== PORTFOLIO_BASE_MEMBER && nodes <= limit).map(([s]) => s)];
77
+ const set = PORTFOLIO_SETS[key] ?? [];
78
+ return [PORTFOLIO_BASE_MEMBER, ...set.filter((s) => s !== PORTFOLIO_BASE_MEMBER).slice(0, k)];
79
+ }
80
+
81
+ /**
82
+ * This function routes the graph `graph` with the portfolio of its size band, rule and cap. The base runs first, then the members in their order.
83
+ * The key is `portfolioKey`: a member wins only when it passes the guard against the base. Of those members, the winner has the most ink saved.
84
+ * A tie within the `minEffect` of B1 goes to the first member (`selectBest`). A member that throws does not win.
85
+ * The result carries the trace of the pick and the move count `move.pick`.
86
+ */
87
+ export function routePortfolio(graph: Graph, options: PortfolioRouterOptions = {}): PortfolioRoute {
88
+ // A member with a node limit below the size of the graph does not run. The base `shared` has no such limit below the limit of the router.
89
+ const names = portfolioMembers(graph.nodes.length, options).filter((name) => name === PORTFOLIO_BASE_MEMBER || (portfolioMember(name)?.maxNodes ?? Infinity) >= graph.nodes.length);
90
+ const { rule, cap } = checked(options);
91
+ // The key compares each member with the output of the base, so the run of the base keeps its output.
92
+ let base: Result | undefined;
93
+ const members: Member<Graph, Result>[] = names.map((name) => {
94
+ const s = portfolioMember(name);
95
+ if (s === undefined) throw new Error(`The portfolio router has no member "${name}".`);
96
+ const router = asRouter(s);
97
+ if (name !== PORTFOLIO_BASE_MEMBER) return { id: name, run: (g: Graph) => router.route(g) };
98
+ return {
99
+ id: name,
100
+ run: (g: Graph) => {
101
+ base = router.route(g);
102
+ return base;
103
+ },
104
+ };
105
+ });
106
+ const p = portfolio(portfolioName(options), members, portfolioKey(graph, rule, cap, () => base), {
107
+ ...(options.budgetMs === undefined ? {} : { budgetMs: options.budgetMs }),
108
+ ...(options.maxMembers === undefined ? {} : { maxMembers: options.maxMembers }),
109
+ ...(options.now === undefined ? {} : { now: options.now }),
110
+ });
111
+ const pick = p.pick(graph);
112
+ const moves = Object.fromEntries(Object.entries(pickMoves(pick.trace)).map(([k, v]) => [`move.${k}`, v]));
113
+ return { result: pick.output, winner: pick.winner, trace: pick.trace, moves, band: portfolioBand(graph.nodes.length).id, members: names };
114
+ }
115
+
116
+ /** This function gives a router that routes each graph with `routePortfolio` and the options `options`. Its name is `portfolioName(options)`. */
117
+ export function portfolioRouter(options: PortfolioRouterOptions = {}): Router {
118
+ checked(options);
119
+ return { name: portfolioName(options), maxNodes: 10_000, route: (graph) => routePortfolio(graph, options).result };
120
+ }
121
+
122
+ /**
123
+ * The portfolio router as a router strategy. It is not in the registry `STRATEGIES`, so the benchmark and the site do not race it by default.
124
+ * A caller adds it to a registry of its own, or calls `routePortfolio` for the trace.
125
+ */
126
+ export const portfolioStrategy: Strategy<"router", PortfolioRouterOptions> = defineStrategy<"router", PortfolioRouterOptions>({
127
+ step: "router",
128
+ name: "portfolio",
129
+ describe: "The portfolio router (PORTFOLIO-API). It runs `shared` and the fit of the size band, and gives the output with the most ink saved inside the rule and the cap.",
130
+ defaults: { rule: "collection", cap: 1.5 },
131
+ maxNodes: 10_000,
132
+ run: (graph, options) => routePortfolio(graph, options).result,
133
+ });
@@ -0,0 +1,28 @@
1
+ // This file is generated by `scripts/portfolio-sets.ts` (`pnpm portfolio:sets`). Do not edit it. The test `sets.test.ts` checks that it is current.
2
+ // The records come from the CI runs 40631516113, 40644555345, 40644557536, 40644560237, 40644562652, 40644565132, 40644567956, 40644570455, 40644572910, 40644575649, 40644578541, 40644580965, 40644583603.
3
+ import type { PortfolioCap, PortfolioRule } from "./bands.ts";
4
+
5
+ /**
6
+ * The greedy set of each rule, cap and size band, by the key `<rule>|<cap>|<band>`. The set of k strategies is the first k names.
7
+ * Each set comes from the train seeds (0 to 4) of its band. The base `shared` is not in a set: the portfolio always runs it first.
8
+ */
9
+ export const PORTFOLIO_SETS: Readonly<Record<`${PortfolioRule}|${PortfolioCap}|${string}`, readonly string[]>> = {
10
+ "collection|1.1|n1-12": ["corners-side", "w2c:select+stretch", "trunks-gilbert-a05", "w3a:spiral-a15", "trunks-tree-bf08", "hw:zipper", "w2c:level-links+stretch-b1.1", "hw:stubs"],
11
+ "collection|1.1|n13-24": ["w3e:crossbars+stretch", "corners-side", "shared-p2", "w2c:zipper+stretch-b1.1", "trunks-tree-bf08", "hw:weiszfeld", "trunks-gilbert-a07", "hw:stubs"],
12
+ "collection|1.1|n25-40": ["w2a:shared-ways+breakeven", "w2b:weiszfeld-a1", "w2c:level-links+stretch-b1.1"],
13
+ "collection|1.25|n1-12": ["corners-side", "trunks-tree-bf06", "w3f:p-hub-a0.2", "hw:stubs", "hw:p-hub", "hw:spiral", "w2c:level-links+stretch", "w3f:spectral-c0.3"],
14
+ "collection|1.25|n13-24": ["w2a:shared-ways+stretch", "shared-p3", "trunks-gilbert-a07", "w3e:crossbars+stretch", "trunks-tree-bf06", "w3c:hdbscan+stretch", "shared-wind-p2-r1-e2", "hw:stubs"],
15
+ "collection|1.25|n25-40": ["w2a:shared-ways+stretch", "shared-p3", "shared-order-shortest", "w2b:weiszfeld-a1", "w3e:crossbars+stretch", "trunks-tree-bf08", "w2a:shared-ways+ambiguity", "w2b:weiszfeld-a0"],
16
+ "collection|1.5|n1-12": ["trunks-gilbert-a03", "w3e:areas-keys", "trunks-tree-bf02", "w3f:p-hub-a0.2", "w2b:zipper-a1", "w3a:spiral-a35", "w3f:spectral-c0.3", "w2c:level-links+stretch"],
17
+ "collection|1.5|n13-24": ["shared-wind-p2-r0.5", "trunks-gilbert-a03", "w3e:crossbars+stretch", "shared-wind-p2-r1-e2", "trunks-tree-bf06", "shared-flow-a06-p3", "shared-wind-p3-r1", "trunks-gilbert-a07"],
18
+ "collection|1.5|n25-40": ["shared-p3", "hw:lanes", "shared-wind-p2-r1-e2", "hw:crossbars", "shared-flow-a06-p3", "trunks-tree-bf06", "shared-wind-p2-r1", "shared-wind-p2-r2"],
19
+ "all|1.1|n1-12": ["w2b:zipper-p2", "hw:cubu", "w3d:kdeeb+stretch", "w3c:hough-wide", "w3d:kdeeb-routes", "hw:stubs", "hw:spiral", "w3b:growth-s0.7"],
20
+ "all|1.1|n13-24": ["w3e:frechet-e0.5", "w3a:diffusion-wide", "w2c:zipper+stretch-b1.1", "w3d:cubu+stretch", "w3a:spiral-a15", "w3d:kdeeb-routes", "hw:diffusion", "hw:mesh"],
21
+ "all|1.1|n25-40": ["w2c:zipper+stretch-b1.1", "w3a:spiral-a15", "w3d:mesh+stretch", "w3a:diffusion+stretch", "w3a:diffusion-wide", "w2c:road-classes-d0.3", "w3d:kdeeb-routes"],
22
+ "all|1.25|n1-12": ["w3d:kdeeb-routes", "w3e:mlseb+stretch", "hw:growth", "hw:level-links", "w3b:growth-input", "w3e:confluent+stretch", "w3d:kdeeb-nomask", "w3a:diffusion-g2"],
23
+ "all|1.25|n13-24": ["w3d:kdeeb-routes", "w3e:mlseb+stretch", "w3f:p-hub-a0.2", "w3a:spiral-n3", "w3a:diffusion-l3", "trunks-tree-bf06", "hw:cubu", "w3a:spiral-a35"],
24
+ "all|1.25|n25-40": ["w3d:kdeeb-routes", "hw:spiral", "hw:shared-ways-all", "w3a:diffusion-g2", "w3e:frechet+stretch", "hw:cubu"],
25
+ "all|1.5|n1-12": ["w3b:growth-s0.3", "w3e:mlseb-a0", "w3e:mlseb-routes", "w3c:edge-paths-k1.5", "w3f:territories-r2", "hw:mlseb", "hw:growth", "w3e:mlseb-parabola"],
26
+ "all|1.5|n13-24": ["hw:growth", "w3b:growth-input", "hw:mlseb", "w3e:mlseb+stretch", "w3b:growth-s0.3"],
27
+ "all|1.5|n25-40": ["hw:growth", "w3b:growth-s0.7", "w3b:growth-s0.3", "w3b:growth-t1.2", "w3b:growth-input"],
28
+ };
@@ -1,5 +1,6 @@
1
1
  import { center, dist, type Point } from "../geom.ts";
2
2
  import { nodeIndex, type Edge, type Graph, type GroupRule, type Result, type Route, type Router, type SharedRun } from "../model.ts";
3
+ import { channelWidth } from "../channels.ts";
3
4
  import { placePlug, type Plug } from "../plugs.ts";
4
5
  import { buildSections, type Section } from "../sections.ts";
5
6
  import { orderLanes, type RunOrder } from "../segments.ts";
@@ -44,6 +45,20 @@ export interface SharedOptions extends WayOptions {
44
45
  * With `snell`, the route meets the run at the angle `acos(d)`, with `d` the cost share of the run for the route (NAT-035).
45
46
  */
46
47
  readonly merge?: ShareMerge;
48
+ /**
49
+ * The pull of the busy links of the pass before (HWY-005, Winding Roads). With a value above 0 and two passes or more, each later pass routes each edge again from no ways.
50
+ * A link then costs `len / (1 + reuse * u)^reuseExponent`, with `u` the edges of the key on it in the pass before. The default is 0: the later passes are rip-up passes.
51
+ */
52
+ readonly reuse?: number;
53
+ /** The exponent of the pull of `reuse`. The default is 1. */
54
+ readonly reuseExponent?: number;
55
+ /**
56
+ * The cost of a lane that does not fit in its channel (HWY-016). One more edge on a link with `f` edges of the key costs `capacity * max(0, (f + 1) * laneWidth - width)` more.
57
+ * `width` is the free space across the link (`channelWidth`). The default is 0: the router does not read the channels.
58
+ */
59
+ readonly capacity?: number;
60
+ /** The width of one lane, for `capacity`. The default is 4. */
61
+ readonly laneWidth?: number;
47
62
  /** The benchmark skips a fixture with more nodes. */
48
63
  readonly maxNodes?: number;
49
64
  }
@@ -94,6 +109,10 @@ export function sharedRouter(options: SharedOptions): Router {
94
109
  // The cost share of a link for one more edge, when `f` edges of the key use it already. The share falls when `f` grows.
95
110
  const share = (f: number): number => (f <= 0 ? 1 : flow ? (f + 1) ** alpha - f ** alpha : discount);
96
111
  const passes = Math.max(1, Math.floor(options.passes ?? 1));
112
+ const reuse = Math.max(0, options.reuse ?? 0);
113
+ const reuseExponent = Math.max(0, options.reuseExponent ?? 1);
114
+ const capacity = Math.max(0, options.capacity ?? 0);
115
+ const laneWidth = Math.max(0, options.laneWidth ?? 4);
97
116
  const keyOf = (e: Edge): string => {
98
117
  if (!options.rule) return "*";
99
118
  return options.rule(e) ?? `\u0000${e.id}`;
@@ -131,14 +150,34 @@ export function sharedRouter(options: SharedOptions): Router {
131
150
  const order = orderWays(all, options.order ?? "span", graph);
132
151
  // The largest number of edges on one link of each key. The cost share of that number is the floor of the search.
133
152
  const top = new Map<string, number>();
134
- const route = (w: Way): void => {
153
+ // The width of the channel of each link, for the option `capacity`. Each link gets its width at its first use.
154
+ const widths = new Map<number, number>();
155
+ const widthOf = (i: number, j: number): number => {
156
+ const k = link(i, j);
157
+ let wd = widths.get(k);
158
+ if (wd === undefined) widths.set(k, (wd = channelWidth(ways.obstacles, corners.points[i]!, corners.points[j]!)));
159
+ return wd;
160
+ };
161
+ /** This function routes one way. `before` holds the edges of the key on each link in the pass before, for the option `reuse`. */
162
+ const route = (w: Way, before?: ReadonlyMap<number, number>): void => {
135
163
  let used = discounted.get(w.key);
136
164
  if (!used) discounted.set(w.key, (used = new Map()));
137
165
  const mine = used;
138
- const floor = flow ? share(top.get(w.key) ?? 0) : discount;
139
- const cost = flow
166
+ let floor = flow ? share(top.get(w.key) ?? 0) : discount;
167
+ let cost = flow
140
168
  ? (i: number, j: number, d: number): number => d * share(mine.get(link(i, j))?.length ?? 0)
141
169
  : (i: number, j: number, d: number): number => (mine.has(link(i, j)) ? d * discount : d);
170
+ if (capacity > 0) {
171
+ const plain = cost;
172
+ cost = (i, j, d) => plain(i, j, d) + capacity * Math.max(0, ((mine.get(link(i, j))?.length ?? 0) + 1) * laneWidth - widthOf(i, j));
173
+ }
174
+ if (before !== undefined) {
175
+ const plain = cost;
176
+ let most = 0;
177
+ for (const u of before.values()) most = Math.max(most, u);
178
+ cost = (i, j, d) => plain(i, j, d) / (1 + reuse * (before.get(link(i, j)) ?? 0)) ** reuseExponent;
179
+ floor /= (1 + reuse * most) ** reuseExponent;
180
+ }
142
181
  const way = corners.path(ways.out(w.ps), ways.out(w.pt), cost, floor);
143
182
  if (!way) return;
144
183
  w.found = true;
@@ -157,6 +196,20 @@ export function sharedRouter(options: SharedOptions): Router {
157
196
  for (const w of order) route(w);
158
197
  // Each later pass takes each edge off its links and finds its way again, with the ways of the other edges fixed.
159
198
  for (let pass = 1; pass < passes; pass++) {
199
+ if (reuse > 0) {
200
+ // Winding Roads (HWY-005): each edge finds its way again from no ways, and the busy links of the pass before pull it.
201
+ const before = new Map<string, Map<number, number>>();
202
+ for (const [key, links] of discounted) before.set(key, new Map([...links].map(([k, list]) => [k, list.length])));
203
+ discounted.clear();
204
+ top.clear();
205
+ for (const w of order) {
206
+ w.found = false;
207
+ w.seq = [];
208
+ w.ids = [];
209
+ }
210
+ for (const w of order) route(w, before.get(w.key) ?? new Map());
211
+ continue;
212
+ }
160
213
  for (const w of order) {
161
214
  if (!w.found) continue;
162
215
  const mine = discounted.get(w.key)!;