Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optimize wolf-sheep movement performance #2565

Open
EwoutH opened this issue Dec 22, 2024 · 3 comments
Open

Optimize wolf-sheep movement performance #2565

EwoutH opened this issue Dec 22, 2024 · 3 comments
Assignees
Labels
example Changes the examples or adds to them. Performance

Comments

@EwoutH
Copy link
Member

EwoutH commented Dec 22, 2024

The current movement implementation in the wolf-sheep model performs redundant checks when agents search their neighborhood for valid moves, after #2503.

cells_without_wolves = self.cell.neighborhood.select(
lambda cell: not any(isinstance(obj, Wolf) for obj in cell.agents)
)

cells_with_grass = cells_without_wolves.select(
lambda cell: any(
isinstance(obj, GrassPatch) and obj.fully_grown for obj in cell.agents
)
)

cells_with_sheep = self.cell.neighborhood.select(
lambda cell: any(isinstance(obj, Sheep) for obj in cell.agents)
)

This could be performance optimized. Two promising optimization approaches:

  1. Early exit strategy
    Instead of checking every cell in the neighborhood for agent types, we can break out of loops early once a valid cell is found. For example, when a sheep checks for wolves, it can immediately skip a cell once a wolf is detected rather than continuing to check other agents.

  2. PropertyLayer-based vectorization
    We can leverage Mesa's PropertyLayer to track wolf and grass positions as boolean layers. By having wolves and grass patches update their position in the property layer during movement, we can perform vectorized operations to find valid moves instead of iterating through agent lists. This would allow expressing the entire movement logic using fast numpy operations.

Trade-offs:

  • Early exits: Simpler implementation but more limited gains
  • PropertyLayer: More complex but potentially much faster for large grids, at cost of extra memory

Worth benchmarking both approaches against current implementation with different model parameters.

Somewhat related:

@EwoutH EwoutH added Performance example Changes the examples or adds to them. labels Dec 22, 2024
@EwoutH EwoutH changed the title Performance optimization opportunities for wolf-sheep movement Optimize wolf-sheep movement performance Dec 22, 2024
@ricor07
Copy link

ricor07 commented Dec 28, 2024

Hello, I'd like to work on the project by fixing this issue. Can you assign this to me? Thanks

@EwoutH
Copy link
Member Author

EwoutH commented Dec 28, 2024

Awesome! Let me know if you have any questions.

@Sahil-Chhoker
Copy link
Contributor

@EwoutH I looked into this issue and tried optimizing the code for movement using property layers but I couldn't get that much of a performance boost. Is my approach wrong, and If yes how can it be improved?
What I have done is I centralized the movement logic inside Animal class using three property layers each for grass, sheep and wolf, something like this:

def move(self):
        """Generic movement logic using property layers."""
        current_pos = self.cell.coordinate
        x, y = current_pos
        w = self.model.grid.width
        h = self.model.grid.height

        # Get neighbor positions using array operations
        neighbors = np.array(
            [((x + 1) % w, y), ((x - 1) % w, y), (x, (y + 1) % h), (x, (y - 1) % h)]
        )

        # Get target layer based on agent type
        target_layer = (
            self.model.sheep_layer if isinstance(self, Wolf) else self.model.grass_layer
        )
        avoid_layer = self.model.wolf_layer if isinstance(self, Sheep) else None

        # Create masks for valid moves
        if avoid_layer is not None:
            valid_mask = np.array(
                [avoid_layer.data[tuple(pos)] == 0 for pos in neighbors]
            )
            if not valid_mask.any():
                return False
            neighbors = neighbors[valid_mask]

        target_mask = np.array([target_layer.data[tuple(pos)] > 0 for pos in neighbors])

        # Choose target position
        if len(neighbors) == 0:
            return False

        if target_mask.any():
            idx = self.random.randint(0, int(target_mask.sum()) - 1)
            target_pos = neighbors[target_mask][idx]
        else:
            idx = self.random.randint(0, len(neighbors) - 1)
            target_pos = neighbors[idx]

        # Update position and layers
        self._update_layer(-1)  # Remove from old position
        self.cell = self.model.grid[tuple(target_pos)]
        self._update_layer(1)  # Add to new position
        return True

Upon running the profiler and looking at results:
Total Function Calls:

  • Before: 1,569,209 calls in 0.632 seconds
  • After: 1,397,708 calls in 0.655 seconds
  • Change: -171,501 total calls, +0.023 seconds
Function Before After Change in Cumtime
ncalls tottime cumtime ncalls tottime cumtime
run_simulation 1 0.000 0.632 1 0.000 0.655 +0.023
_wrapped_step 100 0.000 0.549 100 0.000 0.582 +0.033
step 100 0.000 0.549 100 0.000 0.582 +0.033
shuffle_do 200 0.007 0.328 200 0.007 0.368 +0.040
agents.step 6593 0.015 0.311 6703 0.013 0.351 +0.040
agents.move (sheep) 3708 0.011 0.182 - - - N/A
agents.move (wolf) 2885 0.007 0.076 - - - N/A
agents.move (accumulated) - - - 6703 0.136 0.296 N/A
collect 101 0.001 0.224 101 0.001 0.216 -0.008
model.<lambda> (line 88) 101 0.001 0.221 101 0.001 0.214 -0.007
agent.select 101 0.000 0.220 101 0.000 0.213 -0.007
agent.__init__ 107 0.006 0.220 105 0.007 0.212 -0.008
agent_generator 29574 0.086 0.202 30559 0.086 0.195 -0.007
model.__init__ 1 0.004 0.083 1 0.005 0.073 -0.010
model.<lambda> (line 89) 252500 0.049 0.069 252500 0.043 0.062 -0.007
cell - - - 7554 0.009 0.040 N/A
agents.__init__ (line 132) 2500 0.005 0.042 2500 0.005 0.039 -0.003

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
example Changes the examples or adds to them. Performance
Projects
None yet
Development

No branches or pull requests

3 participants