Skip to content

Add Floquet port boundary condition#693

Merged
simlapointe merged 24 commits into
mainfrom
simlapointe/floquet-port-clean
Jun 12, 2026
Merged

Add Floquet port boundary condition#693
simlapointe merged 24 commits into
mainfrom
simlapointe/floquet-port-clean

Conversation

@simlapointe

Copy link
Copy Markdown
Contributor

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.

@simlapointe simlapointe added the no-long-tests This PR does not require the long tests to be merged label Mar 31, 2026
@simlapointe

simlapointe commented Mar 31, 2026

Copy link
Copy Markdown
Contributor Author

Right now the linear frequency dependence of the Floquet wave vector in driven simulations is turned on by specifying "FloquetReferenceFrequency" in the config file. This can be a bit confusing and we might want to think of alternative ways of specifying that, such as incidence angles.

@simlapointe simlapointe marked this pull request as ready for review March 31, 2026 22:56
@simlapointe simlapointe requested a review from hughcars March 31, 2026 22:57

@Sbozzolo Sbozzolo left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a very brief look at the PR and left some comments

Comment thread palace/models/portexcitations.cpp Outdated
{
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",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was changed to fmt::format_to in #653 for C++20

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to is just a lambda calling fmt:format_to (a few lines above) so I'd think it's ok?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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...);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, reverted back to the explicit form in 775deef

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread palace/utils/prettyprint.hpp Outdated
@@ -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.)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file looks very weird with all these NaNs. Maybe we should explain somewhere that that this is expected/fine?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a mention in the docs

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we have more unit tests and don't rely as much on the regression test?

@hughcars hughcars left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread docs/src/config/boundaries.md
Comment thread docs/src/examples/dielectric_grating.md
Comment thread docs/src/guide/boundaries.md Outdated
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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Math formatting, this is plain text rn.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

Comment thread docs/src/examples/dielectric_grating.md
Comment thread docs/src/config/boundaries.md Outdated
Comment on lines +644 to +649
`"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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added some text in boundaries.md to help users map between angles and relative freq

Comment thread palace/models/floquetportoperator.cpp
Comment thread palace/models/floquetportoperator.cpp Outdated
Comment thread palace/models/postoperator.cpp Outdated
Comment thread palace/models/postoperator.cpp Outdated
Comment thread palace/utils/prettyprint.hpp Outdated
simlapointe and others added 21 commits June 11, 2026 13:44
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)
@simlapointe simlapointe force-pushed the simlapointe/floquet-port-clean branch from 4efc874 to 1ecc714 Compare June 11, 2026 21:36
@simlapointe

Copy link
Copy Markdown
Contributor Author

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.

@simlapointe simlapointe requested a review from hughcars June 11, 2026 22:26

@hughcars hughcars left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few more small fixes, typos etc, but essentially ready imo.

Comment thread palace/utils/configfile.cpp
Comment thread palace/models/spaceoperator.cpp
Comment thread palace/models/postoperatorcsv.cpp Outdated
t.reserve(nr_expected_measurement_rows, 100);
t.insert("idx", "f (GHz)", -1, 0, PrecIndexCol(solver_t), "");

bool circular_output = false;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now done in 775deef

Comment thread docs/src/config/boundaries.md Outdated
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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed the docs in 775deef

Comment thread palace/models/portexcitations.cpp Outdated
{
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",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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...);

@simlapointe simlapointe requested a review from hughcars June 12, 2026 16:58

@hughcars hughcars left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@simlapointe simlapointe merged commit b2b8ed4 into main Jun 12, 2026
68 checks passed
@simlapointe simlapointe deleted the simlapointe/floquet-port-clean branch June 12, 2026 21:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

no-long-tests This PR does not require the long tests to be merged

Projects

None yet

3 participants