@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.
- package/README.md +1 -1
- package/dist/api.js +18 -4
- package/dist/api.js.map +3 -3
- package/dist/common/attr-get.d.ts +30 -0
- package/dist/common/attr-get.d.ts.map +1 -0
- package/dist/common/htmltable-emit-fill.d.ts +20 -2
- package/dist/common/htmltable-emit-fill.d.ts.map +1 -1
- package/dist/common/htmltable-emit.d.ts.map +1 -1
- package/dist/common/htmltable-lex-expat.d.ts.map +1 -1
- package/dist/common/htmltable-pos-runs.d.ts +7 -4
- package/dist/common/htmltable-pos-runs.d.ts.map +1 -1
- package/dist/common/htmltable.d.ts.map +1 -1
- package/dist/common/libm-trig.d.ts +5 -0
- package/dist/common/libm-trig.d.ts.map +1 -0
- package/dist/common/make-label.d.ts.map +1 -1
- package/dist/common/multicolor.d.ts +21 -13
- package/dist/common/multicolor.d.ts.map +1 -1
- package/dist/common/poly-gencode.d.ts +1 -2
- package/dist/common/poly-gencode.d.ts.map +1 -1
- package/dist/common/poly-init.d.ts +2 -12
- package/dist/common/poly-init.d.ts.map +1 -1
- package/dist/common/splines-clip.d.ts +7 -0
- package/dist/common/splines-clip.d.ts.map +1 -1
- package/dist/gvc/context.d.ts +6 -0
- package/dist/gvc/context.d.ts.map +1 -1
- package/dist/gvc/device-cluster.d.ts +15 -0
- package/dist/gvc/device-cluster.d.ts.map +1 -1
- package/dist/gvc/device.d.ts.map +1 -1
- package/dist/gvc/first-layer.d.ts +9 -0
- package/dist/gvc/first-layer.d.ts.map +1 -0
- package/dist/gvc/gvrender-set-style.d.ts +28 -0
- package/dist/gvc/gvrender-set-style.d.ts.map +1 -0
- package/dist/gvc/usershape.d.ts +18 -0
- package/dist/gvc/usershape.d.ts.map +1 -1
- package/dist/index.js +2478 -912
- package/dist/index.js.map +4 -4
- package/dist/layout/dot/compound.d.ts.map +1 -1
- package/dist/layout/dot/edge-route-clip.d.ts +14 -4
- package/dist/layout/dot/edge-route-clip.d.ts.map +1 -1
- package/dist/layout/dot/edge-route-routing.d.ts.map +1 -1
- package/dist/layout/dot/graph-label.d.ts.map +1 -1
- package/dist/layout/neato/__fixtures__/dijkstra-f.d.ts +11 -0
- package/dist/layout/neato/__fixtures__/dijkstra-f.d.ts.map +1 -0
- package/dist/layout/neato/__fixtures__/hier-imds.d.ts +36 -0
- package/dist/layout/neato/__fixtures__/hier-imds.d.ts.map +1 -0
- package/dist/layout/neato/__fixtures__/hier-majorization.d.ts +32 -0
- package/dist/layout/neato/__fixtures__/hier-majorization.d.ts.map +1 -0
- package/dist/layout/neato/cdt-surface.d.ts.map +1 -1
- package/dist/layout/neato/compute-hierarchy.d.ts +24 -0
- package/dist/layout/neato/compute-hierarchy.d.ts.map +1 -0
- package/dist/layout/neato/conjgrad-mkernel.d.ts +20 -0
- package/dist/layout/neato/conjgrad-mkernel.d.ts.map +1 -0
- package/dist/layout/neato/conjgrad.d.ts +19 -0
- package/dist/layout/neato/conjgrad.d.ts.map +1 -1
- package/dist/layout/neato/constrained-majorization.d.ts +26 -0
- package/dist/layout/neato/constrained-majorization.d.ts.map +1 -0
- package/dist/layout/neato/dijkstra.d.ts +33 -16
- package/dist/layout/neato/dijkstra.d.ts.map +1 -1
- package/dist/layout/neato/gts-triangle.d.ts +14 -0
- package/dist/layout/neato/gts-triangle.d.ts.map +1 -0
- package/dist/layout/neato/init.d.ts +2 -12
- package/dist/layout/neato/init.d.ts.map +1 -1
- package/dist/layout/neato/kkutils.d.ts +20 -0
- package/dist/layout/neato/kkutils.d.ts.map +1 -0
- package/dist/layout/neato/make-graph-data.d.ts +43 -0
- package/dist/layout/neato/make-graph-data.d.ts.map +1 -0
- package/dist/layout/neato/matrix-ops.d.ts +46 -0
- package/dist/layout/neato/matrix-ops.d.ts.map +1 -1
- package/dist/layout/neato/opt-arrangement.d.ts +20 -0
- package/dist/layout/neato/opt-arrangement.d.ts.map +1 -0
- package/dist/layout/neato/quad-prog-solve.d.ts +44 -0
- package/dist/layout/neato/quad-prog-solve.d.ts.map +1 -0
- package/dist/layout/neato/smart-ini-x.d.ts +9 -0
- package/dist/layout/neato/smart-ini-x.d.ts.map +1 -0
- package/dist/layout/neato/splines.d.ts +1 -1
- package/dist/layout/neato/splines.d.ts.map +1 -1
- package/dist/layout/neato/start.d.ts +4 -1
- package/dist/layout/neato/start.d.ts.map +1 -1
- package/dist/layout/neato/stress.d.ts +8 -0
- package/dist/layout/neato/stress.d.ts.map +1 -1
- package/dist/model/edge.d.ts +1 -1
- package/dist/model/edge.d.ts.map +1 -1
- package/dist/model/lazy-attrs.d.ts +10 -0
- package/dist/model/lazy-attrs.d.ts.map +1 -0
- package/dist/model/node.d.ts +1 -1
- package/dist/model/node.d.ts.map +1 -1
- package/dist/ortho/index.d.ts.map +1 -1
- package/dist/render/dot/edge-draw.d.ts +1 -1
- package/dist/render/dot/edge-draw.d.ts.map +1 -1
- package/dist/render/svg-multicolor.d.ts +2 -2
- package/dist/render/svg-multicolor.d.ts.map +1 -1
- package/dist/render.js +2358 -792
- package/dist/render.js.map +4 -4
- package/package.json +1 -1
- package/src/common/attr-get.ts +46 -0
- package/src/common/graph-init.ts +1 -1
- package/src/common/htmltable-emit-fill.ts +56 -11
- package/src/common/htmltable-emit.ts +4 -3
- package/src/common/htmltable-lex-expat.ts +21 -3
- package/src/common/htmltable-pos-runs.ts +11 -6
- package/src/common/htmltable.ts +28 -116
- package/src/common/libm-trig.ts +43 -0
- package/src/common/make-label.ts +14 -2
- package/src/common/multicolor.ts +88 -24
- package/src/common/poly-gencode.ts +16 -8
- package/src/common/poly-init.ts +23 -19
- package/src/common/postproc.ts +2 -2
- package/src/common/splines-clip.ts +15 -12
- package/src/common/splines-routespl.ts +2 -2
- package/src/common/textmeasure-lookup.ts +1 -1
- package/src/gvc/context.ts +8 -0
- package/src/gvc/device-cluster.ts +63 -8
- package/src/gvc/device.ts +16 -2
- package/src/gvc/first-layer.ts +26 -0
- package/src/gvc/gvrender-set-style.ts +114 -0
- package/src/gvc/usershape.ts +26 -0
- package/src/layout/dot/cluster.ts +1 -1
- package/src/layout/dot/compound.ts +3 -4
- package/src/layout/dot/conc.ts +1 -1
- package/src/layout/dot/edge-route-clip.ts +19 -7
- package/src/layout/dot/edge-route-routing.ts +11 -3
- package/src/layout/dot/fastgr.ts +2 -2
- package/src/layout/dot/graph-label.ts +1 -15
- package/src/layout/neato/__fixtures__/dijkstra-f.ts +17 -0
- package/src/layout/neato/__fixtures__/hier/c1554.gv +33 -0
- package/src/layout/neato/__fixtures__/hier/c1554.plain +26 -0
- package/src/layout/neato/__fixtures__/hier/c1554.xdot +161 -0
- package/src/layout/neato/__fixtures__/hier/cases/cycle.gv +1 -0
- package/src/layout/neato/__fixtures__/hier/cases/cycle.plain +10 -0
- package/src/layout/neato/__fixtures__/hier/cases/cycle.stderr +0 -0
- package/src/layout/neato/__fixtures__/hier/cases/dirnone.gv +1 -0
- package/src/layout/neato/__fixtures__/hier/cases/dirnone.plain +11 -0
- package/src/layout/neato/__fixtures__/hier/cases/dirnone.stderr +0 -0
- package/src/layout/neato/__fixtures__/hier/cases/epsilon.gv +1 -0
- package/src/layout/neato/__fixtures__/hier/cases/epsilon.plain +13 -0
- package/src/layout/neato/__fixtures__/hier/cases/epsilon.stderr +0 -0
- package/src/layout/neato/__fixtures__/hier/cases/levelsgap.gv +1 -0
- package/src/layout/neato/__fixtures__/hier/cases/levelsgap.plain +8 -0
- package/src/layout/neato/__fixtures__/hier/cases/levelsgap.stderr +0 -0
- package/src/layout/neato/__fixtures__/hier/cases/mds.gv +1 -0
- package/src/layout/neato/__fixtures__/hier/cases/mds.plain +10 -0
- package/src/layout/neato/__fixtures__/hier/cases/mds.stderr +0 -0
- package/src/layout/neato/__fixtures__/hier/cases/multiedge.gv +1 -0
- package/src/layout/neato/__fixtures__/hier/cases/multiedge.plain +10 -0
- package/src/layout/neato/__fixtures__/hier/cases/multiedge.stderr +0 -0
- package/src/layout/neato/__fixtures__/hier/cases/neglevelsgap.gv +1 -0
- package/src/layout/neato/__fixtures__/hier/cases/neglevelsgap.plain +8 -0
- package/src/layout/neato/__fixtures__/hier/cases/neglevelsgap.stderr +0 -0
- package/src/layout/neato/__fixtures__/hier/cases/startrandom.gv +1 -0
- package/src/layout/neato/__fixtures__/hier/cases/startrandom.plain +10 -0
- package/src/layout/neato/__fixtures__/hier/cases/startrandom.stderr +0 -0
- package/src/layout/neato/__fixtures__/hier/cases/startregular.gv +1 -0
- package/src/layout/neato/__fixtures__/hier/cases/startregular.plain +10 -0
- package/src/layout/neato/__fixtures__/hier/cases/startregular.stderr +0 -0
- package/src/layout/neato/__fixtures__/hier/cases/tdist.gv +1 -0
- package/src/layout/neato/__fixtures__/hier/cases/tdist.plain +13 -0
- package/src/layout/neato/__fixtures__/hier/cases/tdist.stderr +0 -0
- package/src/layout/neato/__fixtures__/hier/cases/undirected.gv +1 -0
- package/src/layout/neato/__fixtures__/hier/cases/undirected.plain +10 -0
- package/src/layout/neato/__fixtures__/hier/cases/undirected.stderr +0 -0
- package/src/layout/neato/__fixtures__/hier/hier-probe.gv +1 -0
- package/src/layout/neato/__fixtures__/hier/hier-probe.plain +16 -0
- package/src/layout/neato/__fixtures__/hier/hier-probe.xdot +62 -0
- package/src/layout/neato/__fixtures__/hier-imds.ts +151 -0
- package/src/layout/neato/__fixtures__/hier-majorization.ts +145 -0
- package/src/layout/neato/cdt-surface.ts +6 -16
- package/src/layout/neato/compute-hierarchy.ts +90 -0
- package/src/layout/neato/conjgrad-mkernel.ts +141 -0
- package/src/layout/neato/conjgrad.ts +208 -0
- package/src/layout/neato/constrained-majorization.ts +479 -0
- package/src/layout/neato/dijkstra.ts +218 -92
- package/src/layout/neato/edge-len.ts +2 -2
- package/src/layout/neato/fdp-adjust.ts +3 -3
- package/src/layout/neato/gts-triangle.ts +42 -0
- package/src/layout/neato/init.ts +57 -57
- package/src/layout/neato/kk.ts +2 -2
- package/src/layout/neato/kkutils.ts +96 -0
- package/src/layout/neato/make-graph-data.ts +166 -0
- package/src/layout/neato/matrix-ops.ts +138 -0
- package/src/layout/neato/opt-arrangement.ts +86 -0
- package/src/layout/neato/quad-prog-solve.ts +489 -0
- package/src/layout/neato/smart-ini-x.ts +503 -0
- package/src/layout/neato/splines.ts +4 -4
- package/src/layout/neato/start.ts +7 -6
- package/src/layout/neato/stress.ts +28 -0
- package/src/model/edge.ts +3 -2
- package/src/model/lazy-attrs.ts +36 -0
- package/src/model/node.ts +3 -2
- package/src/ortho/index.ts +2 -3
- package/src/parser/dot.js +1 -1
- package/src/parser/dot.pegjs +1 -1
- package/src/parser/index.ts +1 -1
- package/src/render/color-resolve.ts +2 -2
- package/src/render/dot/edge-draw.ts +6 -3
- package/src/render/dot.ts +1 -1
- package/src/render/svg-edge-split.ts +2 -2
- package/src/render/svg-multicolor.ts +4 -3
- package/src/render/svg-parallel-edge.ts +2 -2
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Dense k-D stress majorization under hierarchy constraints (`mode=hier`):
|
|
5
|
+
* partitions the nodes into levels (compute_y_coords + compute_hierarchy),
|
|
6
|
+
* then runs the majorization loop, solving the y axis by constrained
|
|
7
|
+
* majorization (quadratic programming over the levels) and every other axis
|
|
8
|
+
* by conjugate gradient.
|
|
9
|
+
*
|
|
10
|
+
* `Verbose` timing and `fprintf` lines are not ported (the port has no Verbose).
|
|
11
|
+
* The C function takes nine parameters; the trailing scalars travel as
|
|
12
|
+
* {@link HierarchyOpts} to stay within the five-parameter limit.
|
|
13
|
+
*
|
|
14
|
+
* @see lib/neatogen/constrained_majorization.c
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { RenderError } from '../../errors.js';
|
|
18
|
+
import type { Node } from '../../model/node.js';
|
|
19
|
+
import { conjugateGradientMkernelFused, rightMultWithVectorFFFused } from './conjgrad-mkernel.js';
|
|
20
|
+
import { computeHierarchy, type Hierarchy } from './compute-hierarchy.js';
|
|
21
|
+
import type { VtxData } from './dijkstra.js';
|
|
22
|
+
import { MODEL_CIRCUIT, MODEL_MDS, MODEL_SUBSET } from './init.js';
|
|
23
|
+
import {
|
|
24
|
+
fma, invertSqrtVec, invertVec, orthog1, sqrtVecF, squareVec, vectorsInnerProductF,
|
|
25
|
+
} from './matrix-ops.js';
|
|
26
|
+
import type { DirectedVtxData } from './opt-arrangement.js';
|
|
27
|
+
import { computeYCoords } from './opt-arrangement.js';
|
|
28
|
+
import {
|
|
29
|
+
constrainedMajorizationNewWithGaps, initConstrainedMajorization, type CMajEnv,
|
|
30
|
+
} from './quad-prog-solve.js';
|
|
31
|
+
import { imdsGivenDim } from './smart-ini-x.js';
|
|
32
|
+
import {
|
|
33
|
+
computeApspPacked, computeWeightedApspPacked, initLayout, stressMajorizationKDMkernel,
|
|
34
|
+
} from './stress-kernel.js';
|
|
35
|
+
import { mdsModel, OPT_SMART_INIT, TOLERANCE_CG } from './stress.js';
|
|
36
|
+
|
|
37
|
+
const fr = Math.fround;
|
|
38
|
+
|
|
39
|
+
/** `#define localConstrMajorIterations` @see lib/neatogen/constrained_majorization.c */
|
|
40
|
+
const LOCAL_CONSTR_MAJOR_ITERATIONS = 15;
|
|
41
|
+
/** `#define levels_sep_tol` @see lib/neatogen/constrained_majorization.c */
|
|
42
|
+
const LEVELS_SEP_TOL = 1e-1;
|
|
43
|
+
/** `abs_tol`: gaps smaller than this are noise. @see lib/neatogen/constrained_majorization.c */
|
|
44
|
+
const ABS_TOL = 1e-2;
|
|
45
|
+
/** Iteration cap of the (dim-1)-D layout that seeds dim > 2. */
|
|
46
|
+
const SUBSPACE_ITERATIONS = 15;
|
|
47
|
+
/** `FLT_MAX` @see float.h */
|
|
48
|
+
const FLT_MAX = 3.4028234663852886e38;
|
|
49
|
+
/** `DBL_MAX` @see float.h */
|
|
50
|
+
const DBL_MAX = Number.MAX_VALUE;
|
|
51
|
+
/** Level count below which there is no hierarchy to impose. */
|
|
52
|
+
const MIN_LEVELS = 1;
|
|
53
|
+
/** The y axis, solved by quadratic programming. */
|
|
54
|
+
const Y_AXIS = 1;
|
|
55
|
+
|
|
56
|
+
/** The scalar parameters of `stress_majorization_with_hierarchy`. */
|
|
57
|
+
export interface HierarchyOpts {
|
|
58
|
+
/** Dimensionality of the layout. */
|
|
59
|
+
dim: number;
|
|
60
|
+
/** `opt_*` bits. */
|
|
61
|
+
opts: number;
|
|
62
|
+
/** `MODEL_*` (neato.h values). */
|
|
63
|
+
model: number;
|
|
64
|
+
/** Maximum number of iterations. */
|
|
65
|
+
maxi: number;
|
|
66
|
+
/** Minimum separation of consecutive levels (`levelsgap`). */
|
|
67
|
+
levelsGap: number;
|
|
68
|
+
/** The `Epsilon` global: convergence tolerance (`epsilon` attribute). */
|
|
69
|
+
epsilon: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** The scalar parameters of `stress_majorization_kD_mkernel`. */
|
|
73
|
+
interface KdOpts {
|
|
74
|
+
dim: number;
|
|
75
|
+
opts: number;
|
|
76
|
+
model: number;
|
|
77
|
+
maxi: number;
|
|
78
|
+
epsilon: number;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Throw where C runs a distance model or initialisation this port does not have. */
|
|
82
|
+
function unsupported(what: string): never {
|
|
83
|
+
throw new RenderError(`hierarchical stress majorization: ${what} is not supported yet`, 'UNSUPPORTED_FEATURE');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Distance model dispatch shared by both drivers: the MDS model is ported
|
|
88
|
+
* (`mdsModel`, `null` when the graph has no lengths); the subset and circuit
|
|
89
|
+
* models need C helpers that are not ported (AD-6, stop 12) and stay loud.
|
|
90
|
+
* @see lib/neatogen/stress.c:stress_majorization_kD_mkernel
|
|
91
|
+
* @see lib/neatogen/constrained_majorization.c:stress_majorization_with_hierarchy
|
|
92
|
+
*/
|
|
93
|
+
function modelDistances(graph: VtxData[], n: number, model: number): Float32Array | null {
|
|
94
|
+
if (model === MODEL_SUBSET) unsupported('the subset distance model');
|
|
95
|
+
if (model === MODEL_CIRCUIT) unsupported('the circuit distance model');
|
|
96
|
+
return model === MODEL_MDS ? mdsModel(graph, n) : null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* `stress_majorization_kD_mkernel`: the distance matrix per model, then the
|
|
101
|
+
* ported kernel. C takes `smart_ini` through `sparse_stress_subspace_majorization_kD`,
|
|
102
|
+
* which is not ported.
|
|
103
|
+
* @see lib/neatogen/stress.c:stress_majorization_kD_mkernel
|
|
104
|
+
*/
|
|
105
|
+
function unconstrainedMajorization(
|
|
106
|
+
graph: VtxData[], n: number, dCoords: Float64Array[], nodes: Node[], o: KdOpts,
|
|
107
|
+
): number {
|
|
108
|
+
if (o.maxi < 0) return 0;
|
|
109
|
+
const weighted = (graph[0]?.ewgts.length ?? 0) > 0;
|
|
110
|
+
const Dij = modelDistances(graph, n, o.model)
|
|
111
|
+
?? (weighted ? computeWeightedApspPacked(graph, n) : computeApspPacked(graph, n));
|
|
112
|
+
if ((o.opts & OPT_SMART_INIT) !== 0 && n > 1) unsupported('the smart initial layout');
|
|
113
|
+
return stressMajorizationKDMkernel(Dij, n, dCoords, nodes, {
|
|
114
|
+
dim: o.dim, opts: o.opts, maxi: o.maxi, epsilon: o.epsilon,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Does any edge carry a direction? (`edists` absent, or all zero, means none.)
|
|
120
|
+
* @see lib/neatogen/constrained_majorization.c:stress_majorization_with_hierarchy
|
|
121
|
+
*/
|
|
122
|
+
function hasDirectionality(graph: DirectedVtxData[], n: number): boolean {
|
|
123
|
+
if (!graph[0]?.edists) return false;
|
|
124
|
+
let directionalityExist = false;
|
|
125
|
+
for (let i = 0; i < n; i++) {
|
|
126
|
+
const edists = graph[i]!.edists;
|
|
127
|
+
for (let j = 1; j < graph[i]!.nedges; j++) {
|
|
128
|
+
directionalityExist ||= edists![j] !== 0;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return directionalityExist;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Push the levels apart: each level moves up by the shortfall to `levelsGap`.
|
|
136
|
+
* @see lib/neatogen/constrained_majorization.c:stress_majorization_with_hierarchy
|
|
137
|
+
*/
|
|
138
|
+
function separateLevels(y: Float64Array, h: Hierarchy, n: number, levelsGap: number): void {
|
|
139
|
+
const { ordering, levels, numLevels } = h;
|
|
140
|
+
let displacement = 0;
|
|
141
|
+
for (let i = 0; i < numLevels; i++) {
|
|
142
|
+
const shortfall = levelsGap - (y[ordering[levels[i]!]!]! + displacement - y[ordering[levels[i]! - 1]!]!);
|
|
143
|
+
displacement += 0 > shortfall ? 0 : shortfall; // C MAX(a, b): `a > b ? a : b`
|
|
144
|
+
const stop = i < numLevels - 1 ? levels[i + 1]! : n;
|
|
145
|
+
for (let j = levels[i]!; j < stop; j++) y[ordering[j]!]! += displacement;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* The smart-init layering: y from `compute_y_coords`, levels from it, the
|
|
151
|
+
* levels separated, then x from `IMDS_given_dim`. Returns the C return value
|
|
152
|
+
* when the function ends here, else the hierarchy.
|
|
153
|
+
* @see lib/neatogen/constrained_majorization.c:stress_majorization_with_hierarchy
|
|
154
|
+
*/
|
|
155
|
+
function smartInitLayers(
|
|
156
|
+
graph: DirectedVtxData[], n: number, dCoords: Float64Array[], nodes: Node[], o: HierarchyOpts,
|
|
157
|
+
): Hierarchy | number {
|
|
158
|
+
const { dim } = o;
|
|
159
|
+
if (dim > 2) {
|
|
160
|
+
// the dim==2 case is handled below
|
|
161
|
+
const sub = { dim: dim - 1, opts: o.opts, model: o.model, maxi: SUBSPACE_ITERATIONS, epsilon: o.epsilon };
|
|
162
|
+
if (unconstrainedMajorization(graph, n, dCoords.slice(1), nodes, sub) < 0) return -1;
|
|
163
|
+
// now copy the y-axis into the (dim-1)-axis
|
|
164
|
+
dCoords[dim - 1]!.set(dCoords[1]!.subarray(0, n));
|
|
165
|
+
}
|
|
166
|
+
const x = dCoords[0]!;
|
|
167
|
+
const y = dCoords[1]!;
|
|
168
|
+
if (computeYCoords(graph, n, y, n)) return -1;
|
|
169
|
+
const h = computeHierarchy(graph, n, ABS_TOL, LEVELS_SEP_TOL, y);
|
|
170
|
+
if (h === null) return -1;
|
|
171
|
+
if (h.numLevels < MIN_LEVELS) {
|
|
172
|
+
// no hierarchy found, use faster algorithm
|
|
173
|
+
return unconstrainedMajorization(graph, n, dCoords, nodes, o);
|
|
174
|
+
}
|
|
175
|
+
if (o.levelsGap > 0) separateLevels(y, h, n, o.levelsGap); // ensure the initial layout separates levels
|
|
176
|
+
if (dim === 2 && imdsGivenDim(graph, n, y, x, o.epsilon)) return -1;
|
|
177
|
+
return h;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Layering without smart init: the ordinary initial layout, levels from
|
|
182
|
+
* `compute_y_coords` (which draws from the libc `rand()` stream).
|
|
183
|
+
* @see lib/neatogen/constrained_majorization.c:stress_majorization_with_hierarchy
|
|
184
|
+
*/
|
|
185
|
+
function plainInitLayers(
|
|
186
|
+
graph: DirectedVtxData[], n: number, dCoords: Float64Array[], nodes: Node[], dim: number,
|
|
187
|
+
): Hierarchy | number {
|
|
188
|
+
initLayout(n, dim, dCoords, nodes);
|
|
189
|
+
const h = computeHierarchy(graph, n, ABS_TOL, LEVELS_SEP_TOL, null);
|
|
190
|
+
return h === null ? -1 : h;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Euclidean distance between nodes `i` and `j` in `dim` dimensions.
|
|
195
|
+
* @see lib/neatogen/kkutils.c:distance_kD
|
|
196
|
+
*/
|
|
197
|
+
function distanceKD(coords: Float64Array[], dim: number, i: number, j: number): number {
|
|
198
|
+
let sum = 0;
|
|
199
|
+
for (let k = 0; k < dim; k++) {
|
|
200
|
+
const d = coords[k]![i]! - coords[k]![j]!;
|
|
201
|
+
sum = fma(d, d, sum);
|
|
202
|
+
}
|
|
203
|
+
return Math.sqrt(sum);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* For numerical stability, scale the layout down (no jiggling: it might
|
|
208
|
+
* conflict with the constraints).
|
|
209
|
+
* @see lib/neatogen/constrained_majorization.c:stress_majorization_with_hierarchy
|
|
210
|
+
*/
|
|
211
|
+
function scaleDownLayout(dCoords: Float64Array[], n: number, dim: number): void {
|
|
212
|
+
let max = 1;
|
|
213
|
+
for (let i = 0; i < dim; i++) {
|
|
214
|
+
for (let j = 0; j < n; j++) {
|
|
215
|
+
const a = Math.abs(dCoords[i]![j]!);
|
|
216
|
+
if (a > max) max = a; // fmax ignores a NaN operand, as `>` does
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
for (let i = 0; i < dim; i++) {
|
|
220
|
+
for (let j = 0; j < n; j++) dCoords[i]![j]! *= 10 / max;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Scale the distances so the layout's average stretch is one.
|
|
226
|
+
* @see lib/neatogen/constrained_majorization.c:stress_majorization_with_hierarchy
|
|
227
|
+
*/
|
|
228
|
+
function scaleDistances(Dij: Float32Array, dCoords: Float64Array[], n: number, dim: number): void {
|
|
229
|
+
const length = n + Math.trunc((n * (n - 1)) / 2);
|
|
230
|
+
const sum1 = Math.trunc((n * (n - 1)) / 2);
|
|
231
|
+
let sum2 = 0;
|
|
232
|
+
for (let count = 0, i = 0; i < n - 1; i++) {
|
|
233
|
+
count++; // skip self distance
|
|
234
|
+
for (let j = i + 1; j < n; j++, count++) {
|
|
235
|
+
sum2 += distanceKD(dCoords, dim, i, j) / Dij[count]!;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
const scaleRatio = fr(sum2 / sum1);
|
|
239
|
+
for (let i = 0; i < length; i++) Dij[i] = Dij[i]! * scaleRatio;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Centre every axis; y is not centred but translated so that `y[0] = 0`.
|
|
244
|
+
* @see lib/neatogen/constrained_majorization.c:stress_majorization_with_hierarchy
|
|
245
|
+
*/
|
|
246
|
+
function centerLayout(dCoords: Float64Array[], n: number, dim: number): void {
|
|
247
|
+
for (let i = 0; i < dim; i++) orthog1(n, dCoords[i]!);
|
|
248
|
+
const y0 = dCoords[Y_AXIS]![0]!;
|
|
249
|
+
for (let i = 0; i < n; i++) dCoords[Y_AXIS]![i]! -= y0;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* The row sums of the off-diagonal entries of `lap2`, as the negated degrees.
|
|
254
|
+
* @see lib/neatogen/constrained_majorization.c:stress_majorization_with_hierarchy
|
|
255
|
+
*/
|
|
256
|
+
function negatedDegrees(lap2: Float32Array, n: number): Float64Array {
|
|
257
|
+
const degrees = new Float64Array(n);
|
|
258
|
+
for (let i = 0, count = 0; i < n - 1; i++) {
|
|
259
|
+
let degree = 0;
|
|
260
|
+
count++; // skip main diag entry
|
|
261
|
+
for (let j = 1; j < n - i; j++, count++) {
|
|
262
|
+
const val = lap2[count]!;
|
|
263
|
+
degree += val;
|
|
264
|
+
degrees[i + j]! -= val;
|
|
265
|
+
}
|
|
266
|
+
degrees[i]! -= degree;
|
|
267
|
+
}
|
|
268
|
+
return degrees;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Turn the packed distances into the negated Laplacian `lap2`, in place:
|
|
273
|
+
* off-diagonals `1/d^2`, diagonals minus the row sums.
|
|
274
|
+
* @see lib/neatogen/constrained_majorization.c:stress_majorization_with_hierarchy
|
|
275
|
+
*/
|
|
276
|
+
function buildLap2(lap2: Float32Array, n: number): void {
|
|
277
|
+
const lapLength = n + Math.trunc((n * (n - 1)) / 2);
|
|
278
|
+
squareVec(lapLength, lap2);
|
|
279
|
+
invertVec(lapLength, lap2); // off-diagonal entries
|
|
280
|
+
const degrees = negatedDegrees(lap2, n);
|
|
281
|
+
for (let step = n, count = 0, i = 0; i < n; i++, count += step, step--) {
|
|
282
|
+
lap2[count] = degrees[i]!;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** Working storage of the majorization loop. */
|
|
287
|
+
interface LoopState {
|
|
288
|
+
n: number;
|
|
289
|
+
dim: number;
|
|
290
|
+
lap1: Float32Array;
|
|
291
|
+
lap2: Float32Array;
|
|
292
|
+
coords: Float32Array[];
|
|
293
|
+
b: Float32Array[];
|
|
294
|
+
tmpCoords: Float32Array;
|
|
295
|
+
distAccumulator: Float32Array;
|
|
296
|
+
degrees: Float64Array;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Squared distances between node `i` and `i+1..n-1`, summed over the axes,
|
|
301
|
+
* then inverted to `1/d_ij` (zero where undefined or overflowing).
|
|
302
|
+
* @see lib/neatogen/constrained_majorization.c:stress_majorization_with_hierarchy
|
|
303
|
+
*/
|
|
304
|
+
function inverseDistances(st: LoopState, i: number, len: number): void {
|
|
305
|
+
const { coords, tmpCoords, distAccumulator } = st;
|
|
306
|
+
distAccumulator.fill(0, 0, st.n);
|
|
307
|
+
for (let k = 0; k < st.dim; k++) {
|
|
308
|
+
const ck = coords[k]!;
|
|
309
|
+
for (let j = 0; j < len; j++) tmpCoords[j] = ck[i]! - ck[i + 1 + j]!;
|
|
310
|
+
for (let j = 0; j < len; j++) tmpCoords[j] = tmpCoords[j]! * tmpCoords[j]!;
|
|
311
|
+
for (let j = 0; j < len; j++) distAccumulator[j] = tmpCoords[j]! + distAccumulator[j]!;
|
|
312
|
+
}
|
|
313
|
+
invertSqrtVec(len, distAccumulator); // convert to 1/d_{ij}
|
|
314
|
+
for (let j = 0; j < len; j++) {
|
|
315
|
+
// detect overflows
|
|
316
|
+
if (distAccumulator[j]! >= FLT_MAX || distAccumulator[j]! < 0) distAccumulator[j] = 0;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* The Laplacian of `1/(d_ij*|p_i-p_j|)` at the current coordinates.
|
|
322
|
+
* @see lib/neatogen/constrained_majorization.c:stress_majorization_with_hierarchy
|
|
323
|
+
*/
|
|
324
|
+
function buildLap1(st: LoopState): void {
|
|
325
|
+
const { n, lap1, lap2, degrees, distAccumulator } = st;
|
|
326
|
+
degrees.fill(0);
|
|
327
|
+
sqrtVecF(n + Math.trunc((n * (n - 1)) / 2), lap2, lap1);
|
|
328
|
+
let count = 0;
|
|
329
|
+
for (let i = 0; i < n - 1; i++) {
|
|
330
|
+
const len = n - i - 1;
|
|
331
|
+
inverseDistances(st, i, len);
|
|
332
|
+
count++; // save place for the main diagonal entry
|
|
333
|
+
let degree = 0;
|
|
334
|
+
for (let j = 0; j < len; j++, count++) {
|
|
335
|
+
const val = fr(lap1[count]! * distAccumulator[j]!);
|
|
336
|
+
lap1[count] = val;
|
|
337
|
+
degree += val;
|
|
338
|
+
degrees[i + j + 1]! -= val;
|
|
339
|
+
}
|
|
340
|
+
degrees[i]! -= degree;
|
|
341
|
+
}
|
|
342
|
+
for (let step = n, count2 = 0, i = 0; i < n; i++, count2 += step, step--) {
|
|
343
|
+
lap1[count2] = degrees[i]!;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* The stress at the current coordinates; also leaves `b[k] = lap1*coords[k]`.
|
|
349
|
+
* The Laplacians are negated, so the second sum is subtracted.
|
|
350
|
+
* @see lib/neatogen/constrained_majorization.c:stress_majorization_with_hierarchy
|
|
351
|
+
*/
|
|
352
|
+
function computeNewStress(st: LoopState, constantTerm: number): number {
|
|
353
|
+
const { n, dim, lap1, lap2, coords, b, tmpCoords } = st;
|
|
354
|
+
for (let k = 0; k < dim; k++) rightMultWithVectorFFFused(lap1, n, coords[k]!, b[k]!);
|
|
355
|
+
let newStress = 0;
|
|
356
|
+
for (let k = 0; k < dim; k++) newStress += vectorsInnerProductF(n, coords[k]!, b[k]!);
|
|
357
|
+
newStress *= 2;
|
|
358
|
+
newStress += constantTerm; // only after mult by 2
|
|
359
|
+
for (let k = 0; k < dim; k++) {
|
|
360
|
+
rightMultWithVectorFFFused(lap2, n, coords[k]!, tmpCoords);
|
|
361
|
+
newStress -= vectorsInnerProductF(n, coords[k]!, tmpCoords);
|
|
362
|
+
}
|
|
363
|
+
return newStress;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** Everything the loop needs besides its working storage. */
|
|
367
|
+
interface SolveCtx {
|
|
368
|
+
cMajEnv: CMajEnv;
|
|
369
|
+
levelsGap: number;
|
|
370
|
+
maxi: number;
|
|
371
|
+
epsilon: number;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Optimise `trace(X'LX)+X'B` per axis: constrained majorization in y, conjugate
|
|
376
|
+
* gradient elsewhere. Returns true when conjugate gradient fails.
|
|
377
|
+
* @see lib/neatogen/constrained_majorization.c:stress_majorization_with_hierarchy
|
|
378
|
+
*/
|
|
379
|
+
function solveAxes(st: LoopState, ctx: SolveCtx): boolean {
|
|
380
|
+
for (let k = 0; k < st.dim; k++) {
|
|
381
|
+
if (k === Y_AXIS) {
|
|
382
|
+
// use quad solver in the y-dimension
|
|
383
|
+
constrainedMajorizationNewWithGaps(
|
|
384
|
+
ctx.cMajEnv, st.b[k]!, st.coords, k, LOCAL_CONSTR_MAJOR_ITERATIONS, ctx.levelsGap,
|
|
385
|
+
);
|
|
386
|
+
} else if (conjugateGradientMkernelFused(st.lap2, st.coords[k]!, st.b[k]!, st.n, { tol: TOLERANCE_CG, maxIterations: st.n })) {
|
|
387
|
+
// use conjugate gradient for all dimensions except y
|
|
388
|
+
return true;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return false;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* The majorization loop. Returns the iteration count, or -1 when conjugate
|
|
396
|
+
* gradient failed (coordinates are then not written back).
|
|
397
|
+
* @see lib/neatogen/constrained_majorization.c:stress_majorization_with_hierarchy
|
|
398
|
+
*/
|
|
399
|
+
function majorize(st: LoopState, ctx: SolveCtx, dCoords: Float64Array[]): number {
|
|
400
|
+
const constantTerm = Math.trunc((st.n * (st.n - 1)) / 2);
|
|
401
|
+
let oldStress = DBL_MAX; // at least one iteration
|
|
402
|
+
let iterations = 0;
|
|
403
|
+
for (let converged = false; iterations < ctx.maxi && !converged; iterations++) {
|
|
404
|
+
buildLap1(st);
|
|
405
|
+
const newStress = computeNewStress(st, constantTerm);
|
|
406
|
+
// check for convergence
|
|
407
|
+
converged = Math.abs(newStress - oldStress) / Math.abs(oldStress + 1e-10) < ctx.epsilon;
|
|
408
|
+
// in first iteration we allowed stress increase, which might result by imposing constraints
|
|
409
|
+
converged ||= iterations > 1 && newStress > oldStress;
|
|
410
|
+
oldStress = newStress;
|
|
411
|
+
if (solveAxes(st, ctx)) return -1;
|
|
412
|
+
}
|
|
413
|
+
for (let i = 0; i < st.dim; i++) {
|
|
414
|
+
for (let j = 0; j < st.n; j++) dCoords[i]![j] = st.coords[i]![j]!;
|
|
415
|
+
}
|
|
416
|
+
return iterations;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* The `float` working copy of the layout.
|
|
421
|
+
* @see lib/neatogen/constrained_majorization.c:stress_majorization_with_hierarchy
|
|
422
|
+
*/
|
|
423
|
+
function toFloatCoords(dCoords: Float64Array[], n: number, dim: number): Float32Array[] {
|
|
424
|
+
const coords: Float32Array[] = [];
|
|
425
|
+
for (let i = 0; i < dim; i++) coords.push(Float32Array.from(dCoords[i]!.subarray(0, n)));
|
|
426
|
+
return coords;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Fresh working storage for the loop.
|
|
431
|
+
* @see lib/neatogen/constrained_majorization.c:stress_majorization_with_hierarchy
|
|
432
|
+
*/
|
|
433
|
+
function newLoopState(lap2: Float32Array, coords: Float32Array[], n: number): LoopState {
|
|
434
|
+
const dim = coords.length;
|
|
435
|
+
return {
|
|
436
|
+
n, dim, lap2, coords,
|
|
437
|
+
lap1: new Float32Array(n + Math.trunc((n * (n - 1)) / 2)),
|
|
438
|
+
b: Array.from({ length: dim }, () => new Float32Array(n)),
|
|
439
|
+
tmpCoords: new Float32Array(n),
|
|
440
|
+
distAccumulator: new Float32Array(n),
|
|
441
|
+
degrees: new Float64Array(n),
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Stress majorization of the layout under hierarchy constraints. Writes the
|
|
447
|
+
* layout to `dCoords` and returns the iteration count (C's return value; -1
|
|
448
|
+
* on failure). Graphs without directed edges, and layouts that find no
|
|
449
|
+
* hierarchy, go to the unconstrained stress majorization.
|
|
450
|
+
* @see lib/neatogen/constrained_majorization.c:stress_majorization_with_hierarchy
|
|
451
|
+
*/
|
|
452
|
+
export function stressMajorizationWithHierarchy(
|
|
453
|
+
graph: DirectedVtxData[], n: number, dCoords: Float64Array[], nodes: Node[], o: HierarchyOpts,
|
|
454
|
+
): number {
|
|
455
|
+
if (!hasDirectionality(graph, n)) return unconstrainedMajorization(graph, n, dCoords, nodes, o);
|
|
456
|
+
|
|
457
|
+
// First, partition nodes into layers: these are our constraints
|
|
458
|
+
const smartIni = (o.opts & OPT_SMART_INIT) !== 0;
|
|
459
|
+
const layers = smartIni
|
|
460
|
+
? smartInitLayers(graph, n, dCoords, nodes, o)
|
|
461
|
+
: plainInitLayers(graph, n, dCoords, nodes, o.dim);
|
|
462
|
+
if (typeof layers === 'number') return layers;
|
|
463
|
+
if (n === 1) return 0;
|
|
464
|
+
|
|
465
|
+
// Compute the all-pairs-shortest-distances matrix
|
|
466
|
+
if (o.maxi === 0) return 0;
|
|
467
|
+
const Dij = modelDistances(graph, n, o.model) ?? computeApspPacked(graph, n);
|
|
468
|
+
if (!smartIni) scaleDownLayout(dCoords, n, o.dim);
|
|
469
|
+
if (o.levelsGap > 0) scaleDistances(Dij, dCoords, n, o.dim);
|
|
470
|
+
|
|
471
|
+
// Layout initialization, Laplacian computation
|
|
472
|
+
centerLayout(dCoords, n, o.dim);
|
|
473
|
+
const st = newLoopState(Dij, toFloatCoords(dCoords, n, o.dim), n);
|
|
474
|
+
buildLap2(Dij, n);
|
|
475
|
+
|
|
476
|
+
// Layout optimization
|
|
477
|
+
const cMajEnv = initConstrainedMajorization(Dij, n, layers.ordering, layers.levels, layers.numLevels);
|
|
478
|
+
return majorize(st, { cMajEnv, levelsGap: o.levelsGap, maxi: o.maxi, epsilon: o.epsilon }, dCoords);
|
|
479
|
+
}
|