Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion pdnkit/pi/CavityModel.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include "pi/CavityModel.h"
#include "pi/Dielectric.h"

#include <Eigen/Dense>

Expand All @@ -21,7 +22,19 @@ std::complex<double> cavity_impedance(

const double mu = cfg.mu_r * kMu0;
// Complex permittivity: eps = eps_r eps_0 (1 - j tan_delta).
const cd eps = cfg.eps_r * kEps0 * cd(1.0, -cfg.tan_delta);
// Wideband path: pull eps_r' and eps_r" from the Djordjevic-Sarkar fit
// at this frequency. Equivalent to the constant form with
// tan_delta = eps_r"/eps_r' set per-frequency.
cd eps;
if (cfg.wideband_dielectric) {
const double f_hz = omega / (2.0 * std::numbers::pi);
DjordjevicSarkar ds{cfg.ds_eps_inf, cfg.ds_delta_eps,
cfg.ds_f1_hz, cfg.ds_f2_hz};
auto sample = dj_sarkar_at(ds, f_hz);
eps = (sample.eps_r_real - cd(0.0, 1.0) * sample.eps_r_imag) * kEps0;
} else {
eps = cfg.eps_r * kEps0 * cd(1.0, -cfg.tan_delta);
}
const cd k_squared = omega * omega * mu * eps;

cd sum(0.0, 0.0);
Expand Down
9 changes: 9 additions & 0 deletions pdnkit/pi/CavityModel.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,15 @@ struct CavityConfig {
double mu_r = 1.0; // relative permeability (~1 for PCB dielectrics)
double tan_delta = 0.020; // dielectric loss tangent (FR-4 default)
int max_modes = 30; // m, n each summed 0..max_modes inclusive

// Optional: replace the constant (eps_r, tan_delta) with the wideband
// Djordjevic-Sarkar fit so eps(f) ramps causally across the band. When
// off (default), the constant model above is used and nothing changes.
bool wideband_dielectric = false;
double ds_eps_inf = 3.8;
double ds_delta_eps = 1.0;
double ds_f1_hz = 1.0e3;
double ds_f2_hz = 1.0e9;
};

// Self/transfer impedance at angular frequency omega (rad/s) between ports
Expand Down
1 change: 1 addition & 0 deletions pdnkit/tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ add_executable(pdnkit_tests
ir_solver_test.cpp
ir_result_mesh_test.cpp
cavity_model_test.cpp
cavity_ds_test.cpp
decap_optimizer_test.cpp
transient_test.cpp
e2e_test.cpp
Expand Down
119 changes: 119 additions & 0 deletions pdnkit/tests/cavity_ds_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
#include <catch2/catch_approx.hpp>
#include <catch2/catch_test_macros.hpp>

#include <cmath>
#include <complex>
#include <numbers>

#include "pi/CavityModel.h"

using namespace pdnkit::pi;
using Catch::Approx;

namespace {
CavityConfig fr4_default_plane() {
CavityConfig cfg;
cfg.a = 0.100;
cfg.b = 0.080;
cfg.d = 1.6e-3;
cfg.eps_r = 4.3;
cfg.tan_delta = 0.020;
cfg.max_modes = 15;
return cfg;
}
} // namespace

// Regression: wideband_dielectric=false (default) reproduces the
// original cavity_impedance output bit-for-bit. Locks in that the new
// branch does not perturb existing behavior.
TEST_CASE("cavity-ds: default (off) is identical to constant-eps",
"[cavity-ds][validation]") {
CavityConfig cfg = fr4_default_plane();
cfg.wideband_dielectric = false;
const double f = 1.0e8;
const double w = 2.0 * std::numbers::pi * f;
auto z = cavity_impedance(cfg, 0.020, 0.020, 0.080, 0.060, w);

CavityConfig cfg_legacy = fr4_default_plane(); // no DS fields set
auto z_legacy = cavity_impedance(cfg_legacy, 0.020, 0.020, 0.080, 0.060, w);
REQUIRE(z.real() == Approx(z_legacy.real()));
REQUIRE(z.imag() == Approx(z_legacy.imag()));
}

// At a frequency well below the DS lower corner, the model gives
// eps_r ~ eps_inf + delta_eps. So enabling wideband with corners
// (1 kHz, 1 GHz) and eps_inf=3.8, delta=1.0 (-> eps_DC=4.8) at
// f=10 Hz should match a constant-eps run with eps_r=4.8.
TEST_CASE("cavity-ds: low-frequency limit matches eps_DC",
"[cavity-ds][validation]") {
CavityConfig dyn = fr4_default_plane();
dyn.wideband_dielectric = true;
dyn.ds_eps_inf = 3.8;
dyn.ds_delta_eps = 1.0;
dyn.ds_f1_hz = 1.0e3;
dyn.ds_f2_hz = 1.0e9;
const double f = 10.0; // 100x below f1
const double w = 2.0 * std::numbers::pi * f;
auto z_dyn = cavity_impedance(dyn, 0.020, 0.020, 0.080, 0.060, w);

CavityConfig stat = fr4_default_plane();
stat.eps_r = 4.8;
stat.tan_delta = 1.0e-4; // DS gives ~zero loss this far below f1
auto z_stat = cavity_impedance(stat, 0.020, 0.020, 0.080, 0.060, w);

// Within 5% magnitude (DS imag part is small but not exactly zero).
const double rel = std::abs(std::abs(z_dyn) - std::abs(z_stat))
/ std::abs(z_stat);
INFO("|Z_dyn| = " << std::abs(z_dyn)
<< " |Z_stat| = " << std::abs(z_stat)
<< " rel = " << rel);
REQUIRE(rel < 0.05);
}

// Well above the DS upper corner the model gives eps_r ~ eps_inf.
TEST_CASE("cavity-ds: high-frequency limit matches eps_inf",
"[cavity-ds][validation]") {
CavityConfig dyn = fr4_default_plane();
dyn.wideband_dielectric = true;
dyn.ds_eps_inf = 3.8;
dyn.ds_delta_eps = 1.0;
dyn.ds_f1_hz = 1.0e3;
dyn.ds_f2_hz = 1.0e9;
const double f = 1.0e11; // 100x above f2
const double w = 2.0 * std::numbers::pi * f;
auto z_dyn = cavity_impedance(dyn, 0.020, 0.020, 0.080, 0.060, w);

CavityConfig stat = fr4_default_plane();
stat.eps_r = 3.8;
stat.tan_delta = 1.0e-4;
auto z_stat = cavity_impedance(stat, 0.020, 0.020, 0.080, 0.060, w);

const double rel = std::abs(std::abs(z_dyn) - std::abs(z_stat))
/ std::abs(z_stat);
REQUIRE(rel < 0.05);
}

// Cavity peak shifts UP in frequency when eps_r drops with f, because
// peak position scales as 1/sqrt(eps_r). DS reduces eps_r at high f
// so the second mode peak should appear at a slightly higher frequency
// than the constant-eps model predicts.
TEST_CASE("cavity-ds: wideband peak above constant-eps peak",
"[cavity-ds]") {
CavityConfig dyn = fr4_default_plane();
dyn.wideband_dielectric = true;
CavityConfig stat = fr4_default_plane();

// Look at |Z| at a frequency where constant-eps (4.3) shows a peak;
// wideband at this point has eps_r ~ 3.85 (above the cavity's first
// resonance band) so the peak has already moved. Wideband |Z| should
// be lower at the constant-eps peak frequency.
// Sample at the analytic TM10 freq for the constant model:
// f_TM10 = c / (2*a*sqrt(eps_r)) = 3e8 / (2*0.1*sqrt(4.3)) ~ 723 MHz
const double f = 723.0e6;
const double w = 2.0 * std::numbers::pi * f;
auto z_dyn = cavity_impedance(dyn, 0.020, 0.020, 0.080, 0.060, w);
auto z_stat = cavity_impedance(stat, 0.020, 0.020, 0.080, 0.060, w);
// No equality assertion -- just confirm the wideband answer is
// physically different (off-peak there).
REQUIRE(std::abs(z_dyn) != Approx(std::abs(z_stat)).epsilon(0.01));
}
Loading