Generate Random Graph

#!/usr/bin/python3
# ====================================================================
# generate a random graph for testing the shortest path code
#
# due to the random nature of the compact cost data, it is possible
# for bugs to be introduced into in the graph. only extensive
# test will tell. (for example every node should have an inbound
# and an outbound connection.)
#
# see function fix_zero (fix zero inbound connections error)
# ====================================================================

import json
from random import randint, choice

CMINMAX = (10,100)         # minimum/maximum connection cost
VMAX    = 12               # maximum number of vertices
ECON    = (1,4)            # range of edge (outbound) connections
                           # per vertex (minimum,maximum)

jfile   = 'compact_cost_data.json'


class Vertex:

    def __init__(self,idx,cons,name=None):
        self.idx  = idx                 # list index
        self.cons = cons                # outbound connections
        self.name = name                # name


# --------------------------------------------------------------------
# ---- title string
# --------------------------------------------------------------------

def title_str(title,length=70):
    if not title:
        t = '-' * length
    elif len(title) > length:
        t = title[:length]
    else:
        t = title + '-' * (length - len(title))
    return t


# --------------------------------------------------------------------
# ---- display vertex's (outbound) connections (node,cost)
# --------------------------------------------------------------------

def display_vertex_outbound_connections(v):
    print(f'cons: {v.cons}')


# --------------------------------------------------------------------
# ---- display vertex
# --------------------------------------------------------------------

def display_vertex(v):
    print(title_str(f'---- vertex {v.idx} ',20))
    print(f'idx : {v.idx}')
    display_vertex_outbound_connections(v)


# --------------------------------------------------------------------
# ---- display graph
# --------------------------------------------------------------------

def display_graph(g):
    for v in g:
        display_vertex(v)
        

# -------------------------------------------------------------------
# ---- create a randomly connected graph
# -------------------------------------------------------------------

def create_random_graph(vmax,econ,cminmax):

    # ---- create a list of the vertices (indexes) in the graph

    vlst = [i for i in range(vmax)]
    
    # ---- create a graph
    
    graph = []

    # ---- for every source node (vertex) in the graph create
    # ---- outbound connection list
        
    for sidx in range(vmax):

        # ---- remove source idx from the list of vertices leaving
        # ---- a list of potential nodes (vertices) to connect to
    
        vvlst = vlst.copy()
        vvlst.remove(sidx)
                                   
        # ---- generate a random list of outbound connections

        cons = []
        
        for _ in range(randint(econ[0],econ[1])):
            cidx  = choice(vvlst)
            ccost = randint(cminmax[0],cminmax[1])
            cons.append((cidx,ccost))
            vvlst.remove(cidx)
            
        graph.append(Vertex(sidx,cons))

    return graph


# --------------------------------------------------------------------
# ---- display outbound connections
# --------------------------------------------------------------------

def display_outbound_connections(outbound,max=45):
    print(title_str('---- outbound connections ',max))
    for idx,rcn in enumerate(outbound):
        print(f'{idx:<2} {str(rcn[1]):40} {rcn[2]}')
    print(title_str('',max))


# --------------------------------------------------------------------
# ---- output JSON file
# --------------------------------------------------------------------

def output_json_file(jdata,outfile):
    with open(outfile,'w') as ofile:
        ofile.write(json.dumps(jdata))


# --------------------------------------------------------------------
# ---- verify the number of inbound and outbound connections
# --------------------------------------------------------------------

def verify_connection_counts(costs):

    siz = len(costs)           # number of rows (nodes)
    
    inbound_count  = [0 for _ in range(siz)]
    outbound_count = [0 for _ in range(siz)]
    
    for i,r in enumerate(costs):
        outbound_count[i] = len(r[1])
        for c in r[1]:
            inbound_count[c[0]] += 1

    print()
    print(' row  outbound  inbound')
    for i in range(siz):
        print(f'[{i:2}]   {outbound_count[i]:4}     {inbound_count[i]:4}')

    # ---- test for zero counts

    for c in inbound_count:
        if c < 1:
            return False
    for c in outbound_count:
        if c < 1:
            return False
    
    return True


# --------------------------------------------------------------------
# ---- fix zero inbound connections error
# ---- 
# ---- because the compact cost data consists of outbound connections
# ---- generated randomly, some nodes may not have an inbound
# ---- connection. this function adds an inbound connection. the
# ---- inbound connection is a mirror of the node's first outbound
# ---- connection in the compact cost data.
# ----
# ---- note: because of the way outbound connections are generated,
# ----       every node has at least one outbound connection
# --------------------------------------------------------------------

def fix_zero(costs):

    siz = len(costs)
    
    inbound_count = [0 for _ in range(siz)]
    
    for i,r in enumerate(costs):
        for c in r[1]:
            inbound_count[c[0]] += 1

    # ---- check each connection count (row (node) and count)

    for r,c in enumerate(inbound_count):

        if c < 1:

            print(f'row {r} has zero inbound connections')
            
            # ---- outbound connections of row r

            cons = costs[r][1]

            # ---- first outbound connection

            con = cons[0]        # first outbound connection
            
            print(f'first outbound connection row={r} cost={con}')

            # ---- create mirror connection

            print(f'adding mirror row={con[0]} cost=({r},{con[1]})')
            
            costs[con[0]][1].append((r,con[1]))

    return

# --------------------------------------------------------------------
# ---- main
# --------------------------------------------------------------------

if __name__ == '__main__':

    # ---- create and initialize a graph (list of vertices)

    graph = create_random_graph(VMAX,ECON,CMINMAX)

    # ---- create compact cost data

    costs = []
    for v in graph:
        costs.append([v.idx,v.cons,v.name])

    print()
    display_outbound_connections(costs,60)
    
    # ---- verify connection counts

    print()
    print('verify connection count')
    
    tf = verify_connection_counts(costs)

    if not tf:
        print()
        print('error: found node(s) with zero inbound connections')

        print()
        print('Fixing node(s) with zero inbound connections')

        fix_zero(costs)

        print()
        print(title_str('---- fixed node(s) with no inbound connections',60))
        display_outbound_connections(costs,60)

    else:
        print()
        print('no nodes with zero inbound connections found')

    # ---- output compact cost data to a json file

    output_json_file(costs,jfile)

    print()
    print(f'created JSON file {jfile}')
    print()