Getting an infeasibility certificate from Gurobi when solving hard LPs

I guess something is wrong with the barrier algorithm, but I have no expertise to read its logging.
I log it here, don’t know if someone can have some explanations.
(The whole logging has no Warning.)

julia> JuMP.optimize!(cen)
Gurobi Optimizer version 12.0.3 build v12.0.3rc0 (linux64 - "Debian GNU/Linux 12 (bookworm)")

CPU model: AMD EPYC 7763 64-Core Processor, instruction set [SSE2|AVX|AVX2]
Thread count: 128 physical cores, 256 logical processors, using up to 4 threads

Non-default parameters:
Threads  4

Optimize a model with 5151162 rows, 7821552 columns and 23288280 nonzeros
Model fingerprint: 0x6babbe4f
Variable types: 4857600 continuous, 2963952 integer (2963952 binary)
Coefficient statistics:
  Matrix range     [9e-01, 2e+01]
  Objective range  [0e+00, 0e+00]
  Bounds range     [1e+00, 6e+01]
  RHS range        [1e+00, 2e+05]
Presolve removed 91387 rows and 1020414 columns (presolve time = 5s)...
...
Presolve removed 853207 rows and 1905781 columns (presolve time = 105s)...
Presolve removed 853338 rows and 1905783 columns
Presolve time: 109.72s
Presolved: 4297824 rows, 5915769 columns, 18494192 nonzeros
Variable types: 3402454 continuous, 2513315 integer (2513314 binary)
Deterministic concurrent LP optimizer: primal simplex, dual simplex, and barrier
Showing barrier log only...

Root barrier log...

Ordering time: 1.82s

Barrier statistics:
 AA' NZ     : 8.654e+06
 Factor NZ  : 4.399e+07 (roughly 3.0 GB of memory)
 Factor Ops : 1.189e+09 (less than 1 second per iteration)
 Threads    : 2

                  Objective                Residual
Iter       Primal          Dual         Primal    Dual     Compl     Time
   0   0.00000000e+00 -6.88706954e+05  1.41e+07 0.00e+00  6.01e+01   154s
   1   0.00000000e+00 -7.74201321e+05  3.40e+06 3.00e-02  1.48e+01   155s
   2   0.00000000e+00 -8.12851051e+05  4.65e+05 2.78e-16  2.10e+00   157s
   3   0.00000000e+00 -6.89035310e+05  4.16e+04 2.22e-16  2.35e-01   159s
   4   0.00000000e+00 -3.88446661e+05  1.59e+04 1.18e-14  8.72e-02   160s
   5   0.00000000e+00 -2.48343698e+05  9.24e+03 3.22e-15  4.80e-02   162s
   6   0.00000000e+00 -1.80267407e+05  6.23e+03 2.33e-15  3.20e-02   163s
   7   0.00000000e+00 -1.38219878e+05  5.04e+03 1.78e-15  2.43e-02   165s
   8   0.00000000e+00 -7.22861492e+04  3.97e+03 4.88e-15  1.46e-02   166s
   9   0.00000000e+00 -5.12928141e+04  2.76e+03 5.33e-15  1.01e-02   168s
  10   0.00000000e+00 -3.92082704e+04  2.19e+03 1.02e-14  7.81e-03   170s
  11   0.00000000e+00 -2.45448528e+04  2.03e+03 1.20e-14  6.05e-03   172s
  12   0.00000000e+00 -2.08076244e+04  1.74e+03 7.11e-15  5.18e-03   174s
  13   0.00000000e+00 -1.80383436e+04  1.55e+03 5.77e-15  4.61e-03   176s
  14   0.00000000e+00 -1.25464595e+04  1.41e+03 1.07e-14  3.87e-03   178s
  15   0.00000000e+00 -8.34815437e+03  1.28e+03 1.15e-14  3.27e-03   181s
  16   0.00000000e+00 -7.31037660e+03  1.10e+03 1.02e-14  2.87e-03   183s
  17   0.00000000e+00 -5.06462050e+03  1.02e+03 1.51e-14  2.58e-03   185s
  18   0.00000000e+00 -3.57104659e+03  9.62e+02 9.77e-15  2.43e-03   187s
  19   0.00000000e+00 -1.42493497e+03  9.04e+02 1.78e-14  2.22e-03   189s
  20   0.00000000e+00 -4.72630017e+02  8.44e+02 1.78e-14  2.06e-03   192s
  21   0.00000000e+00  7.56505394e+02  7.99e+02 1.51e-14  1.93e-03   194s
  22   0.00000000e+00  2.46575834e+03  7.26e+02 2.75e-14  1.71e-03   197s
  23   0.00000000e+00  3.42806308e+03  6.72e+02 2.49e-14  1.58e-03   199s
  24   0.00000000e+00  4.26795138e+03  6.34e+02 3.20e-14  1.48e-03   201s
  25   0.00000000e+00  4.97484852e+03  6.01e+02 3.02e-14  1.40e-03   203s
  26   0.00000000e+00  6.03125457e+03  5.62e+02 3.73e-14  1.30e-03   206s
  27   0.00000000e+00  6.89613681e+03  5.41e+02 3.38e-14  1.26e-03   208s
  28   0.00000000e+00  9.08357846e+03  5.10e+02 3.38e-14  1.20e-03   211s
  29   0.00000000e+00  1.12562526e+04  4.95e+02 4.26e-14  1.21e-03   213s
  30   0.00000000e+00  1.30323270e+04  4.69e+02 3.55e-14  1.14e-03   215s
  31   0.00000000e+00  1.49877607e+04  4.59e+02 3.20e-14  1.15e-03   218s
  32   0.00000000e+00  1.66252235e+04  4.39e+02 5.68e-14  1.08e-03   220s
  33   0.00000000e+00  1.86920993e+04  4.29e+02 5.68e-14  1.08e-03   222s
  34   0.00000000e+00  2.30207394e+04  4.18e+02 1.14e-13  1.11e-03   225s
  35   0.00000000e+00  2.52802063e+04  4.00e+02 5.68e-14  1.01e-03   227s
  36   0.00000000e+00  3.15050831e+04  3.90e+02 7.11e-14  1.04e-03   230s
  37   0.00000000e+00  3.84280799e+04  3.82e+02 1.49e-13  1.09e-03   232s
  38   0.00000000e+00  4.58503800e+04  3.73e+02 1.28e-13  1.08e-03   234s
  39   0.00000000e+00 -6.88706954e+09  1.97e+07 0.00e+00  8.40e+05   238s
  40   0.00000000e+00 -8.04808648e+09  3.18e+06 1.81e+02  1.41e+05   240s
  41   0.00000000e+00 -8.10176890e+09  6.04e+04 2.73e-12  3.40e+03   242s
  42   0.00000000e+00 -4.76383641e+09  2.42e+04 2.96e-12  1.27e+03   244s
  43   0.00000000e+00 -2.62423725e+09  1.41e+04 4.55e-12  6.24e+02   246s
  44   0.00000000e+00 -1.66236910e+09  8.44e+03 9.09e-12  3.52e+02   247s
  45   0.00000000e+00 -1.20713454e+09  5.85e+03 1.46e-11  2.38e+02   248s
  46   0.00000000e+00 -8.80730715e+08  4.48e+03 1.82e-11  1.72e+02   250s
  47   0.00000000e+00 -6.87515079e+08  3.59e+03 1.82e-11  1.33e+02   251s
  48   0.00000000e+00 -5.10906643e+08  3.08e+03 2.18e-11  1.05e+02   252s
  49   0.00000000e+00 -3.84044265e+08  2.60e+03 2.91e-11  8.31e+01   254s
  50   0.00000000e+00 -3.23626874e+08  2.22e+03 2.91e-11  7.04e+01   255s
  51   0.00000000e+00 -3.03355010e+08  2.13e+03 2.91e-11  6.69e+01   256s
  52   0.00000000e+00 -2.86691608e+08  2.00e+03 3.27e-11  6.32e+01   258s
  53   0.00000000e+00 -2.51195726e+08  1.89e+03 2.91e-11  5.80e+01   259s
  54   0.00000000e+00 -2.16106356e+08  1.71e+03 4.73e-11  5.17e+01   260s
  55   0.00000000e+00 -1.80371005e+08  1.62e+03 6.55e-11  4.71e+01   262s
  56   0.00000000e+00 -1.58371825e+08  1.55e+03 5.82e-11  4.40e+01   263s
  57   0.00000000e+00 -1.35700129e+08  1.42e+03 5.09e-11  4.03e+01   264s
  58   0.00000000e+00 -1.21859820e+08  1.26e+03 6.55e-11  3.62e+01   265s
  59   0.00000000e+00 -1.12457489e+08  1.19e+03 6.55e-11  3.42e+01   267s
  60   0.00000000e+00 -1.04386556e+08  1.15e+03 4.00e-11  3.30e+01   268s
  61   0.00000000e+00 -8.68128442e+07  1.10e+03 4.37e-11  3.08e+01   269s
  62   0.00000000e+00 -7.94649107e+07  1.02e+03 5.82e-11  2.89e+01   271s
  63   0.00000000e+00 -7.06142308e+07  9.96e+02 5.82e-11  2.79e+01   272s
  64   0.00000000e+00 -6.63949318e+07  9.32e+02 5.09e-11  2.63e+01   273s
  65   0.00000000e+00 -5.40806504e+07  9.11e+02 5.82e-11  2.54e+01   275s
  66   0.00000000e+00 -4.89147973e+07  8.68e+02 5.82e-11  2.42e+01   276s
  67   0.00000000e+00 -4.06393671e+07  8.48e+02 5.82e-11  2.35e+01   277s
  68   0.00000000e+00 -3.37421437e+07  8.26e+02 5.82e-11  2.28e+01   278s
  69   0.00000000e+00 -2.94863431e+07  8.03e+02 1.16e-10  2.22e+01   280s
  70   0.00000000e+00 -2.36628609e+07  7.78e+02 1.15e-10  2.15e+01   281s
  71   0.00000000e+00 -1.88220728e+07  7.54e+02 1.29e-10  2.08e+01   282s
  72   0.00000000e+00 -1.44814320e+07  7.39e+02 1.05e-10  2.03e+01   284s
  73   0.00000000e+00 -9.31725725e+06  7.24e+02 1.43e-10  1.99e+01   285s
  74   0.00000000e+00 -5.68047178e+06  6.87e+02 1.53e-10  1.89e+01   286s
  75   0.00000000e+00  2.10322457e+06  6.65e+02 5.82e-11  1.83e+01   288s
  76   0.00000000e+00  7.87957824e+06  6.50e+02 1.00e-10  1.78e+01   289s
  77   0.00000000e+00  1.30567848e+07  6.37e+02 1.83e-10  1.75e+01   290s
  78   0.00000000e+00  1.81921969e+07  6.24e+02 2.63e-10  1.71e+01   292s
  79   0.00000000e+00  2.09561408e+07  6.14e+02 2.58e-10  1.69e+01   293s
  80   0.00000000e+00  2.42527179e+07  5.95e+02 2.83e-10  1.63e+01   294s
  81   0.00000000e+00  2.98598050e+07  5.86e+02 2.86e-10  1.61e+01   296s
  82   0.00000000e+00  3.54788797e+07  5.74e+02 2.64e-10  1.58e+01   297s
  83   0.00000000e+00  4.02390755e+07  5.59e+02 2.74e-10  1.54e+01   298s
  84   0.00000000e+00  4.75369104e+07  5.44e+02 3.64e-10  1.49e+01   299s
  85   0.00000000e+00  5.25670770e+07  5.39e+02 4.38e-10  1.49e+01   301s
  86   0.00000000e+00  5.70028610e+07  5.33e+02 4.18e-10  1.48e+01   302s
  87   0.00000000e+00  6.10445019e+07  5.26e+02 2.46e-10  1.46e+01   304s
  88   0.00000000e+00  7.09836818e+07  5.08e+02 1.45e-10  1.41e+01   306s
  89   0.00000000e+00  7.75769769e+07  4.96e+02 2.33e-10  1.37e+01   308s
  90   0.00000000e+00  9.08558100e+07  4.80e+02 1.46e-10  1.33e+01   310s
  91   0.00000000e+00  9.70406808e+07  4.65e+02 2.33e-10  1.28e+01   312s
  92   0.00000000e+00  1.08753422e+08  4.61e+02 4.86e-10  1.28e+01   313s
  93   0.00000000e+00  1.52931036e+08  4.41e+02 2.04e-09  1.28e+01   315s
  94   0.00000000e+00  1.69695954e+08  4.37e+02 2.62e-10  1.29e+01   317s
  95   0.00000000e+00  1.76146247e+08  4.24e+02 4.66e-10  1.22e+01   319s
  96   0.00000000e+00  2.02481947e+08  4.14e+02 4.66e-10  1.21e+01   321s
  97   0.00000000e+00  2.13366422e+08  4.11e+02 3.49e-10  1.21e+01   322s
  98   0.00000000e+00  2.29811567e+08  4.04e+02 4.07e-10  1.19e+01   324s
  99   0.00000000e+00  2.43844289e+08  4.02e+02 6.22e-10  1.19e+01   326s
 100   0.00000000e+00  3.27790565e+08  3.93e+02 1.78e-09  1.28e+01   328s
 101   0.00000000e+00  3.69993973e+08  3.81e+02 9.90e-10  1.21e+01   330s
 102   0.00000000e+00  4.20712929e+08  3.78e+02 8.44e-10  1.25e+01   332s
 103   0.00000000e+00  5.24036772e+08  3.73e+02 1.62e-09  1.33e+01   334s
 104   0.00000000e+00  6.06543612e+08  3.68e+02 2.94e-09  1.37e+01   336s
 105   0.00000000e+00  6.74520640e+08  3.63e+02 1.44e-09  1.34e+01   337s
 106   0.00000000e+00  7.53023964e+08  3.58e+02 1.30e-09  1.34e+01   339s
 107   0.00000000e+00  8.35661448e+08  3.56e+02 2.59e-09  1.37e+01   341s
 108   0.00000000e+00  1.01821075e+09  3.52e+02 6.64e-09  1.42e+01   343s
 109   0.00000000e+00  1.16581807e+09  3.49e+02 3.29e-09  1.46e+01   345s
 110   0.00000000e+00  1.27476152e+09  3.47e+02 4.34e-09  1.50e+01   346s
 111   0.00000000e+00  1.45504153e+09  3.44e+02 6.02e-09  1.48e+01   348s
 112   0.00000000e+00  1.52807217e+09  3.42e+02 6.40e-09  1.45e+01   350s
 113   0.00000000e+00  1.59833957e+09  3.41e+02 5.63e-09  1.45e+01   352s
 114   0.00000000e+00  2.21649883e+09  3.38e+02 1.13e-08  1.64e+01   355s
 115   0.00000000e+00  2.51842907e+09  3.37e+02 1.04e-08  1.75e+01   358s
 116   0.00000000e+00  2.91072908e+09  3.36e+02 1.05e-08  1.85e+01   360s
 117   0.00000000e+00  3.24914272e+09  3.34e+02 9.92e-09  1.87e+01   362s
 118   0.00000000e+00  3.62219438e+09  3.34e+02 1.41e-08  2.01e+01   364s
 119   0.00000000e+00  3.82247550e+09  3.34e+02 1.28e-08  2.03e+01   366s

Barrier performed 119 iterations in 366.26 seconds (350.52 work units)
Numerical trouble encountered

Model may be infeasible or unbounded.  Consider using the
homogeneous algorithm (through parameter 'BarHomogeneous')

Waiting for other threads to finish...         431s
...
Waiting for other threads to finish...        2338s
^C # I interrupt here <<<

Root relaxation: interrupted, 1025366 iterations, 2250.67 seconds (985.75 work units)

    Nodes    |    Current Node    |     Objective Bounds      |     Work
 Expl Unexpl |  Obj  Depth IntInf | Incumbent    BestBd   Gap | It/Node Time

     0     0          -    0               -    0.00000      -     - 2377s

Explored 1 nodes (1025366 simplex iterations) in 2378.04 seconds (1093.66 work units)
Thread count was 4 (of 256 available processors)

Solution count 0

Solve interrupted
Best objective -, best bound 0.000000000000e+00, gap -

User-callback calls 1946707, time in user-callback 0.47 sec

julia> JuMP.solution_summary(cen)
solution_summary(; result = 1, verbose = false)
├ solver_name          : Gurobi
├ Termination
│ ├ termination_status : INTERRUPTED
│ ├ result_count       : 0
│ ├ raw_status         : Optimization was terminated by the user.
│ └ objective_bound    : 0.00000e+00
├ Solution (result = 1)
│ ├ primal_status        : NO_SOLUTION
│ ├ dual_status          : NO_SOLUTION
│ └ relative_gap         : 1.00000e+100
└ Work counters
  ├ solve_time (sec)   : 2.37810e+03
  ├ simplex_iterations : 1025366
  ├ barrier_iterations : 0
  └ node_count         : 1

As you can see, I performed a feasibility MIP (obj === 0).

Model may be infeasible or unbounded. Consider using the
homogeneous algorithm (through parameter ‘BarHomogeneous’)

If let me form an opinion:

  1. My problem has obj===0, so it cannot be unbounded.
  2. Why not let that parameter be an inner automatic setting, before the JuMP.optimize!() returns/interrupted by the user?:neutral_face:

Another important thing to note about JuMP:

  • From the logging we know that the root LP was not solved properly. So no information is valid after this optimize! call. So, to get any useful valid solution info, we need to at first check primal_status. (Yes, the objective_bound here is invalid, although it has value 0.0). Moreover, in this case it is advisable to relax_integrality and solve the LP relaxation: 1. if that LP relaxation is INFEASIBLE, the MIP is also INFEASIBLE. 2. if that LP relaxation is solved properly and yields a valid objective_bound, that bound is also valid for the original MIP, which can be recovered by undo().

Yes, I’ve made a meaningful discovery.

Sometimes the MIP is so hard that Gurobi cannot find an integer-feasible solution in reasonable time. In this case one might want to understand some info early:

  1. is there an infeasibility certificate?
  2. is there a valid dual bound?

In this situation, he can set NodeLimit to 1 and optimize (even without monitoring Logging, in other words, purely program controllable).
If the termination status is INFEASIBLE, then he gets the first point. If the termination status is NODE_LIMIT, then he can gets the second point by objective_bound.

The INFEASIBLE case is like

Non-default parameters:
NodeLimit  1
Threads  1

Root relaxation: infeasible, 145766 iterations, 33.75 seconds (34.53 work units)

    Nodes    |    Current Node    |     Objective Bounds      |     Work
 Expl Unexpl |  Obj  Depth IntInf | Incumbent    BestBd   Gap | It/Node Time

     0     0 infeasible    0               - infeasible      -     -   39s

Explored 1 nodes (145766 simplex iterations) in 39.13 seconds (40.32 work units)
Thread count was 1 (of 256 available processors)

Solution count 0

Model is infeasible
Best objective -, best bound -, gap -

User-callback calls 2191, time in user-callback 0.00 sec

julia> JuMP.termination_status(cen)
INFEASIBLE::TerminationStatusCode = 2

The NODE_LIMIT case is like:

Non-default parameters:
NodeLimit  1
Threads  4

Root relaxation: objective 8.716843e+03, 42566 iterations, 25.39 seconds (19.07 work units)
Total elapsed time = 51.79s (DegenMoves)

    Nodes    |    Current Node    |     Objective Bounds      |     Work
 Expl Unexpl |  Obj  Depth IntInf | Incumbent    BestBd   Gap | It/Node Time

     0     0 8716.84279    0 4006          - 8716.84279      -     -   57s
     0     2 8724.20860    0 4556          - 8724.20860      -     -  321s

Cutting planes:
  Learned: 1

Explored 1 nodes (180197 simplex iterations) in 321.60 seconds (294.38 work units)
Thread count was 4 (of 256 available processors)

Solution count 0

Node limit reached
Best objective -, best bound 8.724208602567e+03, gap -

User-callback calls 28641, time in user-callback 0.03 sec

julia> JuMP.solution_summary(cen)
solution_summary(; result = 1, verbose = false)
├ solver_name          : Gurobi
├ Termination
│ ├ termination_status : NODE_LIMIT
│ ├ result_count       : 0
│ ├ raw_status         : Optimization terminated because the total number of branch-and-cut nodes explored exceeded the value specified in the NodeLimit parameter.
│ └ objective_bound    : 8.72421e+03

Is there a question here? Or did you manage to find the answers?

Your model has numerical issues. It’s hard to say why without a reproducible example.

1 Like

It doesn’t have. It is just an “ambiguous in feasibility” feasibility system (with obj===0), which is almost feasible (or almost infeasible). So the barrier algorithm fails (but the simplex method is very slow…).

I’m not sure if I can upload the mps file (which is 917MB). (I probably don’t want to).

Maybe I can share the source code (but I do not guarantee that everyone can reproduce building this large model, since I have 256 threads on my machine which is rare. I built that model using multithreads).

src code
const (K, F, Krho, my_seed) = (64, 1, 3, 7)
import Random; Random.seed!(hash(my_seed));
import Gurobi; const GRB_ENV = Gurobi.Env();
import JuMP, Statistics
import JuMP.value as ı
scalar!(X, j, m, x) = (o = m.moi_backend; Gurobi.GRBgetdblattrelement(o, "X", Gurobi.c_column(o, JuMP.index(x[j])), view(X, j)));
value!(X, m, x) = foreach(j -> scalar!(X, j, m, x), eachindex(X));
get_simple_model() = (m = JuMP.direct_model(Gurobi.Optimizer(GRB_ENV)); JuMP.set_silent(m); JuMP.set_attribute(m, "Threads", 1); m);
const DEFAULT_THREADS, THREAD_FOR_MAIN_LOOP, THREAD_FOR_MASTER = Threads.nthreads(:default), 1, 1;
const THREAD_FOR_BLOCKS = DEFAULT_THREADS - THREAD_FOR_MAIN_LOOP - THREAD_FOR_MASTER;
const J, T, rho = (K)THREAD_FOR_BLOCKS, 24, 25/100 * Krho;
const LOG_TIME = 15; 
const mstsolvetime, pairblockrgap, selfblockrgap = Float64[], Float64[], Float64[];
ˈ96ˈ24(t) = (t-1)÷F+1;
abstract type Vertex end # TODO [need to add Qbus etc. to the vertices]
struct Vertexs <: Vertex
    bEV::Vector{Float64}
    bU::Matrix{Float64}
    pBus::Vector{Float64}
    Vertexs(U) = new(
        Vector{Float64}(undef, T),
        Matrix{Float64}(undef, T, U),
        Vector{Float64}(undef, T)
    )
end;
new_vertex(j) = j in Rng1 ? Vertexp(length(D[j].U_1), length(D[j].U_2)) : Vertexs(length(D[j].U));
fillvertex!(v::Vertex, j, X; inn = inn) = ((m, x) = (inn[j], X[j]); foreach(s -> value!(getproperty(v, s), m, getproperty(x, s)), propertynames(v)));
struct Vertexp <: Vertex
    bES::Vector{Float64}
    bLent::Vector{Float64}
    bEV_1::Vector{Float64}
    bEV_2::Vector{Float64}
    bU_1::Matrix{Float64}
    bU_2::Matrix{Float64}
    pBus::Vector{Float64}
    Vertexp(U1, U2) = new(
        Vector{Float64}(undef, T * F),
        Vector{Float64}(undef, T),
        Vector{Float64}(undef, T),
        Vector{Float64}(undef, T),
        Matrix{Float64}(undef, T, U1),
        Matrix{Float64}(undef, T, U2),
        Vector{Float64}(undef, T * F)
    )
end;
function get_C_and_O()
    C = F === 1 ? [
        [10, 9, 9, 9, 10, 12, 15, 18, 20, 18, 16, 15, 14, 15, 16, 18, 22, 28, 32, 30, 26, 20, 15, 12],# Case 1: Flat midday, strong evening peak
        [12, 11, 11, 12, 14, 18, 24, 26, 22, 18, 15, 14, 13, 14, 18, 24, 30, 34, 32, 28, 22, 18, 15, 13],# Case 2: Two peaks (morning + evening), midday dip
        [16, 15, 14, 14, 15, 18, 24, 30, 28, 22, 18, 12, 10, 12, 16, 22, 28, 34, 36, 32, 28, 24, 20, 18],# Case 3: Midday solar effect (cheapest at noon, peaks morning & evening)
        [8, 8, 8, 9, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 36, 34, 30, 26, 20, 14],# Case 4: Steady climb during day, single high plateau evening
        [5, 5, 5, 6, 8, 12, 18, 24, 28, 32, 36, 38, 36, 34, 30, 28, 26, 24, 22, 20, 18, 14, 10, 8]# Case 5: Inverted (very low off-peak overnight, high midday, gentle evening)
    ] : [
        [10, 10, 9, 9, 9, 10, 12, 14, 17, 19, 22, 25, 28, 30, 32, 33, 34, 34, 33, 31, 29, 26, 24, 22, 20, 19, 18, 18, 19, 20, 22, 25, 27, 29, 32, 34, 36, 37, 38, 38, 38, 37, 35, 33, 31, 28, 25, 23, 21, 19, 18, 17, 16, 15, 14, 13, 12, 12, 12, 12, 12, 13, 14, 16, 18, 20, 22, 24, 25, 26, 26, 25, 24, 22, 20, 18, 16, 15, 14, 13, 12, 11, 11, 10, 10, 10, 10, 9, 9, 9, 9, 9, 9, 9, 9, 10], # Morning–Evening Peaks
        [28, 29, 28, 27, 26, 25, 24, 22, 21, 19, 18, 17, 15, 14, 13, 13, 13, 13, 14, 15, 16, 18, 19, 21, 23, 25, 27, 28, 29, 29, 30, 31, 32, 33, 34, 34, 34, 34, 33, 32, 31, 30, 29, 28, 27, 26, 26, 26, 27, 28, 29, 30, 31, 31, 31, 31, 30, 29, 28, 27, 26, 25, 24, 24, 23, 22, 22, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 31, 32, 33, 33, 33, 33, 33, 32, 31, 30, 29, 29, 29, 29, 29, 29, 29, 29, 29], # Midday Solar Dip
        [28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 16, 15, 15, 15, 16, 17, 18, 19, 20, 22, 23, 24, 25, 25, 25, 25, 25, 24, 24, 24, 25, 26, 27, 28, 29, 30, 31, 31, 30, 29, 28, 27, 26, 25, 25, 25, 25, 26, 27, 28, 29, 29, 29, 28, 27, 26, 25, 24, 24, 24, 24, 25, 26, 27, 28, 28, 28, 27, 26, 25, 25, 25, 25, 25, 26, 27, 28, 29, 29, 29, 29, 28, 27, 26, 25, 25, 25, 25, 26, 27, 28, 29, 29], # Night Peak
        [14, 15, 13, 22, 30, 16, 17, 18, 14, 24, 26, 38, 12, 13, 27, 30, 13, 17, 28, 35, 20, 14, 18, 26, 16, 19, 15, 13, 30, 34, 12, 28, 14, 17, 35, 22, 12, 13, 25, 16, 19, 15, 18, 27, 32, 15, 13, 19, 21, 23, 24, 27, 31, 18, 12, 13, 15, 21, 33, 16, 19, 15, 12, 28, 34, 15, 18, 25, 12, 27, 26, 22, 14, 12, 13, 19, 29, 34, 38, 20, 12, 18, 13, 17, 26, 31, 14, 15, 19, 12, 13, 24, 34, 20, 15, 14], # Volatile / Spiky Day
        [8, 8, 8, 9, 9, 9, 10, 10, 10, 11, 11, 12, 12, 13, 13, 14, 15, 15, 16, 17, 18, 18, 19, 20, 21, 21, 22, 23, 23, 24, 25, 25, 26, 27, 27, 28, 29, 30, 30, 31, 31, 32, 33, 33, 34, 34, 35, 35, 36, 36, 36, 37, 37, 37, 37, 37, 38, 38, 38, 38, 38, 37, 37, 37, 37, 36, 36, 36, 35, 35, 34, 34, 33, 33, 32, 32, 31, 30, 30, 29, 28, 27, 27, 26, 26, 25, 25, 24, 24, 24, 24, 24, 23, 23, 22, 22] # Gradual Rising
    ]
    O = [
        [28,28,27,27,28,29,31,33,35,36,37,38,38,38,37,36,35,34,32,31,30,29,29,28],# Case 1: Typical hot day (peak ~38°C around 15:00)
        [29,28,28,28,29,31,33,35,37,39,40,41,42,42,41,40,38,36,34,33,32,31,30,29],# Case 2: Extremely hot day (peak ~42°C, late afternoon peak)
        [27,27,27,27,28,29,30,32,33,34,35,35,35,35,34,33,32,31,30,29,28,28,28,27],# Case 3: Milder hot day (peak ~35°C, smooth curve)
        [32,32,31,31,32,34,36,38,40,42,43,44,44,44,43,42,41,40,38,37,36,35,34,33],# Case 4: Heatwave night (doesn’t cool much at night, peak 44°C)
        [27,27,27,27,28,29,30,33,35,37,38,39,39,39,38,37,36,34,32,31,30,29,28,27]# Case 5: Cool morning, sharp rise, peak ~39°C
    ]
    C[rand(1:5)], O[rand(1:5)]
end;
get_pair_and_self_Rng(J) = (d = round(Int, rho * J); (1:d, d+1:J)); # Rng1, Rng2
prev(t, d, T) = (n = t-d; n<1 ? T+n : n);
pc_P_AC(O, OH, CND, Q_I, COP) = ceil(Int, ((maximum(O) - OH)CND + maximum(Q_I)) / COP);
function gen_ac_data()::Tuple
    CND   = .5rand(1:7) 
    INR   = rand(6:20)  
    COP   = rand(2:.5:4)
    Q_I   = rand(3:9, T)
    Q_BUS = rand(25:35) 
    OH    = rand(24:29) 
    OΔ    = rand(4:9)   
    P_AC  = pc_P_AC(O, OH, CND, Q_I, COP)
    CND, INR, COP, Q_I, Q_BUS, OH, OΔ, P_AC
end;
function add_AC_module!(model, O, CND, INR, COP, Q_I, Q_BUS, OH, OΔ, P_AC)
    pAC = JuMP.@variable(model, [1:T], lower_bound = 0, upper_bound = P_AC)
    o = JuMP.@variable(model, [1:T], lower_bound = OH-OΔ, upper_bound = OH)
    q = JuMP.@variable(model, [1:T], lower_bound = 0, upper_bound = Q_BUS)
    JuMP.@constraint(model, [t=1:T], (O[t]-o[t])CND + Q_I[t] -q[t] -pAC[t]COP == (o[t<T ? t+1 : 1]-o[t])INR)
    o, q, pAC
end;
function add_U_module!(model, U)
    bU::Matrix{JuMP.VariableRef} = JuMP.@variable(model, [t = 1:T, i = eachindex(U)], Bin)
    JuMP.@constraint(model, sum(bU; dims = 1) .≥ true)
    pU = JuMP.@expression(model, [t=1:T], sum(sum(bU[prev(t,φ-1,T), i]P for (φ,P) = enumerate(v)) for (i,v) = enumerate(U))) # Vector{JuMP.AffExpr}
    return bU, pU
end;
function add_self_EV_module!(model, P_EV, E_EV) # self means a household in a block with cardinality 1
    bEV, pEV = JuMP.@variable(model, [1:T], Bin), JuMP.@variable(model, [1:T])
    JuMP.@constraint(model, (P_EV.m)bEV .≤ pEV)
    JuMP.@constraint(model, pEV .≤ (P_EV.M)bEV)
    JuMP.@constraint(model, sum(pEV) ≥ E_EV)
    bEV, pEV # bEV can be _inferred from_ pEV
end;
function pc_self_P_BUS(D, U, P_EV, E_EV, O, CND, INR, COP, Q_I, OH, OΔ, P_AC)::Tuple{Bool, Int64}
    success, P_BUS = true, -1
    model = get_simple_model()
    bU, pU = add_U_module!(model, U)
    bEV, pEV = add_self_EV_module!(model, P_EV, E_EV)
    o, q, pAC = add_AC_module!(model, O, CND, INR, COP, Q_I, 0, OH, OΔ, P_AC) # Q_BUS = 0
    pBus = JuMP.@variable(model)
    JuMP.@constraint(model, pBus .≥ D + pU + pEV + pAC) # No G | ES
    JuMP.@objective(model, Min, pBus)
    JuMP.set_attribute(model, "TimeLimit", 15)
    JuMP.optimize!(model)
    ps, ts = JuMP.primal_status(model), JuMP.termination_status(model)
    ps === JuMP.FEASIBLE_POINT || return false, P_BUS
    (ts === JuMP.OPTIMAL || ts === JuMP.TIME_LIMIT) || return false, P_BUS
    val = JuMP.objective_value(model)
    val > 0 || return false, P_BUS
    P_BUS = ceil(Int, val)
    success, P_BUS
end;
function add_EV_1_module!(model, P_EV_1, E_EV_1)
        bLent, pLent = JuMP.@variable(model, [1:T], Bin), JuMP.@variable(model, [1:T])
        bEV_1, pEV_1 = JuMP.@variable(model, [1:T], Bin), JuMP.@variable(model, [1:T])
        JuMP.@constraint(model, bEV_1 .≤ bLent)
        JuMP.@constraint(model, (1 .- bLent)P_EV_1.m .≤ pLent)
        JuMP.@constraint(model, pLent .≤ (1 .- bLent)P_EV_1.M)
        JuMP.@constraint(model, (P_EV_1.m)bEV_1 .≤ pEV_1)
        JuMP.@constraint(model, pEV_1 .≤ (P_EV_1.M)bEV_1)
        JuMP.@constraint(model, sum(pEV_1) ≥ E_EV_1)
        return bEV_1, pEV_1, bLent, pLent
end;
function add_EV_2_module!(model, P_EV_2, E_EV_2, bLent, pLent)
        bEV_2, pEV_2 = JuMP.@variable(model, [1:T], Bin), JuMP.@variable(model, [1:T])
        JuMP.@constraint(model, bEV_2 .≤ bLent)
        JuMP.@constraint(model, (P_EV_2.m)bEV_2 .≤ pEV_2)
        JuMP.@constraint(model, pEV_2 .≤ (P_EV_2.M)bEV_2)
        JuMP.@constraint(model, sum(pEV_2 + pLent) ≥ E_EV_2)
        return bEV_2, pEV_2
end;
function add_self_circuit_breaker_module!(model, P_BUS, D, pU, pEV, pAC)
    pBus = JuMP.@variable(model, [1:T], lower_bound = 0, upper_bound = P_BUS)
    JuMP.@constraint(model, pBus .≥ D + pU + pEV + pAC) # No G | ES
    pBus
end;
function add_circuit_breaker_pair_module!(model, P_BUS_1, P_BUS_2, p_ES, G, pLent, pEV_1, pU_1, pAC_1, D_1, pEV_2, pU_2, pAC_2, D_2; T = T, F = F)
    pBus_1 = JuMP.@variable(model, [1:(F)T], lower_bound = -P_BUS_1, upper_bound = P_BUS_1)
    pBus_2 = JuMP.@variable(model, [1:T], lower_bound = 0, upper_bound = P_BUS_2)
    f = ˈ96ˈ24
    JuMP.@constraint(model, [t = 1:(F)T], pBus_1[t] == p_ES.c[t] -p_ES.d[t] -G[t] + pLent[f(t)] + pEV_1[f(t)] + pU_1[f(t)] + pAC_1[f(t)] + D_1[f(t)])
    JuMP.@constraint(model, pBus_2 .≥ pEV_2 + pU_2 + pAC_2 + D_2)
    pBus = JuMP.@variable(model, [1:(F)T])
    JuMP.@constraint(model, [t = 1:(F)T], pBus[t] == pBus_1[t] + pBus_2[f(t)])
    pBus, pBus_1, pBus_2
end;
function add_ES_module!(model, P_ES, E_ES; T = T, F = F)
    pES = ( # eES is dependent variable
        c = JuMP.@variable(model, [1:(F)T], lower_bound = 0),
        d = JuMP.@variable(model, [1:(F)T], lower_bound = 0)
    ); bES = JuMP.@variable(model, [1:(F)T], Bin)
        JuMP.@constraint(model, pES.c .≤ (bES)P_ES.c)
        JuMP.@constraint(model, pES.d .≤ (1 .- bES)P_ES.d)
    eES = JuMP.@variable(model, [t=1:(F)T], lower_bound = t<(F)T ? 0 : E_ES.e, upper_bound = E_ES.M)
    JuMP.@constraint(model, [t=1:(F)T], pES.c[t]*.95/F - pES.d[t]/.95/F == eES[t]-(t>1 ? eES[t-1] : E_ES.i))
    pES, bES, eES
end;
function get_E_ES(Rng)::NamedTuple # # unaltered when scale is changed
    M = rand(Rng) # Max E
    i = rand(0:M) # initial SOC _is_ this value
    e = rand(0:min(M, 21)) # ending SOC should ≥ this value
    (; i, M, e)
end;
function get_a_paired_block(O)::NamedTuple
    while true
        model = get_simple_model() # for a block who has a lender and a borrower house
        JuMP.set_attribute(model, "TimeLimit", 18)
        # 6 lines
        G = rand(0:17, (F)T)
        D_1 = rand(0:5, T)
        P_ES, E_ES = (c = rand(1:6), d = rand(1:6)), get_E_ES(19:55) # unaltered when scale is changed
        U_1 = [rand(1:4, rand(2:5)) for _ = 1:rand(1:4)] # each entry is a cycle vector of an uninterruptible load 
        P_EV_1, E_EV_1 = (m = rand((1., 1.5)), M = rand(3:7)), rand(10:39)
        CND_1, INR_1, COP_1, Q_I_1, Q_BUS_1, OH_1, OΔ_1, P_AC_1 = gen_ac_data()
        # lender house
        pES, bES, eES = add_ES_module!(model, P_ES, E_ES)
        bU_1, pU_1 = add_U_module!(model, U_1)
        bEV_1, pEV_1, bLent, pLent = add_EV_1_module!(model, P_EV_1, E_EV_1)
        o_1, q_1, pAC_1 = add_AC_module!(model, O, CND_1, INR_1, COP_1, Q_I_1, 0, OH_1, OΔ_1, P_AC_1) # Q_BUS = 0
        # 4 lines
        D_2 = rand(0:5, T) # borrower house
        U_2 = [rand(1:4, rand(2:5)) for _ = 1:rand(1:4)] # each entry is a cycle vector of an uninterruptible load 
        P_EV_2, E_EV_2 = (m = rand((1., 1.5)), M = rand(3:7)), rand(10:39)
        CND_2, INR_2, COP_2, Q_I_2, Q_BUS_2, OH_2, OΔ_2, P_AC_2 = gen_ac_data()
        # borrower house
        bU_2, pU_2 = add_U_module!(model, U_2)
        bEV_2, pEV_2 = add_EV_2_module!(model, P_EV_2, E_EV_2, bLent, pLent)
        o_2, q_2, pAC_2 = add_AC_module!(model, O, CND_2, INR_2, COP_2, Q_I_2, 0, OH_2, OΔ_2, P_AC_2) # Q_BUS = 0
        # determine the circuit breaker limit
        pBus_2 = JuMP.@variable(model, [1:T], lower_bound = 0)
        temp_x = JuMP.@variable(model)
        temp_c = JuMP.@constraint(model, pBus_2 .== temp_x)
        JuMP.@constraint(model, pBus_2 .≥ pEV_2 + pU_2 + pAC_2 + D_2)
        JuMP.@objective(model, Min, temp_x)
        JuMP.optimize!(model)
        ps, ts = JuMP.primal_status(model), JuMP.termination_status(model)
        ps === JuMP.FEASIBLE_POINT || (printstyled("\nget_a_paired_block> re-generating...\n"; color = :yellow); continue)
        (ts === JuMP.OPTIMAL || ts === JuMP.TIME_LIMIT) || (printstyled("\nget_a_paired_block> re-generating...\n"; color = :yellow); continue)
        temp_float64 = ı(temp_x)
        temp_float64 > 0 || (printstyled("\nget_a_paired_block> re-generating...\n"; color = :yellow); continue)
        P_BUS_2 = ceil(Int, temp_float64)
        JuMP.delete(model, temp_c)
        JuMP.delete(model, temp_x)
        JuMP.set_upper_bound.(pBus_2, P_BUS_2)
        temp_x = JuMP.@variable(model) # reuse the local name
        f = ˈ96ˈ24
        JuMP.@constraint(model, [t = 1:(F)T], -temp_x ≤ pES.c[t] -pES.d[t] -G[t] + pLent[f(t)] + pEV_1[f(t)] + pU_1[f(t)] + pAC_1[f(t)] + D_1[f(t)])
        JuMP.@constraint(model, [t = 1:(F)T],  temp_x ≥ pES.c[t] -pES.d[t] -G[t] + pLent[f(t)] + pEV_1[f(t)] + pU_1[f(t)] + pAC_1[f(t)] + D_1[f(t)])
        JuMP.@objective(model, Min, temp_x)
        JuMP.optimize!(model)
        ps, ts = JuMP.primal_status(model), JuMP.termination_status(model)
        ps === JuMP.FEASIBLE_POINT || (printstyled("\nget_a_paired_block> re-generating...\n"; color = :yellow); continue)
        (ts === JuMP.OPTIMAL || ts === JuMP.TIME_LIMIT) || (printstyled("\nget_a_paired_block> re-generating...\n"; color = :yellow); continue)
        temp_float64 = ı(temp_x)
        temp_float64 > -1e-5 || (printstyled("\nget_a_paired_block> re-generating...\n"; color = :yellow); continue)
        P_BUS_1 = max(1, ceil(Int, temp_float64))
        return (;P_BUS_1, P_BUS_2, G, P_ES, E_ES, D_1, D_2, U_1, U_2, P_EV_1, P_EV_2,
        E_EV_1, E_EV_2, CND_1, CND_2, INR_1, INR_2, COP_1, COP_2, Q_I_1, Q_I_2,
        Q_BUS_1, Q_BUS_2, OH_1, OH_2, OΔ_1, OΔ_2, P_AC_1, P_AC_2)
    end
end; 
function get_a_self_block(O)::NamedTuple
    while true
        D = rand(0:5, T) # base demand
        U = [rand(1:4, rand(2:5)) for _ = 1:rand(1:4)] # each entry is a cycle vector of an uninterruptible load 
        P_EV, E_EV = (m = rand((1., 1.5)), M = rand(3:7)), rand(10:39)
        CND, INR, COP, Q_I, Q_BUS, OH, OΔ, P_AC = gen_ac_data()
        success, P_BUS = pc_self_P_BUS(D, U, P_EV, E_EV, O, CND, INR, COP, Q_I, OH, OΔ, P_AC)
        success && return (;P_BUS, D, U, P_EV, E_EV, CND, INR, COP, Q_I, Q_BUS, OH, OΔ, P_AC)
        printstyled("get_a_self_block> re-generating...\n"; color = :yellow)
    end
end;
function add_a_paired_block!(model, d::NamedTuple)::NamedTuple
        # lender house
        pES, bES, eES = add_ES_module!(model, d.P_ES, d.E_ES)
        bU_1, pU_1 = add_U_module!(model, d.U_1)
        bEV_1, pEV_1, bLent, pLent = add_EV_1_module!(model, d.P_EV_1, d.E_EV_1)
        o_1, q_1, pAC_1 = add_AC_module!(model, O, d.CND_1, d.INR_1, d.COP_1, d.Q_I_1, 0, d.OH_1, d.OΔ_1, d.P_AC_1) # Q_BUS = 0
        # borrower house
        bU_2, pU_2 = add_U_module!(model, d.U_2)
        bEV_2, pEV_2 = add_EV_2_module!(model, d.P_EV_2, d.E_EV_2, bLent, pLent)
        o_2, q_2, pAC_2 = add_AC_module!(model, O, d.CND_2, d.INR_2, d.COP_2, d.Q_I_2, 0, d.OH_2, d.OΔ_2, d.P_AC_2) # Q_BUS = 0
        # circuit breaker pair
        pBus, pBus_1, pBus_2 = add_circuit_breaker_pair_module!(model, d.P_BUS_1, d.P_BUS_2,
            pES, d.G, pLent, pEV_1, pU_1, pAC_1, d.D_1,
            pEV_2, pU_2, pAC_2, d.D_2)
        (;pBus, pBus_1, pBus_2, bLent, bES, bEV_1, bEV_2, bU_1, bU_2, q_1, q_2)
end;
function add_a_self_block!(model, d::NamedTuple)::NamedTuple
        bU, pU = add_U_module!(model, d.U)
        bEV, pEV = add_self_EV_module!(model, d.P_EV, d.E_EV)
        o, q, pAC = add_AC_module!(model, O, d.CND, d.INR, d.COP, d.Q_I, 0, d.OH, d.OΔ, d.P_AC) # Q_BUS = 0
        pBus = add_self_circuit_breaker_module!(model, d.P_BUS, d.D, pU, pEV, pAC)
        (;pBus, bEV, bU, q)
end;
function build_cen!(D; cen = cen, Rng1 = Rng1, T = T, F = F)
    pA = map(_ -> zero(JuMP.AffExpr), 1:(F)T)
    for j = 1:J
        addf = ifelse(j ∈ Rng1, add_a_paired_block!, add_a_self_block!)
        nt = addf(cen, D[j])
        e = nt.pBus
        if j ∈ Rng1
            foreach(t -> JuMP.add_to_expression!(pA[t], e[t]), 1:(F)T)
        else
            foreach(t -> JuMP.add_to_expression!(pA[t], e[ˈ96ˈ24(t)]), 1:(F)T)
        end
        print("\rbuild_cen> j = $j / $J = J")
    end
    JuMP.@variable(cen, pA_scalar)
    JuMP.@constraint(cen, pA_scalar .≥ pA)
    pA_scalar
end;
function fill_inn!(X, D; inn = inn, O = O, Rng1 = Rng1)
    z = Threads.Atomic{Int}(J)
    f = function(j)
        p = j ∈ Rng1
        getf = ifelse(p, get_a_paired_block, get_a_self_block)
        D[j] = Dj = getf(O)
        addf = ifelse(p, add_a_paired_block!, add_a_self_block!)
        X[j] = addf(inn[j], Dj)
        Threads.atomic_sub!(z, 1)
        print("\rfill_inn!> rest = $(z.value), j = $j")
    end
    tasks = map(j -> Threads.@spawn(f(j)), 1:J)
    foreach(wait, tasks)
    println()
end;
function solve_mst_and_up_value!(m, s, θ, β)
    JuMP.optimize!(m)
    ts = JuMP.termination_status(m)
    ts === JuMP.OPTIMAL || error("solve_mst> $ts") # The LP should always be solved to OPTIMAL
    push!(mstsolvetime, JuMP.solve_time(m))
    s.ub.x = JuMP.objective_bound(m)
    value!(s.β, m, β)
    value!(s.θ, m, θ)
end;
function shot!(ref)
    s = rEF.x
    @lock mst_lock solve_mst_and_up_value!(model, s, θ, β) # `s` gets updated/mutated here
    s_tmp = ref.x
    setfield!(ref, :x, s) # the ref gets updated here
    setfield!(rEF, :x, s_tmp)
end;
function initialize_out()
    model = get_simple_model()
    JuMP.@variable(model, β[1:(F)T] ≥ 0)
    JuMP.@constraint(model, sum(β) == 1) # ⚠️ special
    JuMP.@variable(model, θ[1:J])
    JuMP.@expression(model, out_obj_tbMax, sum(θ))
    JuMP.@objective(model, Max, out_obj_tbMax)
    # JuMP.@constraint(model, out_obj_tbMax ≤ an_UB)
    model, θ, β
end;
function bilin_expr(j, iˈı::Function, β)
    if j in Rng1
        JuMP.@expression(model, sum(iˈı(p)b for (b, p) = zip(β, X[j].pBus)))
    else
        p = X[j].pBus
        JuMP.@expression(model, sum(iˈı(p[ˈ96ˈ24(t)])β[t] for t = 1:(F)T))
    end
end;
sublog(j) = println("block(j = $j)> no solution, go back to re-optimize...")
function subproblemˈs_duty(j; initialize = false)
    mj = inn[j]
    while true
        s = getfield(ref, :x)
        JuMP.set_objective_function(mj, bilin_expr(j, identity, s.β))
        JuMP.optimize!(mj)
        ps, ts = JuMP.primal_status(mj), JuMP.termination_status(mj)
        ts === JuMP.INTERRUPTED && return
        if ps !== JuMP.FEASIBLE_POINT || (ts !== JuMP.OPTIMAL && ts !== JuMP.TIME_LIMIT)
            JuMP.set_attribute(mj, "Seed", rand(0:2000000000))
            Threads.@spawn(:interactive, sublog(j))
            continue
        end
        if !initialize
            s = getfield(ref, :x)
            s.θ[j] - bilin_expr(j, ı, s.β) > COT || return
        end
        K_VCG[j] === MaxVerNum && return
        K_VCG[j] += 1
        fillvertex!(VCG[j][K_VCG[j]], j, X)
        if j in Rng1
            @lock pairblockrgap_lock push!(pairblockrgap, JuMP.relative_gap(mj))
        else
            @lock selfblockrgap_lock push!(selfblockrgap, JuMP.relative_gap(mj))
        end
        con = JuMP.@build_constraint(θ[j] ≤ bilin_expr(j, ı, β))
        @lock mst_lock JuMP.add_constraint(model, con)
        Threads.atomic_add!(Ncuts, 1)
        return
    end
end;
function subproblemˈs_objbnd!(pvv, j)
    mj = inn[j]
    s = getfield(ref, :x)
    JuMP.set_objective_function(mj, bilin_expr(j, identity, s.β))
    JuMP.set_attribute(mj, "TimeLimit", 120)
    while true
        JuMP.optimize!(mj)
        ps, ts = JuMP.primal_status(model), JuMP.termination_status(model)
        if ps === JuMP.FEASIBLE_POINT && (ts === JuMP.OPTIMAL || ts === JuMP.TIME_LIMIT)
            pvv[j] = JuMP.objective_bound(mj)
            return
        end
        JuMP.set_attribute(mj, "TimeLimit", 20)
        printstyled("subproblemˈs_objbnd!> re-solving block $j...\n"; color = :yellow)
    end
end;
function warm()
    tasks = map(j -> Threads.@spawn(subproblemˈs_duty(j; initialize = true)), 1:J)
    foreach(wait, tasks)
    Ncuts.value === J || error(); # each block contributes 1 cut to make the master have a finite initial dual bound
    shot!(ref)
    nothing
end;
next(j, J) = j === J ? 1 : j+1;
ntasksaway(tasks) = count(!istaskdone, tasks); # num of tasks away (i.e. not controllable)
function mainlog(tasks, ref, Ncuts, tabs0)
    a = ntasksaway(tasks)
    ub = ref.x.ub.x
    n = Ncuts.value
    t = round(Int, time() - tabs0) 
    println("main> N_BlockTasks_Away = $a, ub = $ub, Ncuts = $n, $t sec")
end;
function main(train_time, mst_task_ref, tasks)
    tabs0 = time(); tabs1 = tabs0 + train_time # Time control region, do NOT move this line
    #################################################
    Ncuts0 = Ncuts.value
    j = 1
    #################################################
    tlog0 = time() # Logging control region, do NOT move this line
    while proceed_main.x && time() < tabs1 # Run with a time limit, or you manually interrupt
        if time() - tlog0 > LOG_TIME
            Threads.@spawn(:interactive, mainlog(tasks, ref, Ncuts, tabs0))
            tlog0 = time()
        end
        if istaskdone(mst_task_ref.x)
            Ncuts_now = Ncuts.value
            if Ncuts_now !== Ncuts0 # detect if new cuts have been added to the master
                if ntasksaway(tasks) < THREAD_FOR_BLOCKS
                    mst_task_ref.x = Threads.@spawn(shot!(ref)) # master's duty
                    Ncuts0 = Ncuts_now
                end
            end
        end
        while ntasksaway(tasks) < THREAD_FOR_BLOCKS  # for block subproblems
            if istaskdone(tasks[j])
                tasks[j] = Threads.@spawn(subproblemˈs_duty($j))
            end
            j = next(j, J) # go to ask the next block
        end
        GC.safepoint()
    end
    foreach(j -> Gurobi.GRBterminate(JuMP.backend(inn[j])), 1:J)
end;
function main(train_time)
    mst_task_ref = Ref(Threads.@spawn(shot!(ref)))
    tasks = map(_ -> Threads.@spawn(missing), 1:J)
    main_task = Threads.@spawn(main(train_time, mst_task_ref, tasks))
    (main = main_task, master = mst_task_ref, blocks = tasks)
end;
macro get_int_decision(model, expr) return esc(quote
    let e = JuMP.@expression($model, $expr), a
        a = map(_ -> JuMP.@variable($model, integer = true), e)
        JuMP.@constraint($model, a .== e)
        a
    end
end) end;
get_prob_decision(model, I) = (x = JuMP.@variable(model, [1:I], lower_bound = 0); JuMP.@constraint(model, sum(x) == 1); x);
function primal_recovery(model) 
    JuMP.set_attribute(model, "Threads", 4)
    l = [get_prob_decision(model, K_VCG[j]) for j = 1:J]
    Y = [j ∈ Rng1 ? (
        bES = @get_int_decision(model, sum((t.bES)l for (l, t) = zip(l[j], VCG[j]))),
        bU_1 = @get_int_decision(model, sum((t.bU_1)l for (l, t) = zip(l[j], VCG[j]))),
        bU_2 = @get_int_decision(model, sum((t.bU_2)l for (l, t) = zip(l[j], VCG[j]))),
        bEV_1 = @get_int_decision(model, sum((t.bEV_1)l for (l, t) = zip(l[j], VCG[j]))),
        bEV_2 = @get_int_decision(model, sum((t.bEV_2)l for (l, t) = zip(l[j], VCG[j]))),
        bLent = @get_int_decision(model, sum((t.bLent)l for (l, t) = zip(l[j], VCG[j]))),
        pBus = JuMP.@expression(model, sum((t.pBus)l for (l, t) = zip(l[j], VCG[j])))
    ) : (
        bU = @get_int_decision(model, sum((t.bU)l for (l, t) = zip(l[j], VCG[j]))),
        bEV = @get_int_decision(model, sum((t.bEV)l for (l, t) = zip(l[j], VCG[j]))),
        pBus = JuMP.@expression(model, sum((t.pBus)l for (l, t) = zip(l[j], VCG[j])))
    ) for j = 1:J]
    a = [zero(JuMP.AffExpr) for t = 1:(F)T]
    for j = 1:J
        p = Y[j].pBus
        if j in Rng1
            foreach(t -> JuMP.add_to_expression!(a[t], p[t]), 1:(F)T)
        else
            foreach(t -> JuMP.add_to_expression!(a[t], p[ˈ96ˈ24(t)]), 1:(F)T)
        end
    end
    JuMP.@variable(model, e)
    JuMP.@objective(model, Min, e)
    JuMP.@constraint(model, e .≥ a)
    JuMP.optimize!(model)
    prec_time = JuMP.solve_time(model)
    printstyled("Primal Opt time = $prec_time\n"; color = :cyan)
    ub = Inf
    ps, ts = JuMP.primal_status(model), JuMP.termination_status(model)
    if ps === JuMP.FEASIBLE_POINT && (ts === JuMP.OPTIMAL || ts === JuMP.TIME_LIMIT)
        ub = JuMP.objective_value(model)
    end
    prec_time, ub
end;
const (Rng1, Rng2), (C, O) = get_pair_and_self_Rng(J), get_C_and_O();
const MaxVerNum, proceed_main = 150, Ref(true); # increase this if not sufficient
const COT, an_UB = 0.5/J, 30.0J;
const mst_lock, selfblockrgap_lock, pairblockrgap_lock = ReentrantLock(), ReentrantLock(), ReentrantLock();
const (model, θ, β), inn, Ncuts = initialize_out(), [get_simple_model() for j = 1:J], Threads.Atomic{Int}(0);
const ref, rEF = Ref((θ = fill(an_UB/J, J), β = fill(1/F/T, (F)T), ub = Ref(an_UB))), Ref((θ = fill(an_UB/J, J), β = fill(1/F/T, (F)T), ub = Ref(an_UB)));
const X, D = Vector{NamedTuple}(undef, J), Vector{NamedTuple}(undef, J); # X is inn's
fill_inn!(X, D); # build the J blocks in parallel, and get the raw Data

const cen = get_simple_model();
const pA_scalar = build_cen!(D)
JuMP.set_upper_bound(pA_scalar, 324265 * 0.98)
JuMP.set_attribute(cen, "NodeLimit", 0)
JuMP.set_attribute(cen, "Method", 2)
const undo = JuMP.relax_integrality(cen);
JuMP.unset_silent(cen)
Gurobi.GRBreset(JuMP.backend(cen), 0)
JuMP.optimize!(cen)
Gurobi.GRBwrite(JuMP.backend(cen), "model.mps") # very large

I think this is a good example to be referred to, if in the future someone’s seeking a very hard LP instance that makes Gurobi struggle.

Consider using the
homogeneous algorithm (through parameter ‘BarHomogeneous’)

Okay, this is indeed efficient:

Non-default parameters:
Method  2
BarHomogeneous  1
Threads  1

Optimize a model with 5151290 rows, 7825201 columns and 22917888 nonzeros
Model fingerprint: 0xc0869613
Coefficient statistics:
  Matrix range     [9e-01, 2e+01]
  Objective range  [0e+00, 0e+00]
  Bounds range     [1e+00, 3e+05]
  RHS range        [1e+00, 2e+02]
Presolve removed 1849501 rows and 1574285 columns (presolve time = 20s)...
Presolve time: 27.07s
Presolved: 3301789 rows, 6250916 columns, 20473244 nonzeros

Ordering time: 2.70s

Barrier statistics:
 AA' NZ     : 1.277e+07
 Factor NZ  : 5.351e+07 (roughly 4.0 GB of memory)
 Factor Ops : 1.352e+09 (less than 1 second per iteration)
 Threads    : 1

                  Objective                Residual
Iter       Primal          Dual         Primal    Dual     Compl     Time
   0   0.00000000e+00 -4.76646314e+06  8.77e+07 0.00e+00  5.87e+02    37s
   1   0.00000000e+00 -5.00328330e+06  4.23e+07 4.04e-02  2.90e+02    40s
   2   0.00000000e+00 -5.26160474e+06  1.08e+07 4.46e-03  7.79e+01    42s
   3   0.00000000e+00 -5.07795388e+06  4.22e+05 2.36e-16  3.39e+00    45s
   4   0.00000000e+00 -1.10448320e+06  1.01e+05 1.48e-16  3.30e-01    49s
   5   0.00000000e+00 -2.94152694e+05  3.69e+04 1.54e-16  6.89e-02    52s
   6   0.00000000e+00 -1.76398016e+05  2.15e+04 1.30e-16  3.39e-02    55s
   7   0.00000000e+00 -9.50509259e+04  1.20e+04 2.90e-15  1.56e-02    58s
   8   0.00000000e+00 -6.81468074e+04  7.60e+03 2.14e-15  9.81e-03    60s
   9   0.00000000e+00 -4.71985453e+04  5.63e+03 1.45e-15  6.62e-03    63s
  10   0.00000000e+00 -2.86991449e+04  4.24e+03 1.82e-15  4.19e-03    66s
  11   0.00000000e+00 -1.82919417e+04  3.21e+03 1.27e-15  2.79e-03    69s
  12   0.00000000e+00 -1.57964031e+04  2.27e+03 1.01e-15  2.18e-03    73s
  13   0.00000000e+00 -1.14519182e+04  1.79e+03 7.32e-16  1.64e-03    77s
  14   0.00000000e+00 -9.10709156e+03  1.47e+03 4.04e-15  1.34e-03    81s
  15   0.00000000e+00 -7.52587936e+03  1.30e+03 3.87e-15  1.17e-03    85s
  16   0.00000000e+00 -6.25731368e+03  1.17e+03 1.23e-14  1.05e-03    88s
  17   0.00000000e+00 -5.79891549e+03  1.01e+03 7.20e-15  9.65e-04    92s
  18   0.00000000e+00 -4.82040734e+03  8.82e+02 2.64e-14  8.77e-04    96s
  19   0.00000000e+00 -4.48710235e+03  7.97e+02 5.19e-14  8.46e-04    99s
  20   0.00000000e+00 -4.10588352e+03  6.78e+02 7.03e-14  8.03e-04   103s
  21   0.00000000e+00 -3.67661883e+03  6.17e+02 2.59e-13  8.05e-04   107s
  22   0.00000000e+00 -3.31058494e+03  5.70e+02 8.30e-13  8.46e-04   110s
  23   0.00000000e+00 -3.03163894e+03  5.29e+02 8.92e-13  9.26e-04   114s
  24   0.00000000e+00 -2.79976604e+03  4.93e+02 4.13e-12  1.07e-03   118s
  25   0.00000000e+00 -2.57651721e+03  4.60e+02 1.15e-11  1.40e-03   122s
  26   0.00000000e+00 -2.51165364e+03  4.34e+02 4.91e-11  2.16e-03   126s
  27   0.00000000e+00  4.18957280e+04  4.20e+02 1.42e-07  3.10e+00   130s
  28*  0.00000000e+00  1.76244750e+08  3.87e-03 9.51e-07  1.12e-05   133s
  29*  0.00000000e+00  8.86758696e+12  3.87e-09 9.00e-07  1.12e-11   136s

Barrier performed 29 iterations in 135.60 seconds (138.33 work units)
Infeasible model

My point is that: This routine should be adopted automatically (after the default barrier algorithm fails), rather than manual setting by a user. The current behavior is: if Gurobi fails with the default barrier algorithm, it starts a new simplex algorithm which can be slow.

 103   0.00000000e+00  2.53482347e+09  2.90e+02 7.57e-10  1.66e+01   328s
 104   0.00000000e+00  2.65287083e+09  2.88e+02 9.31e-10  1.55e+01   331s
 105   0.00000000e+00  3.29485894e+09  2.87e+02 2.56e-09  1.66e+01   335s
 106   0.00000000e+00  4.13426175e+09  2.86e+02 8.15e-09  1.78e+01   339s

Barrier performed 106 iterations in 338.87 seconds (345.14 work units)
Numerical trouble encountered

Model may be infeasible or unbounded.  Consider using the
homogeneous algorithm (through parameter 'BarHomogeneous')

Iteration    Objective       Primal Inf.    Dual Inf.      Time
Iteration    Objective       Primal Inf.    Dual Inf.      Time
       0    0.0000000e+00   2.466441e+06   0.000000e+00    342s
  109710    1.8011722e-01   2.400691e+06   0.000000e+00    346s
  255990    8.1314183e-01   2.270725e+06   0.000000e+00    351s
  365700    1.1154440e+00   1.946478e+06   0.000000e+00    355s
  511980    1.4032147e+00   1.528571e+06   0.000000e+00    361s
  621690    1.5685294e+00   1.257645e+06   0.000000e+00    365s
  731400    1.6967655e+00   1.046632e+06   0.000000e+00    370s
  841110    1.7946280e+00   1.037387e+06   0.000000e+00    375s
  950820    1.8745060e+00   1.076037e+06   0.000000e+00    380s
 1060530    1.9478232e+00   1.172295e+06   0.000000e+00    387s
 1097100    1.9733687e+00   1.152776e+06   0.000000e+00    390s
 1170240    2.0220612e+00   1.247817e+06   0.000000e+00    397s
 1206810    2.0458962e+00   1.158666e+06   0.000000e+00    400s
 1279950    2.0900210e+00   1.179323e+06   0.000000e+00    408s
 1316520    2.1116527e+00   1.177040e+06   0.000000e+00    413s
 1353090    2.1320868e+00   1.267447e+06   0.000000e+00    417s
 1389660    2.1525210e+00   1.151949e+06   0.000000e+00    423s
 1426230    2.1721089e+00   1.275990e+06   0.000000e+00    428s
 1462800    2.1910737e+00   1.161564e+06   0.000000e+00    432s
 1499370    2.2091075e+00   1.554679e+06   0.000000e+00    438s
 1535940    2.2263905e+00   1.063023e+06   0.000000e+00    444s
 1572510    2.2438933e+00   1.813954e+06   0.000000e+00    451s
 1609080    2.2596700e+00   2.028634e+06   0.000000e+00    458s
 1645650    2.2754823e+00   2.061612e+06   0.000000e+00    465s
 1682220    2.2903398e+00   1.575590e+06   0.000000e+00    471s
 1718790    2.3043923e+00   1.469651e+06   0.000000e+00    478s
 1755360    2.3181441e+00   1.259798e+06   0.000000e+00    485s
 1790090    2.3310155e+00   1.766453e+06   0.000000e+00    493s
 1824980    2.3432203e+00   2.112911e+06   0.000000e+00    500s
 1861550    2.3554564e+00   1.044158e+06   0.000000e+00    508s
 1896900    2.3667480e+00   9.414480e+05   0.000000e+00    516s
 1931710    2.3774712e+00   8.658374e+05   0.000000e+00    523s
 1967180    2.3879567e+00   1.097886e+06   0.000000e+00    531s
 2001640    2.3976177e+00   1.034233e+06   0.000000e+00    539s
 2037710    2.4070626e+00   1.000828e+06   0.000000e+00    547s
 2074150    2.4161762e+00   8.386380e+05   0.000000e+00    555s
 2110580    2.4244972e+00   1.417942e+06   0.000000e+00    563s
 2144950    2.4320973e+00   8.205064e+05   0.000000e+00    570s
 2178290    2.4388268e+00   8.643476e+05   0.000000e+00    578s
 2211890    2.4452470e+00   1.038320e+06   0.000000e+00    585s
 2240420    2.4507582e+00   1.119746e+06   0.000000e+00    593s
 2271670    2.4563384e+00   1.565561e+06   0.000000e+00    601s
 2299890    2.4612471e+00   1.244946e+06   0.000000e+00    609s
 2328610    2.4658886e+00   8.531455e+05   0.000000e+00    616s
 2360260    2.4704162e+00   1.272277e+06   0.000000e+00    624s
 2385560    2.4743651e+00   1.500942e+06   0.000000e+00    632s
 2412310    2.4778530e+00   1.197653e+06   0.000000e+00    639s
 2438340    2.4811283e+00   7.336807e+05   0.000000e+00    647s
 2465260    2.4843041e+00   9.416847e+05   0.000000e+00    655s
 2490590    2.4871226e+00   2.406924e+06   0.000000e+00    662s
 2514540    2.4897078e+00   1.477720e+06   0.000000e+00    670s
 2538810    2.4920959e+00   1.933593e+06   0.000000e+00    677s
 2561770    2.4941215e+00   2.246290e+06   0.000000e+00    685s
 2584030    2.4959590e+00   1.543720e+06   0.000000e+00    692s
 2605570    2.4976243e+00   1.949453e+06   0.000000e+00    700s
 2625840    2.4988329e+00   1.855587e+06   0.000000e+00    708s
 2644710    2.5000791e+00   1.926816e+06   0.000000e+00    715s
 2660900    2.5009753e+00   3.777251e+06   0.000000e+00    722s
 2669995    2.5033769e+00   3.438192e+06   0.000000e+00    732s
 2671662    2.5050269e+00   3.036461e+06   0.000000e+00    741s
 2672252    2.5063270e+00   1.943163e+06   0.000000e+00    747s
 2673202    2.5086493e+00   1.875913e+06   0.000000e+00    765s
 2674362    2.5113920e+00   1.777126e+06   0.000000e+00    784s
 2675432    2.5139227e+00   1.676542e+06   0.000000e+00    803s
 2676352    2.5158061e+00   1.904125e+06   0.000000e+00    822s
 2676762    2.5166058e+00   1.615767e+06   0.000000e+00    836s
 2677872    2.5192902e+00   1.668128e+06   0.000000e+00    854s
 2679212    2.5224867e+00   1.324876e+06   0.000000e+00    873s
 2679672    2.5241868e+00   1.328464e+06   0.000000e+00    887s
 2680802    2.5267520e+00   1.337193e+06   0.000000e+00    905s
 2682112    2.5291243e+00   1.374830e+06   0.000000e+00    924s
 2683242    2.5313993e+00   1.301414e+06   0.000000e+00    943s
 2683722    2.5325792e+00   1.242338e+06   0.000000e+00    956s
 2684772    2.5348874e+00   1.260935e+06   0.000000e+00    975s
 2685742    2.5366244e+00   1.268089e+06   0.000000e+00    994s
 2687022    2.5379028e+00   1.340052e+06   0.000000e+00   1013s
 2688032    2.5395903e+00   1.585502e+06   0.000000e+00   1032s
 2688402    2.5415632e+00   1.487363e+06   0.000000e+00   1048s
 2688662    2.5426402e+00   1.140033e+06   0.000000e+00   1062s
 2688892    2.5443424e+00   1.361655e+06   0.000000e+00   1078s
 2689112    2.5459921e+00   2.294964e+06   0.000000e+00   1093s
 2689332    2.5469888e+00   1.158473e+06   0.000000e+00   1108s
 2689562    2.5477669e+00   1.156053e+06   0.000000e+00   1123s
 2689862    2.5480347e+00   1.638477e+06   0.000000e+00   1138s
 2691052    2.5499091e+00   1.867416e+06   0.000000e+00   1157s
 2691622    2.5511140e+00   1.729155e+06   0.000000e+00   1171s

I give a recap for this topic here:

The model in this topic is a large-scale LP, which is essentially a feasibility system (obj===0). Theoretically, there should be efficient (e.g. polynomial) algorithms for this sort of problem (as opposed to giving an infeasibility certificate to a large-scale MIP, which is NP-hard).

In practice, we need to manually set the BarHomogeneous param to 1 in Gurobi to invoke this efficient algorithm. Otherwise the default routine might not come up with any valid useful info in reasonable time (the default Barrier fails, whereas the simplex algorithm seems to be non-polynomial, i.e. inefficient).