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
786impl SolverInterface for HighsSolver {
787 fn name(&self) -> &'static str {
788 "HiGHS"
789 }
790
791 fn load_model(&mut self, template: &StageTemplate) {
792 let t0 = Instant::now();
793 assert!(
803 i32::try_from(template.num_cols).is_ok(),
804 "num_cols {} overflows i32: LP exceeds HiGHS API limit",
805 template.num_cols
806 );
807 assert!(
808 i32::try_from(template.num_rows).is_ok(),
809 "num_rows {} overflows i32: LP exceeds HiGHS API limit",
810 template.num_rows
811 );
812 assert!(
813 i32::try_from(template.num_nz).is_ok(),
814 "num_nz {} overflows i32: LP exceeds HiGHS API limit",
815 template.num_nz
816 );
817 #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
819 let num_col = template.num_cols as i32;
820 #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
821 let num_row = template.num_rows as i32;
822 #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
823 let num_nz = template.num_nz as i32;
824 let status = unsafe {
825 ffi::cobre_highs_pass_lp(
826 self.handle,
827 num_col,
828 num_row,
829 num_nz,
830 ffi::HIGHS_MATRIX_FORMAT_COLWISE,
831 ffi::HIGHS_OBJ_SENSE_MINIMIZE,
832 0.0, template.objective.as_ptr(),
834 template.col_lower.as_ptr(),
835 template.col_upper.as_ptr(),
836 template.row_lower.as_ptr(),
837 template.row_upper.as_ptr(),
838 template.col_starts.as_ptr(),
839 template.row_indices.as_ptr(),
840 template.values.as_ptr(),
841 )
842 };
843
844 assert_ne!(
845 status,
846 ffi::HIGHS_STATUS_ERROR,
847 "cobre_highs_pass_lp failed with status {status}"
848 );
849
850 self.num_cols = template.num_cols;
851 self.num_rows = template.num_rows;
852 self.has_model = true;
853
854 self.col_value.resize(self.num_cols, 0.0);
857 self.col_dual.resize(self.num_cols, 0.0);
858 self.row_value.resize(self.num_rows, 0.0);
859 self.row_dual.resize(self.num_rows, 0.0);
860
861 self.basis_col_i32.resize(self.num_cols, 0);
864 self.basis_row_i32.resize(self.num_rows, 0);
865 self.stats.total_load_model_time_seconds += t0.elapsed().as_secs_f64();
866 self.stats.load_model_count += 1;
867 }
868
869 fn add_rows(&mut self, cuts: &RowBatch) {
870 let t0 = Instant::now();
871 assert!(
872 i32::try_from(cuts.num_rows).is_ok(),
873 "cuts.num_rows {} overflows i32: RowBatch exceeds HiGHS API limit",
874 cuts.num_rows
875 );
876 assert!(
877 i32::try_from(cuts.col_indices.len()).is_ok(),
878 "cuts nnz {} overflows i32: RowBatch exceeds HiGHS API limit",
879 cuts.col_indices.len()
880 );
881 #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
883 let num_new_row = cuts.num_rows as i32;
884 #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
885 let num_new_nz = cuts.col_indices.len() as i32;
886
887 let status = unsafe {
895 ffi::cobre_highs_add_rows(
896 self.handle,
897 num_new_row,
898 cuts.row_lower.as_ptr(),
899 cuts.row_upper.as_ptr(),
900 num_new_nz,
901 cuts.row_starts.as_ptr(),
902 cuts.col_indices.as_ptr(),
903 cuts.values.as_ptr(),
904 )
905 };
906
907 assert_ne!(
908 status,
909 ffi::HIGHS_STATUS_ERROR,
910 "cobre_highs_add_rows failed with status {status}"
911 );
912
913 self.num_rows += cuts.num_rows;
914
915 self.row_value.resize(self.num_rows, 0.0);
917 self.row_dual.resize(self.num_rows, 0.0);
918
919 self.basis_row_i32.resize(self.num_rows, 0);
921 self.stats.total_add_rows_time_seconds += t0.elapsed().as_secs_f64();
922 self.stats.add_rows_count += 1;
923 }
924
925 fn set_row_bounds(&mut self, indices: &[usize], lower: &[f64], upper: &[f64]) {
926 assert!(
927 indices.len() == lower.len() && indices.len() == upper.len(),
928 "set_row_bounds: indices ({}), lower ({}), and upper ({}) must have equal length",
929 indices.len(),
930 lower.len(),
931 upper.len()
932 );
933 if indices.is_empty() {
934 return;
935 }
936
937 assert!(
938 i32::try_from(indices.len()).is_ok(),
939 "set_row_bounds: indices.len() {} overflows i32",
940 indices.len()
941 );
942 #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
943 let num_entries = indices.len() as i32;
944
945 let t0 = Instant::now();
946 let status = unsafe {
953 ffi::cobre_highs_change_rows_bounds_by_set(
954 self.handle,
955 num_entries,
956 self.convert_to_i32_scratch(indices).as_ptr(),
957 lower.as_ptr(),
958 upper.as_ptr(),
959 )
960 };
961
962 assert_ne!(
963 status,
964 ffi::HIGHS_STATUS_ERROR,
965 "cobre_highs_change_rows_bounds_by_set failed with status {status}"
966 );
967 self.stats.total_set_bounds_time_seconds += t0.elapsed().as_secs_f64();
968 }
969
970 fn set_col_bounds(&mut self, indices: &[usize], lower: &[f64], upper: &[f64]) {
971 assert!(
972 indices.len() == lower.len() && indices.len() == upper.len(),
973 "set_col_bounds: indices ({}), lower ({}), and upper ({}) must have equal length",
974 indices.len(),
975 lower.len(),
976 upper.len()
977 );
978 if indices.is_empty() {
979 return;
980 }
981
982 assert!(
983 i32::try_from(indices.len()).is_ok(),
984 "set_col_bounds: indices.len() {} overflows i32",
985 indices.len()
986 );
987 #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
988 let num_entries = indices.len() as i32;
989
990 let t0 = Instant::now();
991 let status = unsafe {
997 ffi::cobre_highs_change_cols_bounds_by_set(
998 self.handle,
999 num_entries,
1000 self.convert_to_i32_scratch(indices).as_ptr(),
1001 lower.as_ptr(),
1002 upper.as_ptr(),
1003 )
1004 };
1005
1006 assert_ne!(
1007 status,
1008 ffi::HIGHS_STATUS_ERROR,
1009 "cobre_highs_change_cols_bounds_by_set failed with status {status}"
1010 );
1011 self.stats.total_set_bounds_time_seconds += t0.elapsed().as_secs_f64();
1012 }
1013
1014 fn solve(&mut self) -> Result<SolutionView<'_>, SolverError> {
1015 assert!(
1016 self.has_model,
1017 "solve called without a loaded model — call load_model first"
1018 );
1019
1020 self.set_iteration_limits();
1026
1027 let t0 = Instant::now();
1028 let model_status = self.run_once();
1029 let solve_time = t0.elapsed().as_secs_f64();
1030
1031 self.stats.solve_count += 1;
1032
1033 if model_status == ffi::HIGHS_MODEL_STATUS_OPTIMAL {
1034 #[allow(clippy::cast_sign_loss)]
1039 let iterations =
1040 unsafe { ffi::cobre_highs_get_simplex_iteration_count(self.handle) } as u64;
1041 self.stats.success_count += 1;
1042 self.stats.first_try_successes += 1;
1043 self.stats.total_iterations += iterations;
1044 self.stats.total_solve_time_seconds += solve_time;
1045 self.restore_iteration_limits();
1046 return Ok(self.extract_solution_view(solve_time));
1047 }
1048
1049 let is_unbounded = model_status == ffi::HIGHS_MODEL_STATUS_UNBOUNDED;
1060 let initial_retryable = is_unbounded
1061 || model_status == ffi::HIGHS_MODEL_STATUS_ITERATION_LIMIT
1062 || model_status == ffi::HIGHS_MODEL_STATUS_TIME_LIMIT
1063 || solve_time > 15.0;
1064 if !initial_retryable {
1065 if let Some(terminal_err) = self.interpret_terminal_status(model_status, solve_time) {
1066 self.restore_iteration_limits();
1067 self.stats.failure_count += 1;
1068 return Err(terminal_err);
1069 }
1070 }
1071
1072 match self.retry_escalation(is_unbounded) {
1074 Ok(outcome) => {
1075 self.stats.retry_count += outcome.attempts;
1076 self.stats.success_count += 1;
1077 self.stats.total_iterations += outcome.iterations;
1078 self.stats.total_solve_time_seconds += outcome.solve_time;
1079 self.stats.retry_level_histogram[outcome.level as usize] += 1;
1080 Ok(self.extract_solution_view(outcome.solve_time))
1081 }
1082 Err((attempts, err)) => {
1083 self.stats.retry_count += attempts;
1084 self.stats.failure_count += 1;
1085 Err(err)
1086 }
1087 }
1088 }
1089
1090 fn reset(&mut self) {
1091 let status = unsafe { ffi::cobre_highs_clear_solver(self.handle) };
1096 debug_assert_ne!(
1097 status,
1098 ffi::HIGHS_STATUS_ERROR,
1099 "cobre_highs_clear_solver failed — HiGHS internal state may be inconsistent"
1100 );
1101 self.num_cols = 0;
1103 self.num_rows = 0;
1104 self.has_model = false;
1105 }
1108
1109 fn get_basis(&mut self, out: &mut crate::types::Basis) {
1110 assert!(
1111 self.has_model,
1112 "get_basis called without a loaded model — call load_model first"
1113 );
1114
1115 out.col_status.resize(self.num_cols, 0);
1116 out.row_status.resize(self.num_rows, 0);
1117
1118 let get_status = unsafe {
1124 ffi::cobre_highs_get_basis(
1125 self.handle,
1126 out.col_status.as_mut_ptr(),
1127 out.row_status.as_mut_ptr(),
1128 )
1129 };
1130
1131 assert_ne!(
1132 get_status,
1133 ffi::HIGHS_STATUS_ERROR,
1134 "cobre_highs_get_basis failed: basis must exist after a successful solve (programming error)"
1135 );
1136 }
1137
1138 fn solve_with_basis(
1139 &mut self,
1140 basis: &crate::types::Basis,
1141 ) -> Result<crate::types::SolutionView<'_>, SolverError> {
1142 assert!(
1143 self.has_model,
1144 "solve_with_basis called without a loaded model — call load_model first"
1145 );
1146 assert!(
1147 basis.col_status.len() == self.num_cols,
1148 "basis column count {} does not match LP column count {}",
1149 basis.col_status.len(),
1150 self.num_cols
1151 );
1152
1153 self.stats.basis_offered += 1;
1155
1156 self.basis_col_i32[..self.num_cols].copy_from_slice(&basis.col_status);
1159
1160 let basis_rows = basis.row_status.len();
1164 let lp_rows = self.num_rows;
1165 let copy_len = basis_rows.min(lp_rows);
1166 self.basis_row_i32[..copy_len].copy_from_slice(&basis.row_status[..copy_len]);
1167 if lp_rows > basis_rows {
1168 self.basis_row_i32[basis_rows..lp_rows].fill(ffi::HIGHS_BASIS_STATUS_BASIC);
1169 }
1170
1171 let basis_set_start = Instant::now();
1178 let set_status = unsafe {
1179 ffi::cobre_highs_set_basis(
1180 self.handle,
1181 self.basis_col_i32.as_ptr(),
1182 self.basis_row_i32.as_ptr(),
1183 )
1184 };
1185 self.stats.total_basis_set_time_seconds += basis_set_start.elapsed().as_secs_f64();
1186
1187 if set_status == ffi::HIGHS_STATUS_ERROR {
1189 self.stats.basis_rejections += 1;
1190 debug_assert!(false, "raw basis rejected; falling back to cold-start");
1191 }
1192
1193 self.solve()
1195 }
1196
1197 fn statistics(&self) -> SolverStatistics {
1198 self.stats.clone()
1199 }
1200}
1201
1202#[cfg(feature = "test-support")]
1208impl HighsSolver {
1209 #[must_use]
1217 pub fn raw_handle(&self) -> *mut std::os::raw::c_void {
1218 self.handle
1219 }
1220}
1221
1222#[cfg(test)]
1223mod tests {
1224 use super::HighsSolver;
1225 use crate::{
1226 SolverInterface,
1227 types::{Basis, RowBatch, StageTemplate},
1228 };
1229
1230 fn make_fixture_stage_template() -> StageTemplate {
1243 StageTemplate {
1244 num_cols: 3,
1245 num_rows: 2,
1246 num_nz: 3,
1247 col_starts: vec![0_i32, 2, 2, 3],
1248 row_indices: vec![0_i32, 1, 1],
1249 values: vec![1.0, 2.0, 1.0],
1250 col_lower: vec![0.0, 0.0, 0.0],
1251 col_upper: vec![10.0, f64::INFINITY, 8.0],
1252 objective: vec![0.0, 1.0, 50.0],
1253 row_lower: vec![6.0, 14.0],
1254 row_upper: vec![6.0, 14.0],
1255 n_state: 1,
1256 n_transfer: 0,
1257 n_dual_relevant: 1,
1258 n_hydro: 1,
1259 max_par_order: 0,
1260 col_scale: Vec::new(),
1261 row_scale: Vec::new(),
1262 }
1263 }
1264
1265 fn make_fixture_row_batch() -> RowBatch {
1269 RowBatch {
1270 num_rows: 2,
1271 row_starts: vec![0_i32, 2, 4],
1272 col_indices: vec![0_i32, 1, 0, 1],
1273 values: vec![-5.0, 1.0, 3.0, 1.0],
1274 row_lower: vec![20.0, 80.0],
1275 row_upper: vec![f64::INFINITY, f64::INFINITY],
1276 }
1277 }
1278
1279 #[test]
1280 fn test_highs_solver_create_and_name() {
1281 let solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1282 assert_eq!(solver.name(), "HiGHS");
1283 }
1285
1286 #[test]
1287 fn test_highs_solver_send_bound() {
1288 fn assert_send<T: Send>() {}
1289 assert_send::<HighsSolver>();
1290 }
1291
1292 #[test]
1293 fn test_highs_solver_statistics_initial() {
1294 let solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1295 let stats = solver.statistics();
1296 assert_eq!(stats.solve_count, 0);
1297 assert_eq!(stats.success_count, 0);
1298 assert_eq!(stats.failure_count, 0);
1299 assert_eq!(stats.total_iterations, 0);
1300 assert_eq!(stats.retry_count, 0);
1301 assert_eq!(stats.total_solve_time_seconds, 0.0);
1302 }
1303
1304 #[test]
1305 fn test_highs_load_model_updates_dimensions() {
1306 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1307 let template = make_fixture_stage_template();
1308
1309 solver.load_model(&template);
1310
1311 assert_eq!(solver.num_cols, 3, "num_cols must be 3 after load_model");
1312 assert_eq!(solver.num_rows, 2, "num_rows must be 2 after load_model");
1313 assert_eq!(
1314 solver.col_value.len(),
1315 3,
1316 "col_value buffer must be resized to num_cols"
1317 );
1318 assert_eq!(
1319 solver.col_dual.len(),
1320 3,
1321 "col_dual buffer must be resized to num_cols"
1322 );
1323 assert_eq!(
1324 solver.row_value.len(),
1325 2,
1326 "row_value buffer must be resized to num_rows"
1327 );
1328 assert_eq!(
1329 solver.row_dual.len(),
1330 2,
1331 "row_dual buffer must be resized to num_rows"
1332 );
1333 }
1334
1335 #[test]
1336 fn test_highs_add_rows_updates_dimensions() {
1337 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1338 let template = make_fixture_stage_template();
1339 let cuts = make_fixture_row_batch();
1340
1341 solver.load_model(&template);
1342 solver.add_rows(&cuts);
1343
1344 assert_eq!(solver.num_rows, 4, "num_rows must be 4 after add_rows");
1346 assert_eq!(
1347 solver.row_dual.len(),
1348 4,
1349 "row_dual buffer must be resized to 4 after add_rows"
1350 );
1351 assert_eq!(
1352 solver.row_value.len(),
1353 4,
1354 "row_value buffer must be resized to 4 after add_rows"
1355 );
1356 assert_eq!(solver.num_cols, 3, "num_cols must be unchanged by add_rows");
1358 }
1359
1360 #[test]
1361 fn test_highs_set_row_bounds_no_panic() {
1362 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1363 let template = make_fixture_stage_template();
1364 solver.load_model(&template);
1365
1366 solver.set_row_bounds(&[0], &[4.0], &[4.0]);
1368 }
1369
1370 #[test]
1371 fn test_highs_set_col_bounds_no_panic() {
1372 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1373 let template = make_fixture_stage_template();
1374 solver.load_model(&template);
1375
1376 solver.set_col_bounds(&[1], &[10.0], &[f64::INFINITY]);
1378 }
1379
1380 #[test]
1381 fn test_highs_set_bounds_empty_no_panic() {
1382 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1383 let template = make_fixture_stage_template();
1384 solver.load_model(&template);
1385
1386 solver.set_row_bounds(&[], &[], &[]);
1388 solver.set_col_bounds(&[], &[], &[]);
1389 }
1390
1391 #[test]
1394 fn test_highs_solve_basic_lp() {
1395 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1396 let template = make_fixture_stage_template();
1397 solver.load_model(&template);
1398
1399 let solution = solver
1400 .solve()
1401 .expect("solve() must succeed on a feasible LP");
1402
1403 assert!(
1404 (solution.objective - 100.0).abs() < 1e-8,
1405 "objective must be 100.0, got {}",
1406 solution.objective
1407 );
1408 assert_eq!(solution.primal.len(), 3, "primal must have 3 elements");
1409 assert!(
1410 (solution.primal[0] - 6.0).abs() < 1e-8,
1411 "primal[0] (x0) must be 6.0, got {}",
1412 solution.primal[0]
1413 );
1414 assert!(
1415 (solution.primal[1] - 0.0).abs() < 1e-8,
1416 "primal[1] (x1) must be 0.0, got {}",
1417 solution.primal[1]
1418 );
1419 assert!(
1420 (solution.primal[2] - 2.0).abs() < 1e-8,
1421 "primal[2] (x2) must be 2.0, got {}",
1422 solution.primal[2]
1423 );
1424 }
1425
1426 #[test]
1430 fn test_highs_solve_with_cuts() {
1431 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1432 let template = make_fixture_stage_template();
1433 let cuts = make_fixture_row_batch();
1434 solver.load_model(&template);
1435 solver.add_rows(&cuts);
1436
1437 let solution = solver
1438 .solve()
1439 .expect("solve() must succeed on a feasible LP with cuts");
1440
1441 assert!(
1442 (solution.objective - 162.0).abs() < 1e-8,
1443 "objective must be 162.0, got {}",
1444 solution.objective
1445 );
1446 assert!(
1447 (solution.primal[0] - 6.0).abs() < 1e-8,
1448 "primal[0] must be 6.0, got {}",
1449 solution.primal[0]
1450 );
1451 assert!(
1452 (solution.primal[1] - 62.0).abs() < 1e-8,
1453 "primal[1] must be 62.0, got {}",
1454 solution.primal[1]
1455 );
1456 assert!(
1457 (solution.primal[2] - 2.0).abs() < 1e-8,
1458 "primal[2] must be 2.0, got {}",
1459 solution.primal[2]
1460 );
1461 }
1462
1463 #[test]
1466 fn test_highs_solve_after_rhs_patch() {
1467 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1468 let template = make_fixture_stage_template();
1469 let cuts = make_fixture_row_batch();
1470 solver.load_model(&template);
1471 solver.add_rows(&cuts);
1472
1473 solver.set_row_bounds(&[0], &[4.0], &[4.0]);
1475
1476 let solution = solver
1477 .solve()
1478 .expect("solve() must succeed after RHS patch");
1479
1480 assert!(
1481 (solution.objective - 368.0).abs() < 1e-8,
1482 "objective must be 368.0, got {}",
1483 solution.objective
1484 );
1485 }
1486
1487 #[test]
1489 fn test_highs_solve_statistics_increment() {
1490 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1491 let template = make_fixture_stage_template();
1492 solver.load_model(&template);
1493
1494 solver.solve().expect("first solve must succeed");
1495 solver.solve().expect("second solve must succeed");
1496
1497 let stats = solver.statistics();
1498 assert_eq!(stats.solve_count, 2, "solve_count must be 2");
1499 assert_eq!(stats.success_count, 2, "success_count must be 2");
1500 assert_eq!(stats.failure_count, 0, "failure_count must be 0");
1501 assert!(
1502 stats.total_iterations > 0,
1503 "total_iterations must be positive"
1504 );
1505 }
1506
1507 #[test]
1509 fn test_highs_reset_preserves_stats() {
1510 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1511 let template = make_fixture_stage_template();
1512 solver.load_model(&template);
1513 solver.solve().expect("solve must succeed");
1514
1515 let stats_before = solver.statistics();
1516 assert_eq!(
1517 stats_before.solve_count, 1,
1518 "solve_count must be 1 before reset"
1519 );
1520
1521 solver.reset();
1522
1523 let stats_after = solver.statistics();
1524 assert_eq!(
1525 stats_after.solve_count, stats_before.solve_count,
1526 "solve_count must be unchanged after reset"
1527 );
1528 assert_eq!(
1529 stats_after.success_count, stats_before.success_count,
1530 "success_count must be unchanged after reset"
1531 );
1532 assert_eq!(
1533 stats_after.total_iterations, stats_before.total_iterations,
1534 "total_iterations must be unchanged after reset"
1535 );
1536 }
1537
1538 #[test]
1540 fn test_highs_solve_iterations_positive() {
1541 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1542 let template = make_fixture_stage_template();
1543 solver.load_model(&template);
1544
1545 let solution = solver.solve().expect("solve must succeed");
1546 assert!(
1547 solution.iterations > 0,
1548 "iterations must be positive, got {}",
1549 solution.iterations
1550 );
1551 }
1552
1553 #[test]
1555 fn test_highs_solve_time_positive() {
1556 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1557 let template = make_fixture_stage_template();
1558 solver.load_model(&template);
1559
1560 let solution = solver.solve().expect("solve must succeed");
1561 assert!(
1562 solution.solve_time_seconds > 0.0,
1563 "solve_time_seconds must be positive, got {}",
1564 solution.solve_time_seconds
1565 );
1566 }
1567
1568 #[test]
1571 fn test_highs_solve_statistics_single() {
1572 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1573 let template = make_fixture_stage_template();
1574 solver.load_model(&template);
1575
1576 solver.solve().expect("solve must succeed");
1577
1578 let stats = solver.statistics();
1579 assert_eq!(stats.solve_count, 1, "solve_count must be 1");
1580 assert_eq!(stats.success_count, 1, "success_count must be 1");
1581 assert_eq!(stats.failure_count, 0, "failure_count must be 0");
1582 assert!(
1583 stats.total_iterations > 0,
1584 "total_iterations must be positive after a successful solve"
1585 );
1586 }
1587
1588 #[test]
1591 fn test_get_basis_valid_status_codes() {
1592 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1593 let template = make_fixture_stage_template();
1594 solver.load_model(&template);
1595 solver.solve().expect("solve must succeed before get_basis");
1596
1597 let mut basis = Basis::new(0, 0);
1598 solver.get_basis(&mut basis);
1599
1600 for &code in &basis.col_status {
1601 assert!(
1602 (0..=4).contains(&code),
1603 "col_status code {code} is outside valid HiGHS range 0..=4"
1604 );
1605 }
1606 for &code in &basis.row_status {
1607 assert!(
1608 (0..=4).contains(&code),
1609 "row_status code {code} is outside valid HiGHS range 0..=4"
1610 );
1611 }
1612 }
1613
1614 #[test]
1617 fn test_get_basis_resizes_output() {
1618 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1619 let template = make_fixture_stage_template();
1620 solver.load_model(&template);
1621 solver.solve().expect("solve must succeed before get_basis");
1622
1623 let mut basis = Basis::new(0, 0);
1624 assert_eq!(
1625 basis.col_status.len(),
1626 0,
1627 "initial col_status must be empty"
1628 );
1629 assert_eq!(
1630 basis.row_status.len(),
1631 0,
1632 "initial row_status must be empty"
1633 );
1634
1635 solver.get_basis(&mut basis);
1636
1637 assert_eq!(
1638 basis.col_status.len(),
1639 3,
1640 "col_status must be resized to 3 (num_cols of SS1.1)"
1641 );
1642 assert_eq!(
1643 basis.row_status.len(),
1644 2,
1645 "row_status must be resized to 2 (num_rows of SS1.1)"
1646 );
1647 }
1648
1649 #[test]
1652 fn test_solve_with_basis_warm_start() {
1653 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1654 let template = make_fixture_stage_template();
1655 solver.load_model(&template);
1656 solver.solve().expect("cold-start solve must succeed");
1657
1658 let mut basis = Basis::new(0, 0);
1659 solver.get_basis(&mut basis);
1660
1661 solver.load_model(&template);
1663 let result = solver
1664 .solve_with_basis(&basis)
1665 .expect("warm-start solve must succeed");
1666
1667 assert!(
1668 (result.objective - 100.0).abs() < 1e-8,
1669 "warm-start objective must be 100.0, got {}",
1670 result.objective
1671 );
1672 assert!(
1673 result.iterations <= 1,
1674 "warm-start from exact basis must use at most 1 iteration, got {}",
1675 result.iterations
1676 );
1677
1678 let stats = solver.statistics();
1679 assert_eq!(
1680 stats.basis_rejections, 0,
1681 "basis_rejections must be 0 when raw basis is accepted, got {}",
1682 stats.basis_rejections
1683 );
1684 }
1685
1686 #[test]
1690 fn test_solve_with_basis_dimension_mismatch() {
1691 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1692 let template = make_fixture_stage_template();
1693 let cuts = make_fixture_row_batch();
1694
1695 solver.load_model(&template);
1697 solver.solve().expect("SS1.1 solve must succeed");
1698 let mut basis = Basis::new(0, 0);
1699 solver.get_basis(&mut basis);
1700 assert_eq!(
1701 basis.row_status.len(),
1702 2,
1703 "captured basis must have 2 row statuses"
1704 );
1705
1706 solver.load_model(&template);
1708 solver.add_rows(&cuts);
1709 assert_eq!(solver.num_rows, 4, "LP must have 4 rows after add_rows");
1710
1711 let result = solver
1713 .solve_with_basis(&basis)
1714 .expect("solve with dimension-mismatched basis must succeed");
1715
1716 assert!(
1717 (result.objective - 162.0).abs() < 1e-8,
1718 "objective with both cuts active must be 162.0, got {}",
1719 result.objective
1720 );
1721 }
1722}
1723
1724#[cfg(test)]
1736#[allow(clippy::doc_markdown)]
1737mod research_tests_ticket_023 {
1738 unsafe fn research_load_ss11_lp(highs: *mut std::os::raw::c_void) {
1749 use crate::ffi;
1750 let col_cost: [f64; 3] = [0.0, 1.0, 50.0];
1751 let col_lower: [f64; 3] = [0.0, 0.0, 0.0];
1752 let col_upper: [f64; 3] = [10.0, f64::INFINITY, 8.0];
1753 let row_lower: [f64; 2] = [6.0, 14.0];
1754 let row_upper: [f64; 2] = [6.0, 14.0];
1755 let a_start: [i32; 4] = [0, 2, 2, 3];
1756 let a_index: [i32; 3] = [0, 1, 1];
1757 let a_value: [f64; 3] = [1.0, 2.0, 1.0];
1758 let status = unsafe {
1760 ffi::cobre_highs_pass_lp(
1761 highs,
1762 3,
1763 2,
1764 3,
1765 ffi::HIGHS_MATRIX_FORMAT_COLWISE,
1766 ffi::HIGHS_OBJ_SENSE_MINIMIZE,
1767 0.0,
1768 col_cost.as_ptr(),
1769 col_lower.as_ptr(),
1770 col_upper.as_ptr(),
1771 row_lower.as_ptr(),
1772 row_upper.as_ptr(),
1773 a_start.as_ptr(),
1774 a_index.as_ptr(),
1775 a_value.as_ptr(),
1776 )
1777 };
1778 assert_eq!(
1779 status,
1780 ffi::HIGHS_STATUS_OK,
1781 "research_load_ss11_lp pass_lp failed"
1782 );
1783 }
1784
1785 #[test]
1791 fn test_research_probe_limit_status_on_ss11_lp() {
1792 use crate::ffi;
1793
1794 let highs = unsafe { ffi::cobre_highs_create() };
1796 assert!(!highs.is_null());
1797 unsafe { ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0) };
1798 unsafe { research_load_ss11_lp(highs) };
1799 let _ = unsafe { ffi::cobre_highs_set_double_option(highs, c"time_limit".as_ptr(), 0.0) };
1800 let run_status = unsafe { ffi::cobre_highs_run(highs) };
1801 let model_status = unsafe { ffi::cobre_highs_get_model_status(highs) };
1802 let obj = unsafe { ffi::cobre_highs_get_objective_value(highs) };
1803 eprintln!(
1804 "SS1.1 + time_limit=0: run_status={run_status}, model_status={model_status}, obj={obj}"
1805 );
1806 unsafe { ffi::cobre_highs_destroy(highs) };
1807
1808 let highs = unsafe { ffi::cobre_highs_create() };
1810 assert!(!highs.is_null());
1811 unsafe { ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0) };
1812 unsafe { research_load_ss11_lp(highs) };
1813 let _ = unsafe {
1814 ffi::cobre_highs_set_int_option(highs, c"simplex_iteration_limit".as_ptr(), 0)
1815 };
1816 let run_status = unsafe { ffi::cobre_highs_run(highs) };
1817 let model_status = unsafe { ffi::cobre_highs_get_model_status(highs) };
1818 let obj = unsafe { ffi::cobre_highs_get_objective_value(highs) };
1819 eprintln!(
1820 "SS1.1 + iteration_limit=0: run_status={run_status}, model_status={model_status}, obj={obj}"
1821 );
1822 unsafe { ffi::cobre_highs_destroy(highs) };
1823 }
1824
1825 unsafe fn research_load_larger_lp(highs: *mut std::os::raw::c_void) {
1845 use crate::ffi;
1846 let col_cost: [f64; 5] = [1.0, 1.0, 1.0, 1.0, 1.0];
1847 let col_lower: [f64; 5] = [0.0; 5];
1848 let col_upper: [f64; 5] = [100.0; 5];
1849 let row_lower: [f64; 4] = [10.0, 8.0, 6.0, 4.0];
1850 let row_upper: [f64; 4] = [f64::INFINITY; 4];
1851 let a_start: [i32; 6] = [0, 1, 3, 5, 7, 8];
1853 let a_index: [i32; 8] = [0, 0, 1, 1, 2, 2, 3, 3];
1854 let a_value: [f64; 8] = [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0];
1855 let status = unsafe {
1857 ffi::cobre_highs_pass_lp(
1858 highs,
1859 5,
1860 4,
1861 8,
1862 ffi::HIGHS_MATRIX_FORMAT_COLWISE,
1863 ffi::HIGHS_OBJ_SENSE_MINIMIZE,
1864 0.0,
1865 col_cost.as_ptr(),
1866 col_lower.as_ptr(),
1867 col_upper.as_ptr(),
1868 row_lower.as_ptr(),
1869 row_upper.as_ptr(),
1870 a_start.as_ptr(),
1871 a_index.as_ptr(),
1872 a_value.as_ptr(),
1873 )
1874 };
1875 assert_eq!(
1876 status,
1877 ffi::HIGHS_STATUS_OK,
1878 "research_load_larger_lp pass_lp failed"
1879 );
1880 }
1881
1882 #[test]
1891 fn test_research_time_limit_zero_triggers_time_limit_status() {
1892 use crate::ffi;
1893
1894 let highs = unsafe { ffi::cobre_highs_create() };
1895 assert!(!highs.is_null());
1896 unsafe { ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0) };
1897 unsafe { research_load_larger_lp(highs) };
1898
1899 let opt_status =
1900 unsafe { ffi::cobre_highs_set_double_option(highs, c"time_limit".as_ptr(), 0.0) };
1901 assert_eq!(opt_status, ffi::HIGHS_STATUS_OK);
1902
1903 let run_status = unsafe { ffi::cobre_highs_run(highs) };
1904 let model_status = unsafe { ffi::cobre_highs_get_model_status(highs) };
1905
1906 eprintln!(
1907 "time_limit=0 on larger LP: run_status={run_status}, model_status={model_status}"
1908 );
1909
1910 assert_eq!(
1911 run_status,
1912 ffi::HIGHS_STATUS_WARNING,
1913 "time_limit=0 must return HIGHS_STATUS_WARNING (1), got {run_status}"
1914 );
1915 assert_eq!(
1916 model_status,
1917 ffi::HIGHS_MODEL_STATUS_TIME_LIMIT,
1918 "time_limit=0 must give MODEL_STATUS_TIME_LIMIT (13), got {model_status}"
1919 );
1920
1921 unsafe { ffi::cobre_highs_destroy(highs) };
1922 }
1923
1924 #[test]
1933 fn test_research_iteration_limit_zero_triggers_iteration_limit_status() {
1934 use crate::ffi;
1935
1936 let highs = unsafe { ffi::cobre_highs_create() };
1937 assert!(!highs.is_null());
1938 unsafe { ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0) };
1939 unsafe { ffi::cobre_highs_set_string_option(highs, c"presolve".as_ptr(), c"off".as_ptr()) };
1941 unsafe { research_load_larger_lp(highs) };
1942
1943 let opt_status = unsafe {
1944 ffi::cobre_highs_set_int_option(highs, c"simplex_iteration_limit".as_ptr(), 0)
1945 };
1946 assert_eq!(opt_status, ffi::HIGHS_STATUS_OK);
1947
1948 let run_status = unsafe { ffi::cobre_highs_run(highs) };
1949 let model_status = unsafe { ffi::cobre_highs_get_model_status(highs) };
1950
1951 eprintln!(
1952 "iteration_limit=0 on larger LP: run_status={run_status}, model_status={model_status}"
1953 );
1954
1955 assert_eq!(
1956 run_status,
1957 ffi::HIGHS_STATUS_WARNING,
1958 "iteration_limit=0 must return HIGHS_STATUS_WARNING (1), got {run_status}"
1959 );
1960 assert_eq!(
1961 model_status,
1962 ffi::HIGHS_MODEL_STATUS_ITERATION_LIMIT,
1963 "iteration_limit=0 must give MODEL_STATUS_ITERATION_LIMIT (14), got {model_status}"
1964 );
1965
1966 unsafe { ffi::cobre_highs_destroy(highs) };
1967 }
1968
1969 #[test]
1975 fn test_research_partial_solution_availability() {
1976 use crate::ffi;
1977
1978 {
1980 let highs = unsafe { ffi::cobre_highs_create() };
1981 assert!(!highs.is_null());
1982 unsafe { ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0) };
1983 unsafe { research_load_larger_lp(highs) };
1984 unsafe { ffi::cobre_highs_set_double_option(highs, c"time_limit".as_ptr(), 0.0) };
1985 unsafe { ffi::cobre_highs_run(highs) };
1986
1987 let obj = unsafe { ffi::cobre_highs_get_objective_value(highs) };
1988 let model_status = unsafe { ffi::cobre_highs_get_model_status(highs) };
1989 assert_eq!(model_status, ffi::HIGHS_MODEL_STATUS_TIME_LIMIT);
1990 eprintln!("TIME_LIMIT: obj={obj}, finite={}", obj.is_finite());
1991 unsafe { ffi::cobre_highs_destroy(highs) };
1992 }
1993
1994 {
1996 let highs = unsafe { ffi::cobre_highs_create() };
1997 assert!(!highs.is_null());
1998 unsafe { ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0) };
1999 unsafe {
2000 ffi::cobre_highs_set_string_option(highs, c"presolve".as_ptr(), c"off".as_ptr())
2001 };
2002 unsafe { research_load_larger_lp(highs) };
2003 unsafe {
2004 ffi::cobre_highs_set_int_option(highs, c"simplex_iteration_limit".as_ptr(), 0)
2005 };
2006 unsafe { ffi::cobre_highs_run(highs) };
2007
2008 let obj = unsafe { ffi::cobre_highs_get_objective_value(highs) };
2009 let model_status = unsafe { ffi::cobre_highs_get_model_status(highs) };
2010 assert_eq!(model_status, ffi::HIGHS_MODEL_STATUS_ITERATION_LIMIT);
2011 eprintln!("ITERATION_LIMIT: obj={obj}, finite={}", obj.is_finite());
2012 unsafe { ffi::cobre_highs_destroy(highs) };
2013 }
2014 }
2015
2016 #[test]
2019 fn test_research_restore_defaults_allows_subsequent_optimal_solve() {
2020 use crate::ffi;
2021
2022 let highs = unsafe { ffi::cobre_highs_create() };
2023 assert!(!highs.is_null());
2024
2025 unsafe { ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0) };
2026
2027 unsafe {
2029 ffi::cobre_highs_set_string_option(highs, c"solver".as_ptr(), c"simplex".as_ptr());
2030 ffi::cobre_highs_set_int_option(highs, c"simplex_strategy".as_ptr(), 1);
2031 ffi::cobre_highs_set_string_option(highs, c"presolve".as_ptr(), c"off".as_ptr());
2032 ffi::cobre_highs_set_string_option(highs, c"parallel".as_ptr(), c"off".as_ptr());
2033 ffi::cobre_highs_set_double_option(
2034 highs,
2035 c"primal_feasibility_tolerance".as_ptr(),
2036 1e-7,
2037 );
2038 ffi::cobre_highs_set_double_option(highs, c"dual_feasibility_tolerance".as_ptr(), 1e-7);
2039 }
2040
2041 let col_cost: [f64; 3] = [0.0, 1.0, 50.0];
2042 let col_lower: [f64; 3] = [0.0, 0.0, 0.0];
2043 let col_upper: [f64; 3] = [10.0, f64::INFINITY, 8.0];
2044 let row_lower: [f64; 2] = [6.0, 14.0];
2045 let row_upper: [f64; 2] = [6.0, 14.0];
2046 let a_start: [i32; 4] = [0, 2, 2, 3];
2047 let a_index: [i32; 3] = [0, 1, 1];
2048 let a_value: [f64; 3] = [1.0, 2.0, 1.0];
2049
2050 unsafe {
2052 ffi::cobre_highs_pass_lp(
2053 highs,
2054 3,
2055 2,
2056 3,
2057 ffi::HIGHS_MATRIX_FORMAT_COLWISE,
2058 ffi::HIGHS_OBJ_SENSE_MINIMIZE,
2059 0.0,
2060 col_cost.as_ptr(),
2061 col_lower.as_ptr(),
2062 col_upper.as_ptr(),
2063 row_lower.as_ptr(),
2064 row_upper.as_ptr(),
2065 a_start.as_ptr(),
2066 a_index.as_ptr(),
2067 a_value.as_ptr(),
2068 );
2069 ffi::cobre_highs_set_int_option(highs, c"simplex_iteration_limit".as_ptr(), 0);
2070 ffi::cobre_highs_run(highs);
2071 }
2072 let status1 = unsafe { ffi::cobre_highs_get_model_status(highs) };
2073 assert_eq!(status1, ffi::HIGHS_MODEL_STATUS_ITERATION_LIMIT);
2074
2075 unsafe {
2077 ffi::cobre_highs_set_string_option(highs, c"solver".as_ptr(), c"simplex".as_ptr());
2078 ffi::cobre_highs_set_int_option(highs, c"simplex_strategy".as_ptr(), 1);
2079 ffi::cobre_highs_set_string_option(highs, c"presolve".as_ptr(), c"off".as_ptr());
2080 ffi::cobre_highs_set_double_option(
2081 highs,
2082 c"primal_feasibility_tolerance".as_ptr(),
2083 1e-7,
2084 );
2085 ffi::cobre_highs_set_double_option(highs, c"dual_feasibility_tolerance".as_ptr(), 1e-7);
2086 ffi::cobre_highs_set_string_option(highs, c"parallel".as_ptr(), c"off".as_ptr());
2087 ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0);
2088 ffi::cobre_highs_set_int_option(highs, c"simplex_iteration_limit".as_ptr(), i32::MAX);
2090 }
2091
2092 unsafe { ffi::cobre_highs_clear_solver(highs) };
2094 unsafe { ffi::cobre_highs_run(highs) };
2095 let status2 = unsafe { ffi::cobre_highs_get_model_status(highs) };
2096 let obj = unsafe { ffi::cobre_highs_get_objective_value(highs) };
2097 assert_eq!(
2098 status2,
2099 ffi::HIGHS_MODEL_STATUS_OPTIMAL,
2100 "after restoring defaults, second solve must be OPTIMAL, got {status2}"
2101 );
2102 assert!(
2103 (obj - 100.0).abs() < 1e-8,
2104 "objective after restore must be 100.0, got {obj}"
2105 );
2106
2107 unsafe { ffi::cobre_highs_destroy(highs) };
2108 }
2109
2110 #[test]
2115 fn test_research_iteration_limit_one_triggers_iteration_limit_status() {
2116 use crate::ffi;
2117
2118 let highs = unsafe { ffi::cobre_highs_create() };
2119 assert!(!highs.is_null());
2120
2121 unsafe { ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0) };
2122
2123 let col_cost: [f64; 3] = [0.0, 1.0, 50.0];
2124 let col_lower: [f64; 3] = [0.0, 0.0, 0.0];
2125 let col_upper: [f64; 3] = [10.0, f64::INFINITY, 8.0];
2126 let row_lower: [f64; 2] = [6.0, 14.0];
2127 let row_upper: [f64; 2] = [6.0, 14.0];
2128 let a_start: [i32; 4] = [0, 2, 2, 3];
2129 let a_index: [i32; 3] = [0, 1, 1];
2130 let a_value: [f64; 3] = [1.0, 2.0, 1.0];
2131
2132 unsafe {
2133 ffi::cobre_highs_pass_lp(
2134 highs,
2135 3,
2136 2,
2137 3,
2138 ffi::HIGHS_MATRIX_FORMAT_COLWISE,
2139 ffi::HIGHS_OBJ_SENSE_MINIMIZE,
2140 0.0,
2141 col_cost.as_ptr(),
2142 col_lower.as_ptr(),
2143 col_upper.as_ptr(),
2144 row_lower.as_ptr(),
2145 row_upper.as_ptr(),
2146 a_start.as_ptr(),
2147 a_index.as_ptr(),
2148 a_value.as_ptr(),
2149 );
2150 ffi::cobre_highs_set_int_option(highs, c"simplex_iteration_limit".as_ptr(), 1);
2151 ffi::cobre_highs_run(highs);
2152 }
2153
2154 let model_status = unsafe { ffi::cobre_highs_get_model_status(highs) };
2155 eprintln!("iteration_limit=1 model_status: {model_status}");
2156 assert!(
2159 model_status == ffi::HIGHS_MODEL_STATUS_ITERATION_LIMIT
2160 || model_status == ffi::HIGHS_MODEL_STATUS_OPTIMAL,
2161 "expected ITERATION_LIMIT or OPTIMAL, got {model_status}"
2162 );
2163
2164 unsafe { ffi::cobre_highs_destroy(highs) };
2165 }
2166}