From 97f1d1dd190a09ed1ab4f9c60c4c5de1cf138608 Mon Sep 17 00:00:00 2001 From: Bradley Ellert Date: Mon, 14 May 2018 17:00:51 -0700 Subject: [PATCH 01/26] Move maps() from demo.py to loader.py --- demo.py | 34 +++---------------- .../mmp_network/loader.py | 29 ++++++++++++++-- 2 files changed, 32 insertions(+), 31 deletions(-) diff --git a/demo.py b/demo.py index 7006f25..eb56847 100644 --- a/demo.py +++ b/demo.py @@ -15,30 +15,6 @@ import dwave_structural_imbalance_demo as sbdemo -def maps(): - Global = sbdemo.global_signed_social_network() - - # The Syria subregion - syria_groups = set() - for v, data in Global.nodes(data=True): - if 'map' not in data: - continue - if data['map'] in {'Syria', 'Aleppo'}: - syria_groups.add(v) - Syria = Global.subgraph(syria_groups) - - # The Iraq subregion - iraq_groups = set() - for v, data in Global.nodes(data=True): - if 'map' not in data: - continue - if data['map'] == 'Iraq': - iraq_groups.add(v) - Iraq = Global.subgraph(iraq_groups) - - return Global, Syria, Iraq - - def diagramDateRange(directory_name, start, end, graph, sampler, subsolver, subarea=None, subarea_name=None): # Create directories directory_path = os.path.join('Results', directory_name) @@ -96,18 +72,18 @@ def diagramDateRange(directory_name, start, end, graph, sampler, subsolver, suba subsolver = None # get the graphs - Global, Syria, Iraq = maps() + graphs = sbdemo.maps() # calculate the imbalance of Global - imbalance, bicoloring = dnx.structural_imbalance(Global, sampler, solver=subsolver) + imbalance, bicoloring = dnx.structural_imbalance(graphs['Global'], sampler, solver=subsolver) # draw the Global graph - sbdemo.draw('syria_imbalance.png', Global, imbalance, bicoloring) + sbdemo.draw('syria_imbalance.png', graphs['Global'], imbalance, bicoloring) # Images of the structural imbalance in the local Syrian SSN # for years 2010-2016 showing frustrated and unfrustrated edges. - diagramDateRange('Syrian Theatre', 2010, 2016 + 1, Syria, sampler, subsolver) + diagramDateRange('Syrian Theatre', 2010, 2016 + 1, graphs['Syria'], sampler, subsolver) # Images of the structural imbalance in the world SSN for # years 2007-2016 showing frustrated and unfrustrated edges. - diagramDateRange('World Network', 2007, 2016 + 1, Global, sampler, subsolver, Syria, 'Syria') + diagramDateRange('World Network', 2007, 2016 + 1, graphs['Global'], sampler, subsolver, graphs['Syria'], 'Syria') diff --git a/dwave_structural_imbalance_demo/mmp_network/loader.py b/dwave_structural_imbalance_demo/mmp_network/loader.py index 421b0c2..576ff52 100644 --- a/dwave_structural_imbalance_demo/mmp_network/loader.py +++ b/dwave_structural_imbalance_demo/mmp_network/loader.py @@ -7,11 +7,11 @@ def global_signed_social_network(): """Return the global network for the Militant's Mapping Project. - + Reference: Mapping Militant Organizations, Stanford University, last modified February 28, 2016, http://web.stanford.edu/group/mappingmilitants/cgi-bin/. - + Examples: >>> import dwave_structural_imbalance_demo as sbdemo >>> Global = global_signed_social_network() @@ -95,3 +95,28 @@ def global_signed_social_network(): S.nodes[group_id]['map'] = map_name return S + + +def maps(): + maps = dict() + maps['Global'] = global_signed_social_network() + + # The Syria subregion + syria_groups = set() + for v, data in maps['Global'].nodes(data=True): + if 'map' not in data: + continue + if data['map'] in {'Syria', 'Aleppo'}: + syria_groups.add(v) + maps['Syria'] = maps['Global'].subgraph(syria_groups) + + # The Iraq subregion + iraq_groups = set() + for v, data in maps['Global'].nodes(data=True): + if 'map' not in data: + continue + if data['map'] == 'Iraq': + iraq_groups.add(v) + maps['Iraq'] = maps['Global'].subgraph(iraq_groups) + + return maps From 41f9bea0c9f34aab111cd5012f98c949d3b1f6c0 Mon Sep 17 00:00:00 2001 From: Bradley Ellert Date: Mon, 14 May 2018 17:02:03 -0700 Subject: [PATCH 02/26] Add interface function to get map List of edges: (node1, node2, sign) --- dwave_structural_imbalance_demo/interfaces.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 dwave_structural_imbalance_demo/interfaces.py diff --git a/dwave_structural_imbalance_demo/interfaces.py b/dwave_structural_imbalance_demo/interfaces.py new file mode 100644 index 0000000..4aa13e7 --- /dev/null +++ b/dwave_structural_imbalance_demo/interfaces.py @@ -0,0 +1,12 @@ +from dwave_structural_imbalance_demo.mmp_network import maps + +def get_map(subregion='Global', year=None): + graphs = maps() + + graph = graphs[subregion] + + if year: + filtered_edges = ((u, v) for u, v, a in graph.edges(data=True) if a['event_date'].year <= year) + graph = graph.edge_subgraph(filtered_edges) + + return list(graph.edges(data='sign')) From 0c63cf6ddfe856bf86c83cebd1ae33aacd0a5c28 Mon Sep 17 00:00:00 2001 From: Bradley Ellert Date: Wed, 16 May 2018 10:06:43 -0700 Subject: [PATCH 03/26] Encapsulate interface in class --- dwave_structural_imbalance_demo/__init__.py | 1 + dwave_structural_imbalance_demo/interfaces.py | 79 +++++++++++++++++-- .../mmp_network/loader.py | 25 ------ 3 files changed, 72 insertions(+), 33 deletions(-) diff --git a/dwave_structural_imbalance_demo/__init__.py b/dwave_structural_imbalance_demo/__init__.py index dc68275..bfc14f9 100644 --- a/dwave_structural_imbalance_demo/__init__.py +++ b/dwave_structural_imbalance_demo/__init__.py @@ -1,2 +1,3 @@ +from dwave_structural_imbalance_demo.interfaces import * from dwave_structural_imbalance_demo.mmp_network import * from dwave_structural_imbalance_demo.drawing import * diff --git a/dwave_structural_imbalance_demo/interfaces.py b/dwave_structural_imbalance_demo/interfaces.py index 4aa13e7..f3220e7 100644 --- a/dwave_structural_imbalance_demo/interfaces.py +++ b/dwave_structural_imbalance_demo/interfaces.py @@ -1,12 +1,75 @@ -from dwave_structural_imbalance_demo.mmp_network import maps +from dwave_structural_imbalance_demo.mmp_network import global_signed_social_network -def get_map(subregion='Global', year=None): - graphs = maps() +import dwave_qbsolv as qbsolv - graph = graphs[subregion] +import dwave_networkx as dnx - if year: - filtered_edges = ((u, v) for u, v, a in graph.edges(data=True) if a['event_date'].year <= year) - graph = graph.edge_subgraph(filtered_edges) +try: + import dwave.system.samplers as dwsamplers + import dwave.system.composites as dwcomposites + _qpu = True +except ImportError: + _qpu = False - return list(graph.edges(data='sign')) +import dimod + + +class GlobalSignedSocialNetwork(object): + def __init__(self): + maps = dict() + maps['Global'] = global_signed_social_network() + + # The Syria subregion + syria_groups = set() + for v, data in maps['Global'].nodes(data=True): + if 'map' not in data: + continue + if data['map'] in {'Syria', 'Aleppo'}: + syria_groups.add(v) + maps['Syria'] = maps['Global'].subgraph(syria_groups) + + # The Iraq subregion + iraq_groups = set() + for v, data in maps['Global'].nodes(data=True): + if 'map' not in data: + continue + if data['map'] == 'Iraq': + iraq_groups.add(v) + maps['Iraq'] = maps['Global'].subgraph(iraq_groups) + + self.maps = maps + self.qbsolv = qbsolv.QBSolv() + if _qpu: + self.embedding_composite = dwcomposites.EmbeddingComposite(dwsamplers.DWaveSampler()) + self.exact_solver = dimod.ExactSolver() + + def get_graph(self, subregion='Global', year=None): + graph = self.maps[subregion] + if year: + filtered_edges = ((u, v) for u, v, a in graph.edges(data=True) if a['event_date'].year <= year) + graph = graph.edge_subgraph(filtered_edges) + return graph + + def get_weighted_edges(self, subregion='Global', year=None): + # from networkx import node_link_data + graph = self.get_graph(subregion, year) + return list(graph.edges(data='sign')) + + def solve_structural_imbalance(self, subregion='Global', year=None): + graph = self.get_graph(subregion, year) + print(subregion,year) + if _qpu: + try: + imbalance, bicoloring = dnx.structural_imbalance(graph, self.embedding_composite) + print("EmbeddingComposite") + except ValueError: + imbalance, bicoloring = dnx.structural_imbalance(graph, self.qbsolv, solver=self.embedding_composite) + print("QBSolv w/ EmbeddingComposite") + else: + if len(graph) < 20: + imbalance, bicoloring = dnx.structural_imbalance(graph, self.exact_solver) + print("ExactSolver") + else: + imbalance, bicoloring = dnx.structural_imbalance(graph, self.qbsolv, solver='tabu') + print("QBSolv w/o QPU") + return {'imbalance': imbalance, 'bicoloring': bicoloring} diff --git a/dwave_structural_imbalance_demo/mmp_network/loader.py b/dwave_structural_imbalance_demo/mmp_network/loader.py index 576ff52..87a33cb 100644 --- a/dwave_structural_imbalance_demo/mmp_network/loader.py +++ b/dwave_structural_imbalance_demo/mmp_network/loader.py @@ -95,28 +95,3 @@ def global_signed_social_network(): S.nodes[group_id]['map'] = map_name return S - - -def maps(): - maps = dict() - maps['Global'] = global_signed_social_network() - - # The Syria subregion - syria_groups = set() - for v, data in maps['Global'].nodes(data=True): - if 'map' not in data: - continue - if data['map'] in {'Syria', 'Aleppo'}: - syria_groups.add(v) - maps['Syria'] = maps['Global'].subgraph(syria_groups) - - # The Iraq subregion - iraq_groups = set() - for v, data in maps['Global'].nodes(data=True): - if 'map' not in data: - continue - if data['map'] == 'Iraq': - iraq_groups.add(v) - maps['Iraq'] = maps['Global'].subgraph(iraq_groups) - - return maps From dd3457e9939e9e4e6b60d77bd3eabe02e8a17367 Mon Sep 17 00:00:00 2001 From: Bradley Ellert Date: Wed, 16 May 2018 10:13:31 -0700 Subject: [PATCH 04/26] Change graph representation Use networkx's built-in node_link_data --- dwave_structural_imbalance_demo/interfaces.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/dwave_structural_imbalance_demo/interfaces.py b/dwave_structural_imbalance_demo/interfaces.py index f3220e7..49fe610 100644 --- a/dwave_structural_imbalance_demo/interfaces.py +++ b/dwave_structural_imbalance_demo/interfaces.py @@ -1,8 +1,9 @@ -from dwave_structural_imbalance_demo.mmp_network import global_signed_social_network +import networkx as nx +import dwave_networkx as dnx import dwave_qbsolv as qbsolv -import dwave_networkx as dnx +from dwave_structural_imbalance_demo.mmp_network import global_signed_social_network try: import dwave.system.samplers as dwsamplers @@ -50,14 +51,13 @@ def get_graph(self, subregion='Global', year=None): graph = graph.edge_subgraph(filtered_edges) return graph - def get_weighted_edges(self, subregion='Global', year=None): - # from networkx import node_link_data + def get_node_link_data(self, subregion='Global', year=None): graph = self.get_graph(subregion, year) - return list(graph.edges(data='sign')) + return nx.node_link_data(graph) def solve_structural_imbalance(self, subregion='Global', year=None): graph = self.get_graph(subregion, year) - print(subregion,year) + print(subregion, year) if _qpu: try: imbalance, bicoloring = dnx.structural_imbalance(graph, self.embedding_composite) From e460fbbb1d20321b934686586658c466104ea9dd Mon Sep 17 00:00:00 2001 From: Bradley Ellert Date: Wed, 16 May 2018 13:44:33 -0700 Subject: [PATCH 05/26] Update drawing function Accept node_link_data Draw whether it's been solved or not --- dwave_structural_imbalance_demo/drawing.py | 57 +++++++++++++++------- 1 file changed, 39 insertions(+), 18 deletions(-) diff --git a/dwave_structural_imbalance_demo/drawing.py b/dwave_structural_imbalance_demo/drawing.py index 33ab65a..2ec4080 100644 --- a/dwave_structural_imbalance_demo/drawing.py +++ b/dwave_structural_imbalance_demo/drawing.py @@ -1,39 +1,56 @@ +from collections import defaultdict +from itertools import product + import networkx as nx import matplotlib.pyplot as plt -def draw(filename, S, imbalance, coloring, position=None): +def draw(filename, node_link_data, position=None): """Plot the given signed social network. Args: filename: The name of the file to be generated. - S: The network - imbalance: The set of frustrated edges. - coloring: A two-coloring of the network + node_link_data: The network represented as returned by nx.node_link_data() position (optional): The position for the nodes. """ - if position is None: - position = nx.circular_layout(S) + S = nx.node_link_graph(node_link_data) # we need a consistent ordering of the edges edgelist = S.edges() nodelist = S.nodes() + if position is None: + try: + # group bipartition if nodes are colored + dist = defaultdict(dict) + for u, v in product(nodelist, repeat=2): + if u == v: # node has no distance from itself + dist[u][v] = 0 + elif nodelist[u]['color'] == nodelist[v]['color']: # make same color nodes closer together + dist[u][v] = 1 + else: # make different color nodes further apart + dist[u][v] = 2 + position = nx.kamada_kawai_layout(S, dist) + except KeyError: + # default to circular layout if nodes aren't colored + position = nx.circular_layout(S) + # get the colors assigned to each edge based on friendly/hostile - sign_edge_color = [-S[u][v]['sign'] for u, v in edgelist] - sign_node_color = [0 for __ in nodelist] - sign_edge_style = ['solid' if S[u][v]['sign'] > 0 else 'dotted' for u, v in edgelist] + sign_edge_color = [S[u][v]['sign'] for u, v in edgelist] # get the colors assigned to each node by coloring - coloring_node_color = [-2 * coloring[v] + 1 for v in nodelist] + try: + coloring_node_color = [-2 * nodelist[v]['color'] + 1 for v in nodelist] + except KeyError: + coloring_node_color = [0 for __ in nodelist] - # get the colors of the violated edges - c1 = 1 # grey - c2 = .61 # ochre - coloring_edge_color = [c2 if (u, v) in imbalance else c1 for u, v in edgelist] - edge_violation_style = ['dotted' if (u, v) in imbalance else 'solid' for u, v in edgelist] + # get the styles of the violated edges + try: + edge_violation_style = ['dotted' if S[u][v]['frustrated'] else 'solid' for u, v in edgelist] + except KeyError: + edge_violation_style = ['solid' for __ in edgelist] # draw the the coloring nx.draw(S, @@ -45,9 +62,13 @@ def draw(filename, S, imbalance, coloring, position=None): edgelist=edgelist, nodelist=nodelist, width=2, node_color=coloring_node_color, - edge_color=coloring_edge_color, - edge_cmap=plt.get_cmap('nipy_spectral'), + edge_color=sign_edge_color, + edge_cmap=plt.get_cmap('RdYlGn'), + edgecolors='black', + style=edge_violation_style ) plt.savefig(filename, facecolor='white', dpi=500) - plt.clf() \ No newline at end of file + plt.clf() + + return position From 8f025d3b86a0886712a95cad3c8c592ca2044eae Mon Sep 17 00:00:00 2001 From: Bradley Ellert Date: Wed, 16 May 2018 16:08:09 -0700 Subject: [PATCH 06/26] Update demo to use new interface --- demo.py | 77 ++++++++----------- dwave_structural_imbalance_demo/interfaces.py | 45 ++++++----- 2 files changed, 60 insertions(+), 62 deletions(-) diff --git a/demo.py b/demo.py index eb56847..0a57264 100644 --- a/demo.py +++ b/demo.py @@ -1,49 +1,41 @@ from __future__ import division import os -import dwave_qbsolv as qbsolv - -import dwave_networkx as dnx - -try: - import dwave.system.samplers as dwsamplers - import dwave.system.composites as dwcomposites - _qpu = True -except ImportError: - _qpu = False - import dwave_structural_imbalance_demo as sbdemo -def diagramDateRange(directory_name, start, end, graph, sampler, subsolver, subarea=None, subarea_name=None): +def diagramDateRange(gssn, graph_name, start, end, subarea_name=None): # Create directories - directory_path = os.path.join('Results', directory_name) + directory_path = os.path.join('Results', graph_name) if not os.path.exists(directory_path): os.makedirs(directory_path) # Write .csv header line - csv = open(os.path.join(directory_path, 'Structural Imbalance.csv'), 'w') + csv_name = 'Structural Imbalance.csv' + csv_path = os.path.join(directory_path, 'Structural Imbalance.csv') + csv = open(csv_path, 'w') csv.write('year,total groups,total edges,imbalanced edges,structural imbalance') - if subarea is not None and subarea_name is not None: + if subarea_name is not None: csv.write(',%s groups,%s edges,%s imbalanced edges,%s structural imbalance' % tuple([subarea_name] * 4)) csv.write('\n') for year in range(start, end): # Compute structural imbalance up to and including year - filtered_edges = ((u, v) for u, v, a in graph.edges(data=True) if a['event_date'].year <= year) - subrange = graph.edge_subgraph(filtered_edges) - imbalance, bicoloring = dnx.structural_imbalance(subrange, sampler, solver=subsolver) + nld_subrange = gssn.get_node_link_data(graph_name, year) + nld_subrange_solved = gssn.solve_structural_imbalance(graph_name, year) # Write stats to .csv - num_nodes = len(subrange.nodes) - num_edges = len(subrange.edges) - num_imbalanced = len(imbalance) + num_nodes = len(nld_subrange['nodes']) + num_edges = len(nld_subrange['links']) + num_imbalanced = sum([edge['frustrated'] for edge in nld_subrange_solved['links']]) ratio = num_imbalanced / num_edges csv.write('%s,%s,%s,%s,%s' % (year, num_nodes, num_edges, num_imbalanced, ratio)) - if subarea is not None and subarea_name is not None: - num_nodes = len(set.intersection(set(subrange.nodes), set(subarea.nodes))) - num_edges = len(set.intersection(set(subrange.edges), set(subarea.edges))) - num_imbalanced = len(set.intersection(set(imbalance), set(subarea.edges))) + if subarea_name is not None: + nld_subarea = gssn.get_node_link_data(subarea_name, year) + num_nodes = len([node for node in nld_subrange['nodes'] if node in nld_subarea['nodes']]) + event_ids = [edge['event_id'] for edge in nld_subrange['links'] if edge in nld_subarea['links']] + num_edges = len(event_ids) + num_imbalanced = len([edge['event_id'] for edge in nld_subrange_solved['links'] if edge['event_id'] in event_ids]) if num_edges != 0: ratio = num_imbalanced / num_edges else: @@ -54,36 +46,35 @@ def diagramDateRange(directory_name, start, end, graph, sampler, subsolver, suba # Draw graph file_name = 'Structural Imbalance %s.png' % year file_path = os.path.join(directory_path, file_name) - sbdemo.draw(file_path, subrange, imbalance, bicoloring) - print('output %s' % file_path) + sbdemo.draw(file_path, nld_subrange_solved) + print('Output %s\n' % file_path) + print('Output %s' % csv_path) csv.close() if __name__ == '__main__': - # get a sampler - sampler = qbsolv.QBSolv() - - if _qpu: - subsolver = dwcomposites.EmbeddingComposite(dwsamplers.DWaveSampler()) - print("Running on the QPU") - else: - print("Running classically") - subsolver = None - # get the graphs - graphs = sbdemo.maps() + gssn = sbdemo.GlobalSignedSocialNetwork() + + # draw Global graph before solving; save node layout for reuse + nld_global = gssn.get_node_link_data() + position = sbdemo.draw('global.png', nld_global) + print('Output %s' % 'global.png') # calculate the imbalance of Global - imbalance, bicoloring = dnx.structural_imbalance(graphs['Global'], sampler, solver=subsolver) + nld_global_solved = gssn.solve_structural_imbalance() - # draw the Global graph - sbdemo.draw('syria_imbalance.png', graphs['Global'], imbalance, bicoloring) + # draw the Global graph; reusing the above layout, and calculating a new grouped layout + sbdemo.draw('global_imbalance.png', nld_global_solved, position) + print('Output %s' % 'global_imbalance.png') + sbdemo.draw('global_imbalance_grouped', nld_global_solved) + print('Output %s' % 'global_imbalance_grouped\n') # Images of the structural imbalance in the local Syrian SSN # for years 2010-2016 showing frustrated and unfrustrated edges. - diagramDateRange('Syrian Theatre', 2010, 2016 + 1, graphs['Syria'], sampler, subsolver) + diagramDateRange(gssn, 'Syria', 2010, 2016 + 1) # Images of the structural imbalance in the world SSN for # years 2007-2016 showing frustrated and unfrustrated edges. - diagramDateRange('World Network', 2007, 2016 + 1, graphs['Global'], sampler, subsolver, graphs['Syria'], 'Syria') + diagramDateRange(gssn, 'Global', 2007, 2016 + 1, 'Syria') diff --git a/dwave_structural_imbalance_demo/interfaces.py b/dwave_structural_imbalance_demo/interfaces.py index 49fe610..fbc77c6 100644 --- a/dwave_structural_imbalance_demo/interfaces.py +++ b/dwave_structural_imbalance_demo/interfaces.py @@ -44,32 +44,39 @@ def __init__(self): self.embedding_composite = dwcomposites.EmbeddingComposite(dwsamplers.DWaveSampler()) self.exact_solver = dimod.ExactSolver() - def get_graph(self, subregion='Global', year=None): - graph = self.maps[subregion] + def _get_graph(self, subregion='Global', year=None): + G = self.maps[subregion] if year: - filtered_edges = ((u, v) for u, v, a in graph.edges(data=True) if a['event_date'].year <= year) - graph = graph.edge_subgraph(filtered_edges) - return graph + filtered_edges = ((u, v) for u, v, a in G.edges(data=True) if a['event_date'].year <= year) + G = G.edge_subgraph(filtered_edges) + return G def get_node_link_data(self, subregion='Global', year=None): - graph = self.get_graph(subregion, year) - return nx.node_link_data(graph) + G = self._get_graph(subregion, year) + return nx.node_link_data(G) def solve_structural_imbalance(self, subregion='Global', year=None): - graph = self.get_graph(subregion, year) - print(subregion, year) + G = self._get_graph(subregion, year) + if _qpu: try: - imbalance, bicoloring = dnx.structural_imbalance(graph, self.embedding_composite) - print("EmbeddingComposite") + imbalance, bicoloring = dnx.structural_imbalance(G, self.embedding_composite) + print("Ran on the QPU using EmbeddingComposite") except ValueError: - imbalance, bicoloring = dnx.structural_imbalance(graph, self.qbsolv, solver=self.embedding_composite) - print("QBSolv w/ EmbeddingComposite") + imbalance, bicoloring = dnx.structural_imbalance(G, self.qbsolv, solver=self.embedding_composite) + print("Ran on the QPU using QBSolv w/ EmbeddingComposite") else: - if len(graph) < 20: - imbalance, bicoloring = dnx.structural_imbalance(graph, self.exact_solver) - print("ExactSolver") + if len(G) < 20: + imbalance, bicoloring = dnx.structural_imbalance(G, self.exact_solver) + print("Ran classically using ExactSolver") else: - imbalance, bicoloring = dnx.structural_imbalance(graph, self.qbsolv, solver='tabu') - print("QBSolv w/o QPU") - return {'imbalance': imbalance, 'bicoloring': bicoloring} + imbalance, bicoloring = dnx.structural_imbalance(G, self.qbsolv, solver='tabu') + print("Ran classically using QBSolv") + + G = G.copy() + for edge in G.edges: + G.edges[edge]['frustrated'] = edge in imbalance + for node in G.nodes: + G.nodes[node]['color'] = bicoloring[node] + + return nx.node_link_data(G) From cfc18ebdd6ea31bf61067ee75d3810c064693acd Mon Sep 17 00:00:00 2001 From: Bradley Ellert Date: Wed, 16 May 2018 17:18:09 -0700 Subject: [PATCH 07/26] Add draft jsonschema --- dwave_structural_imbalance_demo/__init__.py | 1 + .../json_schema.json | 77 +++++++++++++++++++ .../json_schema.py | 19 +++++ 3 files changed, 97 insertions(+) create mode 100644 dwave_structural_imbalance_demo/json_schema.json create mode 100644 dwave_structural_imbalance_demo/json_schema.py diff --git a/dwave_structural_imbalance_demo/__init__.py b/dwave_structural_imbalance_demo/__init__.py index bfc14f9..6c7e21c 100644 --- a/dwave_structural_imbalance_demo/__init__.py +++ b/dwave_structural_imbalance_demo/__init__.py @@ -1,3 +1,4 @@ from dwave_structural_imbalance_demo.interfaces import * from dwave_structural_imbalance_demo.mmp_network import * from dwave_structural_imbalance_demo.drawing import * +from json_schema import * diff --git a/dwave_structural_imbalance_demo/json_schema.json b/dwave_structural_imbalance_demo/json_schema.json new file mode 100644 index 0000000..d4d8c2d --- /dev/null +++ b/dwave_structural_imbalance_demo/json_schema.json @@ -0,0 +1,77 @@ +{ + "title": "Node Link Data", + "version": "1.0.0", + "type": "object", + "required": [ + "nodes", + "links" + ], + "properties": { + "nodes": { + "type": "array", + "items": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "number" + }, + "map": { + "type": "string" + }, + "color": { + "type": "number" + } + } + } + }, + "links": { + "type": "array", + "items": { + "type": "object", + "required": [ + "source", + "target", + "sign" + ], + "properties": { + "source": { + "type": "number" + }, + "target": { + "type": "number" + }, + "sign": { + "type": "number" + }, + "event_id": { + "type": "string" + }, + "event_type": { + "type": "string" + }, + "event_description": { + "type": "string" + }, + "event_date": { + "type": "datetime.date" + }, + "frustrated": { + "type": "boolean" + } + } + } + }, + "graph": { + "type": "object" + }, + "directed": { + "type": "boolean" + }, + "multigraph": { + "type": "boolean" + } + } +} \ No newline at end of file diff --git a/dwave_structural_imbalance_demo/json_schema.py b/dwave_structural_imbalance_demo/json_schema.py new file mode 100644 index 0000000..692d748 --- /dev/null +++ b/dwave_structural_imbalance_demo/json_schema.py @@ -0,0 +1,19 @@ +# Copyright 2018 D-Wave Systems Inc. + +# Licensed under the Apache License, Version 2.0 (the "License") +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http: // www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from pkg_resources import resource_filename +import json + +with open(resource_filename(__name__, 'json_schema.json'), 'r') as schema_file: + json_schema = json.load(schema_file) From 955ab478c53166a121baa42018a4b38b9bd5b7a5 Mon Sep 17 00:00:00 2001 From: Bradley Ellert Date: Thu, 17 May 2018 10:47:36 -0700 Subject: [PATCH 08/26] Change date to string instead of datetime.date Conform to json --- dwave_structural_imbalance_demo/json_schema.json | 2 +- dwave_structural_imbalance_demo/mmp_network/loader.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dwave_structural_imbalance_demo/json_schema.json b/dwave_structural_imbalance_demo/json_schema.json index d4d8c2d..ff5027c 100644 --- a/dwave_structural_imbalance_demo/json_schema.json +++ b/dwave_structural_imbalance_demo/json_schema.json @@ -56,7 +56,7 @@ "type": "string" }, "event_date": { - "type": "datetime.date" + "type": "string" }, "frustrated": { "type": "boolean" diff --git a/dwave_structural_imbalance_demo/mmp_network/loader.py b/dwave_structural_imbalance_demo/mmp_network/loader.py index 87a33cb..71c7788 100644 --- a/dwave_structural_imbalance_demo/mmp_network/loader.py +++ b/dwave_structural_imbalance_demo/mmp_network/loader.py @@ -57,7 +57,7 @@ def global_signed_social_network(): # fill out the datafield data = {'event_id': id_, 'event_type': type_, - 'event_date': dateinfo, + 'event_date': date, # don't use datetime to conform with json 'event_description': description} # finally cast the different relation types to either hostile (sign=-1) From 039ed5dec246005e28833448b74189a651e57697 Mon Sep 17 00:00:00 2001 From: Bradley Ellert Date: Thu, 17 May 2018 11:05:43 -0700 Subject: [PATCH 09/26] Only store year out of date Logic to handle improperly formatted dates forces strange assumptions Only year is needed for filtering --- dwave_structural_imbalance_demo/interfaces.py | 2 +- dwave_structural_imbalance_demo/json_schema.json | 6 +++--- dwave_structural_imbalance_demo/mmp_network/loader.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/dwave_structural_imbalance_demo/interfaces.py b/dwave_structural_imbalance_demo/interfaces.py index fbc77c6..ea52a91 100644 --- a/dwave_structural_imbalance_demo/interfaces.py +++ b/dwave_structural_imbalance_demo/interfaces.py @@ -47,7 +47,7 @@ def __init__(self): def _get_graph(self, subregion='Global', year=None): G = self.maps[subregion] if year: - filtered_edges = ((u, v) for u, v, a in G.edges(data=True) if a['event_date'].year <= year) + filtered_edges = ((u, v) for u, v, a in G.edges(data=True) if a['event_year'] <= year) G = G.edge_subgraph(filtered_edges) return G diff --git a/dwave_structural_imbalance_demo/json_schema.json b/dwave_structural_imbalance_demo/json_schema.json index ff5027c..43d7ef7 100644 --- a/dwave_structural_imbalance_demo/json_schema.json +++ b/dwave_structural_imbalance_demo/json_schema.json @@ -55,8 +55,8 @@ "event_description": { "type": "string" }, - "event_date": { - "type": "string" + "event_year": { + "type": "number" }, "frustrated": { "type": "boolean" @@ -74,4 +74,4 @@ "type": "boolean" } } -} \ No newline at end of file +} diff --git a/dwave_structural_imbalance_demo/mmp_network/loader.py b/dwave_structural_imbalance_demo/mmp_network/loader.py index 71c7788..080d045 100644 --- a/dwave_structural_imbalance_demo/mmp_network/loader.py +++ b/dwave_structural_imbalance_demo/mmp_network/loader.py @@ -57,7 +57,7 @@ def global_signed_social_network(): # fill out the datafield data = {'event_id': id_, 'event_type': type_, - 'event_date': date, # don't use datetime to conform with json + 'event_year': int(year), # whole date isn't needed 'event_description': description} # finally cast the different relation types to either hostile (sign=-1) From c9688a11978257be147165fcb1943f30c9739c94 Mon Sep 17 00:00:00 2001 From: Bradley Ellert Date: Thu, 17 May 2018 12:04:40 -0700 Subject: [PATCH 10/26] Add descriptions to relevant fields --- dwave_structural_imbalance_demo/json_schema.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/dwave_structural_imbalance_demo/json_schema.json b/dwave_structural_imbalance_demo/json_schema.json index 43d7ef7..a6aca4c 100644 --- a/dwave_structural_imbalance_demo/json_schema.json +++ b/dwave_structural_imbalance_demo/json_schema.json @@ -1,6 +1,6 @@ { "title": "Node Link Data", - "version": "1.0.0", + "description": "NetworkX graph data; returned by nx.node_link_data()", "type": "object", "required": [ "nodes", @@ -8,6 +8,7 @@ ], "properties": { "nodes": { + "description": "vertices in graph representing groups", "type": "array", "items": { "type": "object", @@ -16,18 +17,21 @@ ], "properties": { "id": { + "description": "marks unique identifier of group; corresponds to source and target in links", "type": "number" }, "map": { "type": "string" }, "color": { + "description": "group bicoloring; returned by solve_structural_imbalance()", "type": "number" } } } }, "links": { + "desciption": "edges in graph respresenting interactions between groups", "type": "array", "items": { "type": "object", @@ -38,12 +42,15 @@ ], "properties": { "source": { + "description": "marks one group in interaction; corresponds to id in nodes", "type": "number" }, "target": { + "description": "marks one group in interaction; corresponds to id in nodes", "type": "number" }, "sign": { + "description": "marks whether interaction is friendly or hostile; input to solve_structural_imbalance()", "type": "number" }, "event_id": { @@ -59,6 +66,7 @@ "type": "number" }, "frustrated": { + "description": "interactions that violate sign; returned by solve_structural_imbalance()", "type": "boolean" } } From a7412316d3cd1d486a879546780737422d451e8f Mon Sep 17 00:00:00 2001 From: Bradley Ellert Date: Thu, 17 May 2018 12:18:44 -0700 Subject: [PATCH 11/26] Add CI boilerplate --- .circleci/config.yml | 142 +++++++++++++++++++++++++++++++++++++++++ tests/__init__.py | 0 tests/requirements.txt | 4 ++ 3 files changed, 146 insertions(+) create mode 100644 .circleci/config.yml create mode 100644 tests/__init__.py create mode 100644 tests/requirements.txt diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..dd976d4 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,142 @@ +version: 2 +jobs: + test-3.6: &full-test-template + docker: + - image: circleci/python:3.6-jessie + + working_directory: ~/repo + + steps: + + - checkout + + - restore_cache: + keys: + - v1-dependencies-{{ checksum "tests/requirements.txt" }}-{{ .Environment.CIRCLE_JOB }} + + - run: + name: create virtualenv + command: | + python -m virtualenv env + + - run: &install-dependencies-template + name: install dependencies + command: | + . env/bin/activate + python --version + pip install -r tests/requirements.txt --extra-index-url https://pypi.dwavesys.com/simple + + - save_cache: + paths: + - ./env + key: v1-dependencies-{{ checksum "tests/requirements.txt" }}-{{ .Environment.CIRCLE_JOB }} + + - run: &run-tests-template + name: run unittests + command: | + . env/bin/activate + python --version + coverage run -m unittest discover + + test-3.5: + <<: *full-test-template + docker: + - image: circleci/python:3.5-jessie + + test-3.4: + <<: *full-test-template + docker: + - image: circleci/python:3.4-jessie + + test-2.7: + <<: *full-test-template + docker: + - image: circleci/python:2.7-jessie + + test-osx: + macos: + xcode: "9.0" + + working_directory: ~/repo + + steps: + - checkout + + - run: + name: install pyenv + command: | + brew install pyenv + + - restore_cache: + keys: + - pyenv-versions-{{ .Environment.CIRCLE_JOB }} + - dependencies2.7-{{ checksum "tests/requirements.txt" }}-{{ .Environment.CIRCLE_JOB }} + + - run: + name: install python 2.7.14 and 3.6.5 with pyenv + command: | + pyenv install 2.7.14 -s + pyenv install 3.6.5 -s + + - save_cache: + paths: + - ~/.pyenv + key: pyenv-versions-{{ .Environment.CIRCLE_JOB }} + + - run: + name: create virtualenv for 2.7.14 + command: | + eval "$(pyenv init -)" + pyenv local 2.7.14 + python -m pip install virtualenv + python -m virtualenv env + + - run: *install-dependencies-template + + - save_cache: + paths: + - ./env + key: dependencies2.7-{{ checksum "tests/requirements.txt" }}-{{ .Environment.CIRCLE_JOB }} + + - run: *run-tests-template + + - run: + name: clear virtual environment + command: | + rm -rf env + + - restore_cache: + keys: + - dependencies3.6-{{ checksum "tests/requirements.txt" }}-{{ .Environment.CIRCLE_JOB }} + + - run: + name: create virtualenv for 3.6.5 + command: | + eval "$(pyenv init -)" + pyenv local 3.6.5 + python -m pip install virtualenv + python -m virtualenv env + + - run: *install-dependencies-template + + - save_cache: + paths: + - ./env + key: dependencies3.6-{{ checksum "tests/requirements.txt" }}-{{ .Environment.CIRCLE_JOB }} + + - run: *run-tests-template + +workflows: + version: 2 + test: + jobs: + - test-3.6 + - test-2.7 + - test-3.5 + - test-3.4 + - test-osx: + requires: + - test-3.6 + - test-2.7 + - test-3.5 + - test-3.4 diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/requirements.txt b/tests/requirements.txt new file mode 100644 index 0000000..131c54e --- /dev/null +++ b/tests/requirements.txt @@ -0,0 +1,4 @@ +-r ../requirements.txt + +coverage +coveralls From 7b9ff1d8e1411eb74a018683e1169f349d743385 Mon Sep 17 00:00:00 2001 From: Bradley Ellert Date: Thu, 17 May 2018 12:22:29 -0700 Subject: [PATCH 12/26] Make gssn.maps a `_` member Should only be accessed through _get_graph --- dwave_structural_imbalance_demo/interfaces.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dwave_structural_imbalance_demo/interfaces.py b/dwave_structural_imbalance_demo/interfaces.py index ea52a91..09e76ab 100644 --- a/dwave_structural_imbalance_demo/interfaces.py +++ b/dwave_structural_imbalance_demo/interfaces.py @@ -38,14 +38,14 @@ def __init__(self): iraq_groups.add(v) maps['Iraq'] = maps['Global'].subgraph(iraq_groups) - self.maps = maps + self._maps = maps self.qbsolv = qbsolv.QBSolv() if _qpu: self.embedding_composite = dwcomposites.EmbeddingComposite(dwsamplers.DWaveSampler()) self.exact_solver = dimod.ExactSolver() def _get_graph(self, subregion='Global', year=None): - G = self.maps[subregion] + G = self._maps[subregion] if year: filtered_edges = ((u, v) for u, v, a in G.edges(data=True) if a['event_year'] <= year) G = G.edge_subgraph(filtered_edges) From d6fa59b988686347906a5c2e64f158c6c13cbd67 Mon Sep 17 00:00:00 2001 From: Bradley Ellert Date: Thu, 17 May 2018 13:00:02 -0700 Subject: [PATCH 13/26] Explicitly check year against None Handle case where year=0 --- dwave_structural_imbalance_demo/interfaces.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dwave_structural_imbalance_demo/interfaces.py b/dwave_structural_imbalance_demo/interfaces.py index 09e76ab..39f957d 100644 --- a/dwave_structural_imbalance_demo/interfaces.py +++ b/dwave_structural_imbalance_demo/interfaces.py @@ -46,7 +46,7 @@ def __init__(self): def _get_graph(self, subregion='Global', year=None): G = self._maps[subregion] - if year: + if year is not None: filtered_edges = ((u, v) for u, v, a in G.edges(data=True) if a['event_year'] <= year) G = G.edge_subgraph(filtered_edges) return G From beeefc4f5d3f6fe8bf7711c7e16557c4a987600a Mon Sep 17 00:00:00 2001 From: Bradley Ellert Date: Thu, 17 May 2018 13:04:25 -0700 Subject: [PATCH 14/26] Add check for year being an int --- dwave_structural_imbalance_demo/interfaces.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dwave_structural_imbalance_demo/interfaces.py b/dwave_structural_imbalance_demo/interfaces.py index 39f957d..78da85c 100644 --- a/dwave_structural_imbalance_demo/interfaces.py +++ b/dwave_structural_imbalance_demo/interfaces.py @@ -47,6 +47,8 @@ def __init__(self): def _get_graph(self, subregion='Global', year=None): G = self._maps[subregion] if year is not None: + if not isinstance(year, int): + raise ValueError("year must be int") filtered_edges = ((u, v) for u, v, a in G.edges(data=True) if a['event_year'] <= year) G = G.edge_subgraph(filtered_edges) return G From b608d2f71e7e145c11fbbd32a7cc3c76ba680177 Mon Sep 17 00:00:00 2001 From: Bradley Ellert Date: Thu, 17 May 2018 16:04:43 -0700 Subject: [PATCH 15/26] Add tests --- tests/test_interfaces.py | 55 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 tests/test_interfaces.py diff --git a/tests/test_interfaces.py b/tests/test_interfaces.py new file mode 100644 index 0000000..46f3fa3 --- /dev/null +++ b/tests/test_interfaces.py @@ -0,0 +1,55 @@ +# Copyright 2018 D-Wave Systems Inc. + +# Licensed under the Apache License, Version 2.0 (the "License") +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http: // www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest +from random import randint, choice +import jsonschema + +from dwave_structural_imbalance_demo.interfaces import GlobalSignedSocialNetwork +from dwave_structural_imbalance_demo.json_schema import json_schema + + +class TestInterfaces(unittest.TestCase): + gssn = GlobalSignedSocialNetwork() + + def test_get_node_link_data_output(self): + subgroup = choice(['Global', 'Syria', 'Iraq']) + year = randint(2007, 2016) + output = self.gssn.get_node_link_data(subgroup, year) + jsonschema.validate(output, json_schema) + + def test_solve_structural_imbalance_output(self): + subgroup = 'Syria' + year = 2013 + output = self.gssn.solve_structural_imbalance(subgroup, year) + jsonschema.validate(output, json_schema) + + def test_invalid_subgroup(self): + subgroup = 'unknown_subgroup' + self.assertRaises(KeyError, self.gssn.get_node_link_data, subgroup) + self.assertRaises(KeyError, self.gssn.solve_structural_imbalance, subgroup) + + def test_invalid_year(self): + year = 'not_an_integer' + self.assertRaises(ValueError, self.gssn.get_node_link_data, year=year) + self.assertRaises(ValueError, self.gssn.solve_structural_imbalance, year=year) + + def test_year_zero(self): + year = 0 + output = self.gssn.get_node_link_data(year=year) + self.assertFalse(output['nodes']) + self.assertFalse(output['links']) + output = self.gssn.solve_structural_imbalance(year=year) + self.assertFalse(output['nodes']) + self.assertFalse(output['links']) From d559e0dcac3b4634e292ebaf9e579feafb605f41 Mon Sep 17 00:00:00 2001 From: Bradley Ellert Date: Thu, 17 May 2018 16:16:54 -0700 Subject: [PATCH 16/26] Split out test_year_zero This part requires QPU ExactSolver returns 0 samples on empty input dnx.structural_imbalance() fails --- tests/requirements.txt | 2 +- tests/test_interfaces.py | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/requirements.txt b/tests/requirements.txt index 131c54e..211d207 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1,4 +1,4 @@ --r ../requirements.txt +-r ../requirements_cpu.txt coverage coveralls diff --git a/tests/test_interfaces.py b/tests/test_interfaces.py index 46f3fa3..21e5097 100644 --- a/tests/test_interfaces.py +++ b/tests/test_interfaces.py @@ -16,6 +16,12 @@ from random import randint, choice import jsonschema +try: + import dwave.system + _qpu = True +except ImportError: + _qpu = False + from dwave_structural_imbalance_demo.interfaces import GlobalSignedSocialNetwork from dwave_structural_imbalance_demo.json_schema import json_schema @@ -50,6 +56,10 @@ def test_year_zero(self): output = self.gssn.get_node_link_data(year=year) self.assertFalse(output['nodes']) self.assertFalse(output['links']) + + @unittest.skipIf(not _qpu, "Requires access to QPU via dwave-system") + def test_year_zero_qpu(self): + year = 0 output = self.gssn.solve_structural_imbalance(year=year) self.assertFalse(output['nodes']) self.assertFalse(output['links']) From 13a20269177b336210198833ed029c0fdb0ad5e0 Mon Sep 17 00:00:00 2001 From: Bradley Ellert Date: Fri, 18 May 2018 10:02:35 -0700 Subject: [PATCH 17/26] Add option to override QPU access --- dwave_structural_imbalance_demo/interfaces.py | 22 ++++++++++--------- tests/test_interfaces.py | 12 +++++----- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/dwave_structural_imbalance_demo/interfaces.py b/dwave_structural_imbalance_demo/interfaces.py index 78da85c..6ce4d95 100644 --- a/dwave_structural_imbalance_demo/interfaces.py +++ b/dwave_structural_imbalance_demo/interfaces.py @@ -16,7 +16,7 @@ class GlobalSignedSocialNetwork(object): - def __init__(self): + def __init__(self, qpu=_qpu): maps = dict() maps['Global'] = global_signed_social_network() @@ -39,10 +39,12 @@ def __init__(self): maps['Iraq'] = maps['Global'].subgraph(iraq_groups) self._maps = maps - self.qbsolv = qbsolv.QBSolv() - if _qpu: - self.embedding_composite = dwcomposites.EmbeddingComposite(dwsamplers.DWaveSampler()) - self.exact_solver = dimod.ExactSolver() + self._qpu = qpu + self._qbsolv = qbsolv.QBSolv() + if qpu: + self._embedding_composite = dwcomposites.EmbeddingComposite(dwsamplers.DWaveSampler()) + else: + self._exact_solver = dimod.ExactSolver() def _get_graph(self, subregion='Global', year=None): G = self._maps[subregion] @@ -60,19 +62,19 @@ def get_node_link_data(self, subregion='Global', year=None): def solve_structural_imbalance(self, subregion='Global', year=None): G = self._get_graph(subregion, year) - if _qpu: + if self._qpu: try: - imbalance, bicoloring = dnx.structural_imbalance(G, self.embedding_composite) + imbalance, bicoloring = dnx.structural_imbalance(G, self._embedding_composite) print("Ran on the QPU using EmbeddingComposite") except ValueError: - imbalance, bicoloring = dnx.structural_imbalance(G, self.qbsolv, solver=self.embedding_composite) + imbalance, bicoloring = dnx.structural_imbalance(G, self._qbsolv, solver=self._embedding_composite) print("Ran on the QPU using QBSolv w/ EmbeddingComposite") else: if len(G) < 20: - imbalance, bicoloring = dnx.structural_imbalance(G, self.exact_solver) + imbalance, bicoloring = dnx.structural_imbalance(G, self._exact_solver) print("Ran classically using ExactSolver") else: - imbalance, bicoloring = dnx.structural_imbalance(G, self.qbsolv, solver='tabu') + imbalance, bicoloring = dnx.structural_imbalance(G, self._qbsolv, solver='tabu') print("Ran classically using QBSolv") G = G.copy() diff --git a/tests/test_interfaces.py b/tests/test_interfaces.py index 21e5097..86a0030 100644 --- a/tests/test_interfaces.py +++ b/tests/test_interfaces.py @@ -16,13 +16,7 @@ from random import randint, choice import jsonschema -try: - import dwave.system - _qpu = True -except ImportError: - _qpu = False - -from dwave_structural_imbalance_demo.interfaces import GlobalSignedSocialNetwork +from dwave_structural_imbalance_demo.interfaces import GlobalSignedSocialNetwork, _qpu from dwave_structural_imbalance_demo.json_schema import json_schema @@ -63,3 +57,7 @@ def test_year_zero_qpu(self): output = self.gssn.solve_structural_imbalance(year=year) self.assertFalse(output['nodes']) self.assertFalse(output['links']) + + @unittest.skipIf(_qpu, "Can only be tested is dwave-system isn't installed") + def test_qpu_without_dwave_system(self): + self.assertRaises(NameError, GlobalSignedSocialNetwork, True) From b10f13b2280c4b7a52a30af00469ac1935b99afc Mon Sep 17 00:00:00 2001 From: Bradley Ellert Date: Fri, 18 May 2018 10:23:43 -0700 Subject: [PATCH 18/26] Add requirement for scipy Needed for nx.kamada_kawai_layout() --- requirements_base.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements_base.txt b/requirements_base.txt index 434f046..2c579e9 100644 --- a/requirements_base.txt +++ b/requirements_base.txt @@ -3,3 +3,4 @@ dwave_networkx==0.6.1 dwave_qbsolv==0.2.6 matplotlib==2.2.2 +scipy==1.1.0 From ac6355e0be7400ff2b8c92672eba109e31d83263 Mon Sep 17 00:00:00 2001 From: Bradley Ellert Date: Fri, 18 May 2018 10:42:28 -0700 Subject: [PATCH 19/26] Separate extra requirements Only need dwave-system if running on QPU --- setup.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 8f2e756..626bf74 100644 --- a/setup.py +++ b/setup.py @@ -22,11 +22,11 @@ install_requires = ['networkx>=2.0,<3.0', 'dwave_networkx>=0.6.1,<0.7.0', 'dwave_qbsolv>=0.2.6,<0.3.0', - 'dwave-system>=0.2.6,<0.3.0', - 'matplotlib>=2.2.2,<3.0.0'] + 'matplotlib>=2.2.2,<3.0.0', + 'scipy>=1.1.0,<2.0.0'] # Any extra requirements, to be used by pip install PACKAGENAME['keyname'] -extras_require = {} +extras_require = {'qpu': ['dwave-system>=0.2.6,<0.3.0']} # The packages in this repo that are to be installed. Either list these explictly, or use setuptools.find_packages. If # the latter, take care to filter unwanted packages (e.g. tests) From dab5c7988ae21054a6e840b70ee75c0e6fd5fbd0 Mon Sep 17 00:00:00 2001 From: Bradley Ellert Date: Fri, 18 May 2018 11:01:50 -0700 Subject: [PATCH 20/26] Add text about setting up and configuring the demo --- README.rst | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/README.rst b/README.rst index 64e024d..c7d1f98 100644 --- a/README.rst +++ b/README.rst @@ -1,24 +1,36 @@ Demo for the Structural Imbalance Project ========================================= -References ----------- - Data is from the Stanford Militants Mapping Project -Mapping Militant Organizations, Stanford University, last modified February 28, 2016, http://web.stanford.edu/group/mappingmilitants/cgi-bin/. +Mapping Militant Organizations, Stanford University, last modified February 28, 2016, +http://web.stanford.edu/group/mappingmilitants/cgi-bin/. -Running the Demo ----------------- +Setting Up the Demo +------------------- + +Copy (clone) this structural-imbalance-demo repository to your local machine. -You can run the demo on classical hardware (a CPU) or on a D-Wave QPU, with the selection made by the pip requirements -file used. +To set up the required dependencies, in the root directory of a copy (clone) of this repository, run the following: .. code-block:: bash - pip install -r requirements_cpu.txt # to run on CPU - pip install -r requirements_qpu.txt --extra-index-url https://pypi.dwavesys.com/simple # to run on QPU + pip install . +Configuring the Demo +-------------------- + +To run the demo on the QPU, extra dependencies must be installed by running the following: + +.. code-block:: bash + + pip install .[qpu] --extra-index-url https://pypi.dwavesys.com/simple + +Access to a D-Wave system must be configured, as described in the `dwave-cloud-client`_ documentation. A default solver +is required. + +Running the Demo +---------------- To run the demo: @@ -26,7 +38,6 @@ To run the demo: python demo.py - License ------- From 95e60ee3509de3c95c66ed4f359ee11196a95937 Mon Sep 17 00:00:00 2001 From: Bradley Ellert Date: Fri, 18 May 2018 11:42:27 -0700 Subject: [PATCH 21/26] Flesh out draw docstring --- dwave_structural_imbalance_demo/drawing.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/dwave_structural_imbalance_demo/drawing.py b/dwave_structural_imbalance_demo/drawing.py index 2ec4080..287fd6e 100644 --- a/dwave_structural_imbalance_demo/drawing.py +++ b/dwave_structural_imbalance_demo/drawing.py @@ -9,9 +9,18 @@ def draw(filename, node_link_data, position=None): """Plot the given signed social network. Args: - filename: The name of the file to be generated. - node_link_data: The network represented as returned by nx.node_link_data() - position (optional): The position for the nodes. + filename (string): + The name of the file to be generated. + node_link_data (dict): + The network represented as returned by nx.node_link_data(). Each edge is required to have a 'sign' + attribute. Optionally, edges can have 'frustrated' attributes and nodes can have 'color' attributes. + position (dict, optional): + The position for the nodes. If no position is provided, a layout will be calculated. If the nodes have + 'color' attributes, a Kamanda-Kawai layout will be used to group nodes of the same color together. + Otherwise, a circular layout will be used. + + Returns: + A dictionary of positions keyed by node. """ From 28a762da10f3359f9d4fdd9d341d7d452cf5866c Mon Sep 17 00:00:00 2001 From: Bradley Ellert Date: Fri, 18 May 2018 11:50:03 -0700 Subject: [PATCH 22/26] Add docstrings for GlobalSignedSocialNetwork --- dwave_structural_imbalance_demo/interfaces.py | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/dwave_structural_imbalance_demo/interfaces.py b/dwave_structural_imbalance_demo/interfaces.py index 6ce4d95..191551c 100644 --- a/dwave_structural_imbalance_demo/interfaces.py +++ b/dwave_structural_imbalance_demo/interfaces.py @@ -16,6 +16,14 @@ class GlobalSignedSocialNetwork(object): + """A class encapsulating access to graphs from the Stanford Militants Mapping Project. + + Args: + qpu (bool, optional): + Specifies whether structural imblance problems will be solved on the QPU or CPU. Defaults to True if + dwave-system is installed, False otherwise. + + """ def __init__(self, qpu=_qpu): maps = dict() maps['Global'] = global_signed_social_network() @@ -56,10 +64,43 @@ def _get_graph(self, subregion='Global', year=None): return G def get_node_link_data(self, subregion='Global', year=None): + """Accessor for Stanford Militants Mapping Project node link data. + + Args: + subregion (str, optional): + Filter graph by subregion. One of ['Global', 'Syria', 'Iraq']. Defaults to 'Global' (entire network). + year (int, optional): + Filter graph by year. Returns only events in or before year. Defaults to None (no filter applied). + + Returns: + A dictionary with node-link formatted data. Conforms to dwave_structural_imbalance_demo.json_schema. + + """ + G = self._get_graph(subregion, year) return nx.node_link_data(G) def solve_structural_imbalance(self, subregion='Global', year=None): + """Solves specified Stanford Militants Mapping Project structural imbalance problem and returns annotated graph. + + If self._qpu is True (set during object initialization), this function will first attempt to embed the entire + problem on the hardware graph using EmbeddingComposite. Failing this, it will fallback on QBSolv to decompose + the problem. If self._qpu is False, this function will use ExactSolver for problems with less than 20 nodes. + For problems with 20 more more nodes, it will use QBSolv to solve the problem classically. + + Args: + subregion (str, optional): + Filter graph by subregion. One of ['Global', 'Syria', 'Iraq']. Defaults to 'Global' (entire network). + year (int, optional): + Filter graph by year. Returns only events in or before year. Defaults to None (no filter applied). + + Returns: + A dictionary with node-link formatted data. Conforms to dwave_structural_imbalance_demo.json_schema. + Optional property 'color' is set for each item in 'nodes'. Optional property 'frustrated' is set for each + item in 'links'. + + """ + G = self._get_graph(subregion, year) if self._qpu: From af8b48cd9850376a3e4b51bd093b5ca75db48e3c Mon Sep 17 00:00:00 2001 From: Bradley Ellert Date: Fri, 18 May 2018 11:57:08 -0700 Subject: [PATCH 23/26] Add examples to docstrings --- dwave_structural_imbalance_demo/drawing.py | 11 ++++++++ dwave_structural_imbalance_demo/interfaces.py | 27 +++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/dwave_structural_imbalance_demo/drawing.py b/dwave_structural_imbalance_demo/drawing.py index 287fd6e..8146aae 100644 --- a/dwave_structural_imbalance_demo/drawing.py +++ b/dwave_structural_imbalance_demo/drawing.py @@ -22,6 +22,17 @@ def draw(filename, node_link_data, position=None): Returns: A dictionary of positions keyed by node. + Examples: + >>> import dwave_structural_imbalance_demo as sbdemo + >>> gssn = sbdemo.GlobalSignedSocialNetwork() + >>> nld_before = gssn.get_node_link_data('Syria', 2013) + >>> nld_after = gssn.solve_structural_imbalance('Syria', 2013) + # draw Global graph before solving; save node layout for reuse + >>> position = sbdemo.draw('syria.png', nld_before) + # draw the Global graph; reusing the above layout, and calculating a new grouped layout + >>> sbdemo.draw('syria_imbalance.png', nld_after, position) + >>> sbdemo.draw('syria_imbalance_grouped', nld_after) + """ S = nx.node_link_graph(node_link_data) diff --git a/dwave_structural_imbalance_demo/interfaces.py b/dwave_structural_imbalance_demo/interfaces.py index 191551c..2a1abd0 100644 --- a/dwave_structural_imbalance_demo/interfaces.py +++ b/dwave_structural_imbalance_demo/interfaces.py @@ -23,6 +23,33 @@ class GlobalSignedSocialNetwork(object): Specifies whether structural imblance problems will be solved on the QPU or CPU. Defaults to True if dwave-system is installed, False otherwise. + Examples: + >>> import dwave_structural_imbalance_demo as sbdemo + >>> gssn = sbdemo.GlobalSignedSocialNetwork() + >>> nld_before = gssn.get_node_link_data('Syria', 2013) + >>> nld_before['nodes'][0] + {'id': 1, 'map': 'Aleppo'} + >>> nld_before['links'][0] + {'event_description': 'Ahrar al-Sham and the Islamic State coordinated an attack on Alawite villages in the Latakia governorate that killed 190 civilians.', + 'event_id': '1821', + 'event_type': 'all', + 'event_year': 2013, + 'sign': 1, + 'source': 1, + 'target': 523} + >>> nld_after = gssn.solve_structural_imbalance('Syria', 2013) + >>> nld_after['nodes'][0] + {'color': 0, 'id': 1, 'map': 'Aleppo'} + >>> nld_after['links'][0] + {'event_description': 'Ahrar al-Sham and the Islamic State coordinated an attack on Alawite villages in the Latakia governorate that killed 190 civilians.', + 'event_id': '1821', + 'event_type': 'all', + 'event_year': 2013, + 'frustrated': False, + 'sign': 1, + 'source': 1, + 'target': 523} + """ def __init__(self, qpu=_qpu): maps = dict() From 83a1b5e614497b0648fbeb0dce9421e43d50e33d Mon Sep 17 00:00:00 2001 From: Bradley Ellert Date: Fri, 18 May 2018 12:01:31 -0700 Subject: [PATCH 24/26] Add missing link to dwave-cloud-client docs --- README.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index c7d1f98..77253ec 100644 --- a/README.rst +++ b/README.rst @@ -26,8 +26,9 @@ To run the demo on the QPU, extra dependencies must be installed by running the pip install .[qpu] --extra-index-url https://pypi.dwavesys.com/simple -Access to a D-Wave system must be configured, as described in the `dwave-cloud-client`_ documentation. A default solver -is required. +Access to a D-Wave system must be configured, as described in the `dwave-cloud-client +`_ documentation. A default +solver is required. Running the Demo ---------------- From c4a06c4e5c02b65492657fea76a9b0488b343044 Mon Sep 17 00:00:00 2001 From: Bradley Ellert Date: Fri, 18 May 2018 13:05:16 -0700 Subject: [PATCH 25/26] 0.0.1 --> 0.0.2 --- dwave_structural_imbalance_demo/package_info.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dwave_structural_imbalance_demo/package_info.py b/dwave_structural_imbalance_demo/package_info.py index 2b63b5f..0402096 100644 --- a/dwave_structural_imbalance_demo/package_info.py +++ b/dwave_structural_imbalance_demo/package_info.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = '0.0.1' +__version__ = '0.0.2' __author__ = 'D-Wave Systems Inc.' __authoremail__ = 'bellert@dwavesys.com' __description__ = 'Demo for analyzing the structural imbalance on a signed social network.' From a9c7625cd6b197f8290d68ad558ba55c2f247885 Mon Sep 17 00:00:00 2001 From: Bradley Ellert Date: Fri, 18 May 2018 13:57:15 -0700 Subject: [PATCH 26/26] Fix error in import for json_schema --- dwave_structural_imbalance_demo/__init__.py | 2 +- tests/test_interfaces.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dwave_structural_imbalance_demo/__init__.py b/dwave_structural_imbalance_demo/__init__.py index 6c7e21c..f3475dd 100644 --- a/dwave_structural_imbalance_demo/__init__.py +++ b/dwave_structural_imbalance_demo/__init__.py @@ -1,4 +1,4 @@ from dwave_structural_imbalance_demo.interfaces import * from dwave_structural_imbalance_demo.mmp_network import * from dwave_structural_imbalance_demo.drawing import * -from json_schema import * +from dwave_structural_imbalance_demo.json_schema import * diff --git a/tests/test_interfaces.py b/tests/test_interfaces.py index 86a0030..6510ef6 100644 --- a/tests/test_interfaces.py +++ b/tests/test_interfaces.py @@ -58,6 +58,6 @@ def test_year_zero_qpu(self): self.assertFalse(output['nodes']) self.assertFalse(output['links']) - @unittest.skipIf(_qpu, "Can only be tested is dwave-system isn't installed") + @unittest.skipIf(_qpu, "Can only be tested if dwave-system isn't installed") def test_qpu_without_dwave_system(self): self.assertRaises(NameError, GlobalSignedSocialNetwork, True)