Skip to content

Fix: nominal temperature Tnom runs at 0 °C — init param misrouted to hidden_state slot#145

Open
Gerd-Vandersteen wants to merge 1 commit into
ChipFlow:mainfrom
Gerd-Vandersteen:upstream-tnom-fix
Open

Fix: nominal temperature Tnom runs at 0 °C — init param misrouted to hidden_state slot#145
Gerd-Vandersteen wants to merge 1 commit into
ChipFlow:mainfrom
Gerd-Vandersteen:upstream-tnom-fix

Conversation

@Gerd-Vandersteen

Copy link
Copy Markdown

Problem

A model's nominal temperature Tnom can silently run at 0 °C instead of its intended
default (e.g. EKV's DEFAULT_TNOM = 25 °C), mis-scaling every pow(ratioT, BEX) mobility/threshold
temperature correction.

Root cause is a case-insensitive parameter-name collision in the init→eval index map
(vajax/analysis/openvaf_models.py, compile_openvaf_models). A model can expose both an input
param and a same-named hidden_state cache slot that collide when lowercased — EKV exposes input
TNOM (kind=param) and hidden_state Tnom (kind=hidden_state). The map was built last-wins:

eval_name_to_idx = {n.lower(): i for i, n in enumerate(param_names)}

so 'tnom' resolved to the hidden_state slot. Init params are inputs, but the generated init
function then sourced TNOM's value from that hidden_state slot — pre-initialized to 0.0 — so
the model computed Tnom = 0 + P_CELSIUS0 = 273.15 K`.

Fix

Two changes:

  1. init→eval map: on a lowercase collision, prefer a real input kind
    (param/param_given/temperature/voltage/implicit_unknown/sysfun) over hidden_state,
    while preserving legacy last-wins among entries of the same preference (so a param and its
    param_given twin are untouched). This is the load-bearing fix — init inputs must never source
    from a hidden_state (init-output) slot.
  2. un-given nominal-temp default: tnom/tref/tr fall back to the -\NOT_GIVEN sentinel (model_defaults.get(pname, 1e21)) so the model selects its own DEFAULT_TNOM`.

Evidence

EKV Id at Vg=0.7, Vd=1.0 goes 7.17 → 5.9588 µA, matching an independent VACASK/OSDI run of the
same .va exactly (ratio 1.0000); every d(Id)/dθ matches to 0.00%.

Blast radius (compiled all 11 bundled models): every mapping the fix changes is
hidden_state → input — the same shadowed-input bug — ekv (TNOM), bsimimg (WR, TNOM), bsimcmg
(RHORSD, TNOM, NVTM, CGSP, CGDP), bsimbulk (RBxx, TNOM), hisim2 (mfactor, NFALP1/2, NSUBD/NW).
There are no param ↔ param_given flips. All are latent "init input silently read as 0.0" bugs,
so the promotion is strictly a correctness improvement (these models were not previously
numerically re-validated against VACASK — worth a look during their bring-up).

Note

The cleanest upstream fix for the default half would be teaching openvaf_py get_param_defaults()
to emit sentinel/computed defaults, so the runtime fallback need never guess. The map fix (change 1)
is independent of that and is the essential correctness fix. A companion PR fixes the device
temperature (TEMP); this PR is independent of it.

@Gerd-Vandersteen

Copy link
Copy Markdown
Author

Companion PR: #144 fixes the device temperature (TEMP). Same temperature-defaults family; independent (either can merge alone).

…4 params corrupted)

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.
@Gerd-Vandersteen

Copy link
Copy Markdown
Author

Updated (force-push): generalized the collision fix after BSIM4 bring-up surfaced a third case — a model can expose a value param, its param_given flag, and a hidden_state slot all colliding under .lower() (BSIM4 does this for ~176 params: tnom, vth0, u0, ...). The map now ranks collisions value-input > param_given > hidden_state (last-wins within a tier) so an init value-param always reads its value, not the given-flag. EKV and the simple models are unchanged (EKV DC stays VACASK-exact).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant