//  [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_train_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.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.svd(layer_class_means.T)  # l_c_m.T is (d x C)

                # Calculate principal angles
                rank = dataset.num_classes
                S = torch.svd(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

                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.svd(weights_tx.detach())
                U_m,S_m,Vh_m = torch.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.svd(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

        return pd.DataFrame(out)