//  [python]
def measure(self, wrapped_model: Models.ForwardHookedOutput, dataset: DatasetWrapper,
                shared_cache=None) -> pd.DataFrame:
        if shared_cache is None:
            shared_cache = SharedMeasurementVarsCache()

        wrapped_model.base_model.eval()
        device = next(wrapped_model.parameters()).device

        class_means, class_num_samples = shared_cache.get_test_class_means_nums(wrapped_model, dataset)

        out: List[Dict[str, Any]] = []
        for layer_name in tqdm.tqdm(class_means.keys(), desc='  ConvAngleBet.Subspc., calculating', leave=False):
            layer_obj = utils.rgetattr(wrapped_model.base_model, layer_name)
            if isinstance(layer_obj, torch.nn.Linear):  # For MLP models and classifier in VGG/ResNet
                # Get layer weights
                try:
                    weights = layer_obj.weight
                except AttributeError as e:
                    warnings.warn(
                        f"Module: {layer_name}, {layer_obj}\ndoes not have a 'weight' parameter. Make sure it is a fc-layer.")
                    continue
                weights = weights.detach()

                # Decompose layer weights
                U_w, S_w, Vh_w = torch.linalg.svd(
                    weights)  # weights == U_w @ "np.diag(S_w).reshape(weights.shape) (padded)" Vh_w

                # Decompose class means
                layer_class_means = class_means[layer_name]
                U_m, S_m, Vh_m = torch.linalg.svd(layer_class_means.T)  # l_c_m.T is (d x C)

                # Calculate principal angles
                rank = dataset.num_classes
                S = torch.linalg.svdvals(Vh_w[:rank] @ U_m[:, :rank]).to('cpu').numpy()
                avg_cos_angle = np.sum(S) / rank

                out.append({'value': avg_cos_angle.item(), 'rank': rank, 'layer_name': layer_name, 'layer_type': 'fc'})

            elif isinstance(layer_obj, torch.nn.Conv2d):  # For all convlayers in VGG (and others)
                # Get layer weights
                try:
                    weights = layer_obj.weight
                except AttributeError as e:
                    warnings.warn(
                        f"Module: {layer_name}, {layer_obj}\ndoes not have a 'weight' parameter. Make sure it is a fc-layer.")
                    continue

                """
                # Preprocess by reshaping etc.
                weights = weights.flatten(-2, -1).transpose(1, 0).cpu().detach().numpy()
                layer_class_means = class_means[layer_name].flatten(-2, -1).permute(1, 2, 0).detach().cpu().numpy()

                # Decompose weights
                S_w, (U_w, V_w, X_w) = tensorly.decomposition.tucker(weights, rank=rank)

                # Decompose class means
                S_m, (U_m, V_m, X_m) = tensorly.decomposition.tucker(layer_class_means, rank=rank)

                S = scipy.linalg.svdvals(
                    U_w.T @ U_m  # U_w[:, :rank].T @ U_m[:, :rank]
                )
                """

                weights_tx = weights.transpose(1, 0).flatten(start_dim=1)  # ch_in x (C * h * w)
                features_tx = class_means[layer_name].transpose(1, 0).flatten(start_dim=1)  # ch_in x (ch_out * h * w)
                U_w, S_w, Vh_w = torch.linalg.svd(weights_tx.detach())
                U_m, S_m, Vh_m = torch.linalg.svd(features_tx.detach())  # Note: Sm is rank (C * h * w)

                # rank = dataset.num_classes
                max_rank = min(U_w.shape[1], U_m.shape[1])
                ranks = list(range(1, min(100, max_rank + 1), 1)) + list(range(100, max_rank + 1, dataset.num_classes))
                for rank in ranks:
                    S = torch.linalg.svdvals(U_w[:, :rank].t() @ U_m[:, :rank]).to('cpu').detach().numpy()
                    avg_cos_angle = np.sum(S) / rank
                    out.append(
                        {'value': avg_cos_angle.item(), 'rank': rank, 'layer_name': layer_name, 'layer_type': 'conv'})

            else:
                continue

            # layer_rel_class_means = (class_means[layer_name] - global_mean[layer_name]).flatten(start_dim=1).to('cpu')
            # layer_classwise_cov_within = classwise_cov_within[layer_name].to('cpu')
            # layer_cov_between = torch.matmul(layer_rel_class_means.T, layer_rel_class_means) / dataset.num_classes

            # U, S, Vh = scipy.linalg.svd(Vh_w @ U_m)

            # S_sum = np.cumsum(S) / 10  # TODO(marius): Remove "/10" and update NCPlotter._plot_angleBetweenSubspaces (i.e. remove "*10")

            # for idx, (sigma, sigma_sum) in enumerate(zip(S, S_sum)):
            #     out.append({'value': sigma, 'sigma_idx': idx, 'layer_name': layer_name, 'sum': False})
            #     out.append({'value': sigma_sum, 'sigma_idx': idx, 'layer_name': layer_name, 'sum': True})

        return pd.DataFrame(out)