1use std::ffi::CStr;
23use std::os::raw::c_void;
24use std::time::Instant;
25
26use crate::{
27 SolverInterface, ffi,
28 types::{RowBatch, SolutionView, SolverError, SolverStatistics, StageTemplate},
29};
30
31enum OptionValue {
40 Str(&'static CStr),
42 Int(i32),
44 Bool(i32),
46 Double(f64),
48}
49
50struct DefaultOption {
52 name: &'static CStr,
53 value: OptionValue,
54}
55
56impl DefaultOption {
57 unsafe fn apply(&self, handle: *mut c_void) -> i32 {
63 unsafe {
64 match &self.value {
65 OptionValue::Str(val) => {
66 ffi::cobre_highs_set_string_option(handle, self.name.as_ptr(), val.as_ptr())
67 }
68 OptionValue::Int(val) => {
69 ffi::cobre_highs_set_int_option(handle, self.name.as_ptr(), *val)
70 }
71 OptionValue::Bool(val) => {
72 ffi::cobre_highs_set_bool_option(handle, self.name.as_ptr(), *val)
73 }
74 OptionValue::Double(val) => {
75 ffi::cobre_highs_set_double_option(handle, self.name.as_ptr(), *val)
76 }
77 }
78 }
79 }
80}
81
82fn default_options() -> [DefaultOption; 8] {
91 [
92 DefaultOption {
93 name: c"solver",
94 value: OptionValue::Str(c"simplex"),
95 },
96 DefaultOption {
97 name: c"simplex_strategy",
98 value: OptionValue::Int(1), },
100 DefaultOption {
101 name: c"simplex_scale_strategy",
102 value: OptionValue::Int(0), },
104 DefaultOption {
105 name: c"presolve",
106 value: OptionValue::Str(c"off"),
107 },
108 DefaultOption {
109 name: c"parallel",
110 value: OptionValue::Str(c"off"),
111 },
112 DefaultOption {
113 name: c"output_flag",
114 value: OptionValue::Bool(0),
115 },
116 DefaultOption {
117 name: c"primal_feasibility_tolerance",
118 value: OptionValue::Double(1e-7),
119 },
120 DefaultOption {
121 name: c"dual_feasibility_tolerance",
122 value: OptionValue::Double(1e-7),
123 },
124 ]
125}
126
127pub struct HighsSolver {
144 handle: *mut c_void,
146 col_value: Vec<f64>,
149 col_dual: Vec<f64>,
152 row_value: Vec<f64>,
155 row_dual: Vec<f64>,
158 scratch_i32: Vec<i32>,
162 basis_col_i32: Vec<i32>,
166 basis_row_i32: Vec<i32>,
170 num_cols: usize,
172 num_rows: usize,
174 has_model: bool,
177 stats: SolverStatistics,
180}
181
182unsafe impl Send for HighsSolver {}
190
191struct RetryOutcome {
196 attempts: u64,
197 solve_time: f64,
198 iterations: u64,
199 level: u32,
201}
202
203impl HighsSolver {
204 pub fn new() -> Result<Self, SolverError> {
229 let handle = unsafe { ffi::cobre_highs_create() };
234
235 if handle.is_null() {
236 return Err(SolverError::InternalError {
237 message: "HiGHS instance creation failed: Highs_create() returned null".to_string(),
238 error_code: None,
239 });
240 }
241
242 if let Err(e) = Self::apply_default_config(handle) {
245 unsafe { ffi::cobre_highs_destroy(handle) };
250 return Err(e);
251 }
252
253 Ok(Self {
254 handle,
255 col_value: Vec::new(),
256 col_dual: Vec::new(),
257 row_value: Vec::new(),
258 row_dual: Vec::new(),
259 scratch_i32: Vec::new(),
260 basis_col_i32: Vec::new(),
261 basis_row_i32: Vec::new(),
262 num_cols: 0,
263 num_rows: 0,
264 has_model: false,
265 stats: SolverStatistics::default(),
266 })
267 }
268
269 fn apply_default_config(handle: *mut c_void) -> Result<(), SolverError> {
275 for opt in &default_options() {
276 let status = unsafe { opt.apply(handle) };
278 if status == ffi::HIGHS_STATUS_ERROR {
279 return Err(SolverError::InternalError {
280 message: format!(
281 "HiGHS configuration failed: {}",
282 opt.name.to_str().unwrap_or("?")
283 ),
284 error_code: Some(status),
285 });
286 }
287 }
288 Ok(())
289 }
290
291 fn extract_solution_view(&mut self, solve_time_seconds: f64) -> SolutionView<'_> {
298 let status = unsafe {
300 ffi::cobre_highs_get_solution(
301 self.handle,
302 self.col_value.as_mut_ptr(),
303 self.col_dual.as_mut_ptr(),
304 self.row_value.as_mut_ptr(),
305 self.row_dual.as_mut_ptr(),
306 )
307 };
308 assert_ne!(
309 status,
310 ffi::HIGHS_STATUS_ERROR,
311 "cobre_highs_get_solution failed after optimal solve"
312 );
313
314 let objective = unsafe { ffi::cobre_highs_get_objective_value(self.handle) };
316
317 #[allow(clippy::cast_sign_loss)]
319 let iterations =
320 unsafe { ffi::cobre_highs_get_simplex_iteration_count(self.handle) } as u64;
321
322 SolutionView {
323 objective,
324 primal: &self.col_value[..self.num_cols],
325 dual: &self.row_dual[..self.num_rows],
326 reduced_costs: &self.col_dual[..self.num_cols],
327 iterations,
328 solve_time_seconds,
329 }
330 }
331
332 fn restore_default_settings(&mut self) {
339 for opt in &default_options() {
340 let status = unsafe { opt.apply(self.handle) };
342 debug_assert_eq!(
343 status,
344 ffi::HIGHS_STATUS_OK,
345 "restore_default_settings: option {:?} failed with status {status}",
346 opt.name,
347 );
348 }
349 }
350
351 fn run_once(&mut self) -> i32 {
353 let run_status = unsafe { ffi::cobre_highs_run(self.handle) };
355 if run_status == ffi::HIGHS_STATUS_ERROR {
356 return ffi::HIGHS_MODEL_STATUS_SOLVE_ERROR;
357 }
358 unsafe { ffi::cobre_highs_get_model_status(self.handle) }
360 }
361
362 fn set_iteration_limits(&mut self) {
374 let simplex_iter_limit = self.num_cols.saturating_mul(50).max(100_000);
375 unsafe {
378 #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
379 ffi::cobre_highs_set_int_option(
380 self.handle,
381 c"simplex_iteration_limit".as_ptr(),
382 simplex_iter_limit as i32,
383 );
384 ffi::cobre_highs_set_int_option(self.handle, c"ipm_iteration_limit".as_ptr(), 10_000);
385 }
386 }
387
388 fn restore_iteration_limits(&mut self) {
392 unsafe {
394 ffi::cobre_highs_set_int_option(
395 self.handle,
396 c"simplex_iteration_limit".as_ptr(),
397 i32::MAX,
398 );
399 ffi::cobre_highs_set_int_option(self.handle, c"ipm_iteration_limit".as_ptr(), i32::MAX);
400 }
401 }
402
403 fn interpret_terminal_status(
408 &mut self,
409 status: i32,
410 solve_time_seconds: f64,
411 ) -> Option<SolverError> {
412 match status {
413 ffi::HIGHS_MODEL_STATUS_OPTIMAL => {
414 None
416 }
417 ffi::HIGHS_MODEL_STATUS_INFEASIBLE => Some(SolverError::Infeasible),
418 ffi::HIGHS_MODEL_STATUS_UNBOUNDED_OR_INFEASIBLE => {
419 let mut has_dual_ray: i32 = 0;
423 let mut dual_buf = vec![0.0_f64; self.num_rows];
426 let dual_status = unsafe {
428 ffi::cobre_highs_get_dual_ray(
429 self.handle,
430 &raw mut has_dual_ray,
431 dual_buf.as_mut_ptr(),
432 )
433 };
434 if dual_status != ffi::HIGHS_STATUS_ERROR && has_dual_ray != 0 {
435 return Some(SolverError::Infeasible);
436 }
437 let mut has_primal_ray: i32 = 0;
438 let mut primal_buf = vec![0.0_f64; self.num_cols];
439 let primal_status = unsafe {
441 ffi::cobre_highs_get_primal_ray(
442 self.handle,
443 &raw mut has_primal_ray,
444 primal_buf.as_mut_ptr(),
445 )
446 };
447 if primal_status != ffi::HIGHS_STATUS_ERROR && has_primal_ray != 0 {
448 return Some(SolverError::Unbounded);
449 }
450 Some(SolverError::Infeasible)
451 }
452 ffi::HIGHS_MODEL_STATUS_UNBOUNDED => Some(SolverError::Unbounded),
453 ffi::HIGHS_MODEL_STATUS_TIME_LIMIT => Some(SolverError::TimeLimitExceeded {
454 elapsed_seconds: solve_time_seconds,
455 }),
456 ffi::HIGHS_MODEL_STATUS_ITERATION_LIMIT => {
457 #[allow(clippy::cast_sign_loss)]
459 let iterations =
460 unsafe { ffi::cobre_highs_get_simplex_iteration_count(self.handle) } as u64;
461 Some(SolverError::IterationLimit { iterations })
462 }
463 ffi::HIGHS_MODEL_STATUS_SOLVE_ERROR | ffi::HIGHS_MODEL_STATUS_UNKNOWN => {
464 None
466 }
467 other => Some(SolverError::InternalError {
468 message: format!("HiGHS returned unexpected model status {other}"),
469 error_code: Some(other),
470 }),
471 }
472 }
473
474 fn convert_to_i32_scratch(&mut self, source: &[usize]) -> &[i32] {
478 if source.len() > self.scratch_i32.len() {
479 self.scratch_i32.resize(source.len(), 0);
480 }
481 for (i, &v) in source.iter().enumerate() {
482 debug_assert!(
483 i32::try_from(v).is_ok(),
484 "usize index {v} overflows i32::MAX at position {i}"
485 );
486 #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
488 {
489 self.scratch_i32[i] = v as i32;
490 }
491 }
492 &self.scratch_i32[..source.len()]
493 }
494
495 fn retry_escalation(&mut self, is_unbounded: bool) -> Result<RetryOutcome, (u64, SolverError)> {
505 let phase1_wall_budget = 15.0_f64;
532 let phase2_wall_budget = 30.0_f64;
533 let overall_budget = 120.0_f64;
534 let num_retry_levels = 12_u32;
535
536 let retry_start = Instant::now();
537 let mut retry_attempts: u64 = 0;
538 let mut terminal_err: Option<SolverError> = None;
539 let mut found_optimal = false;
540 let mut optimal_time = 0.0_f64;
541 let mut optimal_iterations: u64 = 0;
542 let mut optimal_level = 0_u32;
543
544 for level in 0..num_retry_levels {
545 if retry_start.elapsed().as_secs_f64() >= overall_budget {
547 break;
548 }
549
550 self.apply_retry_level_options(level);
551
552 retry_attempts += 1;
553
554 let t_retry = Instant::now();
555 let retry_status = self.run_once();
556 let retry_time = t_retry.elapsed().as_secs_f64();
557
558 if retry_status == ffi::HIGHS_MODEL_STATUS_OPTIMAL {
559 #[allow(clippy::cast_sign_loss)]
562 let iters =
563 unsafe { ffi::cobre_highs_get_simplex_iteration_count(self.handle) } as u64;
564 found_optimal = true;
565 optimal_time = retry_time;
566 optimal_iterations = iters;
567 optimal_level = level;
568 break;
569 }
570
571 let level_budget = if level <= 4 {
577 phase1_wall_budget
578 } else {
579 phase2_wall_budget
580 };
581 let budget_exceeded = retry_time > level_budget;
582 let retryable = retry_status == ffi::HIGHS_MODEL_STATUS_UNBOUNDED
583 || retry_status == ffi::HIGHS_MODEL_STATUS_ITERATION_LIMIT
584 || budget_exceeded;
585 if !retryable {
586 if let Some(e) = self.interpret_terminal_status(retry_status, retry_time) {
587 terminal_err = Some(e);
588 break;
589 }
590 }
591 }
594
595 self.restore_default_settings();
599 self.restore_iteration_limits();
600 unsafe {
601 ffi::cobre_highs_set_int_option(self.handle, c"user_objective_scale".as_ptr(), 0);
602 ffi::cobre_highs_set_int_option(self.handle, c"user_bound_scale".as_ptr(), 0);
603 }
604
605 if found_optimal {
606 return Ok(RetryOutcome {
607 attempts: retry_attempts,
608 solve_time: optimal_time,
609 iterations: optimal_iterations,
610 level: optimal_level,
611 });
612 }
613
614 Err((
615 retry_attempts,
616 terminal_err.unwrap_or_else(|| {
617 if is_unbounded {
619 SolverError::Unbounded
620 } else {
621 SolverError::NumericalDifficulty {
622 message:
623 "HiGHS failed to reach optimality after all retry escalation levels"
624 .to_string(),
625 }
626 }
627 }),
628 ))
629 }
630
631 fn apply_retry_level_options(&mut self, level: u32) {
644 match level {
645 0 => {
649 unsafe { ffi::cobre_highs_clear_solver(self.handle) };
650 self.set_iteration_limits();
651 }
652 1 => unsafe {
654 ffi::cobre_highs_set_string_option(
655 self.handle,
656 c"presolve".as_ptr(),
657 c"on".as_ptr(),
658 );
659 },
660 2 => unsafe {
663 ffi::cobre_highs_set_int_option(self.handle, c"simplex_strategy".as_ptr(), 1);
664 },
665 3 => unsafe {
668 ffi::cobre_highs_set_double_option(
669 self.handle,
670 c"primal_feasibility_tolerance".as_ptr(),
671 1e-6,
672 );
673 ffi::cobre_highs_set_double_option(
674 self.handle,
675 c"dual_feasibility_tolerance".as_ptr(),
676 1e-6,
677 );
678 },
679 4 => unsafe {
682 ffi::cobre_highs_set_string_option(
683 self.handle,
684 c"solver".as_ptr(),
685 c"ipm".as_ptr(),
686 );
687 },
688
689 _ => self.apply_extended_retry_options(level),
693 }
694 }
695
696 fn apply_extended_retry_options(&mut self, level: u32) {
702 self.restore_default_settings();
703 self.set_iteration_limits();
704 unsafe {
707 ffi::cobre_highs_set_string_option(self.handle, c"presolve".as_ptr(), c"on".as_ptr());
708 }
709 match level {
710 5 => unsafe {
711 ffi::cobre_highs_set_int_option(self.handle, c"simplex_scale_strategy".as_ptr(), 3);
712 },
713 6 => unsafe {
714 ffi::cobre_highs_set_int_option(self.handle, c"simplex_strategy".as_ptr(), 1);
715 ffi::cobre_highs_set_int_option(self.handle, c"simplex_scale_strategy".as_ptr(), 4);
716 },
717 7 => unsafe {
718 ffi::cobre_highs_set_int_option(self.handle, c"simplex_scale_strategy".as_ptr(), 3);
719 ffi::cobre_highs_set_double_option(
720 self.handle,
721 c"primal_feasibility_tolerance".as_ptr(),
722 1e-6,
723 );
724 ffi::cobre_highs_set_double_option(
725 self.handle,
726 c"dual_feasibility_tolerance".as_ptr(),
727 1e-6,
728 );
729 },
730 8 => unsafe {
731 ffi::cobre_highs_set_int_option(self.handle, c"user_objective_scale".as_ptr(), -10);
732 },
733 9 => unsafe {
734 ffi::cobre_highs_set_int_option(self.handle, c"simplex_strategy".as_ptr(), 1);
735 ffi::cobre_highs_set_int_option(self.handle, c"user_objective_scale".as_ptr(), -10);
736 ffi::cobre_highs_set_int_option(self.handle, c"user_bound_scale".as_ptr(), -5);
737 },
738 10 => unsafe {
739 ffi::cobre_highs_set_int_option(self.handle, c"user_objective_scale".as_ptr(), -13);
740 ffi::cobre_highs_set_int_option(self.handle, c"user_bound_scale".as_ptr(), -8);
741 ffi::cobre_highs_set_double_option(
742 self.handle,
743 c"primal_feasibility_tolerance".as_ptr(),
744 1e-6,
745 );
746 ffi::cobre_highs_set_double_option(
747 self.handle,
748 c"dual_feasibility_tolerance".as_ptr(),
749 1e-6,
750 );
751 },
752 11 => unsafe {
753 ffi::cobre_highs_set_string_option(
754 self.handle,
755 c"solver".as_ptr(),
756 c"ipm".as_ptr(),
757 );
758 ffi::cobre_highs_set_int_option(self.handle, c"user_objective_scale".as_ptr(), -10);
759 ffi::cobre_highs_set_int_option(self.handle, c"user_bound_scale".as_ptr(), -5);
760 ffi::cobre_highs_set_double_option(
761 self.handle,
762 c"primal_feasibility_tolerance".as_ptr(),
763 1e-6,
764 );
765 ffi::cobre_highs_set_double_option(
766 self.handle,
767 c"dual_feasibility_tolerance".as_ptr(),
768 1e-6,
769 );
770 },
771 _ => unreachable!(),
772 }
773 }
774}
775
776impl Drop for HighsSolver {
777 fn drop(&mut self) {
778 unsafe { ffi::cobre_highs_destroy(self.handle) };
780 }
781}
782
783impl SolverInterface for HighsSolver {
784 fn name(&self) -> &'static str {
785 "HiGHS"
786 }
787
788 fn load_model(&mut self, template: &StageTemplate) {
789 let t0 = Instant::now();
790 assert!(
800 i32::try_from(template.num_cols).is_ok(),
801 "num_cols {} overflows i32: LP exceeds HiGHS API limit",
802 template.num_cols
803 );
804 assert!(
805 i32::try_from(template.num_rows).is_ok(),
806 "num_rows {} overflows i32: LP exceeds HiGHS API limit",
807 template.num_rows
808 );
809 assert!(
810 i32::try_from(template.num_nz).is_ok(),
811 "num_nz {} overflows i32: LP exceeds HiGHS API limit",
812 template.num_nz
813 );
814 #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
816 let num_col = template.num_cols as i32;
817 #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
818 let num_row = template.num_rows as i32;
819 #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
820 let num_nz = template.num_nz as i32;
821 let status = unsafe {
822 ffi::cobre_highs_pass_lp(
823 self.handle,
824 num_col,
825 num_row,
826 num_nz,
827 ffi::HIGHS_MATRIX_FORMAT_COLWISE,
828 ffi::HIGHS_OBJ_SENSE_MINIMIZE,
829 0.0, template.objective.as_ptr(),
831 template.col_lower.as_ptr(),
832 template.col_upper.as_ptr(),
833 template.row_lower.as_ptr(),
834 template.row_upper.as_ptr(),
835 template.col_starts.as_ptr(),
836 template.row_indices.as_ptr(),
837 template.values.as_ptr(),
838 )
839 };
840
841 assert_ne!(
842 status,
843 ffi::HIGHS_STATUS_ERROR,
844 "cobre_highs_pass_lp failed with status {status}"
845 );
846
847 self.num_cols = template.num_cols;
848 self.num_rows = template.num_rows;
849 self.has_model = true;
850
851 self.col_value.resize(self.num_cols, 0.0);
854 self.col_dual.resize(self.num_cols, 0.0);
855 self.row_value.resize(self.num_rows, 0.0);
856 self.row_dual.resize(self.num_rows, 0.0);
857
858 self.basis_col_i32.resize(self.num_cols, 0);
861 self.basis_row_i32.resize(self.num_rows, 0);
862 self.stats.total_load_model_time_seconds += t0.elapsed().as_secs_f64();
863 self.stats.load_model_count += 1;
864 }
865
866 fn add_rows(&mut self, cuts: &RowBatch) {
867 let t0 = Instant::now();
868 assert!(
869 i32::try_from(cuts.num_rows).is_ok(),
870 "cuts.num_rows {} overflows i32: RowBatch exceeds HiGHS API limit",
871 cuts.num_rows
872 );
873 assert!(
874 i32::try_from(cuts.col_indices.len()).is_ok(),
875 "cuts nnz {} overflows i32: RowBatch exceeds HiGHS API limit",
876 cuts.col_indices.len()
877 );
878 #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
880 let num_new_row = cuts.num_rows as i32;
881 #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
882 let num_new_nz = cuts.col_indices.len() as i32;
883
884 let status = unsafe {
892 ffi::cobre_highs_add_rows(
893 self.handle,
894 num_new_row,
895 cuts.row_lower.as_ptr(),
896 cuts.row_upper.as_ptr(),
897 num_new_nz,
898 cuts.row_starts.as_ptr(),
899 cuts.col_indices.as_ptr(),
900 cuts.values.as_ptr(),
901 )
902 };
903
904 assert_ne!(
905 status,
906 ffi::HIGHS_STATUS_ERROR,
907 "cobre_highs_add_rows failed with status {status}"
908 );
909
910 self.num_rows += cuts.num_rows;
911
912 self.row_value.resize(self.num_rows, 0.0);
914 self.row_dual.resize(self.num_rows, 0.0);
915
916 self.basis_row_i32.resize(self.num_rows, 0);
918 self.stats.total_add_rows_time_seconds += t0.elapsed().as_secs_f64();
919 self.stats.add_rows_count += 1;
920 }
921
922 fn set_row_bounds(&mut self, indices: &[usize], lower: &[f64], upper: &[f64]) {
923 assert!(
924 indices.len() == lower.len() && indices.len() == upper.len(),
925 "set_row_bounds: indices ({}), lower ({}), and upper ({}) must have equal length",
926 indices.len(),
927 lower.len(),
928 upper.len()
929 );
930 if indices.is_empty() {
931 return;
932 }
933
934 assert!(
935 i32::try_from(indices.len()).is_ok(),
936 "set_row_bounds: indices.len() {} overflows i32",
937 indices.len()
938 );
939 #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
940 let num_entries = indices.len() as i32;
941
942 let t0 = Instant::now();
943 let status = unsafe {
950 ffi::cobre_highs_change_rows_bounds_by_set(
951 self.handle,
952 num_entries,
953 self.convert_to_i32_scratch(indices).as_ptr(),
954 lower.as_ptr(),
955 upper.as_ptr(),
956 )
957 };
958
959 assert_ne!(
960 status,
961 ffi::HIGHS_STATUS_ERROR,
962 "cobre_highs_change_rows_bounds_by_set failed with status {status}"
963 );
964 self.stats.total_set_bounds_time_seconds += t0.elapsed().as_secs_f64();
965 }
966
967 fn set_col_bounds(&mut self, indices: &[usize], lower: &[f64], upper: &[f64]) {
968 assert!(
969 indices.len() == lower.len() && indices.len() == upper.len(),
970 "set_col_bounds: indices ({}), lower ({}), and upper ({}) must have equal length",
971 indices.len(),
972 lower.len(),
973 upper.len()
974 );
975 if indices.is_empty() {
976 return;
977 }
978
979 assert!(
980 i32::try_from(indices.len()).is_ok(),
981 "set_col_bounds: indices.len() {} overflows i32",
982 indices.len()
983 );
984 #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
985 let num_entries = indices.len() as i32;
986
987 let t0 = Instant::now();
988 let status = unsafe {
994 ffi::cobre_highs_change_cols_bounds_by_set(
995 self.handle,
996 num_entries,
997 self.convert_to_i32_scratch(indices).as_ptr(),
998 lower.as_ptr(),
999 upper.as_ptr(),
1000 )
1001 };
1002
1003 assert_ne!(
1004 status,
1005 ffi::HIGHS_STATUS_ERROR,
1006 "cobre_highs_change_cols_bounds_by_set failed with status {status}"
1007 );
1008 self.stats.total_set_bounds_time_seconds += t0.elapsed().as_secs_f64();
1009 }
1010
1011 fn solve(&mut self) -> Result<SolutionView<'_>, SolverError> {
1012 assert!(
1013 self.has_model,
1014 "solve called without a loaded model — call load_model first"
1015 );
1016
1017 self.set_iteration_limits();
1023
1024 let t0 = Instant::now();
1025 let model_status = self.run_once();
1026 let solve_time = t0.elapsed().as_secs_f64();
1027
1028 self.stats.solve_count += 1;
1029
1030 if model_status == ffi::HIGHS_MODEL_STATUS_OPTIMAL {
1031 #[allow(clippy::cast_sign_loss)]
1036 let iterations =
1037 unsafe { ffi::cobre_highs_get_simplex_iteration_count(self.handle) } as u64;
1038 self.stats.success_count += 1;
1039 self.stats.first_try_successes += 1;
1040 self.stats.total_iterations += iterations;
1041 self.stats.total_solve_time_seconds += solve_time;
1042 self.restore_iteration_limits();
1043 return Ok(self.extract_solution_view(solve_time));
1044 }
1045
1046 let is_unbounded = model_status == ffi::HIGHS_MODEL_STATUS_UNBOUNDED;
1057 let initial_retryable = is_unbounded
1058 || model_status == ffi::HIGHS_MODEL_STATUS_ITERATION_LIMIT
1059 || model_status == ffi::HIGHS_MODEL_STATUS_TIME_LIMIT
1060 || solve_time > 15.0;
1061 if !initial_retryable {
1062 if let Some(terminal_err) = self.interpret_terminal_status(model_status, solve_time) {
1063 self.restore_iteration_limits();
1064 self.stats.failure_count += 1;
1065 return Err(terminal_err);
1066 }
1067 }
1068
1069 match self.retry_escalation(is_unbounded) {
1071 Ok(outcome) => {
1072 self.stats.retry_count += outcome.attempts;
1073 self.stats.success_count += 1;
1074 self.stats.total_iterations += outcome.iterations;
1075 self.stats.total_solve_time_seconds += outcome.solve_time;
1076 self.stats.retry_level_histogram[outcome.level as usize] += 1;
1077 Ok(self.extract_solution_view(outcome.solve_time))
1078 }
1079 Err((attempts, err)) => {
1080 self.stats.retry_count += attempts;
1081 self.stats.failure_count += 1;
1082 Err(err)
1083 }
1084 }
1085 }
1086
1087 fn reset(&mut self) {
1088 let status = unsafe { ffi::cobre_highs_clear_solver(self.handle) };
1093 debug_assert_ne!(
1094 status,
1095 ffi::HIGHS_STATUS_ERROR,
1096 "cobre_highs_clear_solver failed — HiGHS internal state may be inconsistent"
1097 );
1098 self.num_cols = 0;
1100 self.num_rows = 0;
1101 self.has_model = false;
1102 }
1105
1106 fn get_basis(&mut self, out: &mut crate::types::Basis) {
1107 assert!(
1108 self.has_model,
1109 "get_basis called without a loaded model — call load_model first"
1110 );
1111
1112 out.col_status.resize(self.num_cols, 0);
1113 out.row_status.resize(self.num_rows, 0);
1114
1115 let get_status = unsafe {
1121 ffi::cobre_highs_get_basis(
1122 self.handle,
1123 out.col_status.as_mut_ptr(),
1124 out.row_status.as_mut_ptr(),
1125 )
1126 };
1127
1128 assert_ne!(
1129 get_status,
1130 ffi::HIGHS_STATUS_ERROR,
1131 "cobre_highs_get_basis failed: basis must exist after a successful solve (programming error)"
1132 );
1133 }
1134
1135 fn solve_with_basis(
1136 &mut self,
1137 basis: &crate::types::Basis,
1138 ) -> Result<crate::types::SolutionView<'_>, SolverError> {
1139 assert!(
1140 self.has_model,
1141 "solve_with_basis called without a loaded model — call load_model first"
1142 );
1143 assert!(
1144 basis.col_status.len() == self.num_cols,
1145 "basis column count {} does not match LP column count {}",
1146 basis.col_status.len(),
1147 self.num_cols
1148 );
1149
1150 self.stats.basis_offered += 1;
1152
1153 self.basis_col_i32[..self.num_cols].copy_from_slice(&basis.col_status);
1156
1157 let basis_rows = basis.row_status.len();
1161 let lp_rows = self.num_rows;
1162 let copy_len = basis_rows.min(lp_rows);
1163 self.basis_row_i32[..copy_len].copy_from_slice(&basis.row_status[..copy_len]);
1164 if lp_rows > basis_rows {
1165 self.basis_row_i32[basis_rows..lp_rows].fill(ffi::HIGHS_BASIS_STATUS_BASIC);
1166 }
1167
1168 let basis_set_start = Instant::now();
1175 let set_status = unsafe {
1176 ffi::cobre_highs_set_basis(
1177 self.handle,
1178 self.basis_col_i32.as_ptr(),
1179 self.basis_row_i32.as_ptr(),
1180 )
1181 };
1182 self.stats.total_basis_set_time_seconds += basis_set_start.elapsed().as_secs_f64();
1183
1184 if set_status == ffi::HIGHS_STATUS_ERROR {
1186 self.stats.basis_rejections += 1;
1187 debug_assert!(false, "raw basis rejected; falling back to cold-start");
1188 }
1189
1190 self.solve()
1192 }
1193
1194 fn statistics(&self) -> SolverStatistics {
1195 self.stats.clone()
1196 }
1197}
1198
1199#[cfg(feature = "test-support")]
1205impl HighsSolver {
1206 #[must_use]
1214 pub fn raw_handle(&self) -> *mut std::os::raw::c_void {
1215 self.handle
1216 }
1217}
1218
1219#[cfg(test)]
1220mod tests {
1221 use super::HighsSolver;
1222 use crate::{
1223 SolverInterface,
1224 types::{Basis, RowBatch, StageTemplate},
1225 };
1226
1227 fn make_fixture_stage_template() -> StageTemplate {
1240 StageTemplate {
1241 num_cols: 3,
1242 num_rows: 2,
1243 num_nz: 3,
1244 col_starts: vec![0_i32, 2, 2, 3],
1245 row_indices: vec![0_i32, 1, 1],
1246 values: vec![1.0, 2.0, 1.0],
1247 col_lower: vec![0.0, 0.0, 0.0],
1248 col_upper: vec![10.0, f64::INFINITY, 8.0],
1249 objective: vec![0.0, 1.0, 50.0],
1250 row_lower: vec![6.0, 14.0],
1251 row_upper: vec![6.0, 14.0],
1252 n_state: 1,
1253 n_transfer: 0,
1254 n_dual_relevant: 1,
1255 n_hydro: 1,
1256 max_par_order: 0,
1257 col_scale: Vec::new(),
1258 row_scale: Vec::new(),
1259 }
1260 }
1261
1262 fn make_fixture_row_batch() -> RowBatch {
1266 RowBatch {
1267 num_rows: 2,
1268 row_starts: vec![0_i32, 2, 4],
1269 col_indices: vec![0_i32, 1, 0, 1],
1270 values: vec![-5.0, 1.0, 3.0, 1.0],
1271 row_lower: vec![20.0, 80.0],
1272 row_upper: vec![f64::INFINITY, f64::INFINITY],
1273 }
1274 }
1275
1276 #[test]
1277 fn test_highs_solver_create_and_name() {
1278 let solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1279 assert_eq!(solver.name(), "HiGHS");
1280 }
1282
1283 #[test]
1284 fn test_highs_solver_send_bound() {
1285 fn assert_send<T: Send>() {}
1286 assert_send::<HighsSolver>();
1287 }
1288
1289 #[test]
1290 fn test_highs_solver_statistics_initial() {
1291 let solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1292 let stats = solver.statistics();
1293 assert_eq!(stats.solve_count, 0);
1294 assert_eq!(stats.success_count, 0);
1295 assert_eq!(stats.failure_count, 0);
1296 assert_eq!(stats.total_iterations, 0);
1297 assert_eq!(stats.retry_count, 0);
1298 assert_eq!(stats.total_solve_time_seconds, 0.0);
1299 }
1300
1301 #[test]
1302 fn test_highs_load_model_updates_dimensions() {
1303 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1304 let template = make_fixture_stage_template();
1305
1306 solver.load_model(&template);
1307
1308 assert_eq!(solver.num_cols, 3, "num_cols must be 3 after load_model");
1309 assert_eq!(solver.num_rows, 2, "num_rows must be 2 after load_model");
1310 assert_eq!(
1311 solver.col_value.len(),
1312 3,
1313 "col_value buffer must be resized to num_cols"
1314 );
1315 assert_eq!(
1316 solver.col_dual.len(),
1317 3,
1318 "col_dual buffer must be resized to num_cols"
1319 );
1320 assert_eq!(
1321 solver.row_value.len(),
1322 2,
1323 "row_value buffer must be resized to num_rows"
1324 );
1325 assert_eq!(
1326 solver.row_dual.len(),
1327 2,
1328 "row_dual buffer must be resized to num_rows"
1329 );
1330 }
1331
1332 #[test]
1333 fn test_highs_add_rows_updates_dimensions() {
1334 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1335 let template = make_fixture_stage_template();
1336 let cuts = make_fixture_row_batch();
1337
1338 solver.load_model(&template);
1339 solver.add_rows(&cuts);
1340
1341 assert_eq!(solver.num_rows, 4, "num_rows must be 4 after add_rows");
1343 assert_eq!(
1344 solver.row_dual.len(),
1345 4,
1346 "row_dual buffer must be resized to 4 after add_rows"
1347 );
1348 assert_eq!(
1349 solver.row_value.len(),
1350 4,
1351 "row_value buffer must be resized to 4 after add_rows"
1352 );
1353 assert_eq!(solver.num_cols, 3, "num_cols must be unchanged by add_rows");
1355 }
1356
1357 #[test]
1358 fn test_highs_set_row_bounds_no_panic() {
1359 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1360 let template = make_fixture_stage_template();
1361 solver.load_model(&template);
1362
1363 solver.set_row_bounds(&[0], &[4.0], &[4.0]);
1365 }
1366
1367 #[test]
1368 fn test_highs_set_col_bounds_no_panic() {
1369 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1370 let template = make_fixture_stage_template();
1371 solver.load_model(&template);
1372
1373 solver.set_col_bounds(&[1], &[10.0], &[f64::INFINITY]);
1375 }
1376
1377 #[test]
1378 fn test_highs_set_bounds_empty_no_panic() {
1379 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1380 let template = make_fixture_stage_template();
1381 solver.load_model(&template);
1382
1383 solver.set_row_bounds(&[], &[], &[]);
1385 solver.set_col_bounds(&[], &[], &[]);
1386 }
1387
1388 #[test]
1391 fn test_highs_solve_basic_lp() {
1392 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1393 let template = make_fixture_stage_template();
1394 solver.load_model(&template);
1395
1396 let solution = solver
1397 .solve()
1398 .expect("solve() must succeed on a feasible LP");
1399
1400 assert!(
1401 (solution.objective - 100.0).abs() < 1e-8,
1402 "objective must be 100.0, got {}",
1403 solution.objective
1404 );
1405 assert_eq!(solution.primal.len(), 3, "primal must have 3 elements");
1406 assert!(
1407 (solution.primal[0] - 6.0).abs() < 1e-8,
1408 "primal[0] (x0) must be 6.0, got {}",
1409 solution.primal[0]
1410 );
1411 assert!(
1412 (solution.primal[1] - 0.0).abs() < 1e-8,
1413 "primal[1] (x1) must be 0.0, got {}",
1414 solution.primal[1]
1415 );
1416 assert!(
1417 (solution.primal[2] - 2.0).abs() < 1e-8,
1418 "primal[2] (x2) must be 2.0, got {}",
1419 solution.primal[2]
1420 );
1421 }
1422
1423 #[test]
1427 fn test_highs_solve_with_cuts() {
1428 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1429 let template = make_fixture_stage_template();
1430 let cuts = make_fixture_row_batch();
1431 solver.load_model(&template);
1432 solver.add_rows(&cuts);
1433
1434 let solution = solver
1435 .solve()
1436 .expect("solve() must succeed on a feasible LP with cuts");
1437
1438 assert!(
1439 (solution.objective - 162.0).abs() < 1e-8,
1440 "objective must be 162.0, got {}",
1441 solution.objective
1442 );
1443 assert!(
1444 (solution.primal[0] - 6.0).abs() < 1e-8,
1445 "primal[0] must be 6.0, got {}",
1446 solution.primal[0]
1447 );
1448 assert!(
1449 (solution.primal[1] - 62.0).abs() < 1e-8,
1450 "primal[1] must be 62.0, got {}",
1451 solution.primal[1]
1452 );
1453 assert!(
1454 (solution.primal[2] - 2.0).abs() < 1e-8,
1455 "primal[2] must be 2.0, got {}",
1456 solution.primal[2]
1457 );
1458 }
1459
1460 #[test]
1463 fn test_highs_solve_after_rhs_patch() {
1464 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1465 let template = make_fixture_stage_template();
1466 let cuts = make_fixture_row_batch();
1467 solver.load_model(&template);
1468 solver.add_rows(&cuts);
1469
1470 solver.set_row_bounds(&[0], &[4.0], &[4.0]);
1472
1473 let solution = solver
1474 .solve()
1475 .expect("solve() must succeed after RHS patch");
1476
1477 assert!(
1478 (solution.objective - 368.0).abs() < 1e-8,
1479 "objective must be 368.0, got {}",
1480 solution.objective
1481 );
1482 }
1483
1484 #[test]
1486 fn test_highs_solve_statistics_increment() {
1487 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1488 let template = make_fixture_stage_template();
1489 solver.load_model(&template);
1490
1491 solver.solve().expect("first solve must succeed");
1492 solver.solve().expect("second solve must succeed");
1493
1494 let stats = solver.statistics();
1495 assert_eq!(stats.solve_count, 2, "solve_count must be 2");
1496 assert_eq!(stats.success_count, 2, "success_count must be 2");
1497 assert_eq!(stats.failure_count, 0, "failure_count must be 0");
1498 assert!(
1499 stats.total_iterations > 0,
1500 "total_iterations must be positive"
1501 );
1502 }
1503
1504 #[test]
1506 fn test_highs_reset_preserves_stats() {
1507 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1508 let template = make_fixture_stage_template();
1509 solver.load_model(&template);
1510 solver.solve().expect("solve must succeed");
1511
1512 let stats_before = solver.statistics();
1513 assert_eq!(
1514 stats_before.solve_count, 1,
1515 "solve_count must be 1 before reset"
1516 );
1517
1518 solver.reset();
1519
1520 let stats_after = solver.statistics();
1521 assert_eq!(
1522 stats_after.solve_count, stats_before.solve_count,
1523 "solve_count must be unchanged after reset"
1524 );
1525 assert_eq!(
1526 stats_after.success_count, stats_before.success_count,
1527 "success_count must be unchanged after reset"
1528 );
1529 assert_eq!(
1530 stats_after.total_iterations, stats_before.total_iterations,
1531 "total_iterations must be unchanged after reset"
1532 );
1533 }
1534
1535 #[test]
1537 fn test_highs_solve_iterations_positive() {
1538 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1539 let template = make_fixture_stage_template();
1540 solver.load_model(&template);
1541
1542 let solution = solver.solve().expect("solve must succeed");
1543 assert!(
1544 solution.iterations > 0,
1545 "iterations must be positive, got {}",
1546 solution.iterations
1547 );
1548 }
1549
1550 #[test]
1552 fn test_highs_solve_time_positive() {
1553 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1554 let template = make_fixture_stage_template();
1555 solver.load_model(&template);
1556
1557 let solution = solver.solve().expect("solve must succeed");
1558 assert!(
1559 solution.solve_time_seconds > 0.0,
1560 "solve_time_seconds must be positive, got {}",
1561 solution.solve_time_seconds
1562 );
1563 }
1564
1565 #[test]
1568 fn test_highs_solve_statistics_single() {
1569 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1570 let template = make_fixture_stage_template();
1571 solver.load_model(&template);
1572
1573 solver.solve().expect("solve must succeed");
1574
1575 let stats = solver.statistics();
1576 assert_eq!(stats.solve_count, 1, "solve_count must be 1");
1577 assert_eq!(stats.success_count, 1, "success_count must be 1");
1578 assert_eq!(stats.failure_count, 0, "failure_count must be 0");
1579 assert!(
1580 stats.total_iterations > 0,
1581 "total_iterations must be positive after a successful solve"
1582 );
1583 }
1584
1585 #[test]
1588 fn test_get_basis_valid_status_codes() {
1589 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1590 let template = make_fixture_stage_template();
1591 solver.load_model(&template);
1592 solver.solve().expect("solve must succeed before get_basis");
1593
1594 let mut basis = Basis::new(0, 0);
1595 solver.get_basis(&mut basis);
1596
1597 for &code in &basis.col_status {
1598 assert!(
1599 (0..=4).contains(&code),
1600 "col_status code {code} is outside valid HiGHS range 0..=4"
1601 );
1602 }
1603 for &code in &basis.row_status {
1604 assert!(
1605 (0..=4).contains(&code),
1606 "row_status code {code} is outside valid HiGHS range 0..=4"
1607 );
1608 }
1609 }
1610
1611 #[test]
1614 fn test_get_basis_resizes_output() {
1615 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1616 let template = make_fixture_stage_template();
1617 solver.load_model(&template);
1618 solver.solve().expect("solve must succeed before get_basis");
1619
1620 let mut basis = Basis::new(0, 0);
1621 assert_eq!(
1622 basis.col_status.len(),
1623 0,
1624 "initial col_status must be empty"
1625 );
1626 assert_eq!(
1627 basis.row_status.len(),
1628 0,
1629 "initial row_status must be empty"
1630 );
1631
1632 solver.get_basis(&mut basis);
1633
1634 assert_eq!(
1635 basis.col_status.len(),
1636 3,
1637 "col_status must be resized to 3 (num_cols of SS1.1)"
1638 );
1639 assert_eq!(
1640 basis.row_status.len(),
1641 2,
1642 "row_status must be resized to 2 (num_rows of SS1.1)"
1643 );
1644 }
1645
1646 #[test]
1649 fn test_solve_with_basis_warm_start() {
1650 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1651 let template = make_fixture_stage_template();
1652 solver.load_model(&template);
1653 solver.solve().expect("cold-start solve must succeed");
1654
1655 let mut basis = Basis::new(0, 0);
1656 solver.get_basis(&mut basis);
1657
1658 solver.load_model(&template);
1660 let result = solver
1661 .solve_with_basis(&basis)
1662 .expect("warm-start solve must succeed");
1663
1664 assert!(
1665 (result.objective - 100.0).abs() < 1e-8,
1666 "warm-start objective must be 100.0, got {}",
1667 result.objective
1668 );
1669 assert!(
1670 result.iterations <= 1,
1671 "warm-start from exact basis must use at most 1 iteration, got {}",
1672 result.iterations
1673 );
1674
1675 let stats = solver.statistics();
1676 assert_eq!(
1677 stats.basis_rejections, 0,
1678 "basis_rejections must be 0 when raw basis is accepted, got {}",
1679 stats.basis_rejections
1680 );
1681 }
1682
1683 #[test]
1687 fn test_solve_with_basis_dimension_mismatch() {
1688 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1689 let template = make_fixture_stage_template();
1690 let cuts = make_fixture_row_batch();
1691
1692 solver.load_model(&template);
1694 solver.solve().expect("SS1.1 solve must succeed");
1695 let mut basis = Basis::new(0, 0);
1696 solver.get_basis(&mut basis);
1697 assert_eq!(
1698 basis.row_status.len(),
1699 2,
1700 "captured basis must have 2 row statuses"
1701 );
1702
1703 solver.load_model(&template);
1705 solver.add_rows(&cuts);
1706 assert_eq!(solver.num_rows, 4, "LP must have 4 rows after add_rows");
1707
1708 let result = solver
1710 .solve_with_basis(&basis)
1711 .expect("solve with dimension-mismatched basis must succeed");
1712
1713 assert!(
1714 (result.objective - 162.0).abs() < 1e-8,
1715 "objective with both cuts active must be 162.0, got {}",
1716 result.objective
1717 );
1718 }
1719}
1720
1721#[cfg(test)]
1733#[allow(clippy::doc_markdown)]
1734mod research_tests_ticket_023 {
1735 unsafe fn research_load_ss11_lp(highs: *mut std::os::raw::c_void) {
1746 use crate::ffi;
1747 let col_cost: [f64; 3] = [0.0, 1.0, 50.0];
1748 let col_lower: [f64; 3] = [0.0, 0.0, 0.0];
1749 let col_upper: [f64; 3] = [10.0, f64::INFINITY, 8.0];
1750 let row_lower: [f64; 2] = [6.0, 14.0];
1751 let row_upper: [f64; 2] = [6.0, 14.0];
1752 let a_start: [i32; 4] = [0, 2, 2, 3];
1753 let a_index: [i32; 3] = [0, 1, 1];
1754 let a_value: [f64; 3] = [1.0, 2.0, 1.0];
1755 let status = unsafe {
1757 ffi::cobre_highs_pass_lp(
1758 highs,
1759 3,
1760 2,
1761 3,
1762 ffi::HIGHS_MATRIX_FORMAT_COLWISE,
1763 ffi::HIGHS_OBJ_SENSE_MINIMIZE,
1764 0.0,
1765 col_cost.as_ptr(),
1766 col_lower.as_ptr(),
1767 col_upper.as_ptr(),
1768 row_lower.as_ptr(),
1769 row_upper.as_ptr(),
1770 a_start.as_ptr(),
1771 a_index.as_ptr(),
1772 a_value.as_ptr(),
1773 )
1774 };
1775 assert_eq!(
1776 status,
1777 ffi::HIGHS_STATUS_OK,
1778 "research_load_ss11_lp pass_lp failed"
1779 );
1780 }
1781
1782 #[test]
1788 fn test_research_probe_limit_status_on_ss11_lp() {
1789 use crate::ffi;
1790
1791 let highs = unsafe { ffi::cobre_highs_create() };
1793 assert!(!highs.is_null());
1794 unsafe { ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0) };
1795 unsafe { research_load_ss11_lp(highs) };
1796 let _ = unsafe { ffi::cobre_highs_set_double_option(highs, c"time_limit".as_ptr(), 0.0) };
1797 let run_status = unsafe { ffi::cobre_highs_run(highs) };
1798 let model_status = unsafe { ffi::cobre_highs_get_model_status(highs) };
1799 let obj = unsafe { ffi::cobre_highs_get_objective_value(highs) };
1800 eprintln!(
1801 "SS1.1 + time_limit=0: run_status={run_status}, model_status={model_status}, obj={obj}"
1802 );
1803 unsafe { ffi::cobre_highs_destroy(highs) };
1804
1805 let highs = unsafe { ffi::cobre_highs_create() };
1807 assert!(!highs.is_null());
1808 unsafe { ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0) };
1809 unsafe { research_load_ss11_lp(highs) };
1810 let _ = unsafe {
1811 ffi::cobre_highs_set_int_option(highs, c"simplex_iteration_limit".as_ptr(), 0)
1812 };
1813 let run_status = unsafe { ffi::cobre_highs_run(highs) };
1814 let model_status = unsafe { ffi::cobre_highs_get_model_status(highs) };
1815 let obj = unsafe { ffi::cobre_highs_get_objective_value(highs) };
1816 eprintln!(
1817 "SS1.1 + iteration_limit=0: run_status={run_status}, model_status={model_status}, obj={obj}"
1818 );
1819 unsafe { ffi::cobre_highs_destroy(highs) };
1820 }
1821
1822 unsafe fn research_load_larger_lp(highs: *mut std::os::raw::c_void) {
1842 use crate::ffi;
1843 let col_cost: [f64; 5] = [1.0, 1.0, 1.0, 1.0, 1.0];
1844 let col_lower: [f64; 5] = [0.0; 5];
1845 let col_upper: [f64; 5] = [100.0; 5];
1846 let row_lower: [f64; 4] = [10.0, 8.0, 6.0, 4.0];
1847 let row_upper: [f64; 4] = [f64::INFINITY; 4];
1848 let a_start: [i32; 6] = [0, 1, 3, 5, 7, 8];
1850 let a_index: [i32; 8] = [0, 0, 1, 1, 2, 2, 3, 3];
1851 let a_value: [f64; 8] = [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0];
1852 let status = unsafe {
1854 ffi::cobre_highs_pass_lp(
1855 highs,
1856 5,
1857 4,
1858 8,
1859 ffi::HIGHS_MATRIX_FORMAT_COLWISE,
1860 ffi::HIGHS_OBJ_SENSE_MINIMIZE,
1861 0.0,
1862 col_cost.as_ptr(),
1863 col_lower.as_ptr(),
1864 col_upper.as_ptr(),
1865 row_lower.as_ptr(),
1866 row_upper.as_ptr(),
1867 a_start.as_ptr(),
1868 a_index.as_ptr(),
1869 a_value.as_ptr(),
1870 )
1871 };
1872 assert_eq!(
1873 status,
1874 ffi::HIGHS_STATUS_OK,
1875 "research_load_larger_lp pass_lp failed"
1876 );
1877 }
1878
1879 #[test]
1888 fn test_research_time_limit_zero_triggers_time_limit_status() {
1889 use crate::ffi;
1890
1891 let highs = unsafe { ffi::cobre_highs_create() };
1892 assert!(!highs.is_null());
1893 unsafe { ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0) };
1894 unsafe { research_load_larger_lp(highs) };
1895
1896 let opt_status =
1897 unsafe { ffi::cobre_highs_set_double_option(highs, c"time_limit".as_ptr(), 0.0) };
1898 assert_eq!(opt_status, ffi::HIGHS_STATUS_OK);
1899
1900 let run_status = unsafe { ffi::cobre_highs_run(highs) };
1901 let model_status = unsafe { ffi::cobre_highs_get_model_status(highs) };
1902
1903 eprintln!(
1904 "time_limit=0 on larger LP: run_status={run_status}, model_status={model_status}"
1905 );
1906
1907 assert_eq!(
1908 run_status,
1909 ffi::HIGHS_STATUS_WARNING,
1910 "time_limit=0 must return HIGHS_STATUS_WARNING (1), got {run_status}"
1911 );
1912 assert_eq!(
1913 model_status,
1914 ffi::HIGHS_MODEL_STATUS_TIME_LIMIT,
1915 "time_limit=0 must give MODEL_STATUS_TIME_LIMIT (13), got {model_status}"
1916 );
1917
1918 unsafe { ffi::cobre_highs_destroy(highs) };
1919 }
1920
1921 #[test]
1930 fn test_research_iteration_limit_zero_triggers_iteration_limit_status() {
1931 use crate::ffi;
1932
1933 let highs = unsafe { ffi::cobre_highs_create() };
1934 assert!(!highs.is_null());
1935 unsafe { ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0) };
1936 unsafe { ffi::cobre_highs_set_string_option(highs, c"presolve".as_ptr(), c"off".as_ptr()) };
1938 unsafe { research_load_larger_lp(highs) };
1939
1940 let opt_status = unsafe {
1941 ffi::cobre_highs_set_int_option(highs, c"simplex_iteration_limit".as_ptr(), 0)
1942 };
1943 assert_eq!(opt_status, ffi::HIGHS_STATUS_OK);
1944
1945 let run_status = unsafe { ffi::cobre_highs_run(highs) };
1946 let model_status = unsafe { ffi::cobre_highs_get_model_status(highs) };
1947
1948 eprintln!(
1949 "iteration_limit=0 on larger LP: run_status={run_status}, model_status={model_status}"
1950 );
1951
1952 assert_eq!(
1953 run_status,
1954 ffi::HIGHS_STATUS_WARNING,
1955 "iteration_limit=0 must return HIGHS_STATUS_WARNING (1), got {run_status}"
1956 );
1957 assert_eq!(
1958 model_status,
1959 ffi::HIGHS_MODEL_STATUS_ITERATION_LIMIT,
1960 "iteration_limit=0 must give MODEL_STATUS_ITERATION_LIMIT (14), got {model_status}"
1961 );
1962
1963 unsafe { ffi::cobre_highs_destroy(highs) };
1964 }
1965
1966 #[test]
1972 fn test_research_partial_solution_availability() {
1973 use crate::ffi;
1974
1975 {
1977 let highs = unsafe { ffi::cobre_highs_create() };
1978 assert!(!highs.is_null());
1979 unsafe { ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0) };
1980 unsafe { research_load_larger_lp(highs) };
1981 unsafe { ffi::cobre_highs_set_double_option(highs, c"time_limit".as_ptr(), 0.0) };
1982 unsafe { ffi::cobre_highs_run(highs) };
1983
1984 let obj = unsafe { ffi::cobre_highs_get_objective_value(highs) };
1985 let model_status = unsafe { ffi::cobre_highs_get_model_status(highs) };
1986 assert_eq!(model_status, ffi::HIGHS_MODEL_STATUS_TIME_LIMIT);
1987 eprintln!("TIME_LIMIT: obj={obj}, finite={}", obj.is_finite());
1988 unsafe { ffi::cobre_highs_destroy(highs) };
1989 }
1990
1991 {
1993 let highs = unsafe { ffi::cobre_highs_create() };
1994 assert!(!highs.is_null());
1995 unsafe { ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0) };
1996 unsafe {
1997 ffi::cobre_highs_set_string_option(highs, c"presolve".as_ptr(), c"off".as_ptr())
1998 };
1999 unsafe { research_load_larger_lp(highs) };
2000 unsafe {
2001 ffi::cobre_highs_set_int_option(highs, c"simplex_iteration_limit".as_ptr(), 0)
2002 };
2003 unsafe { ffi::cobre_highs_run(highs) };
2004
2005 let obj = unsafe { ffi::cobre_highs_get_objective_value(highs) };
2006 let model_status = unsafe { ffi::cobre_highs_get_model_status(highs) };
2007 assert_eq!(model_status, ffi::HIGHS_MODEL_STATUS_ITERATION_LIMIT);
2008 eprintln!("ITERATION_LIMIT: obj={obj}, finite={}", obj.is_finite());
2009 unsafe { ffi::cobre_highs_destroy(highs) };
2010 }
2011 }
2012
2013 #[test]
2016 fn test_research_restore_defaults_allows_subsequent_optimal_solve() {
2017 use crate::ffi;
2018
2019 let highs = unsafe { ffi::cobre_highs_create() };
2020 assert!(!highs.is_null());
2021
2022 unsafe { ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0) };
2023
2024 unsafe {
2026 ffi::cobre_highs_set_string_option(highs, c"solver".as_ptr(), c"simplex".as_ptr());
2027 ffi::cobre_highs_set_int_option(highs, c"simplex_strategy".as_ptr(), 1);
2028 ffi::cobre_highs_set_string_option(highs, c"presolve".as_ptr(), c"off".as_ptr());
2029 ffi::cobre_highs_set_string_option(highs, c"parallel".as_ptr(), c"off".as_ptr());
2030 ffi::cobre_highs_set_double_option(
2031 highs,
2032 c"primal_feasibility_tolerance".as_ptr(),
2033 1e-7,
2034 );
2035 ffi::cobre_highs_set_double_option(highs, c"dual_feasibility_tolerance".as_ptr(), 1e-7);
2036 }
2037
2038 let col_cost: [f64; 3] = [0.0, 1.0, 50.0];
2039 let col_lower: [f64; 3] = [0.0, 0.0, 0.0];
2040 let col_upper: [f64; 3] = [10.0, f64::INFINITY, 8.0];
2041 let row_lower: [f64; 2] = [6.0, 14.0];
2042 let row_upper: [f64; 2] = [6.0, 14.0];
2043 let a_start: [i32; 4] = [0, 2, 2, 3];
2044 let a_index: [i32; 3] = [0, 1, 1];
2045 let a_value: [f64; 3] = [1.0, 2.0, 1.0];
2046
2047 unsafe {
2049 ffi::cobre_highs_pass_lp(
2050 highs,
2051 3,
2052 2,
2053 3,
2054 ffi::HIGHS_MATRIX_FORMAT_COLWISE,
2055 ffi::HIGHS_OBJ_SENSE_MINIMIZE,
2056 0.0,
2057 col_cost.as_ptr(),
2058 col_lower.as_ptr(),
2059 col_upper.as_ptr(),
2060 row_lower.as_ptr(),
2061 row_upper.as_ptr(),
2062 a_start.as_ptr(),
2063 a_index.as_ptr(),
2064 a_value.as_ptr(),
2065 );
2066 ffi::cobre_highs_set_int_option(highs, c"simplex_iteration_limit".as_ptr(), 0);
2067 ffi::cobre_highs_run(highs);
2068 }
2069 let status1 = unsafe { ffi::cobre_highs_get_model_status(highs) };
2070 assert_eq!(status1, ffi::HIGHS_MODEL_STATUS_ITERATION_LIMIT);
2071
2072 unsafe {
2074 ffi::cobre_highs_set_string_option(highs, c"solver".as_ptr(), c"simplex".as_ptr());
2075 ffi::cobre_highs_set_int_option(highs, c"simplex_strategy".as_ptr(), 1);
2076 ffi::cobre_highs_set_string_option(highs, c"presolve".as_ptr(), c"off".as_ptr());
2077 ffi::cobre_highs_set_double_option(
2078 highs,
2079 c"primal_feasibility_tolerance".as_ptr(),
2080 1e-7,
2081 );
2082 ffi::cobre_highs_set_double_option(highs, c"dual_feasibility_tolerance".as_ptr(), 1e-7);
2083 ffi::cobre_highs_set_string_option(highs, c"parallel".as_ptr(), c"off".as_ptr());
2084 ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0);
2085 ffi::cobre_highs_set_int_option(highs, c"simplex_iteration_limit".as_ptr(), i32::MAX);
2087 }
2088
2089 unsafe { ffi::cobre_highs_clear_solver(highs) };
2091 unsafe { ffi::cobre_highs_run(highs) };
2092 let status2 = unsafe { ffi::cobre_highs_get_model_status(highs) };
2093 let obj = unsafe { ffi::cobre_highs_get_objective_value(highs) };
2094 assert_eq!(
2095 status2,
2096 ffi::HIGHS_MODEL_STATUS_OPTIMAL,
2097 "after restoring defaults, second solve must be OPTIMAL, got {status2}"
2098 );
2099 assert!(
2100 (obj - 100.0).abs() < 1e-8,
2101 "objective after restore must be 100.0, got {obj}"
2102 );
2103
2104 unsafe { ffi::cobre_highs_destroy(highs) };
2105 }
2106
2107 #[test]
2112 fn test_research_iteration_limit_one_triggers_iteration_limit_status() {
2113 use crate::ffi;
2114
2115 let highs = unsafe { ffi::cobre_highs_create() };
2116 assert!(!highs.is_null());
2117
2118 unsafe { ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0) };
2119
2120 let col_cost: [f64; 3] = [0.0, 1.0, 50.0];
2121 let col_lower: [f64; 3] = [0.0, 0.0, 0.0];
2122 let col_upper: [f64; 3] = [10.0, f64::INFINITY, 8.0];
2123 let row_lower: [f64; 2] = [6.0, 14.0];
2124 let row_upper: [f64; 2] = [6.0, 14.0];
2125 let a_start: [i32; 4] = [0, 2, 2, 3];
2126 let a_index: [i32; 3] = [0, 1, 1];
2127 let a_value: [f64; 3] = [1.0, 2.0, 1.0];
2128
2129 unsafe {
2130 ffi::cobre_highs_pass_lp(
2131 highs,
2132 3,
2133 2,
2134 3,
2135 ffi::HIGHS_MATRIX_FORMAT_COLWISE,
2136 ffi::HIGHS_OBJ_SENSE_MINIMIZE,
2137 0.0,
2138 col_cost.as_ptr(),
2139 col_lower.as_ptr(),
2140 col_upper.as_ptr(),
2141 row_lower.as_ptr(),
2142 row_upper.as_ptr(),
2143 a_start.as_ptr(),
2144 a_index.as_ptr(),
2145 a_value.as_ptr(),
2146 );
2147 ffi::cobre_highs_set_int_option(highs, c"simplex_iteration_limit".as_ptr(), 1);
2148 ffi::cobre_highs_run(highs);
2149 }
2150
2151 let model_status = unsafe { ffi::cobre_highs_get_model_status(highs) };
2152 eprintln!("iteration_limit=1 model_status: {model_status}");
2153 assert!(
2156 model_status == ffi::HIGHS_MODEL_STATUS_ITERATION_LIMIT
2157 || model_status == ffi::HIGHS_MODEL_STATUS_OPTIMAL,
2158 "expected ITERATION_LIMIT or OPTIMAL, got {model_status}"
2159 );
2160
2161 unsafe { ffi::cobre_highs_destroy(highs) };
2162 }
2163}