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(4), },
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 set_status = unsafe {
1151 ffi::cobre_highs_set_basis(
1152 self.handle,
1153 self.basis_col_i32.as_ptr(),
1154 self.basis_row_i32.as_ptr(),
1155 )
1156 };
1157
1158 if set_status == ffi::HIGHS_STATUS_ERROR {
1160 self.stats.basis_rejections += 1;
1161 debug_assert!(false, "raw basis rejected; falling back to cold-start");
1162 }
1163
1164 self.solve()
1166 }
1167
1168 fn statistics(&self) -> SolverStatistics {
1169 self.stats.clone()
1170 }
1171}
1172
1173#[cfg(feature = "test-support")]
1179impl HighsSolver {
1180 #[must_use]
1188 pub fn raw_handle(&self) -> *mut std::os::raw::c_void {
1189 self.handle
1190 }
1191}
1192
1193#[cfg(test)]
1194mod tests {
1195 use super::HighsSolver;
1196 use crate::{
1197 SolverInterface,
1198 types::{Basis, RowBatch, StageTemplate},
1199 };
1200
1201 fn make_fixture_stage_template() -> StageTemplate {
1214 StageTemplate {
1215 num_cols: 3,
1216 num_rows: 2,
1217 num_nz: 3,
1218 col_starts: vec![0_i32, 2, 2, 3],
1219 row_indices: vec![0_i32, 1, 1],
1220 values: vec![1.0, 2.0, 1.0],
1221 col_lower: vec![0.0, 0.0, 0.0],
1222 col_upper: vec![10.0, f64::INFINITY, 8.0],
1223 objective: vec![0.0, 1.0, 50.0],
1224 row_lower: vec![6.0, 14.0],
1225 row_upper: vec![6.0, 14.0],
1226 n_state: 1,
1227 n_transfer: 0,
1228 n_dual_relevant: 1,
1229 n_hydro: 1,
1230 max_par_order: 0,
1231 col_scale: Vec::new(),
1232 row_scale: Vec::new(),
1233 }
1234 }
1235
1236 fn make_fixture_row_batch() -> RowBatch {
1240 RowBatch {
1241 num_rows: 2,
1242 row_starts: vec![0_i32, 2, 4],
1243 col_indices: vec![0_i32, 1, 0, 1],
1244 values: vec![-5.0, 1.0, 3.0, 1.0],
1245 row_lower: vec![20.0, 80.0],
1246 row_upper: vec![f64::INFINITY, f64::INFINITY],
1247 }
1248 }
1249
1250 #[test]
1251 fn test_highs_solver_create_and_name() {
1252 let solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1253 assert_eq!(solver.name(), "HiGHS");
1254 }
1256
1257 #[test]
1258 fn test_highs_solver_send_bound() {
1259 fn assert_send<T: Send>() {}
1260 assert_send::<HighsSolver>();
1261 }
1262
1263 #[test]
1264 fn test_highs_solver_statistics_initial() {
1265 let solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1266 let stats = solver.statistics();
1267 assert_eq!(stats.solve_count, 0);
1268 assert_eq!(stats.success_count, 0);
1269 assert_eq!(stats.failure_count, 0);
1270 assert_eq!(stats.total_iterations, 0);
1271 assert_eq!(stats.retry_count, 0);
1272 assert_eq!(stats.total_solve_time_seconds, 0.0);
1273 }
1274
1275 #[test]
1276 fn test_highs_load_model_updates_dimensions() {
1277 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1278 let template = make_fixture_stage_template();
1279
1280 solver.load_model(&template);
1281
1282 assert_eq!(solver.num_cols, 3, "num_cols must be 3 after load_model");
1283 assert_eq!(solver.num_rows, 2, "num_rows must be 2 after load_model");
1284 assert_eq!(
1285 solver.col_value.len(),
1286 3,
1287 "col_value buffer must be resized to num_cols"
1288 );
1289 assert_eq!(
1290 solver.col_dual.len(),
1291 3,
1292 "col_dual buffer must be resized to num_cols"
1293 );
1294 assert_eq!(
1295 solver.row_value.len(),
1296 2,
1297 "row_value buffer must be resized to num_rows"
1298 );
1299 assert_eq!(
1300 solver.row_dual.len(),
1301 2,
1302 "row_dual buffer must be resized to num_rows"
1303 );
1304 }
1305
1306 #[test]
1307 fn test_highs_add_rows_updates_dimensions() {
1308 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1309 let template = make_fixture_stage_template();
1310 let cuts = make_fixture_row_batch();
1311
1312 solver.load_model(&template);
1313 solver.add_rows(&cuts);
1314
1315 assert_eq!(solver.num_rows, 4, "num_rows must be 4 after add_rows");
1317 assert_eq!(
1318 solver.row_dual.len(),
1319 4,
1320 "row_dual buffer must be resized to 4 after add_rows"
1321 );
1322 assert_eq!(
1323 solver.row_value.len(),
1324 4,
1325 "row_value buffer must be resized to 4 after add_rows"
1326 );
1327 assert_eq!(solver.num_cols, 3, "num_cols must be unchanged by add_rows");
1329 }
1330
1331 #[test]
1332 fn test_highs_set_row_bounds_no_panic() {
1333 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1334 let template = make_fixture_stage_template();
1335 solver.load_model(&template);
1336
1337 solver.set_row_bounds(&[0], &[4.0], &[4.0]);
1339 }
1340
1341 #[test]
1342 fn test_highs_set_col_bounds_no_panic() {
1343 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1344 let template = make_fixture_stage_template();
1345 solver.load_model(&template);
1346
1347 solver.set_col_bounds(&[1], &[10.0], &[f64::INFINITY]);
1349 }
1350
1351 #[test]
1352 fn test_highs_set_bounds_empty_no_panic() {
1353 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1354 let template = make_fixture_stage_template();
1355 solver.load_model(&template);
1356
1357 solver.set_row_bounds(&[], &[], &[]);
1359 solver.set_col_bounds(&[], &[], &[]);
1360 }
1361
1362 #[test]
1365 fn test_highs_solve_basic_lp() {
1366 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1367 let template = make_fixture_stage_template();
1368 solver.load_model(&template);
1369
1370 let solution = solver
1371 .solve()
1372 .expect("solve() must succeed on a feasible LP");
1373
1374 assert!(
1375 (solution.objective - 100.0).abs() < 1e-8,
1376 "objective must be 100.0, got {}",
1377 solution.objective
1378 );
1379 assert_eq!(solution.primal.len(), 3, "primal must have 3 elements");
1380 assert!(
1381 (solution.primal[0] - 6.0).abs() < 1e-8,
1382 "primal[0] (x0) must be 6.0, got {}",
1383 solution.primal[0]
1384 );
1385 assert!(
1386 (solution.primal[1] - 0.0).abs() < 1e-8,
1387 "primal[1] (x1) must be 0.0, got {}",
1388 solution.primal[1]
1389 );
1390 assert!(
1391 (solution.primal[2] - 2.0).abs() < 1e-8,
1392 "primal[2] (x2) must be 2.0, got {}",
1393 solution.primal[2]
1394 );
1395 }
1396
1397 #[test]
1401 fn test_highs_solve_with_cuts() {
1402 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1403 let template = make_fixture_stage_template();
1404 let cuts = make_fixture_row_batch();
1405 solver.load_model(&template);
1406 solver.add_rows(&cuts);
1407
1408 let solution = solver
1409 .solve()
1410 .expect("solve() must succeed on a feasible LP with cuts");
1411
1412 assert!(
1413 (solution.objective - 162.0).abs() < 1e-8,
1414 "objective must be 162.0, got {}",
1415 solution.objective
1416 );
1417 assert!(
1418 (solution.primal[0] - 6.0).abs() < 1e-8,
1419 "primal[0] must be 6.0, got {}",
1420 solution.primal[0]
1421 );
1422 assert!(
1423 (solution.primal[1] - 62.0).abs() < 1e-8,
1424 "primal[1] must be 62.0, got {}",
1425 solution.primal[1]
1426 );
1427 assert!(
1428 (solution.primal[2] - 2.0).abs() < 1e-8,
1429 "primal[2] must be 2.0, got {}",
1430 solution.primal[2]
1431 );
1432 }
1433
1434 #[test]
1437 fn test_highs_solve_after_rhs_patch() {
1438 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1439 let template = make_fixture_stage_template();
1440 let cuts = make_fixture_row_batch();
1441 solver.load_model(&template);
1442 solver.add_rows(&cuts);
1443
1444 solver.set_row_bounds(&[0], &[4.0], &[4.0]);
1446
1447 let solution = solver
1448 .solve()
1449 .expect("solve() must succeed after RHS patch");
1450
1451 assert!(
1452 (solution.objective - 368.0).abs() < 1e-8,
1453 "objective must be 368.0, got {}",
1454 solution.objective
1455 );
1456 }
1457
1458 #[test]
1460 fn test_highs_solve_statistics_increment() {
1461 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1462 let template = make_fixture_stage_template();
1463 solver.load_model(&template);
1464
1465 solver.solve().expect("first solve must succeed");
1466 solver.solve().expect("second solve must succeed");
1467
1468 let stats = solver.statistics();
1469 assert_eq!(stats.solve_count, 2, "solve_count must be 2");
1470 assert_eq!(stats.success_count, 2, "success_count must be 2");
1471 assert_eq!(stats.failure_count, 0, "failure_count must be 0");
1472 assert!(
1473 stats.total_iterations > 0,
1474 "total_iterations must be positive"
1475 );
1476 }
1477
1478 #[test]
1480 fn test_highs_reset_preserves_stats() {
1481 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1482 let template = make_fixture_stage_template();
1483 solver.load_model(&template);
1484 solver.solve().expect("solve must succeed");
1485
1486 let stats_before = solver.statistics();
1487 assert_eq!(
1488 stats_before.solve_count, 1,
1489 "solve_count must be 1 before reset"
1490 );
1491
1492 solver.reset();
1493
1494 let stats_after = solver.statistics();
1495 assert_eq!(
1496 stats_after.solve_count, stats_before.solve_count,
1497 "solve_count must be unchanged after reset"
1498 );
1499 assert_eq!(
1500 stats_after.success_count, stats_before.success_count,
1501 "success_count must be unchanged after reset"
1502 );
1503 assert_eq!(
1504 stats_after.total_iterations, stats_before.total_iterations,
1505 "total_iterations must be unchanged after reset"
1506 );
1507 }
1508
1509 #[test]
1511 fn test_highs_solve_iterations_positive() {
1512 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1513 let template = make_fixture_stage_template();
1514 solver.load_model(&template);
1515
1516 let solution = solver.solve().expect("solve must succeed");
1517 assert!(
1518 solution.iterations > 0,
1519 "iterations must be positive, got {}",
1520 solution.iterations
1521 );
1522 }
1523
1524 #[test]
1526 fn test_highs_solve_time_positive() {
1527 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1528 let template = make_fixture_stage_template();
1529 solver.load_model(&template);
1530
1531 let solution = solver.solve().expect("solve must succeed");
1532 assert!(
1533 solution.solve_time_seconds > 0.0,
1534 "solve_time_seconds must be positive, got {}",
1535 solution.solve_time_seconds
1536 );
1537 }
1538
1539 #[test]
1542 fn test_highs_solve_statistics_single() {
1543 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1544 let template = make_fixture_stage_template();
1545 solver.load_model(&template);
1546
1547 solver.solve().expect("solve must succeed");
1548
1549 let stats = solver.statistics();
1550 assert_eq!(stats.solve_count, 1, "solve_count must be 1");
1551 assert_eq!(stats.success_count, 1, "success_count must be 1");
1552 assert_eq!(stats.failure_count, 0, "failure_count must be 0");
1553 assert!(
1554 stats.total_iterations > 0,
1555 "total_iterations must be positive after a successful solve"
1556 );
1557 }
1558
1559 #[test]
1562 fn test_get_basis_valid_status_codes() {
1563 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1564 let template = make_fixture_stage_template();
1565 solver.load_model(&template);
1566 solver.solve().expect("solve must succeed before get_basis");
1567
1568 let mut basis = Basis::new(0, 0);
1569 solver.get_basis(&mut basis);
1570
1571 for &code in &basis.col_status {
1572 assert!(
1573 (0..=4).contains(&code),
1574 "col_status code {code} is outside valid HiGHS range 0..=4"
1575 );
1576 }
1577 for &code in &basis.row_status {
1578 assert!(
1579 (0..=4).contains(&code),
1580 "row_status code {code} is outside valid HiGHS range 0..=4"
1581 );
1582 }
1583 }
1584
1585 #[test]
1588 fn test_get_basis_resizes_output() {
1589 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1590 let template = make_fixture_stage_template();
1591 solver.load_model(&template);
1592 solver.solve().expect("solve must succeed before get_basis");
1593
1594 let mut basis = Basis::new(0, 0);
1595 assert_eq!(
1596 basis.col_status.len(),
1597 0,
1598 "initial col_status must be empty"
1599 );
1600 assert_eq!(
1601 basis.row_status.len(),
1602 0,
1603 "initial row_status must be empty"
1604 );
1605
1606 solver.get_basis(&mut basis);
1607
1608 assert_eq!(
1609 basis.col_status.len(),
1610 3,
1611 "col_status must be resized to 3 (num_cols of SS1.1)"
1612 );
1613 assert_eq!(
1614 basis.row_status.len(),
1615 2,
1616 "row_status must be resized to 2 (num_rows of SS1.1)"
1617 );
1618 }
1619
1620 #[test]
1623 fn test_solve_with_basis_warm_start() {
1624 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1625 let template = make_fixture_stage_template();
1626 solver.load_model(&template);
1627 solver.solve().expect("cold-start solve must succeed");
1628
1629 let mut basis = Basis::new(0, 0);
1630 solver.get_basis(&mut basis);
1631
1632 solver.load_model(&template);
1634 let result = solver
1635 .solve_with_basis(&basis)
1636 .expect("warm-start solve must succeed");
1637
1638 assert!(
1639 (result.objective - 100.0).abs() < 1e-8,
1640 "warm-start objective must be 100.0, got {}",
1641 result.objective
1642 );
1643 assert!(
1644 result.iterations <= 1,
1645 "warm-start from exact basis must use at most 1 iteration, got {}",
1646 result.iterations
1647 );
1648
1649 let stats = solver.statistics();
1650 assert_eq!(
1651 stats.basis_rejections, 0,
1652 "basis_rejections must be 0 when raw basis is accepted, got {}",
1653 stats.basis_rejections
1654 );
1655 }
1656
1657 #[test]
1661 fn test_solve_with_basis_dimension_mismatch() {
1662 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1663 let template = make_fixture_stage_template();
1664 let cuts = make_fixture_row_batch();
1665
1666 solver.load_model(&template);
1668 solver.solve().expect("SS1.1 solve must succeed");
1669 let mut basis = Basis::new(0, 0);
1670 solver.get_basis(&mut basis);
1671 assert_eq!(
1672 basis.row_status.len(),
1673 2,
1674 "captured basis must have 2 row statuses"
1675 );
1676
1677 solver.load_model(&template);
1679 solver.add_rows(&cuts);
1680 assert_eq!(solver.num_rows, 4, "LP must have 4 rows after add_rows");
1681
1682 let result = solver
1684 .solve_with_basis(&basis)
1685 .expect("solve with dimension-mismatched basis must succeed");
1686
1687 assert!(
1688 (result.objective - 162.0).abs() < 1e-8,
1689 "objective with both cuts active must be 162.0, got {}",
1690 result.objective
1691 );
1692 }
1693}
1694
1695#[cfg(test)]
1707#[allow(clippy::doc_markdown)]
1708mod research_tests_ticket_023 {
1709 unsafe fn research_load_ss11_lp(highs: *mut std::os::raw::c_void) {
1720 use crate::ffi;
1721 let col_cost: [f64; 3] = [0.0, 1.0, 50.0];
1722 let col_lower: [f64; 3] = [0.0, 0.0, 0.0];
1723 let col_upper: [f64; 3] = [10.0, f64::INFINITY, 8.0];
1724 let row_lower: [f64; 2] = [6.0, 14.0];
1725 let row_upper: [f64; 2] = [6.0, 14.0];
1726 let a_start: [i32; 4] = [0, 2, 2, 3];
1727 let a_index: [i32; 3] = [0, 1, 1];
1728 let a_value: [f64; 3] = [1.0, 2.0, 1.0];
1729 let status = unsafe {
1731 ffi::cobre_highs_pass_lp(
1732 highs,
1733 3,
1734 2,
1735 3,
1736 ffi::HIGHS_MATRIX_FORMAT_COLWISE,
1737 ffi::HIGHS_OBJ_SENSE_MINIMIZE,
1738 0.0,
1739 col_cost.as_ptr(),
1740 col_lower.as_ptr(),
1741 col_upper.as_ptr(),
1742 row_lower.as_ptr(),
1743 row_upper.as_ptr(),
1744 a_start.as_ptr(),
1745 a_index.as_ptr(),
1746 a_value.as_ptr(),
1747 )
1748 };
1749 assert_eq!(
1750 status,
1751 ffi::HIGHS_STATUS_OK,
1752 "research_load_ss11_lp pass_lp failed"
1753 );
1754 }
1755
1756 #[test]
1762 fn test_research_probe_limit_status_on_ss11_lp() {
1763 use crate::ffi;
1764
1765 let highs = unsafe { ffi::cobre_highs_create() };
1767 assert!(!highs.is_null());
1768 unsafe { ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0) };
1769 unsafe { research_load_ss11_lp(highs) };
1770 let _ = unsafe { ffi::cobre_highs_set_double_option(highs, c"time_limit".as_ptr(), 0.0) };
1771 let run_status = unsafe { ffi::cobre_highs_run(highs) };
1772 let model_status = unsafe { ffi::cobre_highs_get_model_status(highs) };
1773 let obj = unsafe { ffi::cobre_highs_get_objective_value(highs) };
1774 eprintln!(
1775 "SS1.1 + time_limit=0: run_status={run_status}, model_status={model_status}, obj={obj}"
1776 );
1777 unsafe { ffi::cobre_highs_destroy(highs) };
1778
1779 let highs = unsafe { ffi::cobre_highs_create() };
1781 assert!(!highs.is_null());
1782 unsafe { ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0) };
1783 unsafe { research_load_ss11_lp(highs) };
1784 let _ = unsafe {
1785 ffi::cobre_highs_set_int_option(highs, c"simplex_iteration_limit".as_ptr(), 0)
1786 };
1787 let run_status = unsafe { ffi::cobre_highs_run(highs) };
1788 let model_status = unsafe { ffi::cobre_highs_get_model_status(highs) };
1789 let obj = unsafe { ffi::cobre_highs_get_objective_value(highs) };
1790 eprintln!(
1791 "SS1.1 + iteration_limit=0: run_status={run_status}, model_status={model_status}, obj={obj}"
1792 );
1793 unsafe { ffi::cobre_highs_destroy(highs) };
1794 }
1795
1796 unsafe fn research_load_larger_lp(highs: *mut std::os::raw::c_void) {
1816 use crate::ffi;
1817 let col_cost: [f64; 5] = [1.0, 1.0, 1.0, 1.0, 1.0];
1818 let col_lower: [f64; 5] = [0.0; 5];
1819 let col_upper: [f64; 5] = [100.0; 5];
1820 let row_lower: [f64; 4] = [10.0, 8.0, 6.0, 4.0];
1821 let row_upper: [f64; 4] = [f64::INFINITY; 4];
1822 let a_start: [i32; 6] = [0, 1, 3, 5, 7, 8];
1824 let a_index: [i32; 8] = [0, 0, 1, 1, 2, 2, 3, 3];
1825 let a_value: [f64; 8] = [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0];
1826 let status = unsafe {
1828 ffi::cobre_highs_pass_lp(
1829 highs,
1830 5,
1831 4,
1832 8,
1833 ffi::HIGHS_MATRIX_FORMAT_COLWISE,
1834 ffi::HIGHS_OBJ_SENSE_MINIMIZE,
1835 0.0,
1836 col_cost.as_ptr(),
1837 col_lower.as_ptr(),
1838 col_upper.as_ptr(),
1839 row_lower.as_ptr(),
1840 row_upper.as_ptr(),
1841 a_start.as_ptr(),
1842 a_index.as_ptr(),
1843 a_value.as_ptr(),
1844 )
1845 };
1846 assert_eq!(
1847 status,
1848 ffi::HIGHS_STATUS_OK,
1849 "research_load_larger_lp pass_lp failed"
1850 );
1851 }
1852
1853 #[test]
1862 fn test_research_time_limit_zero_triggers_time_limit_status() {
1863 use crate::ffi;
1864
1865 let highs = unsafe { ffi::cobre_highs_create() };
1866 assert!(!highs.is_null());
1867 unsafe { ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0) };
1868 unsafe { research_load_larger_lp(highs) };
1869
1870 let opt_status =
1871 unsafe { ffi::cobre_highs_set_double_option(highs, c"time_limit".as_ptr(), 0.0) };
1872 assert_eq!(opt_status, ffi::HIGHS_STATUS_OK);
1873
1874 let run_status = unsafe { ffi::cobre_highs_run(highs) };
1875 let model_status = unsafe { ffi::cobre_highs_get_model_status(highs) };
1876
1877 eprintln!(
1878 "time_limit=0 on larger LP: run_status={run_status}, model_status={model_status}"
1879 );
1880
1881 assert_eq!(
1882 run_status,
1883 ffi::HIGHS_STATUS_WARNING,
1884 "time_limit=0 must return HIGHS_STATUS_WARNING (1), got {run_status}"
1885 );
1886 assert_eq!(
1887 model_status,
1888 ffi::HIGHS_MODEL_STATUS_TIME_LIMIT,
1889 "time_limit=0 must give MODEL_STATUS_TIME_LIMIT (13), got {model_status}"
1890 );
1891
1892 unsafe { ffi::cobre_highs_destroy(highs) };
1893 }
1894
1895 #[test]
1904 fn test_research_iteration_limit_zero_triggers_iteration_limit_status() {
1905 use crate::ffi;
1906
1907 let highs = unsafe { ffi::cobre_highs_create() };
1908 assert!(!highs.is_null());
1909 unsafe { ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0) };
1910 unsafe { ffi::cobre_highs_set_string_option(highs, c"presolve".as_ptr(), c"off".as_ptr()) };
1912 unsafe { research_load_larger_lp(highs) };
1913
1914 let opt_status = unsafe {
1915 ffi::cobre_highs_set_int_option(highs, c"simplex_iteration_limit".as_ptr(), 0)
1916 };
1917 assert_eq!(opt_status, ffi::HIGHS_STATUS_OK);
1918
1919 let run_status = unsafe { ffi::cobre_highs_run(highs) };
1920 let model_status = unsafe { ffi::cobre_highs_get_model_status(highs) };
1921
1922 eprintln!(
1923 "iteration_limit=0 on larger LP: run_status={run_status}, model_status={model_status}"
1924 );
1925
1926 assert_eq!(
1927 run_status,
1928 ffi::HIGHS_STATUS_WARNING,
1929 "iteration_limit=0 must return HIGHS_STATUS_WARNING (1), got {run_status}"
1930 );
1931 assert_eq!(
1932 model_status,
1933 ffi::HIGHS_MODEL_STATUS_ITERATION_LIMIT,
1934 "iteration_limit=0 must give MODEL_STATUS_ITERATION_LIMIT (14), got {model_status}"
1935 );
1936
1937 unsafe { ffi::cobre_highs_destroy(highs) };
1938 }
1939
1940 #[test]
1946 fn test_research_partial_solution_availability() {
1947 use crate::ffi;
1948
1949 {
1951 let highs = unsafe { ffi::cobre_highs_create() };
1952 assert!(!highs.is_null());
1953 unsafe { ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0) };
1954 unsafe { research_load_larger_lp(highs) };
1955 unsafe { ffi::cobre_highs_set_double_option(highs, c"time_limit".as_ptr(), 0.0) };
1956 unsafe { ffi::cobre_highs_run(highs) };
1957
1958 let obj = unsafe { ffi::cobre_highs_get_objective_value(highs) };
1959 let model_status = unsafe { ffi::cobre_highs_get_model_status(highs) };
1960 assert_eq!(model_status, ffi::HIGHS_MODEL_STATUS_TIME_LIMIT);
1961 eprintln!("TIME_LIMIT: obj={obj}, finite={}", obj.is_finite());
1962 unsafe { ffi::cobre_highs_destroy(highs) };
1963 }
1964
1965 {
1967 let highs = unsafe { ffi::cobre_highs_create() };
1968 assert!(!highs.is_null());
1969 unsafe { ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0) };
1970 unsafe {
1971 ffi::cobre_highs_set_string_option(highs, c"presolve".as_ptr(), c"off".as_ptr())
1972 };
1973 unsafe { research_load_larger_lp(highs) };
1974 unsafe {
1975 ffi::cobre_highs_set_int_option(highs, c"simplex_iteration_limit".as_ptr(), 0)
1976 };
1977 unsafe { ffi::cobre_highs_run(highs) };
1978
1979 let obj = unsafe { ffi::cobre_highs_get_objective_value(highs) };
1980 let model_status = unsafe { ffi::cobre_highs_get_model_status(highs) };
1981 assert_eq!(model_status, ffi::HIGHS_MODEL_STATUS_ITERATION_LIMIT);
1982 eprintln!("ITERATION_LIMIT: obj={obj}, finite={}", obj.is_finite());
1983 unsafe { ffi::cobre_highs_destroy(highs) };
1984 }
1985 }
1986
1987 #[test]
1990 fn test_research_restore_defaults_allows_subsequent_optimal_solve() {
1991 use crate::ffi;
1992
1993 let highs = unsafe { ffi::cobre_highs_create() };
1994 assert!(!highs.is_null());
1995
1996 unsafe { ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0) };
1997
1998 unsafe {
2000 ffi::cobre_highs_set_string_option(highs, c"solver".as_ptr(), c"simplex".as_ptr());
2001 ffi::cobre_highs_set_int_option(highs, c"simplex_strategy".as_ptr(), 4);
2002 ffi::cobre_highs_set_string_option(highs, c"presolve".as_ptr(), c"off".as_ptr());
2003 ffi::cobre_highs_set_string_option(highs, c"parallel".as_ptr(), c"off".as_ptr());
2004 ffi::cobre_highs_set_double_option(
2005 highs,
2006 c"primal_feasibility_tolerance".as_ptr(),
2007 1e-7,
2008 );
2009 ffi::cobre_highs_set_double_option(highs, c"dual_feasibility_tolerance".as_ptr(), 1e-7);
2010 }
2011
2012 let col_cost: [f64; 3] = [0.0, 1.0, 50.0];
2013 let col_lower: [f64; 3] = [0.0, 0.0, 0.0];
2014 let col_upper: [f64; 3] = [10.0, f64::INFINITY, 8.0];
2015 let row_lower: [f64; 2] = [6.0, 14.0];
2016 let row_upper: [f64; 2] = [6.0, 14.0];
2017 let a_start: [i32; 4] = [0, 2, 2, 3];
2018 let a_index: [i32; 3] = [0, 1, 1];
2019 let a_value: [f64; 3] = [1.0, 2.0, 1.0];
2020
2021 unsafe {
2023 ffi::cobre_highs_pass_lp(
2024 highs,
2025 3,
2026 2,
2027 3,
2028 ffi::HIGHS_MATRIX_FORMAT_COLWISE,
2029 ffi::HIGHS_OBJ_SENSE_MINIMIZE,
2030 0.0,
2031 col_cost.as_ptr(),
2032 col_lower.as_ptr(),
2033 col_upper.as_ptr(),
2034 row_lower.as_ptr(),
2035 row_upper.as_ptr(),
2036 a_start.as_ptr(),
2037 a_index.as_ptr(),
2038 a_value.as_ptr(),
2039 );
2040 ffi::cobre_highs_set_int_option(highs, c"simplex_iteration_limit".as_ptr(), 0);
2041 ffi::cobre_highs_run(highs);
2042 }
2043 let status1 = unsafe { ffi::cobre_highs_get_model_status(highs) };
2044 assert_eq!(status1, ffi::HIGHS_MODEL_STATUS_ITERATION_LIMIT);
2045
2046 unsafe {
2048 ffi::cobre_highs_set_string_option(highs, c"solver".as_ptr(), c"simplex".as_ptr());
2049 ffi::cobre_highs_set_int_option(highs, c"simplex_strategy".as_ptr(), 4);
2050 ffi::cobre_highs_set_string_option(highs, c"presolve".as_ptr(), c"off".as_ptr());
2051 ffi::cobre_highs_set_double_option(
2052 highs,
2053 c"primal_feasibility_tolerance".as_ptr(),
2054 1e-7,
2055 );
2056 ffi::cobre_highs_set_double_option(highs, c"dual_feasibility_tolerance".as_ptr(), 1e-7);
2057 ffi::cobre_highs_set_string_option(highs, c"parallel".as_ptr(), c"off".as_ptr());
2058 ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0);
2059 ffi::cobre_highs_set_int_option(highs, c"simplex_iteration_limit".as_ptr(), i32::MAX);
2061 }
2062
2063 unsafe { ffi::cobre_highs_clear_solver(highs) };
2065 unsafe { ffi::cobre_highs_run(highs) };
2066 let status2 = unsafe { ffi::cobre_highs_get_model_status(highs) };
2067 let obj = unsafe { ffi::cobre_highs_get_objective_value(highs) };
2068 assert_eq!(
2069 status2,
2070 ffi::HIGHS_MODEL_STATUS_OPTIMAL,
2071 "after restoring defaults, second solve must be OPTIMAL, got {status2}"
2072 );
2073 assert!(
2074 (obj - 100.0).abs() < 1e-8,
2075 "objective after restore must be 100.0, got {obj}"
2076 );
2077
2078 unsafe { ffi::cobre_highs_destroy(highs) };
2079 }
2080
2081 #[test]
2086 fn test_research_iteration_limit_one_triggers_iteration_limit_status() {
2087 use crate::ffi;
2088
2089 let highs = unsafe { ffi::cobre_highs_create() };
2090 assert!(!highs.is_null());
2091
2092 unsafe { ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0) };
2093
2094 let col_cost: [f64; 3] = [0.0, 1.0, 50.0];
2095 let col_lower: [f64; 3] = [0.0, 0.0, 0.0];
2096 let col_upper: [f64; 3] = [10.0, f64::INFINITY, 8.0];
2097 let row_lower: [f64; 2] = [6.0, 14.0];
2098 let row_upper: [f64; 2] = [6.0, 14.0];
2099 let a_start: [i32; 4] = [0, 2, 2, 3];
2100 let a_index: [i32; 3] = [0, 1, 1];
2101 let a_value: [f64; 3] = [1.0, 2.0, 1.0];
2102
2103 unsafe {
2104 ffi::cobre_highs_pass_lp(
2105 highs,
2106 3,
2107 2,
2108 3,
2109 ffi::HIGHS_MATRIX_FORMAT_COLWISE,
2110 ffi::HIGHS_OBJ_SENSE_MINIMIZE,
2111 0.0,
2112 col_cost.as_ptr(),
2113 col_lower.as_ptr(),
2114 col_upper.as_ptr(),
2115 row_lower.as_ptr(),
2116 row_upper.as_ptr(),
2117 a_start.as_ptr(),
2118 a_index.as_ptr(),
2119 a_value.as_ptr(),
2120 );
2121 ffi::cobre_highs_set_int_option(highs, c"simplex_iteration_limit".as_ptr(), 1);
2122 ffi::cobre_highs_run(highs);
2123 }
2124
2125 let model_status = unsafe { ffi::cobre_highs_get_model_status(highs) };
2126 eprintln!("iteration_limit=1 model_status: {model_status}");
2127 assert!(
2130 model_status == ffi::HIGHS_MODEL_STATUS_ITERATION_LIMIT
2131 || model_status == ffi::HIGHS_MODEL_STATUS_OPTIMAL,
2132 "expected ITERATION_LIMIT or OPTIMAL, got {model_status}"
2133 );
2134
2135 unsafe { ffi::cobre_highs_destroy(highs) };
2136 }
2137}