KarrLab/conv_opt

View on GitHub
conv_opt/solver/cylp.py

Summary

Maintainability
C
7 hrs
Test Coverage
F
7%
""" CyLP 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 cylp.cy
    import cylp.py.modeling
except ImportError:  # pragma: no cover
    import warnings  # pragma: no cover
    warnings.warn('CyLP is not installed', UserWarning)  # pragma: no cover
import numpy


class CylpModel(SolverModel):
    """ CyLP solver """

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

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

        Returns:
            :obj:`cylp.cy.CyClpSimplex`: the model in CyLP's 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, a constraint is unbounded, or the model is not of a supported type
        """
        from cylp.py.modeling.CyLPModel import getCoinInfinity
        inf = getCoinInfinity()

        solver_model = cylp.cy.CyClpSimplex()

        # variables
        for i_variable, variable in enumerate(conv_opt_model.variables):
            if variable.type in [VariableType.binary, VariableType.integer]:
                is_int = True
            elif variable.type == VariableType.continuous:
                is_int = False
            else:
                raise ConvOptError('Unsupported variable of type "{}"'.format(variable.type))
            solver_variable = solver_model.addVariable(variable.name or '', 1, isInt=is_int)
            solver_model.setVariableName(i_variable, variable.name or '')

            lb = -inf
            ub = inf

            if variable.lower_bound is not None:
                lb = max(-inf, variable.lower_bound)

            if variable.upper_bound is not None:
                ub = min(inf, variable.upper_bound)

            if variable.type == VariableType.binary:
                lb = max(lb, 0)
                ub = min(ub, 1)

            solver_variable.lower = numpy.array([lb])
            solver_variable.upper = numpy.array([ub])

        # objective
        objective = numpy.zeros((len(conv_opt_model.variables), ))
        for term in conv_opt_model.objective_terms:
            if isinstance(term, LinearTerm):
                i_variable = conv_opt_model.variables.index(term.variable)
                objective[i_variable] += [term.coefficient]
            else:
                raise ConvOptError('Unsupported objective term of type "{}"'.format(term.__class__.__name__))
        solver_model.objective = objective

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

        # constraints
        for i_constraint, constraint in enumerate(conv_opt_model.constraints):
            expr = None
            for term in constraint.terms:
                if isinstance(term, LinearTerm):
                    term_expr = term.coefficient * solver_model.variables[conv_opt_model.variables.index(term.variable)]
                    if expr is None:
                        expr = term_expr
                    else:
                        expr += term_expr
                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:
                ub = cylp.py.modeling.CyLPArray([constraint.upper_bound])
                solver_model += expr <= ub
            elif constraint.upper_bound is None:
                lb = cylp.py.modeling.CyLPArray([constraint.lower_bound])
                solver_model += expr >= lb
            elif constraint.lower_bound == constraint.upper_bound:
                lb = cylp.py.modeling.CyLPArray([constraint.lower_bound])
                solver_model += expr == lb
            else:
                lb = cylp.py.modeling.CyLPArray([constraint.lower_bound])
                ub = cylp.py.modeling.CyLPArray([constraint.upper_bound])
                solver_model += lb <= expr <= ub

            solver_model.setConstraintName(i_constraint, constraint.name or '')

        # return model
        return solver_model

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

        Returns:
            :obj:`Result`: result
        """
        model = self._model

        # presolve
        if self._options.presolve == Presolve.on:
            presolve = 'on'
        elif self._options.presolve == Presolve.off:
            presolve = 'off'
        else:
            raise ConvOptError('Unsupported presolve mode "{}"'.format(self._options.presolve))

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

        # solve
        if self._options.verbosity.value >= Verbosity.status.value:
            model.logLevel = 1
        else:
            model.logLevel = 0
        model.initialSolve(presolve=presolve)

        # get solution
        if model.getStatusCode() == 0:
            status_code = StatusCode.optimal
        elif model.getStatusCode() == 1:
            status_code = StatusCode.infeasible
        else:
            status_code = StatusCode.other
        status_message = model.getStatusString()

        if status_code == StatusCode.optimal:
            value = model.objectiveValue
            primals = numpy.array([model.primalVariableSolution[name][0] for name in model.variableNames])
            duals = numpy.array([model.dualConstraintSolution[c.name] for c in model.constraints])
        else:
            value = numpy.nan
            primals = numpy.full((len(model.variables), ), numpy.nan)
            duals = numpy.full((len(model.constraints), ), numpy.nan)

        reduced_costs = numpy.full((len(model.variables), ), numpy.nan)

        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

        variable_statuses = []
        for variable in model.variables:
            variable_status = model.getVariableStatus(variable)
            variable_statuses.append(variable_status)

        constraint_statuses = []
        for constraint in model.constraints:
            constraint_status = model.getConstraintStatus(constraint.name)
            constraint_statuses.append(constraint_status)

        cbc_model = model.getCbcModel()

        return {
            'variable_statuses': variable_statuses,
            'constraint_statuses': constraint_statuses,
            'pivot_variable': model.getPivotVariable(),
            'pivot_constraint': model.pivotRow(),
            'complementarity_list': model.getComplementarityList(),
            'is_relaxation_abandoned': cbc_model.isRelaxationAbondoned(),
            'is_relaxation_dual_infeasible': cbc_model.isRelaxationDualInfeasible(),
            'is_relaxation_infeasible': cbc_model.isRelaxationInfeasible(),
            'is_relaxation_optimal': cbc_model.isRelaxationOptimal(),
        }