.. DO NOT EDIT. .. THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY. .. TO MAKE CHANGES, EDIT THE SOURCE PYTHON FILE: .. "tutorials/plot_17_pcb_import.py" .. LINE NUMBERS ARE GIVEN BELOW. .. only:: html .. note:: :class: sphx-glr-download-link-note :ref:`Go to the end ` to download the full example code. .. rst-class:: sphx-glr-example-title .. _sphx_glr_tutorials_plot_17_pcb_import.py: Importing a circuit board ========================= Tutorial 09 built a microstrip line out of three bricks. That works for a line, and stops working the moment the board has more than one trace on it: a real layout is drawn in a layout tool, and redrawing it with primitives is both work and a fresh source of discrepancies between what was simulated and what was manufactured. A board leaves its layout tool as *fabrication data* — the set of files a board house is sent. :func:`~magnelio.io.import_pcb` reads that set, so the model comes from the same files the board is built from. This tutorial imports a 50 Ω through line and runs it, and along the way shows the one setting a board simulation needs and nothing else does. The fabrication set (``microstrip.gbrjob`` and the Gerber files it names) sits next to this script. .. GENERATED FROM PYTHON SOURCE LINES 20-41 .. code-block:: Python from pathlib import Path import matplotlib.pyplot as plt import numpy as np import magnelio as mio from magnelio import geo, ports from magnelio.constants import * from magnelio.io import import_pcb # The fabrication set sits next to this script, or in the working # directory. HERE = Path(__file__).parent if "__file__" in globals() else Path.cwd() FAB = HERE / "microstrip.gbrjob" L = 20.0e-3 # board length, and the length of the line H_AIR = 4.0e-3 # air above the board F_MAX = 15.0e9 .. GENERATED FROM PYTHON SOURCE LINES 43-56 What is in a fabrication set ---------------------------- The set is a Gerber file per copper layer, one for the board outline, drill files for the holes, and a **job file** that records the stackup. The job file is the one that matters here: Gerber files are flat drawings, and nothing in them says how thick a copper layer is or what the dielectric between two layers is made of. Without it there is no third dimension to build, and the import says so rather than inventing one. Import first, ask questions later — called without materials, :func:`~magnelio.io.import_pcb` returns everything the set describes. .. GENERATED FROM PYTHON SOURCE LINES 56-64 .. code-block:: Python board = import_pcb(FAB) for solid in board.members(): low, high = solid.bounding_box() span = ", ".join(f"{lo * 1e3:6.3f}…{hi * 1e3:6.3f}" for lo, hi in zip(low, high)) print(f"{solid.name:<14} {span} mm {solid.material.name}") .. rst-class:: sphx-glr-script-out .. code-block:: none UserWarning: The stackup states a loss tangent for dielectric_1 (tan d = 0.02), which was not modelled: the imported dielectric is lossless. A loss tangent is one number, and the job file does not record the frequency it was measured at. Supply that frequency and pass the result through materials=, for example Material.dispersive(name=..., model=DispersionModel.djordjevic_sarkar(eps_r=..., tan_delta=..., f_ref=...)). F.Cu -0.000…20.000, 3.400… 4.600, 0.000… 0.035 mm PEC dielectric_1 0.000…20.000, 0.000… 8.000, -0.800… 0.000 mm FR4 B.Cu 0.000…20.000, 0.000… 8.000, -0.835…-0.800 mm PEC .. GENERATED FROM PYTHON SOURCE LINES 65-108 Three things in that listing are worth reading carefully. The **names** are the layer names of the stackup. They are the handle materials are assigned against, and they survive a re-export of the board, so a mapping written once keeps working after the layout changes. The **z ranges** show the stackup taken literally: the top face of the substrate is the origin, the top copper occupies 0 to 35 µm, and the stack grows downwards. Nothing was fitted or snapped — the layers meet on coincident faces. And the **materials** came from the job file: copper is a perfect conductor by default, and the substrate carries the permittivity the stackup states. What the job file's loss tangent does *not* do is turn into a lossy material, and the import says so with a warning: a loss tangent is one number at a frequency the job file does not record, and a constant loss tangent is not causal. Modelling it needs that frequency, which only you can supply: .. code-block:: python from magnelio.materials.dispersion import DispersionModel fr4 = mio.Material.dispersive( name="FR4", model=DispersionModel.djordjevic_sarkar( eps_r=4.3, tan_delta=0.02, f_ref=10e9 ), ) board = import_pcb(FAB, {"dielectric_1": fr4}) This tutorial keeps the line lossless, so the numbers below are about the geometry and nothing else. The model around the board -------------------------- A board is not a model on its own: a microstrip lives as much in the air above the trace as in the substrate under it, so the air has to be part of the domain. It is added as an ordinary brick sitting on top of the board — the cells *beside* the trace fall to the model background, which is air as well. .. GENERATED FROM PYTHON SOURCE LINES 108-116 .. code-block:: Python model = mio.GeometryModel() model.add(board) model.add(geo.Brick(origin=(0.0, 0.0, 35e-6), size=(L, 8.0e-3, H_AIR), material="air")) model.add_port(ports.PortWaveguide(name="port1", plane="xmin", n_modes=1)) model.add_port(ports.PortWaveguide(name="port2", plane="xmax", n_modes=1)) .. rst-class:: sphx-glr-script-out .. code-block:: none GeometryModel(4 shapes, background=air) .. GENERATED FROM PYTHON SOURCE LINES 117-135 Meshing a board: the one setting that matters --------------------------------------------- Copper on a board is 35 µm thick. That is two decades below any cell size a board simulation can afford, and a grid that resolved it would be unusable — which is why tutorial 09 drew its trace 0.2 mm thick, thicker than any real copper, so that the grid could carry it. It does not have to. A perfectly conducting layer thinner than the **cell-size floor** gets a single grid plane on its substrate side, and its thickness enters through the sub-cell material fractions of the neighbouring cells instead of through a layer of cells of its own. The floor is what "thin" is measured against, so it has to be set — without ``min_cell_size`` the mechanism does not run at all and the mesher tries to resolve the metal. Pick it from the smallest feature the *fields* need resolved, well above the copper thickness: .. GENERATED FROM PYTHON SOURCE LINES 135-148 .. code-block:: Python mesh = mio.Mesh.from_geometry( model, mio.MeshControl(min_nodes_per_wavelength=25, min_cell_size=150e-6), f_max=F_MAX, ) print(f"grid: {mesh.Nx} x {mesh.Ny} x {mesh.Nz} cells") print(f"smallest cell: {min(np.diff(mesh.grid.z)) * 1e6:.0f} um, copper: 35 um") fig, ax = model.plot_cross_section("x", L / 2, mesh=mesh, title="board cross-section") ax.set_xlim(2.6, 5.4) # mm — the trace, not the whole 8 mm width ax.set_ylim(-1.05, 0.95) .. image-sg:: /tutorials/images/sphx_glr_plot_17_pcb_import_001.png :alt: board cross-section :srcset: /tutorials/images/sphx_glr_plot_17_pcb_import_001.png :class: sphx-glr-single-img .. rst-class:: sphx-glr-script-out .. code-block:: none mesh | feature planes mesh | grid lines mesh | materials mesh | conformal cells mesh | PEC masks mesh | 52 x 24 x 11 cells grid: 52 x 24 x 11 cells smallest cell: 200 um, copper: 35 um (-1.05, 0.95) .. GENERATED FROM PYTHON SOURCE LINES 149-167 The cross-section shows the payoff — zoomed onto the trace, with three more millimetres of air above the frame. The substrate boundaries anchor grid planes and the cells are finest inside the substrate and coarsen into the air, exactly as in tutorial 09, but the smallest cell printed above is hundreds of micrometres, many times the copper it carries, and the whole board fits in a grid of some twenty thousand cells. Trace and ground plane are drawn at their true 35 µm, and neither costs a layer of cells: the metal is there in the physics without being there in the grid. The quasi-TEM mode, from a drawn trace -------------------------------------- From here on nothing is specific to an imported board. The port solves the 2D cross-section problem numerically, because a microstrip has no exact TEM mode — the field would have to travel at two speeds at once, in the substrate and in the air — and no closed formula for the compromise it settles on. .. GENERATED FROM PYTHON SOURCE LINES 167-177 .. code-block:: Python analysis = mio.AnalysisScatteringTD(mesh=mesh, 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}") .. rst-class:: sphx-glr-script-out .. code-block:: none Port 'port1' — 1 mode(s) z_line = 51.97 Ω (quasi-static, on this grid) [0] QTEM_lap00 TEM f_c = 0.0000 GHz z_line = 51.97 Ω ε_eff = 3.171 termination = mur (chain spread 2.8e-01) eps_eff (quasi-static): 3.171 .. GENERATED FROM PYTHON SOURCE LINES 178-183 Running the line ---------------- A matched straight line is the simplest S-parameter test there is: everything should go through, nothing should come back. .. GENERATED FROM PYTHON SOURCE LINES 183-194 .. code-block:: Python result = analysis.run(excited=["port1"]) fig, ax = result.plot_s(("port2", "port1"), ("port1", "port1")) ax.set_title("50 Ω microstrip, imported from fabrication data") 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") .. image-sg:: /tutorials/images/sphx_glr_plot_17_pcb_import_002.png :alt: 50 Ω microstrip, imported from fabrication data :srcset: /tutorials/images/sphx_glr_plot_17_pcb_import_002.png :class: sphx-glr-single-img .. rst-class:: sphx-glr-script-out .. code-block:: none |S21|: min -0.00 dB |S11|: max -34.5 dB .. GENERATED FROM PYTHON SOURCE LINES 195-207 Transmission hugs 0 dB and the reflection sits in the −30 dB class — the honest broadband floor of a quasi-TEM port termination, which tutorial 09 discusses at length. The line behaves; the point of this tutorial is that nobody drew it here. Dispersion, read off the phase ------------------------------ The port's ε_eff was the quasi-static limit. A microstrip is dispersive — as frequency rises the field retreats into the substrate and ε_eff creeps toward εᵣ — and the phase of S21 measures it, since the mode accumulates φ = −βL over the line. .. GENERATED FROM PYTHON SOURCE LINES 207-222 .. code-block:: Python f_axis = result.f_axis phase = np.unwrap(np.angle(s21)) eps_eff = (C0 * (-phase) / (2 * np.pi * f_axis * L)) ** 2 fig, ax = plt.subplots(figsize=(6.0, 3.6)) ax.plot(f_axis * 1e-9, eps_eff, label="from S21 phase") ax.axhline(eps_eff_static, ls="--", c="grey", label="port, quasi-static") ax.axhline(4.3, ls=":", c="black", label="substrate εᵣ") ax.set_xlabel("frequency (GHz)") ax.set_ylabel(r"$\varepsilon_\mathrm{eff}$") ax.set_title("microstrip dispersion") ax.legend() fig.tight_layout() .. image-sg:: /tutorials/images/sphx_glr_plot_17_pcb_import_003.png :alt: microstrip dispersion :srcset: /tutorials/images/sphx_glr_plot_17_pcb_import_003.png :class: sphx-glr-single-img .. GENERATED FROM PYTHON SOURCE LINES 223-243 What to take away ----------------- * The **job file is required**. Fill in the physical stackup in the layout tool before exporting; a stackup without thicknesses is refused, because a layout without them has no shape. * **Names are the interface.** Copper layers keep their stackup names, dielectrics are numbered (``dielectric_1``, …) because layout tools name every core after its material, and plated barrels are ``via_1``, ``via_2``, … in coordinate order. * **Set ``min_cell_size``.** It is what lets the mesher carry 35 µm of copper below the cell size, and it only works for a perfect conductor — the default for imported copper. * What the fabrication data cannot say, the import does not invent: a substrate with no stated permittivity arrives without a material rather than silently as vacuum, and a loss tangent is reported rather than modelled. See :doc:`/methods/pcb-import` for the full treatment — multilayer stackups, blind and buried vias, and what is deliberately left out. .. rst-class:: sphx-glr-timing **Total running time of the script:** (0 minutes 1.500 seconds) .. _sphx_glr_download_tutorials_plot_17_pcb_import.py: .. only:: html .. container:: sphx-glr-footer sphx-glr-footer-example .. container:: sphx-glr-download sphx-glr-download-jupyter :download:`Download Jupyter notebook: plot_17_pcb_import.ipynb ` .. container:: sphx-glr-download sphx-glr-download-python :download:`Download Python source code: plot_17_pcb_import.py ` .. container:: sphx-glr-download sphx-glr-download-zip :download:`Download zipped: plot_17_pcb_import.zip ` .. only:: html .. rst-class:: sphx-glr-signature `Gallery generated by Sphinx-Gallery `_