KarrLab/conv_opt

View on GitHub
conv_opt/solver/xpress.py

Summary

Maintainability
F
3 days
Test Coverage
F
8%
""" FICO XPRESS module

:Author: Jonathan Karr <jonrkarr@gmail.com>
:Date: 2017-11-22
:Copyright: 2017, Karr Lab
:License: MIT
"""

from ..core import (ModelType, ObjectiveDirection, Presolve,
                    SolveOptions, Solver, StatusCode, VariableType, Verbosity,
                    Constraint, LinearTerm, Model, QuadraticTerm, Term, Variable, Result, ConvOptError,
                    SolverModel)
try:
    import capturer
except ModuleNotFoundError:  # pragma: no cover
    capturer = None  # pragma: no cover
import numpy
import sys
try:
    import xpress
except (ImportError, RuntimeError):  # pragma: no cover
    import warnings  # pragma: no cover
    warnings.warn('FICO XPRESS is not installed', UserWarning)  # pragma: no cover


class XpressModel(SolverModel):
    """ FICO XPRESS solver """

    def load(self, conv_opt_model):
        """ Load a model to XPRESS' data structure

        Args:
            conv_opt_model (:obj:`Model`): model

        Returns:
            :obj:`object`: the model in XPRESS' data structure

        Raises:
            :obj:`ConvOptError`: if the presolve mode is unsupported, a variable has an unsupported type,
                an objective has an unsupported term, a constraint
                has an unsupported term, or a constraint is unbounded
        """

        solver_model = xpress.problem(name=conv_opt_model.name)

        # variables
        for variable in conv_opt_model.variables:
            if variable.type == VariableType.binary:
                vartype = xpress.binary
            elif variable.type == VariableType.integer:
                vartype = xpress.integer
            elif variable.type == VariableType.continuous:
                vartype = xpress.continuous
            elif variable.type == VariableType.semi_integer:
                vartype = xpress.semiinteger
            elif variable.type == VariableType.semi_continuous:
                vartype = xpress.semicontinuous
            elif variable.type == VariableType.partially_integer:
                vartype = xpress.partiallyinteger
            else:
                raise ConvOptError('Unsupported variable of type "{}"'.format(variable.type))

            if variable.lower_bound is None:
                lb = -float('inf')
            else:
                lb = variable.lower_bound

            if variable.upper_bound is None:
                ub = float('inf')
            else:
                ub = variable.upper_bound

            solver_model.addVariable(xpress.var(name=variable.name, lb=lb, ub=ub, vartype=vartype))

        # objective
        if conv_opt_model.objective_direction in [ObjectiveDirection.max, ObjectiveDirection.maximize]:
            sense = xpress.maximize
        elif conv_opt_model.objective_direction in [ObjectiveDirection.min, ObjectiveDirection.minimize]:
            sense = xpress.minimize
        else:
            raise ConvOptError('Unsupported objective direction "{}"'.format(conv_opt_model.objective_direction))

        terms = []
        for term in conv_opt_model.objective_terms:
            if isinstance(term, LinearTerm):
                i_variable = conv_opt_model.variables.index(term.variable)
                terms.append(term.coefficient * solver_model.getVariable(index=i_variable))
            elif isinstance(term, QuadraticTerm):
                i_variable_1 = conv_opt_model.variables.index(term.variable_1)
                i_variable_2 = conv_opt_model.variables.index(term.variable_2)
                terms.append(term.coefficient
                             * solver_model.getVariable(index=i_variable_1)
                             * solver_model.getVariable(index=i_variable_2))
            else:
                raise ConvOptError('Unsupported objective term of type "{}"'.format(term.__class__.__name__))

        solver_model.setObjective(xpress.Sum(terms), sense)

        # constraints
        for constraint in conv_opt_model.constraints:
            body = []
            for term in constraint.terms:
                if isinstance(term, LinearTerm):
                    i_variable = conv_opt_model.variables.index(term.variable)
                    body.append(term.coefficient * solver_model.getVariable(index=i_variable))
                # elif isinstance(term, QuadraticTerm):
                    # :todo: implement quadratic constraints
                    # raise ConvOptError('Unsupported constraint term of type "{}"'.format(term.__class__.__name__))
                else:
                    raise ConvOptError('Unsupported constraint term of type "{}"'.format(term.__class__.__name__))

            if constraint.lower_bound is None and constraint.upper_bound is None:
                raise ConvOptError('Constraints must have at least one bound')
            elif constraint.lower_bound is None:
                solver_model.addConstraint(xpress.constraint(body=xpress.Sum(
                    body), ub=constraint.upper_bound, sense=xpress.leq, name=constraint.name))
            elif constraint.upper_bound is None:
                solver_model.addConstraint(xpress.constraint(body=xpress.Sum(
                    body), lb=constraint.lower_bound, sense=xpress.geq, name=constraint.name))
            elif constraint.lower_bound == constraint.upper_bound:
                solver_model.addConstraint(xpress.constraint(body=xpress.Sum(
                    body), rhs=constraint.lower_bound, sense=xpress.eq, name=constraint.name))
            else:
                # :todo: figure out how to use xpress.range type
                solver_model.addConstraint(xpress.constraint(body=xpress.Sum(body), sense=xpress.geq,
                                                             rhs=constraint.lower_bound, name=constraint.name + '__lower__'))
                solver_model.addConstraint(xpress.constraint(body=xpress.Sum(body), sense=xpress.leq,
                                                             rhs=constraint.upper_bound, name=constraint.name + '__upper__'))

        return solver_model

    def solve(self):
        """ Solve the model

        Returns:
            :obj:`Result`: result
        """

        model = self._model

        # verbosity
        if self._options.verbosity == Verbosity.off:
            model.setlogfile('xpress.log')

        # presolve
        if self._options.presolve == Presolve.on:
            if capturer and self._options.verbosity.value <= Verbosity.error.value:
                capture_output = capturer.CaptureOutput(merged=False, relay=False)
                capture_output.start_capture()

            model.presolve()

            if capturer and self._options.verbosity.value <= Verbosity.error.value:
                capture_output.finish_capture()

                err = capture_output.stderr.get_text()
                if err and self._options.verbosity == Verbosity.error:
                    sys.stderr.write(err)
            else:
                model.presolve()
        elif self._options.presolve != Presolve.off:
            raise ConvOptError('Unsupported presolve mode "{}"'.format(self._options.presolve))

        # tune
        if self._options.tune:
            raise ConvOptError('Unsupported tuning mode "{}"'.format(self._options.tune))

        # solve
        model.solve()

        # get status
        if self.is_mixed_integer():
            if model.getProbStatus() == xpress.mip_optimal:
                status_code = StatusCode.optimal
            elif model.getProbStatus() == xpress.mip_infeas:
                status_code = StatusCode.infeasible
            else:
                status_code = StatusCode.other
        elif self.is_non_linear():
            if model.getProbStatus() in [xpress.nlp_locally_optimal]:
                status_code = StatusCode.optimal
            elif model.getProbStatus() in [xpress.lp_infeas, xpress.nlp_infeasible]:
                status_code = StatusCode.infeasible
            else:
                status_code = StatusCode.other
        else:
            if model.getProbStatus() == xpress.lp_optimal:
                status_code = StatusCode.optimal
            elif model.getProbStatus() == xpress.lp_infeas:
                status_code = StatusCode.infeasible
            else:
                status_code = StatusCode.other

        status_message = model.getProbStatusString()

        # get solution
        if status_code == StatusCode.optimal:
            value = model.getObjVal()
            primals = numpy.array([model.getSolution(var) for var in model.getVariable()])
            if self.is_mixed_integer():
                reduced_costs = numpy.full((len(model.getVariable()), ), numpy.nan)
                duals = numpy.array([numpy.nan
                                     for constraint in model.getConstraint() if not constraint.name.endswith('__upper__')])
            else:
                reduced_costs = numpy.array([model.getRCost(var) for var in model.getVariable()])
                duals = numpy.array([model.getDual(constraint)
                                     for constraint in model.getConstraint() if not constraint.name.endswith('__upper__')])
        else:
            value = numpy.nan
            primals = numpy.full((len(model.getVariable()), ), numpy.nan)
            reduced_costs = numpy.full((len(model.getVariable()), ), numpy.nan)
            duals = numpy.array([numpy.nan
                                 for constraint in model.getConstraint() if not constraint.name.endswith('__upper__')])

        return Result(status_code, status_message, value, primals, reduced_costs, duals)

    def get_stats(self):
        """ Get diagnostic information about the model

        Returns:
            :obj:`dict`: diagnostic information about the model
        """
        model = self._model

        sol = model.getSolution()

        stats = {}

        stats['slacks'] = [numpy.nan] * len(model.getConstraint())
        model.calcslacks(sol, stats['slacks'])

        if not self.is_non_linear():
            stats['row_status'] = [0] * len(model.getConstraint())
            stats['col_status'] = [0] * len(model.getVariable())
            model.getbasis(stats['row_status'], stats['col_status'])

        stats['primal_infeasible_variables'] = []
        stats['primal_infeasible_constraints'] = []
        stats['dual_infeasible_constraints'] = []
        stats['dual_infeasible_variables'] = []

        if not self.is_mixed_integer():
            model.getinfeas(
                stats['primal_infeasible_variables'], stats['primal_infeasible_constraints'],
                stats['dual_infeasible_constraints'], stats['dual_infeasible_variables'])

            duals = model.getDual()
            stats['abs_primal_infeas'] = model.calcsolinfo(sol, duals, xpress.solinfo_absprimalinfeas)
            stats['rel_primal_infeas'] = model.calcsolinfo(sol, duals, xpress.solinfo_relprimalinfeas)
            stats['abs_dual_infeas'] = model.calcsolinfo(sol, duals, xpress.solinfo_absdualinfeas)
            stats['rel_dual_infeas'] = model.calcsolinfo(sol, duals, xpress.solinfo_reldualinfeas)
            stats['max_mip_fractional'] = model.calcsolinfo(sol, duals, xpress.solinfo_maxmipfractional)

        stats['sos'] = model.getSOS()

        return stats

    def is_mixed_integer(self):
        """ Determine if the model has at least one binary or integer variable

        Returns:
            :obj:`bool`: :obj:`True` if the model has at least one binary or integer variable, and false otherwise
        """
        for variable in self._model.getVariable():
            if variable.vartype in [xpress.binary, xpress.integer, xpress.semiinteger, xpress.partiallyinteger]:
                return True
        return False

    def is_non_linear(self):
        """ Determine if the model is non-linear

        Returns:
            :obj:`bool`: :obj:`True` if the model is non-linear and false otherwise
        """
        n_var = len(self._model.getVariable())
        size = int((n_var + 1) * n_var / 2)
        mstart = [0.] * size
        mclind = [0.] * size
        dobjval = [0.] * size
        self._model.getmqobj(mstart, mclind, dobjval, size, 0, n_var - 1)
        return len(dobjval) != 0