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/README.rst b/README.rst index 64e024d..77253ec 100644 --- a/README.rst +++ b/README.rst @@ -1,24 +1,37 @@ 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 +39,6 @@ To run the demo: python demo.py - License ------- diff --git a/demo.py b/demo.py index 7006f25..0a57264 100644 --- a/demo.py +++ b/demo.py @@ -1,73 +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 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): +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: @@ -78,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 - Global, Syria, Iraq = 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(Global, sampler, solver=subsolver) + nld_global_solved = gssn.solve_structural_imbalance() - # draw the Global graph - sbdemo.draw('syria_imbalance.png', 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, 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, Global, sampler, subsolver, Syria, 'Syria') + diagramDateRange(gssn, 'Global', 2007, 2016 + 1, 'Syria') diff --git a/dwave_structural_imbalance_demo/__init__.py b/dwave_structural_imbalance_demo/__init__.py index dc68275..f3475dd 100644 --- a/dwave_structural_imbalance_demo/__init__.py +++ b/dwave_structural_imbalance_demo/__init__.py @@ -1,2 +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 dwave_structural_imbalance_demo.json_schema import * diff --git a/dwave_structural_imbalance_demo/drawing.py b/dwave_structural_imbalance_demo/drawing.py index 33ab65a..8146aae 100644 --- a/dwave_structural_imbalance_demo/drawing.py +++ b/dwave_structural_imbalance_demo/drawing.py @@ -1,39 +1,76 @@ +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 - 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. + + 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) """ - 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 +82,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 diff --git a/dwave_structural_imbalance_demo/interfaces.py b/dwave_structural_imbalance_demo/interfaces.py new file mode 100644 index 0000000..2a1abd0 --- /dev/null +++ b/dwave_structural_imbalance_demo/interfaces.py @@ -0,0 +1,154 @@ +import networkx as nx + +import dwave_networkx as dnx +import dwave_qbsolv as qbsolv + +from dwave_structural_imbalance_demo.mmp_network import global_signed_social_network + +try: + import dwave.system.samplers as dwsamplers + import dwave.system.composites as dwcomposites + _qpu = True +except ImportError: + _qpu = False + +import dimod + + +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. + + 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() + 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._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] + 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 + + 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: + try: + 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) + print("Ran on the QPU using QBSolv w/ EmbeddingComposite") + else: + if len(G) < 20: + 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') + 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) diff --git a/dwave_structural_imbalance_demo/json_schema.json b/dwave_structural_imbalance_demo/json_schema.json new file mode 100644 index 0000000..a6aca4c --- /dev/null +++ b/dwave_structural_imbalance_demo/json_schema.json @@ -0,0 +1,85 @@ +{ + "title": "Node Link Data", + "description": "NetworkX graph data; returned by nx.node_link_data()", + "type": "object", + "required": [ + "nodes", + "links" + ], + "properties": { + "nodes": { + "description": "vertices in graph representing groups", + "type": "array", + "items": { + "type": "object", + "required": [ + "id" + ], + "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", + "required": [ + "source", + "target", + "sign" + ], + "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": { + "type": "string" + }, + "event_type": { + "type": "string" + }, + "event_description": { + "type": "string" + }, + "event_year": { + "type": "number" + }, + "frustrated": { + "description": "interactions that violate sign; returned by solve_structural_imbalance()", + "type": "boolean" + } + } + } + }, + "graph": { + "type": "object" + }, + "directed": { + "type": "boolean" + }, + "multigraph": { + "type": "boolean" + } + } +} 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) diff --git a/dwave_structural_imbalance_demo/mmp_network/loader.py b/dwave_structural_imbalance_demo/mmp_network/loader.py index 421b0c2..080d045 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() @@ -57,7 +57,7 @@ def global_signed_social_network(): # fill out the datafield data = {'event_id': id_, 'event_type': type_, - 'event_date': dateinfo, + 'event_year': int(year), # whole date isn't needed 'event_description': description} # finally cast the different relation types to either hostile (sign=-1) 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.' 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 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) 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..211d207 --- /dev/null +++ b/tests/requirements.txt @@ -0,0 +1,4 @@ +-r ../requirements_cpu.txt + +coverage +coveralls diff --git a/tests/test_interfaces.py b/tests/test_interfaces.py new file mode 100644 index 0000000..6510ef6 --- /dev/null +++ b/tests/test_interfaces.py @@ -0,0 +1,63 @@ +# 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, _qpu +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']) + + @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']) + + @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)