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 {
266 retry_level_histogram: vec![0u64; 12],
267 ..SolverStatistics::default()
268 },
269 })
270 }
271
272 fn apply_default_config(handle: *mut c_void) -> Result<(), SolverError> {
278 for opt in &default_options() {
279 let status = unsafe { opt.apply(handle) };
281 if status == ffi::HIGHS_STATUS_ERROR {
282 return Err(SolverError::InternalError {
283 message: format!(
284 "HiGHS configuration failed: {}",
285 opt.name.to_str().unwrap_or("?")
286 ),
287 error_code: Some(status),
288 });
289 }
290 }
291 Ok(())
292 }
293
294 fn extract_solution_view(&mut self, solve_time_seconds: f64) -> SolutionView<'_> {
301 let status = unsafe {
303 ffi::cobre_highs_get_solution(
304 self.handle,
305 self.col_value.as_mut_ptr(),
306 self.col_dual.as_mut_ptr(),
307 self.row_value.as_mut_ptr(),
308 self.row_dual.as_mut_ptr(),
309 )
310 };
311 assert_ne!(
312 status,
313 ffi::HIGHS_STATUS_ERROR,
314 "cobre_highs_get_solution failed after optimal solve"
315 );
316
317 let objective = unsafe { ffi::cobre_highs_get_objective_value(self.handle) };
319
320 #[allow(clippy::cast_sign_loss)]
322 let iterations =
323 unsafe { ffi::cobre_highs_get_simplex_iteration_count(self.handle) } as u64;
324
325 SolutionView {
326 objective,
327 primal: &self.col_value[..self.num_cols],
328 dual: &self.row_dual[..self.num_rows],
329 reduced_costs: &self.col_dual[..self.num_cols],
330 iterations,
331 solve_time_seconds,
332 }
333 }
334
335 fn restore_default_settings(&mut self) {
342 for opt in &default_options() {
343 let status = unsafe { opt.apply(self.handle) };
345 debug_assert_eq!(
346 status,
347 ffi::HIGHS_STATUS_OK,
348 "restore_default_settings: option {:?} failed with status {status}",
349 opt.name,
350 );
351 }
352 }
353
354 fn run_once(&mut self) -> i32 {
356 let run_status = unsafe { ffi::cobre_highs_run(self.handle) };
358 if run_status == ffi::HIGHS_STATUS_ERROR {
359 return ffi::HIGHS_MODEL_STATUS_SOLVE_ERROR;
360 }
361 unsafe { ffi::cobre_highs_get_model_status(self.handle) }
363 }
364
365 fn set_iteration_limits(&mut self) {
377 let simplex_iter_limit = self.num_cols.saturating_mul(50).max(100_000);
378 unsafe {
381 #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
382 ffi::cobre_highs_set_int_option(
383 self.handle,
384 c"simplex_iteration_limit".as_ptr(),
385 simplex_iter_limit as i32,
386 );
387 ffi::cobre_highs_set_int_option(self.handle, c"ipm_iteration_limit".as_ptr(), 10_000);
388 }
389 }
390
391 fn restore_iteration_limits(&mut self) {
395 unsafe {
397 ffi::cobre_highs_set_int_option(
398 self.handle,
399 c"simplex_iteration_limit".as_ptr(),
400 i32::MAX,
401 );
402 ffi::cobre_highs_set_int_option(self.handle, c"ipm_iteration_limit".as_ptr(), i32::MAX);
403 }
404 }
405
406 fn interpret_terminal_status(
411 &mut self,
412 status: i32,
413 solve_time_seconds: f64,
414 ) -> Option<SolverError> {
415 match status {
416 ffi::HIGHS_MODEL_STATUS_OPTIMAL => {
417 None
419 }
420 ffi::HIGHS_MODEL_STATUS_INFEASIBLE => Some(SolverError::Infeasible),
421 ffi::HIGHS_MODEL_STATUS_UNBOUNDED_OR_INFEASIBLE => {
422 let mut has_dual_ray: i32 = 0;
426 let mut dual_buf = vec![0.0_f64; self.num_rows];
429 let dual_status = unsafe {
431 ffi::cobre_highs_get_dual_ray(
432 self.handle,
433 &raw mut has_dual_ray,
434 dual_buf.as_mut_ptr(),
435 )
436 };
437 if dual_status != ffi::HIGHS_STATUS_ERROR && has_dual_ray != 0 {
438 return Some(SolverError::Infeasible);
439 }
440 let mut has_primal_ray: i32 = 0;
441 let mut primal_buf = vec![0.0_f64; self.num_cols];
442 let primal_status = unsafe {
444 ffi::cobre_highs_get_primal_ray(
445 self.handle,
446 &raw mut has_primal_ray,
447 primal_buf.as_mut_ptr(),
448 )
449 };
450 if primal_status != ffi::HIGHS_STATUS_ERROR && has_primal_ray != 0 {
451 return Some(SolverError::Unbounded);
452 }
453 Some(SolverError::Infeasible)
454 }
455 ffi::HIGHS_MODEL_STATUS_UNBOUNDED => Some(SolverError::Unbounded),
456 ffi::HIGHS_MODEL_STATUS_TIME_LIMIT => Some(SolverError::TimeLimitExceeded {
457 elapsed_seconds: solve_time_seconds,
458 }),
459 ffi::HIGHS_MODEL_STATUS_ITERATION_LIMIT => {
460 #[allow(clippy::cast_sign_loss)]
462 let iterations =
463 unsafe { ffi::cobre_highs_get_simplex_iteration_count(self.handle) } as u64;
464 Some(SolverError::IterationLimit { iterations })
465 }
466 ffi::HIGHS_MODEL_STATUS_SOLVE_ERROR | ffi::HIGHS_MODEL_STATUS_UNKNOWN => {
467 None
469 }
470 other => Some(SolverError::InternalError {
471 message: format!("HiGHS returned unexpected model status {other}"),
472 error_code: Some(other),
473 }),
474 }
475 }
476
477 fn convert_to_i32_scratch(&mut self, source: &[usize]) -> &[i32] {
481 if source.len() > self.scratch_i32.len() {
482 self.scratch_i32.resize(source.len(), 0);
483 }
484 for (i, &v) in source.iter().enumerate() {
485 debug_assert!(
486 i32::try_from(v).is_ok(),
487 "usize index {v} overflows i32::MAX at position {i}"
488 );
489 #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
491 {
492 self.scratch_i32[i] = v as i32;
493 }
494 }
495 &self.scratch_i32[..source.len()]
496 }
497
498 fn retry_escalation(&mut self, is_unbounded: bool) -> Result<RetryOutcome, (u64, SolverError)> {
508 let phase1_wall_budget = 15.0_f64;
535 let phase2_wall_budget = 30.0_f64;
536 let overall_budget = 120.0_f64;
537 let num_retry_levels = 12_u32;
538
539 let retry_start = Instant::now();
540 let mut retry_attempts: u64 = 0;
541 let mut terminal_err: Option<SolverError> = None;
542 let mut found_optimal = false;
543 let mut optimal_time = 0.0_f64;
544 let mut optimal_iterations: u64 = 0;
545 let mut optimal_level = 0_u32;
546
547 for level in 0..num_retry_levels {
548 if retry_start.elapsed().as_secs_f64() >= overall_budget {
550 break;
551 }
552
553 self.apply_retry_level_options(level);
554
555 retry_attempts += 1;
556
557 let t_retry = Instant::now();
558 let retry_status = self.run_once();
559 let retry_time = t_retry.elapsed().as_secs_f64();
560
561 if retry_status == ffi::HIGHS_MODEL_STATUS_OPTIMAL {
562 #[allow(clippy::cast_sign_loss)]
565 let iters =
566 unsafe { ffi::cobre_highs_get_simplex_iteration_count(self.handle) } as u64;
567 found_optimal = true;
568 optimal_time = retry_time;
569 optimal_iterations = iters;
570 optimal_level = level;
571 break;
572 }
573
574 let level_budget = if level <= 4 {
580 phase1_wall_budget
581 } else {
582 phase2_wall_budget
583 };
584 let budget_exceeded = retry_time > level_budget;
585 let retryable = retry_status == ffi::HIGHS_MODEL_STATUS_UNBOUNDED
586 || retry_status == ffi::HIGHS_MODEL_STATUS_ITERATION_LIMIT
587 || budget_exceeded;
588 if !retryable {
589 if let Some(e) = self.interpret_terminal_status(retry_status, retry_time) {
590 terminal_err = Some(e);
591 break;
592 }
593 }
594 }
597
598 self.restore_default_settings();
602 self.restore_iteration_limits();
603 unsafe {
604 ffi::cobre_highs_set_int_option(self.handle, c"user_objective_scale".as_ptr(), 0);
605 ffi::cobre_highs_set_int_option(self.handle, c"user_bound_scale".as_ptr(), 0);
606 }
607
608 if found_optimal {
609 return Ok(RetryOutcome {
610 attempts: retry_attempts,
611 solve_time: optimal_time,
612 iterations: optimal_iterations,
613 level: optimal_level,
614 });
615 }
616
617 Err((
618 retry_attempts,
619 terminal_err.unwrap_or_else(|| {
620 if is_unbounded {
622 SolverError::Unbounded
623 } else {
624 SolverError::NumericalDifficulty {
625 message:
626 "HiGHS failed to reach optimality after all retry escalation levels"
627 .to_string(),
628 }
629 }
630 }),
631 ))
632 }
633
634 fn apply_retry_level_options(&mut self, level: u32) {
647 match level {
648 0 => {
652 unsafe { ffi::cobre_highs_clear_solver(self.handle) };
653 self.set_iteration_limits();
654 }
655 1 => unsafe {
657 ffi::cobre_highs_set_string_option(
658 self.handle,
659 c"presolve".as_ptr(),
660 c"on".as_ptr(),
661 );
662 },
663 2 => unsafe {
666 ffi::cobre_highs_set_int_option(self.handle, c"simplex_strategy".as_ptr(), 1);
667 },
668 3 => unsafe {
671 ffi::cobre_highs_set_double_option(
672 self.handle,
673 c"primal_feasibility_tolerance".as_ptr(),
674 1e-6,
675 );
676 ffi::cobre_highs_set_double_option(
677 self.handle,
678 c"dual_feasibility_tolerance".as_ptr(),
679 1e-6,
680 );
681 },
682 4 => unsafe {
685 ffi::cobre_highs_set_string_option(
686 self.handle,
687 c"solver".as_ptr(),
688 c"ipm".as_ptr(),
689 );
690 },
691
692 _ => self.apply_extended_retry_options(level),
696 }
697 }
698
699 fn apply_extended_retry_options(&mut self, level: u32) {
705 self.restore_default_settings();
706 self.set_iteration_limits();
707 unsafe {
710 ffi::cobre_highs_set_string_option(self.handle, c"presolve".as_ptr(), c"on".as_ptr());
711 }
712 match level {
713 5 => unsafe {
714 ffi::cobre_highs_set_int_option(self.handle, c"simplex_scale_strategy".as_ptr(), 3);
715 },
716 6 => unsafe {
717 ffi::cobre_highs_set_int_option(self.handle, c"simplex_strategy".as_ptr(), 1);
718 ffi::cobre_highs_set_int_option(self.handle, c"simplex_scale_strategy".as_ptr(), 4);
719 },
720 7 => unsafe {
721 ffi::cobre_highs_set_int_option(self.handle, c"simplex_scale_strategy".as_ptr(), 3);
722 ffi::cobre_highs_set_double_option(
723 self.handle,
724 c"primal_feasibility_tolerance".as_ptr(),
725 1e-6,
726 );
727 ffi::cobre_highs_set_double_option(
728 self.handle,
729 c"dual_feasibility_tolerance".as_ptr(),
730 1e-6,
731 );
732 },
733 8 => unsafe {
734 ffi::cobre_highs_set_int_option(self.handle, c"user_objective_scale".as_ptr(), -10);
735 },
736 9 => unsafe {
737 ffi::cobre_highs_set_int_option(self.handle, c"simplex_strategy".as_ptr(), 1);
738 ffi::cobre_highs_set_int_option(self.handle, c"user_objective_scale".as_ptr(), -10);
739 ffi::cobre_highs_set_int_option(self.handle, c"user_bound_scale".as_ptr(), -5);
740 },
741 10 => unsafe {
742 ffi::cobre_highs_set_int_option(self.handle, c"user_objective_scale".as_ptr(), -13);
743 ffi::cobre_highs_set_int_option(self.handle, c"user_bound_scale".as_ptr(), -8);
744 ffi::cobre_highs_set_double_option(
745 self.handle,
746 c"primal_feasibility_tolerance".as_ptr(),
747 1e-6,
748 );
749 ffi::cobre_highs_set_double_option(
750 self.handle,
751 c"dual_feasibility_tolerance".as_ptr(),
752 1e-6,
753 );
754 },
755 11 => unsafe {
756 ffi::cobre_highs_set_string_option(
757 self.handle,
758 c"solver".as_ptr(),
759 c"ipm".as_ptr(),
760 );
761 ffi::cobre_highs_set_int_option(self.handle, c"user_objective_scale".as_ptr(), -10);
762 ffi::cobre_highs_set_int_option(self.handle, c"user_bound_scale".as_ptr(), -5);
763 ffi::cobre_highs_set_double_option(
764 self.handle,
765 c"primal_feasibility_tolerance".as_ptr(),
766 1e-6,
767 );
768 ffi::cobre_highs_set_double_option(
769 self.handle,
770 c"dual_feasibility_tolerance".as_ptr(),
771 1e-6,
772 );
773 },
774 _ => unreachable!(),
775 }
776 }
777}
778
779impl Drop for HighsSolver {
780 fn drop(&mut self) {
781 unsafe { ffi::cobre_highs_destroy(self.handle) };
783 }
784}
785
786#[must_use]
800pub fn highs_version() -> String {
801 let major = unsafe { crate::ffi::cobre_highs_version_major() };
805 let minor = unsafe { crate::ffi::cobre_highs_version_minor() };
806 let patch = unsafe { crate::ffi::cobre_highs_version_patch() };
807 format!("{major}.{minor}.{patch}")
808}
809
810impl SolverInterface for HighsSolver {
811 fn name(&self) -> &'static str {
812 "HiGHS"
813 }
814
815 fn solver_name_version(&self) -> String {
816 format!("HiGHS {}", highs_version())
817 }
818
819 fn load_model(&mut self, template: &StageTemplate) {
820 let t0 = Instant::now();
821 assert!(
831 i32::try_from(template.num_cols).is_ok(),
832 "num_cols {} overflows i32: LP exceeds HiGHS API limit",
833 template.num_cols
834 );
835 assert!(
836 i32::try_from(template.num_rows).is_ok(),
837 "num_rows {} overflows i32: LP exceeds HiGHS API limit",
838 template.num_rows
839 );
840 assert!(
841 i32::try_from(template.num_nz).is_ok(),
842 "num_nz {} overflows i32: LP exceeds HiGHS API limit",
843 template.num_nz
844 );
845 #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
847 let num_col = template.num_cols as i32;
848 #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
849 let num_row = template.num_rows as i32;
850 #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
851 let num_nz = template.num_nz as i32;
852 let status = unsafe {
853 ffi::cobre_highs_pass_lp(
854 self.handle,
855 num_col,
856 num_row,
857 num_nz,
858 ffi::HIGHS_MATRIX_FORMAT_COLWISE,
859 ffi::HIGHS_OBJ_SENSE_MINIMIZE,
860 0.0, template.objective.as_ptr(),
862 template.col_lower.as_ptr(),
863 template.col_upper.as_ptr(),
864 template.row_lower.as_ptr(),
865 template.row_upper.as_ptr(),
866 template.col_starts.as_ptr(),
867 template.row_indices.as_ptr(),
868 template.values.as_ptr(),
869 )
870 };
871
872 assert_ne!(
873 status,
874 ffi::HIGHS_STATUS_ERROR,
875 "cobre_highs_pass_lp failed with status {status}"
876 );
877
878 self.num_cols = template.num_cols;
879 self.num_rows = template.num_rows;
880 self.has_model = true;
881
882 self.col_value.resize(self.num_cols, 0.0);
885 self.col_dual.resize(self.num_cols, 0.0);
886 self.row_value.resize(self.num_rows, 0.0);
887 self.row_dual.resize(self.num_rows, 0.0);
888
889 self.basis_col_i32.resize(self.num_cols, 0);
892 self.basis_row_i32.resize(self.num_rows, 0);
893 self.stats.total_load_model_time_seconds += t0.elapsed().as_secs_f64();
894 self.stats.load_model_count += 1;
895 }
896
897 fn add_rows(&mut self, cuts: &RowBatch) {
898 let t0 = Instant::now();
899 assert!(
900 i32::try_from(cuts.num_rows).is_ok(),
901 "cuts.num_rows {} overflows i32: RowBatch exceeds HiGHS API limit",
902 cuts.num_rows
903 );
904 assert!(
905 i32::try_from(cuts.col_indices.len()).is_ok(),
906 "cuts nnz {} overflows i32: RowBatch exceeds HiGHS API limit",
907 cuts.col_indices.len()
908 );
909 #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
911 let num_new_row = cuts.num_rows as i32;
912 #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
913 let num_new_nz = cuts.col_indices.len() as i32;
914
915 let status = unsafe {
923 ffi::cobre_highs_add_rows(
924 self.handle,
925 num_new_row,
926 cuts.row_lower.as_ptr(),
927 cuts.row_upper.as_ptr(),
928 num_new_nz,
929 cuts.row_starts.as_ptr(),
930 cuts.col_indices.as_ptr(),
931 cuts.values.as_ptr(),
932 )
933 };
934
935 assert_ne!(
936 status,
937 ffi::HIGHS_STATUS_ERROR,
938 "cobre_highs_add_rows failed with status {status}"
939 );
940
941 self.num_rows += cuts.num_rows;
942
943 self.row_value.resize(self.num_rows, 0.0);
945 self.row_dual.resize(self.num_rows, 0.0);
946
947 self.basis_row_i32.resize(self.num_rows, 0);
949 self.stats.total_add_rows_time_seconds += t0.elapsed().as_secs_f64();
950 self.stats.add_rows_count += 1;
951 }
952
953 fn set_row_bounds(&mut self, indices: &[usize], lower: &[f64], upper: &[f64]) {
954 assert!(
955 indices.len() == lower.len() && indices.len() == upper.len(),
956 "set_row_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_row_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 {
981 ffi::cobre_highs_change_rows_bounds_by_set(
982 self.handle,
983 num_entries,
984 self.convert_to_i32_scratch(indices).as_ptr(),
985 lower.as_ptr(),
986 upper.as_ptr(),
987 )
988 };
989
990 assert_ne!(
991 status,
992 ffi::HIGHS_STATUS_ERROR,
993 "cobre_highs_change_rows_bounds_by_set failed with status {status}"
994 );
995 self.stats.total_set_bounds_time_seconds += t0.elapsed().as_secs_f64();
996 }
997
998 fn set_col_bounds(&mut self, indices: &[usize], lower: &[f64], upper: &[f64]) {
999 assert!(
1000 indices.len() == lower.len() && indices.len() == upper.len(),
1001 "set_col_bounds: indices ({}), lower ({}), and upper ({}) must have equal length",
1002 indices.len(),
1003 lower.len(),
1004 upper.len()
1005 );
1006 if indices.is_empty() {
1007 return;
1008 }
1009
1010 assert!(
1011 i32::try_from(indices.len()).is_ok(),
1012 "set_col_bounds: indices.len() {} overflows i32",
1013 indices.len()
1014 );
1015 #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
1016 let num_entries = indices.len() as i32;
1017
1018 let t0 = Instant::now();
1019 let status = unsafe {
1025 ffi::cobre_highs_change_cols_bounds_by_set(
1026 self.handle,
1027 num_entries,
1028 self.convert_to_i32_scratch(indices).as_ptr(),
1029 lower.as_ptr(),
1030 upper.as_ptr(),
1031 )
1032 };
1033
1034 assert_ne!(
1035 status,
1036 ffi::HIGHS_STATUS_ERROR,
1037 "cobre_highs_change_cols_bounds_by_set failed with status {status}"
1038 );
1039 self.stats.total_set_bounds_time_seconds += t0.elapsed().as_secs_f64();
1040 }
1041
1042 fn solve(&mut self) -> Result<SolutionView<'_>, SolverError> {
1043 assert!(
1044 self.has_model,
1045 "solve called without a loaded model — call load_model first"
1046 );
1047
1048 self.set_iteration_limits();
1054
1055 let t0 = Instant::now();
1056 let model_status = self.run_once();
1057 let solve_time = t0.elapsed().as_secs_f64();
1058
1059 self.stats.solve_count += 1;
1060
1061 if model_status == ffi::HIGHS_MODEL_STATUS_OPTIMAL {
1062 #[allow(clippy::cast_sign_loss)]
1067 let iterations =
1068 unsafe { ffi::cobre_highs_get_simplex_iteration_count(self.handle) } as u64;
1069 self.stats.success_count += 1;
1070 self.stats.first_try_successes += 1;
1071 self.stats.total_iterations += iterations;
1072 self.stats.total_solve_time_seconds += solve_time;
1073 self.restore_iteration_limits();
1074 return Ok(self.extract_solution_view(solve_time));
1075 }
1076
1077 let is_unbounded = model_status == ffi::HIGHS_MODEL_STATUS_UNBOUNDED;
1088 let initial_retryable = is_unbounded
1089 || model_status == ffi::HIGHS_MODEL_STATUS_ITERATION_LIMIT
1090 || model_status == ffi::HIGHS_MODEL_STATUS_TIME_LIMIT
1091 || solve_time > 15.0;
1092 if !initial_retryable {
1093 if let Some(terminal_err) = self.interpret_terminal_status(model_status, solve_time) {
1094 self.restore_iteration_limits();
1095 self.stats.failure_count += 1;
1096 return Err(terminal_err);
1097 }
1098 }
1099
1100 match self.retry_escalation(is_unbounded) {
1102 Ok(outcome) => {
1103 self.stats.retry_count += outcome.attempts;
1104 self.stats.success_count += 1;
1105 self.stats.total_iterations += outcome.iterations;
1106 self.stats.total_solve_time_seconds += outcome.solve_time;
1107 self.stats.retry_level_histogram[outcome.level as usize] += 1;
1108 Ok(self.extract_solution_view(outcome.solve_time))
1109 }
1110 Err((attempts, err)) => {
1111 self.stats.retry_count += attempts;
1112 self.stats.failure_count += 1;
1113 Err(err)
1114 }
1115 }
1116 }
1117
1118 fn reset(&mut self) {
1119 let status = unsafe { ffi::cobre_highs_clear_solver(self.handle) };
1124 debug_assert_ne!(
1125 status,
1126 ffi::HIGHS_STATUS_ERROR,
1127 "cobre_highs_clear_solver failed — HiGHS internal state may be inconsistent"
1128 );
1129 self.num_cols = 0;
1131 self.num_rows = 0;
1132 self.has_model = false;
1133 }
1136
1137 fn get_basis(&mut self, out: &mut crate::types::Basis) {
1138 assert!(
1139 self.has_model,
1140 "get_basis called without a loaded model — call load_model first"
1141 );
1142
1143 out.col_status.resize(self.num_cols, 0);
1144 out.row_status.resize(self.num_rows, 0);
1145
1146 let get_status = unsafe {
1152 ffi::cobre_highs_get_basis(
1153 self.handle,
1154 out.col_status.as_mut_ptr(),
1155 out.row_status.as_mut_ptr(),
1156 )
1157 };
1158
1159 assert_ne!(
1160 get_status,
1161 ffi::HIGHS_STATUS_ERROR,
1162 "cobre_highs_get_basis failed: basis must exist after a successful solve (programming error)"
1163 );
1164 }
1165
1166 fn solve_with_basis(
1167 &mut self,
1168 basis: &crate::types::Basis,
1169 ) -> Result<crate::types::SolutionView<'_>, SolverError> {
1170 assert!(
1171 self.has_model,
1172 "solve_with_basis called without a loaded model — call load_model first"
1173 );
1174 assert!(
1175 basis.col_status.len() == self.num_cols,
1176 "basis column count {} does not match LP column count {}",
1177 basis.col_status.len(),
1178 self.num_cols
1179 );
1180
1181 self.stats.basis_offered += 1;
1183
1184 self.basis_col_i32[..self.num_cols].copy_from_slice(&basis.col_status);
1187
1188 let basis_rows = basis.row_status.len();
1192 let lp_rows = self.num_rows;
1193 let copy_len = basis_rows.min(lp_rows);
1194 self.basis_row_i32[..copy_len].copy_from_slice(&basis.row_status[..copy_len]);
1195 if lp_rows > basis_rows {
1196 self.basis_row_i32[basis_rows..lp_rows].fill(ffi::HIGHS_BASIS_STATUS_BASIC);
1197 }
1198
1199 let basis_set_start = Instant::now();
1206 let set_status = unsafe {
1207 ffi::cobre_highs_set_basis(
1208 self.handle,
1209 self.basis_col_i32.as_ptr(),
1210 self.basis_row_i32.as_ptr(),
1211 )
1212 };
1213 self.stats.total_basis_set_time_seconds += basis_set_start.elapsed().as_secs_f64();
1214
1215 if set_status == ffi::HIGHS_STATUS_ERROR {
1217 self.stats.basis_rejections += 1;
1218 debug_assert!(false, "raw basis rejected; falling back to cold-start");
1219 }
1220
1221 self.solve()
1223 }
1224
1225 fn statistics(&self) -> SolverStatistics {
1226 self.stats.clone()
1227 }
1228}
1229
1230#[cfg(feature = "test-support")]
1236impl HighsSolver {
1237 #[must_use]
1245 pub fn raw_handle(&self) -> *mut std::os::raw::c_void {
1246 self.handle
1247 }
1248}
1249
1250#[cfg(test)]
1251mod tests {
1252 use super::HighsSolver;
1253 use crate::{
1254 SolverInterface,
1255 types::{Basis, RowBatch, StageTemplate},
1256 };
1257
1258 fn make_fixture_stage_template() -> StageTemplate {
1271 StageTemplate {
1272 num_cols: 3,
1273 num_rows: 2,
1274 num_nz: 3,
1275 col_starts: vec![0_i32, 2, 2, 3],
1276 row_indices: vec![0_i32, 1, 1],
1277 values: vec![1.0, 2.0, 1.0],
1278 col_lower: vec![0.0, 0.0, 0.0],
1279 col_upper: vec![10.0, f64::INFINITY, 8.0],
1280 objective: vec![0.0, 1.0, 50.0],
1281 row_lower: vec![6.0, 14.0],
1282 row_upper: vec![6.0, 14.0],
1283 n_state: 1,
1284 n_transfer: 0,
1285 n_dual_relevant: 1,
1286 n_hydro: 1,
1287 max_par_order: 0,
1288 col_scale: Vec::new(),
1289 row_scale: Vec::new(),
1290 }
1291 }
1292
1293 fn make_fixture_row_batch() -> RowBatch {
1297 RowBatch {
1298 num_rows: 2,
1299 row_starts: vec![0_i32, 2, 4],
1300 col_indices: vec![0_i32, 1, 0, 1],
1301 values: vec![-5.0, 1.0, 3.0, 1.0],
1302 row_lower: vec![20.0, 80.0],
1303 row_upper: vec![f64::INFINITY, f64::INFINITY],
1304 }
1305 }
1306
1307 #[test]
1308 fn test_highs_solver_create_and_name() {
1309 let solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1310 assert_eq!(solver.name(), "HiGHS");
1311 }
1313
1314 #[test]
1315 fn test_highs_solver_send_bound() {
1316 fn assert_send<T: Send>() {}
1317 assert_send::<HighsSolver>();
1318 }
1319
1320 #[test]
1321 fn test_highs_solver_statistics_initial() {
1322 let solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1323 let stats = solver.statistics();
1324 assert_eq!(stats.solve_count, 0);
1325 assert_eq!(stats.success_count, 0);
1326 assert_eq!(stats.failure_count, 0);
1327 assert_eq!(stats.total_iterations, 0);
1328 assert_eq!(stats.retry_count, 0);
1329 assert_eq!(stats.total_solve_time_seconds, 0.0);
1330 }
1331
1332 #[test]
1333 fn test_highs_load_model_updates_dimensions() {
1334 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1335 let template = make_fixture_stage_template();
1336
1337 solver.load_model(&template);
1338
1339 assert_eq!(solver.num_cols, 3, "num_cols must be 3 after load_model");
1340 assert_eq!(solver.num_rows, 2, "num_rows must be 2 after load_model");
1341 assert_eq!(
1342 solver.col_value.len(),
1343 3,
1344 "col_value buffer must be resized to num_cols"
1345 );
1346 assert_eq!(
1347 solver.col_dual.len(),
1348 3,
1349 "col_dual buffer must be resized to num_cols"
1350 );
1351 assert_eq!(
1352 solver.row_value.len(),
1353 2,
1354 "row_value buffer must be resized to num_rows"
1355 );
1356 assert_eq!(
1357 solver.row_dual.len(),
1358 2,
1359 "row_dual buffer must be resized to num_rows"
1360 );
1361 }
1362
1363 #[test]
1364 fn test_highs_add_rows_updates_dimensions() {
1365 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1366 let template = make_fixture_stage_template();
1367 let cuts = make_fixture_row_batch();
1368
1369 solver.load_model(&template);
1370 solver.add_rows(&cuts);
1371
1372 assert_eq!(solver.num_rows, 4, "num_rows must be 4 after add_rows");
1374 assert_eq!(
1375 solver.row_dual.len(),
1376 4,
1377 "row_dual buffer must be resized to 4 after add_rows"
1378 );
1379 assert_eq!(
1380 solver.row_value.len(),
1381 4,
1382 "row_value buffer must be resized to 4 after add_rows"
1383 );
1384 assert_eq!(solver.num_cols, 3, "num_cols must be unchanged by add_rows");
1386 }
1387
1388 #[test]
1389 fn test_highs_set_row_bounds_no_panic() {
1390 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1391 let template = make_fixture_stage_template();
1392 solver.load_model(&template);
1393
1394 solver.set_row_bounds(&[0], &[4.0], &[4.0]);
1396 }
1397
1398 #[test]
1399 fn test_highs_set_col_bounds_no_panic() {
1400 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1401 let template = make_fixture_stage_template();
1402 solver.load_model(&template);
1403
1404 solver.set_col_bounds(&[1], &[10.0], &[f64::INFINITY]);
1406 }
1407
1408 #[test]
1409 fn test_highs_set_bounds_empty_no_panic() {
1410 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1411 let template = make_fixture_stage_template();
1412 solver.load_model(&template);
1413
1414 solver.set_row_bounds(&[], &[], &[]);
1416 solver.set_col_bounds(&[], &[], &[]);
1417 }
1418
1419 #[test]
1422 fn test_highs_solve_basic_lp() {
1423 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1424 let template = make_fixture_stage_template();
1425 solver.load_model(&template);
1426
1427 let solution = solver
1428 .solve()
1429 .expect("solve() must succeed on a feasible LP");
1430
1431 assert!(
1432 (solution.objective - 100.0).abs() < 1e-8,
1433 "objective must be 100.0, got {}",
1434 solution.objective
1435 );
1436 assert_eq!(solution.primal.len(), 3, "primal must have 3 elements");
1437 assert!(
1438 (solution.primal[0] - 6.0).abs() < 1e-8,
1439 "primal[0] (x0) must be 6.0, got {}",
1440 solution.primal[0]
1441 );
1442 assert!(
1443 (solution.primal[1] - 0.0).abs() < 1e-8,
1444 "primal[1] (x1) must be 0.0, got {}",
1445 solution.primal[1]
1446 );
1447 assert!(
1448 (solution.primal[2] - 2.0).abs() < 1e-8,
1449 "primal[2] (x2) must be 2.0, got {}",
1450 solution.primal[2]
1451 );
1452 }
1453
1454 #[test]
1458 fn test_highs_solve_with_cuts() {
1459 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1460 let template = make_fixture_stage_template();
1461 let cuts = make_fixture_row_batch();
1462 solver.load_model(&template);
1463 solver.add_rows(&cuts);
1464
1465 let solution = solver
1466 .solve()
1467 .expect("solve() must succeed on a feasible LP with cuts");
1468
1469 assert!(
1470 (solution.objective - 162.0).abs() < 1e-8,
1471 "objective must be 162.0, got {}",
1472 solution.objective
1473 );
1474 assert!(
1475 (solution.primal[0] - 6.0).abs() < 1e-8,
1476 "primal[0] must be 6.0, got {}",
1477 solution.primal[0]
1478 );
1479 assert!(
1480 (solution.primal[1] - 62.0).abs() < 1e-8,
1481 "primal[1] must be 62.0, got {}",
1482 solution.primal[1]
1483 );
1484 assert!(
1485 (solution.primal[2] - 2.0).abs() < 1e-8,
1486 "primal[2] must be 2.0, got {}",
1487 solution.primal[2]
1488 );
1489 }
1490
1491 #[test]
1494 fn test_highs_solve_after_rhs_patch() {
1495 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1496 let template = make_fixture_stage_template();
1497 let cuts = make_fixture_row_batch();
1498 solver.load_model(&template);
1499 solver.add_rows(&cuts);
1500
1501 solver.set_row_bounds(&[0], &[4.0], &[4.0]);
1503
1504 let solution = solver
1505 .solve()
1506 .expect("solve() must succeed after RHS patch");
1507
1508 assert!(
1509 (solution.objective - 368.0).abs() < 1e-8,
1510 "objective must be 368.0, got {}",
1511 solution.objective
1512 );
1513 }
1514
1515 #[test]
1517 fn test_highs_solve_statistics_increment() {
1518 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1519 let template = make_fixture_stage_template();
1520 solver.load_model(&template);
1521
1522 solver.solve().expect("first solve must succeed");
1523 solver.solve().expect("second solve must succeed");
1524
1525 let stats = solver.statistics();
1526 assert_eq!(stats.solve_count, 2, "solve_count must be 2");
1527 assert_eq!(stats.success_count, 2, "success_count must be 2");
1528 assert_eq!(stats.failure_count, 0, "failure_count must be 0");
1529 assert!(
1530 stats.total_iterations > 0,
1531 "total_iterations must be positive"
1532 );
1533 }
1534
1535 #[test]
1537 fn test_highs_reset_preserves_stats() {
1538 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1539 let template = make_fixture_stage_template();
1540 solver.load_model(&template);
1541 solver.solve().expect("solve must succeed");
1542
1543 let stats_before = solver.statistics();
1544 assert_eq!(
1545 stats_before.solve_count, 1,
1546 "solve_count must be 1 before reset"
1547 );
1548
1549 solver.reset();
1550
1551 let stats_after = solver.statistics();
1552 assert_eq!(
1553 stats_after.solve_count, stats_before.solve_count,
1554 "solve_count must be unchanged after reset"
1555 );
1556 assert_eq!(
1557 stats_after.success_count, stats_before.success_count,
1558 "success_count must be unchanged after reset"
1559 );
1560 assert_eq!(
1561 stats_after.total_iterations, stats_before.total_iterations,
1562 "total_iterations must be unchanged after reset"
1563 );
1564 }
1565
1566 #[test]
1568 fn test_highs_solve_iterations_positive() {
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 let solution = solver.solve().expect("solve must succeed");
1574 assert!(
1575 solution.iterations > 0,
1576 "iterations must be positive, got {}",
1577 solution.iterations
1578 );
1579 }
1580
1581 #[test]
1583 fn test_highs_solve_time_positive() {
1584 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1585 let template = make_fixture_stage_template();
1586 solver.load_model(&template);
1587
1588 let solution = solver.solve().expect("solve must succeed");
1589 assert!(
1590 solution.solve_time_seconds > 0.0,
1591 "solve_time_seconds must be positive, got {}",
1592 solution.solve_time_seconds
1593 );
1594 }
1595
1596 #[test]
1599 fn test_highs_solve_statistics_single() {
1600 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1601 let template = make_fixture_stage_template();
1602 solver.load_model(&template);
1603
1604 solver.solve().expect("solve must succeed");
1605
1606 let stats = solver.statistics();
1607 assert_eq!(stats.solve_count, 1, "solve_count must be 1");
1608 assert_eq!(stats.success_count, 1, "success_count must be 1");
1609 assert_eq!(stats.failure_count, 0, "failure_count must be 0");
1610 assert!(
1611 stats.total_iterations > 0,
1612 "total_iterations must be positive after a successful solve"
1613 );
1614 }
1615
1616 #[test]
1619 fn test_get_basis_valid_status_codes() {
1620 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1621 let template = make_fixture_stage_template();
1622 solver.load_model(&template);
1623 solver.solve().expect("solve must succeed before get_basis");
1624
1625 let mut basis = Basis::new(0, 0);
1626 solver.get_basis(&mut basis);
1627
1628 for &code in &basis.col_status {
1629 assert!(
1630 (0..=4).contains(&code),
1631 "col_status code {code} is outside valid HiGHS range 0..=4"
1632 );
1633 }
1634 for &code in &basis.row_status {
1635 assert!(
1636 (0..=4).contains(&code),
1637 "row_status code {code} is outside valid HiGHS range 0..=4"
1638 );
1639 }
1640 }
1641
1642 #[test]
1645 fn test_get_basis_resizes_output() {
1646 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1647 let template = make_fixture_stage_template();
1648 solver.load_model(&template);
1649 solver.solve().expect("solve must succeed before get_basis");
1650
1651 let mut basis = Basis::new(0, 0);
1652 assert_eq!(
1653 basis.col_status.len(),
1654 0,
1655 "initial col_status must be empty"
1656 );
1657 assert_eq!(
1658 basis.row_status.len(),
1659 0,
1660 "initial row_status must be empty"
1661 );
1662
1663 solver.get_basis(&mut basis);
1664
1665 assert_eq!(
1666 basis.col_status.len(),
1667 3,
1668 "col_status must be resized to 3 (num_cols of SS1.1)"
1669 );
1670 assert_eq!(
1671 basis.row_status.len(),
1672 2,
1673 "row_status must be resized to 2 (num_rows of SS1.1)"
1674 );
1675 }
1676
1677 #[test]
1680 fn test_solve_with_basis_warm_start() {
1681 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1682 let template = make_fixture_stage_template();
1683 solver.load_model(&template);
1684 solver.solve().expect("cold-start solve must succeed");
1685
1686 let mut basis = Basis::new(0, 0);
1687 solver.get_basis(&mut basis);
1688
1689 solver.load_model(&template);
1691 let result = solver
1692 .solve_with_basis(&basis)
1693 .expect("warm-start solve must succeed");
1694
1695 assert!(
1696 (result.objective - 100.0).abs() < 1e-8,
1697 "warm-start objective must be 100.0, got {}",
1698 result.objective
1699 );
1700 assert!(
1701 result.iterations <= 1,
1702 "warm-start from exact basis must use at most 1 iteration, got {}",
1703 result.iterations
1704 );
1705
1706 let stats = solver.statistics();
1707 assert_eq!(
1708 stats.basis_rejections, 0,
1709 "basis_rejections must be 0 when raw basis is accepted, got {}",
1710 stats.basis_rejections
1711 );
1712 }
1713
1714 #[test]
1718 fn test_solve_with_basis_dimension_mismatch() {
1719 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1720 let template = make_fixture_stage_template();
1721 let cuts = make_fixture_row_batch();
1722
1723 solver.load_model(&template);
1725 solver.solve().expect("SS1.1 solve must succeed");
1726 let mut basis = Basis::new(0, 0);
1727 solver.get_basis(&mut basis);
1728 assert_eq!(
1729 basis.row_status.len(),
1730 2,
1731 "captured basis must have 2 row statuses"
1732 );
1733
1734 solver.load_model(&template);
1736 solver.add_rows(&cuts);
1737 assert_eq!(solver.num_rows, 4, "LP must have 4 rows after add_rows");
1738
1739 let result = solver
1741 .solve_with_basis(&basis)
1742 .expect("solve with dimension-mismatched basis must succeed");
1743
1744 assert!(
1745 (result.objective - 162.0).abs() < 1e-8,
1746 "objective with both cuts active must be 162.0, got {}",
1747 result.objective
1748 );
1749 }
1750}
1751
1752#[cfg(test)]
1764#[allow(clippy::doc_markdown)]
1765mod research_tests_ticket_023 {
1766 unsafe fn research_load_ss11_lp(highs: *mut std::os::raw::c_void) {
1777 use crate::ffi;
1778 let col_cost: [f64; 3] = [0.0, 1.0, 50.0];
1779 let col_lower: [f64; 3] = [0.0, 0.0, 0.0];
1780 let col_upper: [f64; 3] = [10.0, f64::INFINITY, 8.0];
1781 let row_lower: [f64; 2] = [6.0, 14.0];
1782 let row_upper: [f64; 2] = [6.0, 14.0];
1783 let a_start: [i32; 4] = [0, 2, 2, 3];
1784 let a_index: [i32; 3] = [0, 1, 1];
1785 let a_value: [f64; 3] = [1.0, 2.0, 1.0];
1786 let status = unsafe {
1788 ffi::cobre_highs_pass_lp(
1789 highs,
1790 3,
1791 2,
1792 3,
1793 ffi::HIGHS_MATRIX_FORMAT_COLWISE,
1794 ffi::HIGHS_OBJ_SENSE_MINIMIZE,
1795 0.0,
1796 col_cost.as_ptr(),
1797 col_lower.as_ptr(),
1798 col_upper.as_ptr(),
1799 row_lower.as_ptr(),
1800 row_upper.as_ptr(),
1801 a_start.as_ptr(),
1802 a_index.as_ptr(),
1803 a_value.as_ptr(),
1804 )
1805 };
1806 assert_eq!(
1807 status,
1808 ffi::HIGHS_STATUS_OK,
1809 "research_load_ss11_lp pass_lp failed"
1810 );
1811 }
1812
1813 #[test]
1819 fn test_research_probe_limit_status_on_ss11_lp() {
1820 use crate::ffi;
1821
1822 let highs = unsafe { ffi::cobre_highs_create() };
1824 assert!(!highs.is_null());
1825 unsafe { ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0) };
1826 unsafe { research_load_ss11_lp(highs) };
1827 let _ = unsafe { ffi::cobre_highs_set_double_option(highs, c"time_limit".as_ptr(), 0.0) };
1828 let run_status = unsafe { ffi::cobre_highs_run(highs) };
1829 let model_status = unsafe { ffi::cobre_highs_get_model_status(highs) };
1830 let obj = unsafe { ffi::cobre_highs_get_objective_value(highs) };
1831 eprintln!(
1832 "SS1.1 + time_limit=0: run_status={run_status}, model_status={model_status}, obj={obj}"
1833 );
1834 unsafe { ffi::cobre_highs_destroy(highs) };
1835
1836 let highs = unsafe { ffi::cobre_highs_create() };
1838 assert!(!highs.is_null());
1839 unsafe { ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0) };
1840 unsafe { research_load_ss11_lp(highs) };
1841 let _ = unsafe {
1842 ffi::cobre_highs_set_int_option(highs, c"simplex_iteration_limit".as_ptr(), 0)
1843 };
1844 let run_status = unsafe { ffi::cobre_highs_run(highs) };
1845 let model_status = unsafe { ffi::cobre_highs_get_model_status(highs) };
1846 let obj = unsafe { ffi::cobre_highs_get_objective_value(highs) };
1847 eprintln!(
1848 "SS1.1 + iteration_limit=0: run_status={run_status}, model_status={model_status}, obj={obj}"
1849 );
1850 unsafe { ffi::cobre_highs_destroy(highs) };
1851 }
1852
1853 unsafe fn research_load_larger_lp(highs: *mut std::os::raw::c_void) {
1873 use crate::ffi;
1874 let col_cost: [f64; 5] = [1.0, 1.0, 1.0, 1.0, 1.0];
1875 let col_lower: [f64; 5] = [0.0; 5];
1876 let col_upper: [f64; 5] = [100.0; 5];
1877 let row_lower: [f64; 4] = [10.0, 8.0, 6.0, 4.0];
1878 let row_upper: [f64; 4] = [f64::INFINITY; 4];
1879 let a_start: [i32; 6] = [0, 1, 3, 5, 7, 8];
1881 let a_index: [i32; 8] = [0, 0, 1, 1, 2, 2, 3, 3];
1882 let a_value: [f64; 8] = [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0];
1883 let status = unsafe {
1885 ffi::cobre_highs_pass_lp(
1886 highs,
1887 5,
1888 4,
1889 8,
1890 ffi::HIGHS_MATRIX_FORMAT_COLWISE,
1891 ffi::HIGHS_OBJ_SENSE_MINIMIZE,
1892 0.0,
1893 col_cost.as_ptr(),
1894 col_lower.as_ptr(),
1895 col_upper.as_ptr(),
1896 row_lower.as_ptr(),
1897 row_upper.as_ptr(),
1898 a_start.as_ptr(),
1899 a_index.as_ptr(),
1900 a_value.as_ptr(),
1901 )
1902 };
1903 assert_eq!(
1904 status,
1905 ffi::HIGHS_STATUS_OK,
1906 "research_load_larger_lp pass_lp failed"
1907 );
1908 }
1909
1910 #[test]
1919 fn test_research_time_limit_zero_triggers_time_limit_status() {
1920 use crate::ffi;
1921
1922 let highs = unsafe { ffi::cobre_highs_create() };
1923 assert!(!highs.is_null());
1924 unsafe { ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0) };
1925 unsafe { research_load_larger_lp(highs) };
1926
1927 let opt_status =
1928 unsafe { ffi::cobre_highs_set_double_option(highs, c"time_limit".as_ptr(), 0.0) };
1929 assert_eq!(opt_status, ffi::HIGHS_STATUS_OK);
1930
1931 let run_status = unsafe { ffi::cobre_highs_run(highs) };
1932 let model_status = unsafe { ffi::cobre_highs_get_model_status(highs) };
1933
1934 eprintln!(
1935 "time_limit=0 on larger LP: run_status={run_status}, model_status={model_status}"
1936 );
1937
1938 assert_eq!(
1939 run_status,
1940 ffi::HIGHS_STATUS_WARNING,
1941 "time_limit=0 must return HIGHS_STATUS_WARNING (1), got {run_status}"
1942 );
1943 assert_eq!(
1944 model_status,
1945 ffi::HIGHS_MODEL_STATUS_TIME_LIMIT,
1946 "time_limit=0 must give MODEL_STATUS_TIME_LIMIT (13), got {model_status}"
1947 );
1948
1949 unsafe { ffi::cobre_highs_destroy(highs) };
1950 }
1951
1952 #[test]
1961 fn test_research_iteration_limit_zero_triggers_iteration_limit_status() {
1962 use crate::ffi;
1963
1964 let highs = unsafe { ffi::cobre_highs_create() };
1965 assert!(!highs.is_null());
1966 unsafe { ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0) };
1967 unsafe { ffi::cobre_highs_set_string_option(highs, c"presolve".as_ptr(), c"off".as_ptr()) };
1969 unsafe { research_load_larger_lp(highs) };
1970
1971 let opt_status = unsafe {
1972 ffi::cobre_highs_set_int_option(highs, c"simplex_iteration_limit".as_ptr(), 0)
1973 };
1974 assert_eq!(opt_status, ffi::HIGHS_STATUS_OK);
1975
1976 let run_status = unsafe { ffi::cobre_highs_run(highs) };
1977 let model_status = unsafe { ffi::cobre_highs_get_model_status(highs) };
1978
1979 eprintln!(
1980 "iteration_limit=0 on larger LP: run_status={run_status}, model_status={model_status}"
1981 );
1982
1983 assert_eq!(
1984 run_status,
1985 ffi::HIGHS_STATUS_WARNING,
1986 "iteration_limit=0 must return HIGHS_STATUS_WARNING (1), got {run_status}"
1987 );
1988 assert_eq!(
1989 model_status,
1990 ffi::HIGHS_MODEL_STATUS_ITERATION_LIMIT,
1991 "iteration_limit=0 must give MODEL_STATUS_ITERATION_LIMIT (14), got {model_status}"
1992 );
1993
1994 unsafe { ffi::cobre_highs_destroy(highs) };
1995 }
1996
1997 #[test]
2003 fn test_research_partial_solution_availability() {
2004 use crate::ffi;
2005
2006 {
2008 let highs = unsafe { ffi::cobre_highs_create() };
2009 assert!(!highs.is_null());
2010 unsafe { ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0) };
2011 unsafe { research_load_larger_lp(highs) };
2012 unsafe { ffi::cobre_highs_set_double_option(highs, c"time_limit".as_ptr(), 0.0) };
2013 unsafe { ffi::cobre_highs_run(highs) };
2014
2015 let obj = unsafe { ffi::cobre_highs_get_objective_value(highs) };
2016 let model_status = unsafe { ffi::cobre_highs_get_model_status(highs) };
2017 assert_eq!(model_status, ffi::HIGHS_MODEL_STATUS_TIME_LIMIT);
2018 eprintln!("TIME_LIMIT: obj={obj}, finite={}", obj.is_finite());
2019 unsafe { ffi::cobre_highs_destroy(highs) };
2020 }
2021
2022 {
2024 let highs = unsafe { ffi::cobre_highs_create() };
2025 assert!(!highs.is_null());
2026 unsafe { ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0) };
2027 unsafe {
2028 ffi::cobre_highs_set_string_option(highs, c"presolve".as_ptr(), c"off".as_ptr())
2029 };
2030 unsafe { research_load_larger_lp(highs) };
2031 unsafe {
2032 ffi::cobre_highs_set_int_option(highs, c"simplex_iteration_limit".as_ptr(), 0)
2033 };
2034 unsafe { ffi::cobre_highs_run(highs) };
2035
2036 let obj = unsafe { ffi::cobre_highs_get_objective_value(highs) };
2037 let model_status = unsafe { ffi::cobre_highs_get_model_status(highs) };
2038 assert_eq!(model_status, ffi::HIGHS_MODEL_STATUS_ITERATION_LIMIT);
2039 eprintln!("ITERATION_LIMIT: obj={obj}, finite={}", obj.is_finite());
2040 unsafe { ffi::cobre_highs_destroy(highs) };
2041 }
2042 }
2043
2044 #[test]
2047 fn test_research_restore_defaults_allows_subsequent_optimal_solve() {
2048 use crate::ffi;
2049
2050 let highs = unsafe { ffi::cobre_highs_create() };
2051 assert!(!highs.is_null());
2052
2053 unsafe { ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0) };
2054
2055 unsafe {
2057 ffi::cobre_highs_set_string_option(highs, c"solver".as_ptr(), c"simplex".as_ptr());
2058 ffi::cobre_highs_set_int_option(highs, c"simplex_strategy".as_ptr(), 1);
2059 ffi::cobre_highs_set_string_option(highs, c"presolve".as_ptr(), c"off".as_ptr());
2060 ffi::cobre_highs_set_string_option(highs, c"parallel".as_ptr(), c"off".as_ptr());
2061 ffi::cobre_highs_set_double_option(
2062 highs,
2063 c"primal_feasibility_tolerance".as_ptr(),
2064 1e-7,
2065 );
2066 ffi::cobre_highs_set_double_option(highs, c"dual_feasibility_tolerance".as_ptr(), 1e-7);
2067 }
2068
2069 let col_cost: [f64; 3] = [0.0, 1.0, 50.0];
2070 let col_lower: [f64; 3] = [0.0, 0.0, 0.0];
2071 let col_upper: [f64; 3] = [10.0, f64::INFINITY, 8.0];
2072 let row_lower: [f64; 2] = [6.0, 14.0];
2073 let row_upper: [f64; 2] = [6.0, 14.0];
2074 let a_start: [i32; 4] = [0, 2, 2, 3];
2075 let a_index: [i32; 3] = [0, 1, 1];
2076 let a_value: [f64; 3] = [1.0, 2.0, 1.0];
2077
2078 unsafe {
2080 ffi::cobre_highs_pass_lp(
2081 highs,
2082 3,
2083 2,
2084 3,
2085 ffi::HIGHS_MATRIX_FORMAT_COLWISE,
2086 ffi::HIGHS_OBJ_SENSE_MINIMIZE,
2087 0.0,
2088 col_cost.as_ptr(),
2089 col_lower.as_ptr(),
2090 col_upper.as_ptr(),
2091 row_lower.as_ptr(),
2092 row_upper.as_ptr(),
2093 a_start.as_ptr(),
2094 a_index.as_ptr(),
2095 a_value.as_ptr(),
2096 );
2097 ffi::cobre_highs_set_int_option(highs, c"simplex_iteration_limit".as_ptr(), 0);
2098 ffi::cobre_highs_run(highs);
2099 }
2100 let status1 = unsafe { ffi::cobre_highs_get_model_status(highs) };
2101 assert_eq!(status1, ffi::HIGHS_MODEL_STATUS_ITERATION_LIMIT);
2102
2103 unsafe {
2105 ffi::cobre_highs_set_string_option(highs, c"solver".as_ptr(), c"simplex".as_ptr());
2106 ffi::cobre_highs_set_int_option(highs, c"simplex_strategy".as_ptr(), 1);
2107 ffi::cobre_highs_set_string_option(highs, c"presolve".as_ptr(), c"off".as_ptr());
2108 ffi::cobre_highs_set_double_option(
2109 highs,
2110 c"primal_feasibility_tolerance".as_ptr(),
2111 1e-7,
2112 );
2113 ffi::cobre_highs_set_double_option(highs, c"dual_feasibility_tolerance".as_ptr(), 1e-7);
2114 ffi::cobre_highs_set_string_option(highs, c"parallel".as_ptr(), c"off".as_ptr());
2115 ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0);
2116 ffi::cobre_highs_set_int_option(highs, c"simplex_iteration_limit".as_ptr(), i32::MAX);
2118 }
2119
2120 unsafe { ffi::cobre_highs_clear_solver(highs) };
2122 unsafe { ffi::cobre_highs_run(highs) };
2123 let status2 = unsafe { ffi::cobre_highs_get_model_status(highs) };
2124 let obj = unsafe { ffi::cobre_highs_get_objective_value(highs) };
2125 assert_eq!(
2126 status2,
2127 ffi::HIGHS_MODEL_STATUS_OPTIMAL,
2128 "after restoring defaults, second solve must be OPTIMAL, got {status2}"
2129 );
2130 assert!(
2131 (obj - 100.0).abs() < 1e-8,
2132 "objective after restore must be 100.0, got {obj}"
2133 );
2134
2135 unsafe { ffi::cobre_highs_destroy(highs) };
2136 }
2137
2138 #[test]
2143 fn test_research_iteration_limit_one_triggers_iteration_limit_status() {
2144 use crate::ffi;
2145
2146 let highs = unsafe { ffi::cobre_highs_create() };
2147 assert!(!highs.is_null());
2148
2149 unsafe { ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0) };
2150
2151 let col_cost: [f64; 3] = [0.0, 1.0, 50.0];
2152 let col_lower: [f64; 3] = [0.0, 0.0, 0.0];
2153 let col_upper: [f64; 3] = [10.0, f64::INFINITY, 8.0];
2154 let row_lower: [f64; 2] = [6.0, 14.0];
2155 let row_upper: [f64; 2] = [6.0, 14.0];
2156 let a_start: [i32; 4] = [0, 2, 2, 3];
2157 let a_index: [i32; 3] = [0, 1, 1];
2158 let a_value: [f64; 3] = [1.0, 2.0, 1.0];
2159
2160 unsafe {
2161 ffi::cobre_highs_pass_lp(
2162 highs,
2163 3,
2164 2,
2165 3,
2166 ffi::HIGHS_MATRIX_FORMAT_COLWISE,
2167 ffi::HIGHS_OBJ_SENSE_MINIMIZE,
2168 0.0,
2169 col_cost.as_ptr(),
2170 col_lower.as_ptr(),
2171 col_upper.as_ptr(),
2172 row_lower.as_ptr(),
2173 row_upper.as_ptr(),
2174 a_start.as_ptr(),
2175 a_index.as_ptr(),
2176 a_value.as_ptr(),
2177 );
2178 ffi::cobre_highs_set_int_option(highs, c"simplex_iteration_limit".as_ptr(), 1);
2179 ffi::cobre_highs_run(highs);
2180 }
2181
2182 let model_status = unsafe { ffi::cobre_highs_get_model_status(highs) };
2183 eprintln!("iteration_limit=1 model_status: {model_status}");
2184 assert!(
2187 model_status == ffi::HIGHS_MODEL_STATUS_ITERATION_LIMIT
2188 || model_status == ffi::HIGHS_MODEL_STATUS_OPTIMAL,
2189 "expected ITERATION_LIMIT or OPTIMAL, got {model_status}"
2190 );
2191
2192 unsafe { ffi::cobre_highs_destroy(highs) };
2193 }
2194}