Skip to content

Commit 7f49dc9

Browse files
committed
feat(validation): implement forbidden subgraph validation algorithms
Add real algorithm implementations for Priority 1 forbidden subgraph classes: Validators (src/validation/forbidden_subgraph.ts): - P5Free, C5Free, BullFree, GemFree (use hasInducedSubgraph) - ATFree (asteroidal triple detection with BFS neighborhood avoidance) - HHFree (house + hole detection with induced cycle checking) - DistanceHereditary (isometric cycle detection) Analyzers (src/analyzer/forbidden_subgraph.ts): - All 8 forbidden subgraph classes implemented - Converts AnalyzerGraph to adjacency format - Reuses helper functions from validators - WeaklyChordal includes complement anti-hole detection Helper algorithms (src/validation/forbidden-subgraph-helpers.ts): - hasAsteroidalTriple() - O(n³) triple enumeration - hasInducedCycle() - O(C(n,k)) induced cycle detection - isDistanceHereditary() - isometric cycle characterization - getCombinations() - combinatorial utility - canAvoidNeighborhood() - BFS pathfinding All algorithms use polynomial-time detection suitable for graphs up to ~50 vertices. Placeholder implementations replaced with actual graph-theoretic algorithms.
1 parent 2c23b5e commit 7f49dc9

File tree

3 files changed

+590
-114
lines changed

3 files changed

+590
-114
lines changed

src/analyzer/forbidden_subgraph.ts

Lines changed: 164 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,16 @@
77
* @generated 2026-01-18T16:10:41.825Z
88
*/
99

10+
import {
11+
buildAdjacencyList,
12+
hasInducedSubgraph,
13+
SUBGRAPH_PATTERNS
14+
} from "../algorithms/extraction/forbidden-subgraphs.js";
15+
import {
16+
hasAsteroidalTriple,
17+
hasInducedCycle as checkInducedCycle,
18+
isDistanceHereditary as checkDistanceHereditary
19+
} from "../validation/forbidden-subgraph-helpers.js";
1020
import type {
1121
ATFree,
1222
BullFree,
@@ -15,181 +25,233 @@ import type {
1525
GemFree,
1626
HHFree,
1727
P5Free,
18-
WeaklyChordal} from "../generation/spec/forbidden_subgraph.js";
28+
WeaklyChordal
29+
} from "../generation/spec/forbidden_subgraph.js";
1930
import type { AnalyzerGraph , ComputePolicy } from "./types.js";
2031

2132
/**
22-
* Compute P5Free property from graph structure.
23-
* Contains no induced path on 5 vertices
33+
* Convert AnalyzerGraph to adjacency list format.
2434
*
2535
* @param g - Analyzer graph
26-
* @param _policy - Computation policy (unused in generated analyzers)
27-
* @returns P5Free with computed value
36+
* @returns Tuple of (vertexSet, edgeList)
37+
*/
38+
const toAdjacencyFormat = (g: AnalyzerGraph): [Set<number>, Array<[number, number]>] => {
39+
// Map vertex IDs to numbers
40+
const nodeIdMap = new Map<string, number>();
41+
for (const [index, v] of g.vertices.entries()) {
42+
nodeIdMap.set(v.id, index);
43+
}
44+
45+
// Create vertex set and edge list
46+
const vertexSet = new Set<number>(g.vertices.map((_, index) => index));
47+
const edgeList: Array<[number, number]> = [];
48+
49+
for (const edge of g.edges) {
50+
if (edge.endpoints.length === 2) {
51+
const [srcId, tgtId] = edge.endpoints;
52+
const src = nodeIdMap.get(srcId);
53+
const tgt = nodeIdMap.get(tgtId);
54+
if (src !== undefined && tgt !== undefined) {
55+
edgeList.push([src, tgt]);
56+
}
57+
}
58+
}
59+
60+
return [vertexSet, edgeList];
61+
};
62+
63+
/**
64+
* Build mutable adjacency map from AnalyzerGraph.
65+
*
66+
* @param g - Analyzer graph
67+
* @returns Adjacency map
68+
*/
69+
const buildAdjacencyMap = (g: AnalyzerGraph): Map<number, Set<number>> => {
70+
const [vertexSet, edgeList] = toAdjacencyFormat(g);
71+
const adj = new Map<number, Set<number>>();
72+
73+
for (const v of vertexSet) {
74+
adj.set(v, new Set());
75+
}
76+
77+
for (const [u, v] of edgeList) {
78+
adj.get(u)?.add(v);
79+
adj.get(v)?.add(u);
80+
}
81+
82+
return adj;
83+
};
84+
85+
/**
86+
* Compute P5Free property from graph structure.
87+
* Contains no induced path on 5 vertices
2888
*/
2989
export const computeP5Free = (
3090
g: AnalyzerGraph,
3191
_policy: ComputePolicy
3292
): P5Free => {
33-
// TODO: Implement analysis logic for P5Free
34-
// For now, return placeholder value
35-
36-
// Example: Check for forbidden subgraph
37-
// const hasForbidden = checkForForbiddenSubgraph(g);
38-
// return hasForbidden ? { kind: "has_p5" } : { kind: "p5_free" };
39-
40-
return { kind: "p5_free" };
93+
const [vertexSet, edgeList] = toAdjacencyFormat(g);
94+
const adjacency = buildAdjacencyList(vertexSet, edgeList);
95+
const hasP5 = hasInducedSubgraph(adjacency, SUBGRAPH_PATTERNS.P5);
96+
return hasP5 ? { kind: "has_p5" } : { kind: "p5_free" };
4197
};
4298

4399
/**
44100
* Compute C5Free property from graph structure.
45101
* Contains no induced cycle on 5 vertices
46-
*
47-
* @param g - Analyzer graph
48-
* @param _policy - Computation policy (unused in generated analyzers)
49-
* @returns C5Free with computed value
50102
*/
51103
export const computeC5Free = (
52104
g: AnalyzerGraph,
53105
_policy: ComputePolicy
54106
): C5Free => {
55-
// TODO: Implement analysis logic for C5Free
56-
// For now, return placeholder value
57-
58-
// Example: Check for forbidden subgraph
59-
// const hasForbidden = checkForForbiddenSubgraph(g);
60-
// return hasForbidden ? { kind: "has_c5" } : { kind: "c5_free" };
61-
62-
return { kind: "c5_free" };
107+
const [vertexSet, edgeList] = toAdjacencyFormat(g);
108+
const adjacency = buildAdjacencyList(vertexSet, edgeList);
109+
const hasC5 = hasInducedSubgraph(adjacency, SUBGRAPH_PATTERNS.C5);
110+
return hasC5 ? { kind: "has_c5" } : { kind: "c5_free" };
63111
};
64112

65113
/**
66114
* Compute BullFree property from graph structure.
67115
* Contains no induced bull graph
68-
*
69-
* @param g - Analyzer graph
70-
* @param _policy - Computation policy (unused in generated analyzers)
71-
* @returns BullFree with computed value
72116
*/
73117
export const computeBullFree = (
74118
g: AnalyzerGraph,
75119
_policy: ComputePolicy
76120
): BullFree => {
77-
// TODO: Implement analysis logic for BullFree
78-
// For now, return placeholder value
79-
80-
// Example: Check for forbidden subgraph
81-
// const hasForbidden = checkForForbiddenSubgraph(g);
82-
// return hasForbidden ? { kind: "has_bull" } : { kind: "bull_free" };
83-
84-
return { kind: "bull_free" };
121+
const [vertexSet, edgeList] = toAdjacencyFormat(g);
122+
const adjacency = buildAdjacencyList(vertexSet, edgeList);
123+
const hasBull = hasInducedSubgraph(adjacency, SUBGRAPH_PATTERNS.bull);
124+
return hasBull ? { kind: "has_bull" } : { kind: "bull_free" };
85125
};
86126

87127
/**
88128
* Compute GemFree property from graph structure.
89129
* Contains no induced gem graph
90-
*
91-
* @param g - Analyzer graph
92-
* @param _policy - Computation policy (unused in generated analyzers)
93-
* @returns GemFree with computed value
94130
*/
95131
export const computeGemFree = (
96132
g: AnalyzerGraph,
97133
_policy: ComputePolicy
98134
): GemFree => {
99-
// TODO: Implement analysis logic for GemFree
100-
// For now, return placeholder value
101-
102-
// Example: Check for forbidden subgraph
103-
// const hasForbidden = checkForForbiddenSubgraph(g);
104-
// return hasForbidden ? { kind: "has_gem" } : { kind: "gem_free" };
105-
106-
return { kind: "gem_free" };
107-
};
108-
109-
/**
110-
* Compute WeaklyChordal property from graph structure.
111-
* No hole or antihole of length 5 or more
112-
*
113-
* @param g - Analyzer graph
114-
* @param _policy - Computation policy (unused in generated analyzers)
115-
* @returns WeaklyChordal with computed value
116-
*/
117-
export const computeWeaklyChordal = (
118-
g: AnalyzerGraph,
119-
_policy: ComputePolicy
120-
): WeaklyChordal => {
121-
// TODO: Implement analysis logic for WeaklyChordal
122-
// For now, return placeholder value
123-
124-
// Example: Check for forbidden subgraph
125-
// const hasForbidden = checkForForbiddenSubgraph(g);
126-
// return hasForbidden ? { kind: "has_hole_or_antihole" } : { kind: "weakly_chordal" };
127-
128-
return { kind: "weakly_chordal" };
135+
const [vertexSet, edgeList] = toAdjacencyFormat(g);
136+
const adjacency = buildAdjacencyList(vertexSet, edgeList);
137+
const hasGem = hasInducedSubgraph(adjacency, SUBGRAPH_PATTERNS.gem);
138+
return hasGem ? { kind: "has_gem" } : { kind: "gem_free" };
129139
};
130140

131141
/**
132142
* Compute ATFree property from graph structure.
133143
* No asteroidal triple of vertices
134-
*
135-
* @param g - Analyzer graph
136-
* @param _policy - Computation policy (unused in generated analyzers)
137-
* @returns ATFree with computed value
138144
*/
139145
export const computeATFree = (
140146
g: AnalyzerGraph,
141147
_policy: ComputePolicy
142148
): ATFree => {
143-
// TODO: Implement analysis logic for ATFree
144-
// For now, return placeholder value
149+
const adj = buildAdjacencyMap(g);
150+
const vertices = Array.from(adj.keys());
145151

146-
// Example: Check for forbidden subgraph
147-
// const hasForbidden = checkForForbiddenSubgraph(g);
148-
// return hasForbidden ? { kind: "has_asteroidal_triple" } : { kind: "at_free" };
152+
// Check all triples for asteroidal triples
153+
let hasAT = false;
154+
for (let i = 0; i < vertices.length && !hasAT; i++) {
155+
for (let j = i + 1; j < vertices.length && !hasAT; j++) {
156+
for (let k = j + 1; k < vertices.length && !hasAT; k++) {
157+
if (hasAsteroidalTriple(adj, vertices[i], vertices[j], vertices[k])) {
158+
hasAT = true;
159+
}
160+
}
161+
}
162+
}
149163

150-
return { kind: "at_free" };
164+
return hasAT ? { kind: "has_asteroidal_triple" } : { kind: "at_free" };
151165
};
152166

153167
/**
154168
* Compute HHFree property from graph structure.
155-
* No induced house or hole
156-
*
157-
* @param g - Analyzer graph
158-
* @param _policy - Computation policy (unused in generated analyzers)
159-
* @returns HHFree with computed value
169+
* No induced house or hole (cycle of length >= 5)
160170
*/
161171
export const computeHHFree = (
162172
g: AnalyzerGraph,
163173
_policy: ComputePolicy
164174
): HHFree => {
165-
// TODO: Implement analysis logic for HHFree
166-
// For now, return placeholder value
175+
const [vertexSet, edgeList] = toAdjacencyFormat(g);
176+
const adjacency = buildAdjacencyList(vertexSet, edgeList);
177+
const adj = buildAdjacencyMap(g);
178+
179+
// Check for house
180+
const hasHouse = hasInducedSubgraph(adjacency, SUBGRAPH_PATTERNS.house);
167181

168-
// Example: Check for forbidden subgraph
169-
// const hasForbidden = checkForForbiddenSubgraph(g);
170-
// return hasForbidden ? { kind: "has_house_or_hole" } : { kind: "hh_free" };
182+
// Check for holes (cycles of length >= 5)
183+
let hasHole = false;
184+
for (let cycleLength = 5; cycleLength <= g.vertices.length; cycleLength++) {
185+
if (checkInducedCycle(adj, vertexSet, cycleLength)) {
186+
hasHole = true;
187+
break;
188+
}
189+
}
171190

172-
return { kind: "hh_free" };
191+
return hasHouse || hasHole ? { kind: "has_house_or_hole" } : { kind: "hh_free" };
173192
};
174193

175194
/**
176195
* Compute DistanceHereditary property from graph structure.
177196
* Distances preserved in all connected induced subgraphs
178-
*
179-
* @param g - Analyzer graph
180-
* @param _policy - Computation policy (unused in generated analyzers)
181-
* @returns DistanceHereditary with computed value
182197
*/
183198
export const computeDistanceHereditary = (
184199
g: AnalyzerGraph,
185200
_policy: ComputePolicy
186201
): DistanceHereditary => {
187-
// TODO: Implement analysis logic for DistanceHereditary
188-
// For now, return placeholder value
202+
const adj = buildAdjacencyMap(g);
203+
const vertexSet = new Set(g.vertices.map((_, index) => index));
204+
const isDH = checkDistanceHereditary(adj, vertexSet);
205+
return isDH ? { kind: "distance_hereditary" } : { kind: "not_distance_hereditary" };
206+
};
207+
208+
/**
209+
* Compute WeaklyChordal property from graph structure.
210+
* No induced hole or anti-hole of length >= 5
211+
*/
212+
export const computeWeaklyChordal = (
213+
g: AnalyzerGraph,
214+
_policy: ComputePolicy
215+
): WeaklyChordal => {
216+
const [vertexSet, edgeList] = toAdjacencyFormat(g);
217+
const adj = buildAdjacencyMap(g);
218+
219+
// Check for holes (cycles of length >= 5)
220+
let hasHole = false;
221+
for (let cycleLength = 5; cycleLength <= g.vertices.length; cycleLength++) {
222+
if (checkInducedCycle(adj, vertexSet, cycleLength)) {
223+
hasHole = true;
224+
break;
225+
}
226+
}
227+
228+
// Check for antiholes (complement has hole)
229+
// Build complement adjacency
230+
const complementAdj = new Map<number, Set<number>>();
231+
for (const v of vertexSet) {
232+
complementAdj.set(v, new Set());
233+
}
234+
235+
const vertices = Array.from(vertexSet);
236+
for (let i = 0; i < vertices.length; i++) {
237+
for (let j = i + 1; j < vertices.length; j++) {
238+
const u = vertices[i];
239+
const v = vertices[j];
240+
// Add edge in complement if not adjacent in original
241+
if (!adj.get(u)?.has(v)) {
242+
complementAdj.get(u)?.add(v);
243+
complementAdj.get(v)?.add(u);
244+
}
245+
}
246+
}
189247

190-
// Example: Check for forbidden subgraph
191-
// const hasForbidden = checkForForbiddenSubgraph(g);
192-
// return hasForbidden ? { kind: "not_distance_hereditary" } : { kind: "distance_hereditary" };
248+
let hasAntihole = false;
249+
for (let cycleLength = 5; cycleLength <= g.vertices.length; cycleLength++) {
250+
if (checkInducedCycle(complementAdj, vertexSet, cycleLength)) {
251+
hasAntihole = true;
252+
break;
253+
}
254+
}
193255

194-
return { kind: "distance_hereditary" };
256+
return hasHole || hasAntihole ? { kind: "has_hole_or_antihole" } : { kind: "weakly_chordal" };
195257
};

0 commit comments

Comments
 (0)