graphwise 1.7.0 → 1.8.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 +81 -30
- package/dist/adjacency-map-B6wPtmaq.cjs +234 -0
- package/dist/adjacency-map-B6wPtmaq.cjs.map +1 -0
- package/dist/adjacency-map-D-Ul7V1r.js +229 -0
- package/dist/adjacency-map-D-Ul7V1r.js.map +1 -0
- package/dist/async/index.cjs +16 -0
- package/dist/async/index.js +3 -0
- package/dist/expansion/dfs-priority.d.ts +11 -0
- package/dist/expansion/dfs-priority.d.ts.map +1 -1
- package/dist/expansion/dome.d.ts +20 -0
- package/dist/expansion/dome.d.ts.map +1 -1
- package/dist/expansion/edge.d.ts +18 -0
- package/dist/expansion/edge.d.ts.map +1 -1
- package/dist/expansion/flux.d.ts +16 -0
- package/dist/expansion/flux.d.ts.map +1 -1
- package/dist/expansion/frontier-balanced.d.ts +11 -0
- package/dist/expansion/frontier-balanced.d.ts.map +1 -1
- package/dist/expansion/fuse.d.ts +16 -0
- package/dist/expansion/fuse.d.ts.map +1 -1
- package/dist/expansion/hae.d.ts +16 -0
- package/dist/expansion/hae.d.ts.map +1 -1
- package/dist/expansion/index.cjs +43 -0
- package/dist/expansion/index.js +2 -0
- package/dist/expansion/lace.d.ts +16 -0
- package/dist/expansion/lace.d.ts.map +1 -1
- package/dist/expansion/maze.d.ts +17 -0
- package/dist/expansion/maze.d.ts.map +1 -1
- package/dist/expansion/pipe.d.ts +16 -0
- package/dist/expansion/pipe.d.ts.map +1 -1
- package/dist/expansion/random-priority.d.ts +18 -0
- package/dist/expansion/random-priority.d.ts.map +1 -1
- package/dist/expansion/reach.d.ts +17 -0
- package/dist/expansion/reach.d.ts.map +1 -1
- package/dist/expansion/sage.d.ts +15 -0
- package/dist/expansion/sage.d.ts.map +1 -1
- package/dist/expansion/sift.d.ts +16 -0
- package/dist/expansion/sift.d.ts.map +1 -1
- package/dist/expansion/standard-bfs.d.ts +11 -0
- package/dist/expansion/standard-bfs.d.ts.map +1 -1
- package/dist/expansion/tide.d.ts +16 -0
- package/dist/expansion/tide.d.ts.map +1 -1
- package/dist/expansion/warp.d.ts +16 -0
- package/dist/expansion/warp.d.ts.map +1 -1
- package/dist/expansion-FkmEYlrQ.cjs +1949 -0
- package/dist/expansion-FkmEYlrQ.cjs.map +1 -0
- package/dist/expansion-sldRognt.js +1704 -0
- package/dist/expansion-sldRognt.js.map +1 -0
- package/dist/extraction/index.cjs +630 -0
- package/dist/extraction/index.cjs.map +1 -0
- package/dist/extraction/index.js +621 -0
- package/dist/extraction/index.js.map +1 -0
- package/dist/graph/index.cjs +2 -229
- package/dist/graph/index.js +1 -228
- package/dist/index/index.cjs +131 -3406
- package/dist/index/index.js +14 -3334
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/jaccard-Bmd1IEFO.cjs +50 -0
- package/dist/jaccard-Bmd1IEFO.cjs.map +1 -0
- package/dist/jaccard-Yddrtt5D.js +39 -0
- package/dist/jaccard-Yddrtt5D.js.map +1 -0
- package/dist/{kmeans-BIgSyGKu.cjs → kmeans-D3yX5QFs.cjs} +1 -1
- package/dist/{kmeans-BIgSyGKu.cjs.map → kmeans-D3yX5QFs.cjs.map} +1 -1
- package/dist/{kmeans-87ExSUNZ.js → kmeans-DVCe61Me.js} +1 -1
- package/dist/{kmeans-87ExSUNZ.js.map → kmeans-DVCe61Me.js.map} +1 -1
- package/dist/ops-4nmI-pwk.cjs +277 -0
- package/dist/ops-4nmI-pwk.cjs.map +1 -0
- package/dist/ops-Zsu4ecEG.js +212 -0
- package/dist/ops-Zsu4ecEG.js.map +1 -0
- package/dist/priority-queue-ChVLoG6T.cjs +148 -0
- package/dist/priority-queue-ChVLoG6T.cjs.map +1 -0
- package/dist/priority-queue-DqCuFTR8.js +143 -0
- package/dist/priority-queue-DqCuFTR8.js.map +1 -0
- package/dist/ranking/index.cjs +43 -0
- package/dist/ranking/index.js +4 -0
- package/dist/ranking/mi/adamic-adar.d.ts +8 -0
- package/dist/ranking/mi/adamic-adar.d.ts.map +1 -1
- package/dist/ranking/mi/adaptive.d.ts +8 -0
- package/dist/ranking/mi/adaptive.d.ts.map +1 -1
- package/dist/ranking/mi/cosine.d.ts +7 -0
- package/dist/ranking/mi/cosine.d.ts.map +1 -1
- package/dist/ranking/mi/etch.d.ts +8 -0
- package/dist/ranking/mi/etch.d.ts.map +1 -1
- package/dist/ranking/mi/hub-promoted.d.ts +7 -0
- package/dist/ranking/mi/hub-promoted.d.ts.map +1 -1
- package/dist/ranking/mi/index.cjs +581 -0
- package/dist/ranking/mi/index.cjs.map +1 -0
- package/dist/ranking/mi/index.js +555 -0
- package/dist/ranking/mi/index.js.map +1 -0
- package/dist/ranking/mi/jaccard.d.ts +7 -0
- package/dist/ranking/mi/jaccard.d.ts.map +1 -1
- package/dist/ranking/mi/notch.d.ts +8 -0
- package/dist/ranking/mi/notch.d.ts.map +1 -1
- package/dist/ranking/mi/overlap-coefficient.d.ts +7 -0
- package/dist/ranking/mi/overlap-coefficient.d.ts.map +1 -1
- package/dist/ranking/mi/resource-allocation.d.ts +8 -0
- package/dist/ranking/mi/resource-allocation.d.ts.map +1 -1
- package/dist/ranking/mi/scale.d.ts +7 -0
- package/dist/ranking/mi/scale.d.ts.map +1 -1
- package/dist/ranking/mi/skew.d.ts +7 -0
- package/dist/ranking/mi/skew.d.ts.map +1 -1
- package/dist/ranking/mi/sorensen.d.ts +7 -0
- package/dist/ranking/mi/sorensen.d.ts.map +1 -1
- package/dist/ranking/mi/span.d.ts +8 -0
- package/dist/ranking/mi/span.d.ts.map +1 -1
- package/dist/ranking/mi/types.d.ts +12 -0
- package/dist/ranking/mi/types.d.ts.map +1 -1
- package/dist/ranking/parse.d.ts +24 -1
- package/dist/ranking/parse.d.ts.map +1 -1
- package/dist/ranking-mUm9rV-C.js +1016 -0
- package/dist/ranking-mUm9rV-C.js.map +1 -0
- package/dist/ranking-riRrEVAR.cjs +1093 -0
- package/dist/ranking-riRrEVAR.cjs.map +1 -0
- package/dist/seeds/index.cjs +1 -1
- package/dist/seeds/index.js +1 -1
- package/dist/structures/index.cjs +2 -143
- package/dist/structures/index.js +1 -142
- package/dist/utils/index.cjs +1 -1
- package/dist/utils/index.js +1 -1
- package/dist/utils-CcIrKAEb.js +22 -0
- package/dist/utils-CcIrKAEb.js.map +1 -0
- package/dist/utils-CpyzmzIF.cjs +33 -0
- package/dist/utils-CpyzmzIF.cjs.map +1 -0
- package/package.json +6 -1
- package/dist/graph/index.cjs.map +0 -1
- package/dist/graph/index.js.map +0 -1
- package/dist/index/index.cjs.map +0 -1
- package/dist/index/index.js.map +0 -1
- package/dist/structures/index.cjs.map +0 -1
- package/dist/structures/index.js.map +0 -1
package/README.md
CHANGED
|
@@ -14,6 +14,7 @@ Low-dependency TypeScript graph algorithms for citation network analysis: novel
|
|
|
14
14
|
- **Seed selection**: GRASP, Stratified
|
|
15
15
|
- **Subgraph extraction**: ego-network, k-core, k-truss, motif, induced, filter
|
|
16
16
|
- **Optional WebGPU acceleration**
|
|
17
|
+
- **Async support**: Generator coroutine protocol, sync/async runners, all algorithms available as `*Async` variants
|
|
17
18
|
|
|
18
19
|
## Installation
|
|
19
20
|
|
|
@@ -37,7 +38,7 @@ const ranked = parse(graph, result.paths, { mi: jaccard });
|
|
|
37
38
|
|
|
38
39
|
### Expansion: BASE Framework
|
|
39
40
|
|
|
40
|
-
**Boundary-free Adaptive Seeded Expansion** (BASE)
|
|
41
|
+
**Boundary-free Adaptive Seeded Expansion** (BASE) discovers the neighbourhood around seed nodes without any configuration. You provide seeds and a priority function; BASE expands outward, visiting the most interesting nodes first and recording paths when search frontiers from different seeds collide. It stops naturally when there is nothing left to explore — no depth limits, no size thresholds, no parameters to tune.
|
|
41
42
|
|
|
42
43
|
$$G_S = (V_S, E_S) \quad \text{where} \quad V_S = \bigcup_{v \in S} \text{Expand}(v, \pi)$$
|
|
43
44
|
|
|
@@ -51,7 +52,7 @@ Three key properties:
|
|
|
51
52
|
|
|
52
53
|
#### DOME: Degree-Ordered Multi-seed Expansion
|
|
53
54
|
|
|
54
|
-
|
|
55
|
+
Explores low-connectivity nodes before hubs. In a social network, DOME visits niche specialists before reaching the well-connected influencers, discovering the quiet corners of the graph before the busy crossroads.
|
|
55
56
|
|
|
56
57
|
$$\pi(v) = \frac{\deg^{+}(v) + \deg^{-}(v)}{w_V(v) + \varepsilon}$$
|
|
57
58
|
|
|
@@ -81,99 +82,117 @@ where $\deg^{+}(v)$ is weighted out-degree, $\deg^{-}(v)$ is weighted in-degree,
|
|
|
81
82
|
|
|
82
83
|
#### EDGE: Entropy-Driven Graph Expansion
|
|
83
84
|
|
|
85
|
+
Finds nodes that sit at the boundary between different kinds of things. If a person's friends include scientists, artists, and engineers (high type diversity), EDGE visits them early — they are likely bridges between communities.
|
|
86
|
+
|
|
84
87
|
$$\pi_{\text{EDGE}}(v) = \frac{1}{H_{\text{local}}(v) + \varepsilon} \times \log(\deg(v) + 1)$$
|
|
85
88
|
|
|
86
|
-
where $H_{\text{local}}(v) = -\sum_{\tau} p(\tau) \log p(\tau)$ is the Shannon entropy of the neighbour type distribution.
|
|
89
|
+
where $H_{\text{local}}(v) = -\sum_{\tau} p(\tau) \log p(\tau)$ is the Shannon entropy of the neighbour type distribution.
|
|
87
90
|
|
|
88
91
|
---
|
|
89
92
|
|
|
90
93
|
#### PIPE: Path-potential Informed Priority Expansion
|
|
91
94
|
|
|
95
|
+
Rushes towards nodes that are about to connect two search frontiers. When expanding from multiple seeds, PIPE detects that a node's neighbours have already been reached by another seed's frontier — meaning a connecting path is one step away.
|
|
96
|
+
|
|
92
97
|
$$\pi_{\text{PIPE}}(v) = \frac{\deg(v)}{1 + \mathrm{pathPotential}(v)}$$
|
|
93
98
|
|
|
94
|
-
where $\mathrm{pathPotential}(v) = \lvert N(v) \cap \bigcup_{j \neq i} V_j \rvert$ counts neighbours already visited by other seed frontiers.
|
|
99
|
+
where $\mathrm{pathPotential}(v) = \lvert N(v) \cap \bigcup_{j \neq i} V_j \rvert$ counts neighbours already visited by other seed frontiers.
|
|
95
100
|
|
|
96
101
|
---
|
|
97
102
|
|
|
98
103
|
#### SAGE: Salience-Accumulation Guided Expansion
|
|
99
104
|
|
|
105
|
+
Learns from its own discoveries. Phase 1 explores by degree (like DOME). Once the first path is found, SAGE switches to Phase 2: nodes that appear in many discovered paths get top priority, guiding expansion towards structurally rich regions.
|
|
106
|
+
|
|
100
107
|
$$
|
|
101
108
|
\pi_{\text{SAGE}}(v) = \begin{cases} \log(\deg(v) + 1) & \text{Phase 1 (before first path)} \\ -(\text{salience}(v) \times 1000 - \deg(v)) & \text{Phase 2 (after first path)} \end{cases}
|
|
102
109
|
$$
|
|
103
110
|
|
|
104
|
-
where $\text{salience}(v)$ counts discovered paths containing $v$.
|
|
111
|
+
where $\text{salience}(v)$ counts discovered paths containing $v$.
|
|
105
112
|
|
|
106
113
|
---
|
|
107
114
|
|
|
108
115
|
#### REACH: Retrospective Expansion with Adaptive Convergence
|
|
109
116
|
|
|
117
|
+
Uses the quality of already-discovered paths to steer future exploration. Phase 1 explores by degree. Once paths are found, REACH asks "which unexplored nodes look structurally similar to the endpoints of my best paths?" and prioritises those — seeking more of what already worked.
|
|
118
|
+
|
|
110
119
|
$$
|
|
111
120
|
\pi_{\text{REACH}}(v) = \begin{cases} \log(\deg(v) + 1) & \text{Phase 1} \\ \log(\deg(v) + 1) \times (1 - \widehat{\text{MI}}(v)) & \text{Phase 2} \end{cases}
|
|
112
121
|
$$
|
|
113
122
|
|
|
114
|
-
where $\widehat{\text{MI}}(v)$ estimates MI via Jaccard similarity to discovered path endpoints
|
|
115
|
-
|
|
116
|
-
$$
|
|
117
|
-
\widehat{\text{MI}}(v) = \frac{1}{\lvert \mathcal{P}\_{\text{top}} \rvert} \sum\_{p} J(N(v), N(p\_{\text{endpoint}}))
|
|
118
|
-
$$
|
|
123
|
+
where $\widehat{\text{MI}}(v) = \frac{1}{\lvert \mathcal{P}\_{\text{top}} \rvert} \sum\_{p} J(N(v), N(p\_{\text{endpoint}}))$ estimates MI via Jaccard similarity to discovered path endpoints.
|
|
119
124
|
|
|
120
125
|
---
|
|
121
126
|
|
|
122
127
|
#### MAZE: Multi-frontier Adaptive Zone Expansion
|
|
123
128
|
|
|
129
|
+
Combines the best of PIPE and SAGE across three phases. First, it races to find initial paths using path potential (like PIPE). Then it refines exploration using salience feedback (like SAGE). Finally, it decides when to stop based on whether it's still discovering diverse, high-quality paths.
|
|
130
|
+
|
|
124
131
|
$$
|
|
125
132
|
\pi^{(1)}(v) = \frac{\deg(v)}{1 + \mathrm{pathPotential}(v)} \qquad \pi^{(2)}(v) = \pi^{(1)}(v) \times \frac{1}{1 + \lambda \cdot \text{salience}(v)}
|
|
126
133
|
$$
|
|
127
134
|
|
|
128
|
-
Phase 1 uses
|
|
135
|
+
Phase 1 uses path potential until $M$ paths found. Phase 2 adds salience feedback. Phase 3 evaluates diversity, path count, and salience plateau for termination.
|
|
129
136
|
|
|
130
137
|
---
|
|
131
138
|
|
|
132
139
|
#### TIDE: Total Interconnected Degree Expansion
|
|
133
140
|
|
|
141
|
+
Avoids dense clusters by looking at total neighbourhood connectivity. A node surrounded by other well-connected nodes gets deferred; a node in a quiet corner of the graph gets explored first.
|
|
142
|
+
|
|
134
143
|
$$\pi_{\text{TIDE}}(v) = \deg(v) + \sum_{w \in N(v)} \deg(w)$$
|
|
135
144
|
|
|
136
|
-
|
|
145
|
+
Related to EDGE but uses raw degree sums rather than entropy.
|
|
137
146
|
|
|
138
147
|
---
|
|
139
148
|
|
|
140
149
|
#### LACE: Local Affinity-Computed Expansion
|
|
141
150
|
|
|
151
|
+
Explores towards nodes that are most similar to what the frontier has already seen. If a candidate node shares many neighbours with the explored region, it gets priority — building outward from a coherent core.
|
|
152
|
+
|
|
142
153
|
$$\pi_{\text{LACE}}(v) = 1 - \overline{\text{MI}}(v, \text{frontier})$$
|
|
143
154
|
|
|
144
|
-
|
|
155
|
+
Related to HAE but uses MI to visited nodes rather than type entropy.
|
|
145
156
|
|
|
146
157
|
---
|
|
147
158
|
|
|
148
159
|
#### WARP: Weighted Adjacent Reachability Priority
|
|
149
160
|
|
|
161
|
+
Aggressively prioritises nodes that look like they will connect two search frontiers, regardless of their degree. If a node's neighbours have been visited by another seed's search, it gets top priority.
|
|
162
|
+
|
|
150
163
|
$$\pi_{\text{WARP}}(v) = \frac{1}{1 + \text{bridge}(v)}$$
|
|
151
164
|
|
|
152
|
-
|
|
165
|
+
Related to PIPE but omits the degree numerator, making it more aggressive at prioritising bridge nodes.
|
|
153
166
|
|
|
154
167
|
---
|
|
155
168
|
|
|
156
169
|
#### FUSE: Fused Utility-Salience Expansion
|
|
157
170
|
|
|
171
|
+
Balances two signals simultaneously: how connected a node is (degree) and how strongly it relates to the explored region (MI). The weight $w$ controls the trade-off — at $w=0$ it behaves like DOME, at $w=1$ it behaves like LACE.
|
|
172
|
+
|
|
158
173
|
$$\pi_{\text{FUSE}}(v) = (1 - w) \cdot \deg(v) + w \cdot (1 - \overline{\text{MI}})$$
|
|
159
174
|
|
|
160
|
-
|
|
175
|
+
Related to SAGE but uses continuous blending rather than two-phase transition.
|
|
161
176
|
|
|
162
177
|
---
|
|
163
178
|
|
|
164
179
|
#### SIFT: Salience-Informed Frontier Threshold
|
|
165
180
|
|
|
181
|
+
Acts as a gate: nodes with MI above a threshold get MI-based priority (explore the promising ones); nodes below the threshold get deferred with a large degree-based penalty (ignore the unpromising ones). A binary version of REACH's continuous approach.
|
|
182
|
+
|
|
166
183
|
$$
|
|
167
184
|
\pi_{\text{SIFT}}(v) = \begin{cases} 1 - \overline{\text{MI}} & \text{if } \overline{\text{MI}} \geq \tau \\ \deg(v) + 100 & \text{otherwise} \end{cases}
|
|
168
185
|
$$
|
|
169
186
|
|
|
170
|
-
|
|
187
|
+
Related to REACH but uses a hard threshold instead of continuous MI-weighted priority.
|
|
171
188
|
|
|
172
189
|
---
|
|
173
190
|
|
|
174
191
|
#### FLUX: Flexible Local Utility Crossover
|
|
175
192
|
|
|
176
|
-
|
|
193
|
+
Adapts its strategy to the local topology of each node. In dense regions it uses low-degree-first exploration (like EDGE); near frontier boundaries it uses bridge detection (like PIPE); in sparse regions it falls back to degree ordering (like DOME). Different parts of the graph are explored with different strategies simultaneously.
|
|
194
|
+
|
|
195
|
+
Related to MAZE but adapts spatially (per-node) rather than temporally (per-phase).
|
|
177
196
|
|
|
178
197
|
---
|
|
179
198
|
|
|
@@ -192,73 +211,83 @@ Density-adaptive strategy switching. Selects between DOME, EDGE, and PIPE modes
|
|
|
192
211
|
|
|
193
212
|
### Path Ranking: PARSE
|
|
194
213
|
|
|
195
|
-
**Path Aggregation Ranked by Salience Estimation** (PARSE)
|
|
214
|
+
**Path Aggregation Ranked by Salience Estimation** (PARSE) ranks discovered paths by asking "how consistently strong is every edge along this path?" It uses the geometric mean of per-edge MI scores, which means one weak link drags down the entire path — unlike arithmetic mean where a strong edge can compensate for a weak one. A 10-hop path with consistently good edges scores the same as a 2-hop path with equally good edges.
|
|
196
215
|
|
|
197
216
|
$$M(P) = \exp\left( \frac{1}{k} \sum_{i=1}^{k} \log I(u_i, v_i) \right)$$
|
|
198
217
|
|
|
199
|
-
where $k$ is path length (number of edges) and $I(u_i, v_i)$ is the per-edge MI score from any variant below.
|
|
218
|
+
where $k$ is path length (number of edges) and $I(u_i, v_i)$ is the per-edge MI score from any variant below.
|
|
200
219
|
|
|
201
220
|
---
|
|
202
221
|
|
|
203
222
|
### MI Variants
|
|
204
223
|
|
|
205
|
-
|
|
224
|
+
MI variants answer the question "how strongly are two connected nodes related?" Each measures the overlap between their neighbourhoods, then optionally weights by structural properties like density, degree rarity, clustering, or entity type. PARSE uses these as per-edge scores in its geometric mean.
|
|
206
225
|
|
|
207
226
|
---
|
|
208
227
|
|
|
209
228
|
#### Jaccard (baseline)
|
|
210
229
|
|
|
211
|
-
|
|
230
|
+
What fraction of combined neighbours do two nodes share? If Alice and Bob know 3 of the same people out of 10 total acquaintances between them, their Jaccard score is 0.3.
|
|
212
231
|
|
|
213
|
-
|
|
232
|
+
$$I_{\text{Jac}}(u, v) = \frac{|N(u) \cap N(v)|}{|N(u) \cup N(v)|}$$
|
|
214
233
|
|
|
215
234
|
---
|
|
216
235
|
|
|
217
236
|
#### Adamic-Adar
|
|
218
237
|
|
|
219
|
-
|
|
238
|
+
Counts shared neighbours, but recognises that sharing a rare connection is more meaningful than sharing a popular one. If two researchers both cite a niche paper, that says more about their relationship than both citing a famous textbook.
|
|
220
239
|
|
|
221
|
-
|
|
240
|
+
$$I_{\text{AA}}(u, v) = \sum_{w \in N(u) \cap N(v)} \frac{1}{\log(\deg(w) + 1)}$$
|
|
222
241
|
|
|
223
242
|
---
|
|
224
243
|
|
|
225
244
|
#### SCALE: Structural Correction via Adjusted Local Estimation
|
|
226
245
|
|
|
246
|
+
Adjusts for graph density. In a dense network where everyone knows everyone, sharing neighbours is expected and less meaningful. In a sparse network, the same overlap is rare and significant. SCALE divides Jaccard by density to make scores comparable across differently-dense regions.
|
|
247
|
+
|
|
227
248
|
$$I_{\text{SCALE}}(u, v) = \frac{J(N(u), N(v))}{\rho(G)}$$
|
|
228
249
|
|
|
229
|
-
where $\rho(G) = \frac{2|E|}{|V|(|V|-1)}$ is graph density.
|
|
250
|
+
where $\rho(G) = \frac{2|E|}{|V|(|V|-1)}$ is graph density.
|
|
230
251
|
|
|
231
252
|
---
|
|
232
253
|
|
|
233
254
|
#### SKEW: Sparse-weighted Knowledge Emphasis Weighting
|
|
234
255
|
|
|
256
|
+
Rewards edges between rare (low-degree) nodes and penalises edges involving hubs. Like TF-IDF in search engines: a connection between two niche nodes is more informative than a connection between two mega-hubs that connect to everything.
|
|
257
|
+
|
|
235
258
|
$$I_{\text{SKEW}}(u, v) = J(N(u), N(v)) \cdot \log\!\left(\frac{N}{\deg(u) + 1}\right) \cdot \log\!\left(\frac{N}{\deg(v) + 1}\right)$$
|
|
236
259
|
|
|
237
|
-
where $N = |V|$.
|
|
260
|
+
where $N = |V|$.
|
|
238
261
|
|
|
239
262
|
---
|
|
240
263
|
|
|
241
264
|
#### SPAN: Spanning-community Penalty for Adjacent Nodes
|
|
242
265
|
|
|
266
|
+
Rewards edges that bridge separate communities and penalises edges within tight-knit groups. If both endpoints sit in dense clusters where everyone knows everyone (high clustering coefficient), the edge is probably redundant. If at least one endpoint is a bridge between groups, the edge is structurally interesting.
|
|
267
|
+
|
|
243
268
|
$$I_{\text{SPAN}}(u, v) = J(N(u), N(v)) \cdot \bigl(1 - \max(C(u), C(v))\bigr)$$
|
|
244
269
|
|
|
245
|
-
where $C(v)$ is the local clustering coefficient.
|
|
270
|
+
where $C(v)$ is the local clustering coefficient.
|
|
246
271
|
|
|
247
272
|
---
|
|
248
273
|
|
|
249
274
|
#### ETCH: Edge Type Contrast Heuristic
|
|
250
275
|
|
|
276
|
+
Boosts edges of rare types. If a graph has 1000 "knows" edges but only 5 "mentors" edges, a mentoring relationship is worth more than an acquaintanceship. ETCH multiplies Jaccard by the log-rarity of the edge type.
|
|
277
|
+
|
|
251
278
|
$$I_{\text{ETCH}}(u, v) = J(N(u), N(v)) \cdot \log\!\left(\frac{|E|}{\text{count}(\text{edges with type}(u,v))}\right)$$
|
|
252
279
|
|
|
253
|
-
|
|
280
|
+
Requires edge-type annotations; falls back to Jaccard when unavailable.
|
|
254
281
|
|
|
255
282
|
---
|
|
256
283
|
|
|
257
284
|
#### NOTCH: Node Type Contrast Heuristic
|
|
258
285
|
|
|
286
|
+
Boosts edges connecting rare node types. In a graph with 500 people but only 10 organisations, an edge involving an organisation is more distinctive. NOTCH multiplies Jaccard by the log-rarity of both endpoint types.
|
|
287
|
+
|
|
259
288
|
$$I_{\text{NOTCH}}(u, v) = J(N(u), N(v)) \cdot \log\!\left(\frac{|V|}{c(\tau_u)}\right) \cdot \log\!\left(\frac{|V|}{c(\tau_v)}\right)$$
|
|
260
289
|
|
|
261
|
-
where $c(\tau_u)$ is the count of nodes with the same type as $u$.
|
|
290
|
+
where $c(\tau_u)$ is the count of nodes with the same type as $u$.
|
|
262
291
|
|
|
263
292
|
---
|
|
264
293
|
|
|
@@ -296,7 +325,7 @@ where $c(\tau_u)$ is the count of nodes with the same type as $u$. Weights Jacca
|
|
|
296
325
|
|
|
297
326
|
### Seed Selection: GRASP
|
|
298
327
|
|
|
299
|
-
**Graph-agnostic Representative seed pAir Sampling
|
|
328
|
+
**Graph-agnostic Representative seed pAir Sampling** picks starting points for expansion algorithms. Given a graph you have never seen before, GRASP streams through its edges, samples a representative set of nodes, clusters them by structural role (hubs, bridges, peripherals), and returns seed pairs that cover the full range of structural diversity — without loading the entire graph into memory.
|
|
300
329
|
|
|
301
330
|
Three phases:
|
|
302
331
|
|
|
@@ -323,6 +352,28 @@ import { ... } from 'graphwise/extraction'; // Subgraph extraction
|
|
|
323
352
|
import { ... } from 'graphwise/utils'; // Utilities
|
|
324
353
|
import { ... } from 'graphwise/gpu'; // WebGPU acceleration
|
|
325
354
|
import { ... } from 'graphwise/schemas'; // Zod schemas
|
|
355
|
+
import { ... } from 'graphwise/async'; // Async runners & protocol
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
### Async Usage
|
|
359
|
+
|
|
360
|
+
All algorithms are available as `*Async` variants for use with remote or lazy graph data sources:
|
|
361
|
+
|
|
362
|
+
```typescript
|
|
363
|
+
import { domeAsync, parseAsync, jaccardAsync } from "graphwise";
|
|
364
|
+
import type { AsyncReadableGraph } from "graphwise/graph";
|
|
365
|
+
|
|
366
|
+
// Your async graph implementation
|
|
367
|
+
const remoteGraph: AsyncReadableGraph = createRemoteGraph();
|
|
368
|
+
|
|
369
|
+
const result = await domeAsync(remoteGraph, seeds, {
|
|
370
|
+
signal: controller.signal,
|
|
371
|
+
onProgress: (stats) => console.log(stats),
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
const ranked = await parseAsync(remoteGraph, result.paths, {
|
|
375
|
+
mi: jaccardAsync,
|
|
376
|
+
});
|
|
326
377
|
```
|
|
327
378
|
|
|
328
379
|
## Commands
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
//#region src/graph/adjacency-map.ts
|
|
2
|
+
/**
|
|
3
|
+
* Graph implementation using adjacency map data structure.
|
|
4
|
+
*
|
|
5
|
+
* Uses Map<NodeId, N> for node storage and Map<NodeId, Map<NodeId, E>>
|
|
6
|
+
* for adjacency representation. This provides O(1) average-case lookup
|
|
7
|
+
* for nodes and edges, with memory proportional to V + E.
|
|
8
|
+
*
|
|
9
|
+
* @typeParam N - Node data type, must extend NodeData
|
|
10
|
+
* @typeParam E - Edge data type, must extend EdgeData
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* // Create a directed citation graph
|
|
15
|
+
* const graph = AdjacencyMapGraph.directed<AuthorNode, CitationEdge>()
|
|
16
|
+
* .addNode({ id: 'A1', name: 'Alice' })
|
|
17
|
+
* .addNode({ id: 'B1', name: 'Bob' })
|
|
18
|
+
* .addEdge({ source: 'A1', target: 'B1', year: 2024 });
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
var AdjacencyMapGraph = class AdjacencyMapGraph {
|
|
22
|
+
directed;
|
|
23
|
+
nodes;
|
|
24
|
+
adjacency;
|
|
25
|
+
reverseAdjacency;
|
|
26
|
+
_edgeCount;
|
|
27
|
+
constructor(directed) {
|
|
28
|
+
this.directed = directed;
|
|
29
|
+
this.nodes = /* @__PURE__ */ new Map();
|
|
30
|
+
this.adjacency = /* @__PURE__ */ new Map();
|
|
31
|
+
this.reverseAdjacency = directed ? /* @__PURE__ */ new Map() : null;
|
|
32
|
+
this._edgeCount = 0;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Create a new directed graph.
|
|
36
|
+
*
|
|
37
|
+
* In a directed graph, edges have direction from source to target.
|
|
38
|
+
* The `neighbours` method with direction 'out' returns successors,
|
|
39
|
+
* and direction 'in' returns predecessors.
|
|
40
|
+
*
|
|
41
|
+
* @typeParam N - Node data type
|
|
42
|
+
* @typeParam E - Edge data type
|
|
43
|
+
* @returns A new empty directed graph
|
|
44
|
+
*/
|
|
45
|
+
static directed() {
|
|
46
|
+
return new AdjacencyMapGraph(true);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Create a new undirected graph.
|
|
50
|
+
*
|
|
51
|
+
* In an undirected graph, edges have no direction. Adding an edge
|
|
52
|
+
* from A to B automatically creates the connection from B to A.
|
|
53
|
+
*
|
|
54
|
+
* @typeParam N - Node data type
|
|
55
|
+
* @typeParam E - Edge data type
|
|
56
|
+
* @returns A new empty undirected graph
|
|
57
|
+
*/
|
|
58
|
+
static undirected() {
|
|
59
|
+
return new AdjacencyMapGraph(false);
|
|
60
|
+
}
|
|
61
|
+
get nodeCount() {
|
|
62
|
+
return this.nodes.size;
|
|
63
|
+
}
|
|
64
|
+
get edgeCount() {
|
|
65
|
+
return this._edgeCount;
|
|
66
|
+
}
|
|
67
|
+
hasNode(id) {
|
|
68
|
+
return this.nodes.has(id);
|
|
69
|
+
}
|
|
70
|
+
getNode(id) {
|
|
71
|
+
return this.nodes.get(id);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Iterate over all node identifiers in the graph.
|
|
75
|
+
*
|
|
76
|
+
* @returns An iterable of all node IDs
|
|
77
|
+
*/
|
|
78
|
+
*nodeIds() {
|
|
79
|
+
yield* this.nodes.keys();
|
|
80
|
+
}
|
|
81
|
+
neighbours(id, direction = "out") {
|
|
82
|
+
if (!this.nodes.has(id)) return [];
|
|
83
|
+
if (this.directed) {
|
|
84
|
+
if (direction === "out") return this.adjacency.get(id)?.keys() ?? [];
|
|
85
|
+
if (direction === "in") return this.reverseAdjacency?.get(id)?.keys() ?? [];
|
|
86
|
+
return this.iterateBothDirections(id);
|
|
87
|
+
}
|
|
88
|
+
return this.adjacency.get(id)?.keys() ?? [];
|
|
89
|
+
}
|
|
90
|
+
*iterateBothDirections(id) {
|
|
91
|
+
const seen = /* @__PURE__ */ new Set();
|
|
92
|
+
const outNeighbours = this.adjacency.get(id);
|
|
93
|
+
if (outNeighbours !== void 0) {
|
|
94
|
+
for (const neighbour of outNeighbours.keys()) if (!seen.has(neighbour)) {
|
|
95
|
+
seen.add(neighbour);
|
|
96
|
+
yield neighbour;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const inNeighbours = this.reverseAdjacency?.get(id);
|
|
100
|
+
if (inNeighbours !== void 0) {
|
|
101
|
+
for (const neighbour of inNeighbours.keys()) if (!seen.has(neighbour)) {
|
|
102
|
+
seen.add(neighbour);
|
|
103
|
+
yield neighbour;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
degree(id, direction = "out") {
|
|
108
|
+
if (!this.nodes.has(id)) return 0;
|
|
109
|
+
if (this.directed) {
|
|
110
|
+
if (direction === "out") return this.adjacency.get(id)?.size ?? 0;
|
|
111
|
+
if (direction === "in") return this.reverseAdjacency?.get(id)?.size ?? 0;
|
|
112
|
+
return (this.adjacency.get(id)?.size ?? 0) + (this.reverseAdjacency?.get(id)?.size ?? 0);
|
|
113
|
+
}
|
|
114
|
+
return this.adjacency.get(id)?.size ?? 0;
|
|
115
|
+
}
|
|
116
|
+
getEdge(source, target) {
|
|
117
|
+
const forward = this.adjacency.get(source)?.get(target);
|
|
118
|
+
if (forward !== void 0) return forward;
|
|
119
|
+
if (!this.directed) return this.adjacency.get(target)?.get(source);
|
|
120
|
+
}
|
|
121
|
+
*edges() {
|
|
122
|
+
const emitted = /* @__PURE__ */ new Set();
|
|
123
|
+
for (const [, neighbours] of this.adjacency) for (const [, edge] of neighbours) if (this.directed) yield edge;
|
|
124
|
+
else {
|
|
125
|
+
const key = this.edgeKey(edge.source, edge.target);
|
|
126
|
+
if (!emitted.has(key)) {
|
|
127
|
+
emitted.add(key);
|
|
128
|
+
yield edge;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
edgeKey(source, target) {
|
|
133
|
+
const [a, b] = source < target ? [source, target] : [target, source];
|
|
134
|
+
return `${a}::${b}`;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Add a node to the graph (builder pattern).
|
|
138
|
+
*
|
|
139
|
+
* If a node with the same ID already exists, it is not replaced.
|
|
140
|
+
*
|
|
141
|
+
* @param node - The node data to add
|
|
142
|
+
* @returns this (for method chaining)
|
|
143
|
+
*/
|
|
144
|
+
addNode(node) {
|
|
145
|
+
if (!this.nodes.has(node.id)) this.nodes.set(node.id, node);
|
|
146
|
+
return this;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Add an edge to the graph (builder pattern).
|
|
150
|
+
*
|
|
151
|
+
* @param edge - The edge data to add
|
|
152
|
+
* @returns this (for method chaining)
|
|
153
|
+
* @throws Error if either endpoint node does not exist
|
|
154
|
+
*/
|
|
155
|
+
addEdge(edge) {
|
|
156
|
+
if (!this.nodes.has(edge.source) || !this.nodes.has(edge.target)) throw new Error(`Cannot add edge: nodes ${edge.source} and/or ${edge.target} do not exist`);
|
|
157
|
+
if (!this.directed) {
|
|
158
|
+
const [cSource, cTarget] = edge.source < edge.target ? [edge.source, edge.target] : [edge.target, edge.source];
|
|
159
|
+
if (this.adjacency.get(cSource)?.get(cTarget) !== void 0) {
|
|
160
|
+
this.adjacency.get(cSource)?.set(cTarget, edge);
|
|
161
|
+
return this;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
let forwardMap = this.adjacency.get(edge.source);
|
|
165
|
+
if (forwardMap === void 0) {
|
|
166
|
+
forwardMap = /* @__PURE__ */ new Map();
|
|
167
|
+
this.adjacency.set(edge.source, forwardMap);
|
|
168
|
+
}
|
|
169
|
+
const isNewEdge = !forwardMap.has(edge.target);
|
|
170
|
+
forwardMap.set(edge.target, edge);
|
|
171
|
+
if (this.directed) {
|
|
172
|
+
let reverseMap = this.reverseAdjacency?.get(edge.target);
|
|
173
|
+
if (reverseMap === void 0) {
|
|
174
|
+
reverseMap = /* @__PURE__ */ new Map();
|
|
175
|
+
this.reverseAdjacency?.set(edge.target, reverseMap);
|
|
176
|
+
}
|
|
177
|
+
reverseMap.set(edge.source, edge);
|
|
178
|
+
} else {
|
|
179
|
+
let reverseMap = this.adjacency.get(edge.target);
|
|
180
|
+
if (reverseMap === void 0) {
|
|
181
|
+
reverseMap = /* @__PURE__ */ new Map();
|
|
182
|
+
this.adjacency.set(edge.target, reverseMap);
|
|
183
|
+
}
|
|
184
|
+
reverseMap.set(edge.source, edge);
|
|
185
|
+
}
|
|
186
|
+
if (isNewEdge) this._edgeCount++;
|
|
187
|
+
return this;
|
|
188
|
+
}
|
|
189
|
+
removeNode(id) {
|
|
190
|
+
if (!this.nodes.has(id)) return false;
|
|
191
|
+
const outNeighbours = [...this.adjacency.get(id)?.keys() ?? []];
|
|
192
|
+
for (const neighbour of outNeighbours) this.removeEdgeInternal(id, neighbour);
|
|
193
|
+
if (this.directed && this.reverseAdjacency !== null) {
|
|
194
|
+
const inNeighbours = [...this.reverseAdjacency.get(id)?.keys() ?? []];
|
|
195
|
+
for (const neighbour of inNeighbours) this.removeEdgeFromDirected(neighbour, id);
|
|
196
|
+
}
|
|
197
|
+
this.nodes.delete(id);
|
|
198
|
+
this.adjacency.delete(id);
|
|
199
|
+
this.reverseAdjacency?.delete(id);
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Remove an edge from a directed graph, updating both adjacency maps.
|
|
204
|
+
* This handles the case where we're removing an edge that points TO the removed node.
|
|
205
|
+
*/
|
|
206
|
+
removeEdgeFromDirected(source, target) {
|
|
207
|
+
if (this.adjacency.get(source)?.delete(target) === true) this._edgeCount--;
|
|
208
|
+
this.reverseAdjacency?.get(target)?.delete(source);
|
|
209
|
+
}
|
|
210
|
+
removeEdgeInternal(source, target) {
|
|
211
|
+
if (this.adjacency.get(source)?.delete(target) === true) this._edgeCount--;
|
|
212
|
+
if (this.directed) this.reverseAdjacency?.get(target)?.delete(source);
|
|
213
|
+
else this.adjacency.get(target)?.delete(source);
|
|
214
|
+
}
|
|
215
|
+
removeEdge(source, target) {
|
|
216
|
+
if (!this.hasEdgeInternal(source, target)) return false;
|
|
217
|
+
this.removeEdgeInternal(source, target);
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
hasEdgeInternal(source, target) {
|
|
221
|
+
if (this.adjacency.get(source)?.has(target) === true) return true;
|
|
222
|
+
if (!this.directed) return this.adjacency.get(target)?.has(source) === true;
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
//#endregion
|
|
227
|
+
Object.defineProperty(exports, "AdjacencyMapGraph", {
|
|
228
|
+
enumerable: true,
|
|
229
|
+
get: function() {
|
|
230
|
+
return AdjacencyMapGraph;
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
//# sourceMappingURL=adjacency-map-B6wPtmaq.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adjacency-map-B6wPtmaq.cjs","names":[],"sources":["../src/graph/adjacency-map.ts"],"sourcesContent":["/**\n * Adjacency map graph implementation.\n *\n * This module provides a flexible graph implementation using nested Maps\n * for efficient adjacency list representation. It supports both directed\n * and undirected graphs with builder pattern for convenient construction.\n */\n\nimport type { NodeId, NodeData, EdgeData, Direction } from \"./types\";\nimport type { MutableGraph } from \"./interfaces\";\n\n/**\n * Graph implementation using adjacency map data structure.\n *\n * Uses Map<NodeId, N> for node storage and Map<NodeId, Map<NodeId, E>>\n * for adjacency representation. This provides O(1) average-case lookup\n * for nodes and edges, with memory proportional to V + E.\n *\n * @typeParam N - Node data type, must extend NodeData\n * @typeParam E - Edge data type, must extend EdgeData\n *\n * @example\n * ```typescript\n * // Create a directed citation graph\n * const graph = AdjacencyMapGraph.directed<AuthorNode, CitationEdge>()\n * .addNode({ id: 'A1', name: 'Alice' })\n * .addNode({ id: 'B1', name: 'Bob' })\n * .addEdge({ source: 'A1', target: 'B1', year: 2024 });\n * ```\n */\nexport class AdjacencyMapGraph<\n\tN extends NodeData = NodeData,\n\tE extends EdgeData = EdgeData,\n> implements MutableGraph<N, E> {\n\treadonly directed: boolean;\n\n\tprivate readonly nodes: Map<NodeId, N>;\n\tprivate readonly adjacency: Map<NodeId, Map<NodeId, E>>;\n\tprivate readonly reverseAdjacency: Map<NodeId, Map<NodeId, E>> | null;\n\tprivate _edgeCount: number;\n\n\tprivate constructor(directed: boolean) {\n\t\tthis.directed = directed;\n\t\tthis.nodes = new Map();\n\t\tthis.adjacency = new Map();\n\t\tthis.reverseAdjacency = directed ? new Map() : null;\n\t\tthis._edgeCount = 0;\n\t}\n\n\t/**\n\t * Create a new directed graph.\n\t *\n\t * In a directed graph, edges have direction from source to target.\n\t * The `neighbours` method with direction 'out' returns successors,\n\t * and direction 'in' returns predecessors.\n\t *\n\t * @typeParam N - Node data type\n\t * @typeParam E - Edge data type\n\t * @returns A new empty directed graph\n\t */\n\tstatic directed<\n\t\tN extends NodeData = NodeData,\n\t\tE extends EdgeData = EdgeData,\n\t>(): AdjacencyMapGraph<N, E> {\n\t\treturn new AdjacencyMapGraph<N, E>(true);\n\t}\n\n\t/**\n\t * Create a new undirected graph.\n\t *\n\t * In an undirected graph, edges have no direction. Adding an edge\n\t * from A to B automatically creates the connection from B to A.\n\t *\n\t * @typeParam N - Node data type\n\t * @typeParam E - Edge data type\n\t * @returns A new empty undirected graph\n\t */\n\tstatic undirected<\n\t\tN extends NodeData = NodeData,\n\t\tE extends EdgeData = EdgeData,\n\t>(): AdjacencyMapGraph<N, E> {\n\t\treturn new AdjacencyMapGraph<N, E>(false);\n\t}\n\n\tget nodeCount(): number {\n\t\treturn this.nodes.size;\n\t}\n\n\tget edgeCount(): number {\n\t\treturn this._edgeCount;\n\t}\n\n\thasNode(id: NodeId): boolean {\n\t\treturn this.nodes.has(id);\n\t}\n\n\tgetNode(id: NodeId): N | undefined {\n\t\treturn this.nodes.get(id);\n\t}\n\n\t/**\n\t * Iterate over all node identifiers in the graph.\n\t *\n\t * @returns An iterable of all node IDs\n\t */\n\t*nodeIds(): Iterable<NodeId> {\n\t\tyield* this.nodes.keys();\n\t}\n\n\tneighbours(id: NodeId, direction: Direction = \"out\"): Iterable<NodeId> {\n\t\tif (!this.nodes.has(id)) {\n\t\t\treturn [];\n\t\t}\n\n\t\tif (this.directed) {\n\t\t\tif (direction === \"out\") {\n\t\t\t\treturn this.adjacency.get(id)?.keys() ?? [];\n\t\t\t}\n\t\t\tif (direction === \"in\") {\n\t\t\t\treturn this.reverseAdjacency?.get(id)?.keys() ?? [];\n\t\t\t}\n\t\t\t// direction === 'both'\n\t\t\treturn this.iterateBothDirections(id);\n\t\t}\n\n\t\t// Undirected: all neighbours are in adjacency\n\t\treturn this.adjacency.get(id)?.keys() ?? [];\n\t}\n\n\tprivate *iterateBothDirections(id: NodeId): Iterable<NodeId> {\n\t\tconst seen = new Set<NodeId>();\n\n\t\t// Yield outgoing neighbours\n\t\tconst outNeighbours = this.adjacency.get(id);\n\t\tif (outNeighbours !== undefined) {\n\t\t\tfor (const neighbour of outNeighbours.keys()) {\n\t\t\t\tif (!seen.has(neighbour)) {\n\t\t\t\t\tseen.add(neighbour);\n\t\t\t\t\tyield neighbour;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Yield incoming neighbours\n\t\tconst inNeighbours = this.reverseAdjacency?.get(id);\n\t\tif (inNeighbours !== undefined) {\n\t\t\tfor (const neighbour of inNeighbours.keys()) {\n\t\t\t\tif (!seen.has(neighbour)) {\n\t\t\t\t\tseen.add(neighbour);\n\t\t\t\t\tyield neighbour;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tdegree(id: NodeId, direction: Direction = \"out\"): number {\n\t\tif (!this.nodes.has(id)) {\n\t\t\treturn 0;\n\t\t}\n\n\t\tif (this.directed) {\n\t\t\tif (direction === \"out\") {\n\t\t\t\treturn this.adjacency.get(id)?.size ?? 0;\n\t\t\t}\n\t\t\tif (direction === \"in\") {\n\t\t\t\treturn this.reverseAdjacency?.get(id)?.size ?? 0;\n\t\t\t}\n\t\t\t// direction === 'both': count unique neighbours\n\t\t\tconst outSize = this.adjacency.get(id)?.size ?? 0;\n\t\t\tconst inSize = this.reverseAdjacency?.get(id)?.size ?? 0;\n\t\t\t// Simple sum is sufficient as edges are stored separately\n\t\t\treturn outSize + inSize;\n\t\t}\n\n\t\t// Undirected\n\t\treturn this.adjacency.get(id)?.size ?? 0;\n\t}\n\n\tgetEdge(source: NodeId, target: NodeId): E | undefined {\n\t\t// For undirected, try both orders\n\t\tconst forward = this.adjacency.get(source)?.get(target);\n\t\tif (forward !== undefined) {\n\t\t\treturn forward;\n\t\t}\n\n\t\tif (!this.directed) {\n\t\t\treturn this.adjacency.get(target)?.get(source);\n\t\t}\n\n\t\treturn undefined;\n\t}\n\n\t*edges(): Iterable<E> {\n\t\tconst emitted = new Set<string>();\n\n\t\tfor (const [, neighbours] of this.adjacency) {\n\t\t\tfor (const [, edge] of neighbours) {\n\t\t\t\tif (this.directed) {\n\t\t\t\t\tyield edge;\n\t\t\t\t} else {\n\t\t\t\t\t// For undirected, avoid emitting duplicate edges\n\t\t\t\t\tconst key = this.edgeKey(edge.source, edge.target);\n\t\t\t\t\tif (!emitted.has(key)) {\n\t\t\t\t\t\temitted.add(key);\n\t\t\t\t\t\tyield edge;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate edgeKey(source: NodeId, target: NodeId): string {\n\t\t// Create a canonical key for undirected edges\n\t\tconst [a, b] = source < target ? [source, target] : [target, source];\n\t\treturn `${a}::${b}`;\n\t}\n\n\t/**\n\t * Add a node to the graph (builder pattern).\n\t *\n\t * If a node with the same ID already exists, it is not replaced.\n\t *\n\t * @param node - The node data to add\n\t * @returns this (for method chaining)\n\t */\n\taddNode(node: N): this {\n\t\tif (!this.nodes.has(node.id)) {\n\t\t\tthis.nodes.set(node.id, node);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * Add an edge to the graph (builder pattern).\n\t *\n\t * @param edge - The edge data to add\n\t * @returns this (for method chaining)\n\t * @throws Error if either endpoint node does not exist\n\t */\n\taddEdge(edge: E): this {\n\t\t// Ensure both nodes exist\n\t\tif (!this.nodes.has(edge.source) || !this.nodes.has(edge.target)) {\n\t\t\tthrow new Error(\n\t\t\t\t`Cannot add edge: nodes ${edge.source} and/or ${edge.target} do not exist`,\n\t\t\t);\n\t\t}\n\n\t\tif (!this.directed) {\n\t\t\t// Canonical direction: source < target (prevents duplicate storage)\n\t\t\tconst [cSource, cTarget] =\n\t\t\t\tedge.source < edge.target\n\t\t\t\t\t? [edge.source, edge.target]\n\t\t\t\t\t: [edge.target, edge.source];\n\t\t\t// Check if edge already exists before incrementing edgeCount\n\t\t\tconst existingEdge = this.adjacency.get(cSource)?.get(cTarget);\n\t\t\tif (existingEdge !== undefined) {\n\t\t\t\t// Edge already exists — update data but don't increment count\n\t\t\t\tthis.adjacency.get(cSource)?.set(cTarget, edge);\n\t\t\t\treturn this;\n\t\t\t}\n\t\t}\n\n\t\t// Store in forward adjacency\n\t\tlet forwardMap = this.adjacency.get(edge.source);\n\t\tif (forwardMap === undefined) {\n\t\t\tforwardMap = new Map();\n\t\t\tthis.adjacency.set(edge.source, forwardMap);\n\t\t}\n\n\t\tconst isNewEdge = !forwardMap.has(edge.target);\n\t\tforwardMap.set(edge.target, edge);\n\n\t\tif (this.directed) {\n\t\t\t// Store reverse reference for efficient predecessor lookup\n\t\t\tlet reverseMap = this.reverseAdjacency?.get(edge.target);\n\t\t\tif (reverseMap === undefined) {\n\t\t\t\treverseMap = new Map();\n\t\t\t\tthis.reverseAdjacency?.set(edge.target, reverseMap);\n\t\t\t}\n\t\t\treverseMap.set(edge.source, edge);\n\t\t} else {\n\t\t\t// For undirected, also store in reverse direction\n\t\t\tlet reverseMap = this.adjacency.get(edge.target);\n\t\t\tif (reverseMap === undefined) {\n\t\t\t\treverseMap = new Map();\n\t\t\t\tthis.adjacency.set(edge.target, reverseMap);\n\t\t\t}\n\t\t\treverseMap.set(edge.source, edge);\n\t\t}\n\n\t\tif (isNewEdge) {\n\t\t\tthis._edgeCount++;\n\t\t}\n\t\treturn this;\n\t}\n\n\tremoveNode(id: NodeId): boolean {\n\t\tif (!this.nodes.has(id)) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// Remove all outgoing edges from this node\n\t\tconst outNeighbours = [...(this.adjacency.get(id)?.keys() ?? [])];\n\t\tfor (const neighbour of outNeighbours) {\n\t\t\tthis.removeEdgeInternal(id, neighbour);\n\t\t}\n\n\t\t// For directed graphs, also remove incoming edges to this node\n\t\tif (this.directed && this.reverseAdjacency !== null) {\n\t\t\tconst inNeighbours = [...(this.reverseAdjacency.get(id)?.keys() ?? [])];\n\t\t\tfor (const neighbour of inNeighbours) {\n\t\t\t\t// Remove the edge from neighbour -> id\n\t\t\t\tthis.removeEdgeFromDirected(neighbour, id);\n\t\t\t}\n\t\t}\n\n\t\t// Remove the node itself\n\t\tthis.nodes.delete(id);\n\t\tthis.adjacency.delete(id);\n\t\tthis.reverseAdjacency?.delete(id);\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Remove an edge from a directed graph, updating both adjacency maps.\n\t * This handles the case where we're removing an edge that points TO the removed node.\n\t */\n\tprivate removeEdgeFromDirected(source: NodeId, target: NodeId): void {\n\t\t// Remove from forward adjacency (source -> target)\n\t\tconst forwardMap = this.adjacency.get(source);\n\t\tif (forwardMap?.delete(target) === true) {\n\t\t\tthis._edgeCount--;\n\t\t}\n\n\t\t// Remove from reverse adjacency\n\t\tthis.reverseAdjacency?.get(target)?.delete(source);\n\t}\n\n\tprivate removeEdgeInternal(source: NodeId, target: NodeId): void {\n\t\t// Remove from forward adjacency\n\t\tconst forwardMap = this.adjacency.get(source);\n\t\tif (forwardMap?.delete(target) === true) {\n\t\t\tthis._edgeCount--;\n\t\t}\n\n\t\tif (this.directed) {\n\t\t\t// Remove from reverse adjacency\n\t\t\tthis.reverseAdjacency?.get(target)?.delete(source);\n\t\t} else {\n\t\t\t// For undirected, remove both directions\n\t\t\tthis.adjacency.get(target)?.delete(source);\n\t\t}\n\t}\n\n\tremoveEdge(source: NodeId, target: NodeId): boolean {\n\t\t// Check if edge exists\n\t\tif (!this.hasEdgeInternal(source, target)) {\n\t\t\treturn false;\n\t\t}\n\n\t\tthis.removeEdgeInternal(source, target);\n\t\treturn true;\n\t}\n\n\tprivate hasEdgeInternal(source: NodeId, target: NodeId): boolean {\n\t\tconst forward = this.adjacency.get(source)?.has(target) === true;\n\t\tif (forward) {\n\t\t\treturn true;\n\t\t}\n\n\t\tif (!this.directed) {\n\t\t\treturn this.adjacency.get(target)?.has(source) === true;\n\t\t}\n\n\t\treturn false;\n\t}\n}\n"],"mappings":"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"}
|