-
Notifications
You must be signed in to change notification settings - Fork 235
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
More efficient method for check_parallel_jacobian #1395
base: main
Are you sure you want to change the base?
Changes from 2 commits
62a6346
6a46229
5e4da39
8fbb339
da664b0
9b8a25d
7fe9c5e
ab30049
8c9959c
5021f29
e4ce707
8438d09
e048a22
6d53c8d
12109f9
e175405
8cedc01
a8da700
345a813
330953f
5939198
e30c6d5
50903cc
a54f189
9bd6ce8
0cd2d2d
42e00a2
940d253
c922565
fb1bb91
e226647
1aed06f
02db37e
631e7c5
bf5f26d
ffd0071
10250a5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -27,7 +27,7 @@ | |
import numpy as np | ||
from scipy.linalg import svd | ||
from scipy.sparse.linalg import svds, norm | ||
from scipy.sparse import issparse, find | ||
from scipy.sparse import issparse, find, triu | ||
|
||
from pyomo.environ import ( | ||
Binary, | ||
|
@@ -3653,6 +3653,72 @@ def check_parallel_jacobian(model, tolerance: float = 1e-4, direction: str = "ro | |
|
||
jac, nlp = get_jacobian(model, scaled=False) | ||
|
||
# Get vectors that we will check, and the Pyomo components | ||
# they correspond to. | ||
if direction == "row": | ||
components = nlp.get_pyomo_constraints() | ||
mat = jac.tocsr() | ||
|
||
elif direction == "column": | ||
components = nlp.get_pyomo_variables() | ||
mat = jac.transpose().tocsr() | ||
|
||
norms = [norm(mat[i, :], ord="fro") for i in range(len(components))] | ||
|
||
# Take product of all rows/columns with all rows/columns by taking outer | ||
# product of matrix with itself | ||
outer = mat @ mat.transpose() | ||
# Get rid of duplicate values by only taking upper triangular part of | ||
# resulting matrix | ||
upper_tri = triu(outer) | ||
# List to store pairs of parallel components | ||
parallel = [] | ||
|
||
for row, col, val in zip(*find(upper_tri), strict=True): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When I try to test this, I get an error due to this keyword argument passed to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry about that. No, For reference |
||
if row == col: | ||
# A vector is parallel to itself | ||
continue | ||
diff = abs(abs(val) - norms[row] * norms[col]) | ||
if diff <= tolerance or diff <= tolerance * max(norms[row], norms[col]): | ||
parallel.append((components[row], components[col])) | ||
|
||
return parallel | ||
|
||
|
||
def check_parallel_jacobian_old(model, tolerance: float = 1e-4, direction: str = "row"): | ||
dallan-keylogic marked this conversation as resolved.
Show resolved
Hide resolved
|
||
""" | ||
Check for near-parallel rows or columns in the Jacobian. | ||
|
||
Near-parallel rows or columns indicate a potential degeneracy in the model, | ||
as this means that the associated constraints or variables are (near) | ||
duplicates of each other. | ||
|
||
This method is based on work published in: | ||
|
||
Klotz, E., Identification, Assessment, and Correction of Ill-Conditioning and | ||
Numerical Instability in Linear and Integer Programs, Informs 2014, pgs. 54-108 | ||
https://pubsonline.informs.org/doi/epdf/10.1287/educ.2014.0130 | ||
|
||
Args: | ||
model: model to be analysed | ||
tolerance: tolerance to use to determine if constraints/variables are parallel | ||
direction: 'row' (default, constraints) or 'column' (variables) | ||
|
||
Returns: | ||
list of 2-tuples containing parallel Pyomo components | ||
|
||
""" | ||
# Thanks to Robby Parker for the sparse matrix implementation and | ||
# significant performance improvements | ||
|
||
if direction not in ["row", "column"]: | ||
raise ValueError( | ||
f"Unrecognised value for direction ({direction}). " | ||
"Must be 'row' or 'column'." | ||
) | ||
|
||
jac, nlp = get_jacobian(model, scaled=False) | ||
|
||
# Get vectors that we will check, and the Pyomo components | ||
# they correspond to. | ||
if direction == "row": | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The previous implementation was designed to avoid taking every possible dot product by first sorting vectors by their nonzero structure. I would have expected this to be slow due to unnecessary dot products, but in some testing, it doesn't seem slower.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is sparse matrix multiplication carried out by Scipy. The key here is that 1) Sorting through which products to take should be carried out in the sparse matrix multiplication routine, not manually and
2) The looping required to do this sorting and multiplication is done in compiled C++ code, not Python code, and (should) be much faster than doing it in Python code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The scipy matrix product is taking every possible combination of non-zero dot products, while the previous implementation takes only dot products between vectors with the same sparsity structure. This is fewer dot products, although, in practice, probably not by a wide margin.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm afraid I don't entirely follow. Scipy's matrix multiply, as near as I can tell, does the same thing. It implements the SMMP algorithm which occurs in two steps: the first to determine the nonzero structure of the matrix, the second to actually compute the values (as near as I can tell). That's basically what you're doing, except 1) it isn't filtering out entries as being "too small to contribute" and 2) it's doing it in C++ instead of Python, which is much faster.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For example, if two rows are
(1, 2, 3)
and(1, 2, 0)
, they cannot possibly be parallel. The previous implementation will not compute this dot product, while the new implementation will.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So you're ruling out rows being parallel based on one having an element for the third index (3) and the other having zero for the third index? That can fail if the first row was
(1, 2, 3e-8)
instead of(1, 2, 3)
:(1, 2, 3e-8)
is still effectively parallel to(1, 2, 0)
even if they structurally differ.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We do this after filtering out small entries.