Sharing a Substrate Between Traces

A board has one substrate and many traces on it. If each trace carried its own copy of the permittivity, fitting the board would fit the same physical quantity several times over, once per trace, with nothing keeping the answers consistent.

ParamRF needs no special machinery for this. A pmrf.materials.Substrate groups the height, dielectric, conductor and metallization thickness into one module, and a pmrf.models.AbstractBuilder holding one substrate and injecting it into each line in build is what makes the sharing real.

One Substrate, Two Traces

import pmrf as prf
from pmrf.materials import ConstantDielectric, Substrate
from pmrf.models import AbstractBuilder, MicrostripLine

class Board(AbstractBuilder):
    substrate: Substrate
    w1: prf.Param
    w2: prf.Param

    def build(self):
        return (
            MicrostripLine(w=self.w1, substrate=self.substrate, length=0.1)
            ** MicrostripLine(w=self.w2, substrate=self.substrate, length=0.2)
        )

board = Board(
    substrate=Substrate(h=1.6e-3, dielectric=ConstantDielectric(ep_r=4.3, tand=0.02)),
    w1=1.0e-3,
    w2=2.0e-3,
)
list(board.named_params())
['substrate.h',
 'substrate.dielectric.ep_r',
 'substrate.dielectric.tand',
 'substrate.dielectric.sigma',
 'substrate.dielectric.mu_r',
 'substrate.conductor.sigma',
 'substrate.conductor.mu_r']

There is one ep_r, not two. build is lazy and uncached, so the substrate is a leaf of the builder and both lines are reconstructed on every call from the same already-traced values. Fitting board therefore fits a single permittivity, shared by construction.

The Contrast: A Shared Param Object

Handing the same pmrf.Param object to two separately-constructed lines looks like sharing, but is not:

ep_r = prf.Param(value=4.3)
lines = (
    MicrostripLine(w=1e-3, h=1.6e-3, dielectric=ConstantDielectric(ep_r=ep_r), length=0.1)
    ** MicrostripLine(w=2e-3, h=1.6e-3, dielectric=ConstantDielectric(ep_r=ep_r), length=0.2)
)
[name for name in lines.named_params() if name.endswith("ep_r")]
['cascade[0].substrate.dielectric.ep_r',
 'cascade[1].substrate.dielectric.ep_r']

PyTree flattening gives one leaf per line regardless of the object identity that went in, so the optimizer sees two independent permittivities that are free to drift apart. The builder pattern above is the one that dedupes.

Two Idioms, One PyTree

A line may be given a grouped substrate=, or the loose h, dielectric, conductor and t fields. Both build the same canonical pmrf.materials.Substrate, so the two forms are the same model:

import jax

loose = MicrostripLine(w=3e-3, h=1.6e-3, dielectric=4.3, length=0.1)
grouped = MicrostripLine(w=3e-3, substrate=Substrate(h=1.6e-3, dielectric=4.3), length=0.1)

jax.tree_util.tree_structure(loose) == jax.tree_util.tree_structure(grouped)
True

The loose form stays convenient for a single line; the grouped form is what a board shares. They may not be mixed in one call.

A coaxial line has no substrate, and keeps its dielectric and conductor fields directly.