Exemple #1
0
class BasicGenerator(InstanceGenerator):
    """Most basic instance generator implementation."""
    def __init__(self, conf_args):
        super(BasicGenerator, self).__init__(conf_args)
        self._prg = None
        self._observer = Observer()
        self._solve_opts = [
            "-t {0}".format(self._args.threads),
            "--project",
            # "--opt-mode=optN",
            "-n {0}".format(self._args.num)
        ]
        if self._args.random:
            self._solve_opts.extend([
                '--rand-freq={}'.format(self._args.rand_freq),
                '--sign-def={}'.format(self._args.sign_def),
                '--seed={}'.format(self._args.seed)
            ])
        if self._args.solve_opts:
            self._solve_opts.extend(self._args.solve_opts)
        self._inits = None
        self._object_counters = {}
        self._instance_count = 0
        self._instances = []
        self.dest_dirs = []

        if self._args.cluster_x is None:
            self._cluster_x = int((self._args.grid_x - 2 + 0.5) / 2)
        else:
            self._cluster_x = self._args.cluster_x
        if self._args.cluster_y is None:
            self._cluster_y = int((self._args.grid_y - 4 + 0.5) / 2)
        else:
            self._cluster_y = self._args.cluster_y

    def _on_model(self, model):
        """Call back method for clingo."""
        self._instance_count += 1
        self._inits = []
        if self._args.console:
            LOG.info("\n~~~~ %s. Instance ~~~~", str(self._instance_count))
        atoms_list = model.symbols(terms=True)
        LOG.debug("Found model: %s",
                  '.\n'.join([str(atm) for atm in model.symbols(atoms=True)]))
        atm = None
        for atm in [atm for atm in atoms_list if atm.name == "init"]:
            self._inits.append(atm)
        self._update_object_counter(atoms_list)
        LOG.info("Stats: %s",
                 {k: int(v)
                  for k, v in self._object_counters.items()})
        self._save()

    def _update_object_counter(self, atoms_list):
        '''Count objects in atom list.'''
        self._object_counters = {
            "x": 0,
            "y": 0,
            "node": 0,
            "highway": 0,
            "robot": 0,
            "shelf": 0,
            "pickingStation": 0,
            "product": 0,
            "order": 0,
            "units": 0
        }
        objects = []
        for atm in [atm for atm in atoms_list if atm.name == "init"]:
            args = atm.arguments
            if len(args) == 2 and args[0].name == "object":
                obj = args[0]
                object_name = obj.arguments[0].name
                if obj not in objects:
                    objects.append(obj)
                    if object_name not in self._object_counters:
                        self._object_counters[object_name] = 1
                    else:
                        self._object_counters[object_name] += 1
                    if args[1].name == "value" and len(args[1].arguments) == 2:
                        value_type = args[1].arguments[0].name
                        value_value = args[1].arguments[1]
                        if object_name == "node" and value_type == "at":
                            self._object_counters["x"] = max(
                                self._object_counters["x"],
                                value_value.arguments[0].number)
                            self._object_counters["y"] = max(
                                self._object_counters["y"],
                                value_value.arguments[1].number)
                if (object_name == "product" and args[1].name == "value"
                        and len(args[1].arguments) == 2):
                    value_type = args[1].arguments[0].name
                    value_value = args[1].arguments[1]
                    if value_type == "on" and len(value_value.arguments) == 2:
                        self._object_counters["units"] = (
                            self._object_counters["units"] +
                            value_value.arguments[1].number)

    def generate(self):
        """Regular instance generation.

        :param args: Optional argparse.Namespace.

        """
        sig_handled = [False]  # Outter flag to indicate sig handler usage

        class TimeoutError(Exception):
            """Time out exception."""
            pass

        def sig_handler(signum, frame):
            """Custom signal handler."""
            sig_handled[0] = True
            if signum == signal.SIGINT:
                LOG.warn("\n! Received Keyboard Interruption (Ctrl-C).\n")
                raise KeyboardInterrupt
            elif signum == signal.SIGALRM:
                LOG.warn("Solve call timed out!")
                raise TimeoutError()

        signal.signal(signal.SIGINT, sig_handler)
        signal.signal(signal.SIGALRM, sig_handler)
        igen = BasicGenerator(self._args)
        try:
            signal.alarm(self._args.wait)
            instances = igen._solve()
            return instances, igen.dest_dirs
        except Exception as exc:
            if sig_handled[0]:
                pass
            else:
                LOG.error("%s: %s", type(exc).__name__, exc)
        finally:
            igen.interrupt()

    def _solve(self):
        """Grounds and solves the relevant programs for instance generation."""
        LOG.debug("Used args for grounding & solving: %s", str(self._args))

        self._prg = clingo.Control(self._solve_opts)

        # Register ground program observer to analyze grounding size impact per program parts
        self._prg.register_observer(self._observer)

        # Problem encoding
        self._prg.load(
            os.path.join(os.path.dirname(os.path.realpath(__file__)),
                         '../encodings/ig.lp'))

        # Templates
        for template in self._args.template:
            self._prg.load(template)
        if self._args.template_str:
            self._prg.add("base", [], self._args.template_str)
        self._ground([("base", [])])
        if self._args.template or self._args.template_str:
            self._ground([("template_stub", [])])

        LOG.info("Grounding...")

        # Nodes
        if self._args.grid_x and self._args.grid_y:
            if self._args.nodes is None:
                self._ground([("nodes", [
                    self._args.grid_x, self._args.grid_y,
                    self._args.grid_x * self._args.grid_y
                ])])
            else:
                self._ground([
                    ("nodes",
                     [self._args.grid_x, self._args.grid_y, self._args.nodes])
                ])

        # Floor dimensions and total number nodes (grounded independently to also support floor
        # templates)
        self._ground([("floor_dimensions", [])])

        # Object quantities and IDs
        if self._args.robots:
            self._ground([("robots_cl", [self._args.robots,
                                         self._args.robots])])
        self._ground([("robots", [])])

        if self._args.shelves:
            self._ground([("shelves_cl",
                           [self._args.shelves, self._args.shelves])])
        self._ground([("shelves", [])])

        if self._args.picking_stations:
            self._ground([
                ("picking_stations_cl",
                 [self._args.picking_stations, self._args.picking_stations])
            ])
        self._ground([("picking_stations", [])])

        if self._args.products:
            self._ground([("products_cl",
                           [self._args.products, self._args.products])])
        self._ground([("products", [])])

        if self._args.orders:
            self._ground([("orders", [self._args.orders])])

        # Layouts
        if self._args.highway_layout:
            self._ground([("highway_layout", [
                self._cluster_x, self._cluster_y, self._args.beltway_width,
                self._args.ph_pickstas, self._args.ph_robots
            ])])
        else:
            self._ground([("random_layout", [])])
            if self._args.grid_x and self._args.grid_y and self._args.nodes:
                self._ground([("grid_gaps", [self._args.gap_size])])

        # Object quantities and IDs contd.: depending on layout definitions
        if self._args.shelf_coverage:
            self._ground([("shelf_coverage", [self._args.shelf_coverage])])

        # Object inits
        self._ground([("robots_init", [])])
        self._ground([("shelves_init", [])])
        self._ground([("picking_stations_init", [])])
        if self._args.product_units_total:
            self._ground([(
                "product_units",
                [
                    # min
                    self._args.product_units_total,
                    # max
                    self._args.product_units_total,
                    # minpus
                    self._args.product_units_per_product_shelf
                    or self._args.min_product_units_per_product_shelf or 1,
                    # maxpus
                    self._args.product_units_per_product_shelf
                    or self._args.max_product_units_per_product_shelf
                    or self._args.product_units_total,
                    # minprs
                    self._args.products_per_shelf
                    or self._args.min_products_per_shelf or 0,
                    # maxprs
                    self._args.products_per_shelf
                    or self._args.max_products_per_shelf
                    or self._args.products,
                    # minspr
                    self._args.shelves_per_product
                    or self._args.min_shelves_per_product or 0,
                    # maxspr
                    self._args.shelves_per_product
                    or self._args.max_shelves_per_product
                    or self._args.shelves,
                    # cnd_minpsr
                    1 if self._args.conditional_min_products_per_shelf else 0
                ])])
        self._ground([(
            "orders_init",
            [
                # minlines
                self._args.order_lines or self._args.min_order_lines or 1,
                # maxlines
                self._args.order_lines or self._args.max_order_lines,
                # maxpupr
                ((self._args.product_units_per_product_shelf
                  or self._args.max_product_units_per_product_shelf) *
                 (self._args.shelves_per_product
                  or self._args.max_shelves_per_product))
                or self._args.product_units_total
            ])])

        # Layouts contd.: constraints related to interior object placement
        # TODO: simplify grounding order of layouts, constraints, object inits program parts
        # - use init/2 instead of poss/2 in layout constraints above
        if self._args.reachable_shelves:
            self._ground([("reachable_shelves", [])])

        # Order constraints & optimizations
        if self._args.order_all_products:
            self._ground([("order_all_products", [])])
        # self._ground([("orders_different", [])])

        # General rules
        self._ground([("base", [])])
        self._ground([("symmetry", [])])

        # Projection to subsets of init/2
        if self._args.prj_orders:
            self._ground([("project_orders", [])])
        elif self._args.prj_warehouse:
            self._ground([("project_warehouse", [])])
        else:
            self._ground([("project_all", [])])

        LOG.info("Solving...")

        # solve_future = self._prg.solve_async(self._on_model)
        # solve_future.wait(self._args.wait)
        # solve_future.cancel()
        # solve_result = solve_future.get()

        solve_result = self._prg.solve(on_model=self._on_model)

        if self._args.write_selected and self._args.write_selected > len(
                self._instances):
            LOG.warn(
                "Selected instance %s not written since not enumerated, i.e., "
                "only enumerated %s!", str(self._args.write_selected),
                str(len(self._instances)))

        LOG.info("Parallel mode: %s",
                 str(self._prg.configuration.solve.parallel_mode))
        LOG.info("Generated instances: %s", str(self._instance_count))
        LOG.info("Solve result: %s", str(solve_result))
        LOG.info("Search finished: %s", str(not solve_result.interrupted))
        LOG.info("Search space exhausted: %s", str(solve_result.exhausted))
        LOG.debug("Statistics: %s", self._prg.statistics)

        if self._args.grounding_stats:
            self._get_grounding_stats("AFTER CONCLUDING SOLVE CALL",
                                      self._args.grounding_stats)

        return self._instances

    def _save(self):
        """Writes instance to file."""
        file_name = ''
        if (not self._args.write_instance
                or (self._args.write_selected
                    and self._args.write_selected != self._instance_count)):
            file_name = os.devnull
        elif self._args.console:
            file_name = "/dev/stdout"
        else:
            if self._args.instance_count:
                instance_count = self._args.instance_count
            else:
                instance_count = self._instance_count
            local_name = (self._args.name_prefix + "_".join([
                "x" + str(self._object_counters["x"]),
                "y" + str(self._object_counters["y"]),
                "n" + str(self._object_counters["node"]),
                "r" + str(self._object_counters["robot"]),
                "s" + str(self._object_counters["shelf"]),
                "ps" + str(self._object_counters["pickingStation"]),
                "pr" + str(self._object_counters["product"]),
                "u" + str(self._object_counters["units"]),
                "o" + str(self._object_counters["order"]),
                "N" + str(instance_count).zfill(3)
            ]))
            if self._args.instance_dir:
                dir_suffix = local_name
            else:
                dir_suffix = ''
            if self._args.instance_dir_suffix:
                dir_suffix += self._args.instance_dir_suffix
            dest_dir = self._args.directory + dir_suffix
            self.dest_dirs.append(dest_dir)
            if not os.path.exists(dest_dir):
                os.makedirs(dest_dir)
            file_name = (dest_dir + "/" + local_name + ".lp")

        # Instance preamble
        instance = (
            "%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%\n"
            "% Grid Size X:                      {}\n"
            "% Grid Size Y:                      {}\n"
            "% Number of Nodes:                  {}\n"
            "% Number of Highway Nodes:          {}\n"
            "% Number of Robots:                 {}\n"
            "% Number of Shelves:                {}\n"
            "% Number of Picking Stations:       {}\n"
            "% Number of Products:               {}\n"
            "% Number of Product Units in Total: {}\n"
            "% Number of Orders:                 {}\n"
            "%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%\n\n")
        instance = instance.format(
            str(self._object_counters["x"]), str(self._object_counters["y"]),
            str(self._object_counters["node"]),
            str(self._object_counters["highway"]),
            str(self._object_counters["robot"]),
            str(self._object_counters["shelf"]),
            str(self._object_counters["pickingStation"]),
            str(self._object_counters["product"]),
            str(self._object_counters["units"]),
            str(self._object_counters["order"]))

        # Instance facts
        instance += ("#program base.\n\n" "% init\n")
        self._inits.sort()
        for init in self._inits:
            instance += str(init) + ".\n"
        self._instances.append(instance)
        try:
            with open(file_name, "w") as ofile:
                ofile.write(instance)
        except IOError as err:
            LOG.error("IOError while trying to write instance file \'%s\': %s",
                      file_name, err)

    def interrupt(self):
        '''Kill all solve call threads.'''
        self._prg.interrupt()

    def _ground(self, parts, context=None):
        """For grounding self._prg, wrapper of clingo.Control.ground to include optional statistics.

        """
        self._prg.ground(parts, context)
        if self._args.grounding_stats:
            description = 'parts: ' + str(parts)
            if context:
                description += '| context: ' + str(context)
            self._get_grounding_stats(description, self._args.grounding_stats)

    def _get_grounding_stats(self, description=None, show='stats'):
        """Returns grounding size statistics for current ASP program."""
        self._prg.ground([("project_all", [])])
        self._prg.solve()
        ctx = self._observer.finalize()
        prg = clingo.Control()
        prg.load(
            os.path.join(os.path.dirname(os.path.realpath(__file__)),
                         '../encodings/grnd_stats.lp'))
        if show == 'stats':
            prg.add('base', [], '#show stats/2. #show stats_total/1.')
        elif show == 'atoms':
            pass
        else:
            raise ValueError(
                "Unsupported grounding stats option \'{}\'".format(show))
        prg.add("observed", [], "o(@get()).")
        prg.ground([('base', []), ("observed", [])], ctx)

        def __on_model(model):
            LOG.info(("Grounding size stats for program %s:\n"
                      "                                                %s\n"),
                     description,
                     ", ".join([str(sym)
                                for sym in model.symbols(shown=True)]))

        prg.solve(on_model=__on_model)