Esempio n. 1
0
    def solve_via_data(self, data, warm_start, verbose, solver_opts,
                       solver_cache=None):
        """Returns the result of the call to the solver.

        Parameters
        ----------
        data : dict
            Data generated via an apply call.
        warm_start : Bool
            Whether to warm_start diffcp.
        verbose : Bool
            Control the verbosity.
        solver_opts : dict
            SCS-specific solver options.

        Returns
        -------
        The result returned by a call to scs.solve().
        """
        import diffcp
        A = data[s.A]
        b = data[s.B]
        c = data[s.C]
        cones = scs_conif.dims_to_solver_dict(data[ConicSolver.DIMS])
        # Default to eps = 1e-4 instead of 1e-3.
        solver_opts['eps'] = solver_opts.get('eps', 1e-4)
        results = diffcp.solve_and_derivative_internal(
            A, b, c, cones, verbose=verbose, raise_on_error=False,
            **solver_opts)
        if solver_cache is not None:
            solver_cache[self.name()] = results
        return results
Esempio n. 2
0
    def __init__(self, problem, parameters, variables):
        """Construct a CvxpyLayer

        Args:
          problem: The CVXPY problem; must be DPP.
          parameters: A list of CVXPY Parameters in the problem; the order
                      of the Parameters determines the order in which parameter
                      values must be supplied in the forward pass. Must include
                      every parameter involved in problem.
          variables: A list of CVXPY Variables in the problem; the order of the
                     Variables determines the order of the optimal variable
                     values returned from the forward pass.
        """
        super(CvxpyLayer, self).__init__()

        if not problem.is_dpp():
            raise ValueError('Problem must be DPP.')
        if not set(problem.parameters()) == set(parameters):
            raise ValueError("The layer's parameters must exactly match "
                             "problem.parameters")
        if not set(variables).issubset(set(problem.variables())):
            raise ValueError("Argument variables must be a subset of "
                             "problem.variables")

        self.param_order = parameters
        self.param_ids = [p.id for p in self.param_order]
        self.variables = variables
        self.var_dict = {v.id for v in self.variables}

        # Construct compiler
        data, _, _ = problem.get_problem_data(solver=cp.SCS)
        self.compiler = data[cp.settings.PARAM_PROB]
        self.cone_dims = dims_to_solver_dict(data["dims"])
Esempio n. 3
0
 def _define_data(self, data: Dict[str, Any]) -> Tuple:
     """Define data parts from the data reference."""
     c = data[s.C]
     b = data[s.B]
     A = dok_matrix(data[s.A])
     # Save the dok_matrix.
     data[s.A] = A
     dims = dims_to_solver_dict(data[s.DIMS])
     return A, b, c, dims
Esempio n. 4
0
    def solve_via_data(self, data, warm_start: bool, verbose: bool, solver_opts,
                       solver_cache=None):
        """Returns the result of the call to the solver.

        Parameters
        ----------
        data : dict
            Data generated via an apply call.
        warm_start : Bool
            Whether to warm_start diffcp.
        verbose : Bool
            Control the verbosity.
        solver_opts : dict
            SCS-specific solver options.

        Returns
        -------
        The result returned by a call to scs.solve().
        """
        import diffcp
        A = data[s.A]
        b = data[s.B]
        c = data[s.C]
        cones = scs_conif.dims_to_solver_dict(data[ConicSolver.DIMS])

        solver_opts["solve_method"] = solver_opts.get("solve_method", s.SCS)
        warm_start_tuple = None

        if solver_opts["solve_method"] == s.SCS:
            import scs
            if Version(scs.__version__) < Version('3.0.0'):
                # Default to eps = 1e-4 instead of 1e-3.
                solver_opts["eps"] = solver_opts.get("eps", 1e-4)
            else:
                solver_opts['eps_abs'] = solver_opts.get('eps_abs', 1e-5)
                solver_opts['eps_rel'] = solver_opts.get('eps_rel', 1e-5)

            if warm_start and solver_cache is not None and \
                    self.name() in solver_cache:
                warm_start_tuple = (solver_cache[self.name()]["x"],
                                    solver_cache[self.name()]["y"],
                                    solver_cache[self.name()]["s"])

        start = time.time()
        results = diffcp.solve_and_derivative_internal(
            A, b, c, cones, verbose=verbose,
            warm_start=warm_start_tuple,
            raise_on_error=False,
            **solver_opts)
        end = time.time()
        results["TOT_TIME"] = end - start
        results["solve_method"] = solver_opts["solve_method"]

        if solver_cache is not None:
            solver_cache[self.name()] = results
        return results
Esempio n. 5
0
    def solve_via_data(self,
                       data,
                       warm_start,
                       verbose,
                       solver_opts,
                       solver_cache=None):
        """Returns the result of the call to the solver.

        Parameters
        ----------
        data : dict
            Data generated via an apply call.
        warm_start : Bool
            Whether to warm_start diffcp.
        verbose : Bool
            Control the verbosity.
        solver_opts : dict
            SCS-specific solver options.

        Returns
        -------
        The result returned by a call to scs.solve().
        """
        import diffcp
        A = data[s.A]
        b = data[s.B]
        c = data[s.C]
        cones = scs_conif.dims_to_solver_dict(data[ConicSolver.DIMS])
        # This interface is tied to SCS, so force SCS for now.
        solver_opts['solve_method'] = 'SCS'
        # Default to eps = 1e-4 instead of 1e-3.
        solver_opts['eps'] = solver_opts.get('eps', 1e-4)
        warm_start_tuple = None
        if warm_start and solver_cache is not None and \
                self.name() in solver_cache:
            warm_start_tuple = (solver_cache[self.name()]["x"],
                                solver_cache[self.name()]["y"],
                                solver_cache[self.name()]["s"])
        start = time.time()
        results = diffcp.solve_and_derivative_internal(
            A,
            b,
            c,
            cones,
            verbose=verbose,
            warm_start=warm_start_tuple,
            raise_on_error=False,
            **solver_opts)
        end = time.time()
        results['TOT_TIME'] = end - start
        if solver_cache is not None:
            solver_cache[self.name()] = results
        return results
Esempio n. 6
0
    def __init__(self, problem, parameters, variables, gp=False):
        """Construct a CvxpyLayer

        Args:
          problem: The CVXPY problem; must be DPP.
          parameters: A list of CVXPY Parameters in the problem; the order
                      of the Parameters determines the order in which parameter
                      values must be supplied in the forward pass.
          variables: A list of CVXPY Variables in the problem; the order of the
                     Variables determines the order of the optimal variable
                     values returned from the forward pass.
          gp: Whether to parse the problem using DGP (True or False).
        """
        if gp:
            if not problem.is_dgp(dpp=True):
                raise ValueError('Problem must be DPP.')
        else:
            if not problem.is_dcp(dpp=True):
                raise ValueError('Problem must be DPP.')
        if set(parameters) != set(problem.parameters()):
            raise ValueError("The layer's parameters must exactly match "
                             "problem.parameters")
        if not set(variables).issubset(set(problem.variables())):
            raise ValueError('Argument `variables` must be a subset of '
                             '`problem.variables()`')
        self.params = parameters
        self.gp = gp

        if self.gp:
            for param in parameters:
                if param.value is None:
                    raise ValueError("An initial value for each parameter is "
                                     "required when gp=True.")
            data, solving_chain, _ = (
                problem.get_problem_data(solver=cp.SCS, gp=True)
            )
            self.asa_maps = data[cp.settings.PARAM_PROB]
            self.dgp2dcp = solving_chain.get(cp.reductions.Dgp2Dcp)
            self.param_ids = [p.id for p in self.asa_maps.parameters]
        else:
            data, _, _ = problem.get_problem_data(solver=cp.SCS)
            self.asa_maps = data[cp.settings.PARAM_PROB]
            self.param_ids = [p.id for p in self.params]

        self.cones = dims_to_solver_dict(data['dims'])
        self.vars = variables
    def solve_via_data(self,
                       data,
                       warm_start,
                       verbose,
                       solver_opts,
                       solver_cache=None):
        """Returns the result of the call to SuperSCS.

        Parameters
        ----------
        data : dict
            Data generated via an apply call.
        warm_start : Bool
            Whether to warm_start SuperSCS.
        verbose : Bool
            Control the verbosity.
        solver_opts : dict
            SuperSCS-specific options.

        Returns
        -------
        The result returned by a call to superscs.solve().
        """
        import superscs
        args = {"A": data[s.A], "b": data[s.B], "c": data[s.C]}
        if warm_start and solver_cache is not None and \
           self.name in solver_cache:
            args["x"] = solver_cache[self.name()]["x"]
            args["y"] = solver_cache[self.name()]["y"]
            args["s"] = solver_cache[self.name()]["s"]
        cones = dims_to_solver_dict(data[ConicSolver.DIMS])
        # settings
        user_opts = list(solver_opts.keys())
        for k in list(SuperSCS.DEFAULT_SETTINGS.keys()):
            if k not in user_opts:
                solver_opts[k] = SuperSCS.DEFAULT_SETTINGS[k]
        results = superscs.solve(args, cones, verbose=verbose, **solver_opts)
        if solver_cache is not None:
            solver_cache[self.name()] = results
        return results
Esempio n. 8
0
    def solve_via_data(self,
                       data,
                       warm_start: bool,
                       verbose: bool,
                       solver_opts,
                       solver_cache=None):

        import xpress as xp

        c = data[s.C]  # objective coefficients

        dims = dims_to_solver_dict(
            data[s.DIMS])  # contains number of columns, rows, etc.

        nrowsEQ = dims[s.EQ_DIM]
        nrowsLEQ = dims[s.LEQ_DIM]
        nrows = nrowsEQ + nrowsLEQ

        # linear constraints
        b = data[s.B][:nrows]  # right-hand side
        A = data[s.A][:nrows]  # coefficient matrix

        # Problem
        self.prob_ = xp.problem()

        mstart = makeMstart(A, len(c), 1)

        varGroups = {
        }  # If origprob is passed, used to tie IIS to original constraints
        transf2Orig = {
        }  # Ties transformation constraints to originals via varGroups
        nOrigVar = len(c)

        # Uses flat naming. Warning: this mixes
        # original with auxiliary variables.

        varnames = ['x_{0:05d}'.format(i) for i in range(len(c))]
        linRownames = ['lc_{0:05d}'.format(i) for i in range(len(b))]

        if verbose:
            self.prob_.controls.miplog = 2
            self.prob_.controls.lplog = 1
            self.prob_.controls.outputlog = 1
        else:
            self.prob_.controls.miplog = 0
            self.prob_.controls.lplog = 0
            self.prob_.controls.outputlog = 0
            self.prob_.controls.xslp_log = -1

        self.prob_.loadproblem(
            probname="CVX_xpress_conic",
            # constraint types
            qrtypes=['E'] * nrowsEQ + ['L'] * nrowsLEQ,
            rhs=b,  # rhs
            range=None,  # range
            obj=c,  # obj coeff
            mstart=mstart,  # mstart
            mnel=None,  # mnel (unused)
            # linear coefficients
            mrwind=A.indices[A.data != 0],  # row indices
            dmatval=A.data[A.data != 0],  # coefficients
            dlb=[-xp.infinity] * len(c),  # lower bound
            dub=[xp.infinity] * len(c),  # upper bound
            colnames=varnames,  # column names
            rownames=linRownames)  # row    names

        # Set variable types for discrete variables
        self.prob_.chgcoltype(
            data[s.BOOL_IDX] + data[s.INT_IDX],
            'B' * len(data[s.BOOL_IDX]) + 'I' * len(data[s.INT_IDX]))

        currow = nrows

        iCone = 0

        auxVars = set(range(nOrigVar, len(c)))

        # Conic constraints
        #
        # Quadratic objective and constraints fall in this category,
        # as all quadratic stuff is converted into a cone via a linear transformation
        for k in dims[s.SOC_DIM]:

            # k is the size of the i-th cone, where i is the index
            # within dims [s.SOC_DIM]. The cone variables in
            # CVXOPT, apparently, are separate variables that are
            # marked as conic but not shown in a cone explicitly.

            A = data[s.A][currow:currow + k].tocsr()
            b = data[s.B][currow:currow + k]
            currow += k

            # Create new (cone) variables and add them to the problem
            conevar = np.array([
                xp.var(name='cX{0:d}_{1:d}'.format(iCone, i),
                       lb=-xp.infinity if i > 0 else 0) for i in range(k)
            ])

            self.prob_.addVariable(conevar)

            initrow = self.prob_.attributes.rows

            mstart = makeMstart(A, k, 0)

            trNames = ['linT_qc{0:d}_{1:d}'.format(iCone, i) for i in range(k)]

            # Linear transformation for cone variables <--> original variables
            self.prob_.addrows(
                ['E'] * k,  # qrtypes
                b,  # rhs
                mstart,  # mstart
                A.indices[A.data != 0],  # ind
                A.data[A.data != 0],  # dmatval
                names=trNames)  # row names

            self.prob_.chgmcoef([initrow + i for i in range(k)], conevar,
                                [1] * k)

            conename = 'cone_qc{0:d}'.format(iCone)
            # Real cone on the cone variables (if k == 1 there's no
            # need for this constraint as y**2 >= 0 is redundant)
            if k > 1:
                self.prob_.addConstraint(
                    xp.constraint(constraint=xp.Sum(
                        conevar[i]**2 for i in range(1, k)) <= conevar[0]**2,
                                  name=conename))

            auxInd = list(set(A.indices) & auxVars)

            if len(auxInd) > 0:
                group = varGroups[varnames[auxInd[0]]]
                for i in trNames:
                    transf2Orig[i] = group
                transf2Orig[conename] = group

            iCone += 1

        # End of the conditional (warm-start vs. no warm-start) code,
        # set options, solve, and report.

        # Set options
        #
        # The parameter solver_opts is a dictionary that contains only
        # one key, 'solver_opt', and its value is a dictionary
        # {'control': value}, matching perfectly the format used by
        # the Xpress Python interface.

        self.prob_.setControl({
            i: solver_opts[i]
            for i in solver_opts if i in xp.controls.__dict__
        })

        if 'bargaptarget' not in solver_opts:
            self.prob_.controls.bargaptarget = 1e-30

        if 'feastol' not in solver_opts:
            self.prob_.controls.feastol = 1e-9

        # If option given, write file before solving
        if 'write_mps' in solver_opts:
            self.prob_.write(solver_opts['write_mps'])

        # Solve
        self.prob_.solve()

        results_dict = {
            'problem': self.prob_,
            'status': self.prob_.getProbStatus(),
            'obj_value': self.prob_.getObjVal(),
        }

        status_map_lp, status_map_mip = get_status_maps()

        if 'mip_' in self.prob_.getProbStatusString():
            status = status_map_mip[results_dict['status']]
        else:
            status = status_map_lp[results_dict['status']]

        results_dict[s.XPRESS_TROW] = transf2Orig

        results_dict[
            s.XPRESS_IIS] = None  # Return no IIS if problem is feasible

        if status in s.SOLUTION_PRESENT:
            results_dict['x'] = self.prob_.getSolution()
            if not (data[s.BOOL_IDX] or data[s.INT_IDX]):
                results_dict['y'] = -np.array(self.prob_.getDual())

        elif status == s.INFEASIBLE:

            # Retrieve all IIS. For LPs there can be more than one,
            # but for QCQPs there is only support for one IIS.

            iisIndex = 0

            self.prob_.iisfirst(0)  # compute all IIS

            row, col, rtype, btype, duals, rdcs, isrows, icols = [], [], [], [], [], [], [], []

            self.prob_.getiisdata(0, row, col, rtype, btype, duals, rdcs,
                                  isrows, icols)

            origrow = []
            for iRow in row:
                if iRow.name in transf2Orig:
                    name = transf2Orig[iRow.name]
                else:
                    name = iRow.name

                if name not in origrow:
                    origrow.append(name)

            results_dict[s.XPRESS_IIS] = [{
                'orig_row': origrow,
                'row': row,
                'col': col,
                'rtype': rtype,
                'btype': btype,
                'duals': duals,
                'redcost': rdcs,
                'isolrow': isrows,
                'isolcol': icols
            }]

            while self.prob_.iisnext() == 0:
                iisIndex += 1
                self.prob_.getiisdata(iisIndex, row, col, rtype, btype, duals,
                                      rdcs, isrows, icols)
                results_dict[s.XPRESS_IIS].append(
                    (row, col, rtype, btype, duals, rdcs, isrows, icols))

        # Generate solution.
        solution = {}

        status_map_lp, status_map_mip = get_status_maps()

        if data[s.BOOL_IDX] or data[s.INT_IDX]:
            solution[s.STATUS] = status_map_mip[results_dict['status']]
        else:
            solution[s.STATUS] = status_map_lp[results_dict['status']]

        if solution[s.STATUS] in s.SOLUTION_PRESENT:

            solution[s.PRIMAL] = results_dict['x']
            solution[s.VALUE] = results_dict['obj_value']

            if not (data[s.BOOL_IDX] or data[s.INT_IDX]):
                solution[s.EQ_DUAL] = results_dict['y'][0:dims[s.EQ_DIM]]
                solution[s.INEQ_DUAL] = results_dict['y'][dims[s.EQ_DIM]:]

        solution[s.XPRESS_IIS] = results_dict[s.XPRESS_IIS]
        solution[s.XPRESS_TROW] = results_dict[s.XPRESS_TROW]

        solution['getObjVal'] = self.prob_.getObjVal()

        solution[s.SOLVE_TIME] = self.prob_.attributes.time

        del self.prob_

        return solution
Esempio n. 9
0
    def solve_via_data(self,
                       data,
                       warm_start,
                       verbose,
                       solver_opts,
                       solver_cache=None):
        import cplex

        c = data[s.C]
        b = data[s.B]
        A = dok_matrix(data[s.A])
        # Save the dok_matrix.
        data[s.A] = A
        dims = dims_to_solver_dict(data[s.DIMS])

        n = c.shape[0]

        model = cplex.Cplex()
        variables = []
        # cpx_constrs will contain CpxConstr namedtuples (see above).
        cpx_constrs = []
        vtype = []
        if data[s.BOOL_IDX] or data[s.INT_IDX]:
            for i in range(n):
                # Set variable type.
                if i in data[s.BOOL_IDX]:
                    vtype.append('B')
                elif i in data[s.INT_IDX]:
                    vtype.append('I')
                else:
                    vtype.append('C')
        else:
            # If we specify types (even with 'C'), then the problem will
            # be interpreted as a MIP. Leaving vtype as an empty list
            # here, will ensure that the problem type remains an LP.
            pass
        # Add the variables in a batch
        variables = list(
            model.variables.add(
                obj=[c[i] for i in range(n)],
                lb=[-cplex.infinity] * n,  # default LB is 0
                ub=[cplex.infinity] * n,
                types="".join(vtype),
                names=["x_%d" % i for i in range(n)]))

        # Add equality constraints
        cpx_constrs += [
            _CpxConstr(_LIN, x) for x in self.add_model_lin_constr(
                model, variables, range(dims[s.EQ_DIM]), 'E', A, b)
        ]

        # Add inequality (<=) constraints
        leq_start = dims[s.EQ_DIM]
        leq_end = dims[s.EQ_DIM] + dims[s.LEQ_DIM]
        cpx_constrs += [
            _CpxConstr(_LIN, x) for x in self.add_model_lin_constr(
                model, variables, range(leq_start, leq_end), 'L', A, b)
        ]

        # Add SOC constraints
        soc_start = leq_end
        for constr_len in dims[s.SOC_DIM]:
            soc_end = soc_start + constr_len
            soc_constr, new_leq, new_vars = self.add_model_soc_constr(
                model, variables, range(soc_start, soc_end), A, b)
            cpx_constrs.append(_CpxConstr(_QUAD, soc_constr))
            cpx_constrs += [_CpxConstr(_LIN, x) for x in new_leq]
            variables += new_vars
            soc_start += constr_len

        # Set verbosity
        if not verbose:
            model.set_results_stream(None)
            model.set_warning_stream(None)
            model.set_error_stream(None)
            model.set_log_stream(None)
        else:
            # By default the output will be sent to stdout.
            pass

        # TODO: user option to not compute duals.
        model.parameters.preprocessing.qcpduals.set(
            model.parameters.preprocessing.qcpduals.values.force)

        # TODO: Parameter support is functional, but perhaps not ideal.
        # The user must pass parameter names as used in the CPLEX Python
        # API, and raw values (i.e., no enum support).
        kwargs = sorted(solver_opts.keys())
        if "cplex_params" in kwargs:
            for param, value in solver_opts["cplex_params"].items():
                try:
                    eval("model.parameters.{0}.set({1})".format(param, value))
                except AttributeError:
                    raise ValueError(
                        "invalid CPLEX parameter, value pair ({0}, {1})".
                        format(param, value))
            kwargs.remove("cplex_params")
        if "cplex_filename" in kwargs:
            filename = solver_opts["cplex_filename"]
            if filename:
                model.write(filename)
            kwargs.remove("cplex_filename")
        if s.BOOL_IDX in kwargs:
            kwargs.remove(s.BOOL_IDX)
        if s.INT_IDX in kwargs:
            kwargs.remove(s.INT_IDX)
        if kwargs:
            raise ValueError("invalid keyword-argument '{0}'".format(
                kwargs[0]))

        solution = {}
        start_time = model.get_time()
        solution[s.SOLVE_TIME] = -1
        try:
            model.solve()
            solution[s.SOLVE_TIME] = model.get_time() - start_time
            solution["value"] = model.solution.get_objective_value()
            solution["primal"] = np.array(model.solution.get_values(variables))
            solution["status"] = self._get_status(model)

            # Only add duals if not a MIP.
            if not (data[s.BOOL_IDX] or data[s.INT_IDX]):
                vals = []
                for con in cpx_constrs:
                    assert con.index is not None
                    if con.constr_type == _LIN:
                        vals.append(model.solution.get_dual_values(con.index))
                    else:
                        assert con.constr_type == _QUAD
                        # Quadratic constraints not queried directly.
                        vals.append(0.0)
                solution["y"] = -np.array(vals)
                solution[s.EQ_DUAL] = solution["y"][0:dims[s.EQ_DIM]]
                solution[s.INEQ_DUAL] = solution["y"][dims[s.EQ_DIM]:]
        except Exception:
            if solution[s.SOLVE_TIME] < 0.0:
                solution[s.SOLVE_TIME] = model.get_time() - start_time
            solution["status"] = s.SOLVER_ERROR

        return solution
Esempio n. 10
0
    def solve_via_data(self,
                       data,
                       warm_start,
                       verbose,
                       solver_opts,
                       solver_cache=None):
        """Returns the result of the call to the solver.

        Parameters
        ----------
        data : dict
            Data used by the solver.
        warm_start : bool
            Not used.
        verbose : bool
            Should the solver print output?
        solver_opts : dict
            Additional arguments for the solver.

        Returns
        -------
        tuple
            (status, optimal value, primal, equality dual, inequality dual)
        """
        import gurobipy

        c = data[s.C]
        b = data[s.B]
        A = dok_matrix(data[s.A])
        # Save the dok_matrix.
        data[s.A] = A
        dims = dims_to_solver_dict(data[s.DIMS])

        n = c.shape[0]

        model = gurobipy.Model()
        variables = []
        for i in range(n):
            # Set variable type.
            if i in data[s.BOOL_IDX]:
                vtype = gurobipy.GRB.BINARY
            elif i in data[s.INT_IDX]:
                vtype = gurobipy.GRB.INTEGER
            else:
                vtype = gurobipy.GRB.CONTINUOUS
            variables.append(
                model.addVar(
                    obj=c[i],
                    name="x_%d" % i,
                    vtype=vtype,
                    # Gurobi's default LB is 0 (WHY???)
                    lb=-gurobipy.GRB.INFINITY,
                    ub=gurobipy.GRB.INFINITY))
        model.update()

        eq_constrs = self.add_model_lin_constr(model, variables,
                                               list(range(dims[s.EQ_DIM])),
                                               gurobipy.GRB.EQUAL, A, b)
        leq_start = dims[s.EQ_DIM]
        leq_end = dims[s.EQ_DIM] + dims[s.LEQ_DIM]
        ineq_constrs = self.add_model_lin_constr(
            model, variables, list(range(leq_start, leq_end)),
            gurobipy.GRB.LESS_EQUAL, A, b)
        soc_start = leq_end
        soc_constrs = []
        new_leq_constrs = []
        for constr_len in dims[s.SOC_DIM]:
            soc_end = soc_start + constr_len
            soc_constr, new_leq, new_vars = self.add_model_soc_constr(
                model, variables, list(range(soc_start, soc_end)), A, b)
            soc_constrs.append(soc_constr)
            new_leq_constrs += new_leq
            variables += new_vars
            soc_start += constr_len

        gur_constrs = eq_constrs + ineq_constrs + \
            soc_constrs + new_leq_constrs
        model.update()

        # Set verbosity and other parameters
        model.setParam("OutputFlag", verbose)
        # TODO user option to not compute duals.
        model.setParam("QCPDual", True)

        for key, value in list(solver_opts.items()):
            model.setParam(key, value)

        solution = {}
        try:
            model.optimize()
            solution["value"] = model.ObjVal
            solution["primal"] = np.array([v.X for v in variables])

            # Only add duals if not a MIP.
            # Not sure why we need to negate the following,
            # but need to in order to be consistent with other solvers.
            if not (data[s.BOOL_IDX] or data[s.INT_IDX]):
                vals = []
                for lc in gur_constrs:
                    if lc is not None:
                        if isinstance(lc, gurobipy.QConstr):
                            vals.append(lc.QCPi)
                        else:
                            vals.append(lc.Pi)
                    else:
                        vals.append(0)
                solution["y"] = -np.array(vals)
                solution[s.EQ_DUAL] = solution["y"][0:dims[s.EQ_DIM]]
                solution[s.INEQ_DUAL] = solution["y"][dims[s.EQ_DIM]:]
        except Exception:
            pass
        solution[s.SOLVE_TIME] = model.Runtime
        solution["status"] = self.STATUS_MAP.get(model.Status, s.SOLVER_ERROR)
        if solution["status"] == s.SOLVER_ERROR and model.SolCount:
            solution["status"] = s.OPTIMAL_INACCURATE

        return solution
Esempio n. 11
0
    def solve_via_data(self,
                       data,
                       warm_start: bool,
                       verbose: bool,
                       solver_opts,
                       solver_cache=None):
        """Returns the result of the call to the solver.

        Parameters
        ----------
        data : dict
            Data used by the solver.
        warm_start : bool
            Not used.
        verbose : bool
            Should the solver print output?
        solver_opts : dict
            Additional arguments for the solver.

        Returns
        -------
        tuple
            (status, optimal value, primal, equality dual, inequality dual)
        """
        import gurobipy

        c = data[s.C]
        b = data[s.B]
        A = sp.csr_matrix(data[s.A])
        dims = dims_to_solver_dict(data[s.DIMS])

        n = c.shape[0]

        # Create a new model
        if 'env' in solver_opts:
            # Specifies environment to create Gurobi model for control over licensing and parameters
            # https://www.gurobi.com/documentation/9.1/refman/environments.html
            default_env = solver_opts['env']
            del solver_opts['env']
            model = gurobipy.Model(env=default_env)
        else:
            # Create Gurobi model using default (unspecified) environment
            model = gurobipy.Model()

        # Pass through verbosity
        model.setParam("OutputFlag", verbose)

        variables = []
        for i in range(n):
            # Set variable type.
            if i in data[s.BOOL_IDX]:
                vtype = gurobipy.GRB.BINARY
            elif i in data[s.INT_IDX]:
                vtype = gurobipy.GRB.INTEGER
            else:
                vtype = gurobipy.GRB.CONTINUOUS
            variables.append(
                model.addVar(
                    obj=c[i],
                    name="x_%d" % i,
                    vtype=vtype,
                    # Gurobi's default LB is 0 (WHY???)
                    lb=-gurobipy.GRB.INFINITY,
                    ub=gurobipy.GRB.INFINITY))
        model.update()

        leq_start = dims[s.EQ_DIM]
        leq_end = dims[s.EQ_DIM] + dims[s.LEQ_DIM]
        if hasattr(model, 'addMConstrs'):
            eq_constrs = model.addMConstrs(A[:leq_start, :], None,
                                           gurobipy.GRB.EQUAL, b[:leq_start])
            ineq_constrs = model.addMConstrs(A[leq_start:leq_end, :], None,
                                             gurobipy.GRB.LESS_EQUAL,
                                             b[leq_start:leq_end])
        else:
            eq_constrs = self.add_model_lin_constr(model, variables,
                                                   range(dims[s.EQ_DIM]),
                                                   gurobipy.GRB.EQUAL, A, b)
            ineq_constrs = self.add_model_lin_constr(model, variables,
                                                     range(leq_start, leq_end),
                                                     gurobipy.GRB.LESS_EQUAL,
                                                     A, b)

        # TODO: add all SOC constrs at once! Be careful with return values
        soc_start = leq_end
        soc_constrs = []
        new_leq_constrs = []
        for constr_len in dims[s.SOC_DIM]:
            soc_end = soc_start + constr_len
            soc_constr, new_leq, new_vars = self.add_model_soc_constr(
                model, variables, range(soc_start, soc_end), A, b)
            soc_constrs.append(soc_constr)
            new_leq_constrs += new_leq
            variables += new_vars
            soc_start += constr_len

        # Set parameters
        # TODO user option to not compute duals.
        model.setParam("QCPDual", True)
        for key, value in solver_opts.items():
            model.setParam(key, value)

        solution = {}
        try:
            model.optimize()
            # Reoptimize if INF_OR_UNBD, to get definitive answer.
            if model.Status == 4:
                model.setParam("DualReductions", 0)
                model.optimize()
            solution["value"] = model.ObjVal
            solution["primal"] = np.array([v.X for v in variables])

            # Only add duals if not a MIP.
            # Not sure why we need to negate the following,
            # but need to in order to be consistent with other solvers.
            vals = []
            if not (data[s.BOOL_IDX] or data[s.INT_IDX]):
                lin_constrs = eq_constrs + ineq_constrs + new_leq_constrs
                vals += model.getAttr('Pi', lin_constrs)
                vals += model.getAttr('QCPi', soc_constrs)
                solution["y"] = -np.array(vals)
                solution[s.EQ_DUAL] = solution["y"][0:dims[s.EQ_DIM]]
                solution[s.INEQ_DUAL] = solution["y"][dims[s.EQ_DIM]:]
        except Exception:
            pass
        solution[s.SOLVE_TIME] = model.Runtime
        solution["status"] = self.STATUS_MAP.get(model.Status, s.SOLVER_ERROR)
        if solution["status"] == s.SOLVER_ERROR and model.SolCount:
            solution["status"] = s.OPTIMAL_INACCURATE
        if solution["status"] == s.USER_LIMIT and not model.SolCount:
            solution["status"] = s.INFEASIBLE_INACCURATE
        solution["model"] = model

        return solution
Esempio n. 12
0
    def solve_via_data(self,
                       data,
                       warm_start,
                       verbose,
                       solver_opts,
                       solver_cache=None):
        import xpress

        if 'no_qp_reduction' in solver_opts.keys(
        ) and solver_opts['no_qp_reduction'] is True:
            self.translate_back_QP_ = True

        c = data[s.C]  # objective coefficients

        dims = dims_to_solver_dict(
            data[s.DIMS])  # contains number of columns, rows, etc.

        nrowsEQ = dims[s.EQ_DIM]
        nrowsLEQ = dims[s.LEQ_DIM]
        nrows = nrowsEQ + nrowsLEQ

        # linear constraints
        b = data[s.B][:nrows]  # right-hand side
        A = data[s.A][:nrows]  # coefficient matrix

        # Problem
        self.prob_ = xpress.problem()

        mstart = makeMstart(A, len(c), 1)

        varGroups = {
        }  # If origprob is passed, used to tie IIS to original constraints
        transf2Orig = {
        }  # Ties transformation constraints to originals via varGroups
        nOrigVar = len(c)

        # Uses flat naming. Warning: this mixes
        # original with auxiliary variables.

        varnames = ['x_{0:05d}'.format(i) for i in range(len(c))]
        linRownames = ['lc_{0:05d}'.format(i) for i in range(len(b))]

        self.prob_.loadproblem(
            "CVXproblem",
            ['E'] * nrowsEQ + ['L'] * nrowsLEQ,  # qrtypes
            None,  # range
            c,  # obj coeff
            mstart,  # mstart
            None,  # mnel
            A.indices,  # row indices
            A.data,  # coefficients
            [-xpress.infinity] * len(c),  # lower bound
            [xpress.infinity] * len(c),  # upper bound
            colnames=varnames,  # column names
            rownames=linRownames)  # row    names

        x = np.array(self.prob_.getVariable())  # get whole variable vector

        # Set variable types for discrete variables
        self.prob_.chgcoltype(
            data[s.BOOL_IDX] + data[s.INT_IDX],
            'B' * len(data[s.BOOL_IDX]) + 'I' * len(data[s.INT_IDX]))

        currow = nrows

        iCone = 0

        auxVars = set(range(nOrigVar, len(c)))

        # Conic constraints
        #
        # Quadratic objective and constraints fall in this category,
        # as all quadratic stuff is converted into a cone via a linear transformation
        for k in dims[s.SOC_DIM]:

            # k is the size of the i-th cone, where i is the index
            # within dims [s.SOC_DIM]. The cone variables in
            # CVXOPT, apparently, are separate variables that are
            # marked as conic but not shown in a cone explicitly.

            A = data[s.A][currow:currow + k].tocsr()
            b = data[s.B][currow:currow + k]
            currow += k

            if self.translate_back_QP_:

                # Conic problem passed by CVXPY is translated back
                # into a QP problem. The problem is passed to us
                # as follows:
                #
                # min c'x
                # s.t. Ax <>= b
                #      y[i] = P[i]' * x + b[i]
                #      ||y[i][1:]||_2 <= y[i][0]
                #
                # where P[i] is a matrix, b[i] is a vector. Get
                # rid of the y variables by explicitly rewriting
                # the conic constraint as quadratic:
                #
                # y[i][1:]' * y[i][1:] <= y[i][0]^2
                #
                # and hence
                #
                # (P[i][1:]' * x + b[i][1:])^2 <= (P[i][0]' * x + b[i][0])^2

                Plhs = A[1:]
                Prhs = A[0]

                indRowL, indColL = Plhs.nonzero()
                indRowR, indColR = Prhs.nonzero()

                coeL = Plhs.data
                coeR = Prhs.data

                lhs = list(b[1:])
                rhs = b[0]

                for i in range(len(coeL)):
                    lhs[indRowL[i]] -= coeL[i] * x[indColL[i]]

                for i in range(len(coeR)):
                    rhs -= coeR[i] * x[indColR[i]]

                self.prob_.addConstraint(
                    xpress.Sum([lhs[i]**2 for i in range(len(lhs))]) <= rhs**2)

            else:

                # Create new (cone) variables and add them to the problem
                conevar = np.array([
                    xpress.var(name='cX{0:d}_{1:d}'.format(iCone, i),
                               lb=-xpress.infinity if i > 0 else 0)
                    for i in range(k)
                ])

                self.prob_.addVariable(conevar)

                initrow = self.prob_.attributes.rows

                mstart = makeMstart(A, k, 0)

                trNames = [
                    'linT_qc{0:d}_{1:d}'.format(iCone, i) for i in range(k)
                ]

                # Linear transformation for cone variables <--> original variables
                self.prob_.addrows(
                    ['E'] * k,  # qrtypes
                    b,  # rhs
                    mstart,  # mstart
                    A.indices,  # ind
                    A.data,  # dmatval
                    names=trNames)  # row names

                self.prob_.chgmcoef([initrow + i for i in range(k)], conevar,
                                    [1] * k)

                conename = 'cone_qc{0:d}'.format(iCone)
                # Real cone on the cone variables (if k == 1 there's no
                # need for this constraint as y**2 >= 0 is redundant)
                if k > 1:
                    self.prob_.addConstraint(
                        xpress.constraint(constraint=xpress.Sum(
                            conevar[i]**2
                            for i in range(1, k)) <= conevar[0]**2,
                                          name=conename))

                auxInd = list(set(A.indices) & auxVars)

                if len(auxInd) > 0:
                    group = varGroups[varnames[auxInd[0]]]
                    for i in trNames:
                        transf2Orig[i] = group
                    transf2Orig[conename] = group

            iCone += 1

        # Objective. Minimize is by default both here and in CVXOPT
        self.prob_.setObjective(xpress.Sum(c[i] * x[i] for i in range(len(c))))

        # End of the conditional (warm-start vs. no warm-start) code,
        # set options, solve, and report.

        # Set options
        #
        # The parameter solver_opts is a dictionary that contains only
        # one key, 'solver_opt', and its value is a dictionary
        # {'control': value}, matching perfectly the format used by
        # the Xpress Python interface.

        if verbose:
            self.prob_.controls.miplog = 2
            self.prob_.controls.lplog = 1
            self.prob_.controls.outputlog = 1
        else:
            self.prob_.controls.miplog = 0
            self.prob_.controls.lplog = 0
            self.prob_.controls.outputlog = 0

        if 'solver_opts' in solver_opts.keys():
            self.prob_.setControl(solver_opts['solver_opts'])

        self.prob_.setControl({
            i: solver_opts[i]
            for i in solver_opts.keys()
            if i in xpress.controls.__dict__.keys()
        })

        # Solve
        self.prob_.solve()

        results_dict = {
            'problem': self.prob_,
            'status': self.prob_.getProbStatus(),
            'obj_value': self.prob_.getObjVal(),
        }

        status_map_lp, status_map_mip = self.get_status_maps()

        if self.is_mip(data):
            status = status_map_mip[results_dict['status']]
        else:
            status = status_map_lp[results_dict['status']]

        results_dict[s.XPRESS_TROW] = transf2Orig

        results_dict[
            s.XPRESS_IIS] = None  # Return no IIS if problem is feasible

        if status in s.SOLUTION_PRESENT:
            results_dict['x'] = self.prob_.getSolution()
            if not (data[s.BOOL_IDX] or data[s.INT_IDX]):
                results_dict['y'] = self.prob_.getDual()

        elif status == s.INFEASIBLE:

            # Retrieve all IIS. For LPs there can be more than one,
            # but for QCQPs there is only support for one IIS.

            iisIndex = 0

            self.prob_.iisfirst(0)  # compute all IIS

            row, col, rtype, btype, duals, rdcs, isrows, icols = [], [], [], [], [], [], [], []

            self.prob_.getiisdata(0, row, col, rtype, btype, duals, rdcs,
                                  isrows, icols)

            origrow = []
            for iRow in row:
                if iRow.name in transf2Orig.keys():
                    name = transf2Orig[iRow.name]
                else:
                    name = iRow.name

                if name not in origrow:
                    origrow.append(name)

            results_dict[s.XPRESS_IIS] = [{
                'orig_row': origrow,
                'row': row,
                'col': col,
                'rtype': rtype,
                'btype': btype,
                'duals': duals,
                'redcost': rdcs,
                'isolrow': isrows,
                'isolcol': icols
            }]

            while self.prob_.iisnext() == 0:
                iisIndex += 1
                self.prob_.getiisdata(iisIndex, row, col, rtype, btype, duals,
                                      rdcs, isrows, icols)
                results_dict[s.XPRESS_IIS].append(
                    (row, col, rtype, btype, duals, rdcs, isrows, icols))

        # Generate solution.
        solution = {}

        if results_dict["status"] != s.SOLVER_ERROR:

            self.prob_ = results_dict['problem']

            vartypes = []
            self.prob_.getcoltype(vartypes, 0, len(data[s.C]) - 1)

        status_map_lp, status_map_mip = self.get_status_maps()

        if data[s.BOOL_IDX] or data[s.INT_IDX]:
            solution[s.STATUS] = status_map_mip[results_dict['status']]
        else:
            solution[s.STATUS] = status_map_lp[results_dict['status']]

        if solution[s.STATUS] in s.SOLUTION_PRESENT:

            solution[s.PRIMAL] = results_dict['x']
            solution[s.VALUE] = results_dict['obj_value']

            if not (data[s.BOOL_IDX] or data[s.INT_IDX]):
                solution[s.EQ_DUAL] = [-v for v in results_dict['y']]

        solution[s.XPRESS_IIS] = results_dict[s.XPRESS_IIS]
        solution[s.XPRESS_TROW] = results_dict[s.XPRESS_TROW]

        return solution
Esempio n. 13
0
 def cone_dims_for_scs(self):
     return dims_to_solver_dict(self._data['dims'])
Esempio n. 14
0
def repair(prob, params, r=None, verbose=True, maxiter=10, maxiter_pgm=25,
           lam=1, lam_factor=2, lr=.1):
    """
    Repairs prob by altering params.
    Minimizes r(params) subject to the constraint that prob is solvable.

    Args:
        - prob: cvxpy Problem object
        - params: list of cvxpy Parameters involved in prob
        - r (optional): callable that takes a list of cvxpy Variables as input
            (of the same dimension as params), and returns a cvxpy expression
            representing the performance metric (default=None).
        - verbose (optional): whether or not to print diagnostic informtion (default=True).
        - maxiter (optional): Maximum number of outer iterations (default=10).
        - maxiter_pgm (optional): Maximum number of inner iterations (default=25).
        - lam (optional): Starting value for 1/lambda, multiplied by lam_factor
            each iteration (default=1).
        - lam_factor (optional): Factor to multiply lambda by each iteration (default=2). 
        - lr (optional): initial step size for proximal gradient method (default=.1).
    """
    assert set(prob.parameters()) == set(params)
    assert hasattr(prob, "get_problem_data")

    # compile problem
    data, _, _ = prob.get_problem_data(solver=cp.SCS)
    compiler = data[cp.settings.PARAM_PROB]
    cone_dict = dims_to_solver_dict(data["dims"])
    param_ids = [p.id for p in params]
    warm_start = None

    for k in range(maxiter):
        # minimize (1/lam) * r(A, b, c) + t(A, b, c)
        objective = float("inf")

        for k_pgm in range(maxiter_pgm):
            # compute derivative
            c, _, neg_A, b = compiler.apply_parameters(
                dict(zip(param_ids, [p.value for p in params])))
            A = -neg_A
            dA, db, dc, t, _, _, _, warm_start = derivative(
                A, b, c, cone_dict, warm_start=warm_start, acceleration_lookback=0, eps=1e-8)
            del_param_dict = compiler.apply_param_jac(dc, -dA, db)
            param_derivative = [del_param_dict[i] for i in param_ids]

            # compute objective
            objective = t
            new_objective = float("inf")
            if r is not None:
                variable_params = [cp.Variable(p.shape) for p in params]
                for vp, p in zip(variable_params, params):
                    vp.value = p.value
                obj, cons = r(variable_params)
                objective += (1 / lam) * obj.value

            # update step size until objective decreases
            old_params = [p.value.copy() for p in params]
            while True:
                lr = np.clip(lr, 1e-6, 1e6)
                new_params = []
                for i in range(len(param_ids)):
                    new_params += [old_params[i] -
                                   lr * param_derivative[i]]

                if r is not None:
                    variable_params = [cp.Variable(
                        p.shape) for p in params]
                    obj, cons = r(variable_params)
                    obj = lr * obj / lam
                    obj += cp.sum([.5 * cp.sum_squares(p - v)
                                   for (p, v) in zip(new_params, variable_params)])
                    prob = cp.Problem(cp.Minimize(obj), cons)
                    try:
                        prob.solve(solver=cp.MOSEK)
                    except:
                        prob.solve(solver=cp.SCS, acceleration_lookback=0)
                    for i in range(len(param_ids)):
                        params[i].value = variable_params[i].value
                else:
                    for i in range(len(param_ids)):
                        params[i].value = new_params[i]

                # compute objective
                c, _, neg_A, b = compiler.apply_parameters(
                    dict(zip(param_ids, [p.value for p in params])))
                A = -neg_A
                _, _, _, t_new, _, _, _, warm_start = derivative(
                    A, b, c, cone_dict, warm_start=warm_start, acceleration_lookback=0, eps=1e-8)
                new_objective = t_new
                if r is not None:
                    variable_params = [cp.Variable(p.shape) for p in params]
                    for vp, p in zip(variable_params, params):
                        vp.value = p.value
                    obj, cons = r(variable_params)
                    new_objective += obj.value / lam

                if new_objective < objective:
                    lr *= 1.2
                    break
                elif lr > 1e-6:
                    lr /= 2.0
                else:
                    break

            if lr <= 1e-6:
                break

        # update lam
        lam *= lam_factor
        if verbose:
            print(f'Updating lambda to {lam}')

        r_val = 0.0
        if r is not None:
            variable_params = [cp.Variable(p.shape) for p in params]
            for vp, p in zip(variable_params, params):
                vp.value = p.value
            obj, cons = r(variable_params)
            r_val += obj.value
        if verbose:
            print("Proximal gradient method completed in %d/%d iterations" %
                  (k_pgm + 1, maxiter_pgm))
            print("Iteration: %d, r: %3.3f, t: %3.3f" % (k, r_val, t))
Esempio n. 15
0
    def solve_via_data(self, data, warm_start, verbose, solver_opts, solver_cache=None):
        import cplex

        c = data[s.C]
        b = data[s.B]
        A = dok_matrix(data[s.A])
        # Save the dok_matrix.
        data[s.A] = A
        dims = dims_to_solver_dict(data[s.DIMS])

        n = c.shape[0]

        model = cplex.Cplex()
        variables = []
        # cpx_constrs will contain CpxConstr namedtuples (see above).
        cpx_constrs = []
        vtype = []
        if data[s.BOOL_IDX] or data[s.INT_IDX]:
            for i in range(n):
                # Set variable type.
                if i in data[s.BOOL_IDX]:
                    vtype.append('B')
                elif i in data[s.INT_IDX]:
                    vtype.append('I')
                else:
                    vtype.append('C')
        else:
            # If we specify types (even with 'C'), then the problem will
            # be interpreted as a MIP. Leaving vtype as an empty list
            # here, will ensure that the problem type remains an LP.
            pass
        # Add the variables in a batch
        variables = list(model.variables.add(
            obj=[c[i] for i in range(n)],
            lb=[-cplex.infinity]*n,  # default LB is 0
            ub=[cplex.infinity]*n,
            types="".join(vtype),
            names=["x_%d" % i for i in range(n)]))

        # Add equality constraints
        cpx_constrs += [_CpxConstr(_LIN, x)
                        for x in self.add_model_lin_constr(
                                model, variables,
                                range(dims[s.EQ_DIM]),
                                'E', A, b)]

        # Add inequality (<=) constraints
        leq_start = dims[s.EQ_DIM]
        leq_end = dims[s.EQ_DIM] + dims[s.LEQ_DIM]
        cpx_constrs += [_CpxConstr(_LIN, x)
                        for x in self.add_model_lin_constr(
                                model, variables,
                                range(leq_start, leq_end),
                                'L', A, b)]

        # Add SOC constraints
        soc_start = leq_end
        for constr_len in dims[s.SOC_DIM]:
            soc_end = soc_start + constr_len
            soc_constr, new_leq, new_vars = self.add_model_soc_constr(
                model, variables, range(soc_start, soc_end), A, b)
            cpx_constrs.append(_CpxConstr(_QUAD, soc_constr))
            cpx_constrs += [_CpxConstr(_LIN, x) for x in new_leq]
            variables += new_vars
            soc_start += constr_len

        # Set verbosity
        if not verbose:
            hide_solver_output(model)

        # For CVXPY, we set the qcpduals parameter here, but the user can
        # easily override it via the "cplex_params" solver option (see
        # set_parameters function).
        model.parameters.preprocessing.qcpduals.set(
            model.parameters.preprocessing.qcpduals.values.force)

        # Set parameters
        set_parameters(model, solver_opts)

        # Solve problem
        solution = {"model": model}
        try:
            start_time = model.get_time()
            model.solve()
            solution[s.SOLVE_TIME] = model.get_time() - start_time
        except Exception:
            pass

        return solution
Esempio n. 16
0
def CvxpyLayer(problem, parameters, variables, gp=False):
    """Construct a CvxpyLayer

    Args:
        problem: The CVXPY problem; must be DPP.
        parameters: A list of CVXPY Parameters in the problem; the order
                    of the Parameters determines the order in which parameter
                    values must be supplied in the forward pass. Must include
                    every parameter involved in problem.
        variables: A list of CVXPY Variables in the problem; the order of the
                   Variables determines the order of the optimal variable
                   values returned from the forward pass.
        gp: Whether to parse the problem using DGP (True or False).

    Returns:
        A callable that solves the problem.
    """

    if gp:
        if not problem.is_dgp(dpp=True):
            raise ValueError('Problem must be DPP.')
    else:
        if not problem.is_dcp(dpp=True):
            raise ValueError('Problem must be DPP.')

    if not set(problem.parameters()) == set(parameters):
        raise ValueError("The layer's parameters must exactly match "
                         "problem.parameters")
    if not set(variables).issubset(set(problem.variables())):
        raise ValueError("Argument variables must be a subset of "
                         "problem.variables")
    if not isinstance(parameters, list) and \
            not isinstance(parameters, tuple):
        raise ValueError("The layer's parameters must be provided as "
                         "a list or tuple")
    if not isinstance(variables, list) and \
            not isinstance(variables, tuple):
        raise ValueError("The layer's variables must be provided as "
                         "a list or tuple")

    var_dict = {v.id for v in variables}

    # Construct compiler
    param_order = parameters
    if gp:
        for param in parameters:
            if param.value is None:
                raise ValueError("An initial value for each parameter is "
                                 "required when gp=True.")
        data, solving_chain, _ = problem.get_problem_data(solver=cp.SCS,
                                                          gp=True)
        compiler = data[cp.settings.PARAM_PROB]
        dgp2dcp = solving_chain.get(cp.reductions.Dgp2Dcp)
        param_ids = [p.id for p in compiler.parameters]
        old_params_to_new_params = (dgp2dcp.canon_methods._parameters)
    else:
        data, _, _ = problem.get_problem_data(solver=cp.SCS)
        compiler = data[cp.settings.PARAM_PROB]
        param_ids = [p.id for p in param_order]
        dgp2dcp = None
    cone_dims = dims_to_solver_dict(data["dims"])

    info = {}
    CvxpyLayerFn_p = core.Primitive("CvxpyLayerFn_" + str(hash(problem)))

    @partial(jax.custom_vjp, nondiff_argnums=(0, ))
    def CvxpyLayerFn(solver_args, *params):
        return CvxpyLayerFn_p.bind(solver_args, *params)

    def CvxpyLayerFn_impl(solver_args, *params):
        """Solve problem (or a batch of problems) corresponding to `params`

        Args:
            solver_args: a dict of optional arguments, to send to `diffcp`.
                         Keys should be the names of keyword arguments.
            params: a sequence of JAX arrays; the n-th argument specifies
                    the value for the n-th CVXPY Parameter. These arrays
                    can be batched: if a array has 3 dimensions, then its
                    first dimension is interpreted as the batch size. These
                    arrays must all have the same dtype.

        Returns:
            a list of optimal variable values, one for each CVXPY Variable
            supplied to the constructor.
        """
        if len(params) != len(param_ids):
            raise ValueError('An array must be provided for each CVXPY '
                             'parameter; received %d arrays, expected %d' %
                             (len(params), len(param_ids)))

        dtype, batch, batch_sizes, batch_size = batch_info(params, param_order)

        if gp:
            param_map = {}
            # construct a list of params for the DCP problem
            for param, value in zip(param_order, params):
                if param in old_params_to_new_params:
                    new_id = old_params_to_new_params[param].id
                    param_map[new_id] = jnp.log(value)
                else:
                    new_id = param.id
                    param_map[new_id] = value
            params_numpy = [np.array(param_map[pid]) for pid in param_ids]
        else:
            params_numpy = [np.array(p) for p in params]

        # canonicalize problem
        start = time.time()
        As, bs, cs, cone_dicts, shapes = [], [], [], [], []
        for i in range(batch_size):
            params_numpy_i = [
                p if sz == 0 else p[i]
                for p, sz in zip(params_numpy, batch_sizes)
            ]
            c, _, neg_A, b = compiler.apply_parameters(dict(
                zip(param_ids, params_numpy_i)),
                                                       keep_zeros=True)
            A = -neg_A  # cvxpy canonicalizes -A
            As.append(A)
            bs.append(b)
            cs.append(c)
            cone_dicts.append(cone_dims)
            shapes.append(A.shape)
        info['canon_time'] = time.time() - start
        info['shapes'] = shapes

        # compute solution and derivative function
        start = time.time()
        try:
            xs, _, _, _, DT_batch = diffcp.solve_and_derivative_batch(
                As, bs, cs, cone_dicts, **solver_args)
            info['DT_batch'] = DT_batch
        except diffcp.SolverError as e:
            print("Please consider re-formulating your problem so that "
                  "it is always solvable or increasing the number of "
                  "solver iterations.")
            raise e
        info['solve_time'] = time.time() - start

        # extract solutions and append along batch dimension
        start = time.time()
        sol = [[] for i in range(len(variables))]
        for i in range(batch_size):
            sltn_dict = compiler.split_solution(xs[i], active_vars=var_dict)
            for j, v in enumerate(variables):
                sol[j].append(
                    jnp.expand_dims(jnp.array(sltn_dict[v.id], dtype=dtype),
                                    axis=0))
        sol = [jnp.concatenate(s, axis=0) for s in sol]

        if not batch:
            sol = [jnp.squeeze(s, axis=0) for s in sol]

        if gp:
            sol = [jnp.exp(s) for s in sol]

        return tuple(sol)

    CvxpyLayerFn_p.def_impl(CvxpyLayerFn_impl)

    def CvxpyLayerFn_fwd_vjp(solver_args, *params):
        sol = CvxpyLayerFn(solver_args, *params)
        return sol, (params, sol)

    def CvxpyLayerFn_bwd_vjp(solver_args, res, dvars):
        params, sol = res
        dtype, batch, batch_sizes, batch_size = batch_info(params, param_order)

        # Use info here to retrieve this from the forward pass because
        # the residual in JAX's vjp doesn't allow non-JAX types to be
        # easily returned. This works when calling this serially,
        # but will break if this is called in parallel.
        shapes = info['shapes']
        DT_batch = info['DT_batch']

        if gp:
            # derivative of exponential recovery transformation
            dvars = [dvar * s for dvar, s in zip(dvars, sol)]

        dvars_numpy = [np.array(dvar) for dvar in dvars]

        if not batch:
            dvars_numpy = [np.expand_dims(dvar, 0) for dvar in dvars_numpy]

        # differentiate from cvxpy variables to cone problem data
        dxs, dys, dss = [], [], []
        for i in range(batch_size):
            del_vars = {}
            for v, dv in zip(variables, [dv[i] for dv in dvars_numpy]):
                del_vars[v.id] = dv
            dxs.append(compiler.split_adjoint(del_vars))
            dys.append(np.zeros(shapes[i][0]))
            dss.append(np.zeros(shapes[i][0]))

        dAs, dbs, dcs = DT_batch(dxs, dys, dss)

        # differentiate from cone problem data to cvxpy parameters
        start = time.time()
        grad = [[] for _ in range(len(param_ids))]
        for i in range(batch_size):
            del_param_dict = compiler.apply_param_jac(dcs[i], -dAs[i], dbs[i])
            for j, pid in enumerate(param_ids):
                grad[j] += [
                    jnp.expand_dims(jnp.array(del_param_dict[pid],
                                              dtype=dtype),
                                    axis=0)
                ]
        grad = [jnp.concatenate(g, axis=0) for g in grad]
        if gp:
            # differentiate through the log transformation of params
            dcp_grad = grad
            grad = []
            dparams = {pid: g for pid, g in zip(param_ids, dcp_grad)}
            for param, value in zip(param_order, params):
                v = 0.0 if param.id not in dparams else dparams[param.id]
                if param in old_params_to_new_params:
                    dcp_param_id = old_params_to_new_params[param].id
                    # new_param.value == log(param), apply chain rule
                    v += (1.0 / value) * dparams[dcp_param_id]
                grad.append(v)
        info['dcanon_time'] = time.time() - start

        if not batch:
            grad = [jnp.squeeze(g, axis=0) for g in grad]
        else:
            for i, sz in enumerate(batch_sizes):
                if sz == 0:
                    grad[i] = jnp.sum(grad[i], axis=0)

        return tuple(grad)

    CvxpyLayerFn.defvjp(CvxpyLayerFn_fwd_vjp, CvxpyLayerFn_bwd_vjp)

    # Default solver_args to an optional empty dict
    def f(*params, **kwargs):
        solver_args = kwargs.get('solver_args', {})
        return CvxpyLayerFn(solver_args, *params)

    return f
Esempio n. 17
0
    def solve_via_data(self, data, warm_start, verbose, solver_opts, solver_cache=None):
        # Import basic modelling tools of cylp
        from cylp.cy import CyClpSimplex
        from cylp.py.modeling.CyLPModel import CyLPModel, CyLPArray

        c = data[s.C]
        b = data[s.B]
        A = data[s.A]
        dims = dims_to_solver_dict(data[s.DIMS])

        n = c.shape[0]

        # Problem
        model = CyLPModel()

        # Variables
        x = model.addVariable('x', n)

        # Constraints
        # eq
        model += A[0:dims[s.EQ_DIM], :] * x == CyLPArray(b[0:dims[s.EQ_DIM]])

        # leq
        leq_start = dims[s.EQ_DIM]
        leq_end = dims[s.EQ_DIM] + dims[s.LEQ_DIM]
        model += A[leq_start:leq_end, :] * x <= CyLPArray(b[leq_start:leq_end])

        # Objective
        model.objective = c

        # Convert model
        model = CyClpSimplex(model)

        # No boolean vars available in Cbc -> model as int + restrict to [0,1]
        if data[s.BOOL_IDX] or data[s.INT_IDX]:
            # Mark integer- and binary-vars as "integer"
            model.setInteger(x[data[s.BOOL_IDX]])
            model.setInteger(x[data[s.INT_IDX]])

            # Restrict binary vars only
            idxs = data[s.BOOL_IDX]
            n_idxs = len(idxs)

            model.setColumnLowerSubset(np.arange(n_idxs, dtype=np.int32),
                                       np.array(idxs, np.int32),
                                       np.zeros(n_idxs))

            model.setColumnUpperSubset(np.arange(n_idxs, dtype=np.int32),
                                       np.array(idxs, np.int32),
                                       np.ones(n_idxs))

        # Verbosity Clp
        if not verbose:
            model.logLevel = 0

        # Build model & solve
        status = None
        if data[s.BOOL_IDX] or data[s.INT_IDX]:
            # Convert model
            cbcModel = model.getCbcModel()
            for key, value in solver_opts.items():
                setattr(cbcModel, key, value)

            # Verbosity Cbc
            if not verbose:
                cbcModel.logLevel = 0

            # cylp: /cylp/cy/CyCbcModel.pyx#L134
            # Call CbcMain. Solve the problem using the same parameters used by
            # CbcSolver. Equivalent to solving the model from the command line
            # using cbc's binary.
            cbcModel.solve()
            status = cbcModel.status
        else:
            # cylp: /cylp/cy/CyClpSimplex.pyx
            # Run CLP's initialSolve. It does a presolve and uses primal or dual
            # Simplex to solve a problem.
            status = model.initialSolve()

        solution = {}
        if data[s.BOOL_IDX] or data[s.INT_IDX]:
            solution["status"] = self.STATUS_MAP_MIP[status]
            solution["primal"] = cbcModel.primalVariableSolution['x']
            solution["value"] = cbcModel.objectiveValue
        else:
            solution["status"] = self.STATUS_MAP_LP[status]
            solution["primal"] = model.primalVariableSolution['x']
            solution["value"] = model.objectiveValue

        return solution