ScratMan / HASmartThermostat

Smart Thermostat with PID controller for HomeAssistant
336 stars 48 forks source link

pid_i should not be capped to zero from below #150

Closed Chupaka closed 4 months ago

Chupaka commented 1 year ago

Describe the bug During PID calculations, integral part is capped to [_out_min; _out_max] range (effectively, [0;100] in heat mode): https://github.com/ScratMan/HASmartThermostat/blob/89b6800139894c5854f2c261698920fe0daed375/custom_components/smart_thermostat/pid_controller/__init__.py#L201 When used together with outdoor temperature compensation, where pid_e is non-negative, in a situation where target temperature is reached and stabilized without external heating, I'd like to set control_output to zero, but as pid_p and pid_d are zeroes and pid_e is positive, the only way to compensate pid_e is pid_i. But due to the capping mentioned above, it can't become negative, so my smart thermostat is constantly over-heating :(

Expected behavior pid_i becomes negative to compensate pid_e and make control_output to become a zero.

ScratMan commented 1 year ago

Hello, the purpose of pid_i is to compensate for losses, same as pid_e. So if your pid_e component is forcing heater on while temperature is above target, it means your gains are not balanced. You should rather decrease the ke gain or set it to 0 to disable the compensation and let the integral do the job

Chupaka commented 1 year ago

Let's assume that the temperature inside is at the target level, and outside is lower (non-AC mode :) ). Then pid_p and pid_d are zeroes.

You should rather decrease the ke gain

That won't help, as pid_e is always positive in this case, and while pid_i can't go below zero, control_output will always be positive. That's why I propose to relax this rule while keeping output value in range.

ScratMan commented 1 year ago

If outdoor temperature is lower than indoor temperature, that means you'll have losses. So the output should be slightly positive to get a small amount of heating to compensate these losses. If your heater is then increasing the temperature instead of maintaining it at the setpoint, it means pid_e is too high compared to the losses, so that means ke should be lowered. Consider pid_i as a low speed losses compensation and pid_e as a fast and low amplitude modulation of the pid_i value.

Chupaka commented 1 year ago

Well, technically I have another constant heater (like the sun during a day) that can compensate small differences, and the best place for it is in integral part of PID :) The negative part of it...

ScratMan commented 1 year ago

No, the sun acts on the pid_p part, it increases the error so pid_p becomes negative, quickly compensating the pid_i. If the sun could make pid_i negative, then when the sun hides, the system would need a very long period to come back to positive pid_i, letting the temperature drop.

Don't forget pid_i is there to compensate the residual error when pid_p is close to zero. Changing it's behaviour won't help on overheating. If pid_e is making your system overheat, please disable pid_e.

Chupaka commented 1 year ago

the sun acts on the pid_p part, it increases the error so pid_p becomes negative

What if it compensates only the heat loss? Without affecting the temperature, so error is still 0. But the heater needs to be switched off, not just working at low power.

when the sun hides, the system would need a very long period to come back to positive pid_i, letting the temperature drop

Well, the sun hides not immediately, it's kinda smooth process, so we have some time. Also, when the temperature drops quickly, we have pid_p and pid_d to compensate this right here and right now, without waiting for integral part to slowly grow.

ScratMan commented 1 year ago

If following your mind, PID would be useless. I keep on thinking the ke is not adapted to your conditions. I insist on the fact external temperature compensation is optional on the smart thermostat, added to make it flexible (one can use only P+E or P+I or P+I+D or P+I+D+E), but if E makes the system unstable or overheating, then it should be disabled.

Chupaka commented 1 year ago

If following your mind, PID would be useless

I never said that, moreover, with my changes it works perfectly for me in all modes. And its behavior should not change [significantly] for others working in the middle of the band. Do you have a counterexample?

ScratMan commented 1 year ago

I checked what could happen with your modifications, and it may make the integral change while the system overshoots, which is not supposed to happen, the overshoot should be managed by pid_p, and the pid_i should change only once the system is settled and there is a very small residual error.

As the stability of a PID is already difficult to obtain, I prefer not to change it's behaviour by altering a code that is already widely used in industry.

flicker581 commented 4 months ago

Generically speaking, purpose of pid_i is to compensate for cumulative error, not for losses. This error can have any sign. Of course, pid_p also can compensate this error, but at price of overshoot. Technically, PID becomes PD in this case.

It is pid_e what compensates for losses. So I am sure pid_i should become negative when needed.

Also I see your point of not making changes in working algorithm. So, probably, we need more tunables with defaults at current level. Like pid_i_min and pid_i_max.

ScratMan commented 4 months ago

I disagree with this. Integral should be a non zero, positive value only. An heating system can't "remove" heat, so we need to consider the behaviour when the heating is the only energy source. In such condition there will be losses, and pid_i must be >> 0 and pid_p ~= 0. Once sun starts heating the house, or when you are cooking, the pid_p will start increasing, and pid_i will slowly decrease, both reducing heating power.

If it doesn't work correctly in your setup, that means your gains are not balanced, not that the system needs to be modified.

flicker581 commented 4 months ago

Yes, the system is unbalanced in some modes. It is introduction of pid_e what makes it unbalanced. To perfectly balance the system, I will need to get rid of pid_e. But pid_e is there on purpose. It is not ideal, because it is static and therefore unable to follow different system modes (where external heater kicks in). Nevertheless, it is useful. So, because of having pid_e, there is need for negative pid_i to compensate. Pid_p is terrific way to compensate pid_e, because it shall be ~0 at setpoint.

chmtc94 commented 4 months ago

Hi,

I fully agree with you when the external temperature compensation is not used but when it is, it acts as an initial value for the integral and stay always present during the regulation O = P + E + I + D

When the temperature reach the set point : P = D = 0 and O = E + I which should be between 0 and 100 if heating is well operational

This is the real constraint to consider and then the integral must be capped :

a) if external temperature compensation is not used :

        0  <  I  <  100 (the actual code)

b) if external temperature compensation is used :

       0  < I + E  <  100   and so :

       -E  <  I  < 100 - E  which is the real good capping for the integral

The E compensation is to be estimated as the final expected integral (and output) we would have obtained without this compensation and the role of the integral is then to compensate potential error in this estimation positively or negatively

Moreover, the E compensation act as a booster each time the set point change.

When E is used and properly estimated following precedent considerations, it is also logical to reset the integral each time the set point change (considering that the E should be the final output after stabilisation)

Following corresponding very simple piece of code I modified in pid-controller init code and which works very fine 😉

Best regards

update : add code marks for better code preview and copy 😉

        # In order to prevent windup, only integrate if the process is not saturated and set point
        # is stable
        if self._out_min < self._last_output < self._out_max and \
                self._last_set_point == self._set_point:
            self._integral += self._Ki * self._error * self._dt
 # CM : Prise en compte de la compensation de température extérieure dans la limitation de l'intégrale
 # CM : (out_min < I + E < out_max)
            # self._integral = max(min(self._integral, self._out_max), self._out_min)
            self._integral = max(min(self._integral, self._out_max - self._external), \
                self._out_min - self._external)
 # CM : Reset integrale quand cible change et compensation température extérieure active
 # car nouvelle valeur de E sensée correspondre à la valeur de la sortie attendue
 # en fin de stabilisation (P = D = 0)
        if ext_temp is not None and self._last_set_point != self._set_point:
            self._integral = 0