fizgrid.utils
1import math, type_enforced 2 3 4class IDGenerator: 5 """ 6 A class to generate unique identifiers. 7 """ 8 9 def __init__(self): 10 self.__id__ = 0 11 12 def __call__(self): 13 """ 14 Generates the next unique identifier. 15 """ 16 self.__id__ += 1 17 return str(self.__id__) 18 19 20unique_id = IDGenerator() 21 22 23@type_enforced.Enforcer(enabled=True) 24class Shape: 25 @staticmethod 26 def circle( 27 radius: int | float, num_points: int = 6, round_to: int = 2 28 ) -> list[list]: 29 """ 30 Returns a list of addative coordinates that form a circle around a given point. 31 32 Args: 33 34 - radius (int|float): Radius of the circle. 35 - num_points (int): Number of points to generate around the circle. 36 - Default: 6 37 - This is used to determine the number of points to generate around the circle. 38 - round_to (int): Number of decimal places to round to. 39 - Default: 2 40 - This is used to round the coordinates to a specific number of decimal places. 41 42 Returns: 43 44 - list[list]: A list of coordinates representing the circle. 45 - Each coordinate is a list of two values [x, y]. 46 - The coordinates are relative to the center of the circle. 47 """ 48 return [ 49 [ 50 round( 51 radius * math.cos(2 * math.pi / num_points * i), round_to 52 ), 53 round( 54 radius * math.sin(2 * math.pi / num_points * i), round_to 55 ), 56 ] 57 for i in range(num_points) 58 ] 59 60 @staticmethod 61 def rectangle( 62 x_len: float | int, y_len: float | int, round_to: int = 2 63 ) -> list[list]: 64 """ 65 Returns a list of addative coordinates that form a rectangle around a given point. 66 67 Args: 68 69 - x_len (float|int): Length of the rectangle along the x-axis. 70 - y_len (float|int): Length of the rectangle along the y-axis. 71 - round_to (int): Number of decimal places to round to. 72 - Default: 2 73 - This is used to round the coordinates to a specific number of decimal places. 74 75 Returns: 76 77 - list[list]: A list of coordinates representing the rectangle. 78 - Each coordinate is a list of two values [x, y]. 79 - The coordinates are relative to the center of the rectangle. 80 """ 81 return [ 82 [round(x_len / 2, round_to), round(y_len / 2, round_to)], 83 [round(-x_len / 2, round_to), round(y_len / 2, round_to)], 84 [round(-x_len / 2, round_to), round(-y_len / 2, round_to)], 85 [round(x_len / 2, round_to), round(-y_len / 2, round_to)], 86 ] 87 88 @staticmethod 89 def rotate( 90 radians: float | int, shape: list[list[float | int]], round_to: int = 2 91 ) -> list[list[float | int]]: 92 """ 93 Rotates a shape by a given angle in radians around its origin (0, 0). 94 Args: 95 - radians (float|int): The angle in radians to rotate the shape. 96 - shape (list[list[float|int]]): The shape to rotate. 97 - This is a list of coordinates representing the shape. 98 - Each coordinate is a list of two values [x, y]. 99 Returns: 100 - list[list[float|int]]: The rotated shape. 101 - This is a list of coordinates representing the rotated shape. 102 - Each coordinate is a list of two values [x, y]. 103 """ 104 return [ 105 [ 106 round( 107 coord[0] * math.cos(radians) - coord[1] * math.sin(radians), 108 round_to, 109 ), 110 round( 111 coord[0] * math.sin(radians) + coord[1] * math.cos(radians), 112 round_to, 113 ), 114 ] 115 for coord in shape 116 ] 117 118 @staticmethod 119 def get_rotated_shape( 120 shape: list[list[float | int]], 121 x_shift: float | int, 122 y_shift: float | int, 123 ): 124 """ 125 Rotates the shape to align with the direction of motion. 126 127 Args: 128 129 - shape (list[list[float|int]]): List of coordinates representing the shape's vertices relative to its center. 130 - x_shift (float|int): Total distance the shape moves along the x-axis during [t_start, t_end]. 131 - y_shift (float|int): Total distance the shape moves along the y-axis during [t_start, t_end]. 132 133 Returns: 134 135 - list[list[float|int]]: The rotated shape. 136 - This is a list of coordinates representing the rotated shape. 137 - Each coordinate is a list of two values [x, y]. 138 """ 139 if x_shift == 0 and y_shift == 0: 140 return shape 141 radians = math.atan2(y_shift, x_shift) 142 return Shape.rotate(radians=radians, shape=shape) 143 144 145class ShapeMoverUtils: 146 @staticmethod 147 def moving_segment_overlap_intervals( 148 seg_start: int | float, 149 seg_end: int | float, 150 t_start: int | float, 151 t_end: int | float, 152 shift: int | float, 153 ): 154 """ 155 Calculates the time intervals during which a moving 1D line segment overlaps with each unit-length 156 integer-aligned range along the x-axis. 157 158 Args: 159 160 - seg_start (int|float): Initial position of the left end of the line segment. 161 - seg_end (int|float): Initial position of the right end of the line segment. 162 - t_start (int|float): Start time of the motion. 163 - t_end (int|float): End time of the motion. 164 - shift (int|float): Total distance the line segment moves along the x-axis during [t_start, t_end]. 165 166 Returns: 167 168 - dict[int, tuplie(int|float,int|float)]: A dictionary mapping each integer `i` to the time interval [t_in, t_out] 169 during which any part of the line overlaps the range [i, i+1). 170 Only includes ranges with non-zero overlap duration. 171 """ 172 duration = t_end - t_start 173 velocity = shift / duration if duration != 0 else 0 174 175 result = {} 176 177 final_start = seg_start + shift 178 final_end = seg_end + shift 179 global_min = min(seg_start, final_start) 180 global_max = max(seg_end, final_end) 181 182 for i in range(int(global_min) - 1, int(global_max) + 2): 183 if velocity == 0: 184 if seg_end > i and seg_start < i + 1 and t_start < t_end: 185 result[i] = (t_start, t_end) 186 continue 187 # Solve for times when the line enters and exits overlap with [i, i+1) 188 t1 = (i - seg_end) / velocity + t_start 189 t2 = (i + 1 - seg_start) / velocity + t_start 190 entry_time = max(min(t1, t2), t_start) 191 exit_time = min(max(t1, t2), t_end) 192 193 if exit_time > entry_time: 194 result[i] = (entry_time, exit_time) 195 196 return result 197 198 @staticmethod 199 def moving_rectangle_overlap_intervals( 200 x_start: float | int, 201 x_end: float | int, 202 y_start: float | int, 203 y_end: float | int, 204 x_shift: float | int, 205 y_shift: float | int, 206 t_start: float | int, 207 t_end: float | int, 208 ): 209 """ 210 Calculates the time intervals during which a moving rectangle overlaps with each unit-length 211 integer-aligned range along the x and y axes. 212 213 Args: 214 215 - x_start (float|int): Initial position of the left end of the rectangle along the x-axis. 216 - x_end (float|int): Initial position of the right end of the rectangle along the x-axis. 217 - y_start (float|int): Initial position of the bottom end of the rectangle along the y-axis. 218 - y_end (float|int): Initial position of the top end of the rectangle along the y-axis. 219 - x_shift (float|int): Total distance the rectangle moves along the x-axis during [t_start, t_end]. 220 - y_shift (float|int): Total distance the rectangle moves along the y-axis during [t_start, t_end]. 221 - t_start (float|int): Start time of the motion. 222 - t_end (float|int): End time of the motion. 223 224 Returns: 225 226 - dict[tuple(int,int),tuple(int|float,int|float)]: A dictionary mapping each integer (i,j) to the time interval [t_in, t_out] 227 during which any part of the rectangle overlaps the range [i, i+1) x [j, j+1). 228 Only includes ranges with non-zero overlap duration. 229 230 """ 231 x_intervals = ShapeMoverUtils.moving_segment_overlap_intervals( 232 seg_start=x_start, 233 seg_end=x_end, 234 t_start=t_start, 235 t_end=t_end, 236 shift=x_shift, 237 ) 238 y_intervals = ShapeMoverUtils.moving_segment_overlap_intervals( 239 seg_start=y_start, 240 seg_end=y_end, 241 t_start=t_start, 242 t_end=t_end, 243 shift=y_shift, 244 ) 245 result = {} 246 for x_key, x_interval in x_intervals.items(): 247 for y_key, y_interval in y_intervals.items(): 248 # Only add intervals with time overlap 249 if ( 250 x_interval[1] > y_interval[0] 251 and y_interval[1] > x_interval[0] 252 ): 253 result[(x_key, y_key)] = ( 254 max(x_interval[0], y_interval[0]), 255 min(x_interval[1], y_interval[1]), 256 ) 257 258 return result 259 260 @staticmethod 261 def argmin(lst): 262 return min(enumerate(lst), key=lambda x: x[1])[0] 263 264 @staticmethod 265 def argmax(lst): 266 return max(enumerate(lst), key=lambda x: x[1])[0] 267 268 @staticmethod 269 def find_extreme_orthogonal_vertices( 270 points: list[tuple[int | float, int | float]], slope: float | int 271 ): 272 """ 273 Finds the points in a list that are the furthest apart in the direction 274 orthogonal to the given slope. This is useful for finding the extreme 275 points in a set of 2D coordinates that are orthogonal to a given line. 276 277 Args: 278 279 - points (list of tuples): A list of (x, y) coordinates. 280 - slope (float): The slope of the line. 281 282 Returns: 283 284 - tuple: The points with the minimum and maximum projections. 285 """ 286 # Compute orthogonal slope 287 orthogonal = float("inf") if slope == 0 else -1 / slope 288 # Define direction projections (a normalized direction vector, but without the linear algebra) 289 # Handle vertical slope 290 if orthogonal == float("inf"): 291 x_proj = 0.0 292 y_proj = 1.0 293 else: 294 # Note: Technically normalized length is (1**2 + orthogonal**2)**.5, but we avoid the extra square for performance 295 length = (1 + orthogonal**2) ** 0.5 296 x_proj = 1.0 / length 297 y_proj = orthogonal / length 298 # Compute projections 299 projections = [x * x_proj + y * y_proj for x, y in points] 300 # Return the min and max projections 301 return ( 302 points[ShapeMoverUtils.argmin(projections)], 303 points[ShapeMoverUtils.argmax(projections)], 304 ) 305 306 @staticmethod 307 def find_extreme_orthogonal_vertices_simplified( 308 points: list[tuple[int | float, int | float]], slope: float | int 309 ): 310 """ 311 A simplified version of the function `find_extreme_orthogonal_vertices` 312 that assumes the slope is never 0. 313 314 Finds the points in a list that are the furthest apart in the direction 315 orthogonal to the given slope. This is useful for finding the extreme 316 points in a set of 2D coordinates that are orthogonal to a given line. 317 318 Args: 319 320 - points (list of tuples): A list of (x, y) coordinates. 321 - slope (float): The slope of the line. 322 - Note: This should never be 0 or infinite for this function. 323 324 Returns: 325 326 - tuple: The points with the minimum and maximum projections. 327 """ 328 # Compute orthogonal slope 329 orthogonal = -1 / slope 330 # Define direction projections (a normalized direction vector, but without the linear algebra) 331 # Note: Technically normalized length is (1**2 + orthogonal**2)**.5, but we avoid the extra square for performance 332 length = (1 + orthogonal**2) ** 0.5 333 projections = [ 334 x * 1.0 / length + y * orthogonal / length for x, y in points 335 ] 336 # Return the min and max verticies 337 vertex_1 = points[ShapeMoverUtils.argmin(projections)] 338 vertex_2 = points[ShapeMoverUtils.argmax(projections)] 339 if slope < 0: 340 return vertex_1, vertex_2 341 else: 342 return vertex_2, vertex_1 343 344 @staticmethod 345 def remove_untouched_intervals( 346 intervals: dict[tuple[int, int], tuple[int | float, int | float]], 347 slope: float | int, 348 absolute_shape: list[tuple[int | float, int | float]], 349 ): 350 """ 351 Removes unnecessary intervals from the dictionary of intervals. 352 353 Args: 354 355 - intervals (dict[tuple(int,int),tuple(int|float,int|float)]): A dictionary mapping each integer (i,j) to the time interval [t_in, t_out] 356 during which any part of the shape overlaps the range [i, i+1) x [j, j+1). 357 - slope (float|int): The slope of the line. 358 - absolute_shape (list(tuple[int|float, int|float])): A list of coordinates representing the shape's vertices relative to its center. 359 360 Returns: 361 362 - dict[tuple(int,int),tuple(int|float,int|float)]: A dictionary with unnecessary intervals removed. 363 """ 364 min_vertex, max_vertex = ( 365 ShapeMoverUtils.find_extreme_orthogonal_vertices_simplified( 366 points=absolute_shape, slope=slope 367 ) 368 ) 369 shape_min_intercept = min_vertex[1] - slope * min_vertex[0] 370 shape_max_intercept = max_vertex[1] - slope * max_vertex[0] 371 372 # Given the slope, determine which cell vertex to use for the max and min intercepts 373 # This is to ensure that the intervals are removed correctly 374 ltx_increment = 1 if slope < 0 else 0 375 gtx_increment = 0 if slope < 0 else 1 376 377 remove_keys = [] 378 for x_cell, y_cell in intervals.keys(): 379 cell_min_intercept = y_cell - (slope * (x_cell + gtx_increment)) 380 cell_max_intercept = (y_cell + 1) - ( 381 slope * (x_cell + ltx_increment) 382 ) 383 # If the intercept ranges do not overlap, remove the interval 384 if not ( 385 cell_min_intercept < shape_max_intercept 386 and shape_min_intercept < cell_max_intercept 387 ): 388 remove_keys.append((x_cell, y_cell)) 389 for key in remove_keys: 390 del intervals[key] 391 return intervals 392 393 @staticmethod 394 def moving_shape_overlap_intervals( 395 x_coord: float | int, 396 y_coord: float | int, 397 x_shift: float | int, 398 y_shift: float | int, 399 t_start: float | int, 400 t_end: float | int, 401 shape: list[list[float | int]], 402 cell_density: int = 1, 403 ): 404 """ 405 Calculates the time intervals during which a moving shape overlaps with each unit-length 406 integer-aligned range along the x and y axes. 407 408 Note: This converts each shape into a full bounding box rectangle and then uses the rectangle overlap function to calculate the intervals. 409 410 Args: 411 412 - x_coord (float|int): Initial x-coordinate of the shape's center. 413 - y_coord (float|int): Initial y-coordinate of the shape's center. 414 - x_shift (float|int): Total distance the shape moves along the x-axis during [t_start, t_end]. 415 - y_shift (float|int): Total distance the shape moves along the y-axis during [t_start, t_end]. 416 - t_start (float|int): Start time of the motion. 417 - t_end (float|int): End time of the motion. 418 - shape (list[list[float|int]]): List of coordinates representing the shape's vertices relative to its center. 419 - cell_density (int): The number of cells per unit of length. 420 421 422 Returns: 423 424 - dict[tuple(int,int),tuple(int|float,int|float)]: A dictionary mapping each integer (i,j) to the time interval [t_in, t_out] 425 during which any part of the shape overlaps the range [i, i+1) x [j, j+1). 426 Only includes ranges with non-zero overlap duration. 427 """ 428 # Get the overlap intervals for a rectangle that bounds the shape in cell space 429 absolute_shape = [ 430 [ 431 (x_coord + coord[0]) * cell_density, 432 (y_coord + coord[1]) * cell_density, 433 ] 434 for coord in shape 435 ] 436 # Calculate the rectangle overlap intervals in cell space 437 rectangle_overlap_intervals = ( 438 ShapeMoverUtils.moving_rectangle_overlap_intervals( 439 x_start=min([coord[0] for coord in absolute_shape]), 440 x_end=max([coord[0] for coord in absolute_shape]), 441 y_start=min([coord[1] for coord in absolute_shape]), 442 y_end=max([coord[1] for coord in absolute_shape]), 443 x_shift=x_shift * cell_density, 444 y_shift=y_shift * cell_density, 445 t_start=t_start, 446 t_end=t_end, 447 ) 448 ) 449 # If the shape is only moving vertically or horizontally, we can just return the rectangle overlap intervals 450 if x_shift == 0 or y_shift == 0: 451 return rectangle_overlap_intervals 452 # If the shape is moving diagonally, we should remove intervals that are never passed through by the shape 453 return ShapeMoverUtils.remove_untouched_intervals( 454 intervals=rectangle_overlap_intervals, 455 slope=y_shift / x_shift, 456 absolute_shape=absolute_shape, 457 )
5class IDGenerator: 6 """ 7 A class to generate unique identifiers. 8 """ 9 10 def __init__(self): 11 self.__id__ = 0 12 13 def __call__(self): 14 """ 15 Generates the next unique identifier. 16 """ 17 self.__id__ += 1 18 return str(self.__id__)
A class to generate unique identifiers.
24@type_enforced.Enforcer(enabled=True) 25class Shape: 26 @staticmethod 27 def circle( 28 radius: int | float, num_points: int = 6, round_to: int = 2 29 ) -> list[list]: 30 """ 31 Returns a list of addative coordinates that form a circle around a given point. 32 33 Args: 34 35 - radius (int|float): Radius of the circle. 36 - num_points (int): Number of points to generate around the circle. 37 - Default: 6 38 - This is used to determine the number of points to generate around the circle. 39 - round_to (int): Number of decimal places to round to. 40 - Default: 2 41 - This is used to round the coordinates to a specific number of decimal places. 42 43 Returns: 44 45 - list[list]: A list of coordinates representing the circle. 46 - Each coordinate is a list of two values [x, y]. 47 - The coordinates are relative to the center of the circle. 48 """ 49 return [ 50 [ 51 round( 52 radius * math.cos(2 * math.pi / num_points * i), round_to 53 ), 54 round( 55 radius * math.sin(2 * math.pi / num_points * i), round_to 56 ), 57 ] 58 for i in range(num_points) 59 ] 60 61 @staticmethod 62 def rectangle( 63 x_len: float | int, y_len: float | int, round_to: int = 2 64 ) -> list[list]: 65 """ 66 Returns a list of addative coordinates that form a rectangle around a given point. 67 68 Args: 69 70 - x_len (float|int): Length of the rectangle along the x-axis. 71 - y_len (float|int): Length of the rectangle along the y-axis. 72 - round_to (int): Number of decimal places to round to. 73 - Default: 2 74 - This is used to round the coordinates to a specific number of decimal places. 75 76 Returns: 77 78 - list[list]: A list of coordinates representing the rectangle. 79 - Each coordinate is a list of two values [x, y]. 80 - The coordinates are relative to the center of the rectangle. 81 """ 82 return [ 83 [round(x_len / 2, round_to), round(y_len / 2, round_to)], 84 [round(-x_len / 2, round_to), round(y_len / 2, round_to)], 85 [round(-x_len / 2, round_to), round(-y_len / 2, round_to)], 86 [round(x_len / 2, round_to), round(-y_len / 2, round_to)], 87 ] 88 89 @staticmethod 90 def rotate( 91 radians: float | int, shape: list[list[float | int]], round_to: int = 2 92 ) -> list[list[float | int]]: 93 """ 94 Rotates a shape by a given angle in radians around its origin (0, 0). 95 Args: 96 - radians (float|int): The angle in radians to rotate the shape. 97 - shape (list[list[float|int]]): The shape to rotate. 98 - This is a list of coordinates representing the shape. 99 - Each coordinate is a list of two values [x, y]. 100 Returns: 101 - list[list[float|int]]: The rotated shape. 102 - This is a list of coordinates representing the rotated shape. 103 - Each coordinate is a list of two values [x, y]. 104 """ 105 return [ 106 [ 107 round( 108 coord[0] * math.cos(radians) - coord[1] * math.sin(radians), 109 round_to, 110 ), 111 round( 112 coord[0] * math.sin(radians) + coord[1] * math.cos(radians), 113 round_to, 114 ), 115 ] 116 for coord in shape 117 ] 118 119 @staticmethod 120 def get_rotated_shape( 121 shape: list[list[float | int]], 122 x_shift: float | int, 123 y_shift: float | int, 124 ): 125 """ 126 Rotates the shape to align with the direction of motion. 127 128 Args: 129 130 - shape (list[list[float|int]]): List of coordinates representing the shape's vertices relative to its center. 131 - x_shift (float|int): Total distance the shape moves along the x-axis during [t_start, t_end]. 132 - y_shift (float|int): Total distance the shape moves along the y-axis during [t_start, t_end]. 133 134 Returns: 135 136 - list[list[float|int]]: The rotated shape. 137 - This is a list of coordinates representing the rotated shape. 138 - Each coordinate is a list of two values [x, y]. 139 """ 140 if x_shift == 0 and y_shift == 0: 141 return shape 142 radians = math.atan2(y_shift, x_shift) 143 return Shape.rotate(radians=radians, shape=shape)
26 @staticmethod 27 def circle( 28 radius: int | float, num_points: int = 6, round_to: int = 2 29 ) -> list[list]: 30 """ 31 Returns a list of addative coordinates that form a circle around a given point. 32 33 Args: 34 35 - radius (int|float): Radius of the circle. 36 - num_points (int): Number of points to generate around the circle. 37 - Default: 6 38 - This is used to determine the number of points to generate around the circle. 39 - round_to (int): Number of decimal places to round to. 40 - Default: 2 41 - This is used to round the coordinates to a specific number of decimal places. 42 43 Returns: 44 45 - list[list]: A list of coordinates representing the circle. 46 - Each coordinate is a list of two values [x, y]. 47 - The coordinates are relative to the center of the circle. 48 """ 49 return [ 50 [ 51 round( 52 radius * math.cos(2 * math.pi / num_points * i), round_to 53 ), 54 round( 55 radius * math.sin(2 * math.pi / num_points * i), round_to 56 ), 57 ] 58 for i in range(num_points) 59 ]
Returns a list of addative coordinates that form a circle around a given point.
Args:
- radius (int|float): Radius of the circle.
- num_points (int): Number of points to generate around the circle.
- Default: 6
- This is used to determine the number of points to generate around the circle.
- round_to (int): Number of decimal places to round to.
- Default: 2
- This is used to round the coordinates to a specific number of decimal places.
Returns:
- list[list]: A list of coordinates representing the circle.
- Each coordinate is a list of two values [x, y].
- The coordinates are relative to the center of the circle.
61 @staticmethod 62 def rectangle( 63 x_len: float | int, y_len: float | int, round_to: int = 2 64 ) -> list[list]: 65 """ 66 Returns a list of addative coordinates that form a rectangle around a given point. 67 68 Args: 69 70 - x_len (float|int): Length of the rectangle along the x-axis. 71 - y_len (float|int): Length of the rectangle along the y-axis. 72 - round_to (int): Number of decimal places to round to. 73 - Default: 2 74 - This is used to round the coordinates to a specific number of decimal places. 75 76 Returns: 77 78 - list[list]: A list of coordinates representing the rectangle. 79 - Each coordinate is a list of two values [x, y]. 80 - The coordinates are relative to the center of the rectangle. 81 """ 82 return [ 83 [round(x_len / 2, round_to), round(y_len / 2, round_to)], 84 [round(-x_len / 2, round_to), round(y_len / 2, round_to)], 85 [round(-x_len / 2, round_to), round(-y_len / 2, round_to)], 86 [round(x_len / 2, round_to), round(-y_len / 2, round_to)], 87 ]
Returns a list of addative coordinates that form a rectangle around a given point.
Args:
- x_len (float|int): Length of the rectangle along the x-axis.
- y_len (float|int): Length of the rectangle along the y-axis.
- round_to (int): Number of decimal places to round to.
- Default: 2
- This is used to round the coordinates to a specific number of decimal places.
Returns:
- list[list]: A list of coordinates representing the rectangle.
- Each coordinate is a list of two values [x, y].
- The coordinates are relative to the center of the rectangle.
89 @staticmethod 90 def rotate( 91 radians: float | int, shape: list[list[float | int]], round_to: int = 2 92 ) -> list[list[float | int]]: 93 """ 94 Rotates a shape by a given angle in radians around its origin (0, 0). 95 Args: 96 - radians (float|int): The angle in radians to rotate the shape. 97 - shape (list[list[float|int]]): The shape to rotate. 98 - This is a list of coordinates representing the shape. 99 - Each coordinate is a list of two values [x, y]. 100 Returns: 101 - list[list[float|int]]: The rotated shape. 102 - This is a list of coordinates representing the rotated shape. 103 - Each coordinate is a list of two values [x, y]. 104 """ 105 return [ 106 [ 107 round( 108 coord[0] * math.cos(radians) - coord[1] * math.sin(radians), 109 round_to, 110 ), 111 round( 112 coord[0] * math.sin(radians) + coord[1] * math.cos(radians), 113 round_to, 114 ), 115 ] 116 for coord in shape 117 ]
Rotates a shape by a given angle in radians around its origin (0, 0). Args:
- radians (float|int): The angle in radians to rotate the shape.
- shape (list[list[float|int]]): The shape to rotate.
- This is a list of coordinates representing the shape.
- Each coordinate is a list of two values [x, y]. Returns:
- list[list[float|int]]: The rotated shape.
- This is a list of coordinates representing the rotated shape.
- Each coordinate is a list of two values [x, y].
119 @staticmethod 120 def get_rotated_shape( 121 shape: list[list[float | int]], 122 x_shift: float | int, 123 y_shift: float | int, 124 ): 125 """ 126 Rotates the shape to align with the direction of motion. 127 128 Args: 129 130 - shape (list[list[float|int]]): List of coordinates representing the shape's vertices relative to its center. 131 - x_shift (float|int): Total distance the shape moves along the x-axis during [t_start, t_end]. 132 - y_shift (float|int): Total distance the shape moves along the y-axis during [t_start, t_end]. 133 134 Returns: 135 136 - list[list[float|int]]: The rotated shape. 137 - This is a list of coordinates representing the rotated shape. 138 - Each coordinate is a list of two values [x, y]. 139 """ 140 if x_shift == 0 and y_shift == 0: 141 return shape 142 radians = math.atan2(y_shift, x_shift) 143 return Shape.rotate(radians=radians, shape=shape)
Rotates the shape to align with the direction of motion.
Args:
- shape (list[list[float|int]]): List of coordinates representing the shape's vertices relative to its center.
- x_shift (float|int): Total distance the shape moves along the x-axis during [t_start, t_end].
- y_shift (float|int): Total distance the shape moves along the y-axis during [t_start, t_end].
Returns:
- list[list[float|int]]: The rotated shape.
- This is a list of coordinates representing the rotated shape.
- Each coordinate is a list of two values [x, y].
146class ShapeMoverUtils: 147 @staticmethod 148 def moving_segment_overlap_intervals( 149 seg_start: int | float, 150 seg_end: int | float, 151 t_start: int | float, 152 t_end: int | float, 153 shift: int | float, 154 ): 155 """ 156 Calculates the time intervals during which a moving 1D line segment overlaps with each unit-length 157 integer-aligned range along the x-axis. 158 159 Args: 160 161 - seg_start (int|float): Initial position of the left end of the line segment. 162 - seg_end (int|float): Initial position of the right end of the line segment. 163 - t_start (int|float): Start time of the motion. 164 - t_end (int|float): End time of the motion. 165 - shift (int|float): Total distance the line segment moves along the x-axis during [t_start, t_end]. 166 167 Returns: 168 169 - dict[int, tuplie(int|float,int|float)]: A dictionary mapping each integer `i` to the time interval [t_in, t_out] 170 during which any part of the line overlaps the range [i, i+1). 171 Only includes ranges with non-zero overlap duration. 172 """ 173 duration = t_end - t_start 174 velocity = shift / duration if duration != 0 else 0 175 176 result = {} 177 178 final_start = seg_start + shift 179 final_end = seg_end + shift 180 global_min = min(seg_start, final_start) 181 global_max = max(seg_end, final_end) 182 183 for i in range(int(global_min) - 1, int(global_max) + 2): 184 if velocity == 0: 185 if seg_end > i and seg_start < i + 1 and t_start < t_end: 186 result[i] = (t_start, t_end) 187 continue 188 # Solve for times when the line enters and exits overlap with [i, i+1) 189 t1 = (i - seg_end) / velocity + t_start 190 t2 = (i + 1 - seg_start) / velocity + t_start 191 entry_time = max(min(t1, t2), t_start) 192 exit_time = min(max(t1, t2), t_end) 193 194 if exit_time > entry_time: 195 result[i] = (entry_time, exit_time) 196 197 return result 198 199 @staticmethod 200 def moving_rectangle_overlap_intervals( 201 x_start: float | int, 202 x_end: float | int, 203 y_start: float | int, 204 y_end: float | int, 205 x_shift: float | int, 206 y_shift: float | int, 207 t_start: float | int, 208 t_end: float | int, 209 ): 210 """ 211 Calculates the time intervals during which a moving rectangle overlaps with each unit-length 212 integer-aligned range along the x and y axes. 213 214 Args: 215 216 - x_start (float|int): Initial position of the left end of the rectangle along the x-axis. 217 - x_end (float|int): Initial position of the right end of the rectangle along the x-axis. 218 - y_start (float|int): Initial position of the bottom end of the rectangle along the y-axis. 219 - y_end (float|int): Initial position of the top end of the rectangle along the y-axis. 220 - x_shift (float|int): Total distance the rectangle moves along the x-axis during [t_start, t_end]. 221 - y_shift (float|int): Total distance the rectangle moves along the y-axis during [t_start, t_end]. 222 - t_start (float|int): Start time of the motion. 223 - t_end (float|int): End time of the motion. 224 225 Returns: 226 227 - dict[tuple(int,int),tuple(int|float,int|float)]: A dictionary mapping each integer (i,j) to the time interval [t_in, t_out] 228 during which any part of the rectangle overlaps the range [i, i+1) x [j, j+1). 229 Only includes ranges with non-zero overlap duration. 230 231 """ 232 x_intervals = ShapeMoverUtils.moving_segment_overlap_intervals( 233 seg_start=x_start, 234 seg_end=x_end, 235 t_start=t_start, 236 t_end=t_end, 237 shift=x_shift, 238 ) 239 y_intervals = ShapeMoverUtils.moving_segment_overlap_intervals( 240 seg_start=y_start, 241 seg_end=y_end, 242 t_start=t_start, 243 t_end=t_end, 244 shift=y_shift, 245 ) 246 result = {} 247 for x_key, x_interval in x_intervals.items(): 248 for y_key, y_interval in y_intervals.items(): 249 # Only add intervals with time overlap 250 if ( 251 x_interval[1] > y_interval[0] 252 and y_interval[1] > x_interval[0] 253 ): 254 result[(x_key, y_key)] = ( 255 max(x_interval[0], y_interval[0]), 256 min(x_interval[1], y_interval[1]), 257 ) 258 259 return result 260 261 @staticmethod 262 def argmin(lst): 263 return min(enumerate(lst), key=lambda x: x[1])[0] 264 265 @staticmethod 266 def argmax(lst): 267 return max(enumerate(lst), key=lambda x: x[1])[0] 268 269 @staticmethod 270 def find_extreme_orthogonal_vertices( 271 points: list[tuple[int | float, int | float]], slope: float | int 272 ): 273 """ 274 Finds the points in a list that are the furthest apart in the direction 275 orthogonal to the given slope. This is useful for finding the extreme 276 points in a set of 2D coordinates that are orthogonal to a given line. 277 278 Args: 279 280 - points (list of tuples): A list of (x, y) coordinates. 281 - slope (float): The slope of the line. 282 283 Returns: 284 285 - tuple: The points with the minimum and maximum projections. 286 """ 287 # Compute orthogonal slope 288 orthogonal = float("inf") if slope == 0 else -1 / slope 289 # Define direction projections (a normalized direction vector, but without the linear algebra) 290 # Handle vertical slope 291 if orthogonal == float("inf"): 292 x_proj = 0.0 293 y_proj = 1.0 294 else: 295 # Note: Technically normalized length is (1**2 + orthogonal**2)**.5, but we avoid the extra square for performance 296 length = (1 + orthogonal**2) ** 0.5 297 x_proj = 1.0 / length 298 y_proj = orthogonal / length 299 # Compute projections 300 projections = [x * x_proj + y * y_proj for x, y in points] 301 # Return the min and max projections 302 return ( 303 points[ShapeMoverUtils.argmin(projections)], 304 points[ShapeMoverUtils.argmax(projections)], 305 ) 306 307 @staticmethod 308 def find_extreme_orthogonal_vertices_simplified( 309 points: list[tuple[int | float, int | float]], slope: float | int 310 ): 311 """ 312 A simplified version of the function `find_extreme_orthogonal_vertices` 313 that assumes the slope is never 0. 314 315 Finds the points in a list that are the furthest apart in the direction 316 orthogonal to the given slope. This is useful for finding the extreme 317 points in a set of 2D coordinates that are orthogonal to a given line. 318 319 Args: 320 321 - points (list of tuples): A list of (x, y) coordinates. 322 - slope (float): The slope of the line. 323 - Note: This should never be 0 or infinite for this function. 324 325 Returns: 326 327 - tuple: The points with the minimum and maximum projections. 328 """ 329 # Compute orthogonal slope 330 orthogonal = -1 / slope 331 # Define direction projections (a normalized direction vector, but without the linear algebra) 332 # Note: Technically normalized length is (1**2 + orthogonal**2)**.5, but we avoid the extra square for performance 333 length = (1 + orthogonal**2) ** 0.5 334 projections = [ 335 x * 1.0 / length + y * orthogonal / length for x, y in points 336 ] 337 # Return the min and max verticies 338 vertex_1 = points[ShapeMoverUtils.argmin(projections)] 339 vertex_2 = points[ShapeMoverUtils.argmax(projections)] 340 if slope < 0: 341 return vertex_1, vertex_2 342 else: 343 return vertex_2, vertex_1 344 345 @staticmethod 346 def remove_untouched_intervals( 347 intervals: dict[tuple[int, int], tuple[int | float, int | float]], 348 slope: float | int, 349 absolute_shape: list[tuple[int | float, int | float]], 350 ): 351 """ 352 Removes unnecessary intervals from the dictionary of intervals. 353 354 Args: 355 356 - intervals (dict[tuple(int,int),tuple(int|float,int|float)]): A dictionary mapping each integer (i,j) to the time interval [t_in, t_out] 357 during which any part of the shape overlaps the range [i, i+1) x [j, j+1). 358 - slope (float|int): The slope of the line. 359 - absolute_shape (list(tuple[int|float, int|float])): A list of coordinates representing the shape's vertices relative to its center. 360 361 Returns: 362 363 - dict[tuple(int,int),tuple(int|float,int|float)]: A dictionary with unnecessary intervals removed. 364 """ 365 min_vertex, max_vertex = ( 366 ShapeMoverUtils.find_extreme_orthogonal_vertices_simplified( 367 points=absolute_shape, slope=slope 368 ) 369 ) 370 shape_min_intercept = min_vertex[1] - slope * min_vertex[0] 371 shape_max_intercept = max_vertex[1] - slope * max_vertex[0] 372 373 # Given the slope, determine which cell vertex to use for the max and min intercepts 374 # This is to ensure that the intervals are removed correctly 375 ltx_increment = 1 if slope < 0 else 0 376 gtx_increment = 0 if slope < 0 else 1 377 378 remove_keys = [] 379 for x_cell, y_cell in intervals.keys(): 380 cell_min_intercept = y_cell - (slope * (x_cell + gtx_increment)) 381 cell_max_intercept = (y_cell + 1) - ( 382 slope * (x_cell + ltx_increment) 383 ) 384 # If the intercept ranges do not overlap, remove the interval 385 if not ( 386 cell_min_intercept < shape_max_intercept 387 and shape_min_intercept < cell_max_intercept 388 ): 389 remove_keys.append((x_cell, y_cell)) 390 for key in remove_keys: 391 del intervals[key] 392 return intervals 393 394 @staticmethod 395 def moving_shape_overlap_intervals( 396 x_coord: float | int, 397 y_coord: float | int, 398 x_shift: float | int, 399 y_shift: float | int, 400 t_start: float | int, 401 t_end: float | int, 402 shape: list[list[float | int]], 403 cell_density: int = 1, 404 ): 405 """ 406 Calculates the time intervals during which a moving shape overlaps with each unit-length 407 integer-aligned range along the x and y axes. 408 409 Note: This converts each shape into a full bounding box rectangle and then uses the rectangle overlap function to calculate the intervals. 410 411 Args: 412 413 - x_coord (float|int): Initial x-coordinate of the shape's center. 414 - y_coord (float|int): Initial y-coordinate of the shape's center. 415 - x_shift (float|int): Total distance the shape moves along the x-axis during [t_start, t_end]. 416 - y_shift (float|int): Total distance the shape moves along the y-axis during [t_start, t_end]. 417 - t_start (float|int): Start time of the motion. 418 - t_end (float|int): End time of the motion. 419 - shape (list[list[float|int]]): List of coordinates representing the shape's vertices relative to its center. 420 - cell_density (int): The number of cells per unit of length. 421 422 423 Returns: 424 425 - dict[tuple(int,int),tuple(int|float,int|float)]: A dictionary mapping each integer (i,j) to the time interval [t_in, t_out] 426 during which any part of the shape overlaps the range [i, i+1) x [j, j+1). 427 Only includes ranges with non-zero overlap duration. 428 """ 429 # Get the overlap intervals for a rectangle that bounds the shape in cell space 430 absolute_shape = [ 431 [ 432 (x_coord + coord[0]) * cell_density, 433 (y_coord + coord[1]) * cell_density, 434 ] 435 for coord in shape 436 ] 437 # Calculate the rectangle overlap intervals in cell space 438 rectangle_overlap_intervals = ( 439 ShapeMoverUtils.moving_rectangle_overlap_intervals( 440 x_start=min([coord[0] for coord in absolute_shape]), 441 x_end=max([coord[0] for coord in absolute_shape]), 442 y_start=min([coord[1] for coord in absolute_shape]), 443 y_end=max([coord[1] for coord in absolute_shape]), 444 x_shift=x_shift * cell_density, 445 y_shift=y_shift * cell_density, 446 t_start=t_start, 447 t_end=t_end, 448 ) 449 ) 450 # If the shape is only moving vertically or horizontally, we can just return the rectangle overlap intervals 451 if x_shift == 0 or y_shift == 0: 452 return rectangle_overlap_intervals 453 # If the shape is moving diagonally, we should remove intervals that are never passed through by the shape 454 return ShapeMoverUtils.remove_untouched_intervals( 455 intervals=rectangle_overlap_intervals, 456 slope=y_shift / x_shift, 457 absolute_shape=absolute_shape, 458 )
147 @staticmethod 148 def moving_segment_overlap_intervals( 149 seg_start: int | float, 150 seg_end: int | float, 151 t_start: int | float, 152 t_end: int | float, 153 shift: int | float, 154 ): 155 """ 156 Calculates the time intervals during which a moving 1D line segment overlaps with each unit-length 157 integer-aligned range along the x-axis. 158 159 Args: 160 161 - seg_start (int|float): Initial position of the left end of the line segment. 162 - seg_end (int|float): Initial position of the right end of the line segment. 163 - t_start (int|float): Start time of the motion. 164 - t_end (int|float): End time of the motion. 165 - shift (int|float): Total distance the line segment moves along the x-axis during [t_start, t_end]. 166 167 Returns: 168 169 - dict[int, tuplie(int|float,int|float)]: A dictionary mapping each integer `i` to the time interval [t_in, t_out] 170 during which any part of the line overlaps the range [i, i+1). 171 Only includes ranges with non-zero overlap duration. 172 """ 173 duration = t_end - t_start 174 velocity = shift / duration if duration != 0 else 0 175 176 result = {} 177 178 final_start = seg_start + shift 179 final_end = seg_end + shift 180 global_min = min(seg_start, final_start) 181 global_max = max(seg_end, final_end) 182 183 for i in range(int(global_min) - 1, int(global_max) + 2): 184 if velocity == 0: 185 if seg_end > i and seg_start < i + 1 and t_start < t_end: 186 result[i] = (t_start, t_end) 187 continue 188 # Solve for times when the line enters and exits overlap with [i, i+1) 189 t1 = (i - seg_end) / velocity + t_start 190 t2 = (i + 1 - seg_start) / velocity + t_start 191 entry_time = max(min(t1, t2), t_start) 192 exit_time = min(max(t1, t2), t_end) 193 194 if exit_time > entry_time: 195 result[i] = (entry_time, exit_time) 196 197 return result
Calculates the time intervals during which a moving 1D line segment overlaps with each unit-length integer-aligned range along the x-axis.
Args:
- seg_start (int|float): Initial position of the left end of the line segment.
- seg_end (int|float): Initial position of the right end of the line segment.
- t_start (int|float): Start time of the motion.
- t_end (int|float): End time of the motion.
- shift (int|float): Total distance the line segment moves along the x-axis during [t_start, t_end].
Returns:
- dict[int, tuplie(int|float,int|float)]: A dictionary mapping each integer
ito the time interval [t_in, t_out] during which any part of the line overlaps the range [i, i+1). Only includes ranges with non-zero overlap duration.
199 @staticmethod 200 def moving_rectangle_overlap_intervals( 201 x_start: float | int, 202 x_end: float | int, 203 y_start: float | int, 204 y_end: float | int, 205 x_shift: float | int, 206 y_shift: float | int, 207 t_start: float | int, 208 t_end: float | int, 209 ): 210 """ 211 Calculates the time intervals during which a moving rectangle overlaps with each unit-length 212 integer-aligned range along the x and y axes. 213 214 Args: 215 216 - x_start (float|int): Initial position of the left end of the rectangle along the x-axis. 217 - x_end (float|int): Initial position of the right end of the rectangle along the x-axis. 218 - y_start (float|int): Initial position of the bottom end of the rectangle along the y-axis. 219 - y_end (float|int): Initial position of the top end of the rectangle along the y-axis. 220 - x_shift (float|int): Total distance the rectangle moves along the x-axis during [t_start, t_end]. 221 - y_shift (float|int): Total distance the rectangle moves along the y-axis during [t_start, t_end]. 222 - t_start (float|int): Start time of the motion. 223 - t_end (float|int): End time of the motion. 224 225 Returns: 226 227 - dict[tuple(int,int),tuple(int|float,int|float)]: A dictionary mapping each integer (i,j) to the time interval [t_in, t_out] 228 during which any part of the rectangle overlaps the range [i, i+1) x [j, j+1). 229 Only includes ranges with non-zero overlap duration. 230 231 """ 232 x_intervals = ShapeMoverUtils.moving_segment_overlap_intervals( 233 seg_start=x_start, 234 seg_end=x_end, 235 t_start=t_start, 236 t_end=t_end, 237 shift=x_shift, 238 ) 239 y_intervals = ShapeMoverUtils.moving_segment_overlap_intervals( 240 seg_start=y_start, 241 seg_end=y_end, 242 t_start=t_start, 243 t_end=t_end, 244 shift=y_shift, 245 ) 246 result = {} 247 for x_key, x_interval in x_intervals.items(): 248 for y_key, y_interval in y_intervals.items(): 249 # Only add intervals with time overlap 250 if ( 251 x_interval[1] > y_interval[0] 252 and y_interval[1] > x_interval[0] 253 ): 254 result[(x_key, y_key)] = ( 255 max(x_interval[0], y_interval[0]), 256 min(x_interval[1], y_interval[1]), 257 ) 258 259 return result
Calculates the time intervals during which a moving rectangle overlaps with each unit-length integer-aligned range along the x and y axes.
Args:
- x_start (float|int): Initial position of the left end of the rectangle along the x-axis.
- x_end (float|int): Initial position of the right end of the rectangle along the x-axis.
- y_start (float|int): Initial position of the bottom end of the rectangle along the y-axis.
- y_end (float|int): Initial position of the top end of the rectangle along the y-axis.
- x_shift (float|int): Total distance the rectangle moves along the x-axis during [t_start, t_end].
- y_shift (float|int): Total distance the rectangle moves along the y-axis during [t_start, t_end].
- t_start (float|int): Start time of the motion.
- t_end (float|int): End time of the motion.
Returns:
- dict[tuple(int,int),tuple(int|float,int|float)]: A dictionary mapping each integer (i,j) to the time interval [t_in, t_out] during which any part of the rectangle overlaps the range [i, i+1) x [j, j+1). Only includes ranges with non-zero overlap duration.
269 @staticmethod 270 def find_extreme_orthogonal_vertices( 271 points: list[tuple[int | float, int | float]], slope: float | int 272 ): 273 """ 274 Finds the points in a list that are the furthest apart in the direction 275 orthogonal to the given slope. This is useful for finding the extreme 276 points in a set of 2D coordinates that are orthogonal to a given line. 277 278 Args: 279 280 - points (list of tuples): A list of (x, y) coordinates. 281 - slope (float): The slope of the line. 282 283 Returns: 284 285 - tuple: The points with the minimum and maximum projections. 286 """ 287 # Compute orthogonal slope 288 orthogonal = float("inf") if slope == 0 else -1 / slope 289 # Define direction projections (a normalized direction vector, but without the linear algebra) 290 # Handle vertical slope 291 if orthogonal == float("inf"): 292 x_proj = 0.0 293 y_proj = 1.0 294 else: 295 # Note: Technically normalized length is (1**2 + orthogonal**2)**.5, but we avoid the extra square for performance 296 length = (1 + orthogonal**2) ** 0.5 297 x_proj = 1.0 / length 298 y_proj = orthogonal / length 299 # Compute projections 300 projections = [x * x_proj + y * y_proj for x, y in points] 301 # Return the min and max projections 302 return ( 303 points[ShapeMoverUtils.argmin(projections)], 304 points[ShapeMoverUtils.argmax(projections)], 305 )
Finds the points in a list that are the furthest apart in the direction orthogonal to the given slope. This is useful for finding the extreme points in a set of 2D coordinates that are orthogonal to a given line.
Args:
- points (list of tuples): A list of (x, y) coordinates.
- slope (float): The slope of the line.
Returns:
- tuple: The points with the minimum and maximum projections.
307 @staticmethod 308 def find_extreme_orthogonal_vertices_simplified( 309 points: list[tuple[int | float, int | float]], slope: float | int 310 ): 311 """ 312 A simplified version of the function `find_extreme_orthogonal_vertices` 313 that assumes the slope is never 0. 314 315 Finds the points in a list that are the furthest apart in the direction 316 orthogonal to the given slope. This is useful for finding the extreme 317 points in a set of 2D coordinates that are orthogonal to a given line. 318 319 Args: 320 321 - points (list of tuples): A list of (x, y) coordinates. 322 - slope (float): The slope of the line. 323 - Note: This should never be 0 or infinite for this function. 324 325 Returns: 326 327 - tuple: The points with the minimum and maximum projections. 328 """ 329 # Compute orthogonal slope 330 orthogonal = -1 / slope 331 # Define direction projections (a normalized direction vector, but without the linear algebra) 332 # Note: Technically normalized length is (1**2 + orthogonal**2)**.5, but we avoid the extra square for performance 333 length = (1 + orthogonal**2) ** 0.5 334 projections = [ 335 x * 1.0 / length + y * orthogonal / length for x, y in points 336 ] 337 # Return the min and max verticies 338 vertex_1 = points[ShapeMoverUtils.argmin(projections)] 339 vertex_2 = points[ShapeMoverUtils.argmax(projections)] 340 if slope < 0: 341 return vertex_1, vertex_2 342 else: 343 return vertex_2, vertex_1
A simplified version of the function find_extreme_orthogonal_vertices
that assumes the slope is never 0.
Finds the points in a list that are the furthest apart in the direction orthogonal to the given slope. This is useful for finding the extreme points in a set of 2D coordinates that are orthogonal to a given line.
Args:
- points (list of tuples): A list of (x, y) coordinates.
- slope (float): The slope of the line.
- Note: This should never be 0 or infinite for this function.
Returns:
- tuple: The points with the minimum and maximum projections.
345 @staticmethod 346 def remove_untouched_intervals( 347 intervals: dict[tuple[int, int], tuple[int | float, int | float]], 348 slope: float | int, 349 absolute_shape: list[tuple[int | float, int | float]], 350 ): 351 """ 352 Removes unnecessary intervals from the dictionary of intervals. 353 354 Args: 355 356 - intervals (dict[tuple(int,int),tuple(int|float,int|float)]): A dictionary mapping each integer (i,j) to the time interval [t_in, t_out] 357 during which any part of the shape overlaps the range [i, i+1) x [j, j+1). 358 - slope (float|int): The slope of the line. 359 - absolute_shape (list(tuple[int|float, int|float])): A list of coordinates representing the shape's vertices relative to its center. 360 361 Returns: 362 363 - dict[tuple(int,int),tuple(int|float,int|float)]: A dictionary with unnecessary intervals removed. 364 """ 365 min_vertex, max_vertex = ( 366 ShapeMoverUtils.find_extreme_orthogonal_vertices_simplified( 367 points=absolute_shape, slope=slope 368 ) 369 ) 370 shape_min_intercept = min_vertex[1] - slope * min_vertex[0] 371 shape_max_intercept = max_vertex[1] - slope * max_vertex[0] 372 373 # Given the slope, determine which cell vertex to use for the max and min intercepts 374 # This is to ensure that the intervals are removed correctly 375 ltx_increment = 1 if slope < 0 else 0 376 gtx_increment = 0 if slope < 0 else 1 377 378 remove_keys = [] 379 for x_cell, y_cell in intervals.keys(): 380 cell_min_intercept = y_cell - (slope * (x_cell + gtx_increment)) 381 cell_max_intercept = (y_cell + 1) - ( 382 slope * (x_cell + ltx_increment) 383 ) 384 # If the intercept ranges do not overlap, remove the interval 385 if not ( 386 cell_min_intercept < shape_max_intercept 387 and shape_min_intercept < cell_max_intercept 388 ): 389 remove_keys.append((x_cell, y_cell)) 390 for key in remove_keys: 391 del intervals[key] 392 return intervals
Removes unnecessary intervals from the dictionary of intervals.
Args:
- intervals (dict[tuple(int,int),tuple(int|float,int|float)]): A dictionary mapping each integer (i,j) to the time interval [t_in, t_out] during which any part of the shape overlaps the range [i, i+1) x [j, j+1).
- slope (float|int): The slope of the line.
- absolute_shape (list(tuple[int|float, int|float])): A list of coordinates representing the shape's vertices relative to its center.
Returns:
- dict[tuple(int,int),tuple(int|float,int|float)]: A dictionary with unnecessary intervals removed.
394 @staticmethod 395 def moving_shape_overlap_intervals( 396 x_coord: float | int, 397 y_coord: float | int, 398 x_shift: float | int, 399 y_shift: float | int, 400 t_start: float | int, 401 t_end: float | int, 402 shape: list[list[float | int]], 403 cell_density: int = 1, 404 ): 405 """ 406 Calculates the time intervals during which a moving shape overlaps with each unit-length 407 integer-aligned range along the x and y axes. 408 409 Note: This converts each shape into a full bounding box rectangle and then uses the rectangle overlap function to calculate the intervals. 410 411 Args: 412 413 - x_coord (float|int): Initial x-coordinate of the shape's center. 414 - y_coord (float|int): Initial y-coordinate of the shape's center. 415 - x_shift (float|int): Total distance the shape moves along the x-axis during [t_start, t_end]. 416 - y_shift (float|int): Total distance the shape moves along the y-axis during [t_start, t_end]. 417 - t_start (float|int): Start time of the motion. 418 - t_end (float|int): End time of the motion. 419 - shape (list[list[float|int]]): List of coordinates representing the shape's vertices relative to its center. 420 - cell_density (int): The number of cells per unit of length. 421 422 423 Returns: 424 425 - dict[tuple(int,int),tuple(int|float,int|float)]: A dictionary mapping each integer (i,j) to the time interval [t_in, t_out] 426 during which any part of the shape overlaps the range [i, i+1) x [j, j+1). 427 Only includes ranges with non-zero overlap duration. 428 """ 429 # Get the overlap intervals for a rectangle that bounds the shape in cell space 430 absolute_shape = [ 431 [ 432 (x_coord + coord[0]) * cell_density, 433 (y_coord + coord[1]) * cell_density, 434 ] 435 for coord in shape 436 ] 437 # Calculate the rectangle overlap intervals in cell space 438 rectangle_overlap_intervals = ( 439 ShapeMoverUtils.moving_rectangle_overlap_intervals( 440 x_start=min([coord[0] for coord in absolute_shape]), 441 x_end=max([coord[0] for coord in absolute_shape]), 442 y_start=min([coord[1] for coord in absolute_shape]), 443 y_end=max([coord[1] for coord in absolute_shape]), 444 x_shift=x_shift * cell_density, 445 y_shift=y_shift * cell_density, 446 t_start=t_start, 447 t_end=t_end, 448 ) 449 ) 450 # If the shape is only moving vertically or horizontally, we can just return the rectangle overlap intervals 451 if x_shift == 0 or y_shift == 0: 452 return rectangle_overlap_intervals 453 # If the shape is moving diagonally, we should remove intervals that are never passed through by the shape 454 return ShapeMoverUtils.remove_untouched_intervals( 455 intervals=rectangle_overlap_intervals, 456 slope=y_shift / x_shift, 457 absolute_shape=absolute_shape, 458 )
Calculates the time intervals during which a moving shape overlaps with each unit-length integer-aligned range along the x and y axes.
Note: This converts each shape into a full bounding box rectangle and then uses the rectangle overlap function to calculate the intervals.
Args:
- x_coord (float|int): Initial x-coordinate of the shape's center.
- y_coord (float|int): Initial y-coordinate of the shape's center.
- x_shift (float|int): Total distance the shape moves along the x-axis during [t_start, t_end].
- y_shift (float|int): Total distance the shape moves along the y-axis during [t_start, t_end].
- t_start (float|int): Start time of the motion.
- t_end (float|int): End time of the motion.
- shape (list[list[float|int]]): List of coordinates representing the shape's vertices relative to its center.
- cell_density (int): The number of cells per unit of length.
Returns:
- dict[tuple(int,int),tuple(int|float,int|float)]: A dictionary mapping each integer (i,j) to the time interval [t_in, t_out] during which any part of the shape overlaps the range [i, i+1) x [j, j+1). Only includes ranges with non-zero overlap duration.