Reviewer: Please look at openva_location.txt and justins_osdi_modification.txt

This commit is contained in:
Justin Fisher 2026-07-06 17:50:37 +02:00
parent 0246ba761a
commit 8fd86075c1
100 changed files with 9273 additions and 400 deletions

View File

@ -21,6 +21,12 @@ http://gareus.org/oss/spicesound/start
Example files are available at \examples\wave Example files are available at \examples\wave
Compiling for MS Windows, 64 bit, with VS2022 or VS2026:
Download https://github.com/libsndfile/libsndfile/releases/download/1.2.2/libsndfile-1.2.2-win64.zip
Expand libsndfile-1.2.2-win64 into same level as ngspice, rename the directory to libsndfile.
Download https://github.com/libsndfile/libsamplerate/releases/download/0.2.2/libsamplerate-0.2.2-win64.zip
Expand libsamplerate-0.2.2-win64 into same level as ngspice, rename the directory to libsamplerate.
Compiling for macOS M2 requires installing libsndfile and libsamplerate from Homebrew. Compiling for macOS M2 requires installing libsndfile and libsamplerate from Homebrew.
compile_macos_clang_M2.sh has been enhanced by adding compile_macos_clang_M2.sh has been enhanced by adding
-I/opt/homebrew/opt/libsndfile/include -I/opt/homebrew/opt/libsamplerate/include -I/opt/homebrew/opt/libsndfile/include -I/opt/homebrew/opt/libsamplerate/include

View File

@ -117,7 +117,7 @@ fi
echo "Removing files to be remade" echo "Removing files to be remade"
rm -f Makefile.in aclocal.m4 ar-lib config.guess config.sub rm -f Makefile.in aclocal.m4 ar-lib config.guess config.sub
rm -f depcomp install-sh ltmain.sh missing ylwrap rm -f depcomp install-sh install.sh ltmain.sh missing ylwrap
rm -r -f autom4te.cache rm -r -f autom4te.cache
echo "Running $LIBTOOLIZE" echo "Running $LIBTOOLIZE"

View File

@ -23,6 +23,7 @@ m4_define([ngspice_version],
# Initialization of configure # Initialization of configure
AC_INIT([ngspice], [ngspice_version], [http://ngspice.sourceforge.net/bugrep.html]) AC_INIT([ngspice], [ngspice_version], [http://ngspice.sourceforge.net/bugrep.html])
AC_CONFIG_AUX_DIR([.])
# Revision stamp the generated ./configure script # Revision stamp the generated ./configure script
AC_REVISION([$Revision: ngspice_version$]) AC_REVISION([$Revision: ngspice_version$])

View File

@ -4,14 +4,16 @@ There are two commits on top of the osdi-fixes branch.
The first commit tells ngspice that a .model line with level=77 means "use the bsimbulk OSDI device". Before this change, level=77 ended up with INPmodType = -1 and the model card got registered with an invalid type. I also added bsimbulk to the model-type allowlist in INP2M() so the M-prefixed MOSFET instance lines using a BSIM-BULK model don't get rejected with "incorrect model type". (The original fix in my main work tree also touched inpgmod.c for multi-bin support - that piece is unrelated to the wiring demonstration here so I left it out.) The first commit tells ngspice that a .model line with level=77 means "use the bsimbulk OSDI device". Before this change, level=77 ended up with INPmodType = -1 and the model card got registered with an invalid type. I also added bsimbulk to the model-type allowlist in INP2M() so the M-prefixed MOSFET instance lines using a BSIM-BULK model don't get rejected with "incorrect model type". (The original fix in my main work tree also touched inpgmod.c for multi-bin support - that piece is unrelated to the wiring demonstration here so I left it out.)
The second commit is a bug demonstration. The function model_numnodes() in src/spicelib/parser/inp2m.c returns the expected number of terminals for a given model type. Without an entry for bsimbulk and bsimcmg_va it falls through to the default "return 4", so the parser sets up only 4 GENnode slots on the instance. The OSDI runtime then reads its 5-port descriptor but port 5 doesn't exist, so it effectively rewires the body (or temperature) terminal to whatever happens to be in that memory slot. The second commit is a bug demonstration. The function model_numnodes() in src/spicelib/parser/inp2m.c returns the expected number of terminals for a given model type. Without an entry for bsimbulk and bsimcmg_va it falls through to the default "return 4", so the parser sets up only 4 GENnode slots on the instance. The OSDI runtime then reads its 5-port descriptor but port 5 doesn't exist, so it rewires the body (or temperature) terminal to whatever happens to be in that memory slot.
What this looks like in simulation: a PMOS body diode forward-biased through the supply rail. A simple CMOS inverter draws amps in steady state and fails to switch rail-to-rail. Build the binary at the first commit and you can see the bad behaviour; build it at the second commit and the wiring is correct. In simulation a PMOS body diode forward-biased through the supply rail. A simple CMOS inverter draws amps in steady state and fails to switch rail-to-rail. Build the binary at the first commit and you can see the bad behavior; build it at the second commit and the wiring is correct. Am I promising this is fool-proof? I am not! This, and everything I subsequently upload need to be THOROUGHLY tested by a 3rd party!!!
I also added bsimcmg_va to the model-type allowlist in INP2M() for consistency - same five-node BSIM-CMG OSDI device, same reasoning. I also added bsimcmg_va to the model-type allowlist in INP2M() for consistency - same five-node BSIM-CMG OSDI device with the same reasoning.
A note on the 5th pin. It's a thermal node, not just a temperature input. The model injects its own power dissipation as a current out of the node, and reads the resulting voltage back as a temperature rise above ambient (1 V at the thermal node = +1 K). So whatever you wire to T affects the device's actual operating temperature. You'd typically attach a thermal RC network (Rth to set steady-state degrees-per-watt, Cth to set thermal time-constant) or just short T to ground if you don't care about self-heating in this particular run. Leaving T floating gives a singular matrix during OP solve. A note on the 5th pin. As I understand it, it's a thermal node, not just a temperature input. The model injects its own power dissipation as a current out of the node, and reads the resulting voltage back as a temperature rise above ambient (1 V at the thermal node = +1 K). So whatever you wire to T affects the device's actual operating temperature. One would typically attach a thermal RC network (Rth to set steady-state degrees-per-watt, Cth to set thermal time-constant) or just short T to ground if you don't care about self-heating in this particular run. Leaving T floating gives a singular matrix during OP solve.
For the basic wiring demonstration on this branch the right move is to short T to ground on each device - that pins V(T) = 0 (ambient) and takes the self-heating dynamics out of the picture, so what we're testing is purely whether the body terminal lands on the actual body node instead of whatever stale value was in the OSDI node_mapping slot 5. Verifying self-heating with a real Rth/Cth is a separate test for another day. For the basic wiring demonstration on this branch, I think the thing to do is to short T to ground on each device - that pins V(T) = 0 (ambient) and takes the self-heating dynamics out of the picture, so what we're testing is purely whether the body terminal lands on the actual body node instead of whatever stale value was in the OSDI node_mapping slot 5. Verifying self-heating with a real Rth/Cth is a separate test for another day.
Bottom line - given my so far lack of interest in the thermal performance of the devices, I've not paid this much attention.
Justin. Justin.

962
justins_ngspice_changes.txt Normal file
View File

@ -0,0 +1,962 @@
Changes in NGSPICE
==================
This file started out life as a notepad of the changes I was making for OpenVA. As it turned out, there was a lot more work than I had anticipated, and this file is now a bit of a mess. Though I've done my best to get rid of any double entries or things I changed, then changed back.
It's worth pointing out that the testing I did with foundry models was specifically aimed at work I have a vested interest in. In other words, I know I can simulate with reasonably good accuracy in things like switching converters and Class D amplifier drivers using commercial processes with BSIM6 or HiCuM devices, but I've not looked at much else. Bandgaps, comparators, opamps etc - I've not had time to do a full sweep of this stuff.
Also worth pointing out - there are references made here to commercial foundry models. I used these for debugging, but due to strict NDA's, these can't be shared here. If you're wondering what references like "driver_lv_2v5_tb" are - those are my test benches that use commercial foundry files.
In OpenVA, I created a bunch of test files for each VA function, and those functions appear to be ok. That doesn't mean everything will work for you. It just means my tests worked. I'm sure there are bugs. I did test with some slightly more complicated VA models; some comparator models and some opamps with laplace functions for instance, and these all looked good to me. Again - your models might show up the limitations of this work.
Treat this document with a pinch of salt. It'll give you a reasonably good idea of what I did and why, but it's not a bible! I've tried to comment the code base as I've made changes - but I've NOT run any Spice regressions except the OpenVA tests I've been working on. I have double checked some past projects which do seem to work, so I'm reasonably confident that this branch doesn't break absolutely everything, but I'm not the NGSPICE grand guru, so I leave that up to those who know far more than I ever will.
==========================================
Summary
-------
This branch adds the simulator-side support for compiler-side `$limit` synthesis on OSDI Verilog-A models — the mechanism that lets unmodifiable CMC models (BSIM-BULK 106.2, PSP, HiCuM, ...) converge in transient analysis the way C-coded BSIM4 already does. The compiler-side work happens in OpenVA (the upstream-divergent fork of OpenVAF maintained in:
https://sourceforge.net/projects/openva/
NGSPICE provides the ABI, the runtime parameters, and the matrix-level safety nets that make the compiled limiters effective.
The original symptom that drove this work:
doAnalyses: TRAN: Timestep too small;
time = 1.00082e-07, timestep = 1.25e-21: cause unrecorded.
...during a two-stage 0.9V CMOS inverter chain (22nm BSIM6). The root cause turned out to be Newton divergence triggered by Miller-coupled fast turn-on of the stage-2 PMOS — the predicted iterate ran V(g) out to -1.8e34 V over twelve iterations, the BSIM-BULK eval went non-finite, sanitize_jacobian fired step rejection on every retry, dctran cut delta to 1e-20 with no recovery, abort. The fix is a combination of (a) compiler-side limiter clamping V() reads on the OpenVA side and (b) the simulator-side machinery below to wire that limiter, sanitize the residual/Jacobian path, and tame the timestep controller.
My goal across all of this work was to keep NGSPICE voltage-range-agnostic. No hardcoded `OSDI_V_HI = 1.8` / `OSDI_V_LO = -0.7` rails anywhere in the C — the same binary has to work for 0.9V core CMOS, 5V LDMOS, and 40V power devices without per-PDK tuning, the way commercial simulators do. I notice that there is no hard coded voltage levels in the Verilog-A files supplied by the big foundries, but commercial simulators can handle it, as can BSIM 4 in NGSPICE. So I wanted the same functionality for Verilog-A BSIM6 in NGSPICE as we already have for BSIM4.
STEP 1: src/include/NGSPICE/cktdefs.h
--------------------------------------
New fields on struct CKTcircuit_:
int CKTosdiStepReject /* set by OSDI eval / sanitize when the
iterate is structurally invalid */
int CKTosdiStepRejectOff /* `.option noosdistepreject` opt-out */
int CKThugeJThisIter /* count of |J_ij| > 1e6 entries clipped */
int CKTdtClearOff /* `.option nodtclear` opt-out */
double CKTosdiVlim /* generic per-iter Δv bound */
double CKTosdiVlimVds /* per-shape overrides */
double CKTosdiVlimVgs
double CKTosdiVlimVbs
double CKTosdiVlimNqs
REASON:
The OSDI runtime needs both transient-time flags (set during CKTload, read by NIiter) and persistent options (set by `.option` at parse, read by osdiload.c::get_simparams). Putting them on CKTcircuit_ keeps the data flow trivial: load handlers and convergence-rescue paths share state without globals.
STEP 2: src/include/NGSPICE/optdefs.h, src/spicelib/analysis/cktsopt.c
-----------------------------------------------------------------------
Enum + table additions for the new `.option` knobs:
OPT_NOOSDISTEPREJECT → `.option noosdistepreject` (flag)
OPT_NODTCLEAR → `.option nodtclear` (flag)
OPT_OSDIVLIM → `.option osdi_vlim = N` (real, V)
OPT_OSDIVLIM_VDS → `.option osdi_vlim_vds = N` (real, V)
OPT_OSDIVLIM_VGS → `.option osdi_vlim_vgs = N` (real, V)
OPT_OSDIVLIM_VBS → `.option osdi_vlim_vbs = N` (real, V)
OPT_OSDIVLIM_NQS → `.option osdi_vlim_nqs = N` (real, V)
REASON:
Two flag opts (noosdistepreject, nodtclear) gate the convergence-rescue paths in osdiload.c / niiter.c — left ON by default, opt-out for users who want raw failure modes. Five real opts feed the compiler-side limiter via the OSDI simparam cascade (see STEP 4). Default of 0 means "not set"; the cascade in osdiload.c falls back to a 0.1V generic value when nothing is configured.
STEP 3: src/maths/ni/niiter.c
------------------------------
Two new rescue paths inserted after the inner CKTload() call.
3a. Small-dt CKTnoncon clear (gated by !ckt->CKTdtClearOff)
At very small CKTdelta during transient, the integration coefficient 2/dt amplifies tiny charge changes into apparent currents that exceed osdiload.c's defensive 1 A clip. The clip's CKTnoncon++ then blocks NIconvTest, so a perfectly settled iterate cannot converge — NIiter spins until maxIter, dctran cuts delta, eventually aborts.
When CKTdelta < 1 ps and we are in MODETRAN, clear CKTnoncon so the downstream NIconvTest can evaluate solution stability directly. Genuine divergence still fires because NIconvTest tests |Δx| against (reltol·|x| + vntol) per node — a truly diverging iterate fails that test regardless of CKTnoncon state.
3b. Axis-3 step rejection
If CKTload set CKTosdiStepReject (because an OSDI model raised EVAL_RET_FLAG_REJECT_STEP, or sanitize_jacobian saw a NaN/Inf entry), return E_ITERLIM immediately so dctran cuts CKTdelta by 8 and retries with a better-conditioned predicted state. This is what lets the compiler-side limiter recover from a momentarily wild iterate — the model signals "this point is junk, give me a smaller step" instead of silently emitting garbage charges.
Both paths reset their flags (CKTosdiStepReject, CKThugeJThisIter) at the top of the iteration loop.
STEP 4: src/osdi/osdiload.c
----------------------------
This is the bulk of the OSDI runtime work.
4a. sim_params cascade (lines 28-68)
Extended NUM_SIM_PARAMS from 10 to 15. Added five names — osdi_vlim, osdi_vlim_vds, osdi_vlim_vgs, osdi_vlim_vbs, osdi_vlim_nqs — and a fallback cascade in get_simparams():
osdi_vlim_v = CKTosdiVlim > 0 ? CKTosdiVlim : 0.1
osdi_vlim_<S> = CKTosdiVlim<S> > 0 ? CKTosdiVlim<S> : osdi_vlim_v
The OSDI-compiled model reads these via `$simparam_opt("osdi_vlim_<S>", 0.1)`; the compiler emits per-probe-shape names so the user can tune each probe class independently via `.option`.
4b. sanitize_residuals (line 91)
Walks every node's resistive (and reactive, if transient) residual after eval(). NaN/Inf gets zeroed and bumps CKTnoncon — those are model anomalies and Newton needs to retry with a smaller step.
Finite-magnitude clamp is now at OSDI_I_MAX_FINITE = 1 GA. This is a backstop (don't panic!). No CKTnoncon++ on the magnitude clip. An earlier version used 1 A with CKTnoncon++; that broke convergence for power designs — a PMIC driver, automotive battery switch, or motor-control LDMOS array with m × nf in the thousands can legitimately stamp hundreds-to-thousands of amps on a terminal node, and clipping such currents to 1 A while flagging non- convergence left the Jacobian and residual inconsistent, blocking Newton at OP. Real anomalies still surface through the NaN/Inf path and through sanitize_jacobian's NaN handling (which raises CKTosdiStepReject).
4c. sanitize_jacobian (line 124)
Same idea but for Jacobian entries. NaN diagonal → gmin or, if a finite residual is available for the node, max(gmin, |I|/0.9) (keeps Newton step bounded to ~one supply rail per iter). NaN off-diagonal → 0. Finite |J_ij| > 1e6 S clipped to ±1e6 (diag) or 0 (off-diag).
On NaN, raises CKTosdiStepReject (unless `.option noosdistepreject`). NaN/Inf in a Jacobian entry means the model's eval diverged at the predicted operating point — no amount of clipping can save a falsified linearization; only retrying at a smaller predicted step will fix it.
4d. load(): reactive integration sanitization (lines ~250-300)
For each node's reactive residual, run NIintegrate to get q_dot, then:
- NaN/Inf integrated → reset state to 0, stamp 0, bump CKTnoncon
- residual_react == q_old_charge → TRAP would produce an artificial sign-flipped oscillation; zero the stamp to break it
- |q_dot| > OSDI_I_MAX_FINITE (1 GA backstop) → cap stamp, preserve true derivative in state for history; NO CKTnoncon increment (power devices legitimately swing reactive current at kA scale during fast switching — see 4b for the same rationale on the resistive path).
4e. EVAL_RET_FLAG_LIM and EVAL_RET_FLAG_REJECT_STEP handling (lines ~454-473)
The OSDI ABI return-flag handling. LIM (set by the compiler-emitted StoreLimit callback when a $limit clamp actually changed a probe value) increments CKTnoncon so Newton keeps iterating rather than accepting the clamped iterate as converged. REJECT_STEP raises CKTosdiStepReject (which niiter.c::3b consumes) and the simulator cuts delta on the next pass.
STEP 5: src/osdi/osditrunc.c
-----------------------------
A 2× per-step growth cap on the timestep returned by OSDItrunc.
REASON:
CKTterr is backward-looking: just before a switching transition it sees smooth, low-curvature charge history and recommends a step that spans the entire transition in one shot. That single huge step puts the predictor outside the model's validity envelope before the limiter ever gets to fire.
Cap growth at 2× the most recently accepted step (CKTdeltaOld[0]) so fast edges are approached gradually. This is a Spectre-style envelope control, not a hardcoded voltage bound — it remains voltage-range- agnostic because the criterion is purely relative (`max_step <= 2 * prev_step`).
STEP 7: src/osdi/osdiregistry.c
--------------------------------
Inside the limiter-symbol resolution loop in load_object_file: if a limiter entry already has func_ptr set, skip the canonical-name dispatch.
REASON:
OpenVA emits limiter implementations for any analog function tagged `(* osdi_limiter = "<name>" *)` — including custom names that have no meaning in the simulator. The compiled .osdi binary already binds the function pointer; the simulator must respect that binding and not warn about unknown names or overwrite the model-supplied pointer with a built-in (pnjlim, limvds, fetlim, ...). This is what lets a Verilog-A model entirely own its limiter implementation when it has one.
STEP 8: src/frontend/commands.c + src/frontend/inpcom.c
--------------------------------------------------------
This I got wrong the first time!
- commands.c: the earlier `pre_osdi` command alias (a second name for com_osdi) has been REMOVED. `pre_` is NGSPICE's documented command PREFIX (manual 13.5.56) -- `pre_osdi <file>` already means "run `osdi <file>` before the circuit is parsed", handled generically in inp.c by stripping `pre_` and queueing the remainder. Registering `pre_osdi` as a command name was redundant and collided semantically with that prefix. Our .control decks now use the plain `osdi` command (loaded before the `source` of the netlist), which is the correct form for a control script; the documented `pre_osdi` PREFIX form remains available for netlist-body use via inp.c. All testbench decks + the tests/bin/NGSPICE.pm harness were swapped pre_osdi -> osdi.
- inpcom.c keeps `osdi` AND `pre_osdi` in the case-preservation exception list (so file-path case survives the preprocessor's lowercasing) -- the `pre_osdi` entry still serves the legitimate documented PREFIX form. Also adds `set sourcepath` to that list for the same reason.
STEP 9: src/spicelib/parser/inpgmod.c + related
------------------------------------------------
OSDI model handling at parse time:
- INPgetModBin: support W-binning of OSDI models with optional wmin/wmax. Bin selection compares per-finger W only (w/nf); `m` is NOT applied — it is a parallel-instance multiplier, not a per-finger subdivider, so the per-finger geometry that the bin tables are written against is unchanged by it. An earlier version of this fork did divide by m here; that turned out to push moderately-large multi-instance devices (e.g. m=266 power PMOS arrays) below every bin's lower W bound and broke binning silently. 22nm BSIM6 and BCD BSIM-BULK decks author wmin/wmax against the commercial simulators per-finger convention.
- INPgetModBin: bins are authored in EFFECTIVE (post-shrink) dimensions; instance W/L arrive already effective (the commercial simulators subckt `scale` is applied to the device expressions before numparam, see STEP 31), so bin matching compares w/l directly. A second-pass nearest-bin fallback handles legitimate over-range cases (e.g. drawn L below the foundry minimum) with a Warning print.
- INPdevParse: skip unknown instance parameters silently for OSDI models instead of erroring out — necessary because the OSDI parameter set comes from the compiled binary, not from a static IFparm[] table
- parser fix for BSIM-BULK (level 77) model type lookup and bin selection — upstream's MOSFET path assumed BSIM4 was the only level using `nmos`/`pmos` keywords
- numparam: suppress error messages for `.option` lines with unknown parameters (compatible with commercial simulators decks that probe options we don't recognize) and ignore substitution errors for string-valued options
- INPdoOpts: deduplicate unknown-option warnings and skip the `=value` token after a flagged unknown name
- INPdoOpts: a small static allowlist of commercial simulators options that we recognize but do not yet implement (`tmiflag`, `modmonte`, `tmipath`, `etmiusrinput`) is matched case-insensitively and downgraded from `Error:` to `Warning:` in the unknown-option message, so users can tell a known-pending feature apart from a real typo at a glance. Extend the array in inpdoopt.c when a new such option appears in foundry decks.
STEP 10: src/osdi/osdi_defer.{c,h} + src/osdi/osdisetup.c
+ src/frontend/numparam/{spicenum.c,xpressn.c,numparam.h}
-------------------------------------------------------------------
Deferred evaluation of `.model` card `{...}` expressions that reference per-instance geometry symbols (`l`, `w`, `nf`, `m`, `xnf`, `l_calc`).
The need: commercial simulators and Spectre let a `.model` card author per-bin parameters as expressions over the instance geometry, e.g.
.model egnfet_s2 nmos (level=72 ...
+ eotacc = {(l<=1e-7)*4.04e-9 + (l>1e-7)*(l<=1.5e-7)*(...) + ...}
+ rdsw = {(xnf<=3)*215.93 + (xnf>3)*(xnf<=4)*(...) + ...}
+ ... ~20 more)
These expressions reference symbols that are not bound at parse time — they are populated per instantiation from `l=...`, `w=...`, `nf=...` on each device line. commercial simulators defers the evaluation to instance-bind time and substitutes the resolved values. NGSPICE's numparam, in contrast, evaluates `{...}` eagerly at parse and errors out with `Undefined parameter [l]`, blocking every foundry PDK card that uses this idiom (14nm FinFET, 22nm BSIM6, BCD, ...).
Mechanism (four layers, three of them new):
1. PARSE-TIME DETECTION (numparam preprocessor). Before numparam walks a `.model` card and turns each `{...}` into a `numparm____` MARKER, `osdi_defer_preprocess_line()` scans the *original* line (`dicoS->dynrefptr[linenum]`, NOT `card->line` which has already been rewritten with MARKERs). For each `{body}` or `'body'` whose body contains a bare identifier in the recognized set (`l`, `w`, `nf`, `m`, `xnf`, `l_calc`), it:
- registers `(model_name, param_name, body)` in a static linked list (`g_defer_head`) for later use, and
- rewrites the brace body in-place to `{0}` so numparam still sees a syntactically-valid expression, evaluates it to 0, and substitutes that 0 into the MARKER slot.
Only level 72 (BSIM-CMG) and level 77 (BSIM-BULK) model cards participate; extend `osdi_defer.c` if new OSDI MOS levels appear. `expr_refs_instance_geom()` does identifier-boundary checks so `slope`, `nfb`, etc. don't trigger false positives.
2. EVAL-WITH-SCOPE HELPER (xpressn.c). `nupa_eval_with_scope()` pushes a fresh symbol-table scope onto numparam's stack, populates it with `(name, value)` pairs via `attrib()`, runs `formula()` to evaluate an expression in that scope, then pops and frees the scope. No persistent side effects — does NOT promote any names to the qualified-name table. Used by the OSDI-side eval (layer 3) and by anything else that needs a transient binding (commercial simulators-style runtime expressions).
An external accessor `nupa_get_dico()` exposes the static `dicoS` for callers outside numparam (e.g. `osdi_defer.c`) that need to invoke `nupa_eval_with_scope()` without seeing the symbol directly.
3. MODEL-LEVEL PRE-EVAL (osdisetup.c::OSDIsetup). Before the first `setup_model()` callback runs on a BSIM-CMG / BSIM-BULK model, `OSDIsetup` walks the deferred-table for that model and writes each evaluated expression into the *model* storage slot at a representative default geometry (L=30nm, W=100nm, NF=1, M=1). Chosen to land in the middle of typical FinFET bins so range- check expressions of the form `(l<=1e-7)*A + (l>1e-7)*B` etc. produce in-range physical values. Without this, setup_model's parameter-range validation rejects the `0` placeholder that layer 1 wrote (BSIM-CMG aborts with `Parameter EOTACC is out of bounds!` etc.). Per-instance values override in layer 4.
4. PER-INSTANCE BIND (osdisetup.c::OSDItemp). In the inner loop after `setup_model()`, OSDItemp reads each instance's actual `l`, `w`, `nf`, `m` via `read_inst_real()`, then re-evaluates every deferred expression at those values and installs the result onto the instance slot via `write_inst_real()` with `ACCESS_FLAG_SET | ACCESS_FLAG_INSTANCE`. This is what makes two instances of the same model with different L/W produce correctly-binned parameter values, the way commercial simulators do it.
Why both a model-level default and a per-instance bind: setup_model runs before any instance is examined, so it cannot see real geometry — but its range-check is what raises bounds errors. Layer 3 satisfies the check with a representative default. Layer 4 then installs the real per-instance values onto the instance storage slots (which OSDI's `descr->access()` keeps separate from model storage), so the actual eval inside setup_instance and the load callbacks sees the correct per-instance L/W/NF.
Helper layout in osdisetup.c: `find_param_idx()`, `read_inst_real()`, `write_inst_real()`, `write_model_real()`, plus the two `apply_deferred_param*` callbacks and their context structs, all live above `OSDIsetup` (forward visibility) and are shared by both OSDIsetup and OSDItemp.
xnf and l_calc: BSIM-CMG model cards use `xnf` as a synonym for `nf` (commercial simulators convention) and `l_calc` as the foundry-computed effective length (`l + p_la` in the subckt wrapper). The eval-scope binds `xnf = nf` and `l_calc = l` so cards that reference these symbols evaluate correctly.
Validated end-to-end on a bare 14nm FinFET inverter (no PDK subckt wrapper): 22 deferred params per model resolve cleanly at default geometry, no bounds errors, transient runs to completion. Pre-existing BSIM-BULK paths are unaffected: the preprocessor returns early for any model card without `{...}` braces in deferred-eligible param positions, and `osdi_defer_has()` returns false for them at setup time, skipping the whole pre-eval / re-bind machinery.
ADDITIONS (2026-05-19 — 14nm FinFET full PDK end-to-end work):
10a. SUBCKT-SCOPE SNAPSHOT. commercial simulators-style PDKs (14nm FinFET et al.) put `.model` cards INSIDE a `.subckt` body and let the model's expressions reference subckt-scope `.param` symbols. For 14nm FinFET's nfet bin:
vsat1 = ((281630)+(181990.0)*exp(-1*(l_calc+xl_nfet)/
(4.7639e-008)))*(1+vsat_nfet/11259)*velsat_mult
`xl_nfet`, `vsat_nfet`, `velsat_mult` are passed to the subckt via its `params:` list and resolved per-instance at subckt expansion time. They are not in the global .param dictionary, and the layer-2 `nupa_eval_with_scope()` only pushes the six reserved instance-geom symbols. Without a snapshot the expression fails with `Undefined parameter [xl_nfet]` and defaults to 0, leading to runaway BSIM-CMG behavior (VSAT1=0 gives unbounded drift current, source-stepping drives 18854V on gate nodes, full Newton blowup).
Fix: `osdi_defer_register()` now snapshots subckt-scope bindings AT REGISTRATION TIME. By that point the surrounding subckt has been expanded per-instance, so `entrynb(dico, ...)` resolves identifiers like `xl_nfet` to the value bound by THIS subckt instance's params list. The snapshot is per-entry (each subckt instance produces its own per-instance .model card with a unique prefix like `x2.xmn1:nfet.0`, so each gets its own snapshot — multi-flavor PDKs work correctly without cross-instance contamination).
Implementation:
- `OsdiDeferEntry` extended with `(n_snap, snap_names, snap_values)`.
- `capture_scope_snapshot()` walks the expression for identifiers, skips reserved instance-geom names AND known math keywords (exp, log, sqrt, etc.), looks each up via `entrynb()` (walks the dico's scope stack), and snapshots NUPA_REAL values into the entry.
- `osdi_defer_eval()` looks up its entry by `expr_text` pointer equality (caller got the pointer from us via `osdi_defer_for_model`'s callback), then pushes snap (lower priority) + base l/w/nf/m/xnf/l_calc (higher priority — base wins on name conflict) into one merged scope before calling `nupa_eval_with_scope()`.
- `osdi_defer_clear()` frees the snap arrays.
Behavior for non-affected paths:
- BSIM4 / BSIM3 / HiSIM / HiSIM_HV / native MOS: never enter `osdi_defer_preprocess_line()` (level filter rejects them) → never register → entire mechanism is a no-op.
- OSDI models whose .model expressions don't reference subckt-scope params (most level-72 / level-77 PDKs): snap captures nothing → eval falls into the original code path → identical behavior.
10b. BIN-RANGE SYMMETRIC PREFIX STRIP. `INPgetModBin` records bin ranges keyed by `modtmp->INPmodName`, which carries the subckt-path prefix (`x3.xmn1:nfet.0`). `osdi_defer_get_bin_ range()` originally only stripped the prefix on the QUERY side — so a runtime lookup for `x3.xmn1:nfet.0` against a stored `x3.xmn1:nfet.0` produced `strcasecmp("x3.xmn1:nfet.0", "nfet.0")` and missed. Without the bin range, OSDIsetup fell back to a hardcoded `default_l = 30e-9` for model-level pre-eval, which made 14nm FinFET's `(l==14n)*X + (l==16n)*Y`-style expressions return 0 (neither equality test fires at 30nm), and BSIM-CMG's setup_model auto-bumped MEXP=0 → 2 with a warning. Fixed by stripping the `<subckt-path>:` prefix on BOTH sides of the comparison.
10c. FLOAT-TOLERANT `==` / `<>` IN NUMPARAM. See STEP 12 for the fix in `xpressn.c::operate`. The relationship to deferred eval: even with snap (10a) and bin range (10b) in place, `(l == 0.014e-6)` evaluated to 0 because bin midpoint `0.5*(1e-8+1.8e-8) = 1.3999999999999999e-08` is 1 ULP off from `0.014e-6 = 1.4e-08`, and instance `l=14n` parses as `14 * 1e-9 = 1.4000000000000001e-08` (also 1 ULP off). Strict IEEE `==` fails in both directions; commercial simulators-compatible tolerance lets both shapes match. Without all three fixes, 14nm FinFET fails Newton convergence even though every individual layer looks correct.
STEP 11: src/spicelib/parser/inp2m.c (model_numnodes)
------------------------------------------------------
Add OSDI BSIM-BULK and BSIM-CMG to `model_numnodes()` returning 5:
if (type == INPtypelook("bsimbulk") || /* 5 ; (d,g,s,b,t) */
type == INPtypelook("bsimcmg_va")) /* 5 ; (d,g,s,e,t) */
{
return 5;
}
Without this, the parser fell through to the default `return 4` for OSDI BSIM-BULK / BSIM-CMG model types. The two functions in `INP2M()` that consume this count then bound only 4 ports on the GENinstance — the 5th port (temperature / extra contact) was left unbound. Empirically this manifested as drain/source mis-bind on PMOS instances of BSIM-BULK (symptom: simple CMOS inverters drew many amperes in steady state and failed to switch rail-to-rail — the body diode forward-biased through the supply).
The fix aligns the parser's port count with the OSDI device's declared port count. With model_numnodes=4, the loop at inp2m.c:158 set up only 4 GENnode slots; the OSDI runtime's node_mapping[] then read past the bound region for port 5 and picked up a stale/wrong node index, effectively short-circuiting the body to an unrelated node. With numnodes=5, port 5 is explicitly initialized (to -1 if the instance line has only 4 node tokens, the conventional "unconnected" default) and the runtime's mapping is well-defined.
This was the root cause of something that happened earlier: BSIM6 / BSIM-BULK "PMOS inverter drawing amps"
STEP 12: src/frontend/numparam/xpressn.c (operate)
---------------------------------------------------
commercial simulators-style tolerance for `==` (case `'='`) and `<>` (case `'#'`) on floating-point operands. Strict IEEE bit-equality breaks foundry-PDK expressions of the form `(l == 0.014e-6) * X + (l == 0.016e-6) * Y` because instance `l=14n` parses as `14 * 1e-9 = 1.400000000000001e-08` (1 ULP above 1.4e-08), and the bin midpoint `0.5*(1e-8+1.8e-8) = 1.39999999999999e-08` (1 ULP below). Either way the literal `0.014e-6 = 1.4e-08` exact comparison fails and the expression returns 0.
New equality rule (applied to both `==` and `<>` symmetrically):
|x - y| <= max(|x|, |y|) * 1e-9 OR |x - y| < 1e-18
- 1e-9 relative tolerance is generous enough to absorb several ULPs of accumulated rounding through a chain of `*` / `+` / `exp` operations.
- 1e-18 absolute floor handles values right at zero (no relative scale to compare against).
- Integer comparisons stay unambiguous: `mexp == 2` checks the distance between e.g. 1.0 and 2.0, which is 100% relative — far beyond 1e-9.
Verified safe for:
- 14nm FinFET tb_driver: 236,939 transient rows, 0 errors.
- 22nm BSIM6 ULP driver_tb: 2,505 rows, 0 errors (was passing before).
- 22nm BSIM6 ULP driver_lv (with `osdi` loader added): 2,280 rows.
- 22nm BSIM6 RF driver (with `osdi` loader added): 2,224 rows.
- BCD sample test: see STEP 17 — passes after two additional numparam fixes (`*+` and `var`-as-identifier).
STEP 13: src/spicelib/devices/hisimhv2/hsmhv2ld.c
--------------------------------------------------
NaN-guards around HiSIM_HV (LDMOS, level=73) load function. The testbench symptom: at the first DC OP iteration of a foundry deck that mixes HiSIM_HV LDMOS (5V driver output stage) with BSIM-CMG core CMOS, the LDMOS load function received NaN voltage inputs from `rhsOld[]` because upstream BSIM-CMG instances had produced NaN solutions during source stepping (caused by VSAT1=0 — see STEP 10's deferred-eval failure modes, since fixed). Once a NaN enters the solution vector, HiSIM_HV's 5000+-line eval cascades it through every subsequent device.
Pragmatic guards added (each with a `static int n = 0; if (n++ < 20)` cap on diagnostic printf to prevent log explosion):
- Input-side guard in the else / Newton branch (around line 645): vbs/vgs/vds/vges/vdbd/vsbs/vbse/vgse/vdse/vsubs/deltemp NaN → state0 fallback, with hard non-NaN defaults if state0 itself is degenerate (0/0/0) or NaN.
- MODEINITJCT off-mode path (around line 508): bump zero-voltage sentinel to 0.1 V instead of 0.0 V to keep junction-diode log arguments finite.
- Linear-branch voltage NaN-guard (line 1165): vddp / vggp / vssp / vbpdb / vbpb / vbpsb (the device's internal-branch voltages derived from rhsOld) NaN → 0.0 before being passed to HSMHV2evaluate / HSMHV2rdrift / HSMHV2dio. This was the guard that finally broke the feedback loop: at iteration 2 (MODEINITFIX) `rhsOld[dNode] - rhsOld[dNodePrime]` would propagate NaN into the eval, the eval would return clean values for ids/gm/qd but rdrift would compute Rd from the NaN vddp directly, producing Rd=-nan and re-injecting NaN into the stamp.
- Output-side guard after HSMHV2evaluate: zero NaN entries in HSMHV2_ids, _dIds_dVdsi, _dIds_dVgsi, _qd, _qg, _Rd, _Rs etc. before they're stamped into the matrix.
Validated with 14nm FinFET's ld3nfet (HiSIM_HV type=28, 5V LDMOS driver): pre-eval shows vddp=0 (post-guard) instead of NaN, eval returns finite ids=186µA at DC OP, transient runs to completion.
STEP 14: src/frontend/inpcom.c (parser performance + commercial simulators compat)
--------------------------------------------------------------------
Several parse-time performance and commercial simulators-compatibility fixes that were needed for loading large foundry PDKs end-to-end. 14nm FinFET's tt-corner deck is ~190k lines and triggered every one of these as a hot path.
- `inp_sort_params` O(N²) → O(N) hash. Two nested-N² loops (duplicate-detection + dependency-scan via `search_plain_identifier`) replaced with a single `NGHASHPTR` from param_name → (i+1) used for both passes. 14nm FinFET's TT corner calls this ~500 times with N up to 2700; pre-fix this was the dominant runtime cost (~10 min of total parse). Now O(N) + O(Σ expression chars) per call.
- `find_function` O(N) → O(1) hash. Added `NGHASHPTR fcn_hash` to `struct function_env`, populated alongside the existing linked list of `.func`s. `find_function` was being called once per `(` per line during macro expansion (millions of times). 14nm FinFET's TT corner loads ~1300 `.func`s into env at top level, so the old linear scan was averaging ~1300 strcmps per call. Pre-fix runs hit 7.6M calls × ~1300 = ~10 billion strcmp operations. With the hash, lookup is O(env_chain_depth) per call. The linked list is kept for ownership/iteration; freed last in `delete_function_env`.
- `$`-as-end-of-line comment relaxed in newcompat.hs mode. Previously `inp_stripcomments_line` required ` `, `,`, or `\t` immediately before `$` for it to be recognized as a comment. 14nm FinFET's `nwres.inc:62`-style `...)'$ comment` has `'` (single quote) immediately before `$` and so was NOT stripped, leaking `$` into the brace expression handed to numparam and producing a cascade of `Preparing expression for numparam / What is this? / $)*(...)` errors. In HS mode `$` is now accepted as a comment regardless of preceding char. Non-HS modes are unchanged (conservative).
- `inp_casefix` keepquotes preserves inner content case. When the `keepquotes` flag was set, the function preserved the surrounding `"` characters but still lowercased the content between them. Foundry PDKs put case-sensitive file names inside `str("...")` calls (e.g. `str("./RF_COMPONENTS/ egnfet_SHE.table")`); lower-casing breaks fopen on case- sensitive file systems. Added `string++` in the keepquotes branch so the while-loop's inner-skip advances to the closing quote and preserves the inner content.
- `inp_read` line-by-line lowercaser made quote-aware. Before `inp_casefix` runs, `inp_read`'s line-by-line lowercaser unconditionally lowered every char on the line (with a small `.lib`/`.inc`/control-cmd exception list). `.param` lines containing `table_param(str("..."))` fell into "all other lines" and got their path arguments lowercased. Added `in_dq` quote tracking so chars inside double-quoted strings are preserved.
- Comfile `*ng_script` detection past leading blank lines (3 places: `inpcom.c::inp_read`, `inpcom.c::inp_readall`, `inp.c::inp_spsource`). Previously the test inspected only the FIRST line read from the file. If the file had leading blank line(s) (14nm FinFET's `tran` control file does), comfile detection failed and the file was treated as a spice deck. The `.control { source other.net }` block STILL ran (which is why things "worked"), but it triggered a SECOND full inp_readall/inp_spsource pass for the inner netlist — doubling parse time and producing two `Note: Compatibility modes` / `Circuit:` headers. Now: walk past leading blanks before the `ciprefix("*ng_script", ...)` test.
STEP 15: src/frontend/numparam/table_param.{c,h}
+ src/frontend/numparam/xpressn.c::formula() (XFU_TABLE_PARAM)
+ src/frontend/numparam/spicenum.c (cardsource plumbing)
------------------------------------------------------------------
commercial simulators's `table_param()` function — multi-dimensional lookup from an external `.table` file with linear interpolation. 14nm FinFET's PDK uses it ~1300 times across `fets.lib`, `fets_rf.lib`, `fets_hf.lib`, etc. for self-heating thermal-resistance lookup (e.g. `rth0_n = table_param(str("./RF_COMPONENTS/egnfet_SHE.table"), 2, xnf_clamp, nfin_clamp, 1, l_clamp, 1)`). Without this, every RF/self-heating param ends up undefined and the cascade kills PDK read.
Implementation:
- `table_param.c/h` (new): parses the .table file (header line declares column names + types, data rows follow), caches the parsed result in an `NGHASHPTR` keyed by filename, does multi- dim interpolation (integer keys exact-match, real keys linear-interpolate between bracketing rows; tensor-product multilinear for n_real > 1), clamps out-of-range queries (matches commercial simulators's no-extrapolation policy).
- `xpressn.c::formula()`: new XFU_TABLE_PARAM enum entry and `else if (fu == XFU_TABLE_PARAM)` branch. Parses variadic args (up to 64), strips the `str(...)` wrapper and quotes from the filename arg, evaluates the remaining args via recursive `formula()`, and calls `table_param_lookup()`.
- `spicenum.c::nupa_eval`: sets `dicoS->cardsource = card->linesource` so xpressn's XFU_TABLE_PARAM dispatch can derive `dir_hint` from the calling card's source-file directory.
- `table_param_lookup` path resolution order: absolute → `inp_pathresolve` (sourcepath + cwd) → relative to `dir_hint` (the .lib's directory — matches commercial simulators's behavior of resolving relative paths against the file that emitted the call) → relative to `inputdir`.
`numparam.h` adds the `cardsource` field to `dico_t` and the `nupa_skip_line()` declaration.
STEP 16: src/frontend/subckt.c (X-prefix → OSDI VA-module fallback)
--------------------------------------------------------------------
14nm FinFET's PDK uses `X<name>` to instantiate both subckts AND Verilog-A modules (commercial simulators convention). NGSPICE's `inp_subcktexpand` only knew how to handle `X<name>` → subckt; an X-instance whose target is a registered OSDI device emitted "Cannot find subcircuit" and aborted the deck.
Fix: when an `X<name>` instance's target type isn't a `.subckt`, call `INPtypelook(target)` on the token after the instance's nodes. If it's a registered OSDI device (e.g. `esd_nfet_monitor` after the testbench loads `esd_modules.osdi`), comment out the line and warn once per session. The commercial simulators convention is that `X<name>` calls either a `.subckt` OR a Verilog-A module; NGSPICE's expander only handled the former. For diagnostic-only modules (ESD monitors emit `$strobe` for spec violations, no current/ charge contributions) the comment-out is semantically safe.
A more complete fix would auto-synthesize a `.model + device-prefix` shim so the OSDI dispatch can attach to a non-MOSFET VA module, but that needs the OSDI runtime to accept generic-prefix instances; left for a future change.
The X-prefix fallback also calls `nupa_skip_line(linenum)` to mark the now-commented line inert from numparam's perspective — otherwise numparam still tries to dispatch the line as a subckt call ("X" category) and emits "illegal subckt call" / "Cannot find subcircuit".
STEP 17: src/frontend/numparam/xpressn.c (formula parser dispatch)
-------------------------------------------------------------------
Two BCD read fixes in the inner formula-parser loop. Both were uncovered by running the BCD sample `NLDMOS`, which loads `fixed_corner_bcdlite.inc` (twelve-way switch chains of the form `(sw0)*(0)+(sw1)*(0)+ (sw2)*(±X)+...+(sw11)*(0.67*±X)`) and `diode_rr.inc` (subckt parameters with chained references including a parameter named `var`). Before these fixes the sample produced 443 errors / 180 "Expression err". After: 0 errors, 1,212 transient rows.
17a. UNARY `+` AFTER A BINARY OPERATOR. The state-machine pair
check at the top of the inner loop handles `binop binop -` specially — it absorbs the `-` as a unary-minus on the next atom (sets `negate = 1`, `continue`):
if (oldstate == S_binop && state == S_binop && c == '-') {
ok = 1;
negate = 1;
continue;
}
There was no symmetric branch for unary `+`. `5*+3`, `0.67*+2e-8`, `1+2*+3` all failed with "Misplaced operator" while `5*-3`, `0.67*-2e-8`, `1+2*-3` parsed fine. Foundry BCD's fixed-corner switch chains use explicit `+` signs for readability (`(sw4)*(0.67*+0.08)` next to `(sw5)*(0.67*-0.08)`), so the asymmetry blocked the whole PDK.
Fix: add the symmetric branch right below the `-` one. No `negate` flag needed — unary `+` is a no-op sign; just absorb the token and continue.
17b. FUNCTION-KEYWORD VS. IDENTIFIER DISAMBIGUATION. When numparam
sees an identifier in the formula loop, it tests it against `fmathS` (the list of built-in function names: sqrt, sin, exp, ..., min, max, pow, var, vec, table_param, etc.). If the name matches, `state = S_init` waits for the `(` to start argument parsing.
But the code did NOT check whether `(` actually follows. If the identifier happens to match a keyword but is being used as a parameter name (no `(`), the parser silently dropped it — leaving `state = S_init` with no atom on the stack, and produced "Expression err: <name>" / "Cannot compute substitute" downstream.
Foundry decks use this pattern routinely. A foundry BCD diode_rr.inc declares the subckt parameter chain:
+ var = 0.0
+ vrb = 'var'
+ vb = 'vrb'
When numparam evaluates `vrb='var'`, the identifier `var` matches the built-in `var(...)` (read an NGSPICE variable) and gets classified as a function, then never resolves.
Fix: peek past the identifier and whitespace; only treat as a function call if the next non-space char is `(`. Otherwise fall back to `fetchnumentry()` for the parameter lookup, and reset `fu = 0`.
Verified together:
- Foundry BCD sample: 1,212 rows, 0 errors (was 443 errors with 180 Expression err before this session).
- 14nm FinFET tb_driver: 236,340 rows, 0 errors, 0 warnings — unaffected (no `*+` or keyword-as-identifier uses).
- 22nm BSIM6 ULP driver_tb: 2,505 rows, 0 errors — unaffected.
Both fixes are conservative. 17a only fires when `state` is already advancing from binop to binop, and only adds an absorbing branch for `+` that mirrors the existing `-` branch. 17b only changes behavior when an identifier matches a function keyword AND has no `(` following — previously a silent failure, now a working identifier lookup.
STEP 18: src/osdi/osdi.h, osdiregistry.c (OSDI 0.5 ABI bump)
-------------------------------------------------------------
S3a of the OSDI 0.5 event-driven operators rollout — see https://sourceforge.net/projects/openva/ /OSDI_0_5_DESIGN.md for the full design. The 0.5 minor bump adds the simulator surface that compiler-side emission of `last_crossing`, `@(cross())`, `@(timer())`, and `@(initial_step)`/`@(final_step)` needs. All new fields are appended (struct tails), so 0.4 binaries still load against the 0.5 layout — the loader's bounds-checked descriptor-size read in osdiregistry.c handles the size mismatch gracefully.
18a. EVAL_RET_FLAG additions and version bump.
OSDI_VERSION_MINOR_CURR: 3 → 5. New return-flag bits the model ORs into the eval-return word: EVAL_RET_FLAG_EVENT = 32 — model populated entries in sim_info->pending_events[] this eval and wants the runtime to schedule them. EVAL_RET_FLAG_CROSS = 64 — model wrote one or more cross_expr[] slots this eval and wants the runtime to run direction-aware sign-flip detection against the previous accepted iterate. Existing flags (LIM=1, FATAL=2, FINISH=4, STOP=8, REJECT_STEP=16) are unchanged. 32/64 was chosen during the design phase (originally 16/32) specifically to dodge the existing REJECT_STEP=16 bit — collision-free is more important than contiguous numbering.
18b. OsdiSimInfo tail extension (4 new fields appended at offset 72):
double *cross_expr — per-instance array sized by descr->num_cross_exprs, lazily allocated by the runtime. OsdiEventRequest *pending_events — per-instance scratch array sized by descr->num_event_slots, written by the model when it wants to request a future event. uint32_t at_scheduled_event — non-zero on the eval where the runtime fires a previously- scheduled event. Functions as a bool widened to u32 for ABI stability. uint32_t fired_event_id — meaningful only when at_scheduled_event != 0; indexes descr->event_slot_metadata. sizeof(OsdiSimInfo) grows from 72 to 96 bytes.
18c. New ABI structs (declared inline above OsdiDescriptor):
OsdiEventRequest { double at_time; uint32_t event_id; uint32_t kind; } — model→runtime: "fire an event of `kind` (CROSS/TIMER/...) at simulator time `at_time` with discriminant `event_id`." OsdiCrossExprMeta { int32_t direction; double ttol; double etol; uint32_t event_id; } — per-cross_expr metadata: direction is +1 / -1 / 0 (rising / falling / any), ttol/etol are the desired bisection tolerances (0.0 = simulator default), event_id links the cross slot to a downstream event_slot. OsdiEventSlotMeta { uint32_t kind; char *name; } — per-event- slot metadata: kind is OSDI_EVENT_KIND_CROSS / TIMER / INITIAL_STEP / FINAL_STEP, name is a diagnostic string surfaced in logs.
18d. OsdiDescriptor tail extension (4 new fields after the 0.4 v0.4
ABI block): uint32_t num_cross_exprs; OsdiCrossExprMeta *cross_expr_metadata; uint32_t num_event_slots; OsdiEventSlotMeta *event_slot_metadata; sizeof(OsdiDescriptor) = 344 bytes.
18e. OSDI_EVENT_KIND_* discriminants — CROSS=1, TIMER=2,
INITIAL_STEP=3, FINAL_STEP=4. Used by the runtime to dispatch the right body when a scheduled event fires.
18f. osdiregistry.c: hard-coded the v0.3 strict-equality compatibility
check that previously used the symbolic OSDI_VERSION_MAJOR_CURR / MINOR_CURR macros. Bumping those macros to 0/5 would have made the "original OpenVAF v0.3" detection branch dead code, so the literal `0/3` is now used instead — the branch's purpose is to identify true 0.3 binaries, not "the current ABI."
S3a is purely declarative. The runtime that acts on these new fields ships in STEP 19.
STEP 19: src/osdi/osdiload.c, osditrunc.c, osdidefs.h (OSDI 0.5 event runtime)
-------------------------------------------------------------------------------
S3b — the per-instance event-state machine that turns the new SimInfo fields and ret-flag bits into actual behavior. Built on top of the STEP 18 declarations; nothing else in NGSPICE needs to change to support a 0.5-aware model.
19a. OsdiExtraInstData extension (osdidefs.h). Per-instance state
the runtime maintains independent of the model: double *cross_expr_arr — array of size num_cross_exprs, aliased into sim_info->cross_expr every eval so the model writes here directly. double *prev_cross_expr_arr — value at the previous accepted eval; the runtime compares cross_expr_arr against this to detect direction-matched flips. OsdiEventRequest *pending_events_arr — model-writable scratch for new event requests. OsdiEventRequest *scheduled_events — sorted-by-at_time queue of events the runtime has already accepted from pending_events_arr and not yet fired. uint32_t scheduled_count — live entries in scheduled_events. bool cross_init — guards flip detection until at least one prior eval has latched prev_cross_expr_arr. ExtraInstData grows from 24 to 64 bytes; all-zero default is a valid quiescent state (NULL pointers, no scheduled events).
19b. osdi_event_prepare() (osdiload.c). Runs immediately before
descr->eval each call:
- lazy-allocates cross_expr_arr / pending_events_arr / scheduled_events the first time num_cross_exprs or num_event_slots is non-zero for an instance,
- wires sim_info_local->cross_expr / pending_events to point at the per-instance arrays,
- peeks the front-of-queue scheduled event: if its at_time matches the current abstime within 1 ps, sets at_scheduled_event=1, fired_event_id=meta.event_id, and pops the event.
19c. osdi_event_postprocess() (osdiload.c). Runs immediately after
descr->eval returns:
- if eval_flags & EVAL_RET_FLAG_EVENT, drains pending_events_arr[] into scheduled_events (sorted insert by at_time);
- if eval_flags & EVAL_RET_FLAG_CROSS and cross_init is true, compares cross_expr_arr[i] vs prev_cross_expr_arr[i] direction-aware (meta.direction > 0 needs prev<0 && curr>=0, etc.) and on a flip schedules a CROSS event at the current abstime (S3c will refine this to true bisection);
- latches cross_expr_arr → prev_cross_expr_arr and sets cross_init = true.
19d. eval() wrapper (osdiload.c). Wraps each descr->eval call in
osdi_event_prepare / postprocess, using a per-eval mutable OsdiSimInfo copy (sim_info_local) so the cross_expr / pending_events pointer wiring is per-instance and safe under OMP.
19e. OSDItrunc dt clamp (osditrunc.c). Before letting the LTE
truncation routine grow dt unboundedly, peek the front of scheduled_events for each instance. If a scheduled event is in the future and closer than the LTE-proposed timestep, clamp dt so the next step lands exactly on the event time. This is what makes the prepare()-time abstime-match fire reliably — the break-point math is done in the truncation layer, the event match in the prepare layer. Combined with the existing 1.5× growth cap (STEP 5), dt smoothly closes on each scheduled event.
Verification: a hand-rolled synthetic-timer OSDI library (https://sourceforge.net/projects/openva/ /integration_tests/SYNTH_TIMER) advertises num_event_slots=1, schedules a TIMER 1 ns from the first eval, and checks that NGSPICE fires the event with at_scheduled_event=1, fired_event_id=0 at exactly t=1 ns. Trace shows the dt-clamp closing onto t=1ns and the model's event-fire detection landing on that step.
S3c (true sample-exact bisection on cross-flip) is deferred — for now the cross event fires at the post-flip step's abstime, accuracy = local dt. The infrastructure is in place to swap in a bracket-and-midpoint loop when the accuracy demand justifies it.
Future cleanups
----------------
- The 0.7 V / 0.9 V references inside sanitize_jacobian (clamping NaN diagonal conductance by a "supply-rail-like" denominator) are still pragmatic constants. They are not used as voltage limits per se, but they are the closest thing to a voltage-rail assumption left in the file and should eventually be replaced by something derived from the per-instance Jacobian column scaling.
- Once the upstream OSDI ABI exposes per-iteration delta-V information natively, the per-shape simparams in 4a become a fallback path; the limiter will read the bound directly from the ABI.
STEP 20: src/osdi/osdi.h, osdiload.c, osdidefs.h (OSDI 0.5 sample-exact cross events — S3c plan B)
--------------------------------------------------------------------------------------------------
S3b's cross-event fire was off by one local dt: the runtime detected the sign flip during the eval that overshot the crossing and scheduled the event at that step's abstime, so the model saw at_scheduled_event = 1 some `dt` past the actual zero. For a 1 kHz sine sampled at 10 us, that's ~5.8 us of error on every fire — fine for "did something cross?" semantics but useless if the model wants the time of the crossing for phase / period / arrival accounting.
S3c was originally going to be true bisection: on flip, set the [t_lower, t_upper] bracket, signal REJECT_STEP, let OSDItrunc clamp dt to the bracket midpoint, narrow until convergence, but that collided with dctran's reject path:
- REJECT_STEP routes through CKTosdiStepReject, which raises CKTnoncon, which makes niiter return E_ITERLIM.
- dctran handles E_ITERLIM by backing CKTtime up by CKTdelta AND cutting CKTdelta /= 8.
- dctran does NOT call CKTtrunc between rejected retries (it jumps via `goto resume:` which only runs setup, not truncation).
- So the OSDItrunc bisection dt-clamp never gets the chance to drive dt toward the bracket midpoint; the /8 shrinkage compounds unchecked and ten rejections later the simulator aborts with "Timestep too small".
Plan B replaces bisection with linear interpolation done inline in postprocess: t_cross = t_prev + dt * |prev| / (|prev| + |curr|). The current step still accepts; the event sits in scheduled_events at t_cross even though t_cross < CKTtime; prepare()'s fire check relaxes from a fabs-equality window to "abstime >= t_event - 1ps"; and the model latches the true crossing time from a new SimInfo field rather than abstime.
20a. OsdiSimInfo tail extension (osdi.h, +8 bytes; struct grows
96 -> 104). Single trailing double:
double scheduled_event_time;
Defined when at_scheduled_event != 0; carries the t_event the runtime scheduled (linear-interp crossing time for CROSS events; the model-provided at_time for TIMER events). Zero when no event is firing. Existing 0.5 binaries that don't read this field continue working — the bounds-checked descriptor read in osdiregistry.c accepts the new SimInfo size, and the leading fields keep the same offsets.
20b. OsdiExtraInstData (osdidefs.h) gains `double prev_eval_time`.
The abstime of the last accepted eval that latched prev_cross_expr_arr. Used as the lower-bound t_prev for the linear-interp formula. Default -1.0 means "no prior eval"; postprocess gates flip detection on it being >= 0 so the first eval (where prev_cross_expr_arr is still zero) doesn't trigger a spurious crossing.
20c. osdi_event_prepare (osdiload.c) relaxes the match window from
fabs(t_event - abstime) < 1e-12
to
abstime >= t_event - 1e-12
and on fire, also writes the popped event's at_time into sim_info_local->scheduled_event_time before calling descr->eval. Cross events almost always fall BETWEEN two simulator steps so the exact-equality window from S3b would have meant the cross event never fired with linear-interp scheduling. Timer events still fire on the exact match because they're scheduled via CKTsetBreak-style integer time-multiples; nothing changes for them.
20d. osdi_event_postprocess (osdiload.c) replaces the S3b
"schedule at current abstime" stub with the linear-interp formula. Falls back to abstime when |prev|+|curr| would be zero (both endpoints exactly zero — degenerate, fire-now is the safest default). cross_init still gates against the first eval's all-zero prev_cross_expr_arr.
20e. osditrunc.c — no net change. The earlier "OSDItrunc bisection
dt-clamp" experiment is reverted. The scheduled-event dt-clamp from STEP 19 still runs; it just sees cross events whose at_time may be in the past relative to CKTtime (negative dt_to_event, no clamp). That's fine: the event fires on whatever step happens to be at or past t_event.
Verified residual error: a SYNTH_CROSS hand-rolled OSDI model that writes sin(2*pi*1k*t) to cross_expr[0] and dumps the bisected time on fire shows t_cross = 1.000000271358e-3 / 2.000000271358e-3 on the first two rising zeros — 271 picosecond residual vs the analytic 1 ms / 2 ms. That's the second-order Taylor error of linear interp against the sine curvature; for a 1 kHz sine sampled at 10 us the predicted residual is ~80 ps so we're roughly in the right magnitude. S3b's residual was the full local dt ≈ 5.85 us, so plan B is ~20,000x better despite leaving the simulator's step controller entirely untouched.
True bisection — accuracy bounded by meta.ttol / etol rather than the linear-interp error — would need either:
- a new CKT field (e.g. CKTosdiNextDelta) that osdiload could set to override dctran's /8 shrinkage between rejected retries, or
- inline midpoint probes via descr->eval with the contribution flags cleared (cheap for $abstime-driven cross expressions, accurate-but-stale for V()-driven ones because the network state would be pinned to the post-flip step). Both are larger changes than plan B and aren't justified until a model in the corpus actually needs sub-ns crossing accuracy.
STEP 21: src/maths/ni/niiter.c (stagnation-gated Stage B damping)
=====================================================================
The two-stage Newton-step limiter introduced in STEP 9 fixed pinb / net_7 oscillation on 22nm BSIM6 ULP by:
Stage A — scalar damping when max|Δv| exceeds dv_max (CKTabsDv, default 0.5 V). Stage B — past iter 3, progressively halve dv_max (down to ~8 mV by iter 10) so any oscillating Newton damps out.
Stage A is fine. Stage B's UNCONDITIONAL halving was breaking the opposite failure mode on the same testbench: the VSN node — an L1 (2.7 nH) coupled supply driving a 500-finger-multi BSIM-BULK driver — naturally needs ~100 mV swings per Newton iteration to track each switching transition (the inductor kick is V = L·di/dt ≈ 1.8 V). By iter 10 the dv_max cap had collapsed to 8 mV and the clamp prevented Newton from moving enough per iteration to reach the solution within itl4 = 10, even though max|Δv| was DECREASING monotonically iter-to- iter. Newton was converging in spirit but artificially under- corrected to look like it was failing. dctran's failure handler cut CKTdelta by 8 fourteen times in succession (5e-9 → 6.25e-21), well below CKTdelmin, and the run aborted at t ≈ 1.382 µs / 42851 rows.
Fix: gate Stage B on a STAGNATION test. Track the previous iteration's pre-clamp max|Δv| in a local NIiter variable; on each iter past 3, only apply the progressive halving when
max_dv > 0.7 · prev_max_dv
i.e. max|Δv| isn't dropping by at least 30% iter-to-iter. When Newton is making real monotonic progress, dv_max stays at the full CKTabsDv and Newton steps at its natural rate. When Newton is oscillating or stuck (max_dv flat or growing), Stage B kicks in as before.
Why 0.7: well-conditioned systems converge quadratically (ratios <<0.5), stiff systems linearly (ratios ~0.5). 0.7 catches both as "making progress" while flagging slow / flat as stagnating. Could relax to 0.8 if a future workload shows it.
Why track PRE-clamp max_dv: post-clamp max_dv is pinned at dv_max whenever Stage A fires, which destroys the iteration-trend signal. Pre-clamp tracks Newton's actual residual norm.
Pinb / net_7 regression preserved: when Newton truly oscillates, the ratio stays near 1.0 (the oscillation amplitude doesn't decrease without damping), Stage B fires on schedule, and the test passes the same way it did under STEP 9 + STEP 7.
Acceptance criterion: 22nm BSIM6 ULP driver_lv_2v5_tb must run to the full 10 µs / convergence, not abort at 1.382 µs. 14nm FinFET (236,939 rows) and BCD (1,212 rows) must continue to pass.
STEP 21b update (STEP 21 alone insufficient): the Stage-B gating made Newton's damping schedule more permissive but the failure mode was upstream of Newton — see STEP 22 below.
STEP 22: src/spicelib/analysis/dctran.c (non-uniform-history predictor guard)
=============================================================================
Newton iteration converges from a STARTING POINT supplied by the predictor (NIpred). Order-2 Newton-divided-difference for trapezoidal integration:
b = -dt_now / (2 * dt_prev)
a = 1 - b
dd0 = (sols[0] - sols[1]) / dt_prev
dd1 = (sols[1] - sols[2]) / dt_prev_prev
pred = sols[0] + (a*dd0 + b*dd1) * dt_now
This is ok when dt_prev and dt_prev_prev are within an order of magnitude. It is NOT ok after the simulator has just been around a breakpoint:
- At a breakpoint the dctran logic at line 488 cuts dt to pico-second scale and sets CKTorder=1.
- One step later, line 822-830 bumps CKTorder back to 2 if the LTE test passes at the cut dt.
- The 1.5× OSDItrunc growth cap then re-grows dt to the nanosecond scale over the next ~15 accepted steps.
- But after the FIRST few steps in that growth sequence, dt_prev_prev still records the picosecond cut step from the breakpoint. The dd1 term — (sols[1] - sols[2]) / dt_prev_prev — gets dividing by a value 100×10000× smaller than dt_prev, so dd1 magnitudes blow up to ~10^10 V/s for any node whose state changed across the breakpoint.
Concrete numbers from the 22nm BSIM6 ULP failure at t = 1.38187 µs:
dt_prev = 4.66 ns (just-accepted big step) dt_prev_prev = 21 ps (the cut step at the previous breakpoint) Newton-divided-difference dd1 for VSN ≈ 0.5 V / 21 ps = 2.4e10 V/s b * dd1 * dt_now ≈ -0.057 * 2.4e10 * 0.53 ns ≈ -0.72 V Predictor starts Newton at VSN ≈ sols[0] - 0.72 V.
Newton then has to undo a 0.72 V starting offset within itl4 = 10 iterations, against Stage A's 0.5 V/iter scalar clamp — possible in principle (2 iters at 0.5 V each gets you within 0.28 V), but the overshoot also propagates to coupled nodes (the driver subcircuit's internal nodes pinb / net_6 / net_7 etc. all see correlated wrong predictions), and the multi-node simultaneous correction within Stage A's proportional structure burns more iterations than itl4 allows.
Fix: in dctran.c, just before NIcomCof, sanity-check the step-size history:
if (CKTorder > 1 &&
CKTdeltaOld[1] > 0 && CKTdeltaOld[2] > 0) {
double r = CKTdeltaOld[1] / CKTdeltaOld[2];
if (r > 10.0 || r < 0.1)
CKTorder = 1;
}
When dt_prev / dt_prev_prev is more than 10× off in either direction, force CKTorder=1 for this step. NIpred at order=1 is plain linear extrapolation from sols[0..1] — immune to the dd1-blowup pathway because it doesn't use sols[2] at all. dctran's existing order-bump logic at line 822-830 will resume order=2 on the next step once dt_prev_prev catches up to the post-breakpoint step scale.
Threshold of 10× chosen so smooth dt-growth sequences (the 1.5× OSDItrunc cap, factor 1.5 per step) never trigger — only genuine breakpoint-induced discontinuities in dt history. A future refinement could derive the threshold from the integration order itself (the order-2 truncation error is exact when dt is uniform and degrades as ratio^2), but 10× is well inside the safe range.
Round-2 verification: 22nm BSIM6 ULP advanced from 42851 rows (abort at VSN, t = 1.382 µs) to 43344 rows (abort at PINB, t = 1.369 µs). VSN convergence resolved; PINB still failing — see STEP 23 below. 14nm FinFET: 256,194 rows, full pass (better than the 236,939 pre-S8 baseline — the smaller-dt-growth-per-step pattern from STEP 22 lets more steps land in transients).
STEP 23: src/osdi/osditrunc.c (growth cap tightened from 1.5× → 1.2×)
=====================================================================
Round-1 + round-2 fixes resolved the order-2-predictor blowup on VSN but exposed a residual pinb / net_7 failure at t ≈ 1.369 µs on 22nm BSIM6 ULP — (the 2.0× → 1.5× tightening of OSDItrunc's growth cap). The pinb failure mode per the project_pinb_clamp_oscillation memory is
CKTterr sees low charge curvature in the quiet between switching edges → allows dt to grow → at the next sharp edge Newton can't track a 0.5 V swing in the grown dt → step rejected → /8 cuts.
1.5× growth allows dt to reach the user's max step in 10-15 accepted steps starting from a ~10 ps post-breakpoint cut. In the quiet 5-6 ns between switching edges of the 150 MHz pulse train, that's plenty of headroom for dt to saturate at 5 ns, then choke on the next edge. 1.2× growth keeps dt much smaller in the same window — 10 ps × 1.2^12 ≈ 90 ps after 12 steps — so the next sharp edge sees a Newton linearization at ~100 ps scale, easily tracking sub-V swings.
Trade-off: more accepted steps per simulation (slower wall clock, larger raw files). 14nm FinFET still runs in similar time (256,194 rows vs. pre-S8 236,939 — within tens of percent). 22nm BSIM6 acceptance is the gating criterion.
STEP 24: src/frontend/inpcom.c (skip quote-to-brace on .del / .include)
=======================================================================
inp_change_quotes converts single quotes to braces line-by-line for commercial simulators-compat handling, so that `r1 a b r='100*scale'` becomes `r1 a b r={100*scale}` and numparam evaluates the expression.
Side effect: lines like .del lib '../models/design_wrapper.lib' FET_tt_post get rewritten to .del lib {../models/design_wrapper.lib} FET_tt_post and numparam (correctly) reports Number format error: "../models/design_wrapper.lib} fet_tt_post"
The existing `.lib` skip at line 3949 prevented this for top-level `.lib '...'` lines but did NOT match `.del lib '...'` (used inside `.alter` blocks) or `.include '...'` / `.inc '...'`. Observed on BCD sample_netlist, which uses `.del lib '../models/design_wrapper.lib' <section>` to switch process corner between alter blocks.
Fix: extend the skip list to `.del`, `.include`, `.inc ` (with trailing space on `.inc` to avoid accidentally matching `.include` or future commands starting with `.inc...`).
Acceptance criterion: BCD sample_netlist parses without "Number format error" on the .del lib lines. 14nm FinFET + 22nm BSIM6 ULP unchanged.
STEP 25: src/frontend/numparam/{spicenum.c,numparam.h},
src/spicelib/parser/inpeval.c (commercial simulators-style bare .param identifier as numeric value)
==========================================================================
commercial simulators accepts a bare .param identifier wherever a numeric literal is expected:
.param vgswp=0 vgmax=3.3
vgs n2 n3 vgswp <-- bare param as V-source value
.dc vgswp -0.1 vgmax 0.01 <-- bare params as .dc sweep
start/stop
Pre-fix, INPevaluate (src/spicelib/parser/inpeval.c) only knew how to parse numbers; numparam's existing single-to-brace quote pass handled `'vgswp'` / `{vgswp}` but not the bare form. When INPevaluate failed on `vgswp`, INPdevParse fell through to its parameter-name loop, treated `vgswp` as a device instance-parameter name, found no such V- source parm, and aborted:
Error on line 49 or its substitute: vgs n2 n3 vgswp unknown parameter (vgswp)
Fix. Add a public lookup helper to numparam:
int nupa_get_real(const char *name, double *value); (spicenum.c)
— wraps the static dicoS via entrynb(); returns 1 + sets *value if the name resolves to a NUPA_REAL, otherwise 0.
In INPevaluate, when the first non-sign character isn't a digit / `.` (the existing "not a number" branch), check whether the token matches the identifier pattern [A-Za-z_][A-Za-z0-9_]* AND is a known NUPA_REAL. If so, advance `*line` past the identifier and return its value with error = 0. Otherwise fall through to the existing "number looks like just a sign" error path.
This catches the bare-identifier case for ALL INPevaluate callers (device-line leading values, INPgetValue across .dc / .ac / .tran / .measure / device parameters, etc.) with one localized change. No new keywords reserved; identifiers that aren't .param names continue to error in the same way as before.
Safety: only fires when number-parse outright failed (no digit/sign/ dot prefix). Numeric literals are unaffected. `{expr}` and `'expr'` forms continue to be handled upstream by the quote-to-brace pass + numparam dispatch — INPevaluate never sees them.
Acceptance criterion: BCD sample_netlist parses past line 49 (`vgs n2 n3 vgswp`) AND line 61 (`.dc vgswp -0.1 vgmax 0.01`). 14nm FinFET + 22nm BSIM6 ULP unchanged.
STEP 26: src/frontend/inpcom.c (commercial simulators .alter blocks → silently skipped)
========================================================================
commercial simulators `.alter <label>` re-runs the simulation with the section of the deck following it (typically `.del lib ... / .lib ... <corner>` to swap a process corner) as overrides on the already-loaded main circuit. NGSPICE doesn't implement .alter and reports:
Error on line 63 or its substitute: .alter fff unimplemented dot command '.alter'
Pre-fix, the test deck terminated with a fatal error before any analysis could run. Per memory's existing convention for .biaschk (silently warned-and-skipped at inpcom.c:1514), do the same for .alter — but with one difference: .alter introduces a CONTAINED BLOCK that runs until the next .alter or .end. The block contains `.del lib '<path>' <section>` / `.lib '<path>' <section>` lines that would, if processed in isolation, re-load a different corner on top of the already-loaded one — so we have to skip the WHOLE block, not just the .alter line.
Implementation: two file-scope static bools. When `.alter` is seen, emit a one-shot warning and set `in_alter_block = TRUE`. Subsequent non-`.end` lines are silently dropped until either another `.alter` (starts a new alter block — keep skipping) or `.end` (closes the deck — clear the flag and let .end fall through to the main loop's processing). `.ends` (subckt terminator) is explicitly NOT treated as `.end` here.
Single warning per deck. No state carries across decks beyond what inpcom.c's other one-shot warnings already do.
Acceptance criterion: BCD sample_netlist runs through to .end without a fatal at .alter, completes the .dc sweep + .measure, and writes results. 14nm FinFET + 22nm BSIM6 ULP unchanged (neither uses .alter).
STEP 27: commercial simulators-compat `.dc paramName ...` sweep
=================================================
NGSPICE's .dc analysis only knows how to sweep voltage sources, current sources, resistors, and `temp`. commercial simulators additionally allows
.param vgswp=0 vgmax=3.3
vgs n2 n3 vgswp
.dc vgswp -0.1 vgmax 0.01
— sweep a .param across a range, and every V/I source whose DC value was set from that .param picks up the new value on each step. Without this, the BCD testbench aborts:
Fatal error: DC Transfer Function: Voltage source, current source, or resistor named "vgswp" is not in the circuit
Implementation across four files:
1. src/include/NGSPICE/trcvdefs.h Define PARAM_CODE = 1022 next to the existing TEMP_CODE = 1023.
2. src/frontend/numparam/spicenum.c + numparam.h Add `int nupa_set_real(const char *name, double value)` — a writer companion to nupa_get_real that updates an existing NUPA_REAL entry's `vl`. Refuses to create new entries (would mask the simulator's "unknown name" diagnostic).
3. src/spicelib/parser/inpdpar.c + inpeval.c Maintain a parse-time table of (paramName, devName, devType) bindings. When INPevaluate's commercial simulators-compat path (STEP 25) resolves a bare identifier from numparam, it stashes the resolved name in a static `inpeval_last_param_name` slot; INPdevParse reads that immediately after each INPevaluate call and registers a binding tagged with the device's GENname and dev-type code.
Public accessors `dpar_first_param_binding / dpar_binding_param_name / dev_name / dev_type / next` let dctrcurv.c iterate without exposing the struct.
4. src/spicelib/analysis/dctrcurv.c + ckttroub.c New PARAM_CODE branch alongside vcode / icode / rcode / TEMP_CODE in every place the others appear:
- setup: nupa_get_real lookup as last resort before erroring.
- step / stop-condition check: nupa_get_real reads the current value, compares against TRCVvStop.
- nest-level reset: nupa_set_real to start value, then dctrcurv_push_param_to_bindings to propagate to every V/I source bound to this param.
- increment: bump by TRCVvStep, push to bindings.
- cleanup: restore TRCVvSave (the parse-time value) and push.
- varUid: "param-sweep".
- ckttroub.c troubled-domain message: read current value.
`dctrcurv_push_param_to_bindings` walks the dpar_binding list for the swept param's name, finds each VSRC / ISRC instance by name in `ckt->CKThead[vcode/icode]`, and mutates VSRCdcValue / VSRCdcGiven (or ISRCdcValue / ISRCdcGiven) directly — same pattern as the existing source-name sweep path.
Scope limits:
- Only the leading-numeric value of V/I sources is bound. Other commercial simulators constructs that reference .params (PULSE/SIN parameters, expression-valued device parameters etc.) still need the {expr} / 'expr' form to participate in re-evaluation; bare identifiers there fall through to the existing temper-style pipeline.
- The dpar_binding list is process-global and never freed. In practice a deck registers a handful of bindings; not a concern for batch runs. A multi-deck interactive session would accumulate entries — acceptable trade-off for simplicity.
Acceptance criterion: BCD 5V PLDMOS runs the `.dc vgswp -0.1 vgmax 0.01` sweep, completes the two `.measure` statements (idsat_uA/um, idoff_pA/um), and exits 0. 14nm FinFET + 22nm BSIM6 ULP unchanged (neither uses .dc-on-param).
STEP 28: commercial simulators-shorthand `.measure` analysis-type inference
=============================================================
commercial simulators accepts
.measure vtlin find v(n2) when ...
with the {DC|AC|TRAN} analysis-type token omitted. NGSPICE historically defaulted to TRAN in this case in two places:
src/frontend/com_measure2.c::measure_extract_variables Save-var registration (called from ft_savedotargs at deck load time). When defaulting to TRAN, the save names were tagged "tran" — fine for a tran deck, fatal for a deck whose analysis is .dc, because OUTpBeginPlot then sees zero matching save vars under the "DC" tag, and reports Error: no data saved for D.C. Transfer curve analysis; analysis not run
src/frontend/measure.c::do_measure Measurement-eval parser (called from ft_cktcoms after the analysis ran). Hard-errored on the unknown first token: Error: unrecognized analysis type 'vtlin' for the following .meas statement on line 58884
Fix. Both call sites infer the analysis type by scanning `ft_curckt->ci_commands` once for the first .dc / .ac / .tran directive; fall back to TRAN if none found. In do_measure, the parser additionally re-classifies its three already-gettok'd tokens — what looked like an_type / resname / meastype is actually resname / meastype / first-argument — and splices the first-argument back into `line` so the rest of the parser sees the original structure.
Scope limits:
- Defaults to the FIRST analysis directive found. A deck with both .tran and .dc + shorthand .measure attaches all such measures to whichever appeared first. Decks that need both should keep the explicit `DC` / `TRAN` keyword.
- The shorthand splice in do_measure allocates a new line buffer that isn't freed. Acceptable for one-pass parsing.
Acceptance criterion: BCD 5V NLDMOS completes the .dc sweep, all four .measure statements report values, and exits 0. 14nm FinFET + 22nm BSIM6 ULP unchanged (both already use explicit-typed .measure / .write within .control sections).
Verification (2026-06-09 round 12, final):
22nm BSIM6 ULP driver_lv_2v5_tb: 482,785 rows, 0 errors 14nm FinFET tb_driver: 325,966 rows, 0 errors BCD NLDMOS: 1,212 rows, 0 errors, all four .measure values produced (sample_netlist_ng/ path). Updates to STEPs 21-28 verified working end-to-end across all three PDKs.
Update to STEPs 25 + 27 (commercial simulators-shorthand .measure analysis-type inference / .dc paramName sweep): the inferred-analysis scan must walk `ft_curckt->ci_deck` (the main card list) rather than `ft_curckt->ci_commands` (which only holds .print/.plot/.meas/ .op/.tf/.four/.width/.sndprint/.sndparam). Original STEP 28 called out the wrong list; corrected during round-9 debugging.
STEP 27 .dc-by-param expanded to recover bindings via deck text scan (dctrcurv.c::dctrcurv_scan_deck_for_bindings) using `ft_curckt->ci_origdeck` (the pre-substitution copy saved by inp.c::inp_spsource as `deck->actualLine = realdeck` from `inp_deckcopy(deck)` at line 633, BEFORE inp_subcktexpand's nupa_copy substitutes bare-identifier values). Each device line whose first letter is v/V/i/I and whose LAST whitespace-token matches the swept param (with optional `{...}` strip) registers a binding. Parse-time INPdevParse hooks remain as a no-op fallback for the rare case where numparam doesn't pre-substitute.
STEP 27 endpoint correctness: PARAM_CODE sweep uses `cur_val = start + N*step` plus a snap-to-stop when the next iteration would overshoot, with the snapped value routed through numparam's "% 23.15e " format then re-parsed via INPevaluate so it lands on the SAME FP value the measure side's INPevaluate2 will see when the m_val expression (`vmax` etc.) is substituted. Without the parse-back round-trip, `.dc paramName -0.1 vmax 0.01` + `.measure ... when X=vmax` could miss by 1 ULP because formula() (sscanf %lG, correctly rounded) and INPevaluate (digit-by-digit accumulator) take different roundoff paths.
BCD 1212 rows: dec sweep produces 341 rows. The remaining ~871 rows come from .measure-generated synthetic save vectors that NGSPICE creates per `par('expr')` expression to make those addressable in `.measure ... find par('...') / when par('...')` contexts. Same count commercial simulators produces for the same deck.
================================================================
STEP 29: subckt.c — skip commercial simulators source-type marker before POLY
================================================================
Path: src/frontend/subckt.c Lines: ~1461-1477 (new block) plus 1480 (added next_name && guard) Where: inside the case 'e'/'f'/'g'/'h' translation branch, after the output-node translation loop, before the POLY peek.
Problem
-------
BCD HV NMOS devices instantiate thermal_branch.inc, which defines two polynomial VCCSs with commercial simulators-style syntax:
Gtnode 0 tnode vccs poly(2) d2 d1 d s 0 0 0 0 'expr' m=nf
Gshe s d vccs poly(2) tnode 0 d2 d1 0 0 0 0 'expr' m=nf
The `vccs` literal is commercial simulators's optional source-type marker between the two output nodes and the POLY descriptor. NGSPICE's subckt translator's e/f/g/h case reads N output nodes, then peeks the next token expecting `POLY`. Finding `vccs` instead, it falls into the dim=1 branch and consumes `vccs` then `poly` as the two controlling nodes. The post-translation line therefore reads:
g.xmn1.gtnode 0 xmn1.tnode xmn1.vccs xmn1.poly 2) d2 d1 d s ...
— `vccs` and `poly` scoped as if they were node names, `(2)` left as garbage tokens (the `(` is consumed by gettok_node, the `)` and `2` survive as `2)`). Then inp2g.c parses 4 nodes (`0`,`xmn1.tnode`,`xmn1.vccs`,`xmn1.poly`) and PARSECALL on the remainder hits `(2) d2 d1 ...`, reports "unknown parameter (d2)".
Result: every isoednfet HV deck except 5p0_lr fails parse (8p0_lr, 10p0_lr, 12p0_lr, 12p0_reg, 20p0_lr_sw, 20p0_reg_sw — 6 NMOS files). Plus isoedpfet_20p0_reg_sw (1 PMOS). 5p0_lr and 12p0_hybrid pass because their subcircuits don't include thermal_branch.
Fix
---
After translating the output nodes and before the POLY peek, insert a peek-and-skip for the commercial simulators source-type marker:
t = s;
next_name = gettok_noparens(&t);
if (next_name && (
(dev_type == 'g' && cieq(next_name, "vccs")) ||
(dev_type == 'e' && cieq(next_name, "vcvs")) ||
(dev_type == 'f' && cieq(next_name, "cccs")) ||
(dev_type == 'h' && cieq(next_name, "ccvs")))) {
s = t; /* commit consumption */
tfree(next_name);
t = s;
next_name = gettok_noparens(&t);
}
Then the existing POLY check guards `next_name &&` (in case the
line ended after the source-type marker — defensive, not
expected in real decks). The marker is consumed but NOT
emitted: the device-letter (g/e/f/h) already encodes the type,
so the downstream XSPICE ENHtranslate_poly translator sees a
clean `G n+ n- POLY(N) <controls> <coeffs>` line — its existing
"is the 4th token 'poly'?" detector at inpcom.c:8046 works
unchanged.
Acceptance criterion
--------------------
All 7 affected BCD *_ng.sp decks (6 NMOS + 1 PMOS) parse past the thermal_branch.inc G-poly lines. Downstream success depends on the per-deck model bin / convergence (see STEP 30 for the PMOS bin issue), but the parser stops being the obstacle.
Verification (2026-06-10, post-rebuild):
Re-ran every sample_netlist_ng/*.sp deck individually. Result: 27/30 pass clean (up from 18 pre-fix). Specifically for the 7 vccs/poly targets: isoednfet_8p0_lr.sp: 1,932 rows PASS isoednfet_10p0_lr.sp: 1,212 rows PASS isoednfet_12p0_lr.sp: 1,452 rows PASS isoednfet_12p0_reg.sp: 1,452 rows PASS isoednfet_20p0_lr_sw.sp: 1,452 rows PASS isoednfet_20p0_reg_sw.sp: 1,452 rows PASS isoedpfet_20p0_reg_sw.sp: 33 rows PASS Plus several PMOS HV decks that previously appeared stuck on gmin under CPU contention also pass when given exclusive runtime (10p0_lr 3417 rows, 8p0_lr 2737 rows, 12p0_hybrid 4097 rows, 12p0_lr 4097 rows). Previously-passing decks all still pass (no regression on the 18 working baseline). Three remain: 5V PLDMOS and 20V PLDMOS fail with "could not find a valid modelname" (separate bin/ scoping issue, not parser); isoedpfet_12p0_reg.sp times out at 90s in gmin stepping (likely test-deck sweep polarity wrong for this device — see project memory).
================================================================
STEP 30: subckt.c — bin-pruning must fail open, not closed
================================================================
Path: src/frontend/subckt.c Where: doit() pass 2, the "reduce binning models" block that runs for hs/spe compat when the X invocation line carries literal w= and l= values (c->w > 0 && c->l > 0). Plus two new static helpers above doit(): get_model_bins(), get_model_basename().
Problem
-------
BCD 5V PLDMOS and 20V PLDMOS failed with "could not find a valid modelname" while every NMOS sibling passed. The PMOS/NMOS asymmetry had nothing to do with device type:
- NMOS testbench: xmn1 ... nldmos_5 l={lm} w={wm}
- PMOS testbench: xmp1 ... pldmos_5 l=0.222u w=11.11u
inpcom.c parses literal w=/l= off X lines into card->w/card->l (INPevaluate fails on the {braced} NMOS values, leaving w=l=0). In doit() pass 2, when card->w/l are nonzero, the copied subckt body is pruned: every .model card whose [lmin,lmax)x[wmin,wmax) bin does not contain (scale*l, scale*w/nf) is DELETED — a memory optimization for big binned PDKs ("su_deck is full of unused binning models").
For BCD HV devices the X-line dimensions are DRAWN values; the subcircuit rescales them internally via numparam (l_scale='l*shrink_factor', shrink_factor=0.9) before its MOS line, and the model card's bin limits are POST-shrink. Drawn 0.222u vs lmax=2.01e-7 -> no bin matches -> the device's ONLY model card was deleted from the copy. Downstream, modtranslate saw no .model in the body (so neither the card nor the M-line reference got the xmp1: scope prefix) and INPgetModBin had nothing to bind -> "could not find a valid modelname".
The existing safety check (if (!su_deck) error) only catches a COMPLETELY emptied copy, which never happens because device lines survive. 22nm BSIM6/14nm FinFET never hit this because their X lines carry {braced} parameter dims -> card->w stays 0 -> pruning is skipped entirely.
Fix
---
Restructure the pruning into two passes:
Pass 1: for every binned .model card in the copy, test the bin against (scale*l, scale*w/nf). Record the model BASE NAME (name minus a trailing '.<digits>' binning suffix, cf. model_name_match()) of every card that matches, in a wordlist.
Pass 2: delete an out-of-bin .model card ONLY when its base name is in the matched list, i.e. only when a better bin of the same model family was identified. If no bin of a name matched, the X-line w/l are simply not comparable to the bin ranges (in-subckt shrink, as above) — keep all bins of that name and let INPgetModBin choose after numparam expansion, which is the exact path the brace-valued decks already use.
Also fixes a latent null-deref: the old code did prev->nextcard with prev==NULL if the first card of a subckt body was a prunable .model. And it no longer deletes binned models of OTHER families (e.g. a binned parasitic diode model) whose ranges are in different units than the MOS w/l being tested.
Memory impact: none for the PDKs that motivated the optimization (22nm BSIM6 / 14nm FinFET either skip the block or still prune to the same single bin). BCD keeps its one model card per device.
20V PLDMOS also needed a one-line deck fix: the _ng port kept `vg s g vgswp` from the commercial simulators original but dropped the original's `.param vgswp=0` when converting the `.dc vgswp` param sweep to a direct `dc ... VG` source sweep. Added `.param vgswp=0` to the deck (testbench bug, not a simulator issue; pristine original in /techfiles has the param).
Verification (2026-06-10, post-rebuild)
---------------------------------------
5V PLDMOS: 1,313 rows (was: modelname error) 20V PLDMOS: 33 rows, matches reg_sw sibling (was: modelname error) Full 30-deck sample_netlist_ng sweep + 22nm BSIM6 ULP driver_lv_2v5_tb + 14nm FinFET tb_driver re-run: results recorded below after completion.
================================================================
STEP 31: remove hard-coded geoshrink; honor commercial simulators subckt `scale`
Note: I added a hard -oded geoshrink because I misunderstood the commercial simulator methodology. I made a big mess! I reverted it and went in a different direction, but it needs to be checked.
================================================================
Paths: src/frontend/subckt.c (inp_apply_subckt_scale + scale_geom_param:
wrap geometry in scale-declaring subckt bodies) src/frontend/subckt.h (declare inp_apply_subckt_scale) src/frontend/inpcom.c (call inp_apply_subckt_scale before inp_fix_for_numparam; drop geoshrink parse) src/frontend/inp.c (drop geoshrink option parse x2) src/spicelib/parser/inpgmod.c (drop geoshrink from bin select) src/osdi/osdiparam.c (drop geoshrink from OSDI geom) src/frontend/spiceif.c (drop geoshrink pre-scan) src/spicelib/parser/inpdoopt.c (geoshrink now silently ignored)
Problem
-------
`geoshrink` was a NGSPICE-Justin-invented `.option` I got that completely wrong! (Set in the 22nm BSIM6 NGSPICE wrapper as `.option geoshrink=0.855`). The C read it from a cp_var in two functional places -- INPgetModBin (multiply instance W/L before bin comparison) and osdiparam (multiply W/L before writing to the OSDI model) -- to convert DRAWN instance dimensions to the EFFECTIVE (post-shrink) dimensions the foundry bins and models expect.
geoshrink is NOT commercial simulators syntax; it is a per-PDK hack hard-coded into the simulator. The 22nm BSIM6 commercial simulators model carries no `.option scale`, no `.option geoshrink`, and no device `scale=`: it relies on commercial simulators's element-scale convention, where a subcircuit that declares a `scale` parameter applies it as the geometry scale factor for the MOSFETs inside it. 22nm BSIM6's device macros are `.subckt nch_mac ... scale= 'scale_mos'` (scale_mos = 0.855). BCD process instead bakes the shrink into the device-line dimension expressions via numparam (l_scale='l*shrink_factor'), so its expanded M-line already carries effective dims and needs no option at all.
Why the device line is not enough (the key fact)
-------------------------------------------------
22nm BSIM6's actual device line is: main n1 n2 n3 n4 nch w='w+dw*nf' l=ll ... (dw=0, ll=(l+dl)*...) There is NO `*scale` on w/l -- the drawn dimensions go to the model unscaled. The 0.855 enters ONLY through the subckt header's reserved `scale='scale_mos'`, which commercial simulators applies as an element scale factor. The model is written FOR that round-trip: it pre-divides its own parasitics expecting the simulator to multiply them back, e.g. sd='2*_dmcg/scale' -> expanded sd = 6.265e-8/0.855 = 7.327e-8 ad='(...)/scale*(.../scale)' (i.e. .../scale^2) sa='saref/scale' sb='sbref/scale' The `.model nch.N` bins are authored in POST-shrink dims (lmin=4.275e-7 = 5e-7*0.855). So the simulator MUST apply the full commercial simulators element-scale table -- lengths *scale, areas *scale^2 -- to both bin selection and the device. Scaling only w/l would leave the parasitics wrong by scale^2. (Confirmed against the expanded deck: numparam DOES resolve the subckt `scale` formal to 0.855 -- the earlier `w={(...)*(scale)}` wrap failed only because it mangled the single-quoted commercial simulators expression `'w+dw*nf'`, not because of scoping.)
Fix
---
Generic, per-instance, no foundry tokens matched. inp_apply_subckt_scale() walks the deck; for each subckt that declares a `scale` formal (subckt_has_scale: word-boundary `scale=`, so `l_scale=`, `noscale=`, and the `scale_mos` token in a value do NOT match) it wraps every geometry value-expression in that subckt's BODY so it is multiplied by the in-scope `scale`:
pname=expr -> pname='(expr)*scale' (lengths/perimeters)
pname=expr -> pname='(expr)*scale*scale' (areas)
w l pd ps sa sb sc sd : *scale
ad as : *scale*scale
(nrd nrs m nf sca scb scc delvto mulu0 total : untouched)
numparam then evaluates the EFFECTIVE geometry per instance, which feeds bin selection (inpgmod), OSDI (osdiparam) and built-in BSIM naturally -- none need any scale logic of their own. scale_geom_param() handles the three value forms in foundry models: single-quoted `'expr'`, braced `{expr}`, and a bare token/number; output is always single-quoted.
WHERE it must run: inpcom's inp_readall(), immediately BEFORE inp_fix_for_numparam().
* NOT in doit()/inp_subcktexpand(): numparam (nupa_copy) runs first and converts each `'expr'`/`{expr}` into a placeholder it later evaluates; an expression wrapped after that is left raw and reaches the device parser as text ("unknown parameter (')"). Verified: a wrap added in doit produced `w='(2u)*scale'` literally in the expanded deck. * NOT detected from sss->su_args in doit(): by then numparam has STRIPPED the `params: scale=...` off the .subckt line, so su_args is just the node list ("d g s b ") and the `scale=` is gone (verified via DBG). * NOT gated on newcompat.hs: a netlist sourced from a .control block (the 22nm BSIM6 flow: `NGSPICE -b tran` -> `source ...net`) reaches doit() with hs still FALSE even though the "hs" banner printed. geoshrink survived this only because it was ungated.
inp_fix_for_numparam() is where expressions are marked for substitution, so wrapping just before it (and before the .subckt param list is stripped) is the one site where both detection works and numparam evaluates the result. .lib/.include content is already flattened into `working` by then.
IMPORTANT (why the first attempt failed): a standalone tag `hsscale={scale}` appended to the instance line does NOT resolve -- numparam evaluates `{}`/`'...'` inside .param definitions and inside a parameter's value-expression, but it drops a bare inline `{scale}` token on a spliced instance line. Wrapping the EXISTING value- expression (`w='(w+dw*nf)*scale'`) DOES resolve (verified: w='2u*scale' in a scale=0.5 subckt -> 1e-6; self-ref `ad='(ad)*scale*scale'` with a .param ad -> *0.25; bare `l='(ll)*scale'` -> *scale). So the carrier must be the expression, not a tag.
This is per-instance: a multi-flavor deck with different per-subckt scales stays correct (geoshrink, a single global, could not do this), and applies to ANY geometric element in the subckt, not just one MOSFET -- commercial simulators's "whole subcircuit resized" (distinct from the M multiplier / replication, left untouched).
NOTE vs geoshrink: the old geoshrink scaled ONLY W and L (osdiparam comment: "Apply geoshrink to W and L"); the parasitics ad/as/pd/ps/ sa/sb/sd were left at their pre-scale .param values -- i.e. ad was too small by scale^2. This fix scales the full geometry set per commercial simulators, so it is MORE correct than the geoshrink build and its row count may differ slightly from the geoshrink-era 482828 (W/L dominate, so it should be near it, and far from the broken 493587 drawn-geometry run).
geoshrink is deleted from inpgmod.c (w_cmp/l_cmp use w/l directly), osdiparam.c (write W/L unchanged), and the option parsers in inp.c / spiceif.c. inpdoopt.c keeps `geoshrink` as a silently-ignored no-op so PDK wrappers that still emit `.option geoshrink=...` (e.g. 22nm BSIM6's embedded_usage.l) don't raise unknown-option warnings -- the subckt `scale` now does the real work.
Coverage across foundries (verified by model inspection):
22nm BSIM6 ULP : 52 `.subckt ... scale='scale_mos'` macros (all flavors scale_mos*=0.855) -> hsscale tag applied. BCD: shrink baked via shrink_factor expressions; no scale formal -> no tag -> untouched. 14nm FinFET : no scale, FinFET (bins on l/nfin) -> untouched. SMIC 90nm : BSIM4, no scale, bins in DRAWN dims -> untouched.
(VERIFIED 2026-06-11)
--------------------------------
22nm BSIM6 ULP driver_lv_2v5_tb: 486493 rows, 0 errors, 6 warnings.
- Moved off the broken drawn-geometry 493587.
- Within 0.8% of the geoshrink-era 482828; the small difference is the correct parasitic scaling geoshrink omitted (geoshrink scaled only W/L; this scales ad/as by scale^2 etc., shifting junction caps and the adaptive timestep). Warnings dropped 40 -> 6.
- Expanded `main`: w 8.999e-5 -> 7.695e-5 (x0.855), l 3e-8 -> 2.565e-8, ad 3.447e-12 -> 2.520e-12 (x0.855^2). Exactly the commercial simulators table. BCD nldmos_5: 1212 rows, 0 errors -- UNCHANGED (no scale formal; shrink stays in its own shrink_factor expressions). 14nm FinFET tb_driver: 325966 rows, 0 errors, 0 warnings -- UNCHANGED (no scale formal, FinFET).
Earlier dead ends (do NOT reintroduce): tag `hsscale={scale}` (numparam drops it); wrap in doit() (too late for numparam); detect via su_args in doit() (scale= already stripped); gate on newcompat.hs (false for .control-sourced decks).
================================================================
STEP 32: unify INPevaluate number parsing with formula() (strtod)
================================================================
Paths: src/spicelib/parser/inpeval.c (INPevaluate -> strtod)
src/spicelib/analysis/dctrcurv.c (drop snap-to-stop round-trip)
Problem
-------
NGSPICE parses numeric literals through two independent code paths that rounded the SAME decimal string to slightly different doubles:
- INPevaluate() (inpeval.c) accumulated the mantissa digit-by-digit (mantis = 10*mantis + digit) and multiplied by pow(10, expo) at the end. For long mantissas / non-representable exponents this drifts up to 1 ULP off the correctly-rounded value.
- formula() / numparam's fetchnumber() (xpressn.c) uses sscanf "%lG" (== strtod), which IS correctly rounded.
So `.param x = 3.3` (stored via %lG) and a `.dc`/device value `3.3` (via INPevaluate) could end up 1 ULP apart. dctrcurv.c's `.dc` param sweep papered over this for `.measure when X = <param>`: when it snapped the last swept value to the stop, it round-tripped the stop through "% 23.15e" + INPevaluate so the saved row bit-matched the measure RHS (which parses via INPevaluate). "Works, but brittle."
Fix
---
INPevaluate now parses the numeric magnitude with strtod() -- the same correctly-rounded parse fetchnumber() already uses -- so the two paths agree to the bit. Kept around strtod: the leading sign (applied separately), the commercial simulators bare-`.param` identifier fallback, the SI scale suffix (T/G/k/u/n/p/f/a, m / Meg / Mil with Mil = x25.4e-6), and the "suffix char is left unconsumed" pointer semantics. A `:` (ternary) or any other non-numeric char terminates strtod naturally, preserving the old early-outs. Fortran 'D'/'d' exponents (e.g. 1.5D3), which strtod does not handle, are finished by hand with strtol so legacy decks keep working. The old digit-by-digit accumulator + mantis/expo1/expo2/expsgn state are gone. (The separate 4k7-style RKM parser further down inpeval.c is unrelated and untouched.)
With both paths unified, dctrcurv.c's snap-to-stop drops the "% 23.15e"+INPevaluate round-trip and just assigns cur_val = TRCVvStop -- now already bit-equal to the measure m_val.
Acceptance (VERIFIED 2026-06-11)
--------------------------------
- BCD sample_netlist_ng: 30/30 pass, 0 errors. (These decks are DC curve traces / `dc ... plot`, not .measure-based -- the older note about idsat/idlin from these _ng decks was stale.)
- 22nm BSIM6 ULP driver_lv_2v5_tb: 476973 rows (was 476961), 0 errors -- +12 rows is the expected 1-ULP FP-sensitivity nudging the adaptive timestep; nothing regressed.
- 14nm FinFET tb_driver: 325966 rows, 0 errors -- UNCHANGED.
- PARAM_CODE snap-to-stop, verified with a dedicated deck (`.param psw` swept via `.dc psw 0 3.3 0.3`, bare-param-bound vsrc): the endpoint lands on EXACTLY 3.300000e+00 (= strtod("3.3"), not the 3.2999.../3.3000...3 that N*step accumulation gives), and meas-dc `when v(n2)=<bracketed exact value>` finds it. (`when v(n2)=3.3` at the very last sweep point reports "out of interval" -- an NGSPICE measure-interpolation boundary, same before/after, not a regression.)
================================================================
STEP 33: OSDI at_final_step signal for @(final_step)
================================================================
Paths: src/osdi/osdi.h (OsdiSimInfo: append at_final_step)
src/osdi/osdiload.c (set it on the final transient timepoint)
A Verilog-A `@(final_step)` body must run on the LAST transient timepoint, which only the simulator knows. Append a `uint32_t at_final_step` field to the TAIL of OsdiSimInfo (after scheduled_event_time, field index 11) and set it to 1 in osdiload.c whenever is_tran && AlmostEqualUlps(ckt->CKTtime, ckt->CKTfinalTime, 100) i.e. the eval(s) of the timepoint that dctran's hard breakpoint at CKTfinalTime lands exactly on. 0 every other eval.
ABI: append-only, so older .osdi binaries (which never read field 11) keep working unchanged -- verified bsimbulk.osdi recompiles byte- identical (md5 7ff363ad) because its GEPs only touch fields 0..10.
The OpenVA side (openva_changes.txt) mirrors the field in the OsdiSimInfo LLVM type + osdi_0_4.h, adds ParamKind::AtFinalStep (reads field 11), and gates the @(final_step) body on at_final_step != 0 plus a persistent "already-ran" flag so it fires exactly once across the final step's Newton iterations -- the mirror image of the @(initial_step) gating.
Verified: a @(final_step) model fires exactly once at t = tstop (n=1); BCD/22nm BSIM6 OSDI decks unchanged under the new 11-field SimInfo.
================================================================
STEP 34: OSDI @(cross) quadratic crossing-time interpolation
================================================================
Paths: src/osdi/osdidefs.h (OsdiExtraInstData: 2nd prior cross sample)
src/osdi/osdiload.c (osdi_cross_time: 3-point quadratic fit)
Plan B (STEP 20) scheduled a cross event at a 2-point LINEAR-interpolated crossing time: O(dt^2) error. For fast carriers (PWM/RF, 100 MHz+, ~5 ns period) where the crossing TIME is the measured quantity, which is too coarse -- ~270 ps on a coarse local step is 5.4% of a 5 ns period.
Fix: keep a 2-deep history of (time, cross_expr) samples and, on a sign flip, fit a 3-point QUADRATIC to the cross_expr VALUE and take the in-bracket root -- O(dt^3). The cross_expr value already folds in BOTH the cross-expr nonlinearity AND the node-trajectory curvature, so a higher-order fit of the value alone sharpens the crossing time with NO model re-evaluation and NO instance-state risk (the re-eval approach floated in STEP 20 needs state save/restore; this avoids it entirely). osdi_cross_time() falls back to linear for a degenerate quadratic (near-zero leading coeff, negative discriminant, no in-bracket root) and during startup (< 3 samples). NGSPICE-only; no OSDI ABI change.
Verified (1 GHz sine cross_expr, zeros at k*0.5 ns, controlled maxstep): 200 ps step (~5 pts/period): err <= 5.5 ps (linear would be ~100+ ps) 100 ps step: err ~ 0.5 ps 50 ps step: err ~ 0.06 ps Error falls ~8-10x per halving of dt -> O(dt^3) confirmed (linear is 4x). Comfortably inside the 10 ps target at coarse sampling.
SEPARATE bug found AND fixed (OpenVA side, see openva_changes.txt): last_crossing()'s latched value reset to -1 between crossings -- its latch used a CKTstates state slot that rotates out, same failure mode as the old @(timer) n_fires counter. Fixed by routing the latch through the persistent-state-in-instance-memory slot in the hir_lower last_crossing lowering. Pure OpenVA / instance-memory -- no NGSPICE ABI change. Verified: last_crossing now HOLDS the (sub-ps-accurate) crossing time between crossings (0 stale -1 reads); bsimbulk.osdi byte-identical.
================================================================
STEP 35: OSDI axis 2 — Newton iteration count exposed to models
================================================================
Paths: src/maths/ni/niiter.c (publish iterno)
src/include/NGSPICE/cktdefs.h (CKTosdiNewtonIter field) src/osdi/osdi.h (OsdiSimInfo.newton_iter, field 12) src/osdi/osdiload.c (populate per eval)
The compiler-side $limit synthesis (OpenVA) and model-authored limiters had no way to see HOW HARD the Newton solve is working -- the limit magnitude was fixed (per-device TOXE-derived or per-shape simparam) but not iteration-aware. Axis 2 closes that: NIiter publishes the current Newton iteration index and OSDI forwards it to the model each eval, so a limiter can tighten when the iterate is struggling (the principled form of the earlier fixed-VLIM sweeps).
- niiter.c: at the Newton-loop top (before each CKTload, next to the CKTnoncon / CKTosdiStepReject / CKThugeJThisIter resets) publish ckt->CKTosdiNewtonIter = iterno + 1. 1-based, the iteration the upcoming load computes; resets to 1 each NIiter call so it is per-Newton-solve, not cumulative.
- cktdefs.h: int CKTosdiNewtonIter.
- osdi.h: uint32_t newton_iter appended to the OsdiSimInfo tail (field 12, after at_final_step). ABI-safe: older .osdi binaries that never read it stay compatible (verified -- 14nm FinFET's old-header models run unchanged on the new binary).
- osdiload.c: .newton_iter = (uint32_t)ckt->CKTosdiNewtonIter in the per-eval sim_info initializer.
OpenVA side (see openva_changes.txt) reads it as ParamKind::NewtonIter and tightens the synthesized clamp hyperbolically with iteration count.
Validated end-to-end (new NGSPICE + adaptive bsimbulk.osdi):
0.9V LV inverter (inverter_tb/tran): 1930 rows, runs full 1us, 0 err (historical plateau was 131 rows / abort at 100 ns). 22nm BSIM6 driver_lv_2v5_tb: 477010 rows, 0 err (was ~476973 -- no regress). 14nm FinFET tb_driver: 325966 rows, 0 err (old-header osdi -- back-compat).
================================================================
STEP 36: OSDI absdelay exact transport — simulator-managed ring
================================================================
Paths: src/osdi/osdi.h (descriptor num_delay_sites; SimInfo
delay_input/delay_maxtd/delay_state/delay_read) src/osdi/osdidefs.h (OsdiExtraInstData per-site rings + decl) src/osdi/osdicallbacks.c (osdi_delay_read: interpolating reader) src/osdi/osdiload.c (lazy alloc + SimInfo wiring; osdi_delay_push + OSDIaccept accept-hook) src/osdi/osdiext.h (OSDIaccept decl) src/osdi/osdiinit.c (DEVaccept = OSDIaccept) src/osdi/osdiregistry.c (descriptor-size-guarded num_delay_sites) src/include/NGSPICE/osdiitf.h (OsdiRegistryEntry.num_delay_sites)
OpenVA absdelay was a (2,2) Pade approximant -> rings on edges faster than ~1/tau (serdes/transmission-line use is exactly that regime). Replaced with a TRUE transport delay backed by a simulator-managed per-instance (time,value) ring:
- Model writes its current input to SimInfo.delay_input[site] each eval and reads x(abstime-td) via SimInfo.delay_read = osdi_delay_read (linear interp over the ring; interpolates toward the current input for td<dt so small delays are exact).
- OSDIaccept (registered as DEVaccept, fired once per ACCEPTED step from CKTaccept) pushes (CKTtime, delay_input[site]) into each ring -> only the converged trajectory is recorded (the model can't tell accept from a Newton iterate; the simulator can). osdi_delay_push grows geometrically and prunes samples older than abstime - max_td (the chosen dynamic-grow+prune policy; max_td<0 -> 1<<22 ceiling).
- ABI-SAFETY GUARD: descriptor field num_delay_sites is read only when the published OSDI_DESCRIPTOR_SIZE reaches it (osdiregistry.c offsetof check), cached in OsdiRegistryEntry; a pre-2C .osdi reads 0, not garbage. All new descriptor/SimInfo fields are tail-appended.
Verified (new NGSPICE): absdelay of a 10 ps-edge PULSE delayed 0.25 ns -> v(out) = v(in) shifted 0.2507 ns (0.7 ps err at 2 ps maxstep), range [0,1] exactly -- NO Pade ringing. No regression: 22nm BSIM6 driver_lv_2v5_tb 477010/0 and 14nm FinFET tb_driver 325966/0 with their PRE-2C models on the new binary (the guard in action).
KNOWN LIMITATION: time-domain mechanism -> delays in transient only; AC reduces to the instantaneous input (no e^-jwtd). AC delay = follow-up. -> RESOLVED in STEP 37 below.
STEP 37: OSDI absdelay exact AC phase delay — e^{-jw*td} stamp
================================================================
Paths: src/osdi/osdi.h (JACOBIAN_ENTRY_DELAY=16; descriptor tail:
num_delay_jacobian_entries, write_jacobian_array_delay, delay_jacobian_sites) src/osdi/osdidefs.h (OsdiExtraInstData.delay_td_arr) src/osdi/osdiload.c (alloc delay_td_arr alongside the rings) src/osdi/osdicallbacks.c (osdi_delay_read stashes the clamped td) src/osdi/osdiacld.c (the AC stamp, in OSDIacLoad)
Companion to STEP 36. The transport ring makes absdelay exact in TRANSIENT; this makes it exact in AC. G+jwC cannot represent the transcendental delay e^{-jw*td}, so OpenVA emits the delay jacobian VALUE V = ddx(-input) (real, computed at the OP) as a tail-appended descriptor array, and the simulator supplies the frequency factor: for each jacobian entry flagged JACOBIAN_ENTRY_DELAY, OSDIacLoad stamps V*cos(w*td) into the real part and V*(-sin(w*td)) into the imag part of the complex matrix element. td(site) is the per-instance operating-point delay captured by osdi_delay_read into delay_td_arr. ABI-guarded by entry->num_delay_sites (the STEP 36 guard), so a pre-AC-delay .osdi never touches the new tail fields; transient unchanged.
Verified: integration_tests/ABSDELAY/absdelay_ac (td=0.25 ns) -> |v(out)|=1 at all frequencies with phase = -w*td exactly (-90deg @1GHz, -180deg @2GHz); transient still 0.2509 ns. NB the kT/q $limit synthesis (OpenVA a16d2c7) is OpenVA-side only -- no NGSPICE change, so no STEP here for it.
STEP 38: exclude OSDI analog-operator state nodes from gmin stepping
================================================================
(pairs with OpenVA realize_cascade_real_roots for laplace_*/zi_*)
Paths: src/include/NGSPICE/cktdefs.h (CKTnode.nogmin:1 flag)
src/include/NGSPICE/smpdefs.h (sSMPmatrix.gmin_skip, char* by node#) src/osdi/osdisetup.c (set nogmin on implicit_equation_* nodes) src/maths/ni/niiter.c (build gmin_skip from CKTnodes once) src/maths/sparse/spsmp.c (LoadGmin honors skip; SMPdestroy frees) src/maths/KLU/klusmp.c (LoadGmin_CSC_skip honors skip)
Problem. A multi-pole laplace_*/zi_* OpenVA model realizes its transfer function as cascaded internal implicit-equation state nodes. NGSPICE's dynamic/true gmin stepping (LoadGmin) adds CKTdiagGmin to EVERY matrix diagonal. For a real circuit node that is a harmless leak-to-ground; for an integrator state node whose resistive diagonal is the pole value -p_k (small, or zero for a pole at the origin) it CORRUPTS the state equation, so as gmin climbs during the homotopy the poles shift and Newton diverges ("gmin stepping failed"). `option gminsteps=0` converges, default stepping fails. (The realization is the OpenVA cascade -- which fixes the prior SINGULAR-matrix failure; this is the other, NGSPICE, half: making the cascade converge under gmin stepping.)
Fix: Flag the synthesized state nodes and skip them in the diagonal-gmin pass.
- CKTnode.nogmin:1 -- new flag bit.
- osdisetup.c: in the internal-node creation loop, set tmp->nogmin ONLY when the OSDI node name begins "implicit_equation_" (the name OpenVA emits for SimUnknownKind::Implicit; physical internal nodes -- BSIM RD/RS, NQS -- keep their real Verilog-A names and are left alone, so PDK DC convergence is unaffected). This name-based discrimination is the robustness crux: flagging "all internal nodes" would strip gmin stepping from physical parasitic nodes that need it.
- niiter.c: at the top of NIiter, lazily build matrix->gmin_skip -- a char array indexed by node number, 1 where node->nogmin. Cached on the matrix; TMALLOC zero-fills; NULL == legacy "gmin on every diagonal".
- spsmp.c LoadGmin: Diag is indexed by internal row I; map back to the node number via Matrix->IntToExtRowMap[I] and skip when gmin_skip[ext]. Free gmin_skip in SMPdestroy (SP_FREE -- matches the TMALLOC allocator).
- klusmp.c: LoadGmin_CSC_skip subsumes (and replaces) the old LoadGmin_CSC at the two non-CIDER factor sites. KLU column i is post-collapse; the node number is NodeCollapsingNewToOld[i] + 1 (KLU cols 0-based, node numbers 1-based -- cf. singular_col+1 reporting). When gmin_skip/NewToOld is NULL it is byte-for-byte the legacy "gmin on every diagonal" loop, so non-OSDI and CIDER paths are unchanged.

View File

@ -1,4 +1,4 @@
This branch (jfisher/osdi-fixes) is for testing verilog-a. It's really just an ergonomic fix that I needed for testing Verilog-a. This branch is for testing verilog-a. It's really just an ergonomic fix that I needed for testing Verilog-a.
It adds 2 things. An option for the old pre_osdi syntax (I had a load of control files that used this and it was easier to just add it here - it seems to do no harm) and an option for case preservation in "set sourcepath" My sourcepath was /projects/VA_test/ in a linux environment. This was being turned into va_test so any subsequent source or osdi lookup relative to sourcepath would fail. It adds 2 things. An option for the old pre_osdi syntax (I had a load of control files that used this and it was easier to just add it here - it seems to do no harm) and an option for case preservation in "set sourcepath" My sourcepath was /projects/VA_test/ in a linux environment. This was being turned into va_test so any subsequent source or osdi lookup relative to sourcepath would fail.

2514
ng_existing_code_changes.txt Normal file

File diff suppressed because it is too large Load Diff

1
openva_location.txt Normal file
View File

@ -0,0 +1 @@
https://sourceforge.net/projects/openva/

View File

@ -331,8 +331,30 @@ measure_extract_variables(char *line)
(strcasecmp(analysis, "TRAN") == 0)) { (strcasecmp(analysis, "TRAN") == 0)) {
analysis = copy(analysis); analysis = copy(analysis);
} else { } else {
/* sometimes operation is optional - for now just pick trans */ /* HSPICE-shorthand: `.measure <result> ...` with the
analysis = copy("TRAN"); * analysis type omitted. The second token (held here in
* `analysis`) is actually the measurement function (find /
* trig / when / avg / ...) not what we just gettok'd.
* Infer the analysis from whatever the deck declares.
* Defaulting to TRAN (the old behaviour) silently broke
* decks with a .dc analysis because the save list ended
* up tagged "tran" and the .dc plot saw none of those
* names OUTpBeginPlot reported
* Error: no data saved for D.C. Transfer curve analysis
* Scan ci_deck (the main card list) ci_commands holds
* only .print/.plot/.meas/.op/.tf/.four/.width/.sndprint/
* .sndparam, NOT analysis directives. */
const char *inferred = "TRAN";
struct card *c;
for (c = ft_curckt ? ft_curckt->ci_deck : NULL;
c; c = c->nextcard) {
const char *w = c->line;
if (!w || *w == '*') continue;
if (ciprefix(".dc", w)) { inferred = "DC"; break; }
if (ciprefix(".ac", w)) { inferred = "AC"; break; }
if (ciprefix(".tran", w)) { inferred = "TRAN"; break; }
}
analysis = copy(inferred);
} }
do { do {

View File

@ -531,9 +531,20 @@ inp_spsource(FILE *fp, bool comfile, char *filename, bool intfile)
if (fp || intfile) { if (fp || intfile) {
deck = inp_readall(fp, dir_name, filename, comfile, intfile, &expr_w_temper); deck = inp_readall(fp, dir_name, filename, comfile, intfile, &expr_w_temper);
/* files starting with *ng_script are user supplied command files */ /* files starting with *ng_script are user supplied command files.
if (deck && ciprefix("*ng_script", deck->line)) * Walk past any leading blank cards (see same logic in
comfile = TRUE; * inpcom.c::inp_readall) so a leading blank line doesn't
* defeat comfile detection. */
if (deck) {
struct card *probe = deck;
while (probe && probe->line &&
(probe->line[0] == '\0' ||
probe->line[0] == '\n' ||
(probe->line[0] == '\r' && probe->line[1] == '\n')))
probe = probe->nextcard;
if (probe && ciprefix("*ng_script", probe->line))
comfile = TRUE;
}
/* save a copy of the deck for later reloading with 'mc_source' */ /* save a copy of the deck for later reloading with 'mc_source' */
if (deck && !comfile) { if (deck && !comfile) {
/* stored to new circuit ci_mcdeck in fcn */ /* stored to new circuit ci_mcdeck in fcn */
@ -598,9 +609,18 @@ inp_spsource(FILE *fp, bool comfile, char *filename, bool intfile)
return 0; return 0;
} }
/* files starting with *ng_script are user supplied command files */ /* files starting with *ng_script are user supplied command files.
if (ciprefix("*ng_script", deck->line)) * Walk past any leading blank cards (see same logic above). */
comfile = TRUE; {
struct card *probe = deck;
while (probe && probe->line &&
(probe->line[0] == '\0' ||
probe->line[0] == '\n' ||
(probe->line[0] == '\r' && probe->line[1] == '\n')))
probe = probe->nextcard;
if (probe && ciprefix("*ng_script", probe->line))
comfile = TRUE;
}
if (!comfile) { if (!comfile) {
/* Extract the .option lines from the deck into 'options', /* Extract the .option lines from the deck into 'options',
@ -780,9 +800,18 @@ inp_spsource(FILE *fp, bool comfile, char *filename, bool intfile)
/* Special control lines outside of .control section? */ /* Special control lines outside of .control section? */
if (prefix("*#", s)) bool from_hash = prefix("*#", s);
if (from_hash)
s = skip_ws(s + 2); s = skip_ws(s + 2);
/* *#.foo lines are SPICE deck cards embedded for other tools'
* backward compatibility, not ngspice commands. Skip silently. */
if (from_hash && *s == '.') {
ld->nextcard = dd->nextcard;
line_free(dd, FALSE);
continue;
}
if (ciprefix("pre_", s)) { if (ciprefix("pre_", s)) {
/* Assemble all commands starting with pre_ after stripping /* Assemble all commands starting with pre_ after stripping
* pre_, to be executed before circuit parsing. * pre_, to be executed before circuit parsing.
@ -940,6 +969,7 @@ inp_spsource(FILE *fp, bool comfile, char *filename, bool intfile)
return 1; return 1;
} }
/* replace agauss(x,y,z) in each b-line by suitable value, one for all */ /* replace agauss(x,y,z) in each b-line by suitable value, one for all */
bool statlocal = cp_getvar("statlocal", CP_BOOL, NULL, 0); bool statlocal = cp_getvar("statlocal", CP_BOOL, NULL, 0);
if (!statlocal) { if (!statlocal) {

View File

@ -23,6 +23,7 @@ Author: 1985 Wayne A. Christopher
#include "ngspice/compatmode.h" #include "ngspice/compatmode.h"
#include "ngspice/cpdefs.h" #include "ngspice/cpdefs.h"
#include "ngspice/dstring.h" #include "ngspice/dstring.h"
#include "ngspice/hash.h"
#include "ngspice/dvec.h" #include "ngspice/dvec.h"
#include "ngspice/ftedefs.h" #include "ngspice/ftedefs.h"
#include "ngspice/fteext.h" #include "ngspice/fteext.h"
@ -98,6 +99,12 @@ struct function_env
int num_parameters; int num_parameters;
const char *accept; const char *accept;
} *functions; } *functions;
/* Hash on `name` for O(1) lookup in find_function. Foundry PDKs
* register hundreds-to-thousands of .funcs (Samsung 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. */
NGHASHPTR fcn_hash;
}; };
struct func_temper struct func_temper
@ -143,6 +150,7 @@ static void inp_reorder_params(
static int inp_split_multi_param_lines(struct card *deck, int line_number); static int inp_split_multi_param_lines(struct card *deck, int line_number);
static void inp_sort_params(struct card *param_cards, static void inp_sort_params(struct card *param_cards,
struct card *card_bf_start, struct card *s_c, struct card *e_c); struct card *card_bf_start, struct card *s_c, struct card *e_c);
static int identifier_char(char c);
static void inp_compat(struct card *deck); static void inp_compat(struct card *deck);
static void inp_bsource_compat(struct card *deck); static void inp_bsource_compat(struct card *deck);
static bool inp_temper_compat(struct card *card); static bool inp_temper_compat(struct card *card);
@ -1072,9 +1080,22 @@ struct card *inp_readall(FILE *fp, const char *dir_name, const char* file_name,
return cc; return cc;
} }
/* files starting with *ng_script are user supplied command files */ /* files starting with *ng_script are user supplied command files.
if (cc && ciprefix("*ng_script", cc->line)) * Walk past any leading blank cards so a `*ng_script` that follows
comfile = TRUE; * a blank top-of-file line is still recognised. Without this,
* such a file gets treated as a spice deck and the inner
* `.control { source ... }` triggers a second full inp_readall
* pass doubling parse time on big PDK decks. */
{
struct card *probe = cc;
while (probe && probe->line &&
(probe->line[0] == '\0' ||
probe->line[0] == '\n' ||
(probe->line[0] == '\r' && probe->line[1] == '\n')))
probe = probe->nextcard;
if (probe && ciprefix("*ng_script", probe->line))
comfile = TRUE;
}
/* The following processing of an input file is not required for command /* The following processing of an input file is not required for command
files like spinit or .spiceinit, so return command files here. */ files like spinit or .spiceinit, so return command files here. */
@ -1120,6 +1141,12 @@ struct card *inp_readall(FILE *fp, const char *dir_name, const char* file_name,
inp_probe(working); inp_probe(working);
/* HSPICE element scale: wrap geometry in `scale`-declaring subckt
bodies BEFORE numparam preparation, so numparam evaluates the
effective dimensions per instance (see subckt.c). Must precede
inp_fix_for_numparam(), which marks the expressions to substitute. */
inp_apply_subckt_scale(working);
inp_fix_for_numparam(subckt_w_params, working); inp_fix_for_numparam(subckt_w_params, working);
inp_remove_excess_ws(working); inp_remove_excess_ws(working);
@ -1406,11 +1433,28 @@ static struct inp_read_t inp_read(FILE* fp, int call_depth, const char* dir_name
/* OK -- now we have loaded the next line into 'buffer'. Process it. /* OK -- now we have loaded the next line into 'buffer'. Process it.
*/ */
if (first) { if (first) {
/* Files starting *ng_script are user supplied command files. */ /* Files starting *ng_script are user supplied command files.
*
if (ciprefix("*ng_script", buffer)) * Treat any leading blank lines as not-yet-first, so a
comfile = TRUE; * comfile that has a blank line above `*ng_script` is
first = FALSE; * still recognised. Without this, ngspice processes
* such a file twice: once as the "main" deck (because
* comfile stays FALSE, the !comfile branch runs full
* parse + print_compat_mode + Circuit:), and again
* when its `.control { source other.net }` triggers a
* fresh inp_spsource for the inner netlist. Real
* symptom: doubled "Compatibility modes selected" /
* "Circuit:" headers and ~2x parse time. */
bool buffer_blank = (strcmp(buffer, "\n") == 0) ||
(strcmp(buffer, "\r\n") == 0) ||
(buffer[0] == '\0');
if (buffer_blank) {
/* skip — re-check on the next line */
} else {
if (ciprefix("*ng_script", buffer))
comfile = TRUE;
first = FALSE;
}
} }
/* If input line is blank, ignore it & continue looping. */ /* If input line is blank, ignore it & continue looping. */
@ -1482,6 +1526,46 @@ static struct inp_read_t inp_read(FILE* fp, int call_depth, const char* dir_name
tfree(buffer); tfree(buffer);
continue; continue;
} }
/* HSPICE .alter: re-runs the simulation with the lib section
* and parameter overrides defined between this .alter and the
* next .alter / .end. Not implemented in ngspice silently
* skip the directive line AND every line that follows until
* the next .alter (skip continues) or .end (stop skipping;
* let .end terminate the deck normally). Without this, the
* contained `.del lib '...'` / `.lib '...' <section>` lines
* would re-load a different process corner on top of the
* already-loaded main circuit. Single warning per deck. */
{
static bool alter_warned = FALSE;
static bool in_alter_block = FALSE;
if (in_alter_block) {
/* `.end` closes the alter block AND the deck — let
* the main loop process it. Another `.alter` opens
* a new alter block keep skipping. */
if (ciprefix(".end", buffer) && !ciprefix(".ends", buffer)) {
in_alter_block = FALSE;
/* fall through to normal processing of .end */
} else {
/* still inside an alter block — skip this line */
tfree(buffer);
continue;
}
}
if (ciprefix(".alter", buffer)) {
if (!alter_warned) {
fprintf(cp_err, "Warning: Dot command .alter is not "
"implemented; .alter blocks "
"(re-simulation with process corner "
"/ parameter overrides) are skipped.\n");
fprintf(cp_err, " This message will be posted "
"only once!\n\n");
alter_warned = TRUE;
}
in_alter_block = TRUE;
tfree(buffer);
continue;
}
}
/* now handle .include statements */ /* now handle .include statements */
if (ciprefix(".include", buffer) || ciprefix(".inc", buffer)) { if (ciprefix(".include", buffer) || ciprefix(".inc", buffer)) {
@ -1808,9 +1892,22 @@ static struct inp_read_t inp_read(FILE* fp, int call_depth, const char* dir_name
ciprefix("set sourcepath", buffer) || ciprefix("set sourcepath", buffer) ||
ciprefix("strcmp", buffer) || ciprefix("strcmp", buffer) ||
ciprefix("strstr", buffer))))) { ciprefix("strstr", buffer))))) {
/* lower case for all other lines */ /* Lower case for all other lines, but preserve the
for (s = buffer; *s && (*s != '\n'); s++) * content INSIDE double-quoted strings. Foundry PDKs
*s = tolower_c(*s); * embed case-sensitive filenames in .param/.subckt
* lines via constructs like
* `.param x = 'table_param(str("./RF_COMPONENTS/foo.table"), ...)'`.
* Without quote preservation, the inner path gets
* folded to lowercase and fails to open on Linux. */
bool in_dq = false;
for (s = buffer; *s && (*s != '\n'); s++) {
if (*s == '"') {
in_dq = !in_dq;
continue;
}
if (!in_dq)
*s = tolower_c(*s);
}
} }
else { else {
/* s points to end of buffer for all cases not treated so far /* s points to end of buffer for all cases not treated so far
@ -3512,6 +3609,16 @@ void inp_casefix(char *string)
if (*string == '"') { if (*string == '"') {
if (!keepquotes) if (!keepquotes)
*string++ = ' '; *string++ = ' ';
else
string++; /* keepquotes: preserve opening " and
ALSO preserve content inside quotes
(case-sensitive filenames in str("...")
for HSPICE table_param et al.).
Without this, the loop below entered
AT the opening quote and exited
immediately, leaving the inner chars
to be lowercased by the outer-loop
case-folding. */
while (*string && *string != '"') while (*string && *string != '"')
string++; string++;
if (*string == '\0') if (*string == '\0')
@ -3617,9 +3724,19 @@ static void inp_stripcomments_line(char *s, bool cs, bool inc)
} }
/* outside of .control section, and not in PS mode */ /* outside of .control section, and not in PS mode */
else if (!cs && (c == '$') && !newcompat.ps) { else if (!cs && (c == '$') && !newcompat.ps) {
/* The character before '&' has to be ',' or ' ' or tab. /* HSPICE treats '$' as an end-of-line comment regardless of
A valid numerical expression directly before '$' is not yet * the preceding character foundry decks (Samsung 14LPU,
supported. */ * TSMC, GF) routinely write `...)'$ comment` or `...=10u$ ...`
* with no separator. In ngbehavior=hs / hsa, accept that.
*
* Outside HS mode keep the original conservative rule (only
* after space/comma/tab) so a literal `$` inside a token
* (e.g. a parameter named `foo$bar`, environment vars, etc.)
* isn't misread as a comment delimiter. */
if (newcompat.hs) {
d--;
break;
}
if ((d - 2 >= s) && if ((d - 2 >= s) &&
((d[-2] == ' ') || (d[-2] == ',') || (d[-2] == '\t'))) { ((d[-2] == ' ') || (d[-2] == ',') || (d[-2] == '\t'))) {
d--; d--;
@ -3878,6 +3995,18 @@ static void inp_fix_for_numparam(
if (*(c->line) == '*' || ciprefix(".lib", c->line)) if (*(c->line) == '*' || ciprefix(".lib", c->line))
continue; continue;
/* `.del lib '<path>' <section>` and `.include` / `.inc`
* take literal file paths; skip the single-to-brace quote
* conversion that would otherwise hand the path to
* numparam as an expression and trigger
* Number format error: "../path...} <section>"
* Observed on GF55 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) ||
ciprefix(".include", c->line) || ciprefix(".inc ", c->line))
continue;
/* exclude lines between .control and .endc from getting quotes /* exclude lines between .control and .endc from getting quotes
* changed */ * changed */
if (ciprefix(".control", c->line)) if (ciprefix(".control", c->line))
@ -4389,6 +4518,8 @@ static struct function *new_function(struct function_env *env, char *name)
f->next = env->functions; f->next = env->functions;
env->functions = f; env->functions = f;
if (env->fcn_hash)
nghash_insert(env->fcn_hash, name, f);
return f; return f;
} }
@ -4396,12 +4527,25 @@ static struct function *new_function(struct function_env *env, char *name)
static struct function *find_function(struct function_env *env, char *name) static struct function *find_function(struct function_env *env, char *name)
{ {
struct function *f; /* Walk the env chain inner-to-outer. At each level, hash-lookup
* the function name (O(1)). Total cost is O(env_chain_depth) per
for (; env; env = env->up) * call instead of O(total_funcs). See struct function_env comment
for (f = env->functions; f; f = f->next) * for why this matters. */
if (strcmp(f->name, name) == 0) for (; env; env = env->up) {
if (env->fcn_hash) {
struct function *f =
(struct function *)nghash_find(env->fcn_hash, name);
if (f)
return f; return f;
} else {
/* Fallback for envs created before the hash was added or
* if hash init failed (out of memory). Same semantics as
* the original linear scan. */
for (struct function *f = env->functions; f; f = f->next)
if (strcmp(f->name, name) == 0)
return f;
}
}
return NULL; return NULL;
} }
@ -4826,6 +4970,9 @@ static struct function_env *new_function_env(struct function_env *up)
env->up = up; env->up = up;
env->functions = NULL; env->functions = NULL;
/* Initial size is a guess; nghash grows automatically. Big PDK
* envs reach ~1300 funcs, so start with 256 to reduce rehashes. */
env->fcn_hash = nghash_init(256);
return env; return env;
} }
@ -4836,6 +4983,13 @@ static struct function_env *delete_function_env(struct function_env *env)
struct function_env *up = env->up; struct function_env *up = env->up;
struct function *f; struct function *f;
/* Free the hash first. Pass NULL for both freers — the function
* pointers inside the hash are owned by the linked list (freed
* below), and the keys are owned by the function structs (freed
* by free_function). */
if (env->fcn_hash)
nghash_free(env->fcn_hash, NULL, NULL);
for (f = env->functions; f;) { for (f = env->functions; f;) {
struct function *here = f; struct function *here = f;
f = f->next; f = f->next;
@ -5483,37 +5637,91 @@ static void inp_sort_params(struct card *param_cards,
num_params++; num_params++;
} }
// look for duplicately defined parameters and mark earlier one to skip /* Duplicate detection and dependency scanning via a single hash from
// param list is ordered as defined in netlist * param_name -> (i+1). Replaces two nested-N^2 loops:
* - duplicate-check: was O(N^2) strcmp pairs
* - dependency-scan: was O(N^2 * L) search_plain_identifier calls,
* 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
* ~500 times, totalling many billions of ops in the old code).
*
* Encoding: hash stores `(void *)(intptr_t)(i + 1)` so that the NULL
* return value from nghash_find unambiguously means "not present"
* (param index 0 is a valid hit, encoded as the integer 1).
*
* For repeated names, the hash overwrites with the LATER index, so
* the recorded index for any name is the latest occurrence. In the
* duplicate-mark pass below, every entry whose own index differs
* from the hash's recorded index gets skip=1, matching the original
* semantics (earlier duplicates are skipped, the last one wins). */
NGHASHPTR name_to_idx = nghash_init(num_params > 0 ? num_params : 1);
for (i = 0; i < num_params; i++)
nghash_insert(name_to_idx, deps[i].param_name,
(void *)(intptr_t)(i + 1));
skipped = 0; skipped = 0;
for (i = 0; i < num_params; i++) { for (i = 0; i < num_params; i++) {
for (j = i + 1; j < num_params; j++) int latest =
if (strcmp(deps[i].param_name, deps[j].param_name) == 0) (int)(intptr_t)nghash_find(name_to_idx, deps[i].param_name) - 1;
break; if (latest != i) {
if (j < num_params) {
deps[i].skip = 1; deps[i].skip = 1;
skipped++; skipped++;
} }
} }
for (i = 0; i < num_params; i++) /* Tokenize each non-skipped param's expression once; for each
if (!deps[i].skip) { * identifier-shaped token, look up in name_to_idx. If the token
char *param = deps[i].param_name; * names another (non-skipped) param, record the dependency. Token
for (j = 0; j < num_params; j++) * boundaries use the same identifier_char predicate as
if (j != i && * search_plain_identifier, so behavior matches the old code. */
search_plain_identifier(deps[j].param_str, param)) { for (j = 0; j < num_params; j++) {
for (ind = 0; deps[j].depends_on[ind]; ind++) if (deps[j].skip) continue;
; char *expr = deps[j].param_str;
deps[j].depends_on[ind++] = param; if (!expr) continue;
if (ind == DEPENDSON) {
fprintf(stderr, "Error in netlist: Too many parameter dependencies (> %d)\n", ind); char *p = expr;
while (*p) {
while (*p && !identifier_char(*p)) p++;
if (!*p) break;
char *tok_start = p;
while (*p && identifier_char(*p)) p++;
char saved = *p;
*p = '\0';
int dep_idx =
(int)(intptr_t)nghash_find(name_to_idx, tok_start) - 1;
*p = saved;
if (dep_idx >= 0 && dep_idx != j && !deps[dep_idx].skip) {
/* avoid recording the same dependency twice for this
* expression (e.g. `a + 2*a`); scan the small list */
bool already = false;
for (ind = 0; deps[j].depends_on[ind]; ind++)
if (deps[j].depends_on[ind] ==
deps[dep_idx].param_name) {
already = true;
break;
}
if (!already) {
deps[j].depends_on[ind] = deps[dep_idx].param_name;
if (ind + 1 == DEPENDSON) {
fprintf(stderr,
"Error in netlist: Too many parameter "
"dependencies (> %d)\n",
ind + 1);
fprintf(stderr, " Please check your netlist.\n"); fprintf(stderr, " Please check your netlist.\n");
controlled_exit(EXIT_BAD); controlled_exit(EXIT_BAD);
} }
deps[j].depends_on[ind] = NULL; deps[j].depends_on[ind + 1] = NULL;
} }
}
} }
}
nghash_free(name_to_idx, NULL, NULL);
max_level = 0; max_level = 0;
for (i = 0; i < num_params; i++) { for (i = 0; i < num_params; i++) {

View File

@ -301,16 +301,38 @@ do_measure(
continue; continue;
} }
/* HSPICE-shorthand: `.measure <result> <fn> ...` (no
* explicit analysis type). When the first token doesn't
* match a recognized analysis type, treat it as the
* result name and infer the analysis from the deck.
* Scan ci_deck ci_commands holds only .print/.meas/etc.,
* NOT .dc/.tran/.ac. */
if (chkAnalysisType(an_type) != TRUE) { if (chkAnalysisType(an_type) != TRUE) {
if (!chk_only) { const char *inferred = "tran";
fprintf(cp_err, "Error: unrecognized analysis type '%s' for the following .meas statement on line %d:\n", an_type, meas_card->linenum); struct card *c;
fprintf(cp_err, " %s\n", meas_card->line); for (c = ft_curckt ? ft_curckt->ci_deck : NULL;
c; c = c->nextcard) {
const char *w = c->line;
if (!w || *w == '*') continue;
if (ciprefix(".dc", w)) { inferred = "dc"; break; }
if (ciprefix(".ac", w)) { inferred = "ac"; break; }
if (ciprefix(".tran", w)) { inferred = "tran"; break; }
} }
char *new_resname = an_type;
txfree(an_type); char *new_meastype = resname;
txfree(resname); char *new_extra = meastype;
txfree(meastype); an_type = copy(inferred);
continue; resname = new_resname;
meastype = new_meastype;
/* meastype was the third gettok; shift the rest of
* the line back so the remaining parser sees the new
* meastype's argument as the next token. The line
* already advanced past `meastype` (now extra), so
* we need to splice `extra` back into the front of
* `line` (concatenate with a space). */
char *new_line_buf = tprintf("%s %s", new_extra, line);
line = new_line_buf;
tfree(new_extra);
} }
/* print header before evaluating first .meas line */ /* print header before evaluating first .meas line */
else if (first_time) { else if (first_time) {
@ -431,16 +453,30 @@ do_measure(
continue; continue;
} }
/* HSPICE-shorthand: same path as the first-pass fix at
* line 304. Treat the original first token as resname,
* shift everything back one slot, and splice the old
* meastype back into `line` for the rest of the parser. */
if (chkAnalysisType(an_type) != TRUE) { if (chkAnalysisType(an_type) != TRUE) {
if (!chk_only) { const char *inferred = "tran";
fprintf(cp_err, "Error: unrecognized analysis type '%s' for the following .meas statement on line %d:\n", an_type, meas_card->linenum); struct card *c2;
fprintf(cp_err, " %s\n", meas_card->line); for (c2 = ft_curckt ? ft_curckt->ci_deck : NULL;
c2; c2 = c2->nextcard) {
const char *w = c2->line;
if (!w || *w == '*') continue;
if (ciprefix(".dc", w)) { inferred = "dc"; break; }
if (ciprefix(".ac", w)) { inferred = "ac"; break; }
if (ciprefix(".tran", w)) { inferred = "tran"; break; }
} }
char *new_resname2 = an_type;
txfree(an_type); char *new_meastype2 = resname;
txfree(resname); char *new_extra2 = meastype;
txfree(meastype); an_type = copy(inferred);
continue; resname = new_resname2;
meastype = new_meastype2;
char *new_line_buf2 = tprintf("%s %s", new_extra2, line);
line = new_line_buf2;
tfree(new_extra2);
} }
if (strcmp(an_name, an_type) != 0) { if (strcmp(an_name, an_type) != 0) {
txfree(an_type); txfree(an_type);
@ -551,9 +587,42 @@ measure_parse_line(char *line)
char *item; /* parsed item */ char *item; /* parsed item */
char *long_str; /* concatenated string */ char *long_str; /* concatenated string */
char *extra_item; /* extra item */ char *extra_item; /* extra item */
bool shorthand_first = FALSE; /* synth an_type token */
wl = NULL; wl = NULL;
line = nexttok(line); line = nexttok(line);
/* HSPICE-shorthand: `.measure <result> <fn> ...` (no
* explicit analysis type). Peek at the first real token; if
* it isn't DC / AC / TRAN / SP, infer from the deck and
* synthesize a leading wordlist entry so get_measure2's
* positional parser (mAnalysis at [0], mName at [1],
* mFunction at [2]) lines up. */
{
char *probe = line;
char *probe_tok = gettok(&probe);
if (probe_tok) {
if (strcasecmp(probe_tok, "dc") != 0 &&
strcasecmp(probe_tok, "ac") != 0 &&
strcasecmp(probe_tok, "tran") != 0 &&
strcasecmp(probe_tok, "sp") != 0) {
const char *inferred = "tran";
struct card *c;
for (c = ft_curckt ? ft_curckt->ci_deck : NULL;
c; c = c->nextcard) {
const char *w = c->line;
if (!w || *w == '*') continue;
if (ciprefix(".dc", w)) { inferred = "dc"; break; }
if (ciprefix(".ac", w)) { inferred = "ac"; break; }
if (ciprefix(".tran", w)) { inferred = "tran"; break; }
}
new_item = wl_cons(copy(inferred), NULL);
wl = wl_append(wl, new_item);
shorthand_first = TRUE;
}
tfree(probe_tok);
}
(void)shorthand_first;
}
do { do {
item = gettok(&line); item = gettok(&line);
if (!(item)) if (!(item))

View File

@ -8,9 +8,11 @@ libnumparam_la_SOURCES = \
spicenum.c \ spicenum.c \
xpressn.c \ xpressn.c \
mystring.c \ mystring.c \
table_param.c \
general.h \ general.h \
numpaif.h \ numpaif.h \
numparam.h numparam.h \
table_param.h
AM_CPPFLAGS = @AM_CPPFLAGS@ -I$(top_srcdir)/src/include AM_CPPFLAGS = @AM_CPPFLAGS@ -I$(top_srcdir)/src/include
AM_CFLAGS = $(STATIC) AM_CFLAGS = $(STATIC)

View File

@ -58,6 +58,13 @@ typedef struct { /* the input scanner data structure */
int hs_compatibility; /* allow extra keywords */ int hs_compatibility; /* allow extra keywords */
int linecount; /* number of lines in deck */ int linecount; /* number of lines in deck */
char* cardline; /* line of card treated currently */ char* cardline; /* line of card treated currently */
const char* cardsource; /* linesource of card treated currently
* (read-only; owned by the card) used
* by HSPICE table_param() to resolve
* relative .table paths against the
* directory of the .lib that emitted
* the call */
bool suppress_errors; /* when true, message() is a no-op */
} dico_t; } dico_t;
@ -75,3 +82,43 @@ entry_t *entrynb(dico_t *dico, char *s);
entry_t *attrib(dico_t *, NGHASHPTR htable, char *t, char op); entry_t *attrib(dico_t *, NGHASHPTR htable, char *t, char op);
void del_attrib(void *); void del_attrib(void *);
void nupa_copy_inst_entry(char *param_name, entry_t *proto); void nupa_copy_inst_entry(char *param_name, entry_t *proto);
/* Evaluate a brace-body expression in a transient scope: push
* the (name, value) pairs, run numparam's formula(), pop the scope.
* No persistent side effects. Returns 0 / writes result to *out on
* success, non-zero on error. See xpressn.c for full docs. */
int nupa_eval_with_scope(dico_t *dico, const char *expr,
const char *const *names, const double *values,
int n, double *out);
/* External accessor for the active dico_t. Used by callers outside
* numparam (e.g. osdi_defer.c) that need to invoke
* nupa_eval_with_scope() without seeing the static dicoS. */
dico_t *nupa_get_dico(void);
/* Mark a line as inert from numparam's perspective — used when a
* later pass (e.g. inp_subcktexpand's X-prefixOSDI fallback)
* comments out a line that numparam had already categorized as a
* subckt call or .param. Without this, numparam still tries to
* dispatch the now-commented-out line and emits "illegal subckt
* call" / "Cannot find subcircuit" errors. Also rewrites the
* stored dynrefptr line to start with `*` so any remaining
* downstream pass treats it as comment. */
void nupa_skip_line(int linenum);
/* Public-name lookup for a real-valued numparam entry. Returns 1 and
* sets *value on success; returns 0 if name is unknown or not a
* NUPA_REAL. Used by spicelib/parser/inpdpar.c so HSPICE-style bare
* `.param` identifiers as device value fields (e.g. `vgs n2 n3 vgswp`
* paired with `.param vgswp=0`) resolve to a leading numeric value
* instead of erroring with "unknown parameter (vgswp)". */
int nupa_get_real(const char *name, double *value);
/* Companion writer to nupa_get_real. Updates the value of an
* existing NUPA_REAL entry. Returns 1 on success, 0 if the name
* is unknown or not NUPA_REAL. Used by the .dc analysis loop
* (src/spicelib/analysis/dctrcurv.c) when sweeping a .param: each
* step updates the dictionary value, then pushes it to any V/I
* source that bound this .param at parse time (via the
* dpar_param_bindings table see src/spicelib/parser/inpdpar.c). */
int nupa_set_real(const char *name, double value);

View File

@ -29,6 +29,7 @@ Todo:
#include "ngspice/fteext.h" #include "ngspice/fteext.h"
#include "ngspice/stringskip.h" #include "ngspice/stringskip.h"
#include "ngspice/compatmode.h" #include "ngspice/compatmode.h"
#include "ngspice/osdi_defer.h"
#ifdef SHARED_MODULE #ifdef SHARED_MODULE
extern ATTRIBUTE_NORETURN void shared_exit(int status); extern ATTRIBUTE_NORETURN void shared_exit(int status);
@ -297,6 +298,66 @@ static bool firstsignalS = 1;
static dico_t *dicoS = NULL; static dico_t *dicoS = NULL;
static dico_t *dicos_list[100]; static dico_t *dicos_list[100];
/* External accessor for the active dico_t. Used by the OSDI deferred-
* evaluation path (osdi_defer.c) to invoke nupa_eval_with_scope() at
* instance bind time without exposing the static `dicoS` itself. May
* return NULL if called before any deck has been processed. */
dico_t *nupa_get_dico(void) {
return dicoS;
}
/* Public-name lookup for a real-valued numparam. Returns 1 and sets
* *value on success; returns 0 if name is unknown or not a NUPA_REAL.
* Used by the device-line parser (spicelib/parser/inpdpar.c) so that
*
* .param vgswp=0
* vgs n2 n3 vgswp
*
* i.e. an HSPICE-style bare-identifier value field resolves the
* leading number from the global numparam dictionary instead of
* aborting with "unknown parameter (vgswp)". Walks the dico's full
* scope stack via entrynb(), so subckt-scope params resolve too.
*
* Reentrant: returns 0 if dicoS hasn't been allocated yet (no deck
* processed). Read-only on the dictionary. */
int nupa_get_real(const char *name, double *value) {
if (!dicoS || !name || !value) return 0;
entry_t *entry = entrynb(dicoS, (char *)name);
if (!entry || entry->tp != NUPA_REAL) return 0;
*value = entry->vl;
return 1;
}
/* Companion writer to nupa_get_real. Updates the value of an
* existing NUPA_REAL entry by name. Used by the .dc analysis loop
* (dctrcurv.c) to sweep a .param across its start/stop range, then
* call the parser/binder hooks to push the new value through to V/I
* sources that referenced the param at parse time.
*
* Returns 1 on success, 0 if the name is unknown or not NUPA_REAL.
* Does NOT create new entries sweeping an undefined name would
* mask the simulator's "unknown name" diagnostic. */
int nupa_set_real(const char *name, double value) {
if (!dicoS || !name) return 0;
entry_t *entry = entrynb(dicoS, (char *)name);
if (!entry || entry->tp != NUPA_REAL) return 0;
entry->vl = value;
return 1;
}
/* See numparam.h. Called from inp_subcktexpand's X→VA fallback to
* tell numparam this line is now a comment. */
void nupa_skip_line(int linenum) {
if (!dicoS) return;
if (linenum < 0 || linenum > dicoS->linecount) return;
/* Force the line's stored copy to start with `*` so any later
* scan treats it as comment. */
if (dicoS->dynrefptr[linenum] && dicoS->dynrefptr[linenum][0])
dicoS->dynrefptr[linenum][0] = '*';
/* Reset the category so dispatch in nupa_eval does nothing. */
dicoS->dyncategory[linenum] = '?';
}
static void static void
nupa_init(void) nupa_init(void)
@ -673,6 +734,7 @@ nupa_eval(struct card *card)
dicoS->srcline = linenum; dicoS->srcline = linenum;
dicoS->oldline = orig_linenum; dicoS->oldline = orig_linenum;
dicoS->cardline = s; dicoS->cardline = s;
dicoS->cardsource = card->linesource;
c = dicoS->dyncategory[linenum]; c = dicoS->dyncategory[linenum];
@ -687,7 +749,39 @@ nupa_eval(struct card *card)
} else if (c == 'B') { /* substitute braces line */ } else if (c == 'B') { /* substitute braces line */
/* nupa_substitute() may reallocate line buffer. */ /* nupa_substitute() may reallocate line buffer. */
/* HSPICE-style OSDI model cards (Samsung 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
* .model-parse time because no instance exists yet. Detect
* them, save into the OSDI deferred-eval side table, and
* rewrite each such body to `{0}` in the ORIGINAL line so
* numparam's subsequent brace pass evaluates 0 (instead of
* choking on undefined `l`) and substitutes that into the
* corresponding MARKER placeholder. At instance creation,
* osdisetup.c walks the side table and installs the proper
* per-instance values via OSDIparam.
*
* NOTE: we must rewrite `dynrefptr[linenum]` (the original
* line with `{...}` blocks still visible) rather than
* `card->line` (which already has MARKER placeholders the
* `{` chars are gone at this point). Numparam walks both
* lines in parallel during nupa_substitute. */
osdi_defer_preprocess_line(&dicoS->dynrefptr[linenum]);
/* HSPICE uses single-quoted string literals for some .option values
* (e.g. tmipath='modeldir') that look like parameter references to
* numparam but are not defined as parameters. Suppress errors for
* .option lines so these unsupported HSPICE options are silently
* skipped rather than printing noise or aborting the run. */
bool is_option_line = prefix(".option", dicoS->dynrefptr[linenum]);
if (is_option_line)
dicoS->suppress_errors = true;
err = nupa_substitute(dicoS, dicoS->dynrefptr[linenum], &card->line); err = nupa_substitute(dicoS, dicoS->dynrefptr[linenum], &card->line);
if (is_option_line) {
dicoS->suppress_errors = false;
err = 0;
}
s = card->line; s = card->line;
} else if (c == 'X') { } else if (c == 'X') {
/* compute args of subcircuit, if required */ /* compute args of subcircuit, if required */

View File

@ -0,0 +1,507 @@
/*
* HSPICE table_param() see table_param.h for syntax.
*
* Implementation strategy:
*
* 1. Parse each .table file once on first reference. Stored as a
* flat array of rows (key + value doubles per row). The header
* column count tells us how many doubles per row. We don't
* pre-sort or index rows typical PDK tables are <1000 rows so
* a linear filter is fine, AND the keys-then-interpolation
* structure makes row order matter (rows are pre-sorted by the
* foundry tool with the real-key column ascending, so we can
* find bracketing rows by simple scan).
*
* 2. Lookup: filter rows where ALL integer keys exactly match (the
* n_int leftmost columns). Among matching rows, linear-interpolate
* over the real-key columns (n_real columns immediately after the
* integer keys) using the requested point.
*
* 3. Cache: NGHASHPTR from resolved-filename -> Table*. Cache lives
* for process lifetime. Foundry PDKs typically reference ~20
* distinct .table files, each ~hundreds of KB; total memory cost
* a few MB tops.
*
* Interpolation: for a single real key, this is straight linear
* interpolation between the two bracketing rows. For multiple real
* keys, we use the obvious tensor-product extension find the
* bracketing range in EACH real-key dimension, then iterate over the
* 2^n_real corners and weight them by the multilinear factors. Out-
* of-range real values clamp to the table's edge value (no
* extrapolation, matches HSPICE's default).
*/
#include "ngspice/ngspice.h"
#include "ngspice/cpdefs.h"
#include "ngspice/ftedefs.h"
#include "ngspice/hash.h"
#include "ngspice/fteinp.h"
#include "ngspice/stringutil.h"
#include "../inpcom.h"
#include "table_param.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <math.h>
typedef struct Table {
int n_cols; /* total columns including keys */
int n_rows;
double *data; /* row-major: data[row * n_cols + col] */
} Table;
static NGHASHPTR g_table_cache = NULL;
/* ---------------------------------------------------------------- */
/* File parsing. */
/* ---------------------------------------------------------------- */
/* Count whitespace-separated tokens on a line. Stops at end of
* string or '\n'. Used once to size the column count from the
* header. */
static int count_tokens(const char *s) {
int n = 0;
while (*s) {
while (*s && isspace((unsigned char)*s)) s++;
if (!*s || *s == '\n') break;
n++;
while (*s && !isspace((unsigned char)*s)) s++;
}
return n;
}
/* Parse one whitespace-separated double from `*p`, advance `*p` past
* it. Returns true on success, false on EOL/EOF or parse error. */
static bool parse_double_token(const char **p, double *out) {
const char *s = *p;
while (*s && isspace((unsigned char)*s) && *s != '\n') s++;
if (!*s || *s == '\n' || *s == '\0') return false;
char *end;
double v = strtod(s, &end);
if (end == s) return false;
*out = v;
*p = end;
return true;
}
/* Free a parsed table. Callback signature matches nghash_free's
* del_data slot. */
static void free_table(void *p) {
Table *t = (Table *)p;
if (!t) return;
free(t->data);
free(t);
}
/* Read the file at `path`, parse the header to learn column count,
* then parse data rows. Skip lines starting with '*' or '#' (the
* latter only after the first one the first '#' line IS the
* header). Returns the allocated Table on success, NULL on
* failure (with error reported via fprintf). */
static Table *load_table_file(const char *path) {
FILE *fp = fopen(path, "r");
if (!fp) {
fprintf(stderr,
"Error: table_param: cannot open table file '%s'\n",
path);
return NULL;
}
/* First non-blank line MUST be the header (starting with '#').
* Count its tokens (excluding the leading '#') to learn n_cols. */
char buf[8192];
int n_cols = -1;
while (fgets(buf, sizeof buf, fp)) {
char *p = buf;
while (*p && isspace((unsigned char)*p)) p++;
if (!*p || *p == '\n') continue;
if (*p == '#') {
p++; /* skip the # */
n_cols = count_tokens(p);
break;
}
/* No header — treat first line as data, count its tokens.
* Then we'll need to re-read it. Simpler: insist on a header. */
fprintf(stderr,
"Error: table_param: file '%s' missing '#'-header "
"line\n",
path);
fclose(fp);
return NULL;
}
if (n_cols <= 0) {
fprintf(stderr,
"Error: table_param: file '%s' header has 0 columns\n",
path);
fclose(fp);
return NULL;
}
/* Read all subsequent data rows. Grow buffer as needed. */
int cap = 256;
int n_rows = 0;
double *data = (double *)malloc(sizeof(double) * cap * n_cols);
if (!data) {
fprintf(stderr, "Error: table_param: out of memory\n");
fclose(fp);
return NULL;
}
while (fgets(buf, sizeof buf, fp)) {
const char *p = buf;
while (*p && isspace((unsigned char)*p)) p++;
if (!*p || *p == '\n' || *p == '*' || *p == '#') continue;
if (n_rows >= cap) {
cap *= 2;
double *nd = (double *)realloc(
data, sizeof(double) * cap * n_cols);
if (!nd) {
free(data);
fprintf(stderr, "Error: table_param: out of memory\n");
fclose(fp);
return NULL;
}
data = nd;
}
double *row = &data[n_rows * n_cols];
int c;
for (c = 0; c < n_cols; c++) {
if (!parse_double_token(&p, &row[c])) {
fprintf(stderr,
"Error: table_param: file '%s' row %d "
"has fewer than %d columns\n",
path, n_rows + 1, n_cols);
free(data);
fclose(fp);
return NULL;
}
}
n_rows++;
}
fclose(fp);
Table *t = (Table *)malloc(sizeof(Table));
if (!t) {
free(data);
fprintf(stderr, "Error: table_param: out of memory\n");
return NULL;
}
t->n_cols = n_cols;
t->n_rows = n_rows;
t->data = data;
return t;
}
/* ---------------------------------------------------------------- */
/* Cache. */
/* ---------------------------------------------------------------- */
/* Resolve a relative path via ngspice's sourcepath search. Returns
* 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
* 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.
* ngspice doesn't track per-card include-directory currently, so:
* 1. Try the path as-is (works for absolute paths or if user cd'd
* into the PDK directory).
* 2. Walk ngspice's `sourcepath` list (inp_pathresolve handles this)
* works if the user added the PDK's NGSPICE dir to sourcepath.
* 3. Try the directory of the most recently included file
* (`inputdir` global).
* If none of those work, the caller will get a clear file-not-found
* error and can add the PDK directory to `set sourcepath`. */
extern char *inputdir; /* set by inp_spsource during deck loading */
/* Try to find `filename` relative to `dir`. Returns malloc'd
* absolute path on success, NULL on failure. `dir` is the
* directory containing the .lib file that emitted the call. */
static char *try_in_dir(const char *filename, const char *dir) {
if (!dir || !dir[0])
return NULL;
size_t need = strlen(dir) + 1 + strlen(filename) + 1;
char *combined = (char *)malloc(need);
if (!combined) return NULL;
snprintf(combined, need, "%s/%s", dir, filename);
char *resolved = inp_pathresolve(combined);
free(combined);
return resolved;
}
/* Strip the trailing filename component from `path` so the result
* is the directory. Caller frees. */
static char *dirname_of(const char *path) {
if (!path) return NULL;
const char *slash = strrchr(path, '/');
if (!slash) return copy("."); /* no separator → cwd */
size_t n = (size_t)(slash - path);
char *r = (char *)malloc(n + 1);
if (!r) return NULL;
memcpy(r, path, n);
r[n] = '\0';
return r;
}
/* Resolve a relative .table path. Search order:
* 1. Absolute path: use as-is.
* 2. As-given (cwd-relative or sourcepath-listed).
* 3. Relative to `dir_hint` (caller passes the dirname of the
* .lib file containing the table_param call matches HSPICE).
* 4. Relative to ngspice's `inputdir` (the netlist file's dir).
* 5. Bare filename fopen will fail with a clear error. */
static char *resolve_table_path(const char *filename,
const char *dir_hint) {
/* 1. Absolute. */
if (filename[0] == '/')
return copy(filename);
/* 2. ngspice sourcepath / cwd. */
char *resolved = inp_pathresolve(filename);
if (resolved)
return resolved;
/* 3. Relative to the file the call came from (the typical case
* for foundry PDKs: `./RF_COMPONENTS/...` inside fets_rf.lib
* is relative to fets_rf.lib's directory). */
resolved = try_in_dir(filename, dir_hint);
if (resolved)
return resolved;
/* 4. Relative to inputdir (set by inp_spsource for the netlist
* itself). */
resolved = try_in_dir(filename, inputdir);
if (resolved)
return resolved;
/* 5. Last resort. */
return copy(filename);
}
static Table *get_or_load_table(const char *filename,
const char *dir_hint) {
if (!g_table_cache)
g_table_cache = nghash_init(8);
Table *t = (Table *)nghash_find(g_table_cache, (void *)filename);
if (t)
return t;
char *path = resolve_table_path(filename, dir_hint);
t = load_table_file(path);
tfree(path);
if (!t)
return NULL;
/* Key the cache by an owned copy of the original filename string
* (the caller's pointer may not persist). */
char *key = copy(filename);
nghash_insert(g_table_cache, key, t);
return t;
}
void table_param_clear_cache(void) {
if (!g_table_cache) return;
nghash_free(g_table_cache, free_table, free);
g_table_cache = NULL;
}
/* ---------------------------------------------------------------- */
/* Lookup. */
/* ---------------------------------------------------------------- */
/* Returns 1 if integer keys at this row match query (within
* floating-point tolerance), 0 otherwise. */
static int row_int_keys_match(const Table *t, int row,
const double *int_vals, int n_int) {
const double *r = &t->data[row * t->n_cols];
for (int i = 0; i < n_int; i++) {
if (fabs(r[i] - int_vals[i]) > 0.5)
return 0;
}
return 1;
}
/* For the subset of rows where integer keys match, linearly
* interpolate the output_col value over the n_real real keys.
*
* Implementation: this is the tensor-product multilinear
* interpolation. For each real-key dimension d in 0..n_real-1,
* find the two distinct values v_lo, v_hi present in the matching
* rows that bracket real_vals[d]. Compute a weight w_d in [0,1]
* such that real_vals[d] = (1-w_d)*v_lo + w_d*v_hi. Then the
* interpolated output is the weighted sum over the 2^n_real corners
* of the bracketing hyper-rectangle.
*
* Out-of-range real values clamp to the nearest available endpoint
* (no extrapolation).
*/
static int interpolate_output(const Table *t,
const double *int_vals, int n_int,
const double *real_vals, int n_real,
int output_col,
double *out)
{
/* output_col is 1-based, indexed into value columns (after the
* keys). Convert to absolute column index. */
int data_col = n_int + n_real + (output_col - 1);
if (data_col < n_int + n_real || data_col >= t->n_cols) {
fprintf(stderr,
"Error: table_param: output column %d out of range "
"(table has %d value columns)\n",
output_col, t->n_cols - n_int - n_real);
return 1;
}
/* For each real-key dimension, collect the distinct values
* present in matching rows, sorted. */
int found_any = 0;
double v_lo[16], v_hi[16]; /* up to 16 real keys; foundry uses ≤3 */
double w[16];
if (n_real > 16) {
fprintf(stderr,
"Error: table_param: too many real keys (%d, max 16)\n",
n_real);
return 1;
}
for (int d = 0; d < n_real; d++) {
double target = real_vals[d];
/* Find bracketing values: max v ≤ target and min v ≥ target,
* among rows whose integer keys match AND whose previous real
* keys match the previously-bracketed indices. For simplicity
* we do a per-dimension scan without the inter-dimensional
* constraint the foundry tables are products of grids in
* each key dim, so this gives the correct result. */
double best_lo = -INFINITY, best_hi = INFINITY;
int have_lo = 0, have_hi = 0;
for (int r = 0; r < t->n_rows; r++) {
if (!row_int_keys_match(t, r, int_vals, n_int))
continue;
double v = t->data[r * t->n_cols + n_int + d];
if (v <= target && v > best_lo) {
best_lo = v;
have_lo = 1;
}
if (v >= target && v < best_hi) {
best_hi = v;
have_hi = 1;
}
found_any = 1;
}
if (!have_lo && !have_hi) {
fprintf(stderr,
"Error: table_param: no matching rows for "
"integer keys\n");
return 1;
}
/* Clamp at edges */
if (!have_lo) best_lo = best_hi;
if (!have_hi) best_hi = best_lo;
v_lo[d] = best_lo;
v_hi[d] = best_hi;
if (best_hi == best_lo)
w[d] = 0.0;
else
w[d] = (target - best_lo) / (best_hi - best_lo);
if (w[d] < 0.0) w[d] = 0.0;
if (w[d] > 1.0) w[d] = 1.0;
}
if (!found_any) {
fprintf(stderr,
"Error: table_param: no matching rows found\n");
return 1;
}
/* Iterate over the 2^n_real corners. For each corner, find the
* row whose real-key columns match v_lo[d] or v_hi[d] per the
* corner's bits, and accumulate weighted output. */
double sum = 0.0;
double corner_weight_sum = 0.0;
int n_corners = 1 << n_real;
for (int c = 0; c < n_corners; c++) {
/* Build the real-key target for this corner. */
double rk[16];
double weight = 1.0;
for (int d = 0; d < n_real; d++) {
if (c & (1 << d)) {
rk[d] = v_hi[d];
weight *= w[d];
} else {
rk[d] = v_lo[d];
weight *= (1.0 - w[d]);
}
}
if (weight == 0.0)
continue;
/* Find the matching row. */
int found = 0;
for (int r = 0; r < t->n_rows; r++) {
if (!row_int_keys_match(t, r, int_vals, n_int))
continue;
int rk_match = 1;
for (int d = 0; d < n_real; d++) {
double v = t->data[r * t->n_cols + n_int + d];
if (fabs(v - rk[d]) > 1e-12 * (fabs(rk[d]) + 1e-30)) {
rk_match = 0;
break;
}
}
if (!rk_match) continue;
sum += weight * t->data[r * t->n_cols + data_col];
corner_weight_sum += weight;
found = 1;
break;
}
if (!found) {
/* Sparse table: a bracketing corner row doesn't exist.
* The output will still be a useful approximation as
* long as at least one corner was found. */
}
}
if (corner_weight_sum == 0.0) {
fprintf(stderr,
"Error: table_param: interpolation failed "
"(no bracketing rows found)\n");
return 1;
}
/* Normalize in case some corners were missing — preserves the
* weighted-average interpretation. */
*out = sum / corner_weight_sum;
return 0;
}
int table_param_lookup(const char *filename,
const char *dir_hint,
const double *int_vals, int n_int,
const double *real_vals, int n_real,
int output_col,
double *out)
{
Table *t = get_or_load_table(filename, dir_hint);
if (!t)
return 1;
if (t->n_cols < n_int + n_real + 1) {
fprintf(stderr,
"Error: table_param: file '%s' has %d columns but "
"call uses %d keys + at least 1 value column\n",
filename, t->n_cols, n_int + n_real);
return 1;
}
return interpolate_output(t, int_vals, n_int,
real_vals, n_real,
output_col, out);
}

View File

@ -0,0 +1,71 @@
/*
* HSPICE table_param() implementation.
*
* Foundry PDKs (Samsung 14LPU, TSMC, 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
* references it ~1300 times.
*
* Syntax:
* table_param(file, N_int_keys, int_val1, ..., int_valN,
* N_real_keys, real_val1, ..., real_valM,
* output_col)
*
* file path to .table file (relative paths resolve via
* ngspice sourcepath)
* N_int_keys number of integer-typed search keys (exact match)
* int_valX integer key values to match
* N_real_keys number of real-typed search keys (linear-interpolated)
* real_valX real key values to interpolate between
* output_col 1-based index into the *value* columns of the table
* (i.e., columns after the keys)
*
* Table file format:
* #key1 key2 ... keyN val1 val2 ... valM (header comment)
* k1 k2 ... kN v1 v2 ... vM (data rows)
*
* The header line names the columns; the first N columns are keys
* (matching the call's int + real keys in declared order); the
* remaining M columns are output values selected by output_col.
*
* Files are loaded on first reference and cached for the lifetime
* of the process (foundry PDKs reference the same table file
* hundreds of times).
*/
#ifndef TABLE_PARAM_H
#define TABLE_PARAM_H
#include <stdbool.h>
/* Evaluate a table_param() call.
*
* filename table file path (caller's string, not retained)
* int_vals array of integer key values; length n_int
* n_int number of integer keys
* real_vals array of real key values; length n_real
* n_real number of real keys
* output_col 1-based index into value columns
* out written with the looked-up (interpolated) value
* on success; unchanged on failure
*
* Returns 0 on success, non-zero on error (file not found, key out
* of range, output column out of range, etc.). Errors are reported
* via fprintf(stderr, ...).
*/
/* `dir_hint` is the directory of the file that originated this
* call (typically the .lib file containing the table_param()
* invocation). Relative `filename` paths resolve against this dir
* first, matching HSPICE's behavior. May be NULL falls back to
* cwd / sourcepath / ngspice's `inputdir`. */
int table_param_lookup(const char *filename,
const char *dir_hint,
const double *int_vals, int n_int,
const double *real_vals, int n_real,
int output_col,
double *out);
/* Free all cached tables. Called at simulator shutdown. */
void table_param_clear_cache(void);
#endif /* TABLE_PARAM_H */

View File

@ -8,6 +8,7 @@
#include "general.h" #include "general.h"
#include "numparam.h" #include "numparam.h"
#include "table_param.h"
#include "ngspice/cpdefs.h" #include "ngspice/cpdefs.h"
#include "ngspice/ftedefs.h" #include "ngspice/ftedefs.h"
#include "ngspice/dvec.h" #include "ngspice/dvec.h"
@ -92,14 +93,15 @@ static const char *fmathS = /* all math functions */
"sqr sqrt sin cos exp ln arctan abs pow pwr max min int log log10 sinh cosh" "sqr sqrt sin cos exp ln arctan abs pow pwr max min int log log10 sinh cosh"
" tanh ternary_fcn agauss sgn gauss unif aunif limit ceil floor" " tanh ternary_fcn agauss sgn gauss unif aunif limit ceil floor"
" asin acos atan asinh acosh atanh tan nint" " asin acos atan asinh acosh atanh tan nint"
" vec var"; " vec var table_param";
enum { enum {
XFU_SQR = 1, XFU_SQRT, XFU_SIN, XFU_COS, XFU_EXP, XFU_LN, XFU_ARCTAN, XFU_ABS, XFU_POW, XFU_PWR, XFU_MAX, XFU_MIN, XFU_INT, XFU_LOG, XFU_LOG10, XFU_SINH, XFU_COSH, XFU_SQR = 1, XFU_SQRT, XFU_SIN, XFU_COS, XFU_EXP, XFU_LN, XFU_ARCTAN, XFU_ABS, XFU_POW, XFU_PWR, XFU_MAX, XFU_MIN, XFU_INT, XFU_LOG, XFU_LOG10, XFU_SINH, XFU_COSH,
XFU_TANH, XFU_TERNARY_FCN, XFU_AGAUSS, XFU_SGN, XFU_GAUSS, XFU_UNIF, XFU_AUNIF, XFU_LIMIT, XFU_CEIL, XFU_FLOOR, XFU_TANH, XFU_TERNARY_FCN, XFU_AGAUSS, XFU_SGN, XFU_GAUSS, XFU_UNIF, XFU_AUNIF, XFU_LIMIT, XFU_CEIL, XFU_FLOOR,
XFU_ASIN, XFU_ACOS, XFU_ATAN, XFU_ASINH, XFU_ACOSH, XFU_ATANH, XFU_TAN, XFU_NINT, XFU_ASIN, XFU_ACOS, XFU_ATAN, XFU_ASINH, XFU_ACOSH, XFU_ATANH, XFU_TAN, XFU_NINT,
XFU_VEC, XFU_VAR // String arguments. XFU_VEC, XFU_VAR, // String arguments.
XFU_TABLE_PARAM // Variadic HSPICE table_param(file, n_int, ints..., n_real, reals..., out_col).
}; };
@ -223,6 +225,9 @@ static bool message(dico_t *dico, const char *fmt, ...)
static bool static bool
message(dico_t *dico, const char *fmt, ...) message(dico_t *dico, const char *fmt, ...)
{ {
if (dico->suppress_errors)
return 1;
va_list ap; va_list ap;
if (dico->srcline >= 0) { if (dico->srcline >= 0) {
@ -275,6 +280,7 @@ initdico(dico_t *dico)
else else
dico->hs_compatibility = 0; dico->hs_compatibility = 0;
dico->cardline = NULL; dico->cardline = NULL;
dico->suppress_errors = false;
} }
@ -817,16 +823,34 @@ operate(char op, double x, double y)
x = ((x != 0.0) || (y != 0.0)) ? 1.0 : 0.0; x = ((x != 0.0) || (y != 0.0)) ? 1.0 : 0.0;
break; break;
case '=': case '=':
if (x == y) /* HSPICE-compatible tolerant equality. Strict IEEE `==`
x = u; * breaks foundry-PDK expressions like `(l==0.014e-6)` where
else * `l` arrives as `14*1e-9 = 1.4000000000000001e-08` (1 ULP
x = z; * off from `0.014e-6 = 1.4e-08`), or as the bin midpoint
* `0.5*(1e-8+1.8e-8) = 1.3999999999999999e-08`. Use a
* hybrid relative+absolute tolerance: differences smaller
* than `1e-9 * max(|x|,|y|)` or below an absolute floor of
* `1e-18` count as equal. Integer comparisons (mexp==2,
* etc.) remain unambiguous 1 vs 2 differ by 100% relative. */
{
double dxy = fabs(x - y);
double mxy = fabs(x) > fabs(y) ? fabs(x) : fabs(y);
if (dxy <= mxy * 1e-9 || dxy < 1e-18)
x = u;
else
x = z;
}
break; break;
case '#': /* <> */ case '#': /* <> */
if (x != y) /* Symmetric with `==` above. */
x = u; {
else double dxy = fabs(x - y);
x = z; double mxy = fabs(x) > fabs(y) ? fabs(x) : fabs(y);
if (dxy <= mxy * 1e-9 || dxy < 1e-18)
x = z;
else
x = u;
}
break; break;
case '>': case '>':
if (x > y) if (x > y)
@ -953,6 +977,159 @@ formula(dico_t *dico, const char *s, const char *s_end, bool *perror)
if (kptr >= s_end) { if (kptr >= s_end) {
error = message(dico, "Closing \")\" not found.\n"); error = message(dico, "Closing \")\" not found.\n");
natom++; /* shut up other error message */ natom++; /* shut up other error message */
} else if (fu == XFU_TABLE_PARAM) {
/* HSPICE table_param() — variadic. Syntax:
* table_param(file, n_int, int_vals..., n_real,
* real_vals..., output_col)
*
* arg0 is a string: either bare "filename" or wrapped
* as str("filename"). All subsequent args are
* numeric expressions, parsed recursively via
* formula(). Walk the comma-separated arg list
* manually since the standard parser handles only
* up to 3 args. */
/* Extract args by scanning for top-level commas
* inside (s .. kptr). */
#define TP_MAX_ARGS 64
const char *arg_starts[TP_MAX_ARGS];
const char *arg_ends[TP_MAX_ARGS];
int n_args = 0;
const char *p = s;
const char *arg_begin = p;
int lvl_tp = 0;
while (p < kptr) {
if (*p == '(') lvl_tp++;
else if (*p == ')') lvl_tp--;
else if (*p == ',' && lvl_tp == 0) {
if (n_args < TP_MAX_ARGS) {
arg_starts[n_args] = arg_begin;
arg_ends[n_args] = p;
n_args++;
}
arg_begin = p + 1;
}
p++;
}
if (n_args < TP_MAX_ARGS) {
arg_starts[n_args] = arg_begin;
arg_ends[n_args] = kptr;
n_args++;
}
if (n_args < 3) {
error = message(dico,
"table_param: need at least 3 args "
"(file, n_int_or_n_real, output_col).\n");
natom++;
} else {
/* Parse arg 0 as filename. Strip optional
* str(...) wrapper and surrounding quotes. */
char filename[1024];
const char *fp = arg_starts[0];
const char *fe = arg_ends[0];
while (fp < fe && isspace((unsigned char)*fp)) fp++;
while (fe > fp && isspace((unsigned char)fe[-1])) fe--;
/* unwrap str( ... ) */
if (fe - fp > 5 &&
(fp[0] == 's' || fp[0] == 'S') &&
(fp[1] == 't' || fp[1] == 'T') &&
(fp[2] == 'r' || fp[2] == 'R') &&
fp[3] == '(' && fe[-1] == ')') {
fp += 4;
fe -= 1;
while (fp < fe && isspace((unsigned char)*fp)) fp++;
while (fe > fp && isspace((unsigned char)fe[-1])) fe--;
}
/* strip "..." or '...' quotes */
if (fe - fp >= 2 &&
((fp[0] == '"' && fe[-1] == '"') ||
(fp[0] == '\'' && fe[-1] == '\''))) {
fp++;
fe--;
}
size_t flen = (size_t)(fe - fp);
if (flen >= sizeof(filename)) flen = sizeof(filename) - 1;
memcpy(filename, fp, flen);
filename[flen] = '\0';
/* Evaluate the rest of the args as numbers. */
double argv[TP_MAX_ARGS];
int i;
bool sub_err = 0;
for (i = 1; i < n_args && !sub_err; i++) {
argv[i] = formula(dico, arg_starts[i],
arg_ends[i], &sub_err);
}
if (sub_err) {
error = 1;
natom++;
} else {
/* Decode the (n_int, int_vals..., n_real,
* real_vals..., output_col) shape. */
int n_int = (int)argv[1];
if (n_int < 0 || 2 + n_int >= n_args) {
error = message(dico,
"table_param: bad n_int.\n");
natom++;
} else {
int n_real_idx = 2 + n_int;
int n_real = (int)argv[n_real_idx];
int out_col_idx = n_real_idx + 1 + n_real;
if (n_real < 0 ||
out_col_idx >= n_args) {
error = message(dico,
"table_param: bad n_real.\n");
natom++;
} else {
double int_vals[16], real_vals[16];
int j;
for (j = 0; j < n_int && j < 16; j++)
int_vals[j] = argv[2 + j];
for (j = 0; j < n_real && j < 16; j++)
real_vals[j] =
argv[n_real_idx + 1 + j];
int out_col = (int)argv[out_col_idx];
double looked_up = 0.0;
/* Derive the dir of the .lib file
* that emitted this call from the
* current card's linesource. */
char dir_buf[1024];
const char *dir_hint = NULL;
if (dico->cardsource) {
const char *src = dico->cardsource;
const char *slash = strrchr(src, '/');
if (slash) {
size_t n = (size_t)(slash - src);
if (n >= sizeof dir_buf)
n = sizeof dir_buf - 1;
memcpy(dir_buf, src, n);
dir_buf[n] = '\0';
dir_hint = dir_buf;
}
}
int rc = table_param_lookup(
filename,
dir_hint,
int_vals, n_int,
real_vals, n_real,
out_col,
&looked_up);
if (rc) {
error = 1;
natom++;
} else {
u = looked_up;
}
}
}
}
}
#undef TP_MAX_ARGS
state = S_atom;
s = kptr + 1;
fu = 0;
} else if (fu >= XFU_VEC) { } else if (fu >= XFU_VEC) {
struct dvec *d; struct dvec *d;
char *vec_name; char *vec_name;
@ -1029,7 +1206,27 @@ formula(dico_t *dico, const char *s, const char *s_end, bool *perror)
const char *s_next = fetchid(s, s_end); const char *s_next = fetchid(s, s_end);
fu = keyword(fmathS, s, s_next); /* numeric function? */ fu = keyword(fmathS, s, s_next); /* numeric function? */
if (fu > 0) { if (fu > 0) {
state = S_init; /* S_init means: ignore for the moment */ /* The identifier matched a function keyword (e.g. `var`,
* `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
* diode_rr.inc) use `var` as a subckt parameter, and
* shadowing it with the built-in function broke
* `vrb='var'` and similar chains. */
const char *peek = s_next;
while (peek < s_end && (*peek == ' ' || *peek == '\t'))
peek++;
if (peek >= s_end || *peek != '(') {
/* Not a function call — treat as identifier. */
ds_clear(&tstr);
pscopy(&tstr, s, s_next);
u = fetchnumentry(dico, ds_get_buf(&tstr), &error);
state = S_atom;
fu = 0;
} else {
state = S_init; /* wait for the `(` to be consumed */
}
} else { } else {
ds_clear(&tstr); ds_clear(&tstr);
pscopy(&tstr, s, s_next); pscopy(&tstr, s, s_next);
@ -1060,6 +1257,17 @@ formula(dico_t *dico, const char *s, const char *s_end, bool *perror)
negate = 1; negate = 1;
continue; continue;
} }
/* 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
* 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. */
if (oldstate == S_binop && state == S_binop && c == '+') {
ok = 1;
continue;
}
if (!ok) if (!ok)
error = message(dico, " Misplaced operator\n"); error = message(dico, " Misplaced operator\n");
@ -1250,6 +1458,65 @@ evaluate_expr(dico_t *dico, DSTRINGPTR qstr_p, const char *t, const char * const
} }
/* Evaluate `expr` (a brace-body string, with no enclosing { } or ' ')
* with the (name, value) pairs in `names`/`values` pushed onto a
* transient new scope on top of `dico`'s symbol stack. Pops the scope
* before returning, so global state is unchanged. Result is written
* to *out. Returns 0 on success, non-zero on parse/eval error.
*
* Used by the OSDI deferred-evaluation path (osdi_defer.c): at
* instance bind time, push the instance's l/w/nf/m/xnf, evaluate the
* model-card expression that was stashed at parse time, install the
* scalar result via OSDIparam. No persistent side effects on the
* numparam dictionary.
*/
int
nupa_eval_with_scope(dico_t *dico, const char *expr,
const char *const *names, const double *values,
int n, double *out)
{
if (!dico || !expr || !out) return 1;
/* Push a new scope. */
dico->stack_depth++;
if (dico->stack_depth >= dico->max_stack_depth) {
int asize = (dico->max_stack_depth *= 2);
dico->symbols = TREALLOC(NGHASHPTR, dico->symbols, asize);
dico->inst_name = TREALLOC(char*, dico->inst_name, asize);
}
dico->symbols[dico->stack_depth] = nghash_init(NGHASH_MIN_SIZE);
dico->inst_name[dico->stack_depth] = NULL;
/* Populate it. */
NGHASHPTR ht = dico->symbols[dico->stack_depth];
for (int i = 0; i < n; i++) {
entry_t *e = attrib(dico, ht, (char *)names[i], 'N');
if (e) {
e->tp = NUPA_REAL;
e->vl = values[i];
e->ivl = 0;
e->sbbase = NULL;
}
}
/* Evaluate. */
bool err = 0;
double v = formula(dico, expr, expr + strlen(expr), &err);
/* Pop scope: free the hash + entries we just added. We do NOT
* use dicostack_pop here because that promotes locals to qualified
* globals; we want the scope to vanish completely. */
nghash_free(ht, del_attrib, NULL);
dico->symbols[dico->stack_depth] = NULL;
dico->inst_name[dico->stack_depth] = NULL;
dico->stack_depth--;
if (err) return 1;
*out = v;
return 0;
}
/********* interface functions for spice3f5 extension ***********/ /********* interface functions for spice3f5 extension ***********/
static bool static bool

View File

@ -62,6 +62,8 @@ Modified: 2000 AlansFixes
#include "ngspice/stringskip.h" #include "ngspice/stringskip.h"
#include "ngspice/compatmode.h" #include "ngspice/compatmode.h"
#include "ngspice/hash.h" #include "ngspice/hash.h"
#include "ngspice/inpdefs.h" /* for INPtypelook */
#include "numparam/numparam.h" /* for nupa_skip_line */
#include <stdarg.h> #include <stdarg.h>
@ -101,6 +103,8 @@ static int numnodes(const char* line, struct subs* subs);
static int numdevs(char *s); static int numdevs(char *s);
static wordlist *modtranslate(struct card *deck, char *subname, wordlist *new_modnames); static wordlist *modtranslate(struct card *deck, char *subname, wordlist *new_modnames);
static void devmodtranslate(struct card *deck, char *subname, wordlist * const orig_modnames); static void devmodtranslate(struct card *deck, char *subname, wordlist * const orig_modnames);
static bool subckt_has_scale(const char *args);
static char *scale_geom_param(const char *line, const char *pname, int power);
/* hash table to store the global nodes /* hash table to store the global nodes
* For now its use is limited to avoid double entries in global_nodes[] */ * For now its use is limited to avoid double entries in global_nodes[] */
@ -184,6 +188,75 @@ free_global_nodes(void)
} }
/*-------------------------------------------------------------------
HSPICE element scale. For each subcircuit that declares a `scale`
parameter, wrap the geometry value-expressions in its body so they
are multiplied by the in-scope `scale` -- lengths by scale, areas by
scale^2 (ad/as). A unit cell instantiated at scale=0.855 then behaves
as the 0.855x physical cell, exactly how HSPICE applies a subcircuit's
element scale factor, and the generic, per-instance replacement for the
old hard-coded `geoshrink` option. Foundry models pre-divide their
parasitics (ad=.../scale^2, sa=saref/scale) expecting this multiply
back, so the full geometry set is scaled, not just W/L.
This MUST run before numparam preparation (inp_fix_for_numparam in
inpcom): numparam only substitutes `'expr'`/`{expr}` it has marked, so
an expression added afterwards is left raw and reaches the device parser
as text. Hence it is called from inp_readall(), not from the later
inp_subcktexpand(). Detection reads the .subckt line's parameter list,
which numparam has not yet stripped. m/nf (multipliers) and
nrd/nrs/sca-scc (dimensionless) are left untouched; nested X calls are
skipped (a nested subcircuit's own definition is wrapped when its own
.subckt is processed).
-------------------------------------------------------------------*/
void
inp_apply_subckt_scale(struct card *deck)
{
static const struct { const char *name; int pwr; } geo[] = {
{ "w", 1 }, { "l", 1 }, { "ad", 2 }, { "as", 2 },
{ "pd", 1 }, { "ps", 1 },
{ "sa", 1 }, { "sb", 1 }, { "sc", 1 }, { "sd", 1 },
};
struct card *c;
for (c = deck; c; c = c->nextcard) {
char *s, *nm;
struct card *b;
int nest;
bool has;
if (!ciprefix(".subckt", c->line)) /* not a .subckt definition */
continue;
s = nexttok(c->line); /* skip the .subckt keyword */
nm = gettok(&s); /* name; s -> formal args */
has = (nm && subckt_has_scale(s));
tfree(nm);
if (!has)
continue;
for (b = c->nextcard, nest = 0; b; b = b->nextcard) {
char *ds;
size_t gi;
if (ciprefix(".subckt", b->line)) { nest++; continue; }
if (ciprefix(".ends", b->line)) {
if (nest == 0) break; /* matching .ends */
nest--; continue;
}
if (nest > 0)
continue; /* inside a nested subckt */
ds = skip_ws(b->line);
if (!isalpha_c((unsigned char) *ds))
continue; /* not an element line */
if (tolower_c(*ds) == 'x')
continue; /* nested subckt call */
for (gi = 0; gi < sizeof geo / sizeof geo[0]; gi++) {
char *nl = scale_geom_param(b->line, geo[gi].name,
geo[gi].pwr);
if (nl) { tfree(b->line); b->line = nl; }
}
}
}
}
/*------------------------------------------------------------------- /*-------------------------------------------------------------------
inp_subcktexpand is the top level function which translates inp_subcktexpand is the top level function which translates
.subckts into mainlined code. Note that there are several things .subckts into mainlined code. Note that there are several things
@ -232,6 +305,9 @@ inp_subcktexpand(struct card *deck) {
} }
#endif #endif
/* HSPICE element scale is applied earlier, in inp_apply_subckt_scale(),
called from inpcom before the numparam preparation pass. */
nupa_signal(NUPADECKCOPY); nupa_signal(NUPADECKCOPY);
/* get the subckt names from the deck */ /* get the subckt names from the deck */
for (c = deck; c; c = c->nextcard) { /* first Numparam pass */ for (c = deck; c; c = c->nextcard) { /* first Numparam pass */
@ -359,9 +435,86 @@ inp_subcktexpand(struct card *deck) {
dynMaxckt++; dynMaxckt++;
} }
/* Now check to see if there are still subckt instances undefined... */ /* Now check to see if there are still subckt instances undefined.
*
* HSPICE convention: an `X<inst>` instance line can call EITHER a
* `.subckt` OR a Verilog-A module loaded via OSDI. The target
* 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
* instances like `xesd_monitor d g s b esd_nfet_monitor ...`
* inside their FET subckts, expecting HSPICE-style dispatch.
*
* Compromise: if the target name is a registered OSDI device
* (INPtypelook 0), comment out the line. ESD monitor modules in
* foundry decks are diagnostic-only `$strobe` messages for spec
* violations, no current/charge contributions so dropping them
* doesn't change circuit behavior, only disables the diagnostics.
* A proper fix would auto-generate a `.model + N-prefix` shim, but
* needs ngspice's OSDI dispatch to accept generic-prefix instances
* for non-MOSFET VA modules; that's a separate change. */
for (c = deck; c; c = c->nextcard) for (c = deck; c; c = c->nextcard)
if (ciprefix(invoke, c->line)) { if (ciprefix(invoke, c->line)) {
/* Try to extract the target type token from `X<name>
* n1 n2 ... <target>`. For the typical foundry case with
* 4 nodes (d, g, s, b) it's the 5th whitespace-separated
* token. Scan forward token by token; whichever token
* matches a registered device type wins. Search up to a
* reasonable cap so we don't walk an entire line of
* key=value pairs. */
const int max_tokens_to_check = 12;
char *line_copy = copy(c->line);
char *cursor = line_copy;
int matched_type = -1;
char *matched_token = NULL;
for (int t = 0; t < max_tokens_to_check; t++) {
char *tok = gettok(&cursor);
if (!tok) break;
/* Skip key=value tokens (those come after the type). */
if (strchr(tok, '=')) { tfree(tok); break; }
int ty = INPtypelook(tok);
if (ty >= 0) {
matched_type = ty;
matched_token = tok; /* keep — printed in warning */
break;
}
tfree(tok);
}
if (matched_type >= 0) {
static int hspice_xva_warn_once = 0;
if (!hspice_xva_warn_once) {
fprintf(cp_err,
"Note: foundry-PDK X-instance(s) reference "
"Verilog-A modules instead of subckts "
"(HSPICE convention). Commenting these "
"out — they're typically diagnostic-only "
"(ESD monitors etc.) and don't affect "
"circuit operation. First example: %s\n",
matched_token);
fprintf(cp_err,
" in line no. %d from file %s\n",
c->linenum_orig, c->linesource);
hspice_xva_warn_once = 1;
}
/* Comment out the line in-place. */
if (c->line && c->line[0])
c->line[0] = '*';
/* numparam tracks the line independently of card->line
* via dyncategory[linenum] + dynrefptr[linenum]. Both
* were set during the initial nupa_copy() pass BEFORE
* we got here; if we just comment-mark card->line and
* leave numparam's state alone, the line still
* dispatches as 'X' (subckt call) and errors with
* "illegal subckt call". Reset numparam's view too. */
nupa_skip_line(c->linenum);
tfree(matched_token);
tfree(line_copy);
continue;
}
tfree(line_copy);
fprintf(cp_err, "Error: unknown subckt: %s\n", c->line); fprintf(cp_err, "Error: unknown subckt: %s\n", c->line);
fprintf(cp_err, " in line no. %d from file %s\n", c->linenum_orig, c->linesource); fprintf(cp_err, " in line no. %d from file %s\n", c->linenum_orig, c->linesource);
@ -419,6 +572,126 @@ find_ends(struct card *l)
} }
/* Extract the four binning range parameters from a .model card.
Returns TRUE only if all four are present and evaluate cleanly. */
static bool
get_model_bins(char *curr_line, float *fwmin, float *fwmax,
float *flmin, float *flmax)
{
const char *keys[4] = { " wmin=", " wmax=", " lmin=", " lmax=" };
float *dsts[4];
int i;
dsts[0] = fwmin;
dsts[1] = fwmax;
dsts[2] = flmin;
dsts[3] = flmax;
for (i = 0; i < 4; i++) {
int err;
char *p = strstr(curr_line, keys[i]);
if (!p)
return FALSE;
p += 6;
*dsts[i] = (float) INPevaluate(&p, &err, 0);
if (err)
return FALSE;
}
return TRUE;
}
/* True if the subcircuit formal-argument string declares a `scale`
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
by a word-boundary "scale" immediately followed (after optional
whitespace) by '=', so names like `l_scale=` / `noscale=` don't
match. */
static bool
subckt_has_scale(const char *args)
{
const char *p = args;
if (!args)
return FALSE;
while ((p = strstr(p, "scale")) != NULL) {
bool left_ok = (p == args) || isspace_c((unsigned char) p[-1]);
const char *q = p + 5; /* past "scale" */
while (isspace_c((unsigned char) *q))
q++;
if (left_ok && *q == '=')
return TRUE;
p += 5;
}
return FALSE;
}
/* Wrap the value-expression of geometry parameter `pname` on `line` so
that it is multiplied by the in-scope `scale` symbol raised to `power`
(1 for lengths/perimeters, 2 for areas): `pname=expr` becomes
`pname='(expr)*scale'` (or `*scale*scale`). numparam evaluates the
result per instance, so binning and the device both see the effective,
scaled geometry -- this is how HSPICE applies a subcircuit's element
scale factor. Handles the three value forms found in foundry models:
single-quoted `'expr'`, braced `{expr}`, and a bare token / number.
The output always uses single quotes, the form proven to resolve on an
expanded instance line in hs/spe mode. Returns a newly-allocated line,
or NULL if `pname` is absent (value left untouched). */
static char *
scale_geom_param(const char *line, const char *pname, int power)
{
char key[8];
const char *at, *vstart, *vend, *inner_b, *inner_e;
const char *suffix = (power == 2) ? "*scale*scale" : "*scale";
snprintf(key, sizeof key, " %s=", pname);
at = strstr(line, key);
if (!at)
return NULL;
vstart = at + strlen(key);
if (*vstart == '\'') { /* 'expr' (HSPICE) */
for (vend = vstart + 1; *vend && *vend != '\''; vend++)
;
if (*vend != '\'')
return NULL; /* unterminated -- leave alone */
inner_b = vstart + 1;
inner_e = vend; /* the closing quote */
vend++;
}
else if (*vstart == '{') { /* {expr} */
int depth = 0;
for (vend = vstart; *vend; vend++) {
if (*vend == '{')
depth++;
else if (*vend == '}') {
depth--;
if (depth == 0) { vend++; break; }
}
}
if (depth != 0)
return NULL; /* unbalanced -- leave alone */
inner_b = vstart + 1;
inner_e = vend - 1; /* the closing brace */
}
else { /* bare token / number */
for (vend = vstart; *vend && !isspace_c((unsigned char) *vend); vend++)
;
inner_b = vstart;
inner_e = vend;
}
return tprintf("%.*s'(%.*s)%s'%s",
(int)(vstart - line), line,
(int)(inner_e - inner_b), inner_b,
suffix, vend);
}
#define MAXNEST 21 #define MAXNEST 21
/*-------------------------------------------------------------------*/ /*-------------------------------------------------------------------*/
/* doit does the actual substitution of .subckts. */ /* doit does the actual substitution of .subckts. */
@ -617,102 +890,73 @@ doit(struct card *deck, wordlist *modnames) {
if (sss) { if (sss) {
// tprint(sss->su_def); // tprint(sss->su_def);
struct card *su_deck = inp_deckcopy(sss->su_def); struct card *su_deck = inp_deckcopy(sss->su_def);
/* (HSPICE element scale is applied earlier, in
inp_subcktexpand(), by wrapping the subckt body's
geometry expressions before the numparam passes.) */
/* If we have modern PDKs, we have to reduce the amount of memory required. /* If we have modern PDKs, we have to reduce the amount of memory required.
We try to reduce the models to the one really used. We try to reduce the models to the one really used.
Otherwise su_deck is full of unused binning models.*/ Otherwise su_deck is full of unused binning models.*/
if ((newcompat.hs || newcompat.spe) && c->w > 0 && c->l > 0) { if ((newcompat.hs || newcompat.spe) && c->w > 0 && c->l > 0) {
/* extract wmin, wmax, lmin, lmax */
struct card* new_deck = su_deck; struct card* new_deck = su_deck;
struct card* prev = NULL; struct card* prev = NULL;
while (su_deck) { struct card* scan;
if (!ciprefix(".model", su_deck->line)) { int nmatch = 0;
prev = su_deck;
su_deck = su_deck->nextcard;
continue;
}
char* curr_line = su_deck->line; float csl = (float)scale * c->l;
/* scale by nf */
float csw = (float)scale * c->w / c->nf;
/* Does any bin contain the instance's w/l? These
are the DRAWN dimensions off the X line; the bin
ranges are post-shrink. For PDKs that apply the
shrink inside the subcircuit (l_scale='l*shrink',
as foundry HV devices do) the drawn dims do not
map to the bins here, so nothing matches. In that
case keep every model card and let INPgetModBin do
the binning after numparam has applied the shrink
to the MOS line. */
for (scan = su_deck; scan; scan = scan->nextcard) {
float fwmin, fwmax, flmin, flmax; float fwmin, fwmax, flmin, flmax;
char *wmin = strstr(curr_line, " wmin="); if (!ciprefix(".model", scan->line))
if (wmin) {
int err;
wmin = wmin + 6;
fwmin = (float)INPevaluate(&wmin, &err, 0);
if (err) {
prev = su_deck;
su_deck = su_deck->nextcard;
continue;
}
}
else {
prev = su_deck;
su_deck = su_deck->nextcard;
continue; continue;
} if (!get_model_bins(scan->line, &fwmin, &fwmax,
char *wmax = strstr(curr_line, " wmax="); &flmin, &flmax))
if (wmax) { continue;
int err; if (csl >= flmin && csl < flmax &&
wmax = wmax + 6; csw >= fwmin && csw < fwmax)
fwmax = (float)INPevaluate(&wmax, &err, 0); nmatch++;
if (err) { }
prev = su_deck;
su_deck = su_deck->nextcard; /* Only when a bin matched do we drop the others (the
continue; memory optimization). Otherwise leave su_deck
} untouched. */
} while (nmatch > 0 && su_deck) {
else { float fwmin, fwmax, flmin, flmax;
if (!ciprefix(".model", su_deck->line) ||
!get_model_bins(su_deck->line, &fwmin, &fwmax,
&flmin, &flmax) ||
(csl >= flmin && csl < flmax &&
csw >= fwmin && csw < fwmax)) {
/* keep: non-model, unbinned, or matching */
prev = su_deck; prev = su_deck;
su_deck = su_deck->nextcard; su_deck = su_deck->nextcard;
continue; continue;
} }
char* lmin = strstr(curr_line, " lmin="); /* out-of-bin model card: drop it */
if (lmin) { {
int err;
lmin = lmin + 6;
flmin = (float)INPevaluate(&lmin, &err, 0);
if (err) {
prev = su_deck;
su_deck = su_deck->nextcard;
continue;
}
}
else {
prev = su_deck;
su_deck = su_deck->nextcard;
continue;
}
char* lmax = strstr(curr_line, " lmax=");
if (lmax) {
int err;
lmax = lmax + 6;
flmax = (float)INPevaluate(&lmax, &err, 0);
if (err) {
prev = su_deck;
su_deck = su_deck->nextcard;
continue;
}
}
else {
prev = su_deck;
su_deck = su_deck->nextcard;
continue;
}
float csl = (float)scale * c->l;
/* scale by nf */
float csw = (float)scale * c->w / c->nf;
/*fprintf(stdout, "Debug: nf = %f\n", c->nf);*/
if (csl >= flmin && csl < flmax && csw >= fwmin && csw < fwmax) {
/* use the current .model card */
prev = su_deck;
su_deck = su_deck->nextcard;
continue;
}
else {
struct card* tmpcard = su_deck->nextcard; struct card* tmpcard = su_deck->nextcard;
line_free_x(prev->nextcard, FALSE); if (prev) {
su_deck = prev->nextcard = tmpcard; line_free_x(prev->nextcard, FALSE);
su_deck = prev->nextcard = tmpcard;
}
else {
line_free_x(new_deck, FALSE);
su_deck = new_deck = tmpcard;
}
} }
} }
su_deck = new_deck; su_deck = new_deck;
@ -1379,12 +1623,28 @@ translate(struct card *deck, char *formal, int flen, char *actual, char *scname,
bxx_putc(&buffer, ' '); bxx_putc(&buffer, ' ');
} }
/* Next we handle the POLY (if any) */ /* HSPICE compat: skip optional source-type marker
/* get next token */ * (vccs/vcvs/cccs/ccvs) between output nodes and POLY.
* The letter `g`/`e`/`f`/`h` already encodes the type, so
* the marker is redundant; consume it so the downstream
* POLY translator (and inp2g/e/f/h) sees a clean line. */
t = s; t = s;
next_name = gettok_noparens(&t); next_name = gettok_noparens(&t);
if ((strcmp(next_name, "POLY") == 0) || if (next_name && (
(strcmp(next_name, "poly") == 0)) { (dev_type == 'g' && cieq(next_name, "vccs")) ||
(dev_type == 'e' && cieq(next_name, "vcvs")) ||
(dev_type == 'f' && cieq(next_name, "cccs")) ||
(dev_type == 'h' && cieq(next_name, "ccvs")))) {
s = t;
tfree(next_name);
t = s;
next_name = gettok_noparens(&t);
}
/* Next we handle the POLY (if any) */
if (next_name &&
((strcmp(next_name, "POLY") == 0) ||
(strcmp(next_name, "poly") == 0))) {
#ifdef TRACE #ifdef TRACE
printf("In translate, looking at e, f, g, h found poly\n"); printf("In translate, looking at e, f, g, h found poly\n");

View File

@ -7,6 +7,7 @@
#define ngspice_SUBCKT_H #define ngspice_SUBCKT_H
struct card *inp_subcktexpand(struct card *deck); struct card *inp_subcktexpand(struct card *deck);
void inp_apply_subckt_scale(struct card *deck);
struct card *inp_deckcopy(struct card *deck); struct card *inp_deckcopy(struct card *deck);
struct card *inp_deckcopy_oc(struct card *deck); struct card *inp_deckcopy_oc(struct card *deck);
struct card *inp_deckcopy_ln(struct card *deck); struct card *inp_deckcopy_ln(struct card *deck);

View File

@ -54,6 +54,14 @@ struct CKTnode {
CKTnode *next; /* pointer to the next node */ CKTnode *next; /* pointer to the next node */
unsigned int icGiven:1; /* FLAG ic given */ unsigned int icGiven:1; /* FLAG ic given */
unsigned int nsGiven:1; /* FLAG nodeset given */ unsigned int nsGiven:1; /* FLAG nodeset given */
unsigned int nogmin:1; /* FLAG: exclude this node's diagonal from the
* dynamic/true gmin-stepping pass (LoadGmin).
* Set for OSDI model-internal nodes that back
* implicit equations (laplace/zi/idt state):
* gmin on their small-valued diagonals is not a
* harmless leak-to-ground -- it heavily damps
* the integrator-state Newton update and breaks
* convergence ("gmin stepping failed"). */
}; };
/* defines for node parameters */ /* defines for node parameters */
@ -119,6 +127,66 @@ struct CKTcircuit {
double *CKTrhsOld; /* previous rhs value for convergence double *CKTrhsOld; /* previous rhs value for convergence
testing */ testing */
double *CKTrhsSpare; /* spare rhs value for reordering */ double *CKTrhsSpare; /* spare rhs value for reordering */
/* Axis 4 — Spectre/HSPICE-style absolute residual convergence check.
* NIconvTest gates on CKTresidConverged so that Newton declares
* convergence only when BOTH the solution vector AND the residual
* are within tolerance. CKTresidCheckDisabled is the
* `.option noresidcheck` opt-out. */
int CKTresidConverged;
int CKTresidCheckDisabled;
/* OSDI axis-3 step rejection (OSDI v0.5 EVAL_RET_FLAG_REJECT_STEP).
* CKTosdiStepReject is raised inside CKTload by an OSDI model
* returning REJECT_STEP, or by sanitize_jacobian when it sees a
* NaN/Inf Jacobian entry. NIiter checks the latch and bails with
* E_ITERLIM. CKTosdiStepRejectOff is the `.option noosdistepreject`
* opt-out. */
int CKTosdiStepReject;
int CKTosdiStepRejectOff;
/* 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
* point) where Newton can produce unphysical updates. NIiter uses
* this to gate per-node Δv limiting (voltage-range-agnostic
* substitute for model-supplied $limit when the model doesn't
* provide one). */
int CKThugeJThisIter;
/* Axis 2 — current Newton iteration index (1-based, per-NIiter-solve),
* published by NIiter before each CKTload and forwarded to OSDI models
* via SimInfo.newton_iter for iteration-aware limiter behaviour. */
int CKTosdiNewtonIter;
/* Small-dt convergence rescue. At very small CKTdelta during transient,
* sanitize-* paths in osdiload.c (and similar in other device loaders)
* fire spurious CKTnoncon++ events because the integration coefficient
* 2/dt amplifies tiny charge changes into apparent currents that exceed
* defensive clip thresholds. Those increments block NIconvTest from
* ever running, so even a perfectly settled iterate cannot declare
* convergence NIiter spins until maxIter and returns E_ITERLIM,
* causing dctran to cut delta to abort.
*
* When this flag is 0 (default), NIiter clears CKTnoncon after CKTload
* returns if CKTdelta is below 1 ps in transient mode, letting the
* downstream NIconvTest evaluate solution stability directly. Genuine
* non-convergence still fires because NIconvTest tests |Δx| against
* (reltol·|x| + vntol) per node an actually-diverging iterate fails
* that test regardless of CKTnoncon state. */
int CKTdtClearOff;
/* osdi_vlim: per-iteration Δv bound for OpenVA's compiler-side $limit
* synthesis (Mijalković-style DEVlimvds substitute applied at compile
* time). Generic fallback; per-shape variants below cascade through
* to this when not separately set. Default 0.1 V. Voltage-range-
* agnostic bounds Newton step magnitude, not absolute voltage. */
double CKTosdiVlim;
/* Per-MOSFET-shape variants. OpenVA's compiler-side synthesis emits
* $simparam_opt("osdi_vlim_vds"/"_vgs"/"_vbs", 0.1) depending on the
* classified probe shape (drain-source / gate-source / body-junction).
* ngspice's get_simparams cascades: per-shape value if > 0, else
* CKTosdiVlim, else 0.1V default. This lets the user tune each
* MOSFET-style probe class independently. */
double CKTosdiVlimVds;
double CKTosdiVlimVgs;
double CKTosdiVlimVbs;
double CKTosdiVlimNqs;
double *CKTirhs; /* current rhs value - being loaded double *CKTirhs; /* current rhs value - being loaded
(imag) */ (imag) */
double *CKTirhsOld; /* previous rhs value (imaginary)*/ double *CKTirhsOld; /* previous rhs value (imaginary)*/

View File

@ -92,6 +92,9 @@ enum {
OPT_GMINSTEPS, OPT_GMINSTEPS,
OPT_MINBREAK, OPT_MINBREAK,
OPT_NOOPITER, OPT_NOOPITER,
OPT_NORESIDCHECK, /* `.option noresidcheck` opts out of axis-4 dual-norm
* convergence (residual-vector check); revert to
* SPICE3 solution-only behaviour. */
OPT_EQNS, OPT_EQNS,
OPT_REORDTIME, OPT_REORDTIME,
OPT_METHOD, OPT_METHOD,
@ -136,6 +139,21 @@ enum {
OPT_LTEABSTOL, OPT_LTEABSTOL,
OPT_LTETRTOL, OPT_LTETRTOL,
OPT_NEWTRUNC, OPT_NEWTRUNC,
OPT_NOOSDISTEPREJECT, /* `.option noosdistepreject` disables the axis-3
* step-rejection mechanism (OSDI EVAL_RET_FLAG_REJECT_STEP
* + sanitize_jacobian-driven REJECT). Default off
* (rejection active). */
OPT_OSDIVLIM, /* `.option osdi_vlim=N` sets the per-iteration Δv
* bound consumed by OpenVA's compiler-side $limit
* synthesis. Default 0.1 V. Generic fallback;
* per-shape variants below override when set. */
OPT_OSDIVLIM_VDS, /* `.option osdi_vlim_vds=N` — drain-source-shape probes */
OPT_OSDIVLIM_VGS, /* `.option osdi_vlim_vgs=N` — gate-channel-shape probes */
OPT_OSDIVLIM_VBS, /* `.option osdi_vlim_vbs=N` — body-junction-shape probes */
OPT_OSDIVLIM_NQS, /* `.option osdi_vlim_nqs=N` — NQS helper-node probes (V(NC)/V(N1)/V(NR)/etc — uppercase-N convention) */
OPT_NODTCLEAR, /* `.option nodtclear` disables the small-dt
* CKTnoncon clear in niiter.c. See CKTdtClearOff
* comment in cktdefs.h. */
}; };
#ifdef XSPICE #ifdef XSPICE

View File

@ -0,0 +1,132 @@
/*
* OSDI deferred-evaluation side table.
*
* HSPICE-style PDK model cards (Samsung 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
* references `l`/`w`/`nf` etc. per-instance geometry that is not defined
* at .model parse time. HSPICE defers evaluation until each instance is
* bound and re-evaluates with the instance's geometry in scope.
*
* ngspice's numparam evaluates `{...}` expressions eagerly when the
* `.model` line is processed, which fails ("Undefined parameter [l]")
* before any instance exists. This side table fixes that path for OSDI
* models only:
*
* - At numparam time, when we see a `{...}` on a `.model` line that is
* headed for an OSDI device type AND that contains per-instance
* symbols, we save the expression text here (keyed by model + param
* name) and replace the brace block with the literal `0` so numparam
* can finish the line successfully. The model gets a sane (if
* placeholder) default for the parameter.
*
* - At instance creation, osdiload.c walks the saved entries for this
* instance's model, evaluates each expression with the instance's
* `l`/`w`/`nf`/`m`/`xnf` pushed into a transient numparam scope, and
* calls OSDIparam to install the result as a per-instance override of
* the model default.
*
* Built-in MOSFETs (BSIM3/4, HiSIM, etc.) are unaffected their model
* cards historically use pure-numeric values (with binning), so they
* never hit the deferred path.
*/
#ifndef NGSPICE_OSDI_DEFER_H
#define NGSPICE_OSDI_DEFER_H
#include "ngspice/ngspice.h"
/* Per-(model, param) deferred-expression entry. Stored in a singly-linked
* list per model name; lookups happen at instance bind time. Both
* `model_name` and `param_name` are case-insensitively compared.
*
* `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
* `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
* `x2.xmn1:nfet.0`), so we capture the scalar values of every
* non-instance-geom identifier referenced by the expression. At
* eval time these are pushed alongside l/w/nf/m so the expression
* can resolve. Without this, every subckt-scope reference fails
* with `Undefined parameter` and the deferred parameter ends up 0. */
typedef struct osdi_defer_entry_s {
char *model_name; /* duplicated, freed at exit */
char *param_name; /* duplicated */
char *expr_text; /* duplicated; the raw { ... } body */
int n_snap; /* count of snapshotted scope bindings */
char **snap_names; /* malloc'd array, each entry duplicated */
double *snap_values; /* malloc'd array, parallel to snap_names */
struct osdi_defer_entry_s *next;
} OsdiDeferEntry;
/* Register a deferred expression. Returns OK / non-zero on alloc fail.
* Strings are duplicated internally; caller owns its inputs. */
int osdi_defer_register(const char *model_name,
const char *param_name,
const char *expr_text);
/* Iterate all entries for a given model name; calls `cb` once per entry
* with the (param_name, expr_text, cb_arg). Returns the number of entries
* iterated. */
int osdi_defer_for_model(const char *model_name,
void (*cb)(const char *param_name,
const char *expr_text,
void *cb_arg),
void *cb_arg);
/* Returns true if any deferred entries exist for the given model name.
* Avoids the iteration overhead at instance-bind time when there's
* nothing to evaluate. */
bool osdi_defer_has(const char *model_name);
/* Free the entire side table. Called at simulator shutdown. */
void osdi_defer_clear(void);
/* Record (lmin, lmax) for a bin-selected model name. Called from
* 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
* `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). */
void osdi_defer_record_bin_range(const char *model_name,
double lmin, double lmax);
/* Look up the recorded (lmin, lmax) for a model name. Returns true
* on hit (fills *lmin, *lmax), false otherwise. Used by OSDIsetup
* to set the pre-eval default L per-model. */
bool osdi_defer_get_bin_range(const char *model_name,
double *lmin, double *lmax);
/* Preprocess a raw `.model` line. If the line targets an OSDI-level
* device (level=72 / level=77) and contains `{...}` or `'...'`
* expressions that reference per-instance symbols (l, w, nf, m, xnf),
* extract each such expression into the side table keyed by
* (model_name, param_name) and rewrite the expression in the line to
* the literal `0` so the downstream numparam pass sees a clean
* numeric value. The model parameter then gets a placeholder 0; the
* actual value is computed per-instance at bind time by walking the
* side table.
*
* `*line_p` must point to a heap-allocated line buffer; this function
* may free it and replace with a freshly-allocated buffer of the
* rewritten line. Returns 0 on success, non-zero on alloc error
* (line is left unchanged). No-op (returns 0, leaves line intact) for
* non-`.model` lines or non-OSDI levels. */
int osdi_defer_preprocess_line(char **line_p);
/* Evaluate a deferred expression with per-instance geometry in scope.
* Names `l`, `w`, `nf`, `m`, `xnf` are bound to the four numeric args
* (xnf aliases nf HSPICE convention). Returns 0 / writes scalar
* result to *out on success, non-zero on parse/eval error. Thin
* wrapper over nupa_eval_with_scope in the numparam engine. */
int osdi_defer_eval(const char *expr_text,
double l, double w, double nf, double m,
double *out);
#endif /* NGSPICE_OSDI_DEFER_H */

View File

@ -24,6 +24,18 @@ typedef struct OsdiRegistryEntry {
bool has_m; bool has_m;
/* OSDI v0.5 (2C) — number of absdelay transport-delay sites, guarded at
* load time against the published OSDI_DESCRIPTOR_SIZE so a model compiled
* before this field existed reads 0 (not garbage past its descriptor). */
uint32_t num_delay_sites;
/* OSDI 0.5 — persistent-state array (transition()/slew()/event toolkit):
* byte offset within the instance + slot count, both guarded against the
* published descriptor size. Used to snapshot/restore the array for
* deferred commit (only accepted steps update the model's history). */
uint32_t persistent_state_offset;
uint32_t persistent_state_count;
#ifdef KLU #ifdef KLU
uint32_t matrix_ptr_offset; uint32_t matrix_ptr_offset;
#endif #endif

View File

@ -27,6 +27,12 @@ Modified: 2000 AlansFixes
typedef struct sSMPmatrix { typedef struct sSMPmatrix {
MatrixFrame *SPmatrix ; /* pointer to sparse matrix */ MatrixFrame *SPmatrix ; /* pointer to sparse matrix */
/* Per-(external-row / node-number) flag: 1 => exclude this diagonal from
* the dynamic/true gmin-stepping pass (LoadGmin). Built lazily in NIiter
* from CKTnode.nogmin (set for OSDI model-internal implicit-equation
* nodes); NULL => add gmin to every diagonal (legacy behaviour). */
char *gmin_skip ;
#ifdef KLU #ifdef KLU
KLUmatrix *SMPkluMatrix ; /* KLU Pointer to the KLU Matrix Data Structure (only for CIDER, for the moment) */ KLUmatrix *SMPkluMatrix ; /* KLU Pointer to the KLU Matrix Data Structure (only for CIDER, for the moment) */
unsigned int CKTkluMODE:1 ; /* KLU MODE parameter to enable KLU or not from the heuristic */ unsigned int CKTkluMODE:1 ; /* KLU MODE parameter to enable KLU or not from the heuristic */

View File

@ -25,6 +25,16 @@ Modified: 1999 Paolo Nenzi
#define TEMP_CODE 1023 #define TEMP_CODE 1023
#endif #endif
/* HSPICE-compat sweep of a `.param` name (instead of a V/I source,
* resistor, or temperature). Used by `.dc paramName start stop step`
* where paramName matches an existing .param. Each step updates the
* numparam entry's value and pushes it to any V/I source whose DC
* field was bound at parse time (see inpdpar.c
* dpar_register_binding). */
#ifndef PARAM_CODE
#define PARAM_CODE 1022
#endif
typedef struct { typedef struct {
int JOBtype; int JOBtype;
JOB *JOBnextJob; JOB *JOBnextJob;
@ -41,6 +51,13 @@ typedef struct {
int TRCVset[TRCVNESTLEVEL]; /* flag to indicate this nest level used */ int TRCVset[TRCVNESTLEVEL]; /* flag to indicate this nest level used */
int TRCVnestLevel; /* number of levels of nesting called for */ int TRCVnestLevel; /* number of levels of nesting called for */
int TRCVnestState; /* iteration state during pause */ int TRCVnestState; /* iteration state during pause */
int TRCVstepCount[TRCVNESTLEVEL]; /* count of accepted steps so far —
* used by PARAM_CODE sweeps to compute
* cur_val = start + N*step instead of
* accumulating += step, which would
* miss the exact endpoint by ~340 ULP
* after 340 increments and break
* `.measure ... when X=<endpoint>`. */
} TRCV; } TRCV;
enum { enum {

View File

@ -64,6 +64,14 @@ struct TSKtask {
double TSKdefaultMosAS; double TSKdefaultMosAS;
unsigned int TSKfixLimit:1; unsigned int TSKfixLimit:1;
unsigned int TSKnoOpIter:1; /* no OP iterating, go straight to gmin step */ unsigned int TSKnoOpIter:1; /* no OP iterating, go straight to gmin step */
unsigned int TSKnoResidCheck:1; /* `.option noresidcheck` opt-out of
* axis-4 dual-norm convergence */
unsigned int TSKnoOsdiStepReject:1; /* `.option noosdistepreject` disables
* axis-3 step rejection in osdiload.c
* + niiter.c */
unsigned int TSKnoDtClear:1; /* `.option nodtclear` disables the
* small-dt CKTnoncon clear in
* niiter.c */
unsigned int TSKtryToCompact:1; /* flag for LTRA lines */ unsigned int TSKtryToCompact:1; /* flag for LTRA lines */
unsigned int TSKbadMos3:1; /* flag for MOS3 models */ unsigned int TSKbadMos3:1; /* flag for MOS3 models */
unsigned int TSKkeepOpInfo:1; /* flag for small signal analyses */ unsigned int TSKkeepOpInfo:1; /* flag for small signal analyses */
@ -72,6 +80,11 @@ struct TSKtask {
unsigned int TSKnoopac:1; /* flag for no OP calculation before AC */ unsigned int TSKnoopac:1; /* flag for no OP calculation before AC */
double TSKabsDv; /* abs limit for iter-iter voltage change */ double TSKabsDv; /* abs limit for iter-iter voltage change */
double TSKrelDv; /* rel limit for iter-iter voltage change */ double TSKrelDv; /* rel limit for iter-iter voltage change */
double TSKosdiVlim; /* `.option osdi_vlim=N` — generic */
double TSKosdiVlimVds; /* `.option osdi_vlim_vds=N` */
double TSKosdiVlimVgs; /* `.option osdi_vlim_vgs=N` */
double TSKosdiVlimVbs; /* `.option osdi_vlim_vbs=N` */
double TSKosdiVlimNqs; /* `.option osdi_vlim_nqs=N` — NQS helper nodes */
double TSKepsmin; /* minimum value for log */ double TSKepsmin; /* minimum value for log */
#ifdef KLU #ifdef KLU

View File

@ -35,7 +35,7 @@ CircuitIsDigital (void)
#endif #endif
} }
static void LoadGmin_CSC (double **diag, unsigned int n, double Gmin) ; static void LoadGmin_CSC_skip (SMPmatrix *eMatrix, double Gmin) ;
static void LoadGmin (SMPmatrix *eMatrix, double Gmin) ; static void LoadGmin (SMPmatrix *eMatrix, double Gmin) ;
typedef struct sElement { typedef struct sElement {
@ -585,7 +585,7 @@ SMPluFac (SMPmatrix *Matrix, double PivTol, double Gmin)
} }
if (Matrix->SMPkluMatrix->KLUloadDiagGmin) { if (Matrix->SMPkluMatrix->KLUloadDiagGmin) {
LoadGmin_CSC (Matrix->SMPkluMatrix->KLUmatrixDiag, Matrix->SMPkluMatrix->KLUmatrixN, Gmin) ; LoadGmin_CSC_skip (Matrix, Gmin) ;
} }
ret = klu_refactor (Matrix->SMPkluMatrix->KLUmatrixAp, Matrix->SMPkluMatrix->KLUmatrixAi, Matrix->SMPkluMatrix->KLUmatrixAx, ret = klu_refactor (Matrix->SMPkluMatrix->KLUmatrixAp, Matrix->SMPkluMatrix->KLUmatrixAi, Matrix->SMPkluMatrix->KLUmatrixAx,
@ -764,7 +764,7 @@ SMPreorder (SMPmatrix *Matrix, double PivTol, double PivRel, double Gmin)
} }
if (Matrix->SMPkluMatrix->KLUloadDiagGmin) { if (Matrix->SMPkluMatrix->KLUloadDiagGmin) {
LoadGmin_CSC (Matrix->SMPkluMatrix->KLUmatrixDiag, Matrix->SMPkluMatrix->KLUmatrixN, Gmin) ; LoadGmin_CSC_skip (Matrix, Gmin) ;
} }
Matrix->SMPkluMatrix->KLUmatrixCommon->tol = PivRel ; Matrix->SMPkluMatrix->KLUmatrixCommon->tol = PivRel ;
@ -1768,18 +1768,34 @@ SMPcDProd (SMPmatrix *Matrix, SPcomplex *pMantissa, int *pExponent)
*/ */
/* KLU diagonal-gmin pass. Adds Gmin to every diagonal element EXCEPT the
* synthesized analog-operator implicit-equation state nodes (laplace, zi, idt,
* transition-delay) flagged in eMatrix->gmin_skip -- gmin there over-damps
* the integrator-state Newton update and breaks gmin stepping (see spsmp.c
* LoadGmin for the full rationale). KLU column i is the post-collapse index;
* the original ngspice node number is NewToOld[i] + 1 (KLU columns are 0-based,
* node numbers 1-based -- cf. the singular_col + 1 node reporting in this
* file). When gmin_skip is NULL (no flagged nodes) this is byte-for-byte the
* legacy "gmin on every diagonal" behaviour. */
static void static void
LoadGmin_CSC (double **diag, unsigned int n, double Gmin) LoadGmin_CSC_skip (SMPmatrix *eMatrix, double Gmin)
{ {
KLUmatrix *klu = eMatrix->SMPkluMatrix ;
double **diag = klu->KLUmatrixDiag ;
unsigned int n = klu->KLUmatrixN ;
char *skip = eMatrix->gmin_skip ;
unsigned int *new2old = klu->KLUmatrixNodeCollapsingNewToOld ;
unsigned int i ; unsigned int i ;
if (Gmin != 0.0) { if (Gmin == 0.0)
for (i = 0 ; i < n ; i++) { return ;
if (diag [i] != NULL) {
// Not all the elements on the diagonal are present, when the circuit is parsed for (i = 0 ; i < n ; i++) {
*(diag [i]) += Gmin ; if (diag [i] == NULL)
} continue ; // not all diagonal elements are present after parsing
} if (skip && new2old && skip [new2old [i] + 1])
continue ;
*(diag [i]) += Gmin ;
} }
} }

View File

@ -90,10 +90,22 @@ NIconvTest(CKTcircuit *ckt)
/* CKTconvTest early-returns nonzero 'i' on the first error /* CKTconvTest early-returns nonzero 'i' on the first error
* in evaluating convergence (such as parameter out of range) so * in evaluating convergence (such as parameter out of range) so
* there may be untested devices that have not yet converged */ * there may be untested devices that have not yet converged */
if (i) if (i) {
ckt->CKTtroubleNode = 0; ckt->CKTtroubleNode = 0;
return(i); return(i);
#else /* NEWCONV */ }
return(0);
#endif /* NEWCONV */ #endif /* NEWCONV */
/* Bürmen axis 4: dual-norm convergence requires the residual to
* also be stable, not just the solution vector. Catches the
* SPICE3 false-convergence pathology where the iterate stops
* moving (damping/scaling artifact) but f(x) is still nonzero.
* The flag was set by NIiter from CKTrhs immediately after
* CKTload. Opt-out via `.option noresidcheck`. */
if (!ckt->CKTresidCheckDisabled && !ckt->CKTresidConverged) {
ckt->CKTtroubleNode = 0;
return 1;
}
return 0;
} }

View File

@ -19,9 +19,9 @@ NIdestroy(CKTcircuit *ckt)
if (ckt->CKTmatrix) if (ckt->CKTmatrix)
SMPdestroy(ckt->CKTmatrix); SMPdestroy(ckt->CKTmatrix);
FREE(ckt->CKTmatrix); FREE(ckt->CKTmatrix);
if(ckt->CKTrhs) FREE(ckt->CKTrhs); if(ckt->CKTrhs) FREE(ckt->CKTrhs);
if(ckt->CKTrhsOld) FREE(ckt->CKTrhsOld); if(ckt->CKTrhsOld) FREE(ckt->CKTrhsOld);
if(ckt->CKTrhsSpare) FREE(ckt->CKTrhsSpare); if(ckt->CKTrhsSpare) FREE(ckt->CKTrhsSpare);
if(ckt->CKTirhs) FREE(ckt->CKTirhs); if(ckt->CKTirhs) FREE(ckt->CKTirhs);
if(ckt->CKTirhsOld) FREE(ckt->CKTirhsOld); if(ckt->CKTirhsOld) FREE(ckt->CKTirhsOld);
if(ckt->CKTirhsSpare) FREE(ckt->CKTirhsSpare); if(ckt->CKTirhsSpare) FREE(ckt->CKTirhsSpare);

View File

@ -33,11 +33,15 @@ NIiter(CKTcircuit *ckt, int maxIter)
int iterno = 0; int iterno = 0;
int ipass = 0; int ipass = 0;
/* Track max|Δv| across iterations to gate the Stage B
* progressive-halving below see the damping block. */
double prev_max_dv = 0.0;
/* some convergence issues that get resolved by increasing max iter */ /* some convergence issues that get resolved by increasing max iter */
if (maxIter < 100) if (maxIter < 100)
maxIter = 100; maxIter = 100;
if ((ckt->CKTmode & MODETRANOP) && (ckt->CKTmode & MODEUIC)) { if ((ckt->CKTmode & MODETRANOP) && (ckt->CKTmode & MODEUIC)) {
SWAP(double *, ckt->CKTrhs, ckt->CKTrhsOld); SWAP(double *, ckt->CKTrhs, ckt->CKTrhsOld);
error = CKTload(ckt); error = CKTload(ckt);
@ -66,9 +70,36 @@ NIiter(CKTcircuit *ckt, int maxIter)
/* OldCKTstate0 = TMALLOC(double, ckt->CKTnumStates + 1); */ /* OldCKTstate0 = TMALLOC(double, ckt->CKTnumStates + 1); */
/* Build the gmin-skip map once: matrix rows (by node number) to EXCLUDE
* from the dynamic/true gmin-stepping diagonal pass. OSDI model-internal
* nodes that back analog-operator implicit equations (laplace/zi/idt
* state) are flagged node->nogmin in osdisetup; gmin on their small
* diagonals over-damps the state Newton update and breaks gmin stepping
* ("gmin stepping failed"). TMALLOC zero-fills, so only nogmin nodes are
* set; NULL/legacy means "add gmin to every diagonal". */
if (ckt->CKTmatrix && !ckt->CKTmatrix->gmin_skip && ckt->CKTmaxEqNum > 0) {
CKTnode *gnode;
ckt->CKTmatrix->gmin_skip = TMALLOC(char, ckt->CKTmaxEqNum + 1);
if (ckt->CKTmatrix->gmin_skip)
for (gnode = ckt->CKTnodes; gnode; gnode = gnode->next)
if (gnode->nogmin && gnode->number > 0 &&
gnode->number <= ckt->CKTmaxEqNum)
ckt->CKTmatrix->gmin_skip[gnode->number] = 1;
}
for (;;) { for (;;) {
ckt->CKTnoncon = 0; ckt->CKTnoncon = 0;
ckt->CKTosdiStepReject = 0;
ckt->CKThugeJThisIter = 0;
/* Axis 2 — publish the current Newton iteration index (1-based:
* the iteration the upcoming CKTload computes) so OSDI models see
* it via SimInfo.newton_iter and can make their $limit / limiter
* behaviour iteration-aware (e.g. tighten the clamp as the count
* climbs to break oscillation). Resets to 1 on every NIiter call,
* so it is per-Newton-solve, not cumulative. */
ckt->CKTosdiNewtonIter = iterno + 1;
#ifdef NEWPRED #ifdef NEWPRED
if (!(ckt->CKTmode & MODEINITPRED)) if (!(ckt->CKTmode & MODEINITPRED))
@ -88,6 +119,47 @@ NIiter(CKTcircuit *ckt, int maxIter)
return (error); return (error);
} }
/* Small-dt convergence rescue. At very small CKTdelta during
* transient, sanitize-* paths in osdiload.c (and similar in
* other device loaders) fire spurious CKTnoncon++ events
* because the integration coefficient 2/dt amplifies tiny
* charge changes into apparent currents that exceed the
* defensive 1 A clip. Those increments block NIconvTest
* from running, so a perfectly settled iterate cannot
* converge NIiter spins until maxIter and returns
* E_ITERLIM, dctran cuts delta, eventually aborts with
* "Timestep too small".
*
* Clear CKTnoncon here so the downstream NIconvTest can
* evaluate solution stability directly. Genuine
* divergence still fires: NIconvTest tests |Δx| against
* (reltol·|x| + vntol) per node an actually-diverging
* iterate fails that test regardless of CKTnoncon state.
*
* Threshold = 1 ps. Above that, the integration
* coefficient is small enough that sanitize-* increments
* reflect real model anomalies, not numerical artifacts.
*
* Opt-out via `.option nodtclear`. */
if ((ckt->CKTmode & MODETRAN) &&
!ckt->CKTdtClearOff &&
ckt->CKTdelta < 1.0e-12) {
ckt->CKTnoncon = 0;
}
/* Axis-3 step rejection: an OSDI model has explicitly raised
* EVAL_RET_FLAG_REJECT_STEP, or sanitize_jacobian has seen
* a NaN/Inf Jacobian entry (catastrophic clipping can't
* save a falsified linearization). Return E_ITERLIM so
* dctran cuts CKTdelta by 8 and retries with a better-
* conditioned predicted state. Only honored during
* transient. */
if (ckt->CKTosdiStepReject && (ckt->CKTmode & MODETRAN)) {
ckt->CKTstat->STATnumIter += iterno;
FREE(OldCKTstate0);
return (E_ITERLIM);
}
/* printf("after loading, before solving\n"); */ /* printf("after loading, before solving\n"); */
/* CKTdump(ckt); */ /* CKTdump(ckt); */
@ -250,10 +322,24 @@ NIiter(CKTcircuit *ckt, int maxIter)
memcpy(OldCKTstate0, ckt->CKTstate0, memcpy(OldCKTstate0, ckt->CKTstate0,
(size_t) ckt->CKTnumStates * sizeof(double)); (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;
startTime = SPfrontEnd->IFseconds(); startTime = SPfrontEnd->IFseconds();
SMPsolve(ckt->CKTmatrix, ckt->CKTrhs, ckt->CKTrhsSpare); SMPsolve(ckt->CKTmatrix, ckt->CKTrhs, ckt->CKTrhsSpare);
ckt->CKTstat->STATsolveTime += ckt->CKTstat->STATsolveTime +=
SPfrontEnd->IFseconds() - startTime; SPfrontEnd->IFseconds() - startTime;
#ifdef STEPDEBUG #ifdef STEPDEBUG
/*XXXX*/ /*XXXX*/
if (ckt->CKTrhs[0] != 0.0) if (ckt->CKTrhs[0] != 0.0)
@ -268,6 +354,93 @@ NIiter(CKTcircuit *ckt, int maxIter)
ckt->CKTrhsSpare[0] = 0; ckt->CKTrhsSpare[0] = 0;
ckt->CKTrhsOld[0] = 0; ckt->CKTrhsOld[0] = 0;
/* Newton-step limiter (simulator-side $limit substitute).
* Always-on 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-
* supplied analog of this. Threshold is CKTabsDv (default
* 0.5 V) a model-parameter-agnostic tolerance, not a
* voltage rail.
*
* Skipped on iteration 1 (no previous iterate to compare).
* Skipped when CKTnodes is NULL (matrix not yet built).
*
* For models compiled with the OpenVA compiler-side
* $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) {
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
* stagnation gate. */
CKTnode *node;
double max_dv = 0.0;
for (node = ckt->CKTnodes->next; node; node = node->next) {
if (node->type != SP_VOLTAGE) continue;
double diff = fabs(ckt->CKTrhs[node->number] -
ckt->CKTrhsOld[node->number]);
if (diff > max_dv) max_dv = diff;
}
/* Two-stage limiter:
*
* Stage A (always): SCALAR damping if any node's |Δv|
* exceeds dv_max scale all node updates by
* dv_max/max_dv. Preserves the proportional
* structure Newton's linearization relies on, so
* the local consistency between coupled nodes
* (e.g. pinb E1 net_6 Rs3 net_7) is
* maintained even when the natural step is
* unphysically large.
*
* Stage B (past iter 3, ONLY when stagnating):
* progressively tighten dv_max by halving so any
* persistent oscillation damps out within a few
* iterations. Capped at 6 halvings (dv_max/64
* 8 mV for default 0.5 V).
*
* 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
* 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
* iter 10 even though Newton was making strong
* monotonic progress the apparent "10 iters with
* no convergence" was actually "10 iters of forced
* under-correction." Gating on stagnation lets
* Newton keep stepping at its natural rate when
* it's converging, and only suppresses when it's
* genuinely oscillating. */
bool stagnating = (iterno > 3) &&
(prev_max_dv > 0.0) &&
(max_dv > 0.7 * prev_max_dv);
if (stagnating) {
int shift = iterno - 3;
if (shift > 6) shift = 6;
dv_max /= (double)(1 << shift);
}
/* Save the PRE-clamp max|Δv| for next iter's
* stagnation check. Post-clamp values would be
* pinned at dv_max whenever Stage A fires, which
* destroys the iteration-trend signal Stage B needs
* to distinguish "Newton genuinely converging but
* being clamped" from "Newton stuck oscillating." */
prev_max_dv = max_dv;
if (max_dv > dv_max) {
double scale = dv_max / max_dv;
for (node = ckt->CKTnodes->next; node; node = node->next) {
if (node->type != SP_VOLTAGE) continue;
double diff = ckt->CKTrhs[node->number] -
ckt->CKTrhsOld[node->number];
ckt->CKTrhs[node->number] =
ckt->CKTrhsOld[node->number] + scale * diff;
}
}
}
if (iterno > maxIter) { if (iterno > maxIter) {
ckt->CKTstat->STATnumIter += iterno; ckt->CKTstat->STATnumIter += iterno;
/* we don't use this info during transient analysis */ /* we don't use this info during transient analysis */

View File

@ -299,6 +299,7 @@ SMPnewMatrixForCIDER (SMPmatrix *Matrix, int size, int complex)
void void
SMPdestroy(SMPmatrix *Matrix) SMPdestroy(SMPmatrix *Matrix)
{ {
SP_FREE( Matrix->gmin_skip );
spDestroy( Matrix->SPmatrix ); spDestroy( Matrix->SPmatrix );
} }
@ -460,6 +461,7 @@ static void
LoadGmin(SMPmatrix *eMatrix, double Gmin) LoadGmin(SMPmatrix *eMatrix, double Gmin)
{ {
MatrixPtr Matrix = eMatrix->SPmatrix; MatrixPtr Matrix = eMatrix->SPmatrix;
char *skip = eMatrix->gmin_skip;
int I; int I;
ArrayOfElementPtrs Diag; ArrayOfElementPtrs Diag;
ElementPtr diag; ElementPtr diag;
@ -470,6 +472,13 @@ LoadGmin(SMPmatrix *eMatrix, double Gmin)
if (Gmin != 0.0) { if (Gmin != 0.0) {
Diag = Matrix->Diag; Diag = Matrix->Diag;
for (I = Matrix->Size; I > 0; I--) { for (I = Matrix->Size; I > 0; I--) {
/* Skip OSDI internal (implicit-equation) node diagonals -- gmin
* there over-damps the integrator-state Newton update and breaks
* gmin stepping. Matrix->Diag is indexed by internal row I; map
* back to the external node number to look up the per-node flag. */
if (skip && Matrix->IntToExtRowMap &&
skip[Matrix->IntToExtRowMap[I]])
continue;
if ((diag = Diag[I]) != NULL) if ((diag = Diag[I]) != NULL)
diag->Real += Gmin; diag->Real += Gmin;
} }

View File

@ -6,6 +6,7 @@ libosdi_la_SOURCES = \
osdi.h \ osdi.h \
osdidefs.h \ osdidefs.h \
osdiext.h \ osdiext.h \
\
osdiinit.c \ osdiinit.c \
osdiload.c \ osdiload.c \
osdiacld.c \ osdiacld.c \
@ -15,7 +16,8 @@ libosdi_la_SOURCES = \
osdisetup.c \ osdisetup.c \
osditrunc.c \ osditrunc.c \
osdipzld.c \ osdipzld.c \
osdicallbacks.c osdicallbacks.c \
osdi_defer.c
libosdi_la_LIBADD = $(XSPICEDLLIBS) libosdi_la_LIBADD = $(XSPICEDLLIBS)

View File

@ -18,7 +18,7 @@
#define OSDI_VERSION_MAJOR_CURR 0 #define OSDI_VERSION_MAJOR_CURR 0
#define OSDI_VERSION_MINOR_CURR 3 #define OSDI_VERSION_MINOR_CURR 5
#define PARA_TY_MASK 3 #define PARA_TY_MASK 3
#define PARA_TY_REAL 0 #define PARA_TY_REAL 0
@ -37,6 +37,9 @@
#define JACOBIAN_ENTRY_REACT_CONST 2 #define JACOBIAN_ENTRY_REACT_CONST 2
#define JACOBIAN_ENTRY_RESIST 4 #define JACOBIAN_ENTRY_RESIST 4
#define JACOBIAN_ENTRY_REACT 8 #define JACOBIAN_ENTRY_REACT 8
/* OSDI v0.5 (2C AC delay) — entry has a delay-coupling part loaded
* x e^{-jw*td} during AC (touches both real and imag matrix parts). */
#define JACOBIAN_ENTRY_DELAY 16
#define CALC_RESIST_RESIDUAL 1 #define CALC_RESIST_RESIDUAL 1
#define CALC_REACT_RESIDUAL 2 #define CALC_REACT_RESIDUAL 2
@ -60,6 +63,43 @@
#define EVAL_RET_FLAG_FATAL 2 #define EVAL_RET_FLAG_FATAL 2
#define EVAL_RET_FLAG_FINISH 4 #define EVAL_RET_FLAG_FINISH 4
#define EVAL_RET_FLAG_STOP 8 #define EVAL_RET_FLAG_STOP 8
/* OSDI v0.5 axis-3 step rejection. Model raises this when it detects
* an ill-conditioned regime (degenerate Jacobian, predicted-voltage
* out of validity, internal-node blow-up). ngspice cuts the transient
* step and retries with smaller delta instead of iterating fruitlessly
* on a falsified linearization. The simulator may also raise this
* internally (see sanitize_jacobian in osdiload.c) to honor the same
* step-rejection semantics when the model itself doesn't set the flag. */
#define EVAL_RET_FLAG_REJECT_STEP 16
/* OSDI v0.5 axis-4 event-driven analog operators. See
* OSDI_0_5_DESIGN.md in the OpenVA tree for the full design.
*
* EVENT the model wrote one or more entries into the
* pending_events array of OsdiSimInfo. The simulator
* should schedule them and advance to the requested
* times instead of the LTE-chosen ones.
* CROSS the model wrote non-zero values into one or more
* slots of the cross_expr array. The simulator should
* track them across consecutive accepted evals and
* bisect on sign-flip per the corresponding
* OsdiCrossExprMeta direction.
*
* S3a (this commit) defines the bits; S3b implements the runtime
* loop logic that acts on them.
*/
#define EVAL_RET_FLAG_EVENT 32
#define EVAL_RET_FLAG_CROSS 64
/* OSDI v0.5 OsdiEventKind discriminants (§2.6 of the design doc).
* Stored in the `kind` field of OsdiEventRequest (filled by the
* model) and OsdiEventSlotMeta (defined by OpenVA at compile time)
* to tell the simulator which kind of analog-block event-control
* body originated the slot. */
#define OSDI_EVENT_KIND_CROSS 1
#define OSDI_EVENT_KIND_TIMER 2
#define OSDI_EVENT_KIND_INITIAL_STEP 3
#define OSDI_EVENT_KIND_FINAL_STEP 4
#define LOG_LVL_MASK 8 #define LOG_LVL_MASK 8
@ -88,6 +128,48 @@ typedef struct OsdiSimParas {
char **vals_str; char **vals_str;
}OsdiSimParas; }OsdiSimParas;
/* OSDI v0.5 — event request slot in OsdiSimInfo.pending_events.
*
* The model writes one of these per eval() per active timer/event
* body that wants the simulator to advance to a specific future
* time. The simulator consumes the slots, schedules the requests,
* and zeros at_time before the next eval to indicate "consumed".
* See OSDI_0_5_DESIGN.md §2.5 for the layout and lifecycle.
*/
typedef struct OsdiEventRequest {
double at_time; /* zero / NaN = no request from this slot */
uint32_t event_id; /* which OsdiEventSlotMeta this corresponds to */
uint32_t kind; /* OSDI_EVENT_KIND_* */
}OsdiEventRequest;
/* OSDI v0.5 — per-cross-expression metadata in OsdiDescriptor.
*
* One entry per `@(cross(...))` body the model declares. OpenVA
* fills this at compile time from the body's explicit arguments
* (direction, ttol, etol). The simulator uses the metadata to
* decide whether each newly-observed cross_expr value qualifies as
* a sign-flip crossing it should bisect on.
*/
typedef struct OsdiCrossExprMeta {
int32_t direction; /* +1 rising, -1 falling, 0 any */
double ttol; /* bisection time tolerance */
double etol; /* bisection expression tolerance */
uint32_t event_id; /* which OsdiEventSlotMeta fires when this
* cross is bracketed */
}OsdiCrossExprMeta;
/* OSDI v0.5 — per-event-slot metadata in OsdiDescriptor.
*
* One entry per `@(cross)` / `@(timer)` / `@(initial_step)` /
* `@(final_step)` body. `kind` is the OSDI_EVENT_KIND_*
* discriminant. `name` is the body's `:label` (e.g.
* `@(timer(...) : sample)`), or NULL when the body wasn't labelled.
*/
typedef struct OsdiEventSlotMeta {
uint32_t kind; /* OSDI_EVENT_KIND_* */
char *name; /* user label, or NULL */
}OsdiEventSlotMeta;
typedef struct OsdiSimInfo { typedef struct OsdiSimInfo {
OsdiSimParas paras; OsdiSimParas paras;
double abstime; double abstime;
@ -95,6 +177,56 @@ typedef struct OsdiSimInfo {
double *prev_state; double *prev_state;
double *next_state; double *next_state;
uint32_t flags; uint32_t flags;
/* OSDI v0.5 — see OSDI_0_5_DESIGN.md §2.3.
* Allocated per-instance by setup_instance (sized from the
* descriptor's num_cross_exprs / num_event_slots). Zero-init
* is safe and represents "no event-driven state for this
* instance". Models that don't use cross/timer features see
* NULL pointers and zero discriminants; they never read these
* fields because the descriptor advertises zero counts.
*/
double *cross_expr;
OsdiEventRequest *pending_events;
uint32_t at_scheduled_event; /* bool, widened to u32 for ABI
* stability across compilers */
uint32_t fired_event_id;
/* S3c — when at_scheduled_event != 0, this carries the true
* scheduled t_event the simulator originally computed (e.g.
* the linear-interpolated cross-expr crossing time). The
* simulator can't always land EXACTLY on t_event, so it fires
* the event on the first eval at or past t_event and reports
* the sample-exact value here. The compiler-side
* last_crossing lowering reads THIS field instead of abstime
* to latch the correct crossing time. */
double scheduled_event_time;
/* OSDI v0.5 — set to 1 by the simulator on the eval(s) of the final
* transient timepoint (CKTtime == CKTfinalTime), 0 otherwise. Lets a
* model gate an `@(final_step)` body. Appended at the struct tail so
* older .osdi binaries (which never read it) stay ABI-compatible. */
uint32_t at_final_step;
/* OSDI v0.5 (axis 2) — the current Newton iteration index (1-based,
* per-Newton-solve; resets each NIiter call), forwarded from
* ckt->CKTosdiNewtonIter. Lets a model / compiler-synthesised $limit
* make its damping iteration-aware (tighten as the count climbs to
* break oscillation). Appended at the struct tail so older .osdi
* binaries (which never read it) stay ABI-compatible. */
uint32_t newton_iter;
/* OSDI v0.5 (2C — absdelay exact transport). Tail-appended, ABI-safe.
* delay_input : per-site scratch (sized num_delay_sites); the model
* writes its current input x there each eval. The
* simulator pushes (abstime, delay_input[site]) into
* the per-instance ring at each ACCEPTED step.
* delay_state : simulator-owned per-instance ring array (one ring
* per site), opaque to the model.
* delay_read : simulator's interpolating reader the model calls
* mid-eval: delay_read(info, site, td, max_td)
* returns x(abstime - td) by linear interpolation over
* the ring (max_td < 0 means "unbounded" no prune). */
double *delay_input;
double *delay_maxtd;
void *delay_state;
double (*delay_read)(const struct OsdiSimInfo *info, uint32_t site,
double td, double max_td);
}OsdiSimInfo; }OsdiSimInfo;
typedef union OsdiInitErrorPayload { typedef union OsdiInitErrorPayload {
@ -202,6 +334,68 @@ typedef struct OsdiDescriptor {
void (*load_jacobian_resist)(void *inst, void* model); void (*load_jacobian_resist)(void *inst, void* model);
void (*load_jacobian_react)(void *inst, void* model, double alpha); void (*load_jacobian_react)(void *inst, void* model, double alpha);
void (*load_jacobian_tran)(void *inst, void* model, double alpha); void (*load_jacobian_tran)(void *inst, void* model, double alpha);
/* OSDI v0.4 fields — present in the descriptor produced by
* OpenVA / openvaf-reloaded for some time but not previously
* declared in this header (ngspice didn't need them). Added
* now because the OSDI v0.5 fields below sit after them and
* the C struct must mirror the binary's layout to compute
* the right offsets. */
uint32_t (*given_flag_model)(void *model, uint32_t id);
uint32_t (*given_flag_instance)(void *inst, uint32_t id);
uint32_t num_resistive_jacobian_entries;
uint32_t num_reactive_jacobian_entries;
void (*write_jacobian_array_resist)(void *inst, void* model, double *destination);
void (*write_jacobian_array_react)(void *inst, void* model, double *destination);
uint32_t num_inputs;
OsdiNodePair *inputs;
void (*load_jacobian_with_offset_resist)(void *inst, void* model, size_t offset);
void (*load_jacobian_with_offset_react)(void *inst, void* model, size_t offset);
/* OSDI v0.5 — event-driven analog operators (§2.4 of the design
* doc). S3a (this commit) declares the field layout; S3b uses
* `num_cross_exprs` / `num_event_slots` to size the per-instance
* SimInfo arrays and walks the metadata arrays to drive the
* bisection state machine. Models that don't use cross/timer
* features emit zero for the counts and NULL for the metadata
* pointers, so older non-0.5 binaries (where these fields don't
* exist) read as zero via the OSDI_DESCRIPTOR_SIZE bounds-check
* the loader already enforces. */
uint32_t num_cross_exprs;
OsdiCrossExprMeta *cross_expr_metadata;
uint32_t num_event_slots;
OsdiEventSlotMeta *event_slot_metadata;
/* OSDI v0.5 (2C — absdelay exact transport). Number of per-instance
* transport-delay "sites" (one per absdelay call site in the model).
* The simulator sizes and manages a (time, value) ring buffer per site
* per instance; the model records its input each eval and reads delayed
* values via SimInfo.delay_read. Zero for models with no absdelay.
* Tail-appended ABI-safe via the OSDI_DESCRIPTOR_SIZE bounds check. */
uint32_t num_delay_sites;
/* OSDI v0.5 (2C AC delay — 1B). The delay-coupling jacobian category:
* absdelay's AC contribution. num_delay_jacobian_entries jacobian entries
* carry a delay part (flagged JACOBIAN_ENTRY_DELAY). write_jacobian_array
* _delay(inst, model, dst) writes their raw values (ddx(-input)) into dst[
* 0..num_delay_jacobian_entries] in jacobian order. delay_jacobian_sites[i]
* gives the absdelay site for the i-th delay entry, so the simulator uses
* delay_td[site] for that entry's e^{-jw*td} = cos(w*td) - j*sin(w*td),
* stamping value*cos into the real matrix part and value*(-sin) into imag.
* Tail-appended ABI-safe. Zero/NULL for models without absdelay. */
uint32_t num_delay_jacobian_entries;
void (*write_jacobian_array_delay)(void *inst, void *model, double *dst);
uint32_t *delay_jacobian_sites;
/* OSDI 0.5 — per-instance persistent state (transition()/slew()/event
* toolkit). Byte offset and slot count of the persistent_state f64 array
* inside the instance struct. The simulator snapshots this array and DEFERS
* its commit to accepted steps only, so predictor / Newton iterates can't
* corrupt the previous-accepted value the model reads back. Both zero for
* models with no persistent state. Tail-appended ABI-safe via the
* OSDI_DESCRIPTOR_SIZE bounds check. */
uint32_t persistent_state_offset;
uint32_t persistent_state_count;
}OsdiDescriptor; }OsdiDescriptor;

608
src/osdi/osdi_defer.c Normal file
View File

@ -0,0 +1,608 @@
/*
* OSDI deferred-evaluation side table implementation.
* See osdi_defer.h for the design rationale.
*/
#include "ngspice/osdi_defer.h"
#include <string.h>
#include <stdlib.h>
/* The numparam directory's public header for the eval-with-scope
* helper and the dico_t accessor. Path is relative to src/include's
* search root + the per-component layout. */
#include "../frontend/numparam/numparam.h"
/* Single global list head. ngspice runs a single simulator process per
* netlist, so a global is sufficient and matches existing patterns
* (e.g. modtabhash). */
static OsdiDeferEntry *g_defer_head = NULL;
static char *xstrdup(const char *s) {
size_t n = strlen(s) + 1;
char *r = (char *)malloc(n);
if (!r) return NULL;
memcpy(r, s, n);
return r;
}
/* Forward decl: scope-snapshot helper, defined further down (needs the
* identifier-scanning helpers from the line-preprocessor section). */
static void capture_scope_snapshot(OsdiDeferEntry *e);
int osdi_defer_register(const char *model_name,
const char *param_name,
const char *expr_text)
{
if (!model_name || !param_name || !expr_text) return 1;
OsdiDeferEntry *e = (OsdiDeferEntry *)malloc(sizeof *e);
if (!e) return 1;
e->model_name = xstrdup(model_name);
e->param_name = xstrdup(param_name);
e->expr_text = xstrdup(expr_text);
e->n_snap = 0;
e->snap_names = NULL;
e->snap_values = NULL;
if (!e->model_name || !e->param_name || !e->expr_text) {
free(e->model_name);
free(e->param_name);
free(e->expr_text);
free(e);
return 1;
}
/* Capture the subckt-instance scope NOW. By register time the
* surrounding subckt has been expanded per-instance, so the dico
* resolves identifiers like `xl_nfet` to the value bound by THIS
* subckt instance's params list. */
capture_scope_snapshot(e);
e->next = g_defer_head;
g_defer_head = e;
return 0;
}
/* Match a runtime model name (possibly subckt-prefixed and bin-suffixed)
* against a deferred-eval entry's registered name. Registration captures
* the ORIGINAL model name as it appeared on the .model card (e.g.
* `pfet` or `egnfet_s2`). After subckt expansion + W/L binning, the
* runtime sees names like `x2.xmp1:pfet.0` for the SAME logical model.
* Strip those decorations before comparison.
*
* Algorithm:
* 1. Drop any trailing `.[0-9]+` (bin suffix).
* 2. Drop any leading `<subckt-path>:` (subckt name prefix), defined
* as everything up to and including the LAST `:` in the name.
* 3. Case-insensitive strcmp against the registered name. */
static bool name_matches(const char *registered, const char *runtime) {
if (!registered || !runtime) return false;
/* Strip everything up to and including the LAST `:` from the
* runtime name. Registration captures the model name as it
* appeared on the .model card (e.g. `pfet.0`). Subckt expansion
* prefixes the model name with the subckt path: `x2.xmp1:pfet.0`.
* The bin suffix `.N` is part of BOTH names (the .model card had
* `.0` already), so we don't touch it. */
const char *base = runtime;
const char *last_colon = strrchr(runtime, ':');
if (last_colon)
base = last_colon + 1;
return strcasecmp(registered, base) == 0;
}
bool osdi_defer_has(const char *model_name) {
if (!model_name) return false;
for (OsdiDeferEntry *e = g_defer_head; e; e = e->next)
if (name_matches(e->model_name, model_name))
return true;
return false;
}
int osdi_defer_for_model(const char *model_name,
void (*cb)(const char *, const char *, void *),
void *cb_arg)
{
if (!model_name || !cb) return 0;
int n = 0;
for (OsdiDeferEntry *e = g_defer_head; e; e = e->next) {
if (name_matches(e->model_name, model_name)) {
cb(e->param_name, e->expr_text, cb_arg);
n++;
}
}
return n;
}
/* Bin-range side table: separate list from the deferred-expression
* entries because populated by a different caller (INPgetModBin in
* the parser) at a different time (after subckt expansion + binning,
* but before OSDIsetup). Keyed by the runtime model name (e.g.
* `pfet.0`); OSDIsetup strips the subckt prefix before looking up. */
typedef struct OsdiBinRange {
char *model_name;
double lmin, lmax;
struct OsdiBinRange *next;
} OsdiBinRange;
static OsdiBinRange *g_bin_head = NULL;
void osdi_defer_record_bin_range(const char *model_name,
double lmin, double lmax) {
if (!model_name) return;
/* Replace if already present (idempotent for repeated calls). */
for (OsdiBinRange *b = g_bin_head; b; b = b->next) {
if (strcasecmp(b->model_name, model_name) == 0) {
b->lmin = lmin;
b->lmax = lmax;
return;
}
}
OsdiBinRange *b = (OsdiBinRange *)malloc(sizeof *b);
if (!b) return;
b->model_name = xstrdup(model_name);
b->lmin = lmin;
b->lmax = lmax;
b->next = g_bin_head;
g_bin_head = b;
}
bool osdi_defer_get_bin_range(const char *model_name,
double *lmin, double *lmax) {
if (!model_name) return false;
/* Same subckt-prefix stripping as name_matches — applied to BOTH
* sides because INPgetModBin records `INPmodName` which carries
* the subckt path prefix (e.g. `x3.xmn1:nfet.0`), and the runtime
* lookup also passes a prefixed name. Symmetric stripping ensures
* lookups find the bin range regardless of which subckt-instance
* recorded it. */
const char *q_base = model_name;
const char *q_colon = strrchr(model_name, ':');
if (q_colon) q_base = q_colon + 1;
for (OsdiBinRange *b = g_bin_head; b; b = b->next) {
const char *s_base = b->model_name;
const char *s_colon = strrchr(b->model_name, ':');
if (s_colon) s_base = s_colon + 1;
if (strcasecmp(s_base, q_base) == 0) {
if (lmin) *lmin = b->lmin;
if (lmax) *lmax = b->lmax;
return true;
}
}
return false;
}
void osdi_defer_clear(void) {
OsdiDeferEntry *e = g_defer_head;
while (e) {
OsdiDeferEntry *next = e->next;
free(e->model_name);
free(e->param_name);
free(e->expr_text);
for (int i = 0; i < e->n_snap; i++) free(e->snap_names[i]);
free(e->snap_names);
free(e->snap_values);
free(e);
e = next;
}
g_defer_head = NULL;
}
/* ============================================================
* Line preprocessor: detect + rewrite deferred-eval expressions
* on `.model` lines targeting OSDI device types.
* ============================================================ */
#include <ctype.h>
/* Skip leading whitespace. */
static const char *skip_ws(const char *p) {
while (*p && isspace((unsigned char)*p)) p++;
return p;
}
/* Identifier characters per HSPICE/Verilog-A rules. */
static int is_ident_start(int c) {
return isalpha(c) || c == '_';
}
static int is_ident_cont(int c) {
return isalnum(c) || c == '_';
}
/* Returns true if `expr` (a brace-or-quote body, no enclosing chars)
* mentions any of the per-instance reserved identifiers as a bare
* word case-insensitively, not as a substring of a larger
* identifier and not after a `.` (subckt-path qualifier).
*
* Reserved set:
* l, w, nf, m, xnf standard HSPICE instance params
* l_calc Samsung/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[] = {
"l", "w", "nf", "m", "xnf", "l_calc", NULL
};
for (const char *p = expr; *p; ) {
if (is_ident_start((unsigned char)*p)) {
const char *start = p;
while (is_ident_cont((unsigned char)*p)) p++;
size_t n = (size_t)(p - start);
/* skip if preceded by '.' (subckt-path) */
if (start > expr && start[-1] == '.') continue;
for (int i = 0; RES[i]; i++) {
size_t rn = strlen(RES[i]);
if (n == rn && strncasecmp(start, RES[i], rn) == 0)
return true;
}
} else {
p++;
}
}
return false;
}
/* Look for `level=<digits>` in the first part of a `.model` line and
* return the integer. Returns -1 if absent or unparseable. */
static int parse_level(const char *line) {
/* find "level" then '=' then digits */
for (const char *p = line; *p; p++) {
if ((p == line || !is_ident_cont((unsigned char)p[-1])) &&
(p[0] == 'l' || p[0] == 'L') &&
(p[1] == 'e' || p[1] == 'E') &&
(p[2] == 'v' || p[2] == 'V') &&
(p[3] == 'e' || p[3] == 'E') &&
(p[4] == 'l' || p[4] == 'L') &&
!is_ident_cont((unsigned char)p[5])) {
p += 5;
p = skip_ws(p);
if (*p != '=') continue;
p++;
p = skip_ws(p);
/* possible single-quote wrap */
if (*p == '\'' || *p == '{') p++;
if (isdigit((unsigned char)*p))
return atoi(p);
}
}
return -1;
}
/* Parse the model name from a `.model` line: the second whitespace-
* separated token. Returns a freshly-allocated string the caller must
* free, or NULL on parse failure. */
static char *parse_model_name(const char *line) {
const char *p = skip_ws(line);
if (strncasecmp(p, ".model", 6) != 0) return NULL;
p += 6;
p = skip_ws(p);
const char *start = p;
while (*p && !isspace((unsigned char)*p) && *p != '(' && *p != '=')
p++;
if (p == start) return NULL;
size_t n = (size_t)(p - start);
char *r = (char *)malloc(n + 1);
if (!r) return NULL;
memcpy(r, start, n);
r[n] = '\0';
return r;
}
/* Given that we've just seen `param_name` followed by `=` on a model
* line, scan back from `eq_pos` to find the parameter name's start.
* Returns the index in `line` of the first char of the name, or
* (size_t)-1 if not found. */
static size_t find_param_name_start(const char *line, size_t eq_pos) {
if (eq_pos == 0) return (size_t)-1;
size_t i = eq_pos;
/* skip whitespace before '=' */
while (i > 0 && isspace((unsigned char)line[i - 1])) i--;
size_t end = i;
/* walk back over identifier chars */
while (i > 0 && is_ident_cont((unsigned char)line[i - 1])) i--;
if (i == end) return (size_t)-1;
return i;
}
int osdi_defer_preprocess_line(char **line_p) {
if (!line_p || !*line_p) return 0;
const char *line = *line_p;
/* Quick reject: not a .model line. */
const char *p = skip_ws(line);
if (strncasecmp(p, ".model", 6) != 0) return 0;
/* Only OSDI-targeted levels (72 BSIM-CMG, 77 BSIM-BULK) participate.
* Adjust here if new OSDI MOS levels are added to inpdomod.c. */
int level = parse_level(line);
if (level != 72 && level != 77) return 0;
char *model_name = parse_model_name(line);
if (!model_name) return 0;
/* Walk the line copying chars; when we hit `<param>={...}` or
* `<param>='...'` whose body references instance geom, register and
* rewrite to `<param>=0`. Build the output in a new buffer. */
size_t cap = strlen(line) + 64;
char *out = (char *)malloc(cap);
if (!out) { free(model_name); return 1; }
size_t olen = 0;
#define EMIT_CH(ch) do { \
if (olen + 1 >= cap) { \
cap *= 2; \
char *_t = (char *)realloc(out, cap); \
if (!_t) { free(out); free(model_name); return 1; } \
out = _t; \
} \
out[olen++] = (ch); \
} while (0)
const char *cur = line;
while (*cur) {
if (*cur != '{' && *cur != '\'') {
EMIT_CH(*cur);
cur++;
continue;
}
char open = *cur;
char close = (open == '{') ? '}' : '\'';
/* Locate matching close (brace-balanced for '{', literal for '\''). */
const char *body_start = cur + 1;
const char *q = body_start;
int nest = 1;
while (*q) {
if (open == '{') {
if (*q == '{') nest++;
else if (*q == '}') { nest--; if (nest == 0) break; }
} else {
if (*q == '\'') { nest = 0; break; }
}
q++;
}
if (*q == '\0') {
/* unterminated — give up, emit verbatim and continue */
EMIT_CH(*cur);
cur++;
continue;
}
/* `body` = [body_start, q); doesn't include the closing delim. */
size_t body_len = (size_t)(q - body_start);
char *body = (char *)malloc(body_len + 1);
if (!body) { free(out); free(model_name); return 1; }
memcpy(body, body_start, body_len);
body[body_len] = '\0';
bool defer = expr_refs_instance_geom(body);
if (defer) {
/* Find the param name immediately preceding the `=` that
* precedes this expression. olen is the current write
* position in out; the just-emitted run ends with the `=`. */
size_t eq_pos = olen;
while (eq_pos > 0 && out[eq_pos - 1] != '=') eq_pos--;
if (eq_pos == 0) {
/* no `=` found — not an `<param>=<expr>` shape; bail */
defer = false;
} else {
size_t name_start = find_param_name_start(out, eq_pos - 1);
if (name_start == (size_t)-1) {
defer = false;
} else {
/* Slice param name out of `out` for registration. */
size_t name_len = (eq_pos - 1) - name_start;
/* trim trailing ws in name */
while (name_len > 0 && isspace((unsigned char)out[name_start + name_len - 1]))
name_len--;
if (name_len == 0) {
defer = false;
} else {
char *pname = (char *)malloc(name_len + 1);
if (!pname) { free(body); free(out); free(model_name); return 1; }
memcpy(pname, out + name_start, name_len);
pname[name_len] = '\0';
osdi_defer_register(model_name, pname, body);
free(pname);
}
}
}
}
if (defer) {
/* Replace `{ body }` / `' body '` with `{0}` / `'0'` in the
* output. Keep the delimiters so numparam still recognises
* the slot as a brace expression and substitutes the
* computed value (0) into the corresponding MARKER
* placeholder in the rewritten card->line. The model
* parameter ends up = 0 as a placeholder; per-instance
* deferred-eval in OSDItemp installs the real value. */
EMIT_CH(open);
EMIT_CH('0');
EMIT_CH(close);
} else {
/* Not deferred; emit the original expression verbatim so
* numparam can run it. */
EMIT_CH(open);
for (size_t k = 0; k < body_len; k++) EMIT_CH(body[k]);
EMIT_CH(close);
}
free(body);
cur = q + 1;
}
EMIT_CH('\0');
/* roll back the implicit terminator so olen excludes it */
olen--;
/* Replace the input buffer. */
free(*line_p);
*line_p = out;
free(model_name);
return 0;
#undef EMIT_CH
}
/* ============================================================
* Scope-snapshot capture (called from osdi_defer_register)
* ============================================================ */
/* Names the eval path always pushes explicitly; never snapshot them
* from the dico (would either duplicate or shadow the per-instance
* value with a parse-time value). */
static bool is_eval_reserved(const char *name, size_t n) {
static const char *const RES[] = {
"l", "w", "nf", "m", "xnf", "l_calc", NULL
};
for (int i = 0; RES[i]; i++) {
size_t rn = strlen(RES[i]);
if (n == rn && strncasecmp(name, RES[i], rn) == 0) return true;
}
return false;
}
/* Well-known math/keyword identifiers — saves a dico lookup for the
* hottest non-symbols. Lookup failure is harmless, so this list need
* not be exhaustive. */
static bool is_known_keyword(const char *name, size_t n) {
static const char *const KW[] = {
"exp", "log", "ln", "log10", "sqrt", "sin", "cos", "tan",
"asin","acos","atan","atan2", "sinh", "cosh","tanh",
"abs", "min", "max", "pow", "int", "floor","ceil",
"if", "else","then","sgn", "fabs", "hypot",
NULL
};
for (int i = 0; KW[i]; i++) {
size_t kn = strlen(KW[i]);
if (n == kn && strncasecmp(name, KW[i], kn) == 0) return true;
}
return false;
}
static void capture_scope_snapshot(OsdiDeferEntry *e) {
if (!e || !e->expr_text) return;
dico_t *dico = nupa_get_dico();
if (!dico) return;
int cap = 0;
/* Walk identifiers in expr_text, applying the same lexical rules
* as expr_refs_instance_geom: idents preceded by `.` are
* subckt-path qualifiers, not free variables. For each unique
* non-reserved ident, look up the current dico (which walks the
* full scope stack) and snapshot its scalar value if it's
* NUPA_REAL. Anything that doesn't resolve is left for the
* eval-time path to flag its absence is either a true missing
* param OR a math keyword we didn't filter. */
for (const char *p = e->expr_text; *p; ) {
if (!is_ident_start((unsigned char)*p)) { p++; continue; }
const char *start = p;
while (is_ident_cont((unsigned char)*p)) p++;
size_t n = (size_t)(p - start);
/* Skip subckt-path-qualified identifiers (`foo.bar` — the
* `bar` half is qualified). */
if (start > e->expr_text && start[-1] == '.') continue;
if (is_eval_reserved(start, n)) continue;
if (is_known_keyword(start, n)) continue;
if (n >= 128) continue; /* implausibly long, skip */
/* Dedupe against snapshot built so far. */
bool dup = false;
for (int i = 0; i < e->n_snap; i++) {
if (strncasecmp(e->snap_names[i], start, n) == 0 &&
e->snap_names[i][n] == '\0') { dup = true; break; }
}
if (dup) continue;
char buf[128];
memcpy(buf, start, n);
buf[n] = '\0';
entry_t *de = entrynb(dico, buf);
if (!de || de->tp != NUPA_REAL) continue;
/* Grow arrays as needed. */
if (e->n_snap == cap) {
int ncap = cap ? cap * 2 : 8;
char **nn = (char **) realloc(e->snap_names, ncap * sizeof *nn);
double *nv = (double *)realloc(e->snap_values, ncap * sizeof *nv);
if (!nn || !nv) {
/* allocation failed — keep what we have, abandon the rest */
if (nn) e->snap_names = nn;
if (nv) e->snap_values = nv;
return;
}
e->snap_names = nn;
e->snap_values = nv;
cap = ncap;
}
e->snap_names[e->n_snap] = xstrdup(buf);
e->snap_values[e->n_snap] = de->vl;
if (!e->snap_names[e->n_snap]) return;
e->n_snap++;
}
}
/* ============================================================
* Per-instance evaluation: thin wrapper over nupa_eval_with_scope
* ============================================================ */
int osdi_defer_eval(const char *expr_text,
double l, double w, double nf, double m,
double *out)
{
if (!expr_text || !out) return 1;
dico_t *dico = nupa_get_dico();
if (!dico) return 1;
/* Per-instance symbols in scope:
* l, w, nf, m standard HSPICE instance params
* xnf alias of nf (HSPICE convention)
* l_calc foundry "effective" length, defaults to l (the
* foundry subckt would compute `l + p_la` but
* p_la is 0 unless set per-instance). */
static const char *base_names[] = { "l", "w", "nf", "m", "xnf", "l_calc" };
double base_values[6] = { l, w, nf, m, nf, l };
const int n_base = 6;
/* Locate the entry that owns this expr_text — we passed the
* pointer to the caller via osdi_defer_for_model's cb, so
* pointer-equality is the canonical lookup. Falls back to no-snap
* if not found (defensive shouldn't happen in practice). */
const OsdiDeferEntry *entry = NULL;
for (OsdiDeferEntry *it = g_defer_head; it; it = it->next) {
if (it->expr_text == expr_text) { entry = it; break; }
}
if (!entry || entry->n_snap == 0) {
return nupa_eval_with_scope(dico, expr_text,
base_names, base_values, n_base, out);
}
/* Merge snap + base. Insertion order matters: nupa_eval_with_scope
* inserts into one hash with op='N', so the LAST insert for a
* given name wins. Put snap FIRST (lower priority), base LAST
* (higher priority instance geom overrides any snap that
* happened to capture the same name from a parent scope). */
int n_total = entry->n_snap + n_base;
const char **names = (const char **)malloc(n_total * sizeof *names);
double *values = (double *) malloc(n_total * sizeof *values);
if (!names || !values) { free(names); free(values); return 1; }
for (int i = 0; i < entry->n_snap; i++) {
names[i] = entry->snap_names[i];
values[i] = entry->snap_values[i];
}
for (int i = 0; i < n_base; i++) {
names[entry->n_snap + i] = base_names[i];
values[entry->n_snap + i] = base_values[i];
}
int rc = nupa_eval_with_scope(dico, expr_text, names, values, n_total, out);
free(names);
free(values);
return rc;
}

View File

@ -17,6 +17,7 @@
#include "osdi.h" #include "osdi.h"
#include "osdidefs.h" #include "osdidefs.h"
#include <math.h>
#include <stdint.h> #include <stdint.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
@ -38,6 +39,40 @@ int OSDIacLoad(GENmodel *inModel, CKTcircuit *ckt) {
// operating point iterations // operating point iterations
descr->load_jacobian_resist(inst, model); descr->load_jacobian_resist(inst, model);
descr->load_jacobian_react(inst, model, ckt->CKTomega); descr->load_jacobian_react(inst, model, ckt->CKTomega);
/* OSDI v0.5 (2C AC delay — 1B). absdelay's exact AC phase: each delay
* jacobian entry's value V (= ddx(-input), computed at the OP) is stamped
* as V * e^{-jw*td(site)} into the complex matrix -- V*cos(w*td) into the
* real part and V*(-sin(w*td)) into the imag part. This is the
* transcendental delay term that G+jwC cannot represent; the model's
* autodiff supplies V (real), the simulator supplies the frequency
* factor. Guarded by the ABI-size-checked entry->num_delay_sites so a
* pre-2C-AC .osdi never reaches the new descriptor tail fields. */
if (entry->num_delay_sites > 0 && descr->num_delay_jacobian_entries > 0) {
OsdiExtraInstData *extra = osdi_extra_instance_data(entry, gen_inst);
if (extra && extra->delay_td_arr) {
double **jac_resist =
(double **)(((char *)inst) + descr->jacobian_ptr_resist_offset);
uint32_t ndel = descr->num_delay_jacobian_entries;
double *vals = TMALLOC(double, ndel);
descr->write_jacobian_array_delay(inst, model, vals);
double w = ckt->CKTomega;
uint32_t di = 0;
for (uint32_t i = 0; i < descr->num_jacobian_entries; i++) {
if (descr->jacobian_entries[i].flags & JACOBIAN_ENTRY_DELAY) {
uint32_t site = descr->delay_jacobian_sites[di];
double td = extra->delay_td_arr[site];
double c = cos(w * td);
double s = sin(w * td);
double *elt = jac_resist[i]; /* real part; elt[1] is imag */
elt[0] += vals[di] * c;
elt[1] += vals[di] * (-s);
di++;
}
}
FREE(vals);
}
}
} }
} }
return (OK); return (OK);

View File

@ -86,3 +86,68 @@ double osdi_limitlog(bool init, bool *check, double vnew, double vold,
*check = icheck != 0; *check = icheck != 0;
return res; return res;
} }
/* OSDI v0.5 (2C) — absdelay exact-transport reader. Bound to
* SimInfo.delay_read; the model calls it mid-eval to obtain
* x(abstime - td) by linear interpolation over the per-instance ring
* (accepted (time,value) samples, oldest..newest). SimInfo.delay_state
* points at the instance's OsdiExtraInstData so the ring is reachable
* without a handle (thread-safe: SimInfo is per-eval). max_td < 0 means
* "unbounded"; otherwise td is clamped to max_td (matching absdelay's
* max_delay clamp). For a target time newer than the last accepted
* sample (td smaller than the current local step) we interpolate toward
* the current eval's input (delay_input_arr[site], just written by the
* model this eval), so small delays are exact rather than held. */
double osdi_delay_read(const OsdiSimInfo *info, uint32_t site, double td,
double max_td) {
OsdiExtraInstData *extra = (OsdiExtraInstData *)info->delay_state;
if (!extra || !extra->delay_t || !extra->delay_v || !extra->delay_count)
return 0.0;
if (td < 0.0)
td = 0.0;
if (max_td >= 0.0 && td > max_td)
td = max_td;
/* OSDI v0.5 (2C AC delay) — stash the (clamped) td so OSDIacLoad can read
* td(OP) for this site's e^{-jw*td} delay jacobian. */
if (extra->delay_td_arr)
extra->delay_td_arr[site] = td;
uint32_t n = extra->delay_count[site];
double *t = extra->delay_t[site];
double *v = extra->delay_v[site];
double t_target = info->abstime - td;
if (n == 0)
return extra->delay_input_arr ? extra->delay_input_arr[site] : 0.0;
/* Older than the oldest accepted sample: hold the oldest value. */
if (t_target <= t[0])
return v[0];
/* Newer than the last accepted sample: interpolate toward the current
* eval's input at abstime. */
if (t_target >= t[n - 1]) {
double t_now = info->abstime;
double v_now = extra->delay_input_arr ? extra->delay_input_arr[site]
: v[n - 1];
double span = t_now - t[n - 1];
if (span <= 0.0)
return v[n - 1];
double frac = (t_target - t[n - 1]) / span;
if (frac > 1.0)
frac = 1.0;
return v[n - 1] + frac * (v_now - v[n - 1]);
}
/* Find the bracket [t[i], t[i+1]] with t[i] <= t_target < t[i+1],
* scanning back from the newest (delays are usually a few steps). */
uint32_t i = n - 1;
while (i > 0 && t[i] > t_target)
i--;
double span = t[i + 1] - t[i];
if (span <= 0.0)
return v[i];
double frac = (t_target - t[i]) / span;
return v[i] + frac * (v[i + 1] - v[i]);
}

View File

@ -65,6 +65,90 @@ typedef struct OsdiExtraInstData {
bool dt_given; bool dt_given;
uint32_t eval_flags; uint32_t eval_flags;
/* OSDI v0.5 — event-driven analog operators (S3b).
*
* Per-instance scratch buffers for the SimInfo.cross_expr and
* .pending_events pointers. Lazily allocated on first eval if
* the descriptor advertises any cross expressions or event slots.
* cross_expr_arr is sized to descr->num_cross_exprs; the model
* writes current values here. prev_cross_expr_arr stores the
* last accepted value (one per slot) so the eval wrapper can
* detect sign flips between consecutive accepted steps.
*
* pending_events_arr is sized to descr->num_event_slots; the
* model writes one OsdiEventRequest per slot when it wants to
* schedule a future event (timer expiration, etc.). The eval
* wrapper drains non-zero entries into the scheduled_events
* queue after each accepted eval and zeros at_time to indicate
* consumed.
*
* scheduled_events is the per-instance pending-event queue,
* sorted by at_time ascending. scheduled_count is the live
* length; capacity is descr->num_event_slots (we never have
* more pending than the model could request in one eval).
*
* cross_init flips true after the first eval has populated
* prev_cross_expr_arr suppresses a spurious "sign flip" at
* t=0 when prev is still zero. */
double *cross_expr_arr;
double *prev_cross_expr_arr;
OsdiEventRequest *pending_events_arr;
OsdiEventRequest *scheduled_events; /* sorted queue */
uint32_t scheduled_count;
bool cross_init;
/* OSDI v0.5 (S3c) — abstime of the last accepted eval that
* latched prev_cross_expr_arr. Used as the lower bound for
* interpolating the crossing time when the next eval detects a
* sign flip. Default -1.0 (no prior eval); the first latched
* eval sets it to the eval's abstime. */
double prev_eval_time;
/* OSDI v0.5 — the SECOND prior accepted (time, cross_expr) sample,
* kept so the crossing time can be QUADRATICALLY interpolated from
* three points (t_prev2, t_prev, t_now) -> O(dt^3) error, instead of
* the 2-point linear fit -> O(dt^2). The cross_expr VALUE already
* encodes both the cross-expr nonlinearity and the node-trajectory
* curvature, so fitting a higher-order polynomial to it (no model
* re-eval) sharpens the crossing time directly. Matters for fast
* carriers (PWM / RF, 100 MHz+) where the crossing TIME itself is the
* measured quantity and a coarse local step would otherwise leave a
* O(dt^2) ~hundreds-of-ps error. Default -1.0 / NULL until two
* accepted evals have latched; the detector falls back to the linear
* fit until then. */
double *prev2_cross_expr_arr;
double prev2_eval_time;
/* OSDI v0.5 (2C) — absdelay exact-transport delay rings. Lazily
* allocated on first eval when descr->num_delay_sites > 0.
* delay_input_arr[site] : current input written by the model each eval
* (exposed as SimInfo.delay_input); pushed into the ring at accept.
* delay_t[site] / delay_v[site] : grow-on-demand (time, value) ring for
* each site, delay_count[site] live samples in oldest..newest order,
* delay_cap[site] allocated capacity. Pushed at each ACCEPTED step,
* pruned of samples older than abstime - max_td. SimInfo.delay_state
* points back at THIS struct so SimInfo.delay_read can reach them. */
double *delay_input_arr;
double *delay_maxtd_arr;
/* OSDI v0.5 (2C AC delay) — last td the model passed to delay_read per site,
* stashed by osdi_delay_read. At the AC operating point this holds td(OP),
* which OSDIacLoad uses for e^{-jw*td} on that site's delay jacobian. */
double *delay_td_arr;
double **delay_t;
double **delay_v;
uint32_t *delay_count;
uint32_t *delay_cap;
/* OSDI 0.5 — DEFERRED-COMMIT snapshot of the model's persistent_state array
* (transition()/slew()/event toolkit). Sized to
* entry->persistent_state_count, lazily allocated on first eval. Holds the
* value as of the last ACCEPTED step. Each eval restores the live
* persistent_state from this snapshot BEFORE running the model, so predictor
* and Newton iterates always read the previous-accepted history and their
* own writes are transient; OSDIaccept copies the converged live array back
* into the snapshot. persist_init flips true once seeded. */
double *persist_snapshot;
bool persist_init;
} ALIGN(MAX_ALIGN) OsdiExtraInstData; } ALIGN(MAX_ALIGN) OsdiExtraInstData;
typedef struct OsdiModelData { typedef struct OsdiModelData {
@ -111,3 +195,8 @@ double osdi_limitlog(bool init, bool *icheck, double vnew, double vold,
double LIM_TOL); double LIM_TOL);
double osdi_fetlim(bool init, bool *icheck, double vnew, double vold, double osdi_fetlim(bool init, bool *icheck, double vnew, double vold,
double vto); double vto);
/* OSDI v0.5 (2C) — absdelay exact-transport reader (bound to
* SimInfo.delay_read). */
double osdi_delay_read(const OsdiSimInfo *info, uint32_t site, double td,
double max_td);

View File

@ -27,6 +27,7 @@ extern int OSDIload(GENmodel *, CKTcircuit *);
extern int OSDItemp(GENmodel *, CKTcircuit *); extern int OSDItemp(GENmodel *, CKTcircuit *);
extern int OSDIacLoad(GENmodel *, CKTcircuit *); extern int OSDIacLoad(GENmodel *, CKTcircuit *);
extern int OSDItrunc(GENmodel *, CKTcircuit *, double *); extern int OSDItrunc(GENmodel *, CKTcircuit *, double *);
extern int OSDIaccept(CKTcircuit *, GENmodel *);
extern int OSDIpzLoad(GENmodel *, CKTcircuit *, SPcomplex *); extern int OSDIpzLoad(GENmodel *, CKTcircuit *, SPcomplex *);
extern int OSDInoise(int, int, GENmodel *, CKTcircuit *, Ndata *, double *); extern int OSDInoise(int, int, GENmodel *, CKTcircuit *, Ndata *, double *);

View File

@ -194,6 +194,7 @@ extern SPICEdev *osdi_create_spicedev(const OsdiRegistryEntry *entry) {
OSDIinfo->DEVtemperature = OSDItemp; OSDIinfo->DEVtemperature = OSDItemp;
OSDIinfo->DEVunsetup = OSDIunsetup; OSDIinfo->DEVunsetup = OSDIunsetup;
OSDIinfo->DEVload = OSDIload; OSDIinfo->DEVload = OSDIload;
OSDIinfo->DEVaccept = OSDIaccept;
OSDIinfo->DEVacLoad = OSDIacLoad; OSDIinfo->DEVacLoad = OSDIacLoad;
OSDIinfo->DEVpzLoad = OSDIpzLoad; OSDIinfo->DEVpzLoad = OSDIpzLoad;
OSDIinfo->DEVtrunc = OSDItrunc; OSDIinfo->DEVtrunc = OSDItrunc;

View File

@ -17,21 +17,25 @@
#include "osdi.h" #include "osdi.h"
#include "osdidefs.h" #include "osdidefs.h"
#include <math.h>
#include <stdint.h> #include <stdint.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <sys/types.h> #include <sys/types.h>
#define NUM_SIM_PARAMS 10 #define NUM_SIM_PARAMS 15
char *sim_params[NUM_SIM_PARAMS + 1] = { char *sim_params[NUM_SIM_PARAMS + 1] = {
"iniLim", "gmin", "gdev", "tnom", "iniLim", "gmin", "gdev", "tnom",
"simulatorVersion", "sourceScaleFactor", "simulatorVersion", "sourceScaleFactor",
"epsmin", "reltol", "vntol", "abstol", "epsmin", "reltol", "vntol", "abstol",
"osdi_vlim",
"osdi_vlim_vds", "osdi_vlim_vgs", "osdi_vlim_vbs", "osdi_vlim_nqs",
NULL}; NULL};
char *sim_params_str[1] = {NULL}; char *sim_params_str[1] = {NULL};
double sim_param_vals[NUM_SIM_PARAMS] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; double sim_param_vals[NUM_SIM_PARAMS] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
/* values returned by $simparam*/ /* values returned by $simparam*/
OsdiSimParas get_simparams(const CKTcircuit *ckt) { OsdiSimParas get_simparams(const CKTcircuit *ckt) {
@ -42,10 +46,25 @@ OsdiSimParas get_simparams(const CKTcircuit *ckt) {
: (ckt->CKTdiagGmin); : (ckt->CKTdiagGmin);
double initializeLimiting = (ckt->CKTmode & MODEINITJCT) ? 1 : 0; double initializeLimiting = (ckt->CKTmode & MODEINITJCT) ? 1 : 0;
/* osdi_vlim*: per-iteration Δv bounds consumed by OpenVA's compiler-side
* $limit synthesis. Each V() probe inside a branch contribution gets
* wrapped with a clamp |V_new - V_prev| osdi_vlim_<shape>, where
* <shape> is the MOSFET-style classification of the probe (vds/vgs/vbs),
* with osdi_vlim as the generic fallback. Cascade: per-shape value if
* user set it > 0, else generic osdi_vlim, else 0.1 V default. All
* voltage-range-agnostic bounds Newton step magnitude, not supply
* rails. */
double osdi_vlim_v = (ckt->CKTosdiVlim > 0.0) ? ckt->CKTosdiVlim : 0.1;
double osdi_vlim_vds_v = (ckt->CKTosdiVlimVds > 0.0) ? ckt->CKTosdiVlimVds : osdi_vlim_v;
double osdi_vlim_vgs_v = (ckt->CKTosdiVlimVgs > 0.0) ? ckt->CKTosdiVlimVgs : osdi_vlim_v;
double osdi_vlim_vbs_v = (ckt->CKTosdiVlimVbs > 0.0) ? ckt->CKTosdiVlimVbs : osdi_vlim_v;
double osdi_vlim_nqs_v = (ckt->CKTosdiVlimNqs > 0.0) ? ckt->CKTosdiVlimNqs : osdi_vlim_v;
double sim_param_vals_[NUM_SIM_PARAMS] = { double sim_param_vals_[NUM_SIM_PARAMS] = {
// Verilog-A tnom is in degrees Celsius // Verilog-A tnom is in degrees Celsius
initializeLimiting, gmin, gdev, ckt->CKTnomTemp-CONSTCtoK, simulatorVersion, sourceScaleFactor, initializeLimiting, gmin, gdev, ckt->CKTnomTemp-CONSTCtoK, simulatorVersion, sourceScaleFactor,
ckt->CKTepsmin, ckt->CKTreltol, ckt->CKTvoltTol, ckt->CKTabstol }; ckt->CKTepsmin, ckt->CKTreltol, ckt->CKTvoltTol, ckt->CKTabstol,
osdi_vlim_v,
osdi_vlim_vds_v, osdi_vlim_vgs_v, osdi_vlim_vbs_v, osdi_vlim_nqs_v };
memcpy(&sim_param_vals, &sim_param_vals_, sizeof(double) * NUM_SIM_PARAMS); memcpy(&sim_param_vals, &sim_param_vals_, sizeof(double) * NUM_SIM_PARAMS);
OsdiSimParas sim_params_ = {.names = sim_params, OsdiSimParas sim_params_ = {.names = sim_params,
.vals = (double *)&sim_param_vals, .vals = (double *)&sim_param_vals,
@ -54,14 +73,374 @@ OsdiSimParas get_simparams(const CKTcircuit *ckt) {
return sim_params_; return sim_params_;
} }
/* OSDI v0.5 (S3b) — per-instance event-state preparation BEFORE
* descr->eval is called. Lazy-allocate cross_expr / pending_events
* buffers (sized from the descriptor), wire them onto the per-eval
* SimInfo, and decide whether this eval is at a previously
* scheduled event time.
*
* `sim_info_local` is a per-eval mutable copy of the caller's
* shared SimInfo the cross/event pointers are per-instance and
* must not share across threads under OMP. */
static void osdi_event_prepare(OsdiExtraInstData *extra,
const OsdiDescriptor *descr,
uint32_t num_delay_sites,
OsdiSimInfo *sim_info_local) {
if (descr->num_cross_exprs > 0 && extra->cross_expr_arr == NULL) {
extra->cross_expr_arr = TMALLOC(double, descr->num_cross_exprs);
extra->prev_cross_expr_arr = TMALLOC(double, descr->num_cross_exprs);
extra->prev2_cross_expr_arr = TMALLOC(double, descr->num_cross_exprs);
for (uint32_t i = 0; i < descr->num_cross_exprs; i++) {
extra->cross_expr_arr[i] = 0.0;
extra->prev_cross_expr_arr[i] = 0.0;
extra->prev2_cross_expr_arr[i] = 0.0;
}
extra->prev_eval_time = -1.0;
extra->prev2_eval_time = -1.0;
}
if (descr->num_event_slots > 0 && extra->pending_events_arr == NULL) {
extra->pending_events_arr =
TMALLOC(OsdiEventRequest, descr->num_event_slots);
extra->scheduled_events =
TMALLOC(OsdiEventRequest, descr->num_event_slots);
for (uint32_t i = 0; i < descr->num_event_slots; i++) {
extra->pending_events_arr[i].at_time = 0.0;
extra->pending_events_arr[i].event_id = 0;
extra->pending_events_arr[i].kind = 0;
extra->scheduled_events[i].at_time = 0.0;
extra->scheduled_events[i].event_id = 0;
extra->scheduled_events[i].kind = 0;
}
extra->scheduled_count = 0;
}
if (num_delay_sites > 0 && extra->delay_input_arr == NULL) {
extra->delay_input_arr = TMALLOC(double, num_delay_sites);
extra->delay_maxtd_arr = TMALLOC(double, num_delay_sites);
extra->delay_td_arr = TMALLOC(double, num_delay_sites);
extra->delay_t = TMALLOC(double *, num_delay_sites);
extra->delay_v = TMALLOC(double *, num_delay_sites);
extra->delay_count = TMALLOC(uint32_t, num_delay_sites);
extra->delay_cap = TMALLOC(uint32_t, num_delay_sites);
for (uint32_t i = 0; i < num_delay_sites; i++) {
extra->delay_input_arr[i] = 0.0;
extra->delay_maxtd_arr[i] = -1.0;
extra->delay_td_arr[i] = 0.0;
extra->delay_t[i] = NULL;
extra->delay_v[i] = NULL;
extra->delay_count[i] = 0;
extra->delay_cap[i] = 0;
}
}
sim_info_local->cross_expr = extra->cross_expr_arr;
sim_info_local->pending_events = extra->pending_events_arr;
sim_info_local->delay_input = extra->delay_input_arr;
sim_info_local->delay_maxtd = extra->delay_maxtd_arr;
sim_info_local->delay_state = (void *)extra;
sim_info_local->delay_read = osdi_delay_read;
/* Is this eval at or PAST the front-of-queue scheduled event
* time? S3c plan-B firing semantics: cross events are scheduled
* at a linear-interpolated t_event that almost always lands
* BETWEEN two simulator steps there's no way to make the
* stepper land exactly on t_event without back-stepping (which
* dctran's /8 rejection path makes impractical). Instead we
* fire on the first eval at or past t_event and report the
* true t_event to the model via scheduled_event_time so the
* lowering can latch the sample-exact value, not the post-fire
* abstime. */
sim_info_local->at_scheduled_event = 0;
sim_info_local->fired_event_id = 0;
sim_info_local->scheduled_event_time = 0.0;
if (extra->scheduled_count > 0) {
double t_event = extra->scheduled_events[0].at_time;
if (sim_info_local->abstime >= t_event - 1.0e-12) {
sim_info_local->at_scheduled_event = 1;
sim_info_local->fired_event_id = extra->scheduled_events[0].event_id;
sim_info_local->scheduled_event_time = t_event;
/* Pop the consumed event. */
for (uint32_t i = 1; i < extra->scheduled_count; i++) {
extra->scheduled_events[i - 1] = extra->scheduled_events[i];
}
extra->scheduled_count--;
}
}
}
/* Crossing time of a cross_expr within the sign-flip bracket [t1, t2]
* (v1, v2 have opposite signs). When a second prior sample (t0, v0) is
* available, fit a quadratic through the three (time, value) points and
* take the in-bracket root -> O(dt^3) error; otherwise fall back to the
* 2-point linear fit -> O(dt^2). The cross_expr VALUE already folds in
* both the cross-expr nonlinearity and the node-trajectory curvature, so
* a higher-order fit of the value sharpens the crossing time with no
* model re-evaluation. Degenerate quadratics (near-zero leading coeff,
* negative discriminant, no in-bracket root) fall back to linear. */
static double osdi_cross_time(double t0, double v0,
double t1, double v1,
double t2, double v2) {
double span = t2 - t1;
if (t0 >= 0.0 && t1 > t0 && span > 0.0) {
/* Newton divided differences centered on [t1, t2]:
* v(t) = v1 + s*f12 + s*(s + (t1 - t2))*f012, s = t - t1
* = f012*s^2 + (f12 + (t1 - t2)*f012)*s + v1 */
double f12 = (v2 - v1) / span;
double f01 = (v1 - v0) / (t1 - t0);
double f012 = (f12 - f01) / (t2 - t0);
double A = f012;
double B = f12 + (t1 - t2) * f012;
double C = v1;
if (A != 0.0) {
double disc = B * B - 4.0 * A * C;
if (disc >= 0.0) {
double sq = sqrt(disc);
double s1 = (-B + sq) / (2.0 * A);
double s2 = (-B - sq) / (2.0 * A);
bool in1 = (s1 > 0.0 && s1 <= span);
bool in2 = (s2 > 0.0 && s2 <= span);
double s = -1.0;
if (in1 && in2) {
s = (s1 < s2) ? s1 : s2; /* earliest crossing in the bracket */
} else if (in1) {
s = s1;
} else if (in2) {
s = s2;
}
if (s >= 0.0) {
return t1 + s;
}
}
}
/* fall through to linear for a degenerate quadratic */
}
double denom = fabs(v1) + fabs(v2);
return (denom > 0.0) ? t1 + span * (fabs(v1) / denom) : t2;
}
/* OSDI v0.5 — post-eval processing.
*
* S3b: drain model-written pending_events into the sorted
* scheduled_events queue.
* S3c (plan B): detect cross_expr sign flips and schedule the
* cross event at a linear-interpolated crossing time. The
* original idea back-step the simulator and bisect runs
* into ngspice's reject path: dctran cuts CKTdelta by 8 per
* rejection cumulatively and skips CKTtrunc between retries,
* so the bisection dt-clamp never gets to drive the next
* step toward the bracket midpoint. Linear interpolation
* gives O(dt^2) accuracy for smooth signals typically a
* ~1000× improvement over S3b's "post-flip step abstime"
* without touching the transient stepper.
*
* The interpolated t_event almost always falls between two
* simulator steps. Prepare() handles that mismatch by firing the
* event on the first eval at or past t_event and exposing the
* true t_event via sim_info_local->scheduled_event_time. */
static void osdi_event_postprocess(OsdiExtraInstData *extra,
const OsdiDescriptor *descr,
const OsdiSimInfo *sim_info_local) {
/* 1. Drain pending events. */
if (descr->num_event_slots > 0 &&
(extra->eval_flags & EVAL_RET_FLAG_EVENT)) {
for (uint32_t i = 0; i < descr->num_event_slots; i++) {
OsdiEventRequest *req = &extra->pending_events_arr[i];
if (req->at_time > sim_info_local->abstime &&
extra->scheduled_count < descr->num_event_slots) {
/* Insert into scheduled_events keeping sorted order. */
uint32_t j = 0;
while (j < extra->scheduled_count &&
extra->scheduled_events[j].at_time < req->at_time) {
j++;
}
for (uint32_t k = extra->scheduled_count; k > j; k--) {
extra->scheduled_events[k] = extra->scheduled_events[k - 1];
}
extra->scheduled_events[j] = *req;
extra->scheduled_count++;
}
/* Zero the pending slot to indicate consumed. */
req->at_time = 0.0;
req->event_id = 0;
req->kind = 0;
}
}
/* 2. Detect cross sign flips + schedule at linear-interp time. */
if (descr->num_cross_exprs > 0 &&
(extra->eval_flags & EVAL_RET_FLAG_CROSS) &&
extra->cross_init && extra->prev_eval_time >= 0.0) {
double t_now = sim_info_local->abstime;
for (uint32_t i = 0; i < descr->num_cross_exprs; i++) {
const OsdiCrossExprMeta *meta = &descr->cross_expr_metadata[i];
double prev_val = extra->prev_cross_expr_arr[i];
double curr_val = extra->cross_expr_arr[i];
bool flipped = false;
if (meta->direction > 0 && prev_val < 0.0 && curr_val >= 0.0) {
flipped = true;
} else if (meta->direction < 0 &&
prev_val > 0.0 && curr_val <= 0.0) {
flipped = true;
} else if (meta->direction == 0 && prev_val * curr_val < 0.0) {
flipped = true;
}
if (flipped && extra->scheduled_count < descr->num_event_slots) {
/* Crossing time within [t_prev, t_now]. A 3-point quadratic fit
* through the second-prior sample (O(dt^3)) when available, else
* the 2-point linear fit (O(dt^2)). See osdi_cross_time(). */
double t_cross = osdi_cross_time(
extra->prev2_eval_time, extra->prev2_cross_expr_arr[i],
extra->prev_eval_time, prev_val,
t_now, curr_val);
extra->scheduled_events[extra->scheduled_count].at_time = t_cross;
extra->scheduled_events[extra->scheduled_count].event_id =
meta->event_id;
extra->scheduled_events[extra->scheduled_count].kind =
OSDI_EVENT_KIND_CROSS;
extra->scheduled_count++;
}
}
}
/* 3. Latch current cross values + times, shifting the 2-deep history
* (prev2 <- prev <- curr) so the next flip can fit a quadratic. */
if (descr->num_cross_exprs > 0) {
for (uint32_t i = 0; i < descr->num_cross_exprs; i++) {
extra->prev2_cross_expr_arr[i] = extra->prev_cross_expr_arr[i];
extra->prev_cross_expr_arr[i] = extra->cross_expr_arr[i];
}
extra->cross_init = true;
extra->prev2_eval_time = extra->prev_eval_time;
extra->prev_eval_time = sim_info_local->abstime;
}
}
static void eval(const OsdiDescriptor *descr, const GENinstance *gen_inst, static void eval(const OsdiDescriptor *descr, const GENinstance *gen_inst,
void *inst, OsdiExtraInstData *extra_inst_data, void *inst, OsdiExtraInstData *extra_inst_data,
const void *model, const OsdiSimInfo *sim_info) { const void *model, const OsdiSimInfo *sim_info) {
OsdiNgspiceHandle handle = OsdiNgspiceHandle handle =
(OsdiNgspiceHandle){.kind = 3, .name = gen_inst->GENname}; (OsdiNgspiceHandle){.kind = 3, .name = gen_inst->GENname};
/* OSDI v0.5 — per-instance scratch SimInfo with event-state
* pointers wired up. Originals stay shared/read-only across the
* OMP parallel region. */
OsdiSimInfo sim_info_local = *sim_info;
OsdiRegistryEntry *entry = osdi_reg_entry_inst(gen_inst);
osdi_event_prepare(extra_inst_data, descr, entry->num_delay_sites,
&sim_info_local);
/* TODO initial conditions? */ /* TODO initial conditions? */
extra_inst_data->eval_flags = descr->eval(&handle, inst, model, sim_info); extra_inst_data->eval_flags =
descr->eval(&handle, inst, model, &sim_info_local);
osdi_event_postprocess(extra_inst_data, descr, &sim_info_local);
}
static void sanitize_residuals(CKTcircuit *ckt, void *inst,
const OsdiDescriptor *descr, bool is_tran,
const char *name, double t) {
/* NaN/Inf in a residual means the model's eval produced a non-finite
* value always a real anomaly. Zero it out and signal non-
* convergence so Newton retries with a smaller step.
*
* Finite-magnitude clamping was removed: it had no physical basis.
* For body-diode-style "huge" residuals (V/Vt deep enough that
* exp(V/Vt) blows up to 1e83+ A), the model's Jacobian scales with
* the current itself, so Newton's update Δx = -F/J is naturally
* bounded by the model. And in power designs (PMIC, LDMOS arrays,
* battery switches) a single OSDI instance with m × nf in the
* thousands can legitimately stamp hundreds-to-thousands of amps
* clipping those was breaking convergence. HSPICE and Spectre do
* not magnitude-clip residuals either. */
for (uint32_t i = 0; i < descr->num_nodes; i++) {
if (descr->nodes[i].resist_residual_off != UINT32_MAX) {
double *r = (double *)(((char *)inst) + descr->nodes[i].resist_residual_off);
if (!isfinite(*r)) {
*r = 0.0; ckt->CKTnoncon++;
}
}
if (is_tran && descr->nodes[i].react_residual_off != UINT32_MAX) {
double *r = (double *)(((char *)inst) + descr->nodes[i].react_residual_off);
if (!isfinite(*r)) {
*r = 0.0; ckt->CKTnoncon++;
}
}
}
}
static void sanitize_jacobian(CKTcircuit *ckt, void *inst,
const OsdiDescriptor *descr,
const char *name, double t) {
double **jptr =
(double **)(((char *)inst) + descr->jacobian_ptr_resist_offset);
bool is_tran = (ckt->CKTmode & MODETRAN) != 0;
for (uint32_t i = 0; i < descr->num_jacobian_entries; i++) {
bool is_diag = (descr->jacobian_entries[i].nodes.node_1 ==
descr->jacobian_entries[i].nodes.node_2);
/* During transient, load_jacobian_tran folds the reactive Jacobian into
this resistive slot as ag0*dQ/dV, where ag0 ~ 1/dt. At a small step
(e.g. the 1e-14 s initial step) a perfectly physical reactive coupling
can therefore be huge -- a 1 pF cap gives 1e2, a 1 F differentiator
(bare ddt(V)) gives 1e14. That is NOT a bad linearization, so it must
not be hit by the huge-finite cap below (which exists to bound bad
*resistive* linearizations). The underlying dQ/dV is still guarded by
the reactive-pointer cap further down. PDK transcaps (~1e-15 F) never
approach the threshold even at the initial step, so they are unaffected. */
bool has_react = descr->jacobian_entries[i].react_ptr_off != UINT32_MAX;
if (jptr[i] && !isfinite(*jptr[i])) {
if (is_diag) {
/* Use |I_resist|/VDD so N-R step stays bounded to ~VDD instead of
the 2.4MV update that gmin alone would allow. */
double g_replacement = ckt->CKTgmin;
uint32_t node_idx = descr->jacobian_entries[i].nodes.node_1;
if (node_idx < descr->num_nodes &&
descr->nodes[node_idx].resist_residual_off != UINT32_MAX) {
double resid = fabs(*((double *)(((char *)inst) +
descr->nodes[node_idx].resist_residual_off)));
double g_safe = resid / 0.9;
if (g_safe > g_replacement) g_replacement = g_safe;
}
*jptr[i] = g_replacement;
} else {
*jptr[i] = 0.0;
}
ckt->CKTnoncon++;
/* Axis-3: NaN/Inf in the Jacobian means the model's evaluation
diverged at the predicted operating point. No amount of
clipping or iteration can recover a falsified linearization;
signal step rejection so the transient driver retries with
smaller delta. Gated by .option noosdistepreject. */
if (!ckt->CKTosdiStepRejectOff)
ckt->CKTosdiStepReject = 1;
} else if (jptr[i] && fabs(*jptr[i]) > 1.0e6 && !(is_tran && has_react)) {
/* Huge-finite Jacobian: cap so the N-R solve cannot produce absurd
node updates. CKTnoncon is incremented so the iteration loop
knows the iterate hasn't truly settled. CKThugeJThisIter is
incremented so NIiter knows to apply per-node Δv limiting on
this iteration. Skipped for reactive-bearing entries in transient
(see has_react note above) -- their magnitude is the physical
ag0*dQ/dV companion, not a falsified linearization. */
*jptr[i] = is_diag ? ((*jptr[i] > 0) ? 1.0e6 : -1.0e6) : 0.0;
ckt->CKTnoncon++;
ckt->CKThugeJThisIter++;
}
/* Sanitize the per-entry reactive Jacobian pointer (separate sparse-matrix
slot from the resist stamp; may be NaN independently). */
uint32_t rpt_off = descr->jacobian_entries[i].react_ptr_off;
if (rpt_off != UINT32_MAX) {
double *rp = *(double **)(((char *)inst) + rpt_off);
if (rp && !isfinite(*rp)) {
*rp = is_diag ? ckt->CKTgmin : 0.0;
ckt->CKTnoncon++;
if (!ckt->CKTosdiStepRejectOff)
ckt->CKTosdiStepReject = 1;
} else if (rp && fabs(*rp) > 1.0e6) {
*rp = is_diag ? 1.0e6 : 0.0;
ckt->CKTnoncon++;
ckt->CKThugeJThisIter++;
}
}
}
} }
static void load(CKTcircuit *ckt, const GENinstance *gen_inst, void *model, static void load(CKTcircuit *ckt, const GENinstance *gen_inst, void *model,
@ -70,12 +449,21 @@ static void load(CKTcircuit *ckt, const GENinstance *gen_inst, void *model,
NG_IGNORE(extra_inst_data); NG_IGNORE(extra_inst_data);
const char *name = gen_inst->GENname;
double t = ckt->CKTtime;
double dump; double dump;
if (is_tran) { if (is_tran) {
/* zero NaN residuals before they propagate into the RHS */
sanitize_residuals(ckt, inst, descr, true, name, t);
/* load dc matrix and capacitances (charge derivative multiplied with /* load dc matrix and capacitances (charge derivative multiplied with
* CKTag[0]) */ * CKTag[0]) */
descr->load_jacobian_tran(inst, model, ckt->CKTag[0]); descr->load_jacobian_tran(inst, model, ckt->CKTag[0]);
/* zero any NaN entries written into the sparse matrix */
sanitize_jacobian(ckt, inst, descr, name, t);
/* load static rhs and dynamic linearized rhs (SUM Vb * dIa/dVb)*/ /* load static rhs and dynamic linearized rhs (SUM Vb * dIa/dVb)*/
descr->load_spice_rhs_tran(inst, model, ckt->CKTrhs, ckt->CKTrhsOld, descr->load_spice_rhs_tran(inst, model, ckt->CKTrhs, ckt->CKTrhsOld,
ckt->CKTag[0]); ckt->CKTag[0]);
@ -91,6 +479,9 @@ static void load(CKTcircuit *ckt, const GENinstance *gen_inst, void *model,
double residual_react = double residual_react =
*((double *)(((char *)inst) + descr->nodes[i].react_residual_off)); *((double *)(((char *)inst) + descr->nodes[i].react_residual_off));
/* save previous accepted charge for oscillation detection */
double q_old_charge = ckt->CKTstate1[state];
/* store charges in state vector*/ /* store charges in state vector*/
ckt->CKTstate0[state] = residual_react; ckt->CKTstate0[state] = residual_react;
if (is_init_tran) { if (is_init_tran) {
@ -103,7 +494,29 @@ static void load(CKTcircuit *ckt, const GENinstance *gen_inst, void *model,
NIintegrate(ckt, &dump, &dump, 0, state); NIintegrate(ckt, &dump, &dump, 0, state);
/* add the numeric derivative to the rhs */ /* add the numeric derivative to the rhs */
ckt->CKTrhs[node_mapping[i]] -= ckt->CKTstate0[state + 1]; double integrated = ckt->CKTstate0[state + 1];
double stamp;
if (!isfinite(integrated)) {
/* NaN/inf: reset history to prevent propagation */
ckt->CKTstate0[state + 1] = 0.0;
stamp = 0.0;
ckt->CKTnoncon++;
} else if (residual_react == q_old_charge) {
/* Charge is identical to the previous accepted value (zeroth-order
predictor). TRAP gives q_dot_new = q_dot_old, so the sign
flips every timestep an artificial oscillation that reverses
the sign of the RHS contribution and derails the first N-R step.
Zero both the stamp and the stored derivative to kill it. */
ckt->CKTstate0[state + 1] = 0.0;
stamp = 0.0;
} else {
/* Finite q_dot of any magnitude is stamped as-is. No
magnitude clamp see sanitize_residuals comment for why
clipping was actively wrong for power devices. Genuine
overflow surfaces as NaN/Inf and is caught above. */
stamp = integrated;
}
ckt->CKTrhs[node_mapping[i]] -= stamp;
if (is_init_tran) { if (is_init_tran) {
ckt->CKTstate1[state + 1] = ckt->CKTstate0[state + 1]; ckt->CKTstate1[state + 1] = ckt->CKTstate0[state + 1];
@ -113,15 +526,181 @@ static void load(CKTcircuit *ckt, const GENinstance *gen_inst, void *model,
} }
} }
} else { } else {
/* zero NaN residuals before they propagate into the RHS */
sanitize_residuals(ckt, inst, descr, false, name, t);
/* copy internal derivatives into global matrix */ /* copy internal derivatives into global matrix */
descr->load_jacobian_resist(inst, model); descr->load_jacobian_resist(inst, model);
/* zero any NaN entries written into the sparse matrix */
sanitize_jacobian(ckt, inst, descr, name, t);
/* calculate spice RHS from internal currents and store into global RHS /* calculate spice RHS from internal currents and store into global RHS
*/ */
descr->load_spice_rhs_dc(inst, model, ckt->CKTrhs, ckt->CKTrhsOld); descr->load_spice_rhs_dc(inst, model, ckt->CKTrhs, ckt->CKTrhsOld);
/* Seed the reactive (charge) state from the transient operating point.
*
* The OP solve runs with is_tran == 0, so the reactive integration block
* above is skipped and the per-node charge slots CKTstate0[state] are
* never written -- they stay 0. dctran then memcpy's CKTstate0 ->
* CKTstate1 right before MODEINITTRAN, propagating that 0. The first
* transient step therefore sees a charge that jumps from 0 to the real
* OP charge in one step, injecting a phantom dq/dt (an OSDI device that
* holds a nonzero charge at the bias point -- e.g. a DC-biased cap --
* starts the transient as if discharged and relaxes back over its RC).
*
* Native ngspice devices avoid this by seeding CKTstate1[qcap] =
* CKTstate0[qcap] = C*vcap at the operating point (see capload.c). Mirror
* that here: CALC_REACT_RESIDUAL was requested for the transient OP
* (MODETRANOP) above, so the model's react residual now holds the OP
* charge; store it into both state slots so the first step's dq/dt = 0. */
if (ckt->CKTmode & MODETRANOP) {
int state = gen_inst->GENstate + (int)descr->num_states;
for (uint32_t i = 0; i < descr->num_nodes; i++) {
if (descr->nodes[i].react_residual_off != UINT32_MAX) {
double residual_react =
*((double *)(((char *)inst) + descr->nodes[i].react_residual_off));
if (isfinite(residual_react)) {
ckt->CKTstate0[state] = residual_react;
ckt->CKTstate1[state] = residual_react;
}
state += 2;
}
}
}
} }
} }
/* OSDI v0.5 (2C) — push one accepted (time, value) sample into delay ring
* `s`, growing capacity geometrically, then prune leading samples that can
* no longer bracket any delay <= maxtd (keeping >= 2 for interpolation).
* maxtd < 0 means unbounded capped at a defensive ceiling so a deck that
* never supplies max_delay can't grow the ring without bound. */
static void osdi_delay_push(OsdiExtraInstData *extra, uint32_t s, double t,
double v, double maxtd) {
uint32_t n = extra->delay_count[s];
if (n == extra->delay_cap[s]) {
uint32_t newcap = extra->delay_cap[s] ? extra->delay_cap[s] * 2 : 64;
extra->delay_t[s] = TREALLOC(double, extra->delay_t[s], newcap);
extra->delay_v[s] = TREALLOC(double, extra->delay_v[s], newcap);
extra->delay_cap[s] = newcap;
}
extra->delay_t[s][n] = t;
extra->delay_v[s][n] = v;
extra->delay_count[s] = n + 1;
double *tt = extra->delay_t[s];
double *vv = extra->delay_v[s];
uint32_t cnt = extra->delay_count[s];
uint32_t drop = 0;
if (maxtd >= 0.0) {
double horizon = t - maxtd;
/* advance past samples strictly older than the one bracketing the
* horizon; keep that bracketing sample and everything newer. */
while (drop + 2 < cnt && tt[drop + 1] <= horizon) {
drop++;
}
} else {
const uint32_t CEIL = 1u << 22; /* ~4M samples */
if (cnt > CEIL) {
drop = cnt - CEIL;
}
}
if (drop > 0) {
memmove(tt, tt + drop, (cnt - drop) * sizeof(double));
memmove(vv, vv + drop, (cnt - drop) * sizeof(double));
extra->delay_count[s] = cnt - drop;
}
}
/* OSDI 0.5 — DEFERRED-COMMIT persistent state.
*
* The model's persistent_state array (transition()/slew()/event toolkit) is
* read-modify-written in place during eval, so a predictor eval and every
* Newton iterate within a timestep would overwrite the previous-ACCEPTED value
* the model needs to read back -- in a loose-feedback wrapper that corrupts the
* state and the builtin computes the wrong value. Fix: keep a per-instance
* snapshot of the array as of the last accepted step. Restore the live array
* from the snapshot BEFORE every eval (so reads always see the committed value
* and in-step writes are transient); commit the snapshot from the live array
* only at an accepted step (OSDIaccept).
*
* osdi_persist_restore: called before each eval. Lazily allocates the snapshot
* (capturing the initial zero state) on first use, then copies snapshot -> live.
*/
static void osdi_persist_restore(const OsdiRegistryEntry *entry,
GENinstance *gen_inst, void *inst,
OsdiExtraInstData *extra) {
uint32_t n = entry->persistent_state_count;
if (n == 0) {
return;
}
NG_IGNORE(gen_inst);
double *live = (double *)(((char *)inst) + entry->persistent_state_offset);
if (!extra->persist_snapshot) {
/* First eval: capture the (zero-initialised) live array as the committed
* baseline. Do NOT overwrite live, so this eval still sees is_first. */
extra->persist_snapshot = TMALLOC(double, n);
memcpy(extra->persist_snapshot, live, (size_t)n * sizeof(double));
extra->persist_init = true;
} else {
/* Restore the previous-accepted value before the model reads it. */
memcpy(live, extra->persist_snapshot, (size_t)n * sizeof(double));
}
}
/* osdi_persist_commit: called once per ACCEPTED step (OSDIaccept). Copies the
* converged live array into the snapshot so the next step reads it back. */
static void osdi_persist_commit(const OsdiRegistryEntry *entry, void *inst,
OsdiExtraInstData *extra) {
uint32_t n = entry->persistent_state_count;
if (n == 0 || !extra->persist_snapshot) {
return;
}
double *live = (double *)(((char *)inst) + entry->persistent_state_offset);
memcpy(extra->persist_snapshot, live, (size_t)n * sizeof(double));
}
/* OSDI v0.5 (2C) — DEVaccept hook. Fires once per ACCEPTED transient step
* (from CKTaccept); records each instance's current delay input (written by
* the model during the converged eval) into its per-site transport-delay
* ring. Only the accepted trajectory is recorded, which the model itself
* could not do (it can't tell an accepted step from a Newton iterate). */
int OSDIaccept(CKTcircuit *ckt, GENmodel *in_model) {
OsdiRegistryEntry *entry = osdi_reg_entry_model(in_model);
uint32_t num_delay_sites = entry->num_delay_sites;
uint32_t persistent_count = entry->persistent_state_count;
/* Nothing per-instance to do if this model has neither delay rings nor
* deferred-commit persistent state. */
if (num_delay_sites == 0 && persistent_count == 0) {
return OK;
}
double t = ckt->CKTtime;
for (GENmodel *model = in_model; model; model = model->GENnextModel) {
for (GENinstance *inst = model->GENinstances; inst;
inst = inst->GENnextInstance) {
OsdiExtraInstData *extra = osdi_extra_instance_data(entry, inst);
if (!extra) {
continue;
}
/* Commit the converged persistent state (the model's history) for this
* accepted step so the next step reads it back. */
if (persistent_count) {
void *idata = osdi_instance_data(entry, inst);
osdi_persist_commit(entry, idata, extra);
}
if (num_delay_sites && extra->delay_input_arr != NULL) {
for (uint32_t s = 0; s < num_delay_sites; s++) {
osdi_delay_push(extra, s, t, extra->delay_input_arr[s],
extra->delay_maxtd_arr[s]);
}
}
}
}
return OK;
}
extern int OSDIload(GENmodel *inModel, CKTcircuit *ckt) { extern int OSDIload(GENmodel *inModel, CKTcircuit *ckt) {
GENmodel *gen_model; GENmodel *gen_model;
GENinstance *gen_inst; GENinstance *gen_inst;
@ -132,7 +711,20 @@ extern int OSDIload(GENmodel *inModel, CKTcircuit *ckt) {
bool is_tran = ckt->CKTmode & (MODETRAN); bool is_tran = ckt->CKTmode & (MODETRAN);
bool is_tran_op = ckt->CKTmode & (MODETRANOP); bool is_tran_op = ckt->CKTmode & (MODETRANOP);
bool is_init_tran = ckt->CKTmode & MODEINITTRAN; bool is_init_tran = ckt->CKTmode & MODEINITTRAN;
bool is_init_junc = ckt->CKTmode & MODEINITJCT; /* INIT_LIM gating: fire whenever the simulator is in any DC-OP
* initialization mode where PrevState may be stale (== 0). Includes
* the literal first iter (MODEINITJCT), the floating-restart mode used
* by gmin and source stepping when normal Newton fails (MODEINITFLOAT),
* and the fixed-IC mode (MODEINITFIX). All three lead the simulator
* to re-enter CKTload with PrevState still uninitialized, so the
* compiler-side $limit synthesis must bypass its per-iter Δv clamp
* (which would otherwise pin every V() to ±vlim of zero, derailing
* the OP-finding machinery). Transient init modes (MODEINITTRAN /
* MODEINITPRED) are intentionally EXCLUDED by the time transient
* starts the OP has converged and PrevState carries a valid value
* from there; the clamp is desired throughout transient. */
bool is_init_junc = ckt->CKTmode &
(MODEINITJCT | MODEINITFLOAT | MODEINITFIX);
OsdiSimInfo sim_info = { OsdiSimInfo sim_info = {
.paras = get_simparams(ckt), .paras = get_simparams(ckt),
@ -141,8 +733,36 @@ extern int OSDIload(GENmodel *inModel, CKTcircuit *ckt) {
.prev_state = ckt->CKTstates[0], .prev_state = ckt->CKTstates[0],
.next_state = ckt->CKTstates[0], .next_state = ckt->CKTstates[0],
.flags = CALC_RESIST_JACOBIAN, .flags = CALC_RESIST_JACOBIAN,
/* Axis 2 — current Newton iteration index for iteration-aware
* limiting (published by NIiter before this CKTload). */
.newton_iter = (uint32_t)ckt->CKTosdiNewtonIter,
}; };
/* OSDI limiting at the DC->transient boundary.
*
* The compiler-synthesized $limit on V() probes is a Newton step limiter:
* it clamps |V_probe - PrevState| to a knee (~10*kT/q). Throughout
* transient PrevState is read from CKTstates[0] (== next_state), which works
* because each N-R iteration writes its NewState there for the next one to
* read. But at the very first transient step (MODEINITTRAN) CKTstates[0]
* does not hold the operating point: dctran rotates the state pointers
* (cktdefs: CKTstates[i+1] = CKTstates[i]) just before this solve, which
* moves the converged OP limiter state into CKTstates[1] and leaves
* CKTstates[0] pointing at a recycled (stale, == 0) buffer. PrevState would
* therefore be 0, and the limiter would clamp the steady-state OP voltage as
* if stepping up from zero -- e.g. a device biased at 1 V starts the
* transient with its probe log-compressed to ~0.61 V, corrupting the first
* step (a linear cap then "converges" at the clamped value; stiff models
* iterate out of it, which is why this stayed hidden).
*
* Read PrevState from CKTstates[1] (the rotated-away OP state) for this one
* step so the limiter sees delta = V_guess - V_op ~ 0 and stays transparent.
* NewState is still written to CKTstates[0]. Limiting remains fully active
* for every subsequent step. */
if ((ckt->CKTmode & MODEINITTRAN) && ckt->CKTstates[1]) {
sim_info.prev_state = ckt->CKTstates[1];
}
sim_info.flags |= CALC_OP; sim_info.flags |= CALC_OP;
if (is_dc) { if (is_dc) {
@ -156,10 +776,24 @@ extern int OSDIload(GENmodel *inModel, CKTcircuit *ckt) {
if (is_tran) { if (is_tran) {
sim_info.flags |= CALC_REACT_JACOBIAN | CALC_REACT_RESIDUAL | sim_info.flags |= CALC_REACT_JACOBIAN | CALC_REACT_RESIDUAL |
CALC_REACT_LIM_RHS | ANALYSIS_TRAN; CALC_REACT_LIM_RHS | ANALYSIS_TRAN;
/* OSDI 0.5 — flag the final transient timepoint so a model can fire
* an @(final_step) body. dctran puts a hard breakpoint at
* CKTfinalTime, so the last accepted step lands exactly on it. */
if (AlmostEqualUlps(ckt->CKTtime, ckt->CKTfinalTime, 100)) {
sim_info.at_final_step = 1;
}
} }
if (is_tran_op) { if (is_tran_op) {
sim_info.flags |= ANALYSIS_TRAN; sim_info.flags |= ANALYSIS_TRAN;
/* Compute the reactive residual (charge) at the transient operating
* point so it can be seeded into the state vector. The OP solve itself
* does not stamp any reactive term (the DC load path below only loads the
* resistive Jacobian/RHS), so requesting the charge here is side-effect
* free for the OP and gives the first transient step a correct dq/dt = 0
* starting history (see the seeding block in load()). */
sim_info.flags |= CALC_REACT_RESIDUAL;
} }
if (is_ac) { if (is_ac) {
@ -201,6 +835,10 @@ extern int OSDIload(GENmodel *inModel, CKTcircuit *ckt) {
OsdiExtraInstData *extra_inst_data = OsdiExtraInstData *extra_inst_data =
osdi_extra_instance_data(entry, gen_inst); osdi_extra_instance_data(entry, gen_inst);
/* Deferred-commit: feed this eval the previous-accepted persistent
* state (done synchronously before the async task). */
osdi_persist_restore(entry, gen_inst, inst, extra_inst_data);
#pragma omp task firstprivate(gen_inst, inst, extra_inst_data, model) #pragma omp task firstprivate(gen_inst, inst, extra_inst_data, model)
eval(descr, gen_inst, inst, extra_inst_data, model, &sim_info); eval(descr, gen_inst, inst, extra_inst_data, model, &sim_info);
} }
@ -221,6 +859,7 @@ extern int OSDIload(GENmodel *inModel, CKTcircuit *ckt) {
void *inst = osdi_instance_data(entry, gen_inst); void *inst = osdi_instance_data(entry, gen_inst);
OsdiExtraInstData *extra_inst_data = OsdiExtraInstData *extra_inst_data =
osdi_extra_instance_data(entry, gen_inst); osdi_extra_instance_data(entry, gen_inst);
load(ckt, gen_inst, model, inst, extra_inst_data, is_tran, is_init_tran, load(ckt, gen_inst, model, inst, extra_inst_data, is_tran, is_init_tran,
descr); descr);
eval_flags |= extra_inst_data->eval_flags; eval_flags |= extra_inst_data->eval_flags;
@ -236,6 +875,11 @@ extern int OSDIload(GENmodel *inModel, CKTcircuit *ckt) {
OsdiExtraInstData *extra_inst_data = OsdiExtraInstData *extra_inst_data =
osdi_extra_instance_data(entry, gen_inst); osdi_extra_instance_data(entry, gen_inst);
/* Deferred-commit: feed this eval the previous-accepted persistent
* state so predictor/Newton iterates can't corrupt the model's history. */
osdi_persist_restore(entry, gen_inst, inst, extra_inst_data);
eval(descr, gen_inst, inst, extra_inst_data, model, &sim_info); eval(descr, gen_inst, inst, extra_inst_data, model, &sim_info);
/* init small signal analysis does not require loading values into /* init small signal analysis does not require loading values into
@ -254,9 +898,25 @@ extern int OSDIload(GENmodel *inModel, CKTcircuit *ckt) {
return E_PANIC; return E_PANIC;
} }
/* EVAL_RET_FLAG_LIM: model applied Verilog-A $limit to damp a node voltage
* (e.g. NQS internal nodes) during this N-R step. $limit is the designed
* mechanism for NQS convergence the step is internally clamped to a safe
* range, but the solution has not yet converged. Signal non-convergence so
* the N-R keeps iterating rather than accepting the under-damped iterate. */
if (eval_flags & EVAL_RET_FLAG_LIM) { if (eval_flags & EVAL_RET_FLAG_LIM) {
ckt->CKTnoncon++;
ckt->CKTtroubleElt = gen_inst; ckt->CKTtroubleElt = gen_inst;
ckt->CKTnoncon++;
}
/* Axis-3 (OSDI v0.5): a model has raised REJECT_STEP, signalling that
* its linearization is invalid at the current operating point (predicted
* voltage outside model validity, internal-node blow-up, ...). NIiter
* will observe CKTosdiStepReject after CKTload returns and bail with
* E_ITERLIM so dctran cuts CKTdelta. */
if ((eval_flags & EVAL_RET_FLAG_REJECT_STEP) && !ckt->CKTosdiStepRejectOff) {
ckt->CKTtroubleElt = gen_inst;
ckt->CKTosdiStepReject = 1;
ckt->CKTnoncon++;
} }
if (eval_flags & EVAL_RET_FLAG_STOP) { if (eval_flags & EVAL_RET_FLAG_STOP) {

View File

@ -12,6 +12,7 @@
#include "ngspice/iferrmsg.h" #include "ngspice/iferrmsg.h"
#include "ngspice/ngspice.h" #include "ngspice/ngspice.h"
#include "ngspice/typedefs.h" #include "ngspice/typedefs.h"
#include "ngspice/fteext.h"
#include "osdidefs.h" #include "osdidefs.h"
@ -109,6 +110,9 @@ extern int OSDIparam(int param, IFvalue *value, GENinstance *instPtr,
void *dst = descr->access(inst, NULL, (uint32_t)param, void *dst = descr->access(inst, NULL, (uint32_t)param,
ACCESS_FLAG_SET | ACCESS_FLAG_INSTANCE); ACCESS_FLAG_SET | ACCESS_FLAG_INSTANCE);
/* W and L arrive already shrunk: the subcircuit's `scale` parameter
(applied in subckt expansion) or the model's own dimension
expressions multiply the drawn geometry before it reaches here. */
return osdi_write_param(dst, value, param, descr); return osdi_write_param(dst, value, param, descr);
} }

View File

@ -398,12 +398,16 @@ extern OsdiObjectFile load_object_file(const char *input) {
} }
descriptor_size = *OSDI_DESCRIPTOR_SIZE; descriptor_size = *OSDI_DESCRIPTOR_SIZE;
} else { } else {
// Original OpenVAF, must be version==0.3 /* Original OpenVAF binaries don't publish OSDI_DESCRIPTOR_SIZE
if (OSDI_VERSION_MAJOR != OSDI_VERSION_MAJOR_CURR || * and must be v0.3 exactly. OSDI_VERSION_MAJOR_CURR /
OSDI_VERSION_MINOR != OSDI_VERSION_MINOR_CURR) { * OSDI_VERSION_MINOR_CURR now track the current
printf("NGSPICE only supports OSDI v%d.%d but \"%s\" uses v%d.%d!", * openvaf-reloaded/OpenVA version (0.5 as of S3a) so the
OSDI_VERSION_MAJOR_CURR, OSDI_VERSION_MINOR_CURR, path, * v0.3 check is hard-coded here. */
OSDI_VERSION_MAJOR, OSDI_VERSION_MINOR); if (OSDI_VERSION_MAJOR != 0 || OSDI_VERSION_MINOR != 3) {
printf("NGSPICE only supports OSDI v0.3 (original OpenVAF) "
"or v0.4+ (OpenVAF-reloaded / OpenVA) but \"%s\" uses "
"v%d.%d!",
path, OSDI_VERSION_MAJOR, OSDI_VERSION_MINOR);
txfree(path); txfree(path);
return INVALID_OBJECT; return INVALID_OBJECT;
} }
@ -430,6 +434,18 @@ extern OsdiObjectFile load_object_file(const char *input) {
} }
for (uint32_t i = 0; i < lim_table_len; i++) { for (uint32_t i = 0; i < lim_table_len; i++) {
/* If the model provided its own limiter implementation, respect it.
* Two cases:
* - Standard name (e.g. "pnjlim") with the canonical signature: the
* model is shadowing the simulator's default with its own version.
* - Custom name with a model-defined signature: the model has both the
* call site (in its compiled eval()) and the implementation, so the
* simulator never has to know what the signature is.
* In either case the simulator stays out of the way: do not overwrite
* func_ptr, and do not warn about an "unknown" name. */
if (lim_table[i].func_ptr != NULL) {
continue;
}
int expected_args = -1; int expected_args = -1;
IS_LIM_FUN("pnjlim", 2, osdi_pnjlim) IS_LIM_FUN("pnjlim", 2, osdi_pnjlim)
IS_LIM_FUN("limvds", 0, osdi_limvds) IS_LIM_FUN("limvds", 0, osdi_limvds)
@ -475,6 +491,29 @@ extern OsdiObjectFile load_object_file(const char *input) {
size_t inst_off = calc_osdi_instance_data_off(descr); size_t inst_off = calc_osdi_instance_data_off(descr);
size_t noise_off = calc_osdi_noise_off(descr); size_t noise_off = calc_osdi_noise_off(descr);
/* OSDI v0.5 (2C) — only trust num_delay_sites if the published
* descriptor size actually reaches that field; a pre-2C .osdi has a
* smaller descriptor and reading it would dereference past its slot. */
uint32_t num_delay_sites =
(descriptor_size >= offsetof(OsdiDescriptor, num_delay_sites) +
sizeof(uint32_t))
? descr->num_delay_sites
: 0u;
/* Persistent-state offset/count: only trust them if the published
* descriptor actually reaches these tail fields (a pre-0.5 .osdi has a
* smaller descriptor). */
uint32_t persistent_state_count =
(descriptor_size >= offsetof(OsdiDescriptor, persistent_state_count) +
sizeof(uint32_t))
? descr->persistent_state_count
: 0u;
uint32_t persistent_state_offset =
(descriptor_size >= offsetof(OsdiDescriptor, persistent_state_offset) +
sizeof(uint32_t))
? descr->persistent_state_offset
: 0u;
dst[i] = (OsdiRegistryEntry){ dst[i] = (OsdiRegistryEntry){
.descriptor = descr, .descriptor = descr,
.inst_offset = (uint32_t)inst_off, .inst_offset = (uint32_t)inst_off,
@ -482,6 +521,9 @@ extern OsdiObjectFile load_object_file(const char *input) {
.dt = dt, .dt = dt,
.temp = temp, .temp = temp,
.has_m = has_m, .has_m = has_m,
.num_delay_sites = num_delay_sites,
.persistent_state_offset = persistent_state_offset,
.persistent_state_count = persistent_state_count,
#ifdef KLU #ifdef KLU
.matrix_ptr_offset = (uint32_t)calc_osdi_instance_matrix_off(descr), .matrix_ptr_offset = (uint32_t)calc_osdi_instance_matrix_off(descr),

View File

@ -17,6 +17,7 @@
#include "osdi.h" #include "osdi.h"
#include "osdidefs.h" #include "osdidefs.h"
#include "ngspice/osdi_defer.h"
#include <stdint.h> #include <stdint.h>
#include <stdio.h> #include <stdio.h>
@ -178,9 +179,130 @@ static int init_matrix(SMPmatrix *matrix, const OsdiDescriptor *descr,
*jacobian_ptr_react = ptr + 1; *jacobian_ptr_react = ptr + 1;
} }
} }
return (OK); return (OK);
} }
/* Look up a parameter slot by case-insensitive name. Returns the
* param_opvar index, or (uint32_t)-1 if not found. Used by the
* deferred-eval per-instance binding to find named instance parameters
* (l, w, nf, m) and the model-param slots that hold the deferred-
* expression results. */
static uint32_t
find_param_idx(const OsdiDescriptor *descr, const char *name) {
for (uint32_t i = 0; i < descr->num_params + descr->num_opvars; i++) {
for (uint32_t j = 0; j < descr->param_opvar[i].num_alias + 1; j++) {
if (descr->param_opvar[i].name[j] &&
strcasecmp(descr->param_opvar[i].name[j], name) == 0) {
return i;
}
}
}
return (uint32_t)-1;
}
/* Read a Real-typed instance parameter by its param index. Returns the
* value as double, or `dflt` if the index is invalid or the type is
* non-Real. */
static double
read_inst_real(const OsdiDescriptor *descr, void *inst, void *model,
uint32_t idx, double dflt) {
if (idx == (uint32_t)-1) return dflt;
OsdiParamOpvar *p = &descr->param_opvar[idx];
if ((p->flags & PARA_TY_MASK) != PARA_TY_REAL || p->len) return dflt;
uint32_t flags = ACCESS_FLAG_READ;
if (idx < descr->num_instance_params) flags |= ACCESS_FLAG_INSTANCE;
void *src = descr->access(inst, model, idx, flags);
if (!src) return dflt;
return *(double *)src;
}
/* Write a Real-typed parameter on the instance level (override of model
* default). Used to install the per-instance value computed from a
* deferred expression. */
static void
write_inst_real(const OsdiDescriptor *descr, void *inst, uint32_t idx,
double value) {
if (idx == (uint32_t)-1) return;
OsdiParamOpvar *p = &descr->param_opvar[idx];
if ((p->flags & PARA_TY_MASK) != PARA_TY_REAL || p->len) return;
void *dst = descr->access(inst, NULL, idx,
ACCESS_FLAG_SET | ACCESS_FLAG_INSTANCE);
if (dst) *(double *)dst = value;
}
/* Write a Real-typed parameter on the MODEL level. Used to populate
* the deferred-eval results into model storage BEFORE setup_model runs
* so its parameter-range validation sees sensible values. Per-instance
* overrides happen separately in the OSDItemp inner loop. */
static void
write_model_real(const OsdiDescriptor *descr, void *model, uint32_t idx,
double value) {
if (idx == (uint32_t)-1) return;
OsdiParamOpvar *p = &descr->param_opvar[idx];
if ((p->flags & PARA_TY_MASK) != PARA_TY_REAL || p->len) return;
void *dst = descr->access(NULL, model, idx, ACCESS_FLAG_SET);
if (dst) *(double *)dst = value;
}
/* Read a Real-typed MODEL-level parameter, returning `dflt` if absent
* or not a Real scalar. Used by the deferred-eval pre-pass to pick a
* representative L within each model's lmin/lmax range, so model
* expressions of the form `(l==14e-9)*A + (l==16e-9)*B` evaluate to
* a non-zero value rather than 0 (which BSIM-CMG's setup_model
* rejects with "Parameter VSAT1 = 0 is not positive" etc.). */
static double
read_model_real(const OsdiDescriptor *descr, void *model, uint32_t idx,
double dflt) {
if (idx == (uint32_t)-1) return dflt;
OsdiParamOpvar *p = &descr->param_opvar[idx];
if ((p->flags & PARA_TY_MASK) != PARA_TY_REAL || p->len) return dflt;
void *src = descr->access(NULL, model, idx, ACCESS_FLAG_READ);
if (!src) return dflt;
return *(double *)src;
}
/* Callback ctx + handler for model-level pre-eval. Writes to `model`
* storage with a default representative geometry so setup_model's
* bound check passes. */
typedef struct {
const OsdiDescriptor *descr;
void *model;
double l, w, nf, m;
} defer_model_apply_ctx;
static void
apply_deferred_param_to_model(const char *param_name,
const char *expr_text,
void *cb_arg) {
defer_model_apply_ctx *c = (defer_model_apply_ctx *)cb_arg;
double v = 0.0;
if (osdi_defer_eval(expr_text, c->l, c->w, c->nf, c->m, &v) != 0) return;
uint32_t idx = find_param_idx(c->descr, param_name);
write_model_real(c->descr, c->model, idx, v);
}
/* Per-callback closure for per-instance binding: resolved geometry and
* the OSDI access plumbing needed to install each evaluated expression. */
typedef struct {
const OsdiDescriptor *descr;
void *inst;
double l, w, nf, m;
} defer_apply_ctx;
static void
apply_deferred_param(const char *param_name,
const char *expr_text,
void *cb_arg) {
defer_apply_ctx *c = (defer_apply_ctx *)cb_arg;
double v = 0.0;
if (osdi_defer_eval(expr_text, c->l, c->w, c->nf, c->m, &v) != 0) {
return;
}
uint32_t idx = find_param_idx(c->descr, param_name);
write_inst_real(c->descr, c->inst, idx, v);
}
int OSDIsetup(SMPmatrix *matrix, GENmodel *inModel, CKTcircuit *ckt, int OSDIsetup(SMPmatrix *matrix, GENmodel *inModel, CKTcircuit *ckt,
int *states) { int *states) {
OsdiInitInfo init_info; OsdiInitInfo init_info;
@ -211,6 +333,43 @@ int OSDIsetup(SMPmatrix *matrix, GENmodel *inModel, CKTcircuit *ckt,
for (gen_model = inModel; gen_model; gen_model = gen_model->GENnextModel) { for (gen_model = inModel; gen_model; gen_model = gen_model->GENnextModel) {
void *model = osdi_model_data(gen_model); void *model = osdi_model_data(gen_model);
/* Apply deferred-eval to MODEL storage with representative default
* geometry before setup_model runs. setup_model validates parameter
* 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
* 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
* 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). */
if (osdi_defer_has(gen_model->GENmodName)) {
/* Pick a representative L within this model's bin range. The
* (lmin, lmax) was recorded by INPgetModBin during bin
* selection see osdi_defer_record_bin_range. We can't read
* lmin/lmax from OSDI param storage because they're SPICE-side
* bin-selection hints, not Verilog-A model params (BSIM-CMG's
* .va file doesn't declare them). */
double lmin_v = 0.0, lmax_v = 0.0;
double default_l = 30e-9;
if (osdi_defer_get_bin_range(gen_model->GENmodName,
&lmin_v, &lmax_v)
&& lmin_v > 0.0 && lmax_v > lmin_v) {
default_l = 0.5 * (lmin_v + lmax_v);
}
defer_model_apply_ctx mctx = {
.descr = descr, .model = model,
.l = default_l, .w = 100e-9, .nf = 1.0, .m = 1.0,
};
osdi_defer_for_model(gen_model->GENmodName,
apply_deferred_param_to_model, &mctx);
}
/* setup model parameter (setup_model)*/ /* setup model parameter (setup_model)*/
handle = (OsdiNgspiceHandle){.kind = 1, .name = gen_model->GENmodName}; handle = (OsdiNgspiceHandle){.kind = 1, .name = gen_model->GENmodName};
descr->setup_model((void *)&handle, model, sim_params, &init_info); descr->setup_model((void *)&handle, model, sim_params, &init_info);
@ -273,6 +432,21 @@ int OSDIsetup(SMPmatrix *matrix, GENmodel *inModel, CKTcircuit *ckt,
} }
if (error) if (error)
return (error); return (error);
/* Exclude ONLY the synthesized analog-operator implicit-equation state
* nodes (laplace, zi, idt, transition-delay) from gmin stepping. Their
* resistive diagonal is the realized pole value -p_k -- small, or zero
* for a pole at the origin -- so the dynamic gmin-stepping pass (LoadGmin)
* over-damps the state Newton update and the homotopy fails ("gmin
* stepping failed"). Physical model-internal nodes (e.g. BSIM RD/RS
* parasitic and NQS charge nodes) are NOT flagged: they have real
* resistive diagonals and legitimately benefit from gmin stepping, so
* excluding them would regress PDK DC convergence. OpenVA names the
* synthesized state nodes "implicit_equation_<N>" (osdi metadata.rs);
* physical nodes keep their real Verilog-A node names. */
if (descr->nodes[i].name &&
strncmp(descr->nodes[i].name, "implicit_equation_", 18) == 0) {
tmp->nogmin = 1;
}
node_ids[i] = (uint32_t)tmp->number; node_ids[i] = (uint32_t)tmp->number;
// TODO nodeset? // TODO nodeset?
} }
@ -317,6 +491,12 @@ extern int OSDItemp(GENmodel *inModel, CKTcircuit *ckt) {
gen_model = gen_model->GENnextModel) { gen_model = gen_model->GENnextModel) {
void *model = osdi_model_data(gen_model); void *model = osdi_model_data(gen_model);
/* Model-level deferred-eval defaults were already installed by
* OSDIsetup before its first setup_model call. No need to repeat
* here; setup_model on a temperature step re-reads from the model
* slots that OSDIsetup populated. Per-instance overrides happen
* later in this loop. */
handle = (OsdiNgspiceHandle){.kind = 4, .name = gen_model->GENmodName}; handle = (OsdiNgspiceHandle){.kind = 4, .name = gen_model->GENmodName};
descr->setup_model((void *)&handle, model, sim_params, &init_info); descr->setup_model((void *)&handle, model, sim_params, &init_info);
res = handle_init_info(init_info, descr); res = handle_init_info(init_info, descr);
@ -325,10 +505,42 @@ extern int OSDItemp(GENmodel *inModel, CKTcircuit *ckt) {
continue; continue;
} }
/* Pre-resolve the four per-instance geometry param indices for this
* model's descriptor. These map names to OSDI param slots; values
* are read per-instance inside the loop. -1 means the model
* doesn't expose the symbol the deferred expression will fall
* back to the default 0 for it. */
const char *model_name = gen_model->GENmodName;
bool has_deferred = osdi_defer_has(model_name);
uint32_t idx_l = 0, idx_w = 0, idx_nf = 0, idx_m = 0;
if (has_deferred) {
idx_l = find_param_idx(descr, "l");
idx_w = find_param_idx(descr, "w");
idx_nf = find_param_idx(descr, "nf");
idx_m = find_param_idx(descr, "m");
}
for (gen_inst = gen_model->GENinstances; gen_inst != NULL; for (gen_inst = gen_model->GENinstances; gen_inst != NULL;
gen_inst = gen_inst->GENnextInstance) { gen_inst = gen_inst->GENnextInstance) {
void *inst = osdi_instance_data(entry, gen_inst); void *inst = osdi_instance_data(entry, gen_inst);
/* Deferred-expression evaluation per instance. Runs BEFORE
* 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. */
if (has_deferred) {
defer_apply_ctx ctx = {
.descr = descr,
.inst = inst,
.l = read_inst_real(descr, inst, model, idx_l, 0.0),
.w = read_inst_real(descr, inst, model, idx_w, 0.0),
.nf = read_inst_real(descr, inst, model, idx_nf, 1.0),
.m = read_inst_real(descr, inst, model, idx_m, 1.0),
};
osdi_defer_for_model(model_name, apply_deferred_param, &ctx);
}
// special handleing for temperature parameters // special handleing for temperature parameters
double temp = ckt->CKTtemp; double temp = ckt->CKTtemp;
OsdiExtraInstData *extra_inst_data = OsdiExtraInstData *extra_inst_data =

View File

@ -11,6 +11,7 @@
#include "ngspice/cktdefs.h" #include "ngspice/cktdefs.h"
#include "osdidefs.h" #include "osdidefs.h"
#include <stdio.h>
int OSDItrunc(GENmodel *in_model, CKTcircuit *ckt, double *timestep) { int OSDItrunc(GENmodel *in_model, CKTcircuit *ckt, double *timestep) {
OsdiRegistryEntry *entry = osdi_reg_entry_model(in_model); OsdiRegistryEntry *entry = osdi_reg_entry_model(in_model);
@ -19,6 +20,15 @@ int OSDItrunc(GENmodel *in_model, CKTcircuit *ckt, double *timestep) {
bool has_boundstep = offset != UINT32_MAX; bool has_boundstep = offset != UINT32_MAX;
offset += entry->inst_offset; offset += entry->inst_offset;
/* OSDI v0.5 (S3b) — clamp the proposed timestep against any
* pending scheduled events on instances of this model. Each
* instance has a sorted per-instance queue of scheduled events;
* the earliest one bounds dt so the simulator lands exactly on
* the event time and the model's `@(timer)` / `@(cross)` body
* fires with at_scheduled_event = true. */
double t_now = ckt->CKTtime;
bool model_has_events = descr->num_event_slots > 0;
for (GENmodel *model = in_model; model; model = model->GENnextModel) { for (GENmodel *model = in_model; model; model = model->GENnextModel) {
for (GENinstance *inst = model->GENinstances; inst; for (GENinstance *inst = model->GENinstances; inst;
inst = inst->GENnextInstance) { inst = inst->GENnextInstance) {
@ -30,6 +40,18 @@ int OSDItrunc(GENmodel *in_model, CKTcircuit *ckt, double *timestep) {
} }
} }
if (model_has_events) {
OsdiExtraInstData *extra =
osdi_extra_instance_data(entry, inst);
if (extra && extra->scheduled_count > 0) {
double t_event = extra->scheduled_events[0].at_time;
double dt_to_event = t_event - t_now;
if (dt_to_event > 0.0 && dt_to_event < *timestep) {
*timestep = dt_to_event;
}
}
}
int state = inst->GENstate + (int)descr->num_states; int state = inst->GENstate + (int)descr->num_states;
for (uint32_t i = 0; i < descr->num_nodes; i++) { for (uint32_t i = 0; i < descr->num_nodes; i++) {
if (descr->nodes[i].react_residual_off != UINT32_MAX) { if (descr->nodes[i].react_residual_off != UINT32_MAX) {
@ -39,5 +61,29 @@ int OSDItrunc(GENmodel *in_model, CKTcircuit *ckt, double *timestep) {
} }
} }
} }
/* CKTterr is backward-looking: it sees low curvature in the charges just
* before a switching transition and allows the timestep to grow, producing
* a large step that spans the entire transition. Limit growth to 1.2×
* the most recently accepted step so that fast edges are resolved
* 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
* 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
* at the next switching edge from a 5 ns extrapolation, the
* linearization was too coarse to track within itl4. 1.2× still
* grows by ~6× over 10 accepted steps (1.2^10), so a quiet 6 ns
* interval gets ~12 steps to grow from 10 ps to ~300 ps small
* enough that the next sharp edge is easily resolved. */
if (ckt->CKTdeltaOld[0] > 0.0) {
double max_step = ckt->CKTdeltaOld[0] * 1.2;
if (*timestep > max_step) {
*timestep = max_step;
}
}
return 0; return 0;
} }

View File

@ -97,6 +97,14 @@ CKTdoJob(CKTcircuit* ckt, int reset, TSKtask* task)
ckt->CKTdefaultMosAS = task->TSKdefaultMosAS; ckt->CKTdefaultMosAS = task->TSKdefaultMosAS;
ckt->CKTfixLimit = task->TSKfixLimit; ckt->CKTfixLimit = task->TSKfixLimit;
ckt->CKTnoOpIter = task->TSKnoOpIter; ckt->CKTnoOpIter = task->TSKnoOpIter;
ckt->CKTresidCheckDisabled = task->TSKnoResidCheck;
ckt->CKTosdiStepRejectOff = task->TSKnoOsdiStepReject;
ckt->CKTdtClearOff = task->TSKnoDtClear;
ckt->CKTosdiVlim = task->TSKosdiVlim;
ckt->CKTosdiVlimVds = task->TSKosdiVlimVds;
ckt->CKTosdiVlimVgs = task->TSKosdiVlimVgs;
ckt->CKTosdiVlimVbs = task->TSKosdiVlimVbs;
ckt->CKTosdiVlimNqs = task->TSKosdiVlimNqs;
ckt->CKTtryToCompact = task->TSKtryToCompact; ckt->CKTtryToCompact = task->TSKtryToCompact;
ckt->CKTbadMos3 = task->TSKbadMos3; ckt->CKTbadMos3 = task->TSKbadMos3;
ckt->CKTkeepOpInfo = task->TSKkeepOpInfo; ckt->CKTkeepOpInfo = task->TSKkeepOpInfo;

View File

@ -67,6 +67,9 @@ CKTnewTask(CKTcircuit *ckt, TSKtask **taskPtr, IFuid taskName, TSKtask **defPtr)
tsk->TSKdefaultMosAS = def->TSKdefaultMosAS; tsk->TSKdefaultMosAS = def->TSKdefaultMosAS;
/* fixLimit */ /* fixLimit */
tsk->TSKnoOpIter = def->TSKnoOpIter; tsk->TSKnoOpIter = def->TSKnoOpIter;
tsk->TSKnoResidCheck = def->TSKnoResidCheck;
tsk->TSKnoOsdiStepReject = def->TSKnoOsdiStepReject;
tsk->TSKnoDtClear = def->TSKnoDtClear;
tsk->TSKtryToCompact = def->TSKtryToCompact; tsk->TSKtryToCompact = def->TSKtryToCompact;
tsk->TSKbadMos3 = def->TSKbadMos3; tsk->TSKbadMos3 = def->TSKbadMos3;
tsk->TSKkeepOpInfo = def->TSKkeepOpInfo; tsk->TSKkeepOpInfo = def->TSKkeepOpInfo;
@ -74,6 +77,11 @@ CKTnewTask(CKTcircuit *ckt, TSKtask **taskPtr, IFuid taskName, TSKtask **defPtr)
tsk->TSKnodeDamping = def->TSKnodeDamping; tsk->TSKnodeDamping = def->TSKnodeDamping;
tsk->TSKabsDv = def->TSKabsDv; tsk->TSKabsDv = def->TSKabsDv;
tsk->TSKrelDv = def->TSKrelDv; tsk->TSKrelDv = def->TSKrelDv;
tsk->TSKosdiVlim = def->TSKosdiVlim;
tsk->TSKosdiVlimVds = def->TSKosdiVlimVds;
tsk->TSKosdiVlimVgs = def->TSKosdiVlimVgs;
tsk->TSKosdiVlimVbs = def->TSKosdiVlimVbs;
tsk->TSKosdiVlimNqs = def->TSKosdiVlimNqs;
tsk->TSKnoopac = def->TSKnoopac; tsk->TSKnoopac = def->TSKnoopac;
tsk->TSKepsmin = def->TSKepsmin; tsk->TSKepsmin = def->TSKepsmin;
@ -130,6 +138,9 @@ CKTnewTask(CKTcircuit *ckt, TSKtask **taskPtr, IFuid taskName, TSKtask **defPtr)
tsk->TSKdefaultMosAD = 0; tsk->TSKdefaultMosAD = 0;
tsk->TSKdefaultMosAS = 0; tsk->TSKdefaultMosAS = 0;
tsk->TSKnoOpIter = 0; tsk->TSKnoOpIter = 0;
tsk->TSKnoResidCheck = 0;
tsk->TSKnoOsdiStepReject = 0;
tsk->TSKnoDtClear = 0;
tsk->TSKtryToCompact = 0; tsk->TSKtryToCompact = 0;
tsk->TSKbadMos3 = 0; tsk->TSKbadMos3 = 0;
tsk->TSKkeepOpInfo = 0; tsk->TSKkeepOpInfo = 0;
@ -137,6 +148,11 @@ CKTnewTask(CKTcircuit *ckt, TSKtask **taskPtr, IFuid taskName, TSKtask **defPtr)
tsk->TSKnodeDamping = 0; tsk->TSKnodeDamping = 0;
tsk->TSKabsDv = 0.5; tsk->TSKabsDv = 0.5;
tsk->TSKrelDv = 2.0; tsk->TSKrelDv = 2.0;
tsk->TSKosdiVlim = 0.1;
tsk->TSKosdiVlimVds = 0.0; /* 0 = inherit from osdi_vlim */
tsk->TSKosdiVlimVgs = 0.0;
tsk->TSKosdiVlimVbs = 0.0;
tsk->TSKosdiVlimNqs = 0.0;
tsk->TSKepsmin = 1e-28; tsk->TSKepsmin = 1e-28;
#ifdef KLU #ifdef KLU

View File

@ -41,6 +41,15 @@ CKTsetOpt(CKTcircuit *ckt, JOB *anal, int opt, IFvalue *val)
case OPT_NOOPITER: case OPT_NOOPITER:
task->TSKnoOpIter = (val->iValue != 0); task->TSKnoOpIter = (val->iValue != 0);
break; break;
case OPT_NORESIDCHECK:
task->TSKnoResidCheck = (val->iValue != 0);
break;
case OPT_NOOSDISTEPREJECT:
task->TSKnoOsdiStepReject = (val->iValue != 0);
break;
case OPT_NODTCLEAR:
task->TSKnoDtClear = (val->iValue != 0);
break;
case OPT_GMIN: case OPT_GMIN:
task->TSKgmin = val->rValue; task->TSKgmin = val->rValue;
break; break;
@ -166,6 +175,21 @@ CKTsetOpt(CKTcircuit *ckt, JOB *anal, int opt, IFvalue *val)
case OPT_RELDV: case OPT_RELDV:
task->TSKrelDv = val->rValue; task->TSKrelDv = val->rValue;
break; break;
case OPT_OSDIVLIM:
task->TSKosdiVlim = val->rValue;
break;
case OPT_OSDIVLIM_VDS:
task->TSKosdiVlimVds = val->rValue;
break;
case OPT_OSDIVLIM_VGS:
task->TSKosdiVlimVgs = val->rValue;
break;
case OPT_OSDIVLIM_VBS:
task->TSKosdiVlimVbs = val->rValue;
break;
case OPT_OSDIVLIM_NQS:
task->TSKosdiVlimNqs = val->rValue;
break;
case OPT_NOOPAC: case OPT_NOOPAC:
task->TSKnoopac = (val->iValue != 0); task->TSKnoopac = (val->iValue != 0);
break; break;
@ -273,6 +297,9 @@ static IFparm OPTtbl[] = {
#endif #endif
{ "cshunt", OPT_CSHUNT, IF_SET|IF_REAL, "Shunt capacitor from analog nodes to ground" }, { "cshunt", OPT_CSHUNT, IF_SET|IF_REAL, "Shunt capacitor from analog nodes to ground" },
{ "noopiter", OPT_NOOPITER,IF_SET|IF_FLAG,"Go directly to gmin stepping" }, { "noopiter", OPT_NOOPITER,IF_SET|IF_FLAG,"Go directly to gmin stepping" },
{ "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" },
{ "gmin", OPT_GMIN,IF_SET|IF_REAL,"Minimum conductance" }, { "gmin", OPT_GMIN,IF_SET|IF_REAL,"Minimum conductance" },
{ "gshunt", OPT_GSHUNT,IF_SET|IF_REAL,"Shunt conductance" }, { "gshunt", OPT_GSHUNT,IF_SET|IF_REAL,"Shunt conductance" },
{ "reltol", OPT_RELTOL,IF_SET|IF_REAL ,"Relative error tolerence"}, { "reltol", OPT_RELTOL,IF_SET|IF_REAL ,"Relative error tolerence"},
@ -359,6 +386,16 @@ static IFparm OPTtbl[] = {
"Maximum absolute iter-iter node voltage change" }, "Maximum absolute iter-iter node voltage change" },
{ "reldv", OPT_RELDV, IF_SET|IF_REAL, { "reldv", OPT_RELDV, IF_SET|IF_REAL,
"Maximum relative iter-iter node voltage change" }, "Maximum relative iter-iter node voltage change" },
{ "osdi_vlim", OPT_OSDIVLIM, IF_SET|IF_REAL,
"Per-iter Δv bound for OpenVA compiler-side $limit synthesis (generic, default 0.1 V)" },
{ "osdi_vlim_vds", OPT_OSDIVLIM_VDS, IF_SET|IF_REAL,
"Per-iter Δv bound for drain-source-shape probes (overrides osdi_vlim when set)" },
{ "osdi_vlim_vgs", OPT_OSDIVLIM_VGS, IF_SET|IF_REAL,
"Per-iter Δv bound for gate-channel-shape probes (overrides osdi_vlim when set)" },
{ "osdi_vlim_vbs", OPT_OSDIVLIM_VBS, IF_SET|IF_REAL,
"Per-iter Δv bound for body-junction-shape probes (overrides osdi_vlim when set)" },
{ "osdi_vlim_nqs", OPT_OSDIVLIM_NQS, IF_SET|IF_REAL,
"Per-iter Δv bound for NQS helper-node probes — V(NC)/V(N1)/V(NR)/etc (overrides osdi_vlim when set)" },
{ "noopac", OPT_NOOPAC, IF_SET|IF_FLAG, { "noopac", OPT_NOOPAC, IF_SET|IF_FLAG,
"No op calculation in ac if circuit is linear" }, "No op calculation in ac if circuit is linear" },
{ "epsmin", OPT_EPSMIN, IF_SET|IF_REAL, { "epsmin", OPT_EPSMIN, IF_SET|IF_REAL,

View File

@ -71,6 +71,11 @@ CKTtrouble(CKTcircuit *ckt, char *optmsg)
else if (cv->TRCVvType[i] == rcode) { else if (cv->TRCVvType[i] == rcode) {
sprintf(msg_p, " %s = %g: ", cv->TRCVvName[i], sprintf(msg_p, " %s = %g: ", cv->TRCVvName[i],
((RESinstance*)(cv->TRCVvElt[i]))->RESresist); ((RESinstance*)(cv->TRCVvElt[i]))->RESresist);
} else if (cv->TRCVvType[i] == PARAM_CODE) { /* .param sweep */
extern int nupa_get_real(const char *name, double *value);
double v = 0.0;
nupa_get_real(cv->TRCVvName[i], &v);
sprintf(msg_p, " %s = %g: ", cv->TRCVvName[i], v);
} else { } else {
sprintf(msg_p, " %s = %g: ", cv->TRCVvName[i], sprintf(msg_p, " %s = %g: ", cv->TRCVvName[i],
((ISRCinstance*)(cv->TRCVvElt[i]))->ISRCdcValue); ((ISRCinstance*)(cv->TRCVvElt[i]))->ISRCdcValue);

View File

@ -678,6 +678,46 @@ resume:
/* time abort? */ /* time abort? */
ckt->CKTtime += ckt->CKTdelta; ckt->CKTtime += ckt->CKTdelta;
ckt->CKTdeltaOld[0]=ckt->CKTdelta; ckt->CKTdeltaOld[0]=ckt->CKTdelta;
/* Non-uniform-history predictor guard.
*
* The order-2 Newton-divided-difference predictor used by
* NIpred (case TRAPEZOIDAL/case 2 in nipred.c) extrapolates
* pred = sols[0] + (a*dd0 + b*dd1) * dt_now
* where
* dd1 = (sols[1] - sols[2]) / dt_prev_prev
* b = -dt_now / (2 * dt_prev)
* `dd1` blows up when dt_prev_prev is much smaller than the
* other history deltas exactly the pattern that arises
* just after a breakpoint cuts dt to the picosecond scale
* for one step and then the 1.5× growth cap re-grows dt to
* the nanosecond scale within a handful of accepted steps.
* The polynomial fit through three points with widely
* 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
* 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
* 0.5 V/iter scalar clamp.
*
* Detect the non-uniform pattern via a simple ratio sanity
* check on CKTdeltaOld[1] vs CKTdeltaOld[2] and force
* CKTorder=1 (linear extrapolation, immune to this failure
* mode) for the current step. The order=2 predictor
* resumes naturally on the next step once the history has
* three smoothly-spaced samples again.
*
* Threshold of 10× chosen so smooth dt-growth sequences
* (e.g. the 1.5× cap, factor 1.5 per step) never trigger
* only genuine breakpoint-induced discontinuities. */
if (ckt->CKTorder > 1 &&
ckt->CKTdeltaOld[1] > 0.0 && ckt->CKTdeltaOld[2] > 0.0) {
double r = ckt->CKTdeltaOld[1] / ckt->CKTdeltaOld[2];
if (r > 10.0 || r < 0.1) {
ckt->CKTorder = 1;
}
}
NIcomCof(ckt); NIcomCof(ckt);
#ifdef PREDICTOR #ifdef PREDICTOR
error = NIpred(ckt); error = NIpred(ckt);
@ -901,6 +941,29 @@ resume:
(void)printf("delta at delmin\n"); (void)printf("delta at delmin\n");
#endif #endif
} else { } else {
/* Before reporting the abort, identify the
* worst-moving node so the failure message says
* something useful instead of "cause unrecorded".
* Standard CKTtroubleNode/Elt get reset between
* NIiter calls, so we re-scan here at the exit. */
if (!ckt->CKTtroubleNode && !ckt->CKTtroubleElt &&
ckt->CKTnodes) {
CKTnode *n;
double worst = 0.0;
int worst_idx = 0;
for (n = ckt->CKTnodes->next; n; n = n->next) {
if (n->type != SP_VOLTAGE) continue;
int idx = n->number;
double diff = fabs(ckt->CKTrhs[idx] -
ckt->CKTrhsOld[idx]);
if (diff > worst) {
worst = diff;
worst_idx = idx;
}
}
if (worst_idx > 0)
ckt->CKTtroubleNode = worst_idx;
}
UPDATE_STATS(DOING_TRAN); UPDATE_STATS(DOING_TRAN);
errMsg = CKTtrouble(ckt, "Timestep too small"); errMsg = CKTtrouble(ckt, "Timestep too small");
SPfrontEnd->OUTendPlot(job->TRANplot); SPfrontEnd->OUTendPlot(job->TRANplot);

View File

@ -29,6 +29,148 @@ Modified: 1999 Paolo Nenzi
static double actval, actdiff; static double actval, actdiff;
#endif #endif
/* Sweeping a .param: we update the numparam dictionary AND any V/I
* source's DC value that was bound to this param at parse time.
* Bindings come from inpdpar.c's dpar_register_binding() table; we
* read them via the opaque accessors below. nupa_get_real /
* nupa_set_real live in frontend/numparam/spicenum.c. */
extern int nupa_get_real(const char *name, double *value);
extern int nupa_set_real(const char *name, double value);
typedef struct dpar_param_binding dpar_param_binding_t;
extern dpar_param_binding_t *dpar_first_param_binding(void);
extern const char *dpar_binding_param_name(const dpar_param_binding_t *b);
extern const char *dpar_binding_dev_name(const dpar_param_binding_t *b);
extern int dpar_binding_dev_type(const dpar_param_binding_t *b);
extern const dpar_param_binding_t *dpar_binding_next(const dpar_param_binding_t *b);
/* Push `value` into the DC field of every V/I source bound to the
* named .param. Lookup matches on the (param_name, dev_type) pair
* recorded by dpar_register_binding at INPdevParse time, then finds
* the actual instance by name in CKThead[dev_type]. Mirrors how
* the existing source-name sweep path mutates VSRCdcValue/
* ISRCdcValue directly. */
static void dctrcurv_push_param_to_bindings(
CKTcircuit *ckt, const char *param_name, double value,
int vcode, int icode)
{
const dpar_param_binding_t *b;
for (b = dpar_first_param_binding(); b; b = dpar_binding_next(b)) {
if (strcmp(dpar_binding_param_name(b), param_name) != 0) continue;
int dt = dpar_binding_dev_type(b);
const char *devn = dpar_binding_dev_name(b);
if (dt == vcode && vcode >= 0) {
VSRCmodel *m;
VSRCinstance *here;
for (m = (VSRCmodel *)ckt->CKThead[vcode]; m; m = VSRCnextModel(m))
for (here = VSRCinstances(m); here; here = VSRCnextInstance(here)) {
if (here->VSRCname && strcmp(here->VSRCname, devn) == 0) {
here->VSRCdcValue = value;
here->VSRCdcGiven = 1;
goto next_binding;
}
}
} else if (dt == icode && icode >= 0) {
ISRCmodel *m;
ISRCinstance *here;
for (m = (ISRCmodel *)ckt->CKThead[icode]; m; m = ISRCnextModel(m))
for (here = ISRCinstances(m); here; here = ISRCnextInstance(here))
if (here->ISRCname && strcmp(here->ISRCname, devn) == 0) {
here->ISRCdcValue = value;
here->ISRCdcGiven = 1;
goto next_binding;
}
}
next_binding:;
}
}
/* Forward decl for the parse-time binding registry — defined in
* src/spicelib/parser/inpdpar.c. */
extern void dpar_register_binding(const char *param_name,
const char *dev_name, int dev_type);
/* Scan the original (un-substituted) deck for device lines that
* mention `param_name` as a bare identifier in the value position
* and register a binding for each match. Numparam pre-substitutes
* bare-identifier values in V/I/R device lines BEFORE INPdevParse
* is called, so the parse-time bind path (inpeval.c +
* inpdpar.c::INPdevParse) can't capture them by the time the
* parser sees the line, the original `.param` name has been
* replaced with its numeric value. This deck-text scan recovers
* the binding at .dc setup time using the saved actualLine text
* via `ft_curckt->ci_origdeck`.
*
* Match rules: the value field is the LAST whitespace-delimited
* token before the end of the (trimmed) line. We accept it as a
* binding when it equals `param_name` exactly (case-insensitive)
* and the device name's first letter is v/V/i/I (voltage or
* current source). No expression context `.dc paramName` only
* propagates to sources whose value field is literally
* `paramName`, not to `2*paramName` or `paramName+0.1`. */
static void dctrcurv_scan_deck_for_bindings(const char *param_name,
int vcode, int icode)
{
if (!ft_curckt) return;
struct card *c;
for (c = ft_curckt->ci_origdeck ? ft_curckt->ci_origdeck : ft_curckt->ci_deck;
c; c = c->nextcard) {
const char *line = c->line;
if (!line || !*line) continue;
if (*line == '*' || *line == '.' || *line == '+') continue;
/* Classify by first letter — only V/I sources are bindable. */
int dev_type = -1;
if (*line == 'v' || *line == 'V') dev_type = vcode;
else if (*line == 'i' || *line == 'I') dev_type = icode;
else continue;
if (dev_type < 0) continue;
/* Extract dev name (first token, lowercased to match
* GENname's canonical form). */
const char *name_end = line;
while (*name_end && *name_end != ' ' && *name_end != '\t')
name_end++;
size_t name_len = (size_t)(name_end - line);
if (name_len == 0 || name_len > 63) continue;
char dev_name_buf[64];
for (size_t i = 0; i < name_len; i++) {
char ch = line[i];
if (ch >= 'A' && ch <= 'Z') ch = (char)(ch - 'A' + 'a');
dev_name_buf[i] = ch;
}
dev_name_buf[name_len] = '\0';
/* Trim trailing whitespace and grab the last token of the
* line that's the value field (ngspice / HSPICE put the
* source value at end-of-line for simple DC sources). */
const char *end = line + strlen(line);
while (end > line && (end[-1] == ' ' || end[-1] == '\t' ||
end[-1] == '\n' || end[-1] == '\r'))
end--;
const char *last = end;
while (last > line && last[-1] != ' ' && last[-1] != '\t')
last--;
/* Strip `{...}` braces if present — inpcom.c wraps bare
* param refs in V/I source values into `{name}` for
* numparam-driven substitution. We want the inner name. */
if (last < end && *last == '{' && end[-1] == '}') {
last++;
end--;
}
size_t tok_len = (size_t)(end - last);
size_t plen = strlen(param_name);
bool match = (tok_len == plen);
if (match) {
for (size_t i = 0; i < plen; i++) {
char a = last[i], b = param_name[i];
if (a >= 'A' && a <= 'Z') a = (char)(a - 'A' + 'a');
if (b >= 'A' && b <= 'Z') b = (char)(b - 'A' + 'a');
if (a != b) { match = false; break; }
}
}
if (!match) continue;
dpar_register_binding(param_name, dev_name_buf, dev_type);
}
}
int int
DCtrCurv(CKTcircuit *ckt, int restart) DCtrCurv(CKTcircuit *ckt, int restart)
@ -150,6 +292,33 @@ DCtrCurv(CKTcircuit *ckt, int restart)
goto found; goto found;
} }
/* HSPICE-compat: sweep a .param. If the name matches a
* NUPA_REAL entry in the numparam global dictionary, push
* the start value into the dictionary AND into any V/I
* source whose DC field was bound to this .param.
*
* Bindings are recovered by scanning the original (un-
* substituted) deck text in `ft_curckt->ci_origdeck`
* numparam pre-substitutes bare-identifier values in V/I/R
* device lines BEFORE INPdevParse runs, so the parse-time
* registry from inpdpar.c can't capture them. */
{
double cur_val;
if (nupa_get_real(job->TRCVvName[i], &cur_val)) {
dctrcurv_scan_deck_for_bindings(
job->TRCVvName[i], vcode, icode);
job->TRCVvSave[i] = cur_val;
job->TRCVvType[i] = PARAM_CODE;
job->TRCVvElt[i] = NULL;
job->TRCVstepCount[i] = 0;
nupa_set_real(job->TRCVvName[i], job->TRCVvStart[i]);
dctrcurv_push_param_to_bindings(
ckt, job->TRCVvName[i], job->TRCVvStart[i],
vcode, icode);
goto found;
}
}
SPfrontEnd->IFerrorf (ERR_FATAL, SPfrontEnd->IFerrorf (ERR_FATAL,
"DC Transfer Function: Voltage source, current source, or " "DC Transfer Function: Voltage source, current source, or "
"resistor named \"%s\" is not in the circuit", "resistor named \"%s\" is not in the circuit",
@ -185,6 +354,8 @@ DCtrCurv(CKTcircuit *ckt, int restart)
SPfrontEnd->IFnewUid (ckt, &varUid, NULL, "temp-sweep", UID_OTHER, NULL); SPfrontEnd->IFnewUid (ckt, &varUid, NULL, "temp-sweep", UID_OTHER, NULL);
else if (job->TRCVvType[0] == rcode) else if (job->TRCVvType[0] == rcode)
SPfrontEnd->IFnewUid (ckt, &varUid, NULL, "res-sweep", UID_OTHER, NULL); SPfrontEnd->IFnewUid (ckt, &varUid, NULL, "res-sweep", UID_OTHER, NULL);
else if (job->TRCVvType[0] == PARAM_CODE)
SPfrontEnd->IFnewUid (ckt, &varUid, NULL, "param-sweep", UID_OTHER, NULL);
else else
SPfrontEnd->IFnewUid (ckt, &varUid, NULL, "?-sweep", UID_OTHER, NULL); SPfrontEnd->IFnewUid (ckt, &varUid, NULL, "?-sweep", UID_OTHER, NULL);
@ -261,6 +432,19 @@ DCtrCurv(CKTcircuit *ckt, int restart)
break; break;
goto nextstep; goto nextstep;
} }
} else if (job->TRCVvType[i] == PARAM_CODE) { /* param sweep */
double cur_val = 0.0;
nupa_get_real(job->TRCVvName[i], &cur_val);
if (SGN(job->TRCVvStep[i]) *
(cur_val - job->TRCVvStop[i]) > DBL_EPSILON * 1e+03)
{
i++;
firstTime = 1;
ckt->CKTmode = (ckt->CKTmode & MODEUIC) | MODEDCTRANCURVE | MODEINITJCT;
if (i > job->TRCVnestLevel)
break;
goto nextstep;
}
} }
while (--i >= 0) while (--i >= 0)
@ -279,6 +463,12 @@ DCtrCurv(CKTcircuit *ckt, int restart)
job->TRCVvStart[i]; job->TRCVvStart[i];
RESupdate_conduct((RESinstance *)(job->TRCVvElt[i]), FALSE); RESupdate_conduct((RESinstance *)(job->TRCVvElt[i]), FALSE);
DEVices[rcode]->DEVload(job->TRCVvElt[i]->GENmodPtr, ckt); DEVices[rcode]->DEVload(job->TRCVvElt[i]->GENmodPtr, ckt);
} else if (job->TRCVvType[i] == PARAM_CODE) {
job->TRCVstepCount[i] = 0;
nupa_set_real(job->TRCVvName[i], job->TRCVvStart[i]);
dctrcurv_push_param_to_bindings(
ckt, job->TRCVvName[i], job->TRCVvStart[i],
vcode, icode);
} }
/* Rotate state vectors. */ /* Rotate state vectors. */
@ -376,6 +566,11 @@ DCtrCurv(CKTcircuit *ckt, int restart)
ckt->CKTtime = ((RESinstance *)(job->TRCVvElt[0]))->RESresist; ckt->CKTtime = ((RESinstance *)(job->TRCVvElt[0]))->RESresist;
else if (job->TRCVvType[0] == TEMP_CODE) else if (job->TRCVvType[0] == TEMP_CODE)
ckt->CKTtime = ckt->CKTtemp - CONSTCtoK; ckt->CKTtime = ckt->CKTtemp - CONSTCtoK;
else if (job->TRCVvType[0] == PARAM_CODE) {
double v = 0.0;
nupa_get_real(job->TRCVvName[0], &v);
ckt->CKTtime = v;
}
#ifdef XSPICE #ifdef XSPICE
/* If first time through, call CKTdump to output Operating Point info */ /* If first time through, call CKTdump to output Operating Point info */
@ -465,6 +660,49 @@ DCtrCurv(CKTcircuit *ckt, int restart)
inp_evaluate_temper(ft_curckt); inp_evaluate_temper(ft_curckt);
CKTtemp(ckt); CKTtemp(ckt);
} else if (job->TRCVvType[i] == PARAM_CODE) { /* param sweep */
/* Exact arithmetic: cur_val = start + N*step instead of
* accumulating += step. Accumulation drifts by ~N ULP
* over N iterations and prevents `.measure when X=stop`
* from finding the endpoint.
*
* Even start+N*step isn't bit-exact when start/step
* aren't representable in binary FP (start=-0.1,
* step=0.01 -0.1+340*0.01 3.2999999... not 3.3).
* 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`
* relies on this. */
job->TRCVstepCount[i]++;
double cur_val = job->TRCVvStart[i] +
job->TRCVstepCount[i] * job->TRCVvStep[i];
/* Snap to stop ONLY on the iter that lands within one
* step of stop AND whose successor would overshoot.
* Without the cur-side guard, every iter past stop
* keeps getting snapped back to stop and the loop's
* own stop-check (top of for(;;)) never triggers. */
double next_val = job->TRCVvStart[i] +
(job->TRCVstepCount[i] + 1) * job->TRCVvStep[i];
double sign = SGN(job->TRCVvStep[i]);
double cur_offset = sign * (cur_val - job->TRCVvStop[i]);
double next_offset = sign * (next_val - job->TRCVvStop[i]);
if (next_offset > DBL_EPSILON * 1e+03 &&
cur_offset <= DBL_EPSILON * 1e+03) {
/* Snap to stop exactly. INPevaluate (the .dc stop and
* the measure `when X = <param>` RHS) and formula()'s
* fetchnumber (which stores the .param's value via
* sscanf "%lG") now BOTH parse numbers through strtod, so
* they round identically: the snapped TRCVvStop is already
* bit-equal to the m_val the measure compares against.
* (Previously the two paths differed by 1 ULP and this had
* to route the snap through INPevaluate's "% 23.15e"
* round-trip to match -- see inpeval.c.) */
cur_val = job->TRCVvStop[i];
}
nupa_set_real(job->TRCVvName[i], cur_val);
dctrcurv_push_param_to_bindings(
ckt, job->TRCVvName[i], cur_val, vcode, icode);
} }
if (SPfrontEnd->IFpauseTest()) { if (SPfrontEnd->IFpauseTest()) {
@ -500,6 +738,10 @@ DCtrCurv(CKTcircuit *ckt, int restart)
ckt->CKTtemp = job->TRCVvSave[i]; ckt->CKTtemp = job->TRCVvSave[i];
inp_evaluate_temper(ft_curckt); inp_evaluate_temper(ft_curckt);
CKTtemp(ckt); CKTtemp(ckt);
} else if (job->TRCVvType[i] == PARAM_CODE) {
nupa_set_real(job->TRCVvName[i], job->TRCVvSave[i]);
dctrcurv_push_param_to_bindings(
ckt, job->TRCVvName[i], job->TRCVvSave[i], vcode, icode);
} }
SPfrontEnd->OUTendPlot (plot); SPfrontEnd->OUTendPlot (plot);

View File

@ -507,11 +507,18 @@ int HSMHV2load(
} }
else if ( ( ckt->CKTmode & (MODEINITJCT | MODEINITFIX) ) && else if ( ( ckt->CKTmode & (MODEINITJCT | MODEINITFIX) ) &&
here->HSMHV2_off ) { here->HSMHV2_off ) {
vbs = vgs = vds = 0.0; vges = 0.0; vdbd = vsbs = 0.0; /* Off-mode initial condition. Setting everything to 0
* lands the device at exactly zero-bias, which is a
* singular point for HiSIM_HV's eval `1/Vgs`-shape
* terms blow up. Match the heuristic in the non-off
* INITJCT branch (line ~499) and seed a small forward
* bias instead. vbs/vsbs/vdbd stay at 0 (body biased to
* source). */
vbs = vsbs = vdbd = 0.0;
vgs = vges = vgse = 0.1;
vds = vdse = 0.1;
if (flg_subNode > 0) vsubs = 0.0; if (flg_subNode > 0) vsubs = 0.0;
if( here->HSMHV2_coselfheat > 0 ) deltemp=0.0; if( here->HSMHV2_coselfheat > 0 ) deltemp=0.0;
vdse = vds ;
vgse = vgs ;
Qi_nqs = Qb_nqs = 0.0 ; Qi_nqs = Qb_nqs = 0.0 ;
} }
else { else {
@ -642,6 +649,83 @@ int HSMHV2load(
} }
#endif /* PREDICTOR */ #endif /* PREDICTOR */
/* NaN-recovery (sanity damping).
*
* Symptom: on big PDKs (Samsung 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
* back to the matrix, the next solve returns NaN voltages,
* those propagate to vbs/vgs/vds/vbse/vgse/vdse via the
* `rhsOld - rhsOld` subtractions above and the per-iteration
* limiters (DEVfetlim, DEVlimvds, DEVpnjlim further below) are
* helpless because their input is already NaN. Newton can
* never recover.
*
* The mechanism is structurally identical to BSIM-BULK's
* Miller-coupled blowup that the OpenVA `$limit` synthesis
* work addresses for OSDI VA modules. HiSIM_HV is native C
* (not VA) so we get no help from that path. Hand-coded
* recovery here, equivalent to BSIM4's internal damping:
* when any external-bias quantity comes back as NaN, snap
* each NaN value back to its state0 (last accepted) value so
* downstream physics + limiters can do their job on a finite
* input. Per-instance, no model parameters needed, no
* supply-rail constants. */
{
/* Hard fallback values. Used when state0 is itself
* non-finite (a previous iteration wrote NaN to state) or
* degenerate (LDMOS-off initialization leaves 0/0/0 a
* singular point for HiSIM_HV physics). Mirrors the
* MODEINITJCT heuristic at line ~499. */
double sf_vbs = *(ckt->CKTstate0 + here->HSMHV2vbs);
double sf_vgs = *(ckt->CKTstate0 + here->HSMHV2vgs);
double sf_vds = *(ckt->CKTstate0 + here->HSMHV2vds);
double sf_vges = *(ckt->CKTstate0 + here->HSMHV2vges);
double sf_vdbd = *(ckt->CKTstate0 + here->HSMHV2vdbd);
double sf_vsbs = *(ckt->CKTstate0 + here->HSMHV2vsbs);
double sf_vbse = *(ckt->CKTstate0 + here->HSMHV2vbse);
double sf_vgse = *(ckt->CKTstate0 + here->HSMHV2vgse);
double sf_vdse = *(ckt->CKTstate0 + here->HSMHV2vdse);
if (isnan(sf_vbs)) sf_vbs = 0.0;
if (isnan(sf_vgs)) sf_vgs = 0.1;
if (isnan(sf_vds)) sf_vds = 0.1;
if (isnan(sf_vges)) sf_vges = 0.1;
if (isnan(sf_vdbd)) sf_vdbd = 0.0;
if (isnan(sf_vsbs)) sf_vsbs = 0.0;
if (isnan(sf_vbse)) sf_vbse = 0.0;
if (isnan(sf_vgse)) sf_vgse = 0.1;
if (isnan(sf_vdse)) sf_vdse = 0.1;
/* Additional safety: if state0_degenerate (all-zero), bump
* the gate/drain values just like the INITJCT path. */
bool state0_degenerate =
(sf_vbs == 0.0 && sf_vgs == 0.0 && sf_vds == 0.0);
if (state0_degenerate) {
sf_vgs = sf_vds = sf_vges = sf_vgse = sf_vdse = 0.1;
}
bool any_nan =
isnan(vbs) || isnan(vgs) || isnan(vds) ||
isnan(vges) || isnan(vdbd) || isnan(vsbs) ||
isnan(vbse) || isnan(vgse) || isnan(vdse) ||
isnan(vsubs) || isnan(deltemp);
if (any_nan) {
if (isnan(vbs)) vbs = sf_vbs;
if (isnan(vgs)) vgs = sf_vgs;
if (isnan(vds)) vds = sf_vds;
if (isnan(vges)) vges = sf_vges;
if (isnan(vdbd)) vdbd = sf_vdbd;
if (isnan(vsbs)) vsbs = sf_vsbs;
if (isnan(vbse)) vbse = sf_vbse;
if (isnan(vgse)) vgse = sf_vgse;
if (isnan(vdse)) vdse = sf_vdse;
if (isnan(vsubs)) vsubs = 0.0;
if (isnan(deltemp)) deltemp = 0.0;
if (ckt->CKTosdiStepReject == 0)
ckt->CKTosdiStepReject = 1;
}
}
/* printf("HSMHV2_load: (from rhs ) vds.. = %e %e %e %e %e %e\n", /* printf("HSMHV2_load: (from rhs ) vds.. = %e %e %e %e %e %e\n",
vds,vgs,vbs,vdse,vgse,vbse); */ vds,vgs,vbs,vdse,vgse,vbse); */
@ -1056,6 +1140,23 @@ int HSMHV2load(
*(ckt->CKTrhsOld+here->HSMHV2sbNode)); *(ckt->CKTrhsOld+here->HSMHV2sbNode));
} }
/* NaN-guard for the linear-branch voltages computed from
* rhsOld. If a previous iteration corrupted the matrix and
* the solution returned NaN for a node, these subtractions
* propagate the NaN here. vddp in particular feeds rdrift,
* which produces NaN Rd / Rs that then poison the stamp.
* Fall back to 0 (the MODEINITJCT non-off heuristic at line
* 1128) equivalent to "no voltage difference across the
* parasitic resistors this iteration". Combined with the
* input-side guard above, this severs the NaN feedback loop
* even for the parasitic branches. */
if (isnan(vddp)) vddp = 0.0;
if (isnan(vggp)) vggp = 0.0;
if (isnan(vssp)) vssp = 0.0;
if (isnan(vbpdb)) vbpdb = 0.0;
if (isnan(vbpb)) vbpb = 0.0;
if (isnan(vbpsb)) vbpsb = 0.0;
#ifdef DEBUG_HISIMHVLD_VX #ifdef DEBUG_HISIMHVLD_VX
printf( "vbd = %12.5e\n" , vbd ); printf( "vbd = %12.5e\n" , vbd );
printf( "vbs = %12.5e\n" , vbs ); printf( "vbs = %12.5e\n" , vbs );
@ -1123,15 +1224,68 @@ int HSMHV2load(
#endif #endif
/* call model evaluation */ /* call model evaluation */
if ( HSMHV2evaluate(ivdse,ivgse,ivbse,ivds, ivgs, ivbs, vbs_jct, vbd_jct, vsubs, vddp, deltemp, here, model, ckt) == HiSIM_ERROR ) int ev_rc = HSMHV2evaluate(ivdse,ivgse,ivbse,ivds, ivgs, ivbs, vbs_jct, vbd_jct, vsubs, vddp, deltemp, here, model, ckt);
if ( ev_rc == HiSIM_ERROR )
return (HiSIM_ERROR); return (HiSIM_ERROR);
if ( here->HSMHV2_cordrift == 1 ) { if ( here->HSMHV2_cordrift == 1 ) {
if ( HSMHV2rdrift(vddp, vds, vbs, vsubs, deltemp, here, model, ckt) == HiSIM_ERROR ) int rd_rc = HSMHV2rdrift(vddp, vds, vbs, vsubs, deltemp, here, model, ckt);
return (HiSIM_ERROR); if (rd_rc == HiSIM_ERROR)
return (HiSIM_ERROR);
} }
if ( HSMHV2dio(vbs_jct, vbd_jct, deltemp, here, model, ckt) == HiSIM_ERROR ) int dio_rc = HSMHV2dio(vbs_jct, vbd_jct, deltemp, here, model, ckt);
if (dio_rc == HiSIM_ERROR)
return (HiSIM_ERROR); return (HiSIM_ERROR);
/* Output-side NaN guard. Even with all bias inputs sanitized to
* finite values by the input-side guard above, HiSIM_HV's
* eval can populate certain `here->HSMHV2_*` fields with NaN at
* problematic operating points (the 3.3 V LDMOS at startup
* being the canonical case). If we let those NaN values flow
* into the matrix entries below, the next Newton iteration
* reads NaN voltages back, the input-side guard cleans them but
* the physics produces NaN again infinite NaN feedback loop.
*
* Defense in depth: zero out any non-finite output BEFORE it
* touches the matrix. This makes the device contribute zero
* stamp this iteration equivalent to a step rejection from
* the matrix's perspective. Combined with CKTosdiStepReject,
* the framework retries with a different timestep / smaller
* delta and the device can recover. */
{
double *outs[] = {
&here->HSMHV2_ids,
&here->HSMHV2_dIds_dVdsi, &here->HSMHV2_dIds_dVgsi,
&here->HSMHV2_dIds_dVbsi,
&here->HSMHV2_dIds_dVdse, &here->HSMHV2_dIds_dVgse,
&here->HSMHV2_dIds_dVbse,
&here->HSMHV2_qd,
&here->HSMHV2_dQdi_dVdsi, &here->HSMHV2_dQdi_dVgsi,
&here->HSMHV2_dQdi_dVbsi,
&here->HSMHV2_qg,
&here->HSMHV2_dQg_dVdsi, &here->HSMHV2_dQg_dVgsi,
&here->HSMHV2_dQg_dVbsi,
&here->HSMHV2_qs,
&here->HSMHV2_dQsi_dVdsi, &here->HSMHV2_dQsi_dVgsi,
&here->HSMHV2_dQsi_dVbsi,
&here->HSMHV2_dQb_dVdsi, &here->HSMHV2_dQb_dVgsi,
&here->HSMHV2_dQb_dVbsi,
&here->HSMHV2_isub,
&here->HSMHV2_dIsub_dVdsi, &here->HSMHV2_dIsub_dVgsi,
&here->HSMHV2_dIsub_dVbsi,
&here->HSMHV2_Rd, &here->HSMHV2_Rs,
&here->HSMHV2_gm, &here->HSMHV2_gds, &here->HSMHV2_gmbs,
};
bool any_nan_out = false;
for (size_t i = 0; i < sizeof(outs)/sizeof(outs[0]); i++) {
if (isnan(*outs[i]) || isinf(*outs[i])) {
any_nan_out = true;
*outs[i] = 0.0;
}
}
if (any_nan_out && ckt->CKTosdiStepReject == 0)
ckt->CKTosdiStepReject = 1;
}
#ifdef DEBUG_HISIMHVCGG #ifdef DEBUG_HISIMHVCGG
printf("HSMHV2_ids %e ", here->HSMHV2_ids ) ; printf("HSMHV2_ids %e ", here->HSMHV2_ids ) ;
printf("HSMHV2_cggb %e ", here->HSMHV2_cggb ) ; printf("HSMHV2_cggb %e ", here->HSMHV2_cggb ) ;

View File

@ -380,6 +380,13 @@ char *INPdomodel(CKTcircuit *ckt, struct card *image, INPtables * tab)
err = tprintf("Device type HiSIMHV version %s not available in this binary\n", ver); err = tprintf("Device type HiSIMHV version %s not available in this binary\n", ver);
} }
break; break;
case 72: /* BSIM-CMG (OSDI) — FinFET CMC standard model */
type = INPtypelook("bsimcmg_va");
if (type < 0) {
err = INPmkTemp
("Device type bsimcmg_va not available - load bsimcmg.osdi before .model\n");
}
break;
case 77: /* BSIM-BULK (OSDI) */ case 77: /* BSIM-BULK (OSDI) */
type = INPtypelook("bsimbulk"); type = INPtypelook("bsimbulk");
if (type < 0) { if (type < 0) {
@ -389,7 +396,7 @@ char *INPdomodel(CKTcircuit *ckt, struct card *image, INPtables * tab)
break; break;
default: /* placeholder; use level xxx for the next model */ default: /* placeholder; use level xxx for the next model */
err = INPmkTemp err = INPmkTemp
("Only MOS device levels 1-6,8-10,14,49,54-58,60,68,73,77 are supported in this binary\n"); ("Only MOS device levels 1-6,8-10,14,49,54-58,60,68,72,73,77 are supported in this binary\n");
break; break;
} }
INPmakeMod(modname, type, image); INPmakeMod(modname, type, image);

View File

@ -17,6 +17,46 @@ Modified: 2000 AlansFixes
#include "ngspice/fteext.h" #include "ngspice/fteext.h"
/* Track unknown option names that have already been warned about so that
* large PDK libraries (which repeat the same .option lines many times) do
* not flood stderr with identical messages. */
static struct {
char *names[64];
int count;
} warned_opts;
static bool
unknown_opt_first_seen(const char *name)
{
int i;
for (i = 0; i < warned_opts.count; i++)
if (strcmp(warned_opts.names[i], name) == 0)
return false;
if (warned_opts.count < 64)
warned_opts.names[warned_opts.count++] = copy(name);
return true;
}
/* Advance line past an optional '=value' token so that the value string
* is not mistakenly parsed as the next option keyword. */
static void
skip_opt_value(char **line)
{
while (isspace((unsigned char)**line)) (*line)++;
if (**line != '=')
return;
(*line)++;
while (isspace((unsigned char)**line)) (*line)++;
char *endp;
strtod(*line, &endp);
if (endp > *line) {
*line = endp;
} else {
while (**line && !isspace((unsigned char)**line)) (*line)++;
}
}
void void
INPdoOpts( INPdoOpts(
CKTcircuit *ckt, CKTcircuit *ckt,
@ -26,7 +66,7 @@ INPdoOpts(
{ {
char *line; char *line;
char *token; char *token;
char *errmsg; char *errmsg; /* used for unimplemented-option and can't-set-option warnings */
IFvalue *val; IFvalue *val;
int error; int error;
int which; int which;
@ -67,14 +107,57 @@ INPdoOpts(
} }
continue; continue;
} }
/* print err message only if it is not just a number */
/* geoshrink: obsolete. The MOSFET W/L shrink is now applied
* generically from the subcircuit's `scale` parameter during
* subckt expansion (see subckt.c), matching HSPICE. Decks that
* still carry `.option geoshrink=<val>` are accepted but the
* value is ignored silently consume it so it isn't reparsed. */
if (strcmp(token, "geoshrink") == 0) {
while (isspace((unsigned char)*line)) line++;
char *endp;
(void) strtod(line, &endp);
if (endp > line)
line = endp;
continue;
}
/* Skip '=value' so the value string is not re-parsed as an option. */
skip_opt_value(&line);
/* Warn only for non-numeric tokens, and only the first time each
* unknown option name is encountered. Do NOT set optCard->error:
* unknown options are silently ignored, not fatal card errors.
*
* INPgetTok(gobble=1) consumes the '=' before returning, so
* skip_opt_value above cannot see it and may leave a string value
* token as the next token in line. Numparam substitution replaces
* string literals with 'numparm__________XXXXXXXX' placeholders;
* these are artifacts, not real option names discard silently. */
if (strncmp(token, "numparm__________", 17) == 0)
continue;
char* ctoken = token; char* ctoken = token;
while (*ctoken && strchr("0123456789.e+-", *ctoken)) while (*ctoken && strchr("0123456789.e+-", *ctoken))
ctoken++; ctoken++;
if (*ctoken) { if (*ctoken && unknown_opt_first_seen(token)) {
errmsg = tprintf("Error: unknown option %s - ignored\n", token); /* Recognised-but-unimplemented HSPICE options get a softer
optCard->error = INPerrCat(optCard->error, errmsg); * "Warning:" prefix so user scripts that intentionally rely
fprintf(stderr, "%s\n", optCard->error); * on these features can distinguish them from real typos.
* Add to this list when a new known-pending HSPICE option
* appears in foundry decks. */
static const char *const known_hspice_pending[] = {
"tmiflag", "modmonte", "tmipath", "etmiusrinput",
NULL
};
const char *const *p = known_hspice_pending;
bool known_pending = false;
for (; *p; p++) {
if (strcasecmp(token, *p) == 0) { known_pending = true; break; }
}
if (known_pending)
fprintf(stderr, "Warning: unknown option %s - ignored\n", token);
else
fprintf(stderr, "Error: unknown option %s - ignored\n", token);
} }
} }
} }

View File

@ -20,6 +20,68 @@ Author: 1985 Thomas L. Quarles
#include "ngspice/fteext.h" #include "ngspice/fteext.h"
#include "inpxx.h" #include "inpxx.h"
/* Side-channel from INPevaluate (inpeval.c) — when INPevaluate
* resolves a bare identifier as a .param value (HSPICE-compat path),
* it stashes the parameter's name here so INPdevParse can record a
* binding for later .dc-by-param sweeping. Cleared by INPevaluate
* itself at every entry, so non-bare-param paths never see a stale
* value. Read once and ignore otherwise. */
extern char *inpeval_last_param_name;
/* dpar_param_binding records "this V/I source's DC value was set
* from .param paramName at parse time" — used by the .dc analysis
* loop to push a swept value through to dependent sources.
*
* Owned strings: param_name + dev_name (allocated by us).
* Lifetime: file-scope list; never freed in practice (one entry per
* V/I source that uses a bare-param leading value typically a
* handful per deck). */
typedef struct dpar_param_binding {
char *param_name;
char *dev_name;
int dev_type; /* CKTtypelook code: VSRC or ISRC */
struct dpar_param_binding *next;
} dpar_param_binding_t;
static dpar_param_binding_t *dpar_binding_head = NULL;
void dpar_register_binding(const char *param_name,
const char *dev_name, int dev_type) {
if (!param_name || !dev_name) return;
for (dpar_param_binding_t *p = dpar_binding_head; p; p = p->next) {
if (p->dev_type == dev_type &&
strcmp(p->param_name, param_name) == 0 &&
strcmp(p->dev_name, dev_name) == 0)
return; /* already registered — netlist re-parse */
}
dpar_param_binding_t *b = TMALLOC(dpar_param_binding_t, 1);
b->param_name = copy(param_name);
b->dev_name = copy(dev_name);
b->dev_type = dev_type;
b->next = dpar_binding_head;
dpar_binding_head = b;
}
/* Public accessor — called by dctrcurv.c. Caller iterates bindings
* for a given param_name and updates the source's DC value field
* directly. */
dpar_param_binding_t *dpar_first_param_binding(void) {
return dpar_binding_head;
}
const char *dpar_binding_param_name(const dpar_param_binding_t *b) {
return b ? b->param_name : NULL;
}
const char *dpar_binding_dev_name(const dpar_param_binding_t *b) {
return b ? b->dev_name : NULL;
}
int dpar_binding_dev_type(const dpar_param_binding_t *b) {
return b ? b->dev_type : -1;
}
const dpar_param_binding_t *dpar_binding_next(const dpar_param_binding_t *b) {
return b ? b->next : NULL;
}
static IFparm * static IFparm *
find_instance_parameter(char *name, IFdevice *device) find_instance_parameter(char *name, IFdevice *device)
{ {
@ -53,6 +115,7 @@ INPdevParse(char **line, CKTcircuit *ckt, int dev, GENinstance *fast,
/* check for leading value */ /* check for leading value */
*waslead = 0; *waslead = 0;
inpeval_last_param_name = NULL;
*leading = INPevaluate(line, &error, 1); *leading = INPevaluate(line, &error, 1);
if (error == 0) /* found a good leading number */ if (error == 0) /* found a good leading number */
@ -60,6 +123,16 @@ INPdevParse(char **line, CKTcircuit *ckt, int dev, GENinstance *fast,
else else
*leading = 0.0; *leading = 0.0;
/* If INPevaluate resolved the leading value from a bare .param
* identifier (HSPICE-compat path), register a binding so the
* .dc analysis loop can push a swept value through to this
* device's DC field. fast->GENname carries the device's
* canonical instance name (lowercased, e.g. "vgs"). */
if (*waslead && inpeval_last_param_name && fast && fast->GENname) {
dpar_register_binding(inpeval_last_param_name, fast->GENname, dev);
}
inpeval_last_param_name = NULL;
wordlist *x = fast->GENmodPtr->defaults; wordlist *x = fast->GENmodPtr->defaults;
for (; x; x = x->wl_next->wl_next) { for (; x; x = x->wl_next->wl_next) {
char *parameter = x->wl_word; char *parameter = x->wl_word;
@ -125,10 +198,29 @@ INPdevParse(char **line, CKTcircuit *ckt, int dev, GENinstance *fast,
if (!p) { if (!p) {
if (eq(parm, "$")) { if (eq(parm, "$")) {
errbuf = copy(" unknown parameter ($). Check the compatibility flag!\n"); errbuf = copy(" unknown parameter ($). Check the compatibility flag!\n");
rtn = errbuf;
goto quit;
} }
else { /* OSDI models may receive extra instance parameters from PDK
errbuf = tprintf(" unknown parameter (%s) \n", parm); * subcircuits (e.g. 'total' from TSMC 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) {
while (isspace((unsigned char)**line)) (*line)++;
if (**line == '=') {
(*line)++;
while (isspace((unsigned char)**line)) (*line)++;
char *endp;
strtod(*line, &endp);
if (endp > *line)
*line = endp;
else
while (**line && !isspace((unsigned char)**line)) (*line)++;
}
FREE(parm);
continue;
} }
errbuf = tprintf(" unknown parameter (%s) \n", parm);
rtn = errbuf; rtn = errbuf;
goto quit; goto quit;
} }

View File

@ -9,6 +9,16 @@ Author: 1985 Thomas L. Quarles
#include "ngspice/inpdefs.h" #include "ngspice/inpdefs.h"
#include "inpxx.h" #include "inpxx.h"
/* Side channel for INPdevParse (inpdpar.c). When INPevaluate's
* HSPICE-compat path resolves a bare .param identifier, this points
* at the resolved name. INPdevParse reads it once after each
* INPevaluate call and registers a parse-time binding for later
* .dc-by-param sweeping; clears to NULL after reading. All other
* INPevaluate code paths leave this NULL (callers should still
* defensively assume any non-NULL value belongs to the most recent
* call). */
char *inpeval_last_param_name = NULL;
double double
INPevaluate(char **line, int *error, int gobble) INPevaluate(char **line, int *error, int gobble)
@ -16,11 +26,7 @@ INPevaluate(char **line, int *error, int gobble)
{ {
char *token; char *token;
char *here; char *here;
double mantis;
int expo1;
int expo2;
int sign; int sign;
int expsgn;
char *tmpline; char *tmpline;
/* setup */ /* setup */
@ -37,11 +43,7 @@ INPevaluate(char **line, int *error, int gobble)
*error = 0; *error = 0;
} }
mantis = 0;
expo1 = 0;
expo2 = 0;
sign = 1; sign = 1;
expsgn = 1;
/* loop through all of the input token */ /* loop through all of the input token */
here = token; here = token;
@ -54,6 +56,58 @@ INPevaluate(char **line, int *error, int gobble)
} }
if ((*here == '\0') || ((!(isdigit_c(*here))) && (*here != '.'))) { if ((*here == '\0') || ((!(isdigit_c(*here))) && (*here != '.'))) {
/* HSPICE-compat: token doesn't look like a number — try to
* resolve it as a bare .param identifier from the global
* numparam dictionary. Lets parser callers like
* vgs n2 n3 vgswp
* .dc vgswp -0.1 vgmax 0.01
* accept bare `.param` names where they expect a numeric
* value. `{vgswp}` / `'vgswp'` already work via the
* single-to-brace quote pass this just removes the
* "must be quoted" requirement.
*
* Identifier guard: starts with letter or underscore, then
* letters / digits / underscore. Any other character (e.g.
* leading '$' or operator) falls through to the unchanged
* error path.
*
* On success, also stash the resolved name in
* `inpeval_last_param_name` so INPdevParse can register a
* binding for later .dc-by-param sweeping. */
char *id = here;
if ((*id >= 'A' && *id <= 'Z') ||
(*id >= 'a' && *id <= 'z') || *id == '_') {
char *id_end = id;
while ((*id_end >= 'A' && *id_end <= 'Z') ||
(*id_end >= 'a' && *id_end <= 'z') ||
(*id_end >= '0' && *id_end <= '9') ||
*id_end == '_')
id_end++;
if (*id_end == '\0') {
extern int nupa_get_real(const char *name, double *value);
double pv;
if (nupa_get_real(id, &pv)) {
/* Stash a copy of the resolved name. Static
* buffer is fine here: INPevaluate is the
* single producer, and consumers (INPdevParse)
* read it synchronously before the next call. */
static char last_name_buf[128];
size_t len = (size_t)(id_end - id);
if (len >= sizeof last_name_buf)
len = sizeof last_name_buf - 1;
memcpy(last_name_buf, id, len);
last_name_buf[len] = '\0';
extern char *inpeval_last_param_name;
inpeval_last_param_name = last_name_buf;
if (gobble) {
FREE(token);
} else {
*line = id_end;
}
return sign * pv;
}
}
}
/* number looks like just a sign! */ /* number looks like just a sign! */
*error = 1; *error = 1;
if (gobble) { if (gobble) {
@ -64,142 +118,69 @@ INPevaluate(char **line, int *error, int gobble)
return (0); return (0);
} }
while (isdigit_c(*here)) { /* Parse the numeric magnitude with strtod so the result is the
/* digit, so accumulate it. */ * correctly-rounded IEEE-754 value. This matches formula()'s
mantis = 10 * mantis + *here - '0'; * fetchnumber() (xpressn.c, sscanf "%lG") to the bit and removes the
here++; * 1-ULP drift the old digit-by-digit accumulator + pow(10, expo)
} * produced for long mantissas. `here` is positioned just past any
* leading sign, on a digit or '.', so strtod sees the unsigned
* magnitude and `sign` is applied separately. A `:` (ternary) or any
* other non-numeric character naturally terminates strtod, preserving
* the old early-out behaviour. */
{
char *numend;
double value = strtod(here, &numend);
if (*here == '\0') { /* Fortran-style 'D'/'d' exponent (e.g. 1.5D3) — strtod stops at
/* reached the end of token - done. */ * the 'D', so finish the exponent by hand to keep legacy decks
if (gobble) { * working (the old code accepted D/d as an exponent marker). */
FREE(token); if (numend != here && (*numend == 'D' || *numend == 'd')) {
} else { char *expend;
*line = here; long fexp = strtol(numend + 1, &expend, 10);
} if (expend != numend + 1) {
return ((double) mantis * sign); value *= pow(10.0, (double) fexp);
} numend = expend;
if (*here == ':') {
/* ':' is no longer used for subcircuit node numbering
but is part of ternary function a?b:c
FIXME : subcircuit models still use ':' for model numbering
Will this hurt somewhere? */
if (gobble) {
FREE(token);
} else {
*line = here;
}
return ((double) mantis * sign);
}
/* after decimal point! */
if (*here == '.') {
/* found a decimal point! */
here++; /* skip to next character */
if (*here == '\0') {
/* number ends in the decimal point */
if (gobble) {
FREE(token);
} else {
*line = here;
} }
return ((double) mantis * sign);
} }
while (isdigit_c(*here)) { here = numend; /* at the scale-factor suffix, or token end */
/* digit, so accumulate it. */
mantis = 10 * mantis + *here - '0';
expo1 = expo1 - 1;
here++;
}
}
/* now look for "E","e",etc to indicate an exponent */ /* Scale factor (alphabetic suffix). As before, the suffix
if ((*here == 'E') || (*here == 'e') || (*here == 'D') || (*here == 'd')) { * character is NOT consumed from the line: `here` is left pointing
* at it. */
/* have an exponent, so skip the e */ double scale = 1.0;
here++; switch (*here) {
case 't': case 'T': scale = 1.0e12; break;
/* now look for exponent sign */ case 'g': case 'G': scale = 1.0e9; break;
if (*here == '+') case 'k': case 'K': scale = 1.0e3; break;
here++; /* just skip + */ case 'u': case 'U': scale = 1.0e-6; break;
else if (*here == '-') { case 'n': case 'N': scale = 1.0e-9; break;
here++; /* skip over minus sign */ case 'p': case 'P': scale = 1.0e-12; break;
expsgn = -1; /* and make a negative exponent */ case 'f': case 'F': scale = 1.0e-15; break;
/* now look for the digits of the exponent */ case 'a': case 'A': scale = 1.0e-18; break;
case 'm': case 'M':
if (((here[1] == 'E') || (here[1] == 'e')) &&
((here[2] == 'G') || (here[2] == 'g'))) {
scale = 1.0e6; /* Meg */
} else if (((here[1] == 'I') || (here[1] == 'i')) &&
((here[2] == 'L') || (here[2] == 'l'))) {
scale = 25.4e-6; /* Mil */
} else {
scale = 1.0e-3; /* m, milli */
}
break;
default:
break;
} }
while (isdigit_c(*here)) { if (gobble) {
expo2 = 10 * expo2 + *here - '0'; FREE(token);
here++;
}
}
/* now we have all of the numeric part of the number, time to
* look for the scale factor (alphabetic)
*/
switch (*here) {
case 't':
case 'T':
expo1 = expo1 + 12;
break;
case 'g':
case 'G':
expo1 = expo1 + 9;
break;
case 'k':
case 'K':
expo1 = expo1 + 3;
break;
case 'u':
case 'U':
expo1 = expo1 - 6;
break;
case 'n':
case 'N':
expo1 = expo1 - 9;
break;
case 'p':
case 'P':
expo1 = expo1 - 12;
break;
case 'f':
case 'F':
expo1 = expo1 - 15;
break;
case 'a':
case 'A':
expo1 = expo1 - 18;
break;
case 'm':
case 'M':
if (((here[1] == 'E') || (here[1] == 'e')) &&
((here[2] == 'G') || (here[2] == 'g')))
{
expo1 = expo1 + 6; /* Meg */
} else if (((here[1] == 'I') || (here[1] == 'i')) &&
((here[2] == 'L') || (here[2] == 'l')))
{
expo1 = expo1 - 6;
mantis *= 25.4; /* Mil */
} else { } else {
expo1 = expo1 - 3; /* m, milli */ *line = here;
} }
break;
default:
break;
}
if (gobble) { return sign * value * scale;
FREE(token);
} else {
*line = here;
} }
return (sign * mantis *
pow(10.0, (double) (expo1 + expsgn * expo2)));
} }

View File

@ -14,6 +14,7 @@ Modified: 2001 Paolo Nenzi (Cider Integration)
#include "ngspice/fteext.h" #include "ngspice/fteext.h"
#include "ngspice/compatmode.h" #include "ngspice/compatmode.h"
#include "ngspice/devdefs.h" #include "ngspice/devdefs.h"
#include "ngspice/osdi_defer.h"
#include "inpxx.h" #include "inpxx.h"
#include <errno.h> #include <errno.h>
#include <stdio.h> #include <stdio.h>
@ -115,13 +116,39 @@ create_model(CKTcircuit *ckt, INPmodel *modtmp, INPtables *tab)
INPgetNetTok(&line, &parm, 1); /* throw away 'modname' */ INPgetNetTok(&line, &parm, 1); /* throw away 'modname' */
tfree(parm); tfree(parm);
bool is_osdi = false;
#ifdef OSDI #ifdef OSDI
/* osdi models don't accept their device type as an argument */ /* osdi models don't accept their device type as an argument */
bool is_osdi = false; if (device->registry_entry) {
if (device->registry_entry){ /* Skip the OSDI device type (the module name). We must stop at '('
INPgetNetTok(&line, &parm, 1); /* throw away osdi */ * as well as whitespace: a no-space card like
* .model m slew_probe(max_pos=500, max_neg=-500)
* is standard SPICE syntax, but INPgetNetTok does NOT treat '(' as a
* token terminator, so it would grab "slew_probe(max_pos" as the
* "type" and silently swallow the FIRST model parameter. Extract just
* the type name, stopping at the first '(', '=', ',' or whitespace, and
* leave `line` pointing at the parameter list. */
while (*line == ' ' || *line == '\t')
line++;
char *type_start = line;
while (*line && *line != ' ' && *line != '\t' && *line != '(' &&
*line != '=' && *line != ',')
line++;
parm = copy_substring(type_start, line);
bool is_pmos = (strcasecmp(parm, "pmos") == 0 || strcasecmp(parm, "psoi") == 0);
tfree(parm); tfree(parm);
is_osdi = true; is_osdi = true;
/* Commercial simulators handle nmos/pmos polarity implicitly. For OSDI
* models with a 'type' parameter (e.g. BSIM-BULK TYPE=1/-1), inject -1
* before card params are parsed so the PDK can still override explicitly. */
if (is_pmos) {
IFparm *type_parm = find_model_parameter("type", device);
if (type_parm && (type_parm->dataType & IF_INTEGER)) {
IFvalue type_val;
type_val.iValue = -1;
ft_sim->setModelParm(ckt, modtmp->INPmodfast, type_parm->id, &type_val, NULL);
}
}
} }
#endif #endif
@ -184,7 +211,11 @@ create_model(CKTcircuit *ckt, INPmodel *modtmp, INPtables *tab)
perror("strtod"); perror("strtod");
controlled_exit(EXIT_FAILURE); controlled_exit(EXIT_FAILURE);
} }
if (endptr == parm) /* it was no number - it is really a string */ /* For OSDI models, PDK cards converted from HSPICE often contain
* simulator-specific parameters (tmemod, psatbmod, b0, etc.) that
* the public OSDI model binary does not implement. Silently skip
* them rather than flooding the output with "Model issue" lines. */
if (endptr == parm && !is_osdi)
err = INPerrCat(err, err = INPerrCat(err,
tprintf("unrecognized parameter (%s) - ignored", tprintf("unrecognized parameter (%s) - ignored",
parm)); parm));
@ -274,9 +305,22 @@ INPgetModBin(CKTcircuit *ckt, char *name, INPmodel **model, INPtables *tab, char
*model = NULL; *model = NULL;
/* read W and L. If not on the instance line, leave */ /* Read L (required) and W (optional). FinFET PDKs (Samsung 14LPU
if (!parse_line(line, instance_tokens, 2, parse_values, parse_found)) * et al.) have no W on the instance line only `l=` and
return NULL; * `nfin=` and their `.model` cards bin on `lmin/lmax/nfinmin/
* nfinmax`, not on W. Previously this function bailed out as
* soon as W was missing, which prevented bin selection for any
* FinFET model. Now: require only L, and if W is absent set it
* to 0 (any model with a real `wmin/wmax` range still gets a
* sensible has_w_range==true check and rejects this instance,
* which matches HSPICE's behaviour of "instance W=0 ≠ any
* wmin/wmax range starting above 0"). */
if (!parse_line(line, instance_tokens, 1, parse_values, parse_found))
return NULL; /* No `l=` either — really not a MOSFET-shape line. */
/* Now try to read W; if absent, w stays at parse_values[1]=0. */
parse_values[1] = 0.;
parse_line(line, instance_tokens, 2, parse_values, parse_found);
/* This is for reading nf. If nf is not available, set to 1 if in HSPICE or Spectre compatibility mode */ /* This is for reading nf. If nf is not available, set to 1 if in HSPICE or Spectre compatibility mode */
if (!parse_line(line, instance_tokens, 3, parse_values, parse_found)) { if (!parse_line(line, instance_tokens, 3, parse_values, parse_found)) {
@ -301,13 +345,30 @@ INPgetModBin(CKTcircuit *ckt, char *name, INPmodel **model, INPtables *tab, char
l = parse_values[0] * scale; l = parse_values[0] * scale;
w = parse_values[1] / parse_values[2] * scale; w = parse_values[1] / parse_values[2] * scale;
/* 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
* 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. */
for (modtmp = modtab; modtmp; modtmp = modtmp->INPnextModel) { for (modtmp = modtab; modtmp; modtmp = modtmp->INPnextModel) {
if (model_name_match(name, modtmp->INPmodName) < 2) if (model_name_match(name, modtmp->INPmodName) < 2)
continue; continue;
/* skip if not binnable */ /* if illegal device type, skip this candidate rather than aborting */
if (modtmp->INPmodType != INPtypelook("BSIM3") && if (modtmp->INPmodType < 0)
continue;
/* skip if not binnable: accept OSDI models (detected via registry_entry)
* as well as the classic BSIM/HiSIM families that have always been supported */
IFdevice *dev = ft_sim->devices[modtmp->INPmodType];
bool is_osdi = (dev->registry_entry != NULL);
if (!is_osdi &&
modtmp->INPmodType != INPtypelook("BSIM3") &&
modtmp->INPmodType != INPtypelook("BSIM3v32") && modtmp->INPmodType != INPtypelook("BSIM3v32") &&
modtmp->INPmodType != INPtypelook("BSIM3v0") && modtmp->INPmodType != INPtypelook("BSIM3v0") &&
modtmp->INPmodType != INPtypelook("BSIM3v1") && modtmp->INPmodType != INPtypelook("BSIM3v1") &&
@ -322,19 +383,38 @@ INPgetModBin(CKTcircuit *ckt, char *name, INPmodel **model, INPtables *tab, char
continue; continue;
} }
/* if illegal device type */ /* parse lmin/lmax from the model line; wmin/wmax are optional.
if (modtmp->INPmodType < 0) { * parse_line returns FALSE if any requested token is absent, so call
*model = NULL; * it unconditionally and inspect parse_found[] individually. */
return tprintf("Unknown device type for model %s\n", name); parse_line(modtmp->INPmodLine->line, model_tokens, 4, parse_values, parse_found);
}
if (!parse_line(modtmp->INPmodLine->line, model_tokens, 4, parse_values, parse_found)) if (!parse_found[0] || !parse_found[1])
continue; continue; /* lmin/lmax are required for bin selection */
lmin = parse_values[0]; lmax = parse_values[1]; lmin = parse_values[0]; lmax = parse_values[1];
wmin = parse_values[2]; wmax = parse_values[3];
if (in_range(l, lmin, lmax) && in_range(w, wmin, wmax)) { bool has_w_range = parse_found[2] && parse_found[3];
if (has_w_range) {
wmin = parse_values[2]; wmax = parse_values[3];
}
/* 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
* = 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
* the W/L parsed here are already EFFECTIVE no shrink factor is
* applied at this point.
*
* `m` is intentionally NOT in the calculation it is a
* parallel-instance multiplier (the whole device is replicated
* m times), so per-finger geometry is unchanged by it. */
double w_cmp = w;
double l_cmp = l;
if (in_range(l_cmp, lmin, lmax) && (!has_w_range || in_range(w_cmp, wmin, wmax))) {
/* create unless model is already defined */ /* create unless model is already defined */
if (!modtmp->INPmodfast) { if (!modtmp->INPmodfast) {
int error = create_model(ckt, modtmp, tab); int error = create_model(ckt, modtmp, tab);
@ -343,10 +423,74 @@ INPgetModBin(CKTcircuit *ckt, char *name, INPmodel **model, INPtables *tab, char
} }
*model = modtmp; *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
* `(l==14n)*X + (l==16n)*Y` evaluate to non-zero. */
if (is_osdi)
osdi_defer_record_bin_range(modtmp->INPmodName, lmin, lmax);
return NULL; return NULL;
} }
} }
/* Second pass for OSDI models: if no exact bin match was found (device W
* is outside all bin width ranges), pick the l-matching bin whose wmin/wmax
* is closest to the device width. This mirrors HSPICE/Spectre behaviour. */
{
double best_w_dist = -1.0; /* negative = "no candidate yet" */
INPmodel *best_mod = NULL;
for (modtmp = modtab; modtmp; modtmp = modtmp->INPnextModel) {
if (model_name_match(name, modtmp->INPmodName) < 2)
continue;
if (modtmp->INPmodType < 0)
continue;
IFdevice *dev = ft_sim->devices[modtmp->INPmodType];
if (dev->registry_entry == NULL)
continue; /* nearest-bin fallback only for OSDI models */
parse_line(modtmp->INPmodLine->line, model_tokens, 4, parse_values, parse_found);
if (!parse_found[0] || !parse_found[1])
continue;
lmin = parse_values[0]; lmax = parse_values[1];
/* Same convention as the primary pass: the W/L parsed here
* are already EFFECTIVE (shrink applied upstream). */
if (!in_range(l, lmin, lmax))
continue;
double w_c = w;
double w_dist;
if (!parse_found[2] || !parse_found[3]) {
w_dist = 0.0;
} else {
wmin = parse_values[2]; wmax = parse_values[3];
if (w_c < wmin) w_dist = wmin - w_c;
else if (w_c > wmax) w_dist = w_c - wmax;
else w_dist = 0.0;
}
if (best_w_dist < 0.0 || w_dist < best_w_dist) {
best_w_dist = w_dist;
best_mod = modtmp;
}
}
if (best_mod) {
fprintf(stderr,
"Warning: OSDI model '%s': device W=%.3g is outside all bin"
" width ranges; using nearest bin\n",
name, w);
if (!best_mod->INPmodfast) {
int error = create_model(ckt, best_mod, tab);
if (error)
return NULL;
}
*model = best_mod;
}
}
return NULL; return NULL;
} }

View File

@ -0,0 +1,23 @@
* absdelay test
.model adm addelay
n1 out in adm
rout out 0 1meg
vin in 0 PULSE(0 1 1n 10p 10p 2n 4n)
.control
pre_osdi absdelay.osdi
tran 50p 6n
meas tran vin_at_0_5 find v(in) at=0.5n
meas tran vout_at_0_5 find v(out) at=0.5n
meas tran vin_at_1_1 find v(in) at=1.1n
meas tran vout_at_1_1 find v(out) at=1.1n
meas tran vin_at_1_4 find v(in) at=1.4n
meas tran vout_at_1_4 find v(out) at=1.4n
meas tran vin_at_3_1 find v(in) at=3.1n
meas tran vout_at_3_1 find v(out) at=3.1n
meas tran vin_at_3_4 find v(in) at=3.4n
meas tran vout_at_3_4 find v(out) at=3.4n
print vin_at_0_5 vout_at_0_5 vin_at_1_1 vout_at_1_1 vin_at_1_4 vout_at_1_4 vin_at_3_1 vout_at_3_1 vin_at_3_4 vout_at_3_4
quit
.endc
.end

Binary file not shown.

View File

@ -0,0 +1,11 @@
`include "disciplines.vams"
module addelay(out, in);
inout out, in;
electrical out, in;
parameter real td = 0.25n; // transport delay
parameter real maxtd = 1n; // ring-pruning bound
analog begin
V(out) <+ absdelay(V(in), td, maxtd);
end
endmodule

View File

@ -0,0 +1,12 @@
* at_cross test
.model ac at_cross_probe
n1 p ac
vp p 0 1
.control
pre_osdi at_cross.osdi
tran 10u 6m
print @n1[n_crossings] @n1[last_event_time]
quit
.endc
.end

Binary file not shown.

View File

@ -0,0 +1,27 @@
`include "disciplines.vams"
module at_cross_probe(p);
inout p;
electrical p;
real probe;
(*desc="probe expression value (sin(2pi*1k*t))"*)
real probe_out;
(*desc="number of rising zero crossings observed via @(cross)"*)
real n_crossings;
(*desc="last $abstime where @(cross) fired"*)
real last_event_time;
analog begin
probe = sin(2.0 * 3.14159265358979 * 1.0e3 * $abstime);
probe_out = probe;
@(cross(probe, 1)) begin
n_crossings = n_crossings + 1.0;
last_event_time = $abstime;
end
// Token contribution so the device stays in the node equations.
I(p) <+ 1.0e-6 * V(p);
end
endmodule

View File

@ -0,0 +1,12 @@
* at_timer test
.model at at_timer_probe
n1 p at
vp p 0 1
.control
pre_osdi at_timer.osdi
tran 10u 6m
print @n1[n_fires] @n1[last_event_time]
quit
.endc
.end

Binary file not shown.

View File

@ -0,0 +1,21 @@
`include "disciplines.vams"
module at_timer_probe(p);
inout p;
electrical p;
(*desc="periodic timer fire counter"*)
real n_fires;
(*desc="last abstime where @(timer) fired"*)
real last_event_time;
analog begin
@(timer(1.0e-3, 1.0e-3)) begin
n_fires = n_fires + 1.0;
last_event_time = $abstime;
end
// Token contribution so the device stays in the node equations.
I(p) <+ 1.0e-6 * V(p);
end
endmodule

View File

@ -0,0 +1,18 @@
#!/bin/bash
OPENVA=~/bin/openva
SPICE=/usr/share/ngspice_VA2/bin/ngspice
FILTER="SPARSE|KLU|CPU|Dynamic|Note|Circuit|Trying|Reference|Date|Doing|---|v-sweep|time|est|Error|Warning|Data|Index|trans|acan|oise|nalysis|ole|Total|memory|urrent|Got|Added|BSIM|bsim|B4SOI|b4soi|codemodel|^binary raw file|^ngspice.*done|Operating"
for f in *.va; do
name=$(basename "$f" .va)
echo "Compiling $f..."
$OPENVA -o "${name}.osdi" "$f"
done
for f in *.cir; do
name=$(basename "$f" .cir)
echo "Running $f..."
$SPICE --batch -r "${name}.raw" "$f" > "${name}.test"
egrep -v "$FILTER" "${name}.test" > "${name}.out"
rm -f "${name}.test" "${name}.raw"
done

View File

@ -0,0 +1,12 @@
* initial_final_step test
.model ifs initial_final_probe
n1 p ifs
vp p 0 1
.control
pre_osdi initial_final_step.osdi
tran 10u 6.5m
print @n1[cnt] @n1[is_final]
quit
.endc
.end

Binary file not shown.

View File

@ -0,0 +1,29 @@
`include "disciplines.vams"
module initial_final_probe(p);
inout p;
electrical p;
(*desc="counter initialized in initial_step"*)
real cnt;
(*desc="final step flag"*)
real is_final;
analog begin
@(initial_step) begin
cnt = 1.0;
end
@(timer(1.0e-3, 1.0e-3)) begin
cnt = cnt + 1.0;
end
@(final_step) begin
is_final = 1.0;
$strobe("FINAL_STEP executed at abstime=%g, cnt=%g\n", $abstime, cnt);
end
// Token contribution so the device stays in the node equations.
I(p) <+ 1.0e-6 * V(p);
end
endmodule

View File

@ -0,0 +1,22 @@
* laplace test
.model lp laplace_probe
n1 y_nd y_np y_zd y_zp in lp
vin in 0 dc 0 PULSE(0 1 0.1m 1u 1u 10m 20m)
ry1 y_nd 0 1meg
ry2 y_np 0 1meg
ry3 y_zd 0 1meg
ry4 y_zp 0 1meg
.control
pre_osdi laplace_filter.osdi
tran 10u 5m
meas tran ynd_at_1_1 find v(y_nd) at=1.1m
meas tran ynp_at_1_1 find v(y_np) at=1.1m
meas tran yzd_at_1_1 find v(y_zd) at=1.1m
meas tran yzp_at_1_1 find v(y_zp) at=1.1m
meas tran ynd_at_4_1 find v(y_nd) at=4.1m
meas tran yzp_at_4_1 find v(y_zp) at=4.1m
print ynd_at_1_1 ynp_at_1_1 yzd_at_1_1 yzp_at_1_1 ynd_at_4_1 yzp_at_4_1
quit
.endc
.end

Binary file not shown.

View File

@ -0,0 +1,19 @@
`include "disciplines.vams"
module laplace_probe(y_nd, y_np, y_zd, y_zp, in);
inout y_nd, y_np, y_zd, y_zp, in;
electrical y_nd, y_np, y_zd, y_zp, in;
analog begin
// strictly proper low-pass filter: H(s) = 1000 / (s + 1000)
V(y_nd) <+ laplace_nd(V(in), {1000.0}, {1000.0, 1.0});
V(y_np) <+ laplace_np(V(in), {1000.0}, {-1000.0, 0.0});
// strictly proper band-pass/low-pass: H(s) = (s + 2000) / ((s + 1000)(s + 3000))
// zeros: {-2000, 0}
// poles: {-1000, 0, -3000, 0}
// denom coeff: constant = 3e6, s^1 = 4000, s^2 = 1
V(y_zd) <+ laplace_zd(V(in), {-2000.0, 0.0}, {3.0e6, 4000.0, 1.0});
V(y_zp) <+ laplace_zp(V(in), {-2000.0, 0.0}, {-1000.0, 0.0, -3000.0, 0.0});
end
endmodule

View File

@ -0,0 +1,16 @@
* last_crossing test
.model lc last_crossing_probe
n1 p lc
vp p 0 1
.control
pre_osdi last_crossing.osdi
save all @n1[tcross_out]
tran 10u 2.5m
meas tran tcross_0_5 find @n1[tcross_out] at=0.5m
meas tran tcross_1_5 find @n1[tcross_out] at=1.5m
meas tran tcross_2_2 find @n1[tcross_out] at=2.2m
print tcross_0_5 tcross_1_5 tcross_2_2
quit
.endc
.end

Binary file not shown.

View File

@ -0,0 +1,24 @@
`include "disciplines.vams"
module last_crossing_probe(p);
inout p;
electrical p;
real tcross;
real expr;
(*desc="time of most recent rising sin(2pi*1k*t) crossing"*)
real tcross_out;
(*desc="last abstime the model saw eval()"*)
real tnow_out;
(*desc="probe expression value"*)
real expr_out;
analog begin
expr = sin(2.0 * 3.14159265358979 * 1.0e3 * $abstime);
tcross = last_crossing(expr, 1);
tcross_out = tcross;
tnow_out = $abstime;
expr_out = expr;
I(p) <+ 1.0e-6 * V(p);
end
endmodule

View File

@ -0,0 +1,20 @@
* limexp test
.model lp limexp_probe
n1 out in lp
vin in 0 dc 0
rout out 0 1meg
.control
pre_osdi limexp.osdi
alter vin dc 79.0
op
print v(out)
alter vin dc 80.0
op
print v(out)
alter vin dc 81.0
op
print v(out)
quit
.endc
.end

BIN
tests/regression/osdi/limexp.osdi Executable file

Binary file not shown.

View File

@ -0,0 +1,10 @@
`include "disciplines.vams"
module limexp_probe(out, in);
inout out, in;
electrical out, in;
analog begin
V(out) <+ limexp(V(in)) * 1e-34;
end
endmodule

View File

@ -0,0 +1,14 @@
* noise test
.model nt noise_test_probe
n1 p 0 nt
r1 in p 1k
vsrc in 0 dc 0 ac 1
.control
pre_osdi noise_test.osdi
noise v(p) vsrc dec 1 1 10meg
setplot noise1
print onoise_spectrum[0] onoise_spectrum[3] onoise_spectrum[6]
quit
.endc
.end

Binary file not shown.

View File

@ -0,0 +1,13 @@
`include "disciplines.vams"
module noise_test_probe(p, n);
inout p, n;
electrical p, n;
analog begin
I(p, n) <+ V(p, n) / 1000.0;
I(p, n) <+ white_noise(1e-20, "thermal_floor");
I(p, n) <+ flicker_noise(1e-15, 1.0, "flicker");
I(p, n) <+ noise_table({1.0, 1e-22, 1e3, 1e-21, 1e6, 1e-20}, "inline_table");
end
endmodule

View File

@ -0,0 +1,21 @@
* slew test
.model sl slew_probe(max_pos=500.0, max_neg=-500.0)
n1 out in sl
rout out 0 1meg
vin in 0 dc 0 PULSE(0 1 2m 1m 1m 5m 10m)
.control
pre_osdi slew.osdi
tran 50u 10m
meas tran vout_at_2 find v(out) at=2m
meas tran vout_at_2_5 find v(out) at=2.5m
meas tran vout_at_3 find v(out) at=3m
meas tran vout_at_4 find v(out) at=4m
meas tran vout_at_7 find v(out) at=7m
meas tran vout_at_7_5 find v(out) at=7.5m
meas tran vout_at_8 find v(out) at=8m
meas tran vout_at_9 find v(out) at=9m
print vout_at_2 vout_at_2_5 vout_at_3 vout_at_4 vout_at_7 vout_at_7_5 vout_at_8 vout_at_9
quit
.endc
.end

BIN
tests/regression/osdi/slew.osdi Executable file

Binary file not shown.

View File

@ -0,0 +1,12 @@
`include "disciplines.vams"
module slew_probe(out, in);
inout out, in;
electrical out, in;
parameter real max_pos = 1.0e3;
parameter real max_neg = -1.0e3;
analog begin
I(out) <+ (V(out) - slew(V(in), max_pos, max_neg)) / 1.0;
end
endmodule

View File

@ -0,0 +1,13 @@
* slew test const
.model sl slew_probe(max_pos=500.0, max_neg=-500.0)
n1 out in sl
rout out 0 1meg
vin in 0 dc 1.0
.control
pre_osdi slew.osdi
tran 50u 10m
print v(out)
quit
.endc
.end

View File

@ -0,0 +1,74 @@
* slew model-parameter application regression test
*
* Guards the OSDI .model parser bug (src/spicelib/parser/inpgmod.c) where the
* FIRST model parameter on a standard no-space card "type(p1=...,p2=...)" was
* silently dropped: the device-type skip used INPgetNetTok, whose tokenizer
* does NOT treat '(' as a terminator, so it grabbed "slew_probe(max_pos=..."
* as the "type" and ate max_pos. The dropped parameter then fell back to its
* module default (1e3), so the affected slew edge tracked the input instead of
* limiting.
*
* This test gives max_pos and max_neg DISTINCT values (400 / -700, both well
* below the 1e3 default) and lists them in BOTH orderings (sl_a: max_pos first,
* sl_b: max_neg first). The input slews at 2000 V/s -- far faster than either
* limit -- so both edges must clamp. The measured slew rate on each edge must
* equal the .model value; if a parameter were dropped that edge would run at
* the 1e3 default and the corresponding assertion fails. sl_a's rise asserts
* max_pos-when-first; sl_b's fall asserts max_neg-when-first -- i.e. the
* first-parameter-drop bug is caught regardless of which parameter is first.
.model sl_a slew_probe(max_pos=400.0, max_neg=-700.0)
.model sl_b slew_probe(max_neg=-700.0, max_pos=400.0)
na outa in sl_a
nb outb in sl_b
ra outa 0 1meg
rb outb 0 1meg
vin in 0 dc 0 PULSE(0 1 1m 0.5m 0.5m 3m 10m)
.control
pre_osdi slew.osdi
tran 20u 6m
* rising-edge slope in the limiting region (input held at 1): must be +max_pos
meas tran a_r2 find v(outa) at=2m
meas tran a_r3 find v(outa) at=3m
meas tran b_r2 find v(outb) at=2m
meas tran b_r3 find v(outb) at=3m
* falling-edge slope in the limiting region: must be max_neg
meas tran a_f5 find v(outa) at=5m
meas tran a_f55 find v(outa) at=5.5m
meas tran b_f5 find v(outb) at=5m
meas tran b_f55 find v(outb) at=5.5m
let a_rise = (a_r3 - a_r2) / 1e-3
let b_rise = (b_r3 - b_r2) / 1e-3
let a_fall = (a_f55 - a_f5) / 0.5e-3
let b_fall = (b_f55 - b_f5) / 0.5e-3
let fail = 0
if abs(a_rise - 400) > 20
echo "ERROR: model A (max_pos first) rise rate = $&a_rise V/s, expected +400 -- max_pos dropped?"
let fail = fail + 1
end
if abs(b_rise - 400) > 20
echo "ERROR: model B rise rate = $&b_rise V/s, expected +400"
let fail = fail + 1
end
if abs(a_fall + 700) > 35
echo "ERROR: model A fall rate = $&a_fall V/s, expected -700"
let fail = fail + 1
end
if abs(b_fall + 700) > 35
echo "ERROR: model B (max_neg first) fall rate = $&b_fall V/s, expected -700 -- max_neg dropped?"
let fail = fail + 1
end
if fail eq 0
echo "INFO: success -- both slew params apply (max_pos=400, max_neg=-700, both orderings)"
quit 0
else
echo "ERROR: $&fail slew model-parameter assertion(s) failed"
quit 1
end
.endc
.end

View File

@ -0,0 +1,12 @@
* test ddt
.model td test_ddt
n1 out in td
rout out 0 1meg
vin in 0 dc 1.0
.control
pre_osdi test_ddt.osdi
tran 10p 100p
print v(in) v(out)
quit
.endc
.end

Binary file not shown.

View File

@ -0,0 +1,9 @@
`include "disciplines.vams"
module test_ddt(out, in);
inout out, in;
electrical out, in, nx;
branch (nx) b;
analog begin
V(out) <+ ddt(V(in));
end
endmodule

View File

@ -0,0 +1,16 @@
* transition test
.model tr transition_probe
n1 p tr
.control
pre_osdi transition.osdi
tran 5u 7m
meas tran vp_at_1 find v(p) at=1m ; 0
meas tran vp_at_2_5 find v(p) at=2.5m ; 0.5
meas tran vp_at_4 find v(p) at=4m ; 1
meas tran vp_at_6_25 find v(p) at=6.25m ; 0.5
meas tran vp_at_6_8 find v(p) at=6.8m ; 0
print vp_at_1 vp_at_2_5 vp_at_4 vp_at_6_25 vp_at_6_8
quit
.endc
.end

Binary file not shown.

View File

@ -0,0 +1,13 @@
`include "disciplines.vams"
module transition_probe(p);
inout p;
electrical p;
real lvl;
analog begin
lvl = ($abstime >= 6.0e-3) ? 0.0 : (($abstime >= 2.0e-3) ? 1.0 : 0.0);
V(p) <+ transition(lvl, 0.0, 1.0e-3, 0.5e-3);
end
endmodule

View File

@ -0,0 +1,24 @@
* zi test
.model zp zi_probe
n1 y_nd y_zd y_np y_zp in zp
vin in 0 dc 1
ry1 y_nd 0 1meg
ry2 y_zd 0 1meg
ry3 y_np 0 1meg
ry4 y_zp 0 1meg
.control
pre_osdi zi_filter.osdi
tran 10u 4.5m
meas tran ynd_at_0_5m find v(y_nd) at=0.5m
meas tran yzd_at_0_5m find v(y_zd) at=0.5m
meas tran ynd_at_1_5m find v(y_nd) at=1.5m
meas tran yzd_at_1_5m find v(y_zd) at=1.5m
meas tran ynd_at_2_5m find v(y_nd) at=2.5m
meas tran yzd_at_2_5m find v(y_zd) at=2.5m
meas tran ynd_at_3_5m find v(y_nd) at=3.5m
meas tran yzd_at_3_5m find v(y_zd) at=3.5m
print ynd_at_0_5m yzd_at_0_5m ynd_at_1_5m yzd_at_1_5m ynd_at_2_5m yzd_at_2_5m ynd_at_3_5m yzd_at_3_5m
quit
.endc
.end

Binary file not shown.

View File

@ -0,0 +1,13 @@
`include "disciplines.vams"
module zi_probe(y_nd, y_zd, y_np, y_zp, in);
inout y_nd, y_zd, y_np, y_zp, in;
electrical y_nd, y_zd, y_np, y_zp, in;
analog begin
V(y_nd) <+ zi_nd(V(in), {1.0, 0.5}, {1.0, -0.5}, 1e-3);
V(y_zd) <+ zi_zd(V(in), {-0.5, 0.0}, {1.0, -0.5}, 1e-3);
V(y_np) <+ zi_np(V(in), {1.0, 0.5}, {0.5, 0.0}, 1e-3);
V(y_zp) <+ zi_zp(V(in), {-0.5, 0.0}, {0.5, 0.0}, 1e-3);
end
endmodule

View File

@ -55,6 +55,9 @@ copy %cmsrc%\tlines64.cm %dst%\lib\ngspice\tlines.cm
copy xspice\verilog\ivlng.dll %dst%\lib\ngspice\ivlng.dll copy xspice\verilog\ivlng.dll %dst%\lib\ngspice\ivlng.dll
copy xspice\verilog\shim.vpi %dst%\lib\ngspice\ivlng.vpi copy xspice\verilog\shim.vpi %dst%\lib\ngspice\ivlng.vpi
copy ..\..\libsndfile\bin\sndfile.dll %dst%\bin\
copy ..\..\libsamplerate\bin\samplerate.dll %dst%\bin\
if "%2" == "fftw" goto copy2-64 if "%2" == "fftw" goto copy2-64
if "%3" == "fftw" goto copy2-64 if "%3" == "fftw" goto copy2-64

View File

@ -55,6 +55,9 @@ copy %cmsrc%\tlines64.cm %dst%\lib\ngspice\tlines.cm
copy xspice\verilog\ivlng.dll %dst%\lib\ngspice\ivlng.dll copy xspice\verilog\ivlng.dll %dst%\lib\ngspice\ivlng.dll
copy xspice\verilog\shim.vpi %dst%\lib\ngspice\ivlng.vpi copy xspice\verilog\shim.vpi %dst%\lib\ngspice\ivlng.vpi
copy ..\..\libsndfile\bin\sndfile.dll %dst%\bin\
copy ..\..\libsamplerate\bin\samplerate.dll %dst%\bin\
if "%2" == "fftw" goto copy2-64 if "%2" == "fftw" goto copy2-64
if "%3" == "fftw" goto copy2-64 if "%3" == "fftw" goto copy2-64

View File

@ -305,7 +305,7 @@ lib /machine:x64 /def:..\..\fftw-3.3-dll64\libfftw3-3.def /out:$(IntDir)libfftw3
</Midl> </Midl>
<ClCompile> <ClCompile>
<Optimization>Disabled</Optimization> <Optimization>Disabled</Optimization>
<AdditionalIncludeDirectories>..\src\maths\poly;..\src\frontend;..\src\spicelib\devices;tmp-bison;src\include;..\src\spicelib\parser;..\src\include;..\src\include\cppduals;.;..\..\fftw-3.3-dll64;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> <AdditionalIncludeDirectories>..\..\libsamplerate\include;..\..\libsndfile\include;..\src\maths\poly;..\src\frontend;..\src\spicelib\devices;tmp-bison;src\include;..\src\spicelib\parser;..\src\include;..\src\include\cppduals;.;..\..\fftw-3.3-dll64;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PreprocessorDefinitions>_CRT_SECURE_NO_DEPRECATE;SIMULATOR;XSPICE;NGDEBUG;CONFIG64;HAVE_LIBFFTW3;%(PreprocessorDefinitions)</PreprocessorDefinitions> <PreprocessorDefinitions>_CRT_SECURE_NO_DEPRECATE;SIMULATOR;XSPICE;NGDEBUG;CONFIG64;HAVE_LIBFFTW3;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<MinimalRebuild>false</MinimalRebuild> <MinimalRebuild>false</MinimalRebuild>
<ExceptionHandling> <ExceptionHandling>
@ -323,7 +323,7 @@ lib /machine:x64 /def:..\..\fftw-3.3-dll64\libfftw3-3.def /out:$(IntDir)libfftw3
<AdditionalOptions>/openmp:llvm %(AdditionalOptions)</AdditionalOptions> <AdditionalOptions>/openmp:llvm %(AdditionalOptions)</AdditionalOptions>
</ClCompile> </ClCompile>
<Link> <Link>
<AdditionalDependencies>psapi.lib;KLU_COMPLEX.lib;$(IntDir)libfftw3-3.lib;%(AdditionalDependencies)</AdditionalDependencies> <AdditionalDependencies>sndfile.lib;samplerate.lib;psapi.lib;KLU_COMPLEX.lib;$(IntDir)libfftw3-3.lib;%(AdditionalDependencies)</AdditionalDependencies>
<GenerateDebugInformation>true</GenerateDebugInformation> <GenerateDebugInformation>true</GenerateDebugInformation>
<SubSystem>Windows</SubSystem> <SubSystem>Windows</SubSystem>
<HeapReserveSize>0</HeapReserveSize> <HeapReserveSize>0</HeapReserveSize>
@ -335,10 +335,12 @@ lib /machine:x64 /def:..\..\fftw-3.3-dll64\libfftw3-3.def /out:$(IntDir)libfftw3
</DataExecutionPrevention> </DataExecutionPrevention>
<TargetMachine>MachineX64</TargetMachine> <TargetMachine>MachineX64</TargetMachine>
<LargeAddressAware>true</LargeAddressAware> <LargeAddressAware>true</LargeAddressAware>
<AdditionalLibraryDirectories>KLU\x64\Debug;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories> <AdditionalLibraryDirectories>../../libsamplerate/lib;../../libsndfile/lib;KLU\x64\Debug;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
</Link> </Link>
<PostBuildEvent> <PostBuildEvent>
<Command> <Command>
copy /y "..\..\libsndfile\bin\sndfile.dll" "$(OutDir)"
copy /y "..\..\libsamplerate\bin\samplerate.dll" "$(OutDir)"
copy /y "..\..\fftw-3.3-dll64\libfftw3-3.dll" "$(OutDir)" copy /y "..\..\fftw-3.3-dll64\libfftw3-3.dll" "$(OutDir)"
make-install-vngspiced.bat $(OutDir) fftw 64 make-install-vngspiced.bat $(OutDir) fftw 64
</Command> </Command>
@ -358,7 +360,7 @@ lib /machine:x64 /def:..\..\fftw-3.3-dll64\libfftw3-3.def /out:$(IntDir)libfftw3
<IntrinsicFunctions>true</IntrinsicFunctions> <IntrinsicFunctions>true</IntrinsicFunctions>
<FavorSizeOrSpeed>Speed</FavorSizeOrSpeed> <FavorSizeOrSpeed>Speed</FavorSizeOrSpeed>
<WholeProgramOptimization>true</WholeProgramOptimization> <WholeProgramOptimization>true</WholeProgramOptimization>
<AdditionalIncludeDirectories>..\src\maths\poly;..\src\frontend;..\src\spicelib\devices;tmp-bison;src\include;..\src\spicelib\parser;..\src\include;..\src\include\cppduals;.;..\..\fftw-3.3-dll64;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> <AdditionalIncludeDirectories>..\..\libsamplerate\include;..\..\libsndfile\include;..\src\maths\poly;..\src\frontend;..\src\spicelib\devices;tmp-bison;src\include;..\src\spicelib\parser;..\src\include;..\src\include\cppduals;.;..\..\fftw-3.3-dll64;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PreprocessorDefinitions>_CRT_SECURE_NO_DEPRECATE;SIMULATOR;XSPICE;CONFIG64;HAVE_LIBFFTW3;%(PreprocessorDefinitions)</PreprocessorDefinitions> <PreprocessorDefinitions>_CRT_SECURE_NO_DEPRECATE;SIMULATOR;XSPICE;CONFIG64;HAVE_LIBFFTW3;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<MinimalRebuild>false</MinimalRebuild> <MinimalRebuild>false</MinimalRebuild>
<ExceptionHandling> <ExceptionHandling>
@ -376,7 +378,7 @@ lib /machine:x64 /def:..\..\fftw-3.3-dll64\libfftw3-3.def /out:$(IntDir)libfftw3
<AdditionalOptions>/openmp:llvm %(AdditionalOptions)</AdditionalOptions> <AdditionalOptions>/openmp:llvm %(AdditionalOptions)</AdditionalOptions>
</ClCompile> </ClCompile>
<Link> <Link>
<AdditionalDependencies>psapi.lib;KLU_COMPLEX.lib;$(IntDir)libfftw3-3.lib;%(AdditionalDependencies)</AdditionalDependencies> <AdditionalDependencies>sndfile.lib;samplerate.lib;psapi.lib;KLU_COMPLEX.lib;$(IntDir)libfftw3-3.lib;%(AdditionalDependencies)</AdditionalDependencies>
<GenerateDebugInformation>true</GenerateDebugInformation> <GenerateDebugInformation>true</GenerateDebugInformation>
<SubSystem>Windows</SubSystem> <SubSystem>Windows</SubSystem>
<HeapReserveSize>0</HeapReserveSize> <HeapReserveSize>0</HeapReserveSize>
@ -393,10 +395,13 @@ lib /machine:x64 /def:..\..\fftw-3.3-dll64\libfftw3-3.def /out:$(IntDir)libfftw3
</DataExecutionPrevention> </DataExecutionPrevention>
<TargetMachine>MachineX64</TargetMachine> <TargetMachine>MachineX64</TargetMachine>
<LargeAddressAware>true</LargeAddressAware> <LargeAddressAware>true</LargeAddressAware>
<AdditionalLibraryDirectories>KLU\x64\Release;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories> <AdditionalLibraryDirectories>../../libsamplerate/lib;../../libsndfile/lib;KLU\x64\Release;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
</Link> </Link>
<PostBuildEvent> <PostBuildEvent>
<Command> <Command>
copy /y "..\..\libsndfile\bin\sndfile.dll" "$(OutDir)"
copy /y "..\..\libsamplerate\bin\samplerate.dll" "$(OutDir)"
copy /y "..\..\fftw-3.3-dll64\libfftw3-3.dll" "$(OutDir)" copy /y "..\..\fftw-3.3-dll64\libfftw3-3.dll" "$(OutDir)"
make-install-vngspice.bat $(OutDir) fftw 64 make-install-vngspice.bat $(OutDir) fftw 64
</Command> </Command>
@ -511,7 +516,7 @@ lib /machine:x64 /def:..\..\fftw-3.3-dll64\libfftw3-3.def /out:$(IntDir)libfftw3
</Midl> </Midl>
<ClCompile> <ClCompile>
<Optimization>Disabled</Optimization> <Optimization>Disabled</Optimization>
<AdditionalIncludeDirectories>..\src\maths\poly;..\src\frontend;..\src\spicelib\devices;tmp-bison;src\include;..\src\spicelib\parser;..\src\include;..\src\include\cppduals;.;..\..\fftw-3.3-dll64;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> <AdditionalIncludeDirectories>..\..\libsamplerate\include;..\..\libsndfile\include;..\src\maths\poly;..\src\frontend;..\src\spicelib\devices;tmp-bison;src\include;..\src\spicelib\parser;..\src\include;..\src\include\cppduals;.;..\..\fftw-3.3-dll64;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PreprocessorDefinitions>_CRT_SECURE_NO_DEPRECATE;SIMULATOR;XSPICE;NGDEBUG;CONSOLE;CONFIG64;HAVE_LIBFFTW3;%(PreprocessorDefinitions)</PreprocessorDefinitions> <PreprocessorDefinitions>_CRT_SECURE_NO_DEPRECATE;SIMULATOR;XSPICE;NGDEBUG;CONSOLE;CONFIG64;HAVE_LIBFFTW3;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<MinimalRebuild>false</MinimalRebuild> <MinimalRebuild>false</MinimalRebuild>
<ExceptionHandling> <ExceptionHandling>
@ -529,7 +534,7 @@ lib /machine:x64 /def:..\..\fftw-3.3-dll64\libfftw3-3.def /out:$(IntDir)libfftw3
<AdditionalOptions>/openmp:llvm %(AdditionalOptions)</AdditionalOptions> <AdditionalOptions>/openmp:llvm %(AdditionalOptions)</AdditionalOptions>
</ClCompile> </ClCompile>
<Link> <Link>
<AdditionalDependencies>psapi.lib;KLU_COMPLEX.lib;$(IntDir)libfftw3-3.lib;%(AdditionalDependencies)</AdditionalDependencies> <AdditionalDependencies>sndfile.lib;samplerate.lib;psapi.lib;KLU_COMPLEX.lib;$(IntDir)libfftw3-3.lib;%(AdditionalDependencies)</AdditionalDependencies>
<GenerateDebugInformation>true</GenerateDebugInformation> <GenerateDebugInformation>true</GenerateDebugInformation>
<SubSystem>Console</SubSystem> <SubSystem>Console</SubSystem>
<HeapReserveSize>0</HeapReserveSize> <HeapReserveSize>0</HeapReserveSize>
@ -541,10 +546,12 @@ lib /machine:x64 /def:..\..\fftw-3.3-dll64\libfftw3-3.def /out:$(IntDir)libfftw3
</DataExecutionPrevention> </DataExecutionPrevention>
<TargetMachine>MachineX64</TargetMachine> <TargetMachine>MachineX64</TargetMachine>
<LargeAddressAware>true</LargeAddressAware> <LargeAddressAware>true</LargeAddressAware>
<AdditionalLibraryDirectories>KLU\x64\Debug;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories> <AdditionalLibraryDirectories>../../libsamplerate/lib;../../libsndfile/lib;KLU\x64\Debug;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
</Link> </Link>
<PostBuildEvent> <PostBuildEvent>
<Command> <Command>
copy /y "..\..\libsndfile\bin\sndfile.dll" "$(OutDir)"
copy /y "..\..\libsamplerate\bin\samplerate.dll" "$(OutDir)"
copy /y "..\..\fftw-3.3-dll64\libfftw3-3.dll" "$(OutDir)" copy /y "..\..\fftw-3.3-dll64\libfftw3-3.dll" "$(OutDir)"
make-install-vngspiced.bat $(OutDir) fftw 64 make-install-vngspiced.bat $(OutDir) fftw 64
</Command> </Command>
@ -564,7 +571,7 @@ lib /machine:x64 /def:..\..\fftw-3.3-dll64\libfftw3-3.def /out:$(IntDir)libfftw3
<IntrinsicFunctions>true</IntrinsicFunctions> <IntrinsicFunctions>true</IntrinsicFunctions>
<FavorSizeOrSpeed>Speed</FavorSizeOrSpeed> <FavorSizeOrSpeed>Speed</FavorSizeOrSpeed>
<WholeProgramOptimization>true</WholeProgramOptimization> <WholeProgramOptimization>true</WholeProgramOptimization>
<AdditionalIncludeDirectories>..\src\maths\poly;..\src\frontend;..\src\spicelib\devices;tmp-bison;src\include;..\src\spicelib\parser;..\src\include;..\src\include\cppduals;.;..\..\fftw-3.3-dll64;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> <AdditionalIncludeDirectories>..\..\libsamplerate\include;..\..\libsndfile\include;..\src\maths\poly;..\src\frontend;..\src\spicelib\devices;tmp-bison;src\include;..\src\spicelib\parser;..\src\include;..\src\include\cppduals;.;..\..\fftw-3.3-dll64;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PreprocessorDefinitions>_CRT_SECURE_NO_DEPRECATE;SIMULATOR;XSPICE;CONSOLE;CONFIG64;HAVE_LIBFFTW3;%(PreprocessorDefinitions)</PreprocessorDefinitions> <PreprocessorDefinitions>_CRT_SECURE_NO_DEPRECATE;SIMULATOR;XSPICE;CONSOLE;CONFIG64;HAVE_LIBFFTW3;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<MinimalRebuild>false</MinimalRebuild> <MinimalRebuild>false</MinimalRebuild>
<ExceptionHandling> <ExceptionHandling>
@ -582,7 +589,7 @@ lib /machine:x64 /def:..\..\fftw-3.3-dll64\libfftw3-3.def /out:$(IntDir)libfftw3
<AdditionalOptions>/openmp:llvm %(AdditionalOptions)</AdditionalOptions> <AdditionalOptions>/openmp:llvm %(AdditionalOptions)</AdditionalOptions>
</ClCompile> </ClCompile>
<Link> <Link>
<AdditionalDependencies>psapi.lib;KLU_COMPLEX.lib;$(IntDir)libfftw3-3.lib;%(AdditionalDependencies)</AdditionalDependencies> <AdditionalDependencies>sndfile.lib;samplerate.lib;psapi.lib;KLU_COMPLEX.lib;$(IntDir)libfftw3-3.lib;%(AdditionalDependencies)</AdditionalDependencies>
<GenerateDebugInformation>true</GenerateDebugInformation> <GenerateDebugInformation>true</GenerateDebugInformation>
<SubSystem>Console</SubSystem> <SubSystem>Console</SubSystem>
<HeapReserveSize>0</HeapReserveSize> <HeapReserveSize>0</HeapReserveSize>
@ -597,7 +604,7 @@ lib /machine:x64 /def:..\..\fftw-3.3-dll64\libfftw3-3.def /out:$(IntDir)libfftw3
</DataExecutionPrevention> </DataExecutionPrevention>
<TargetMachine>MachineX64</TargetMachine> <TargetMachine>MachineX64</TargetMachine>
<LargeAddressAware>true</LargeAddressAware> <LargeAddressAware>true</LargeAddressAware>
<AdditionalLibraryDirectories>KLU\x64\Release;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories> <AdditionalLibraryDirectories>../../libsamplerate/lib;../../libsndfile/lib;KLU\x64\Release;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
</Link> </Link>
<PostBuildEvent> <PostBuildEvent>
<Command> <Command>
@ -673,7 +680,7 @@ lib /machine:x64 /def:..\..\fftw-3.3-dll64\libfftw3-3.def /out:$(IntDir)libfftw3
<IntrinsicFunctions>true</IntrinsicFunctions> <IntrinsicFunctions>true</IntrinsicFunctions>
<FavorSizeOrSpeed>Speed</FavorSizeOrSpeed> <FavorSizeOrSpeed>Speed</FavorSizeOrSpeed>
<WholeProgramOptimization>true</WholeProgramOptimization> <WholeProgramOptimization>true</WholeProgramOptimization>
<AdditionalIncludeDirectories>..\src\maths\poly;..\src\frontend;..\src\spicelib\devices;tmp-bison;src\include;..\src\spicelib\parser;..\src\include;..\src\include\cppduals;.;..\..\fftw-3.3-dll64;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> <AdditionalIncludeDirectories>..\..\libsamplerate\include;..\..\libsndfile\include;..\src\maths\poly;..\src\frontend;..\src\spicelib\devices;tmp-bison;src\include;..\src\spicelib\parser;..\src\include;..\src\include\cppduals;.;..\..\fftw-3.3-dll64;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PreprocessorDefinitions>_CRT_SECURE_NO_DEPRECATE;SIMULATOR;XSPICE;USE_OMP;CONFIG64;HAVE_LIBFFTW3;%(PreprocessorDefinitions)</PreprocessorDefinitions> <PreprocessorDefinitions>_CRT_SECURE_NO_DEPRECATE;SIMULATOR;XSPICE;USE_OMP;CONFIG64;HAVE_LIBFFTW3;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<MinimalRebuild>false</MinimalRebuild> <MinimalRebuild>false</MinimalRebuild>
<ExceptionHandling> <ExceptionHandling>
@ -693,7 +700,7 @@ lib /machine:x64 /def:..\..\fftw-3.3-dll64\libfftw3-3.def /out:$(IntDir)libfftw3
<AdditionalOptions>/openmp:llvm %(AdditionalOptions)</AdditionalOptions> <AdditionalOptions>/openmp:llvm %(AdditionalOptions)</AdditionalOptions>
</ClCompile> </ClCompile>
<Link> <Link>
<AdditionalDependencies>psapi.lib;KLU_COMPLEX.lib;$(IntDir)libfftw3-3.lib;%(AdditionalDependencies)</AdditionalDependencies> <AdditionalDependencies>sndfile.lib;samplerate.lib;psapi.lib;KLU_COMPLEX.lib;$(IntDir)libfftw3-3.lib;%(AdditionalDependencies)</AdditionalDependencies>
<GenerateDebugInformation>true</GenerateDebugInformation> <GenerateDebugInformation>true</GenerateDebugInformation>
<SubSystem>Windows</SubSystem> <SubSystem>Windows</SubSystem>
<HeapReserveSize>0</HeapReserveSize> <HeapReserveSize>0</HeapReserveSize>
@ -710,10 +717,12 @@ lib /machine:x64 /def:..\..\fftw-3.3-dll64\libfftw3-3.def /out:$(IntDir)libfftw3
</DataExecutionPrevention> </DataExecutionPrevention>
<TargetMachine>MachineX64</TargetMachine> <TargetMachine>MachineX64</TargetMachine>
<LargeAddressAware>true</LargeAddressAware> <LargeAddressAware>true</LargeAddressAware>
<AdditionalLibraryDirectories>KLU\x64\Release;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories> <AdditionalLibraryDirectories>../../libsamplerate/lib;../../libsndfile/lib;KLU\x64\Release;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
</Link> </Link>
<PostBuildEvent> <PostBuildEvent>
<Command> <Command>
copy /y "..\..\libsndfile\bin\sndfile.dll" "$(OutDir)"
copy /y "..\..\libsamplerate\bin\samplerate.dll" "$(OutDir)"
copy /y "..\..\fftw-3.3-dll64\libfftw3-3.dll" "$(OutDir)" copy /y "..\..\fftw-3.3-dll64\libfftw3-3.dll" "$(OutDir)"
make-install-vngspice.bat $(OutDir) fftw 64 make-install-vngspice.bat $(OutDir) fftw 64
</Command> </Command>
@ -785,7 +794,7 @@ lib /machine:x64 /def:..\..\fftw-3.3-dll64\libfftw3-3.def /out:$(IntDir)libfftw3
<IntrinsicFunctions>true</IntrinsicFunctions> <IntrinsicFunctions>true</IntrinsicFunctions>
<FavorSizeOrSpeed>Speed</FavorSizeOrSpeed> <FavorSizeOrSpeed>Speed</FavorSizeOrSpeed>
<WholeProgramOptimization>true</WholeProgramOptimization> <WholeProgramOptimization>true</WholeProgramOptimization>
<AdditionalIncludeDirectories>..\src\maths\poly;..\src\frontend;..\src\spicelib\devices;tmp-bison;src\include;..\src\spicelib\parser;..\src\include;..\src\include\cppduals;.;..\..\fftw-3.3-dll64;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> <AdditionalIncludeDirectories>..\..\libsamplerate\include;..\..\libsndfile\include;..\src\maths\poly;..\src\frontend;..\src\spicelib\devices;tmp-bison;src\include;..\src\spicelib\parser;..\src\include;..\src\include\cppduals;.;..\..\fftw-3.3-dll64;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PreprocessorDefinitions>_CRT_SECURE_NO_DEPRECATE;SIMULATOR;XSPICE;CONSOLE;CONFIG64;USE_OMP;HAVE_LIBFFTW3;%(PreprocessorDefinitions)</PreprocessorDefinitions> <PreprocessorDefinitions>_CRT_SECURE_NO_DEPRECATE;SIMULATOR;XSPICE;CONSOLE;CONFIG64;USE_OMP;HAVE_LIBFFTW3;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<MinimalRebuild>false</MinimalRebuild> <MinimalRebuild>false</MinimalRebuild>
<ExceptionHandling> <ExceptionHandling>
@ -805,7 +814,7 @@ lib /machine:x64 /def:..\..\fftw-3.3-dll64\libfftw3-3.def /out:$(IntDir)libfftw3
<AdditionalOptions>/openmp:llvm %(AdditionalOptions)</AdditionalOptions> <AdditionalOptions>/openmp:llvm %(AdditionalOptions)</AdditionalOptions>
</ClCompile> </ClCompile>
<Link> <Link>
<AdditionalDependencies>psapi.lib;KLU_COMPLEX.lib;$(IntDir)libfftw3-3.lib;%(AdditionalDependencies)</AdditionalDependencies> <AdditionalDependencies>sndfile.lib;samplerate.lib;psapi.lib;KLU_COMPLEX.lib;$(IntDir)libfftw3-3.lib;%(AdditionalDependencies)</AdditionalDependencies>
<GenerateDebugInformation>true</GenerateDebugInformation> <GenerateDebugInformation>true</GenerateDebugInformation>
<SubSystem>Console</SubSystem> <SubSystem>Console</SubSystem>
<HeapReserveSize>0</HeapReserveSize> <HeapReserveSize>0</HeapReserveSize>
@ -820,10 +829,12 @@ lib /machine:x64 /def:..\..\fftw-3.3-dll64\libfftw3-3.def /out:$(IntDir)libfftw3
</DataExecutionPrevention> </DataExecutionPrevention>
<TargetMachine>MachineX64</TargetMachine> <TargetMachine>MachineX64</TargetMachine>
<LargeAddressAware>true</LargeAddressAware> <LargeAddressAware>true</LargeAddressAware>
<AdditionalLibraryDirectories>KLU\x64\Release;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories> <AdditionalLibraryDirectories>../../libsamplerate/lib;../../libsndfile/lib;KLU\x64\Release;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
</Link> </Link>
<PostBuildEvent> <PostBuildEvent>
<Command> <Command>
copy /y "..\..\libsndfile\bin\sndfile.dll" "$(OutDir)"
copy /y "..\..\libsamplerate\bin\samplerate.dll" "$(OutDir)"
copy /y "..\..\fftw-3.3-dll64\libfftw3-3.dll" "$(OutDir)" copy /y "..\..\fftw-3.3-dll64\libfftw3-3.dll" "$(OutDir)"
make-install-vngspice.bat $(OutDir) fftw 64 make-install-vngspice.bat $(OutDir) fftw 64
</Command> </Command>
@ -934,6 +945,7 @@ lib /machine:x64 /def:..\..\fftw-3.3-dll64\libfftw3-3.def /out:$(IntDir)libfftw3
<ClInclude Include="..\src\frontend\runcoms2.h" /> <ClInclude Include="..\src\frontend\runcoms2.h" />
<ClInclude Include="..\src\frontend\shyu.h" /> <ClInclude Include="..\src\frontend\shyu.h" />
<ClInclude Include="..\src\frontend\signal_handler.h" /> <ClInclude Include="..\src\frontend\signal_handler.h" />
<ClInclude Include="..\src\frontend\sndprint.h" />
<ClInclude Include="..\src\frontend\spec.h" /> <ClInclude Include="..\src\frontend\spec.h" />
<ClInclude Include="..\src\frontend\spiceif.h" /> <ClInclude Include="..\src\frontend\spiceif.h" />
<ClInclude Include="..\src\frontend\streams.h" /> <ClInclude Include="..\src\frontend\streams.h" />
@ -1351,6 +1363,7 @@ lib /machine:x64 /def:..\..\fftw-3.3-dll64\libfftw3-3.def /out:$(IntDir)libfftw3
<ClInclude Include="..\src\spicelib\devices\vdmos\vdmosext.h" /> <ClInclude Include="..\src\spicelib\devices\vdmos\vdmosext.h" />
<ClInclude Include="..\src\spicelib\devices\vdmos\vdmosinit.h" /> <ClInclude Include="..\src\spicelib\devices\vdmos\vdmosinit.h" />
<ClInclude Include="..\src\spicelib\devices\vdmos\vdmositf.h" /> <ClInclude Include="..\src\spicelib\devices\vdmos\vdmositf.h" />
<ClInclude Include="..\src\spicelib\devices\vsrc\vsjack.h" />
<ClInclude Include="..\src\spicelib\devices\vsrc\vsrcdefs.h" /> <ClInclude Include="..\src\spicelib\devices\vsrc\vsrcdefs.h" />
<ClInclude Include="..\src\spicelib\devices\vsrc\vsrcext.h" /> <ClInclude Include="..\src\spicelib\devices\vsrc\vsrcext.h" />
<ClInclude Include="..\src\spicelib\devices\vsrc\vsrcinit.h" /> <ClInclude Include="..\src\spicelib\devices\vsrc\vsrcinit.h" />
@ -1563,6 +1576,7 @@ lib /machine:x64 /def:..\..\fftw-3.3-dll64\libfftw3-3.def /out:$(IntDir)libfftw3
<ClCompile Include="..\src\frontend\runcoms2.c" /> <ClCompile Include="..\src\frontend\runcoms2.c" />
<ClCompile Include="..\src\frontend\shyu.c" /> <ClCompile Include="..\src\frontend\shyu.c" />
<ClCompile Include="..\src\frontend\signal_handler.c" /> <ClCompile Include="..\src\frontend\signal_handler.c" />
<ClCompile Include="..\src\frontend\sndprint.c" />
<ClCompile Include="..\src\frontend\spec.c" /> <ClCompile Include="..\src\frontend\spec.c" />
<ClCompile Include="..\src\frontend\spiceif.c" /> <ClCompile Include="..\src\frontend\spiceif.c" />
<ClCompile Include="..\src\frontend\streams.c" /> <ClCompile Include="..\src\frontend\streams.c" />
@ -2732,6 +2746,7 @@ lib /machine:x64 /def:..\..\fftw-3.3-dll64\libfftw3-3.def /out:$(IntDir)libfftw3
<ClCompile Include="..\src\spicelib\devices\vdmos\vdmossoachk.c" /> <ClCompile Include="..\src\spicelib\devices\vdmos\vdmossoachk.c" />
<ClCompile Include="..\src\spicelib\devices\vdmos\vdmostemp.c" /> <ClCompile Include="..\src\spicelib\devices\vdmos\vdmostemp.c" />
<ClCompile Include="..\src\spicelib\devices\vdmos\vdmostrun.c" /> <ClCompile Include="..\src\spicelib\devices\vdmos\vdmostrun.c" />
<ClCompile Include="..\src\spicelib\devices\vsrc\vsjack.c" />
<ClCompile Include="..\src\spicelib\devices\vsrc\vsrc.c" /> <ClCompile Include="..\src\spicelib\devices\vsrc\vsrc.c" />
<ClCompile Include="..\src\spicelib\devices\vsrc\vsrcacct.c" /> <ClCompile Include="..\src\spicelib\devices\vsrc\vsrcacct.c" />
<ClCompile Include="..\src\spicelib\devices\vsrc\vsrcacld.c" /> <ClCompile Include="..\src\spicelib\devices\vsrc\vsrcacld.c" />

View File

@ -348,6 +348,8 @@
</Link> </Link>
<PostBuildEvent> <PostBuildEvent>
<Command> <Command>
copy /y "..\..\libsndfile\bin\sndfile.dll" "$(OutDir)"
copy /y "..\..\libsamplerate\bin\samplerate.dll" "$(OutDir)"
make-install-vngspiced.bat $(OutDir) 64 make-install-vngspiced.bat $(OutDir) 64
</Command> </Command>
</PostBuildEvent> </PostBuildEvent>
@ -383,7 +385,7 @@
<CompileAs>Default</CompileAs> <CompileAs>Default</CompileAs>
<MultiProcessorCompilation>true</MultiProcessorCompilation> <MultiProcessorCompilation>true</MultiProcessorCompilation>
<LanguageStandard>stdcpp14</LanguageStandard> <LanguageStandard>stdcpp14</LanguageStandard>
<AdditionalOptions>%(AdditionalOptions)</AdditionalOptions> <AdditionalOptions>/openmp:llvm %(AdditionalOptions)</AdditionalOptions>
</ClCompile> </ClCompile>
<Link> <Link>
<AdditionalDependencies>sndfile.lib;samplerate.lib;psapi.lib;KLU_COMPLEX.lib;%(AdditionalDependencies)</AdditionalDependencies> <AdditionalDependencies>sndfile.lib;samplerate.lib;psapi.lib;KLU_COMPLEX.lib;%(AdditionalDependencies)</AdditionalDependencies>
@ -407,6 +409,8 @@
</Link> </Link>
<PostBuildEvent> <PostBuildEvent>
<Command> <Command>
copy /y "..\..\libsndfile\bin\sndfile.dll" "$(OutDir)"
copy /y "..\..\libsamplerate\bin\samplerate.dll" "$(OutDir)"
make-install-vngspice.bat $(OutDir) 64 make-install-vngspice.bat $(OutDir) 64
</Command> </Command>
</PostBuildEvent> </PostBuildEvent>
@ -556,6 +560,8 @@
</Link> </Link>
<PostBuildEvent> <PostBuildEvent>
<Command> <Command>
copy /y "..\..\libsndfile\bin\sndfile.dll" "$(OutDir)"
copy /y "..\..\libsamplerate\bin\samplerate.dll" "$(OutDir)"
make-install-vngspiced.bat $(OutDir) 64 make-install-vngspiced.bat $(OutDir) 64
</Command> </Command>
</PostBuildEvent> </PostBuildEvent>
@ -726,6 +732,8 @@
</Link> </Link>
<PostBuildEvent> <PostBuildEvent>
<Command> <Command>
copy /y "..\..\libsndfile\bin\sndfile.dll" "$(OutDir)"
copy /y "..\..\libsamplerate\bin\samplerate.dll" "$(OutDir)"
make-install-vngspice.bat $(OutDir) 64 make-install-vngspice.bat $(OutDir) 64
</Command> </Command>
</PostBuildEvent> </PostBuildEvent>
@ -779,7 +787,8 @@
<AdditionalLibraryDirectories>../../libsamplerate/lib;../../libsndfile/lib;KLU/Release/;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories> <AdditionalLibraryDirectories>../../libsamplerate/lib;../../libsndfile/lib;KLU/Release/;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
</Link> </Link>
<PostBuildEvent> <PostBuildEvent>
<Command>make-install-vngspice.bat $(OutDir)</Command> <Command>
make-install-vngspice.bat $(OutDir)</Command>
</PostBuildEvent> </PostBuildEvent>
<Manifest> <Manifest>
<AdditionalManifestFiles>$(ProjectDir)ngspice-x86.exe.manifest</AdditionalManifestFiles> <AdditionalManifestFiles>$(ProjectDir)ngspice-x86.exe.manifest</AdditionalManifestFiles>
@ -837,6 +846,8 @@
</Link> </Link>
<PostBuildEvent> <PostBuildEvent>
<Command> <Command>
copy /y "..\..\libsndfile\bin\sndfile.dll" "$(OutDir)"
copy /y "..\..\libsamplerate\bin\samplerate.dll" "$(OutDir)"
make-install-vngspice.bat $(OutDir) 64 make-install-vngspice.bat $(OutDir) 64
</Command> </Command>
</PostBuildEvent> </PostBuildEvent>
@ -869,6 +880,7 @@
<ClInclude Include="..\src\frontend\com_dump.h" /> <ClInclude Include="..\src\frontend\com_dump.h" />
<ClInclude Include="..\src\frontend\com_echo.h" /> <ClInclude Include="..\src\frontend\com_echo.h" />
<ClInclude Include="..\src\frontend\com_fft.h" /> <ClInclude Include="..\src\frontend\com_fft.h" />
<ClInclude Include="..\src\frontend\com_fileio.h" />
<ClInclude Include="..\src\frontend\com_ghelp.h" /> <ClInclude Include="..\src\frontend\com_ghelp.h" />
<ClInclude Include="..\src\frontend\com_gnuplot.h" /> <ClInclude Include="..\src\frontend\com_gnuplot.h" />
<ClInclude Include="..\src\frontend\com_hardcopy.h" /> <ClInclude Include="..\src\frontend\com_hardcopy.h" />
@ -885,7 +897,6 @@
<ClInclude Include="..\src\frontend\com_shift.h" /> <ClInclude Include="..\src\frontend\com_shift.h" />
<ClInclude Include="..\src\frontend\com_state.h" /> <ClInclude Include="..\src\frontend\com_state.h" />
<ClInclude Include="..\src\frontend\com_strcmp.h" /> <ClInclude Include="..\src\frontend\com_strcmp.h" />
<ClInclude Include="..\src\frontend\com_fileio.h" />
<ClInclude Include="..\src\frontend\com_unset.h" /> <ClInclude Include="..\src\frontend\com_unset.h" />
<ClInclude Include="..\src\frontend\com_wr_ic.h" /> <ClInclude Include="..\src\frontend\com_wr_ic.h" />
<ClInclude Include="..\src\frontend\control.h" /> <ClInclude Include="..\src\frontend\control.h" />
@ -1482,6 +1493,7 @@
<ClCompile Include="..\src\frontend\com_dump.c" /> <ClCompile Include="..\src\frontend\com_dump.c" />
<ClCompile Include="..\src\frontend\com_echo.c" /> <ClCompile Include="..\src\frontend\com_echo.c" />
<ClCompile Include="..\src\frontend\com_fft.c" /> <ClCompile Include="..\src\frontend\com_fft.c" />
<ClCompile Include="..\src\frontend\com_fileio.c" />
<ClCompile Include="..\src\frontend\com_ghelp.c" /> <ClCompile Include="..\src\frontend\com_ghelp.c" />
<ClCompile Include="..\src\frontend\com_gnuplot.c" /> <ClCompile Include="..\src\frontend\com_gnuplot.c" />
<ClCompile Include="..\src\frontend\com_hardcopy.c" /> <ClCompile Include="..\src\frontend\com_hardcopy.c" />
@ -1498,7 +1510,6 @@
<ClCompile Include="..\src\frontend\com_shift.c" /> <ClCompile Include="..\src\frontend\com_shift.c" />
<ClCompile Include="..\src\frontend\com_state.c" /> <ClCompile Include="..\src\frontend\com_state.c" />
<ClCompile Include="..\src\frontend\com_strcmp.c" /> <ClCompile Include="..\src\frontend\com_strcmp.c" />
<ClCompile Include="..\src\frontend\com_fileio.c" />
<ClCompile Include="..\src\frontend\com_sysinfo.c" /> <ClCompile Include="..\src\frontend\com_sysinfo.c" />
<ClCompile Include="..\src\frontend\com_unset.c" /> <ClCompile Include="..\src\frontend\com_unset.c" />
<ClCompile Include="..\src\frontend\com_wr_ic.c" /> <ClCompile Include="..\src\frontend\com_wr_ic.c" />