ngspice 47 (also present in pre-master-48 as of 2 Oct 2026), macOS 15, x86_64, built as the shared library (--with-ngshared).
Summary. With a GetSyncData callback registered through ngSpice_Init_Sync, any operating point that falls back to OPtran (the transient op) spins forever at 100 % CPU and never returns. bg_halt does not stop it. Without the callback the same circuit finishes at once.
Why. In sharedsync() (sharedspice.c), a step that would pass the final time is clamped to end 1.1 * delmin short of it. OPtran's end test is AlmostEqualUlps(optime, opfinaltime, 100), which a time 1.1 * delmin short never satisfies, so OPtran doesn't finish. Its loop that throws out passed breakpoints then clears the final breakpoint. Unlike the same loop in dctran.c, it has no guard that stops at the final time, and OPclrBreak keeps at least two entries, so it goes round for ever. A sample of the hung process has every sample in DCtran -> CKTop -> OPtran -> AlmostEqualUlps.
Reproduce. The attached optran-hang.c loads an RC circuit and sets optran 0 0 0 100n 10u 0, so the operating point goes straight to OPtran. It registers a callback that does nothing: it leaves delta alone and asks for no redo. The optran command only makes the fallback certain; we first hit this with the defaults, on a 555 astable whose gmin and source stepping both fail. A watchdog ends the program after 10 s. ./optran-hang nosync is the same run without the callback.
cc -o optran-hang optran-hang.c -I<dir of sharedspice.h> -L<dir> -lngspice
Results:
stock ngspice 47: HUNG at 100 % CPU, killed after 10 s; time frozen at 1e-05 (OPtran's end)
stock ngspice 47, nosync: finishes
47 + optran-sync-hang.patch: finishes; OPtran runs to 10 us, then the transient
Patch (attached, against pre-master-48; also applies to 47). Two guards in optran.c, the same shape as DCtran's:
CKTminBreak of the end, as well as when almost equal;opbreaks[0] < opfinaltime).The patch also makes a third change, in sharedspice.c. In sharedsync()'s redo branch, the clamp finalt - *pckttime - 1.1 * delmin can be zero or negative when the time is already that close to the end. The non-redo branch already guards against this (if (newdelta <= 0.0) newdelta = finalt - *pckttime); the redo branch didn't. Nothing we know of needs it once OPtran is fixed; it makes the two branches match.
With the patch, the 555 astable runs in 70–77 ms with the callback registered, with the same waveforms as without it.
By coincidence I encountered a related bug a few days ago: the "never move beyond final time" code drove the time step to zero in a transient simulation. The fix was to remove that code entirely, and it also fixes this one.
The code is old and has likely been made obsolete by changes to the transient code that made end-time detection more robust, possibly aaa67989.
Last edit: Giles Atkinson 2026-10-04
With dctran I did a lot of tinkering around this issue, hopefully it has been fixed. I indeed should have taken optran into account as well, as it uses similar code.
Fix : f4f9cbdf