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}
200
201impl HighsSolver {
202 pub fn new() -> Result<Self, SolverError> {
227 let handle = unsafe { ffi::cobre_highs_create() };
232
233 if handle.is_null() {
234 return Err(SolverError::InternalError {
235 message: "HiGHS instance creation failed: Highs_create() returned null".to_string(),
236 error_code: None,
237 });
238 }
239
240 if let Err(e) = Self::apply_default_config(handle) {
243 unsafe { ffi::cobre_highs_destroy(handle) };
248 return Err(e);
249 }
250
251 Ok(Self {
252 handle,
253 col_value: Vec::new(),
254 col_dual: Vec::new(),
255 row_value: Vec::new(),
256 row_dual: Vec::new(),
257 scratch_i32: Vec::new(),
258 basis_col_i32: Vec::new(),
259 basis_row_i32: Vec::new(),
260 num_cols: 0,
261 num_rows: 0,
262 has_model: false,
263 stats: SolverStatistics::default(),
264 })
265 }
266
267 fn apply_default_config(handle: *mut c_void) -> Result<(), SolverError> {
273 for opt in &default_options() {
274 let status = unsafe { opt.apply(handle) };
276 if status == ffi::HIGHS_STATUS_ERROR {
277 return Err(SolverError::InternalError {
278 message: format!(
279 "HiGHS configuration failed: {}",
280 opt.name.to_str().unwrap_or("?")
281 ),
282 error_code: Some(status),
283 });
284 }
285 }
286 Ok(())
287 }
288
289 fn extract_solution_view(&mut self, solve_time_seconds: f64) -> SolutionView<'_> {
296 let status = unsafe {
298 ffi::cobre_highs_get_solution(
299 self.handle,
300 self.col_value.as_mut_ptr(),
301 self.col_dual.as_mut_ptr(),
302 self.row_value.as_mut_ptr(),
303 self.row_dual.as_mut_ptr(),
304 )
305 };
306 assert_ne!(
307 status,
308 ffi::HIGHS_STATUS_ERROR,
309 "cobre_highs_get_solution failed after optimal solve"
310 );
311
312 let objective = unsafe { ffi::cobre_highs_get_objective_value(self.handle) };
314
315 #[allow(clippy::cast_sign_loss)]
317 let iterations =
318 unsafe { ffi::cobre_highs_get_simplex_iteration_count(self.handle) } as u64;
319
320 SolutionView {
321 objective,
322 primal: &self.col_value[..self.num_cols],
323 dual: &self.row_dual[..self.num_rows],
324 reduced_costs: &self.col_dual[..self.num_cols],
325 iterations,
326 solve_time_seconds,
327 }
328 }
329
330 fn restore_default_settings(&mut self) {
334 for opt in &default_options() {
335 unsafe { opt.apply(self.handle) };
337 }
338 }
339
340 fn run_once(&mut self) -> i32 {
342 let run_status = unsafe { ffi::cobre_highs_run(self.handle) };
344 if run_status == ffi::HIGHS_STATUS_ERROR {
345 return ffi::HIGHS_MODEL_STATUS_SOLVE_ERROR;
346 }
347 unsafe { ffi::cobre_highs_get_model_status(self.handle) }
349 }
350
351 fn set_iteration_limits(&mut self) {
363 let simplex_iter_limit = self.num_cols.saturating_mul(50).max(100_000);
364 unsafe {
367 #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
368 ffi::cobre_highs_set_int_option(
369 self.handle,
370 c"simplex_iteration_limit".as_ptr(),
371 simplex_iter_limit as i32,
372 );
373 ffi::cobre_highs_set_int_option(self.handle, c"ipm_iteration_limit".as_ptr(), 10_000);
374 }
375 }
376
377 fn restore_iteration_limits(&mut self) {
381 unsafe {
383 ffi::cobre_highs_set_int_option(
384 self.handle,
385 c"simplex_iteration_limit".as_ptr(),
386 i32::MAX,
387 );
388 ffi::cobre_highs_set_int_option(self.handle, c"ipm_iteration_limit".as_ptr(), i32::MAX);
389 }
390 }
391
392 fn interpret_terminal_status(
397 &mut self,
398 status: i32,
399 solve_time_seconds: f64,
400 ) -> Option<SolverError> {
401 match status {
402 ffi::HIGHS_MODEL_STATUS_OPTIMAL => {
403 None
405 }
406 ffi::HIGHS_MODEL_STATUS_INFEASIBLE => Some(SolverError::Infeasible),
407 ffi::HIGHS_MODEL_STATUS_UNBOUNDED_OR_INFEASIBLE => {
408 let mut has_dual_ray: i32 = 0;
412 let mut dual_buf = vec![0.0_f64; self.num_rows];
415 let dual_status = unsafe {
417 ffi::cobre_highs_get_dual_ray(
418 self.handle,
419 &raw mut has_dual_ray,
420 dual_buf.as_mut_ptr(),
421 )
422 };
423 if dual_status != ffi::HIGHS_STATUS_ERROR && has_dual_ray != 0 {
424 return Some(SolverError::Infeasible);
425 }
426 let mut has_primal_ray: i32 = 0;
427 let mut primal_buf = vec![0.0_f64; self.num_cols];
428 let primal_status = unsafe {
430 ffi::cobre_highs_get_primal_ray(
431 self.handle,
432 &raw mut has_primal_ray,
433 primal_buf.as_mut_ptr(),
434 )
435 };
436 if primal_status != ffi::HIGHS_STATUS_ERROR && has_primal_ray != 0 {
437 return Some(SolverError::Unbounded);
438 }
439 Some(SolverError::Infeasible)
440 }
441 ffi::HIGHS_MODEL_STATUS_UNBOUNDED => Some(SolverError::Unbounded),
442 ffi::HIGHS_MODEL_STATUS_TIME_LIMIT => Some(SolverError::TimeLimitExceeded {
443 elapsed_seconds: solve_time_seconds,
444 }),
445 ffi::HIGHS_MODEL_STATUS_ITERATION_LIMIT => {
446 #[allow(clippy::cast_sign_loss)]
448 let iterations =
449 unsafe { ffi::cobre_highs_get_simplex_iteration_count(self.handle) } as u64;
450 Some(SolverError::IterationLimit { iterations })
451 }
452 ffi::HIGHS_MODEL_STATUS_SOLVE_ERROR | ffi::HIGHS_MODEL_STATUS_UNKNOWN => {
453 None
455 }
456 other => Some(SolverError::InternalError {
457 message: format!("HiGHS returned unexpected model status {other}"),
458 error_code: Some(other),
459 }),
460 }
461 }
462
463 fn convert_to_i32_scratch(&mut self, source: &[usize]) -> &[i32] {
467 if source.len() > self.scratch_i32.len() {
468 self.scratch_i32.resize(source.len(), 0);
469 }
470 for (i, &v) in source.iter().enumerate() {
471 debug_assert!(
472 i32::try_from(v).is_ok(),
473 "usize index {v} overflows i32::MAX at position {i}"
474 );
475 #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
477 {
478 self.scratch_i32[i] = v as i32;
479 }
480 }
481 &self.scratch_i32[..source.len()]
482 }
483
484 fn retry_escalation(&mut self, is_unbounded: bool) -> Result<RetryOutcome, (u64, SolverError)> {
494 let phase1_wall_budget = 15.0_f64;
521 let phase2_wall_budget = 30.0_f64;
522 let overall_budget = 120.0_f64;
523 let num_retry_levels = 12_u32;
524
525 let retry_start = Instant::now();
526 let mut retry_attempts: u64 = 0;
527 let mut terminal_err: Option<SolverError> = None;
528 let mut found_optimal = false;
529 let mut optimal_time = 0.0_f64;
530 let mut optimal_iterations: u64 = 0;
531
532 for level in 0..num_retry_levels {
533 if retry_start.elapsed().as_secs_f64() >= overall_budget {
535 break;
536 }
537
538 self.apply_retry_level_options(level);
539
540 retry_attempts += 1;
541
542 let t_retry = Instant::now();
543 let retry_status = self.run_once();
544 let retry_time = t_retry.elapsed().as_secs_f64();
545
546 if retry_status == ffi::HIGHS_MODEL_STATUS_OPTIMAL {
547 #[allow(clippy::cast_sign_loss)]
550 let iters =
551 unsafe { ffi::cobre_highs_get_simplex_iteration_count(self.handle) } as u64;
552 found_optimal = true;
553 optimal_time = retry_time;
554 optimal_iterations = iters;
555 break;
556 }
557
558 let level_budget = if level <= 4 {
564 phase1_wall_budget
565 } else {
566 phase2_wall_budget
567 };
568 let budget_exceeded = retry_time > level_budget;
569 let retryable = retry_status == ffi::HIGHS_MODEL_STATUS_UNBOUNDED
570 || retry_status == ffi::HIGHS_MODEL_STATUS_ITERATION_LIMIT
571 || budget_exceeded;
572 if !retryable {
573 if let Some(e) = self.interpret_terminal_status(retry_status, retry_time) {
574 terminal_err = Some(e);
575 break;
576 }
577 }
578 }
581
582 self.restore_default_settings();
586 self.restore_iteration_limits();
587 unsafe {
588 ffi::cobre_highs_set_int_option(self.handle, c"user_objective_scale".as_ptr(), 0);
589 ffi::cobre_highs_set_int_option(self.handle, c"user_bound_scale".as_ptr(), 0);
590 }
591
592 if found_optimal {
593 return Ok(RetryOutcome {
594 attempts: retry_attempts,
595 solve_time: optimal_time,
596 iterations: optimal_iterations,
597 });
598 }
599
600 Err((
601 retry_attempts,
602 terminal_err.unwrap_or_else(|| {
603 if is_unbounded {
605 SolverError::Unbounded
606 } else {
607 SolverError::NumericalDifficulty {
608 message:
609 "HiGHS failed to reach optimality after all retry escalation levels"
610 .to_string(),
611 }
612 }
613 }),
614 ))
615 }
616
617 fn apply_retry_level_options(&mut self, level: u32) {
630 match level {
631 0 => {
635 unsafe { ffi::cobre_highs_clear_solver(self.handle) };
636 self.set_iteration_limits();
637 }
638 1 => unsafe {
640 ffi::cobre_highs_set_string_option(
641 self.handle,
642 c"presolve".as_ptr(),
643 c"on".as_ptr(),
644 );
645 },
646 2 => unsafe {
649 ffi::cobre_highs_set_int_option(self.handle, c"simplex_strategy".as_ptr(), 1);
650 },
651 3 => unsafe {
654 ffi::cobre_highs_set_double_option(
655 self.handle,
656 c"primal_feasibility_tolerance".as_ptr(),
657 1e-6,
658 );
659 ffi::cobre_highs_set_double_option(
660 self.handle,
661 c"dual_feasibility_tolerance".as_ptr(),
662 1e-6,
663 );
664 },
665 4 => unsafe {
668 ffi::cobre_highs_set_string_option(
669 self.handle,
670 c"solver".as_ptr(),
671 c"ipm".as_ptr(),
672 );
673 },
674
675 _ => self.apply_extended_retry_options(level),
679 }
680 }
681
682 fn apply_extended_retry_options(&mut self, level: u32) {
688 self.restore_default_settings();
689 self.set_iteration_limits();
690 unsafe {
693 ffi::cobre_highs_set_string_option(self.handle, c"presolve".as_ptr(), c"on".as_ptr());
694 }
695 match level {
696 5 => unsafe {
697 ffi::cobre_highs_set_int_option(self.handle, c"simplex_scale_strategy".as_ptr(), 3);
698 },
699 6 => unsafe {
700 ffi::cobre_highs_set_int_option(self.handle, c"simplex_strategy".as_ptr(), 1);
701 ffi::cobre_highs_set_int_option(self.handle, c"simplex_scale_strategy".as_ptr(), 4);
702 },
703 7 => unsafe {
704 ffi::cobre_highs_set_int_option(self.handle, c"simplex_scale_strategy".as_ptr(), 3);
705 ffi::cobre_highs_set_double_option(
706 self.handle,
707 c"primal_feasibility_tolerance".as_ptr(),
708 1e-6,
709 );
710 ffi::cobre_highs_set_double_option(
711 self.handle,
712 c"dual_feasibility_tolerance".as_ptr(),
713 1e-6,
714 );
715 },
716 8 => unsafe {
717 ffi::cobre_highs_set_int_option(self.handle, c"user_objective_scale".as_ptr(), -10);
718 },
719 9 => unsafe {
720 ffi::cobre_highs_set_int_option(self.handle, c"simplex_strategy".as_ptr(), 1);
721 ffi::cobre_highs_set_int_option(self.handle, c"user_objective_scale".as_ptr(), -10);
722 ffi::cobre_highs_set_int_option(self.handle, c"user_bound_scale".as_ptr(), -5);
723 },
724 10 => unsafe {
725 ffi::cobre_highs_set_int_option(self.handle, c"user_objective_scale".as_ptr(), -13);
726 ffi::cobre_highs_set_int_option(self.handle, c"user_bound_scale".as_ptr(), -8);
727 ffi::cobre_highs_set_double_option(
728 self.handle,
729 c"primal_feasibility_tolerance".as_ptr(),
730 1e-6,
731 );
732 ffi::cobre_highs_set_double_option(
733 self.handle,
734 c"dual_feasibility_tolerance".as_ptr(),
735 1e-6,
736 );
737 },
738 11 => unsafe {
739 ffi::cobre_highs_set_string_option(
740 self.handle,
741 c"solver".as_ptr(),
742 c"ipm".as_ptr(),
743 );
744 ffi::cobre_highs_set_int_option(self.handle, c"user_objective_scale".as_ptr(), -10);
745 ffi::cobre_highs_set_int_option(self.handle, c"user_bound_scale".as_ptr(), -5);
746 ffi::cobre_highs_set_double_option(
747 self.handle,
748 c"primal_feasibility_tolerance".as_ptr(),
749 1e-6,
750 );
751 ffi::cobre_highs_set_double_option(
752 self.handle,
753 c"dual_feasibility_tolerance".as_ptr(),
754 1e-6,
755 );
756 },
757 _ => unreachable!(),
758 }
759 }
760}
761
762impl Drop for HighsSolver {
763 fn drop(&mut self) {
764 unsafe { ffi::cobre_highs_destroy(self.handle) };
766 }
767}
768
769impl SolverInterface for HighsSolver {
770 fn name(&self) -> &'static str {
771 "HiGHS"
772 }
773
774 fn load_model(&mut self, template: &StageTemplate) {
775 let t0 = Instant::now();
776 assert!(
786 i32::try_from(template.num_cols).is_ok(),
787 "num_cols {} overflows i32: LP exceeds HiGHS API limit",
788 template.num_cols
789 );
790 assert!(
791 i32::try_from(template.num_rows).is_ok(),
792 "num_rows {} overflows i32: LP exceeds HiGHS API limit",
793 template.num_rows
794 );
795 assert!(
796 i32::try_from(template.num_nz).is_ok(),
797 "num_nz {} overflows i32: LP exceeds HiGHS API limit",
798 template.num_nz
799 );
800 #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
802 let num_col = template.num_cols as i32;
803 #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
804 let num_row = template.num_rows as i32;
805 #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
806 let num_nz = template.num_nz as i32;
807 let status = unsafe {
808 ffi::cobre_highs_pass_lp(
809 self.handle,
810 num_col,
811 num_row,
812 num_nz,
813 ffi::HIGHS_MATRIX_FORMAT_COLWISE,
814 ffi::HIGHS_OBJ_SENSE_MINIMIZE,
815 0.0, template.objective.as_ptr(),
817 template.col_lower.as_ptr(),
818 template.col_upper.as_ptr(),
819 template.row_lower.as_ptr(),
820 template.row_upper.as_ptr(),
821 template.col_starts.as_ptr(),
822 template.row_indices.as_ptr(),
823 template.values.as_ptr(),
824 )
825 };
826
827 assert_ne!(
828 status,
829 ffi::HIGHS_STATUS_ERROR,
830 "cobre_highs_pass_lp failed with status {status}"
831 );
832
833 self.num_cols = template.num_cols;
834 self.num_rows = template.num_rows;
835 self.has_model = true;
836
837 self.col_value.resize(self.num_cols, 0.0);
840 self.col_dual.resize(self.num_cols, 0.0);
841 self.row_value.resize(self.num_rows, 0.0);
842 self.row_dual.resize(self.num_rows, 0.0);
843
844 self.basis_col_i32.resize(self.num_cols, 0);
847 self.basis_row_i32.resize(self.num_rows, 0);
848 self.stats.total_load_model_time_seconds += t0.elapsed().as_secs_f64();
849 self.stats.load_model_count += 1;
850 }
851
852 fn add_rows(&mut self, cuts: &RowBatch) {
853 let t0 = Instant::now();
854 assert!(
855 i32::try_from(cuts.num_rows).is_ok(),
856 "cuts.num_rows {} overflows i32: RowBatch exceeds HiGHS API limit",
857 cuts.num_rows
858 );
859 assert!(
860 i32::try_from(cuts.col_indices.len()).is_ok(),
861 "cuts nnz {} overflows i32: RowBatch exceeds HiGHS API limit",
862 cuts.col_indices.len()
863 );
864 #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
866 let num_new_row = cuts.num_rows as i32;
867 #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
868 let num_new_nz = cuts.col_indices.len() as i32;
869
870 let status = unsafe {
878 ffi::cobre_highs_add_rows(
879 self.handle,
880 num_new_row,
881 cuts.row_lower.as_ptr(),
882 cuts.row_upper.as_ptr(),
883 num_new_nz,
884 cuts.row_starts.as_ptr(),
885 cuts.col_indices.as_ptr(),
886 cuts.values.as_ptr(),
887 )
888 };
889
890 assert_ne!(
891 status,
892 ffi::HIGHS_STATUS_ERROR,
893 "cobre_highs_add_rows failed with status {status}"
894 );
895
896 self.num_rows += cuts.num_rows;
897
898 self.row_value.resize(self.num_rows, 0.0);
900 self.row_dual.resize(self.num_rows, 0.0);
901
902 self.basis_row_i32.resize(self.num_rows, 0);
904 self.stats.total_add_rows_time_seconds += t0.elapsed().as_secs_f64();
905 self.stats.add_rows_count += 1;
906 }
907
908 fn set_row_bounds(&mut self, indices: &[usize], lower: &[f64], upper: &[f64]) {
909 assert!(
910 indices.len() == lower.len() && indices.len() == upper.len(),
911 "set_row_bounds: indices ({}), lower ({}), and upper ({}) must have equal length",
912 indices.len(),
913 lower.len(),
914 upper.len()
915 );
916 if indices.is_empty() {
917 return;
918 }
919
920 assert!(
921 i32::try_from(indices.len()).is_ok(),
922 "set_row_bounds: indices.len() {} overflows i32",
923 indices.len()
924 );
925 #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
926 let num_entries = indices.len() as i32;
927
928 let t0 = Instant::now();
929 let status = unsafe {
936 ffi::cobre_highs_change_rows_bounds_by_set(
937 self.handle,
938 num_entries,
939 self.convert_to_i32_scratch(indices).as_ptr(),
940 lower.as_ptr(),
941 upper.as_ptr(),
942 )
943 };
944
945 assert_ne!(
946 status,
947 ffi::HIGHS_STATUS_ERROR,
948 "cobre_highs_change_rows_bounds_by_set failed with status {status}"
949 );
950 self.stats.total_set_bounds_time_seconds += t0.elapsed().as_secs_f64();
951 }
952
953 fn set_col_bounds(&mut self, indices: &[usize], lower: &[f64], upper: &[f64]) {
954 assert!(
955 indices.len() == lower.len() && indices.len() == upper.len(),
956 "set_col_bounds: indices ({}), lower ({}), and upper ({}) must have equal length",
957 indices.len(),
958 lower.len(),
959 upper.len()
960 );
961 if indices.is_empty() {
962 return;
963 }
964
965 assert!(
966 i32::try_from(indices.len()).is_ok(),
967 "set_col_bounds: indices.len() {} overflows i32",
968 indices.len()
969 );
970 #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
971 let num_entries = indices.len() as i32;
972
973 let t0 = Instant::now();
974 let status = unsafe {
980 ffi::cobre_highs_change_cols_bounds_by_set(
981 self.handle,
982 num_entries,
983 self.convert_to_i32_scratch(indices).as_ptr(),
984 lower.as_ptr(),
985 upper.as_ptr(),
986 )
987 };
988
989 assert_ne!(
990 status,
991 ffi::HIGHS_STATUS_ERROR,
992 "cobre_highs_change_cols_bounds_by_set failed with status {status}"
993 );
994 self.stats.total_set_bounds_time_seconds += t0.elapsed().as_secs_f64();
995 }
996
997 fn solve(&mut self) -> Result<SolutionView<'_>, SolverError> {
998 assert!(
999 self.has_model,
1000 "solve called without a loaded model — call load_model first"
1001 );
1002
1003 self.set_iteration_limits();
1009
1010 let t0 = Instant::now();
1011 let model_status = self.run_once();
1012 let solve_time = t0.elapsed().as_secs_f64();
1013
1014 self.stats.solve_count += 1;
1015
1016 if model_status == ffi::HIGHS_MODEL_STATUS_OPTIMAL {
1017 #[allow(clippy::cast_sign_loss)]
1022 let iterations =
1023 unsafe { ffi::cobre_highs_get_simplex_iteration_count(self.handle) } as u64;
1024 self.stats.success_count += 1;
1025 self.stats.first_try_successes += 1;
1026 self.stats.total_iterations += iterations;
1027 self.stats.total_solve_time_seconds += solve_time;
1028 self.restore_iteration_limits();
1029 return Ok(self.extract_solution_view(solve_time));
1030 }
1031
1032 let is_unbounded = model_status == ffi::HIGHS_MODEL_STATUS_UNBOUNDED;
1043 let initial_retryable = is_unbounded
1044 || model_status == ffi::HIGHS_MODEL_STATUS_ITERATION_LIMIT
1045 || model_status == ffi::HIGHS_MODEL_STATUS_TIME_LIMIT
1046 || solve_time > 15.0;
1047 if !initial_retryable {
1048 if let Some(terminal_err) = self.interpret_terminal_status(model_status, solve_time) {
1049 self.restore_iteration_limits();
1050 self.stats.failure_count += 1;
1051 return Err(terminal_err);
1052 }
1053 }
1054
1055 match self.retry_escalation(is_unbounded) {
1057 Ok(outcome) => {
1058 self.stats.retry_count += outcome.attempts;
1059 self.stats.success_count += 1;
1060 self.stats.total_iterations += outcome.iterations;
1061 self.stats.total_solve_time_seconds += outcome.solve_time;
1062 Ok(self.extract_solution_view(outcome.solve_time))
1063 }
1064 Err((attempts, err)) => {
1065 self.stats.retry_count += attempts;
1066 self.stats.failure_count += 1;
1067 Err(err)
1068 }
1069 }
1070 }
1071
1072 fn reset(&mut self) {
1073 let status = unsafe { ffi::cobre_highs_clear_solver(self.handle) };
1078 debug_assert_ne!(
1079 status,
1080 ffi::HIGHS_STATUS_ERROR,
1081 "cobre_highs_clear_solver failed — HiGHS internal state may be inconsistent"
1082 );
1083 self.num_cols = 0;
1085 self.num_rows = 0;
1086 self.has_model = false;
1087 }
1090
1091 fn get_basis(&mut self, out: &mut crate::types::Basis) {
1092 assert!(
1093 self.has_model,
1094 "get_basis called without a loaded model — call load_model first"
1095 );
1096
1097 out.col_status.resize(self.num_cols, 0);
1098 out.row_status.resize(self.num_rows, 0);
1099
1100 let get_status = unsafe {
1106 ffi::cobre_highs_get_basis(
1107 self.handle,
1108 out.col_status.as_mut_ptr(),
1109 out.row_status.as_mut_ptr(),
1110 )
1111 };
1112
1113 assert_ne!(
1114 get_status,
1115 ffi::HIGHS_STATUS_ERROR,
1116 "cobre_highs_get_basis failed: basis must exist after a successful solve (programming error)"
1117 );
1118 }
1119
1120 fn solve_with_basis(
1121 &mut self,
1122 basis: &crate::types::Basis,
1123 ) -> Result<crate::types::SolutionView<'_>, SolverError> {
1124 assert!(
1125 self.has_model,
1126 "solve_with_basis called without a loaded model — call load_model first"
1127 );
1128 assert!(
1129 basis.col_status.len() == self.num_cols,
1130 "basis column count {} does not match LP column count {}",
1131 basis.col_status.len(),
1132 self.num_cols
1133 );
1134
1135 self.stats.basis_offered += 1;
1137
1138 self.basis_col_i32[..self.num_cols].copy_from_slice(&basis.col_status);
1141
1142 let basis_rows = basis.row_status.len();
1146 let lp_rows = self.num_rows;
1147 let copy_len = basis_rows.min(lp_rows);
1148 self.basis_row_i32[..copy_len].copy_from_slice(&basis.row_status[..copy_len]);
1149 if lp_rows > basis_rows {
1150 self.basis_row_i32[basis_rows..lp_rows].fill(ffi::HIGHS_BASIS_STATUS_BASIC);
1151 }
1152
1153 let basis_set_start = Instant::now();
1160 let set_status = unsafe {
1161 ffi::cobre_highs_set_basis(
1162 self.handle,
1163 self.basis_col_i32.as_ptr(),
1164 self.basis_row_i32.as_ptr(),
1165 )
1166 };
1167 self.stats.total_basis_set_time_seconds += basis_set_start.elapsed().as_secs_f64();
1168
1169 if set_status == ffi::HIGHS_STATUS_ERROR {
1171 self.stats.basis_rejections += 1;
1172 debug_assert!(false, "raw basis rejected; falling back to cold-start");
1173 }
1174
1175 self.solve()
1177 }
1178
1179 fn statistics(&self) -> SolverStatistics {
1180 self.stats.clone()
1181 }
1182}
1183
1184#[cfg(feature = "test-support")]
1190impl HighsSolver {
1191 #[must_use]
1199 pub fn raw_handle(&self) -> *mut std::os::raw::c_void {
1200 self.handle
1201 }
1202}
1203
1204#[cfg(test)]
1205mod tests {
1206 use super::HighsSolver;
1207 use crate::{
1208 SolverInterface,
1209 types::{Basis, RowBatch, StageTemplate},
1210 };
1211
1212 fn make_fixture_stage_template() -> StageTemplate {
1225 StageTemplate {
1226 num_cols: 3,
1227 num_rows: 2,
1228 num_nz: 3,
1229 col_starts: vec![0_i32, 2, 2, 3],
1230 row_indices: vec![0_i32, 1, 1],
1231 values: vec![1.0, 2.0, 1.0],
1232 col_lower: vec![0.0, 0.0, 0.0],
1233 col_upper: vec![10.0, f64::INFINITY, 8.0],
1234 objective: vec![0.0, 1.0, 50.0],
1235 row_lower: vec![6.0, 14.0],
1236 row_upper: vec![6.0, 14.0],
1237 n_state: 1,
1238 n_transfer: 0,
1239 n_dual_relevant: 1,
1240 n_hydro: 1,
1241 max_par_order: 0,
1242 col_scale: Vec::new(),
1243 row_scale: Vec::new(),
1244 }
1245 }
1246
1247 fn make_fixture_row_batch() -> RowBatch {
1251 RowBatch {
1252 num_rows: 2,
1253 row_starts: vec![0_i32, 2, 4],
1254 col_indices: vec![0_i32, 1, 0, 1],
1255 values: vec![-5.0, 1.0, 3.0, 1.0],
1256 row_lower: vec![20.0, 80.0],
1257 row_upper: vec![f64::INFINITY, f64::INFINITY],
1258 }
1259 }
1260
1261 #[test]
1262 fn test_highs_solver_create_and_name() {
1263 let solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1264 assert_eq!(solver.name(), "HiGHS");
1265 }
1267
1268 #[test]
1269 fn test_highs_solver_send_bound() {
1270 fn assert_send<T: Send>() {}
1271 assert_send::<HighsSolver>();
1272 }
1273
1274 #[test]
1275 fn test_highs_solver_statistics_initial() {
1276 let solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1277 let stats = solver.statistics();
1278 assert_eq!(stats.solve_count, 0);
1279 assert_eq!(stats.success_count, 0);
1280 assert_eq!(stats.failure_count, 0);
1281 assert_eq!(stats.total_iterations, 0);
1282 assert_eq!(stats.retry_count, 0);
1283 assert_eq!(stats.total_solve_time_seconds, 0.0);
1284 }
1285
1286 #[test]
1287 fn test_highs_load_model_updates_dimensions() {
1288 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1289 let template = make_fixture_stage_template();
1290
1291 solver.load_model(&template);
1292
1293 assert_eq!(solver.num_cols, 3, "num_cols must be 3 after load_model");
1294 assert_eq!(solver.num_rows, 2, "num_rows must be 2 after load_model");
1295 assert_eq!(
1296 solver.col_value.len(),
1297 3,
1298 "col_value buffer must be resized to num_cols"
1299 );
1300 assert_eq!(
1301 solver.col_dual.len(),
1302 3,
1303 "col_dual buffer must be resized to num_cols"
1304 );
1305 assert_eq!(
1306 solver.row_value.len(),
1307 2,
1308 "row_value buffer must be resized to num_rows"
1309 );
1310 assert_eq!(
1311 solver.row_dual.len(),
1312 2,
1313 "row_dual buffer must be resized to num_rows"
1314 );
1315 }
1316
1317 #[test]
1318 fn test_highs_add_rows_updates_dimensions() {
1319 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1320 let template = make_fixture_stage_template();
1321 let cuts = make_fixture_row_batch();
1322
1323 solver.load_model(&template);
1324 solver.add_rows(&cuts);
1325
1326 assert_eq!(solver.num_rows, 4, "num_rows must be 4 after add_rows");
1328 assert_eq!(
1329 solver.row_dual.len(),
1330 4,
1331 "row_dual buffer must be resized to 4 after add_rows"
1332 );
1333 assert_eq!(
1334 solver.row_value.len(),
1335 4,
1336 "row_value buffer must be resized to 4 after add_rows"
1337 );
1338 assert_eq!(solver.num_cols, 3, "num_cols must be unchanged by add_rows");
1340 }
1341
1342 #[test]
1343 fn test_highs_set_row_bounds_no_panic() {
1344 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1345 let template = make_fixture_stage_template();
1346 solver.load_model(&template);
1347
1348 solver.set_row_bounds(&[0], &[4.0], &[4.0]);
1350 }
1351
1352 #[test]
1353 fn test_highs_set_col_bounds_no_panic() {
1354 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1355 let template = make_fixture_stage_template();
1356 solver.load_model(&template);
1357
1358 solver.set_col_bounds(&[1], &[10.0], &[f64::INFINITY]);
1360 }
1361
1362 #[test]
1363 fn test_highs_set_bounds_empty_no_panic() {
1364 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1365 let template = make_fixture_stage_template();
1366 solver.load_model(&template);
1367
1368 solver.set_row_bounds(&[], &[], &[]);
1370 solver.set_col_bounds(&[], &[], &[]);
1371 }
1372
1373 #[test]
1376 fn test_highs_solve_basic_lp() {
1377 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1378 let template = make_fixture_stage_template();
1379 solver.load_model(&template);
1380
1381 let solution = solver
1382 .solve()
1383 .expect("solve() must succeed on a feasible LP");
1384
1385 assert!(
1386 (solution.objective - 100.0).abs() < 1e-8,
1387 "objective must be 100.0, got {}",
1388 solution.objective
1389 );
1390 assert_eq!(solution.primal.len(), 3, "primal must have 3 elements");
1391 assert!(
1392 (solution.primal[0] - 6.0).abs() < 1e-8,
1393 "primal[0] (x0) must be 6.0, got {}",
1394 solution.primal[0]
1395 );
1396 assert!(
1397 (solution.primal[1] - 0.0).abs() < 1e-8,
1398 "primal[1] (x1) must be 0.0, got {}",
1399 solution.primal[1]
1400 );
1401 assert!(
1402 (solution.primal[2] - 2.0).abs() < 1e-8,
1403 "primal[2] (x2) must be 2.0, got {}",
1404 solution.primal[2]
1405 );
1406 }
1407
1408 #[test]
1412 fn test_highs_solve_with_cuts() {
1413 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1414 let template = make_fixture_stage_template();
1415 let cuts = make_fixture_row_batch();
1416 solver.load_model(&template);
1417 solver.add_rows(&cuts);
1418
1419 let solution = solver
1420 .solve()
1421 .expect("solve() must succeed on a feasible LP with cuts");
1422
1423 assert!(
1424 (solution.objective - 162.0).abs() < 1e-8,
1425 "objective must be 162.0, got {}",
1426 solution.objective
1427 );
1428 assert!(
1429 (solution.primal[0] - 6.0).abs() < 1e-8,
1430 "primal[0] must be 6.0, got {}",
1431 solution.primal[0]
1432 );
1433 assert!(
1434 (solution.primal[1] - 62.0).abs() < 1e-8,
1435 "primal[1] must be 62.0, got {}",
1436 solution.primal[1]
1437 );
1438 assert!(
1439 (solution.primal[2] - 2.0).abs() < 1e-8,
1440 "primal[2] must be 2.0, got {}",
1441 solution.primal[2]
1442 );
1443 }
1444
1445 #[test]
1448 fn test_highs_solve_after_rhs_patch() {
1449 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1450 let template = make_fixture_stage_template();
1451 let cuts = make_fixture_row_batch();
1452 solver.load_model(&template);
1453 solver.add_rows(&cuts);
1454
1455 solver.set_row_bounds(&[0], &[4.0], &[4.0]);
1457
1458 let solution = solver
1459 .solve()
1460 .expect("solve() must succeed after RHS patch");
1461
1462 assert!(
1463 (solution.objective - 368.0).abs() < 1e-8,
1464 "objective must be 368.0, got {}",
1465 solution.objective
1466 );
1467 }
1468
1469 #[test]
1471 fn test_highs_solve_statistics_increment() {
1472 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1473 let template = make_fixture_stage_template();
1474 solver.load_model(&template);
1475
1476 solver.solve().expect("first solve must succeed");
1477 solver.solve().expect("second solve must succeed");
1478
1479 let stats = solver.statistics();
1480 assert_eq!(stats.solve_count, 2, "solve_count must be 2");
1481 assert_eq!(stats.success_count, 2, "success_count must be 2");
1482 assert_eq!(stats.failure_count, 0, "failure_count must be 0");
1483 assert!(
1484 stats.total_iterations > 0,
1485 "total_iterations must be positive"
1486 );
1487 }
1488
1489 #[test]
1491 fn test_highs_reset_preserves_stats() {
1492 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1493 let template = make_fixture_stage_template();
1494 solver.load_model(&template);
1495 solver.solve().expect("solve must succeed");
1496
1497 let stats_before = solver.statistics();
1498 assert_eq!(
1499 stats_before.solve_count, 1,
1500 "solve_count must be 1 before reset"
1501 );
1502
1503 solver.reset();
1504
1505 let stats_after = solver.statistics();
1506 assert_eq!(
1507 stats_after.solve_count, stats_before.solve_count,
1508 "solve_count must be unchanged after reset"
1509 );
1510 assert_eq!(
1511 stats_after.success_count, stats_before.success_count,
1512 "success_count must be unchanged after reset"
1513 );
1514 assert_eq!(
1515 stats_after.total_iterations, stats_before.total_iterations,
1516 "total_iterations must be unchanged after reset"
1517 );
1518 }
1519
1520 #[test]
1522 fn test_highs_solve_iterations_positive() {
1523 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1524 let template = make_fixture_stage_template();
1525 solver.load_model(&template);
1526
1527 let solution = solver.solve().expect("solve must succeed");
1528 assert!(
1529 solution.iterations > 0,
1530 "iterations must be positive, got {}",
1531 solution.iterations
1532 );
1533 }
1534
1535 #[test]
1537 fn test_highs_solve_time_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.solve_time_seconds > 0.0,
1545 "solve_time_seconds must be positive, got {}",
1546 solution.solve_time_seconds
1547 );
1548 }
1549
1550 #[test]
1553 fn test_highs_solve_statistics_single() {
1554 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1555 let template = make_fixture_stage_template();
1556 solver.load_model(&template);
1557
1558 solver.solve().expect("solve must succeed");
1559
1560 let stats = solver.statistics();
1561 assert_eq!(stats.solve_count, 1, "solve_count must be 1");
1562 assert_eq!(stats.success_count, 1, "success_count must be 1");
1563 assert_eq!(stats.failure_count, 0, "failure_count must be 0");
1564 assert!(
1565 stats.total_iterations > 0,
1566 "total_iterations must be positive after a successful solve"
1567 );
1568 }
1569
1570 #[test]
1573 fn test_get_basis_valid_status_codes() {
1574 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1575 let template = make_fixture_stage_template();
1576 solver.load_model(&template);
1577 solver.solve().expect("solve must succeed before get_basis");
1578
1579 let mut basis = Basis::new(0, 0);
1580 solver.get_basis(&mut basis);
1581
1582 for &code in &basis.col_status {
1583 assert!(
1584 (0..=4).contains(&code),
1585 "col_status code {code} is outside valid HiGHS range 0..=4"
1586 );
1587 }
1588 for &code in &basis.row_status {
1589 assert!(
1590 (0..=4).contains(&code),
1591 "row_status code {code} is outside valid HiGHS range 0..=4"
1592 );
1593 }
1594 }
1595
1596 #[test]
1599 fn test_get_basis_resizes_output() {
1600 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1601 let template = make_fixture_stage_template();
1602 solver.load_model(&template);
1603 solver.solve().expect("solve must succeed before get_basis");
1604
1605 let mut basis = Basis::new(0, 0);
1606 assert_eq!(
1607 basis.col_status.len(),
1608 0,
1609 "initial col_status must be empty"
1610 );
1611 assert_eq!(
1612 basis.row_status.len(),
1613 0,
1614 "initial row_status must be empty"
1615 );
1616
1617 solver.get_basis(&mut basis);
1618
1619 assert_eq!(
1620 basis.col_status.len(),
1621 3,
1622 "col_status must be resized to 3 (num_cols of SS1.1)"
1623 );
1624 assert_eq!(
1625 basis.row_status.len(),
1626 2,
1627 "row_status must be resized to 2 (num_rows of SS1.1)"
1628 );
1629 }
1630
1631 #[test]
1634 fn test_solve_with_basis_warm_start() {
1635 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1636 let template = make_fixture_stage_template();
1637 solver.load_model(&template);
1638 solver.solve().expect("cold-start solve must succeed");
1639
1640 let mut basis = Basis::new(0, 0);
1641 solver.get_basis(&mut basis);
1642
1643 solver.load_model(&template);
1645 let result = solver
1646 .solve_with_basis(&basis)
1647 .expect("warm-start solve must succeed");
1648
1649 assert!(
1650 (result.objective - 100.0).abs() < 1e-8,
1651 "warm-start objective must be 100.0, got {}",
1652 result.objective
1653 );
1654 assert!(
1655 result.iterations <= 1,
1656 "warm-start from exact basis must use at most 1 iteration, got {}",
1657 result.iterations
1658 );
1659
1660 let stats = solver.statistics();
1661 assert_eq!(
1662 stats.basis_rejections, 0,
1663 "basis_rejections must be 0 when raw basis is accepted, got {}",
1664 stats.basis_rejections
1665 );
1666 }
1667
1668 #[test]
1672 fn test_solve_with_basis_dimension_mismatch() {
1673 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1674 let template = make_fixture_stage_template();
1675 let cuts = make_fixture_row_batch();
1676
1677 solver.load_model(&template);
1679 solver.solve().expect("SS1.1 solve must succeed");
1680 let mut basis = Basis::new(0, 0);
1681 solver.get_basis(&mut basis);
1682 assert_eq!(
1683 basis.row_status.len(),
1684 2,
1685 "captured basis must have 2 row statuses"
1686 );
1687
1688 solver.load_model(&template);
1690 solver.add_rows(&cuts);
1691 assert_eq!(solver.num_rows, 4, "LP must have 4 rows after add_rows");
1692
1693 let result = solver
1695 .solve_with_basis(&basis)
1696 .expect("solve with dimension-mismatched basis must succeed");
1697
1698 assert!(
1699 (result.objective - 162.0).abs() < 1e-8,
1700 "objective with both cuts active must be 162.0, got {}",
1701 result.objective
1702 );
1703 }
1704}
1705
1706#[cfg(test)]
1718#[allow(clippy::doc_markdown)]
1719mod research_tests_ticket_023 {
1720 unsafe fn research_load_ss11_lp(highs: *mut std::os::raw::c_void) {
1731 use crate::ffi;
1732 let col_cost: [f64; 3] = [0.0, 1.0, 50.0];
1733 let col_lower: [f64; 3] = [0.0, 0.0, 0.0];
1734 let col_upper: [f64; 3] = [10.0, f64::INFINITY, 8.0];
1735 let row_lower: [f64; 2] = [6.0, 14.0];
1736 let row_upper: [f64; 2] = [6.0, 14.0];
1737 let a_start: [i32; 4] = [0, 2, 2, 3];
1738 let a_index: [i32; 3] = [0, 1, 1];
1739 let a_value: [f64; 3] = [1.0, 2.0, 1.0];
1740 let status = unsafe {
1742 ffi::cobre_highs_pass_lp(
1743 highs,
1744 3,
1745 2,
1746 3,
1747 ffi::HIGHS_MATRIX_FORMAT_COLWISE,
1748 ffi::HIGHS_OBJ_SENSE_MINIMIZE,
1749 0.0,
1750 col_cost.as_ptr(),
1751 col_lower.as_ptr(),
1752 col_upper.as_ptr(),
1753 row_lower.as_ptr(),
1754 row_upper.as_ptr(),
1755 a_start.as_ptr(),
1756 a_index.as_ptr(),
1757 a_value.as_ptr(),
1758 )
1759 };
1760 assert_eq!(
1761 status,
1762 ffi::HIGHS_STATUS_OK,
1763 "research_load_ss11_lp pass_lp failed"
1764 );
1765 }
1766
1767 #[test]
1773 fn test_research_probe_limit_status_on_ss11_lp() {
1774 use crate::ffi;
1775
1776 let highs = unsafe { ffi::cobre_highs_create() };
1778 assert!(!highs.is_null());
1779 unsafe { ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0) };
1780 unsafe { research_load_ss11_lp(highs) };
1781 let _ = unsafe { ffi::cobre_highs_set_double_option(highs, c"time_limit".as_ptr(), 0.0) };
1782 let run_status = unsafe { ffi::cobre_highs_run(highs) };
1783 let model_status = unsafe { ffi::cobre_highs_get_model_status(highs) };
1784 let obj = unsafe { ffi::cobre_highs_get_objective_value(highs) };
1785 eprintln!(
1786 "SS1.1 + time_limit=0: run_status={run_status}, model_status={model_status}, obj={obj}"
1787 );
1788 unsafe { ffi::cobre_highs_destroy(highs) };
1789
1790 let highs = unsafe { ffi::cobre_highs_create() };
1792 assert!(!highs.is_null());
1793 unsafe { ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0) };
1794 unsafe { research_load_ss11_lp(highs) };
1795 let _ = unsafe {
1796 ffi::cobre_highs_set_int_option(highs, c"simplex_iteration_limit".as_ptr(), 0)
1797 };
1798 let run_status = unsafe { ffi::cobre_highs_run(highs) };
1799 let model_status = unsafe { ffi::cobre_highs_get_model_status(highs) };
1800 let obj = unsafe { ffi::cobre_highs_get_objective_value(highs) };
1801 eprintln!(
1802 "SS1.1 + iteration_limit=0: run_status={run_status}, model_status={model_status}, obj={obj}"
1803 );
1804 unsafe { ffi::cobre_highs_destroy(highs) };
1805 }
1806
1807 unsafe fn research_load_larger_lp(highs: *mut std::os::raw::c_void) {
1827 use crate::ffi;
1828 let col_cost: [f64; 5] = [1.0, 1.0, 1.0, 1.0, 1.0];
1829 let col_lower: [f64; 5] = [0.0; 5];
1830 let col_upper: [f64; 5] = [100.0; 5];
1831 let row_lower: [f64; 4] = [10.0, 8.0, 6.0, 4.0];
1832 let row_upper: [f64; 4] = [f64::INFINITY; 4];
1833 let a_start: [i32; 6] = [0, 1, 3, 5, 7, 8];
1835 let a_index: [i32; 8] = [0, 0, 1, 1, 2, 2, 3, 3];
1836 let a_value: [f64; 8] = [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0];
1837 let status = unsafe {
1839 ffi::cobre_highs_pass_lp(
1840 highs,
1841 5,
1842 4,
1843 8,
1844 ffi::HIGHS_MATRIX_FORMAT_COLWISE,
1845 ffi::HIGHS_OBJ_SENSE_MINIMIZE,
1846 0.0,
1847 col_cost.as_ptr(),
1848 col_lower.as_ptr(),
1849 col_upper.as_ptr(),
1850 row_lower.as_ptr(),
1851 row_upper.as_ptr(),
1852 a_start.as_ptr(),
1853 a_index.as_ptr(),
1854 a_value.as_ptr(),
1855 )
1856 };
1857 assert_eq!(
1858 status,
1859 ffi::HIGHS_STATUS_OK,
1860 "research_load_larger_lp pass_lp failed"
1861 );
1862 }
1863
1864 #[test]
1873 fn test_research_time_limit_zero_triggers_time_limit_status() {
1874 use crate::ffi;
1875
1876 let highs = unsafe { ffi::cobre_highs_create() };
1877 assert!(!highs.is_null());
1878 unsafe { ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0) };
1879 unsafe { research_load_larger_lp(highs) };
1880
1881 let opt_status =
1882 unsafe { ffi::cobre_highs_set_double_option(highs, c"time_limit".as_ptr(), 0.0) };
1883 assert_eq!(opt_status, ffi::HIGHS_STATUS_OK);
1884
1885 let run_status = unsafe { ffi::cobre_highs_run(highs) };
1886 let model_status = unsafe { ffi::cobre_highs_get_model_status(highs) };
1887
1888 eprintln!(
1889 "time_limit=0 on larger LP: run_status={run_status}, model_status={model_status}"
1890 );
1891
1892 assert_eq!(
1893 run_status,
1894 ffi::HIGHS_STATUS_WARNING,
1895 "time_limit=0 must return HIGHS_STATUS_WARNING (1), got {run_status}"
1896 );
1897 assert_eq!(
1898 model_status,
1899 ffi::HIGHS_MODEL_STATUS_TIME_LIMIT,
1900 "time_limit=0 must give MODEL_STATUS_TIME_LIMIT (13), got {model_status}"
1901 );
1902
1903 unsafe { ffi::cobre_highs_destroy(highs) };
1904 }
1905
1906 #[test]
1915 fn test_research_iteration_limit_zero_triggers_iteration_limit_status() {
1916 use crate::ffi;
1917
1918 let highs = unsafe { ffi::cobre_highs_create() };
1919 assert!(!highs.is_null());
1920 unsafe { ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0) };
1921 unsafe { ffi::cobre_highs_set_string_option(highs, c"presolve".as_ptr(), c"off".as_ptr()) };
1923 unsafe { research_load_larger_lp(highs) };
1924
1925 let opt_status = unsafe {
1926 ffi::cobre_highs_set_int_option(highs, c"simplex_iteration_limit".as_ptr(), 0)
1927 };
1928 assert_eq!(opt_status, ffi::HIGHS_STATUS_OK);
1929
1930 let run_status = unsafe { ffi::cobre_highs_run(highs) };
1931 let model_status = unsafe { ffi::cobre_highs_get_model_status(highs) };
1932
1933 eprintln!(
1934 "iteration_limit=0 on larger LP: run_status={run_status}, model_status={model_status}"
1935 );
1936
1937 assert_eq!(
1938 run_status,
1939 ffi::HIGHS_STATUS_WARNING,
1940 "iteration_limit=0 must return HIGHS_STATUS_WARNING (1), got {run_status}"
1941 );
1942 assert_eq!(
1943 model_status,
1944 ffi::HIGHS_MODEL_STATUS_ITERATION_LIMIT,
1945 "iteration_limit=0 must give MODEL_STATUS_ITERATION_LIMIT (14), got {model_status}"
1946 );
1947
1948 unsafe { ffi::cobre_highs_destroy(highs) };
1949 }
1950
1951 #[test]
1957 fn test_research_partial_solution_availability() {
1958 use crate::ffi;
1959
1960 {
1962 let highs = unsafe { ffi::cobre_highs_create() };
1963 assert!(!highs.is_null());
1964 unsafe { ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0) };
1965 unsafe { research_load_larger_lp(highs) };
1966 unsafe { ffi::cobre_highs_set_double_option(highs, c"time_limit".as_ptr(), 0.0) };
1967 unsafe { ffi::cobre_highs_run(highs) };
1968
1969 let obj = unsafe { ffi::cobre_highs_get_objective_value(highs) };
1970 let model_status = unsafe { ffi::cobre_highs_get_model_status(highs) };
1971 assert_eq!(model_status, ffi::HIGHS_MODEL_STATUS_TIME_LIMIT);
1972 eprintln!("TIME_LIMIT: obj={obj}, finite={}", obj.is_finite());
1973 unsafe { ffi::cobre_highs_destroy(highs) };
1974 }
1975
1976 {
1978 let highs = unsafe { ffi::cobre_highs_create() };
1979 assert!(!highs.is_null());
1980 unsafe { ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0) };
1981 unsafe {
1982 ffi::cobre_highs_set_string_option(highs, c"presolve".as_ptr(), c"off".as_ptr())
1983 };
1984 unsafe { research_load_larger_lp(highs) };
1985 unsafe {
1986 ffi::cobre_highs_set_int_option(highs, c"simplex_iteration_limit".as_ptr(), 0)
1987 };
1988 unsafe { ffi::cobre_highs_run(highs) };
1989
1990 let obj = unsafe { ffi::cobre_highs_get_objective_value(highs) };
1991 let model_status = unsafe { ffi::cobre_highs_get_model_status(highs) };
1992 assert_eq!(model_status, ffi::HIGHS_MODEL_STATUS_ITERATION_LIMIT);
1993 eprintln!("ITERATION_LIMIT: obj={obj}, finite={}", obj.is_finite());
1994 unsafe { ffi::cobre_highs_destroy(highs) };
1995 }
1996 }
1997
1998 #[test]
2001 fn test_research_restore_defaults_allows_subsequent_optimal_solve() {
2002 use crate::ffi;
2003
2004 let highs = unsafe { ffi::cobre_highs_create() };
2005 assert!(!highs.is_null());
2006
2007 unsafe { ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0) };
2008
2009 unsafe {
2011 ffi::cobre_highs_set_string_option(highs, c"solver".as_ptr(), c"simplex".as_ptr());
2012 ffi::cobre_highs_set_int_option(highs, c"simplex_strategy".as_ptr(), 1);
2013 ffi::cobre_highs_set_string_option(highs, c"presolve".as_ptr(), c"off".as_ptr());
2014 ffi::cobre_highs_set_string_option(highs, c"parallel".as_ptr(), c"off".as_ptr());
2015 ffi::cobre_highs_set_double_option(
2016 highs,
2017 c"primal_feasibility_tolerance".as_ptr(),
2018 1e-7,
2019 );
2020 ffi::cobre_highs_set_double_option(highs, c"dual_feasibility_tolerance".as_ptr(), 1e-7);
2021 }
2022
2023 let col_cost: [f64; 3] = [0.0, 1.0, 50.0];
2024 let col_lower: [f64; 3] = [0.0, 0.0, 0.0];
2025 let col_upper: [f64; 3] = [10.0, f64::INFINITY, 8.0];
2026 let row_lower: [f64; 2] = [6.0, 14.0];
2027 let row_upper: [f64; 2] = [6.0, 14.0];
2028 let a_start: [i32; 4] = [0, 2, 2, 3];
2029 let a_index: [i32; 3] = [0, 1, 1];
2030 let a_value: [f64; 3] = [1.0, 2.0, 1.0];
2031
2032 unsafe {
2034 ffi::cobre_highs_pass_lp(
2035 highs,
2036 3,
2037 2,
2038 3,
2039 ffi::HIGHS_MATRIX_FORMAT_COLWISE,
2040 ffi::HIGHS_OBJ_SENSE_MINIMIZE,
2041 0.0,
2042 col_cost.as_ptr(),
2043 col_lower.as_ptr(),
2044 col_upper.as_ptr(),
2045 row_lower.as_ptr(),
2046 row_upper.as_ptr(),
2047 a_start.as_ptr(),
2048 a_index.as_ptr(),
2049 a_value.as_ptr(),
2050 );
2051 ffi::cobre_highs_set_int_option(highs, c"simplex_iteration_limit".as_ptr(), 0);
2052 ffi::cobre_highs_run(highs);
2053 }
2054 let status1 = unsafe { ffi::cobre_highs_get_model_status(highs) };
2055 assert_eq!(status1, ffi::HIGHS_MODEL_STATUS_ITERATION_LIMIT);
2056
2057 unsafe {
2059 ffi::cobre_highs_set_string_option(highs, c"solver".as_ptr(), c"simplex".as_ptr());
2060 ffi::cobre_highs_set_int_option(highs, c"simplex_strategy".as_ptr(), 1);
2061 ffi::cobre_highs_set_string_option(highs, c"presolve".as_ptr(), c"off".as_ptr());
2062 ffi::cobre_highs_set_double_option(
2063 highs,
2064 c"primal_feasibility_tolerance".as_ptr(),
2065 1e-7,
2066 );
2067 ffi::cobre_highs_set_double_option(highs, c"dual_feasibility_tolerance".as_ptr(), 1e-7);
2068 ffi::cobre_highs_set_string_option(highs, c"parallel".as_ptr(), c"off".as_ptr());
2069 ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0);
2070 ffi::cobre_highs_set_int_option(highs, c"simplex_iteration_limit".as_ptr(), i32::MAX);
2072 }
2073
2074 unsafe { ffi::cobre_highs_clear_solver(highs) };
2076 unsafe { ffi::cobre_highs_run(highs) };
2077 let status2 = unsafe { ffi::cobre_highs_get_model_status(highs) };
2078 let obj = unsafe { ffi::cobre_highs_get_objective_value(highs) };
2079 assert_eq!(
2080 status2,
2081 ffi::HIGHS_MODEL_STATUS_OPTIMAL,
2082 "after restoring defaults, second solve must be OPTIMAL, got {status2}"
2083 );
2084 assert!(
2085 (obj - 100.0).abs() < 1e-8,
2086 "objective after restore must be 100.0, got {obj}"
2087 );
2088
2089 unsafe { ffi::cobre_highs_destroy(highs) };
2090 }
2091
2092 #[test]
2097 fn test_research_iteration_limit_one_triggers_iteration_limit_status() {
2098 use crate::ffi;
2099
2100 let highs = unsafe { ffi::cobre_highs_create() };
2101 assert!(!highs.is_null());
2102
2103 unsafe { ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0) };
2104
2105 let col_cost: [f64; 3] = [0.0, 1.0, 50.0];
2106 let col_lower: [f64; 3] = [0.0, 0.0, 0.0];
2107 let col_upper: [f64; 3] = [10.0, f64::INFINITY, 8.0];
2108 let row_lower: [f64; 2] = [6.0, 14.0];
2109 let row_upper: [f64; 2] = [6.0, 14.0];
2110 let a_start: [i32; 4] = [0, 2, 2, 3];
2111 let a_index: [i32; 3] = [0, 1, 1];
2112 let a_value: [f64; 3] = [1.0, 2.0, 1.0];
2113
2114 unsafe {
2115 ffi::cobre_highs_pass_lp(
2116 highs,
2117 3,
2118 2,
2119 3,
2120 ffi::HIGHS_MATRIX_FORMAT_COLWISE,
2121 ffi::HIGHS_OBJ_SENSE_MINIMIZE,
2122 0.0,
2123 col_cost.as_ptr(),
2124 col_lower.as_ptr(),
2125 col_upper.as_ptr(),
2126 row_lower.as_ptr(),
2127 row_upper.as_ptr(),
2128 a_start.as_ptr(),
2129 a_index.as_ptr(),
2130 a_value.as_ptr(),
2131 );
2132 ffi::cobre_highs_set_int_option(highs, c"simplex_iteration_limit".as_ptr(), 1);
2133 ffi::cobre_highs_run(highs);
2134 }
2135
2136 let model_status = unsafe { ffi::cobre_highs_get_model_status(highs) };
2137 eprintln!("iteration_limit=1 model_status: {model_status}");
2138 assert!(
2141 model_status == ffi::HIGHS_MODEL_STATUS_ITERATION_LIMIT
2142 || model_status == ffi::HIGHS_MODEL_STATUS_OPTIMAL,
2143 "expected ITERATION_LIMIT or OPTIMAL, got {model_status}"
2144 );
2145
2146 unsafe { ffi::cobre_highs_destroy(highs) };
2147 }
2148}