2024-07-12 13:56:11 +02:00
|
|
|
#!/usr/bin/env python3
|
2025-01-10 11:35:44 +01:00
|
|
|
# Copyright 2010-2025 Google LLC
|
2019-05-31 21:42:45 +02:00
|
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
|
# you may not use this file except in compliance with the License.
|
|
|
|
|
# You may obtain a copy of the License at
|
|
|
|
|
#
|
|
|
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
|
#
|
|
|
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
|
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
|
# See the License for the specific language governing permissions and
|
|
|
|
|
# limitations under the License.
|
|
|
|
|
|
2024-07-12 13:56:11 +02:00
|
|
|
"""Cutting stock problem with the objective to minimize wasted space."""
|
2019-05-31 21:42:45 +02:00
|
|
|
|
|
|
|
|
import collections
|
|
|
|
|
import time
|
|
|
|
|
|
2023-03-03 18:55:16 +04:00
|
|
|
from absl import app
|
|
|
|
|
from absl import flags
|
2024-07-12 13:56:11 +02:00
|
|
|
import numpy as np
|
|
|
|
|
|
2023-03-03 18:55:16 +04:00
|
|
|
from ortools.linear_solver.python import model_builder as mb
|
2019-05-31 21:42:45 +02:00
|
|
|
from ortools.sat.python import cp_model
|
|
|
|
|
|
2023-03-03 18:55:16 +04:00
|
|
|
|
|
|
|
|
_OUTPUT_PROTO = flags.DEFINE_string(
|
2024-07-12 13:56:11 +02:00
|
|
|
"output_proto", "", "Output file to write the cp_model proto to."
|
|
|
|
|
)
|
2023-03-03 18:55:16 +04:00
|
|
|
_PARAMS = flags.DEFINE_string(
|
2024-07-12 13:56:11 +02:00
|
|
|
"params",
|
|
|
|
|
"num_search_workers:8,log_search_progress:true,max_time_in_seconds:10",
|
|
|
|
|
"Sat solver parameters.",
|
|
|
|
|
)
|
|
|
|
|
_SOLVER = flags.DEFINE_string("solver", "sat", "Method used to solve: sat, mip.")
|
2019-05-31 21:42:45 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
DESIRED_LENGTHS = [
|
2024-07-24 14:54:57 -07:00
|
|
|
2490,
|
|
|
|
|
3980,
|
|
|
|
|
2490,
|
|
|
|
|
3980,
|
|
|
|
|
2391,
|
|
|
|
|
2391,
|
|
|
|
|
2391,
|
|
|
|
|
596,
|
|
|
|
|
596,
|
|
|
|
|
596,
|
|
|
|
|
2456,
|
|
|
|
|
2456,
|
|
|
|
|
3018,
|
|
|
|
|
938,
|
|
|
|
|
3018,
|
|
|
|
|
938,
|
|
|
|
|
943,
|
|
|
|
|
3018,
|
|
|
|
|
943,
|
|
|
|
|
3018,
|
|
|
|
|
2490,
|
|
|
|
|
3980,
|
|
|
|
|
2490,
|
|
|
|
|
3980,
|
|
|
|
|
2391,
|
|
|
|
|
2391,
|
|
|
|
|
2391,
|
|
|
|
|
596,
|
|
|
|
|
596,
|
|
|
|
|
596,
|
|
|
|
|
2456,
|
|
|
|
|
2456,
|
|
|
|
|
3018,
|
|
|
|
|
938,
|
|
|
|
|
3018,
|
|
|
|
|
938,
|
|
|
|
|
943,
|
|
|
|
|
3018,
|
|
|
|
|
943,
|
|
|
|
|
3018,
|
|
|
|
|
2890,
|
|
|
|
|
3980,
|
|
|
|
|
2890,
|
|
|
|
|
3980,
|
|
|
|
|
2391,
|
|
|
|
|
2391,
|
|
|
|
|
2391,
|
|
|
|
|
596,
|
|
|
|
|
596,
|
|
|
|
|
596,
|
|
|
|
|
2856,
|
|
|
|
|
2856,
|
|
|
|
|
3018,
|
|
|
|
|
938,
|
|
|
|
|
3018,
|
|
|
|
|
938,
|
|
|
|
|
943,
|
|
|
|
|
3018,
|
|
|
|
|
943,
|
|
|
|
|
3018,
|
|
|
|
|
3290,
|
|
|
|
|
3980,
|
|
|
|
|
3290,
|
|
|
|
|
3980,
|
|
|
|
|
2391,
|
|
|
|
|
2391,
|
|
|
|
|
2391,
|
|
|
|
|
596,
|
|
|
|
|
596,
|
|
|
|
|
596,
|
|
|
|
|
3256,
|
|
|
|
|
3256,
|
|
|
|
|
3018,
|
|
|
|
|
938,
|
|
|
|
|
3018,
|
|
|
|
|
938,
|
|
|
|
|
943,
|
|
|
|
|
3018,
|
|
|
|
|
943,
|
|
|
|
|
3018,
|
|
|
|
|
3690,
|
|
|
|
|
3980,
|
|
|
|
|
3690,
|
|
|
|
|
3980,
|
|
|
|
|
2391,
|
|
|
|
|
2391,
|
|
|
|
|
2391,
|
|
|
|
|
596,
|
|
|
|
|
596,
|
|
|
|
|
596,
|
|
|
|
|
3656,
|
|
|
|
|
3656,
|
|
|
|
|
3018,
|
|
|
|
|
938,
|
|
|
|
|
3018,
|
|
|
|
|
938,
|
|
|
|
|
943,
|
|
|
|
|
3018,
|
|
|
|
|
943,
|
|
|
|
|
3018,
|
|
|
|
|
2790,
|
|
|
|
|
3980,
|
|
|
|
|
2790,
|
|
|
|
|
3980,
|
|
|
|
|
2391,
|
|
|
|
|
2391,
|
|
|
|
|
2391,
|
|
|
|
|
596,
|
|
|
|
|
596,
|
|
|
|
|
596,
|
|
|
|
|
2756,
|
|
|
|
|
2756,
|
|
|
|
|
3018,
|
|
|
|
|
938,
|
|
|
|
|
3018,
|
|
|
|
|
938,
|
|
|
|
|
943,
|
|
|
|
|
3018,
|
|
|
|
|
943,
|
|
|
|
|
3018,
|
|
|
|
|
2790,
|
|
|
|
|
3980,
|
|
|
|
|
2790,
|
|
|
|
|
3980,
|
|
|
|
|
2391,
|
|
|
|
|
2391,
|
|
|
|
|
2391,
|
|
|
|
|
596,
|
|
|
|
|
596,
|
|
|
|
|
596,
|
|
|
|
|
2756,
|
|
|
|
|
2756,
|
|
|
|
|
3018,
|
|
|
|
|
938,
|
|
|
|
|
3018,
|
|
|
|
|
938,
|
|
|
|
|
943,
|
2019-05-31 21:42:45 +02:00
|
|
|
]
|
|
|
|
|
POSSIBLE_CAPACITIES = [4000, 5000, 6000, 7000, 8000]
|
|
|
|
|
|
|
|
|
|
# Toy problem
|
|
|
|
|
# DESIRED_LENGTHS = [12, 12, 8, 8, 8]
|
|
|
|
|
# POSSIBLE_CAPACITIES = [10, 20]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def regroup_and_count(raw_input):
|
2019-06-05 13:31:43 +02:00
|
|
|
"""Regroup all equal capacities in a multiset."""
|
2019-05-31 21:42:45 +02:00
|
|
|
grouped = collections.defaultdict(int)
|
|
|
|
|
for i in raw_input:
|
|
|
|
|
grouped[i] += 1
|
|
|
|
|
output = []
|
|
|
|
|
for size, count in grouped.items():
|
|
|
|
|
output.append([size, count])
|
2019-06-05 13:31:43 +02:00
|
|
|
output.sort(reverse=False)
|
2019-05-31 21:42:45 +02:00
|
|
|
return output
|
|
|
|
|
|
|
|
|
|
|
2019-06-05 13:31:43 +02:00
|
|
|
def price_usage(usage, capacities):
|
|
|
|
|
"""Compute the best price for a given usage and possible capacities."""
|
2019-05-31 21:42:45 +02:00
|
|
|
price = max(capacities)
|
2019-06-05 13:31:43 +02:00
|
|
|
for capacity in capacities:
|
|
|
|
|
if capacity < usage:
|
2019-05-31 21:42:45 +02:00
|
|
|
continue
|
2019-06-05 13:31:43 +02:00
|
|
|
price = min(capacity - usage, price)
|
2019-05-31 21:42:45 +02:00
|
|
|
return price
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def create_state_graph(items, max_capacity):
|
2019-06-05 13:31:43 +02:00
|
|
|
"""Create a state graph from a multiset of items, and a maximum capacity."""
|
2019-05-31 21:42:45 +02:00
|
|
|
states = []
|
|
|
|
|
state_to_index = {}
|
|
|
|
|
states.append(0)
|
|
|
|
|
state_to_index[0] = 0
|
|
|
|
|
transitions = []
|
|
|
|
|
|
|
|
|
|
for item_index, size_and_count in enumerate(items):
|
|
|
|
|
size, count = size_and_count
|
|
|
|
|
num_states = len(states)
|
|
|
|
|
for state_index in range(num_states):
|
2019-06-05 13:31:43 +02:00
|
|
|
current_state = states[state_index]
|
|
|
|
|
current_state_index = state_index
|
2019-05-31 21:42:45 +02:00
|
|
|
|
2019-06-05 13:31:43 +02:00
|
|
|
for card in range(count):
|
|
|
|
|
new_state = current_state + size * (card + 1)
|
2019-05-31 21:42:45 +02:00
|
|
|
if new_state > max_capacity:
|
|
|
|
|
break
|
|
|
|
|
if new_state in state_to_index:
|
|
|
|
|
new_state_index = state_to_index[new_state]
|
|
|
|
|
else:
|
|
|
|
|
new_state_index = len(states)
|
|
|
|
|
states.append(new_state)
|
|
|
|
|
state_to_index[new_state] = new_state_index
|
|
|
|
|
# Add the transition
|
2024-07-12 13:56:11 +02:00
|
|
|
transitions.append(
|
|
|
|
|
[current_state_index, new_state_index, item_index, card + 1]
|
|
|
|
|
)
|
2019-06-05 13:31:43 +02:00
|
|
|
|
2019-05-31 21:42:45 +02:00
|
|
|
return states, transitions
|
|
|
|
|
|
|
|
|
|
|
2023-03-03 18:55:16 +04:00
|
|
|
def solve_cutting_stock_with_arc_flow_and_sat(output_proto_file: str, params: str):
|
2019-06-05 13:31:43 +02:00
|
|
|
"""Solve the cutting stock with arc-flow and the CP-SAT solver."""
|
2019-05-31 21:42:45 +02:00
|
|
|
items = regroup_and_count(DESIRED_LENGTHS)
|
2024-07-12 13:56:11 +02:00
|
|
|
print("Items:", items)
|
2019-05-31 21:42:45 +02:00
|
|
|
num_items = len(DESIRED_LENGTHS)
|
|
|
|
|
|
|
|
|
|
max_capacity = max(POSSIBLE_CAPACITIES)
|
|
|
|
|
states, transitions = create_state_graph(items, max_capacity)
|
|
|
|
|
|
2024-07-12 13:56:11 +02:00
|
|
|
print(
|
|
|
|
|
"Dynamic programming has generated",
|
|
|
|
|
len(states),
|
|
|
|
|
"states and",
|
|
|
|
|
len(transitions),
|
|
|
|
|
"transitions",
|
|
|
|
|
)
|
2019-05-31 21:42:45 +02:00
|
|
|
|
|
|
|
|
incoming_vars = collections.defaultdict(list)
|
|
|
|
|
outgoing_vars = collections.defaultdict(list)
|
|
|
|
|
incoming_sink_vars = []
|
|
|
|
|
item_vars = collections.defaultdict(list)
|
2019-06-05 13:31:43 +02:00
|
|
|
item_coeffs = collections.defaultdict(list)
|
|
|
|
|
transition_vars = []
|
2019-05-31 21:42:45 +02:00
|
|
|
|
|
|
|
|
model = cp_model.CpModel()
|
|
|
|
|
|
|
|
|
|
objective_vars = []
|
|
|
|
|
objective_coeffs = []
|
|
|
|
|
|
2019-06-05 13:31:43 +02:00
|
|
|
for outgoing, incoming, item_index, card in transitions:
|
2019-05-31 21:42:45 +02:00
|
|
|
count = items[item_index][1]
|
2019-06-05 13:31:43 +02:00
|
|
|
max_count = count // card
|
2019-05-31 21:42:45 +02:00
|
|
|
count_var = model.NewIntVar(
|
2024-07-12 13:56:11 +02:00
|
|
|
0, max_count, "i%i_f%i_t%i_C%s" % (item_index, incoming, outgoing, card)
|
|
|
|
|
)
|
2019-05-31 21:42:45 +02:00
|
|
|
incoming_vars[incoming].append(count_var)
|
|
|
|
|
outgoing_vars[outgoing].append(count_var)
|
|
|
|
|
item_vars[item_index].append(count_var)
|
2019-06-05 13:31:43 +02:00
|
|
|
item_coeffs[item_index].append(card)
|
|
|
|
|
transition_vars.append(count_var)
|
2019-05-31 21:42:45 +02:00
|
|
|
|
|
|
|
|
for state_index, state in enumerate(states):
|
|
|
|
|
if state_index == 0:
|
|
|
|
|
continue
|
2024-07-12 13:56:11 +02:00
|
|
|
exit_var = model.NewIntVar(0, num_items, "e%i" % state_index)
|
2019-05-31 21:42:45 +02:00
|
|
|
outgoing_vars[state_index].append(exit_var)
|
|
|
|
|
incoming_sink_vars.append(exit_var)
|
2019-06-05 13:31:43 +02:00
|
|
|
price = price_usage(state, POSSIBLE_CAPACITIES)
|
2019-05-31 21:42:45 +02:00
|
|
|
objective_vars.append(exit_var)
|
|
|
|
|
objective_coeffs.append(price)
|
|
|
|
|
|
|
|
|
|
# Flow conservation
|
|
|
|
|
for state_index in range(1, len(states)):
|
2024-07-12 13:56:11 +02:00
|
|
|
model.Add(sum(incoming_vars[state_index]) == sum(outgoing_vars[state_index]))
|
2019-05-31 21:42:45 +02:00
|
|
|
|
|
|
|
|
# Flow going out of the source must go in the sink
|
|
|
|
|
model.Add(sum(outgoing_vars[0]) == sum(incoming_sink_vars))
|
|
|
|
|
|
|
|
|
|
# Items must be placed
|
|
|
|
|
for item_index, size_and_count in enumerate(items):
|
2019-06-05 13:31:43 +02:00
|
|
|
num_arcs = len(item_vars[item_index])
|
|
|
|
|
model.Add(
|
2024-07-12 13:56:11 +02:00
|
|
|
sum(
|
|
|
|
|
item_vars[item_index][i] * item_coeffs[item_index][i]
|
|
|
|
|
for i in range(num_arcs)
|
|
|
|
|
)
|
|
|
|
|
== size_and_count[1]
|
|
|
|
|
)
|
2019-05-31 21:42:45 +02:00
|
|
|
|
|
|
|
|
# Objective is the sum of waste
|
|
|
|
|
model.Minimize(
|
2024-07-12 13:56:11 +02:00
|
|
|
sum(objective_vars[i] * objective_coeffs[i] for i in range(len(objective_vars)))
|
|
|
|
|
)
|
2019-05-31 21:42:45 +02:00
|
|
|
|
|
|
|
|
# Output model proto to file.
|
2021-07-13 13:47:57 +02:00
|
|
|
if output_proto_file:
|
2023-03-03 18:55:16 +04:00
|
|
|
model.ExportToFile(output_proto_file)
|
2019-05-31 21:42:45 +02:00
|
|
|
|
|
|
|
|
# Solve model.
|
|
|
|
|
solver = cp_model.CpSolver()
|
2023-03-03 18:55:16 +04:00
|
|
|
if params:
|
2025-07-23 17:38:49 +02:00
|
|
|
solver.parameters.parse_text_format(params)
|
2019-05-31 21:42:45 +02:00
|
|
|
solver.parameters.log_search_progress = True
|
2023-03-03 18:55:16 +04:00
|
|
|
solver.Solve(model)
|
2019-05-31 21:42:45 +02:00
|
|
|
|
|
|
|
|
|
2019-06-05 13:31:43 +02:00
|
|
|
def solve_cutting_stock_with_arc_flow_and_mip():
|
|
|
|
|
"""Solve the cutting stock with arc-flow and a MIP solver."""
|
2019-05-31 21:42:45 +02:00
|
|
|
items = regroup_and_count(DESIRED_LENGTHS)
|
2024-07-12 13:56:11 +02:00
|
|
|
print("Items:", items)
|
2019-05-31 21:42:45 +02:00
|
|
|
num_items = len(DESIRED_LENGTHS)
|
|
|
|
|
max_capacity = max(POSSIBLE_CAPACITIES)
|
|
|
|
|
states, transitions = create_state_graph(items, max_capacity)
|
|
|
|
|
|
2024-07-12 13:56:11 +02:00
|
|
|
print(
|
|
|
|
|
"Dynamic programming has generated",
|
|
|
|
|
len(states),
|
|
|
|
|
"states and",
|
|
|
|
|
len(transitions),
|
|
|
|
|
"transitions",
|
|
|
|
|
)
|
2019-05-31 21:42:45 +02:00
|
|
|
|
|
|
|
|
incoming_vars = collections.defaultdict(list)
|
|
|
|
|
outgoing_vars = collections.defaultdict(list)
|
|
|
|
|
incoming_sink_vars = []
|
|
|
|
|
item_vars = collections.defaultdict(list)
|
2019-06-05 13:31:43 +02:00
|
|
|
item_coeffs = collections.defaultdict(list)
|
2019-05-31 21:42:45 +02:00
|
|
|
|
|
|
|
|
start_time = time.time()
|
2023-03-03 18:55:16 +04:00
|
|
|
model = mb.ModelBuilder()
|
2019-05-31 21:42:45 +02:00
|
|
|
|
|
|
|
|
objective_vars = []
|
|
|
|
|
objective_coeffs = []
|
|
|
|
|
|
|
|
|
|
var_index = 0
|
2019-06-05 13:31:43 +02:00
|
|
|
for outgoing, incoming, item_index, card in transitions:
|
2019-05-31 21:42:45 +02:00
|
|
|
count = items[item_index][1]
|
2023-03-03 18:55:16 +04:00
|
|
|
count_var = model.new_int_var(
|
2024-07-12 13:56:11 +02:00
|
|
|
0,
|
|
|
|
|
count,
|
|
|
|
|
"a%i_i%i_f%i_t%i_c%i" % (var_index, item_index, incoming, outgoing, card),
|
|
|
|
|
)
|
2019-05-31 21:42:45 +02:00
|
|
|
var_index += 1
|
|
|
|
|
incoming_vars[incoming].append(count_var)
|
|
|
|
|
outgoing_vars[outgoing].append(count_var)
|
|
|
|
|
item_vars[item_index].append(count_var)
|
2019-06-05 13:31:43 +02:00
|
|
|
item_coeffs[item_index].append(card)
|
2019-05-31 21:42:45 +02:00
|
|
|
|
|
|
|
|
for state_index, state in enumerate(states):
|
|
|
|
|
if state_index == 0:
|
|
|
|
|
continue
|
2024-07-12 13:56:11 +02:00
|
|
|
exit_var = model.new_int_var(0, num_items, "e%i" % state_index)
|
2019-05-31 21:42:45 +02:00
|
|
|
outgoing_vars[state_index].append(exit_var)
|
|
|
|
|
incoming_sink_vars.append(exit_var)
|
2019-06-05 13:31:43 +02:00
|
|
|
price = price_usage(state, POSSIBLE_CAPACITIES)
|
2019-05-31 21:42:45 +02:00
|
|
|
objective_vars.append(exit_var)
|
|
|
|
|
objective_coeffs.append(price)
|
|
|
|
|
|
|
|
|
|
# Flow conservation
|
|
|
|
|
for state_index in range(1, len(states)):
|
2023-03-03 18:55:16 +04:00
|
|
|
model.add(
|
2024-07-12 13:56:11 +02:00
|
|
|
mb.LinearExpr.sum(incoming_vars[state_index])
|
|
|
|
|
== mb.LinearExpr.sum(outgoing_vars[state_index])
|
|
|
|
|
)
|
2019-05-31 21:42:45 +02:00
|
|
|
|
|
|
|
|
# Flow going out of the source must go in the sink
|
2023-03-03 18:55:16 +04:00
|
|
|
model.add(
|
2024-07-12 13:56:11 +02:00
|
|
|
mb.LinearExpr.sum(outgoing_vars[0]) == mb.LinearExpr.sum(incoming_sink_vars)
|
|
|
|
|
)
|
2019-05-31 21:42:45 +02:00
|
|
|
|
|
|
|
|
# Items must be placed
|
|
|
|
|
for item_index, size_and_count in enumerate(items):
|
2019-06-05 13:31:43 +02:00
|
|
|
num_arcs = len(item_vars[item_index])
|
2023-03-03 18:55:16 +04:00
|
|
|
model.add(
|
2024-07-12 13:56:11 +02:00
|
|
|
mb.LinearExpr.sum(
|
|
|
|
|
[
|
|
|
|
|
item_vars[item_index][i] * item_coeffs[item_index][i]
|
|
|
|
|
for i in range(num_arcs)
|
|
|
|
|
]
|
|
|
|
|
)
|
|
|
|
|
== size_and_count[1]
|
|
|
|
|
)
|
2019-05-31 21:42:45 +02:00
|
|
|
|
|
|
|
|
# Objective is the sum of waste
|
2023-03-03 18:55:16 +04:00
|
|
|
model.minimize(np.dot(objective_vars, objective_coeffs))
|
2019-05-31 21:42:45 +02:00
|
|
|
|
2024-07-12 13:56:11 +02:00
|
|
|
solver = mb.ModelSolver("scip")
|
2023-03-03 18:55:16 +04:00
|
|
|
solver.enable_output(True)
|
|
|
|
|
status = solver.solve(model)
|
2019-05-31 21:42:45 +02:00
|
|
|
|
|
|
|
|
### Output the solution.
|
2023-03-03 18:55:16 +04:00
|
|
|
if status == mb.SolveStatus.OPTIMAL or status == mb.SolveStatus.FEASIBLE:
|
2024-07-12 13:56:11 +02:00
|
|
|
print(
|
|
|
|
|
"Objective value = %f found in %.2f s"
|
|
|
|
|
% (solver.objective_value, time.time() - start_time)
|
|
|
|
|
)
|
2019-05-31 21:42:45 +02:00
|
|
|
else:
|
2024-07-12 13:56:11 +02:00
|
|
|
print("No solution")
|
2019-05-31 21:42:45 +02:00
|
|
|
|
|
|
|
|
|
2023-03-03 18:55:16 +04:00
|
|
|
def main(_):
|
2024-07-12 13:56:11 +02:00
|
|
|
"""Main function."""
|
|
|
|
|
if _SOLVER.value == "sat":
|
|
|
|
|
solve_cutting_stock_with_arc_flow_and_sat(_OUTPUT_PROTO.value, _PARAMS.value)
|
2019-05-31 21:42:45 +02:00
|
|
|
else: # 'mip'
|
2019-06-05 13:31:43 +02:00
|
|
|
solve_cutting_stock_with_arc_flow_and_mip()
|
2019-05-31 21:42:45 +02:00
|
|
|
|
|
|
|
|
|
2024-07-12 13:56:11 +02:00
|
|
|
if __name__ == "__main__":
|
2023-03-03 18:55:16 +04:00
|
|
|
app.run(main)
|