//  [python]
def test_loss_SV(Xs, ys, test_X, test_y):

    M = len(Xs)
    D = Xs[0].shape[1]
    orderings = list(permutations(range(M)))

    s_values = np.zeros(M)
    monte_carlo_s_values = np.zeros(M)

    # Monte-carlo : shuffling the ordering and taking the first K orderings
    np.random.shuffle(orderings)
    K = 4 # number of permutations to sample

    # Truncated monte-carlo: in addition to MC, truncate the marginal contribution calculation when loss is within tolerance
    train_X = torch.cat(Xs).reshape(-1, D)
    train_y = torch.cat(ys).reshape(-1, 1)

    pinv = torch.pinverse(train_X)
    test_loss = (torch.norm( test_X @ pinv @ train_y - test_y )) ** 2

    truncated_mc_s_values = np.zeros(M)

    # Bootstrap 10 percent samples for 10 times to calculate tol
    percentage = 0.1
    bootstrap_times = 10
    # data_size = np.sum([len(test_label) for test_label in test_labels])
    data_size = len(test_y)
    bootstrap_indices = torch.rand(bootstrap_times, data_size).argsort(1)[:,:int(data_size * percentage)]
    bootstrap_test_X = test_X[bootstrap_indices]
    bootstrap_test_y = test_y[bootstrap_indices]
    bootstrap_errors = (bootstrap_test_X @ pinv @ train_y - bootstrap_test_y)
    bootstrap_losses = np.apply_along_axis(lambda error: np.linalg.norm(error) ** 2, 0, bootstrap_errors) * (1/percentage)
    tol = np.std(bootstrap_losses)/np.sqrt(bootstrap_times) # This is to be determined through performance vairation in bootstrap samples (standard error)

    # Random initilaization, needed to compute marginal against empty set
    random_init = torch.normal(0, 1, (D, 1)).double()

    init_test_loss = (torch.norm( test_X @ random_init - test_y )) ** 2

    for ordering_count, ordering in enumerate(orderings):

        prefix_pinvs = []
        prefix_test_losses = []
        for position, i in enumerate(ordering):

            curr_indices = set(ordering[:position+1])

            curr_train_X = torch.cat([dataset for j, dataset in enumerate(Xs) if j in curr_indices ]).reshape(-1, D)
            curr_train_y = torch.cat([label for j, label in enumerate(ys)  if j in curr_indices ]).reshape(-1, 1)

            # curr_pinv_ = np.linalg.pinv(curr_train_X)
            curr_pinv = torch.pinverse(curr_train_X)
            curr_test_loss = (torch.norm(test_X @ curr_pinv @ curr_train_y - test_y ))**2

            if position == 0: # first in the ordering
                marginal = init_test_loss - curr_test_loss         
            else:
                marginal = prefix_test_losses[-1] - curr_test_loss
            s_values[i] += marginal

            prefix_pinvs.append(curr_pinv)
            prefix_test_losses.append(curr_test_loss)

            if ordering_count < K:
                monte_carlo_s_values[i] += marginal
                
                if np.abs(test_loss - curr_test_loss) > tol or ordering_count == 0:
                    truncated_mc_s_values[i] += marginal

    s_values /= factorial(M)
    monte_carlo_s_values /= K
    truncated_mc_s_values /= K
    # print('------Test loss Shapley value Statistics ------')
    # print("Test loss-based Shapley values:", s_values)
    # print("Test loss-based MC-Shapley values:", monte_carlo_s_values)
    # print("Test loss-based TMC-Shapley values:", truncated_mc_s_values)
    # print('-------------------------------------')

    return s_values, monte_carlo_s_values, truncated_mc_s_values