fizgrid.utils

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

Generates a unique identifier.

@type_enforced.Enforcer(enabled=True)
class Shape:
 12@type_enforced.Enforcer(enabled=True)
 13class Shape:
 14    @staticmethod
 15    def circle(
 16        radius: int | float, num_points: int = 6, round_to: int = 2
 17    ) -> list[list]:
 18        """
 19        Returns a list of addative coordinates that form a circle around a given point.
 20
 21        Args:
 22
 23        - radius (int|float): Radius of the circle.
 24        - num_points (int): Number of points to generate around the circle.
 25            - Default: 6
 26            - This is used to determine the number of points to generate around the circle.
 27        - round_to (int): Number of decimal places to round to.
 28            - Default: 2
 29            - This is used to round the coordinates to a specific number of decimal places.
 30
 31        Returns:
 32
 33        - list[list]: A list of coordinates representing the circle.
 34            - Each coordinate is a list of two values [x, y].
 35            - The coordinates are relative to the center of the circle.
 36        """
 37        return [
 38            [
 39                round(
 40                    radius * math.cos(2 * math.pi / num_points * i), round_to
 41                ),
 42                round(
 43                    radius * math.sin(2 * math.pi / num_points * i), round_to
 44                ),
 45            ]
 46            for i in range(num_points)
 47        ]
 48
 49    @staticmethod
 50    def rectangle(
 51        x_len: float | int, y_len: float | int, round_to: int = 2
 52    ) -> list[list]:
 53        """
 54        Returns a list of addative coordinates that form a rectangle around a given point.
 55
 56        Args:
 57
 58        - x_len (float|int): Length of the rectangle along the x-axis.
 59        - y_len (float|int): Length of the rectangle along the y-axis.
 60        - round_to (int): Number of decimal places to round to.
 61            - Default: 2
 62            - This is used to round the coordinates to a specific number of decimal places.
 63
 64        Returns:
 65
 66        - list[list]: A list of coordinates representing the rectangle.
 67            - Each coordinate is a list of two values [x, y].
 68            - The coordinates are relative to the center of the rectangle.
 69        """
 70        return [
 71            [round(x_len / 2, round_to), round(y_len / 2, round_to)],
 72            [round(-x_len / 2, round_to), round(y_len / 2, round_to)],
 73            [round(-x_len / 2, round_to), round(-y_len / 2, round_to)],
 74            [round(x_len / 2, round_to), round(-y_len / 2, round_to)],
 75        ]
 76
 77    @staticmethod
 78    def rotate(
 79        radians: float | int, shape: list[list[float | int]], round_to: int = 2
 80    ) -> list[list[float | int]]:
 81        """
 82        Rotates a shape by a given angle in radians around its origin (0, 0).
 83        Args:
 84        - radians (float|int): The angle in radians to rotate the shape.
 85        - shape (list[list[float|int]]): The shape to rotate.
 86            - This is a list of coordinates representing the shape.
 87            - Each coordinate is a list of two values [x, y].
 88        Returns:
 89        - list[list[float|int]]: The rotated shape.
 90            - This is a list of coordinates representing the rotated shape.
 91            - Each coordinate is a list of two values [x, y].
 92        """
 93        return [
 94            [
 95                round(
 96                    coord[0] * math.cos(radians) - coord[1] * math.sin(radians),
 97                    round_to,
 98                ),
 99                round(
100                    coord[0] * math.sin(radians) + coord[1] * math.cos(radians),
101                    round_to,
102                ),
103            ]
104            for coord in shape
105        ]
106
107    @staticmethod
108    def get_rotated_shape(
109        shape: list[list[float | int]],
110        x_shift: float | int,
111        y_shift: float | int,
112    ):
113        """
114        Rotates the shape to align with the direction of motion.
115
116        Args:
117
118        - shape (list[list[float|int]]): List of coordinates representing the shape's vertices relative to its center.
119        - x_shift (float|int): Total distance the shape moves along the x-axis during [t_start, t_end].
120        - y_shift (float|int): Total distance the shape moves along the y-axis during [t_start, t_end].
121
122        Returns:
123
124        - list[list[float|int]]: The rotated shape.
125            - This is a list of coordinates representing the rotated shape.
126            - Each coordinate is a list of two values [x, y].
127        """
128        if x_shift == 0 and y_shift == 0:
129            return shape
130        radians = math.atan2(y_shift, x_shift)
131        return Shape.rotate(radians=radians, shape=shape)
@staticmethod
def circle( radius: int | float, num_points: int = 6, round_to: int = 2) -> list[list]:
14    @staticmethod
15    def circle(
16        radius: int | float, num_points: int = 6, round_to: int = 2
17    ) -> list[list]:
18        """
19        Returns a list of addative coordinates that form a circle around a given point.
20
21        Args:
22
23        - radius (int|float): Radius of the circle.
24        - num_points (int): Number of points to generate around the circle.
25            - Default: 6
26            - This is used to determine the number of points to generate around the circle.
27        - round_to (int): Number of decimal places to round to.
28            - Default: 2
29            - This is used to round the coordinates to a specific number of decimal places.
30
31        Returns:
32
33        - list[list]: A list of coordinates representing the circle.
34            - Each coordinate is a list of two values [x, y].
35            - The coordinates are relative to the center of the circle.
36        """
37        return [
38            [
39                round(
40                    radius * math.cos(2 * math.pi / num_points * i), round_to
41                ),
42                round(
43                    radius * math.sin(2 * math.pi / num_points * i), round_to
44                ),
45            ]
46            for i in range(num_points)
47        ]

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.
@staticmethod
def rectangle(x_len: float | int, y_len: float | int, round_to: int = 2) -> list[list]:
49    @staticmethod
50    def rectangle(
51        x_len: float | int, y_len: float | int, round_to: int = 2
52    ) -> list[list]:
53        """
54        Returns a list of addative coordinates that form a rectangle around a given point.
55
56        Args:
57
58        - x_len (float|int): Length of the rectangle along the x-axis.
59        - y_len (float|int): Length of the rectangle along the y-axis.
60        - round_to (int): Number of decimal places to round to.
61            - Default: 2
62            - This is used to round the coordinates to a specific number of decimal places.
63
64        Returns:
65
66        - list[list]: A list of coordinates representing the rectangle.
67            - Each coordinate is a list of two values [x, y].
68            - The coordinates are relative to the center of the rectangle.
69        """
70        return [
71            [round(x_len / 2, round_to), round(y_len / 2, round_to)],
72            [round(-x_len / 2, round_to), round(y_len / 2, round_to)],
73            [round(-x_len / 2, round_to), round(-y_len / 2, round_to)],
74            [round(x_len / 2, round_to), round(-y_len / 2, round_to)],
75        ]

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.
@staticmethod
def rotate( radians: float | int, shape: list[list[float | int]], round_to: int = 2) -> list[list[float | int]]:
 77    @staticmethod
 78    def rotate(
 79        radians: float | int, shape: list[list[float | int]], round_to: int = 2
 80    ) -> list[list[float | int]]:
 81        """
 82        Rotates a shape by a given angle in radians around its origin (0, 0).
 83        Args:
 84        - radians (float|int): The angle in radians to rotate the shape.
 85        - shape (list[list[float|int]]): The shape to rotate.
 86            - This is a list of coordinates representing the shape.
 87            - Each coordinate is a list of two values [x, y].
 88        Returns:
 89        - list[list[float|int]]: The rotated shape.
 90            - This is a list of coordinates representing the rotated shape.
 91            - Each coordinate is a list of two values [x, y].
 92        """
 93        return [
 94            [
 95                round(
 96                    coord[0] * math.cos(radians) - coord[1] * math.sin(radians),
 97                    round_to,
 98                ),
 99                round(
100                    coord[0] * math.sin(radians) + coord[1] * math.cos(radians),
101                    round_to,
102                ),
103            ]
104            for coord in shape
105        ]

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].
@staticmethod
def get_rotated_shape( shape: list[list[float | int]], x_shift: float | int, y_shift: float | int):
107    @staticmethod
108    def get_rotated_shape(
109        shape: list[list[float | int]],
110        x_shift: float | int,
111        y_shift: float | int,
112    ):
113        """
114        Rotates the shape to align with the direction of motion.
115
116        Args:
117
118        - shape (list[list[float|int]]): List of coordinates representing the shape's vertices relative to its center.
119        - x_shift (float|int): Total distance the shape moves along the x-axis during [t_start, t_end].
120        - y_shift (float|int): Total distance the shape moves along the y-axis during [t_start, t_end].
121
122        Returns:
123
124        - list[list[float|int]]: The rotated shape.
125            - This is a list of coordinates representing the rotated shape.
126            - Each coordinate is a list of two values [x, y].
127        """
128        if x_shift == 0 and y_shift == 0:
129            return shape
130        radians = math.atan2(y_shift, x_shift)
131        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].
class ShapeMoverUtils:
134class ShapeMoverUtils:
135    @staticmethod
136    def moving_segment_overlap_intervals(
137        seg_start: int | float,
138        seg_end: int | float,
139        t_start: int | float,
140        t_end: int | float,
141        shift: int | float,
142    ):
143        """
144        Calculates the time intervals during which a moving 1D line segment overlaps with each unit-length
145        integer-aligned range along the x-axis.
146
147        Args:
148
149        - seg_start (int|float): Initial position of the left end of the line segment.
150        - seg_end (int|float): Initial position of the right end of the line segment.
151        - t_start (int|float): Start time of the motion.
152        - t_end (int|float): End time of the motion.
153        - shift (int|float): Total distance the line segment moves along the x-axis during [t_start, t_end].
154
155        Returns:
156
157        - dict[int, tuplie(int|float,int|float)]: A dictionary mapping each integer `i` to the time interval [t_in, t_out]
158                                during which any part of the line overlaps the range [i, i+1).
159                                Only includes ranges with non-zero overlap duration.
160        """
161        duration = t_end - t_start
162        velocity = shift / duration if duration != 0 else 0
163
164        result = {}
165
166        final_start = seg_start + shift
167        final_end = seg_end + shift
168        global_min = min(seg_start, final_start)
169        global_max = max(seg_end, final_end)
170
171        for i in range(int(global_min) - 1, int(global_max) + 2):
172            if velocity == 0:
173                if seg_end > i and seg_start < i + 1 and t_start < t_end:
174                    result[i] = (t_start, t_end)
175                continue
176            # Solve for times when the line enters and exits overlap with [i, i+1)
177            t1 = (i - seg_end) / velocity + t_start
178            t2 = (i + 1 - seg_start) / velocity + t_start
179            entry_time = max(min(t1, t2), t_start)
180            exit_time = min(max(t1, t2), t_end)
181
182            if exit_time > entry_time:
183                result[i] = (entry_time, exit_time)
184
185        return result
186
187    @staticmethod
188    def moving_rectangle_overlap_intervals(
189        x_start: float | int,
190        x_end: float | int,
191        y_start: float | int,
192        y_end: float | int,
193        x_shift: float | int,
194        y_shift: float | int,
195        t_start: float | int,
196        t_end: float | int,
197    ):
198        """
199        Calculates the time intervals during which a moving rectangle overlaps with each unit-length
200        integer-aligned range along the x and y axes.
201
202        Args:
203
204        - x_start (float|int): Initial position of the left end of the rectangle along the x-axis.
205        - x_end (float|int): Initial position of the right end of the rectangle along the x-axis.
206        - y_start (float|int): Initial position of the bottom end of the rectangle along the y-axis.
207        - y_end (float|int): Initial position of the top end of the rectangle along the y-axis.
208        - x_shift (float|int): Total distance the rectangle moves along the x-axis during [t_start, t_end].
209        - y_shift (float|int): Total distance the rectangle moves along the y-axis during [t_start, t_end].
210        - t_start (float|int): Start time of the motion.
211        - t_end (float|int): End time of the motion.
212
213        Returns:
214
215        - dict[tuple(int,int),tuple(int|float,int|float)]: A dictionary mapping each integer (i,j) to the time interval [t_in, t_out]
216            during which any part of the rectangle overlaps the range [i, i+1) x [j, j+1).
217            Only includes ranges with non-zero overlap duration.
218
219        """
220        x_intervals = ShapeMoverUtils.moving_segment_overlap_intervals(
221            seg_start=x_start,
222            seg_end=x_end,
223            t_start=t_start,
224            t_end=t_end,
225            shift=x_shift,
226        )
227        y_intervals = ShapeMoverUtils.moving_segment_overlap_intervals(
228            seg_start=y_start,
229            seg_end=y_end,
230            t_start=t_start,
231            t_end=t_end,
232            shift=y_shift,
233        )
234        result = {}
235        for x_key, x_interval in x_intervals.items():
236            for y_key, y_interval in y_intervals.items():
237                # Only add intervals with time overlap
238                if (
239                    x_interval[1] > y_interval[0]
240                    and y_interval[1] > x_interval[0]
241                ):
242                    result[(x_key, y_key)] = (
243                        max(x_interval[0], y_interval[0]),
244                        min(x_interval[1], y_interval[1]),
245                    )
246
247        return result
248
249    @staticmethod
250    def argmin(lst):
251        return min(enumerate(lst), key=lambda x: x[1])[0]
252
253    @staticmethod
254    def argmax(lst):
255        return max(enumerate(lst), key=lambda x: x[1])[0]
256
257    @staticmethod
258    def find_extreme_orthogonal_vertices(
259        points: list[tuple[int | float, int | float]], slope: float | int
260    ):
261        """
262        Finds the points in a list that are the furthest apart in the direction
263        orthogonal to the given slope. This is useful for finding the extreme
264        points in a set of 2D coordinates that are orthogonal to a given line.
265
266        Args:
267
268        - points (list of tuples): A list of (x, y) coordinates.
269        - slope (float): The slope of the line.
270
271        Returns:
272
273        - tuple: The points with the minimum and maximum projections.
274        """
275        # Compute orthogonal slope
276        orthogonal = float("inf") if slope == 0 else -1 / slope
277        # Define direction projections (a normalized direction vector, but without the linear algebra)
278        # Handle vertical slope
279        if orthogonal == float("inf"):
280            x_proj = 0.0
281            y_proj = 1.0
282        else:
283            # Note: Technically normalized length is (1**2 + orthogonal**2)**.5, but we avoid the extra square for performance
284            length = (1 + orthogonal**2) ** 0.5
285            x_proj = 1.0 / length
286            y_proj = orthogonal / length
287        # Compute projections
288        projections = [x * x_proj + y * y_proj for x, y in points]
289        # Return the min and max projections
290        return (
291            points[ShapeMoverUtils.argmin(projections)],
292            points[ShapeMoverUtils.argmax(projections)],
293        )
294
295    @staticmethod
296    def find_extreme_orthogonal_vertices_simplified(
297        points: list[tuple[int | float, int | float]], slope: float | int
298    ):
299        """
300        A simplified version of the function `find_extreme_orthogonal_vertices`
301        that assumes the slope is never 0.
302
303        Finds the points in a list that are the furthest apart in the direction
304        orthogonal to the given slope. This is useful for finding the extreme
305        points in a set of 2D coordinates that are orthogonal to a given line.
306
307        Args:
308
309        - points (list of tuples): A list of (x, y) coordinates.
310        - slope (float): The slope of the line.
311            - Note: This should never be 0 or infinite for this function.
312
313        Returns:
314
315        - tuple: The points with the minimum and maximum projections.
316        """
317        # Compute orthogonal slope
318        orthogonal = -1 / slope
319        # Define direction projections (a normalized direction vector, but without the linear algebra)
320        # Note: Technically normalized length is (1**2 + orthogonal**2)**.5, but we avoid the extra square for performance
321        length = (1 + orthogonal**2) ** 0.5
322        projections = [
323            x * 1.0 / length + y * orthogonal / length for x, y in points
324        ]
325        # Return the min and max verticies
326        vertex_1 = points[ShapeMoverUtils.argmin(projections)]
327        vertex_2 = points[ShapeMoverUtils.argmax(projections)]
328        if slope < 0:
329            return vertex_1, vertex_2
330        else:
331            return vertex_2, vertex_1
332
333    @staticmethod
334    def remove_untouched_intervals(
335        intervals: dict[tuple[int, int], tuple[int | float, int | float]],
336        slope: float | int,
337        absolute_shape: list[tuple[int | float, int | float]],
338    ):
339        """
340        Removes unnecessary intervals from the dictionary of intervals.
341
342        Args:
343
344        - 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]
345            during which any part of the shape overlaps the range [i, i+1) x [j, j+1).
346        - slope (float|int): The slope of the line.
347        - absolute_shape (list(tuple[int|float, int|float])): A list of coordinates representing the shape's vertices relative to its center.
348
349        Returns:
350
351        - dict[tuple(int,int),tuple(int|float,int|float)]: A dictionary with unnecessary intervals removed.
352        """
353        min_vertex, max_vertex = (
354            ShapeMoverUtils.find_extreme_orthogonal_vertices_simplified(
355                points=absolute_shape, slope=slope
356            )
357        )
358        shape_min_intercept = min_vertex[1] - slope * min_vertex[0]
359        shape_max_intercept = max_vertex[1] - slope * max_vertex[0]
360
361        # Given the slope, determine which cell vertex to use for the max and min intercepts
362        # This is to ensure that the intervals are removed correctly
363        ltx_increment = 1 if slope < 0 else 0
364        gtx_increment = 0 if slope < 0 else 1
365
366        remove_keys = []
367        for x_cell, y_cell in intervals.keys():
368            cell_min_intercept = y_cell - (slope * (x_cell + gtx_increment))
369            cell_max_intercept = (y_cell + 1) - (
370                slope * (x_cell + ltx_increment)
371            )
372            # If the intercept ranges do not overlap, remove the interval
373            if not (
374                cell_min_intercept < shape_max_intercept
375                and shape_min_intercept < cell_max_intercept
376            ):
377                remove_keys.append((x_cell, y_cell))
378        for key in remove_keys:
379            del intervals[key]
380        return intervals
381
382    @staticmethod
383    def moving_shape_overlap_intervals(
384        x_coord: float | int,
385        y_coord: float | int,
386        x_shift: float | int,
387        y_shift: float | int,
388        t_start: float | int,
389        t_end: float | int,
390        shape: list[list[float | int]],
391        cell_density: int = 1,
392    ):
393        """
394        Calculates the time intervals during which a moving shape overlaps with each unit-length
395        integer-aligned range along the x and y axes.
396
397        Note: This converts each shape into a full bounding box rectangle and then uses the rectangle overlap function to calculate the intervals.
398
399        Args:
400
401        - x_coord (float|int): Initial x-coordinate of the shape's center.
402        - y_coord (float|int): Initial y-coordinate of the shape's center.
403        - x_shift (float|int): Total distance the shape moves along the x-axis during [t_start, t_end].
404        - y_shift (float|int): Total distance the shape moves along the y-axis during [t_start, t_end].
405        - t_start (float|int): Start time of the motion.
406        - t_end (float|int): End time of the motion.
407        - shape (list[list[float|int]]): List of coordinates representing the shape's vertices relative to its center.
408        - cell_density (int): The number of cells per unit of length.
409
410
411        Returns:
412
413        - dict[tuple(int,int),tuple(int|float,int|float)]: A dictionary mapping each integer (i,j) to the time interval [t_in, t_out]
414                                during which any part of the shape overlaps the range [i, i+1) x [j, j+1).
415                                Only includes ranges with non-zero overlap duration.
416        """
417        # Get the overlap intervals for a rectangle that bounds the shape in cell space
418        absolute_shape = [
419            [
420                (x_coord + coord[0]) * cell_density,
421                (y_coord + coord[1]) * cell_density,
422            ]
423            for coord in shape
424        ]
425        # Calculate the rectangle overlap intervals in cell space
426        rectangle_overlap_intervals = (
427            ShapeMoverUtils.moving_rectangle_overlap_intervals(
428                x_start=min([coord[0] for coord in absolute_shape]),
429                x_end=max([coord[0] for coord in absolute_shape]),
430                y_start=min([coord[1] for coord in absolute_shape]),
431                y_end=max([coord[1] for coord in absolute_shape]),
432                x_shift=x_shift * cell_density,
433                y_shift=y_shift * cell_density,
434                t_start=t_start,
435                t_end=t_end,
436            )
437        )
438        # If the shape is only moving vertically or horizontally, we can just return the rectangle overlap intervals
439        if x_shift == 0 or y_shift == 0:
440            return rectangle_overlap_intervals
441        # If the shape is moving diagonally, we should remove intervals that are never passed through by the shape
442        return ShapeMoverUtils.remove_untouched_intervals(
443            intervals=rectangle_overlap_intervals,
444            slope=y_shift / x_shift,
445            absolute_shape=absolute_shape,
446        )
@staticmethod
def moving_segment_overlap_intervals( seg_start: int | float, seg_end: int | float, t_start: int | float, t_end: int | float, shift: int | float):
135    @staticmethod
136    def moving_segment_overlap_intervals(
137        seg_start: int | float,
138        seg_end: int | float,
139        t_start: int | float,
140        t_end: int | float,
141        shift: int | float,
142    ):
143        """
144        Calculates the time intervals during which a moving 1D line segment overlaps with each unit-length
145        integer-aligned range along the x-axis.
146
147        Args:
148
149        - seg_start (int|float): Initial position of the left end of the line segment.
150        - seg_end (int|float): Initial position of the right end of the line segment.
151        - t_start (int|float): Start time of the motion.
152        - t_end (int|float): End time of the motion.
153        - shift (int|float): Total distance the line segment moves along the x-axis during [t_start, t_end].
154
155        Returns:
156
157        - dict[int, tuplie(int|float,int|float)]: A dictionary mapping each integer `i` to the time interval [t_in, t_out]
158                                during which any part of the line overlaps the range [i, i+1).
159                                Only includes ranges with non-zero overlap duration.
160        """
161        duration = t_end - t_start
162        velocity = shift / duration if duration != 0 else 0
163
164        result = {}
165
166        final_start = seg_start + shift
167        final_end = seg_end + shift
168        global_min = min(seg_start, final_start)
169        global_max = max(seg_end, final_end)
170
171        for i in range(int(global_min) - 1, int(global_max) + 2):
172            if velocity == 0:
173                if seg_end > i and seg_start < i + 1 and t_start < t_end:
174                    result[i] = (t_start, t_end)
175                continue
176            # Solve for times when the line enters and exits overlap with [i, i+1)
177            t1 = (i - seg_end) / velocity + t_start
178            t2 = (i + 1 - seg_start) / velocity + t_start
179            entry_time = max(min(t1, t2), t_start)
180            exit_time = min(max(t1, t2), t_end)
181
182            if exit_time > entry_time:
183                result[i] = (entry_time, exit_time)
184
185        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 i to 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.
@staticmethod
def moving_rectangle_overlap_intervals( x_start: float | int, x_end: float | int, y_start: float | int, y_end: float | int, x_shift: float | int, y_shift: float | int, t_start: float | int, t_end: float | int):
187    @staticmethod
188    def moving_rectangle_overlap_intervals(
189        x_start: float | int,
190        x_end: float | int,
191        y_start: float | int,
192        y_end: float | int,
193        x_shift: float | int,
194        y_shift: float | int,
195        t_start: float | int,
196        t_end: float | int,
197    ):
198        """
199        Calculates the time intervals during which a moving rectangle overlaps with each unit-length
200        integer-aligned range along the x and y axes.
201
202        Args:
203
204        - x_start (float|int): Initial position of the left end of the rectangle along the x-axis.
205        - x_end (float|int): Initial position of the right end of the rectangle along the x-axis.
206        - y_start (float|int): Initial position of the bottom end of the rectangle along the y-axis.
207        - y_end (float|int): Initial position of the top end of the rectangle along the y-axis.
208        - x_shift (float|int): Total distance the rectangle moves along the x-axis during [t_start, t_end].
209        - y_shift (float|int): Total distance the rectangle moves along the y-axis during [t_start, t_end].
210        - t_start (float|int): Start time of the motion.
211        - t_end (float|int): End time of the motion.
212
213        Returns:
214
215        - dict[tuple(int,int),tuple(int|float,int|float)]: A dictionary mapping each integer (i,j) to the time interval [t_in, t_out]
216            during which any part of the rectangle overlaps the range [i, i+1) x [j, j+1).
217            Only includes ranges with non-zero overlap duration.
218
219        """
220        x_intervals = ShapeMoverUtils.moving_segment_overlap_intervals(
221            seg_start=x_start,
222            seg_end=x_end,
223            t_start=t_start,
224            t_end=t_end,
225            shift=x_shift,
226        )
227        y_intervals = ShapeMoverUtils.moving_segment_overlap_intervals(
228            seg_start=y_start,
229            seg_end=y_end,
230            t_start=t_start,
231            t_end=t_end,
232            shift=y_shift,
233        )
234        result = {}
235        for x_key, x_interval in x_intervals.items():
236            for y_key, y_interval in y_intervals.items():
237                # Only add intervals with time overlap
238                if (
239                    x_interval[1] > y_interval[0]
240                    and y_interval[1] > x_interval[0]
241                ):
242                    result[(x_key, y_key)] = (
243                        max(x_interval[0], y_interval[0]),
244                        min(x_interval[1], y_interval[1]),
245                    )
246
247        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.
@staticmethod
def argmin(lst):
249    @staticmethod
250    def argmin(lst):
251        return min(enumerate(lst), key=lambda x: x[1])[0]
@staticmethod
def argmax(lst):
253    @staticmethod
254    def argmax(lst):
255        return max(enumerate(lst), key=lambda x: x[1])[0]
@staticmethod
def find_extreme_orthogonal_vertices(points: list[tuple[int | float, int | float]], slope: float | int):
257    @staticmethod
258    def find_extreme_orthogonal_vertices(
259        points: list[tuple[int | float, int | float]], slope: float | int
260    ):
261        """
262        Finds the points in a list that are the furthest apart in the direction
263        orthogonal to the given slope. This is useful for finding the extreme
264        points in a set of 2D coordinates that are orthogonal to a given line.
265
266        Args:
267
268        - points (list of tuples): A list of (x, y) coordinates.
269        - slope (float): The slope of the line.
270
271        Returns:
272
273        - tuple: The points with the minimum and maximum projections.
274        """
275        # Compute orthogonal slope
276        orthogonal = float("inf") if slope == 0 else -1 / slope
277        # Define direction projections (a normalized direction vector, but without the linear algebra)
278        # Handle vertical slope
279        if orthogonal == float("inf"):
280            x_proj = 0.0
281            y_proj = 1.0
282        else:
283            # Note: Technically normalized length is (1**2 + orthogonal**2)**.5, but we avoid the extra square for performance
284            length = (1 + orthogonal**2) ** 0.5
285            x_proj = 1.0 / length
286            y_proj = orthogonal / length
287        # Compute projections
288        projections = [x * x_proj + y * y_proj for x, y in points]
289        # Return the min and max projections
290        return (
291            points[ShapeMoverUtils.argmin(projections)],
292            points[ShapeMoverUtils.argmax(projections)],
293        )

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.
@staticmethod
def find_extreme_orthogonal_vertices_simplified(points: list[tuple[int | float, int | float]], slope: float | int):
295    @staticmethod
296    def find_extreme_orthogonal_vertices_simplified(
297        points: list[tuple[int | float, int | float]], slope: float | int
298    ):
299        """
300        A simplified version of the function `find_extreme_orthogonal_vertices`
301        that assumes the slope is never 0.
302
303        Finds the points in a list that are the furthest apart in the direction
304        orthogonal to the given slope. This is useful for finding the extreme
305        points in a set of 2D coordinates that are orthogonal to a given line.
306
307        Args:
308
309        - points (list of tuples): A list of (x, y) coordinates.
310        - slope (float): The slope of the line.
311            - Note: This should never be 0 or infinite for this function.
312
313        Returns:
314
315        - tuple: The points with the minimum and maximum projections.
316        """
317        # Compute orthogonal slope
318        orthogonal = -1 / slope
319        # Define direction projections (a normalized direction vector, but without the linear algebra)
320        # Note: Technically normalized length is (1**2 + orthogonal**2)**.5, but we avoid the extra square for performance
321        length = (1 + orthogonal**2) ** 0.5
322        projections = [
323            x * 1.0 / length + y * orthogonal / length for x, y in points
324        ]
325        # Return the min and max verticies
326        vertex_1 = points[ShapeMoverUtils.argmin(projections)]
327        vertex_2 = points[ShapeMoverUtils.argmax(projections)]
328        if slope < 0:
329            return vertex_1, vertex_2
330        else:
331            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.
@staticmethod
def remove_untouched_intervals( intervals: dict[tuple[int, int], tuple[int | float, int | float]], slope: float | int, absolute_shape: list[tuple[int | float, int | float]]):
333    @staticmethod
334    def remove_untouched_intervals(
335        intervals: dict[tuple[int, int], tuple[int | float, int | float]],
336        slope: float | int,
337        absolute_shape: list[tuple[int | float, int | float]],
338    ):
339        """
340        Removes unnecessary intervals from the dictionary of intervals.
341
342        Args:
343
344        - 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]
345            during which any part of the shape overlaps the range [i, i+1) x [j, j+1).
346        - slope (float|int): The slope of the line.
347        - absolute_shape (list(tuple[int|float, int|float])): A list of coordinates representing the shape's vertices relative to its center.
348
349        Returns:
350
351        - dict[tuple(int,int),tuple(int|float,int|float)]: A dictionary with unnecessary intervals removed.
352        """
353        min_vertex, max_vertex = (
354            ShapeMoverUtils.find_extreme_orthogonal_vertices_simplified(
355                points=absolute_shape, slope=slope
356            )
357        )
358        shape_min_intercept = min_vertex[1] - slope * min_vertex[0]
359        shape_max_intercept = max_vertex[1] - slope * max_vertex[0]
360
361        # Given the slope, determine which cell vertex to use for the max and min intercepts
362        # This is to ensure that the intervals are removed correctly
363        ltx_increment = 1 if slope < 0 else 0
364        gtx_increment = 0 if slope < 0 else 1
365
366        remove_keys = []
367        for x_cell, y_cell in intervals.keys():
368            cell_min_intercept = y_cell - (slope * (x_cell + gtx_increment))
369            cell_max_intercept = (y_cell + 1) - (
370                slope * (x_cell + ltx_increment)
371            )
372            # If the intercept ranges do not overlap, remove the interval
373            if not (
374                cell_min_intercept < shape_max_intercept
375                and shape_min_intercept < cell_max_intercept
376            ):
377                remove_keys.append((x_cell, y_cell))
378        for key in remove_keys:
379            del intervals[key]
380        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.
@staticmethod
def moving_shape_overlap_intervals( x_coord: float | int, y_coord: float | int, x_shift: float | int, y_shift: float | int, t_start: float | int, t_end: float | int, shape: list[list[float | int]], cell_density: int = 1):
382    @staticmethod
383    def moving_shape_overlap_intervals(
384        x_coord: float | int,
385        y_coord: float | int,
386        x_shift: float | int,
387        y_shift: float | int,
388        t_start: float | int,
389        t_end: float | int,
390        shape: list[list[float | int]],
391        cell_density: int = 1,
392    ):
393        """
394        Calculates the time intervals during which a moving shape overlaps with each unit-length
395        integer-aligned range along the x and y axes.
396
397        Note: This converts each shape into a full bounding box rectangle and then uses the rectangle overlap function to calculate the intervals.
398
399        Args:
400
401        - x_coord (float|int): Initial x-coordinate of the shape's center.
402        - y_coord (float|int): Initial y-coordinate of the shape's center.
403        - x_shift (float|int): Total distance the shape moves along the x-axis during [t_start, t_end].
404        - y_shift (float|int): Total distance the shape moves along the y-axis during [t_start, t_end].
405        - t_start (float|int): Start time of the motion.
406        - t_end (float|int): End time of the motion.
407        - shape (list[list[float|int]]): List of coordinates representing the shape's vertices relative to its center.
408        - cell_density (int): The number of cells per unit of length.
409
410
411        Returns:
412
413        - dict[tuple(int,int),tuple(int|float,int|float)]: A dictionary mapping each integer (i,j) to the time interval [t_in, t_out]
414                                during which any part of the shape overlaps the range [i, i+1) x [j, j+1).
415                                Only includes ranges with non-zero overlap duration.
416        """
417        # Get the overlap intervals for a rectangle that bounds the shape in cell space
418        absolute_shape = [
419            [
420                (x_coord + coord[0]) * cell_density,
421                (y_coord + coord[1]) * cell_density,
422            ]
423            for coord in shape
424        ]
425        # Calculate the rectangle overlap intervals in cell space
426        rectangle_overlap_intervals = (
427            ShapeMoverUtils.moving_rectangle_overlap_intervals(
428                x_start=min([coord[0] for coord in absolute_shape]),
429                x_end=max([coord[0] for coord in absolute_shape]),
430                y_start=min([coord[1] for coord in absolute_shape]),
431                y_end=max([coord[1] for coord in absolute_shape]),
432                x_shift=x_shift * cell_density,
433                y_shift=y_shift * cell_density,
434                t_start=t_start,
435                t_end=t_end,
436            )
437        )
438        # If the shape is only moving vertically or horizontally, we can just return the rectangle overlap intervals
439        if x_shift == 0 or y_shift == 0:
440            return rectangle_overlap_intervals
441        # If the shape is moving diagonally, we should remove intervals that are never passed through by the shape
442        return ShapeMoverUtils.remove_untouched_intervals(
443            intervals=rectangle_overlap_intervals,
444            slope=y_shift / x_shift,
445            absolute_shape=absolute_shape,
446        )

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.