Cell and Agent Managers#

Here we address some frequently asked questions regarding the CellManager and AgentManager.


Entity Traits#

What are these trait objects needed for manager setup?#

The Utopia::CellTraits and Utopia::AgentTraits are structs that define the properties and behaviour of the cells or agents, respectively. They are passed to the managers to allow them to set up these entities in the desired way. Only two properties need to always be defined: the state type of the entity and its update mode. A trait definition can then look like this:

/// Traits for cells with synchronous update
using MySyncCell = Utopia::CellTraits<MyCellState, Update::sync>;

/// Traits for cells with asynchronous update
using MyAsyncCell = Utopia::CellTraits<MyCellState, Update::async>;

/// Traits for cells with determination of update via apply_rule call
using MyManualCell = Utopia::CellTraits<MyCellState, Update::manual>;

Here, MyCellState can be any type that is to be associated with an entity (a cell or an agent).

The update modes behave as follows:

  • (always) synchronously: all agents of a manager are updated “at once”

    • Internally, these entities have a buffer for their new state, where all changes are written to.

      Only after all state buffers have been updated, the actual state is updated.

    • This two-step process emulates an update process where a state change happens at the same time for all agents.

  • (always) asynchronously: all agents change their state directly

    • These entities do not have a state buffer; all changes are applied directly.

    • This emulates a process where state changes occur sequentially.

  • manually: whether the state is updated synchronously or asynchronously is determined by arguments to the apply_rule call

    • This allows for more flexibility in the apply_rule calls

    • Downside: creates some memory allocation overhead when using synchronous apply_rule calls, because the buffer needs to be stored anew

Hint

Which Update mode should I choose in the entity state?

To decide, which Update mode to use in the entity state, the following can help:

  • If your model will use only few rules and their update mode is always the same: use sync or async.

  • If you desire more flexibility and need to mix different update rules, use manual.

Initialize cells and agents#

Which ways are there to initialize cells and agents?#

There are three different ways to initialize cells and agents in your CellManager or AgentManager. The examples below are for the CellManager but apply analogously to the AgentManager.

Constructing an initial state from the default constructor#

As default constructors can sometimes lead to undefined behaviour, they need to be explicitly allowed. This happens via the Utopia::CellTraits struct.

/// A cell state definition that is default-constructible
struct CellStateDC {
    double a_double;
    std::string a_string;
    bool a_bool;

    CellStateDC()
    :
        a_double(3.14), a_string("foo"), a_bool(false)
    {}
};

/// Traits for a default-constructible cell state with synchronous update
using CellTraitsDC = Utopia::CellTraits<CellStateDC, Update::sync, true>;

In such a case, the manager (as with config-constructible) does not require an initial state.

Note

For setting up cell states individually for each cell, see the question regarding the use of random number generators.

Explicit initial state#

In this mode, all cells have an identical initial state, which is passed down from the CellManager. Assuming you are setting up the manager as a member of MyFancyModel, this would look something like this:

/// The cell state type
struct MyCellState {
    int foo;
    double bar;
}

/// Traits for cells with synchronous update
using MyCellTraits = Utopia::CellTraits<MyCellState, Update::sync>;

// Define an appropriate initial cell state
const auto initial_cell_state = MyCellState(42, 3.14);

// ...

class MyFancyModel {
public:
    // MyFancyModel member definitions
    CellManager<MyCellTraits, MyFancyModel> _cm;

    /// MyFancyModel constructor
    template<class ParentModel>
    MyFancyModel (const std::string name, ParentModel &parent)
    :
        // ...
        // Set up the cell manager, passing the initial cell state
        _cm(*this, initial_cell_state),
        // ...
    {}
};

Can I use a random number generator when constructing cells or agents?#

Yes. The respective managers have access to the shared RNG of the model. If cells or agents provide a constructor that allows passing not only a const Config&, but also a random number generator, that constructor has precedence over the one that does not allow passing an RNG:

/// A cell state definition that is config-constructible and has an RNG
struct CellStateRC {
    double a_double;
    std::string a_string;
    bool a_bool;

    // Construct a cell state with the use of a RNG
    template<class RNGType>
    CellStateRC(const Config& cfg, const std::shared_ptr<RNGType>& rng)
    :
        a_double(Utopia::get_as<double>("a_double", cfg)),
        a_string(Utopia::get_as<std::string>("a_string", cfg))
    {
        // Do something with the RNG to set the boolean
        std::uniform_real_distribution<double> dist(0., 1.);
        a_bool = (dist(*rng) < a_double);
    }
};

With this constructor available, a constructor with the signature CellStateRC(const Config& cfg) is not necessary and would not be called!

Keep in mind to also change the CellTraitsRC such that the CellStateRC creation is done with the config constructor and not the default constructor. For this, set the boolean correctly at the end of the template list to true, as explained above:

/// Traits for a default-constructible cell state with synchronous update
using CellTraitsRC = Utopia::CellTraits<CellStateRC, Update::sync, true>;

Note

In order to have a reproducible state for the RNG, Utopia sets the RNG seed globally. That is why the RNG needs to be passed through all the way down to the cell state constructor.

You should not create a new RNG, neither here nor anywhere else.

CellManager FAQs#

Neighborhood calculation#

Where and how are neighborhoods calculated?#

The neighborhood computation does not take place in the CellManager itself, but in the underlying Grid object and based on the cells’ IDs. The CellManager then retrieves the corresponding shared pointers from the IDs and makes them available via the neighbors_of method.

Are neighborhoods computed on the fly, or can I cache them?#

Yes, the CellManager allows caching the neighborhood computation’s result. This feature can be controlled via the compute_and_store argument.

If enabled (which is the default), the neighborhood is computed once for each cell, stored, and retrieved upon calls to neighbors_of. For more information, see the doxygen documentation.

Having this feature enabled gives a slight performance gain in most situations. However, if memory is limited, it might make sense to disable it:

cell_manager:
  neighborhood:
    mode: Moore
    compute_and_store: false

Note

In the Grid itself, the IDs of the cells in the neighborhood are always computed on the fly.

Can I change the grid discretization? And when should I?#

Yes, the grid discretization can be changed. Currently available are the square and hexagonal grid discretizations. To change this, select the respective structure and neighborhood/mode in the cell_manager’s configuration:

# model configuration
---
cell_manager:
  grid:
    structure: square   # can be: square or hexagonal
    resolution: 42      # cells per unit length (of space)

  neighborhood:
    mode: Moore         # can be: empty (0), vonNeumann (4), Moore (8) (with square structure)
                        # can be: empty (0), hexagonal (6) (with hexagonal structure)
                        # the number indicates the number of neighbors per cell

  # Other cell_manager configuration parameters ...

# Other model configuration parameters ...

Note

The resolution of the hexagonal discretization is evaluated per unit area (of space), instead of unit length, as the extent of a hexagon is non-isotropic. I.e. with a resolution of 32 in a space with extent (1.0, 1.0), there will be 30 x 34 = 1020 cells.

The grid discretization, together with the respectively available neighborhoods, should be changed when exploring the influence of geometry and cell-connectivity on cell-cell interactions. In particular, the number of neighbors per cell can be varied between

  • 4 (square grid with vonNeumann neighborhood)

  • 6 (hexagonal grid with hexagonal neighborhood)

  • 8 (square grid with Moore neighborhood)

  • 0 (either grid with empty neighborhood).

On the other hand, the two discretizations differ in how paths in space are constructed when moving from cell to cell: In the vonNeumann neighborhood on a square lattice the 4 next neighborhoods have unit distance, while the diagonal cells are overly far with distance 2. In the Moore neighborhood they are too close with distance 1, where the true distance of the cell centers would be \(\sqrt{2}\). In the hexagonal discretization all neighbors have the true unit distance, however this is only true for paths that are 60° (instead of 90°) apart.

For more details (e.g. regarding coordinate mode, cell orientation etc.) have a look at the grid implementation and this excellent introduction to hexagonal grid representation.

Note

For an example for comparing these, have a look at the SEIRD model and its grid_structure_sweep config set.

Hint

The plot function specialized on cellular automata, .plot.ca, can visualize hexagonal grids out-of-the-box. For more info, see the corresponding page.