Skip to content

Commit b12f184

Browse files
Add parser to read SCIP statistics (#859)
* Add readStatistics * Add test for readStatistics * Update CHANGELOG * Organize statistics in class * Add some comments * Some more comments * Update Statistics class and documentation * Update CHANGELOG
1 parent c405187 commit b12f184

File tree

4 files changed

+262
-2
lines changed

4 files changed

+262
-2
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## Unreleased
44
### Added
5+
- Created Statistics class
6+
- Added parser to read .stats file
57
### Fixed
68
### Changed
79
### Removed

src/pyscipopt/scip.pxi

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ from posix.stdio cimport fileno
1717

1818
from collections.abc import Iterable
1919
from itertools import repeat
20+
from dataclasses import dataclass
2021

2122
include "expr.pxi"
2223
include "lp.pxi"
@@ -5102,6 +5103,97 @@ cdef class Model:
51025103
PY_SCIP_CALL(SCIPprintStatistics(self._scip, cfile))
51035104

51045105
locale.setlocale(locale.LC_NUMERIC,user_locale)
5106+
5107+
def readStatistics(self, filename):
5108+
"""
5109+
Given a .stats file of a solved model, reads it and returns an instance of the Statistics class
5110+
holding some statistics.
5111+
5112+
Keyword arguments:
5113+
filename -- name of the input file
5114+
"""
5115+
result = {}
5116+
file = open(filename)
5117+
data = file.readlines()
5118+
5119+
assert "problem is solved" in data[0], "readStatistics can only be called if the problem was solved"
5120+
available_stats = ["Total Time", "solving", "presolving", "reading", "copying",
5121+
"Problem name", "Variables", "Constraints", "number of runs",
5122+
"nodes", "Solutions found", "First Solution", "Primal Bound",
5123+
"Dual Bound", "Gap", "primal-dual"]
5124+
5125+
seen_cons = 0
5126+
for i, line in enumerate(data):
5127+
split_line = line.split(":")
5128+
split_line[1] = split_line[1][:-1] # removing \n
5129+
stat_name = split_line[0].strip()
5130+
5131+
if seen_cons == 2 and stat_name == "Constraints":
5132+
continue
5133+
5134+
if stat_name in available_stats:
5135+
cur_stat = split_line[0].strip()
5136+
relevant_value = split_line[1].strip()
5137+
5138+
if stat_name == "Variables":
5139+
relevant_value = relevant_value[:-1] # removing ")"
5140+
var_stats = {}
5141+
split_var = relevant_value.split("(")
5142+
var_stats["total"] = int(split_var[0])
5143+
split_var = split_var[1].split(",")
5144+
5145+
for var_type in split_var:
5146+
split_result = var_type.strip().split(" ")
5147+
var_stats[split_result[1]] = int(split_result[0])
5148+
5149+
if "Original" in data[i-2]:
5150+
result["Variables"] = var_stats
5151+
else:
5152+
result["Presolved Variables"] = var_stats
5153+
5154+
continue
5155+
5156+
if stat_name == "Constraints":
5157+
seen_cons += 1
5158+
con_stats = {}
5159+
split_con = relevant_value.split(",")
5160+
for con_type in split_con:
5161+
split_result = con_type.strip().split(" ")
5162+
con_stats[split_result[1]] = int(split_result[0])
5163+
5164+
if "Original" in data[i-3]:
5165+
result["Constraints"] = con_stats
5166+
else:
5167+
result["Presolved Constraints"] = con_stats
5168+
continue
5169+
5170+
relevant_value = relevant_value.split(" ")[0]
5171+
if stat_name == "Problem name":
5172+
if "Original" in data[i-1]:
5173+
result["Problem name"] = relevant_value
5174+
else:
5175+
result["Presolved Problem name"] = relevant_value
5176+
continue
5177+
5178+
if stat_name == "Gap":
5179+
result["Gap (%)"] = float(relevant_value[:-1])
5180+
continue
5181+
5182+
if _is_number(relevant_value):
5183+
result[cur_stat] = float(relevant_value)
5184+
else: # it's a string
5185+
result[cur_stat] = relevant_value
5186+
5187+
# changing keys to pythonic variable names
5188+
treated_keys = {"Total Time": "total_time", "solving":"solving_time", "presolving":"presolving_time", "reading":"reading_time", "copying":"copying_time",
5189+
"Problem name": "problem_name", "Presolved Problem name": "presolved_problem_name", "Variables":"_variables",
5190+
"Presolved Variables":"_presolved_variables", "Constraints": "_constraints", "Presolved Constraints":"_presolved_constraints",
5191+
"number of runs": "n_runs", "nodes":"n_nodes", "Solutions found": "n_solutions_found", "First Solution": "first_solution",
5192+
"Primal Bound":"primal_bound", "Dual Bound":"dual_bound", "Gap (%)":"gap", "primal-dual":"primal_dual_integral"}
5193+
treated_result = dict((treated_keys[key], value) for (key, value) in result.items())
5194+
5195+
stats = Statistics(**treated_result)
5196+
return stats
51055197

51065198
def getNLPs(self):
51075199
"""gets total number of LPs solved so far"""
@@ -5445,6 +5537,147 @@ cdef class Model:
54455537
"""Get an estimation of the final tree size """
54465538
return SCIPgetTreesizeEstimation(self._scip)
54475539

5540+
@dataclass
5541+
class Statistics:
5542+
"""
5543+
Attributes
5544+
----------
5545+
total_time : float
5546+
Total time since model was created
5547+
solving_time: float
5548+
Time spent solving the problem
5549+
presolving_time: float
5550+
Time spent on presolving
5551+
reading_time: float
5552+
Time spent on reading
5553+
copying_time: float
5554+
Time spent on copying
5555+
problem_name: str
5556+
Name of problem
5557+
presolved_problem_name: str
5558+
Name of presolved problem
5559+
n_nodes: int
5560+
The number of nodes explored in the branch-and-bound tree
5561+
n_solutions_found: int
5562+
number of found solutions
5563+
first_solution: float
5564+
objective value of first found solution
5565+
primal_bound: float
5566+
The best primal bound found
5567+
dual_bound: float
5568+
The best dual bound found
5569+
gap: float
5570+
The gap between the primal and dual bounds
5571+
primal_dual_integral: float
5572+
The primal-dual integral
5573+
n_vars: int
5574+
number of variables in the model
5575+
n_binary_vars: int
5576+
number of binary variables in the model
5577+
n_integer_vars: int
5578+
number of integer variables in the model
5579+
n_implicit_integer_vars: int
5580+
number of implicit integer variables in the model
5581+
n_continuous_vars: int
5582+
number of continuous variables in the model
5583+
n_presolved_vars: int
5584+
number of variables in the presolved model
5585+
n_presolved_continuous_vars: int
5586+
number of continuous variables in the presolved model
5587+
n_presolved_binary_vars: int
5588+
number of binary variables in the presolved model
5589+
n_presolved_integer_vars: int
5590+
number of integer variables in the presolved model
5591+
n_presolved_implicit_integer_vars: int
5592+
number of implicit integer variables in the presolved model
5593+
n_maximal_cons: int
5594+
number of maximal constraints in the model
5595+
n_initial_cons: int
5596+
number of initial constraints in the presolved model
5597+
n_presolved_maximal_cons: int
5598+
number of maximal constraints in the presolved model
5599+
n_presolved_conss: int
5600+
number of initial constraints in the model
5601+
"""
5602+
5603+
total_time: float
5604+
solving_time: float
5605+
presolving_time: float
5606+
reading_time: float
5607+
copying_time: float
5608+
problem_name: str
5609+
presolved_problem_name: str
5610+
_variables: dict # Dictionary with number of variables by type
5611+
_presolved_variables: dict # Dictionary with number of presolved variables by type
5612+
_constraints: dict # Dictionary with number of constraints by type
5613+
_presolved_constraints: dict # Dictionary with number of presolved constraints by type
5614+
n_runs: int
5615+
n_nodes: int
5616+
n_solutions_found: int
5617+
first_solution: float
5618+
primal_bound: float
5619+
dual_bound: float
5620+
gap: float
5621+
primal_dual_integral: float
5622+
5623+
# unpacking the _variables, _presolved_variables, _constraints
5624+
# _presolved_constraints dictionaries
5625+
@property
5626+
def n_vars(self):
5627+
return self._variables["total"]
5628+
5629+
@property
5630+
def n_binary_vars(self):
5631+
return self._variables["binary"]
5632+
5633+
@property
5634+
def n_integer_vars(self):
5635+
return self._variables["integer"]
5636+
5637+
@property
5638+
def n_implicit_integer_vars(self):
5639+
return self._variables["implicit"]
5640+
5641+
@property
5642+
def n_continuous_vars(self):
5643+
return self._variables["continuous"]
5644+
5645+
@property
5646+
def n_presolved_vars(self):
5647+
return self._presolved_variables["total"]
5648+
5649+
@property
5650+
def n_presolved_binary_vars(self):
5651+
return self._presolved_variables["binary"]
5652+
5653+
@property
5654+
def n_presolved_integer_vars(self):
5655+
return self._presolved_variables["integer"]
5656+
5657+
@property
5658+
def n_presolved_implicit_integer_vars(self):
5659+
return self._presolved_variables["implicit"]
5660+
5661+
@property
5662+
def n_presolved_continuous_vars(self):
5663+
return self._presolved_variables["continuous"]
5664+
5665+
@property
5666+
def n_conss(self):
5667+
return self._constraints["initial"]
5668+
5669+
@property
5670+
def n_maximal_cons(self):
5671+
return self._constraints["maximal"]
5672+
5673+
@property
5674+
def n_presolved_conss(self):
5675+
return self._presolved_constraints["initial"]
5676+
5677+
@property
5678+
def n_presolved_maximal_cons(self):
5679+
return self._presolved_constraints["maximal"]
5680+
54485681
# debugging memory management
54495682
def is_memory_freed():
54505683
return BMSgetMemoryUsed() == 0

tests/test_model.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -369,7 +369,6 @@ def test_locale():
369369
except Exception:
370370
pytest.skip("pt_PT locale was not found. It might need to be installed.")
371371

372-
373372
def test_version_external_codes():
374373
scip = Model()
375374
scip.printVersion()

tests/test_reader.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,4 +78,30 @@ def test_sudoku_reader():
7878
input = f.readline()
7979
assert input == "sudoku"
8080

81-
deleteFile("model.sod")
81+
deleteFile("model.sod")
82+
83+
def test_readStatistics():
84+
m = Model(problemName="readStats")
85+
x = m.addVar(vtype="I")
86+
y = m.addVar()
87+
88+
m.addCons(x+y <= 3)
89+
m.hideOutput()
90+
m.optimize()
91+
m.writeStatistics(os.path.join("tests", "data", "readStatistics.stats"))
92+
result = m.readStatistics(os.path.join("tests", "data", "readStatistics.stats"))
93+
94+
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
95+
assert type(result.total_time) == float
96+
assert result.problem_name == "readStats"
97+
assert result.presolved_problem_name == "t_readStats"
98+
assert type(result.primal_dual_integral) == float
99+
assert result.n_solutions_found == 1
100+
assert type(result.gap) == float
101+
assert result._presolved_constraints == {"initial": 1, "maximal": 1}
102+
assert result._variables == {"total": 2, "binary": 0, "integer": 1, "implicit": 0, "continuous": 1}
103+
assert result._presolved_variables == {"total": 0, "binary": 0, "integer": 0, "implicit": 0, "continuous": 0}
104+
assert result.n_vars == 2
105+
assert result.n_presolved_vars == 0
106+
assert result.n_binary_vars == 0
107+
assert result.n_integer_vars == 1

0 commit comments

Comments
 (0)