Source code for whynot.simulators.lotka_volterra.simulator

"""Simulation code for Lotka-Volterra model."""
import dataclasses

import numpy as np
from scipy.integrate import odeint

import whynot as wn
from whynot.dynamics import BaseConfig, BaseIntervention, BaseState


[docs]@dataclasses.dataclass class Config(BaseConfig): # pylint: disable-msg=too-few-public-methods """Parameterization of Lotka-Volterra dynamics. Examples -------- >>> # Configure the simulator so each caught rabbit creates 2 foxes >>> lotka_volterra.Config(fox_growth=0.5) """ # Dynamics parameters #: Natural growth rate of rabbits, when there's no foxes. rabbit_growth: float = 1.0 #: Natural death rate of rabbits, due to predation. rabbit_death: float = 0.1 #: Natural death rate of fox, when there's no rabbits. fox_death: float = 1.5 #: Factor describing how many caught rabbits create a new fox. fox_growth: float = 0.75 # Simulator book-keeping #: Start time of the simulator (in years). start_time: float = 0 #: End time of the simulator (in years). end_time: float = 100 #: Spacing of the evaluation grid delta_t: float = 1.0
[docs]@dataclasses.dataclass class State(BaseState): # pylint: disable-msg=too-few-public-methods """State of the Lotka-Volterra model.""" #: Number of rabbits. rabbits: float = 10.0 #: Number of foxes. foxes: float = 5.0
[docs]class Intervention(BaseIntervention): # pylint: disable-msg=too-few-public-methods """Parameterization of an intervention in the Lotka-Volterra model. An intervention changes a subset of the configuration variables in the specified year. The remaining variables are unchanged. Examples -------- >>> # Starting at time 25, set fox_growth to 0.4 (leave other variables unchanged) >>> Intervention(time=25, fox_growth=0.4) """
[docs] def __init__(self, time=30, **kwargs): """Specify an intervention in lotka_volterra. Parameters ---------- time: int Time of intervention in simulator dynamics. kwargs: dict Only valid keyword arguments are parameters of Config. """ super(Intervention, self).__init__(Config, time, **kwargs)
def dynamics(state, time, config, intervention=None): """Noisy Lotka-Volterra equations for 2 species. Equations from: https://scipy-cookbook.readthedocs.io/items/LoktaVolterraTutorial.html Parameters ---------- state: np.ndarray or tuple or list Instantenous system state time: float Time to evaluate the derivative config: whynot.simulators.lotka_volterra.Config Configuration parameters for the dynamics intervention: whynot.simulators.lotka_volterra.Intervention (Optional) Parameters specifying when/how to update the dynamics Returns ------- ds_dt: np.ndarray Derivative of the state with respect to time, evaluated at state and time. """ if intervention and time >= intervention.time: config = config.update(intervention) rabbits, foxes = state delta_rabbits = ( config.rabbit_growth * rabbits - config.rabbit_death * rabbits * foxes ) delta_foxes = ( -config.fox_death * foxes + config.fox_growth * config.rabbit_death * rabbits * foxes ) ds_dt = np.array([delta_rabbits, delta_foxes]) return ds_dt def simulate(initial_state, config, intervention=None, seed=None): """Simulate a run of the Lotka volterra model. Parameters ---------- initial_state: whynot.lotka_volterra.State config: whynot.lotka_volterra.Config Base parameters for the simulator run intervention: whynot.lotka_volterra.Intervention (Optional) Parameters specifying a change in dynamics seed: int Unused since the simulator is deterministic. Returns ------- run: whynot.dynamics.Run Simulator rollout """ # pylint: disable-msg=unused-argument t_eval = np.arange( config.start_time, config.end_time + config.delta_t, config.delta_t ) solution = odeint( dynamics, y0=dataclasses.astuple(initial_state), t=t_eval, args=(config, intervention), rtol=1e-4, atol=1e-4, ) states = [initial_state] + [State(*state) for state in solution[1:]] return wn.dynamics.Run(states=states, times=t_eval)