Skip to content

Commit 63947fc

Browse files
committed
feat(cllc): P4 — SPICE rewrite (active SR, snubbers, GEAR, IC, Evab, sense, analytical fallback)
Per CLLC_REWRITE_PLAN.md §6: - Active SR bridge Sa..Sd replacing Ds1..Ds4 (synchronous gating tied to pwm1/pwm2) — eliminates B-source ideal-diode chatter that broke ngspice convergence in forward mode. - Per-switch RC snubbers (Rsn_*=1k, Csn_*=1nF) across each switch. - SW1 model VT=2.5 VH=0.8 RON=0.01 ROFF=1Meg (Guide §5). - Evab VCVS probe `V(node_a)-V(node_b)` for converter-port stream. - METHOD=GEAR + TRTOL=7 solver options. - IC pre-charge: v(vout_p)=Vo, v(pri_l1_in)=0, v(sec_c2_in)=0. - Vout_sense zero-volt source on the output rail. - Time step period/200 (period/500 above 1.5·fr). - Divide-by-zero guard: Rload = max(Vo/max(Io, 1e-9), 1e-3). - Throws on switchingFrequency <= 0. - Analytical fallback in simulate_and_extract_operating_points when ngspice unavailable or sim fails — tags OP name with [analytical] (DAB.cpp:1249–1299 pattern). iPri NRMSE on PtP fixtures (gate 30%): Telecom-500W 4.42% Telecom-250W 3.56% Infineon-11kW 15.45% (was 23.2% pre-SR rewrite) Tests: +Test_Cllc_SPICE_Netlist, +Test_Cllc_DivideByZero_Guard, updated Test_CllcConverter_NetlistGeneration (Sa..Sd checks). 26/26 [cllc-topology] tests passing.
1 parent 743a5ff commit 63947fc

2 files changed

Lines changed: 220 additions & 72 deletions

File tree

src/converter_models/Cllc.cpp

Lines changed: 125 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
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

Comments
 (0)