"""
Microstrip lines: quasi-TEM ports
=================================

The tutorials so far split into two worlds: closed metal pipes
(waveguides, tutorial 06/07) and ideal two-conductor lines (coax,
tutorials 02–04).  This one opens the third and — for most RF work —
most important world: **printed transmission lines**.  A microstrip is
a flat conductor trace on a dielectric substrate over a ground plane,
and its cross-section is *inhomogeneous*: part of the field travels in
the dielectric, part in the air above.  That single fact changes the
character of the fundamental mode, and this tutorial is about
understanding — and measuring — exactly how.

The structure is deliberately plain: a straight 50 Ω line on an
FR4-class substrate inside a shielding box.  Bends, junctions and
components come in the next tutorial; here the line itself is the
subject.
"""

# sphinx_gallery_thumbnail_number = 2

# %%
# The geometry: substrate, trace, shield
# --------------------------------------
#
# Three bricks build the cross-section: the dielectric substrate
# (εᵣ = 4.3, 0.8 mm — FR4 territory), the air volume above it with the
# trace cut out, and the PEC trace itself, 1.2 mm wide and 0.2 mm
# thick.  Everything sits in a PEC shield box, whose floor doubles as
# the ground plane — the same hole-in-metal pattern as before, just
# with two filling materials instead of one.
#
# Two remarks on the box.  It is wide and tall enough (side walls
# more than four trace widths away, lid five substrate heights up)
# that it barely disturbs the line.  And like every closed metal
# enclosure it has resonances of its own: box modes that would sit on
# top of the line's behaviour.  The band below stays under the first
# one — for this cross-section the lowest box mode comes in near
# 17 GHz, and we stop at 15.
#
# Half the model is enough
# ------------------------
#
# The cross-section is mirror-symmetric about the vertical plane
# through the trace centre, and so is the mode we want: the field
# pushes straight down from trace to ground, so the transverse field
# component *across* that plane vanishes on it, and the magnetic field
# threads through it at right angles.  That is exactly a **magnetic
# wall**, and declaring one on the ``xmin`` face lets the mesher stop
# at the plane and simulate the right half only:
#
# .. code-block:: python
#
#     mio.GeometryModel(boundary_conditions={"xmin": "SymmetryPMC"})
#
# The geometry stays as it is — full bricks, centred on the plane.
# The declaration alone decides how much of it gets meshed, so
# switching the symmetry off later means deleting one argument, not
# rebuilding the model.  Half the cells means half the memory and
# roughly half the run time, and it costs nothing in accuracy — if
# anything the opposite, because the domain now ends exactly at the
# trace centre, so the discretisation is symmetric about it by
# construction rather than by luck.
#
# What symmetry does cost is *modes*.  A magnetic wall keeps only the
# fields that are symmetric about it, so any resonance of the box that
# happens to be antisymmetric is filtered out of the model entirely —
# convenient here, where such a mode could only be spurious clutter,
# but worth remembering whenever a symmetry plane is declared: the
# structure and the excitation must both respect it.

import matplotlib.pyplot as plt
import numpy as np

import magnelio as mio
from magnelio import geo, ports
from magnelio.constants import *

h_sub = 0.8e-3  # substrate height
w_strip = 1.2e-3  # trace width (tuned for 50 ohm, see below)
t_strip = 0.2e-3  # trace thickness
W_box = 8.0e-3  # shield width
H_box = 5.0e-3  # shield height
L = 20.0e-3  # line length
eps_r = 4.3
f_max = 15.0e9

pec = mio.Material.pec()
air = mio.Material.air()
fr4 = mio.Material.from_isotropic(name="FR4", epsilon=eps_r)

substrate = geo.Brick(origin=(-W_box / 2, 0.0, 0.0), size=(W_box, h_sub, L), material=fr4)
air_cap = geo.Brick(origin=(-W_box / 2, h_sub, 0.0), size=(W_box, H_box - h_sub, L), material=air)
strip = geo.Brick(origin=(-w_strip / 2, h_sub, 0.0), size=(w_strip, t_strip, L), material=pec)

model = mio.GeometryModel(boundary_conditions={"xmin": "SymmetryPMC"})
model.add(substrate)
model.add(geo.Difference(air_cap, strip))
model.add(strip)

model.add_port(ports.PortWaveguide(name="port1", plane="zmin", n_modes=1))
model.add_port(ports.PortWaveguide(name="port2", plane="zmax", n_modes=1))

mesh = mio.Mesh.from_geometry(
    model,
    mio.MeshControl(min_nodes_per_wavelength=25),
    f_max=f_max,
)
print(f"grid: {mesh.Nx} x {mesh.Ny} x {mesh.Nz} cells")

fig, ax = model.plot_cross_section("z", L / 2, mesh=mesh, title="microstrip cross-section")

# %%
# The cross-section plot shows the mesher at work on thin layers: the
# 0.8 mm substrate and the 0.2 mm trace anchor grid planes at their
# material boundaries, so the y-cells grade from fine around the
# trace to coarse in the air above.  Nobody meshed this by hand — the
# geometry *is* the meshing instruction.  It also shows the symmetry
# plane doing its work: the drawn structure still spans the full
# width, but the grid covers only the right half of it.
#
# The quasi-TEM mode
# ------------------
#
# In a coax, air everywhere, the fundamental mode is exactly TEM and
# its impedance is a closed formula.  Here no exact TEM mode exists:
# the field would have to travel at two different speeds at once,
# in the substrate and in the air.  The physical fundamental is
# **quasi-TEM** — almost transverse, zero cut-off, but with its
# properties set by a weighted compromise between the two dielectrics.
# There is no textbook formula for that compromise; the port solves
# the 2D cross-section problem numerically, before any time stepping:

analysis = mio.AnalysisScatteringTD(mesh=mesh, f_max=f_max, verbose=False)

report = analysis.solve_ports()["port1"]
print(report)

qtem = report.modes[0]
eps_eff_static = (C0 * qtem.gamma(10e9).imag / (2 * np.pi * 10e9)) ** 2
print(f"eps_eff (quasi-static): {eps_eff_static:.3f}")

# %%
# Two numbers to hold on to.  The line impedance comes out at
# **51.5 Ω** — the trace width was picked to land near 50 Ω, and it
# does so within 3 %.  The classic Hammerstad hand formula (open
# microstrip, infinitely thin trace) predicts about 58 Ω for this
# width; the shield lid pushes the impedance down and so does the very
# real 0.2 mm trace thickness.  Closed formulas stop where real
# cross-sections begin — which is precisely why the port runs a
# numerical mode solver.
#
# Note what the report says above the numbers: the port window is cut
# by the symmetry plane, and the impedance is reported for the *full*
# model.  On the meshed half the mode solver actually measures twice
# that value, since half a trace over half a ground plane holds half
# the capacitance; the two halves sit in parallel, and the port does
# that bookkeeping so the number on screen is the one the physical
# line has.
#
# And the effective permittivity is **2.99**: between air (1) and
# substrate (4.3), the exact weighting of the field's split residence.
# The mode profile shows that split directly — the field crowds into
# the substrate under the trace, with a fringing skirt in the air.
# It is drawn across the full width: only half of it was solved, and
# the mirror image is filled in for the picture.

fig, ax = qtem.plot(geometry=model)
ax.set_title("quasi-TEM mode, transverse E")

# %%
# Running the line and reading the S-parameters
# ---------------------------------------------
#
# A matched straight line is the simplest possible S-parameter test:
# everything should go through, nothing should come back.

result = analysis.run(excited=["port1"])

fig, ax = result.plot_s(("port2", "port1"), ("port1", "port1"))
ax.set_title("straight 50 Ω microstrip")

s11 = result.S("port1", "port1")
s21 = result.S("port2", "port1")
print(f"|S21|: min {20 * np.log10(np.abs(s21).min()):.2f} dB")
print(f"|S11|: max {20 * np.log10(np.abs(s11).max()):.1f} dB")

# %%
# Transmission hugs 0 dB.  The reflection sits near −32 dB at its
# worst — and it is worth understanding what that number *is*.  It is
# not a property of the line (a uniform line reflects nothing); it is
# the residual of the port termination absorbing a dispersive
# quasi-TEM wave.  For exact-TEM lines, tutorial 03 showed floors
# beyond −100 dB, because there the termination can be made
# analytically exact.  A quasi-TEM mode has no such exact absorber,
# and the −30 dB class is the honest broadband floor of its
# termination — background, not physics, and far below anything a
# real component (or a real connector) will reflect.
#
# Dispersion: the microstrip's signature
# --------------------------------------
#
# The port's ε_eff was *one number* — the quasi-static limit.  But a
# microstrip is dispersive: as frequency rises the field retreats
# into the substrate and ε_eff creeps upward toward εᵣ.  The 3D
# simulation contains that physics, and the phase of S21 is the
# instrument to extract it: over a line of length L, the mode
# accumulates φ = −β L, so β — and with it
# ε_eff = (c₀ β / ω)² — can be read off per frequency:

f_axis = result.f_axis
phase = np.unwrap(np.angle(s21))
eps_eff_td = (C0 * (-phase) / (2 * np.pi * f_axis * L)) ** 2

sel = f_axis >= 1.0e9  # phase-derived values are 0/0-noisy near DC
fig, ax = plt.subplots(figsize=(7, 4.2))
ax.plot(f_axis[sel] / 1e9, eps_eff_td[sel], label="3D simulation (from S21 phase)")
ax.axhline(eps_eff_static, color="gray", ls="--", label="port mode solver (quasi-static)")
ax.set_xlabel("frequency [GHz]")
ax.set_ylabel(r"$\varepsilon_\mathrm{eff}$")
ax.legend()
ax.set_title("microstrip dispersion")
fig.tight_layout()

for f_probe in (5e9, 10e9, 15e9):
    print(f"eps_eff({f_probe / 1e9:.0f} GHz) = {float(np.interp(f_probe, f_axis, eps_eff_td)):.3f}")

# %%
# The curve starts at the quasi-static value and rises to ≈ 3.3 at
# 15 GHz — a 10 % walk toward εᵣ across the band, right in the range
# classical dispersion models of the Getsinger family predict for
# this geometry.  This is the practical reason quasi-static design
# formulas come with frequency disclaimers, and why a broadband
# design gets verified in a full-wave solver: the line the formulas
# describe at 1 GHz is measurably *electrically longer* at 15.
#
# Where to go next
# ----------------
#
# New in this tutorial: an inhomogeneous cross-section built from two
# dielectrics plus a trace, a symmetry plane that halves the model for
# free, the quasi-TEM port with its numerically solved impedance and
# mode profile, the honest reading of a quasi-TEM termination floor,
# and dispersion extracted from the S21 phase.  The next tutorial
# bends this line around corners and builds a real component out of
# it — a Wilkinson power divider, including its lumped isolation
# resistor.
