diff --git a/pyomo/contrib/solver/test/__init__.py b/pyomo/contrib/solver/test/__init__.py new file mode 100644 index 00000000000..4642c8a9f52 --- /dev/null +++ b/pyomo/contrib/solver/test/__init__.py @@ -0,0 +1,48 @@ +import re +from typing import List, Optional, Type, Union +from pyomo.common import unittest +from pyomo.contrib.solver.common.base import SolverBase +from .registry import SolverTestFilter, SolverTestRegistry +from .builder import SolverTestBuilder +from . import base +from . import linear +from . import dual +from . import quadratic +from . import nonlinear + + +def add_tests( + test_case_cls: type[unittest.TestCase], + opt_cls: Type[SolverBase], + *, + include: Optional[List[Union[str, re.Pattern]]] = None, + exclude: Optional[List[Union[str, re.Pattern]]] = None, + include_tags: Optional[List[str]] = None, + exclude_tags: Optional[List[str]] = None, + warn_unsupported: bool = False, + warn_unavailable: bool = False, +) -> None: + + if not issubclass(test_case_cls, unittest.TestCase): + raise TypeError(f"{test_case_cls} must be a TestCase subclass") + + if not issubclass(opt_cls, SolverBase): + raise TypeError(f"{opt_cls} must be a SolverBase subclass") + + test_builder = SolverTestBuilder(warn_unsupported, warn_unavailable) + + test_filter = SolverTestFilter( + include=include, + exclude=exclude, + include_tags=include_tags, + exclude_tags=exclude_tags, + ) + + filtered_tests = SolverTestRegistry.get_filtered_tests(test_filter) + + for base_test_name, test_meta in filtered_tests.items(): + + test_method = test_builder.build(opt_cls, test_meta) + if test_method is not None: + test_name = f"{base_test_name}_{opt_cls.name}" + setattr(test_case_cls, test_name, test_method) diff --git a/pyomo/contrib/solver/test/base.py b/pyomo/contrib/solver/test/base.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/pyomo/contrib/solver/test/builder.py b/pyomo/contrib/solver/test/builder.py new file mode 100644 index 00000000000..a0a274cd373 --- /dev/null +++ b/pyomo/contrib/solver/test/builder.py @@ -0,0 +1,46 @@ +from collections.abc import Callable +from typing import Optional, Type + +from pyomo.common import unittest +from pyomo.contrib.solver.common.base import SolverBase +from .registry import SolverTestMeta + + +class SolverTestBuilder: + def __init__(self, warn_unsupported: bool, warn_unavailable: bool): + self.warn_unsupported = warn_unsupported + self.warn_unavailable = warn_unavailable + + def build( + self, opt_cls: Type[SolverBase], test_meta: SolverTestMeta + ) -> Optional[Callable[[unittest.TestCase], None]]: + try: + avail = bool(opt_cls().available()) + except Exception as e: + avail = False + + can_run, skip_reason = test_meta.can_run_on(opt_cls) + + if can_run and avail: + return self._build_runnable_test(opt_cls, test_meta) + elif can_run and not avail and self.warn_unavailable: + return self._build_skip_test(f"Solver {opt_cls.name} is not available") + elif not can_run and self.warn_unsupported: + return self._build_skip_test(f"Solver {opt_cls.name} {skip_reason}") + + else: + return None + + def _build_runnable_test( + self, opt_cls: Type[SolverBase], test_meta: SolverTestMeta + ): + def test_method(self: unittest.TestCase): + test_meta.func(self, opt_cls) + + return test_method + + def _build_skip_test(self, reason: str): + def test_method(self: unittest.TestCase): + self.skipTest(reason) + + return test_method diff --git a/pyomo/contrib/solver/test/capability.py b/pyomo/contrib/solver/test/capability.py new file mode 100644 index 00000000000..a7b46b69d98 --- /dev/null +++ b/pyomo/contrib/solver/test/capability.py @@ -0,0 +1,359 @@ +from collections.abc import Set +from dataclasses import dataclass, field +from enum import Enum +from typing import Dict, Optional + + +class Capability(Enum): + """ + Solver capabilities enumeration. + + Each capability represents a specific feature that a solver may or may not support. + The testing framework uses these to determine which tests can run on which solvers. + """ + + OBJECTIVE_SENSE = 0 + OBJECTIVE_LINEAR = 1 + OBJECTIVE_QUADRATIC = 2 + OBJECTIVE_NONLINEAR = 3 + OBJECTIVE_MULTI = 4 + + VARIABLE_CONTINUOUS = 100 + VARIABLE_BINARY = 101 + VARIABLE_INTEGER = 102 + VARIABLE_SEMICONTINUOUS = 103 + VARIABLE_SEMIINTEGER = 104 + + CONSTRAINT_LINEAR_EQ = 200 + CONSTRAINT_LINEAR_GE = 201 + CONSTRAINT_LINEAR_LE = 202 + CONSTRAINT_LINEAR_LG = 203 + CONSTRAINT_LINEAR = 204 + + CONSTRAINT_QUADRATIC_EQ = 250 + CONSTRAINT_QUADRATIC_GE = 251 + CONSTRAINT_QUADRATIC_LE = 252 + CONSTRAINT_QUADRATIC_LG = 253 + CONSTRAINT_QUADRATIC = 254 + + CONSTRAINT_NONLINEAR_EQ = 300 + CONSTRAINT_NONLINEAR_GE = 301 + CONSTRAINT_NONLINEAR_LE = 302 + CONSTRAINT_NONLINEAR_LG = 303 + CONSTRAINT_NONLINEAR = 304 + + CONSTRAINT_SOS_ONE = 350 + CONSTRAINT_SOS_TWO = 351 + CONSTRAINT_SOS = 352 + + CONSTRAINT_CONIC = 400 + CONSTRAINT_COMPLEMENTARITY = 401 + + SOLUTION_VARIABLE_PRIMAL = 500 + SOLUTION_VARIABLE_DUAL = 501 + SOLUTION_VARIABLE_REDUCED_COST = 502 + SOLUTION_CONSTRAINT_DUAL = 503 + SOLUTION_CONSTRAINT_SLACK = 504 + + +class CapabilityCategory(Enum): + """High-level categories for organizing capabilities.""" + + OBJECTIVE = "objective" + VARIABLE = "variable" + CONSTRAINT = "constraint" + SOLUTION = "solution" + + +@dataclass(frozen=True) +class CapabilityMeta: + """Metadata about a capability.""" + + name: str + description: str + category: CapabilityCategory + implies: Set[Capability] = field(default_factory=set) + + +@dataclass +class CapabilityRegistryClass: + """ + Registry that maintains capability metadata and relationships. + """ + + _meta: Dict[Capability, CapabilityMeta] = field(default_factory=dict) + + def register( + self, + cap: Capability, + name: str, + description: str, + category: CapabilityCategory, + implies: Optional[Set[Capability]] = None, + ): + if implies is None: + implies = set() + self._meta[cap] = CapabilityMeta(name, description, category, implies) + + def get_meta(self, cap: Capability) -> Optional[CapabilityMeta]: + return self._meta.get(cap) + + def resolve_implications(self, caps: Set[Capability]) -> Set[Capability]: + """Recursively resolve all implied capabilities.""" + resolved = set(caps) + to_process = list(caps) + while to_process: + current = to_process.pop() + meta = self.get_meta(current) + if meta: + for implied in meta.implies: + if implied not in resolved: + resolved.add(implied) + to_process.append(implied) + return resolved + + +CapabilityRegistry = CapabilityRegistryClass() + +CapabilityRegistry.register( + Capability.OBJECTIVE_SENSE, + name="Objective Sense", + description="Supports having an objective sense.", + category=CapabilityCategory.OBJECTIVE, +) +CapabilityRegistry.register( + Capability.OBJECTIVE_LINEAR, + name="Objective Linear", + description="Supports having a linear objective.", + category=CapabilityCategory.OBJECTIVE, + implies={Capability.OBJECTIVE_SENSE}, +) +CapabilityRegistry.register( + Capability.OBJECTIVE_QUADRATIC, + name="Objective Quadratic", + description="Supports having a quadratic objective.", + category=CapabilityCategory.OBJECTIVE, + implies={Capability.OBJECTIVE_LINEAR}, +) +CapabilityRegistry.register( + Capability.OBJECTIVE_NONLINEAR, + name="Objective Nonlinear", + description="Supports having a nonlinear objective.", + category=CapabilityCategory.OBJECTIVE, + implies={Capability.OBJECTIVE_QUADRATIC}, +) +CapabilityRegistry.register( + Capability.OBJECTIVE_MULTI, + name="Objective Multi", + description="Supports having a multi-objective.", + category=CapabilityCategory.OBJECTIVE, +) +CapabilityRegistry.register( + Capability.VARIABLE_CONTINUOUS, + name="Variable Continuous", + description="Supports continuous variables.", + category=CapabilityCategory.VARIABLE, +) +CapabilityRegistry.register( + Capability.VARIABLE_BINARY, + name="Variable Binary", + description="Supports binary variables.", + category=CapabilityCategory.VARIABLE, +) +CapabilityRegistry.register( + Capability.VARIABLE_INTEGER, + name="Variable Integer", + description="Supports integer variables.", + category=CapabilityCategory.VARIABLE, +) +CapabilityRegistry.register( + Capability.VARIABLE_SEMICONTINUOUS, + name="Variable Semicontinuous", + description="Supports semicontinuous variables.", + category=CapabilityCategory.VARIABLE, +) +CapabilityRegistry.register( + Capability.VARIABLE_SEMIINTEGER, + name="Variable Semiinteger", + description="Supports semi-integer variables.", + category=CapabilityCategory.VARIABLE, +) +CapabilityRegistry.register( + Capability.CONSTRAINT_LINEAR_EQ, + name="Constraint Linear Equality", + description="Supports linear equality constraints.", + category=CapabilityCategory.CONSTRAINT, +) +CapabilityRegistry.register( + Capability.CONSTRAINT_LINEAR_LE, + name="Constraint Linear Less Than or Equal", + description="Supports linear less than or equal constraints.", + category=CapabilityCategory.CONSTRAINT, +) +CapabilityRegistry.register( + Capability.CONSTRAINT_LINEAR_GE, + name="Constraint Linear Greater Than or Equal", + description="Supports linear greater than or equal constraints.", + category=CapabilityCategory.CONSTRAINT, +) +CapabilityRegistry.register( + Capability.CONSTRAINT_LINEAR_LG, + name="Constraint Linear Range", + description="Supports linear range constraints.", + category=CapabilityCategory.CONSTRAINT, + implies={Capability.CONSTRAINT_LINEAR_LE, Capability.CONSTRAINT_LINEAR_GE}, +) +CapabilityRegistry.register( + Capability.CONSTRAINT_LINEAR, + name="Constraint Linear", + description="Supports linear constraints.", + category=CapabilityCategory.CONSTRAINT, + implies={Capability.CONSTRAINT_LINEAR_EQ, Capability.CONSTRAINT_LINEAR_LG}, +) +CapabilityRegistry.register( + Capability.CONSTRAINT_QUADRATIC_EQ, + name="Constraint Quadratic Equality", + description="Supports quadratic equality constraints.", + category=CapabilityCategory.CONSTRAINT, + implies={Capability.CONSTRAINT_LINEAR_EQ}, +) +CapabilityRegistry.register( + Capability.CONSTRAINT_QUADRATIC_LE, + name="Constraint Quadratic Less Than or Equal", + description="Supports quadratic less than or equal constraints.", + category=CapabilityCategory.CONSTRAINT, + implies={Capability.CONSTRAINT_LINEAR_LE}, +) +CapabilityRegistry.register( + Capability.CONSTRAINT_QUADRATIC_GE, + name="Constraint Quadratic Greater Than or Equal", + description="Supports quadratic greater than or equal constraints.", + category=CapabilityCategory.CONSTRAINT, + implies={Capability.CONSTRAINT_LINEAR_GE}, +) +CapabilityRegistry.register( + Capability.CONSTRAINT_QUADRATIC_LG, + name="Constraint Quadratic Range", + description="Supports quadratic range constraints.", + category=CapabilityCategory.CONSTRAINT, + implies={ + Capability.CONSTRAINT_LINEAR_LG, + Capability.CONSTRAINT_QUADRATIC_LE, + Capability.CONSTRAINT_QUADRATIC_GE, + }, +) +CapabilityRegistry.register( + Capability.CONSTRAINT_QUADRATIC, + name="Constraint Quadratic", + description="Supports quadratic constraints.", + category=CapabilityCategory.CONSTRAINT, + implies={ + Capability.CONSTRAINT_LINEAR, + Capability.CONSTRAINT_QUADRATIC_EQ, + Capability.CONSTRAINT_QUADRATIC_LG, + }, +) +CapabilityRegistry.register( + Capability.CONSTRAINT_NONLINEAR_EQ, + name="Constraint Nonlinear Equality", + description="Supports nonlinear equality constraints.", + category=CapabilityCategory.CONSTRAINT, + implies={Capability.CONSTRAINT_QUADRATIC_EQ}, +) +CapabilityRegistry.register( + Capability.CONSTRAINT_NONLINEAR_LE, + name="Constraint Nonlinear Less Than or Equal", + description="Supports nonlinear less than or equal constraints.", + category=CapabilityCategory.CONSTRAINT, + implies={Capability.CONSTRAINT_QUADRATIC_LE}, +) +CapabilityRegistry.register( + Capability.CONSTRAINT_NONLINEAR_GE, + name="Constraint Nonlinear Greater Than or Equal", + description="Supports nonlinear greater than or equal constraints.", + category=CapabilityCategory.CONSTRAINT, + implies={Capability.CONSTRAINT_QUADRATIC_GE}, +) +CapabilityRegistry.register( + Capability.CONSTRAINT_NONLINEAR_LG, + name="Constraint Nonlinear Range", + description="Supports nonlinear range constraints.", + category=CapabilityCategory.CONSTRAINT, + implies={ + Capability.CONSTRAINT_QUADRATIC_LG, + Capability.CONSTRAINT_NONLINEAR_LE, + Capability.CONSTRAINT_NONLINEAR_GE, + }, +) +CapabilityRegistry.register( + Capability.CONSTRAINT_NONLINEAR, + name="Constraint Nonlinear", + description="Supports nonlinear constraints.", + category=CapabilityCategory.CONSTRAINT, + implies={ + Capability.CONSTRAINT_QUADRATIC, + Capability.CONSTRAINT_NONLINEAR_LG, + Capability.CONSTRAINT_NONLINEAR_EQ, + }, +) +CapabilityRegistry.register( + Capability.CONSTRAINT_SOS_ONE, + name="Constraint SOS1", + description="Supports SOS1 constraints.", + category=CapabilityCategory.CONSTRAINT, +) +CapabilityRegistry.register( + Capability.CONSTRAINT_SOS_TWO, + name="Constraint SOS2", + description="Supports SOS2 constraints.", + category=CapabilityCategory.CONSTRAINT, +) +CapabilityRegistry.register( + Capability.CONSTRAINT_SOS, + name="Constraint SOS", + description="Supports SOS constraints.", + category=CapabilityCategory.CONSTRAINT, + implies={Capability.CONSTRAINT_SOS_ONE, Capability.CONSTRAINT_SOS_TWO}, +) +CapabilityRegistry.register( + Capability.CONSTRAINT_CONIC, + name="Constraint Conic", + description="Supports conic constraints.", + category=CapabilityCategory.CONSTRAINT, +) +CapabilityRegistry.register( + Capability.CONSTRAINT_COMPLEMENTARITY, + name="Constraint Complementarity", + description="Supports complementarity constraints.", + category=CapabilityCategory.CONSTRAINT, +) +CapabilityRegistry.register( + Capability.SOLUTION_VARIABLE_PRIMAL, + name="Solution Variable Primal", + description="Supports primal variable solutions.", + category=CapabilityCategory.SOLUTION, +) +CapabilityRegistry.register( + Capability.SOLUTION_VARIABLE_DUAL, + name="Solution Variable Dual", + description="Supports dual variable solutions.", + category=CapabilityCategory.SOLUTION, +) +CapabilityRegistry.register( + Capability.SOLUTION_VARIABLE_REDUCED_COST, + name="Solution Variable Reduced Cost", + description="Supports reduced cost for variable solutions.", + category=CapabilityCategory.SOLUTION, +) +CapabilityRegistry.register( + Capability.SOLUTION_CONSTRAINT_DUAL, + name="Solution Constraint Dual", + description="Supports dual constraint solutions.", + category=CapabilityCategory.SOLUTION, +) +CapabilityRegistry.register( + Capability.SOLUTION_CONSTRAINT_SLACK, + name="Solution Constraint Slack", + description="Supports slack for constraint solutions.", + category=CapabilityCategory.SOLUTION, +) diff --git a/pyomo/contrib/solver/test/dual.py b/pyomo/contrib/solver/test/dual.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/pyomo/contrib/solver/test/linear.py b/pyomo/contrib/solver/test/linear.py new file mode 100644 index 00000000000..77c3b0cb399 --- /dev/null +++ b/pyomo/contrib/solver/test/linear.py @@ -0,0 +1,45 @@ +from pyomo.contrib.solver.common.results import SolutionStatus +from pyomo.common import unittest +from pyomo.contrib.solver.common.base import SolverBase +import pyomo.environ as pyo + + +from .registry import SolverCapabilityRegistry, SolverTestRegistry +from .capability import Capability + + +@SolverTestRegistry.requires(Capability.OBJECTIVE_LINEAR) +@SolverTestRegistry.requires(Capability.VARIABLE_CONTINUOUS) +@SolverTestRegistry.requires(Capability.CONSTRAINT_LINEAR_EQ) +@SolverTestRegistry.requires(Capability.SOLUTION_VARIABLE_PRIMAL) +@SolverTestRegistry.tags("linear", "basic") +def test_linear_equality(test_case: unittest.TestCase, opt_cls: type[SolverBase]): + """ + A simple linear programming test with equality constraints. + Minimize: x + y + Subject to: x + 2y = 1 + x, y >= 0 + Optimal solution: x = 0, y = 0.5, objective = 0.5 + """ + + m = pyo.ConcreteModel() + m.x = pyo.Var(bounds=(0, None)) + m.y = pyo.Var(bounds=(0, None)) + m.obj = pyo.Objective(expr=m.x + m.y) + m.c = pyo.Constraint(expr=m.x + 2 * m.y == 1) + + opt = opt_cls() + + results = opt.solve(m) + + test_case.assertEqual(results.solution_status, SolutionStatus.optimal) + test_case.assertEqual(results.incumbent_objective, 0.5) + test_case.assertAlmostEqual(pyo.value(m.x), 0.0) + test_case.assertAlmostEqual(pyo.value(m.y), 0.5) + + if SolverCapabilityRegistry.supports( + opt_cls, Capability.SOLUTION_VARIABLE_REDUCED_COST + ): + reduced_costs = results.solution_loader.get_reduced_costs() + test_case.assertAlmostEqual(reduced_costs[m.x], 0.5) + test_case.assertAlmostEqual(reduced_costs[m.y], 0.0) diff --git a/pyomo/contrib/solver/test/nonlinear.py b/pyomo/contrib/solver/test/nonlinear.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/pyomo/contrib/solver/test/quadratic.py b/pyomo/contrib/solver/test/quadratic.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/pyomo/contrib/solver/test/registry.py b/pyomo/contrib/solver/test/registry.py new file mode 100644 index 00000000000..8c786cd6a4d --- /dev/null +++ b/pyomo/contrib/solver/test/registry.py @@ -0,0 +1,154 @@ +from collections.abc import Callable, MutableSet, Set +from dataclasses import dataclass, field +import re +from typing import Dict, List, Optional, Tuple, Type, Union + +from pyomo.common import unittest +from pyomo.contrib.solver.common.base import SolverBase +from .capability import Capability, CapabilityRegistry + + +class SolverCapabilityRegistryClass: + _caps: Dict[Type[SolverBase], MutableSet[Capability]] + + def __init__(self): + self._caps = {} + + def register(self, opt_cls: Type[SolverBase], *caps: Capability): + if issubclass(opt_cls, SolverBase) is False: + msg = f"{opt_cls} is not a subclass of SolverBase" + raise TypeError(msg) + + if opt_cls not in self._caps: + self._caps[opt_cls] = set() + + resolved_caps = CapabilityRegistry.resolve_implications(set(caps)) + for cap in resolved_caps: + self._caps[opt_cls].add(cap) + + def supports(self, opt_cls: Type[SolverBase], *caps: Capability) -> bool: + if opt_cls not in self._caps: + return False + return all(cap in self._caps[opt_cls] for cap in caps) + + def get_missing_caps( + self, opt_cls: Type[SolverBase], *caps: Capability + ) -> Set[Capability]: + if opt_cls not in self._caps: + return set(caps) + return {cap for cap in caps if cap not in self._caps[opt_cls]} + + +SolverCapabilityRegistry = SolverCapabilityRegistryClass() + + +@dataclass +class SolverTestMeta: + """Metadata for solver tests.""" + + func: Callable[[unittest.TestCase, type[SolverBase]], None] + reqs: MutableSet[Capability] = field(default_factory=set) + skip_reason: Optional[str] = field(default=None) + tags: MutableSet[str] = field(default_factory=set) + + def can_run_on(self, opt_cls: Type[SolverBase]) -> Tuple[bool, Optional[str]]: + if self.skip_reason is not None: + return False, self.skip_reason + + if SolverCapabilityRegistry.supports(opt_cls, *self.reqs): + return True, None + + missing = SolverCapabilityRegistry.get_missing_caps(opt_cls, *self.reqs) + msg = f"Solver {opt_cls.__name__} does not support required capabilities: {missing}" + return False, msg + + +class SolverTestFilter: + def __init__( + self, + include: Optional[List[Union[str, re.Pattern]]] = None, + exclude: Optional[List[Union[str, re.Pattern]]] = None, + include_tags: Optional[List[str]] = None, + exclude_tags: Optional[List[str]] = None, + ): + self.include = include + self.exclude = exclude or [] + self.include_tags = set(include_tags) if include_tags is not None else None + self.exclude_tags = set(exclude_tags or []) + + def should_include(self, test_name: str, test_meta: SolverTestMeta) -> bool: + if any(re.match(pat, test_name) for pat in self.exclude): + return False + + if self.include is not None and not any(re.match(pat, test_name) for pat in self.include): + return False + + if any(tag in self.exclude_tags for tag in test_meta.tags): + return False + + if self.include_tags is not None and not any( + tag in self.include_tags for tag in test_meta.tags + ): + return False + + return True + + +class SolverTestRegistryClass: + _tests: Dict[str, SolverTestMeta] + + def __init__(self): + self._tests = {} + + def _get_or_create_test_meta(self, func: Callable) -> SolverTestMeta: + name = func.__name__ + if name not in self._tests: + self._tests[name] = SolverTestMeta(func=func) + return self._tests[name] + + def requires(self, *caps: Capability): + def decorator(func: Callable): + test_meta = self._get_or_create_test_meta(func) + for cap in caps: + test_meta.reqs.add(cap) + return func + + return decorator + + def tags(self, *tags: str): + def decorator(func: Callable): + test_meta = self._get_or_create_test_meta(func) + for tag in tags: + test_meta.tags.add(tag) + return func + + return decorator + + def skip_if(self, condition: Union[bool, Callable[[], bool]], reason: str = ""): + def decorator(func: Callable): + test_meta = self._get_or_create_test_meta(func) + should_skip = condition() if callable(condition) else condition + if should_skip: + test_meta.skip_reason = reason or "Conditional skip" + return func + + return decorator + + def register(self): + def decorator(func: Callable): + self._get_or_create_test_meta(func) + return func + + return decorator + + def get_filtered_tests( + self, test_filter: SolverTestFilter + ) -> Dict[str, SolverTestMeta]: + return { + name: meta + for name, meta in self._tests.items() + if test_filter.should_include(name, meta) + } + + +SolverTestRegistry = SolverTestRegistryClass() diff --git a/pyomo/contrib/solver/tests/solvers/test_solver.py b/pyomo/contrib/solver/tests/solvers/test_solver.py new file mode 100644 index 00000000000..a203510d988 --- /dev/null +++ b/pyomo/contrib/solver/tests/solvers/test_solver.py @@ -0,0 +1,55 @@ +from pyomo.common import unittest + +from pyomo.contrib.solver.test import add_tests +from pyomo.contrib.solver.test.capability import Capability +from pyomo.contrib.solver.test.registry import SolverCapabilityRegistry +from pyomo.contrib.solver.solvers.gurobi_direct import GurobiDirect +from pyomo.contrib.solver.solvers.gurobi_persistent import GurobiPersistent +from pyomo.contrib.solver.solvers.ipopt import Ipopt + + +SolverCapabilityRegistry.register( + GurobiDirect, + Capability.OBJECTIVE_QUADRATIC, + Capability.VARIABLE_CONTINUOUS, + Capability.VARIABLE_INTEGER, + Capability.VARIABLE_BINARY, + Capability.CONSTRAINT_LINEAR, + Capability.CONSTRAINT_QUADRATIC_GE, + Capability.SOLUTION_VARIABLE_PRIMAL, + Capability.SOLUTION_VARIABLE_REDUCED_COST, + Capability.SOLUTION_CONSTRAINT_DUAL, +) +SolverCapabilityRegistry.register( + GurobiPersistent, + Capability.OBJECTIVE_QUADRATIC, + Capability.VARIABLE_CONTINUOUS, + Capability.VARIABLE_INTEGER, + Capability.VARIABLE_BINARY, + Capability.CONSTRAINT_LINEAR, + Capability.CONSTRAINT_QUADRATIC_GE, + Capability.SOLUTION_VARIABLE_PRIMAL, + Capability.SOLUTION_VARIABLE_REDUCED_COST, + Capability.SOLUTION_CONSTRAINT_DUAL, +) +SolverCapabilityRegistry.register( + Ipopt, + Capability.OBJECTIVE_NONLINEAR, + Capability.VARIABLE_CONTINUOUS, + Capability.VARIABLE_INTEGER, + Capability.VARIABLE_BINARY, + Capability.CONSTRAINT_NONLINEAR, + Capability.SOLUTION_VARIABLE_PRIMAL, + Capability.SOLUTION_VARIABLE_REDUCED_COST, + Capability.SOLUTION_CONSTRAINT_DUAL, +) + + +class TestSolvers(unittest.TestCase): + pass + + +add_tests(TestSolvers, GurobiDirect) +add_tests(TestSolvers, GurobiPersistent) +add_tests(TestSolvers, Ipopt, warn_unavailable=True) +