rust-or / rust-lp-modeler

Lp modeler written in Rust
MIT License
96 stars 29 forks source link

Simplify overflows the stack #37

Open remysucre opened 5 years ago

remysucre commented 5 years ago

I have a very long objective function (sum of 1000+ terms) which causes simplify(expr: &LpExpression) -> LpExpression to overflow the stack when adding the objective function to the problem. Similarly solver.run also triggers the overflow. I understand this might not be the standard use case, and there might not be an easy fix. But I'd like to know what the purpose of simplify is, and if there's a way I can manually get my function into its "normal form" so that I can remove the call to simplify.

zappolowski commented 5 years ago

This is caused by simplify not being tail recursive. As a quick workaround you could try is to increase (or even remove) the stack size limit (ulimit).

remysucre commented 5 years ago

Example for replication:

use lp_modeler::dsl::*;

fn main() {
    let mut problem = LpProblem::new("overflow", LpObjective::Minimize);
    let obj_vec: Vec<LpExpression> = {
        (1..2000).map(|n| {
            let v = "v".to_owned() + &n.to_string();
            let bin = LpBinary::new(&v);
            3 * bin
        }).collect()
    };
    problem += obj_vec.sum();
}
remysucre commented 5 years ago

@zappolowski worked like a charm, thanks! For reference, I also needed to set RUST_MIN_STACK in addition to running ulimit.

jcavat commented 5 years ago

Effectively, the call is not tailrec. Even if it was, rust compiler don't optimize it. Right now, one of the problems is we need sometime to replay the simplify call. Sometimes, some expressions are not simplify enough. This is an ugly workaround, I admit.

An improvement I have in mind is to apply the technic we use for the same library for Scala https://github.com/jcavat/scalinea where we fix the problem using different System/Domain :

The DSL (visible API layer) is an AST like ruls-lp-modeler under the form :

Add( Mult(Mult(x,x), 3), 5 )

     Add
     / \
  Mult  5
  /  \
Mult  2
/ \
x x

and a Clause System (hidden layer) that keep a more flatten representation always keeping a list of terms.

In such a layer, Vars is a Map<Symbol, Exponent> and Terms (list of terms) is a Map<Vars, Constant>

The above AST would be transformed to :

Terms( 
    Vars(x -> 2) -> 3, 
    Vars() -> 5
)

It simplifies the operations. Multiplication and addition are just operations on Map.

You can take a look here to have an idea : Expr (DSL) and Terms/Vars (Clauses System)

Don't hesitate to give your opinion about possible refactoring under the same form.

zappolowski commented 5 years ago

Interestingly, that looks quite similar to what I started to implement yesterday

#[derive(Clone, Debug)]
enum Kind {
    Binary,
    Integer(Option<i32>, Option<i32>),
    Continuous(Option<f32>, Option<f32>),
}

#[derive(Clone, Debug)]
pub struct Variable {
    name: String,
    kind: Kind,
}

#[derive(Debug)]
pub struct Expression {
    variables: HashMap<String, (Variable, f64)>,
    constant: f64,
}

as I also ran into stack issues.

It's currently in a really rough state as I've implemented all the numeric operations manually (d'oh ... probably will take a look into impl_ops).

If this looks good to you, I'd try to wrap it up and crate a MR.

Maybe some of more my thoughts for discussion:

jcavat commented 5 years ago

Yes, non-linear equations should simplify. I imagined no degree limit for quadratic solver (such as Gurobi) or even different kind of solver if we use different format (Matrix export in addition to lp_format). Overriding operators would force to check degree at runtime. I totally agree with the objective function evaluation. In my opinion it should be return by the run function :

let (solver_status, values, objective) = solver::run(&prob).unwrap();

I created an issue for this : #38

remysucre commented 4 years ago

Getting back to this, now even when simplify doesn't overflow the stack it simply takes too long. So lmk how I can help with this :)

jcavat commented 4 years ago

With pleasure ! Maybe we should use list of terms instead of a recursive Tree expr.

jcavat commented 3 years ago

It should be fixed by #48, #67 and #68