Skip to content

Geometry reference

Every supported transmission line geometry, with its required dimensional fields and which closed-form formula it dispatches to.

Single-line

Microstrip

lineforge.geometry.types.Microstrip

Bases: _BaseGeometry

Microstrip: a strip on top of a dielectric over a ground plane.

::

 ┌───────┐  ← strip (W wide, T thick)
 │       │
─┴───────┴─ ─ ─ ─ ─
  dielectric (H thick, εr)
 ──────────────────  ← ground plane
Source code in src/lineforge/geometry/types.py
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
class Microstrip(_BaseGeometry):
    """Microstrip: a strip on top of a dielectric over a ground plane.

    ::

         ┌───────┐  ← strip (W wide, T thick)
         │       │
        ─┴───────┴─ ─ ─ ─ ─
          dielectric (H thick, εr)
         ──────────────────  ← ground plane

    """

    type: Literal["microstrip"] = "microstrip"
    W: Length = Field(..., description="Strip width.")
    H: Length = Field(..., description="Dielectric height.")
    T: Length = Field(..., description="Strip thickness.")
    er: float = Field(..., description="Relative permittivity of the dielectric.", ge=1)
    tan_delta: float = Field(0.0, description="Loss tangent of the dielectric.", ge=0)
    rho: float = Field(COPPER_RHO, description="Conductor resistivity [Ω·m].", gt=0)

Solver: Hammerstad-Jensen with Wheeler thickness correction. Validity: 0.05 ≤ W/H ≤ 20, εr ≤ 128.

Embedded (coated) microstrip

lineforge.geometry.types.EmbeddedMicrostrip

Bases: _BaseGeometry

Embedded (coated) microstrip: microstrip plus a coating dielectric on top.

::

 ╳╳╳╳╳╳╳╳╳╳╳╳╳╳  ← coating (H2 thick, er2)
 ╳╳┌──────┐╳╳╳
 ╳╳│      │╳╳╳
─┴─┴──────┴─┴─
    dielectric (H, er)
 ──────────────  ← ground plane
Source code in src/lineforge/geometry/types.py
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
class EmbeddedMicrostrip(_BaseGeometry):
    """Embedded (coated) microstrip: microstrip plus a coating dielectric on top.

    ::

         ╳╳╳╳╳╳╳╳╳╳╳╳╳╳  ← coating (H2 thick, er2)
         ╳╳┌──────┐╳╳╳
         ╳╳│      │╳╳╳
        ─┴─┴──────┴─┴─
            dielectric (H, er)
         ──────────────  ← ground plane

    """

    type: Literal["embedded_microstrip"] = "embedded_microstrip"
    W: Length = Field(..., description="Strip width.")
    H: Length = Field(..., description="Substrate dielectric height.")
    H2: Length = Field(..., description="Coating thickness above the strip.")
    T: Length = Field(..., description="Strip thickness.")
    er: float = Field(..., description="Substrate relative permittivity.", ge=1)
    er2: float = Field(..., description="Coating relative permittivity.", ge=1)
    tan_delta: float = Field(0.0, ge=0)
    tan_delta_2: float = Field(0.0, ge=0)
    rho: float = Field(COPPER_RHO, gt=0)

Solver: IPC-2141A coated-microstrip blend (η-factor based on coating thickness).

Symmetric stripline

lineforge.geometry.types.StriplineSymmetric

Bases: _BaseGeometry

Symmetric stripline: a strip centered between two ground planes.

::

 ──────────────  ← top ground
      ┌───┐      ← strip (W, T)
      │   │
 ──────────────  ← bottom ground

 B = total plate separation, dielectric fills it (er)
Source code in src/lineforge/geometry/types.py
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
class StriplineSymmetric(_BaseGeometry):
    """Symmetric stripline: a strip centered between two ground planes.

    ::

         ──────────────  ← top ground
              ┌───┐      ← strip (W, T)
              │   │
         ──────────────  ← bottom ground

         B = total plate separation, dielectric fills it (er)
    """

    type: Literal["stripline_symmetric"] = "stripline_symmetric"
    W: Length = Field(..., description="Strip width.")
    T: Length = Field(..., description="Strip thickness.")
    B: Length = Field(..., description="Plate-to-plate separation.")
    er: float = Field(..., ge=1)
    tan_delta: float = Field(0.0, ge=0)
    rho: float = Field(COPPER_RHO, gt=0)

Solver: Cohn wide-strip formula with Wadell finite-thickness correction. Validity: W/(B−T) > 0.35, T < 0.25·B.

Asymmetric stripline

lineforge.geometry.types.StriplineAsymmetric

Bases: _BaseGeometry

Asymmetric stripline: strip is offset from the midline between ground planes.

H1 is the dielectric thickness above the strip, H2 below. The strip itself is at the boundary between H1 and H2; total cavity = H1 + T + H2.

For real PCB stackups where the dielectric above and below the strip differ (e.g. Core above, Prepreg below: common when routing on an inner signal layer between a plane and a power layer), pass er_above / er_below (and optionally tan_delta_above / tan_delta_below). When supplied, these override the single er / tan_delta values: the closed-form solver uses a capacitance-weighted εr_eff, and the bitmap rasterizer paints the two halves with different materials.

For multi-layer stacks on either side (e.g. Prepreg + voided plane + Core when an intermediate plane is voided to push the reference down), pass stack_above / stack_below as a list of :class:DielectricLayer. The stack is series-reduced via the parallel-plate (C-series) formula εr_eq = h_total / Σ(hᵢ/εᵢ) to derive H1/H2/εr_above/εr_below automatically: the explicit per-side fields are then unused.

Source code in src/lineforge/geometry/types.py
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
class StriplineAsymmetric(_BaseGeometry):
    """Asymmetric stripline: strip is offset from the midline between ground planes.

    H1 is the dielectric thickness above the strip, H2 below. The strip itself
    is at the boundary between H1 and H2; total cavity = H1 + T + H2.

    For real PCB stackups where the dielectric above and below the strip differ
    (e.g. Core above, Prepreg below: common when routing on an inner signal
    layer between a plane and a power layer), pass ``er_above`` / ``er_below``
    (and optionally ``tan_delta_above`` / ``tan_delta_below``). When supplied,
    these override the single ``er`` / ``tan_delta`` values: the closed-form
    solver uses a capacitance-weighted εr_eff, and the bitmap rasterizer
    paints the two halves with different materials.

    For multi-layer stacks on either side (e.g. Prepreg + voided plane + Core
    when an intermediate plane is voided to push the reference down), pass
    ``stack_above`` / ``stack_below`` as a list of :class:`DielectricLayer`.
    The stack is series-reduced via the parallel-plate (C-series) formula
    ``εr_eq = h_total / Σ(hᵢ/εᵢ)`` to derive H1/H2/εr_above/εr_below
    automatically: the explicit per-side fields are then unused.
    """

    type: Literal["stripline_asymmetric"] = "stripline_asymmetric"
    W: Length = Field(..., description="Strip width.")
    T: Length = Field(..., description="Strip thickness.")
    H1: Length = Field(..., description="Dielectric thickness above the strip.")
    H2: Length = Field(..., description="Dielectric thickness below the strip.")
    er: float = Field(
        ...,
        description="Bulk relative permittivity (used when er_above/er_below not given).",
        ge=1,
    )
    tan_delta: float = Field(0.0, ge=0)
    rho: float = Field(COPPER_RHO, gt=0)
    er_above: float | None = Field(
        None,
        description="Optional εr of the dielectric above the strip (overrides er for that half).",
        ge=1,
    )
    er_below: float | None = Field(
        None,
        description="Optional εr of the dielectric below the strip (overrides er for that half).",
        ge=1,
    )
    tan_delta_above: float | None = Field(
        None,
        description="Optional loss tangent above the strip.",
        ge=0,
    )
    tan_delta_below: float | None = Field(
        None,
        description="Optional loss tangent below the strip.",
        ge=0,
    )
    stack_above: list[DielectricLayer] | None = Field(
        None,
        description=(
            "Optional multi-layer dielectric stack above the strip. When set, "
            "H1, er_above and tan_delta_above are derived from the stack via "
            "C-series reduction; do not also pass H1/er_above/tan_delta_above "
            "explicitly."
        ),
    )
    stack_below: list[DielectricLayer] | None = Field(
        None,
        description=(
            "Optional multi-layer dielectric stack below the strip. When set, "
            "H2, er_below and tan_delta_below are derived from the stack."
        ),
    )

    @model_validator(mode="before")
    @classmethod
    def _derive_h_from_stacks(cls, data: Any) -> Any:
        """Pre-fill H1/er_above/tan_delta_above (and below) from stacks if given.

        Runs in 'before' mode so the derived values pass through the normal
        Length / float field validators downstream. Also fills the bulk ``er``
        field with the larger of the two stack-derived εr_eq values (it acts
        only as a fallback once split-εr fields are populated, but Pydantic
        still requires it to be ≥ 1).
        """
        if not isinstance(data, dict):
            return data

        any_stack = False
        for side, h_key, er_key, tan_key in (
            ("stack_above", "H1", "er_above", "tan_delta_above"),
            ("stack_below", "H2", "er_below", "tan_delta_below"),
        ):
            stack = data.get(side)
            if not stack:
                continue
            any_stack = True
            # Allow either DielectricLayer instances or plain dicts.
            layers = [
                layer if isinstance(layer, DielectricLayer) else DielectricLayer(**layer)
                for layer in stack
            ]
            h_total, er_eq, tan_eq = series_reduce(layers)
            for k, v in ((h_key, h_total), (er_key, er_eq), (tan_key, tan_eq)):
                existing = data.get(k)
                if existing not in (None, 0, 0.0):
                    # Allow the model_dump → model_validate_json round-trip:
                    # the dump emits both ``stack_below`` and the derived
                    # ``H2``/``er_below``/``tan_delta_below``. Accept exact
                    # numerical agreement; reject genuine inconsistencies.
                    if isinstance(existing, (int, float)) and abs(existing - v) <= max(
                        1e-9 * abs(v), 1e-12
                    ):
                        continue
                    raise ValueError(
                        f"StriplineAsymmetric: cannot pass both {side} and {k}; "
                        f"the stack derives {k} automatically."
                    )
                data[k] = v
            # Materialize the validated layers back into the dict so the field
            # type sees DielectricLayer instances (not raw dicts).
            data[side] = layers

        # When at least one stack is given, ``er`` is unused (er_above/er_below
        # take over) but Pydantic still requires er ≥ 1. Default it to the
        # larger derived εr_eq so it's at least dimensionally sensible.
        if any_stack and "er" not in data:
            candidates = [data.get("er_above"), data.get("er_below")]
            data["er"] = max(c for c in candidates if c is not None)
        return data

Solver: Wadell two-parallel-stripline approximation.

CPWG (coplanar waveguide with ground)

lineforge.geometry.types.CPWG

Bases: _BaseGeometry

Coplanar waveguide with ground plane (grounded coplanar waveguide).

Center signal trace flanked by two coplanar ground rails (gap S each side), with a continuous ground plane H below. ::

 ████  ┌────┐  ████   ← top: coplanar grounds + signal (W)
      S│    │S        ← S is the gap to each side ground
 ─────┴────┴───────
      dielectric (H, er)
 ──────────────────   ← bottom ground plane
Source code in src/lineforge/geometry/types.py
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
class CPWG(_BaseGeometry):
    """Coplanar waveguide with ground plane (grounded coplanar waveguide).

    Center signal trace flanked by two coplanar ground rails (gap S each side),
    with a continuous ground plane H below.
    ::

         ████  ┌────┐  ████   ← top: coplanar grounds + signal (W)
              S│    │S        ← S is the gap to each side ground
         ─────┴────┴───────
              dielectric (H, er)
         ──────────────────   ← bottom ground plane
    """

    type: Literal["cpwg"] = "cpwg"
    W: Length = Field(..., description="Center signal trace width.")
    S: Length = Field(..., description="Gap from signal to each side ground.")
    H: Length = Field(..., description="Substrate height to bottom ground plane.")
    T: Length = Field(..., description="Conductor thickness.")
    er: float = Field(..., ge=1)
    tan_delta: float = Field(0.0, ge=0)
    rho: float = Field(COPPER_RHO, gt=0)

Solver: Wen elliptic-integral formula via scipy.special.ellipk.

Differential

Edge-coupled diff microstrip

lineforge.geometry.types.EdgeCoupledDiffMicrostrip

Bases: _BaseGeometry

Edge-coupled differential pair (microstrip).

Two parallel strips on top of a dielectric, separated by a gap S.

Source code in src/lineforge/geometry/types.py
276
277
278
279
280
281
282
283
284
285
286
287
288
289
class EdgeCoupledDiffMicrostrip(_BaseGeometry):
    """Edge-coupled differential pair (microstrip).

    Two parallel strips on top of a dielectric, separated by a gap S.
    """

    type: Literal["edge_coupled_diff_microstrip"] = "edge_coupled_diff_microstrip"
    W: Length = Field(..., description="Each strip's width.")
    S: Length = Field(..., description="Edge-to-edge gap between the two strips.")
    H: Length = Field(..., description="Dielectric height to ground plane.")
    T: Length = Field(..., description="Strip thickness.")
    er: float = Field(..., ge=1)
    tan_delta: float = Field(0.0, ge=0)
    rho: float = Field(COPPER_RHO, gt=0)

Solver: IPC-2141A coupling correction on the Hammerstad-Jensen single-trace Z₀.

Edge-coupled diff stripline

lineforge.geometry.types.EdgeCoupledDiffStripline

Bases: _BaseGeometry

Edge-coupled differential pair (stripline).

Two parallel strips centered between two ground planes (separation B), with edge-to-edge gap S in the dielectric (er).

Source code in src/lineforge/geometry/types.py
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
class EdgeCoupledDiffStripline(_BaseGeometry):
    """Edge-coupled differential pair (stripline).

    Two parallel strips centered between two ground planes (separation B),
    with edge-to-edge gap S in the dielectric (er).
    """

    type: Literal["edge_coupled_diff_stripline"] = "edge_coupled_diff_stripline"
    W: Length = Field(..., description="Each strip's width.")
    S: Length = Field(..., description="Edge-to-edge gap between the two strips.")
    B: Length = Field(..., description="Plate-to-plate separation.")
    T: Length = Field(..., description="Strip thickness.")
    er: float = Field(..., ge=1)
    tan_delta: float = Field(0.0, ge=0)
    rho: float = Field(COPPER_RHO, gt=0)

Solver: IPC-2141A coupling correction on the symmetric-stripline Z₀.

Broadside-coupled diff stripline

lineforge.geometry.types.BroadsideCoupledDiffStripline

Bases: _BaseGeometry

Broadside-coupled differential pair (stripline).

Two strips stacked vertically, separated by H_between of dielectric, centered between two ground planes.

Source code in src/lineforge/geometry/types.py
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
class BroadsideCoupledDiffStripline(_BaseGeometry):
    """Broadside-coupled differential pair (stripline).

    Two strips stacked vertically, separated by H_between of dielectric,
    centered between two ground planes.
    """

    type: Literal["broadside_coupled_diff_stripline"] = "broadside_coupled_diff_stripline"
    W: Length = Field(..., description="Strip width (both strips identical).")
    H1: Length = Field(..., description="Distance from each strip to the nearer ground.")
    H_between: Length = Field(..., description="Dielectric thickness between the two strips.")
    T: Length = Field(..., description="Strip thickness.")
    er: float = Field(..., ge=1)
    tan_delta: float = Field(0.0, ge=0)
    rho: float = Field(COPPER_RHO, gt=0)

Solver: Wadell §6.5 broadside formula with finite-thickness correction.

Result types

lineforge.results.TLineResult

Bases: BaseModel

Result of a single-mode transmission-line solve.

Phase 1 (analytical) populates the impedance and dielectric fields. Phase 2 (C and Gp) adds C, Gp, and a refined vp. Phase 3 (L and Rs) populates the full RLGC fields, at which point :class:RLGCResult is the preferred return type.

Source code in src/lineforge/results.py
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
class TLineResult(BaseModel):
    """Result of a single-mode transmission-line solve.

    Phase 1 (analytical) populates the impedance and dielectric fields. Phase 2
    (C and Gp) adds C, Gp, and a refined vp. Phase 3 (L and Rs) populates the
    full RLGC fields, at which point :class:`RLGCResult` is the preferred
    return type.
    """

    model_config = ConfigDict(extra="forbid")

    # --- impedance / propagation ---------------------------------------------
    z0: float = Field(..., description="Characteristic impedance Z0 [Ω].", gt=0)
    eps_eff: float = Field(..., description="Effective relative permittivity εeff [-].", ge=1)
    vp: float = Field(..., description="Phase velocity [m/s].", gt=0)
    td_per_inch: float = Field(..., description="Propagation delay [s/inch].", gt=0)

    # --- distributed RLGC (filled in by later phases) ------------------------
    L_per_m: float | None = Field(
        None, description="Series inductance per meter [H/m]. Populated by Phase 2/3.", gt=0
    )
    C_per_m: float | None = Field(
        None, description="Shunt capacitance per meter [F/m]. Populated by Phase 2.", gt=0
    )
    Rs_per_m: float | None = Field(
        None, description="Series resistance per meter [Ω/m]. Populated by Phase 3.", ge=0
    )
    Gp_per_m: float | None = Field(
        None, description="Shunt conductance per meter [S/m]. Populated by Phase 2.", ge=0
    )

    # --- loss estimates -------------------------------------------------------
    conductor_loss_db_per_in: float | None = Field(
        None, description="Conductor loss [dB/inch]. Phase 1 estimate; Phase 3 exact.", ge=0
    )
    dielectric_loss_db_per_in: float | None = Field(
        None, description="Dielectric loss [dB/inch]. Phase 1 estimate; Phase 2 exact.", ge=0
    )

    # --- metadata -------------------------------------------------------------
    method: str = Field(..., description="Which solver produced this result.")
    frequency_hz: float | None = Field(
        None, description="Solve frequency [Hz]. None for frequency-independent analytical.", gt=0
    )
    warnings: list[SolverWarning] = Field(default_factory=list)

lineforge.results.DiffResult

Bases: BaseModel

Result of a differential-pair solve.

Source code in src/lineforge/results.py
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
class DiffResult(BaseModel):
    """Result of a differential-pair solve."""

    model_config = ConfigDict(extra="forbid")

    z_odd: float = Field(..., description="Odd-mode characteristic impedance [Ω].", gt=0)
    z_even: float = Field(..., description="Even-mode characteristic impedance [Ω].", gt=0)
    z_diff: float = Field(..., description="Differential impedance Zdiff = 2·Zodd [Ω].", gt=0)
    z_common: float = Field(..., description="Common-mode impedance Zcommon = Zeven/2 [Ω].", gt=0)
    eps_eff_odd: float = Field(..., description="εeff for the odd mode.", ge=1)
    eps_eff_even: float = Field(..., description="εeff for the even mode.", ge=1)
    vp_odd: float = Field(..., description="Odd-mode phase velocity [m/s].", gt=0)
    vp_even: float = Field(..., description="Even-mode phase velocity [m/s].", gt=0)

    method: str = Field(..., description="Which solver produced this result.")
    frequency_hz: float | None = Field(None, gt=0)
    warnings: list[SolverWarning] = Field(default_factory=list)

lineforge.results.SolverWarning

Bases: BaseModel

A non-fatal warning attached to a result.

Examples: Rs_low_confidence when conductors are too close (atlc2's red-text condition), out_of_range when an analytical formula's validity bounds are exceeded.

Source code in src/lineforge/results.py
19
20
21
22
23
24
25
26
27
28
29
30
31
class SolverWarning(BaseModel):
    """A non-fatal warning attached to a result.

    Examples: ``Rs_low_confidence`` when conductors are too close (atlc2's
    red-text condition), ``out_of_range`` when an analytical formula's
    validity bounds are exceeded.
    """

    model_config = ConfigDict(extra="forbid")

    code: str = Field(..., description="Stable machine-readable warning code.")
    message: str = Field(..., description="Human-readable explanation.")
    severity: Literal["info", "warning", "error"] = "warning"