Skip to content

Commit 80f220e

Browse files
AlfVIIclaude
andcommitted
fix(cllc): reject infeasible turns-ratio operating points instead of emitting zero-power waveforms
A CLLC operating point whose turns ratio is inconsistent with Vin/Vout (e.g. n=1 for a 48->12 V 4:1 step-down) demands a tank voltage gain n*Vout/Vin far below what a resonant tank can supply — it operates near the unity-gain load-independent point. No bounded power-transferring steady state exists for such a point. On current main this had stopped throwing and instead SILENTLY emitted a fabricated ~0.5 W zero-power waveform (Isec_rms ~0.04 A vs a requested 5 A) — a regression from 25170b5, whose observable-only convergence gate accepts the near-zero state (trivially antisymmetric, so a tiny finite residual rather than +inf), bypassing the existing no-fallback guard. The older compiled .so threw "no converging seed" honestly; current main returned garbage (reported as abt#1 from Heaviside). Add an analytical feasibility pre-check: M_req = |Vo|/|Vi|. If M_req is outside [0.5, 3.0] — a band that clears every matched full-bridge (M~1) and half-bridge (M~2) design across its input range — throw InvalidInputException(INVALID_DESIGN_REQUIREMENTS) naming the gain mismatch and the correct turns ratio (~Vin/Vout). The +inf "no converging seed" guard is upgraded from std::runtime_error to the same exception type, so both infeasibility paths are classifiable and distinct from a model convergence failure. The observable-residual gate from 25170b5 is preserved for all legitimate designs. Tests: cllc-topology 37/37 (incl. new Test_CllcConverter_Infeasible_TurnsRatio_Throws), cllc PtP reference designs 6/6, cllc-loss-breakdown 1/1. Zero regressions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 6f1cfe6 commit 80f220e

2 files changed

Lines changed: 84 additions & 1 deletion

File tree

src/converter_models/Cllc.cpp

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1151,6 +1151,46 @@ double get_value_or(T&& val, double default_val) {
11511151
Vo = n * outputVoltage; // reflected output (pri-referred)
11521152
}
11531153

1154+
// -------------------------------------------------------------------
1155+
// Feasibility pre-check (abt#1). A resonant CLLC tank operates around
1156+
// its load-independent point, where the primary-referred voltage gain
1157+
// is exactly 1.0; the turns ratio absorbs the nominal step-up/down and
1158+
// the tank only trims it over a narrow window. So a well-matched design
1159+
// has M_req = |Vo|/|Vi| = (k_bridge·)n·Vout/Vin ≈ 1 (≈2 for a
1160+
// half-bridge, where the bridge factor halves Vi). If M_req is far
1161+
// outside the band the tank can reach, NO bounded power-transferring
1162+
// steady state exists: the solver converges only to a fabricated
1163+
// zero-power waveform, which the observable-residual gate (commit
1164+
// 25170b5) no longer rejects — it now passes with a tiny finite
1165+
// residual. Throw a specific, actionable error instead of emitting
1166+
// fabricated zeros (CLAUDE.md no-fallback).
1167+
//
1168+
// The reported failure is a turns ratio inconsistent with Vin/Vout
1169+
// (e.g. n=1 for a 48→12 V 4:1 step-down gives M_req≈0.25, well below
1170+
// anything a resonant tank supplies). The [0.5, 3.0] band clears every
1171+
// matched full-bridge (M≈0.9–1.1) and half-bridge (M≈1.9–2.1) design
1172+
// across its input range while rejecting the 4×-off turns ratio.
1173+
double M_req = (std::abs(Vi) > 0.0)
1174+
? std::abs(Vo) / std::abs(Vi)
1175+
: std::numeric_limits<double>::infinity();
1176+
if (M_req < 0.5 || M_req > 3.0) {
1177+
double gainAtFsw = get_voltage_gain(switchingFrequency, params);
1178+
std::string msg =
1179+
"CLLC: operating point is infeasible for this resonant tank — the "
1180+
"requested conversion gain n*Vout/Vin = " + std::to_string(M_req) +
1181+
" is outside the range a resonant tank can supply (it operates near "
1182+
"unity gain; tank FHA gain at fsw=" + std::to_string(switchingFrequency) +
1183+
" Hz is " + std::to_string(gainAtFsw) + "). The solver would only "
1184+
"converge to a fabricated zero-power waveform. Vin=" +
1185+
std::to_string(inputVoltage) + " V, Vout=" + std::to_string(outputVoltage) +
1186+
" V, n=" + std::to_string(n) + ".";
1187+
if (!isReverse && outputVoltage > 0.0) {
1188+
msg += " For this conversion the turns ratio should be ~Vin/Vout = " +
1189+
std::to_string(inputVoltage / outputVoltage) + ".";
1190+
}
1191+
throw InvalidInputException(ErrorCode::INVALID_DESIGN_REQUIREMENTS, msg);
1192+
}
1193+
11541194
double period = 1.0 / switchingFrequency;
11551195
double Thalf = period / 2.0;
11561196
// Dead time is not represented by the TDA model (would require a 5th
@@ -1353,6 +1393,8 @@ double get_value_or(T&& val, double default_val) {
13531393
// left all-zero, which the freewheel propagator turns into a benign-
13541394
// looking ZERO-POWER waveform that is silently emitted as if valid. Throw
13551395
// on that (non-finite residual) instead of returning fabricated zeros.
1396+
// The infeasible-conversion-ratio case (turns ratio inconsistent with
1397+
// Vin/Vout) is caught earlier by the M_req feasibility pre-check.
13561398
//
13571399
// A *finite* residual means a seed did win (non-degenerate waveform); the
13581400
// asymmetric CLLC analytical TDA model is known to plateau well above the
@@ -1362,7 +1404,7 @@ double get_value_or(T&& val, double default_val) {
13621404
// zero-power bug. residual == -1.0 is the sanctioned symmetric-path seed
13631405
// fallback.
13641406
if (residual != -1.0 && !std::isfinite(residual)) {
1365-
throw std::runtime_error(
1407+
throw InvalidInputException(ErrorCode::INVALID_DESIGN_REQUIREMENTS,
13661408
"CLLC: steady-state solver found no converging seed (all multi-start seeds failed); "
13671409
"the all-zero state would emit a fabricated zero-power waveform. Vi=" +
13681410
std::to_string(Vi) + " V, Vo=" + std::to_string(Vo) + " V.");

tests/TestCllc.cpp

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,47 @@ namespace {
121121
return cllcJson;
122122
}
123123

124+
// =========================================================================
125+
// Regression (abt#1): an infeasible operating point — a turns ratio
126+
// inconsistent with Vin/Vout (n=1 for a 48->12 V, 4:1 step-down) — has no
127+
// power-transferring steady state (required tank gain n*Vout/Vin = 0.25 is
128+
// below the tank's asymptotic minimum gain ~0.69). MKF must throw a clear
129+
// INVALID_DESIGN_REQUIREMENTS error naming the gain mismatch and the correct
130+
// turns ratio, NOT silently emit a fabricated zero-power waveform. The
131+
// observable-only convergence gate (commit 25170b5) would otherwise accept
132+
// the near-zero state as "converged".
133+
// =========================================================================
134+
TEST_CASE("Test_CllcConverter_Infeasible_TurnsRatio_Throws",
135+
"[converter-model][cllc-topology][infeasible][smoke-test]") {
136+
json j;
137+
j["inputVoltage"] = {{"minimum", 36}, {"maximum", 60}, {"nominal", 48}};
138+
j["maxSwitchingFrequency"] = 300000;
139+
j["minSwitchingFrequency"] = 80000;
140+
j["efficiency"] = 0.95;
141+
j["operatingPoints"] = json::array();
142+
json op;
143+
op["outputVoltages"] = {12.0};
144+
op["outputCurrents"] = {5.0};
145+
op["switchingFrequency"] = 150000.0;
146+
op["ambientTemperature"] = 25.0;
147+
op["powerFlow"] = "forward";
148+
j["operatingPoints"].push_back(op);
149+
150+
OpenMagnetics::CllcConverter cllc(j);
151+
// Forcing n=1 on a 4:1 step-down must be rejected as infeasible.
152+
REQUIRE_THROWS_AS(cllc.process_operating_points({1.0}, 1e-3),
153+
InvalidInputException);
154+
155+
// Sanity: the physically-correct turns ratio (n = Vin/Vout = 4) converges
156+
// and delivers real power.
157+
auto params = cllc.calculate_resonant_parameters();
158+
REQUIRE_THAT(params.turnsRatio, Catch::Matchers::WithinAbs(4.0, 0.05));
159+
std::vector<OperatingPoint> ops;
160+
REQUIRE_NOTHROW(ops = cllc.process_operating_points({params.turnsRatio},
161+
params.magnetizingInductance));
162+
REQUIRE(!ops.empty());
163+
}
164+
124165
// =========================================================================
125166
// TEST 1: Forward Mode at Resonance (fs = fr)
126167
// =========================================================================

0 commit comments

Comments
 (0)