Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added nesting module, class, gh component, format and lint #248

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased

### Added
* Added `Nester` class for one-dimensional bin packing. this is to fit the beams of an assembly into material stock of a given length
* Added `Beam Nester` GH component for `Nester` class

### Changed

Expand Down
32 changes: 32 additions & 0 deletions src/compas_timber/ghpython/components/CT_Beam_Nesting/code.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from ghpythonlib.componentbase import executingcomponent as component
from Grasshopper.Kernel.GH_RuntimeMessageLevel import Warning
from Grasshopper import DataTree
from Grasshopper.Kernel.Data import GH_Path
from compas_timber.planning import Nester


class NestingComponent(component):

def RunScript(self, assembly, stock_length, tolerance, iterations):

if not assembly:
self.AddRuntimeMessage(Warning, "input Assembly failed to collect data")
return
if not stock_length:
self.AddRuntimeMessage(Warning, "input Stock Length failed to collect data")
return

nester = Nester()
nesting_dict = nester.get_bins(assembly.beams, stock_length, tolerance, iterations)

data_tree = DataTree[object]()
for key, value in nesting_dict.items():
path = GH_Path(int(key))
data_tree.AddRange(value, path)

info = []
info.append("total length: {:.2f}".format(nester.total_length))
info.append("bin count = {0}".format(len(nesting_dict.items())))
info.append("cutoff waste = {:.2f}".format(nester.total_space(nesting_dict)))

return data_tree, info
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{
"name": "Beam Nester",
"nickname": "Nester",
"category": "COMPAS Timber",
"subcategory": "Fabrication",
"description": "Nests beams into stock lengths.",
"exposure": 2,
"ghpython": {
"isAdvancedMode": true,
"iconDisplay": 0,
"inputParameters": [
{
"name": "Assembly",
"description": "Compas_Timber Assembly",
"typeHintID": "none",
"scriptParamAccess": 0
},
{
"name": "Stock Length",
"description": "length of timber stock the beams are packed into.",
"typeHintID": "float",
"scriptParamAccess": 0
},
{
"name": "Tolerance",
"description": "amount of cutoff per stock piece that is considered complete.",
"typeHintID": "float",
"scriptParamAccess": 0
},
{
"name": "Iterations",
"description": "number of cycles the nester will run.",
"typeHintID": "int",
"scriptParamAccess":0
}
],
"outputParameters": [
{
"name": "Packing",
"description": "Packing info as a tree."
},
{
"name": "Info",
"description": "General info about packing."
}
]
}
}
8 changes: 2 additions & 6 deletions src/compas_timber/planning/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,6 @@
from .sequencer import BuildingPlan
from .sequencer import SimpleSequenceGenerator
from .sequencer import Step
from .beam_nesting import Nester

__all__ = [
"Actor",
"BuildingPlan",
"Step",
"SimpleSequenceGenerator",
]
__all__ = ["Actor", "BuildingPlan", "Step", "SimpleSequenceGenerator", "Nester"]
179 changes: 179 additions & 0 deletions src/compas_timber/planning/beam_nesting.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
from collections import OrderedDict
import random


class Nester(object):
"""Class for nesting beams into a stock beam


Attributes
----------
stock_length : float
length of the stock beam in which to be nested
tolerance : float
tolerance for the nesting algorithm, if the remaining space in a bin is less than the tolerance, the bin is considered full
total_length : float
total length of all beams to be nested

"""

def __init__(self):
pass

def shuffle(self, lst):
random.shuffle(lst)

def space_remaining(self, bin):
"""returns the space remaining in a bin"""
if len(bin) == 0:
return self.stock_length
return self.stock_length - (sum([beam.blank_length for beam in bin]))

def sorted_dict(self, bin_dict):
"""sorts a dictionary of bins by space remaining in each bin"""
if len(bin_dict.items()) < 2:
return bin_dict
bin_list = sorted(bin_dict.values(), key=lambda x: self.space_remaining(x))
dict = OrderedDict()
for i in range(len(bin_list)):
dict[i] = bin_list[i]
return dict

def total_space(self, bin_dict):
"""returns the total space remaining in all bins, AKA total waste"""
return sum([self.space_remaining(bin) for bin in bin_dict.values()])

def get_bins_basic(self, beams):
"""returns a dictionary of bins with beams nested in them"""
beams_sorted = sorted(beams, key=lambda z: z.length, reverse=True)
bins = OrderedDict([(0, [])])
for beam in beams_sorted:
fits = False
bins = self.sorted_dict(bins)
for bin in bins.values():
if self.space_remaining(bin) >= beam.blank_length:
bin.append(beam)
fits = True
break
if not fits:
bins[str(len(bins))] = [beam]
return bins

def fill_bins(self, bins, beams, sort=True, shuffle=False):
"""fills a partial bins dictionary with beams, returns a dictionary of bins with beams nested in them"""
if sort:
beams_sorted = sorted(beams, key=lambda z: z.length, reverse=True)
elif shuffle:
beams_sorted = beams
self.shuffle(beams_sorted)
for beam in beams_sorted:
fits = False
bins = self.sorted_dict(bins)
for bin in bins.values():
if self.space_remaining(bin) >= beam.blank_length:
bin.append(beam)
fits = True
break
if not fits:
bins[str(len(bins))] = [beam]
return bins

def longest_cutoff(self, bin_dict):
"""returns the longest cutoff in a bin dictionary"""
sorted_bins = self.sorted_dict(bin_dict)
return [self.space_remaining(bin) for bin in sorted_bins.values()]

def parse_bins(self, bin_dict):
"""evaluates the success of the nesting, returns a dictionary with the results of the nesting process"""
dict_out = {"done": False}
dict_out["finished_bins"] = bin_dict
if self.total_space(bin_dict) < self.stock_length:
dict_out["done"] = True
return dict_out

else:
recycled_beams = []
temporary_bins = OrderedDict()
for bin in bin_dict.values():
if self.space_remaining(bin) > self.tolerance:
recycled_beams.extend(bin)
else:
temporary_bins[str(len(temporary_bins))] = bin
dict_out["temporary_bins"] = temporary_bins
dict_out["recycled_beams"] = recycled_beams
return dict_out

def validate_bin_results(self, bins, beams):
beam_list_out = []
for val in bins.values():
beam_list_out.extend(val)

if set(beam_list_out) != set(beams):
raise Exception("Beams input and nesting output dont match")

def get_bins(self, beams, stock_length, tolerance=None, iterations=0):
"""returns a dictionary of bins with beams nested in them

Parameters
----------
beams : list(:class:`compas_timber.parts.Beam`)
list of beams to be nested
stock_length : float
length of the stock beam in which to be nested
tolerance : float
tolerance for the nesting algorithm, if the remaining space in a bin is less than the tolerance, the bin is considered full
iterations : int
number of iterations to run the nesting algorithm, the algorithm will stop when the total waste is less than the stock length

"""
self.stock_length = stock_length
if tolerance is None:
self.tolerance = stock_length / 100
else:
self.tolerance = tolerance
if iterations is None:
iterations = 0

self.total_length = sum([beam.blank_length for beam in beams])
bins_out = None
all_bins = []

if iterations == 0:
return self.get_bins_basic(beams)

else:
for i in range(iterations): # try with different shuffling
these_beams = beams
these_bins = self.get_bins_basic(these_beams)
results_dict = self.parse_bins(these_bins)
if results_dict["done"]: # if the nesting is successful
bins_out = results_dict["finished_bins"]
else:
sort = False
shuffle = True
for x in range(
iterations
): # tries to repack the beams that don't fit in the bins within the cutoff tolerance
these_beams = results_dict["recycled_beams"]
temp_bins = self.fill_bins(results_dict["temporary_bins"], these_beams, sort, shuffle)
results_dict = self.parse_bins(temp_bins)
if results_dict["done"]:
bins_out = results_dict["finished_bins"]
print("success after {0} iterations.".format(x))
break
elif x == iterations - 1: # if the last iteration is reached, the best result is taken
all_bins.append(results_dict["finished_bins"])
else:
sort = not sort
shuffle = not shuffle
if results_dict["done"]:
break

if not bins_out:
bins_out = min(
all_bins, key=lambda x: len(x)
) # if no successful nesting is found, the one with the least bins is taken

self.validate_bin_results(bins_out, beams)

return bins_out
Loading