Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package de.hatoka.eos.optimization.capi.business;

import de.hatoka.eos.simulation.capi.business.config.SimulationConfig;
import de.hatoka.eos.simulation.capi.business.config.SimulationTimeSettings;

import java.time.Duration;
import java.time.ZonedDateTime;

public record OptimizationRequest(ZonedDateTime startDate, ZonedDateTime endDate, Duration stepDuration)
{
public static OptimizationRequest valueOf(SimulationConfig simulationConfig)
{
SimulationTimeSettings timeSettings = simulationConfig.getTimeSettings();
return new OptimizationRequest(timeSettings.getZonedStartTime(), timeSettings.getZonedEndTime(), timeSettings.getStepDuration());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,19 @@

/**
* OptimizationResult contains the simulation result and the calculated penalty for optimization goals.
* @param simulationResult result of simulation
* @param penalty penalty for non reached goals
* @param manipulators
*/
public record OptimizationResult(SimulationResult simulationResult, Money penalty)
public record OptimizationResult(Money penalty,
java.util.List<de.hatoka.eos.simulation.capi.business.simulation.DeviceManipulator> manipulators)
{
public Money getPenalty()
{
return penalty;
}

public boolean isBetterThan(OptimizationResult other)
{
return getPenalty().isLessThan(other.getPenalty());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@
*/
public interface Optimizer
{
OptimizationResult optimize(InstallationConfig config, OptimizationGoals goals);
OptimizationResult optimize(InstallationConfig config, OptimizationGoals goals, OptimizationRequest optimizationRequest);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package de.hatoka.eos.optimization.capi.tasks;

import de.hatoka.eos.simulation.capi.business.device.Device;
import de.hatoka.eos.simulation.capi.business.device.DeviceRef;
import de.hatoka.eos.simulation.capi.business.device.DeviceType;
import de.hatoka.eos.simulation.capi.business.simulation.DeviceManipulator;
import de.hatoka.eos.simulation.internal.business.devices.ElectricCar;
import de.hatoka.eos.units.capi.Percentage;

import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public record CarCharge(int hours, long from, long to, long start) implements DeviceManipulator
{
public static CarCharge valueOf(ZonedDateTime from, ZonedDateTime to, ZonedDateTime start, int hours)
{
return new CarCharge(hours, from.toInstant().toEpochMilli(), to.toInstant().toEpochMilli(), start.toInstant().toEpochMilli());
}

public static CarCharge init(ZonedDateTime from, ZonedDateTime to, int hours)
{
return valueOf(from, to, from, hours);
}

@Override
public Map<DeviceRef, Device> apply(ZonedDateTime time, Map<DeviceRef, Device> devices)
{
Map<DeviceRef, Device> result = new HashMap<>();
devices.forEach((r, d) -> {
result.put(r, apply(time, r, d));
});
return result;
}
private static final ZoneId UTC = ZoneId.of("UTC");

public ZonedDateTime getZonedDateTime(long time)
{
return Instant.ofEpochMilli(time).atZone(UTC);
}

private Device apply(ZonedDateTime time, DeviceRef deviceRef, Device device)
{
if (!DeviceType.ELECTRIC_CAR.equals(deviceRef.type()))
{
return device;
}
if (device instanceof ElectricCar car)
{
if (time.isBefore(getZonedDateTime(start)) || time.isAfter(getZonedDateTime(start).plusHours(hours)))
{
return car.resetOverrideForceChargingLimit();
}
return car.setOverrideForceChargingLimit(Percentage.ONE_HUNDRED);
}
return device;
}

@Override
public List<DeviceManipulator> evolute()
{
List<DeviceManipulator> result = new ArrayList<>();
if (getZonedDateTime(start()).plusHours(1).isBefore(getZonedDateTime(to)))
{
result.add(new CarCharge(hours, from, to, getZonedDateTime(start).plusHours(1).toInstant().toEpochMilli()));
}
if (!getZonedDateTime(from).isAfter(getZonedDateTime(start()).minusHours(1)))
{
result.add(new CarCharge(hours, from, to, getZonedDateTime(start).minusHours(1).toInstant().toEpochMilli()));
}
result.add(new CarCharge(hours + 1, from, to, start));
if (hours - 1 > 0)
{
result.add(new CarCharge(hours - 1, from, to, start));
}
return result;
}
}
Original file line number Diff line number Diff line change
@@ -1,28 +1,100 @@
package de.hatoka.eos.optimization.internal.business;

import de.hatoka.eos.simulation.capi.business.config.InstallationConfig;
import de.hatoka.eos.simulation.capi.business.device.DeviceFactory;
import de.hatoka.eos.simulation.capi.business.simulation.EnergySystem;
import de.hatoka.eos.simulation.capi.business.simulation.SimulationResult;
import de.hatoka.eos.optimization.capi.business.OptimizationRequest;
import de.hatoka.eos.optimization.capi.business.OptimizationResult;
import de.hatoka.eos.optimization.capi.business.Optimizer;
import de.hatoka.eos.optimization.capi.goals.OptimizationGoals;
import de.hatoka.eos.optimization.capi.tasks.CarCharge;
import de.hatoka.eos.simulation.capi.business.config.InstallationConfig;
import de.hatoka.eos.simulation.capi.business.device.DeviceFactory;
import de.hatoka.eos.simulation.capi.business.forecast.Forecasts;
import de.hatoka.eos.simulation.capi.business.simulation.DeviceManipulator;
import de.hatoka.eos.simulation.capi.business.simulation.SimulationRequest;
import de.hatoka.eos.simulation.capi.business.simulation.SimulationResult;
import de.hatoka.eos.simulation.capi.business.simulation.Simulator;
import jakarta.annotation.Nonnull;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.*;

/**
* Implementation of an Optimizer
*/
@Singleton
public class OptimizerImpl implements Optimizer
{
private static Logger LOGGER = LoggerFactory.getLogger(OptimizerImpl.class);
@Inject
private DeviceFactory deviceFactory;
@Inject
private Simulator simulator;

private final List<List<DeviceManipulator>> toDos = new ArrayList<>();
private final Set<List<DeviceManipulator>> done = new HashSet<>();

@Override
public OptimizationResult optimize(InstallationConfig config, OptimizationGoals goals)
public OptimizationResult optimize(InstallationConfig config, OptimizationGoals goals, OptimizationRequest optimizationRequest)
{
// initial without manipulation
OptimizationResult result = evaluate(config, goals, optimizationRequest, 0, List.of());
LOGGER.info("initial result {}", result);

toDos.add(List.of(CarCharge.init(optimizationRequest.startDate(), optimizationRequest.endDate(), 1)));
for (int counter = 1; counter < 1000 && !toDos.isEmpty(); counter++)
{
List<DeviceManipulator> manipulators = toDos.removeFirst();
OptimizationResult optResult = evaluate(config, goals, optimizationRequest, counter, manipulators);
if (optResult.isBetterThan(result))
{
result = optResult;
LOGGER.info("found better result {}", optResult);
}
else {
LOGGER.trace("found worse result {}", optResult);
}
done.add(manipulators);
toDos.addAll(evolute(manipulators).stream().filter(this::doesNotExist).toList());
}
return result;
}

private boolean doesNotExist(List<DeviceManipulator> manipulators)
{
return !contains(manipulators);
}

private boolean contains(List<DeviceManipulator> manipulators)
{
return done.contains(manipulators) || toDos.contains(manipulators);
}

private List<List<DeviceManipulator>> evolute(List<DeviceManipulator> manipulators)
{
List<List<DeviceManipulator>> result = new ArrayList<>();
for (DeviceManipulator manipulator : manipulators)
{
for (DeviceManipulator child : manipulator.evolute())
{
List<DeviceManipulator> others = new ArrayList<>(manipulators);
others.remove(manipulator);
others.add(child);
result.add(others);
}
}
return result;
}

@Nonnull
private OptimizationResult evaluate(InstallationConfig config, OptimizationGoals goals, OptimizationRequest optimizationRequest, int counter,
List<DeviceManipulator> manipulators)
{
SimulationResult simResult = new SimulationResult(null, null, deviceFactory.createInitialState(config.getDevices()), EnergySystem.INIT);
return new OptimizationResult(simResult, goals.getPenalty(simResult));
SimulationRequest request = new SimulationRequest("optimization-" + counter, optimizationRequest.startDate(), optimizationRequest.endDate(),
optimizationRequest.stepDuration(), deviceFactory.createDevices(config.getDevices()), Collections.emptyMap(),
Forecasts.STANDARD);
SimulationResult simResult = simulator.simulate(request, manipulators);
return new OptimizationResult(goals.getPenalty(simResult), manipulators);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public void testPenaltyCalculationWhenBatteryIs60Percent() throws IOException
// Set goal to reach 80% charge using reflection since fields are private
Money actualPenalty = goals.getPenalty(new SimulationResult(null, null, Map.of(carRef, carState), EnergySystem.INIT));

// Shortfall: 90% - 60% = 30% -> 3 x 5 EUR = 15 EUR
assertEquals(Money.ofEur(15), actualPenalty);
// Shortfall: 90% - 60% = 30% -> 3 x 20 EUR = 60 EUR
assertEquals(Money.ofEur(60), actualPenalty);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public void testPenaltyCalculationWhenBatteryIs60Percent() throws IOException
Money actualPenalty = goals.getPenalty(new SimulationResult(null, null, Map.of(carRef, carState),
EnergySystem.INIT.importEnergy(Energy.ofKwh(10.0), Money.ofEur(2.40)).exportEnergy(Energy.ofKwh(10.0), Money.ofEur(1.40))));

// Shortfall: 90% - 60% = 30% -> 3 x 5 EUR = 15 EUR + 1EUR (for grid difference)
assertEquals(Money.ofEur(16), actualPenalty);
// Shortfall: 90% - 60% = 30% -> 3 x 20 EUR = 60 EUR + 1EUR (for grid difference)
assertEquals(Money.ofEur(61), actualPenalty);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package de.hatoka.eos.optimization.capi.tasks;

import de.hatoka.eos.simulation.capi.business.simulation.DeviceManipulator;
import de.hatoka.eos.units.capi.Percentage;
import org.junit.jupiter.api.Test;

import java.time.ZonedDateTime;
import java.util.List;

import static org.junit.jupiter.api.Assertions.*;

/**
* Test for CarCharge task, specifically the evolute() method.
*/
public class CarChargeTest
{
private CarCharge from(DeviceManipulator manipulator)
{
if (manipulator instanceof CarCharge carCharge)
{
return carCharge;
}
return null;
}

@Test
public void testEvoluteReturnsNeighbors()
{
ZonedDateTime from = ZonedDateTime.parse("2026-01-02T08:00:00Z");
ZonedDateTime to = ZonedDateTime.parse("2026-01-02T18:00:00Z");
ZonedDateTime start = ZonedDateTime.parse("2026-01-02T12:00:00Z");

CarCharge carCharge = CarCharge.valueOf(from, to, start, 5);

List<DeviceManipulator> neighbors = carCharge.evolute();
assertEquals(4, neighbors.size(), "Expected 4 - 2 dimensions - start time and duration");
// first plus one hour
assertEquals(ZonedDateTime.parse("2026-01-02T13:00:00Z").toInstant().toEpochMilli(), from(neighbors.get(0)).start());
// second minus one hour
assertEquals(ZonedDateTime.parse("2026-01-02T11:00:00Z").toInstant().toEpochMilli(), from(neighbors.get(1)).start());
// third plus one hour in duration
assertEquals(6, from(neighbors.get(2)).hours());
// third minus one hour in duration
assertEquals(4, from(neighbors.get(3)).hours());
}
}

Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package de.hatoka.eos.optimization.internal.business;

import de.hatoka.eos.optimization.capi.business.OptimizationRequest;
import de.hatoka.eos.simulation.capi.business.config.InstallationConfig;
import de.hatoka.eos.simulation.capi.business.config.SimulationConfig;
import de.hatoka.eos.units.capi.Money;
import de.hatoka.eos.simulation.internal.business.config.ConfigurationLoader;
import de.hatoka.eos.optimization.capi.business.OptimizationResult;
Expand All @@ -12,6 +14,8 @@
import org.junit.jupiter.api.Test;

import java.io.IOException;
import java.time.ZoneId;
import java.time.ZonedDateTime;

import static org.junit.jupiter.api.Assertions.assertEquals;

Expand All @@ -34,10 +38,12 @@ public class OptimizerTest
@Test
public void testDoNothing() throws IOException
{
InstallationConfig config = configurationLoader.loadInstallation("test-installation-for-optimization.yaml");
InstallationConfig config = configurationLoader.loadInstallation("test-installation-for-optimization-car-only.yaml");
SimulationConfig simulationConfig = configurationLoader.loadSimulation("test-simulation.yaml");
OptimizationGoals goals = optimizationConfigurationLoader.loadGoals("goal-for-optimization.yaml");
OptimizationResult result = optimizer.optimize(config, goals);
OptimizationRequest request = OptimizationRequest.valueOf(simulationConfig);
OptimizationResult result = optimizer.optimize(config, goals, request);
// starts at 80% goal is 90% makes 10% difference, 5 EUR
assertEquals(Money.ofEur(5), result.getPenalty());
assertEquals(Money.ofEur(17.16), result.getPenalty());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,6 @@ public void testLoadOptimizationGoal() throws IOException
assertEquals(0.1, goal.getCarCharging().getPenalty().percentage().value());

assertNotNull(goal.getCarCharging().getPenalty().price());
assertEquals(Money.ofEur(5), goal.getCarCharging().getPenalty().price());
assertEquals(Money.ofEur(20), goal.getCarCharging().getPenalty().price());
}
}
2 changes: 1 addition & 1 deletion optimization/src/test/resources/goal-for-optimization.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ carCharging:
penalty:
percentage: 0.1
price:
amount: 5
amount: 20
currency: "EUR"
Loading