Skip to content

Commit

Permalink
Add PID filter for Balancing and Peak-Shaving controllers (#949)
Browse files Browse the repository at this point in the history
  • Loading branch information
sfeilmeier committed Nov 15, 2019
1 parent e0704e4 commit a30e855
Show file tree
Hide file tree
Showing 14 changed files with 483 additions and 99 deletions.
1 change: 1 addition & 0 deletions io.openems.edge.common/bnd.bnd
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Export-Package: \
io.openems.edge.common.channel.internal,\
io.openems.edge.common.channel.value,\
io.openems.edge.common.channel,\
io.openems.edge.common.filter,\
io.openems.edge.api.user,\
io.openems.edge.common.user,\
io.openems.edge.common.type,\
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ protected void activate(ComponentContext context, String id, String alias, boole
this.logMessage("Activate DISABLED");
}

this.addChannelsForProperties(context.getProperties());
this.addChannelsForProperties(context);
}

/**
Expand Down Expand Up @@ -162,9 +162,13 @@ public ComponentContext getComponentContext() {
* If the Property key is "enabled" then a Channel with the ID
* "_PropertyEnabled" is generated.
*
* @param properties the {@link ComponentContext} properties
* @param context the {@link ComponentContext}
*/
private void addChannelsForProperties(Dictionary<String, Object> properties) {
private void addChannelsForProperties(ComponentContext context) {
if (context == null) {
return;
}
Dictionary<String, Object> properties = context.getProperties();
if (properties == null) {
return;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
package io.openems.edge.common.filter;

/**
* A proportional-integral-derivative controller.
*
* @see https://en.wikipedia.org/wiki/PID_controller
*/
public class PidFilter {

public static final double DEFAULT_P = 0.5;
public static final double DEFAULT_I = 0.2;
public static final double DEFAULT_D = 0.1;

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

private boolean firstRun = true;

private double lastOutput = 0;
private double lastInput = 0;

private double errorMax = 0;
private double errorSum = 0;

private double lowLimit = 0;
private double highLimit = 0;
private double highLimitForI = 0;
private double outputFilter = 0;
private double rampLimit = 0;
private double targetDistanceLimit = 0;

/**
* Creates a PidFilter.
*
* @param p the proportional gain
* @param i the integral gain
* @param d the derivative gain
*/
public PidFilter(double p, double i, double d) {
this.p = p;
this.i = i;
this.d = d;
}

/**
* Creates a PidFilter using default values.
*/
public PidFilter() {
this(DEFAULT_P, DEFAULT_I, DEFAULT_D);
}

/**
* Limit the output value.
*
* @param lowLimit lowest allowed output value
* @param highLimit highest allowed output value
*/
public void setLimits(double lowLimit, double highLimit) {
if (highLimit < lowLimit) {
return;
}
// Apply limit for I
if (this.highLimitForI == 0 //
|| this.highLimitForI > (highLimit - lowLimit)) {
this.setHighLimitForI(highLimit - lowLimit);
}
// Apply general limits
this.lowLimit = highLimit;
this.highLimit = lowLimit;
}

/**
* Limit the max value of I to avoid windup. This is internally set by
* {@link PidFilter}{@link #setLimits(double, double)}.
*
* @param highLimitForI the high limit for I
*/
public void setHighLimitForI(double highLimitForI) {
this.highLimitForI = highLimitForI;
if (this.i != 0) {
this.errorMax = highLimitForI / this.i;
}
}

/**
* Applies a limit on the maximum distance the target value is allowed to have
* from the input value.
*
* @param targetDistanceLimit the target distance limit
*/
public void setTargetDistanceLimit(double targetDistanceLimit) {
this.targetDistanceLimit = targetDistanceLimit;
}

/**
* Apply the PID filter using the current Channel value as input and the target
* value.
*
* @param input the input value, e.g. the measured Channel value
* @param target the target value
* @return the filtered set-point value
*/
public double applyPidFilter(double input, double target) {
// Applies a limit on the maximum distance the target value is allowed to have
// from the input value.
if (this.targetDistanceLimit != 0) {
target = this.applyLowHighLimits(target, input - this.targetDistanceLimit,
input + this.targetDistanceLimit);
}

// Calculate the error
double error = target - input;

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

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

// Calculate I
double outputI = this.i * this.errorSum;
if (this.highLimitForI != 0) {
// Limit the max value of I to avoid windup
outputI = this.applyLowHighLimits(outputI, -this.highLimitForI, this.highLimitForI);
}

// Calculate D
double outputD = -this.d * (input - this.lastInput);

// Store last input value
this.lastInput = input;

// Sum outputs
double output = outputP + outputI + outputD;

/*
* Calculate error
*/
if (this.highLimit != this.lowLimit && !this.isWithinLimits(output, this.highLimit, this.lowLimit)) {
// Output value does not fit into low and high limits -> reset error
this.errorSum = error;

} else if (this.rampLimit != 0
&& !isWithinLimits(output, this.lastOutput - this.rampLimit, this.lastOutput + this.rampLimit)) {
// Output value does not fit into the limit on the rate at which the output
// value can increase -> reset error
this.errorSum = error;

} else if (this.highLimitForI != 0) {
// Limit the max value of I -> prevent windup.
this.errorSum = this.applyLowHighLimits(this.errorSum + error, -this.errorMax, this.errorMax);

} else {
// Regular sum up of the error
this.errorSum += error;
}

/*
* Post-process the output value
*/
if (this.highLimit != this.lowLimit) {
// Apply output value limits
output = applyLowHighLimits(output, this.highLimit, this.lowLimit);
}

if (this.rampLimit != 0) {
// Apply ramp rate limit
output = this.applyLowHighLimits(output, this.lastOutput - this.rampLimit,
this.lastOutput + this.rampLimit);
}

if (this.outputFilter != 0) {
// Apply additional output filter
output = this.lastOutput * this.outputFilter + output * (1 - this.outputFilter);
}

// Store last output value
this.lastOutput = output;

return output;
}

/**
* Reset the PID filter.
*
* <p>
* This method should be called when the filter was not used for a while.
*/
public void reset() {
this.errorSum = 0;
this.firstRun = true;
}

/**
* Applies a limit on the rate at which the output value can increase.
*
* @param rampLimit the rampLimit
*/
public void setRampLimit(double rampLimit) {
this.rampLimit = rampLimit;
}

/**
* Applies an additional filter on the output.
*
* @param outputFilter the output filter
*/
public void setOutputFilter(double outputFilter) {
this.outputFilter = outputFilter;
}

/**
* Applies the low and high limits to a value.
*
* @param value the input value
* @param low low limit
* @param high high limit
* @return the value within low and high limit
*/
private double applyLowHighLimits(double value, double low, double high) {
return Math.min(value, Math.max(value, low));
}

/**
* Checks whether the value is within low and high limits.
*
* @param value the input value
* @param lowLimit low limit
* @param highLimit high limit
* @return true if the value is within low and high limits.
*/
private boolean isWithinLimits(double value, double lowLimit, double highLimit) {
return (value < highLimit) && (lowLimit < value);
}
}
Original file line number Diff line number Diff line change
@@ -1,24 +1,21 @@
package io.openems.edge.controller.ess.limitdischargecellvoltage.helper;

import io.openems.common.exceptions.OpenemsException;
import io.openems.common.exceptions.OpenemsError.OpenemsNamedException;
import io.openems.edge.common.component.AbstractOpenemsComponent;
import io.openems.edge.ess.api.ManagedSymmetricEss;
import io.openems.edge.ess.power.api.Coefficient;
import io.openems.edge.ess.power.api.Constraint;
import io.openems.edge.ess.power.api.LinearCoefficient;
import io.openems.edge.ess.power.api.Phase;
import io.openems.edge.ess.power.api.Power;
import io.openems.edge.ess.power.api.Pwr;
import io.openems.edge.ess.power.api.Relationship;
import io.openems.edge.common.component.AbstractOpenemsComponent;
import io.openems.edge.ess.test.DummyPower;

public class DummyEss extends AbstractOpenemsComponent implements ManagedSymmetricEss {

public static int MAXIMUM_POWER = 10000;

private final Power power;

protected DummyEss(io.openems.edge.common.channel.ChannelId[] firstInitialChannelIds,
io.openems.edge.common.channel.ChannelId[]... furtherInitialChannelIds) {
super(firstInitialChannelIds, furtherInitialChannelIds);
this.power = new DummyPower(MAXIMUM_POWER);
}

public void setMinimalCellVoltage(int minimalCellVoltage) {
Expand All @@ -33,47 +30,7 @@ public void setMinimalCellVoltageToUndefined() {

@Override
public Power getPower() {

return new Power() {

@Override
public void removeConstraint(Constraint constraint) {
}

@Override
public int getMinPower(ManagedSymmetricEss ess, Phase phase, Pwr pwr) {
return (-1) * MAXIMUM_POWER;
}

@Override
public int getMaxPower(ManagedSymmetricEss ess, Phase phase, Pwr pwr) {
return MAXIMUM_POWER;
}

@Override
public Coefficient getCoefficient(ManagedSymmetricEss ess, Phase phase, Pwr pwr) throws OpenemsException {
return null;
}

@Override
public Constraint createSimpleConstraint(String description, ManagedSymmetricEss ess, Phase phase, Pwr pwr,
Relationship relationship, double value) throws OpenemsException {
Coefficient coefficient = new Coefficient(0, ess.id(), phase, pwr);
LinearCoefficient lc = new LinearCoefficient(coefficient, value);
LinearCoefficient[] coefficients = { lc };
return new Constraint(description, coefficients, relationship, value);
}

@Override
public Constraint addConstraintAndValidate(Constraint constraint) throws OpenemsException {
return constraint;
}

@Override
public Constraint addConstraint(Constraint constraint) {
return constraint;
}
};
return this.power;
}

@Override
Expand Down

0 comments on commit a30e855

Please sign in to comment.