fizgrid

Fizgrid

PyPI version License: MIT

Simulate entities that take up space over time in grid based environments.

Overview

This package introduces a new approach to simulating physical movement within a grid. Rather than following the traditional method of discretizing time and iterating over spatial configurations, this simulation inverts the model: space is discretized, and time is continuous.

Events are scheduled into a priority queue, ordered by their occurrence time. The simulation processes events in chronological order, updating only the relevant entities and grid cells affected by each event. This design allows for highly efficient simulation of large numbers of entities over long periods of simulated time.

Entity Movement and Scheduling

Entity movements are planned in advance but only realized when an associated event is triggered. When a route is assigned to an entity, each grid cell along its path is reserved for the time the entity is expected to occupy it. During this reservation process, the system checks for potential conflicts. For example, whether another entity is scheduled to occupy the same cell at the same time.

If a conflict is detected, a collision event is added to the queue for both entities. Only the first potential collision between any pair of entities is added to avoid redundancy. Additionally, an end-of-route event is queued for the entity to signal the completion of its movement.

Event Handling

When an end-of-route event is processed:

  • The entity’s position is updated to where it would be at that time.
  • All pending events related to its previous route are removed from the queue.
  • A placeholder (blank) route is assigned to the entity to occupy its current position until a new route is defined.
    • This may trigger new collisions, which will be resolved in their own time.

When a collision event is processed:

  • The entity is moved to its calculated position at the collision time.
  • All future events tied to its current route are removed.
  • Collision events for other entities that would have involved this entity are also removed.
    • The counterpart entity in the current collision still processes its corresponding event.
  • A blank route is assigned to the entity to mark its occupancy of the current cell until reassigned.
    • This may trigger new collisions will be resolved in their own time.

Setup

Make sure you have Python 3.11.x (or higher) installed on your system. You can download it here.

Installation

pip install fizgrid

Basic Usage

Technical Docs

The technical documentation is available at https://connor-makowski.github.io/fizgrid/index.html.

Example

from fizgrid.grid import Grid
from fizgrid.entities import Entity, StaticEntity, GhostEntity
from fizgrid.utils import Shape

# Create a grid with exterior walls
grid = Grid(
    name="living_room",
    x_size=10,
    y_size=10,
    add_exterior_walls=True,
    cell_density=1,
)

# Add some static entities
sofa = grid.add_entity(
    StaticEntity(
        name="sofa",
        shape=Shape.rectangle(x_len=2, y_len=1),
        x_coord=5,
        y_coord=5.5,
    )
)

# Add a dynamic entity
robot = grid.add_entity(
    Entity(
        name="robot",
        shape=Shape.rectangle(x_len=1, y_len=1),
        x_coord=5,
        y_coord=2,
    )
)

# Add a dynamic entity that can pass through other entities
ghost = grid.add_entity(
    GhostEntity(
        name="ghost",
        shape=Shape.rectangle(x_len=1, y_len=1),
        x_coord=5,
        y_coord=2,
    )
)

# Attempt to move the robot through the sofa
# Starting at time=0, the robot will try to move to (5,8) over 12 seconds
# This will bump into the sofa and stop
robot.add_route(time=0, waypoints=[(5, 8, 12)])


# Attempt to move the ghost through the sofa
# Starting at time=0, the ghost will try to move to (5,8) over 12 seconds
# This will not bump into the sofa and complete its route
ghost.add_route(time=0, waypoints=[(5, 8, 12)])

# Simulate the grid
grid.simulate()

# Show the history of the robot
print(robot.history)
# [{'x': 5, 'y': 2, 't': 0, 'c': False}, {'x': 5.0, 'y': 4.5, 't': 5.0, 'c': True}]
# This means that the robot started at (5,2) at time=0 and moved to (5,4.5) at time=5 where it collided with the sofa
# Since there is no additional logic to handle the collision, the robot just stopped moving at time 5 and waited for the end of the simulation


# Show the history of the ghost
print(ghost.history)
# [{'x': 5, 'y': 2, 't': 0, 'c': False}, {'x': 5, 'y': 8, 't': 12, 'c': False}]
# This means that the ghost started at (5,2) at time=0 and moved to (5,8) at time=12 without any collisions

Advanced Usage

Create a custom sniffer entity

Create a Truffle Pig that sniffs out truffles on a grid.

from fizgrid.grid import Grid
from fizgrid.entities import Entity
from fizgrid.utils import Shape
import random, math


class Pig(Entity):
    def __init__(self, *args, **kwargs):
        # Override the init method of the Entity class to extend the functionality and add custom attributes.
        super().__init__(*args, **kwargs)
        self.goal_x = None
        self.goal_y = None
        self.tolerance = None
        self.speed = 1

    def get_dist_from_goal(self):
        # A function to calculate the distance from the entity to the goal.
        return (
            (self.goal_x - self.x_coord) ** 2
            + (self.goal_y - self.y_coord) ** 2
        ) ** 0.5

    def detect_truffle(self, x_coord, y_coord, tolerance=1):
        # Set the goal for the entity to reach and start the routing process.
        self.goal_x = x_coord
        self.goal_y = y_coord
        self.tolerance = tolerance
        self.add_next_route()

    def add_next_route(self):
        # Determine the next route for the entity to take.

        # This route is randomly calculated based on the angle and distance to the goal.
        #     - If the entity is within the tolerance of the goal, it will stop.
        #     - If the entity is not within the tolerance
        #         - It will calculate the target angle towards the goal.
        #         - It will calculate the target distance to the goal.
        #         - A random angle is generated by using a normal distribution centered on the goal angle.
        #         - A random distance is generated between 0 and the minimum of the distance to the goal and 5.
        distance_from_goal = self.get_dist_from_goal()
        if distance_from_goal < self.tolerance:
            return
        goal_angle_rad = math.atan2(
            self.goal_y - self.y_coord, self.goal_x - self.x_coord
        )
        random_angle = random.normalvariate(goal_angle_rad, math.pi / 2)
        distance = random.uniform(0, min(distance_from_goal, 5))
        x_shift = distance * math.cos(random_angle)
        y_shift = distance * math.sin(random_angle)

        self.add_route(
            waypoints=[
                (
                    self.x_coord + x_shift,
                    self.y_coord + y_shift,
                    distance * self.speed,
                ),
            ]
        )

    def on_realize(self, **kwargs):
        # This method is a blank method that should be overridden by the user.
        # It is called when an event for the entity is realized.

        # In this case, it is used to implment the goal checking logic and continuously add routes until the goal is reached.

        # The method checks if the entity has a goal set and if it is within the tolerance of the goal.
        # If the entity is not within the tolerance, it will add the next route.
        # If the entity is within the tolerance, it will set the goal to None.
        if self.goal_x is not None and self.goal_y is not None:
            if self.get_dist_from_goal() > self.tolerance:
                self.add_next_route()
            else:
                self.goal_x = None
                self.goal_y = None


forest = Grid(
    name="forest",
    x_size=100,
    y_size=100,
    cell_density=10,
    add_exterior_walls=True,
)

# Add some pigs to the forest
truffle_pig_1 = forest.add_entity(
    Pig(
        name="Truffle_Pig_1",
        shape=Shape.rectangle(x_len=1, y_len=1, round_to=2),
        x_coord=45,
        y_coord=50,
        auto_rotate=True,
    )
)
truffle_pig_2 = forest.add_entity(
    Pig(
        name="Truffle_Pig_2",
        shape=Shape.rectangle(x_len=1, y_len=1, round_to=2),
        x_coord=50,
        y_coord=45,
        auto_rotate=True,
    )
)

truffle_pig_1.detect_truffle(
    x_coord=55,
    y_coord=50,
)

truffle_pig_2.detect_truffle(
    x_coord=50,
    y_coord=55,
)

# Run the sim
forest.simulate()

print({
    'Name': truffle_pig_1.name,
    'x_coord': truffle_pig_1.x_coord,
    'y_coord': truffle_pig_1.y_coord,
})
print({
    'Name': truffle_pig_2.name,
    'x_coord': truffle_pig_2.x_coord,
    'y_coord': truffle_pig_2.y_coord,
})

# Example Output
# {'Name': 'Truffle_Pig_1', 'x_coord': 54.6408, 'y_coord': 50.3864}
# {'Name': 'Truffle_Pig_2', 'x_coord': 50.1886, 'y_coord': 55.9795}

Helpers and Utils

Fizgrid provides a number of utils and helper functions to streamline your workflows.

You should take special note of the following:

Development

Running Tests, Prettifying Code, and Updating Docs

Make sure Docker is installed and running on a Unix system (Linux, MacOS, WSL2).

  • Create a docker container and drop into a shell
    • ./run.sh
  • Run all tests (see ./utils/test.sh)
    • ./run.sh test
  • Prettify the code (see ./utils/prettify.sh)
    • ./run.sh prettify
  • Update the docs (see ./utils/docs.sh)

    • ./run.sh docs
  • Note: You can and should modify the Dockerfile to test different python versions.

  1"""
  2# Fizgrid
  3[![PyPI version](https://badge.fury.io/py/fizgrid.svg)](https://badge.fury.io/py/fizgrid)
  4[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
  5
  6Simulate entities that take up space over time in grid based environments.
  7
  8
  9## Overview
 10This package introduces a new approach to simulating physical movement within a grid. Rather than following the traditional method of discretizing time and iterating over spatial configurations, this simulation inverts the model: space is discretized, and time is continuous.
 11
 12Events are scheduled into a priority queue, ordered by their occurrence time. The simulation processes events in chronological order, updating only the relevant entities and grid cells affected by each event. This design allows for highly efficient simulation of large numbers of entities over long periods of simulated time.
 13
 14### Entity Movement and Scheduling
 15
 16Entity movements are planned in advance but only realized when an associated event is triggered. When a route is assigned to an entity, each grid cell along its path is reserved for the time the entity is expected to occupy it. During this reservation process, the system checks for potential conflicts. For example, whether another entity is scheduled to occupy the same cell at the same time.
 17
 18If a conflict is detected, a collision event is added to the queue for both entities. Only the first potential collision between any pair of entities is added to avoid redundancy. Additionally, an end-of-route event is queued for the entity to signal the completion of its movement.
 19
 20### Event Handling
 21
 22When an end-of-route event is processed:
 23
 24- The entity’s position is updated to where it would be at that time.
 25- All pending events related to its previous route are removed from the queue.
 26- A placeholder (blank) route is assigned to the entity to occupy its current position until a new route is defined.
 27    - This may trigger new collisions, which will be resolved in their own time.
 28
 29When a collision event is processed:
 30
 31- The entity is moved to its calculated position at the collision time.
 32- All future events tied to its current route are removed.
 33- Collision events for other entities that would have involved this entity are also removed.
 34    - The counterpart entity in the current collision still processes its corresponding event.
 35- A blank route is assigned to the entity to mark its occupancy of the current cell until reassigned.
 36    - This may trigger new collisions will be resolved in their own time.
 37
 38## Setup
 39
 40Make sure you have Python 3.11.x (or higher) installed on your system. You can download it [here](https://www.python.org/downloads/).
 41
 42### Installation
 43
 44```
 45pip install fizgrid
 46```
 47
 48## Basic Usage
 49
 50### Technical Docs
 51The technical documentation is available at [https://connor-makowski.github.io/fizgrid/index.html](https://connor-makowski.github.io/fizgrid/index.html).
 52
 53### Example
 54```py
 55from fizgrid.grid import Grid
 56from fizgrid.entities import Entity, StaticEntity, GhostEntity
 57from fizgrid.utils import Shape
 58
 59# Create a grid with exterior walls
 60grid = Grid(
 61    name="living_room",
 62    x_size=10,
 63    y_size=10,
 64    add_exterior_walls=True,
 65    cell_density=1,
 66)
 67
 68# Add some static entities
 69sofa = grid.add_entity(
 70    StaticEntity(
 71        name="sofa",
 72        shape=Shape.rectangle(x_len=2, y_len=1),
 73        x_coord=5,
 74        y_coord=5.5,
 75    )
 76)
 77
 78# Add a dynamic entity
 79robot = grid.add_entity(
 80    Entity(
 81        name="robot",
 82        shape=Shape.rectangle(x_len=1, y_len=1),
 83        x_coord=5,
 84        y_coord=2,
 85    )
 86)
 87
 88# Add a dynamic entity that can pass through other entities
 89ghost = grid.add_entity(
 90    GhostEntity(
 91        name="ghost",
 92        shape=Shape.rectangle(x_len=1, y_len=1),
 93        x_coord=5,
 94        y_coord=2,
 95    )
 96)
 97
 98# Attempt to move the robot through the sofa
 99# Starting at time=0, the robot will try to move to (5,8) over 12 seconds
100# This will bump into the sofa and stop
101robot.add_route(time=0, waypoints=[(5, 8, 12)])
102
103
104# Attempt to move the ghost through the sofa
105# Starting at time=0, the ghost will try to move to (5,8) over 12 seconds
106# This will not bump into the sofa and complete its route
107ghost.add_route(time=0, waypoints=[(5, 8, 12)])
108
109# Simulate the grid
110grid.simulate()
111
112# Show the history of the robot
113print(robot.history)
114# [{'x': 5, 'y': 2, 't': 0, 'c': False}, {'x': 5.0, 'y': 4.5, 't': 5.0, 'c': True}]
115# This means that the robot started at (5,2) at time=0 and moved to (5,4.5) at time=5 where it collided with the sofa
116# Since there is no additional logic to handle the collision, the robot just stopped moving at time 5 and waited for the end of the simulation
117
118
119# Show the history of the ghost
120print(ghost.history)
121# [{'x': 5, 'y': 2, 't': 0, 'c': False}, {'x': 5, 'y': 8, 't': 12, 'c': False}]
122# This means that the ghost started at (5,2) at time=0 and moved to (5,8) at time=12 without any collisions
123```
124
125## Advanced Usage
126
127### Create a custom sniffer entity
128Create a Truffle Pig that sniffs out truffles on a grid.
129```py
130from fizgrid.grid import Grid
131from fizgrid.entities import Entity
132from fizgrid.utils import Shape
133import random, math
134
135
136class Pig(Entity):
137    def __init__(self, *args, **kwargs):
138        # Override the init method of the Entity class to extend the functionality and add custom attributes.
139        super().__init__(*args, **kwargs)
140        self.goal_x = None
141        self.goal_y = None
142        self.tolerance = None
143        self.speed = 1
144
145    def get_dist_from_goal(self):
146        # A function to calculate the distance from the entity to the goal.
147        return (
148            (self.goal_x - self.x_coord) ** 2
149            + (self.goal_y - self.y_coord) ** 2
150        ) ** 0.5
151
152    def detect_truffle(self, x_coord, y_coord, tolerance=1):
153        # Set the goal for the entity to reach and start the routing process.
154        self.goal_x = x_coord
155        self.goal_y = y_coord
156        self.tolerance = tolerance
157        self.add_next_route()
158
159    def add_next_route(self):
160        # Determine the next route for the entity to take.
161
162        # This route is randomly calculated based on the angle and distance to the goal.
163        #     - If the entity is within the tolerance of the goal, it will stop.
164        #     - If the entity is not within the tolerance
165        #         - It will calculate the target angle towards the goal.
166        #         - It will calculate the target distance to the goal.
167        #         - A random angle is generated by using a normal distribution centered on the goal angle.
168        #         - A random distance is generated between 0 and the minimum of the distance to the goal and 5.
169        distance_from_goal = self.get_dist_from_goal()
170        if distance_from_goal < self.tolerance:
171            return
172        goal_angle_rad = math.atan2(
173            self.goal_y - self.y_coord, self.goal_x - self.x_coord
174        )
175        random_angle = random.normalvariate(goal_angle_rad, math.pi / 2)
176        distance = random.uniform(0, min(distance_from_goal, 5))
177        x_shift = distance * math.cos(random_angle)
178        y_shift = distance * math.sin(random_angle)
179
180        self.add_route(
181            waypoints=[
182                (
183                    self.x_coord + x_shift,
184                    self.y_coord + y_shift,
185                    distance * self.speed,
186                ),
187            ]
188        )
189
190    def on_realize(self, **kwargs):
191        # This method is a blank method that should be overridden by the user.
192        # It is called when an event for the entity is realized.
193
194        # In this case, it is used to implment the goal checking logic and continuously add routes until the goal is reached.
195
196        # The method checks if the entity has a goal set and if it is within the tolerance of the goal.
197        # If the entity is not within the tolerance, it will add the next route.
198        # If the entity is within the tolerance, it will set the goal to None.
199        if self.goal_x is not None and self.goal_y is not None:
200            if self.get_dist_from_goal() > self.tolerance:
201                self.add_next_route()
202            else:
203                self.goal_x = None
204                self.goal_y = None
205
206
207forest = Grid(
208    name="forest",
209    x_size=100,
210    y_size=100,
211    cell_density=10,
212    add_exterior_walls=True,
213)
214
215# Add some pigs to the forest
216truffle_pig_1 = forest.add_entity(
217    Pig(
218        name="Truffle_Pig_1",
219        shape=Shape.rectangle(x_len=1, y_len=1, round_to=2),
220        x_coord=45,
221        y_coord=50,
222        auto_rotate=True,
223    )
224)
225truffle_pig_2 = forest.add_entity(
226    Pig(
227        name="Truffle_Pig_2",
228        shape=Shape.rectangle(x_len=1, y_len=1, round_to=2),
229        x_coord=50,
230        y_coord=45,
231        auto_rotate=True,
232    )
233)
234
235truffle_pig_1.detect_truffle(
236    x_coord=55,
237    y_coord=50,
238)
239
240truffle_pig_2.detect_truffle(
241    x_coord=50,
242    y_coord=55,
243)
244
245# Run the sim
246forest.simulate()
247
248print({
249    'Name': truffle_pig_1.name,
250    'x_coord': truffle_pig_1.x_coord,
251    'y_coord': truffle_pig_1.y_coord,
252})
253print({
254    'Name': truffle_pig_2.name,
255    'x_coord': truffle_pig_2.x_coord,
256    'y_coord': truffle_pig_2.y_coord,
257})
258
259# Example Output
260# {'Name': 'Truffle_Pig_1', 'x_coord': 54.6408, 'y_coord': 50.3864}
261# {'Name': 'Truffle_Pig_2', 'x_coord': 50.1886, 'y_coord': 55.9795}
262```
263
264## Helpers and Utils
265
266Fizgrid provides a number of utils and helper functions to streamline your workflows.
267
268You should take special note of the following:
269- `fizgrid.utils.Shape`: A class to help define the shape of an entity. (See: [Shape](https://connor-makowski.github.io/fizgrid/fizgrid/utils.html#Shape))
270- `fizgrid.helpers.waypoint_timing`: A function to help calculate the timing of waypoints. (See: [waypoint_timing](https://connor-makowski.github.io/fizgrid/fizgrid/helpers/waypoint_timing.html))
271
272
273# Development
274## Running Tests, Prettifying Code, and Updating Docs
275
276Make sure Docker is installed and running on a Unix system (Linux, MacOS, WSL2).
277
278- Create a docker container and drop into a shell
279    - `./run.sh`
280- Run all tests (see ./utils/test.sh)
281    - `./run.sh test`
282- Prettify the code (see ./utils/prettify.sh)
283    - `./run.sh prettify`
284- Update the docs (see ./utils/docs.sh)
285    - `./run.sh docs`
286
287- Note: You can and should modify the `Dockerfile` to test different python versions."""