Skip to content

Commit

Permalink
Merge pull request #871 from Joao-Dionisio/get_statistics
Browse files Browse the repository at this point in the history
Expand getStatistics to allow unbounded, infeasible, and user-interrupted problems
  • Loading branch information
mmghannam authored Aug 14, 2024
2 parents f86fd95 + 2ec7876 commit a3a1d67
Show file tree
Hide file tree
Showing 4 changed files with 160 additions and 103 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Unreleased
### Added
- Expanded Statistics class to more problems.
- Created Statistics class
- Added parser to read .stats file
- Release checklist in `RELEASE.md`
Expand Down Expand Up @@ -30,6 +31,7 @@
### Fixed
- Fixed locale errors in reading
### Changed
- Made readStatistics a standalone function
### Removed

## 5.1.1 - 2024-06-22
Expand Down
1 change: 1 addition & 0 deletions src/pyscipopt/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
221 changes: 122 additions & 99 deletions src/pyscipopt/scip.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -5151,97 +5151,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()

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(":")
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

# 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": "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)
return stats

def getNLPs(self):
"""gets total number of LPs solved so far"""
Expand Down Expand Up @@ -5952,6 +5861,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
Expand Down Expand Up @@ -6010,6 +5921,7 @@ class Statistics:
number of initial constraints in the model
"""

status: str
total_time: float
solving_time: float
presolving_time: float
Expand All @@ -6021,14 +5933,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
Expand Down Expand Up @@ -6088,6 +6000,117 @@ class Statistics:
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():
return BMSgetMemoryUsed() == 0
Expand Down
39 changes: 35 additions & 4 deletions tests/test_reader.py
Original file line number Diff line number Diff line change
@@ -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):

Expand Down Expand Up @@ -89,9 +89,10 @@ 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 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"
Expand All @@ -104,4 +105,34 @@ 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
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 = 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 = 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 = readStatistics(os.path.join("tests", "data", "readStatistics.stats"))
assert result.status == "user_interrupt"
assert result.gap == None

0 comments on commit a3a1d67

Please sign in to comment.