Skip to content

OSDI Extension: Delays and Discontinuities #73

Description

@pepijndevos

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:

  1. MIR analysis identifies the states feeding into expr (node voltages, branch
    currents). This dependency analysis already exists in OpenVAF for Jacobian computation.

  2. 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.

  3. 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
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions