diff --git a/conftest.py b/conftest.py index 4c67893..0b59e34 100644 --- a/conftest.py +++ b/conftest.py @@ -8,23 +8,25 @@ # Configuration for slow tests # --------------------------------------------------------------------------------------- + def pytest_addoption(parser): """ Grabbed from: https://docs.pytest.org/en/latest/example/simple.html#control-skipping-of-tests-according-to-command-line-option """ - parser.addoption('--runslow', action='store_true', default=False, help='run slow tests') + parser.addoption("--runslow", action="store_true", default=False, help="run slow tests") def pytest_collection_modifyitems(config, items): - if config.getoption('--runslow'): + if config.getoption("--runslow"): # --runslow given in cli: do not skip slow tests return - skip_slow = pytest.mark.skip(reason='need --runslow option to run') + skip_slow = pytest.mark.skip(reason="need --runslow option to run") for item in items: - if 'slow' in item.keywords: + if "slow" in item.keywords: item.add_marker(skip_slow) + # --------------------------------------------------------------------------------------- # Graph objects shared between tests # --------------------------------------------------------------------------------------- @@ -34,60 +36,66 @@ def pytest_collection_modifyitems(config, items): # Graph 1: Simple graph. Required edges only. CPP == RPP # ------------------------------------------------------------------------------------- -@pytest.fixture(scope='session', autouse=True) -def GRAPH_1(): - return nx.MultiGraph([ - ('a', 'b', {'id': 1, 'distance': 5}), - ('a', 'c', {'id': 2, 'distance': 20}), - ('b', 'c', {'id': 3, 'distance': 10}), - ('c', 'd', {'id': 4, 'distance': 3}), - ('d', 'b', {'id': 5, 'distance': 2}) - ]) - -@pytest.fixture(scope='session', autouse=True) +@pytest.fixture(scope="session", autouse=True) +def GRAPH_1(): + return nx.MultiGraph( + [ + ("a", "b", {"id": 1, "distance": 5}), + ("a", "c", {"id": 2, "distance": 20}), + ("b", "c", {"id": 3, "distance": 10}), + ("c", "d", {"id": 4, "distance": 3}), + ("d", "b", {"id": 5, "distance": 2}), + ] + ) + + +@pytest.fixture(scope="session", autouse=True) def GRAPH_1_EDGELIST_DF_W_ID(): - return pd.DataFrame({ - 'node1': ['a', 'a', 'b', 'c', 'd'], - 'node2': ['b', 'c', 'c', 'd', 'b'], - 'distance': [5, 20, 10, 3, 2], - 'id': [1, 2, 3, 4, 5] - }, columns=['node1', 'node2', 'distance', 'id']) - - -@pytest.fixture(scope='function', autouse=True, ) + return pd.DataFrame( + { + "node1": ["a", "a", "b", "c", "d"], + "node2": ["b", "c", "c", "d", "b"], + "distance": [5, 20, 10, 3, 2], + "id": [1, 2, 3, 4, 5], + }, + columns=["node1", "node2", "distance", "id"], + ) + + +@pytest.fixture( + scope="function", + autouse=True, +) def GRAPH_1_EDGELIST_DF(GRAPH_1_EDGELIST_DF_W_ID): - return GRAPH_1_EDGELIST_DF_W_ID.drop('id', axis=1) + return GRAPH_1_EDGELIST_DF_W_ID.drop("id", axis=1) -@pytest.fixture(scope='function', autouse=True) +@pytest.fixture(scope="function", autouse=True) def GRAPH_1_EDGELIST_CSV(GRAPH_1_EDGELIST_DF): return create_mock_csv_from_dataframe(GRAPH_1_EDGELIST_DF) -@pytest.fixture(scope='function', autouse=True) +@pytest.fixture(scope="function", autouse=True) def GRAPH_1_EDGELIST_W_ID_CSV(GRAPH_1_EDGELIST_DF_W_ID): return create_mock_csv_from_dataframe(GRAPH_1_EDGELIST_DF_W_ID) -@pytest.fixture(scope='session', autouse=True) +@pytest.fixture(scope="session", autouse=True) def GRAPH_1_NODE_ATTRIBUTES(): - return pd.DataFrame({ - 'id': ['a', 'b', 'c', 'd'], - 'attr_fruit': ['apple', 'banana', 'cherry', 'durian'] - }) + return pd.DataFrame({"id": ["a", "b", "c", "d"], "attr_fruit": ["apple", "banana", "cherry", "durian"]}) -@pytest.fixture(scope='session', autouse=True) +@pytest.fixture(scope="session", autouse=True) def GRAPH_1_CIRCUIT_CPP(): return [ - ('a', 'b', 0, {'distance': 5, 'id': 0}), - ('b', 'd', 0, {'distance': 2, 'augmented': True, 'id': 4}), - ('d', 'c', 0, {'distance': 3, 'augmented': True, 'id': 3}), - ('c', 'b', 0, {'distance': 10, 'id': 2}), - ('b', 'd', 0, {'distance': 2, 'id': 4}), - ('d', 'c', 0, {'distance': 3, 'id': 3}), - ('c', 'a', 0, {'distance': 20, 'id': 1}) + ("a", "b", 0, {"distance": 5, "id": 0}), + ("b", "d", 0, {"distance": 2, "augmented": True, "id": 4}), + ("d", "c", 0, {"distance": 3, "augmented": True, "id": 3}), + ("c", "b", 0, {"distance": 10, "id": 2}), + ("b", "d", 0, {"distance": 2, "id": 4}), + ("d", "c", 0, {"distance": 3, "id": 3}), + ("c", "a", 0, {"distance": 20, "id": 1}), ] @@ -96,35 +104,37 @@ def GRAPH_1_CIRCUIT_CPP(): # ------------------------------------------------------------------------------------- -@pytest.fixture(scope='session', autouse=True) +@pytest.fixture(scope="session", autouse=True) def GRAPH_2(): - return nx.MultiGraph([ - ('a', 'b', {'distance': 20, 'required': 1}), - ('a', 'c', {'distance': 25, 'required': 1}), - ('a', 'd', {'distance': 30, 'required': 1}), - ('a', 'e', {'distance': 35, 'required': 1}), - ('b', 'c', {'distance': 2, 'required': 0}), - ('c', 'd', {'distance': 3, 'required': 0}), - ('d', 'e', {'distance': 4, 'required': 0}), - ('e', 'b', {'distance': 6, 'required': 0}) - ]) - - -@pytest.fixture(scope='session', autouse=True) + return nx.MultiGraph( + [ + ("a", "b", {"distance": 20, "required": 1}), + ("a", "c", {"distance": 25, "required": 1}), + ("a", "d", {"distance": 30, "required": 1}), + ("a", "e", {"distance": 35, "required": 1}), + ("b", "c", {"distance": 2, "required": 0}), + ("c", "d", {"distance": 3, "required": 0}), + ("d", "e", {"distance": 4, "required": 0}), + ("e", "b", {"distance": 6, "required": 0}), + ] + ) + + +@pytest.fixture(scope="session", autouse=True) def GRAPH_2_EDGELIST_CSV(GRAPH_2): - edgelist = nx.to_pandas_edgelist(GRAPH_2, source='_node1', target='_node2') + edgelist = nx.to_pandas_edgelist(GRAPH_2, source="_node1", target="_node2") return create_mock_csv_from_dataframe(edgelist) -@pytest.fixture(scope='session', autouse=True) +@pytest.fixture(scope="session", autouse=True) def GRAPH_2_CIRCUIT_RPP(): return [ - ('a', 'c', 0, {'distance': 25, 'id': 4, 'required': 1}), - ('c', 'b', 0, {'distance': 2, 'id': 6, 'augmented': True, 'required': 0}), - ('b', 'a', 0, {'distance': 20, 'id': 3, 'required': 1}), - ('a', 'e', 0, {'distance': 35, 'id': 5, 'required': 1}), - ('e', 'd', 0, {'distance': 4, 'id': 2, 'augmented': True, 'required': 0}), - ('d', 'a', 0, {'distance': 30, 'id': 0, 'required': 1}) + ("a", "c", 0, {"distance": 25, "id": 4, "required": 1}), + ("c", "b", 0, {"distance": 2, "id": 6, "augmented": True, "required": 0}), + ("b", "a", 0, {"distance": 20, "id": 3, "required": 1}), + ("a", "e", 0, {"distance": 35, "id": 5, "required": 1}), + ("e", "d", 0, {"distance": 4, "id": 2, "augmented": True, "required": 0}), + ("d", "a", 0, {"distance": 30, "id": 0, "required": 1}), ] @@ -133,23 +143,25 @@ def GRAPH_2_CIRCUIT_RPP(): # ------------------------------------------------------------------------------------- -@pytest.fixture(scope='session', autouse=True) +@pytest.fixture(scope="session", autouse=True) def GRAPH_3(): - return nx.MultiGraph([ - ('a', 'b', {'distance': 20, 'required': 1}), - ('a', 'c', {'distance': 25, 'required': 1}), - ('a', 'd', {'distance': 30, 'required': 1}), - ('a', 'e', {'distance': 35, 'required': 1}), - ('b', 'c', {'distance': 2, 'required': 0}), - ('c', 'd', {'distance': 3, 'required': 0}), - ('d', 'e', {'distance': 4, 'required': 0}), - ('e', 'b', {'distance': 6, 'required': 0}), - ('b', 'f', {'distance': 7, 'required': 0}), - ('f', 'g', {'distance': 8, 'required': 1}) - ]) - - -@pytest.fixture(scope='session', autouse=True) + return nx.MultiGraph( + [ + ("a", "b", {"distance": 20, "required": 1}), + ("a", "c", {"distance": 25, "required": 1}), + ("a", "d", {"distance": 30, "required": 1}), + ("a", "e", {"distance": 35, "required": 1}), + ("b", "c", {"distance": 2, "required": 0}), + ("c", "d", {"distance": 3, "required": 0}), + ("d", "e", {"distance": 4, "required": 0}), + ("e", "b", {"distance": 6, "required": 0}), + ("b", "f", {"distance": 7, "required": 0}), + ("f", "g", {"distance": 8, "required": 1}), + ] + ) + + +@pytest.fixture(scope="session", autouse=True) def GRAPH_3_EDGELIST_CSV(GRAPH_3): - edgelist = nx.to_pandas_edgelist(GRAPH_3, source='_node1', target='_node2') - return create_mock_csv_from_dataframe(edgelist) \ No newline at end of file + edgelist = nx.to_pandas_edgelist(GRAPH_3, source="_node1", target="_node2") + return create_mock_csv_from_dataframe(edgelist) diff --git a/postman_problems/examples/seven_bridges/cpp_seven_bridges.py b/postman_problems/examples/seven_bridges/cpp_seven_bridges.py index 126db64..c48a064 100644 --- a/postman_problems/examples/seven_bridges/cpp_seven_bridges.py +++ b/postman_problems/examples/seven_bridges/cpp_seven_bridges.py @@ -36,16 +36,18 @@ def main(): # PARAMS / DATA --------------------------------------------------------------------- # inputs - EDGELIST = pkg_resources.resource_filename('postman_problems', 'examples/seven_bridges/edgelist_seven_bridges.csv') - START_NODE = 'D' + EDGELIST = pkg_resources.resource_filename("postman_problems", "examples/seven_bridges/edgelist_seven_bridges.csv") + START_NODE = "D" # outputs - PNG_PATH = pkg_resources.resource_filename('postman_problems', 'examples/seven_bridges/output/png/') - CPP_VIZ_FILENAME = pkg_resources.resource_filename('postman_problems', 'examples/seven_bridges/output/cpp_graph') - CPP_BASE_VIZ_FILENAME = pkg_resources.resource_filename('postman_problems', - 'examples/seven_bridges/output/base_cpp_graph') - CPP_GIF_FILENAME = pkg_resources.resource_filename('postman_problems', - 'examples/seven_bridges/output/cpp_graph.gif') + PNG_PATH = pkg_resources.resource_filename("postman_problems", "examples/seven_bridges/output/png/") + CPP_VIZ_FILENAME = pkg_resources.resource_filename("postman_problems", "examples/seven_bridges/output/cpp_graph") + CPP_BASE_VIZ_FILENAME = pkg_resources.resource_filename( + "postman_problems", "examples/seven_bridges/output/base_cpp_graph" + ) + CPP_GIF_FILENAME = pkg_resources.resource_filename( + "postman_problems", "examples/seven_bridges/output/cpp_graph.gif" + ) # setup logging logging.basicConfig(level=logging.INFO) @@ -53,51 +55,55 @@ def main(): # SOLVE CPP ------------------------------------------------------------------------- - logger.info('Solve CPP') + logger.info("Solve CPP") circuit, graph = cpp(edgelist_filename=EDGELIST, start_node=START_NODE) - logger.info('Print the CPP solution:') + logger.info("Print the CPP solution:") for e in circuit: logger.info(e) - logger.info('Solution summary stats:') + logger.info("Solution summary stats:") for k, v in calculate_postman_solution_stats(circuit).items(): - logger.info(str(k) + ' : ' + str(v)) + logger.info(str(k) + " : " + str(v)) # VIZ ------------------------------------------------------------------------------- try: from postman_problems.viz import plot_circuit_graphviz, make_circuit_images, make_circuit_video - logger.info('Creating single SVG of base graph') - plot_circuit_graphviz(circuit=circuit, - graph=graph, - filename=CPP_BASE_VIZ_FILENAME, - edge_label_attr='distance', - format='svg', - engine='circo', - graph_attr={'label': 'Base Graph: Distances', 'labelloc': 't'}) - - logger.info('Creating single SVG of CPP solution') - plot_circuit_graphviz(circuit=circuit, - graph=graph, - filename=CPP_VIZ_FILENAME, - format='svg', - engine='circo', - graph_attr={'label': 'Base Graph: Chinese Postman Solution', 'labelloc': 't'}) - - logger.info('Creating PNG files for GIF') - make_circuit_images(circuit=circuit, - graph=graph, - outfile_dir=PNG_PATH, - format='png', - engine='circo', - graph_attr={'label': 'Base Graph: Chinese Postman Solution', 'labelloc': 't'}) - - logger.info('Creating GIF') - video_message = make_circuit_video(infile_dir_images=PNG_PATH, - outfile_movie=CPP_GIF_FILENAME, - fps=0.5) + logger.info("Creating single SVG of base graph") + plot_circuit_graphviz( + circuit=circuit, + graph=graph, + filename=CPP_BASE_VIZ_FILENAME, + edge_label_attr="distance", + format="svg", + engine="circo", + graph_attr={"label": "Base Graph: Distances", "labelloc": "t"}, + ) + + logger.info("Creating single SVG of CPP solution") + plot_circuit_graphviz( + circuit=circuit, + graph=graph, + filename=CPP_VIZ_FILENAME, + format="svg", + engine="circo", + graph_attr={"label": "Base Graph: Chinese Postman Solution", "labelloc": "t"}, + ) + + logger.info("Creating PNG files for GIF") + make_circuit_images( + circuit=circuit, + graph=graph, + outfile_dir=PNG_PATH, + format="png", + engine="circo", + graph_attr={"label": "Base Graph: Chinese Postman Solution", "labelloc": "t"}, + ) + + logger.info("Creating GIF") + video_message = make_circuit_video(infile_dir_images=PNG_PATH, outfile_movie=CPP_GIF_FILENAME, fps=0.5) logger.info(video_message) logger.info("and that's a wrap, checkout the output!") @@ -107,5 +113,5 @@ def main(): print("Sorry, looks like you don't have all the needed visualization dependencies.") -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/postman_problems/examples/sleeping_giant/cpp_sleeping_giant.py b/postman_problems/examples/sleeping_giant/cpp_sleeping_giant.py index d1205e6..5a92b6d 100644 --- a/postman_problems/examples/sleeping_giant/cpp_sleeping_giant.py +++ b/postman_problems/examples/sleeping_giant/cpp_sleeping_giant.py @@ -45,21 +45,24 @@ def main(): # PARAMS / DATA --------------------------------------------------------------------- # inputs - EDGELIST = pkg_resources.resource_filename('postman_problems', - 'examples/sleeping_giant/edgelist_sleeping_giant.csv') - NODELIST = pkg_resources.resource_filename('postman_problems', - 'examples/sleeping_giant/nodelist_sleeping_giant.csv') + EDGELIST = pkg_resources.resource_filename( + "postman_problems", "examples/sleeping_giant/edgelist_sleeping_giant.csv" + ) + NODELIST = pkg_resources.resource_filename( + "postman_problems", "examples/sleeping_giant/nodelist_sleeping_giant.csv" + ) START_NODE = "b_end_east" # outputs - GRAPH_ATTR = {'dpi': '65'} - EDGE_ATTR = {'fontsize': '20'} - NODE_ATTR = {'shape': 'point', 'color': 'black', 'width': '0.1', 'fixedsize': 'true'} + GRAPH_ATTR = {"dpi": "65"} + EDGE_ATTR = {"fontsize": "20"} + NODE_ATTR = {"shape": "point", "color": "black", "width": "0.1", "fixedsize": "true"} - PNG_PATH = pkg_resources.resource_filename('postman_problems', 'examples/sleeping_giant/output/png/') - CPP_SVG_FILENAME = pkg_resources.resource_filename('postman_problems', 'examples/sleeping_giant/output/cpp_graph') - CPP_GIF_FILENAME = pkg_resources.resource_filename('postman_problems', - 'examples/sleeping_giant/output/cpp_graph.gif') + PNG_PATH = pkg_resources.resource_filename("postman_problems", "examples/sleeping_giant/output/png/") + CPP_SVG_FILENAME = pkg_resources.resource_filename("postman_problems", "examples/sleeping_giant/output/cpp_graph") + CPP_GIF_FILENAME = pkg_resources.resource_filename( + "postman_problems", "examples/sleeping_giant/output/cpp_graph.gif" + ) # setup logging logging.basicConfig(level=logging.INFO) @@ -67,44 +70,49 @@ def main(): # SOLVE CPP ------------------------------------------------------------------------- - logger.info('Solve CPP') + logger.info("Solve CPP") circuit, graph = cpp(EDGELIST, START_NODE) - logger.info('Print the CPP solution:') + logger.info("Print the CPP solution:") for e in circuit: logger.info(e) - logger.info('Solution summary stats:') + logger.info("Solution summary stats:") for k, v in calculate_postman_solution_stats(circuit).items(): - logger.info(str(k) + ' : ' + str(v)) + logger.info(str(k) + " : " + str(v)) # VIZ ------------------------------------------------------------------------------- try: from postman_problems.viz import ( - add_pos_node_attribute, add_node_attributes, plot_circuit_graphviz, make_circuit_images, make_circuit_video + add_pos_node_attribute, + add_node_attributes, + plot_circuit_graphviz, + make_circuit_images, + make_circuit_video, ) - logger.info('Add node attributes to graph') + logger.info("Add node attributes to graph") nodelist_df = pd.read_csv(NODELIST) graph = add_node_attributes(graph, nodelist_df) # add attributes - graph = add_pos_node_attribute(graph, origin='topleft') # add X,Y positions in format for graphviz - - logger.info('Creating single SVG of CPP solution') - plot_circuit_graphviz(circuit=circuit, - graph=graph, - filename=CPP_SVG_FILENAME, - format='svg', - engine='neato', - graph_attr=GRAPH_ATTR, - edge_attr=EDGE_ATTR, - node_attr=NODE_ATTR) - + graph = add_pos_node_attribute(graph, origin="topleft") # add X,Y positions in format for graphviz + + logger.info("Creating single SVG of CPP solution") + plot_circuit_graphviz( + circuit=circuit, + graph=graph, + filename=CPP_SVG_FILENAME, + format="svg", + engine="neato", + graph_attr=GRAPH_ATTR, + edge_attr=EDGE_ATTR, + node_attr=NODE_ATTR, + ) except FileNotFoundError(OSError) as e: print(e) print("Sorry, looks like you don't have all the needed visualization dependencies.") -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/postman_problems/examples/sleeping_giant/rpp_sleeping_giant.py b/postman_problems/examples/sleeping_giant/rpp_sleeping_giant.py index b56ed80..d8854cc 100644 --- a/postman_problems/examples/sleeping_giant/rpp_sleeping_giant.py +++ b/postman_problems/examples/sleeping_giant/rpp_sleeping_giant.py @@ -1,6 +1,4 @@ -""" - -""" +""" """ import logging import pkg_resources @@ -15,21 +13,24 @@ def main(): # PARAMS / DATA --------------------------------------------------------------------- # inputs - EDGELIST = pkg_resources.resource_filename('postman_problems', - 'examples/sleeping_giant/edgelist_sleeping_giant.csv') - NODELIST = pkg_resources.resource_filename('postman_problems', - 'examples/sleeping_giant/nodelist_sleeping_giant.csv') + EDGELIST = pkg_resources.resource_filename( + "postman_problems", "examples/sleeping_giant/edgelist_sleeping_giant.csv" + ) + NODELIST = pkg_resources.resource_filename( + "postman_problems", "examples/sleeping_giant/nodelist_sleeping_giant.csv" + ) START_NODE = "b_end_east" # outputs - GRAPH_ATTR = {'dpi': '65'} - EDGE_ATTR = {'fontsize': '20'} - NODE_ATTR = {'shape': 'point', 'color': 'black', 'width': '0.1', 'fixedsize': 'true'} + GRAPH_ATTR = {"dpi": "65"} + EDGE_ATTR = {"fontsize": "20"} + NODE_ATTR = {"shape": "point", "color": "black", "width": "0.1", "fixedsize": "true"} - PNG_PATH = pkg_resources.resource_filename('postman_problems', 'examples/sleeping_giant/output/png/') - RPP_SVG_FILENAME = pkg_resources.resource_filename('postman_problems', 'examples/sleeping_giant/output/rpp_graph') - RPP_GIF_FILENAME = pkg_resources.resource_filename('postman_problems', - 'examples/sleeping_giant/output/rpp_graph.gif') + PNG_PATH = pkg_resources.resource_filename("postman_problems", "examples/sleeping_giant/output/png/") + RPP_SVG_FILENAME = pkg_resources.resource_filename("postman_problems", "examples/sleeping_giant/output/rpp_graph") + RPP_GIF_FILENAME = pkg_resources.resource_filename( + "postman_problems", "examples/sleeping_giant/output/rpp_graph.gif" + ) # setup logging logging.basicConfig(level=logging.INFO) @@ -37,59 +38,64 @@ def main(): # SOLVE RPP ------------------------------------------------------------------------- - logger.info('Solve RPP') + logger.info("Solve RPP") circuit, graph = rpp(EDGELIST, START_NODE) - logger.info('Print the RPP solution:') + logger.info("Print the RPP solution:") for e in circuit: logger.info(e) - logger.info('Solution summary stats:') + logger.info("Solution summary stats:") for k, v in calculate_postman_solution_stats(circuit).items(): - logger.info(str(k) + ' : ' + str(v)) + logger.info(str(k) + " : " + str(v)) # VIZ ------------------------------------------------------------------------------- try: from postman_problems.viz import ( - add_pos_node_attribute, add_node_attributes, plot_circuit_graphviz, make_circuit_images, make_circuit_video + add_pos_node_attribute, + add_node_attributes, + plot_circuit_graphviz, + make_circuit_images, + make_circuit_video, ) - logger.info('Add node attributes to graph') + logger.info("Add node attributes to graph") nodelist_df = pd.read_csv(NODELIST) graph = add_node_attributes(graph, nodelist_df) # add attributes - graph = add_pos_node_attribute(graph, origin='topleft') # add X,Y positions in format for graphviz + graph = add_pos_node_attribute(graph, origin="topleft") # add X,Y positions in format for graphviz - logger.info('Add style edge attribute to make optional edges dotted') + logger.info("Add style edge attribute to make optional edges dotted") for e in graph.edges(data=True, keys=True): - graph[e[0]][e[1]][e[2]]['style'] = 'solid' if graph[e[0]][e[1]][e[2]]['required'] else 'dashed' - - logger.info('Creating single SVG of RPP solution') - plot_circuit_graphviz(circuit=circuit, - graph=graph, - filename=RPP_SVG_FILENAME, - format='svg', - engine='neato', - graph_attr=GRAPH_ATTR, - edge_attr=EDGE_ATTR, - node_attr=NODE_ATTR - ) - - logger.info('Creating PNG files for GIF') - images_message = make_circuit_images(circuit=circuit, - graph=graph, - outfile_dir=PNG_PATH, - format='png', - engine='neato', - graph_attr=GRAPH_ATTR, - edge_attr=EDGE_ATTR, - node_attr=NODE_ATTR) + graph[e[0]][e[1]][e[2]]["style"] = "solid" if graph[e[0]][e[1]][e[2]]["required"] else "dashed" + + logger.info("Creating single SVG of RPP solution") + plot_circuit_graphviz( + circuit=circuit, + graph=graph, + filename=RPP_SVG_FILENAME, + format="svg", + engine="neato", + graph_attr=GRAPH_ATTR, + edge_attr=EDGE_ATTR, + node_attr=NODE_ATTR, + ) + + logger.info("Creating PNG files for GIF") + images_message = make_circuit_images( + circuit=circuit, + graph=graph, + outfile_dir=PNG_PATH, + format="png", + engine="neato", + graph_attr=GRAPH_ATTR, + edge_attr=EDGE_ATTR, + node_attr=NODE_ATTR, + ) logger.info(images_message) - logger.info('Creating GIF') - video_message = make_circuit_video(infile_dir_images=PNG_PATH, - outfile_movie=RPP_GIF_FILENAME, - fps=3) + logger.info("Creating GIF") + video_message = make_circuit_video(infile_dir_images=PNG_PATH, outfile_movie=RPP_GIF_FILENAME, fps=3) logger.info(video_message) logger.info("and that's a wrap, checkout the output!") @@ -98,5 +104,5 @@ def main(): print("Sorry, looks like you don't have all the needed visualization dependencies.") -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/postman_problems/examples/star/rpp_star.py b/postman_problems/examples/star/rpp_star.py index fae541a..6600b3b 100644 --- a/postman_problems/examples/star/rpp_star.py +++ b/postman_problems/examples/star/rpp_star.py @@ -21,12 +21,12 @@ def create_star_graph(n_nodes=10, ring=True): graph = nx.MultiGraph() node_names = list(string.ascii_lowercase)[:n_nodes] nx.add_star(graph, node_names) - nx.set_edge_attributes(graph, 10, 'distance') - nx.set_edge_attributes(graph, 1, 'required') - nx.set_edge_attributes(graph, 'solid', 'style') + nx.set_edge_attributes(graph, 10, "distance") + nx.set_edge_attributes(graph, 1, "required") + nx.set_edge_attributes(graph, "solid", "style") if ring: for e in list(zip(node_names[1:-1] + [node_names[1]], node_names[2:] + [node_names[-1]])): - graph.add_edge(e[0], e[1], distance=2, required=0, style='dashed') + graph.add_edge(e[0], e[1], distance=2, required=0, style="dashed") return graph @@ -36,18 +36,20 @@ def main(): # PARAMS / DATA --------------------------------------------------------------------- # inputs - START_NODE = 'a' + START_NODE = "a" N_NODES = 13 # filepaths - CPP_REQUIRED_SVG_FILENAME = pkg_resources.resource_filename('postman_problems', - 'examples/star/output/cpp_graph_req') - CPP_OPTIONAL_SVG_FILENAME = pkg_resources.resource_filename('postman_problems', - 'examples/star/output/cpp_graph_opt') - RPP_SVG_FILENAME = pkg_resources.resource_filename('postman_problems', 'examples/star/output/rpp_graph') - RPP_BASE_SVG_FILENAME = pkg_resources.resource_filename('postman_problems', 'examples/star/output/base_rpp_graph') - PNG_PATH = pkg_resources.resource_filename('postman_problems', 'examples/star/output/png/') - RPP_GIF_FILENAME = pkg_resources.resource_filename('postman_problems', 'examples/star/output/rpp_graph.gif') + CPP_REQUIRED_SVG_FILENAME = pkg_resources.resource_filename( + "postman_problems", "examples/star/output/cpp_graph_req" + ) + CPP_OPTIONAL_SVG_FILENAME = pkg_resources.resource_filename( + "postman_problems", "examples/star/output/cpp_graph_opt" + ) + RPP_SVG_FILENAME = pkg_resources.resource_filename("postman_problems", "examples/star/output/rpp_graph") + RPP_BASE_SVG_FILENAME = pkg_resources.resource_filename("postman_problems", "examples/star/output/base_rpp_graph") + PNG_PATH = pkg_resources.resource_filename("postman_problems", "examples/star/output/png/") + RPP_GIF_FILENAME = pkg_resources.resource_filename("postman_problems", "examples/star/output/rpp_graph.gif") # setup logging logging.basicConfig(level=logging.INFO) @@ -55,25 +57,25 @@ def main(): # CREATE GRAPH ---------------------------------------------------------------------- - logger.info('Solve RPP') + logger.info("Solve RPP") graph_base = create_star_graph(N_NODES) - edgelist = nx.to_pandas_edgelist(graph_base, source='_node1', target='_node2') + edgelist = nx.to_pandas_edgelist(graph_base, source="_node1", target="_node2") # SOLVE CPP ------------------------------------------------------------------------- # with required edges only edgelist_file = create_mock_csv_from_dataframe(edgelist) circuit_cpp_req, graph_cpp_req = cpp(edgelist_file, start_node=START_NODE) - logger.info('Print the CPP solution (required edges only):') + logger.info("Print the CPP solution (required edges only):") for e in circuit_cpp_req: logger.info(e) # with required and optional edges as required edgelist_all_req = edgelist.copy() - edgelist_all_req.drop(['required'], axis=1, inplace=True) + edgelist_all_req.drop(["required"], axis=1, inplace=True) edgelist_file = create_mock_csv_from_dataframe(edgelist_all_req) circuit_cpp_opt, graph_cpp_opt = cpp(edgelist_file, start_node=START_NODE) - logger.info('Print the CPP solution (optional and required edges):') + logger.info("Print the CPP solution (optional and required edges):") for e in circuit_cpp_opt: logger.info(e) @@ -82,74 +84,69 @@ def main(): edgelist_file = create_mock_csv_from_dataframe(edgelist) # need to regenerate circuit_rpp, graph_rpp = rpp(edgelist_file, start_node=START_NODE) - logger.info('Print the RPP solution:') + logger.info("Print the RPP solution:") for e in circuit_rpp: logger.info(e) - logger.info('Solution summary stats:') + logger.info("Solution summary stats:") for k, v in calculate_postman_solution_stats(circuit_rpp).items(): - logger.info(str(k) + ' : ' + str(v)) + logger.info(str(k) + " : " + str(v)) # VIZ ------------------------------------------------------------------------------- try: from postman_problems.viz import plot_circuit_graphviz, plot_graphviz, make_circuit_images, make_circuit_video - logger.info('Creating single SVG of base graph') - plot_graphviz(graph=graph_base, - filename=RPP_BASE_SVG_FILENAME, - edge_label_attr='distance', - format='svg', - engine='circo', - graph_attr={'label': 'Base Graph: Distances', 'labelloc': 't'} - ) - - logger.info('Creating single SVG of CPP solution (required edges only)') - plot_circuit_graphviz(circuit=circuit_cpp_req, - graph=graph_cpp_req, - filename=CPP_REQUIRED_SVG_FILENAME, - format='svg', - engine='circo', - graph_attr={ - 'label': 'Base Graph: Chinese Postman Solution (required edges only)', - 'labelloc': 't'} - ) - - logger.info('Creating single SVG of CPP solution (required & optional edges)') - plot_circuit_graphviz(circuit=circuit_cpp_opt, - graph=graph_cpp_opt, - filename=CPP_OPTIONAL_SVG_FILENAME, - format='svg', - engine='circo', - graph_attr={'label': 'Base Graph: Chinese Postman Solution (required & optional edges)', - 'labelloc': 't'} - ) - - logger.info('Creating single SVG of RPP solution') - plot_circuit_graphviz(circuit=circuit_rpp, - graph=graph_rpp, - filename=RPP_SVG_FILENAME, - format='svg', - engine='circo', - graph_attr={'label': 'Base Graph: Rural Postman Solution', 'labelloc': 't'} - ) - - logger.info('Creating PNG files for GIF') - make_circuit_images(circuit=circuit_rpp, - graph=graph_rpp, - outfile_dir=PNG_PATH, - format='png', - engine='circo') - - logger.info('Creating GIF') - make_circuit_video(infile_dir_images=PNG_PATH, - outfile_movie=RPP_GIF_FILENAME, - fps=1) + logger.info("Creating single SVG of base graph") + plot_graphviz( + graph=graph_base, + filename=RPP_BASE_SVG_FILENAME, + edge_label_attr="distance", + format="svg", + engine="circo", + graph_attr={"label": "Base Graph: Distances", "labelloc": "t"}, + ) + + logger.info("Creating single SVG of CPP solution (required edges only)") + plot_circuit_graphviz( + circuit=circuit_cpp_req, + graph=graph_cpp_req, + filename=CPP_REQUIRED_SVG_FILENAME, + format="svg", + engine="circo", + graph_attr={"label": "Base Graph: Chinese Postman Solution (required edges only)", "labelloc": "t"}, + ) + + logger.info("Creating single SVG of CPP solution (required & optional edges)") + plot_circuit_graphviz( + circuit=circuit_cpp_opt, + graph=graph_cpp_opt, + filename=CPP_OPTIONAL_SVG_FILENAME, + format="svg", + engine="circo", + graph_attr={"label": "Base Graph: Chinese Postman Solution (required & optional edges)", "labelloc": "t"}, + ) + + logger.info("Creating single SVG of RPP solution") + plot_circuit_graphviz( + circuit=circuit_rpp, + graph=graph_rpp, + filename=RPP_SVG_FILENAME, + format="svg", + engine="circo", + graph_attr={"label": "Base Graph: Rural Postman Solution", "labelloc": "t"}, + ) + + logger.info("Creating PNG files for GIF") + make_circuit_images(circuit=circuit_rpp, graph=graph_rpp, outfile_dir=PNG_PATH, format="png", engine="circo") + + logger.info("Creating GIF") + make_circuit_video(infile_dir_images=PNG_PATH, outfile_movie=RPP_GIF_FILENAME, fps=1) except FileNotFoundError(OSError) as e: print(e) print("Sorry, looks like you don't have all the needed visualization dependencies.") -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/postman_problems/graph.py b/postman_problems/graph.py index 35ccad4..2230bb7 100644 --- a/postman_problems/graph.py +++ b/postman_problems/graph.py @@ -14,23 +14,26 @@ def read_edgelist(edgelist_filename, keep_optional=False): pandas dataframe of edgelist """ el = pd.read_csv(edgelist_filename, dtype={0: str, 1: str}) # node_ids as strings makes life easier - el = el.dropna(how='all') # drop rows with all NAs... as I find CSVs created w Numbers annoyingly do. + el = el.dropna(how="all") # drop rows with all NAs... as I find CSVs created w Numbers annoyingly do. - if (not keep_optional) & ('required' in el.columns): - el = el[el['required'] == 1] + if (not keep_optional) & ("required" in el.columns): + el = el[el["required"] == 1] - assert 'augmented' not in el.columns, \ + assert "augmented" not in el.columns, ( 'Edgelist cannot contain a column named "augmented", sorry. This will cause computation problems' - - if 'id' in el.columns: - warnings.warn("Edgelist contains field named 'id'. This is a field that will be assigned to edge attributes " - "with the `create_networkx_graph_from_edgelist function. That is OK though. We'll use your 'id'" - "field if it is unique.") - assert el['id'].nunique() == len(el), 'Provided edge "id" field is not unique. Please drop "id" or try again.' + ) + + if "id" in el.columns: + warnings.warn( + "Edgelist contains field named 'id'. This is a field that will be assigned to edge attributes " + "with the `create_networkx_graph_from_edgelist function. That is OK though. We'll use your 'id'" + "field if it is unique." + ) + assert el["id"].nunique() == len(el), 'Provided edge "id" field is not unique. Please drop "id" or try again.' return el -def create_networkx_graph_from_edgelist(edgelist, edge_id='id'): +def create_networkx_graph_from_edgelist(edgelist, edge_id="id"): """ Create a networkx MultiGraph object from an edgelist (pandas dataframe). Used to create the user's starting graph for which a CPP solution is desired. @@ -47,9 +50,11 @@ def create_networkx_graph_from_edgelist(edgelist, edge_id='id'): """ g = nx.MultiGraph() if edge_id in edgelist.columns: - warnings.warn('{} is already an edge attribute in `edgelist`. We will try to use it, but recommend ' - 'renaming this column in your edgelist to allow this function to create it in a standardized way' - 'where it is guaranteed to be unique'.format(edge_id)) + warnings.warn( + "{} is already an edge attribute in `edgelist`. We will try to use it, but recommend " + "renaming this column in your edgelist to allow this function to create it in a standardized way" + "where it is guaranteed to be unique".format(edge_id) + ) for i, row in enumerate(edgelist.iterrows()): edge_attr_dict = row[1][2:].to_dict() @@ -103,7 +108,7 @@ def get_even_nodes(graph): return _get_even_or_odd_nodes(graph, 0) -def get_shortest_paths_distances(graph, pairs, edge_weight_name='distance'): +def get_shortest_paths_distances(graph, pairs, edge_weight_name="distance"): """ Calculate shortest distance between each pair of nodes in a graph @@ -136,7 +141,7 @@ def create_complete_graph(pair_weights, flip_weights=True): g = nx.Graph() for k, v in pair_weights.items(): wt_i = -v if flip_weights else v - g.add_edge(k[0], k[1], **{'distance': v, 'weight': wt_i}) + g.add_edge(k[0], k[1], **{"distance": v, "weight": wt_i}) return g @@ -154,7 +159,7 @@ def dedupe_matching(matching): return list(set(matched_pairs_w_dupes)) -def add_augmenting_path_to_graph(graph, min_weight_pairs, edge_weight_name='weight'): +def add_augmenting_path_to_graph(graph, min_weight_pairs, edge_weight_name="weight"): """ Add the min weight matching edges to the original graph Note the resulting graph could (and likely will) have edges that didn't exist on the original graph. To get the @@ -171,11 +176,14 @@ def add_augmenting_path_to_graph(graph, min_weight_pairs, edge_weight_name='weig """ graph_aug = graph.copy() # so we don't mess with the original graph for pair in min_weight_pairs: - graph_aug.add_edge(pair[0], - pair[1], - **{'distance': nx.dijkstra_path_length(graph, pair[0], pair[1], weight=edge_weight_name), - 'augmented': True} - ) + graph_aug.add_edge( + pair[0], + pair[1], + **{ + "distance": nx.dijkstra_path_length(graph, pair[0], pair[1], weight=edge_weight_name), + "augmented": True, + }, + ) return graph_aug @@ -197,22 +205,29 @@ def create_eulerian_circuit(graph_augmented, graph_original, start_node=None): """ euler_circuit = list(nx.eulerian_circuit(graph_augmented, source=start_node, keys=True)) - assert len(graph_augmented.edges()) == len(euler_circuit), 'graph and euler_circuit do not have equal number of edges.' + assert len(graph_augmented.edges()) == len(euler_circuit), ( + "graph and euler_circuit do not have equal number of edges." + ) for edge in euler_circuit: - aug_path = nx.shortest_path(graph_original, edge[0], edge[1], weight='distance') + aug_path = nx.shortest_path(graph_original, edge[0], edge[1], weight="distance") edge_attr = graph_augmented[edge[0]][edge[1]][edge[2]] - if not edge_attr.get('augmented'): + if not edge_attr.get("augmented"): yield edge + (edge_attr,) else: for edge_aug in list(zip(aug_path[:-1], aug_path[1:])): # find edge with shortest distance (if there are two parallel edges between the same nodes) edge_aug_dict = graph_original[edge_aug[0]][edge_aug[1]] - edge_key = min(edge_aug_dict.keys(), key=(lambda k: edge_aug_dict[k]['distance'])) # index with min distance + edge_key = min( + edge_aug_dict.keys(), key=(lambda k: edge_aug_dict[k]["distance"]) + ) # index with min distance edge_aug_shortest = edge_aug_dict[edge_key] - edge_aug_shortest['augmented'] = True - edge_aug_shortest['id'] = edge_aug_dict[edge_key]['id'] - yield edge_aug + (edge_key, edge_aug_shortest, ) + edge_aug_shortest["augmented"] = True + edge_aug_shortest["id"] = edge_aug_dict[edge_key]["id"] + yield edge_aug + ( + edge_key, + edge_aug_shortest, + ) def create_required_graph(graph): @@ -231,7 +246,7 @@ def create_required_graph(graph): # remove optional edges for e in list(graph_req.edges(data=True, keys=True)): - if not e[3]['required']: + if not e[3]["required"]: graph_req.remove_edge(e[0], e[1], key=e[2]) # remove any nodes left isolated after optional edges are removed (no required incident edges) @@ -252,9 +267,10 @@ def assert_graph_is_connected(graph): True if graph is connected """ - assert nx.algorithms.connected.is_connected(graph), "Sorry, the required graph is not a connected graph after " \ - "the optional edges are removed. This is a requirement for " \ - "this implementation of the RPP here which generalizes to the " \ - "CPP." + assert nx.algorithms.connected.is_connected(graph), ( + "Sorry, the required graph is not a connected graph after " + "the optional edges are removed. This is a requirement for " + "this implementation of the RPP here which generalizes to the " + "CPP." + ) return True - diff --git a/postman_problems/postman_chinese.py b/postman_problems/postman_chinese.py index 2445a70..4d6dcb7 100644 --- a/postman_problems/postman_chinese.py +++ b/postman_problems/postman_chinese.py @@ -2,9 +2,9 @@ def chinese_postman(): - generic_postman('chinese') + generic_postman("chinese") -if __name__ == '__main__': +if __name__ == "__main__": """Parse command line arguments and solve the CPP""" chinese_postman() diff --git a/postman_problems/postman_rural.py b/postman_problems/postman_rural.py index 82616fe..cee3d1a 100644 --- a/postman_problems/postman_rural.py +++ b/postman_problems/postman_rural.py @@ -2,9 +2,9 @@ def rural_postman(): - generic_postman('rural') + generic_postman("rural") -if __name__ == '__main__': +if __name__ == "__main__": """Parse command line arguments and solve the CPP""" rural_postman() diff --git a/postman_problems/postman_template.py b/postman_problems/postman_template.py index 630fbc3..4f21d9d 100644 --- a/postman_problems/postman_template.py +++ b/postman_problems/postman_template.py @@ -11,105 +11,123 @@ def get_args(): Returns: argparse.Namespace: parsed arguments from the user """ - parser = argparse.ArgumentParser(description='Arguments to the Chinese Postman Problem solver') + parser = argparse.ArgumentParser(description="Arguments to the Chinese Postman Problem solver") # --------------------------------------------------------------- # CPP optimization # --------------------------------------------------------------- - parser.add_argument('--edgelist', - required=True, - type=str, - help='Filename of edgelist.' - 'Expected to be comma delimited text file readable with pandas.read_csv' - 'The first two columns should be the "from" and "to" node names.' - 'Additional columns can be provided for edge attributes.' - 'The first row should be the edge attribute names.') - - parser.add_argument('--nodelist', - required=False, - type=str, - default=None, - help='Filename of node attributes (optional).' - 'Expected to be comma delimited text file readable with pandas.read_csv' - 'The first column should be the node name.' - 'Additional columns should be used to provide node attributes.' - 'The first row should be the node attribute names.') - - parser.add_argument('--start_node', - required=False, - type=str, - default=None, - help='Node to start the CPP solution from (optional).' - 'If not provided, a starting node will be selected at random.') - - parser.add_argument('--edge_weight', - required=False, - type=str, - default='distance', - help='Edge attribute used to specify the distance between nodes (optional).' - 'Default is "distance".') + parser.add_argument( + "--edgelist", + required=True, + type=str, + help="Filename of edgelist." + "Expected to be comma delimited text file readable with pandas.read_csv" + 'The first two columns should be the "from" and "to" node names.' + "Additional columns can be provided for edge attributes." + "The first row should be the edge attribute names.", + ) + + parser.add_argument( + "--nodelist", + required=False, + type=str, + default=None, + help="Filename of node attributes (optional)." + "Expected to be comma delimited text file readable with pandas.read_csv" + "The first column should be the node name." + "Additional columns should be used to provide node attributes." + "The first row should be the node attribute names.", + ) + + parser.add_argument( + "--start_node", + required=False, + type=str, + default=None, + help="Node to start the CPP solution from (optional)." + "If not provided, a starting node will be selected at random.", + ) + + parser.add_argument( + "--edge_weight", + required=False, + type=str, + default="distance", + help='Edge attribute used to specify the distance between nodes (optional).Default is "distance".', + ) # --------------------------------------------------------------- # CPP viz # --------------------------------------------------------------- - parser.add_argument('--viz', - action='store_true', - help='Write out the static image of the CPP solution using graphviz?') - - parser.add_argument('--animation', - action='store_true', - help='Write out an animation (GIF) of the CPP solution using a series of static graphviz images') - - parser.add_argument('--viz_engine', - required=False, - type=str, - default='dot', - help='graphviz engine to use for viz layout: "dot", "neato", "fdp", etc) of solution static viz') - - parser.add_argument('--animation_engine', - required=False, - type=str, - default='dot', - help='graphviz engine to use for viz layout: "dot", "neato", "fdp", etc) of solution animation ' - 'viz') - - parser.add_argument('--animation_format', - required=False, - type=str, - default='png', - help='File type to use when writing out animation of CPP solution viz using graphviz.') - - parser.add_argument('--fps', - required=False, - type=float, - default=3, - help='Frames per second to use for CPP solution animation.') - - parser.add_argument('--viz_filename', - required=False, - type=str, - default='cpp_graph.svg', - help='Filename to write static (single) viz of CPP solution to. Do not include the file type ' - 'suffix. This is added with the `format` argument.') - - parser.add_argument('--animation_filename', - required=False, - type=str, - default='cpp_graph.gif', - help='Filename to write animation (GIF) of CPP solution viz to.') - - parser.add_argument('--animation_images_dir', - required=False, - type=str, - default=None, - help='Directory where the series of static visualizations will be produced that get stitched ' - 'into the animation.') + parser.add_argument( + "--viz", action="store_true", help="Write out the static image of the CPP solution using graphviz?" + ) + + parser.add_argument( + "--animation", + action="store_true", + help="Write out an animation (GIF) of the CPP solution using a series of static graphviz images", + ) + + parser.add_argument( + "--viz_engine", + required=False, + type=str, + default="dot", + help='graphviz engine to use for viz layout: "dot", "neato", "fdp", etc) of solution static viz', + ) + + parser.add_argument( + "--animation_engine", + required=False, + type=str, + default="dot", + help='graphviz engine to use for viz layout: "dot", "neato", "fdp", etc) of solution animation viz', + ) + + parser.add_argument( + "--animation_format", + required=False, + type=str, + default="png", + help="File type to use when writing out animation of CPP solution viz using graphviz.", + ) + + parser.add_argument( + "--fps", required=False, type=float, default=3, help="Frames per second to use for CPP solution animation." + ) + + parser.add_argument( + "--viz_filename", + required=False, + type=str, + default="cpp_graph.svg", + help="Filename to write static (single) viz of CPP solution to. Do not include the file type " + "suffix. This is added with the `format` argument.", + ) + + parser.add_argument( + "--animation_filename", + required=False, + type=str, + default="cpp_graph.gif", + help="Filename to write animation (GIF) of CPP solution viz to.", + ) + + parser.add_argument( + "--animation_images_dir", + required=False, + type=str, + default=None, + help="Directory where the series of static visualizations will be produced that get stitched " + "into the animation.", + ) # Grabbing viz file format from the filename args = parser.parse_args() - args.viz_format = os.path.basename(args.viz_filename).split('.')[-1] + args.viz_format = os.path.basename(args.viz_filename).split(".")[-1] return args @@ -122,9 +140,9 @@ def generic_postman(postman_type): postman_type (str): "rural" or "chinese" """ - if postman_type == 'rural': + if postman_type == "rural": postman_algo = rpp - elif postman_type == 'chinese': + elif postman_type == "chinese": postman_algo = cpp # get args @@ -134,44 +152,48 @@ def generic_postman(postman_type): logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) - logger.info('Solving the {} postman problem..'.format(postman_type)) - circuit, graph = postman_algo(edgelist_filename=args.edgelist, - start_node=args.start_node, - edge_weight=args.edge_weight) + logger.info("Solving the {} postman problem..".format(postman_type)) + circuit, graph = postman_algo( + edgelist_filename=args.edgelist, start_node=args.start_node, edge_weight=args.edge_weight + ) - logger.info('Solution:') + logger.info("Solution:") for edge in circuit: logger.info(edge) - logger.info('Solution summary stats:') + logger.info("Solution summary stats:") for k, v in calculate_postman_solution_stats(circuit).items(): - logger.info(str(k) + ' : ' + str(v)) + logger.info(str(k) + " : " + str(v)) if args.viz: - logger.info('Creating single image of {} postman solution...'.format(postman_type)) - message_static = plot_circuit_graphviz(circuit=circuit, - graph=graph, - filename=args.viz_filename, - format=args.viz_format, - engine=args.viz_engine) + logger.info("Creating single image of {} postman solution...".format(postman_type)) + message_static = plot_circuit_graphviz( + circuit=circuit, graph=graph, filename=args.viz_filename, format=args.viz_format, engine=args.viz_engine + ) logger.info(message_static) if args.animation: - animation_images_dir = args.animation_images_dir if args.animation_images_dir else os.path.join( - os.path.dirname(args.animation_filename), 'img') - - logger.info('Creating individual files for animation...') - message_images = make_circuit_images(circuit=circuit, - graph=graph, - outfile_dir=animation_images_dir, - format=args.animation_format, - engine=args.animation_engine) + animation_images_dir = ( + args.animation_images_dir + if args.animation_images_dir + else os.path.join(os.path.dirname(args.animation_filename), "img") + ) + + logger.info("Creating individual files for animation...") + message_images = make_circuit_images( + circuit=circuit, + graph=graph, + outfile_dir=animation_images_dir, + format=args.animation_format, + engine=args.animation_engine, + ) logger.info(message_images) - logger.info('Creating animation...') - message_animation = make_circuit_video(infile_dir_images=animation_images_dir, - outfile_movie=args.animation_filename, - fps=args.fps, - format=args.animation_format) + logger.info("Creating animation...") + message_animation = make_circuit_video( + infile_dir_images=animation_images_dir, + outfile_movie=args.animation_filename, + fps=args.fps, + format=args.animation_format, + ) logger.info(message_animation) - diff --git a/postman_problems/solver.py b/postman_problems/solver.py index 2e904df..75741b5 100644 --- a/postman_problems/solver.py +++ b/postman_problems/solver.py @@ -2,16 +2,25 @@ import logging import networkx as nx -from postman_problems.graph import read_edgelist, create_networkx_graph_from_edgelist, create_required_graph, \ - assert_graph_is_connected, get_odd_nodes, get_shortest_paths_distances, create_complete_graph, dedupe_matching, \ - add_augmenting_path_to_graph, create_eulerian_circuit +from postman_problems.graph import ( + read_edgelist, + create_networkx_graph_from_edgelist, + create_required_graph, + assert_graph_is_connected, + get_odd_nodes, + get_shortest_paths_distances, + create_complete_graph, + dedupe_matching, + add_augmenting_path_to_graph, + create_eulerian_circuit, +) -logger_rpp = logging.getLogger('{0}.{1}'.format(__name__, 'rpp')) -logger_cpp = logging.getLogger('{0}.{1}'.format(__name__, 'cpp')) +logger_rpp = logging.getLogger("{0}.{1}".format(__name__, "rpp")) +logger_cpp = logging.getLogger("{0}.{1}".format(__name__, "cpp")) -def rpp(edgelist_filename, start_node=None, edge_weight='distance', verbose=False): +def rpp(edgelist_filename, start_node=None, edge_weight="distance", verbose=False): """ Solving the RPP from beginning (load network data) to end (finding optimal route). This optimization makes a relatively strong assumption: the starting graph must stay a connected graph when optional edges are removed. @@ -34,35 +43,35 @@ def rpp(edgelist_filename, start_node=None, edge_weight='distance', verbose=Fals logger_rpp.disabled = not verbose - logger_rpp.info('read edgelist') + logger_rpp.info("read edgelist") el = read_edgelist(edgelist_filename, keep_optional=True) - logger_rpp.info('create full and required graph') + logger_rpp.info("create full and required graph") g_full = create_networkx_graph_from_edgelist(el) g_req = create_required_graph(g_full) assert_graph_is_connected(g_req) - logger_rpp.info('getting odd node pairs') + logger_rpp.info("getting odd node pairs") odd_nodes = get_odd_nodes(g_req) odd_node_pairs = itertools.combinations(odd_nodes, 2) - logger_rpp.info('get shortest paths between odd nodes') + logger_rpp.info("get shortest paths between odd nodes") odd_node_pairs_shortest_paths = get_shortest_paths_distances(g_full, odd_node_pairs, edge_weight) - logger_rpp.info('Find min weight matching using blossom algorithm') + logger_rpp.info("Find min weight matching using blossom algorithm") g_odd_complete = create_complete_graph(odd_node_pairs_shortest_paths, flip_weights=True) odd_matching = nx.algorithms.max_weight_matching(g_odd_complete, True) - logger_rpp.info('add the min weight matching edges to g') + logger_rpp.info("add the min weight matching edges to g") g_aug = add_augmenting_path_to_graph(g_req, odd_matching) - logger_rpp.info('get eulerian circuit route') + logger_rpp.info("get eulerian circuit route") circuit = list(create_eulerian_circuit(g_aug, g_full, start_node)) return circuit, g_full -def cpp(edgelist_filename, start_node=None, edge_weight='distance', verbose=False): +def cpp(edgelist_filename, start_node=None, edge_weight="distance", verbose=False): """ Solving the CPP from beginning (load network data) to end (finding optimal route). Can be run from command line with arguments from cpp.py, or from an interactive Python session (ex jupyter notebook) @@ -83,23 +92,23 @@ def cpp(edgelist_filename, start_node=None, edge_weight='distance', verbose=Fals """ logger_cpp.disabled = not verbose - logger_cpp.info('read edgelist and create base graph') + logger_cpp.info("read edgelist and create base graph") el = read_edgelist(edgelist_filename, keep_optional=False) g = create_networkx_graph_from_edgelist(el) - logger_cpp.info('get augmenting path for odd nodes') + logger_cpp.info("get augmenting path for odd nodes") odd_nodes = get_odd_nodes(g) odd_node_pairs = itertools.combinations(odd_nodes, 2) odd_node_pairs_shortest_paths = get_shortest_paths_distances(g, odd_node_pairs, edge_weight) g_odd_complete = create_complete_graph(odd_node_pairs_shortest_paths, flip_weights=True) - logger_cpp.info('Find min weight matching using blossom algorithm') + logger_cpp.info("Find min weight matching using blossom algorithm") odd_matching = nx.algorithms.max_weight_matching(g_odd_complete, True) - logger_cpp.info('add the min weight matching edges to g') + logger_cpp.info("add the min weight matching edges to g") g_aug = add_augmenting_path_to_graph(g, odd_matching) - logger_cpp.info('get eulerian circuit route') + logger_cpp.info("get eulerian circuit route") circuit = list(create_eulerian_circuit(g_aug, g, start_node)) return circuit, g diff --git a/postman_problems/stats.py b/postman_problems/stats.py index f5915bd..af68455 100644 --- a/postman_problems/stats.py +++ b/postman_problems/stats.py @@ -1,7 +1,7 @@ import collections -def calculate_postman_solution_stats(circuit, edge_weight_name='distance'): +def calculate_postman_solution_stats(circuit, edge_weight_name="distance"): """ Calculate summary stats on the route Args: @@ -14,17 +14,19 @@ def calculate_postman_solution_stats(circuit, edge_weight_name='distance'): summary_stats = collections.OrderedDict() # Distance - summary_stats['distance_walked'] = sum([e[3][edge_weight_name] for e in circuit]) - summary_stats['distance_doublebacked'] = sum([e[3][edge_weight_name] for e in circuit if 'augmented' in e[3]]) - summary_stats['distance_walked_once'] = summary_stats['distance_walked'] - summary_stats['distance_doublebacked'] - summary_stats['distance_walked_optional'] = sum([e[3]['distance'] for e in circuit if e[3].get('required') == 0]) - summary_stats['distance_walked_required'] = summary_stats['distance_walked'] - summary_stats['distance_walked_optional'] + summary_stats["distance_walked"] = sum([e[3][edge_weight_name] for e in circuit]) + summary_stats["distance_doublebacked"] = sum([e[3][edge_weight_name] for e in circuit if "augmented" in e[3]]) + summary_stats["distance_walked_once"] = summary_stats["distance_walked"] - summary_stats["distance_doublebacked"] + summary_stats["distance_walked_optional"] = sum([e[3]["distance"] for e in circuit if e[3].get("required") == 0]) + summary_stats["distance_walked_required"] = ( + summary_stats["distance_walked"] - summary_stats["distance_walked_optional"] + ) # Number of edges - summary_stats['edges_walked'] = len(circuit) - summary_stats['edges_doublebacked'] = collections.Counter([e[3].get('augmented') for e in circuit])[True] - summary_stats['edges_walked_once'] = summary_stats['edges_walked'] - summary_stats['edges_doublebacked'] - summary_stats['edges_walked_optional'] = collections.Counter([e[3].get('required') for e in circuit])[0] - summary_stats['edges_walked_required'] = summary_stats['edges_walked'] - summary_stats['edges_walked_optional'] + summary_stats["edges_walked"] = len(circuit) + summary_stats["edges_doublebacked"] = collections.Counter([e[3].get("augmented") for e in circuit])[True] + summary_stats["edges_walked_once"] = summary_stats["edges_walked"] - summary_stats["edges_doublebacked"] + summary_stats["edges_walked_optional"] = collections.Counter([e[3].get("required") for e in circuit])[0] + summary_stats["edges_walked_required"] = summary_stats["edges_walked"] - summary_stats["edges_walked_optional"] return summary_stats diff --git a/postman_problems/tests/test_chinese_postman.py b/postman_problems/tests/test_chinese_postman.py index 53960ea..c3d09ce 100644 --- a/postman_problems/tests/test_chinese_postman.py +++ b/postman_problems/tests/test_chinese_postman.py @@ -10,14 +10,26 @@ # input params -EDGELIST_SEVEN_BRIDGES = pkg_resources.resource_filename('postman_problems', 'examples/seven_bridges/edgelist_seven_bridges.csv') -EDGELIST_SLEEPING_GIANT = pkg_resources.resource_filename('postman_problems', 'examples/sleeping_giant/edgelist_sleeping_giant.csv') -NODELIST_SLEEPING_GIANT = pkg_resources.resource_filename('postman_problems', 'examples/sleeping_giant/nodelist_sleeping_giant.csv') +EDGELIST_SEVEN_BRIDGES = pkg_resources.resource_filename( + "postman_problems", "examples/seven_bridges/edgelist_seven_bridges.csv" +) +EDGELIST_SLEEPING_GIANT = pkg_resources.resource_filename( + "postman_problems", "examples/sleeping_giant/edgelist_sleeping_giant.csv" +) +NODELIST_SLEEPING_GIANT = pkg_resources.resource_filename( + "postman_problems", "examples/sleeping_giant/nodelist_sleeping_giant.csv" +) # output params -OUT_SEVEN_BRIDGES_SVG = pkg_resources.resource_filename('postman_problems', 'examples/seven_bridges/output/cpp_graph.svg') -OUT_SEVEN_BRIDGES_GIF = pkg_resources.resource_filename('postman_problems', 'examples/seven_bridges/output/cpp_graph.gif') -OUT_SLEEPING_GIANT_SVG = pkg_resources.resource_filename('postman_problems', 'examples/sleeping_giant/output/cpp_graph.svg') +OUT_SEVEN_BRIDGES_SVG = pkg_resources.resource_filename( + "postman_problems", "examples/seven_bridges/output/cpp_graph.svg" +) +OUT_SEVEN_BRIDGES_GIF = pkg_resources.resource_filename( + "postman_problems", "examples/seven_bridges/output/cpp_graph.gif" +) +OUT_SLEEPING_GIANT_SVG = pkg_resources.resource_filename( + "postman_problems", "examples/sleeping_giant/output/cpp_graph.svg" +) def test_chinese_postman_seven_bridges(): @@ -29,18 +41,22 @@ def test_chinese_postman_seven_bridges(): tmpdir = tempfile.mkdtemp() saved_umask = os.umask(00) # 0077 - testargs = ["chinese_postman", - "--edgelist", EDGELIST_SEVEN_BRIDGES, - "--viz", - "--animation", - "--viz_filename", os.path.join(tmpdir, 'test_cpp_graph.png'), - "--animation_filename", os.path.join(tmpdir, 'test_cpp_graph.gif') - ] - with patch.object(sys, 'argv', testargs): + testargs = [ + "chinese_postman", + "--edgelist", + EDGELIST_SEVEN_BRIDGES, + "--viz", + "--animation", + "--viz_filename", + os.path.join(tmpdir, "test_cpp_graph.png"), + "--animation_filename", + os.path.join(tmpdir, "test_cpp_graph.gif"), + ] + with patch.object(sys, "argv", testargs): chinese_postman() - assert os.path.isfile(os.path.join(tmpdir, 'test_cpp_graph.png')) - assert os.path.isfile(os.path.join(tmpdir, 'test_cpp_graph.gif')) + assert os.path.isfile(os.path.join(tmpdir, "test_cpp_graph.png")) + assert os.path.isfile(os.path.join(tmpdir, "test_cpp_graph.gif")) # clean up os.umask(saved_umask) @@ -48,11 +64,8 @@ def test_chinese_postman_seven_bridges(): def test_chinese_postman_sleeping_giant(): - testargs = ["chinese_postman", - "--edgelist", EDGELIST_SLEEPING_GIANT, - "--nodelist", NODELIST_SLEEPING_GIANT - ] - with patch.object(sys, 'argv', testargs): + testargs = ["chinese_postman", "--edgelist", EDGELIST_SLEEPING_GIANT, "--nodelist", NODELIST_SLEEPING_GIANT] + with patch.object(sys, "argv", testargs): chinese_postman() @@ -61,7 +74,7 @@ def test_entry_point_example_chinese_postman_seven_bridges(script_runner): Just testing that seven_bridges example runs with pre-parameterized config. Will overwrite output in the examples dir... that's OK. """ - ret = script_runner.run('chinese_postman_seven_bridges') + ret = script_runner.run("chinese_postman_seven_bridges") assert ret.success assert os.path.isfile(OUT_SEVEN_BRIDGES_SVG) assert os.path.isfile(OUT_SEVEN_BRIDGES_GIF) @@ -74,6 +87,6 @@ def test_entry_point_example_chinese_postman_sleeping_giant(script_runner): Just testing that Sleeping Giant example run with pre-parameterized config. Will overwrite output in the examples dir... that's OK. """ - ret = script_runner.run('chinese_postman_sleeping_giant') + ret = script_runner.run("chinese_postman_sleeping_giant") assert ret.success assert os.path.isfile(OUT_SLEEPING_GIANT_SVG) diff --git a/postman_problems/tests/test_example_sleeping_giant.py b/postman_problems/tests/test_example_sleeping_giant.py index b6778b4..6dd0904 100644 --- a/postman_problems/tests/test_example_sleeping_giant.py +++ b/postman_problems/tests/test_example_sleeping_giant.py @@ -5,7 +5,10 @@ import networkx as nx from postman_problems.viz import add_node_attributes from postman_problems.graph import ( - read_edgelist, create_networkx_graph_from_edgelist, get_odd_nodes, get_shortest_paths_distances + read_edgelist, + create_networkx_graph_from_edgelist, + get_odd_nodes, + get_shortest_paths_distances, ) from postman_problems.solver import rpp, cpp @@ -13,43 +16,44 @@ # PARAMETERS / DATA # # ################### -EDGELIST = pkg_resources.resource_filename('postman_problems', 'examples/sleeping_giant/edgelist_sleeping_giant.csv') -NODELIST = pkg_resources.resource_filename('postman_problems', 'examples/sleeping_giant/nodelist_sleeping_giant.csv') -START_NODE = 'b_end_east' +EDGELIST = pkg_resources.resource_filename("postman_problems", "examples/sleeping_giant/edgelist_sleeping_giant.csv") +NODELIST = pkg_resources.resource_filename("postman_problems", "examples/sleeping_giant/nodelist_sleeping_giant.csv") +START_NODE = "b_end_east" ######### # TESTS # ######### + def test_read_sleeping_giant_edgelist(): df = read_edgelist(EDGELIST, keep_optional=True) # check that our Sleeping Giant example dataset contains the correct fields and values - assert ['node1', 'node2', 'trail', 'color', 'distance', 'estimate', 'required'] in df.columns.values - assert math.isclose(df[df['required'] == 1]['distance'].sum(), 26.01) - assert math.isclose(df['distance'].sum(), 30.48) + assert ["node1", "node2", "trail", "color", "distance", "estimate", "required"] in df.columns.values + assert math.isclose(df[df["required"] == 1]["distance"].sum(), 26.01) + assert math.isclose(df["distance"].sum(), 30.48) df_req = read_edgelist(EDGELIST, keep_optional=False) - assert math.isclose(df_req['distance'].sum(), 26.01) - assert 'req' not in df_req.columns + assert math.isclose(df_req["distance"].sum(), 26.01) + assert "req" not in df_req.columns def test_create_networkx_graph_from_edgelist(): df = read_edgelist(EDGELIST, keep_optional=True) - graph = create_networkx_graph_from_edgelist(df, edge_id='id') + graph = create_networkx_graph_from_edgelist(df, edge_id="id") # check that our starting graph is created correctly assert isinstance(graph, nx.MultiGraph) assert len(graph.edges()) == 133 assert len(graph.nodes()) == 78 - assert graph['b_end_east']['b_y'][0]['color'] == 'blue' - assert graph['b_end_east']['b_y'][0]['trail'] == 'b' - assert graph['b_end_east']['b_y'][0]['distance'] == 1.32 + assert graph["b_end_east"]["b_y"][0]["color"] == "blue" + assert graph["b_end_east"]["b_y"][0]["trail"] == "b" + assert graph["b_end_east"]["b_y"][0]["distance"] == 1.32 # check that starting graph with required trails only is correct df_req = read_edgelist(EDGELIST, keep_optional=False) - graph_req = create_networkx_graph_from_edgelist(df_req, edge_id='id') + graph_req = create_networkx_graph_from_edgelist(df_req, edge_id="id") assert isinstance(graph_req, nx.MultiGraph) assert len(graph_req.edges()) == 121 assert len(graph_req.nodes()) == 74 @@ -58,7 +62,7 @@ def test_create_networkx_graph_from_edgelist(): def test_add_node_attributes(): # create objects for testing df = read_edgelist(EDGELIST) - graph = create_networkx_graph_from_edgelist(df, edge_id='id') + graph = create_networkx_graph_from_edgelist(df, edge_id="id") nodelist_df = pd.read_csv(NODELIST) graph_node_attrs = add_node_attributes(graph, nodelist_df) @@ -66,29 +70,29 @@ def test_add_node_attributes(): # check that each node attribute has an X and Y coordinate for k, v in graph_node_attrs.nodes(data=True): - assert 'X' in v - assert 'Y' in v + assert "X" in v + assert "Y" in v # spot check node attributes for first node node_data_from_graph = list(graph_node_attrs.nodes(data=True)) node_names = [n[0] for n in node_data_from_graph] - assert 'rs_end_north' in node_names + assert "rs_end_north" in node_names - key = node_names.index('rs_end_north') - assert node_data_from_graph[key][1]['X'] == 1772 - assert node_data_from_graph[key][1]['Y'] == 172 + key = node_names.index("rs_end_north") + assert node_data_from_graph[key][1]["X"] == 1772 + assert node_data_from_graph[key][1]["Y"] == 172 def test_get_shortest_paths_distances(): df = read_edgelist(EDGELIST) - graph = create_networkx_graph_from_edgelist(df, edge_id='id') + graph = create_networkx_graph_from_edgelist(df, edge_id="id") odd_nodes = get_odd_nodes(graph) odd_node_pairs = list(itertools.combinations(odd_nodes, 2)) # coarsely checking structure of `get_shortest_paths_distances` return value - odd_node_pairs_shortest_paths = get_shortest_paths_distances(graph, odd_node_pairs, 'distance') + odd_node_pairs_shortest_paths = get_shortest_paths_distances(graph, odd_node_pairs, "distance") assert len(odd_node_pairs_shortest_paths) == 630 assert type(odd_node_pairs_shortest_paths) == dict @@ -104,16 +108,18 @@ def test_nodelist_edgelist_overlap(): """ eldf = read_edgelist(EDGELIST, keep_optional=True) nldf = pd.read_csv(NODELIST) - edgelist_nodes = set(eldf['node1'].append(eldf['node2'])) - nodelist_nodes = set(nldf['id']) + edgelist_nodes = set(eldf["node1"].append(eldf["node2"])) + nodelist_nodes = set(nldf["id"]) nodes_in_el_but_not_nl = edgelist_nodes - nodelist_nodes - assert nodes_in_el_but_not_nl == set(), \ + assert nodes_in_el_but_not_nl == set(), ( "Warning: The following nodes are in the edgelist, but not the nodelist: {}".format(nodes_in_el_but_not_nl) + ) nodes_in_nl_but_not_el = nodelist_nodes - edgelist_nodes - assert nodes_in_nl_but_not_el == set(), \ + assert nodes_in_nl_but_not_el == set(), ( "Warning: The following nodes are in the nodelist, but not the edgelist: {}".format(nodes_in_nl_but_not_el) + ) def test_sleeping_giant_cpp_solution(): @@ -123,7 +129,7 @@ def test_sleeping_giant_cpp_solution(): assert len(cpp_solution) == 155 # make sure our total mileage is correct - cpp_solution_distance = sum([edge[3]['distance'] for edge in cpp_solution]) + cpp_solution_distance = sum([edge[3]["distance"] for edge in cpp_solution]) assert math.isclose(cpp_solution_distance, 33.25) # make sure our circuit begins and ends at the same place @@ -131,7 +137,7 @@ def test_sleeping_giant_cpp_solution(): # make sure original graph is properly returned assert len(graph.edges()) == 121 - [e[2].get('augmented') for e in graph.edges(data=True)].count(True) == 35 + [e[2].get("augmented") for e in graph.edges(data=True)].count(True) == 35 def test_sleeping_giant_rpp_solution(): @@ -141,7 +147,7 @@ def test_sleeping_giant_rpp_solution(): assert len(rpp_solution) == 151 # make sure our total mileage is correct - rpp_solution_distance = sum([edge[3]['distance'] for edge in rpp_solution]) + rpp_solution_distance = sum([edge[3]["distance"] for edge in rpp_solution]) assert math.isclose(rpp_solution_distance, 32.12) # make sure our circuit begins and ends at the same place @@ -149,5 +155,4 @@ def test_sleeping_giant_rpp_solution(): # make sure original graph is properly returned assert len(graph.edges()) == 133 - [e[3].get('augmented') for e in graph.edges(data=True, keys=True)].count(True) == 30 - + [e[3].get("augmented") for e in graph.edges(data=True, keys=True)].count(True) == 30 diff --git a/postman_problems/tests/test_graph.py b/postman_problems/tests/test_graph.py index 53c7a69..dcf340f 100644 --- a/postman_problems/tests/test_graph.py +++ b/postman_problems/tests/test_graph.py @@ -5,9 +5,17 @@ import networkx as nx import pytest from postman_problems.graph import ( - read_edgelist, create_networkx_graph_from_edgelist, get_odd_nodes, get_even_nodes, get_shortest_paths_distances, - create_complete_graph, dedupe_matching, add_augmenting_path_to_graph, create_eulerian_circuit, - assert_graph_is_connected, create_required_graph + read_edgelist, + create_networkx_graph_from_edgelist, + get_odd_nodes, + get_even_nodes, + get_shortest_paths_distances, + create_complete_graph, + dedupe_matching, + add_augmenting_path_to_graph, + create_eulerian_circuit, + assert_graph_is_connected, + create_required_graph, ) @@ -15,18 +23,19 @@ # PARAMETERS / DATA # # ################### -NODE_PAIRS = {('a', 'b'): 2, ('b', 'c'): 5, ('c', 'd'): 10} -MATCHING = {'a': 'b', 'd': 'c', 'b': 'a', 'c': 'd'} +NODE_PAIRS = {("a", "b"): 2, ("b", "c"): 5, ("c", "d"): 10} +MATCHING = {"a": "b", "d": "c", "b": "a", "c": "d"} ######### # TESTS # ######### + def test_read_edgelist(GRAPH_1_EDGELIST_CSV): df = read_edgelist(GRAPH_1_EDGELIST_CSV) assert df.shape == (5, 3) - assert set(df.columns) == set(['distance', 'node1', 'node2']) + assert set(df.columns) == set(["distance", "node1", "node2"]) def test_read_edgelist_w_ids(GRAPH_1_EDGELIST_W_ID_CSV): @@ -39,31 +48,31 @@ def test_read_edgelist_w_ids(GRAPH_1_EDGELIST_W_ID_CSV): assert "Edgelist contains field named 'id'" in str(w[-1].message) assert df.shape == (5, 4) - assert set(df.columns) == set(['distance', 'node1', 'node2', 'id']) + assert set(df.columns) == set(["distance", "node1", "node2", "id"]) def _test_graph_structure(graph): assert len(graph.edges()) == 5 assert len(graph.nodes()) == 4 - assert graph['a']['b'][0]['distance'] == 5 - assert set([e[3]['distance'] for e in graph.edges(data=True, keys=True)]) == set([5, 20, 10, 2, 3]) + assert graph["a"]["b"][0]["distance"] == 5 + assert set([e[3]["distance"] for e in graph.edges(data=True, keys=True)]) == set([5, 20, 10, 2, 3]) def test_create_networkx_graph_from_edgelist_w_ids_warning(GRAPH_1_EDGELIST_DF_W_ID): with warnings.catch_warnings(record=True) as w: - graph = create_networkx_graph_from_edgelist(GRAPH_1_EDGELIST_DF_W_ID, edge_id='id') + graph = create_networkx_graph_from_edgelist(GRAPH_1_EDGELIST_DF_W_ID, edge_id="id") # make sure correct warning was given assert len(w) == 1 assert issubclass(w[-1].category, UserWarning) - assert 'already an edge attribute in `edgelist`' in str(w[-1].message) + assert "already an edge attribute in `edgelist`" in str(w[-1].message) # make sure our graph is as it should be _test_graph_structure(graph) def test_create_networkx_graph_from_edgelist_w_ids(GRAPH_1_EDGELIST_DF): - graph = create_networkx_graph_from_edgelist(GRAPH_1_EDGELIST_DF, edge_id='id') + graph = create_networkx_graph_from_edgelist(GRAPH_1_EDGELIST_DF, edge_id="id") _test_graph_structure(graph) # make sure our graph is as it should be @@ -79,10 +88,10 @@ def test_get_shortest_paths_distances(GRAPH_1): odd_node_pairs = list(itertools.combinations(odd_nodes, 2)) # coarsely checking structure of `get_shortest_paths_distances` return value - odd_node_pairs_shortest_paths = get_shortest_paths_distances(GRAPH_1, odd_node_pairs, 'distance') + odd_node_pairs_shortest_paths = get_shortest_paths_distances(GRAPH_1, odd_node_pairs, "distance") assert len(odd_node_pairs_shortest_paths) == 1 assert type(odd_node_pairs_shortest_paths) == dict - bc_key = ('b', 'c') if ('b', 'c') in odd_node_pairs_shortest_paths else ('c', 'b') # tuple keys are unordered + bc_key = ("b", "c") if ("b", "c") in odd_node_pairs_shortest_paths else ("c", "b") # tuple keys are unordered assert odd_node_pairs_shortest_paths[bc_key] == 5 @@ -90,58 +99,56 @@ def test_create_complete_graph(): # with flipped weights graph_complete = create_complete_graph(NODE_PAIRS, flip_weights=True) assert len(graph_complete.edges()) == 3 - assert set([e[2]['distance'] for e in graph_complete.edges(data=True)]) == set([2, 5, 10]) - assert set([e[2]['weight'] for e in graph_complete.edges(data=True)]) == set([-2, -5, -10]) + assert set([e[2]["distance"] for e in graph_complete.edges(data=True)]) == set([2, 5, 10]) + assert set([e[2]["weight"] for e in graph_complete.edges(data=True)]) == set([-2, -5, -10]) # without flipped weights graph_complete_noflip = create_complete_graph(NODE_PAIRS, flip_weights=False) assert len(graph_complete_noflip.edges()) == 3 - assert set([e[2]['distance'] for e in graph_complete_noflip.edges(data=True)]) == set([2, 5, 10]) - assert set([e[2]['weight'] for e in graph_complete_noflip.edges(data=True)]) == set([2, 5, 10]) + assert set([e[2]["distance"] for e in graph_complete_noflip.edges(data=True)]) == set([2, 5, 10]) + assert set([e[2]["weight"] for e in graph_complete_noflip.edges(data=True)]) == set([2, 5, 10]) def test_dedupe_matching(): deduped_edges = [set(edge) for edge in dedupe_matching(MATCHING)] - assert set(['b', 'a']) in deduped_edges - assert set(['c', 'd']) in deduped_edges + assert set(["b", "a"]) in deduped_edges + assert set(["c", "d"]) in deduped_edges assert len(deduped_edges) == 2 def test_add_augmenting_path_to_graph(GRAPH_1): - graph_aug = add_augmenting_path_to_graph(GRAPH_1, [('b', 'c')], 'distance') + graph_aug = add_augmenting_path_to_graph(GRAPH_1, [("b", "c")], "distance") assert len(graph_aug.edges()) == 6 - assert sum([e[3]['distance'] for e in graph_aug.edges(data=True, keys=True)]) == 45 - assert [set([e[0], e[1]]) for e in graph_aug.edges(data=True)].count(set(['b', 'c'])) == 2 + assert sum([e[3]["distance"] for e in graph_aug.edges(data=True, keys=True)]) == 45 + assert [set([e[0], e[1]]) for e in graph_aug.edges(data=True)].count(set(["b", "c"])) == 2 def test_create_eulerian_circuit(GRAPH_1): - graph_aug = add_augmenting_path_to_graph(GRAPH_1, [('b', 'c')], 'distance') - circuit = list(create_eulerian_circuit(graph_aug, GRAPH_1, 'a')) + graph_aug = add_augmenting_path_to_graph(GRAPH_1, [("b", "c")], "distance") + circuit = list(create_eulerian_circuit(graph_aug, GRAPH_1, "a")) assert len(circuit) == 7 - assert sum([e[3]['distance'] for e in circuit]) == 45 - assert circuit[0][0] == 'a' - assert circuit[-1][1] == 'a' - assert collections.Counter([e[3]['id'] for e in circuit]) == collections.Counter({4: 2, 5: 2, 2: 1, 3: 1, 1: 1}) + assert sum([e[3]["distance"] for e in circuit]) == 45 + assert circuit[0][0] == "a" + assert circuit[-1][1] == "a" + assert collections.Counter([e[3]["id"] for e in circuit]) == collections.Counter({4: 2, 5: 2, 2: 1, 3: 1, 1: 1}) def test_check_graph_is_connected(GRAPH_1): assert assert_graph_is_connected(GRAPH_1) # check that a connected graph is deemed as such GRAPH_2_COMP = GRAPH_1.copy() - GRAPH_2_COMP.add_edge('e', 'f') + GRAPH_2_COMP.add_edge("e", "f") with pytest.raises(AssertionError): assert_graph_is_connected(GRAPH_2_COMP) # check that unconnected graph raises assertion def test_create_required_graph(GRAPH_1): GRAPH_1_FULL = GRAPH_1.copy() - nx.set_edge_attributes(GRAPH_1_FULL, 1, 'required') - GRAPH_1_FULL['b']['d'][0]['required'] = 0 - GRAPH_1_FULL['c']['d'][0]['required'] = False # testing 0 and False values for 'required' + nx.set_edge_attributes(GRAPH_1_FULL, 1, "required") + GRAPH_1_FULL["b"]["d"][0]["required"] = 0 + GRAPH_1_FULL["c"]["d"][0]["required"] = False # testing 0 and False values for 'required' GRAPH_1_REQ = create_required_graph(GRAPH_1_FULL) - assert set(GRAPH_1_REQ.nodes()) == set(['a', 'b', 'c']) - assert set(GRAPH_1.nodes()) == set(['a', 'b', 'c', 'd']) + assert set(GRAPH_1_REQ.nodes()) == set(["a", "b", "c"]) + assert set(GRAPH_1.nodes()) == set(["a", "b", "c", "d"]) assert len(GRAPH_1_REQ.edges()) == 3 assert len(GRAPH_1.edges()) == 5 - - diff --git a/postman_problems/tests/test_rural_postman.py b/postman_problems/tests/test_rural_postman.py index 7a77164..11399c2 100644 --- a/postman_problems/tests/test_rural_postman.py +++ b/postman_problems/tests/test_rural_postman.py @@ -8,12 +8,18 @@ # input params -EDGELIST_SLEEPING_GIANT = pkg_resources.resource_filename('postman_problems', 'examples/sleeping_giant/edgelist_sleeping_giant.csv') -NODELIST_SLEEPING_GIANT = pkg_resources.resource_filename('postman_problems', 'examples/sleeping_giant/nodelist_sleeping_giant.csv') +EDGELIST_SLEEPING_GIANT = pkg_resources.resource_filename( + "postman_problems", "examples/sleeping_giant/edgelist_sleeping_giant.csv" +) +NODELIST_SLEEPING_GIANT = pkg_resources.resource_filename( + "postman_problems", "examples/sleeping_giant/nodelist_sleeping_giant.csv" +) # output params -OUT_SLEEPING_GIANT_SVG = pkg_resources.resource_filename('postman_problems', 'examples/sleeping_giant/output/rpp_graph.svg') -OUT_STAR_SVG = pkg_resources.resource_filename('postman_problems', 'examples/star/output/rpp_graph.svg') +OUT_SLEEPING_GIANT_SVG = pkg_resources.resource_filename( + "postman_problems", "examples/sleeping_giant/output/rpp_graph.svg" +) +OUT_STAR_SVG = pkg_resources.resource_filename("postman_problems", "examples/star/output/rpp_graph.svg") @pytest.mark.slow @@ -23,7 +29,7 @@ def test_entry_point_example_rural_postman_problem_sleeping_giant(script_runner) Just testing that Sleeping Giant example runs with pre-parameterized config. Will overwrite output in the examples dir... that's OK. """ - ret = script_runner.run('rural_postman_sleeping_giant') + ret = script_runner.run("rural_postman_sleeping_giant") assert ret.success assert os.path.isfile(OUT_SLEEPING_GIANT_SVG) @@ -33,15 +39,12 @@ def test_entry_point_example_rural_postman_problem_star(script_runner): Just testing that star example runs with pre-parameterized config. Will overwrite output in the examples dir... that's OK. """ - ret = script_runner.run('rural_postman_star') + ret = script_runner.run("rural_postman_star") assert ret.success assert os.path.isfile(OUT_STAR_SVG) def test_rural_postman_sleeping_giant(): - testargs = ["rural_postman", - "--edgelist", EDGELIST_SLEEPING_GIANT, - "--nodelist", NODELIST_SLEEPING_GIANT - ] - with patch.object(sys, 'argv', testargs): + testargs = ["rural_postman", "--edgelist", EDGELIST_SLEEPING_GIANT, "--nodelist", NODELIST_SLEEPING_GIANT] + with patch.object(sys, "argv", testargs): rural_postman() diff --git a/postman_problems/tests/test_solver.py b/postman_problems/tests/test_solver.py index e75934e..b9c46b5 100644 --- a/postman_problems/tests/test_solver.py +++ b/postman_problems/tests/test_solver.py @@ -2,12 +2,12 @@ from postman_problems.solver import cpp, rpp from postman_problems.tests.test_stats import ( test_stats_on_simple_graph_required_edges_only, - test_stats_on_star_graph_with_optional_edges + test_stats_on_star_graph_with_optional_edges, ) def test_cpp_graph_1(GRAPH_1_EDGELIST_CSV): - circuit, graph = cpp(GRAPH_1_EDGELIST_CSV, start_node='a') + circuit, graph = cpp(GRAPH_1_EDGELIST_CSV, start_node="a") assert len(circuit) == 7 # Re-use the test for the summary_stats function to ensure CPP solution is correct @@ -15,7 +15,7 @@ def test_cpp_graph_1(GRAPH_1_EDGELIST_CSV): def test_rpp_graph_2(GRAPH_2_EDGELIST_CSV): - circuit, graph = rpp(GRAPH_2_EDGELIST_CSV, start_node='a') + circuit, graph = rpp(GRAPH_2_EDGELIST_CSV, start_node="a") assert len(circuit) == 6 # Re-use the test for the summary_stats function to ensure RPP solution is correct @@ -25,7 +25,4 @@ def test_rpp_graph_2(GRAPH_2_EDGELIST_CSV): def test_rpp_graph_3(GRAPH_3_EDGELIST_CSV): """Testing that RPP fails on graph with 2 connected components when optional edges are removed.""" with pytest.raises(AssertionError): - _, _ = rpp(GRAPH_3_EDGELIST_CSV, start_node='a') - - - + _, _ = rpp(GRAPH_3_EDGELIST_CSV, start_node="a") diff --git a/postman_problems/tests/test_stats.py b/postman_problems/tests/test_stats.py index fd783c6..c727db3 100644 --- a/postman_problems/tests/test_stats.py +++ b/postman_problems/tests/test_stats.py @@ -5,32 +5,31 @@ def test_stats_on_simple_graph_required_edges_only(GRAPH_1_CIRCUIT_CPP): stats = calculate_postman_solution_stats(GRAPH_1_CIRCUIT_CPP) assert isinstance(stats, dict) - assert stats['distance_walked'] == 45 - assert stats['distance_doublebacked'] == 5 - assert stats['distance_walked_once'] == 40 - assert stats['distance_walked_required'] == 45 - assert stats['distance_walked_optional'] == 0 + assert stats["distance_walked"] == 45 + assert stats["distance_doublebacked"] == 5 + assert stats["distance_walked_once"] == 40 + assert stats["distance_walked_required"] == 45 + assert stats["distance_walked_optional"] == 0 - assert stats['edges_walked'] == 7 - assert stats['edges_doublebacked'] == 2 - assert stats['edges_walked_once'] == 5 - assert stats['edges_walked_required'] == 7 - assert stats['edges_walked_optional'] == 0 + assert stats["edges_walked"] == 7 + assert stats["edges_doublebacked"] == 2 + assert stats["edges_walked_once"] == 5 + assert stats["edges_walked_required"] == 7 + assert stats["edges_walked_optional"] == 0 def test_stats_on_star_graph_with_optional_edges(GRAPH_2_CIRCUIT_RPP): stats = calculate_postman_solution_stats(GRAPH_2_CIRCUIT_RPP) assert isinstance(stats, dict) - assert stats['distance_walked'] == 116 - assert stats['distance_doublebacked'] == 6 - assert stats['distance_walked_once'] == 110 - assert stats['distance_walked_required'] == 110 - assert stats['distance_walked_optional'] == 6 - - assert stats['edges_walked'] == 6 - assert stats['edges_doublebacked'] == 2 - assert stats['edges_walked_once'] == 4 - assert stats['edges_walked_required'] == 4 - assert stats['edges_walked_optional'] == 2 - + assert stats["distance_walked"] == 116 + assert stats["distance_doublebacked"] == 6 + assert stats["distance_walked_once"] == 110 + assert stats["distance_walked_required"] == 110 + assert stats["distance_walked_optional"] == 6 + + assert stats["edges_walked"] == 6 + assert stats["edges_doublebacked"] == 2 + assert stats["edges_walked_once"] == 4 + assert stats["edges_walked_required"] == 4 + assert stats["edges_walked_optional"] == 2 diff --git a/postman_problems/tests/test_viz.py b/postman_problems/tests/test_viz.py index 24604c8..31c9274 100644 --- a/postman_problems/tests/test_viz.py +++ b/postman_problems/tests/test_viz.py @@ -7,25 +7,26 @@ # PARAMETERS / DATA # # ################### -GRAPH = nx.MultiGraph([ - ('a', 'b', {'id': 1, 'distance': 5}), - ('a', 'c', {'id': 2, 'distance': 20}), - ('b', 'c', {'id': 3, 'distance': 10}), - ('c', 'd', {'id': 4, 'distance': 3}), - ('d', 'b', {'id': 5, 'distance': 2}) -]) +GRAPH = nx.MultiGraph( + [ + ("a", "b", {"id": 1, "distance": 5}), + ("a", "c", {"id": 2, "distance": 20}), + ("b", "c", {"id": 3, "distance": 10}), + ("c", "d", {"id": 4, "distance": 3}), + ("d", "b", {"id": 5, "distance": 2}), + ] +) -NODE_ATTRIBUTES = pd.DataFrame({ - 'id': ['a', 'b', 'c', 'd'], - 'attr_fruit': ['apple', 'banana', 'cherry', 'durian'] -}) +NODE_ATTRIBUTES = pd.DataFrame({"id": ["a", "b", "c", "d"], "attr_fruit": ["apple", "banana", "cherry", "durian"]}) ######### # TESTS # ######### + def test_add_node_attributes(): graph_node_attrs = add_node_attributes(GRAPH, NODE_ATTRIBUTES) - assert set([n[1]['attr_fruit'] for n in graph_node_attrs.nodes(data=True)]) == \ - set(['apple', 'banana', 'cherry', 'durian']) + assert set([n[1]["attr_fruit"] for n in graph_node_attrs.nodes(data=True)]) == set( + ["apple", "banana", "cherry", "durian"] + ) diff --git a/postman_problems/tests/utils.py b/postman_problems/tests/utils.py index 212f7ec..58fa56b 100644 --- a/postman_problems/tests/utils.py +++ b/postman_problems/tests/utils.py @@ -22,4 +22,4 @@ def create_mock_csv_from_dataframe(df): writer.writerow(row[1].to_dict()) csvfile.seek(0) - return csvfile \ No newline at end of file + return csvfile diff --git a/postman_problems/viz.py b/postman_problems/viz.py index 5cb431e..6147688 100644 --- a/postman_problems/viz.py +++ b/postman_problems/viz.py @@ -23,11 +23,11 @@ def add_node_attributes(graph, nodelist): networkx graph: original `graph` augmented w node attributes """ for i, row in nodelist.iterrows(): - nx.set_node_attributes(graph, {row['id']: row.to_dict()}) + nx.set_node_attributes(graph, {row["id"]: row.to_dict()}) return graph -def add_pos_node_attribute(graph, origin='bottomleft'): +def add_pos_node_attribute(graph, origin="bottomleft"): """ Add node attribute 'pos' with X and Y coordinates to networkx graph so that we can the positions of each node to graphviz for plotting. The origin for the X,Y plane is provided as some tools for grabbing coordinates from images @@ -42,20 +42,21 @@ def add_pos_node_attribute(graph, origin='bottomleft'): """ ori = { - 'bottomleft': {'X': 1, 'Y': 1}, - 'topleft': {'X': 1, 'Y': -1}, - 'topright': {'X': -1, 'Y': -1}, - 'bottomright': {'X': -1, 'Y': 1} + "bottomleft": {"X": 1, "Y": 1}, + "topleft": {"X": 1, "Y": -1}, + "topright": {"X": -1, "Y": -1}, + "bottomright": {"X": -1, "Y": 1}, }[origin] for node_id in graph.nodes(): try: # dividing by arbitrary number to make pos appear as required type: double - graph.nodes[node_id]['pos'] = "{},{}!".format(ori['X']*graph.nodes[node_id]['X']/100, - ori['Y']*graph.nodes[node_id]['Y']/100) + graph.nodes[node_id]["pos"] = "{},{}!".format( + ori["X"] * graph.nodes[node_id]["X"] / 100, ori["Y"] * graph.nodes[node_id]["Y"] / 100 + ) except KeyError as e: print(e) - print('No X, Y coordinates found for node: {}'.format(node_id)) + print("No X, Y coordinates found for node: {}".format(node_id)) return graph @@ -76,19 +77,20 @@ def prepare_networkx_graph_circuit_for_transformation_to_graphviz(circuit, graph """ edge_cnter = defaultdict(lambda: 0) for i, e in enumerate(circuit): - # edge attributes - eid = e[3]['id'] + eid = e[3]["id"] key = e[2] if eid not in edge_cnter: - graph[e[0]][e[1]][key]['label'] = str(graph[e[0]][e[1]][key][edge_label_attr]) if edge_label_attr else str(i) - graph[e[0]][e[1]][key]['penwidth'] = 1 - graph[e[0]][e[1]][key]['decorate'] = 'true' + graph[e[0]][e[1]][key]["label"] = ( + str(graph[e[0]][e[1]][key][edge_label_attr]) if edge_label_attr else str(i) + ) + graph[e[0]][e[1]][key]["penwidth"] = 1 + graph[e[0]][e[1]][key]["decorate"] = "true" else: if edge_label_attr is None: - graph[e[0]][e[1]][key]['label'] += ', ' + str(i) - graph[e[0]][e[1]][key]['penwidth'] += 3 + graph[e[0]][e[1]][key]["label"] += ", " + str(i) + graph[e[0]][e[1]][key]["penwidth"] += 3 edge_cnter[eid] += 1 return graph @@ -115,7 +117,7 @@ def convert_networkx_graph_to_graphiz(graph, directed=False): # add nodes and their attributes to graphviz object for n in graph.nodes(): n_attr = {k: str(v) for k, v in graph.nodes[n].items()} - G.attr('node', n_attr) + G.attr("node", n_attr) G.node(str(n), str(n)) # add edges and their attributes to graphviz object @@ -126,8 +128,16 @@ def convert_networkx_graph_to_graphiz(graph, directed=False): return G -def plot_graphviz(graph, filename=None, format='svg', engine='dot', edge_label_attr=None, - graph_attr={'strict': 'false', 'forcelabels': 'true'}, node_attr=None, edge_attr=None): +def plot_graphviz( + graph, + filename=None, + format="svg", + engine="dot", + edge_label_attr=None, + graph_attr={"strict": "false", "forcelabels": "true"}, + node_attr=None, + edge_attr=None, +): """ Creates a dot (graphviz) representation of a networkx graph and saves a visualization. @@ -149,7 +159,7 @@ def plot_graphviz(graph, filename=None, format='svg', engine='dot', edge_label_a if edge_label_attr: for i, e in enumerate(graph.edges(data=True, keys=True)): key = e[2] - graph[e[0]][e[1]][key]['label'] = str(graph[e[0]][e[1]][key][edge_label_attr]) + graph[e[0]][e[1]][key]["label"] = str(graph[e[0]][e[1]][key][edge_label_attr]) # convert networkx object to graphviz object graph_gv = convert_networkx_graph_to_graphiz(graph, directed=False) @@ -178,8 +188,17 @@ def plot_graphviz(graph, filename=None, format='svg', engine='dot', edge_label_a return "Plot written to {}".format(filename) -def plot_circuit_graphviz(circuit, graph, filename=None, format='svg', engine='dot', edge_label_attr=None, - graph_attr={'strict': 'false', 'forcelabels': 'true'}, node_attr=None, edge_attr=None): +def plot_circuit_graphviz( + circuit, + graph, + filename=None, + format="svg", + engine="dot", + edge_label_attr=None, + graph_attr={"strict": "false", "forcelabels": "true"}, + node_attr=None, + edge_attr=None, +): """ Builds single graphviz graph with CPP solution. Wrapper around functions: @@ -206,8 +225,16 @@ def plot_circuit_graphviz(circuit, graph, filename=None, format='svg', engine='d return plot_graphviz(graph_gv, filename, format, engine, edge_label_attr, graph_attr, node_attr, edge_attr) -def make_circuit_images(circuit, graph, outfile_dir, format='png', engine='neato', - graph_attr={'strict': 'false', 'forcelabels': 'true'}, node_attr=None, edge_attr=None): +def make_circuit_images( + circuit, + graph, + outfile_dir, + format="png", + engine="neato", + graph_attr={"strict": "false", "forcelabels": "true"}, + node_attr=None, + edge_attr=None, +): """ Builds (in a hacky way) a sequence of plots that simulate the network growing according to the eulerian path. TODO: fix bug where edge labels populate with each direction (multiple walk) as soon as the first one comes up. @@ -229,31 +256,34 @@ def make_circuit_images(circuit, graph, outfile_dir, format='png', engine='neato # Start w a blank (OK, opaque) canvas for e in graph_white.edges(keys=True): - graph_white.nodes[e[0]]['color'] = graph_white.nodes[e[1]]['color'] = '#eeeeee' - graph_white[e[0]][e[1]][e[2]]['color'] = '#eeeeee' - graph_white[e[0]][e[1]][e[2]]['label'] = '' + graph_white.nodes[e[0]]["color"] = graph_white.nodes[e[1]]["color"] = "#eeeeee" + graph_white[e[0]][e[1]][e[2]]["color"] = "#eeeeee" + graph_white[e[0]][e[1]][e[2]]["label"] = "" # Now let's start adding some color to it, one edge at a time: for i, e in enumerate(tqdm.tqdm(circuit)): - # adding node colors - eid = e[3]['id'] - graph_white.nodes[e[0]]['color'] = 'black' - graph_white.nodes[e[1]]['color'] = 'red' # will get overwritten at next step + eid = e[3]["id"] + graph_white.nodes[e[0]]["color"] = "black" + graph_white.nodes[e[1]]["color"] = "red" # will get overwritten at next step # adding edge colors and attributes key = e[2] - graph_white[e[0]][e[1]][key]['color'] = graph[e[0]][e[1]][key]['color'] if 'color' in graph[e[0]][e[1]][key] else 'red' + graph_white[e[0]][e[1]][key]["color"] = ( + graph[e[0]][e[1]][key]["color"] if "color" in graph[e[0]][e[1]][key] else "red" + ) - png_filename = os.path.join(outfile_dir, 'img' + str(i)) - graph_gv = plot_circuit_graphviz(circuit[0:i + 1], graph_white, png_filename, format, engine, None, graph_attr, node_attr, edge_attr) + png_filename = os.path.join(outfile_dir, "img" + str(i)) + graph_gv = plot_circuit_graphviz( + circuit[0 : i + 1], graph_white, png_filename, format, engine, None, graph_attr, node_attr, edge_attr + ) - graph_white[e[0]][e[1]][key]['color'] = 'black' # set walked edge back to black + graph_white[e[0]][e[1]][key]["color"] = "black" # set walked edge back to black - return 'Images created in {}'.format(outfile_dir) + return "Images created in {}".format(outfile_dir) -def make_circuit_video(infile_dir_images, outfile_movie, fps=3, format='png'): +def make_circuit_video(infile_dir_images, outfile_movie, fps=3, format="png"): """ Create a movie that visualizes the CPP solution from a series of static images. Args: @@ -266,13 +296,13 @@ def make_circuit_video(infile_dir_images, outfile_movie, fps=3, format='png'): No return value. Writes a movie/gif to disk """ # sorting filenames in order - filenames = glob.glob(os.path.join(infile_dir_images, 'img*.%s' % format)) - filenames_sort_indices = np.argsort([int(os.path.basename(filename).split('.')[0][3:]) for filename in filenames]) + filenames = glob.glob(os.path.join(infile_dir_images, "img*.%s" % format)) + filenames_sort_indices = np.argsort([int(os.path.basename(filename).split(".")[0][3:]) for filename in filenames]) filenames = [filenames[i] for i in filenames_sort_indices] # make movie - with imageio.get_writer(outfile_movie, mode='I', fps=fps) as writer: + with imageio.get_writer(outfile_movie, mode="I", fps=fps) as writer: for filename in tqdm.tqdm(filenames): image = imageio.imread(filename) writer.append_data(image) - return 'Movie written to {}'.format(outfile_movie) + return "Movie written to {}".format(outfile_movie) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..8869345 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,19 @@ +[tool.ruff] +line-length = 120 +target-version = "py313" + +[tool.ruff.lint] +select = [ + "ALL" +] + +ignore = [ + "COM812", # missing-trailing-comma (incompatible with ruff format) + "D202", # no-blank-line-after-function (looks stupid) + "D203", # one-blank-line-before-class + "D212", # multi-line-summary-first-line + "ERA001", # commented-out-code + "G004", # logging-f-string (allow f-strings in logging calls) + "PD002", # pandas-use-of-inplace-argument + "S101", # assert (allow to use assertions) +] diff --git a/setup.py b/setup.py index bc2ef28..1aa900a 100644 --- a/setup.py +++ b/setup.py @@ -12,49 +12,45 @@ def read(fname): setup( - name='postman_problems', - version='0.4', - author='Andrew Brooks', - author_email='andrewbrooks@gmail.com', - description='Solutions to Postman graph optimization problems: Chinese and Rural Postman problems', - license='MIT License', - keywords='chinese postman problem networkx optimization network graph arc routing', - url='https://github.com/brooksandrew/postman_problems', - download_url='https://github.com/brooksandrew/postman_problems/archive/v0.3.tar.gz', + name="postman_problems", + version="0.4", + author="Andrew Brooks", + author_email="andrewbrooks@gmail.com", + description="Solutions to Postman graph optimization problems: Chinese and Rural Postman problems", + license="MIT License", + keywords="chinese postman problem networkx optimization network graph arc routing", + url="https://github.com/brooksandrew/postman_problems", + download_url="https://github.com/brooksandrew/postman_problems/archive/v0.3.tar.gz", packages=find_packages(), - long_description=read('README.rst'), + long_description=read("README.rst"), classifiers=[ - 'Development Status :: 2 - Pre-Alpha', - 'Topic :: Scientific/Engineering :: Mathematics', - 'Topic :: Scientific/Engineering :: Visualization', - 'License :: OSI Approved :: MIT License' + "Development Status :: 2 - Pre-Alpha", + "Topic :: Scientific/Engineering :: Mathematics", + "Topic :: Scientific/Engineering :: Visualization", + "License :: OSI Approved :: MIT License", ], package_data={ - 'postman_problems': [ - 'postman_problems/examples/sleeping_giant/edgelist_sleeping_giant.csv', - 'postman_problems/examples/sleeping_giant/nodelist_sleeping_giant.csv', - 'postman_problems/examples/seven_bridges/edgelist_seven_bridges.csv' + "postman_problems": [ + "postman_problems/examples/sleeping_giant/edgelist_sleeping_giant.csv", + "postman_problems/examples/sleeping_giant/nodelist_sleeping_giant.csv", + "postman_problems/examples/seven_bridges/edgelist_seven_bridges.csv", ] }, include_package_data=True, entry_points={ - 'console_scripts': [ - 'chinese_postman=postman_problems.postman_chinese:chinese_postman', - 'rural_postman=postman_problems.postman_rural:rural_postman', - 'chinese_postman_sleeping_giant=postman_problems.examples.sleeping_giant.cpp_sleeping_giant:main', - 'rural_postman_sleeping_giant=postman_problems.examples.sleeping_giant.rpp_sleeping_giant:main', - 'chinese_postman_seven_bridges=postman_problems.examples.seven_bridges.cpp_seven_bridges:main', - 'rural_postman_star=postman_problems.examples.star.rpp_star:main' + "console_scripts": [ + "chinese_postman=postman_problems.postman_chinese:chinese_postman", + "rural_postman=postman_problems.postman_rural:rural_postman", + "chinese_postman_sleeping_giant=postman_problems.examples.sleeping_giant.cpp_sleeping_giant:main", + "rural_postman_sleeping_giant=postman_problems.examples.sleeping_giant.rpp_sleeping_giant:main", + "chinese_postman_seven_bridges=postman_problems.examples.seven_bridges.cpp_seven_bridges:main", + "rural_postman_star=postman_problems.examples.star.rpp_star:main", ] }, - python_requires='>=3.7.1', - install_requires=[ - 'pandas', - 'networkx>=2.0' - ], + python_requires=">=3.7.1", + install_requires=["pandas", "networkx>=2.0"], extras_require={ - 'viz': ['imageio', 'matplotlib', 'graphviz', 'tqdm'], - 'test': ['pytest', 'pytest-cov', 'pytest-console-scripts'] - } + "viz": ["imageio", "matplotlib", "graphviz", "tqdm"], + "test": ["pytest", "pytest-cov", "pytest-console-scripts"], + }, ) -