From 3e6c1423ea5d2f099547d3994b62b1308c8e7cc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20H=C3=BClsmann?= Date: Mon, 5 May 2025 23:29:43 +0200 Subject: [PATCH 1/3] formatting project (with ruff) --- conftest.py | 172 ++++++------ .../seven_bridges/cpp_seven_bridges.py | 90 ++++--- .../sleeping_giant/cpp_sleeping_giant.py | 68 ++--- .../sleeping_giant/rpp_sleeping_giant.py | 104 +++---- postman_problems/examples/star/rpp_star.py | 141 +++++----- postman_problems/graph.py | 86 +++--- postman_problems/postman_chinese.py | 4 +- postman_problems/postman_rural.py | 4 +- postman_problems/postman_template.py | 254 ++++++++++-------- postman_problems/solver.py | 47 ++-- postman_problems/stats.py | 24 +- .../tests/test_chinese_postman.py | 59 ++-- .../tests/test_example_sleeping_giant.py | 69 ++--- postman_problems/tests/test_graph.py | 81 +++--- postman_problems/tests/test_rural_postman.py | 25 +- postman_problems/tests/test_solver.py | 11 +- postman_problems/tests/test_stats.py | 43 ++- postman_problems/tests/test_viz.py | 27 +- postman_problems/tests/utils.py | 2 +- postman_problems/viz.py | 112 +++++--- pyproject.toml | 19 ++ setup.py | 64 +++-- 22 files changed, 827 insertions(+), 679 deletions(-) create mode 100644 pyproject.toml 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"], + }, ) - From 4cdb6aaee74a39e2765c2eab32843a96ecff28c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20H=C3=BClsmann?= Date: Mon, 5 May 2025 23:48:52 +0200 Subject: [PATCH 2/3] Auto-fixing linter findings --- conftest.py | 4 +-- postman_problems/__init__.py | 1 - .../seven_bridges/cpp_seven_bridges.py | 4 ++- .../sleeping_giant/cpp_sleeping_giant.py | 8 +++--- .../sleeping_giant/rpp_sleeping_giant.py | 8 ++++-- postman_problems/examples/star/rpp_star.py | 12 ++++---- postman_problems/graph.py | 25 +++++++++++------ postman_problems/postman_template.py | 11 +++++--- postman_problems/solver.py | 15 +++++----- postman_problems/stats.py | 1 + .../tests/test_chinese_postman.py | 12 ++++---- .../tests/test_example_sleeping_giant.py | 18 ++++++------ postman_problems/tests/test_graph.py | 18 ++++++------ postman_problems/tests/test_rural_postman.py | 6 ++-- postman_problems/tests/test_solver.py | 1 + postman_problems/tests/test_viz.py | 4 +-- postman_problems/tests/utils.py | 1 + postman_problems/viz.py | 28 +++++++++++++------ setup.py | 3 +- 19 files changed, 107 insertions(+), 73 deletions(-) diff --git a/conftest.py b/conftest.py index 0b59e34..d28fe6a 100644 --- a/conftest.py +++ b/conftest.py @@ -1,8 +1,8 @@ -import pytest import networkx as nx import pandas as pd -from postman_problems.tests.utils import create_mock_csv_from_dataframe +import pytest +from postman_problems.tests.utils import create_mock_csv_from_dataframe # --------------------------------------------------------------------------------------- # Configuration for slow tests diff --git a/postman_problems/__init__.py b/postman_problems/__init__.py index f12c0c7..0c8a7ea 100644 --- a/postman_problems/__init__.py +++ b/postman_problems/__init__.py @@ -1,5 +1,4 @@ import logging - logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) diff --git a/postman_problems/examples/seven_bridges/cpp_seven_bridges.py b/postman_problems/examples/seven_bridges/cpp_seven_bridges.py index c48a064..f146af7 100644 --- a/postman_problems/examples/seven_bridges/cpp_seven_bridges.py +++ b/postman_problems/examples/seven_bridges/cpp_seven_bridges.py @@ -25,7 +25,9 @@ """ import logging + import pkg_resources + from postman_problems.solver import cpp from postman_problems.stats import calculate_postman_solution_stats @@ -69,7 +71,7 @@ def main(): # VIZ ------------------------------------------------------------------------------- try: - from postman_problems.viz import plot_circuit_graphviz, make_circuit_images, make_circuit_video + from postman_problems.viz import make_circuit_images, make_circuit_video, plot_circuit_graphviz logger.info("Creating single SVG of base graph") plot_circuit_graphviz( diff --git a/postman_problems/examples/sleeping_giant/cpp_sleeping_giant.py b/postman_problems/examples/sleeping_giant/cpp_sleeping_giant.py index 5a92b6d..ab9f0a1 100644 --- a/postman_problems/examples/sleeping_giant/cpp_sleeping_giant.py +++ b/postman_problems/examples/sleeping_giant/cpp_sleeping_giant.py @@ -33,8 +33,10 @@ """ import logging -import pkg_resources + import pandas as pd +import pkg_resources + from postman_problems.solver import cpp from postman_problems.stats import calculate_postman_solution_stats @@ -85,11 +87,9 @@ def main(): try: from postman_problems.viz import ( - add_pos_node_attribute, add_node_attributes, + add_pos_node_attribute, plot_circuit_graphviz, - make_circuit_images, - make_circuit_video, ) logger.info("Add node attributes to graph") diff --git a/postman_problems/examples/sleeping_giant/rpp_sleeping_giant.py b/postman_problems/examples/sleeping_giant/rpp_sleeping_giant.py index d8854cc..474e691 100644 --- a/postman_problems/examples/sleeping_giant/rpp_sleeping_giant.py +++ b/postman_problems/examples/sleeping_giant/rpp_sleeping_giant.py @@ -1,8 +1,10 @@ """ """ import logging -import pkg_resources + import pandas as pd +import pkg_resources + from postman_problems.solver import rpp from postman_problems.stats import calculate_postman_solution_stats @@ -53,11 +55,11 @@ def main(): try: from postman_problems.viz import ( - add_pos_node_attribute, add_node_attributes, - plot_circuit_graphviz, + add_pos_node_attribute, make_circuit_images, make_circuit_video, + plot_circuit_graphviz, ) logger.info("Add node attributes to graph") diff --git a/postman_problems/examples/star/rpp_star.py b/postman_problems/examples/star/rpp_star.py index 6600b3b..bf4589a 100644 --- a/postman_problems/examples/star/rpp_star.py +++ b/postman_problems/examples/star/rpp_star.py @@ -1,10 +1,12 @@ -import pkg_resources import logging import string + import networkx as nx -from postman_problems.tests.utils import create_mock_csv_from_dataframe +import pkg_resources + +from postman_problems.solver import cpp, rpp from postman_problems.stats import calculate_postman_solution_stats -from postman_problems.solver import rpp, cpp +from postman_problems.tests.utils import create_mock_csv_from_dataframe def create_star_graph(n_nodes=10, ring=True): @@ -25,7 +27,7 @@ def create_star_graph(n_nodes=10, ring=True): 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]])): + for e in list(zip(node_names[1:-1] + [node_names[1]], node_names[2:] + [node_names[-1]], strict=False)): graph.add_edge(e[0], e[1], distance=2, required=0, style="dashed") return graph @@ -95,7 +97,7 @@ def main(): # VIZ ------------------------------------------------------------------------------- try: - from postman_problems.viz import plot_circuit_graphviz, plot_graphviz, make_circuit_images, make_circuit_video + from postman_problems.viz import make_circuit_images, make_circuit_video, plot_circuit_graphviz, plot_graphviz logger.info("Creating single SVG of base graph") plot_graphviz( diff --git a/postman_problems/graph.py b/postman_problems/graph.py index 2230bb7..d4c4778 100644 --- a/postman_problems/graph.py +++ b/postman_problems/graph.py @@ -1,4 +1,5 @@ import warnings + import networkx as nx import pandas as pd @@ -12,6 +13,7 @@ def read_edgelist(edgelist_filename, keep_optional=False): Returns: 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. @@ -47,13 +49,14 @@ def create_networkx_graph_from_edgelist(edgelist, edge_id="id"): Returns: networkx.MultiGraph: Returning a MultiGraph rather than Graph to support parallel edges + """ 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 " + f"{edge_id} 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) + "where it is guaranteed to be unique" ) for i, row in enumerate(edgelist.iterrows()): @@ -73,6 +76,7 @@ def _get_even_or_odd_nodes(graph, mod): Returns: list[str]: list of node names of odd or even degree + """ degree_nodes = [] for v, d in graph.degree(): @@ -90,6 +94,7 @@ def get_odd_nodes(graph): Returns: list[str]: names of nodes with odd degree + """ return _get_even_or_odd_nodes(graph, 1) @@ -119,6 +124,7 @@ def get_shortest_paths_distances(graph, pairs, edge_weight_name="distance"): Returns: dict: mapping each pair in `pairs` to the shortest path using `edge_weight_name` between them. + """ distances = {} for pair in pairs: @@ -137,11 +143,12 @@ def create_complete_graph(pair_weights, flip_weights=True): Returns: complete (fully connected graph) networkx graph using the node pairs and distances provided in `pair_weights` + """ 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,6 +161,7 @@ def dedupe_matching(matching): Returns: list[2tuples]: list of node pairs from `matching` deduped (ignoring order). + """ matched_pairs_w_dupes = [tuple(sorted([k, v])) for k, v in matching.items()] return list(set(matched_pairs_w_dupes)) @@ -173,16 +181,14 @@ def add_augmenting_path_to_graph(graph, min_weight_pairs, edge_weight_name="weig Returns: networkx graph: `graph` augmented with edges between the odd nodes specified in `min_weight_pairs` + """ 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, - }, + distance=nx.dijkstra_path_length(graph, pair[0], pair[1], weight=edge_weight_name), augmented=True, ) return graph_aug @@ -202,6 +208,7 @@ def create_eulerian_circuit(graph_augmented, graph_original, start_node=None): Returns: networkx graph (`graph_original`) augmented with edges directly between the odd nodes + """ euler_circuit = list(nx.eulerian_circuit(graph_augmented, source=start_node, keys=True)) @@ -215,7 +222,7 @@ def create_eulerian_circuit(graph_augmented, graph_original, start_node=None): if not edge_attr.get("augmented"): yield edge + (edge_attr,) else: - for edge_aug in list(zip(aug_path[:-1], aug_path[1:])): + for edge_aug in list(zip(aug_path[:-1], aug_path[1:], strict=False)): # 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( @@ -240,6 +247,7 @@ def create_required_graph(graph): Returns: networkx MultiGraph with optional nodes and edges deleted + """ graph_req = graph.copy() # preserve original structure @@ -265,6 +273,7 @@ def assert_graph_is_connected(graph): Returns: True if graph is connected + """ assert nx.algorithms.connected.is_connected(graph), ( diff --git a/postman_problems/postman_template.py b/postman_problems/postman_template.py index 4f21d9d..88f280a 100644 --- a/postman_problems/postman_template.py +++ b/postman_problems/postman_template.py @@ -1,15 +1,17 @@ -import os import argparse import logging +import os + from postman_problems.solver import cpp, rpp -from postman_problems.viz import plot_circuit_graphviz, make_circuit_video, make_circuit_images from postman_problems.stats import calculate_postman_solution_stats +from postman_problems.viz import make_circuit_images, make_circuit_video, plot_circuit_graphviz def get_args(): """ Returns: argparse.Namespace: parsed arguments from the user + """ parser = argparse.ArgumentParser(description="Arguments to the Chinese Postman Problem solver") @@ -138,6 +140,7 @@ def generic_postman(postman_type): Args: postman_type (str): "rural" or "chinese" + """ if postman_type == "rural": @@ -152,7 +155,7 @@ def generic_postman(postman_type): logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) - logger.info("Solving the {} postman problem..".format(postman_type)) + logger.info(f"Solving the {postman_type} postman problem..") circuit, graph = postman_algo( edgelist_filename=args.edgelist, start_node=args.start_node, edge_weight=args.edge_weight ) @@ -166,7 +169,7 @@ def generic_postman(postman_type): logger.info(str(k) + " : " + str(v)) if args.viz: - logger.info("Creating single image of {} postman solution...".format(postman_type)) + logger.info(f"Creating single image of {postman_type} postman solution...") message_static = plot_circuit_graphviz( circuit=circuit, graph=graph, filename=args.viz_filename, format=args.viz_format, engine=args.viz_engine ) diff --git a/postman_problems/solver.py b/postman_problems/solver.py index 75741b5..3a1a6fc 100644 --- a/postman_problems/solver.py +++ b/postman_problems/solver.py @@ -1,21 +1,20 @@ import itertools import logging + import networkx as nx from postman_problems.graph import ( - read_edgelist, + add_augmenting_path_to_graph, + assert_graph_is_connected, + create_complete_graph, + create_eulerian_circuit, 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, + read_edgelist, ) - logger_rpp = logging.getLogger("{0}.{1}".format(__name__, "rpp")) logger_cpp = logging.getLogger("{0}.{1}".format(__name__, "cpp")) @@ -39,6 +38,7 @@ def rpp(edgelist_filename, start_node=None, edge_weight="distance", verbose=Fals The second element is the end ("to") node. The third element is the dict of edge attributes for that edge. The original graph is returned as well. This is needed for visualization + """ logger_rpp.disabled = not verbose @@ -89,6 +89,7 @@ def cpp(edgelist_filename, start_node=None, edge_weight="distance", verbose=Fals The second element is the end ("to") node. The third element is the dict of edge attributes for that edge. The original graph is returned as well. This is needed for visualization + """ logger_cpp.disabled = not verbose diff --git a/postman_problems/stats.py b/postman_problems/stats.py index af68455..14f63dd 100644 --- a/postman_problems/stats.py +++ b/postman_problems/stats.py @@ -10,6 +10,7 @@ def calculate_postman_solution_stats(circuit, edge_weight_name="distance"): Returns: summary table (OrderedDict) + """ summary_stats = collections.OrderedDict() diff --git a/postman_problems/tests/test_chinese_postman.py b/postman_problems/tests/test_chinese_postman.py index c3d09ce..9283375 100644 --- a/postman_problems/tests/test_chinese_postman.py +++ b/postman_problems/tests/test_chinese_postman.py @@ -1,13 +1,13 @@ -import sys import os -import pytest -import tempfile import shutil -import pkg_resources +import sys +import tempfile from unittest.mock import patch -from postman_problems.postman_chinese import chinese_postman -from pytest_console_scripts import script_runner +import pkg_resources +import pytest + +from postman_problems.postman_chinese import chinese_postman # input params EDGELIST_SEVEN_BRIDGES = pkg_resources.resource_filename( diff --git a/postman_problems/tests/test_example_sleeping_giant.py b/postman_problems/tests/test_example_sleeping_giant.py index 6dd0904..f38685a 100644 --- a/postman_problems/tests/test_example_sleeping_giant.py +++ b/postman_problems/tests/test_example_sleeping_giant.py @@ -1,16 +1,18 @@ -import math -import pkg_resources import itertools -import pandas as pd +import math + import networkx as nx -from postman_problems.viz import add_node_attributes +import pandas as pd +import pkg_resources + from postman_problems.graph import ( - read_edgelist, create_networkx_graph_from_edgelist, get_odd_nodes, get_shortest_paths_distances, + read_edgelist, ) -from postman_problems.solver import rpp, cpp +from postman_problems.solver import cpp, rpp +from postman_problems.viz import add_node_attributes # ################### # PARAMETERS / DATA # @@ -113,12 +115,12 @@ def test_nodelist_edgelist_overlap(): nodes_in_el_but_not_nl = edgelist_nodes - nodelist_nodes 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) + f"Warning: The following nodes are in the edgelist, but not the nodelist: {nodes_in_el_but_not_nl}" ) nodes_in_nl_but_not_el = nodelist_nodes - edgelist_nodes 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) + f"Warning: The following nodes are in the nodelist, but not the edgelist: {nodes_in_nl_but_not_el}" ) diff --git a/postman_problems/tests/test_graph.py b/postman_problems/tests/test_graph.py index dcf340f..090ccca 100644 --- a/postman_problems/tests/test_graph.py +++ b/postman_problems/tests/test_graph.py @@ -4,21 +4,21 @@ 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_complete_graph, + create_eulerian_circuit, + create_networkx_graph_from_edgelist, create_required_graph, + dedupe_matching, + get_even_nodes, + get_odd_nodes, + get_shortest_paths_distances, + read_edgelist, ) - # ################### # PARAMETERS / DATA # # ################### diff --git a/postman_problems/tests/test_rural_postman.py b/postman_problems/tests/test_rural_postman.py index 11399c2..bac0e1a 100644 --- a/postman_problems/tests/test_rural_postman.py +++ b/postman_problems/tests/test_rural_postman.py @@ -1,11 +1,11 @@ import os import sys +from unittest.mock import patch + import pkg_resources import pytest -from unittest.mock import patch -from pytest_console_scripts import script_runner -from postman_problems.postman_rural import rural_postman +from postman_problems.postman_rural import rural_postman # input params EDGELIST_SLEEPING_GIANT = pkg_resources.resource_filename( diff --git a/postman_problems/tests/test_solver.py b/postman_problems/tests/test_solver.py index b9c46b5..86ff9e3 100644 --- a/postman_problems/tests/test_solver.py +++ b/postman_problems/tests/test_solver.py @@ -1,4 +1,5 @@ import pytest + from postman_problems.solver import cpp, rpp from postman_problems.tests.test_stats import ( test_stats_on_simple_graph_required_edges_only, diff --git a/postman_problems/tests/test_viz.py b/postman_problems/tests/test_viz.py index 31c9274..a5075d6 100644 --- a/postman_problems/tests/test_viz.py +++ b/postman_problems/tests/test_viz.py @@ -1,7 +1,7 @@ -import pandas as pd import networkx as nx -from postman_problems.viz import add_node_attributes +import pandas as pd +from postman_problems.viz import add_node_attributes # ################### # PARAMETERS / DATA # diff --git a/postman_problems/tests/utils.py b/postman_problems/tests/utils.py index 58fa56b..19a11b2 100644 --- a/postman_problems/tests/utils.py +++ b/postman_problems/tests/utils.py @@ -11,6 +11,7 @@ def create_mock_csv_from_dataframe(df): df (pandas dataframe): to be converted into a StringIO object Returns: io.StringIO representation of `df` + """ csvfile = StringIO() diff --git a/postman_problems/viz.py b/postman_problems/viz.py index 6147688..8fad7f9 100644 --- a/postman_problems/viz.py +++ b/postman_problems/viz.py @@ -1,11 +1,12 @@ -import os import glob +import os +from collections import defaultdict + +import graphviz as gv import imageio -import tqdm -import numpy as np import networkx as nx -import graphviz as gv -from collections import defaultdict +import numpy as np +import tqdm def add_node_attributes(graph, nodelist): @@ -21,6 +22,7 @@ def add_node_attributes(graph, nodelist): Returns: networkx graph: original `graph` augmented w node attributes + """ for i, row in nodelist.iterrows(): nx.set_node_attributes(graph, {row["id"]: row.to_dict()}) @@ -39,6 +41,7 @@ def add_pos_node_attribute(graph, origin="bottomleft"): Returns: networkx graph with the node attributes added to graph + """ ori = { @@ -56,7 +59,7 @@ def add_pos_node_attribute(graph, origin="bottomleft"): ) except KeyError as e: print(e) - print("No X, Y coordinates found for node: {}".format(node_id)) + print(f"No X, Y coordinates found for node: {node_id}") return graph @@ -74,6 +77,7 @@ def prepare_networkx_graph_circuit_for_transformation_to_graphviz(circuit, graph Returns: networkx graph: `graph` augmented with information from `circuit` + """ edge_cnter = defaultdict(lambda: 0) for i, e in enumerate(circuit): @@ -108,6 +112,7 @@ def convert_networkx_graph_to_graphiz(graph, directed=False): Returns: graphviz.dot.Graph: conversion of `graph` to a graphviz dot object. + """ if directed: G = gv.Digraph() @@ -154,6 +159,7 @@ def plot_graphviz( Returns: graphviz.Graph or graphviz.DirectedGraph with Writes a visualization to disk if filename is provided. + """ if edge_label_attr: @@ -185,7 +191,7 @@ def plot_graphviz( if filename: graph_gv.render(filename=filename, view=False) - return "Plot written to {}".format(filename) + return f"Plot written to {filename}" def plot_circuit_graphviz( @@ -219,6 +225,7 @@ def plot_circuit_graphviz( Returns: graphviz.Graph or graphviz.DirectedGraph with enriched route and plotting data. Writes a visualization to disk if filename is provided. + """ graph_gv = prepare_networkx_graph_circuit_for_transformation_to_graphviz(circuit, graph, edge_label_attr) @@ -251,6 +258,7 @@ def make_circuit_images( Returns: No return value. Writes a viz to disk for each instruction in the CPP. + """ graph_white = prepare_networkx_graph_circuit_for_transformation_to_graphviz(circuit, graph.copy()) @@ -280,12 +288,13 @@ def make_circuit_images( graph_white[e[0]][e[1]][key]["color"] = "black" # set walked edge back to black - return "Images created in {}".format(outfile_dir) + return f"Images created in {outfile_dir}" 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: infile_dir_images (str): path to list of images named like `img[X].png`. These are produced from make_circuit_images outfile_movie (str): filename of created movie/gif (output) @@ -294,6 +303,7 @@ def make_circuit_video(infile_dir_images, outfile_movie, fps=3, format="png"): Returns: 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)) @@ -305,4 +315,4 @@ def make_circuit_video(infile_dir_images, outfile_movie, fps=3, format="png"): for filename in tqdm.tqdm(filenames): image = imageio.imread(filename) writer.append_data(image) - return "Movie written to {}".format(outfile_movie) + return f"Movie written to {outfile_movie}" diff --git a/setup.py b/setup.py index 1aa900a..8ca7377 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,6 @@ import os -from setuptools import setup, find_packages + +from setuptools import find_packages, setup def read(fname): From 66923f92a1bdb4a0cf5b1a95416c878e353ffc72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20H=C3=BClsmann?= Date: Tue, 6 May 2025 00:03:02 +0200 Subject: [PATCH 3/3] version bump --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 8ca7377..1456d37 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ def read(fname): setup( name="postman_problems", - version="0.4", + version="0.5", author="Andrew Brooks", author_email="andrewbrooks@gmail.com", description="Solutions to Postman graph optimization problems: Chinese and Rural Postman problems",