sycan.headroom

Headroom analysis — symbolic input range that keeps every MOSFET saturated.

Given a circuit and one input axis — either a single independent source whose value is swept, or a group of sources whose values are all tied to one common scalar variable — solve_headroom() returns the symbolic interval of that variable for which every MOSFET in the circuit is in saturation.

For each MOSFET the analysis builds two saturation predicates straight from the device equations,

  • c1 = V_GS_eff - V_TH(V_SB) > 0 (above threshold, in inversion)

  • c2 = V_DS_eff - (V_GS_eff - V_TH) >= 0 (V_DS past the overdrive knee)

with the long-channel body-effect threshold for 4T cells. The DC operating point is solved with the saturation-form drain currents — I_D = (1/2) β (V_GS_eff V_TH)² (1 + λ V_DS_eff) — so cas.solve sees a polynomial system. Substituting the solved node voltages turns each predicate into an expression in the input variable (and any leftover symbolic parameters); the interval edges then come from cas.solve of each predicate against the input, never from a numeric sweep.

For circuits whose KCLs cas.solve can’t close in one shot — the canonical case is a 5T-OTA with a diode-connected current mirror — pass a pre-computed op_point= mapping. Compute it however you like (sequential elimination, hand algebra, your own solver) and the analysis will pick up from there with the predicates / boundaries.

Typical use:

from sycan import parse, solve_headroom
c = parse("""...netlist with MOSFETs and a single Vin...""")
result = solve_headroom(c, "Vin")
print(result)                          # symbolic / numeric interval
print(result.predicates["MN"])         # what MN demands of x
print(result.boundaries)               # per-device edge values

For a differential pair, the input axis is one symbol \(V_{id}\) that drives two physical sources:

V_id = cas.Symbol("V_id", real=True)
result = solve_headroom(
    c,
    sources={"Vinp": Rational(9,10) + V_id/2,
             "Vinm": Rational(9,10) - V_id/2},
    var=V_id,
)

Functions

solve_headroom(circuit, sources[, var, ...])

Symbolic headroom: input range that keeps every MOSFET in saturation.

Classes

HeadroomResult(var, node_voltages, ...)

Symbolic outcome of a solve_headroom() call.

class sycan.headroom.HeadroomResult(var, node_voltages, predicates, boundaries, interval, binding)[source]

Bases: object

Symbolic outcome of a solve_headroom() call.

Parameters:
  • var (Symbol)

  • node_voltages (dict[Symbol, Expr])

  • predicates (dict[str, list[Expr]])

  • boundaries (list[tuple[str, str, Expr]])

  • interval (tuple[Expr, Expr] | None)

  • binding (dict[str, str | None])

var

The input variable the analysis sweeps.

Type:

sympy.core.symbol.Symbol

node_voltages

Symbolic operating-point map {V(node): expr_in_var}, solved with each MOSFET pinned to its saturation drain current.

Type:

dict[sympy.core.symbol.Symbol, sympy.core.expr.Expr]

predicates

{device_name: [c1, c2]} — for each MOSFET, the two saturation predicates (must be > 0 for c1 and >= 0 for c2). Substitute concrete values to check margin.

Type:

dict[str, list[sympy.core.expr.Expr]]

boundaries

[(device, kind, var_value), ...] — every place a predicate crosses zero, in symbolic form, with the device name and which predicate ("threshold" for c1, "overdrive" for c2).

Type:

list[tuple[str, str, sympy.core.expr.Expr]]

interval

(low, high) — symbolic edges of the widest contiguous all-saturation interval, or None if no interval is consistent (e.g. a fixed bias kills one device regardless of the input).

Type:

tuple[sympy.core.expr.Expr, sympy.core.expr.Expr] | None

binding

{"low": device, "high": device} — the device that sets each interval edge.

Type:

dict[str, str | None]

var: Symbol
node_voltages: dict[Symbol, Expr]
predicates: dict[str, list[Expr]]
boundaries: list[tuple[str, str, Expr]]
interval: tuple[Expr, Expr] | None
binding: dict[str, str | None]
summary()[source]

Multi-line human-readable report.

Return type:

str

sycan.headroom.solve_headroom(circuit, sources, var=None, *, op_point=None, simplify=True)[source]

Symbolic headroom: input range that keeps every MOSFET in saturation.

Parameters:
  • circuit (Circuit) – A Circuit containing one or more MOSFETs. Sub-threshold-only devices and BJTs are ignored.

  • sources (str | Mapping[str, Expr]) – Either the name of one independent voltage / current source — in which case the source’s value is replaced by var (or by a freshly minted symbol named after the source) — or a {name: sympy_expression} mapping. Every expression in the mapping must depend on the same single var (other free symbols are taken as circuit parameters).

  • var (Symbol | None) – The swept input variable. Optional for the single-source form; for the dict form, it is auto-detected as the unique symbol common to all expressions, or pass it explicitly to override.

  • op_point (Mapping[Symbol, Expr] | None) – Optional pre-computed operating-point map {V(node): expr}. When provided, the analysis skips cas.solve of the saturation-form DC system and uses these values directly to substitute into the predicates. Useful for circuits whose KCLs cas.solve can’t close in one shot (5T-OTA with a current mirror): derive the operating point yourself with sequential elimination and feed it in. Only the node voltages your predicates actually reference need to be present.

  • simplify (bool) – Run cas.simplify on the operating-point voltages and the saturation predicates before returning them.

Returns:

See the dataclass for fields. The most useful one is interval — a (low, high) pair of sympy expressions in the swept variable’s coefficients.

Return type:

HeadroomResult