@knowvah/dot-engine 2.6.12 → 2.7.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 (198) hide show
  1. package/README.md +1 -1
  2. package/dist/api.js +18 -4
  3. package/dist/api.js.map +3 -3
  4. package/dist/common/attr-get.d.ts +30 -0
  5. package/dist/common/attr-get.d.ts.map +1 -0
  6. package/dist/common/htmltable-emit-fill.d.ts +20 -2
  7. package/dist/common/htmltable-emit-fill.d.ts.map +1 -1
  8. package/dist/common/htmltable-emit.d.ts.map +1 -1
  9. package/dist/common/htmltable-lex-expat.d.ts.map +1 -1
  10. package/dist/common/htmltable-pos-runs.d.ts +7 -4
  11. package/dist/common/htmltable-pos-runs.d.ts.map +1 -1
  12. package/dist/common/htmltable.d.ts.map +1 -1
  13. package/dist/common/libm-trig.d.ts +5 -0
  14. package/dist/common/libm-trig.d.ts.map +1 -0
  15. package/dist/common/make-label.d.ts.map +1 -1
  16. package/dist/common/multicolor.d.ts +21 -13
  17. package/dist/common/multicolor.d.ts.map +1 -1
  18. package/dist/common/poly-gencode.d.ts +1 -2
  19. package/dist/common/poly-gencode.d.ts.map +1 -1
  20. package/dist/common/poly-init.d.ts +2 -12
  21. package/dist/common/poly-init.d.ts.map +1 -1
  22. package/dist/common/splines-clip.d.ts +7 -0
  23. package/dist/common/splines-clip.d.ts.map +1 -1
  24. package/dist/gvc/context.d.ts +6 -0
  25. package/dist/gvc/context.d.ts.map +1 -1
  26. package/dist/gvc/device-cluster.d.ts +15 -0
  27. package/dist/gvc/device-cluster.d.ts.map +1 -1
  28. package/dist/gvc/device.d.ts.map +1 -1
  29. package/dist/gvc/first-layer.d.ts +9 -0
  30. package/dist/gvc/first-layer.d.ts.map +1 -0
  31. package/dist/gvc/gvrender-set-style.d.ts +28 -0
  32. package/dist/gvc/gvrender-set-style.d.ts.map +1 -0
  33. package/dist/gvc/usershape.d.ts +18 -0
  34. package/dist/gvc/usershape.d.ts.map +1 -1
  35. package/dist/index.js +2478 -912
  36. package/dist/index.js.map +4 -4
  37. package/dist/layout/dot/compound.d.ts.map +1 -1
  38. package/dist/layout/dot/edge-route-clip.d.ts +14 -4
  39. package/dist/layout/dot/edge-route-clip.d.ts.map +1 -1
  40. package/dist/layout/dot/edge-route-routing.d.ts.map +1 -1
  41. package/dist/layout/dot/graph-label.d.ts.map +1 -1
  42. package/dist/layout/neato/__fixtures__/dijkstra-f.d.ts +11 -0
  43. package/dist/layout/neato/__fixtures__/dijkstra-f.d.ts.map +1 -0
  44. package/dist/layout/neato/__fixtures__/hier-imds.d.ts +36 -0
  45. package/dist/layout/neato/__fixtures__/hier-imds.d.ts.map +1 -0
  46. package/dist/layout/neato/__fixtures__/hier-majorization.d.ts +32 -0
  47. package/dist/layout/neato/__fixtures__/hier-majorization.d.ts.map +1 -0
  48. package/dist/layout/neato/cdt-surface.d.ts.map +1 -1
  49. package/dist/layout/neato/compute-hierarchy.d.ts +24 -0
  50. package/dist/layout/neato/compute-hierarchy.d.ts.map +1 -0
  51. package/dist/layout/neato/conjgrad-mkernel.d.ts +20 -0
  52. package/dist/layout/neato/conjgrad-mkernel.d.ts.map +1 -0
  53. package/dist/layout/neato/conjgrad.d.ts +19 -0
  54. package/dist/layout/neato/conjgrad.d.ts.map +1 -1
  55. package/dist/layout/neato/constrained-majorization.d.ts +26 -0
  56. package/dist/layout/neato/constrained-majorization.d.ts.map +1 -0
  57. package/dist/layout/neato/dijkstra.d.ts +33 -16
  58. package/dist/layout/neato/dijkstra.d.ts.map +1 -1
  59. package/dist/layout/neato/gts-triangle.d.ts +14 -0
  60. package/dist/layout/neato/gts-triangle.d.ts.map +1 -0
  61. package/dist/layout/neato/init.d.ts +2 -12
  62. package/dist/layout/neato/init.d.ts.map +1 -1
  63. package/dist/layout/neato/kkutils.d.ts +20 -0
  64. package/dist/layout/neato/kkutils.d.ts.map +1 -0
  65. package/dist/layout/neato/make-graph-data.d.ts +43 -0
  66. package/dist/layout/neato/make-graph-data.d.ts.map +1 -0
  67. package/dist/layout/neato/matrix-ops.d.ts +46 -0
  68. package/dist/layout/neato/matrix-ops.d.ts.map +1 -1
  69. package/dist/layout/neato/opt-arrangement.d.ts +20 -0
  70. package/dist/layout/neato/opt-arrangement.d.ts.map +1 -0
  71. package/dist/layout/neato/quad-prog-solve.d.ts +44 -0
  72. package/dist/layout/neato/quad-prog-solve.d.ts.map +1 -0
  73. package/dist/layout/neato/smart-ini-x.d.ts +9 -0
  74. package/dist/layout/neato/smart-ini-x.d.ts.map +1 -0
  75. package/dist/layout/neato/splines.d.ts +1 -1
  76. package/dist/layout/neato/splines.d.ts.map +1 -1
  77. package/dist/layout/neato/start.d.ts +4 -1
  78. package/dist/layout/neato/start.d.ts.map +1 -1
  79. package/dist/layout/neato/stress.d.ts +8 -0
  80. package/dist/layout/neato/stress.d.ts.map +1 -1
  81. package/dist/model/edge.d.ts +1 -1
  82. package/dist/model/edge.d.ts.map +1 -1
  83. package/dist/model/lazy-attrs.d.ts +10 -0
  84. package/dist/model/lazy-attrs.d.ts.map +1 -0
  85. package/dist/model/node.d.ts +1 -1
  86. package/dist/model/node.d.ts.map +1 -1
  87. package/dist/ortho/index.d.ts.map +1 -1
  88. package/dist/render/dot/edge-draw.d.ts +1 -1
  89. package/dist/render/dot/edge-draw.d.ts.map +1 -1
  90. package/dist/render/svg-multicolor.d.ts +2 -2
  91. package/dist/render/svg-multicolor.d.ts.map +1 -1
  92. package/dist/render.js +2358 -792
  93. package/dist/render.js.map +4 -4
  94. package/package.json +1 -1
  95. package/src/common/attr-get.ts +46 -0
  96. package/src/common/graph-init.ts +1 -1
  97. package/src/common/htmltable-emit-fill.ts +56 -11
  98. package/src/common/htmltable-emit.ts +4 -3
  99. package/src/common/htmltable-lex-expat.ts +21 -3
  100. package/src/common/htmltable-pos-runs.ts +11 -6
  101. package/src/common/htmltable.ts +28 -116
  102. package/src/common/libm-trig.ts +43 -0
  103. package/src/common/make-label.ts +14 -2
  104. package/src/common/multicolor.ts +88 -24
  105. package/src/common/poly-gencode.ts +16 -8
  106. package/src/common/poly-init.ts +23 -19
  107. package/src/common/postproc.ts +2 -2
  108. package/src/common/splines-clip.ts +15 -12
  109. package/src/common/splines-routespl.ts +2 -2
  110. package/src/common/textmeasure-lookup.ts +1 -1
  111. package/src/gvc/context.ts +8 -0
  112. package/src/gvc/device-cluster.ts +63 -8
  113. package/src/gvc/device.ts +16 -2
  114. package/src/gvc/first-layer.ts +26 -0
  115. package/src/gvc/gvrender-set-style.ts +114 -0
  116. package/src/gvc/usershape.ts +26 -0
  117. package/src/layout/dot/cluster.ts +1 -1
  118. package/src/layout/dot/compound.ts +3 -4
  119. package/src/layout/dot/conc.ts +1 -1
  120. package/src/layout/dot/edge-route-clip.ts +19 -7
  121. package/src/layout/dot/edge-route-routing.ts +11 -3
  122. package/src/layout/dot/fastgr.ts +2 -2
  123. package/src/layout/dot/graph-label.ts +1 -15
  124. package/src/layout/neato/__fixtures__/dijkstra-f.ts +17 -0
  125. package/src/layout/neato/__fixtures__/hier/c1554.gv +33 -0
  126. package/src/layout/neato/__fixtures__/hier/c1554.plain +26 -0
  127. package/src/layout/neato/__fixtures__/hier/c1554.xdot +161 -0
  128. package/src/layout/neato/__fixtures__/hier/cases/cycle.gv +1 -0
  129. package/src/layout/neato/__fixtures__/hier/cases/cycle.plain +10 -0
  130. package/src/layout/neato/__fixtures__/hier/cases/cycle.stderr +0 -0
  131. package/src/layout/neato/__fixtures__/hier/cases/dirnone.gv +1 -0
  132. package/src/layout/neato/__fixtures__/hier/cases/dirnone.plain +11 -0
  133. package/src/layout/neato/__fixtures__/hier/cases/dirnone.stderr +0 -0
  134. package/src/layout/neato/__fixtures__/hier/cases/epsilon.gv +1 -0
  135. package/src/layout/neato/__fixtures__/hier/cases/epsilon.plain +13 -0
  136. package/src/layout/neato/__fixtures__/hier/cases/epsilon.stderr +0 -0
  137. package/src/layout/neato/__fixtures__/hier/cases/levelsgap.gv +1 -0
  138. package/src/layout/neato/__fixtures__/hier/cases/levelsgap.plain +8 -0
  139. package/src/layout/neato/__fixtures__/hier/cases/levelsgap.stderr +0 -0
  140. package/src/layout/neato/__fixtures__/hier/cases/mds.gv +1 -0
  141. package/src/layout/neato/__fixtures__/hier/cases/mds.plain +10 -0
  142. package/src/layout/neato/__fixtures__/hier/cases/mds.stderr +0 -0
  143. package/src/layout/neato/__fixtures__/hier/cases/multiedge.gv +1 -0
  144. package/src/layout/neato/__fixtures__/hier/cases/multiedge.plain +10 -0
  145. package/src/layout/neato/__fixtures__/hier/cases/multiedge.stderr +0 -0
  146. package/src/layout/neato/__fixtures__/hier/cases/neglevelsgap.gv +1 -0
  147. package/src/layout/neato/__fixtures__/hier/cases/neglevelsgap.plain +8 -0
  148. package/src/layout/neato/__fixtures__/hier/cases/neglevelsgap.stderr +0 -0
  149. package/src/layout/neato/__fixtures__/hier/cases/startrandom.gv +1 -0
  150. package/src/layout/neato/__fixtures__/hier/cases/startrandom.plain +10 -0
  151. package/src/layout/neato/__fixtures__/hier/cases/startrandom.stderr +0 -0
  152. package/src/layout/neato/__fixtures__/hier/cases/startregular.gv +1 -0
  153. package/src/layout/neato/__fixtures__/hier/cases/startregular.plain +10 -0
  154. package/src/layout/neato/__fixtures__/hier/cases/startregular.stderr +0 -0
  155. package/src/layout/neato/__fixtures__/hier/cases/tdist.gv +1 -0
  156. package/src/layout/neato/__fixtures__/hier/cases/tdist.plain +13 -0
  157. package/src/layout/neato/__fixtures__/hier/cases/tdist.stderr +0 -0
  158. package/src/layout/neato/__fixtures__/hier/cases/undirected.gv +1 -0
  159. package/src/layout/neato/__fixtures__/hier/cases/undirected.plain +10 -0
  160. package/src/layout/neato/__fixtures__/hier/cases/undirected.stderr +0 -0
  161. package/src/layout/neato/__fixtures__/hier/hier-probe.gv +1 -0
  162. package/src/layout/neato/__fixtures__/hier/hier-probe.plain +16 -0
  163. package/src/layout/neato/__fixtures__/hier/hier-probe.xdot +62 -0
  164. package/src/layout/neato/__fixtures__/hier-imds.ts +151 -0
  165. package/src/layout/neato/__fixtures__/hier-majorization.ts +145 -0
  166. package/src/layout/neato/cdt-surface.ts +6 -16
  167. package/src/layout/neato/compute-hierarchy.ts +90 -0
  168. package/src/layout/neato/conjgrad-mkernel.ts +141 -0
  169. package/src/layout/neato/conjgrad.ts +208 -0
  170. package/src/layout/neato/constrained-majorization.ts +479 -0
  171. package/src/layout/neato/dijkstra.ts +218 -92
  172. package/src/layout/neato/edge-len.ts +2 -2
  173. package/src/layout/neato/fdp-adjust.ts +3 -3
  174. package/src/layout/neato/gts-triangle.ts +42 -0
  175. package/src/layout/neato/init.ts +57 -57
  176. package/src/layout/neato/kk.ts +2 -2
  177. package/src/layout/neato/kkutils.ts +96 -0
  178. package/src/layout/neato/make-graph-data.ts +166 -0
  179. package/src/layout/neato/matrix-ops.ts +138 -0
  180. package/src/layout/neato/opt-arrangement.ts +86 -0
  181. package/src/layout/neato/quad-prog-solve.ts +489 -0
  182. package/src/layout/neato/smart-ini-x.ts +503 -0
  183. package/src/layout/neato/splines.ts +4 -4
  184. package/src/layout/neato/start.ts +7 -6
  185. package/src/layout/neato/stress.ts +28 -0
  186. package/src/model/edge.ts +3 -2
  187. package/src/model/lazy-attrs.ts +36 -0
  188. package/src/model/node.ts +3 -2
  189. package/src/ortho/index.ts +2 -3
  190. package/src/parser/dot.js +1 -1
  191. package/src/parser/dot.pegjs +1 -1
  192. package/src/parser/index.ts +1 -1
  193. package/src/render/color-resolve.ts +2 -2
  194. package/src/render/dot/edge-draw.ts +6 -3
  195. package/src/render/dot.ts +1 -1
  196. package/src/render/svg-edge-split.ts +2 -2
  197. package/src/render/svg-multicolor.ts +4 -3
  198. package/src/render/svg-parallel-edge.ts +2 -2
@@ -0,0 +1,503 @@
1
+ // SPDX-License-Identifier: EPL-2.0
2
+
3
+ /**
4
+ * Port of neatogen/smart_ini_x.c: `IMDS_given_dim`, which finds the second
5
+ * coordinate of a hierarchical layout given the first. The inner products of
6
+ * this file are inlined by the 16.1 arm64 `-O3 -flto=thin` build at each call
7
+ * site, so every site carries its own unfused block width (see matrix-ops.ts);
8
+ * the constants below name them.
9
+ *
10
+ * @see lib/neatogen/smart_ini_x.c
11
+ */
12
+
13
+ import { crand } from '../../common/crand.js';
14
+ import { conjugateGradientF } from './conjgrad.js';
15
+ import type { VtxData } from './dijkstra.js';
16
+ import { computeApsp } from './kkutils.js';
17
+ import {
18
+ BLOCK_NONE,
19
+ copyVector,
20
+ fma,
21
+ innerProduct,
22
+ norm,
23
+ scadd,
24
+ vectorsScalarMult,
25
+ } from './matrix-ops.js';
26
+
27
+ const fr = Math.fround;
28
+
29
+ /** `#define SCALE_FACTOR` @see lib/neatogen/smart_ini_x.c */
30
+ const SCALE_FACTOR = 256;
31
+
32
+ /** `DBL_EPSILON` @see float.h */
33
+ const DBL_EPSILON = 2.220446049250313e-16;
34
+
35
+ /** The `len < 1e-10` colinearity / null-space test of power iteration. */
36
+ const POWER_LENGTH_FLOOR = 1e-10;
37
+
38
+ /**
39
+ * Unfused block widths per inlined call site of the 16.1 arm64 `-O3 -flto=thin`
40
+ * build (`BLOCK_NONE` = plain scalar loop). Calibrated bit for bit against the
41
+ * oracle's own libneatogen.a on random DAGs (n = 5..40, 192 graphs): each width
42
+ * was the only one of {1, 4, 8, 16} matching the native intermediate values.
43
+ * Sites inside the eigenvector loops over earlier eigenvectors (j < i) are
44
+ * never reached: CMDS_orthog asks for one eigenvector.
45
+ */
46
+
47
+ /** standardize: the norm of the centred vector. */
48
+ const BLOCK_STANDARDIZE_NORM = 4;
49
+ /** mat_mult_vec_orthog: each row of mat * vec. */
50
+ const BLOCK_MAT_MULT_ROW = 4;
51
+ /** mat_mult_vec_orthog: result . orthog. */
52
+ const BLOCK_MAT_MULT_ORTHOG = 8;
53
+ /** power iteration: orthog . guess. */
54
+ const BLOCK_GUESS_ORTHOG = 8;
55
+ /** power iteration: norm of the orthogonalized guess (scalar loop). */
56
+ const BLOCK_GUESS_LEN = BLOCK_NONE;
57
+ /** power iteration: norm inside the iteration (scalar loop). */
58
+ const BLOCK_LOOP_LEN = BLOCK_NONE;
59
+ /** power iteration: curr . last. */
60
+ const BLOCK_ANGLE = 8;
61
+ /**
62
+ * IMDS_given_dim: the sum1/sum2 loops. The disassembly shows a 16-wide body
63
+ * and a 4-wide epilogue; on the harness data 4 and a scalar loop agree.
64
+ */
65
+ const BLOCK_UNIFORM_SUMS = 4;
66
+ /** compute_avgs: sums of squared int distances (exact below 2^26, so unobservable). */
67
+ const BLOCK_AVGS = 4;
68
+
69
+ /** Dense float matrix (`float **`), one `Float32Array` per row. */
70
+ type FloatMatrix = Float32Array[];
71
+
72
+ /**
73
+ * Centre `orthog` and scale it to unit length (left alone when degenerate).
74
+ * @see lib/neatogen/smart_ini_x.c:standardize
75
+ */
76
+ function standardize(orthog: Float64Array, nvtxs: number): void {
77
+ let avg = 0;
78
+ for (let i = 0; i < nvtxs; i++) avg += orthog[i]!;
79
+ avg /= nvtxs;
80
+
81
+ /* centralize: */
82
+ for (let i = 0; i < nvtxs; i++) orthog[i] = orthog[i]! - avg;
83
+
84
+ /* normalize: */
85
+ const len = norm(orthog, nvtxs - 1, BLOCK_STANDARDIZE_NORM);
86
+
87
+ // if we have a degenerate length, do not attempt to scale by it
88
+ if (Math.abs(len) < DBL_EPSILON) return;
89
+
90
+ vectorsScalarMult(nvtxs, orthog, 1.0 / len, orthog);
91
+ }
92
+
93
+ /**
94
+ * result = (I - orthog orthog^T) mat vec, `mat` being dim1 x dim2.
95
+ * @see lib/neatogen/smart_ini_x.c:mat_mult_vec_orthog
96
+ */
97
+ function matMultVecOrthog(
98
+ mat: FloatMatrix,
99
+ dim: number,
100
+ vec: Float64Array,
101
+ result: Float64Array,
102
+ orthog: Float64Array,
103
+ ): void {
104
+ /* computes mat*vec, where mat is a dim*dim matrix */
105
+ const blocked = dim - (dim % BLOCK_MAT_MULT_ROW);
106
+ for (let i = 0; i < dim; i++) {
107
+ const row = mat[i]!;
108
+ let sum = 0;
109
+ let j = 0;
110
+ for (; j < blocked; j++) sum += row[j]! * vec[j]!;
111
+ for (; j < dim; j++) sum = fma(row[j]!, vec[j]!, sum);
112
+ result[i] = sum;
113
+ }
114
+ const alpha = -innerProduct(dim, result, orthog, BLOCK_MAT_MULT_ORTHOG);
115
+ scadd(result, dim - 1, alpha, orthog);
116
+ }
117
+
118
+ /** Inputs of power iteration shared by its phases. */
119
+ interface PowerIteration {
120
+ mat: FloatMatrix;
121
+ n: number;
122
+ eigs: Float64Array[];
123
+ evals: Float64Array;
124
+ orthog: Float64Array;
125
+ }
126
+
127
+ /**
128
+ * Orthogonalize `vec` against the first `count` eigenvectors.
129
+ * @see lib/neatogen/smart_ini_x.c:power_iteration_orthog
130
+ */
131
+ function orthogonalizeAgainstEigs(
132
+ p: PowerIteration,
133
+ vec: Float64Array,
134
+ count: number,
135
+ block: number,
136
+ ): void {
137
+ for (let j = 0; j < count; j++) {
138
+ const alpha = -innerProduct(p.n, p.eigs[j]!, vec, block);
139
+ scadd(vec, p.n - 1, alpha, p.eigs[j]!);
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Guess the i-th eigenvector (`choose:` in C): random, orthogonal to `orthog`
145
+ * and the earlier eigenvectors, normalized; retried while nearly colinear.
146
+ * @see lib/neatogen/smart_ini_x.c:power_iteration_orthog
147
+ */
148
+ function chooseStartVector(p: PowerIteration, i: number): void {
149
+ const curr = p.eigs[i]!;
150
+ for (;;) {
151
+ for (let j = 0; j < p.n; j++) curr[j] = crand() % 100;
152
+ const alpha = -innerProduct(p.n, p.orthog, curr, BLOCK_GUESS_ORTHOG);
153
+ scadd(curr, p.n - 1, alpha, p.orthog);
154
+ // orthogonalize against higher eigenvectors
155
+ orthogonalizeAgainstEigs(p, curr, i, BLOCK_NONE);
156
+ const len = norm(curr, p.n - 1, BLOCK_GUESS_LEN);
157
+ if (len < POWER_LENGTH_FLOOR) continue; // colinear with previous ones: goto choose
158
+ vectorsScalarMult(p.n, curr, 1.0 / len, curr);
159
+ return;
160
+ }
161
+ }
162
+
163
+ /** Result of one eigenvector's power iteration. */
164
+ interface IterationResult {
165
+ /** true when the null space was reached (C `goto exit`). */
166
+ nullSpace: boolean;
167
+ eval: number;
168
+ }
169
+
170
+ /**
171
+ * Iterate `curr` to the dominant eigenvector (`do { } while` in C).
172
+ * @see lib/neatogen/smart_ini_x.c:power_iteration_orthog
173
+ */
174
+ function iterateEigenvector(
175
+ p: PowerIteration,
176
+ i: number,
177
+ tol: number,
178
+ work: { tmp: Float64Array; last: Float64Array },
179
+ ): IterationResult {
180
+ const curr = p.eigs[i]!;
181
+ for (;;) {
182
+ copyVector(p.n, curr, work.last);
183
+ matMultVecOrthog(p.mat, p.n, curr, work.tmp, p.orthog);
184
+ copyVector(p.n, work.tmp, curr);
185
+ /* orthogonalize against higher eigenvectors */
186
+ orthogonalizeAgainstEigs(p, curr, i, BLOCK_NONE);
187
+ const len = norm(curr, p.n - 1, BLOCK_LOOP_LEN);
188
+ if (len < POWER_LENGTH_FLOOR) {
189
+ /* We have reached the null space (e.vec. associated with e.val. 0) */
190
+ return { nullSpace: true, eval: 0 };
191
+ }
192
+ vectorsScalarMult(p.n, curr, 1.0 / len, curr);
193
+ const angle = innerProduct(p.n, curr, work.last, BLOCK_ANGLE);
194
+ if (!(Math.abs(angle) < tol)) {
195
+ /* the Rayleigh quotient (up to errors due to orthogonalization) */
196
+ return { nullSpace: false, eval: angle * len };
197
+ }
198
+ }
199
+ }
200
+
201
+ /**
202
+ * Remaining eigenvectors, probably of eigenvalue 0, for which power
203
+ * iteration is dangerous: random, orthogonalized, normalized, eval 0.
204
+ * @see lib/neatogen/smart_ini_x.c:power_iteration_orthog
205
+ */
206
+ function fillNullSpaceEigenvectors(p: PowerIteration, from: number, neigs: number): void {
207
+ for (let i = from; i < neigs; i++) {
208
+ const curr = p.eigs[i]!;
209
+ /* guess the i-th eigen vector */
210
+ for (let j = 0; j < p.n; j++) curr[j] = crand() % 100;
211
+ /* orthogonalize against higher eigenvectors */
212
+ orthogonalizeAgainstEigs(p, curr, i, BLOCK_NONE);
213
+ const len = norm(curr, p.n - 1, BLOCK_NONE);
214
+ vectorsScalarMult(p.n, curr, 1.0 / len, curr);
215
+ p.evals[i] = 0;
216
+ }
217
+ }
218
+
219
+ /**
220
+ * Sort the vectors by decreasing eigenvalue, against possible mis-convergence.
221
+ * @see lib/neatogen/smart_ini_x.c:power_iteration_orthog
222
+ */
223
+ function sortEigenvectors(p: PowerIteration, neigs: number, tmp: Float64Array): void {
224
+ for (let i = 0; i < neigs - 1; i++) {
225
+ let largestIndex = i;
226
+ let largestEval = p.evals[largestIndex]!;
227
+ for (let j = i + 1; j < neigs; j++) {
228
+ if (largestEval < p.evals[j]!) {
229
+ largestIndex = j;
230
+ largestEval = p.evals[largestIndex]!;
231
+ }
232
+ }
233
+ if (largestIndex !== i) { // exchange eigenvectors:
234
+ copyVector(p.n, p.eigs[i]!, tmp);
235
+ copyVector(p.n, p.eigs[largestIndex]!, p.eigs[i]!);
236
+ copyVector(p.n, tmp, p.eigs[largestIndex]!);
237
+
238
+ p.evals[largestIndex] = p.evals[i]!;
239
+ p.evals[i] = largestEval;
240
+ }
241
+ }
242
+ }
243
+
244
+ /**
245
+ * Power iteration with (I - orthog orthog^T) mat (I - orthog orthog^T).
246
+ * @see lib/neatogen/smart_ini_x.c:power_iteration_orthog
247
+ */
248
+ function powerIterationOrthog(
249
+ p: PowerIteration,
250
+ neigsIn: number,
251
+ pIterationThreshold: number,
252
+ ): void {
253
+ const work = { tmp: new Float64Array(p.n), last: new Float64Array(p.n) };
254
+ const tol = 1 - pIterationThreshold;
255
+ const neigs = neigsIn >= p.n ? p.n : neigsIn;
256
+
257
+ let i = 0;
258
+ for (; i < neigs; i++) {
259
+ chooseStartVector(p, i);
260
+ const res = iterateEigenvector(p, i, tol, work);
261
+ if (res.nullSpace) break; // goto exit (i is not advanced)
262
+ p.evals[i] = res.eval;
263
+ }
264
+ fillNullSpaceEigenvectors(p, i, neigs);
265
+ sortEigenvectors(p, neigs, work.tmp);
266
+ }
267
+
268
+ /**
269
+ * Row and overall averages of the squared distances, as floats.
270
+ * @see lib/neatogen/smart_ini_x.c:compute_avgs
271
+ */
272
+ function computeAvgs(Dij: Int32Array[], n: number): { rowAvg: Float32Array; allAvg: number } {
273
+ const rowAvg = new Float32Array(n);
274
+ let sum = 0;
275
+ const blocked = n - (n % BLOCK_AVGS);
276
+
277
+ for (let i = 0; i < n; i++) {
278
+ let sumRow = 0;
279
+ const row = Dij[i]!;
280
+ let j = 0;
281
+ for (; j < blocked; j++) {
282
+ sum += row[j]! * row[j]!;
283
+ sumRow += row[j]! * row[j]!;
284
+ }
285
+ for (; j < n; j++) {
286
+ sum = fma(row[j]!, row[j]!, sum);
287
+ sumRow = fma(row[j]!, row[j]!, sumRow);
288
+ }
289
+ rowAvg[i] = fr(fr(sumRow) / n);
290
+ }
291
+ return { rowAvg, allAvg: fr(fr(sum) / (n * n)) };
292
+ }
293
+
294
+ /**
295
+ * The double-centred squared-distance matrix, as floats.
296
+ * @see lib/neatogen/smart_ini_x.c:compute_Bij
297
+ */
298
+ function computeBij(Dij: Int32Array[], n: number): FloatMatrix {
299
+ const Bij: FloatMatrix = [];
300
+ for (let i = 0; i < n; i++) Bij.push(new Float32Array(n));
301
+
302
+ const { rowAvg, allAvg } = computeAvgs(Dij, n);
303
+ for (let i = 0; i < n; i++) {
304
+ for (let j = 0; j <= i; j++) {
305
+ // -(float)D*D + row_avg[i] contracts to fmsub (float), then two float ops
306
+ const fD = fr(Dij[i]![j]!);
307
+ const t = fr(fma(-fD, fD, rowAvg[i]!));
308
+ Bij[i]![j] = fr(fr(t + rowAvg[j]!) - allAvg);
309
+ Bij[j]![i] = Bij[i]![j]!;
310
+ }
311
+ }
312
+ return Bij;
313
+ }
314
+
315
+ /**
316
+ * Classical MDS in one dimension, orthogonal to `orthog`.
317
+ * @see lib/neatogen/smart_ini_x.c:CMDS_orthog
318
+ */
319
+ function cmdsOrthog(
320
+ n: number,
321
+ dim: number,
322
+ eigs: Float64Array[],
323
+ tol: number,
324
+ input: { orthog: Float64Array; Dij: Int32Array[] },
325
+ ): void {
326
+ const { orthog, Dij } = input;
327
+ const Bij = computeBij(Dij, n);
328
+ const evals = new Float64Array(dim);
329
+
330
+ const orthogAux = Float64Array.from(orthog.subarray(0, n));
331
+ standardize(orthogAux, n);
332
+ powerIterationOrthog({ mat: Bij, n, eigs, evals, orthog: orthogAux }, dim, tol);
333
+
334
+ for (let i = 0; i < dim; i++) {
335
+ for (let j = 0; j < n; j++) eigs[i]![j] = eigs[i]![j]! * Math.sqrt(Math.abs(evals[i]!));
336
+ }
337
+ }
338
+
339
+ /**
340
+ * Scale `x` so that distances along it best fit the ideal ones; returns the
341
+ * scale (`uniLength`, 0 when the sums overflow).
342
+ * @see lib/neatogen/smart_ini_x.c:IMDS_given_dim
343
+ */
344
+ function scaleGivenCoords(Dij: Int32Array[], n: number, x: Float64Array): number {
345
+ let sum1 = 0;
346
+ let sum2 = 0;
347
+ for (let i = 1; i < n; i++) {
348
+ const blocked = i - (i % BLOCK_UNIFORM_SUMS);
349
+ for (let j = 0; j < i; j++) {
350
+ const d = Dij[i]![j]!;
351
+ const f = Math.abs(x[i]! - x[j]!);
352
+ const t1 = 1.0 / d;
353
+ const t2 = (1.0 / Math.imul(d, d)) * f;
354
+ if (j < blocked) {
355
+ sum1 += t1 * f;
356
+ sum2 += t2 * f;
357
+ } else {
358
+ sum1 = fma(t1, f, sum1);
359
+ sum2 = fma(t2, f, sum2);
360
+ }
361
+ }
362
+ }
363
+ const uniLength = Math.abs(sum2) === Infinity ? 0 : sum1 / sum2;
364
+ for (let i = 0; i < n; i++) x[i] = x[i]! * uniLength;
365
+ return uniLength;
366
+ }
367
+
368
+ /**
369
+ * The Laplacian of the weights w_ij = 1 / D_ij^2, as floats.
370
+ * @see lib/neatogen/smart_ini_x.c:IMDS_given_dim
371
+ */
372
+ function computeLaplacian(Dij: Int32Array[], n: number): FloatMatrix {
373
+ const lap: FloatMatrix = [];
374
+ for (let i = 0; i < n; i++) {
375
+ const row = new Float32Array(n);
376
+ let degree = fr(0);
377
+ for (let j = 0; j < n; j++) {
378
+ if (j === i) continue;
379
+ const w = fr(fr(-1.0) / fr(fr(Dij[i]![j]!) * fr(Dij[i]![j]!))); // w_{ij}
380
+ row[j] = w;
381
+ degree = fr(degree - w);
382
+ }
383
+ row[i] = degree;
384
+ lap.push(row);
385
+ }
386
+ return lap;
387
+ }
388
+
389
+ /**
390
+ * Replace the distances by their residuals after removing the x component.
391
+ * @see lib/neatogen/smart_ini_x.c:IMDS_given_dim
392
+ */
393
+ function computeResidualDistances(Dij: Int32Array[], n: number, x: Float64Array): void {
394
+ for (let i = 1; i < n; i++) {
395
+ const posI = x[i]!;
396
+ for (let j = 0; j < i; j++) {
397
+ const t = posI - x[j]!;
398
+ const d = Dij[i]![j]!;
399
+ const diff = fma(d, d, -(t * t));
400
+ const v = diff > 0 ? Math.sqrt(diff) : 0;
401
+ Dij[i]![j] = v;
402
+ Dij[j]![i] = v;
403
+ }
404
+ }
405
+ }
406
+
407
+ /**
408
+ * One entry of the balance vector: sum_j sign * delta_ij * w_ij, the sign
409
+ * following the order of y_i and y_j.
410
+ * @see lib/neatogen/smart_ini_x.c:IMDS_given_dim
411
+ */
412
+ function balanceOf(i: number, n: number, y: Float64Array, ctx: BalanceCtx): number {
413
+ const posI = y[i]!;
414
+ let b = 0;
415
+ for (let j = 0; j < n; j++) {
416
+ if (j === i) continue;
417
+ const term = fr(fr(ctx.Dij[i]![j]!) * -ctx.lap[i]![j]!); // w_{ij}*delta_{ij}
418
+ if (posI >= y[j]!) b += term;
419
+ else b -= term;
420
+ }
421
+ return b;
422
+ }
423
+
424
+ /** Distances and Laplacian read by {@link balanceOf}. */
425
+ interface BalanceCtx {
426
+ Dij: Int32Array[];
427
+ lap: FloatMatrix;
428
+ }
429
+
430
+ /**
431
+ * Alternate conjugate gradient and balance updates until the balance is stable.
432
+ * Returns 1 when conjugate gradient fails (C tests `< 0`, which it never is).
433
+ * @see lib/neatogen/smart_ini_x.c:IMDS_given_dim
434
+ */
435
+ function solveBalanced(
436
+ ctx: BalanceCtx,
437
+ n: number,
438
+ y: Float64Array,
439
+ balance: Float64Array,
440
+ convTol: number,
441
+ ): 0 | 1 {
442
+ let converged = false;
443
+ for (let iterations2 = 0; iterations2 < 200 && !converged; iterations2++) {
444
+ if (conjugateGradientF(ctx.lap, y, balance, n, { tol: convTol, maxIterations: n, ortho1: true }) < 0) return 1;
445
+ converged = true;
446
+ for (let i = 0; i < n; i++) {
447
+ const b = balanceOf(i, n, y, ctx);
448
+ if (b !== balance[i] && Math.abs(1 - b / balance[i]!) > 1e-5) {
449
+ converged = false;
450
+ balance[i] = b;
451
+ }
452
+ }
453
+ }
454
+ return 0;
455
+ }
456
+
457
+ /**
458
+ * Compute `newCoords` (y) given `givenCoords` (x) for a hierarchical layout;
459
+ * `givenCoords` is rescaled in place and restored at the end. Returns 1 when
460
+ * conjugate gradient fails.
461
+ * @see lib/neatogen/smart_ini_x.c:IMDS_given_dim
462
+ */
463
+ export function imdsGivenDim(
464
+ graph: VtxData[],
465
+ n: number,
466
+ givenCoords: Float64Array,
467
+ newCoords: Float64Array,
468
+ conjTol: number,
469
+ ): 0 | 1 {
470
+ const x = givenCoords;
471
+ const y = newCoords;
472
+ const Dij = computeApsp(graph, n);
473
+
474
+ /* scaling up the distances to enable an 'sqrt' operation later
475
+ * (in case distances are integers) */
476
+ for (let i = 0; i < n; i++) {
477
+ for (let j = 0; j < n; j++) Dij[i]![j] = Dij[i]![j]! * SCALE_FACTOR;
478
+ }
479
+
480
+ const uniLength = scaleGivenCoords(Dij, n, x);
481
+
482
+ /* smart ini: */
483
+ cmdsOrthog(n, 1, [y], conjTol, { orthog: x, Dij });
484
+
485
+ /* Compute Laplacian: */
486
+ const lap = computeLaplacian(Dij, n);
487
+
488
+ /* compute residual distances */
489
+ computeResidualDistances(Dij, n, x);
490
+
491
+ /* Compute the balance vector: */
492
+ const ctx: BalanceCtx = { Dij, lap };
493
+ const balance = new Float64Array(n);
494
+ for (let i = 0; i < n; i++) balance[i] = balanceOf(i, n, y, ctx);
495
+
496
+ if (solveBalanced(ctx, n, y, balance, conjTol) === 1) return 1;
497
+
498
+ for (let i = 0; !(Math.abs(uniLength) < DBL_EPSILON) && i < n; i++) {
499
+ x[i] = x[i]! / uniLength;
500
+ y[i] = y[i]! / uniLength;
501
+ }
502
+ return 0;
503
+ }
@@ -21,8 +21,8 @@ import type { SplineInfo, PolygonT, TextlabelT } from '../../common/types.js';
21
21
  import { polygonOutlineRing } from '../../common/poly-sizing.js';
22
22
  import { fma, fms } from '../../common/fma.js';
23
23
  import { ShapeKind } from '../../common/types.js';
24
- import type { ExpandT } from './sep-factor.js';
25
- import { esepFactor } from './sep-factor.js';
24
+ import { esepFactor, type ExpandT } from './sep-factor.js';
25
+ import { libmCos, libmSin } from '../../common/libm-trig.js';
26
26
  import type { Poly, VConfig } from '../../pathplan/types.js';
27
27
  import {
28
28
  obsOpen, obsClose, obsPath,
@@ -138,8 +138,8 @@ function ellipseTangentSlope(a: number, b: number, p: Point): number {
138
138
  function circumscribedCorner(a: number, b: number, i: number, nsides: number): Point {
139
139
  const angle0 = (2 * Math.PI * (i - 0.5)) / nsides;
140
140
  const angle1 = (2 * Math.PI * (i + 0.5)) / nsides;
141
- const p0 = { x: a * Math.cos(angle0), y: b * Math.sin(angle0) };
142
- const p1 = { x: a * Math.cos(angle1), y: b * Math.sin(angle1) };
141
+ const p0 = { x: a * libmCos(angle0), y: b * libmSin(angle0) }; // host libm
142
+ const p1 = { x: a * libmCos(angle1), y: b * libmSin(angle1) };
143
143
  const m0 = ellipseTangentSlope(a, b, p0);
144
144
  const m1 = ellipseTangentSlope(a, b, p1);
145
145
  // line_intersection, contracted as the binary: fnmsub, fmls, +; y = fmadd.
@@ -2,7 +2,10 @@
2
2
  /**
3
3
  * Neato `start` handling (setSeed / initRegular / checkStart) and the
4
4
  * fail-loud checks for the neato init features this port does not have
5
- * (smart init, the subset/circuit distance models, hier/ipsep modes).
5
+ * (smart init for the stress path, the subset/circuit distance models, ipsep
6
+ * with constraints). mode=hier runs `stress_majorization_with_hierarchy`; its
7
+ * own unported branches (subset/circuit models, the smart-init fallback
8
+ * without a hierarchy) are loud in constrained-majorization.ts.
6
9
  *
7
10
  * Each loud check sits where neato's C branches into the unported code and
8
11
  * fires only for the modes whose C path reaches it (ADR-3).
@@ -20,7 +23,7 @@ import {
20
23
  MODE_KK, MODE_MAJOR, MODE_HIER, MODE_IPSEP, MODE_SGD,
21
24
  MODEL_CIRCUIT, MODEL_SUBSET,
22
25
  } from './init.js';
23
- import { agerrLineOn } from '../../common/agerr.js';
26
+ import { agerrLineOn, agwarningf } from '../../common/agerr.js';
24
27
 
25
28
  /** @see lib/neatogen/neato.h:INIT_SELF */
26
29
  export const INIT_SELF = 0;
@@ -144,14 +147,12 @@ export function neatoMode(g: Graph): number {
144
147
  if (str === undefined || str === '') return MODE_MAJOR;
145
148
  const mode = modeByName().get(str);
146
149
  if (mode !== undefined) return mode;
147
- agerrLineOn('warn', `Illegal value ${str} for attribute "mode" in graph ${g.root.name} - ignored`);
150
+ agwarningf(`Illegal value ${str} for attribute "mode" in graph ${g.root.name} - ignored`);
148
151
  return MODE_MAJOR;
149
152
  }
150
153
 
151
154
  function checkMode(g: Graph, mode: number): void {
152
- const value = g.root.attrs.get('mode') || (mode === MODE_HIER ? 'hier' : 'ipsep');
153
- // @see lib/neatogen/neatoinit.c:1117 (stress_majorization_with_hierarchy)
154
- if (mode === MODE_HIER) unsupported('mode', value, 'hierarchical stress majorization');
155
+ const value = g.root.attrs.get('mode') || 'ipsep';
155
156
  // @see lib/neatogen/neatoinit.c:1121-1170 (stress_majorization_cola)
156
157
  if (mode === MODE_IPSEP && ipsepHasConstraints(g)) {
157
158
  unsupported('mode', value, 'constrained (ipsep) majorization');
@@ -296,6 +296,34 @@ export class DijHelper {
296
296
  }
297
297
  }
298
298
 
299
+ // ---------------------------------------------------------------------------
300
+ // mdsModel
301
+ // ---------------------------------------------------------------------------
302
+
303
+ /**
304
+ * Packed shortest paths with the user-supplied edge lengths written over the
305
+ * entries of adjacent nodes; `null` (C `NULL`) when the graph carries no
306
+ * lengths (`graph->ewgts == NULL`, an empty `ewgts` here). C's `delta`
307
+ * accumulator is only printed under `Verbose` and is not ported.
308
+ * @see lib/neatogen/stress.c:mdsModel
309
+ */
310
+ export function mdsModel(graph: VtxData[], nG: number): Float32Array | null {
311
+ if (graph[0]!.ewgts.length === 0) return null;
312
+ // first, compute shortest paths to fill in non-edges
313
+ const Dij = computeWeightedApspPacked(graph, nG);
314
+ // then, replace edge entries with user-supplied len
315
+ let shift = 0;
316
+ for (let i = 0; i < nG; i++) {
317
+ shift += i;
318
+ for (let e = 1; e < graph[i]!.nedges; e++) {
319
+ const j = graph[i]!.edges[e]!;
320
+ if (j < i) continue;
321
+ Dij[i * nG + j - shift] = graph[i]!.ewgts[e]!;
322
+ }
323
+ }
324
+ return Dij;
325
+ }
326
+
299
327
  // ---------------------------------------------------------------------------
300
328
  // CoordHelper — coordinate conversion and initialization
301
329
  // ---------------------------------------------------------------------------
package/src/model/edge.ts CHANGED
@@ -8,6 +8,7 @@
8
8
  */
9
9
 
10
10
  import type { Node } from './node.js';
11
+ import { EMPTY_ATTRS } from './lazy-attrs.js';
11
12
  import { type EdgeInfo, makeEdgeInfo, makePort } from './edgeInfo.js';
12
13
 
13
14
  /**
@@ -93,12 +94,12 @@ export class Edge {
93
94
  info: EdgeInfo;
94
95
 
95
96
  /** @see lib/cgraph/edge.c:agedge */
96
- constructor(tail: Node, head: Node, name: string) {
97
+ constructor(tail: Node, head: Node, name: string, lazyAttrs = false) {
97
98
  this.seq = Edge._nextSeq++;
98
99
  this.tail = tail;
99
100
  this.head = head;
100
101
  this.name = name;
101
- this.attrs = new Map();
102
+ this.attrs = lazyAttrs ? EMPTY_ATTRS : new Map();
102
103
  this.info = makeEdgeInfo(makePort(), makePort());
103
104
  }
104
105
  }
@@ -0,0 +1,36 @@
1
+ // SPDX-License-Identifier: EPL-2.0
2
+
3
+ /**
4
+ * Shared empty attribute map for dot virtual nodes/edges (AD-12).
5
+ *
6
+ * C virtual nodes/edges carry no attribute record; allocating an empty `Map`
7
+ * per object cost ~372 B per virtual pair. Virtual objects start with this
8
+ * single shared map instead; reads see an empty map, and the first write goes
9
+ * through {@link ownAttrs}, which materialises a private `Map`. A direct
10
+ * mutation of the shared map throws rather than leaking into every virtual.
11
+ * @see lib/dotgen/fastgr.c:virtual_node
12
+ */
13
+
14
+ class SharedEmptyAttrs extends Map<string, string> {
15
+ override set(key: string, _value: string): never {
16
+ throw new Error(`attrs.set("${key}") on a shared empty map; use ownAttrs()`);
17
+ }
18
+ override delete(key: string): never {
19
+ throw new Error(`attrs.delete("${key}") on a shared empty map; use ownAttrs()`);
20
+ }
21
+ override clear(): never {
22
+ throw new Error('attrs.clear() on a shared empty map; use ownAttrs()');
23
+ }
24
+ }
25
+
26
+ /** The one shared empty attribute map (read-only by construction). */
27
+ export const EMPTY_ATTRS: Map<string, string> = new SharedEmptyAttrs();
28
+
29
+ /**
30
+ * Return the object's private attribute map, materialising it on first write
31
+ * when the object still holds {@link EMPTY_ATTRS}.
32
+ */
33
+ export function ownAttrs(o: { attrs: Map<string, string> }): Map<string, string> {
34
+ if (o.attrs === EMPTY_ATTRS) o.attrs = new Map();
35
+ return o.attrs;
36
+ }
package/src/model/node.ts CHANGED
@@ -8,6 +8,7 @@
8
8
 
9
9
  import type { Edge } from './edge.js';
10
10
  import type { Graph } from './graph.js';
11
+ import { EMPTY_ATTRS } from './lazy-attrs.js';
11
12
  import { type NodeInfo, makeNodeInfo } from './nodeInfo.js';
12
13
 
13
14
  /**
@@ -64,10 +65,10 @@ export class Node {
64
65
  nodeDefaultsSnapshot?: Map<string, string>;
65
66
 
66
67
  /** @see lib/cgraph/node.c:agnode */
67
- constructor(id: number, name: string, root: Graph) {
68
+ constructor(id: number, name: string, root: Graph, lazyAttrs = false) {
68
69
  this.id = id;
69
70
  this.name = name;
70
- this.attrs = new Map();
71
+ this.attrs = lazyAttrs ? EMPTY_ATTRS : new Map();
71
72
  this.info = makeNodeInfo();
72
73
  this.root = root;
73
74
  }
@@ -20,7 +20,7 @@ import {
20
20
  } from "./sgraph.js";
21
21
  import { convertSPtoRoute, assignSegs, assignTracks, vtrack, htrack } from "./ortho-route.js";
22
22
  import { gvQsort } from "../util/bsd-qsort.js";
23
- import { agerrLineOn } from '../common/agerr.js';
23
+ import { agwarningf } from '../common/agerr.js';
24
24
 
25
25
  export type { OrthoGraph, OrthoEdge, OrthoNode, OrthoPoint, ClipAndInstallFn };
26
26
  export { SEED } from "./partition.js";
@@ -145,8 +145,7 @@ export function orthoEdges(
145
145
  clipAndInstall?: ClipAndInstallFn,
146
146
  ): number {
147
147
  if (useLbls) {
148
- agerrLineOn(
149
- 'warn',
148
+ agwarningf(
150
149
  "Orthogonal edges do not currently handle edge labels. Try using xlabels.\n",
151
150
  );
152
151
  useLbls = false; // eslint-disable-line no-param-reassign