@@ -86,6 +86,17 @@ pub(crate) struct Solver {
8686 sq_norms_update_helper : Vec < f64 > ,
8787 inv_basis_row_coeffs : SparseVec ,
8888 row_coeffs : ScatteredVec ,
89+
90+ /// Persisted branch-and-bound state for resuming after a time limit.
91+ pub ( crate ) bb_state : Option < BranchAndBoundState > ,
92+ }
93+
94+ #[ derive( Clone , Debug ) ]
95+ pub ( crate ) struct BranchAndBoundState {
96+ dfs_stack : Vec < Step > ,
97+ best_cost : f64 ,
98+ /// Whether `self` (the Solver) represents a valid best integer solution found so far.
99+ has_best : bool ,
89100}
90101
91102#[ derive( Clone , Debug ) ]
@@ -407,6 +418,7 @@ impl Solver {
407418 sq_norms_update_helper,
408419 inv_basis_row_coeffs : SparseVec :: new ( ) ,
409420 row_coeffs : ScatteredVec :: empty ( num_total_vars - num_constraints) ,
421+ bb_state : None ,
410422 } ;
411423
412424 debug ! (
@@ -556,34 +568,65 @@ impl Solver {
556568 cur_solution : Solution ,
557569 direction : OptimizationDirection ,
558570 ) -> Result < StopReason , Error > {
559- let mut best_cost = if direction == OptimizationDirection :: Maximize {
560- f64:: NEG_INFINITY
561- } else {
562- f64:: INFINITY
563- } ;
564- let mut best_solution = None ;
565- debug ! ( "{:?}" , cur_solution. iter( ) . collect:: <Vec <_>>( ) ) ;
566- let mut dfs_stack =
567- if let Some ( var) = choose_branch_var ( & cur_solution, & self . orig_var_domains ) {
571+ // If we have persisted B&B state from a previous time-limited run, restore it.
572+ let ( mut dfs_stack, mut best_cost, mut best_solution) =
573+ if let Some ( state) = self . bb_state . take ( ) {
568574 debug ! (
569- "starting branch&bound, current obj. value: {:.2}" ,
570- self . cur_obj_val
575+ "resuming branch&bound, dfs_stack size: {}, best_cost: {:.2}, has_best: {}" ,
576+ state. dfs_stack. len( ) ,
577+ state. best_cost,
578+ state. has_best
571579 ) ;
572- new_steps ( cur_solution, var, & self . orig_var_domains )
580+ let best_solution = if state. has_best {
581+ // self was restored to the best solution's solver state before returning,
582+ // so cur_solution (built from self) represents the best solution.
583+ Some ( cur_solution)
584+ } else {
585+ None
586+ } ;
587+ ( state. dfs_stack , state. best_cost , best_solution)
573588 } else {
574- debug ! (
575- "found optimal solution with initial relaxation! cost: {:.2}" ,
576- self . cur_obj_val
577- ) ;
578- return Ok ( StopReason :: Finished ) ;
589+ // Fresh start
590+ let best_cost = if direction == OptimizationDirection :: Maximize {
591+ f64:: NEG_INFINITY
592+ } else {
593+ f64:: INFINITY
594+ } ;
595+ let best_solution = None ;
596+ debug ! ( "{:?}" , cur_solution. iter( ) . collect:: <Vec <_>>( ) ) ;
597+ let dfs_stack =
598+ if let Some ( var) = choose_branch_var ( & cur_solution, & self . orig_var_domains ) {
599+ debug ! (
600+ "starting branch&bound, current obj. value: {:.2}" ,
601+ self . cur_obj_val
602+ ) ;
603+ new_steps ( cur_solution, var, & self . orig_var_domains )
604+ } else {
605+ debug ! (
606+ "found optimal solution with initial relaxation! cost: {:.2}" ,
607+ self . cur_obj_val
608+ ) ;
609+ return Ok ( StopReason :: Finished ) ;
610+ } ;
611+ ( dfs_stack, best_cost, best_solution)
579612 } ;
580613
614+ // Cache orig_var_domains locally so the B&B loop never depends on self's
615+ // mutable state (self may be overwritten with the best solution's solver).
616+ let orig_var_domains = self . orig_var_domains . clone ( ) ;
617+
581618 for iter in 0 .. {
582619 if iter % 100 == 0 && check_deadline ( & self . deadline ) == StopReason :: Limit {
620+ let has_best = best_solution. is_some ( ) ;
583621 if let Some ( solution) = best_solution {
584622 let solution: Solution = solution;
585623 * self = solution. solver ;
586624 }
625+ self . bb_state = Some ( BranchAndBoundState {
626+ dfs_stack,
627+ best_cost,
628+ has_best,
629+ } ) ;
587630 return Ok ( StopReason :: Limit ) ;
588631 }
589632
@@ -593,6 +636,14 @@ impl Solver {
593636 None => break ,
594637 } ;
595638 let mut cur_solution = cur_step. start_solution . clone ( ) ;
639+ // Propagate the current deadline to the cloned solver. The step's
640+ // start_solution was captured earlier (possibly in a previous
641+ // time-limited run) and its deadline may have expired. Without this,
642+ // `add_constraint` / `fix_var` → `restore_feasibility()` would see
643+ // the stale deadline, return `Ok(StopReason::Limit)` immediately
644+ // without performing any dual-simplex pivots, and leave the solver
645+ // in a primal-infeasible state with garbage variable values.
646+ cur_solution. solver . deadline = self . deadline ;
596647 let branch_direction = match cur_step. kind {
597648 BranchKind :: Floor => ComparisonOp :: Le ,
598649 BranchKind :: Ceil => ComparisonOp :: Ge ,
@@ -630,9 +681,9 @@ impl Solver {
630681 continue ;
631682 }
632683
633- if let Some ( var) = choose_branch_var ( & cur_solution, & self . orig_var_domains ) {
684+ if let Some ( var) = choose_branch_var ( & cur_solution, & orig_var_domains) {
634685 // Search deeper
635- let steps = new_steps ( cur_solution, var, & self . orig_var_domains ) ;
686+ let steps = new_steps ( cur_solution, var, & orig_var_domains) ;
636687 dfs_stack. extend ( steps) ;
637688 } else {
638689 // Found integral solution
0 commit comments