From 46759ebbdcebfcc51a1316d8ba3a0e2aad7b4705 Mon Sep 17 00:00:00 2001 From: Gerd Vandersteen Date: Fri, 3 Jul 2026 09:28:29 +0200 Subject: [PATCH] Fix: init param misrouted on name collision (nominal Tnom = 0 C; BSIM4 params corrupted) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An init param sources its runtime *value* from the eval input slot of the same name. A model can expose several eval slots that collide under .lower(): a value `param`, its `param_given` flag, and a `hidden_state` cache slot. EKV exposes `TNOM` (param) and `Tnom` (hidden_state); BSIM4 exposes all three for `tnom`, `vth0`, `u0`, ... The map `eval_name_to_idx = {n.lower(): i ...}` was last-wins, so an init value-param could source from the hidden_state slot (an init *output*, pre-initialized to 0.0) or the given-flag (0/1) instead of the value — silently corrupting it. On EKV this ran the nominal temperature at Tnom = 0 + P_CELSIUS0 = 273.15 K instead of DEFAULT_TNOM = 25 C; on BSIM4 it corrupted 176 params (NaN DC solve). Fix: rank collisions by preference so a value-param always reads its value — value-input (param/temperature/voltage/implicit_unknown/sysfun) > param_given > hidden_state; last-wins within a tier. Also default un-given tnom/tref/tr to the -`NOT_GIVEN sentinel so the model selects its own DEFAULT_TNOM. Evidence: EKV Id at Vg=0.7,Vd=1.0 goes 7.17 -> 5.9588 uA, matching an independent VACASK/OSDI run of the same .va exactly (ratio 1.0000); all d(Id)/dtheta match to 0.00%. Companion PR fixes the device temperature (TEMP); this PR is independent of it. --- vajax/analysis/openvaf_models.py | 36 +++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/vajax/analysis/openvaf_models.py b/vajax/analysis/openvaf_models.py index bb2fb34e..44e02dba 100644 --- a/vajax/analysis/openvaf_models.py +++ b/vajax/analysis/openvaf_models.py @@ -510,8 +510,32 @@ def log(msg): f" {model_type}: init function done in {t4 - t3:.1f}s (cache_size={init_meta['cache_size']})" ) - # Build init->eval index mapping - eval_name_to_idx = {n.lower(): i for i, n in enumerate(param_names)} + # Build init->eval index mapping (case-insensitive). + # An init param sources its runtime *value* from the eval input slot of the same name. A + # model can expose several eval slots that collide when lowercased: a value `param`, its + # `param_given` flag, and a `hidden_state` cache slot (e.g. EKV `TNOM`/`Tnom`; BSIM4 + # exposes all three for `tnom`, `vth0`, `u0`, ...). A naive last-wins lowercase map points + # the init param at whichever comes last — the hidden_state (an init *output*, value 0.0) + # or the given-flag (0/1) — silently corrupting the parameter. Rank by preference so a + # value-param always reads its value: value-input > param_given > hidden_state; last-wins + # within a tier (legacy behaviour among equal-preference slots). + _VALUE_INPUT_KINDS = frozenset( + {"param", "temperature", "voltage", "implicit_unknown", "sysfun"} + ) + + def _name_pref(kind: str) -> int: + if kind in _VALUE_INPUT_KINDS: + return 2 + if kind == "param_given": + return 1 + return 0 # hidden_state and everything else (init outputs) + + eval_name_to_idx: Dict[str, int] = {} + for i, n in enumerate(param_names): + key = n.lower() + prev = eval_name_to_idx.get(key) + if prev is None or _name_pref(param_kinds[i]) >= _name_pref(param_kinds[prev]): + eval_name_to_idx[key] = i init_to_eval_indices = [] for name in init_meta["param_names"]: eval_idx = eval_name_to_idx.get(name.lower(), -1) @@ -766,7 +790,13 @@ def prepare_static_inputs( for pname, cols in param_to_cols.items(): if pname not in all_unique: if pname in ("tnom", "tref", "tr"): - default = 27.0 + # Un-given nominal temperature. Prefer the model's own literal default; if + # the model expresses "not given" via the -`NOT_GIVEN sentinel idiom (EKV: + # `if (TNOM == -`NOT_GIVEN) Tnom = `DEFAULT_TNOM + `P_CELSIUS0`), then + # get_param_defaults drops that computed default, so fall back to the sentinel + # and let the model select its own DEFAULT_TNOM (matches VACASK) rather than + # reading a spurious 0/27 degC. + default = model_defaults.get(pname, 1e21) elif pname in ("nf", "mult", "ns", "nd"): default = 1.0 else: