Source code for ugnn.utils.masks

import torch
import numpy as np


[docs] def non_zero_degree_mask(As, n, T): """ Create a data mask which removes nodes with zero connections at each time step. Args: As (list of np.ndarray): List of adjacency matrices for each time step. n (int): Number of nodes. T (int): Number of time steps. Returns: np.ndarray: A boolean mask indicating usable node/time pairs. """ data_mask = np.array([[True] * T for _ in range(n)]) for t in range(T): data_mask[np.where(np.sum(As[t], axis=0) == 0)[0], t] = False return data_mask
[docs] def mask_split(mask, split_props, seed=0, regime="transductive"): """ Split a mask into train/valid/calib/test based on the specified regime. Args: mask (np.ndarray): A boolean mask of shape (n, T), where n is the number of nodes and T is the number of time steps. split_props (list of float): Proportions for splitting the data into train/valid/calib/test. The proportions should sum to 1. seed (int, optional): Random seed for reproducibility. Defaults to 0. regime (str, optional): Splitting regime, either "transductive" or "semi-inductive". Defaults to "transductive". Returns: list of np.ndarray: A list of boolean masks for each split (train, valid, calib, test). Each mask has the same shape as the input mask. """ np.random.seed(seed) n, T = mask.shape if regime == "transductive": flat_mask = mask.T.reshape(-1) n_masks = np.sum(flat_mask) flat_mask_idx = np.where(flat_mask)[0] np.random.shuffle(flat_mask_idx) split_ns = np.cumsum([round(n_masks * prop) for prop in split_props[:-1]]) split_idx = np.split(flat_mask_idx, split_ns) elif regime == "semi-inductive": T_trunc = np.where( np.cumsum(np.sum(mask, axis=0) / np.sum(mask)) >= 1 - split_props[-1] )[0][0] flat_mask_start = mask[:, :T_trunc].T.reshape(-1) flat_mask_end = mask[:, T_trunc:].T.reshape(-1) n_masks_start = np.sum(flat_mask_start) flat_mask_start_idx = np.where(flat_mask_start)[0] np.random.shuffle(flat_mask_start_idx) split_props_start = split_props[:-1] / np.sum(split_props[:-1]) split_ns = np.cumsum( [round(n_masks_start * prop) for prop in split_props_start[:-1]] ) split_idx = np.split(flat_mask_start_idx, split_ns) split_idx.append(n * T_trunc + np.where(flat_mask_end)[0]) elif regime == "temporal transductive": T_trunc = np.where( np.cumsum(np.sum(mask, axis=0) / np.sum(mask)) >= 1 - split_props[-1] )[0][0] flat_mask_start = mask[:, :T_trunc].T.reshape(-1) flat_mask_end = mask[:, T_trunc:].T.reshape(-1) n_masks_start = np.sum(flat_mask_start) n_masks_end = np.sum(flat_mask_end) flat_mask_start_idx = np.where(flat_mask_start)[0] np.random.shuffle(flat_mask_start_idx) split_props_start = split_props[:-2] / np.sum(split_props[:-2]) split_ns = np.cumsum( [round(n_masks_start * prop) for prop in split_props_start[:-1]] ) split_idx = np.split(flat_mask_start_idx, split_ns) flat_mask_end_idx = np.where(flat_mask_end)[0] np.random.shuffle(flat_mask_end_idx) split_props_end = split_props[-2:] / np.sum(split_props[-2:]) split_ns = np.cumsum( [round(n_masks_end * prop) for prop in split_props_end[:-1]] ) split_idx.append(n * T_trunc + np.split(flat_mask_end_idx, split_ns)[0]) split_idx.append(n * T_trunc + np.split(flat_mask_end_idx, split_ns)[1]) split_masks = np.array([[False] * n * T for _ in range(len(split_props))]) for i in range(len(split_props)): split_masks[i, split_idx[i]] = True return split_masks
[docs] def mask_mix(mask_1, mask_2, seed=0): np.random.seed(seed) n = len(mask_1) n1 = np.sum(mask_1) mask_idx = np.where(mask_1 + mask_2)[0] np.random.shuffle(mask_idx) split_idx = np.split(mask_idx, [n1]) split_masks = np.array([[False] * n for _ in range(2)]) for i in range(2): split_masks[i, split_idx[i]] = True return split_masks
[docs] def pad_unfolded_mask(mask, n): """ Padding required due to the n extra anchor nodes introduced in the unfolded representation. These nodes are not included in training. Args: mask (np.ndarray): The original mask. n (int): The number of nodes. Returns: np.ndarray: The padded mask. """ return torch.tensor(np.concatenate((np.array([False] * n), mask)))