Part 1 · Why this file even exists
The one job: after the geometry pass has sized every mirror in the bias generator by ratio, a handful of devices aren't mirrors at all — their job is to set an absolute rail voltage. This file re-sizes exactly those, and nothing else.
Here is the whole story before a single line of code.
The gm/Id sizer builds the bias generator from an intent table. Most of its transistors are current mirrors: each one copies the reference current scaled by a ratio, so its width follows a clean rule — W ∝ I. Hand the geometry pass a target current and a ratio, and it sizes the device. Done.
But a bias generator also contains level devices, whose reason for existing is to pin a node at a chosen voltage. You can't size those by a current ratio, because their spec is a voltage, not a mirror factor. Think of a tailor who sizes every suit as a scaled copy of one reference suit. That works beautifully — until someone hands him a doorframe and asks him to make it exactly 2.03 m tall. "Scaled copy of what?" There is no ratio; there is only an absolute target. This file is the specialist called in for the doorframes.
There are exactly two level structures the file knows how to spot, and it finds each one structurally — by the actual shape of device terminals, so a foreign netlist without these shapes passes straight through untouched:
- Cascode-leg level diodes — a diode-connected device riding a resistor to a supply. The rail it produces must sit at a downstream cascode's
V_GSplus a small saturation floor. This is the bulk of the file, and the whole of Parts 2 and 5. - The pref branch's wide-swing cascode level (
ncasc) — a mirror-drain cascode topped by a diode, tuned so the mirror'sVdssits as close to the master reference's as headroom allows. Part 6.
The cast of characters
Eight small functions. The two _tune_* routines do the real work; the rest are leaves they lean on. Hover any box to trace who it calls.
One more fact about where this runs: it fires after the geometry pass and the DC operating-point check, and its resistor values override the generic fallback that size_resistors assigns to nets it can't derive a level for. It is the last, most-informed word on these specific nets.
Looking ahead: Part 2 builds the one picture the whole file is drawing — a rail voltage split into a
V_GSpart carried by a diode and a small floor part carried by a resistor.
Part 2 · What a rail "level" actually is (one picture)
Everything the cascode-leg tuner does serves one target voltage. Let's build it.
The demand: a downstream cascode needs its gate at a certain height
Somewhere out in the op-amp, a cascode transistor sits on top of a device. For it to work, its gate must be held at a specific voltage: enough to give the cascode its own V_GS, plus enough to keep the device stacked beneath it in saturation. That second piece — the minimum voltage the stack underneath needs — is the saturation floor. So the rail that feeds this gate must sit at:
This is exactly the number _bias_rail_target_v() returns — the mean voltage the rail's consumers demand. Call it target_abs.
The supply: build that height out of two stacked pieces
Now, how do you make a rail sit at, say, 0.75 V above ground? The cascode leg does it with a two-storey stack between the supply and the rail:
- a resistor sitting on the supply, dropping the small floor across the leg current, and
- a diode-connected transistor on top of it, dropping its own
V_GS— and the diode's drain is the rail.
The genius is in the pairing: the diode supplies the big V_GS part, and the resistor supplies the small floor part. Add them and you land on the target.
The one knob on the diode: gm/Id
How do we set the diode's V_GS to whatever value we want? By choosing its width — through the gm/Id lookup table. A narrow diode runs in strong inversion with a large V_GS; a wide diode runs in weak inversion with a small V_GS. Sweep the width and you sweep V_GS.
The reveal, planted early: the leg's diode is the same transistor type as the consumer cascode it feeds. So when we size the diode to reproduce the consumer's
V_GS, the rail automatically tracks Vth — over process and temperature both voltages drift together. We'll cash this out in Part 8.
Part 3 · _snap_w() — the warm-up
The one job: round a computed width to the nearest value the manufacturing grid allows, then clamp it into the legal range.
def _snap_w(tech: TechParams, w_um: float) -> float:
g = tech.width
return float(min(max(round(w_um / g.step) * g.step, g.min), g.max))
Analogy: you can order lumber only in whole inches. Ask for 23.4 inches and the yard hands you 23. That is all this does — snap to the grid, then refuse anything below min or above max.
Traced with real numbers
Say step = 0.1 µm, min = 0.5, max = 100, and the tuner computed w_um = 4.27:
round(4.27 / 0.1) * 0.1 = round(42.7) * 0.1 = 43 * 0.1 = 4.3 µm
max(4.3, 0.5) = 4.3 min(4.3, 100) = 4.3 -> 4.3 µm
Every width the two tuners invent is passed through this, so whatever V_GS they aim for, the device they actually place is manufacturable. Keep it in your pocket — it shows up inside both tuners.
Part 4 · Picking a diode width — _diode_candidates(), _size_diode_for_vgs(), _sized()
The one job: given a target
V_GS, find the diode width whose|V_GS|lands closest to it — optionally never exceeding a cap.
This is the shared engine both tuners call to turn a voltage wish into a width. It comes in two small steps: enumerate the candidates, then pick the closest.
Step 1 — enumerate one width per LUT point
def _diode_candidates(model, tech, dtype, l_um, ids_a) -> list:
out = []
for gm_id in model.lut.gm_id_axis:
idw = model.lut.id_per_w(dtype, float(gm_id), l_um) # current density
if idw <= 0: continue
w = _snap_w(tech, abs(ids_a) / idw) # width to carry this current
out.append((w, abs(model.vgs(dtype, w, l_um, ids_a))))
return out
For each gm/Id point in the table, it asks "at this operating point, how much current does a 1 µm-wide device carry?" (id_per_w), then back-solves the width that carries our fixed current ids_a, snaps it to the grid, and records the pair (width, |V_GS|). The axis runs weak-to-strong, so the list runs from small |V_GS| to large.
Step 2 — pick the width closest to the target
def _size_diode_for_vgs(model, tech, dtype, l_um, ids_a, target_vgs,
cap_vgs=None) -> float | None:
cands = _diode_candidates(model, tech, dtype, l_um, ids_a)
if cap_vgs is not None:
cands = [c for c in cands if c[1] <= cap_vgs] or []
if not cands: return None
return min(cands, key=lambda c: abs(c[1] - target_vgs))[0]
Analogy: you want a paint shade of "0.65". You're handed a fan deck of chips, each a real available shade. You throw out any chip darker than your ceiling (cap_vgs), then pick the remaining chip closest to 0.65. If the cap threw out every chip, you return None and the caller leaves the device alone.
And _sized() — the little constructor
def _sized(model, ref, dtype, w_um, l_um, ids_a) -> TransistorSizing:
return TransistorSizing(ref=ref, w_um=w_um, l_um=l_um, ids_a=ids_a,
vgs_v=model.vgs(dtype, w_um, l_um, ids_a),
vds_sat_v=model.vds_sat(dtype, w_um, l_um, ids_a))
Once a width is chosen, this packages it back into a full TransistorSizing record — re-reading the model for the actual V_GS and Vdsat that width produces. That "actual, after snapping" V_GS is what the tuner reads next; it won't be exactly the target, and the code is honest about that.
Looking ahead: now we have the engine. Part 5 wires it into the cascode-leg tuner and traces a full rail voltage — diode plus floor — with real numbers.
Part 5 · _tune_cascode_legs() — the heart
The one job: for each cascode leg, re-size its diode so
V_GSmatches what the leg's consumers actually need, then set the floor resistor to make up the remaining rail voltage.
This is the most important function in the file. Take it in four beats.
Beat 1 — find the leg, structurally
for r in bg.resistors:
t1, t2 = r.terminals.get("t1"), r.terminals.get("t2")
supply = next((t for t in (t1, t2) if t in RAILS), None)
mid = next((t for t in (t1, t2) if t and t not in RAILS), None)
if supply is None or mid is None: continue
diode = next((d for d in bg.mosfets
if d.terminals.get("s") == mid
and d.terminals.get("d") == d.terminals.get("g")), None)
if diode is None: continue # plain rail resistor — size_resistors owns it
rail = diode.terminals["d"]
A leg is recognized by its shape: a resistor with one end on a supply and the other on an internal node mid, with a diode-connected device (drain = gate) whose source sits on that same mid. The diode's drain is the rail. No mid-riding diode? Then it's just an ordinary rail resistor, and the generic sizer keeps it.
Beat 2 — read what the consumers demand
target_abs = _bias_rail_target_v(rail, consumers, sizing, spec) # rail voltage
vgs_targets = [abs(cs.vgs_v) for dev in consumers
if dev.terminals.get("g") == rail # gated by this rail
and dev.terminals.get("d") != rail # not the diode itself
and (cs := sizing.get(dev.ref)) is not None and cs.vgs_v]
if not vgs_targets: continue
target_vgs = sum(vgs_targets) / len(vgs_targets) # mean consumer |Vgs|
i_leg = abs(ids_map.get(diode.ref, spec.ibias)) or spec.ibias
Two demands come out of the consumers. target_abs is the absolute rail voltage they need (V_GS + floor, from Part 2). target_vgs is the mean planned V_GS of the actual consumer cascodes gated by this rail — this is the number the diode will be re-sized to reproduce. If several consumers share one rail with conflicting wishes, the mean is the compromise a single leg can offer.
Beat 3 — re-size the diode to the consumers' V_GS
w = _size_diode_for_vgs(model, tech, diode.type, s.l_um, s.ids_a, target_vgs)
if w is not None:
sizing[diode.ref] = _sized(model, diode.ref, diode.type, w, s.l_um, s.ids_a)
vgs_actual = abs(sizing[diode.ref].vgs_v)
Straight into the Part 4 engine: pick the width whose V_GS is closest to target_vgs, rebuild the record, then read back the actual V_GS the grid gave us. This is the line that makes the rail track Vth — the diode now carries the same V_GS as the consumer, and it's the same device type.
Beat 4 — the resistor makes up the difference
span = (target_abs - spec.vss) if supply != "vdd!" \
else (spec.vdd - target_abs)
floor = max(span - vgs_actual, 0.0)
out_r[r.ref] = floor / i_leg if floor > 0 else 1.0
The diode already covers vgs_actual of the rail. Whatever is left between the supply and the target is the floor, and a resistor drops exactly that across the leg current: R = floor / I_leg (Ohm's law, solved for R). If the diode alone already overshoots, floor clamps to 0 and the resistor becomes a nominal 1.0 Ω — a near-short, since no drop is needed.
Why is it called floor?
The consumer cascode needs its own
V_GS, plus just enough extra to keep the device stacked beneath it in saturation — the minimum voltage floor that stack sits on. That is precisely the small piece the resistor supplies. I really like this name: it says the physics out loud.
The whole leg, traced by hand
Take a cascode_gnd leg (supply = vss = 0). The consumers want V_GS = 0.65 V and their stack needs a floor, giving a rail target target_abs = 0.75 V. The leg carries I_leg = 10 µA.
| Step | Computation | Result |
|---|---|---|
target_vgs (mean consumer V_GS) | given by consumers | 0.650 V |
| diode re-sized, actual V_GS | nearest LUT chip to 0.65 | 0.646 V |
span | 0.75 − 0 (vss supply) | 0.750 V |
floor | max(0.750 − 0.646, 0) | 0.104 V |
R = floor / I_leg | 0.104 / 10 µA | 10.4 kΩ |
| rail produced | 0.646 (diode) + 0.104 (R·I) | 0.750 V |
Part 6 · _tune_pref_cascode() — the wide-swing level
The one job: in the pref branch, push the mirror's drain voltage (
Vds) as close to the master reference'sV_GSas headroom allows — by making the cascode as "short" as possible and thencascdiode as "tall" as the ceiling permits.
Why bother? A plain NMOS mirror in this branch would run at Vds = vdd − |V_GSP|, far from the master's operating point — the ~4% extra-mirror-hop error from issue #103. Cascoding the mirror lets us pin its Vds wherever we choose. We choose "as near the master as we can reach."
Beat 1 — find the chain, structurally
master = diode-connected nmos with source on a rail # names ibias net
mirror = another nmos, gate on ibias, source on a rail
cascode = nmos, source on the mirror's drain, gate NOT ibias
ncasc_diode = diode-connected nmos whose d=g = the cascode's gate
pref_diode = pmos diode on the cascode's drain
The whole chain is recovered from terminals alone: the master names the ibias net, the mirror hangs off it, the mirror's drain carries a cascode, the cascode's gate names the ncasc net, and a pref PMOS diode caps the top.
Beat 2 — make the cascode as short as possible
gm_id_weak = float(model.lut.gm_id_axis[-1]) # weakest inversion
idw = model.lut.id_per_w("nmos", gm_id_weak, s_c.l_um)
if idw > 0:
w_c = _snap_w(tech, abs(s_c.ids_a) / idw)
sizing[cascode.ref] = _sized(model, cascode.ref, "nmos", w_c, s_c.l_um, s_c.ids_a)
Push the cascode to the weak-inversion end of the table — the last axis point. That gives it the lowest V_GS and lowest Vdsat. A shorter cascode leaves more of the ceiling for the ncasc diode to push the pinned node upward. It is the same "widen the device to shrink its V_GS" trick from Part 2, dialed to the extreme.
Beat 3 — aim the ncasc diode at the master, under the ceiling
v_pref = spec.vdd - abs(s_p.vgs_v) # pref node headroom
vgs_master = abs(sizing.get(master.ref, s_m).vgs_v)
n1_max = v_pref - abs(s_c.vds_sat_v) - _MARGIN_V - spec.vss
target = vgs_master + abs(s_c.vgs_v) # ideal ncasc |Vgs|
cap = n1_max + abs(s_c.vgs_v) # ceiling on ncasc |Vgs|
w_d = _size_diode_for_vgs(model, tech, "nmos", s_d.l_um, s_d.ids_a,
target, cap_vgs=cap)
The pinned node is n1 = V_GS,ncasc − V_GS,cascode. To make n1 equal the master's V_GS, we want the ncasc diode's V_GS = vgs_master + V_GS,cascode — that's target. But n1 can't exceed n1_max (the pref node minus the cascode's own Vdsat minus a 0.05 V safety margin), so we cap the diode's V_GS at cap. Part 4's engine picks the nearest chip that respects that ceiling.
Traced by hand — the cap bites
With vdd = 1.0, vss = 0, pref PMOS |V_GS| = 0.50, master V_GS = 0.40, and the weak-inversion cascode landing at V_GS = 0.35, Vdsat = 0.10:
| Quantity | Computation | Value |
|---|---|---|
v_pref | 1.0 − 0.50 | 0.50 V |
n1_max | 0.50 − 0.10 − 0.05 − 0 | 0.35 V |
target (ideal ncasc V_GS) | 0.40 + 0.35 | 0.75 V |
cap (ceiling) | 0.35 + 0.35 | 0.70 V |
| ncasc V_GS chosen | nearest to 0.75, but ≤ 0.70 | 0.70 V |
| pinned Vds (n1) | 0.70 − 0.35 | 0.35 V |
The ideal would have pinned Vds at the master's 0.40 V, but the pref node's headroom caps it at 0.35 V — within 50 mV, a big improvement on the raw mirror hop, and honestly limited by the ceiling rather than pretending.
Part 7 · tune_bias_levels() — the orchestrator
The one job: guard the inputs, copy the sizing so nothing is mutated, run both tuners, and hand back the tuned sizing plus the legs' floor-resistor values.
def tune_bias_levels(blocks, ids_map, sizing, model, spec, tech):
if not isinstance(model, GmIdModel): return sizing, {}
bg = blocks.blocks.get("bias_gen")
if bg is None or not bg.mosfets: return sizing, {}
tuned = dict(sizing) # never mutate the caller's map
level_r: dict[str, float] = {}
consumers = [d for name, b in blocks.blocks.items()
if name != "bias_gen" for d in b.mosfets]
_tune_cascode_legs(bg, consumers, ids_map, tuned, model, spec, tech, level_r)
_tune_pref_cascode(bg, tuned, model, spec, tech)
return tuned, level_r
Read it top to bottom. Two guards bail out harmlessly — wrong model, or no bias generator — returning the original sizing and an empty resistor map. Then dict(sizing) makes the promise real: the input mapping is never touched; a fresh copy absorbs every change. The consumers list is "every device that isn't in the bias generator" — the outside world the legs must satisfy. Finally the two tuners run in place on tuned and level_r, and both come back.
That last output matters: level_r is meant to be merged over the generic values from size_resistors, which cannot derive a level for a mid net and falls back to a representative value. This file supplies the real number.
Part 8 · The hidden idea, the limits, and the one line
The idea the code never writes down
Both tuners are building the same equation — a rail voltage split into a device's V_GS plus a small, deliberate margin:
The cascode leg builds it out of atoms — a diode for the |V_GS| term, a resistor for the V_floor term. The pref branch builds the same shape out of two stacked transistors (ncasc diode minus cascode) to place a pinned node. One equation, two constructions.
The reveal: why the rail tracks Vth
Here is the payoff promised in Part 2. The leg's diode is the same device type as the consumer cascode, and it was re-sized to carry the same V_GS. So imagine temperature rises and threshold voltage Vth drifts up by 60 mV:
- The consumer's
V_GSrises by ~60 mV — it now needs a higher gate voltage. - The diode's
V_GSrises by the same ~60 mV — so the rail it produces rises by ~60 mV too. - The resistor's floor (
I·R) doesn't move — but it was never supposed to; it's a fixed margin, not the tracking part.
The rail and the demand move in lockstep. Had the leg used a fixed resistive divider for the whole rail, the rail would have stood still while the consumer's need climbed — and the cascode would drift out of its intended operating point. The diode mirroring the consumer's V_GS is the entire reason the bias is robust over process and temperature.
What it honestly does not do
- It acts only on the
GmIdModelpath — any other model returns the sizing untouched with an empty resistor map. - It touches only the two structural shapes it recognizes; a bias generator (or foreign netlist) without a diode-on-a-resistor leg or a mirror-drain cascode passes straight through.
- A shared rail with conflicting consumer demands gets only the mean — the best a single leg can offer, not a per-consumer optimum.
- The pref cascode reaches the master's
Vdsonly as far as the pref node's headroom allows; when the cap bites, it stops short rather than pretending. - Widths are snapped to the grid, so the actual
V_GSis the nearest available chip, not the exact target — and the code reads back the actual value rather than assuming.
Why it's elegant
The whole file rides one insight: a diode-connected device is a self-calibrating V_GS reference. Size it to reproduce a downstream device's V_GS and it will track that device over every corner for free — leaving only a small, fixed floor for a humble resistor to supply. Two tiny constructions, one robust idea.
In one sentence: bias_levels.py re-sizes the bias generator's level devices — the ones set by an absolute voltage rather than a mirror ratio — by making each cascode leg's diode reproduce its consumers' V_GS (so the rail tracks Vth) and a floor resistor supply the small remainder, and by pinning the pref branch's cascoded mirror as near the master reference as headroom allows.