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/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/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/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 50260cd11..6e034b179 100644 --- a/src/maths/ni/niiter.c +++ b/src/maths/ni/niiter.c @@ -411,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- @@ -419,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). * @@ -426,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 diff --git a/src/osdi/osdisetup.c b/src/osdi/osdisetup.c index 6558ba70e..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); 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 c0908f592..7a8e8dd2a 100644 --- a/src/spicelib/analysis/dctran.c +++ b/src/spicelib/analysis/dctran.c @@ -773,8 +773,14 @@ resume: * 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. */ - if (ckt->CKTstate0 && ckt->CKTstate1) + * 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)); 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) {