1

I'm trying to build a model to optimize steel production. The objective is to reduce the amount of waste (leftover material after the steel has been cut). The code below is what I have so far. It is lacking a constraint to make sure that each item in work_tbl is fulfilled from a single item in inventory_tbl (but several items from work_tbl can be fulfilled by one item in inventory_tbl, given that there is enough length available).

code/reproducible example:

# Load required packages
library(dplyr)
library(ompr)
library(ompr.roi)
library(ROI)


# Define the problem data
work_tbl <- data.frame(length = c(2500, 500, 700, 1200, 1500, 2000, 2500, 3000, 4000, 5250))
inventory_tbl <-
  data.frame(length = c(1300, 2000, 1800, 2600, 3000, 2000, 5000, 6000, 7000, 9000, 2000, 500, 4000, 12000, 7400, 13000))

# Define variables to be used 

work_count <- nrow(work_tbl)
invent_count <- nrow(inventory_tbl)
big_M <- sum(work_tbl$length) * 1.1

# Initialize the model

steel_model <- ompr::MIPModel() %>% 
  
  # Binary decision variable - steel to be cut
  add_variable(steel_cut[work, inventory],
               work = 1:work_count,
               inventory = 1:invent_count,
               type = "binary") %>% 
  
  # Binary decision variable: Take new item from inventory?
  add_variable(take_item[inventory],
               inventory = 1:invent_count,
               type = "binary") %>% 
  
  # Constraint 1: Each item in work_tbl must be cut
  add_constraint(sum_over(steel_cut[work, inventory],
                          inventory = 1:invent_count) == 1,
                 work = 1:work_count)  %>% 
  
  # Constraint 2: The sum of each item used to cut from needs to be equal to or smaller than the work item
  add_constraint(sum_over(steel_cut[work, inventory] * work_tbl$length[work],
                          work = 1:work_count) <= inventory_tbl$length[inventory],
                 inventory = 1:invent_count, work = 1:work_count) %>% 
  
  # Constraint 3: big_M constraint to activate take_item whenever a length is cut
  add_constraint(
    sum_over(steel_cut[work, inventory],
             work = 1:work_count) <= big_M * take_item[inventory],
    inventory = 1:invent_count
  ) %>% 
  
  # Set objective function to minimize scrap / waste 
  set_objective(
    sum_over(
      take_item[inventory] * inventory_tbl$length[inventory],
      inventory = 1:invent_count
    ) - sum_over(steel_cut[work, inventory] * work_tbl$length[work],
                 work = 1:work_count,
                 inventory = 1:invent_count), sense = "min"
  )

# View the model
steel_model

# Solve the model

solution <- ompr::solve_model(steel_model, with_ROI(solver = "glpk", verbose = TRUE))  

# Check objective value
solution$objective_value

# Get the solution 
steel_model_soln <-
  ompr::get_solution(solution, steel_cut[work, inventory]) %>% filter(value > 0) %>% 
  mutate(cut_length = work_tbl$length[work])

# View the solution
steel_model_soln
  • 1
    Some vocabulary will help you greatly! This is called a "cutting stock" problem in linear programming and is VERY common. There are many examples on this site, some of which are in `r`: https://stackoverflow.com/search?q=cutting+stock+%5Br%5D – AirSquid Apr 17 '23 at 21:01
  • I would think the only variable you need is a binary one, call it `take` or something that denotes that you are taking piece `i` from stock piece `j` ... `take[i, j]` like your `cut_order` variable, but just make it binary... then you can multiply it by the lengths of the orders or stock as needed to make your constraints – AirSquid Apr 17 '23 at 21:04
  • @AirSquid Thanks for the vocabulary. Unfortunately, very few of the results from your query is actually about the problem, and none of them uses the ompr package. – RegressionSquirrel Apr 19 '23 at 21:17

1 Answers1

1

My r syntax is pretty rusty, but... Your model is close. Why don't you:

  1. Change cut_order to a binary variable, indicating that you are cutting work from inventory.

  2. 'take_item' looks good as a binary also...leave it.

  3. Change constraint 1 to a loop because you need a "for each work item".... You need a summation constraint for each item in work. It is minimization, so don't fret the >=. In pseudocode:

for each work:
    add_constraint(sum(cut_order[work, inventory] over inventory) >= 1)
  1. change constraint 2 to loop over each inventory to again create a constraint "for each" inventory piece and multiply the selection variable by the work length in the constraint

for each inventory:
    add_constraint(sum(cut_order[work, inventory]*work_length[work]) <= inventory_length[inventory])
  1. Constraint 3 looks good

  2. Change your objective to accumulate the scrap...

sum(take_item[inventory]*inventory_length[inventory] - sum(cut_order[work, inventory]*work_length[work] over work) over inventory)

Another alternative that you might consider is just minimizing(take_item) to use the minimal number of sticks consumed, or penalizing slightly with a weight the use of longer sticks, to help consume the short stuff first, etc. All that is gravy after you get the model breathing...

Edit: An (untested) stab at the r code:

# Load libraries

# Load required packages
library(dplyr)
library(ompr)
library(ompr.roi)
library(ROI)


# Define the problem data
work_tbl <- data.frame(length = c(2500, 500, 700))
inventory_tbl <-
  data.frame(length = c(1300, 2000, 1800, 2600, 3000))

work_count <- nrow(work_tbl)
invent_count <- nrow(inventory_tbl)
big_M <- (work_tbl$length) * 1.1

# initialize the model

steel_model <- ompr::MIPModel() %>% 
  
  # How much steel to be cut for each item
  add_variable(steel_cut[work, inventory],
               work = 1:work_count,
               inventory = 1:invent_count,
               type = "binary") %>% 
  
  # Take new item from inventory?
  add_variable(take_item[inventory],
               inventory = 1:invent_count,
               type = "binary") %>% 
  
  # Constraint 1: each work item must be fulfilled
  # You need to make a sum over all work FOR EACH inventory item, so inventory should be outside the sum
  add_constraint(sum_over(steel_cut[work, inventory], inventory = 1:invent_count ) == 1,
                work = 1:work_count)  %>% 
  
  # Constraint 2: for each item, ensure lengths cut is less than or equal to length of item
  # Again, here the summation of stuff used is compared to EACH inventory item, so that
  # needs to be outside the summation, but WORK is alread summed inside and should not be included...
  # you are basically saying, for each inventory item, sum up all of the work assigned to it and compare it
  # to the length of that inventory item
  add_constraint(sum_over(steel_cut[work, inventory] * work_tbl$length[work], work = 1:work_count) <= inventory_tbl$length[inventory],
                inventory = 1:invent_count) %>% 

  
  # Constraint 3: big_M to activate take_item whenever a length is cut
  add_constraint(sum_over(steel_cut[work, inventory], work = 1:work_count) <= big_M * take_item[inventory],
                inventory = 1:invent_count) %>% 
  
  # needed to multiply the steel_cut selection variable by the length of the work
  set_objective(sum_over(take_item[inventory] * inventory_tbl$length[inventory], inventory = 1:invent_count) 
              - sum_over(steel_cut[work, inventory] * work_tbl$length[work], work = 1:work_count, inventory = 1:invent_count), sense = "min")

steel_model

solution <- ompr::solve_model(steel_model, with_ROI(solver = "glpk", verbose = TRUE))  

solution$objective_value

steel_model_soln <-
  ompr::get_solution(solution, steel_cut[work, inventory]) %>% filter(value > 0)
AirSquid
  • 10,214
  • 2
  • 7
  • 31
  • Hi - thanks. I'll give that a whirl. One question - when `cut_order`is binary, there isn't a continuous variable to minimize, so I'm guessing that in your example, work_length and inventory_length would be the length column of the respective tables? I might be off, but is the loop really necessary? This code: `sum_over(cut_order[work, inventory],work = 1:work_count) <= inventory_tbl$length[inventory], inventory = 1:invent_count )` essentially does that? – RegressionSquirrel Apr 21 '23 at 06:27
  • first part: yes, the `work_length` and `inventory_length` are the fixed (parameter) values from your tables. You do not need a a continuous variable here, if you stubby-pencil a couple examples for the objective function, you will see that the use of the binaries * the lengths is "totalling up" the selections. – AirSquid Apr 21 '23 at 14:15
  • Second, yeah, the loop is necessary. Any time you have a "for each" constraint, a set of constraints need to be generated. You have 2 such instances. For the first, you are saying "for each work order" ... it must be cut from at least one inventory. – AirSquid Apr 21 '23 at 14:18
  • I'm not sure that is the case, I have other models with similar problems, and it works without any for loops in those. I'm not able to get the pseudo-code working, but thanks for the help :) – RegressionSquirrel Apr 23 '23 at 12:33
  • Ahhh…. Well, you definitely need to make a constraint for each item of `work` in Constraint 1, etc. But after looking at it again, I think I misread the `r` syntax and you are in fact already doing that within the `add_constraint()` call. So, I think the “loop” I mentioned in pseudocode is already rolled up in that. So, aside from that, what isn’t working? – AirSquid Apr 23 '23 at 15:28
  • It's working in the sense that it cuts the first piece from the right item (if the first item has a length of 2500, it correctly chooses the third piece to match it to), but it's only matches the first item in the work_tbl. – RegressionSquirrel Apr 23 '23 at 16:28
  • 1
    Hmmm... could you edit your original post and just augment it with the edited version of your code in entirety to take a look? After deducing what is going on with the `add_constraint()` syntax, it is quite readable. – AirSquid Apr 23 '23 at 16:35
  • Sure, it's edited now :) – RegressionSquirrel Apr 23 '23 at 16:37
  • take a look at my edit and comments, you had a small error in both C1 and C2 that I think I got and commented. LMK if the code comments aren't clear – AirSquid Apr 23 '23 at 17:03
  • The code runs with a small correction where [work] was not defined, but the status of the model is `LP HAS NO PRIMAL FEASIBLE SOLUTION` so the constraints are too strict. – RegressionSquirrel Apr 23 '23 at 17:09
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/253293/discussion-between-airsquid-and-regressionsquirrel). – AirSquid Apr 23 '23 at 17:12
  • Using the updated first post (which now works thanks to @airsquid), I've tried to expand on this to include another dimension, but so far I've been unsuccessful. Let's say there are different types of steel, exemplified by the following: `work_tbl <- data.frame( length = c(2500, 500, 700, 1200, 1500, 2000, 2500, 3000, 4000, 5250), type = sample(c('Type A', 'Type B'), 10, replace=TRUE)` I've tried to recode the type as a integer variable (in reality, there are more than just two types, so binary does not work). Any tips on how to add this as a constraint? – RegressionSquirrel May 15 '23 at 07:29