Interactive Diagnostics Lab
This lab provides interactive cells for exploring how solver settings, contraction strength, graph density, and Jacobian penalties change SILVA behavior. It is a documentation-native companion to the package notebooks.
Lab Variables
| Control | Meaning | Typical range |
|---|---|---|
alpha |
damping in \(z_{k+1}=(1-\alpha)z_k+\alpha f(z_k)\) | 0.1 to 1.0 |
solver |
fixed-point method | picard, anderson, broyden |
spectral_scale |
state-weight scale before the nonlinearity | 0.1 to 1.2 |
k |
graph neighbors in a kNN adapter | 2 to 12 |
jacobian_weight |
penalty multiplier for \(\|J_fv\|^2\) | 0.0 to 1e-2 |
Scalar Stability Cell
import torch
from silva_networks import SolverConfig, fixed_point, spectral_radius
alpha = 0.6
solver = "anderson"
spectral_scale = 0.75
W = torch.tensor([[spectral_scale, 0.05], [0.0, 0.4 * spectral_scale]])
b = torch.tensor([0.2, -0.1])
def f(z):
return torch.tanh(W @ z + b)
z0 = torch.zeros(2)
result = fixed_point(
f,
z0,
SolverConfig(solver=solver, alpha=alpha, max_iter=30, tol=1e-6),
)
def damped_step(z):
return (1 - alpha) * z + alpha * f(z)
rho = spectral_radius(damped_step, result.z)
print(result.solver, result.converged, result.residuals)
print("spectral radius", rho)
Interpretation:
is local evidence that the fixed point is attracting for the damped solver map.
Graph Density Cell
import torch
from silva_networks import GraphTensorBatch, SILVAGraphNetwork, SolverConfig
k = 4
num_nodes = 24
x = torch.randn(num_nodes, 5)
src = torch.arange(num_nodes).repeat_interleave(k)
offsets = torch.arange(1, k + 1).repeat(num_nodes)
dst = (src + offsets) % num_nodes
edge_index = torch.stack([src, dst])
graph = GraphTensorBatch(x=x, edge_index=edge_index)
graph.validate()
model = SILVAGraphNetwork(
in_dim=5,
hidden_dims=[12],
out_dim=2,
task="node",
config=SolverConfig(solver="anderson", alpha=0.5, max_iter=12),
)
out = model(x, edge_index=edge_index, return_result=True)
print(out.logits.shape)
print([r.residuals for r in out.solver_results])
As k increases, the local field has more neighbors per node. This can improve
information flow, but it also changes the Lipschitz behavior of the transition.
Jacobian Penalty Cell
import torch
from silva_networks import silva_jacobian_regularization_loss
z = torch.randn(8, 4, requires_grad=True)
def transition(state):
return torch.tanh(0.5 * state + 0.1)
penalty = silva_jacobian_regularization_loss(transition, z, samples=2)
loss = z.square().mean() + 1e-3 * penalty
loss.backward()
print(float(penalty.detach()))
print(z.grad.norm())
The penalty estimates
which discourages high-gain transitions near the current state.
Diagnostic Questions
| Observation | Likely next check |
|---|---|
| Residual oscillates | lower alpha, increase Anderson ridge, or try Picard |
| Residual drops then stalls | increase max_iter, check state scale, inspect Jacobian radius |
| Spectral radius above one | lower damping or regularize the transition |
Graph validation metric changes sharply with k |
compare local/global ablations and graph density |
| Gradient norms explode | add Jacobian penalty or reduce state-weight scale |
Repeat each comparison with the same random seed, input tensor, transition parameters, and stopping rule. Solver changes should be compared at matched residual tolerance, not only at matched iteration count.
The derivations behind these cells are in Jacobians and Stability and Solver Derivation Lab. Primary sources are listed under Solvers and Linear Algebra and Equilibrium and Implicit Layers.
Worked Evidence Bridge
The derivation above becomes a complete SILVA study when the state, condition, solver result, task result, and gradient path are kept separate. Here the state is one state vector per graph node and the condition is features, edges, and graph context. The compact relation is
The following is the complete executable program used by the repository tests:
from __future__ import annotations
import torch
from silva_networks import SILVAGraphLayer, SolverConfig, stability_report
def main() -> None:
torch.manual_seed(7)
x = torch.randn(8, 5)
y = (x[:, 0] + x[:, 1] > 0).long()
edge_index = torch.tensor(
[[0, 1, 2, 3, 4, 5, 6, 7], [1, 2, 3, 4, 5, 6, 7, 0]],
dtype=torch.long,
)
layer = SILVAGraphLayer(5, 12, config=SolverConfig(max_iter=18, alpha=0.45))
head = torch.nn.Linear(12, 2)
z = layer(x, edge_index=edge_index)
loss = torch.nn.functional.cross_entropy(head(torch.tanh(z)), y)
loss.backward()
report = stability_report(lambda zz: layer.f(zz, x, edge_index=edge_index), z, samples=2, iters=10)
print("state_shape", tuple(z.shape))
print("loss", float(loss.detach()))
print("residual", report.residual)
print("spectral_radius", report.spectral_radius)
if __name__ == "__main__":
main()
Run it from the project root:
Measured Output
state_shape (8, 12)
loss 0.7801069021224976
residual 0.07725001126527786
spectral_radius 0.7778381109237671
What This Result Establishes
This run records task loss, normalized solver evidence, and a spectral-radius estimate. It establishes that the compact mechanism is executable with finite outputs and that its stated shape or structural contract can be inspected. It does not establish source-scale accuracy by itself.
For the next controlled study, hold model and graph fixed while sweeping solver, damping, tolerance, and maximum iterations. Keep the compact run as a regression case. For every larger run, archive the resolved data source and split, preprocessing, seed, constructor arguments, forward and backward solver settings, task metric, normalized residual, iteration count, gradient norm, runtime, peak memory, and convergence failures. This keeps task quality, numerical convergence, and computational cost from being collapsed into one number.
Where to Go Next
| Question | Page |
|---|---|
| How are Jacobians connected to local stability? | Jacobians and Stability |
| Which diagnostic functions are available? | Diagnostics API |
| How do solver choices change residual traces? | Solver Derivation Lab |