charging optimization
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
package com.frankenergie.smartasset.config
|
||||
|
||||
import com.frankenergie.smartasset.model.ChargingGroup
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import java.math.BigDecimal
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalDateTime
|
||||
|
||||
@Configuration
|
||||
class ChargingGroupsConfig {
|
||||
|
||||
@Bean
|
||||
fun chargingGroups(): List<ChargingGroup> {
|
||||
val today = LocalDate.now()
|
||||
return listOf(
|
||||
ChargingGroup("A", today.atTime(0, 0), today.atTime(8, 30), BigDecimal("5"), BigDecimal("2")),
|
||||
ChargingGroup("B", today.atTime(0, 0), today.atTime(11, 0), BigDecimal("10"), BigDecimal("3")),
|
||||
ChargingGroup("C", today.atTime(13, 0), today.atTime(18, 0), BigDecimal("4"), BigDecimal("1")),
|
||||
ChargingGroup("D", today.atTime(13, 0), today.atTime(21, 0), BigDecimal("20"), BigDecimal("6")),
|
||||
ChargingGroup("E", today.atTime(17, 30), today.atTime(22, 0), BigDecimal("5"), BigDecimal("2")),
|
||||
ChargingGroup("F", today.atTime(17, 30), today.plusDays(1).atTime(0, 0), BigDecimal("15"), BigDecimal("5"))
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.frankenergie.smartasset.model
|
||||
|
||||
import java.math.BigDecimal
|
||||
import java.time.LocalDateTime
|
||||
|
||||
data class ChargingGroup(
|
||||
val id: String,
|
||||
val startTime: LocalDateTime,
|
||||
val endTime: LocalDateTime,
|
||||
val neededChargeMWh: BigDecimal,
|
||||
val maxPowerMW: BigDecimal
|
||||
)
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.frankenergie.smartasset.model
|
||||
|
||||
import java.math.BigDecimal
|
||||
|
||||
data class ChargingPlan(
|
||||
val groupAllocations: List<GroupAllocation>,
|
||||
val totalCost: BigDecimal
|
||||
) {
|
||||
val averagePricePerMWh: BigDecimal
|
||||
get() {
|
||||
val totalMWh = groupAllocations.sumOf { it.totalMWh }
|
||||
return if (totalMWh > BigDecimal.ZERO) totalCost.divide(totalMWh, 2, java.math.RoundingMode.HALF_UP) else BigDecimal.ZERO
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.frankenergie.smartasset.model
|
||||
|
||||
import java.math.BigDecimal
|
||||
import java.time.LocalDateTime
|
||||
|
||||
data class GroupAllocation(
|
||||
val groupId: String,
|
||||
val allocations: List<QuarterAllocation>,
|
||||
val totalMWh: BigDecimal,
|
||||
val totalCost: BigDecimal
|
||||
)
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.frankenergie.smartasset.model
|
||||
|
||||
import java.math.BigDecimal
|
||||
import java.time.LocalDateTime
|
||||
|
||||
data class QuarterAllocation(
|
||||
val startTime: LocalDateTime,
|
||||
val endTime: LocalDateTime,
|
||||
val mwh: BigDecimal,
|
||||
val pricePerMWh: BigDecimal
|
||||
) {
|
||||
val cost: BigDecimal get() = mwh * pricePerMWh
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package com.frankenergie.smartasset.service
|
||||
|
||||
import com.frankenergie.smartasset.domain.OrderBook
|
||||
import com.frankenergie.smartasset.model.*
|
||||
import org.springframework.stereotype.Service
|
||||
import java.math.BigDecimal
|
||||
import java.time.LocalDateTime
|
||||
|
||||
@Service
|
||||
class ChargingOptimizerService {
|
||||
private val quarterDurationHours = BigDecimal("0.25")
|
||||
|
||||
fun optimize(
|
||||
groups: List<ChargingGroup>,
|
||||
orderBook: OrderBook,
|
||||
chargedSoFar: Map<String, BigDecimal> = emptyMap()
|
||||
): ChargingPlan {
|
||||
val groupAllocations = groups.map { group ->
|
||||
optimizeGroup(group, orderBook, chargedSoFar[group.id] ?: BigDecimal.ZERO)
|
||||
}
|
||||
|
||||
val totalCost = groupAllocations.sumOf { it.totalCost }
|
||||
return ChargingPlan(groupAllocations, totalCost)
|
||||
}
|
||||
|
||||
private fun optimizeGroup(
|
||||
group: ChargingGroup,
|
||||
orderBook: OrderBook,
|
||||
alreadyCharged: BigDecimal
|
||||
): GroupAllocation {
|
||||
val remainingNeed = (group.neededChargeMWh - alreadyCharged).max(BigDecimal.ZERO)
|
||||
val maxPerQuarter = group.maxPowerMW * quarterDurationHours
|
||||
val quarters = getQuartersInWindow(group.startTime, group.endTime)
|
||||
|
||||
val quartersWithPrices = quarters.mapNotNull { period ->
|
||||
val bestAsk = orderBook.getBestAsk(period)
|
||||
bestAsk?.let { period to it.price }
|
||||
}.sortedBy { it.second }
|
||||
|
||||
val allocations = mutableListOf<QuarterAllocation>()
|
||||
var allocated = BigDecimal.ZERO
|
||||
|
||||
for ((period, price) in quartersWithPrices) {
|
||||
if (allocated >= remainingNeed) break
|
||||
|
||||
val toAllocate = maxPerQuarter.min(remainingNeed - allocated)
|
||||
allocations.add(
|
||||
QuarterAllocation(
|
||||
startTime = period.startTime,
|
||||
endTime = period.endTime,
|
||||
mwh = toAllocate,
|
||||
pricePerMWh = price
|
||||
)
|
||||
)
|
||||
allocated += toAllocate
|
||||
}
|
||||
|
||||
return GroupAllocation(
|
||||
groupId = group.id,
|
||||
allocations = allocations,
|
||||
totalMWh = allocated,
|
||||
totalCost = allocations.sumOf { it.cost }
|
||||
)
|
||||
}
|
||||
|
||||
private fun getQuartersInWindow(start: LocalDateTime, end: LocalDateTime): List<DeliveryPeriod> {
|
||||
val quarters = mutableListOf<DeliveryPeriod>()
|
||||
var current = start
|
||||
while (current < end) {
|
||||
val quarterEnd = current.plusMinutes(15)
|
||||
quarters.add(DeliveryPeriod(current, quarterEnd.coerceAtMost(end)))
|
||||
current = quarterEnd
|
||||
}
|
||||
return quarters
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user