Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add unboundedness detection #16

Merged
merged 2 commits into from
Nov 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions pygradflow/iterate.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,9 @@ def stat_res(self) -> float:
r = self.obj_grad + self.cons_jac.T.dot(self.y) + self.bound_duals
return np.linalg.norm(r, np.inf)

def is_feasible(self, tol):
return (self.cons_violation <= tol) and (self.bound_violation <= tol)

@property
def total_res(self) -> float:
return max(self.cons_violation, self.bound_violation, self.stat_res)
2 changes: 2 additions & 0 deletions pygradflow/params.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ class Params:
time_limit: float = np.inf
display_interval: float = 0.1

obj_lower_limit: float = -1e10

@property
def dtype(self):
return np.float32 if self.precision == Precision.Single else np.float64
19 changes: 13 additions & 6 deletions pygradflow/solver.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class SolverStatus(Enum):
Converged = (auto(), "Convergence achieved")
IterationLimit = (auto(), "Reached iteration limit")
TimeLimit = (auto(), "Reached time limit")
Unbounded = (auto(), "Unbounded")
LocallyInfeasible = (auto(), "Local infeasibility detected")

def __new__(cls, value, description):
Expand All @@ -37,13 +38,16 @@ def success(status):


class SolverResult:
def __init__(self, x, y, d, success, status):
def __init__(self, x, y, d, status):
self.x = x
self.y = y
self.d = d
self.success = success
self.status = status

@property
def success(self):
return SolverStatus.success(self.status)


header_interval = 25

Expand Down Expand Up @@ -153,8 +157,6 @@ def solve(self, x_0: np.ndarray, y_0: np.ndarray) -> SolverResult:

lamb = params.lamb_init

success = True

controller = step_controller(problem, params)

self._deriv_check(x, y)
Expand Down Expand Up @@ -191,6 +193,12 @@ def solve(self, x_0: np.ndarray, y_0: np.ndarray) -> SolverResult:
status = SolverStatus.LocallyInfeasible
break

if (iterate.obj <= params.obj_lower_limit) and \
(iterate.is_feasible(params.opt_tol)):
logger.debug("Unboundedness detected")
status = SolverStatus.Unbounded
break

step_result = self.compute_step(controller, iterate, 1.0 / lamb)

x = iterate.x
Expand Down Expand Up @@ -252,7 +260,6 @@ def solve(self, x_0: np.ndarray, y_0: np.ndarray) -> SolverResult:
break

else:
success = False
status = SolverStatus.IterationLimit
logger.debug("Iteration limit reached")

Expand All @@ -274,4 +281,4 @@ def solve(self, x_0: np.ndarray, y_0: np.ndarray) -> SolverResult:
y = iterate.y
d = iterate.bound_duals

return SolverResult(x, y, d, success, status)
return SolverResult(x, y, d, status)
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "pygradflow"
version = "0.2.4"
version = "0.2.5"
description = "PyGradFlow is a simple implementation of the sequential homotopy method to be used to solve general nonlinear programs."
authors = ["Christoph Hansknecht <[email protected]>"]
readme = "README.md"
Expand Down
40 changes: 39 additions & 1 deletion tests/pygradflow/test_solver.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import numpy as np
import scipy as sp

import pytest

from pygradflow.implicit_func import ImplicitFunc
from pygradflow.iterate import Iterate
from pygradflow.solver import Solver
from pygradflow.problem import Problem
from pygradflow.solver import Solver, SolverStatus
from pygradflow.newton import newton_method

from pygradflow.params import (
Expand Down Expand Up @@ -251,3 +253,39 @@ def cons_jac(x):
invalid_col = jac.col[invalid_index]

assert (e.invalid_indices == [[invalid_row, invalid_col]]).all()


def test_detect_unbounded():
num_vars = 1

class UnboundedProblem(Problem):
def __init__(self):
var_lb = np.full(shape=(num_vars,), fill_value=-np.inf)
var_ub = np.full(shape=(num_vars,), fill_value=np.inf)
super().__init__(var_lb, var_ub)

def obj(self, x):
return x[0]

def obj_grad(self, x):
return np.array([1.] + [0.] * (num_vars - 1))

def cons(self, x):
return np.array([])

def cons_jac(self, x):
return sp.sparse.coo_matrix((0, num_vars))

def lag_hess(self, x, lag):
return sp.sparse.diags([0.]*num_vars)

problem = UnboundedProblem()

solver = Solver(problem)

x0 = np.array([0.0]*num_vars)
y0 = np.array([])

result = solver.solve(x0, y0)

assert result.status == SolverStatus.Unbounded