m-lundberg / simple-pid

A simple and easy to use PID controller in Python
MIT License
767 stars 209 forks source link

Proportional on measurement #8

Closed Feargus closed 5 years ago

Feargus commented 5 years ago

Hey, I'm not quite sure if this is a code issue, or if the problem is sitting in front of the keyboard. Anyway, I want to use a simple PID to control control a resistive heater via PWM on a raspberry pi. In the "normal", proportional-on-error mode, everything works fine. However, using proportional-on-measurement = True, I for the life of me cannot get the P-term to become negative (checking with print(pid.components)). This seems to be caused by my output limits of [0,100] (which are the limits for the PWM control). From what I understand from the last graph here: http://brettbeauregard.com/blog/2017/06/introducing-proportional-on-measurement/ the P term is supposed to go negative. Granted, I could change the output limits, but from what I understand, the limits should only affect the I-term. Also, the P-term goes negative just fine in the "normal" proportional-on-error mode.

From the above link, I gathered that the same tuning parameters should work for both modes with similar results. Yet neither those parameters nor any other (extremely large/small) values work.

Is this limiting of the P-term intended? Or am I doing something wrong? Thanks

m-lundberg commented 5 years ago

Hi!

Thanks for your feedback. You are correct in that the P-term is limited by the library to be in the output limits when using proportional on measurement, and I do agree with you that this is probably not how it should be. The problem with just removing it though is that the P-term could start growing kind of like in integral windup, although I don't know how big of a problem it would be. I will see if I can come up with a better solution when I have time.

Feargus commented 5 years ago

Maybe a simple toggle? Right now, the PoM mode is useless to me, because even after symmetric bounds, and a parameter transformation, the system doesn't really work as intended, due to the way bounds are used. Anyway, I don't really see how the P-term could wind up. It is (anti-)proportional to the measurement, and tries to return the system to the initial state, no? It acts more like a friction, than a driving force. The I-term on the other hand, potentially increases without bounds in a static system away from the setpoint.

For example, a heater with a large thermal load (a large water pot) on top, takes a long time to reach the setpoint, and the I-term grows without bounds. If you then take off the pot, the system will strongly overshoot, due to the lower "inertia". The P-term doesn't change all this time, since the temperature doesn't change either. Anyway I'll leave you in peace now, thanks for your library, I'll try to make do as is.

Huizerd commented 5 years ago

You could just overwrite PID's __call__ method, commenting out the line you don't want/need, like so:

class CustomPID(PID):
    def __call__(self, input_, **kwargs):
        if not self.auto_mode:
            return self._last_output

        now = _current_time()
        dt = now - self._last_time if now - self._last_time else 1e-16

        if (
            self.sample_time is not None
            and dt < self.sample_time
            and self._last_output is not None
        ):
            # Only update every sample_time seconds
            return self._last_output

        # Compute error terms
        error = self.setpoint - input_
        d_input = input_ - (
            self._last_input if self._last_input is not None else input_
        )

        # Compute the proportional term
        if not self.proportional_on_measurement:
            # Regular proportional-on-error, simply set the proportional term
            self._proportional = self.Kp * error
        else:
            # Add the proportional error on measurement to error_sum
            self._proportional -= self.Kp * d_input
            # self._proportional = self._clamp(self._proportional, self.output_limits)

        # Compute integral and derivative terms
        self._integral += self.Ki * error * dt
        self._integral = self._clamp(
            self._integral, self.output_limits
        )  # avoid integral windup

        self._derivative = -self.Kd * d_input / dt

        # Compute final output
        output = self._proportional + self._integral + self._derivative
        output = self._clamp(output, self.output_limits)

        # Keep track of state
        self._last_output = output
        self._last_input = input_
        self._last_time = now

        return output

    def _clamp(self, value, limits):
        lower, upper = limits
        if value is None:
            return None
        elif upper is not None and value > upper:
            return upper
        elif lower is not None and value < lower:
            return lower
        return value
m-lundberg commented 5 years ago

@Feargus I agree with your reasoning that the P-term should not be able to grow uncontrollably as the I-term. I removed the clamping on the P-term in version 0.2.2, please try it out and tell me how it works for you!

Feargus commented 5 years ago

Works perfectly, thanks a lot