ankane / or-tools-ruby

Operations research tools for Ruby
Apache License 2.0
171 stars 20 forks source link

ORTools::SatIntVar can't be coerced into Integer (TypeError) #11

Closed braindeaf closed 3 years ago

braindeaf commented 3 years ago

Hi there,

I'm working on trying to port one Python example to Ruby and I've hit a roadblock. (I'll prefix that by saying this was reasonably smooth up until now). I am not going to pretend I understand how this works but my assumption is everytime you do something like

model.add(->> this is something that is evaluated later <<-)

In my port of a Staff Scheduling solution

https://github.com/braindeaf/staff_scheduling

I am now stuck.

namely, how I would do something like..

    cost_variables = []
    cost_coefficients = []
    sum_var = model.NewIntVar(hard_min, hard_max, '')
    # This adds the hard constraints on the sum.
    model.Add(sum_var == sum(works))

    # Penalize sums below the soft_min target.
    if soft_min > hard_min and min_cost > 0:
        delta = model.NewIntVar(-len(works), len(works), '') 
        model.Add(delta == soft_min - sum_var) # this line right here.
model.add(delta == soft_min - sum_var)

causes

staff_scheduling_sat.rb:286:in `-': ORTools::SatIntVar can't be coerced into Integer (TypeError)

I know that soft_min is an Integer, so it's evaluating now and doesn't make sense. Is there a method for doing that subtraction in a way that is evaluated later?)

Many many thanks in advance :)

RobL

ankane commented 3 years ago

Hey @braindeaf, try rewriting the expression to:

model.add(delta + sum_var == soft_min)

One difference in the Ruby library is ORTools variables must appear on the left side of the expression.

x + 1 # uses ORTools::SatIntVar + method (works)
1 + x # uses Integer + method (doesn't work)

Another way that would ideally work is:

model.add(delta == -sum_var + soft_min)

But it looks like the - method isn't implemented for ORTools::SatIntVar yet. Should work on master.

braindeaf commented 3 years ago

dang! I tried the latter first and it didn't work earlier. If I'd only thought about moving the sum_var to the opposite side of the equation :S

I'll certainly try it out w/the change anyway.

That then revealed

staff_scheduling_sat.rb:286:in add_soft_sum_constraint': undefined methoddelta' for # (NoMethodError)

So I guess 'delta' isn't supported yet. Is it possible to add that too?

Many thanks Andrew.

braindeaf commented 3 years ago

I'm being a spanner, there is no delta method :)

braindeaf commented 3 years ago

Last one, I promise....

  delta = model.new_int_var(-7, 7, '')
  model.add(delta + soft_max == sum_var)
  excess = model.new_int_var(0, 7, prefix + ': over_sum')
  model.add_max_equality(excess, [delta, 0])

staff_scheduling_sat.rb:289:in `add_max_equality': Unable to convert Integer to operations_research::sat::IntVar (ArgumentError)

ankane commented 3 years ago

Ha, no worries, this is really useful for the library, so keep them coming. I don't see an immediate for fix this one, but you should be able to convert the constant to an int var manually for now.

model.add_max_equality(excess, [delta, model.new_int_var(0, 0, "zero")])
# or with master
model.add_max_equality(excess, [delta, model.new_constant(0)])

(I think you can just remove if it's zero though)

Edit 2: Just fyi, added many more constraint building methods on master - add_min_equality, add_bool_and, add_bool_xor, and many others

braindeaf commented 3 years ago

This is looking great so far. I'm sure I've managed to mess up and I need to go back and debug, as it's taking three times as long as python and the solution is giving me differences in the debug output. I can't install latest version yet for some reason (battle for another day). I noticed that it's not possible to access the ObjectiveSolutionPrinter class as yet. Is it possible to add that?

Many thanks.

braindeaf commented 3 years ago

Looks like my example is ported across as much as I can. However, there is a distinct discrepancy I'm struggling with.

The python example stops after 10 seconds, with a FEASIBLE result, presumably some built in default solve time. Upping the max time doesn't seem to get to OPTIMAL, re-running at 5 and now 10 mins....

Solution 31, time = 160.86 s, objective = 52 Solution 32, time = 160.90 s, objective = 49

It seems to stall here.

My Ruby example finishes in 110 seconds with an OPTIMAL result which makes me more than suspicious. Would we expect Ruby to be this performant compared to Python or are strange things afoot?

ankane commented 3 years ago

Just added ObjectiveSolutionPrinter as well as a better inspect method for CpModel. I haven't noticed a difference in runtime between the libraries (most of the work should be in C++ for both).

I'd see if the output matches between the new model.inspect and Python's print(model). Also, make sure you're comparing with version 8.1 of the Python library.

ankane commented 3 years ago

fwiw, I just tried to run it and get the same results between Ruby and Python with master. Here they are after manually stopping after 5 seconds or so.

Edit: Ran it for 10 minutes as well, and they both get to Solution 32, objective = 62.

Python

Solution 0, time = 0.20 s, objective = 248
Solution 1, time = 0.24 s, objective = 226
Solution 2, time = 3.07 s, objective = 213
Solution 3, time = 3.13 s, objective = 204
Solution 4, time = 3.18 s, objective = 195
Solution 5, time = 3.22 s, objective = 191
Solution 6, time = 3.28 s, objective = 182
Solution 7, time = 3.33 s, objective = 164
Solution 8, time = 3.37 s, objective = 162
Solution 9, time = 3.44 s, objective = 157
Solution 10, time = 3.48 s, objective = 156
Solution 11, time = 3.53 s, objective = 153
Solution 12, time = 3.57 s, objective = 152
Solution 13, time = 3.62 s, objective = 149
^C^C pressed 1 times. Interrupting the solver. Press 3 times to force termination.

          M T W T F S S M T W T F S S M T W T F S S 
worker 0: O M M N N O A A A O M A O A A A A A O N A 
worker 1: O M N N A A A A A M M N N O M M A A A O N 
worker 2: M A M A O N O N N A A O A O O N N A A A A 
worker 3: M A N N O M M O A A A O N A A M N N N O M 
worker 4: A A O M M N N O N N O M M M M A O M O N A 
worker 5: A O A M N N O M M O O M N N N O M M M M O 
worker 6: A O O A A A A A M M N N A O M O M N N A O 
worker 7: N N A A M O O M N N A A O A A A M O M N O 

Ruby

Solution 0, time = 0.18 s, objective = 248
Solution 1, time = 0.22 s, objective = 226
Solution 2, time = 3.06 s, objective = 213
Solution 3, time = 3.12 s, objective = 204
Solution 4, time = 3.16 s, objective = 195
Solution 5, time = 3.21 s, objective = 191
Solution 6, time = 3.26 s, objective = 182
Solution 7, time = 3.32 s, objective = 164
Solution 8, time = 3.35 s, objective = 162
Solution 9, time = 3.42 s, objective = 157
Solution 10, time = 3.46 s, objective = 156
Solution 11, time = 3.50 s, objective = 153
Solution 12, time = 3.54 s, objective = 152
Solution 13, time = 3.58 s, objective = 149
^CWARNING: Logging before InitGoogleLogging() is written to STDERR
I0610 19:41:55.818603  5154 sigint.cc:30] ^C pressed 1 times. Interrupting the solver. Press 3 times to force termination.
          M T W T F S S M T W T F S S M T W T F S S 
worker 0: O M M N N O A A A O M A O A A A A A O N A 
worker 1: O M N N A A A A A M M N N O M M A A A O N 
worker 2: M A M A O N O N N A A O A O O N N A A A A 
worker 3: M A N N O M M O A A A O N A A M N N N O M 
worker 4: A A O M M N N O N N O M M M M A O M O N A 
worker 5: A O A M N N O M M O O M N N N O M M M M O 
worker 6: A O O A A A A A M M N N A O M O M N N A O 
worker 7: N N A A M O O M N N A A O A A A M O M N O 
braindeaf commented 3 years ago

It's getting a bit confusing and a little disheartening that earlier I was getting OPTIMAL for one and FEASIBLE for another. It might be because I was flipping between 0.3.0 and now 0.4.0. However, now I'm the same as you when I set the timeout to 15 seconds for both. I'll take what I have and make a PR with these examples in tomorrow :)

Thank you again, this is great progress for us here.