diff options
| -rw-r--r-- | playground/csp/.gitignore | 1 | ||||
| -rw-r--r-- | playground/csp/main.py | 50 | ||||
| -rw-r--r-- | src_py/opcut/common.py | 49 | ||||
| -rw-r--r-- | src_py/opcut/csp.py | 111 | ||||
| -rw-r--r-- | src_py/opcut/output.py | 108 |
5 files changed, 217 insertions, 102 deletions
diff --git a/playground/csp/.gitignore b/playground/csp/.gitignore new file mode 100644 index 0000000..61d48f9 --- /dev/null +++ b/playground/csp/.gitignore @@ -0,0 +1 @@ +/output.pdf diff --git a/playground/csp/main.py b/playground/csp/main.py index d08f748..8aed531 100644 --- a/playground/csp/main.py +++ b/playground/csp/main.py @@ -1,38 +1,42 @@ import sys sys.path += ['../../src_py'] +from opcut import common from opcut import csp +from opcut import output def main(): panels = [ - csp.Panel(id='p1', width=100, height=100) + common.Panel(id='p1', width=100, height=65) ] items = [ - csp.Item(id='i1', width=10, height=10, rotate=True), - csp.Item(id='i2', width=10, height=9, rotate=True), - csp.Item(id='i3', width=20, height=8, rotate=True), - csp.Item(id='i4', width=10, height=20, rotate=True), - csp.Item(id='i5', width=30, height=19, rotate=True), - csp.Item(id='i6', width=10, height=18, rotate=True), - csp.Item(id='i7', width=10, height=17, rotate=True), - csp.Item(id='i8', width=20, height=16, rotate=True), - csp.Item(id='i9', width=10, height=15, rotate=True), - csp.Item(id='i10', width=30, height=14, rotate=True), - csp.Item(id='i11', width=10, height=20, rotate=True), - csp.Item(id='i12', width=30, height=19, rotate=True), - csp.Item(id='i13', width=10, height=18, rotate=True), - csp.Item(id='i14', width=10, height=17, rotate=True), - csp.Item(id='i15', width=20, height=16, rotate=True), - csp.Item(id='i16', width=10, height=15, rotate=True), - csp.Item(id='i17', width=30, height=14, rotate=True), - csp.Item(id='i18', width=10, height=20, rotate=True), - csp.Item(id='i19', width=30, height=19, rotate=True), + common.Item(id='i1', width=10, height=10, can_rotate=True), + common.Item(id='i2', width=10, height=9, can_rotate=True), + common.Item(id='i3', width=20, height=8, can_rotate=True), + common.Item(id='i4', width=10, height=20, can_rotate=True), + common.Item(id='i5', width=30, height=19, can_rotate=True), + common.Item(id='i6', width=10, height=18, can_rotate=False), + common.Item(id='i7', width=10, height=17, can_rotate=True), + common.Item(id='i8', width=20, height=16, can_rotate=True), + common.Item(id='i9', width=10, height=15, can_rotate=True), + common.Item(id='i10', width=30, height=14, can_rotate=True), + common.Item(id='i11', width=10, height=20, can_rotate=True), + common.Item(id='i12', width=19, height=30, can_rotate=False), + common.Item(id='i13', width=10, height=18, can_rotate=True), + common.Item(id='i14', width=10, height=17, can_rotate=True), + common.Item(id='i15', width=20, height=16, can_rotate=True), + common.Item(id='i16', width=10, height=15, can_rotate=True), + common.Item(id='i17', width=30, height=14, can_rotate=True), + common.Item(id='i18', width=10, height=20, can_rotate=True), + common.Item(id='i19', width=30, height=19, can_rotate=True), ] - cut_width = 1 - method = csp.Method.FORWARD_GREEDY + cut_width = 0.4 + method = common.Method.FORWARD_GREEDY result = csp.calculate(panels, items, cut_width, method) - print(result) + pdf_bytes = output.generate_pdf(result) + with open('output.pdf', 'wb') as f: + f.write(pdf_bytes) if __name__ == '__main__': diff --git a/src_py/opcut/common.py b/src_py/opcut/common.py new file mode 100644 index 0000000..c7884e8 --- /dev/null +++ b/src_py/opcut/common.py @@ -0,0 +1,49 @@ +import enum + +from opcut import util + + +State = util.namedtuple( + 'State', + ['cut_width', 'float'], + ['panels', 'List[Panel]'], + ['items', 'List[Item]'], + ['used', 'List[Used]'], + ['unused', 'List[Unused]']) + +Panel = util.namedtuple( + 'Panel', + ['id', 'Any'], + ['width', 'float'], + ['height', 'float']) + +Item = util.namedtuple( + 'Item', + ['id', 'Any'], + ['width', 'float'], + ['height', 'float'], + ['can_rotate', 'bool']) + +Used = util.namedtuple( + 'Used', + ['panel', 'Panel'], + ['item', 'Item'], + ['x', 'float'], + ['y', 'float'], + ['rotate', 'bool']) + +Unused = util.namedtuple( + 'Unused', + ['panel', 'Panel'], + ['width', 'float'], + ['height', 'float'], + ['x', 'float'], + ['y', 'float']) + +Method = enum.Enum('Method', [ + 'GREEDY', + 'FORWARD_GREEDY']) + + +class UnresolvableError(Exception): + """Exception raised when State is not solvable""" diff --git a/src_py/opcut/csp.py b/src_py/opcut/csp.py index 209acec..4d53d6e 100644 --- a/src_py/opcut/csp.py +++ b/src_py/opcut/csp.py @@ -1,82 +1,35 @@ -import enum import itertools -from opcut import util - - -State = util.namedtuple( - '_State', - ['cut_width', 'float'], - ['panels', 'List[Panel]'], - ['items', 'List[Item]'], - ['used', 'List[Used]'], - ['unused', 'List[Unused]']) - -Panel = util.namedtuple( - 'Panel', - ['id', 'Any'], - ['width', 'float'], - ['height', 'float']) - -Item = util.namedtuple( - 'Item', - ['id', 'Any'], - ['width', 'float'], - ['height', 'float'], - ['rotate', 'bool']) - -Used = util.namedtuple( - 'Used', - ['panel', 'Panel'], - ['item', 'Item'], - ['x', 'float'], - ['y', 'float'], - ['rotate', 'bool']) - -Unused = util.namedtuple( - 'Unused', - ['panel', 'Panel'], - ['width', 'float'], - ['height', 'float'], - ['x', 'float'], - ['y', 'float']) - -Method = enum.Enum('Method', [ - 'GREEDY', - 'FORWARD_GREEDY']) - - -class UnresolvableError(Exception): - pass +from opcut import common def calculate(panels, items, cut_width, method): """Calculate cutting stock problem Args: - panels (List[Panel]): input panels - items (List[Item]): input items + panels (List[common.Panel]): input panels + items (List[common.Item]): input items cut_width (float): cut width - method (Method): calculation method + method (common.Method): calculation method Returns: State """ - state = State( + state = common.State( cut_width=cut_width, panels=panels, items=items, used=[], - unused=[Unused(panel=panel, - width=panel.width, - height=panel.height, - x=0, - y=0) + unused=[common.Unused(panel=panel, + width=panel.width, + height=panel.height, + x=0, + y=0) for panel in panels]) return { - Method.GREEDY: _calculate_greedy, - Method.FORWARD_GREEDY: _calculate_forward_greedy + common.Method.GREEDY: _calculate_greedy, + common.Method.FORWARD_GREEDY: _calculate_forward_greedy }[method](state) @@ -93,7 +46,7 @@ def _calculate_greedy(state): new_state = next_state new_fitness = next_state_fitness if not new_state: - raise UnresolvableError() + raise common.UnresolvableError() state = new_state return state @@ -105,13 +58,13 @@ def _calculate_forward_greedy(state): for next_state in _get_next_states(state): try: next_state_fitness = _fitness(_calculate_greedy(next_state)) - except UnresolvableError: + except common.UnresolvableError: continue if new_fitness is None or next_state_fitness < new_fitness: new_state = next_state new_fitness = next_state_fitness if not new_state: - raise UnresolvableError() + raise common.UnresolvableError() state = new_state return state @@ -134,7 +87,7 @@ def _get_next_states(state): def _get_next_states_for_item(state, item): ret = [] loop_iter = ((False, i, unused) for i, unused in enumerate(state.unused)) - if item.rotate: + if item.can_rotate: loop_iter = itertools.chain( loop_iter, ((True, i, unused) for i, unused in enumerate(state.unused))) @@ -155,28 +108,28 @@ def _cut_item_from_unused(unused, item, rotate, cut_width, vertical): item_height = item.height if not rotate else item.width if unused.height < item_height or unused.width < item_width: return None, [] - used = Used(panel=unused.panel, - item=item, - x=unused.x, - y=unused.y, - rotate=rotate) + used = common.Used(panel=unused.panel, + item=item, + x=unused.x, + y=unused.y, + rotate=rotate) new_unused = [] width = unused.width - item_width - cut_width - height = unused.height if vertical else item.height + height = unused.height if vertical else item_height if width > 0: - new_unused.append(Unused(panel=unused.panel, - width=width, - height=height, - x=unused.x + item_width + cut_width, - y=unused.y)) + new_unused.append(common.Unused(panel=unused.panel, + width=width, + height=height, + x=unused.x + item_width + cut_width, + y=unused.y)) width = item_width if vertical else unused.width height = unused.height - item_height - cut_width if height > 0: - new_unused.append(Unused(panel=unused.panel, - width=width, - height=height, - x=unused.x, - y=unused.y + item_height + cut_width)) + new_unused.append(common.Unused(panel=unused.panel, + width=width, + height=height, + x=unused.x, + y=unused.y + item_height + cut_width)) return used, new_unused diff --git a/src_py/opcut/output.py b/src_py/opcut/output.py new file mode 100644 index 0000000..bd1def1 --- /dev/null +++ b/src_py/opcut/output.py @@ -0,0 +1,108 @@ +import io + +from reportlab.pdfgen import canvas +from reportlab.lib.units import mm + +from opcut import util + + +def generate_pdf(state, pagesize_mm=(210, 297), + margin_top_mm=10, margin_bottom_mm=20, + margin_left_mm=10, margin_right_mm=10): + """Generate PDF output + + Args: + state (opcut.common.State): state + pagesize (Tuple[float,float]): page size as (with, height) in mm + margin_top_mm (float): margin top in mm + margin_bottom_mm (float): margin bottom in mm + margin_left_mm (float): margin left in mm + margin_right_mm (float): margin right in mm + + Returns: + bytes + + """ + pagesize = tuple(map(lambda x: x * mm, pagesize_mm)) + margin = _Margin(top=margin_top_mm * mm, + right=margin_right_mm * mm, + bottom=margin_bottom_mm * mm, + left=margin_left_mm * mm) + ret = io.BytesIO() + c = canvas.Canvas(ret, pagesize=pagesize) + c.setFillColorRGB(0.9, 0.9, 0.9) + for panel in state.panels: + _pdf_write_panel(c, pagesize, margin, state, panel) + c.save() + return ret.getvalue() + + +def generate_csv(state): + """Generate CSV output + + Args: + state (opcut.common.State): state + + Returns: + bytes + + """ + return b'' + + +_Margin = util.namedtuple('_Margin', 'top', 'right', 'bottom', 'left') + + +def _pdf_write_panel(c, pagesize, margin, state, panel): + if (panel.width / panel.height > + (pagesize[0] - margin.left - margin.right) / + (pagesize[1] - margin.top - margin.bottom)): + scale = ( + (pagesize[0] - (margin.left + margin.right) * mm) / + panel.width) + else: + scale = ( + (pagesize[1] - (margin.top + margin.bottom) * mm) / + panel.height) + width = panel.width * scale + height = panel.height * scale + x0 = ((pagesize[0] - width) * margin.left / + (margin.left + margin.right)) + y0 = ((pagesize[1] - height) * margin.bottom / + (margin.top + margin.bottom)) + c.setFillColorRGB(0.5, 0.5, 0.5) + c.rect(x0, y0, width, height, stroke=1, fill=1) + for used in state.used: + if used.panel != panel: + continue + _pdf_write_used(c, x0, y0, scale, state, used) + for unused in state.unused: + if unused.panel != panel: + continue + _pdf_write_unused(c, x0, y0, scale, state, unused) + c.setFillColorRGB(0, 0, 0) + c.drawCentredString(pagesize[0] / 2, margin.bottom / 2, + panel.id) + c.showPage() + + +def _pdf_write_used(c, x0, y0, scale, state, used): + width = used.item.width * scale + height = used.item.height * scale + if used.rotate: + width, height = height, width + x = used.x * scale + x0 + y = (used.panel.height - used.y) * scale + y0 - height + c.setFillColorRGB(1, 1, 1) + c.rect(x, y, width, height, stroke=0, fill=1) + c.setFillColorRGB(0, 0, 0) + c.drawCentredString(x + width / 2, y + height / 2 - 6, used.item.id) + + +def _pdf_write_unused(c, x0, y0, scale, state, unused): + width = unused.width * scale + height = unused.height * scale + x = unused.x * scale + x0 + y = (unused.panel.height - unused.y) * scale + y0 - height + c.setFillColorRGB(0.9, 0.9, 0.9) + c.rect(x, y, width, height, stroke=0, fill=1) |
