diff --git a/include/cupdlpx.h b/include/cupdlpx.h index 5ad5e11..6667f59 100644 --- a/include/cupdlpx.h +++ b/include/cupdlpx.h @@ -30,7 +30,8 @@ extern "C" const double *con_ub, const double *var_lb, const double *var_ub, - const double *objective_constant); + const double *objective_constant, + objective_sense_t objective_sense); // Set up initial primal and dual solution for an lp_problem_t void set_start_values(lp_problem_t *prob, const double *primal, const double *dual); diff --git a/include/cupdlpx_types.h b/include/cupdlpx_types.h index d874206..2d6e148 100644 --- a/include/cupdlpx_types.h +++ b/include/cupdlpx_types.h @@ -43,6 +43,12 @@ extern "C" NORM_TYPE_L_INF = 1 } norm_type_t; + typedef enum + { + OBJECTIVE_SENSE_MINIMIZE = 0, + OBJECTIVE_SENSE_MAXIMIZE = 1 + } objective_sense_t; + typedef struct { int num_variables; @@ -51,6 +57,7 @@ extern "C" double *variable_upper_bound; double *objective_vector; double objective_constant; + objective_sense_t objective_sense; int *constraint_matrix_row_pointers; int *constraint_matrix_col_indices; diff --git a/internal/internal_types.h b/internal/internal_types.h index 1302430..71d44f7 100644 --- a/internal/internal_types.h +++ b/internal/internal_types.h @@ -43,6 +43,7 @@ typedef struct double *variable_upper_bound; double *objective_vector; double objective_constant; + double original_objective_sign; cu_sparse_matrix_csr_t *constraint_matrix; cu_sparse_matrix_csr_t *constraint_matrix_t; double *constraint_lower_bound; diff --git a/internal/solver.h b/internal/solver.h index b07aba8..cc37c12 100644 --- a/internal/solver.h +++ b/internal/solver.h @@ -23,7 +23,7 @@ extern "C" { #endif - cupdlpx_result_t *optimize(const pdhg_parameters_t *params, lp_problem_t *original_problem); + cupdlpx_result_t *optimize(const pdhg_parameters_t *params, const lp_problem_t *original_problem); #ifdef __cplusplus } diff --git a/internal/utils.h b/internal/utils.h index c31fd62..ab24a13 100644 --- a/internal/utils.h +++ b/internal/utils.h @@ -124,7 +124,15 @@ extern "C" void check_termination_criteria(pdhg_solver_state_t *solver_state, const termination_criteria_t *criteria); - void print_initial_info(const pdhg_parameters_t *params, lp_problem_t *problem); + void print_initial_info(const pdhg_parameters_t *params, const lp_problem_t *problem); + + void filter_constraint_matrix_entries(lp_problem_t *out, const lp_problem_t *in, const pdhg_parameters_t *params); + + lp_problem_t preprocess_problem(const lp_problem_t *original, const pdhg_parameters_t *params); + + void free_preprocessed_problem(const lp_problem_t *preprocessed, const lp_problem_t *original); + + void restore_original_objective_sense(cupdlpx_result_t *result, objective_sense_t sense); void pdhg_final_log(const cupdlpx_result_t *result, const pdhg_parameters_t *params); diff --git a/python/cupdlpx/model.py b/python/cupdlpx/model.py index 827a6b7..873795f 100644 --- a/python/cupdlpx/model.py +++ b/python/cupdlpx/model.py @@ -355,23 +355,20 @@ def optimize(self): # check model sense if self.ModelSense not in (PDLP.MINIMIZE, PDLP.MAXIMIZE): raise ValueError("model_sense must be PDLP.MINIMIZE or PDLP.MAXIMIZE") - # determine sign - sign = 1 if self.ModelSense == PDLP.MINIMIZE else -1 - # effective objective based on sense - c_eff = sign * self.c if self.c is not None else None - c0_eff = sign * self.c0 if self.c0 is not None else None + minimize = self.ModelSense == PDLP.MINIMIZE # call the core solver info = solve_once( self.A, - c_eff, - c0_eff, + self.c, + self.c0, self.lb, self.ub, self.constr_lb, self.constr_ub, params=self._params, primal_start=self._primal_start, - dual_start=self._dual_start + dual_start=self._dual_start, + minimize=minimize, ) # solutions self._x = np.asarray(info.get("X")) if info.get("X") is not None else None @@ -380,8 +377,8 @@ def optimize(self): # objectives & gaps primal_obj_eff = info.get("PrimalObj") dual_obj_eff = info.get("DualObj") - self._objval = sign * primal_obj_eff if primal_obj_eff is not None else None - self._dualobj = sign * dual_obj_eff if dual_obj_eff is not None else None + self._objval = primal_obj_eff if primal_obj_eff is not None else None + self._dualobj = dual_obj_eff if dual_obj_eff is not None else None self._gap = info.get("ObjectiveGap") self._rel_gap = info.get("RelativeObjectiveGap") # status & counters @@ -398,8 +395,8 @@ def optimize(self): self._max_d_ray = info.get("MaxDualRayInfeas") p_ray_lin_eff = info.get("PrimalRayLinObj") d_ray_obj_eff = info.get("DualRayObj") - self._p_ray_lin_obj = sign * p_ray_lin_eff if p_ray_lin_eff is not None else None - self._d_ray_obj = sign * d_ray_obj_eff if d_ray_obj_eff is not None else None + self._p_ray_lin_obj = p_ray_lin_eff if p_ray_lin_eff is not None else None + self._d_ray_obj = d_ray_obj_eff if d_ray_obj_eff is not None else None def _clear_solution_cache(self) -> None: """ diff --git a/python_bindings/_core_bindings.cpp b/python_bindings/_core_bindings.cpp index f46924f..ca168e0 100644 --- a/python_bindings/_core_bindings.cpp +++ b/python_bindings/_core_bindings.cpp @@ -464,7 +464,8 @@ static py::dict solve_once(py::object A, py::object constraint_upper_bound, // u (optional → inf) py::object params = py::none(), // PDHG parameters (optional → default) py::object primal_start = py::none(), // warm start primal solution (optional) - py::object dual_start = py::none() // warm start dual solution (optional) + py::object dual_start = py::none(), // warm start dual solution (optional) + bool minimize = true // objective sense (true → minimize) ) { // parse matrix @@ -492,13 +493,14 @@ static py::dict solve_once(py::object A, } // build problem - lp_problem_t *prob = create_lp_problem(c_ptr, // objective vector - &view.desc, // constraint matrix - l_ptr, // constraint lower bound - u_ptr, // constraint upper bound - lb_ptr, // variable lower bound - ub_ptr, // variable upper bound - c0_ptr // objective constant + lp_problem_t *prob = create_lp_problem(c_ptr, // objective vector + &view.desc, // constraint matrix + l_ptr, // constraint lower bound + u_ptr, // constraint upper bound + lb_ptr, // variable lower bound + ub_ptr, // variable upper bound + c0_ptr, // objective constant + minimize ? OBJECTIVE_SENSE_MINIMIZE : OBJECTIVE_SENSE_MAXIMIZE // objective sense ); if (!prob) { @@ -595,5 +597,6 @@ PYBIND11_MODULE(_cupdlpx_core, m) py::arg("constraint_upper_bound") = py::none(), py::arg("params") = py::none(), py::arg("primal_start") = py::none(), - py::arg("dual_start") = py::none()); + py::arg("dual_start") = py::none(), + py::arg("minimize") = true); } diff --git a/src/cupdlpx.c b/src/cupdlpx.c index 035e520..982f855 100644 --- a/src/cupdlpx.c +++ b/src/cupdlpx.c @@ -29,7 +29,8 @@ lp_problem_t *create_lp_problem(const double *objective_c, const double *con_ub, const double *var_lb, const double *var_ub, - const double *objective_constant) + const double *objective_constant, + objective_sense_t objective_sense) { lp_problem_t *prob = (lp_problem_t *)safe_malloc(sizeof(lp_problem_t)); prob->primal_start = NULL; @@ -105,6 +106,7 @@ lp_problem_t *create_lp_problem(const double *objective_c, // default fill values prob->objective_constant = objective_constant ? *objective_constant : 0.0; + prob->objective_sense = objective_sense; fill_or_copy(&prob->objective_vector, prob->num_variables, objective_c, 0.0); fill_or_copy(&prob->variable_lower_bound, prob->num_variables, var_lb, -INFINITY); fill_or_copy(&prob->variable_upper_bound, prob->num_variables, var_ub, INFINITY); diff --git a/src/mps_parser.c b/src/mps_parser.c index c37687e..7ad6225 100644 --- a/src/mps_parser.c +++ b/src/mps_parser.c @@ -326,7 +326,7 @@ typedef struct char *objective_row_name; char *current_col_name; double objective_constant; - bool is_maximize; + objective_sense_t objective_sense; int error_flag; } MpsParserState; @@ -434,7 +434,7 @@ lp_problem_t *read_mps_file(const char *filename) if (n_tokens == 0) continue; - if (n_tokens == 1 && isalpha(tokens[0][0])) + if (isalpha((unsigned char)tokens[0][0])) { MpsSection next_section = SEC_NONE; if (strcmp(tokens[0], "ROWS") == 0) @@ -447,32 +447,51 @@ lp_problem_t *read_mps_file(const char *filename) next_section = SEC_RANGES; else if (strcmp(tokens[0], "BOUNDS") == 0) next_section = SEC_BOUNDS; - else if (strcmp(tokens[0], "OBJSENSE") == 0) + else if (strcmp(tokens[0], "OBJSENSE") == 0 || strcmp(tokens[0], "OBJSENS") == 0) next_section = SEC_OBJSENSE; else if (strcmp(tokens[0], "ENDATA") == 0) { next_section = SEC_ENDATA; } - if (current_section == SEC_ROWS && next_section != SEC_ROWS && !rows_finalized) + bool inline_max = next_section == SEC_OBJSENSE && n_tokens >= 2 && + (strcmp(tokens[1], "MAX") == 0 || strcmp(tokens[1], "MAXIMIZE") == 0); + bool inline_min = next_section == SEC_OBJSENSE && n_tokens >= 2 && + (strcmp(tokens[1], "MIN") == 0 || strcmp(tokens[1], "MINIMIZE") == 0); + bool is_header = next_section != SEC_NONE && (n_tokens == 1 || inline_max || inline_min); + + if (is_header) { - if (finalize_rows(&state) != 0) - state.error_flag = 1; - rows_finalized = true; - } + if (current_section == SEC_ROWS && next_section != SEC_ROWS && !rows_finalized) + { + if (finalize_rows(&state) != 0) + state.error_flag = 1; + rows_finalized = true; + } - current_section = next_section; - if (current_section == SEC_ENDATA) - break; - continue; + current_section = next_section; + if (current_section == SEC_ENDATA) + break; + + if (inline_max) + state.objective_sense = OBJECTIVE_SENSE_MAXIMIZE; + else if (inline_min) + state.objective_sense = OBJECTIVE_SENSE_MINIMIZE; + + continue; + } } switch (current_section) { case SEC_OBJSENSE: - if (n_tokens > 0 && (strcmp(tokens[0], "MAX") == 0 || strcmp(tokens[0], "MAXIMIZE") == 0)) + if (strcmp(tokens[0], "MAX") == 0 || strcmp(tokens[0], "MAXIMIZE") == 0) + { + state.objective_sense = OBJECTIVE_SENSE_MAXIMIZE; + } + else if (strcmp(tokens[0], "MIN") == 0 || strcmp(tokens[0], "MINIMIZE") == 0) { - state.is_maximize = true; + state.objective_sense = OBJECTIVE_SENSE_MINIMIZE; } break; case SEC_ROWS: @@ -515,7 +534,8 @@ lp_problem_t *read_mps_file(const char *filename) prob->num_variables = state.col_map.size; prob->num_constraints = state.row_map.size; prob->constraint_matrix_num_nonzeros = state.coo_matrix.nnz; - prob->objective_constant = state.is_maximize ? -state.objective_constant : state.objective_constant; + prob->objective_constant = state.objective_constant; + prob->objective_sense = state.objective_sense; prob->objective_vector = state.objective_coeffs; prob->variable_lower_bound = state.var_lower_bounds; @@ -532,14 +552,6 @@ lp_problem_t *read_mps_file(const char *filename) state.constraint_lower_bounds = NULL; state.constraint_upper_bounds = NULL; - if (state.is_maximize) - { - for (int i = 0; i < prob->num_variables; ++i) - { - prob->objective_vector[i] *= -1.0; - } - } - if (mps_coo_to_csr(prob, &state.coo_matrix, prob->num_constraints) != 0) { fprintf(stderr, "ERROR: Failed to convert matrix to CSR format.\n"); diff --git a/src/presolve.c b/src/presolve.c index 44b5437..67b7b20 100644 --- a/src/presolve.c +++ b/src/presolve.c @@ -39,6 +39,7 @@ lp_problem_t *convert_pslp_to_cupdlpx(PresolvedProblem *reduced_prob, const lp_p cupdlpx_prob->dual_start = NULL; cupdlpx_prob->objective_constant = original_prob->objective_constant + reduced_prob->obj_offset; + cupdlpx_prob->objective_sense = original_prob->objective_sense; cupdlpx_prob->objective_vector = reduced_prob->c; cupdlpx_prob->constraint_lower_bound = reduced_prob->lhs; @@ -188,6 +189,10 @@ void pslp_postsolve(const cupdlpx_presolve_info_t *info, cupdlpx_result_t *resul result->num_reduced_nonzeros = info->presolver->reduced_prob->nnz; result->presolve_status = info->presolve_status; + free(result->primal_solution); + free(result->dual_solution); + free(result->reduced_cost); + result->primal_solution = (double *)safe_malloc(original_prob->num_variables * sizeof(double)); result->dual_solution = (double *)safe_malloc(original_prob->num_constraints * sizeof(double)); result->reduced_cost = (double *)safe_malloc(original_prob->num_variables * sizeof(double)); diff --git a/src/solver.cu b/src/solver.cu index 7ad9301..816a6ba 100644 --- a/src/solver.cu +++ b/src/solver.cu @@ -62,7 +62,8 @@ static cupdlpx_result_t *create_result_from_state(pdhg_solver_state_t *state, co static void perform_restart(pdhg_solver_state_t *state, const pdhg_parameters_t *params); static void initialize_step_size_and_primal_weight(pdhg_solver_state_t *state, const pdhg_parameters_t *params); static pdhg_solver_state_t *initialize_solver_state(const lp_problem_t *working_problem, - const pdhg_parameters_t *params); + const pdhg_parameters_t *params, + objective_sense_t original_objective_sense); static void compute_fixed_point_error(pdhg_solver_state_t *state); void pdhg_solver_state_free(pdhg_solver_state_t *state); void rescale_info_free(rescale_info_t *info); @@ -127,28 +128,32 @@ static void sync_step_sizes_to_gpu(pdhg_solver_state_t *state); void sync_inner_count_to_gpu(pdhg_solver_state_t *state); static void check_params_validity(const pdhg_parameters_t *params); -cupdlpx_result_t *optimize(const pdhg_parameters_t *params, lp_problem_t *original_problem) +cupdlpx_result_t *optimize(const pdhg_parameters_t *params, const lp_problem_t *original_problem) { check_params_validity(params); + print_initial_info(params, original_problem); - cupdlpx_presolve_info_t *presolve_info = NULL; - const lp_problem_t *working_problem = original_problem; + lp_problem_t preprocessed_problem = preprocess_problem(original_problem, params); + const lp_problem_t *working_problem = &preprocessed_problem; + cupdlpx_presolve_info_t *presolve_info = NULL; if (params->presolve) { - presolve_info = pslp_presolve(original_problem, params); + presolve_info = pslp_presolve(&preprocessed_problem, params); if (presolve_info->problem_solved_during_presolve) { - cupdlpx_result_t *result = create_result_from_presolve(presolve_info, original_problem); + cupdlpx_result_t *result = create_result_from_presolve(presolve_info, &preprocessed_problem); cupdlpx_presolve_info_free(presolve_info); + restore_original_objective_sense(result, original_problem->objective_sense); pdhg_final_log(result, params); + free_preprocessed_problem(&preprocessed_problem, original_problem); return result; } working_problem = presolve_info->reduced_problem; } - pdhg_solver_state_t *state = initialize_solver_state(working_problem, params); + pdhg_solver_state_t *state = initialize_solver_state(working_problem, params, original_problem->objective_sense); display_iteration_stats(state, params->verbose); initialize_step_size_and_primal_weight(state, params); @@ -244,17 +249,20 @@ cupdlpx_result_t *optimize(const pdhg_parameters_t *params, lp_problem_t *origin feasibility_polish(params, state); } - cupdlpx_result_t *result = create_result_from_state(state, original_problem); + cupdlpx_result_t *result = create_result_from_state(state, &preprocessed_problem); if (params->presolve && presolve_info) { - pslp_postsolve(presolve_info, result, original_problem); + pslp_postsolve(presolve_info, result, &preprocessed_problem); cupdlpx_presolve_info_free(presolve_info); } - pdhg_final_log(result, params); pdhg_solver_state_free(state); CUDA_CHECK(cudaGetLastError()); + + restore_original_objective_sense(result, original_problem->objective_sense); + pdhg_final_log(result, params); + free_preprocessed_problem(&preprocessed_problem, original_problem); return result; } @@ -314,7 +322,8 @@ __global__ void compute_and_rescale_reduced_cost_kernel(double *__restrict__ red } static pdhg_solver_state_t *initialize_solver_state(const lp_problem_t *working_problem, - const pdhg_parameters_t *params) + const pdhg_parameters_t *params, + objective_sense_t original_objective_sense) { pdhg_solver_state_t *state = (pdhg_solver_state_t *)safe_calloc(1, sizeof(pdhg_solver_state_t)); @@ -327,6 +336,7 @@ static pdhg_solver_state_t *initialize_solver_state(const lp_problem_t *working_ state->num_variables = n_vars; state->num_constraints = n_cons; state->objective_constant = working_problem->objective_constant; + state->original_objective_sign = (original_objective_sense == OBJECTIVE_SENSE_MAXIMIZE) ? -1.0 : 1.0; state->constraint_matrix = (cu_sparse_matrix_csr_t *)safe_malloc(sizeof(cu_sparse_matrix_csr_t)); state->constraint_matrix_t = (cu_sparse_matrix_csr_t *)safe_malloc(sizeof(cu_sparse_matrix_csr_t)); diff --git a/src/utils.cu b/src/utils.cu index 5f9f303..edef51d 100644 --- a/src/utils.cu +++ b/src/utils.cu @@ -331,25 +331,25 @@ void set_default_parameters(pdhg_parameters_t *params) params->matrix_zero_tol = 1e-9; } -static void filter_constraint_matrix_entries(lp_problem_t *problem, const pdhg_parameters_t *params) +void filter_constraint_matrix_entries(lp_problem_t *out, const lp_problem_t *in, const pdhg_parameters_t *params) { - if (problem == NULL) + if (out == NULL || in == NULL) { fprintf(stderr, "Error: problem pointer is NULL.\n"); exit(EXIT_FAILURE); } - const int num_rows = problem->num_constraints; - const int nnz = problem->constraint_matrix_num_nonzeros; + const int num_rows = in->num_constraints; + const int nnz = in->constraint_matrix_num_nonzeros; if (num_rows == 0 || nnz == 0) { return; } - const int *row_ptr = problem->constraint_matrix_row_pointers; - const int *col_ind = problem->constraint_matrix_col_indices; - const double *vals = problem->constraint_matrix_values; + const int *row_ptr = in->constraint_matrix_row_pointers; + const int *col_ind = in->constraint_matrix_col_indices; + const double *vals = in->constraint_matrix_values; if (!row_ptr || !col_ind || !vals) { @@ -374,6 +374,15 @@ static void filter_constraint_matrix_entries(lp_problem_t *problem, const pdhg_p return; } + if (params->verbose) + { + const int dropped = nnz - filtered_nnz; + printf("Dropped %d near-zero %s (|value| <= %.1e) from the constraint matrix\n", + dropped, + (dropped == 1 ? "entry" : "entries"), + params->matrix_zero_tol); + } + const size_t alloc_nnz = (filtered_nnz > 0) ? (size_t)filtered_nnz : 1; int *new_row_ptr = (int *)safe_malloc((size_t)(num_rows + 1) * sizeof(int)); int *new_col_ind = (int *)safe_malloc(alloc_nnz * sizeof(int)); @@ -397,24 +406,58 @@ static void filter_constraint_matrix_entries(lp_problem_t *problem, const pdhg_p new_row_ptr[i + 1] = pos; } - free(problem->constraint_matrix_row_pointers); - free(problem->constraint_matrix_col_indices); - free(problem->constraint_matrix_values); + out->constraint_matrix_row_pointers = new_row_ptr; + out->constraint_matrix_col_indices = new_col_ind; + out->constraint_matrix_values = new_vals; + out->constraint_matrix_num_nonzeros = filtered_nnz; +} - problem->constraint_matrix_row_pointers = new_row_ptr; - problem->constraint_matrix_col_indices = new_col_ind; - problem->constraint_matrix_values = new_vals; - problem->constraint_matrix_num_nonzeros = filtered_nnz; +lp_problem_t preprocess_problem(const lp_problem_t *original, const pdhg_parameters_t *params) +{ + lp_problem_t working = *original; + if (original->objective_sense == OBJECTIVE_SENSE_MAXIMIZE) + { + double *negated = (double *)safe_malloc((size_t)original->num_variables * sizeof(double)); + for (int i = 0; i < original->num_variables; ++i) + { + negated[i] = -original->objective_vector[i]; + } + working.objective_vector = negated; + working.objective_constant = -original->objective_constant; + working.objective_sense = OBJECTIVE_SENSE_MINIMIZE; + } + filter_constraint_matrix_entries(&working, original, params); + return working; +} - if (((nnz - filtered_nnz) > 0) && params->verbose) +void free_preprocessed_problem(const lp_problem_t *preprocessed, const lp_problem_t *original) +{ + if (preprocessed->objective_vector != original->objective_vector) + free(preprocessed->objective_vector); + if (preprocessed->constraint_matrix_values != original->constraint_matrix_values) { - printf("Dropped %d near-zero %s (|value| <= %.1e) from the constraint matrix\n", - nnz - filtered_nnz, - (nnz - filtered_nnz == 1 ? "entry" : "entries"), - params->matrix_zero_tol); + free(preprocessed->constraint_matrix_row_pointers); + free(preprocessed->constraint_matrix_col_indices); + free(preprocessed->constraint_matrix_values); } } +void restore_original_objective_sense(cupdlpx_result_t *result, objective_sense_t sense) +{ + if (result == NULL || sense != OBJECTIVE_SENSE_MAXIMIZE) + return; + if (result->dual_solution != NULL) + for (int i = 0; i < result->num_constraints; ++i) + result->dual_solution[i] = -result->dual_solution[i]; + if (result->reduced_cost != NULL) + for (int i = 0; i < result->num_variables; ++i) + result->reduced_cost[i] = -result->reduced_cost[i]; + result->primal_objective_value = -result->primal_objective_value; + result->dual_objective_value = -result->dual_objective_value; + result->primal_ray_linear_objective = -result->primal_ray_linear_objective; + result->dual_ray_objective = -result->dual_ray_objective; +} + #define PRINT_DIFF_INT(name, current, default_val) \ do \ { \ @@ -442,7 +485,7 @@ static void filter_constraint_matrix_entries(lp_problem_t *problem, const pdhg_p } \ } while (0) -void print_initial_info(const pdhg_parameters_t *params, lp_problem_t *problem) +void print_initial_info(const pdhg_parameters_t *params, const lp_problem_t *problem) { pdhg_parameters_t default_params; set_default_parameters(&default_params); @@ -462,7 +505,6 @@ void print_initial_info(const pdhg_parameters_t *params, lp_problem_t *problem) printf("---------------------------------------------------------------------" "------------------\n"); - filter_constraint_matrix_entries(problem, params); printf("Problem: %d rows, %d columns, %d nonzeros\n", problem->num_constraints, problem->num_variables, @@ -560,8 +602,8 @@ void display_iteration_stats(const pdhg_solver_state_t *state, bool verbose) printf("%6d %.1e | %8.1e %8.1e | %.1e %.1e %.1e | %.1e %.1e %.1e \n", state->total_count, state->cumulative_time_sec, - state->primal_objective_value, - state->dual_objective_value, + state->original_objective_sign * state->primal_objective_value, + state->original_objective_sign * state->dual_objective_value, state->absolute_primal_residual, state->absolute_dual_residual, state->objective_gap, @@ -1169,7 +1211,7 @@ void display_feas_polish_iteration_stats(const pdhg_solver_state_t *state, bool printf("%6d %.1e | %8.1e | %.1e | %.1e \n", state->total_count, state->cumulative_time_sec, - state->primal_objective_value, + state->original_objective_sign * state->primal_objective_value, state->absolute_primal_residual, state->relative_primal_residual); } @@ -1178,7 +1220,7 @@ void display_feas_polish_iteration_stats(const pdhg_solver_state_t *state, bool printf("%6d %.1e | %8.1e | %.1e | %.1e \n", state->total_count, state->cumulative_time_sec, - state->dual_objective_value, + state->original_objective_sign * state->dual_objective_value, state->absolute_dual_residual, state->relative_dual_residual); } diff --git a/test/test_basic.py b/test/test_basic.py index e69d4e6..8ffee5f 100644 --- a/test/test_basic.py +++ b/test/test_basic.py @@ -68,7 +68,9 @@ def test_maximize_solution_correct(base_lp_data, atol): x2 <= 2 3*x1 + 2*x2 <= 8 x1, x2 >= 0 - Optimal solution: x* = (1.5, 1.75), y* = (-0.25, 0, -0.25), objective = 3.25 + Optimal solution: x* = (1.5, 1.75), y* = (0.25, 0, 0.25), objective = 3.25 + (duals are reported in the maximization sense: a binding <= constraint has a + nonnegative shadow price.) """ # setup model c, A, l, u, lb, ub = base_lp_data @@ -90,7 +92,7 @@ def test_maximize_solution_correct(base_lp_data, atol): assert np.allclose(model.X, [1.5, 1.75], atol=atol), f"Unexpected primal solution: {model.X}" # check dual solution assert hasattr(model, "Pi"), "Model.Pi (dual solution) not exposed." - assert np.allclose(model.Pi, [-0.25, 0, -0.25], atol=atol), f"Unexpected dual solution: {model.Pi}" + assert np.allclose(model.Pi, [0.25, 0, 0.25], atol=atol), f"Unexpected dual solution: {model.Pi}" # check objective assert hasattr(model, "ObjVal"), "Model.ObjVal (objective value) not exposed." assert np.isclose(model.ObjVal, 3.25, atol=atol), f"Unexpected objective value: {model.ObjVal}" \ No newline at end of file