Updated osdiregistry.c and osdi.h to hopefully make OSDI files from all current, known flavors run without crashing

This commit is contained in:
Justin Fisher 2026-08-11 17:08:08 +02:00
parent 96551df137
commit 59b126346d
7 changed files with 88 additions and 87 deletions

View File

@ -37,7 +37,7 @@
#define JACOBIAN_ENTRY_REACT_CONST 2
#define JACOBIAN_ENTRY_RESIST 4
#define JACOBIAN_ENTRY_REACT 8
/* OSDI v0.5 (2C AC delay) — entry has a delay-coupling part loaded
/* OpenVA OSDI — 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
@ -63,7 +63,7 @@
#define EVAL_RET_FLAG_FATAL 2
#define EVAL_RET_FLAG_FINISH 4
#define EVAL_RET_FLAG_STOP 8
/* OSDI v0.5 axis-3 step rejection. Model raises this when it detects
/* OpenVA OSDI 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
@ -72,8 +72,7 @@
* 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.
/* OpenVA OSDI event-driven analog operators.
*
* EVENT the model wrote one or more entries into the
* pending_events array of OsdiSimInfo. The simulator
@ -84,14 +83,11 @@
* 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).
/* OpenVA OSDI OsdiEventKind discriminants.
* 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
@ -128,13 +124,12 @@ typedef struct OsdiSimParas {
char **vals_str;
}OsdiSimParas;
/* OSDI v0.5 — event request slot in OsdiSimInfo.pending_events.
/* OpenVA OSDI — 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 */
@ -142,7 +137,7 @@ typedef struct OsdiEventRequest {
uint32_t kind; /* OSDI_EVENT_KIND_* */
}OsdiEventRequest;
/* OSDI v0.5 — per-cross-expression metadata in OsdiDescriptor.
/* OpenVA OSDI — 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
@ -158,7 +153,7 @@ typedef struct OsdiCrossExprMeta {
* cross is bracketed */
}OsdiCrossExprMeta;
/* OSDI v0.5 — per-event-slot metadata in OsdiDescriptor.
/* OpenVA OSDI — per-event-slot metadata in OsdiDescriptor.
*
* One entry per `@(cross)` / `@(timer)` / `@(initial_step)` /
* `@(final_step)` body. `kind` is the OSDI_EVENT_KIND_*
@ -177,7 +172,7 @@ typedef struct OsdiSimInfo {
double *prev_state;
double *next_state;
uint32_t flags;
/* OSDI v0.5 — see OSDI_0_5_DESIGN.md §2.3.
/* OpenVA OSDI event-driven state.
* 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
@ -190,7 +185,7 @@ typedef struct OsdiSimInfo {
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
/* 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
@ -199,19 +194,19 @@ typedef struct OsdiSimInfo {
* 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
/* OpenVA OSDI — 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,
/* OpenVA OSDI — 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.
/* OpenVA OSDI (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
@ -335,12 +330,12 @@ typedef struct OsdiDescriptor {
void (*load_jacobian_react)(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. */
/* OpenVAF-Reloaded OSDI fields — present in the descriptor
* produced by OpenVAF-Reloaded and OpenVA but not previously
* declared in this header (ngspice didn't need them). Declared
* because the OpenVA OSDI 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;
@ -352,29 +347,30 @@ typedef struct OsdiDescriptor {
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. */
/* OpenVA OSDI — event-driven analog operators. The simulator
* 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. Older binaries (OpenVAF / OpenVAF-Reloaded,
* where these fields don't exist) also read as zero: the loader
* copies each descriptor into a zeroed full-size struct at load
* time (osdiregistry.c), so all tail fields beyond the file's
* real descriptor are guaranteed 0. */
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
/* OpenVA OSDI (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. */
* Tail-appended ABI-safe via the loader's zeroed-copy normalization. */
uint32_t num_delay_sites;
/* OSDI v0.5 (2C AC delay — 1B). The delay-coupling jacobian category:
/* OpenVA OSDI (AC delay). 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[
@ -387,13 +383,13 @@ typedef struct OsdiDescriptor {
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
/* OpenVA OSDI — 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. */
* loader's zeroed-copy normalization. */
uint32_t persistent_state_offset;
uint32_t persistent_state_count;
}OsdiDescriptor;

View File

@ -40,7 +40,7 @@ int OSDIacLoad(GENmodel *inModel, CKTcircuit *ckt) {
descr->load_jacobian_resist(inst, model);
descr->load_jacobian_react(inst, model, ckt->CKTomega);
/* OSDI v0.5 (2C AC delay — 1B). absdelay's exact AC phase: each delay
/* OpenVA OSDI (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

View File

@ -87,7 +87,7 @@ double osdi_limitlog(bool init, bool *check, double vnew, double vold,
return res;
}
/* OSDI v0.5 (2C) — absdelay exact-transport reader. Bound to
/* OpenVA OSDI (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
@ -108,7 +108,7 @@ double osdi_delay_read(const OsdiSimInfo *info, uint32_t site, double td,
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
/* OpenVA OSDI (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;

View File

@ -65,7 +65,7 @@ typedef struct OsdiExtraInstData {
bool dt_given;
uint32_t eval_flags;
/* OSDI v0.5 — event-driven analog operators (S3b).
/* OpenVA OSDI — event-driven analog operators (S3b).
*
* Per-instance scratch buffers for the SimInfo.cross_expr and
* .pending_events pointers. Lazily allocated on first eval if
@ -97,14 +97,14 @@ typedef struct OsdiExtraInstData {
uint32_t scheduled_count;
bool cross_init;
/* OSDI v0.5 (S3c) — abstime of the last accepted eval that
/* OpenVA OSDI (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,
/* OpenVA OSDI — 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
@ -119,7 +119,7 @@ typedef struct OsdiExtraInstData {
double *prev2_cross_expr_arr;
double prev2_eval_time;
/* OSDI v0.5 (2C) — absdelay exact-transport delay rings. Lazily
/* OpenVA OSDI (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.
@ -130,7 +130,7 @@ typedef struct OsdiExtraInstData {
* 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,
/* OpenVA OSDI (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;
@ -139,7 +139,7 @@ typedef struct OsdiExtraInstData {
uint32_t *delay_count;
uint32_t *delay_cap;
/* OSDI 0.5 — DEFERRED-COMMIT snapshot of the model's persistent_state array
/* OpenVA OSDI — 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
@ -196,7 +196,7 @@ double osdi_limitlog(bool init, bool *icheck, double vnew, double vold,
double osdi_fetlim(bool init, bool *icheck, double vnew, double vold,
double vto);
/* OSDI v0.5 (2C) — absdelay exact-transport reader (bound to
/* OpenVA OSDI (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

@ -73,7 +73,7 @@ OsdiSimParas get_simparams(const CKTcircuit *ckt) {
return sim_params_;
}
/* OSDI v0.5 (S3b) — per-instance event-state preparation BEFORE
/* OpenVA OSDI (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
@ -217,7 +217,7 @@ static double osdi_cross_time(double t0, double v0,
return (denom > 0.0) ? t1 + span * (fabs(v1) / denom) : t2;
}
/* OSDI v0.5 — post-eval processing.
/* OpenVA OSDI — post-eval processing.
*
* S3b: drain model-written pending_events into the sorted
* scheduled_events queue.
@ -321,7 +321,7 @@ static void eval(const OsdiDescriptor *descr, const GENinstance *gen_inst,
OsdiNgspiceHandle handle =
(OsdiNgspiceHandle){.kind = 3, .name = gen_inst->GENname};
/* OSDI v0.5 — per-instance scratch SimInfo with event-state
/* OpenVA OSDI — 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;
@ -572,7 +572,7 @@ static void load(CKTcircuit *ckt, const GENinstance *gen_inst, void *model,
}
}
/* OSDI v0.5 (2C) — push one accepted (time, value) sample into delay ring
/* OpenVA OSDI (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
@ -614,7 +614,7 @@ static void osdi_delay_push(OsdiExtraInstData *extra, uint32_t s, double t,
}
}
/* OSDI 0.5 — DEFERRED-COMMIT persistent state.
/* OpenVA OSDI — 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
@ -662,7 +662,7 @@ static void osdi_persist_commit(const OsdiRegistryEntry *entry, void *inst,
memcpy(extra->persist_snapshot, live, (size_t)n * sizeof(double));
}
/* OSDI v0.5 (2C) — DEVaccept hook. Fires once per ACCEPTED transient step
/* OpenVA OSDI (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
@ -777,7 +777,7 @@ extern int OSDIload(GENmodel *inModel, CKTcircuit *ckt) {
sim_info.flags |= CALC_REACT_JACOBIAN | CALC_REACT_RESIDUAL |
CALC_REACT_LIM_RHS | ANALYSIS_TRAN;
/* OSDI 0.5 — flag the final transient timepoint so a model can fire
/* OpenVA OSDI — 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)) {
@ -908,7 +908,7 @@ extern int OSDIload(GENmodel *inModel, CKTcircuit *ckt) {
ckt->CKTnoncon++;
}
/* Axis-3 (OSDI v0.5): a model has raised REJECT_STEP, signalling that
/* Axis-3 (OpenVA OSDI): 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

View File

@ -400,9 +400,8 @@ extern OsdiObjectFile load_object_file(const char *input) {
} else {
/* Original OpenVAF binaries don't publish OSDI_DESCRIPTOR_SIZE
* and must be v0.3 exactly. OSDI_VERSION_MAJOR_CURR /
* OSDI_VERSION_MINOR_CURR now track the current
* openvaf-reloaded/OpenVA version (0.5 as of S3a) so the
* v0.3 check is hard-coded here. */
* OSDI_VERSION_MINOR_CURR track the current OpenVA OSDI
* version, so the v0.3 check is hard-coded here. */
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 "
@ -411,7 +410,11 @@ extern OsdiObjectFile load_object_file(const char *input) {
txfree(path);
return INVALID_OBJECT;
}
descriptor_size = sizeof(OsdiDescriptor);
/* The v0.3 descriptor ends right where the v0.4 fields begin: it is
* the prefix of the current struct up to given_flag_model. Using
* sizeof(OsdiDescriptor) here would walk a multi-descriptor v0.3
* file with the wrong stride. */
descriptor_size = offsetof(OsdiDescriptor, given_flag_model);
}
GET_CONST(OSDI_NUM_DESCRIPTORS, uint32_t);
@ -461,12 +464,31 @@ extern OsdiObjectFile load_object_file(const char *input) {
}
OsdiRegistryEntry *dst = TMALLOC(OsdiRegistryEntry, OSDI_NUM_DESCRIPTORS);
char* desc_ptr = (char*)OSDI_DESCRIPTORS;
/* Normalize the file's descriptors to this build's layout. The .osdi
* stores an array with stride descriptor_size, which for a v0.3/v0.4 file
* is smaller than the current struct (and for a future version may be
* larger). Copy each descriptor into a zeroed full-size OsdiDescriptor so
* every field this build knows about reads as a true zero when the file
* predates it downstream code (osdiload.c, osditrunc.c) reads tail
* fields like num_cross_exprs without per-field size checks. The copies
* only hold counts/offsets and pointers back into the .so, which stays
* loaded for the lifetime of the process. */
OsdiDescriptor *descriptors = TMALLOC(OsdiDescriptor, OSDI_NUM_DESCRIPTORS);
memset(descriptors, 0, sizeof(OsdiDescriptor) * OSDI_NUM_DESCRIPTORS);
{
size_t copy_size = descriptor_size < sizeof(OsdiDescriptor)
? descriptor_size
: sizeof(OsdiDescriptor);
for (uint32_t i = 0; i < OSDI_NUM_DESCRIPTORS; i++)
memcpy(&descriptors[i],
(const char *)OSDI_DESCRIPTORS + (size_t)i * descriptor_size,
copy_size);
}
for (uint32_t i = 0; i < OSDI_NUM_DESCRIPTORS; i++) {
const OsdiDescriptor *descr = (OsdiDescriptor*)desc_ptr;
desc_ptr += descriptor_size;
const OsdiDescriptor *descr = &descriptors[i];
uint32_t dt = descr->num_params + descr->num_opvars;
bool has_m = false;
uint32_t temp = descr->num_params + descr->num_opvars + 1;
@ -491,28 +513,11 @@ extern OsdiObjectFile load_object_file(const char *input) {
size_t inst_off = calc_osdi_instance_data_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;
/* v0.5 tail fields: the normalized copy guarantees these read as zero
* when the .osdi predates them, so no per-field size checks needed. */
uint32_t num_delay_sites = descr->num_delay_sites;
uint32_t persistent_state_count = descr->persistent_state_count;
uint32_t persistent_state_offset = descr->persistent_state_offset;
dst[i] = (OsdiRegistryEntry){
.descriptor = descr,

View File

@ -20,7 +20,7 @@ int OSDItrunc(GENmodel *in_model, CKTcircuit *ckt, double *timestep) {
bool has_boundstep = offset != UINT32_MAX;
offset += entry->inst_offset;
/* OSDI v0.5 (S3b) — clamp the proposed timestep against any
/* OpenVA OSDI (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