├── .gitignore ├── LICENSE ├── README.md ├── base_util ├── data_util.py ├── data_util_test.py ├── json_util.py └── json_util_test.py ├── config ├── config.py ├── config_test.py └── runner.config ├── data ├── city_visit.py ├── city_visit_test.py ├── city_visit_test_utils.py ├── database_connection.py ├── point.py ├── point_test.py ├── read_csv.py ├── read_csv_test.py ├── runner_util.py ├── test_nyc_1.csv ├── test_sf_1.csv └── test_util.py ├── finder ├── city_visit_finder.py ├── city_visit_finder_test.py ├── runner.py ├── runner_doc.py ├── runner_nyc_1.py ├── runner_sf_1.py └── runner_test.py ├── ranker ├── age_group_rank_adjuster.py ├── age_group_rank_adjuster_test.py ├── point_type_rank_adjuster.py ├── point_type_rank_adjuster_test.py ├── points_ranker.py ├── points_ranker_test.py ├── popularity_rank_adjuster.py ├── popularity_rank_adjuster_test.py ├── rank_adjuster_interface.py ├── runner.py ├── runner_nyc_1.py ├── runner_sf_1.py ├── runner_test.py └── test_util.py └── router ├── city_visit_accumulator.py ├── city_visit_accumulator_test.py ├── city_visit_heap.py ├── city_visit_heap_test.py ├── city_visit_points_left.py ├── city_visit_points_left_test.py ├── city_visit_router.py ├── city_visit_router_test.py ├── cost_accumulator.py ├── cost_accumulator_test.py ├── day_visit_cost_calculator.py ├── day_visit_cost_calculator_interface.py ├── day_visit_cost_calculator_test.py ├── day_visit_heap.py ├── day_visit_heap_test.py ├── day_visit_router.py ├── day_visit_router_test.py ├── days_permutations.py ├── days_permutations_test.py ├── move_calculator.py ├── move_calculator_test.py ├── multi_day_visit_cost_calculator.py ├── multi_day_visit_cost_calculator_test.py ├── point_fit.py ├── point_fit_test.py ├── points_queue.py ├── points_queue_test.py ├── runner.py ├── runner_nyc_1.py ├── runner_sf_1.py ├── runner_test.py ├── test_points_nyc.txt ├── test_points_sf.txt └── test_util.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 igushev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /base_util/data_util.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | 3 | 4 | def Repr(obj): 5 | return '\n'.join(['%s: %s' % (key, value) 6 | for key, value in sorted(obj.__dict__.items())]) 7 | 8 | 9 | def HashKey(obj): 10 | if hasattr(obj, 'HashKey'): 11 | return obj.HashKey() 12 | elif hasattr(obj, '__dict__'): 13 | return HashKey(obj.__dict__) 14 | elif isinstance(obj, (list, tuple)): 15 | m = hashlib.md5() 16 | for item in obj: 17 | m.update(HashKey(item).encode('utf-8')) 18 | return m.hexdigest() 19 | elif isinstance(obj, set): 20 | m = hashlib.md5() 21 | for item in sorted(obj): 22 | m.update(HashKey(item).encode('utf-8')) 23 | return m.hexdigest() 24 | elif isinstance(obj, dict): 25 | m = hashlib.md5() 26 | for key, value in sorted(obj.items()): 27 | m.update(HashKey(key).encode('utf-8')) 28 | m.update(HashKey(value).encode('utf-8')) 29 | return m.hexdigest() 30 | else: 31 | m = hashlib.md5() 32 | m.update(repr(obj).encode('utf-8')) 33 | return m.hexdigest() 34 | 35 | 36 | class AbstractObject(object): 37 | 38 | def __str__(self): 39 | return Repr(self) 40 | 41 | def __repr__(self): 42 | return Repr(self) 43 | 44 | def __eq__(self, other): 45 | if other is None: 46 | return False 47 | return self.__dict__ == other.__dict__ 48 | 49 | def __hash__(self): 50 | return int(HashKey(self.__dict__), 16) 51 | 52 | def HashKey(self): 53 | return HashKey(self.__dict__) 54 | -------------------------------------------------------------------------------- /base_util/data_util_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import data_util 4 | 5 | 6 | class ReprTest(unittest.TestCase): 7 | 8 | def testGeneral(self): 9 | class Breakfast(object): 10 | def __init__(self, ham, egg): 11 | self._ham = ham 12 | self._egg = egg 13 | 14 | self.assertEqual( 15 | '_egg: scramble\n_ham: canadian', 16 | data_util.Repr(Breakfast('canadian', 'scramble'))) 17 | 18 | 19 | class HashKeyTest(unittest.TestCase): 20 | 21 | def testWithDict(self): 22 | class Breakfast(object): 23 | def __init__(self, ham): 24 | self._ham = ham 25 | 26 | # An object with __dict__. 27 | self.assertIsNotNone(data_util.HashKey(Breakfast('egg'))) 28 | 29 | def testList(self): 30 | # A list. 31 | self.assertIsNotNone(data_util.HashKey(['spam', 'egg', 'ham'])) 32 | 33 | def testSet(self): 34 | # A set. 35 | self.assertIsNotNone(data_util.HashKey({'spam', 'egg', 'ham'})) 36 | 37 | def testDict(self): 38 | # A dict. 39 | self.assertIsNotNone(data_util.HashKey({1: 'spam', 3: 'egg', 5: 'ham'})) 40 | 41 | def testWithRepr(self): 42 | # A simple object with repr. 43 | self.assertIsNotNone(data_util.HashKey('spam')) 44 | self.assertIsNotNone(data_util.HashKey(1)) 45 | self.assertIsNotNone(data_util.HashKey(3.)) 46 | 47 | 48 | if __name__ == '__main__': 49 | unittest.main() 50 | -------------------------------------------------------------------------------- /base_util/json_util.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import datetime 3 | import inspect 4 | import json 5 | import os 6 | 7 | 8 | DATE_FORMAT = '%Y-%m-%d' 9 | TIME_FORMAT = '%H:%M:%S' 10 | DATETIME_FORMAT = '%sT%s' % (DATE_FORMAT, TIME_FORMAT) 11 | DATETIME_FORMAT_1 = '%s %s' % (DATE_FORMAT, TIME_FORMAT) 12 | MODULE_FIELD = '__module__' 13 | CLASS_FIELD = '__class__' 14 | FUNC_FIELD = '__func__' 15 | 16 | 17 | def AssertIsInstance(obj, expected_type): 18 | if not isinstance(obj, expected_type): 19 | raise AssertionError('Type must be %s, but type is %s, value is %s' % (expected_type, type(obj), obj)) 20 | 21 | 22 | class JSONObjectInterface(object): 23 | 24 | def ToSimple(self, field_obj): 25 | raise NotImplemented() 26 | 27 | def FromSimple(self, simple): 28 | raise NotImplemented() 29 | 30 | 31 | class JSONString(JSONObjectInterface): 32 | 33 | def ToSimple(self, field_obj): 34 | AssertIsInstance(field_obj, str) 35 | return field_obj 36 | 37 | def FromSimple(self, simple): 38 | AssertIsInstance(simple, str) 39 | return simple 40 | 41 | 42 | class JSONFloat(JSONObjectInterface): 43 | 44 | def ToSimple(self, field_obj): 45 | AssertIsInstance(field_obj, float) 46 | return field_obj 47 | 48 | def FromSimple(self, simple): 49 | AssertIsInstance(simple, (float, int)) 50 | return float(simple) 51 | 52 | 53 | class JSONInt(JSONObjectInterface): 54 | 55 | def ToSimple(self, field_obj): 56 | AssertIsInstance(field_obj, int) 57 | return field_obj 58 | 59 | def FromSimple(self, simple): 60 | AssertIsInstance(simple, (float, int)) 61 | return int(simple) 62 | 63 | 64 | class JSONBool(JSONObjectInterface): 65 | 66 | def ToSimple(self, field_obj): 67 | AssertIsInstance(field_obj, bool) 68 | return field_obj 69 | 70 | def FromSimple(self, simple): 71 | AssertIsInstance(simple, (bool, int)) 72 | return bool(simple) 73 | 74 | 75 | class JSONDate(JSONObjectInterface): 76 | 77 | def ToSimple(self, field_obj): 78 | AssertIsInstance(field_obj, datetime.date) 79 | return field_obj.strftime(DATE_FORMAT) 80 | 81 | def FromSimple(self, simple): 82 | AssertIsInstance(simple, str) 83 | return datetime.datetime.strptime(simple, DATE_FORMAT).date() 84 | 85 | 86 | class JSONDateTime(JSONObjectInterface): 87 | 88 | def ToSimple(self, field_obj): 89 | AssertIsInstance(field_obj, datetime.datetime) 90 | return field_obj.strftime(DATETIME_FORMAT) 91 | 92 | def FromSimple(self, simple): 93 | AssertIsInstance(simple, str) 94 | try: 95 | return datetime.datetime.strptime(simple, DATETIME_FORMAT) 96 | except: # For backward compatibility. 97 | return datetime.datetime.strptime(simple, DATETIME_FORMAT_1) 98 | 99 | 100 | class JSONFunction(JSONObjectInterface): 101 | 102 | def ToSimple(self, field_obj): 103 | assert inspect.isfunction(field_obj) 104 | return {MODULE_FIELD: field_obj.__module__, 105 | FUNC_FIELD: field_obj.__qualname__} 106 | 107 | def FromSimple(self, simple): 108 | AssertIsInstance(simple, dict) 109 | obj = os.sys.modules[simple[MODULE_FIELD]] 110 | for name in simple[FUNC_FIELD].split('.'): 111 | obj = getattr(obj, name) 112 | return obj 113 | 114 | 115 | class JSONObject(JSONObjectInterface): 116 | 117 | def __init__(self, cls): 118 | self._cls = cls 119 | 120 | def ToSimple(self, field_obj): 121 | return field_obj.ToSimple() 122 | 123 | def FromSimple(self, simple): 124 | return self._cls.FromSimple(simple) 125 | 126 | 127 | class JSONTuple(JSONObjectInterface): 128 | 129 | def __init__(self, json_obj_list): 130 | for json_obj in json_obj_list: 131 | AssertIsInstance(json_obj, JSONObjectInterface) 132 | self._json_obj_list = json_obj_list 133 | 134 | def ToSimple(self, field_obj): 135 | AssertIsInstance(field_obj, tuple) 136 | return [(json_obj.ToSimple(item_obj) if item_obj is not None else None) 137 | for json_obj, item_obj in zip(self._json_obj_list, field_obj)] 138 | 139 | def FromSimple(self, simple): 140 | AssertIsInstance(simple, list) 141 | return tuple((json_obj.FromSimple(item_simple) if item_simple is not None else None) 142 | for json_obj, item_simple in zip(self._json_obj_list, simple)) 143 | 144 | 145 | class JSONList(JSONObjectInterface): 146 | 147 | def __init__(self, json_obj): 148 | AssertIsInstance(json_obj, JSONObjectInterface) 149 | self._json_obj = json_obj 150 | 151 | def ToSimple(self, field_obj): 152 | AssertIsInstance(field_obj, list) 153 | return [(self._json_obj.ToSimple(item_obj) if item_obj is not None else None) 154 | for item_obj in field_obj] 155 | 156 | def FromSimple(self, simple): 157 | AssertIsInstance(simple, list) 158 | return [(self._json_obj.FromSimple(item_simple) if item_simple is not None else None) 159 | for item_simple in simple] 160 | 161 | 162 | class JSONDict(object): 163 | 164 | def __init__(self, key_json_obj, value_json_obj): 165 | AssertIsInstance(key_json_obj, JSONObjectInterface) 166 | AssertIsInstance(value_json_obj, JSONObjectInterface) 167 | self._key_json_obj = key_json_obj 168 | self._value_json_obj = value_json_obj 169 | 170 | def ToSimple(self, field_obj): 171 | AssertIsInstance(field_obj, dict) 172 | return {self._key_json_obj.ToSimple(key_obj): 173 | (self._value_json_obj.ToSimple(value_obj) if value_obj is not None else None) 174 | for key_obj, value_obj in field_obj.items()} 175 | 176 | def FromSimple(self, simple): 177 | AssertIsInstance(simple, dict) 178 | return {self._key_json_obj.FromSimple(key_simple): 179 | (self._value_json_obj.FromSimple(value_simple) if value_simple is not None else None) 180 | for key_simple, value_simple in simple.items()} 181 | 182 | 183 | def ToSimple(self): 184 | obj_cls = self.__class__ 185 | simple = dict() 186 | for key, value_obj in self.__dict__.items(): 187 | json_obj = obj_cls.desc_dict[key] 188 | simple[key] = (json_obj.ToSimple(value_obj) 189 | if value_obj is not None else None) 190 | for key, json_obj in obj_cls.desc_dict.items(): 191 | if key in self.__dict__: 192 | continue 193 | simple[key] = None 194 | if obj_cls.inherited: 195 | simple[MODULE_FIELD] = obj_cls.__module__ 196 | simple[CLASS_FIELD] = obj_cls.__name__ 197 | return simple 198 | 199 | 200 | def ToJSON(self): 201 | return json.dumps(self.ToSimple()) 202 | 203 | 204 | @classmethod 205 | def FromSimple(cls, simple): 206 | if cls.inherited: 207 | obj_cls = getattr(os.sys.modules[simple[MODULE_FIELD]], simple[CLASS_FIELD]) 208 | else: 209 | obj_cls = cls 210 | 211 | obj_dict = dict() 212 | for key, value_simple in simple.items(): 213 | if key in [MODULE_FIELD, CLASS_FIELD]: 214 | continue 215 | json_obj = obj_cls.desc_dict[key] 216 | obj_dict[str(key)] = (json_obj.FromSimple(value_simple) 217 | if value_simple is not None else None) 218 | for key, json_obj in obj_cls.desc_dict.items(): 219 | if key in simple: 220 | continue 221 | obj_dict[key] = None 222 | obj = obj_cls.__new__(obj_cls) 223 | obj.__dict__ = obj_dict 224 | return obj 225 | 226 | 227 | @classmethod 228 | def FromJSON(cls, data): 229 | return cls.FromSimple(json.loads(data)) 230 | 231 | 232 | class JSONDecorator(object): 233 | 234 | def __init__(self, desc_dict=None, inherited=False): 235 | self.desc_dict = desc_dict or {} 236 | self.inherited = inherited 237 | 238 | def __call__(self, cls): 239 | desc_dict = copy.copy(self.desc_dict) 240 | inherited = self.inherited 241 | 242 | cls_queue = [cls] 243 | cls_visited = set([]) 244 | while len(cls_queue): 245 | curr_cls = cls_queue.pop(0) 246 | for base in curr_cls.__bases__: 247 | if getattr(base, 'desc_dict', None) is None: 248 | continue 249 | if base in cls_visited: 250 | continue 251 | cls_visited.add(base) 252 | desc_dict.update(base.desc_dict) 253 | inherited = inherited or base.inherited 254 | cls_queue.append(base) 255 | 256 | cls.desc_dict = desc_dict 257 | cls.inherited = inherited 258 | cls.ToSimple = ToSimple 259 | cls.ToJSON = ToJSON 260 | cls.FromSimple = FromSimple 261 | cls.FromJSON = FromJSON 262 | return cls 263 | -------------------------------------------------------------------------------- /config/config.py: -------------------------------------------------------------------------------- 1 | from configparser import ConfigParser 2 | 3 | from data import city_visit 4 | from data import test_util as point_test_util 5 | from finder import city_visit_finder as city_visit_finder_ 6 | from ranker import age_group_rank_adjuster 7 | from ranker import point_type_rank_adjuster 8 | from ranker import points_ranker as points_ranker_ 9 | from ranker import popularity_rank_adjuster 10 | from router import point_fit as point_fit_ 11 | from router import cost_accumulator 12 | from router import move_calculator 13 | from router import day_visit_cost_calculator 14 | from router import multi_day_visit_cost_calculator 15 | from router import day_visit_router as day_visit_router_ 16 | from router import city_visit_points_left 17 | from router import points_queue 18 | from router import city_visit_router as city_visit_router_ 19 | from router import city_visit_accumulator 20 | 21 | 22 | def GetConfig(filepath=None): 23 | config = ConfigParser() 24 | if filepath: 25 | config.read(filepath) 26 | return config 27 | 28 | 29 | def GetPointsRanker(config): 30 | rank_adjusters = [popularity_rank_adjuster.PopularityRankAdjuster(), 31 | point_type_rank_adjuster.PointTypeRankAdjuster(), 32 | age_group_rank_adjuster.AgeGroupRankAdjuster()] 33 | 34 | points_ranker = points_ranker_.PointsRanker(rank_adjusters) 35 | return points_ranker 36 | 37 | 38 | def GetPointFit(config): 39 | point_fit = point_fit_.SimplePointFit() 40 | return point_fit 41 | 42 | 43 | def GetCostAccumulatorGenerator(config): 44 | cag_section = 'cost_accumulator_generator' 45 | 46 | point_visit_factor = config.getfloat(cag_section, 'point_visit_factor') 47 | move_walking_factor = config.getfloat(cag_section, 'move_walking_factor') 48 | move_driving_factor = config.getfloat(cag_section, 'move_driving_factor') 49 | move_ptt_factor = config.getfloat(cag_section, 'move_ptt_factor') 50 | lunch_factor = config.getfloat(cag_section, 'lunch_factor') 51 | no_point_visit_factor = config.getfloat(cag_section, 'no_point_visit_factor') 52 | no_point_visit_const = config.getfloat(cag_section, 'no_point_visit_const') 53 | unused_time_factor = config.getfloat(cag_section, 'unused_time_factor') 54 | 55 | cost_accumulator_generator = cost_accumulator.FactorCostAccumulatorGenerator( 56 | point_visit_factor=point_visit_factor, 57 | move_walking_factor=move_walking_factor, 58 | move_driving_factor=move_driving_factor, 59 | move_ptt_factor=move_ptt_factor, 60 | lunch_factor=lunch_factor, 61 | no_point_visit_factor=no_point_visit_factor, 62 | no_point_visit_const=no_point_visit_const, 63 | unused_time_factor=unused_time_factor) 64 | 65 | return cost_accumulator_generator 66 | 67 | 68 | def GetDayVisitCostCalculatorGenerator(config, point_fit, cost_accumulator_generator): 69 | dvccg_section = 'day_visit_const_calculator_generator' 70 | 71 | if (config.has_option(dvccg_section, 'driving_speed') or 72 | config.has_option(dvccg_section, 'pause_before_driving')): 73 | assert (config.has_option(dvccg_section, 'driving_speed') and 74 | config.has_option(dvccg_section, 'pause_before_driving')), ( 75 | 'driving_speed and pause_before_driving should both either' 76 | ' present or absent') 77 | driving = True 78 | driving_speed = config.getfloat(dvccg_section, 'driving_speed') 79 | pause_before_driving = ( 80 | config.getfloat(dvccg_section, 'pause_before_driving')) 81 | else: 82 | driving = False 83 | 84 | walking_speed = config.getfloat(dvccg_section, 'walking_speed') 85 | pause_before_walking = config.getfloat(dvccg_section, 'pause_before_walking') 86 | ptt_speed = config.getfloat(dvccg_section, 'ptt_speed') 87 | pause_before_ptt = config.getfloat(dvccg_section, 'pause_before_ptt') 88 | ptt_cost_mult = config.getfloat(dvccg_section, 'ptt_cost_mult') 89 | assert ptt_cost_mult < ptt_speed / walking_speed 90 | 91 | # Minimum distance which can be set as max_walking_distance, since using 92 | # PTT less would cause PTT taking more time than walking. 93 | min_max_walking_distance_before_ptt = ( 94 | ptt_speed * pause_before_ptt * walking_speed / 95 | (ptt_speed - walking_speed)) 96 | # Maximum distance which can set as max_walking_distance, since walking 97 | # more would cause not increasingly monotonic function of cost. 98 | max_max_walking_distance_before_ptt = ( 99 | ptt_speed * pause_before_ptt * walking_speed / 100 | ((ptt_speed / ptt_cost_mult) - walking_speed)) 101 | if config.has_option(dvccg_section, 'max_walking_distance'): 102 | max_walking_distance = config.getfloat(dvccg_section, 'max_walking_distance') 103 | else: 104 | max_walking_distance = min_max_walking_distance_before_ptt 105 | 106 | validate_max_walking_distance = config.getboolean(dvccg_section, 'validate_max_walking_distance') 107 | if validate_max_walking_distance: 108 | assert max_walking_distance >= min_max_walking_distance_before_ptt 109 | assert max_walking_distance <= max_max_walking_distance_before_ptt 110 | 111 | if driving: 112 | driving_move_calculator = move_calculator.SimpleMoveCalculator( 113 | driving_speed, city_visit.MoveType.driving, pause=pause_before_driving) 114 | 115 | walking_move_calculator = move_calculator.SimpleMoveCalculator( 116 | walking_speed, city_visit.MoveType.walking, pause=pause_before_walking) 117 | ptt_move_calculator = move_calculator.SimpleMoveCalculator( 118 | ptt_speed, city_visit.MoveType.ptt, pause=pause_before_ptt) 119 | 120 | walking_ptt_move_calculator = move_calculator.MultiMoveCalculator( 121 | [max_walking_distance], 122 | [walking_move_calculator, ptt_move_calculator]) 123 | 124 | if driving: 125 | driving_day_visit_const_calculator_generator = ( 126 | day_visit_cost_calculator.DayVisitCostCalculatorGenerator( 127 | move_calculator=driving_move_calculator, 128 | point_fit=point_fit, 129 | cost_accumulator_generator=cost_accumulator_generator)) 130 | 131 | ptt_day_visit_const_calculator_generator = day_visit_cost_calculator.DayVisitCostCalculatorGenerator( 132 | move_calculator=walking_ptt_move_calculator, 133 | point_fit=point_fit, 134 | cost_accumulator_generator=cost_accumulator_generator) 135 | 136 | if driving: 137 | day_visit_const_calculator_generator = ( 138 | multi_day_visit_cost_calculator.MultiDayVisitCostCalculatorGenerator( 139 | [driving_day_visit_const_calculator_generator, 140 | ptt_day_visit_const_calculator_generator])) 141 | else: 142 | day_visit_const_calculator_generator = ( 143 | ptt_day_visit_const_calculator_generator) 144 | 145 | return day_visit_const_calculator_generator 146 | 147 | 148 | def GetPointsQueueGenerator(config): 149 | points_queue_generator = points_queue.OneByOnePointsQueueGenerator() 150 | return points_queue_generator 151 | 152 | 153 | def GetCityVisitRouter(config): 154 | cvr_section = 'city_visit_router' 155 | 156 | point_fit = GetPointFit(config) 157 | cost_accumulator_generator = GetCostAccumulatorGenerator(config) 158 | day_visit_const_calculator_generator = ( 159 | GetDayVisitCostCalculatorGenerator( 160 | config, 161 | point_fit=point_fit, 162 | cost_accumulator_generator=cost_accumulator_generator)) 163 | 164 | day_visit_heap_size = config.getint(cvr_section, 'day_visit_heap_size') 165 | day_visit_router = day_visit_router_.DayVisitRouter( 166 | calculator_generator=day_visit_const_calculator_generator, 167 | day_visit_heap_size=day_visit_heap_size) 168 | 169 | city_visit_points_left_generator = city_visit_points_left.CityVisitPointsLeftGenerator( 170 | cost_accumulator_generator=cost_accumulator_generator) 171 | points_queue_generator = GetPointsQueueGenerator(config) 172 | shard_num_days = config.getint(cvr_section, 'shard_num_days') 173 | max_depth = config.getint(cvr_section, 'max_depth') 174 | city_visit_heap_size = config.getint(cvr_section, 'city_visit_heap_size') 175 | max_non_pushed_points = config.getint(cvr_section, 'max_non_pushed_points') 176 | if config.has_option(cvr_section, 'num_processes'): 177 | num_processes = config.getint(cvr_section, 'num_processes') 178 | else: 179 | num_processes = None 180 | 181 | city_visit_router = city_visit_router_.CityVisitRouter( 182 | day_visit_router=day_visit_router, 183 | city_visit_points_left_generator=city_visit_points_left_generator, 184 | points_queue_generator=points_queue_generator, 185 | shard_num_days=shard_num_days, 186 | max_depth=max_depth, 187 | city_visit_heap_size=city_visit_heap_size, 188 | max_non_pushed_points=max_non_pushed_points, 189 | num_processes=num_processes) 190 | 191 | return city_visit_router 192 | 193 | 194 | def GetCityVisitFinder(config): 195 | points_ranker = GetPointsRanker(config) 196 | city_visit_router = GetCityVisitRouter(config) 197 | city_visit_finder = city_visit_finder_.CityVisitFinder( 198 | points_ranker=points_ranker, 199 | city_visit_router=city_visit_router) 200 | return city_visit_finder 201 | 202 | 203 | def GetCityVisitAccumulatorGenerator(config): 204 | city_visit_accumulator_generator = city_visit_accumulator.CityVisitAccumulatorGenerator() 205 | return city_visit_accumulator_generator 206 | -------------------------------------------------------------------------------- /config/config_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import config 4 | from router import day_visit_cost_calculator 5 | from router import multi_day_visit_cost_calculator 6 | 7 | 8 | class ConfigTest(unittest.TestCase): 9 | 10 | def setUp(self): 11 | super(ConfigTest, self).setUp() 12 | self.config = config.GetConfig() 13 | 14 | def _SetAndGetPointsRanker(self): 15 | points_ranker_runner = config.GetPointsRanker(self.config) 16 | return points_ranker_runner 17 | 18 | def testGetPointsRanker(self): 19 | self.assertIsNotNone(self._SetAndGetPointsRanker()) 20 | 21 | def _SetAndGetPointFit(self): 22 | point_fit = config.GetPointFit(self.config) 23 | return point_fit 24 | 25 | def testGetPointFit(self): 26 | self.assertIsNotNone(self._SetAndGetPointFit()) 27 | 28 | def _SetAndGetCostAccumulatorGenerator(self): 29 | cag_section = 'cost_accumulator_generator' 30 | 31 | self.config.add_section(cag_section) 32 | for option in ['point_visit_factor', 33 | 'move_walking_factor', 34 | 'move_driving_factor', 35 | 'move_ptt_factor', 36 | 'lunch_factor', 37 | 'no_point_visit_factor', 38 | 'no_point_visit_const', 39 | 'unused_time_factor']: 40 | self.config.set(cag_section, option, str(1.)) 41 | cost_accumulator_generator = config.GetCostAccumulatorGenerator(self.config) 42 | return cost_accumulator_generator 43 | 44 | def testGetCostAccumulatorGenerator(self): 45 | self.assertIsNotNone(self._SetAndGetCostAccumulatorGenerator()) 46 | 47 | def _SetAndGetDayVisitCostCalculatorGeneratorDriving(self): 48 | point_fit = self._SetAndGetPointFit() 49 | cost_accumulator_generator = self._SetAndGetCostAccumulatorGenerator() 50 | 51 | dvccg_section = 'day_visit_const_calculator_generator' 52 | self.config.add_section(dvccg_section) 53 | self.config.set(dvccg_section, 'driving_speed', str(20.)) 54 | self.config.set(dvccg_section, 'pause_before_driving', str(0.3)) 55 | self.config.set(dvccg_section, 'walking_speed', str(2.)) 56 | self.config.set(dvccg_section, 'pause_before_walking', str(0.)) 57 | self.config.set(dvccg_section, 'ptt_speed', str(15.)) 58 | self.config.set(dvccg_section, 'pause_before_ptt', str(0.25)) 59 | self.config.set(dvccg_section, 'ptt_cost_mult', str(7.49)) 60 | self.config.set(dvccg_section, 'validate_max_walking_distance', str(True)) 61 | 62 | day_visit_const_calculator_generator = ( 63 | config.GetDayVisitCostCalculatorGenerator( 64 | self.config, 65 | point_fit=point_fit, 66 | cost_accumulator_generator=cost_accumulator_generator)) 67 | return day_visit_const_calculator_generator 68 | 69 | def testGetDayVisitCostCalculatorGeneratorDriving(self): 70 | day_visit_const_calculator_generator = ( 71 | self._SetAndGetDayVisitCostCalculatorGeneratorDriving()) 72 | self.assertIsNotNone(day_visit_const_calculator_generator) 73 | self.assertTrue( 74 | isinstance( 75 | day_visit_const_calculator_generator, 76 | multi_day_visit_cost_calculator.MultiDayVisitCostCalculatorGenerator)) 77 | self.assertEqual( 78 | 2, len(day_visit_const_calculator_generator.calculator_generators)) 79 | self.assertTrue( 80 | isinstance( 81 | day_visit_const_calculator_generator.calculator_generators[0], 82 | day_visit_cost_calculator.DayVisitCostCalculatorGenerator)) 83 | self.assertTrue( 84 | isinstance( 85 | day_visit_const_calculator_generator.calculator_generators[1], 86 | day_visit_cost_calculator.DayVisitCostCalculatorGenerator)) 87 | 88 | def _SetAndGetDayVisitCostCalculatorGeneratorNoDriving( 89 | self, point_fit, cost_accumulator_generator): 90 | 91 | dvccg_section = 'day_visit_const_calculator_generator' 92 | self.config.add_section(dvccg_section) 93 | self.config.set(dvccg_section, 'walking_speed', str(2.)) 94 | self.config.set(dvccg_section, 'pause_before_walking', str(0.)) 95 | self.config.set(dvccg_section, 'ptt_speed', str(15.)) 96 | self.config.set(dvccg_section, 'pause_before_ptt', str(0.25)) 97 | self.config.set(dvccg_section, 'ptt_cost_mult', str(7.49)) 98 | self.config.set(dvccg_section, 'validate_max_walking_distance', str(True)) 99 | 100 | day_visit_const_calculator_generator = ( 101 | config.GetDayVisitCostCalculatorGenerator( 102 | self.config, 103 | point_fit=point_fit, 104 | cost_accumulator_generator=cost_accumulator_generator)) 105 | return day_visit_const_calculator_generator 106 | 107 | def testGetDayVisitCostCalculatorGeneratorNoDriving(self): 108 | point_fit = self._SetAndGetPointFit() 109 | cost_accumulator_generator = self._SetAndGetCostAccumulatorGenerator() 110 | day_visit_const_calculator_generator = ( 111 | self._SetAndGetDayVisitCostCalculatorGeneratorNoDriving( 112 | point_fit, cost_accumulator_generator)) 113 | self.assertIsNotNone(day_visit_const_calculator_generator) 114 | self.assertTrue( 115 | isinstance( 116 | day_visit_const_calculator_generator, 117 | day_visit_cost_calculator.DayVisitCostCalculatorGenerator)) 118 | 119 | def _SetAndGetPointsQueueGenerator(self): 120 | points_queue_generator = config.GetPointsQueueGenerator(self.config) 121 | return points_queue_generator 122 | 123 | def testGetPointsQueueGenerator(self): 124 | self.assertIsNotNone(self._SetAndGetPointsQueueGenerator()) 125 | 126 | def _SetAndGetCityVisitRouter(self): 127 | point_fit = self._SetAndGetPointFit() 128 | cost_accumulator_generator = self._SetAndGetCostAccumulatorGenerator() 129 | self._SetAndGetDayVisitCostCalculatorGeneratorNoDriving( 130 | point_fit, cost_accumulator_generator) 131 | 132 | cvr_section = 'city_visit_router' 133 | self.config.add_section(cvr_section) 134 | for option in ['day_visit_heap_size', 'shard_num_days', 'max_depth', 135 | 'city_visit_heap_size', 'max_non_pushed_points']: 136 | self.config.set(cvr_section, option, str(1)) 137 | 138 | city_visit_router = config.GetCityVisitRouter(self.config) 139 | return city_visit_router 140 | 141 | def testGetCityVisitRouter(self): 142 | self.assertIsNotNone(self._SetAndGetCityVisitRouter()) 143 | 144 | def _SetAndGetCityVisitFinder(self): 145 | self._SetAndGetPointsRanker() 146 | self._SetAndGetCityVisitRouter() 147 | 148 | city_visit_finder = config.GetCityVisitFinder(self.config) 149 | return city_visit_finder 150 | 151 | def testGetCityVisitFinder(self): 152 | self.assertIsNotNone(self._SetAndGetCityVisitFinder()) 153 | 154 | def _SetAndGetCityVisitAccumulatorGenerator(self): 155 | city_visit_accumulator_generator = ( 156 | config.GetCityVisitAccumulatorGenerator(self.config)) 157 | return city_visit_accumulator_generator 158 | 159 | def testGetCityVisitAccumulatorGenerator(self): 160 | self.assertIsNotNone(self._SetAndGetCityVisitAccumulatorGenerator()) 161 | 162 | 163 | if __name__ == '__main__': 164 | unittest.main() 165 | -------------------------------------------------------------------------------- /config/runner.config: -------------------------------------------------------------------------------- 1 | [cost_accumulator_generator] 2 | point_visit_factor=0. 3 | move_walking_factor=1. 4 | # ptt_cost_mult 5 | move_driving_factor=7.49 6 | # ptt_cost_mult 7 | move_ptt_factor=7.49 8 | lunch_factor=0. 9 | no_point_visit_factor = 0. 10 | no_point_visit_const = 1000. 11 | unused_time_factor = 0.01 12 | 13 | [day_visit_const_calculator_generator] 14 | # Speed of car in traffic jams in mph. 15 | driving_speed=20. 16 | # 10 minutes to find and than park a car and 10 minutes to find a parking 17 | # spot when arrived. 18 | pause_before_driving=0.3 19 | # Walking speed in mph. 20 | walking_speed=2. 21 | pause_before_walking=0. 22 | # Speed of Public Transportation or Taxi in mph. 23 | ptt_speed=15. 24 | # 15 minutes to buy a ticket and wait in case of public transportation or 25 | # call a taxi. 26 | pause_before_ptt=0.25 27 | # Multiplier which penalize PTT against walking. 28 | ptt_cost_mult = 7.49 29 | max_walking_distance=1.0 30 | validate_max_walking_distance=False 31 | 32 | [city_visit_router] 33 | day_visit_heap_size=1000 34 | shard_num_days=2 35 | max_depth=1 36 | city_visit_heap_size=10 37 | max_non_pushed_points=5 38 | # Let infer from number of CPUs. 39 | # num_processes=None 40 | -------------------------------------------------------------------------------- /data/city_visit_test.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import unittest 3 | 4 | from data import city_visit 5 | from data import city_visit_test_utils 6 | 7 | 8 | class DayVisitParametersTest(city_visit_test_utils.CityVisitTestExample): 9 | 10 | def testDatelessHashKey(self): 11 | day_visit_parameters_9to21 = city_visit.DayVisitParameters( 12 | start_datetime=datetime.datetime(2014, 9, 1, 9, 0, 0), 13 | end_datetime=datetime.datetime(2014, 9, 1, 21, 0, 0), 14 | lunch_start_datetime=datetime.datetime(2014, 9, 1, 13, 0, 0), 15 | lunch_hours=1., 16 | start_coordinates=self.hotel_coordinates, 17 | end_coordinates=self.hotel_coordinates) 18 | day_visit_parameters_9to23 = city_visit.DayVisitParameters( 19 | start_datetime=datetime.datetime(2014, 9, 1, 9, 0, 0), 20 | end_datetime=datetime.datetime(2014, 9, 1, 23, 0, 0), 21 | lunch_start_datetime=datetime.datetime(2014, 9, 1, 13, 0, 0), 22 | lunch_hours=1., 23 | start_coordinates=self.hotel_coordinates, 24 | end_coordinates=self.hotel_coordinates) 25 | # Stupid test. 26 | self.assertEqual(day_visit_parameters_9to21.DatelessHashKey(), 27 | day_visit_parameters_9to21.DatelessHashKey()) 28 | self.assertNotEqual(day_visit_parameters_9to21.DatelessHashKey(), 29 | day_visit_parameters_9to23.DatelessHashKey()) 30 | 31 | 32 | class MoveBetweenTest(city_visit_test_utils.CityVisitTestExample): 33 | 34 | def testInitValidation(self): 35 | # move_hours in move_description are not consistent with 36 | # start_end_datetime. 37 | self.assertRaises( 38 | AssertionError, city_visit.MoveBetween, 39 | city_visit.StartEndDatetime(datetime.datetime(2014, 9, 1, 11, 15, 0), 40 | datetime.datetime(2014, 9, 1, 11, 45, 0)), 41 | city_visit.MoveDescription(self.points['Ferry Building'].coordinates_ends, 42 | self.points['Pier 39'].coordinates_starts, 43 | 1.0, city_visit.MoveType.walking)) 44 | 45 | 46 | class DayVisitTest(city_visit_test_utils.CityVisitTestExample): 47 | 48 | def testDatelessHashKey(self): 49 | self.assertEqual(self.day_visit_1.DatelessHashKey(), 50 | self.day_visit_1.DatelessHashKey()) 51 | self.assertNotEqual(self.day_visit_1.DatelessHashKey(), 52 | self.day_visit_2.DatelessHashKey()) 53 | 54 | def testGetPoints(self): 55 | self.assertEqual([self.points['Ferry Building'], self.points['Pier 39']], 56 | self.day_visit_1.GetPoints()) 57 | self.assertEqual([self.points['Golden Gate Bridge']], 58 | self.day_visit_2.GetPoints()) 59 | 60 | def testInitValidation(self): 61 | # No final move. 62 | self.assertRaisesRegex( 63 | AssertionError, 'Wrong number of actions.', 64 | city_visit.DayVisit, datetime.datetime(2014, 9, 1, 9, 0, 0), [ 65 | self.from_hotel_to_ferry_building_move, 66 | self.ferry_building_point_visit, 67 | self.from_ferry_building_to_pier_39_move, 68 | self.pier_39_point_visit], 10.) 69 | # No start move. 70 | self.assertRaisesRegex( 71 | AssertionError, 'Wrong order of actions: no MoveBetween.', 72 | city_visit.DayVisit, datetime.datetime(2014, 9, 1, 9, 0, 0), [ 73 | self.ferry_building_point_visit, 74 | self.from_ferry_building_to_pier_39_move, 75 | self.pier_39_point_visit, 76 | self.from_pier_39_to_hotel], 10.) 77 | # No middle moves. 78 | self.assertRaisesRegex( 79 | AssertionError, 'Wrong order of actions: no MoveBetween.', 80 | city_visit.DayVisit, datetime.datetime(2014, 9, 1, 9, 0, 0), [ 81 | self.from_hotel_to_ferry_building_move, 82 | self.ferry_building_point_visit, 83 | self.pier_39_point_visit, 84 | self.from_pier_39_to_hotel], 10.) 85 | # No first point. 86 | self.assertRaisesRegex( 87 | AssertionError, 'Wrong order of actions: no PointVisit.', 88 | city_visit.DayVisit, datetime.datetime(2014, 9, 1, 9, 0, 0), [ 89 | self.from_hotel_to_ferry_building_move, 90 | self.from_ferry_building_to_pier_39_move, 91 | self.pier_39_point_visit, 92 | self.from_pier_39_to_hotel], 10.) 93 | # No second point. 94 | self.assertRaisesRegex( 95 | AssertionError, 'Wrong order of actions: no PointVisit.', 96 | city_visit.DayVisit, datetime.datetime(2014, 9, 1, 9, 0, 0), [ 97 | self.from_hotel_to_ferry_building_move, 98 | self.ferry_building_point_visit, 99 | self.from_ferry_building_to_pier_39_move, 100 | self.from_pier_39_to_hotel], 10.) 101 | 102 | 103 | class CityVisitTest(city_visit_test_utils.CityVisitTestExample): 104 | 105 | def testGetPoints(self): 106 | self.assertEqual([self.points['Ferry Building'], self.points['Pier 39'], 107 | self.points['Golden Gate Bridge']], 108 | self.city_visit.GetPoints()) 109 | 110 | def testStr(self): 111 | city_visit_str_actual = '%s' % self.city_visit 112 | city_visit_str_expected = """Date: 2014-09-01 113 | Walking from 37.7833:-122.4167 to 37.7955:-122.3937 from 09:00:00 to 10:15:00 114 | Visiting point "Ferry Building" from 10:15:00 to 11:15:00 115 | Walking from 37.7955:-122.3937 to 37.8100:-122.4104 from 11:15:00 to 11:45:00 116 | Visiting point "Pier 39" from 11:45:00 to 14:45:00 117 | Walking from 37.8100:-122.4104 to 37.7833:-122.4167 from 14:45:00 to 16:15:00 118 | Cost: 12.00 119 | Price: 0.00 120 | Date: 2014-09-02 121 | Driving from 37.7833:-122.4167 to 37.8197:-122.4786 from 09:00:00 to 09:15:00 122 | Visiting point "Golden Gate Bridge" from 09:15:00 to 09:45:00 123 | Driving from 37.8197:-122.4786 to 37.7833:-122.4167 from 09:45:00 to 10:00:00 124 | Cost: 10.00 125 | Price: 0.00 126 | Total cost: 20.00 127 | Total price: 0.00""" 128 | self.assertEqual(city_visit_str_expected, city_visit_str_actual) 129 | 130 | 131 | if __name__ == '__main__': 132 | unittest.main() 133 | -------------------------------------------------------------------------------- /data/city_visit_test_utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import datetime 3 | import unittest 4 | 5 | from data import city_visit 6 | from data import read_csv 7 | from data import point 8 | 9 | 10 | class CityVisitTestExample(unittest.TestCase): 11 | 12 | @staticmethod 13 | def GetHotelCoordinates(): 14 | # San Francisco coordinates. 15 | return point.Coordinates(37.7833, -122.4167) 16 | 17 | @staticmethod 18 | def GetDayVisitParameters(start_datetime, end_datetime, lunch_start_datetime): 19 | return city_visit.DayVisitParameters( 20 | start_datetime=start_datetime, 21 | end_datetime=end_datetime, 22 | lunch_start_datetime=lunch_start_datetime, 23 | lunch_hours=1., 24 | start_coordinates=CityVisitTestExample.GetHotelCoordinates(), 25 | end_coordinates=CityVisitTestExample.GetHotelCoordinates()) 26 | 27 | def setUp(self): 28 | 29 | self.day_visit_parameters_1 = CityVisitTestExample.GetDayVisitParameters( 30 | start_datetime=datetime.datetime(2014, 9, 1, 9, 0, 0), 31 | end_datetime=datetime.datetime(2014, 9, 1, 21, 0, 0), 32 | lunch_start_datetime=datetime.datetime(2014, 9, 1, 22, 0, 0)) 33 | 34 | self.day_visit_parameters_2 = CityVisitTestExample.GetDayVisitParameters( 35 | start_datetime=datetime.datetime(2014, 9, 2, 9, 0, 0), 36 | end_datetime=datetime.datetime(2014, 9, 2, 21, 0, 0), 37 | lunch_start_datetime=datetime.datetime(2014, 9, 2, 22, 0, 0)) 38 | 39 | self.points = read_csv.ReadCSVToDict(os.path.join('data', 'test_sf_1.csv')) 40 | 41 | self.hotel_coordinates = ( 42 | CityVisitTestExample.GetHotelCoordinates()) 43 | 44 | self.from_hotel_to_ferry_building_move = city_visit.MoveBetween( 45 | city_visit.StartEndDatetime(datetime.datetime(2014, 9, 1, 9, 0, 0), 46 | datetime.datetime(2014, 9, 1, 10, 15, 0)), 47 | city_visit.MoveDescription(self.hotel_coordinates, 48 | self.points['Ferry Building'].coordinates_starts, 49 | 1.25, city_visit.MoveType.walking)) 50 | self.ferry_building_point_visit = city_visit.PointVisit( 51 | city_visit.StartEndDatetime(datetime.datetime(2014, 9, 1, 10, 15, 0), 52 | datetime.datetime(2014, 9, 1, 11, 15, 0)), 53 | self.points['Ferry Building']) 54 | self.from_ferry_building_to_pier_39_move = city_visit.MoveBetween( 55 | city_visit.StartEndDatetime(datetime.datetime(2014, 9, 1, 11, 15, 0), 56 | datetime.datetime(2014, 9, 1, 11, 45, 0)), 57 | city_visit.MoveDescription(self.points['Ferry Building'].coordinates_ends, 58 | self.points['Pier 39'].coordinates_starts, 59 | 0.5, city_visit.MoveType.walking)) 60 | self.pier_39_point_visit = city_visit.PointVisit( 61 | city_visit.StartEndDatetime(datetime.datetime(2014, 9, 1, 11, 45, 0), 62 | datetime.datetime(2014, 9, 1, 14, 45, 0)), 63 | self.points['Pier 39']) 64 | self.from_pier_39_to_hotel = city_visit.MoveBetween( 65 | city_visit.StartEndDatetime(datetime.datetime(2014, 9, 1, 14, 45, 0), 66 | datetime.datetime(2014, 9, 1, 16, 15, 0)), 67 | city_visit.MoveDescription(self.points['Pier 39'].coordinates_ends, 68 | self.hotel_coordinates, 69 | 1.5, city_visit.MoveType.walking)) 70 | 71 | self.day_visit_1 = city_visit.DayVisit(datetime.datetime(2014, 9, 1, 9, 0, 0), [ 72 | self.from_hotel_to_ferry_building_move, 73 | self.ferry_building_point_visit, 74 | self.from_ferry_building_to_pier_39_move, 75 | self.pier_39_point_visit, 76 | self.from_pier_39_to_hotel], 12.) 77 | 78 | self.from_hotel_to_golden_gate_bridge_move = city_visit.MoveBetween( 79 | city_visit.StartEndDatetime(datetime.datetime(2014, 9, 2, 9, 0, 0), 80 | datetime.datetime(2014, 9, 2, 9, 15, 0)), 81 | city_visit.MoveDescription(self.hotel_coordinates, 82 | self.points['Golden Gate Bridge'].coordinates_starts, 83 | 0.25, city_visit.MoveType.driving)) 84 | self.golden_gate_bridge_point_visit = city_visit.PointVisit( 85 | city_visit.StartEndDatetime(datetime.datetime(2014, 9, 2, 9, 15, 0), 86 | datetime.datetime(2014, 9, 2, 9, 45, 0)), 87 | self.points['Golden Gate Bridge']) 88 | self.from_golden_gate_bridge_to_hotel_move = city_visit.MoveBetween( 89 | city_visit.StartEndDatetime(datetime.datetime(2014, 9, 2, 9, 45, 0), 90 | datetime.datetime(2014, 9, 2, 10, 0, 0)), 91 | city_visit.MoveDescription(self.points['Golden Gate Bridge'].coordinates_ends, 92 | self.hotel_coordinates, 93 | 0.25, city_visit.MoveType.driving)) 94 | 95 | self.day_visit_2 = city_visit.DayVisit(datetime.datetime(2014, 9, 2, 9, 0, 0), [ 96 | self.from_hotel_to_golden_gate_bridge_move, 97 | self.golden_gate_bridge_point_visit, 98 | self.from_golden_gate_bridge_to_hotel_move], 10.) 99 | 100 | self.city_visit_summary = city_visit.CityVisitSummary(20., 0.) 101 | self.city_visit = city_visit.CityVisit([self.day_visit_1, self.day_visit_2], self.city_visit_summary) 102 | 103 | super(CityVisitTestExample, self).setUp() 104 | -------------------------------------------------------------------------------- /data/database_connection.py: -------------------------------------------------------------------------------- 1 | class DatabaseConnectionInterface(object): 2 | """Abstract class for DatabaseConnection.""" 3 | 4 | def GetPoints(self, visit_location): 5 | """For given visit_location return initial set of points.""" 6 | raise NotImplemented() 7 | 8 | def GetPoint(self, visit_location, point_name): 9 | """For given visit_location and Point name return a point.""" 10 | raise NotImplemented() 11 | -------------------------------------------------------------------------------- /data/point.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import datetime 3 | 4 | from base_util import data_util 5 | from base_util import json_util 6 | 7 | 8 | class CoordinatesInterface(data_util.AbstractObject): 9 | """Coordinates on Earth interface.""" 10 | 11 | def Copy(self): 12 | return copy.deepcopy(self) 13 | 14 | 15 | @json_util.JSONDecorator() 16 | class Coordinates(CoordinatesInterface): 17 | """Coordinates on Earth using latitude and longitude.""" 18 | 19 | def __init__(self, latitude, longitude): 20 | assert isinstance(latitude, float) 21 | assert isinstance(longitude, float) 22 | self.latitude = latitude 23 | self.longitude = longitude 24 | 25 | def __str__(self): 26 | return '%.4f:%.4f' % (self.latitude, self.longitude) 27 | 28 | 29 | class OperatingHoursInterface(data_util.AbstractObject): 30 | """Operating hours of a Point interface.""" 31 | pass 32 | 33 | 34 | @json_util.JSONDecorator({ 35 | 'opens': json_util.JSONDateTime(), 36 | 'closes': json_util.JSONDateTime()}) 37 | class OperatingHours(OperatingHoursInterface): 38 | """Operating hours of a Point straightforward implementation. Doesn't know 39 | about days of week, seasons, etc.""" 40 | 41 | def __init__(self, opens, closes): 42 | assert isinstance(opens, datetime.time) 43 | assert isinstance(closes, datetime.time) 44 | self.opens = opens 45 | self.closes = closes 46 | 47 | def __str__(self): 48 | return '%s - %s' % (self.opens, self.closes) 49 | 50 | 51 | class PointTypeInterface(data_util.AbstractObject): 52 | """Type of a point interface.""" 53 | pass 54 | 55 | 56 | @json_util.JSONDecorator({ 57 | 'city_tours': json_util.JSONInt(), 58 | 'landmarks': json_util.JSONInt(), 59 | 'nature': json_util.JSONInt(), 60 | 'museums': json_util.JSONInt(), 61 | 'shopping': json_util.JSONInt(), 62 | 'dining': json_util.JSONInt()}) 63 | class PointType(PointTypeInterface): 64 | """Type of a point implementation using assigned value to each type.""" 65 | 66 | def __init__(self, city_tours, landmarks, nature, museums, shopping, dining): 67 | """For each type of a point (city_tours, landmarks, nature, museums, 68 | shopping, dining) specifies a number from 0 to 100 how important this type 69 | to the user.""" 70 | 71 | if city_tours is not None: 72 | assert isinstance(city_tours, int) 73 | if landmarks is not None: 74 | assert isinstance(landmarks, int) 75 | if nature is not None: 76 | assert isinstance(nature, int) 77 | if museums is not None: 78 | assert isinstance(museums, int) 79 | if shopping is not None: 80 | assert isinstance(shopping, int) 81 | if dining is not None: 82 | assert isinstance(dining, int) 83 | 84 | self.city_tours = city_tours or 0 85 | self.landmarks = landmarks or 0 86 | self.nature = nature or 0 87 | self.museums = museums or 0 88 | self.shopping = shopping or 0 89 | self.dining = dining or 0 90 | 91 | def GetNamesPointTypes(self): 92 | return {'City Tours': self.city_tours, 93 | 'Landmarks': self.landmarks, 94 | 'Nature': self.nature, 95 | 'Museums': self.museums, 96 | 'Shopping': self.shopping, 97 | 'Dining': self.dining} 98 | 99 | def __str__(self): 100 | names_point_types = [ 101 | (name, point_type) 102 | for name, point_type in sorted(self.GetNamesPointTypes().items()) 103 | if point_type > 0] 104 | names_point_types = sorted( 105 | names_point_types, key = lambda name_point_type: name_point_type[1], 106 | reverse=True) 107 | if names_point_types: 108 | return ', '.join( 109 | ['%s (%d)' % (name, point_type) 110 | for name, point_type in names_point_types]) 111 | else: 112 | return 'No point type' 113 | 114 | 115 | class AgeGroupInterface(data_util.AbstractObject): 116 | """Age groups most suitable for a Point interface.""" 117 | pass 118 | 119 | 120 | @json_util.JSONDecorator({ 121 | 'senior': json_util.JSONInt(), 122 | 'adult': json_util.JSONInt(), 123 | 'junior': json_util.JSONInt(), 124 | 'child': json_util.JSONInt(), 125 | 'toddlers': json_util.JSONInt()}) 126 | class AgeGroup(AgeGroupInterface): 127 | """Age groups most suitable for a Point implementation using assigned value to 128 | each age group.""" 129 | 130 | def __init__(self, senior, adult, junior, child, toddlers): 131 | """For each age group (senior, adult, junior, child, toddlers) specifies a 132 | number from 0 to 100 how tailored trip should be to given age group.""" 133 | if senior is not None: 134 | assert isinstance(senior, int) 135 | if adult is not None: 136 | assert isinstance(adult, int) 137 | if junior is not None: 138 | assert isinstance(junior, int) 139 | if child is not None: 140 | assert isinstance(child, int) 141 | if toddlers is not None: 142 | assert isinstance(toddlers, int) 143 | 144 | self.senior = senior or 0 145 | self.adult = adult or 0 146 | self.junior = junior or 0 147 | self.child = child or 0 148 | self.toddlers = toddlers or 0 149 | 150 | def GetNamesAgeGroups(self): 151 | return {'Senior': self.senior, 152 | 'Adult': self.adult, 153 | 'Junior': self.junior, 154 | 'Child': self.child, 155 | 'Toddlers': self.toddlers} 156 | 157 | def __str__(self): 158 | names_age_groups = [ 159 | (name, age_group) 160 | for name, age_group in sorted(self.GetNamesAgeGroups().items()) 161 | if age_group > 0] 162 | names_age_groups = sorted( 163 | names_age_groups, key = lambda name_age_group: name_age_group[1], 164 | reverse=True) 165 | if names_age_groups: 166 | return ', '.join( 167 | ['%s (%d)' % (name, age_group) 168 | for name, age_group in names_age_groups]) 169 | else: 170 | return 'No age group' 171 | 172 | 173 | class PointInterface(data_util.AbstractObject): 174 | """Sightseeing, Attraction or Point Of Interest interface.""" 175 | pass 176 | 177 | 178 | @json_util.JSONDecorator({ 179 | 'coordinates_starts': json_util.JSONObject(Coordinates), 180 | 'coordinates_ends': json_util.JSONObject(Coordinates), 181 | 'operating_hours': json_util.JSONObject(OperatingHours), 182 | 'popularity': json_util.JSONInt(), 183 | 'point_type': json_util.JSONObject(PointType), 184 | 'age_group': json_util.JSONObject(AgeGroup), 185 | 'parking': json_util.JSONInt(), 186 | 'eating': json_util.JSONInt()}) 187 | class Point(PointInterface): 188 | """Sightseeing, Attraction or Point Of Interest common implementation.""" 189 | 190 | def __init__(self, name, coordinates_starts, coordinates_ends, 191 | operating_hours, duration, popularity, point_type, 192 | age_group, price, parking, eating): 193 | assert isinstance(name, str) # Must not be None 194 | # Must not be None 195 | assert isinstance(coordinates_starts, CoordinatesInterface) 196 | if coordinates_ends is not None: 197 | assert isinstance(coordinates_ends, CoordinatesInterface) 198 | if operating_hours is not None: 199 | assert isinstance(operating_hours, OperatingHoursInterface) 200 | assert isinstance(duration, float) # Must not be None 201 | assert isinstance(popularity, int) # Must not be None 202 | assert isinstance(point_type, PointTypeInterface) # Must not be None 203 | assert isinstance(age_group, AgeGroupInterface) # Must not be None 204 | if price is not None: 205 | assert isinstance(price, float) 206 | if parking is not None: 207 | assert isinstance(parking, int) 208 | if eating is not None: 209 | assert isinstance(eating, int) 210 | 211 | self.name = name 212 | self.coordinates_starts = coordinates_starts 213 | self.coordinates_ends = ( 214 | coordinates_ends if coordinates_ends is not None else coordinates_starts) 215 | self.operating_hours = operating_hours # None means 24/7. 216 | self.duration = duration 217 | self.popularity = popularity 218 | self.point_type = point_type 219 | self.age_group = age_group 220 | self.price = price or 0. 221 | self.parking = parking or 0 222 | self.eating = eating or 0 223 | 224 | def __str__(self): 225 | s = str() 226 | s += 'Name: "%s"\n' % self.name 227 | s += 'Coordinates Starts: %s\n' % self.coordinates_starts 228 | s += 'Coordinates Ends: %s\n' % self.coordinates_ends 229 | s += 'Operating Hours: %s\n' % ( 230 | self.operating_hours if self.operating_hours is not None else '24/7') 231 | s += 'Duration: %.2f\n' % self.duration 232 | s += 'Popularity: %d\n' % self.popularity 233 | s += 'Point type: %s\n' % self.point_type 234 | s += 'Age group: %s\n' % self.age_group 235 | s += 'Price: %.2f\n' % self.price 236 | s += 'Parking: %d\n' % self.parking 237 | s += 'Eating: %d\n' % self.eating 238 | return s 239 | -------------------------------------------------------------------------------- /data/point_test.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | 4 | from data import read_csv 5 | from data import point 6 | 7 | 8 | class PointTypeTest(unittest.TestCase): 9 | 10 | def testStr(self): 11 | point_type = point.PointType( 12 | city_tours=None, 13 | landmarks=None, 14 | nature=None, 15 | museums=None, 16 | shopping=None, 17 | dining=None) 18 | self.assertEqual('No point type', '%s' % point_type) 19 | 20 | point_type = point.PointType( 21 | city_tours=None, 22 | landmarks=30, 23 | nature=None, 24 | museums=None, 25 | shopping=50, 26 | dining=50) 27 | self.assertEqual('Dining (50), Shopping (50), Landmarks (30)', 28 | '%s' % point_type) 29 | 30 | class AgeGroupTest(unittest.TestCase): 31 | 32 | def testStr(self): 33 | age_group = point.AgeGroup( 34 | senior=None, 35 | adult=None, 36 | junior=None, 37 | child=None, 38 | toddlers=None) 39 | self.assertEqual('No age group', '%s' % age_group) 40 | 41 | age_group = point.AgeGroup( 42 | senior=50, 43 | adult=50, 44 | junior=30, 45 | child=None, 46 | toddlers=None) 47 | self.assertEqual('Adult (50), Senior (50), Junior (30)', 48 | '%s' % age_group) 49 | 50 | 51 | class PointTest(unittest.TestCase): 52 | 53 | def testStr(self): 54 | points = read_csv.ReadCSVToDict(os.path.join('data', 'test_sf_1.csv')) 55 | 56 | ferry_building_str_actual = '%s' % points['Ferry Building'] 57 | ferry_building_str_expected = """Name: "Ferry Building" 58 | Coordinates Starts: 37.7955:-122.3937 59 | Coordinates Ends: 37.7955:-122.3937 60 | Operating Hours: 09:00:00 - 18:00:00 61 | Duration: 1.00 62 | Popularity: 80 63 | Point type: Landmarks (100) 64 | Age group: Adult (90), Senior (90), Child (70), Junior (40) 65 | Price: 0.00 66 | Parking: 0 67 | Eating: 100 68 | """ 69 | self.assertEqual(ferry_building_str_expected, ferry_building_str_actual) 70 | 71 | golden_gate_bridge_str_actual = '%s' % points['Golden Gate Bridge'] 72 | ferry_building_str_expected = """Name: "Golden Gate Bridge" 73 | Coordinates Starts: 37.8197:-122.4786 74 | Coordinates Ends: 37.8197:-122.4786 75 | Operating Hours: 24/7 76 | Duration: 0.50 77 | Popularity: 100 78 | Point type: Landmarks (100), City Tours (50) 79 | Age group: Adult (90), Child (70), Junior (70), Senior (70) 80 | Price: 0.00 81 | Parking: 50 82 | Eating: 0 83 | """ 84 | self.assertEqual(ferry_building_str_expected, golden_gate_bridge_str_actual) 85 | 86 | 87 | if __name__ == '__main__': 88 | unittest.main() 89 | 90 | -------------------------------------------------------------------------------- /data/read_csv.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | import datetime 3 | import time 4 | 5 | import numpy as np 6 | import pandas as pd 7 | 8 | from data import point as point_ 9 | 10 | 11 | lat_long_delimeter = ',' 12 | 13 | 14 | # NOTE(igushev): This module extracts empty cells as None. 15 | def __IsNone(input_): 16 | if not input_: 17 | return True 18 | if isinstance(input_, str): # and input_ 19 | return False 20 | if np.isnan(input_): 21 | return True 22 | return False 23 | 24 | 25 | def ExtractString(input_str): 26 | if __IsNone(input_str): 27 | return None 28 | return str(input_str.strip()) 29 | 30 | 31 | def ExtractFloat(input_float): 32 | if __IsNone(input_float): 33 | return None 34 | return float(input_float) 35 | 36 | 37 | def ExtractInt(input_int): 38 | if __IsNone(input_int): 39 | return None 40 | return int(input_int) 41 | 42 | 43 | def __CoordinateStrToFloat(input_str, pos_suffix, neg_suffix): 44 | sign = 1 45 | if input_str.endswith(pos_suffix): 46 | input_str = input_str[:-len(pos_suffix)] 47 | elif input_str.endswith(neg_suffix): 48 | sign = -1 49 | input_str = input_str[:-len(neg_suffix)] 50 | return sign * float(input_str) 51 | 52 | 53 | def ExtractCoordinates(coordinates_str): 54 | coordinates_str = ExtractString(coordinates_str) 55 | if __IsNone(coordinates_str): 56 | return None 57 | latitude_str, longitude_str = coordinates_str.split(lat_long_delimeter) 58 | latitude_str = ExtractString(latitude_str) 59 | longitude_str = ExtractString(longitude_str) 60 | if __IsNone(latitude_str) or __IsNone(longitude_str): 61 | return None 62 | latitude = __CoordinateStrToFloat(latitude_str, 'N', 'S') 63 | longitude = __CoordinateStrToFloat(longitude_str, 'E', 'W') 64 | return point_.Coordinates(latitude, longitude) 65 | 66 | 67 | def __HoursStrToTime(input_str): 68 | input_time_struct = time.strptime(input_str, '%H:%M:%S') 69 | return datetime.time( 70 | input_time_struct.tm_hour, input_time_struct.tm_min, 71 | input_time_struct.tm_sec) 72 | 73 | 74 | def ExtractOperatingHours(opens_str, closes_str): 75 | opens_str = ExtractString(opens_str) 76 | closes_str = ExtractString(closes_str) 77 | if __IsNone(opens_str) or __IsNone(closes_str): 78 | return None 79 | opens = __HoursStrToTime(opens_str) 80 | closes = __HoursStrToTime(closes_str) 81 | return point_.OperatingHours(opens, closes) 82 | 83 | 84 | def ReadCSV(csv_filepath): 85 | """Read CSV file to list of Points.""" 86 | points_df = pd.read_csv(csv_filepath) 87 | points = [] 88 | for _, point_series in points_df.iterrows(): 89 | points.append(point_.Point( 90 | name=ExtractString(point_series['Name']), 91 | coordinates_starts=ExtractCoordinates(point_series['CoordinatesStarts']), 92 | coordinates_ends=ExtractCoordinates(point_series['CoordinatesEnds']), 93 | operating_hours=ExtractOperatingHours( 94 | point_series['OperatingHoursOpens'], 95 | point_series['OperatingHoursCloses']), 96 | duration=ExtractFloat(point_series['Duration']), 97 | popularity=ExtractInt(point_series['Popularity']), 98 | point_type=point_.PointType( 99 | city_tours=ExtractInt(point_series['City Tours']), 100 | landmarks=ExtractInt(point_series['Landmarks']), 101 | nature=ExtractInt(point_series['Nature']), 102 | museums=ExtractInt(point_series['Museums']), 103 | shopping=ExtractInt(point_series['Shopping']), 104 | dining=ExtractInt(point_series['Dining'])), 105 | age_group=point_.AgeGroup( 106 | senior=ExtractInt(point_series['Senior']), 107 | adult=ExtractInt(point_series['Adult']), 108 | junior=ExtractInt(point_series['Junior']), 109 | child=ExtractInt(point_series['Child']), 110 | toddlers=ExtractInt(point_series['Toddlers'])), 111 | price=ExtractFloat(point_series['Price']), 112 | parking=ExtractInt(point_series['Parking']), 113 | eating=ExtractInt(point_series['Eating']))) 114 | 115 | return points 116 | 117 | 118 | def ReadCSVToDict(csv_filepath): 119 | """Read CSV file to dictionary of name of a Point to the Point.""" 120 | points = ReadCSV(csv_filepath) 121 | points_dict = OrderedDict() 122 | for point in points: 123 | points_dict[point.name] = point 124 | return points_dict 125 | -------------------------------------------------------------------------------- /data/read_csv_test.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import tempfile 3 | import unittest 4 | 5 | from data import read_csv 6 | from data import point 7 | 8 | 9 | class ReadCSVTest(unittest.TestCase): 10 | 11 | def testExtractCoordinatesGeneral(self): 12 | ferry_building_coordinates = read_csv.ExtractCoordinates('37.7955N, 122.3937W') 13 | self.assertEqual(point.Coordinates(37.7955, -122.3937), 14 | ferry_building_coordinates) 15 | 16 | ferry_building_coordinates = read_csv.ExtractCoordinates('37.7955, -122.3937') 17 | self.assertEqual(point.Coordinates(37.7955, -122.3937), 18 | ferry_building_coordinates) 19 | 20 | kremlin_coordinates = read_csv.ExtractCoordinates('55.7517N, 37.6178E') 21 | self.assertEqual(point.Coordinates(55.7517, 37.6178), kremlin_coordinates) 22 | 23 | kremlin_coordinates = read_csv.ExtractCoordinates('55.7517, 37.6178') 24 | self.assertEqual(point.Coordinates(55.7517, 37.6178), kremlin_coordinates) 25 | 26 | christ_the_redeemer_coordinates = read_csv.ExtractCoordinates('22.9519S, 43.2106W') 27 | self.assertEqual(point.Coordinates(-22.9519, -43.2106), 28 | christ_the_redeemer_coordinates) 29 | 30 | christ_the_redeemer_coordinates = read_csv.ExtractCoordinates('-22.9519, -43.2106') 31 | self.assertEqual(point.Coordinates(-22.9519, -43.2106), 32 | christ_the_redeemer_coordinates) 33 | 34 | self.assertEqual(None, read_csv.ExtractCoordinates('')) 35 | 36 | def testExtractOperatingHoursGeneral(self): 37 | de_young_museum_operating_hours = ( 38 | read_csv.ExtractOperatingHours('9:30:00', '17:15:00')) 39 | self.assertEqual(point.OperatingHours(datetime.time(9, 30, 0), 40 | datetime.time(17, 15, 0)), 41 | de_young_museum_operating_hours) 42 | 43 | def testReadCSVGeneral(self): 44 | s = str() 45 | s += 'ID,Name,CoordinatesStarts,CoordinatesEnds,OperatingHoursOpens,OperatingHoursCloses,Duration,Popularity,City Tours,Landmarks,Nature,Museums,Shopping,Dining,Senior,Adult,Junior,Child,Toddlers,Price,Parking,Eating\n' 46 | s += '1,Ferry Building,"37.7955N, 122.3937W",,09:00:00,18:00:00,1,80,,100,,,,,90,90,40,70,,,,100\n' 47 | s += '2,Pier 39,"37.8100N, 122.4104W",,10:00:00,22:00:00,3,80,,100,,,30,60,70,70,70,90,,,,100\n' 48 | csv_filepath = tempfile.mktemp() 49 | with open(csv_filepath, 'w') as csv_file: 50 | csv_file.write(s) 51 | 52 | pier_39 = point.Point( 53 | name='Pier 39', 54 | coordinates_starts=point.Coordinates(37.8100, -122.4104), 55 | coordinates_ends=None, 56 | operating_hours=point.OperatingHours(datetime.time(10, 0, 0), datetime.time(22, 0, 0)), 57 | duration=3., 58 | popularity=80, 59 | point_type=point.PointType( 60 | city_tours=None, 61 | landmarks=100, 62 | nature=None, 63 | museums=None, 64 | shopping=30, 65 | dining=60), 66 | age_group=point.AgeGroup( 67 | senior=70, 68 | adult=70, 69 | junior=70, 70 | child=90, 71 | toddlers=None), 72 | price=None, 73 | parking=None, 74 | eating=100) 75 | 76 | points = read_csv.ReadCSV(csv_filepath) 77 | self.assertEqual(2, len(points)) 78 | self.assertEqual(pier_39, points[1]) 79 | points = read_csv.ReadCSVToDict(csv_filepath) 80 | self.assertEqual(2, len(points)) 81 | self.assertEqual(set(['Ferry Building', 'Pier 39']), set(points.keys())) 82 | self.assertEqual(pier_39, points['Pier 39']) 83 | 84 | 85 | if __name__ == '__main__': 86 | unittest.main() 87 | 88 | -------------------------------------------------------------------------------- /data/runner_util.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from data import city_visit 4 | from data import point 5 | 6 | 7 | def GetCityVisitParameters(visit_location, day_visit_parameterss): 8 | parameters_point_types = point.PointType( 9 | city_tours=90, 10 | landmarks=90, 11 | nature=10, 12 | museums=10, 13 | shopping=50, 14 | dining=50) 15 | 16 | parameters_age_groups = point.AgeGroup( 17 | senior=None, 18 | adult=90, 19 | junior=None, 20 | child=None, 21 | toddlers=10) 22 | 23 | return city_visit.CityVisitParameters( 24 | visit_location=visit_location, 25 | day_visit_parameterss=day_visit_parameterss, 26 | point_type=parameters_point_types, 27 | age_group=parameters_age_groups) 28 | 29 | 30 | def GetDayVisitParameterss(start_end_coordinates, first_day, last_day): 31 | def GetDayVisitParameters(day): 32 | return city_visit.DayVisitParameters( 33 | start_datetime=datetime.datetime(2015, 7, day, 10, 0, 0), 34 | end_datetime=datetime.datetime(2015, 7, day, 19, 0, 0), 35 | lunch_start_datetime=datetime.datetime(2015, 7, day, 14, 0, 0), 36 | lunch_hours=1., 37 | start_coordinates=start_end_coordinates, 38 | end_coordinates=start_end_coordinates) 39 | 40 | return [GetDayVisitParameters(day) for day in range(first_day, last_day)] -------------------------------------------------------------------------------- /data/test_sf_1.csv: -------------------------------------------------------------------------------- 1 | ID,Name,CoordinatesStarts,CoordinatesEnds,OperatingHoursOpens,OperatingHoursCloses,Duration,Popularity,City Tours,Landmarks,Nature,Museums,Shopping,Dining,Senior,Adult,Junior,Child,Toddlers,Price,Parking,Eating 2 | 1,Ferry Building,"37.7955N, 122.3937W",,09:00:00,18:00:00,1,80,,100,,,,,90,90,40,70,,,,100 3 | 2,Pier 39,"37.8100N, 122.4104W",,10:00:00,22:00:00,3,80,,100,,,30,60,70,70,70,90,,,,100 4 | 3,Golden Gate Bridge,"37.8197N, 122.4786W",,,,0.5,100,50,100,,,,,70,90,70,70,,,50, 5 | 4,Union Square,"37.7881N, 122.4075W",,,,1,70,20,60,,,70,70,50,70,90,50,,,80,100 6 | 5,Lombard Street,"37.8019N, 122.4189W",,,,0.5,90,20,90,,,,,50,50,50,30,,,, 7 | 6,Coit Tower,"37.8025N, 122.4058W",,10:00:00,18:00:00,1,70,50,90,,,,,50,70,50,70,,8,20, 8 | 7,Att Park,"37.7786N, 122.3892W",,,,0.5,30,,10,,,,,10,30,50,50,,,, 9 | 8,San Francisco-Oakland Bay Bridge,"37.8181N, 122.3467W",,,,0.5,50,,90,,,,,70,90,70,70,,,, 10 | 9,Alcatraz Island,"37.8267N, 122.4233W",,,,3,100,,80,,30,,,70,70,50,50,,30,20,30 11 | 10,Cable Car,"37.7937N, 122.3961W","37.7850N, 122.4072W",,,0.25,100,,80,,30,,,70,70,70,90,,6,, 12 | 11,Golden Gate Park,"37.7697N, 122.4769W",,,,4,30,,20,80,,,,50,70,30,30,,,50,20 13 | 12,De Young Museum,"37.7713N, 122.4686W",,9:30:00,17:15:00,4,30,,,,80,,,50,70,50,70,,20,30,40 14 | 13,China Town,"37.7947N, 122.4072W",,,,2,50,,70,,,10,30,50,50,50,50,,,,100 15 | 14,Baker Beach,"37.7932N, 122.4840W",,,,1,30,,50,70,,,,30,70,30,30,,,80,70 16 | 15,Sutro Baths,"37.7800N, 122.5136W",,,,1,20,,50,50,30,,,30,70,70,50,,,50,20 17 | 16,Presidio,"37.7981N, 122.4658W",,,,3,20,,,80,,,,50,70,50,50,,,, 18 | 17,Palace of Fine Arts,"37.8028N, 122.4483W",,,,1,40,,80,,20,,,70,70,30,30,,,30, 19 | 18,Legion of Honor,"37.7839N, 122.5011W",,,,1,30,,,,80,,,70,70,50,30,,20,50,30 20 | 19,San Francisco City Hall,"37.7792N, 122.4191W",,08:00:00,20:00:00,1.5,30,,30,,,,,70,50,50,70,,,, 21 | 20,Twin Peaks,"37.7516N, 122.4477W",,,,0.5,20,50,50,,,,,30,70,70,30,,,50, 22 | 21,Alamo Square,"37.7764N, 122.4347W",,,,1,70,50,50,,,,,50,70,70,50,,,,10 23 | 22,Cable Car Museum,"37.7956N, 122.4075W",,10:00:00,18:00:00,2,10,,30,,70,,,50,50,50,50,,,, 24 | -------------------------------------------------------------------------------- /data/test_util.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from data import database_connection 4 | from data import read_csv 5 | 6 | 7 | def GetPointsInput(csv_dirpath, csv_filename): 8 | points_dict = read_csv.ReadCSVToDict(os.path.join(csv_dirpath, csv_filename)) 9 | return points_dict 10 | 11 | 12 | def GetPointsKeys(keys_dirpath, keys_filename): 13 | test_points_filepath = os.path.join(keys_dirpath, keys_filename) 14 | lines = open(test_points_filepath).readlines() 15 | lines = [line.strip() for line in lines] 16 | keys = [line for line in lines if line and not line.startswith('#')] 17 | return keys 18 | 19 | 20 | def FilterAndSortByKeys(points_dict, keys): 21 | return [points_dict[key] for key in keys] 22 | -------------------------------------------------------------------------------- /finder/city_visit_finder.py: -------------------------------------------------------------------------------- 1 | from ranker import points_ranker as points_ranker_ 2 | from router import city_visit_router as city_visit_router_ 3 | from data import city_visit as city_visit_ 4 | from data import point 5 | from data import database_connection as database_connection_ 6 | 7 | 8 | class CityVisitFinderInterface(object): 9 | """Abstract class which finds CityVisit.""" 10 | 11 | def FindCityVisit(self, city_visit_parameters, 12 | city_visit_accumulator_generator): 13 | """Find CityVisit with appropriate points by given city_visit_parameters.""" 14 | raise NotImplemented() 15 | 16 | 17 | class CityVisitFinder(CityVisitFinderInterface): 18 | """Finds CityVisit by getting points, ranking them and routing.""" 19 | 20 | def __init__(self, points_ranker, city_visit_router): 21 | assert isinstance(points_ranker, points_ranker_.PointsRankerInterface) 22 | assert isinstance(city_visit_router, city_visit_router_.CityVisitRouterInterface) 23 | 24 | self.points_ranker = points_ranker 25 | self.city_visit_router = city_visit_router 26 | 27 | def FindCityVisit(self, points_input, city_visit_parameters, city_visit_accumulator_generator): 28 | for point_input in points_input: 29 | assert isinstance(point_input, point.PointInterface) 30 | assert isinstance(city_visit_parameters, city_visit_.CityVisitParametersInterface) 31 | 32 | points_ranked = self.points_ranker.RankPoints( 33 | points_input, city_visit_parameters) 34 | for point_ranked in points_ranked: 35 | assert isinstance(point_ranked, point.PointInterface) 36 | 37 | city_visit, points_left = self.city_visit_router.RouteCityVisit( 38 | points_ranked, city_visit_parameters.day_visit_parameterss, 39 | city_visit_accumulator_generator) 40 | assert isinstance(city_visit, city_visit_.CityVisitInterface) 41 | for point_left in points_left: 42 | assert isinstance(point_left, point.PointInterface) 43 | 44 | return city_visit 45 | -------------------------------------------------------------------------------- /finder/city_visit_finder_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from finder import city_visit_finder as city_visit_finder_ 4 | from ranker import points_ranker as points_ranker_ 5 | from router import city_visit_router as city_visit_router_ 6 | from data import city_visit as city_visit_ 7 | from data import point 8 | from data import database_connection as database_connection_ 9 | from router import city_visit_accumulator 10 | 11 | 12 | class MockCityVisitAccumulatorGenerator(city_visit_accumulator.CityVisitAccumulatorGeneratorInterface): 13 | pass 14 | 15 | 16 | class MockVisitLocation(city_visit_.VisitLocationInterface): 17 | pass 18 | 19 | 20 | class MockDayVisitParameters(city_visit_.DayVisitParametersInterface): 21 | pass 22 | 23 | 24 | class MockPointType(point.PointTypeInterface): 25 | pass 26 | 27 | 28 | class MockAgeGroup(point.AgeGroupInterface): 29 | pass 30 | 31 | 32 | class MockPoint(point.PointInterface): 33 | pass 34 | 35 | 36 | class MockCityVisit(city_visit_.CityVisitInterface): 37 | pass 38 | 39 | 40 | class MockDatabaseConnection(database_connection_.DatabaseConnectionInterface): 41 | 42 | def __init__(self, test_obj, visit_location_expected, points_input): 43 | self.test_obj = test_obj 44 | self.visit_location_expected = visit_location_expected 45 | self.points_input = points_input 46 | 47 | def GetPoints(self, visit_location): 48 | self.test_obj.assertTrue(visit_location is self.visit_location_expected) 49 | return self.points_input 50 | 51 | 52 | class MockPointsRanker(points_ranker_.PointsRankerInterface): 53 | 54 | def __init__(self, test_obj, points_input_expected, 55 | city_visit_parameters_expected, points_ranked): 56 | self.test_obj = test_obj 57 | self.points_input_expected = points_input_expected 58 | self.city_visit_parameters_expected = city_visit_parameters_expected 59 | self.points_ranked = points_ranked 60 | 61 | def RankPoints(self, points, city_visit_parameters): 62 | self.test_obj.assertTrue(points is self.points_input_expected) 63 | self.test_obj.assertTrue( 64 | city_visit_parameters is self.city_visit_parameters_expected) 65 | return self.points_ranked 66 | 67 | 68 | class MockCityVisitRouter(city_visit_router_.CityVisitRouterInterface): 69 | 70 | def __init__(self, test_obj, points_ranked_expected, 71 | day_visit_parameterss_expected, 72 | city_visit_accumulator_generator_expected, city_visit, points_left): 73 | self.test_obj = test_obj 74 | self.points_ranked_expected = points_ranked_expected 75 | self.day_visit_parameterss_expected = day_visit_parameterss_expected 76 | self.city_visit_accumulator_generator_expected = ( 77 | city_visit_accumulator_generator_expected) 78 | self.city_visit = city_visit 79 | self.points_left = points_left 80 | 81 | def RouteCityVisit(self, points, day_visit_parameterss, 82 | city_visit_accumulator_generator): 83 | self.test_obj.assertTrue(points is self.points_ranked_expected) 84 | self.test_obj.assertTrue( 85 | day_visit_parameterss is self.day_visit_parameterss_expected) 86 | self.test_obj.assertTrue( 87 | city_visit_accumulator_generator is 88 | self.city_visit_accumulator_generator_expected) 89 | return self.city_visit, self.points_left 90 | 91 | 92 | class CityVisitFinderTest(unittest.TestCase): 93 | 94 | def testGeneral(self): 95 | visit_location = MockVisitLocation() 96 | day_visit_parameterss=[MockDayVisitParameters()] 97 | 98 | city_visit_parameters = city_visit_.CityVisitParameters( 99 | visit_location=visit_location, 100 | day_visit_parameterss=day_visit_parameterss, 101 | point_type=MockPointType(), 102 | age_group=MockAgeGroup()) 103 | 104 | city_visit_accumulator_generator = MockCityVisitAccumulatorGenerator() 105 | 106 | points_input = [MockPoint(), MockPoint()] 107 | points_ranked = [MockPoint(), MockPoint()] 108 | points_left = [MockPoint(), MockPoint()] 109 | city_visit = MockCityVisit() 110 | 111 | points_ranker = MockPointsRanker( 112 | self, points_input, city_visit_parameters, points_ranked) 113 | city_visit_router = MockCityVisitRouter( 114 | self, points_ranked, day_visit_parameterss, 115 | city_visit_accumulator_generator, city_visit, points_left) 116 | 117 | city_visit_finder = city_visit_finder_.CityVisitFinder( 118 | points_ranker=points_ranker, 119 | city_visit_router=city_visit_router) 120 | 121 | city_visit_actual = ( 122 | city_visit_finder.FindCityVisit( 123 | points_input, city_visit_parameters, city_visit_accumulator_generator)) 124 | 125 | self.assertTrue(city_visit_actual is city_visit) 126 | 127 | 128 | if __name__ == '__main__': 129 | unittest.main() 130 | -------------------------------------------------------------------------------- /finder/runner.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import os 3 | 4 | from config import config as config_ 5 | 6 | 7 | class CityVisitFinderRunner(object): 8 | 9 | def __init__(self): 10 | config = config_.GetConfig(os.path.join('config', 'runner.config')) 11 | self.city_visit_finder = config_.GetCityVisitFinder(config) 12 | self.city_visit_accumulator_generator = config_.GetCityVisitAccumulatorGenerator(config) 13 | 14 | def Run(self, points_input, city_visit_parameters): 15 | start = datetime.datetime.now() 16 | 17 | city_visit = self.city_visit_finder.FindCityVisit( 18 | points_input, city_visit_parameters, self.city_visit_accumulator_generator) 19 | 20 | print('Input points: %s' % ', '.join(point.name for point in points_input)) 21 | print('Your schedule:') 22 | print(city_visit) 23 | print('Elapsed time %s' % (datetime.datetime.now() - start)) 24 | -------------------------------------------------------------------------------- /finder/runner_doc.py: -------------------------------------------------------------------------------- 1 | import os 2 | from config import config as config_ 3 | config = config_.GetConfig(os.path.join('config', 'runner.config')) 4 | 5 | import datetime 6 | from data import city_visit 7 | from data import point 8 | 9 | start_end_coordinates = point.Coordinates(40.763582, -73.988470) 10 | 11 | day1 = city_visit.DayVisitParameters( 12 | start_datetime=datetime.datetime(2019, 7, 1, 10, 0, 0), 13 | end_datetime=datetime.datetime(2019, 7, 1, 19, 0, 0), 14 | lunch_start_datetime=datetime.datetime(2019, 7, 1, 14, 0, 0), 15 | lunch_hours=1., 16 | start_coordinates=start_end_coordinates, 17 | end_coordinates=start_end_coordinates) 18 | 19 | day2 = city_visit.DayVisitParameters( 20 | start_datetime=datetime.datetime(2019, 7, 2, 10, 0, 0), 21 | end_datetime=datetime.datetime(2019, 7, 2, 17, 0, 0), 22 | lunch_start_datetime=datetime.datetime(2019, 7, 1, 14, 0, 0), 23 | lunch_hours=1., 24 | start_coordinates=start_end_coordinates, 25 | end_coordinates=start_end_coordinates) 26 | 27 | from data import city_visit 28 | from data import point 29 | 30 | visit_location = city_visit.VisitLocation('New York City') 31 | 32 | parameters_point_types = point.PointType( 33 | city_tours=90, 34 | landmarks=90, 35 | nature=10, 36 | museums=10, 37 | shopping=50, 38 | dining=50) 39 | 40 | parameters_age_groups = point.AgeGroup( 41 | senior=None, 42 | adult=90, 43 | junior=None, 44 | child=None, 45 | toddlers=10) 46 | 47 | city_visit_parameters = city_visit.CityVisitParameters( 48 | visit_location=visit_location, 49 | day_visit_parameterss=[day1, day2], 50 | point_type=parameters_point_types, 51 | age_group=parameters_age_groups) 52 | 53 | from data import test_util as point_test_util 54 | points_input = list(point_test_util.GetPointsInput('data', 'test_nyc_1.csv').values()) 55 | 56 | from config import config as config_ 57 | city_visit_finder = config_.GetCityVisitFinder(config) 58 | city_visit_accumulator_generator = config_.GetCityVisitAccumulatorGenerator(config) 59 | city_visit = city_visit_finder.FindCityVisit(points_input, city_visit_parameters, city_visit_accumulator_generator) 60 | 61 | print('Your schedule:') 62 | print(city_visit) 63 | -------------------------------------------------------------------------------- /finder/runner_nyc_1.py: -------------------------------------------------------------------------------- 1 | from data import city_visit 2 | from data import point 3 | from data import runner_util 4 | from data import test_util as point_test_util 5 | from finder import runner as finder_runner 6 | 7 | 8 | def main(): 9 | points_input = list(point_test_util.GetPointsInput('data', 'test_nyc_1.csv').values()) 10 | city_visit_finder_runner = finder_runner.CityVisitFinderRunner() 11 | visit_location = city_visit.VisitLocation('New York City') 12 | # 746 Ninth Ave, New York, NY 10019. 13 | start_end_coordinates = point.Coordinates(40.763582, -73.988470) 14 | first_day, last_day = 13, 16 15 | day_visit_parameterss = runner_util.GetDayVisitParameterss(start_end_coordinates, first_day, last_day) 16 | city_visit_parameters = runner_util.GetCityVisitParameters(visit_location, day_visit_parameterss) 17 | 18 | city_visit_finder_runner.Run(points_input, city_visit_parameters) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /finder/runner_sf_1.py: -------------------------------------------------------------------------------- 1 | from data import city_visit 2 | from data import runner_util 3 | from data import test_util as point_test_util 4 | from finder import runner as finder_runner 5 | 6 | 7 | def main(): 8 | points_dict = point_test_util.GetPointsInput('data', 'test_sf_1.csv') 9 | points_input = list(points_dict.values()) 10 | city_visit_finder_runner = finder_runner.CityVisitFinderRunner() 11 | visit_location = city_visit.VisitLocation('San Francisco') 12 | start_end_coordinates = points_dict['Union Square'].coordinates_starts 13 | first_day, last_day = 1, 4 14 | day_visit_parameterss = runner_util.GetDayVisitParameterss(start_end_coordinates, first_day, last_day) 15 | city_visit_parameters = runner_util.GetCityVisitParameters(visit_location, day_visit_parameterss) 16 | 17 | city_visit_finder_runner.Run(points_input, city_visit_parameters) 18 | 19 | 20 | if __name__ == '__main__': 21 | main() 22 | -------------------------------------------------------------------------------- /finder/runner_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from data import city_visit 4 | from data import runner_util 5 | from data import test_util as point_test_util 6 | from finder import runner as finder_runner 7 | 8 | 9 | class CityVisitFinderRunnerTest(unittest.TestCase): 10 | 11 | def testGeneral(self): 12 | points_dict = point_test_util.GetPointsInput('data', 'test_sf_1.csv') 13 | points_input = list(points_dict.values()) 14 | city_visit_finder_runner = finder_runner.CityVisitFinderRunner() 15 | visit_location = city_visit.VisitLocation('San Francisco') 16 | start_end_coordinates = points_dict['Union Square'].coordinates_starts 17 | first_day, last_day = 1, 2 18 | day_visit_parameterss = runner_util.GetDayVisitParameterss(start_end_coordinates, first_day, last_day) 19 | city_visit_parameters = runner_util.GetCityVisitParameters(visit_location, day_visit_parameterss) 20 | 21 | city_visit_finder_runner.Run(points_input, city_visit_parameters) 22 | 23 | 24 | if __name__ == '__main__': 25 | unittest.main() 26 | -------------------------------------------------------------------------------- /ranker/age_group_rank_adjuster.py: -------------------------------------------------------------------------------- 1 | from data import city_visit 2 | from ranker import rank_adjuster_interface 3 | 4 | 5 | # NOTE(igushev): This class works only with AgeGroup implementation of 6 | # AgeGroupInterface. 7 | class AgeGroupRankAdjuster(rank_adjuster_interface.RankAdjusterInterface): 8 | """Adjusts rank of points by age group.""" 9 | 10 | @staticmethod 11 | def _PointScoreMult(point_names_age_groups, parameters_names_age_groups): 12 | return ( 13 | sum(max(age_group, 1) * 14 | max(point_names_age_groups[name], 1) 15 | for name, age_group in parameters_names_age_groups.items()) / 16 | float(100 * 100) / 17 | len(parameters_names_age_groups)) 18 | 19 | def AdjustRank(self, score_points, city_visit_parameters): 20 | for score_point in score_points: 21 | assert isinstance(score_point, rank_adjuster_interface.ScorePoint) 22 | assert isinstance(city_visit_parameters, city_visit.CityVisitParametersInterface) 23 | 24 | parameters_names_age_groups = ( 25 | city_visit_parameters.age_group.GetNamesAgeGroups()) 26 | result_score_points = [] 27 | for score, point in score_points: 28 | point_names_age_groups = point.age_group.GetNamesAgeGroups() 29 | point_score_mult = AgeGroupRankAdjuster._PointScoreMult( 30 | point_names_age_groups, parameters_names_age_groups) 31 | result_score_points.append(rank_adjuster_interface.ScorePoint(score * point_score_mult, point)) 32 | return result_score_points 33 | -------------------------------------------------------------------------------- /ranker/age_group_rank_adjuster_test.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | 4 | from data import city_visit 5 | from data import point 6 | from data import read_csv 7 | from ranker import age_group_rank_adjuster 8 | from ranker import rank_adjuster_interface 9 | from ranker import test_util 10 | 11 | 12 | class AgeGroupRankAdjusterTest(test_util.RankAdjusterTestUtils): 13 | 14 | def setUp(self): 15 | self.points = read_csv.ReadCSVToDict(os.path.join('data', 'test_sf_1.csv')) 16 | self.age_group_rank_adjuster = age_group_rank_adjuster.AgeGroupRankAdjuster() 17 | super(AgeGroupRankAdjusterTest, self).setUp() 18 | 19 | def testGeneral(self): 20 | score_points_input = [ 21 | rank_adjuster_interface.ScorePoint(100., self.points['Ferry Building']), 22 | rank_adjuster_interface.ScorePoint(100., self.points['Cable Car']), 23 | rank_adjuster_interface.ScorePoint(100., self.points['Twin Peaks'])] 24 | 25 | parameters_age_groups = point.AgeGroup( 26 | senior=None, 27 | adult=90, 28 | junior=None, 29 | child=None, 30 | toddlers=10) 31 | 32 | city_visit_parameters = city_visit.CityVisitParameters( 33 | test_util.MockVisitLocation(), 34 | day_visit_parameterss=[test_util.MockDayVisitParameters()], 35 | point_type=test_util.MockPointType(), 36 | age_group=parameters_age_groups) 37 | 38 | score_points_actual = ( 39 | self.age_group_rank_adjuster.AdjustRank( 40 | score_points_input, city_visit_parameters)) 41 | 42 | score_points_expected = [ 43 | rank_adjuster_interface.ScorePoint(16.62, self.points['Ferry Building']), 44 | rank_adjuster_interface.ScorePoint(13.08, self.points['Cable Car']), 45 | rank_adjuster_interface.ScorePoint(12.88, self.points['Twin Peaks'])] 46 | 47 | self.assertScorePointsEqual( 48 | score_points_expected, score_points_actual, places=3) 49 | 50 | 51 | if __name__ == '__main__': 52 | unittest.main() 53 | -------------------------------------------------------------------------------- /ranker/point_type_rank_adjuster.py: -------------------------------------------------------------------------------- 1 | from data import city_visit 2 | from ranker import rank_adjuster_interface 3 | 4 | 5 | # NOTE(igushev): This class works only with PointType implementation of 6 | # PointTypeInterface. 7 | class PointTypeRankAdjuster(rank_adjuster_interface.RankAdjusterInterface): 8 | """Adjusts rank of points by point types.""" 9 | 10 | @staticmethod 11 | def _PointScoreMult(point_names_point_types, parameters_names_point_types): 12 | return ( 13 | sum(max(point_type, 1) * 14 | max(point_names_point_types[name], 1) 15 | for name, point_type in parameters_names_point_types.items()) / 16 | float(100 * 100) / 17 | len(parameters_names_point_types)) 18 | 19 | def AdjustRank(self, score_points, city_visit_parameters): 20 | for score_point in score_points: 21 | assert isinstance(score_point, rank_adjuster_interface.ScorePoint) 22 | assert isinstance(city_visit_parameters, city_visit.CityVisitParametersInterface) 23 | 24 | parameters_names_point_types = ( 25 | city_visit_parameters.point_type.GetNamesPointTypes()) 26 | result_score_points = [] 27 | for score, point in score_points: 28 | point_names_point_types = point.point_type.GetNamesPointTypes() 29 | point_score_mult = PointTypeRankAdjuster._PointScoreMult( 30 | point_names_point_types, parameters_names_point_types) 31 | result_score_points.append(rank_adjuster_interface.ScorePoint(score * point_score_mult, point)) 32 | return result_score_points 33 | -------------------------------------------------------------------------------- /ranker/point_type_rank_adjuster_test.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | 4 | from data import city_visit 5 | from data import point 6 | from data import read_csv 7 | from ranker import point_type_rank_adjuster 8 | from ranker import rank_adjuster_interface 9 | from ranker import test_util 10 | 11 | 12 | class PointTypeRankAdjusterTest(test_util.RankAdjusterTestUtils): 13 | 14 | def setUp(self): 15 | self.points = read_csv.ReadCSVToDict(os.path.join('data', 'test_sf_1.csv')) 16 | self.point_type_rank_adjuster = point_type_rank_adjuster.PointTypeRankAdjuster() 17 | super(PointTypeRankAdjusterTest, self).setUp() 18 | 19 | def testGeneral(self): 20 | score_points_input = [ 21 | rank_adjuster_interface.ScorePoint(100., self.points['Pier 39']), 22 | rank_adjuster_interface.ScorePoint(100., self.points['Golden Gate Bridge']), 23 | rank_adjuster_interface.ScorePoint(100., self.points['Sutro Baths'])] 24 | 25 | parameters_point_types = point.PointType( 26 | city_tours=90, 27 | landmarks=90, 28 | nature=10, 29 | museums=10, 30 | shopping=50, 31 | dining=50) 32 | 33 | city_visit_parameters = city_visit.CityVisitParameters( 34 | test_util.MockVisitLocation(), 35 | day_visit_parameterss=[test_util.MockDayVisitParameters()], 36 | point_type=parameters_point_types, 37 | age_group=test_util.MockAgeGroup()) 38 | 39 | score_points_actual = ( 40 | self.point_type_rank_adjuster.AdjustRank( 41 | score_points_input, city_visit_parameters)) 42 | 43 | score_points_expected = [ 44 | rank_adjuster_interface.ScorePoint(22.683, self.points['Pier 39']), 45 | rank_adjuster_interface.ScorePoint(22.7, self.points['Golden Gate Bridge']), 46 | rank_adjuster_interface.ScorePoint(9.15, self.points['Sutro Baths'])] 47 | 48 | self.assertScorePointsEqual( 49 | score_points_expected, score_points_actual, places=3) 50 | 51 | 52 | if __name__ == '__main__': 53 | unittest.main() 54 | -------------------------------------------------------------------------------- /ranker/points_ranker.py: -------------------------------------------------------------------------------- 1 | from data import point as point_ 2 | from data import city_visit 3 | from ranker import rank_adjuster_interface 4 | 5 | 6 | class PointsRankerInterface(object): 7 | """Abstract class which ranks points.""" 8 | 9 | def RankPoints(self, points, city_visit_parameters): 10 | """Rank given points by given city_visit_parameters.""" 11 | raise NotImplemented() 12 | 13 | 14 | class PointsRanker(PointsRankerInterface): 15 | """Ranks points by given RankAdjusters.""" 16 | 17 | def __init__(self, rank_adjusters): 18 | for rank_adjuster in rank_adjusters: 19 | assert isinstance(rank_adjuster, rank_adjuster_interface.RankAdjusterInterface) 20 | 21 | self.rank_adjusters = rank_adjusters 22 | 23 | def RankPoints(self, points, city_visit_parameters): 24 | for point in points: 25 | isinstance(point, point_.PointInterface) 26 | assert isinstance(city_visit_parameters, city_visit.CityVisitParametersInterface) 27 | 28 | score_points = [rank_adjuster_interface.ScorePoint(100., point) for point in points] 29 | for rank_adjuster in self.rank_adjusters: 30 | score_points = rank_adjuster.AdjustRank( 31 | score_points, city_visit_parameters) 32 | score_points_sorted = sorted( 33 | score_points, key=lambda score_point: score_point.Score, reverse=True) 34 | return [point for _, point in score_points_sorted] 35 | -------------------------------------------------------------------------------- /ranker/points_ranker_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from data import point as point_ 4 | from ranker import points_ranker as points_ranker_ 5 | from ranker import rank_adjuster_interface 6 | from ranker import test_util 7 | 8 | 9 | class MockPoint(point_.PointInterface): 10 | pass 11 | 12 | 13 | class MockRankAdjuster(rank_adjuster_interface.RankAdjusterInterface): 14 | 15 | def __init__(self, test_obj, point_score_mults, score_points_expected, 16 | city_visit_parameters_expected, places=None): 17 | self.test_obj = test_obj 18 | self.point_score_mults = point_score_mults 19 | self.score_points_expected = score_points_expected 20 | self.city_visit_parameters_expected = city_visit_parameters_expected 21 | self.places = places 22 | 23 | def AdjustRank(self, score_points, city_visit_parameters): 24 | for ((score_expected, point_expected), 25 | (score, point)) in zip(self.score_points_expected, 26 | score_points): 27 | self.test_obj.assertAlmostEqual(score_expected, score, places=self.places) 28 | self.test_obj.assertTrue(point is point_expected) 29 | self.test_obj.assertTrue( 30 | city_visit_parameters is self.city_visit_parameters_expected) 31 | assert len(self.point_score_mults) == len(score_points) 32 | return [rank_adjuster_interface.ScorePoint(point_score_mult * score, point) 33 | for point_score_mult, (score, point) 34 | in zip(self.point_score_mults, score_points)] 35 | 36 | 37 | class PointsRankerTest(unittest.TestCase): 38 | 39 | def testGeneral(self): 40 | a = MockPoint() 41 | b = MockPoint() 42 | c = MockPoint() 43 | points_input = [a, b, c] 44 | city_visit_parameters = test_util.MockCityVisitParameters() 45 | points_ranker = points_ranker_.PointsRanker([ 46 | MockRankAdjuster( 47 | self, [1.0, 0.9, 0.9], 48 | [rank_adjuster_interface.ScorePoint(100., a), 49 | rank_adjuster_interface.ScorePoint(100., b), 50 | rank_adjuster_interface.ScorePoint(100., c)], 51 | city_visit_parameters), 52 | MockRankAdjuster( 53 | self, [0.7, 0.9, 0.8], 54 | [rank_adjuster_interface.ScorePoint(100., a), 55 | rank_adjuster_interface.ScorePoint(90., b), 56 | rank_adjuster_interface.ScorePoint(90., c)], 57 | city_visit_parameters), 58 | MockRankAdjuster( 59 | self, [0.9, 0.9, 0.9], 60 | [rank_adjuster_interface.ScorePoint(70., a), 61 | rank_adjuster_interface.ScorePoint(81., b), 62 | rank_adjuster_interface.ScorePoint(72., c)], 63 | city_visit_parameters)]) 64 | 65 | points_actual = points_ranker.RankPoints(points_input, city_visit_parameters) 66 | 67 | points_expected = [b, c, a] 68 | self.assertEqual(points_expected, points_actual) 69 | 70 | def testNoPoints(self): 71 | points_input = [] 72 | city_visit_parameters = test_util.MockCityVisitParameters() 73 | points_ranker = points_ranker_.PointsRanker([ 74 | MockRankAdjuster(self, [], [], city_visit_parameters), 75 | MockRankAdjuster(self, [], [], city_visit_parameters), 76 | MockRankAdjuster(self, [], [], city_visit_parameters)]) 77 | 78 | points_actual = points_ranker.RankPoints( 79 | points_input, city_visit_parameters) 80 | 81 | points_expected = points_input 82 | self.assertEqual(points_expected, points_actual) 83 | 84 | def testNoRankAdjusters(self): 85 | a = MockPoint() 86 | b = MockPoint() 87 | c = MockPoint() 88 | points_input = [a, b, c] 89 | city_visit_parameters = test_util.MockCityVisitParameters() 90 | points_ranker = points_ranker_.PointsRanker([]) 91 | 92 | points_actual = points_ranker.RankPoints(points_input, city_visit_parameters) 93 | 94 | points_expected = points_input 95 | self.assertEqual(points_expected, points_actual) 96 | 97 | 98 | 99 | 100 | if __name__ == '__main__': 101 | unittest.main() 102 | -------------------------------------------------------------------------------- /ranker/popularity_rank_adjuster.py: -------------------------------------------------------------------------------- 1 | from data import city_visit 2 | from ranker import rank_adjuster_interface 3 | 4 | 5 | class PopularityRankAdjuster(rank_adjuster_interface.RankAdjusterInterface): 6 | """Adjusts rank of points by popularity.""" 7 | 8 | def AdjustRank(self, score_points, city_visit_parameters): 9 | for score_point in score_points: 10 | assert isinstance(score_point, rank_adjuster_interface.ScorePoint) 11 | assert isinstance(city_visit_parameters, city_visit.CityVisitParametersInterface) 12 | 13 | return [rank_adjuster_interface.ScorePoint(score * point.popularity / float(100), point) 14 | for score, point in score_points] 15 | -------------------------------------------------------------------------------- /ranker/popularity_rank_adjuster_test.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | 4 | from data import read_csv 5 | from ranker import popularity_rank_adjuster 6 | from ranker import rank_adjuster_interface 7 | from ranker import test_util 8 | 9 | 10 | class PopularityRankAdjusterTest(test_util.RankAdjusterTestUtils): 11 | 12 | def setUp(self): 13 | self.points = read_csv.ReadCSVToDict(os.path.join('data', 'test_sf_1.csv')) 14 | self.popularity_rank_adjuster = popularity_rank_adjuster.PopularityRankAdjuster() 15 | super(PopularityRankAdjusterTest, self).setUp() 16 | 17 | def testGeneral(self): 18 | score_points_input = [ 19 | rank_adjuster_interface.ScorePoint(100., self.points['Ferry Building']), 20 | rank_adjuster_interface.ScorePoint(100., self.points['Golden Gate Bridge']), 21 | rank_adjuster_interface.ScorePoint(100., self.points['Sutro Baths'])] 22 | 23 | score_points_actual = ( 24 | self.popularity_rank_adjuster.AdjustRank( 25 | score_points_input, test_util.MockCityVisitParameters())) 26 | 27 | score_points_expected = [ 28 | rank_adjuster_interface.ScorePoint(80., self.points['Ferry Building']), 29 | rank_adjuster_interface.ScorePoint(100., self.points['Golden Gate Bridge']), 30 | rank_adjuster_interface.ScorePoint(20., self.points['Sutro Baths'])] 31 | 32 | self.assertScorePointsEqual(score_points_expected, score_points_actual, places=3) 33 | 34 | 35 | if __name__ == '__main__': 36 | unittest.main() 37 | -------------------------------------------------------------------------------- /ranker/rank_adjuster_interface.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | 3 | 4 | ScorePoint = namedtuple('ScorePoint', 'Score Point') 5 | 6 | 7 | class RankAdjusterInterface(object): 8 | """Abstract class which adjusts rank of points.""" 9 | 10 | def AdjustRank(self, score_points, city_visit_parameters): 11 | """Adjusts scores of points.""" 12 | raise NotImplemented() 13 | -------------------------------------------------------------------------------- /ranker/runner.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import os 3 | 4 | from config import config as config_ 5 | 6 | 7 | class PointsRankerRunner(object): 8 | 9 | def __init__(self): 10 | config = ( 11 | config_.GetConfig(os.path.join( 12 | 'config', 'runner.config'))) 13 | self.points_ranker = config_.GetPointsRanker(config) 14 | 15 | def Run(self, points_input, city_visit_parameters): 16 | start = datetime.datetime.now() 17 | 18 | points_ranked = ( 19 | self.points_ranker.RankPoints(points_input, city_visit_parameters)) 20 | 21 | print('Input points: %s' % 22 | ', '.join(point.name for point in points_input)) 23 | print('Points ranked: %s' % 24 | ', '.join(point_left.name for point_left in points_ranked)) 25 | print('Elapsed time %s' % (datetime.datetime.now() - start)) 26 | -------------------------------------------------------------------------------- /ranker/runner_nyc_1.py: -------------------------------------------------------------------------------- 1 | from data import runner_util 2 | from data import test_util as point_test_util 3 | from ranker import runner as ranker_runner 4 | from ranker import test_util as ranker_test_util 5 | 6 | 7 | def main(): 8 | points_input = list(point_test_util.GetPointsInput('data', 'test_nyc_1.csv').values()) 9 | points_ranker_runner = ranker_runner.PointsRankerRunner() 10 | city_visit_parameters = ( 11 | runner_util.GetCityVisitParameters(ranker_test_util.MockVisitLocation(), 12 | [ranker_test_util.MockDayVisitParameters()])) 13 | points_ranker_runner.Run(points_input, city_visit_parameters) 14 | 15 | 16 | if __name__ == '__main__': 17 | main() 18 | -------------------------------------------------------------------------------- /ranker/runner_sf_1.py: -------------------------------------------------------------------------------- 1 | from data import runner_util 2 | from data import test_util as point_test_util 3 | from ranker import runner as ranker_runner 4 | from ranker import test_util as ranker_test_util 5 | 6 | 7 | def main(): 8 | points_input = list(point_test_util.GetPointsInput('data', 'test_sf_1.csv').values()) 9 | points_ranker_runner = ranker_runner.PointsRankerRunner() 10 | city_visit_parameters = ( 11 | runner_util.GetCityVisitParameters(ranker_test_util.MockVisitLocation(), 12 | [ranker_test_util.MockDayVisitParameters()])) 13 | points_ranker_runner.Run(points_input, city_visit_parameters) 14 | 15 | 16 | if __name__ == '__main__': 17 | main() 18 | -------------------------------------------------------------------------------- /ranker/runner_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from data import runner_util 4 | from data import test_util as point_test_util 5 | from ranker import runner as ranker_runner 6 | from ranker import test_util as ranker_test_util 7 | 8 | 9 | class PointsRankerRunnerTest(unittest.TestCase): 10 | 11 | def testGeneral(self): 12 | points_input = list(point_test_util.GetPointsInput('data', 'test_sf_1.csv').values()) 13 | points_ranker_runner = ranker_runner.PointsRankerRunner() 14 | city_visit_parameters = ( 15 | runner_util.GetCityVisitParameters(ranker_test_util.MockVisitLocation(), 16 | [ranker_test_util.MockDayVisitParameters()])) 17 | points_ranker_runner.Run(points_input, city_visit_parameters) 18 | 19 | 20 | if __name__ == '__main__': 21 | unittest.main() 22 | -------------------------------------------------------------------------------- /ranker/test_util.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from data import city_visit 3 | from data import point 4 | 5 | 6 | class MockVisitLocation(city_visit.VisitLocationInterface): 7 | pass 8 | 9 | 10 | class MockDayVisitParameters(city_visit.DayVisitParametersInterface): 11 | pass 12 | 13 | 14 | class MockPointType(point.PointTypeInterface): 15 | pass 16 | 17 | 18 | class MockAgeGroup(point.AgeGroupInterface): 19 | pass 20 | 21 | 22 | class MockCityVisitParameters(city_visit.CityVisitParametersInterface): 23 | pass 24 | 25 | 26 | class RankAdjusterTestUtils(unittest.TestCase): 27 | 28 | def assertScorePointsEqual( 29 | self, score_points_expected, score_points_actual, places=None): 30 | for ((score_expected, point_expected), 31 | (score_actual, point_actual)) in zip(score_points_expected, 32 | score_points_actual): 33 | self.assertAlmostEqual(score_expected, score_actual, places=places) 34 | self.assertEqual(point_expected, point_actual) 35 | -------------------------------------------------------------------------------- /router/city_visit_accumulator.py: -------------------------------------------------------------------------------- 1 | class CityVisitAccumulatorInterface(object): 2 | """Abstract class which accumulates results for CityVisit object.""" 3 | 4 | def AddDayVisits(self, day_visits): 5 | """Add DayVisit objects.""" 6 | raise NotImplemented() 7 | 8 | def AddPointsLeft(self, points_left): 9 | """Add points which couldn't be visited.""" 10 | raise NotImplemented() 11 | 12 | def Result(self, cost_accumulator_generator): 13 | """Returns tuple of CityVisit and points_left.""" 14 | raise NotImplemented() 15 | 16 | 17 | class CityVisitAccumulatorGeneratorInterface(object): 18 | """Abstract class which returns every time new clean instance of 19 | CityVisitAccumulatorInterface.""" 20 | 21 | def Generate(self): 22 | """Generate new clean instance of CityVisitAccumulatorInterface.""" 23 | raise NotImplemented() 24 | 25 | 26 | class CityVisitAccumulator(CityVisitAccumulatorInterface): 27 | """Accumulates CityVisit and points left.""" 28 | 29 | def __init__(self): 30 | self.day_visits = [] 31 | self.day_visit_parameterss = [] 32 | self.points_left = [] 33 | 34 | def AddDayVisits(self, day_visits, day_visit_parameterss): 35 | assert len(day_visits) == len(day_visit_parameterss) 36 | self.day_visits.extend(day_visits) 37 | self.day_visit_parameterss.extend(day_visit_parameterss) 38 | 39 | def AddPointsLeft(self, points_left): 40 | self.points_left.extend(points_left) 41 | 42 | def Result(self, city_visit_points_left_generator): 43 | city_visit_points_left = ( 44 | city_visit_points_left_generator.Generate( 45 | self.day_visits, self.day_visit_parameterss, self.points_left)) 46 | return (city_visit_points_left.city_visit, 47 | city_visit_points_left.points_left) 48 | 49 | 50 | class CityVisitAccumulatorGenerator(CityVisitAccumulatorGeneratorInterface): 51 | """Returns every time new clean instance of CityVisitAccumulator.""" 52 | 53 | def Generate(self): 54 | return CityVisitAccumulator() 55 | -------------------------------------------------------------------------------- /router/city_visit_accumulator_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from data import city_visit as city_visit_ 4 | from data import city_visit_test_utils 5 | from router import cost_accumulator 6 | from router import city_visit_points_left 7 | from router import city_visit_accumulator as city_visit_accumulator_ 8 | 9 | 10 | class CityVisitAccumulatorTest(city_visit_test_utils.CityVisitTestExample): 11 | 12 | def setUp(self): 13 | self.no_point_visit_factor = 0. 14 | self.no_point_visit_const = 1000. 15 | self.unused_time_factor = 0.01 16 | cost_accumulator_generator=cost_accumulator.FactorCostAccumulatorGenerator( 17 | no_point_visit_factor=self.no_point_visit_factor, 18 | no_point_visit_const=self.no_point_visit_const, 19 | unused_time_factor=self.unused_time_factor) 20 | self.city_visit_points_left_generator = ( 21 | city_visit_points_left.CityVisitPointsLeftGenerator( 22 | cost_accumulator_generator=cost_accumulator_generator)) 23 | self.city_visit_accumulator_generator = city_visit_accumulator_.CityVisitAccumulatorGenerator() 24 | super(CityVisitAccumulatorTest, self).setUp() 25 | 26 | def testNoDayVisitsNoPointsLeft(self): 27 | city_visit_accumulator = self.city_visit_accumulator_generator.Generate() 28 | city_visit, points_left = ( 29 | city_visit_accumulator.Result( 30 | self.city_visit_points_left_generator)) 31 | self.assertEqual(city_visit_.CityVisit([], city_visit_.CityVisitSummary(0., 0.)), city_visit) 32 | self.assertEqual([], points_left) 33 | 34 | def testTwoDayVisitsTwoPointsLeftSeparateCalls(self): 35 | city_visit_accumulator = self.city_visit_accumulator_generator.Generate() 36 | city_visit_accumulator.AddDayVisits( 37 | [self.day_visit_1], [self.day_visit_parameters_1]) 38 | city_visit_accumulator.AddDayVisits( 39 | [self.day_visit_2], [self.day_visit_parameters_2]) 40 | city_visit_accumulator.AddPointsLeft([self.points['Union Square']]) 41 | city_visit_accumulator.AddPointsLeft([self.points['Lombard Street']]) 42 | city_visit, points_left = ( 43 | city_visit_accumulator.Result( 44 | self.city_visit_points_left_generator)) 45 | self.assertEqual( 46 | city_visit_.CityVisit([self.day_visit_1, self.day_visit_2], 47 | city_visit_.CityVisitSummary(17.7 + 2 * self.no_point_visit_const, 0.)), 48 | city_visit) 49 | self.assertEqual([self.points['Union Square'], 50 | self.points['Lombard Street']], points_left) 51 | 52 | def testTwoDayVisitsTwoPointsLeftOneCall(self): 53 | city_visit_accumulator = self.city_visit_accumulator_generator.Generate() 54 | city_visit_accumulator.AddDayVisits( 55 | [self.day_visit_1, self.day_visit_2], 56 | [self.day_visit_parameters_1, self.day_visit_parameters_2]) 57 | city_visit_accumulator.AddPointsLeft( 58 | [self.points['Union Square'],self.points['Lombard Street']]) 59 | city_visit, points_left = ( 60 | city_visit_accumulator.Result( 61 | self.city_visit_points_left_generator)) 62 | self.assertEqual( 63 | city_visit_.CityVisit([self.day_visit_1, self.day_visit_2], 64 | city_visit_.CityVisitSummary(17.7 + 2 * self.no_point_visit_const, 0.)), 65 | city_visit) 66 | self.assertEqual([self.points['Union Square'], 67 | self.points['Lombard Street']], points_left) 68 | 69 | 70 | if __name__ == '__main__': 71 | unittest.main() 72 | -------------------------------------------------------------------------------- /router/city_visit_heap.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | 3 | from data import city_visit 4 | from router import city_visit_points_left 5 | 6 | 7 | class CityVisitHeap(object): 8 | """Keeps track of the best CityVisits.""" 9 | 10 | def __init__(self, max_count, day_visit_parameterss): 11 | assert isinstance(max_count, int) 12 | for day_visit_parameters in day_visit_parameterss: 13 | assert isinstance(day_visit_parameters, city_visit.DayVisitParametersInterface) 14 | 15 | self.max_count = max_count 16 | self.day_visit_parameterss = day_visit_parameterss 17 | self.cvpls = dict() 18 | self.invariant = True 19 | self.cvpls_sorted = [] 20 | 21 | def PushCityVisit(self, add_city_visit_points_left): 22 | assert isinstance(add_city_visit_points_left, city_visit_points_left.CityVisitPointsLeft) 23 | 24 | add_hash_key = self._CityVisitDatelessHashKey(add_city_visit_points_left) 25 | # If we already has this city_visit_points_left in our list. 26 | if add_hash_key in self.cvpls: 27 | # If new one is more expensive than existing, 28 | # just return False and don't reset invariant. 29 | if (add_city_visit_points_left.city_visit.city_visit_summary.cost >= 30 | self.cvpls[add_hash_key].city_visit.city_visit_summary.cost): 31 | return False 32 | # Else remove information about old one. 33 | else: 34 | del self.cvpls[add_hash_key] 35 | # Add information about new one. 36 | self.cvpls[add_hash_key] = add_city_visit_points_left 37 | self.invariant = False 38 | return True 39 | 40 | def Shrink(self): 41 | # Sort by cost and take first max_count. 42 | self.cvpls_sorted = ( 43 | sorted(self.cvpls.values(), 44 | key=lambda cvpl: cvpl.city_visit.city_visit_summary.cost) 45 | [:self.max_count]) 46 | # Recompute hashes and indices. 47 | self.cvpls = { 48 | self._CityVisitDatelessHashKey(cvpl) : cvpl 49 | for cvpl in self.cvpls_sorted} 50 | # Set invariant. 51 | self.invariant = True 52 | 53 | def GetCityVisitPointsLeftList(self): 54 | assert self.invariant, ( 55 | 'CityVisit list cannot be returned. Please call Shrink first.') 56 | return self.cvpls_sorted 57 | 58 | def Size(self): 59 | return len(self.cvpls) 60 | 61 | def Clear(self): 62 | self.cvpls = dict() 63 | self.invariant = True 64 | self.cvpls_sorted = [] 65 | 66 | def _CityVisitDatelessHashKey(self, cvpl): 67 | hash_keys = [] 68 | for day_visit_parameters, day_visit in ( 69 | zip(self.day_visit_parameterss, cvpl.city_visit.day_visits)): 70 | m_day = hashlib.md5() 71 | m_day.update(day_visit_parameters.DatelessHashKey().encode('utf-8')) 72 | m_day.update(day_visit.DatelessHashKey().encode('utf-8')) 73 | hash_keys.append(m_day.hexdigest()) 74 | 75 | m = hashlib.md5() 76 | # Order of days doesn't matter. 77 | for hash_key in sorted(hash_keys): 78 | m.update(hash_key.encode('utf-8')) 79 | return m.hexdigest() 80 | -------------------------------------------------------------------------------- /router/city_visit_heap_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from data import city_visit as city_visit_ 4 | from router import city_visit_heap as city_visit_heap_ 5 | from router import city_visit_points_left 6 | 7 | 8 | class MockDayVisitParameters(city_visit_.DayVisitParametersInterface): 9 | 10 | def __init__(self, hash_key): 11 | assert isinstance(hash_key, str) 12 | self.hash_key = hash_key 13 | 14 | def DatelessHashKey(self): 15 | return self.hash_key 16 | 17 | 18 | class MockDayVisit(city_visit_.DayVisitInterface): 19 | 20 | def __init__(self, hash_key): 21 | assert isinstance(hash_key, str) 22 | self.hash_key = hash_key 23 | 24 | def DatelessHashKey(self): 25 | return self.hash_key 26 | 27 | 28 | def MockCityVisitPointsLeft(day_visit_hash_keys, cost): 29 | assert isinstance(day_visit_hash_keys, list) 30 | assert isinstance(cost, float) 31 | 32 | city_visit = city_visit_.CityVisit( 33 | [MockDayVisit(day_visit_hash_key) 34 | for day_visit_hash_key in day_visit_hash_keys], 35 | city_visit_.CityVisitSummary(cost, 0.)) 36 | return city_visit_points_left.CityVisitPointsLeft(city_visit, []) 37 | 38 | 39 | class CityVisitHeapTest(unittest.TestCase): 40 | 41 | def testGeneral(self): 42 | city_visit_heap = city_visit_heap_.CityVisitHeap(3, [MockDayVisitParameters('par')]) 43 | self.assertEqual(0, city_visit_heap.Size()) 44 | self.assertEqual([], city_visit_heap.GetCityVisitPointsLeftList()) 45 | 46 | visit_a = MockCityVisitPointsLeft(['a'], 10.) 47 | city_visit_heap.PushCityVisit(visit_a) 48 | self.assertEqual(1, city_visit_heap.Size()) 49 | self.assertRaises(AssertionError, 50 | city_visit_heap.GetCityVisitPointsLeftList) 51 | 52 | visit_b = MockCityVisitPointsLeft(['b'], 5.) 53 | city_visit_heap.PushCityVisit(visit_b) 54 | self.assertEqual(2, city_visit_heap.Size()) 55 | self.assertRaises(AssertionError, 56 | city_visit_heap.GetCityVisitPointsLeftList) 57 | 58 | visit_c = MockCityVisitPointsLeft(['c'], 7.) 59 | city_visit_heap.PushCityVisit(visit_c) 60 | self.assertEqual(3, city_visit_heap.Size()) 61 | self.assertRaises(AssertionError, 62 | city_visit_heap.GetCityVisitPointsLeftList) 63 | 64 | city_visit_heap.Shrink() 65 | self.assertEqual(3, city_visit_heap.Size()) 66 | self.assertEqual([visit_b, visit_c, visit_a], 67 | city_visit_heap.GetCityVisitPointsLeftList()) 68 | 69 | visit_d = MockCityVisitPointsLeft(['d'], 3.) 70 | city_visit_heap.PushCityVisit(visit_d) 71 | self.assertEqual(4, city_visit_heap.Size()) 72 | self.assertRaises(AssertionError, 73 | city_visit_heap.GetCityVisitPointsLeftList) 74 | 75 | visit_e = MockCityVisitPointsLeft(['e'], 15.) 76 | city_visit_heap.PushCityVisit(visit_e) 77 | self.assertEqual(5, city_visit_heap.Size()) 78 | self.assertRaises(AssertionError, 79 | city_visit_heap.GetCityVisitPointsLeftList) 80 | 81 | city_visit_heap.Shrink() 82 | self.assertEqual(3, city_visit_heap.Size()) 83 | self.assertEqual([visit_d, visit_b, visit_c], 84 | city_visit_heap.GetCityVisitPointsLeftList()) 85 | 86 | city_visit_heap.Clear() 87 | self.assertEqual(0, city_visit_heap.Size()) 88 | self.assertEqual([], city_visit_heap.GetCityVisitPointsLeftList()) 89 | 90 | def testAddingSameOrderlessHashKeyShrink(self): 91 | city_visit_heap = city_visit_heap_.CityVisitHeap(3, [MockDayVisitParameters('par')]) 92 | self.assertEqual(0, city_visit_heap.Size()) 93 | self.assertEqual([], city_visit_heap.GetCityVisitPointsLeftList()) 94 | 95 | visit_a = MockCityVisitPointsLeft(['adf'], 10.) 96 | city_visit_heap.PushCityVisit(visit_a) 97 | self.assertEqual(1, city_visit_heap.Size()) 98 | self.assertRaises(AssertionError, 99 | city_visit_heap.GetCityVisitPointsLeftList) 100 | 101 | visit_b = MockCityVisitPointsLeft(['bc'], 5.) 102 | city_visit_heap.PushCityVisit(visit_b) 103 | self.assertEqual(2, city_visit_heap.Size()) 104 | self.assertRaises(AssertionError, 105 | city_visit_heap.GetCityVisitPointsLeftList) 106 | 107 | visit_c = MockCityVisitPointsLeft(['bc'], 7.) # Same day_visit_hash_keys. 108 | city_visit_heap.PushCityVisit(visit_c) # cost higher. 109 | self.assertEqual(2, city_visit_heap.Size()) 110 | self.assertRaises(AssertionError, 111 | city_visit_heap.GetCityVisitPointsLeftList) 112 | 113 | city_visit_heap.Shrink() 114 | self.assertEqual(2, city_visit_heap.Size()) 115 | self.assertEqual([visit_b, visit_a], 116 | city_visit_heap.GetCityVisitPointsLeftList()) 117 | 118 | visit_d = MockCityVisitPointsLeft(['adf'], 3.) # Same day_visit_hash_keys. 119 | city_visit_heap.PushCityVisit(visit_d) # const lower. 120 | self.assertEqual(2, city_visit_heap.Size()) 121 | self.assertRaises(AssertionError, 122 | city_visit_heap.GetCityVisitPointsLeftList) 123 | 124 | city_visit_heap.Shrink() 125 | self.assertEqual(2, city_visit_heap.Size()) 126 | self.assertEqual([visit_d, visit_b], 127 | city_visit_heap.GetCityVisitPointsLeftList()) 128 | 129 | visit_e = MockCityVisitPointsLeft(['eg'], 15.) 130 | city_visit_heap.PushCityVisit(visit_e) 131 | self.assertEqual(3, city_visit_heap.Size()) 132 | self.assertRaises(AssertionError, 133 | city_visit_heap.GetCityVisitPointsLeftList) 134 | 135 | city_visit_heap.Shrink() 136 | self.assertEqual(3, city_visit_heap.Size()) 137 | self.assertEqual([visit_d, visit_b, visit_e], 138 | city_visit_heap.GetCityVisitPointsLeftList()) 139 | 140 | visit_f = MockCityVisitPointsLeft(['adf'], 1.) # Same day_visit_hash_keys. 141 | city_visit_heap.PushCityVisit(visit_f) # const lower. 142 | self.assertEqual(3, city_visit_heap.Size()) 143 | self.assertRaises(AssertionError, 144 | city_visit_heap.GetCityVisitPointsLeftList) 145 | 146 | visit_g = MockCityVisitPointsLeft(['eg'], 12.) # Same day_visit_hash_keys. 147 | city_visit_heap.PushCityVisit(visit_g) # const lower. 148 | self.assertEqual(3, city_visit_heap.Size()) 149 | self.assertRaises(AssertionError, 150 | city_visit_heap.GetCityVisitPointsLeftList) 151 | 152 | city_visit_heap.Shrink() 153 | self.assertEqual(3, city_visit_heap.Size()) 154 | self.assertEqual([visit_f, visit_b, visit_g], 155 | city_visit_heap.GetCityVisitPointsLeftList()) 156 | 157 | city_visit_heap.Clear() 158 | self.assertEqual(0, city_visit_heap.Size()) 159 | self.assertEqual([], city_visit_heap.GetCityVisitPointsLeftList()) 160 | 161 | def testAddingSameOrderlessHashKeyNoShrink(self): 162 | city_visit_heap = city_visit_heap_.CityVisitHeap(3, [MockDayVisitParameters('par')]) 163 | self.assertEqual(0, city_visit_heap.Size()) 164 | self.assertEqual([], city_visit_heap.GetCityVisitPointsLeftList()) 165 | 166 | visit_a = MockCityVisitPointsLeft(['adf'], 10.) 167 | city_visit_heap.PushCityVisit(visit_a) 168 | self.assertEqual(1, city_visit_heap.Size()) 169 | self.assertRaises(AssertionError, 170 | city_visit_heap.GetCityVisitPointsLeftList) 171 | 172 | visit_b = MockCityVisitPointsLeft(['bc'], 5.) 173 | city_visit_heap.PushCityVisit(visit_b) 174 | self.assertEqual(2, city_visit_heap.Size()) 175 | self.assertRaises(AssertionError, 176 | city_visit_heap.GetCityVisitPointsLeftList) 177 | 178 | visit_c = MockCityVisitPointsLeft(['bc'], 7.) # Same day_visit_hash_keys. 179 | city_visit_heap.PushCityVisit(visit_c) # cost higher. 180 | self.assertEqual(2, city_visit_heap.Size()) 181 | self.assertRaises(AssertionError, 182 | city_visit_heap.GetCityVisitPointsLeftList) 183 | 184 | visit_d = MockCityVisitPointsLeft(['adf'], 3.) # Same day_visit_hash_keys. 185 | city_visit_heap.PushCityVisit(visit_d) # const lower. 186 | self.assertEqual(2, city_visit_heap.Size()) 187 | self.assertRaises(AssertionError, 188 | city_visit_heap.GetCityVisitPointsLeftList) 189 | 190 | visit_e = MockCityVisitPointsLeft(['eg'], 15.) 191 | city_visit_heap.PushCityVisit(visit_e) 192 | self.assertEqual(3, city_visit_heap.Size()) 193 | self.assertRaises(AssertionError, 194 | city_visit_heap.GetCityVisitPointsLeftList) 195 | 196 | visit_f = MockCityVisitPointsLeft(['adf'], 1.) # Same day_visit_hash_keys. 197 | city_visit_heap.PushCityVisit(visit_f) # const lower. 198 | self.assertEqual(3, city_visit_heap.Size()) 199 | self.assertRaises(AssertionError, 200 | city_visit_heap.GetCityVisitPointsLeftList) 201 | 202 | visit_g = MockCityVisitPointsLeft(['eg'], 12.) # Same day_visit_hash_keys. 203 | city_visit_heap.PushCityVisit(visit_g) # const lower. 204 | self.assertEqual(3, city_visit_heap.Size()) 205 | self.assertRaises(AssertionError, 206 | city_visit_heap.GetCityVisitPointsLeftList) 207 | 208 | city_visit_heap.Shrink() 209 | self.assertEqual(3, city_visit_heap.Size()) 210 | self.assertEqual([visit_f, visit_b, visit_g], 211 | city_visit_heap.GetCityVisitPointsLeftList()) 212 | 213 | city_visit_heap.Clear() 214 | self.assertEqual(0, city_visit_heap.Size()) 215 | self.assertEqual([], city_visit_heap.GetCityVisitPointsLeftList()) 216 | 217 | 218 | def testCityVisitOrderlessHashKey(self): 219 | city_visit_heap_a = city_visit_heap_.CityVisitHeap(3, [ 220 | MockDayVisitParameters('parX'), MockDayVisitParameters('parY')]) 221 | city_visit_heap_b = city_visit_heap_.CityVisitHeap(3, [ 222 | MockDayVisitParameters('parY'), MockDayVisitParameters('parX')]) 223 | visit_a = MockCityVisitPointsLeft(['dayX', 'dayY'], 10.) 224 | visit_b = MockCityVisitPointsLeft(['dayY', 'dayX'], 10.) 225 | # Same pairs of day_visit_parameters and day_visit, but different order. 226 | self.assertEqual(city_visit_heap_a._CityVisitDatelessHashKey(visit_a), 227 | city_visit_heap_b._CityVisitDatelessHashKey(visit_b)) 228 | # Same here. 229 | self.assertEqual(city_visit_heap_a._CityVisitDatelessHashKey(visit_b), 230 | city_visit_heap_b._CityVisitDatelessHashKey(visit_a)) 231 | # Parameterss are different for the same day_visits. 232 | self.assertNotEqual(city_visit_heap_a._CityVisitDatelessHashKey(visit_a), 233 | city_visit_heap_b._CityVisitDatelessHashKey(visit_a)) 234 | # Day_visits are different for the same parameterss. 235 | self.assertNotEqual(city_visit_heap_a._CityVisitDatelessHashKey(visit_a), 236 | city_visit_heap_a._CityVisitDatelessHashKey(visit_b)) 237 | 238 | 239 | 240 | if __name__ == '__main__': 241 | unittest.main() 242 | 243 | -------------------------------------------------------------------------------- /router/city_visit_points_left.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | 3 | from data import city_visit as city_visit_ 4 | 5 | 6 | CityVisitPointsLeft = namedtuple( 7 | 'CityVisitPointsLeft', 'city_visit points_left') 8 | 9 | 10 | class CityVisitPointsLeftGeneratorInterface(object): 11 | """Abstract class which generates an instance of CityVisitPointsLeft.""" 12 | 13 | def Generate(self, day_visits, day_visit_parameterss, points_left): 14 | """Generates an instance of CityVisitPointsLeft.""" 15 | raise NotImplemented() 16 | 17 | 18 | class CityVisitPointsLeftGenerator(CityVisitPointsLeftGeneratorInterface): 19 | """Generates an instance of CityVisitPointsLeft.""" 20 | 21 | def __init__(self, cost_accumulator_generator): 22 | self.cost_accumulator_generator = cost_accumulator_generator 23 | 24 | def Generate(self, day_visits, day_visit_parameterss, points_left): 25 | 26 | cost_accumulator = self.cost_accumulator_generator.Generate() 27 | price = 0 28 | for day_visit, day_visit_parameters in ( 29 | zip(day_visits, day_visit_parameterss)): 30 | for action in day_visit.actions: 31 | if isinstance(action, city_visit_.PointVisit): 32 | cost_accumulator.AddPointVisit(action.point) 33 | elif isinstance(action, city_visit_.MoveBetween): 34 | cost_accumulator.AddMoveBetween(action.move_description) 35 | elif isinstance(action, city_visit_.Lunch): 36 | cost_accumulator.AddLunch(action.lunch_hours) 37 | else: 38 | raise NotImplemented('Unknown action type %s' % type(action)) 39 | unused_time = (day_visit_parameters.end_datetime - 40 | day_visit.actions[-1].start_end_datetime.end) 41 | cost_accumulator.AddUnusedTime(unused_time) 42 | price += day_visit.price 43 | for point_left in points_left: 44 | cost_accumulator.AddPointLeft(point_left) 45 | city_visit_summary = city_visit_.CityVisitSummary(cost_accumulator.Cost(), price) 46 | city_visit = city_visit_.CityVisit(day_visits, city_visit_summary) 47 | return CityVisitPointsLeft(city_visit, points_left) 48 | -------------------------------------------------------------------------------- /router/city_visit_points_left_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from data import city_visit 4 | from data import city_visit_test_utils 5 | from router import city_visit_points_left as city_visit_points_left_ 6 | from router import cost_accumulator 7 | 8 | 9 | class CityVisitPointsLeftTest(city_visit_test_utils.CityVisitTestExample): 10 | 11 | def setUp(self): 12 | self.no_point_visit_factor = 0. 13 | self.no_point_visit_const = 1000. 14 | self.unused_time_factor = 0.01 15 | cost_accumulator_generator=cost_accumulator.FactorCostAccumulatorGenerator( 16 | no_point_visit_factor=self.no_point_visit_factor, 17 | no_point_visit_const=self.no_point_visit_const, 18 | unused_time_factor=self.unused_time_factor) 19 | self.city_visit_points_left_generator = ( 20 | city_visit_points_left_.CityVisitPointsLeftGenerator( 21 | cost_accumulator_generator=cost_accumulator_generator)) 22 | super(CityVisitPointsLeftTest, self).setUp() 23 | 24 | def testDayVisitsNoPointsLeft(self): 25 | city_visit_points_left = ( 26 | self.city_visit_points_left_generator.Generate( 27 | [self.day_visit_1, self.day_visit_2], 28 | [self.day_visit_parameters_1, self.day_visit_parameters_2], [])) 29 | self.assertEqual(city_visit.CityVisitSummary(17.7, 0.), 30 | city_visit_points_left.city_visit.city_visit_summary) 31 | 32 | def testDayVisitsOnePointsLeft(self): 33 | city_visit_points_left = ( 34 | self.city_visit_points_left_generator.Generate( 35 | [self.day_visit_1, self.day_visit_2], 36 | [self.day_visit_parameters_1, self.day_visit_parameters_2], 37 | [self.points['Union Square']])) 38 | self.assertEqual(city_visit.CityVisitSummary(17.7 + self.no_point_visit_const, 0.), 39 | city_visit_points_left.city_visit.city_visit_summary) 40 | 41 | def testDayVisitsTwoPointsLeft(self): 42 | city_visit_points_left = ( 43 | self.city_visit_points_left_generator.Generate( 44 | [self.day_visit_1, self.day_visit_2], 45 | [self.day_visit_parameters_1, self.day_visit_parameters_2], 46 | [self.points['Union Square'], self.points['Lombard Street']])) 47 | self.assertEqual(city_visit.CityVisitSummary(17.7 + 2 * self.no_point_visit_const, 0.), 48 | city_visit_points_left.city_visit.city_visit_summary) 49 | 50 | def testDayVisitsFourPointsLeft(self): 51 | city_visit_points_left = ( 52 | self.city_visit_points_left_generator.Generate( 53 | [self.day_visit_1, self.day_visit_2], 54 | [self.day_visit_parameters_1, self.day_visit_parameters_2], 55 | [self.points['Union Square'], self.points['Lombard Street'], 56 | self.points['Coit Tower'], self.points['Att Park']])) 57 | self.assertEqual(city_visit.CityVisitSummary(17.7 + 4 * self.no_point_visit_const, 0.), 58 | city_visit_points_left.city_visit.city_visit_summary) 59 | 60 | def testNoDayVisitsNoPointsLeft(self): 61 | city_visit_points_left = ( 62 | self.city_visit_points_left_generator.Generate([], [], [])) 63 | self.assertEqual(city_visit.CityVisitSummary(0., 0.), 64 | city_visit_points_left.city_visit.city_visit_summary) 65 | 66 | def testNoDayVisitsOnePointsLeft(self): 67 | city_visit_points_left = ( 68 | self.city_visit_points_left_generator.Generate( 69 | [], [], 70 | [self.points['Union Square']])) 71 | self.assertEqual(city_visit.CityVisitSummary(self.no_point_visit_const, 0.), 72 | city_visit_points_left.city_visit.city_visit_summary) 73 | 74 | def testNoDayVisitsTwoPointsLeft(self): 75 | city_visit_points_left = ( 76 | self.city_visit_points_left_generator.Generate( 77 | [], [], 78 | [self.points['Union Square'], self.points['Lombard Street']])) 79 | self.assertEqual(city_visit.CityVisitSummary(2 * self.no_point_visit_const, 0.), 80 | city_visit_points_left.city_visit.city_visit_summary) 81 | 82 | def testNoDayVisitsFourPointsLeft(self): 83 | city_visit_points_left = ( 84 | self.city_visit_points_left_generator.Generate( 85 | [], [], 86 | [self.points['Union Square'], self.points['Lombard Street'], 87 | self.points['Coit Tower'], self.points['Att Park']])) 88 | self.assertEqual(city_visit.CityVisitSummary(4 * self.no_point_visit_const, 0.), 89 | city_visit_points_left.city_visit.city_visit_summary) 90 | 91 | 92 | if __name__ == '__main__': 93 | unittest.main() 94 | -------------------------------------------------------------------------------- /router/city_visit_router.py: -------------------------------------------------------------------------------- 1 | import multiprocessing 2 | 3 | from data import point as point_ 4 | from data import city_visit 5 | from router import city_visit_points_left as city_visit_points_left_ 6 | from router import city_visit_heap as city_visit_heap_ 7 | from router import days_permutations 8 | from router import day_visit_router as day_visit_router_ 9 | from router import points_queue as points_queue_ 10 | from router import city_visit_accumulator as city_visit_accumulator_ 11 | 12 | 13 | ################################################################################ 14 | # NOTE(igushev): Code between hashtags lines is to be run inside worker 15 | # processes, therefore instead of module-level should be considered as 16 | # process-level. 17 | _day_visit_router = None 18 | _city_visit_points_left_generator = None 19 | _max_depth = None 20 | _city_visit_heap_size = None 21 | 22 | 23 | def _PushPointsToDayVisitsInit( 24 | day_visit_router, city_visit_points_left_generator, max_depth, 25 | city_visit_heap_size): 26 | global _day_visit_router 27 | global _city_visit_points_left_generator 28 | global _max_depth 29 | global _city_visit_heap_size 30 | _day_visit_router = day_visit_router 31 | _city_visit_points_left_generator = city_visit_points_left_generator 32 | _max_depth = max_depth 33 | _city_visit_heap_size = city_visit_heap_size 34 | 35 | 36 | class CouldPush(object): 37 | 38 | def __init__(self): 39 | self.value = False 40 | 41 | 42 | def _PushPointsToDayVisitsImpl( 43 | depth, points, days_consider, day_visits, points_left, 44 | could_push, city_visit_heap, day_visit_parameterss, points_left_consistent): 45 | assert len(days_consider) == len(day_visits) 46 | assert len(day_visits) == len(day_visit_parameterss) 47 | for days_permutation in days_permutations.DaysPermutations(points, days_consider): 48 | # Initialize structure for next iteration. 49 | next_points_left = [] 50 | next_day_visits_consider = days_consider[:] 51 | next_day_visits = day_visits[:] 52 | 53 | # Try to fit to each day its points. 54 | for i, day_points_add in days_permutation.items(): 55 | day_points_all = day_visits[i].GetPoints() 56 | day_points_all.extend(day_points_add) 57 | day_visit_best, points_left_best = ( 58 | _day_visit_router.RouteDayVisit( 59 | day_points_all, day_visit_parameterss[i])) 60 | assert isinstance(day_visit_best, city_visit.DayVisitInterface) 61 | for point_left_best in points_left_best: 62 | assert isinstance(point_left_best, point_.PointInterface) 63 | next_points_left.extend(points_left_best) 64 | next_day_visits_consider[i] = False 65 | next_day_visits = ( 66 | next_day_visits[:i] + [day_visit_best] + next_day_visits[i+1:]) 67 | 68 | if len(next_points_left) > 1: 69 | print('More than one point left after adding to existing DayVisits!') 70 | 71 | # NOTE(igushev): The only option when next_points_left are the same as 72 | # input points, it than each corresponding day has not fit its points. The 73 | # recursive call will check other days, which should be covered by this 74 | # level of permutation. 75 | # NOTE(igushev): If maximum depth of recursion or no next_points_left, add 76 | # a potential result. 77 | if (set(next_points_left) == set(points) or 78 | depth == _max_depth or 79 | not next_points_left or 80 | all(not day_visit_consider 81 | for day_visit_consider in next_day_visits_consider)): 82 | city_visit_points_left = ( 83 | _city_visit_points_left_generator.Generate( 84 | next_day_visits, day_visit_parameterss, 85 | points_left + next_points_left)) 86 | city_visit_heap.PushCityVisit(city_visit_points_left) 87 | # If next_points_left are only the ones we consistently can't push, 88 | # consider this iteration successful and assign could_push True. 89 | if set(next_points_left) <= points_left_consistent: 90 | could_push.value = True 91 | continue 92 | 93 | # NOTE(igushev): Recursion call. 94 | _PushPointsToDayVisitsImpl( 95 | depth+1, next_points_left, next_day_visits_consider, next_day_visits, 96 | points_left, could_push, city_visit_heap, day_visit_parameterss, 97 | points_left_consistent) 98 | 99 | 100 | def _PushPointsToDayVisitsWork( 101 | points, day_visits, points_left, day_visit_parameterss, 102 | points_left_consistent): 103 | days_consider = [True] * len(day_visits) 104 | could_push = CouldPush() 105 | city_visit_heap = city_visit_heap_.CityVisitHeap( 106 | _city_visit_heap_size, day_visit_parameterss) 107 | _PushPointsToDayVisitsImpl( 108 | 1, points, days_consider, day_visits, points_left, 109 | could_push, city_visit_heap, day_visit_parameterss, 110 | points_left_consistent) 111 | city_visit_heap.Shrink() 112 | return could_push, city_visit_heap 113 | ################################################################################ 114 | 115 | 116 | class CityVisitRouterInterface(object): 117 | """Abstract class which routes points during CityVisit.""" 118 | 119 | def RouteCityVisit(self, points, day_visit_parameterss, 120 | city_visit_accumulator_generator): 121 | """Route maximum number of points with minimum cost for CityVisit.""" 122 | raise NotImplemented() 123 | 124 | 125 | class CityVisitRouter(CityVisitRouterInterface): 126 | """Routes points during CityVisit using permutation and keeping track of 127 | best so far.""" 128 | 129 | def __init__(self, day_visit_router, city_visit_points_left_generator, 130 | points_queue_generator, shard_num_days, max_depth, 131 | city_visit_heap_size, max_non_pushed_points, num_processes): 132 | assert isinstance(day_visit_router, day_visit_router_.DayVisitRouterInterface) 133 | assert isinstance(city_visit_points_left_generator, 134 | city_visit_points_left_.CityVisitPointsLeftGenerator) 135 | assert isinstance(points_queue_generator, points_queue_.PointsQueueGeneratorInterface) 136 | if shard_num_days is not None: 137 | assert isinstance(shard_num_days, int) 138 | assert isinstance(max_depth, int) 139 | assert isinstance(city_visit_heap_size, int) 140 | assert isinstance(max_non_pushed_points, int) 141 | if num_processes is not None: 142 | assert isinstance(num_processes, int) 143 | 144 | self.day_visit_router = day_visit_router 145 | self.city_visit_points_left_generator = city_visit_points_left_generator 146 | self.points_queue_generator = points_queue_generator 147 | self.shard_num_days = shard_num_days 148 | self.max_depth = max_depth # Don't need, but still add as member. 149 | self.city_visit_heap_size = city_visit_heap_size 150 | self.max_non_pushed_points = max_non_pushed_points 151 | self.workers_pool = multiprocessing.Pool( 152 | num_processes, 153 | initializer=_PushPointsToDayVisitsInit, 154 | initargs=(day_visit_router, city_visit_points_left_generator, 155 | max_depth, city_visit_heap_size)) 156 | 157 | def RouteCityVisitShard(self, points_queue, day_visit_parameterss, 158 | points_left_consistent): 159 | initial_day_visits = [] 160 | for day_visit_parameters in day_visit_parameterss: 161 | day_visit, points_left = ( 162 | self.day_visit_router.RouteDayVisit([], day_visit_parameters)) 163 | assert isinstance(day_visit, city_visit.DayVisitInterface) 164 | assert not points_left 165 | initial_day_visits.append(day_visit) 166 | city_visit_points_lefts = [ 167 | self.city_visit_points_left_generator.Generate( 168 | initial_day_visits, day_visit_parameterss, [])] 169 | could_not_push = 0 170 | while points_queue.HasPoints(): 171 | # NOTE(igushev): Run in parallel pushing points to different 172 | # CityVisitPointsLefts from previous heap and collect AsyncResult 173 | # objects. 174 | push_points = points_queue.GetPushPoints(day_visit_parameterss) 175 | push_points_to_day_visits_results = [] 176 | for city_visit_points_left in city_visit_points_lefts: 177 | day_visits = city_visit_points_left.city_visit.day_visits 178 | points_left = city_visit_points_left.points_left 179 | push_points_to_day_visits_results.append( 180 | self.workers_pool.apply_async( 181 | _PushPointsToDayVisitsWork, 182 | args=(push_points, day_visits, points_left, 183 | day_visit_parameterss, points_left_consistent))) 184 | 185 | # NOTE(igushev): Process results and fill overall could_push and 186 | # city_visit_heap. 187 | could_push = CouldPush() 188 | city_visit_heap = city_visit_heap_.CityVisitHeap( 189 | self.city_visit_heap_size, day_visit_parameterss) 190 | for push_points_to_day_visits_result in push_points_to_day_visits_results: 191 | could_push_one, city_visit_heap_one = ( 192 | push_points_to_day_visits_result.get()) 193 | if could_push_one.value: 194 | could_push.value = True 195 | for city_visit_points_left in city_visit_heap_one.GetCityVisitPointsLeftList(): 196 | city_visit_heap.PushCityVisit(city_visit_points_left) 197 | 198 | city_visit_heap.Shrink() 199 | city_visit_points_lefts = city_visit_heap.GetCityVisitPointsLeftList() 200 | if not could_push.value: 201 | could_not_push += 1 202 | if could_not_push >= self.max_non_pushed_points: 203 | break 204 | assert len(city_visit_points_lefts) >= 1 205 | city_visit_points_left_best = city_visit_points_lefts[0] 206 | return (city_visit_points_left_best.city_visit, 207 | city_visit_points_left_best.points_left) 208 | 209 | def RouteCityVisit(self, points, day_visit_parameterss, 210 | city_visit_accumulator_generator): 211 | for point in points: 212 | assert isinstance(point, point_.PointInterface) 213 | for day_visit_parameters in day_visit_parameterss: 214 | assert isinstance(day_visit_parameters, city_visit.DayVisitParametersInterface) 215 | assert isinstance(city_visit_accumulator_generator, 216 | city_visit_accumulator_.CityVisitAccumulatorGenerator) 217 | 218 | shard_num_days = self.shard_num_days or len(day_visit_parameterss) 219 | city_visit_accumulator = city_visit_accumulator_generator.Generate() 220 | points_queue = self.points_queue_generator.Generate(points) 221 | points_left_consistent = set([]) 222 | for shard_i, begin in ( 223 | enumerate(range(0, len(day_visit_parameterss), shard_num_days))): 224 | end = min(begin + shard_num_days, len(day_visit_parameterss)) 225 | print('Processing shard %d from %d to %d' % (shard_i+1, begin, end)) 226 | 227 | city_visit_points_left_shard, points_left_shard = ( 228 | self.RouteCityVisitShard( 229 | points_queue, day_visit_parameterss[begin:end], 230 | points_left_consistent)) 231 | 232 | city_visit_accumulator.AddDayVisits( 233 | city_visit_points_left_shard.day_visits, 234 | day_visit_parameterss[begin:end]) 235 | # Next iteration points_queue should points left from previous iteration 236 | # and the rest of points. 237 | points_queue.AddBackToQueue(points_left_shard) 238 | points_left_consistent.update(set(points_left_shard)) 239 | 240 | city_visit_accumulator.AddPointsLeft(points_queue.GetPointsLeft()) 241 | return city_visit_accumulator.Result( 242 | self.city_visit_points_left_generator) 243 | -------------------------------------------------------------------------------- /router/cost_accumulator.py: -------------------------------------------------------------------------------- 1 | import copy 2 | from datetime import timedelta 3 | 4 | from data import point as point_ 5 | from data import city_visit 6 | 7 | 8 | class CostAccumulatorInterface(object): 9 | """Abstract class which accumulates cost for a day.""" 10 | 11 | def __init__(self): 12 | self.cost = 0. 13 | 14 | def Copy(self): 15 | return copy.deepcopy(self) 16 | 17 | def Cost(self): 18 | """Get current cost.""" 19 | return self.cost 20 | 21 | def AddPointVisit(self, point): 22 | """Add visiting point to the cost.""" 23 | raise NotImplemented() 24 | 25 | def AddMoveBetween(self, move_description): 26 | """Add moving to the cost.""" 27 | raise NotImplemented() 28 | 29 | def AddLunch(self, lunch_hour): 30 | """Add lunch to the cost.""" 31 | raise NotImplemented() 32 | 33 | def AddPointLeft(self, point): 34 | """Add point that cannot be visited to the cost.""" 35 | raise NotImplemented() 36 | 37 | 38 | class CostAccumulatorGeneratorInterface(object): 39 | """Abstract class which returns every time new clean instance of 40 | CostAccumulatorInterface.""" 41 | 42 | def Generate(self): 43 | """Generate new clean instance of CostAccumulatorInterface.""" 44 | raise NotImplemented() 45 | 46 | 47 | class FactorCostAccumulator(CostAccumulatorInterface): 48 | """Accumulates time spent for each action multiplied by a factor.""" 49 | 50 | def __init__(self, 51 | point_visit_factor=1., 52 | move_walking_factor=1., 53 | move_driving_factor=1., 54 | move_ptt_factor=1., 55 | lunch_factor=1., 56 | no_point_visit_factor=1., 57 | no_point_visit_const=0., 58 | unused_time_factor=1.): 59 | assert isinstance(point_visit_factor, float) 60 | assert isinstance(move_walking_factor, float) 61 | assert isinstance(move_driving_factor, float) 62 | assert isinstance(move_ptt_factor, float) 63 | assert isinstance(lunch_factor, float) 64 | assert isinstance(no_point_visit_factor, float) 65 | assert isinstance(no_point_visit_const, float) 66 | assert isinstance(unused_time_factor, float) 67 | 68 | self._point_visit_factor = point_visit_factor 69 | self._move_walking_factor = move_walking_factor 70 | self._move_driving_factor = move_driving_factor 71 | self._move_ptt_factor = move_ptt_factor 72 | self._lunch_factor = lunch_factor 73 | self._no_point_visit_factor = no_point_visit_factor 74 | self._no_point_visit_const = no_point_visit_const 75 | self._unused_time_factor = unused_time_factor 76 | super(FactorCostAccumulator, self).__init__() 77 | 78 | def AddPointVisit(self, point): 79 | assert isinstance(point, point_.PointInterface) 80 | 81 | self.cost += point.duration * self._point_visit_factor 82 | 83 | def AddMoveBetween(self, move_description): 84 | assert isinstance(move_description, city_visit.MoveDescription) 85 | 86 | if move_description.move_type == city_visit.MoveType.walking: 87 | factor = self._move_walking_factor 88 | elif move_description.move_type == city_visit.MoveType.driving: 89 | factor = self._move_driving_factor 90 | elif move_description.move_type == city_visit.MoveType.ptt: 91 | factor = self._move_ptt_factor 92 | else: 93 | raise NotImplemented('Unknown MoveType: %s' % move_description.move_type) 94 | self.cost += move_description.move_hours * factor 95 | 96 | def AddLunch(self, lunch_hour): 97 | assert isinstance(lunch_hour, float) 98 | 99 | self.cost += lunch_hour * self._lunch_factor 100 | 101 | def AddPointLeft(self, point): 102 | assert isinstance(point, point_.PointInterface) 103 | 104 | self.cost += (point.duration * self._no_point_visit_factor + 105 | self._no_point_visit_const) 106 | 107 | def AddUnusedTime(self, unused_time): 108 | assert isinstance(unused_time, timedelta) 109 | 110 | self.cost += unused_time.total_seconds() / 60. * self._unused_time_factor 111 | 112 | 113 | class FactorCostAccumulatorGenerator(CostAccumulatorGeneratorInterface): 114 | """Returns every time new clean instance of FactorCostAccumulator.""" 115 | 116 | def __init__(self, 117 | point_visit_factor=1., 118 | move_walking_factor=1., 119 | move_driving_factor=1., 120 | move_ptt_factor=1., 121 | lunch_factor=1., 122 | no_point_visit_factor=1., 123 | no_point_visit_const=0., 124 | unused_time_factor=1.): 125 | assert isinstance(point_visit_factor, float) 126 | assert isinstance(move_walking_factor, float) 127 | assert isinstance(move_driving_factor, float) 128 | assert isinstance(move_ptt_factor, float) 129 | assert isinstance(lunch_factor, float) 130 | assert isinstance(no_point_visit_factor, float) 131 | assert isinstance(no_point_visit_const, float) 132 | assert isinstance(unused_time_factor, float) 133 | 134 | self._point_visit_factor = point_visit_factor 135 | self._move_walking_factor = move_walking_factor 136 | self._move_driving_factor = move_driving_factor 137 | self._move_ptt_factor = move_ptt_factor 138 | self._lunch_factor = lunch_factor 139 | self._no_point_visit_factor = no_point_visit_factor 140 | self._no_point_visit_const = no_point_visit_const 141 | self._unused_time_factor = unused_time_factor 142 | 143 | def Generate(self): 144 | return FactorCostAccumulator( 145 | point_visit_factor=self._point_visit_factor, 146 | move_walking_factor=self._move_walking_factor, 147 | move_driving_factor=self._move_driving_factor, 148 | move_ptt_factor=self._move_ptt_factor, 149 | lunch_factor=self._lunch_factor, 150 | no_point_visit_factor=self._no_point_visit_factor, 151 | no_point_visit_const=self._no_point_visit_const, 152 | unused_time_factor=self._unused_time_factor) 153 | -------------------------------------------------------------------------------- /router/cost_accumulator_test.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | import os 3 | import unittest 4 | 5 | from data import read_csv 6 | from data import point 7 | from data import city_visit 8 | from router import cost_accumulator as cost_accumulator_ 9 | 10 | 11 | class FactorCostAccumulatorTest(unittest.TestCase): 12 | 13 | def setUp(self): 14 | self.san_francisco_coordinates = point.Coordinates(37.7833, -122.4167) 15 | self.points = read_csv.ReadCSVToDict(os.path.join('data', 'test_sf_1.csv')) 16 | super(FactorCostAccumulatorTest, self).setUp() 17 | 18 | 19 | def testDefaults(self): 20 | cost_accumulator = cost_accumulator_.FactorCostAccumulatorGenerator().Generate() 21 | self.assertEqual(0., cost_accumulator.Cost()) 22 | cost_accumulator.AddMoveBetween( 23 | city_visit.MoveDescription( 24 | self.san_francisco_coordinates, 25 | self.points['Ferry Building'].coordinates_starts, 26 | 1.25, city_visit.MoveType.walking)) 27 | self.assertEqual(1.25, cost_accumulator.Cost()) 28 | cost_accumulator.AddPointVisit(self.points['Ferry Building']) 29 | self.assertEqual(2.25, cost_accumulator.Cost()) 30 | cost_accumulator.AddLunch(1.0) 31 | self.assertEqual(3.25, cost_accumulator.Cost()) 32 | cost_accumulator.AddMoveBetween( 33 | city_visit.MoveDescription( 34 | self.points['Ferry Building'].coordinates_ends, 35 | self.points['Pier 39'].coordinates_starts, 36 | 0.05, city_visit.MoveType.driving)) 37 | self.assertEqual(3.30, cost_accumulator.Cost()) 38 | cost_accumulator.AddPointVisit(self.points['Pier 39']) 39 | self.assertEqual(6.30, cost_accumulator.Cost()) 40 | cost_accumulator.AddMoveBetween( 41 | city_visit.MoveDescription( 42 | self.points['Pier 39'].coordinates_ends, 43 | self.san_francisco_coordinates, 44 | 0.50, city_visit.MoveType.ptt)) 45 | self.assertEqual(6.80, cost_accumulator.Cost()) 46 | cost_accumulator.AddPointLeft(self.points['Golden Gate Bridge']) 47 | self.assertEqual(7.30, cost_accumulator.Cost()) 48 | cost_accumulator.AddUnusedTime(timedelta(hours=0.005)) 49 | self.assertAlmostEqual(7.60, cost_accumulator.Cost()) 50 | cost_accumulator.AddUnusedTime(timedelta(seconds=30)) 51 | self.assertAlmostEqual(8.10, cost_accumulator.Cost()) 52 | 53 | def testGeneral(self): 54 | cost_accumulator = cost_accumulator_.FactorCostAccumulatorGenerator( 55 | point_visit_factor=0.5, 56 | move_walking_factor=1., 57 | move_driving_factor=2., 58 | move_ptt_factor=3., 59 | lunch_factor=0.25, 60 | no_point_visit_factor=10., 61 | no_point_visit_const=100., 62 | unused_time_factor=2.).Generate() 63 | self.assertEqual(0., cost_accumulator.Cost()) 64 | cost_accumulator.AddMoveBetween( 65 | city_visit.MoveDescription( 66 | self.san_francisco_coordinates, 67 | self.points['Ferry Building'].coordinates_starts, 68 | 1.25, city_visit.MoveType.walking)) 69 | self.assertEqual(1.25, cost_accumulator.Cost()) 70 | cost_accumulator.AddPointVisit(self.points['Ferry Building']) 71 | self.assertEqual(1.75, cost_accumulator.Cost()) 72 | cost_accumulator.AddLunch(1.0) 73 | self.assertEqual(2.00, cost_accumulator.Cost()) 74 | cost_accumulator.AddMoveBetween( 75 | city_visit.MoveDescription( 76 | self.points['Ferry Building'].coordinates_ends, 77 | self.points['Pier 39'].coordinates_starts, 78 | 0.05, city_visit.MoveType.driving)) 79 | self.assertEqual(2.10, cost_accumulator.Cost()) 80 | cost_accumulator.AddPointVisit(self.points['Pier 39']) 81 | self.assertEqual(3.60, cost_accumulator.Cost()) 82 | cost_accumulator.AddMoveBetween( 83 | city_visit.MoveDescription( 84 | self.points['Pier 39'].coordinates_ends, 85 | self.san_francisco_coordinates, 86 | 0.50, city_visit.MoveType.ptt)) 87 | self.assertEqual(5.10, cost_accumulator.Cost()) 88 | cost_accumulator.AddPointLeft(self.points['Golden Gate Bridge']) 89 | self.assertEqual(110.10, cost_accumulator.Cost()) 90 | cost_accumulator.AddUnusedTime(timedelta(hours=0.005)) 91 | self.assertAlmostEqual(110.70, cost_accumulator.Cost()) 92 | cost_accumulator.AddUnusedTime(timedelta(seconds=30)) 93 | self.assertAlmostEqual(111.70, cost_accumulator.Cost()) 94 | 95 | 96 | if __name__ == '__main__': 97 | unittest.main() 98 | -------------------------------------------------------------------------------- /router/day_visit_cost_calculator.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import datetime 3 | 4 | from data import point as point_ 5 | from data import city_visit 6 | from router import day_visit_cost_calculator_interface 7 | from router import move_calculator as move_calculator_ 8 | from router import point_fit as point_fit_ 9 | from router import cost_accumulator as cost_accumulator_ 10 | 11 | 12 | class DayVisitCostCalculatorState(object): 13 | def __init__(self, current_datetime, current_coordinates, cost_accumulator, 14 | actions): 15 | self.current_datetime = current_datetime 16 | self.current_coordinates = current_coordinates 17 | self.cost_accumulator = cost_accumulator 18 | self.actions = actions 19 | 20 | @classmethod 21 | def Init(cls, day_visit_parameters, cost_accumulator_generator): 22 | return cls( 23 | day_visit_parameters.start_datetime, 24 | day_visit_parameters.start_coordinates, 25 | cost_accumulator_generator.Generate(), 26 | []) 27 | 28 | def Copy(self): 29 | return self.__class__( 30 | copy.deepcopy(self.current_datetime), 31 | self.current_coordinates.Copy(), 32 | self.cost_accumulator.Copy(), 33 | self.actions[:]) 34 | 35 | 36 | class ActionAdderInterface(object): 37 | 38 | def StartEndDatetime(self, current_state): 39 | raise NotImplemented() 40 | 41 | def Add(self, current_state): 42 | raise NotImplemented() 43 | 44 | 45 | class PointVisitAdder(ActionAdderInterface): 46 | 47 | def __init__(self, calculator, point): 48 | self.calculator = calculator 49 | self.point = point 50 | 51 | def StartEndDatetime(self, current_state): 52 | visit_timedelta = datetime.timedelta(hours=self.point.duration) 53 | visit_start_end_datetime = city_visit.StartEndDatetime( 54 | current_state.current_datetime, 55 | current_state.current_datetime + visit_timedelta) 56 | return visit_start_end_datetime 57 | 58 | def Add(self, current_state): 59 | visit_start_end_datetime = self.StartEndDatetime(current_state) 60 | if not self.calculator.point_fit.IfPointFit( 61 | visit_start_end_datetime, self.point.operating_hours): 62 | return False 63 | if (visit_start_end_datetime.end > 64 | self.calculator.day_visit_parameters.end_datetime): 65 | return False 66 | current_state.current_datetime = visit_start_end_datetime.end 67 | current_state.current_coordinates = self.point.coordinates_ends 68 | current_state.cost_accumulator.AddPointVisit(self.point) 69 | current_state.actions.append(city_visit.PointVisit(visit_start_end_datetime, self.point)) 70 | return True 71 | 72 | 73 | class MoveBetweenAdder(ActionAdderInterface): 74 | 75 | def __init__(self, calculator, to_coordinates): 76 | self.calculator = calculator 77 | self.to_coordinates = to_coordinates 78 | 79 | def StartEndDatetime(self, current_state): 80 | move_description = ( 81 | self.calculator.move_calculator.CalculateMoveDescription( 82 | current_state.current_coordinates, self.to_coordinates)) 83 | move_timedelta = datetime.timedelta(hours=move_description.move_hours) 84 | move_start_end_datetime = city_visit.StartEndDatetime( 85 | current_state.current_datetime, 86 | current_state.current_datetime + move_timedelta) 87 | return move_start_end_datetime 88 | 89 | def Add(self, current_state): 90 | move_description = ( 91 | self.calculator.move_calculator.CalculateMoveDescription( 92 | current_state.current_coordinates, self.to_coordinates)) 93 | move_timedelta = datetime.timedelta(hours=move_description.move_hours) 94 | move_start_end_datetime = city_visit.StartEndDatetime( 95 | current_state.current_datetime, 96 | current_state.current_datetime + move_timedelta) 97 | if (move_start_end_datetime.end > 98 | self.calculator.day_visit_parameters.end_datetime): 99 | return False 100 | current_state.current_datetime = move_start_end_datetime.end 101 | current_state.current_coordinates = move_description.to_coordinates 102 | current_state.cost_accumulator.AddMoveBetween(move_description) 103 | current_state.actions.append(city_visit.MoveBetween(move_start_end_datetime, move_description)) 104 | return True 105 | 106 | 107 | class LunchAdder(ActionAdderInterface): 108 | 109 | def __init__(self, calculator): 110 | self.calculator = calculator 111 | 112 | def StartEndDatetime(self, current_state): 113 | lunch_timedelta = datetime.timedelta( 114 | hours=self.calculator.day_visit_parameters.lunch_hours) 115 | lunch_start_end_datetime = city_visit.StartEndDatetime( 116 | current_state.current_datetime, 117 | current_state.current_datetime + lunch_timedelta) 118 | return lunch_start_end_datetime 119 | 120 | def Add(self, current_state): 121 | lunch_start_end_datetime = self.StartEndDatetime(current_state) 122 | if (lunch_start_end_datetime.end > 123 | self.calculator.day_visit_parameters.end_datetime): 124 | return False 125 | current_state.current_datetime = lunch_start_end_datetime.end 126 | current_state.cost_accumulator.AddLunch( 127 | self.calculator.day_visit_parameters.lunch_hours) 128 | current_state.actions.append(city_visit.Lunch(lunch_start_end_datetime, 129 | self.calculator.day_visit_parameters.lunch_hours)) 130 | return True 131 | 132 | 133 | # NOTE(igushev): PushPoint responsible for checking point itself, move to this 134 | # point and if day visit can be finalized. After first such point can't be 135 | # pushed, none of following will be pushed, since higher level modules should 136 | # be responsible for permutation. 137 | class DayVisitCostCalculator(day_visit_cost_calculator_interface.DayVisitCostCalculatorInterface): 138 | """Constructs DayVisit and calculates its cost.""" 139 | 140 | def __init__(self, move_calculator, point_fit, day_visit_parameters, 141 | current_state, points_left): 142 | self.move_calculator = move_calculator 143 | self.point_fit = point_fit 144 | self.day_visit_parameters = day_visit_parameters 145 | self.current_state = current_state 146 | self.points_left = points_left 147 | 148 | @classmethod 149 | def Init(cls, move_calculator, point_fit, day_visit_parameters, 150 | cost_accumulator_generator): 151 | return cls( 152 | move_calculator, 153 | point_fit, 154 | day_visit_parameters, 155 | DayVisitCostCalculatorState.Init( 156 | day_visit_parameters, cost_accumulator_generator), 157 | []) 158 | 159 | def Copy(self): 160 | return self.__class__( 161 | self.move_calculator, 162 | self.point_fit, 163 | self.day_visit_parameters, 164 | self.current_state.Copy(), 165 | self.points_left[:]) 166 | 167 | def _AddActionAndLunch(self, action_adder, current_state): 168 | action_start_end_datetime = action_adder.StartEndDatetime(current_state) 169 | if action_start_end_datetime.Fit( 170 | self.day_visit_parameters.lunch_start_datetime): 171 | # We have lunch during action and should add it. 172 | gap_between_start_and_lunch = ( 173 | self.day_visit_parameters.lunch_start_datetime - 174 | action_start_end_datetime.start) 175 | gap_between_lunch_and_end = ( 176 | action_start_end_datetime.end - 177 | self.day_visit_parameters.lunch_start_datetime) 178 | lunch_adder = LunchAdder(self) 179 | if gap_between_start_and_lunch > gap_between_lunch_and_end: 180 | # It's better to have lunch after action. 181 | if not action_adder.Add(current_state): 182 | return False 183 | if not lunch_adder.Add(current_state): 184 | return False 185 | else: 186 | # It's better to have lunch before action. 187 | if not lunch_adder.Add(current_state): 188 | return False 189 | if not action_adder.Add(current_state): 190 | return False 191 | return True 192 | else: 193 | # We don't have lunch during action. Just add action. 194 | return action_adder.Add(current_state) 195 | 196 | def _CanPushPoint(self, point, current_state): 197 | move_between_adder = MoveBetweenAdder(self, point.coordinates_starts) 198 | if not self._AddActionAndLunch(move_between_adder, current_state): 199 | return False 200 | point_visit_adder = PointVisitAdder(self, point) 201 | if not self._AddActionAndLunch(point_visit_adder, current_state): 202 | return False 203 | finalized_current_state = current_state.Copy() 204 | finalize_action_adder = ( 205 | MoveBetweenAdder(self, self.day_visit_parameters.end_coordinates)) 206 | if not self._AddActionAndLunch( 207 | finalize_action_adder, finalized_current_state): 208 | return False 209 | return True 210 | 211 | def PushPoint(self, point): 212 | assert isinstance(point, point_.PointInterface) 213 | 214 | # If self.points_left is empty, it means no point has not been pushed, so 215 | # we can proceed. 216 | if not self.points_left: 217 | current_state = self.current_state.Copy() 218 | can_push = self._CanPushPoint(point, current_state) 219 | else: 220 | can_push = False 221 | 222 | if can_push: 223 | self.current_state = current_state 224 | return True 225 | else: 226 | self.current_state.cost_accumulator.AddPointLeft(point) 227 | self.points_left.append(point) 228 | return False 229 | 230 | def _FinalizedCurrentState(self): 231 | finalized_current_state = self.current_state.Copy() 232 | finalize_action_adder = ( 233 | MoveBetweenAdder(self, self.day_visit_parameters.end_coordinates)) 234 | assert self._AddActionAndLunch( 235 | finalize_action_adder, finalized_current_state), ( 236 | 'Finalizing Move must be able to be added.') 237 | unused_time = (self.day_visit_parameters.end_datetime - 238 | finalized_current_state.current_datetime) 239 | finalized_current_state.cost_accumulator.AddUnusedTime(unused_time) 240 | return finalized_current_state 241 | 242 | def FinalizedCost(self): 243 | finalized_current_state = self._FinalizedCurrentState() 244 | return finalized_current_state.cost_accumulator.Cost() 245 | 246 | def FinalizedDayVisit(self): 247 | finalized_current_state = self._FinalizedCurrentState() 248 | return city_visit.DayVisit( 249 | self.day_visit_parameters.start_datetime, 250 | finalized_current_state.actions, 251 | finalized_current_state.cost_accumulator.Cost()) 252 | 253 | def GetPointsLeft(self): 254 | return self.points_left 255 | 256 | # NOTE(igushev): Methods below are not part of Interface API. 257 | def CurrentTime(self): 258 | return self.current_state.current_datetime 259 | 260 | def CurrentCoordinates(self): 261 | return self.current_state.current_coordinates 262 | 263 | def CurrentCost(self): 264 | return self.current_state.cost_accumulator.Cost() 265 | 266 | 267 | class DayVisitCostCalculatorGenerator(day_visit_cost_calculator_interface.DayVisitCostCalculatorGeneratorInterface): 268 | """Returns every time new clean instance of DayVisitCostCalculator.""" 269 | 270 | def __init__(self, move_calculator, point_fit, cost_accumulator_generator): 271 | assert isinstance(move_calculator, move_calculator_.MoveCalculatorInterface) 272 | assert isinstance(point_fit, point_fit_.PointFitInterface) 273 | assert isinstance(cost_accumulator_generator, 274 | cost_accumulator_.CostAccumulatorGeneratorInterface) 275 | 276 | self.move_calculator = move_calculator 277 | self.point_fit = point_fit 278 | self.cost_accumulator_generator = cost_accumulator_generator 279 | 280 | def Generate(self, day_visit_parameters): 281 | assert isinstance(day_visit_parameters, city_visit.DayVisitParametersInterface) 282 | 283 | return DayVisitCostCalculator.Init( 284 | self.move_calculator, self.point_fit, day_visit_parameters, 285 | self.cost_accumulator_generator) 286 | -------------------------------------------------------------------------------- /router/day_visit_cost_calculator_interface.py: -------------------------------------------------------------------------------- 1 | class DayVisitCostCalculatorInterface(object): 2 | """Abstract class which constructs DayVisit and calculates its cost. 3 | 4 | If a Point cannot be fit into DayVisit, Calculator stops accepting new points 5 | (even if a following Point can be fit) and just collects left points. 6 | """ 7 | 8 | def Copy(self): 9 | raise NotImplemented() 10 | 11 | def PushPoint(self, point): 12 | """Try to push a new point to the DayVisit.""" 13 | raise NotImplemented() 14 | 15 | def FinalizedCost(self): 16 | """DayVisit cost including returning to end coordinates.""" 17 | raise NotImplemented() 18 | 19 | def FinalizedDayVisit(self): 20 | """DayVisit including returning to end coordinates.""" 21 | raise NotImplemented() 22 | 23 | def GetPointsLeft(self): 24 | """Get points which cannot be fit into DayVisit.""" 25 | raise NotImplemented() 26 | 27 | 28 | class DayVisitCostCalculatorGeneratorInterface(object): 29 | """Abstract class which returns every time new clean instance of 30 | DayVisitCostCalculatorInterface.""" 31 | 32 | def Generate(self, day_visit_parameters): 33 | """Generate new clean instance of DayVisitCostCalculatorInterface.""" 34 | raise NotImplemented() 35 | -------------------------------------------------------------------------------- /router/day_visit_heap.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | 3 | 4 | PointsCalculator = namedtuple('PointsCalculator', 'Points Calculator') 5 | 6 | 7 | class DayVisitHeap(object): 8 | """Keeps track of the best DayVisits.""" 9 | 10 | def __init__(self, max_count): 11 | assert isinstance(max_count, int) 12 | 13 | self.max_count = max_count 14 | self._points_calculator_list = [] 15 | self._invariant = True 16 | 17 | def Append(self, points_calculator): 18 | assert isinstance(points_calculator, PointsCalculator) 19 | 20 | if self._points_calculator_list: 21 | assert (len(self._points_calculator_list[0].Points) == 22 | len(points_calculator.Points)), ( 23 | 'Points count must be the same in the all heap') 24 | self._points_calculator_list.append(points_calculator) 25 | self._invariant = False 26 | 27 | def Shrink(self): 28 | self._points_calculator_list = ( 29 | sorted(self._points_calculator_list, 30 | key=lambda points_calculator: ( 31 | points_calculator.Calculator.FinalizedCost()))[:self.max_count]) 32 | self._invariant = True 33 | 34 | def GetPointsCalculatorList(self): 35 | assert self._invariant, ( 36 | 'PointsCalculator list cannot be returned. Please call Shrink first.') 37 | return self._points_calculator_list 38 | 39 | def Size(self): 40 | return len(self._points_calculator_list) 41 | 42 | def Clear(self): 43 | self._points_calculator_list = [] 44 | self._invariant = True 45 | -------------------------------------------------------------------------------- /router/day_visit_heap_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from data import point 4 | from router import day_visit_heap as day_visit_heap_ 5 | from router import day_visit_cost_calculator_interface 6 | 7 | 8 | class MockPoint(point.PointInterface): 9 | pass 10 | 11 | 12 | class MockDayVisitCostCalculator(day_visit_cost_calculator_interface.DayVisitCostCalculatorInterface): 13 | def __init__(self, cost): 14 | self.cost = cost 15 | 16 | def FinalizedCost(self): 17 | return self.cost 18 | 19 | 20 | class DayVisitHeapTest(unittest.TestCase): 21 | 22 | def testGeneral(self): 23 | day_visit_heap = day_visit_heap_.DayVisitHeap(2) 24 | self.assertEqual(0, day_visit_heap.Size()) 25 | self.assertEqual([], day_visit_heap.GetPointsCalculatorList()) 26 | 27 | visit_a = day_visit_heap_.PointsCalculator([MockPoint(), MockPoint()], 28 | MockDayVisitCostCalculator(1)) 29 | day_visit_heap.Append(visit_a) 30 | self.assertEqual(1, day_visit_heap.Size()) 31 | self.assertRaises(AssertionError, day_visit_heap.GetPointsCalculatorList) 32 | 33 | visit_b = day_visit_heap_.PointsCalculator([MockPoint(), MockPoint()], 34 | MockDayVisitCostCalculator(9)) 35 | day_visit_heap.Append(visit_b) 36 | self.assertEqual(2, day_visit_heap.Size()) 37 | self.assertRaises(AssertionError, day_visit_heap.GetPointsCalculatorList) 38 | 39 | # Should evict visit_b. 40 | visit_c = day_visit_heap_.PointsCalculator([MockPoint(), MockPoint()], 41 | MockDayVisitCostCalculator(5)) 42 | day_visit_heap.Append(visit_c) 43 | self.assertEqual(3, day_visit_heap.Size()) 44 | self.assertRaises(AssertionError, day_visit_heap.GetPointsCalculatorList) 45 | 46 | day_visit_heap.Shrink() 47 | self.assertEqual(2, day_visit_heap.Size()) 48 | self.assertEqual([visit_a, visit_c], 49 | day_visit_heap.GetPointsCalculatorList()) 50 | 51 | # Should evict visit_c 52 | visit_d = day_visit_heap_.PointsCalculator([MockPoint(), MockPoint()], 53 | MockDayVisitCostCalculator(3)) 54 | day_visit_heap.Append(visit_d) 55 | self.assertEqual(3, day_visit_heap.Size()) 56 | self.assertRaises(AssertionError, day_visit_heap.GetPointsCalculatorList) 57 | 58 | visit_e = day_visit_heap_.PointsCalculator([MockPoint(), MockPoint()], 59 | MockDayVisitCostCalculator(7)) 60 | day_visit_heap.Append(visit_e) 61 | self.assertEqual(4, day_visit_heap.Size()) 62 | self.assertRaises(AssertionError, day_visit_heap.GetPointsCalculatorList) 63 | 64 | day_visit_heap.Shrink() 65 | self.assertEqual(2, day_visit_heap.Size()) 66 | self.assertEqual([visit_a, visit_d], 67 | day_visit_heap.GetPointsCalculatorList()) 68 | 69 | # Number of points in inconsistent. 70 | visit_f = day_visit_heap_.PointsCalculator([MockPoint(), MockPoint(), MockPoint()], 71 | MockDayVisitCostCalculator(1)) 72 | self.assertRaises(AssertionError, day_visit_heap.Append, visit_f) 73 | self.assertEqual(2, day_visit_heap.Size()) 74 | self.assertEqual([visit_a, visit_d], 75 | day_visit_heap.GetPointsCalculatorList()) 76 | 77 | day_visit_heap.Clear() 78 | self.assertEqual(0, day_visit_heap.Size()) 79 | self.assertEqual([], day_visit_heap.GetPointsCalculatorList()) 80 | 81 | 82 | 83 | if __name__ == '__main__': 84 | unittest.main() 85 | -------------------------------------------------------------------------------- /router/day_visit_router.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | import copy 3 | 4 | from data import point as point_ 5 | from data import city_visit 6 | from router import day_visit_heap 7 | from router import day_visit_cost_calculator_interface 8 | 9 | 10 | class DayVisitRouterInterface(object): 11 | """Abstract class which routes points during particular day.""" 12 | 13 | def RouteDayVisit(self, all_points, day_visit_parameters): 14 | """Route maximum number of points with minimum cost for DayVisit.""" 15 | raise NotImplemented() 16 | 17 | 18 | class DayVisitRouter(DayVisitRouterInterface): 19 | """Routes points during particular day using permutation and keeping track of 20 | best so far.""" 21 | 22 | def __init__(self, calculator_generator, day_visit_heap_size): 23 | assert isinstance(calculator_generator, 24 | day_visit_cost_calculator_interface.DayVisitCostCalculatorGeneratorInterface) 25 | assert isinstance(day_visit_heap_size, int) 26 | 27 | self.calculator_generator = calculator_generator 28 | self.day_visit_heap_size = day_visit_heap_size 29 | 30 | # TODO(igushev): Use set instead of list for Points. 31 | def RouteDayVisit(self, all_points, day_visit_parameters): 32 | for point in all_points: 33 | assert isinstance(point, point_.PointInterface) 34 | assert isinstance(day_visit_parameters, city_visit.DayVisitParametersInterface) 35 | 36 | points_calculator_heap = day_visit_heap.DayVisitHeap(self.day_visit_heap_size) 37 | points_calculator_heap.Append( 38 | day_visit_heap.PointsCalculator( 39 | all_points, 40 | self.calculator_generator.Generate(day_visit_parameters))) 41 | points_calculator_heap.Shrink() 42 | while True: 43 | next_points_calculator_heap = day_visit_heap.DayVisitHeap(self.day_visit_heap_size) 44 | pushed_to_next = [] 45 | for points, calculator in ( 46 | points_calculator_heap.GetPointsCalculatorList()): 47 | for i, point in enumerate(points): 48 | next_calculator = calculator.Copy() 49 | pushed_to_next.append(next_calculator.PushPoint(point)) 50 | next_points = points[:i] + points[i+1:] # -= point 51 | next_points_calculator_heap.Append( 52 | day_visit_heap.PointsCalculator(next_points, next_calculator)) 53 | next_points_calculator_heap.Shrink() 54 | prev_points_calculator_heap = points_calculator_heap 55 | points_calculator_heap = next_points_calculator_heap 56 | if not any(pushed_to_next): 57 | break 58 | 59 | # Return the best. 60 | assert prev_points_calculator_heap.Size() >= 1 61 | points_left, calculator_best = ( 62 | prev_points_calculator_heap.GetPointsCalculatorList()[0]) 63 | return (calculator_best.FinalizedDayVisit(), 64 | calculator_best.GetPointsLeft() + points_left) 65 | -------------------------------------------------------------------------------- /router/day_visit_router_test.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import unittest 3 | 4 | from data import city_visit 5 | from router import cost_accumulator 6 | from router import day_visit_router 7 | from router import point_fit as point_fit_ 8 | from router import day_visit_cost_calculator 9 | from router import test_util 10 | 11 | 12 | class DayVisitRouterTest(unittest.TestCase): 13 | 14 | @staticmethod 15 | def GetDayVisitParameters(start_datetime, end_datetime): 16 | return city_visit.DayVisitParameters( 17 | start_datetime=start_datetime, 18 | end_datetime=end_datetime, 19 | lunch_start_datetime=datetime.datetime( 20 | start_datetime.year, start_datetime.month, start_datetime.day, 21 | 13, 0, 0), 22 | lunch_hours=1., 23 | start_coordinates=test_util.MockCoordinates('Hotel'), 24 | end_coordinates=test_util.MockCoordinates('Restaurant')) 25 | 26 | def setUp(self): 27 | no_point_visit_factor = 0. 28 | no_point_visit_const = 1000. 29 | unused_time_factor = 0.01 30 | day_visit_heap_size = 1000 31 | self.points = test_util.MockPoints() 32 | move_calculator = test_util.MockMoveCalculator() 33 | point_fit = point_fit_.SimplePointFit() 34 | cost_accumulator_generator=cost_accumulator.FactorCostAccumulatorGenerator( 35 | no_point_visit_factor=no_point_visit_factor, 36 | no_point_visit_const=no_point_visit_const, 37 | unused_time_factor=unused_time_factor) 38 | day_visit_cost_calculator_generator = day_visit_cost_calculator.DayVisitCostCalculatorGenerator( 39 | move_calculator=move_calculator, 40 | point_fit=point_fit, 41 | cost_accumulator_generator=cost_accumulator_generator) 42 | self.day_visit_router = day_visit_router.DayVisitRouter( 43 | calculator_generator=day_visit_cost_calculator_generator, 44 | day_visit_heap_size=day_visit_heap_size) 45 | super(DayVisitRouterTest, self).setUp() 46 | 47 | 48 | def testTwoFitTwoLeft(self): 49 | day_visit_parameters = DayVisitRouterTest.GetDayVisitParameters( 50 | start_datetime=datetime.datetime(2014, 9, 1, 9, 0, 0), 51 | end_datetime=datetime.datetime(2014, 9, 1, 22, 0, 0)) 52 | 53 | day_visit_best, points_left = self.day_visit_router.RouteDayVisit( 54 | [self.points['Ferry Building'], 55 | self.points['Pier 39'], 56 | self.points['Golden Gate Bridge'], 57 | self.points['Twin Peaks']], 58 | day_visit_parameters) 59 | 60 | day_visit_best_str_expected = """Date: 2014-09-01 61 | Walking from Hotel to Ferry Building from 09:00:00 to 10:00:00 62 | Visiting point "Ferry Building" from 10:00:00 to 11:00:00 63 | Having lunch from 11:00:00 to 12:00:00 64 | Walking from Ferry Building to Twin Peaks from 12:00:00 to 17:00:00 65 | Visiting point "Twin Peaks" from 17:00:00 to 17:30:00 66 | Walking from Twin Peaks to Restaurant from 17:30:00 to 19:30:00 67 | Cost: 12.00 68 | Price: 0.00""" 69 | self.assertEqual(day_visit_best_str_expected, str(day_visit_best)) 70 | self.assertEqual( 71 | [self.points['Pier 39'], 72 | self.points['Golden Gate Bridge']], 73 | points_left) 74 | 75 | def testEverythingFit(self): 76 | day_visit_parameters = DayVisitRouterTest.GetDayVisitParameters( 77 | start_datetime=datetime.datetime(2014, 9, 1, 9, 0, 0), 78 | end_datetime=datetime.datetime(2014, 9, 1, 22, 0, 0)) 79 | 80 | day_visit_best, points_left = self.day_visit_router.RouteDayVisit( 81 | [self.points['Ferry Building'], 82 | self.points['Pier 39']], 83 | day_visit_parameters) 84 | 85 | day_visit_best_str_expected = """Date: 2014-09-01 86 | Walking from Hotel to Ferry Building from 09:00:00 to 10:00:00 87 | Visiting point "Ferry Building" from 10:00:00 to 11:00:00 88 | Walking from Ferry Building to Pier 39 from 11:00:00 to 12:00:00 89 | Having lunch from 12:00:00 to 13:00:00 90 | Visiting point "Pier 39" from 13:00:00 to 16:00:00 91 | Walking from Pier 39 to Restaurant from 16:00:00 to 20:00:00 92 | Cost: 12.20 93 | Price: 0.00""" 94 | self.assertEqual(day_visit_best_str_expected, str(day_visit_best)) 95 | self.assertEqual([], points_left) 96 | 97 | def testNothingFit(self): 98 | day_visit_parameters = DayVisitRouterTest.GetDayVisitParameters( 99 | start_datetime=datetime.datetime(2014, 9, 1, 9, 0, 0), 100 | end_datetime=datetime.datetime(2014, 9, 1, 10, 30, 0)) 101 | 102 | day_visit_best, points_left = self.day_visit_router.RouteDayVisit( 103 | [self.points['Ferry Building'], 104 | self.points['Pier 39'], 105 | self.points['Golden Gate Bridge'], 106 | self.points['Twin Peaks']], 107 | day_visit_parameters) 108 | 109 | day_visit_best_str_expected = """Date: 2014-09-01 110 | Walking from Hotel to Restaurant from 09:00:00 to 10:00:00 111 | Cost: 1.30 112 | Price: 0.00""" 113 | self.assertEqual(day_visit_best_str_expected, str(day_visit_best)) 114 | self.assertEqual( 115 | [self.points['Ferry Building'], 116 | self.points['Pier 39'], 117 | self.points['Golden Gate Bridge'], 118 | self.points['Twin Peaks']], 119 | points_left) 120 | 121 | def testNoPoints(self): 122 | day_visit_parameters = DayVisitRouterTest.GetDayVisitParameters( 123 | start_datetime=datetime.datetime(2014, 9, 1, 9, 0, 0), 124 | end_datetime=datetime.datetime(2014, 9, 1, 22, 0, 0)) 125 | 126 | day_visit_best, points_left = self.day_visit_router.RouteDayVisit( 127 | [], 128 | day_visit_parameters) 129 | 130 | day_visit_best_str_expected = """Date: 2014-09-01 131 | Walking from Hotel to Restaurant from 09:00:00 to 10:00:00 132 | Cost: 8.20 133 | Price: 0.00""" 134 | self.assertEqual(day_visit_best_str_expected, str(day_visit_best)) 135 | self.assertEqual([], points_left) 136 | 137 | 138 | if __name__ == '__main__': 139 | unittest.main() 140 | -------------------------------------------------------------------------------- /router/days_permutations.py: -------------------------------------------------------------------------------- 1 | import collections 2 | 3 | 4 | class DaysToPoints(collections.defaultdict): 5 | 6 | def __init__(self, *args, **kwargs): 7 | super(DaysToPoints, self).__init__(list, *args, **kwargs) 8 | 9 | def Copy(self): 10 | return DaysToPoints(((i, points[:]) for i, points in self.items())) 11 | 12 | 13 | def DaysPermutations(points, days_consider): 14 | """Returns list of potential permutations. Each permutation is a dictionary 15 | of day number to points to visit that day.""" 16 | results = [DaysToPoints()] 17 | for point in points: 18 | next_results = [] 19 | for result in results: 20 | for i in range(len(days_consider)): 21 | if not days_consider[i]: 22 | continue 23 | next_result = result.Copy() 24 | next_result[i].append(point) 25 | next_results.append(next_result) 26 | results = next_results 27 | return results -------------------------------------------------------------------------------- /router/days_permutations_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from data import point 4 | from router import days_permutations 5 | 6 | 7 | class MockPoint(point.PointInterface): 8 | pass 9 | 10 | 11 | class DaysPermutationsTest(unittest.TestCase): 12 | 13 | def testOnePoint(self): 14 | a = MockPoint() 15 | actual = days_permutations.DaysPermutations([a], [True, True, True]) 16 | self.assertEqual(3, len(actual)) 17 | self.assertEqual({0: [a]}, dict(actual[0])) 18 | self.assertEqual({1: [a]}, dict(actual[1])) 19 | self.assertEqual({2: [a]}, dict(actual[2])) 20 | 21 | def testTwoPoint(self): 22 | a, b = MockPoint(), MockPoint() 23 | actual = days_permutations.DaysPermutations([a, b], [True, True, True]) 24 | self.assertEqual(9, len(actual)) 25 | self.assertEqual({0: [a, b]}, dict(actual[0])) 26 | self.assertEqual({0: [a], 1: [b]}, dict(actual[1])) 27 | self.assertEqual({0: [a], 2: [b]}, dict(actual[2])) 28 | self.assertEqual({1: [a], 0: [b]}, dict(actual[3])) 29 | self.assertEqual({1: [a, b]}, dict(actual[4])) 30 | self.assertEqual({1: [a], 2: [b]}, dict(actual[5])) 31 | self.assertEqual({2: [a], 0: [b]}, dict(actual[6])) 32 | self.assertEqual({2: [a], 1: [b]}, dict(actual[7])) 33 | self.assertEqual({2: [a, b]}, dict(actual[8])) 34 | 35 | def testThreePoints(self): 36 | a, b, c = MockPoint(), MockPoint(), MockPoint() 37 | actual = days_permutations.DaysPermutations([a, b, c], [True, False, True]) 38 | self.assertEqual(8, len(actual)) 39 | self.assertEqual({0: [a, b, c]}, dict(actual[0])) 40 | self.assertEqual({0: [a, b], 2: [c]}, dict(actual[1])) 41 | self.assertEqual({0: [a, c], 2: [b]}, dict(actual[2])) 42 | self.assertEqual({0: [a], 2: [b, c]}, dict(actual[3])) 43 | self.assertEqual({0: [b, c], 2: [a]}, dict(actual[4])) 44 | self.assertEqual({0: [b], 2: [a, c]}, dict(actual[5])) 45 | self.assertEqual({0: [c], 2: [a, b]}, dict(actual[6])) 46 | self.assertEqual({2: [a, b, c]}, dict(actual[7])) 47 | 48 | def testOneDay(self): 49 | a, b, c = MockPoint(), MockPoint(), MockPoint() 50 | actual = days_permutations.DaysPermutations([a, b, c], [True]) 51 | self.assertEqual(1, len(actual)) 52 | self.assertEqual({0: [a, b, c]}, dict(actual[0])) 53 | 54 | 55 | if __name__ == '__main__': 56 | unittest.main() 57 | -------------------------------------------------------------------------------- /router/move_calculator.py: -------------------------------------------------------------------------------- 1 | # NOTE(igushev): Source http://www.movable-type.co.uk/scripts/latlong.html 2 | # (Bearing) 3 | import math 4 | 5 | from data import point 6 | from data import city_visit 7 | 8 | 9 | R = 3959 # Earth radius in miles. 10 | 11 | 12 | def CalculateDistance(coordinates_from, coordinates_to): 13 | """Calculates distance in km/miles.""" 14 | assert isinstance(coordinates_from, point.CoordinatesInterface) 15 | assert isinstance(coordinates_to, point.CoordinatesInterface) 16 | 17 | lat_1 = math.radians(coordinates_from.latitude) 18 | lat_2 = math.radians(coordinates_to.latitude) 19 | delta_lat = math.radians(coordinates_to.latitude - coordinates_from.latitude) 20 | long_1 = math.radians(coordinates_from.longitude) 21 | long_2 = math.radians(coordinates_to.longitude) 22 | delta_long = math.radians( 23 | coordinates_to.longitude - coordinates_from.longitude) 24 | 25 | a = (math.sin(delta_lat/2) * math.sin(delta_lat/2) + 26 | math.cos(lat_1) * math.cos(lat_2) * 27 | math.sin(delta_long/2) * math.sin(delta_long/2)) 28 | c = 2 * math.atan2(math.sqrt(a), math.sqrt(1-a)) 29 | d = R * c 30 | return d 31 | 32 | 33 | def CalculateCityDistance(coordinates_from, coordinates_to): 34 | d = CalculateDistance(coordinates_from, coordinates_to) 35 | return d * math.sqrt(2) # Because most cities have grid streets. 36 | 37 | 38 | class MoveCalculatorInterface(object): 39 | """Abstract class which calculates move_description.""" 40 | 41 | def CalculateMoveDescription(self, coordinates_from, coordinates_to): 42 | """Calculate time and type to move from one coordinates to another.""" 43 | raise NotImplemented() 44 | 45 | 46 | class SimpleMoveCalculator(MoveCalculatorInterface): 47 | """Calculates move_description considering pause before moving.""" 48 | 49 | def __init__(self, speed, move_type, pause=0.): 50 | assert isinstance(speed, float) 51 | assert isinstance(move_type, int) 52 | assert isinstance(pause, float) 53 | 54 | self._speed = speed 55 | self._move_type = move_type 56 | self._pause = pause 57 | 58 | def CalculateMoveDescription(self, coordinates_from, coordinates_to): 59 | assert isinstance(coordinates_from, point.CoordinatesInterface) 60 | assert isinstance(coordinates_to, point.CoordinatesInterface) 61 | 62 | d = CalculateCityDistance(coordinates_from, coordinates_to) 63 | return city_visit.MoveDescription(coordinates_from, coordinates_to, 64 | (d / self._speed) + self._pause, self._move_type) 65 | 66 | 67 | class MultiMoveCalculator(MoveCalculatorInterface): 68 | """Calculates move_description differently depending on distance.""" 69 | 70 | def __init__(self, distances_splits, move_calculators): 71 | for move_calculator in move_calculators: 72 | assert isinstance(move_calculator, MoveCalculatorInterface) 73 | for distances_split in distances_splits: 74 | assert isinstance(distances_split, float) 75 | assert sorted(distances_splits) == distances_splits, ( 76 | 'Distances splits must be in sorted order.') 77 | assert len(move_calculators) == len(distances_splits) + 1, ( 78 | 'Number of MoveCalculatorInterface should exactly one more than' 79 | ' distances splits.') 80 | 81 | self._distances_splits = distances_splits 82 | self._move_calculators = move_calculators 83 | 84 | def CalculateMoveDescription(self, coordinates_from, coordinates_to): 85 | assert isinstance(coordinates_from, point.CoordinatesInterface) 86 | assert isinstance(coordinates_to, point.CoordinatesInterface) 87 | 88 | d = CalculateCityDistance(coordinates_from, coordinates_to) 89 | 90 | for i, distances_split in enumerate(self._distances_splits): 91 | if d < distances_split: 92 | move_calculator = self._move_calculators[i] 93 | break 94 | else: 95 | move_calculator = self._move_calculators[-1] 96 | 97 | return move_calculator.CalculateMoveDescription( 98 | coordinates_from, coordinates_to) 99 | -------------------------------------------------------------------------------- /router/multi_day_visit_cost_calculator.py: -------------------------------------------------------------------------------- 1 | from data import point as point_ 2 | from data import city_visit 3 | from router import day_visit_cost_calculator_interface 4 | 5 | 6 | class MultiDayVisitCostCalculator(day_visit_cost_calculator_interface.DayVisitCostCalculatorInterface): 7 | """Calculates the best among DayVisitCostCalculators. 8 | 9 | Keeps track of several DayVisitCostCalculators and chooses the best. 10 | """ 11 | 12 | def __init__(self, calculators): 13 | for calculator in calculators: 14 | assert isinstance(calculator, day_visit_cost_calculator_interface.DayVisitCostCalculatorInterface) 15 | 16 | self.calculators = calculators 17 | 18 | def Copy(self): 19 | return self.__class__( 20 | [calculator.Copy() for calculator in self.calculators]) 21 | 22 | def PushPoint(self, point): 23 | assert isinstance(point, point_.PointInterface) 24 | 25 | # Must create a list since any() would skip all calculator after first 26 | # successful push. 27 | return any([calculator.PushPoint(point) for calculator in self.calculators]) 28 | 29 | def FinalizedCost(self): 30 | return min(calculator.FinalizedCost() for calculator in self.calculators) 31 | 32 | def _GetMinIndex(self): 33 | # Get index of calculator with minimum FinalizedCost(). 34 | min_index, _ = min(enumerate(self.calculators), 35 | key=lambda index_calculator: index_calculator[1].FinalizedCost()) 36 | return min_index 37 | 38 | def FinalizedDayVisit(self): 39 | min_index = self._GetMinIndex() 40 | return self.calculators[min_index].FinalizedDayVisit() 41 | 42 | def GetPointsLeft(self): 43 | min_index = self._GetMinIndex() 44 | return self.calculators[min_index].GetPointsLeft() 45 | 46 | 47 | class MultiDayVisitCostCalculatorGenerator( 48 | day_visit_cost_calculator_interface.DayVisitCostCalculatorGeneratorInterface): 49 | """Returns every time new clean instance of MultiDayVisitCostCalculator.""" 50 | 51 | def __init__(self, calculator_generators): 52 | for calculator_generator in calculator_generators: 53 | assert isinstance(calculator_generator, 54 | day_visit_cost_calculator_interface.DayVisitCostCalculatorGeneratorInterface) 55 | 56 | self.calculator_generators = calculator_generators 57 | 58 | def Generate(self, day_visit_parameters): 59 | assert isinstance(day_visit_parameters, city_visit.DayVisitParametersInterface) 60 | 61 | return MultiDayVisitCostCalculator( 62 | [calculator_generator.Generate(day_visit_parameters) 63 | for calculator_generator in self.calculator_generators]) 64 | -------------------------------------------------------------------------------- /router/multi_day_visit_cost_calculator_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from data import point as point_ 4 | from data import city_visit 5 | from router import day_visit_cost_calculator_interface 6 | from router import multi_day_visit_cost_calculator 7 | 8 | 9 | class MockPoint(point_.PointInterface): 10 | pass 11 | 12 | 13 | class MockDayVisitParameters(city_visit.DayVisitParametersInterface): 14 | pass 15 | 16 | 17 | # NOTE(igushev): We imitate time constrains by cost constrains in this class. 18 | class MockDayVisitCostCalculator(day_visit_cost_calculator_interface.DayVisitCostCalculatorInterface): 19 | 20 | def __init__(self, costs, finalization_cost, no_push_cost, max_cost): 21 | self.costs = costs 22 | self.finalization_cost = finalization_cost 23 | self.no_push_cost = no_push_cost 24 | self.max_cost = max_cost 25 | self.cost = 0 26 | self.pos = 0 27 | self.points_left = [] 28 | 29 | def PushPoint(self, point): 30 | assert isinstance(point, MockPoint) 31 | assert self.pos <= len(self.costs) 32 | if (self.points_left or 33 | self.cost + self.costs[self.pos] + self.finalization_cost > 34 | self.max_cost): 35 | self.cost += self.no_push_cost 36 | self.points_left.append(point) 37 | return False 38 | self.cost += self.costs[self.pos] 39 | self.pos += 1 40 | return True 41 | 42 | def FinalizedCost(self): 43 | return self.cost + self.finalization_cost 44 | 45 | def FinalizedDayVisit(self): 46 | return self 47 | 48 | def GetPointsLeft(self): 49 | return self.points_left 50 | 51 | def Pos(self): 52 | assert self.pos <= len(self.costs) 53 | return self.pos 54 | 55 | 56 | class MockDayVisitCostCalculatorGenerator(day_visit_cost_calculator_interface.DayVisitCostCalculatorGeneratorInterface): 57 | 58 | def __init__(self, costs, finalization_cost, no_push_cost, max_cost): 59 | self.costs = costs 60 | self.finalization_cost = finalization_cost 61 | self.no_push_cost = no_push_cost 62 | self.max_cost = max_cost 63 | 64 | def Generate(self, day_visit_parameters): 65 | assert isinstance(day_visit_parameters, MockDayVisitParameters) 66 | return MockDayVisitCostCalculator( 67 | self.costs, self.finalization_cost, self.no_push_cost, self.max_cost) 68 | 69 | 70 | class MultiDayVisitCostCalculatorTest(unittest.TestCase): 71 | 72 | def testBothPushed(self): 73 | calculator_generator_1 = ( 74 | MockDayVisitCostCalculatorGenerator([3, 4], 1, 100, 9)) 75 | calculator_generator_2 = ( 76 | MockDayVisitCostCalculatorGenerator([1, 6], 2, 100, 10)) 77 | calculator = ( 78 | multi_day_visit_cost_calculator.MultiDayVisitCostCalculatorGenerator( 79 | [calculator_generator_1, calculator_generator_2]). 80 | Generate(MockDayVisitParameters())) 81 | calculator_1, calculator_2 = calculator.calculators 82 | 83 | # Empty calculator. 84 | # First one is better. 85 | self.assertEqual(0, calculator_1.Pos()) 86 | self.assertEqual(0, calculator_2.Pos()) 87 | self.assertEqual([], calculator_1.GetPointsLeft()) 88 | self.assertEqual([], calculator_2.GetPointsLeft()) 89 | self.assertEqual(1, calculator_1.FinalizedCost()) 90 | self.assertEqual(2, calculator_2.FinalizedCost()) 91 | self.assertEqual(1, calculator.FinalizedCost()) 92 | self.assertIs(calculator_1, calculator.FinalizedDayVisit()) 93 | self.assertEqual([], calculator.GetPointsLeft()) 94 | 95 | # Pushing first point. 96 | # Second one is better. 97 | self.assertTrue(calculator.PushPoint(MockPoint())) 98 | self.assertEqual(1, calculator_1.Pos()) 99 | self.assertEqual(1, calculator_2.Pos()) 100 | self.assertEqual([], calculator_1.GetPointsLeft()) 101 | self.assertEqual([], calculator_2.GetPointsLeft()) 102 | self.assertEqual(4, calculator_1.FinalizedCost()) 103 | self.assertEqual(3, calculator_2.FinalizedCost()) 104 | self.assertEqual(3, calculator.FinalizedCost()) 105 | self.assertIs(calculator_2, calculator.FinalizedDayVisit()) 106 | self.assertEqual([], calculator.GetPointsLeft()) 107 | 108 | # Pushing second point. 109 | # First one again better. 110 | self.assertTrue(calculator.PushPoint(MockPoint())) 111 | self.assertEqual(2, calculator_1.Pos()) 112 | self.assertEqual(2, calculator_2.Pos()) 113 | self.assertEqual([], calculator_1.GetPointsLeft()) 114 | self.assertEqual([], calculator_2.GetPointsLeft()) 115 | self.assertEqual(8, calculator_1.FinalizedCost()) 116 | self.assertEqual(9, calculator_2.FinalizedCost()) 117 | self.assertEqual(8, calculator.FinalizedCost()) 118 | self.assertIs(calculator_1, calculator.FinalizedDayVisit()) 119 | self.assertEqual([], calculator.GetPointsLeft()) 120 | 121 | def testOnePushed(self): 122 | calculator_generator_1 = ( 123 | MockDayVisitCostCalculatorGenerator([3, 4], 1, 100, 7.5)) 124 | calculator_generator_2 = ( 125 | MockDayVisitCostCalculatorGenerator([1, 6], 2, 100, 10)) 126 | calculator = ( 127 | multi_day_visit_cost_calculator.MultiDayVisitCostCalculatorGenerator( 128 | [calculator_generator_1, calculator_generator_2]). 129 | Generate(MockDayVisitParameters())) 130 | calculator_1, calculator_2 = calculator.calculators 131 | 132 | # See testGeneral. 133 | # Empty calculator. 134 | # First one is better. 135 | # Pushing first point. 136 | # Second one is better. 137 | self.assertTrue(calculator.PushPoint(MockPoint())) 138 | 139 | # Pushing second point. 140 | # First one can't be finalized. 141 | # Second one is better. 142 | second_point = MockPoint() 143 | self.assertTrue(calculator.PushPoint(second_point)) 144 | self.assertEqual(1, calculator_1.Pos()) 145 | self.assertEqual(2, calculator_2.Pos()) 146 | self.assertEqual([second_point], calculator_1.GetPointsLeft()) 147 | self.assertEqual([], calculator_2.GetPointsLeft()) 148 | self.assertEqual(104, calculator_1.FinalizedCost()) 149 | self.assertEqual(9, calculator_2.FinalizedCost()) 150 | self.assertEqual(9, calculator.FinalizedCost()) 151 | self.assertIs(calculator_2, calculator.FinalizedDayVisit()) 152 | self.assertEqual([], calculator.GetPointsLeft()) 153 | 154 | def testNonePushed(self): 155 | calculator_generator_1 = ( 156 | MockDayVisitCostCalculatorGenerator([3, 4], 1, 100, 7.5)) 157 | calculator_generator_2 = ( 158 | MockDayVisitCostCalculatorGenerator([1, 6], 2, 100, 8)) 159 | calculator = ( 160 | multi_day_visit_cost_calculator.MultiDayVisitCostCalculatorGenerator( 161 | [calculator_generator_1, calculator_generator_2]). 162 | Generate(MockDayVisitParameters())) 163 | calculator_1, calculator_2 = calculator.calculators 164 | 165 | # See testGeneral. 166 | # Empty calculator. 167 | # First one is better. 168 | # Pushing first point. 169 | # Second one is better. 170 | self.assertTrue(calculator.PushPoint(MockPoint())) 171 | 172 | # Pushing second point. 173 | # None can be finalized. 174 | second_point = MockPoint() 175 | self.assertFalse(calculator.PushPoint(second_point)) 176 | self.assertEqual(1, calculator_1.Pos()) 177 | self.assertEqual(1, calculator_2.Pos()) 178 | self.assertEqual([second_point], calculator_1.GetPointsLeft()) 179 | self.assertEqual([second_point], calculator_2.GetPointsLeft()) 180 | self.assertEqual(104, calculator_1.FinalizedCost()) 181 | self.assertEqual(103, calculator_2.FinalizedCost()) 182 | self.assertEqual(103, calculator.FinalizedCost()) 183 | self.assertEqual(calculator_2, calculator.FinalizedDayVisit()) 184 | self.assertEqual([second_point], calculator.GetPointsLeft()) 185 | 186 | 187 | if __name__ == '__main__': 188 | unittest.main() 189 | -------------------------------------------------------------------------------- /router/point_fit.py: -------------------------------------------------------------------------------- 1 | from data import point 2 | from data import city_visit 3 | 4 | 5 | class PointFitInterface(object): 6 | """Abstract class which checks if point can be visited.""" 7 | 8 | def IfPointFit(self, start_end_datetime, operating_hours): 9 | """Return if point with given operating_hours can be visited during given 10 | start_end_datetime.""" 11 | raise NotImplemented() 12 | 13 | 14 | # NOTE(igushev): This class works only with StartEndDatetime of 15 | # StartEndDatetimeInterface implementation and OperatingHours implementation 16 | # of OperatingHoursInterface. 17 | class SimplePointFit(PointFitInterface): 18 | """Checks if point can be visited in a very straightforward way. Doesn't know 19 | about days of week, seasons, etc.""" 20 | 21 | def IfPointFit(self, start_end_datetime, operating_hours): 22 | assert isinstance(start_end_datetime, city_visit.StartEndDatetimeInterface) 23 | if operating_hours is not None: 24 | assert isinstance(operating_hours, point.OperatingHoursInterface) 25 | 26 | # If it's 24/7, point can be visited regardless of start_end_datetime. 27 | if not operating_hours: 28 | return True 29 | # If start and end are different days, point can't be visited. 30 | if start_end_datetime.start.date() != start_end_datetime.end.date(): 31 | return False 32 | if (start_end_datetime.start.time() >= operating_hours.opens and 33 | start_end_datetime.end.time() <= operating_hours.closes): 34 | return True 35 | return False 36 | 37 | -------------------------------------------------------------------------------- /router/point_fit_test.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import unittest 3 | 4 | from data import point 5 | from data import city_visit 6 | from router import point_fit as point_fit_ 7 | 8 | 9 | class PointFitTest(unittest.TestCase): 10 | 11 | def setUp(self): 12 | # Ferry Building, San Francisco. 13 | self.ferry_building_operating_hours = point.OperatingHours( 14 | datetime.time(9, 0, 0), datetime.time(18, 0, 0)) 15 | # Pier 39, San Francisco. 16 | self.pier_39_operating_hours = point.OperatingHours( 17 | datetime.time(10, 0, 0), datetime.time(22, 0, 0)) 18 | # Golden Gate Bridge, San Francisco. 19 | self.golden_gate_bridge_operating_hours = None 20 | 21 | 22 | self.early_morning_start_end_datetime = city_visit.StartEndDatetime( 23 | datetime.datetime(2014, 9, 1, 9, 0, 0), 24 | datetime.datetime(2014, 9, 1, 12, 0, 0)) 25 | self.late_everning_start_end_datetime = city_visit.StartEndDatetime( 26 | datetime.datetime(2014, 9, 1, 18, 0, 0), 27 | datetime.datetime(2014, 9, 1, 21, 0, 0)) 28 | self.after_midnight_start_end_datetime = city_visit.StartEndDatetime( 29 | datetime.datetime(2014, 9, 1, 0, 0, 0), 30 | datetime.datetime(2014, 9, 1, 3, 0, 0)) 31 | 32 | super(PointFitTest, self).setUp() 33 | 34 | def testIfPointFitGeneral(self): 35 | point_fit = point_fit_.SimplePointFit() 36 | self.assertEqual(True, point_fit.IfPointFit( 37 | self.early_morning_start_end_datetime, 38 | self.ferry_building_operating_hours)) # Left border. 39 | self.assertEqual(False, point_fit.IfPointFit( 40 | self.late_everning_start_end_datetime, 41 | self.ferry_building_operating_hours)) # Right border. 42 | self.assertEqual(False, point_fit.IfPointFit( 43 | self.after_midnight_start_end_datetime, 44 | self.ferry_building_operating_hours)) # No overlap. 45 | self.assertEqual(False, point_fit.IfPointFit( 46 | self.early_morning_start_end_datetime, 47 | self.pier_39_operating_hours)) # Partial overlap. 48 | self.assertEqual(True, point_fit.IfPointFit( 49 | self.late_everning_start_end_datetime, 50 | self.pier_39_operating_hours)) # Fully inside. 51 | self.assertEqual(False, point_fit.IfPointFit( 52 | self.after_midnight_start_end_datetime, 53 | self.pier_39_operating_hours)) # No overlap. 54 | self.assertEqual(True, point_fit.IfPointFit( 55 | self.early_morning_start_end_datetime, 56 | self.golden_gate_bridge_operating_hours)) 57 | self.assertEqual(True, point_fit.IfPointFit( 58 | self.late_everning_start_end_datetime, 59 | self.golden_gate_bridge_operating_hours)) 60 | self.assertEqual(True, point_fit.IfPointFit( 61 | self.after_midnight_start_end_datetime, 62 | self.golden_gate_bridge_operating_hours)) 63 | 64 | 65 | if __name__ == '__main__': 66 | unittest.main() 67 | -------------------------------------------------------------------------------- /router/points_queue.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | 4 | class PointsQueueInterface(object): 5 | """Abstract class which maintains points queue for pushing.""" 6 | 7 | def HasPoints(self): 8 | """If queue has more points.""" 9 | raise NotImplemented() 10 | 11 | def GetPushPoints(self, day_visit_parameterss): 12 | """Returns list of points to push by given day_visit_parameterss.""" 13 | raise NotImplemented() 14 | 15 | def AddBackToQueue(self, points_left): 16 | """Add given points_left back to queue to return later.""" 17 | raise NotImplemented() 18 | 19 | def GetPointsLeft(self): 20 | """Returns points which still left in the queue.""" 21 | raise NotImplemented() 22 | 23 | 24 | class PointsQueueGeneratorInterface(object): 25 | """Abstract which returns new instance of PointsQueueInterface.""" 26 | 27 | def Generate(self, points): 28 | """Returns new instance of PointsQueueInterface.""" 29 | raise NotImplemented() 30 | 31 | 32 | class OneByOnePointsQueue(PointsQueueInterface): 33 | 34 | def __init__(self, points): 35 | self._points = points 36 | 37 | def HasPoints(self): 38 | return bool(self._points) 39 | 40 | def GetPushPoints(self, day_visit_parameterss): 41 | return [self._points.pop(0)] 42 | 43 | def AddBackToQueue(self, points_left): 44 | self._points = points_left + self._points 45 | 46 | def GetPointsLeft(self): 47 | return self._points 48 | 49 | 50 | class OneByOnePointsQueueGenerator(PointsQueueGeneratorInterface): 51 | 52 | def Generate(self, points): 53 | return OneByOnePointsQueue(points) 54 | 55 | 56 | class AllPointsQueue(PointsQueueInterface): 57 | 58 | def __init__(self, points, cut_off_multiplier): 59 | self._points = points 60 | self._cut_off_multiplier = cut_off_multiplier 61 | 62 | def HasPoints(self): 63 | return bool(self._points) 64 | 65 | def GetPushPoints(self, day_visit_parameterss): 66 | total_timedelta = datetime.timedelta() 67 | for day_visit_parameters in day_visit_parameterss: 68 | total_timedelta += ( 69 | day_visit_parameters.end_datetime - 70 | day_visit_parameters.start_datetime - 71 | datetime.timedelta(hours=day_visit_parameters.lunch_hours)) 72 | total_hours = total_timedelta.total_seconds() / 60. / 60. 73 | cut_off_hours = total_hours * self._cut_off_multiplier 74 | 75 | points_hours = 0. 76 | for cut_off_i in range(len(self._points)): 77 | points_hours += self._points[cut_off_i].duration 78 | if points_hours >= cut_off_hours: 79 | break 80 | 81 | push_points = self._points[:cut_off_i+1] 82 | self._points = self._points[cut_off_i+1:] 83 | return push_points 84 | 85 | def AddBackToQueue(self, points_left): 86 | self._points = points_left + self._points 87 | 88 | def GetPointsLeft(self): 89 | return self._points 90 | 91 | 92 | class AllPointsQueueGenerator(PointsQueueGeneratorInterface): 93 | 94 | def __init__(self, cut_off_multiplier): 95 | self._cut_off_multiplier = cut_off_multiplier 96 | 97 | def Generate(self, points): 98 | return AllPointsQueue(points, self._cut_off_multiplier) 99 | -------------------------------------------------------------------------------- /router/points_queue_test.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import os 3 | import unittest 4 | 5 | from data import city_visit 6 | from data import read_csv 7 | from router import day_visit_cost_calculator_interface 8 | from router import points_queue as points_queue_ 9 | from router import test_util 10 | 11 | 12 | class MockDayVisitCostCalculator(day_visit_cost_calculator_interface.DayVisitCostCalculatorInterface): 13 | def __init__(self): 14 | pass 15 | 16 | 17 | def GetDayVisitParameterss(first_day, last_day): 18 | def GetDayVisitParameters(day): 19 | return city_visit.DayVisitParameters( 20 | start_datetime=datetime.datetime(2015, 7, day, 10, 0, 0), 21 | end_datetime=datetime.datetime(2015, 7, day, 15, 0, 0), 22 | lunch_start_datetime=datetime.datetime(2015, 7, day, 14, 0, 0), 23 | lunch_hours=1., 24 | start_coordinates=test_util.MockCoordinates('Hotel'), 25 | end_coordinates=test_util.MockCoordinates('Hotel')) 26 | return [GetDayVisitParameters(day) for day in range(first_day, last_day)] 27 | 28 | 29 | class OneByOnePointsQueueTest(unittest.TestCase): 30 | 31 | def setUp(self): 32 | self.points = read_csv.ReadCSVToDict(os.path.join('data', 'test_sf_1.csv')) 33 | 34 | def testGeneral(self): 35 | points = [self.points['Golden Gate Bridge'], 36 | self.points['Ferry Building'], 37 | self.points['Pier 39'], 38 | self.points['Union Square'], 39 | self.points['Twin Peaks']] 40 | day_visit_parameterss = [MockDayVisitCostCalculator()] 41 | 42 | points_queue = points_queue_.OneByOnePointsQueueGenerator().Generate(points) 43 | self.assertTrue(points_queue.HasPoints()) 44 | self.assertEqual(points, points_queue.GetPointsLeft()) 45 | 46 | self.assertEqual([self.points['Golden Gate Bridge']], 47 | points_queue.GetPushPoints(day_visit_parameterss)) 48 | self.assertTrue(points_queue.HasPoints()) 49 | self.assertEqual([self.points['Ferry Building'], 50 | self.points['Pier 39'], 51 | self.points['Union Square'], 52 | self.points['Twin Peaks']], 53 | points_queue.GetPointsLeft()) 54 | 55 | self.assertEqual([self.points['Ferry Building']], 56 | points_queue.GetPushPoints(day_visit_parameterss)) 57 | self.assertTrue(points_queue.HasPoints()) 58 | self.assertEqual([self.points['Pier 39'], 59 | self.points['Union Square'], 60 | self.points['Twin Peaks']], 61 | points_queue.GetPointsLeft()) 62 | 63 | self.assertEqual([self.points['Pier 39']], 64 | points_queue.GetPushPoints(day_visit_parameterss)) 65 | self.assertTrue(points_queue.HasPoints()) 66 | self.assertEqual([self.points['Union Square'], 67 | self.points['Twin Peaks']], 68 | points_queue.GetPointsLeft()) 69 | 70 | points_queue.AddBackToQueue([self.points['Ferry Building'], 71 | self.points['Pier 39']]) 72 | self.assertTrue(points_queue.HasPoints()) 73 | self.assertEqual([self.points['Ferry Building'], 74 | self.points['Pier 39'], 75 | self.points['Union Square'], 76 | self.points['Twin Peaks']], 77 | points_queue.GetPointsLeft()) 78 | 79 | self.assertEqual([self.points['Ferry Building']], 80 | points_queue.GetPushPoints(day_visit_parameterss)) 81 | self.assertTrue(points_queue.HasPoints()) 82 | self.assertEqual([self.points['Pier 39'], 83 | self.points['Union Square'], 84 | self.points['Twin Peaks']], 85 | points_queue.GetPointsLeft()) 86 | 87 | points_queue.GetPushPoints(day_visit_parameterss) 88 | points_queue.GetPushPoints(day_visit_parameterss) 89 | self.assertEqual([self.points['Twin Peaks']], 90 | points_queue.GetPushPoints(day_visit_parameterss)) 91 | self.assertFalse(points_queue.HasPoints()) 92 | self.assertEqual([], points_queue.GetPointsLeft()) 93 | 94 | 95 | class AllPointsQueueTest(unittest.TestCase): 96 | 97 | def setUp(self): 98 | self.points = read_csv.ReadCSVToDict(os.path.join('data', 'test_sf_1.csv')) 99 | 100 | def testGeneral(self): 101 | day_visit_parameterss = GetDayVisitParameterss(1, 3) 102 | points = [self.points['Golden Gate Bridge'], 103 | self.points['Ferry Building'], 104 | self.points['Pier 39'], 105 | self.points['Union Square'], 106 | self.points['Lombard Street'], 107 | self.points['Coit Tower'], 108 | self.points['Att Park'], 109 | self.points['Alcatraz Island'], 110 | self.points['Golden Gate Park'], 111 | self.points['De Young Museum']] 112 | 113 | points_queue = points_queue_.AllPointsQueueGenerator(1.2).Generate(points) 114 | self.assertTrue(points_queue.HasPoints()) 115 | self.assertEqual(points, points_queue.GetPointsLeft()) 116 | 117 | self.assertEqual([self.points['Golden Gate Bridge'], 118 | self.points['Ferry Building'], 119 | self.points['Pier 39'], 120 | self.points['Union Square'], 121 | self.points['Lombard Street'], 122 | self.points['Coit Tower'], 123 | self.points['Att Park'], 124 | self.points['Alcatraz Island']], 125 | points_queue.GetPushPoints(day_visit_parameterss)) 126 | self.assertTrue(points_queue.HasPoints()) 127 | self.assertEqual([self.points['Golden Gate Park'], 128 | self.points['De Young Museum']], 129 | points_queue.GetPointsLeft()) 130 | 131 | self.assertEqual([self.points['Golden Gate Park'], 132 | self.points['De Young Museum']], 133 | points_queue.GetPushPoints(day_visit_parameterss)) 134 | self.assertFalse(points_queue.HasPoints()) 135 | self.assertEqual([], points_queue.GetPointsLeft()) 136 | 137 | def testLargeCutOffMultiplier(self): 138 | day_visit_parameterss = GetDayVisitParameterss(1, 3) 139 | points = [self.points['Golden Gate Bridge'], 140 | self.points['Ferry Building'], 141 | self.points['Pier 39'], 142 | self.points['Union Square'], 143 | self.points['Lombard Street'], 144 | self.points['Coit Tower'], 145 | self.points['Att Park'], 146 | self.points['Alcatraz Island'], 147 | self.points['Golden Gate Park'], 148 | self.points['De Young Museum']] 149 | 150 | points_queue = points_queue_.AllPointsQueueGenerator(2.0).Generate(points) 151 | self.assertTrue(points_queue.HasPoints()) 152 | self.assertEqual(points, points_queue.GetPointsLeft()) 153 | 154 | self.assertEqual(points, 155 | points_queue.GetPushPoints(day_visit_parameterss)) 156 | self.assertFalse(points_queue.HasPoints()) 157 | self.assertEqual([], points_queue.GetPointsLeft()) 158 | 159 | 160 | if __name__ == '__main__': 161 | unittest.main() 162 | -------------------------------------------------------------------------------- /router/runner.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import os 3 | 4 | from config import config as config_ 5 | 6 | 7 | class CityVisitRouterRunner(object): 8 | 9 | def __init__(self): 10 | config = config_.GetConfig(os.path.join('config', 'runner.config')) 11 | self.city_visit_router = config_.GetCityVisitRouter(config) 12 | self.city_visit_accumulator_generator = config_.GetCityVisitAccumulatorGenerator(config) 13 | 14 | 15 | def Run(self, points_input, day_visit_parameterss): 16 | start = datetime.datetime.now() 17 | 18 | city_visit_best, points_left = ( 19 | self.city_visit_router.RouteCityVisit( 20 | points_input, day_visit_parameterss, self.city_visit_accumulator_generator)) 21 | 22 | print('Points to visit in priority: %s' % 23 | ', '.join(point.name for point in points_input)) 24 | print('Your schedule:') 25 | print(city_visit_best) 26 | print('Points left: %s' % 27 | ', '.join(point_left.name for point_left in points_left)) 28 | 29 | print('Elapsed time %s' % (datetime.datetime.now() - start)) 30 | -------------------------------------------------------------------------------- /router/runner_nyc_1.py: -------------------------------------------------------------------------------- 1 | from data import point 2 | from data import runner_util 3 | from data import test_util 4 | from router import runner as router_runner 5 | 6 | 7 | def main(): 8 | points_dict = test_util.GetPointsInput('data', 'test_nyc_1.csv') 9 | points_keys = test_util.GetPointsKeys('router', 'test_points_nyc.txt') 10 | points_input = test_util.FilterAndSortByKeys(points_dict, points_keys) 11 | 12 | # 746 Ninth Ave, New York, NY 10019. 13 | start_end_coordinates = point.Coordinates(40.763582, -73.988470) 14 | first_day, last_day = 13, 16 15 | day_visit_parameterss = runner_util.GetDayVisitParameterss(start_end_coordinates, first_day, last_day) 16 | city_visit_router_runner = router_runner.CityVisitRouterRunner() 17 | city_visit_router_runner.Run(points_input, day_visit_parameterss) 18 | 19 | 20 | if __name__ == '__main__': 21 | main() 22 | -------------------------------------------------------------------------------- /router/runner_sf_1.py: -------------------------------------------------------------------------------- 1 | from data import runner_util 2 | from data import test_util 3 | from router import runner as router_runner 4 | 5 | 6 | def main(): 7 | points_dict = test_util.GetPointsInput('data', 'test_sf_1.csv') 8 | points_keys = test_util.GetPointsKeys('router', 'test_points_sf.txt') 9 | points_input = test_util.FilterAndSortByKeys(points_dict, points_keys) 10 | 11 | start_end_coordinates = points_dict['Union Square'].coordinates_starts 12 | first_day, last_day = 1, 4 13 | day_visit_parameterss = runner_util.GetDayVisitParameterss(start_end_coordinates, first_day, last_day) 14 | 15 | city_visit_router_runner = router_runner.CityVisitRouterRunner() 16 | city_visit_router_runner.Run(points_input, day_visit_parameterss) 17 | 18 | 19 | if __name__ == '__main__': 20 | main() 21 | -------------------------------------------------------------------------------- /router/runner_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from data import runner_util 4 | from data import test_util 5 | from router import runner as router_runner 6 | 7 | 8 | class CityVisitRouterRunnerTest(unittest.TestCase): 9 | 10 | def testGeneral(self): 11 | points_dict = test_util.GetPointsInput('data', 'test_sf_1.csv') 12 | points_keys = test_util.GetPointsKeys('router', 'test_points_sf.txt') 13 | points_input = test_util.FilterAndSortByKeys(points_dict, points_keys) 14 | 15 | start_end_coordinates = points_dict['Union Square'].coordinates_starts 16 | first_day, last_day = 1, 2 17 | day_visit_parameterss = runner_util.GetDayVisitParameterss(start_end_coordinates, first_day, last_day) 18 | 19 | city_visit_router_runner = router_runner.CityVisitRouterRunner() 20 | city_visit_router_runner.Run(points_input, day_visit_parameterss) 21 | 22 | 23 | if __name__ == '__main__': 24 | unittest.main() 25 | -------------------------------------------------------------------------------- /router/test_points_nyc.txt: -------------------------------------------------------------------------------- 1 | Fifth Avenue (Shopping) 2 | Rockefeller Center 3 | One World Observatory 4 | National September 11 Memorial And Museum 5 | Skyscraper Museum 6 | 7 | Upper East Side (Lower) 8 | Upper West Side (Lower) 9 | 10 | Flatiron Building 11 | 12 | Columbia University 13 | Fort Tryon Park 14 | The Cloisters 15 | 16 | Lincoln Center for the Performing Arts 17 | Headquarters of the United Nations 18 | Metropolitan Museum of Art 19 | Madison Square 20 | 21 | Trinity Church 22 | New York City Criminal Court 23 | Tweed Courthouse 24 | Manhattan Municipal Building 25 | Federal Hall 26 | Federal Reserve Bank of New York 27 | Madison Square Garden 28 | 29 | Union Square 30 | Macy's Herald Square 31 | 32 | Meatpacking District 33 | Columbus Park 34 | Morningside Park 35 | Hudson River Park 36 | Conservatory Garden 37 | 38 | New York University 39 | Metropolitan Life Insurance Company Tower 40 | Museum of Modern Art 41 | 42 | Soldiers' and Sailors' Monument 43 | Lower East Side Tenement Museum 44 | Titanic Memorial 45 | Ground Zero Catholic Memorial 46 | Park Avenue Armory 47 | 48 | St. Peter's Roman Catholic Church 49 | Church of St. Mary the Virgin 50 | St. Paul's Chapel 51 | Cathedral of Saint John the Divine 52 | Church of St. Ignatius Loyola 53 | 54 | Hans Christian Andersen Statue 55 | Alice in Wonderland Statue 56 | 57 | Little Red Lighthouse 58 | Peace Fountain 59 | Unisphere 60 | Riverside Church 61 | Apollo Theater 62 | Sutton Place Park 63 | 64 | Upper East Side (Middle) 65 | Upper West Side (Middle) 66 | Greenwich Village 67 | West Village 68 | SoHo 69 | 70 | -------------------------------------------------------------------------------- /router/test_points_sf.txt: -------------------------------------------------------------------------------- 1 | De Young Museum 2 | Cable Car Museum 3 | Baker Beach 4 | Sutro Baths 5 | Presidio 6 | Ferry Building 7 | Lombard Street 8 | Alamo Square 9 | Twin Peaks 10 | Golden Gate Bridge 11 | Golden Gate Park 12 | -------------------------------------------------------------------------------- /router/test_util.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from data import point as point_ 4 | from data import city_visit 5 | from data import read_csv 6 | from router import move_calculator 7 | 8 | 9 | class MockCoordinates(point_.CoordinatesInterface): 10 | def __init__(self, name): 11 | assert isinstance(name, str) 12 | self.name = name 13 | 14 | def __eq__(self, other): 15 | return self.__dict__ == other.__dict__ 16 | 17 | def __str__(self): 18 | return self.name 19 | 20 | 21 | def MockPoints(): 22 | points = read_csv.ReadCSVToDict(os.path.join('data', 'test_sf_1.csv')) 23 | for point in list(points.values()): 24 | point.coordinates_starts = MockCoordinates(point.name) 25 | point.coordinates_ends = MockCoordinates(point.name) 26 | return points 27 | 28 | 29 | # NOTE(igushev): Here we mock Coordinates with simple str and in this 30 | # dictionary just define move_description between two given points. 31 | # MockMoveCalculator relies instead of some sort of calculation of this 32 | # dictionary. 33 | MOVE_HOURS = {frozenset({'Hotel', 'Ferry Building'}): 1., 34 | frozenset({'Hotel', 'Pier 39'}): 3., 35 | frozenset({'Hotel', 'Golden Gate Bridge'}): 6., 36 | frozenset({'Hotel', 'Union Square'}): 1., 37 | frozenset({'Hotel', 'Twin Peaks'}): 3., 38 | frozenset({'Hotel', 'Restaurant'}): 1., 39 | frozenset({'Ferry Building', 'Pier 39'}): 1., 40 | frozenset({'Ferry Building', 'Golden Gate Bridge'}): 8., 41 | frozenset({'Ferry Building', 'Union Square'}): 2., 42 | frozenset({'Ferry Building', 'Twin Peaks'}): 5., 43 | frozenset({'Ferry Building', 'Restaurant'}): 2., 44 | frozenset({'Pier 39', 'Golden Gate Bridge'}): 5., 45 | frozenset({'Pier 39', 'Union Square'}): 2., 46 | frozenset({'Pier 39', 'Twin Peaks'}): 5., 47 | frozenset({'Pier 39', 'Restaurant'}): 4., 48 | frozenset({'Golden Gate Bridge', 'Union Square'}): 5., 49 | frozenset({'Golden Gate Bridge', 'Twin Peaks'}): 5., 50 | frozenset({'Golden Gate Bridge', 'Restaurant'}): 6., 51 | frozenset({'Twin Peaks', 'Union Square'}): 3., 52 | frozenset({'Twin Peaks', 'Restaurant'}): 2., 53 | frozenset({'Union Square', 'Restaurant'}): 1.} 54 | 55 | 56 | class MockMoveCalculator(move_calculator.MoveCalculatorInterface): 57 | def CalculateMoveDescription(self, coordinates_from, coordinates_to): 58 | coordinates_between = ( 59 | frozenset({coordinates_from.name, coordinates_to.name})) 60 | if not coordinates_between in MOVE_HOURS: 61 | raise AssertionError( 62 | 'MOVE_HOURS is not defined between coordinates: %s and %s' % ( 63 | coordinates_from, coordinates_to)) 64 | return city_visit.MoveDescription(coordinates_from, coordinates_to, 65 | MOVE_HOURS[coordinates_between], city_visit.MoveType.walking) 66 | --------------------------------------------------------------------------------