Skip to content

Add rational surface impedance boundary condition#770

Open
krono-i2 wants to merge 5 commits into
awslabs:mainfrom
krono-i2:rational-impedance-bc
Open

Add rational surface impedance boundary condition#770
krono-i2 wants to merge 5 commits into
awslabs:mainfrom
krono-i2:rational-impedance-bc

Conversation

@krono-i2

Copy link
Copy Markdown

This PR adds a RationalImpedance boundary condition: a surface (Robin) impedance boundary whose per-square impedance is an arbitrary rational function of frequency, Zs(s) = N(s)/D(s) with s = iω, given by numerator and denominator polynomial coefficients. It generalizes the parallel-RLC Impedance boundary to any passive lumped-network response. It closes #553 and #642.

Ivan Iudice added 5 commits June 17, 2026 11:46
Add a SurfaceRationalImpedanceOperator implementing a Robin boundary whose
per-square surface impedance is a user-specified rational function of
frequency, Zs(s) = N(s)/D(s) with s = iω. Numerator and denominator
polynomial coefficients (zeros and poles) are given as lists in the new
boundaries["RationalImpedance"] config block, allowing any passive
lumped-network response.

Being a general function of frequency, the boundary contributes iω/Zs(iω) to
the frequency-dependent system matrix A2(ω) (like SurfaceConductivityOperator),
and is available for the driven, eigenmode, and boundary-mode problem types.
Coefficients are nondimensionalized as a_k/(Z0*tc^k) and b_k/tc^k.

Wiring spans configfile (RationalImpedanceData and parser), iodata
(nondimensionalization, problem-type warnings, nonlinear-eigenmode
sparsity-pattern handling), spaceoperator (assembly, auxiliary-space marker,
multiple-BC check), the JSON schema, and the docs.

Add a verification example (examples/rational_impedance) comparing, on a
parallel-plate TEM line, a parallel-RLC sheet realized as a RationalImpedance
against the same RLC realized as a LumpedPort; the two S11 responses agree to
solver tolerance.
…undary

Validate user-specified rational surface impedances and warn on physically
questionable inputs, following the Palace Mpi::Warning convention:

- Warn when the numerator/denominator degree difference exceeds one, since a
  positive-real (passive) impedance has |deg(N) - deg(D)| <= 1.
- Warn, once per boundary, when Re{Zs(iw)} < 0 at an evaluated frequency, with
  a relative tolerance so lossless reactive terminations do not trigger it.

Real-valued coefficients (enforced by the JSON schema and parser) guarantee a
Hermitian Zs(iw), and hence a real time-domain response.
Add the RationalImpedance boundary attributes to the aggregated boundary
attribute list built in BoundaryData. Without this, external rational-impedance
boundaries raised a spurious "no associated boundary condition, PMC assumed"
warning, and internal rational-impedance sheets were skipped by the
interface-element (mesh cracking) logic, so the boundary term was not applied
on them.
Document the boundaries["RationalImpedance"] configuration block in the config
reference: the Numerator/Denominator polynomial coefficients of the per-square
impedance Zs(s) = N(s)/D(s) with s = iw, the real-coefficient Hermitian
guarantee, the passivity warnings (degree excess and Re{Zs} < 0), and the
frequency-domain-only restriction (driven, eigenmode, boundary mode).
@laylagi laylagi self-requested a review June 17, 2026 15:16

@laylagi laylagi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for putting this together. This is a useful addition, and the driven RLC comparison is a nice way to validate the basic behavior. I left a few comments, mostly around edge cases and solver integration.

(dbc_marker[i] || farfield_marker[i] || surf_sigma_marker[i] ||
surf_z_Rs_marker[i] || surf_z_Ls_marker[i] || lumped_port_Rs_marker[i] ||
lumped_port_Ls_marker[i] || wave_port_marker[i] || floquet_port_marker[i]);
surf_z_Rs_marker[i] || surf_z_Rs_marker[i] || surf_z_Ls_marker[i] ||

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
surf_z_Rs_marker[i] || surf_z_Rs_marker[i] || surf_z_Ls_marker[i] ||
surf_z_Rs_marker[i] || surf_rz_marker[i] || surf_z_Ls_marker[i] ||

Was this intentional? surf_z_Rs_marker appears twice, while surf_rz_marker is defined above and used later for mutual-exclusion checking. Should one of those entries be surf_rz_marker[i]?

{
for (auto attr : data.attributes)
{
MFEM_VERIFY(!impedance_marker[attr - 1],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Could we reorder this validation so we check that attr is in range and exists on the mesh before indexing impedance_marker[attr - 1]? For example:

    if (attr <= 0 || attr > bdr_attr_max || !bdr_attr_marker[attr - 1])
    {
      bdr_warn_list.insert(attr);
      continue;
    }

    MFEM_VERIFY(!impedance_marker[attr - 1],
                "Multiple definitions of rational impedance boundary properties for "
                "boundary attribute " << attr << "!");
    impedance_marker[attr - 1] = 1;

The duplicate-definition check is useful, but it should only run for valid attributes. Otherwise invalid input like boundary attribute 0 or an attribute larger than bdr_attr_max can index out of bounds before the warning/ignore path runs.

I noticed a couple of existing surface-BC operators have the same ordering issue, so those probably deserve a follow-up cleanup, but I don't think we should copy that pattern into new code.

Comment on lines +203 to +217
const std::complex<double> Z = N / D;
// Passivity necessary condition on the imaginary axis: Re{Zs(iω)} >= 0. Warn once per
// boundary (relative tolerance avoids false alarms on lossless reactive terminations).
if (!bdr.warned_passivity && Z.real() < -1.0e-9 * std::abs(Z))
{
const double f_ghz = omega * freq_scale / (2.0 * M_PI);
Mpi::Warning("Rational impedance boundary (attribute {:d}) is not passive at "
"f = {:.4f} GHz: Re(Zs) = {:.3e} < 0!\n",
bdr.attr_list.Size() ? bdr.attr_list[0] : -1, f_ghz, Z.real());
bdr.warned_passivity = true;
}
for (auto attr : bdr.attr_list)
{
const double sc = bdr.attr_scaling.at(attr);
const std::complex<double> coef = s / (Z * sc); // iω·Ys per square

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Could we compute the coefficient directly as s * D / (N * sc) instead of forming Z = N / D and then computing s / Z? The two forms are algebraically equivalent when N(s) and D(s) are nonzero, but the direct admittance form is more robust. At an impedance pole where D(s) = 0 and N(s) != 0, Zs = N / D is infinite, while the Robin contribution s / Zs = s * D / N should go to zero. Forming Z first can introduce inf/nan intermediates unnecessarily.

// A rational surface impedance contributes only to the frequency-dependent system matrix
// A2(ω); it has no time-domain (transient) or static realization here.
MFEM_VERIFY(impedance.empty() || problem_type == ProblemType::DRIVEN ||
problem_type == ProblemType::EIGENMODE ||

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Could you clarify and validate eigenmode support for RationalImpedance?
As implemented, this BC contributes through A2(omega), so eigenmode solves trigger the nonlinear eigenmode path. The nonlinear paths I checked evaluate A2 using abs(Im(lambda)), rather than the full complex eigenvalue.

I tried adapting the PR’s parallel RLC example to an eigenmode config using the same equivalent rational coefficients:

Numerator   = [2.2588181359893346e-06, 0.0]
Denominator = [9.542690323697736e-20, 5.995849163282243e-09, 376.730313668]

This case should reduce to s / Zs(s) = 1/Ls + (1/Rs)s + Cs s^2, but the rational version still triggered nonlinear refinement through A2(omega) and did not converge reliably.

Could cases where s / Zs(s) reduces to a quadratic polynomial be assembled into K/C/M instead of A2(omega)? For genuinely nonquadratic rational impedances, I think we need an eigenmode validation test or documented limitations.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Heads up that an upcoming change affects this. Once #778 is merged, A2 will be evaluated using the full complex eigenvalue, $\lambda$, instead of $|Im(\lambda)|$.

// A2(ω); it has no time-domain (transient) or static realization here.
MFEM_VERIFY(impedance.empty() || problem_type == ProblemType::DRIVEN ||
problem_type == ProblemType::EIGENMODE ||
problem_type == ProblemType::BOUNDARYMODE,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Could you double-check whether ProblemType::BOUNDARYMODE should be allowed here? I may be missing it, but I don’t see SurfaceRationalImpedanceOperator constructed or used BoundaryModeOperator.

If rational impedance is not applied in boundary-mode solves yet, maybe we should remove ProblemType::BOUNDARYMODE from this allowed list for now, or add the missing wiring if support is intended.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow frequency-dependent lumped port impedance (vector input)

3 participants