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 rvs(self, n_scenarios: int, n_steps: int, random_state: RandomState = None) -> np.ndarray: """ Returns the array of random variates used to generate a batch of scenarios with shape (n_scenarios, n_steps, self.dim). If dim == 1, then the third dimension will be squeezed, so the returned array will have shape (n_scenarios, n_steps). Parameters ---------- 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 random_state : Union[int, np.random.RandomState, None], either an integer seed or a numpy RandomState object directly, if reproducibility is desired Returns ------- rvs : np.ndarray, an array of the random variates used to generate scenarios """ rvs = np.zeros(shape=(n_scenarios, n_steps, self.dim)) for i in range(n_steps): random_state = check_random_state(random_state) rvs[:, i, :] = self.dW.rvs(size=(n_scenarios, self.dim), random_state=random_state) return rvs.squeeze()
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()