• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

graphty-org / graphty-element / 20514590651

26 Dec 2025 02:37AM UTC coverage: 70.559% (-0.3%) from 70.836%
20514590651

push

github

apowers313
ci: fix npm ci

9591 of 13363 branches covered (71.77%)

Branch coverage included in aggregate %.

25136 of 35854 relevant lines covered (70.11%)

6233.71 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.24
/src/algorithms/MinCutAlgorithm.ts
1
/**
2
 * @file Minimum Cut Algorithm wrapper
3
 *
4
 * This algorithm finds the minimum cut that separates a source from a sink
5
 * or the global minimum cut of a graph. Uses the max-flow min-cut theorem.
6
 */
7

8
import {Graph as AlgorithmGraph, kargerMinCut, minSTCut, stoerWagner} from "@graphty/algorithms";
15✔
9
import {z} from "zod/v4";
15✔
10

11
import {defineOptions, type OptionsSchema as ZodOptionsSchema, type SuggestedStylesConfig} from "../config";
15✔
12
import {Algorithm} from "./Algorithm";
15✔
13
import {type OptionsSchema} from "./types/OptionSchema";
14

15
/**
16
 * Zod-based options schema for Min Cut algorithm
17
 */
18
export const minCutOptionsSchema = defineOptions({
15✔
19
    source: {
15✔
20
        schema: z.union([z.string(), z.number()]).nullable().default(null),
15✔
21
        meta: {
15✔
22
            label: "Source Node",
15✔
23
            description: "Source node for s-t cut (uses global min cut if not set)",
15✔
24
        },
15✔
25
    },
15✔
26
    sink: {
15✔
27
        schema: z.union([z.string(), z.number()]).nullable().default(null),
15✔
28
        meta: {
15✔
29
            label: "Sink Node",
15✔
30
            description: "Sink node for s-t cut (uses global min cut if not set)",
15✔
31
        },
15✔
32
    },
15✔
33
    useGlobalMinCut: {
15✔
34
        schema: z.boolean().default(false),
15✔
35
        meta: {
15✔
36
            label: "Use Global Min Cut",
15✔
37
            description: "Use global minimum cut algorithm instead of s-t cut",
15✔
38
        },
15✔
39
    },
15✔
40
    useKarger: {
15✔
41
        schema: z.boolean().default(false),
15✔
42
        meta: {
15✔
43
            label: "Use Karger Algorithm",
15✔
44
            description: "Use Karger's randomized algorithm instead of Stoer-Wagner for global min cut",
15✔
45
            advanced: true,
15✔
46
        },
15✔
47
    },
15✔
48
    kargerIterations: {
15✔
49
        schema: z.number().int().min(1).max(10000).default(100),
15✔
50
        meta: {
15✔
51
            label: "Karger Iterations",
15✔
52
            description: "Number of iterations for Karger's algorithm (higher = better accuracy but slower)",
15✔
53
            advanced: true,
15✔
54
        },
15✔
55
    },
15✔
56
});
15✔
57

58
/**
59
 * Options for Min Cut algorithm
60
 */
61
export interface MinCutOptions extends Record<string, unknown> {
62
    /** Source node for s-t cut (optional - uses global min cut if not provided) */
63
    source: string | number | null;
64
    /** Sink node for s-t cut (optional - uses global min cut if not provided) */
65
    sink: string | number | null;
66
    /** Whether to use Stoer-Wagner global minimum cut instead of s-t cut */
67
    useGlobalMinCut: boolean;
68
    /** Whether to use Karger's randomized algorithm instead of Stoer-Wagner for global min cut */
69
    useKarger: boolean;
70
    /** Number of iterations for Karger's algorithm (higher = better accuracy) */
71
    kargerIterations: number;
72
}
73

74
/**
75
 * Minimum Cut algorithm using max-flow min-cut theorem
76
 *
77
 * Finds the minimum cut that separates a source from a sink (s-t cut)
78
 * or the global minimum cut of the graph using Stoer-Wagner or Karger's algorithm.
79
 */
80
export class MinCutAlgorithm extends Algorithm<MinCutOptions> {
15✔
81
    static namespace = "graphty";
13✔
82
    static type = "min-cut";
13✔
83

84
    static zodOptionsSchema: ZodOptionsSchema = minCutOptionsSchema;
13✔
85

86
    /**
87
     * Options schema for Min Cut algorithm
88
     */
89
    static optionsSchema: OptionsSchema = {
13✔
90
        source: {
13✔
91
            type: "nodeId",
13✔
92
            default: null,
13✔
93
            label: "Source Node",
13✔
94
            description: "Source node for s-t cut (uses global min cut if not set)",
13✔
95
            required: false,
13✔
96
        },
13✔
97
        sink: {
13✔
98
            type: "nodeId",
13✔
99
            default: null,
13✔
100
            label: "Sink Node",
13✔
101
            description: "Sink node for s-t cut (uses global min cut if not set)",
13✔
102
            required: false,
13✔
103
        },
13✔
104
        useGlobalMinCut: {
13✔
105
            type: "boolean",
13✔
106
            default: false,
13✔
107
            label: "Use Global Min Cut",
13✔
108
            description: "Use global minimum cut algorithm instead of s-t cut",
13✔
109
            required: false,
13✔
110
        },
13✔
111
        useKarger: {
13✔
112
            type: "boolean",
13✔
113
            default: false,
13✔
114
            label: "Use Karger Algorithm",
13✔
115
            description: "Use Karger's randomized algorithm instead of Stoer-Wagner for global min cut",
13✔
116
            advanced: true,
13✔
117
        },
13✔
118
        kargerIterations: {
13✔
119
            type: "integer",
13✔
120
            default: 100,
13✔
121
            label: "Karger Iterations",
13✔
122
            description: "Number of iterations for Karger's algorithm (higher = better accuracy but slower)",
13✔
123
            min: 1,
13✔
124
            max: 10000,
13✔
125
            advanced: true,
13✔
126
        },
13✔
127
    };
13✔
128

129
    /**
130
     * Legacy options set via configure() for backward compatibility
131
     */
132
    private legacyOptions: {source?: string, sink?: string, useGlobalMinCut?: boolean} | null = null;
13✔
133

134
    static suggestedStyles = (): SuggestedStylesConfig => ({
120✔
135
        layers: [
13✔
136
            {
13✔
137
                edge: {
13✔
138
                    selector: "",
13✔
139
                    style: {enabled: true},
13✔
140
                    calculatedStyle: {
13✔
141
                        inputs: ["algorithmResults.graphty.min-cut.inCut"],
13✔
142
                        output: "style.line.color",
13✔
143
                        expr: "{ return StyleHelpers.color.binary.orangeWarning(arguments[0]) }",
13✔
144
                    },
13✔
145
                },
13✔
146
                metadata: {
13✔
147
                    name: "Min Cut - Cut Edges",
13✔
148
                    description: "Highlights edges in the minimum cut (orange) - colorblind-safe",
13✔
149
                },
13✔
150
            },
13✔
151
            {
13✔
152
                edge: {
13✔
153
                    selector: "algorithmResults.graphty.\"min-cut\".inCut == `false`",
13✔
154
                    style: {
13✔
155
                        enabled: true,
13✔
156
                        line: {
13✔
157
                            opacity: 0.4,
13✔
158
                        },
13✔
159
                    },
13✔
160
                },
13✔
161
                metadata: {
13✔
162
                    name: "Min Cut - Non-Cut Edges",
13✔
163
                    description: "Dims edges not in the minimum cut",
13✔
164
                },
13✔
165
            },
13✔
166
            {
13✔
167
                node: {
13✔
168
                    selector: "",
13✔
169
                    style: {enabled: true},
13✔
170
                    calculatedStyle: {
13✔
171
                        inputs: ["algorithmResults.graphty.min-cut.partition"],
13✔
172
                        output: "style.texture.color",
13✔
173
                        expr: "{ return StyleHelpers.color.categorical.okabeIto(Number(arguments[0] ?? 1) - 1) }",
13✔
174
                    },
13✔
175
                },
13✔
176
                metadata: {
13✔
177
                    name: "Min Cut - Partition Colors",
13✔
178
                    description: "Colors nodes by partition - colorblind-safe",
13✔
179
                },
13✔
180
            },
13✔
181
        ],
13✔
182
        description: "Visualizes minimum cut edges and the resulting graph partition",
13✔
183
        category: "path",
13✔
184
    });
13✔
185

186
    /**
187
     * Configure the algorithm with source, sink, and useGlobalMinCut options
188
     * @param options - Configuration options
189
     * @param options.source - The source node for s-t cut (optional)
190
     * @param options.sink - The sink node for s-t cut (optional)
191
     * @param options.useGlobalMinCut - Whether to use global min cut (optional)
192
     * @returns This algorithm instance for chaining
193
     * @deprecated Use constructor options instead. This method is kept for backward compatibility.
194
     */
195
    configure(options: {source?: string, sink?: string, useGlobalMinCut?: boolean}): this {
19✔
196
        this.legacyOptions = options;
1✔
197
        return this;
1✔
198
    }
1✔
199

200
    /**
201
     * Executes the minimum cut algorithm on the graph
202
     *
203
     * Computes either s-t cut or global minimum cut and marks cut edges and partitions.
204
     */
205
    async run(): Promise<void> {
19✔
206
        const g = this.graph;
6✔
207
        const edges = Array.from(g.getDataManager().edges.values());
6✔
208
        const nodes = Array.from(g.getDataManager().nodes.values());
6✔
209

210
        if (edges.length === 0 || nodes.length === 0) {
6✔
211
            return;
1✔
212
        }
1✔
213

214
        // Build weighted graph from edges - Map format for stoerWagner/kargerMinCut
215
        const weightedGraphMap = new Map<string, Map<string, number>>();
5✔
216
        // Also build AlgorithmGraph for minSTCut (which requires Graph type)
217
        const weightedGraph = new AlgorithmGraph({directed: false});
5✔
218

219
        // Initialize nodes
220
        for (const node of nodes) {
6✔
221
            weightedGraphMap.set(String(node.id), new Map());
34✔
222
            weightedGraph.addNode(String(node.id));
34✔
223
        }
34✔
224

225
        // Add edges with weights
226
        for (const edge of edges) {
6✔
227
            const srcId = String(edge.srcId);
39✔
228
            const dstId = String(edge.dstId);
39✔
229

230
            // Get weight from edge data
231
            const edgeData = edge.data as Record<string, unknown> | undefined;
39✔
232
            const edgeObject = edge as unknown as Record<string, unknown>;
39✔
233
            const rawWeight = edgeData?.value ?? edgeObject.value ?? 1;
39!
234
            const weight: number = typeof rawWeight === "number" ? rawWeight : 1;
39!
235

236
            // Add to Map (for stoerWagner/kargerMinCut)
237
            const srcNeighbors = weightedGraphMap.get(srcId);
39✔
238
            if (srcNeighbors) {
39✔
239
                srcNeighbors.set(dstId, weight);
39✔
240
            }
39✔
241

242
            // Add reverse edge for undirected graph (Map)
243
            const dstNeighbors = weightedGraphMap.get(dstId);
39✔
244
            if (dstNeighbors) {
39✔
245
                dstNeighbors.set(srcId, weight);
39✔
246
            }
39✔
247

248
            // Add to AlgorithmGraph (for minSTCut)
249
            weightedGraph.addEdge(srcId, dstId, weight);
39✔
250
        }
39✔
251

252
        // Get options from legacy or schema options
253
        // Legacy configure() takes precedence for backward compatibility
254
        const useGlobalMinCut = this.legacyOptions?.useGlobalMinCut ?? this._schemaOptions.useGlobalMinCut;
6!
255
        const sourceOption = this.legacyOptions?.source ?? this._schemaOptions.source;
6!
256
        const sinkOption = this.legacyOptions?.sink ?? this._schemaOptions.sink;
6!
257
        const {useKarger, kargerIterations} = this._schemaOptions;
6✔
258

259
        // Determine which algorithm to use
260
        let partition1: Set<string>;
6✔
261
        let partition2: Set<string>;
6✔
262
        let cutEdges: {from: string, to: string, weight: number}[];
6✔
263
        let cutValue: number;
6✔
264

265
        if (useGlobalMinCut || (sourceOption === null && sinkOption === null)) {
6✔
266
            // Use global minimum cut algorithm
267
            if (useKarger) {
3!
268
                // Use Karger's randomized algorithm (accepts Map)
269
                const result = kargerMinCut(weightedGraphMap, kargerIterations);
×
270
                ({partition1, partition2, cutEdges, cutValue} = result);
×
271
            } else {
3✔
272
                // Use Stoer-Wagner for global minimum cut (accepts Map)
273
                const result = stoerWagner(weightedGraphMap);
3✔
274
                ({partition1, partition2, cutEdges, cutValue} = result);
3✔
275
            }
3✔
276
        } else {
4!
277
            // Use min s-t cut via max flow (requires AlgorithmGraph)
278
            const source = sourceOption !== null ? String(sourceOption) : String(Array.from(g.getDataManager().nodes.keys())[0]);
2!
279
            const sink = sinkOption !== null ? String(sinkOption) : String(Array.from(g.getDataManager().nodes.keys()).pop());
2!
280

281
            const result = minSTCut(weightedGraph, source, sink);
2✔
282
            ({partition1, partition2, cutEdges, cutValue} = result);
2✔
283
        }
2✔
284

285
        // Create set of cut edge keys for fast lookup
286
        const cutEdgeKeys = new Set<string>();
5✔
287
        for (const edge of cutEdges) {
6✔
288
            cutEdgeKeys.add(`${edge.from}:${edge.to}`);
6✔
289
            cutEdgeKeys.add(`${edge.to}:${edge.from}`);
6✔
290
        }
6✔
291

292
        // Store edge results
293
        for (const edge of edges) {
6✔
294
            const edgeKey = `${edge.srcId}:${edge.dstId}`;
39✔
295
            const inCut = cutEdgeKeys.has(edgeKey);
39✔
296

297
            // Find weight if in cut
298
            let weight = 0;
39✔
299
            if (inCut) {
39✔
300
                const cutEdge = cutEdges.find(
6✔
301
                    (ce) =>
6✔
302
                        (ce.from === String(edge.srcId) && ce.to === String(edge.dstId)) ||
7✔
303
                        (ce.to === String(edge.srcId) && ce.from === String(edge.dstId)),
4✔
304
                );
6✔
305
                weight = cutEdge?.weight ?? 0;
6!
306
            }
6✔
307

308
            this.addEdgeResult(edge, "inCut", inCut);
39✔
309
            this.addEdgeResult(edge, "cutWeight", weight);
39✔
310
        }
39✔
311

312
        // Store node results
313
        for (const node of nodes) {
6✔
314
            const nodeId = String(node.id);
34✔
315
            const isInPartition1 = partition1.has(nodeId);
34✔
316
            const partition = isInPartition1 ? "1" : "2";
34✔
317

318
            this.addNodeResult(node.id, "partition", partition);
34✔
319
            this.addNodeResult(node.id, "isInPartition1", isInPartition1);
34✔
320
            this.addNodeResult(node.id, "isInPartition2", partition2.has(nodeId));
34✔
321
        }
34✔
322

323
        // Store graph-level results
324
        this.addGraphResult("cutValue", cutValue);
5✔
325
        this.addGraphResult("cutEdgeCount", cutEdges.length);
5✔
326
        this.addGraphResult("partition1Size", partition1.size);
5✔
327
        this.addGraphResult("partition2Size", partition2.size);
5✔
328
        this.addGraphResult("partition1", Array.from(partition1));
5✔
329
        this.addGraphResult("partition2", Array.from(partition2));
5✔
330
    }
6✔
331
}
19✔
332

333
// Auto-register this algorithm when the module is imported
334
Algorithm.register(MinCutAlgorithm);
15✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc