TGraphX vs Hand-Rolled Graph Code: Maintainability Trade-Offs
Every research project starts with the question: use a library or write it
yourself? For graph neural networks, this question has a nuanced answer.
Hand-rolled code gives you maximum control and no dependency risk; library
code gives you validation, conventions, and infrastructure you don't have to
build. Neither is universally correct.
What Hand-Rolled Code Looks Like
A minimal hand-rolled GNN for node classification:
import torch
import torch.nn as nn
class HandRolledSAGE(nn.Module):
def __init__(self, in_dim, out_dim):
super().__init__()
self.W = nn.Linear(in_dim * 2, out_dim)
def forward(self, node_feats, edge_index):
src, dst = edge_index[0], edge_index[1]
msgs = node_feats[src] # [E, d]
agg = torch.zeros_like(node_feats) # [N, d]
agg.index_add_(0, dst, msgs) # aggregate to target nodes
combined = torch.cat([node_feats, agg], dim=1) # [N, 2d]
return self.W(combined) # [N, out_dim]
This is ~10 lines. It works. It runs on GPU if you move node_feats and
edge_index to CUDA. It has zero dependencies beyond PyTorch.
What the Same Code Looks Like with TGraphX
from tgraphx.layers import TensorGraphSAGELayer
from tgraphx import Graph
from tgraphx.performance import recommended_device
device = recommended_device()
layer = TensorGraphSAGELayer(in_dim, out_dim).to(device)
# Graph validates shapes and devices at construction
g = Graph(
node_features=node_feats,
edge_index=edge_index, # [2, E]
edge_features=torch.zeros(edge_index.shape[1], 1),
node_labels=labels,
).to(device)
out = layer(g)
More lines for setup, but the Graph object carries validation. The question
is whether that validation is worth the cost.
Trade-Off Table
| Dimension | Hand-rolled | TGraphX |
|---|---|---|
| Lines of code (minimal) | Fewer for initial version | More for initial version |
| Dependency risk | None | Beta library may change API |
| Shape validation | None (must add yourself) | Built in at construction |
| Device management | Manual for each tensor | g.to(device) moves all tensors |
| Aggregation correctness | Your responsibility | Tested by library tests |
| Custom aggregation logic | Full control | Limited to provided layers |
| Batching multiple graphs | Implement yourself | GraphBatch.from_graphs() |
| Experiment logging | Implement yourself | CSVLogger, write_* functions |
| Reproducibility helpers | Implement yourself | set_seed, env_report |
| Documentation trail | Depends on your discipline | Write functions create artifacts |
| Long-term maintenance | You own the code entirely | Library upgrades may break API |
| Debuggability | All code visible to you | Layer internals opaque |
When Hand-Rolled Code Is Better
Custom aggregation logic: If your research requires a novel aggregation
scheme not in TGraphX's layer set, hand-rolled code is the right choice.
Wrapping a novel operation in a library's convention system may add friction
without benefit.
Non-standard graph structures: Hypergraphs, heterogeneous graphs (different
node types), temporal graphs with changing topology — these don't fit TGraphX's
[N, ...] / [2, E] / [E, ...] schema. Hand-rolled code can use whatever
representation suits the problem.
Minimal dependency requirements: Some deployment environments prohibit
external dependencies. A 10-line scatter operation in pure PyTorch is
deployable anywhere PyTorch runs.
One-shot experiments: If you're testing an idea in a notebook that you
expect to throw away, the overhead of the Graph abstraction adds friction
without payoff.
When TGraphX Is Better
Multi-module research code: When graph data flows through many functions
and files, having a typed Graph object prevents convention drift. Every
function receiving a Graph knows the contract.
Team projects: Explicit conventions documented in the library's type system
reduce misunderstandings between collaborators.
Experiment reproduction: write_graph_stats, write_experiment_config,
and the Runner infrastructure create artifacts that make reproduction
easier than hand-rolled equivalents.
Shape validation at boundaries: In a multi-step pipeline (preprocessing →
augmentation → training), placing Graph construction at each major boundary
catches shape errors at the transition rather than deep in a forward pass.
The Hybrid Approach
The most common practical approach is hybrid: use TGraphX for the graph
representation layer (validation, device management, batching) and write
custom layers when the built-in layer set doesn't fit.
from tgraphx import Graph, GraphBatch
from tgraphx.performance import recommended_device
import torch.nn as nn
class MyCustomLayer(nn.Module):
# Custom layer that operates on TGraphX Graph objects.
def __init__(self, in_dim, out_dim):
super().__init__()
self.W = nn.Linear(in_dim, out_dim)
self.W_neigh = nn.Linear(in_dim, out_dim)
def forward(self, g: Graph) -> torch.Tensor:
# Access graph components via the validated Graph API
x = g.node_features # [N, d]
src, dst = g.edge_index[0], g.edge_index[1] # edge conventions clear
# Custom aggregation
msgs = x[src] * g.edge_features.squeeze(-1).unsqueeze(-1)
agg = torch.zeros_like(x)
agg.index_add_(0, dst, msgs)
return torch.relu(self.W(x) + self.W_neigh(agg))
This layer uses the TGraphX Graph type for its input, gaining the validation
and convention benefits, while implementing entirely custom aggregation logic.
Code Comparison: Adding a Second Layer
Hand-rolled:
# First call
h = sage_layer(node_feats, edge_index)
# Second call — who validates edge_index hasn't changed?
# Is h still the right shape for sage_layer?
h2 = sage_layer(torch.relu(h), edge_index)
With TGraphX Graph:
# Layer updates node_features in a new Graph
g1 = layer1(g) # returns updated Graph or tensor
# If layer returns a tensor, the convention is explicit: g still has edge_index
h = torch.relu(layer1(g))
g2 = Graph(node_features=h, edge_index=g.edge_index,
edge_features=g.edge_features, node_labels=g.node_labels)
h2 = layer2(g2)
The second version is more verbose at each step, but the convention is
explicit: g.edge_index is always [2, E] and you're constructing each
intermediate graph with validated shapes.
The Maintenance Horizon
For experiments with a short lifetime (prototype, ablation, paper submission),
hand-rolled code is often fine. For research infrastructure that will be used
and modified over months — by multiple people, across multiple projects —
the maintainability benefits of validated types and documented conventions
compound significantly.
TGraphX's Beta status is a real concern for long-lived projects. Before
committing to it as infrastructure, check the changelog for API stability
trends and pin the version.