aboutsummaryrefslogtreecommitdiff
path: root/src_py
diff options
context:
space:
mode:
authorbozokopic <bozo.kopic@gmail.com>2018-04-09 17:12:29 +0200
committerbozokopic <bozo.kopic@gmail.com>2018-04-09 17:12:29 +0200
commitfaeeb9ef7c8253fb4273b6bf5e340ad24883d9e8 (patch)
tree89fb37601bf6204878b104ba4384d0b7b538bb3f /src_py
parent70032f471c28a7c635c628a93553418bd72e739c (diff)
pyopcut - GREEDY and FOWARD_GREEDY implementation
Diffstat (limited to 'src_py')
-rw-r--r--src_py/opcut/csp.py210
-rw-r--r--src_py/opcut/util.py40
2 files changed, 210 insertions, 40 deletions
diff --git a/src_py/opcut/csp.py b/src_py/opcut/csp.py
index 8d91697..04a6d39 100644
--- a/src_py/opcut/csp.py
+++ b/src_py/opcut/csp.py
@@ -1,67 +1,199 @@
-import collections
import enum
+import itertools
+from opcut import util
-InputPanel = collections.namedtuple('InputPanel', ['width', 'height', 'label'])
-InputItem = collections.namedtuple('InputItem', ['width', 'height', 'label',
- 'rotate'])
-OutputPanel = collections.namedtuple('OutputPanel', ['width', 'height',
- 'label', 'items'])
-OutputItem = collections.namedtuple('OutputItem', ['width', 'height', 'label',
- 'x', 'y'])
-Method = enum.Enum('Method', ['FORWARD_GREEDY'])
+State = util.namedtuple(
+ '_State',
+ ['cut_width', 'float'],
+ ['panels', 'List[Panel]'],
+ ['items', 'List[Item]'],
+ ['used', 'List[Used]'],
+ ['unused', 'List[Unused]'])
+
+Panel = util.namedtuple(
+ 'Panel',
+ ['width', 'float'],
+ ['height', 'float'],
+ ['label', 'str'])
+
+Item = util.namedtuple(
+ 'Item',
+ ['width', 'float'],
+ ['height', 'float'],
+ ['label', 'str'],
+ ['rotate', 'bool'])
+
+Used = util.namedtuple(
+ 'Used',
+ ['panel', 'Panel'],
+ ['item', 'Item'],
+ ['x', 'float'],
+ ['y', 'float'],
+ ['rotate', 'bool'])
+
+Unused = util.namespace(
+ 'Unused',
+ ['panel', 'Panel'],
+ ['width', 'float'],
+ ['height', 'float'],
+ ['x', 'float'],
+ ['y', 'float'])
+
+Method = enum.Enum('Method', [
+ 'GREEDY',
+ 'FORWARD_GREEDY'])
+
+
+class UnresolvableError(Exception):
+ pass
def calculate(panels, items, cut_width, method):
"""Calculate cutting stock problem
Args:
- panels (List[InputPanel]): input panels
- items (List[InputItem]): input items
+ panels (List[Panel]): input panels
+ items (List[Item]): input items
cut_width (float): cut width
method (Method): calculation method
Returns:
- List[OutputPanel]
+ State
"""
+ state = State(
+ cut_width=cut_width,
+ panels=panels,
+ items=items,
+ used=[],
+ unused=[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
- }[method](panels, items)
+ }[method](state)
-def _calculate_forward_greedy(panels, items):
- pass
+_fitness_K = 0.03
-_Part = collections.namedtuple('_Part', ['panel', 'width', 'height', 'x', 'y'])
-_State = collections.namedtuple('_State', ['inputs', 'outputs'])
-_fitness_K = 0.03
+def _calculate_greedy(state):
+ while not _is_done(state):
+ new_state = None
+ new_fitness = None
+ for next_state in _get_next_states(state):
+ next_state_fitness = _fitness(next_state)
+ 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()
+ state = new_state
+ return state
+
+
+def _calculate_forward_greedy(state):
+ while not _is_done(state):
+ new_state = None
+ new_fitness = None
+ for next_state in _get_next_states(state):
+ try:
+ next_state_fitness = _fitness(_calculate_greedy(next_state))
+ except 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()
+ state = new_state
+ return state
+
+
+def _get_next_states(state):
+ selected_item = None
+ used_items = {used.item for used in state.used}
+ for item in state.items:
+ if item in used_items:
+ continue
+ if (not selected_item or
+ max(item.width, item.height) >
+ max(selected_item.width, selected_item.height)):
+ selected_item = item
+ if not selected_item:
+ raise Exception('state is done')
+ return _get_next_states_for_item(state, selected_item)
+
+
+def _get_next_states_for_item(state, item):
+ ret = []
+ loop_iter = ((False, i, unused) for i, unused in enumerate(state.unused))
+ if item.rotate:
+ loop_iter = itertools.chain(
+ loop_iter,
+ ((True, i, unused) for i, unused in enumerate(state.unused)))
+ for rotate, i, unused in loop_iter:
+ for vertical in [True, False]:
+ new_used, new_unused = _cut_item_from_unused(
+ unused, item, rotate, state.cut_width, vertical)
+ if not new_used:
+ continue
+ ret.append(state._replace(
+ used=state.used + [new_used],
+ unused=state.unused[:i] + new_unused + state.unused[i+1:]))
+ return ret
+
+
+def _cut_item_from_unused(unused, item, rotate, cut_width, vertical):
+ item_width = item.width if not rotate else item.height
+ 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)
+ new_unused = []
+ width = unused.width - item_width - cut_width
+ 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))
+ 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))
+ return used, new_unused
+
+
+def _is_done(state):
+ return len(state.items) == len(state.used)
def _fitness(state):
- panel_states = {}
- for i in state.inputs:
- if i.panel not in panel_states:
- panel_states[i.panel] = _State([i], [])
- else:
- panel_states[i.panel].inputs.append(i)
- for i in state.outputs:
- if i.panel not in panel_states:
- panel_states[i.panel] = _State([], [i])
- else:
- panel_states[i.panel].outputs.append(i)
-
- total_area = sum(i.width * i.height for i in panel_states.keys())
+ total_area = sum(panel.width * panel.height for panel in state.panels)
result = 0
- for panel, panel_state in panel_states.items():
- result += (panel.width * panel.height -
- sum(i.width * i.height
- for i in panel_state.outputs)) / total_area
+ for panel in state.panels:
+ used_areas = [used.item.width * used.item.height
+ for used in panel.used]
+ unused_areas = [unused.width * unused.height
+ for unused in panel.unused]
+ result += (panel.width * panel.height - sum(used_areas)) / total_area
result -= (_fitness_K *
- min((i.width * i.height for i in panel_state.outputs),
- default=0) *
- max((i.width * i.height for i in panel_state.inputs),
- default=0)) / (total_area * total_area)
+ min(used_areas, default=0) * max(unused_areas, default=0) /
+ (total_area * total_area))
return result
diff --git a/src_py/opcut/util.py b/src_py/opcut/util.py
index 3ceb843..a13cd14 100644
--- a/src_py/opcut/util.py
+++ b/src_py/opcut/util.py
@@ -1,6 +1,44 @@
import contextlib
import asyncio
import sys
+import collections
+
+
+def namedtuple(name, *props):
+ """Create documented namedtuple
+
+ Args:
+ name (Union[str,Tuple[str,str]]):
+ named tuple's name or named tuple's name with documentation
+ props (Iterable[Union[str,Tuple[str,str],Tuple[str,str,Any]]]):
+ named tuple' properties with optional documentation and
+ optional default value
+
+ Returns:
+ class implementing collections.namedtuple
+
+ """
+ props = [(i, None) if isinstance(i, str) else list(i) for i in props]
+ cls = collections.namedtuple(name if isinstance(name, str) else name[0],
+ [i[0] for i in props])
+ default_values = []
+ for i in props:
+ if default_values and len(i) < 3:
+ raise Exception("property with default value not at end")
+ if len(i) > 2:
+ default_values.append(i[2])
+ if default_values:
+ cls.__new__.__defaults__ = tuple(default_values)
+ if not isinstance(name, str) and name[1]:
+ cls.__doc__ = name[1]
+ for i in props:
+ if i[1]:
+ getattr(cls, i[0]).__doc__ = i[1]
+ try:
+ cls.__module__ = sys._getframe(1).f_globals.get('__name__', '__main__')
+ except (AttributeError, ValueError):
+ pass
+ return cls
def run_until_complete_without_interrupt(future):
@@ -14,7 +52,7 @@ def run_until_complete_without_interrupt(future):
KeyboardInterrupt is suppressed (while event loop is running) and is mapped
to single cancelation of running task. If multipple KeyboardInterrupts
- occur, task is canceled only once.
+ occur, task is cancelled only once.
"""
async def ping_loop():