Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified io.openems.edge.common/doc/PID-Simulator.xlsx
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,14 @@ public class PidFilter {
public static final double DEFAULT_I = 0.3;
public static final double DEFAULT_D = 0.1;

public static final int ERROR_SUM_LIMIT_FACTOR = 10;

private final double p;
private final double i;
private final double d;

private boolean firstRun = true;

private double lastInput = 0;
private int lastTarget = 0;
private double errorSum = 0;
private Integer lowLimit = null;
private Integer highLimit = null;
Expand Down Expand Up @@ -69,8 +68,14 @@ public void setLimits(Integer lowLimit, Integer highLimit) {
* @return the filtered set-point value
*/
public int applyPidFilter(int input, int target) {
// Pre-process the target value: apply output value limits
target = this.applyLowHighLimits(target);
// Check if target direction changed (sign transition indicates mode change:
// charge/discharge/zero)
final var targetDirectionChanged = //
this.firstRun // Always on first run
|| (this.lastTarget > 0 && target <= 0) // Positive to negative/zero
|| (this.lastTarget < 0 && target >= 0) // Negative to positive/zero
|| (this.lastTarget == 0 && target != 0); // Zero to positive/negative
this.lastTarget = target;

// Calculate the error
var error = target - input;
Expand All @@ -80,17 +85,21 @@ public int applyPidFilter(int input, int target) {
return target;
}

// Calculate P
var outputP = this.p * error;

// Set last values on first run
if (this.firstRun) {
this.lastInput = input;
this.firstRun = false;
}

final var candidateErrorSum = targetDirectionChanged //
? 0 //
: this.errorSum + error;

// Calculate P
var outputP = this.p * error;

// Calculate I
var outputI = this.i * this.errorSum;
var outputI = this.i * candidateErrorSum;

// Calculate D
var outputD = -this.d * (input - this.lastInput);
Expand All @@ -99,14 +108,27 @@ public int applyPidFilter(int input, int target) {
this.lastInput = input;

// Sum outputs
var output = outputP + outputI + outputD;
var rawOutput = (int) Math.round(outputP + outputI + outputD);

// Post-process the output value
// 1. Apply value limits
int output = this.applyLowHighLimits(rawOutput);
final var isSaturated = rawOutput != output;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

if (targetDirectionChanged || !isSaturated) {
// Anti-windup: accept candidate errorSum only if output is not saturated
this.errorSum = candidateErrorSum;
}

// Sum up the error and limit Error-Sum to not grow too much. Otherwise the PID
// filter will stop reacting on changes properly.
this.errorSum = this.applyErrorSumLimit(this.errorSum + error);
// 2. Ensure output direction never contradicts target direction
if (target > 0 && output < 0) {
output = 0;
} else if (target < 0 && output > 0) {
output = 0;
} else if (target == 0) {
output = 0;
}

// Post-process the output value: convert to integer and apply value limits
return this.applyLowHighLimits(Math.round((float) output));
return output;
}

/**
Expand All @@ -118,6 +140,7 @@ public int applyPidFilter(int input, int target) {
public void reset() {
this.errorSum = 0;
this.firstRun = true;
this.lastTarget = 0;
}

/**
Expand All @@ -135,36 +158,4 @@ protected int applyLowHighLimits(int value) {
}
return value;
}

/**
* Applies the low and high limits to the error sum.
*
* @param value the input value
* @return the value within low and high limit
*/
private double applyErrorSumLimit(double value) {
// find (positive) limit from low & high limits
double errorSumLimit;
if (this.lowLimit != null && this.highLimit != null) {
errorSumLimit = Math.max(Math.abs(this.lowLimit), Math.abs(this.highLimit));
} else if (this.lowLimit != null) {
errorSumLimit = Math.abs(this.lowLimit);
} else if (this.highLimit != null) {
errorSumLimit = Math.abs(this.highLimit);
} else {
return value;
}

// apply additional factor to increase limit
errorSumLimit *= ERROR_SUM_LIMIT_FACTOR;

// apply limit
if (value < errorSumLimit * -1) {
return errorSumLimit * -1;
}
if (value > errorSumLimit) {
return errorSumLimit;
}
return value;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@
/**
* Use this class to test if the PID filter does what it should. Test cases can
* be generated using the Excel file in the docs directory. Just copy the
* contents of the Excel sheet "Unit-Test" into the testhis.t() method in this
* file.
* contents of the Excel sheet "Unit-Test" into the test() method in this file.
*/
public class PidFilterTest {

Expand All @@ -22,90 +21,58 @@ public void prepare() {
public void test() {
var p = new PidFilter(0.3, 0.3, 0);
p.setLimits(-100000, 100000);
this.t(p, 0, 0, 0);
this.t(p, 0, 0, 0);
this.t(p, 0, 0, 0);
this.t(p, 0, 0, 0);
this.t(p, 0, 0, 0);
this.t(p, 0, 0, 0);
this.t(p, 0, 0, 0);
this.t(p, 0, 0, 0);
this.t(p, 0, 0, 0);
this.t(p, 0, 0, 0);
this.t(p, 0, 20000, 6000);
this.t(p, 0, 20000, 12000);
this.t(p, 3793, 20000, 16862);
this.t(p, 8981, 20000, 20168);
this.t(p, 13963, 20000, 21979);
this.t(p, 17885, 20000, 22613);
this.t(p, 20473, 20000, 22472);
this.t(p, 21826, 20000, 21924);
this.t(p, 22234, 20000, 21254);
this.t(p, 22038, 20000, 20642);
this.t(p, 21542, 20000, 20180);
this.t(p, 20973, 20000, 19888);
this.t(p, 20472, 20000, 19746);
this.t(p, 20103, 20000, 19715);
this.t(p, 19877, 20000, 19752);
this.t(p, 19775, 20000, 19820);
this.t(p, 19760, 20000, 19892);
this.t(p, 19798, 20000, 19952);
this.t(p, 19857, 20000, 19995);
this.t(p, 19917, 20000, 20020);
this.t(p, 19966, 20000, 20030);
this.t(p, 20000, 20000, 20000);
this.t(p, 20019, 20000, 20024);
this.t(p, 20007, 20000, 20022);
this.t(p, 20018, 20000, 20017);
this.t(p, 20021, 20000, 20011);
this.t(p, 20018, 20000, 20005);
this.t(p, 20014, 20000, 20001);
this.t(p, 20008, 20000, 19999);
t(p, 0, 0, 0);
t(p, 0, 20000, 6000);
t(p, 0, 20000, 12000);
t(p, 3793, 20000, 15724);
t(p, 8981, 20000, 17474);
t(p, 13963, 20000, 17790);
t(p, 17885, 20000, 17248);
t(p, 20473, 20000, 16330);
t(p, 21826, 20000, 15376);
t(p, 22234, 20000, 14583);
t(p, 22038, 20000, 14031);
t(p, 21542, 20000, 13717);
t(p, 20973, 20000, 13596);
t(p, 20472, 20000, 13604);
t(p, 20103, 20000, 13684);
t(p, 19877, 20000, 13789);
t(p, 19775, 20000, 13887);
t(p, 19760, 20000, 13964);
t(p, 19798, 20000, 14013);
t(p, 19857, 20000, 14038);
t(p, 19917, 20000, 14045);
t(p, 19966, 20000, 14040);
}

@Test
public void testLimits() {
var p = new PidFilter(0.3, 0.3, 0);
p.setLimits(-10000, 10000);
this.t(p, 0, 0, 0);
this.t(p, 0, 0, 0);
this.t(p, 0, 0, 0);
this.t(p, 0, 0, 0);
this.t(p, 0, 0, 0);
this.t(p, 0, 0, 0);
this.t(p, 0, 0, 0);
this.t(p, 0, 0, 0);
this.t(p, 0, 0, 0);
this.t(p, 0, 0, 0);
this.t(p, 0, 10000, 3000);
this.t(p, 0, 10000, 6000);
this.t(p, 1896, 10000, 8431);
this.t(p, 4490, 10000, 10000);
this.t(p, 6981, 10000, 10000);
this.t(p, 8889, 10000, 10000);
this.t(p, 9591, 10000, 10000);
this.t(p, 9850, 10000, 10000);
this.t(p, 9945, 10000, 10000);
this.t(p, 9980, 10000, 10000);
this.t(p, 9993, 10000, 10000);
this.t(p, 9997, 10000, 10000);
this.t(p, 9999, 10000, 10000);
this.t(p, 10000, 10000, 10000);
this.t(p, 10000, 10000, 10000);
this.t(p, 10000, 10000, 10000);
this.t(p, 10000, 10000, 10000);
this.t(p, 10000, 10000, 10000);
this.t(p, 10000, 10000, 10000);
this.t(p, 10000, 10000, 10000);
this.t(p, 10000, 10000, 10000);
this.t(p, 10000, 10000, 10000);
this.t(p, 10000, 10000, 10000);
this.t(p, 10000, 10000, 10000);
this.t(p, 10000, 10000, 10000);
this.t(p, 10000, 10000, 10000);
this.t(p, 10000, 10000, 10000);
this.t(p, 10000, 10000, 10000);
this.t(p, 10000, 10000, 10000);
t(p, 0, 0, 0);
t(p, 0, 0, 0);
t(p, 0, 0, 0);
t(p, 0, 0, 0);
t(p, 0, 0, 0);
t(p, 0, 0, 0);
t(p, 0, 0, 0);
t(p, 0, 0, 0);
t(p, 0, 0, 0);
t(p, 0, 0, 0);
t(p, 0, 10000, 3000);
t(p, 0, 10000, 6000);
t(p, 1896, 10000, 7862);
t(p, 4490, 10000, 8737);
t(p, 6981, 10000, 8896);
t(p, 8889, 10000, 8656);
t(p, 9591, 10000, 8569);
t(p, 9850, 10000, 8536);
t(p, 9945, 10000, 8524);
t(p, 9980, 10000, 8519);
t(p, 9993, 10000, 8518);
t(p, 9997, 10000, 8517);
t(p, 9999, 10000, 8517);
t(p, 10000, 10000, 10000);
}

/**
Expand All @@ -119,39 +86,99 @@ public void testPriority() {

// Cycle 1
p.setLimits(-1000, 1000); // set by FixActivePower
this.t(p, 1000, 5000, 1000);
t(p, 1000, 5000, 1000);

// Cycle 2
// Limits and input value stay always at 1000 by FixActivePower
this.t(p, 1000, 5000, 1000);
t(p, 1000, 5000, 1000);

// Cycle 3
this.t(p, 1000, 5000, 1000);
t(p, 1000, 5000, 1000);

// Cycle 4
this.t(p, 1000, 5000, 1000);
t(p, 1000, 5000, 1000);

// Cycle 5
this.t(p, 1000, 5000, 1000);
t(p, 1000, 5000, 1000);

// Cycle 6
this.t(p, 1000, 5000, 1000);
t(p, 1000, 5000, 1000);

// Cycle 7
this.t(p, 1000, 5000, 1000);
t(p, 1000, 5000, 1000);

// Cycle 8
p.setLimits(-9999, 9999); // disable FixActivePower
this.t(p, 1000, 5000, 1200);
t(p, 1000, 5000, 2400);

// Cycle 9
this.t(p, 1000, 5000, 2400);
t(p, 1000, 5000, 3600);

// Cycle 10
this.t(p, 1000, 5000, 3600);
t(p, 1000, 5000, 4800);
}

private void t(PidFilter p, int input, int output, int expectedOutput) {
@Test
public void testDirectionChange() {
var p = new PidFilter(0.3, 0.3, 0.1);
p.setLimits(-10000, 10000);

// Discharge
t(p, 0, 3000, 900);
t(p, 0, 3000, 1800);
t(p, 0, 3000, 2700);
t(p, 569, 3000, 3202);
t(p, 569, 3000, 3988);
t(p, 1347, 3000, 4173);
t(p, 2942, 3000, 3630);
t(p, 3514, 3000, 3406);
t(p, 3871, 3000, 3059);
t(p, 4021, 3000, 2729);
t(p, 3989, 3000, 2460);
t(p, 3826, 3000, 2274);
t(p, 3585, 3000, 2179);
t(p, 3321, 3000, 2164);
t(p, 3075, 3000, 2213);
t(p, 2878, 3000, 2304);
t(p, 2747, 3000, 2413);
t(p, 2683, 3000, 2520);
t(p, 2680, 3000, 2611);
t(p, 2722, 3000, 2678);
t(p, 2793, 3000, 2715);
t(p, 2876, 3000, 2726);
t(p, 2957, 3000, 2715);
t(p, 3024, 3000, 2689);
t(p, 3071, 3000, 2656);
t(p, 3098, 3000, 2621);
t(p, 3104, 3000, 2590);
t(p, 3093, 3000, 2567);
t(p, 3072, 3000, 2552);

// Charge
t(p, 2900, -3000, -1753);
t(p, 2600, -3000, -3330);
t(p, 900, -3000, -3850);
t(p, -1347, -3000, -3617);
t(p, -2202, -3000, -3739);
t(p, -3321, -3000, -3281);
t(p, -3075, -3000, -3469);
t(p, -2878, -3000, -3559);
t(p, -2876, -3000, -3578);
t(p, -2957, -3000, -3558);
t(p, -3024, -3000, -3532);
t(p, -3071, -3000, -3499);
t(p, -3098, -3000, -3463);
t(p, -3104, -3000, -3432);
t(p, -3093, -3000, -3409);
t(p, -3072, -3000, -3395);

// Zero
t(p, -2500, 0, 0);
t(p, -1000, 0, 0);
t(p, 0, 0, 0);
}

private static void t(PidFilter p, int input, int output, int expectedOutput) {
System.out.println(String.format("%10d %10d %10d", input, output, expectedOutput));
assertEquals(expectedOutput, p.applyPidFilter(input, output));
}
Expand Down
Loading