Source code for macrostat.core.behavior

"""
Behavior classes for the MacroStat model.
"""

__author__ = ["Karl Naumann-Woleske"]
__credits__ = ["Karl Naumann-Woleske"]
__license__ = "MIT"
__maintainer__ = ["Karl Naumann-Woleske"]

import logging

import torch

from macrostat.core.parameters import Parameters
from macrostat.core.scenarios import Scenarios
from macrostat.core.variables import Variables

logger = logging.getLogger(__name__)


[docs] class Behavior(torch.nn.Module): """Base class for the behavior of the MacroStat model."""
[docs] def __init__( self, parameters: Parameters, scenarios: Scenarios, variables: Variables, record: bool = False, scenario: int = 0, differentiable: bool = False, debug: bool = False, ): """Initialize the behavior of the MacroStat model. Parameters ---------- parameters: macrostat.core.parameters.Parameters The parameters of the model. scenarios: macrostat.core.scenarios.Scenarios The scenarios of the model. variables: macrostat.core.variables.Variables The variables of the model. record: bool Whether to record the model output as a whole timeseries, or just the state variables (less memory-intensive). scenario: int The scenario to use for the model run. debug: bool Whether to print debug information. """ # Initialize the parent class super().__init__() # Initialize the parameters self.params = parameters.to_nn_parameters() self.hyper = parameters.hyper # Initialize the scenarios self.scenarios = scenarios.to_nn_parameters(scenario=scenario) self.scenarioID = scenario # Initialize the variables self.variables = variables # Settings self.differentiable = differentiable self.record = record self.debug = debug
[docs] def forward(self): """Forward pass of the behavior. This should include the model's main loop, and is implemented as a placeholder. The idea is for users to implement an initialize() and step() function, which will be called by the forward() function. If there are additional steps necessary, users may wish to overwrite this function. """ # Set the seed torch.manual_seed(self.hyper["seed"]) # Initialize the output tensors kwargs = { "dtype": torch.float32, "requires_grad": self.hyper["requires_grad"], "device": self.hyper["device"], } self.state, self.history = self.variables.initialize_tensors( t=self.hyper["timesteps"], **kwargs ) # Initialize the model logger.debug( f"Initializing model (t=0...{self.hyper['timesteps_initialization']})" ) self.initialize() if self.record: for t in range(self.hyper["timesteps_initialization"]): self.variables.record_state(t, self.state) for t in range(self.hyper["timesteps_initialization"]): self.history = self.variables.update_history(self.state) self.prior = self.state self.state = self.variables.new_state() # Run the model for the remaining timesteps logger.debug( f"Simulating model (t={self.hyper['timesteps_initialization'] + 1}...{self.hyper['timesteps']})" ) for t in range( self.hyper["timesteps_initialization"] + 1, self.hyper["timesteps"] ): # Get scenario series for this point in time idx = torch.where( torch.arange(self.hyper["timesteps"]) == t, torch.ones(1), torch.zeros(1), ) scenario = {k: idx @ v for k, v in self.scenarios.items()} self.step(t, scenario) self.variables.record_state(t, self.state) self.history = self.variables.update_history(self.state) self.prior = self.state self.state = self.variables.new_state() return None
[docs] def initialize(self): """Initialize the behavior. This should include the model's initialization steps, and set all of the necessary state variables. They only need to be set for one period, and will then be copied to the history and prior to be used in the step function. """ raise NotImplementedError("Behavior.initialize() to be implemented by model")
[docs] def step(self, t: int, scenario: dict): """Step function of the behavior. This should include the model's main loop. Parameters ---------- t: int The current timestep. scenario: dict The scenario information for the current timestep. """ raise NotImplementedError("Behavior.step() to be implemented by model")
# Some Differentiable PyTorch Alternatives
[docs] def diffwhere(self, condition, x1, x2): """Where condition that is differentiable with respect to the condition. Requires: self.hyper['diffwhere'] = True self.hyper['sigmoid_constant'] as a large number Note: For non-NaN/inf, where(x > eps, z, y) is (x - eps > 0) * (z - y) + y, so we can use the sigmoid function to approximate the where function. Parameters ---------- condition : torch.Tensor Condition to be evaluated expressed as x - eps x1 : torch.Tensor Value to be returned if condition is True x2 : torch.Tensor Value to be returned if condition is False """ if self.hyper["diffwhere"]: sig = torch.sigmoid(torch.mul(condition, self.hyper["sigmoid_constant"])) out = torch.add(torch.mul(sig, torch.sub(x1, x2)), x2) else: out = torch.where(condition > 0, x1, x2) return out
[docs] def tanhmask(self, x): """Convert a variable into 0 (x<0) and 1 (x>0) Requires: self.hyper['tanh_constant'] as a large number Parameters ---------- x: torch.Tensor The variable to be converted. """ kwg = {"dtype": torch.float64, "requires_grad": True} return torch.div( torch.add( torch.ones(x.size(), **kwg), torch.tanh(torch.mul(x, self.hyper["tanh_constant"])), ), torch.tensor(2.0, **kwg), )
[docs] def diffmin(self, x1, x2): """Smooth approximation to the minimum B: https://mathoverflow.net/questions/35191/a-differentiable-approximation-to-the-minimum-function Requires: self.hyper['min_constant'] as a large number Parameters ---------- x1: torch.Tensor The first variable to be compared. x2: torch.Tensor The second variable to be compared. """ r = self.hyper["min_constant"] pt1 = torch.exp(torch.mul(x1, -1 * r)) pt2 = torch.exp(torch.mul(x2, -1 * r)) return torch.mul(-1 / r, torch.log(torch.add(pt1, pt2)))
[docs] def diffmax(self, x1, x2): """Smooth approximation to the minimum B: https://mathoverflow.net/questions/35191/a-differentiable-approximation-to-the-minimum-function Requires: self.hyper['max_constant'] as a large number Parameters ---------- x1: torch.Tensor The first variable to be compared. x2: torch.Tensor The second variable to be compared. """ r = self.hyper["max_constant"] pt1 = torch.exp(torch.mul(x1, r)) pt2 = torch.exp(torch.mul(x2, r)) return torch.mul(1 / r, torch.log(torch.add(pt1, pt2)))
[docs] def diffmin_v(self, x): """Smooth approximation to the minimum. See diffmin Parameters ---------- x: torch.Tensor The variable to be converted. Requires: self.hyper['min_constant'] as a large number """ r = self.hyper["min_constant"] temp = torch.exp(torch.mul(x, -1 * r)) return torch.mul(-1 / r, torch.log(torch.sum(temp)))
[docs] def diffmax_v(self, x): """Smooth approximation to the maximum for a tensor. See diffmax Requires: self.hyper['max_constant'] as a large number Parameters ---------- x: torch.Tensor The variable to be converted. """ r = self.hyper["max_constant"] temp = torch.exp(torch.mul(x, r)) return torch.mul(1 / r, torch.log(torch.sum(temp)))
if __name__ == "__main__": pass