Skip to content

Commit

Permalink
Unit tests for config, results, and util complete
Browse files Browse the repository at this point in the history
  • Loading branch information
mrmundt committed Feb 14, 2024
1 parent cb085e1 commit 2a3b573
Show file tree
Hide file tree
Showing 8 changed files with 187 additions and 53 deletions.
6 changes: 4 additions & 2 deletions pyomo/contrib/solver/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ def __init__(
ConfigValue(
domain=str,
default=None,
description="The directory in which generated files should be saved. This replaced the `keepfiles` option.",
description="The directory in which generated files should be saved. "
"This replaced the `keepfiles` option.",
),
)
self.load_solutions: bool = self.declare(
Expand All @@ -79,7 +80,8 @@ def __init__(
ConfigValue(
domain=bool,
default=True,
description="If False, the `solve` method will continue processing even if the returned result is nonoptimal.",
description="If False, the `solve` method will continue processing "
"even if the returned result is nonoptimal.",
),
)
self.symbolic_solver_labels: bool = self.declare(
Expand Down
103 changes: 69 additions & 34 deletions pyomo/contrib/solver/results.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,12 @@
NonNegativeFloat,
ADVANCED_OPTION,
)
from pyomo.common.errors import PyomoException
from pyomo.opt.results.solution import SolutionStatus as LegacySolutionStatus
from pyomo.opt.results.solver import (
TerminationCondition as LegacyTerminationCondition,
SolverStatus as LegacySolverStatus,
)


class SolverResultsError(PyomoException):
"""
General exception to catch solver system errors
"""
from pyomo.common.timing import HierarchicalTimer


class TerminationCondition(enum.Enum):
Expand Down Expand Up @@ -167,12 +161,16 @@ class Results(ConfigDict):
iteration_count: int
The total number of iterations.
timing_info: ConfigDict
A ConfigDict containing three pieces of information:
start_time: UTC timestamp of when run was initiated
A ConfigDict containing two pieces of information:
start_timestamp: UTC timestamp of when run was initiated
wall_time: elapsed wall clock time for entire process
solver_wall_time: elapsed wall clock time for solve call
timer: a HierarchicalTimer object containing timing data about the solve
extra_info: ConfigDict
A ConfigDict to store extra information such as solver messages.
solver_configuration: ConfigDict
A copy of the SolverConfig ConfigDict, for later inspection/reproducibility.
solver_log: str
(ADVANCED OPTION) Any solver log messages.
"""

def __init__(
Expand All @@ -191,55 +189,104 @@ def __init__(
visibility=visibility,
)

self.solution_loader = self.declare('solution_loader', ConfigValue())
self.solution_loader = self.declare(
'solution_loader',
ConfigValue(
description="Object for loading the solution back into the model."
),
)
self.termination_condition: TerminationCondition = self.declare(
'termination_condition',
ConfigValue(
domain=In(TerminationCondition), default=TerminationCondition.unknown
domain=In(TerminationCondition),
default=TerminationCondition.unknown,
description="The reason the solver exited. This is a member of the "
"TerminationCondition enum.",
),
)
self.solution_status: SolutionStatus = self.declare(
'solution_status',
ConfigValue(domain=In(SolutionStatus), default=SolutionStatus.noSolution),
ConfigValue(
domain=In(SolutionStatus),
default=SolutionStatus.noSolution,
description="The result of the solve call. This is a member of "
"the SolutionStatus enum.",
),
)
self.incumbent_objective: Optional[float] = self.declare(
'incumbent_objective', ConfigValue(domain=float, default=None)
'incumbent_objective',
ConfigValue(
domain=float,
default=None,
description="If a feasible solution was found, this is the objective "
"value of the best solution found. If no feasible solution was found, this is None.",
),
)
self.objective_bound: Optional[float] = self.declare(
'objective_bound', ConfigValue(domain=float, default=None)
'objective_bound',
ConfigValue(
domain=float,
default=None,
description="The best objective bound found. For minimization problems, "
"this is the lower bound. For maximization problems, this is the "
"upper bound. For solvers that do not provide an objective bound, "
"this should be -inf (minimization) or inf (maximization)",
),
)
self.solver_name: Optional[str] = self.declare(
'solver_name', ConfigValue(domain=str)
'solver_name',
ConfigValue(domain=str, description="The name of the solver in use."),
)
self.solver_version: Optional[Tuple[int, ...]] = self.declare(
'solver_version', ConfigValue(domain=tuple)
'solver_version',
ConfigValue(
domain=tuple,
description="A tuple representing the version of the solver in use.",
),
)
self.iteration_count: Optional[int] = self.declare(
'iteration_count', ConfigValue(domain=NonNegativeInt, default=None)
'iteration_count',
ConfigValue(
domain=NonNegativeInt,
default=None,
description="The total number of iterations.",
),
)
self.timing_info: ConfigDict = self.declare(
'timing_info', ConfigDict(implicit=True)
)

self.timing_info.start_timestamp: datetime = self.timing_info.declare(
'start_timestamp', ConfigValue(domain=Datetime)
'start_timestamp',
ConfigValue(
domain=Datetime, description="UTC timestamp of when run was initiated."
),
)
self.timing_info.wall_time: Optional[float] = self.timing_info.declare(
'wall_time', ConfigValue(domain=NonNegativeFloat)
'wall_time',
ConfigValue(
domain=NonNegativeFloat,
description="Elapsed wall clock time for entire process.",
),
)
self.extra_info: ConfigDict = self.declare(
'extra_info', ConfigDict(implicit=True)
)
self.solver_configuration: ConfigDict = self.declare(
'solver_configuration',
ConfigValue(
description="A copy of the config object used in the solve",
description="A copy of the config object used in the solve call.",
visibility=ADVANCED_OPTION,
),
)
self.solver_log: str = self.declare(
'solver_log',
ConfigValue(domain=str, default=None, visibility=ADVANCED_OPTION),
ConfigValue(
domain=str,
default=None,
visibility=ADVANCED_OPTION,
description="Any solver log messages.",
),
)

def display(
Expand All @@ -248,18 +295,6 @@ def display(
return super().display(content_filter, indent_spacing, ostream, visibility)


class ResultsReader:
pass


def parse_yaml():
pass


def parse_json():
pass


# Everything below here preserves backwards compatibility

legacy_termination_condition_map = {
Expand Down
8 changes: 4 additions & 4 deletions pyomo/contrib/solver/sol_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

from pyomo.common.errors import DeveloperError
from pyomo.repn.plugins.nl_writer import NLWriterInfo
from .results import Results, SolverResultsError, SolutionStatus, TerminationCondition
from .results import Results, SolutionStatus, TerminationCondition


class SolFileData:
Expand Down Expand Up @@ -69,7 +69,7 @@ def parse_sol_file(
line = sol_file.readline()
model_objects.append(int(line))
else:
raise SolverResultsError("ERROR READING `sol` FILE. No 'Options' line found.")
raise Exception("ERROR READING `sol` FILE. No 'Options' line found.")
# Identify the total number of variables and constraints
number_of_cons = model_objects[number_of_options + 1]
number_of_vars = model_objects[number_of_options + 3]
Expand All @@ -85,12 +85,12 @@ def parse_sol_file(
if line and ('objno' in line):
exit_code_line = line.split()
if len(exit_code_line) != 3:
raise SolverResultsError(
raise Exception(
f"ERROR READING `sol` FILE. Expected two numbers in `objno` line; received {line}."
)
exit_code = [int(exit_code_line[1]), int(exit_code_line[2])]
else:
raise SolverResultsError(
raise Exception(
f"ERROR READING `sol` FILE. Expected `objno`; received {line}."
)
result.extra_info.solver_message = message.strip().replace('\n', '; ')
Expand Down
6 changes: 5 additions & 1 deletion pyomo/contrib/solver/solution.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@


class SolutionLoaderBase(abc.ABC):
"""
Base class for all future SolutionLoader classes.
Intent of this class and its children is to load the solution back into the model.
"""
def load_vars(
self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None
) -> NoReturn:
Expand Down Expand Up @@ -58,7 +63,6 @@ def get_primals(
primals: ComponentMap
Maps variables to solution values
"""
pass

def get_duals(
self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None
Expand Down
61 changes: 60 additions & 1 deletion pyomo/contrib/solver/tests/unit/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@
# ___________________________________________________________________________

from pyomo.common import unittest
from pyomo.contrib.solver.config import SolverConfig, BranchAndBoundConfig
from pyomo.contrib.solver.config import (
SolverConfig,
BranchAndBoundConfig,
AutoUpdateConfig,
PersistentSolverConfig,
)


class TestSolverConfig(unittest.TestCase):
Expand Down Expand Up @@ -59,3 +64,57 @@ def test_interface_custom_instantiation(self):
self.assertIsInstance(config.time_limit, float)
config.rel_gap = 2.5
self.assertEqual(config.rel_gap, 2.5)


class TestAutoUpdateConfig(unittest.TestCase):
def test_interface_default_instantiation(self):
config = AutoUpdateConfig()
self.assertTrue(config.check_for_new_or_removed_constraints)
self.assertTrue(config.check_for_new_or_removed_vars)
self.assertTrue(config.check_for_new_or_removed_params)
self.assertTrue(config.check_for_new_objective)
self.assertTrue(config.update_constraints)
self.assertTrue(config.update_vars)
self.assertTrue(config.update_named_expressions)
self.assertTrue(config.update_objective)
self.assertTrue(config.update_objective)
self.assertTrue(config.treat_fixed_vars_as_params)

def test_interface_custom_instantiation(self):
config = AutoUpdateConfig(description="A description")
config.check_for_new_objective = False
self.assertEqual(config._description, "A description")
self.assertTrue(config.check_for_new_or_removed_constraints)
self.assertFalse(config.check_for_new_objective)


class TestPersistentSolverConfig(unittest.TestCase):
def test_interface_default_instantiation(self):
config = PersistentSolverConfig()
self.assertIsNone(config._description)
self.assertEqual(config._visibility, 0)
self.assertFalse(config.tee)
self.assertTrue(config.load_solutions)
self.assertTrue(config.raise_exception_on_nonoptimal_result)
self.assertFalse(config.symbolic_solver_labels)
self.assertIsNone(config.timer)
self.assertIsNone(config.threads)
self.assertIsNone(config.time_limit)
self.assertTrue(config.auto_updates.check_for_new_or_removed_constraints)
self.assertTrue(config.auto_updates.check_for_new_or_removed_vars)
self.assertTrue(config.auto_updates.check_for_new_or_removed_params)
self.assertTrue(config.auto_updates.check_for_new_objective)
self.assertTrue(config.auto_updates.update_constraints)
self.assertTrue(config.auto_updates.update_vars)
self.assertTrue(config.auto_updates.update_named_expressions)
self.assertTrue(config.auto_updates.update_objective)
self.assertTrue(config.auto_updates.update_objective)
self.assertTrue(config.auto_updates.treat_fixed_vars_as_params)

def test_interface_custom_instantiation(self):
config = PersistentSolverConfig(description="A description")
config.tee = True
config.auto_updates.check_for_new_objective = False
self.assertTrue(config.tee)
self.assertEqual(config._description, "A description")
self.assertFalse(config.auto_updates.check_for_new_objective)
6 changes: 3 additions & 3 deletions pyomo/contrib/solver/tests/unit/test_results.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ def test_codes(self):


class TestResults(unittest.TestCase):
def test_declared_items(self):
def test_member_list(self):
res = results.Results()
expected_declared = {
'extra_info',
Expand All @@ -88,7 +88,7 @@ def test_declared_items(self):
actual_declared = res._declared
self.assertEqual(expected_declared, actual_declared)

def test_uninitialized(self):
def test_default_initialization(self):
res = results.Results()
self.assertIsNone(res.incumbent_objective)
self.assertIsNone(res.objective_bound)
Expand Down Expand Up @@ -118,7 +118,7 @@ def test_uninitialized(self):
):
res.solution_loader.get_reduced_costs()

def test_results(self):
def test_generated_results(self):
m = pyo.ConcreteModel()
m.x = ScalarVar()
m.y = ScalarVar()
Expand Down
18 changes: 13 additions & 5 deletions pyomo/contrib/solver/tests/unit/test_solution.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,30 @@
# ___________________________________________________________________________

from pyomo.common import unittest
from pyomo.contrib.solver import solution
from pyomo.contrib.solver.solution import SolutionLoaderBase, PersistentSolutionLoader


class TestPersistentSolverBase(unittest.TestCase):
class TestSolutionLoaderBase(unittest.TestCase):
def test_abstract_member_list(self):
expected_list = ['get_primals']
member_list = list(solution.SolutionLoaderBase.__abstractmethods__)
member_list = list(SolutionLoaderBase.__abstractmethods__)
self.assertEqual(sorted(expected_list), sorted(member_list))

@unittest.mock.patch.multiple(
solution.SolutionLoaderBase, __abstractmethods__=set()
SolutionLoaderBase, __abstractmethods__=set()
)
def test_solution_loader_base(self):
self.instance = solution.SolutionLoaderBase()
self.instance = SolutionLoaderBase()
self.assertEqual(self.instance.get_primals(), None)
with self.assertRaises(NotImplementedError):
self.instance.get_duals()
with self.assertRaises(NotImplementedError):
self.instance.get_reduced_costs()


class TestPersistentSolutionLoader(unittest.TestCase):
def test_abstract_member_list(self):
# We expect no abstract members at this point because it's a real-life
# instantiation of SolutionLoaderBase
member_list = list(PersistentSolutionLoader('ipopt').__abstractmethods__)
self.assertEqual(member_list, [])
Loading

0 comments on commit 2a3b573

Please sign in to comment.