/* * Copyright 2015-2016 Cel Skeggs. * * This file is part of the CCRE, the Common Chicken Runtime Engine. * * The CCRE is free software: you can redistribute it and/or modify it under the * terms of the GNU Lesser General Public License as published by the Free * Software Foundation, either version 3 of the License, or (at your option) any * later version. * * The CCRE is distributed in the hope that it will be useful, but WITHOUT ANY * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR * A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more * details. * * You should have received a copy of the GNU Lesser General Public License * along with the CCRE. If not, see <http://www.gnu.org/licenses/>. */ package ccre.ctrl; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import org.junit.After; import org.junit.Before; import org.junit.Test; import ccre.channel.CancelOutput; import ccre.channel.EventCell; import ccre.channel.EventInput; import ccre.channel.FloatCell; import ccre.channel.FloatInput; import ccre.scheduler.VirtualTime; import ccre.testing.CountingEventOutput; @SuppressWarnings("javadoc") public class PIDControllerTest { private FloatCell input, setpoint, P, I, D; private PIDController pid; @Before public void setUp() throws Exception { input = new FloatCell(0); setpoint = new FloatCell(0); P = new FloatCell(1); I = new FloatCell(0.1f); D = new FloatCell(0.01f); pid = new PIDController(input, setpoint, P, I, D); } @After public void tearDown() throws Exception { input = setpoint = null; pid = null; } @Test public void testCreateFixed() { EventCell updateOn = new EventCell(); setpoint.set(0); PIDController cf = PIDController.createFixed(updateOn, input, setpoint, P.get(), I.get(), D.get()); setpoint.set(1); cf.update(1000); assertEquals(cf.get(), P.get() + I.get() + D.get(), 0.000001); } @Test(expected = NullPointerException.class) public void testCreateFixedNullA() { PIDController.createFixed(null, input, setpoint, 1, 1, 1); } @Test(expected = NullPointerException.class) public void testCreateFixedNullB() { PIDController.createFixed(EventInput.never, null, setpoint, 1, 1, 1); } @Test(expected = NullPointerException.class) public void testCreateFixedNullC() { PIDController.createFixed(EventInput.never, input, null, 1, 1, 1); } @Test(expected = NullPointerException.class) public void testCreateNullA() { new PIDController(null, setpoint, P, I, D); } @Test(expected = NullPointerException.class) public void testCreateNullB() { new PIDController(input, null, P, I, D); } @Test(expected = NullPointerException.class) public void testCreateNullC() { new PIDController(input, setpoint, null, I, D); } @Test(expected = NullPointerException.class) public void testCreateNullD() { new PIDController(input, setpoint, P, null, D); } @Test(expected = NullPointerException.class) public void testCreateNullE() { new PIDController(input, setpoint, P, I, null); } @Test public void testPIDControllerSignsPositive() { input.set(5); setpoint.set(10); // which means that an update should yield a positive result, because we // want to have a greater input! pid.update(1000); assertTrue(pid.get() > 0); } @Test public void testPIDControllerSignsNegative() { input.set(10); setpoint.set(5); // which means that an update should yield a negative result, because we // want to have a smaller input! pid.update(1000); assertTrue(pid.get() < 0); } @Test public void testSetOutputBoundsFloat() { input.set(0); setpoint.set(100); pid.setOutputBounds(0.3f); pid.update(1000); assertTrue(Math.abs(pid.get()) <= 0.3f); setpoint.set(-10000); pid.update(1000); assertTrue(Math.abs(pid.get()) <= 0.3f); } @Test public void testSetOutputBoundsFloatInput() { input.set(0); setpoint.set(100); FloatCell bound = new FloatCell(0.4f); pid.setOutputBounds(bound); pid.update(1000); assertTrue(Math.abs(pid.get()) == 0.4f); setpoint.set(-10000); bound.set(0.2f); pid.update(1000); assertTrue(Math.abs(pid.get()) == 0.2f); } @Test public void testSetIntegralBoundsFloat() { input.set(0); setpoint.set(1000); pid.update(1000); assertTrue(pid.integralTotal.get() > 10); pid.integralTotal.set(0); setpoint.set(0); pid.update(1000); assertTrue(pid.integralTotal.get() == 0); pid.setIntegralBounds(1.3f); setpoint.set(1000); pid.update(1000); assertTrue(pid.integralTotal.get() == 1.3f); setpoint.set(-10000000); pid.update(1000); assertTrue(pid.integralTotal.get() == -1.3f); } @Test public void testSetIntegralBoundsFloatInput() { input.set(0); setpoint.set(1000); pid.update(1000); assertTrue(pid.integralTotal.get() > 10); pid.integralTotal.set(0); setpoint.set(0); pid.update(1000); assertTrue(pid.integralTotal.get() == 0); FloatCell ib = new FloatCell(1.7f); pid.setIntegralBounds(ib); setpoint.set(1000); pid.update(1000); assertTrue(pid.integralTotal.get() == 1.7f); ib.set(1.8f); pid.update(1000); assertTrue(pid.integralTotal.get() == 1.8f); ib.set(1.1f); pid.update(1000); assertTrue(pid.integralTotal.get() == 1.1f); } @Test public void testSetMaximumTimeDeltaFloat() { pid = new PIDController(input, setpoint, FloatInput.zero, I, FloatInput.zero); pid.setMaximumTimeDelta(0.1f); setpoint.set(1); pid.update(650); float shortValue = pid.get(); pid = new PIDController(input, setpoint, FloatInput.zero, I, FloatInput.zero); pid.setMaximumTimeDelta(0.6f); setpoint.set(1); pid.update(650); float longValue = pid.get(); assertEquals(longValue, shortValue * 6, longValue / 1000); } @Test public void testSetMaximumTimeDeltaFloatInput() { FloatCell max = new FloatCell(0.1f); pid = new PIDController(input, setpoint, FloatInput.zero, I, FloatInput.zero); pid.setMaximumTimeDelta(max); setpoint.set(1); pid.update(650); float shortValue = pid.get(); setpoint.set(0); max.set(0.6f); pid.update(1000); pid.integralTotal.set(0); setpoint.set(1); pid.update(650); float longValue = pid.get(); assertEquals(longValue, shortValue * 6, longValue / 1000); } @Test(expected = IllegalArgumentException.class) public void testUpdateNegative() { pid.update(-1); } @Test public void testEvent() throws InterruptedException { setpoint.set(1); pid = new PIDController(input, setpoint, P, I, D); pid.update(1000); pid.update(50); float result = pid.get(); VirtualTime.startFakeTime(); VirtualTime.forward(123429); pid = new PIDController(input, setpoint, P, I, D); pid.event(); VirtualTime.forward(50); pid.event(); assertEquals(result, pid.get(), 0.000001); VirtualTime.endFakeTime(); } @Test public void testOnUpdate() { setpoint.set(1); CountingEventOutput ceo = new CountingEventOutput(); pid.onUpdate(ceo); ceo.ifExpected = true; pid.update(1000); ceo.check(); ceo.ifExpected = true; pid.update(1000); ceo.check(); } @Test public void testOnUpdateR() { setpoint.set(1); CountingEventOutput ceo = new CountingEventOutput(); CancelOutput unbind = pid.onUpdate(ceo); ceo.ifExpected = true; pid.update(1000); ceo.check(); unbind.cancel(); pid.update(1000); } @Test public void testSums() throws Throwable { setpoint.set(1); pid.update(1000); float total = pid.get(); pid = new PIDController(input, setpoint, P, FloatInput.zero, FloatInput.zero); pid.update(1000); float p = pid.get(); pid = new PIDController(input, setpoint, FloatInput.zero, I, FloatInput.zero); pid.update(1000); float i = pid.get(); pid = new PIDController(input, setpoint, FloatInput.zero, FloatInput.zero, D); pid.update(1000); float d = pid.get(); assertEquals(total, p + i + d, 0.00001f); } @Test public void testProportional() throws Throwable { pid = new PIDController(input, setpoint, P, FloatInput.zero, FloatInput.zero); input.set(1); setpoint.set(3.2f); pid.update(100); assertEquals(setpoint.get() - input.get(), pid.get(), 0.0001f); } @Test public void testIntegral() throws Throwable { pid = new PIDController(input, setpoint, FloatInput.zero, I, FloatInput.zero); input.set(1); setpoint.set(3.2f); float base_integral = 1.773f; pid.integralTotal.set(base_integral); pid.update(100); assertEquals(0.1f * (setpoint.get() - input.get()), pid.integralTotal.get() - base_integral, 0.0001f); assertEquals(I.get() * pid.integralTotal.get(), pid.get(), 0.0001f); } @Test public void testDerivative() throws Throwable { setpoint.set(1); pid = new PIDController(input, setpoint, FloatInput.zero, FloatInput.zero, D); pid.update(100); assertTrue(pid.getPreviousError() == 1); setpoint.set(3.2f); pid.update(100); assertTrue(pid.getPreviousError() == 3.2f); assertEquals(D.get() * (3.2f - 1.0f) / (0.1f), pid.get(), 0.00001f); } @Test public void testGetPreviousError() throws Throwable { assertTrue(pid.getPreviousError() == 0); setpoint.set(1); pid.update(100); assertTrue(pid.getPreviousError() == setpoint.get() - input.get()); } @Test public void testForNaNs() { input.set(Float.NaN); pid.update(100); assertTrue(Float.isNaN(pid.get())); input.set(Float.POSITIVE_INFINITY); pid.update(100); assertTrue(Float.isNaN(pid.get())); input.set(Float.NEGATIVE_INFINITY); pid.update(100); assertTrue(Float.isNaN(pid.get())); // does it keep functioning? input.set(0); pid.update(100); assertEquals(pid.get(), 0, 0); } @Test public void testZeroLengthInfo() { input.set(1000000); pid.update(0); input.set(0); pid.update(100); assertEquals(pid.get(), 0, 0); } @Test public void testPractical() throws Throwable { pid.setIntegralBounds(0.05f); pid.setOutputBounds(1.1f); for (float sp : new float[] { 10, -5, 0, 3 }) { setpoint.set(sp); for (int i = 0; i < 1000; i++) { input.set(input.get() + pid.get() * 0.05f); pid.update(20); } assertEquals(input.get(), sp, 0.01f); } } }