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        )
class IDGenerator:
 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.

unique_id = <IDGenerator object>
@type_enforced.Enforcer(enabled=True)
class Shape:
 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)
@staticmethod
def circle( radius: int | float, num_points: int = 6, round_to: int = 2) -> list[list]:
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.
@staticmethod
def rectangle(x_len: float | int, y_len: float | int, round_to: int = 2) -> list[list]:
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.
@staticmethod
def rotate( radians: float | int, shape: list[list[float | int]], round_to: int = 2) -> list[list[float | int]]:
 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].
@staticmethod
def get_rotated_shape( shape: list[list[float | int]], x_shift: float | int, y_shift: float | int):
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].
class ShapeMoverUtils:
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        )
@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):
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 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):
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.
@staticmethod
def argmin(lst):
261    @staticmethod
262    def argmin(lst):
263        return min(enumerate(lst), key=lambda x: x[1])[0]
@staticmethod
def argmax(lst):
265    @staticmethod
266    def argmax(lst):
267        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):
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.
@staticmethod
def find_extreme_orthogonal_vertices_simplified(points: list[tuple[int | float, int | float]], slope: float | int):
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.
@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]]):
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.
@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):
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.