def random_choice(seq, n=1):
    """
    """
    if n == 1:
        return seq[random_pick(indices(seq))]
    else:
        return [seq[i] for i in random_pick(indices(seq), size=n)]
    def random_walk(self, iters):
        """ Generate new plan states by random walking and stop after given 
        iterations. 

        Note that this is not searching because the walking is not directed by
        the objective value.

        Args:
            iters (int): how many steps before stopping

        """
        # instantialize a transition object to handle actions
        transition = Transition(silent=self.silent, pr_actions=[1., 0., 0., 0.])

        for i in range(iters):
            if not self.silent:
                print("\n\n------------------- Iter: %d --------------------" % i)
            # randomly pick a action with different weighting
            # apply the picked action to picked room and get new state
            action = random_pick(transition.actions, p=transition.pr_actions)
            if not self.silent:
                print("\nSelected action:", action)
            getattr(transition, action)(self)
            if not self.silent:
                print("\nRoom states after action:"); self._pprint_rooms()
            # parse the stats and evaluate the stats
            # plot the plan every 20 iterations
            self._parse()
            self._evaluate()
            if i % self.plot_freq == 0: self._plot_intermediate(i)

        self.plot_fig.ioff()
    def _slice_a_subsequence(self, seq):
        """ Randomly slice a consecutive subsequence out of given sequence.
        """
        n = len(seq)
        if n == 1:
            return seq
        else:
            start  = random_pick(n)
            length = random_pick(range(1, n))
            end = start + length
            if end < n:
                sub_seq = seq[start:end]
            else:
                sub_seq = seq[start:n] + seq[0:end - n - 1]

            return sub_seq
    def random_walk(self, iters):
        """ Generate new plan states by random walking and stop after given 
        iterations. 

        Note that this is not searching because the walking is not directed by
        the objective value.

        Args:
            iters (int): how many steps before stopping

        """
        # instantialize a transition object to handle actions
        transition = Transition(silent=self.silent,
                                pr_actions=[1., 0., 0., 0.])

        for i in range(iters):
            if not self.silent:
                print("\n\n------------------- Iter: %d --------------------" %
                      i)
            # randomly pick a action with different weighting
            # apply the picked action to picked room and get new state
            action = random_pick(transition.actions, p=transition.pr_actions)
            if not self.silent:
                print("\nSelected action:", action)
            getattr(transition, action)(self)
            if not self.silent:
                print("\nRoom states after action:")
                self._pprint_rooms()
            # parse the stats and evaluate the stats
            # plot the plan every 20 iterations
            self._parse()
            self._evaluate()
            if i % self.plot_freq == 0: self._plot_intermediate(i)

        self.plot_fig.ioff()
    def split(self, plan):
        """ Split a room into 2 same-function rooms by a random x or y axis
        inside this room's x, y ranges. 

        """
        room = plan._pick_a_room()
        axis = random_pick(['x', 'y'])
        if not self.silent:
            print("Split room:", room.rid)

        # randomly pick a split line
        left  = room.stats["min_"+axis] + 0.5
        right = room.stats["max_"+axis] - 0.5
        if right - left == 0: 
            if not self.silent:
                print("This room has width 1 at axis %s" % axis)
            return
        split_line = random_pick(np.arange(left, right))

        # split the grids
        xys_1, xys_2 = [], []
        for xy in room.xys:
            if getattr(plan.grids[xy], axis) <= split_line: 
                xys_1.append(xy)
            else: 
                xys_2.append(xy)

        # create 2 new rooms based on split 2 groups of xys
        # append the new rooms to self.rooms and set original room to None
        room_1 = Room(function=room.function, xys=xys_1, rid=plan.room_count)
        room_2 = Room(function=room.function, xys=xys_2, rid=plan.room_count + 1)
        plan.rooms += [room_1, room_2]
        plan.rooms[room.rid] = None

        plan.room_count += 2

        # update the rids of split xys 
        for xy in xys_1: plan.grids[xy].rid = room_1.rid
        for xy in xys_2: plan.grids[xy].rid = room_2.rid

        if not self.silent:
            print("Into 2 rooms: %d and %d" % (room_1.rid, room_2.rid))
    def merge(self, plan):
        """ Merge 2 same-function, adjacent rooms together.

        """
        # find all pairs of adjacent, same-function rooms keyed by function
        groups = self._group_rooms_by_function(plan)
        pairs  = {function:[] for function in plan.functions}
        for function in plan.functions:
            for rid in groups[function]:
                for adjacent_rid in plan.rooms[rid].find_adjacent_rids(plan.grids):
                    if adjacent_rid in groups[function]:
                        pairs[function].append((rid, adjacent_rid))

        # purge the function without adjacent pairs
        # if no function contains adjacent pairs, exit this action.
        pairs = purge_dict(pairs)
        if not pairs: 
            if not self.silent:
                print("No pairs of adjacent same-function rooms found.")
            return

        # randomly pick a function by weighting. (we may want to using weights
        # to control the chance of merge for different room function)
        # redo picking if the picked function has no adjacent pairs
        picked_function = ""
        while picked_function not in pairs.keys():
            picked_function = random_pick(plan.functions, p=plan.pr_merge)

        # randomly pick a pair of adjacent rooms
        # create a new room with same function and merged xys
        picked_pair = random_choice(pairs[picked_function])
        room_new    = Room(
            function=picked_function, 
            rid = plan.room_count,
            xys=plan.rooms[picked_pair[0]].xys + plan.rooms[picked_pair[1]].xys
        )
        plan.room_count += 1
        if not self.silent:
            print("Merge rooms %d and %d" % (plan.rooms[picked_pair[0]].rid,
                plan.rooms[picked_pair[1]].rid))

        # update self.rooms
        plan.rooms[picked_pair[0]] = None
        plan.rooms[picked_pair[1]] = None
        plan.rooms.append(room_new)
        if not self.silent:
            print("into room %d" % room_new.rid)

        # update the rid of merged xys
        for xy in room_new.xys:
            plan.grids[xy].rid = room_new.rid
    def _random_pick_walls_to_expand(self, walls):
        """ Randomly pick {a portion of a wall, a wall, multiple walls} by 
        probabilities {0.2, 0.7, 0.1} to expand outward
        """
        
        # choose whether to expand multiple walls or just one wall. The prob 
        # of picking 1 is 0.9, and the rest 0.1 prob is evenly distributed 
        # among 2 to n_walls.
        n_walls = len(walls)
        pr_pick = [0.9]+[0.1/(n_walls-1) for i in range(n_walls-1)]
        n_pick  = random_pick(range(n_walls), p=pr_pick)

        # if we select multiple walls to expand:
        if n_pick > 1:
            outward_xys = flatten(random_choice(walls, n=n_pick))
            if not self.silent: 
                print("Selected number of walls to expand:", n_pick)
                print("Surrounding xys to be taken: ", outward_xys)

        # if we select 1 wall to expand, we further choose whether to expand
        # the whole wall or a portion of wall. The prob to select whole wall
        # is 0.7, and select a portion is 0.3.
        else:
            n_pick = random_pick(["portion", "whole"], p=[0.3, 0.7])
            if n_pick == "whole":
                outward_xys = random_choice(walls)
                if not self.silent: 
                    print("Selected number of walls to expand:", 1)
                    print("Surrounding xys to be taken: ", outward_xys)
            else:
                outward_xys = self._slice_a_subsequence(random_choice(walls))
                if not self.silent: 
                    print("A portion of 1 wall to expand.")
                    print("Surrounding xys to be taken: ", outward_xys)

        return outward_xys
    def _divide_xys(self, n):
        """ Randomly divide the xys into n continuous groups.
        
        Args:
            n (int): number of groups to have

        Returns:
            groups (list): the list of divided xys groups

        """
        groups = [[] for i in range(n)]

        # randomly pick n xys(points) in plan 
        xys    = list(self.grids.keys())
        idx    = random_pick(indices(xys), size=n, replace=False)
        points = [xys[i] for i in idx]

        # assign each xy to the nearest point(l1 version of voronoi).
        for xy in xys:
            dists    = [l1_dist((xy, point)) for point in points]
            min_dist = min(dists)
            groups[dists.index(min_dist)].append(xy)
            
        return groups
    def _divide_xys(self, n):
        """ Randomly divide the xys into n continuous groups.
        
        Args:
            n (int): number of groups to have

        Returns:
            groups (list): the list of divided xys groups

        """
        groups = [[] for i in range(n)]

        # randomly pick n xys(points) in plan
        xys = list(self.grids.keys())
        idx = random_pick(indices(xys), size=n, replace=False)
        points = [xys[i] for i in idx]

        # assign each xy to the nearest point(l1 version of voronoi).
        for xy in xys:
            dists = [l1_dist((xy, point)) for point in points]
            min_dist = min(dists)
            groups[dists.index(min_dist)].append(xy)

        return groups
 def _pick_a_room(self, ):
     """ Randomly pick a valid room.
     """
     rooms = self._purge_room()
     return random_pick(rooms)
import numpy as np
from numpy.random import choice as random_pick

from Transition import Transition
from Room import Room
from Grid import Grid
from Wall import Wall
import Visual


# ---------------------------- global functions ------------------------------
indices    = lambda seq: range(len(seq))
flatten    = lambda l: list(set([item for sublist in l for item in sublist]))
purge_dict = lambda d: dict((k, v) for k, v in d.items() if v)
l1_dist    = lambda xys: abs(xys[0][0] - xys[1][0]) + abs(xys[0][1] - xys[1][1])
random_choice = lambda seq: seq[random_pick(indices(seq))]


class Plan(object):

    """
    The plan class implements the real-world plan objects. It's consisted of 
    different types of rooms, and each room is consisted of a number of unit 
    grids and enclosing walls. Besides the state attributes, it also has stats 
    attribtues like total area, space efficiency etc.

    Inputs:
    -------
    - function_params (dict): requirements for each function as a dict
        {
            'function':{
 def _pick_a_room(self, ):
     """ Randomly pick a valid room.
     """
     rooms = self._purge_room()
     return random_pick(rooms)
from pprint import pprint
import numpy as np
from numpy.random import choice as random_pick

from Transition import Transition
from Room import Room
from Grid import Grid
from Wall import Wall
import Visual

# ---------------------------- global functions ------------------------------
indices = lambda seq: range(len(seq))
flatten = lambda l: list(set([item for sublist in l for item in sublist]))
purge_dict = lambda d: dict((k, v) for k, v in d.items() if v)
l1_dist = lambda xys: abs(xys[0][0] - xys[1][0]) + abs(xys[0][1] - xys[1][1])
random_choice = lambda seq: seq[random_pick(indices(seq))]


class Plan(object):
    """
    The plan class implements the real-world plan objects. It's consisted of 
    different types of rooms, and each room is consisted of a number of unit 
    grids and enclosing walls. Besides the state attributes, it also has stats 
    attribtues like total area, space efficiency etc.

    Inputs:
    -------
    - function_params (dict): requirements for each function as a dict
        {
            'function':{
                'area':total area of each function,