-
Notifications
You must be signed in to change notification settings - Fork 280
Improved right-hand side for NSEM primary secondary #1661
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
base: main
Are you sure you want to change the base?
Conversation
|
Looking through the code, the only thing this really does different to the current implementation is that it solves the primary problem on a mesh that is padded beyond the current mesh correct? Is there any reason we don't just use the analytic response for this in general? |
I wasn't quite sure what boundary conditions were being imposed originally because the underlying math wasn't documented. I only knew that they weren't correct. One thing this PR is working towards is better organization of the NSEM module and setting things up to add the fictitious sources approach. |
|
I think I see more now looking at it. Currently the Primary field solution uses a Dirichlet Boundary condition on the top and bottom nodes with values taken from the analytic solution. It would appear that in this proposal that you're using a Neumann boundary on and at the bottom you're using a Robin boundary condition (or at least something close to one), If so, then it is indeed different, and we can likely simplify up this implementation a decent amount using some of |
| # Extract vertical discretization | ||
| if mesh.dim == 1: | ||
| hz = mesh.h | ||
| else: | ||
| hz = mesh.h[-1] | ||
|
|
||
| if len(hz) != len(sigma_1d): | ||
| raise ValueError( | ||
| "Number of cells in vertical direction must match length of 'sigma_1d'. Here hz has length {} and sigma_1d has length {}".format( | ||
| len(hz), len(sigma_1d) | ||
| ) | ||
| ) | ||
|
|
||
| # Generate extended 1D mesh and model to solve 1D problem | ||
| hz_ext = np.r_[hz[0] * np.ones(n_pad), hz, hz[-1] * np.ones(n_pad)] | ||
| mesh_1d_ext = TensorMesh([hz_ext], origin=[mesh.origin[-1] - hz[0] * n_pad]) | ||
|
|
||
| sigma_1d_ext = np.r_[ | ||
| sigma_1d[0] * np.ones(n_pad), sigma_1d, sigma_1d[-1] * np.ones(n_pad) | ||
| ] | ||
| sigma_1d_ext = mesh_1d_ext.average_face_to_cell.T * sigma_1d_ext | ||
| sigma_1d_ext[0] = sigma_1d[1] | ||
| sigma_1d_ext[-1] = sigma_1d[-2] | ||
|
|
||
| # Solve the 1D problem for electric fields on nodes | ||
| w = 2 * np.pi * freq | ||
| k = np.sqrt(-1.0j * w * mu_0 * sigma_1d_ext[0]) | ||
|
|
||
| A = ( | ||
| mesh_1d_ext.nodal_gradient.T @ mesh_1d_ext.nodal_gradient | ||
| + 1j * w * mu_0 * sdiag(sigma_1d_ext) | ||
| ) | ||
| A[0, 0] = (1.0 + 1j * k * hz[0]) / hz[0] ** 2 + 1j * w * mu_0 * sigma_1d[0] | ||
| A[0, 1] = -1 / hz[0] ** 2 | ||
|
|
||
| q = np.zeros(mesh_1d_ext.n_faces, dtype=np.complex128) | ||
| q[-1] = -1j * w * mu_0 / hz[-1] | ||
|
|
||
| Ainv = Solver(A) | ||
| e_1d = Ainv * q | ||
|
|
||
| # Return solution along original vertical discretization | ||
| return e_1d[n_pad:-n_pad] |
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.
| # Extract vertical discretization | |
| if mesh.dim == 1: | |
| hz = mesh.h | |
| else: | |
| hz = mesh.h[-1] | |
| if len(hz) != len(sigma_1d): | |
| raise ValueError( | |
| "Number of cells in vertical direction must match length of 'sigma_1d'. Here hz has length {} and sigma_1d has length {}".format( | |
| len(hz), len(sigma_1d) | |
| ) | |
| ) | |
| # Generate extended 1D mesh and model to solve 1D problem | |
| hz_ext = np.r_[hz[0] * np.ones(n_pad), hz, hz[-1] * np.ones(n_pad)] | |
| mesh_1d_ext = TensorMesh([hz_ext], origin=[mesh.origin[-1] - hz[0] * n_pad]) | |
| sigma_1d_ext = np.r_[ | |
| sigma_1d[0] * np.ones(n_pad), sigma_1d, sigma_1d[-1] * np.ones(n_pad) | |
| ] | |
| sigma_1d_ext = mesh_1d_ext.average_face_to_cell.T * sigma_1d_ext | |
| sigma_1d_ext[0] = sigma_1d[1] | |
| sigma_1d_ext[-1] = sigma_1d[-2] | |
| # Solve the 1D problem for electric fields on nodes | |
| w = 2 * np.pi * freq | |
| k = np.sqrt(-1.0j * w * mu_0 * sigma_1d_ext[0]) | |
| A = ( | |
| mesh_1d_ext.nodal_gradient.T @ mesh_1d_ext.nodal_gradient | |
| + 1j * w * mu_0 * sdiag(sigma_1d_ext) | |
| ) | |
| A[0, 0] = (1.0 + 1j * k * hz[0]) / hz[0] ** 2 + 1j * w * mu_0 * sigma_1d[0] | |
| A[0, 1] = -1 / hz[0] ** 2 | |
| q = np.zeros(mesh_1d_ext.n_faces, dtype=np.complex128) | |
| q[-1] = -1j * w * mu_0 / hz[-1] | |
| Ainv = Solver(A) | |
| e_1d = Ainv * q | |
| # Return solution along original vertical discretization | |
| return e_1d[n_pad:-n_pad] | |
| def primary_e_1d_solution(mesh, sigma_1d, freq, n_pad=2000): | |
| # Extract vertical discretizations | |
| hz = mesh.h[-1] | |
| if len(hz) != len(sigma_1d): | |
| raise ValueError( | |
| "Number of cells in vertical direction must match length of 'sigma_1d'. Here hz has length {} and sigma_1d has length {}".format( | |
| len(hz), len(sigma_1d) | |
| ) | |
| ) | |
| # Generate extended 1D mesh and model to solve 1D problem | |
| hz = np.pad(hz, (n_pad, n_pad), mode='edge') | |
| mesh = discretize.TensorMesh([hz], origin=[mesh.origin[-1] - hz[0] * n_pad]) | |
| sigma_1d = np.pad(sigma_1d, (n_pad, n_pad), mode='edge') | |
| mu = np.full(mesh.n_cells, mu_0) | |
| # Solve the 1D problem for electric fields on nodes/faces | |
| G = mesh.nodal_gradient | |
| M_e_mui = mesh.get_edge_inner_product(mu, invert_model=True) | |
| M_f_sigma = mesh.get_face_inner_product(sigma_1d) | |
| omega = 2 * np.pi * freq | |
| k_bot = np.sqrt(-1.0j * omega * mu[0] * sigma[0]) | |
| q = np.zeros(mesh.n_nodes, dtype=np.complex128) | |
| q[-1] = -1j * omega | |
| A = G.T @ M_e_mui @ G + 1j * omega * M_f_sigma | |
| A[0, 0] += 1j * k_bot/mu[0] | |
| Ainv = get_default_solver()(A) | |
| e_1d = Ainv @ q | |
| # Return solution along original vertical discretization | |
| if n_pad != 0: | |
| e_1d = e_1d[n_pad:-n_pad] | |
| return e_1d |
This is a bit cleaner code to understand imo, and also allows for potentially including spatially variable
|
Went through my own derivation to double check the boundary conditions, see my notes below, but I do get to the same result as your code, but I'm just not convinced it is a better boundary condition than just using the analytic as dirichlet conditions, here are the real and imaginary parts for a simple layered model: DerivationFirst the PDE you are solving, with boundary conditions clearly stated. s.t. and in a weak form, Discretizing this with an E-B formulation with Using the two boundary conditions to solve for Which gives multiply by Substitute in discretized Faraday's Law, Re-arrange So then system of equations is then: Where |
Ya, basically. I wanted to use the prebuilt operators in discretize for boundary conditions but couldn't quite figure it out. |
My derivation is almost the identical. However, I'm pretty sure you have a sign missing on the boundary conditions at the top. There are two negatives that cancel out. |
Devin' DerivationThe system: Boundary condition at the top: We set: Therefore from Faraday's law: Boundary condition at the bottom: At the bottom, there is only a downgoing wave of the form: where So if And the boundary condition here is: Inner products: Inner-products: Integrate second equation by parts: Discrete system: Get discrete systems (multiply second equation by Which gives us: Or if |
Ah yep, missed that on the copy over. |
Can you clarify what is meant by 'analytic as Dirichlet conditions'? Do you just mean setting ex=1 on the top? I remember there being a reason I liked setting the top boundary condition using the Neumann condition but I can't remember why at the moment. |
|
The current formulation sets Dirichlet conditions on E at the top and bottom boundary. Their values come from the analytic solution of a layered earth, normalized to 1 at the top. |
jcapriot
left a comment
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.
You're still re-creating a lot of the inner_product functionality from discretize here for 1D making it a bit more difficult to maintain. See my previous suggested change for a simpler implementation.
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #1661 +/- ##
==========================================
- Coverage 81.52% 80.39% -1.13%
==========================================
Files 420 418 -2
Lines 55174 54778 -396
Branches 5254 5259 +5
==========================================
- Hits 44978 44040 -938
- Misses 8790 9344 +554
+ Partials 1406 1394 -12 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
…b.com/simpeg/simpeg into dcowan/nsem_sources_primary_secondary
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Set default for bot_bc to "robin". Stable when 1D mesh has extreme padding




Summary
This PR is meant to improve the 3D NSEM primary secondary formulation. More suitable boundary conditions are implemented when solving the 1D problem that is used as the primary solution on the 3D mesh.
PR Checklist
expect style.
to a Pull Request
@simpeg/simpeg-developerswhen ready for review.What does this implement/fix?
The boundary conditions that are currently used to solve the 1D problem for the 3D primary-secondary simulation are sub-optimal. As a result, one needs to pad out excessively when using the 3D primary secondary formulation to obtain numerically accurate results. Here, more suitable boundary conditions are implemented to solve the 1D problem.