From 64a67ecd5379a9d9d525902168d1ddebd499241c Mon Sep 17 00:00:00 2001 From: danielfeismann Date: Sat, 24 Aug 2024 10:32:58 +0200 Subject: [PATCH] Refactoring of ThermalGrid.handleInfeed to fix thermal storage recharge correctly when empty --- CHANGELOG.md | 1 + .../simona/model/participant/HpModel.scala | 7 + .../simona/model/thermal/ThermalGrid.scala | 334 ++++++++++++++---- .../edu/ie3/simona/agent/em/EmAgentIT.scala | 16 +- .../model/thermal/ThermalGridTestData.scala | 4 + .../ThermalGridWithHouseAndStorageSpec.scala | 8 +- .../ThermalGridWithHouseOnlySpec.scala | 12 + .../ThermalGridWithStorageOnlySpec.scala | 12 + .../test/common/input/EmInputTestData.scala | 2 +- 9 files changed, 317 insertions(+), 79 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 74bc169d92..3f51e98bd9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -106,6 +106,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fix activation of Hp when not under control of an EM [#922](https://github.com/ie3-institute/simona/issues/922) - Fixed ThermalStorageResults having multiple entries [#924](https://github.com/ie3-institute/simona/issues/924) - Fix determineState of ThermalHouse [#926](https://github.com/ie3-institute/simona/issues/926) +- Refactoring of `ThermalGrid.handleInfeed` to fix thermal storage recharge correctly when empty [#930](https://github.com/ie3-institute/simona/issues/930) ## [3.0.0] - 2023-08-07 diff --git a/src/main/scala/edu/ie3/simona/model/participant/HpModel.scala b/src/main/scala/edu/ie3/simona/model/participant/HpModel.scala index 569bd48c10..7e2da343fe 100644 --- a/src/main/scala/edu/ie3/simona/model/participant/HpModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant/HpModel.scala @@ -217,6 +217,10 @@ final case class HpModel( * data of heat pump including state of the heat pump * @param isRunning * determines whether the heat pump is running or not + * @param houseDemand + * determines if the thermal house has heat demand + * @param storageDemand + * determines if the thermal storage has heat demand * @return * next [[HpState]] */ @@ -245,7 +249,10 @@ final case class HpModel( state.thermalGridState, state.ambientTemperature.getOrElse(relevantData.ambientTemperature), relevantData.ambientTemperature, + isRunning, newThermalPower, + houseDemand, + storageDemand, ) HpState( diff --git a/src/main/scala/edu/ie3/simona/model/thermal/ThermalGrid.scala b/src/main/scala/edu/ie3/simona/model/thermal/ThermalGrid.scala index 28d299d67e..bd63a8030b 100644 --- a/src/main/scala/edu/ie3/simona/model/thermal/ThermalGrid.scala +++ b/src/main/scala/edu/ie3/simona/model/thermal/ThermalGrid.scala @@ -146,6 +146,7 @@ final case class ThermalGrid( } /** Update the current state of the grid + * * @param tick * Instance in time * @param state @@ -154,8 +155,14 @@ final case class ThermalGrid( * Ambient temperature until this tick * @param ambientTemperature * Actual ambient temperature + * @param isRunning + * determines whether the heat pump is running or not * @param qDot * Thermal energy balance + * @param houseDemand + * determines if the thermal house has heat demand + * @param storageDemand + * determines if the thermal storage has heat demand * @return * The updated state of the grid */ @@ -164,9 +171,21 @@ final case class ThermalGrid( state: ThermalGridState, lastAmbientTemperature: Temperature, ambientTemperature: Temperature, + isRunning: Boolean, qDot: Power, + houseDemand: Boolean, + storageDemand: Boolean, ): (ThermalGridState, Option[ThermalThreshold]) = if (qDot > zeroKW) - handleInfeed(tick, lastAmbientTemperature, ambientTemperature, state, qDot) + handleInfeed( + tick, + lastAmbientTemperature, + ambientTemperature, + state, + isRunning, + qDot, + houseDemand, + storageDemand, + ) else handleConsumption( tick, @@ -176,8 +195,9 @@ final case class ThermalGrid( qDot, ) - /** Handles the case, when a grid has infeed. First, heat up all the houses to - * their maximum temperature, then fill up the storages + /** Handles the case, when a grid has infeed. Depending which entity has some + * heat demand the house or the storage will be heated up / filled up. + * * @param tick * Current tick * @param lastAmbientTemperature @@ -186,8 +206,14 @@ final case class ThermalGrid( * Actual ambient temperature * @param state * Current state of the houses + * @param isRunning + * determines whether the heat pump is running or not * @param qDot * Infeed to the grid + * @param houseDemand + * determines if the thermal house has heat demand + * @param storageDemand + * determines if the thermal storage has heat demand * @return * Updated thermal grid state */ @@ -196,41 +222,221 @@ final case class ThermalGrid( lastAmbientTemperature: Temperature, ambientTemperature: Temperature, state: ThermalGridState, + isRunning: Boolean, qDot: Power, - ): (ThermalGridState, Option[ThermalThreshold]) = - house.zip(state.houseState) match { - case Some((thermalHouse, lastHouseState)) => - /* Set thermal power exchange with storage to zero */ - // TODO: We would need to issue a storage result model here... - val updatedStorageState = storage.zip(state.storageState) match { - case Some((thermalStorage, storageState)) => - Some( - thermalStorage - .updateState( - tick, - zeroKW, - storageState, - ) - ._1 - ) - case _ => state.storageState + houseDemand: Boolean, + heatStorageDemand: Boolean, + ): (ThermalGridState, Option[ThermalThreshold]) = { + // TODO: We would need to issue a storage result model here... + + /* Consider the action in the last state */ + val (qDotHouseLastState, qDotStorageLastState) = state match { + case ThermalGridState(Some(houseState), Some(storageState)) => + (houseState.qDot, storageState.qDot) + case ThermalGridState(Some(houseState), None) => (houseState.qDot, zeroKW) + case ThermalGridState(None, Some(storageState)) => + (zeroKW, storageState.qDot) + case _ => + throw new InconsistentStateException( + "There should be at least a house or a storage state." + ) + } + + if ( + (qDotHouseLastState > zeroKW && !(qDotStorageLastState < zeroKW)) | (qDotStorageLastState > zeroKW & heatStorageDemand) + ) { + val (updatedHouseState, thermalHouseThreshold, remainingQDotHouse) = + handleInfeedHouse( + tick, + lastAmbientTemperature, + ambientTemperature, + state, + qDotHouseLastState, + ) + val (updatedStorageState, thermalStorageThreshold) = + if ( + qDotStorageLastState >= zeroKW && remainingQDotHouse > qDotStorageLastState + ) { + handleInfeedStorage( + tick, + state, + remainingQDotHouse, + ) + } else { + handleInfeedStorage( + tick, + state, + qDotStorageLastState, + ) } - val (updatedHouseState, maybeHouseThreshold) = - thermalHouse.determineState( + val nextThreshold = determineMostRecentThreshold( + thermalHouseThreshold, + thermalStorageThreshold, + ) + ( + state.copy( + houseState = updatedHouseState, + storageState = updatedStorageState, + ), + nextThreshold, + ) + } + // Handle edge case where house get heated from storage and HP will be activated in between + else if ((qDotHouseLastState > zeroKW && qDotStorageLastState < zeroKW)) { + if (isRunning) { + handleCases( + tick, + lastAmbientTemperature, + ambientTemperature, + state, + qDot, + zeroKW, + ) + } else { + + handleCases( + tick, + lastAmbientTemperature, + ambientTemperature, + state, + qDotHouseLastState, + qDotStorageLastState, + ) + } + } else { + + (houseDemand, heatStorageDemand) match { + + case (true, _) => + // house first then heatStorage after heating House + handleCases( tick, - lastHouseState, lastAmbientTemperature, ambientTemperature, + state, qDot, + zeroKW, ) + case (false, true) => + handleCases( + tick, + lastAmbientTemperature, + ambientTemperature, + state, + zeroKW, + qDot, + ) + + case (false, false) => + handleCases( + tick, + lastAmbientTemperature, + ambientTemperature, + state, + zeroKW, + zeroKW, + ) + case _ => + throw new InconsistentStateException( + "There should be at least a house or a storage state." + ) + } + } + } + + /** Handles the different cases, of thermal flows from and into the thermal + * grid. + * + * @param tick + * Current tick + * @param lastAmbientTemperature + * Ambient temperature until this tick + * @param ambientTemperature + * actual ambient temperature + * @param state + * Current state of the thermal grid + * @param qDotHouse + * Infeed to the house + * @param qDotStorage + * Infeed to the heat storage + * @return + * Updated thermal grid state and the next threshold if there is one + */ + private def handleCases( + tick: Long, + lastAmbientTemperature: Temperature, + ambientTemperature: Temperature, + state: ThermalGridState, + qDotHouse: Power, + qDotHeatStorage: Power, + ): (ThermalGridState, Option[ThermalThreshold]) = { + // FIXME: Is there any case where we get back some remainingQDotHouse? + val (updatedHouseState, thermalHouseThreshold, remainingQDotHouse) = + handleInfeedHouse( + tick, + lastAmbientTemperature, + ambientTemperature, + state, + qDotHouse, + ) + + val (updatedStorageState, thermalStorageThreshold) = + handleInfeedStorage(tick, state, qDotHeatStorage) + + val nextThreshold = determineMostRecentThreshold( + thermalHouseThreshold, + thermalStorageThreshold, + ) + + ( + state.copy( + houseState = updatedHouseState, + storageState = updatedStorageState, + ), + nextThreshold, + ) + } + + /** Handles the case, when the house has heat demand and will be heated up + * here. + * + * @param tick + * Current tick + * @param lastAmbientTemperature + * Ambient temperature until this tick + * @param ambientTemperature + * actual ambient temperature + * @param state + * Current state of the houses + * @param qDot + * Infeed to the grid + * @return + * Updated thermal house state, a ThermalThreshold and the remaining qDot + */ + private def handleInfeedHouse( + tick: Long, + lastAmbientTemperature: Temperature, + ambientTemperature: Temperature, + state: ThermalGridState, + qDot: Power, + ): (Option[ThermalHouseState], Option[ThermalThreshold], Power) = { + (house, state.houseState) match { + case (Some(thermalHouse), Some(lastHouseState)) => + val (newState, threshold) = thermalHouse.determineState( + tick, + lastHouseState, + lastAmbientTemperature, + ambientTemperature, + qDot, + ) + /* Check if house can handle the thermal feed in */ if ( thermalHouse.isInnerTemperatureTooHigh( - updatedHouseState.innerTemperature + newState.innerTemperature ) ) { - /* The house is already heated up fully, set back the infeed and put it into storage, if available */ val (fullHouseState, maybeFullHouseThreshold) = thermalHouse.determineState( tick, @@ -239,54 +445,44 @@ final case class ThermalGrid( ambientTemperature, zeroKW, ) - storage.zip(updatedStorageState) match { - case Some((thermalStorage, storageState)) => - val (updatedStorageState, maybeStorageThreshold) = - thermalStorage.updateState(tick, qDot, storageState) - - /* Both house and storage are updated. Determine what reaches the next threshold */ - val nextThreshold = determineMostRecentThreshold( - maybeFullHouseThreshold, - maybeStorageThreshold, - ) - - ( - state.copy( - houseState = Some(fullHouseState), - storageState = Some(updatedStorageState), - ), - nextThreshold, - ) - case None => - /* There is no storage, house determines the next activation */ - ( - state.copy(houseState = Some(fullHouseState)), - maybeFullHouseThreshold, - ) - } + (Some(fullHouseState), maybeFullHouseThreshold, qDot) } else { - /* The house can handle the infeed */ - ( - state.copy(houseState = Some(updatedHouseState)), - maybeHouseThreshold, - ) + (Some(newState), threshold, zeroKW) } + case _ => (None, None, zeroKW) + } + } - case None => - storage.zip(state.storageState) match { - case Some((thermalStorage, storageState)) => - val (updatedStorageState, maybeStorageThreshold) = - thermalStorage.updateState(tick, qDot, storageState) - ( - state.copy(storageState = Some(updatedStorageState)), - maybeStorageThreshold, - ) - case None => - throw new InconsistentStateException( - "A thermal grid has to contain either at least a house or a storage." - ) - } + /** Handles the cases, when the storage has heat demand and will be filled up + * here (positive qDot) or will be return its stored energy into the thermal + * grid (negative qDot). + * @param tick + * Current tick + * @param ambientTemperature + * Ambient temperature + * @param state + * Current state of the houses + * @param qDot + * Infeed to the grid + * @return + * Updated thermal grid state + */ + private def handleInfeedStorage( + tick: Long, + state: ThermalGridState, + qDot: Power, + ): (Option[ThermalStorageState], Option[ThermalThreshold]) = { + (storage, state.storageState) match { + case (Some(thermalStorage), Some(lastStorageState)) => + val (newState, threshold) = thermalStorage.updateState( + tick, + qDot, + lastStorageState, + ) + (Some(newState), threshold) + case _ => (None, None) } + } private def determineMostRecentThreshold( maybeHouseThreshold: Option[ThermalThreshold], diff --git a/src/test/scala/edu/ie3/simona/agent/em/EmAgentIT.scala b/src/test/scala/edu/ie3/simona/agent/em/EmAgentIT.scala index 09f6acb019..be1cee377f 100644 --- a/src/test/scala/edu/ie3/simona/agent/em/EmAgentIT.scala +++ b/src/test/scala/edu/ie3/simona/agent/em/EmAgentIT.scala @@ -680,7 +680,7 @@ class EmAgentIT Celsius(0d), MetersPerSecond(0d), ), - Some(28800), + Some(56000), ) } @@ -692,26 +692,26 @@ class EmAgentIT emResult.getQ should equalWithTolerance(0.000088285537.asMegaVar) } - scheduler.expectMessage(Completion(emAgentActivation, Some(28665))) + scheduler.expectMessage(Completion(emAgentActivation, Some(44398))) - /* TICK 28666 + /* TICK 44398 LOAD: 0.000269 MW (unchanged) PV: -0.000032 MW (unchanged) Heat pump: Is turned on again and cannot be turned off -> flex signal is no control -> 0.00485 MW */ - emAgentActivation ! Activation(28665) + emAgentActivation ! Activation(44398) resultListener.expectMessageType[ParticipantResultEvent] match { case ParticipantResultEvent(emResult: EmResult) => emResult.getInputModel shouldBe emInput.getUuid - emResult.getTime shouldBe 28665.toDateTime - emResult.getP should equalWithTolerance(0.0050867679996.asMegaWatt) - emResult.getQ should equalWithTolerance(0.001073120040.asMegaVar) + emResult.getTime shouldBe 44398.toDateTime + emResult.getP should equalWithTolerance(0.005086768.asMegaWatt) + emResult.getQ should equalWithTolerance(0.00107312004.asMegaVar) } - scheduler.expectMessage(Completion(emAgentActivation, Some(28800))) + scheduler.expectMessage(Completion(emAgentActivation, Some(56000))) } } diff --git a/src/test/scala/edu/ie3/simona/model/thermal/ThermalGridTestData.scala b/src/test/scala/edu/ie3/simona/model/thermal/ThermalGridTestData.scala index 6011c6caf2..0a016d4566 100644 --- a/src/test/scala/edu/ie3/simona/model/thermal/ThermalGridTestData.scala +++ b/src/test/scala/edu/ie3/simona/model/thermal/ThermalGridTestData.scala @@ -25,4 +25,8 @@ trait ThermalGridTestData { protected val testGridQDotInfeed: Power = Kilowatts(15d) protected val testGridQDotConsumption: Power = Kilowatts(-42d) protected val testGridQDotConsumptionHigh: Power = Kilowatts(-200d) + protected val noThermalDemand: Boolean = false + protected val thermalDemand: Boolean = true + protected val isRunning: Boolean = true + protected val isNotRunning: Boolean = false } diff --git a/src/test/scala/edu/ie3/simona/model/thermal/ThermalGridWithHouseAndStorageSpec.scala b/src/test/scala/edu/ie3/simona/model/thermal/ThermalGridWithHouseAndStorageSpec.scala index 1ea45a656d..b2b732ae0e 100644 --- a/src/test/scala/edu/ie3/simona/model/thermal/ThermalGridWithHouseAndStorageSpec.scala +++ b/src/test/scala/edu/ie3/simona/model/thermal/ThermalGridWithHouseAndStorageSpec.scala @@ -496,7 +496,10 @@ class ThermalGridWithHouseAndStorageSpec testGridAmbientTemperature, testGridAmbientTemperature, initialGridState, + isNotRunning, externalQDot, + thermalDemand, + noThermalDemand, ) updatedGridState match { @@ -510,7 +513,7 @@ class ThermalGridWithHouseAndStorageSpec innerTemperature should approximate(Celsius(18.9999d)) qDotHouse should approximate(externalQDot) - storageTick shouldBe -1L + storageTick shouldBe 0L storedEnergy should approximate( initialGridState.storageState .map(_.storedEnergy) @@ -542,7 +545,10 @@ class ThermalGridWithHouseAndStorageSpec testGridAmbientTemperature, testGridAmbientTemperature, gridState, + isNotRunning, externalQDot, + noThermalDemand, + thermalDemand, ) updatedGridState match { diff --git a/src/test/scala/edu/ie3/simona/model/thermal/ThermalGridWithHouseOnlySpec.scala b/src/test/scala/edu/ie3/simona/model/thermal/ThermalGridWithHouseOnlySpec.scala index eb447d0fc4..4ecbfc819b 100644 --- a/src/test/scala/edu/ie3/simona/model/thermal/ThermalGridWithHouseOnlySpec.scala +++ b/src/test/scala/edu/ie3/simona/model/thermal/ThermalGridWithHouseOnlySpec.scala @@ -179,7 +179,10 @@ class ThermalGridWithHouseOnlySpec extends UnitSpec with ThermalHouseTestData { testGridAmbientTemperature, testGridAmbientTemperature, gridState, + isNotRunning, testGridQDotInfeed, + thermalDemand, + noThermalDemand, ) updatedGridState match { @@ -205,7 +208,10 @@ class ThermalGridWithHouseOnlySpec extends UnitSpec with ThermalHouseTestData { ThermalGrid.startingState(thermalGrid), testGridAmbientTemperature, testGridAmbientTemperature, + isRunning, testGridQDotInfeed, + thermalDemand, + noThermalDemand, ) match { case ( ThermalGridState( @@ -228,7 +234,10 @@ class ThermalGridWithHouseOnlySpec extends UnitSpec with ThermalHouseTestData { ThermalGrid.startingState(thermalGrid), testGridAmbientTemperature, testGridAmbientTemperature, + isNotRunning, testGridQDotConsumption, + thermalDemand, + noThermalDemand, ) match { case ( ThermalGridState( @@ -251,7 +260,10 @@ class ThermalGridWithHouseOnlySpec extends UnitSpec with ThermalHouseTestData { ThermalGrid.startingState(thermalGrid), testGridAmbientTemperature, testGridAmbientTemperature, + isNotRunning, Megawatts(0d), + thermalDemand, + noThermalDemand, ) match { case ( ThermalGridState( diff --git a/src/test/scala/edu/ie3/simona/model/thermal/ThermalGridWithStorageOnlySpec.scala b/src/test/scala/edu/ie3/simona/model/thermal/ThermalGridWithStorageOnlySpec.scala index 93a45e3180..c1ded4c6dc 100644 --- a/src/test/scala/edu/ie3/simona/model/thermal/ThermalGridWithStorageOnlySpec.scala +++ b/src/test/scala/edu/ie3/simona/model/thermal/ThermalGridWithStorageOnlySpec.scala @@ -182,7 +182,10 @@ class ThermalGridWithStorageOnlySpec testGridAmbientTemperature, testGridAmbientTemperature, gridState, + isNotRunning, testGridQDotInfeed, + noThermalDemand, + thermalDemand, ) updatedGridState match { @@ -206,7 +209,10 @@ class ThermalGridWithStorageOnlySpec ThermalGrid.startingState(thermalGrid), testGridAmbientTemperature, testGridAmbientTemperature, + isRunning, testGridQDotInfeed, + noThermalDemand, + thermalDemand, ) nextThreshold shouldBe Some(StorageFull(276000L)) @@ -239,7 +245,10 @@ class ThermalGridWithStorageOnlySpec ), testGridAmbientTemperature, testGridAmbientTemperature, + isRunning, testGridQDotConsumptionHigh, + thermalDemand, + noThermalDemand, ) match { case ( ThermalGridState( @@ -262,7 +271,10 @@ class ThermalGridWithStorageOnlySpec ThermalGrid.startingState(thermalGrid), testGridAmbientTemperature, testGridAmbientTemperature, + isRunning, Kilowatts(0d), + noThermalDemand, + noThermalDemand, ) updatedState match { case ( diff --git a/src/test/scala/edu/ie3/simona/test/common/input/EmInputTestData.scala b/src/test/scala/edu/ie3/simona/test/common/input/EmInputTestData.scala index c5f3cf8efe..9c5abd30de 100644 --- a/src/test/scala/edu/ie3/simona/test/common/input/EmInputTestData.scala +++ b/src/test/scala/edu/ie3/simona/test/common/input/EmInputTestData.scala @@ -140,7 +140,7 @@ trait EmInputTestData UUID.fromString("91940626-bdd0-41cf-96dd-47c94c86b20e"), "thermal house", thermalBusInput, - Quantities.getQuantity(0.325, StandardUnits.THERMAL_TRANSMISSION), + Quantities.getQuantity(0.15, StandardUnits.THERMAL_TRANSMISSION), Quantities.getQuantity(75, StandardUnits.HEAT_CAPACITY), Quantities.getQuantity(20.3, StandardUnits.TEMPERATURE), Quantities.getQuantity(22.0, StandardUnits.TEMPERATURE),