15

In the context of resistor networks and finding the (equivalent) resistance between two arbitrary nodes, I am trying to learn how to write a generic approach in Mathematica, generic as in an approach that also lends itself to large spatially randomly distributed graphs as well (not just lattices), where then one has to deal with sparse matrices. Before getting there, I've tried simply recreating a piece of algorithm written in Julia for solving an example on a square grid, with all resistances set to 1.

Here's the grid where each edge depicts a resistor between its incident nodes (all resistance values are assumed to be $1 \Omega$) and two arbitrary nodes ($A$ at {2,2} and $B$ at {7,8}) are highlighted, question is to find the resistance between them.

enter image description here

In the Julia's code snippet, the approach of injecting a current and measuring the voltages at the two nodes is adopted, as shown below: (source)

N = 10
D1 = speye(N-1,N) - spdiagm(ones(N-1),1,N-1,N)
D = [ kron(D1, speye(N)); kron(speye(N), D1) ]
i, j = N*1 + 2, N*7+7
b = zeros(N^2); b[i], b[j] = 1, -1
v = (D' * D) \ b
v[i] - v[j]

Output: 1.6089912417307288

I've tried to recreate exactly the same approach in Mathematica, here's what I have done:

n = 10;
grid = GridGraph[{n, n}];
i = n*1 + 2;
j = n*7 + 7;
b = ConstantArray[0, {n*n, 1}];
b[[i]] = {1};
b[[j]] = {-1};
incidenceMat = IncidenceMatrix[grid];
matrixA = incidenceMat.Transpose[incidenceMat];
v = LinearSolve[matrixA, b]

I feel very silly, but I must be missing something probably very obvious as LinearSolve does not manage to find a solution (for the chosen nodes the answer is know to be $1.608991...$, which is obtained by taking the potential difference between A and B since the current is set to 1).

Questions

  • Have I mis-interpreted something in my replication of the algorithm sample written in Julia?

  • It would be very interesting and useful if someone could comment on how extensible these approaches are to more general systems (2d, 3d and not only for lattices). For instance, which approaches would be more suitable to adopt in Mathematica for larger resistor networks (in terms of efficiency, as one would have to deal with very sparse matrices probably).


As a side-note, on the same Rosetta article, there are two alternative code snippets provided for Mathematica (which follows Maxima's approach, essentially similar to the one written Julia). In case someone is interested I include them here: (source for both)

gridresistor[p_, q_, ai_, aj_, bi_, bj_] := 
  Block[{A, B, k, c, V}, A = ConstantArray[0, {p*q, p*q}];
   Do[k = (i - 1) q + j;
    If[{i, j} == {ai, aj}, A[[k, k]] = 1, c = 0;
     If[1 <= i + 1 <= p && 1 <= j <= q, c++; A[[k, k + q]] = -1];
     If[1 <= i - 1 <= p && 1 <= j <= q, c++; A[[k, k - q]] = -1];
     If[1 <= i <= p && 1 <= j + 1 <= q, c++; A[[k, k + 1]] = -1];
     If[1 <= i <= p && 1 <= j - 1 <= q, c++; A[[k, k - 1]] = -1];
     A[[k, k]] = c], {i, p}, {j, q}];
   B = SparseArray[(k = (bi - 1) q + bj) -> 1, p*q];
   LinearSolve[A, B][[k]]];
N[gridresistor[10, 10, 2, 2, 8, 7], 40]

Alternatively:

graphresistor[g_, a_, b_] := 
  LinearSolve[
    SparseArray[{{a, a} -> 1, {i_, i_} :> Length@AdjacencyList[g, i], 
      Alternatives @@ Join[#, Reverse /@ #] &[
        List @@@ EdgeList[VertexDelete[g, a]]] -> -1}, {VertexCount[
       g], VertexCount[g]}], SparseArray[b -> 1, VertexCount[g]]][[b]];
N[graphresistor[GridGraph[{10, 10}], 12, 77], 40]
Henrik Schumacher
  • 106,770
  • 7
  • 179
  • 309

2 Answers2

19

In addition to Carl Woll's post:

Computing the pseudoinverse of a the graph Laplacian matrix (a.k.a. the KirchhoffMatrix) is very expensive and in general leads to a dense matrix that, if the graph is too large, cannot be stored in RAM. In the case that you have to compute only a comparatively small block of the resistance distance matrix, you can employ sparse methods as follows:

Generating a graph with 160000 vertices.

g = GridGraph[{400, 400}, GraphLayout -> None];
L = N@KirchhoffMatrix[g];

The idea here is that I know in advance that $\mathbf{A}$ is symmetric and positive semidefinite and that $$ \operatorname{ker}(\mathbf{L}) = \operatorname{im}(\mathbf{L})^\perp = \mathbb{R} \, \mathbf{1}. $$ (The latter holds only if the graph is connected.)

Fix a vector $\mathbf{b}$ and denote the Kirchhoff matrix by $\mathbf{L}$ and its pseudoinverse by $\mathbf{L}^\dagger$. Denote the orthogonal projection of $\mathbf{b}$ onto $\operatorname{im}(\mathbf{L})$ by $\mathbf{y}$, so that we have $ \mathbf{b} = \mathbf{y} + \mathbf{1} \, \lambda $ with some $\lambda \in \mathbb{R}$. The orthogonal projector onto $\operatorname{im}(\mathbf{L})$ is given by $\mathbf{L} \, \mathbf{L}^\dagger$ so that we have $\mathbf{y} = \mathbf{L} \, \mathbf{L}^\dagger \, \mathbf{b} = \mathbf{L} \, \mathbf{x}$. Thus: $$ \mathbf{b} = \mathbf{L} \, \mathbf{x} + \mathbf{1} \, \lambda. $$ We have $\operatorname{ker}(\mathbf{L})^\perp = \operatorname{ima}(\mathbf{L}^\dagger)$, hence $ \mathbf{1}^\intercal \, \mathbf{x} = \mathbf{1}^\intercal \, \mathbf{L}^\dagger \mathbf{b} = 0, $ hence $$\mathbf{1}^\intercal \, \mathbf{x} = 0.$$

That is, it suffices to solve the linear saddle point system $$ \begin{pmatrix} \mathbf{L} & \mathbf{1} \\ \mathbf{1}^\intercal &0 \end{pmatrix} \begin{pmatrix} \mathbf{x} \\ \lambda \end{pmatrix} = \begin{pmatrix} \mathbf{b} \\ \mathbf{0} \end{pmatrix}. $$ The good things are that the saddle point matrix is (i) invertible and (ii) usually quite sparse. So we may employ LinearSolve to solve this linear system.

The following builds the saddle point matrix A and computes an $LU$-factorization S of it (You may read S basically as the inverse of A).

A = With[{a = SparseArray[ConstantArray[1., {1, VertexCount[g]}]]},
   ArrayFlatten[{{L, a\[Transpose]}, {a, 0.}}]
   ];
S = LinearSolve[A]; // AbsoluteTiming

Applying the pseudoinverse of L to a vector b is now equivalent to

b = RandomReal[{-1, 1}, VertexCount[g]];
x = S[Join[b, {0.}]][[1 ;; -2]];

We may exploit that via the following helper function; internally, it computes only few columns of the pseudoinverse and returns the corresponding resistance graph matrix.

resistanceDistanceMatrix[S_LinearSolveFunction, idx_List] := 
  Module[{n, basis, Γ},
   n = S[[1, 1]];
   basis = SparseArray[
     Transpose[{idx, Range[Length[idx]]}] -> 1.,
     {n, Length[idx]}
     ];
   Γ = S[basis][[idx]];
   (* stealing from Carl Woll *)
   Outer[Plus, Diagonal[Γ], Diagonal[Γ]] - Γ - Transpose[Γ]
   ];

Let's compute the resistance distance matrix for 5 random vertices:

SeedRandom[123];
idx = RandomSample[1 ;; VertexCount[g], 5];
resistanceDistanceMatrix[S, idx] // MatrixForm

$$\left( \begin{array}{ccccc} 0. & 2.65527 & 2.10199 & 2.20544 & 2.76988 \\ 2.65527 & 0. & 2.98857 & 2.85428 & 2.3503 \\ 2.10199 & 2.98857 & 0. & 2.63996 & 3.05817 \\ 2.20544 & 2.85428 & 2.63996 & 0. & 3.04984 \\ 2.76988 & 2.3503 & 3.05817 & 3.04984 & 0. \\ \end{array} \right)$$

This requires $k$ linear solves for $k (k-1) /2 $ distances, so it is even more efficient than the method you posted (which needs one linear solve per distance).

The most expensive part of the code is to generate the LinearSolveFunction S. Thus, I designed the code so that S can be reused.

Under the hood, a sparse LU-factorization is computed via UMFPACK. Since the graph g is planar, this is guaranteed to be very quick compared to computing the whole pseudoinverse.

For nonplanar graphs, things become complicated. Often, using LU-factorization will work in reasonable time. But that is not guaranteed. If you have for example a cubical grid in 3D, LU-factorization will take much longer than a 2D-problem of similar size even if you measure size by the number of nonzero entries. In such cases, iterative linear solvers with suitable preconditioners may perform much better. One such method (with somewhat built-in preconditioner) is the (geometric or algebraic) multigrid method. You can find an implementation of such a solver along with a brief explanation of its working here. For a timing comparison of linear solves on a cubical grid topology see here. The drawback of this method is that you have to create a nested hierarchy of graphs on your own (e.g. by edge collapse). You may find more info on the topic by googling for "multigrid"+"graph".

Henrik Schumacher
  • 106,770
  • 7
  • 179
  • 309
  • Sorry for my late comment, many thanks for this wonderful analysis and extremely useful discussions. You mentioned that this approach works for connected graphs only. To better understand its domain of validity, while the connected requirement is met, would it work e.g. for i) cases where there may be large loops present? ii) random graphs with spatially distributed nodes in 3D? iii) what about networks with random resistance values?(inhomogeneity) As a side-question, given two nodes, do you reckon one can use this method to simply detect edges that are carrying non-zero current between them? –  Mar 13 '19 at 15:30
  • What I meant is the computation of the pseudoinverse: If you have several connected components you have to add a row and a column for each component to the matrix: The entries have to be all 1 for vertices of the given coponent and zero otherwise. However, the resistance distance formula from https://en.wikipedia.org/wiki/Resistance_distance#Definition is quite likely not correct in this case (the resitance between vertices in different connected components of the graph should be infinite). – Henrik Schumacher Mar 13 '19 at 15:38
  • At i) I don't see at the moment why loops should be a problem. At ii) The method should work but may become slower. It really depends on the topology of the graph at hand, and thus indirectly also on the graph probability distribution. At iii) There is a notion of a graph Laplacians for weighted graphs. Probably one has to employ the inverses of the edge resitances as weights, but I am not sure about that. At your last question: I don't understand it. Maybe you can explain what you mean? – Henrik Schumacher Mar 13 '19 at 15:44
  • See also here: http://match.pmf.kg.ac.rs/electronic_versions/Match50/match50_73-82.pdf I had a short gilmpse into the paper and I found the statement that for computing resistance distance, every generalized inverse will do (and will return the same result). So I guess the method should also apply to networks with varying edge resistances. – Henrik Schumacher Mar 13 '19 at 15:51
  • IIRC, instead of L = N@KirchhoffMatrix[g], you have to use the matrix L from this post in the case of nonconstant edge resistances. In that post, I defined the vector conductances which is a function on edges; if you set conductances to the inverse of the edge resistances, everything should work out correctly. – Henrik Schumacher Mar 19 '19 at 10:46
  • Hm. I do not expect that this will work for arbitrary resistor networks. van der Pauw's theorem requires that the geometry is two-dimensional and simply-connected so that the Riemann mapping theorem can be applied to transform it conformally to the round disk (or the upper half plane). It might work though if you graph is the edgegraph of suitably fine, quasi-uniform triangulation of a planar, simply connected domain. But with the code above, you can try yourself... – Henrik Schumacher Oct 01 '19 at 12:44
  • (...) In order to test if this method indeed works for random-simple-connected graphs with homogeneous resistors, is there a case we could consider for which we know the answer of resistivity/conductivity of the graph, so we could use to sanity check?Any ideas/hunches you might have would be very helpful! –  Oct 01 '19 at 14:11
  • "simple graph" - I was not talking about a simple graph, embedded into $\R^2$; what I meant was a simply-connected and bounded domain in $\R^2$ and its finite element discretization (i.e., using the edge weights of the $\cotan$-Laplacian. Actually, for a general resistance graphs, I don't know how you want to define the sheet resistance at all. In a nutshell, I tried to tell you that this is not going to work in the setting you desire. – Henrik Schumacher Oct 02 '19 at 17:08
  • I am not well read in this field, so what I do is a google search with keywords "resistance network" and "pseudoinverse" and there are indeed a few hits. So yes, it is well-known that the pseudoinverse plays an important role here. Maybe it is less well-known that the pseudoinverse can be computed by inverting a suitable saddle point matrix. The fact just pops out if you work a lot with saddle point matrices (as I do in the context of finite element analysis). But I do not have any particular reference for that. – Henrik Schumacher Oct 18 '19 at 11:46
  • I don't know about the kron reduction, so I cannot tell... Sorry. – Henrik Schumacher Oct 23 '19 at 14:01
  • Thank you sooooo much for the added explanations! You're awesome :), I think I fully got it now, I will read it over and over and get back to you in case im stuck again. Having now seen your explanation, I think I've seen this method in one of the circuit theory papers I've read recently, but cannot recall which at the moment, will send you the link if I find it again. Thanks again Henrik! –  Oct 23 '19 at 15:39
  • You're welcome. – Henrik Schumacher Oct 23 '19 at 18:28
  • Hi, if I may ask 1-2 follow-up questions regarding the approach: i) so I understand that the image of $L$ is all vectors (vec) orthogonal to vec of all 1's, therefore $\operatorname{im}(L)^\perp = \mathbf{1},$ and $LL^\dagger$ projects orthogonally onto $\operatorname{im}(L),$ while $L^\dagger L$ projects onto $\operatorname{im}(L^T)$ which can be written as orthogonal complement of kernel of $L,$ right? but how do get $1^T \mathbf{x}=0?$ isn't $\mathbf{x}=L^\dagger \mathbf{b}?$ I understand the rhs that $1^T L^\dagger L \mathbf{b}=0$ because $L^\dagger L$ projects onto $\mathbf{1}^\perp.$ –  Oct 24 '19 at 14:37
  • ii)We wrote $\mathbf{y}=\mathbf{L}\mathbf{L}^\dagger$ but shouldn't it be $\mathbf{L}\mathbf{L}^\dagger \mathbf{b}$ which makes $\mathbf{x}=\mathbf{L}^\dagger \mathbf{b}$ (not sure). Again notation wise, isn't $\operatorname{ker}(\mathbf{L})^\perp=\operatorname{im}(\mathbf{L}^T)$ instead of $\operatorname{im}(\mathbf{L}^\dagger)?$ iii) Sorry for all these questions, last thing: why do we not choose our $\mathbf{b}$ directly from the image of $\mathbf{L}$ so that we can set $\lambda$ to zero? i.e. can we not just choose $\mathbf{b}$ as $(1,-1,0,0,0,0...)?$ Thx a lot in advance for your feedback –  Oct 24 '19 at 14:38
  • i) + ii) Were typos. Corrected. iii) You can. But you still have to solve a linear syste,.. – Henrik Schumacher Oct 24 '19 at 15:46
  • Right, with nonstandard edge weights, you should use $L = D^T , G , D$. And yes, the x in my code is not essential (it was computed with a random right hand side anyways). It was only meant to show you that S acts like the inverse of the matrix A, but without computing the inverse of A (which would usually be prohibitively). – Henrik Schumacher Nov 11 '19 at 13:40
  • Maybe Gdirected= Graph[Map[#[[1]]->#[[2]] &, EdgeList[G] ]]; reorders vertices? – Henrik Schumacher Nov 11 '19 at 18:31
  • Ahh! You've hit the nail on the head! It seems I need to indicate the vertex set as well to avoid that, and do so in a sorted manner, I mean: Gdirected= Graph[Sort[VertexList[EdgeList[G]]] ,Map[#[[1]]->#[[2]] &, EdgeList[G] ]]; then it gives the correct result of 1.28. I hope Sort and VertexList are efficient for such purposes :p –  Nov 12 '19 at 11:27
  • Hm. No, I do not know any clever way for computing the trace of $\mathbf{L}^\dagger$. Sorry. – Henrik Schumacher Nov 18 '19 at 12:40
  • hi Henrik, it seems I'll never run out of questions on this post :D I just wanted to ask, say we don't use the Carl Woll trick at the end with resistanceDistanceMat... (which I don't really understand anyway :/), and we simply create the saddle point matrix and solve the resulting linear system, is that already giving us a considerable speed up compared to if we were to solve the original Laplacian lin sys? speed up because we are sparsifying the matrix and can thus use sparse lin solvers. Moreover, with the saddle approach, we make the lin system solvable irrespective of the rhs vector (?) –  May 20 '20 at 09:57
  • The question is not about speed. The original linear system has nontrivial kernel and cokernel, so that "solving" it withs standard methods might leads to immense errors. The slowdown is only a side effect because the linear solve detects that something does not work nicely, so it desperately tries to use a couple of expensive tools to compensate the ill-definedness of the problem. The saddle point system will lead to a "solution" for each right hand side, but it actually solves a modified equation in which the RHS is first projected onto the image of the Laplacian. – Henrik Schumacher May 20 '20 at 11:10
  • "but I guess we will have one linear solve per distance in that case, right?" Yes, that is right. This is fast if only a few distances are needed. But for computing all distances, probably computing the pseudoinverse is better. – Henrik Schumacher May 20 '20 at 11:47
  • Nope. As I tried to say before, LinearSolve is not good at "inverting" singular matrices. Moreover LinearSolve computes an $LU$-decomposition of $A$, which is not directly related to the pseudoinverse unless the matrix is nonsingular. But even then, the inverse of $A$ is never computed by LinearSolve; actually, that is the key point about LinearSolve. I am afraid you have to use Pseudoinverse. There might be cleverer solvers out there like SPQR that might be more suitable to computing the pseudoinverse of sparse matrices. But they are not built-in. – Henrik Schumacher May 20 '20 at 12:15
  • Oh I see, I will have to look further into this then, thanks for the explanations. I admit, I am very puzzled about one thing though, namely, I knew the Laplacian L is singular, but I thought when we build the saddle point matrix as you show here, we create a matrix (A) that is no longer singular, because you pointed out it is in fact invertible. But, is our saddle point matrix here, which contains the Laplacian, in fact still singular? –  May 20 '20 at 12:20
  • No the saddle point matrix is not singular. That is the point. If done correctly (I cannot check the details at the moment because I am busy), then applying its LinearSolveFunction to $Append[b,0]$ (and stripping of the last entry of the solution) is equivalent to applying the pseudoinverse of the Laplacian to b. So in principle it is possible to compute the pseudoinvers by applying the LinearSolveFunction to all vectors of the standard basis. But I doubt that this will be more efficient than PseudoInverse. But may be it is?I don't know. – Henrik Schumacher May 20 '20 at 12:28
  • Aha! I think I get it now, your last point was really helpful! so indeed in the routine resistanceDistanceMatrix the pseudoinverse is being calculated with Γ = S[basis][[idx]]; when idx is the Range[VertexCount[g]]. Is applying a the LU decomposition to basis vectors a known method for computing inverses? From a mathematica point of view, I struggle to understand what is basis and the notation of S[basis][[idx]] :/ Thanks very much for all your patience Henrik and that you took time to explain during your work, much appreciated! –  May 20 '20 at 14:45
  • Instead of explicitly computing the PseudoInverse[], it is (usually) better to just replace LinearSolve[] with LeastSquares[], if you're expecting LinearSolve[] to fail quite frequently. @user – J. M.'s missing motivation May 20 '20 at 21:03
  • Well, once a factorization with not so much fill-in is found, a linear solve will cost you probably $O(n)$ where $n$ is the number of nodes. So computing all resistance distances amounts to $O(n^2)$. I am not sure about the factorization. For a planar graph, it should have $O(n \log(n))$-ish complexity. For a dense matrix, it should be $O(n^3)$ time and $O(n^2)$ memory. – Henrik Schumacher Mar 16 '21 at 11:04
  • I am not sure how much a pseudoinverse would cost or what the best method would be used there. Usually direct methods for eigendecomposition or SVD take very long for large sparse matrices. Sorry that I cannot help you any further here. – Henrik Schumacher Mar 16 '21 at 11:45
  • No don't say sorry: you have been of great help already! If I may ask one last thing: when you said (paraphrasing here), it requires k lin solves for k(k-1)/2 distances, is this assumed to be after the LU decomposition of the saddle matrix is done? and where does k(k-1)/2 come from? – CuriousBadger Mar 16 '21 at 12:44
  • If idx is of length $k$, then resistanceDistanceMatrix[S, idx] is a symmetric $k \times k$ matrix with zeroes on the diagonal. This effectively encodes $k (k-1)/2$ distances (because the zeros on the diagonal stem from the fact that a point to itself has distance $0$.) – Henrik Schumacher Mar 16 '21 at 12:49
16

Based on rcampion2012's answer to Efficient Implementation of Resistance Distance for graphs?, you could use:

resistanceGraph[g_] := With[{Γ = PseudoInverse[N @ KirchhoffMatrix[g]]},
    Outer[Plus, Diagonal[Γ], Diagonal[Γ]] - Γ - Transpose[Γ]
]

Then, you can find the resistance using:

r = resistanceGraph[GridGraph[{10, 10}]];
r[[12, 68]]

1.60899

Carl Woll
  • 130,679
  • 6
  • 243
  • 355
  • Nice answer, but why did you take the [[12,68]] part and what are the right terms to take in the general case of n x n system? – Hans Olo Mar 13 '19 at 11:04
  • 4
    @Loki There are no right choices here, one is usually interested to know the equivalent resistance between two arbitrary points of the network, in this example the 12th and 68th nodes are chosen (they correspond to the nodes I had highlighted in the question, and if you collapse the grid into a vector you'd get these indices). –  Mar 13 '19 at 11:35
  • Thanks, I hadn't noticed the small red dots in the graph. Nice answer, my +1 – Hans Olo Mar 13 '19 at 11:40