-
Notifications
You must be signed in to change notification settings - Fork 196
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
Enforce gauge condition in preconditioners for ConjugateGradientPoissonSolver #3802
Conversation
P = mean(p) | ||
grid = solver.grid | ||
arch = architecture(grid) | ||
launch!(arch, grid, :xyz, subtract_and_mask!, p, grid, P) |
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.
@xkykai I think this is the crucial lines. Without this the pressure does not have a zero mean and I suspect that can throw off the CG solver. But I'm checking.
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.
@jm-c might be good to have your input
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 have found that I needed to enforce the zero mean (e.g., when solving Laplace's or Poisson's equation) when I was using the conjugate gradient solver with @elise-palethorpe to compare with MultiGrid
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 think this was a key ingredient missing from previous implementation. The FFT-based preconditioner zeros out the mean over the underlying grid, but does not zero out the mean on the immersed grid.
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.
Checked the derivation, looks good to me!
I like what's suggested in this PR! |
P = mean(p) | ||
grid = solver.grid | ||
arch = architecture(grid) | ||
launch!(arch, grid, :xyz, subtract_and_mask!, p, grid, P) |
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.
Checked the derivation, looks good to me!
@Yixiao-Zhang here's the implementation of the gauge fixing that you proposed! |
Isn't this a bit different from what @Yixiao-Zhang proposed? This PR uses the same criteria that we use for the FFT-based solver without immersed boundaries, ie set the mean pressure to 0: Oceananigans.jl/src/Solvers/fft_based_poisson_solver.jl Lines 112 to 115 in 45838a5
In what I saw, the mean pressure was not set to 0. |
It is different. I think the issue is that the conjugate gradient (CG) method can be numerically unstable for positive semi-definite matrices. I have a Python script (shown at the end of this comment) that demonstrates this. The residual keeps oscillating (see the attached figure). Besides, if we add a small random perturbation to There are two potential solutions:
My method belongs to the second category. I create a new matrix where In contrast, enforcing the zero mean belongs to the first category. It definitely helps, since the CG method will likely blow up if the mean is not zero (which means there is no solution). However, dealing with round-off errors is always tricky, especially in this case where fast convergence leads to a rapid decrease in the "signal-to-error ratio". import numpy as np
import matplotlib.pyplot as plt
def gram_schmidt(X, row_vecs=True, norm = True):
if not row_vecs:
X = X.T
Y = X[0:1,:].copy()
for i in range(1, X.shape[0]):
proj = np.diag((X[i,:].dot(Y.T)/np.linalg.norm(Y,axis=1)**2).flat).dot(Y)
Y = np.vstack((Y, X[i,:] - proj.sum(0)))
if norm:
Y = np.diag(1/np.linalg.norm(Y,axis=1)).dot(Y)
if row_vecs:
return Y
else:
return Y.T
def cg_solve(A,b,N):
x = np.zeros_like(b)
r = b - A.dot(x)
p = r.copy()
rs = []
for i in range(N):
rs.append(np.linalg.norm(r))
Ap = A.dot(p)
alpha = np.dot(p,r)/np.dot(p,Ap)
x = x + alpha*p
# r = b - A.dot(x) # a numerically more stable version
r -= alpha * Ap
beta = -np.dot(r,Ap)/np.dot(p,Ap)
p = r + beta*p
return np.array(rs)
def main():
N = 256
# create a positive definite diagonal matrix
matrix_D = np.diag(np.random.rand(N) + 1.0)
# create a vector
b0 = np.random.normal(size=N)
b0 /= np.linalg.norm(b0)
# set one eigenvalue value to zero and update the vector
matrix_D[0, 0] = 0.0
b0[0] = 0.0
# create an orthogonal matrix
matrix_q = gram_schmidt(np.random.normal(size=(N, N)))
# create a positive semi-definite symmetric matrix
matrix_A = np.dot(matrix_q, np.dot(matrix_D, matrix_q.T))
b = np.dot(matrix_q, b0)
# add a random perturbation
# b += 1e-5 * np.random.normal(size=N)
# return the residuals at every iteration
rs = cg_solve(matrix_A, b, N=64)
# plot residuals as a function of iterations
fig, ax = plt.subplots()
ax.plot(rs, marker='o')
ax.set_yscale('log')
ax.set_xlabel('Number of Iterations')
ax.set_ylabel('Residual')
plt.savefig('cg_residual.png')
if __name__ == '__main__':
main() |
Right, although I am not sure that this is done correctly in this PR. There may be more work to do...
I was a bit confused about the motivation for defining a new matrix, but with some help from @amontoison I feel I understand this better. The basic idea applies to any semi-definite system; the idea is to "shift" an operator by the identity: Where The key to this method is the smallness of We do not automatically have In addition to making which, we can show, allows us to recover For the purpose of implementing shifted operators, some methods in Krylov.jl support provision of a "shift" as an argument to the solver, eg the argument It'd be easy to support a shift of the Poisson operator, the caveat being that the problem being solved is no longer exact. We could additionally support finding the exact solution through a second CG iteration via the method described above, but this would be a bit more work. |
@glwagner The goal of shifting the system is to recover a positive definite matrix by adding \lambda to all eigenvalue of the problem. I just don't understand why we will find the same solution because CG should return one solution among an infinity of solution (before the shift). |
Okay thanks to you both, I think I understand this better. I was a bit confused by notation. Let's call where We write the Poisson equation where Another way to close the system is to change the Poisson equation. Written discretely the proposal is to use where where unlike the unregularized Poisson equation when In terms of implementation, this is simply implemented by defining a new linear operator that adds the additional term, eg Oceananigans.jl/src/Solvers/conjugate_gradient_poisson_solver.jl Lines 47 to 51 in 4f6ffd7
Here are a few more loose ends:
Oceananigans.jl/src/Solvers/fft_based_poisson_solver.jl Lines 109 to 115 in efb8b71
Therefore it is unclear to me whether additionally solving the regularized Poisson equation is necessary or valid when we already have a linear constraint embedded in the preconditioner. Also, note that the constrained implemented in this PR is simply a shifting of the constraint embedded in the FFT-based preconditioner. While the FFT preconditioner zeros out the mean pressure over the whole domain, the constraint in this PR takes into account the immersed boundary when zeroing the mean pressure.
The "infinity of solutions" differ only in their mean value right? Or in other words, in the arbitrary value |
I'm experimenting with this regularization on #3848 |
In my implementation (Yixiao-Zhang@c7983b8002b91cd5939018a7c999eae77e2105ac\), the preconditioner is also perturbed. The goal is to solve the following equation in an immersed boundary grid. The perturbed preconditioner solves this equation in a regular grid: where for a random It is easy to verify that the preconditioner gives the exact solution in a regular grid. Besides, in my implementation (Yixiao-Zhang@c7983b8002b91cd5939018a7c999eae77e2105ac\), I perturb the preconditioner directly by adding the mean of the input vector. However, a more efficient solution is to a new parameter to the FFT-based Poisson solver, so that it solves where |
One way to motivate "defining a new matrix" is to view the conjugate gradient iteration method as a process of minimization of the quadratic function We can see In our case, The problem becomes minimization of where Similarly, We can see Actually, we can verify |
Isn't this simply setting Also if we set Oceananigans.jl/src/Models/NonhydrostaticModels/solve_for_pressure.jl Lines 88 to 89 in efb8b71
then I suppose we would have |
Also does a bit of clean up.
In the future we need to move
ConjugateGradientPoissonSolver
toSolvers
module.Also I would like to rename
PreconditionedConjugateGradientSolver
to justConjugateGradientSolver
cause repetitive stress injury from too much typing is a real problem.