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()
Exemple #3
0
    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()