stage_interface.py — explained

circuitgenome/sizer/gmid/stage_interface.py — the level the second stage forces onto the first-stage output node, versus the window the cascode load needs it to stay inside.

Part 1 · Why this file even exists

The one job: after every transistor is sized, does the DC level the second stage forces onto the first-stage output node actually land inside the window the cascode load needs — and if not, can we quietly nudge it back in before wasting a SPICE run?

Here is the whole story with no code at all.

The gm/Id sizer plans one device at a time. It hands each transistor a current, reads its lookup table at a fixed Vds, and sizes the width to hit a target gm/Id. That works beautifully — per device. But two devices meet at the first-stage output node, and nobody ever compares what they each want that one node to be.

Picture a single shared shelf between two neighbours. The neighbour on the right (the second stage's input device) nails the shelf to one height — its gate is that node, so the node is pinned at the second stage's own |VGS| above its source rail. The neighbour on the left (the cascode load) doesn't care about a single height; it needs the shelf to sit inside a window: high enough that its NMOS output leg stays saturated, low enough that its PMOS output leg stays saturated. Per-device planning nails the shelf and draws the window — but never checks the nail is inside the window.

When the pinned level falls outside, the load's output cascode drops out of saturation into triode. Its output resistance collapses, the gain evaporates, and — because per-device planning saw nothing wrong — the candidate comes back bias_feasible = True and only dies later at the SPICE .op gate. That is issue #124: every telescopic-load candidate sailing through planning and face-planting at the bench.

good — pin inside window upper window lower pin — output leg saturated ✓ bad — pin below window upper lower pin — output cascode in triode ✗ too low
Same shelf, same window. On the right the second stage pins the node below the load's lower wall — the output cascode has nowhere to stand and slips into triode.

So this file is a referee that runs after sizing. It computes both numbers — the pinned level and the window — detects the mismatch, tries to close the gap with the plan's real degrees of freedom, and only then reports an honest verdict (bias_feasible) so the SPICE gate is never asked to judge a candidate that provably cannot pass.

The cast of characters

Thirteen small functions, but only a handful carry weight. Hover any box to trace who it calls.

check_stage_interface() _check_fd_interface() the equality variant (FD) _pin_device() who nails the node? _stack_bound() the window's two walls _mirror_group_devs() which devices to move _resize_at() re-size at a gm/Id _pin_level() where is it pinned? _diode_level() recover mirror bias _snap_w() · _rail_v() grid & rail primitives
The orchestrator on top; the two number-makers (_pin_level, _stack_bound) in the middle; the repair helpers and leaves below. We learn it bottom-up so the top reads easily.

Looking ahead: before any code, Part 2 builds the three ideas the whole file rests on — the pin, the window, and the one inequality that ties them together — as a single picture.

Part 2 · The pin, the window, and the one rule

Three ideas. Each is simpler than its name.

The pin — a single height the second stage forces

The second stage's input transistor has its gate tied to the first-stage output node. A transistor's gate sits a fixed |VGS| away from its source. So once that device is sized, the node has no freedom left — it is pinned. For a common-source NMOS second stage whose source is the bottom rail:

pin = Vss + |VGS,ss|

With Vss = 0 and |VGS,ss| = 0.55 V, the node is nailed at 0.55 V. (A follower pins it differently — Part 4 covers both.)

The window — a range the cascode load can tolerate

The load doesn't want a single height; it wants a range. Its NMOS output leg (input pair + cascode, stacked up from the bottom rail) needs the node to sit above some level to stay saturated — that is the lower wall. Its PMOS output leg (mirror + cascode, stacked down from the top rail) needs the node below some level — the upper wall. Together:

lower ≤ node ≤ upper

The rule — the pin must land inside the window

Per-device planning computes each of these three numbers in isolation and never lines them up. The entire file exists to check the one inequality nobody else checks:

lower ≤ pin ≤ upper
↑ Vdd ↓ Vss saturation window upper = 0.90 V — PMOS stack lower = 0.60 V — NMOS stack pin = 0.55 V — 2nd stage short by 50 mV
The pin lands 50 mV under the lower wall — outside the window. The load's NMOS output cascode is starved of its Vdsat and triodes. This is the bug, drawn to scale.

The one knob that saves us: gm/Id

Just as in bias.py, raising a device's gm/Id pushes it toward weak inversion, shrinking both its |VGS| and its Vdsat. Every wall of the window is built from those two voltages, so the knob moves the walls:

  • Weaken the load's NMOS mirror group → smaller diode VGS and Vdsat → the lower wall drops, opening room below the pin.
  • Weaken the load's PMOS mirror group → the upper wall rises.
  • Weaken the second-stage device → its |VGS| shrinks → the pin itself moves (down, for a common-source stage).

And the loophole is the same one bias.py exploits: mirror devices carry no gm requirement at all, so re-sizing a whole mirror group toward weak inversion is always spec-safe. A second-stage device may only move while gm/Id · Id still clears its gm minimum — which, as we'll see, sometimes means it can't move the helpful way.

Part 3 · _snap_w() & _rail_v() — the two leaves

The one job: two tiny primitives — snap a width to the manufacturing grid, and turn a rail's name into its voltage.

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))

def _rail_v(net: str, spec: SizingSpec) -> float:
    return spec.vdd if net == "vdd!" else spec.vss

_snap_w is the lumber-yard rule: you can order boards in whole grid steps only. Ask for 2.34 µm on a 0.1 µm grid and you get round(2.34/0.1)·0.1 = 2.3 µm, then it's clamped between min and max. Every width the repair invents passes through it, so nothing unbuildable escapes.

_rail_v is even smaller: the netlist calls the top rail "vdd!" and everything else is the bottom rail. This one line is the anchor every voltage calculation eventually bottoms out on — the base case of the recursion in Part 5 and the rail exit in Part 6 both call it.

Part 4 · _pin_level() — where the node gets nailed

The one job: given the already-sized second-stage input device, compute the single DC level it forces onto its gate — the stage-interface node.

First, which device does the nailing? _pin_device() just walks a priority list of slots and returns the first stage's signal device it finds:

def _pin_device(blocks: OpAmpBlocks):
    for slot in ("second_stage", "second_stage_p", "second_stage_n",
                 "third_stage", "third_stage_p", "third_stage_n"):
        b = blocks.blocks.get(slot)
        sig = b.signal_device if b else None
        if sig is not None:
            return sig
    return None

Then _pin_level() reads the level off that device's terminals:

def _pin_level(dev, s: TransistorSizing, spec: SizingSpec) -> float | None:
    vgs = abs(s.vgs_v)
    src, drn = dev.terminals.get("s"), dev.terminals.get("d")
    if src in RAILS:  # common-source: gate = source rail ± V_GS
        return _rail_v(src, spec) + (vgs if dev.type == "nmos" else -vgs)
    if drn in RAILS:  # follower: gate = quiescent output ± V_GS
        vout_q = (spec.vdd + spec.vss) / 2.0
        return vout_q + (vgs if dev.type == "nmos" else -vgs)
    return None

Two shapes, one idea — the gate sits one |VGS| from whichever terminal is fixed.

Second-stage kindWhat's fixedPinned node
Common-sourcesource is a railrail ± |VGS|
Followerdrain is a railVout,q ± |VGS|

The ± is just the transistor flavour: an NMOS gate sits above its source (+), a PMOS gate below ().

Traced with real numbers

Our second stage is a common-source NMOS with |VGS| = 0.55 V and its source on the bottom rail (Vss = 0):

src = "vss!"  ->  in RAILS
pin = _rail_v("vss!") + vgs   =   0 + 0.55   =   0.55 V
Vdd 2nd-stage pin node (gate) Vss = 0 source on rail |V_GS| = 0.55 V pin = 0.55 V
Source welded to the bottom rail, gate exactly one V_GS above it — the node has no freedom left. It is 0.55 V.

Returning None when neither terminal is a rail is the honest escape hatch: if this device isn't a recognizable common-source or follower, the file declines to guess and the whole check bows out feasible.

Part 5 · _diode_level() — recovering the mirror's bias

The one job: find the DC level of a net when a chain of diode-connected devices ties it to a supply rail — by climbing the chain one diode drop at a time.

Why do we need this? The load's cascode gates aren't nailed to a rail directly — they're set by a mirror's diode branch. To know the window's walls in Part 6 we first have to recover those hidden bias voltages, and a diode chain is a staircase: you can't know the level of one step without first knowing the step below it. That's recursion.

def _diode_level(net, diode_by_net, sizing, spec, _depth=0) -> float | None:
    if net is None or _depth > 8:
        return None
    if net in RAILS:
        return _rail_v(net, spec)          # base case: a rail is a known voltage
    d = diode_by_net.get(net)
    s = sizing.get(d.ref) if d is not None else None
    if s is None:
        return None
    base = _diode_level(d.terminals.get("s"), diode_by_net, sizing, spec, _depth + 1)
    if base is None:
        return None
    return base + abs(s.vgs_v) if d.type == "nmos" else base - abs(s.vgs_v)

Read the last line as the physics: a diode-connected device's gate sits one |VGS| off its source. NMOS climbs up from the rail (+); PMOS hangs down from it (). The _depth > 8 guard is a seatbelt against a malformed loop.

Traced by hand — a single PMOS diode off Vdd

Our load's cascode gate is set by one PMOS diode with |VGS| = 0.50 V hanging off Vdd = 1.2:

_diode_level("mbias") → base − 0.50 recurse on source net _diode_level("vdd!") → 1.20 (base) returns 1.20 mbias = 1.20 − 0.50 = 0.70 V PMOS diode → subtract one |V_GS|
The rail is the base case (1.20 V); the value climbs back up, losing one V_GS per PMOS diode, to give the cascode's gate bias of 0.70 V.

That 0.70 V is exactly the g_lvl Part 6 needs to place the PMOS wall. Why is it called base? Because it's the level of the step directly below — the foundation this diode stacks its own VGS onto. A staircase you must build from the ground up. I like that naming.

Part 6 · _stack_bound() — building the two walls

The one job: starting at the stage-interface net, walk a series stack of same-type devices toward its anchor, adding up each device's Vdsat floor, and return the voltage that stack demands of the node.

This is the heart of the file. Call it once for "nmos" and you get the lower wall; call it for "pmos" and you get the upper wall.

def _stack_bound(net, dtype, devs, pair_refs, sizing, spec) -> float | None:
    sgn = 1.0 if dtype == "nmos" else -1.0
    vcm = (spec.vdd + spec.vss) / 2.0
    # index devices of this type by their drain net; note the diode-tied ones
    by_drain, diode_by_net = {}, {}
    for d in devs:
        if d.type != dtype: continue
        by_drain.setdefault(d.terminals.get("d"), d)
        if d.terminals.get("g") == d.terminals.get("d"):
            diode_by_net.setdefault(d.terminals.get("d"), d)
    node, floor, seen = net, 0.0, set()
    while node not in seen:
        seen.add(node)
        dev = by_drain.get(node)
        s = sizing.get(dev.ref) if dev is not None else None
        if s is None: return None
        floor += abs(s.vds_sat_v)                       # stack one more Vdsat
        if dev.ref in pair_refs:                        # anchor A: the input pair
            return vcm - sgn * abs(s.vgs_v) + sgn * floor
        gate = dev.terminals.get("g")
        g_lvl = (_diode_level(gate, diode_by_net, sizing, spec)
                 if gate != node else None)
        if g_lvl is not None:                          # anchor B: a mirror cascode
            return g_lvl - sgn * abs(s.vgs_v) + sgn * floor
        src = dev.terminals.get("s")
        if src in RAILS:                             # anchor C: a supply rail
            return _rail_v(src, spec) + sgn * floor
        node = src                                    # else hop down one device
    return None

It's a linked-list walk over circuit nets, exactly like bias.py's stack-to-rail — each device's source is the next device's drain. But this walk carries a running total, floor, and stops at the first anchor whose absolute level it can pin down. Three kinds of anchor:

AnchorLevel known fromFormula returned
A · input pairfeedback holds gates at Vcm, source at Vcm ∓ |VGS|Vcm − sgn·|VGS| + sgn·floor
B · mirror cascodegate level from _diode_level (Part 5)g_lvl − sgn·|VGS| + sgn·floor
C · supply rail_rail_v (Part 3)rail + sgn·floor

Why is it called floor?

Every device the walk crosses stacks one more minimum voltage under the node — like stacking books to raise a shelf. floor is the minimum voltage floor built up beneath this node so far. The sgn flips the whole construction for PMOS: the NMOS stack builds a floor upward from below (a lower bound), the PMOS stack hangs a ceiling downward from above (an upper bound). One function, mirrored by a single ±1. I really like this.

Both walls, traced by hand

Our telescopic load. Vcm = 0.6. Walk the NMOS side (sgn = +1) from the node down to the input-pair anchor, and the PMOS side (sgn = −1) up to the diode-biased cascode from Part 5.

net_mid (the node) Vdd = 1.2 PMOS mirror PMOS cascode gate @ 0.70 V − Vdsat 0.20 upper = 0.90 V NMOS cascode input pair (anchor) + Vdsat 0.20 + Vdsat 0.20 lower = 0.60 V Vss = 0
Upward from the node, two PMOS Vdsats hang below the 0.70 V bias to fix the upper wall at 0.90 V; downward, two NMOS Vdsats stack above the pair's 0.20 V source to fix the lower wall at 0.60 V.

The arithmetic, both anchors:

NMOS  (anchor A, sgn=+1):  0.6 − 0.40 + (0.20 + 0.20)  =  0.60 V   →  lower
PMOS  (anchor B, sgn=−1):  0.70 + 0.40 − 0.20          =  0.90 V   →  upper

For the NMOS wall the pair's |VGS| = 0.40 puts its source at 0.6 − 0.40 = 0.20 V, then two 0.20 V floors stack on top. For the PMOS wall the cascode's gate sits at 0.70 V (Part 5), its source one |VGS| = 0.40 above at 1.10 V, and its drain (our node) can rise to at most 1.10 − 0.20 = 0.90 V before it triodes.

Window = [0.60 V, 0.90 V]. And from Part 4, pin = 0.55 V — 50 mV below the lower wall.

Part 7 · The repair knobs — _resize_at() & _mirror_group_devs()

The one job: two helpers the repair leans on — re-size a single device at a new gm/Id (keeping its current), and select the mirror devices that are safe to move together.

def _resize_at(model, tech, dev, s, gm_id) -> TransistorSizing | None:
    idw = model.lut.id_per_w(dev.type, gm_id, s.l_um)
    if idw <= 0: return None
    w = _snap_w(tech, abs(s.ids_a) / idw)          # hold current, change width
    return TransistorSizing(ref=dev.ref, w_um=w, l_um=s.l_um, ids_a=s.ids_a,
        vgs_v=model.vgs(dev.type, w, s.l_um, s.ids_a),
        vds_sat_v=model.vds_sat(dev.type, w, s.l_um, s.ids_a))

The current ids_a is held fixed (Kirchhoff already decided it); only the width moves to hit the new gm/Id, and the resulting VGS and Vdsat are re-read. This is the single move that turns the gm/Id knob.

def _mirror_group_devs(load_devs, dtype) -> list:
    groups = {}
    for d in load_devs:
        gate = d.terminals.get("g")
        if d.type == dtype and gate:
            groups.setdefault(gate, []).append(d)
    return [d for gate, devs in groups.items()
            if any(m.terminals.get("d") == gate for m in devs)
            for d in devs]

It buckets load devices by shared gate, then keeps only the buckets that contain a diode member (some device whose drain is that gate). That test — "does a diode set this gate?" — is what identifies a real current mirror, whose whole group can be re-sized at one gm/Id because they share a gate and W ∝ I preserves every mirror ratio. Move the whole group together, and the mirror stays a mirror.

shared gate net diode member drain = gate copy copy one gm/Id for the whole group → ratios preserved, no gm spec to break
A group counts as a mirror only if a diode member sets its gate. That's the spec-safe knob — the load carries no gm requirement.

Part 8 · check_stage_interface() — the referee

The one job: compute the window and the pin, and if the pin is outside, search the two knobs — mirror group and second-stage gm/Id — for an assignment that lands it inside; otherwise return an honest bias_feasible = False. Never mutate the caller's sizing.

Beat 1 — bail out unless this is a cascode-load SE case

if not isinstance(model, GmIdModel):        return sizing, [], True
if blocks.is_fully_differential:
    return _check_fd_interface(...)          # Part 9
if not blocks.has_cascode_load():           return sizing, [], True
net_mid = blocks.first_stage_out_net()
ss_dev  = _pin_device(blocks)
ld, ip  = blocks.load, blocks.input_pair
s_ss = sizing.get(ss_dev.ref) if ss_dev is not None else None
if not (net_mid and ld and ld.mosfets and s_ss): return sizing, [], True

Every guard returns the input sizing untouched and True. Non-cascode loads have enough headroom that the window never binds, so the check declines. "If I can't help, I do no harm."

Beat 2 — compute the window, the pin, and the slack

pair_refs  = {d.ref for d in ip.mosfets} if ip else set()
stack_devs = ld.mosfets + (ip.mosfets if ip else [])

def window(szg):
    return (_stack_bound(net_mid, "nmos", stack_devs, pair_refs, szg, spec),
            _stack_bound(net_mid, "pmos", stack_devs, pair_refs, szg, spec))

def slack(pin, lo, hi):
    return min(pin - lo if lo is not None else float("inf"),
               hi - pin if hi is not None else float("inf"))

lo, hi = window(sizing)
pin = _pin_level(ss_dev, s_ss, spec)
if pin is None or slack(pin, lo, hi) >= _MARGIN_V:
    return sizing, [], True          # already comfortably inside

slack is the punchline in one line: the distance from the pin to the nearer wall. Positive and roomy → done. Our numbers: slack(0.55, 0.60, 0.90) = min(−0.05, 0.35) = −0.05. Negative — the pin is outside. Repair time.

Beat 3 — pick which mirror side to move, then search

mirror_devs = []
if lo is not None and pin < lo + _MARGIN_V:
    mirror_devs += _mirror_group_devs(ld.mosfets, "nmos")   # lower the floor
if hi is not None and pin > hi - _MARGIN_V:
    mirror_devs += _mirror_group_devs(ld.mosfets, "pmos")   # raise the ceiling
gm_req_ss = gm_req_map.get(ss_dev.ref, 0.0)
ss_cands  = [None] + [g for g in axis if g * abs(s_ss.ids_a) >= gm_req_ss]

The violated side chooses the knob: our pin is below the lower wall, so only the NMOS mirror group is enrolled. The second-stage candidate list starts with None (leave it) and adds only gm/Id points that still clear the stage's gm minimum — the g · Id >= gm_req filter. That filter is exactly why a common-source stage sized at gm_req/Id can't raise its pin: raising the pin needs a bigger |VGS|, i.e. a lower gm/Id, which the filter forbids. So here, the pin can't move up — the lower wall must come down.

best, best_slack = sizing, slack(pin, lo, hi)
for gm_m in [None] + (axis if mirror_devs else []):
    base = ...resize mirror_devs at gm_m...
    for gm_s in ss_cands:
        cand = ...resize ss_dev at gm_s (or base)...
        lo2, hi2 = window(cand)
        pin2 = _pin_level(ss_dev, cand[ss_dev.ref], spec)
        sl = slack(pin2, lo2, hi2)
        if sl >= _MARGIN_V: return cand, [], True   # first fit = least deviation
        if sl > best_slack: best, best_slack = cand, sl

Beat 4 — the verdict, traced by hand

Re-sizing the NMOS mirror group toward weak inversion shrinks its diode VGS and Vdsat, so _stack_bound returns a lower floor. Say the group's Vdsats drop enough to pull the lower wall from 0.60 to 0.50 V:

start: pin 0.55 < lower 0.60 slack −50 mV — outside window pin can't rise (gm min), so… weaken NMOS mirror group lower 0.60 → 0.50 V pin 0.55 inside [0.50, 0.90], slack +50 mV ✓ mirror carries no gm req → re-sizing the group is spec-safe first fit wins = least deviation from the plan slack ≥ _MARGIN_V (0.05) → feasible, new sizing returned
The pin is stuck (a common-source stage at its gm minimum can't rise), so the spec-safe mirror knob drops the lower wall until the node sits back inside the window.

Two honest fallbacks close the function. If no assignment clears the margin but the best one still clears the raw bounds (best_slack >= 0), it keeps that closest sizing and lets the SPICE gate ground the verdict. Only when even the raw bounds can't be cleared does it return False with a warning that names which wall was hit and why:

parts = []
if lo is not None and pin < lo: parts.append(f"≥ {lo:.2f} V (its NMOS output-leg stack)")
if hi is not None and pin > hi: parts.append(f"≤ {hi:.2f} V (its PMOS output-leg stack)")
warning = (f"stage interface cannot bias: the second-stage input pins the "
           f"first-stage output at {pin:.2f} V but the load needs it "
           + " and ".join(parts) + " — … the load's output leg will triode …")
return sizing, [warning], False

Part 9 · The fully-differential path — a window that collapses to a point

The one job: when the amplifier is fully differential and a CMFB pins the interface, the constraint is not a window but an equality — the second stage's |VGS| must match the servoed common-mode level, or the open-loop output rails.

_fd_interface_target() decides whether an equality even applies. A common-mode feedback loop that senses the interface nets holds their CM at mid-rail; that is the only family where the level is genuinely pinned:

def _fd_interface_target(blocks, spec, iface_nets) -> float | None:
    cmfb = blocks.blocks.get("cmfb")
    if cmfb is None: return None
    touched = set()
    for d in cmfb.mosfets: touched.update(d.terminals.values())
    for r in cmfb.resistors: touched.update(r.terminals.values())
    if touched & iface_nets: return (spec.vdd + spec.vss) / 2.0
    return None

Returning None exempts every other family — resistor loads self-bias on the load line, a CMFB sensing the second-stage outputs closes the loop around this node itself, and a mirror/diode load without CMFB leaves its high-impedance side floating to absorb the mismatch. Forcing an equality on those would over-constrain a circuit that SPICE proves is fine (tracked in issue #162).

When the target does apply, _check_fd_interface() compares it to the pin and — if they differ by more than _MARGIN_V — scans the second-stage gm/Id with both sides moved together, keeping the pair symmetric:

pin = _pin_level(ss[0], s0, spec)
if pin is None or abs(pin - target) <= _MARGIN_V:
    return sizing, [], True
# else: scan gm/Id (both sides) for the closest match; within margin → fit,
# within 2×margin → keep best & let SPICE decide, beyond → feasible = False
target = 0.60 V (CMFB mid-rail) the window is a single point pin must move both sides to match
Fully differential with an interface-sensing CMFB: there is no tolerance band. The pin must land on the target, or the open-loop second-stage output CM rails.

Part 10 · The hidden inequality, limits, and the one line

The file never writes it as a single formula, but every branch is testing one physical statement — the pinned interface node must sit inside the load's saturation window:

nmos Vdsat (Vrail± |VGS,ss|) Vdd pmos Vdsat

that is, lower ≤ pin ≤ upper.

Read it aloud: "the level the second stage forces onto the node must stay above the NMOS output-leg stack and below the PMOS output-leg stack, so neither cascode triodes." The SE path checks both walls; the FD path collapses the two walls onto one target and checks equality. _stack_bound builds each wall as a rail/anchor plus a sum of Vdsat floors — the entire cascode-headroom theory compressed into forty lines.

What it honestly does not do

  • Only the GmIdModel path is checked — any other model returns feasible = True untouched.
  • Single-ended: cascode loads only. Non-cascode SE loads have enough headroom that the window never binds, so the check declines.
  • Fully differential: only families with an interface-sensing CMFB get the equality. Resistor loads, second-stage-sensing CMFBs, and unregulated mirror/diode loads are exempt — an equality would over-constrain them (issue #162).
  • It repairs only with the plan's real knobs (mirror group, second-stage gm/Id). A common-source stage at its gm minimum simply cannot raise its pin, and the file says so honestly rather than faking a fit.
  • When even the raw bounds can't be cleared, it warns and rejects before the SPICE run — but when a candidate merely clears the raw bounds, the SPICE .op gate stays the ground truth.
  • It never mutates the caller's sizing; a repair always comes back as a new mapping.

Why it's elegant

The whole check rides one insight per side: a series stack of transistors demands exactly anchor ± Σ Vdsat of the node it hangs from, and the same gm/Id knob that bias.py used for headroom moves those walls here too — spec-safely, because mirror devices carry no gm requirement. One function with a single ±1 builds both walls; one slack line renders the verdict; and the repair only ever spends the degrees of freedom the plan genuinely has.


In one sentence: stage_interface.py computes the DC level the second stage pins onto the first-stage output node and the [lower, upper] saturation window the cascode load needs it inside, repairs a violation by weakening the offending mirror group (or, spec permitting, the second-stage device) toward weak inversion, and returns an honest bias_feasible verdict — catching the issue-#124 telescopic-load candidates that per-device planning waved through only to have the mirror's output cascode triode at the SPICE bench.