예제 #1
0
    def setUp(self):
        self._actuator = mock.Mock(spec_set=Actuator)
        self._deadband_width = .1

        self._setpoint = 1.0

        self._deadband_upper = self._setpoint + .05
        self._deadband_lower = self._setpoint - .05
        self._small = 1e-9

        self._brew_state = 1.0
        self._memory_time_seconds = .1
        self._derivative_threshold = .1
        self._derivative_trip_band_width = .05

        self._controller = BangBangController(
            self._actuator,
            self._extract_actual,
            self._deadband_width,
            memory_time_seconds=self._memory_time_seconds,
            derivative_threshold=self._derivative_threshold
        )
        self._d_controller = BangBangController(
            self._actuator,
            self._extract_actual,
            self._deadband_width,
            memory_time_seconds=self._memory_time_seconds,
            derivative_tripband_width=self._derivative_trip_band_width,
            derivative_threshold=self._derivative_threshold

        )

        self._controller.set_setpoint(self._setpoint)
        self._d_controller.set_setpoint(self._setpoint)
예제 #2
0
    def setUp(self):
        self._control_interval = 1.0
        # 20/.067 gives a dwell time of 300s
        self._plant = FakeTankAndHeater(
            20.0,
            .067,  # about 1 gal per minute
            10.0,
            1.0,
            1.0,
            1.5,
            self._control_interval)

        self._deadband_width = 1.0
        self._derivative_tripband_width = .6
        self._controller = BangBangController(
            self._plant,
            self._extract_actual,
            memory_time_seconds=30.0,
            derivative_tripband_width=self._derivative_tripband_width,
            deadband_width=self._deadband_width,
            derivative_threshold=0.1)
        self._controller.set_setpoint(0.0)

        self._time = 0.0

        self._simulation = Simulation(self._plant, self._controller,
                                      self._control_interval,
                                      self._increment_time, self._get_time)
예제 #3
0
    def setUp(self):
        self._actuator = mock.Mock(spec_set=Actuator)
        self._deadband_width = .1

        self._setpoint = 1.0

        self._deadband_upper = self._setpoint + .05
        self._deadband_lower = self._setpoint - .05
        self._small = 1e-9

        self._brew_state = 1.0
        self._memory_time_seconds = .1
        self._derivative_threshold = .1
        self._derivative_trip_band_width = .05

        self._controller = BangBangController(
            self._actuator,
            self._extract_actual,
            self._deadband_width,
            memory_time_seconds=self._memory_time_seconds,
            derivative_threshold=self._derivative_threshold)
        self._d_controller = BangBangController(
            self._actuator,
            self._extract_actual,
            self._deadband_width,
            memory_time_seconds=self._memory_time_seconds,
            derivative_tripband_width=self._derivative_trip_band_width,
            derivative_threshold=self._derivative_threshold)

        self._controller.set_setpoint(self._setpoint)
        self._d_controller.set_setpoint(self._setpoint)
예제 #4
0
    def setUp(self):
        self._control_interval = 1.0
        # 20/.067 gives a dwell time of 300s
        self._plant = FakeTankAndHeater(
            20.0,
            .067, # about 1 gal per minute
            10.0,
            1.0,
            1.0,
            1.5,
            self._control_interval
        )

        self._deadband_width = 1.0
        self._derivative_tripband_width = .6
        self._controller = BangBangController(
            self._plant,
            self._extract_actual,
            memory_time_seconds=30.0,
            derivative_tripband_width=self._derivative_tripband_width,
            deadband_width=self._deadband_width,
            derivative_threshold=0.1
        )
        self._controller.set_setpoint(0.0)

        self._time = 0.0

        self._simulation = Simulation(
            self._plant,
            self._controller,
            self._control_interval,
            self._increment_time,
            self._get_time
        )
예제 #5
0
 def test___init__raises_for_negative_deadband(self):
     with self.assertRaises(RuntimeError):
         BangBangController(
             self._actuator,
             self._extract_actual,
             -self._deadband_width,
         )
예제 #6
0
 def test___init__raises_for_derivative_tripband_larger_than_deadband(self):
     with self.assertRaises(RuntimeError):
         BangBangController(
             self._actuator,
             self._extract_actual,
             self._deadband_width,
             derivative_tripband_width=self._deadband_width,
         )
예제 #7
0
class TestBangBangControllerInSimulation(unittest.TestCase):
    def setUp(self):
        self._control_interval = 1.0
        # 20/.067 gives a dwell time of 300s
        self._plant = FakeTankAndHeater(
            20.0,
            .067, # about 1 gal per minute
            10.0,
            1.0,
            1.0,
            1.5,
            self._control_interval
        )

        self._deadband_width = 1.0
        self._derivative_tripband_width = .6
        self._controller = BangBangController(
            self._plant,
            self._extract_actual,
            memory_time_seconds=30.0,
            derivative_tripband_width=self._derivative_tripband_width,
            deadband_width=self._deadband_width,
            derivative_threshold=0.1
        )
        self._controller.set_setpoint(0.0)

        self._time = 0.0

        self._simulation = Simulation(
            self._plant,
            self._controller,
            self._control_interval,
            self._increment_time,
            self._get_time
        )

    def test_well_mixed_massless_system(self):
        temperature_difference = self._derivative_tripband_width/3.0
        self._plant.set_mass_flowrate(20.0)
        self._plant.set_mixing_time(1.0)
        self._plant.set_heating_temperature_difference(temperature_difference)
        self._plant.set_cooling_temperature_difference(temperature_difference)
        self._simulation.set_control_interval(.1)
        self._controller.set_memory_time_seconds(1.0)

        t, temperature, actuated = self._simulation.simulate()

        # pylab.plot(t, temperature, 'k')
        # pylab.plot(t, actuated, 'r')
        # pylab.show()

        self._assert_at_least_last_half_in_deadband(temperature, atol=temperature_difference)

    def test_poorly_mixed_massive_system(self):
        temperature_difference = .2123
        self._plant.set_mixing_time(1e6)
        self._plant.set_heating_temperature_difference(temperature_difference)
        self._plant.set_cooling_temperature_difference(temperature_difference)
        self._simulation.set_control_interval(.5)
        self._controller.set_memory_time_seconds(10.0)

        # # with no mixing whatsoever, the derivative control can be very sensitive
        # self._controller.set_derivative_tripband_width(None)


        t, temperature, actuated = self._simulation.simulate(n_dwell_times=20)

        pylab.plot(t, actuated, 'r')
        pylab.plot(t, temperature, 'k')
        pylab.show()

        self._assert_at_least_last_half_in_deadband(temperature, atol=temperature_difference)

    def _extract_actual(self, brew_state):
        return brew_state

    def _get_time(self):
        return self._time

    def _increment_time(self, dt):
        self._time += dt

    def _assert_at_least_last_half_in_deadband(self, temperature, atol=None):
        first_in = np.flatnonzero(self._in_deadband_mask(temperature, atol=atol))[0]
        self.assertLess(first_in, len(temperature) / 2)
        self.assertTrue(np.all(self._in_deadband_mask(temperature[first_in:], atol=atol)))

    def _in_deadband_mask(self, temperature, atol=None):
        t = self._deadband_width*.55 if atol is None else (self._deadband_width*.5 + atol)
        setpoint = self._controller.get_setpoint()
        return np.abs(temperature - setpoint) < t
예제 #8
0
class TestBangBangController(unittest.TestCase):

    def setUp(self):
        self._actuator = mock.Mock(spec_set=Actuator)
        self._deadband_width = .1

        self._setpoint = 1.0

        self._deadband_upper = self._setpoint + .05
        self._deadband_lower = self._setpoint - .05
        self._small = 1e-9

        self._brew_state = 1.0
        self._memory_time_seconds = .1
        self._derivative_threshold = .1
        self._derivative_trip_band_width = .05

        self._controller = BangBangController(
            self._actuator,
            self._extract_actual,
            self._deadband_width,
            memory_time_seconds=self._memory_time_seconds,
            derivative_threshold=self._derivative_threshold
        )
        self._d_controller = BangBangController(
            self._actuator,
            self._extract_actual,
            self._deadband_width,
            memory_time_seconds=self._memory_time_seconds,
            derivative_tripband_width=self._derivative_trip_band_width,
            derivative_threshold=self._derivative_threshold

        )

        self._controller.set_setpoint(self._setpoint)
        self._d_controller.set_setpoint(self._setpoint)

    def test___init__raises_for_negative_deadband(self):
        with self.assertRaises(RuntimeError):
            BangBangController(
                self._actuator,
                self._extract_actual,
                -self._deadband_width,
            )

    def test___init__raises_for_negative_derivative_tripband(self):
        with self.assertRaises(RuntimeError):
            BangBangController(
                self._actuator,
                self._extract_actual,
                self._deadband_width,
                derivative_tripband_width=-1,
            )

    def test___init__raises_for_negative_derivative_threshold(self):
        with self.assertRaises(RuntimeError):
            BangBangController(
                self._actuator,
                self._extract_actual,
                self._deadband_width,
                derivative_threshold=-1,
            )

    def test___init__raises_for_derivative_tripband_larger_than_deadband(self):
        with self.assertRaises(RuntimeError):
            BangBangController(
                self._actuator,
                self._extract_actual,
                self._deadband_width,
                derivative_tripband_width=self._deadband_width,
            )

    def test_control_actuates_below_deadband_without_derivative_control(self):
        state = self._deadband_lower - self._small
        self._controller.control(state)
        self._assert_actuated_once(state)

    def test_control_deactuates_above_deadband_without_derivative_control(self):
        state = self._deadband_upper + self._small
        self._controller.control(state)
        self._assert_deactuated_once(state)

    def test_control_neither_actuates_nor_deactuates_in_deadband_without_derivative_control(self):
        state = self._deadband_upper - self._small
        self._controller.control(state)
        self._assert_no_action_taken()

        state = self._deadband_lower + self._small
        self._controller.control(state)
        self._assert_no_action_taken()

    def test_control_actuates_in_derivative_upper_tripband_when_falling(self):
        self._setup_last_state_for_falling()
        state = self._setpoint + .5*self._derivative_trip_band_width - self._small
        self._d_controller.control(state)
        self._assert_actuated_once(state)

    def test_control_does_not_actuate_in_derivative_lower_tripband_when_falling(self):
        self._setup_last_state_for_falling()
        state = self._setpoint - .5 * self._derivative_trip_band_width + self._small
        self._d_controller.control(state)
        self._assert_no_action_taken()

    def test_control_actuates_below_deadband_when_falling(self):
        self._setup_last_state_for_falling()
        state = self._deadband_lower - self._small
        self._d_controller.control(state)
        self._assert_actuated_once(state)

    def test_control_actuates_below_deadband_when_rising(self):
        self._setup_last_state_for_rising()
        state = self._deadband_lower - self._small
        self._d_controller.control(state)
        self._assert_actuated_once(state)

    def test_control_deactuates_when_rising_in_derivative_lower_tripband(self):
        self._setup_last_state_for_rising()
        state = self._setpoint - .5 * self._derivative_trip_band_width + self._small
        self._d_controller.control(state)
        self._assert_deactuated_once(state)

    def test_control_does_not_deactuate_when_rising_in_derivative_upper_tripband(self):
        self._setup_last_state_for_rising()
        state = self._setpoint + .5 * self._derivative_trip_band_width - self._small
        self._d_controller.control(state)
        self._assert_no_action_taken()

    def test_control_deactuates_above_deadband_when_falling(self):
        self._setup_last_state_for_falling()
        state = self._deadband_upper + self._small
        self._d_controller.control(state)
        self._assert_deactuated_once(state)

    def test_control_deactuates_above_deadband_when_rising(self):
        self._setup_last_state_for_rising()
        state = self._deadband_upper + self._small
        self._d_controller.control(state)
        self._assert_deactuated_once(state)

    def test_control_remembers_on_first_call(self):
        c = self._controller
        self._assert_no_state_remembered(c)
        c.control(self._brew_state)
        self.assertEqual(self._brew_state, c._last_state)
        self.assertFalse(c._last_time is None)

    def test_control_does_not_remember_state_before_memory_time(self):
        self._controller.control(self._brew_state)
        new_state = self._brew_state + 1.0
        time.sleep(self._memory_time_seconds/5.0)
        self._controller.control(new_state)
        self.assertAlmostEqual(self._brew_state, self._controller._last_state)

    def test_control_remembers_state_after_memory_time(self):
        self._controller.control(self._brew_state)
        new_state = self._brew_state
        for i in range(3):
            time.sleep(self._memory_time_seconds)
            new_state += 1.0
            self._controller.control(new_state)
            self.assertAlmostEqual(new_state, self._controller._last_state)

    def test__is_falling_false_without_previous_state(self):
        self._assert_no_state_remembered(self._controller)
        self.assertFalse(self._controller._is_falling(self._brew_state))

    def test__is_falling(self):
        self._controller.control(self._brew_state)
        self.assertFalse(self._controller._is_falling(self._brew_state - self._small))
        self.assertFalse(self._controller._is_falling(self._brew_state - self._derivative_threshold + self._small))
        self.assertTrue(self._controller._is_falling(self._brew_state - self._derivative_threshold - self._small))

    def test__is_rising_false_without_previous_state(self):
        self._assert_no_state_remembered(self._controller)
        self.assertFalse(self._controller._is_rising(self._brew_state))

    def test__is_rising(self):
        self._controller.control(self._brew_state)
        self.assertFalse(self._controller._is_rising(self._brew_state + self._small))
        self.assertFalse(self._controller._is_rising(self._brew_state + self._derivative_threshold - self._small))
        self.assertTrue(self._controller._is_rising(self._brew_state + self._derivative_threshold + self._small))

    def _setup_last_state_for_falling(self):
        last_state = self._setpoint + 1000.0
        self._d_controller.control(last_state)
        self._actuator.reset_mock()
        self._assert_no_action_taken()

    def _setup_last_state_for_rising(self):
        last_state = self._setpoint - 1000.0
        self._d_controller.control(last_state)
        self._actuator.reset_mock()
        self._assert_no_action_taken()

    def _extract_actual(self, brew_state):
        return brew_state

    def _assert_no_state_remembered(self, c):
        self.assertTrue(c._last_state is None)
        self.assertTrue(c._last_time is None)

    def _assert_no_action_taken(self):
        self.assertFalse(self._actuator.actuate.called)
        self.assertFalse(self._actuator.deactuate.called)

    def _assert_actuated_once(self, state):
        self._actuator.actuate.assert_called_once_with(state)
        self.assertFalse(self._actuator.deactuate.called)

    def _assert_deactuated_once(self, state):
        self._actuator.deactuate.assert_called_once_with(state)
        self.assertFalse(self._actuator.actuate.called)
예제 #9
0
class TestBangBangControllerInSimulation(unittest.TestCase):
    def setUp(self):
        self._control_interval = 1.0
        # 20/.067 gives a dwell time of 300s
        self._plant = FakeTankAndHeater(
            20.0,
            .067,  # about 1 gal per minute
            10.0,
            1.0,
            1.0,
            1.5,
            self._control_interval)

        self._deadband_width = 1.0
        self._derivative_tripband_width = .6
        self._controller = BangBangController(
            self._plant,
            self._extract_actual,
            memory_time_seconds=30.0,
            derivative_tripband_width=self._derivative_tripband_width,
            deadband_width=self._deadband_width,
            derivative_threshold=0.1)
        self._controller.set_setpoint(0.0)

        self._time = 0.0

        self._simulation = Simulation(self._plant, self._controller,
                                      self._control_interval,
                                      self._increment_time, self._get_time)

    def test_well_mixed_massless_system(self):
        temperature_difference = self._derivative_tripband_width / 3.0
        self._plant.set_mass_flowrate(20.0)
        self._plant.set_mixing_time(1.0)
        self._plant.set_heating_temperature_difference(temperature_difference)
        self._plant.set_cooling_temperature_difference(temperature_difference)
        self._simulation.set_control_interval(.1)
        self._controller.set_memory_time_seconds(1.0)

        t, temperature, actuated = self._simulation.simulate()

        # pylab.plot(t, temperature, 'k')
        # pylab.plot(t, actuated, 'r')
        # pylab.show()

        self._assert_at_least_last_half_in_deadband(
            temperature, atol=temperature_difference)

    def test_poorly_mixed_massive_system(self):
        temperature_difference = .2123
        self._plant.set_mixing_time(1e6)
        self._plant.set_heating_temperature_difference(temperature_difference)
        self._plant.set_cooling_temperature_difference(temperature_difference)
        self._simulation.set_control_interval(.5)
        self._controller.set_memory_time_seconds(10.0)

        # # with no mixing whatsoever, the derivative control can be very sensitive
        # self._controller.set_derivative_tripband_width(None)

        t, temperature, actuated = self._simulation.simulate(n_dwell_times=20)

        pylab.plot(t, actuated, 'r')
        pylab.plot(t, temperature, 'k')
        pylab.show()

        self._assert_at_least_last_half_in_deadband(
            temperature, atol=temperature_difference)

    def _extract_actual(self, brew_state):
        return brew_state

    def _get_time(self):
        return self._time

    def _increment_time(self, dt):
        self._time += dt

    def _assert_at_least_last_half_in_deadband(self, temperature, atol=None):
        first_in = np.flatnonzero(
            self._in_deadband_mask(temperature, atol=atol))[0]
        self.assertLess(first_in, len(temperature) / 2)
        self.assertTrue(
            np.all(self._in_deadband_mask(temperature[first_in:], atol=atol)))

    def _in_deadband_mask(self, temperature, atol=None):
        t = self._deadband_width * .55 if atol is None else (
            self._deadband_width * .5 + atol)
        setpoint = self._controller.get_setpoint()
        return np.abs(temperature - setpoint) < t
예제 #10
0
class TestBangBangController(unittest.TestCase):
    def setUp(self):
        self._actuator = mock.Mock(spec_set=Actuator)
        self._deadband_width = .1

        self._setpoint = 1.0

        self._deadband_upper = self._setpoint + .05
        self._deadband_lower = self._setpoint - .05
        self._small = 1e-9

        self._brew_state = 1.0
        self._memory_time_seconds = .1
        self._derivative_threshold = .1
        self._derivative_trip_band_width = .05

        self._controller = BangBangController(
            self._actuator,
            self._extract_actual,
            self._deadband_width,
            memory_time_seconds=self._memory_time_seconds,
            derivative_threshold=self._derivative_threshold)
        self._d_controller = BangBangController(
            self._actuator,
            self._extract_actual,
            self._deadband_width,
            memory_time_seconds=self._memory_time_seconds,
            derivative_tripband_width=self._derivative_trip_band_width,
            derivative_threshold=self._derivative_threshold)

        self._controller.set_setpoint(self._setpoint)
        self._d_controller.set_setpoint(self._setpoint)

    def test___init__raises_for_negative_deadband(self):
        with self.assertRaises(RuntimeError):
            BangBangController(
                self._actuator,
                self._extract_actual,
                -self._deadband_width,
            )

    def test___init__raises_for_negative_derivative_tripband(self):
        with self.assertRaises(RuntimeError):
            BangBangController(
                self._actuator,
                self._extract_actual,
                self._deadband_width,
                derivative_tripband_width=-1,
            )

    def test___init__raises_for_negative_derivative_threshold(self):
        with self.assertRaises(RuntimeError):
            BangBangController(
                self._actuator,
                self._extract_actual,
                self._deadband_width,
                derivative_threshold=-1,
            )

    def test___init__raises_for_derivative_tripband_larger_than_deadband(self):
        with self.assertRaises(RuntimeError):
            BangBangController(
                self._actuator,
                self._extract_actual,
                self._deadband_width,
                derivative_tripband_width=self._deadband_width,
            )

    def test_control_actuates_below_deadband_without_derivative_control(self):
        state = self._deadband_lower - self._small
        self._controller.control(state)
        self._assert_actuated_once(state)

    def test_control_deactuates_above_deadband_without_derivative_control(
            self):
        state = self._deadband_upper + self._small
        self._controller.control(state)
        self._assert_deactuated_once(state)

    def test_control_neither_actuates_nor_deactuates_in_deadband_without_derivative_control(
            self):
        state = self._deadband_upper - self._small
        self._controller.control(state)
        self._assert_no_action_taken()

        state = self._deadband_lower + self._small
        self._controller.control(state)
        self._assert_no_action_taken()

    def test_control_actuates_in_derivative_upper_tripband_when_falling(self):
        self._setup_last_state_for_falling()
        state = self._setpoint + .5 * self._derivative_trip_band_width - self._small
        self._d_controller.control(state)
        self._assert_actuated_once(state)

    def test_control_does_not_actuate_in_derivative_lower_tripband_when_falling(
            self):
        self._setup_last_state_for_falling()
        state = self._setpoint - .5 * self._derivative_trip_band_width + self._small
        self._d_controller.control(state)
        self._assert_no_action_taken()

    def test_control_actuates_below_deadband_when_falling(self):
        self._setup_last_state_for_falling()
        state = self._deadband_lower - self._small
        self._d_controller.control(state)
        self._assert_actuated_once(state)

    def test_control_actuates_below_deadband_when_rising(self):
        self._setup_last_state_for_rising()
        state = self._deadband_lower - self._small
        self._d_controller.control(state)
        self._assert_actuated_once(state)

    def test_control_deactuates_when_rising_in_derivative_lower_tripband(self):
        self._setup_last_state_for_rising()
        state = self._setpoint - .5 * self._derivative_trip_band_width + self._small
        self._d_controller.control(state)
        self._assert_deactuated_once(state)

    def test_control_does_not_deactuate_when_rising_in_derivative_upper_tripband(
            self):
        self._setup_last_state_for_rising()
        state = self._setpoint + .5 * self._derivative_trip_band_width - self._small
        self._d_controller.control(state)
        self._assert_no_action_taken()

    def test_control_deactuates_above_deadband_when_falling(self):
        self._setup_last_state_for_falling()
        state = self._deadband_upper + self._small
        self._d_controller.control(state)
        self._assert_deactuated_once(state)

    def test_control_deactuates_above_deadband_when_rising(self):
        self._setup_last_state_for_rising()
        state = self._deadband_upper + self._small
        self._d_controller.control(state)
        self._assert_deactuated_once(state)

    def test_control_remembers_on_first_call(self):
        c = self._controller
        self._assert_no_state_remembered(c)
        c.control(self._brew_state)
        self.assertEqual(self._brew_state, c._last_state)
        self.assertFalse(c._last_time is None)

    def test_control_does_not_remember_state_before_memory_time(self):
        self._controller.control(self._brew_state)
        new_state = self._brew_state + 1.0
        time.sleep(self._memory_time_seconds / 5.0)
        self._controller.control(new_state)
        self.assertAlmostEqual(self._brew_state, self._controller._last_state)

    def test_control_remembers_state_after_memory_time(self):
        self._controller.control(self._brew_state)
        new_state = self._brew_state
        for i in range(3):
            time.sleep(self._memory_time_seconds)
            new_state += 1.0
            self._controller.control(new_state)
            self.assertAlmostEqual(new_state, self._controller._last_state)

    def test__is_falling_false_without_previous_state(self):
        self._assert_no_state_remembered(self._controller)
        self.assertFalse(self._controller._is_falling(self._brew_state))

    def test__is_falling(self):
        self._controller.control(self._brew_state)
        self.assertFalse(
            self._controller._is_falling(self._brew_state - self._small))
        self.assertFalse(
            self._controller._is_falling(self._brew_state -
                                         self._derivative_threshold +
                                         self._small))
        self.assertTrue(
            self._controller._is_falling(self._brew_state -
                                         self._derivative_threshold -
                                         self._small))

    def test__is_rising_false_without_previous_state(self):
        self._assert_no_state_remembered(self._controller)
        self.assertFalse(self._controller._is_rising(self._brew_state))

    def test__is_rising(self):
        self._controller.control(self._brew_state)
        self.assertFalse(
            self._controller._is_rising(self._brew_state + self._small))
        self.assertFalse(
            self._controller._is_rising(self._brew_state +
                                        self._derivative_threshold -
                                        self._small))
        self.assertTrue(
            self._controller._is_rising(self._brew_state +
                                        self._derivative_threshold +
                                        self._small))

    def _setup_last_state_for_falling(self):
        last_state = self._setpoint + 1000.0
        self._d_controller.control(last_state)
        self._actuator.reset_mock()
        self._assert_no_action_taken()

    def _setup_last_state_for_rising(self):
        last_state = self._setpoint - 1000.0
        self._d_controller.control(last_state)
        self._actuator.reset_mock()
        self._assert_no_action_taken()

    def _extract_actual(self, brew_state):
        return brew_state

    def _assert_no_state_remembered(self, c):
        self.assertTrue(c._last_state is None)
        self.assertTrue(c._last_time is None)

    def _assert_no_action_taken(self):
        self.assertFalse(self._actuator.actuate.called)
        self.assertFalse(self._actuator.deactuate.called)

    def _assert_actuated_once(self, state):
        self._actuator.actuate.assert_called_once_with(state)
        self.assertFalse(self._actuator.deactuate.called)

    def _assert_deactuated_once(self, state):
        self._actuator.deactuate.assert_called_once_with(state)
        self.assertFalse(self._actuator.actuate.called)