Closed Foggalong closed 2 months ago
For checking the two methods within Gurobi, aside from verifying output from gurobi_robust_genetics_sqp
was consistent with Julian's output (it was), I used the following internal comparison:
import alphargs
# key problem variables loaded from standard format txt files
sigma, mubar, omega, n = alphargs.load_problem(
"examples/04/A04.txt",
"examples/04/EBV04.txt",
"examples/04/S04.txt"
)
sires = range(0, n, 2)
dams = range(1, n, 2)
lam = 0.5
kap = 1
# robust direct optimization using Gurobi
w_rbs, z_rbs, obj_rbs = alphargs.gurobi_robust_genetics(
sigma, mubar, omega, sires, dams, lam, kap, n)
# robust SQP optimization using Gurobi
w_sqp, z_sqp, obj_sqp = alphargs.gurobi_robust_genetics_sqp(
sigma, mubar, omega, sires, dams, lam, kap, n)
# compare robust using direct and SQP
alphargs.print_compare_solutions(
w_rbs, w_sqp, obj_rbs, obj_sqp, z1=z_rbs, z2=z_sqp,
name1="w_rbs", name2="w_sqp")
print("\nDirect:")
if not alphargs.check_uncertainty_constraint(z_rbs, w_rbs, omega, debug=True):
raise ValueError
print("\nSQP:")
if not alphargs.check_uncertainty_constraint(z_sqp, w_sqp, omega, debug=True):
raise ValueError
print("\nDone!")
which produces the output
i w_rbs w_sqp
1 0.38200 0.38214
2 0.38200 0.38214
3 0.11800 0.11786
4 0.11800 0.11786
w_rbs objective: 0.77684 (z = 0.37924)
w_sqp objective: 0.77684 (z = 0.37892)
Maximum change: 0.00014
Average change: 0.00014
Minimum change: 0.00014
Direct:
z: 0.37923871642022844
w'*Ω*w: 0.3792386953366983
Diff: 2.1083530143961582e-08
SQP:
z: 0.37892405482420255
w'*Ω*w: 0.37892405801748014
Diff: -3.1932775867993257e-09
I'm not sure why the slight (but not insignificant) difference between direct robust optimization and using SQP, though I haven't yet checked the MPS files to see if there's a model error or similar.
Using a similar test to above but just looking at the standard non-robust problem,
import alphargs
sigma, mubar, omega, n = alphargs.load_problem(
"examples/50/A50.txt",
"examples/50/EBV50.txt",
"examples/50/S50.txt",
issparse=True # loads into SciPy, not NumPy
)
sires = range(0, n, 2)
dams = range(1, n, 2)
lam = 0.5
kap = 1
w_gurobi, obj_gurobi = alphargs.gurobi_standard_genetics(
sigma, mubar, sires, dams, lam, n)
w_highs, obj_highs = alphargs.highs_standard_genetics(
sigma, mubar, sires, dams, lam, n)
alphargs.print_compare_solutions(
w_gurobi, w_highs, obj_gurobi, obj_highs, name1="Gurobi", name2="HiGHS")
gives weights that differ by order $10^{-5}$ between Gurobi and HiGHS, the largest being a difference of $0.00014$. Tried to output model files to compare, but output through highspy
didn't seem to be working. Unsure if this is an error somewhere in my order of operations or a bug elsewhere.
Tried to output model files to compare, but output through
highspy
didn't seem to be working. Unsure if this is an error somewhere in my order of operations or a bug elsewhere.model file can be used externally for verification # BUG not working
if debug: h.setOptionValue('write_model_to_file', True) h.setOptionValue('write_model_file', 'standard-opt.mps')
These option values force writing of the model to a file only when running HiGHS from the command line - allows conversion from .mps
to .lp
. When using the callable library (or highspy
) you need to call h.writeModel("foo.mps")
I say "working" in ba6d088 because the HiGHS SQP function gives the correct answer for the $n = 4$ example, but is 33 times slower than Gurobi. It also core dumps for the $n = 50$ on the first call of h.run()
, never even making it to the first new constraint being added.
Between the two, it points to some issue with how I've defined the $z$ variable as an extension of $\mathbf{w}$. There may be issues with the constraint definition too, but that $n=50$ doesn't even get there shows it's not the immediate problem.
@jajhall Once h.passModel(...)
has been used, is it still possible to do something like
h.addVar(0, highspy.kHighsInf)
h.changeColCost(dimension, kappa)
to add an additional variable? I tried doing this to incorporate $z$ as an alternative to what was pushed in ba6d088, but that just resulted in 'Model error' without any of the usual hints about what it was that caused the error.
I've checked out https://github.com/Foggalong/robust-genetics/commit/ba6d0883f7f85c945756bf43f807106480dd25aa, but python3 example.py
seems just to run gurobi
.
What version of highspy
are you running? There was a bug with the naming of addvar
- in particular following it with changeColCost
that's fixed in v1.7.1
What version of
highspy
are you running?
According to pip show highspy
it's version 1.7.1.dev2
.
I've checked out https://github.com/Foggalong/robust-genetics/commit/ba6d0883f7f85c945756bf43f807106480dd25aa, but
python3 example.py
seems just to rungurobi
.
Yeah, of the back of what you said earlier I've been trying to avoid committing changes to the example file to avoid flooding the history with minor changes while debugging. I've tried to make usage of the two similar, so it just replaces gurobi_
with highs_
and adds issparse=True
to the loader if it wasn't already there.
This is what I'm using to compare HiGHS-HiGHS:
sigma, mubar, omega, n = alphargs.load_problem(
"examples/04/A04.txt",
"examples/04/EBV04.txt",
"examples/04/S04.txt",
issparse=True
)
sires = range(0, n, 2)
dams = range(1, n, 2)
lam = 0.5
kap = 1
# computes the standard and robust genetic selection solutions
w_std, obj_std = alphargs.highs_standard_genetics(
sigma, mubar, sires, dams, lam, n)
w_rbs, z_rbs, obj_rbs = alphargs.highs_robust_genetics_sqp(
sigma, mubar, omega, sires, dams, lam, kap, n)
alphargs.print_compare_solutions(
w_std, w_rbs, obj_std, obj_rbs, z2=z_rbs, name1="w_std", name2="w_rbs")
And this is what I'm using to compare Gurobi-HiGHS:
sigma, mubar, omega, n = alphargs.load_problem(
"examples/50/A50.txt",
"examples/50/EBV50.txt",
"examples/50/S50.txt",
issparse=True
)
sires = range(0, n, 2)
dams = range(1, n, 2)
lam = 0.5
kap = 1
t0 = time.time()
w_grb, z_grb, obj_grb = alphargs.gurobi_robust_genetics_sqp(
sigma, mubar, omega, sires, dams, lam, kap, n, debug=False)
t1 = time.time()
print(f"Gurobi took {t1-t0:.5f} seconds")
t0 = time.time()
w_his, z_his, obj_his = alphargs.highs_robust_genetics_sqp(
sigma, mubar, omega, sires, dams, lam, kap, n, debug=False)
t1 = time.time()
print(f"HiGHS took {t1-t0:.5f} seconds")
alphargs.print_compare_solutions(
w_grb, w_his, obj_grb, obj_his, z1=z_grb, z2=z_his,
name1="Gurobi", name2="HiGHS", tol=1e-6
)
I'm not certain yet, but I think the issue for the pushed method comes from not extending sigma
correctly when z
is incorporated. Think I may have jumbled the indexes and pointers in those definitions.
Commenting out the
if not debug: h.setOptionValue('output_flag', False) h.setOptionValue('log_to_console', False)
I immediately see reported
ERROR: Matrix dimension validation fails on index size = 50 < 51 = number of nonzeros ERROR: LP dimension validation (passModel) fails on a_matrix dimensions ERROR: LP dimension validation (passModel) fails h.passModel(model) returns status = HighsStatus.kError
Your model.lp_.a_matrix_.start_
contains values [0, 25, 51]
so HiGHS expects model.lp_.a_matrix_.index_
to be of size 51, but it's of size 50
Correcting the model.lp_.a_matrix_.start_
error and running again, I get
Running HiGHS 1.7.1 (git hash: 0c240d8): Copyright (c) 2024 HiGHS under MIT licence terms ERROR: Hessian matrix packed vector 51 has illegal start of 2500 > 51 = number of nonzeros h.passModel(model) returns status = HighsStatus.kError
That's because model.hessian_.start_
should be
= np.append(sigma.indptr, dimension*dimension)
not
= np.append(sigma.indptr, dimension + 1)
Correcting that, I see that HiGHS runs for max_iterations
because reduction of the gap stalls at
gap = 5.2256630966862616e-08 ; robust_gap_tol = 1e-08
If I test against 10*robust_gap_tol
then the output is
python3 example.py Set parameter Username Academic license - for non-commercial use only - expires 2024-12-21 Gurobi took 0.07078 seconds
dams = 25
sires = 25
dimension = 50
start = 0 25 50
hessian_start = [0, 50, 100, 150, 200, 250, 300, 350, 400, 450, 500, 550, 600, 650, 700, 750, 800, 850, 900, 950, 1000, 1050, 1100, 1150, 1200, 1250, 1300, 1350, 1400, 1450, 1500, 1550, 1600, 1650, 1700, 1750, 1800, 1850, 1900, 1950, 2000, 2050, 2100, 2150, 2200, 2250, 2300, 2350, 2400, 2450, 2500, 2500]
h.passModel(model) returns status = HighsStatus.kOk Iter 0 : gap = 0.18082919748787718 ; robust_gap_tol = 1e-08 Iter 1 : gap = 0.12806381581161647 ; robust_gap_tol = 1e-08 Iter 2 : gap = 0.032786471129276046 ; robust_gap_tol = 1e-08 Iter 3 : gap = 0.021426794862761944 ; robust_gap_tol = 1e-08 Iter 4 : gap = 0.00900572532136007 ; robust_gap_tol = 1e-08 Iter 5 : gap = 0.004463685289424507 ; robust_gap_tol = 1e-08 Iter 6 : gap = 0.0011589246184516289 ; robust_gap_tol = 1e-08 Iter 7 : gap = 0.00030844113417907715 ; robust_gap_tol = 1e-08 Iter 8 : gap = 0.00023256718986991443 ; robust_gap_tol = 1e-08 Iter 9 : gap = 5.8201795532508704e-05 ; robust_gap_tol = 1e-08 Iter 10 : gap = 1.4559719441559205e-05 ; robust_gap_tol = 1e-08 Iter 11 : gap = 3.8792773184304075e-06 ; robust_gap_tol = 1e-08 Iter 12 : gap = 1.036192015219095e-06 ; robust_gap_tol = 1e-08 Iter 13 : gap = 4.596713923388229e-07 ; robust_gap_tol = 1e-08 Iter 14 : gap = 2.650064735709723e-07 ; robust_gap_tol = 1e-08 Iter 15 : gap = 1.215385057318219e-07 ; robust_gap_tol = 1e-08 Iter 16 : gap = 6.85891470286748e-08 ; robust_gap_tol = 1e-08 HiGHS took 0.01694 seconds i Gurobi HiGHS 02 0.07811 0.07812 03 0.12466 0.12470 08 0.08075 0.08069 10 0.03534 0.03530 15 0.10591 0.10595 16 0.06712 0.06712 25 0.03404 0.03394 32 0.04460 0.04455 33 0.09334 0.09337 36 0.01040 0.01043 37 0.14205 0.14204 38 0.07912 0.07918 42 0.08135 0.08136 48 0.02321 0.02324
Gurobi objective: 1.92338 (z = 0.15064) HiGHS objective: 1.92338 (z = 0.15065) Maximum change: 0.00010 Average change: 0.00001 Minimum change: 0.00000
Done!
My instinct is that, once the gap gets to 5.2256630966862616e-08
, the QP solver returns the same point, so each new linearization is identical. Hard to tell, as I can't debug when running from Python.
It would be good to know why you get a segfault in h.run()
after h.passModel(model)
returns an error. It shouldn't happen. Again, it's hard to debug using Python, and I don't have time to try to reproduce it in C++.
The good news is that Gurobi took 0.07078 seconds
; HiGHS took 0.01694 seconds
:-)
A take-home from this is that, when you're developing the coding of model-building, keep the logging on, and test the return code from h.passModel(model)
, although I know that the documentation of highspy
is very limited!
Your
model.lp_.a_matrix_.start_
contains values[0, 25, 51]
so HiGHS expectsmodel.lp_.a_matrix_.index_
to be of size 51, but it's of size 50.model.hessian_.start_
should benp.append(sigma.indptr, dimension*dimension)
notnp.append(sigma.indptr, dimension + 1)
Ach damn! I'd suspected an issue with how I'd extended it but couldn't find which values were wrong before leaving. Thanks!
when you're developing the coding of model-building, keep the logging on
Is there a way to tell HiGHS what level of logging to provide? It would be useful to print warnings and errors, but skip everything else. Since it's solving a problem at each iteration there's a lot of terminal output which can obscure the important information
test the return code from
h.passModel(model)
Useful to know that provides a return code, will incorporate that! Are these the values it returns? Tried to find the docs for passModel
itself but couldn't find them (for any language, not just Python)
It would be good to know why you get a segfault in h.run() after h.passModel(model) returns an error. It shouldn't happen. Again, it's hard to debug using Python, and I don't have time to try to reproduce it in C++.
I've kept a minimum example of this error to one side in case you want it later. Specifically, in that situation where the matrix pointers/indices are setup wrong and it's non-diagonal, there's a segfault if h.writeModel(...)
is called
Running HiGHS 1.7.0 (git hash: 1ddc6c347): Copyright (c) 2024 HiGHS under MIT licence terms
ERROR: Matrix dimension validation fails on index size = 4 < 5 = number of nonzeros
ERROR: LP dimension validation (passModel) fails on a_matrix dimensions
ERROR: LP dimension validation (passModel) fails
Segmentation fault (core dumped)
and an aborted core dump if h.run()
is called
Running HiGHS 1.7.0 (git hash: 1ddc6c347): Copyright (c) 2024 HiGHS under MIT licence terms
ERROR: Matrix dimension validation fails on index size = 4 < 5 = number of nonzeros
ERROR: LP dimension validation (passModel) fails on a_matrix dimensions
ERROR: LP dimension validation (passModel) fails
Coefficient ranges:
Matrix [1e+00, 1e+00]
Cost [1e+00, 2e+00]
Bound [1e+00, 1e+00]
RHS [5e-01, 5e-01]
double free or corruption (out)
Aborted (core dumped)
I've kept a minimum example of this error to one side in case you want it later. Specifically, in that situation where the matrix pointers/indices are setup wrong and it's non-diagonal, there's a segfault if
h.writeModel(...)
is called
Thanks. If passModel
gives an error return, then I thought that the incumbent model remained empty, but there's some evidence that it isn't. I should be able to reproduce what you're observing without the specific example.
Useful to know that provides a return code, will incorporate that! Are these the values it returns? Tried to find the docs for
passModel
itself but couldn't find them (for any language, not just Python)
The most complete documentation is for the C API
https://ergo-code.github.io/HiGHS/stable/interfaces/c_api/
(since it's the one most used by developers of interfaces to HiGHS). Unfortunately, since you can't pass Python parameters by reference, some of the highspy
parameter lists are different, and the values passed by reference are returned.
The rule of thumb with HiGHS is that if the method doesn't return a structure like HighsLp
or HighsBasis
(in which case nothing can go wrong, as it's just returning a const reference to a well-defined internal data structure) it will return a status
https://ergo-code.github.io/HiGHS/dev/structures/enums/#HighsStatus
Is there a way to tell HiGHS what level of logging to provide? It would be useful to print warnings and errors, but skip everything else. Since it's solving a problem at each iteration there's a lot of terminal output which can obscure the important information
No, the INFO
logging can't be switched off independently - as with Gurobi. What you can do it switch logging off before run()
and then switch it on afterwards. By default, logging also goes to a file, but this might not work with Python.
logging also goes to a file, but this might not work with Python.
Yeah that doesn't seem to be working, at least when I first tried playing around with it. These are all good to know though, thanks!
Regression introduced, $n = 4$ HiGHS SQP now segfaults. Using h.run()
gives
free(): invalid pointer
Aborted (core dumped)
while h.writeModel(...)
gives a simple Segmentation fault (core dumped)
. Associated error log is
WARNING: LP matrix packed vector contains 1 |values| in [0, 0] less than or equal to 1e-09: ignored
ERROR: Matrix dimension validation fails on index size = 4 < 16 = number of nonzeros
ERROR: Matrix dimension validation fails on value size = 4 < 16 = number of nonzeros
so likely the same problem as last time, I've just oops a CSR index somewhere. Think it's that model.hessian_.start_
should be
np.append(sigma.indptr, sigma.indptr[-1])
not dimension*dimension
or dimension + 1
, but just double checking now.
Update: right place, wrong correction. Hopefully about to fix
An SQP method using Gurobi was added as a proof of concept while I was working out the HiGHS documentation. Now clarified that this is the HiGHS' reference QP example (with this using
passModel
as an alternative), we will seek to implement this method in HiGHS instead.Initial draft of the PR just templates the functions, nothing more. Aside from implementing the method, it'll be necessary to write some new loaders to get HiGHS what it needs. The key steps to completion (in no particular order) are: