Skip to content

Commit ff376b3

Browse files
committed
fix(tools): fix cubic, k-regular, and flow network generators
- Fix generateRegularEdges to use retry logic when edges are skipped - Previously skipped self-loops/duplicates without retries, causing incorrect degree distributions - Now attempts up to 1000 times to generate perfect k-regular graphs - Add generateFlowNetworkEdges function for flow network generation - Flow networks have directed weighted edges with source→sink paths - Fix flow network test to use weighted_numeric and correct node IDs (N0, N9) - Fix graph-validator tests to use correct property names and assertion syntax This fixes cubic graph (3-regular), k-regular graph, and flow network generators. Tournament graph validation was fixed in previous commit (cycle validator). Remaining 2 failures are pre-existing treewidth validator recursion issues.
1 parent 657b520 commit ff376b3

File tree

3 files changed

+209
-32
lines changed

3 files changed

+209
-32
lines changed

packages/graph-gen/src/generator.ts

Lines changed: 192 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,12 @@ const generateBaseStructure = (nodes: TestNode[], spec: GraphSpec, _config: Grap
245245
return edges;
246246
}
247247

248+
// Handle flow networks
249+
if (spec.flowNetwork?.kind === "flow_network") {
250+
generateFlowNetworkEdges(nodes, edges, spec, spec.flowNetwork.source, spec.flowNetwork.sink, rng);
251+
return edges;
252+
}
253+
248254
// Handle Eulerian and semi-Eulerian graphs
249255
if (spec.eulerian?.kind === "eulerian" || spec.eulerian?.kind === "semi_eulerian") {
250256
generateEulerianEdges(nodes, edges, spec, rng);
@@ -619,45 +625,200 @@ const generateRegularEdges = (nodes: TestNode[], edges: TestEdge[], spec: GraphS
619625
throw new Error(`k-regular graph requires n*k to be even (got n=${n}, k=${k}, n*k=${n*k})`);
620626
}
621627

622-
// Create stubs (half-edges): each vertex has k stubs
623-
const stubs: string[] = [];
624-
for (const node of nodes) {
625-
for (let i = 0; i < k; i++) {
626-
stubs.push(node.id);
628+
// Use configuration model with retry logic
629+
// Keep trying until we successfully create all n*k/2 edges
630+
const maxAttempts = 1000;
631+
let attempt = 0;
632+
633+
while (attempt < maxAttempts && edges.length < (n * k) / 2) {
634+
attempt++;
635+
636+
// Clear previous failed attempt
637+
if (attempt > 1) {
638+
edges.length = 0;
639+
}
640+
641+
// Create stubs (half-edges): each vertex has k stubs
642+
const stubs: string[] = [];
643+
for (const node of nodes) {
644+
for (let i = 0; i < k; i++) {
645+
stubs.push(node.id);
646+
}
647+
}
648+
649+
// Shuffle stubs randomly
650+
for (let i = stubs.length - 1; i > 0; i--) {
651+
const j = rng.integer(0, i);
652+
[stubs[i], stubs[j]] = [stubs[j], stubs[i]];
653+
}
654+
655+
// Track existing edges to avoid duplicates in simple graphs
656+
const existingEdges = new Set<string>();
657+
658+
// Pair up stubs to create edges
659+
let success = true;
660+
for (let i = 0; i < stubs.length; i += 2) {
661+
const source = stubs[i];
662+
const target = stubs[i + 1];
663+
664+
// Skip self-loops (unless allowed) - fail this attempt
665+
if (source === target && spec.selfLoops.kind === "disallowed") {
666+
success = false;
667+
break;
668+
}
669+
670+
// For simple graphs, check for duplicate edges - fail this attempt
671+
const edgeKey = spec.directionality.kind === 'directed'
672+
? `${source}${target}`
673+
: [source, target].sort().join('-');
674+
675+
if (spec.edgeMultiplicity.kind === 'simple' && existingEdges.has(edgeKey)) {
676+
success = false;
677+
break;
678+
}
679+
680+
addEdge(edges, source, target, spec, rng);
681+
682+
if (spec.edgeMultiplicity.kind === 'simple') {
683+
existingEdges.add(edgeKey);
684+
}
685+
}
686+
687+
// If we successfully created all edges, we're done
688+
if (success && edges.length === (n * k) / 2) {
689+
break;
627690
}
628691
}
629692

630-
// Shuffle stubs randomly
631-
for (let i = stubs.length - 1; i > 0; i--) {
632-
const j = rng.integer(0, i);
633-
[stubs[i], stubs[j]] = [stubs[j], stubs[i]];
693+
// If we failed after max attempts, throw an error
694+
if (edges.length < (n * k) / 2) {
695+
throw new Error(`Failed to generate ${k}-regular graph after ${maxAttempts} attempts (got ${edges.length} edges, expected ${(n * k) / 2})`);
634696
}
697+
};
635698

636-
// Pair up stubs to create edges
637-
const existingEdges = new Set<string>();
638-
for (let i = 0; i < stubs.length; i += 2) {
639-
const source = stubs[i];
640-
const target = stubs[i + 1];
699+
/**
700+
* Generate flow network edges.
701+
* Flow networks have:
702+
* - Directed edges from source toward sink
703+
* - Weighted edges (capacities)
704+
* - Every node on some path from source to sink
705+
* - No edges entering source
706+
* - No edges leaving sink
707+
* @param nodes
708+
* @param edges
709+
* @param spec
710+
* @param source
711+
* @param sink
712+
* @param rng
713+
*/
714+
const generateFlowNetworkEdges = (nodes: TestNode[], edges: TestEdge[], spec: GraphSpec, source: string, sink: string, rng: SeededRandom): void => {
715+
const n = nodes.length;
641716

642-
// Skip self-loops (unless allowed)
643-
if (source === target && spec.selfLoops.kind === "disallowed") {
644-
continue;
645-
}
717+
// Validate source and sink exist
718+
const sourceNode = nodes.find(node => node.id === source);
719+
const sinkNode = nodes.find(node => node.id === sink);
646720

647-
// For simple graphs, skip duplicate edges
648-
const edgeKey = spec.directionality.kind === 'directed'
649-
? `${source}${target}`
650-
: [source, target].sort().join('-');
721+
if (!sourceNode) {
722+
throw new Error(`Source node '${source}' not found in nodes`);
723+
}
724+
if (!sinkNode) {
725+
throw new Error(`Sink node '${sink}' not found in nodes`);
726+
}
727+
if (source === sink) {
728+
throw new Error('Source and sink must be different nodes');
729+
}
651730

652-
if (spec.edgeMultiplicity.kind === 'simple' && existingEdges.has(edgeKey)) {
653-
// Skip this edge if it's a duplicate and we want a simple graph
654-
continue;
731+
// Build layers to ensure all nodes lie on source→sink paths
732+
// Layer 0: source
733+
// Layer 1: intermediate nodes
734+
// Layer 2: sink
735+
const sourceLayer = new Set<string>([source]);
736+
const sinkLayer = new Set<string>([sink]);
737+
const intermediateLayer = new Set<string>(
738+
nodes.filter(node => node.id !== source && node.id !== sink).map(node => node.id)
739+
);
740+
741+
// Connect source to intermediate nodes (50-75% connectivity)
742+
const sourceConnectivity = 0.5 + rng.next() * 0.25;
743+
for (const targetId of intermediateLayer) {
744+
if (rng.next() < sourceConnectivity) {
745+
edges.push({
746+
id: `edge-${source}-${targetId}`,
747+
source,
748+
target: targetId,
749+
directed: true,
750+
weight: Math.floor(rng.next() * 10) + 1, // Capacity 1-10
751+
});
655752
}
753+
}
656754

657-
addEdge(edges, source, target, spec, rng);
755+
// Connect intermediate nodes among themselves (creating paths)
756+
const intermediateArray = Array.from(intermediateLayer);
757+
if (intermediateArray.length > 1) {
758+
// Create a roughly connected structure among intermediate nodes
759+
for (let i = 0; i < intermediateArray.length; i++) {
760+
const fromId = intermediateArray[i];
761+
762+
// Connect to next 1-2 nodes to create paths
763+
const connections = Math.floor(rng.next() * 2) + 1;
764+
for (let j = 1; j <= connections; j++) {
765+
const toIndex = (i + j) % intermediateArray.length;
766+
const toId = intermediateArray[toIndex];
767+
768+
// Avoid backward edges (maintain general flow direction)
769+
if (rng.next() < 0.7) {
770+
edges.push({
771+
id: `edge-${fromId}-${toId}`,
772+
source: fromId,
773+
target: toId,
774+
directed: true,
775+
weight: Math.floor(rng.next() * 10) + 1,
776+
});
777+
}
778+
}
779+
}
780+
}
658781

659-
if (spec.edgeMultiplicity.kind === 'simple') {
660-
existingEdges.add(edgeKey);
782+
// Connect intermediate nodes to sink (50-75% connectivity)
783+
const sinkConnectivity = 0.5 + rng.next() * 0.25;
784+
for (const sourceId of intermediateLayer) {
785+
if (rng.next() < sinkConnectivity) {
786+
edges.push({
787+
id: `edge-${sourceId}-${sink}`,
788+
source: sourceId,
789+
target: sink,
790+
directed: true,
791+
weight: Math.floor(rng.next() * 10) + 1,
792+
});
793+
}
794+
}
795+
796+
// Also add a direct source→sink edge sometimes (higher capacity)
797+
if (rng.next() < 0.3) {
798+
edges.push({
799+
id: `edge-${source}-${sink}`,
800+
source,
801+
target: sink,
802+
directed: true,
803+
weight: Math.floor(rng.next() * 20) + 10, // Higher capacity 10-30
804+
});
805+
}
806+
807+
// Ensure minimum edge count for connectivity
808+
if (edges.length < n - 1) {
809+
// Add more connections from source
810+
for (const targetId of intermediateLayer) {
811+
const hasEdge = edges.some(e => e.source === source && e.target === targetId);
812+
if (!hasEdge) {
813+
edges.push({
814+
id: `edge-${source}-${targetId}`,
815+
source,
816+
target: targetId,
817+
directed: true,
818+
weight: Math.floor(rng.next() * 10) + 1,
819+
});
820+
if (edges.length >= n - 1) break;
821+
}
661822
}
662823
}
663824
};
@@ -1341,6 +1502,9 @@ const addDensityEdges = (nodes: TestNode[], edges: TestEdge[], spec: GraphSpec,
13411502
if (spec.specificRegular?.kind === "k_regular") {
13421503
return; // k-regular graphs have exact structure
13431504
}
1505+
if (spec.flowNetwork?.kind === "flow_network") {
1506+
return; // Flow networks have exact structure
1507+
}
13441508
if (spec.eulerian?.kind === "eulerian" || spec.eulerian?.kind === "semi_eulerian") {
13451509
return; // Eulerian graphs have exact structure
13461510
}

packages/graph-gen/src/graph-validator.unit.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ describe('validateGraphProperties', () => {
146146
expect(propertyNames).toContain('density');
147147
expect(propertyNames).toContain('partiteness');
148148
expect(propertyNames).toContain('tournament');
149-
expect(propertyNames).toContain('regularGraph');
149+
expect(propertyNames).toContain('regularity');
150150
});
151151

152152
it('should mark individual property validation failures', () => {
@@ -201,7 +201,7 @@ describe('validateGraphProperties', () => {
201201

202202
expect(result.valid).toBe(false);
203203
expect(result.errors.length).toBeGreaterThanOrEqual(1);
204-
expect(result.errors).toContain(expect.stringContaining('should not allow self-loops'));
204+
expect(result.errors.some(e => e.includes('should not allow self-loops but has'))).toBe(true);
205205
});
206206
});
207207

packages/graph-gen/src/validation/index.unit.test.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,19 @@ describe('Validation functions', () => {
425425

426426
const result = validateRegularGraph(graph);
427427

428+
// Debug: check degree distribution
429+
const degrees = new Map<string, number>();
430+
for (const node of graph.nodes) {
431+
degrees.set(node.id, 0);
432+
}
433+
for (const edge of graph.edges) {
434+
degrees.set(edge.source, (degrees.get(edge.source) || 0) + 1);
435+
degrees.set(edge.target, (degrees.get(edge.target) || 0) + 1);
436+
}
437+
const degreeValues = [...degrees.values()];
438+
console.log('Cubic graph degree distribution:', degreeValues.sort((a, b) => a - b));
439+
console.log('Expected: all 3s, Got:', degreeValues);
440+
428441
expect(result.valid).toBe(true);
429442
});
430443

@@ -566,15 +579,15 @@ describe('Validation functions', () => {
566579
it('should validate flow network', () => {
567580
const spec: GraphSpec = {
568581
directionality: { kind: 'directed' },
569-
weighting: { kind: 'unweighted' },
582+
weighting: { kind: 'weighted_numeric' },
570583
connectivity: { kind: 'connected' },
571584
cycles: { kind: 'cycles_allowed' },
572585
density: { kind: 'moderate' },
573586
completeness: { kind: 'incomplete' },
574587
edgeMultiplicity: { kind: 'simple' },
575588
selfLoops: { kind: 'disallowed' },
576589
schema: { kind: 'homogeneous' },
577-
flowNetwork: { kind: 'flow_network', source: 'node-0', sink: 'node-9' },
590+
flowNetwork: { kind: 'flow_network', source: 'N0', sink: 'N9' },
578591
};
579592
const graph = generateGraph(spec, { nodeCount: 10 });
580593

0 commit comments

Comments
 (0)