Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expand getStatistics to allow unbounded, infeasible, and user-interrupted problems #871

Merged
merged 19 commits into from
Aug 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -262,7 +262,7 @@
if rc == SCIP_OKAY:
pass
elif rc == SCIP_ERROR:
raise Exception('SCIP: unspecified error!')

Check failure on line 265 in src/pyscipopt/scip.pxi

View workflow job for this annotation

GitHub Actions / test-coverage (3.11)

SCIP: unspecified error!
elif rc == SCIP_NOMEMORY:
raise MemoryError('SCIP: insufficient memory error!')
elif rc == SCIP_READERROR:
Expand Down Expand Up @@ -5151,97 +5151,6 @@
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 @@
"""
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 @@
number of initial constraints in the model
"""

status: str
total_time: float
solving_time: float
presolving_time: float
Expand All @@ -6021,14 +5933,14 @@
_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 @@
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
Loading