CovidGridContagion
==================

To show how the ``Melodie.Grid`` module can be used,
we provide this `CovidGridContagion <https://github.com/ABM4ALL/CovidGridContagion>`_ model,
which is based the `CovidContagion <https://github.com/ABM4ALL/CovidContagion>`_ model.
So, if you haven't, we will strongly suggest to read the :ref:`Tutorial` section first.

The differences are:

* Agents walk on a 2D ``grid`` randomly.
* The ``grid`` is constructed with spots. Each ``spot`` has an attribute ``stay_prob``, which decides if the agent standing on this spot will move probabilistically.
* The infected agents can pass the virus to the other uninfected agents in the neighborhood.

Project Structure
_________________

The project structure is as below.
Compared with the structure of the `CovidContagion <https://github.com/ABM4ALL/CovidContagion>`_ model,
one ``grid.py`` file is added in the ``source`` folder,
and one ``Parameter_GridStayProb.xlsx`` file is added in the ``data/input`` folder.

::

    CovidGridContagion
    ├── data
    │   ├── input
    │   │   ├── SimulatorScenarios.xlsx
    │   │   ├── ID_HealthState.xlsx
    │   │   ├── ID_AgeGroup.xlsx
    │   │   ├── Parameter_GridStayProb.xlsx
    │   │   └── Parameter_AgeGroup_TransitionProb.xlsx
    │   └── output
    │       ├── CovidGridContagion.sqlite
    │       ├── PopulationInfection_S0R0.png
    │       └── PopulationInfection_S1R0.png
    ├── source
    │   ├── agent.py
    │   ├── environment.py
    │   ├── grid.py
    │   ├── data_collector.py
    │   ├── data_info.py
    │   ├── data_loader.py
    │   ├── scenario.py
    │   ├── model.py
    │   └── analyzer.py
    ├── config.py
    ├── run_simulator.py
    ├── run_analyzer.py
    └── readme.md

Grid and Spot
_____________

To include such differences about ``grid``, ``Melodie`` provides two classes ``Grid`` and ``Spot``,
based on which the ``CovidGrid`` and ``CovidSpot`` classes are defined.

.. code-block:: Python
   :caption: grid.py
   :linenos:
   :emphasize-lines: 15

   from Melodie import Spot, Grid
   from source import data_info


   class CovidSpot(Spot):

       def setup(self):
           self.stay_prob = 0.0


   class CovidGrid(Grid):

       def setup(self):
           self.set_spot_property(
               "stay_prob", self.scenario.get_matrix(data_info.grid_stay_prob)
           )

As shown, the two classes are defined in brief, i.e., most functions and attributes are inherited from ``Melodie``.
The only reason to define this ``CovidGrid`` class for the model, is to include the ``stay_prob`` attribute of each ``CovidSpot``.
Or, one can also use the ``Grid`` and ``Spot`` classes of ``Melodie`` directly.

Matrix Data
___________

The ``stay_prob`` values of the spots are saved in a matrix, which is the same size with the grid.
Like other dataframes, the matrix is first registered in the ``data_info.py`` and loaded by the ``data_loader``,
then can be accessed by the ``scenario`` object by using ``get_matrix`` function (Line 15).
The registry of the matrix is as follows.

.. code-block:: Python
   :caption: data_info.py
   :linenos:

   import sqlalchemy

   from Melodie import MatrixInfo


   grid_stay_prob = MatrixInfo(
       mat_name="grid_stay_prob",
       data_type=sqlalchemy.Float(),
       file_name="Parameter_GridStayProb.xlsx",
   )

Model
_____

With the classes and data, the next step is to include the ``grid`` object as a new component of the ``model``.
As shown in Line 24, ``Melodie.Model`` provides a ``create_grid`` function,
taking the two class variables as input - ``CovidGrid`` and ``CovidSpot``.

.. code-block:: Python
   :caption: model.py
   :linenos:
   :emphasize-lines: 24, 31-35

   from typing import TYPE_CHECKING

   from Melodie import Model

   from source import data_info
   from source.agent import CovidAgent
   from source.data_collector import CovidDataCollector
   from source.environment import CovidEnvironment
   from source.grid import CovidGrid
   from source.grid import CovidSpot
   from source.scenario import CovidScenario

   if TYPE_CHECKING:
       from Melodie import AgentList


   class CovidModel(Model):
       scenario: "CovidScenario"

       def create(self):
           self.agents: "AgentList[CovidAgent]" = self.create_agent_list(CovidAgent)
           self.environment = self.create_environment(CovidEnvironment)
           self.data_collector = self.create_data_collector(CovidDataCollector)
           self.grid = self.create_grid(CovidGrid, CovidSpot)

       def setup(self):
           self.agents.setup_agents(
               agents_num=self.scenario.agent_num,
               params_df=self.scenario.get_dataframe(data_info.agent_params),
           )
           self.grid.setup_params(
               width=self.scenario.grid_x_size,
               height=self.scenario.grid_y_size
           )
           self.grid.setup_agent_locations(self.agents)

In the ``setup`` function, the ``grid`` needs to be initialized with size parameters from ``scenario``.
Then, the agents need to be located on the ``grid``,
which requires that they are already initialized with the attributes ``x`` and ``y``.

GridAgent
_________

The agents that can walk on the grid are defined by inheriting the ``GridAgent`` class of ``Melodie``.
They have three additional attributes: ``category``, ``x``, and ``y``.
Additionally, they also have access to the ``grid`` (Line 26) and some grid-related functions,
e.g., ``rand_move_agent`` in Line 32.

.. code-block:: Python
   :caption: agent.py
   :linenos:
   :emphasize-lines: 18, 22-23, 28, 32, 35-36

   import random
   from typing import TYPE_CHECKING

   from Melodie import GridAgent

   if TYPE_CHECKING:
       from source.scenario import CovidScenario
       from Melodie import AgentList
       from source.grid import CovidSpot
       from source.grid import CovidGrid


   class CovidAgent(GridAgent):
       scenario: "CovidScenario"
       grid: "CovidGrid[CovidSpot]"
       spot: "CovidSpot"

       def set_category(self):
           self.category = 0

       def setup(self):
           self.x: int = 0
           self.y: int = 0
           self.health_state: int = 0
           self.age_group: int = 0

       def move(self):
           spot: "CovidSpot" = self.grid.get_spot(self.x, self.y)
           stay_prob = spot.stay_prob
           if random.uniform(0, 1) > stay_prob:
               move_radius = 1
               self.rand_move_agent(move_radius, move_radius)

       def infection(self, agents: "AgentList[CovidAgent]"):
           neighbors = self.grid.get_neighbors(self)
           for neighbor_category, neighbor_id in neighbors:
               neighbor_agent: "CovidAgent" = agents.get_agent(neighbor_id)
               if neighbor_agent.health_state == 1:
                   if random.uniform(0, 1) < self.scenario.infection_prob:
                       self.health_state = 1
                       break

One thing that might be confused: Why is there the ``category`` attribute?

The ``category`` attribute is to make sure that, when there are multiple groups of agents walking on the ``grid``,
the ``grid`` can distinguish them and work well.
So, the function ``set_category`` much be implemented in a class that inherits the ``GridAgent`` class.
For example, as shown in Line 35-36, when iterating through the ``neighbors`` returned by ``grid.get_neighbors``,
each neighbor is indexed with both ``category`` and the ``id`` of the agent.

For more details of the ``Grid`` and ``Spot`` modules,
please refer to the :ref:`API Reference` section.