I've been wrestling with how to best add absdelay to OpenVAF, which makes a Delay Differential Equation.
After studying implementations in SciML and Diffrax I nudged Claude towards the following proposal, which also covers discontinuities which are currently silently ignored. Ignorant simulators can remain ignorant, while good simulators can do better.
Motivation
Verilog-A's absdelay and $discontinuity are currently poorly supported in the
OpenVAF/OSDI ecosystem. absdelay is lowered to a passthrough (the delayed value is
silently dropped), and $discontinuity is ignored except for $discontinuity(-1) inside
$limit contexts. This causes incorrect simulation results, as demonstrated by
OpenVAF#106 where a 20 GHz signal
through a 10ps delay showed attenuation and distortion instead of a clean time shift.
This proposal adds two small extensions to OSDI that let compiled models declare their
delay and discontinuity structure, and optionally let capable simulators provide
high-quality delay evaluation.
Design Principles
- Minimal API surface. Two new fields on
OsdiDescriptor, two on OsdiSimInfo.
- Graceful degradation. Unaware simulators ignore the new fields entirely. Models
include a built-in fallback for delay evaluation.
- Clean mapping to SciML. The descriptors map directly to DDE problem construction
(constant_lags, dependent_lags, tstops, ContinuousCallback).
Extension 1: Discontinuity Descriptors
Appended to OsdiDescriptor:
uint32_t num_discontinuities;
OsdiDiscontinuity *discontinuities;
Each descriptor declares one source of discontinuities or delays in the model:
#define OSDI_DISCONT_FIXED_TIME 0 /* known time point, optionally periodic */
#define OSDI_DISCONT_CROSSING 1 /* state-dependent zero crossing */
#define OSDI_DISCONT_CONST_DELAY 2 /* constant delay (absdelay with param) */
#define OSDI_DISCONT_VAR_DELAY 3 /* state-dependent delay */
typedef struct OsdiDiscontinuity {
uint32_t type;
double time; /* time point or delay value tau */
double period; /* repetition period, 0 if not periodic */
double (*fn)(void *inst, void *model, double *state, double time);
int32_t order; /* discontinuity order, 0 if unused (delays) */
} OsdiDiscontinuity;
Field usage by type:
| Type |
time |
period |
fn |
order |
FIXED_TIME |
time point |
repeat period (0 = once) |
NULL |
0, 1, 2, ... |
CROSSING |
unused (0) |
unused (0) |
zero-crossing function |
0, 1, 2, ... |
CONST_DELAY |
tau |
unused (0) |
NULL |
unused (0) |
VAR_DELAY |
unused (0) |
unused (0) |
lag function |
unused (0) |
The function pointer signature is the same for crossings and variable delays:
double (*fn)(void *inst, void *model, double *state, double time);
For CROSSING: returns a value; the simulator root-finds for zero crossings.
For VAR_DELAY: returns the current delay tau; the simulator uses it for
discontinuity tracking (detecting when t - tau(state, t) crosses a known
past discontinuity).
Extension 2: Past State Query
Appended to OsdiSimInfo:
double (*query_past_state)(void *ctx, uint32_t state_idx, double time);
void *query_past_state_ctx;
The model calls this during eval() to get node voltages and branch currents at
past times. state_idx uses the same index space as the existing node mapping.
If query_past_state is NULL, the model falls back to an internal ring buffer.
OpenVAF Implementation
Lowering absdelay
Currently absdelay(expr, tau, max_delay) is lowered to just expr (line 719 of
hir_lower/src/expr.rs). The new lowering:
-
MIR analysis identifies the states feeding into expr (node voltages, branch
currents). This dependency analysis already exists in OpenVAF for Jacobian computation.
-
Descriptor emission. For constant tau (a parameter), emit OSDI_DISCONT_CONST_DELAY
with time = tau. For state-dependent tau, emit OSDI_DISCONT_VAR_DELAY with a
compiled function that evaluates the delay expression.
-
Eval codegen. For each absdelay call, emit code that:
- Computes
t_delayed = abstime - tau
- For each state feeding into
expr: queries it at t_delayed via
query_past_state if available, otherwise reads from an internal ring buffer
- Evaluates
expr using the delayed states
-
Internal ring buffer (fallback). Only allocated if query_past_state is NULL at
setup_instance time. Sized from max_delay (the third argument Verilog-A requires).
Stores (time, value) pairs for each state feeding into delayed expressions. Uses
cubic Hermite interpolation. Written to at every eval() call. If the simulator
provides query_past_state, no buffer is allocated and no memory is wasted.
Lowering $discontinuity
Currently ignored (except -1 in $limit). The new lowering:
$discontinuity(n) at a statically-known time → emit OSDI_DISCONT_FIXED_TIME with
the time and order = n.
$discontinuity(n) inside a conditional on state → emit OSDI_DISCONT_CROSSING with a
compiled function evaluating the branch condition, order = n. This lets the simulator
root-find proactively rather than react after the fact.
All discontinuity reporting is purely declarative via descriptors. There is no runtime
flag — the existing EVAL_RET_FLAG_LIM remains as-is for its original purpose (Newton
convergence hinting for $limit), but is unrelated to this proposal.
PWL Sources (periodic breakpoints)
For a PWL source with finite repeat count: expand breakpoints at compile time into
individual OSDI_DISCONT_FIXED_TIME entries. No periodicity needed.
For truly periodic sources (indefinite repetition): emit one OSDI_DISCONT_FIXED_TIME
per breakpoint within one period, each with period set to the repetition period.
Simulator Integration
Ignorant Simulator (e.g. ngspice, no changes)
Does nothing. Sets neither field on OsdiSimInfo. The model uses its internal ring
buffer for delays. Results are adequate when the simulator timestep is small enough
relative to the signal bandwidth, which can be encouraged by the model writing
bound_step (already supported by OSDI).
The model can set bound_step = tau / N for some reasonable N when delays are present,
ensuring the ring buffer has enough samples even without simulator awareness.
Minimally Aware Simulator
Reads num_discontinuities and the descriptors. Uses the information:
FIXED_TIME entries → add to tstops / forced stepping times
CONST_DELAY entries → set max timestep to min(tau) / N
- Callback types → ignored
Still sets query_past_state = NULL. Model uses internal buffer. But timestep control
ensures the internal buffer has good resolution.
Fully Capable Simulator (e.g. Cadnip.jl)
Reads all descriptors and maps them to SciML constructs:
for d in descriptors
if d.type == OSDI_DISCONT_FIXED_TIME
if d.period > 0
# periodic: add tstops at d.time + k*d.period for k = 0, 1, ...
# up to method order for delay-induced, or indefinitely for sources
else
push!(tstops, d.time)
end
push!(d_discontinuities, Discontinuity(d.time, d.order))
elseif d.type == OSDI_DISCONT_CROSSING
push!(continuous_callbacks, ContinuousCallback(
(u, t, integrator) -> d.fn(inst, model, u, t),
integrator -> nothing # affect: just step onto it
))
elseif d.type == OSDI_DISCONT_CONST_DELAY
push!(constant_lags, d.time)
elseif d.type == OSDI_DISCONT_VAR_DELAY
push!(dependent_lags, (u, p, t) -> d.fn(inst, model, u, t))
end
end
Provides query_past_state using the solver's dense output interpolant:
function query_past_state(ctx, state_idx, t_delayed)
sol = unsafe_pointer_to_objref(ctx)
return sol(t_delayed; idxs=state_idx)
end
For delays, constructs a DDEProblem with MethodOfSteps and gets proper
discontinuity tracking, high-order interpolation of delayed states, and
unconstrained stepping with fixed-point iteration — all from SciML's existing
DDE infrastructure.
Summary
|
Descriptors on OsdiDescriptor |
Callbacks on OsdiSimInfo |
| New fields |
num_discontinuities, *discontinuities |
query_past_state, query_past_state_ctx |
| Types |
4 (fixed time, crossing, const delay, var delay) |
1 function pointer |
Total: one struct definition, four type constants, four new fields across two
existing structs.
I've been wrestling with how to best add absdelay to OpenVAF, which makes a Delay Differential Equation.
After studying implementations in SciML and Diffrax I nudged Claude towards the following proposal, which also covers discontinuities which are currently silently ignored. Ignorant simulators can remain ignorant, while good simulators can do better.
Motivation
Verilog-A's
absdelayand$discontinuityare currently poorly supported in theOpenVAF/OSDI ecosystem.
absdelayis lowered to a passthrough (the delayed value issilently dropped), and
$discontinuityis ignored except for$discontinuity(-1)inside$limitcontexts. This causes incorrect simulation results, as demonstrated byOpenVAF#106 where a 20 GHz signal
through a 10ps delay showed attenuation and distortion instead of a clean time shift.
This proposal adds two small extensions to OSDI that let compiled models declare their
delay and discontinuity structure, and optionally let capable simulators provide
high-quality delay evaluation.
Design Principles
OsdiDescriptor, two onOsdiSimInfo.include a built-in fallback for delay evaluation.
(
constant_lags,dependent_lags,tstops,ContinuousCallback).Extension 1: Discontinuity Descriptors
Appended to
OsdiDescriptor:Each descriptor declares one source of discontinuities or delays in the model:
Field usage by type:
timeperiodfnorderFIXED_TIMECROSSINGCONST_DELAYVAR_DELAYThe function pointer signature is the same for crossings and variable delays:
For
CROSSING: returns a value; the simulator root-finds for zero crossings.For
VAR_DELAY: returns the current delay tau; the simulator uses it fordiscontinuity tracking (detecting when
t - tau(state, t)crosses a knownpast discontinuity).
Extension 2: Past State Query
Appended to
OsdiSimInfo:The model calls this during
eval()to get node voltages and branch currents atpast times.
state_idxuses the same index space as the existing node mapping.If
query_past_stateis NULL, the model falls back to an internal ring buffer.OpenVAF Implementation
Lowering
absdelayCurrently
absdelay(expr, tau, max_delay)is lowered to justexpr(line 719 ofhir_lower/src/expr.rs). The new lowering:MIR analysis identifies the states feeding into
expr(node voltages, branchcurrents). This dependency analysis already exists in OpenVAF for Jacobian computation.
Descriptor emission. For constant tau (a parameter), emit
OSDI_DISCONT_CONST_DELAYwith
time = tau. For state-dependent tau, emitOSDI_DISCONT_VAR_DELAYwith acompiled function that evaluates the delay expression.
Eval codegen. For each
absdelaycall, emit code that:t_delayed = abstime - tauexpr: queries it att_delayedviaquery_past_stateif available, otherwise reads from an internal ring bufferexprusing the delayed statesInternal ring buffer (fallback). Only allocated if
query_past_stateis NULL atsetup_instancetime. Sized frommax_delay(the third argument Verilog-A requires).Stores
(time, value)pairs for each state feeding into delayed expressions. Usescubic Hermite interpolation. Written to at every
eval()call. If the simulatorprovides
query_past_state, no buffer is allocated and no memory is wasted.Lowering
$discontinuityCurrently ignored (except
-1in$limit). The new lowering:$discontinuity(n)at a statically-known time → emitOSDI_DISCONT_FIXED_TIMEwiththe time and
order = n.$discontinuity(n)inside a conditional on state → emitOSDI_DISCONT_CROSSINGwith acompiled function evaluating the branch condition,
order = n. This lets the simulatorroot-find proactively rather than react after the fact.
All discontinuity reporting is purely declarative via descriptors. There is no runtime
flag — the existing
EVAL_RET_FLAG_LIMremains as-is for its original purpose (Newtonconvergence hinting for
$limit), but is unrelated to this proposal.PWL Sources (periodic breakpoints)
For a PWL source with finite repeat count: expand breakpoints at compile time into
individual
OSDI_DISCONT_FIXED_TIMEentries. No periodicity needed.For truly periodic sources (indefinite repetition): emit one
OSDI_DISCONT_FIXED_TIMEper breakpoint within one period, each with
periodset to the repetition period.Simulator Integration
Ignorant Simulator (e.g. ngspice, no changes)
Does nothing. Sets neither field on
OsdiSimInfo. The model uses its internal ringbuffer for delays. Results are adequate when the simulator timestep is small enough
relative to the signal bandwidth, which can be encouraged by the model writing
bound_step(already supported by OSDI).The model can set
bound_step = tau / Nfor some reasonable N when delays are present,ensuring the ring buffer has enough samples even without simulator awareness.
Minimally Aware Simulator
Reads
num_discontinuitiesand the descriptors. Uses the information:FIXED_TIMEentries → add totstops/ forced stepping timesCONST_DELAYentries → set max timestep tomin(tau) / NStill sets
query_past_state = NULL. Model uses internal buffer. But timestep controlensures the internal buffer has good resolution.
Fully Capable Simulator (e.g. Cadnip.jl)
Reads all descriptors and maps them to SciML constructs:
Provides
query_past_stateusing the solver's dense output interpolant:For delays, constructs a
DDEProblemwithMethodOfStepsand gets properdiscontinuity tracking, high-order interpolation of delayed states, and
unconstrained stepping with fixed-point iteration — all from SciML's existing
DDE infrastructure.
Summary
OsdiDescriptorOsdiSimInfonum_discontinuities,*discontinuitiesquery_past_state,query_past_state_ctxTotal: one struct definition, four type constants, four new fields across two
existing structs.