44#include " support/Utils.h"
55#include < cfloat>
66#include < cmath>
7+ #include < iostream>
78#include < complex>
89#include < limits>
910#include " support/Exceptions.h"
@@ -1179,6 +1180,10 @@ double get_value_or(T&& val, double default_val) {
11791180 double inputVoltage = inputVoltages[inputVoltageIndex];
11801181 auto opPoint = get_operating_points ()[operatingPointIndex];
11811182 double switchingFrequency = get_value_or (opPoint.get_switching_frequency (), 0.0 );
1183+ if (switchingFrequency <= 0 ) {
1184+ throw std::invalid_argument (
1185+ " CLLC SPICE: operating point has invalid switching frequency" );
1186+ }
11821187 double outputVoltage = opPoint.get_output_voltages ()[0 ];
11831188 double outputCurrent = opPoint.get_output_currents ()[0 ];
11841189 double n = turnsRatio;
@@ -1188,6 +1193,7 @@ double get_value_or(T&& val, double default_val) {
11881193 double L2 = params.secondaryResonantInductance ;
11891194 double C2 = params.secondaryResonantCapacitance ;
11901195 double Lm = params.magnetizingInductance ;
1196+ double fr = params.resonantFrequency ;
11911197
11921198 double period = 1.0 / switchingFrequency;
11931199 double halfPeriod = period / 2.0 ;
@@ -1206,115 +1212,152 @@ double get_value_or(T&& val, double default_val) {
12061212 int totalPeriods = steadyPeriods + periodsToExtract;
12071213 double simTime = totalPeriods * period;
12081214 double startTime = steadyPeriods * period;
1209- double stepTime = period / 200.0 ;
1215+ // Time step: period/200 baseline, period/500 when fs > 1.5·fr
1216+ // (super-resonant ringing demands tighter resolution).
1217+ // CLLC_REWRITE_PLAN §6.2.
1218+ double stepDivisor = (fr > 0 && switchingFrequency > 1.5 * fr) ? 500.0 : 200.0 ;
1219+ double stepTime = period / stepDivisor;
12101220
1211- // Load resistance
1212- double Rload = outputVoltage / outputCurrent;
1221+ // Load resistance — divide-by-zero guard per plan §6.2 (R_load floor).
1222+ double Rload = std::max ( outputVoltage / std::max ( outputCurrent, 1e-9 ), 1e-3 ) ;
12131223
12141224 // Secondary inductance for coupled inductor model
12151225 double Lsec = Lm / (n * n); // Lm referred to secondary for coupling
12161226
12171227 std::ostringstream circuit;
1228+ circuit << std::scientific;
12181229
12191230 circuit << " * CLLC Bidirectional Resonant Converter - Generated by OpenMagnetics\n " ;
12201231 circuit << " * Vin=" << inputVoltage << " V, Vout=" << outputVoltage
1221- << " V, f=" << (switchingFrequency/1e3 ) << " kHz\n " ;
1232+ << " V, Pout=" << (outputVoltage*outputCurrent) << " W, fs="
1233+ << (switchingFrequency/1e3 ) << " kHz, fr=" << (fr/1e3 ) << " kHz\n " ;
12221234 circuit << " * n=" << n << " , L1=" << (L1 *1e6 ) << " uH, C1=" << (C1 *1e9 ) << " nF\n " ;
12231235 circuit << " * Lm=" << (Lm*1e6 ) << " uH, L2=" << (L2 *1e6 ) << " uH, C2=" << (C2 *1e9 ) << " nF\n\n " ;
12241236
12251237 // DC Input
12261238 circuit << " * DC Input\n " ;
12271239 circuit << " Vin vin_p 0 " << inputVoltage << " \n\n " ;
12281240
1229- // Switch models
1230- circuit << " * Switch and diode models\n " ;
1231- circuit << " .model SW1 SW VT=2.5 VH=0.5\n " ;
1232- circuit << " .model DIDEAL D(IS=1e-14 RS=0.01 CJO=1e-12)\n\n " ;
1241+ // Switch model: VT=2.5 VH=0.8 RON=0.01 ROFF=1Meg per plan §6.2
1242+ circuit << " * Switch model (CLLC_REWRITE_PLAN §6.2)\n " ;
1243+ circuit << " .model SW1 SW(VT=2.5 VH=0.8 RON=0.01 ROFF=1Meg)\n\n " ;
12331244
12341245 // PWM control signals for full bridge
12351246 // Pair 1: S1 (high-side leg A), S4 (low-side leg B) - first half cycle
12361247 // Pair 2: S2 (low-side leg A), S3 (high-side leg B) - second half cycle
12371248 circuit << " * PWM control signals (complementary pairs with dead time)\n " ;
1238- circuit << " Vpwm1 pwm1 0 PULSE(0 5 0 10n 10n " << std::scientific << tOn
1239- << " " << period << std::fixed << " )\n " ;
1240- circuit << " Vpwm2 pwm2 0 PULSE(0 5 " << std::scientific << halfPeriod
1241- << " 10n 10n " << tOn << " " << period << std::fixed << " )\n\n " ;
1242-
1243- // Primary full bridge
1244- circuit << " * Primary Full Bridge\n " ;
1245- circuit << " * Leg A: S1 (high-side), S2 (low-side)\n " ;
1249+ circuit << " Vpwm1 pwm1 0 PULSE(0 5 0 10n 10n " << tOn
1250+ << " " << period << " )\n " ;
1251+ circuit << " Vpwm2 pwm2 0 PULSE(0 5 " << halfPeriod
1252+ << " 10n 10n " << tOn << " " << period << " )\n\n " ;
1253+
1254+ // Primary full bridge — 4 active switches.
1255+ circuit << " * Primary Full Bridge (4 active switches)\n " ;
12461256 circuit << " S1 vin_p node_a pwm1 0 SW1\n " ;
1247- circuit << " S2 node_a 0 pwm2 0 SW1\n " ;
1248- circuit << " * Leg B: S3 (high-side), S4 (low-side)\n " ;
1257+ circuit << " S2 node_a 0 pwm2 0 SW1\n " ;
12491258 circuit << " S3 vin_p node_b pwm2 0 SW1\n " ;
1250- circuit << " S4 node_b 0 pwm1 0 SW1\n\n " ;
1259+ circuit << " S4 node_b 0 pwm1 0 SW1\n " ;
1260+ // Per-switch snubbers (1k + 1nF) — 4 primary, 4 secondary = 8 total
1261+ circuit << " * Primary snubbers (1k + 1nF across each switch)\n " ;
1262+ circuit << " Rsn_S1 vin_p ns1 1k\n Csn_S1 ns1 node_a 1n\n " ;
1263+ circuit << " Rsn_S2 node_a ns2 1k\n Csn_S2 ns2 0 1n\n " ;
1264+ circuit << " Rsn_S3 vin_p ns3 1k\n Csn_S3 ns3 node_b 1n\n " ;
1265+ circuit << " Rsn_S4 node_b ns4 1k\n Csn_S4 ns4 0 1n\n\n " ;
1266+
1267+ // Bridge midpoint differential probe.
1268+ circuit << " * Bridge differential voltage probe\n " ;
1269+ circuit << " Evab vab 0 VALUE={V(node_a) - V(node_b)}\n\n " ;
12511270
12521271 // Primary current sense
12531272 circuit << " * Primary current sense\n " ;
12541273 circuit << " Vpri_sense node_a pri_c1_in 0\n\n " ;
12551274
12561275 // Primary resonant tank: C1 in series with L1
1257- circuit << " * Primary Resonant Tank (C1 series with L1 )\n " ;
1258- circuit << " C_res1 pri_c1_in pri_l1_in " << std::scientific << C1 << std::fixed << " \n " ;
1259- circuit << " L_res1 pri_l1_in pri_trafo_in " << std::scientific << L1 << std::fixed << " \n\n " ;
1260-
1261- // Transformer: coupled inductors (Lpri = Lm, Lsec = Lm/n²)
1262- // The magnetizing inductance IS the primary inductor of the transformer
1263- circuit << " * Transformer (coupled inductors)\n " ;
1264- circuit << " Lpri pri_trafo_in node_b " << std::scientific << Lm << std::fixed << " \n " ;
1265- circuit << " Lsec sec_trafo_p sec_trafo_n " << std::scientific << Lsec << std::fixed << " \n " ;
1276+ circuit << " * Primary Resonant Tank (Cr1 series with Lr1 )\n " ;
1277+ circuit << " C_res1 pri_c1_in pri_l1_in " << C1 << " \n " ;
1278+ circuit << " L_res1 pri_l1_in pri_trafo_in " << L1 << " \n\n " ;
1279+
1280+ // Transformer: coupled inductors (Lpri = Lm, Lsec = Lm/n²). K = 0.9999
1281+ // (never 1.0) so SPICE doesn't choke on a singular coupling matrix.
1282+ circuit << " * Transformer (coupled inductors, K=0.9999 — never 1.0 )\n " ;
1283+ circuit << " Lpri pri_trafo_in node_b " << Lm << " \n " ;
1284+ circuit << " Lsec sec_trafo_p sec_trafo_n " << Lsec << " \n " ;
12661285 circuit << " Kpri_sec Lpri Lsec 0.9999\n\n " ;
12671286
12681287 // Secondary resonant tank: L2 in series with C2
1269- circuit << " * Secondary Resonant Tank (L2 series with C2) \n " ;
1288+ circuit << " * Secondary Resonant Tank (Lr2 series with Cr2) + sense \n " ;
12701289 circuit << " Vsec_sense sec_trafo_p sec_l2_in 0\n " ;
1271- circuit << " L_res2 sec_l2_in sec_c2_in " << std::scientific << L2 << std::fixed << " \n " ;
1272- circuit << " C_res2 sec_c2_in node_c " << std::scientific << C2 << std::fixed << " \n\n " ;
1290+ circuit << " L_res2 sec_l2_in sec_c2_in " << L2 << " \n " ;
1291+ circuit << " C_res2 sec_c2_in node_c " << C2 << " \n\n " ;
12731292
1274- // Secondary node_d = sec_trafo_n
1275- circuit << " * Secondary bridge reference\n " ;
1293+ // node_d = sec_trafo_n; high-Z DC path for solver convergence
1294+ circuit << " * Secondary bridge reference node \n " ;
12761295 circuit << " Vd_ref sec_trafo_n node_d 0\n " ;
1277- // Add high-value resistor to provide DC path for convergence
12781296 circuit << " Rdc_sec sec_trafo_n 0 1G\n\n " ;
12791297
1280- // Secondary full bridge rectifier (diodes for simplicity)
1281- // In forward mode: Ds1,Ds4 conduct first half; Ds2,Ds3 conduct second half
1282- circuit << " * Secondary Full Bridge Rectifier (diodes)\n " ;
1283- circuit << " Ds1 node_c vout_p DIDEAL\n " ;
1284- circuit << " Ds2 vout_n node_c DIDEAL\n " ;
1285- circuit << " Ds3 node_d vout_p DIDEAL\n " ;
1286- circuit << " Ds4 vout_n node_d DIDEAL\n\n " ;
1287-
1288- // Snubber resistors for convergence
1289- circuit << " * Snubber resistors for convergence\n " ;
1290- circuit << " Rsnub1 node_c vout_p 1MEG\n " ;
1291- circuit << " Rsnub2 vout_n node_c 1MEG\n " ;
1292- circuit << " Rsnub3 node_d vout_p 1MEG\n " ;
1293- circuit << " Rsnub4 vout_n node_d 1MEG\n\n " ;
1298+ // ---- Active synchronous-rectifier full bridge on secondary ----
1299+ // Per plan §6.1: in forward mode the SR is gated synchronously with
1300+ // the primary PWM (pwm_s1 = pwm_p1, pwm_s2 = pwm_p2). This avoids
1301+ // the chatter that an ideal-diode B-source emulator suffers at the
1302+ // V(D,S) ≈ 0 transition (timestep collapse). Reverse mode (P8)
1303+ // will drive the secondary as inverter and re-purpose the primary
1304+ // as the synchronous rectifier with its own gating.
1305+ //
1306+ // Conduction map (forward, full-bridge):
1307+ // pwm1 high → S1 (high-A) + S4 (low-B) ON → node_a > node_b
1308+ // → secondary node_c > node_d
1309+ // → SR Sa (node_c → vout_p) and Sd (vout_n → node_d) ON
1310+ // pwm2 high → opposite polarity → SR Sb (vout_n → node_c) and
1311+ // Sc (node_d → vout_p) ON
1312+ circuit << " * Secondary Active Synchronous Rectifier (4 SR switches)\n " ;
1313+ circuit << " * pwm1 -> Sa, Sd pwm2 -> Sb, Sc (forward mode SR)\n " ;
1314+ circuit << " Sa node_c vout_p pwm1 0 SW1\n " ;
1315+ circuit << " Sb vout_n node_c pwm2 0 SW1\n " ;
1316+ circuit << " Sc node_d vout_p pwm2 0 SW1\n " ;
1317+ circuit << " Sd vout_n node_d pwm1 0 SW1\n " ;
1318+ // Per-switch snubbers (1k + 1nF) on the SR side
1319+ circuit << " * Secondary snubbers (1k + 1nF across each SR switch)\n " ;
1320+ circuit << " Rsn_Sa node_c nsa 1k\n Csn_Sa nsa vout_p 1n\n " ;
1321+ circuit << " Rsn_Sb vout_n nsb 1k\n Csn_Sb nsb node_c 1n\n " ;
1322+ circuit << " Rsn_Sc node_d nsc 1k\n Csn_Sc nsc vout_p 1n\n " ;
1323+ circuit << " Rsn_Sd vout_n nsd 1k\n Csn_Sd nsd node_d 1n\n\n " ;
12941324
12951325 // Ground reference for secondary
12961326 circuit << " * Secondary ground reference\n " ;
12971327 circuit << " Vgnd_sec vout_n 0 0\n\n " ;
12981328
1299- // Output filter and load
1300- circuit << " * Output filter and load\n " ;
1329+ // Output filter, sense source, and load
1330+ circuit << " * Output filter, current sense, and load\n " ;
13011331 circuit << " Cout vout_p vout_n 100u IC=" << outputVoltage << " \n " ;
1302- circuit << " Rload vout_p vout_n " << Rload << " \n\n " ;
1303-
1304- // Transient analysis
1305- circuit << " * Transient Analysis\n " ;
1306- circuit << " .tran " << std::scientific << stepTime << " " << simTime
1307- << " " << startTime << std::fixed << " UIC\n\n " ;
1308-
1309- // Save signals
1310- circuit << " * Output signals\n " ;
1311- circuit << " .save v(vin_p) v(pri_trafo_in) v(node_b) i(Vin) i(Vpri_sense)"
1312- << " v(sec_trafo_p) v(sec_trafo_n) i(Vsec_sense)"
1313- << " v(vout_p) v(vout_n)\n\n " ;
1314-
1315- // Options
1316- circuit << " .options RELTOL=0.003 ABSTOL=1e-8 VNTOL=1e-5 TRTOL=10 ITL1=500 ITL4=100\n " ;
1317- circuit << " .ic v(vout_p)=" << outputVoltage << " \n\n " ;
1332+ circuit << " Vout_sense vout_p vout_load 0\n " ;
1333+ circuit << " Rload vout_load vout_n " << std::fixed << Rload
1334+ << std::scientific << " \n\n " ;
1335+
1336+ // Transient analysis with UIC honoring the .ic statements below.
1337+ circuit << " * Transient Analysis (UIC honors .ic pre-charge)\n " ;
1338+ circuit << " .tran " << stepTime << " " << simTime
1339+ << " " << startTime << " UIC\n\n " ;
1340+
1341+ // Save signals — node-level and current branches that the
1342+ // extractor maps into Primary/Secondary windings and ports.
1343+ circuit << " * Save signals\n " ;
1344+ circuit << " .save v(vab) v(vin_p) v(pri_trafo_in) v(node_a) v(node_b)\n " ;
1345+ circuit << " + v(node_c) v(node_d) v(sec_trafo_p) v(sec_trafo_n)\n " ;
1346+ circuit << " + v(vout_p) v(vout_n) v(pri_c1_in) v(sec_c2_in)\n " ;
1347+ circuit << " + i(Vin) i(Vpri_sense) i(Vsec_sense) i(Vout_sense)\n\n " ;
1348+
1349+ // Solver options — verbatim from plan §6.2
1350+ circuit << " * Solver options (CLLC_REWRITE_PLAN §6.2)\n " ;
1351+ circuit << " .options RELTOL=0.01 ABSTOL=1e-7 VNTOL=1e-4 ITL1=500 ITL4=500\n " ;
1352+ circuit << " .options METHOD=GEAR TRTOL=7\n\n " ;
1353+
1354+ // Initial conditions: pre-charge output cap to Vo, leave resonant
1355+ // caps at zero. SPICE node names for cap voltages: the node BETWEEN
1356+ // C1 (pri_c1_in -- pri_l1_in) and BETWEEN C2 (sec_c2_in -- node_c).
1357+ circuit << " * Initial conditions (UIC pre-charge)\n " ;
1358+ circuit << " .ic v(vout_p)=" << std::fixed << outputVoltage
1359+ << std::scientific << " \n " ;
1360+ circuit << " .ic v(pri_l1_in)=0 v(sec_c2_in)=0\n\n " ;
13181361
13191362 circuit << " .end\n " ;
13201363
@@ -1333,7 +1376,9 @@ double get_value_or(T&& val, double default_val) {
13331376
13341377 NgspiceRunner runner;
13351378 if (!runner.is_available ()) {
1336- throw std::runtime_error (" ngspice is not available for simulation" );
1379+ std::cerr << " [CLLC] ngspice not available — falling back to analytical "
1380+ " operating-point generation" << std::endl;
1381+ return process_operating_points (turnsRatios, magnetizingInductance);
13371382 }
13381383
13391384 std::vector<double > inputVoltages;
@@ -1370,7 +1415,19 @@ double get_value_or(T&& val, double default_val) {
13701415 auto simResult = runner.run_simulation (netlist, config);
13711416
13721417 if (!simResult.success ) {
1373- throw std::runtime_error (" CLLC Simulation failed: " + simResult.errorMessage );
1418+ std::cerr << " [CLLC] ngspice run failed: " << simResult.errorMessage
1419+ << " — falling back to analytical for this op." << std::endl;
1420+ auto analytical = process_operating_point_for_input_voltage (
1421+ inputVoltages[inputVoltageIndex], cllcOpPoint, n,
1422+ magnetizingInductance, params);
1423+ std::string aname = inputVoltagesNames[inputVoltageIndex] + " input volt." ;
1424+ if (get_operating_points ().size () > 1 ) {
1425+ aname += " op. point " + std::to_string (opIndex);
1426+ }
1427+ aname += " [analytical]" ;
1428+ analytical.set_name (aname);
1429+ operatingPoints.push_back (analytical);
1430+ continue ;
13741431 }
13751432
13761433 // Map waveform names to winding excitations
0 commit comments