dirkschumacher / ompr

R package to model Mixed Integer Linear Programs
https://dirkschumacher.github.io/ompr/
Other
268 stars 35 forks source link

Limiting Variable Definition By Ordered Pair #441

Closed datadrivensupplychain closed 1 year ago

datadrivensupplychain commented 1 year ago

I am modeling a supply chain distribution network with 50 distribution centers (DCs, i.e., warehouses) and 100 items. The decision variable is how to flow items between DCs.

optim_model <- ompr::MILPModel() %>%
  add_variable(dc_sku_flow[origin_dcindex, dest_dcindex, skuindex], origin_dcindex=1:50, dest_dcindex=1:50, skuindex=1:100,
type = 'continuous',lb=0)

This leads to 50^2 * 100 = 250,000 decision variables. But in this network, a given DC can only flow products to maybe 10 adjacent DCs. So (for example) flowing from a DC in New York City to a DC in Los Angeles is prohibited.

I have a dataframe listing all permitted permutations of origin and destination DCs. How can I limit my variable definition to just these options? I would just explicitly constrain flows to zero between prohibited DC permutations, but that adds a lot of constraints possibly unneccessarily.

sbmack commented 1 year ago

Here are Discussion items that demonstrate how sparse matrices can be used in an ompr formulation.

https://github.com/dirkschumacher/ompr/discussions/406#discussion-3843794

https://github.com/dirkschumacher/ompr/discussions/412#discussion-3853109

Please let us know how you make out. SteveM

datadrivensupplychain commented 1 year ago

For speed (the actual model is much larger than my example), I've decided to use MILPModel backend. I've created a dataframe with all eligible origin-destination-SKU combinations (~215K combinations). So now the primary decision variable is the value to move along each of these pathways.

optim_model <- ompr::MILPModel() %>%
    add_variable( flow[odskuindex], odskuindex=1:odskurowcount,type='continuous',lb=0)

So far, so good. I now need to add a constraint that says that says for each SKU and destination DC, the sum of value of the decision variables must equal demand (i.e., demand must be satisfied). I have a separate dataframe with demand for each DC/SKU combination - about 50K combinations. It takes ~20 minutes to add these one-by-one in a for-loop so I'd like to add them with a single constraint. I tried this constraint.

LHS: sum of all decision variables that represent flows of SKU "A" into DC1. RHS: demand for SKU "A" in DC1

optim_model %<>% add_constraint(sum_expr(flow[j],   j =   _rows that represent all possible flows into the destination DC for a SKU )_ == 
    ompr::colwise(sales_dataframe$demand[i]), i= 1:50000)  

When I add this constraint (using a smaller index for i), then call up the model structure via optim_model on the CLI, I get this:

> optim_model
Mixed integer linear optimization problem
Variables:
  Continuous: 214514 
  Integer: 0 
  Binary: 0 
No objective function. 
Error: You have definied variables for 1 rows, but you add a constant with 3 elements. The length of the two have to match or the constant is of length 1, i.e. a scalar.

When I then call up optim_model$constraints on the CLI, I see just a single constraint with multiple values on the RHS, instead of multiple constraints each with a single value on the RHS.

I can do the for-loop if absolutely necessary but would prefer to figure out what is going on here.

FYI I have not tried this yet using the MIPModel backend, just MILPModel.

Thanks in advance

Ralph ralph@datadrivensupplychain.com

sbmack commented 1 year ago

For speed (the actual model is much larger than my example), I've decided to use MILPModel backend.

As an aside, note that MILPModel has been shelved because its syntax is complicated. Dirk integrated the increased performance features of MILPModel into MIPModel while maintaining the cleaner syntax (no colwise calls). Here are the links to install the development version of MIPModel which is very stable:

remotes::install_github("dirkschumacher/ompr") remotes::install_github("dirkschumacher/ompr.roi")

datadrivensupplychain commented 1 year ago

For speed (the actual model is much larger than my example), I've decided to use MILPModel backend.

As an aside, note that MILPModel has been shelved because its syntax is complicated. Dirk integrated the increased performance features of MILPModel into MIPModel while maintaining the cleaner syntax (no colwise calls). Here are the links to install the development version of MIPModel which is very stable:

remotes::install_github("dirkschumacher/ompr") remotes::install_github("dirkschumacher/ompr.roi")

I have ompr v1.0.2.900 installed, I assume this version doesn't have the MILPModel performance within MIPModel, but the dev version does?

sbmack commented 1 year ago

I have ompr v1.0.2.900 installed, I assume this version doesn't have the MILPModel performance within MIPModel, but the dev version does?

I don't know. My developer version is 1.0.3.9000 I would use that.

datadrivensupplychain commented 1 year ago

OK, now I've installed the dev version of ompr and ompr.roi, getting a different issue with the MIPModel backend. Per Dirk's advice in another thread, I'm limiting variable definitions via a binary array that says which origin/destination/SKU combinations are permitted.

optim_mipmodel <- ompr::MIPModel() %>%
  add_variable( dc_dc_sku_flow[origdcindex,destdcindex,skuindex],
                origdcindex=1:dccount,
                destdcindex=1:dccount,
                skuindex=1:skucount,
                type='continuous',lb=0,
                flow_permited_array[origdcindex,destdcindex,skuindex] == 1)

I then try to establish the "satisfy demand" constraint. Each row in sales_sku_dc_year_df has a value for SKU Index, DC Index, and Demand, so I reference those in the constraint.


optim_mipmodel <- optim_mipmodel %>% add_constraint(
  sum_over(dc_dc_sku_flow[  origdcindex,
                          sales_sku_dc_year_df$dc_index[i],
                          sales_sku_dc_year_df$sku_index[i]   ],
           origdcindex=1:dccount) == 
    sales_sku_dc_year_df$demand[i], i=1:100)

When I try to add this constraint, I get this error: Error indc_dc_sku_flow[origdcindex, sales_sku_dc_year_df$dc_index[i], sales_sku_dc_year_df$sku_index[i]]: ! Variable not found for indexes . I've validated that sales_sku_dc_year_df$dc_index[i], sales_sku_dc_year_df$sku_index[i], and sales_sku_dc_year_df$demand[i] all yield valid values.

I quit caffeine a while ago, so maybe I'm not mentally as sharp as I used to be, but I think this constraint is set up properly. Don't know where I'm going wrong here. When I try to add the constraint with just a scalar value of i=1 instead of referencing i=1:100, I get the same issue.

prubin73 commented 1 year ago

Maybe I'm misreading your constraint, but it appears that for every row in sales_sku_dc_year you are generating a constraint that sums dc_dc_sku_flow[o, ..., ...] for every o from 1 to dccount. Wouldn't that sum include index combinations for which flow_permited_array = 0 (meaning the dc_dc_sku_flow variable was not created for that combination)? I think you need to add the flow_permited_array qualifier to the sum_over expression (if the syntax allows it).

datadrivensupplychain commented 1 year ago

@prubin73 I came back to say just that, I had to add the flow_permited_array qualifier into the summation. This works (solves + constraint works as intended) when I iterate over a small number of rows in the demand dataframe. Going to get it started on all 50K rows of the demand dataframe now.

optim_mipmodel <- optim_mipmodel %>% add_constraint(
  sum_over(dc_dc_sku_flow[  origdcindex,
                            sales_sku_dc_year_df$dc_index[i],
                            sales_sku_dc_year_df$sku_index[i]   ],
           origdcindex=1:dccount, 

           flow_permited_array[origdcindex,
                               sales_sku_dc_year_df$dc_index[i],
                               sales_sku_dc_year_df$sku_index[i]] == 1
           ) == 
    sales_sku_dc_year_df$demand[i], i=1:nrow(sales_sku_dc_year_df))
datadrivensupplychain commented 1 year ago

Update, I moved it all into MIPModel and was able to add ~50K constraints in about 15 minutes. Plenty fine for what I need it for.