Skip to content

Commit ad1330a

Browse files
authored
Merge pull request #61 from TAMUparametric/revertas
Adds method for overdetermined active sets for geometric algorithm
2 parents 43b288a + 8632f63 commit ad1330a

File tree

9 files changed

+105
-24
lines changed

9 files changed

+105
-24
lines changed

doc/multiobjective.rst

Whitespace-only changes.

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from setuptools import find_packages, setup
22

3-
__version__ = "1.6.8"
3+
__version__ = "1.6.9"
44

55
short_desc = (
66
"Extensible Multiparametric Solver in Python"

src/ppopt/mp_solvers/solver_utils.py

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@ def manufacture_lambda(attempted, murder_list):
6666
return lambda x: x not in attempted and murder_list.check(x)
6767

6868

69-
def generate_reduce(candidate: tuple, murder_list=None, attempted=None, equality_set: Optional[Set[int]] = None) -> list:
69+
def generate_reduce(candidate: tuple, murder_list=None, attempted=None,
70+
equality_set: Optional[Set[int]] = None) -> list:
7071
if equality_set is None:
7172
equality_set = set()
7273

@@ -164,6 +165,43 @@ def check(x) -> bool:
164165
return [[*active_set, i] for i in range(active_set[-1] + 1, num_constraints) if check([*active_set, i])]
165166

166167

168+
def find_sub_active_set(program: Union[MPLP_Program, MPQP_Program], active_set: List[int]) -> List[int]:
169+
"""
170+
In the situation that there is an overdetermined active set we need to find the subset that is full rank and allows
171+
for generating active sets that are well defined.
172+
173+
174+
"""
175+
176+
eq_cons = program.equality_indices
177+
ineq_constraints = [i for i in active_set if i not in eq_cons]
178+
kept_inequalities = []
179+
180+
current_rank = 0
181+
182+
if len(eq_cons) == 0:
183+
current_rank = 0
184+
else:
185+
current_rank = numpy.linalg.matrix_rank(program.A[eq_cons])
186+
187+
for i in ineq_constraints:
188+
189+
trial_ineq = [*kept_inequalities, i]
190+
191+
A_block = numpy.block([[program.A[eq_cons]], [program.A[trial_ineq]]])
192+
193+
A_rank = numpy.linalg.matrix_rank(A_block)
194+
195+
if A_rank > current_rank:
196+
kept_inequalities.append(i)
197+
current_rank = A_rank
198+
199+
if current_rank == program.num_x():
200+
return [*eq_cons, *kept_inequalities]
201+
202+
return [*eq_cons, *kept_inequalities]
203+
204+
167205
def get_facet_centers(A: numpy.ndarray, b: numpy.ndarray) -> List[Tuple[numpy.ndarray, numpy.ndarray, float]]:
168206
r"""
169207
This takes the polytope P, and finds all the chebychev centers and normal vectors of each facet and the radius.
@@ -192,7 +230,6 @@ def get_facet_centers(A: numpy.ndarray, b: numpy.ndarray) -> List[Tuple[numpy.nd
192230
if chev_ball is not None:
193231
theta = chev_ball.sol[:-1]
194232
radius = chev_ball.sol[-1]
195-
196233
if theta is None:
197234
continue
198235

@@ -254,6 +291,10 @@ def fathem_facet(center: numpy.ndarray, normal: numpy.ndarray, radius: float, pr
254291
# noinspection PyTypeChecker
255292
projected_set: List[int] = sol.active_set.tolist()
256293

294+
# if we have an overdetermined active set we need to find the subset that is full rank
295+
if len(projected_set) > program.num_x():
296+
projected_set = find_sub_active_set(program, projected_set)
297+
257298
# test for accidental self inclusion
258299
if projected_set == current_active_set:
259300
continue

src/ppopt/solver_interface/cvxopt_interface.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,22 @@ def process_cvxopt_solution(sol, equality_constraints, inequality_constraints, n
3232
lagrange[equality_constraints] = -numpy.array(sol['y']).flatten()
3333
lagrange[inequality_constraints] = -numpy.array(sol['z']).flatten()
3434

35-
non_zero_duals = numpy.where(lagrange != 0)[0]
36-
active_set = numpy.array([i for i in range(num_constraints) if i in non_zero_duals or i in equality_constraints])
35+
active = []
36+
37+
for i in range(num_constraints):
38+
if abs(slack[i]) <= 10 ** -10 or lagrange[i] != 0:
39+
active.append(i)
40+
41+
active = numpy.array(active)
42+
43+
# non_zero_duals = numpy.where(lagrange != 0)[0]
44+
# active_set = numpy.array([i for i in range(num_constraints) if i in non_zero_duals or i in equality_constraints])
3745

3846
if not get_duals:
3947
lagrange = None
4048

4149
return SolverOutput(obj=sol['primal objective'], sol=numpy.array(sol['x']).flatten(), slack=slack,
42-
active_set=active_set,
50+
active_set=active,
4351
dual=lagrange)
4452

4553

src/ppopt/solver_interface/daqp_solver_interface.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,13 @@ def solve_qp_daqp(Q: numpy.ndarray, c: Matrix, A: Matrix, b: Matrix,
9393

9494
slack = b - A @ make_column(x_star)
9595

96-
non_zero_duals = numpy.where(lagrange != 0)[0]
97-
active_set = numpy.array([i for i in range(num_constraints) if i in non_zero_duals or i in equality_constraints])
96+
active = []
9897

99-
return SolverOutput(opt, x_star, slack.flatten(), numpy.array(active_set).astype('int64'), lagrange)
98+
for i in range(num_constraints):
99+
if abs(slack[i]) <= 10 ** -10 or lagrange[i] != 0:
100+
active.append(i)
101+
102+
# non_zero_duals = numpy.where(lagrange != 0)[0]
103+
# active_set = numpy.array([i for i in range(num_constraints) if i in non_zero_duals or i in equality_constraints])
104+
105+
return SolverOutput(opt, x_star, slack.flatten(), numpy.array(active).astype('int64'), lagrange)

src/ppopt/solver_interface/gurobi_solver_interface.py

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -132,14 +132,7 @@ def solve_miqp_gurobi(Q: Matrix = None, c: Matrix = None, A: Matrix = None,
132132
sol.dual = numpy.array(model.getAttr("Pi"))
133133

134134
sol.slack = numpy.array(model.getAttr("Slack"))
135-
136-
if sol.dual is not None:
137-
# we have a continuous problem, and we can get the duals directly
138-
non_zero_duals = numpy.where(sol.dual != 0)[0]
139-
sol.active_set = numpy.array(
140-
[i for i in range(num_constraints) if i in non_zero_duals or i in equality_constraints])
141-
else:
142-
sol.active_set = numpy.where((A @ sol.sol.flatten() - b.flatten()) ** 2 < 10 ** -12)[0]
135+
sol.active_set = numpy.where((A @ sol.sol.flatten() - b.flatten()) ** 2 < 10 ** -12)[0]
143136

144137
return sol
145138

src/ppopt/solver_interface/quad_prog_interface.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,18 +68,21 @@ def solve_qp_quadprog(Q: numpy.ndarray, c: numpy.ndarray, A: numpy.ndarray, b: n
6868
x_star = sol[0]
6969
opt = sol[1]
7070
duals = sol[4]
71-
# active = []
71+
active = []
7272
indexing = [*equality_constraints, *ineq]
7373
lagrange[indexing] = duals
7474
lagrange[ineq] = -lagrange[ineq]
7575

7676
slack = b - A @ make_column(x_star)
77+
for i in range(num_constraints):
78+
if abs(slack[i]) <= 10 ** -10 or lagrange[i] != 0:
79+
active.append(i)
7780

78-
non_zero_duals = numpy.where(lagrange != 0)[0]
79-
active_set = numpy.array(
80-
[i for i in range(num_constraints) if i in non_zero_duals or i in equality_constraints])
81+
# non_zero_duals = numpy.where(lagrange != 0)[0]
82+
# active_set = numpy.array(
83+
# [i for i in range(num_constraints) if i in non_zero_duals or i in equality_constraints])
8184

82-
return SolverOutput(opt, x_star, slack.flatten(), numpy.array(active_set).astype('int64'), lagrange)
85+
return SolverOutput(opt, x_star, slack.flatten(), numpy.array(active).astype('int64'), lagrange)
8386

8487
except ValueError:
8588
# just swallow the error as something happened Infeasibility or non-symmetry

tests/other_tests/test_solve_mpqp.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from src.ppopt.utils.chebyshev_ball import chebyshev_ball
44
from src.ppopt.mp_solvers import mpqp_parrallel_combinatorial
55
from src.ppopt.mp_solvers.solve_mpqp import mpqp_algorithm, solve_mpqp
6-
from tests.test_fixtures import qp_problem, simple_mpLP, portfolio_problem_analog, non_negative_least_squares
6+
from tests.test_fixtures import qp_problem, simple_mpLP, portfolio_problem_analog, non_negative_least_squares, over_determined_as_mplp
77

88

99
def test_solve_mpqp_combinatorial(qp_problem):
@@ -84,6 +84,12 @@ def test_solve_mplp_combinatorial(simple_mpLP):
8484
assert solution is not None
8585
assert len(solution.critical_regions) == 4
8686

87+
def tesT_solve_mplp_overdetermined_active_set(over_determined_as_mplp):
88+
89+
solution = solve_mpqp(over_determined_as_mplp, mpqp_algorithm.combinatorial)
90+
assert solution is not None
91+
assert len(solution.critical_regions) == 5
92+
8793

8894
def test_solve_missing_algorithm(qp_problem):
8995
try:

tests/test_fixtures.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -532,4 +532,28 @@ def pappas_multi_objective_2():
532532
m.add_constrs(x[i] >= 0 for i in range(1, 3))
533533
m.add_constrs(x[i] <= 100 for i in range(1, 3))
534534

535-
return m.formulate_problem()
535+
return m.formulate_problem()
536+
537+
@pytest.fixture()
538+
def over_determined_as_mplp():
539+
"""
540+
The relaxation of this mpMILP was found to generate over determined active sets in some regions of the parametric
541+
space. So it would cause problems with incomplete solutions.
542+
543+
"""
544+
A = numpy.array(
545+
[[1, 1, 0, 0, 0], [0, 0, 1, 1, 0], [-1, 0, -1, 0, 0], [0, -1, 0, -1, -500], [-1, 0, 0, 0, 0], [0, -1, 0, 0, 0],
546+
[0, 0, -1, 0, 0], [0, 0, 0, -1, 0], [0, 0, 0, 0, -1], [0, 0, 0, 0, 1]], float)
547+
b = numpy.array([350, 600, 0, 0, 0, 0, 0, 0, 0, 1], float).reshape(-1, 1)
548+
F = numpy.array([[0, 0], [0, 0], [-1, 0], [0, -1], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0]], float)
549+
A_t = numpy.array([[1.0, 0.0], [0.0, 1.0], [-1.0, 0.0], [0.0, -1.0]], float)
550+
b_t = numpy.array([[1000.0], [1000.0], [0.0], [0.0]], float)
551+
H = numpy.zeros([5, 2])
552+
binary_indices = [4]
553+
554+
Q = 2 * numpy.diag([153, 162, 162, 126, 0.1])
555+
c = numpy.array([25, 25, 25, 25, 100]).reshape(-1, 1)
556+
557+
miqp_prog = MPMILP_Program(A, b, c, H, A_t, b_t, F, binary_indices)
558+
559+
return miqp_prog.generate_relaxed_problem()

0 commit comments

Comments
 (0)