Fix consumption tracking while charging
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
package com.evsuite.abrp;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
public class ChargingStateTest {
|
||||
|
||||
@Test
|
||||
public void vendorStatusIsConclusiveOnItsOwn() {
|
||||
// The port says false and the power is unreadable: status still decides.
|
||||
assertTrue(ChargingState.isCharging(1, Boolean.FALSE, null, null));
|
||||
assertTrue(ChargingState.isCharging(10, Boolean.FALSE, null, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void anIdleStatusDoesNotByItselfRuleChargingOut() {
|
||||
// Status 0 with the cable in and power flowing in is still a charge; the vendor
|
||||
// code is one signal among several, not a veto.
|
||||
assertTrue(ChargingState.isCharging(0, Boolean.TRUE, -7f, 0f));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inboundPowerAtAStandstillIsACharge() {
|
||||
// This is the case the old port-only test missed: plugged in, port reports false.
|
||||
assertTrue(ChargingState.isCharging(null, Boolean.FALSE, -7f, 0f));
|
||||
assertTrue(ChargingState.isCharging(null, null, -45f, 0f));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inboundPowerWhileMovingIsRegenerationNotCharging() {
|
||||
assertFalse(ChargingState.isCharging(null, Boolean.FALSE, -20f, 60f));
|
||||
assertFalse(ChargingState.isCharging(null, null, -20f, 60f));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void thePortStillCountsWhenItDoesAnswer() {
|
||||
assertTrue(ChargingState.isCharging(null, Boolean.TRUE, -7f, 0f));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void outboundPowerIsNeverACharge() {
|
||||
assertFalse(ChargingState.isCharging(null, Boolean.TRUE, 20f, 60f));
|
||||
assertFalse(ChargingState.isCharging(0, Boolean.TRUE, 2f, 0f));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void noiseAroundZeroIsNotACharge() {
|
||||
assertFalse(ChargingState.isCharging(null, Boolean.TRUE, -0.1f, 0f));
|
||||
assertFalse(ChargingState.isCharging(null, Boolean.TRUE, 0f, 0f));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unreadablePowerWithoutAStatusIsNotAssumedToBeACharge() {
|
||||
assertFalse(ChargingState.isCharging(null, Boolean.TRUE, null, 0f));
|
||||
assertFalse(ChargingState.isCharging(null, null, null, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unreadableSpeedWithInboundPowerFallsBackToCharging() {
|
||||
// Conservative on purpose: without a speed we cannot tell a charge from regen, and
|
||||
// treating it as a charge only suppresses a SOC delta. The opposite choice invents
|
||||
// negative consumption, which is the bug this class was written to stop.
|
||||
assertTrue(ChargingState.isCharging(null, null, -7f, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void creepingSpeedIsStillAStandstill() {
|
||||
// Same dead band the integrator uses, so the two cannot disagree about motion.
|
||||
assertTrue(ChargingState.isCharging(null, null, -7f, 1f));
|
||||
assertFalse(ChargingState.isCharging(null, null, -7f, 10f));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
package com.evsuite.abrp;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
public class ConsumptionMathTest {
|
||||
|
||||
/** One minute, comfortably inside {@link ConsumptionMath#MAX_GAP_HOURS}. */
|
||||
private static final double DT = 1d / 60d;
|
||||
|
||||
/** Float inputs widen to double exactly, but the arithmetic is done in float. */
|
||||
private static final double TOLERANCE = 1e-6;
|
||||
|
||||
@Test
|
||||
public void nonPositiveOrOversizedIntervalsAreDropped() {
|
||||
assertNull(ConsumptionMath.integrate(0d, 50f, 50f, 20f, 20f, 80f, 79f, false));
|
||||
assertNull(ConsumptionMath.integrate(-1d, 50f, 50f, 20f, 20f, 80f, 79f, false));
|
||||
// A process pause must not be bridged into invented driving data.
|
||||
assertNull(ConsumptionMath.integrate(ConsumptionMath.MAX_GAP_HOURS + 0.001d,
|
||||
50f, 50f, 20f, 20f, 80f, 79f, false));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void distanceIsTheTrapezoidOfTheTwoSpeeds() {
|
||||
// 20 and 40 km/h average to 30, which sits on the uncalibrated side of the band.
|
||||
ConsumptionMath.Step step =
|
||||
ConsumptionMath.integrate(DT, 20f, 40f, null, null, null, null, false);
|
||||
assertEquals(30d * DT, step.km, TOLERANCE);
|
||||
assertEquals(DT, step.hours, TOLERANCE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void speedBelowTheDeadBandContributesNoDistance() {
|
||||
ConsumptionMath.Step step =
|
||||
ConsumptionMath.integrate(DT, 2f, 2f, null, null, null, null, false);
|
||||
assertEquals(0d, step.km, TOLERANCE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void motorwayAndMidRangeSpeedsAreCalibrated() {
|
||||
ConsumptionMath.Step mid =
|
||||
ConsumptionMath.integrate(DT, 50f, 50f, null, null, null, null, false);
|
||||
assertEquals(50d * 1.0015d * DT, mid.km, TOLERANCE);
|
||||
|
||||
ConsumptionMath.Step motorway =
|
||||
ConsumptionMath.integrate(DT, 120f, 120f, null, null, null, null, false);
|
||||
assertEquals(120d * 1.0035d * DT, motorway.km, TOLERANCE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void energyIsTheTrapezoidOfNetPowerAndRegenerationSubtracts() {
|
||||
ConsumptionMath.Step drawing =
|
||||
ConsumptionMath.integrate(DT, 60f, 60f, 10f, 20f, null, null, false);
|
||||
assertEquals(15d * DT, drawing.kwh, TOLERANCE);
|
||||
|
||||
// Regeneration keeps its sign: this is what the car's own kWh figure does, and
|
||||
// discarding it is what used to make our number read high.
|
||||
ConsumptionMath.Step regenerating =
|
||||
ConsumptionMath.integrate(DT, 60f, 60f, -20f, -20f, null, null, false);
|
||||
assertEquals(-20d * DT, regenerating.kwh, TOLERANCE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void standstillPowerFlowingInIsNotCreditedToTheDrive() {
|
||||
ConsumptionMath.Step step =
|
||||
ConsumptionMath.integrate(DT, 0f, 0f, -7f, -7f, null, null, false);
|
||||
assertEquals(0d, step.kwh, TOLERANCE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void standstillPowerFlowingOutStillCounts() {
|
||||
// Parked with the climate running is real consumption.
|
||||
ConsumptionMath.Step step =
|
||||
ConsumptionMath.integrate(DT, 0f, 0f, 2f, 2f, null, null, false);
|
||||
assertEquals(2d * DT, step.kwh, TOLERANCE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void anImplausiblePowerReadingSuppressesEnergyButNotDistance() {
|
||||
ConsumptionMath.Step step = ConsumptionMath.integrate(
|
||||
DT, 60f, 60f, 20f, ConsumptionMath.MAX_PLAUSIBLE_KW + 1f, null, null, false);
|
||||
assertEquals(0d, step.kwh, TOLERANCE);
|
||||
assertEquals(60d * 1.0015d * DT, step.km, TOLERANCE);
|
||||
assertEquals(DT, step.hours, TOLERANCE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aMissingPowerReadingSuppressesEnergyOnlyForThatInterval() {
|
||||
ConsumptionMath.Step step =
|
||||
ConsumptionMath.integrate(DT, 60f, 60f, 20f, null, null, null, false);
|
||||
assertEquals(0d, step.kwh, TOLERANCE);
|
||||
assertEquals(DT, step.hours, TOLERANCE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aMissingSpeedReadingFallsBackToThePreviousOne() {
|
||||
ConsumptionMath.Step step =
|
||||
ConsumptionMath.integrate(DT, 60f, null, 20f, 20f, null, null, false);
|
||||
assertEquals(60d * 1.0015d * DT, step.km, TOLERANCE);
|
||||
assertEquals(20d * DT, step.kwh, TOLERANCE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void reverseIsDistanceTravelled() {
|
||||
ConsumptionMath.Step step =
|
||||
ConsumptionMath.integrate(DT, -10f, -10f, null, null, null, null, false);
|
||||
assertEquals(10d * DT, step.km, TOLERANCE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void socDropIsCountedOnlyOffTheCharger() {
|
||||
ConsumptionMath.Step driving =
|
||||
ConsumptionMath.integrate(DT, 60f, 60f, 20f, 20f, 80f, 79.5f, false);
|
||||
assertEquals(0.5d, driving.socDrop, TOLERANCE);
|
||||
|
||||
ConsumptionMath.Step charging =
|
||||
ConsumptionMath.integrate(DT, 0f, 0f, -40f, -40f, 50f, 52f, true);
|
||||
assertEquals(0d, charging.socDrop, TOLERANCE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aMissingSocEndpointYieldsNoDrop() {
|
||||
ConsumptionMath.Step step =
|
||||
ConsumptionMath.integrate(DT, 60f, 60f, 20f, 20f, null, 79f, false);
|
||||
assertEquals(0d, step.socDrop, TOLERANCE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aSteadyDriveIntegratesToTheExpectedAverage() {
|
||||
// 90 km/h at 15 kW for an hour: ~16.6 kWh/100 km, a plausible MG4 motorway figure.
|
||||
KahanSum km = new KahanSum();
|
||||
KahanSum kwh = new KahanSum();
|
||||
for (int i = 0; i < 3600; i++) {
|
||||
ConsumptionMath.Step step = ConsumptionMath.integrate(
|
||||
1d / 3600d, 90f, 90f, 15f, 15f, null, null, false);
|
||||
km.add(step.km);
|
||||
kwh.add(step.kwh);
|
||||
}
|
||||
assertEquals(90d * 1.0015d, km.get(), 1e-4);
|
||||
assertEquals(15d, kwh.get(), 1e-9);
|
||||
assertEquals(16.64d, kwh.get() * 100d / km.get(), 0.01d);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void regenerationOffsetsTheDrawItFollows() {
|
||||
// Ten minutes pulling 30 kW, then ten minutes recovering 6 kW: the net is what the
|
||||
// dashboard shows, and the old gross-only fallback reported 5.0 kWh for this.
|
||||
KahanSum kwh = new KahanSum();
|
||||
for (int i = 0; i < 10; i++) {
|
||||
kwh.add(ConsumptionMath.integrate(DT, 60f, 60f, 30f, 30f, null, null, false).kwh);
|
||||
}
|
||||
for (int i = 0; i < 10; i++) {
|
||||
kwh.add(ConsumptionMath.integrate(DT, 60f, 60f, -6f, -6f, null, null, false).kwh);
|
||||
}
|
||||
assertEquals(4d, kwh.get(), 1e-4);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user