2121 GraphEdgeConnectivityResult ,
2222 GraphEulerianResult ,
2323 GraphGirthResult ,
24+ GraphIndependenceNumberObligation ,
25+ GraphIndependenceNumberRequest ,
2426 GraphIndependenceNumberResult ,
2527 GraphInvariantRequest ,
2628 GraphMaximumMatchingRequest ,
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
7182def _computed [
7283 ResultT : ContractModel ,
@@ -336,20 +347,14 @@ def _k_core_execute(
336347
337348def _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(
452457def _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
464466def _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+
489599def _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
547668BOUNDED_GRAPH_INVARIANT_CAPABILITIES = (
0 commit comments