Skip to content

Commit ab9a463

Browse files
committed
feat: expose the optimisation API in the python wrapper (#148)
The Python API does not enforce a particular optimisation routine. At the moment, we support: - LSU - LUS Both are, therefore, present in Python as well.
1 parent 57bde78 commit ab9a463

File tree

8 files changed

+207
-36
lines changed

8 files changed

+207
-36
lines changed

pumpkin-py/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
mod constraints;
22
mod model;
3+
mod optimisation;
34
mod result;
45
mod variables;
56

@@ -33,6 +34,7 @@ fn pumpkin_py(python: Python, m: &Bound<'_, PyModule>) -> PyResult<()> {
3334
m.add_class::<result::Solution>()?;
3435

3536
submodule!(constraints, python, m);
37+
submodule!(optimisation, python, m);
3638

3739
Ok(())
3840
}

pumpkin-py/src/model.rs

Lines changed: 91 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ use std::num::NonZero;
22
use std::path::PathBuf;
33

44
use pumpkin_solver::containers::KeyedVec;
5+
use pumpkin_solver::optimisation::linear_sat_unsat::LinearSatUnsat;
6+
use pumpkin_solver::optimisation::linear_unsat_sat::LinearUnsatSat;
7+
use pumpkin_solver::optimisation::OptimisationDirection;
58
use pumpkin_solver::options::SolverOptions;
69
use pumpkin_solver::predicate;
710
use pumpkin_solver::proof::Format;
@@ -14,6 +17,9 @@ use pumpkin_solver::Solver;
1417
use pyo3::prelude::*;
1518

1619
use crate::constraints::Constraint;
20+
use crate::optimisation::Direction;
21+
use crate::optimisation::OptimisationResult;
22+
use crate::optimisation::Optimiser;
1723
use crate::result::SatisfactionResult;
1824
use crate::result::Solution;
1925
use crate::variables::BoolExpression;
@@ -163,27 +169,9 @@ impl Model {
163169

164170
#[pyo3(signature = (proof=None))]
165171
fn satisfy(&self, proof: Option<PathBuf>) -> SatisfactionResult {
166-
let proof_log = proof
167-
.map(|path| ProofLog::cp(&path, Format::Text, true, true))
168-
.transpose()
169-
.map(|proof| proof.unwrap_or_default())
170-
.expect("failed to create proof file");
171-
172-
let options = SolverOptions {
173-
proof_log,
174-
..Default::default()
175-
};
176-
177-
let mut solver = Solver::with_options(options);
178-
179-
let solver_setup = self
180-
.create_variable_map(&mut solver)
181-
.and_then(|variable_map| {
182-
self.post_constraints(&mut solver, &variable_map)?;
183-
Ok(variable_map)
184-
});
172+
let solver_setup = self.create_solver(proof);
185173

186-
let Ok(variable_map) = solver_setup else {
174+
let Ok((mut solver, variable_map)) = solver_setup else {
187175
return SatisfactionResult::Unsatisfiable();
188176
};
189177

@@ -202,6 +190,62 @@ impl Model {
202190
pumpkin_solver::results::SatisfactionResult::Unknown => SatisfactionResult::Unknown(),
203191
}
204192
}
193+
194+
#[pyo3(signature = (objective, optimiser=Optimiser::LinearSatUnsat, direction=Direction::Minimise, proof=None))]
195+
fn optimise(
196+
&self,
197+
objective: IntExpression,
198+
optimiser: Optimiser,
199+
direction: Direction,
200+
proof: Option<PathBuf>,
201+
) -> OptimisationResult {
202+
let solver_setup = self.create_solver(proof);
203+
204+
let Ok((mut solver, variable_map)) = solver_setup else {
205+
return OptimisationResult::Unsatisfiable();
206+
};
207+
208+
let mut brancher = solver.default_brancher();
209+
210+
let direction = match direction {
211+
Direction::Minimise => OptimisationDirection::Minimise,
212+
Direction::Maximise => OptimisationDirection::Maximise,
213+
};
214+
215+
let objective = objective.to_affine_view(&variable_map);
216+
217+
let result = match optimiser {
218+
Optimiser::LinearSatUnsat => solver.optimise(
219+
&mut brancher,
220+
&mut Indefinite,
221+
LinearSatUnsat::new(direction, objective, |_, _| {}),
222+
),
223+
Optimiser::LinearUnsatSat => solver.optimise(
224+
&mut brancher,
225+
&mut Indefinite,
226+
LinearUnsatSat::new(direction, objective, |_, _| {}),
227+
),
228+
};
229+
230+
match result {
231+
pumpkin_solver::results::OptimisationResult::Satisfiable(solution) => {
232+
OptimisationResult::Satisfiable(Solution {
233+
solver_solution: solution,
234+
variable_map,
235+
})
236+
}
237+
pumpkin_solver::results::OptimisationResult::Optimal(solution) => {
238+
OptimisationResult::Optimal(Solution {
239+
solver_solution: solution,
240+
variable_map,
241+
})
242+
}
243+
pumpkin_solver::results::OptimisationResult::Unsatisfiable => {
244+
OptimisationResult::Unsatisfiable()
245+
}
246+
pumpkin_solver::results::OptimisationResult::Unknown => OptimisationResult::Unknown(),
247+
}
248+
}
205249
}
206250

207251
impl Model {
@@ -252,6 +296,33 @@ impl Model {
252296

253297
Ok(())
254298
}
299+
300+
fn create_solver(
301+
&self,
302+
proof: Option<PathBuf>,
303+
) -> Result<(Solver, VariableMap), ConstraintOperationError> {
304+
let proof_log = proof
305+
.map(|path| ProofLog::cp(&path, Format::Text, true, true))
306+
.transpose()
307+
.map(|proof| proof.unwrap_or_default())
308+
.expect("failed to create proof file");
309+
310+
let options = SolverOptions {
311+
proof_log,
312+
..Default::default()
313+
};
314+
315+
let mut solver = Solver::with_options(options);
316+
317+
let variable_map = self
318+
.create_variable_map(&mut solver)
319+
.and_then(|variable_map| {
320+
self.post_constraints(&mut solver, &variable_map)?;
321+
Ok(variable_map)
322+
})?;
323+
324+
Ok((solver, variable_map))
325+
}
255326
}
256327

257328
#[derive(Clone)]

pumpkin-py/src/optimisation.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
use pyo3::prelude::*;
2+
3+
use crate::result::Solution;
4+
5+
#[pyclass]
6+
pub enum OptimisationResult {
7+
/// The problem was solved to optimality, and the solution is an optimal one.
8+
Optimal(Solution),
9+
/// At least one solution was identified, and the solution is the best one.
10+
Satisfiable(Solution),
11+
/// The problem was unsatisfiable.
12+
Unsatisfiable(),
13+
/// None of the other variants were concluded.
14+
Unknown(),
15+
}
16+
17+
#[pyclass(eq, eq_int)]
18+
#[derive(Clone, Copy, PartialEq, Eq)]
19+
pub enum Optimiser {
20+
LinearSatUnsat,
21+
LinearUnsatSat,
22+
}
23+
24+
#[pyclass(eq, eq_int)]
25+
#[derive(Clone, Copy, PartialEq, Eq)]
26+
pub enum Direction {
27+
Minimise,
28+
Maximise,
29+
}
30+
31+
pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
32+
m.add_class::<Optimiser>()?;
33+
m.add_class::<Direction>()?;
34+
m.add_class::<OptimisationResult>()?;
35+
Ok(())
36+
}

pumpkin-py/tests/test_optimisation.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
from pumpkin_py import Model
2+
from pumpkin_py.optimisation import Direction, OptimisationResult
3+
4+
5+
def test_linear_sat_unsat_minimisation():
6+
model = Model()
7+
8+
objective = model.new_integer_variable(1, 5, name="objective")
9+
10+
result = model.optimise(objective, direction=Direction.Minimise)
11+
12+
assert isinstance(result, OptimisationResult.Optimal)
13+
14+
solution = result._0
15+
assert solution.int_value(objective) == 1
16+
17+
18+
def test_linear_sat_unsat_maximisation():
19+
model = Model()
20+
21+
objective = model.new_integer_variable(1, 5, name="objective")
22+
23+
result = model.optimise(objective, direction=Direction.Maximise)
24+
25+
assert isinstance(result, OptimisationResult.Optimal)
26+
27+
solution = result._0
28+
assert solution.int_value(objective) == 5
29+
30+

pumpkin-solver/src/bin/pumpkin-solver/flatzinc/mod.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ use pumpkin_solver::constraints::cumulative;
1818
use pumpkin_solver::optimisation::linear_sat_unsat::LinearSatUnsat;
1919
use pumpkin_solver::optimisation::linear_unsat_sat::LinearUnsatSat;
2020
use pumpkin_solver::optimisation::OptimisationDirection;
21-
use pumpkin_solver::optimisation::OptimisationProcedure;
2221
use pumpkin_solver::optimisation::OptimisationStrategy;
2322
use pumpkin_solver::options::CumulativeOptions;
2423
use pumpkin_solver::results::solution_iterator::IteratedSolution;

pumpkin-solver/src/optimisation/linear_sat_unsat.rs

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,34 @@ use crate::variables::IntegerVariable;
1212
use crate::ConstraintOperationError;
1313
use crate::Solver;
1414

15+
/// Implements the linear SAT-UNSAT (LSU) optimisation procedure.
1516
#[derive(Debug, Clone, Copy)]
16-
pub struct LinearSatUnsat<Var: IntegerVariable, Callback> {
17+
pub struct LinearSatUnsat<Var, Callback> {
1718
direction: OptimisationDirection,
1819
objective: Var,
1920
solution_callback: Callback,
2021
}
2122

23+
impl<Var, Callback> LinearSatUnsat<Var, Callback>
24+
where
25+
// The trait bound here is not common; see
26+
// linear_unsat_sat for more info.
27+
Callback: Fn(&Solver, SolutionReference),
28+
{
29+
/// Create a new instance of [`LinearSatUnsat`].
30+
pub fn new(
31+
direction: OptimisationDirection,
32+
objective: Var,
33+
solution_callback: Callback,
34+
) -> Self {
35+
Self {
36+
direction,
37+
objective,
38+
solution_callback,
39+
}
40+
}
41+
}
42+
2243
impl<Var: IntegerVariable, Callback> LinearSatUnsat<Var, Callback> {
2344
/// Given the current objective value `best_objective_value`, it adds a constraint specifying
2445
/// that the objective value should be at most `best_objective_value - 1`. Note that it is
@@ -64,14 +85,6 @@ where
6485
Var: IntegerVariable,
6586
Callback: Fn(&Solver, SolutionReference),
6687
{
67-
fn new(direction: OptimisationDirection, objective: Var, solution_callback: Callback) -> Self {
68-
Self {
69-
direction,
70-
objective,
71-
solution_callback,
72-
}
73-
}
74-
7588
fn optimise(
7689
&mut self,
7790
brancher: &mut impl Brancher,

pumpkin-solver/src/optimisation/linear_unsat_sat.rs

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,24 +12,46 @@ use crate::termination::TerminationCondition;
1212
use crate::variables::IntegerVariable;
1313
use crate::Solver;
1414

15+
/// Implements the linear UNSAT-SAT (LUS) optimisation procedure.
1516
#[derive(Debug, Clone, Copy)]
16-
pub struct LinearUnsatSat<Var: IntegerVariable, Callback> {
17+
pub struct LinearUnsatSat<Var, Callback> {
1718
direction: OptimisationDirection,
1819
objective: Var,
1920
solution_callback: Callback,
2021
}
2122

22-
impl<Var: IntegerVariable, Callback: Fn(&Solver, SolutionReference)>
23-
OptimisationProcedure<Var, Callback> for LinearUnsatSat<Var, Callback>
23+
impl<Var, Callback> LinearUnsatSat<Var, Callback>
24+
where
25+
// The trait bound here is contrary to common
26+
// practice; typically the bounds are only enforced
27+
// where they are required (in this case, in the
28+
// implementation of OptimisationProcedure).
29+
//
30+
// However, if we don't have the trait bound here,
31+
// the compiler may implement `FnOnce` for the
32+
// empty closure, which causes problems. So, we
33+
// have the hint here.
34+
//
35+
// Similar is also the case in linear SAT-UNSAT.
36+
Callback: Fn(&Solver, SolutionReference),
2437
{
25-
fn new(direction: OptimisationDirection, objective: Var, solution_callback: Callback) -> Self {
38+
/// Create a new instance of [`LinearUnsatSat`].
39+
pub fn new(
40+
direction: OptimisationDirection,
41+
objective: Var,
42+
solution_callback: Callback,
43+
) -> Self {
2644
Self {
2745
direction,
2846
objective,
2947
solution_callback,
3048
}
3149
}
50+
}
3251

52+
impl<Var: IntegerVariable, Callback: Fn(&Solver, SolutionReference)>
53+
OptimisationProcedure<Var, Callback> for LinearUnsatSat<Var, Callback>
54+
{
3355
fn optimise(
3456
&mut self,
3557
brancher: &mut impl Brancher,

pumpkin-solver/src/optimisation/mod.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@ pub mod linear_sat_unsat;
1515
pub mod linear_unsat_sat;
1616

1717
pub trait OptimisationProcedure<Var: IntegerVariable, Callback: Fn(&Solver, SolutionReference)> {
18-
fn new(direction: OptimisationDirection, objective: Var, solution_callback: Callback) -> Self;
19-
2018
fn optimise(
2119
&mut self,
2220
brancher: &mut impl Brancher,

0 commit comments

Comments
 (0)