Skip to content

Commit 757d50c

Browse files
committed
feat(cuk): make maximumDutyCycle configurable, replace hardcoded 0.95
Cuk::calculate_duty_cycle previously hard-coded a 0.95 ceiling and threw on D >= 0.95. That was already loud (no silent clamp) but inflexible -- callers with stricter controller constraints (e.g. maxD = 0.7) had no way to express that and got false-positive "working" designs that violated their actual control range. Adds std::optional<double> maximumDutyCycle = 0.95 field with get/set accessors, and threads it through calculate_duty_cycle as a new optional 6th parameter (default 0.95 preserves backward compatibility for the existing tests in TestCuk.cpp). All 9 internal call sites updated to pass maximumDutyCycle.value_or(0.95). The inequality also gains the standard 1 % rounding tolerance to match the rest of the converter family. Tests: all 48 [cuk-topology] cases pass (324 assertions). Phase C/5 of 9. Same pattern as Flyback (04272d7), forward family (683e731), Buck (2c9300c), Boost (96fdb52), IsolatedBuck (703bc80), IsolatedBuckBoost (a158d54).
1 parent a158d54 commit 757d50c

2 files changed

Lines changed: 33 additions & 13 deletions

File tree

src/converter_models/Cuk.cpp

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ namespace OpenMagnetics {
1616
// Static analytical helpers (CUK_PLAN.md §2.13, §4.1–§4.4)
1717
// ============================================================
1818

19-
double Cuk::calculate_duty_cycle(double inputVoltage, double outputVoltageMagnitude, double diodeVoltageDrop, double efficiency, double turnsRatio) {
19+
double Cuk::calculate_duty_cycle(double inputVoltage, double outputVoltageMagnitude, double diodeVoltageDrop, double efficiency, double turnsRatio, double maximumDutyCycle) {
2020
// Cuk CCM, ideal:
2121
// V1/V2 (n=1): M(D) = -D/(1-D)
2222
// V3 isolated: M(D) = -D / ((1-D) · n), n = Np/Ns (Flyback convention)
@@ -37,10 +37,14 @@ namespace OpenMagnetics {
3737
double effVin = inputVoltage * efficiency;
3838
double reflectedVo = (outputVoltageMagnitude + diodeVoltageDrop) * turnsRatio;
3939
double dutyCycle = reflectedVo / (reflectedVo + effVin);
40-
if (dutyCycle >= 0.95) {
40+
// Explicit configurable maxD gate (no silent clamp). 1 % tolerance
41+
// absorbs design-requirements rounding.
42+
constexpr double dutyTolerance = 0.01;
43+
if (dutyCycle > maximumDutyCycle * (1.0 + dutyTolerance)) {
4144
throw InvalidInputException(ErrorCode::INVALID_INPUT,
4245
"Cuk::calculate_duty_cycle: duty cycle " + std::to_string(dutyCycle) +
43-
" >= 0.95 — converter would lose regulation; reduce |Vo| or raise Vin");
46+
" exceeds maximumDutyCycle " + std::to_string(maximumDutyCycle) +
47+
" — converter would lose regulation; reduce |Vo|, raise Vin, or relax maximumDutyCycle.");
4448
}
4549
return dutyCycle;
4650
}
@@ -114,7 +118,7 @@ namespace OpenMagnetics {
114118
for (const auto& op : get_operating_points()) {
115119
double Iout = op.get_output_currents()[0];
116120
double Vo = std::abs(op.get_output_voltages()[0]);
117-
double D = calculate_duty_cycle(maximumInputVoltage, Vo, get_diode_voltage_drop(), efficiency, turnsRatio);
121+
double D = calculate_duty_cycle(maximumInputVoltage, Vo, get_diode_voltage_drop(), efficiency, turnsRatio, maximumDutyCycle.value_or(0.95));
118122
double IL1avg = Iout * D / ((1.0 - D) * turnsRatio * efficiency);
119123
maximumDeltaIL1 = std::max(maximumDeltaIL1, rippleRatio * IL1avg);
120124
}
@@ -124,7 +128,7 @@ namespace OpenMagnetics {
124128
for (const auto& op : get_operating_points()) {
125129
double Iout = op.get_output_currents()[0];
126130
double Vo = std::abs(op.get_output_voltages()[0]);
127-
double D = calculate_duty_cycle(minimumInputVoltage, Vo, get_diode_voltage_drop(), efficiency, turnsRatio);
131+
double D = calculate_duty_cycle(minimumInputVoltage, Vo, get_diode_voltage_drop(), efficiency, turnsRatio, maximumDutyCycle.value_or(0.95));
128132
double IL1avg = Iout * D / ((1.0 - D) * turnsRatio * efficiency);
129133
double IL2avg = Iout;
130134
// Switch-current peak: IS_pk = IL1avg + IL2avg/n + ripples/2.
@@ -141,7 +145,7 @@ namespace OpenMagnetics {
141145
for (const auto& op : get_operating_points()) {
142146
double switchingFrequency = op.get_switching_frequency();
143147
double Vo = std::abs(op.get_output_voltages()[0]);
144-
double D = calculate_duty_cycle(maximumInputVoltage, Vo, get_diode_voltage_drop(), efficiency, turnsRatio);
148+
double D = calculate_duty_cycle(maximumInputVoltage, Vo, get_diode_voltage_drop(), efficiency, turnsRatio, maximumDutyCycle.value_or(0.95));
145149
double L1 = calculate_l1_min(maximumInputVoltage, D, maximumDeltaIL1, switchingFrequency);
146150
maximumNeededInductance = std::max(maximumNeededInductance, L1);
147151
}
@@ -186,7 +190,7 @@ namespace OpenMagnetics {
186190
}
187191
lastTurnsRatio = turnsRatio;
188192

189-
double dutyCycle = calculate_duty_cycle(inputVoltage, outputVoltageMag, diodeVoltageDrop, efficiency, turnsRatio);
193+
double dutyCycle = calculate_duty_cycle(inputVoltage, outputVoltageMag, diodeVoltageDrop, efficiency, turnsRatio, maximumDutyCycle.value_or(0.95));
190194

191195
// Common derived quantities (V1/V2/V3).
192196
// Power balance: Vin·η·IL1avg = |Vo|·Iout ⇒ IL1avg = |Vo|·Iout/(Vin·η).
@@ -467,7 +471,7 @@ namespace OpenMagnetics {
467471
for (const auto& op : get_operating_points()) {
468472
double Iout = op.get_output_currents()[0];
469473
double Vo = std::abs(op.get_output_voltages()[0]);
470-
double D = calculate_duty_cycle(maximumInputVoltage, Vo, get_diode_voltage_drop(), efficiency);
474+
double D = calculate_duty_cycle(maximumInputVoltage, Vo, get_diode_voltage_drop(), efficiency, 1.0, maximumDutyCycle.value_or(0.95));
471475
double IL1avg = Iout * D / (1.0 - D);
472476
maximumDeltaIL1 = std::max(maximumDeltaIL1, rippleRatio * IL1avg);
473477
}
@@ -477,7 +481,7 @@ namespace OpenMagnetics {
477481
for (const auto& op : get_operating_points()) {
478482
double Iout = op.get_output_currents()[0];
479483
double Vo = std::abs(op.get_output_voltages()[0]);
480-
double D = calculate_duty_cycle(minimumInputVoltage, Vo, get_diode_voltage_drop(), efficiency);
484+
double D = calculate_duty_cycle(minimumInputVoltage, Vo, get_diode_voltage_drop(), efficiency, 1.0, maximumDutyCycle.value_or(0.95));
481485
double IL1avg = Iout * D / (1.0 - D);
482486
// IS_peak ≈ IL1avg + IL2avg + (ΔIL1 + ΔIL2)/2; treat ΔIL2 as
483487
// ~30 % of IL2avg (the L2 sizing default), so the L1 ripple
@@ -496,7 +500,7 @@ namespace OpenMagnetics {
496500
for (const auto& op : get_operating_points()) {
497501
double switchingFrequency = op.get_switching_frequency();
498502
double Vo = std::abs(op.get_output_voltages()[0]);
499-
double D = calculate_duty_cycle(maximumInputVoltage, Vo, get_diode_voltage_drop(), efficiency);
503+
double D = calculate_duty_cycle(maximumInputVoltage, Vo, get_diode_voltage_drop(), efficiency, 1.0, maximumDutyCycle.value_or(0.95));
500504
double L1 = calculate_l1_min(maximumInputVoltage, D, maximumDeltaIL1, switchingFrequency);
501505
maximumNeededInductance = std::max(maximumNeededInductance, L1);
502506
}
@@ -538,7 +542,7 @@ namespace OpenMagnetics {
538542
double switchingFrequency = op.get_switching_frequency();
539543
double Vo = std::abs(op.get_output_voltages()[0]);
540544
double Iout = op.get_output_currents()[0];
541-
double D = calculate_duty_cycle(minimumInputVoltage, Vo, get_diode_voltage_drop(), efficiency, turnsRatio);
545+
double D = calculate_duty_cycle(minimumInputVoltage, Vo, get_diode_voltage_drop(), efficiency, turnsRatio, maximumDutyCycle.value_or(0.95));
542546
double IL1avg = Iout * D / ((1.0 - D) * turnsRatio * efficiency);
543547
double VCa = minimumInputVoltage / (1.0 - D);
544548
double deltaIm_target = std::max(0.20 * IL1avg, 1e-6);
@@ -719,7 +723,7 @@ namespace OpenMagnetics {
719723
double efficiency = 1.0;
720724
if (get_efficiency()) efficiency = get_efficiency().value();
721725

722-
double dutyCycle = calculate_duty_cycle(inputVoltage, outputVoltageMag, diodeVoltageDrop, efficiency);
726+
double dutyCycle = calculate_duty_cycle(inputVoltage, outputVoltageMag, diodeVoltageDrop, efficiency, 1.0, maximumDutyCycle.value_or(0.95));
723727

724728
// Internally-sized L2, C1, Co (mirror process_operating_points_for_input_voltage)
725729
double IL1avg = outputCurrent * dutyCycle / (1.0 - dutyCycle);

src/converter_models/Cuk.h

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,19 @@ class Cuk : public MAS::Cuk, public Topology {
134134
int numPeriodsToExtract = 5;
135135
int numSteadyStatePeriods = 50;
136136

137+
// Maximum operating duty cycle. Cuk CCM duty is
138+
// D = (|Vo|+Vd)*n / (Vin*eta + (|Vo|+Vd)*n); for low Vin or high
139+
// step-down/up ratio this approaches 1, where the conversion ratio
140+
// M = -D/(1-D) blows up and regulation collapses. Cuk previously
141+
// hard-coded a 0.95 ceiling in calculate_duty_cycle(); this field
142+
// makes it configurable so callers with stricter controller
143+
// constraints (e.g. maxD = 0.7) get a loud throw rather than
144+
// running well past their controller's range. Mirrors Flyback
145+
// (04272d7b), forward family (683e731c), Buck (2c9300c2), Boost
146+
// (96fdb52a), IsolatedBuck (703bc80e), IsolatedBuckBoost
147+
// (a158d548).
148+
std::optional<double> maximumDutyCycle = 0.95;
149+
137150
// Internal sizing rules-of-thumb for L2 / C1 / Co (V1 only).
138151
// These are fixed defaults; future revisions will expose them via
139152
// schema fields and/or DesignRequirements (see CUK_PLAN.md §13).
@@ -199,6 +212,9 @@ class Cuk : public MAS::Cuk, public Topology {
199212
int get_num_steady_state_periods() const { return numSteadyStatePeriods; }
200213
void set_num_steady_state_periods(int value) { this->numSteadyStatePeriods = value; }
201214

215+
std::optional<double> get_maximum_duty_cycle() const { return maximumDutyCycle; }
216+
void set_maximum_duty_cycle(std::optional<double> value) { this->maximumDutyCycle = value; }
217+
202218
// ---- Per-OP diagnostic accessors ----
203219
double get_last_duty_cycle() const { return lastDutyCycle; }
204220
double get_last_conversion_ratio() const { return lastConversionRatio; }
@@ -251,7 +267,7 @@ class Cuk : public MAS::Cuk, public Topology {
251267
//
252268
// For V3 isolated, turnsRatio = Np/Ns (Flyback convention). Conversion gain
253269
// becomes |Vo|/Vin = D / ((1-D) · turnsRatio); reduces to V1 when turnsRatio=1.
254-
static double calculate_duty_cycle(double inputVoltage, double outputVoltageMagnitude, double diodeVoltageDrop, double efficiency, double turnsRatio = 1.0);
270+
static double calculate_duty_cycle(double inputVoltage, double outputVoltageMagnitude, double diodeVoltageDrop, double efficiency, double turnsRatio = 1.0, double maximumDutyCycle = 0.95);
255271
static double calculate_conversion_ratio(double dutyCycle); // -D/(1-D)
256272
static double calculate_coupling_cap_voltage(double inputVoltage, double dutyCycle);
257273
static double calculate_l1_min(double inputVoltage, double dutyCycle, double deltaIL1, double switchingFrequency);

0 commit comments

Comments
 (0)