If a tentacle pops from every cycle node, then we just need to find which nodes have more than two links to find the nodes of the cycle. This works also for "non-totally connected graphs".
Most of the code is dedicated to generate the graph with graphviz. I didn't check yet, but apparently this library offers a real solution for finding cycles in networks.

# Create an octopus flavoured node map, in the format:
# {0: [1], 1: [2], 2: [3], 3: [4], 4: [5], 5: [6], 6: [7], 7: [8], 8: [72], 9: [10], 10: [11], 11: [12], 12: [13], 13: [14], 14: [15], 15: [16], 16: [17], 17: [73], 18: [19], 19: [20], 20: [21], 21: [22], 22: [23], 23: [24], 24: [25], 25: [26], 26: [74], 27: [28], 28: [29], 29: [30], 30: [31], 31: [32], 32: [33], 33: [34], 34: [35], 35: [75], 36: [37], 37: [38], 38: [39], 39: [40], 40: [41], 41: [42], 42: [43], 43: [44], 44: [76], 45: [46], 46: [47], 47: [48], 48: [49], 49: [50], 50: [51], 51: [52], 52: [53], 53: [77], 54: [55], 55: [56], 56: [57], 57: [58], 58: [59], 59: [60], 60: [61], 61: [62], 62: [78], 63: [64], 64: [65], 65: [66], 66: [67], 67: [68], 68: [69], 69: [70], 70: [71], 71: [79], 72: [73], 73: [74], 74: [75], 75: [76], 76: [77], 77: [78], 78: [79], 79: [72]}
class Octopus():
def create(self, octopus_id, complete=True):
cycle_len = 8
tentacle_len = 9
node_n = 0
output = {}
first_node = octopus_id * ((tentacle_len * cycle_len) + cycle_len)
for i in range(cycle_len):
for j in range(tentacle_len - 1):
tn = (i*tentacle_len) + j
output[first_node+tn] = [first_node+tn+1]
for k in range(cycle_len):
cni = (k * tentacle_len) + tentacle_len - 1
cnf = (tentacle_len * cycle_len) + k
if cni not in output:
output[first_node+cni] = []
if cnf not in output:
output[first_node+cnf] = []
output[first_node+cni] += [first_node+cnf]
if cnf + 1 >= (tentacle_len * cycle_len) + cycle_len:
if complete:
output[first_node+cnf] += [first_node+(tentacle_len * cycle_len)]
else:
output[first_node+cnf] += [first_node+cnf+1]
return output
# The tool that performs the node triage
class CycleFinder():
nodes = {}
# Distinguish nodes from different octopuses
def find_groups(self, newcomers, cycles=[]):
cycle_groups = []
new_cycle = [newcomers]
for cycle in cycles:
linked_cycle = False
for newcomer in newcomers:
if newcomer in cycle:
linked_cycle = True
new_cycle += [cycle]
new_cycle = [list(set([item for sublist in new_cycle for item in sublist]))]
if not linked_cycle:
cycle_groups += [list(set(cycle))]
cycle_groups += new_cycle
return [list(i) for i in set(tuple(i) for i in cycle_groups)]
# Given a node number, return the cycle id to which it belongs
def get_cycle_id(self, node, cycles):
for i,cycle in enumerate(cycles):
if node in cycle:
return i
# Find nodes with more than two links
def get_cycles(self):
serialized_data = {}
for node,link in self.nodes.items():
if link:
link = link[0]
if node not in serialized_data:
serialized_data[node] = []
if link not in serialized_data:
serialized_data[link] = []
serialized_data[node] += [link]
serialized_data[link] += [node]
cycle_nodes = set()
for node, links in serialized_data.items():
if len(links) > 2:
cycle_nodes.add(node)
cycles=[]
for node, links in serialized_data.items():
cycles = self.find_groups([node] + links, cycles)
grouped_cycle_nodes = {}
for node in cycle_nodes:
group_id = self.get_cycle_id(node, cycles)
if group_id not in grouped_cycle_nodes:
grouped_cycle_nodes[group_id] = []
grouped_cycle_nodes[group_id] += [node]
return grouped_cycle_nodes.values()
# *** Generate graph ***
def generate_colors(self, total_colors):
colors = []
for color in range(total_colors):
h = float(color) / total_colors
rgb = tuple(int(i * 255) for i in self.colorsys.hsv_to_rgb(h,1,1))
colors += ['#%02x%02x%02x' % (int(rgb[0]), int(rgb[1]), int(rgb[2]))]
return colors
# Draw pngs
def output_images(self, image_id, cycles):
A=self.pgv.AGraph()
A.edge_attr['style']='bold'
A.edge_attr['len']='2'
A.node_attr['style']='filled, bold'
A.node_attr['shape']='circle'
A.node_attr['width']='.6'
A.node_attr['height']='.6'
A.node_attr['fixedsize']='true'
A.node_attr['fontsize']='10'
A.node_attr['fontname']='Helvetica-Bold'
for node, links in self.nodes.items():
for link in links:
A.add_edge(node,link)
# Get the color gradient in hex
total_colors = 3
colors = self.generate_colors(total_colors)
for color_i, group in enumerate(cycles):
_color = colors[color_i]
for node in group:
A.get_node(node).attr['fillcolor']= _color
A.write('{folder_name}/tmp.dot'.format(folder_name=self.settings['image_folder_name'])) # write to simple.dot
B=self.pgv.AGraph('{folder_name}/tmp.dot'.format(folder_name=self.settings['image_folder_name'])) # create a new graph from file
B.layout('sfdp') # layout with default (neato)
B.draw('{folder_name}/{file_name_prefix}_{file_n}.png'.format(
folder_name=self.settings['image_folder_name'],
file_n = image_id,
file_name_prefix = self.settings['image_file_name_prefix'],
)
)
# Create output folder for the graph image
def prepare_output_folder(self, folder_name, file_name_prefix):
if not self.os.path.exists(folder_name):
self.os.makedirs(folder_name)
else:
if self.settings['delete_previously_generated_images']:
# Careful, this will clean up old png files with our file prefix
# from the output directory we created.
files = self.glob.glob(folder_name + '/' + file_name_prefix + '*.png')
for f in files:
self.os.remove(f)
# Initialize our puzzle solver
def __init__(self, **settings):
# save settings in a property
self.settings = {}
for key, value in settings.iteritems():
self.settings[key] = value
self.nodes_per_group = 3
# init image creation handling, if specified in the settings
if 'create_images' in self.settings and self.settings['create_images']:
# import all modules related to generating the images
self.os = __import__('os')
self.glob = __import__('glob')
self.random = __import__('random')
self.pgv = __import__('pygraphviz')
self.colorsys = __import__('colorsys')
self.prepare_output_folder(self.settings['image_folder_name'], self.settings['image_file_name_prefix'])
self.nodes = self.settings['nodes']
def close(self):
self.log_status()
print 'Finished.'
def main():
# Create a node network, to start
octopuses = Octopus().create(0, complete=False) # create an octopus with a non closed cycle
for i in range(1,3):
octopuses.update(Octopus().create(i)) # create 2 other octopuses
# Create our puzzle solver
ng = CycleFinder(
# Create images
create_images = True,
# Default path where graph images will be created
image_folder_name = 'graphs',
# Filename prefix. Images will be postfixed with a number, eg: combination_0.png
image_file_name_prefix = 'combination',
# Clean up images folder before creating new batch
delete_previously_generated_images = True,
nodes = octopuses,
)
# Discriminate nodes that are part of the cycle
cycles = ng.get_cycles()
# Create an output graph
ng.output_images(1, cycles)
if __name__ == "__main__":
main()