Beispiel #1
0
class Agent:
    # Init Defined in the AFB DisCOP Paper
    def __init__(self, agent_id, total_agents, domain, constraints, cost_functions):
        # Solution CPA and Bound
        self._CPA_SOLUTION = None
        self._B = sys.maxsize

        # Agent information
        self._id = agent_id
        self._constraints = constraints
        self._domain = domain
        self._cost_functions = cost_functions
        self._total_agents = total_agents
        # self._timestamp = [0] * (total_agents + 1)
        self._timestamp = Timestamp(self._total_agents + 1)

        # Instance variables
        self._received_cpa = None
        self._current_cpa = None
        self._next_assignment = 0  # index of value in domain
        self._current_assignment = 0
        self._estimate_value = 0  # scalar value of this agents estimate
        self._queue = []  # A python list can be used a queue
        self._converged = False

        # Only for first Agent
        if self._id == 1:
            cpa = self._generate_cpa()

            # start events
            self._queue.append(Message('CPA_MSG', self._timestamp, 0, self._id, cpa))

    # Return Solution
    def solution(self):
        return self._CPA_SOLUTION

    # Run a sequence of actions once
    def step(self):
        return self._handle_message()

    # Incoming Message
    def receive(self, msg):
        # Check Timestamp
        if self._timestamp < msg.get_timestamp():
            # Add into queue
            self._queue.append(msg)
        #else:
        #    print('failed')

    # Handle message in queue
    def _handle_message(self):
        # Dequeue message from queue
        if len(self._queue) is not 0 and self._converged is not True:

            # Pop message
            msg = self._queue.pop()
            #print(msg.get_timestamp() != self._timestamp)

            # Ignore message if it's an outdated timestamp :)
            if msg.get_timestamp() < self._timestamp and msg.get_timestamp() != self._timestamp:
                #print(msg.get_timestamp())
                #print(self._timestamp)
                return []

            if msg.get_type() == 'FB_CPA':
                # print('Event: FB_CPA')
                return self._calculate_estimate(msg)

            elif msg.get_type() == 'CPA_MSG':
                # print('Event: CPA_MSG')
                return self._handle_cpa(msg)

            elif msg.get_type() == 'FB_ESTIMATE':
                # print('Event: FB_ESTIMATE')

                return self._handle_estimate(msg)

            elif msg.get_type() == 'NEW_SOLUTION':
                # print('Event: NEW_SOLUTION')
                # Update current CPA solution
                self._CPA_SOLUTION = msg.get_payload()
                # Update current Agent B
                self._B = self._CPA_SOLUTION.get_cost()
                return []

            elif msg.get_type() == 'TERMINATE':
                # print('Event: TERMINATE')
                # Status
                self._converged = True
                self._queue = []
                return []

    # Handle CPA
    def _handle_cpa(self, msg):
        # Store incoming CPA
        cpa = msg.get_payload()

        # If higher priority
        if msg.get_source() < self._id:
            self._current_assignment = 0
            self._next_assignment = 0
            self._received_cpa = copy.deepcopy(cpa)
            self._current_cpa = copy.deepcopy(cpa)
            self._timestamp = copy.deepcopy(msg.get_timestamp())

            # Reset future timestamp for lower priority agents
            # relate to this agent
            self._timestamp.reset_timestamp(self._id)

        return self._assign_cpa()

    # Handle creating an estimate value
    # Should return a message if estimate causes a backtrack
    def _handle_estimate(self, msg):
        # do we need to wait for all messages to be received
        # save estimate
        self._estimate_value += msg.get_payload()
        if (self._estimate_value + self._current_cpa.get_cost()) >= self._B:
            # print(self._estimate_value)
            # print(self._current_cpa)
            print('Backtracking due to estimate')
            return self._assign_cpa()

    # Talk about this one tomorrow!!!
    def _calculate_estimate(self, msg):
        # msg = Message('FB_ESTIMATE', msg.timestamp(), self._id, msg.source(), 30)
        # Find best estimate value based on received cpa and possible combinations of unresolved constraints.
        # msg = FB_CPA
        fb_cpa = msg.get_payload()

        min_cost = sys.maxsize  # cost difference
        est_value = None
        for x in range(0, len(self._domain[0])):
            # assignment_cost = self._current_cost(self._current_assignment)
            # f_v =  assignment_cost + self._future_cost(self._current_assignment)
            tmp = self._possible_cpa_assignment_cost(fb_cpa.get_cpa(), x) + self._future_cpa_cost(x)

            if tmp < min_cost:
                min_cost = tmp
                est_value = x

        # Generate a FB_ESTIMATE
        # print('[Agent-%d] Estimate: %d %s'%(self._id, min_cost, self._domain[0][est_value]))
        msg = Message('FB_ESTIMATE', msg.get_timestamp(), self._id, msg.get_source(), min_cost)
        return [msg]

    # Select next value for this agent
    # Currently using an iterative method
    # If we have exhausted all possible values then
    #   return None
    def _next_domain_assignment(self):

        if self._next_assignment == len(self._domain[0]):
            return None
        else:
            assn = self._next_assignment
            self._next_assignment += 1
            return assn

    # Clear Estimates
    def _clear_estimates(self):
        self._estimate_value = 0

    # Define the hashing key
    # Select the higher priority first
    def _key(self, priority1, priority2, val1, val2):
        if priority1 > priority2:
            return '%s,%s' % (val2, val1)
        else:
            return '%s,%s' % (val1, val2)

    # Check constraints and calculate cost in local constraint graph
    # local constraint graph - constrained agents to this agent...(duh, right?)
    def _possible_cpa_assignment_cost(self, cpa, value):
        assignment_cost = 0
        hash_id = 0
        for constraint in self._constraints:
            # Check if constraint is assigned... if not ignore.
            if cpa[constraint] is not None:

                # Get hash table key
                key = self._key(self._id, constraint, self._domain[0][value], cpa[int(constraint)])

                # Add cost to assignment cost
                assignment_cost += self._cost_functions[hash_id][key]

            # Hash Id
            hash_id += 1

        return assignment_cost

    # Get most recent CPA cost provided by higher priority agent
    def _past_cpa_cost(self):
        # Check if current is first
        if self._id == 1:
            # We are in the first agent
            return 0
        else:
            # Not first agent then return receive CPA_Cost
            return self._received_cpa.get_cost()

    # Estimate future cost based on constrained lower priority agents
    def _future_cpa_cost(self, value):
        # Try all possible domain values
        # Case 1: No Lower Priority Constraints
        lower_priority = False
        for x in self._constraints:
            if x > self._id:
                lower_priority = True

        if lower_priority is not True:
            return 0

        # Case 2: Constraints, but we only consider lower priority agents
        # Summation of h functions
        h_sum = 0

        # Iterate over all constraints
        for constraint in self._constraints:
            if constraint > self._id:
                h_sum += self._min_assignment(value, constraint)

        return h_sum

    # Find best assignment for constrained agent given possible assignment value
    def _min_assignment(self, value, j_id):
        min_cost = sys.maxsize
        # For each value in the jth domain
        j_hash_function_index = self._constraints.index(j_id)
        # index of jth hash
        j_hash_function = self._cost_functions[j_hash_function_index]
        j_domain_index = j_hash_function_index + 1
        j_domain = self._domain[j_domain_index]

        for v in j_domain:
            # create a key for the jth hash table
            key = self._key(self._id, j_id, self._domain[0][value], v)

            # access value
            current_cost = j_hash_function[key]
            if current_cost < min_cost:
                min_cost = current_cost

        return min_cost

    # Create the first CPA :D
    def _generate_cpa(self):
        # Generate a CPA class to maintain the problem
        cpa = [None] * (self._total_agents + 1)

        return CPA(0, cpa)

    # Assign next possible domain value for this agent
    def _assign_cpa(self):
        # clear estimates
        self._clear_estimates()

        # if CPA contains an assignment A_i = w_i remove it
        if self._current_cpa.get_cpa()[self._id] is not None:
            self._current_cpa.get_cpa()[self._id] = None  # Add method to modify assignment

        # find a new value (v)
        while True:
            self._current_assignment = self._next_domain_assignment()
            if self._current_assignment is None:
                return self._backtrack()

            assignment_cost = self._possible_cpa_assignment_cost(self._received_cpa.get_cpa(), self._current_assignment)
            f_v = assignment_cost + self._future_cpa_cost(self._current_assignment)

            # Assign Variable
            if (self._past_cpa_cost() + f_v) < self._B:
                # Assign the possible value
                # Update CPA
                self._current_cpa = copy.deepcopy(self._received_cpa)
                self._current_cpa.get_cpa()[self._id] = self._domain[0][self._current_assignment]
                self._current_cpa.set_cost(self._past_cpa_cost() + assignment_cost)

                # Increase timestamp
                self._timestamp.increase_timestamp(self._id)
                break

        # Add value to CPA and calculate cost
        if self._current_cpa.is_full():
            self._CPA_SOLUTION = copy.deepcopy(self._current_cpa)
            self._B = self._current_cpa.get_cost()

            # print(self._CPA_SOLUTION)
            msg_list = []  # broadcast to all agents updated B
            for x in range(1, self._total_agents + 1):
                msg = Message('NEW_SOLUTION', self._timestamp, self._id, x, self._CPA_SOLUTION)
                msg_list.append(msg)

            return msg_list + self._assign_cpa()
        else:
            # send cpa message to next agent in order
            msg_list = [Message('CPA_MSG', self._timestamp, self._id, self._id + 1, self._current_cpa)]

            for x in range(self._id + 1, self._total_agents + 1):
                msg = Message('FB_CPA', self._timestamp, self._id, x, self._current_cpa)
                msg_list.append(msg)
            return msg_list

    # No more possible domain values or pruning search space
    def _backtrack(self):
        # clear estimates
        # Check if in first agent
        self._clear_estimates()
        # print('[Agent-%d] BACKTRACK: %s'%(self._id,str(self._current_cpa)))
        if self._id == 1:
            # Broadcast Terminate
            msg_list = []
            for x in range(2, self._total_agents + 1):
                msg = Message('TERMINATE', self._timestamp, self._id, x, None)
                msg_list.append(msg)

            return msg_list

        else:
            # send CPA_MSG message to A_i-1
            # print('Agent-%d %s'%(self._id,self._current_assignment))
            msg = Message('CPA_MSG', self._timestamp, self._id, self._id - 1, self._received_cpa)
            return [msg]