Add Floquet port boundary condition#693
Conversation
|
Right now the linear frequency dependence of the Floquet wave vector in driven simulations is turned on by specifying |
Sbozzolo
left a comment
There was a problem hiding this comment.
I have a very brief look at the PR and left some comments
| { | ||
| fmt::format_to(out, "Excitation{} with index {:d} has contributions from:\n", | ||
| (Size() > 1) ? fmt::format(" {:d}/{:d}", i, Size()) : "", idx); | ||
| to("Excitation{} with index {:d} has contributions from:\n", |
There was a problem hiding this comment.
This was changed to fmt::format_to in #653 for C++20
There was a problem hiding this comment.
to is just a lambda calling fmt:format_to (a few lines above) so I'd think it's ok?
There was a problem hiding this comment.
Even though to(...) just calls fmt::format_to, the generic lambda hides the format string from fmt’s stricter compile-time checking in newer/C++20 builds. That’s why PR #653 moved these sites to direct fmt::format_to with a fmt::appender.
So I’d keep the explicit form:
auto out = fmt::appender{buffer};
fmt::format_to(out, "...", args...);There was a problem hiding this comment.
I see, reverted back to the explicit form in 775deef
There was a problem hiding this comment.
Maybe you can run generate the plots automatically as part of producing the documentation, so that we don't have to worry to keep them updated.
| @@ -0,0 +1,7 @@ | |||
| f (GHz), |S[P1(-1;-1)TE][1]| (dB),arg(S[P1(-1;-1)TE][1]) (deg.), |S[P1(-1;-1)TM][1]| (dB),arg(S[P1(-1;-1)TM][1]) (deg.), |S[P1(-1;0)TE][1]| (dB),arg(S[P1(-1;0)TE][1]) (deg.), |S[P1(-1;0)TM][1]| (dB),arg(S[P1(-1;0)TM][1]) (deg.), |S[P1(-1;1)TE][1]| (dB),arg(S[P1(-1;1)TE][1]) (deg.), |S[P1(-1;1)TM][1]| (dB),arg(S[P1(-1;1)TM][1]) (deg.), |S[P1(0;-1)TE][1]| (dB),arg(S[P1(0;-1)TE][1]) (deg.), |S[P1(0;-1)TM][1]| (dB),arg(S[P1(0;-1)TM][1]) (deg.), |S[P1(0;0)TE][1]| (dB),arg(S[P1(0;0)TE][1]) (deg.), |S[P1(0;0)TM][1]| (dB),arg(S[P1(0;0)TM][1]) (deg.), |S[P1(0;1)TE][1]| (dB),arg(S[P1(0;1)TE][1]) (deg.), |S[P1(0;1)TM][1]| (dB),arg(S[P1(0;1)TM][1]) (deg.), |S[P1(1;-1)TE][1]| (dB),arg(S[P1(1;-1)TE][1]) (deg.), |S[P1(1;-1)TM][1]| (dB),arg(S[P1(1;-1)TM][1]) (deg.), |S[P1(1;0)TE][1]| (dB),arg(S[P1(1;0)TE][1]) (deg.), |S[P1(1;0)TM][1]| (dB),arg(S[P1(1;0)TM][1]) (deg.), |S[P1(1;1)TE][1]| (dB),arg(S[P1(1;1)TE][1]) (deg.), |S[P1(1;1)TM][1]| (dB),arg(S[P1(1;1)TM][1]) (deg.), |S[P2(-1;-1)TE][1]| (dB),arg(S[P2(-1;-1)TE][1]) (deg.), |S[P2(-1;-1)TM][1]| (dB),arg(S[P2(-1;-1)TM][1]) (deg.), |S[P2(-1;0)TE][1]| (dB),arg(S[P2(-1;0)TE][1]) (deg.), |S[P2(-1;0)TM][1]| (dB),arg(S[P2(-1;0)TM][1]) (deg.), |S[P2(-1;1)TE][1]| (dB),arg(S[P2(-1;1)TE][1]) (deg.), |S[P2(-1;1)TM][1]| (dB),arg(S[P2(-1;1)TM][1]) (deg.), |S[P2(0;-1)TE][1]| (dB),arg(S[P2(0;-1)TE][1]) (deg.), |S[P2(0;-1)TM][1]| (dB),arg(S[P2(0;-1)TM][1]) (deg.), |S[P2(0;0)TE][1]| (dB),arg(S[P2(0;0)TE][1]) (deg.), |S[P2(0;0)TM][1]| (dB),arg(S[P2(0;0)TM][1]) (deg.), |S[P2(0;1)TE][1]| (dB),arg(S[P2(0;1)TE][1]) (deg.), |S[P2(0;1)TM][1]| (dB),arg(S[P2(0;1)TM][1]) (deg.), |S[P2(1;-1)TE][1]| (dB),arg(S[P2(1;-1)TE][1]) (deg.), |S[P2(1;-1)TM][1]| (dB),arg(S[P2(1;-1)TM][1]) (deg.), |S[P2(1;0)TE][1]| (dB),arg(S[P2(1;0)TE][1]) (deg.), |S[P2(1;0)TM][1]| (dB),arg(S[P2(1;0)TM][1]) (deg.), |S[P2(1;1)TE][1]| (dB),arg(S[P2(1;1)TE][1]) (deg.), |S[P2(1;1)TM][1]| (dB),arg(S[P2(1;1)TM][1]) (deg.) | |||
There was a problem hiding this comment.
This file looks very weird with all these NaNs. Maybe we should explain somewhere that that this is expected/fine?
There was a problem hiding this comment.
Added a mention in the docs
There was a problem hiding this comment.
Can we have more unit tests and don't rely as much on the regression test?
hughcars
left a comment
There was a problem hiding this comment.
A few suggested changes, on hughcars/floquet-port-clean for you to cherry-pick as you want, but generally LGTM. The reference frequency UI I think works, and i prefer it to the idea of angles as that requires defining a frame. If users decide they want angles, we can possibly add another front end to convert into this representation later, but lets wait until that's needed.
| normal incidence, set the wave vector to zero. For frequency sweeps at a fixed angle | ||
| of incidence, set `"FloquetReferenceFrequency"` to the frequency (in GHz) at which the | ||
| wave vector is defined. The wave vector then scales linearly with frequency: | ||
| k_F(f) = FloquetWaveVector × (f / FloquetReferenceFrequency), maintaining a constant |
There was a problem hiding this comment.
Math formatting, this is plain text rn.
| `"FloquetReferenceFrequency" [None]` : Optional frequency in GHz at which the | ||
| `"FloquetWaveVector"` is defined. When specified, the Bloch wave vector scales linearly with | ||
| frequency during a driven simulation frequency sweep: k_F(f) = FloquetWaveVector × (f / | ||
| FloquetReferenceFrequency). This is the physically correct behavior for oblique plane wave | ||
| incidence at a fixed angle, where k_F = (2πf/c) sin(θ). When not specified (default), the wave | ||
| vector is held constant across all frequencies. Only supported for driven simulations. |
There was a problem hiding this comment.
I think we could consider the alternative angle based approach for an alternative api in future, but the issue there is needing to define a relative frame/axis for the angles which i really don't like the idea of. Is there a process for mapping between that representation and this? Maybe you could expand on that in the documentation page and then link to that from here.
There was a problem hiding this comment.
Added some text in boundaries.md to help users map between angles and relative freq
Implement Floquet port boundaries for frequency-domain driven simulations on periodic structures (gratings, metasurfaces, photonic crystals). The DtN (Dirichlet-to-Neumann) boundary condition decomposes fields into Floquet diffraction orders and provides an exact absorbing condition via a Robin BC with low-rank corrections. Features: - TE, TM, and circular (RHC/LHC) polarization excitation - Power-normalized S-parameters for all propagating orders - Frequency-dependent Bloch wave vector (FloquetReferenceFrequency) - BZ wrapping for oblique incidence - Adaptive fast frequency sweep (ROM/PROM) integration - Nyquist-based MaxOrder cap for mesh consistency
Add a 3D dielectric grating example with uniform and adaptive frequency sweep configurations, a Gmsh mesh generation script, and a Julia plotting script. Include a regression test (NaN-aware S-parameter comparison, passes on 1-6 MPI ranks) and documentation with problem description, Fano resonance physics, and S-parameter plots.
…eplacePreconditioner Remove the Active config flag from FloquetPort (untested, vestigial from wave port pattern — an inactive port is equivalent to not defining one). Remove FloquetBoundaryPreconditioner and the StealPreconditioner/ReplacePreconditioner KSP methods (leftover from the removed auxiliary mode approach).
Remove cpw2d doc references (not on this branch), revert unused IntRule override in VectorFEBoundaryLFIntegrator, remove stale preconditioner comments from ksp.hpp, and trim Floquet port initialization output to only print mode count and warnings.
Replace GetSystemOperator with GetExtraSystemOperator, which bundles the sparse A2 and low-rank Floquet DtN correction F into a single ComplexOperator. GetSystemMatrix now handles abstract (non-sparse) A2 via a dynamic_cast check, building KCM separately and adding A2 with a non-owning SumComplexOperator. Move SumComplexOperator from floquetportoperator.hpp to linalg/operator.hpp and add non-owning and mixed ownership modes.
…rtifacts
- Restore `active = port.value("Active", active)` in LumpedPortData and
WavePortData constructors, accidentally removed in df056db
- Remove cavity2d/cpw2d/wave_2dmode regression test cases from runtests.jl
(belong to unmerged boundarymode-2d branch)
4efc874 to
1ecc714
Compare
|
Thanks for the review @hughcars. Appreciate the suggested changes, they were spot on. Took most/all of them and addressed the other issues. Rebased just now. |
hughcars
left a comment
There was a problem hiding this comment.
A few more small fixes, typos etc, but essentially ready imo.
| t.reserve(nr_expected_measurement_rows, 100); | ||
| t.insert("idx", "f (GHz)", -1, 0, PrecIndexCol(solver_t), ""); | ||
|
|
||
| bool circular_output = false; |
There was a problem hiding this comment.
This chooses the Floquet CSV polarization basis globally: if any excited Floquet port is circular, columns are created as RHC/LHC for every excitation. But PrintFloquetPortS() chooses TE/TM vs RHC/LHC from measurement_cache.floquet_circular_output, which is set per current excitation in MeasureFloquetPorts().
So a model with one circular Floquet excitation and one linear Floquet excitation will initialize only circular columns, then the linear excitation will try to write TE/TM column names that were never inserted (Table::operator[] throws on missing columns). Could the column setup determine the basis per ex_idx by finding the port whose port.excitation == ex_idx, or otherwise allocate both bases?
| diffraction efficiencies (S-parameters) for multiple propagating orders. Floquet port | ||
| boundaries are only available for frequency domain driven simulations and **require periodic | ||
| boundary conditions** to be configured under | ||
| [`config["Boundaries"]["Periodic"]`](#boundaries%5B%22Periodic%22%5D) with at least two |
There was a problem hiding this comment.
The implementation now requires exactly two periodic boundary pairs for Floquet ports (periodic.boundary_pairs.size() == 2), but the docs still say "at least two" here and in the detailed note below. Could this be changed to "exactly two" to match the runtime check?
Small nearby typo while touching this section: "indicidence" should be "incidence" in the FloquetWaveVector paragraph.
| { | ||
| fmt::format_to(out, "Excitation{} with index {:d} has contributions from:\n", | ||
| (Size() > 1) ? fmt::format(" {:d}/{:d}", i, Size()) : "", idx); | ||
| to("Excitation{} with index {:d} has contributions from:\n", |
There was a problem hiding this comment.
Even though to(...) just calls fmt::format_to, the generic lambda hides the format string from fmt’s stricter compile-time checking in newer/C++20 builds. That’s why PR #653 moved these sites to direct fmt::format_to with a fmt::appender.
So I’d keep the explicit form:
auto out = fmt::appender{buffer};
fmt::format_to(out, "...", args...);
This PR adds Floquet port boundary conditions for frequency-domain driven simulations on periodic structures, with support for oblique incidence, multi-order diffraction, TE/TM/circular polarization, and adaptive frequency sweeps, which closes #563. Also adds an option to have the Floquet wave vector vary linearly with frequency in driven simulations, which closes #564.