Functionalities for flux trapping analysis#461
Conversation
hughcars
left a comment
There was a problem hiding this comment.
I haven't managed to review the MFEM portion directly yet, but this should be a lot to be going on with. At a high level,
- check running on more than 2 ranks, I was getting errors for 3 or more from geom factors not being initialized etc.
- run with order >= 3, I was getting errors where the computed flux was not matching the target flux, not sure what is causing this directly.
On the interface, broadly think more about how to a) avoid any 3D object allocation within the surface pde solving function and b) pass such objects in as buffers. There are already existing A sized gridfunctions, and even a true sized A buffer that can be used to compute the RHS (The next solution vector has already been allocated, you can use that!)
|
In light of the recently merged #459, let's not forget to save all |
|
@hughcars All your requested changes are addressed. Sorry for the delay. Please have a look and feel free to take over if you feel it is more convenient and efficient that way. |
0d0272c to
c7581bf
Compare
hughcars
left a comment
There was a problem hiding this comment.
Thanks, this is looking close. I left a few small inline suggestions for the remaining cleanup/fixes I found while building and running the current PR head through Spack.
The easiest path is probably to cherry-pick the two fix-up commits from:
hughcars/flux-trapping-dev
Those commits are:
11b8ba92f Fix flux loop units and rebase issues285b2fc3b Add flux loop regression coverage
They include the inline-suggested changes, plus a few things that are not easy to express as GitHub suggestions because they touch unchanged lines or add new files:
- config validation for unsupported mixed
SurfaceCurrent+FluxLoopuse, - duplicate
Indexvalidation includingFluxLoop, - C++/Catch2 regression coverage for the circular-hole flux-loop example and reference outputs,
- SchemaVer bump to
1-2-0.
If applying fixes manually instead of cherry-picking, please also make sure the schema version is bumped as a REVISION bump (1-2-0), since this PR extends the accepted config model.
| MFEM_VERIFY(n_step > 0, "No surface current boundaries or flux loops specified for " | ||
| "magnetostatic simulation!"); | ||
| MFEM_VERIFY( |
There was a problem hiding this comment.
Add the solver-side backstop for the unsupported mixed case as well as config validation. This keeps accidental programmatic construction from reaching the mixed postprocessing path.
| MFEM_VERIFY(n_step > 0, "No surface current boundaries or flux loops specified for " | |
| "magnetostatic simulation!"); | |
| MFEM_VERIFY( | |
| MFEM_VERIFY(n_step > 0, "No surface current boundaries or flux loops specified for " | |
| "magnetostatic simulation!"); | |
| MFEM_VERIFY(n_current_steps == 0 || n_flux_steps == 0, | |
| "Combining SurfaceCurrent and FluxLoop excitations in the same " | |
| "magnetostatic simulation is not yet supported!"); | |
| MFEM_VERIFY( |
| if (n_current > 0 && n_flux > 0) | ||
| { | ||
| // Mixed current-flux case: use constraint system M×R = I | ||
| mfem::DenseMatrix R(n); | ||
|
|
||
| // Diagonal terms from energy | ||
| for (int i = 0; i < n; i++) | ||
| { | ||
| if (is_flux_loop[i]) | ||
| R(i, i) = cross_energy(i, i) / (Phi_inc[i] * Phi_inc[i]); | ||
| else | ||
| M(i, i) = cross_energy(i, i) / (I_inc[i] * I_inc[i]); | ||
| } | ||
|
|
||
| int n_off = n * (n - 1) / 2; // Number of off-diagonal elements | ||
| mfem::DenseMatrix A_sys(n * n, 2 * n_off); | ||
| mfem::Vector b_sys(n * n), x_sol(2 * n_off); | ||
| A_sys = 0.0; | ||
| b_sys = 0.0; | ||
|
|
||
| // Set up M×R = I constraint equations | ||
| for (int i = 0; i < n; i++) | ||
| { | ||
| for (int j = 0; j < n; j++) | ||
| { | ||
| int eq = i * n + j; | ||
| b_sys[eq] = (i == j) ? 1.0 : 0.0; | ||
|
|
||
| for (int k = 0; k < n; k++) | ||
| { | ||
| if (i != k && k != j) // Off-diagonal terms | ||
| { | ||
| int M_idx = (i < k) ? i * n + k - (i + 1) * (i + 2) / 2 | ||
| : k * n + i - (k + 1) * (k + 2) / 2; | ||
| int R_idx = (k < j) ? k * n + j - (k + 1) * (k + 2) / 2 | ||
| : j * n + k - (j + 1) * (j + 2) / 2; | ||
| A_sys(eq, M_idx) += (k == j) ? 1.0 : 0.0; | ||
| A_sys(eq, n_off + R_idx) += (i == k) ? 1.0 : 0.0; | ||
| } | ||
| } | ||
| // Diagonal contributions | ||
| if (i == j) | ||
| b_sys[eq] -= M(i, i) * R(j, j); | ||
| } |
There was a problem hiding this comment.
For this PR, I would delete the mixed SurfaceCurrent/FluxLoop reconstruction. The current MR = I loop builds a singular system, and the correct mixed formulation needs a separate hybrid/Legendre treatment.
| if (n_current > 0 && n_flux > 0) | |
| { | |
| // Mixed current-flux case: use constraint system M×R = I | |
| mfem::DenseMatrix R(n); | |
| // Diagonal terms from energy | |
| for (int i = 0; i < n; i++) | |
| { | |
| if (is_flux_loop[i]) | |
| R(i, i) = cross_energy(i, i) / (Phi_inc[i] * Phi_inc[i]); | |
| else | |
| M(i, i) = cross_energy(i, i) / (I_inc[i] * I_inc[i]); | |
| } | |
| int n_off = n * (n - 1) / 2; // Number of off-diagonal elements | |
| mfem::DenseMatrix A_sys(n * n, 2 * n_off); | |
| mfem::Vector b_sys(n * n), x_sol(2 * n_off); | |
| A_sys = 0.0; | |
| b_sys = 0.0; | |
| // Set up M×R = I constraint equations | |
| for (int i = 0; i < n; i++) | |
| { | |
| for (int j = 0; j < n; j++) | |
| { | |
| int eq = i * n + j; | |
| b_sys[eq] = (i == j) ? 1.0 : 0.0; | |
| for (int k = 0; k < n; k++) | |
| { | |
| if (i != k && k != j) // Off-diagonal terms | |
| { | |
| int M_idx = (i < k) ? i * n + k - (i + 1) * (i + 2) / 2 | |
| : k * n + i - (k + 1) * (k + 2) / 2; | |
| int R_idx = (k < j) ? k * n + j - (k + 1) * (k + 2) / 2 | |
| : j * n + k - (j + 1) * (j + 2) / 2; | |
| A_sys(eq, M_idx) += (k == j) ? 1.0 : 0.0; | |
| A_sys(eq, n_off + R_idx) += (i == k) ? 1.0 : 0.0; | |
| } | |
| } | |
| // Diagonal contributions | |
| if (i == j) | |
| b_sys[eq] -= M(i, i) * R(j, j); | |
| } |
| // Solve system | ||
| mfem::DenseMatrixInverse A_inv(A_sys); | ||
| A_inv.Mult(b_sys, x_sol); | ||
|
|
||
| // Extract off-diagonal elements | ||
| int idx = 0; | ||
| for (int i = 0; i < n; i++) |
There was a problem hiding this comment.
Delete the remainder of the mixed solve block.
| // Solve system | |
| mfem::DenseMatrixInverse A_inv(A_sys); | |
| A_inv.Mult(b_sys, x_sol); | |
| // Extract off-diagonal elements | |
| int idx = 0; | |
| for (int i = 0; i < n; i++) |
| for (int j = i + 1; j < n; j++) | ||
| { | ||
| M(i, j) = M(j, i) = x_sol[idx]; | ||
| R(i, j) = R(j, i) = x_sol[n_off + idx]; | ||
| idx++; | ||
| } | ||
| } | ||
|
|
||
| // Compute Minv = R | ||
| Minv = R; | ||
| } |
There was a problem hiding this comment.
Delete the remainder of the mixed solve block.
| for (int j = i + 1; j < n; j++) | |
| { | |
| M(i, j) = M(j, i) = x_sol[idx]; | |
| R(i, j) = R(j, i) = x_sol[n_off + idx]; | |
| idx++; | |
| } | |
| } | |
| // Compute Minv = R | |
| Minv = R; | |
| } |
| // Compute Minv = R | ||
| Minv = R; | ||
| } | ||
| else if (n_flux == n) |
There was a problem hiding this comment.
After deleting the mixed branch, the pure-flux path becomes the first branch.
| else if (n_flux == n) | |
| if (n_flux == n) |
| if constexpr (true) | ||
| { | ||
| // Use Palace's KSP solver (production approach) | ||
| X = 0.0; | ||
| ksp.Mult(RHS, X); | ||
|
|
||
| // Set solution directly in MFEM GridFunction | ||
| A.SetFromTrueDofs(X); | ||
| } | ||
| else | ||
| { | ||
| // Alternative debugging approach using direct MFEM solver | ||
| // Set up and solve system | ||
| mfem::ParBilinearForm a(&nd_fespace_submesh); | ||
|
|
||
| // Add curl term: ∫ (curl A) · (curl v) dΩ | ||
| mfem::ConstantCoefficient curl_reg(1.0); | ||
| a.AddDomainIntegrator(new mfem::CurlCurlIntegrator(curl_reg)); | ||
|
|
||
| // Add small regularization for stability | ||
| mfem::ConstantCoefficient reg_param(1e-6); | ||
| a.AddDomainIntegrator(new mfem::VectorFEMassIntegrator(reg_param)); | ||
|
|
||
| nd_fespace_submesh.GetEssentialTrueDofs(combined_inner_bdr_marker, | ||
| submesh_ess_tdof_list); | ||
| a.Assemble(); | ||
| mfem::ParLinearForm b(&nd_fespace_submesh); | ||
| b.Assemble(); | ||
|
|
||
| mfem::HypreParMatrix A_mat; | ||
| Vector B, X; | ||
| a.FormLinearSystem(submesh_ess_tdof_list, A, b, A_mat, X, B); | ||
|
|
||
| mfem::GMRESSolver gmres(MPI_COMM_WORLD); | ||
| gmres.SetOperator(A_mat); | ||
| gmres.SetRelTol(1e-8); | ||
| gmres.SetMaxIter(1000); | ||
| gmres.SetPrintLevel(0); | ||
| gmres.Mult(B, X); | ||
|
|
||
| a.RecoverFEMSolution(X, b, A); | ||
| } |
There was a problem hiding this comment.
I would delete the unreachable debug/direct-solver branch before merging and keep only the production KSP path.
| if constexpr (true) | |
| { | |
| // Use Palace's KSP solver (production approach) | |
| X = 0.0; | |
| ksp.Mult(RHS, X); | |
| // Set solution directly in MFEM GridFunction | |
| A.SetFromTrueDofs(X); | |
| } | |
| else | |
| { | |
| // Alternative debugging approach using direct MFEM solver | |
| // Set up and solve system | |
| mfem::ParBilinearForm a(&nd_fespace_submesh); | |
| // Add curl term: ∫ (curl A) · (curl v) dΩ | |
| mfem::ConstantCoefficient curl_reg(1.0); | |
| a.AddDomainIntegrator(new mfem::CurlCurlIntegrator(curl_reg)); | |
| // Add small regularization for stability | |
| mfem::ConstantCoefficient reg_param(1e-6); | |
| a.AddDomainIntegrator(new mfem::VectorFEMassIntegrator(reg_param)); | |
| nd_fespace_submesh.GetEssentialTrueDofs(combined_inner_bdr_marker, | |
| submesh_ess_tdof_list); | |
| a.Assemble(); | |
| mfem::ParLinearForm b(&nd_fespace_submesh); | |
| b.Assemble(); | |
| mfem::HypreParMatrix A_mat; | |
| Vector B, X; | |
| a.FormLinearSystem(submesh_ess_tdof_list, A, b, A_mat, X, B); | |
| mfem::GMRESSolver gmres(MPI_COMM_WORLD); | |
| gmres.SetOperator(A_mat); | |
| gmres.SetRelTol(1e-8); | |
| gmres.SetMaxIter(1000); | |
| gmres.SetPrintLevel(0); | |
| gmres.Mult(B, X); | |
| a.RecoverFEMSolution(X, b, A); | |
| } | |
| // Use Palace's KSP solver (production approach) | |
| X = 0.0; | |
| ksp.Mult(RHS, X); | |
| // Set solution directly in MFEM GridFunction | |
| A.SetFromTrueDofs(X); |
| The first configuration (`circular_hole.json`) models a circular metal plate of radius | ||
| ``R = 3\,\mu\text{m}`` with a concentric hole of radius ``r = 1\,\mu\text{m}``. One | ||
| flux quantum ``\Phi_0 = 2.068 \times 10^{-15}\,\text{Wb}`` is prescribed through the hole. |
There was a problem hiding this comment.
Separate the internal solve normalization from the physical one-Φ₀ interpretation.
| The first configuration (`circular_hole.json`) models a circular metal plate of radius | |
| ``R = 3\,\mu\text{m}`` with a concentric hole of radius ``r = 1\,\mu\text{m}``. One | |
| flux quantum ``\Phi_0 = 2.068 \times 10^{-15}\,\text{Wb}`` is prescribed through the hole. | |
| The first configuration (`circular_hole.json`) models a circular metal plate of radius | |
| ``R = 3\,\mu\text{m}`` with a concentric hole of radius ``r = 1\,\mu\text{m}``. A unit | |
| nondimensional flux-loop excitation amplitude is prescribed through the hole. |
| problem is solved. | ||
| - `"HoleAttributes"`: boundary attributes of the hole perimeters where integral | ||
| constraints are applied. | ||
| - `"FluxAmounts"`: prescribed flux through each hole, in units of ``\Phi_0``. |
There was a problem hiding this comment.
This should match the internal-amplitude semantics used by the solver.
| - `"FluxAmounts"`: prescribed flux through each hole, in units of ``\Phi_0``. | |
| - `"FluxAmounts"`: prescribed nondimensional flux-loop excitation amplitudes through | |
| each hole. |
| For the single-hole configuration, the solver extracts a self-inductance of | ||
| ``M = 2.808\,\text{pH}`` for one flux quantum trapped in the hole, with a stored magnetic | ||
| energy of ``E_\text{mag} = 1.680 \times 10^{-13}\,\text{J}``. |
There was a problem hiding this comment.
The extracted inductance is independent of the internal amplitude normalization. If we want to quote the physical energy for one trapped flux quantum, compute it from the extracted M afterward.
| For the single-hole configuration, the solver extracts a self-inductance of | |
| ``M = 2.808\,\text{pH}`` for one flux quantum trapped in the hole, with a stored magnetic | |
| energy of ``E_\text{mag} = 1.680 \times 10^{-13}\,\text{J}``. | |
| For the single-hole configuration, the solver extracts a self-inductance of | |
| ``M = 1.902\,\text{pH}``. This corresponds to a stored magnetic energy of | |
| ``E_\text{mag} = Φ₀^2 / (2M) = 1.124 \times 10^{-18}\,\text{J}`` for one physical flux | |
| quantum ``Φ₀ = 2.0678 \times 10^{-15}\,\text{Wb}`` trapped in the hole. |
| When independent flux excitations are configured, the solver extracts the full inductance | ||
| matrix from the stored magnetic energy: | ||
|
|
||
| ```math | ||
| M_{ij} = \frac{\mathbf{A}_j^T K \mathbf{A}_i}{\Phi_i \Phi_j}, | ||
| ``` | ||
|
|
||
| where ``K`` is the curl-curl stiffness matrix. The diagonal entries ``M_{ii}`` give the |
There was a problem hiding this comment.
M_ij is still the inductance entry in Φ = M I; the issue is what the energy normalization computes directly for flux-controlled solves. It gives reluctance first, then M is recovered by inversion.
| When independent flux excitations are configured, the solver extracts the full inductance | |
| matrix from the stored magnetic energy: | |
| ```math | |
| M_{ij} = \frac{\mathbf{A}_j^T K \mathbf{A}_i}{\Phi_i \Phi_j}, | |
| ``` | |
| where ``K`` is the curl-curl stiffness matrix. The diagonal entries ``M_{ii}`` give the | |
| When independent flux excitations are configured, the solver extracts the full inductance | |
| matrix by first computing the reluctance matrix from the stored magnetic energy: | |
| ```math | |
| R_{ij} = \frac{\mathbf{A}_j^T K \mathbf{A}_i}{\Phi_i \Phi_j}, \qquad M = R^{-1}, | |
| ``` | |
| where ``K`` is the curl-curl stiffness matrix, ``R`` is the reluctance matrix, and ``M`` is | |
| the inductance matrix written to `terminal-M.csv`. The diagonal entries ``M_{ii}`` give the |
This reverts commit d702d6c.
…rate reference data
This PR adds methods to enable flux trapping analysis in magnetostatic simulations. Key additions are:
GetFluxLoopExcitationVectorincurlcurloperator.cppto compute the RHS for flux excitationsurfacecurlsolver.cpp, along with postprocessing function for verificationmagnetostaticsolver.cppto enable 3D solve using flux BCPostprocessTerminalsto enable inductance calculations in flux-only and mix current-flux setups.geodata.cppto find loop boundary edges on submesh and their orientationsconfigfile.cppto look for flux loop terminals for magnetostatics