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
191impl HighsSolver {
192 pub fn new() -> Result<Self, SolverError> {
217 let handle = unsafe { ffi::cobre_highs_create() };
222
223 if handle.is_null() {
224 return Err(SolverError::InternalError {
225 message: "HiGHS instance creation failed: Highs_create() returned null".to_string(),
226 error_code: None,
227 });
228 }
229
230 if let Err(e) = Self::apply_default_config(handle) {
233 unsafe { ffi::cobre_highs_destroy(handle) };
238 return Err(e);
239 }
240
241 Ok(Self {
242 handle,
243 col_value: Vec::new(),
244 col_dual: Vec::new(),
245 row_value: Vec::new(),
246 row_dual: Vec::new(),
247 scratch_i32: Vec::new(),
248 basis_col_i32: Vec::new(),
249 basis_row_i32: Vec::new(),
250 num_cols: 0,
251 num_rows: 0,
252 has_model: false,
253 stats: SolverStatistics::default(),
254 })
255 }
256
257 fn apply_default_config(handle: *mut c_void) -> Result<(), SolverError> {
263 for opt in &default_options() {
264 let status = unsafe { opt.apply(handle) };
266 if status == ffi::HIGHS_STATUS_ERROR {
267 return Err(SolverError::InternalError {
268 message: format!(
269 "HiGHS configuration failed: {}",
270 opt.name.to_str().unwrap_or("?")
271 ),
272 error_code: Some(status),
273 });
274 }
275 }
276 Ok(())
277 }
278
279 fn extract_solution_view(&mut self, solve_time_seconds: f64) -> SolutionView<'_> {
286 let status = unsafe {
288 ffi::cobre_highs_get_solution(
289 self.handle,
290 self.col_value.as_mut_ptr(),
291 self.col_dual.as_mut_ptr(),
292 self.row_value.as_mut_ptr(),
293 self.row_dual.as_mut_ptr(),
294 )
295 };
296 assert_ne!(
297 status,
298 ffi::HIGHS_STATUS_ERROR,
299 "cobre_highs_get_solution failed after optimal solve"
300 );
301
302 let objective = unsafe { ffi::cobre_highs_get_objective_value(self.handle) };
304
305 #[allow(clippy::cast_sign_loss)]
307 let iterations =
308 unsafe { ffi::cobre_highs_get_simplex_iteration_count(self.handle) } as u64;
309
310 SolutionView {
311 objective,
312 primal: &self.col_value[..self.num_cols],
313 dual: &self.row_dual[..self.num_rows],
314 reduced_costs: &self.col_dual[..self.num_cols],
315 iterations,
316 solve_time_seconds,
317 }
318 }
319
320 fn restore_default_settings(&mut self) {
324 for opt in &default_options() {
325 unsafe { opt.apply(self.handle) };
327 }
328 }
329
330 fn run_once(&mut self) -> i32 {
332 let run_status = unsafe { ffi::cobre_highs_run(self.handle) };
334 if run_status == ffi::HIGHS_STATUS_ERROR {
335 return ffi::HIGHS_MODEL_STATUS_SOLVE_ERROR;
336 }
337 unsafe { ffi::cobre_highs_get_model_status(self.handle) }
339 }
340
341 fn interpret_terminal_status(
346 &mut self,
347 status: i32,
348 solve_time_seconds: f64,
349 ) -> Option<SolverError> {
350 match status {
351 ffi::HIGHS_MODEL_STATUS_OPTIMAL => {
352 None
354 }
355 ffi::HIGHS_MODEL_STATUS_INFEASIBLE => Some(SolverError::Infeasible),
356 ffi::HIGHS_MODEL_STATUS_UNBOUNDED_OR_INFEASIBLE => {
357 let mut has_dual_ray: i32 = 0;
361 let mut dual_buf = vec![0.0_f64; self.num_rows];
364 let dual_status = unsafe {
366 ffi::cobre_highs_get_dual_ray(
367 self.handle,
368 &raw mut has_dual_ray,
369 dual_buf.as_mut_ptr(),
370 )
371 };
372 if dual_status != ffi::HIGHS_STATUS_ERROR && has_dual_ray != 0 {
373 return Some(SolverError::Infeasible);
374 }
375 let mut has_primal_ray: i32 = 0;
376 let mut primal_buf = vec![0.0_f64; self.num_cols];
377 let primal_status = unsafe {
379 ffi::cobre_highs_get_primal_ray(
380 self.handle,
381 &raw mut has_primal_ray,
382 primal_buf.as_mut_ptr(),
383 )
384 };
385 if primal_status != ffi::HIGHS_STATUS_ERROR && has_primal_ray != 0 {
386 return Some(SolverError::Unbounded);
387 }
388 Some(SolverError::Infeasible)
389 }
390 ffi::HIGHS_MODEL_STATUS_UNBOUNDED => Some(SolverError::Unbounded),
391 ffi::HIGHS_MODEL_STATUS_TIME_LIMIT => Some(SolverError::TimeLimitExceeded {
392 elapsed_seconds: solve_time_seconds,
393 }),
394 ffi::HIGHS_MODEL_STATUS_ITERATION_LIMIT => {
395 #[allow(clippy::cast_sign_loss)]
397 let iterations =
398 unsafe { ffi::cobre_highs_get_simplex_iteration_count(self.handle) } as u64;
399 Some(SolverError::IterationLimit { iterations })
400 }
401 ffi::HIGHS_MODEL_STATUS_SOLVE_ERROR | ffi::HIGHS_MODEL_STATUS_UNKNOWN => {
402 None
404 }
405 other => Some(SolverError::InternalError {
406 message: format!("HiGHS returned unexpected model status {other}"),
407 error_code: Some(other),
408 }),
409 }
410 }
411
412 fn convert_to_i32_scratch(&mut self, source: &[usize]) -> &[i32] {
416 if source.len() > self.scratch_i32.len() {
417 self.scratch_i32.resize(source.len(), 0);
418 }
419 for (i, &v) in source.iter().enumerate() {
420 debug_assert!(
421 i32::try_from(v).is_ok(),
422 "usize index {v} overflows i32::MAX at position {i}"
423 );
424 #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
426 {
427 self.scratch_i32[i] = v as i32;
428 }
429 }
430 &self.scratch_i32[..source.len()]
431 }
432}
433
434impl Drop for HighsSolver {
435 fn drop(&mut self) {
436 unsafe { ffi::cobre_highs_destroy(self.handle) };
438 }
439}
440
441impl SolverInterface for HighsSolver {
442 fn name(&self) -> &'static str {
443 "HiGHS"
444 }
445
446 fn load_model(&mut self, template: &StageTemplate) {
447 let t0 = Instant::now();
448 assert!(
458 i32::try_from(template.num_cols).is_ok(),
459 "num_cols {} overflows i32: LP exceeds HiGHS API limit",
460 template.num_cols
461 );
462 assert!(
463 i32::try_from(template.num_rows).is_ok(),
464 "num_rows {} overflows i32: LP exceeds HiGHS API limit",
465 template.num_rows
466 );
467 assert!(
468 i32::try_from(template.num_nz).is_ok(),
469 "num_nz {} overflows i32: LP exceeds HiGHS API limit",
470 template.num_nz
471 );
472 #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
474 let num_col = template.num_cols as i32;
475 #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
476 let num_row = template.num_rows as i32;
477 #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
478 let num_nz = template.num_nz as i32;
479 let status = unsafe {
480 ffi::cobre_highs_pass_lp(
481 self.handle,
482 num_col,
483 num_row,
484 num_nz,
485 ffi::HIGHS_MATRIX_FORMAT_COLWISE,
486 ffi::HIGHS_OBJ_SENSE_MINIMIZE,
487 0.0, template.objective.as_ptr(),
489 template.col_lower.as_ptr(),
490 template.col_upper.as_ptr(),
491 template.row_lower.as_ptr(),
492 template.row_upper.as_ptr(),
493 template.col_starts.as_ptr(),
494 template.row_indices.as_ptr(),
495 template.values.as_ptr(),
496 )
497 };
498
499 assert_ne!(
500 status,
501 ffi::HIGHS_STATUS_ERROR,
502 "cobre_highs_pass_lp failed with status {status}"
503 );
504
505 self.num_cols = template.num_cols;
506 self.num_rows = template.num_rows;
507 self.has_model = true;
508
509 self.col_value.resize(self.num_cols, 0.0);
512 self.col_dual.resize(self.num_cols, 0.0);
513 self.row_value.resize(self.num_rows, 0.0);
514 self.row_dual.resize(self.num_rows, 0.0);
515
516 self.basis_col_i32.resize(self.num_cols, 0);
519 self.basis_row_i32.resize(self.num_rows, 0);
520 self.stats.total_load_model_time_seconds += t0.elapsed().as_secs_f64();
521 self.stats.load_model_count += 1;
522 }
523
524 fn add_rows(&mut self, cuts: &RowBatch) {
525 let t0 = Instant::now();
526 assert!(
527 i32::try_from(cuts.num_rows).is_ok(),
528 "cuts.num_rows {} overflows i32: RowBatch exceeds HiGHS API limit",
529 cuts.num_rows
530 );
531 assert!(
532 i32::try_from(cuts.col_indices.len()).is_ok(),
533 "cuts nnz {} overflows i32: RowBatch exceeds HiGHS API limit",
534 cuts.col_indices.len()
535 );
536 #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
538 let num_new_row = cuts.num_rows as i32;
539 #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
540 let num_new_nz = cuts.col_indices.len() as i32;
541
542 let status = unsafe {
550 ffi::cobre_highs_add_rows(
551 self.handle,
552 num_new_row,
553 cuts.row_lower.as_ptr(),
554 cuts.row_upper.as_ptr(),
555 num_new_nz,
556 cuts.row_starts.as_ptr(),
557 cuts.col_indices.as_ptr(),
558 cuts.values.as_ptr(),
559 )
560 };
561
562 assert_ne!(
563 status,
564 ffi::HIGHS_STATUS_ERROR,
565 "cobre_highs_add_rows failed with status {status}"
566 );
567
568 self.num_rows += cuts.num_rows;
569
570 self.row_value.resize(self.num_rows, 0.0);
572 self.row_dual.resize(self.num_rows, 0.0);
573
574 self.basis_row_i32.resize(self.num_rows, 0);
576 self.stats.total_add_rows_time_seconds += t0.elapsed().as_secs_f64();
577 self.stats.add_rows_count += 1;
578 }
579
580 fn set_row_bounds(&mut self, indices: &[usize], lower: &[f64], upper: &[f64]) {
581 assert!(
582 indices.len() == lower.len() && indices.len() == upper.len(),
583 "set_row_bounds: indices ({}), lower ({}), and upper ({}) must have equal length",
584 indices.len(),
585 lower.len(),
586 upper.len()
587 );
588 if indices.is_empty() {
589 return;
590 }
591
592 assert!(
593 i32::try_from(indices.len()).is_ok(),
594 "set_row_bounds: indices.len() {} overflows i32",
595 indices.len()
596 );
597 #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
598 let num_entries = indices.len() as i32;
599
600 let t0 = Instant::now();
601 let status = unsafe {
608 ffi::cobre_highs_change_rows_bounds_by_set(
609 self.handle,
610 num_entries,
611 self.convert_to_i32_scratch(indices).as_ptr(),
612 lower.as_ptr(),
613 upper.as_ptr(),
614 )
615 };
616
617 assert_ne!(
618 status,
619 ffi::HIGHS_STATUS_ERROR,
620 "cobre_highs_change_rows_bounds_by_set failed with status {status}"
621 );
622 self.stats.total_set_bounds_time_seconds += t0.elapsed().as_secs_f64();
623 }
624
625 fn set_col_bounds(&mut self, indices: &[usize], lower: &[f64], upper: &[f64]) {
626 assert!(
627 indices.len() == lower.len() && indices.len() == upper.len(),
628 "set_col_bounds: indices ({}), lower ({}), and upper ({}) must have equal length",
629 indices.len(),
630 lower.len(),
631 upper.len()
632 );
633 if indices.is_empty() {
634 return;
635 }
636
637 assert!(
638 i32::try_from(indices.len()).is_ok(),
639 "set_col_bounds: indices.len() {} overflows i32",
640 indices.len()
641 );
642 #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
643 let num_entries = indices.len() as i32;
644
645 let t0 = Instant::now();
646 let status = unsafe {
652 ffi::cobre_highs_change_cols_bounds_by_set(
653 self.handle,
654 num_entries,
655 self.convert_to_i32_scratch(indices).as_ptr(),
656 lower.as_ptr(),
657 upper.as_ptr(),
658 )
659 };
660
661 assert_ne!(
662 status,
663 ffi::HIGHS_STATUS_ERROR,
664 "cobre_highs_change_cols_bounds_by_set failed with status {status}"
665 );
666 self.stats.total_set_bounds_time_seconds += t0.elapsed().as_secs_f64();
667 }
668
669 #[allow(clippy::too_many_lines)]
670 fn solve(&mut self) -> Result<SolutionView<'_>, SolverError> {
671 assert!(
672 self.has_model,
673 "solve called without a loaded model — call load_model first"
674 );
675 let t0 = Instant::now();
676 let model_status = self.run_once();
677 let solve_time = t0.elapsed().as_secs_f64();
678
679 self.stats.solve_count += 1;
680
681 if model_status == ffi::HIGHS_MODEL_STATUS_OPTIMAL {
682 #[allow(clippy::cast_sign_loss)]
687 let iterations =
688 unsafe { ffi::cobre_highs_get_simplex_iteration_count(self.handle) } as u64;
689 self.stats.success_count += 1;
690 self.stats.first_try_successes += 1;
691 self.stats.total_iterations += iterations;
692 self.stats.total_solve_time_seconds += solve_time;
693 return Ok(self.extract_solution_view(solve_time));
694 }
695
696 let is_unbounded = model_status == ffi::HIGHS_MODEL_STATUS_UNBOUNDED;
701 if !is_unbounded {
702 if let Some(terminal_err) = self.interpret_terminal_status(model_status, solve_time) {
703 self.stats.failure_count += 1;
704 return Err(terminal_err);
705 }
706 }
707
708 let retry_time_limit = 30.0_f64;
726 let num_retry_levels = 12_u32;
727
728 let mut retry_attempts: u64 = 0;
729 let mut terminal_err: Option<SolverError> = None;
730 let mut found_optimal = false;
731 let mut optimal_time = 0.0_f64;
732 let mut optimal_iterations: u64 = 0;
733
734 for level in 0..num_retry_levels {
735 match level {
738 0 => {
742 unsafe { ffi::cobre_highs_clear_solver(self.handle) };
743 }
744 1 => unsafe {
746 ffi::cobre_highs_set_string_option(
747 self.handle,
748 c"presolve".as_ptr(),
749 c"on".as_ptr(),
750 );
751 },
752 2 => unsafe {
755 ffi::cobre_highs_set_int_option(self.handle, c"simplex_strategy".as_ptr(), 1);
756 },
757 3 => unsafe {
760 ffi::cobre_highs_set_double_option(
761 self.handle,
762 c"primal_feasibility_tolerance".as_ptr(),
763 1e-6,
764 );
765 ffi::cobre_highs_set_double_option(
766 self.handle,
767 c"dual_feasibility_tolerance".as_ptr(),
768 1e-6,
769 );
770 },
771 4 => unsafe {
774 ffi::cobre_highs_set_string_option(
775 self.handle,
776 c"solver".as_ptr(),
777 c"ipm".as_ptr(),
778 );
779 },
780
781 5 => {
787 self.restore_default_settings();
788 unsafe {
789 ffi::cobre_highs_set_string_option(
790 self.handle,
791 c"presolve".as_ptr(),
792 c"on".as_ptr(),
793 );
794 ffi::cobre_highs_set_double_option(
795 self.handle,
796 c"time_limit".as_ptr(),
797 retry_time_limit,
798 );
799 ffi::cobre_highs_set_int_option(
800 self.handle,
801 c"simplex_scale_strategy".as_ptr(),
802 3, );
804 }
805 }
806 6 => {
808 self.restore_default_settings();
809 unsafe {
810 ffi::cobre_highs_set_string_option(
811 self.handle,
812 c"presolve".as_ptr(),
813 c"on".as_ptr(),
814 );
815 ffi::cobre_highs_set_double_option(
816 self.handle,
817 c"time_limit".as_ptr(),
818 retry_time_limit,
819 );
820 ffi::cobre_highs_set_int_option(
821 self.handle,
822 c"simplex_strategy".as_ptr(),
823 1,
824 );
825 ffi::cobre_highs_set_int_option(
826 self.handle,
827 c"simplex_scale_strategy".as_ptr(),
828 4, );
830 }
831 }
832 7 => {
834 self.restore_default_settings();
835 unsafe {
836 ffi::cobre_highs_set_string_option(
837 self.handle,
838 c"presolve".as_ptr(),
839 c"on".as_ptr(),
840 );
841 ffi::cobre_highs_set_double_option(
842 self.handle,
843 c"time_limit".as_ptr(),
844 retry_time_limit,
845 );
846 ffi::cobre_highs_set_int_option(
847 self.handle,
848 c"simplex_scale_strategy".as_ptr(),
849 3,
850 );
851 ffi::cobre_highs_set_double_option(
852 self.handle,
853 c"primal_feasibility_tolerance".as_ptr(),
854 1e-6,
855 );
856 ffi::cobre_highs_set_double_option(
857 self.handle,
858 c"dual_feasibility_tolerance".as_ptr(),
859 1e-6,
860 );
861 }
862 }
863 8 => {
865 self.restore_default_settings();
866 unsafe {
867 ffi::cobre_highs_set_string_option(
868 self.handle,
869 c"presolve".as_ptr(),
870 c"on".as_ptr(),
871 );
872 ffi::cobre_highs_set_double_option(
873 self.handle,
874 c"time_limit".as_ptr(),
875 retry_time_limit,
876 );
877 ffi::cobre_highs_set_int_option(
878 self.handle,
879 c"user_objective_scale".as_ptr(),
880 -10,
881 );
882 }
883 }
884 9 => {
886 self.restore_default_settings();
887 unsafe {
888 ffi::cobre_highs_set_string_option(
889 self.handle,
890 c"presolve".as_ptr(),
891 c"on".as_ptr(),
892 );
893 ffi::cobre_highs_set_double_option(
894 self.handle,
895 c"time_limit".as_ptr(),
896 retry_time_limit,
897 );
898 ffi::cobre_highs_set_int_option(
899 self.handle,
900 c"simplex_strategy".as_ptr(),
901 1,
902 );
903 ffi::cobre_highs_set_int_option(
904 self.handle,
905 c"user_objective_scale".as_ptr(),
906 -10,
907 );
908 ffi::cobre_highs_set_int_option(
909 self.handle,
910 c"user_bound_scale".as_ptr(),
911 -5,
912 );
913 }
914 }
915 10 => {
917 self.restore_default_settings();
918 unsafe {
919 ffi::cobre_highs_set_string_option(
920 self.handle,
921 c"presolve".as_ptr(),
922 c"on".as_ptr(),
923 );
924 ffi::cobre_highs_set_double_option(
925 self.handle,
926 c"time_limit".as_ptr(),
927 retry_time_limit,
928 );
929 ffi::cobre_highs_set_int_option(
930 self.handle,
931 c"user_objective_scale".as_ptr(),
932 -13,
933 );
934 ffi::cobre_highs_set_int_option(
935 self.handle,
936 c"user_bound_scale".as_ptr(),
937 -8,
938 );
939 ffi::cobre_highs_set_double_option(
940 self.handle,
941 c"primal_feasibility_tolerance".as_ptr(),
942 1e-6,
943 );
944 ffi::cobre_highs_set_double_option(
945 self.handle,
946 c"dual_feasibility_tolerance".as_ptr(),
947 1e-6,
948 );
949 }
950 }
951 11 => {
954 self.restore_default_settings();
955 unsafe {
956 ffi::cobre_highs_set_string_option(
957 self.handle,
958 c"solver".as_ptr(),
959 c"ipm".as_ptr(),
960 );
961 ffi::cobre_highs_set_string_option(
962 self.handle,
963 c"presolve".as_ptr(),
964 c"on".as_ptr(),
965 );
966 ffi::cobre_highs_set_double_option(
967 self.handle,
968 c"time_limit".as_ptr(),
969 retry_time_limit,
970 );
971 ffi::cobre_highs_set_int_option(
972 self.handle,
973 c"user_objective_scale".as_ptr(),
974 -10,
975 );
976 ffi::cobre_highs_set_int_option(
977 self.handle,
978 c"user_bound_scale".as_ptr(),
979 -5,
980 );
981 ffi::cobre_highs_set_double_option(
982 self.handle,
983 c"primal_feasibility_tolerance".as_ptr(),
984 1e-6,
985 );
986 ffi::cobre_highs_set_double_option(
987 self.handle,
988 c"dual_feasibility_tolerance".as_ptr(),
989 1e-6,
990 );
991 }
992 }
993 _ => unreachable!(),
994 }
995
996 retry_attempts += 1;
997
998 let t_retry = Instant::now();
999 let retry_status = self.run_once();
1000 let retry_time = t_retry.elapsed().as_secs_f64();
1001
1002 if retry_status == ffi::HIGHS_MODEL_STATUS_OPTIMAL {
1003 #[allow(clippy::cast_sign_loss)]
1006 let iters =
1007 unsafe { ffi::cobre_highs_get_simplex_iteration_count(self.handle) } as u64;
1008 found_optimal = true;
1009 optimal_time = retry_time;
1010 optimal_iterations = iters;
1011 break;
1012 }
1013
1014 let retryable = retry_status == ffi::HIGHS_MODEL_STATUS_UNBOUNDED
1019 || retry_status == ffi::HIGHS_MODEL_STATUS_TIME_LIMIT;
1020 if !retryable {
1021 if let Some(e) = self.interpret_terminal_status(retry_status, retry_time) {
1022 terminal_err = Some(e);
1023 break;
1024 }
1025 }
1026 }
1028
1029 self.restore_default_settings();
1033 unsafe {
1034 ffi::cobre_highs_set_double_option(self.handle, c"time_limit".as_ptr(), f64::INFINITY);
1035 ffi::cobre_highs_set_int_option(self.handle, c"user_objective_scale".as_ptr(), 0);
1036 ffi::cobre_highs_set_int_option(self.handle, c"user_bound_scale".as_ptr(), 0);
1037 }
1038
1039 self.stats.retry_count += retry_attempts;
1041
1042 if found_optimal {
1043 self.stats.success_count += 1;
1044 self.stats.total_iterations += optimal_iterations;
1045 self.stats.total_solve_time_seconds += optimal_time;
1046 return Ok(self.extract_solution_view(optimal_time));
1047 }
1048
1049 self.stats.failure_count += 1;
1050 Err(terminal_err.unwrap_or_else(|| {
1051 if is_unbounded {
1053 SolverError::Unbounded
1054 } else {
1055 SolverError::NumericalDifficulty {
1056 message: "HiGHS failed to reach optimality after all retry escalation levels"
1057 .to_string(),
1058 }
1059 }
1060 }))
1061 }
1062
1063 fn reset(&mut self) {
1064 let status = unsafe { ffi::cobre_highs_clear_solver(self.handle) };
1069 debug_assert_ne!(
1070 status,
1071 ffi::HIGHS_STATUS_ERROR,
1072 "cobre_highs_clear_solver failed — HiGHS internal state may be inconsistent"
1073 );
1074 self.num_cols = 0;
1076 self.num_rows = 0;
1077 self.has_model = false;
1078 }
1081
1082 fn get_basis(&mut self, out: &mut crate::types::Basis) {
1083 assert!(
1084 self.has_model,
1085 "get_basis called without a loaded model — call load_model first"
1086 );
1087
1088 out.col_status.resize(self.num_cols, 0);
1089 out.row_status.resize(self.num_rows, 0);
1090
1091 let get_status = unsafe {
1097 ffi::cobre_highs_get_basis(
1098 self.handle,
1099 out.col_status.as_mut_ptr(),
1100 out.row_status.as_mut_ptr(),
1101 )
1102 };
1103
1104 assert_ne!(
1105 get_status,
1106 ffi::HIGHS_STATUS_ERROR,
1107 "cobre_highs_get_basis failed: basis must exist after a successful solve (programming error)"
1108 );
1109 }
1110
1111 fn solve_with_basis(
1112 &mut self,
1113 basis: &crate::types::Basis,
1114 ) -> Result<crate::types::SolutionView<'_>, SolverError> {
1115 assert!(
1116 self.has_model,
1117 "solve_with_basis called without a loaded model — call load_model first"
1118 );
1119 assert!(
1120 basis.col_status.len() == self.num_cols,
1121 "basis column count {} does not match LP column count {}",
1122 basis.col_status.len(),
1123 self.num_cols
1124 );
1125
1126 self.stats.basis_offered += 1;
1128
1129 self.basis_col_i32[..self.num_cols].copy_from_slice(&basis.col_status);
1132
1133 let basis_rows = basis.row_status.len();
1137 let lp_rows = self.num_rows;
1138 let copy_len = basis_rows.min(lp_rows);
1139 self.basis_row_i32[..copy_len].copy_from_slice(&basis.row_status[..copy_len]);
1140 if lp_rows > basis_rows {
1141 self.basis_row_i32[basis_rows..lp_rows].fill(ffi::HIGHS_BASIS_STATUS_BASIC);
1142 }
1143
1144 let basis_set_start = Instant::now();
1151 let set_status = unsafe {
1152 ffi::cobre_highs_set_basis(
1153 self.handle,
1154 self.basis_col_i32.as_ptr(),
1155 self.basis_row_i32.as_ptr(),
1156 )
1157 };
1158 self.stats.total_basis_set_time_seconds += basis_set_start.elapsed().as_secs_f64();
1159
1160 if set_status == ffi::HIGHS_STATUS_ERROR {
1162 self.stats.basis_rejections += 1;
1163 debug_assert!(false, "raw basis rejected; falling back to cold-start");
1164 }
1165
1166 self.solve()
1168 }
1169
1170 fn statistics(&self) -> SolverStatistics {
1171 self.stats.clone()
1172 }
1173}
1174
1175#[cfg(feature = "test-support")]
1181impl HighsSolver {
1182 #[must_use]
1190 pub fn raw_handle(&self) -> *mut std::os::raw::c_void {
1191 self.handle
1192 }
1193}
1194
1195#[cfg(test)]
1196mod tests {
1197 use super::HighsSolver;
1198 use crate::{
1199 SolverInterface,
1200 types::{Basis, RowBatch, StageTemplate},
1201 };
1202
1203 fn make_fixture_stage_template() -> StageTemplate {
1216 StageTemplate {
1217 num_cols: 3,
1218 num_rows: 2,
1219 num_nz: 3,
1220 col_starts: vec![0_i32, 2, 2, 3],
1221 row_indices: vec![0_i32, 1, 1],
1222 values: vec![1.0, 2.0, 1.0],
1223 col_lower: vec![0.0, 0.0, 0.0],
1224 col_upper: vec![10.0, f64::INFINITY, 8.0],
1225 objective: vec![0.0, 1.0, 50.0],
1226 row_lower: vec![6.0, 14.0],
1227 row_upper: vec![6.0, 14.0],
1228 n_state: 1,
1229 n_transfer: 0,
1230 n_dual_relevant: 1,
1231 n_hydro: 1,
1232 max_par_order: 0,
1233 col_scale: Vec::new(),
1234 row_scale: Vec::new(),
1235 }
1236 }
1237
1238 fn make_fixture_row_batch() -> RowBatch {
1242 RowBatch {
1243 num_rows: 2,
1244 row_starts: vec![0_i32, 2, 4],
1245 col_indices: vec![0_i32, 1, 0, 1],
1246 values: vec![-5.0, 1.0, 3.0, 1.0],
1247 row_lower: vec![20.0, 80.0],
1248 row_upper: vec![f64::INFINITY, f64::INFINITY],
1249 }
1250 }
1251
1252 #[test]
1253 fn test_highs_solver_create_and_name() {
1254 let solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1255 assert_eq!(solver.name(), "HiGHS");
1256 }
1258
1259 #[test]
1260 fn test_highs_solver_send_bound() {
1261 fn assert_send<T: Send>() {}
1262 assert_send::<HighsSolver>();
1263 }
1264
1265 #[test]
1266 fn test_highs_solver_statistics_initial() {
1267 let solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1268 let stats = solver.statistics();
1269 assert_eq!(stats.solve_count, 0);
1270 assert_eq!(stats.success_count, 0);
1271 assert_eq!(stats.failure_count, 0);
1272 assert_eq!(stats.total_iterations, 0);
1273 assert_eq!(stats.retry_count, 0);
1274 assert_eq!(stats.total_solve_time_seconds, 0.0);
1275 }
1276
1277 #[test]
1278 fn test_highs_load_model_updates_dimensions() {
1279 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1280 let template = make_fixture_stage_template();
1281
1282 solver.load_model(&template);
1283
1284 assert_eq!(solver.num_cols, 3, "num_cols must be 3 after load_model");
1285 assert_eq!(solver.num_rows, 2, "num_rows must be 2 after load_model");
1286 assert_eq!(
1287 solver.col_value.len(),
1288 3,
1289 "col_value buffer must be resized to num_cols"
1290 );
1291 assert_eq!(
1292 solver.col_dual.len(),
1293 3,
1294 "col_dual buffer must be resized to num_cols"
1295 );
1296 assert_eq!(
1297 solver.row_value.len(),
1298 2,
1299 "row_value buffer must be resized to num_rows"
1300 );
1301 assert_eq!(
1302 solver.row_dual.len(),
1303 2,
1304 "row_dual buffer must be resized to num_rows"
1305 );
1306 }
1307
1308 #[test]
1309 fn test_highs_add_rows_updates_dimensions() {
1310 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1311 let template = make_fixture_stage_template();
1312 let cuts = make_fixture_row_batch();
1313
1314 solver.load_model(&template);
1315 solver.add_rows(&cuts);
1316
1317 assert_eq!(solver.num_rows, 4, "num_rows must be 4 after add_rows");
1319 assert_eq!(
1320 solver.row_dual.len(),
1321 4,
1322 "row_dual buffer must be resized to 4 after add_rows"
1323 );
1324 assert_eq!(
1325 solver.row_value.len(),
1326 4,
1327 "row_value buffer must be resized to 4 after add_rows"
1328 );
1329 assert_eq!(solver.num_cols, 3, "num_cols must be unchanged by add_rows");
1331 }
1332
1333 #[test]
1334 fn test_highs_set_row_bounds_no_panic() {
1335 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1336 let template = make_fixture_stage_template();
1337 solver.load_model(&template);
1338
1339 solver.set_row_bounds(&[0], &[4.0], &[4.0]);
1341 }
1342
1343 #[test]
1344 fn test_highs_set_col_bounds_no_panic() {
1345 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1346 let template = make_fixture_stage_template();
1347 solver.load_model(&template);
1348
1349 solver.set_col_bounds(&[1], &[10.0], &[f64::INFINITY]);
1351 }
1352
1353 #[test]
1354 fn test_highs_set_bounds_empty_no_panic() {
1355 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1356 let template = make_fixture_stage_template();
1357 solver.load_model(&template);
1358
1359 solver.set_row_bounds(&[], &[], &[]);
1361 solver.set_col_bounds(&[], &[], &[]);
1362 }
1363
1364 #[test]
1367 fn test_highs_solve_basic_lp() {
1368 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1369 let template = make_fixture_stage_template();
1370 solver.load_model(&template);
1371
1372 let solution = solver
1373 .solve()
1374 .expect("solve() must succeed on a feasible LP");
1375
1376 assert!(
1377 (solution.objective - 100.0).abs() < 1e-8,
1378 "objective must be 100.0, got {}",
1379 solution.objective
1380 );
1381 assert_eq!(solution.primal.len(), 3, "primal must have 3 elements");
1382 assert!(
1383 (solution.primal[0] - 6.0).abs() < 1e-8,
1384 "primal[0] (x0) must be 6.0, got {}",
1385 solution.primal[0]
1386 );
1387 assert!(
1388 (solution.primal[1] - 0.0).abs() < 1e-8,
1389 "primal[1] (x1) must be 0.0, got {}",
1390 solution.primal[1]
1391 );
1392 assert!(
1393 (solution.primal[2] - 2.0).abs() < 1e-8,
1394 "primal[2] (x2) must be 2.0, got {}",
1395 solution.primal[2]
1396 );
1397 }
1398
1399 #[test]
1403 fn test_highs_solve_with_cuts() {
1404 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1405 let template = make_fixture_stage_template();
1406 let cuts = make_fixture_row_batch();
1407 solver.load_model(&template);
1408 solver.add_rows(&cuts);
1409
1410 let solution = solver
1411 .solve()
1412 .expect("solve() must succeed on a feasible LP with cuts");
1413
1414 assert!(
1415 (solution.objective - 162.0).abs() < 1e-8,
1416 "objective must be 162.0, got {}",
1417 solution.objective
1418 );
1419 assert!(
1420 (solution.primal[0] - 6.0).abs() < 1e-8,
1421 "primal[0] must be 6.0, got {}",
1422 solution.primal[0]
1423 );
1424 assert!(
1425 (solution.primal[1] - 62.0).abs() < 1e-8,
1426 "primal[1] must be 62.0, got {}",
1427 solution.primal[1]
1428 );
1429 assert!(
1430 (solution.primal[2] - 2.0).abs() < 1e-8,
1431 "primal[2] must be 2.0, got {}",
1432 solution.primal[2]
1433 );
1434 }
1435
1436 #[test]
1439 fn test_highs_solve_after_rhs_patch() {
1440 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1441 let template = make_fixture_stage_template();
1442 let cuts = make_fixture_row_batch();
1443 solver.load_model(&template);
1444 solver.add_rows(&cuts);
1445
1446 solver.set_row_bounds(&[0], &[4.0], &[4.0]);
1448
1449 let solution = solver
1450 .solve()
1451 .expect("solve() must succeed after RHS patch");
1452
1453 assert!(
1454 (solution.objective - 368.0).abs() < 1e-8,
1455 "objective must be 368.0, got {}",
1456 solution.objective
1457 );
1458 }
1459
1460 #[test]
1462 fn test_highs_solve_statistics_increment() {
1463 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1464 let template = make_fixture_stage_template();
1465 solver.load_model(&template);
1466
1467 solver.solve().expect("first solve must succeed");
1468 solver.solve().expect("second solve must succeed");
1469
1470 let stats = solver.statistics();
1471 assert_eq!(stats.solve_count, 2, "solve_count must be 2");
1472 assert_eq!(stats.success_count, 2, "success_count must be 2");
1473 assert_eq!(stats.failure_count, 0, "failure_count must be 0");
1474 assert!(
1475 stats.total_iterations > 0,
1476 "total_iterations must be positive"
1477 );
1478 }
1479
1480 #[test]
1482 fn test_highs_reset_preserves_stats() {
1483 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1484 let template = make_fixture_stage_template();
1485 solver.load_model(&template);
1486 solver.solve().expect("solve must succeed");
1487
1488 let stats_before = solver.statistics();
1489 assert_eq!(
1490 stats_before.solve_count, 1,
1491 "solve_count must be 1 before reset"
1492 );
1493
1494 solver.reset();
1495
1496 let stats_after = solver.statistics();
1497 assert_eq!(
1498 stats_after.solve_count, stats_before.solve_count,
1499 "solve_count must be unchanged after reset"
1500 );
1501 assert_eq!(
1502 stats_after.success_count, stats_before.success_count,
1503 "success_count must be unchanged after reset"
1504 );
1505 assert_eq!(
1506 stats_after.total_iterations, stats_before.total_iterations,
1507 "total_iterations must be unchanged after reset"
1508 );
1509 }
1510
1511 #[test]
1513 fn test_highs_solve_iterations_positive() {
1514 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1515 let template = make_fixture_stage_template();
1516 solver.load_model(&template);
1517
1518 let solution = solver.solve().expect("solve must succeed");
1519 assert!(
1520 solution.iterations > 0,
1521 "iterations must be positive, got {}",
1522 solution.iterations
1523 );
1524 }
1525
1526 #[test]
1528 fn test_highs_solve_time_positive() {
1529 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1530 let template = make_fixture_stage_template();
1531 solver.load_model(&template);
1532
1533 let solution = solver.solve().expect("solve must succeed");
1534 assert!(
1535 solution.solve_time_seconds > 0.0,
1536 "solve_time_seconds must be positive, got {}",
1537 solution.solve_time_seconds
1538 );
1539 }
1540
1541 #[test]
1544 fn test_highs_solve_statistics_single() {
1545 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1546 let template = make_fixture_stage_template();
1547 solver.load_model(&template);
1548
1549 solver.solve().expect("solve must succeed");
1550
1551 let stats = solver.statistics();
1552 assert_eq!(stats.solve_count, 1, "solve_count must be 1");
1553 assert_eq!(stats.success_count, 1, "success_count must be 1");
1554 assert_eq!(stats.failure_count, 0, "failure_count must be 0");
1555 assert!(
1556 stats.total_iterations > 0,
1557 "total_iterations must be positive after a successful solve"
1558 );
1559 }
1560
1561 #[test]
1564 fn test_get_basis_valid_status_codes() {
1565 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1566 let template = make_fixture_stage_template();
1567 solver.load_model(&template);
1568 solver.solve().expect("solve must succeed before get_basis");
1569
1570 let mut basis = Basis::new(0, 0);
1571 solver.get_basis(&mut basis);
1572
1573 for &code in &basis.col_status {
1574 assert!(
1575 (0..=4).contains(&code),
1576 "col_status code {code} is outside valid HiGHS range 0..=4"
1577 );
1578 }
1579 for &code in &basis.row_status {
1580 assert!(
1581 (0..=4).contains(&code),
1582 "row_status code {code} is outside valid HiGHS range 0..=4"
1583 );
1584 }
1585 }
1586
1587 #[test]
1590 fn test_get_basis_resizes_output() {
1591 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1592 let template = make_fixture_stage_template();
1593 solver.load_model(&template);
1594 solver.solve().expect("solve must succeed before get_basis");
1595
1596 let mut basis = Basis::new(0, 0);
1597 assert_eq!(
1598 basis.col_status.len(),
1599 0,
1600 "initial col_status must be empty"
1601 );
1602 assert_eq!(
1603 basis.row_status.len(),
1604 0,
1605 "initial row_status must be empty"
1606 );
1607
1608 solver.get_basis(&mut basis);
1609
1610 assert_eq!(
1611 basis.col_status.len(),
1612 3,
1613 "col_status must be resized to 3 (num_cols of SS1.1)"
1614 );
1615 assert_eq!(
1616 basis.row_status.len(),
1617 2,
1618 "row_status must be resized to 2 (num_rows of SS1.1)"
1619 );
1620 }
1621
1622 #[test]
1625 fn test_solve_with_basis_warm_start() {
1626 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1627 let template = make_fixture_stage_template();
1628 solver.load_model(&template);
1629 solver.solve().expect("cold-start solve must succeed");
1630
1631 let mut basis = Basis::new(0, 0);
1632 solver.get_basis(&mut basis);
1633
1634 solver.load_model(&template);
1636 let result = solver
1637 .solve_with_basis(&basis)
1638 .expect("warm-start solve must succeed");
1639
1640 assert!(
1641 (result.objective - 100.0).abs() < 1e-8,
1642 "warm-start objective must be 100.0, got {}",
1643 result.objective
1644 );
1645 assert!(
1646 result.iterations <= 1,
1647 "warm-start from exact basis must use at most 1 iteration, got {}",
1648 result.iterations
1649 );
1650
1651 let stats = solver.statistics();
1652 assert_eq!(
1653 stats.basis_rejections, 0,
1654 "basis_rejections must be 0 when raw basis is accepted, got {}",
1655 stats.basis_rejections
1656 );
1657 }
1658
1659 #[test]
1663 fn test_solve_with_basis_dimension_mismatch() {
1664 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1665 let template = make_fixture_stage_template();
1666 let cuts = make_fixture_row_batch();
1667
1668 solver.load_model(&template);
1670 solver.solve().expect("SS1.1 solve must succeed");
1671 let mut basis = Basis::new(0, 0);
1672 solver.get_basis(&mut basis);
1673 assert_eq!(
1674 basis.row_status.len(),
1675 2,
1676 "captured basis must have 2 row statuses"
1677 );
1678
1679 solver.load_model(&template);
1681 solver.add_rows(&cuts);
1682 assert_eq!(solver.num_rows, 4, "LP must have 4 rows after add_rows");
1683
1684 let result = solver
1686 .solve_with_basis(&basis)
1687 .expect("solve with dimension-mismatched basis must succeed");
1688
1689 assert!(
1690 (result.objective - 162.0).abs() < 1e-8,
1691 "objective with both cuts active must be 162.0, got {}",
1692 result.objective
1693 );
1694 }
1695}
1696
1697#[cfg(test)]
1709#[allow(clippy::doc_markdown)]
1710mod research_tests_ticket_023 {
1711 unsafe fn research_load_ss11_lp(highs: *mut std::os::raw::c_void) {
1722 use crate::ffi;
1723 let col_cost: [f64; 3] = [0.0, 1.0, 50.0];
1724 let col_lower: [f64; 3] = [0.0, 0.0, 0.0];
1725 let col_upper: [f64; 3] = [10.0, f64::INFINITY, 8.0];
1726 let row_lower: [f64; 2] = [6.0, 14.0];
1727 let row_upper: [f64; 2] = [6.0, 14.0];
1728 let a_start: [i32; 4] = [0, 2, 2, 3];
1729 let a_index: [i32; 3] = [0, 1, 1];
1730 let a_value: [f64; 3] = [1.0, 2.0, 1.0];
1731 let status = unsafe {
1733 ffi::cobre_highs_pass_lp(
1734 highs,
1735 3,
1736 2,
1737 3,
1738 ffi::HIGHS_MATRIX_FORMAT_COLWISE,
1739 ffi::HIGHS_OBJ_SENSE_MINIMIZE,
1740 0.0,
1741 col_cost.as_ptr(),
1742 col_lower.as_ptr(),
1743 col_upper.as_ptr(),
1744 row_lower.as_ptr(),
1745 row_upper.as_ptr(),
1746 a_start.as_ptr(),
1747 a_index.as_ptr(),
1748 a_value.as_ptr(),
1749 )
1750 };
1751 assert_eq!(
1752 status,
1753 ffi::HIGHS_STATUS_OK,
1754 "research_load_ss11_lp pass_lp failed"
1755 );
1756 }
1757
1758 #[test]
1764 fn test_research_probe_limit_status_on_ss11_lp() {
1765 use crate::ffi;
1766
1767 let highs = unsafe { ffi::cobre_highs_create() };
1769 assert!(!highs.is_null());
1770 unsafe { ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0) };
1771 unsafe { research_load_ss11_lp(highs) };
1772 let _ = unsafe { ffi::cobre_highs_set_double_option(highs, c"time_limit".as_ptr(), 0.0) };
1773 let run_status = unsafe { ffi::cobre_highs_run(highs) };
1774 let model_status = unsafe { ffi::cobre_highs_get_model_status(highs) };
1775 let obj = unsafe { ffi::cobre_highs_get_objective_value(highs) };
1776 eprintln!(
1777 "SS1.1 + time_limit=0: run_status={run_status}, model_status={model_status}, obj={obj}"
1778 );
1779 unsafe { ffi::cobre_highs_destroy(highs) };
1780
1781 let highs = unsafe { ffi::cobre_highs_create() };
1783 assert!(!highs.is_null());
1784 unsafe { ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0) };
1785 unsafe { research_load_ss11_lp(highs) };
1786 let _ = unsafe {
1787 ffi::cobre_highs_set_int_option(highs, c"simplex_iteration_limit".as_ptr(), 0)
1788 };
1789 let run_status = unsafe { ffi::cobre_highs_run(highs) };
1790 let model_status = unsafe { ffi::cobre_highs_get_model_status(highs) };
1791 let obj = unsafe { ffi::cobre_highs_get_objective_value(highs) };
1792 eprintln!(
1793 "SS1.1 + iteration_limit=0: run_status={run_status}, model_status={model_status}, obj={obj}"
1794 );
1795 unsafe { ffi::cobre_highs_destroy(highs) };
1796 }
1797
1798 unsafe fn research_load_larger_lp(highs: *mut std::os::raw::c_void) {
1818 use crate::ffi;
1819 let col_cost: [f64; 5] = [1.0, 1.0, 1.0, 1.0, 1.0];
1820 let col_lower: [f64; 5] = [0.0; 5];
1821 let col_upper: [f64; 5] = [100.0; 5];
1822 let row_lower: [f64; 4] = [10.0, 8.0, 6.0, 4.0];
1823 let row_upper: [f64; 4] = [f64::INFINITY; 4];
1824 let a_start: [i32; 6] = [0, 1, 3, 5, 7, 8];
1826 let a_index: [i32; 8] = [0, 0, 1, 1, 2, 2, 3, 3];
1827 let a_value: [f64; 8] = [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0];
1828 let status = unsafe {
1830 ffi::cobre_highs_pass_lp(
1831 highs,
1832 5,
1833 4,
1834 8,
1835 ffi::HIGHS_MATRIX_FORMAT_COLWISE,
1836 ffi::HIGHS_OBJ_SENSE_MINIMIZE,
1837 0.0,
1838 col_cost.as_ptr(),
1839 col_lower.as_ptr(),
1840 col_upper.as_ptr(),
1841 row_lower.as_ptr(),
1842 row_upper.as_ptr(),
1843 a_start.as_ptr(),
1844 a_index.as_ptr(),
1845 a_value.as_ptr(),
1846 )
1847 };
1848 assert_eq!(
1849 status,
1850 ffi::HIGHS_STATUS_OK,
1851 "research_load_larger_lp pass_lp failed"
1852 );
1853 }
1854
1855 #[test]
1864 fn test_research_time_limit_zero_triggers_time_limit_status() {
1865 use crate::ffi;
1866
1867 let highs = unsafe { ffi::cobre_highs_create() };
1868 assert!(!highs.is_null());
1869 unsafe { ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0) };
1870 unsafe { research_load_larger_lp(highs) };
1871
1872 let opt_status =
1873 unsafe { ffi::cobre_highs_set_double_option(highs, c"time_limit".as_ptr(), 0.0) };
1874 assert_eq!(opt_status, ffi::HIGHS_STATUS_OK);
1875
1876 let run_status = unsafe { ffi::cobre_highs_run(highs) };
1877 let model_status = unsafe { ffi::cobre_highs_get_model_status(highs) };
1878
1879 eprintln!(
1880 "time_limit=0 on larger LP: run_status={run_status}, model_status={model_status}"
1881 );
1882
1883 assert_eq!(
1884 run_status,
1885 ffi::HIGHS_STATUS_WARNING,
1886 "time_limit=0 must return HIGHS_STATUS_WARNING (1), got {run_status}"
1887 );
1888 assert_eq!(
1889 model_status,
1890 ffi::HIGHS_MODEL_STATUS_TIME_LIMIT,
1891 "time_limit=0 must give MODEL_STATUS_TIME_LIMIT (13), got {model_status}"
1892 );
1893
1894 unsafe { ffi::cobre_highs_destroy(highs) };
1895 }
1896
1897 #[test]
1906 fn test_research_iteration_limit_zero_triggers_iteration_limit_status() {
1907 use crate::ffi;
1908
1909 let highs = unsafe { ffi::cobre_highs_create() };
1910 assert!(!highs.is_null());
1911 unsafe { ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0) };
1912 unsafe { ffi::cobre_highs_set_string_option(highs, c"presolve".as_ptr(), c"off".as_ptr()) };
1914 unsafe { research_load_larger_lp(highs) };
1915
1916 let opt_status = unsafe {
1917 ffi::cobre_highs_set_int_option(highs, c"simplex_iteration_limit".as_ptr(), 0)
1918 };
1919 assert_eq!(opt_status, ffi::HIGHS_STATUS_OK);
1920
1921 let run_status = unsafe { ffi::cobre_highs_run(highs) };
1922 let model_status = unsafe { ffi::cobre_highs_get_model_status(highs) };
1923
1924 eprintln!(
1925 "iteration_limit=0 on larger LP: run_status={run_status}, model_status={model_status}"
1926 );
1927
1928 assert_eq!(
1929 run_status,
1930 ffi::HIGHS_STATUS_WARNING,
1931 "iteration_limit=0 must return HIGHS_STATUS_WARNING (1), got {run_status}"
1932 );
1933 assert_eq!(
1934 model_status,
1935 ffi::HIGHS_MODEL_STATUS_ITERATION_LIMIT,
1936 "iteration_limit=0 must give MODEL_STATUS_ITERATION_LIMIT (14), got {model_status}"
1937 );
1938
1939 unsafe { ffi::cobre_highs_destroy(highs) };
1940 }
1941
1942 #[test]
1948 fn test_research_partial_solution_availability() {
1949 use crate::ffi;
1950
1951 {
1953 let highs = unsafe { ffi::cobre_highs_create() };
1954 assert!(!highs.is_null());
1955 unsafe { ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0) };
1956 unsafe { research_load_larger_lp(highs) };
1957 unsafe { ffi::cobre_highs_set_double_option(highs, c"time_limit".as_ptr(), 0.0) };
1958 unsafe { ffi::cobre_highs_run(highs) };
1959
1960 let obj = unsafe { ffi::cobre_highs_get_objective_value(highs) };
1961 let model_status = unsafe { ffi::cobre_highs_get_model_status(highs) };
1962 assert_eq!(model_status, ffi::HIGHS_MODEL_STATUS_TIME_LIMIT);
1963 eprintln!("TIME_LIMIT: obj={obj}, finite={}", obj.is_finite());
1964 unsafe { ffi::cobre_highs_destroy(highs) };
1965 }
1966
1967 {
1969 let highs = unsafe { ffi::cobre_highs_create() };
1970 assert!(!highs.is_null());
1971 unsafe { ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0) };
1972 unsafe {
1973 ffi::cobre_highs_set_string_option(highs, c"presolve".as_ptr(), c"off".as_ptr())
1974 };
1975 unsafe { research_load_larger_lp(highs) };
1976 unsafe {
1977 ffi::cobre_highs_set_int_option(highs, c"simplex_iteration_limit".as_ptr(), 0)
1978 };
1979 unsafe { ffi::cobre_highs_run(highs) };
1980
1981 let obj = unsafe { ffi::cobre_highs_get_objective_value(highs) };
1982 let model_status = unsafe { ffi::cobre_highs_get_model_status(highs) };
1983 assert_eq!(model_status, ffi::HIGHS_MODEL_STATUS_ITERATION_LIMIT);
1984 eprintln!("ITERATION_LIMIT: obj={obj}, finite={}", obj.is_finite());
1985 unsafe { ffi::cobre_highs_destroy(highs) };
1986 }
1987 }
1988
1989 #[test]
1992 fn test_research_restore_defaults_allows_subsequent_optimal_solve() {
1993 use crate::ffi;
1994
1995 let highs = unsafe { ffi::cobre_highs_create() };
1996 assert!(!highs.is_null());
1997
1998 unsafe { ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0) };
1999
2000 unsafe {
2002 ffi::cobre_highs_set_string_option(highs, c"solver".as_ptr(), c"simplex".as_ptr());
2003 ffi::cobre_highs_set_int_option(highs, c"simplex_strategy".as_ptr(), 1);
2004 ffi::cobre_highs_set_string_option(highs, c"presolve".as_ptr(), c"off".as_ptr());
2005 ffi::cobre_highs_set_string_option(highs, c"parallel".as_ptr(), c"off".as_ptr());
2006 ffi::cobre_highs_set_double_option(
2007 highs,
2008 c"primal_feasibility_tolerance".as_ptr(),
2009 1e-7,
2010 );
2011 ffi::cobre_highs_set_double_option(highs, c"dual_feasibility_tolerance".as_ptr(), 1e-7);
2012 }
2013
2014 let col_cost: [f64; 3] = [0.0, 1.0, 50.0];
2015 let col_lower: [f64; 3] = [0.0, 0.0, 0.0];
2016 let col_upper: [f64; 3] = [10.0, f64::INFINITY, 8.0];
2017 let row_lower: [f64; 2] = [6.0, 14.0];
2018 let row_upper: [f64; 2] = [6.0, 14.0];
2019 let a_start: [i32; 4] = [0, 2, 2, 3];
2020 let a_index: [i32; 3] = [0, 1, 1];
2021 let a_value: [f64; 3] = [1.0, 2.0, 1.0];
2022
2023 unsafe {
2025 ffi::cobre_highs_pass_lp(
2026 highs,
2027 3,
2028 2,
2029 3,
2030 ffi::HIGHS_MATRIX_FORMAT_COLWISE,
2031 ffi::HIGHS_OBJ_SENSE_MINIMIZE,
2032 0.0,
2033 col_cost.as_ptr(),
2034 col_lower.as_ptr(),
2035 col_upper.as_ptr(),
2036 row_lower.as_ptr(),
2037 row_upper.as_ptr(),
2038 a_start.as_ptr(),
2039 a_index.as_ptr(),
2040 a_value.as_ptr(),
2041 );
2042 ffi::cobre_highs_set_int_option(highs, c"simplex_iteration_limit".as_ptr(), 0);
2043 ffi::cobre_highs_run(highs);
2044 }
2045 let status1 = unsafe { ffi::cobre_highs_get_model_status(highs) };
2046 assert_eq!(status1, ffi::HIGHS_MODEL_STATUS_ITERATION_LIMIT);
2047
2048 unsafe {
2050 ffi::cobre_highs_set_string_option(highs, c"solver".as_ptr(), c"simplex".as_ptr());
2051 ffi::cobre_highs_set_int_option(highs, c"simplex_strategy".as_ptr(), 1);
2052 ffi::cobre_highs_set_string_option(highs, c"presolve".as_ptr(), c"off".as_ptr());
2053 ffi::cobre_highs_set_double_option(
2054 highs,
2055 c"primal_feasibility_tolerance".as_ptr(),
2056 1e-7,
2057 );
2058 ffi::cobre_highs_set_double_option(highs, c"dual_feasibility_tolerance".as_ptr(), 1e-7);
2059 ffi::cobre_highs_set_string_option(highs, c"parallel".as_ptr(), c"off".as_ptr());
2060 ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0);
2061 ffi::cobre_highs_set_int_option(highs, c"simplex_iteration_limit".as_ptr(), i32::MAX);
2063 }
2064
2065 unsafe { ffi::cobre_highs_clear_solver(highs) };
2067 unsafe { ffi::cobre_highs_run(highs) };
2068 let status2 = unsafe { ffi::cobre_highs_get_model_status(highs) };
2069 let obj = unsafe { ffi::cobre_highs_get_objective_value(highs) };
2070 assert_eq!(
2071 status2,
2072 ffi::HIGHS_MODEL_STATUS_OPTIMAL,
2073 "after restoring defaults, second solve must be OPTIMAL, got {status2}"
2074 );
2075 assert!(
2076 (obj - 100.0).abs() < 1e-8,
2077 "objective after restore must be 100.0, got {obj}"
2078 );
2079
2080 unsafe { ffi::cobre_highs_destroy(highs) };
2081 }
2082
2083 #[test]
2088 fn test_research_iteration_limit_one_triggers_iteration_limit_status() {
2089 use crate::ffi;
2090
2091 let highs = unsafe { ffi::cobre_highs_create() };
2092 assert!(!highs.is_null());
2093
2094 unsafe { ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0) };
2095
2096 let col_cost: [f64; 3] = [0.0, 1.0, 50.0];
2097 let col_lower: [f64; 3] = [0.0, 0.0, 0.0];
2098 let col_upper: [f64; 3] = [10.0, f64::INFINITY, 8.0];
2099 let row_lower: [f64; 2] = [6.0, 14.0];
2100 let row_upper: [f64; 2] = [6.0, 14.0];
2101 let a_start: [i32; 4] = [0, 2, 2, 3];
2102 let a_index: [i32; 3] = [0, 1, 1];
2103 let a_value: [f64; 3] = [1.0, 2.0, 1.0];
2104
2105 unsafe {
2106 ffi::cobre_highs_pass_lp(
2107 highs,
2108 3,
2109 2,
2110 3,
2111 ffi::HIGHS_MATRIX_FORMAT_COLWISE,
2112 ffi::HIGHS_OBJ_SENSE_MINIMIZE,
2113 0.0,
2114 col_cost.as_ptr(),
2115 col_lower.as_ptr(),
2116 col_upper.as_ptr(),
2117 row_lower.as_ptr(),
2118 row_upper.as_ptr(),
2119 a_start.as_ptr(),
2120 a_index.as_ptr(),
2121 a_value.as_ptr(),
2122 );
2123 ffi::cobre_highs_set_int_option(highs, c"simplex_iteration_limit".as_ptr(), 1);
2124 ffi::cobre_highs_run(highs);
2125 }
2126
2127 let model_status = unsafe { ffi::cobre_highs_get_model_status(highs) };
2128 eprintln!("iteration_limit=1 model_status: {model_status}");
2129 assert!(
2132 model_status == ffi::HIGHS_MODEL_STATUS_ITERATION_LIMIT
2133 || model_status == ffi::HIGHS_MODEL_STATUS_OPTIMAL,
2134 "expected ITERATION_LIMIT or OPTIMAL, got {model_status}"
2135 );
2136
2137 unsafe { ffi::cobre_highs_destroy(highs) };
2138 }
2139}