Skip to content

Commit 1a3fbfe

Browse files
Extend bounded independence number to order 128
1 parent 50ea545 commit 1a3fbfe

5 files changed

Lines changed: 301 additions & 42 deletions

File tree

docs/reference/capabilities/graphs/graph-invariant-batch.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ budgets, and in some cases use a different backend:
7878

7979
| Standalone invariant | Batch registry relationship |
8080
| --- | --- |
81-
| `graph.invariant.independence_number.compute` | Same mathematical value; different input and artifact contract. |
81+
| `graph.invariant.independence_number.compute` | Same mathematical value; a dedicated bounded Z3 optimization accepts inline graphs through order 128 and returns a witness plus an open optimality obligation. |
8282
| `graph.invariant.girth.compute` | Same invariant; the standalone operation uses `0` for acyclic graphs while the batch uses `null`. |
8383
| `graph.invariant.diameter.compute` | Same invariant; the standalone operation uses `-1` for disconnected graphs while the batch reports `NOT_APPLICABLE`. |
8484
| `graph.invariant.clique_number.compute` | Not in registry version `1`. |

src/jacobian/contracts/graph_invariant_operations.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,36 @@ class GraphMaximumMatchingRequest(ContractModel):
3030
graph: GraphMaximumMatchingGraph
3131

3232

33+
class GraphIndependenceNumberGraph(ChromaticGraph):
34+
"""A simple graph bounded for the dedicated independence-number search."""
35+
36+
vertices: tuple[GraphVertex, ...] = Field(max_length=128)
37+
edges: tuple[tuple[GraphVertex, GraphVertex], ...] = Field(max_length=8_128)
38+
39+
40+
class GraphIndependenceNumberBudget(ContractModel):
41+
"""Explicit wall-clock and order limits for independence-number search."""
42+
43+
wall_seconds: StrictInt = Field(default=5, ge=1, le=120)
44+
max_solver_calls: StrictInt = Field(default=1, ge=1, le=33)
45+
max_order: StrictInt = Field(default=128, ge=0, le=128)
46+
47+
48+
class GraphIndependenceNumberRequest(ContractModel):
49+
"""One bounded simple-graph independence-number request."""
50+
51+
graph: GraphIndependenceNumberGraph
52+
resource_budget: GraphIndependenceNumberBudget = Field(
53+
default_factory=GraphIndependenceNumberBudget
54+
)
55+
56+
@model_validator(mode="after")
57+
def enforce_order_budget(self) -> Self:
58+
if len(self.graph.vertices) > self.resource_budget.max_order:
59+
raise ValueError("graph order exceeds the declared max_order budget")
60+
return self
61+
62+
3363
GraphDistance = Annotated[StrictInt, Field(ge=0, le=31)] | None
3464
GraphDistanceRow = Annotated[
3565
tuple[GraphDistance, ...],
@@ -297,6 +327,12 @@ class GraphCliqueNumberResult(GraphCardinalityMaximumResult):
297327

298328

299329
class GraphIndependenceNumberResult(GraphCardinalityMaximumResult):
330+
order: StrictInt = Field(ge=0, le=128)
331+
optimum_value: StrictInt | None = Field(default=None, ge=0, le=128)
332+
incumbent_value: StrictInt = Field(ge=0, le=128)
333+
lower_bound: StrictInt = Field(ge=0, le=128)
334+
upper_bound: StrictInt = Field(ge=0, le=128)
335+
witness_vertices: tuple[GraphVertex, ...] = Field(max_length=128)
300336
convention: Literal["MAXIMUM_EDGE_FREE_VERTEX_SUBSET"] = (
301337
"MAXIMUM_EDGE_FREE_VERTEX_SUBSET"
302338
)
@@ -319,3 +355,23 @@ class GraphCardinalityMaximumObligation(ContractModel):
319355
Literal["WITNESS_FEASIBILITY", "MAXIMUM_CARDINALITY"],
320356
...,
321357
] = ("WITNESS_FEASIBILITY", "MAXIMUM_CARDINALITY")
358+
359+
360+
class GraphIndependenceNumberObligation(ContractModel):
361+
"""Open feasibility and optimality checks for one independence result."""
362+
363+
obligation_schema_version: Literal["1"] = "1"
364+
graph: GraphIndependenceNumberGraph
365+
predicate: Literal["GRAPH_INDEPENDENCE_NUMBER_OPTIMALITY"] = (
366+
"GRAPH_INDEPENDENCE_NUMBER_OPTIMALITY"
367+
)
368+
status: OptimizationStatus
369+
claimed_value: StrictInt | None = Field(default=None, ge=0, le=128)
370+
lower_bound: StrictInt = Field(ge=0, le=128)
371+
upper_bound: StrictInt = Field(ge=0, le=128)
372+
witness_vertices: tuple[GraphVertex, ...] = Field(max_length=128)
373+
tested: tuple[OptimizationSearchStep, ...]
374+
required_checks: tuple[
375+
Literal["WITNESS_FEASIBILITY", "MAXIMUM_CARDINALITY"],
376+
...,
377+
] = ("WITNESS_FEASIBILITY", "MAXIMUM_CARDINALITY")

src/jacobian/domains/graph_optimization/bundle.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ def build_graph_optimization_bundle() -> DomainBundle:
7474
"independence_number": "maximum edge-free vertex subset",
7575
},
7676
"timeout_or_cancellation": (
77-
"UNKNOWN partial result with preserved bounds and tested obligations"
77+
"UNKNOWN partial result with preserved bounds and search evidence"
7878
),
7979
"minimum_spanning_tree_certificate": (
8080
"every non-tree edge is no lighter than the maximum-weight "
@@ -139,9 +139,7 @@ def build_graph_optimization_bundle() -> DomainBundle:
139139
)
140140
),
141141
scope_description="one bounded simple undirected graph",
142-
completeness_basis=(
143-
"Z3 settled every stronger threshold needed to bind the reported optimum"
144-
),
142+
completeness_basis=("bounded Z3 search established the exact reported optimum"),
145143
assurance_basis=(
146144
"bounded Z3 computation with NetworkX witness predicates; an "
147145
"independent checker is still required for VERIFIED assurance"

src/jacobian/domains/graph_optimization/invariants.py

Lines changed: 158 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
GraphEdgeConnectivityResult,
2222
GraphEulerianResult,
2323
GraphGirthResult,
24+
GraphIndependenceNumberObligation,
25+
GraphIndependenceNumberRequest,
2426
GraphIndependenceNumberResult,
2527
GraphInvariantRequest,
2628
GraphMaximumMatchingRequest,
@@ -67,6 +69,15 @@
6769
hint="Supply a simple graph with at most 64 vertices and 2,016 edges.",
6870
)
6971

72+
_INVALID_INDEPENDENCE_NUMBER_REQUEST = CapabilityDiagnostic(
73+
code="INVALID_GRAPH_INDEPENDENCE_NUMBER_REQUEST",
74+
stage="graph_independence_number_input_validation",
75+
message=(
76+
"Input does not satisfy the bounded finite simple-graph independence contract."
77+
),
78+
hint="Supply a simple graph with at most 128 vertices and 8,128 edges.",
79+
)
80+
7081

7182
def _computed[
7283
ResultT: ContractModel,
@@ -336,20 +347,14 @@ def _k_core_execute(
336347

337348
def _maximum_cardinality(
338349
request: GraphOptimizationRequest,
339-
*,
340-
independent: bool,
341-
) -> GraphCliqueNumberResult | GraphIndependenceNumberResult:
350+
) -> GraphCliqueNumberResult:
342351
import networkx as nx
343352

344353
z3 = Z3_LOADER.get()
345-
source = cast(Any, build_simple_graph(request.graph))
346-
graph = nx.complement(source) if independent else source
354+
graph = cast(Any, build_simple_graph(request.graph))
347355
vertices = tuple(request.graph.vertices)
348-
result_model = (
349-
GraphIndependenceNumberResult if independent else GraphCliqueNumberResult
350-
)
351356
if not vertices:
352-
return result_model(
357+
return GraphCliqueNumberResult(
353358
status="EXACT",
354359
order=0,
355360
optimum_value=0,
@@ -435,7 +440,7 @@ def _maximum_cardinality(
435440
upper_bound = len(incumbent)
436441
exact = True
437442

438-
return result_model(
443+
return GraphCliqueNumberResult(
439444
status="EXACT" if exact else "UNKNOWN",
440445
order=len(vertices),
441446
optimum_value=len(incumbent) if exact else None,
@@ -452,24 +457,116 @@ def _maximum_cardinality(
452457
def _clique_execute(
453458
request: GraphOptimizationRequest,
454459
) -> BoundedSearchOutcome[GraphCliqueNumberResult]:
455-
result = cast(
456-
GraphCliqueNumberResult,
457-
_maximum_cardinality(request, independent=False),
458-
)
460+
result = _maximum_cardinality(request)
459461
if result.status == "EXACT":
460462
return BoundedSearchWitness(result)
461463
return BoundedSearchIncomplete(result)
462464

463465

464466
def _independence_execute(
465-
request: GraphOptimizationRequest,
467+
request: GraphIndependenceNumberRequest,
466468
) -> BoundedSearchOutcome[GraphIndependenceNumberResult]:
467-
result = cast(
468-
GraphIndependenceNumberResult,
469-
_maximum_cardinality(request, independent=True),
470-
)
471-
if result.status == "EXACT":
469+
import networkx as nx
470+
471+
z3 = Z3_LOADER.get()
472+
source = cast(Any, build_simple_graph(request.graph))
473+
vertices = tuple(request.graph.vertices)
474+
if not vertices:
475+
result = GraphIndependenceNumberResult(
476+
status="EXACT",
477+
order=0,
478+
optimum_value=0,
479+
incumbent_value=0,
480+
lower_bound=0,
481+
upper_bound=0,
482+
witness_vertices=(),
483+
tested=(),
484+
termination_reason="SPECIAL_CASE",
485+
detail="the empty graph has optimum zero",
486+
)
472487
return BoundedSearchWitness(result)
488+
489+
started = time.monotonic()
490+
complement = nx.complement(source)
491+
incumbent = tuple(sorted(nx.approximation.max_clique(complement)))
492+
remaining_ms = int(
493+
(request.resource_budget.wall_seconds - (time.monotonic() - started)) * 1000
494+
)
495+
if remaining_ms <= 0:
496+
result = GraphIndependenceNumberResult(
497+
status="UNKNOWN",
498+
order=len(vertices),
499+
optimum_value=None,
500+
incumbent_value=len(incumbent),
501+
lower_bound=len(incumbent),
502+
upper_bound=len(vertices),
503+
witness_vertices=incumbent,
504+
tested=(),
505+
termination_reason="WALL_TIME",
506+
detail="the wall-clock budget expired after the initial approximation",
507+
)
508+
return BoundedSearchIncomplete(result)
509+
510+
optimizer = z3.Optimize()
511+
optimizer.set(timeout=max(1, remaining_ms))
512+
selected = {
513+
vertex: z3.Bool(f"selected_{index}") for index, vertex in enumerate(vertices)
514+
}
515+
for left, right in sorted(source.edges):
516+
optimizer.add(z3.Or(z3.Not(selected[left]), z3.Not(selected[right])))
517+
objective = optimizer.maximize(
518+
z3.Sum([z3.If(selected[vertex], 1, 0) for vertex in vertices])
519+
)
520+
status = optimizer.check()
521+
if status == z3.sat:
522+
model = optimizer.model()
523+
optimized = tuple(
524+
sorted(
525+
vertex
526+
for vertex, variable in selected.items()
527+
if z3.is_true(model.eval(variable, model_completion=True))
528+
)
529+
)
530+
if len(optimized) > len(incumbent):
531+
incumbent = optimized
532+
lower = objective.lower()
533+
upper = objective.upper()
534+
if (
535+
z3.is_int_value(lower)
536+
and z3.is_int_value(upper)
537+
and lower.as_long() == upper.as_long() == len(incumbent)
538+
):
539+
result = GraphIndependenceNumberResult(
540+
status="EXACT",
541+
order=len(vertices),
542+
optimum_value=len(incumbent),
543+
incumbent_value=len(incumbent),
544+
lower_bound=len(incumbent),
545+
upper_bound=len(incumbent),
546+
witness_vertices=incumbent,
547+
tested=(),
548+
termination_reason="OPTIMUM_ESTABLISHED",
549+
detail=("bounded Z3 optimization seeded by a NetworkX approximation"),
550+
)
551+
return BoundedSearchWitness(result)
552+
553+
termination: OptimizationTermination = (
554+
"WALL_TIME"
555+
if time.monotonic() - started >= request.resource_budget.wall_seconds
556+
else "SOLVER_UNKNOWN"
557+
)
558+
result = GraphIndependenceNumberResult(
559+
status="UNKNOWN",
560+
order=len(vertices),
561+
optimum_value=None,
562+
incumbent_value=len(incumbent),
563+
lower_bound=len(incumbent),
564+
upper_bound=len(vertices),
565+
witness_vertices=incumbent,
566+
tested=(),
567+
termination_reason=termination,
568+
detail="bounded Z3 optimization did not establish an exact optimum",
569+
)
473570
return BoundedSearchIncomplete(result)
474571

475572

@@ -486,19 +583,41 @@ def _scope(
486583
}
487584

488585

586+
def _independence_scope(
587+
request: GraphIndependenceNumberRequest,
588+
result: GraphIndependenceNumberResult,
589+
) -> dict[str, object]:
590+
del result
591+
return {
592+
"order": len(request.graph.vertices),
593+
"wall_seconds": request.resource_budget.wall_seconds,
594+
"max_solver_calls": request.resource_budget.max_solver_calls,
595+
"max_order": request.resource_budget.max_order,
596+
}
597+
598+
489599
def _obligation(
490600
request: GraphOptimizationRequest,
491-
result: GraphCliqueNumberResult | GraphIndependenceNumberResult,
492-
*,
493-
independent: bool,
601+
result: GraphCliqueNumberResult,
494602
) -> GraphCardinalityMaximumObligation:
495603
return GraphCardinalityMaximumObligation(
496604
graph=request.graph,
497-
predicate=(
498-
"GRAPH_INDEPENDENCE_NUMBER_OPTIMALITY"
499-
if independent
500-
else "GRAPH_CLIQUE_NUMBER_OPTIMALITY"
501-
),
605+
predicate="GRAPH_CLIQUE_NUMBER_OPTIMALITY",
606+
status=result.status,
607+
claimed_value=result.optimum_value,
608+
lower_bound=result.lower_bound,
609+
upper_bound=result.upper_bound,
610+
witness_vertices=result.witness_vertices,
611+
tested=result.tested,
612+
)
613+
614+
615+
def _independence_obligation(
616+
request: GraphIndependenceNumberRequest,
617+
result: GraphIndependenceNumberResult,
618+
) -> GraphIndependenceNumberObligation:
619+
return GraphIndependenceNumberObligation(
620+
graph=request.graph,
502621
status=result.status,
503622
claimed_value=result.optimum_value,
504623
lower_bound=result.lower_bound,
@@ -519,8 +638,8 @@ def _obligation(
519638
scope_parameters=_scope,
520639
is_complete=lambda result: result.status == "EXACT",
521640
obligation_model=GraphCardinalityMaximumObligation,
522-
obligation=lambda request, result: _obligation(request, result, independent=False),
523-
incomplete_basis="the bounded threshold search did not establish optimality",
641+
obligation=_obligation,
642+
incomplete_basis="the bounded optimization did not establish optimality",
524643
tags=("graph", "invariant", "clique", "maximum", "bounded", "z3"),
525644
invalid_request=_INVALID_REQUEST,
526645
)
@@ -529,19 +648,21 @@ def _obligation(
529648
capability_id="graph.invariant.independence_number.compute",
530649
title="Independence number",
531650
description=(
532-
"Compute a maximum independent set under explicit finite search budgets."
651+
"Compute a maximum independent set through order 128 under explicit "
652+
"finite search budgets."
533653
),
534-
request_model=GraphOptimizationRequest,
654+
request_model=GraphIndependenceNumberRequest,
535655
result_model=GraphIndependenceNumberResult,
536656
implementation=_independence_execute,
537657
relation_id="graph.invariant.independence_number.relation",
538-
scope_parameters=_scope,
658+
scope_parameters=_independence_scope,
539659
is_complete=lambda result: result.status == "EXACT",
540-
obligation_model=GraphCardinalityMaximumObligation,
541-
obligation=lambda request, result: _obligation(request, result, independent=True),
542-
incomplete_basis="the bounded threshold search did not establish optimality",
660+
obligation_model=GraphIndependenceNumberObligation,
661+
obligation=_independence_obligation,
662+
incomplete_basis="the bounded optimization did not establish optimality",
543663
tags=("graph", "invariant", "independent-set", "maximum", "bounded", "z3"),
544-
invalid_request=_INVALID_REQUEST,
664+
invalid_request=_INVALID_INDEPENDENCE_NUMBER_REQUEST,
665+
version="2",
545666
)
546667

547668
BOUNDED_GRAPH_INVARIANT_CAPABILITIES = (

0 commit comments

Comments
 (0)