Source code for laser_measles.mixing.base
from abc import ABC
from abc import abstractmethod
import numpy as np
import polars as pl
from laser_core.migration import distance
[docs]
class BaseMixing(ABC):
"""
Base class for migration models.
"""
def __init__(self, scenario, params):
self._scenario = scenario
self.params = params
self._migration_matrix = None
self._mixing_matrix = None
@property
def scenario(self) -> pl.DataFrame:
return self._scenario
@scenario.setter
def scenario(self, scenario: pl.DataFrame) -> None:
self._scenario = scenario
@property
def migration_matrix(self) -> np.ndarray:
"""
Migration matrix computed from get_migration_matrix().
Returns:
np.ndarray: The migration matrix with lazy computation and caching.
"""
if self._migration_matrix is None:
self._migration_matrix = self.get_migration_matrix()
return self._migration_matrix
@property
def mixing_matrix(self) -> np.ndarray:
"""
Mixing matrix computed from get_mixing_matrix().
Returns:
np.ndarray: The mixing matrix with lazy computation and caching.
"""
if self._mixing_matrix is None:
self._mixing_matrix = self.get_mixing_matrix()
return self._mixing_matrix
@abstractmethod
def get_migration_matrix(self) -> np.ndarray:
"""
Initialize a migration/diffusion matrix for population mixing. The diffusion
matrix is a square matrix where each row represents the outbound migration
from a given patch to all other patches e.g., [i,j] = [from_i, to_j].
Convention is:
- Trips into node j: N_i @ M[i,j]
- Trips out of node i: np.sum(M[i,j] * N_i[:,np.newaxis], axis=1)
Returns:
np.ndarray: The diffusion matrix: (N, N)
"""
...
def trips_into(self) -> np.ndarray:
"""Returns the number of trips into each patch per tick."""
return self.scenario["pop"].to_numpy() @ self.migration_matrix
def trips_out_of(self) -> np.ndarray:
"""Returns the number of trips out of each patch per tick."""
return np.sum(self.migration_matrix * self.scenario["pop"].to_numpy()[:, np.newaxis], axis=1)
def get_distances(self) -> np.ndarray:
return distance(
self.scenario["lat"].to_numpy(),
self.scenario["lon"].to_numpy(),
self.scenario["lat"].to_numpy(),
self.scenario["lon"].to_numpy(),
)
def get_mixing_matrix(self) -> np.ndarray:
"""
Initialize a mixing matrix for population mixing.
The mixing matrix is a square matrix where each row represents the
mixing of a given patch to all other patches e.g., [i,j] = [from_i, to_j].
It also includes internal mixing within a patch.
"""
# copy the migration matrix
mixing_matrix = self.migration_matrix.copy() # reduce memory
# sum the probability of travel over all target patches (j) for fixed row (i)
row_sums = mixing_matrix.sum(axis=1)
if np.any(row_sums > 1):
raise ValueError("Migration matrix has row sums greater than 1")
# fill diagonals so that rows sum to 1
np.fill_diagonal(mixing_matrix, 1 - row_sums)
return mixing_matrix