Skip to content

Commit

Permalink
Allow for prioritsation of items (cannot be in last/partial press)
Browse files Browse the repository at this point in the history
  • Loading branch information
tungufoss committed Jan 15, 2024
1 parent 71a5f6a commit 3985d94
Show file tree
Hide file tree
Showing 6 changed files with 151 additions and 109 deletions.
170 changes: 85 additions & 85 deletions data/glulam.csv
Original file line number Diff line number Diff line change
@@ -1,85 +1,85 @@
order,quantity,depth,height,width,layers
SP703563,5,115,315,4770,7
SP703563,5,90,270,4935,6
SP703563,10,160,315,5850,7
SP703563,5,160,315,6600,7
SP703563,5,160,315,6691,7
SP703563,5,90,135,2463,3
SP703563,5,115,315,9017,7
SP703563,14,140,270,4820,6
SP703563,5,185,360,5857,8
SP703563,5,185,360,6693,8
SP703563,5,185,360,5857,8
SP703563,5,185,360,6693,8
SP703563,5,115,225,2508,5
SP703563,5,90,180,2508,4
SP703563,8,140,270,948,6
SP706182,17,140,315,4460,7
SP706182,6,185,540,9200,12
SP706182,10,140,315,4560,7
SP706182,9,140,315,4460,7
SP707443,6,185,225,3617,5
SP707443,4,90,225,2100,5
SP707443,7,185,225,3519,5
SP707443,7,90,135,500,3
SP707443,1,185,225,3617,5
SP707443,2,185,225,3519,5
SP707443,6,185,225,3939,5
SP707443,2,185,180,3991,4
SP707443,10,185,180,3963,4
SP707443,2,185,180,3974,4
SP707443,1,115,315,17689,7
SP707443,1,115,315,11400,7
SP707443,1,115,315,11041,7
SP707443,1,115,315,7430,7
SP707443,1,115,315,10793,7
SP707443,1,140,270,3736,6
SP707443,1,115,270,4178,6
SP707443,2,90,180,3730,4
SP707443,1,90,225,4204,5
SP707443,5,90,225,3315,5
SP707443,5,90,225,4000,5
SP707443,5,90,225,3421,5
SP707443,5,90,225,3440,5
SP707443,4,90,225,3440,5
SP707443,4,90,225,3358,5
SP707443,8,90,180,3440,4
SP708587,144,90,225,5013,5
SP708587,24,140,225,4993,5
SP708587,24,91,720,10215,16
SP708587,24,91,1215,9981,27
SP708587,36,115,135,5013,3
SP708587,6,185,225,2357,5
SP708587,8,90,180,2258,4
SP708587,8,90,225,2144,5
SP708587,4,140,225,2528,5
SP708587,28,90,180,2548,4
SP708587,8,90,180,2431,4
SP708587,8,90,225,2339,5
SP708587,28,90,225,5513,5
SP708587,4,90,225,5495,5
SP708587,5,140,225,5495,5
SP708587,3,140,270,5331,6
SP708587,24,140,180,4779,4
SP708587,36,140,315,1048,7
SP708587,36,90,225,1780,5
SP708588,36,140,225,7586,5
SP708588,16,90,225,4336,5
SP708588,64,90,225,4363,5
SP708588,4,140,225,4336,5
SP708588,16,140,225,4363,5
SP708588,4,115,315,4650,7
SP708588,16,115,315,4600,7
SP708588,20,140,225,3324,5
SP708588,20,115,225,3324,5
SP708588,20,115,180,3324,4
SP708588,20,115,90,2500,2
SP708588,20,115,90,1100,2
SP708588,4,90,450,7677,10
SP708588,4,90,450,3394,10
SP708588,4,90,90,2881,2
SP708588,4,140,90,2938,2
SP708588,4,115,225,3618,5
SP708588,4,115,225,3905,5
SP708588,4,115,225,4190,5
SP708588,4,115,225,4520,5
order,quantity,depth,height,width,layers,priority
SP703563,5,115,315,4770,7,FALSE
SP703563,5,90,270,4935,6,FALSE
SP703563,10,160,315,5850,7,FALSE
SP703563,5,160,315,6600,7,FALSE
SP703563,5,160,315,6691,7,FALSE
SP703563,5,90,135,2463,3,FALSE
SP703563,5,115,315,9017,7,FALSE
SP703563,14,140,270,4820,6,FALSE
SP703563,5,185,360,5857,8,FALSE
SP703563,5,185,360,6693,8,FALSE
SP703563,5,185,360,5857,8,FALSE
SP703563,5,185,360,6693,8,FALSE
SP703563,5,115,225,2508,5,FALSE
SP703563,5,90,180,2508,4,FALSE
SP703563,8,140,270,948,6,FALSE
SP706182,17,140,315,4460,7,FALSE
SP706182,6,185,540,9200,12,FALSE
SP706182,10,140,315,4560,7,FALSE
SP706182,9,140,315,4460,7,FALSE
SP707443,6,185,225,3617,5,FALSE
SP707443,4,90,225,2100,5,FALSE
SP707443,7,185,225,3519,5,FALSE
SP707443,7,90,135,500,3,FALSE
SP707443,1,185,225,3617,5,FALSE
SP707443,2,185,225,3519,5,FALSE
SP707443,6,185,225,3939,5,FALSE
SP707443,2,185,180,3991,4,FALSE
SP707443,10,185,180,3963,4,FALSE
SP707443,2,185,180,3974,4,FALSE
SP707443,1,115,315,17689,7,FALSE
SP707443,1,115,315,11400,7,FALSE
SP707443,1,115,315,11041,7,FALSE
SP707443,1,115,315,7430,7,FALSE
SP707443,1,115,315,10793,7,FALSE
SP707443,1,140,270,3736,6,FALSE
SP707443,1,115,270,4178,6,FALSE
SP707443,2,90,180,3730,4,FALSE
SP707443,1,90,225,4204,5,FALSE
SP707443,5,90,225,3315,5,FALSE
SP707443,5,90,225,4000,5,FALSE
SP707443,5,90,225,3421,5,FALSE
SP707443,5,90,225,3440,5,FALSE
SP707443,4,90,225,3440,5,FALSE
SP707443,4,90,225,3358,5,FALSE
SP707443,8,90,180,3440,4,FALSE
SP708587,144,90,225,5013,5,FALSE
SP708587,24,140,225,4993,5,FALSE
SP708587,24,91,720,10215,16,FALSE
SP708587,24,91,1215,9981,27,FALSE
SP708587,36,115,135,5013,3,FALSE
SP708587,6,185,225,2357,5,FALSE
SP708587,8,90,180,2258,4,FALSE
SP708587,8,90,225,2144,5,FALSE
SP708587,4,140,225,2528,5,FALSE
SP708587,28,90,180,2548,4,FALSE
SP708587,8,90,180,2431,4,FALSE
SP708587,8,90,225,2339,5,FALSE
SP708587,28,90,225,5513,5,FALSE
SP708587,4,90,225,5495,5,FALSE
SP708587,5,140,225,5495,5,FALSE
SP708587,3,140,270,5331,6,FALSE
SP708587,24,140,180,4779,4,FALSE
SP708587,36,140,315,1048,7,FALSE
SP708587,36,90,225,1780,5,FALSE
SP708588,36,140,225,7586,5,FALSE
SP708588,16,90,225,4336,5,FALSE
SP708588,64,90,225,4363,5,FALSE
SP708588,4,140,225,4336,5,FALSE
SP708588,16,140,225,4363,5,FALSE
SP708588,4,115,315,4650,7,FALSE
SP708588,16,115,315,4600,7,FALSE
SP708588,20,140,225,3324,5,FALSE
SP708588,20,115,225,3324,5,FALSE
SP708588,20,115,180,3324,4,FALSE
SP708588,20,115,90,2500,2,FALSE
SP708588,20,115,90,1100,2,FALSE
SP708588,4,90,450,7677,10,FALSE
SP708588,4,90,450,3394,10,FALSE
SP708588,4,90,90,2881,2,FALSE
SP708588,4,140,90,2938,2,FALSE
SP708588,4,115,225,3618,5,FALSE
SP708588,4,115,225,3905,5,FALSE
SP708588,4,115,225,4190,5,FALSE
SP708588,4,115,225,4520,5,FALSE
1 change: 0 additions & 1 deletion main.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ def main(file_path, depth, name, run, mode, overwrite):
return



if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Glulam Production Optimizer")
parser.add_argument(
Expand Down
53 changes: 43 additions & 10 deletions models/pack_n_press.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@ def cb(model, where):
"""
if where == gp.GRB.Callback.MIP:
# Check if a feasible solution is found
if model.cbGet(gp.GRB.Callback.MIP_SOLCNT) > 0 and not hasattr(model, '_feasible_time'):
model._feasible_time = time.time()
if model.cbGet(gp.GRB.Callback.MIP_SOLCNT) > 0 and not hasattr(model, '_feasible'):
model._feasible = time.time()
model._time = time.time()
logger.info(
f"First feasible solution found after {(model._feasible_time - model._start_time) / 60:.2f} minutes.")
f"First feasible solution found after {(model._feasible - model._start_time) / 60:.2f} minutes.")

if where == gp.GRB.Callback.MIPNODE:
# Get model objective
Expand All @@ -43,14 +43,15 @@ def cb(model, where):
model._time = time.time()

# Terminate if a feasible solution has been found and no improvement for over a minute
if hasattr(model, '_feasible_time') and model._feasible_time:
if hasattr(model, '_feasible') and model._feasible:
if time.time() - model._time > GlulamConfig.GUROBI_NO_IMPROVEMENT_TIME_LIMIT:
model._terminated_early = time.time()
model.terminate()
logger.warning(f"Terminating optimization: no improvement for over "
f"{GlulamConfig.GUROBI_NO_IMPROVEMENT_TIME_LIMIT / 60:.0f} minute. "
f"Elapsed time since first feasible solution: "
f"{(model._terminated_early - model._feasible_time) / 60:.2f} minutes.")
if not hasattr(model, '_terminated'):
model._terminated = time.time()
logger.warning(f"Terminating optimization: no improvement for over "
f"{GlulamConfig.GUROBI_NO_IMPROVEMENT_TIME_LIMIT / 60:.0f} minute. "
f"Elapsed time since first feasible solution: "
f"{(model._terminated - model._feasible) / 60:.2f} minutes.")


class GlulamPackagingProcessor:
Expand Down Expand Up @@ -137,6 +138,14 @@ def RW(self):
def I(self):
return self.patterns.I

@property
def I_priority(self):
"""
The set of orders that are prioritized. I_priority ⫋ I.
They that cannot be in the last press (which can be a partial press).
"""
return self.patterns.data.priority_items

@property
def J(self):
return self.patterns.J
Expand Down Expand Up @@ -186,6 +195,7 @@ def pack_n_press(self, time_limit=GlulamConfig.GUROBI_TIME_LIMIT):
self.x = None
self.xn = None
self.h = None
self.run_summary = None

# parameters
bigM = 1e8 # a big number
Expand Down Expand Up @@ -275,13 +285,19 @@ def pack_n_press(self, time_limit=GlulamConfig.GUROBI_TIME_LIMIT):
# make sure that the length of the region is at least the length of the longest pattern in use
pmodel.addConstrs(Lp[k, r] >= x1[j, k, r] * self.L[j] for j in self.J for r in self.R for k in self.K)

# Ensure priority items are not produced in the last press
for j in self.J:
if any(self.A[i, j] > 0 for i in self.I_priority): # This pattern produces a priority item
pmodel.addConstrs(
(x1[j, k, r] == 0 for k in self.K[:-1] for r in self.R), name="no_priority_in_last_press")

# define the surplus of each pattern in each press and region as the difference between the length of the
# pattern and the length of the region
pmodel.addConstrs(
((Lp[k, r] - self.L[j]) * self.H[j] <= F[j, k, r] + (1 - x1[j, k, r]) * bigM
for j in self.J for k in self.K for r in self.R), name="F_surplus_definition")

# now we add the objective function as the sum of waste for all presses and the difference between demand and supply
# the objective function as the sum of waste for all presses and the difference between demand and supply
pmodel.setObjective(
gp.quicksum(F[j, k, r] for j in self.J for k in self.K for r in self.R)
, gp.GRB.MINIMIZE)
Expand Down Expand Up @@ -331,6 +347,23 @@ def pack_n_press(self, time_limit=GlulamConfig.GUROBI_TIME_LIMIT):
axis=0) # Waste in m^2
logger.info(f'Total waste: {self.TotalWaste:.3f} m^2')
logger.debug(f'Waste:\n{self.Waste}')

# Maintain a summary of the run
self.run_summary = {
'time': int(time.time() - pmodel._start_time),
'first_feasible': int(pmodel._feasible - pmodel._start_time) if hasattr(pmodel, '_feasible') else None,
'terminated_early': int(pmodel._terminated - pmodel._feasible) if hasattr(pmodel, '_terminated') else None,
'nconstrs': pmodel.NumConstrs,
'nvars': pmodel.NumVars,
'status': pmodel.status,
'npatterns': self.patterns.n,
'norders': self.patterns.m,
'npresses': self.number_of_presses,
'npresses_used': np.sum(self.presses_in_use),
'objective': self.ObjectiveValue,
'total_waste': self.TotalWaste,
}

return True

def print_results(self):
Expand Down
9 changes: 4 additions & 5 deletions slurm.sh
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
#!/bin/bash

# Default values
hours=5
cpus=48
hours=8
cpus=12
mem=3900
partition=48cpu_192mem

Expand Down Expand Up @@ -31,12 +31,11 @@ cat > "$job_script" <<EOF
#SBATCH --cpus-per-task=${cpus}
#SBATCH --mem-per-cpu=${mem}
#SBATCH --time=${hours}:00:00
#SBATCH --output=data/slurm/%x_%j_%a.out
#SBATCH --error=data/slurm/%x_%j_%a.err
#SBATCH --output=data/slurm/%x_%a_%j.out
#SBATCH --error=data/slurm/%x_%a_%j.err
#SBATCH --array=1-${runs}%${max_sessions}
# Run the command for this array job
echo make data/$version/soln_ES_d${depth}_\${SLURM_ARRAY_TASK_ID}.json depth=${depth} run=\${SLURM_ARRAY_TASK_ID} VERSION=${version}
make data/$version/soln_ES_d${depth}_\${SLURM_ARRAY_TASK_ID}.json depth=${depth} run=\${SLURM_ARRAY_TASK_ID} VERSION=${version}
EOF

Expand Down
14 changes: 7 additions & 7 deletions strategies/evolution_strategy.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,10 +164,11 @@ def Objective(self):

return objective, press

def _add_stats(self, x, sigma, gen, current_obj):
def _add_stats(self, x, sigma, gen, press):
if self.stats is None:
self.stats = {'xstar': [], 'sstar': [], 'sucstar': [], 'waste': [], 'npresses': [], 'x': [], 'sigma': [],
'gen': [], 'npatterns': [], 'objective': []}
'gen': [], 'run_summary': []
}
self.stats['xstar'].append(self.xstar)
self.stats['sstar'].append(self.sstar)
self.stats['sucstar'].append(self.sucstar)
Expand All @@ -176,8 +177,7 @@ def _add_stats(self, x, sigma, gen, current_obj):
self.stats['x'].append(x)
self.stats['sigma'].append(sigma)
self.stats['gen'].append(gen)
self.stats['npatterns'].append(self.merged.n)
self.stats['objective'].append(current_obj)
self.stats['run_summary'].append(press.run_summary)
logger.info(
f"Stats - Generation {gen} - waste = {self.waste}, npresses = {self.npresses}, "
f"xstar = {self.xstar} (#{len(self.xstar)})")
Expand All @@ -197,7 +197,7 @@ def Search(self, filename, x=None):
def save_results(filename):
""" Save results to json. """
results = {'roll_widths': self.xstar, 'presses': self.npresses, 'waste': self.waste, 'stats': self.stats,
'depth': self.merged.data.depth, 'n': self.merged.n, 'm': self.merged.m}
'depth': self.merged.data.depth }
with open(filename, 'w') as f:
json.dump(convert_numpy_to_json(results), f, indent=4)
logger.info(f"Saved the solution to {filename}")
Expand Down Expand Up @@ -231,7 +231,7 @@ def save_results(filename):
self.Selection(x, sigma, success, press)

# now lets start the search, for max max_generations
self._add_stats(x, sigma, 0, (self.waste, self.npresses))
self._add_stats(x, sigma, 0, press)
for gen in range(1, self.max_generations):
logger.info(f"Generation: {gen}/{self.max_generations}")

Expand Down Expand Up @@ -289,7 +289,7 @@ def save_results(filename):
logger.info(f"NEW BEST: the successes are {self.sucstar}")
logger.info(f"NEW BEST: the number of patterns is {self.merged.n}")

self._add_stats(x, sigma, gen, (waste_, npresses_))
self._add_stats(x, sigma, gen, press)
save_results(filename + ".part")

logger.info(f"Search - Finished the search after {self.max_generations} generations.")
Expand Down
13 changes: 12 additions & 1 deletion utils/data_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ def __init__(self, file_path, depth):
# Reset index
self._filtered_data.reset_index(drop=True, inplace=True)

# Priority items are those defined in the file, but if they are all the same order, then there is no priority
# it is the index for the items with column 'priority' set to True (but only if there are some items with False)
self._priority_items = []
priority_items = self._filtered_data['priority']
if priority_items.any() and not priority_items.all():
self._priority_items = np.where(priority_items)[0]

@property
def area(self):
""" Compute the minimum area needed to fit all orders in square meters. """
Expand Down Expand Up @@ -68,6 +75,11 @@ def orders(self):
""" Set of orders. """
return sorted(list(set(self.order.tolist())))

@property
def priority_items(self):
""" Set of priority items. """
return self._priority_items


def convert_numpy_to_json(items):
""" Convert numpy types to json compatible types. """
Expand All @@ -81,4 +93,3 @@ def convert_numpy_to_json(items):
return {k: convert_numpy_to_json(v) for k, v in items.items()}
if isinstance(items, int) or isinstance(items, float) or isinstance(items, str):
return items

0 comments on commit 3985d94

Please sign in to comment.