-1

How can I create the following linear program using PuLP? The structure is as follows. A list of orders must be processed in the order they came in, and has a list of items that can be made by a set of different machines. Each machine can only process one item per order, but each item can be processed by multiple machines in one order. The table below is the list of binary assignment variables that will be created for this LP.

order item machine
1     A      1
1     A      2
1     B      1
1     B      2
2     A      1
2     A      2
2     D      1
2     D      2 
3     E      1
3     E      2

I want the LP to assign items to 1+ machine(s) for each order, to minimize the total run time. The run time is composed of two parts, a fixed runtime for each item, and a cleaning time if a machine has previously processed a specific ingredient. The tables for this data are below:

item runtime      
A    50
B    60
C    70
D    80
E    90

         prev item
         A B C D E
curr   A 0 0 1 1 2
item   B 0 0 2 1 0
       C 0 0 0 1 1
       D 0 0 0 0 1
       E 1 0 0 0 0

For example, if machine 1 makes item A and then wants to make item E next, then there would be 2 seconds added to the runtime.

The only constraints are that each machine can only make 1 item per order, and each item in an order needs to be made by at least one machine. Thanks in advance.

Edit: To reformulate the simplified problem, does this capture all the constraints? also, how can I create the cleaning constraint without using a min function?

Variables:
A(oim) - asn for order, ing, machine
D(oim) - duration of order, ing, machine
D(oi) - duration of order, ing
C(oim) - cleaning for order, ing, machine

Constants:
R(oi) - fixed runtime of order, ing
C(xy) - fixed cleaning for ing x to ing y
M - much larger than total runtimes

Formulation:
min sum( D(oim)) + C(oim) )
s.t. D(oim) <= M * A(oim) for all oim
     sum( A(oim) ) >= 1 for all oi (this may be redundant bc of duration constraint?)
     sum( D(oim) ) = R(oi) for all oi
     M * A(oim) - D(oi) + D(oim) <= M for all oim
     M * A(oim) + D(oi) - D(oim) <= M for all oim
     sum( A(oim) ) <= 1 for all om
     C(xy) * min(A(-oim),A(oim)) <= C(oim) for all oim, where A(-oim) is previous order for machine m
J. Doe
  • 165
  • 5
  • 16
  • _Each machine can only process one item per order_ - your data contradict this. Order 1 machine 1 can process both A and B. – Reinderien Mar 22 '23 at 13:59
  • Perhaps your first table is the exhaustive adjacency matrix of all possible item assignments? Please clarify – Reinderien Mar 22 '23 at 14:02
  • 1
    What have you tried so far? This is just a spin on a job-shop scheduling problem or a "makespan" problem and there are many documented examples and most LP texts cover this well. – AirSquid Mar 22 '23 at 14:22
  • A couple things to consider: You said orders are processed in the order in which they are received. Are all orders known when you make the plan? You said each item could be processed by multiple machines...but there is no info on what the benefit would be. – AirSquid Mar 22 '23 at 14:24
  • Yes, the table is as an exhaustive matrix of all variables. My initial approach was to create the cost function as a function of previous items that the machine has processed up until the most recent cleaning, but that would become nonlinear. – J. Doe Mar 23 '23 at 09:46
  • Yes, all orders are known when the plan is made. If multiple machines process a single item, then the runtime for that item is cut in half. So the tradeoff is between whether the shortened runtime is worth any potential cleaning time that may be caused in the future – J. Doe Mar 23 '23 at 09:47
  • Is it strictly half, or would e.g. three machines reduce it to a third? – Reinderien Mar 23 '23 at 11:41
  • 3 would reduce to 1/3 – J. Doe Mar 24 '23 at 10:29
  • @J.Doe I initially worried that the 1/3 requirement would be nonlinear, but my current solution is able to produce that behaviour if you substitute your input data with e.g. one order and three machines. – Reinderien Mar 26 '23 at 16:11

1 Answers1

0

The following is a Pandas-assisted PuLP implementation. First, the implementation with comments:

import pandas as pd
import pulp


def make_data() -> tuple[
    pd.DataFrame,  # options
    pd.DataFrame,  # cleaning
    pd.Series,     # item runtimes
    int,           # time buffer
]:
    """Create problem dataframes and characteristic parameters"""
    item_idx = pd.Index(name='item', data=('A', 'B', 'C', 'D', 'E'))

    options = pd.DataFrame(
        data=((
            (1,  'A',  1),
            (1,  'A',  2),
            (1,  'B',  1),
            (1,  'B',  2),
            (2,  'A',  1),
            (2,  'A',  2),
            (2,  'D',  1),
            (2,  'D',  2),
            (3,  'E',  1),
            (3,  'E',  2),
        )),
        columns=('order', 'item', 'machine'),
    )

    item_runtimes = pd.Series(
        name='runtime', data=(50, 60, 70, 80, 90),
        index=item_idx,
    )

    # This should be much larger than the difference between any two times within an order
    time_buffer = item_runtimes.max() * len(options)

    cleaning = pd.DataFrame(
        index=item_idx.rename('curr_item'),
        columns=item_idx.rename('prev_item'),
        data=((
            (0, 0, 1, 1, 2),
            (0, 0, 2, 1, 0),
            (0, 0, 0, 1, 1),
            (0, 0, 0, 0, 1),
            (1, 0, 0, 0, 0),
        )),
    )

    return options, cleaning, item_runtimes, time_buffer


def make_problem() -> tuple[
    pulp.LpProblem,
    pulp.LpVariable,  # total runtime
]:
    prob = pulp.LpProblem(name='order_machine_processing', sense=pulp.LpMinimize)
    total_runtime = pulp.LpVariable(name='runtime', lowBound=0)
    prob.objective = pulp.LpAffineExpression(total_runtime)
    return prob, total_runtime


def make_option_vars(row: pd.Series, prob: pulp.LpProblem, item_runtimes: pd.Series, time_buffer: int) -> pd.Series:
    """Given a row from the options dataframe, create and constrain its LP variables"""
    suffix = f'_o{row.order}_i{row["item"]}_m{row.machine}'
    asn = pulp.LpVariable(name='asn' + suffix, cat=pulp.const.LpBinary)
    start = pulp.LpVariable(name='start' + suffix, lowBound=0)
    dur = pulp.LpVariable(name='dur' + suffix, lowBound=0, upBound=item_runtimes[row['item']])

    # If non-assigned, the runtime is zero
    prob.addConstraint(name='unused' + suffix, constraint=dur <= asn*time_buffer)

    return pd.Series((asn, start, dur))


def make_item_vars(group: pd.DataFrame, prob: pulp.LpProblem, item_runtimes: pd.Series, time_buffer: int) -> pd.Series:
    """Given an order-item group dataframe from the options, create and constrain its LP variables"""
    order, item = group.index[0]
    suffix = f'_o{order}_i{item}'

    # There must be at least one machine assigned to each order-item pair
    total = pulp.lpSum(group.asn)
    prob.addConstraint(name='assign' + suffix, constraint=total >= 1)

    # The runtime of each machine in an order-item pair must sum to the runtime of the item
    prob.addConstraint(
        name='time' + suffix,
        constraint=pulp.lpSum(group.dur) == item_runtimes[item]
    )

    start = pulp.LpVariable(name='start' + suffix, lowBound=0)
    # Duration for an order-item pair is nonzero
    dur = pulp.LpVariable(name='dur' + suffix, lowBound=1, upBound=item_runtimes[item])

    # We assume that if multiple machines are working on one order item, they must run at the same time.
    for _, row in group.iterrows():
        tolerance = time_buffer*(1 - row.asn)
        prefix = f'match{suffix}_m{row.machine}_'

        start_diff = row.start - start
        prob.addConstraint(name=prefix + 'startlo', constraint=start_diff <= tolerance)
        prob.addConstraint(name=prefix + 'starthi', constraint=-start_diff <= tolerance)

        dur_diff = row.dur - dur
        prob.addConstraint(name=prefix + 'durlo', constraint=dur_diff <= tolerance)
        prob.addConstraint(name=prefix + 'durhi', constraint=-dur_diff <= tolerance)

    return pd.Series((start, dur))


def make_vars(prob: pulp.LpProblem, options: pd.DataFrame, item_runtimes: pd.Series, time_buffer: int) -> None:
    """Make all the remaining LP variables and add the constraints specific to them"""
    options[['asn', 'start', 'dur']] = options.apply(
        make_option_vars, axis=1, prob=prob, item_runtimes=item_runtimes, time_buffer=time_buffer)

    (
        options.set_index(keys=['order', 'item'])
        .groupby(level=[0, 1])
        .apply(make_item_vars, prob=prob, item_runtimes=item_runtimes, time_buffer=time_buffer)
    )


def constrain_total_runtime(prob: pulp.LpProblem, options: pd.DataFrame, total_runtime: pulp.LpVariable) -> None:
    """Total runtime must be greater than all stops in the final order"""
    for _, row in options[options.order == options.order.iloc[-1]].iterrows():
        prob.addConstraint(
            name=f'runtime_total_o{row.order}_i{row["item"]}_m{row.machine}',
            constraint=total_runtime >= row.start + row.dur)


def constrain_order_machines(prob: pulp.LpProblem, options: pd.DataFrame) -> None:
    """There must be at most one item assigned to each order-machine pair"""
    for (order, machine), group in options.groupby(['order', 'machine']):
        total = pulp.lpSum(group['asn'])
        prob.addConstraint(name=f'assign_o{order}_m{machine}', constraint=total <= 1)


def constrain_order_transitions(prob: pulp.LpProblem, options: pd.DataFrame, cleaning: pd.DataFrame) -> None:
    """Comparing the transition between every order and its next order, enforce minimum time bounds"""
    for (prev_order, prev_group), (curr_order, curr_group) in zip(
        options[options.order < options.order.iloc[-1]].groupby('order'),
        options[options.order > options.order.iloc[0]].groupby('order'),
    ):
        # Comparing every item-machine combination...
        pairs = pd.merge(
            left=prev_group, right=curr_group,
            left_on='machine', right_on='machine',
            suffixes=('_prev', '_curr'),
        )
        for _, combo in pairs.iterrows():
            # always (disregarding clean): start_curr - stop_prev >= 0
            suffix = f'_o{prev_order}{curr_order}_m{combo.machine}_i{combo.item_prev}{combo.item_curr}'
            stop_prev = combo.start_prev + combo.dur_prev
            prob.addConstraint(name='seq' + suffix, constraint=stop_prev <= combo.start_curr)

            # also: if asn_prev and asn_curr, then start_curr - stop_prev >= cleaning[item_curr, item_prev]
            # start_curr - stop_prev >= -cl + cl*asn_prev + cl*asn_curr
            clean_time = cleaning.loc[combo.item_curr, combo.item_prev]
            if clean_time > 0:
                prob.addConstraint(
                    name='seq_clean' + suffix,
                    constraint=combo.start_curr - stop_prev >= clean_time*(combo.asn_curr + combo.asn_prev - 1)
                )


def solve(prob: pulp.LpProblem, verbose: bool) -> None:
    pulp.PULP_CBC_CMD(msg=verbose).solve(prob)
    assert prob.status == pulp.const.LpStatusOptimal


def display(options: pd.DataFrame, item_runtimes: pd.Series) -> None:
    used = options.asn.apply(pulp.LpVariable.value).astype(bool)
    for _, row in options[used].iterrows():
        stop = row.start.value() + row.dur.value()
        durmax = item_runtimes[row["item"]]
        print(
            f'Order {row.order} item {row["item"]} processed by machine {row.machine} '
            f'for {row.dur.value()}/{durmax} from {row.start.value()} to {stop}'
        )


def main(verbose: bool = False) -> None:
    options, cleaning, item_runtimes, time_buffer = make_data()
    prob, total_runtime = make_problem()

    make_vars(prob, options, item_runtimes, time_buffer)
    constrain_total_runtime(prob, options, total_runtime)
    constrain_order_machines(prob, options)
    constrain_order_transitions(prob, options, cleaning)

    if verbose:
        print(prob)

    solve(prob, verbose)
    display(options, item_runtimes)


if __name__ == '__main__':
    main(verbose=True)

Output with verbose=True:

order_machine_processing:
MINIMIZE
1*runtime + 0
SUBJECT TO
unused_o1_iA_m1: - 900 asn_o1_iA_m1 + dur_o1_iA_m1 <= 0

unused_o1_iA_m2: - 900 asn_o1_iA_m2 + dur_o1_iA_m2 <= 0

unused_o1_iB_m1: - 900 asn_o1_iB_m1 + dur_o1_iB_m1 <= 0

unused_o1_iB_m2: - 900 asn_o1_iB_m2 + dur_o1_iB_m2 <= 0

unused_o2_iA_m1: - 900 asn_o2_iA_m1 + dur_o2_iA_m1 <= 0

unused_o2_iA_m2: - 900 asn_o2_iA_m2 + dur_o2_iA_m2 <= 0

unused_o2_iD_m1: - 900 asn_o2_iD_m1 + dur_o2_iD_m1 <= 0

unused_o2_iD_m2: - 900 asn_o2_iD_m2 + dur_o2_iD_m2 <= 0

unused_o3_iE_m1: - 900 asn_o3_iE_m1 + dur_o3_iE_m1 <= 0

unused_o3_iE_m2: - 900 asn_o3_iE_m2 + dur_o3_iE_m2 <= 0

assign_o1_iA: asn_o1_iA_m1 + asn_o1_iA_m2 >= 1

time_o1_iA: dur_o1_iA_m1 + dur_o1_iA_m2 = 50

match_o1_iA_m1_startlo: 900 asn_o1_iA_m1 - start_o1_iA + start_o1_iA_m1 <= 900

match_o1_iA_m1_starthi: 900 asn_o1_iA_m1 + start_o1_iA - start_o1_iA_m1 <= 900

match_o1_iA_m1_durlo: 900 asn_o1_iA_m1 - dur_o1_iA + dur_o1_iA_m1 <= 900

match_o1_iA_m1_durhi: 900 asn_o1_iA_m1 + dur_o1_iA - dur_o1_iA_m1 <= 900

match_o1_iA_m2_startlo: 900 asn_o1_iA_m2 - start_o1_iA + start_o1_iA_m2 <= 900

match_o1_iA_m2_starthi: 900 asn_o1_iA_m2 + start_o1_iA - start_o1_iA_m2 <= 900

match_o1_iA_m2_durlo: 900 asn_o1_iA_m2 - dur_o1_iA + dur_o1_iA_m2 <= 900

match_o1_iA_m2_durhi: 900 asn_o1_iA_m2 + dur_o1_iA - dur_o1_iA_m2 <= 900

assign_o1_iB: asn_o1_iB_m1 + asn_o1_iB_m2 >= 1

time_o1_iB: dur_o1_iB_m1 + dur_o1_iB_m2 = 60

match_o1_iB_m1_startlo: 900 asn_o1_iB_m1 - start_o1_iB + start_o1_iB_m1 <= 900

match_o1_iB_m1_starthi: 900 asn_o1_iB_m1 + start_o1_iB - start_o1_iB_m1 <= 900

match_o1_iB_m1_durlo: 900 asn_o1_iB_m1 - dur_o1_iB + dur_o1_iB_m1 <= 900

match_o1_iB_m1_durhi: 900 asn_o1_iB_m1 + dur_o1_iB - dur_o1_iB_m1 <= 900

match_o1_iB_m2_startlo: 900 asn_o1_iB_m2 - start_o1_iB + start_o1_iB_m2 <= 900

match_o1_iB_m2_starthi: 900 asn_o1_iB_m2 + start_o1_iB - start_o1_iB_m2 <= 900

match_o1_iB_m2_durlo: 900 asn_o1_iB_m2 - dur_o1_iB + dur_o1_iB_m2 <= 900

match_o1_iB_m2_durhi: 900 asn_o1_iB_m2 + dur_o1_iB - dur_o1_iB_m2 <= 900

assign_o2_iA: asn_o2_iA_m1 + asn_o2_iA_m2 >= 1

time_o2_iA: dur_o2_iA_m1 + dur_o2_iA_m2 = 50

match_o2_iA_m1_startlo: 900 asn_o2_iA_m1 - start_o2_iA + start_o2_iA_m1 <= 900

match_o2_iA_m1_starthi: 900 asn_o2_iA_m1 + start_o2_iA - start_o2_iA_m1 <= 900

match_o2_iA_m1_durlo: 900 asn_o2_iA_m1 - dur_o2_iA + dur_o2_iA_m1 <= 900

match_o2_iA_m1_durhi: 900 asn_o2_iA_m1 + dur_o2_iA - dur_o2_iA_m1 <= 900

match_o2_iA_m2_startlo: 900 asn_o2_iA_m2 - start_o2_iA + start_o2_iA_m2 <= 900

match_o2_iA_m2_starthi: 900 asn_o2_iA_m2 + start_o2_iA - start_o2_iA_m2 <= 900

match_o2_iA_m2_durlo: 900 asn_o2_iA_m2 - dur_o2_iA + dur_o2_iA_m2 <= 900

match_o2_iA_m2_durhi: 900 asn_o2_iA_m2 + dur_o2_iA - dur_o2_iA_m2 <= 900

assign_o2_iD: asn_o2_iD_m1 + asn_o2_iD_m2 >= 1

time_o2_iD: dur_o2_iD_m1 + dur_o2_iD_m2 = 80

match_o2_iD_m1_startlo: 900 asn_o2_iD_m1 - start_o2_iD + start_o2_iD_m1 <= 900

match_o2_iD_m1_starthi: 900 asn_o2_iD_m1 + start_o2_iD - start_o2_iD_m1 <= 900

match_o2_iD_m1_durlo: 900 asn_o2_iD_m1 - dur_o2_iD + dur_o2_iD_m1 <= 900

match_o2_iD_m1_durhi: 900 asn_o2_iD_m1 + dur_o2_iD - dur_o2_iD_m1 <= 900

match_o2_iD_m2_startlo: 900 asn_o2_iD_m2 - start_o2_iD + start_o2_iD_m2 <= 900

match_o2_iD_m2_starthi: 900 asn_o2_iD_m2 + start_o2_iD - start_o2_iD_m2 <= 900

match_o2_iD_m2_durlo: 900 asn_o2_iD_m2 - dur_o2_iD + dur_o2_iD_m2 <= 900

match_o2_iD_m2_durhi: 900 asn_o2_iD_m2 + dur_o2_iD - dur_o2_iD_m2 <= 900

assign_o3_iE: asn_o3_iE_m1 + asn_o3_iE_m2 >= 1

time_o3_iE: dur_o3_iE_m1 + dur_o3_iE_m2 = 90

match_o3_iE_m1_startlo: 900 asn_o3_iE_m1 - start_o3_iE + start_o3_iE_m1 <= 900

match_o3_iE_m1_starthi: 900 asn_o3_iE_m1 + start_o3_iE - start_o3_iE_m1 <= 900

match_o3_iE_m1_durlo: 900 asn_o3_iE_m1 - dur_o3_iE + dur_o3_iE_m1 <= 900

match_o3_iE_m1_durhi: 900 asn_o3_iE_m1 + dur_o3_iE - dur_o3_iE_m1 <= 900

match_o3_iE_m2_startlo: 900 asn_o3_iE_m2 - start_o3_iE + start_o3_iE_m2 <= 900

match_o3_iE_m2_starthi: 900 asn_o3_iE_m2 + start_o3_iE - start_o3_iE_m2 <= 900

match_o3_iE_m2_durlo: 900 asn_o3_iE_m2 - dur_o3_iE + dur_o3_iE_m2 <= 900

match_o3_iE_m2_durhi: 900 asn_o3_iE_m2 + dur_o3_iE - dur_o3_iE_m2 <= 900

runtime_total_o3_iE_m1: - dur_o3_iE_m1 + runtime - start_o3_iE_m1 >= 0

runtime_total_o3_iE_m2: - dur_o3_iE_m2 + runtime - start_o3_iE_m2 >= 0

assign_o1_m1: asn_o1_iA_m1 + asn_o1_iB_m1 <= 1

assign_o1_m2: asn_o1_iA_m2 + asn_o1_iB_m2 <= 1

assign_o2_m1: asn_o2_iA_m1 + asn_o2_iD_m1 <= 1

assign_o2_m2: asn_o2_iA_m2 + asn_o2_iD_m2 <= 1

assign_o3_m1: asn_o3_iE_m1 <= 1

assign_o3_m2: asn_o3_iE_m2 <= 1

seq_o12_m1_iAA: dur_o1_iA_m1 + start_o1_iA_m1 - start_o2_iA_m1 <= 0

seq_o12_m1_iAD: dur_o1_iA_m1 + start_o1_iA_m1 - start_o2_iD_m1 <= 0

seq_o12_m1_iBA: dur_o1_iB_m1 + start_o1_iB_m1 - start_o2_iA_m1 <= 0

seq_o12_m1_iBD: dur_o1_iB_m1 + start_o1_iB_m1 - start_o2_iD_m1 <= 0

seq_o12_m2_iAA: dur_o1_iA_m2 + start_o1_iA_m2 - start_o2_iA_m2 <= 0

seq_o12_m2_iAD: dur_o1_iA_m2 + start_o1_iA_m2 - start_o2_iD_m2 <= 0

seq_o12_m2_iBA: dur_o1_iB_m2 + start_o1_iB_m2 - start_o2_iA_m2 <= 0

seq_o12_m2_iBD: dur_o1_iB_m2 + start_o1_iB_m2 - start_o2_iD_m2 <= 0

seq_o23_m1_iAE: dur_o2_iA_m1 + start_o2_iA_m1 - start_o3_iE_m1 <= 0

seq_clean_o23_m1_iAE: - asn_o2_iA_m1 - asn_o3_iE_m1 - dur_o2_iA_m1
 - start_o2_iA_m1 + start_o3_iE_m1 >= -1

seq_o23_m1_iDE: dur_o2_iD_m1 + start_o2_iD_m1 - start_o3_iE_m1 <= 0

seq_o23_m2_iAE: dur_o2_iA_m2 + start_o2_iA_m2 - start_o3_iE_m2 <= 0

seq_clean_o23_m2_iAE: - asn_o2_iA_m2 - asn_o3_iE_m2 - dur_o2_iA_m2
 - start_o2_iA_m2 + start_o3_iE_m2 >= -1

seq_o23_m2_iDE: dur_o2_iD_m2 + start_o2_iD_m2 - start_o3_iE_m2 <= 0

VARIABLES
0 <= asn_o1_iA_m1 <= 1 Integer
0 <= asn_o1_iA_m2 <= 1 Integer
0 <= asn_o1_iB_m1 <= 1 Integer
0 <= asn_o1_iB_m2 <= 1 Integer
0 <= asn_o2_iA_m1 <= 1 Integer
0 <= asn_o2_iA_m2 <= 1 Integer
0 <= asn_o2_iD_m1 <= 1 Integer
0 <= asn_o2_iD_m2 <= 1 Integer
0 <= asn_o3_iE_m1 <= 1 Integer
0 <= asn_o3_iE_m2 <= 1 Integer
1 <= dur_o1_iA <= 50 Continuous
dur_o1_iA_m1 <= 50 Continuous
dur_o1_iA_m2 <= 50 Continuous
1 <= dur_o1_iB <= 60 Continuous
dur_o1_iB_m1 <= 60 Continuous
dur_o1_iB_m2 <= 60 Continuous
1 <= dur_o2_iA <= 50 Continuous
dur_o2_iA_m1 <= 50 Continuous
dur_o2_iA_m2 <= 50 Continuous
1 <= dur_o2_iD <= 80 Continuous
dur_o2_iD_m1 <= 80 Continuous
dur_o2_iD_m2 <= 80 Continuous
1 <= dur_o3_iE <= 90 Continuous
dur_o3_iE_m1 <= 90 Continuous
dur_o3_iE_m2 <= 90 Continuous
runtime Continuous
start_o1_iA Continuous
start_o1_iA_m1 Continuous
start_o1_iA_m2 Continuous
start_o1_iB Continuous
start_o1_iB_m1 Continuous
start_o1_iB_m2 Continuous
start_o2_iA Continuous
start_o2_iA_m1 Continuous
start_o2_iA_m2 Continuous
start_o2_iD Continuous
start_o2_iD_m1 Continuous
start_o2_iD_m2 Continuous
start_o3_iE Continuous
start_o3_iE_m1 Continuous
start_o3_iE_m2 Continuous

Welcome to the CBC MILP Solver 
Version: 2.10.3 
Build Date: Dec 15 2019 

command line - .venv/lib/python3.10/site-packages/pulp/solverdir/cbc/linux/64/cbc /tmp/b35b0c4d0be94ca5bbd06f7e0e658e3e-pulp.mps timeMode elapsed branch printingOptions all solution /tmp/b35b0c4d0be94ca5bbd06f7e0e658e3e-pulp.sol (default strategy 1)
At line 2 NAME          MODEL
At line 3 ROWS
At line 87 COLUMNS
At line 331 RHS
At line 414 BOUNDS
At line 445 ENDATA
Problem MODEL has 82 rows, 41 columns and 222 elements
Coin0008I MODEL read with 0 errors
Option for timeMode changed from cpu to elapsed
Continuous objective value is 115 - 0.00 seconds
Cgl0003I 0 fixed, 0 tightened bounds, 18 strengthened rows, 26 substitutions
Cgl0003I 0 fixed, 0 tightened bounds, 1 strengthened rows, 0 substitutions
Cgl0004I processed model has 43 rows, 22 columns (4 integer (4 of which binary)) and 128 elements
Cbc0038I Initial state - 4 integers unsatisfied sum - 1.07601
Cbc0038I Pass   1: suminf.    0.00000 (0) obj. 175 iterations 10
Cbc0038I Solution found of 175
Cbc0038I Relaxing continuous gives 175
Cbc0038I Before mini branch and bound, 0 integers at bound fixed and 3 continuous
Cbc0038I Full problem 43 rows 22 columns, reduced to 39 rows 19 columns
Cbc0038I Mini branch and bound did not improve solution (0.00 seconds)
Cbc0038I Round again with cutoff of 169
Cbc0038I Pass   2: suminf.    0.01333 (1) obj. 169 iterations 5
Cbc0038I Pass   3: suminf.    0.46667 (1) obj. 169 iterations 7
Cbc0038I Pass   4: suminf.    0.44000 (1) obj. 169 iterations 4
Cbc0038I Pass   5: suminf.    0.44000 (1) obj. 169 iterations 0
Cbc0038I Pass   6: suminf.    0.44000 (1) obj. 169 iterations 0
Cbc0038I Pass   7: suminf.    0.64444 (2) obj. 169 iterations 5
Cbc0038I Pass   8: suminf.    0.44000 (1) obj. 169 iterations 6
Cbc0038I Pass   9: suminf.    0.03556 (1) obj. 169 iterations 7
Cbc0038I Pass  10: suminf.    0.39000 (2) obj. 169 iterations 2
Cbc0038I Pass  11: suminf.    0.03556 (1) obj. 169 iterations 2
Cbc0038I Pass  12: suminf.    0.03556 (1) obj. 169 iterations 0
Cbc0038I Pass  13: suminf.    0.39000 (2) obj. 169 iterations 2
Cbc0038I Pass  14: suminf.    0.03556 (1) obj. 169 iterations 2
Cbc0038I Pass  15: suminf.    0.01333 (1) obj. 169 iterations 7
Cbc0038I Pass  16: suminf.    0.46667 (1) obj. 169 iterations 7
Cbc0038I Pass  17: suminf.    0.20000 (1) obj. 169 iterations 14
Cbc0038I Pass  18: suminf.    0.01333 (1) obj. 169 iterations 7
Cbc0038I Pass  19: suminf.    0.03556 (1) obj. 169 iterations 7
Cbc0038I Pass  20: suminf.    0.44000 (1) obj. 169 iterations 6
Cbc0038I Pass  21: suminf.    0.44000 (1) obj. 169 iterations 0
Cbc0038I Pass  22: suminf.    0.44000 (1) obj. 169 iterations 0
Cbc0038I Pass  23: suminf.    0.44000 (1) obj. 169 iterations 0
Cbc0038I Pass  24: suminf.    0.03556 (1) obj. 169 iterations 7
Cbc0038I Pass  25: suminf.    0.44000 (1) obj. 169 iterations 6
Cbc0038I Pass  26: suminf.    0.44000 (1) obj. 169 iterations 0
Cbc0038I Pass  27: suminf.    0.07500 (1) obj. 169 iterations 15
Cbc0038I Pass  28: suminf.    0.03556 (1) obj. 169 iterations 8
Cbc0038I Pass  29: suminf.    0.03556 (1) obj. 169 iterations 0
Cbc0038I Pass  30: suminf.    0.49000 (2) obj. 169 iterations 9
Cbc0038I Pass  31: suminf.    0.49000 (2) obj. 169 iterations 0
Cbc0038I No solution found this major pass
Cbc0038I Before mini branch and bound, 0 integers at bound fixed and 0 continuous
Cbc0038I Full problem 43 rows 22 columns, reduced to 43 rows 22 columns
Cbc0038I Mini branch and bound did not improve solution (0.01 seconds)
Cbc0038I After 0.01 seconds - Feasibility pump exiting with objective of 175 - took 0.01 seconds
Cbc0012I Integer solution of 175 found by feasibility pump after 0 iterations and 0 nodes (0.01 seconds)
Cbc0031I 7 added rows had average density of 2.1428571
Cbc0013I At root node, 17 cuts changed objective from 115 to 191 in 6 passes
Cbc0014I Cut generator 0 (Probing) - 19 row cuts average 2.7 elements, 1 column cuts (1 active)  in 0.000 seconds - new frequency is 1
Cbc0014I Cut generator 1 (Gomory) - 16 row cuts average 5.8 elements, 0 column cuts (0 active)  in 0.000 seconds - new frequency is 1
Cbc0014I Cut generator 2 (Knapsack) - 0 row cuts average 0.0 elements, 0 column cuts (0 active)  in 0.000 seconds - new frequency is -100
Cbc0014I Cut generator 3 (Clique) - 0 row cuts average 0.0 elements, 0 column cuts (0 active)  in 0.000 seconds - new frequency is -100
Cbc0014I Cut generator 4 (MixedIntegerRounding2) - 1 row cuts average 3.0 elements, 0 column cuts (0 active)  in 0.000 seconds - new frequency is -100
Cbc0014I Cut generator 5 (FlowCover) - 3 row cuts average 3.0 elements, 0 column cuts (0 active)  in 0.000 seconds - new frequency is 1
Cbc0014I Cut generator 6 (TwoMirCuts) - 21 row cuts average 5.4 elements, 0 column cuts (0 active)  in 0.000 seconds - new frequency is -100
Cbc0001I Search completed - best objective 175, took 67 iterations and 0 nodes (0.01 seconds)
Cbc0035I Maximum depth 0, 0 variables fixed on reduced cost
Cuts at root node changed objective from 115 to 191
Probing was tried 6 times and created 20 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)
Gomory was tried 6 times and created 16 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)
Knapsack was tried 6 times and created 0 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)
Clique was tried 6 times and created 0 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)
MixedIntegerRounding2 was tried 6 times and created 1 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)
FlowCover was tried 6 times and created 3 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)
TwoMirCuts was tried 6 times and created 21 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)
ZeroHalf was tried 1 times and created 0 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)

Result - Optimal solution found

Objective value:                175.00000000
Enumerated nodes:               0
Total iterations:               67
Time (CPU seconds):             0.01
Time (Wallclock seconds):       0.01

Option for printingOptions changed from normal to all
Total time (CPU seconds):       0.01   (Wallclock seconds):       0.01

Order 1 item A processed by machine 1 for 50.0/50 from 0.0 to 50.0
Order 1 item B processed by machine 2 for 60.0/60 from 0.0 to 60.0
Order 2 item A processed by machine 2 for 50.0/50 from 60.0 to 110.0
Order 2 item D processed by machine 1 for 80.0/80 from 50.0 to 130.0
Order 3 item E processed by machine 1 for 45.0/90 from 130.0 to 175.0
Order 3 item E processed by machine 2 for 45.0/90 from 130.0 to 175.0

Things to note in the output:

  • Order 2 item A runtime is a normal runtime followed by a wait. This wait occurs so that the joint work on item E occurs simultaneously
  • There is only one cleanup time paid, on machine 1 between orders 2,3 and items A,E
  • The orders are indeed processed in sequence, but the optimizer takes advantage of the fact that machine 2 can start on order 2 while machine 1 is still working on order 1
Reinderien
  • 11,755
  • 5
  • 49
  • 77
  • wow, thank you for such a detailed solution. i have a few questions about your solution. since the runtimes are fixed, why do you create a seperate variable for duration as opposed to doing runtime * [asn_variable]? also, are the start time variables required to track the sequencing or is there another reason you added them? thanks – J. Doe Mar 27 '23 at 11:19
  • @J.Doe The runtimes are not fixed; multiple machines working on one item will produce smaller runtimes. The assignment variable being true does not mean "it will take the full 50 seconds"; it means "the machine is active for this item and will take greater than zero and up to 50 seconds depending on the presence of other machines" – Reinderien Mar 27 '23 at 12:45
  • Yes, start time variables are required to track sequencing, and most importantly to be able to calculate the total runtime. – Reinderien Mar 27 '23 at 12:46
  • could runtime / (asn_machine1 + asn_machine2) work? i am just thinking if theres a way to simplify. do you think this would work if it was scaled to maybe 100 orders with 500 different products and 6 machines? since every combination of sequence needs to be looked at, im not sure if it will scale with so many variables. – J. Doe Mar 27 '23 at 13:09
  • I kind of doubt it. Duration = runtime/assignment is non linear. Part of the problem is that your cleaning is inherently O(n^2). – Reinderien Mar 28 '23 at 03:08
  • makes sense. i will tune your code with some test data and see if it works, then ill let you know. thanks again! – J. Doe Mar 28 '23 at 12:03
  • @J.Doe if this answered your question please consider clicking on the accept tick. – Reinderien Mar 28 '23 at 12:06
  • i get this error when i try to run. PulpError: overlapping constraint names: unused_o1_iA_m1 – J. Doe Mar 28 '23 at 12:35
  • it looks like this line of code is running twice for the same row in options df, instead of iterating each row. """options.apply( make_option_vars, axis=1, prob=prob, item_runtimes=item_runtimes, time_buffer=time_buffer)""" – J. Doe Mar 28 '23 at 12:47
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/252835/discussion-between-reinderien-and-j-doe). – Reinderien Mar 28 '23 at 13:01
  • sidenote, do you think it would be difficult to constrain each machine to wait until the current order is completed for all machines before starting on the next order? – J. Doe Mar 31 '23 at 10:24
  • It would actually be easier to do that, though it will produce a solution that has longer runtime. Post a new question, link it here and I'll show you – Reinderien Mar 31 '23 at 12:50
  • https://stackoverflow.com/questions/75900290/how-to-create-this-lp-logic-using-pulp – J. Doe Mar 31 '23 at 15:32
  • so im trying to constrain each machine to run together so only one activity can occur at a time (machine cannot start on next order or machine cannot be cleaned while another machine is working on the current order). to add this constraint, would i need to create a new variable to track the start time of each order and use that to enforce the start times for each asn? thanks – J. Doe Apr 13 '23 at 14:25
  • Set up the problem so that time slots are implicitly one machine only. Do not represent start times. Simplify the objective so that it's a sum of all runtimes and cleaning times. Remove the end-time slack variable. – Reinderien Apr 13 '23 at 14:29
  • so i remove the start times entirely, have duration variables at o_i_m, o_i, and order levels, keep the asn variables the same, and then simplify the objective as you say. will i be able to constrain the sequencing with just the durations? – J. Doe Apr 13 '23 at 14:44
  • I don't think you need to constrain the sequences at all. They can be implied. – Reinderien Apr 14 '23 at 00:25
  • i added an edit above for the formulation. will this be a proper way to add the cleaning constraints? – J. Doe Apr 17 '23 at 05:30
  • No, because there is no min function directly expressible in LP. You need to linearise with another sum using the previous and current assignments. – Reinderien Apr 17 '23 at 11:37
  • Consider using fixed bounds of 0 <= C <= Cxy, and constraints of `C >= Cxy - CxyAoim1 - CxyAoim0` – Reinderien Apr 17 '23 at 12:14
  • How do I set up fixed bounds for C? Since the value depends on the previous item, then Cxy can be multiple values right? – J. Doe Apr 18 '23 at 00:58
  • For the constraint, should it be C >= -Cxy + CxyAoim1 + CxyAoim0? Since C = Cxy if both m1 and m0 are 1, else C=0. – J. Doe Apr 18 '23 at 01:03
  • Back to https://chat.stackoverflow.com/rooms/253204/milp if you could – Reinderien Apr 18 '23 at 01:13
  • Also, if I wanted to change objective func from min sum(A,B,C) to min max(A,B,C), I could create dummy var D and do min D s.t D >= A, B, C. Would that work? – J. Doe Apr 18 '23 at 01:18
  • i got the logic to work. im thinking about adding something else to the LP and wanted to get your opinion if you get a chance. what would be the best to discuss? – J. Doe May 08 '23 at 12:07
  • @J.Doe https://chat.stackoverflow.com/rooms/info/253534/milp since the old one expired – Reinderien May 08 '23 at 12:19