Repository: m-lundberg/simple-pid

Issue:
Auto_Mode toggling causes disturbance
Hi,

Firstly, great tool, thank you!

I have been working on implementing the controller and I think I've come across an unintended bug in the code when it comes to toggling "auto_mode" off and on again.

The issue arises for two reasons:

1. 
self._last_time is not updated during calls that take place while auto_mode is off, leading to a potentially huge dt term once auto_mode is re-enabled.

2.
The self._error_sum is reset to 0 when auto_mode is re-enabled. I think this should be reset to whatever the last output was set to (possibly in manual mode) to ensure the control begins again as though it never stopped being in control. Lets say in manual mode I've changed my output and my input (PV) has settled at a new value. If I re-enable auto_mode having changed the setpoint to match my stable input value, then my derivative and proportional terms will be zero (proportional because error=0, derivative because d_input reset to 0 via _last_input being reset to None).
In order for the output to hold (the intended behaviour presumably), the error_sum term must be set to my previous output value thus mimicking the state of the controller if it had been responsible for the new stable input value. (Apologies if that isn't clear). 

I've implemented the above changes and have successfully removed the "kick" that I was seeing upon re-enabling auto_mode. Let me know your thoughts.

Best,

James


Comments:

Patch:
diff --git a/README.md b/README.md
index c564b5a..f03e739 100644
--- a/README.md
+++ b/README.md
@@ -62,6 +62,18 @@ To disable the PID so that no new values are computed, set auto mode to False:
 pid.auto_mode = False  # no new values will be computed when pid is called
 pid.auto_mode = True   # pid is enabled again
 ```
+When disabling the PID and controlling a system manually, it might be useful to tell the PID controller where to start from when giving back control to it. This can be done by enabling auto mode like this:
+```python
+pid.set_auto_mode(True, last_output=8.0)
+```
+This will set the I-term to the value given to `last_output`, meaning that if the system that is being controlled was stable at that output value the PID will keep the system stable if started from that point, without any big bumps in the output when turning the PID back on.
+
+When disabling the PID and controlling a system manually, it might be useful to tell the PID controller where to start from when giving back control to it. This can be done by enabling auto mode like this:
+```python
+pid.set_auto_mode(True, last_output=8.0)
+```
+This will set the I-term to the value given to `last_output`, meaning that if the system that is being controlled was stable at that output value the PID will keep the system stable if started from that point, without any big bumps in the output when turning the PID back on.
+
 
 In order to get output values in a certain range, and also to avoid [integral windup](https://en.wikipedia.org/wiki/Integral_windup) (since the integral term will never be allowed to grow outside of these limits), the output can be limited to a range:
 ```python
@@ -69,6 +81,11 @@ pid.output_limits = (0, 10)    # output value will be between 0 and 10
 pid.output_limits = (0, None)  # output will always be above 0, but with no upper bound
 ```
 
+When tuning the PID, it can be useful to see how each of the components contribute to the output. They can be seen like this:
+```python
+p, i, d = pid.components  # the separate terms are now in p, i, d
+```
+
 To eliminate overshoot in certain types of systems, you can calculate the [proportional term directly on the measurement](http://brettbeauregard.com/blog/2017/06/introducing-proportional-on-measurement/) instead of the error. This can be enabled like this:
 ```python
 pid.proportional_on_measurement = True
diff --git a/simple_pid/PID.py b/simple_pid/PID.py
index e71ccc6..39dafdd 100644
--- a/simple_pid/PID.py
+++ b/simple_pid/PID.py
@@ -144,7 +144,27 @@ class PID(object):
             # switching from manual mode to auto, reset
             self._last_output = None
             self._last_input = None
-            self._error_sum = 0
+            self._last_time = _current_time()
+            self._error_sum = _clamp(0, self.output_limits)
+
+        self._auto_mode = enabled
+
+    def set_auto_mode(self, enabled, last_output=None):
+        """
+        Enable or disable the PID controller, optionally setting the last output value.
+        This is useful if some system has been manually controlled and if the PID should take over.
+        In that case, pass the last output variable (the control variable) and it will be set as the starting
+        I-term when the PID is set to auto mode.
+        :param enabled: Whether auto mode should be enabled, True or False
+        :param last_output: The last output, or the control variable, that the PID should start from
+                            when going from manual mode to auto mode
+        """
+        if enabled and not self._auto_mode:
+            # switching from manual mode to auto, reset
+            self._last_output = last_output
+            self._last_input = None
+            self._last_time = _current_time()
+            self._error_sum = (last_output if last_output is not None else 0)
             self._error_sum = _clamp(self._error_sum, self.output_limits)
 
         self._auto_mode = enabled


Test patch:
diff --git a/tests/test_pid.py b/tests/test_pid.py
index 9c1d1f0..4ffa89a 100644
--- a/tests/test_pid.py
+++ b/tests/test_pid.py
@@ -133,6 +133,18 @@ def test_auto_mode():
     assert pid._error_sum == 0
     assert pid(8) == 2
 
+    # last update time should be reset to avoid huge dt
+    from simple_pid.PID import _current_time
+    pid.auto_mode = False
+    time.sleep(1)
+    pid.auto_mode = True
+    assert _current_time() - pid._last_time < 0.01
+
+    # check that setting last_output works
+    pid.auto_mode = False
+    pid.set_auto_mode(True, last_output=10)
+    assert pid._error_sum == 10
+
 
 def test_separate_components():
     pid = PID(1, 0, 1, setpoint=10, sample_time=0.1)