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] {
89 [
90 DefaultOption {
91 name: c"solver",
92 value: OptionValue::Str(c"simplex"),
93 },
94 DefaultOption {
95 name: c"simplex_strategy",
96 value: OptionValue::Int(4), },
98 DefaultOption {
99 name: c"simplex_scale_strategy",
100 value: OptionValue::Int(2), },
102 DefaultOption {
103 name: c"presolve",
104 value: OptionValue::Str(c"off"),
105 },
106 DefaultOption {
107 name: c"parallel",
108 value: OptionValue::Str(c"off"),
109 },
110 DefaultOption {
111 name: c"output_flag",
112 value: OptionValue::Bool(0),
113 },
114 DefaultOption {
115 name: c"primal_feasibility_tolerance",
116 value: OptionValue::Double(1e-7),
117 },
118 DefaultOption {
119 name: c"dual_feasibility_tolerance",
120 value: OptionValue::Double(1e-7),
121 },
122 ]
123}
124
125pub struct HighsSolver {
142 handle: *mut c_void,
144 col_value: Vec<f64>,
147 col_dual: Vec<f64>,
150 row_value: Vec<f64>,
153 row_dual: Vec<f64>,
156 scratch_i32: Vec<i32>,
160 basis_col_i32: Vec<i32>,
164 basis_row_i32: Vec<i32>,
168 num_cols: usize,
170 num_rows: usize,
172 has_model: bool,
175 stats: SolverStatistics,
178}
179
180unsafe impl Send for HighsSolver {}
188
189impl HighsSolver {
190 pub fn new() -> Result<Self, SolverError> {
215 let handle = unsafe { ffi::cobre_highs_create() };
220
221 if handle.is_null() {
222 return Err(SolverError::InternalError {
223 message: "HiGHS instance creation failed: Highs_create() returned null".to_string(),
224 error_code: None,
225 });
226 }
227
228 if let Err(e) = Self::apply_default_config(handle) {
231 unsafe { ffi::cobre_highs_destroy(handle) };
236 return Err(e);
237 }
238
239 Ok(Self {
240 handle,
241 col_value: Vec::new(),
242 col_dual: Vec::new(),
243 row_value: Vec::new(),
244 row_dual: Vec::new(),
245 scratch_i32: Vec::new(),
246 basis_col_i32: Vec::new(),
247 basis_row_i32: Vec::new(),
248 num_cols: 0,
249 num_rows: 0,
250 has_model: false,
251 stats: SolverStatistics::default(),
252 })
253 }
254
255 fn apply_default_config(handle: *mut c_void) -> Result<(), SolverError> {
261 for opt in &default_options() {
262 let status = unsafe { opt.apply(handle) };
264 if status == ffi::HIGHS_STATUS_ERROR {
265 return Err(SolverError::InternalError {
266 message: format!(
267 "HiGHS configuration failed: {}",
268 opt.name.to_str().unwrap_or("?")
269 ),
270 error_code: Some(status),
271 });
272 }
273 }
274 Ok(())
275 }
276
277 fn extract_solution_view(&mut self, solve_time_seconds: f64) -> SolutionView<'_> {
284 let status = unsafe {
286 ffi::cobre_highs_get_solution(
287 self.handle,
288 self.col_value.as_mut_ptr(),
289 self.col_dual.as_mut_ptr(),
290 self.row_value.as_mut_ptr(),
291 self.row_dual.as_mut_ptr(),
292 )
293 };
294 assert_ne!(
295 status,
296 ffi::HIGHS_STATUS_ERROR,
297 "cobre_highs_get_solution failed after optimal solve"
298 );
299
300 let objective = unsafe { ffi::cobre_highs_get_objective_value(self.handle) };
302
303 #[allow(clippy::cast_sign_loss)]
305 let iterations =
306 unsafe { ffi::cobre_highs_get_simplex_iteration_count(self.handle) } as u64;
307
308 SolutionView {
309 objective,
310 primal: &self.col_value[..self.num_cols],
311 dual: &self.row_dual[..self.num_rows],
312 reduced_costs: &self.col_dual[..self.num_cols],
313 iterations,
314 solve_time_seconds,
315 }
316 }
317
318 fn restore_default_settings(&mut self) {
322 for opt in &default_options() {
323 unsafe { opt.apply(self.handle) };
325 }
326 }
327
328 fn run_once(&mut self) -> i32 {
330 let run_status = unsafe { ffi::cobre_highs_run(self.handle) };
332 if run_status == ffi::HIGHS_STATUS_ERROR {
333 return ffi::HIGHS_MODEL_STATUS_SOLVE_ERROR;
334 }
335 unsafe { ffi::cobre_highs_get_model_status(self.handle) }
337 }
338
339 fn interpret_terminal_status(
344 &mut self,
345 status: i32,
346 solve_time_seconds: f64,
347 ) -> Option<SolverError> {
348 match status {
349 ffi::HIGHS_MODEL_STATUS_OPTIMAL => {
350 None
352 }
353 ffi::HIGHS_MODEL_STATUS_INFEASIBLE => Some(SolverError::Infeasible),
354 ffi::HIGHS_MODEL_STATUS_UNBOUNDED_OR_INFEASIBLE => {
355 let mut has_dual_ray: i32 = 0;
359 let mut dual_buf = vec![0.0_f64; self.num_rows];
362 let dual_status = unsafe {
364 ffi::cobre_highs_get_dual_ray(
365 self.handle,
366 &raw mut has_dual_ray,
367 dual_buf.as_mut_ptr(),
368 )
369 };
370 if dual_status != ffi::HIGHS_STATUS_ERROR && has_dual_ray != 0 {
371 return Some(SolverError::Infeasible);
372 }
373 let mut has_primal_ray: i32 = 0;
374 let mut primal_buf = vec![0.0_f64; self.num_cols];
375 let primal_status = unsafe {
377 ffi::cobre_highs_get_primal_ray(
378 self.handle,
379 &raw mut has_primal_ray,
380 primal_buf.as_mut_ptr(),
381 )
382 };
383 if primal_status != ffi::HIGHS_STATUS_ERROR && has_primal_ray != 0 {
384 return Some(SolverError::Unbounded);
385 }
386 Some(SolverError::Infeasible)
387 }
388 ffi::HIGHS_MODEL_STATUS_UNBOUNDED => Some(SolverError::Unbounded),
389 ffi::HIGHS_MODEL_STATUS_TIME_LIMIT => Some(SolverError::TimeLimitExceeded {
390 elapsed_seconds: solve_time_seconds,
391 }),
392 ffi::HIGHS_MODEL_STATUS_ITERATION_LIMIT => {
393 #[allow(clippy::cast_sign_loss)]
395 let iterations =
396 unsafe { ffi::cobre_highs_get_simplex_iteration_count(self.handle) } as u64;
397 Some(SolverError::IterationLimit { iterations })
398 }
399 ffi::HIGHS_MODEL_STATUS_SOLVE_ERROR | ffi::HIGHS_MODEL_STATUS_UNKNOWN => {
400 None
402 }
403 other => Some(SolverError::InternalError {
404 message: format!("HiGHS returned unexpected model status {other}"),
405 error_code: Some(other),
406 }),
407 }
408 }
409
410 fn convert_to_i32_scratch(&mut self, source: &[usize]) -> &[i32] {
414 if source.len() > self.scratch_i32.len() {
415 self.scratch_i32.resize(source.len(), 0);
416 }
417 for (i, &v) in source.iter().enumerate() {
418 debug_assert!(
419 i32::try_from(v).is_ok(),
420 "usize index {v} overflows i32::MAX at position {i}"
421 );
422 #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
424 {
425 self.scratch_i32[i] = v as i32;
426 }
427 }
428 &self.scratch_i32[..source.len()]
429 }
430}
431
432impl Drop for HighsSolver {
433 fn drop(&mut self) {
434 unsafe { ffi::cobre_highs_destroy(self.handle) };
436 }
437}
438
439impl SolverInterface for HighsSolver {
440 fn name(&self) -> &'static str {
441 "HiGHS"
442 }
443
444 fn load_model(&mut self, template: &StageTemplate) {
445 assert!(
455 i32::try_from(template.num_cols).is_ok(),
456 "num_cols {} overflows i32: LP exceeds HiGHS API limit",
457 template.num_cols
458 );
459 assert!(
460 i32::try_from(template.num_rows).is_ok(),
461 "num_rows {} overflows i32: LP exceeds HiGHS API limit",
462 template.num_rows
463 );
464 assert!(
465 i32::try_from(template.num_nz).is_ok(),
466 "num_nz {} overflows i32: LP exceeds HiGHS API limit",
467 template.num_nz
468 );
469 #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
471 let num_col = template.num_cols as i32;
472 #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
473 let num_row = template.num_rows as i32;
474 #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
475 let num_nz = template.num_nz as i32;
476 let status = unsafe {
477 ffi::cobre_highs_pass_lp(
478 self.handle,
479 num_col,
480 num_row,
481 num_nz,
482 ffi::HIGHS_MATRIX_FORMAT_COLWISE,
483 ffi::HIGHS_OBJ_SENSE_MINIMIZE,
484 0.0, template.objective.as_ptr(),
486 template.col_lower.as_ptr(),
487 template.col_upper.as_ptr(),
488 template.row_lower.as_ptr(),
489 template.row_upper.as_ptr(),
490 template.col_starts.as_ptr(),
491 template.row_indices.as_ptr(),
492 template.values.as_ptr(),
493 )
494 };
495
496 assert_ne!(
497 status,
498 ffi::HIGHS_STATUS_ERROR,
499 "cobre_highs_pass_lp failed with status {status}"
500 );
501
502 self.num_cols = template.num_cols;
503 self.num_rows = template.num_rows;
504 self.has_model = true;
505
506 self.col_value.resize(self.num_cols, 0.0);
509 self.col_dual.resize(self.num_cols, 0.0);
510 self.row_value.resize(self.num_rows, 0.0);
511 self.row_dual.resize(self.num_rows, 0.0);
512
513 self.basis_col_i32.resize(self.num_cols, 0);
516 self.basis_row_i32.resize(self.num_rows, 0);
517 }
518
519 fn add_rows(&mut self, cuts: &RowBatch) {
520 assert!(
521 i32::try_from(cuts.num_rows).is_ok(),
522 "cuts.num_rows {} overflows i32: RowBatch exceeds HiGHS API limit",
523 cuts.num_rows
524 );
525 assert!(
526 i32::try_from(cuts.col_indices.len()).is_ok(),
527 "cuts nnz {} overflows i32: RowBatch exceeds HiGHS API limit",
528 cuts.col_indices.len()
529 );
530 #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
532 let num_new_row = cuts.num_rows as i32;
533 #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
534 let num_new_nz = cuts.col_indices.len() as i32;
535
536 let status = unsafe {
544 ffi::cobre_highs_add_rows(
545 self.handle,
546 num_new_row,
547 cuts.row_lower.as_ptr(),
548 cuts.row_upper.as_ptr(),
549 num_new_nz,
550 cuts.row_starts.as_ptr(),
551 cuts.col_indices.as_ptr(),
552 cuts.values.as_ptr(),
553 )
554 };
555
556 assert_ne!(
557 status,
558 ffi::HIGHS_STATUS_ERROR,
559 "cobre_highs_add_rows failed with status {status}"
560 );
561
562 self.num_rows += cuts.num_rows;
563
564 self.row_value.resize(self.num_rows, 0.0);
566 self.row_dual.resize(self.num_rows, 0.0);
567
568 self.basis_row_i32.resize(self.num_rows, 0);
570 }
571
572 fn set_row_bounds(&mut self, indices: &[usize], lower: &[f64], upper: &[f64]) {
573 assert!(
574 indices.len() == lower.len() && indices.len() == upper.len(),
575 "set_row_bounds: indices ({}), lower ({}), and upper ({}) must have equal length",
576 indices.len(),
577 lower.len(),
578 upper.len()
579 );
580 if indices.is_empty() {
581 return;
582 }
583
584 assert!(
585 i32::try_from(indices.len()).is_ok(),
586 "set_row_bounds: indices.len() {} overflows i32",
587 indices.len()
588 );
589 #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
590 let num_entries = indices.len() as i32;
591
592 let status = unsafe {
599 ffi::cobre_highs_change_rows_bounds_by_set(
600 self.handle,
601 num_entries,
602 self.convert_to_i32_scratch(indices).as_ptr(),
603 lower.as_ptr(),
604 upper.as_ptr(),
605 )
606 };
607
608 assert_ne!(
609 status,
610 ffi::HIGHS_STATUS_ERROR,
611 "cobre_highs_change_rows_bounds_by_set failed with status {status}"
612 );
613 }
614
615 fn set_col_bounds(&mut self, indices: &[usize], lower: &[f64], upper: &[f64]) {
616 assert!(
617 indices.len() == lower.len() && indices.len() == upper.len(),
618 "set_col_bounds: indices ({}), lower ({}), and upper ({}) must have equal length",
619 indices.len(),
620 lower.len(),
621 upper.len()
622 );
623 if indices.is_empty() {
624 return;
625 }
626
627 assert!(
628 i32::try_from(indices.len()).is_ok(),
629 "set_col_bounds: indices.len() {} overflows i32",
630 indices.len()
631 );
632 #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
633 let num_entries = indices.len() as i32;
634
635 let status = unsafe {
641 ffi::cobre_highs_change_cols_bounds_by_set(
642 self.handle,
643 num_entries,
644 self.convert_to_i32_scratch(indices).as_ptr(),
645 lower.as_ptr(),
646 upper.as_ptr(),
647 )
648 };
649
650 assert_ne!(
651 status,
652 ffi::HIGHS_STATUS_ERROR,
653 "cobre_highs_change_cols_bounds_by_set failed with status {status}"
654 );
655 }
656
657 #[allow(clippy::too_many_lines)]
658 fn solve(&mut self) -> Result<SolutionView<'_>, SolverError> {
659 assert!(
660 self.has_model,
661 "solve called without a loaded model — call load_model first"
662 );
663 let t0 = Instant::now();
664 let model_status = self.run_once();
665 let solve_time = t0.elapsed().as_secs_f64();
666
667 self.stats.solve_count += 1;
668
669 if model_status == ffi::HIGHS_MODEL_STATUS_OPTIMAL {
670 #[allow(clippy::cast_sign_loss)]
675 let iterations =
676 unsafe { ffi::cobre_highs_get_simplex_iteration_count(self.handle) } as u64;
677 self.stats.success_count += 1;
678 self.stats.first_try_successes += 1;
679 self.stats.total_iterations += iterations;
680 self.stats.total_solve_time_seconds += solve_time;
681 return Ok(self.extract_solution_view(solve_time));
682 }
683
684 let is_unbounded = model_status == ffi::HIGHS_MODEL_STATUS_UNBOUNDED;
689 if !is_unbounded {
690 if let Some(terminal_err) = self.interpret_terminal_status(model_status, solve_time) {
691 self.stats.failure_count += 1;
692 return Err(terminal_err);
693 }
694 }
695
696 let retry_time_limit = 30.0_f64;
714 let num_retry_levels = 12_u32;
715
716 let mut retry_attempts: u64 = 0;
717 let mut terminal_err: Option<SolverError> = None;
718 let mut found_optimal = false;
719 let mut optimal_time = 0.0_f64;
720 let mut optimal_iterations: u64 = 0;
721
722 for level in 0..num_retry_levels {
723 match level {
726 0 => {
730 unsafe { ffi::cobre_highs_clear_solver(self.handle) };
731 }
732 1 => unsafe {
734 ffi::cobre_highs_set_string_option(
735 self.handle,
736 c"presolve".as_ptr(),
737 c"on".as_ptr(),
738 );
739 },
740 2 => unsafe {
743 ffi::cobre_highs_set_int_option(self.handle, c"simplex_strategy".as_ptr(), 1);
744 },
745 3 => unsafe {
748 ffi::cobre_highs_set_double_option(
749 self.handle,
750 c"primal_feasibility_tolerance".as_ptr(),
751 1e-6,
752 );
753 ffi::cobre_highs_set_double_option(
754 self.handle,
755 c"dual_feasibility_tolerance".as_ptr(),
756 1e-6,
757 );
758 },
759 4 => unsafe {
762 ffi::cobre_highs_set_string_option(
763 self.handle,
764 c"solver".as_ptr(),
765 c"ipm".as_ptr(),
766 );
767 },
768
769 5 => {
775 self.restore_default_settings();
776 unsafe {
777 ffi::cobre_highs_set_string_option(
778 self.handle,
779 c"presolve".as_ptr(),
780 c"on".as_ptr(),
781 );
782 ffi::cobre_highs_set_double_option(
783 self.handle,
784 c"time_limit".as_ptr(),
785 retry_time_limit,
786 );
787 ffi::cobre_highs_set_int_option(
788 self.handle,
789 c"simplex_scale_strategy".as_ptr(),
790 3, );
792 }
793 }
794 6 => {
796 self.restore_default_settings();
797 unsafe {
798 ffi::cobre_highs_set_string_option(
799 self.handle,
800 c"presolve".as_ptr(),
801 c"on".as_ptr(),
802 );
803 ffi::cobre_highs_set_double_option(
804 self.handle,
805 c"time_limit".as_ptr(),
806 retry_time_limit,
807 );
808 ffi::cobre_highs_set_int_option(
809 self.handle,
810 c"simplex_strategy".as_ptr(),
811 1,
812 );
813 ffi::cobre_highs_set_int_option(
814 self.handle,
815 c"simplex_scale_strategy".as_ptr(),
816 4, );
818 }
819 }
820 7 => {
822 self.restore_default_settings();
823 unsafe {
824 ffi::cobre_highs_set_string_option(
825 self.handle,
826 c"presolve".as_ptr(),
827 c"on".as_ptr(),
828 );
829 ffi::cobre_highs_set_double_option(
830 self.handle,
831 c"time_limit".as_ptr(),
832 retry_time_limit,
833 );
834 ffi::cobre_highs_set_int_option(
835 self.handle,
836 c"simplex_scale_strategy".as_ptr(),
837 3,
838 );
839 ffi::cobre_highs_set_double_option(
840 self.handle,
841 c"primal_feasibility_tolerance".as_ptr(),
842 1e-6,
843 );
844 ffi::cobre_highs_set_double_option(
845 self.handle,
846 c"dual_feasibility_tolerance".as_ptr(),
847 1e-6,
848 );
849 }
850 }
851 8 => {
853 self.restore_default_settings();
854 unsafe {
855 ffi::cobre_highs_set_string_option(
856 self.handle,
857 c"presolve".as_ptr(),
858 c"on".as_ptr(),
859 );
860 ffi::cobre_highs_set_double_option(
861 self.handle,
862 c"time_limit".as_ptr(),
863 retry_time_limit,
864 );
865 ffi::cobre_highs_set_int_option(
866 self.handle,
867 c"user_objective_scale".as_ptr(),
868 -10,
869 );
870 }
871 }
872 9 => {
874 self.restore_default_settings();
875 unsafe {
876 ffi::cobre_highs_set_string_option(
877 self.handle,
878 c"presolve".as_ptr(),
879 c"on".as_ptr(),
880 );
881 ffi::cobre_highs_set_double_option(
882 self.handle,
883 c"time_limit".as_ptr(),
884 retry_time_limit,
885 );
886 ffi::cobre_highs_set_int_option(
887 self.handle,
888 c"simplex_strategy".as_ptr(),
889 1,
890 );
891 ffi::cobre_highs_set_int_option(
892 self.handle,
893 c"user_objective_scale".as_ptr(),
894 -10,
895 );
896 ffi::cobre_highs_set_int_option(
897 self.handle,
898 c"user_bound_scale".as_ptr(),
899 -5,
900 );
901 }
902 }
903 10 => {
905 self.restore_default_settings();
906 unsafe {
907 ffi::cobre_highs_set_string_option(
908 self.handle,
909 c"presolve".as_ptr(),
910 c"on".as_ptr(),
911 );
912 ffi::cobre_highs_set_double_option(
913 self.handle,
914 c"time_limit".as_ptr(),
915 retry_time_limit,
916 );
917 ffi::cobre_highs_set_int_option(
918 self.handle,
919 c"user_objective_scale".as_ptr(),
920 -13,
921 );
922 ffi::cobre_highs_set_int_option(
923 self.handle,
924 c"user_bound_scale".as_ptr(),
925 -8,
926 );
927 ffi::cobre_highs_set_double_option(
928 self.handle,
929 c"primal_feasibility_tolerance".as_ptr(),
930 1e-6,
931 );
932 ffi::cobre_highs_set_double_option(
933 self.handle,
934 c"dual_feasibility_tolerance".as_ptr(),
935 1e-6,
936 );
937 }
938 }
939 11 => {
942 self.restore_default_settings();
943 unsafe {
944 ffi::cobre_highs_set_string_option(
945 self.handle,
946 c"solver".as_ptr(),
947 c"ipm".as_ptr(),
948 );
949 ffi::cobre_highs_set_string_option(
950 self.handle,
951 c"presolve".as_ptr(),
952 c"on".as_ptr(),
953 );
954 ffi::cobre_highs_set_double_option(
955 self.handle,
956 c"time_limit".as_ptr(),
957 retry_time_limit,
958 );
959 ffi::cobre_highs_set_int_option(
960 self.handle,
961 c"user_objective_scale".as_ptr(),
962 -10,
963 );
964 ffi::cobre_highs_set_int_option(
965 self.handle,
966 c"user_bound_scale".as_ptr(),
967 -5,
968 );
969 ffi::cobre_highs_set_double_option(
970 self.handle,
971 c"primal_feasibility_tolerance".as_ptr(),
972 1e-6,
973 );
974 ffi::cobre_highs_set_double_option(
975 self.handle,
976 c"dual_feasibility_tolerance".as_ptr(),
977 1e-6,
978 );
979 }
980 }
981 _ => unreachable!(),
982 }
983
984 retry_attempts += 1;
985
986 let t_retry = Instant::now();
987 let retry_status = self.run_once();
988 let retry_time = t_retry.elapsed().as_secs_f64();
989
990 if retry_status == ffi::HIGHS_MODEL_STATUS_OPTIMAL {
991 #[allow(clippy::cast_sign_loss)]
994 let iters =
995 unsafe { ffi::cobre_highs_get_simplex_iteration_count(self.handle) } as u64;
996 found_optimal = true;
997 optimal_time = retry_time;
998 optimal_iterations = iters;
999 break;
1000 }
1001
1002 let retryable = retry_status == ffi::HIGHS_MODEL_STATUS_UNBOUNDED
1007 || retry_status == ffi::HIGHS_MODEL_STATUS_TIME_LIMIT;
1008 if !retryable {
1009 if let Some(e) = self.interpret_terminal_status(retry_status, retry_time) {
1010 terminal_err = Some(e);
1011 break;
1012 }
1013 }
1014 }
1016
1017 self.restore_default_settings();
1021 unsafe {
1022 ffi::cobre_highs_set_double_option(self.handle, c"time_limit".as_ptr(), f64::INFINITY);
1023 ffi::cobre_highs_set_int_option(self.handle, c"user_objective_scale".as_ptr(), 0);
1024 ffi::cobre_highs_set_int_option(self.handle, c"user_bound_scale".as_ptr(), 0);
1025 }
1026
1027 self.stats.retry_count += retry_attempts;
1029
1030 if found_optimal {
1031 self.stats.success_count += 1;
1032 self.stats.total_iterations += optimal_iterations;
1033 self.stats.total_solve_time_seconds += optimal_time;
1034 return Ok(self.extract_solution_view(optimal_time));
1035 }
1036
1037 self.stats.failure_count += 1;
1038 Err(terminal_err.unwrap_or_else(|| {
1039 if is_unbounded {
1041 SolverError::Unbounded
1042 } else {
1043 SolverError::NumericalDifficulty {
1044 message: "HiGHS failed to reach optimality after all retry escalation levels"
1045 .to_string(),
1046 }
1047 }
1048 }))
1049 }
1050
1051 fn reset(&mut self) {
1052 let status = unsafe { ffi::cobre_highs_clear_solver(self.handle) };
1057 debug_assert_ne!(
1058 status,
1059 ffi::HIGHS_STATUS_ERROR,
1060 "cobre_highs_clear_solver failed — HiGHS internal state may be inconsistent"
1061 );
1062 self.num_cols = 0;
1064 self.num_rows = 0;
1065 self.has_model = false;
1066 }
1069
1070 fn get_basis(&mut self, out: &mut crate::types::Basis) {
1071 assert!(
1072 self.has_model,
1073 "get_basis called without a loaded model — call load_model first"
1074 );
1075
1076 out.col_status.resize(self.num_cols, 0);
1077 out.row_status.resize(self.num_rows, 0);
1078
1079 let get_status = unsafe {
1085 ffi::cobre_highs_get_basis(
1086 self.handle,
1087 out.col_status.as_mut_ptr(),
1088 out.row_status.as_mut_ptr(),
1089 )
1090 };
1091
1092 assert_ne!(
1093 get_status,
1094 ffi::HIGHS_STATUS_ERROR,
1095 "cobre_highs_get_basis failed: basis must exist after a successful solve (programming error)"
1096 );
1097 }
1098
1099 fn solve_with_basis(
1100 &mut self,
1101 basis: &crate::types::Basis,
1102 ) -> Result<crate::types::SolutionView<'_>, SolverError> {
1103 assert!(
1104 self.has_model,
1105 "solve_with_basis called without a loaded model — call load_model first"
1106 );
1107 assert!(
1108 basis.col_status.len() == self.num_cols,
1109 "basis column count {} does not match LP column count {}",
1110 basis.col_status.len(),
1111 self.num_cols
1112 );
1113
1114 self.stats.basis_offered += 1;
1116
1117 self.basis_col_i32[..self.num_cols].copy_from_slice(&basis.col_status);
1120
1121 let basis_rows = basis.row_status.len();
1125 let lp_rows = self.num_rows;
1126 let copy_len = basis_rows.min(lp_rows);
1127 self.basis_row_i32[..copy_len].copy_from_slice(&basis.row_status[..copy_len]);
1128 if lp_rows > basis_rows {
1129 self.basis_row_i32[basis_rows..lp_rows].fill(ffi::HIGHS_BASIS_STATUS_BASIC);
1130 }
1131
1132 let set_status = unsafe {
1139 ffi::cobre_highs_set_basis(
1140 self.handle,
1141 self.basis_col_i32.as_ptr(),
1142 self.basis_row_i32.as_ptr(),
1143 )
1144 };
1145
1146 if set_status == ffi::HIGHS_STATUS_ERROR {
1148 self.stats.basis_rejections += 1;
1149 debug_assert!(false, "raw basis rejected; falling back to cold-start");
1150 }
1151
1152 self.solve()
1154 }
1155
1156 fn statistics(&self) -> SolverStatistics {
1157 self.stats.clone()
1158 }
1159}
1160
1161#[cfg(feature = "test-support")]
1167impl HighsSolver {
1168 #[must_use]
1176 pub fn raw_handle(&self) -> *mut std::os::raw::c_void {
1177 self.handle
1178 }
1179}
1180
1181#[cfg(test)]
1182mod tests {
1183 use super::HighsSolver;
1184 use crate::{
1185 SolverInterface,
1186 types::{Basis, RowBatch, StageTemplate},
1187 };
1188
1189 fn make_fixture_stage_template() -> StageTemplate {
1202 StageTemplate {
1203 num_cols: 3,
1204 num_rows: 2,
1205 num_nz: 3,
1206 col_starts: vec![0_i32, 2, 2, 3],
1207 row_indices: vec![0_i32, 1, 1],
1208 values: vec![1.0, 2.0, 1.0],
1209 col_lower: vec![0.0, 0.0, 0.0],
1210 col_upper: vec![10.0, f64::INFINITY, 8.0],
1211 objective: vec![0.0, 1.0, 50.0],
1212 row_lower: vec![6.0, 14.0],
1213 row_upper: vec![6.0, 14.0],
1214 n_state: 1,
1215 n_transfer: 0,
1216 n_dual_relevant: 1,
1217 n_hydro: 1,
1218 max_par_order: 0,
1219 col_scale: Vec::new(),
1220 row_scale: Vec::new(),
1221 }
1222 }
1223
1224 fn make_fixture_row_batch() -> RowBatch {
1228 RowBatch {
1229 num_rows: 2,
1230 row_starts: vec![0_i32, 2, 4],
1231 col_indices: vec![0_i32, 1, 0, 1],
1232 values: vec![-5.0, 1.0, 3.0, 1.0],
1233 row_lower: vec![20.0, 80.0],
1234 row_upper: vec![f64::INFINITY, f64::INFINITY],
1235 }
1236 }
1237
1238 #[test]
1239 fn test_highs_solver_create_and_name() {
1240 let solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1241 assert_eq!(solver.name(), "HiGHS");
1242 }
1244
1245 #[test]
1246 fn test_highs_solver_send_bound() {
1247 fn assert_send<T: Send>() {}
1248 assert_send::<HighsSolver>();
1249 }
1250
1251 #[test]
1252 fn test_highs_solver_statistics_initial() {
1253 let solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1254 let stats = solver.statistics();
1255 assert_eq!(stats.solve_count, 0);
1256 assert_eq!(stats.success_count, 0);
1257 assert_eq!(stats.failure_count, 0);
1258 assert_eq!(stats.total_iterations, 0);
1259 assert_eq!(stats.retry_count, 0);
1260 assert_eq!(stats.total_solve_time_seconds, 0.0);
1261 }
1262
1263 #[test]
1264 fn test_highs_load_model_updates_dimensions() {
1265 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1266 let template = make_fixture_stage_template();
1267
1268 solver.load_model(&template);
1269
1270 assert_eq!(solver.num_cols, 3, "num_cols must be 3 after load_model");
1271 assert_eq!(solver.num_rows, 2, "num_rows must be 2 after load_model");
1272 assert_eq!(
1273 solver.col_value.len(),
1274 3,
1275 "col_value buffer must be resized to num_cols"
1276 );
1277 assert_eq!(
1278 solver.col_dual.len(),
1279 3,
1280 "col_dual buffer must be resized to num_cols"
1281 );
1282 assert_eq!(
1283 solver.row_value.len(),
1284 2,
1285 "row_value buffer must be resized to num_rows"
1286 );
1287 assert_eq!(
1288 solver.row_dual.len(),
1289 2,
1290 "row_dual buffer must be resized to num_rows"
1291 );
1292 }
1293
1294 #[test]
1295 fn test_highs_add_rows_updates_dimensions() {
1296 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1297 let template = make_fixture_stage_template();
1298 let cuts = make_fixture_row_batch();
1299
1300 solver.load_model(&template);
1301 solver.add_rows(&cuts);
1302
1303 assert_eq!(solver.num_rows, 4, "num_rows must be 4 after add_rows");
1305 assert_eq!(
1306 solver.row_dual.len(),
1307 4,
1308 "row_dual buffer must be resized to 4 after add_rows"
1309 );
1310 assert_eq!(
1311 solver.row_value.len(),
1312 4,
1313 "row_value buffer must be resized to 4 after add_rows"
1314 );
1315 assert_eq!(solver.num_cols, 3, "num_cols must be unchanged by add_rows");
1317 }
1318
1319 #[test]
1320 fn test_highs_set_row_bounds_no_panic() {
1321 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1322 let template = make_fixture_stage_template();
1323 solver.load_model(&template);
1324
1325 solver.set_row_bounds(&[0], &[4.0], &[4.0]);
1327 }
1328
1329 #[test]
1330 fn test_highs_set_col_bounds_no_panic() {
1331 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1332 let template = make_fixture_stage_template();
1333 solver.load_model(&template);
1334
1335 solver.set_col_bounds(&[1], &[10.0], &[f64::INFINITY]);
1337 }
1338
1339 #[test]
1340 fn test_highs_set_bounds_empty_no_panic() {
1341 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1342 let template = make_fixture_stage_template();
1343 solver.load_model(&template);
1344
1345 solver.set_row_bounds(&[], &[], &[]);
1347 solver.set_col_bounds(&[], &[], &[]);
1348 }
1349
1350 #[test]
1353 fn test_highs_solve_basic_lp() {
1354 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1355 let template = make_fixture_stage_template();
1356 solver.load_model(&template);
1357
1358 let solution = solver
1359 .solve()
1360 .expect("solve() must succeed on a feasible LP");
1361
1362 assert!(
1363 (solution.objective - 100.0).abs() < 1e-8,
1364 "objective must be 100.0, got {}",
1365 solution.objective
1366 );
1367 assert_eq!(solution.primal.len(), 3, "primal must have 3 elements");
1368 assert!(
1369 (solution.primal[0] - 6.0).abs() < 1e-8,
1370 "primal[0] (x0) must be 6.0, got {}",
1371 solution.primal[0]
1372 );
1373 assert!(
1374 (solution.primal[1] - 0.0).abs() < 1e-8,
1375 "primal[1] (x1) must be 0.0, got {}",
1376 solution.primal[1]
1377 );
1378 assert!(
1379 (solution.primal[2] - 2.0).abs() < 1e-8,
1380 "primal[2] (x2) must be 2.0, got {}",
1381 solution.primal[2]
1382 );
1383 }
1384
1385 #[test]
1389 fn test_highs_solve_with_cuts() {
1390 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1391 let template = make_fixture_stage_template();
1392 let cuts = make_fixture_row_batch();
1393 solver.load_model(&template);
1394 solver.add_rows(&cuts);
1395
1396 let solution = solver
1397 .solve()
1398 .expect("solve() must succeed on a feasible LP with cuts");
1399
1400 assert!(
1401 (solution.objective - 162.0).abs() < 1e-8,
1402 "objective must be 162.0, got {}",
1403 solution.objective
1404 );
1405 assert!(
1406 (solution.primal[0] - 6.0).abs() < 1e-8,
1407 "primal[0] must be 6.0, got {}",
1408 solution.primal[0]
1409 );
1410 assert!(
1411 (solution.primal[1] - 62.0).abs() < 1e-8,
1412 "primal[1] must be 62.0, got {}",
1413 solution.primal[1]
1414 );
1415 assert!(
1416 (solution.primal[2] - 2.0).abs() < 1e-8,
1417 "primal[2] must be 2.0, got {}",
1418 solution.primal[2]
1419 );
1420 }
1421
1422 #[test]
1425 fn test_highs_solve_after_rhs_patch() {
1426 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1427 let template = make_fixture_stage_template();
1428 let cuts = make_fixture_row_batch();
1429 solver.load_model(&template);
1430 solver.add_rows(&cuts);
1431
1432 solver.set_row_bounds(&[0], &[4.0], &[4.0]);
1434
1435 let solution = solver
1436 .solve()
1437 .expect("solve() must succeed after RHS patch");
1438
1439 assert!(
1440 (solution.objective - 368.0).abs() < 1e-8,
1441 "objective must be 368.0, got {}",
1442 solution.objective
1443 );
1444 }
1445
1446 #[test]
1448 fn test_highs_solve_statistics_increment() {
1449 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1450 let template = make_fixture_stage_template();
1451 solver.load_model(&template);
1452
1453 solver.solve().expect("first solve must succeed");
1454 solver.solve().expect("second solve must succeed");
1455
1456 let stats = solver.statistics();
1457 assert_eq!(stats.solve_count, 2, "solve_count must be 2");
1458 assert_eq!(stats.success_count, 2, "success_count must be 2");
1459 assert_eq!(stats.failure_count, 0, "failure_count must be 0");
1460 assert!(
1461 stats.total_iterations > 0,
1462 "total_iterations must be positive"
1463 );
1464 }
1465
1466 #[test]
1468 fn test_highs_reset_preserves_stats() {
1469 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1470 let template = make_fixture_stage_template();
1471 solver.load_model(&template);
1472 solver.solve().expect("solve must succeed");
1473
1474 let stats_before = solver.statistics();
1475 assert_eq!(
1476 stats_before.solve_count, 1,
1477 "solve_count must be 1 before reset"
1478 );
1479
1480 solver.reset();
1481
1482 let stats_after = solver.statistics();
1483 assert_eq!(
1484 stats_after.solve_count, stats_before.solve_count,
1485 "solve_count must be unchanged after reset"
1486 );
1487 assert_eq!(
1488 stats_after.success_count, stats_before.success_count,
1489 "success_count must be unchanged after reset"
1490 );
1491 assert_eq!(
1492 stats_after.total_iterations, stats_before.total_iterations,
1493 "total_iterations must be unchanged after reset"
1494 );
1495 }
1496
1497 #[test]
1499 fn test_highs_solve_iterations_positive() {
1500 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1501 let template = make_fixture_stage_template();
1502 solver.load_model(&template);
1503
1504 let solution = solver.solve().expect("solve must succeed");
1505 assert!(
1506 solution.iterations > 0,
1507 "iterations must be positive, got {}",
1508 solution.iterations
1509 );
1510 }
1511
1512 #[test]
1514 fn test_highs_solve_time_positive() {
1515 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1516 let template = make_fixture_stage_template();
1517 solver.load_model(&template);
1518
1519 let solution = solver.solve().expect("solve must succeed");
1520 assert!(
1521 solution.solve_time_seconds > 0.0,
1522 "solve_time_seconds must be positive, got {}",
1523 solution.solve_time_seconds
1524 );
1525 }
1526
1527 #[test]
1530 fn test_highs_solve_statistics_single() {
1531 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1532 let template = make_fixture_stage_template();
1533 solver.load_model(&template);
1534
1535 solver.solve().expect("solve must succeed");
1536
1537 let stats = solver.statistics();
1538 assert_eq!(stats.solve_count, 1, "solve_count must be 1");
1539 assert_eq!(stats.success_count, 1, "success_count must be 1");
1540 assert_eq!(stats.failure_count, 0, "failure_count must be 0");
1541 assert!(
1542 stats.total_iterations > 0,
1543 "total_iterations must be positive after a successful solve"
1544 );
1545 }
1546
1547 #[test]
1550 fn test_get_basis_valid_status_codes() {
1551 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1552 let template = make_fixture_stage_template();
1553 solver.load_model(&template);
1554 solver.solve().expect("solve must succeed before get_basis");
1555
1556 let mut basis = Basis::new(0, 0);
1557 solver.get_basis(&mut basis);
1558
1559 for &code in &basis.col_status {
1560 assert!(
1561 (0..=4).contains(&code),
1562 "col_status code {code} is outside valid HiGHS range 0..=4"
1563 );
1564 }
1565 for &code in &basis.row_status {
1566 assert!(
1567 (0..=4).contains(&code),
1568 "row_status code {code} is outside valid HiGHS range 0..=4"
1569 );
1570 }
1571 }
1572
1573 #[test]
1576 fn test_get_basis_resizes_output() {
1577 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1578 let template = make_fixture_stage_template();
1579 solver.load_model(&template);
1580 solver.solve().expect("solve must succeed before get_basis");
1581
1582 let mut basis = Basis::new(0, 0);
1583 assert_eq!(
1584 basis.col_status.len(),
1585 0,
1586 "initial col_status must be empty"
1587 );
1588 assert_eq!(
1589 basis.row_status.len(),
1590 0,
1591 "initial row_status must be empty"
1592 );
1593
1594 solver.get_basis(&mut basis);
1595
1596 assert_eq!(
1597 basis.col_status.len(),
1598 3,
1599 "col_status must be resized to 3 (num_cols of SS1.1)"
1600 );
1601 assert_eq!(
1602 basis.row_status.len(),
1603 2,
1604 "row_status must be resized to 2 (num_rows of SS1.1)"
1605 );
1606 }
1607
1608 #[test]
1611 fn test_solve_with_basis_warm_start() {
1612 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1613 let template = make_fixture_stage_template();
1614 solver.load_model(&template);
1615 solver.solve().expect("cold-start solve must succeed");
1616
1617 let mut basis = Basis::new(0, 0);
1618 solver.get_basis(&mut basis);
1619
1620 solver.load_model(&template);
1622 let result = solver
1623 .solve_with_basis(&basis)
1624 .expect("warm-start solve must succeed");
1625
1626 assert!(
1627 (result.objective - 100.0).abs() < 1e-8,
1628 "warm-start objective must be 100.0, got {}",
1629 result.objective
1630 );
1631 assert!(
1632 result.iterations <= 1,
1633 "warm-start from exact basis must use at most 1 iteration, got {}",
1634 result.iterations
1635 );
1636
1637 let stats = solver.statistics();
1638 assert_eq!(
1639 stats.basis_rejections, 0,
1640 "basis_rejections must be 0 when raw basis is accepted, got {}",
1641 stats.basis_rejections
1642 );
1643 }
1644
1645 #[test]
1649 fn test_solve_with_basis_dimension_mismatch() {
1650 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1651 let template = make_fixture_stage_template();
1652 let cuts = make_fixture_row_batch();
1653
1654 solver.load_model(&template);
1656 solver.solve().expect("SS1.1 solve must succeed");
1657 let mut basis = Basis::new(0, 0);
1658 solver.get_basis(&mut basis);
1659 assert_eq!(
1660 basis.row_status.len(),
1661 2,
1662 "captured basis must have 2 row statuses"
1663 );
1664
1665 solver.load_model(&template);
1667 solver.add_rows(&cuts);
1668 assert_eq!(solver.num_rows, 4, "LP must have 4 rows after add_rows");
1669
1670 let result = solver
1672 .solve_with_basis(&basis)
1673 .expect("solve with dimension-mismatched basis must succeed");
1674
1675 assert!(
1676 (result.objective - 162.0).abs() < 1e-8,
1677 "objective with both cuts active must be 162.0, got {}",
1678 result.objective
1679 );
1680 }
1681}
1682
1683#[cfg(test)]
1695#[allow(clippy::doc_markdown)]
1696mod research_tests_ticket_023 {
1697 unsafe fn research_load_ss11_lp(highs: *mut std::os::raw::c_void) {
1708 use crate::ffi;
1709 let col_cost: [f64; 3] = [0.0, 1.0, 50.0];
1710 let col_lower: [f64; 3] = [0.0, 0.0, 0.0];
1711 let col_upper: [f64; 3] = [10.0, f64::INFINITY, 8.0];
1712 let row_lower: [f64; 2] = [6.0, 14.0];
1713 let row_upper: [f64; 2] = [6.0, 14.0];
1714 let a_start: [i32; 4] = [0, 2, 2, 3];
1715 let a_index: [i32; 3] = [0, 1, 1];
1716 let a_value: [f64; 3] = [1.0, 2.0, 1.0];
1717 let status = unsafe {
1719 ffi::cobre_highs_pass_lp(
1720 highs,
1721 3,
1722 2,
1723 3,
1724 ffi::HIGHS_MATRIX_FORMAT_COLWISE,
1725 ffi::HIGHS_OBJ_SENSE_MINIMIZE,
1726 0.0,
1727 col_cost.as_ptr(),
1728 col_lower.as_ptr(),
1729 col_upper.as_ptr(),
1730 row_lower.as_ptr(),
1731 row_upper.as_ptr(),
1732 a_start.as_ptr(),
1733 a_index.as_ptr(),
1734 a_value.as_ptr(),
1735 )
1736 };
1737 assert_eq!(
1738 status,
1739 ffi::HIGHS_STATUS_OK,
1740 "research_load_ss11_lp pass_lp failed"
1741 );
1742 }
1743
1744 #[test]
1750 fn test_research_probe_limit_status_on_ss11_lp() {
1751 use crate::ffi;
1752
1753 let highs = unsafe { ffi::cobre_highs_create() };
1755 assert!(!highs.is_null());
1756 unsafe { ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0) };
1757 unsafe { research_load_ss11_lp(highs) };
1758 let _ = unsafe { ffi::cobre_highs_set_double_option(highs, c"time_limit".as_ptr(), 0.0) };
1759 let run_status = unsafe { ffi::cobre_highs_run(highs) };
1760 let model_status = unsafe { ffi::cobre_highs_get_model_status(highs) };
1761 let obj = unsafe { ffi::cobre_highs_get_objective_value(highs) };
1762 eprintln!(
1763 "SS1.1 + time_limit=0: run_status={run_status}, model_status={model_status}, obj={obj}"
1764 );
1765 unsafe { ffi::cobre_highs_destroy(highs) };
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 {
1773 ffi::cobre_highs_set_int_option(highs, c"simplex_iteration_limit".as_ptr(), 0)
1774 };
1775 let run_status = unsafe { ffi::cobre_highs_run(highs) };
1776 let model_status = unsafe { ffi::cobre_highs_get_model_status(highs) };
1777 let obj = unsafe { ffi::cobre_highs_get_objective_value(highs) };
1778 eprintln!(
1779 "SS1.1 + iteration_limit=0: run_status={run_status}, model_status={model_status}, obj={obj}"
1780 );
1781 unsafe { ffi::cobre_highs_destroy(highs) };
1782 }
1783
1784 unsafe fn research_load_larger_lp(highs: *mut std::os::raw::c_void) {
1804 use crate::ffi;
1805 let col_cost: [f64; 5] = [1.0, 1.0, 1.0, 1.0, 1.0];
1806 let col_lower: [f64; 5] = [0.0; 5];
1807 let col_upper: [f64; 5] = [100.0; 5];
1808 let row_lower: [f64; 4] = [10.0, 8.0, 6.0, 4.0];
1809 let row_upper: [f64; 4] = [f64::INFINITY; 4];
1810 let a_start: [i32; 6] = [0, 1, 3, 5, 7, 8];
1812 let a_index: [i32; 8] = [0, 0, 1, 1, 2, 2, 3, 3];
1813 let a_value: [f64; 8] = [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0];
1814 let status = unsafe {
1816 ffi::cobre_highs_pass_lp(
1817 highs,
1818 5,
1819 4,
1820 8,
1821 ffi::HIGHS_MATRIX_FORMAT_COLWISE,
1822 ffi::HIGHS_OBJ_SENSE_MINIMIZE,
1823 0.0,
1824 col_cost.as_ptr(),
1825 col_lower.as_ptr(),
1826 col_upper.as_ptr(),
1827 row_lower.as_ptr(),
1828 row_upper.as_ptr(),
1829 a_start.as_ptr(),
1830 a_index.as_ptr(),
1831 a_value.as_ptr(),
1832 )
1833 };
1834 assert_eq!(
1835 status,
1836 ffi::HIGHS_STATUS_OK,
1837 "research_load_larger_lp pass_lp failed"
1838 );
1839 }
1840
1841 #[test]
1850 fn test_research_time_limit_zero_triggers_time_limit_status() {
1851 use crate::ffi;
1852
1853 let highs = unsafe { ffi::cobre_highs_create() };
1854 assert!(!highs.is_null());
1855 unsafe { ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0) };
1856 unsafe { research_load_larger_lp(highs) };
1857
1858 let opt_status =
1859 unsafe { ffi::cobre_highs_set_double_option(highs, c"time_limit".as_ptr(), 0.0) };
1860 assert_eq!(opt_status, ffi::HIGHS_STATUS_OK);
1861
1862 let run_status = unsafe { ffi::cobre_highs_run(highs) };
1863 let model_status = unsafe { ffi::cobre_highs_get_model_status(highs) };
1864
1865 eprintln!(
1866 "time_limit=0 on larger LP: run_status={run_status}, model_status={model_status}"
1867 );
1868
1869 assert_eq!(
1870 run_status,
1871 ffi::HIGHS_STATUS_WARNING,
1872 "time_limit=0 must return HIGHS_STATUS_WARNING (1), got {run_status}"
1873 );
1874 assert_eq!(
1875 model_status,
1876 ffi::HIGHS_MODEL_STATUS_TIME_LIMIT,
1877 "time_limit=0 must give MODEL_STATUS_TIME_LIMIT (13), got {model_status}"
1878 );
1879
1880 unsafe { ffi::cobre_highs_destroy(highs) };
1881 }
1882
1883 #[test]
1892 fn test_research_iteration_limit_zero_triggers_iteration_limit_status() {
1893 use crate::ffi;
1894
1895 let highs = unsafe { ffi::cobre_highs_create() };
1896 assert!(!highs.is_null());
1897 unsafe { ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0) };
1898 unsafe { ffi::cobre_highs_set_string_option(highs, c"presolve".as_ptr(), c"off".as_ptr()) };
1900 unsafe { research_load_larger_lp(highs) };
1901
1902 let opt_status = unsafe {
1903 ffi::cobre_highs_set_int_option(highs, c"simplex_iteration_limit".as_ptr(), 0)
1904 };
1905 assert_eq!(opt_status, ffi::HIGHS_STATUS_OK);
1906
1907 let run_status = unsafe { ffi::cobre_highs_run(highs) };
1908 let model_status = unsafe { ffi::cobre_highs_get_model_status(highs) };
1909
1910 eprintln!(
1911 "iteration_limit=0 on larger LP: run_status={run_status}, model_status={model_status}"
1912 );
1913
1914 assert_eq!(
1915 run_status,
1916 ffi::HIGHS_STATUS_WARNING,
1917 "iteration_limit=0 must return HIGHS_STATUS_WARNING (1), got {run_status}"
1918 );
1919 assert_eq!(
1920 model_status,
1921 ffi::HIGHS_MODEL_STATUS_ITERATION_LIMIT,
1922 "iteration_limit=0 must give MODEL_STATUS_ITERATION_LIMIT (14), got {model_status}"
1923 );
1924
1925 unsafe { ffi::cobre_highs_destroy(highs) };
1926 }
1927
1928 #[test]
1934 fn test_research_partial_solution_availability() {
1935 use crate::ffi;
1936
1937 {
1939 let highs = unsafe { ffi::cobre_highs_create() };
1940 assert!(!highs.is_null());
1941 unsafe { ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0) };
1942 unsafe { research_load_larger_lp(highs) };
1943 unsafe { ffi::cobre_highs_set_double_option(highs, c"time_limit".as_ptr(), 0.0) };
1944 unsafe { ffi::cobre_highs_run(highs) };
1945
1946 let obj = unsafe { ffi::cobre_highs_get_objective_value(highs) };
1947 let model_status = unsafe { ffi::cobre_highs_get_model_status(highs) };
1948 assert_eq!(model_status, ffi::HIGHS_MODEL_STATUS_TIME_LIMIT);
1949 eprintln!("TIME_LIMIT: obj={obj}, finite={}", obj.is_finite());
1950 unsafe { ffi::cobre_highs_destroy(highs) };
1951 }
1952
1953 {
1955 let highs = unsafe { ffi::cobre_highs_create() };
1956 assert!(!highs.is_null());
1957 unsafe { ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0) };
1958 unsafe {
1959 ffi::cobre_highs_set_string_option(highs, c"presolve".as_ptr(), c"off".as_ptr())
1960 };
1961 unsafe { research_load_larger_lp(highs) };
1962 unsafe {
1963 ffi::cobre_highs_set_int_option(highs, c"simplex_iteration_limit".as_ptr(), 0)
1964 };
1965 unsafe { ffi::cobre_highs_run(highs) };
1966
1967 let obj = unsafe { ffi::cobre_highs_get_objective_value(highs) };
1968 let model_status = unsafe { ffi::cobre_highs_get_model_status(highs) };
1969 assert_eq!(model_status, ffi::HIGHS_MODEL_STATUS_ITERATION_LIMIT);
1970 eprintln!("ITERATION_LIMIT: obj={obj}, finite={}", obj.is_finite());
1971 unsafe { ffi::cobre_highs_destroy(highs) };
1972 }
1973 }
1974
1975 #[test]
1978 fn test_research_restore_defaults_allows_subsequent_optimal_solve() {
1979 use crate::ffi;
1980
1981 let highs = unsafe { ffi::cobre_highs_create() };
1982 assert!(!highs.is_null());
1983
1984 unsafe { ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0) };
1985
1986 unsafe {
1988 ffi::cobre_highs_set_string_option(highs, c"solver".as_ptr(), c"simplex".as_ptr());
1989 ffi::cobre_highs_set_int_option(highs, c"simplex_strategy".as_ptr(), 4);
1990 ffi::cobre_highs_set_string_option(highs, c"presolve".as_ptr(), c"off".as_ptr());
1991 ffi::cobre_highs_set_string_option(highs, c"parallel".as_ptr(), c"off".as_ptr());
1992 ffi::cobre_highs_set_double_option(
1993 highs,
1994 c"primal_feasibility_tolerance".as_ptr(),
1995 1e-7,
1996 );
1997 ffi::cobre_highs_set_double_option(highs, c"dual_feasibility_tolerance".as_ptr(), 1e-7);
1998 }
1999
2000 let col_cost: [f64; 3] = [0.0, 1.0, 50.0];
2001 let col_lower: [f64; 3] = [0.0, 0.0, 0.0];
2002 let col_upper: [f64; 3] = [10.0, f64::INFINITY, 8.0];
2003 let row_lower: [f64; 2] = [6.0, 14.0];
2004 let row_upper: [f64; 2] = [6.0, 14.0];
2005 let a_start: [i32; 4] = [0, 2, 2, 3];
2006 let a_index: [i32; 3] = [0, 1, 1];
2007 let a_value: [f64; 3] = [1.0, 2.0, 1.0];
2008
2009 unsafe {
2011 ffi::cobre_highs_pass_lp(
2012 highs,
2013 3,
2014 2,
2015 3,
2016 ffi::HIGHS_MATRIX_FORMAT_COLWISE,
2017 ffi::HIGHS_OBJ_SENSE_MINIMIZE,
2018 0.0,
2019 col_cost.as_ptr(),
2020 col_lower.as_ptr(),
2021 col_upper.as_ptr(),
2022 row_lower.as_ptr(),
2023 row_upper.as_ptr(),
2024 a_start.as_ptr(),
2025 a_index.as_ptr(),
2026 a_value.as_ptr(),
2027 );
2028 ffi::cobre_highs_set_int_option(highs, c"simplex_iteration_limit".as_ptr(), 0);
2029 ffi::cobre_highs_run(highs);
2030 }
2031 let status1 = unsafe { ffi::cobre_highs_get_model_status(highs) };
2032 assert_eq!(status1, ffi::HIGHS_MODEL_STATUS_ITERATION_LIMIT);
2033
2034 unsafe {
2036 ffi::cobre_highs_set_string_option(highs, c"solver".as_ptr(), c"simplex".as_ptr());
2037 ffi::cobre_highs_set_int_option(highs, c"simplex_strategy".as_ptr(), 4);
2038 ffi::cobre_highs_set_string_option(highs, c"presolve".as_ptr(), c"off".as_ptr());
2039 ffi::cobre_highs_set_double_option(
2040 highs,
2041 c"primal_feasibility_tolerance".as_ptr(),
2042 1e-7,
2043 );
2044 ffi::cobre_highs_set_double_option(highs, c"dual_feasibility_tolerance".as_ptr(), 1e-7);
2045 ffi::cobre_highs_set_string_option(highs, c"parallel".as_ptr(), c"off".as_ptr());
2046 ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0);
2047 ffi::cobre_highs_set_int_option(highs, c"simplex_iteration_limit".as_ptr(), i32::MAX);
2049 }
2050
2051 unsafe { ffi::cobre_highs_clear_solver(highs) };
2053 unsafe { ffi::cobre_highs_run(highs) };
2054 let status2 = unsafe { ffi::cobre_highs_get_model_status(highs) };
2055 let obj = unsafe { ffi::cobre_highs_get_objective_value(highs) };
2056 assert_eq!(
2057 status2,
2058 ffi::HIGHS_MODEL_STATUS_OPTIMAL,
2059 "after restoring defaults, second solve must be OPTIMAL, got {status2}"
2060 );
2061 assert!(
2062 (obj - 100.0).abs() < 1e-8,
2063 "objective after restore must be 100.0, got {obj}"
2064 );
2065
2066 unsafe { ffi::cobre_highs_destroy(highs) };
2067 }
2068
2069 #[test]
2074 fn test_research_iteration_limit_one_triggers_iteration_limit_status() {
2075 use crate::ffi;
2076
2077 let highs = unsafe { ffi::cobre_highs_create() };
2078 assert!(!highs.is_null());
2079
2080 unsafe { ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0) };
2081
2082 let col_cost: [f64; 3] = [0.0, 1.0, 50.0];
2083 let col_lower: [f64; 3] = [0.0, 0.0, 0.0];
2084 let col_upper: [f64; 3] = [10.0, f64::INFINITY, 8.0];
2085 let row_lower: [f64; 2] = [6.0, 14.0];
2086 let row_upper: [f64; 2] = [6.0, 14.0];
2087 let a_start: [i32; 4] = [0, 2, 2, 3];
2088 let a_index: [i32; 3] = [0, 1, 1];
2089 let a_value: [f64; 3] = [1.0, 2.0, 1.0];
2090
2091 unsafe {
2092 ffi::cobre_highs_pass_lp(
2093 highs,
2094 3,
2095 2,
2096 3,
2097 ffi::HIGHS_MATRIX_FORMAT_COLWISE,
2098 ffi::HIGHS_OBJ_SENSE_MINIMIZE,
2099 0.0,
2100 col_cost.as_ptr(),
2101 col_lower.as_ptr(),
2102 col_upper.as_ptr(),
2103 row_lower.as_ptr(),
2104 row_upper.as_ptr(),
2105 a_start.as_ptr(),
2106 a_index.as_ptr(),
2107 a_value.as_ptr(),
2108 );
2109 ffi::cobre_highs_set_int_option(highs, c"simplex_iteration_limit".as_ptr(), 1);
2110 ffi::cobre_highs_run(highs);
2111 }
2112
2113 let model_status = unsafe { ffi::cobre_highs_get_model_status(highs) };
2114 eprintln!("iteration_limit=1 model_status: {model_status}");
2115 assert!(
2118 model_status == ffi::HIGHS_MODEL_STATUS_ITERATION_LIMIT
2119 || model_status == ffi::HIGHS_MODEL_STATUS_OPTIMAL,
2120 "expected ITERATION_LIMIT or OPTIMAL, got {model_status}"
2121 );
2122
2123 unsafe { ffi::cobre_highs_destroy(highs) };
2124 }
2125}