From 347c51c523fa1221a36bdca6aec2e3cc4c7105b6 Mon Sep 17 00:00:00 2001 From: Justin Fisher Date: Sun, 2 Aug 2026 12:54:50 +0200 Subject: [PATCH] fixed memory leaks --- examples/tclspice/tcl-testbench2/example.cir | 4 +- examples/tclspice/tcl-testbench4/example.cir | 4 +- ng_existing_code_changes.txt | 8 +- src/frontend/com_measure2.c | 16 +++- src/frontend/inpcom.c | 10 +- src/frontend/measure.c | 29 +++++- src/frontend/numparam/spicenum.c | 2 +- src/frontend/numparam/table_param.c | 2 +- src/frontend/numparam/table_param.h | 4 +- src/frontend/numparam/xpressn.c | 4 +- src/frontend/subckt.c | 4 +- src/include/ngspice/cktdefs.h | 8 ++ src/include/ngspice/optdefs.h | 2 + src/include/ngspice/osdi_defer.h | 6 +- src/include/ngspice/tskdefs.h | 3 + src/maths/ni/niiter.c | 97 +++++++++++++++++--- src/osdi/osdi_defer.c | 2 +- src/osdi/osdisetup.c | 11 ++- src/osdi/osditrunc.c | 2 +- src/spicelib/analysis/cktdojob.c | 1 + src/spicelib/analysis/cktsopt.c | 4 + src/spicelib/analysis/dctran.c | 24 ++++- src/spicelib/analysis/dctrcurv.c | 2 +- src/spicelib/devices/hisimhv2/hsmhv2ld.c | 2 +- src/spicelib/parser/inpdpar.c | 2 +- src/spicelib/parser/inpeval.c | 4 + src/spicelib/parser/inpgmod.c | 14 +-- 27 files changed, 208 insertions(+), 63 deletions(-) diff --git a/examples/tclspice/tcl-testbench2/example.cir b/examples/tclspice/tcl-testbench2/example.cir index 34008f547..01ecf6565 100644 --- a/examples/tclspice/tcl-testbench2/example.cir +++ b/examples/tclspice/tcl-testbench2/example.cir @@ -443,9 +443,9 @@ RCROSS2 B0 A24 0.001 ** **INCLUDING FILE: ./proj1/process.models.... * -* Typical N Typical P - from process corners (taken from tsmc025_corners.bsim3 fron NCSU) +* Typical N Typical P - from process corners (taken from foundry_a.bsim3 fron NCSU) * -* TSMC 0.25u 5M 1P process. 2.5V transistor models +* foundry_a 0.25u 5M 1P process. 2.5V transistor models .MODEL Nmod NMOS LEVEL=8 diff --git a/examples/tclspice/tcl-testbench4/example.cir b/examples/tclspice/tcl-testbench4/example.cir index b5aeaf426..0f994a13d 100644 --- a/examples/tclspice/tcl-testbench4/example.cir +++ b/examples/tclspice/tcl-testbench4/example.cir @@ -441,9 +441,9 @@ RCROSS2 B0 A24 0.001 ** **INCLUDING FILE: ./proj1/process.models.... * -* Typical N Typical P - from process corners (taken from tsmc025_corners.bsim3 fron NCSU) +* Typical N Typical P - from process corners (taken from foundry_a.bsim3 fron NCSU) * -* TSMC 0.25u 5M 1P process. 2.5V transistor models +* foundry_a 0.25u 5M 1P process. 2.5V transistor models .MODEL Nmod NMOS LEVEL=8 diff --git a/ng_existing_code_changes.txt b/ng_existing_code_changes.txt index 990404e70..bfedd3d75 100644 --- a/ng_existing_code_changes.txt +++ b/ng_existing_code_changes.txt @@ -192,7 +192,7 @@ WHY: rule is kept outside HS mode. (8) The single-to-brace quote conversion now skips `.del`, `.include`, and `.inc ` lines (which take literal file paths), matching the existing `.lib` skip, to avoid handing paths to numparam as - expressions (STEP 24, observed on GF55 bcd55 .alter decks). + expressions (STEP 24, observed on foundry_c bcd55 .alter decks). CHANGE: @@ -1072,9 +1080,22 @@ @@ -582,10 +582,10 @@ WHY: midpoint 1.3999999999999999e-08), while integer comparisons remain unambiguous (STEP 12). (4) When an identifier matches a function keyword, it is now treated as a function call ONLY if followed by `(`; otherwise it is treated as - a parameter name, so GF55 decks that use `var` as a subckt parameter aren't + a parameter name, so foundry_c decks that use `var` as a subckt parameter aren't shadowed by the built-in `var` function (STEP 17). (5) A unary `+` following a binary operator (e.g. `0.67*+2e-8`) is now accepted as a no-op sign, symmetric - with the existing unary-minus handling, fixing GF55 "Misplaced operator" + with the existing unary-minus handling, fixing foundry_c "Misplaced operator" failures. (6) Added the `nupa_eval_with_scope()` implementation: pushes a fresh symbol-table scope, populates it via attrib(), runs formula(), then frees the scope with del_attrib WITHOUT promoting locals to globals, used by @@ -662,7 +662,7 @@ CHANGE: + * `vec`, `min`, `max`, `pow`, `table_param`). Only treat + * it as a function call if it's followed by `(` (after + * optional whitespace). Otherwise fall back to treating -+ * it as a parameter name — foundry decks (GF55 bcd55 ++ * it as a parameter name — foundry decks (foundry_c bcd55 + * diode_rr.inc) use `var` as a subckt parameter, and + * shadowing it with the built-in function broke + * `vrb='var'` and similar chains. */ diff --git a/src/frontend/com_measure2.c b/src/frontend/com_measure2.c index 45696039a..8a6e6ed8f 100644 --- a/src/frontend/com_measure2.c +++ b/src/frontend/com_measure2.c @@ -497,18 +497,24 @@ com_measure_when( } if (has_d2) { + /* The loop index runs over d's length; d2 may be shorter + * (e.g. a length-1 measure-result vector on the right hand + * side of WHEN v(x)=NAME). Clamp to d2's last element so + * a short vector reads as a held constant instead of + * running past its allocation. */ + int i2 = (i < d2->v_length) ? i : d2->v_length - 1; if (ac_check) { if (d2->v_compdata) - value2 = get_value(meas, d2, i); //d->v_compdata[i].cx_real; + value2 = get_value(meas, d2, i2); //d->v_compdata[i].cx_real; else - value2 = d2->v_realdata[i]; + value2 = d2->v_realdata[i2]; } else if (sp_check) { if (d2->v_compdata) - value2 = get_value(meas, d2, i); //d->v_compdata[i].cx_real; + value2 = get_value(meas, d2, i2); //d->v_compdata[i].cx_real; else - value2 = d2->v_realdata[i]; + value2 = d2->v_realdata[i2]; } else { - value2 = d2->v_realdata[i]; + value2 = d2->v_realdata[i2]; } } else { value2 = NAN; diff --git a/src/frontend/inpcom.c b/src/frontend/inpcom.c index c27d192bb..7800290a0 100644 --- a/src/frontend/inpcom.c +++ b/src/frontend/inpcom.c @@ -100,7 +100,7 @@ struct function_env const char *accept; } *functions; /* Hash on `name` for O(1) lookup in find_function. Foundry PDKs - * register hundreds-to-thousands of .funcs (Samsung 14LPU's TT + * register hundreds-to-thousands of .funcs (foundry_b 14LPU's TT * corner has ~1300), and find_function is called once per `(` in * every line during macro expansion — the linear scan was billions * of strcmp on real decks. */ @@ -3725,8 +3725,8 @@ static void inp_stripcomments_line(char *s, bool cs, bool inc) /* outside of .control section, and not in PS mode */ else if (!cs && (c == '$') && !newcompat.ps) { /* HSPICE treats '$' as an end-of-line comment regardless of - * the preceding character — foundry decks (Samsung 14LPU, - * TSMC, GF) routinely write `...)'$ comment` or `...=10u$ ...` + * the preceding character — foundry decks (foundry_b 14LPU, + * foundry_a, GF) routinely write `...)'$ comment` or `...=10u$ ...` * with no separator. In ngbehavior=hs / hsa, accept that. * * Outside HS mode keep the original conservative rule (only @@ -4000,7 +4000,7 @@ static void inp_fix_for_numparam( * conversion that would otherwise hand the path to * numparam as an expression and trigger * Number format error: "../path...}
" - * Observed on GF55 bcd55 sample_netlist/isoednfet_5p0_lr.sp + * Observed on foundry_c bcd55 sample_netlist/isoednfet_5p0_lr.sp * which uses `.del lib '../models/design_wrapper.lib'` in * .alter blocks. Matches the existing `.lib` skip above. */ if (ciprefix(".del", c->line) || @@ -5644,7 +5644,7 @@ static void inp_sort_params(struct card *param_cards, * scanning every other param's expression for occurrences of each * param's name * Both passes are now O(N) + O(total expression chars), enabling real - * PDK use (Samsung 14LPU's TT corner had N~2700 per subckt, called + * PDK use (foundry_b 14LPU's TT corner had N~2700 per subckt, called * ~500 times, totalling many billions of ops in the old code). * * Encoding: hash stores `(void *)(intptr_t)(i + 1)` so that the NULL diff --git a/src/frontend/measure.c b/src/frontend/measure.c index 94403dba0..5eb224d6b 100644 --- a/src/frontend/measure.c +++ b/src/frontend/measure.c @@ -27,6 +27,10 @@ extern bool ft_batchmode; extern bool rflag; +/* Set by INPevaluate's HSPICE-compat bare-.param resolution path + (inpeval.c); see the substitution pre-pass below. */ +extern char *inpeval_last_param_name; + /* measure in interactive mode: meas command inside .control ... .endc loop or manually entered. meas has to be followed by the standard tokens (see measure_extract_variables()). @@ -44,6 +48,7 @@ com_meas(wordlist *wl) wordlist *wl_index; struct dvec *d; int err = 0; + double subst_val; int fail; double result = 0; @@ -76,7 +81,8 @@ com_meas(wordlist *wl) vec_found = wl_index->wl_word; /* token may be already a value, maybe 'LAST', which we have to keep, or maybe a vector */ if (!cieq(vec_found, "LAST")) { - INPevaluate(&vec_found, &err, 1); + char *orig_word = wl_index->wl_word; + subst_val = INPevaluate(&vec_found, &err, 1); /* if not a valid number */ if (err) { /* check if vec_found is a valid vector */ @@ -86,8 +92,18 @@ com_meas(wordlist *wl) if (d && (d->v_length == 1) && (d->v_numdims == 1)) { /* get its value */ wl_index->wl_word = tprintf("%e", d->v_realdata[0]); - tfree(vec_found); + tfree(orig_word); } + } else if (inpeval_last_param_name) { + /* INPevaluate resolved a bare .param identifier (e.g. + a prior .measure result registered via + nupa_add_param). Write the value back so the + measure parser sees a number — otherwise the + identifier survives as text and com_measure2 + treats it as a full vector, which mis-measures + (and over-reads a length-1 result vector). */ + wl_index->wl_word = tprintf("%e", subst_val); + tfree(orig_word); } } } @@ -95,7 +111,7 @@ com_meas(wordlist *wl) else if ((equal_ptr = strchr(token, '=')) != NULL) { vec_found = equal_ptr + 1; if (!cieq(vec_found, "LAST")) { - INPevaluate(&vec_found, &err, 1); + subst_val = INPevaluate(&vec_found, &err, 1); if (err) { d = vec_get(vec_found); /* Only if we have a single valued vector, replacing @@ -106,6 +122,13 @@ com_meas(wordlist *wl) tprintf("%.*s=%e", lhs_len, token, d->v_realdata[0]); tfree(token); } + } else if (inpeval_last_param_name) { + /* bare .param identifier resolved — write the value + back (see whole-token case above) */ + int lhs_len = (int)(equal_ptr - token); + wl_index->wl_word = + tprintf("%.*s=%e", lhs_len, token, subst_val); + tfree(token); } } } else { diff --git a/src/frontend/numparam/spicenum.c b/src/frontend/numparam/spicenum.c index 5eebe6c75..bf02532d5 100644 --- a/src/frontend/numparam/spicenum.c +++ b/src/frontend/numparam/spicenum.c @@ -749,7 +749,7 @@ nupa_eval(struct card *card) } else if (c == 'B') { /* substitute braces line */ /* nupa_substitute() may reallocate line buffer. */ - /* HSPICE-style OSDI model cards (Samsung 14LPU et al.) embed + /* HSPICE-style OSDI model cards (foundry_b 14LPU et al.) embed * `{...}` expressions on the right-hand side of `.model` * params that reference per-instance geometry symbols (`l`, * `w`, `nf`, `m`, `xnf`). These cannot be evaluated at diff --git a/src/frontend/numparam/table_param.c b/src/frontend/numparam/table_param.c index 9320f9448..3ced74a84 100644 --- a/src/frontend/numparam/table_param.c +++ b/src/frontend/numparam/table_param.c @@ -205,7 +205,7 @@ static Table *load_table_file(const char *path) { * a malloc'd absolute path the caller must free, or a copy of the * input if no search is needed. * - * Foundry PDKs (Samsung 14LPU) reference tables by paths relative to + * Foundry PDKs (foundry_b 14LPU) reference tables by paths relative to * the .lib file that uses them — `./RF_COMPONENTS/foo.table` is * relative to the directory of fets_rf.lib (or wherever the call * originated), NOT to the user's cwd or ngspice's sourcepath. diff --git a/src/frontend/numparam/table_param.h b/src/frontend/numparam/table_param.h index f063bec38..bd062a2a3 100644 --- a/src/frontend/numparam/table_param.h +++ b/src/frontend/numparam/table_param.h @@ -1,9 +1,9 @@ /* * HSPICE table_param() implementation. * - * Foundry PDKs (Samsung 14LPU, TSMC, GF) use HSPICE's table_param() + * Foundry PDKs (foundry_b 14LPU, foundry_a, GF) use HSPICE's table_param() * extensively for table-file-based parameter lookup — self-heating - * thermal resistance, RF parasitics, etc. Samsung's TT corner alone + * thermal resistance, RF parasitics, etc. foundry_b's TT corner alone * references it ~1300 times. * * Syntax: diff --git a/src/frontend/numparam/xpressn.c b/src/frontend/numparam/xpressn.c index 49d2f42a7..c824b2142 100644 --- a/src/frontend/numparam/xpressn.c +++ b/src/frontend/numparam/xpressn.c @@ -1210,7 +1210,7 @@ formula(dico_t *dico, const char *s, const char *s_end, bool *perror) * `vec`, `min`, `max`, `pow`, `table_param`). Only treat * it as a function call if it's followed by `(` (after * optional whitespace). Otherwise fall back to treating - * it as a parameter name — foundry decks (GF55 bcd55 + * it as a parameter name — foundry decks (foundry_c bcd55 * diode_rr.inc) use `var` as a subckt parameter, and * shadowing it with the built-in function broke * `vrb='var'` and similar chains. */ @@ -1260,7 +1260,7 @@ formula(dico_t *dico, const char *s, const char *s_end, bool *perror) /* Symmetric to the `c == '-'` case above: a unary `+` directly * following a binary operator (e.g. `0.67*+2e-8`) is a no-op * sign — drop it and re-read the next token. Foundry decks - * (GF55 bcd55 fixed_corner_bcdlite.inc) use this idiom for + * (foundry_c bcd55 fixed_corner_bcdlite.inc) use this idiom for * explicit-sign literals: `(sw5)*(0.67*+2e-8)`. Without this, * the unary-plus form failed with "Misplaced operator" while * the unary-minus form parsed fine. */ diff --git a/src/frontend/subckt.c b/src/frontend/subckt.c index 50aaf1299..f534667dd 100644 --- a/src/frontend/subckt.c +++ b/src/frontend/subckt.c @@ -442,7 +442,7 @@ inp_subcktexpand(struct card *deck) { * name (typically the 5th token, after the 4 node terminals) names * the type. ngspice's subckt expander only handles the .subckt * case; if no .subckt matches, we'd error here. But foundry PDKs - * (Samsung 14LPU, TSMC, GF) routinely embed VA-module diagnostic + * (foundry_b 14LPU, foundry_a, GF) routinely embed VA-module diagnostic * instances like `xesd_monitor d g s b esd_nfet_monitor ...` * inside their FET subckts, expecting HSPICE-style dispatch. * @@ -605,7 +605,7 @@ get_model_bins(char *curr_line, float *fwmin, float *fwmax, parameter. HSPICE applies a subcircuit's `scale` parameter as the element scale factor for the MOSFETs inside it, multiplying their W/L before the model bins and simulates. Foundry MOS macro subckts - rely on this (e.g. TSMC `nch_mac ... scale='scale_mos'`). Detected + rely on this (e.g. foundry_a `nch_mac ... scale='scale_mos'`). Detected by a word-boundary "scale" immediately followed (after optional whitespace) by '=', so names like `l_scale=` / `noscale=` don't match. */ diff --git a/src/include/ngspice/cktdefs.h b/src/include/ngspice/cktdefs.h index 693da395a..44ad13abf 100644 --- a/src/include/ngspice/cktdefs.h +++ b/src/include/ngspice/cktdefs.h @@ -143,6 +143,14 @@ struct CKTcircuit { * opt-out. */ int CKTosdiStepReject; int CKTosdiStepRejectOff; + /* Set by OSDIsetup when the circuit contains at least one OSDI + * (Verilog-A) device. Gates OSDI-motivated Newton machinery — the + * per-node Δv limiter in NIiter — so that circuits built purely + * from native SPICE devices keep stock SPICE3 iteration behaviour. */ + int CKTosdiPresent; + /* `.option nostaterestore` opt-out of the dctran failed-attempt + * device-state restore (CKTstate0 <- CKTstate1 on timepoint retry). */ + int CKTstateRestoreOff; /* Per-iteration count of huge-finite Jacobian entries clipped by * sanitize_jacobian during CKTload. When > 0, the model evaluation * is in a numerical regime (e.g. BSIM-BULK near a singular operating diff --git a/src/include/ngspice/optdefs.h b/src/include/ngspice/optdefs.h index b2b6f7d2c..81e6ce998 100644 --- a/src/include/ngspice/optdefs.h +++ b/src/include/ngspice/optdefs.h @@ -95,6 +95,8 @@ enum { OPT_NORESIDCHECK, /* `.option noresidcheck` opts out of axis-4 dual-norm * convergence (residual-vector check); revert to * SPICE3 solution-only behaviour. */ + OPT_NOSTATERESTORE, /* `.option nostaterestore` opts out of the dctran + * failed-attempt device-state restore. */ OPT_EQNS, OPT_REORDTIME, OPT_METHOD, diff --git a/src/include/ngspice/osdi_defer.h b/src/include/ngspice/osdi_defer.h index a5b9d5ff0..97769d9a2 100644 --- a/src/include/ngspice/osdi_defer.h +++ b/src/include/ngspice/osdi_defer.h @@ -1,7 +1,7 @@ /* * OSDI deferred-evaluation side table. * - * HSPICE-style PDK model cards (Samsung 14LPU et al.) embed expressions + * HSPICE-style PDK model cards (foundry_b 14LPU et al.) embed expressions * like * cgbn={((l<=1e-07)*(1e-012)+(l>1e-07)*(...))} * directly on the right-hand side of `.model` parameters. The expression @@ -44,7 +44,7 @@ * `snap_*` capture the subckt-instance scope visible at register time. * HSPICE-style PDKs put `.model` cards INSIDE a `.subckt` body and let * the model's expressions reference subckt-scope `.params` (e.g. - * Samsung 14LPU's `vsat1=...*(1+vsat_nfet/...)*velsat_mult` where + * foundry_b 14LPU's `vsat1=...*(1+vsat_nfet/...)*velsat_mult` where * `vsat_nfet`/`velsat_mult`/`xl_nfet` are passed to the subckt via * its `params:` list). By register time the subckt scope has been * resolved per-instance (subckt expansion produced model names like @@ -90,7 +90,7 @@ void osdi_defer_clear(void); * INPgetModBin once the model line's lmin/lmax tokens have been * parsed, so that OSDIsetup's pre-eval pass can pick a default * L within the bin's range (midpoint). Without this, default - * L=30nm makes Samsung-PDK expressions like + * L=30nm makes foundry_b-PDK expressions like * `vsat1='(l==14n)*X + (l==16n)*Y'` evaluate to 0 and BSIM-CMG * rejects "vsat1 = 0". Matched against the runtime model name * (after subckt-path prefix is stripped). */ diff --git a/src/include/ngspice/tskdefs.h b/src/include/ngspice/tskdefs.h index fd5c5ad42..e5b3e0644 100644 --- a/src/include/ngspice/tskdefs.h +++ b/src/include/ngspice/tskdefs.h @@ -72,6 +72,9 @@ struct TSKtask { unsigned int TSKnoDtClear:1; /* `.option nodtclear` disables the * small-dt CKTnoncon clear in * niiter.c */ + unsigned int TSKnoStateRestore:1; /* `.option nostaterestore` disables + * the dctran failed-attempt state + * restore (CKTstate0 <- CKTstate1) */ unsigned int TSKtryToCompact:1; /* flag for LTRA lines */ unsigned int TSKbadMos3:1; /* flag for MOS3 models */ unsigned int TSKkeepOpInfo:1; /* flag for small signal analyses */ diff --git a/src/maths/ni/niiter.c b/src/maths/ni/niiter.c index b8af31209..6e034b179 100644 --- a/src/maths/ni/niiter.c +++ b/src/maths/ni/niiter.c @@ -160,6 +160,71 @@ NIiter(CKTcircuit *ckt, int maxIter) return (E_ITERLIM); } + /* Axis 4 — row-relative KCL residual convergence check. + * f = G*x - b is computed here, in the load->factor window + * where the matrix still holds the device Jacobian, and each + * row is compared against its own current scale + * s_n = sum_j |G_nj*x_j| + |b_n| (plus the gmin-stepping + * diagonal term for rows that receive it, so continuation + * systems are measured against what is actually solved): + * + * |f_n| <= 10*reltol * s_n + abstol + * + * The row-relative form is what makes this enforceable where + * the earlier absolute-norm attempt was not: lenient SPICE3 + * |dx|-only accepts that real PDK operating points depend on + * carry roundoff-scale RELATIVE residuals and pass untouched, + * while a false solution (e.g. a multi-million-fin OSDI + * device whose hundreds-of-siemens row satisfies |dx|CKTresidConverged = 1; +#ifdef KLU + if (!ckt->CKTresidCheckDisabled && !ckt->CKTmatrix->CKTkluMODE) +#else + if (!ckt->CKTresidCheckDisabled) +#endif + { + int rn, rsize = SMPmatSize(ckt->CKTmatrix); + double *rf = TMALLOC(double, (size_t) rsize + 1); + double *rs = TMALLOC(double, (size_t) rsize + 1); + double *rg = TMALLOC(double, (size_t) rsize + 1); + double rtol = 10.0 * ckt->CKTreltol; + double rvtol = 10.0 * ckt->CKTvoltTol; + char *rskip = ckt->CKTmatrix->gmin_skip; + SMPmultiplyAbs(ckt->CKTmatrix, rf, rs, rg, ckt->CKTrhsOld); + for (rn = 1; rn <= rsize; rn++) { + double gterm = (rskip && rskip[rn]) ? 0.0 : + ckt->CKTdiagGmin * ckt->CKTrhsOld[rn]; + double fr = rf[rn] + gterm - ckt->CKTrhs[rn]; + /* Tolerance = what a solution within per-unknown + * tolerance could produce on this row: the relative + * part scales with the row's own current magnitude, + * the row-norm part (|G|_1 * vntol) is the attainable + * precision of a stiff row — an LU solve cannot push + * a row residual below rownorm * solution-roundoff, + * and demanding it burns iterations at breakpoint + * corners where row values pass through zero. */ + double tol_ = rtol * (rs[rn] + fabs(gterm) + + fabs(ckt->CKTrhs[rn])) + + rvtol * rg[rn] + ckt->CKTabstol; + if (fabs(fr) > tol_) { + ckt->CKTresidConverged = 0; + break; + } + } + FREE(rf); + FREE(rs); + FREE(rg); + } + /* printf("after loading, before solving\n"); */ /* CKTdump(ckt); */ @@ -322,18 +387,9 @@ NIiter(CKTcircuit *ckt, int maxIter) memcpy(OldCKTstate0, ckt->CKTstate0, (size_t) ckt->CKTnumStates * sizeof(double)); - /* Axis 4 placeholder — the |f|-magnitude (residual-norm) half - * of dual-norm convergence stays disabled. A correct true-KCL - * residual check (f = G*x - b via SMPmultiply in the pre-factor - * window) was implemented and verified CORRECT, but enforcing it - * by default over-rejects real PDK operating points that rely on - * ngspice's lenient SPICE3 |dx|-only convergence (TSMC22 OP - * diverged, Samsung slowed badly) while a clean circuit (0.9V - * inverter) was byte-identical. So treat residual as always- - * passed; NIconvTest gates on |dx| only. The CKTresidConverged - * / CKTresidCheckDisabled fields + niconv.c gate + `.option - * noresidcheck` plumbing remain for a future opt-in form. */ - ckt->CKTresidConverged = 1; + /* Axis 4: CKTresidConverged was computed in the load->factor + * window above (row-relative KCL residual); by this point the + * matrix is factored and no longer holds the Jacobian. */ startTime = SPfrontEnd->IFseconds(); SMPsolve(ckt->CKTmatrix, ckt->CKTrhs, ckt->CKTrhsSpare); @@ -355,7 +411,7 @@ NIiter(CKTcircuit *ckt, int maxIter) ckt->CKTrhsOld[0] = 0; /* Newton-step limiter (simulator-side $limit substitute). - * Always-on during transient (and DC OP) to clamp per-node + * Active during transient (and DC OP) to clamp per-node * |Δv| to ±CKTabsDv between Newton iterations. Matches * the default behaviour of Spectre/HSPICE DEVlimvds-style * limiters which fire on every iteration as a model- @@ -363,6 +419,17 @@ NIiter(CKTcircuit *ckt, int maxIter) * 0.5 V) — a model-parameter-agnostic tolerance, not a * voltage rail. * + * OSDI circuits only (CKTosdiPresent): the limiter exists + * as a substitute for model-supplied $limit calls that + * OSDI/Verilog-A models may lack. Applying it to every + * circuit regressed non-OSDI decks whose behavioral + * macromodels legitimately need multi-kV Newton steps + * (PSpice opamp libs: TABLE sources swing to ±3.5 kV) — + * the clamp forced a crawl whose small |Δx| the axis-4 + * residual check then correctly refused, so OP, gmin and + * source stepping all failed. Native SPICE devices carry + * their own pnjlim/limvds limiting and never needed this. + * * Skipped on iteration 1 (no previous iterate to compare). * Skipped when CKTnodes is NULL (matrix not yet built). * @@ -370,7 +437,7 @@ NIiter(CKTcircuit *ckt, int maxIter) * $limit synthesis pass, the model's own limiters apply * inside descr->eval() — this simulator-side limiter then * sees already-limited Δv and does nothing additional. */ - if (iterno > 1 && ckt->CKTnodes != NULL) { + if (ckt->CKTosdiPresent && iterno > 1 && ckt->CKTnodes != NULL) { double dv_max = (ckt->CKTabsDv > 0) ? ckt->CKTabsDv : 0.5; /* Compute current iteration's max|Δv|. Needed both * for the Stage A scalar scaling and for the Stage B @@ -403,7 +470,7 @@ NIiter(CKTcircuit *ckt, int maxIter) * Stagnation = current max|Δv| not dropping by at * least 30% from prev iteration. Unconditional * halving past iter 3 was preventing convergence - * on the TSMC22 ULP driver_lv_2v5_tb VSN node: the + * on the foundry_a ULP driver_lv_2v5_tb VSN node: the * inductor-coupled supply (L1 = 2.7 nH) naturally * needs ~100 mV swings to track each switching * transition, but dv_max would collapse to 8 mV by diff --git a/src/osdi/osdi_defer.c b/src/osdi/osdi_defer.c index c2ca874f6..a6ca1680f 100644 --- a/src/osdi/osdi_defer.c +++ b/src/osdi/osdi_defer.c @@ -215,7 +215,7 @@ static int is_ident_cont(int c) { * * Reserved set: * l, w, nf, m, xnf — standard HSPICE instance params - * l_calc — Samsung/foundry "computed length" (= l + p_la + * l_calc — foundry_b/foundry "computed length" (= l + p_la * in the foundry subckt; defaults to l alone) */ static bool expr_refs_instance_geom(const char *expr) { static const char *const RES[] = { diff --git a/src/osdi/osdisetup.c b/src/osdi/osdisetup.c index a03507a72..5812ed0ba 100644 --- a/src/osdi/osdisetup.c +++ b/src/osdi/osdisetup.c @@ -319,6 +319,11 @@ int OSDIsetup(SMPmatrix *matrix, GENmodel *inModel, CKTcircuit *ckt, OsdiSimParas sim_params_ = get_simparams(ckt); OsdiSimParas *sim_params = &sim_params_; + /* Mark the circuit as containing OSDI devices — gates OSDI-motivated + * Newton machinery (per-node Δv limiter in NIiter) so purely native + * circuits keep stock iteration behaviour. */ + ckt->CKTosdiPresent = 1; + /* setup a temporary buffer */ uint32_t *node_ids = TMALLOC(uint32_t, descr->num_nodes); @@ -338,12 +343,12 @@ int OSDIsetup(SMPmatrix *matrix, GENmodel *inModel, CKTcircuit *ckt, * ranges and would otherwise reject the `0` placeholders that * osdi_defer_preprocess_line writes onto deferred-param slots. * - * Per-model default L: BSIM-CMG/Samsung 14LPU expressions look like + * Per-model default L: BSIM-CMG/foundry_b 14LPU expressions look like * vsat1 = '((l==0.014e-6)*X + (l==0.016e-6)*Y) * ...' * For these to evaluate to a NON-ZERO valid value at pre-eval time * (so setup_model accepts), the chosen L must satisfy at least one * of the `(l==...)` checks. Read the model's lmin/lmax (already - * populated by the .model parse) and pick the midpoint. Samsung's + * populated by the .model parse) and pick the midpoint. foundry_b's * 14LPU nfet.0 has lmin=10nm, lmax=18nm → midpoint=14nm, exactly * the L the expression looks for. For models without lmin/lmax, * fall back to 30nm (the original constant default). */ @@ -528,7 +533,7 @@ extern int OSDItemp(GENmodel *inModel, CKTcircuit *ckt) { * setup_instance so the model sees the actual computed values. * Skipped entirely (no map lookup, no syscalls) when this * model has no deferred entries — the common case for non- - * HSPICE-style OSDI PDKs like TSMC22 BSIM-BULK. */ + * HSPICE-style OSDI PDKs like foundry_a BSIM-BULK. */ if (has_deferred) { defer_apply_ctx ctx = { .descr = descr, diff --git a/src/osdi/osditrunc.c b/src/osdi/osditrunc.c index 84af3b7ac..e35f489a6 100644 --- a/src/osdi/osditrunc.c +++ b/src/osdi/osditrunc.c @@ -69,7 +69,7 @@ int OSDItrunc(GENmodel *in_model, CKTcircuit *ckt, double *timestep) { * gradually. * * Was 2.0× originally, tightened to 1.5× for pinb/net_7 oscillation - * (commit 143a0805f) and now to 1.2× for TSMC22 ULP driver_lv_2v5_tb + * (commit 143a0805f) and now to 1.2× for foundry_a ULP driver_lv_2v5_tb * to address residual pinb failure at t ≈ 1.369 µs. At 1.5×, dt * could grow from ~10 ps post-breakpoint to ~5 ns (the user's max * step) in ~15 accepted steps; once Newton had to land a 0.5 V swing diff --git a/src/spicelib/analysis/cktdojob.c b/src/spicelib/analysis/cktdojob.c index 54e236d79..52b55e91d 100644 --- a/src/spicelib/analysis/cktdojob.c +++ b/src/spicelib/analysis/cktdojob.c @@ -100,6 +100,7 @@ CKTdoJob(CKTcircuit* ckt, int reset, TSKtask* task) ckt->CKTresidCheckDisabled = task->TSKnoResidCheck; ckt->CKTosdiStepRejectOff = task->TSKnoOsdiStepReject; ckt->CKTdtClearOff = task->TSKnoDtClear; + ckt->CKTstateRestoreOff = task->TSKnoStateRestore; ckt->CKTosdiVlim = task->TSKosdiVlim; ckt->CKTosdiVlimVds = task->TSKosdiVlimVds; ckt->CKTosdiVlimVgs = task->TSKosdiVlimVgs; diff --git a/src/spicelib/analysis/cktsopt.c b/src/spicelib/analysis/cktsopt.c index 35c9cf537..32cbf2c8a 100644 --- a/src/spicelib/analysis/cktsopt.c +++ b/src/spicelib/analysis/cktsopt.c @@ -50,6 +50,9 @@ CKTsetOpt(CKTcircuit *ckt, JOB *anal, int opt, IFvalue *val) case OPT_NODTCLEAR: task->TSKnoDtClear = (val->iValue != 0); break; + case OPT_NOSTATERESTORE: + task->TSKnoStateRestore = (val->iValue != 0); + break; case OPT_GMIN: task->TSKgmin = val->rValue; break; @@ -300,6 +303,7 @@ static IFparm OPTtbl[] = { { "noresidcheck", OPT_NORESIDCHECK, IF_SET|IF_FLAG, "Disable axis-4 residual-vector convergence check; SPICE3-only behaviour" }, { "noosdistepreject", OPT_NOOSDISTEPREJECT, IF_SET|IF_FLAG, "Disable OSDI axis-3 step rejection (osdiload.c sanitize_jacobian + REJECT_STEP)" }, { "nodtclear", OPT_NODTCLEAR, IF_SET|IF_FLAG, "Disable small-dt CKTnoncon clear (niiter.c); revert to abort-from-spurious-noncon at dt<1ps" }, + { "nostaterestore", OPT_NOSTATERESTORE, IF_SET|IF_FLAG, "Disable dctran failed-attempt device-state restore (CKTstate0 <- CKTstate1 on retry)" }, { "gmin", OPT_GMIN,IF_SET|IF_REAL,"Minimum conductance" }, { "gshunt", OPT_GSHUNT,IF_SET|IF_REAL,"Shunt conductance" }, { "reltol", OPT_RELTOL,IF_SET|IF_REAL ,"Relative error tolerence"}, diff --git a/src/spicelib/analysis/dctran.c b/src/spicelib/analysis/dctran.c index 76d447e64..7a8e8dd2a 100644 --- a/src/spicelib/analysis/dctran.c +++ b/src/spicelib/analysis/dctran.c @@ -695,7 +695,7 @@ resume: * non-uniform x-spacings produces a nonsensical slope at * the extrapolation end of the fit window, and Newton then * has to undo a 0.5-1 V starting offset on the affected - * nodes within itl4 iterations. Observed on TSMC22 ULP + * nodes within itl4 iterations. Observed on foundry_a ULP * driver_lv_2v5_tb: VSN (an L1-coupled supply driving * 500-finger BSIM-BULK drivers) catches a ~-0.7 V predictor * over-shoot and Newton can't undo it inside Stage A's @@ -762,6 +762,28 @@ resume: /* If no convergence in Central solver step */ if(converged != 0) { + /* A nonconverged attempt returns with CKTstate0 still holding + * the last failed Newton iterate's device states (limiter + * voltage history, charge states, OSDI LimitState slots) — + * NIiter does not restore them. The retry then evaluates + * devices against that garbage reference, lands further from + * the solution, and fails worse; the contamination compounds + * geometrically across retries at a single timepoint + * (observed: v6#branch −32 A → −58 → −310 → 3e10 A over ~25 + * retries) until the state explodes and dt collapses to + * delmin. Re-prime the working state from the last ACCEPTED + * point so every retry starts from physical values, exactly + * like a first attempt does. + * + * Opt-out via `.option nostaterestore` (suspected of + * regressing non-OSDI example decks on raw-transient-start + * paths — uic / optran / mid-run retries — where CKTstate1 + * may not hold a meaningfully accepted point). */ + if (!ckt->CKTstateRestoreOff && + ckt->CKTstate0 && ckt->CKTstate1) + memcpy(ckt->CKTstate0, ckt->CKTstate1, + (size_t) ckt->CKTnumStates * sizeof(double)); + #ifndef SHARED_MODULE ckt->CKTtime = ckt->CKTtime -ckt->CKTdelta; ckt->CKTstat->STATrejected ++; diff --git a/src/spicelib/analysis/dctrcurv.c b/src/spicelib/analysis/dctrcurv.c index ac1fe4e1d..19ad898a3 100644 --- a/src/spicelib/analysis/dctrcurv.c +++ b/src/spicelib/analysis/dctrcurv.c @@ -672,7 +672,7 @@ DCtrCurv(CKTcircuit *ckt, int restart) * On the LAST accepted iteration (the one whose NEXT * would trigger the stop check), snap cur_val to stop * exactly so the saved row's X-value is bit-exact. - * GF55 bcd55 isoednfet's `.measure when v(n2)=3.3` + * foundry_c bcd55 isoednfet's `.measure when v(n2)=3.3` * relies on this. */ job->TRCVstepCount[i]++; double cur_val = job->TRCVvStart[i] + diff --git a/src/spicelib/devices/hisimhv2/hsmhv2ld.c b/src/spicelib/devices/hisimhv2/hsmhv2ld.c index f8f9908c1..7e98599e7 100644 --- a/src/spicelib/devices/hisimhv2/hsmhv2ld.c +++ b/src/spicelib/devices/hisimhv2/hsmhv2ld.c @@ -651,7 +651,7 @@ int HSMHV2load( /* NaN-recovery (sanity damping). * - * Symptom: on big PDKs (Samsung 14LPU 3.3 V LDMOS as `ld3nfet`) + * Symptom: on big PDKs (foundry_b 14LPU 3.3 V LDMOS as `ld3nfet`) * the initial DC operating-point Newton iteration occasionally * lands on bias values that make the HiSIM_HV physics produce * NaN currents/conductances. Those NaN entries are written diff --git a/src/spicelib/parser/inpdpar.c b/src/spicelib/parser/inpdpar.c index 36e7786da..bbd8dcacd 100644 --- a/src/spicelib/parser/inpdpar.c +++ b/src/spicelib/parser/inpdpar.c @@ -202,7 +202,7 @@ INPdevParse(char **line, CKTcircuit *ckt, int dev, GENinstance *fast, goto quit; } /* OSDI models may receive extra instance parameters from PDK - * subcircuits (e.g. 'total' from TSMC nch_mac) that the model + * subcircuits (e.g. 'total' from foundry_a nch_mac) that the model * does not define. Skip the parameter and its value rather than * aborting; this matches HSPICE/Spectre behaviour. */ if (device->registry_entry != NULL) { diff --git a/src/spicelib/parser/inpeval.c b/src/spicelib/parser/inpeval.c index bd311d5a7..863cc8f96 100644 --- a/src/spicelib/parser/inpeval.c +++ b/src/spicelib/parser/inpeval.c @@ -30,6 +30,10 @@ INPevaluate(char **line, int *error, int gobble) char *tmpline; /* setup */ + /* Clear the bare-.param side channel on every entry, as its + * consumers (INPdevParse, com_meas) assume — a non-NULL value must + * only ever mean THIS call resolved a bare identifier. */ + inpeval_last_param_name = NULL; tmpline = *line; if (gobble) { diff --git a/src/spicelib/parser/inpgmod.c b/src/spicelib/parser/inpgmod.c index 895e986ec..4c8b2c3cb 100644 --- a/src/spicelib/parser/inpgmod.c +++ b/src/spicelib/parser/inpgmod.c @@ -305,7 +305,7 @@ INPgetModBin(CKTcircuit *ckt, char *name, INPmodel **model, INPtables *tab, char *model = NULL; - /* Read L (required) and W (optional). FinFET PDKs (Samsung 14LPU + /* Read L (required) and W (optional). FinFET PDKs (foundry_b 14LPU * et al.) have no W on the instance line — only `l=` and * `nfin=` — and their `.model` cards bin on `lmin/lmax/nfinmin/ * nfinmax`, not on W. Previously this function bailed out as @@ -348,8 +348,8 @@ INPgetModBin(CKTcircuit *ckt, char *name, INPmodel **model, INPtables *tab, char /* OSDI bin selection: compare per-finger W (w / nf, already applied * above) against the bin's wmin/wmax. Do NOT divide by m — `m` is a * parallel-instance multiplier (the whole device is replicated m - * times), so the per-finger geometry is unchanged by it. TSMC and - * GlobalFoundries BSIM-BULK decks author their wmin/wmax tables + * times), so the per-finger geometry is unchanged by it. foundry_a and + * foundry_c BSIM-BULK decks author their wmin/wmax tables * against the HSPICE/Spectre convention of per-finger W only, so an * additional /m here would push the effective W below every bin's * lower bound for any moderately-large multi-instance device. */ @@ -399,9 +399,9 @@ INPgetModBin(CKTcircuit *ckt, char *name, INPmodel **model, INPtables *tab, char } /* Compare the per-finger W (already w/nf above) and L against - * the bin's wmin/wmax and lmin/lmax. Foundry .lib files (TSMC, - * GlobalFoundries, Samsung) author bin boundaries in POST-SHRINK - * (EFFECTIVE) dimensions: e.g. TSMC 22nm ULP nch.1 wmax=2.5651µm + * the bin's wmin/wmax and lmin/lmax. Foundry .lib files (foundry_a, + * foundry_c, foundry_b) author bin boundaries in POST-SHRINK + * (EFFECTIVE) dimensions: e.g. foundry_a 22nm ULP nch.1 wmax=2.5651µm * = drawn 3µm × shrink 0.855. The shrink is applied upstream in * subckt expansion (the subcircuit's `scale` parameter multiplies * the device W/L) or by the model's own dimension expressions, so @@ -425,7 +425,7 @@ INPgetModBin(CKTcircuit *ckt, char *name, INPmodel **model, INPtables *tab, char *model = modtmp; /* Stash the bin's (lmin, lmax) for OSDIsetup's deferred-eval * pre-pass, which needs a representative L within the bin's - * range to make Samsung-PDK expressions like + * range to make foundry_b-PDK expressions like * `(l==14n)*X + (l==16n)*Y` evaluate to non-zero. */ if (is_osdi) osdi_defer_record_bin_range(modtmp->INPmodName, lmin, lmax);