def __init__(self, configs):
        daiquiri.setup(level=logging.INFO)
        self.logger = daiquiri.getLogger(__name__)

        self.configs = configs
        self.tsp = TSP()
        self.msdn = MSDN()
Exemple #2
0
def test_compute_distance_matrix():
    msdn = MSDN()
    coords = [[47.60,-122.33],[-47.67,-122.19],[47.71,-122.19]]
    matrix = msdn.compute_distance_matrix(coords)
    assert len(matrix.keys()) == 3
    for key in matrix:
        for key_ in matrix[key]:
            assert type(matrix[key][key_]) in [int, float]
Exemple #3
0
def test_convert_address():
    msdn = MSDN()
    coordinates = msdn.convert_address(
        street='1 Microsoft Way',
        city='Redmond',
        state='WA',
        zip_code='98052'
    )
    assert type(coordinates) == list
    assert len(coordinates) == 2
    assert type(coordinates[0]) == float
    assert type(coordinates[1]) == float
Exemple #4
0
    def __init__(self):
        daiquiri.setup(level=logging.INFO)
        self.logger = daiquiri.getLogger(__name__)

        self.config_manager = ConfigManager()
        key = self.config_manager.get_config('GOOGLE_KEY')
        if key != None:
            self.key = key
            self.gmaps = googlemaps.Client(key=self.key)
        else:
            msg = "GOOGLE_KEY not found in config. "
            msg += "Run cbc_api add_config --help for more info"
            self.logger.warning(msg)
            self.gmaps = None

        self.msdn = MSDN()
Exemple #5
0
def convert_address():
    """
    Converts an address to lat/long coordinates
    """
    # Get the query parameters from the request
    street = request.args.get('street')
    if street is None:
        error = {'error': 'Street not defined'}
        return make_response(jsonify(error), 400)

    city = request.args.get('city')
    if city is None:
        error = {'error': 'City not defined'}
        return make_response(jsonify(error), 400)

    state = request.args.get('state')
    if state is None:
        error = {'error': 'State not defined'}
        return make_response(jsonify(error), 400)

    zip_code = request.args.get('zipCode')
    if zip_code is None:
        error = {'error': 'Zip Code not defined'}
        return make_response(jsonify(error), 400)

    # Convert the address to coordinates
    msdn = MSDN()
    coordinates = msdn.convert_address(street=street,
                                       city=city,
                                       state=state,
                                       zip_code=zip_code)

    return jsonify({
        'coordinates': coordinates,
        'address': {
            'street': street,
            'city': city,
            'state': state,
            'zipCode': zip_code
        }
    })
Exemple #6
0
class TSP(object):
    def __init__(self):
        daiquiri.setup(level=logging.INFO)
        self.logger = daiquiri.getLogger(__name__)

        self.config_manager = ConfigManager()
        key = self.config_manager.get_config('GOOGLE_KEY')
        if key != None:
            self.key = key
            self.gmaps = googlemaps.Client(key=self.key)
        else:
            msg = "GOOGLE_KEY not found in config. "
            msg += "Run cbc_api add_config --help for more info"
            self.logger.warning(msg)
            self.gmaps = None

        self.msdn = MSDN()

    def solve_tsp(self,
                  origin,
                  destination,
                  waypoints,
                  method='google',
                  constraints=None):
        """
        Solves a TSP problem and returns the waypoints in order
        """
        if method == 'google' and constraints is not None:
            msg = 'Google TSP solver does not support constraints. '
            msg += 'Switching to pyschedule'
            self.logger.warning(msg)
            method = 'pyschedule'

        if method == 'google' and self.gmaps is None:
            msg = 'No Google API Key found. Solving with pyschedule. '
            self.logger.warning(msg)
            method = 'pyschedule'

        if method == 'google':
            soln = self._solve_tsp_gmaps(origin, destination, waypoints)
        elif method == 'pyschedule':
            soln = self._solve_tsp_pyschedule(origin=origin,
                                              destination=destination,
                                              waypoints=waypoints,
                                              constraints=constraints)
        else:
            self.logger.warning('%s is an invalid solver method' % (method))
            soln = None
        return soln

    def _solve_tsp_gmaps(self, origin, destination, waypoints):
        """
        Calls the google directions api to solve a tsp
        """
        # Solve the TSP
        response = self.gmaps.directions(origin=origin,
                                         destination=destination,
                                         waypoints=waypoints,
                                         optimize_waypoints=True)

        # Parse the results
        soln = dict()
        order = response[0]['waypoint_order']
        soln['order'] = order

        durations = []
        for leg in response[0]['legs']:
            duration = int(leg['duration']['text'].split(' ')[0])
            durations.append(duration)
        soln['durations'] = durations

        return soln

    def _solve_tsp_pyschedule(self,
                              origin,
                              destination,
                              waypoints,
                              constraints=None,
                              solver='CBC'):
        """
        Solves the TSP using pyschedule
        """
        # Compute the distance matrix
        coords = [x for x in waypoints]
        coords.append(origin)
        origin_idx = len(coords) - 1
        if destination != origin:
            coords.append(destination)
            dest_idx = len(coords) - 1
        else:
            dest_idx = origin_idx
        distances = self.msdn.compute_distance_matrix(coords)

        # Build the scenario, tasks and resources
        scenario = Scenario('TSP')
        keys = [str(i) for i in range(len(waypoints))]
        keys.append('START')
        keys.append('END')
        tasks = {i: scenario.Task(i) for i in keys}
        worker = scenario.Resource('Worker')

        # Worker needs to pass through every city
        for task in keys:
            tasks[task] += worker

        # Make sure the scenario STARTs and ends at the right place
        scenario += tasks['START'] < {tasks[x] for x in keys if x != 'START'}
        scenario += tasks['END'] > {tasks[x] for x in keys if x != 'END'}

        # Add constraints to the problem
        if constraints:
            for constraint in constraints:
                activity1 = constraint['index1']
                activity2 = constraint['index2']
                constraint_type = constraint['constraint_type']
                if constraint_type == 'Before':
                    scenario += tasks[activity1] < tasks[activity2]
                elif constraint_type == 'After':
                    scenario += tasks[activity1] > tasks[activity2]

        # Add distances as conditional precedences
        driving_distances = []
        for task in keys:
            for task_ in keys:
                if task != task_ and task != 'END' and task_ != 'START':
                    if task == 'START':
                        start_idx = origin_idx
                    else:
                        start_idx = int(task)

                    if task_ == 'END':
                        end_idx = dest_idx
                    else:
                        end_idx = int(task_)

                    distance = distances[start_idx][end_idx]
                    driving_distances.append(tasks[task] +
                                             int(distance) << tasks[task_])
        scenario += driving_distances

        # Add the objective and solve
        scenario += tasks['END'] * 1
        mip.solve_bigm(scenario, kind=solver)
        results = scenario.solution()[1:-1]

        # Parse the results
        soln = {'order': [], 'durations': []}
        for i, result in enumerate(results):
            if i == 0:
                last_activity = origin_idx
            else:
                last_activity = int(results[i - 1][0].name)
            activity = int(result[0].name)
            soln['order'].append(activity)
            duration = int(distances[last_activity][activity])
            soln['durations'].append(duration)
        final_activity = int(results[-1][0].name)
        last_leg = int(distances[final_activity][dest_idx])
        soln['durations'].append(last_leg)

        return soln
class Schedule(object):
    def __init__(self, configs):
        daiquiri.setup(level=logging.INFO)
        self.logger = daiquiri.getLogger(__name__)

        self.configs = configs
        self.tsp = TSP()
        self.msdn = MSDN()

    def schedule_activities(self, method='pyschedule'):
        """
        Schedules the activities using an
        appropriate method given the configs
        """
        if self._valid_tsp():
            self.build_groups()
            self.build_cluster_centroids()

            # Cluster the activities
            if 'days' not in self.configs:
                days = 1 
            else:
                days_ = self.configs['days']
                days = len([x for x in days_ if days_[x] > 0])
            self.configs['activities'] = self.cluster(
                activities=self.configs['activities'],
                n_clusters=days
            )
            all_days = [x['day'] for x in self.configs['activities']]
            unique_days = list(np.unique(all_days))

            # Build the problem settings
            origin = self.configs['origin']['coordinates']
            destination = self.configs['destination']['coordinates']
            waypoints = {}
            max_idx = {}
            for i, activity in enumerate(self.configs['activities']):
                day = activity['day']
                if day not in max_idx:
                    activity['index'] = 0
                    max_idx[day] = 0
                else:
                    activity['index'] = max_idx[day] + 1
                    max_idx[day] += 1
                if day not in waypoints:
                    waypoints[day] = []
                waypoints[day].append(activity['coordinates'])

            # Add the constraints
            activities = self.configs['activities']
            if 'constraints' not in self.configs:
                constraints = []
            else:
                constraints = self.configs['constraints']
                for constraint in constraints:
                    idx1 = self.get_index(constraint['activity1'])
                    constraint['index1'] = idx1
                    idx2 = self.get_index(constraint['activity2'])
                    constraint['index2'] = idx2

            # Only use constraints for activities that occur
            # on the same day
            valid_constraints = {day:[] for day in unique_days}
            for constraint in constraints:
                day1 = self.get_day(constraint['activity1'])
                day2 = self.get_day(constraint['activity2'])
                if day1 == day2:
                    valid_constraints[day1].append(constraint)

            # Find the optimal sequence and append keys
            solns = {}
            for day in unique_days:
                soln = self.tsp.solve_tsp(
                    origin=origin, 
                    destination=destination, 
                    waypoints=waypoints[day],
                    constraints=valid_constraints[day],
                    method=method
                )
                solns[day] = soln

            for day in unique_days:
                num_activities = len(solns[day]['order'])
                for i, index in enumerate(solns[day]['order']):
                    activity = self.get_activity(day, index)
                    activity['order'] = i
                    activity['from_duration'] = solns[day]['durations'][i+1]
                    activity['to_duration'] = solns[day]['durations'][i]
            self.assign_clusters()
            activities = sorted(activities, key=itemgetter('assigned_day','order'))
            self.configs['activities'] = activities

        # Compute the distance matrix
        distance_matrix = self.distance_matrix()
        self.configs['distance_matrix'] = distance_matrix
    
    def cluster(self, activities, n_clusters=2, method='kmeans'):
        """
        Clusters the locations of activities
        """
        # Make sure the number of clusters is less
        # than the number of activities
        num_activities = len(activities)
        if num_activities <= n_clusters:
            n_clusters = num_activities

        coords = np.array([x['cluster_coords'] for x in activities])
        if method == 'kmeans':
            kmeans = KMeans(n_clusters=n_clusters, random_state=0)
            clusters = kmeans.fit_predict(coords)
            for i, cluster in enumerate(clusters):
                activities[i]['day'] = str(cluster)
        self.deconflict_days(kmeans)
        return activities

    def deconflict_days(self, kmeans):
        """
        Ensures that activities that assigned to different days
        are assigned to different clusters
        """ 
        centroids = kmeans.cluster_centers_
        for label in self.configs['day_assignments']:
            activity = self.get_activity_by_label(label)
            day = activity['day']
            allowed_days = self.find_allowed_days(label)
            coords = activity['cluster_coords']
            new_day = self.find_closest_cluster(
                activity, 
                centroids, 
                allowed_days
            )
            activity['day'] = new_day

    def find_allowed_days(self, label):
        """
        Finds which days are allowed to host an activity
        """
        activities = self.configs['activities']
        assignment = self.configs['day_assignments'][label]
        days = [x['day'] for x in activities]
        for activity in activities:
            label_ = activity['label']
            if label_ in self.configs['day_assignments']:
                assignment_ = self.configs['day_assignments'][label_]
                if assignment != assignment_:
                    day = activity['day']
                    days = [x for x in days if x != day]
        return days
                
    def find_closest_cluster(self, activity, centroids, allowed_days):
        """
        Uses vincenty distance to find the closest centroid
        """
        # Initialize variables
        coords = activity['cluster_coords']
        assignment = self.configs['day_assignments'][activity['label']]
        closest = None
        for i, centroid in enumerate(centroids):
            if str(i) in allowed_days:
                if closest is None:
                    closest = i
                else:
                    distance = vincenty(coords, centroid)
                    if distance.miles < closest:
                        closest = i
        return str(closest)

    def assign_clusters(self):
        """
        Assigns clusters to days of the week
        """
        all_days = [x['day'] for x in self.configs['activities']]
        unique_days = list(np.unique(all_days))

        # Build a list of activities and sort them
        activities = []
        for day in unique_days:
            activities.append((self.cluster_time(day), day))
        activities.sort(reverse=True)

        # Build the capacities and sort them
        capacities = []
        for day in self.configs['days']: 
            capacity = self.configs['days'][day] * 60
            capacities.append((capacity, day))
        capacities.sort(reverse=True)

        # Map clusters to assigned days
        assignments = {}
        for activity in self.configs['activities']:
            label = activity['label']
            if label in self.configs['day_assignments']:
                assigned_day = self.configs['day_assignments'][label]
                cluster = activity['day']
                assignments[cluster] = assigned_day
                capacities = [x for x in capacities if x[1] != assigned_day]
                activities = [x for x in activities if x[1] != cluster]

        # Assign the clusters to days
        for i, activity in enumerate(activities):
            cluster = str(activity[1])
            day = str(capacities[i][1])
            assignments[cluster] = day

        # Add the assigned day to the activity
        mapping = {
            "monday" : "0",
            "tuesday" : "1",
            "wednesday" : "2",
            "thursday" : "3",
            "friday" : "4",
            "saturday" : "5",
            "sunday" : "6"
        }
        activities = self.configs['activities']
        for activity in activities:
            day = activity['day']
            assigned_day = mapping[assignments[day]]
            activity['assigned_day'] = assigned_day

    def cluster_time(self, day):
        """
        Computes the total amount of time spent on activities for the day
        """
        activities = self.configs['activities']
        day_activities = [x for x in activities if x['day'] == day]
        total = 0
        for activity in day_activities:
            if activity['index'] == 0:
                total += activity['to_duration']
            total += activity['from_duration']
            total += activity['duration']
        return total

    def check_extra_capacity(self):
        """
        Checks to see if any of the days could be consolidated
        """
        durations = [self.cluster_time(str(i)) for i in range(6)]
        durations = [x for x in durations if x > 0]

        capacities = [self.configs['days'][x]*60 for x in self.configs['days']]
        capacities = [x for x in capacities if x > 0]

        capacity_tester = CapacityTester(capacities, durations)
        extra_days = capacity_tester.find_additional_capacity()

        return extra_days

    def remove_lowest_capacity_day(self, n):
        """
        Removes the lowest capacity day if there is extra capacity
        """
        if n>0:
            for i in range(n):
                lowest = 24
                lowest_day = None
                for day in self.configs['days']:
                    hours = self.configs['days'][day]
                    if hours > 0:
                        if hours < lowest:
                            lowest_day = day
                            lowest = hours
                self.logger.info('Removed %s'%(lowest_day))
                self.configs['days'][lowest_day] = 0

    def build_groups(self):
        """
        Builds sets of grouped items from list of tuples
        """
        pairs = self.configs['groups']

        # Use the group pairings to create sets
        groups = []
        if len(pairs) > 0:
            for pair in pairs:
                found = False
                for group in groups:
                    if pair[0] in group or pair[1] in group:
                        group.add(pair[0])
                        group.add(pair[1])
                        found = True
                if not found:
                    new_group = set()
                    new_group.add(pair[0])
                    new_group.add(pair[1])
                    groups.append(new_group)

            # Remove redundant sets
            group_sets = [groups[0]]
            for group in groups:
                found = False
                for group_ in group_sets:
                    intersection = group.intersection(group_)
                    if len(intersection) == 0:
                        group_sets.append(group)
                    else:
                        for item in group:
                            group_.add(item)

        self.configs['group_sets'] = [list(x) for x in groups]

    def build_cluster_centroids(self):
        """
        Builds the centroids to use during the clustering
        process by finding the mean coord for the group
        """
        groups = self.configs['group_sets']
        activities = self.configs['activities']
        for group in groups:
            # Compute the centroid of the group
            coords = []
            for item in group:
                for activity in activities:
                    if activity['label'] == item:
                        coords.append(activity['coordinates'])

            # Add the centroid for each member of the group
            centroid_x = np.mean([x[0] for x in coords])
            centroid_y = np.mean([x[1] for x in coords])
            centroid = [centroid_x, centroid_y]
            for item in group:
                for activity in activities:
                    if activity['label'] == item:
                        activity['cluster_coords'] = centroid

        # Add cluster centroid for activities that are not grouped
        for activity in activities:
            if 'cluster_coords' not in activity:
                activity['cluster_coords'] = activity['coordinates']

    def get_activity(self, day, index):
        """
        Returns the activity at the given index
        """
        activities = self.configs['activities']
        for activity in activities:
            if activity['index'] == index:
                if activity['day'] == day:
                    return activity
        return None

    def get_activity_by_label(self, label):
        """
        Returns an activity, given a label
        """
        activities = self.configs['activities']
        for activity in activities:
            if activity['label'] == label:
                return activity
        return None

    def get_index(self, label):
        """
        Returns the index of an activity given a label
        """
        label = str(label)
        activities = self.configs['activities']
        for activity in activities:
            if activity['label'] == label:
                return str(activity['index'])
        return None

    def get_day(self, label):
        """
        Returns the day for an activity
        """
        label = str(label)
        activities = self.configs['activities']
        for activity in activities:
            if activity['label'] == label:
                return str(activity['day'])
        return None

    def distance_matrix(self):
        """
        Builds a distance matrix using labels
        """
        # Create the distance matrix
        coords = [x['coordinates'] for x in self.configs['activities']]
        origin = self.configs['origin']['coordinates']
        destination = self.configs['destination']['coordinates']
        coords.append(origin)
        coords.append(destination)
        distances = self.msdn.compute_distance_matrix(coords)
        
        # Map indices to labels
        num_activities = len(self.configs['activities'])
        index_map = {
            num_activities : 'origin',
            num_activities+1 : 'destination'
        }
        for i in range(num_activities):
            label = self.configs['activities'][i]['label']
            index_map[i] = label

        # Create a distance matrix with labels
        label_distances = {}
        for key in distances.keys():
            distances_ = {}
            for key_ in distances[key].keys():
                distances_[index_map[key_]] = distances[key][key_]
            label_distances[index_map[key]] = distances_

        return label_distances


    def _valid_tsp(self):
        """
        Verifies to ensure that the configuration
        is valid
        """
        required_keys = ['activities', 'origin', 'destination']
        for key in required_keys:
            try:
                assert key in self.configs
            except:
                msg = "%s not in json keys"%(key)
                self.logger.error(msg)
                return False
        if 'constraints' in self.configs:
            try:
                assert type(self.configs['constraints']) == list
            except:
                self.logger.error('The constraints key must be a list')
                return False
            constraints = self.configs['constraints']
            for i, constraint in enumerate(constraints):
                required_keys = ['activity1', 'constraint_type', 'activity2']
                for key in required_keys:
                    try:
                        assert key in constraint
                    except:
                        msg = "%s key not in constraint %s"%(key,i)
                        self.logger.error(msg)
                        return False
        return True