def test_edge_costs(self) -> None: graph = AngleGraph(self.example_inst, self.working_expl_corr) self.cfg.angle_weight = 0 self.cfg.edge_weight = 0.5 path = graph.single_sp(**vars(self.cfg)) path, _, cost_sum = graph.transform_path(path) dest_ind = graph.pos2node[tuple(self.dest_inds)] dest_costs = np.min(graph.dists[dest_ind]) a = [] path = np.array(path) weighted_inst = self.example_inst for p in range(len(path) - 1): line = bresenham_line(path[p, 0], path[p, 1], path[p + 1, 0], path[p + 1, 1])[1:-1] line_costs = [weighted_inst[i, j] for (i, j) in line] line_costs = [li for li in line_costs if li < np.inf] bresenham_edge_dist = np.mean(line_costs) * self.cfg.edge_weight shift_costs = np.linalg.norm(path[p] - path[p + 1]) # append edge cost a.append((0.5 * (weighted_inst[tuple(path[p])] + weighted_inst[tuple(path[p + 1])]) + shift_costs * bresenham_edge_dist)) dest_costs_gt = np.sum(a) self.assertTrue(np.isclose(cost_sum, dest_costs_gt)) self.assertTrue(np.isclose(dest_costs, dest_costs_gt)) out_compute_cost_method = compute_geometric_costs( path, weighted_inst, edge_weight=self.cfg.edge_weight) self.assertEqual(np.sum(out_compute_cost_method), cost_sum) self.cfg.angle_weight = 0.25 self.cfg.edge_weight = 0
def test_correct_shortest_path(self) -> None: """ Test the implicit line graph construction """ graph = AngleGraph(self.example_inst, self.working_expl_corr) path = graph.single_sp(**vars(self.cfg)) path, path_costs, cost_sum = graph.transform_path(path) self.assertTupleEqual(graph.instance.shape, self.expl_shape) self.assertEqual(graph.angle_weight + graph.resistance_weight, 1) self.assertNotEqual(len(graph.shifts), 0) # all initialized to infinity # self.assertTrue(not np.any([graph.dists[graph.dists < np.inf]])) # start point was set to normalized value start_ind = graph.pos2node[tuple(self.start_inds)] self.assertEqual(0, graph.dists[start_ind, 0]) # all start dists have same value self.assertEqual(len(np.unique(graph.dists[start_ind])), 1) # not all values still inf self.assertLess(np.min(graph.dists[start_ind]), np.inf) # get actual best path # path, path_costs, cost_sum = graph.get_shortest_path( # self.start_inds, self.dest_inds # ) self.assertNotEqual(len(path), 0) self.assertEqual(len(path), len(path_costs)) self.assertGreaterEqual(cost_sum, 5) for (i, j) in path: self.assertEqual(self.example_inst[i, j], 1)
def test_empty_path(self) -> None: self.cfg.max_angle = np.pi graph = AngleGraph(self.example_inst, self.no_connection_corr) _ = graph.single_sp(**vars(self.cfg)) # assert that destination can NOT be reached dest_ind = graph.pos2node[tuple(self.dest_inds)] self.assertFalse(np.min(graph.dists[dest_ind]) < np.inf)
def test_discrete(self) -> None: """ DISCRETE """ self.cfg["angle_cost_function"] = "discrete" self.cfg["max_angle"] = np.pi / 4 self.cfg["angle_weight"] = .3 graph = AngleGraph(self.instance, self.corridor) _ = graph.single_sp(**self.cfg) gt_angle_cost_arr = graph.angle_cost_array.copy() gt_dists = graph.dists.copy() graph.dists = np.zeros( (len(graph.stack_array), len(graph.shifts)) ) + np.inf graph.dists[0, :] = 0 graph.preds = np.zeros(graph.dists.shape) - 1 graph.angle_cost_function = "some_non_existant" graph.build_source_sp_tree(**self.cfg) self.assertTrue(np.all(gt_angle_cost_arr == graph.angle_cost_array)) self.assertTrue(np.all(np.isclose(gt_dists, graph.dists)))
def _initialize_graph(instance, cfg): """ Auxiliary method to preprocess the instance and initialize the graph @params: instance: 2D numpy array of resistances values (float or int) If cells are forbidden, set them to NaN or a value x and specify x in cfg["forbidden_val"]. cfg: configuration as specified on top of this file @returns: graph: AngleGraph object initialized with instance cfg: updated configuration file """ forbidden_val = cfg.get("forbidden_val", np.nan) logger.info(f"forbidden val: {forbidden_val}") # make forbidden region array project_region = np.ones(instance.shape) project_region[np.isnan(instance)] = 0 project_region[instance == forbidden_val] = 0 # instance must not contain zeros non_zero_add = 1e-8 instance += non_zero_add # compute maximum values without forbidden weight normal_vals = instance[ np.logical_and(instance != forbidden_val, ~np.isnan(instance))] assert np.all( normal_vals < np.inf ), "check forbidden_val parameter in cfg, it is\ not inf but there are inf values in array" # fill values in instance instance[project_region == 0] = np.max(normal_vals) # normalize instance by maximum value (excluding forbidden areas) # normalization is necessary to balance angle- and resistance-costs instance = instance / np.max(normal_vals) assert np.min(instance) > 0 and np.isclose( np.max(instance), 1 ), "Minimum must be greater than zero and maximum ~1 after normalizing" # initialize graph graph = AngleGraph(instance, project_region) return graph, cfg
def test_angle_sp(self) -> None: graph = AngleGraph(self.example_inst, self.high_angle_corr) _ = graph.single_sp(**vars(self.cfg)) # assert that destination can NOT be reached dest_ind = graph.pos2node[tuple(self.dest_inds)] self.assertFalse(np.min(graph.dists[dest_ind]) < np.inf) # Note: this test does not change the stack array because the path is # only empty because of an inf in angles_all # NEXT TRY: more angles allowed self.cfg.max_angle = np.pi graph = AngleGraph(self.example_inst, self.high_angle_corr) path = graph.single_sp(**vars(self.cfg)) path, _, cost_sum = graph.transform_path(path) # assert that dest CAN be reached dest_ind = graph.pos2node[tuple(self.dest_inds)] self.assertTrue(np.min(graph.dists[dest_ind]) < np.inf)
def test_linear(self) -> None: """ LINEAR """ self.cfg["angle_cost_function"] = "linear" self.cfg["max_angle"] = np.pi self.cfg["angle_weight"] = .3 graph = AngleGraph(self.instance, self.corridor) _ = graph.single_sp(**self.cfg) gt_angle_cost_arr = graph.angle_cost_array.copy() gt_dists = graph.dists.copy() graph.dists = np.zeros( (len(graph.stack_array), len(graph.shifts)) ) + np.inf graph.dists[0, :] = 0 graph.preds = np.zeros(graph.dists.shape) - 1 graph.angle_cost_function = "some_non_existant" graph.build_source_sp_tree(**self.cfg) self.assertTrue( np.all(np.isclose(gt_angle_cost_arr, graph.angle_cost_array)) ) # Errors can occur randomly, but seem to be only precision errors # errors only appear if angle_weight > 0, try 1 to reproduce self.assertTrue(np.all(np.isclose(gt_dists, graph.dists, atol=1e-04)))
def optimal_point_spotting( instance, cfg, corridor=None, k=1, algorithm=KSP.ksp ): """ Compute the (angle-) optimal point spotting @params: instance: 2D np array of resistances (see details top of file) cfg: config dict, must contain start and dest (see details top of file) corridor: relevant region --> either - a path, e.g. output of optimal_route, or - a 2D array with 0: do not consider, 1: consider @returns: a single optimal path of points (list of X Y coordinates) or empty list if no path exists """ # initialize graph graph, cfg = _initialize_graph(instance, cfg) # initial project region original_inst = graph.edge_inst.copy() original_corr = (graph.instance < np.inf).astype(int) # initialize corridor if corridor is None: corridor = np.ones(original_corr.shape) # estimate how many edges the graph will have: graph.set_shift(cfg["start_inds"], cfg["dest_inds"], **cfg) orig_shifts = len(graph.shift_tuples) instance_vertices = np.sum(original_corr * corridor > 0) mem_limit = cfg.get("memory_limit", 5e7) # define pipeline pipeline = cfg.get( "pipeline", ut_general.get_pipeline(instance_vertices, orig_shifts, mem_limit) ) assert all( pipeline[i] > pipeline[i + 1] for i in range(len(pipeline) - 1) ), "pipeline must consist of decreasing downsampling factors" assert pipeline[ -1] == 1, "last factor in pipeline must be 1 (= no downsampling)" # execute pipeline logger.info(f"Pipeline set to: {pipeline}") # execute iterative shortest path computation for pipe_step, factor in enumerate(pipeline): assert isinstance(factor, int) or float(factor).is_integer( ), "downsampling factors in pipeline must be integers" logger.info( f"---------- Start {pipe_step+1}th step {factor} ---------------" ) # rescale and set parameters accordingly corridor = (corridor * original_corr > 0).astype(int) current_instance, current_corridor, current_cfg = ut_general.rescale( original_inst, corridor, cfg, factor ) # run shortest path computation graph = AngleGraph(current_instance, current_corridor) if k > 1: paths = _run_ksp(graph, current_cfg, k, algorithm=algorithm) paths = [np.array(path) * factor for path in paths] else: path = graph.single_sp(**current_cfg) paths = [np.asarray(path) * factor] logger.debug(f"got {len(paths)} paths in this step") # compute next corridor if pipe_step < len(pipeline) - 1 and len(paths[0]) > 0: corridor = ut_general.pipeline_corridor( paths, instance.shape, orig_shifts, mem_limit, pipeline[pipe_step + 1] ) if k == 1: return path else: return paths