Skip to content

Commit 617dc49

Browse files
committed
fix(inputs): always subtract integration constant when deriving magnetizing current from voltage
The is_continuously_conducting_power heuristic was wrong for pure inductor mode: a pure inductor has V and I in ~90 degree quadrature, so V*I ~ sin(2wt) crosses zero often, causing the heuristic to misclassify the load as discontinuous and skip average subtraction of the integrated voltage waveform. For harmonic-rich AC excitations (e.g. sinusoidal with high THD), the integration constant was left in place, producing a unipolar [0, +Imax] magnetizing current and an asymmetric flux density whose processed.peak equalled peak-to-peak (Bpeak = 2 x BACpeak in the UI). Fix: always pass subtractAverage=true in this branch. Any intentional DC bias is supplied separately via the dcCurrent argument and added back after. Adds regression test Test_Magnetizing_Current_Pure_Inductor_With_Measured_Current_Is_Bipolar that verifies the MC is bipolar-centered and peak ~ peakToPeak/2. Also fixes test data path helper in TestMagneticAdviser.cpp (unrelated cleanup).
1 parent dfcc041 commit 617dc49

3 files changed

Lines changed: 80 additions & 4 deletions

File tree

src/processors/Inputs.cpp

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2278,8 +2278,24 @@ SignalDescriptor Inputs::calculate_magnetizing_current(OperatingPointExcitation&
22782278
sampledMagnetizingCurrentWaveform = calculate_sampled_waveform(newWaveform, excitation.get_frequency());
22792279
}
22802280
else {
2281-
bool subtractAverage = is_continuously_conducting_power(excitation);
2282-
sampledMagnetizingCurrentWaveform = calculate_integral_waveform(voltageSampledWaveform, subtractAverage);
2281+
// Always subtract the integration constant: in steady state, a pure
2282+
// inductor driven by a symmetric AC voltage has zero net DC in the
2283+
// magnetizing current. The integration of V(t) produces an arbitrary
2284+
// constant of integration that depends on where the numerical
2285+
// integration starts within the cycle — for asymmetric / harmonic-rich
2286+
// waveforms (e.g. distorted "sinusoidal" with high THD) this constant
2287+
// is non-zero and lopsides the resulting current into a unipolar
2288+
// [0, +Imax] shape. Any intentional DC bias is supplied separately
2289+
// via `dcCurrent` and added back below; the integration constant
2290+
// itself is physically meaningless and must always be removed.
2291+
//
2292+
// The previous `is_continuously_conducting_power(excitation)` heuristic
2293+
// was wrong for inductor mode: a pure inductor has V and I in ~90°
2294+
// quadrature, so V·I ≈ sin(2ωt) crosses zero often and the heuristic
2295+
// misclassified the load as discontinuous, leaving the spurious DC in
2296+
// place. The result was Bpeak = peakToPeak (instead of peakToPeak/2)
2297+
// for harmonic-rich AC excitations.
2298+
sampledMagnetizingCurrentWaveform = calculate_integral_waveform(voltageSampledWaveform, true);
22832299
SignalDescriptor magnetizingCurrentExcitation;
22842300

22852301

tests/TestInputs.cpp

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1258,6 +1258,66 @@ TEST_CASE("Test_Magnetizing_Current_Sinusoidal_Dc_Bias_Processed", "[processor][
12581258
REQUIRE_THAT(excitation.get_magnetizing_current().value().get_processed().value().get_peak().value(), Catch::Matchers::WithinAbs(expectedValue, max_error * expectedValue));
12591259
}
12601260

1261+
// Regression: pure-inductor AC excitation with V *and* I both supplied (e.g. a
1262+
// measured / pre-populated current waveform) must still produce a bipolar
1263+
// centered magnetizing current. The previous behaviour fed the excitation into
1264+
// `is_continuously_conducting_power`, which classifies V·I ≈ sin(2ωt) (V & I
1265+
// in ~90° quadrature) as discontinuous because the product spends >10% of the
1266+
// cycle near zero, then suppressed the average-subtraction of the integrated
1267+
// voltage. The integration constant of V(t) was therefore left in place,
1268+
// producing a unipolar [0, +Imax] magnetizing current and an asymmetric flux
1269+
// density whose `processed.peak` was equal to its peak-to-peak (Bpeak =
1270+
// 2 × BACpeak in the UI). See WebFrontend OP3 "HARMONICS" reproducer.
1271+
TEST_CASE("Test_Magnetizing_Current_Pure_Inductor_With_Measured_Current_Is_Bipolar", "[processor][inputs][smoke-test]") {
1272+
json inputsJson;
1273+
inputsJson["operatingPoints"] = json::array();
1274+
json operatingPoint = json();
1275+
operatingPoint["name"] = "Nominal";
1276+
operatingPoint["conditions"] = json();
1277+
operatingPoint["conditions"]["ambientTemperature"] = 42;
1278+
1279+
// Sinusoidal AC voltage, zero DC, no DC bias requested.
1280+
json windingExcitation = json();
1281+
windingExcitation["frequency"] = 200000;
1282+
windingExcitation["voltage"]["processed"]["label"] = WaveformLabel::SINUSOIDAL;
1283+
windingExcitation["voltage"]["processed"]["offset"] = 0;
1284+
windingExcitation["voltage"]["processed"]["peakToPeak"] = 2000;
1285+
// Pre-populated inductor current with zero DC component. This is what
1286+
// turns on the V+I path through is_continuously_conducting_power in the
1287+
// bug scenario.
1288+
windingExcitation["current"]["processed"]["label"] = WaveformLabel::SINUSOIDAL;
1289+
windingExcitation["current"]["processed"]["offset"] = 0;
1290+
windingExcitation["current"]["processed"]["peakToPeak"] = 2;
1291+
operatingPoint["excitationsPerWinding"] = json::array();
1292+
operatingPoint["excitationsPerWinding"].push_back(windingExcitation);
1293+
inputsJson["operatingPoints"].push_back(operatingPoint);
1294+
1295+
inputsJson["designRequirements"] = json();
1296+
inputsJson["designRequirements"]["magnetizingInductance"]["nominal"] = 100e-6;
1297+
inputsJson["designRequirements"]["turnsRatios"] = json::array();
1298+
1299+
OpenMagnetics::Inputs inputs(inputsJson);
1300+
auto excitation = inputs.get_operating_points()[0].get_excitations_per_winding()[0];
1301+
REQUIRE(excitation.get_magnetizing_current().has_value());
1302+
auto mcProcessed = excitation.get_magnetizing_current().value().get_processed().value();
1303+
1304+
double positivePeak = mcProcessed.get_positive_peak().value();
1305+
double negativePeak = mcProcessed.get_negative_peak().value();
1306+
double peak = mcProcessed.get_peak().value();
1307+
double peakToPeak = mcProcessed.get_peak_to_peak().value();
1308+
double offset = mcProcessed.get_offset();
1309+
1310+
// Bipolar centered: |+peak| ≈ |-peak|, midpoint ≈ 0.
1311+
double midpoint = (positivePeak + negativePeak) / 2;
1312+
double tolerance = 0.01 * peakToPeak; // 1% of swing
1313+
REQUIRE_THAT(midpoint, Catch::Matchers::WithinAbs(0.0, tolerance));
1314+
REQUIRE_THAT(offset, Catch::Matchers::WithinAbs(0.0, tolerance));
1315+
REQUIRE(positivePeak > 0);
1316+
REQUIRE(negativePeak < 0);
1317+
// peak must be the half-swing (max of |+peak|, |-peak|), not the full PP.
1318+
REQUIRE_THAT(peak, Catch::Matchers::WithinRel(peakToPeak / 2, 0.02));
1319+
}
1320+
12611321
TEST_CASE("Test_Waveform_Coefficient_Sinusoidal", "[processor][inputs][smoke-test]") {
12621322
json inputsJson;
12631323

tests/TestMagneticAdviser.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3205,7 +3205,7 @@ namespace {
32053205
TEST_CASE("Test_PlanarAdvise_Flow", "[adviser][planar][integration]") {
32063206
try {
32073207
// Load inputs from JSON file
3208-
std::ifstream file("tests/testData/planar_advise.json");
3208+
std::ifstream file(OpenMagneticsTesting::get_test_data_path(std::source_location::current(), "planar_advise.json"));
32093209
REQUIRE(file.is_open());
32103210

32113211
json j;
@@ -3354,7 +3354,7 @@ namespace {
33543354
TEST_CASE("Test_Planar_CoilAdviser_From_Full_MAS", "[adviser][planar][coil][full-mas]") {
33553355
try {
33563356
// Load inputs from extracted JSON file (cleaned from full MAS)
3357-
std::ifstream file("tests/testData/planar_advise_from_mas_inputs.json");
3357+
std::ifstream file(OpenMagneticsTesting::get_test_data_path(std::source_location::current(), "planar_advise_from_mas_inputs.json"));
33583358
REQUIRE(file.is_open());
33593359

33603360
json j;

0 commit comments

Comments
 (0)