hutaobo's picture
Upload cleaned DST-GNN manuscript code
2936fa5 verified
Raw
History Blame Contribute Delete
5.39 kB
"""Data loading utilities for temporal COSTE/SSS graphs."""
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
import numpy as np
import pandas as pd
import torch
from torch_geometric.data import Data
GROUP_TO_STAGE = {"HD": "T1", "LA": "T2", "MA": "T3"}
STAGE_ORDER = ("T1", "T2", "T3")
STAGE_LABELS = {"T1": "healthy", "T2": "less_affected", "T3": "more_affected"}
REQUIRED_COLUMNS = ("row", "column", "value", "sample", "group")
@dataclass
class TemporalGraphBundle:
"""Container holding stage graphs and related metadata."""
node_names: list[str]
sample_stage_map: dict[str, str]
sample_matrices: dict[str, np.ndarray]
stage_matrices: dict[str, np.ndarray]
stage_graphs: list[Data]
sample_counts: dict[str, int]
def load_relationship_table(csv_path: str | Path) -> pd.DataFrame:
"""Load and validate the flattened COSTE/SSS relationship table."""
csv_path = Path(csv_path)
df = pd.read_csv(csv_path)
missing_columns = [column for column in REQUIRED_COLUMNS if column not in df.columns]
if missing_columns:
raise ValueError(f"Missing required columns: {missing_columns}")
unknown_groups = sorted(set(df["group"]) - set(GROUP_TO_STAGE))
if unknown_groups:
raise ValueError(f"Unexpected group labels: {unknown_groups}")
return df.copy()
def build_sample_matrices(
df: pd.DataFrame,
node_names: list[str] | None = None,
) -> tuple[list[str], dict[str, str], dict[str, np.ndarray]]:
"""Build full per-sample directed SSS matrices.
Missing off-diagonal pairs are filled with 1.0, matching the manuscript's
treatment of absent spatial associations. Diagonal values are set to 0.0.
"""
if node_names is None:
node_names = sorted(set(df["row"]).union(set(df["column"])))
node_to_idx = {name: idx for idx, name in enumerate(node_names)}
num_nodes = len(node_names)
sample_stage_map: dict[str, str] = {}
sample_matrices: dict[str, np.ndarray] = {}
for sample, sample_df in df.groupby("sample", sort=True):
stage = GROUP_TO_STAGE[str(sample_df["group"].iloc[0])]
matrix = np.ones((num_nodes, num_nodes), dtype=np.float32)
np.fill_diagonal(matrix, 0.0)
for row in sample_df.itertuples(index=False):
source = node_to_idx[str(row.row)]
target = node_to_idx[str(row.column)]
matrix[source, target] = float(row.value)
sample_stage_map[str(sample)] = stage
sample_matrices[str(sample)] = matrix
return node_names, sample_stage_map, sample_matrices
def aggregate_stage_matrices(
sample_stage_map: dict[str, str],
sample_matrices: dict[str, np.ndarray],
) -> tuple[dict[str, np.ndarray], dict[str, int]]:
"""Average per-sample matrices within each temporal stage."""
grouped: dict[str, list[np.ndarray]] = {stage: [] for stage in STAGE_ORDER}
for sample, matrix in sample_matrices.items():
grouped[sample_stage_map[sample]].append(matrix)
stage_matrices: dict[str, np.ndarray] = {}
sample_counts: dict[str, int] = {}
for stage in STAGE_ORDER:
matrices = grouped[stage]
if not matrices:
raise ValueError(f"No sample matrices found for stage {stage}")
stage_matrices[stage] = np.mean(np.stack(matrices, axis=0), axis=0).astype(np.float32)
sample_counts[stage] = len(matrices)
return stage_matrices, sample_counts
def matrix_to_graph(stage: str, matrix: np.ndarray, node_names: list[str]) -> Data:
"""Convert a directed SSS matrix to a PyG Data object.
Message passing uses an affinity transform of ``1 - SSS`` so that closer
spatial associations produce larger edge weights.
"""
num_nodes = matrix.shape[0]
sss_matrix = np.clip(matrix, 0.0, 1.0).astype(np.float32)
affinity = 1.0 - sss_matrix
np.fill_diagonal(affinity, 0.0)
source_nodes, target_nodes = np.where(affinity > 0)
edge_index = torch.tensor(
np.vstack([source_nodes, target_nodes]),
dtype=torch.long,
)
edge_weight = torch.tensor(affinity[source_nodes, target_nodes], dtype=torch.float32)
x = torch.eye(num_nodes, dtype=torch.float32)
graph = Data(
x=x,
edge_index=edge_index,
edge_weight=edge_weight,
y=torch.from_numpy(sss_matrix),
)
graph.stage_name = stage
graph.stage_index = torch.tensor([STAGE_ORDER.index(stage)], dtype=torch.long)
graph.node_names = node_names
graph.sss_matrix = torch.from_numpy(sss_matrix)
graph.affinity_matrix = torch.from_numpy(affinity.astype(np.float32))
return graph
def build_temporal_graphs(csv_path: str | Path) -> TemporalGraphBundle:
"""Build stage graphs from the flattened COSTE/SSS CSV."""
df = load_relationship_table(csv_path)
node_names, sample_stage_map, sample_matrices = build_sample_matrices(df)
stage_matrices, sample_counts = aggregate_stage_matrices(sample_stage_map, sample_matrices)
stage_graphs = [matrix_to_graph(stage, stage_matrices[stage], node_names) for stage in STAGE_ORDER]
return TemporalGraphBundle(
node_names=node_names,
sample_stage_map=sample_stage_map,
sample_matrices=sample_matrices,
stage_matrices=stage_matrices,
stage_graphs=stage_graphs,
sample_counts=sample_counts,
)