Files
JIMRI/java/test/jmri/jmrit/roster/RosterSpeedProfileTest.java
2026-06-17 14:00:51 +02:00

458 lines
20 KiB
Java

package jmri.jmrit.roster;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.HashMap;
import jmri.util.JUnitUtil;
import jmri.DccThrottle;
import jmri.InstanceManager;
import jmri.LocoAddress;
import jmri.SpeedStepMode;
import jmri.ThrottleListener;
import jmri.ThrottleManager;
import jmri.implementation.SignalSpeedMap;
import jmri.jmrit.roster.RosterSpeedProfile.SpeedSetting;
import jmri.util.JUnitAppender;
import org.junit.jupiter.api.*;
/**
*
* @author Paul Bender Copyright (C) 2017
*/
public class RosterSpeedProfileTest {
private static final float GLOBAL_TOTAL_DISTANCE_TOLERANCE = 2.0f;
private static class ThrottleListen implements ThrottleListener {
DccThrottle throttle;
@Override
public void notifyThrottleFound(DccThrottle t){
throttle = t;
}
@Override
public void notifyFailedThrottleRequest(LocoAddress address, String reason){
throttle = null;
}
@Override
public void notifyDecisionRequired(LocoAddress address, DecisionType question) {
if ( question == DecisionType.STEAL ){
throttle = null;
}
}
}
@Test
public void testCTor() {
RosterSpeedProfile t = new RosterSpeedProfile(new RosterEntry());
assertNotNull( t, "exists");
}
private static org.jdom2.Element getLocoElement100() {
return new org.jdom2.Element("locomotive")
.setAttribute("id", "id info")
.setAttribute("fileName", "file here")
.setAttribute("roadNumber", "431")
.setAttribute("roadName", "SP")
.setAttribute("mfg", "Athearn")
.setAttribute("dccAddress", "1234")
.addContent(new org.jdom2.Element("decoder")
.setAttribute("family", "91")
.setAttribute("model", "33")
)
.addContent(new org.jdom2.Element("locoaddress")
.addContent(new org.jdom2.Element("number").addContent("1234"))
//As there is no throttle manager available all protocols default to dcc short
.addContent(new org.jdom2.Element("protocol").addContent("dcc_short"))
)
.addContent(new org.jdom2.Element("speedprofile")
.addContent(new org.jdom2.Element("overRunTimeForward").addContent("0.0"))
.addContent(new org.jdom2.Element("overRunTimeReverse").addContent("0.0"))
.addContent(new org.jdom2.Element("speeds")
.addContent(new org.jdom2.Element("speed")
.addContent(new org.jdom2.Element("step").addContent("1000"))
.addContent(new org.jdom2.Element("forward").addContent("100.00"))
.addContent(new org.jdom2.Element("reverse").addContent("100.00"))
)
)
);
}
private ReturnValues testScene(RosterEntry rF1, float currentSpeed,
float newSpeed, float testDistance,
float minSpeed, float maxSpeed, SpeedStepMode speedStepMode,
DccThrottle inThrottle ) {
inThrottle.setSpeedStepMode(speedStepMode);
inThrottle.setIsForward(true);
inThrottle.setSpeedSetting(currentSpeed);
RosterSpeedProfile sp = rF1.getSpeedProfile();
float mmFactor = sp.getForwardSpeed(1.0f);
sp.setTestMode(true);
sp.setExtraInitialDelay(0f);
sp.setMinMaxLimits(minSpeed, maxSpeed);
// assertNotNull(throttle);
sp.changeLocoSpeed(inThrottle, testDistance, newSpeed);
ReturnValues returnValues = new ReturnValues();
returnValues.totalDistance = 0.0f;
returnValues.numberOfElements = sp.getSpeedStepTrace().size();
for (SpeedSetting ss : sp.getSpeedStepTrace()) {
returnValues.totalDistance += (ss.getDuration() / 1000.0f) * (ss.getSpeedStep() * mmFactor);
returnValues.finalSpeed = ss.getSpeedStep();
}
returnValues.throttleSpeedSetting = inThrottle.getSpeedSetting();
sp.cancelSpeedChange();
return returnValues;
}
ReturnValuesFromAct testSpeedStep(SpeedStepMode stm,
long fromDistanceMm, long toDistanceMm, long byDistanceMm,
float fromSpeedStep, float bySpeedStep, // limit for speed step is max throttle step
float fromSpeed, float toSpeed, float bySpeed,
float fromMin, float toMin, float byMin,
float fromMax_IsMinPlus, float toMax, float byMax,
org.jdom2.Element speedCurve) {
ReturnValuesFromAct resultSummary = new ReturnValuesFromAct();
long stmLimit;
// statics for test objects
RosterEntry rF1 = new RosterEntry(speedCurve) {
@Override
protected void warnShortLong(String s) {
}
};
ThrottleListen throtListen = new ThrottleListen();
ThrottleManager tm = InstanceManager.getDefault(ThrottleManager.class);
assertTrue( tm.requestThrottle(rF1, throtListen, false) );
stmLimit = stm.numSteps;
resultSummary.testTotalCount = 0;
for (float testDistance = fromDistanceMm; testDistance <= toDistanceMm; testDistance += byDistanceMm) {
//Runtime.getRuntime().gc();
for (float currentSpeedStep = fromSpeedStep; currentSpeedStep >= 0 &&
currentSpeedStep <= stmLimit; currentSpeedStep += bySpeedStep) {
for (float newPercentSpeed = fromSpeed; newPercentSpeed <= toSpeed; newPercentSpeed += bySpeed) {
for (float minPercentSpeed = 0.00f; minPercentSpeed <= 1.0f ; minPercentSpeed+= 0.1f) {
for (float maxPercentSpeed = minPercentSpeed + fromMax_IsMinPlus;
maxPercentSpeed <= toMax; maxPercentSpeed += byMax) {
ReturnValues returnValues = testScene(rF1, //profile
currentSpeedStep / stmLimit, //current speed
newPercentSpeed, // new speed
testDistance, // distance
minPercentSpeed, // minSpeed
maxPercentSpeed, // max speed
stm, // stepmode
throtListen.throttle
);
resultSummary.testTotalCount++;
long ix = Math.round(
(Math.abs(testDistance - returnValues.totalDistance) / testDistance) *
100.0f);
if (ix > 100) {
ix = 101;
}
float expectedSpeed = newPercentSpeed;
if (expectedSpeed > 0.0f &&
minPercentSpeed > 0.0f &&
minPercentSpeed > expectedSpeed) {
expectedSpeed = minPercentSpeed;
}
if (maxPercentSpeed < expectedSpeed) {
expectedSpeed = maxPercentSpeed;
}
// If number of elements is 0 then
// speed was not altered from the input and final speed is unset
if (returnValues.numberOfElements > 0 && Math.abs(expectedSpeed - returnValues.finalSpeed) > (1.0f / stmLimit)) {
resultSummary.failedTestEndSpeed += 1;
}
if (returnValues.numberOfElements == 0 && returnValues.totalDistance == 0.0f &&
Math.abs(expectedSpeed - (currentSpeedStep / stmLimit)) < (1.0f /
stmLimit)) {
// no speed change expected so no distance
resultSummary.zeroTests++;
resultSummary.lengthError[0] += 1;
// speed test will have always failed so dont add.
} else if ((Math.abs(testDistance - returnValues.totalDistance) /
testDistance) < GLOBAL_TOTAL_DISTANCE_TOLERANCE) {
// within tolerance
resultSummary.passedTests++;
resultSummary.lengthError[(int) ix] += 1;
} else {
resultSummary.failedTests++;
// use this to debug individual entries
// log.info("Failed {} CS {} NS {} D {} MIN {} MAX {} ACTD {} AS {} FTS {}",
// stm.name(),
// currentSpeedStep / stmLimit, //current speed
// newPercentSpeed, // new speed
// testDistance, // distance
// minPercentSpeed, // minSpeed
// maxPercentSpeed, returnValues.totalDistance, returnValues.finalSpeed,
// returnValues.throttleSpeedSetting);
resultSummary.lengthError[(int) ix] += 1;
}
// unused returnValues = null;
}
}
}
}
}
return resultSummary;
}
@Test
public void testSpeedProfile_28() {
SpeedStepMode stm = SpeedStepMode.NMRA_DCC_28;
ReturnValuesFromAct resultSummary = testSpeedStep(stm,
100L, 2000L, 100L, //distance steps
1.0f, 1.0f, //from speed
0.0f, 1.0f, 0.1f, // to speeds
0.0f, 0.3f, 0.6f, // min speeds
0.10f, 1.0f, 0.10f,
getLocoElement100());
assertEquals(0, resultSummary.failedTests, () -> getResultString( stm, resultSummary));
assertEquals(0, resultSummary.failedTestEndSpeed,() -> getResultString( stm, resultSummary));
}
@Test
public void testSpeedProfile_128() {
SpeedStepMode stm = SpeedStepMode.NMRA_DCC_128;
ReturnValuesFromAct resultSummary = testSpeedStep(stm,
100L, 2000L, 100L, //distance steps
1.0f,1.0f, //from speed
0.0f, 1.0f, 0.1f, // to speeds
0.0f, 0.3f, 0.6f, // min speeds
0.10f, 1.0f, 0.10f,
getLocoElement100());
assertEquals(0, resultSummary.failedTests, () -> getResultString( stm, resultSummary));
assertEquals(0, resultSummary.failedTestEndSpeed,() -> getResultString( stm, resultSummary));
}
@Test
public void testSpeedProfile_14() {
SpeedStepMode stm = SpeedStepMode.NMRA_DCC_14;
ReturnValuesFromAct resultSummary = testSpeedStep(stm,
100L, 2000L, 100L, //distance steps
1.0f,1.0f, //from speed
0.0f, 1.0f, 0.1f, // to speeds
0.0f, 0.3f, 0.6f, // min speeds
0.10f, 1.0f, 0.10f,
getLocoElement100());
assertEquals(0, resultSummary.failedTests, () -> getResultString( stm, resultSummary));
assertEquals(0, resultSummary.failedTestEndSpeed,() -> getResultString( stm, resultSummary));
}
@Test
public void testSpeedProfile_100() {
SpeedStepMode stm = SpeedStepMode.TMCC1_100;
ReturnValuesFromAct resultSummary = testSpeedStep(stm,
100L, 2000L, 100L, //distance steps
1.0f, 1.0f, //from speed
0.0f, 1.0f, 0.1f, // to speeds
0.0f, 0.3f, 0.6f, // min speeds
0.10f, 1.0f, 0.10f,
getLocoElement100());
assertEquals(0, resultSummary.failedTests, () -> getResultString( stm, resultSummary));
assertEquals(0, resultSummary.failedTestEndSpeed, () -> getResultString( stm, resultSummary));
}
private String getResultString(SpeedStepMode stm, ReturnValuesFromAct resultSummary) {
return stm.name + " Tests [" + resultSummary.testTotalCount
+ "] run. Length Failed [" + resultSummary.failedTests
+ "] Final Speed errors [" + + resultSummary.failedTestEndSpeed + "]"
+ " Zero:" + resultSummary.zeroTests
+ " Passed:" + resultSummary.passedTests;
}
@Test
public void testSpeedProfileFromFiftyPercentToTwentyShortBlock() {
// statics for test objects
org.jdom2.Element f1 = getLocoElement400();
RosterEntry rF1 = new RosterEntry(f1) {
@Override
protected void warnShortLong(String s) {
}
};
ThrottleListen throtListen = new ThrottleListen();
ThrottleManager tm = InstanceManager.getDefault(ThrottleManager.class);
assertTrue( tm.requestThrottle(rF1, throtListen, false), "Throttle request denied");
JUnitUtil.waitFor(()-> ( throtListen.throttle != null), "Got No throttle");
DccThrottle throttle = throtListen.throttle;
assertNotNull(throttle);
throttle.setIsForward(true);
throttle.setSpeedSetting(0.6f);
RosterSpeedProfile sp = rF1.getSpeedProfile();
sp.setTestMode(true);
sp.setExtraInitialDelay(1500f);
sp.changeLocoSpeed(throttle, 152.0f, 0.20f);
// Allow speed step table to be constructed
//JUnitUtil.waitFor(3000);
// Note it must be a perfect 0.20
JUnitUtil.waitFor(()-> Float.compare(throttle.getSpeedSetting(), 0.20f)==0,
"Failed to reach requested speed");
JUnitAppender.assertWarnMessageStartsWith("There is insufficient distance");
// as the calc goes wrong we immediately set speed to final speed. The entries are rubbish so dont bother checking
assertEquals( 0, sp.getSpeedStepTrace().size(), "SpeedStep Table has incorrect number of entries.");
sp.cancelSpeedChange();
}
private static org.jdom2.Element getLocoElement400 () {
return new org.jdom2.Element("locomotive")
.setAttribute("id", "id info")
.setAttribute("fileName", "file here")
.setAttribute("roadNumber", "431")
.setAttribute("roadName", "SP")
.setAttribute("mfg", "Athearn")
.setAttribute("dccAddress", "1234")
.addContent(new org.jdom2.Element("decoder")
.setAttribute("family", "91")
.setAttribute("model", "33")
)
.addContent(new org.jdom2.Element("locoaddress")
.addContent(new org.jdom2.Element("number").addContent("1234"))
//As there is no throttle manager available all protocols default to dcc short
.addContent(new org.jdom2.Element("protocol").addContent("dcc_short"))
)
.addContent(new org.jdom2.Element("speedprofile")
.addContent(new org.jdom2.Element("overRunTimeForward").addContent("0.0"))
.addContent(new org.jdom2.Element("overRunTimeReverse").addContent("0.0"))
.addContent(new org.jdom2.Element("speeds")
.addContent(new org.jdom2.Element("speed")
.addContent(new org.jdom2.Element("step").addContent("200"))
.addContent(new org.jdom2.Element("forward").addContent("40.00"))
.addContent(new org.jdom2.Element("reverse").addContent("40.00"))
)
.addContent(new org.jdom2.Element("speed")
.addContent(new org.jdom2.Element("step").addContent("1000"))
.addContent(new org.jdom2.Element("forward").addContent("400.00"))
.addContent(new org.jdom2.Element("reverse").addContent("400.00"))
)
)
);
}
@Test
public void testconvertThrottleSettingToScaleSpeedWithUnits(){
SignalSpeedMap ssm = InstanceManager.getDefault(SignalSpeedMap.class);
setSpeedInterpretation(ssm, SignalSpeedMap.PERCENT_NORMAL);
assertEquals("0.50 millimeters/sec",RosterSpeedProfile.convertMMSToScaleSpeedWithUnits(0.5f));
setSpeedInterpretation(ssm, SignalSpeedMap.PERCENT_THROTTLE);
assertEquals("0.50 millimeters/sec",RosterSpeedProfile.convertMMSToScaleSpeedWithUnits(0.5f));
setSpeedInterpretation(ssm, SignalSpeedMap.SPEED_KMPH);
assertEquals("0.16 Kilometers/Hour",RosterSpeedProfile.convertMMSToScaleSpeedWithUnits(0.5f));
setSpeedInterpretation(ssm, SignalSpeedMap.SPEED_MPH);
assertEquals("0.10 Miles/Hour",RosterSpeedProfile.convertMMSToScaleSpeedWithUnits(0.5f));
}
@Test
public void testMmsToScaleSpeed(){
org.jdom2.Element f1 = getLocoElement100();
RosterEntry rF1 = new RosterEntry(f1) {
@Override
protected void warnShortLong(String s) {
}
};
RosterSpeedProfile rsp = rF1.getSpeedProfile();
SignalSpeedMap ssm = InstanceManager.getDefault(SignalSpeedMap.class);
var timeBase = InstanceManager.getDefault(jmri.Timebase.class);
timeBase.setRun(false);
Assertions.assertDoesNotThrow(() -> { timeBase.userSetRate(1.0d); } );
setSpeedInterpretation(ssm, SignalSpeedMap.PERCENT_THROTTLE);
Assertions.assertEquals(10.0f, rsp.mmsToScaleSpeed(10, false), 0.0001);
Assertions.assertEquals(10.0f, rsp.mmsToScaleSpeed(10, true), 0.0001);
setSpeedInterpretation(ssm, SignalSpeedMap.SPEED_KMPH);
Assertions.assertEquals(3.13559f, rsp.mmsToScaleSpeed(10, false), 0.001);
Assertions.assertEquals(3.13559f, rsp.mmsToScaleSpeed(10, true), 0.001);
setSpeedInterpretation(ssm, SignalSpeedMap.SPEED_MPH);
Assertions.assertEquals(1.94837f, rsp.mmsToScaleSpeed(10, false), 0.0001);
Assertions.assertEquals(1.94837f, rsp.mmsToScaleSpeed(10, true), 0.0001);
Assertions.assertDoesNotThrow(() -> { timeBase.userSetRate(2.0d); } );
Assertions.assertEquals(1.94837f, rsp.mmsToScaleSpeed(10, false), 0.0001);
Assertions.assertEquals(3.89675f, rsp.mmsToScaleSpeed(10, true), 0.0001);
setSpeedInterpretation(ssm, SignalSpeedMap.SPEED_KMPH);
Assertions.assertEquals(3.13559f, rsp.mmsToScaleSpeed(10, false), 0.001);
Assertions.assertEquals(6.27119f, rsp.mmsToScaleSpeed(10, true), 0.001);
}
private void setSpeedInterpretation(SignalSpeedMap map, int interpretation) {
var speedNames = map.getValidSpeedNames();
HashMap<String, Float> newMap = new HashMap<>(speedNames.size());
for ( var speedName : speedNames ) {
newMap.put(speedName, map.getSpeed(speedName));
// System.out.println("key " + speedName + " value: " + map.getSpeed(speedName));
}
map.setAspects(newMap, interpretation);
}
@BeforeEach
public void setUp() {
JUnitUtil.setUp();
JUnitUtil.initDebugThrottleManager();
}
@AfterEach
public void tearDown() {
JUnitUtil.tearDown();
}
private static class ReturnValues {
// return values from an individual scene
float totalDistance;
float finalSpeed;
int numberOfElements;
float throttleSpeedSetting;
ReturnValues() {
totalDistance = 0.0f;
finalSpeed = 0.0f;
numberOfElements = 0;
throttleSpeedSetting = 0.0f;
}
// Unused Ctor
//ReturnValues(float totalDistance, float finalSpeed, int numberOfElements) {
// this.totalDistance = totalDistance;
// this.finalSpeed = finalSpeed;
// this.numberOfElements = numberOfElements;
//}
@Override
public String toString() {
return Float.toString(totalDistance)
+ "," + Float.toString(finalSpeed)
+ "," + Integer.toString(numberOfElements)
+ "," + Float.toString(throttleSpeedSetting);
}
}
private static class ReturnValuesFromAct {
// return values from a collection of scenes
long passedTests;
long failedTests;
long zeroTests;
//float finalSpeed;
long[] lengthError = new long[102];
long failedTestEndSpeed;
long testTotalCount;
}
}