Example #1
0
    def __init__(
        self,
        *args,
        subrtn_sbi_class: Type[PosteriorEstimator],
        subrtn_sbi_sampling_hparam: Optional[dict] = None,
        **kwargs,
    ):
        """
        Constructor forwarding everything to the superclass

        :param subrtn_sbi_class: sbi algorithm calls for executing the LFI, e.g. SNPE-C
        :param subrtn_sbi_sampling_hparam: keyword arguments forwarded to sbi's `DirectPosterior.sample()` function like
                                          `sample_with_mcmc`, ect.
        """
        if not issubclass(subrtn_sbi_class, PosteriorEstimator):
            raise pyrado.TypeErr(
                msg=f"The given subrtn_sbi_class must be a subclass of PosteriorEstimator, but is {subrtn_sbi_class}!"
            )

        # Call SBIBase's constructor
        super().__init__(*args, num_checkpoints=3, init_checkpoint=-1, **kwargs)

        # Set the sampling parameters used by the sbi subroutine
        default_sampling_hparam = dict(
            mcmc_method="slice_np_vectorized",
            mcmc_parameters=dict(warmup_steps=50, num_chains=100, init_strategy="sir"),  # default: slice_np, 20
        )
        self.subrtn_sbi_sampling_hparam = merge_dicts([default_sampling_hparam, subrtn_sbi_sampling_hparam or dict()])

        # Create algorithm instance
        self._subrtn_sbi_class = subrtn_sbi_class
        self._initialize_subrtn_sbi(subrtn_sbi_class=self._subrtn_sbi_class)
Example #2
0
def draw_categorical(
    plot_type: str,
    ax: plt.Axes,
    data: Union[list, np.ndarray, to.Tensor, pd.DataFrame],
    x_label: Optional[Union[str, Sequence[str]]],
    y_label: Optional[str],
    vline_level: float = None,
    vline_label: str = "approx. solved",
    palette=None,
    title: str = None,
    show_legend: bool = True,
    legend_kwargs: dict = None,
    plot_kwargs: dict = None,
) -> plt.Figure:
    """
    Create a box or violin plot for a list of data arrays or a pandas DataFrame.
    The plot is neither shown nor saved.

    If you want to order the 4th element to the 2nd position in terms of colors use

    .. code-block:: python

        palette.insert(1, palette.pop(3))

    .. note::
        If you want to have a tight layout, it is best to pass axes of a figure with `tight_layout=True` or
        `constrained_layout=True`.

    :param plot_type: tye of categorical plot, pass box or violin
    :param ax: axis of the figure to plot on
    :param data: list of data sets to plot as separate boxes
    :param x_label: labels for the categories on the x-axis, if `data` is not given as a `DataFrame`
    :param y_label: label for the y-axis, pass `None` to set no label
    :param vline_level: if not `None` (default) add a vertical line at the given level
    :param vline_label: label for the vertical line
    :param palette: seaborn color palette, pass `None` to use the default palette
    :param show_legend: if `True` the legend is shown, useful when handling multiple subplots
    :param title: title displayed above the figure, set to None to suppress the title
    :param legend_kwargs: keyword arguments forwarded to pyplot's `legend()` function, e.g. `loc='best'`
    :param plot_kwargs: keyword arguments forwarded to seaborn's `boxplot()` or `violinplot()` function
    :return: handle to the resulting figure
    """
    plot_type = plot_type.lower()
    if plot_type not in ["box", "violin"]:
        raise pyrado.ValueErr(given=plot_type, eq_constraint="box or violin")
    if not isinstance(data, (list, to.Tensor, np.ndarray, pd.DataFrame)):
        raise pyrado.TypeErr(
            given=data,
            expected_type=[list, to.Tensor, np.ndarray, pd.DataFrame])

    # Set defaults which can be overwritten
    plot_kwargs = merge_dicts([dict(alpha=1),
                               plot_kwargs])  # by default no transparency
    alpha = plot_kwargs.pop(
        "alpha")  # can't pass the to the seaborn plotting functions
    legend_kwargs = dict() if legend_kwargs is None else legend_kwargs
    palette = sns.color_palette() if palette is None else palette

    # Preprocess
    if isinstance(data, pd.DataFrame):
        df = data
    else:
        if isinstance(data, list):
            data = np.array(data)
        elif isinstance(data, to.Tensor):
            data = data.detach().cpu().numpy()
        if x_label is not None and not len(x_label) == data.shape[1]:
            raise pyrado.ShapeErr(given=data, expected_match=x_label)
        df = pd.DataFrame(data, columns=x_label)

    if data.shape[0] < data.shape[1]:
        print_cbt(
            f"Less data samples {data.shape[0]} then data dimensions {data.shape[1]}",
            "y",
            bright=True)

    # Plot
    if plot_type == "box":
        ax = sns.boxplot(data=df, ax=ax, **plot_kwargs)

    elif plot_type == "violin":
        plot_kwargs = merge_dicts([
            dict(alpha=0.3, scale="count", inner="box", bw=0.3, cut=0),
            plot_kwargs
        ])
        ax = sns.violinplot(data=df, ax=ax, palette=palette, **plot_kwargs)

        # Plot larger circles for medians (need to memorize the limits)
        medians = df.median().to_numpy()
        left, right = ax.get_xlim()
        locs = ax.get_xticks()
        ax.scatter(locs,
                   medians,
                   marker="o",
                   s=30,
                   zorder=3,
                   color="white",
                   edgecolors="black")
        ax.set_xlim((left, right))

    # Postprocess
    if alpha < 1 and plot_type == "box":
        for patch in ax.artists:
            r, g, b, a = patch.get_facecolor()
            patch.set_facecolor((r, g, b, alpha))
    elif alpha < 1 and plot_type == "violin":
        for violin in ax.collections[::2]:
            violin.set_alpha(alpha)

    if vline_level is not None:
        # Add dashed line to mark a threshold
        ax.axhline(vline_level, c="k", ls="--", lw=1.0, label=vline_label)

    if x_label is None:
        ax.get_xaxis().set_ticks([])

    if y_label is not None:
        ax.set_ylabel(y_label)

    if show_legend:
        ax.legend(**legend_kwargs)

    if title is not None:
        ax.set_title(title)

    return plt.gcf()
Example #3
0
    def eval_posterior(
        posterior: DirectPosterior,
        data_real: to.Tensor,
        num_samples: int,
        calculate_log_probs: bool = True,
        normalize_posterior: bool = True,
        subrtn_sbi_sampling_hparam: Optional[dict] = None,
    ) -> Tuple[to.Tensor, Optional[to.Tensor]]:
        r"""
        Evaluates the posterior by computing parameter samples given observed data, its log probability
        and the simulated trajectory.

        :param posterior: posterior to evaluate, e.g. a normalizing flow, that samples domain parameters conditioned on
                          the provided data
        :param data_real: data from the real-world rollouts a.k.a. set of $x_o$ of shape
                          [num_iter, num_rollouts_per_iter * dim_feat]
        :param num_samples: number of samples to draw from the posterior
        :param calculate_log_probs: if `True`, the log-probabilities are computed, else `None` is returned
        :param normalize_posterior: if `True`, the normalization of the posterior density is enforced by sbi
        :param subrtn_sbi_sampling_hparam: keyword arguments forwarded to sbi's `DirectPosterior.sample()` function
        :return: domain parameters sampled form the posterior of shape [batch_size, num_samples, dim_domain_param], as
                 well as the log-probabilities of these domain parameters
        """
        if not isinstance(data_real, to.Tensor) or data_real.ndim != 2:
            raise pyrado.ShapeErr(
                msg=
                f"The data must be a 2-dim PyTorch tensor, but is of shape {data_real.shape}!"
            )

        batch_size, _ = data_real.shape

        # Sample domain parameters for all batches and stack them
        default_sampling_hparam = dict(
            mcmc_method="slice_np_vectorized",
            mcmc_parameters=dict(warmup_steps=50,
                                 num_chains=100,
                                 init_strategy="sir"),  # default: slice_np, 20
        )
        if subrtn_sbi_sampling_hparam is None:
            subrtn_sbi_sampling_hparam = dict()
        elif isinstance(subrtn_sbi_sampling_hparam, dict):
            subrtn_sbi_sampling_hparam = merge_dicts(
                [default_sampling_hparam, subrtn_sbi_sampling_hparam])
        else:
            raise pyrado.TypeErr(given=subrtn_sbi_sampling_hparam,
                                 expected_type=dict)

        # Sample domain parameters from the posterior
        domain_params = to.stack(
            [
                posterior.sample(
                    (num_samples, ), x=x_o, **subrtn_sbi_sampling_hparam)
                for x_o in data_real
            ],
            dim=0,
        )

        # Check shape
        if not domain_params.ndim == 3 or domain_params.shape[:2] != (
                batch_size, num_samples):
            raise pyrado.ShapeErr(
                msg=
                f"The sampled domain parameters must be a 3-dim tensor where the 1st dimension is {batch_size} and "
                f"the 2nd dimension is {num_samples}, but it is of shape {domain_params.shape}!"
            )

        # Compute the log probability if desired
        if calculate_log_probs:
            # Batch-wise computation and stacking
            with completion_context("Evaluating posterior", color="w"):
                log_probs = to.stack(
                    [
                        posterior.log_prob(
                            dp, x=x_o, norm_posterior=normalize_posterior)
                        for dp, x_o in zip(domain_params, data_real)
                    ],
                    dim=0,
                )

            # Check shape
            if log_probs.shape != (batch_size, num_samples):
                raise pyrado.ShapeErr(given=log_probs,
                                      expected_match=(batch_size, num_samples))

        else:
            log_probs = None

        return domain_params, log_probs
Example #4
0
def draw_surface(x: np.ndarray,
                 y: np.ndarray,
                 z_fcn: Union[Callable[[np.ndarray], np.ndarray], nn.Module],
                 x_label: str,
                 y_label: str,
                 z_label: str,
                 data_format='numpy',
                 fig: plt.Figure = None,
                 title: str = None,
                 plot_kwargs: dict = None) -> plt.Figure:
    """
    Render a 3-dim surface plot by providing a 1-dim array of x and y points and a function to calculate the z values.

    .. note::
        If you want to have a tight layout, it is best to pass axes of a figure with `tight_layout=True` or
        `constrained_layout=True`.

    :param x: x-axis 1-dim grid for constructing the 2-dim mesh grid
    :param y: y-axis 1-dim grid for constructing the 2-dim mesh grid
    :param z_fcn: function that defines the surface, takes a 2-dim vector as input
    :param x_label: label for the x-axis
    :param y_label: label for the y-axis
    :param z_label: label for the z-axis
    :param data_format: data format, 'numpy' or 'torch'
    :param fig: handle to figure, pass None to create a new figure
    :param title: title displayed above the figure, set to None to suppress the title
    :param plot_kwargs: keyword arguments forwarded to pyplot's `plot_surface()` function
    :return: handle to figure
    """
    plot_kwargs = merge_dicts(
        [dict(cmap=mpl.rcParams['image.cmap']), plot_kwargs])

    if fig is None:
        fig = plt.figure()
    ax = Axes3D(fig)

    # Create mesh grid matrices from x and y vectors
    xx, yy = np.meshgrid(x, y)

    # Check which version to use based on the output of the function
    if data_format == 'numpy':
        # Operate on ndarrays
        zz = np.array(
            [z_fcn(np.stack((x, y), axis=0)) for x, y in zip(xx, yy)])

    elif data_format == 'torch':
        # Operate on Tensors
        xx_tensor = to.from_numpy(xx)
        yy_tensor = to.from_numpy(yy)

        if hasattr(z_fcn, '_fcn'):
            # Passed function was wrapped (e.g. by functools)
            check_fcn = z_fcn._fcn
        else:
            check_fcn = z_fcn

        if isinstance(check_fcn, nn.Module):
            # Adapt for batch-first behavior of NN-based policies
            zz = to.stack([
                z_fcn(
                    to.stack((x, y), dim=1).view(-1, 1,
                                                 2).to(to.get_default_dtype()))
                for x, y in zip(xx_tensor, yy_tensor)
            ])
        else:
            zz = to.stack([
                z_fcn(
                    to.stack((x, y),
                             dim=1).transpose(0, 1).to(to.get_default_dtype()))
                for x, y in zip(xx_tensor, yy_tensor)
            ])
        zz = zz.squeeze().detach().cpu().numpy()

    else:
        raise pyrado.ValueErr(given=data_format,
                              eq_constraint="'numpy' or 'torch'")

    # Generate the plot
    ax.plot_surface(xx, yy, zz, **plot_kwargs)

    # Add labels
    ax.set_xlabel(x_label)
    ax.set_ylabel(y_label)
    ax.set_zlabel(z_label)
    if title is not None:
        ax.set_title(title)
    return fig
Example #5
0
def draw_curve(plot_type: str,
               ax: plt.Axes,
               data: pd.DataFrame,
               x_grid: Union[list, np.ndarray, to.Tensor],
               x_label: Optional[Union[str, Sequence[str]]] = None,
               y_label: Optional[str] = None,
               curve_label: Optional[str] = None,
               area_label: Optional[str] = None,
               vline_level: Optional[float] = None,
               vline_label: str = 'approx. solved',
               title: Optional[str] = None,
               show_legend: bool = True,
               legend_kwargs: dict = None,
               plot_kwargs: dict = None) -> plt.Figure:
    """
    Create a box or violin plot for a list of data arrays or a pandas DataFrame.
    The plot is neither shown nor saved.

    .. note::
        If you want to have a tight layout, it is best to pass axes of a figure with `tight_layout=True` or
        `constrained_layout=True`.

        If you want to order the 4th element to the 2nd position in terms of colors use
        .. code-block:: python

            palette.insert(1, palette.pop(3))

    :param plot_type: tye of categorical plot, pass box or violin
    :param ax: axis of the figure to plot on
    :param data: pandas DataFrame containing the columns `mean`, `std`, `min`, and `max` depending on the `plot_type`
    :param x_grid: values to plot the data over, e.g. time
    :param x_label: labels for the categories on the x-axis, if `data` is not given as a `DataFrame`
    :param y_label: label for the y-axis, pass `None` to set no label
    :param curve_label: label of the (1-dim) curve
    :param area_label: label of the (transparent) area
    :param vline_level: if not `None` (default) add a vertical line at the given level
    :param vline_label: label for the vertical line
    :param show_legend: if `True` the legend is shown, useful when handling multiple subplots
    :param title: title displayed above the figure, set to None to suppress the title
    :param legend_kwargs: keyword arguments forwarded to pyplot's `legend()` function, e.g. `loc='best'`
    :param plot_kwargs: keyword arguments forwarded to seaborn's `boxplot()` or `violinplot()` function
    :return: handle to the resulting figure
    """
    plot_type = plot_type.lower()
    if plot_type not in ['mean_std', 'min_mean_max']:
        raise pyrado.ValueErr(given=plot_type,
                              eq_constraint='mean_std or min_mean_max')
    if not isinstance(data, pd.DataFrame):
        raise pyrado.TypeErr(given=data, expected_type=pd.DataFrame)
    if x_label is not None and not isinstance(x_label, str):
        raise pyrado.TypeErr(given=x_label, expected_type=str)
    if y_label is not None and not isinstance(y_label, str):
        raise pyrado.TypeErr(given=y_label, expected_type=str)

    # Set defaults which can be overwritten by passing plot_kwargs
    plot_kwargs = merge_dicts([dict(alpha=0.3), plot_kwargs])
    legend_kwargs = dict() if legend_kwargs is None else legend_kwargs
    # palette = sns.color_palette() if palette is None else palette

    # Preprocess
    if isinstance(x_grid, list):
        x_grid = np.array(x_grid)
    elif isinstance(x_grid, to.Tensor):
        x_grid = x_grid.detach().cpu().numpy()

    # Plot
    if plot_type == 'mean_std':
        if not ('mean' in data.columns and 'std' in data.columns):
            raise pyrado.KeyErr(keys="'mean' and 'std'", container=data)
        num_stds = 2
        if area_label is None:
            area_label = rf'$\pm {num_stds}$ std'
        ax.fill_between(x_grid,
                        data['mean'] - num_stds * data['std'],
                        data['mean'] + num_stds * data['std'],
                        label=area_label,
                        **plot_kwargs)

    elif plot_type == 'min_mean_max':
        if not ('mean' in data.columns and 'min' in data.columns
                and 'max' in data.columns):
            raise pyrado.KeyErr(keys="'mean' and 'min' and 'max'",
                                container=data)
        if area_label is None:
            area_label = r'min \& max'
        ax.fill_between(x_grid,
                        data['min'],
                        data['max'],
                        label=area_label,
                        **plot_kwargs)

    # plot mean last for proper z-ordering
    plot_kwargs['alpha'] = 1
    ax.plot(x_grid, data['mean'], label=curve_label, **plot_kwargs)

    # Postprocess
    if vline_level is not None:
        # Add dashed line to mark a threshold
        ax.axhline(vline_level, c='k', ls='--', lw=1., label=vline_label)

    if x_label is None:
        ax.get_xaxis().set_ticks([])

    if y_label is not None:
        ax.set_ylabel(y_label)

    if show_legend:
        ax.legend(**legend_kwargs)

    if title is not None:
        ax.set_title(title)

    return plt.gcf()
Example #6
0
    def __init__(
        self,
        spec: EnvSpec,
        dim_data: int,
        hidden_size: int,
        num_recurrent_layers: int,
        output_size: int,
        recurrent_network_type: type = nn.RNN,
        only_last_output: bool = False,
        len_rollouts: int = None,
        output_nonlin: Callable = None,
        dropout: float = 0.0,
        init_param_kwargs: Optional[dict] = None,
        downsampling_factor: int = 1,
        state_mask_labels: Optional[Union[Tuple[Union[int, str]],
                                          List[Union[int, str]]]] = None,
        act_mask_labels: Optional[Union[Tuple[Union[int, str]],
                                        List[Union[int, str]]]] = None,
        use_cuda: bool = False,
        **recurrent_net_kwargs,
    ):
        """
        Constructor

        :param spec: environment specification
        :param dim_data: number of dimensions of one data sample, i.e. one time step. By default, this is the sum of the
                         state and action spaces' flat dimensions. This number is doubled if the embedding
                         target domain data.
        :param hidden_size: size of the hidden layers (all equal)
        :param num_recurrent_layers: number of equally sized hidden layers
        :param recurrent_network_type: PyTorch recurrent network class, e.g. `nn.RNN`, `nn.LSTM`, or `nn.GRU`
        :param output_size: size of the features at every time step, which are eventually reshaped into a vector
        :param only_last_output: if `True`, only the last output of the network is used as a feature for sbi, else
                                 there will be an output every `downsampling_factor` time steps. Moreover, if `True` the
                                 constructor does not need to know how long the rollouts are.
        :param len_rollouts: number of time steps per rollout without considering a potential downsampling later
                             (must be the same for all rollouts)
        :param output_nonlin: nonlinearity for output layer
        :param dropout: dropout probability, default = 0 deactivates dropout
        :param init_param_kwargs: additional keyword arguments for the policy parameter initialization
        :param recurrent_net_kwargs: any extra kwargs are passed to the recurrent net's constructor
        :param downsampling_factor: skip evey `downsampling_factor` time series sample, the downsampling is done in the
                                    base class before calling `summary_statistic()`
        :param state_mask_labels: list or tuple of integers or stings to select specific states from their space.
                                  By default `None` all states are passed to sbi.
        :param act_mask_labels: list or tuple of integers or stings to select specific actions from their space.
                                  By default `None` all actions are passed to sbi.
        :param use_cuda: `True` to move the policy to the GPU, `False` (default) to use the CPU
        """
        super().__init__(spec, dim_data, downsampling_factor,
                         state_mask_labels, act_mask_labels, use_cuda)

        # Check the time sequence length if necessary
        if not only_last_output:
            if not isinstance(len_rollouts, int) or len_rollouts < 0:
                raise pyrado.ValueErr(given=len_rollouts,
                                      eq_constraint="1 (int)")
            self._len_rollouts = len_rollouts // downsampling_factor
        else:
            self._len_rollouts = None  # use to signal only_last_output == True

        if recurrent_network_type == nn.RNN:
            recurrent_net_kwargs = merge_dicts(
                [dict(nonlinearity="tanh"), recurrent_net_kwargs])

        # Create the RNN layers
        self.rnn_layers = recurrent_network_type(
            input_size=dim_data if self.data_mask is None else
            np.count_nonzero(self.data_mask),  # includes actions
            hidden_size=hidden_size,
            num_layers=num_recurrent_layers,
            bias=True,
            batch_first=False,
            dropout=dropout,
            bidirectional=False,
            **recurrent_net_kwargs,
        )

        # Create the output layer
        self.output_layer = nn.Linear(hidden_size, output_size)
        self.output_nonlin = output_nonlin

        # Initialize parameter values
        init_param_kwargs = init_param_kwargs if init_param_kwargs is not None else dict(
        )
        self.init_param(None, **init_param_kwargs)
        self.to(self.device)

        # Detach the complete network, i.e. use it with the random initialization
        for p in self.parameters():
            p.requires_grad = False
Example #7
0
    def __init__(
        self,
        save_dir: pyrado.PathLike,
        inputs: to.Tensor,
        targets: to.Tensor,
        policy: Policy,
        max_iter: int,
        max_iter_no_improvement: int = 30,
        optim_class=optim.Adam,
        optim_hparam: dict = None,
        loss_fcn=nn.MSELoss(),
        batch_size: int = 256,
        ratio_train: float = 0.8,
        max_grad_norm: Optional[float] = None,
        lr_scheduler=None,
        lr_scheduler_hparam: Optional[dict] = None,
        logger: StepLogger = None,
    ):
        """
        Constructor

        :param save_dir: directory to save the snapshots i.e. the results in
        :param inputs: input data set, where the samples are along the first dimension
        :param targets: target data set, where the samples are along the first dimension
        :param policy: Pyrado policy (subclass of PyTorch's Module) to train
        :param max_iter: maximum number of iterations
        :param max_iter_no_improvement: if the performance on the validation set did not improve for this many
                                        iterations, the policy is considered to have converged, i.e. training stops
        :param optim_class: PyTorch optimizer class
        :param optim_hparam: hyper-parameters for the PyTorch optimizer
        :param loss_fcn: loss function for training, by default `torch.nn.MSELoss()`
        :param batch_size: number of samples per policy update batch
        :param ratio_train: ratio of the training samples w.r.t. the total sample count
        :param max_grad_norm: maximum L2 norm of the gradients for clipping, set to `None` to disable gradient clipping
        :param lr_scheduler: learning rate scheduler that does one step per epoch (pass through the whole data set)
        :param lr_scheduler_hparam: hyper-parameters for the learning rate scheduler
        :param logger: logger for every step of the algorithm, if `None` the default logger will be created
        """
        if not isinstance(inputs, to.Tensor):
            raise pyrado.TypeErr(given=inputs, expected_type=to.Tensor)
        if not isinstance(targets, to.Tensor):
            raise pyrado.TypeErr(given=targets, expected_type=to.Tensor)
        if not isinstance(ratio_train, float):
            raise pyrado.TypeErr(given=ratio_train, expected_type=float)
        if not (0 < ratio_train < 1):
            raise pyrado.ValueErr(given=ratio_train,
                                  g_constraint="0",
                                  l_constraint="1")

        # Call Algorithm's constructor
        super().__init__(save_dir, max_iter, policy, logger)

        # Construct the dataset (samples along rows)
        inputs = to.atleast_2d(
            inputs).T if inputs.ndimension() == 1 else inputs
        targets = to.atleast_2d(
            targets).T if targets.ndimension() == 1 else targets
        if inputs.shape[0] != targets.shape[0]:
            raise pyrado.ShapeErr(given=targets, expected_match=inputs)
        num_samples_all = inputs.shape[0]
        dataset = TensorDataset(
            inputs, targets)  # shared for training and validation loaders

        # Create training and validation loader
        idcs_all = to.randperm(num_samples_all)
        num_samples_trn = int(ratio_train * num_samples_all)
        num_samples_val = num_samples_all - num_samples_trn
        idcs_trn, idcs_val = idcs_all[:num_samples_trn], idcs_all[
            num_samples_trn:]
        self.loader_trn = DataLoader(
            dataset,
            batch_size=min(batch_size, num_samples_trn),
            drop_last=True,
            sampler=SubsetRandomSampler(idcs_trn),
        )
        self.loader_val = DataLoader(
            dataset,
            batch_size=min(batch_size, num_samples_val),
            drop_last=True,
            sampler=SubsetRandomSampler(idcs_val),
        )

        # Set defaults which can be overwritten by passing optim_hparam, and create the optimizer
        optim_hparam = merge_dicts(
            [dict(lr=5e-3, eps=1e-8, weight_decay=1e-4), optim_hparam])
        self.optim = optim_class([{
            "params": self._policy.parameters()
        }], **optim_hparam)

        self.batch_size = batch_size
        self.ratio_train = ratio_train
        self.loss_fcn = loss_fcn
        self.max_grad_norm = max_grad_norm
        self._lr_scheduler = lr_scheduler
        self._lr_scheduler_hparam = lr_scheduler_hparam
        if lr_scheduler is not None and lr_scheduler_hparam is not None:
            self._lr_scheduler = lr_scheduler(self.optim,
                                              **lr_scheduler_hparam)

        # Stopping criterion
        self._curr_loss_val = pyrado.inf
        self._best_loss_val = pyrado.inf
        self._cnt_iter_no_improvement = 0
        self._max_iter_no_improvement = max_iter_no_improvement

        self.stopping_criterion = self.stopping_criterion | CustomStoppingCriterion(
            self._custom_stopping_criterion)
def compute_traj_distance_metrics(
    states_real: np.ndarray,
    states_ml: np.ndarray,
    states_nom: np.ndarray,
    num_rollouts_real: int,
    normalize: bool = True,
    dtw_config: Optional[dict] = None,
    save: bool = True,
):
    """
    Compute the DTW distance and the RMSE for 2 trajectories w.r.t. a ground truth trajectory, and store it in a table.

    :param states_real: numpy array of states from the real system of shape [num_rollouts, len_time_series, dim_state]
    :param states_ml: numpy array of states from the most likely system [num_rollouts, len_time_series, dim_state]
    :param states_nom: numpy array of states from the nominal system [num_rollouts, len_time_series, dim_state]
    :param num_rollouts_real: number of rollouts
    :param normalize: it `True`, normalize all trajectories by the max. abs. values of the ground truth trajectory for
                      each dimension before computing the metrics
    :param dtw_config: dictionary with options for the `dtw.dtw()` command, e.g.
                       `dict(step_pattern=dtw.rabinerJuangStepPattern(6, "c"))`
    :param save: it `True`, save table as tex-file
    """
    # Configure the metric computations
    default_dtw_config = dict(open_end=True, step_pattern="symmetric2", distance_only=True)
    dtw_config = merge_dicts([default_dtw_config, dtw_config or dict()])

    # Iterate over all rollouts and compute the performance metrics
    table = []
    dtw_dist_ml_avg, dtw_dist_nom_avg, rmse_ml_avg, rmse_nom_avg = 0, 0, 0, 0
    for idx_r in range(num_rollouts_real):
        if normalize:
            # Normalize all trajectories by the max. abs. values of the ground truth trajectory for each dimension
            max_abs_state = np.max(np.abs(states_real[idx_r]), axis=0)
            states_real[idx_r] /= max_abs_state
            states_ml[idx_r] /= max_abs_state
            states_nom[idx_r] /= max_abs_state

        # DTW
        dtw_dist_ml = dtw.dtw(states_real[idx_r], states_ml[idx_r], **dtw_config).distance
        dtw_dist_nom = dtw.dtw(states_real[idx_r], states_nom[idx_r], **dtw_config).distance
        dtw_dist_ml_avg += dtw_dist_ml / num_rollouts_real
        dtw_dist_nom_avg += dtw_dist_nom / num_rollouts_real

        # RMSE averaged over the states
        rmse_ml = np.mean(rmse(states_real[idx_r], states_ml[idx_r], dim=0))
        rmse_nom = np.mean(rmse(states_real[idx_r], states_nom[idx_r], dim=0))
        rmse_ml_avg += rmse_ml / num_rollouts_real
        rmse_nom_avg += rmse_nom / num_rollouts_real

        table.append([idx_r, dtw_dist_ml, dtw_dist_nom, rmse_ml, rmse_nom])

    raw_data = np.array(table)

    # Add last row separately
    table.append(["average", dtw_dist_ml_avg, dtw_dist_nom_avg, rmse_ml_avg, rmse_nom_avg])

    # Print the tabulated data
    headers = ("rollout", "DTW dist. ml", "DTW dist. nom", "mean RMSE ml", "mean RMSE nom")
    print(tabulate(table, headers))

    if save:
        # Save the table for LaTeX
        table_latex_str = tabulate(table, headers, tablefmt="latex")
        str_iter = f"_iter_{args.iter}"
        str_round = f"_round_{args.round}"
        use_rec_str = "_use_rec" if args.use_rec else ""
        file_name = f"distance_metrics{str_iter}{str_round}{use_rec_str}"
        with open(osp.join(ex_dir, f"{file_name}.tex"), "w") as tab_file:
            print(table_latex_str, file=tab_file)

        with open(osp.join(ex_dir, f"{file_name}.npy"), "wb") as np_file:
            np.save(np_file, raw_data)
Example #9
0
def draw_curve_from_data(
    plot_type: str,
    ax: plt.Axes,
    data: Union[list, np.ndarray, to.Tensor, pd.DataFrame],
    x_grid: Union[list, np.ndarray, to.Tensor],
    ax_calc: int,
    x_label: Optional[Union[str, Sequence[str]]] = None,
    y_label: Optional[str] = None,
    curve_label: Optional[str] = None,
    area_label: Optional[str] = "",
    vline_level: Optional[float] = None,
    vline_label: str = "approx. solved",
    title: Optional[str] = None,
    show_legend: bool = True,
    cmp_kwargs: dict = None,
    plot_kwargs: dict = None,
    legend_kwargs: dict = None,
) -> plt.Figure:
    """
    Create a box or violin plot for a list of data arrays or a pandas DataFrame.
    The plot is neither shown nor saved.

    .. note::
        If you want to have a tight layout, it is best to pass axes of a figure with `tight_layout=True` or
        `constrained_layout=True`.

        If you want to order the 4th element to the 2nd position in terms of colors use
        .. code-block:: python

            palette.insert(1, palette.pop(3))

    :param plot_type: tye of 1-dim plot: `mean_std`, `min_mean_max`, or `ci_on_mean`
    :param ax: axis of the figure to plot on
    :param data: data to plot,me.g. a time series
    :param x_grid: values to plot the data over, e.g. time
    :param ax_calc: axis of the data array to calculate the mean, min and max, or std over
    :param x_label: labels for the categories on the x-axis, if `data` is not given as a `DataFrame`
    :param y_label: label for the y-axis, pass `None` to set no label
    :param curve_label: label of the (1-dim) curve, pass `None` for no label
    :param area_label: label of the (transparent) area, pass `None` for no label and "" for the default label
    :param vline_level: if not `None` (default) add a vertical line at the given level
    :param vline_label: label for the vertical line
    :param show_legend: if `True` the legend is shown, useful when handling multiple subplots
    :param title: title displayed above the figure, set to None to suppress the title
    :param cmp_kwargs: keyword arguments forwarded to functions computing the statistics of interest
    :param plot_kwargs: keyword arguments forwarded to the plotting` functions
    :param legend_kwargs: keyword arguments forwarded to pyplot's `legend()` function, e.g. `loc='best'`
    :return: handle to the resulting figure
    """
    plot_type = plot_type.lower()
    if plot_type not in ["mean_std", "min_mean_max", "ci_on_mean"]:
        raise pyrado.ValueErr(
            given=plot_type,
            eq_constraint="mean_std, min_mean_max, ci_on_mean")
    if not isinstance(data, (list, to.Tensor, np.ndarray, pd.DataFrame)):
        raise pyrado.TypeErr(
            given=data,
            expected_type=[list, to.Tensor, np.ndarray, pd.DataFrame])

    # Set defaults which can be overwritten by passing plot_kwargs
    cmp_kwargs = merge_dicts([
        dict(num_reps=1000,
             confidence_level=0.9,
             bias_correction=False,
             studentized=False), cmp_kwargs
    ])

    if isinstance(data, pd.DataFrame):
        data = data.to_numpy()
    elif isinstance(data, list):
        data = np.array(data)
    elif isinstance(data, to.Tensor):
        data = data.detach().cpu().numpy()

    # Extract features from data
    data_mean = np.mean(data, axis=ax_calc)
    df = pd.DataFrame()
    df = df.assign(mean=data_mean)
    if plot_type == "mean_std":
        data_std = np.std(data, axis=ax_calc)
        df = df.assign(std=data_std)

    elif plot_type == "min_mean_max":
        data_min = np.min(data, axis=ax_calc)
        data_max = np.max(data, axis=ax_calc)
        df = df.assign(min=data_min)
        df = df.assign(max=data_max)

    elif plot_type == "ci_on_mean":
        _, data_lo, data_up = bootstrap_ci(
            data.T if ax_calc == 1 else data,
            stat_fcn=np.mean,
            num_reps=cmp_kwargs["num_reps"],
            alpha=cmp_kwargs["confidence_level"],
            ci_sides=2,
            bias_correction=cmp_kwargs["bias_correction"],
            studentized=cmp_kwargs["studentized"],
            seed=0,
        )
        df = df.assign(ci_lo=data_lo)
        df = df.assign(ci_up=data_up)

    # Forward the actual plotting
    return draw_curve(
        plot_type,
        ax,
        df,
        x_grid,
        x_label,
        y_label,
        curve_label,
        area_label,
        vline_level,
        vline_label,
        title,
        show_legend,
        plot_kwargs,
        legend_kwargs,
    )