def __init__( self, mu: Union[List[float], List[int], np.ndarray], sigma: Union[List[float], List[int], np.ndarray], correlation: Union[List[float], np.ndarray], ) -> None: super().__init__(dim=len(mu)) self.mu = to_array(mu) self.sigma = to_array(sigma) self.correlation = to_array(correlation)
def step(self, x0: Array, dt: float, random_state: RandomState = None) -> np.ndarray: """ Applies the stochastic process to an array of initial values using the Euler Discretization method """ # generate an array of independent draws from the dW distribution (defaults to a # normal distribution.) In the general case, we can use matrix multiplication to # combine the random draws with the StochasticProcess's standard deviation. This # means that we can handle both single-dimensional and multi-dimensional # stochastic processes with a single abstract base class. For joint stochastic # processes, the standard deviation is a n x n matrix, where n is the dimension # of the process, so we effectively convert the independent random draws into # correlated random draws. x0 = to_array(x0) rvs = self.dW.rvs(size=x0.shape, random_state=check_random_state(random_state)) if self.dim == 1: dx = rvs * self.standard_deviation(x0=x0, dt=dt) else: if x0.ndim == 1: # single sample from a joint process dx = rvs @ self.standard_deviation(x0=x0, dt=dt).transpose( 1, 0) else: # multiple samples from a joint process # we have rvs as a (samples, dimension) array and standard deviation as # a (samples, dimension, dimension) array. We want to matrix multiply # the rvs (dimension) index with the transposed (dimension, dimension) # standard deviation for each sample to get a (samples, dimension) array dx = np.einsum("ab,acb->ac", rvs, self.standard_deviation(x0=x0, dt=dt)) return self.apply(self.expectation(x0=x0, dt=dt), dx)
def scenarios( # pylint: disable=too-many-arguments self, x0: Array, dt: float, n_scenarios: int, n_steps: int, random_state: RandomState = None, ) -> np.ndarray: """ Returns a recursively-generated scenario, starting with initial values/array, x0 and continuing by steps with length dt for a given number of steps Parameters ---------- x0 : Array, either a single start value or array of start values if applicable dt : float, the length between steps n_scenarios : int, the number of scenarios to generate, e.g. 1000 n_steps : int, the number of steps in the scenario, e.g. 52. In combination with dt, this determines the scope of the scenario, e.g. dt=1/12 and n_step=360 will produce 360 monthly time steps, i.e. a 30-year monthly projection random_state : Union[int, np.random.RandomState, None], either an integer seed or a numpy RandomState object directly, if reproducibility is desired Returns ------- samples : np.ndarray with shape (n_scenarios, n_steps + 1) for a one-dimensional stochastic process, or (n_scenarios, n_steps + 1, dim) for a two-dimensional stochastic process, where the first timestep of each scenario is x0 """ # set a function-level pseudo random number generator, either by creating a new # RandomState object with the integer argument, or using the RandomState object # directly passed in the arguments. prng = check_random_state(random_state) x0 = to_array( x0) # ensure we're working with a numpy array before proceeding # create a shell array that we will populate with values once they are available # this is generally faster than appending subsequent steps to an array each time # we'll generate a 2d array if this process has dim == 1; otherwise it will be 3 shape: Tuple[int, ...] = (n_scenarios, n_steps + 1) if self.dim > 1: shape = (shape[0], shape[1], self.dim) samples = np.empty(shape=shape, dtype=np.float64) try: # can we broadcast the x0 array into the number of scenarios we want? samples[:, 0] = x0 except ValueError: raise ValueError( f"Could not broadcast the input array, with shape {x0.shape}, into " f"the scenario output array, with shape {samples.shape}") # then we iterate through scenarios along the timesteps dimension for i in range(n_steps): samples[:, i + 1] = self.step(x0=samples[:, i], dt=dt, random_state=prng) return samples
def step(self, x0: Array, dt: float, random_state: RandomState = None) -> np.ndarray: """ Applies the stochastic process to an array of initial values using the Euler Discretization method """ # generate an array of independent draws from the dW distribution (defaults to a # normal distribution.) In the general case, we can use matrix multiplication to # combine the random draws with the StochasticProcess's standard deviation. This # means that we can handle both single-dimensional and multi-dimensional # stochastic processes with a single abstract base class. For joint stochastic # processes, the standard deviation is a n x n matrix, where n is the dimension # of the process, so we effectively convert the independent random draws into # correlated random draws. x0 = to_array(x0) rvs = self.dW.rvs(size=x0.shape, random_state=check_random_state(random_state)) dx = rvs @ self.standard_deviation(x0=x0, dt=dt).T return self.apply(self.expectation(x0=x0, dt=dt), dx)
def scenario(self, x0: Array, dt: float, n_step: int, random_state: RandomState = None) -> np.ndarray: """ Returns a recursively-generated scenario, starting with initial values/array, x0 and continuing by steps with length dt for a given number of steps Parameters ---------- x0 : Array, either a single start value or array of start values if applicable dt : float, the length between steps n_step : int, the number of steps in the scenario, e.g. 360. In combination with dt, this determines the scope of the scenario, e.g. dt=1/12 and n_step=360 will produce 360 monthly time steps, i.e. a 30-year monthly projection. random_state : Union[int, np.random.RandomState, None], either an integer seed or a numpy RandomState object directly, if reproducibility is desired Returns ------- samples : np.ndarray with shape (n_step + 1, dim), where samples[0] is the input array, x0, and the subsequent indices are the steps of the scenario """ # set a function-level pseudo random number generator, either by creating a new # RandomState object with the integer argument, or using the RandomState object # directly passed in the arguments. prng = check_random_state(random_state) # create a shell array that we will populate with values once they are available # this is generally faster than appending subsequent steps to an array each time samples = np.empty(shape=(n_step + 1, self.dim), dtype=np.float64) samples[0] = to_array(x0) for i in range(n_step): samples[i + 1] = self.step(x0=samples[i], dt=dt, random_state=prng) # squeeze the final dimension of the array if dim == 1 return samples.squeeze()
def _diffusion(self, x0: np.ndarray) -> np.ndarray: return to_array(self.sigma)
def _drift(self, x0: np.ndarray) -> np.ndarray: return to_array(self.mu - self.dividend - 0.5 * self.sigma * self.sigma)
def logpdf(self, x0: Array, xt: Array, dt: float) -> np.ndarray: """ Returns the log-probability of moving from x0 to x1 starting at time t and moving to time t + dt """ return self.transition_distribution(x0=to_array(x0), dt=dt).logpdf(xt)
def expectation(self, x0: Array, dt: float) -> np.ndarray: """ Returns the expected value of the stochastic process using the Euler Discretization method """ return self.apply(to_array(x0), self.drift(x0=x0) * dt)
def diffusion(self, x0: Array) -> np.ndarray: """Returns the diffusion component of the stochastic process""" return self._diffusion(x0=to_array(x0))
def drift(self, x0: Array) -> np.ndarray: """Returns the drift component of the stochastic process""" return self._drift(x0=to_array(x0))
def apply(self, x0: Array, dx: Array) -> np.ndarray: """Returns a new array of x-values, given a starting array and change vector""" return self._apply(x0=to_array(x0), dx=to_array(dx))
def nnlf(self, x0: Array, xt: Array, dt: float) -> np.ndarray: """ Returns the negative log-likelihood function of moving from x0 to x1 starting at time t and moving to time t + dt """ return -np.sum(self.logpdf(x0=to_array(x0), xt=to_array(xt), dt=dt))
def _diffusion(self, x0: np.ndarray) -> np.ndarray: # diffusion of an Ornstein-Uhlenbeck process does not depend on x0 return to_array(self.sigma)
def _diffusion(self, x0: np.ndarray) -> np.ndarray: # diffusion of a Wiener process does not depend on x0 return to_array(self.sigma)
def _drift(self, x0: np.ndarray) -> np.ndarray: # drift of a Wiener process does not depend on x0 return to_array(self.mu)