PidFilter: improve anti-windup and behaviour on target-direction-change#3581
PidFilter: improve anti-windup and behaviour on target-direction-change#3581sfeilmeier wants to merge 2 commits intodevelopfrom
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## develop #3581 +/- ##
=============================================
- Coverage 58.60% 58.59% -0.00%
Complexity 105 105
=============================================
Files 3091 3091
Lines 134005 134007 +2
Branches 9882 9884 +2
=============================================
- Hits 78517 78514 -3
+ Misses 52590 52586 -4
- Partials 2898 2907 +9 🚀 New features to boost your workflow:
|
|
@parapluplu, @tsicking, @Sn0w3y, @TheSerapher, @huseyinsaht, @dennis-qt, @pooran-c, @venu-sagar: (I marked you, because you have been involved in a previous PR on that topic.): As we also encountered strange behaviour with PID I set down with some help of AI and tried to come up with an alternative solution to previous PR #2960. We already quickly tested this in the lab and it looks promising. What is your opinion on this approach? |
Will it also work with multiple ESSes? 😋😇 |
Sure. At this location it works with any Active-Power-Set-Point, e.g. from Balancing- oder Peak-Shaving-Controller. |
|
Note: I'm neither a programmer nor have I actually tested both this and #2960, so this is purely from my theoretical understanding on how the control logic works. This doesn't seem to address my core concern I've documented in #2960 (comment) at all, where I detailed how the PID filter completely breaks the core logic behind the ESS Balancing controller and why I think having a PID in that position at all might be a design error. IMO the right approach would be to replace the entire PID filter with a mechanism that just slows down the setpoint change over multiple cycles without implementing a dedicated closed control loop. My approach would be to calculate the difference between the current and previous setpoint for the inverter and only partially apply this difference. (Fun fact: this mimics the behavior of a simple RC low-pass filter, and a scaleFactor of |
|
@dennis-qt: Thank you for sharing your insights. I have to admit that I am not able to foresee the possible implications of such a change. I'll have to wait for further discussions and opinions here. Maybe I should have paid more attention to these classes back in University... |
| var error = target - input; | ||
|
|
||
| // We are already there | ||
| if (error == 0) { |
There was a problem hiding this comment.
Reset errorSum cleanly when target is reached?
this.errorSum = 0;
this.lastInput = input;
this.firstRun = false;
| // Post-process the output value | ||
| // 1. Apply value limits | ||
| int output = this.applyLowHighLimits(rawOutput); | ||
| final var isSaturated = rawOutput != output; |
There was a problem hiding this comment.
I would move this to Line 130 - 136
Anti-windup checks AFTER all output clamping (limits + direction) to prevent integral windup when direction-clamp restricts the output ?
Does this make sense to all?
There was a problem hiding this comment.
I think I tried moving it above before, but it would behave very differently, that's why I left it like this. But worth a try probably.... once we settle on the correct overall approach.
|
The anti-windup saturation check was performed before the direction clamp (target>0 but output<0 → 0). This allowed the integral term to accumulate unchecked when the direction clamp was the effective limiter, causing overshoot on recovery. The fix would be imo:
Am i thinking right here? |
@dennis-qt took a deeper look at the code and i think you are basically right. the core problem is you got a double so you end up with two nested loops basically fighting over the same thing. the I-term piles up errors that the outer loop already handles which causes overshoot and the D-term reacts to changes that are partly caused by itself from the last cycle. thats why we keep needing patches like direction clamping and anti-windup fixes - those are basically band aids for a architectural issue the proposed low-pass approach (inverterSetpoint = previousSetpoint + scaleFactor * (newSetpoint - previousSetpoint)) the current PID does converge in practice tho and with P=0.3 the proportional term alone kinda behaves like a low-pass |
@dennis-qt: Ok, so you are saying we should just apply the filter (whichever - low-pass or PID filter) using the previous and current set-point and to not consider the actually measured power of the inverter. This should be ok, because anyway the inverter has it's own filter internally - and relying on the measured power again just adds delay and more fluctuation. We see this delay also in production. We could adjust the existing PID-Filter implementation in this way. Would you still pledge for a simple low-pass filter? What would be a good choice for the |
My proposed solution works that way, yes, but that's not really the core statement I'm trying to make. The system we're dealing with has multiple points that create major time delays in the control loop:
My guess is that because the individual time delays involved are roughly on the same order of magnitude but out of sync, this seems to be the cause of system instability (like for example the grid meter having a 1Hz polling rate, the inverter updating its feedback values at 1Hz, and the OpenEMS cycle time being set to 1s). An external impulse in the form of a load change then causes this system to oscillate at what I think could be considered the resonance frequency of the system.
I think the goal should be to reduce the total system gain at the resonance frequency as much as possible without slowing down reaction time in all other scenarios to unacceptable levels.
The relationship between the scaleFactor and the corresponding frequency response should be easy to calculate, but the optimal value would depend heavily on
You could probably write a whole paper about analyzing and tuning such a system for best performance... I'd probably start with 0.63 (roughly |
|
@dennis-qt and @Sn0w3y: Could you please have a look at my try on implementing a Low-Pass-Filter? #3587 For a clean implementation I had to touch a few more files than technically necessary. |
|
Hi all, thanks for the fruitful comments. We have also been expreimenting with the PID filter in the past, in particular with the integral windup. We had a system where the ESS reported its self consumption of about 1kW as active power when the setpoint was 0, which led to a huge integral term. We did peak shaving in that setup, and most of the time the battery was doing nothing, but the error sum gained 1kW every cycle until the maximum was reached. This resulted in the battery needing about 30s until it discharged when we needed it.
this.errorSum = this.applyErrorSumLimit(this.decayFactor * this.errorSum + error);where the decay factor could be configured in the power component. It means that the When setting the decay factor to 1 the system would behave as before. In the above system we set it to 0.99, which means that the error sum was limited to 100kW, even when the battery stood still for hours (Why? Small exercise for undergraduate students :-)). It worked well in this setup, but by now the problem with the inverter is fixed and our use case changed, so the exponential decay was not needed any more. |
I'll have a closer look at it, hopefully I'll manage today or tomorrow. It definitely sounds interesting. |
@sfeilmeier If I have an ESS cluster with an activePowerSetpoint of +2000 W (i.e. target direction = discharge), and one ESS in the cluster has a force charge constraint, e.g. activePower ≤ -100 W (for example from a limitTotalDischarge controller in forceCharge state), would this mean that the resulting −100 W is corrected to 0 W due to the direction enforcement — effectively preventing the forced charging? Is this behavior intended? |
PidFilter: improve anti-windup and behaviour on target-direction-change
Summary
This PR improves the PidFilter implementation with enhanced anti-windup mechanisms and intelligent handling of target direction changes (charge/discharge/zero transitions). The changes address the flickering behavior that occurs when the PID filter switches between charging and discharging modes.
Problem
The original PID filter implementation suffered from oscillation and flickering behavior when transitioning between charging, discharging, and zero power setpoints due to:
Comparison with PR #2960
PR #2960 Approach (External Limit-Based Solution)
ActivePowerConstraintWithPidwrapper class)PR #3581 Approach (Improved Core Filter)
Key Technical Improvements
1. Direction Change Tracking
2. Smart Error Sum Management
3. Output Direction Enforcement
Performance Comparison
Test Case: Switching between positive and negative targets
Test Results
Both solutions eliminate flickering, but PR #3581 shows:
Files Changed
PidFilter.java: Core filter improvements (37 additions, 46 deletions)PidFilterTest.java: Updated test cases and new direction-change test (118 additions, 91 deletions)Backward Compatibility
✅ Fully backward compatible - The PidFilter API remains unchanged. Only the internal behavior improves.
Recommendation
PR #3581 is the preferred solution because it: