# hier_op — getting an operating point when `.op` will not converge

## The problem

You run `.op` on a large mixed-signal netlist. Newton fails, ngspice walks its whole
fallback ladder — dynamic gmin, true gmin, source stepping, transient op — and every rung
fails too. You have no operating point, so you cannot start.

## The idea

Do not hand the solver the whole problem at once.

Take a subset of the circuit that *does* converge, save its state, add **one** block back,
and solve again *starting from the saved state*. Each solve is then a small nudge to a
circuit that is already solved, instead of a cold solve of everything.

Repeat until every block is back in. The last state is your operating point.

The goal is **a** starting point, at any cost — not the perfect one. Today's alternative is
nothing at all.

## Usage

```
ngspice-hier-op chip.cir
```

or from inside ngspice, or a `.control` block:

```
.control
  op
  * ... does not converge ...
  hier_op chip.cir
.endc
```

Requires `python3` (3.6 or newer — nothing beyond f-strings is used).

**Your netlist is never modified.** Blocks are removed with the `disable` command, in the
copy ngspice holds in memory. The seed and the analysis are passed in a generated file
alongside your deck. Everything written goes to `./hier_op/`.

## Options

| option | default | what it does |
|---|---|---|
| `--list` | flag | show the fold order and exit, running nothing |
| `--t0 SECONDS` | `50n` | length of the first rung **b** transient. Raise it for clocked blocks, which need real time to settle |
| `--doublings N` | `4` | how many times rung **b** doubles the transient before giving up (50 n → 100 n → 200 n → 400 n) |
| `--outdir DIR` | `hier_op` | where states, seeds, logs and `.json` records are written |
| `--min-devices N` | `300` | ignore top-level instances smaller than this. Below it the segmentation is too fine to be worth a step, and removing small glue only creates floating nodes |
| `--rshunt OHMS` | `1e9` | resistance from every node to ground, added to each run. `0` disables it |
| `--ngspice PATH` | `ngspice` | which ngspice binary the tool drives |

Every generated run also carries `.option rshunt` and `.option pstran`. Neither is a tuning
knob here. Removing a block leaves the nets it drove with nothing holding them, so they go
singular and come back as `nan`; `rshunt` ties every node to ground through a large
resistance and is structural to the method (see *Two things worth knowing* below).
`pstran` adds a pseudo-transient rung below ngspice's built-in ladder, giving a seeded
`.op` one more way to land before the run falls through to rung **b**.

## A worked example

Say `chip.cir` has five blocks above the threshold. First, look at what it will do — this runs nothing:

```
$ ngspice-hier-op chip.cir --list
fold candidates (>= 300 devices), smallest-first:
       480  xctrl
      1900  xbias
      4700  xio
     12000  xseq
     15000  xafe
```

Blocks are folded smallest-first, so each step perturbs the solved circuit as little as
possible, and the expensive blocks are added last when the state is already good. Anything
below the threshold (default 300 devices) is left in permanently: below that the segmentation
is too fine to be worth a step, and removing small glue only creates floating nodes.
Raise or lower it with `--min-devices`.

Now run it:

```
$ ngspice-hier-op chip.cir
deck   : /home/you/chip.cir  (never modified)
blocks : 5 to fold, smallest-first: xctrl, xbias, xio, xseq, xafe

--- base: all fold candidates disabled, no seed
    [a] OK    854.0s  converged
   -> base state [op]

--- [1/5] fold xctrl
   seed: 9840 nodes (8 of xctrl's port nets dropped)
    [a] FAIL  270.3s  simulation(s) aborted
    [b: tran uic 50n] OK    56.7s  converged
    [b: .op after 50n] OK  148.9s  converged
   -> state [op] via rung b@50n

--- [2/5] fold xbias
   ...
```

Read the first block like this: a plain `.op` from the saved state failed, so it ran a
50 ns transient from that state, and an `.op` seeded from the transient's endpoint
converged. That is the escalation ladder:

| rung | what it does | seeded with |
|---|---|---|
| **a** | `.op` | previous state as `.nodeset` |
| **b** | `.tran … uic` for 50 ns, then `.op` from its endpoint | previous state as `.ic` |
|  | if that `.op` fails: double the time and retry, up to 4 times (50 n → 100 n → 200 n → 400 n) | |
| **c** | ladder exhausted → hand back the best transient endpoint anyway | |

`tran uic` is used because it is **robust** — it always produces a state. It is not a
functional check; nothing here looks at what your circuit does.

50 ns is the starting point because a non-clocked block settles in about that or never.
Clocked blocks need real time, and how much is something only you know — so the doubling
is a bounded search, not a theory. If your blocks are clocked, start higher:

```
ngspice-hier-op chip.cir --t0 5e-6
```

## What you get

For every step, in `./hier_op/`:

| file | what it is |
|---|---|
| `<step>.nodeset` | the state, for seeding a `.op` |
| `<step>.ic` | the same state, for seeding a `.tran … uic` |
| `<step>.json` | which rung produced it, and what each attempt cost |
| `<step>__*.log` | the ngspice run behind every attempt |

Both seed forms are always written, because **feeding the wrong one is silent**: a `.op`
ignores `.ic` completely, so the run looks seeded and is not.

## Read the label

Each state is labelled in the summary and in its `.json`:

```
=== summary ===
  folded xctrl          [op via b@50n]
  folded xbias          [op via b@100n]
  folded xio            [op via b@100n]
  folded xseq           [op via b@200n]
  folded xafe           [transient via c]
  final state : hier_op/s5_xafe.ic  /  hier_op/s5_xafe.nodeset
  still out   : none
```

- **`op`** — a converged operating point.
- **`transient`** — rung c: the ladder ran out, so you got the endpoint of the longest
  transient instead. It is a usable starting point, **not** an operating point.

That distinction is the whole point of the label. A `transient` state means that block
needs more time than the ladder allowed, and only you can say how much.

## Two things worth knowing

**Removing a block removes its drivers.** Nets that a removed block used to drive go
floating, and `rshunt` pins them near 0 V — which is a perfectly plausible logic level, so
nothing looks wrong. The run warns you:

```
   WARNING: 120 net(s) lost every driver -> rshunt pins them to ~0,
            which is a plausible logic level. Any state is conditioned on them.
      sel_b   still used by xctrl   driver left with xio
```

Here the pad driving `sel_b` was in a removed block, so the logic saw the opposite level
from the pad. It only warns — what to do is your call, and usually the answer is to add a
source in your stimulus for the intermediate steps, or to keep that block in from the start.

**Ideal current sources are suspended while their load is missing.** An ideal current source
with nowhere to go explodes. If the block that sinks it is out, it is commented out for that
step and restored automatically when the block returns — the condition is recomputed from
the netlist every step, so it cannot be forgotten.

## If it stops

If a block fails both rungs, the previous step's transient may simply have been too short.
The run goes back to the immediately preceding step, re-runs it at the longer durations, and
retries. If it still fails, it stops — one backtrack, not a search.

Even then, **every state produced up to that point is on disk and usable.**
