Skip to content

Commit cc3e9b8

Browse files
authored
Merge pull request #2594 from ERGO-Code/fix-2460
Renamed user_cost_scale to user_objective_scale
2 parents 2339044 + bb38cfa commit cc3e9b8

File tree

7 files changed

+261
-10
lines changed

7 files changed

+261
-10
lines changed

highs/highs_bindings.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1206,7 +1206,7 @@ PYBIND11_MODULE(_core, m, py::mod_gil_not_used()) {
12061206
.def_readwrite("objective_target", &HighsOptions::objective_target)
12071207
.def_readwrite("threads", &HighsOptions::threads)
12081208
.def_readwrite("user_bound_scale", &HighsOptions::user_bound_scale)
1209-
.def_readwrite("user_cost_scale", &HighsOptions::user_cost_scale)
1209+
.def_readwrite("user_objective_scale", &HighsOptions::user_objective_scale)
12101210
.def_readwrite("highs_debug_level", &HighsOptions::highs_debug_level)
12111211
.def_readwrite("highs_analysis_level",
12121212
&HighsOptions::highs_analysis_level)

highs/interfaces/highs_c_api.h

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2447,6 +2447,23 @@ HighsInt Highs_getIis(void* highs, HighsInt* iis_num_col, HighsInt* iis_num_row,
24472447
HighsInt* col_index, HighsInt* row_index,
24482448
HighsInt* col_bound, HighsInt* row_bound,
24492449
HighsInt* col_status, HighsInt* row_status);
2450+
/**
2451+
* Identify suggested values of the options user_objective_scale and
2452+
* user_bound_scale to address extremely large or small objective
2453+
* coefficients and bound values
2454+
*
2455+
* @param highs A pointer to the Highs instance.
2456+
* @param HighsInt* suggested_objective_scale The suggested value of
2457+
* user_objective_scale
2458+
* @param HighsInt* suggested_bound_scale The suggested value of
2459+
* user_bound_scale
2460+
*
2461+
* @returns A `kHighsStatus` constant indicating whether the call succeeded.
2462+
*/
2463+
HighsInt Highs_getObjectiveBoundScaling(void* highs,
2464+
HighsInt* suggested_objective_scale,
2465+
HighsInt* suggested_bound_scale);
2466+
24502467
/**
24512468
* Releases all resources held by the global scheduler instance.
24522469
*

highs/lp_data/Highs.cpp

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -965,9 +965,33 @@ HighsStatus Highs::run() {
965965

966966
if (!options_.use_warm_start) this->clearSolver();
967967
this->reportModelStats();
968-
HighsInt num_linear_objective = this->multi_linear_objective_.size();
969-
if (num_linear_objective == 0) {
970-
HighsStatus status = this->optimizeModel();
968+
969+
// Possibly apply user-defined scaling to the incumbent model and solution
970+
HighsUserScaleData user_scale_data;
971+
initialiseUserScaleData(this->options_, user_scale_data);
972+
const bool user_scaling =
973+
user_scale_data.user_objective_scale || user_scale_data.user_bound_scale;
974+
if (user_scaling) {
975+
if (this->userScaleModel(user_scale_data) == HighsStatus::kError)
976+
return HighsStatus::kError;
977+
this->userScaleSolution(user_scale_data);
978+
// Indicate that the scaling has been applied
979+
user_scale_data.applied = true;
980+
// Zero the user scale values to prevent further scaling
981+
this->options_.user_objective_scale = 0;
982+
this->options_.user_bound_scale = 0;
983+
}
984+
985+
// Determine coefficient ranges and possibly warn the user about
986+
// excessive values, obtaining suggested values for user_objective_scale
987+
// and user_bound_scale
988+
assessExcessiveObjectiveBoundScaling(this->options_.log_options, this->model_,
989+
user_scale_data);
990+
// Used when deveoping unit tests in TestUserScale.cpp
991+
// this->writeModel("");
992+
HighsStatus status;
993+
if (!this->multi_linear_objective_.size()) {
994+
status = this->optimizeModel();
971995
if (options_had_highs_files) {
972996
// This call to Highs::run() had HiGHS files in options, so
973997
// recover HiGHS files to options_
@@ -981,6 +1005,44 @@ HighsStatus Highs::run() {
9811005
if (this->options_.write_basis_file != "")
9821006
status = this->writeBasis(this->options_.write_basis_file);
9831007
}
1008+
} else {
1009+
status = this->multiobjectiveSolve();
1010+
}
1011+
if (user_scaling) {
1012+
// Unscale the incumbent model and solution
1013+
//
1014+
// Flip the scaling sign
1015+
user_scale_data.user_objective_scale *= -1;
1016+
user_scale_data.user_bound_scale *= -1;
1017+
HighsStatus unscale_status = this->userScaleModel(user_scale_data);
1018+
if (unscale_status == HighsStatus::kError) {
1019+
highsLogUser(
1020+
this->options_.log_options, HighsLogType::kError,
1021+
"Unexpected error removing user scaling from the incumbent model\n");
1022+
assert(unscale_status != HighsStatus::kError);
1023+
}
1024+
const bool update_kkt = true;
1025+
unscale_status = this->userScaleSolution(user_scale_data, update_kkt);
1026+
// Restore the user scale values, remembering that they've been
1027+
// negated to undo user scaling
1028+
this->options_.user_objective_scale = -user_scale_data.user_objective_scale;
1029+
this->options_.user_bound_scale = -user_scale_data.user_bound_scale;
1030+
// Indicate that the scaling has not been applied
1031+
user_scale_data.applied = false;
1032+
highsLogUser(this->options_.log_options, HighsLogType::kInfo,
1033+
"After solving the user-scaled model, the unscaled solution "
1034+
"has objective value %.12g\n",
1035+
this->info_.objective_function_value);
1036+
if (model_status_ == HighsModelStatus::kOptimal &&
1037+
unscale_status != HighsStatus::kOk) {
1038+
// KKT errors in the unscaled optimal solution, so log a warning and
1039+
// return
1040+
highsLogUser(
1041+
this->options_.log_options, HighsLogType::kWarning,
1042+
"User scaled problem solved to optimality, but unscaled solution "
1043+
"does not satisfy feasibilty and optimality tolerances\n");
1044+
status = HighsStatus::kWarning;
1045+
}
9841046
this->reportSubSolverCallTime();
9851047
return status;
9861048
}

highs/lp_data/HighsLpUtils.cpp

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3524,3 +3524,113 @@ void getSubVectorsTranspose(const HighsIndexCollection& index_collection,
35243524
}
35253525
}
35263526
}
3527+
3528+
void initialiseUserScaleData(const HighsOptions& options,
3529+
HighsUserScaleData& user_scale_data) {
3530+
user_scale_data.initialise(options.user_objective_scale,
3531+
options.user_bound_scale, options.infinite_cost,
3532+
options.infinite_bound, options.small_matrix_value,
3533+
options.large_matrix_value);
3534+
}
3535+
3536+
void HighsUserScaleData::initialise(const HighsInt& user_objective_scale_,
3537+
const HighsInt& user_bound_scale_,
3538+
const double& infinite_cost_,
3539+
const double& infinite_bound_,
3540+
const double& small_matrix_value_,
3541+
const double& large_matrix_value_) {
3542+
this->user_objective_scale = user_objective_scale_;
3543+
this->user_bound_scale = user_bound_scale_;
3544+
this->infinite_cost = infinite_cost_;
3545+
this->infinite_bound = infinite_bound_;
3546+
this->small_matrix_value = small_matrix_value_;
3547+
this->large_matrix_value = large_matrix_value_;
3548+
this->num_infinite_costs = 0;
3549+
this->num_infinite_hessian_values = 0;
3550+
this->num_infinite_col_bounds = 0;
3551+
this->num_infinite_row_bounds = 0;
3552+
this->num_small_matrix_values = 0;
3553+
this->num_large_matrix_values = 0;
3554+
this->suggested_user_objective_scale = 0;
3555+
this->suggested_user_bound_scale = 0;
3556+
this->applied = false;
3557+
}
3558+
3559+
bool HighsUserScaleData::scaleError(std::string& message) const {
3560+
if (this->num_infinite_costs + this->num_infinite_hessian_values +
3561+
this->num_infinite_col_bounds + this->num_infinite_row_bounds +
3562+
this->num_large_matrix_values ==
3563+
0)
3564+
return false;
3565+
assert(this->user_objective_scale != 0 || this->user_bound_scale != 0);
3566+
std::stringstream ss;
3567+
ss.str(std::string());
3568+
ss << "User scaling of";
3569+
if (this->user_objective_scale != 0) {
3570+
ss << " 2**(" << this->user_objective_scale << ") for costs";
3571+
}
3572+
if (this->user_bound_scale != 0) {
3573+
if (this->user_objective_scale != 0) ss << " and";
3574+
ss << " 2**(" << this->user_bound_scale << ") for bounds";
3575+
}
3576+
ss << " yields";
3577+
if (this->num_infinite_costs) {
3578+
ss << " " << this->num_infinite_costs << " infinite cost";
3579+
if (this->num_infinite_costs > 1) ss << "s";
3580+
}
3581+
if (this->num_infinite_hessian_values) {
3582+
if (this->num_infinite_costs) {
3583+
if (this->num_infinite_col_bounds || this->num_infinite_row_bounds) {
3584+
ss << ",";
3585+
} else {
3586+
ss << " and";
3587+
}
3588+
}
3589+
ss << " " << this->num_infinite_hessian_values
3590+
<< " infinite Hessian values";
3591+
if (this->num_infinite_hessian_values > 1) ss << "s";
3592+
}
3593+
if (this->num_infinite_col_bounds) {
3594+
if (this->num_infinite_costs || this->num_infinite_hessian_values) {
3595+
if (this->num_infinite_row_bounds) {
3596+
ss << ",";
3597+
} else {
3598+
ss << " and";
3599+
}
3600+
}
3601+
ss << " " << this->num_infinite_col_bounds << " infinite column bound";
3602+
if (this->num_infinite_col_bounds > 1) ss << "s";
3603+
}
3604+
if (this->num_infinite_row_bounds) {
3605+
if (this->num_infinite_costs || this->num_infinite_hessian_values ||
3606+
this->num_infinite_col_bounds)
3607+
ss << " and";
3608+
ss << " " << this->num_infinite_row_bounds << " infinite row bound";
3609+
if (this->num_infinite_row_bounds > 1) ss << "s";
3610+
}
3611+
if (this->num_large_matrix_values) {
3612+
if (this->num_infinite_costs + this->num_infinite_hessian_values +
3613+
this->num_infinite_col_bounds + this->num_infinite_row_bounds >
3614+
0)
3615+
ss << ", and";
3616+
ss << " " << this->num_large_matrix_values << " large matrix value";
3617+
if (this->num_large_matrix_values > 1) ss << "s";
3618+
}
3619+
ss << "\n";
3620+
message = ss.str();
3621+
return true;
3622+
}
3623+
3624+
bool HighsUserScaleData::scaleWarning(std::string& message) const {
3625+
if (this->num_small_matrix_values == 0) return false;
3626+
assert(this->user_bound_scale != 0);
3627+
std::stringstream ss;
3628+
ss.str(std::string());
3629+
ss << "User scaling of 2**(" << this->user_bound_scale
3630+
<< ") for bounds yields " << this->num_small_matrix_values
3631+
<< " small matrix value";
3632+
if (this->num_small_matrix_values > 1) ss << "s";
3633+
ss << "\n";
3634+
message = ss.str();
3635+
return true;
3636+
}

highs/lp_data/HighsOptions.h

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -336,8 +336,8 @@ struct HighsOptionsStruct {
336336
double objective_bound;
337337
double objective_target;
338338
HighsInt threads;
339+
HighsInt user_objective_scale;
339340
HighsInt user_bound_scale;
340-
HighsInt user_cost_scale;
341341
HighsInt highs_debug_level;
342342
HighsInt highs_analysis_level;
343343
HighsInt simplex_strategy;
@@ -514,8 +514,8 @@ struct HighsOptionsStruct {
514514
objective_bound(0.0),
515515
objective_target(0.0),
516516
threads(0),
517+
user_objective_scale(0),
517518
user_bound_scale(0),
518-
user_cost_scale(0),
519519
highs_debug_level(0),
520520
highs_analysis_level(0),
521521
simplex_strategy(0),
@@ -825,8 +825,9 @@ class HighsOptions : public HighsOptionsStruct {
825825
records.push_back(record_int);
826826

827827
record_int = new OptionRecordInt(
828-
"user_cost_scale", "Exponent of power-of-two cost scaling for model",
829-
advanced, &user_cost_scale, -kHighsIInf, 0, kHighsIInf);
828+
"user_objective_scale",
829+
"Exponent of power-of-two objective scaling for model", advanced,
830+
&user_objective_scale, -kHighsIInf, 0, kHighsIInf);
830831
records.push_back(record_int);
831832

832833
record_int = new OptionRecordInt("highs_debug_level",

highs/lp_data/HighsSolve.cpp

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -382,8 +382,38 @@ HighsStatus solveUnconstrainedLp(const HighsOptions& options, const HighsLp& lp,
382382
return HighsStatus::kOk;
383383
}
384384

385-
void assessExcessiveBoundCost(const HighsLogOptions log_options,
386-
const HighsModel& model) {
385+
// Assuming that any user scaling in user_scale_data has been applied,
386+
// determine the model coefficient ranges, assess it for values
387+
// outside the [small, large] range, and give appropriate scaling
388+
// recommendations
389+
void assessExcessiveObjectiveBoundScaling(const HighsLogOptions log_options,
390+
const HighsModel& model,
391+
HighsUserScaleData& user_scale_data) {
392+
const HighsLp& lp = model.lp_;
393+
if (lp.num_col_ == 0 || lp.num_row_ == 0) return;
394+
const bool user_cost_or_bound_scale =
395+
user_scale_data.user_objective_scale || user_scale_data.user_bound_scale;
396+
const double small_objective_coefficient =
397+
kExcessivelySmallObjectiveCoefficient;
398+
const double large_objective_coefficient =
399+
kExcessivelyLargeObjectiveCoefficient;
400+
const double small_bound = kExcessivelySmallBoundValue;
401+
const double large_bound = kExcessivelyLargeBoundValue;
402+
std::stringstream message;
403+
if (user_cost_or_bound_scale) {
404+
if (user_scale_data.user_objective_scale)
405+
message << highsFormatToString(" user_objective_scale option value of %d",
406+
user_scale_data.user_objective_scale);
407+
if (user_scale_data.user_bound_scale) {
408+
if (user_scale_data.user_objective_scale) message << " and";
409+
message << highsFormatToString(" user_bound_scale option value of %d",
410+
user_scale_data.user_bound_scale);
411+
}
412+
highsLogUser(log_options, HighsLogType::kInfo,
413+
"Assessing costs and bounds after applying%s\n",
414+
message.str().c_str());
415+
}
416+
// Lambda for assessing a finite nonzero
387417
auto assessFiniteNonzero = [&](const double value, double& min_value,
388418
double& max_value) {
389419
double abs_value = std::abs(value);
@@ -556,6 +586,9 @@ void assessExcessiveBoundCost(const HighsLogOptions log_options,
556586
lp.user_bound_scale_ ? "User-scaled problem" : "Problem",
557587
int(suggested_bound_scale_exponent), int(suggested_user_bound_scale));
558588
}
589+
message << highsFormatToString(
590+
" setting the user_objective_scale option to %d",
591+
int(user_scale_data.suggested_user_objective_scale));
559592
}
560593
if (max_finite_row_bound > 0 &&
561594
max_finite_row_bound < kExcessivelySmallBoundValue) {

tests/test_highspy.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2252,3 +2252,31 @@ def test_get_objectives(self):
22522252
self.assertEqual(obj_expr2.vals, [1.0, 2.0, 3.0])
22532253
self.assertEqual(obj_expr2.constant, 5.0)
22542254
self.assertEqual(sense2, highspy.ObjSense.kMaximize)
2255+
2256+
def test_get_user_objective_bound_scale(self):
2257+
inf = highspy.kHighsInf
2258+
h = highspy.Highs()
2259+
h.setOptionValue("output_flag", False)
2260+
h.addVars(2, np.array([1e-8, 1e-8]), np.array([1e8, 1e8]))
2261+
h.changeColsCost(2, np.array([0, 1]), np.array([-4e6, -7e6], dtype=np.double))
2262+
num_cons = 2
2263+
lower = np.array([-inf, -2e+8], dtype=np.double)
2264+
upper = np.array([6e+8, inf], dtype=np.double)
2265+
num_new_nz = 4
2266+
starts = np.array([0, 2])
2267+
indices = np.array([0, 1, 0, 1])
2268+
values = np.array([1, 1, 1, -1], dtype=np.double)
2269+
h.addRows(num_cons, lower, upper, num_new_nz, starts, indices, values)
2270+
h.run()
2271+
unscaled_objective_value = h.getObjectiveValue()
2272+
[status, dual_objective_value] = h.getDualObjectiveValue()
2273+
self.assertAlmostEqual(unscaled_objective_value, dual_objective_value)
2274+
[status, suggested_objective_scale, suggested_bound_scale] = h.getObjectiveBoundScaling();
2275+
h.setOptionValue("user_objective_scale", suggested_objective_scale)
2276+
h.setOptionValue("user_bound_scale", suggested_bound_scale)
2277+
h.run()
2278+
scaled_objective_value = h.getObjectiveValue()
2279+
self.assertAlmostEqual(unscaled_objective_value, scaled_objective_value)
2280+
2281+
2282+

0 commit comments

Comments
 (0)