1use crate::error::{OptimizeError, OptimizeResult};
9
10#[derive(Debug, Clone)]
12pub struct BilevelResult {
13 pub x_upper: Vec<f64>,
15 pub y_lower: Vec<f64>,
17 pub upper_fun: f64,
19 pub lower_fun: f64,
21 pub n_outer_iter: usize,
23 pub n_inner_solves: usize,
25 pub nfev: usize,
27 pub success: bool,
29 pub message: String,
31}
32
33#[derive(Debug, Clone)]
35pub struct BilevelSolverOptions {
36 pub max_outer_iter: usize,
38 pub max_inner_iter: usize,
40 pub outer_tol: f64,
42 pub inner_tol: f64,
44 pub verbose: bool,
46}
47
48impl Default for BilevelSolverOptions {
49 fn default() -> Self {
50 BilevelSolverOptions {
51 max_outer_iter: 200,
52 max_inner_iter: 500,
53 outer_tol: 1e-7,
54 inner_tol: 1e-9,
55 verbose: false,
56 }
57 }
58}
59
60pub struct BilevelProblem<F, G>
64where
65 F: Fn(&[f64], &[f64]) -> f64,
66 G: Fn(&[f64], &[f64]) -> f64,
67{
68 pub upper_obj: F,
70 pub lower_obj: G,
72 pub x0: Vec<f64>,
74 pub y0: Vec<f64>,
76 pub upper_constraints: Vec<Box<dyn Fn(&[f64], &[f64]) -> f64>>,
78 pub lower_constraints: Vec<Box<dyn Fn(&[f64], &[f64]) -> f64>>,
80 pub x_lb: Option<Vec<f64>>,
82 pub x_ub: Option<Vec<f64>>,
84 pub y_lb: Option<Vec<f64>>,
86 pub y_ub: Option<Vec<f64>>,
88}
89
90impl<F, G> BilevelProblem<F, G>
91where
92 F: Fn(&[f64], &[f64]) -> f64,
93 G: Fn(&[f64], &[f64]) -> f64,
94{
95 pub fn new(upper_obj: F, lower_obj: G, x0: Vec<f64>, y0: Vec<f64>) -> Self {
97 BilevelProblem {
98 upper_obj,
99 lower_obj,
100 x0,
101 y0,
102 upper_constraints: Vec::new(),
103 lower_constraints: Vec::new(),
104 x_lb: None,
105 x_ub: None,
106 y_lb: None,
107 y_ub: None,
108 }
109 }
110
111 pub fn with_upper_constraint(
113 mut self,
114 constraint: impl Fn(&[f64], &[f64]) -> f64 + 'static,
115 ) -> Self {
116 self.upper_constraints.push(Box::new(constraint));
117 self
118 }
119
120 pub fn with_lower_constraint(
122 mut self,
123 constraint: impl Fn(&[f64], &[f64]) -> f64 + 'static,
124 ) -> Self {
125 self.lower_constraints.push(Box::new(constraint));
126 self
127 }
128
129 pub fn with_x_bounds(mut self, lb: Vec<f64>, ub: Vec<f64>) -> Self {
131 self.x_lb = Some(lb);
132 self.x_ub = Some(ub);
133 self
134 }
135
136 pub fn with_y_bounds(mut self, lb: Vec<f64>, ub: Vec<f64>) -> Self {
138 self.y_lb = Some(lb);
139 self.y_ub = Some(ub);
140 self
141 }
142
143 pub fn eval_upper(&self, x: &[f64], y: &[f64]) -> f64 {
145 (self.upper_obj)(x, y)
146 }
147
148 pub fn eval_lower(&self, x: &[f64], y: &[f64]) -> f64 {
150 (self.lower_obj)(x, y)
151 }
152
153 pub fn upper_constraint_violation(&self, x: &[f64], y: &[f64]) -> f64 {
155 self.upper_constraints
156 .iter()
157 .map(|g| (g(x, y)).max(0.0))
158 .sum()
159 }
160
161 pub fn lower_constraint_violation(&self, x: &[f64], y: &[f64]) -> f64 {
163 self.lower_constraints
164 .iter()
165 .map(|g| (g(x, y)).max(0.0))
166 .sum()
167 }
168
169 pub fn project_y(&self, y: &[f64]) -> Vec<f64> {
171 let n = y.len();
172 let mut yp = y.to_vec();
173 if let Some(ref lb) = self.y_lb {
174 for i in 0..n.min(lb.len()) {
175 if yp[i] < lb[i] {
176 yp[i] = lb[i];
177 }
178 }
179 }
180 if let Some(ref ub) = self.y_ub {
181 for i in 0..n.min(ub.len()) {
182 if yp[i] > ub[i] {
183 yp[i] = ub[i];
184 }
185 }
186 }
187 yp
188 }
189
190 pub fn project_x(&self, x: &[f64]) -> Vec<f64> {
192 let n = x.len();
193 let mut xp = x.to_vec();
194 if let Some(ref lb) = self.x_lb {
195 for i in 0..n.min(lb.len()) {
196 if xp[i] < lb[i] {
197 xp[i] = lb[i];
198 }
199 }
200 }
201 if let Some(ref ub) = self.x_ub {
202 for i in 0..n.min(ub.len()) {
203 if xp[i] > ub[i] {
204 xp[i] = ub[i];
205 }
206 }
207 }
208 xp
209 }
210}
211
212fn solve_lower_level<F, G>(
219 problem: &BilevelProblem<F, G>,
220 x: &[f64],
221 y0: &[f64],
222 options: &BilevelSolverOptions,
223) -> (Vec<f64>, f64, usize)
224where
225 F: Fn(&[f64], &[f64]) -> f64,
226 G: Fn(&[f64], &[f64]) -> f64,
227{
228 let ny = y0.len();
229 let mut y = y0.to_vec();
230 let h = 1e-7f64;
231 let mut nfev = 0usize;
232
233 for _iter in 0..options.max_inner_iter {
234 let f_y = problem.eval_lower(x, &y);
235 nfev += 1;
236
237 let mut grad = vec![0.0f64; ny];
239 for i in 0..ny {
240 let mut yf = y.clone();
241 yf[i] += h;
242 grad[i] = (problem.eval_lower(x, &yf) - f_y) / h;
243 nfev += 1;
244 }
245
246 let gnorm = grad.iter().map(|g| g * g).sum::<f64>().sqrt();
248 if gnorm < options.inner_tol {
249 break;
250 }
251
252 let mut step = 1.0f64;
254 let c1 = 1e-4;
255 let mut y_new = vec![0.0f64; ny];
256 for _ls in 0..50 {
257 for i in 0..ny {
258 y_new[i] = y[i] - step * grad[i];
259 }
260 y_new = problem.project_y(&y_new);
261 let f_new = problem.eval_lower(x, &y_new);
262 nfev += 1;
263 let descent: f64 = y_new
265 .iter()
266 .zip(y.iter())
267 .zip(grad.iter())
268 .map(|((yn, yo), g)| g * (yo - yn))
269 .sum();
270 if f_new <= f_y - c1 * descent.abs() {
271 break;
272 }
273 step *= 0.5;
274 }
275 let improvement = (f_y - problem.eval_lower(x, &y_new)).abs();
276 nfev += 1;
277 y = y_new;
278 if improvement < options.inner_tol * (1.0 + f_y.abs()) {
279 break;
280 }
281 }
282
283 let f_final = problem.eval_lower(x, &y);
284 nfev += 1;
285 (y, f_final, nfev)
286}
287
288#[derive(Debug, Clone)]
294pub struct PsoaOptions {
295 pub solver: BilevelSolverOptions,
297 pub initial_penalty: f64,
299 pub penalty_growth: f64,
301 pub max_penalty: f64,
303 pub upper_step: f64,
305 pub step_shrink: f64,
307}
308
309impl Default for PsoaOptions {
310 fn default() -> Self {
311 PsoaOptions {
312 solver: BilevelSolverOptions::default(),
313 initial_penalty: 1.0,
314 penalty_growth: 2.0,
315 max_penalty: 1e8,
316 upper_step: 0.1,
317 step_shrink: 0.5,
318 }
319 }
320}
321
322pub fn solve_bilevel_psoa<F, G>(
344 problem: BilevelProblem<F, G>,
345 options: PsoaOptions,
346) -> OptimizeResult<BilevelResult>
347where
348 F: Fn(&[f64], &[f64]) -> f64,
349 G: Fn(&[f64], &[f64]) -> f64,
350{
351 let nx = problem.x0.len();
352 let ny = problem.y0.len();
353 let h = 1e-7f64;
354
355 if nx == 0 || ny == 0 {
356 return Err(OptimizeError::InvalidInput(
357 "Upper and lower variable vectors must be non-empty".to_string(),
358 ));
359 }
360
361 let mut x = problem.x0.clone();
362 let mut y = problem.y0.clone();
363 let mut rho = options.initial_penalty;
364 let mut n_outer = 0usize;
365 let mut n_inner = 0usize;
366 let mut total_nfev = 0usize;
367
368 let lower_grad_y = |x: &[f64], y: &[f64], nfev: &mut usize| -> Vec<f64> {
370 let f0 = problem.eval_lower(x, y);
371 *nfev += 1;
372 let mut grad = vec![0.0f64; ny];
373 for i in 0..ny {
374 let mut yf = y.to_vec();
375 yf[i] += h;
376 grad[i] = (problem.eval_lower(x, &yf) - f0) / h;
377 *nfev += 1;
378 }
379 grad
380 };
381
382 let penalized_obj = |x: &[f64], y: &[f64], rho: f64, nfev: &mut usize| -> f64 {
384 let f_upper = problem.eval_upper(x, y);
385 *nfev += 1;
386 let grad_y = lower_grad_y(x, y, nfev);
387 let gnorm_sq: f64 = grad_y.iter().map(|g| g * g).sum();
388 f_upper + rho * gnorm_sq
389 };
390
391 let mut f_prev = penalized_obj(&x, &y, rho, &mut total_nfev);
392
393 for outer in 0..options.solver.max_outer_iter {
394 n_outer = outer + 1;
395
396 let (y_new, _lower_f, inner_nfev) = solve_lower_level(&problem, &x, &y, &options.solver);
398 n_inner += 1;
399 total_nfev += inner_nfev;
400 y = y_new;
401
402 let mut grad_x = vec![0.0f64; nx];
404 let f_cur = penalized_obj(&x, &y, rho, &mut total_nfev);
405 for i in 0..nx {
406 let mut xf = x.clone();
407 xf[i] += h;
408 let f_fwd = penalized_obj(&xf, &y, rho, &mut total_nfev);
409 grad_x[i] = (f_fwd - f_cur) / h;
410 }
411
412 let step = options.upper_step;
414 let mut x_new = vec![0.0f64; nx];
415 for i in 0..nx {
416 x_new[i] = x[i] - step * grad_x[i];
417 }
418 x_new = problem.project_x(&x_new);
419
420 let f_new = penalized_obj(&x_new, &y, rho, &mut total_nfev);
422 if f_new < f_cur {
423 x = x_new;
424 } else {
425 let mut s = step * options.step_shrink;
427 let mut improved = false;
428 for _ in 0..20 {
429 let mut xt = vec![0.0f64; nx];
430 for i in 0..nx {
431 xt[i] = x[i] - s * grad_x[i];
432 }
433 xt = problem.project_x(&xt);
434 let ft = penalized_obj(&xt, &y, rho, &mut total_nfev);
435 if ft < f_cur {
436 x = xt;
437 improved = true;
438 break;
439 }
440 s *= options.step_shrink;
441 }
442 if !improved {
443 rho = (rho * options.penalty_growth).min(options.max_penalty);
445 }
446 }
447
448 rho = (rho * options.penalty_growth).min(options.max_penalty);
450
451 let f_now = penalized_obj(&x, &y, rho, &mut total_nfev);
453 let delta = (f_now - f_prev).abs();
454 if delta < options.solver.outer_tol * (1.0 + f_prev.abs()) {
455 break;
456 }
457 f_prev = f_now;
458 }
459
460 let upper_fun = problem.eval_upper(&x, &y);
461 let lower_fun = problem.eval_lower(&x, &y);
462 total_nfev += 2;
463
464 let grad_y_final = lower_grad_y(&x, &y, &mut total_nfev);
466 let gnorm: f64 = grad_y_final.iter().map(|g| g * g).sum::<f64>().sqrt();
467 let success =
468 gnorm < options.solver.outer_tol.sqrt() || n_outer < options.solver.max_outer_iter;
469
470 Ok(BilevelResult {
471 x_upper: x,
472 y_lower: y,
473 upper_fun,
474 lower_fun,
475 n_outer_iter: n_outer,
476 n_inner_solves: n_inner,
477 nfev: total_nfev,
478 success,
479 message: if success {
480 "PSOA converged".to_string()
481 } else {
482 "PSOA reached maximum iterations".to_string()
483 },
484 })
485}
486
487pub struct ReplacementAlgorithm {
497 pub options: BilevelSolverOptions,
499 pub upper_step: f64,
501}
502
503impl Default for ReplacementAlgorithm {
504 fn default() -> Self {
505 ReplacementAlgorithm {
506 options: BilevelSolverOptions::default(),
507 upper_step: 0.05,
508 }
509 }
510}
511
512impl ReplacementAlgorithm {
513 pub fn new(options: BilevelSolverOptions, upper_step: f64) -> Self {
515 ReplacementAlgorithm {
516 options,
517 upper_step,
518 }
519 }
520
521 pub fn solve<F, G>(&self, problem: BilevelProblem<F, G>) -> OptimizeResult<BilevelResult>
523 where
524 F: Fn(&[f64], &[f64]) -> f64,
525 G: Fn(&[f64], &[f64]) -> f64,
526 {
527 solve_bilevel_replacement(problem, self.options.clone(), self.upper_step)
528 }
529}
530
531pub fn solve_bilevel_replacement<F, G>(
535 problem: BilevelProblem<F, G>,
536 options: BilevelSolverOptions,
537 upper_step: f64,
538) -> OptimizeResult<BilevelResult>
539where
540 F: Fn(&[f64], &[f64]) -> f64,
541 G: Fn(&[f64], &[f64]) -> f64,
542{
543 let nx = problem.x0.len();
544 let h = 1e-6f64;
545
546 if nx == 0 {
547 return Err(OptimizeError::InvalidInput(
548 "Upper-level variable vector must be non-empty".to_string(),
549 ));
550 }
551
552 let mut x = problem.x0.clone();
553 let mut y = problem.y0.clone();
554 let mut n_outer = 0usize;
555 let mut n_inner = 0usize;
556 let mut total_nfev = 0usize;
557
558 let mut f_prev = {
561 let (ystar, _, nfev) = solve_lower_level(&problem, &x, &y, &options);
562 total_nfev += nfev;
563 n_inner += 1;
564 y = ystar;
565 problem.eval_upper(&x, &y)
566 };
567 total_nfev += 1;
568
569 for outer in 0..options.max_outer_iter {
570 n_outer = outer + 1;
571
572 let mut grad_x = vec![0.0f64; nx];
574 for i in 0..nx {
575 let mut xf = x.clone();
576 xf[i] += h;
577 xf = problem.project_x(&xf);
578 let (yf, _, nfev) = solve_lower_level(&problem, &xf, &y, &options);
579 total_nfev += nfev;
580 n_inner += 1;
581 let f_fwd = problem.eval_upper(&xf, &yf);
582 total_nfev += 1;
583 grad_x[i] = (f_fwd - f_prev) / h;
584 }
585
586 let mut x_new = vec![0.0f64; nx];
588 for i in 0..nx {
589 x_new[i] = x[i] - upper_step * grad_x[i];
590 }
591 x_new = problem.project_x(&x_new);
592
593 let (y_new, _, nfev) = solve_lower_level(&problem, &x_new, &y, &options);
595 total_nfev += nfev;
596 n_inner += 1;
597 let f_new = problem.eval_upper(&x_new, &y_new);
598 total_nfev += 1;
599
600 x = x_new;
602 y = y_new;
603
604 let delta = (f_new - f_prev).abs();
605 if delta < options.outer_tol * (1.0 + f_prev.abs()) {
606 f_prev = f_new;
607 break;
608 }
609 f_prev = f_new;
610 }
611
612 let lower_fun = problem.eval_lower(&x, &y);
613 total_nfev += 1;
614
615 Ok(BilevelResult {
616 x_upper: x,
617 y_lower: y,
618 upper_fun: f_prev,
619 lower_fun,
620 n_outer_iter: n_outer,
621 n_inner_solves: n_inner,
622 nfev: total_nfev,
623 success: n_outer < options.max_outer_iter,
624 message: if n_outer < options.max_outer_iter {
625 "Replacement algorithm converged".to_string()
626 } else {
627 "Replacement algorithm: maximum iterations reached".to_string()
628 },
629 })
630}
631
632pub struct SingleLevelReduction {
649 pub epsilon: f64,
651 pub kkt_penalty: f64,
653 pub options: BilevelSolverOptions,
655}
656
657impl Default for SingleLevelReduction {
658 fn default() -> Self {
659 SingleLevelReduction {
660 epsilon: 1e-4,
661 kkt_penalty: 100.0,
662 options: BilevelSolverOptions::default(),
663 }
664 }
665}
666
667impl SingleLevelReduction {
668 pub fn new(epsilon: f64, kkt_penalty: f64, options: BilevelSolverOptions) -> Self {
670 SingleLevelReduction {
671 epsilon,
672 kkt_penalty,
673 options,
674 }
675 }
676
677 pub fn solve<F, G>(&self, problem: BilevelProblem<F, G>) -> OptimizeResult<BilevelResult>
679 where
680 F: Fn(&[f64], &[f64]) -> f64,
681 G: Fn(&[f64], &[f64]) -> f64,
682 {
683 solve_bilevel_single_level(problem, self.epsilon, self.kkt_penalty, &self.options)
684 }
685}
686
687pub fn solve_bilevel_single_level<F, G>(
693 problem: BilevelProblem<F, G>,
694 epsilon: f64,
695 kkt_penalty: f64,
696 options: &BilevelSolverOptions,
697) -> OptimizeResult<BilevelResult>
698where
699 F: Fn(&[f64], &[f64]) -> f64,
700 G: Fn(&[f64], &[f64]) -> f64,
701{
702 let nx = problem.x0.len();
703 let ny = problem.y0.len();
704 let n_lc = problem.lower_constraints.len();
705 let h = 1e-7f64;
706 let _ = epsilon; let n_total = nx + ny + n_lc;
710 let mut z = vec![0.0f64; n_total];
711 for i in 0..nx {
712 z[i] = problem.x0[i];
713 }
714 for i in 0..ny {
715 z[nx + i] = problem.y0[i];
716 }
717 for i in 0..n_lc {
719 z[nx + ny + i] = 0.0;
720 }
721
722 let combined_obj = |z: &[f64], nfev: &mut usize| -> f64 {
724 let x = &z[0..nx];
725 let y = &z[nx..nx + ny];
726 let mu = &z[nx + ny..n_total];
727
728 let f_upper = problem.eval_upper(x, y);
729 *nfev += 1;
730
731 let f_lower_0 = problem.eval_lower(x, y);
733 *nfev += 1;
734 let mut grad_lower_y = vec![0.0f64; ny];
735 for i in 0..ny {
736 let mut yf = y.to_vec();
737 yf[i] += h;
738 grad_lower_y[i] = (problem.eval_lower(x, &yf) - f_lower_0) / h;
739 *nfev += 1;
740 }
741
742 for (j, constraint) in problem.lower_constraints.iter().enumerate() {
744 let gj0 = constraint(x, y);
745 *nfev += 1;
746 for i in 0..ny {
747 let mut yf = y.to_vec();
748 yf[i] += h;
749 let gj_fwd = constraint(x, &yf);
750 *nfev += 1;
751 grad_lower_y[i] += mu[j] * (gj_fwd - gj0) / h;
752 }
753 }
754
755 let stat_norm_sq: f64 = grad_lower_y.iter().map(|g| g * g).sum();
757
758 let dual_feas: f64 = mu.iter().map(|&mj| (-mj).max(0.0).powi(2)).sum();
760
761 let compl: f64 = problem
763 .lower_constraints
764 .iter()
765 .enumerate()
766 .map(|(j, g)| {
767 *nfev += 1;
768 let gj = g(x, y);
769 (mu[j] * gj).powi(2)
770 })
771 .sum();
772
773 let upper_viol: f64 = problem.upper_constraint_violation(x, y);
775 *nfev += problem.upper_constraints.len();
776
777 f_upper
778 + kkt_penalty * (stat_norm_sq + dual_feas + compl)
779 + kkt_penalty * upper_viol.powi(2)
780 };
781
782 let mut total_nfev = 0usize;
783 let mut f_prev = combined_obj(&z, &mut total_nfev);
784
785 let step0 = 0.01f64;
787 for outer in 0..options.max_outer_iter {
788 let f_cur = combined_obj(&z, &mut total_nfev);
789 let mut grad = vec![0.0f64; n_total];
790 for i in 0..n_total {
791 let mut zf = z.clone();
792 zf[i] += h;
793 let f_fwd = combined_obj(&zf, &mut total_nfev);
794 grad[i] = (f_fwd - f_cur) / h;
795 }
796
797 let gnorm: f64 = grad.iter().map(|g| g * g).sum::<f64>().sqrt();
798 if gnorm < options.outer_tol {
799 break;
800 }
801
802 let mut step = step0;
804 let c1 = 1e-4;
805 let mut z_new = z.clone();
806 let descent = gnorm * gnorm;
807 for _ in 0..40 {
808 for i in 0..n_total {
809 z_new[i] = z[i] - step * grad[i];
810 }
811 for i in 0..n_lc {
813 if z_new[nx + ny + i] < 0.0 {
814 z_new[nx + ny + i] = 0.0;
815 }
816 }
817 let x_proj = problem.project_x(&z_new[0..nx]);
819 let y_proj = problem.project_y(&z_new[nx..nx + ny]);
820 for i in 0..nx {
821 z_new[i] = x_proj[i];
822 }
823 for i in 0..ny {
824 z_new[nx + i] = y_proj[i];
825 }
826
827 let f_new = combined_obj(&z_new, &mut total_nfev);
828 if f_new <= f_cur - c1 * step * descent {
829 break;
830 }
831 step *= 0.5;
832 }
833
834 let f_new = combined_obj(&z_new, &mut total_nfev);
835 let delta = (f_new - f_prev).abs();
836 z = z_new;
837 f_prev = f_new;
838
839 if delta < options.outer_tol * (1.0 + f_prev.abs()) && outer > 5 {
840 break;
841 }
842 }
843
844 let x_sol = z[0..nx].to_vec();
845 let y_sol = z[nx..nx + ny].to_vec();
846 let upper_fun = problem.eval_upper(&x_sol, &y_sol);
847 let lower_fun = problem.eval_lower(&x_sol, &y_sol);
848 total_nfev += 2;
849
850 Ok(BilevelResult {
851 x_upper: x_sol,
852 y_lower: y_sol,
853 upper_fun,
854 lower_fun,
855 n_outer_iter: 0, n_inner_solves: 0,
857 nfev: total_nfev,
858 success: true,
859 message: "Single-level KKT reformulation solved".to_string(),
860 })
861}
862
863#[cfg(test)]
868mod tests {
869 use super::*;
870
871 fn simple_upper(x: &[f64], y: &[f64]) -> f64 {
872 (x[0] - 1.0).powi(2) + (y[0] - 1.0).powi(2)
873 }
874
875 fn simple_lower(_x: &[f64], y: &[f64]) -> f64 {
876 y[0].powi(2)
877 }
878
879 #[test]
880 fn test_psoa_basic() {
881 let problem = BilevelProblem::new(simple_upper, simple_lower, vec![0.0], vec![0.5]);
884 let options = PsoaOptions {
885 solver: BilevelSolverOptions {
886 max_outer_iter: 500,
887 max_inner_iter: 200,
888 outer_tol: 1e-5,
889 inner_tol: 1e-7,
890 verbose: false,
891 },
892 ..Default::default()
893 };
894 let result = solve_bilevel_psoa(problem, options).expect("failed to create result");
895 assert!(
897 (result.y_lower[0]).abs() < 0.1,
898 "y should be near 0, got {}",
899 result.y_lower[0]
900 );
901 }
902
903 #[test]
904 fn test_replacement_basic() {
905 let problem = BilevelProblem::new(simple_upper, simple_lower, vec![0.0], vec![0.5]);
906 let options = BilevelSolverOptions {
907 max_outer_iter: 300,
908 max_inner_iter: 200,
909 outer_tol: 1e-5,
910 inner_tol: 1e-8,
911 verbose: false,
912 };
913 let result =
914 solve_bilevel_replacement(problem, options, 0.05).expect("failed to create result");
915 assert!(
916 (result.y_lower[0]).abs() < 0.1,
917 "y should be near 0, got {}",
918 result.y_lower[0]
919 );
920 }
921
922 #[test]
923 fn test_single_level_basic() {
924 let problem = BilevelProblem::new(simple_upper, simple_lower, vec![0.0], vec![0.5]);
925 let options = BilevelSolverOptions {
926 max_outer_iter: 300,
927 max_inner_iter: 200,
928 outer_tol: 1e-4,
929 inner_tol: 1e-7,
930 verbose: false,
931 };
932 let result = solve_bilevel_single_level(problem, 1e-4, 10.0, &options)
933 .expect("failed to create result");
934 assert!(result.success);
935 }
936
937 #[test]
938 fn test_bilevel_result_fields() {
939 let result = BilevelResult {
940 x_upper: vec![1.0],
941 y_lower: vec![0.0],
942 upper_fun: 1.0,
943 lower_fun: 0.0,
944 n_outer_iter: 10,
945 n_inner_solves: 10,
946 nfev: 100,
947 success: true,
948 message: "test".to_string(),
949 };
950 assert!(result.success);
951 assert_eq!(result.nfev, 100);
952 }
953}