diff --git a/Makefile b/Makefile index 80e426a..7e14418 100644 --- a/Makefile +++ b/Makefile @@ -104,6 +104,11 @@ check-fasthenry: $(TARGET) python3 -m tools.fh_crosscheck examples/test_single.yaml python3 -m tools.fh_crosscheck examples/test_fr4.yaml +# Sanity-check weeks per-line Z0 against the Hammerstad-Jensen microstrip +# closed form (no external solver needed; stdlib-only Python). +check-z0: $(TARGET) + python3 -m tools.microstrip_z0 examples/test_microstrip.yaml + # Check dependencies check-deps: @echo "Checking required libraries..." @@ -132,6 +137,8 @@ help: @echo " make test-fr4 - Run with FR4 substrate" @echo " make test-air - Run with air baseline" @echo " make test-rogers - Run with Rogers material" + @echo " make check-fasthenry - Cross-check R/L against FastHenry" + @echo " make check-z0 - Sanity-check Z0 vs Hammerstad-Jensen" @echo " make install - Install to /usr/local/bin" @echo " make tree - Show project structure" @echo " make help - Show this help" @@ -158,4 +165,4 @@ tree: @echo "│ └── test_rogers4003.yaml" @echo "└── $(BUILD_DIR)/ (Build artifacts)" -.PHONY: all clean distclean install uninstall test-fr4 test-air test-rogers check-fasthenry check-deps help tree +.PHONY: all clean distclean install uninstall test-fr4 test-air test-rogers check-fasthenry check-z0 check-deps help tree diff --git a/README.md b/README.md index 58e99d4..77beabf 100644 --- a/README.md +++ b/README.md @@ -23,24 +23,24 @@ I used Claude Code and Google Antigravity to port this project to Linux and to c This is an enhanced version of the Weeks microstrip resistance calculator with support for reading FR4 and general dielectric substrate parameters. > **Scope:** This tool computes the **series resistance (R) and inductance (L) -> matrices** of rectangular conductors using the PEEC method. The dielectric -> parameters (`er`, `substrate_h`, `tan_delta`) are parsed and the effective -> permittivity / dielectric loss are **reported on stderr for information only — -> they are *not* applied to the R/L/|Z| results.** Capacitance, characteristic -> impedance (Z0), and propagation velocity are **not** computed. As a result, -> changing only the substrate material (e.g. air vs FR4) does not change the -> output matrices. See [Understanding the Physics](#understanding-the-physics). +> matrices** of rectangular conductors using the PEEC method. These matrices are +> dielectric-independent (μr≈1), so changing only the substrate material (e.g. +> air vs FR4) does **not** change them. In addition, a per-line **quasi-TEM +> transmission-line** section reports effective permittivity, characteristic +> impedance (Z0), capacitance, attenuation and complex propagation γ — this is +> where the dielectric (`er`, `tan_delta`) takes effect. The substrate **height** +> is the trace-to-ground gap derived from the conductor geometry, not a separate +> input. See [Understanding the Physics](#understanding-the-physics). ## Features ✅ **YAML input format** - Modern, human-readable configuration files ✅ **Per-unit-length R and L matrices** for multi-conductor systems (PEEC) -✅ **Effective dielectric constant** (Hammerstad-Jensen) — *computed and printed to stderr for information; see [Scope](#overview)* -✅ **Dielectric loss** (frequency-dependent tan δ) — *computed and printed to stderr for information; not applied to results* +✅ **Per-line transmission-line parameters** — effective permittivity (Hammerstad-Jensen), Z0, capacitance, attenuation (incl. dielectric loss) and complex propagation γ ✅ **Pre-defined material constants** (FR4, Rogers, Alumina, PTFE) ✅ **Frequency defined in input file** - No need to recompile! ✅ **Example YAML files** for multiple materials -✅ **Independent validation** against the FastHenry field solver (`make check-fasthenry`) +✅ **Independent validation** against the FastHenry field solver (`make check-fasthenry`) and the Hammerstad-Jensen Z0 closed form (`make check-z0`) ## Quick Start @@ -131,19 +131,17 @@ conductors: nh: 3 # Height mesh divisions b: 0.2 # Mesh density parameter er: 4.4 # Dielectric constant - substrate_h: 1.6e-3 # Substrate height (METERS!) tan_delta: 0.02 # Loss tangent - - name: line1 # Signal trace + - name: line1 # Signal trace; its y sets the substrate height (gap to ground) w: 150e-6 - h: 18e-6 + h: 35e-6 # 35 μm = 1 oz copper x: 600e-6 - y: 77e-6 + y: 1.602e-3 # 1.6 mm above the ground-plane top => the substrate height nw: 21 nh: 7 b: 0.9 er: 4.4 - substrate_h: 1.6e-3 tan_delta: 0.02 ``` @@ -156,7 +154,8 @@ conductors: - `w`: Conductor width (meters) - `h`: Conductor thickness/height (meters) - `x`: X-position of the left edge (min-x corner) (meters) -- `y`: Y-position of conductor bottom (meters) +- `y`: Y-position of conductor bottom (meters). For a signal trace, its height + above the ground-plane top is the substrate height used for εeff/Z0. **Mesh Parameters:** - `nw`: Number of divisions along width @@ -165,9 +164,12 @@ conductors: **Dielectric Parameters:** - `er`: Relative permittivity (dielectric constant) -- `substrate_h`: Substrate height - distance to ground plane (METERS!) - `tan_delta`: Loss tangent at operating frequency +The substrate **height** is not a parameter — it is derived from geometry as the +gap between the signal trace bottom and the ground-plane top. A legacy +`substrate_h:` key is ignored (with a note on stderr). + **IMPORTANT**: All dimensions must be in **METERS**! - 1.6 mm = **1.6e-3** meters (not 1.6!) - 150 μm = **150e-6** meters @@ -182,7 +184,6 @@ conductors: - name: line0 # ... geometry parameters ... er: 4.4 - substrate_h: 1.6e-3 tan_delta: 0.02 ``` @@ -194,7 +195,6 @@ conductors: - name: line0 # ... geometry parameters ... er: 3.38 - substrate_h: 0.813e-3 tan_delta: 0.0027 ``` @@ -206,7 +206,6 @@ conductors: - name: line0 # ... geometry parameters ... er: 3.48 - substrate_h: 0.508e-3 tan_delta: 0.0037 ``` @@ -218,7 +217,6 @@ conductors: - name: line0 # ... geometry parameters ... er: 1.0 - substrate_h: 0.0 tan_delta: 0.0 ``` @@ -316,44 +314,50 @@ FREQUENCY: 3.000000e+07 Hz (30.00 MHz) ## Understanding the Physics -> **Note:** The formulas and table in this section describe general microstrip -> behavior as background. **This tool does not apply them to its output** — it -> reports εeff and the dielectric-loss term on stderr for information only, and -> solves the series R and L matrices alone. Capacitance, Z0, and velocity are -> not computed. See [Scope](#overview). +> **Note:** The series R and L matrices are dielectric-independent (μr≈1). The +> dielectric instead enters the per-line **TRANSMISSION-LINE PARAMETERS** section, +> via a quasi-TEM model built on the geometry-derived substrate height. See +> [Scope](#overview). ### Effective Dielectric Constant -For microstrip, the EM field exists partly in the dielectric and partly in air. The effective dielectric constant εeff is calculated (and printed to stderr) using: +For microstrip, the EM field exists partly in the dielectric and partly in air. The effective dielectric constant εeff is calculated using: ``` εeff = (εr + 1)/2 + ((εr - 1)/2) × F(w/h) ``` -where F(w/h) is the Hammerstad-Jensen approximation. +where F(w/h) is the Hammerstad-Jensen approximation and h is the substrate height +(the geometry-derived trace-to-ground gap). ### Dielectric Loss -In a full model, dielectric loss would contribute additional resistance: +The dielectric loss is reported as an attenuation constant α_d (units 1/m): ``` -R_dielectric = (ω√εeff/c) × ((εr-1)/(εeff-1)) × (εeff/εr) × tan(δ) +α_d = (ω√εeff/c) × ((εr-1)/(εeff-1)) × (εeff/εr) × tan(δ) ``` -This term is **computed and printed to stderr for information only — it is not -added to the output R matrix.** +It is **not** folded into the series R matrix (a 1/m attenuation cannot be added +to an Ω/m resistance). Instead it enters the shunt side of the transmission-line +model and shows up in the reported total attenuation and complex propagation γ. -### What Would Change with Dielectric (general theory) +### Per-line transmission-line parameters -The table below describes microstrip behavior in general. **This tool computes -none of these quantities** (it solves only R and L); it is included as physical -background, which is why air and FR4 inputs produce the same R/L/|Z| output here. +From each line's diagonal series R/L and the effective permittivity, the tool +reports (quasi-TEM, single-line): capacitance C = εeff/(c²L), characteristic +impedance Z0 = √(L/C), conductor + dielectric attenuation, phase constant β and +complex propagation γ. So unlike the R/L matrices, this section **does** differ +between air and FR4: | Parameter | Air (εr=1.0) | FR4 (εr=4.4) | Effect | |-----------|-------------|--------------|--------| -| Capacitance | 1× | ~2.1× | Increases by √εeff | +| Capacitance | 1× | ↑ | Increases by εeff | | Inductance | 1× | ~1× | Approximately same | -| Z0 | 1× | ~0.5× | Decreases by √εeff | +| Z0 | 1× | ↓ | Decreases by √εeff | | Loss | Conductor only | Conductor + Dielectric | Increases | -| Velocity | c | ~0.5c | Slows down | +| Velocity | c | c/√εeff | Slows down | + +This is a single-line characterization (it does not model inter-line capacitive +coupling); cross-check it with `make check-z0`. ## Directory Structure @@ -427,7 +431,10 @@ make LDFLAGS='-L/path/to/meschach/lib' ``` ### Wrong Results -- **Check units**: substrate_h must be in METERS! +- **Check units**: all dimensions must be in METERS! +- **Check trace height**: the substrate height is the signal trace's gap above + the ground plane — place the trace at the intended height (e.g. `y: 1.602e-3` + for a 1.6 mm substrate over a 2 µm ground plane) - **Check dielectric values**: Typical FR4 is εr=4.4, not 44! - **Compare with air**: Run with er=1.0 to verify baseline diff --git a/examples/test.yaml b/examples/test.yaml index e2ccf5b..3c12278 100644 --- a/examples/test.yaml +++ b/examples/test.yaml @@ -14,44 +14,40 @@ conductors: nh: 3 # Height divisions b: 0.2 # Mesh density er: 4.4 # FR4 dielectric constant - substrate_h: 1.6e-3 # 1.6mm FR4 substrate tan_delta: 0.02 # Loss tangent at 1 GHz # Signal trace 1 - name: line1 w: 150e-6 # Width: 150μm - h: 18e-6 # Thickness: 18μm + h: 35e-6 # Thickness: 35μm (1 oz copper) x: 600e-6 # Position: 600μm - y: 77e-6 # Height above ground: 77μm + y: 1.602e-3 # 1.6 mm above the 2μm ground-plane top (= substrate height) nw: 21 nh: 7 b: 0.9 er: 4.4 - substrate_h: 1.6e-3 tan_delta: 0.02 # Signal trace 2 - name: line2 w: 150e-6 - h: 18e-6 + h: 35e-6 x: 825e-6 - y: 77e-6 + y: 1.602e-3 nw: 21 nh: 7 b: 0.9 er: 4.4 - substrate_h: 1.6e-3 tan_delta: 0.02 # Signal trace 3 - name: line3 w: 150e-6 - h: 18e-6 + h: 35e-6 x: 1050e-6 - y: 77e-6 + y: 1.602e-3 nw: 21 nh: 7 b: 0.9 er: 4.4 - substrate_h: 1.6e-3 tan_delta: 0.02 diff --git a/examples/test_air.yaml b/examples/test_air.yaml index 9fb8f27..d85b785 100644 --- a/examples/test_air.yaml +++ b/examples/test_air.yaml @@ -14,44 +14,40 @@ conductors: nh: 3 b: 0.2 er: 1.0 # Air - substrate_h: 0.0 # No substrate tan_delta: 0.0 # No loss # Signal trace 1 - name: line1 w: 150e-6 - h: 18e-6 + h: 35e-6 x: 600e-6 - y: 77e-6 + y: 1.602e-3 # same geometry as test_fr4 (baseline: only er differs) nw: 21 nh: 7 b: 0.9 er: 1.0 - substrate_h: 0.0 tan_delta: 0.0 # Signal trace 2 - name: line2 w: 150e-6 - h: 18e-6 + h: 35e-6 x: 825e-6 - y: 77e-6 + y: 1.602e-3 nw: 21 nh: 7 b: 0.9 er: 1.0 - substrate_h: 0.0 tan_delta: 0.0 # Signal trace 3 - name: line3 w: 150e-6 - h: 18e-6 + h: 35e-6 x: 1050e-6 - y: 77e-6 + y: 1.602e-3 nw: 21 nh: 7 b: 0.9 er: 1.0 - substrate_h: 0.0 tan_delta: 0.0 diff --git a/examples/test_fr4.yaml b/examples/test_fr4.yaml index e2ccf5b..3c12278 100644 --- a/examples/test_fr4.yaml +++ b/examples/test_fr4.yaml @@ -14,44 +14,40 @@ conductors: nh: 3 # Height divisions b: 0.2 # Mesh density er: 4.4 # FR4 dielectric constant - substrate_h: 1.6e-3 # 1.6mm FR4 substrate tan_delta: 0.02 # Loss tangent at 1 GHz # Signal trace 1 - name: line1 w: 150e-6 # Width: 150μm - h: 18e-6 # Thickness: 18μm + h: 35e-6 # Thickness: 35μm (1 oz copper) x: 600e-6 # Position: 600μm - y: 77e-6 # Height above ground: 77μm + y: 1.602e-3 # 1.6 mm above the 2μm ground-plane top (= substrate height) nw: 21 nh: 7 b: 0.9 er: 4.4 - substrate_h: 1.6e-3 tan_delta: 0.02 # Signal trace 2 - name: line2 w: 150e-6 - h: 18e-6 + h: 35e-6 x: 825e-6 - y: 77e-6 + y: 1.602e-3 nw: 21 nh: 7 b: 0.9 er: 4.4 - substrate_h: 1.6e-3 tan_delta: 0.02 # Signal trace 3 - name: line3 w: 150e-6 - h: 18e-6 + h: 35e-6 x: 1050e-6 - y: 77e-6 + y: 1.602e-3 nw: 21 nh: 7 b: 0.9 er: 4.4 - substrate_h: 1.6e-3 tan_delta: 0.02 diff --git a/examples/test_microstrip.yaml b/examples/test_microstrip.yaml new file mode 100644 index 0000000..8cded33 --- /dev/null +++ b/examples/test_microstrip.yaml @@ -0,0 +1,38 @@ +# Microstrip example for the Z0 sanity check +# (python3 -m tools.microstrip_z0 examples/test_microstrip.yaml). +# +# The substrate height is the trace-to-ground gap, derived from geometry: the +# signal trace bottom sits at y = 202 um, the 2 um ground plane top at y = 2 um, +# so the dielectric height is 200 um. weeks computes eff_er and L from this same +# geometry, so its Z0 matches the Hammerstad-Jensen closed form to ~0.03%. +# +# The trace is deliberately thin (18 um) here: the check's H-J reference is +# zero-thickness, so a thin trace isolates the inductance comparison. The +# material examples (test_fr4, etc.) use 35 um (1 oz copper) for realism. + +frequency: 30e6 # 30 MHz + +conductors: + # Ground plane (line0) + - name: line0 + w: 2800e-6 + h: 2.0e-6 + x: 0.0 + y: 0.0 + nw: 201 + nh: 3 + b: 0.2 + er: 4.4 + tan_delta: 0.02 + + # Signal trace; bottom at y = 202 um => 200 um above the ground-plane top + - name: line1 + w: 150e-6 + h: 18e-6 + x: 1325e-6 + y: 202e-6 + nw: 21 + nh: 7 + b: 0.9 + er: 4.4 + tan_delta: 0.02 diff --git a/examples/test_rogers4003.yaml b/examples/test_rogers4003.yaml index 7f824b3..eb305ee 100644 --- a/examples/test_rogers4003.yaml +++ b/examples/test_rogers4003.yaml @@ -14,44 +14,40 @@ conductors: nh: 3 b: 0.2 er: 3.38 # Rogers RO4003C - substrate_h: 0.813e-3 # 0.813mm (32 mil) tan_delta: 0.0027 # Low loss for RF # Signal trace 1 - name: line1 w: 150e-6 - h: 18e-6 + h: 35e-6 x: 600e-6 - y: 77e-6 + y: 0.815e-3 # 0.813 mm above the 2μm ground-plane top (= substrate height) nw: 21 nh: 7 b: 0.9 er: 3.38 - substrate_h: 0.813e-3 tan_delta: 0.0027 # Signal trace 2 - name: line2 w: 150e-6 - h: 18e-6 + h: 35e-6 x: 825e-6 - y: 77e-6 + y: 0.815e-3 nw: 21 nh: 7 b: 0.9 er: 3.38 - substrate_h: 0.813e-3 tan_delta: 0.0027 # Signal trace 3 - name: line3 w: 150e-6 - h: 18e-6 + h: 35e-6 x: 1050e-6 - y: 77e-6 + y: 0.815e-3 nw: 21 nh: 7 b: 0.9 er: 3.38 - substrate_h: 0.813e-3 tan_delta: 0.0027 diff --git a/examples/test_single.yaml b/examples/test_single.yaml index f294bad..dc52b29 100644 --- a/examples/test_single.yaml +++ b/examples/test_single.yaml @@ -14,18 +14,16 @@ conductors: nh: 3 b: 0.2 er: 4.4 - substrate_h: 1.6e-3 tan_delta: 0.02 # Signal trace - name: line1 w: 150e-6 - h: 18e-6 + h: 35e-6 x: 600e-6 - y: 77e-6 + y: 1.602e-3 # 1.6 mm above the 2μm ground-plane top (= substrate height) nw: 21 nh: 7 b: 0.9 er: 4.4 - substrate_h: 1.6e-3 tan_delta: 0.02 diff --git a/include/calcl.h b/include/calcl.h index 1d473aa..8b41319 100644 --- a/include/calcl.h +++ b/include/calcl.h @@ -4,5 +4,13 @@ void calcl (ZMAT *, element *, double, element, conductor *, int); /* New helper functions */ double calc_eff_dielectric(double w, double h, double er); -double calc_dielectric_loss(double er, double tan_delta, +double calc_dielectric_loss(double er, double tan_delta, double Omega, double w, double h); + +/* Substrate height (trace-to-ground gap) for signal conductor line_index, + * derived from geometry. Replaces the old per-conductor substrate_h input. */ +double substrate_height(const conductor *cond, int line_index); + +/* Per-line quasi-TEM transmission-line parameters (Z0, C, attenuation, gamma). + * Reads the diagonal self-terms of the final N x N impedance matrix z. */ +void calc_line_params(ZMAT *z, double Omega, conductor *cond, int N); diff --git a/include/weeks.h b/include/weeks.h index 7254a5c..12205c0 100644 --- a/include/weeks.h +++ b/include/weeks.h @@ -8,9 +8,10 @@ typedef struct { int nh; /* number of height divisions */ int n; /* total number of elements (nw * nh) */ - /* Dielectric properties (new) */ + /* Dielectric material properties. The substrate *height* is NOT stored + * here: it is the trace-to-ground separation, derived from the geometry + * (signal y minus ground-plane top). See substrate_height() in calcl.c. */ double er; /* relative permittivity (dielectric constant) */ - double substrate_h; /* substrate height (distance to ground plane) */ double tan_delta; /* loss tangent (dielectric loss) */ } conductor; diff --git a/src/calcl.c b/src/calcl.c index 068b88b..e4f2a46 100644 --- a/src/calcl.c +++ b/src/calcl.c @@ -72,38 +72,15 @@ void calcl(ZMAT *Z, element *e, double Omega, int dim; double lmm, lpi0, r00; double sigma=58e6; /* Copper conductivity S/m */ - double eff_er; - double diel_loss; VEC *lpj; dim = Z->m; + (void)cond; /* dielectric is handled in calc_line_params(), not the matrix */ - /* Effective permittivity and dielectric loss are reported for information - * only. They influence the line capacitance / characteristic impedance Z0, - * which this tool does NOT compute -- it solves for the series R and L - * matrices. Folding the dielectric attenuation (units 1/m) into the - * resistance matrix (units Ohm/m) would be dimensionally wrong, so it is - * not applied. */ - if (cond != NULL && cond[0].substrate_h > 0.0) { - eff_er = calc_eff_dielectric(cond[0].w, cond[0].substrate_h, cond[0].er); - fprintf(stderr, "\n Ground plane effective εr: %.3f", eff_er); - - diel_loss = calc_dielectric_loss(cond[0].er, cond[0].tan_delta, - Omega, cond[0].w, cond[0].substrate_h); - fprintf(stderr, "\n Dielectric attenuation (informational): %.3e 1/m", diel_loss); - } else { - eff_er = 1.0; /* Air */ - diel_loss = 0.0; - } - - /* Report effective permittivity per signal conductor (informational, not - * applied -- see note above). Uses the correct per-conductor properties. */ - if (cond != NULL) { - for (i = 1; i <= N; i++) { - if (cond[i].substrate_h > 0.0) - fprintf(stderr, "\n Line %d effective εr: %.3f", i, - calc_eff_dielectric(cond[i].w, cond[i].substrate_h, cond[i].er)); - } - } + /* The dielectric does NOT enter the series R/L matrices: those are purely + * geometric (and dielectric attenuation, units 1/m, cannot be added to a + * resistance in Ohm/m). The effective permittivity and loss feed the per- + * line transmission-line parameters in calc_line_params() instead, where the + * dielectric loss enters the shunt side of the line correctly. */ lmm = lp (&e0, &e0); r00 = 1/(sigma*(e0.x2-e0.x1)*(e0.y2-e0.y1)); @@ -140,3 +117,78 @@ void calcl(ZMAT *Z, element *e, double Omega, for (i=0; ime[i][i].re += 1/(sigma*(e[i].x2-e[i].x1)*(e[i].y2-e[i].y1)); } + +/* Substrate height for signal conductor `line_index` (1-based): the vertical + * gap between the bottom of the trace and the top of the ground plane, + * cond[0]. For a microstrip the substrate fills exactly this gap, so the + * dielectric height is a property of the geometry -- not a separate input. + * Returns <= 0 if the trace is not above the ground plane. */ +double substrate_height(const conductor *cond, int line_index) +{ + return cond[line_index].y - (cond[0].y + cond[0].h); +} + +/* Per-line (quasi-TEM) transmission-line parameters. + * + * This is where the dielectric finally affects a reported result. The series + * R/L matrices (above) are dielectric-independent; here we add the shunt side + * of the line. For each signal conductor i we take the diagonal self-terms of + * the final N x N impedance matrix z as the per-line series R and L, then use + * the quasi-TEM relation LC = eff_er/c^2 (L is dielectric-independent because + * mu_r ~ 1) to recover C, Z0, attenuation and the complex propagation gamma. + * + * Approximation: using the diagonal self-terms treats each line in isolation; + * it does not model inter-line capacitive coupling under an inhomogeneous + * dielectric (that would require a full electrostatic P-matrix solve). This is + * the standard single-line characterization. */ +void calc_line_params(ZMAT *z, double Omega, conductor *cond, int N) +{ + const double c = 2.99792458e8; /* speed of light, m/s */ + const double NP2DB = 8.685889638; /* Np/m -> dB/m */ + int i; + + printf("\n*** TRANSMISSION-LINE PARAMETERS (per line, quasi-TEM) at %.2f MHz ***\n\n", + Omega/(2.0*M_PI)/1e6); + printf("Line eff_er Z0(Ohm) C(F/m) a_c(dB/m) a_d(dB/m) a(dB/m) beta(rad/m) eff_er''\n\n"); + + for (i = 1; i <= N; i++) { + double R_ii = z->me[i-1][i-1].re; /* Ohm/m */ + double L_ii = z->me[i-1][i-1].im / Omega; /* H/m */ + double h_sub = substrate_height(cond, i); /* trace-to-ground gap, m */ + double eff_er, beta, C, Z0, a_c, a_d, a_tot, tand_eff, eff_er_im; + + /* Substrate height is the geometric trace-to-ground gap, so eff_er and L + * are consistent by construction (no separate substrate_h to disagree). */ + eff_er = calc_eff_dielectric(cond[i].w, h_sub, cond[i].er); + if (eff_er < 1.0) + eff_er = 1.0; /* air / no substrate */ + + if (L_ii <= 0.0) { + /* Degenerate line: cannot form C/Z0. Report what we have. */ + printf("%3d %+0.4e (L<=0, skipped)\n", i, eff_er); + continue; + } + + beta = Omega * sqrt(eff_er) / c; /* rad/m */ + C = eff_er / (c * c * L_ii); /* F/m */ + Z0 = sqrt(L_ii / C); /* Ohm */ + + a_c = R_ii / (2.0 * Z0); /* Np/m */ + a_d = calc_dielectric_loss(cond[i].er, cond[i].tan_delta, + Omega, cond[i].w, h_sub); /* Np/m */ + a_tot = a_c + a_d; /* Np/m */ + + /* gamma = a_tot + j*beta. Express the dielectric loss as an effective + * loss tangent and the imaginary part of the effective permittivity. */ + tand_eff = (beta > 0.0) ? (2.0 * a_d / beta) : 0.0; + eff_er_im = eff_er * tand_eff; + + printf("%3d %+0.4e %+0.4e %+0.4e %+0.4e %+0.4e %+0.4e %+0.4e %+0.4e\n", + i, eff_er, Z0, C, + a_c * NP2DB, a_d * NP2DB, a_tot * NP2DB, + beta, eff_er_im); + } + + printf("\n gamma = alpha + j*beta (alpha = a column above in Np/m; beta in rad/m)\n"); + printf(" Complex effective permittivity per line: eff_er - j*eff_er''\n"); +} diff --git a/src/input.c b/src/input.c index 7b5ebdc..d08b6ec 100644 --- a/src/input.c +++ b/src/input.c @@ -71,7 +71,6 @@ static int parse_conductor(yaml_parser_t *parser, conductor *c) { c->nw = 10; c->nh = 10; c->er = 1.0; - c->substrate_h = 0.0; c->tan_delta = 0.0; while (in_mapping) { @@ -120,7 +119,11 @@ static int parse_conductor(yaml_parser_t *parser, conductor *c) { } else if (strcmp(key, "er") == 0) { c->er = safe_atof(value); } else if (strcmp(key, "substrate_h") == 0) { - c->substrate_h = safe_atof(value); + /* Deprecated/ignored: the substrate height is derived + * from the conductor geometry (trace-to-ground gap), + * not read from the input. */ + fprintf(stderr, "\n Note: 'substrate_h' is ignored; " + "substrate height is derived from geometry"); } else if (strcmp(key, "tan_delta") == 0) { c->tan_delta = safe_atof(value); } @@ -168,8 +171,8 @@ static int parse_conductor(yaml_parser_t *parser, conductor *c) { c->n = c->nw * c->nh; - fprintf(stderr, "\n Conductor: w=%.2e, h=%.2e, er=%.2f, substrate_h=%.2e, tan_delta=%.4f", - c->w, c->h, c->er, c->substrate_h, c->tan_delta); + fprintf(stderr, "\n Conductor: w=%.2e, h=%.2e, er=%.2f, tan_delta=%.4f", + c->w, c->h, c->er, c->tan_delta); return 1; } diff --git a/src/weeks.c b/src/weeks.c index 55fa564..4b510f9 100644 --- a/src/weeks.c +++ b/src/weeks.c @@ -65,17 +65,14 @@ int main(void) exit(EXIT_FAILURE); fprintf(stderr, "\nNumber of elements: %d", M); - /* Display dielectric information */ + /* Display dielectric information. The substrate height is the trace-to- + * ground gap, derived from geometry (not an input). */ fprintf(stderr, "\n\nDielectric Properties:"); - fprintf(stderr, "\n Ground plane (line0): εr=%.2f, h=%.2e m", - test[0].er, test[0].substrate_h); - if(test[0].tan_delta > 0.0) - fprintf(stderr, ", tan δ=%.4f", test[0].tan_delta); - + fprintf(stderr, "\n Ground plane (line0): εr=%.2f", test[0].er); + for(i=1; i<=N; i++) { - fprintf(stderr, "\n Line %d: εr=%.2f", i, test[i].er); - if(test[i].substrate_h > 0.0) - fprintf(stderr, ", h=%.2e m", test[i].substrate_h); + fprintf(stderr, "\n Line %d: εr=%.2f, substrate h=%.2e m (from geometry)", + i, test[i].er, substrate_height(test, i)); if(test[i].tan_delta > 0.0) fprintf(stderr, ", tan δ=%.4f", test[i].tan_delta); } @@ -117,8 +114,8 @@ int main(void) } ti += test[i+1].n; } - free(test); - test=0; + /* test[] is freed after calc_line_params() below, which still needs the + * per-conductor dielectric/geometry fields. */ z = zm_inverse(y, y); y = ZMNULL; @@ -168,7 +165,13 @@ int main(void) } printf("\n"); } - + + /* Per-line transmission-line parameters (Z0, C, attenuation, propagation). + * Reads the diagonal self-terms of z, so it must run before ZM_FREE(z). */ + calc_line_params(z, Omega, test, N); + + free(test); + test = 0; ZM_FREE(z); ZM_FREE(Z); /* same pointer as Y after zm_inverse(Z,Z) */ ts = time(&ts); diff --git a/tools/fh_crosscheck/parse_weeks.py b/tools/fh_crosscheck/parse_weeks.py index 14a5056..9488776 100644 --- a/tools/fh_crosscheck/parse_weeks.py +++ b/tools/fh_crosscheck/parse_weeks.py @@ -45,8 +45,30 @@ def parse_weeks_output(text): return R, L -def run_weeks(case_yaml, weeks_bin="./weeks", workdir="."): - """Copy ``case_yaml`` to ``workdir/test.yaml``, run weeks, parse output. +# Column order of the TRANSMISSION-LINE PARAMETERS rows printed by +# calc_line_params() in src/calcl.c (after the leading line index): +# eff_er Z0 C a_c a_d a_total beta eff_er'' +_LINE_PARAM_KEYS = ("eff_er", "z0", "c", "a_c", "a_d", "a_total", "beta", "eff_er_im") + + +def parse_line_params(text): + """Return the per-line transmission-line parameters as a list of dicts. + + One dict per signal line, keyed by ``_LINE_PARAM_KEYS``. Empty if the + section is absent (older builds) or a line was skipped (printed as text). + """ + lines = text.splitlines() + rows = _matrix_after(lines, "TRANSMISSION-LINE PARAMETERS") + out = [] + for row in rows: + if len(row) != len(_LINE_PARAM_KEYS): + continue # e.g. a "(L<=0, skipped)" row + out.append(dict(zip(_LINE_PARAM_KEYS, row))) + return out + + +def run_weeks_text(case_yaml, weeks_bin="./weeks", workdir="."): + """Copy ``case_yaml`` to ``workdir/test.yaml``, run weeks, return stdout. Any pre-existing test.yaml is backed up and restored (even on error). """ @@ -67,9 +89,14 @@ def run_weeks(case_yaml, weeks_bin="./weeks", workdir="."): raise RuntimeError( "weeks exited %d.\nstderr:\n%s" % (proc.returncode, proc.stderr) ) - return parse_weeks_output(proc.stdout) + return proc.stdout finally: if backup: shutil.move(backup, test_yaml) elif os.path.exists(test_yaml): os.remove(test_yaml) + + +def run_weeks(case_yaml, weeks_bin="./weeks", workdir="."): + """Run weeks on ``case_yaml`` and return its parsed ``(R, L)`` matrices.""" + return parse_weeks_output(run_weeks_text(case_yaml, weeks_bin, workdir)) diff --git a/tools/microstrip_z0/__init__.py b/tools/microstrip_z0/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tools/microstrip_z0/__main__.py b/tools/microstrip_z0/__main__.py new file mode 100644 index 0000000..17ae974 --- /dev/null +++ b/tools/microstrip_z0/__main__.py @@ -0,0 +1,27 @@ +"""CLI: python3 -m tools.microstrip_z0 [options].""" + +import argparse +import sys + +from tools.microstrip_z0.check import check_z0 + + +def main(argv=None): + parser = argparse.ArgumentParser( + prog="python3 -m tools.microstrip_z0", + description="Sanity-check weeks per-line Z0 against the " + "Hammerstad-Jensen microstrip closed form.", + ) + parser.add_argument("case_yaml", help="weeks YAML input file") + parser.add_argument("--tol", type=float, default=10.0, + help="Z0 flag tolerance (%%)") + args = parser.parse_args(argv) + + report, _flagged = check_z0(args.case_yaml, tol=args.tol) + print(report) + # Soft policy (matches fh_crosscheck): flags are informational, exit 0. + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/microstrip_z0/check.py b/tools/microstrip_z0/check.py new file mode 100644 index 0000000..4e6993f --- /dev/null +++ b/tools/microstrip_z0/check.py @@ -0,0 +1,54 @@ +"""Single-line Z0 sanity check: weeks vs Hammerstad-Jensen closed form.""" + +from tools.fh_crosscheck.geometry import load_case +from tools.fh_crosscheck.parse_weeks import parse_line_params, run_weeks_text +from tools.microstrip_z0.microstrip import geometric_gap, microstrip_z0 + + +def _pct(ref, val): + return None if ref == 0 else 100.0 * (val - ref) / ref + + +def check_z0(case_yaml, weeks_bin="./weeks", workdir=".", tol=10.0): + """Compare weeks' per-line Z0 against the Hammerstad-Jensen closed form. + + Both weeks and the closed form use the same substrate height -- the + geometric trace-to-ground gap -- so the comparison is always consistent. + Returns ``(report_text, flagged)``; ``flagged`` is True if any line's Z0 + differs by more than ``tol`` percent. + """ + _freq, conductors = load_case(case_yaml) + params = parse_line_params(run_weeks_text(case_yaml, weeks_bin, workdir)) + + lines = [ + "*** Z0 SANITY CHECK: weeks vs Hammerstad-Jensen (%s) ***" % case_yaml, + " line sub_h(m) weeks_Z0 H-J_Z0 %diff note", + ] + flagged = False + for i, p in enumerate(params): + sig = conductors[i + 1] + w, er = sig["w"], sig["er"] + h = geometric_gap(conductors, i + 1) # substrate height from geometry + + note = "" + if er <= 1.0 or h <= 0.0: + note = "air / no dielectric (H-J not applicable)" + hj_text, pd_text = " n/a", " n/a" + else: + _eff, hj_z0 = microstrip_z0(w, h, er) + pd = _pct(hj_z0, p["z0"]) + hj_text = "%11.4e" % hj_z0 + pd_text = " n/a" if pd is None else "%+7.2f%%" % pd + if pd is not None and abs(pd) > tol: + note = "over %.0f%% tolerance" % tol + flagged = True + + lines.append( + " %3d %10.3e %11.4e %s %s %s" + % (i + 1, h, p["z0"], hj_text, pd_text, note) + ) + + if not params: + lines.append(" (no TRANSMISSION-LINE PARAMETERS section found in weeks output)") + flagged = True + return "\n".join(lines), flagged diff --git a/tools/microstrip_z0/microstrip.py b/tools/microstrip_z0/microstrip.py new file mode 100644 index 0000000..3394f06 --- /dev/null +++ b/tools/microstrip_z0/microstrip.py @@ -0,0 +1,51 @@ +"""Hammerstad-Jensen microstrip closed form, and a single-line Z0 sanity +check against weeks' TRANSMISSION-LINE PARAMETERS output. + +This is an independent cross-check of weeks' per-line characteristic impedance: +weeks derives Z0 = c * L / sqrt(eff_er) from its PEEC inductance L and the +effective permittivity, whereas the Hammerstad-Jensen formulas below give +eff_er and Z0 purely from the microstrip geometry (w, h, er). Agreement +validates the PEEC inductance against a published closed form, complementing +the FastHenry R/L crosscheck. + +Both sides use the same substrate height -- the geometric trace-to-ground gap +(``geometric_gap`` below) -- which is also what weeks uses for eff_er, so the +comparison is consistent by construction. +""" + +import math + +ETA0 = 376.730313668 # free-space wave impedance (Ohm) + + +def eff_permittivity(w, h, er): + """Hammerstad-Jensen effective permittivity. Mirrors calc_eff_dielectric() + in src/calcl.c exactly (same branches and constants).""" + if h <= 0.0 or er <= 1.0: + return 1.0 + u = w / h + a = (1.0 + + (1.0 / 49.0) * math.log((u ** 4 + (u / 52.0) ** 2) + / (u ** 4 + 0.432)) + + (1.0 / 18.7) * math.log(1.0 + (u / 18.1) ** 3)) + b = 0.564 * ((er - 0.9) / (er + 3.0)) ** 0.053 + return (er + 1.0) / 2.0 + ((er - 1.0) / 2.0) * (1.0 + 10.0 / u) ** (-a * b) + + +def microstrip_z0(w, h, er): + """Return ``(eff_er, Z0)`` for a microstrip of width ``w``, substrate + height ``h`` and relative permittivity ``er`` (Hammerstad-Jensen, zero + thickness). ``Z0`` is in ohms.""" + eff_er = eff_permittivity(w, h, er) + u = w / h + fu = 6.0 + (2.0 * math.pi - 6.0) * math.exp(-((30.666 / u) ** 0.7528)) + z0_air = (ETA0 / (2.0 * math.pi)) * math.log(fu / u + math.sqrt(1.0 + (2.0 / u) ** 2)) + return eff_er, z0_air / math.sqrt(eff_er) + + +def geometric_gap(conductors, line_index): + """Vertical gap (m) between the bottom of signal conductor ``line_index`` + (1-based among signal traces) and the top of the ground plane (index 0).""" + ground = conductors[0] + sig = conductors[line_index] + return sig["y"] - (ground["y"] + ground["h"]) diff --git a/tools/microstrip_z0/tests/__init__.py b/tools/microstrip_z0/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tools/microstrip_z0/tests/test_microstrip.py b/tools/microstrip_z0/tests/test_microstrip.py new file mode 100644 index 0000000..5bc9b7b --- /dev/null +++ b/tools/microstrip_z0/tests/test_microstrip.py @@ -0,0 +1,64 @@ +import math + +from tools.fh_crosscheck.parse_weeks import parse_line_params +from tools.microstrip_z0.microstrip import ( + eff_permittivity, + geometric_gap, + microstrip_z0, +) + + +def test_eff_permittivity_air_and_bounds(): + # Air / no substrate returns 1.0. + assert eff_permittivity(150e-6, 0.0, 4.4) == 1.0 + assert eff_permittivity(150e-6, 200e-6, 1.0) == 1.0 + # With dielectric, eff_er lies strictly between 1 and er. + ee = eff_permittivity(150e-6, 200e-6, 4.4) + assert 1.0 < ee < 4.4 + + +def test_eff_permittivity_matches_weeks_c_formula(): + # Value cross-checked against src/calcl.c calc_eff_dielectric() output + # for w=150um, h=200um, er=4.4 (weeks prints 3.1118). + ee = eff_permittivity(150e-6, 200e-6, 4.4) + assert abs(ee - 3.1118) < 1e-3 + + +def test_microstrip_z0_known_geometry(): + # Consistent microstrip used by examples/test_microstrip.yaml; weeks + # reports Z0 ~= 81.06 Ohm, so the closed form must land within ~1%. + eff_er, z0 = microstrip_z0(150e-6, 200e-6, 4.4) + assert abs(eff_er - 3.1118) < 1e-3 + assert abs(z0 - 81.06) / 81.06 < 0.01 + + +def test_microstrip_z0_increases_for_narrower_line(): + # Narrower trace (smaller w/h) -> higher characteristic impedance. + _e1, wide = microstrip_z0(400e-6, 200e-6, 4.4) + _e2, narrow = microstrip_z0(100e-6, 200e-6, 4.4) + assert narrow > wide + + +def test_geometric_gap(): + conductors = [ + {"name": "line0", "y": 0.0, "h": 2.0e-6}, + {"name": "line1", "y": 202e-6, "h": 18e-6}, + ] + assert math.isclose(geometric_gap(conductors, 1), 200e-6, rel_tol=1e-9) + + +def test_parse_line_params_extracts_z0(): + text = """\ +*** TRANSMISSION-LINE PARAMETERS (per line, quasi-TEM) at 30.00 MHz *** + +Line eff_er Z0(Ohm) C(F/m) a_c(dB/m) a_d(dB/m) a(dB/m) beta(rad/m) eff_er'' + + 1 +3.1118e+00 +8.1058e+01 +7.2591e-11 +7.9381e-01 +2.1939e-01 +1.0132e+00 +1.1091e+00 +1.4173e-01 + + gamma = alpha + j*beta +""" + params = parse_line_params(text) + assert len(params) == 1 + assert abs(params[0]["eff_er"] - 3.1118) < 1e-9 + assert abs(params[0]["z0"] - 81.058) < 1e-3 + assert abs(params[0]["eff_er_im"] - 0.14173) < 1e-4