This project is a C++ graphical user interface (GUI) application that allows users to configure and simulate population dynamics on a discrete grid. Users can adjust the grid size, populate it with various species, set interaction coefficients, and observe how populations spread and interact over time according to specified numerical methods.
- Overview
- Features
- Getting Started
- Usage
- Simulation Details
- Mathematical Formulation of Diffusion
- Code Structure
- Contributing
- License
- Acknowledgements
- Additional Information
- How the Simulation Works
This application simulates the spread and interaction of multiple species across a discrete grid (board). Users can:
- Adjust the dimensions of the grid.
- Add or remove species.
- Configure how species interact with each other using coefficients.
- Set dispersion rates for species.
- Populate the grid with initial populations.
- Run simulations over a specified number of time steps.
- Visualize the population dynamics over time.
- Interactive GUI: Built using Dear ImGui, providing an intuitive interface.
- Dynamic Configuration: Adjust grid size and species parameters on the fly.
- Visualization: Visual representation of the grid and population distributions.
- Customizable Interactions: Define how species affect each other.
- Simulation Control: Set the number of time steps and simulate population dynamics.
- Numerical Methods: Implements specific numerical algorithms for population dispersion and interaction.
- C++ Compiler: A C++17 compliant compiler (e.g., GCC, Clang, MSVC).
- Dear ImGui: The immediate mode GUI library.
- ImPlot: For plotting and visualizing data within ImGui.
- CMake: For building the project (optional but recommended).
-
Clone the Repository:
git clone https://github.com/ErsjanKeri/SimDynamiX.git
-
Install Dependencies:
- Ensure that Dear ImGui and ImPlot are included in your project. You can add them as submodules or include them in your project directory.
git submodule update --init --recursive
-
Build the Project:
mkdir build cd build cmake .. make
Run the compiled executable:
./SimDynamiX
- Board Width and Height: Adjust the dimensions of the grid using the sliders labeled "Board Width" and "Board Height".
- The grid resizes dynamically, and you can see the current size printed in the console.
- Add Species: Click the
+
button to add a new species. Each species is assigned a default name and color. - Remove Species: Click the
-
button to remove the last species. The number of species cannot be less than one. - Rename Species: Enter a new name in the input field next to each species to rename it.
- Interaction Matrix: The table labeled "How does 'column' species affect 'color' one" allows you to set coefficients that define how each species affects others.
- Coefficients: Enter a floating-point number in each cell to represent the interaction strength.
- Dispersion: Set the dispersion rate for each species in the last column. This value must be between 0 and 0.4. You can imagine: the smaller the dispersion, the less "conductive"/less spread happens
- Timesteps: Specify the number of timesteps for the simulation in the "Timesteps" input field. The value must be between 1 and 2000.
- Simulate: Click the "Simulate" button to run the simulation with the current settings. The application will transition to the simulation view.
- Grid Display: The grid represents the discrete field where populations exist.
- Each cell shows the population counts for all species in that location.
- Select a Cell: Click on a cell to select it. The population details for that cell will be displayed below the grid.
- Edit Populations: When a cell is selected, you can adjust the population counts for each species using the input fields provided.
The simulation uses specific numerical methods to model how populations spread and interact over time. These methods include:
To simulate the dispersion (spread) of populations across the grid, the application uses a discrete approximation of diffusion processes. The dispersion is computed separately in one dimension for rows and columns and then combined to yield the two-dimensional dispersion.
-
1D Dispersion: For each row and column, the dispersion is calculated using a dispersion matrix. This matrix is tridiagonal with values that simulate the diffusion effect.
Dispersion Matrix Generation:
vector<vector<int>> generateDispersionMatrix(int n) { auto dispersionMatrix = vector(n, vector(n, 0)); for (int i = 0; i < n; i++) { if (i == 0) { dispersionMatrix[i][i] = -2; dispersionMatrix[i][i + 1] = 1; } else if (i == n - 1) { dispersionMatrix[i][i - 1] = 1; dispersionMatrix[i][i] = -2; } else { dispersionMatrix[i][i - 1] = 1; dispersionMatrix[i][i] = -2; dispersionMatrix[i][i + 1] = 1; } } return dispersionMatrix; }
-
Matrix-Vector Multiplication: The population vector is multiplied by the dispersion matrix to compute the dispersion effect. The Matrix here is Tri-Diagonal, therefore matrix-vector multiplication can be done in
$O(n)$ instead of$O(n^2)$ vector<int> matrixVectorMultiplication(vector<int> vec, vector<vector<int>> matrix) { vector<int> resultVector(vec.size(), 0); for (int row = 0; row < matrix.size(); row++) { int sum = 0; if (row > 0) { sum += vec[row - 1] * matrix[row][row - 1]; // left diagonal element } sum += vec[row] * matrix[row][row]; // main diagonal element if (row < matrix.size() - 1) { sum += vec[row + 1] * matrix[row][row + 1]; // right diagonal element } resultVector[row] = sum; } return resultVector; }
-
Combining Row and Column Dispersion: After computing the dispersion for rows and columns, the results are combined and multiplied with the dispersion coefficient. Why/How this works see Mathematical Formulation of Diffusion
// Combining the 1D dispersions to yield the 2D dispersion for (int row = 0; row < population.size(); row++) { for (int col = 0; col < population[0].size(); col++) { newPopulation[row][col] = static_cast<int>((newPopulationRows[row][col] + newPopulationCols[col][row]) * dispersionCoefficient); } }
The interaction between species is modeled using a set of coefficients that define how one species affects another. This is computed using a scalar product of the populations and the coefficients.
-
Scalar Product Calculation:
float scalarProduct(vector<int> & animals, vector<float> & coefficients) { float res = 0; for (int i = 0; i < animals.size(); i++) { res += static_cast<float>(animals[i]) * coefficients[i]; } return res; }
-
Updating Populations: The population change at each cell is computed based on the interactions.
void computeChangedPopulation(vector<vector<vector<int>>> & board, vector<vector<float>> & coefficients) { for (int y = 0; y < board.size(); y++) { for (int x = 0; x < board[0].size(); x++) { for (int k = 0; k < board[0][0].size(); k++) { board[y][x][k] += static_cast<int>(scalarProduct(board[y][x], coefficients[k])); } } } }
- Initialization: Populations are initialized on the grid based on user input.
- Simulation Loop: For each timestep:
- Population Interaction: Update populations based on interaction coefficients.
- Population Dispersion: Compute dispersion for each species.
- Update Grid: Apply the computed changes to the grid.
- Record State: Save the current state for visualization.
- Visualization: Display the population distributions over time using heatmaps.
Main Simulation Function:
void prepareCalculations() {
add_to_steps(); // Record initial state
for (int i = 0; i < number_steps_t; i++) {
computeChangedPopulation(board, coefficients);
computePopulationsDispersion(board, dispersion_coefficients);
add_to_steps(); // Record state after each timestep
}
}
The diffusion equation (also known as the heat equation) describes how a quantity, such as population density or temperature, diffuses over time:
where:
-
$u = u(x, y, t)$ : The quantity being diffused (e.g., population density).steps[t][y][x]
$\approx u(x,y,t)$ . -
$\frac{\partial u}{\partial t}$ : The time derivative of$u$ , representing how$u$ changes over time. -
$D$ : The diffusion coefficient, which controls the rate of diffusion. -
$\nabla^2 u$ : The Laplacian of$u$ , which represents the rate of change in$u$ across space, in our case space is only two dimensions (that being width and height of the board).
The Laplacian in two dimensions (for our board) would be:
To approximate this on a discrete grid, we use the finite difference method.
-
$u_{i,j}$ : The value at grid pointboard[j][i]
. - Grid spacing in both directions is
$h$ , in our case$h$ is implicitly 1.
The finite difference approximation of the Laplacian at point
This formula approximates the second derivatives in the
The time derivative is also approximated using a finite difference. Let:
-
$u_{i,j}^n$ be the value at grid point$(i, j)$ at time step$n$ . - Time step is
$\Delta t$ . In our case our timestep is also implicitly 1, as our initial goal was to work with discrete values.
Using the explicit Euler method:
By combining the discretized time and space derivatives, we approximate the diffusion equation as:
Rearranging to solve for
This equation tells us how to update the value of computePopulationsDispersion(vector<vector<vector<int>>> & populations, vector<float> dispersionCoefficients)
- The dispersion matrix in the code acts as a discrete Laplacian in 1D, imagine the head equation on a rod, there you can see where does the
-1, 2, -1
pattern come from. - The finite difference scheme is used to compute how each point’s value changes based on its neighbors. It is applied once for all rows and once for all columns.
- This is equivalent to applying the 2D Laplacian, calculated separately for rows and columns, to approximate the diffusion of populations over time.
In summary, the code aims to approximate the heat equation using finite difference methods to compute both the spatial Laplacian and the temporal derivative, thereby modeling how populations disperse across the grid over time.
config_board_size_species()
:- Handles the configuration of the board size and the number of species.
- Updates the grid dimensions and manages species addition and removal.
config_species_list()
:- Provides input fields to rename species.
config_dynamics()
:- Displays the interaction matrix where users set coefficients for species interactions.
- Includes input for dispersion coefficients.
- Contains the "Simulate" button to start the simulation.
board_render()
:- Renders the grid where populations are visualized.
- Allows users to select cells and adjust populations.
- Displays population details for the selected cell.
Numerical.h
andNumerical.cpp
:- Contain the numerical methods used for simulating population dispersion and interaction.
- Key functions include
computePopulationsDispersion
,computeChangedPopulation
, andprepareCalculations
.
board
: A 3D vector representing the grid. Each cell contains a vector of population counts for each species.species
: A vector ofSpecies
objects representing each species in the simulation.coefficients
: A 2D vector holding interaction coefficients between species.dispersion_coefficients
: A vector containing dispersion rates for each species.number_steps_t
: An integer representing the total number of timesteps for the simulation.selected_box
: An integer representing the currently selected cell in the grid.steps
: A vector that records the state of the grid at each timestep for visualization. Basically an array ofboard
If you would like to improve accuracy or efficiency, contributions are welcomed! Please open issues or pull requests for any changes or additions you'd like to make.
- Fork the repository.
- Create your feature branch (
git checkout -b feature/YourFeature
). - Commit your changes (
git commit -am 'Add some feature'
). - Push to the branch (
git push origin feature/YourFeature
). - Open a pull request.
- Dear ImGui: For the immediate mode GUI framework.
- ImPlot: For advanced plotting capabilities within ImGui.
- C++ Standard Library: For data structures and algorithms.
- ImGui: The core GUI library used to build the interface.
- ImPlot: An extension to ImGui that provides advanced plotting features.
- SDL2: Used for window creation and input handling.
- OpenGL: For rendering graphics.