def set_prior_prob(self, map_, energy=False): """Sets the prior probability attribute of the sampler. It is possible to provide prior probability values (`energy=False`) or probability energy values (`energy=True`). In the latter case, the values are transformed to un-normalized probability measures by:: prior_prob.values = exp(-map_.values) where `exp` denotes the exponential function. Args: map_ (LatticeMap): Prior map energy (bool): Indicates if the prior map represents probability values (false) or probability energy (true). Returns: None """ # Minor consistency checks if self._lattice != map_.lattice: raise ValueError(msg.err3000(self._lattice, map_.lattice)) # If the map represents the energy, transform it to probability values values = np.copy(map_.values) if energy: values = np.exp(-values) # Set value within self self._prior_prob = LatticeMap(self._lattice, values)
def __init__(self, lattice): # Also accept objects that can be used to generate a lattice if not isinstance(lattice, Lattice): lattice = Lattice(lattice) self._lattice = lattice # Make default prior maps (all equal probability, all allowed) ones = np.ones(lattice.nnodes_dim) self._prior_prob = LatticeMap(lattice, ones, dtype=int) self._prior_cond = LatticeMap(lattice, ones, dtype=bool)
def test_LatticeMap2D_print(self): lattice = Lattice(self.nodes2D) values = np.random.randn(lattice.nnodes) latticemap = LatticeMap(lattice, values) self.assertTrue(hasattr(latticemap, '__repr__')) self.assertTrue(latticemap.__repr__()) self.assertTrue(hasattr(latticemap, '__str__')) self.assertTrue(latticemap.__str__())
def test_LatticeMap3D_print(self): lattice = Lattice(self.nodes3D) values = np.random.randn(lattice.nnodes) latticemap = LatticeMap(lattice, values) # Force the implementation of __repr__() and __str__() self.assertTrue(hasattr(latticemap, '__repr__')) self.assertTrue(latticemap.__repr__()) self.assertTrue(hasattr(latticemap, '__str__')) self.assertTrue(latticemap.__str__())
def test_LatticeMap2D_normalize_sum(self): nodes = [[0, 1, 2, 3, 4], [-3, -2, -1, 0]] vals = np.random.rand(20) map_ = LatticeMap(nodes, vals) vals_nrm = map_.normalize_sum().values self.assertTrue(np.allclose(vals_nrm, vals / np.sum(vals))) vals_nrm = map_.normalize_sum(axis=0).values sum_vals_nrm = np.sum(vals_nrm.reshape((5, 4), order=NP_ORDER), axis=0) self.assertTrue(np.allclose(sum_vals_nrm, 1)) vals_nrm = map_.normalize_sum(axis=1).values sum_vals_nrm = np.sum(vals_nrm.reshape((5, 4), order=NP_ORDER), axis=1) self.assertTrue(np.allclose(sum_vals_nrm, 1))
def test_LatticeMap3D__eq__(self): lattice = Lattice(self.nodes3D) values = np.random.randn(lattice.nnodes) latticemap = LatticeMap(lattice, values) latticemap_is = latticemap latticemap_eq = LatticeMap(lattice, values) latticemap_non_eq = LatticeMap(lattice, values + 1) self.assertTrue(latticemap is latticemap_is) self.assertTrue(latticemap == latticemap_eq) self.assertEqual(latticemap, latticemap_eq) self.assertFalse(latticemap is latticemap_eq) self.assertFalse(latticemap == latticemap_non_eq)
def test_Trajectory_to_latticemap(self): nodes = [[-2, -1, 0, 1, 2], [-3, 1, 5, 9]] lattice = Lattice(nodes) pts = [(-2, 1), (-1, -4), (0, 3), (1, 4), (2, 8.8)] values = np.array([ 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, ], dtype=bool) map_test = Trajectory(pts).to_latticemap(lattice) map_true = LatticeMap(lattice, values) self.assertEqual(map_true, map_test)
def test_LatticeMap3D__mul__(self): lattice = Lattice(self.nodes3D) num = -25.89 values = np.random.randn(lattice.nnodes) latticemap = LatticeMap(lattice, values) latticmap_mul = LatticeMap(lattice, values * num) latticemap_squared = LatticeMap(lattice, values**2) self.assertEqual(latticmap_mul, latticemap * num) self.assertTrue(latticmap_mul == latticemap * num) self.assertEqual(latticmap_mul, num * latticemap) self.assertTrue(latticmap_mul == num * latticemap) self.assertEqual(latticemap * latticemap, latticemap_squared) with self.assertRaises(TypeError): latticemap * 'foobar'
def _cond_map_from_point(self, components): """Generate a conditioning lattice map from a conditioning point.""" # Initialization nodes = self._lattice.nodes nnodes_dim = self._lattice.nnodes_dim # Generate array with lowest dimension corresponding to the gantry dim = (nnodes_dim[DIM_GANTRY], nnodes_dim[DIM_TABLE]) values = np.zeros(dim, dtype=bool) # Conical opening for the permitted area of the trajectory center = components[DIM_TABLE] con_rat = 2 * self._ratio # The conical opening is symmetric pts = nodes[DIM_TABLE] # Loop in gantry direction through the lattice for inode, node in enumerate(nodes[DIM_GANTRY]): dist = abs(node - components[DIM_GANTRY]) allowed = utl.within_conical_opening(center, dist, con_rat, pts) values[inode, allowed] = True # Correct dimension ordering if needed if DIM_GANTRY > DIM_TABLE: values = values.transpose() values = values.ravel(order=NP_ORDER) return LatticeMap(self._lattice, values)
def test_LatticeMap2D_slice(self): nodes = [[-1.5, 1.5, 5, 8, 9], [1, 2, 3, 4, 5, 6]] lattice = Lattice(nodes) values = [ 0.51, 0.52, 0.53, 0.54, 0.55, 0.61, 0.62, 0.63, 0.64, 0.65, 0.71, 0.72, 0.73, 0.74, 0.75, 0.71, 0.72, 0.73, 0.74, 0.75, 0.61, 0.62, 0.63, 0.64, 0.65, 0.51, 0.52, 0.53, 0.54, 0.55, ] latticemap = LatticeMap(lattice, values=values) lattice_slice_0 = Lattice([[1, 2, 3, 4, 5, 6]]) values_slice_0 = [0.52, 0.62, 0.72, 0.72, 0.62, 0.52] latticemap_slice_0 = LatticeMap(lattice_slice_0, values_slice_0) lattice_slice_1 = Lattice([[-1.5, 1.5, 5, 8, 9]]) values_slice_1 = [0.71, 0.72, 0.73, 0.74, 0.75] latticemap_slice_1 = LatticeMap(lattice_slice_1, values_slice_1) self.assertEqual(latticemap_slice_0, latticemap.slice(0, 1)) self.assertEqual(latticemap_slice_1, latticemap.slice(1, 3))
def test_LatticeMap3D__getitem__(self): lattice = Lattice(self.nodes3D) nodes = lattice.nodes nnodes_dim = lattice.nnodes_dim values = np.random.randn(lattice.nnodes) latticemap = LatticeMap(lattice, values) nx_st, nx_end = 2, 4 ny_st, ny_end = 0, 3 nz_st, nz_end = 2, 3 ind_x = np.arange(nx_st, nx_end) ind_y = np.arange(ny_st, ny_end) ind_z = np.arange(nz_st, nz_end) latticemap_z = latticemap[:, :, nz_st] lin_ind_z = np.array([ ix + iy * nnodes_dim[0] + nz_st * nnodes_dim[0] * nnodes_dim[1] for iy in range(nnodes_dim[1]) for ix in range(nnodes_dim[0]) ]) latticemap_zslice = latticemap[:, :, nz_st:nz_end] lin_ind_zslice = np.array([ ix + iy * nnodes_dim[0] + iz * nnodes_dim[0] * nnodes_dim[1] for iz in ind_z for iy in range(nnodes_dim[1]) for ix in range(nnodes_dim[0]) ]) latticemap_xyzslice = latticemap[nx_st:nx_end, ny_st:ny_end, nz_st:nz_end] lin_ind_xyzslice = np.array([ ix + iy * nnodes_dim[0] + iz * nnodes_dim[0] * nnodes_dim[1] for iz in ind_z for iy in ind_y for ix in ind_x ]) self.assertTrue(latticemap_z == LatticeMap(lattice=lattice[:, :, nz_st], values=values[lin_ind_z])) self.assertTrue(latticemap_zslice == LatticeMap( lattice=lattice[:, :, nz_st:nz_end], values=values[lin_ind_zslice])) self.assertTrue( latticemap_xyzslice == LatticeMap(lattice=lattice[nx_st:nx_end, ny_st:ny_end, nz_st:nz_end], values=values[lin_ind_xyzslice]))
def test_LatticeMap3D_ndim(self): ndim = 3 lattice = Lattice(self.nodes3D) values = np.random.randn(lattice.nnodes) latticemap = LatticeMap(lattice, values) self.assertTrue(hasattr(latticemap, 'ndim')) self.assertEqual(latticemap.ndim, ndim) self.assertTrue(isinstance(latticemap.ndim, int))
def test_LatticeMap2D_make_latticemap_from_txt(self): nodes = [[-1.5, 1.5, 5, 8, 9], [1, 2, 3, 4, 5, 6]] lattice = Lattice(nodes) values = [ 0.5, 0.5, 0.5, 0.5, 0.5, 0.6, 0.6, 0.6, 0.6, 0.6, 0.7, 0.7, 0.7, 0.7, 0.7, 0.7, 0.7, 0.7, 0.7, 0.7, 0.6, 0.6, 0.6, 0.6, 0.6, 0.5, 0.5, 0.5, 0.5, 0.5, ] latticemap_true = LatticeMap(lattice, values) file = PATH_TESTFILES + 'latticemap2d_simple.txt' latticemap_test = LatticeMap.from_txt(file) self.assertTrue(isinstance(latticemap_test, LatticeMap)) self.assertEqual(latticemap_true, latticemap_test)
def test_LatticeMap3D__add__(self): lattice = Lattice(self.nodes3D) nodes_short = [self.x_short, self.y, self.z] lattice_short = Lattice(nodes_short) num = -25.89 values_left = np.random.randn(lattice.nnodes) values_right = np.random.randn(lattice.nnodes) values_short = np.random.randn(lattice_short.nnodes) latticemap_left = LatticeMap(lattice, values_left) latticemap_right = LatticeMap(lattice, values_right) latticemap_short = LatticeMap(lattice_short, values_short) latticmap_sum = LatticeMap(lattice, values_left + values_right) latticmap_sum_num = LatticeMap(lattice, values_left + num) self.assertEqual(latticmap_sum, latticemap_left + latticemap_right) self.assertTrue(latticmap_sum == latticemap_left + latticemap_right) self.assertEqual(latticmap_sum_num, latticemap_left + num) self.assertTrue(latticmap_sum_num == latticemap_left + num) with self.assertRaises(ValueError): latticemap_left + latticemap_short with self.assertRaises(TypeError): latticemap_left + 'foobar'
def test_LatticeMap3D_make_latticemap_from_txt(self): nodes = [[-1.5, 1.5], [5, 8, 9], [-2, 3]] lattice = Lattice(nodes) values = [ 0.5, 0.5, 0.8, 0.8, 0.1, 0.1, 0.6, 0.6, 0.9, 0.9, 0.2, 0.2, ] latticemap_true = LatticeMap(lattice, values) file = PATH_TESTFILES + 'latticemap3d_simple.txt' latticemap_test = LatticeMap.from_txt(file) self.assertTrue(isinstance(latticemap_test, LatticeMap)) self.assertEqual(latticemap_true, latticemap_test)
def test_readfile_latticemap_2D(self): nodes = [[-1.5, 1.5, 5, 8, 9], [1, 2, 3, 4, 5, 6]] values = np.asarray([ [0.5, 0.5, 0.5, 0.5, 0.5], [0.6, 0.6, 0.6, 0.6, 0.6], [0.7, 0.7, 0.7, 0.7, 0.7], [0.7, 0.7, 0.7, 0.7, 0.7], [0.6, 0.6, 0.6, 0.6, 0.6], [0.5, 0.5, 0.5, 0.5, 0.5], ]).ravel(order='C') latticemap_true = LatticeMap(nodes, values) file = PATH_TESTFILES + 'latticemap2d_simple.txt' latticemap_test = readfile_latticemap(file) self.assertEqual(latticemap_true, latticemap_test)
def test_utils_readfile_latticemap3D(self): nodes = [[-1.5, 1.5], [5, 8, 9], [-2, 3]] values = np.asarray([ [0.5, 0.5], [0.8, 0.8], [0.1, 0.1], [0.6, 0.6], [0.9, 0.9], [0.2, 0.2], ]).ravel(order='C') latticemap_true = LatticeMap(nodes, values) file = PATH_TESTFILES + 'latticemap3d_simple.txt' latticemap_test = readfile_latticemap(file) self.assertEqual(latticemap_true, latticemap_test)
def compute_condition_map(self, conditions=None, validate=False): """Computes a lattice map representing the union of a set of conditions. Args: conditions (list, optional): Set of conditions of either strings (that indicate the path of a conditioning file), array_like objects (that represent conditioning points), or conditioning lattice maps. validate (bool, optional): Inspect the given conditioning with respect to the sampler settings and if necessary correct it. Returns: LatticeMap: Conditioning map. """ if not conditions: ones = np.ones(self._lattice.nnodes_dim, dtype=bool) return LatticeMap(self._lattice, ones) # Iteratively transform all conditions into lattice maps cond_maps = [] for cond in conditions: if isinstance(cond, str): # conditioning FILE file = cond cond_map = self._cond_map_from_str(file) elif isinstance(cond, LatticeMap): # conditioning MAP cond_map = cond elif len(cond) == self._lattice.ndim: # conditioning POINT components = np.asarray(cond) cond_map = self._cond_map_from_point(components) else: raise ValueError(msg.err3004(cond)) cond_maps.append(cond_map) # Reduce set of conditioning by multplication of all maps map_ = functools.reduce(lambda a, b: a * b, cond_maps) # Validate final conditioning map if validate: self._validate_conditioning(map_) return map_
def _cond_map_from_str(self, file): """Loads a conditioning map from a saved file. !!! ATTENTION !!! This map invertes the values. AMS is used to indicate blocked nodes by `1` and allowed nodes by `0`. However, for the sampling it is more convenient to use it the other way around. Therefore, the values are inverted withing this function. """ # Read the conditioning file nodes, values = readfile_nodes_values(file) # Invert the values (c.f. docstring) values = utl.ams_val_map_to_bool_map(values) # Check consistency of the lattices if Lattice(nodes) != self._lattice: raise ValueError(msg.err3001(file, self._lattice)) return LatticeMap(nodes, values)
def to_latticemap(self, lattice, dtype=None): """Generates a boolean lattice map indicating the trajectory. Args: lattice (Lattice): Object defining the computational lattice. dtype (data-type, optional): The desired data-type for the map values. If not given, then the type will be determined as the minimum requirement by `numpy`. Returns: LatticeMap: Boolean trajectory according to a lattice grid. """ values = np.zeros(lattice.nnodes, dtype=bool) # Make similar (closest node) computation iteratively for each point for components in self: linear_index = lattice.linear_index_from_point(components) values[linear_index] = True return LatticeMap(lattice, values, dtype=dtype)
def test_LatticeMap2D__getitem__(self): lattice = Lattice(self.nodes2D) nodes = lattice.nodes nnodes_dim = lattice.nnodes_dim values = np.random.randn(lattice.nnodes) latticemap = LatticeMap(lattice, values) nx_st, nx_end = 2, 4 ny_st, ny_end = 0, 3 ind_x = np.arange(nx_st, nx_end) ind_y = np.arange(ny_st, ny_end) latticemap_x = latticemap[nx_st, :] lin_ind_x = np.array( [nx_st + iy * nnodes_dim[0] for iy in range(nnodes_dim[1])]) latticemap_xslice = latticemap[nx_st:nx_end, :] lin_ind_xslice = np.array([ ix + iy * nnodes_dim[0] for iy in range(nnodes_dim[1]) for ix in ind_x ]) latticemap_y = latticemap[:, ny_st] lin_ind_y = np.array( [ix + ny_st * nnodes_dim[0] for ix in range(nnodes_dim[0])]) latticemap_yslice = latticemap[:, ny_st:ny_end] lin_ind_yslice = np.array([ ix + iy * nnodes_dim[0] for iy in ind_y for ix in range(nnodes_dim[0]) ]) latticemap_xyslice = latticemap[nx_st:nx_end, ny_st:ny_end] lin_ind_xyslice = np.array( [ix + iy * nnodes_dim[0] for iy in ind_y for ix in ind_x]) self.assertTrue(latticemap_x == LatticeMap(lattice=lattice[nx_st, :], values=values[lin_ind_x])) self.assertTrue(latticemap_xslice == LatticeMap( lattice=lattice[nx_st:nx_end, :], values=values[lin_ind_xslice])) self.assertTrue(latticemap_y == LatticeMap(lattice=lattice[:, ny_st], values=values[lin_ind_y])) self.assertTrue(latticemap_yslice == LatticeMap( lattice=lattice[:, ny_st:ny_end], values=values[lin_ind_yslice])) self.assertTrue( latticemap_xyslice == LatticeMap(lattice=lattice[nx_st:nx_end, ny_st:ny_end], values=values[lin_ind_xyslice]))
def test_GantryDominant2D_set_prior_cond_with_str(self): nodes = [[1, 2, 3], [4, 5, 6, 7, 8, 9, 10]] lattice = Lattice(nodes) conditions = [PATH_TESTFILES + 'condmap2d_simple.txt'] values_true = np.array([ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, ], dtype=bool) prior_cond_true = LatticeMap(nodes, values_true) ratio = 1.0 sampler = GantryDominant2D(lattice, ratio=ratio) sampler.set_prior_cond(conditions) self.assertEqual(sampler._prior_cond, prior_cond_true)
def test_LatticeMap2D_gen(self): lattice = Lattice(self.nodes2D) values = np.random.randn(lattice.nnodes) latticemap = LatticeMap(lattice, values) self.assertTrue(isinstance(latticemap, LatticeMap)) self.assertTrue(np.all(values == latticemap.values)) with self.assertRaises(ValueError): LatticeMap(lattice, values[:-1]) with self.assertRaises(ValueError): LatticeMap(lattice, []) latticemap = LatticeMap(self.nodes2D, values) self.assertTrue(isinstance(latticemap, LatticeMap)) self.assertTrue(np.all(values == latticemap.values)) with self.assertRaises(ValueError): LatticeMap(self.nodes2D, values[:-1]) with self.assertRaises(ValueError): LatticeMap(self.nodes2D, [])
class GantryDominant2D(Sampler): """`GantryDominant2D` is a 2D gantry dominated trajectory sampler. It is designed to sample trajectories in two-dimensional domains as e.g.:: -------------------------------------- | **** 0000000000 | | *** ** * 0000000000 | ** ****** * 000000 | | * 00 | DIM_TABLE | 00 * | | 000000 ***** **** Trajectory | 0000000000 *** *** | | 0000000000 ** | -------------------------------------- DIM_GANTRY where the zeros indicate prior conditioning. The trajectories are guided by a prior probability map describing higher and lower passage frequencies. Notes If the ratio is too small, the trajectory can not jump to upper or lower neighbors but only straight right (or left). Consider the following setup:: ________________________________ | | | | | | | B | E | H | |_______|_______|_______|_______| | | | | | DIM_TABLE | A | C | F | I | |_______|_______|_______|_______| | | | | | | | D | G | J | |_______|_______|_______|_______| DIM_GANTRY where the nodes are chosen to have horizontal and vertical spread of 1.0. Let us fix a ratio value of 0.5. This means that when moving from the first row to the second (distance=1.0) the trajectory is free to move up or down anything between +0.5 and -0.5 (distance*ratio = 0.5). We observe that in this case, it is not possible to jump from A to B or D, because the vertical difference is 1.0 which is higher than 0.5. Therefore, from A it is only possible to move to C. Considering the spread across two rows (distance=2.0), we have a vertical freedom of 1.0. This would technically allow to start at A and end on E. However, due to the discretization of the grid it would request to go the path A-C-E or A-B-E what would violate at least once the maximum ratio of 0.5. In such cases it is better to adapt the grid and design it more according to the desired ratio as e.g:: ________________________________ | | | | | B | |_______________|_______________| | | | | A | C | |_______________|_______________| | | | | | D | |_______________|_______________| In the latter setup we can keep a ratio of 0.5 and it would be allowed to jump from A to B or from A to D. Args lattice (Lattice or list of array_like): Object defining the computational lattice. ratio (float): Ratio describing the agility of the table with respect to the gantry. order (str {'random', 'max_val', 'max_random'}, optional): Order in which the trajectory points are sampled. 'random' randomly selects one gantry position after the other. 'max_val' treats the gantry positions according to the maximum value in the prior probability map. 'max_random' samples positions according to their maximum value in the prior probability map. """ def __call__(self, conditions=None, validate=False, seed=None): logging.info(f'Start Path Sampling') # Set the random seed for reproducibility and write it to the logger if seed is None: seed = np.random.get_state()[1][0] np.random.seed(seed) logging.info(f'Numpy seed is {seed}') # Sample path and generate a `Trajectory` object points = self._compute_trajectory_points(conditions, validate) return Trajectory(points) def __init__(self, lattice, ratio=1.0, order='random'): # Check the ratio with respect to the lattice, for more information # consult the docstring in :meth:`_check_ratio` self._validate_ratio(lattice, ratio) # Initialize the values super().__init__(lattice) self._ratio = ratio if order not in ['random', 'max_val', 'max_random']: raise ValueError(msg.err3005(order)) self._order = order self._graph = None def __repr__(self): cls_name = type(self).__name__ return f'{cls_name}(lattice={self._lattice}, ' \ f'ratio={repr(self._ratio)})' def __str__(self): return self.__repr__() # Public Methods def compute_adjacency_graph(self): """Computes and stores the adjacency graph of the sampler. Not that we consider a directed graph where the edges always go from the lower to the higher nodes such that the graph:: 2---3 / \ | / \ | / \| 1---4---5 would give the adjacency matrix:: | 1 2 3 4 5 --|-------------------- 1 | * * 2 | * * 3 | * 4 | * 5 | In this matrix, the rows represent 'arriving' and the columns 'departing' edges. """ graph = self._adjacency_graph() self._graph = graph def compute_condition_map(self, conditions=None, validate=False): """Computes a lattice map representing the union of a set of conditions. Args: conditions (list, optional): Set of conditions of either strings (that indicate the path of a conditioning file), array_like objects (that represent conditioning points), or conditioning lattice maps. validate (bool, optional): Inspect the given conditioning with respect to the sampler settings and if necessary correct it. Returns: LatticeMap: Conditioning map. """ if not conditions: ones = np.ones(self._lattice.nnodes_dim, dtype=bool) return LatticeMap(self._lattice, ones) # Iteratively transform all conditions into lattice maps cond_maps = [] for cond in conditions: if isinstance(cond, str): # conditioning FILE file = cond cond_map = self._cond_map_from_str(file) elif isinstance(cond, LatticeMap): # conditioning MAP cond_map = cond elif len(cond) == self._lattice.ndim: # conditioning POINT components = np.asarray(cond) cond_map = self._cond_map_from_point(components) else: raise ValueError(msg.err3004(cond)) cond_maps.append(cond_map) # Reduce set of conditioning by multplication of all maps map_ = functools.reduce(lambda a, b: a * b, cond_maps) # Validate final conditioning map if validate: self._validate_conditioning(map_) return map_ def set_prior_cond(self, conditions=None, validate=False): """Sets the prior conditioning map of the sampler with the possibility to validate it. The prior probability map is updated whenever a new prior conditioning map is set. Args: conditions (list, optional): Set of conditions of either strings (that indicate the path of a conditioning file), array_like objects (that represent conditioning points), or conditioning lattice maps. validate (bool, optional): Inspect the given conditioning with respect to the sampler settings and if necessary correct it. Returns: None """ # Get conditioning map map_ = self.compute_condition_map(conditions, validate) # Minor consistency check if self._lattice != map_.lattice: raise ValueError(msg.err3000(self._lattice, map_.lattice)) # Set value within self self._prior_cond = map_ def set_prior_prob(self, map_, energy=False): """Sets the prior probability attribute of the sampler. It is possible to provide prior probability values (`energy=False`) or probability energy values (`energy=True`). In the latter case, the values are transformed to un-normalized probability measures by:: prior_prob.values = exp(-map_.values) where `exp` denotes the exponential function. Args: map_ (LatticeMap): Prior map energy (bool): Indicates if the prior map represents probability values (false) or probability energy (true). Returns: None """ # Minor consistency checks if self._lattice != map_.lattice: raise ValueError(msg.err3000(self._lattice, map_.lattice)) # If the map represents the energy, transform it to probability values values = np.copy(map_.values) if energy: values = np.exp(-values) # Set value within self self._prior_prob = LatticeMap(self._lattice, values) # Private Methods def _adjacency_graph(self): """Computes an adjacency graph based on a computational lattice. The entry (i, j) of the graph matrix is non-zero if there is a connection starting from node i and joining j. For more information see docstring of :meth:`compute_adjacency_graph`. Returns ndarray: Matrix representing the directed graph """ # Gantry/Table nodes lattice = self._lattice nodes_gantry = lattice.nodes[DIM_GANTRY] nodes_table = lattice.nodes[DIM_TABLE] # Get an array containing the linear indices lin_ind = self._linear_indices() shape, dim = lin_ind.shape, (len(nodes_gantry), len(nodes_table)) if shape != dim: raise ValueError(msg.err3002(shape, dim)) # Iteratively go through the nodes and generate adjacency graph ratio = 2 * self._ratio # The opening is symmetric pts = nodes_table graph = np.zeros((lattice.nnodes, ) * 2, dtype=bool) for igan, node_g in enumerate(nodes_gantry[:-1]): # Conical opening for the permitted area of the trajectory dist = abs(node_g - nodes_gantry[igan + 1]) for itab, node_t in enumerate(nodes_table): cntr = node_t allowed = utl.within_conical_opening(cntr, dist, ratio, pts) from_node = lin_ind[igan, itab] to_nodes = lin_ind[igan + 1, allowed] graph[from_node, to_nodes] = True return graph def _compute_trajectory_points(self, conditions=None, validate=False): """Samples the trajectory points to generate a trajectory reaching through the gantry dimension of the lattice. Args: conditions (list, optional): Set of conditions of either strings (that indicate the path of a conditioning file), array_like objects (that represent conditioning points), or conditioning lattice maps. validate (bool, optional): Inspect the given conditioning with respect to the sampler settings and if necessary correct it. Returns: list of tuples: List of sampled points """ # Initialization nodes_gantry = self._lattice.nodes[DIM_GANTRY] nodes_table = self._lattice.nodes[DIM_TABLE] points = [ None, ] * len(nodes_gantry) # Compute (and eventually validate) the global condition map if not conditions: conditions = [] conditions.append(self.prior_cond) cond_map = self.compute_condition_map(conditions, validate) # Iteratively sample the points following a given gantry order for ipos in self._sampling_indices(): # Compute the distribution in the slice at position `ipos` prior_slice = self._prior_prob.slice(DIM_GANTRY, ipos) cond_slice = cond_map.slice(DIM_GANTRY, ipos) distribution = prior_slice * cond_slice # Normalize the distribution values try: distribution = distribution.normalize_sum() except (ValueError, TypeError) as e: # Write logging file entry logging.error(f'No possible path passage at ipos={ipos}') # Save current permission map to a text file map_ = self._prior_prob * cond_map map_.to_txt(os.path.join(_LOC_FOLDER, LOG_MAP_NAME)) raise e # Select table trajectory point according to the distribution p_choice = distribution.values pos_table = np.random.choice(nodes_table, p=p_choice) # Generate and set new trajectory point new_point = [ None, ] * self._lattice.ndim new_point[DIM_GANTRY] = nodes_gantry[ipos] new_point[DIM_TABLE] = pos_table points[ipos] = tuple(new_point) # Update the restriction map by conditioning the new point cond_map *= self._cond_map_from_point(new_point) return points def _cond_map_from_point(self, components): """Generate a conditioning lattice map from a conditioning point.""" # Initialization nodes = self._lattice.nodes nnodes_dim = self._lattice.nnodes_dim # Generate array with lowest dimension corresponding to the gantry dim = (nnodes_dim[DIM_GANTRY], nnodes_dim[DIM_TABLE]) values = np.zeros(dim, dtype=bool) # Conical opening for the permitted area of the trajectory center = components[DIM_TABLE] con_rat = 2 * self._ratio # The conical opening is symmetric pts = nodes[DIM_TABLE] # Loop in gantry direction through the lattice for inode, node in enumerate(nodes[DIM_GANTRY]): dist = abs(node - components[DIM_GANTRY]) allowed = utl.within_conical_opening(center, dist, con_rat, pts) values[inode, allowed] = True # Correct dimension ordering if needed if DIM_GANTRY > DIM_TABLE: values = values.transpose() values = values.ravel(order=NP_ORDER) return LatticeMap(self._lattice, values) def _cond_map_from_str(self, file): """Loads a conditioning map from a saved file. !!! ATTENTION !!! This map invertes the values. AMS is used to indicate blocked nodes by `1` and allowed nodes by `0`. However, for the sampling it is more convenient to use it the other way around. Therefore, the values are inverted withing this function. """ # Read the conditioning file nodes, values = readfile_nodes_values(file) # Invert the values (c.f. docstring) values = utl.ams_val_map_to_bool_map(values) # Check consistency of the lattices if Lattice(nodes) != self._lattice: raise ValueError(msg.err3001(file, self._lattice)) return LatticeMap(nodes, values) def _linear_indices(self): """Returns the two dimensional array of the linear indices. The lower dimension of the produced array always considers the GANTRY dimension. The setting (DIM_GANTRY < DIM_TABLE and NP_ORDER='F') then produces the same outcome as (DIM_GANTRY > DIM_TABLE and NP_ORDER='C') because in both cases, the linear counter first considers (and increases) along the gantry dimension. Similar reasoning goes for the two settings (DIM_GANTRY > DIM_TABLE and NP_ORDER='F') and (DIM_GANTRY < DIM_TABLE and NP_ORDER='C') because the linear counter first considers (and increases) along the table dimension. """ linear_indices = self._lattice.linear_indices() # As mentioned in the docstring, lower dimension corresponds to gantry if DIM_GANTRY > DIM_TABLE: linear_indices = linear_indices.transpose() return linear_indices def _sampling_indices(self): """This method is used to define the order in which the trajectory points are sampled. There are the following possibilities: - 'random': Randomly choose positions indices, - 'max_val': For each position, compute the maximum value in the prior probability map (i.e. np.max(..., axis=DIM_TABLE) and sort positions indices accordingly in descending order, - 'max_random': As above, but rather than ordering, we sample position indices according to these values. Returns: ndarray: Position indices. """ order = self._order lattice = self._lattice def _max_vals(): """Computing the maximum probability values in table direction. """ values = (self._prior_prob * self.prior_cond).values values = values.reshape(lattice.nnodes_dim, order=NP_ORDER) return np.max(values, axis=DIM_TABLE) if order == 'random': pos = np.arange(lattice.nnodes_dim[DIM_GANTRY]) np.random.shuffle(pos) elif order == 'max_val': pos = np.argsort(_max_vals())[::-1] elif order == 'max_random': prob = _max_vals() prob /= np.sum(prob) npos = lattice.nnodes_dim[DIM_GANTRY] pos = np.arange(npos) pos = np.random.choice(pos, size=npos, replace=False, p=prob) else: raise ValueError(msg.err3005(order)) return pos def _validate_conditioning(self, map_): """Checks if a given conditioning map is valid with respect to the sampler settings. For doing so, we check if each node is potentially reachable by a trajectory that goes through the entire gantry range. If not, the node is set to be 'blocked'. Args: map_ (LatticeMap): Conditioning map """ # The validity of the conditioning us checked by using an adjacency # graph to see if all nodes can be reached f if not self._graph: self.compute_adjacency_graph() graph = np.array(self._graph, copy=True) # Make the changes to an shallow copy of map_.values values = map_.values # Initialize set of nodes to be checked inodes_control = set(np.where(np.logical_not(values))[0]) # Boundary nodes lin_ind = self._linear_indices() no_out = lin_ind[-1, :] # boundary nodes with no outgoing edges no_in = lin_ind[0, :] # boundary nodes with no incoming edges # Loop until there is no potential issue anymore while len(inodes_control) > 0: # Pop next node index to be checked inode = inodes_control.pop() # Get incoming and outgoing edges for the given node index `inode` outgoing = np.where(graph[inode, :])[0] incoming = np.where(graph[:, inode])[0] # Block node if there is no path passing through it issue_out = len(outgoing) == 0 and inode not in no_out issue_in = len(incoming) == 0 and inode not in no_in if not values[inode] or issue_out or issue_in: # Block the specific node values[inode] = False # Remove node edges from graph graph[inode, :] = False graph[:, inode] = False # Add the modified nodes to the control set inodes_control = inodes_control.union(outgoing) inodes_control = inodes_control.union(incoming) @staticmethod def _validate_ratio(lattice, ratio): """Checks if the ratio corresponds well with the lattice definitions. For mor information see the docstring of this class. The ratio can be interpreted as: ratio = d_table / d_gantry. In this function we test if the given ratio is sufficient to make the largest table gap when facing the smallest gantry gap. """ nodes = lattice.nodes spacing_gantry = nodes[DIM_GANTRY][1:] - nodes[DIM_GANTRY][:-1] spacing_table = nodes[DIM_TABLE][1:] - nodes[DIM_TABLE][:-1] ratio_max = np.max(spacing_table) / np.min(spacing_gantry) if ratio < ratio_max: raise ValueError(msg.err3003)
def test_GantryDominant2D_set_prior_cond_with_map(self): nodes = [[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], [0, 1, 2, 3, 4, 5]] lattice = Lattice(nodes) values = np.array([ 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, ], dtype=bool) conditions = [LatticeMap(nodes, values)] values_true = np.array([ 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, ], dtype=bool) prior_cond_true = LatticeMap(nodes, values_true) ratio = 2.0 sampler = GantryDominant2D(lattice, ratio=ratio) sampler.set_prior_cond(conditions, validate=True) self.assertEqual(sampler._prior_cond, prior_cond_true) values_true = np.array([ 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, ], dtype=bool) prior_cond_true = LatticeMap(nodes, values_true) ratio = 1.0 sampler = GantryDominant2D(lattice, ratio=ratio) sampler.set_prior_cond(conditions, validate=True) self.assertEqual(sampler._prior_cond, prior_cond_true)
def test_GantryDominant2D__cond_map_from_point(self): nodes = [[-2, -1, 0, 1, 2], [-2, 0, 2]] lattice = Lattice(nodes) conditions = [(0, 0)] values_true = np.array([ 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, ], dtype=bool) prior_cond_true = LatticeMap(nodes, values_true) ratio = 2.0 sampler = GantryDominant2D(lattice, ratio=ratio) sampler.set_prior_cond(conditions) self.assertEqual(sampler._prior_cond, prior_cond_true) conditions = [(-2, -2)] values_true = np.array([ 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, ], dtype=bool) prior_cond_true = LatticeMap(nodes, values_true) ratio = 2.0 sampler = GantryDominant2D(lattice, ratio=ratio) sampler.set_prior_cond(conditions) self.assertEqual(sampler._prior_cond, prior_cond_true) conditions = [(1, -2)] values_true = np.array([ 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, ], dtype=bool) prior_cond_true = LatticeMap(nodes, values_true) ratio = 4.0 sampler = GantryDominant2D(lattice, ratio=ratio) sampler.set_prior_cond(conditions) self.assertEqual(sampler._prior_cond, prior_cond_true)