ckt: dangling-passive topology reduction

Add CKTtopologyReduce() (cktsetup.c) to remove degree-1 dangling
capacitors/resistors -- e.g. dead-end opamp compensation caps -- that
otherwise cause spurious 'Timestep too small' aborts. Node degree is
counted generically over all device types; the floating node is pinned
with a unit diagonal. Disable with 'set no_topo_reduce'.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
alto555 2026-06-18 10:50:44 -07:00 committed by Holger Vogt
parent 5e27a8f6de
commit aac1952ebe
5 changed files with 185 additions and 4 deletions

View File

@ -13,8 +13,14 @@ Author: 1985 Thomas L. Quarles
#include "ngspice/smpdefs.h"
#include "ngspice/cktdefs.h"
#include "ngspice/devdefs.h"
#include "ngspice/gendefs.h"
#include "ngspice/sperror.h"
#include "ngspice/fteext.h"
#include "ngspice/cpextern.h"
/* device headers needed by CKTtopologyReduce() to mark dangling passives */
#include "../devices/cap/capdefs.h"
#include "../devices/res/resdefs.h"
#ifdef XSPICE
#include "ngspice/enh.h"
@ -30,6 +36,131 @@ Author: 1985 Thomas L. Quarles
return(E_NOMEM);\
}
/* Topology reduction for removing dangling capacitors and resistors.
*
* A node whose only connection is a single passive terminal (a degree-1
* "dangling" node, e.g. the dead end of an opamp compensation cap) carries no
* steady current/charge, but its row becomes ill-conditioned as the timestep
* shrinks and shows up as a spurious "Timestep too small" abort. Commercial
* fast simulators simply remove such elements from the matrix. This
* pass does the same: it finds dangling passive leaves and marks the owning
* capacitor/resistor so that, at load time, the device contributes nothing and
* its floating node is pinned with a unit diagonal (kept nonsingular).
*
* Node degree is counted GENERICALLY over every device type through the
* GENnode() terminal array, so no device family can be missed (which would
* wrongly prune a live node). Only capacitors and resistors are ever removed.
* Disable with `set no_topo_reduce` in .spiceinit. */
static void
CKTtopologyReduce(CKTcircuit *ckt)
{
int i, t, nterm, maxnode, removed_total = 0, reported = 0;
int captype = -1, restype = -1;
int *degree;
GENmodel *gmod;
GENinstance *ginst;
if (cp_getvar("no_topo_reduce", CP_BOOL, NULL, 0))
return;
maxnode = ckt->CKTmaxEqNum;
if (maxnode < 1)
return;
degree = TMALLOC(int, maxnode + 1);
if (!degree)
return;
for (i = 0; i <= maxnode; i++)
degree[i] = 0;
/* complete, type-agnostic node degree over all device terminals */
for (i = 0; i < DEVmaxnum; i++) {
if (!DEVices[i] || !ckt->CKThead[i] || !DEVices[i]->DEVpublic.terms)
continue;
nterm = *(DEVices[i]->DEVpublic.terms);
if (DEVices[i]->DEVpublic.name) {
if (!strcmp(DEVices[i]->DEVpublic.name, "Capacitor")) captype = i;
else if (!strcmp(DEVices[i]->DEVpublic.name, "Resistor")) restype = i;
}
for (gmod = ckt->CKThead[i]; gmod; gmod = gmod->GENnextModel)
for (ginst = gmod->GENinstances; ginst; ginst = ginst->GENnextInstance) {
int *nodes = GENnode(ginst);
for (t = 0; t < nterm; t++) {
int nd = nodes[t];
if (nd > 0 && nd <= maxnode)
degree[nd]++;
}
}
}
if (captype < 0 && restype < 0) {
FREE(degree);
return;
}
/* Leaf-prune to a fixpoint: removing a dangling passive can drop its other
* node to degree 1, exposing the next leaf of a dangling chain. */
for (;;) {
int removed_this_pass = 0;
if (captype >= 0) {
CAPmodel *cm;
for (cm = (CAPmodel *)ckt->CKThead[captype]; cm; cm = CAPnextModel(cm))
for (CAPinstance *ci = CAPinstances(cm); ci; ci = CAPnextInstance(ci)) {
int pn = ci->CAPposNode, nn = ci->CAPnegNode, mode = 0;
if (ci->CAPdangling)
continue;
if (pn > 0 && degree[pn] == 1) mode |= 1;
if (nn > 0 && degree[nn] == 1) mode |= 2;
if (!mode)
continue;
ci->CAPdangling = mode;
if (mode & 1) degree[pn] = 0; else if (pn > 0) degree[pn]--;
if (mode & 2) degree[nn] = 0; else if (nn > 0) degree[nn]--;
removed_this_pass++;
if (reported++ < 40)
fprintf(stdout,
"Topology reduction: removed dangling capacitor %s "
"(floating node %s)\n", ci->CAPname,
(char *)CKTnodName(ckt, (mode & 2) ? nn : pn));
}
}
if (restype >= 0) {
RESmodel *rm;
for (rm = (RESmodel *)ckt->CKThead[restype]; rm; rm = RESnextModel(rm))
for (RESinstance *ri = RESinstances(rm); ri; ri = RESnextInstance(ri)) {
int pn = ri->RESposNode, nn = ri->RESnegNode, mode = 0;
if (ri->RESdangling)
continue;
if (pn > 0 && degree[pn] == 1) mode |= 1;
if (nn > 0 && degree[nn] == 1) mode |= 2;
if (!mode)
continue;
ri->RESdangling = mode;
if (mode & 1) degree[pn] = 0; else if (pn > 0) degree[pn]--;
if (mode & 2) degree[nn] = 0; else if (nn > 0) degree[nn]--;
removed_this_pass++;
if (reported++ < 40)
fprintf(stdout,
"Topology reduction: removed dangling resistor %s "
"(floating node %s)\n", ri->RESname,
(char *)CKTnodName(ckt, (mode & 2) ? nn : pn));
}
}
removed_total += removed_this_pass;
if (!removed_this_pass)
break;
}
if (removed_total)
fprintf(stdout, "Topology reduction: %d dangling passive(s) removed "
"from the matrix.\n", removed_total);
FREE(degree);
}
int
CKTsetup(CKTcircuit *ckt)
{
@ -116,6 +247,13 @@ CKTsetup(CKTcircuit *ckt)
}
}
/* Topology reduction for removing dangling capacitors and resistors:
* mark dangling (degree-1) passive leaves
* for removal. Done after all device setups (so the node degrees are
* complete) but before the matrix is converted/bound for the linear
* solver -- it only changes load-time stamping, not the matrix structure. */
CKTtopologyReduce(ckt);
#ifdef XSPICE
/* gtri - begin - Setup for adding rshunt option resistors */

View File

@ -64,6 +64,12 @@ typedef struct sCAPinstance {
unsigned CAPbv_maxGiven : 1; /* flags indicates maximum voltage is given */
int CAPsenParmNo; /* parameter # for sensitivity use;
set equal to 0 if not a design parameter*/
int CAPdangling; /* topology reduction: 0 = normal device;
bit0 set => pos node is a dangling (degree-1) floating node,
bit1 set => neg node is a dangling floating node. A dangling
cap is removed from the system: it contributes no
charge/current and its floating node(s) are pinned with a unit
diagonal so the matrix stays nonsingular. Set in CKTtopologyReduce(). */
#ifdef KLU
BindElement *CAPposPosBinding ;

View File

@ -50,6 +50,19 @@ CAPload(GENmodel *inModel, CKTcircuit *ckt)
*(ckt->CKTrhsOld+here->CAPnegNode) ;
}
if(ckt->CKTmode & (MODETRAN | MODEAC)) {
if (here->CAPdangling) {
/* Topology reduction: this cap hangs on a
* floating (degree-1) node. Remove it from the system:
* pin the floating node(s) with a unit diagonal so the
* matrix stays nonsingular, and contribute no charge or
* current. This eliminates the spurious LTE pressure
* that otherwise drives "Timestep too small" at the
* dangling node (set in CKTtopologyReduce()). */
if (here->CAPdangling & 1) *(here->CAPposPosPtr) += 1.0;
if (here->CAPdangling & 2) *(here->CAPnegNegPtr) += 1.0;
*(ckt->CKTstate0+here->CAPqcap) = 0.0;
continue;
}
#ifndef PREDICTOR
if(ckt->CKTmode & MODEINITPRED) {
*(ckt->CKTstate0+here->CAPqcap) =
@ -71,10 +84,20 @@ CAPload(GENmodel *inModel, CKTcircuit *ckt)
*(ckt->CKTstate1+here->CAPccap) =
*(ckt->CKTstate0+here->CAPccap);
}
*(here->CAPposPosPtr) += m * geq;
*(here->CAPnegNegPtr) += m * geq;
*(here->CAPposNegPtr) -= m * geq;
*(here->CAPnegPosPtr) -= m * geq;
/* Tiny conductance in parallel with the capacitor so that a
* node connected only through capacitors (e.g. a dangling
* compensation cap) still has a DC reference and cannot drift
* into a singular/ill-conditioned operating point. This
* restores the 1e15 ohm "avoid floating nodes" resistor that
* the old behavioral C=f(v) expansion placed across the cap;
* 1e-15 S is negligible for caps on driven nodes. */
{
double gpar = 1e-15;
*(here->CAPposPosPtr) += m * geq + gpar;
*(here->CAPnegNegPtr) += m * geq + gpar;
*(here->CAPposNegPtr) -= m * geq + gpar;
*(here->CAPnegPosPtr) -= m * geq + gpar;
}
*(ckt->CKTrhs+here->CAPposNode) -= m * ceq;
*(ckt->CKTrhs+here->CAPnegNode) += m * ceq;
} else

View File

@ -81,6 +81,10 @@ typedef struct sRESinstance {
unsigned RESbv_maxGiven : 1; /* flags indicates maximum voltage is given */
int RESsenParmNo; /* parameter # for sensitivity use;
* set equal to 0 if not a design parameter*/
int RESdangling; /* topology reduction: 0 = normal device;
bit0 => pos node dangling, bit1 => neg node dangling. A dangling
resistor is removed from the system and its floating node(s)
pinned with a unit diagonal. Set in CKTtopologyReduce(). */
#ifndef NONOISE
double RESnVar[NSTATVARS][RESNSRCS];

View File

@ -25,6 +25,16 @@ RESload(GENmodel *inModel, CKTcircuit *ckt)
for (here = RESinstances(model); here != NULL ;
here = RESnextInstance(here)) {
if (here->RESdangling) {
/* Topology reduction: dangling (degree-1) resistor.
* Remove it from the system, pin the floating node(s) with a
* unit diagonal (set in CKTtopologyReduce()). */
if (here->RESdangling & 1) *(here->RESposPosPtr) += 1.0;
if (here->RESdangling & 2) *(here->RESnegNegPtr) += 1.0;
here->REScurrent = 0.0;
continue;
}
here->REScurrent = (*(ckt->CKTrhsOld+here->RESposNode) -
*(ckt->CKTrhsOld+here->RESnegNode)) * here->RESconduct;