-1

Let's say we have a set of N nodes that can be coupled, as in a complex network, and we don't care of the direction of the link (so the link between 1 and 2 is the same as 2 and 1).

I want to use a numpy one-dimensional array to represent the state of each link. The state takes values in {1,0}, where 1 means that the link exists. The array, let's call it "state", should be N*(N-1)/2 long, I suppose (auto-loops are excluded).

In such a context, how could I index properly all the links that start in node a, or the links that end in node b? If we call the array "states", I would to say that state[i] = state of a link that starts in node a, or that end in node j. Is there a way, possibly an efficient way, to do this? If we have N=10 nodes, the first 8 entries correspond to links starting from node 1 and ending in nodes 2,3,4,...,10, but I can't find a general way to express this. Thanks.

P.s.: I know that a 2D matrix may be more useful, but for my purposes I would like to solve the issue keeping the states in a 1D array.

wetrust
  • 57
  • 7
  • Please repeat [on topic](https://stackoverflow.com/help/on-topic) from the intro tour. "please solve my design problem" is too broad and under-defined to qualify as a Stack Overflow question. You have already described the data structure you want; it seems that the present sticking point is the programming acumen to write the supporting code for your desired functionality. That comes from practice; when you have a good attempt at the code you want, *that* could make a good Stack Overflow issue. – Prune Apr 30 '20 at 20:38

1 Answers1

0

The index for pair of nodes (a,b) can be computed using a variable base determined by the number of preceding elements for the smaller of the two node IDs. Adding the larger one to that base gives the proper index:

You can make a function to get the state index from a pair of node identifiers (zero based) like this:

def indexOf(a,b,N=10):
    return indexOf(b,a,N) if a>b else N*a-a*(a+1)//2+b

produces indexes as requested:

for a in range(10):
    print([indexOf(a,b) for b in range(10)])

[0, 1,  2,  3,  4,  5,  6,  7,  8,  9]
[1, 10, 11, 12, 13, 14, 15, 16, 17, 18]
[2, 11, 19, 20, 21, 22, 23, 24, 25, 26]
[3, 12, 20, 27, 28, 29, 30, 31, 32, 33]
[4, 13, 21, 28, 34, 35, 36, 37, 38, 39]
[5, 14, 22, 29, 35, 40, 41, 42, 43, 44]
[6, 15, 23, 30, 36, 41, 45, 46, 47, 48]
[7, 16, 24, 31, 37, 42, 46, 49, 50, 51]
[8, 17, 25, 32, 38, 43, 47, 50, 52, 53]
[9, 18, 26, 33, 39, 44, 48, 51, 53, 54]

allPairs = [ (a,b) for a in range(10) for b in range(a,10) ]
print( sorted(allPairs,key=lambda ab:indexOf(*ab) ) )
[(0, 0), (0, 1), (0, 2), (0, 3), (0, 4), (0, 5), (0, 6), (0, 7), (0, 8), (0, 9), (1, 1), (1, 2), (1, 3), (1, 4), (1, 5), (1, 6), (1, 7), (1, 8), (1, 9), (2, 2), (2, 3), (2, 4), (2, 5), (2, 6), (2, 7), (2, 8), (2, 9), (3, 3), (3, 4), (3, 5), (3, 6), (3, 7), (3, 8), (3, 9), (4, 4), (4, 5), (4, 6), (4, 7), (4, 8), (4, 9), (5, 5), (5, 6), (5, 7), (5, 8), (5, 9), (6, 6), (6, 7), (6, 8), (6, 9), (7, 7), (7, 8), (7, 9), (8, 8), (8, 9), (9, 9)]

Note that this allocates entries for node links to themselves

The drawback of that indexing model is that it requires that the number of nodes (N) be known in advance and supplied on every call.

A more generic approach would be to index the flat list differently based on a progression that does not require having a predetermined value for N. Every pair will always be at the same index no matter the total number of nodes:

def indexOf(a,b):
    return indexOf(b,a) if a<b else a*(a+1)//2+b

it can be reversed (to get the node pair at a given index) like this:

def unindex(X):
    b = int( ((8*X+1)**0.5-1)/2 )
    a = X - b*(b+1)//2
    return a,b

The indexes are not in the same order but the function doesn't need to know N:

for a in range(10):
    print([indexOf(a,b) for b in range(10)])

[0,  1,  3,  6,  10, 15, 21, 28, 36, 45]
[1,  2,  4,  7,  11, 16, 22, 29, 37, 46]
[3,  4,  5,  8,  12, 17, 23, 30, 38, 47]
[6,  7,  8,  9,  13, 18, 24, 31, 39, 48]
[10, 11, 12, 13, 14, 19, 25, 32, 40, 49]
[15, 16, 17, 18, 19, 20, 26, 33, 41, 50]
[21, 22, 23, 24, 25, 26, 27, 34, 42, 51]
[28, 29, 30, 31, 32, 33, 34, 35, 43, 52]
[36, 37, 38, 39, 40, 41, 42, 43, 44, 53]
[45, 46, 47, 48, 49, 50, 51, 52, 53, 54]

print( [unindex(x) for x in range(N*(N+1)//2) ])
[(0, 0), (0, 1), (1, 1), (0, 2), (1, 2), (2, 2), (0, 3), (1, 3), (2, 3), (3, 3), (0, 4), (1, 4), (2, 4), (3, 4), (4, 4), (0, 5), (1, 5), (2, 5), (3, 5), (4, 5), (5, 5), (0, 6), (1, 6), (2, 6), (3, 6), (4, 6), (5, 6), (6, 6), (0, 7), (1, 7), (2, 7), (3, 7), (4, 7), (5, 7), (6, 7), (7, 7), (0, 8), (1, 8), (2, 8), (3, 8), (4, 8), (5, 8), (6, 8), (7, 8), (8, 8), (0, 9), (1, 9), (2, 9), (3, 9), (4, 9), (5, 9), (6, 9), (7, 9), (8, 9), (9, 9)]

Using numpy, if you have an array of node pairs, you can get their corresponding state indexes by writing the function (2nd version) like this:

import numpy as np

def indexOf(ab):
    a,b = np.max(ab,axis=-1),np.min(ab,axis=-1)
    return a*(a+1)//2 + b

output:

N = 10

states = np.arange(N*(N+1)//2)%2        # some random node links

pairs = np.array( [[1,3],[2,4],[7,2]] ) # array of node pairs

connected= states[indexOf(pairs)]       # indirection to states

print(states)
# [0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0]
print(indexOf(pairs))
# [ 7 12 30]
print(connected)
# [1 0 0]

if you use the first version of the function, you will need to pass the number of nodes on every call:

def indexOf(ab,N=10):
    a,b = np.min(ab,axis=-1),np.max(ab,axis=-1)
    return N*a-a*(a+1)//2+b

connected= states[indexOf(pairs,N=10)]
Alain T.
  • 40,517
  • 4
  • 31
  • 51