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; 7] {
84 [
85 DefaultOption {
86 name: c"solver",
87 value: OptionValue::Str(c"simplex"),
88 },
89 DefaultOption {
90 name: c"simplex_strategy",
91 value: OptionValue::Int(4),
92 },
93 DefaultOption {
94 name: c"presolve",
95 value: OptionValue::Str(c"off"),
96 },
97 DefaultOption {
98 name: c"parallel",
99 value: OptionValue::Str(c"off"),
100 },
101 DefaultOption {
102 name: c"output_flag",
103 value: OptionValue::Bool(0),
104 },
105 DefaultOption {
106 name: c"primal_feasibility_tolerance",
107 value: OptionValue::Double(1e-7),
108 },
109 DefaultOption {
110 name: c"dual_feasibility_tolerance",
111 value: OptionValue::Double(1e-7),
112 },
113 ]
114}
115
116pub struct HighsSolver {
133 handle: *mut c_void,
135 col_value: Vec<f64>,
138 col_dual: Vec<f64>,
141 row_value: Vec<f64>,
144 row_dual: Vec<f64>,
147 scratch_i32: Vec<i32>,
151 basis_col_i32: Vec<i32>,
155 basis_row_i32: Vec<i32>,
159 num_cols: usize,
161 num_rows: usize,
163 has_model: bool,
166 stats: SolverStatistics,
169}
170
171unsafe impl Send for HighsSolver {}
179
180impl HighsSolver {
181 pub fn new() -> Result<Self, SolverError> {
205 let handle = unsafe { ffi::cobre_highs_create() };
210
211 if handle.is_null() {
212 return Err(SolverError::InternalError {
213 message: "HiGHS instance creation failed: Highs_create() returned null".to_string(),
214 error_code: None,
215 });
216 }
217
218 if let Err(e) = Self::apply_default_config(handle) {
221 unsafe { ffi::cobre_highs_destroy(handle) };
226 return Err(e);
227 }
228
229 Ok(Self {
230 handle,
231 col_value: Vec::new(),
232 col_dual: Vec::new(),
233 row_value: Vec::new(),
234 row_dual: Vec::new(),
235 scratch_i32: Vec::new(),
236 basis_col_i32: Vec::new(),
237 basis_row_i32: Vec::new(),
238 num_cols: 0,
239 num_rows: 0,
240 has_model: false,
241 stats: SolverStatistics::default(),
242 })
243 }
244
245 fn apply_default_config(handle: *mut c_void) -> Result<(), SolverError> {
251 for opt in &default_options() {
252 let status = unsafe { opt.apply(handle) };
254 if status == ffi::HIGHS_STATUS_ERROR {
255 return Err(SolverError::InternalError {
256 message: format!(
257 "HiGHS configuration failed: {}",
258 opt.name.to_str().unwrap_or("?")
259 ),
260 error_code: Some(status),
261 });
262 }
263 }
264 Ok(())
265 }
266
267 fn extract_solution_view(&mut self, solve_time_seconds: f64) -> SolutionView<'_> {
274 let status = unsafe {
276 ffi::cobre_highs_get_solution(
277 self.handle,
278 self.col_value.as_mut_ptr(),
279 self.col_dual.as_mut_ptr(),
280 self.row_value.as_mut_ptr(),
281 self.row_dual.as_mut_ptr(),
282 )
283 };
284 assert_ne!(
285 status,
286 ffi::HIGHS_STATUS_ERROR,
287 "cobre_highs_get_solution failed after optimal solve"
288 );
289
290 let objective = unsafe { ffi::cobre_highs_get_objective_value(self.handle) };
292
293 #[allow(clippy::cast_sign_loss)]
295 let iterations =
296 unsafe { ffi::cobre_highs_get_simplex_iteration_count(self.handle) } as u64;
297
298 SolutionView {
299 objective,
300 primal: &self.col_value[..self.num_cols],
301 dual: &self.row_dual[..self.num_rows],
302 reduced_costs: &self.col_dual[..self.num_cols],
303 iterations,
304 solve_time_seconds,
305 }
306 }
307
308 fn restore_default_settings(&mut self) {
312 for opt in &default_options() {
313 unsafe { opt.apply(self.handle) };
315 }
316 }
317
318 fn run_once(&mut self) -> i32 {
320 let run_status = unsafe { ffi::cobre_highs_run(self.handle) };
322 if run_status == ffi::HIGHS_STATUS_ERROR {
323 return ffi::HIGHS_MODEL_STATUS_SOLVE_ERROR;
324 }
325 unsafe { ffi::cobre_highs_get_model_status(self.handle) }
327 }
328
329 fn interpret_terminal_status(
334 &mut self,
335 status: i32,
336 solve_time_seconds: f64,
337 ) -> Option<SolverError> {
338 match status {
339 ffi::HIGHS_MODEL_STATUS_OPTIMAL => {
340 None
342 }
343 ffi::HIGHS_MODEL_STATUS_INFEASIBLE => Some(SolverError::Infeasible),
344 ffi::HIGHS_MODEL_STATUS_UNBOUNDED_OR_INFEASIBLE => {
345 let mut has_dual_ray: i32 = 0;
349 let mut dual_buf = vec![0.0_f64; self.num_rows];
352 let dual_status = unsafe {
354 ffi::cobre_highs_get_dual_ray(
355 self.handle,
356 &raw mut has_dual_ray,
357 dual_buf.as_mut_ptr(),
358 )
359 };
360 if dual_status != ffi::HIGHS_STATUS_ERROR && has_dual_ray != 0 {
361 return Some(SolverError::Infeasible);
362 }
363 let mut has_primal_ray: i32 = 0;
364 let mut primal_buf = vec![0.0_f64; self.num_cols];
365 let primal_status = unsafe {
367 ffi::cobre_highs_get_primal_ray(
368 self.handle,
369 &raw mut has_primal_ray,
370 primal_buf.as_mut_ptr(),
371 )
372 };
373 if primal_status != ffi::HIGHS_STATUS_ERROR && has_primal_ray != 0 {
374 return Some(SolverError::Unbounded);
375 }
376 Some(SolverError::Infeasible)
377 }
378 ffi::HIGHS_MODEL_STATUS_UNBOUNDED => Some(SolverError::Unbounded),
379 ffi::HIGHS_MODEL_STATUS_TIME_LIMIT => Some(SolverError::TimeLimitExceeded {
380 elapsed_seconds: solve_time_seconds,
381 }),
382 ffi::HIGHS_MODEL_STATUS_ITERATION_LIMIT => {
383 #[allow(clippy::cast_sign_loss)]
385 let iterations =
386 unsafe { ffi::cobre_highs_get_simplex_iteration_count(self.handle) } as u64;
387 Some(SolverError::IterationLimit { iterations })
388 }
389 ffi::HIGHS_MODEL_STATUS_SOLVE_ERROR | ffi::HIGHS_MODEL_STATUS_UNKNOWN => {
390 None
392 }
393 other => Some(SolverError::InternalError {
394 message: format!("HiGHS returned unexpected model status {other}"),
395 error_code: Some(other),
396 }),
397 }
398 }
399
400 fn convert_to_i32_scratch(&mut self, source: &[usize]) -> &[i32] {
404 if source.len() > self.scratch_i32.len() {
405 self.scratch_i32.resize(source.len(), 0);
406 }
407 for (i, &v) in source.iter().enumerate() {
408 debug_assert!(
409 i32::try_from(v).is_ok(),
410 "usize index {v} overflows i32::MAX at position {i}"
411 );
412 #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
414 {
415 self.scratch_i32[i] = v as i32;
416 }
417 }
418 &self.scratch_i32[..source.len()]
419 }
420}
421
422impl Drop for HighsSolver {
423 fn drop(&mut self) {
424 unsafe { ffi::cobre_highs_destroy(self.handle) };
426 }
427}
428
429impl SolverInterface for HighsSolver {
430 fn name(&self) -> &'static str {
431 "HiGHS"
432 }
433
434 fn load_model(&mut self, template: &StageTemplate) {
435 assert!(
445 i32::try_from(template.num_cols).is_ok(),
446 "num_cols {} overflows i32: LP exceeds HiGHS API limit",
447 template.num_cols
448 );
449 assert!(
450 i32::try_from(template.num_rows).is_ok(),
451 "num_rows {} overflows i32: LP exceeds HiGHS API limit",
452 template.num_rows
453 );
454 assert!(
455 i32::try_from(template.num_nz).is_ok(),
456 "num_nz {} overflows i32: LP exceeds HiGHS API limit",
457 template.num_nz
458 );
459 #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
461 let num_col = template.num_cols as i32;
462 #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
463 let num_row = template.num_rows as i32;
464 #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
465 let num_nz = template.num_nz as i32;
466 let status = unsafe {
467 ffi::cobre_highs_pass_lp(
468 self.handle,
469 num_col,
470 num_row,
471 num_nz,
472 ffi::HIGHS_MATRIX_FORMAT_COLWISE,
473 ffi::HIGHS_OBJ_SENSE_MINIMIZE,
474 0.0, template.objective.as_ptr(),
476 template.col_lower.as_ptr(),
477 template.col_upper.as_ptr(),
478 template.row_lower.as_ptr(),
479 template.row_upper.as_ptr(),
480 template.col_starts.as_ptr(),
481 template.row_indices.as_ptr(),
482 template.values.as_ptr(),
483 )
484 };
485
486 assert_ne!(
487 status,
488 ffi::HIGHS_STATUS_ERROR,
489 "cobre_highs_pass_lp failed with status {status}"
490 );
491
492 self.num_cols = template.num_cols;
493 self.num_rows = template.num_rows;
494 self.has_model = true;
495
496 self.col_value.resize(self.num_cols, 0.0);
499 self.col_dual.resize(self.num_cols, 0.0);
500 self.row_value.resize(self.num_rows, 0.0);
501 self.row_dual.resize(self.num_rows, 0.0);
502
503 self.basis_col_i32.resize(self.num_cols, 0);
506 self.basis_row_i32.resize(self.num_rows, 0);
507 }
508
509 fn add_rows(&mut self, cuts: &RowBatch) {
510 assert!(
511 i32::try_from(cuts.num_rows).is_ok(),
512 "cuts.num_rows {} overflows i32: RowBatch exceeds HiGHS API limit",
513 cuts.num_rows
514 );
515 assert!(
516 i32::try_from(cuts.col_indices.len()).is_ok(),
517 "cuts nnz {} overflows i32: RowBatch exceeds HiGHS API limit",
518 cuts.col_indices.len()
519 );
520 #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
522 let num_new_row = cuts.num_rows as i32;
523 #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
524 let num_new_nz = cuts.col_indices.len() as i32;
525
526 let status = unsafe {
534 ffi::cobre_highs_add_rows(
535 self.handle,
536 num_new_row,
537 cuts.row_lower.as_ptr(),
538 cuts.row_upper.as_ptr(),
539 num_new_nz,
540 cuts.row_starts.as_ptr(),
541 cuts.col_indices.as_ptr(),
542 cuts.values.as_ptr(),
543 )
544 };
545
546 assert_ne!(
547 status,
548 ffi::HIGHS_STATUS_ERROR,
549 "cobre_highs_add_rows failed with status {status}"
550 );
551
552 self.num_rows += cuts.num_rows;
553
554 self.row_value.resize(self.num_rows, 0.0);
556 self.row_dual.resize(self.num_rows, 0.0);
557
558 self.basis_row_i32.resize(self.num_rows, 0);
560 }
561
562 fn set_row_bounds(&mut self, indices: &[usize], lower: &[f64], upper: &[f64]) {
563 assert!(
564 indices.len() == lower.len() && indices.len() == upper.len(),
565 "set_row_bounds: indices ({}), lower ({}), and upper ({}) must have equal length",
566 indices.len(),
567 lower.len(),
568 upper.len()
569 );
570 if indices.is_empty() {
571 return;
572 }
573
574 assert!(
575 i32::try_from(indices.len()).is_ok(),
576 "set_row_bounds: indices.len() {} overflows i32",
577 indices.len()
578 );
579 #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
580 let num_entries = indices.len() as i32;
581
582 let status = unsafe {
589 ffi::cobre_highs_change_rows_bounds_by_set(
590 self.handle,
591 num_entries,
592 self.convert_to_i32_scratch(indices).as_ptr(),
593 lower.as_ptr(),
594 upper.as_ptr(),
595 )
596 };
597
598 assert_ne!(
599 status,
600 ffi::HIGHS_STATUS_ERROR,
601 "cobre_highs_change_rows_bounds_by_set failed with status {status}"
602 );
603 }
604
605 fn set_col_bounds(&mut self, indices: &[usize], lower: &[f64], upper: &[f64]) {
606 assert!(
607 indices.len() == lower.len() && indices.len() == upper.len(),
608 "set_col_bounds: indices ({}), lower ({}), and upper ({}) must have equal length",
609 indices.len(),
610 lower.len(),
611 upper.len()
612 );
613 if indices.is_empty() {
614 return;
615 }
616
617 assert!(
618 i32::try_from(indices.len()).is_ok(),
619 "set_col_bounds: indices.len() {} overflows i32",
620 indices.len()
621 );
622 #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
623 let num_entries = indices.len() as i32;
624
625 let status = unsafe {
631 ffi::cobre_highs_change_cols_bounds_by_set(
632 self.handle,
633 num_entries,
634 self.convert_to_i32_scratch(indices).as_ptr(),
635 lower.as_ptr(),
636 upper.as_ptr(),
637 )
638 };
639
640 assert_ne!(
641 status,
642 ffi::HIGHS_STATUS_ERROR,
643 "cobre_highs_change_cols_bounds_by_set failed with status {status}"
644 );
645 }
646
647 #[allow(clippy::too_many_lines)]
648 fn solve(&mut self) -> Result<SolutionView<'_>, SolverError> {
649 assert!(
650 self.has_model,
651 "solve called without a loaded model — call load_model first"
652 );
653 let t0 = Instant::now();
654 let model_status = self.run_once();
655 let solve_time = t0.elapsed().as_secs_f64();
656
657 self.stats.solve_count += 1;
658
659 if model_status == ffi::HIGHS_MODEL_STATUS_OPTIMAL {
660 #[allow(clippy::cast_sign_loss)]
665 let iterations =
666 unsafe { ffi::cobre_highs_get_simplex_iteration_count(self.handle) } as u64;
667 self.stats.success_count += 1;
668 self.stats.total_iterations += iterations;
669 self.stats.total_solve_time_seconds += solve_time;
670 return Ok(self.extract_solution_view(solve_time));
671 }
672
673 if let Some(terminal_err) = self.interpret_terminal_status(model_status, solve_time) {
675 self.stats.failure_count += 1;
676 return Err(terminal_err);
677 }
678
679 let mut retry_attempts: u64 = 0;
683 let mut terminal_err: Option<SolverError> = None;
687 let mut found_optimal = false;
688 let mut optimal_time = 0.0_f64;
689 let mut optimal_iterations: u64 = 0;
690
691 for level in 0..5_u32 {
692 match level {
695 0 => {
696 unsafe { ffi::cobre_highs_clear_solver(self.handle) };
697 }
698 1 => unsafe {
699 ffi::cobre_highs_set_string_option(
700 self.handle,
701 c"presolve".as_ptr(),
702 c"on".as_ptr(),
703 );
704 },
705 2 => unsafe {
706 ffi::cobre_highs_set_int_option(self.handle, c"simplex_strategy".as_ptr(), 1);
707 },
708 3 => unsafe {
709 ffi::cobre_highs_set_double_option(
710 self.handle,
711 c"primal_feasibility_tolerance".as_ptr(),
712 1e-6,
713 );
714 ffi::cobre_highs_set_double_option(
715 self.handle,
716 c"dual_feasibility_tolerance".as_ptr(),
717 1e-6,
718 );
719 },
720 4 => unsafe {
721 ffi::cobre_highs_set_string_option(
722 self.handle,
723 c"solver".as_ptr(),
724 c"ipm".as_ptr(),
725 );
726 },
727 _ => unreachable!(),
728 }
729
730 retry_attempts += 1;
731
732 let t_retry = Instant::now();
733 let retry_status = self.run_once();
734 let retry_time = t_retry.elapsed().as_secs_f64();
735
736 if retry_status == ffi::HIGHS_MODEL_STATUS_OPTIMAL {
737 #[allow(clippy::cast_sign_loss)]
740 let iters =
741 unsafe { ffi::cobre_highs_get_simplex_iteration_count(self.handle) } as u64;
742 found_optimal = true;
743 optimal_time = retry_time;
744 optimal_iterations = iters;
745 break;
746 }
747
748 if let Some(e) = self.interpret_terminal_status(retry_status, retry_time) {
749 terminal_err = Some(e);
750 break;
751 }
752 }
754
755 self.restore_default_settings();
757
758 self.stats.retry_count += retry_attempts;
760
761 if found_optimal {
762 self.stats.success_count += 1;
763 self.stats.total_iterations += optimal_iterations;
764 self.stats.total_solve_time_seconds += optimal_time;
765 return Ok(self.extract_solution_view(optimal_time));
766 }
767
768 self.stats.failure_count += 1;
769 Err(terminal_err.unwrap_or_else(|| {
770 SolverError::NumericalDifficulty {
772 message: "HiGHS failed to reach optimality after all 5 retry escalation levels"
773 .to_string(),
774 }
775 }))
776 }
777
778 fn reset(&mut self) {
779 let status = unsafe { ffi::cobre_highs_clear_solver(self.handle) };
784 debug_assert_ne!(
785 status,
786 ffi::HIGHS_STATUS_ERROR,
787 "cobre_highs_clear_solver failed — HiGHS internal state may be inconsistent"
788 );
789 self.num_cols = 0;
791 self.num_rows = 0;
792 self.has_model = false;
793 }
796
797 fn get_basis(&mut self, out: &mut crate::types::Basis) {
798 assert!(
799 self.has_model,
800 "get_basis called without a loaded model — call load_model first"
801 );
802
803 out.col_status.resize(self.num_cols, 0);
804 out.row_status.resize(self.num_rows, 0);
805
806 let get_status = unsafe {
812 ffi::cobre_highs_get_basis(
813 self.handle,
814 out.col_status.as_mut_ptr(),
815 out.row_status.as_mut_ptr(),
816 )
817 };
818
819 assert_ne!(
820 get_status,
821 ffi::HIGHS_STATUS_ERROR,
822 "cobre_highs_get_basis failed: basis must exist after a successful solve (programming error)"
823 );
824 }
825
826 fn solve_with_basis(
827 &mut self,
828 basis: &crate::types::Basis,
829 ) -> Result<crate::types::SolutionView<'_>, SolverError> {
830 assert!(
831 self.has_model,
832 "solve_with_basis called without a loaded model — call load_model first"
833 );
834 assert!(
835 basis.col_status.len() == self.num_cols,
836 "basis column count {} does not match LP column count {}",
837 basis.col_status.len(),
838 self.num_cols
839 );
840
841 self.basis_col_i32[..self.num_cols].copy_from_slice(&basis.col_status);
844
845 let basis_rows = basis.row_status.len();
849 let lp_rows = self.num_rows;
850 let copy_len = basis_rows.min(lp_rows);
851 self.basis_row_i32[..copy_len].copy_from_slice(&basis.row_status[..copy_len]);
852 if lp_rows > basis_rows {
853 self.basis_row_i32[basis_rows..lp_rows].fill(ffi::HIGHS_BASIS_STATUS_BASIC);
854 }
855
856 let set_status = unsafe {
863 ffi::cobre_highs_set_basis(
864 self.handle,
865 self.basis_col_i32.as_ptr(),
866 self.basis_row_i32.as_ptr(),
867 )
868 };
869
870 if set_status == ffi::HIGHS_STATUS_ERROR {
872 self.stats.basis_rejections += 1;
873 debug_assert!(false, "raw basis rejected; falling back to cold-start");
874 }
875
876 self.solve()
878 }
879
880 fn statistics(&self) -> SolverStatistics {
881 self.stats.clone()
882 }
883}
884
885#[cfg(feature = "test-support")]
891impl HighsSolver {
892 #[must_use]
900 pub fn raw_handle(&self) -> *mut std::os::raw::c_void {
901 self.handle
902 }
903}
904
905#[cfg(test)]
906mod tests {
907 use super::HighsSolver;
908 use crate::{
909 SolverInterface,
910 types::{Basis, RowBatch, StageTemplate},
911 };
912
913 fn make_fixture_stage_template() -> StageTemplate {
926 StageTemplate {
927 num_cols: 3,
928 num_rows: 2,
929 num_nz: 3,
930 col_starts: vec![0_i32, 2, 2, 3],
931 row_indices: vec![0_i32, 1, 1],
932 values: vec![1.0, 2.0, 1.0],
933 col_lower: vec![0.0, 0.0, 0.0],
934 col_upper: vec![10.0, f64::INFINITY, 8.0],
935 objective: vec![0.0, 1.0, 50.0],
936 row_lower: vec![6.0, 14.0],
937 row_upper: vec![6.0, 14.0],
938 n_state: 1,
939 n_transfer: 0,
940 n_dual_relevant: 1,
941 n_hydro: 1,
942 max_par_order: 0,
943 }
944 }
945
946 fn make_fixture_row_batch() -> RowBatch {
950 RowBatch {
951 num_rows: 2,
952 row_starts: vec![0_i32, 2, 4],
953 col_indices: vec![0_i32, 1, 0, 1],
954 values: vec![-5.0, 1.0, 3.0, 1.0],
955 row_lower: vec![20.0, 80.0],
956 row_upper: vec![f64::INFINITY, f64::INFINITY],
957 }
958 }
959
960 #[test]
961 fn test_highs_solver_create_and_name() {
962 let solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
963 assert_eq!(solver.name(), "HiGHS");
964 }
966
967 #[test]
968 fn test_highs_solver_send_bound() {
969 fn assert_send<T: Send>() {}
970 assert_send::<HighsSolver>();
971 }
972
973 #[test]
974 fn test_highs_solver_statistics_initial() {
975 let solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
976 let stats = solver.statistics();
977 assert_eq!(stats.solve_count, 0);
978 assert_eq!(stats.success_count, 0);
979 assert_eq!(stats.failure_count, 0);
980 assert_eq!(stats.total_iterations, 0);
981 assert_eq!(stats.retry_count, 0);
982 assert_eq!(stats.total_solve_time_seconds, 0.0);
983 }
984
985 #[test]
986 fn test_highs_load_model_updates_dimensions() {
987 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
988 let template = make_fixture_stage_template();
989
990 solver.load_model(&template);
991
992 assert_eq!(solver.num_cols, 3, "num_cols must be 3 after load_model");
993 assert_eq!(solver.num_rows, 2, "num_rows must be 2 after load_model");
994 assert_eq!(
995 solver.col_value.len(),
996 3,
997 "col_value buffer must be resized to num_cols"
998 );
999 assert_eq!(
1000 solver.col_dual.len(),
1001 3,
1002 "col_dual buffer must be resized to num_cols"
1003 );
1004 assert_eq!(
1005 solver.row_value.len(),
1006 2,
1007 "row_value buffer must be resized to num_rows"
1008 );
1009 assert_eq!(
1010 solver.row_dual.len(),
1011 2,
1012 "row_dual buffer must be resized to num_rows"
1013 );
1014 }
1015
1016 #[test]
1017 fn test_highs_add_rows_updates_dimensions() {
1018 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1019 let template = make_fixture_stage_template();
1020 let cuts = make_fixture_row_batch();
1021
1022 solver.load_model(&template);
1023 solver.add_rows(&cuts);
1024
1025 assert_eq!(solver.num_rows, 4, "num_rows must be 4 after add_rows");
1027 assert_eq!(
1028 solver.row_dual.len(),
1029 4,
1030 "row_dual buffer must be resized to 4 after add_rows"
1031 );
1032 assert_eq!(
1033 solver.row_value.len(),
1034 4,
1035 "row_value buffer must be resized to 4 after add_rows"
1036 );
1037 assert_eq!(solver.num_cols, 3, "num_cols must be unchanged by add_rows");
1039 }
1040
1041 #[test]
1042 fn test_highs_set_row_bounds_no_panic() {
1043 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1044 let template = make_fixture_stage_template();
1045 solver.load_model(&template);
1046
1047 solver.set_row_bounds(&[0], &[4.0], &[4.0]);
1049 }
1050
1051 #[test]
1052 fn test_highs_set_col_bounds_no_panic() {
1053 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1054 let template = make_fixture_stage_template();
1055 solver.load_model(&template);
1056
1057 solver.set_col_bounds(&[1], &[10.0], &[f64::INFINITY]);
1059 }
1060
1061 #[test]
1062 fn test_highs_set_bounds_empty_no_panic() {
1063 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1064 let template = make_fixture_stage_template();
1065 solver.load_model(&template);
1066
1067 solver.set_row_bounds(&[], &[], &[]);
1069 solver.set_col_bounds(&[], &[], &[]);
1070 }
1071
1072 #[test]
1075 fn test_highs_solve_basic_lp() {
1076 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1077 let template = make_fixture_stage_template();
1078 solver.load_model(&template);
1079
1080 let solution = solver
1081 .solve()
1082 .expect("solve() must succeed on a feasible LP");
1083
1084 assert!(
1085 (solution.objective - 100.0).abs() < 1e-8,
1086 "objective must be 100.0, got {}",
1087 solution.objective
1088 );
1089 assert_eq!(solution.primal.len(), 3, "primal must have 3 elements");
1090 assert!(
1091 (solution.primal[0] - 6.0).abs() < 1e-8,
1092 "primal[0] (x0) must be 6.0, got {}",
1093 solution.primal[0]
1094 );
1095 assert!(
1096 (solution.primal[1] - 0.0).abs() < 1e-8,
1097 "primal[1] (x1) must be 0.0, got {}",
1098 solution.primal[1]
1099 );
1100 assert!(
1101 (solution.primal[2] - 2.0).abs() < 1e-8,
1102 "primal[2] (x2) must be 2.0, got {}",
1103 solution.primal[2]
1104 );
1105 }
1106
1107 #[test]
1111 fn test_highs_solve_with_cuts() {
1112 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1113 let template = make_fixture_stage_template();
1114 let cuts = make_fixture_row_batch();
1115 solver.load_model(&template);
1116 solver.add_rows(&cuts);
1117
1118 let solution = solver
1119 .solve()
1120 .expect("solve() must succeed on a feasible LP with cuts");
1121
1122 assert!(
1123 (solution.objective - 162.0).abs() < 1e-8,
1124 "objective must be 162.0, got {}",
1125 solution.objective
1126 );
1127 assert!(
1128 (solution.primal[0] - 6.0).abs() < 1e-8,
1129 "primal[0] must be 6.0, got {}",
1130 solution.primal[0]
1131 );
1132 assert!(
1133 (solution.primal[1] - 62.0).abs() < 1e-8,
1134 "primal[1] must be 62.0, got {}",
1135 solution.primal[1]
1136 );
1137 assert!(
1138 (solution.primal[2] - 2.0).abs() < 1e-8,
1139 "primal[2] must be 2.0, got {}",
1140 solution.primal[2]
1141 );
1142 }
1143
1144 #[test]
1147 fn test_highs_solve_after_rhs_patch() {
1148 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1149 let template = make_fixture_stage_template();
1150 let cuts = make_fixture_row_batch();
1151 solver.load_model(&template);
1152 solver.add_rows(&cuts);
1153
1154 solver.set_row_bounds(&[0], &[4.0], &[4.0]);
1156
1157 let solution = solver
1158 .solve()
1159 .expect("solve() must succeed after RHS patch");
1160
1161 assert!(
1162 (solution.objective - 368.0).abs() < 1e-8,
1163 "objective must be 368.0, got {}",
1164 solution.objective
1165 );
1166 }
1167
1168 #[test]
1170 fn test_highs_solve_statistics_increment() {
1171 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1172 let template = make_fixture_stage_template();
1173 solver.load_model(&template);
1174
1175 solver.solve().expect("first solve must succeed");
1176 solver.solve().expect("second solve must succeed");
1177
1178 let stats = solver.statistics();
1179 assert_eq!(stats.solve_count, 2, "solve_count must be 2");
1180 assert_eq!(stats.success_count, 2, "success_count must be 2");
1181 assert_eq!(stats.failure_count, 0, "failure_count must be 0");
1182 assert!(
1183 stats.total_iterations > 0,
1184 "total_iterations must be positive"
1185 );
1186 }
1187
1188 #[test]
1190 fn test_highs_reset_preserves_stats() {
1191 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1192 let template = make_fixture_stage_template();
1193 solver.load_model(&template);
1194 solver.solve().expect("solve must succeed");
1195
1196 let stats_before = solver.statistics();
1197 assert_eq!(
1198 stats_before.solve_count, 1,
1199 "solve_count must be 1 before reset"
1200 );
1201
1202 solver.reset();
1203
1204 let stats_after = solver.statistics();
1205 assert_eq!(
1206 stats_after.solve_count, stats_before.solve_count,
1207 "solve_count must be unchanged after reset"
1208 );
1209 assert_eq!(
1210 stats_after.success_count, stats_before.success_count,
1211 "success_count must be unchanged after reset"
1212 );
1213 assert_eq!(
1214 stats_after.total_iterations, stats_before.total_iterations,
1215 "total_iterations must be unchanged after reset"
1216 );
1217 }
1218
1219 #[test]
1221 fn test_highs_solve_iterations_positive() {
1222 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1223 let template = make_fixture_stage_template();
1224 solver.load_model(&template);
1225
1226 let solution = solver.solve().expect("solve must succeed");
1227 assert!(
1228 solution.iterations > 0,
1229 "iterations must be positive, got {}",
1230 solution.iterations
1231 );
1232 }
1233
1234 #[test]
1236 fn test_highs_solve_time_positive() {
1237 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1238 let template = make_fixture_stage_template();
1239 solver.load_model(&template);
1240
1241 let solution = solver.solve().expect("solve must succeed");
1242 assert!(
1243 solution.solve_time_seconds > 0.0,
1244 "solve_time_seconds must be positive, got {}",
1245 solution.solve_time_seconds
1246 );
1247 }
1248
1249 #[test]
1252 fn test_highs_solve_statistics_single() {
1253 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1254 let template = make_fixture_stage_template();
1255 solver.load_model(&template);
1256
1257 solver.solve().expect("solve must succeed");
1258
1259 let stats = solver.statistics();
1260 assert_eq!(stats.solve_count, 1, "solve_count must be 1");
1261 assert_eq!(stats.success_count, 1, "success_count must be 1");
1262 assert_eq!(stats.failure_count, 0, "failure_count must be 0");
1263 assert!(
1264 stats.total_iterations > 0,
1265 "total_iterations must be positive after a successful solve"
1266 );
1267 }
1268
1269 #[test]
1272 fn test_get_basis_valid_status_codes() {
1273 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1274 let template = make_fixture_stage_template();
1275 solver.load_model(&template);
1276 solver.solve().expect("solve must succeed before get_basis");
1277
1278 let mut basis = Basis::new(0, 0);
1279 solver.get_basis(&mut basis);
1280
1281 for &code in &basis.col_status {
1282 assert!(
1283 (0..=4).contains(&code),
1284 "col_status code {code} is outside valid HiGHS range 0..=4"
1285 );
1286 }
1287 for &code in &basis.row_status {
1288 assert!(
1289 (0..=4).contains(&code),
1290 "row_status code {code} is outside valid HiGHS range 0..=4"
1291 );
1292 }
1293 }
1294
1295 #[test]
1298 fn test_get_basis_resizes_output() {
1299 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1300 let template = make_fixture_stage_template();
1301 solver.load_model(&template);
1302 solver.solve().expect("solve must succeed before get_basis");
1303
1304 let mut basis = Basis::new(0, 0);
1305 assert_eq!(
1306 basis.col_status.len(),
1307 0,
1308 "initial col_status must be empty"
1309 );
1310 assert_eq!(
1311 basis.row_status.len(),
1312 0,
1313 "initial row_status must be empty"
1314 );
1315
1316 solver.get_basis(&mut basis);
1317
1318 assert_eq!(
1319 basis.col_status.len(),
1320 3,
1321 "col_status must be resized to 3 (num_cols of SS1.1)"
1322 );
1323 assert_eq!(
1324 basis.row_status.len(),
1325 2,
1326 "row_status must be resized to 2 (num_rows of SS1.1)"
1327 );
1328 }
1329
1330 #[test]
1333 fn test_solve_with_basis_warm_start() {
1334 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1335 let template = make_fixture_stage_template();
1336 solver.load_model(&template);
1337 solver.solve().expect("cold-start solve must succeed");
1338
1339 let mut basis = Basis::new(0, 0);
1340 solver.get_basis(&mut basis);
1341
1342 solver.load_model(&template);
1344 let result = solver
1345 .solve_with_basis(&basis)
1346 .expect("warm-start solve must succeed");
1347
1348 assert!(
1349 (result.objective - 100.0).abs() < 1e-8,
1350 "warm-start objective must be 100.0, got {}",
1351 result.objective
1352 );
1353 assert!(
1354 result.iterations <= 1,
1355 "warm-start from exact basis must use at most 1 iteration, got {}",
1356 result.iterations
1357 );
1358
1359 let stats = solver.statistics();
1360 assert_eq!(
1361 stats.basis_rejections, 0,
1362 "basis_rejections must be 0 when raw basis is accepted, got {}",
1363 stats.basis_rejections
1364 );
1365 }
1366
1367 #[test]
1371 fn test_solve_with_basis_dimension_mismatch() {
1372 let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
1373 let template = make_fixture_stage_template();
1374 let cuts = make_fixture_row_batch();
1375
1376 solver.load_model(&template);
1378 solver.solve().expect("SS1.1 solve must succeed");
1379 let mut basis = Basis::new(0, 0);
1380 solver.get_basis(&mut basis);
1381 assert_eq!(
1382 basis.row_status.len(),
1383 2,
1384 "captured basis must have 2 row statuses"
1385 );
1386
1387 solver.load_model(&template);
1389 solver.add_rows(&cuts);
1390 assert_eq!(solver.num_rows, 4, "LP must have 4 rows after add_rows");
1391
1392 let result = solver
1394 .solve_with_basis(&basis)
1395 .expect("solve with dimension-mismatched basis must succeed");
1396
1397 assert!(
1398 (result.objective - 162.0).abs() < 1e-8,
1399 "objective with both cuts active must be 162.0, got {}",
1400 result.objective
1401 );
1402 }
1403}
1404
1405#[cfg(test)]
1417#[allow(clippy::doc_markdown)]
1418mod research_tests_ticket_023 {
1419 unsafe fn research_load_ss11_lp(highs: *mut std::os::raw::c_void) {
1430 use crate::ffi;
1431 let col_cost: [f64; 3] = [0.0, 1.0, 50.0];
1432 let col_lower: [f64; 3] = [0.0, 0.0, 0.0];
1433 let col_upper: [f64; 3] = [10.0, f64::INFINITY, 8.0];
1434 let row_lower: [f64; 2] = [6.0, 14.0];
1435 let row_upper: [f64; 2] = [6.0, 14.0];
1436 let a_start: [i32; 4] = [0, 2, 2, 3];
1437 let a_index: [i32; 3] = [0, 1, 1];
1438 let a_value: [f64; 3] = [1.0, 2.0, 1.0];
1439 let status = unsafe {
1441 ffi::cobre_highs_pass_lp(
1442 highs,
1443 3,
1444 2,
1445 3,
1446 ffi::HIGHS_MATRIX_FORMAT_COLWISE,
1447 ffi::HIGHS_OBJ_SENSE_MINIMIZE,
1448 0.0,
1449 col_cost.as_ptr(),
1450 col_lower.as_ptr(),
1451 col_upper.as_ptr(),
1452 row_lower.as_ptr(),
1453 row_upper.as_ptr(),
1454 a_start.as_ptr(),
1455 a_index.as_ptr(),
1456 a_value.as_ptr(),
1457 )
1458 };
1459 assert_eq!(
1460 status,
1461 ffi::HIGHS_STATUS_OK,
1462 "research_load_ss11_lp pass_lp failed"
1463 );
1464 }
1465
1466 #[test]
1472 fn test_research_probe_limit_status_on_ss11_lp() {
1473 use crate::ffi;
1474
1475 let highs = unsafe { ffi::cobre_highs_create() };
1477 assert!(!highs.is_null());
1478 unsafe { ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0) };
1479 unsafe { research_load_ss11_lp(highs) };
1480 let _ = unsafe { ffi::cobre_highs_set_double_option(highs, c"time_limit".as_ptr(), 0.0) };
1481 let run_status = unsafe { ffi::cobre_highs_run(highs) };
1482 let model_status = unsafe { ffi::cobre_highs_get_model_status(highs) };
1483 let obj = unsafe { ffi::cobre_highs_get_objective_value(highs) };
1484 eprintln!(
1485 "SS1.1 + time_limit=0: run_status={run_status}, model_status={model_status}, obj={obj}"
1486 );
1487 unsafe { ffi::cobre_highs_destroy(highs) };
1488
1489 let highs = unsafe { ffi::cobre_highs_create() };
1491 assert!(!highs.is_null());
1492 unsafe { ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0) };
1493 unsafe { research_load_ss11_lp(highs) };
1494 let _ = unsafe {
1495 ffi::cobre_highs_set_int_option(highs, c"simplex_iteration_limit".as_ptr(), 0)
1496 };
1497 let run_status = unsafe { ffi::cobre_highs_run(highs) };
1498 let model_status = unsafe { ffi::cobre_highs_get_model_status(highs) };
1499 let obj = unsafe { ffi::cobre_highs_get_objective_value(highs) };
1500 eprintln!(
1501 "SS1.1 + iteration_limit=0: run_status={run_status}, model_status={model_status}, obj={obj}"
1502 );
1503 unsafe { ffi::cobre_highs_destroy(highs) };
1504 }
1505
1506 unsafe fn research_load_larger_lp(highs: *mut std::os::raw::c_void) {
1526 use crate::ffi;
1527 let col_cost: [f64; 5] = [1.0, 1.0, 1.0, 1.0, 1.0];
1528 let col_lower: [f64; 5] = [0.0; 5];
1529 let col_upper: [f64; 5] = [100.0; 5];
1530 let row_lower: [f64; 4] = [10.0, 8.0, 6.0, 4.0];
1531 let row_upper: [f64; 4] = [f64::INFINITY; 4];
1532 let a_start: [i32; 6] = [0, 1, 3, 5, 7, 8];
1534 let a_index: [i32; 8] = [0, 0, 1, 1, 2, 2, 3, 3];
1535 let a_value: [f64; 8] = [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0];
1536 let status = unsafe {
1538 ffi::cobre_highs_pass_lp(
1539 highs,
1540 5,
1541 4,
1542 8,
1543 ffi::HIGHS_MATRIX_FORMAT_COLWISE,
1544 ffi::HIGHS_OBJ_SENSE_MINIMIZE,
1545 0.0,
1546 col_cost.as_ptr(),
1547 col_lower.as_ptr(),
1548 col_upper.as_ptr(),
1549 row_lower.as_ptr(),
1550 row_upper.as_ptr(),
1551 a_start.as_ptr(),
1552 a_index.as_ptr(),
1553 a_value.as_ptr(),
1554 )
1555 };
1556 assert_eq!(
1557 status,
1558 ffi::HIGHS_STATUS_OK,
1559 "research_load_larger_lp pass_lp failed"
1560 );
1561 }
1562
1563 #[test]
1572 fn test_research_time_limit_zero_triggers_time_limit_status() {
1573 use crate::ffi;
1574
1575 let highs = unsafe { ffi::cobre_highs_create() };
1576 assert!(!highs.is_null());
1577 unsafe { ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0) };
1578 unsafe { research_load_larger_lp(highs) };
1579
1580 let opt_status =
1581 unsafe { ffi::cobre_highs_set_double_option(highs, c"time_limit".as_ptr(), 0.0) };
1582 assert_eq!(opt_status, ffi::HIGHS_STATUS_OK);
1583
1584 let run_status = unsafe { ffi::cobre_highs_run(highs) };
1585 let model_status = unsafe { ffi::cobre_highs_get_model_status(highs) };
1586
1587 eprintln!(
1588 "time_limit=0 on larger LP: run_status={run_status}, model_status={model_status}"
1589 );
1590
1591 assert_eq!(
1592 run_status,
1593 ffi::HIGHS_STATUS_WARNING,
1594 "time_limit=0 must return HIGHS_STATUS_WARNING (1), got {run_status}"
1595 );
1596 assert_eq!(
1597 model_status,
1598 ffi::HIGHS_MODEL_STATUS_TIME_LIMIT,
1599 "time_limit=0 must give MODEL_STATUS_TIME_LIMIT (13), got {model_status}"
1600 );
1601
1602 unsafe { ffi::cobre_highs_destroy(highs) };
1603 }
1604
1605 #[test]
1614 fn test_research_iteration_limit_zero_triggers_iteration_limit_status() {
1615 use crate::ffi;
1616
1617 let highs = unsafe { ffi::cobre_highs_create() };
1618 assert!(!highs.is_null());
1619 unsafe { ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0) };
1620 unsafe { ffi::cobre_highs_set_string_option(highs, c"presolve".as_ptr(), c"off".as_ptr()) };
1622 unsafe { research_load_larger_lp(highs) };
1623
1624 let opt_status = unsafe {
1625 ffi::cobre_highs_set_int_option(highs, c"simplex_iteration_limit".as_ptr(), 0)
1626 };
1627 assert_eq!(opt_status, ffi::HIGHS_STATUS_OK);
1628
1629 let run_status = unsafe { ffi::cobre_highs_run(highs) };
1630 let model_status = unsafe { ffi::cobre_highs_get_model_status(highs) };
1631
1632 eprintln!(
1633 "iteration_limit=0 on larger LP: run_status={run_status}, model_status={model_status}"
1634 );
1635
1636 assert_eq!(
1637 run_status,
1638 ffi::HIGHS_STATUS_WARNING,
1639 "iteration_limit=0 must return HIGHS_STATUS_WARNING (1), got {run_status}"
1640 );
1641 assert_eq!(
1642 model_status,
1643 ffi::HIGHS_MODEL_STATUS_ITERATION_LIMIT,
1644 "iteration_limit=0 must give MODEL_STATUS_ITERATION_LIMIT (14), got {model_status}"
1645 );
1646
1647 unsafe { ffi::cobre_highs_destroy(highs) };
1648 }
1649
1650 #[test]
1656 fn test_research_partial_solution_availability() {
1657 use crate::ffi;
1658
1659 {
1661 let highs = unsafe { ffi::cobre_highs_create() };
1662 assert!(!highs.is_null());
1663 unsafe { ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0) };
1664 unsafe { research_load_larger_lp(highs) };
1665 unsafe { ffi::cobre_highs_set_double_option(highs, c"time_limit".as_ptr(), 0.0) };
1666 unsafe { ffi::cobre_highs_run(highs) };
1667
1668 let obj = unsafe { ffi::cobre_highs_get_objective_value(highs) };
1669 let model_status = unsafe { ffi::cobre_highs_get_model_status(highs) };
1670 assert_eq!(model_status, ffi::HIGHS_MODEL_STATUS_TIME_LIMIT);
1671 eprintln!("TIME_LIMIT: obj={obj}, finite={}", obj.is_finite());
1672 unsafe { ffi::cobre_highs_destroy(highs) };
1673 }
1674
1675 {
1677 let highs = unsafe { ffi::cobre_highs_create() };
1678 assert!(!highs.is_null());
1679 unsafe { ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0) };
1680 unsafe {
1681 ffi::cobre_highs_set_string_option(highs, c"presolve".as_ptr(), c"off".as_ptr())
1682 };
1683 unsafe { research_load_larger_lp(highs) };
1684 unsafe {
1685 ffi::cobre_highs_set_int_option(highs, c"simplex_iteration_limit".as_ptr(), 0)
1686 };
1687 unsafe { ffi::cobre_highs_run(highs) };
1688
1689 let obj = unsafe { ffi::cobre_highs_get_objective_value(highs) };
1690 let model_status = unsafe { ffi::cobre_highs_get_model_status(highs) };
1691 assert_eq!(model_status, ffi::HIGHS_MODEL_STATUS_ITERATION_LIMIT);
1692 eprintln!("ITERATION_LIMIT: obj={obj}, finite={}", obj.is_finite());
1693 unsafe { ffi::cobre_highs_destroy(highs) };
1694 }
1695 }
1696
1697 #[test]
1700 fn test_research_restore_defaults_allows_subsequent_optimal_solve() {
1701 use crate::ffi;
1702
1703 let highs = unsafe { ffi::cobre_highs_create() };
1704 assert!(!highs.is_null());
1705
1706 unsafe { ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0) };
1707
1708 unsafe {
1710 ffi::cobre_highs_set_string_option(highs, c"solver".as_ptr(), c"simplex".as_ptr());
1711 ffi::cobre_highs_set_int_option(highs, c"simplex_strategy".as_ptr(), 4);
1712 ffi::cobre_highs_set_string_option(highs, c"presolve".as_ptr(), c"off".as_ptr());
1713 ffi::cobre_highs_set_string_option(highs, c"parallel".as_ptr(), c"off".as_ptr());
1714 ffi::cobre_highs_set_double_option(
1715 highs,
1716 c"primal_feasibility_tolerance".as_ptr(),
1717 1e-7,
1718 );
1719 ffi::cobre_highs_set_double_option(highs, c"dual_feasibility_tolerance".as_ptr(), 1e-7);
1720 }
1721
1722 let col_cost: [f64; 3] = [0.0, 1.0, 50.0];
1723 let col_lower: [f64; 3] = [0.0, 0.0, 0.0];
1724 let col_upper: [f64; 3] = [10.0, f64::INFINITY, 8.0];
1725 let row_lower: [f64; 2] = [6.0, 14.0];
1726 let row_upper: [f64; 2] = [6.0, 14.0];
1727 let a_start: [i32; 4] = [0, 2, 2, 3];
1728 let a_index: [i32; 3] = [0, 1, 1];
1729 let a_value: [f64; 3] = [1.0, 2.0, 1.0];
1730
1731 unsafe {
1733 ffi::cobre_highs_pass_lp(
1734 highs,
1735 3,
1736 2,
1737 3,
1738 ffi::HIGHS_MATRIX_FORMAT_COLWISE,
1739 ffi::HIGHS_OBJ_SENSE_MINIMIZE,
1740 0.0,
1741 col_cost.as_ptr(),
1742 col_lower.as_ptr(),
1743 col_upper.as_ptr(),
1744 row_lower.as_ptr(),
1745 row_upper.as_ptr(),
1746 a_start.as_ptr(),
1747 a_index.as_ptr(),
1748 a_value.as_ptr(),
1749 );
1750 ffi::cobre_highs_set_int_option(highs, c"simplex_iteration_limit".as_ptr(), 0);
1751 ffi::cobre_highs_run(highs);
1752 }
1753 let status1 = unsafe { ffi::cobre_highs_get_model_status(highs) };
1754 assert_eq!(status1, ffi::HIGHS_MODEL_STATUS_ITERATION_LIMIT);
1755
1756 unsafe {
1758 ffi::cobre_highs_set_string_option(highs, c"solver".as_ptr(), c"simplex".as_ptr());
1759 ffi::cobre_highs_set_int_option(highs, c"simplex_strategy".as_ptr(), 4);
1760 ffi::cobre_highs_set_string_option(highs, c"presolve".as_ptr(), c"off".as_ptr());
1761 ffi::cobre_highs_set_double_option(
1762 highs,
1763 c"primal_feasibility_tolerance".as_ptr(),
1764 1e-7,
1765 );
1766 ffi::cobre_highs_set_double_option(highs, c"dual_feasibility_tolerance".as_ptr(), 1e-7);
1767 ffi::cobre_highs_set_string_option(highs, c"parallel".as_ptr(), c"off".as_ptr());
1768 ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0);
1769 ffi::cobre_highs_set_int_option(highs, c"simplex_iteration_limit".as_ptr(), i32::MAX);
1771 }
1772
1773 unsafe { ffi::cobre_highs_clear_solver(highs) };
1775 unsafe { ffi::cobre_highs_run(highs) };
1776 let status2 = unsafe { ffi::cobre_highs_get_model_status(highs) };
1777 let obj = unsafe { ffi::cobre_highs_get_objective_value(highs) };
1778 assert_eq!(
1779 status2,
1780 ffi::HIGHS_MODEL_STATUS_OPTIMAL,
1781 "after restoring defaults, second solve must be OPTIMAL, got {status2}"
1782 );
1783 assert!(
1784 (obj - 100.0).abs() < 1e-8,
1785 "objective after restore must be 100.0, got {obj}"
1786 );
1787
1788 unsafe { ffi::cobre_highs_destroy(highs) };
1789 }
1790
1791 #[test]
1796 fn test_research_iteration_limit_one_triggers_iteration_limit_status() {
1797 use crate::ffi;
1798
1799 let highs = unsafe { ffi::cobre_highs_create() };
1800 assert!(!highs.is_null());
1801
1802 unsafe { ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0) };
1803
1804 let col_cost: [f64; 3] = [0.0, 1.0, 50.0];
1805 let col_lower: [f64; 3] = [0.0, 0.0, 0.0];
1806 let col_upper: [f64; 3] = [10.0, f64::INFINITY, 8.0];
1807 let row_lower: [f64; 2] = [6.0, 14.0];
1808 let row_upper: [f64; 2] = [6.0, 14.0];
1809 let a_start: [i32; 4] = [0, 2, 2, 3];
1810 let a_index: [i32; 3] = [0, 1, 1];
1811 let a_value: [f64; 3] = [1.0, 2.0, 1.0];
1812
1813 unsafe {
1814 ffi::cobre_highs_pass_lp(
1815 highs,
1816 3,
1817 2,
1818 3,
1819 ffi::HIGHS_MATRIX_FORMAT_COLWISE,
1820 ffi::HIGHS_OBJ_SENSE_MINIMIZE,
1821 0.0,
1822 col_cost.as_ptr(),
1823 col_lower.as_ptr(),
1824 col_upper.as_ptr(),
1825 row_lower.as_ptr(),
1826 row_upper.as_ptr(),
1827 a_start.as_ptr(),
1828 a_index.as_ptr(),
1829 a_value.as_ptr(),
1830 );
1831 ffi::cobre_highs_set_int_option(highs, c"simplex_iteration_limit".as_ptr(), 1);
1832 ffi::cobre_highs_run(highs);
1833 }
1834
1835 let model_status = unsafe { ffi::cobre_highs_get_model_status(highs) };
1836 eprintln!("iteration_limit=1 model_status: {model_status}");
1837 assert!(
1840 model_status == ffi::HIGHS_MODEL_STATUS_ITERATION_LIMIT
1841 || model_status == ffi::HIGHS_MODEL_STATUS_OPTIMAL,
1842 "expected ITERATION_LIMIT or OPTIMAL, got {model_status}"
1843 );
1844
1845 unsafe { ffi::cobre_highs_destroy(highs) };
1846 }
1847}