From c3514290ba660dda5a889f114d0a7ee61da03be1 Mon Sep 17 00:00:00 2001 From: Erik van Velzen Date: Tue, 3 Dec 2024 14:48:08 +0100 Subject: [PATCH] quarter-hourly peak validation fix The validation mixed kWh and kW. The timeseries is now converted to kW before comparison. --- .../kotlin/companysurvey/Electricity.kt | 24 ++++--- .../kotlin/companysurvey/TimeSeries.kt | 27 ++++++++ .../kotlin/companysurvey/Validation.kt | 65 +++++++++++++++---- .../kotlin/companysurvey/TimeSeriesTest.kt | 13 ++++ 4 files changed, 106 insertions(+), 23 deletions(-) diff --git a/zummon/src/commonMain/kotlin/companysurvey/Electricity.kt b/zummon/src/commonMain/kotlin/companysurvey/Electricity.kt index ea828e0c..ba51dbda 100644 --- a/zummon/src/commonMain/kotlin/companysurvey/Electricity.kt +++ b/zummon/src/commonMain/kotlin/companysurvey/Electricity.kt @@ -34,23 +34,29 @@ data class Electricity ( fun getHasConnection(): Boolean { return hasConnection ?: false } - + + /** + * Contracted capacity for delivery of electricity from grid to company. + */ fun getContractedConnectionCapacityKw(): Double? { - when(kleinverbruikOrGrootverbruik) { - KleinverbruikOrGrootverbruik.GROOTVERBRUIK -> return grootverbruik?.contractedConnectionDeliveryCapacity_kW?.toDouble() - KleinverbruikOrGrootverbruik.KLEINVERBRUIK -> return kleinverbruik?.connectionCapacity?.toKw() - else -> return kleinverbruik?.connectionCapacity?.toKw() ?: grootverbruik?.contractedConnectionDeliveryCapacity_kW?.toDouble() + return when (kleinverbruikOrGrootverbruik) { + KleinverbruikOrGrootverbruik.GROOTVERBRUIK -> grootverbruik?.contractedConnectionDeliveryCapacity_kW?.toDouble() + KleinverbruikOrGrootverbruik.KLEINVERBRUIK -> kleinverbruik?.connectionCapacity?.toKw() + else -> kleinverbruik?.connectionCapacity?.toKw() ?: grootverbruik?.contractedConnectionDeliveryCapacity_kW?.toDouble() } } fun getPhysicalConnectionCapacityKw(): Double? { - when(kleinverbruikOrGrootverbruik) { - KleinverbruikOrGrootverbruik.GROOTVERBRUIK -> return grootverbruik?.physicalCapacityKw?.toDouble() - KleinverbruikOrGrootverbruik.KLEINVERBRUIK -> return kleinverbruik?.connectionCapacity?.toKw() - else -> return kleinverbruik?.connectionCapacity?.toKw() ?: grootverbruik?.physicalCapacityKw?.toDouble() + return when (kleinverbruikOrGrootverbruik) { + KleinverbruikOrGrootverbruik.GROOTVERBRUIK -> grootverbruik?.physicalCapacityKw?.toDouble() + KleinverbruikOrGrootverbruik.KLEINVERBRUIK -> kleinverbruik?.connectionCapacity?.toKw() + else -> kleinverbruik?.connectionCapacity?.toKw() ?: grootverbruik?.physicalCapacityKw?.toDouble() } } + /** + * Contracted capacity for feed-in of electricity from company to grid. + */ fun getContractedFeedInCapacityKw(): Double? { when (kleinverbruikOrGrootverbruik) { KleinverbruikOrGrootverbruik.GROOTVERBRUIK -> return grootverbruik?.contractedConnectionFeedInCapacity_kW?.toDouble() diff --git a/zummon/src/commonMain/kotlin/companysurvey/TimeSeries.kt b/zummon/src/commonMain/kotlin/companysurvey/TimeSeries.kt index fe519657..e7af2856 100644 --- a/zummon/src/commonMain/kotlin/companysurvey/TimeSeries.kt +++ b/zummon/src/commonMain/kotlin/companysurvey/TimeSeries.kt @@ -3,6 +3,7 @@ package com.zenmo.zummon.companysurvey import kotlinx.serialization.Serializable import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.days +import kotlin.time.Duration.Companion.hours import com.benasher44.uuid.Uuid import com.benasher44.uuid.uuid4 import com.zenmo.zummon.BenasherUuidSerializer @@ -166,3 +167,29 @@ enum class TimeSeriesType { ELECTRICITY_PRODUCTION, GAS_DELIVERY, } + +/** + * Represents a single point within the time series. + * Improvement: add timestamp + */ +data class DataPoint ( + val value: Float, + val unit: TimeSeriesUnit, + val timeStep: kotlin.time.Duration, +) { + fun kWh(): Double { + if (this.unit != TimeSeriesUnit.KWH) { + throw UnsupportedOperationException("Can only get the kWh from a kWh data point") + } + + return value.toDouble() + } + + fun kW(): Double { + if (this.unit != TimeSeriesUnit.KWH) { + throw UnsupportedOperationException("Can only get the kW from a kWh data point") + } + + return value * (1.hours / this.timeStep) + } +} diff --git a/zummon/src/commonMain/kotlin/companysurvey/Validation.kt b/zummon/src/commonMain/kotlin/companysurvey/Validation.kt index 448279b4..82059922 100644 --- a/zummon/src/commonMain/kotlin/companysurvey/Validation.kt +++ b/zummon/src/commonMain/kotlin/companysurvey/Validation.kt @@ -339,12 +339,32 @@ class ElectricityValidator : Validator { } } - //peak of delivery should be less than contracted capacity + /** + * peak of delivery should be less than contracted capacity + */ fun quarterHourlyDeliveryLowContractedCapacity(electricity: Electricity): ValidationResult { - val contractedCapacity = (electricity.getContractedConnectionCapacityKw() ?: 0.0).toFloat() - val pickDelivery = electricity.quarterHourlyDelivery_kWh?.values?.maxOrNull() ?: Float.MIN_VALUE - return if ( pickDelivery <= contractedCapacity) { - ValidationResult(Status.VALID, translate("electricity.quarterHourlyDeliveryLowContractedCapacityKw", contractedCapacity)) + val contractedCapacity_kW = electricity.getContractedConnectionCapacityKw() + if (contractedCapacity_kW == null) { + return ValidationResult(Status.MISSING_DATA, message( + en = "Gecontracteerd vermogen levering ontbreek", + nl = "Contracted delivery capacity missing", + )) + } + + if (electricity.quarterHourlyDelivery_kWh == null) { + return ValidationResult(Status.MISSING_DATA, message( + en = "Kwartierwaarden levering ontbreek", + nl = "Quarter-hourly delivery missing", + )) + } + + val peakDelivery = electricity.quarterHourlyDelivery_kWh.getPeak() + + return if ( peakDelivery.kW() <= contractedCapacity_kW) { + ValidationResult(Status.VALID, message( + en = "Piek van kwartierwaarden levering ${peakDelivery.kWh()} kWh valt binnen gecontracteerd vermogen levering ${contractedCapacity_kW} kW", + nl = "Peak of quarter-hourly delivery ${peakDelivery.kWh()} kWh does not exceed contracted capacity ${contractedCapacity_kW} kW", + )) } else { ValidationResult(Status.INVALID, translate("electricity.quarterHourlyDeliveryHighContractedCapacityKw", contractedCapacity)) } @@ -352,12 +372,33 @@ class ElectricityValidator : Validator { //peak of feed-in should be less than contracted capacity fun quarterFeedInLowContractedCapacity(electricity: Electricity): ValidationResult { - val contractedFeedInCapacity = (electricity.getContractedFeedInCapacityKw() ?: 0.0).toFloat() - val pickFeedIn = electricity.quarterHourlyFeedIn_kWh?.values?.maxOrNull() ?: Float.MIN_VALUE - return if (pickFeedIn < contractedFeedInCapacity) { - ValidationResult(Status.VALID, translate("electricity.quarterHourlyDeliveryLowContractedCapacityKw", contractedFeedInCapacity)) + val contractedFeedInCapacity_kW = (electricity.getContractedFeedInCapacityKw() ?: 0.0).toFloat() + if (contractedFeedInCapacity_kW == null) { + return ValidationResult(Status.MISSING_DATA, message( + en = "Gecontracteerd vermogen teruglevering ontbreek", + nl = "Contracted feed-in capacity missing", + )) + } + + if (electricity.quarterHourlyFeedIn_kWh == null) { + // Or not applicable if no solar panels + return ValidationResult(Status.MISSING_DATA, message( + en = "Kwartierwaarden teruglevering ontbreek", + nl = "Quarter-hourly feed-in missing", + )) + } + + val peakFeedIn = electricity.quarterHourlyFeedIn_kWh.getPeak() + return if (peakFeedIn.kW() < contractedFeedInCapacity_kW) { + ValidationResult(Status.VALID, message( + en = "Piek van kwartierwaarden teruglevering ${peakFeedIn.kWh()} kWh valt binnen gecontracteerd vermogen levering $contractedFeedInCapacity_kW kW", + nl = "Peak of quarter-hourly feed-in ${peakFeedIn.kWh()} kWh does not exceed contracted capacity $contractedFeedInCapacity_kW kW", + )) } else { - ValidationResult(Status.INVALID, translate("electricity.quarterHourlyDeliveryHighContractedCapacityKw", contractedFeedInCapacity)) + ValidationResult(Status.INVALID, message( + nl = "Piek van kwartierwaarden teruglevering ${peakFeedIn.kWh()} kWh mag niet hoger zijn dan gecontracteerd vermogen levering $contractedFeedInCapacity_kW kW", + en = "Peak of quarter-hourly feed-in ${peakFeedIn.kWh()} kWh delivery should be below contracted capacity $contractedFeedInCapacity_kW Kw", + )) } } @@ -595,10 +636,6 @@ val translations: Map>> = mapOf( "notEnoughValues" to "Not enough values for year: needed %d got %d", "annualFeedInMismatch" to "Annual feed in (%d) mismatch the total quarter hourly feed in (%d)", "annualFeedInMismatch" to "Annual feed in (%d) matches the total quarter hourly feed in (%d)", - - "quarterHourlyDeliveryLowContractedCapacityKw" to "Quarter-hourly stays lower than the Contracted CapacityKw (%d)", - "quarterHourlyDeliveryHighContractedCapacityKw" to "Quarter-hourly shouldn't go higher than the Contracted CapacityKw (%d)" - ), "grootverbruik" to mapOf( "notProvided" to "Large consumption data is not provided", diff --git a/zummon/src/commonTest/kotlin/companysurvey/TimeSeriesTest.kt b/zummon/src/commonTest/kotlin/companysurvey/TimeSeriesTest.kt index 0d5281d7..227805a2 100644 --- a/zummon/src/commonTest/kotlin/companysurvey/TimeSeriesTest.kt +++ b/zummon/src/commonTest/kotlin/companysurvey/TimeSeriesTest.kt @@ -112,4 +112,17 @@ class TimeSeriesTest { assertEquals(300f, yearValues.first()) assertEquals(200f, yearValues.last()) } + + @Test + fun testGetPeakKw() { + val timeSeries = TimeSeries( + type = TimeSeriesType.ELECTRICITY_DELIVERY, + start = Instant.parse("2024-01-01T00:00:00+01:00"), + values = floatArrayOf(1f, 2f, 1f) + ) + + val peak = timeSeries.getPeak() + assertEquals(2.0, peak.kWh()) + assertEquals(8.0, peak.kW()) + } }