Beispiel #1
0
def main():
    model = create_model()
    nlp = PyomoNLP(model)

    # initial guesses
    x = nlp.init_primals()
    lam = nlp.init_duals()

    nlp.set_primals(x)
    nlp.set_duals(lam)

    # NLP function evaluations
    f = nlp.evaluate_objective()
    print("Objective Function\n", f)
    df = nlp.evaluate_grad_objective()
    print("Gradient of Objective Function:\n", df)
    c = nlp.evaluate_constraints()
    print("Constraint Values:\n", c)
    c_eq = nlp.evaluate_eq_constraints()
    print("Equality Constraint Values:\n", c_eq)
    c_ineq = nlp.evaluate_ineq_constraints()
    print("Inequality Constraint Values:\n", c_ineq)
    jac = nlp.evaluate_jacobian()
    print("Jacobian of Constraints:\n", jac.toarray())
    jac_eq = nlp.evaluate_jacobian_eq()
    print("Jacobian of Equality Constraints:\n", jac_eq.toarray())
    jac_ineq = nlp.evaluate_jacobian_ineq()
    print("Jacobian of Inequality Constraints:\n", jac_ineq.toarray())
    hess_lag = nlp.evaluate_hessian_lag()
    print("Hessian of Lagrangian\n", hess_lag.toarray())
Beispiel #2
0
nlp.set_primals(x)
nlp.set_duals(lam)

# NLP function evaluations
f = nlp.evaluate_objective()
print("Objective Function\n", f)

df = nlp.evaluate_grad_objective()
print("Gradient of Objective Function:\n", df)

c = nlp.evaluate_constraints()
print("Constraint Values:\n", c)

c_eq = nlp.evaluate_eq_constraints()
print("Equality Constraint Values:\n", c_eq)

c_ineq = nlp.evaluate_ineq_constraints()
print("Inequality Constraint Values:\n", c_ineq)

jac = nlp.evaluate_jacobian()
print("Jacobian of Constraints:\n", jac.toarray())

jac_eq = nlp.evaluate_jacobian_eq()
print("Jacobian of Equality Constraints:\n", jac_eq.toarray())

jac_ineq = nlp.evaluate_jacobian_ineq()
print("Jacobian of Inequality Constraints:\n", jac_ineq.toarray())

hess_lag = nlp.evaluate_hessian_lag()
print("Hessian of Lagrangian\n", hess_lag.toarray())
Beispiel #3
0
class DegeneracyHunter():
    def __init__(self, block_or_jac, solver=None):
        ''' Initialize Degeneracy Hunter Object
    
        Arguments:
            block_or_jac: Pyomo model or Jacobian
            solver: Pyomo SolverFactory
            
        Notes:
            Passing a Jacobian to Degeneracy Hunter is current untested.
        
        '''

        block_like = False
        try:
            block_like = issubclass(block_or_jac.ctype, pyo.Block)
        except AttributeError:
            pass

        if block_like:

            # Add Pyomo model to the object
            self.block = block_or_jac

            # setup pynumero interface
            self.nlp = PyomoNLP(self.block)

            # calculate Jacobian of equality constraints in COO sparse matrix format
            jac_eq = self.nlp.evaluate_jacobian_eq()

            # save the Jacobian
            self.jac_eq = jac_eq

            # Create a list of equality constraint names
            self.eq_con_list = PyomoNLP.get_pyomo_equality_constraints(
                self.nlp)

            self.candidate_eqns = None

        elif type(block_or_jac) is np.array:

            raise NotImplementedError(
                "Degeneracy Hunter only currently supports analyzing a Pyomo model"
            )

            # TODO: Need to refactor, document, and test support for Jacobian
            self.jac_eq = block_or_jac

            self.eq_con_list = None

        else:

            raise TypeError("Check the type for 'block_or_jac'")

        # number of equality constraints, variables
        self.n_eq = self.jac_eq.shape[0]
        self.n_var = self.jac_eq.shape[1]

        # Define default candidate equations (enumerate)
        candidate_eqns = range(self.n_eq)

        # Initialize solver
        if solver is None:
            # TODO: Test performance with open solvers such as cbc
            self.solver = pyo.SolverFactory('gurobi')
            self.solver.options = {'NumericFocus': 3}

        else:
            # TODO: Make this a custom exception following IDAES standards
            # assert type(solver) is SolverFactory, "Argument solver should be type SolverFactory"
            self.solver = solver

        # Create spot to store singular values
        self.s = None

        # Set constants for MILPs
        self.max_nu = 1E5
        self.min_nonzero_nu = 1E-5

    def check_residuals(self, tol=1e-5, print_level=2, sort=True):
        """
        Method to return a ComponentSet of all Constraint components with a
        residual greater than a given threshold which appear in a model.
        Args:
            block : model to be studied
            tol : residual threshold for inclusion in ComponentSet
            print_level: controls to extend of output to the screen
                0: nothing printed
                1: only name of constraint printed
                2: each constraint is pretty printed
                3: pretty print each constraint, then print value for included variable
            sort: sort residuals in descending order for printing
        Returns:
            A ComponentSet including all Constraint components with a residual
            greater than tol which appear in block
        """

        if print_level > 0:
            residual_values = large_residuals_set(self.block, tol, True)
        else:
            return large_residuals_set(self.block, tol, False)

        print(" ")
        if len(residual_values) > 0:
            print("All constraints with residuals larger than", tol, ":")
            if print_level == 1:
                print("Count\tName\t|residual|")

            if sort:
                residual_values = dict(
                    sorted(residual_values.items(),
                           key=itemgetter(1),
                           reverse=True))

            for i, (c, r) in enumerate(residual_values.items()):
                if print_level == 1:
                    # Basic print statement. count, constraint, residual
                    print(i, "\t", c, "\t", r)
                else:
                    # Pretty print constraint
                    print("\ncount =", i, "\t|residual| =", r)
                    c.pprint()

                if print_level == 2:
                    # print values and bounds for each variable in the constraint
                    print("variable\tlower\tvalue\tupper")
                    for v in identify_variables(c.body):
                        self.print_variable_bounds(v)
        else:
            print("No constraints with residuals larger than", tol, "!")

        return residual_values.keys()

    def check_variable_bounds(self,
                              tol=1e-5,
                              relative=False,
                              skip_lb=False,
                              skip_ub=False,
                              verbose=True):
        """
        Return a ComponentSet of all variables within a tolerance of their bounds.
        Args:
            block : model to be studied
            tol : residual threshold for inclusion in ComponentSet (default = 1e-5)
            relative : Boolean, use relative tolerance (default = False)
            skip_lb: Boolean to skip lower bound (default = False)
            skip_ub: Boolean to skip upper bound (default = False)
            verbose: Boolean to toggle on printing to screen (default = True)
        Returns:
            A ComponentSet including all Constraint components with a residual
            greater than tol which appear in block
        """
        vnbs = variables_near_bounds_set(self.block, tol, relative, skip_lb,
                                         skip_ub)

        if verbose:
            print(" ")
            if relative:
                s = "(relative)"
            else:
                s = "(absolute)"
            if len(vnbs) > 0:

                print("Variables within", tol, s, "of their bounds:")
                print("variable\tlower\tvalue\tupper")
                for v in vnbs:
                    self.print_variable_bounds(v)
            else:
                print("No variables within", tol, s, "of their bounds.")

        return vnbs

    def check_rank_equality_constraints(self, tol=1E-6):
        """
        Method to check the rank of the Jacobian of the equality constraints
        
        Args:
            tol: Tolerance for smallest singular value (default=1E-6)
        
        Returns:
            Number of singular values less than tolerance (-1 means error)
        
        """

        print("\nChecking rank of Jacobian of equality constraints...")

        print("Model contains", self.n_eq, "equality constraints and",
              self.n_var, "variables.")

        counter = 0
        if self.n_eq > 1:
            if self.s is None:
                self.svd_analysis()

            n = len(self.s)

            print("Smallest singular value(s):")
            for i in range(n):
                print("%.3E" % self.s[i])
                if self.s[i] < tol:
                    counter += 1
        else:
            # TODO: Make this an exception
            print("Model needs at least 2 equality constraints to check rank.")
            counter = -1

        return counter

    # TODO: Refactor, this should not be a staticmethod
    @staticmethod
    def _prepare_ids_milp(jac_eq, M=1E5):
        '''
        Prepare MILP to compute the irreducible degenerate set
        
        Argument:
            jac_eq Jacobian of equality constraints [matrix]
            M: largest value for nu
            
        Returns:
            m_dh: Pyomo model to calculate irreducible degenerate sets
        '''

        n_eq = jac_eq.shape[0]
        n_var = jac_eq.shape[1]

        # Create Pyomo model for irreducible degenerate set
        m_dh = pyo.ConcreteModel()

        # Create index for constraints
        m_dh.C = pyo.Set(initialize=range(n_eq))

        m_dh.V = pyo.Set(initialize=range(n_var))

        # Add variables
        m_dh.nu = pyo.Var(m_dh.C, bounds=(-M, M), initialize=1.0)
        m_dh.y = pyo.Var(m_dh.C, domain=pyo.Binary)

        # Constraint to enforce set is degenerate
        if issparse(jac_eq):
            m_dh.J = jac_eq.tocsc()

            def eq_degenerate(m_dh, v):

                # Find the columns with non-zero entries
                C_ = find(m_dh.J[:, v])[0]
                return sum(m_dh.J[c, v] * m_dh.nu[c] for c in C_) == 0

        else:
            m_dh.J = jac_eq

            def eq_degenerate(m_dh, v):
                return sum(m_dh.J[c, v] * m_dh.nu[c] for c in m_dh.C) == 0

        m_dh.degenerate = pyo.Constraint(m_dh.V, rule=eq_degenerate)

        def eq_lower(m_dh, c):
            return -M * m_dh.y[c] <= m_dh.nu[c]

        m_dh.lower = pyo.Constraint(m_dh.C, rule=eq_lower)

        def eq_upper(m_dh, c):
            return m_dh.nu[c] <= M * m_dh.y[c]

        m_dh.upper = pyo.Constraint(m_dh.C, rule=eq_upper)

        m_dh.obj = pyo.Objective(expr=sum(m_dh.y[c] for c in m_dh.C))

        return m_dh

    # TODO: Refactor, this should not be a staticmethod
    @staticmethod
    def _prepare_find_candidates_milp(jac_eq, M=1E5, m_small=1E-5):
        '''
        Prepare MILP to find candidate equations for consider for IDS
        
        Argument:
            jac_eq Jacobian of equality constraints [matrix]
            M: maximum value for nu
            m_small: smallest value for nu to be considered non-zero
            
        Returns:
            m_fc: Pyomo model to find candidates
        '''

        n_eq = jac_eq.shape[0]
        n_var = jac_eq.shape[1]

        # Create Pyomo model for irreducible degenerate set
        m_dh = pyo.ConcreteModel()

        # Create index for constraints
        m_dh.C = pyo.Set(initialize=range(n_eq))

        m_dh.V = pyo.Set(initialize=range(n_var))

        # Specify minimum size for nu to be considered non-zero
        m_dh.m_small = m_small

        # Add variables
        m_dh.nu = pyo.Var(m_dh.C,
                          bounds=(-M - m_small, M + m_small),
                          initialize=1.0)
        m_dh.y_pos = pyo.Var(m_dh.C, domain=pyo.Binary)
        m_dh.y_neg = pyo.Var(m_dh.C, domain=pyo.Binary)
        m_dh.abs_nu = pyo.Var(m_dh.C, bounds=(0, M + m_small))

        # Positive exclusive or negative
        def eq_pos_xor_negative(m, c):
            return m.y_pos[c] + m.y_neg[c] <= 1

        m_dh.pos_xor_neg = pyo.Constraint(m_dh.C)

        # Constraint to enforce set is degenerate
        if issparse(jac_eq):
            m_dh.J = jac_eq.tocsc()

            def eq_degenerate(m_dh, v):
                if np.sum(np.abs(m_dh.J[:, v])) > 1E-6:
                    # Find the columns with non-zero entries
                    C_ = find(m_dh.J[:, v])[0]
                    return sum(m_dh.J[c, v] * m_dh.nu[c] for c in C_) == 0
                else:
                    # This variable does not appear in any constraint
                    return pyo.Constraint.Skip

        else:
            m_dh.J = jac_eq

            def eq_degenerate(m_dh, v):
                if np.sum(np.abs(m_dh.J[:, v])) > 1E-6:
                    return sum(m_dh.J[c, v] * m_dh.nu[c] for c in m_dh.C) == 0
                else:
                    # This variable does not appear in any constraint
                    return pyo.Constraint.Skip

        m_dh.pprint()

        m_dh.degenerate = pyo.Constraint(m_dh.V, rule=eq_degenerate)

        # When y_pos = 1, nu >= m_small
        # When y_pos = 0, nu >= - m_small
        def eq_pos_lower(m_dh, c):
            return m_dh.nu[c] >= -m_small + 2 * m_small * m_dh.y_pos[c]

        m_dh.pos_lower = pyo.Constraint(m_dh.C, rule=eq_pos_lower)

        # When y_pos = 1, nu <= M + m_small
        # When y_pos = 0, nu <= m_small
        def eq_pos_upper(m_dh, c):
            return m_dh.nu[c] <= M * m_dh.y_pos[c] + m_small

        m_dh.pos_upper = pyo.Constraint(m_dh.C, rule=eq_pos_upper)

        # When y_neg = 1, nu <= -m_small
        # When y_neg = 0, nu <= m_small
        def eq_neg_upper(m_dh, c):
            return m_dh.nu[c] <= m_small - 2 * m_small * m_dh.y_neg[c]

        m_dh.neg_upper = pyo.Constraint(m_dh.C, rule=eq_neg_upper)

        # When y_neg = 1, nu >= -M - m_small
        # When y_neg = 0, nu >= - m_small
        def eq_neg_lower(m_dh, c):
            return m_dh.nu[c] >= -M * m_dh.y_neg[c] - m_small

        m_dh.neg_lower = pyo.Constraint(m_dh.C, rule=eq_neg_lower)

        # Absolute value
        def eq_abs_lower(m_dh, c):
            return -m_dh.abs_nu[c] <= m_dh.nu[c]

        m_dh.abs_lower = pyo.Constraint(m_dh.C, rule=eq_abs_lower)

        def eq_abs_upper(m_dh, c):
            return m_dh.nu[c] <= m_dh.abs_nu[c]

        m_dh.abs_upper = pyo.Constraint(m_dh.C, rule=eq_abs_upper)

        # At least one constraint must be in the degenerate set
        m_dh.degenerate_set_nonempty = pyo.Constraint(
            expr=sum(m_dh.y_pos[c] + m_dh.y_neg[c] for c in m_dh.C) >= 1)

        # Minimize the L1-norm of nu
        m_dh.obj = pyo.Objective(expr=sum(m_dh.abs_nu[c] for c in m_dh.C))

        return m_dh

    # TODO: Refactor, this should not be a staticmethod
    @staticmethod
    def _check_candidate_ids(ids_milp, solver, c, eq_con_list=None, tee=False):
        ''' Solve MILP to check if equation 'c' is a significant component in an irreducible
            degenerate set
            
            Arguments:
                ids_milp: Pyomo model to calculate IDS
                solver: Pyomo solver (must support MILP)
                c: index for the constraint to consider [integer]
                eq_con_list: names of equality constraints. If none, use elements of ids_milp (default=None)
                tee: Boolean, print solver output (default = False)

            Returns:
                ids: either None or dictionary containing the IDS
        '''

        # Fix weight on candidate equation
        ids_milp.nu[c].fix(1.0)

        # Solve MILP
        results = solver.solve(ids_milp, tee=tee)

        ids_milp.nu[c].unfix()

        if pyo.check_optimal_termination(results):
            # We found an irreducible degenerate set

            # Create empty dictionary
            ids_ = {}

            for i in ids_milp.C:
                # Check if constraint is included
                if ids_milp.y[i]() > 0.9:
                    # If it is, save the value of nu
                    if eq_con_list is None:
                        name = i
                    else:
                        name = eq_con_list[i]
                    ids_[name] = ids_milp.nu[i]()
            return ids_
        else:
            return None

    # TODO: Refactor, this should not be a staticmethod
    @staticmethod
    def _find_candidate_eqs(candidates_milp,
                            solver,
                            eq_con_list=None,
                            tee=False):
        ''' Solve MILP to generate set of candidate equations
            
            Arguments:
                candidates_milp: Pyomo model to calculate IDS
                solver: Pyomo solver (must support MILP)
                eq_con_list: names of equality constraints. If none, use elements of ids_milp (default=None)
                tee: Boolean, print solver output (default = False)

            Returns:
                candidate_eqns: either None or list of indicies
                degenerate_set: either None or dictionary containing the degenerate_set
        '''

        results = solver.solve(candidates_milp, tee=tee)

        if pyo.check_optimal_termination(results):
            # We found a degenerate set

            # Create empty dictionary
            ds_ = {}

            # Create empty list
            candidate_eqns = []

            for i in candidates_milp.C:
                # Check if constraint is included
                if candidates_milp.abs_nu[i](
                ) > candidates_milp.m_small * 0.99:
                    # If it is, save the value of nu
                    if eq_con_list is None:
                        name = i
                    else:
                        name = eq_con_list[i]
                    ds_[name] = candidates_milp.nu[i]()
                    candidate_eqns.append(i)

            return candidate_eqns, ds_
        else:
            return None, None

    def svd_analysis(self, n_smallest_sv=10):
        '''
        Perform SVD analysis of the constraint Jacobian
        
        Args:
            n_smallest_sv: number of smallest singular values to compute
            
        Returns:
            Nothing
            
        Actions:
            Stores SVD results in object
        
        '''

        if self.n_eq > 1:

            # Determine the number of singular values to compute
            # The "-1" is needed to avoid an error with svds
            n_sv = min(n_smallest_sv, min(self.n_eq, self.n_var) - 1)
            print("Computing the", n_sv, "smallest singular value(s)")

            # Perform SVD
            # Recall J is a n_eq x n_var matrix
            # Thus U is a n_eq x n_eq matrix
            # And V is a n_var x n_var
            # (U or V may be smaller in economy mode)
            # Thus we really only care about U
            u, s, v = svds(self.jac_eq, k=n_sv, which='SM')

            # Save results
            self.u = u
            self.s = s
            self.v = v

        else:
            print(
                "Warning: model must contain at least 2 equality constraints to perform svd_analysis"
            )

    def find_candidate_equations(self, verbose=True, tee=False):
        '''
        Solve MILP to find a degenerate set and candidate equations
        
        Args:
            verbose: Print information to the screen (default=True)
            tee: Print solver output to screen (default=True)
        
        Returns:
            ds: either None or dictionary of candidate equations
        
        '''

        if verbose:
            print("*** Searching for a Single Degenerate Set ***")
            print("Building MILP model...")
        self.candidates_milp = self._prepare_find_candidates_milp(
            self.jac_eq, self.max_nu, self.min_nonzero_nu)

        if verbose:
            print("Solving MILP model...")
        ce, ds = self._find_candidate_eqs(self.candidates_milp, self.solver,
                                          self.eq_con_list, tee)

        if ce is not None:
            self.candidate_eqns = ce

        return ds

    def find_irreducible_degenerate_sets(self, verbose=True, tee=False):
        """
        Compute irreducible degenerate sets
        
        Args:
            verbose: Print information to the screen (default=True)
            tee: Print solver output to screen (default=True)
        
        Returns:
            irreducible_degenerate_sets: list of irreducible degenerate sets
        
        """

        # If there are no candidate equations, find them!
        if not self.candidate_eqns:
            self.find_candidate_equations()

        irreducible_degenerate_sets = []

        # Check if it is empty or None
        if self.candidate_eqns:

            if verbose:
                print("*** Searching for Irreducible Degenerate Sets ***")
                print("Building MILP model...")
            self.dh_milp = self._prepare_ids_milp(self.jac_eq, self.max_nu)

            # Loop over candidate equations
            for i, c in enumerate(self.candidate_eqns):

                if verbose:
                    print("Solving MILP", i + 1, "of",
                          len(self.candidate_eqns), "...")

                # Check if equation 'c' is a major element of an IDS
                ids_ = self._check_candidate_ids(self.dh_milp, self.solver, c,
                                                 self.eq_con_list, tee)

                if ids_ is not None:
                    irreducible_degenerate_sets.append(ids_)

            if verbose:
                for i, s in enumerate(irreducible_degenerate_sets):
                    print("\nIrreducible Degenerate Set", i)
                    print("nu\tConstraint Name")
                    for k, v in s.items():
                        print(v, "\t", k)
        else:
            print("No candidate equations. The Jacobian is likely full rank.")

        return irreducible_degenerate_sets

    ### Helper Functions

    # Note: This makes sense as a static method
    @staticmethod
    def print_variable_bounds(v):
        ''' Print variable, bounds, and value
        Argument:
            v: variable
        Return:
            nothing
        '''
        print(v, "\t\t", v.lb, "\t", v.value, "\t", v.ub)