def __init__(self, configs): daiquiri.setup(level=logging.INFO) self.logger = daiquiri.getLogger(__name__) self.configs = configs self.tsp = TSP() self.msdn = MSDN()
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]
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
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 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 } })
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