From 21ec9790252e53dd0ef3ac3df9ef41565df45ed9 Mon Sep 17 00:00:00 2001 From: PatrickOHara Date: Thu, 3 Aug 2023 11:34:16 +0100 Subject: [PATCH 01/12] New experiment for large alpha on londonaq --- pctsp/apps/main_app.py | 2 ++ pctsp/compare/__init__.py | 3 ++- pctsp/compare/heuristic_experiment.py | 21 ++++++++++++++++----- pctsp/compare/product_of_params.py | 25 +++++++++++++++++++++++++ pctsp/vial/experiment.py | 1 + 5 files changed, 46 insertions(+), 6 deletions(-) diff --git a/pctsp/apps/main_app.py b/pctsp/apps/main_app.py index 4b776e2..1fe9c29 100644 --- a/pctsp/apps/main_app.py +++ b/pctsp/apps/main_app.py @@ -42,6 +42,7 @@ dryrun, compare_heuristics, disjoint_tours_vs_heuristics, + londonaq_alpha, simple_branch_cut, tailing_off, ) @@ -96,6 +97,7 @@ ExperimentName.compare_heuristics: compare_heuristics, ExperimentName.cost_cover: cost_cover, ExperimentName.dryrun: dryrun, + ExperimentName.londonaq_alpha: londonaq_alpha, ExperimentName.tailing_off: tailing_off, } diff --git a/pctsp/compare/__init__.py b/pctsp/compare/__init__.py index 448038c..c667208 100644 --- a/pctsp/compare/__init__.py +++ b/pctsp/compare/__init__.py @@ -2,7 +2,7 @@ from .dryrun_experiment import dryrun from .exact_experiment import cost_cover, simple_branch_cut, tailing_off -from .heuristic_experiment import compare_heuristics +from .heuristic_experiment import compare_heuristics, londonaq_alpha from .relaxation_experiment import disjoint_tours_vs_heuristics __all__ = [ @@ -10,6 +10,7 @@ "disjoint_tours_vs_heuristics", "dryrun", "compare_heuristics", + "londonaq_alpha", "simple_branch_cut", "tailing_off", ] diff --git a/pctsp/compare/heuristic_experiment.py b/pctsp/compare/heuristic_experiment.py index c49bb7a..0718059 100644 --- a/pctsp/compare/heuristic_experiment.py +++ b/pctsp/compare/heuristic_experiment.py @@ -14,15 +14,14 @@ from . import params from .product_of_params import ( product_of_londonaq_data_config, + product_of_londonaq_data_config_from_alpha, product_of_preprocessing, product_of_tspwplib_data_config, product_of_vials, ) - -def compare_heuristics(dataset_name: DatasetName, dataset_root: Path) -> List[Vial]: - """Compare the Extension & Collapse heuristic against Suurballe's heuristic""" - model_params_list = [ +def get_all_heuristic_params() -> List[ModelParams]: + return [ ModelParams( # Extension Collapse algorithm=AlgorithmName.bfs_extension_collapse, collapse_paths=False, @@ -67,6 +66,18 @@ def compare_heuristics(dataset_name: DatasetName, dataset_root: Path) -> List[Vi ), ] +def londonaq_alpha(dataset_root: Path) -> List[Vial]: + """Run heuristics on the londonaq dataset with a large alpha""" + data_config_list = product_of_londonaq_data_config_from_alpha( + dataset_root, + params.TSPLIB_ALPHA_LIST, + params.LONDONAQ_GRAPH_NAME_LIST, + ) + preprocessing_list = product_of_preprocessing([False], [False], [True]) + return product_of_vials(data_config_list, get_all_heuristic_params(), preprocessing_list) + +def compare_heuristics(dataset_name: DatasetName, dataset_root: Path) -> List[Vial]: + """Compare the Extension & Collapse heuristic against Suurballe's heuristic""" if dataset_name == DatasetName.tspwplib: data_config_list = product_of_tspwplib_data_config( dataset_root, @@ -87,4 +98,4 @@ def compare_heuristics(dataset_name: DatasetName, dataset_root: Path) -> List[Vi f"{dataset_name} is not a supported dataset for experiment 'compare_heuristics'" ) preprocessing_list = product_of_preprocessing([False], [False], [True]) - return product_of_vials(data_config_list, model_params_list, preprocessing_list) + return product_of_vials(data_config_list, get_all_heuristic_params(), preprocessing_list) diff --git a/pctsp/compare/product_of_params.py b/pctsp/compare/product_of_params.py index 752e1f9..c7807dc 100644 --- a/pctsp/compare/product_of_params.py +++ b/pctsp/compare/product_of_params.py @@ -89,6 +89,31 @@ def product_of_londonaq_data_config( ) return data_config_list +def product_of_londonaq_data_config_from_alpha( + londonaq_root: Path, + alpha_list: List[int], + graph_name_list: List[LondonaqGraphName], +) -> List[DataConfig]: + """Iterate over data settings for the londonaq dataset""" + problems = {} + for name in graph_name_list: + problem_path = build_path_to_londonaq_yaml(londonaq_root, name) + problems[name] = PrizeCollectingTSP.from_yaml(problem_path) + + data_config_list = [] + for alpha, name in itertools.product(alpha_list, graph_name_list): + data_config_list.append( + DataConfig( + cost_function=problems[name].edge_weight_type, + dataset=DatasetName.londonaq, + graph_name=name, + quota=problems[name].get_quota(alpha), + root=problems[name].get_root_vertex(), + alpha=alpha, + ) + ) + return data_config_list + def product_of_model_params( algorithm_list: List[AlgorithmName], diff --git a/pctsp/vial/experiment.py b/pctsp/vial/experiment.py index fbc2fc5..0c4b3f5 100644 --- a/pctsp/vial/experiment.py +++ b/pctsp/vial/experiment.py @@ -17,6 +17,7 @@ class ExperimentName(str, Enum): disjoint_tours_vs_heuristics = "disjoint_tours_vs_heuristics" dryrun = "dryrun" compare_heuristics = "compare_heuristics" + londonaq_alpha = "londonaq_alpha" onerun = "onerun" simple_branch_cut = "simple_branch_cut" tailing_off = "tailing_off" From 70b7d956265a9ae051ae5dde8697be8fe5a11652 Mon Sep 17 00:00:00 2001 From: PatrickOHara Date: Wed, 9 Aug 2023 09:34:01 +0100 Subject: [PATCH 02/12] Add londonaq alpha experiment --- pctsp/apps/main_app.py | 2 + pctsp/compare/__init__.py | 8 +++- pctsp/compare/exact_experiment.py | 67 +++++++++++++++++---------- pctsp/compare/heuristic_experiment.py | 13 ++++-- pctsp/compare/product_of_params.py | 1 + pctsp/lab/run_algorithm.py | 8 ++++ pctsp/vial/experiment.py | 1 + 7 files changed, 72 insertions(+), 28 deletions(-) diff --git a/pctsp/apps/main_app.py b/pctsp/apps/main_app.py index 1fe9c29..62993f8 100644 --- a/pctsp/apps/main_app.py +++ b/pctsp/apps/main_app.py @@ -42,6 +42,7 @@ dryrun, compare_heuristics, disjoint_tours_vs_heuristics, + cc_londonaq_alpha, londonaq_alpha, simple_branch_cut, tailing_off, @@ -96,6 +97,7 @@ ExperimentName.baseline: baseline, ExperimentName.compare_heuristics: compare_heuristics, ExperimentName.cost_cover: cost_cover, + ExperimentName.cc_londonaq_alpha: cc_londonaq_alpha, ExperimentName.dryrun: dryrun, ExperimentName.londonaq_alpha: londonaq_alpha, ExperimentName.tailing_off: tailing_off, diff --git a/pctsp/compare/__init__.py b/pctsp/compare/__init__.py index c667208..3112317 100644 --- a/pctsp/compare/__init__.py +++ b/pctsp/compare/__init__.py @@ -1,13 +1,19 @@ """Functions that return lists of vials to compare algorithms against eachother""" from .dryrun_experiment import dryrun -from .exact_experiment import cost_cover, simple_branch_cut, tailing_off +from .exact_experiment import ( + cost_cover, + cc_londonaq_alpha, + simple_branch_cut, + tailing_off, +) from .heuristic_experiment import compare_heuristics, londonaq_alpha from .relaxation_experiment import disjoint_tours_vs_heuristics __all__ = [ "cost_cover", "disjoint_tours_vs_heuristics", + "cc_londonaq_alpha", "dryrun", "compare_heuristics", "londonaq_alpha", diff --git a/pctsp/compare/exact_experiment.py b/pctsp/compare/exact_experiment.py index 8beb049..64e3bb8 100644 --- a/pctsp/compare/exact_experiment.py +++ b/pctsp/compare/exact_experiment.py @@ -22,6 +22,7 @@ from . import params from .product_of_params import ( product_of_londonaq_data_config, + product_of_londonaq_data_config_from_alpha, product_of_model_params, product_of_preprocessing, product_of_tspwplib_data_config, @@ -122,33 +123,12 @@ def tailing_off(dataset_name, dataset_root: Path) -> List[Vial]: return product_of_vials(data_config_list, model_params_list, preprocessing_list) -def cost_cover(dataset_name: DatasetName, dataset_root: Path) -> List[Vial]: - """Compare the branch and cut algorithm with and without cost cover inequalities""" +def __get_cc_params() -> List[ModelParams]: time_limit = FOUR_HOURS depth_limit = None step = 10 collapse_paths = True heuristic = AlgorithmName.suurballes_path_extension_collapse - - if dataset_name == DatasetName.tspwplib: - data_config_list = product_of_tspwplib_data_config( - dataset_root, - params.TSPLIB_ALPHA_LIST, - params.TSPLIB_KAPPA_LIST, - list(Generation), - params.TSPLIB_GRAPH_NAME_LIST, - params.TSPLIB_COST_FUNCTIONS, - ) - elif dataset_name == DatasetName.londonaq: - data_config_list = product_of_londonaq_data_config( - dataset_root, - params.LONDONAQ_QUOTA_LIST, - params.LONDONAQ_GRAPH_NAME_LIST, - ) - else: - raise ValueError( - f"{dataset_name} is not a supported dataset for experiment 'cost_cover'" - ) branching_strategy = BranchingStrategy.STRONG_AT_TREE_TOP no_cc_params = ModelParams( @@ -212,13 +192,52 @@ def cost_cover(dataset_name: DatasetName, dataset_root: Path) -> List[Vial]: step_size=step, time_limit=time_limit, ) - model_params_list = [ + return [ no_cc_params, disjoint_paths_cc_params, shortest_path_cc_params, ] + + +def cost_cover(dataset_name: DatasetName, dataset_root: Path) -> List[Vial]: + """Compare the branch and cut algorithm with and without cost cover inequalities""" + if dataset_name == DatasetName.tspwplib: + data_config_list = product_of_tspwplib_data_config( + dataset_root, + params.TSPLIB_ALPHA_LIST, + params.TSPLIB_KAPPA_LIST, + list(Generation), + params.TSPLIB_GRAPH_NAME_LIST, + params.TSPLIB_COST_FUNCTIONS, + ) + elif dataset_name == DatasetName.londonaq: + data_config_list = product_of_londonaq_data_config( + dataset_root, + params.LONDONAQ_QUOTA_LIST, + params.LONDONAQ_GRAPH_NAME_LIST, + ) + else: + raise ValueError( + f"{dataset_name} is not a supported dataset for experiment 'cost_cover'" + ) + preprocessing_list = product_of_preprocessing([False], [False], [True]) - return product_of_vials(data_config_list, model_params_list, preprocessing_list) + return product_of_vials(data_config_list, __get_cc_params(), preprocessing_list) + + +def cc_londonaq_alpha(dataset_name: DatasetName, dataset_root: Path) -> List[Vial]: + """Run each cost cover algorithm for different values of alpha on londonaq""" + if dataset_name != DatasetName.londonaq: + raise ValueError( + f"{dataset_name} is not a supported dataset for experiment 'cc_londonaq_alpha'" + ) + data_config_list = product_of_londonaq_data_config_from_alpha( + dataset_root, + params.TSPLIB_ALPHA_LIST, + params.LONDONAQ_GRAPH_NAME_LIST, + ) + preprocessing_list = product_of_preprocessing([False], [False], [True]) + return product_of_vials(data_config_list, __get_cc_params(), preprocessing_list) def baseline(dataset_name: DatasetName, dataset_root: Path) -> List[Vial]: diff --git a/pctsp/compare/heuristic_experiment.py b/pctsp/compare/heuristic_experiment.py index 0718059..bf682bb 100644 --- a/pctsp/compare/heuristic_experiment.py +++ b/pctsp/compare/heuristic_experiment.py @@ -20,6 +20,7 @@ product_of_vials, ) + def get_all_heuristic_params() -> List[ModelParams]: return [ ModelParams( # Extension Collapse @@ -66,7 +67,8 @@ def get_all_heuristic_params() -> List[ModelParams]: ), ] -def londonaq_alpha(dataset_root: Path) -> List[Vial]: + +def londonaq_alpha(dataset_name: DatasetName, dataset_root: Path) -> List[Vial]: """Run heuristics on the londonaq dataset with a large alpha""" data_config_list = product_of_londonaq_data_config_from_alpha( dataset_root, @@ -74,7 +76,10 @@ def londonaq_alpha(dataset_root: Path) -> List[Vial]: params.LONDONAQ_GRAPH_NAME_LIST, ) preprocessing_list = product_of_preprocessing([False], [False], [True]) - return product_of_vials(data_config_list, get_all_heuristic_params(), preprocessing_list) + return product_of_vials( + data_config_list, get_all_heuristic_params(), preprocessing_list + ) + def compare_heuristics(dataset_name: DatasetName, dataset_root: Path) -> List[Vial]: """Compare the Extension & Collapse heuristic against Suurballe's heuristic""" @@ -98,4 +103,6 @@ def compare_heuristics(dataset_name: DatasetName, dataset_root: Path) -> List[Vi f"{dataset_name} is not a supported dataset for experiment 'compare_heuristics'" ) preprocessing_list = product_of_preprocessing([False], [False], [True]) - return product_of_vials(data_config_list, get_all_heuristic_params(), preprocessing_list) + return product_of_vials( + data_config_list, get_all_heuristic_params(), preprocessing_list + ) diff --git a/pctsp/compare/product_of_params.py b/pctsp/compare/product_of_params.py index c7807dc..8cd9e7c 100644 --- a/pctsp/compare/product_of_params.py +++ b/pctsp/compare/product_of_params.py @@ -89,6 +89,7 @@ def product_of_londonaq_data_config( ) return data_config_list + def product_of_londonaq_data_config_from_alpha( londonaq_root: Path, alpha_list: List[int], diff --git a/pctsp/lab/run_algorithm.py b/pctsp/lab/run_algorithm.py index 16adb07..c0ab4f0 100644 --- a/pctsp/lab/run_algorithm.py +++ b/pctsp/lab/run_algorithm.py @@ -205,6 +205,14 @@ def run_algorithm( path_depth_limit=vial.model_params.path_depth_limit, step_size=vial.model_params.step_size, ) + # if the heuristic is not a feasible solution, then ignore solution + if not is_pctsp_yes_instance( + graph, + vial.data_config.quota, + vial.data_config.root, + heuristic_edge_list, + ): + heuristic_edge_list = [] model = Model(problemName=str(vial.uuid), createscip=True, defaultPlugins=False) edge_list = solve_pctsp( diff --git a/pctsp/vial/experiment.py b/pctsp/vial/experiment.py index 0c4b3f5..b2029f8 100644 --- a/pctsp/vial/experiment.py +++ b/pctsp/vial/experiment.py @@ -15,6 +15,7 @@ class ExperimentName(str, Enum): baseline = "baseline" cost_cover = "cost_cover" disjoint_tours_vs_heuristics = "disjoint_tours_vs_heuristics" + cc_londonaq_alpha = "cc_londonaq_alpha" dryrun = "dryrun" compare_heuristics = "compare_heuristics" londonaq_alpha = "londonaq_alpha" From e9986a731a0890e17d5d8e4eccb03f451529a54d Mon Sep 17 00:00:00 2001 From: PatrickOHara Date: Thu, 10 Aug 2023 13:34:14 +0100 Subject: [PATCH 03/12] Tables for londonaq alpha experiment --- pctsp/apps/tables_app.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/pctsp/apps/tables_app.py b/pctsp/apps/tables_app.py index a1deb29..17c3304 100644 --- a/pctsp/apps/tables_app.py +++ b/pctsp/apps/tables_app.py @@ -222,10 +222,11 @@ def cost_cover_table( dataset: DatasetName, tables_dir: Path, lab_dir: Path = LabDirOption, + experiment_name: ExperimentName = ExperimentName.cost_cover, ) -> None: """Write a table of cost cover experiments to LaTeX file""" tables_dir.mkdir(exist_ok=True, parents=False) - experiment_name = ExperimentName.cost_cover + stats_lab = Lab(lab_dir / dataset.value) filename = ( stats_lab.get_experiment_dir(experiment_name) @@ -258,7 +259,10 @@ def get_cc_name(cc_disjoint_paths: bool, cc_shortest_paths: bool) -> str: ccdf["alpha"] = ccdf["alpha"] / 100 gb_cols.extend(["cost_function", "alpha"]) elif dataset == DatasetName.londonaq: - gb_cols.append("quota") + if experiment_name == ExperimentName.cc_londonaq_alpha: + gb_cols.append("alpha") + elif experiment_name == ExperimentName.cost_cover: + gb_cols.append("quota") ccdf = ccdf.loc[ccdf.graph_name != LondonaqGraphName.laqtinyA] gb_cols.append("cc_name") ccgb = ccdf.groupby(gb_cols) @@ -317,13 +321,15 @@ def get_cc_name(cc_disjoint_paths: bool, cc_shortest_paths: bool) -> str: table_tex_filepath.write_text(table_str, encoding="utf-8") -def get_heuristics_df(dataset: DatasetName, lab_dir: Path) -> pd.DataFrame: +def get_heuristics_df( + dataset: DatasetName, + lab_dir: Path, + exact_experiment_name = ExperimentName.cost_cover, + heuristic_experiment_name: ExperimentName = ExperimentName.compare_heuristics, +) -> pd.DataFrame: """Get a dataframe with the gap between the heuristic solution and the lower bounds from an exact algorithm """ - heuristic_experiment_name = ExperimentName.compare_heuristics - exact_experiment_name = ExperimentName.cost_cover - heuristic_df = pd.read_csv( lab_dir / dataset.value @@ -368,9 +374,10 @@ def heuristics_table( experiment_name: ExperimentName, tables_dir: Path, lab_dir: Path = LabDirOption, + exact_experiment_name: ExperimentName = ExperimentName.cost_cover, ) -> None: """Write a table of heuristic performance to a LaTeX file""" - heuristic_df = get_heuristics_df(dataset, lab_dir) + heuristic_df = get_heuristics_df(dataset, lab_dir, exact_experiment_name=exact_experiment_name, heuristic_experiment_name=experiment_name) heuristic_df = heuristic_df[ heuristic_df.index.get_level_values("graph_name") != LondonaqGraphName.laqtinyA ] From 6e70364baebaafda1e16f8ef06c5330470a39fca Mon Sep 17 00:00:00 2001 From: PatrickOHara Date: Mon, 14 Aug 2023 12:20:56 +0100 Subject: [PATCH 04/12] Fixing bugs when no feasible solution found --- pctsp/apps/tables_app.py | 11 ++++++++--- src/algorithms.cpp | 27 +++++++++++++++++++-------- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/pctsp/apps/tables_app.py b/pctsp/apps/tables_app.py index 17c3304..d7c4b24 100644 --- a/pctsp/apps/tables_app.py +++ b/pctsp/apps/tables_app.py @@ -235,8 +235,10 @@ def cost_cover_table( ccdf: pd.DataFrame = pd.read_csv(filename) ccdf = ccdf.loc[ccdf["cost_function"] != EdgeWeightType.SEMI_MST] # create new columns - ccdf["gap"] = (ccdf["upper_bound"] - ccdf["lower_bound"]) / ccdf["lower_bound"] - ccdf["optimal"] = ccdf["gap"] == 0 + SCIP_STATUS_OPTIMAL = 11 # see https://www.scipopt.org/doc/html/type__stat_8h_source.php + SCIP_STATUS_INFEASIBLE = 12 + ccdf["gap"] = ccdf.apply(lambda x: np.nan if x["status"] == SCIP_STATUS_INFEASIBLE else (x["upper_bound"] - x["lower_bound"]) / x["lower_bound"], axis="columns") + ccdf["optimal"] = (ccdf["gap"] == 0) & (ccdf["status"] == SCIP_STATUS_OPTIMAL) def get_cc_name(cc_disjoint_paths: bool, cc_shortest_paths: bool) -> str: if cc_disjoint_paths and not cc_shortest_paths: @@ -286,7 +288,6 @@ def get_cc_name(cc_disjoint_paths: bool, cc_shortest_paths: bool) -> str: df = df.drop( [ "avg_cuts", - "num_feasible_solutions", "num_cost_cover_disjoint_paths", "num_cost_cover_shortest_paths", "nconss_presolve_disjoint_paths", @@ -294,6 +295,10 @@ def get_cc_name(cc_disjoint_paths: bool, cc_shortest_paths: bool) -> str: ], axis="columns", ) + if experiment_name == ExperimentName.cc_londonaq_alpha: + df = df.drop("num_optimal_solutions", axis="columns") + elif experiment_name == ExperimentName.cost_cover: + df = df.drop("num_feasible_solutions", axis="columns") df = df.unstack() df = df.swaplevel(0, 1, axis="columns").sort_index(axis=1) df = pretty_dataframe(df) diff --git a/src/algorithms.cpp b/src/algorithms.cpp index eb0dc0e..1918a03 100644 --- a/src/algorithms.cpp +++ b/src/algorithms.cpp @@ -37,9 +37,12 @@ std::vector> solvePrizeCollectingTSP( ) { auto edge_var_map = modelPrizeCollectingTSP(scip, graph, heuristic_edges, cost_map, prize_map, quota, root_vertex, name); SCIPsolve(scip); - SCIP_SOL* sol = SCIPgetBestSol(scip); - auto solution_edges = getSolutionEdges(scip, graph, sol, edge_var_map); - return getVertexPairVectorFromEdgeSubset(graph, solution_edges); + if (SCIPgetStatus(scip) != SCIP_INFEASIBLE) { + SCIP_SOL* sol = SCIPgetBestSol(scip); + auto solution_edges = getSolutionEdges(scip, graph, sol, edge_var_map); + return getVertexPairVectorFromEdgeSubset(graph, solution_edges); + } + return std::vector>(); } std::vector> solvePrizeCollectingTSP( @@ -55,9 +58,13 @@ std::vector> solvePrizeCollectingTSP( ) { auto edge_var_map = modelPrizeCollectingTSP(scip, graph, edge_list, heuristic_edges, cost_dict, prize_dict, quota, root_vertex, name); SCIPsolve(scip); - SCIP_SOL* sol = SCIPgetBestSol(scip); - auto solution_edges = getSolutionEdges(scip, graph, sol, edge_var_map); - return getVertexPairVectorFromEdgeSubset(graph, solution_edges); + if (SCIPgetStatus(scip) != SCIP_INFEASIBLE) { + SCIP_SOL* sol = SCIPgetBestSol(scip); + auto solution_edges = getSolutionEdges(scip, graph, sol, edge_var_map); + return getVertexPairVectorFromEdgeSubset(graph, solution_edges); + } + BOOST_LOG_TRIVIAL(info) << "Solution is not feasible. Returning empty solution vector."; + return std::vector>(); } SummaryStats getSummaryStatsFromSCIP(SCIP* scip) { @@ -198,10 +205,14 @@ std::vector> solvePrizeCollectingTSP( // solve the model SCIPsolve(scip); + BOOST_LOG_TRIVIAL(info) << "SCIP solve has finished."; // get the solution - SCIP_SOL* sol = SCIPgetBestSol(scip); - auto solution_edges = getSolutionEdges(scip, graph, sol, edge_var_map); + std::vector solution_edges = std::vector(); + if (SCIPgetNSols(scip) > 0) { + SCIP_SOL* sol = SCIPgetBestSol(scip); + auto solution_edges = getSolutionEdges(scip, graph, sol, edge_var_map); + } // get the node stats of the solver // auto node_stats = node_eventhdlr->getNodeStatsVector(); From 4839a519ea10426fdc4a61dd7543297db994c5e5 Mon Sep 17 00:00:00 2001 From: Patrick O'Hara Date: Mon, 14 Aug 2023 17:33:50 +0100 Subject: [PATCH 05/12] Tables changes --- pctsp/apps/tables_app.py | 106 +++++++++++++++++++++++++++------------ pctsp/vial/vial.py | 5 +- 2 files changed, 78 insertions(+), 33 deletions(-) diff --git a/pctsp/apps/tables_app.py b/pctsp/apps/tables_app.py index d7c4b24..222e19a 100644 --- a/pctsp/apps/tables_app.py +++ b/pctsp/apps/tables_app.py @@ -31,15 +31,17 @@ "branching_strategy": "Branching strategy", "cost_function": "Cost function", "avg_cuts": "AVG CUTS", - "avg_cuts_presolve": "PRE-CUTS", + "avg_cuts_presolve": r"$\overline{\text{PRE-CUTS}}$", "duration": "TIME (s)", "graph_name": "Graph name", "kappa": r"$\kappa$", "gap": "GAP", "max_gap": r"$\max(\text{GAP})$", - "mean_duration": "TIME (s)", - "mean_gap": "GAP", - "min_gap": r"$\min(\text{GAP})$", + "mean_duration": r"$\overline{\text{TIME}}$ (s)", + "mean_gap": r"$\overline{\text{GAP}}$", + "mean_lower_bound": r"$\overline{\text{LB}}$", + "mean_upper_bound": r"$\overline{\text{UB}}$", + "min_gap": r"$\min(\\text{GAP})$", "mean_num_nodes": r"$\mu (NODES)$", "mean_num_sec_disjoint_tour": r"$\mu (SEC_DT) $", "mean_num_sec_maxflow_mincut": r"$\mu (SEC_MM) $", @@ -58,6 +60,10 @@ "quota": "Quota", } +SI_GAP = "S[round-mode=places,round-precision=3,scientific-notation=false,table-format=1.3]" +SI_OPT = "S[scientific-notation=false]" +SI_NUM = "S[table-format=1.2e3]" + def make_column_name_pretty(name: str) -> str: """Return a pretty name for the column""" @@ -226,7 +232,7 @@ def cost_cover_table( ) -> None: """Write a table of cost cover experiments to LaTeX file""" tables_dir.mkdir(exist_ok=True, parents=False) - + stats_lab = Lab(lab_dir / dataset.value) filename = ( stats_lab.get_experiment_dir(experiment_name) @@ -237,6 +243,11 @@ def cost_cover_table( # create new columns SCIP_STATUS_OPTIMAL = 11 # see https://www.scipopt.org/doc/html/type__stat_8h_source.php SCIP_STATUS_INFEASIBLE = 12 + SCIP_BIG_NUMBER = 100000000000000000000.0 + def set_to_nan(value: float) -> float: + return np.nan if value == SCIP_BIG_NUMBER else value + ccdf["lower_bound"] = ccdf["lower_bound"].apply(set_to_nan) + ccdf["upper_bound"] = ccdf["upper_bound"].apply(set_to_nan) ccdf["gap"] = ccdf.apply(lambda x: np.nan if x["status"] == SCIP_STATUS_INFEASIBLE else (x["upper_bound"] - x["lower_bound"]) / x["lower_bound"], axis="columns") ccdf["optimal"] = (ccdf["gap"] == 0) & (ccdf["status"] == SCIP_STATUS_OPTIMAL) @@ -271,6 +282,8 @@ def get_cc_name(cc_disjoint_paths: bool, cc_shortest_paths: bool) -> str: df = ccgb.agg( mean_duration=("duration", np.mean), + mean_lower_bound=("lower_bound", np.mean), + mean_upper_bound=("upper_bound", np.mean), mean_gap=("gap", np.mean), num_optimal_solutions=("optimal", sum), num_feasible_solutions=("feasible", sum), @@ -287,28 +300,27 @@ def get_cc_name(cc_disjoint_paths: bool, cc_shortest_paths: bool) -> str: ) df = df.drop( [ - "avg_cuts", "num_cost_cover_disjoint_paths", "num_cost_cover_shortest_paths", "nconss_presolve_disjoint_paths", "nconss_presolve_shortest_paths", + "avg_cuts", ], axis="columns", ) if experiment_name == ExperimentName.cc_londonaq_alpha: - df = df.drop("num_optimal_solutions", axis="columns") + df = df.drop(["num_optimal_solutions", "avg_cuts_presolve", "mean_duration"], axis="columns") + column_format = "l" + 3 * (SI_GAP + SI_NUM + SI_NUM + "r") elif experiment_name == ExperimentName.cost_cover: - df = df.drop("num_feasible_solutions", axis="columns") + df = df.drop(["num_feasible_solutions", "mean_lower_bound", "mean_upper_bound"], axis="columns") + column_format = "l" + 3 * (SI_NUM + SI_NUM + SI_GAP + "r") df = df.unstack() df = df.swaplevel(0, 1, axis="columns").sort_index(axis=1) df = pretty_dataframe(df) table_tex_filepath = tables_dir / f"{dataset.value}_{experiment_name.value}.tex" # style table using the siunitx package - si_gap = "S[round-mode=places,round-precision=3,scientific-notation=false,table-format=1.3]" - si_opt = "S[scientific-notation=false]" - si_num = "S[table-format=1.2e3]" - column_format = "l" + 2 * (si_num + si_num + si_gap + si_opt) + if dataset == DatasetName.tspwplib: column_format = "l" + column_format @@ -319,11 +331,11 @@ def get_cc_name(cc_disjoint_paths: bool, cc_shortest_paths: bool) -> str: styled_df = df.style.format() table_str = styled_df.to_latex( - multicol_align="c", siunitx=True, column_format=column_format + hrules=True, multicol_align="c", siunitx=True, column_format=column_format ) table_str = table_str.replace("cc_name", "") print(table_str) - table_tex_filepath.write_text(table_str, encoding="utf-8") + # table_tex_filepath.write_text(table_str, encoding="utf-8") def get_heuristics_df( @@ -382,38 +394,68 @@ def heuristics_table( exact_experiment_name: ExperimentName = ExperimentName.cost_cover, ) -> None: """Write a table of heuristic performance to a LaTeX file""" + logger = get_pctsp_logger("heuristics-table") heuristic_df = get_heuristics_df(dataset, lab_dir, exact_experiment_name=exact_experiment_name, heuristic_experiment_name=experiment_name) + + # NOTE remove SBL-EC heuristic! + heuristic_df = heuristic_df.loc[heuristic_df["algorithm"] != AlgorithmName.suurballes_extension_collapse] + heuristic_df = heuristic_df[ heuristic_df.index.get_level_values("graph_name") != LondonaqGraphName.laqtinyA ] table_tex_filepath = tables_dir / f"{dataset.value}_{experiment_name.value}.tex" if dataset == DatasetName.tspwplib: - cols = ["kappa", "cost_function", "algorithm"] + cols = ["cost_function", "kappa", "algorithm"] heuristic_df = heuristic_df[ heuristic_df.index.get_level_values("cost_function").isin( params.TSPLIB_COST_FUNCTIONS ) ] - heuristic_gb = heuristic_df.groupby(cols) - summary_df = heuristic_gb.agg( - num_feasible_solutions=("feasible", sum), - ) - summary_df = summary_df.unstack().unstack() - summary_df = summary_df.swaplevel(1, 2, axis="columns") + elif dataset == DatasetName.londonaq and experiment_name == ExperimentName.londonaq_alpha: + cols = ["alpha", "algorithm"] elif dataset == DatasetName.londonaq: - summary_df = heuristic_df.reset_index(level=["dataset", "root"]).set_index( - "algorithm", append=True - )[["gap"]] - summary_df = summary_df.unstack() - replacements = { - key.value: ShortAlgorithmName[key.name].value for key in AlgorithmName - } - replacements[EdgeWeightType.EUC_2D] = "EUC" + cols = ["quota", "algorithm"] + else: + raise ValueError(f"Dataset {dataset} not recognized.") + heuristic_gb = heuristic_df.groupby(cols) + summary_df = heuristic_gb.agg( + # num_optimal_solutions=("gap", lambda x: x==0.0), + num_feasible_solutions=("feasible", sum), + mean_gap=("gap", np.mean), + mean_duration=("duration", np.mean), + ) + summary_df = summary_df.unstack(level="algorithm") + summary_df.columns.rename(["metric", "algorithm"], inplace=True) + summary_df = summary_df.swaplevel("algorithm", "metric", axis="columns") + + if dataset == DatasetName.londonaq: + summary_df.index.rename(PRETTY_COLUMN_NAMES["quota"]) + + summary_df = summary_df.sort_index(axis="columns") + summary_df = summary_df.sort_index(axis="index") + + summary_df = summary_df.rename( + lambda x: ShortAlgorithmName[x], + axis="columns", + level="algorithm" + ).rename(PRETTY_COLUMN_NAMES, axis="columns", level="metric") + summary_df = summary_df.rename({ + EdgeWeightType.EUC_2D: "EUC", + EdgeWeightType.GEO: "GEO", + EdgeWeightType.MST: "MST", + }, axis="index") + print(summary_df) - table_str = summary_df.style.format_index( - formatter=lambda x: replacements[x] if x in replacements else x, axis="columns" - ).to_latex(hrules=True, multicol_align="c") + column_format = "l" + if dataset == DatasetName.tspwplib: + column_format += "l" + column_format += 4*(SI_NUM + SI_GAP + "r") + + table_str = summary_df.style.to_latex( + hrules=True, multicol_align="c", multirow_align="naive", siunitx=True, column_format=column_format + ) print(table_str) + logger.info("Writing table to LaTeX file: %s", table_tex_filepath) table_tex_filepath.write_text(table_str, encoding="utf-8") diff --git a/pctsp/vial/vial.py b/pctsp/vial/vial.py index 18c59c4..c24fba3 100644 --- a/pctsp/vial/vial.py +++ b/pctsp/vial/vial.py @@ -3,7 +3,7 @@ from typing import Any, Dict from uuid import UUID import pandas as pd -from pydantic import BaseModel +from pydantic import BaseModel, ConfigDict from .data_config import DataConfig from .model_params import ModelParams from .preprocessing import Preprocessing @@ -19,6 +19,9 @@ class Vial(BaseModel): model_params: ModelParams preprocessing: Preprocessing + # ignore pydantic namespaces + model_config = ConfigDict(protected_namespaces=()) + def flat_dict_from_vial(vial: Vial) -> Dict[str, Any]: """A flat dictionary from a vial. From 8de8767eee47e6e215f32588304f41e24d9a3c9d Mon Sep 17 00:00:00 2001 From: Patrick O'Hara Date: Wed, 16 Aug 2023 18:38:33 +0100 Subject: [PATCH 06/12] Dataset app changes --- pctsp/apps/dataset_app.py | 154 +++++++++++++++++++++++++------------- 1 file changed, 102 insertions(+), 52 deletions(-) diff --git a/pctsp/apps/dataset_app.py b/pctsp/apps/dataset_app.py index 1557027..d2b3aba 100644 --- a/pctsp/apps/dataset_app.py +++ b/pctsp/apps/dataset_app.py @@ -7,8 +7,12 @@ import typer from tspwplib import ( BaseTSP, + EdgeWeightType, Generation, ProfitsProblem, + NotConnectedException, + asymmetric_from_undirected, + biggest_vertex_id_from_graph, build_path_to_londonaq_yaml, build_path_to_oplib_instance, metricness, @@ -16,84 +20,130 @@ rename_edge_attributes, rename_node_attributes, sparsify_uid, + split_head, total_cost, total_prize, ) -from ..preprocessing import remove_one_connected_components +from ..compare import params +from ..preprocessing import remove_one_connected_components, undirected_vertex_disjoint_paths_map, vertex_disjoint_cost_map +from ..suurballe import suurballe_shortest_vertex_disjoint_paths +from ..utils import get_pctsp_logger from ..vial import DatasetName from .options import LondonaqRootOption, OPLibRootOption -from ..compare import params dataset_app = typer.Typer(name="dataset", help="Making and summarizing datasets") -@dataset_app.command(name="metricness") -def metricness_of_dataset( +@dataset_app.command(name="stats") +def stats_of_dataset( dataset: DatasetName, londonaq_root: Path = LondonaqRootOption, oplib_root: Path = OPLibRootOption, ) -> pd.DataFrame: """Create a pandas dataframe of the metricness and write to CSV""" - dataset_stats: Dict[str, List[Any]] = { - "num_nodes": [], - "num_edges": [], - "total_cost": [], - "total_prize": [], - "metricness": [], - } + logger = get_pctsp_logger("dataset-stats") + dataset_stats: List[Dict[str, float]] = [] names = [] + index=None if dataset == DatasetName.londonaq: - names = params.LONDONAQ_GRAPH_NAME_LIST - elif dataset == DatasetName.tspwplib: - names = params.TSPLIB_GRAPH_NAME_LIST - for graph_name in names: - # load the graph - if dataset == DatasetName.londonaq: + logger.info("Calculating stats for londonaq dataset.") + for graph_name in params.LONDONAQ_GRAPH_NAME_LIST: + logger.info("Loading %s", graph_name.value) problem_path = build_path_to_londonaq_yaml(londonaq_root, graph_name) tsp = BaseTSP.from_yaml(problem_path) - elif dataset == DatasetName.tspwplib: - problem_path = build_path_to_oplib_instance( - oplib_root, - Generation.gen3, - graph_name, - ) - # load the problem from file - problem = ProfitsProblem().load(problem_path) - tsp = BaseTSP.from_tsplib95(problem) - # get the graph in networkx - graph = tsp.get_graph() - rename_edge_attributes(graph, {"weight": "cost"}, del_old_attr=True) - try: # londonaq dataset + graph = tsp.get_graph() + rename_edge_attributes(graph, {"weight": "cost"}, del_old_attr=True) rename_node_attributes(graph, {"demand": "prize"}, del_old_attr=True) - except KeyError: # tsplib dataset - nx.set_node_attributes(graph, problem.get_node_score(), name="prize") + logger.info("Calculating stats for %s", graph_name.value) + dataset_stats.append(get_graph_stats(graph, tsp.depots[0])) + names.append(graph_name.value) + index = pd.Index(names, name="graph_name") - # if removing edges - if dataset == DatasetName.tspwplib: - graph = sparsify_uid(graph, 5) - new_cost = mst_cost(graph, cost_attr="cost") - nx.set_edge_attributes(graph, new_cost, name="cost") - - # preprocessing - graph = remove_one_connected_components(graph, tsp.depots[0]) + elif dataset == DatasetName.tspwplib: + for graph_name in params.TSPLIB_GRAPH_NAME_LIST: + for gen in Generation: + problem_path = build_path_to_oplib_instance(oplib_root, gen, graph_name) + for cost in params.TSPLIB_COST_FUNCTIONS: + for kappa in params.TSPLIB_KAPPA_LIST: + logger.info("Loading %s on generation %s with cost %s and kappa %s", graph_name.value, gen.value, cost.value, kappa) + problem_path = build_path_to_oplib_instance( + oplib_root, + gen, + graph_name, + ) + # load the problem from file + problem = ProfitsProblem().load(problem_path) + tsp = BaseTSP.from_tsplib95(problem) + graph = tsp.get_graph() + nx.set_node_attributes(graph, problem.get_node_score(), name="prize") + rename_edge_attributes(graph, {"weight": "cost"}, del_old_attr=True) + graph = sparsify_uid(graph, kappa) + if cost == EdgeWeightType.MST: + new_cost = mst_cost(graph, cost_attr="cost") + nx.set_edge_attributes(graph, new_cost, name="cost") + logger.info("Calculating stats for %s", graph_name.value) + dataset_stats.append(get_graph_stats(graph, tsp.depots[0])) + names.append((graph_name.value, gen.value, cost.value, kappa)) + index = pd.MultiIndex.from_tuples(names, names=["graph_name", "generation", "cost_function", "kappa"]) - # count the number of edges, vertices, total prize, total cost and the metricness - dataset_stats["num_nodes"].append(graph.number_of_nodes()) - dataset_stats["num_edges"].append(graph.number_of_edges()) - dataset_stats["total_cost"].append( - total_cost(nx.get_edge_attributes(graph, "cost"), list(graph.edges())) - ) - dataset_stats["total_prize"].append( - total_prize(nx.get_node_attributes(graph, "prize"), list(graph.nodes())) - ) - dataset_stats["metricness"].append(metricness(graph)) - df = pd.DataFrame(dataset_stats, index=names) + logger.info("Creating dataframe from dataset stats.") + df = pd.DataFrame(dataset_stats, index=index) print(df) if dataset == DatasetName.londonaq: filepath = londonaq_root / "londonaq_dataset.csv" elif dataset == DatasetName.tspwplib: filepath = oplib_root / "tsplib_dataset.csv" - df.index = df.index.rename("graph_name") + logger.info("Writing dataframe to CSV at %s", filepath) df.to_csv(filepath, index=True) return df + + +def get_graph_stats(graph: nx.Graph, root_vertex: int) -> Dict[str, float]: + # count the number of edges, vertices, total prize, total cost and the metricness + instance_stats = {} + instance_stats["num_nodes"] = graph.number_of_nodes() + instance_stats["num_edges"] = graph.number_of_edges() + instance_stats["total_cost"] = total_cost(nx.get_edge_attributes(graph, "cost"), list(graph.edges())) + og_prize = total_prize(nx.get_node_attributes(graph, "prize"), list(graph.nodes())) + instance_stats["total_prize"] = og_prize + try: + instance_stats["metricness"] = metricness(graph) + except NotConnectedException: + largest_component_graph = graph.subgraph(max(nx.connected_components(graph), key=len)) + instance_stats["metricness"] = metricness(largest_component_graph) + + # evaluate the largest prize of any least-cost vertex-disjoint paths + biggest_vertex = biggest_vertex_id_from_graph(graph) + asymmetric_graph = asymmetric_from_undirected(graph) + tree = suurballe_shortest_vertex_disjoint_paths( + asymmetric_graph, + split_head(biggest_vertex, root_vertex), + weight="cost", + ) + vertex_disjoint_paths_map = undirected_vertex_disjoint_paths_map( + tree, biggest_vertex + ) + biggest_prize = 0 + biggest_vertex = None + prize_map = nx.get_node_attributes(graph, "prize") + for u, (p1, p2) in vertex_disjoint_paths_map.items(): + prize = total_prize(prize_map, p1) + total_prize(prize_map, p2) - prize_map[u] - prize_map[root_vertex] + if prize > biggest_prize: + biggest_prize = prize + biggest_vertex = u + instance_stats["biggest_disjoint_prize"] = biggest_prize + instance_stats["disjoint_prize_ratio"] = float(biggest_prize) / float(og_prize) + + # preprocessing + graph = remove_one_connected_components(graph, root_vertex) + + # re-evaluate stats after preprocessing + instance_stats["preprocessed_num_nodes"] = graph.number_of_nodes() + instance_stats["preprocessed_num_edges"] = graph.number_of_edges() + instance_stats["preprocessed_total_cost"] = total_cost(nx.get_edge_attributes(graph, "cost"), list(graph.edges())) + pp_prize = total_prize(nx.get_node_attributes(graph, "prize"), list(graph.nodes())) + instance_stats["preprocessed_total_prize"] = pp_prize + instance_stats["preprocessed_metricness"] = metricness(graph) + instance_stats["preprocessed_prize_ratio"] = float(pp_prize) / float(og_prize) + return instance_stats From 9936ec7d42210803f6d3ea2b491f3f5922d4270c Mon Sep 17 00:00:00 2001 From: PatrickOHara Date: Mon, 21 Aug 2023 14:30:29 +0100 Subject: [PATCH 07/12] Fixing dataset tables --- pctsp/apps/tables_app.py | 43 ++++++++++++++++++++++++++++++++-------- 1 file changed, 35 insertions(+), 8 deletions(-) diff --git a/pctsp/apps/tables_app.py b/pctsp/apps/tables_app.py index 222e19a..5d46f5d 100644 --- a/pctsp/apps/tables_app.py +++ b/pctsp/apps/tables_app.py @@ -46,8 +46,8 @@ "mean_num_sec_disjoint_tour": r"$\mu (SEC_DT) $", "mean_num_sec_maxflow_mincut": r"$\mu (SEC_MM) $", "metricness": r"$\zeta(G, c)$", - "num_edges": r"$m$", - "num_nodes": r"$n$", + "num_edges": r"$|E(G)|$", + "num_nodes": r"$|V(G)|$", "total_cost": r"$c(E(G))$", "total_prize": r"$p(V(G))$", "num_secs": r"$\mu (SEC)$", @@ -58,11 +58,22 @@ "sec_sepafreq": "SEC freq", "std_gap": r"$\sigma (\text{GAP})$", "quota": "Quota", + "biggest_disjoint_prize": r"$\max_{t \in G} \{ p(V(\cP_1)) + p(V(\cP_2)) - p(t) \}$", + "disjoint_prize_ratio": r"$D(G)$", + "preprocessed_num_nodes": r"$|V(H)|$", + "preprocessed_num_edges": r"$|E(H)|$", + "preprocessed_total_cost": r"$c(E(H))", + "preprocessed_total_prize": r"$p(V(H))$", + "preprocessed_metricness": r"$\zeta(H, c)$", + "preprocessed_prize_ratio": r"$\frac{p(V(H))}{p(V(G))}$", + "mean_preprocessed_prize_ratio": r"\text{AVG}$\left(\frac{p(V(H))}{p(V(G))}\right)$", + "mean_disjoint_prize_ratio": r"\text{AVG}$(D(G))$", } SI_GAP = "S[round-mode=places,round-precision=3,scientific-notation=false,table-format=1.3]" SI_OPT = "S[scientific-notation=false]" SI_NUM = "S[table-format=1.2e3]" +SI_SEP = "S[round-mode=none,group-separator = {,},group-minimum-digits = 4,scientific-notation=false,table-format=5.0]" def make_column_name_pretty(name: str) -> str: @@ -80,21 +91,37 @@ def summarize_dataset( oplib_root: Path = OPLibRootOption, ) -> None: """Create a table summarizing each instance of a dataset""" + dataset_logger = get_pctsp_logger("dataset") if dataset == DatasetName.londonaq: filepath = londonaq_root / "londonaq_dataset.csv" tables_path = tables_dir / "londonaq_dataset.tex" + columns = [ + "graph_name","num_nodes","num_edges","preprocessed_num_nodes","preprocessed_num_edges","preprocessed_prize_ratio", "metricness","disjoint_prize_ratio" + ] + column_format = "l" + 4 * SI_SEP + 3 * SI_GAP elif dataset == DatasetName.tspwplib: filepath = oplib_root / "tsplib_dataset.csv" tables_path = tables_dir / "tsplib_dataset.tex" + column_format = "l" + 4 * SI_GAP + dataset_logger.info("Reading dataset CSV from %s", filepath) df = pd.read_csv(filepath) - df.style.format( - { - "metricness": "{:.2f}", - "graph_name": lambda x: x[:-1] if x in list(LondonaqGraphName) else x, - } - ).format_index(make_column_name_pretty, axis="columns").hide(axis="index").to_latex( + if dataset == DatasetName.londonaq: + df = df[columns].set_index("graph_name") + elif dataset == DatasetName.tspwplib: + dataset_logger.info("Aggregating dataset stats.") + df = df.groupby(["cost_function", "kappa"]).aggregate( + mean_disjoint_prize_ratio=("disjoint_prize_ratio", np.mean), + mean_preprocessed_prize_ratio=("preprocessed_prize_ratio", np.mean), + ) + df = df.unstack(level="cost_function").swaplevel(i="cost_function", j=0, axis="columns").sort_index(axis="columns") + print(df) + dataset_logger.info("Writing dataset LaTeX table to %s", tables_path) + df.style.format_index(make_column_name_pretty, axis="columns").format_index(make_column_name_pretty, axis="index").to_latex( buf=tables_path, hrules=True, + siunitx=True, + column_format=column_format, + multicol_align="c", ) From 545ab9b0f32f1d5965d510e7b5a23ef28f50d9fd Mon Sep 17 00:00:00 2001 From: Patrick O'Hara Date: Mon, 8 Jan 2024 11:48:30 +0000 Subject: [PATCH 08/12] Fixing tables --- pctsp/apps/dataset_app.py | 1 + pctsp/apps/plot_app.py | 11 ++++++++--- pctsp/apps/tables_app.py | 16 +++++++++++----- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/pctsp/apps/dataset_app.py b/pctsp/apps/dataset_app.py index d2b3aba..75cbe70 100644 --- a/pctsp/apps/dataset_app.py +++ b/pctsp/apps/dataset_app.py @@ -134,6 +134,7 @@ def get_graph_stats(graph: nx.Graph, root_vertex: int) -> Dict[str, float]: biggest_vertex = u instance_stats["biggest_disjoint_prize"] = biggest_prize instance_stats["disjoint_prize_ratio"] = float(biggest_prize) / float(og_prize) + instance_stats["max_disjoint_paths_cost"] = max(vertex_disjoint_cost_map(tree, biggest_vertex).values()) # preprocessing graph = remove_one_connected_components(graph, root_vertex) diff --git a/pctsp/apps/plot_app.py b/pctsp/apps/plot_app.py index 263c9b1..96d0986 100644 --- a/pctsp/apps/plot_app.py +++ b/pctsp/apps/plot_app.py @@ -10,6 +10,7 @@ from ..vial import DatasetName, ShortAlgorithmName from .options import LabDirOption from .tables_app import get_heuristics_df +from ..utils import get_pctsp_logger plot_app = typer.Typer(name="plot", help="Plotting results") @@ -29,8 +30,11 @@ def plot_heuristics_figure( lab_dir: Path = LabDirOption, ) -> None: """Plot a figure showing the performance of heuristics on a dataset""" + logger = get_pctsp_logger("plot-heuristics") figures_dir.mkdir(exist_ok=True, parents=False) + logger.info("Reading heuristics results from %s.", lab_dir) tspwplib_df = get_heuristics_df(DatasetName.tspwplib, lab_dir) + logger.info("TSPLIB heuristics dataframe has %s rows.", len(tspwplib_df)) londonaq_df = get_heuristics_df(DatasetName.londonaq, lab_dir) # give short names to algorithms @@ -86,6 +90,7 @@ def plot_heuristics_figure( kappa_df = kappa_df.iloc[ kappa_df.index.get_level_values("cost_function") == cost_function ] + logger.info("Plotting %s points for cost function %s and kappa %s.", len(kappa_df), cost_function.value, kappa) for algorithm in [ ShortAlgorithmName.bfs_extension_collapse, ShortAlgorithmName.bfs_path_extension_collapse, @@ -113,9 +118,9 @@ def plot_heuristics_figure( "x": 1, }, ) - bottom_fig.write_image( - str(figures_dir / f"{DatasetName.tspwplib}_{cost_function}_heuristics.pdf") - ) + figure_path = figures_dir / f"{DatasetName.tspwplib}_{cost_function}_heuristics.pdf" + logger.info("Writing figure for cost function %s to %s", cost_function.value, figure_path) + bottom_fig.write_image(str(figure_path)) def add_traces_heuristic( diff --git a/pctsp/apps/tables_app.py b/pctsp/apps/tables_app.py index 5d46f5d..970c29a 100644 --- a/pctsp/apps/tables_app.py +++ b/pctsp/apps/tables_app.py @@ -409,6 +409,7 @@ def get_heuristics_df( heuristic_df["gap"] = ( heuristic_df["objective"] - heuristic_df["lower_bound"] ) / heuristic_df["lower_bound"] + heuristic_df["is_optimal"] = heuristic_df["gap"] == 0.0 return heuristic_df @@ -419,18 +420,23 @@ def heuristics_table( tables_dir: Path, lab_dir: Path = LabDirOption, exact_experiment_name: ExperimentName = ExperimentName.cost_cover, + sbl_ec_only: bool = False, ) -> None: """Write a table of heuristic performance to a LaTeX file""" logger = get_pctsp_logger("heuristics-table") heuristic_df = get_heuristics_df(dataset, lab_dir, exact_experiment_name=exact_experiment_name, heuristic_experiment_name=experiment_name) # NOTE remove SBL-EC heuristic! - heuristic_df = heuristic_df.loc[heuristic_df["algorithm"] != AlgorithmName.suurballes_extension_collapse] - + if sbl_ec_only: + heuristic_df = heuristic_df.loc[heuristic_df["algorithm"] == AlgorithmName.suurballes_extension_collapse] + table_tex_filepath = tables_dir / f"{dataset.value}_{experiment_name.value}_{ShortAlgorithmName.suurballes_extension_collapse.value}.tex" + else: + heuristic_df = heuristic_df.loc[heuristic_df["algorithm"] != AlgorithmName.suurballes_extension_collapse] + table_tex_filepath = tables_dir / f"{dataset.value}_{experiment_name.value}.tex" heuristic_df = heuristic_df[ heuristic_df.index.get_level_values("graph_name") != LondonaqGraphName.laqtinyA ] - table_tex_filepath = tables_dir / f"{dataset.value}_{experiment_name.value}.tex" + if dataset == DatasetName.tspwplib: cols = ["cost_function", "kappa", "algorithm"] heuristic_df = heuristic_df[ @@ -446,7 +452,7 @@ def heuristics_table( raise ValueError(f"Dataset {dataset} not recognized.") heuristic_gb = heuristic_df.groupby(cols) summary_df = heuristic_gb.agg( - # num_optimal_solutions=("gap", lambda x: x==0.0), + num_optimal_solutions=("is_optimal", sum), num_feasible_solutions=("feasible", sum), mean_gap=("gap", np.mean), mean_duration=("duration", np.mean), @@ -483,7 +489,7 @@ def heuristics_table( ) print(table_str) logger.info("Writing table to LaTeX file: %s", table_tex_filepath) - table_tex_filepath.write_text(table_str, encoding="utf-8") + # table_tex_filepath.write_text(table_str, encoding="utf-8") @tables_app.command(name="all") From 97e3a59888804970df3f8627ec5f54c5744f5807 Mon Sep 17 00:00:00 2001 From: Patrick O'Hara Date: Wed, 27 Mar 2024 13:18:55 +0000 Subject: [PATCH 09/12] Bug fixing in tests --- include/pctsp/graph.hh | 12 ++++++++++++ pctsp/apps/dataset_app.py | 3 +-- src/algorithms.cpp | 6 +++--- src/graph.cpp | 6 ++++++ tests/test_algorithms.cpp | 14 +++++++++++--- tests/test_subtour_elimination.cpp | 16 +++++++++------- 6 files changed, 42 insertions(+), 15 deletions(-) diff --git a/include/pctsp/graph.hh b/include/pctsp/graph.hh index 43c6a13..014c299 100644 --- a/include/pctsp/graph.hh +++ b/include/pctsp/graph.hh @@ -2,6 +2,7 @@ #ifndef __PCTSP_GRAPH__ #define __PCTSP_GRAPH__ +#include #include #include #include @@ -150,6 +151,17 @@ std::vector getEdgeVariables( std::vector& edges ); +template +void printEdges(TGraph& graph, EdgeIt& first, EdgeIt& last) { + // typedef typename boost::graph_traits< TGraph >::edge_descriptor TEdge; + for (; first != last; first++) { + auto edge = *first; + std::cout << boost::source(edge, graph) << ", " << boost::target(edge, graph) << std::endl; + } +}; + +void printEdges(std::vector>& edges); + template std::vector::vertex_descriptor> getVerticesOfEdges( TGraph& graph, diff --git a/pctsp/apps/dataset_app.py b/pctsp/apps/dataset_app.py index 75cbe70..6d8e81c 100644 --- a/pctsp/apps/dataset_app.py +++ b/pctsp/apps/dataset_app.py @@ -10,7 +10,6 @@ EdgeWeightType, Generation, ProfitsProblem, - NotConnectedException, asymmetric_from_undirected, biggest_vertex_id_from_graph, build_path_to_londonaq_yaml, @@ -109,7 +108,7 @@ def get_graph_stats(graph: nx.Graph, root_vertex: int) -> Dict[str, float]: instance_stats["total_prize"] = og_prize try: instance_stats["metricness"] = metricness(graph) - except NotConnectedException: + except nx.exception.NetworkXException: # FIXME change to NotConnectedException largest_component_graph = graph.subgraph(max(nx.connected_components(graph), key=len)) instance_stats["metricness"] = metricness(largest_component_graph) diff --git a/src/algorithms.cpp b/src/algorithms.cpp index 1918a03..c3a8a28 100644 --- a/src/algorithms.cpp +++ b/src/algorithms.cpp @@ -37,7 +37,7 @@ std::vector> solvePrizeCollectingTSP( ) { auto edge_var_map = modelPrizeCollectingTSP(scip, graph, heuristic_edges, cost_map, prize_map, quota, root_vertex, name); SCIPsolve(scip); - if (SCIPgetStatus(scip) != SCIP_INFEASIBLE) { + if (SCIPgetStatus(scip) != SCIP_STATUS_INFEASIBLE) { SCIP_SOL* sol = SCIPgetBestSol(scip); auto solution_edges = getSolutionEdges(scip, graph, sol, edge_var_map); return getVertexPairVectorFromEdgeSubset(graph, solution_edges); @@ -58,7 +58,7 @@ std::vector> solvePrizeCollectingTSP( ) { auto edge_var_map = modelPrizeCollectingTSP(scip, graph, edge_list, heuristic_edges, cost_dict, prize_dict, quota, root_vertex, name); SCIPsolve(scip); - if (SCIPgetStatus(scip) != SCIP_INFEASIBLE) { + if (SCIPgetStatus(scip) != SCIP_STATUS_INFEASIBLE) { SCIP_SOL* sol = SCIPgetBestSol(scip); auto solution_edges = getSolutionEdges(scip, graph, sol, edge_var_map); return getVertexPairVectorFromEdgeSubset(graph, solution_edges); @@ -211,7 +211,7 @@ std::vector> solvePrizeCollectingTSP( std::vector solution_edges = std::vector(); if (SCIPgetNSols(scip) > 0) { SCIP_SOL* sol = SCIPgetBestSol(scip); - auto solution_edges = getSolutionEdges(scip, graph, sol, edge_var_map); + solution_edges = getSolutionEdges(scip, graph, sol, edge_var_map); } // get the node stats of the solver diff --git a/src/graph.cpp b/src/graph.cpp index d11b71c..d9dccac 100644 --- a/src/graph.cpp +++ b/src/graph.cpp @@ -38,6 +38,12 @@ std::vector getEdgeVariables( return getEdgeVariables(scip, graph, edge_variable_map, first, last); } +void printEdges(std::vector>& edges){ + for (auto& edge : edges) { + std::cout << edge.first << ", " << edge.second << std::endl; + } +} + std::vector getEdgesInducedByVertices(PCTSPgraph& graph, std::vector& vertices) { auto first = vertices.begin(); auto last = vertices.end(); diff --git a/tests/test_algorithms.cpp b/tests/test_algorithms.cpp index 5e8ee18..3ac4388 100644 --- a/tests/test_algorithms.cpp +++ b/tests/test_algorithms.cpp @@ -3,6 +3,7 @@ #include "fixtures.hh" #include "pctsp/graph.hh" #include "pctsp/algorithms.hh" +#include "pctsp/node_selection.hh" typedef GraphFixture AlgorithmsFixture; typedef GraphFixture SuurballeGraphFixture; @@ -159,9 +160,16 @@ TEST_P(AlgorithmsFixture, testAddHeuristicVarsToSolver) { // initialise and create the model without subtour elimiation constraints SCIP* scip_model = NULL; SCIPcreate(&scip_model); - includeBranchRules(scip_model); - // SCIPincludeDefaultPlugins(scip_model); - SCIPcreateProbBasic(scip_model, "test-add-heuristic"); + SCIPincludeDefaultPlugins(scip_model); + SCIPcreateProbBasic(scip_model, "testAddHeuristicVarsToSolver"); + + SCIP_MESSAGEHDLR* handler; + std::filesystem::path solver_dir = ".logs"; + std::filesystem::create_directory(solver_dir); + std::string filename = "testAddHeuristicVarsToSolver_" + getParamName() + ".txt"; + std::filesystem::path logs_txt = solver_dir / filename; + SCIPcreateMessagehdlrDefault(&handler, false, logs_txt.c_str(), true); + SCIPsetMessagehdlr(scip_model, handler); // add variables and constraints SCIP_RETCODE code = diff --git a/tests/test_subtour_elimination.cpp b/tests/test_subtour_elimination.cpp index b2f353b..e9eb464 100644 --- a/tests/test_subtour_elimination.cpp +++ b/tests/test_subtour_elimination.cpp @@ -154,7 +154,7 @@ TEST_P(SubtourGraphFixture, testSubtourParams) { int quota; switch (GetParam()) { case GraphType::COMPLETE25: quota = totalPrizeOfGraph(graph, prize_map); break; - default: quota = 3; break; + default: quota = 4; break; } addSelfLoopsToGraph(graph); @@ -204,17 +204,17 @@ TEST_P(SubtourGraphFixture, testSubtourParams) { break; } case GraphType::SUURBALLE: { - expected_cost = 15; + expected_cost = 16; expected_nnodes = 3; + expected_num_sec_maxflow_mincut = 4; break; } case GraphType::COMPLETE4: { - expected_cost = 4; - expected_num_sec_maxflow_mincut = 3; + expected_cost = 6; break; } case GraphType::COMPLETE5: { - expected_cost = 5; + expected_cost = 7; break; } case GraphType::COMPLETE25: { @@ -231,8 +231,8 @@ TEST_P(SubtourGraphFixture, testSubtourParams) { EXPECT_EQ(expected_cost, actual_cost); auto summary_yaml = logger_dir / PCTSP_SUMMARY_STATS_YAML; auto stats = readSummaryStatsFromYaml(summary_yaml); - // EXPECT_EQ(stats.num_sec_maxflow_mincut, expected_num_sec_maxflow_mincut); - // EXPECT_EQ(stats.num_sec_disjoint_tour, expected_num_sec_disjoint_tour); + EXPECT_EQ(stats.num_sec_maxflow_mincut, expected_num_sec_maxflow_mincut); + EXPECT_EQ(stats.num_sec_disjoint_tour, expected_num_sec_disjoint_tour); // EXPECT_EQ(SCIPgetNNodes(scip), expected_nnodes); SCIPfree(&scip); } @@ -282,6 +282,7 @@ TEST(TestSubtourElimination, testPushIntoRollingLpGapList) { EXPECT_EQ(rolling_gaps.back(), gap); } + TEST_P(SubtourGraphFixture, testTailingOff) { PCTSPinitLogging(logging::trivial::warning); bool sec_disjoint_tour = true; @@ -303,6 +304,7 @@ TEST_P(SubtourGraphFixture, testTailingOff) { SCIPcreate(&scip); std::string name = "testTailingOff"; + auto solution_edges = solvePrizeCollectingTSP( scip, graph, From 8bfdd64ad109de760a7dfe1f920fbeeeb0b7bacc Mon Sep 17 00:00:00 2001 From: Patrick O'Hara Date: Wed, 27 Mar 2024 17:07:35 +0000 Subject: [PATCH 10/12] Fixed a bug in the CPP logger --- include/pctsp/logger.hh | 1 + pctsp/apps/dataset_app.py | 103 ++++++++++++++++--------- pctsp/apps/plot_app.py | 17 ++++- pctsp/apps/tables_app.py | 106 +++++++++++++++++++------- pctsp/compare/dryrun_experiment.py | 2 +- pctsp/compare/heuristic_experiment.py | 2 + pctsp/lab/lab.py | 6 +- pctsp/vial/result.py | 2 +- src/algorithms.cpp | 5 +- src/cost_cover.cpp | 2 +- src/logger.cpp | 14 ++++ src/solution.cpp | 2 +- src/subtour_elimination.cpp | 10 +-- tests/conftest.py | 2 +- tests/test_algorithms.py | 25 ++++++ tests/test_logger.cpp | 8 ++ tests/test_params.py | 4 +- 17 files changed, 228 insertions(+), 83 deletions(-) diff --git a/include/pctsp/logger.hh b/include/pctsp/logger.hh index 09f2f05..17e321c 100644 --- a/include/pctsp/logger.hh +++ b/include/pctsp/logger.hh @@ -6,6 +6,7 @@ #include #include #include +#include namespace logging = boost::log; diff --git a/pctsp/apps/dataset_app.py b/pctsp/apps/dataset_app.py index 6d8e81c..ca4bf14 100644 --- a/pctsp/apps/dataset_app.py +++ b/pctsp/apps/dataset_app.py @@ -1,6 +1,7 @@ """Dataset app""" -from typing import Any, Dict, List +import itertools +from typing import Dict, List from pathlib import Path import networkx as nx import pandas as pd @@ -24,7 +25,11 @@ total_prize, ) from ..compare import params -from ..preprocessing import remove_one_connected_components, undirected_vertex_disjoint_paths_map, vertex_disjoint_cost_map +from ..preprocessing import ( + remove_one_connected_components, + undirected_vertex_disjoint_paths_map, + vertex_disjoint_cost_map, +) from ..suurballe import suurballe_shortest_vertex_disjoint_paths from ..utils import get_pctsp_logger from ..vial import DatasetName @@ -44,7 +49,7 @@ def stats_of_dataset( logger = get_pctsp_logger("dataset-stats") dataset_stats: List[Dict[str, float]] = [] names = [] - index=None + index = None if dataset == DatasetName.londonaq: logger.info("Calculating stats for londonaq dataset.") for graph_name in params.LONDONAQ_GRAPH_NAME_LIST: @@ -60,31 +65,40 @@ def stats_of_dataset( index = pd.Index(names, name="graph_name") elif dataset == DatasetName.tspwplib: - for graph_name in params.TSPLIB_GRAPH_NAME_LIST: - for gen in Generation: - problem_path = build_path_to_oplib_instance(oplib_root, gen, graph_name) - for cost in params.TSPLIB_COST_FUNCTIONS: - for kappa in params.TSPLIB_KAPPA_LIST: - logger.info("Loading %s on generation %s with cost %s and kappa %s", graph_name.value, gen.value, cost.value, kappa) - problem_path = build_path_to_oplib_instance( - oplib_root, - gen, - graph_name, - ) - # load the problem from file - problem = ProfitsProblem().load(problem_path) - tsp = BaseTSP.from_tsplib95(problem) - graph = tsp.get_graph() - nx.set_node_attributes(graph, problem.get_node_score(), name="prize") - rename_edge_attributes(graph, {"weight": "cost"}, del_old_attr=True) - graph = sparsify_uid(graph, kappa) - if cost == EdgeWeightType.MST: - new_cost = mst_cost(graph, cost_attr="cost") - nx.set_edge_attributes(graph, new_cost, name="cost") - logger.info("Calculating stats for %s", graph_name.value) - dataset_stats.append(get_graph_stats(graph, tsp.depots[0])) - names.append((graph_name.value, gen.value, cost.value, kappa)) - index = pd.MultiIndex.from_tuples(names, names=["graph_name", "generation", "cost_function", "kappa"]) + for graph_name, gen, cost, kappa in itertools.product( + params.TSPLIB_GRAPH_NAME_LIST, + Generation, + params.TSPLIB_COST_FUNCTIONS, + params.TSPLIB_KAPPA_LIST, + ): + logger.info( + "Loading %s on generation %s with cost %s and kappa %s", + graph_name.value, + gen.value, + cost.value, + kappa, + ) + problem_path = build_path_to_oplib_instance( + oplib_root, + gen, + graph_name, + ) + # load the problem from file + problem = ProfitsProblem().load(problem_path) + tsp = BaseTSP.from_tsplib95(problem) + graph = tsp.get_graph() + nx.set_node_attributes(graph, problem.get_node_score(), name="prize") + rename_edge_attributes(graph, {"weight": "cost"}, del_old_attr=True) + graph = sparsify_uid(graph, kappa) + if cost == EdgeWeightType.MST: + new_cost = mst_cost(graph, cost_attr="cost") + nx.set_edge_attributes(graph, new_cost, name="cost") + logger.info("Calculating stats for %s", graph_name.value) + dataset_stats.append(get_graph_stats(graph, tsp.depots[0])) + names.append((graph_name.value, gen.value, cost.value, kappa)) + index = pd.MultiIndex.from_tuples( + names, names=["graph_name", "generation", "cost_function", "kappa"] + ) logger.info("Creating dataframe from dataset stats.") df = pd.DataFrame(dataset_stats, index=index) @@ -99,17 +113,23 @@ def stats_of_dataset( def get_graph_stats(graph: nx.Graph, root_vertex: int) -> Dict[str, float]: - # count the number of edges, vertices, total prize, total cost and the metricness + """Calculate features such as the number of edges, vertices, total prize, + total cost and the metricness. + """ instance_stats = {} instance_stats["num_nodes"] = graph.number_of_nodes() instance_stats["num_edges"] = graph.number_of_edges() - instance_stats["total_cost"] = total_cost(nx.get_edge_attributes(graph, "cost"), list(graph.edges())) - og_prize = total_prize(nx.get_node_attributes(graph, "prize"), list(graph.nodes())) + instance_stats["total_cost"] = total_cost( + nx.get_edge_attributes(graph, "cost"), list(graph.edges()) + ) + og_prize = total_prize(nx.get_node_attributes(graph, "prize"), list(graph.nodes())) instance_stats["total_prize"] = og_prize try: instance_stats["metricness"] = metricness(graph) - except nx.exception.NetworkXException: # FIXME change to NotConnectedException - largest_component_graph = graph.subgraph(max(nx.connected_components(graph), key=len)) + except nx.exception.NetworkXException: # NOTE change to NotConnectedException + largest_component_graph = graph.subgraph( + max(nx.connected_components(graph), key=len) + ) instance_stats["metricness"] = metricness(largest_component_graph) # evaluate the largest prize of any least-cost vertex-disjoint paths @@ -126,14 +146,21 @@ def get_graph_stats(graph: nx.Graph, root_vertex: int) -> Dict[str, float]: biggest_prize = 0 biggest_vertex = None prize_map = nx.get_node_attributes(graph, "prize") - for u, (p1, p2) in vertex_disjoint_paths_map.items(): - prize = total_prize(prize_map, p1) + total_prize(prize_map, p2) - prize_map[u] - prize_map[root_vertex] + for u, (path1, path2) in vertex_disjoint_paths_map.items(): + prize = ( + total_prize(prize_map, path1) + + total_prize(prize_map, path2) + - prize_map[u] + - prize_map[root_vertex] + ) if prize > biggest_prize: biggest_prize = prize biggest_vertex = u instance_stats["biggest_disjoint_prize"] = biggest_prize instance_stats["disjoint_prize_ratio"] = float(biggest_prize) / float(og_prize) - instance_stats["max_disjoint_paths_cost"] = max(vertex_disjoint_cost_map(tree, biggest_vertex).values()) + instance_stats["max_disjoint_paths_cost"] = max( + vertex_disjoint_cost_map(tree, biggest_vertex).values() + ) # preprocessing graph = remove_one_connected_components(graph, root_vertex) @@ -141,7 +168,9 @@ def get_graph_stats(graph: nx.Graph, root_vertex: int) -> Dict[str, float]: # re-evaluate stats after preprocessing instance_stats["preprocessed_num_nodes"] = graph.number_of_nodes() instance_stats["preprocessed_num_edges"] = graph.number_of_edges() - instance_stats["preprocessed_total_cost"] = total_cost(nx.get_edge_attributes(graph, "cost"), list(graph.edges())) + instance_stats["preprocessed_total_cost"] = total_cost( + nx.get_edge_attributes(graph, "cost"), list(graph.edges()) + ) pp_prize = total_prize(nx.get_node_attributes(graph, "prize"), list(graph.nodes())) instance_stats["preprocessed_total_prize"] = pp_prize instance_stats["preprocessed_metricness"] = metricness(graph) diff --git a/pctsp/apps/plot_app.py b/pctsp/apps/plot_app.py index 96d0986..d74b113 100644 --- a/pctsp/apps/plot_app.py +++ b/pctsp/apps/plot_app.py @@ -90,7 +90,12 @@ def plot_heuristics_figure( kappa_df = kappa_df.iloc[ kappa_df.index.get_level_values("cost_function") == cost_function ] - logger.info("Plotting %s points for cost function %s and kappa %s.", len(kappa_df), cost_function.value, kappa) + logger.info( + "Plotting %s points for cost function %s and kappa %s.", + len(kappa_df), + cost_function.value, + kappa, + ) for algorithm in [ ShortAlgorithmName.bfs_extension_collapse, ShortAlgorithmName.bfs_path_extension_collapse, @@ -118,8 +123,14 @@ def plot_heuristics_figure( "x": 1, }, ) - figure_path = figures_dir / f"{DatasetName.tspwplib}_{cost_function}_heuristics.pdf" - logger.info("Writing figure for cost function %s to %s", cost_function.value, figure_path) + figure_path = ( + figures_dir / f"{DatasetName.tspwplib}_{cost_function}_heuristics.pdf" + ) + logger.info( + "Writing figure for cost function %s to %s", + cost_function.value, + figure_path, + ) bottom_fig.write_image(str(figure_path)) diff --git a/pctsp/apps/tables_app.py b/pctsp/apps/tables_app.py index 970c29a..cf1bb09 100644 --- a/pctsp/apps/tables_app.py +++ b/pctsp/apps/tables_app.py @@ -70,11 +70,18 @@ "mean_disjoint_prize_ratio": r"\text{AVG}$(D(G))$", } -SI_GAP = "S[round-mode=places,round-precision=3,scientific-notation=false,table-format=1.3]" +# pylint: disable=line-too-long,too-many-statements +SI_GAP = ( + "S[round-mode=places,round-precision=3,scientific-notation=false,table-format=1.3]" +) SI_OPT = "S[scientific-notation=false]" SI_NUM = "S[table-format=1.2e3]" SI_SEP = "S[round-mode=none,group-separator = {,},group-minimum-digits = 4,scientific-notation=false,table-format=5.0]" +SCIP_STATUS_OPTIMAL = 11 +SCIP_STATUS_INFEASIBLE = 12 +SCIP_BIG_NUMBER = 100000000000000000000.0 + def make_column_name_pretty(name: str) -> str: """Return a pretty name for the column""" @@ -96,7 +103,14 @@ def summarize_dataset( filepath = londonaq_root / "londonaq_dataset.csv" tables_path = tables_dir / "londonaq_dataset.tex" columns = [ - "graph_name","num_nodes","num_edges","preprocessed_num_nodes","preprocessed_num_edges","preprocessed_prize_ratio", "metricness","disjoint_prize_ratio" + "graph_name", + "num_nodes", + "num_edges", + "preprocessed_num_nodes", + "preprocessed_num_edges", + "preprocessed_prize_ratio", + "metricness", + "disjoint_prize_ratio", ] column_format = "l" + 4 * SI_SEP + 3 * SI_GAP elif dataset == DatasetName.tspwplib: @@ -113,10 +127,16 @@ def summarize_dataset( mean_disjoint_prize_ratio=("disjoint_prize_ratio", np.mean), mean_preprocessed_prize_ratio=("preprocessed_prize_ratio", np.mean), ) - df = df.unstack(level="cost_function").swaplevel(i="cost_function", j=0, axis="columns").sort_index(axis="columns") + df = ( + df.unstack(level="cost_function") + .swaplevel(i="cost_function", j=0, axis="columns") + .sort_index(axis="columns") + ) print(df) dataset_logger.info("Writing dataset LaTeX table to %s", tables_path) - df.style.format_index(make_column_name_pretty, axis="columns").format_index(make_column_name_pretty, axis="index").to_latex( + df.style.format_index(make_column_name_pretty, axis="columns").format_index( + make_column_name_pretty, axis="index" + ).to_latex( buf=tables_path, hrules=True, siunitx=True, @@ -268,14 +288,19 @@ def cost_cover_table( ccdf: pd.DataFrame = pd.read_csv(filename) ccdf = ccdf.loc[ccdf["cost_function"] != EdgeWeightType.SEMI_MST] # create new columns - SCIP_STATUS_OPTIMAL = 11 # see https://www.scipopt.org/doc/html/type__stat_8h_source.php - SCIP_STATUS_INFEASIBLE = 12 - SCIP_BIG_NUMBER = 100000000000000000000.0 + # see https://www.scipopt.org/doc/html/type__stat_8h_source.php + def set_to_nan(value: float) -> float: return np.nan if value == SCIP_BIG_NUMBER else value + ccdf["lower_bound"] = ccdf["lower_bound"].apply(set_to_nan) ccdf["upper_bound"] = ccdf["upper_bound"].apply(set_to_nan) - ccdf["gap"] = ccdf.apply(lambda x: np.nan if x["status"] == SCIP_STATUS_INFEASIBLE else (x["upper_bound"] - x["lower_bound"]) / x["lower_bound"], axis="columns") + ccdf["gap"] = ccdf.apply( + lambda x: np.nan + if x["status"] == SCIP_STATUS_INFEASIBLE + else (x["upper_bound"] - x["lower_bound"]) / x["lower_bound"], + axis="columns", + ) ccdf["optimal"] = (ccdf["gap"] == 0) & (ccdf["status"] == SCIP_STATUS_OPTIMAL) def get_cc_name(cc_disjoint_paths: bool, cc_shortest_paths: bool) -> str: @@ -336,10 +361,16 @@ def get_cc_name(cc_disjoint_paths: bool, cc_shortest_paths: bool) -> str: axis="columns", ) if experiment_name == ExperimentName.cc_londonaq_alpha: - df = df.drop(["num_optimal_solutions", "avg_cuts_presolve", "mean_duration"], axis="columns") + df = df.drop( + ["num_optimal_solutions", "avg_cuts_presolve", "mean_duration"], + axis="columns", + ) column_format = "l" + 3 * (SI_GAP + SI_NUM + SI_NUM + "r") elif experiment_name == ExperimentName.cost_cover: - df = df.drop(["num_feasible_solutions", "mean_lower_bound", "mean_upper_bound"], axis="columns") + df = df.drop( + ["num_feasible_solutions", "mean_lower_bound", "mean_upper_bound"], + axis="columns", + ) column_format = "l" + 3 * (SI_NUM + SI_NUM + SI_GAP + "r") df = df.unstack() df = df.swaplevel(0, 1, axis="columns").sort_index(axis=1) @@ -348,7 +379,6 @@ def get_cc_name(cc_disjoint_paths: bool, cc_shortest_paths: bool) -> str: # style table using the siunitx package - if dataset == DatasetName.tspwplib: column_format = "l" + column_format styled_df = df.style.format_index( @@ -362,13 +392,13 @@ def get_cc_name(cc_disjoint_paths: bool, cc_shortest_paths: bool) -> str: ) table_str = table_str.replace("cc_name", "") print(table_str) - # table_tex_filepath.write_text(table_str, encoding="utf-8") + table_tex_filepath.write_text(table_str, encoding="utf-8") def get_heuristics_df( dataset: DatasetName, lab_dir: Path, - exact_experiment_name = ExperimentName.cost_cover, + exact_experiment_name=ExperimentName.cost_cover, heuristic_experiment_name: ExperimentName = ExperimentName.compare_heuristics, ) -> pd.DataFrame: """Get a dataframe with the gap between the heuristic solution @@ -424,14 +454,26 @@ def heuristics_table( ) -> None: """Write a table of heuristic performance to a LaTeX file""" logger = get_pctsp_logger("heuristics-table") - heuristic_df = get_heuristics_df(dataset, lab_dir, exact_experiment_name=exact_experiment_name, heuristic_experiment_name=experiment_name) + heuristic_df = get_heuristics_df( + dataset, + lab_dir, + exact_experiment_name=exact_experiment_name, + heuristic_experiment_name=experiment_name, + ) # NOTE remove SBL-EC heuristic! if sbl_ec_only: - heuristic_df = heuristic_df.loc[heuristic_df["algorithm"] == AlgorithmName.suurballes_extension_collapse] - table_tex_filepath = tables_dir / f"{dataset.value}_{experiment_name.value}_{ShortAlgorithmName.suurballes_extension_collapse.value}.tex" + heuristic_df = heuristic_df.loc[ + heuristic_df["algorithm"] == AlgorithmName.suurballes_extension_collapse + ] + table_tex_filepath = ( + tables_dir + / f"{dataset.value}_{experiment_name.value}_{ShortAlgorithmName.suurballes_extension_collapse.value}.tex" + ) else: - heuristic_df = heuristic_df.loc[heuristic_df["algorithm"] != AlgorithmName.suurballes_extension_collapse] + heuristic_df = heuristic_df.loc[ + heuristic_df["algorithm"] != AlgorithmName.suurballes_extension_collapse + ] table_tex_filepath = tables_dir / f"{dataset.value}_{experiment_name.value}.tex" heuristic_df = heuristic_df[ heuristic_df.index.get_level_values("graph_name") != LondonaqGraphName.laqtinyA @@ -444,7 +486,10 @@ def heuristics_table( params.TSPLIB_COST_FUNCTIONS ) ] - elif dataset == DatasetName.londonaq and experiment_name == ExperimentName.londonaq_alpha: + elif ( + dataset == DatasetName.londonaq + and experiment_name == ExperimentName.londonaq_alpha + ): cols = ["alpha", "algorithm"] elif dataset == DatasetName.londonaq: cols = ["quota", "algorithm"] @@ -468,24 +513,29 @@ def heuristics_table( summary_df = summary_df.sort_index(axis="index") summary_df = summary_df.rename( - lambda x: ShortAlgorithmName[x], - axis="columns", - level="algorithm" + lambda x: ShortAlgorithmName[x], axis="columns", level="algorithm" ).rename(PRETTY_COLUMN_NAMES, axis="columns", level="metric") - summary_df = summary_df.rename({ - EdgeWeightType.EUC_2D: "EUC", - EdgeWeightType.GEO: "GEO", - EdgeWeightType.MST: "MST", - }, axis="index") + summary_df = summary_df.rename( + { + EdgeWeightType.EUC_2D: "EUC", + EdgeWeightType.GEO: "GEO", + EdgeWeightType.MST: "MST", + }, + axis="index", + ) print(summary_df) column_format = "l" if dataset == DatasetName.tspwplib: column_format += "l" - column_format += 4*(SI_NUM + SI_GAP + "r") + column_format += 4 * (SI_NUM + SI_GAP + "r") table_str = summary_df.style.to_latex( - hrules=True, multicol_align="c", multirow_align="naive", siunitx=True, column_format=column_format + hrules=True, + multicol_align="c", + multirow_align="naive", + siunitx=True, + column_format=column_format, ) print(table_str) logger.info("Writing table to LaTeX file: %s", table_tex_filepath) diff --git a/pctsp/compare/dryrun_experiment.py b/pctsp/compare/dryrun_experiment.py index 1cb7cb4..ba8db8b 100644 --- a/pctsp/compare/dryrun_experiment.py +++ b/pctsp/compare/dryrun_experiment.py @@ -51,5 +51,5 @@ def dryrun(dataset_name: DatasetName, dataset_root: Path) -> List[Vial]: raise ValueError( f"{dataset_name} is not a supported dataset for experiment 'dryrun'" ) - preprocessing_list = product_of_preprocessing([True], [True], [True]) + preprocessing_list = product_of_preprocessing([False], [False], [True]) return product_of_vials(data_config_list, model_params_list, preprocessing_list) diff --git a/pctsp/compare/heuristic_experiment.py b/pctsp/compare/heuristic_experiment.py index bf682bb..9cde8fa 100644 --- a/pctsp/compare/heuristic_experiment.py +++ b/pctsp/compare/heuristic_experiment.py @@ -22,6 +22,7 @@ def get_all_heuristic_params() -> List[ModelParams]: + """Get list of heuristic parameters for all heuristics.""" return [ ModelParams( # Extension Collapse algorithm=AlgorithmName.bfs_extension_collapse, @@ -68,6 +69,7 @@ def get_all_heuristic_params() -> List[ModelParams]: ] +# pylint: disable=unused-argument def londonaq_alpha(dataset_name: DatasetName, dataset_root: Path) -> List[Vial]: """Run heuristics on the londonaq dataset with a large alpha""" data_config_list = product_of_londonaq_data_config_from_alpha( diff --git a/pctsp/lab/lab.py b/pctsp/lab/lab.py index 7f42f40..940dae8 100644 --- a/pctsp/lab/lab.py +++ b/pctsp/lab/lab.py @@ -253,14 +253,16 @@ def write_experiment_to_file( filepath = self.get_experiment_dir(experiment.name) / EXPERIMENT_FILENAME self.logger.info("Writing experiment to %s", filepath) with open(filepath, "w", encoding="utf-8") as json_file: - json.dump(json.loads(experiment.json()), json_file, indent=4) + json.dump(json.loads(experiment.model_dump_json()), json_file, indent=4) def write_results_to_file( self, experiment_name: ExperimentName, results: List[Result] ) -> None: """Write a backup of the results incase writing results to mongo fails""" filepath = self.get_experiment_dir(experiment_name) / RESULT_FILENAME - result_list = [json.loads(result_model.json()) for result_model in results] + result_list = [ + json.loads(result_model.model_dump_json()) for result_model in results + ] with open(filepath, "w", encoding="utf-8") as json_file: json.dump(result_list, json_file) diff --git a/pctsp/vial/result.py b/pctsp/vial/result.py index 59acd1a..3b99fe8 100644 --- a/pctsp/vial/result.py +++ b/pctsp/vial/result.py @@ -37,7 +37,7 @@ def write_to_json_file(self, directory: Path) -> Path: filename = str(self.vial_uuid) + ".json" filepath = directory / filename with open(filepath, "w", encoding="utf-8") as json_file: - json.dump(json.loads(self.json()), json_file, indent=4) + json.dump(json.loads(self.model_dump_json()), json_file, indent=4) return filepath @classmethod diff --git a/src/algorithms.cpp b/src/algorithms.cpp index c3a8a28..37dc50f 100644 --- a/src/algorithms.cpp +++ b/src/algorithms.cpp @@ -314,10 +314,13 @@ std::map modelPrizeCollectingTSP( if (heuristic_edges.size() > 0) { auto first = heuristic_edges.begin(); auto last = heuristic_edges.end(); - BOOST_LOG_TRIVIAL(info) << "Adding starting solution with " << heuristic_edges.size() << " edges to solver."; + BOOST_LOG_TRIVIAL(info) << "Adding starting heuristic solution to solver."; SCIP_HEUR* heur = NULL; addHeuristicEdgesToSolver(scip, graph, heur, edge_variable_map, first, last); } + else { + BOOST_LOG_TRIVIAL(info) << "No heuristic solution passed to solver."; + } return edge_variable_map; } diff --git a/src/cost_cover.cpp b/src/cost_cover.cpp index 08af90e..a7998b4 100644 --- a/src/cost_cover.cpp +++ b/src/cost_cover.cpp @@ -11,7 +11,7 @@ SCIP_RETCODE addCoverInequality( // x(S) <= |x(S)| - 1 int nvars = variables.size(); std::vector var_coefs(nvars); - BOOST_LOG_TRIVIAL(debug) << nvars << " variables added to cover inequality."; + BOOST_LOG_TRIVIAL(debug) << std::to_string(nvars) << " variables added to cover inequality."; for (int i = 0; i < nvars; i ++) { var_coefs[i] = 1; } diff --git a/src/logger.cpp b/src/logger.cpp index b5c7a10..49c30d4 100644 --- a/src/logger.cpp +++ b/src/logger.cpp @@ -1,5 +1,11 @@ #include "pctsp/logger.hh" +#include +#include + +namespace src = boost::log::sources; +namespace expr = boost::log::expressions; +namespace keywords = boost::log::keywords; int getBoostLevelFromPyLevel(int py_logging_level) { int boost_level; @@ -30,5 +36,13 @@ int getBoostLevelFromPyLevel(int py_logging_level) { } void PCTSPinitLogging(int level) { + logging::add_common_attributes(); logging::core::get()->set_filter(logging::trivial::severity >= level); + keywords::format = + ( + expr::stream + << expr::format_date_time< boost::posix_time::ptime >("TimeStamp", "%Y-%m-%d %H:%M:%S") + << ": <" << logging::trivial::severity + << "> " << expr::smessage + ); } diff --git a/src/solution.cpp b/src/solution.cpp index a050d2d..47afcab 100644 --- a/src/solution.cpp +++ b/src/solution.cpp @@ -81,7 +81,7 @@ void logSolutionEdges( auto source = boost::source(edge, graph); auto target = boost::target(edge, graph); auto name = SCIPvarGetName(var); - BOOST_LOG_TRIVIAL(debug) << "Edge " << source << "-" << target << " has value " << value; + BOOST_LOG_TRIVIAL(debug) << "Edge " << std::to_string(source) << "-" << std::to_string(target) << " has value " << std::to_string(value); } } } diff --git a/src/subtour_elimination.cpp b/src/subtour_elimination.cpp index 2b98335..d50d156 100644 --- a/src/subtour_elimination.cpp +++ b/src/subtour_elimination.cpp @@ -100,7 +100,7 @@ SCIP_RETCODE addSubtourEliminationConstraint( // the name of the constraint contains every vertex in the set std::string cons_name = "SubtourElimination_" + joinVariableNames(all_vars); - BOOST_LOG_TRIVIAL(debug) << edge_variables.size() << " edge variables and " << vertex_variables.size() << " vertex variables added to new subtour elimination constraint."; + BOOST_LOG_TRIVIAL(debug) << std::to_string(edge_variables.size()) << " edge variables and " << std::to_string(vertex_variables.size()) << " vertex variables added to new subtour elimination constraint."; // create the subtour elimination constraint double lhs = -SCIPinfinity(scip); @@ -173,8 +173,8 @@ SCIP_DECL_CONSCHECK(PCTSPconshdlrSubtour::scip_check) SCIP_VAR* transvars[nvars]; SCIPgetTransformedVars(scip, nvars, SCIPgetVars(scip), transvars); auto nfixed = numFixedOrAggVars(transvars, nvars); - BOOST_LOG_TRIVIAL(debug) << "scip_check: Checking for subtours. " << nfixed << " fixed/agg vars out of " << nvars; - BOOST_LOG_TRIVIAL(debug) << "LP objective value: " << SCIPgetLPObjval(scip) << ". Solution value: " << SCIPgetPrimalbound(scip); + BOOST_LOG_TRIVIAL(debug) << "scip_check: Checking for subtours. " << std::to_string(nfixed) << " fixed/agg vars out of " << std::to_string(nvars); + BOOST_LOG_TRIVIAL(debug) << "LP objective value: " << std::to_string(SCIPgetLPObjval(scip)) << ". Solution value: " << std::to_string(SCIPgetPrimalbound(scip)); if (isSolSimpleCycle(scip, sol, result)) { BOOST_LOG_TRIVIAL(debug) << "Solution is a simple cycle. No subtour violations found."; *result = SCIP_FEASIBLE; @@ -216,7 +216,7 @@ SCIP_DECL_CONSENFOLP(PCTSPconshdlrSubtour::scip_enfolp) { if (isNodeTailingOff(node_rolling_lp_gap[node_id], sec_lp_gap_improvement_threshold, sec_max_tailing_off_iterations) && (SCIPgetLPSolstat(scip) == SCIP_LPSOLSTAT_UNBOUNDEDRAY || SCIPgetLPSolstat(scip) == SCIP_LPSOLSTAT_OPTIMAL)) { // resolve the infeasibility by branching - BOOST_LOG_TRIVIAL(debug)<< "BRANCHING in enfolp: Node " << node_id << " found to be tailing off. Gap is " << gap << ". Threshold is " << sec_lp_gap_improvement_threshold << std::endl; + BOOST_LOG_TRIVIAL(debug)<< "BRANCHING in enfolp: Node " << std::to_string(node_id) << " found to be tailing off. Gap is " << std::to_string(gap) << ". Threshold is " << std::to_string(sec_lp_gap_improvement_threshold) << std::endl; SCIPbranchLP(scip, result); } else { @@ -396,7 +396,7 @@ SCIP_RETCODE PCTSPseparateMaxflowMincut( auto unreachable = getUnreachableVertices(support_graph, support_root, residual_capacity); if (unreachable.size() >= 3) { // do not add SEC for small groups of vertices // the component not containing the root violates the subtour elimination constraint - BOOST_LOG_TRIVIAL(debug) << unreachable.size() << " vertices are unreachable from root of the residual graph."; + BOOST_LOG_TRIVIAL(debug) << std::to_string(unreachable.size()) << " vertices are unreachable from root of the residual graph."; input_vertices = getOldVertices(lookup, unreachable); } else { // unreachable size is less than 3 diff --git a/tests/conftest.py b/tests/conftest.py index 168a36f..02f3303 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -133,7 +133,7 @@ def dataset_root(request) -> Path: def grid8(dataset_root) -> nx.Graph: """Undirected grid graph with 8 vertices""" filepath = dataset_root / "grid8.dot" - G = nx.Graph(nx.drawing.nx_pydot.read_dot(filepath)) + G = nx.Graph(nx.nx_agraph.read_dot(filepath)) G = nx.relabel.convert_node_labels_to_integers(G) for u, v, data in G.edges(data=True): G[u][v]["cost"] = int(data["cost"]) diff --git a/tests/test_algorithms.py b/tests/test_algorithms.py index 8416a67..d7e0de1 100644 --- a/tests/test_algorithms.py +++ b/tests/test_algorithms.py @@ -252,3 +252,28 @@ def test_cycle_cover_grid8(grid8, root, logger_dir): assert summary.num_cycle_cover == 1 assert is_pctsp_yes_instance(grid8, quota, root, ordered_edges) assert model.getStatus() == "optimal" + + +# NOTE below code is useful when running a python debugger +# if __name__ == "__main__": +# from pathlib import Path +# from tspwplib import ( +# build_path_to_oplib_instance, +# ProfitsProblem, +# Generation, +# GraphName, +# ) +# import itertools + +# oplib_root = Path("/Users/patrick/External/OPLib/") +# for graph_name, generation in itertools.product( +# [GraphName.att48, GraphName.eil76, GraphName.st70], +# [Generation.gen1, Generation.gen2, Generation.gen3], +# ): +# print(graph_name, generation) +# filepath = build_path_to_oplib_instance(oplib_root, generation, graph_name) +# problem = ProfitsProblem.load(filepath) +# graph = problem.get_graph(normalize=True) +# test_pctsp_with_heuristic( +# graph, problem.get_root_vertex(), Path(".logs"), 100.0 +# ) diff --git a/tests/test_logger.cpp b/tests/test_logger.cpp index 111c84a..626238c 100644 --- a/tests/test_logger.cpp +++ b/tests/test_logger.cpp @@ -16,4 +16,12 @@ TEST(TestLogger, testBasicLogger) { BOOST_LOG_TRIVIAL(warning) << "A warning severity message"; BOOST_LOG_TRIVIAL(error) << "An error severity message"; BOOST_LOG_TRIVIAL(fatal) << "A fatal severity message"; +} + +TEST(TestLogger, testLoggingSize) { + int py_warning_level = 30; + int level = getBoostLevelFromPyLevel(py_warning_level); + PCTSPinitLogging(level); + std::vector myVector = {1,2,3,4}; + BOOST_LOG_TRIVIAL(warning) << "Size of myVector: " << std::to_string(myVector.size()); } \ No newline at end of file diff --git a/tests/test_params.py b/tests/test_params.py index 030b77e..493a28b 100644 --- a/tests/test_params.py +++ b/tests/test_params.py @@ -6,13 +6,13 @@ ProfitsProblem, BaseTSP, metricness, + GraphName, ) -from pctsp.compare import params def test_tsplib_graphs(oplib_root, generation): """Check the graphs have the desired properties""" - for graph_name in params.TSPLIB_GRAPH_NAME_LIST: + for graph_name in [GraphName.att48, GraphName.st70, GraphName.eil76]: # the graph is metric filepath = build_path_to_oplib_instance(oplib_root, generation, graph_name) problem = ProfitsProblem.load(filepath) From 035ebb488b2735c1a59eef26044d278590e6b91c Mon Sep 17 00:00:00 2001 From: Patrick O'Hara Date: Wed, 27 Mar 2024 17:18:39 +0000 Subject: [PATCH 11/12] Fix dependency on pydot --- tests/conftest.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 02f3303..e793e8d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -130,13 +130,24 @@ def dataset_root(request) -> Path: @pytest.fixture(scope="function") -def grid8(dataset_root) -> nx.Graph: +def grid8() -> nx.Graph: """Undirected grid graph with 8 vertices""" - filepath = dataset_root / "grid8.dot" - G = nx.Graph(nx.nx_agraph.read_dot(filepath)) - G = nx.relabel.convert_node_labels_to_integers(G) - for u, v, data in G.edges(data=True): - G[u][v]["cost"] = int(data["cost"]) + G = nx.Graph() + G.add_weighted_edges_from( + [ + (0, 1, 1), + (0, 2, 1), + (1, 3, 1), + (1, 4, 5), + (2, 3, 1), + (3, 5, 5), + (4, 5, 1), + (4, 6, 1), + (5, 7, 1), + (6, 7, 1), + ], + weight="cost", + ) nx.set_node_attributes(G, 1, name="prize") return G From b0f9886e4c0c1464ca74b0c28adcdeab65f5f5c7 Mon Sep 17 00:00:00 2001 From: Patrick O'Hara Date: Wed, 27 Mar 2024 17:29:46 +0000 Subject: [PATCH 12/12] Upgrade black and reformat --- pctsp/apps/tables_app.py | 8 +++++--- pctsp/vial/result.py | 1 - tests/test_apps.py | 1 - 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pctsp/apps/tables_app.py b/pctsp/apps/tables_app.py index cf1bb09..b7fa05c 100644 --- a/pctsp/apps/tables_app.py +++ b/pctsp/apps/tables_app.py @@ -296,9 +296,11 @@ def set_to_nan(value: float) -> float: ccdf["lower_bound"] = ccdf["lower_bound"].apply(set_to_nan) ccdf["upper_bound"] = ccdf["upper_bound"].apply(set_to_nan) ccdf["gap"] = ccdf.apply( - lambda x: np.nan - if x["status"] == SCIP_STATUS_INFEASIBLE - else (x["upper_bound"] - x["lower_bound"]) / x["lower_bound"], + lambda x: ( + np.nan + if x["status"] == SCIP_STATUS_INFEASIBLE + else (x["upper_bound"] - x["lower_bound"]) / x["lower_bound"] + ), axis="columns", ) ccdf["optimal"] = (ccdf["gap"] == 0) & (ccdf["status"] == SCIP_STATUS_OPTIMAL) diff --git a/pctsp/vial/result.py b/pctsp/vial/result.py index 3b99fe8..d4ba6db 100644 --- a/pctsp/vial/result.py +++ b/pctsp/vial/result.py @@ -10,7 +10,6 @@ # pylint: disable=abstract-method class Result(BaseModel): - """Results of an experiment""" vial_uuid: UUID diff --git a/tests/test_apps.py b/tests/test_apps.py index 5cd2fb1..7567b67 100644 --- a/tests/test_apps.py +++ b/tests/test_apps.py @@ -1,6 +1,5 @@ """Test the commands that make up the tspwp CLI app""" - import logging import pandas as pd from tspwplib import Generation, GraphName