Skip to content

Commit

Permalink
Time-of-Use: improvements (#2559)
Browse files Browse the repository at this point in the history
Backports from FEMS Release 2023.2.3

- Improve energy calculations
- Change all values to positive
- Add more JUnit tests
- reduce EFFICIENCY_FACTOR from 1.20 to 1.17
- add additional way to calculate reference energy for CHARGE_GRID
- add additional initial populations that suggest CHARGE_GRID and DELAY_DISCHARGE
- Use static seed in JUnit tests
- Add (disabled) integration test
- Cleanup
  • Loading branch information
sfeilmeier authored Mar 3, 2024
1 parent 5cb340b commit eb5fdaf
Show file tree
Hide file tree
Showing 12 changed files with 899 additions and 391 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package io.openems.edge.controller.ess.timeofusetariff.optimizer;

import static io.openems.edge.controller.ess.timeofusetariff.StateMachine.BALANCING;
import static io.openems.edge.controller.ess.timeofusetariff.StateMachine.CHARGE_GRID;
import static io.openems.edge.controller.ess.timeofusetariff.StateMachine.DELAY_DISCHARGE;
import static io.openems.edge.controller.ess.timeofusetariff.optimizer.Utils.findFirstPeakIndex;
import static io.openems.edge.controller.ess.timeofusetariff.optimizer.Utils.findFirstValleyIndex;
import static java.util.Arrays.stream;

import java.util.List;
import java.util.stream.IntStream;
import java.util.stream.Stream;

import com.google.common.collect.ImmutableList;
import com.google.common.math.Quantiles;

import io.jenetics.Genotype;
import io.jenetics.IntegerChromosome;
import io.jenetics.IntegerGene;
import io.openems.edge.common.type.TypeUtils;
import io.openems.edge.controller.ess.timeofusetariff.StateMachine;

public class InitialPopulationUtils {

private InitialPopulationUtils() {
}

/**
* Builds an initial population:
*
* <ol>
* <li>Schedule with all periods BALANCING
* <li>Schedule from currently existing Schedule, i.e. the bestGenotype of last
* optimization run
* </ol>
*
* <p>
* NOTE: providing an "all periods BALANCING" Schedule as first Genotype makes
* sure, that this one wins in case there are other results with same cost, e.g.
* when battery never gets empty anyway.
*
* @param p the {@link Params}
* @return the {@link Genotype}
*/
public static ImmutableList<Genotype<IntegerGene>> buildInitialPopulation(Params p) {
var states = List.of(p.states());
var b = ImmutableList.<Genotype<IntegerGene>>builder(); //

// All BALANCING
b.add(Genotype.of(//
IntStream.range(0, p.numberOfPeriods()) //
.map(i -> states.indexOf(BALANCING)) //
.mapToObj(state -> IntegerChromosome.of(IntegerGene.of(state, 0, p.states().length))) //
.toList()));

if (p.existingSchedule().length > 0 //
&& Stream.of(p.existingSchedule()) //
.anyMatch(s -> s != BALANCING)) {
// Existing Schedule if available
b.add(Genotype.of(//
IntStream.range(0, p.numberOfPeriods()) //
// Map to state index; not-found maps to '-1', corrected to '0'
.map(i -> TypeUtils.fitWithin(0, p.states().length, states.indexOf(//
p.existingSchedule().length > i //
? p.existingSchedule()[i] //
: BALANCING))) //
.mapToObj(state -> IntegerChromosome.of(IntegerGene.of(state, 0, p.states().length))) //
.toList()));
}

// Suggest different combinations of CHARGE_GRID and DELAY_CHARGE
{
var peakIndex = findFirstPeakIndex(findFirstValleyIndex(0, p.prices()), p.prices());
var firstPrices = stream(p.prices()).limit(peakIndex).toArray();
if (firstPrices.length > 0 && states.contains(CHARGE_GRID) && states.contains(DELAY_DISCHARGE)) {
b.add(generateInitialGenotype(p.numberOfPeriods(), firstPrices, states, 5, 50));
b.add(generateInitialGenotype(p.numberOfPeriods(), firstPrices, states, 5, 75));
b.add(generateInitialGenotype(p.numberOfPeriods(), firstPrices, states, 10, 50));
b.add(generateInitialGenotype(p.numberOfPeriods(), firstPrices, states, 10, 75));
}
}

return b.build();
}

private static Genotype<IntegerGene> generateInitialGenotype(int numberOfPeriods, double[] prices,
List<StateMachine> states, int chargeGridPercentile, int delayDischargePercentile) {
var percentiles = Quantiles.percentiles().indexes(chargeGridPercentile, delayDischargePercentile)
.compute(prices);
return Genotype.of(//
IntStream.range(0, numberOfPeriods) //
.mapToObj(i -> {
if (i >= prices.length) {
return BALANCING;
}
var price = prices[i];
return price <= percentiles.get(chargeGridPercentile) //
? CHARGE_GRID //
: price <= percentiles.get(delayDischargePercentile) //
? DELAY_DISCHARGE //
: BALANCING;
}) //
.map(state -> IntegerChromosome.of(IntegerGene.of(states.indexOf(state), 0, states.size()))) //
.toList());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import static io.openems.edge.controller.ess.timeofusetariff.optimizer.Simulator.calculateCost;
import static io.openems.edge.controller.ess.timeofusetariff.optimizer.Utils.calculateExecutionLimitSeconds;
import static io.openems.edge.controller.ess.timeofusetariff.optimizer.Utils.createSimulatorParams;
import static io.openems.edge.controller.ess.timeofusetariff.optimizer.Utils.initializeRandomRegistryForProduction;
import static io.openems.edge.controller.ess.timeofusetariff.optimizer.Utils.logSchedule;
import static java.lang.Thread.sleep;

Expand All @@ -12,14 +13,12 @@
import java.time.ZonedDateTime;
import java.util.TreeMap;
import java.util.function.Supplier;
import java.util.random.RandomGeneratorFactory;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.collect.ImmutableList;

import io.jenetics.util.RandomRegistry;
import io.openems.common.exceptions.InvalidValueException;
import io.openems.common.test.TimeLeapClock;
import io.openems.common.worker.AbstractImmediateWorker;
Expand All @@ -39,16 +38,7 @@ public class Optimizer extends AbstractImmediateWorker {

public Optimizer(Supplier<Context> context) {
this.context = context;

/* Initialize 'Random' */
// Default RandomGenerator "L64X256MixRandom" might not be available. Choose
// best available.
System.setProperty("io.jenetics.util.defaultRandomGenerator", "Random");
var rgf = RandomGeneratorFactory.all() //
.filter(RandomGeneratorFactory::isStatistical) //
.sorted((f, g) -> Integer.compare(g.stateBits(), f.stateBits())).findFirst()
.orElse(RandomGeneratorFactory.of("Random"));
RandomRegistry.random(rgf.create());
initializeRandomRegistryForProduction();

// Run Optimizer thread in LOW PRIORITY
this.setPriority(Thread.MIN_PRIORITY);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ protected Builder existingSchedule(StateMachine... existingSchedule) {
public Params build() {
var numberOfPeriods = min(this.productions.length, min(this.consumptions.length, this.prices.length));
var essChargeInChargeGrid = calculateParamsChargeEnergyInChargeGrid(this.essMinSocEnergy,
this.essMaxSocEnergy, this.productions, this.consumptions);
this.essMaxSocEnergy, this.productions, this.consumptions, this.prices);

return new Params(numberOfPeriods, //
this.time, //
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import static io.jenetics.engine.EvolutionResult.toBestGenotype;
import static io.jenetics.engine.Limits.byExecutionTime;
import static io.openems.edge.controller.ess.timeofusetariff.optimizer.Utils.buildInitialPopulation;
import static io.openems.edge.controller.ess.timeofusetariff.optimizer.InitialPopulationUtils.buildInitialPopulation;
import static io.openems.edge.controller.ess.timeofusetariff.optimizer.Utils.calculateBalancingEnergy;
import static io.openems.edge.controller.ess.timeofusetariff.optimizer.Utils.calculateChargeGridEnergy;
import static io.openems.edge.controller.ess.timeofusetariff.optimizer.Utils.calculateMaxChargeEnergy;
Expand All @@ -29,7 +29,7 @@
public class Simulator {

/** Used to incorporate charge/discharge efficiency. */
public static final double EFFICIENCY_FACTOR = 1.2;
public static final double EFFICIENCY_FACTOR = 1.17;

/**
* Calculates the cost of a Schedule.
Expand Down Expand Up @@ -86,7 +86,8 @@ protected static double calculatePeriodCost(Params p, int i, StateMachine[] stat
final var essMaxChargeInBalancing = calculateMaxChargeEnergy(//
p.essTotalEnergy() /* unlimited in BALANCING */, //
p.essMaxEnergyPerPeriod(), essInitial);
final var essMaxDischarge = calculateMaxDischargeEnergy(p, essInitial);
final var essMaxDischarge = calculateMaxDischargeEnergy(p.essMinSocEnergy(), //
p.essMaxEnergyPerPeriod(), essInitial);
final var essChargeDischargeInBalancing = calculateBalancingEnergy(essMaxChargeInBalancing, essMaxDischarge,
production, consumption);
final int essChargeDischarge;
Expand Down
Loading

0 comments on commit eb5fdaf

Please sign in to comment.