From 6e9b40f1c004020a54500edc5dccc408e88c318b Mon Sep 17 00:00:00 2001 From: danielfeismann Date: Sat, 24 Aug 2024 09:56:41 +0200 Subject: [PATCH 1/2] Refactoring of ThermalGrid.energyGrid to distinguish between demand of house and storage --- CHANGELOG.md | 1 + .../participant/hp/HpAgentFundamentals.scala | 6 +- .../simona/model/participant/HpModel.scala | 148 +++-- .../simona/model/thermal/ThermalGrid.scala | 105 +++- .../model/participant/HpModelSpec.scala | 537 ++++++++++++++---- .../ThermalGridWithHouseAndStorageSpec.scala | 72 ++- .../ThermalGridWithHouseOnlySpec.scala | 32 +- .../ThermalGridWithStorageOnlySpec.scala | 51 +- 8 files changed, 693 insertions(+), 259 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 91b25a5dbf..b1de2d3306 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -78,6 +78,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Updated `Gradle` to version V8.10 [#829](https://github.com/ie3-institute/simona/issues/829) - Updated AUTHORS.md [#905](https://github.com/ie3-institute/simona/issues/905) - Prepare ThermalStorageTestData for Storage without storageVolumeLvlMin [#894](https://github.com/ie3-institute/simona/issues/894) +- Refactoring of `ThermalGrid.energyGrid` to distinguish between demand of house and storage [#928](https://github.com/ie3-institute/simona/issues/928) ### Fixed - Removed a repeated line in the documentation of vn_simona config [#658](https://github.com/ie3-institute/simona/issues/658) diff --git a/src/main/scala/edu/ie3/simona/agent/participant/hp/HpAgentFundamentals.scala b/src/main/scala/edu/ie3/simona/agent/participant/hp/HpAgentFundamentals.scala index 9add5d8ddc..7940120b4c 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant/hp/HpAgentFundamentals.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant/hp/HpAgentFundamentals.scala @@ -293,7 +293,11 @@ trait HpAgentFundamentals calcRelevantData: HpRelevantData, nodalVoltage: squants.Dimensionless, model: HpModel, - ): HpState = model.determineState(modelState, calcRelevantData) + ): HpState = { + val (_, _, state) = + model.determineState(modelState, calcRelevantData) + state + } /** Abstract definition, individual implementations found in individual agent * fundamental classes 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 4d9fc68899..ad512a75e0 100644 --- a/src/main/scala/edu/ie3/simona/model/participant/HpModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant/HpModel.scala @@ -11,16 +11,15 @@ import edu.ie3.simona.agent.participant.data.Data.PrimaryData.ApparentPowerAndHe import edu.ie3.simona.model.SystemComponent import edu.ie3.simona.model.participant.HpModel.{HpRelevantData, HpState} import edu.ie3.simona.model.participant.control.QControl -import edu.ie3.simona.model.thermal.ThermalGrid.ThermalGridState +import edu.ie3.simona.model.thermal.ThermalGrid.{ThermalEnergyDemand, ThermalGridState} import edu.ie3.simona.model.thermal.{ThermalGrid, ThermalThreshold} import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage.ProvideFlexOptions import edu.ie3.simona.ontology.messages.flex.MinMaxFlexibilityMessage.ProvideMinMaxFlexOptions import edu.ie3.util.quantities.PowerSystemUnits import edu.ie3.util.scala.OperationInterval -import edu.ie3.util.scala.quantities.DefaultQuantities import edu.ie3.util.scala.quantities.DefaultQuantities._ -import squants.energy.Kilowatts -import squants.{Power, Temperature} +import squants.energy.{KilowattHours, Kilowatts} +import squants.{Energy, Power, Temperature} import java.time.ZonedDateTime import java.util.UUID @@ -116,19 +115,23 @@ final case class HpModel( * function calculates the heat pump's next state to get the actual active * power of this state use [[calculateActivePower]] with the generated state * - * @param state - * Current state of the heat pump + * @param lastState + * Last state of the heat pump * @param relevantData * data of heat pump including * @return - * next [[HpState]] + * Booleans if Hp can operate and can be out of operation plus next + * [[HpState]] */ def determineState( - state: HpState, + lastState: HpState, relevantData: HpRelevantData, - ): HpState = { - val turnOn = operatesInNextState(state, relevantData) - calcState(state, relevantData, turnOn) + ): (Boolean, Boolean, HpState) = { + val (turnOn, canOperate, canBeOutOfOperation, houseDemand, storageDemand) = + operatesInNextState(lastState, relevantData) + val updatedState = + calcState(lastState, relevantData, turnOn, houseDemand, storageDemand) + (canOperate, canBeOutOfOperation, updatedState) } /** Depending on the input, this function decides whether the heat pump will @@ -142,18 +145,63 @@ final case class HpModel( * @param relevantData * Relevant (external) data * @return - * boolean defining if heat pump runs in next time step + * boolean defining if heat pump runs in next time step, if it can be in + * operation and out of operation plus the demand of house and storage */ private def operatesInNextState( state: HpState, relevantData: HpRelevantData, - ): Boolean = { - val demand = thermalGrid.energyDemand( - relevantData.currentTick, - relevantData.ambientTemperature, - state.thermalGridState, + ): (Boolean, Boolean, Boolean, Boolean, Boolean) = { + val (demandHouse, demandThermalStorage, updatedState) = + thermalGrid.energyDemandAndUpdatedState( + relevantData.currentTick, + state.ambientTemperature.getOrElse(relevantData.ambientTemperature), + relevantData.ambientTemperature, + state.thermalGridState, + ) + + val ( + houseDemand, + heatStorageDemand, + noThermalStorageOrThermalStorageIsEmpty, + ) = determineDemandBooleans( + state, + updatedState, + demandHouse, + demandThermalStorage, ) - demand.hasRequiredDemand || (state.isRunning && demand.hasAdditionalDemand) + + val turnHpOn: Boolean = + houseDemand || heatStorageDemand + + val canOperate = + demandHouse.hasRequiredDemand || demandHouse.hasAdditionalDemand || + demandThermalStorage.hasRequiredDemand || demandThermalStorage.hasAdditionalDemand + val canBeOutOfOperation = + !(demandHouse.hasRequiredDemand && noThermalStorageOrThermalStorageIsEmpty) + + (turnHpOn, canOperate, canBeOutOfOperation, houseDemand, heatStorageDemand) + } + + def determineDemandBooleans( + hpState: HpState, + updatedGridState: ThermalGridState, + demandHouse: ThermalEnergyDemand, + demandThermalStorage: ThermalEnergyDemand, + ): (Boolean, Boolean, Boolean) = { + implicit val tolerance: Energy = KilowattHours(1e-3) + val noThermalStorageOrThermalStorageIsEmpty: Boolean = + updatedGridState.storageState.isEmpty || updatedGridState.storageState + .exists( + _.storedEnergy =~ zeroKWH + ) + + val houseDemand = + (demandHouse.hasRequiredDemand && noThermalStorageOrThermalStorageIsEmpty) || (hpState.isRunning && demandHouse.hasAdditionalDemand) + val heatStorageDemand = { + (demandThermalStorage.hasRequiredDemand) || (hpState.isRunning && demandThermalStorage.hasAdditionalDemand) + } + (houseDemand, heatStorageDemand, noThermalStorageOrThermalStorageIsEmpty) } /** Calculate state depending on whether heat pump is needed or not. Also @@ -173,11 +221,19 @@ final case class HpModel( state: HpState, relevantData: HpRelevantData, isRunning: Boolean, + houseDemand: Boolean, + storageDemand: Boolean, ): HpState = { + val lastStateStorageqDot = state.thermalGridState.storageState + .map(_.qDot) + .getOrElse(zeroKW) + val (newActivePower, newThermalPower) = if (isRunning) (pRated, pThermal) - else (DefaultQuantities.zeroKW, DefaultQuantities.zeroKW) + else if (lastStateStorageqDot < zeroKW) + (zeroKW, lastStateStorageqDot * (-1)) + else (zeroKW, zeroKW) /* Push thermal energy to the thermal grid and get its updated state in return */ val (thermalGridState, maybeThreshold) = @@ -205,23 +261,14 @@ final case class HpModel( lastState: HpState, ): ProvideFlexOptions = { /* Determine the operating state in the given tick */ - val updatedState = determineState(lastState, data) - - /* Determine the options we have */ - val thermalEnergyDemand = thermalGrid.energyDemand( - data.currentTick, - data.ambientTemperature, - lastState.thermalGridState, - ) - val canOperate = - thermalEnergyDemand.hasRequiredDemand || thermalEnergyDemand.hasAdditionalDemand - val canBeOutOfOperation = !thermalEnergyDemand.hasRequiredDemand + val (canOperate, canBeOutOfOperation, updatedHpState) + : (Boolean, Boolean, HpState) = determineState(lastState, data) val lowerBoundary = if (canBeOutOfOperation) zeroKW else - updatedState.activePower + updatedHpState.activePower val upperBoundary = if (canOperate) sRated * cosPhiRated @@ -230,7 +277,7 @@ final case class HpModel( ProvideMinMaxFlexOptions( uuid, - updatedState.activePower, + updatedHpState.activePower, lowerBoundary, upperBoundary, ) @@ -260,13 +307,44 @@ final case class HpModel( ): (HpState, FlexChangeIndicator) = { /* If the setpoint value is above 50 % of the electrical power, turn on the heat pump otherwise turn it off */ val turnOn = setPower > (sRated * cosPhiRated * 0.5) - val updatedState = calcState(lastState, data, turnOn) + + val ( + thermalEnergyDemandHouse, + thermalEnergyDemandStorage, + updatedThermalGridState, + ) = + thermalGrid.energyDemandAndUpdatedState( + data.currentTick, + lastState.ambientTemperature.getOrElse(data.ambientTemperature), + data.ambientTemperature, + lastState.thermalGridState, + ) + + val ( + houseDemand, + heatStorageDemand, + noThermalStorageOrThermalStorageIsEmpty, + ) = determineDemandBooleans( + lastState, + updatedThermalGridState, + thermalEnergyDemandHouse, + thermalEnergyDemandStorage, + ) + + val updatedHpState: HpState = + calcState( + lastState, + data, + turnOn, + houseDemand, + heatStorageDemand, + ) ( - updatedState, + updatedHpState, FlexChangeIndicator( changesAtNextActivation = true, - updatedState.maybeThermalThreshold.map(_.tick), + updatedHpState.maybeThermalThreshold.map(_.tick), ), ) } 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 286b0e829f..3e983b6abe 100644 --- a/src/main/scala/edu/ie3/simona/model/thermal/ThermalGrid.scala +++ b/src/main/scala/edu/ie3/simona/model/thermal/ThermalGrid.scala @@ -43,60 +43,105 @@ final case class ThermalGrid( ) extends LazyLogging { /** Determine the energy demand of the total grid at the given instance in - * time + * time and returns it including the updatedState + * * @param tick * Questioned instance in time + * @param lastAmbientTemperature + * Ambient temperature until this tick * @param ambientTemperature * Ambient temperature in the instance in question * @param state * Currently applicable state of the thermal grid * @return - * The total energy demand of the grid + * The total energy demand of the house and the storage and an updated + * [[ThermalGridState]] */ - def energyDemand( + def energyDemandAndUpdatedState( tick: Long, + // FIXME this is also in state + lastAmbientTemperature: Temperature, ambientTemperature: Temperature, state: ThermalGridState, - ): ThermalEnergyDemand = { - /* First get the energy demand of the houses */ - val houseDemand = house - .zip(state.houseState) - .map { case (house, state) => - house.energyDemand( - tick, - ambientTemperature, - state, - ) + ): (ThermalEnergyDemand, ThermalEnergyDemand, ThermalGridState) = { + /* First get the energy demand of the houses but only if inner temperature is below target temperature */ + + val (houseDemand, updatedHouseState) = + house.zip(state.houseState).headOption match { + case Some((thermalHouse, lastHouseState)) => + val (updatedHouseState, updatedStorageState) = + thermalHouse.determineState( + tick, + lastHouseState, + lastAmbientTemperature, + ambientTemperature, + lastHouseState.qDot, + ) + if ( + updatedHouseState.innerTemperature < thermalHouse.targetTemperature | (lastHouseState.qDot > zeroKW && updatedHouseState.innerTemperature < thermalHouse.upperBoundaryTemperature) + ) { + ( + thermalHouse.energyDemand( + tick, + ambientTemperature, + updatedHouseState, + ), + Some(updatedHouseState), + ) + + } else { + (ThermalEnergyDemand.noDemand, Some(updatedHouseState)) + } + + case None => + (ThermalEnergyDemand.noDemand, None) } - .getOrElse(ThermalEnergyDemand.noDemand) /* Then go over the storages, see what they can provide and what they might be able to charge */ - val (storedEnergy, remainingCapacity) = { + val (storageDemand, updatedStorageState) = { + storage .zip(state.storageState) .map { case (storage, state) => - val usableEnergy = state.storedEnergy - val remaining = storage.getMaxEnergyThreshold - usableEnergy + val (updatedStorageState, _) = + storage.updateState(tick, state.qDot, state) + val storedEnergy = updatedStorageState.storedEnergy + val soc = storedEnergy / storage.getMaxEnergyThreshold + val storageRequired = { + if (soc == 0d) { + storage.getMaxEnergyThreshold - storedEnergy + + } else { + zeroMWH + } + } + + val storagePossible = storage.getMaxEnergyThreshold - storedEnergy ( - usableEnergy, - remaining, + ThermalEnergyDemand( + storageRequired, + storagePossible, + ), + Some(updatedStorageState), ) + } .getOrElse( - (zeroMWH, zeroMWH) + ThermalEnergyDemand(zeroMWH, zeroMWH), + None, ) } - val usedEnergy = - if (storedEnergy >= houseDemand.required) - houseDemand.required - else - storedEnergy - val finallyRemaining = remainingCapacity + usedEnergy - - ThermalEnergyDemand( - houseDemand.required - usedEnergy, - houseDemand.possible + finallyRemaining, + ( + ThermalEnergyDemand( + houseDemand.required, + houseDemand.possible, + ), + ThermalEnergyDemand( + storageDemand.required, + storageDemand.possible, + ), + ThermalGridState(updatedHouseState, updatedStorageState), ) } diff --git a/src/test/scala/edu/ie3/simona/model/participant/HpModelSpec.scala b/src/test/scala/edu/ie3/simona/model/participant/HpModelSpec.scala index e99a5c8c26..81c62e549d 100644 --- a/src/test/scala/edu/ie3/simona/model/participant/HpModelSpec.scala +++ b/src/test/scala/edu/ie3/simona/model/participant/HpModelSpec.scala @@ -208,14 +208,18 @@ class HpModelSpec val hp = hpModel(grid) hp.determineState(state, data) match { - case HpState( - isRunning, + case ( _, _, - activePower, - _, - ThermalGridState(Some(thermalHouseState), _), - maybeThreshold, + HpState( + isRunning, + _, + _, + activePower, + _, + ThermalGridState(Some(thermalHouseState), _), + maybeThreshold, + ), ) => isRunning shouldBe expectedRunningState activePower should approximate(Kilowatts(expectedActivePower)) @@ -234,166 +238,447 @@ class HpModelSpec "determining the flexibility options for different states" should { "deliver correct flexibility options" in { - val testCases = Table( - ("thermalState", "lastState", "expectedValues"), - // House is below lower temperature boundary - ( - ThermalGridState( - Some(ThermalHouseState(0L, Celsius(15), Kilowatts(0))), - Some(ThermalStorageState(0L, KilowattHours(20), Kilowatts(0))), + val testCases = + Table( + ("thermalState", "lastState", "expectedValues"), + // 1. Hp actually not running + // House is below lower temperature boundary + // Heat storage is empty + // hp must be turned on + ( + ThermalGridState( + Some(ThermalHouseState(0L, Celsius(15), Kilowatts(0))), + Some(ThermalStorageState(0L, KilowattHours(0), Kilowatts(0))), + ), + HpState( + isRunning = false, + 0, + Some(hpData.ambientTemperature), + Kilowatts(0.0), + Kilowatts(0.0), + ThermalGridState( + Some(ThermalHouseState(0L, Celsius(15), Kilowatts(0))), + Some(ThermalStorageState(0L, KilowattHours(0), Kilowatts(0))), + ), + None, + ), + (95.0, 95.0, 95.0), ), - HpState( - isRunning = false, - 0, - Some(hpData.ambientTemperature), - Kilowatts(0.0), - Kilowatts(0.0), + // 2. Same as before but heat storage is NOT empty + // should be possible to keep hp off + ( ThermalGridState( Some(ThermalHouseState(0L, Celsius(15), Kilowatts(0))), - Some( - ThermalStorageState(0L, KilowattHours(20), Kilowatts(0)) + Some(ThermalStorageState(0L, KilowattHours(20), Kilowatts(0))), + ), + HpState( + isRunning = false, + 0, + Some(hpData.ambientTemperature), + Kilowatts(0.0), + Kilowatts(0.0), + ThermalGridState( + Some(ThermalHouseState(0L, Celsius(15), Kilowatts(0))), + Some( + ThermalStorageState(0L, KilowattHours(20), Kilowatts(0)) + ), ), + None, ), - None, + (0.0, 0.0, 95.0), ), - (95.0, 95.0, 95.0), - ), - // House is between target temperature and lower temperature boundary, Hp actually running - ( - ThermalGridState( - Some(ThermalHouseState(0L, Celsius(19), Kilowatts(0))), - Some(ThermalStorageState(0L, KilowattHours(20), Kilowatts(0))), + // 3. Hp actually running + // House is below lower temperature boundary + // Heat storage is empty + // Hp must run because of house and storage + ( + ThermalGridState( + Some(ThermalHouseState(0L, Celsius(15), Kilowatts(0))), + Some(ThermalStorageState(0L, KilowattHours(0), Kilowatts(0))), + ), + HpState( + isRunning = true, + 0, + Some(hpData.ambientTemperature), + Kilowatts(0.0), + Kilowatts(0.0), + ThermalGridState( + Some(ThermalHouseState(0L, Celsius(15), Kilowatts(0))), + Some( + ThermalStorageState(0L, KilowattHours(0), Kilowatts(0)) + ), + ), + None, + ), + (95.0, 95.0, 95.0), ), - HpState( - isRunning = true, - 0, - Some(hpData.ambientTemperature), - Kilowatts(95.0), - Kilowatts(80.0), + // 4. Same as before but heat storage is NOT empty + // Hp should not run because of storage but can be turned on + ( ThermalGridState( - Some(ThermalHouseState(0L, Celsius(19), Kilowatts(80))), - Some( - ThermalStorageState(0L, KilowattHours(20), Kilowatts(0)) + Some(ThermalHouseState(0L, Celsius(15), Kilowatts(0))), + Some(ThermalStorageState(0L, KilowattHours(20), Kilowatts(0))), + ), + HpState( + isRunning = false, + 0, + Some(hpData.ambientTemperature), + Kilowatts(0.0), + Kilowatts(0.0), + ThermalGridState( + Some(ThermalHouseState(0L, Celsius(15), Kilowatts(0))), + Some( + ThermalStorageState(0L, KilowattHours(20), Kilowatts(0)) + ), ), + None, ), - None, + (0.0, 0.0, 95.0), ), - (95.0, 0.0, 95.0), - ), - - // House is between target temperature and lower temperature boundary, Hp actually not running - ( - ThermalGridState( - Some(ThermalHouseState(0L, Celsius(19), Kilowatts(0))), - Some(ThermalStorageState(0L, KilowattHours(20), Kilowatts(0))), + // 5. Hp actually running + // House is between target temperature and lower temperature boundary + // Heat storage is empty + // Hp runs but can be turned off + ( + ThermalGridState( + Some(ThermalHouseState(0L, Celsius(19), Kilowatts(0))), + Some(ThermalStorageState(0L, KilowattHours(0), Kilowatts(0))), + ), + HpState( + isRunning = true, + 0, + Some(hpData.ambientTemperature), + Kilowatts(95.0), + Kilowatts(80.0), + ThermalGridState( + Some(ThermalHouseState(0L, Celsius(19), Kilowatts(0))), + Some( + ThermalStorageState(0L, KilowattHours(0), Kilowatts(0)) + ), + ), + None, + ), + (95.0, 0.0, 95.0), ), - HpState( - isRunning = false, - 0, - Some(hpData.ambientTemperature), - Kilowatts(0.0), - Kilowatts(0.0), + // 6. Same as before but heat storage is NOT empty + // should be possible to keep hp off + ( ThermalGridState( Some(ThermalHouseState(0L, Celsius(19), Kilowatts(0))), - Some( - ThermalStorageState(0L, KilowattHours(20), Kilowatts(0)) + Some(ThermalStorageState(0L, KilowattHours(20), Kilowatts(0))), + ), + HpState( + isRunning = true, + 0, + Some(hpData.ambientTemperature), + Kilowatts(95.0), + Kilowatts(80.0), + ThermalGridState( + Some(ThermalHouseState(0L, Celsius(19), Kilowatts(0))), + Some( + ThermalStorageState(0L, KilowattHours(20), Kilowatts(0)) + ), ), + None, ), - None, + (95.0, 0.0, 95.0), ), - (0.0, 0.0, 95.0), - ), - // Storage and house have remaining capacity - ( - ThermalGridState( - Some(ThermalHouseState(0L, Celsius(21), Kilowatts(80))), - Some(ThermalStorageState(0L, KilowattHours(20), Kilowatts(0))), + // 7. Hp actually NOT running + // House is between target temperature and lower temperature boundary + // Heat storage is empty + // Hp should run because of storage but can be turned off + ( + ThermalGridState( + Some(ThermalHouseState(0L, Celsius(19), Kilowatts(0))), + Some(ThermalStorageState(0L, KilowattHours(0), Kilowatts(0))), + ), + HpState( + isRunning = false, + 0, + Some(hpData.ambientTemperature), + Kilowatts(95.0), + Kilowatts(80.0), + ThermalGridState( + Some(ThermalHouseState(0L, Celsius(19), Kilowatts(0))), + Some( + ThermalStorageState(0L, KilowattHours(0), Kilowatts(0)) + ), + ), + None, + ), + (95.0, 0.0, 95.0), ), - HpState( - isRunning = true, - 0, - Some(hpData.ambientTemperature), - Kilowatts(95.0), - Kilowatts(80.0), + // 8. Same as before but heat storage is NOT empty + // Hp should be off but able to turn on + ( ThermalGridState( - Some(ThermalHouseState(0L, Celsius(21), Kilowatts(80))), + Some(ThermalHouseState(0L, Celsius(19), Kilowatts(0))), Some(ThermalStorageState(0L, KilowattHours(20), Kilowatts(0))), ), - Some(HouseTemperatureUpperBoundaryReached(0L)), + HpState( + isRunning = false, + 0, + Some(hpData.ambientTemperature), + Kilowatts(95.0), + Kilowatts(80.0), + ThermalGridState( + Some(ThermalHouseState(0L, Celsius(19), Kilowatts(0))), + Some( + ThermalStorageState(0L, KilowattHours(20), Kilowatts(0)) + ), + ), + None, + ), + (0.0, 0.0, 95.0), ), - (95.0, 0.0, 95.0), - ), - - // Storage is full, House has capacity till upper boundary - ( - ThermalGridState( - Some(ThermalHouseState(0L, Celsius(21), Kilowatts(80))), - Some(ThermalStorageState(0L, KilowattHours(500), Kilowatts(0))), + // 9. Hp actually running + // House is between target temperature and upper temperature boundary + // Heat storage is empty + // Hp will run because of storage but can be turned off + ( + ThermalGridState( + Some(ThermalHouseState(0L, Celsius(21), Kilowatts(0))), + Some(ThermalStorageState(0L, KilowattHours(0), Kilowatts(0))), + ), + HpState( + isRunning = true, + 0, + Some(hpData.ambientTemperature), + Kilowatts(95.0), + Kilowatts(80.0), + ThermalGridState( + Some(ThermalHouseState(0L, Celsius(21), Kilowatts(0))), + Some(ThermalStorageState(0L, KilowattHours(0), Kilowatts(0))), + ), + Some(HouseTemperatureUpperBoundaryReached(0L)), + ), + (95.0, 0.0, 95.0), ), - HpState( - isRunning = false, - 0, - Some(hpData.ambientTemperature), - Kilowatts(0.0), - Kilowatts(0.0), + // 10. Same as before but storage is NOT empty + // Hp should run but can be turned off + ( + ThermalGridState( + Some(ThermalHouseState(0L, Celsius(21), Kilowatts(0))), + Some(ThermalStorageState(0L, KilowattHours(20), Kilowatts(0))), + ), + HpState( + isRunning = true, + 0, + Some(hpData.ambientTemperature), + Kilowatts(95.0), + Kilowatts(80.0), + ThermalGridState( + Some(ThermalHouseState(0L, Celsius(21), Kilowatts(0))), + Some(ThermalStorageState(0L, KilowattHours(20), Kilowatts(0))), + ), + Some(HouseTemperatureUpperBoundaryReached(0L)), + ), + (95.0, 0.0, 95.0), + ), + // 11. Hp actually not running + // House is between target temperature and upper temperature boundary + // Heat storage is empty + // Hp should run because of storage but can be turned off + ( ThermalGridState( - Some(ThermalHouseState(0L, Celsius(21), Kilowatts(80))), - Some( - ThermalStorageState(0L, KilowattHours(500), Kilowatts(0)) + Some(ThermalHouseState(0L, Celsius(21), Kilowatts(0))), + Some(ThermalStorageState(0L, KilowattHours(0), Kilowatts(0))), + ), + HpState( + isRunning = false, + 0, + Some(hpData.ambientTemperature), + Kilowatts(95.0), + Kilowatts(80.0), + ThermalGridState( + Some(ThermalHouseState(0L, Celsius(21), Kilowatts(0))), + Some(ThermalStorageState(0L, KilowattHours(0), Kilowatts(0))), ), + Some(HouseTemperatureUpperBoundaryReached(0L)), ), - Some(HouseTemperatureUpperBoundaryReached(0L)), + (95.0, 0.0, 95.0), + ), + // 12. Same as before but storage is NOT empty + // Hp should not run but can be turned on for storage or house + ( + ThermalGridState( + Some(ThermalHouseState(0L, Celsius(21), Kilowatts(0))), + Some(ThermalStorageState(0L, KilowattHours(20), Kilowatts(0))), + ), + HpState( + isRunning = false, + 0, + Some(hpData.ambientTemperature), + Kilowatts(95.0), + Kilowatts(80.0), + ThermalGridState( + Some(ThermalHouseState(0L, Celsius(21), Kilowatts(0))), + Some(ThermalStorageState(0L, KilowattHours(20), Kilowatts(0))), + ), + Some(HouseTemperatureUpperBoundaryReached(0L)), + ), + (0.0, 0.0, 95.0), ), - (0.0, 0.0, 95.0), - ), - // No capacity for flexibility at all because house is - // at upperTempBoundary and storage is at max capacity - ( - ThermalGridState( - Some(ThermalHouseState(0L, Celsius(22), Kilowatts(80))), - Some(ThermalStorageState(0L, KilowattHours(500), Kilowatts(0))), + // 13. Hp actually running + // House is at upper temperature boundary + // Heat storage is empty + // Hp should run because of storage but can be turned off + ( + ThermalGridState( + Some(ThermalHouseState(0L, Celsius(22), Kilowatts(0))), + Some(ThermalStorageState(0L, KilowattHours(0), Kilowatts(0))), + ), + HpState( + isRunning = true, + 0, + Some(hpData.ambientTemperature), + Kilowatts(95.0), + Kilowatts(80.0), + ThermalGridState( + Some(ThermalHouseState(0L, Celsius(21), Kilowatts(0))), + Some(ThermalStorageState(0L, KilowattHours(0), Kilowatts(0))), + ), + Some(HouseTemperatureUpperBoundaryReached(0L)), + ), + (95.0, 0.0, 95.0), ), - HpState( - isRunning = true, - 0, - Some(hpData.ambientTemperature), - Kilowatts(95.0), - Kilowatts(80.0), + // 14. Same as before but storage is NOT empty + // Hp should run but can be turned off + ( ThermalGridState( - Some(ThermalHouseState(0L, Celsius(22), Kilowatts(80))), - Some( - ThermalStorageState(0L, KilowattHours(500), Kilowatts(0)) + Some(ThermalHouseState(0L, Celsius(22), Kilowatts(0))), + Some(ThermalStorageState(0L, KilowattHours(20), Kilowatts(0))), + ), + HpState( + isRunning = true, + 0, + Some(hpData.ambientTemperature), + Kilowatts(95.0), + Kilowatts(80.0), + ThermalGridState( + Some(ThermalHouseState(0L, Celsius(21), Kilowatts(0))), + Some(ThermalStorageState(0L, KilowattHours(20), Kilowatts(0))), ), + Some(HouseTemperatureUpperBoundaryReached(0L)), ), - Some(HouseTemperatureUpperBoundaryReached(0L)), + (95.0, 0.0, 95.0), + ), + // 15. Hp actually not running + // House is at upper temperature boundary + // Heat storage is empty + // Hp should run because of storage but can be turned off + ( + ThermalGridState( + Some(ThermalHouseState(0L, Celsius(22), Kilowatts(0))), + Some(ThermalStorageState(0L, KilowattHours(0), Kilowatts(0))), + ), + HpState( + isRunning = false, + 0, + Some(hpData.ambientTemperature), + Kilowatts(95.0), + Kilowatts(80.0), + ThermalGridState( + Some(ThermalHouseState(0L, Celsius(21), Kilowatts(0))), + Some(ThermalStorageState(0L, KilowattHours(0), Kilowatts(0))), + ), + Some(HouseTemperatureUpperBoundaryReached(0L)), + ), + (95.0, 0.0, 95.0), ), - (0.0, 0.0, 0.0), - ), - // No capacity for flexibility at all when storage is full and house has been (externally) heated up above upperTemperatureBoundary - ( - ThermalGridState( - Some(ThermalHouseState(0L, Celsius(25), Kilowatts(0))), - Some(ThermalStorageState(0L, KilowattHours(500), Kilowatts(0))), + // 16. Same as before but storage is NOT empty + // Hp should not run but can be turned on for storage + ( + ThermalGridState( + Some(ThermalHouseState(0L, Celsius(22), Kilowatts(0))), + Some(ThermalStorageState(0L, KilowattHours(20), Kilowatts(0))), + ), + HpState( + isRunning = false, + 0, + Some(hpData.ambientTemperature), + Kilowatts(95.0), + Kilowatts(80.0), + ThermalGridState( + Some(ThermalHouseState(0L, Celsius(21), Kilowatts(0))), + Some(ThermalStorageState(0L, KilowattHours(20), Kilowatts(0))), + ), + Some(HouseTemperatureUpperBoundaryReached(0L)), + ), + (0.0, 0.0, 95.0), ), - HpState( - isRunning = false, - 0, - Some(hpData.ambientTemperature), - Kilowatts(95.0), - Kilowatts(80.0), + + // Storage is full, House has capacity till upper boundary + ( + ThermalGridState( + Some(ThermalHouseState(0L, Celsius(21), Kilowatts(0))), + Some(ThermalStorageState(0L, KilowattHours(500), Kilowatts(0))), + ), + HpState( + isRunning = false, + 0, + Some(hpData.ambientTemperature), + Kilowatts(0.0), + Kilowatts(0.0), + ThermalGridState( + Some(ThermalHouseState(0L, Celsius(19), Kilowatts(0))), + Some( + ThermalStorageState(0L, KilowattHours(500), Kilowatts(0)) + ), + ), + Some(HouseTemperatureUpperBoundaryReached(0L)), + ), + (0.0, 0.0, 95.0), + ), + // No capacity for flexibility at all because house is + // at upperTempBoundary and storage is at max capacity + ( + ThermalGridState( + Some(ThermalHouseState(0L, Celsius(22), Kilowatts(0))), + Some(ThermalStorageState(0L, KilowattHours(500), Kilowatts(0))), + ), + HpState( + isRunning = true, + 0, + Some(hpData.ambientTemperature), + Kilowatts(95.0), + Kilowatts(80.0), + ThermalGridState( + Some(ThermalHouseState(0L, Celsius(22), Kilowatts(0))), + Some( + ThermalStorageState(0L, KilowattHours(500), Kilowatts(0)) + ), + ), + Some(HouseTemperatureUpperBoundaryReached(0L)), + ), + (0.0, 0.0, 0.0), + ), + + // No capacity for flexibility at all when storage is full and house has been (externally) heated up above upperTemperatureBoundary + ( ThermalGridState( Some(ThermalHouseState(0L, Celsius(25), Kilowatts(0))), - Some( - ThermalStorageState(0L, KilowattHours(500), Kilowatts(0)) + Some(ThermalStorageState(0L, KilowattHours(500), Kilowatts(0))), + ), + HpState( + isRunning = false, + 0, + Some(hpData.ambientTemperature), + Kilowatts(95.0), + Kilowatts(80.0), + ThermalGridState( + Some(ThermalHouseState(0L, Celsius(25), Kilowatts(0))), + Some( + ThermalStorageState(0L, KilowattHours(500), Kilowatts(0)) + ), ), + None, ), - None, + (0.0, 0.0, 0.0), ), - (0.0, 0.0, 0.0), - ), - ) + ) // Run the test cases forAll(testCases) { 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 5d4fe69d88..1ea45a656d 100644 --- a/src/test/scala/edu/ie3/simona/model/thermal/ThermalGridWithHouseAndStorageSpec.scala +++ b/src/test/scala/edu/ie3/simona/model/thermal/ThermalGridWithHouseAndStorageSpec.scala @@ -96,53 +96,51 @@ class ThermalGridWithHouseAndStorageSpec "deliver the house demand (no demand) with added flexibility by storage" in { val tick = 10800 // after three hours - val gridDemand = thermalGrid.energyDemand( - tick, - testGridAmbientTemperature, - ThermalGrid.startingState(thermalGrid), + val (houseDemand, storageDemand, updatedThermalGridState) = + thermalGrid.energyDemandAndUpdatedState( + tick, + testGridAmbientTemperature, + testGridAmbientTemperature, + ThermalGrid.startingState(thermalGrid), + ) + houseDemand.required should approximate(KilowattHours(0d)) + houseDemand.possible should approximate(KilowattHours(31.05009722d)) + storageDemand.required should approximate(KilowattHours(1150d)) + storageDemand.possible should approximate(KilowattHours(1150d)) + updatedThermalGridState.houseState shouldBe Some( + ThermalHouseState(10800, Kelvin(292.0799935185185), Kilowatts(0d)) ) - - gridDemand.required should approximate(KilowattHours(0d)) - gridDemand.possible should approximate( - KilowattHours(31.05009722 + 1150) + updatedThermalGridState.storageState shouldBe Some( + ThermalStorageState(10800, KilowattHours(0d), Kilowatts(0d)) ) } - "consider stored energy to reduce house demand" in { + "deliver the correct house and storage demand" in { val tick = 10800 // after three hours val startingState = ThermalGrid.startingState(thermalGrid) - val gridDemand = thermalGrid.energyDemand( - tick, - testGridAmbientTemperature, - startingState.copy(houseState = - startingState.houseState.map( - _.copy(innerTemperature = Celsius(16d)) - ) - ), - ) - - gridDemand.required should approximate(KilowattHours(45.60005555555534)) - gridDemand.possible should approximate(KilowattHours(1225.600055555)) - } - - "consider stored energy to reduce house demand if stored energy is not enough" in { - val tick = 10800 // after three hours + val (houseDemand, storageDemand, updatedThermalGridState) = + thermalGrid.energyDemandAndUpdatedState( + tick, + testGridAmbientTemperature, + testGridAmbientTemperature, + startingState.copy(houseState = + startingState.houseState.map( + _.copy(innerTemperature = Celsius(16d)) + ) + ), + ) - val startingState = ThermalGrid.startingState(thermalGrid) - val gridDemand = thermalGrid.energyDemand( - tick, - testGridAmbientTemperature, - startingState.copy(houseState = - startingState.houseState.map( - _.copy(innerTemperature = Celsius(3d)) - ) - ), + houseDemand.required should approximate(KilowattHours(45.6000555)) + houseDemand.possible should approximate(KilowattHours(75.600055555)) + storageDemand.required should approximate(KilowattHours(1150d)) + storageDemand.possible should approximate(KilowattHours(1150d)) + updatedThermalGridState.houseState shouldBe Some( + ThermalHouseState(10800, Celsius(15.959996296296296), Kilowatts(0d)) ) - gridDemand.required should approximate( - KilowattHours(238.64987499999984) + updatedThermalGridState.storageState shouldBe Some( + ThermalStorageState(10800, KilowattHours(0d), Kilowatts(0d)) ) - gridDemand.possible should approximate(KilowattHours(1418.64987499999)) } } 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 d2b48ff3b3..eb447d0fc4 100644 --- a/src/test/scala/edu/ie3/simona/model/thermal/ThermalGridWithHouseOnlySpec.scala +++ b/src/test/scala/edu/ie3/simona/model/thermal/ThermalGridWithHouseOnlySpec.scala @@ -14,9 +14,9 @@ import edu.ie3.simona.model.thermal.ThermalHouse.ThermalHouseThreshold.{ HouseTemperatureUpperBoundaryReached, } import edu.ie3.simona.test.common.UnitSpec -import squants.energy.{Kilowatts, Megawatts, WattHours, Watts} +import squants.energy._ import squants.thermal.Celsius -import squants.{Energy, Power, Temperature} +import squants.{Energy, Kelvin, Power, Temperature} import scala.jdk.CollectionConverters._ @@ -73,21 +73,29 @@ class ThermalGridWithHouseOnlySpec extends UnitSpec with ThermalHouseTestData { "determining the energy demand" should { "exactly be the demand of the house" in { - val tick = 10800 // after three house - val houseDemand = thermalHouse.energyDemand( + val tick = 10800 // after three hours + val expectedHouseDemand = thermalHouse.energyDemand( tick, testGridAmbientTemperature, expectedHouseStartingState, ) - val gridDemand = thermalGrid.energyDemand( - tick, - testGridAmbientTemperature, - ThermalGrid.startingState(thermalGrid), - ) + val (houseDemand, storageDemand, updatedThermalGridState) = + thermalGrid.energyDemandAndUpdatedState( + tick, + testGridAmbientTemperature, + testGridAmbientTemperature, + ThermalGrid.startingState(thermalGrid), + ) - gridDemand.required should approximate(houseDemand.required) - gridDemand.possible should approximate(houseDemand.possible) + houseDemand.required should approximate(expectedHouseDemand.required) + houseDemand.possible should approximate(expectedHouseDemand.possible) + storageDemand.required should approximate(KilowattHours(0d)) + storageDemand.possible should approximate(KilowattHours(0d)) + updatedThermalGridState.houseState shouldBe Some( + ThermalHouseState(10800, Kelvin(292.0799935185185), Kilowatts(0d)) + ) + updatedThermalGridState.storageState shouldBe None } } @@ -127,7 +135,7 @@ class ThermalGridWithHouseOnlySpec extends UnitSpec with ThermalHouseTestData { } "not withdraw energy from the house, if actual consumption is given" in { - val tick = 0L // after three house + val tick = 0L // after three hours val gridState = ThermalGrid.startingState(thermalGrid) val (updatedGridState, reachedThreshold) = 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 bbb804c35f..93a45e3180 100644 --- a/src/test/scala/edu/ie3/simona/model/thermal/ThermalGridWithStorageOnlySpec.scala +++ b/src/test/scala/edu/ie3/simona/model/thermal/ThermalGridWithStorageOnlySpec.scala @@ -80,31 +80,46 @@ class ThermalGridWithStorageOnlySpec "deliver the capabilities of the storage" in { val tick = 10800 // after three hours - val gridDemand = thermalGrid.energyDemand( - tick, - testGridAmbientTemperature, - ThermalGrid.startingState(thermalGrid), - ) - - gridDemand.required should approximate(KilowattHours(0d)) - gridDemand.possible should approximate(KilowattHours(1150d)) + val (houseDemand, storageDemand, updatedThermalGridState) = + thermalGrid.energyDemandAndUpdatedState( + tick, + testGridAmbientTemperature, + testGridAmbientTemperature, + ThermalGrid.startingState(thermalGrid), + ) + houseDemand.required should approximate(KilowattHours(0d)) + houseDemand.possible should approximate(KilowattHours(0d)) + storageDemand.required should approximate(KilowattHours(1150d)) + storageDemand.possible should approximate(KilowattHours(1150d)) + updatedThermalGridState.houseState shouldBe None + updatedThermalGridState.storageState shouldBe Some( + ThermalStorageState(10800, KilowattHours(0d), Kilowatts(0d)) + ) } "deliver the capabilities of a half full storage" in { val tick = 10800 // after three hours - val gridDemand = thermalGrid.energyDemand( - tick, - testGridAmbientTemperature, - ThermalGridState( - None, - Some(ThermalStorageState(0L, KilowattHours(575d), Kilowatts(0d))), - ), - ) + val (houseDemand, storageDemand, updatedThermalGridState) = + thermalGrid.energyDemandAndUpdatedState( + tick, + testGridAmbientTemperature, + testGridAmbientTemperature, + ThermalGridState( + None, + Some(ThermalStorageState(0L, KilowattHours(575d), Kilowatts(0d))), + ), + ) - gridDemand.required should approximate(KilowattHours(0d)) - gridDemand.possible should approximate(KilowattHours(575d)) + houseDemand.required should approximate(KilowattHours(0d)) + houseDemand.possible should approximate(KilowattHours(0d)) + storageDemand.required should approximate(KilowattHours(0d)) + storageDemand.possible should approximate(KilowattHours(575d)) + updatedThermalGridState.houseState shouldBe None + updatedThermalGridState.storageState shouldBe Some( + ThermalStorageState(10800L, KilowattHours(575d), Kilowatts(0d)) + ) } } From 67e2a4e433b0c7d5ef8931ae95299de116f15066 Mon Sep 17 00:00:00 2001 From: danielfeismann Date: Sat, 24 Aug 2024 10:00:39 +0200 Subject: [PATCH 2/2] fmt --- .../scala/edu/ie3/simona/model/participant/HpModel.scala | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 ad512a75e0..569bd48c10 100644 --- a/src/main/scala/edu/ie3/simona/model/participant/HpModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant/HpModel.scala @@ -11,7 +11,10 @@ import edu.ie3.simona.agent.participant.data.Data.PrimaryData.ApparentPowerAndHe import edu.ie3.simona.model.SystemComponent import edu.ie3.simona.model.participant.HpModel.{HpRelevantData, HpState} import edu.ie3.simona.model.participant.control.QControl -import edu.ie3.simona.model.thermal.ThermalGrid.{ThermalEnergyDemand, ThermalGridState} +import edu.ie3.simona.model.thermal.ThermalGrid.{ + ThermalEnergyDemand, + ThermalGridState, +} import edu.ie3.simona.model.thermal.{ThermalGrid, ThermalThreshold} import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage.ProvideFlexOptions import edu.ie3.simona.ontology.messages.flex.MinMaxFlexibilityMessage.ProvideMinMaxFlexOptions