-
Notifications
You must be signed in to change notification settings - Fork 57
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
Cholesky decompositions of LinearOperator
s
#618
Cholesky decompositions of LinearOperator
s
#618
Conversation
Codecov Report
@@ Coverage Diff @@
## main #618 +/- ##
==========================================
+ Coverage 89.40% 89.52% +0.11%
==========================================
Files 181 181
Lines 6684 6875 +191
Branches 1026 1064 +38
==========================================
+ Hits 5976 6155 +179
- Misses 479 488 +9
- Partials 229 232 +3
|
6d65351
to
a50d087
Compare
LinearOperator
sLinearOperator
s
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.
It's great to finally have this property system. I have a few comments which should be easily resolved. Thanks for the effort! One question to think about for the future (not this PR) what do we do with semi-definite operators? Add another property _is_positive_semi_definite
? Often there are algorithms which also work if the linear operator is at least positive semi-definite.
numpy.linalg.LinAlgError | ||
If the :class:`LinearOperator` is not positive definite. | ||
""" | ||
if not self.is_symmetric: |
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.
What happens if the is_symmetric
and is_positive_definite
properties are None
, i.e. it is unknown whether the matrix is spd? According to the docstring .cholesky()
should still run and if it succeeds, set the properties accordingly. However if self.is_symmetric is None
, this conditional should throw an exception, if i'm not mistaken.
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.
According to the Raises
section of the docstring, the method raises an exception if is_symmetric
is not True
, i.e. the method will fail if it is not known that the operator is indeed symmetric.
Similarly I state that the operator raises an exception if the matrix is not positive definite, while still setting linop.is_positive_definite = False
.
This is intentional as it can be used as a test for positive definiteness.
So I don't really see a contradiction to the docstring here. Or am I missing something?
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.
My mistake! We might want to be lenient here and only raise an exception if the properties are False
. Otherwise this may lead to a lot of boilerplate in particular when prototyping. Your call.
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.
In fact, I think it would be more in line with Python philosophy to attempt the Cholesky decomposition if it is not known whether the linop is spd and then raise an exception should Cholesky fail.
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.
In principle, I agree with the second point. In fact this is what happens if is_positive_definite
is None
: We attempt the Cholesky and if it fails, we set it to False
and raise an exception. However, after giving this some thought, this issue is much more complicated with is_symmetric
.
Since matrices are rarely exactly symmetric, an automatic check of symmetry would be much too restrictive, unless we have some sort of "asymmetry tolerance", which is just another one of these arbitrary parameters to make things work. Personally, I came to the conclusion that it is better if the user has to call .symmetrize
on the operator before calling .cholesky
because it makes this whole symmetry business much more transparent.
Moreover, if symmetrize
is too costly, the user can always choose to set linop.symmetric = True
manually, even if the property is only fulfilled approximately.
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.
Note that NumPy and SciPy circumvent this issue by only operating on the lower triangle of the matrix.
>>> a = np.array([
... [1.0, 2.0],
... [0.9, 1.0],
... ])
>>> np.linalg.cholesky(a)
array([[1. , 0. ],
[0.9 , 0.43588989]])
>>> scipy.linalg.cholesky(a, lower=True)
array([[1. , 0. ],
[0.9 , 0.43588989]])
IMHO, this is quite dangerous, since it can produce very subtle bugs.
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.
Sounds good to me! Thanks for the detailed explanation.
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.
If we find a good solution, I'm also fine with removing this in the future.
However, I would propose to make this a separate PR, since this would increase complexity by quite a bit.
We can always make interfaces less restrictive
@@ -520,6 +709,59 @@ def inv(self) -> "LinearOperator": | |||
|
|||
return self.__inverse() | |||
|
|||
def symmetrize(self) -> LinearOperator: |
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.
By introduction of this method the linear operator Symmetrize
in _kronecker.py
should just be removed, I think. Otherwise there are two different ways of achieving the same thing.
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.
Symmetrize
in _kronecker.py
symmetrizes vectorized matrices used in Kronecker algebra, so it operates "on the arguments" of linear operators, while linop.symmetrize
symmetrizes the LinearOperator
itself, so I'd say they serve distinct purposes.
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.
They do, but Symmetrize
is really only used for SymmetricKronecker
and is arguably confusing in its naming if there is a symmetrize()
method. My suggestion would be to fold Symmetrize
into the SymmetricKronecker
implementation as a (future) refactor. I'm okay with considering this as out of scope for this PR.
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 agree, let's make a separate PR here 👍
I agree, we should add more matrix properties over time. I also think that we will need |
49fc7d2
to
66dfdb6
Compare
66dfdb6
to
285b304
Compare
numpy.linalg.LinAlgError | ||
If the :class:`LinearOperator` is not positive definite. | ||
""" | ||
if not self.is_symmetric: |
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.
My mistake! We might want to be lenient here and only raise an exception if the properties are False
. Otherwise this may lead to a lot of boilerplate in particular when prototyping. Your call.
@@ -520,6 +709,59 @@ def inv(self) -> "LinearOperator": | |||
|
|||
return self.__inverse() | |||
|
|||
def symmetrize(self) -> LinearOperator: |
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.
They do, but Symmetrize
is really only used for SymmetricKronecker
and is arguably confusing in its naming if there is a symmetrize()
method. My suggestion would be to fold Symmetrize
into the SymmetricKronecker
implementation as a (future) refactor. I'm okay with considering this as out of scope for this PR.
In a Nutshell
This PR implements Cholesky decompositions for ProbNum's
linops
library.To achieve this goal, I also had to implement symmetrization and parts of a matrix property system.
Detailed Description
LinearOperator.cholesky(lower: bool) -> LinearOperator
LinearOperator.symmetrize() -> LinearOperator
implementing "1/2 * (A + A^T)"is_symmetric
is_{upper,lower}_triangular
is_positive_definite
Kronecker.{inv,cond}
_InverseLinearOperator
uses Cholesky factorization for SPD matrices_InverseLinearOperator
of singular matrixRelated Issues
Implements parts of #569 and #571