Skip to content

Commit deb6506

Browse files
authored
Merge pull request #19 from Specy/dev
add time limit, fixes #14
2 parents aacac44 + 914a175 commit deb6506

4 files changed

Lines changed: 108 additions & 4 deletions

File tree

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@ categories = ["mathematics", "science"]
1616
[dependencies]
1717
sprs = { version = "0.11.4", default-features = false }
1818
log = "0.4.11"
19+
web-time = "1.1.0"
1920

2021
[dev-dependencies]
2122
rand = "0.8.5"
2223
rand_pcg = "0.3.1"
23-
env_logger = "0.11.5"
24+
env_logger = "0.11.5"

src/lib.rs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ mod tests;
6161
use solver::Solver;
6262
use sprs::errors::StructureError;
6363

64+
use core::time::Duration;
65+
use web_time::Instant;
66+
6467
/// An enum indicating whether to minimize or maximize objective function.
6568
#[derive(Clone, Copy, Debug, PartialEq)]
6669
pub enum OptimizationDirection {
@@ -218,6 +221,7 @@ pub struct Problem {
218221
var_maxs: Vec<f64>,
219222
var_domains: Vec<VarDomain>,
220223
constraints: Vec<(CsVec, ComparisonOp, f64)>,
224+
time_limit: Option<Duration>,
221225
}
222226

223227
impl std::fmt::Debug for Problem {
@@ -254,9 +258,19 @@ impl Problem {
254258
var_maxs: vec![],
255259
var_domains: vec![],
256260
constraints: vec![],
261+
time_limit: None,
257262
}
258263
}
259264

265+
/// Set a time limit for the solver. If the solver exceeds this duration,
266+
/// it will return [`Error::Limit`].
267+
///
268+
/// The implementation uses [`web_time::Instant`] under the hood, which works
269+
/// on both native and WebAssembly targets.
270+
pub fn set_time_limit(&mut self, duration: Duration) {
271+
self.time_limit = Some(duration);
272+
}
273+
260274
/// Add a new real variable to the problem.
261275
///
262276
/// `obj_coeff` is a coefficient of the term in the objective function corresponding to this
@@ -353,15 +367,18 @@ impl Problem {
353367
///
354368
/// # Errors
355369
///
356-
/// Will return an error, if the problem is infeasible (constraints can't be satisfied)
357-
/// or if the objective value is unbounded.
370+
/// Will return an error, if the problem is infeasible (constraints can't be satisfied),
371+
/// if the objective value is unbounded, or if a time limit was set and exceeded.
358372
pub fn solve(&self) -> Result<Solution, Error> {
373+
let deadline = self.time_limit.map(|d| Instant::now() + d);
374+
359375
let mut solver = Solver::try_new(
360376
&self.obj_coeffs,
361377
&self.var_mins,
362378
&self.var_maxs,
363379
&self.constraints,
364380
&self.var_domains,
381+
deadline,
365382
)?;
366383
solver.initial_solve()?;
367384

src/solver.rs

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ use crate::{
88
};
99
use sprs::CompressedStorage;
1010

11+
use web_time::Instant;
12+
13+
pub(crate) type Deadline = Option<Instant>;
14+
1115
type CsMat = sprs::CsMatI<f64, usize>;
1216

1317
pub(crate) const MACHINE_EPS: f64 = f64::EPSILON * 10.0;
@@ -25,9 +29,20 @@ pub(crate) fn float_ne(a: f64, b: f64) -> bool {
2529
!float_eq(a, b)
2630
}
2731

32+
#[inline]
33+
fn check_deadline(deadline: &Deadline) -> Result<(), Error> {
34+
if let Some(dl) = deadline {
35+
if Instant::now() >= *dl {
36+
return Err(Error::Limit);
37+
}
38+
}
39+
Ok(())
40+
}
41+
2842
#[derive(Clone)]
2943
pub(crate) struct Solver {
3044
pub(crate) num_vars: usize,
45+
pub(crate) deadline: Deadline,
3146

3247
orig_obj_coeffs: Vec<f64>,
3348
orig_var_mins: Vec<f64>,
@@ -138,6 +153,7 @@ impl Solver {
138153
var_maxs: &[f64],
139154
constraints: &[(CsVec, ComparisonOp, f64)],
140155
var_domains: &[VarDomain],
156+
deadline: Deadline,
141157
) -> Result<Self, Error> {
142158
let enable_steepest_edge = true; // TODO: make user-settable.
143159

@@ -356,6 +372,7 @@ impl Solver {
356372
orig_constraints,
357373
orig_constraints_csc,
358374
orig_rhs,
375+
deadline,
359376
orig_var_domains: var_domains.to_vec(),
360377
/*orig_int_vars: var_domains
361378
.to_vec()
@@ -505,6 +522,8 @@ impl Solver {
505522
}
506523

507524
pub(crate) fn initial_solve(&mut self) -> Result<(), Error> {
525+
check_deadline(&self.deadline)?;
526+
508527
if !self.is_primal_feasible {
509528
self.restore_feasibility()?;
510529
}
@@ -549,6 +568,10 @@ impl Solver {
549568
};
550569

551570
for iter in 0.. {
571+
if iter % 100 == 0 {
572+
check_deadline(&self.deadline)?;
573+
}
574+
552575
//guaranteed to have at an element
553576
let cur_step = match dfs_stack.pop() {
554577
Some(step) => step,
@@ -622,6 +645,8 @@ impl Solver {
622645
fn optimize(&mut self) -> Result<(), Error> {
623646
for iter in 0.. {
624647
if iter % 1000 == 0 {
648+
check_deadline(&self.deadline)?;
649+
625650
let (num_vars, infeasibility) = self.calc_dual_infeasibility();
626651
debug!(
627652
"optimize iter {}: obj.: {}, non-optimal coeffs: {} ({})",
@@ -654,6 +679,8 @@ impl Solver {
654679

655680
for iter in 0.. {
656681
if iter % 1000 == 0 {
682+
check_deadline(&self.deadline)?;
683+
657684
let (num_vars, infeasibility) = self.calc_primal_infeasibility();
658685
debug!(
659686
"restore feasibility iter {}: {}: {}, infeas. vars: {} ({})",
@@ -764,7 +791,6 @@ impl Solver {
764791
}
765792
}
766793

767-
768794
self.is_primal_feasible = false;
769795
self.restore_feasibility()
770796
}
@@ -1621,6 +1647,7 @@ mod tests {
16211647
(to_sparse(&[0.0, 1.0]), ComparisonOp::Eq, 3.0),
16221648
],
16231649
&[VarDomain::Real, VarDomain::Real],
1650+
Default::default(),
16241651
)
16251652
.unwrap();
16261653

@@ -1719,6 +1746,7 @@ mod tests {
17191746
(to_sparse(&[-1.0, 4.0]), ComparisonOp::Le, 20.0),
17201747
],
17211748
&[VarDomain::Real, VarDomain::Real],
1749+
Default::default(),
17221750
)
17231751
.unwrap();
17241752
sol.initial_solve().unwrap();
@@ -1742,6 +1770,7 @@ mod tests {
17421770
(to_sparse(&[1.0, 1.0]), ComparisonOp::Le, 5.0),
17431771
],
17441772
&[VarDomain::Real, VarDomain::Real],
1773+
Default::default(),
17451774
)
17461775
.unwrap()
17471776
.initial_solve();

src/tests/general.rs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ mod tests_general {
33
use crate::solver::float_eq;
44
use crate::*;
55

6+
use core::time::Duration;
7+
68
fn init() {
79
let _ = env_logger::builder().is_test(true).try_init();
810
}
@@ -367,6 +369,61 @@ mod tests_general {
367369
);
368370
}
369371

372+
#[test]
373+
fn time_limit_zero_returns_limit_error() {
374+
init();
375+
let mut problem = Problem::new(OptimizationDirection::Maximize);
376+
let x = problem.add_var(1.0, (0.0, f64::INFINITY));
377+
let y = problem.add_var(2.0, (0.0, 3.0));
378+
problem.add_constraint(&[(x, 1.0), (y, 1.0)], ComparisonOp::Le, 4.0);
379+
problem.add_constraint(&[(x, 2.0), (y, 1.0)], ComparisonOp::Ge, 2.0);
380+
381+
// A zero duration guarantees the deadline is already passed before solving starts.
382+
problem.set_time_limit(Duration::ZERO);
383+
let result = problem.solve();
384+
assert_eq!(result.unwrap_err(), Error::Limit);
385+
}
386+
387+
#[test]
388+
fn time_limit_generous_succeeds() {
389+
init();
390+
let mut problem = Problem::new(OptimizationDirection::Maximize);
391+
let x = problem.add_var(1.0, (0.0, f64::INFINITY));
392+
let y = problem.add_var(2.0, (0.0, 3.0));
393+
problem.add_constraint(&[(x, 1.0), (y, 1.0)], ComparisonOp::Le, 4.0);
394+
problem.add_constraint(&[(x, 2.0), (y, 1.0)], ComparisonOp::Ge, 2.0);
395+
396+
// A generous time limit should let a small problem solve without issue.
397+
problem.set_time_limit(Duration::from_secs(60));
398+
let sol = problem.solve().unwrap();
399+
assert_eq!(sol.objective(), 7.0);
400+
assert_eq!(sol[x], 1.0);
401+
assert_eq!(sol[y], 3.0);
402+
}
403+
404+
#[test]
405+
fn time_limit_zero_integer_returns_limit_error() {
406+
init();
407+
let mut problem = Problem::new(OptimizationDirection::Maximize);
408+
let weights = [10, 60, 30, 40, 30, 20, 20, 2];
409+
let values = [1, 10, 15, 40, 60, 90, 100, 15];
410+
let capacity = 102;
411+
let mut vars = vec![];
412+
for i in 0..weights.len() {
413+
let var = problem.add_binary_var(values[i] as f64);
414+
vars.push(var);
415+
}
416+
let entries = vars
417+
.iter()
418+
.map(|v| (*v, weights[v.0] as f64))
419+
.collect::<Vec<_>>();
420+
problem.add_constraint(&entries, ComparisonOp::Le, capacity as f64);
421+
422+
problem.set_time_limit(Duration::ZERO);
423+
let result = problem.solve();
424+
assert_eq!(result.unwrap_err(), Error::Limit);
425+
}
426+
370427
#[test]
371428
fn solve_big_m() {
372429
init();

0 commit comments

Comments
 (0)