From f255c70a70df3237945f70b02199a5bda7cdbaec Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Thu, 13 Jun 2024 13:25:30 +0100 Subject: [PATCH 01/14] Add readStatistics --- src/pyscipopt/scip.pxi | 79 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 5b9c1b39..6d58a795 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -5087,6 +5087,85 @@ cdef class Model: PY_SCIP_CALL(SCIPprintStatistics(self._scip, cfile)) locale.setlocale(locale.LC_NUMERIC,user_locale) + + def readStatistics(self, filename): + """ + Given a .stats file, reads it and returns a dictionary with some statistics. + + Keyword arguments: + filename -- name of the input file + """ + available_stats = ["Total Time", "solving", "presolving", "reading", "copying", + "Problem name", "Variables", "Constraints", "number of runs", + "nodes", "Root LP Estimate", "Solutions found", "First Solution", + "Primal Bound", "Dual Bound", "Gap", "primal-dual"] + + result = {} + file = open(filename) + data = file.readlines() + seen_cons = 0 + for i, line in enumerate(data): + split_line = line.split(":") + split_line[1] = split_line[1][:-1] # removing \n + stat_name = split_line[0].strip() + + if seen_cons == 2 and stat_name == "Constraints": + continue + + if stat_name in available_stats: + cur_stat = split_line[0].strip() + relevant_value = split_line[1].strip() + + if stat_name == "Variables": + relevant_value = relevant_value[:-1] # removing ")" + var_stats = {} + split_var = relevant_value.split("(") + var_stats["total"] = int(split_var[0]) + split_var = split_var[1].split(",") + + for var_type in split_var: + split_result = var_type.strip().split(" ") + var_stats[split_result[1]] = int(split_result[0]) + + if "Original" in data[i-2]: + result["Variables"] = var_stats + else: + result["Presolved Variables"] = var_stats + + continue + + if stat_name == "Constraints": + seen_cons += 1 + con_stats = {} + split_con = relevant_value.split(",") + for con_type in split_con: + split_result = con_type.strip().split(" ") + con_stats[split_result[1]] = int(split_result[0]) + + if "Original" in data[i-3]: + result["Constraints"] = con_stats + else: + result["Presolved Constraints"] = con_stats + continue + + relevant_value = relevant_value.split(" ")[0] + if stat_name == "Problem name": + if "Original" in data[i-1]: + result["Problem name"] = relevant_value + else: + result["Presolved Problem name"] = relevant_value + continue + + if stat_name == "Gap": + result["Gap (%)"] = float(relevant_value[:-1]) + continue + + if _is_number(relevant_value): + result[cur_stat] = float(relevant_value) + else: # it's a string + result[cur_stat] = relevant_value + + return result def getNLPs(self): """gets total number of LPs solved so far""" From 0252cb190393ce9c4729fee8ad17aa55fc9c514c Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Thu, 13 Jun 2024 13:25:37 +0100 Subject: [PATCH 02/14] Add test for readStatistics --- tests/test_reader.py | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/tests/test_reader.py b/tests/test_reader.py index 2f471227..dabae6c8 100644 --- a/tests/test_reader.py +++ b/tests/test_reader.py @@ -78,4 +78,32 @@ def test_sudoku_reader(): input = f.readline() assert input == "sudoku" - deleteFile("model.sod") \ No newline at end of file + deleteFile("model.sod") + +def test_readStatistics(): + m = Model(problemName="readStats") + x = m.addVar(vtype="I") + y = m.addVar() + + m.addCons(x+y <= 3) + m.writeStatistics(os.path.join("tests", "data", "readStatistics.stats")) + + m2 = Model() + result = m2.readStatistics(os.path.join("tests", "data", "readStatistics.stats")) + + assert result["Variables"]["total"] == 2 + assert result["Variables"]["integer"] == 1 + + m.optimize() + m.writeStatistics(os.path.join("tests", "data", "readStatistics.stats")) + result = m2.readStatistics(os.path.join("tests", "data", "readStatistics.stats")) + + assert type(result["Total Time"]) == float + assert result["Problem name"] == "readStats" + assert result["Presolved Problem name"] == "t_readStats" + assert type(result["primal-dual"]) == float + assert result["Solutions found"] == 1 + assert type(result["Gap (%)"]) == float + assert result["Presolved Constraints"] == {"initial": 1, "maximal": 1} + assert result["Variables"] == {"total": 2, "binary": 0, "integer": 1, "implicit": 0, "continuous": 1} + assert result["Presolved Variables"] == {"total": 0, "binary": 0, "integer": 0, "implicit": 0, "continuous": 0} \ No newline at end of file From ef5d30c4e00059c6798b35277f4c4ab4df5153e5 Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Thu, 13 Jun 2024 13:26:07 +0100 Subject: [PATCH 03/14] Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a1508233..54e5f40c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased ### Added +- Added parser to read .stats file - Added recipe with reformulation for detecting infeasible constraints - Wrapped SCIPcreateOrigSol and added tests - Added verbose option for writeProblem and writeParams From 19430ce7f5600898b316425ba8dc0b184bac45c3 Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Tue, 18 Jun 2024 11:08:04 +0100 Subject: [PATCH 04/14] Organize statistics in class --- src/pyscipopt/scip.pxi | 84 +++++++++++++++++++++++++++++++++++++++--- tests/test_reader.py | 30 +++++++-------- 2 files changed, 91 insertions(+), 23 deletions(-) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 6d58a795..1ef271e2 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -17,6 +17,7 @@ from posix.stdio cimport fileno from collections.abc import Iterable from itertools import repeat +from dataclasses import dataclass include "expr.pxi" include "lp.pxi" @@ -5095,14 +5096,16 @@ cdef class Model: Keyword arguments: filename -- name of the input file """ - available_stats = ["Total Time", "solving", "presolving", "reading", "copying", - "Problem name", "Variables", "Constraints", "number of runs", - "nodes", "Root LP Estimate", "Solutions found", "First Solution", - "Primal Bound", "Dual Bound", "Gap", "primal-dual"] - result = {} file = open(filename) data = file.readlines() + + assert "problem is solved" in data[0], "readStatistics can only be called if the problem was solved" + available_stats = ["Total Time", "solving", "presolving", "reading", "copying", + "Problem name", "Variables", "Constraints", "number of runs", + "nodes", "Solutions found", "First Solution", "Primal Bound", + "Dual Bound", "Gap", "primal-dual"] + seen_cons = 0 for i, line in enumerate(data): split_line = line.split(":") @@ -5165,7 +5168,16 @@ cdef class Model: else: # it's a string result[cur_stat] = relevant_value - return result + treated_keys = {"Total Time": "total_time", "solving":"solving_time", "presolving":"presolving_time", "reading":"reading_time", "copying":"copying_time", + "Problem name": "problem_name", "Presolved Problem name": "presolved_problem_name", "Variables":"_variables", + "Presolved Variables":"_presolved_variables", "Constraints": "_constraints", "Presolved Constraints":"_presolved_constraints", + "number of runs": "n_runs", "nodes":"n_nodes", "Solutions found": "solutions_found", "First Solution": "first_solution", + "Primal Bound":"primal_bound", "Dual Bound":"dual_bound", "Gap (%)":"gap", "primal-dual":"primal_dual_integral"} + treated_result = dict((treated_keys[key], value) for (key, value) in result.items()) + stats = Statistics(**treated_result) + stats._populate_remaining() + + return stats def getNLPs(self): """gets total number of LPs solved so far""" @@ -5509,6 +5521,66 @@ cdef class Model: """Get an estimation of the final tree size """ return SCIPgetTreesizeEstimation(self._scip) +@dataclass +class Statistics: + # Total time since model was created + total_time: float + # Time spent solving the problem + solving_time: float + # Time spent on presolving + presolving_time: float + # Time spent on reading + reading_time: float + # Time spent on copying + copying_time: float + # Name of problem + problem_name: str + # Name of presolved problem + presolved_problem_name: str + # Dictionary with number of variables by type + _variables: dict + # Dictionary with number of presolved variables by type + _presolved_variables: dict + # Dictionary with number of constraints by type + _constraints: dict + # Dictionary with number of presolved constraints by type + _presolved_constraints: dict + # The number of restarts it took to solve the problem (TODO: check this) + n_runs: int + # The number of nodes explored in the branch-and-bound tree + n_nodes: int + # number of found solutions + solutions_found: int + # objective value of first found solution + first_solution: float + # The best primal bound found + primal_bound: float + # The best dual bound found + dual_bound: float + # The gap between the primal and dual bounds + gap: float + # The primal-dual integral + primal_dual_integral: float + + def _populate_remaining(self): + self.n_vars: int = self._variables["total"] + self.n_binary_vars: int = self._variables["binary"] + self.n_implicit_integer_vars: int = self._variables["implicit"] + self.n_continuous_vars: int = self._variables["continuous"] + + self.n_presolved_vars: int = self._presolved_variables["total"] + self.n_presolved_binary_vars: int = self._presolved_variables["binary"] + self.n_presolved_implicit_integer_vars: int = self._presolved_variables["implicit"] + self.n_presolved_continuous_vars: int = self._presolved_variables["continuous"] + + self.n_conss: int = self._constraints["initial"] + self.n_maximal_cons: int = self._constraints["maximal"] + + self.n_presolved_conss: int = self._presolved_constraints["initial"] + self.n_presolved_maximal_cons: int = self._presolved_constraints["maximal"] + + + # debugging memory management def is_memory_freed(): return BMSgetMemoryUsed() == 0 diff --git a/tests/test_reader.py b/tests/test_reader.py index dabae6c8..71a04066 100644 --- a/tests/test_reader.py +++ b/tests/test_reader.py @@ -87,23 +87,19 @@ def test_readStatistics(): m.addCons(x+y <= 3) m.writeStatistics(os.path.join("tests", "data", "readStatistics.stats")) - - m2 = Model() - result = m2.readStatistics(os.path.join("tests", "data", "readStatistics.stats")) - - assert result["Variables"]["total"] == 2 - assert result["Variables"]["integer"] == 1 + m.hideOutput() m.optimize() m.writeStatistics(os.path.join("tests", "data", "readStatistics.stats")) - result = m2.readStatistics(os.path.join("tests", "data", "readStatistics.stats")) - - assert type(result["Total Time"]) == float - assert result["Problem name"] == "readStats" - assert result["Presolved Problem name"] == "t_readStats" - assert type(result["primal-dual"]) == float - assert result["Solutions found"] == 1 - assert type(result["Gap (%)"]) == float - assert result["Presolved Constraints"] == {"initial": 1, "maximal": 1} - assert result["Variables"] == {"total": 2, "binary": 0, "integer": 1, "implicit": 0, "continuous": 1} - assert result["Presolved Variables"] == {"total": 0, "binary": 0, "integer": 0, "implicit": 0, "continuous": 0} \ No newline at end of file + result = m.readStatistics(os.path.join("tests", "data", "readStatistics.stats")) + + assert len([k for k, val in result.__dict__.items() if not str(hex(id(val))) in str(val)]) == 31 # number of attributes. See https://stackoverflow.com/a/57431390/9700522 + assert type(result.total_time) == float + assert result.problem_name == "readStats" + assert result.presolved_problem_name == "t_readStats" + assert type(result.primal_dual_integral) == float + assert result.solutions_found == 1 + assert type(result.gap) == float + assert result._presolved_constraints == {"initial": 1, "maximal": 1} + assert result._variables == {"total": 2, "binary": 0, "integer": 1, "implicit": 0, "continuous": 1} + assert result._presolved_variables == {"total": 0, "binary": 0, "integer": 0, "implicit": 0, "continuous": 0} \ No newline at end of file From 2e8dd6707038de39a17d47b308cee42d13221dc8 Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Tue, 18 Jun 2024 11:14:51 +0100 Subject: [PATCH 05/14] Add some comments --- src/pyscipopt/scip.pxi | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 4ef04f20..65cca80b 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -5572,20 +5572,32 @@ class Statistics: primal_dual_integral: float def _populate_remaining(self): + # number of variables in the model self.n_vars: int = self._variables["total"] + # number of binary variables in the model self.n_binary_vars: int = self._variables["binary"] + # number of implicit integer variables in the model self.n_implicit_integer_vars: int = self._variables["implicit"] + # number of continuous variables in the model self.n_continuous_vars: int = self._variables["continuous"] + # number of variables in the presolved model self.n_presolved_vars: int = self._presolved_variables["total"] + # number of binary variables in the presolved model self.n_presolved_binary_vars: int = self._presolved_variables["binary"] + # number of implicit integer variables in the presolved model self.n_presolved_implicit_integer_vars: int = self._presolved_variables["implicit"] + # number of continuous variables in the presolved model self.n_presolved_continuous_vars: int = self._presolved_variables["continuous"] + # number of initial constraints in the model self.n_conss: int = self._constraints["initial"] + # number of maximal constraints in the model self.n_maximal_cons: int = self._constraints["maximal"] + # number of initial constraints in the presolved model self.n_presolved_conss: int = self._presolved_constraints["initial"] + # number of maximal constraints in the presolved model self.n_presolved_maximal_cons: int = self._presolved_constraints["maximal"] From 14a39a927dea089a13a4b20d375eb793636c03d6 Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Tue, 18 Jun 2024 11:30:15 +0100 Subject: [PATCH 06/14] Some more comments --- src/pyscipopt/scip.pxi | 11 ++++++----- tests/test_reader.py | 2 -- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 65cca80b..bf8af3ec 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -5100,7 +5100,8 @@ cdef class Model: def readStatistics(self, filename): """ - Given a .stats file, reads it and returns a dictionary with some statistics. + Given a .stats file of a solved model, reads it and returns an instance of the Statistics class + holding some statistics. Keyword arguments: filename -- name of the input file @@ -5121,7 +5122,7 @@ cdef class Model: split_line[1] = split_line[1][:-1] # removing \n stat_name = split_line[0].strip() - if seen_cons == 2 and stat_name == "Constraints": + if seen_cons == 2 and stat_name == "Constraints": continue if stat_name in available_stats: @@ -5177,14 +5178,16 @@ cdef class Model: else: # it's a string result[cur_stat] = relevant_value + # changing keys to pythonic variable names treated_keys = {"Total Time": "total_time", "solving":"solving_time", "presolving":"presolving_time", "reading":"reading_time", "copying":"copying_time", "Problem name": "problem_name", "Presolved Problem name": "presolved_problem_name", "Variables":"_variables", "Presolved Variables":"_presolved_variables", "Constraints": "_constraints", "Presolved Constraints":"_presolved_constraints", "number of runs": "n_runs", "nodes":"n_nodes", "Solutions found": "solutions_found", "First Solution": "first_solution", "Primal Bound":"primal_bound", "Dual Bound":"dual_bound", "Gap (%)":"gap", "primal-dual":"primal_dual_integral"} treated_result = dict((treated_keys[key], value) for (key, value) in result.items()) + stats = Statistics(**treated_result) - stats._populate_remaining() + stats._populate_remaining() # retrieve different variable/constraint types from the variable/constraint dictionary return stats @@ -5600,8 +5603,6 @@ class Statistics: # number of maximal constraints in the presolved model self.n_presolved_maximal_cons: int = self._presolved_constraints["maximal"] - - # debugging memory management def is_memory_freed(): return BMSgetMemoryUsed() == 0 diff --git a/tests/test_reader.py b/tests/test_reader.py index 71a04066..ff1af5c7 100644 --- a/tests/test_reader.py +++ b/tests/test_reader.py @@ -86,8 +86,6 @@ def test_readStatistics(): y = m.addVar() m.addCons(x+y <= 3) - m.writeStatistics(os.path.join("tests", "data", "readStatistics.stats")) - m.hideOutput() m.optimize() m.writeStatistics(os.path.join("tests", "data", "readStatistics.stats")) From 044d961d3f653f34d7bdc4f8a3d6c9403e0b1e03 Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Fri, 21 Jun 2024 14:09:33 +0100 Subject: [PATCH 07/14] Update Statistics class and documentation --- src/pyscipopt/scip.pxi | 181 +++++++++++++++++++++++++++++------------ tests/test_reader.py | 10 ++- 2 files changed, 136 insertions(+), 55 deletions(-) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index bf8af3ec..56a1c01e 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -5182,13 +5182,11 @@ cdef class Model: treated_keys = {"Total Time": "total_time", "solving":"solving_time", "presolving":"presolving_time", "reading":"reading_time", "copying":"copying_time", "Problem name": "problem_name", "Presolved Problem name": "presolved_problem_name", "Variables":"_variables", "Presolved Variables":"_presolved_variables", "Constraints": "_constraints", "Presolved Constraints":"_presolved_constraints", - "number of runs": "n_runs", "nodes":"n_nodes", "Solutions found": "solutions_found", "First Solution": "first_solution", + "number of runs": "n_runs", "nodes":"n_nodes", "Solutions found": "n_solutions_found", "First Solution": "first_solution", "Primal Bound":"primal_bound", "Dual Bound":"dual_bound", "Gap (%)":"gap", "primal-dual":"primal_dual_integral"} treated_result = dict((treated_keys[key], value) for (key, value) in result.items()) stats = Statistics(**treated_result) - stats._populate_remaining() # retrieve different variable/constraint types from the variable/constraint dictionary - return stats def getNLPs(self): @@ -5535,74 +5533,153 @@ cdef class Model: @dataclass class Statistics: - # Total time since model was created + """ + Attributes + ---------- + total_time : float + Total time since model was created + solving_time: float + Time spent solving the problem + presolving_time: float + Time spent on presolving + reading_time: float + Time spent on reading + copying_time: float + Time spent on copying + problem_name: str + Name of problem + presolved_problem_name: str + Name of presolved problem + _variables: dict + Dictionary with number of variables by type + _presolved_variables: dict + Dictionary with number of presolved variables by type + _constraints: dict + Dictionary with number of constraints by type + _presolved_constraints: dict + Dictionary with number of presolved constraints by type + n_nodes: int + The number of nodes explored in the branch-and-bound tree + n_solutions_found: int + number of found solutions + first_solution: float + objective value of first found solution + primal_bound: float + The best primal bound found + dual_bound: float + The best dual bound found + gap: float + The gap between the primal and dual bounds + primal_dual_integral: float + The primal-dual integral + n_vars: int + number of variables in the model + n_binary_vars: int + number of binary variables in the model + n_integer_vars: int + number of integer variables in the model + n_implicit_integer_vars: int + number of implicit integer variables in the model + n_continuous_vars: int + number of continuous variables in the model + n_presolved_vars: int + number of variables in the presolved model + n_presolved_continuous_vars: int + number of continuous variables in the presolved model + n_presolved_binary_vars: int + number of binary variables in the presolved model + n_presolved_integer_vars: int + number of integer variables in the presolved model + n_presolved_implicit_integer_vars: int + number of implicit integer variables in the presolved model + n_maximal_cons: int + number of maximal constraints in the model + n_initial_cons: int + number of initial constraints in the presolved model + n_presolved_maximal_cons + number of maximal constraints in the presolved model + n_presolved_conss + number of initial constraints in the model + """ + total_time: float - # Time spent solving the problem solving_time: float - # Time spent on presolving presolving_time: float - # Time spent on reading reading_time: float - # Time spent on copying copying_time: float - # Name of problem problem_name: str - # Name of presolved problem presolved_problem_name: str - # Dictionary with number of variables by type _variables: dict - # Dictionary with number of presolved variables by type _presolved_variables: dict - # Dictionary with number of constraints by type _constraints: dict - # Dictionary with number of presolved constraints by type _presolved_constraints: dict - # The number of restarts it took to solve the problem (TODO: check this) n_runs: int - # The number of nodes explored in the branch-and-bound tree n_nodes: int - # number of found solutions - solutions_found: int - # objective value of first found solution + n_solutions_found: int first_solution: float - # The best primal bound found primal_bound: float - # The best dual bound found dual_bound: float - # The gap between the primal and dual bounds gap: float - # The primal-dual integral primal_dual_integral: float - def _populate_remaining(self): - # number of variables in the model - self.n_vars: int = self._variables["total"] - # number of binary variables in the model - self.n_binary_vars: int = self._variables["binary"] - # number of implicit integer variables in the model - self.n_implicit_integer_vars: int = self._variables["implicit"] - # number of continuous variables in the model - self.n_continuous_vars: int = self._variables["continuous"] - - # number of variables in the presolved model - self.n_presolved_vars: int = self._presolved_variables["total"] - # number of binary variables in the presolved model - self.n_presolved_binary_vars: int = self._presolved_variables["binary"] - # number of implicit integer variables in the presolved model - self.n_presolved_implicit_integer_vars: int = self._presolved_variables["implicit"] - # number of continuous variables in the presolved model - self.n_presolved_continuous_vars: int = self._presolved_variables["continuous"] - - # number of initial constraints in the model - self.n_conss: int = self._constraints["initial"] - # number of maximal constraints in the model - self.n_maximal_cons: int = self._constraints["maximal"] - - # number of initial constraints in the presolved model - self.n_presolved_conss: int = self._presolved_constraints["initial"] - # number of maximal constraints in the presolved model - self.n_presolved_maximal_cons: int = self._presolved_constraints["maximal"] - + # unpacking the _variables, _presolved_variables, _constraints + # _presolved_constraints dictionaries + @property + def n_vars(self): + return self._variables["total"] + + @property + def n_binary_vars(self): + return self._variables["binary"] + + @property + def n_integer_vars(self): + return self._variables["integer"] + + @property + def n_implicit_integer_vars(self): + return self._variables["implicit"] + + @property + def n_continuous_vars(self): + return self._variables["continuous"] + + @property + def n_presolved_vars(self): + return self._presolved_variables["total"] + + @property + def n_presolved_binary_vars(self): + return self._presolved_variables["binary"] + + @property + def n_presolved_integer_vars(self): + return self._presolved_variables["integer"] + + @property + def n_presolved_implicit_integer_vars(self): + return self._presolved_variables["implicit"] + + @property + def n_presolved_continuous_vars(self): + return self._presolved_variables["continuous"] + + @property + def n_conss(self): + return self._constraints["initial"] + + @property + def n_maximal_cons(self): + return self._constraints["maximal"] + + @property + def n_presolved_conss(self): + return self._presolved_constraints["initial"] + + @property + def n_presolved_maximal_cons(self): + return self._presolved_constraints["maximal"] + # debugging memory management def is_memory_freed(): return BMSgetMemoryUsed() == 0 diff --git a/tests/test_reader.py b/tests/test_reader.py index ff1af5c7..4bc976c7 100644 --- a/tests/test_reader.py +++ b/tests/test_reader.py @@ -91,13 +91,17 @@ def test_readStatistics(): m.writeStatistics(os.path.join("tests", "data", "readStatistics.stats")) result = m.readStatistics(os.path.join("tests", "data", "readStatistics.stats")) - assert len([k for k, val in result.__dict__.items() if not str(hex(id(val))) in str(val)]) == 31 # number of attributes. See https://stackoverflow.com/a/57431390/9700522 + assert len([k for k, val in result.__dict__.items() if not str(hex(id(val))) in str(val)]) == 19 # number of attributes. See https://stackoverflow.com/a/57431390/9700522 assert type(result.total_time) == float assert result.problem_name == "readStats" assert result.presolved_problem_name == "t_readStats" assert type(result.primal_dual_integral) == float - assert result.solutions_found == 1 + assert result.n_solutions_found == 1 assert type(result.gap) == float assert result._presolved_constraints == {"initial": 1, "maximal": 1} assert result._variables == {"total": 2, "binary": 0, "integer": 1, "implicit": 0, "continuous": 1} - assert result._presolved_variables == {"total": 0, "binary": 0, "integer": 0, "implicit": 0, "continuous": 0} \ No newline at end of file + assert result._presolved_variables == {"total": 0, "binary": 0, "integer": 0, "implicit": 0, "continuous": 0} + assert result.n_vars == 2 + assert result.n_presolved_vars == 0 + assert result.n_binary_vars == 0 + assert result.n_integer_vars == 1 \ No newline at end of file From ab94980037fe09ce6630e0ce92ee2c52de767992 Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Fri, 21 Jun 2024 14:09:39 +0100 Subject: [PATCH 08/14] Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a3105f5..6aafd04e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased ### Added +- Created Statistics class - Added parser to read .stats file - Added SCIPprintExternalCodes (retrieves version of linked symmetry, lp solver, nl solver etc) - Added recipe with reformulation for detecting infeasible constraints From 9996848eb0f2e22da592f956eaf2b00169233bd0 Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Sat, 22 Jun 2024 13:42:04 +0100 Subject: [PATCH 09/14] Update documentation --- src/pyscipopt/scip.pxi | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 56a1c01e..b65c4ee0 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -5549,15 +5549,7 @@ class Statistics: problem_name: str Name of problem presolved_problem_name: str - Name of presolved problem - _variables: dict - Dictionary with number of variables by type - _presolved_variables: dict - Dictionary with number of presolved variables by type - _constraints: dict - Dictionary with number of constraints by type - _presolved_constraints: dict - Dictionary with number of presolved constraints by type + Name of presolved problem n_nodes: int The number of nodes explored in the branch-and-bound tree n_solutions_found: int @@ -5596,9 +5588,9 @@ class Statistics: number of maximal constraints in the model n_initial_cons: int number of initial constraints in the presolved model - n_presolved_maximal_cons + n_presolved_maximal_cons: int number of maximal constraints in the presolved model - n_presolved_conss + n_presolved_conss: int number of initial constraints in the model """ @@ -5609,10 +5601,10 @@ class Statistics: copying_time: float problem_name: str presolved_problem_name: str - _variables: dict - _presolved_variables: dict - _constraints: dict - _presolved_constraints: dict + _variables: dict # Dictionary with number of variables by type + _presolved_variables: dict # Dictionary with number of presolved variables by type + _constraints: dict # Dictionary with number of constraints by type + _presolved_constraints: dict # Dictionary with number of presolved constraints by type n_runs: int n_nodes: int n_solutions_found: int From 24fcb2a33785bbf35bf0ae92d1e5bf06f65e69a3 Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Mon, 24 Jun 2024 13:10:28 +0100 Subject: [PATCH 10/14] Expand readStats to unbounded, infeasible, user-interrupted prorblems --- src/pyscipopt/scip.pxi | 56 ++++++++++++++++++++++++++++++------------ 1 file changed, 40 insertions(+), 16 deletions(-) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 943e27f9..146e2bec 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -5116,11 +5116,23 @@ cdef class Model: file = open(filename) data = file.readlines() - assert "problem is solved" in data[0], "readStatistics can only be called if the problem was solved" + if "optimal solution found" in data[0]: + result["status"] = "optimal" + elif "infeasible" in data[0]: + result["status"] = "infeasible" + elif "unbounded" in data[0]: + result["status"] = "unbounded" + elif "limit reached" in data[0]: + result["status"] = "user_interrupt" + else: + raise "readStatistics can only be called if the problem was solved" + available_stats = ["Total Time", "solving", "presolving", "reading", "copying", - "Problem name", "Variables", "Constraints", "number of runs", - "nodes", "Solutions found", "First Solution", "Primal Bound", - "Dual Bound", "Gap", "primal-dual"] + "Problem name", "Variables", "Constraints", "number of runs", + "nodes", "Solutions found"] + + if result["status"] in ["optimal", "user_interrupt"]: + available_stats.extend(["First Solution", "Primal Bound", "Dual Bound", "Gap", "primal-dual"]) seen_cons = 0 for i, line in enumerate(data): @@ -5181,15 +5193,24 @@ cdef class Model: if _is_number(relevant_value): result[cur_stat] = float(relevant_value) + if cur_stat == "Solutions found" and result[cur_stat] == 0: + break else: # it's a string result[cur_stat] = relevant_value # changing keys to pythonic variable names - treated_keys = {"Total Time": "total_time", "solving":"solving_time", "presolving":"presolving_time", "reading":"reading_time", "copying":"copying_time", - "Problem name": "problem_name", "Presolved Problem name": "presolved_problem_name", "Variables":"_variables", + treated_keys = {"status": "status", "Total Time": "total_time", "solving":"solving_time", "presolving":"presolving_time", "reading":"reading_time", + "copying":"copying_time", "Problem name": "problem_name", "Presolved Problem name": "presolved_problem_name", "Variables":"_variables", "Presolved Variables":"_presolved_variables", "Constraints": "_constraints", "Presolved Constraints":"_presolved_constraints", - "number of runs": "n_runs", "nodes":"n_nodes", "Solutions found": "n_solutions_found", "First Solution": "first_solution", - "Primal Bound":"primal_bound", "Dual Bound":"dual_bound", "Gap (%)":"gap", "primal-dual":"primal_dual_integral"} + "number of runs": "n_runs", "nodes":"n_nodes", "Solutions found": "n_solutions_found"} + + if result["status"] in ["optimal", "user_interrupt"]: + if result["Solutions found"] > 0: + treated_keys["First Solution"] = "first_solution" + treated_keys["Primal Bound"] = "primal_bound" + treated_keys["Dual Bound"] = "dual_bound" + treated_keys["Gap (%)"] = "gap" + treated_keys["primal-dual"] = "primal_dual_integral" treated_result = dict((treated_keys[key], value) for (key, value) in result.items()) stats = Statistics(**treated_result) @@ -5542,6 +5563,8 @@ class Statistics: """ Attributes ---------- + status: str + Status of the problem (optimal solution found, infeasible, etc.) total_time : float Total time since model was created solving_time: float @@ -5600,6 +5623,7 @@ class Statistics: number of initial constraints in the model """ + status: str total_time: float solving_time: float presolving_time: float @@ -5611,14 +5635,14 @@ class Statistics: _presolved_variables: dict # Dictionary with number of presolved variables by type _constraints: dict # Dictionary with number of constraints by type _presolved_constraints: dict # Dictionary with number of presolved constraints by type - n_runs: int - n_nodes: int - n_solutions_found: int - first_solution: float - primal_bound: float - dual_bound: float - gap: float - primal_dual_integral: float + n_runs: int = None + n_nodes: int = None + n_solutions_found: int = -1 + first_solution: float = None + primal_bound: float = None + dual_bound: float = None + gap: float = None + primal_dual_integral: float = None # unpacking the _variables, _presolved_variables, _constraints # _presolved_constraints dictionaries From 2c1f0c9d522d00fa431002b4aa85473e4722aaa4 Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Mon, 24 Jun 2024 13:10:34 +0100 Subject: [PATCH 11/14] Expand testing --- tests/test_reader.py | 37 +++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/tests/test_reader.py b/tests/test_reader.py index 4bc976c7..69001dae 100644 --- a/tests/test_reader.py +++ b/tests/test_reader.py @@ -91,7 +91,8 @@ def test_readStatistics(): m.writeStatistics(os.path.join("tests", "data", "readStatistics.stats")) result = m.readStatistics(os.path.join("tests", "data", "readStatistics.stats")) - assert len([k for k, val in result.__dict__.items() if not str(hex(id(val))) in str(val)]) == 19 # number of attributes. See https://stackoverflow.com/a/57431390/9700522 + assert result.status == "optimal" + assert len([k for k, val in result.__dict__.items() if not str(hex(id(val))) in str(val)]) == 20 # number of attributes. See https://stackoverflow.com/a/57431390/9700522 assert type(result.total_time) == float assert result.problem_name == "readStats" assert result.presolved_problem_name == "t_readStats" @@ -104,4 +105,36 @@ def test_readStatistics(): assert result.n_vars == 2 assert result.n_presolved_vars == 0 assert result.n_binary_vars == 0 - assert result.n_integer_vars == 1 \ No newline at end of file + assert result.n_integer_vars == 1 + + m = Model() + x = m.addVar() + m.setObjective(-x) + m.hideOutput() + m.optimize() + m.writeStatistics(os.path.join("tests", "data", "readStatistics.stats")) + result = m.readStatistics(os.path.join("tests", "data", "readStatistics.stats")) + assert result.status == "unbounded" + + m = Model() + x = m.addVar() + m.addCons(x <= -1) + m.hideOutput() + m.optimize() + m.writeStatistics(os.path.join("tests", "data", "readStatistics.stats")) + result = m.readStatistics(os.path.join("tests", "data", "readStatistics.stats")) + assert result.status == "infeasible" + assert result.gap == None + assert result.n_solutions_found == 0 + + m = Model() + x = m.addVar() + m.hideOutput() + m.setParam("limits/solutions", 0) + m.optimize() + m.writeStatistics(os.path.join("tests", "data", "readStatistics.stats")) + result = m.readStatistics(os.path.join("tests", "data", "readStatistics.stats")) + assert result.status == "user_interrupt" + assert result.gap == None + +test_readStatistics() \ No newline at end of file From 6140996e347eb7c63296320ecaf61607adcd50dd Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Mon, 24 Jun 2024 13:11:01 +0100 Subject: [PATCH 12/14] Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 43cb8718..6dc7e053 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased ### Added +- Expanded Statistics class to more problems - Created Statistics class - Added parser to read .stats file ### Fixed From 1ce2152a126463960bb7591b28c2004522d497b8 Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Tue, 25 Jun 2024 16:05:26 +0100 Subject: [PATCH 13/14] Make readStatistics standalone --- src/pyscipopt/scip.pxi | 223 ++++++++++++++++++++--------------------- tests/test_reader.py | 14 ++- 2 files changed, 117 insertions(+), 120 deletions(-) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 146e2bec..b385bf2c 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -5103,118 +5103,6 @@ cdef class Model: PY_SCIP_CALL(SCIPprintStatistics(self._scip, cfile)) locale.setlocale(locale.LC_NUMERIC,user_locale) - - def readStatistics(self, filename): - """ - Given a .stats file of a solved model, reads it and returns an instance of the Statistics class - holding some statistics. - - Keyword arguments: - filename -- name of the input file - """ - result = {} - file = open(filename) - data = file.readlines() - - if "optimal solution found" in data[0]: - result["status"] = "optimal" - elif "infeasible" in data[0]: - result["status"] = "infeasible" - elif "unbounded" in data[0]: - result["status"] = "unbounded" - elif "limit reached" in data[0]: - result["status"] = "user_interrupt" - else: - raise "readStatistics can only be called if the problem was solved" - - available_stats = ["Total Time", "solving", "presolving", "reading", "copying", - "Problem name", "Variables", "Constraints", "number of runs", - "nodes", "Solutions found"] - - if result["status"] in ["optimal", "user_interrupt"]: - available_stats.extend(["First Solution", "Primal Bound", "Dual Bound", "Gap", "primal-dual"]) - - seen_cons = 0 - for i, line in enumerate(data): - split_line = line.split(":") - split_line[1] = split_line[1][:-1] # removing \n - stat_name = split_line[0].strip() - - if seen_cons == 2 and stat_name == "Constraints": - continue - - if stat_name in available_stats: - cur_stat = split_line[0].strip() - relevant_value = split_line[1].strip() - - if stat_name == "Variables": - relevant_value = relevant_value[:-1] # removing ")" - var_stats = {} - split_var = relevant_value.split("(") - var_stats["total"] = int(split_var[0]) - split_var = split_var[1].split(",") - - for var_type in split_var: - split_result = var_type.strip().split(" ") - var_stats[split_result[1]] = int(split_result[0]) - - if "Original" in data[i-2]: - result["Variables"] = var_stats - else: - result["Presolved Variables"] = var_stats - - continue - - if stat_name == "Constraints": - seen_cons += 1 - con_stats = {} - split_con = relevant_value.split(",") - for con_type in split_con: - split_result = con_type.strip().split(" ") - con_stats[split_result[1]] = int(split_result[0]) - - if "Original" in data[i-3]: - result["Constraints"] = con_stats - else: - result["Presolved Constraints"] = con_stats - continue - - relevant_value = relevant_value.split(" ")[0] - if stat_name == "Problem name": - if "Original" in data[i-1]: - result["Problem name"] = relevant_value - else: - result["Presolved Problem name"] = relevant_value - continue - - if stat_name == "Gap": - result["Gap (%)"] = float(relevant_value[:-1]) - continue - - if _is_number(relevant_value): - result[cur_stat] = float(relevant_value) - if cur_stat == "Solutions found" and result[cur_stat] == 0: - break - else: # it's a string - result[cur_stat] = relevant_value - - # changing keys to pythonic variable names - treated_keys = {"status": "status", "Total Time": "total_time", "solving":"solving_time", "presolving":"presolving_time", "reading":"reading_time", - "copying":"copying_time", "Problem name": "problem_name", "Presolved Problem name": "presolved_problem_name", "Variables":"_variables", - "Presolved Variables":"_presolved_variables", "Constraints": "_constraints", "Presolved Constraints":"_presolved_constraints", - "number of runs": "n_runs", "nodes":"n_nodes", "Solutions found": "n_solutions_found"} - - if result["status"] in ["optimal", "user_interrupt"]: - if result["Solutions found"] > 0: - treated_keys["First Solution"] = "first_solution" - treated_keys["Primal Bound"] = "primal_bound" - treated_keys["Dual Bound"] = "dual_bound" - treated_keys["Gap (%)"] = "gap" - treated_keys["primal-dual"] = "primal_dual_integral" - treated_result = dict((treated_keys[key], value) for (key, value) in result.items()) - - stats = Statistics(**treated_result) - return stats def getNLPs(self): """gets total number of LPs solved so far""" @@ -5701,6 +5589,117 @@ class Statistics: @property def n_presolved_maximal_cons(self): return self._presolved_constraints["maximal"] + +def readStatistics(filename): + """ + Given a .stats file of a solved model, reads it and returns an instance of the Statistics class + holding some statistics. + + Keyword arguments: + filename -- name of the input file + """ + result = {} + file = open(filename) + data = file.readlines() + + if "optimal solution found" in data[0]: + result["status"] = "optimal" + elif "infeasible" in data[0]: + result["status"] = "infeasible" + elif "unbounded" in data[0]: + result["status"] = "unbounded" + elif "limit reached" in data[0]: + result["status"] = "user_interrupt" + else: + raise "readStatistics can only be called if the problem was solved" + + available_stats = ["Total Time", "solving", "presolving", "reading", "copying", + "Problem name", "Variables", "Constraints", "number of runs", + "nodes", "Solutions found"] + + if result["status"] in ["optimal", "user_interrupt"]: + available_stats.extend(["First Solution", "Primal Bound", "Dual Bound", "Gap", "primal-dual"]) + + seen_cons = 0 + for i, line in enumerate(data): + split_line = line.split(":") + split_line[1] = split_line[1][:-1] # removing \n + stat_name = split_line[0].strip() + + if seen_cons == 2 and stat_name == "Constraints": + continue + + if stat_name in available_stats: + relevant_value = split_line[1].strip() + + if stat_name == "Variables": + relevant_value = relevant_value[:-1] # removing ")" + var_stats = {} + split_var = relevant_value.split("(") + var_stats["total"] = int(split_var[0]) + split_var = split_var[1].split(",") + + for var_type in split_var: + split_result = var_type.strip().split(" ") + var_stats[split_result[1]] = int(split_result[0]) + + if "Original" in data[i-2]: + result["Variables"] = var_stats + else: + result["Presolved Variables"] = var_stats + + continue + + if stat_name == "Constraints": + seen_cons += 1 + con_stats = {} + split_con = relevant_value.split(",") + for con_type in split_con: + split_result = con_type.strip().split(" ") + con_stats[split_result[1]] = int(split_result[0]) + + if "Original" in data[i-3]: + result["Constraints"] = con_stats + else: + result["Presolved Constraints"] = con_stats + continue + + relevant_value = relevant_value.split(" ")[0] + if stat_name == "Problem name": + if "Original" in data[i-1]: + result["Problem name"] = relevant_value + else: + result["Presolved Problem name"] = relevant_value + continue + + if stat_name == "Gap": + relevant_value = relevant_value[:-1] # removing % + + if _is_number(relevant_value): + result[stat_name] = float(relevant_value) + if stat_name == "Solutions found" and result[stat_name] == 0: + break + + else: # it's a string + result[stat_name] = relevant_value + + # changing keys to pythonic variable names + treated_keys = {"status": "status", "Total Time": "total_time", "solving":"solving_time", "presolving":"presolving_time", "reading":"reading_time", + "copying":"copying_time", "Problem name": "problem_name", "Presolved Problem name": "presolved_problem_name", "Variables":"_variables", + "Presolved Variables":"_presolved_variables", "Constraints": "_constraints", "Presolved Constraints":"_presolved_constraints", + "number of runs": "n_runs", "nodes":"n_nodes", "Solutions found": "n_solutions_found"} + + if result["status"] in ["optimal", "user_interrupt"]: + if result["Solutions found"] > 0: + treated_keys["First Solution"] = "first_solution" + treated_keys["Primal Bound"] = "primal_bound" + treated_keys["Dual Bound"] = "dual_bound" + treated_keys["Gap"] = "gap" + treated_keys["primal-dual"] = "primal_dual_integral" + treated_result = dict((treated_keys[key], value) for (key, value) in result.items()) + + stats = Statistics(**treated_result) + return stats # debugging memory management def is_memory_freed(): diff --git a/tests/test_reader.py b/tests/test_reader.py index 69001dae..ef2534de 100644 --- a/tests/test_reader.py +++ b/tests/test_reader.py @@ -1,7 +1,7 @@ import pytest import os -from pyscipopt import Model, quicksum, Reader, SCIP_RESULT +from pyscipopt import Model, quicksum, Reader, SCIP_RESULT, readStatistics class SudokuReader(Reader): @@ -89,7 +89,7 @@ def test_readStatistics(): m.hideOutput() m.optimize() m.writeStatistics(os.path.join("tests", "data", "readStatistics.stats")) - result = m.readStatistics(os.path.join("tests", "data", "readStatistics.stats")) + result = readStatistics(os.path.join("tests", "data", "readStatistics.stats")) assert result.status == "optimal" assert len([k for k, val in result.__dict__.items() if not str(hex(id(val))) in str(val)]) == 20 # number of attributes. See https://stackoverflow.com/a/57431390/9700522 @@ -113,7 +113,7 @@ def test_readStatistics(): m.hideOutput() m.optimize() m.writeStatistics(os.path.join("tests", "data", "readStatistics.stats")) - result = m.readStatistics(os.path.join("tests", "data", "readStatistics.stats")) + result = readStatistics(os.path.join("tests", "data", "readStatistics.stats")) assert result.status == "unbounded" m = Model() @@ -122,7 +122,7 @@ def test_readStatistics(): m.hideOutput() m.optimize() m.writeStatistics(os.path.join("tests", "data", "readStatistics.stats")) - result = m.readStatistics(os.path.join("tests", "data", "readStatistics.stats")) + result = readStatistics(os.path.join("tests", "data", "readStatistics.stats")) assert result.status == "infeasible" assert result.gap == None assert result.n_solutions_found == 0 @@ -133,8 +133,6 @@ def test_readStatistics(): m.setParam("limits/solutions", 0) m.optimize() m.writeStatistics(os.path.join("tests", "data", "readStatistics.stats")) - result = m.readStatistics(os.path.join("tests", "data", "readStatistics.stats")) + result = readStatistics(os.path.join("tests", "data", "readStatistics.stats")) assert result.status == "user_interrupt" - assert result.gap == None - -test_readStatistics() \ No newline at end of file + assert result.gap == None \ No newline at end of file From 3547dbb4914a2475358721623d3b6b879f2425b5 Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Tue, 25 Jun 2024 16:06:40 +0100 Subject: [PATCH 14/14] Update CHANGELOG --- CHANGELOG.md | 3 ++- src/pyscipopt/__init__.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6dc7e053..64cc913e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,12 @@ ## Unreleased ### Added -- Expanded Statistics class to more problems +- Expanded Statistics class to more problems. - Created Statistics class - Added parser to read .stats file ### Fixed ### Changed +- Made readStatistics a standalone function ### Removed ## 5.1.1 - 2024-06-22 diff --git a/src/pyscipopt/__init__.py b/src/pyscipopt/__init__.py index a370c60b..cd6528f7 100644 --- a/src/pyscipopt/__init__.py +++ b/src/pyscipopt/__init__.py @@ -24,6 +24,7 @@ from pyscipopt.scip import Reader from pyscipopt.scip import Sepa from pyscipopt.scip import LP +from pyscipopt.scip import readStatistics from pyscipopt.scip import Expr from pyscipopt.scip import quicksum from pyscipopt.scip import quickprod