diff --git a/CHANGELOG.md b/CHANGELOG.md index 7854a5e918..183d1a781c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -115,6 +115,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Improve code quality in fixedloadmodelspec and other tests [#919](https://github.com/ie3-institute/simona/issues/919) - Fix power flow calculation with em agents [#962](https://github.com/ie3-institute/simona/issues/962) - Fix scheduling at Evcs with more than one Ev at a time without Em [#787](https://github.com/ie3-institute/simona/issues/787) +- Fixed Hp results leading to overheating house and other effects [#827](https://github.com/ie3-institute/simona/issues/827) ## [3.0.0] - 2023-08-07 diff --git a/src/main/scala/edu/ie3/simona/agent/ValueStore.scala b/src/main/scala/edu/ie3/simona/agent/ValueStore.scala index a21f7d0f58..fbae2ff77c 100644 --- a/src/main/scala/edu/ie3/simona/agent/ValueStore.scala +++ b/src/main/scala/edu/ie3/simona/agent/ValueStore.scala @@ -22,7 +22,7 @@ import scala.collection.SortedMap */ final case class ValueStore[+D]( maxTickSpan: Long, - private val store: SortedMap[Long, D] = SortedMap.empty[Long, D], + store: SortedMap[Long, D] = SortedMap.empty[Long, D], ) { /** Determine the lastly known data tick, if available. Includes the given 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..ac34bc082e 100644 --- a/src/main/scala/edu/ie3/simona/model/participant/HpModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant/HpModel.scala @@ -150,6 +150,7 @@ final case class HpModel( ): Boolean = { val demand = thermalGrid.energyDemand( relevantData.currentTick, + state.ambientTemperature.getOrElse(relevantData.ambientTemperature), relevantData.ambientTemperature, state.thermalGridState, ) @@ -210,6 +211,7 @@ final case class HpModel( /* Determine the options we have */ val thermalEnergyDemand = thermalGrid.energyDemand( data.currentTick, + lastState.ambientTemperature.getOrElse(data.ambientTemperature), data.ambientTemperature, lastState.thermalGridState, ) 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 325a90b316..318f0a2b0a 100644 --- a/src/main/scala/edu/ie3/simona/model/thermal/ThermalGrid.scala +++ b/src/main/scala/edu/ie3/simona/model/thermal/ThermalGrid.scala @@ -24,7 +24,7 @@ import edu.ie3.simona.util.TickUtil.TickLong import edu.ie3.util.quantities.QuantityUtils.RichQuantityDouble import edu.ie3.util.scala.quantities.DefaultQuantities._ import squants.energy.Kilowatts -import squants.{Energy, Power, Temperature} +import squants.{Dimensionless, Each, Energy, Power, Temperature} import java.time.ZonedDateTime import scala.jdk.CollectionConverters.SetHasAsScala @@ -46,8 +46,10 @@ final case class ThermalGrid( * time * @param tick * Questioned instance in time - * @param ambientTemperature - * Ambient temperature in the instance in question + * @param lastAmbientTemperature + * Ambient temperature in the instance in question until actual tick + * @param actualAmbientTemperature + * Ambient temperature in the instance in question at tick * @param state * Currently applicable state of the thermal grid * @return @@ -55,27 +57,49 @@ final case class ThermalGrid( */ def energyDemand( tick: Long, - ambientTemperature: Temperature, + // FIXME this is also in state -> refactoring by HiWi + lastAmbientTemperature: Temperature, + actualAmbientTemperature: 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, - ) + /* First get the energy demand of the houses but only if inner temperature is below target temperature */ + + val houseDemand = + house.zip(state.houseState).headOption match { + case Some((thermalHouse, lastHouseState)) => + val (updatedHouseState, updatedStorageState) = + thermalHouse.determineState( + tick, + lastHouseState, + lastAmbientTemperature, + actualAmbientTemperature, + lastHouseState.qDot, + ) + if ( + updatedHouseState.innerTemperature < thermalHouse.targetTemperature + ) { + thermalHouse.energyDemand( + tick, + actualAmbientTemperature, + updatedHouseState, + ) + + } else { + ThermalEnergyDemand.noDemand + } + + case None => + ThermalEnergyDemand.noDemand } - .getOrElse(ThermalEnergyDemand.noDemand) /* Then go over the storages, see what they can provide and what they might be able to charge */ val (storedEnergy, remainingCapacity) = { storage .zip(state.storageState) .map { case (storage, state) => - val usableEnergy = state.storedEnergy + val updatedStorageState = + storage.updateState(tick, state.qDot, state)._1 + val usableEnergy = updatedStorageState.storedEnergy val remaining = storage.getMaxEnergyThreshold - usableEnergy ( usableEnergy, @@ -152,12 +176,202 @@ final case class ThermalGrid( ambientTemperature: Temperature, state: ThermalGridState, qDot: Power, - ): (ThermalGridState, Option[ThermalThreshold]) = - house.zip(state.houseState) match { + ): (ThermalGridState, Option[ThermalThreshold]) = { + + val (qDotHouseLastState, qDotStorageLastState) = determineLastStateQDots( + state + ) + + val actualThermalStorageSoc = determineThermalStorageSoc(tick, state, qDot) + + // Storage was charged in the lastState and is not full + if (qDotStorageLastState > zeroKW && actualThermalStorageSoc < Each(1.0)) { + continuePushingInfeedIntoStorage( + tick, + state, + lastAmbientTemperature, + ambientTemperature, + qDot, + ) + } + // Storage was not charged in the last state or is full + else { + pushInfeedIntoHouseFirst( + tick, + state, + lastAmbientTemperature, + ambientTemperature, + qDot, + ) + } + } + + /** Determines the power infeed or withdraw of house and storage in the last + * state + * + * @param lastState + * The last state of the ThermalGrid + * @return + * The qDot of house and storage in the last state + */ + + private def determineLastStateQDots( + lastState: ThermalGridState + ): (Power, Power) = { + + (lastState.houseState, lastState.storageState) match { + case (Some(house), Some(storage)) => (house.qDot, storage.qDot) + case (Some(house), None) => (house.qDot, zeroKW) + case (None, Some(storage)) => (zeroKW, storage.qDot) + case (None, None) => (zeroKW, zeroKW) + } + } + + /** Determines the actual state of charge (SoC) of the thermal storage. + * + * @param tick + * Instance in time + * @param lastState + * The last applicable state + * @param qDot + * Thermal infeed (+) or withdraw (-) + * @return + * The relative state of charge + */ + + private def determineThermalStorageSoc( + tick: Long, + lastState: ThermalGridState, + qDot: Power, + ): Dimensionless = { + + if (storage.nonEmpty && lastState.storageState.nonEmpty) { + val (thermalStorage, lastStorageState) = + storage.zip(lastState.storageState).head + val (updatedStorageState, maybeStorageThreshold) = + thermalStorage.updateState( + tick, + qDot, + lastStorageState, + ) + Each( + updatedStorageState.storedEnergy / thermalStorage.getMaxEnergyThreshold + ) + } else { + Each(1.0) + } + } + + /** This can be used to continue pushing infeed into the storage. Based on the + * lastState the same qDot will be used to charge the thermal storage, update + * its state and retunr the next threshold. + * + * @param tick + * Instance in time + * @param lastState + * The last applicable state + * @param ambientTemperature + * Ambient temperature + * @param qDot + * Thermal infeed (+) or withdraw (-) + * @return + * Updated ThermalGridState and the next thermal threshold + */ + + private def continuePushingInfeedIntoStorage( + tick: Long, + lastState: ThermalGridState, + lastAmbientTemperature: Temperature, + ambientTemperature: Temperature, + qDot: Power, + ): (ThermalGridState, Option[ThermalThreshold]) = { + storage.zip(lastState.storageState) match { + case Some((thermalStorage, lastStorageState)) => + val ( + updatedHouseState: Option[ThermalHouseState], + maybeHouseThreshold: Option[ThermalThreshold], + ) = + /* Set thermal power exchange with house to zero */ + house.zip(lastState.houseState) match { + case Some((thermalHouse, houseState)) => + val nextHouseState = + thermalHouse.determineState( + tick, + houseState, + lastAmbientTemperature, + ambientTemperature, + zeroKW, + ) + (Some(nextHouseState._1), nextHouseState._2) + + case _ => lastState.houseState + } + + val (updatedStorageState, maybeStorageThreshold) = + thermalStorage.updateState( + tick, + qDot, + lastStorageState, + ) + + /* Both house and storage are updated. Determine what reaches the next threshold */ + val nextThreshold = determineMostRecentThreshold( + maybeHouseThreshold, + maybeStorageThreshold, + ) + + ( + lastState.copy( + houseState = updatedHouseState, + storageState = Some(updatedStorageState), + ), + nextThreshold, + ) + + case None => + storage.zip(lastState.storageState) match { + case Some((thermalStorage, storageState)) => + val (updatedStorageState, maybeStorageThreshold) = + thermalStorage.updateState(tick, qDot, storageState) + ( + lastState.copy(storageState = Some(updatedStorageState)), + maybeStorageThreshold, + ) + case None => + throw new InconsistentStateException( + "A thermal grid has to contain either at least a house or a storage." + ) + } + } + } + + /** Manages the thermal infeed into the house (first) or in case the house is + * already heated up fully, into the thermal storage. + * + * @param tick + * Instance in time + * @param lastState + * The last applicable state + * @param ambientTemperature + * Ambient temperature + * @param qDot + * Thermal infeed (+) or withdraw (-) + * @return + * Updated ThermalGridState and the next thermal threshold + */ + + private def pushInfeedIntoHouseFirst( + tick: Long, + lastState: ThermalGridState, + lastAmbientTemperature: Temperature, + ambientTemperature: Temperature, + qDot: Power, + ): (ThermalGridState, Option[ThermalThreshold]) = { + house.zip(lastState.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 { + val updatedStorageState = storage.zip(lastState.storageState) match { case Some((thermalStorage, storageState)) => Some( thermalStorage @@ -168,7 +382,7 @@ final case class ThermalGrid( ) ._1 ) - case _ => state.storageState + case _ => lastState.storageState } val (updatedHouseState, maybeHouseThreshold) = @@ -206,7 +420,7 @@ final case class ThermalGrid( ) ( - state.copy( + lastState.copy( houseState = Some(fullHouseState), storageState = Some(updatedStorageState), ), @@ -215,25 +429,25 @@ final case class ThermalGrid( case None => /* There is no storage, house determines the next activation */ ( - state.copy(houseState = Some(fullHouseState)), + lastState.copy(houseState = Some(fullHouseState)), maybeFullHouseThreshold, ) } } else { /* The house can handle the infeed */ ( - state.copy(houseState = Some(updatedHouseState)), + lastState.copy(houseState = Some(updatedHouseState)), maybeHouseThreshold, ) } case None => - storage.zip(state.storageState) match { + storage.zip(lastState.storageState) match { case Some((thermalStorage, storageState)) => val (updatedStorageState, maybeStorageThreshold) = thermalStorage.updateState(tick, qDot, storageState) ( - state.copy(storageState = Some(updatedStorageState)), + lastState.copy(storageState = Some(updatedStorageState)), maybeStorageThreshold, ) case None => @@ -242,6 +456,17 @@ final case class ThermalGrid( ) } } + } + + /** Determines the most recent threshold of two given input thresholds + * + * @param maybeHouseThreshold + * Option of a possible next threshold of the thermal house + * @param maybeStorageThreshold + * Option of a possible next threshold of the thermal storage + * @return + * The next threshold + */ private def determineMostRecentThreshold( maybeHouseThreshold: Option[ThermalThreshold], @@ -409,6 +634,44 @@ final case class ThermalGrid( def results( state: ThermalGridState )(implicit startDateTime: ZonedDateTime): Seq[ResultEntity] = { + /* FIXME: We only want to write results when there is a change within the participant. + At the moment we write an storage result when the house result gets updated and vice versa. + * */ + + val houseResultTick: Option[Long] = house + .zip(state.houseState) + .headOption + .flatMap { + case ( + thermalHouse, + ThermalHouseState(tick, _, _), + ) => + Some(tick) + case _ => None + } + + val storageResultTick: Option[Long] = storage + .zip(state.storageState) + .headOption + .flatMap { + case ( + thermalStorage, + ThermalStorageState(tick, _, _), + ) => + Some(tick) + case _ => None + } + + val actualResultTick: Long = (houseResultTick, storageResultTick) match { + case (Some(hTick), Some(sTick)) => math.max(hTick, sTick) + case (Some(hTick), None) => hTick + case (None, Some(sTick)) => sTick + case (None, None) => + throw new RuntimeException( + "ThermalGrid result should be carried out but it was not possible to get the tick for the result" + ) + } + val houseResults = house .zip(state.houseState) .map { @@ -417,7 +680,7 @@ final case class ThermalGrid( ThermalHouseState(tick, innerTemperature, thermalInfeed), ) => Seq.empty[ResultEntity] :+ new ThermalHouseResult( - tick.toDateTime, + actualResultTick.toDateTime, thermalHouse.uuid, thermalInfeed.toMegawatts.asMegaWatt, innerTemperature.toKelvinScale.asKelvin, @@ -433,7 +696,7 @@ final case class ThermalGrid( ThermalStorageState(tick, storedEnergy, qDot), ) => houseResults :+ new CylindricalStorageResult( - tick.toDateTime, + actualResultTick.toDateTime, storage.uuid, storedEnergy.toMegawattHours.asMegaWattHour, qDot.toMegawatts.asMegaWatt, 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..46fe549cce 100644 --- a/src/test/scala/edu/ie3/simona/agent/em/EmAgentIT.scala +++ b/src/test/scala/edu/ie3/simona/agent/em/EmAgentIT.scala @@ -596,9 +596,8 @@ class EmAgentIT /* TICK 7200 LOAD: 0.000269 MW (unchanged) PV: -0.003797 MW - Heat pump: running (turned on from last request), can also be turned off - -> set point ~3.5 kW (bigger than 50 % rated apparent power): stays turned on with unchanged state - -> remaining 0 MW + Heat pump: running (turned on from last request), must be turned off + -> remaining -0.003528 MW */ emAgentActivation ! Activation(7200) @@ -621,8 +620,10 @@ class EmAgentIT case ParticipantResultEvent(emResult: EmResult) => emResult.getInputModel shouldBe emInput.getUuid emResult.getTime shouldBe 7200.toDateTime - emResult.getP should equalWithTolerance(0.00132184544484.asMegaWatt) - emResult.getQ should equalWithTolerance(0.001073120041.asMegaVar) + emResult.getP should equalWithTolerance( + (-0.003528154555).asMegaWatt + ) + emResult.getQ should equalWithTolerance(0.0000882855367.asMegaVar) } scheduler.expectMessage(Completion(emAgentActivation, Some(14400))) @@ -630,8 +631,8 @@ class EmAgentIT /* TICK 14400 LOAD: 0.000269 MW (unchanged) PV: -0.000066 MW - Heat pump: Is still running, can still be turned off - -> flex signal is 0 MW: Heat pump is turned off + Heat pump: Is not running, can be turned on + -> flex signal is 0 MW: Heat pump stays off */ emAgentActivation ! Activation(14400) @@ -659,13 +660,31 @@ class EmAgentIT emResult.getQ should equalWithTolerance(0.000088285537.asMegaVar) } + scheduler.expectMessage(Completion(emAgentActivation, Some(20498))) + + /* TICK 20498 + LOAD: 0.000269 MW (unchanged) + PV: -0.000032 MW (unchanged) + Heat pump: Is not running, since lower temp boundary is reached: Hp is turned on + -> flex signal is no control -> 0.00485 MW + */ + + emAgentActivation ! Activation(20498) + + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(emResult: EmResult) => + emResult.getInputModel shouldBe emInput.getUuid + emResult.getTime shouldBe 20498.toDateTime + emResult.getP should equalWithTolerance(0.005052956264.asMegaWatt) + emResult.getQ should equalWithTolerance(0.001073120041.asMegaVar) + } + scheduler.expectMessage(Completion(emAgentActivation, Some(21600))) /* TICK 21600 - LOAD: 0.000269 MW (unchanged) - PV: -0.000032 MW - Heat pump: Is not running, can run or stay off - -> flex signal is 0 MW: Heat pump is turned off + LOAD: 0.000269 MW (unchanged) + PV: -0.000032 MW (unchanged) + Heat pump: Is running and cannot be turned off */ emAgentActivation ! Activation(21600) @@ -688,27 +707,8 @@ class EmAgentIT case ParticipantResultEvent(emResult: EmResult) => emResult.getInputModel shouldBe emInput.getUuid emResult.getTime shouldBe 21600.toDateTime - emResult.getP should equalWithTolerance(0.0002367679996.asMegaWatt) - emResult.getQ should equalWithTolerance(0.000088285537.asMegaVar) - } - - scheduler.expectMessage(Completion(emAgentActivation, Some(28665))) - - /* TICK 28666 - 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) - - 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.getP should equalWithTolerance(0.005086767999.asMegaWatt) + emResult.getQ should equalWithTolerance(0.001073120041.asMegaVar) } scheduler.expectMessage(Completion(emAgentActivation, Some(28800))) 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..c91f7054e1 100644 --- a/src/test/scala/edu/ie3/simona/model/participant/HpModelSpec.scala +++ b/src/test/scala/edu/ie3/simona/model/participant/HpModelSpec.scala @@ -236,11 +236,14 @@ class HpModelSpec "deliver correct flexibility options" in { 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(20), Kilowatts(0))), + Some(ThermalStorageState(0L, KilowattHours(0), Kilowatts(0))), ), HpState( isRunning = false, @@ -248,17 +251,120 @@ class HpModelSpec 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), + ), + // FIXME + /* + // 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))), + ), + 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, + ), + (0.0, 0.0, 95.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(20), Kilowatts(0)) + ThermalStorageState(0L, KilowattHours(0), Kilowatts(0)) ), ), None, ), (95.0, 95.0, 95.0), ), - // House is between target temperature and lower temperature boundary, Hp actually running + /* + // 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(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, + ), + (0.0, 0.0, 95.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), + ), + // 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))), @@ -271,7 +377,7 @@ class HpModelSpec Kilowatts(95.0), Kilowatts(80.0), ThermalGridState( - Some(ThermalHouseState(0L, Celsius(19), Kilowatts(80))), + Some(ThermalHouseState(0L, Celsius(19), Kilowatts(0))), Some( ThermalStorageState(0L, KilowattHours(20), Kilowatts(0)) ), @@ -280,8 +386,36 @@ class HpModelSpec ), (95.0, 0.0, 95.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), + ), - // House is between target temperature and lower temperature boundary, Hp actually not running + */ + // 8. Same as before but heat storage is NOT empty + // Hp should be off but able to turn on ( ThermalGridState( Some(ThermalHouseState(0L, Celsius(19), Kilowatts(0))), @@ -291,8 +425,8 @@ class HpModelSpec isRunning = false, 0, Some(hpData.ambientTemperature), - Kilowatts(0.0), - Kilowatts(0.0), + Kilowatts(95.0), + Kilowatts(80.0), ThermalGridState( Some(ThermalHouseState(0L, Celsius(19), Kilowatts(0))), Some( @@ -303,10 +437,126 @@ class HpModelSpec ), (0.0, 0.0, 95.0), ), - // Storage and house have remaining capacity + // 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), + ), + // 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(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), + ), + + */ + // 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), + ), + + // 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), + ), + // 14. Same as before but storage is NOT empty + // Hp should run but can be turned off ( ThermalGridState( - Some(ThermalHouseState(0L, Celsius(21), Kilowatts(80))), + Some(ThermalHouseState(0L, Celsius(22), Kilowatts(0))), Some(ThermalStorageState(0L, KilowattHours(20), Kilowatts(0))), ), HpState( @@ -316,18 +566,66 @@ class HpModelSpec Kilowatts(95.0), Kilowatts(80.0), ThermalGridState( - Some(ThermalHouseState(0L, Celsius(21), Kilowatts(80))), + Some(ThermalHouseState(0L, Celsius(21), Kilowatts(0))), Some(ThermalStorageState(0L, KilowattHours(20), Kilowatts(0))), ), 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), + ), + + */ + + // 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), + ), // Storage is full, House has capacity till upper boundary ( ThermalGridState( - Some(ThermalHouseState(0L, Celsius(21), Kilowatts(80))), + Some(ThermalHouseState(0L, Celsius(21), Kilowatts(0))), Some(ThermalStorageState(0L, KilowattHours(500), Kilowatts(0))), ), HpState( @@ -337,7 +635,7 @@ class HpModelSpec Kilowatts(0.0), Kilowatts(0.0), ThermalGridState( - Some(ThermalHouseState(0L, Celsius(21), Kilowatts(80))), + Some(ThermalHouseState(0L, Celsius(19), Kilowatts(0))), Some( ThermalStorageState(0L, KilowattHours(500), Kilowatts(0)) ), @@ -346,12 +644,11 @@ class HpModelSpec ), (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(ThermalHouseState(0L, Celsius(22), Kilowatts(0))), Some(ThermalStorageState(0L, KilowattHours(500), Kilowatts(0))), ), HpState( @@ -361,7 +658,7 @@ class HpModelSpec Kilowatts(95.0), Kilowatts(80.0), ThermalGridState( - Some(ThermalHouseState(0L, Celsius(22), Kilowatts(80))), + Some(ThermalHouseState(0L, Celsius(22), Kilowatts(0))), Some( ThermalStorageState(0L, KilowattHours(500), Kilowatts(0)) ), 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..db0cba85be 100644 --- a/src/test/scala/edu/ie3/simona/model/thermal/ThermalGridWithHouseAndStorageSpec.scala +++ b/src/test/scala/edu/ie3/simona/model/thermal/ThermalGridWithHouseAndStorageSpec.scala @@ -99,6 +99,7 @@ class ThermalGridWithHouseAndStorageSpec val gridDemand = thermalGrid.energyDemand( tick, testGridAmbientTemperature, + testGridAmbientTemperature, ThermalGrid.startingState(thermalGrid), ) @@ -115,6 +116,7 @@ class ThermalGridWithHouseAndStorageSpec val gridDemand = thermalGrid.energyDemand( tick, testGridAmbientTemperature, + testGridAmbientTemperature, startingState.copy(houseState = startingState.houseState.map( _.copy(innerTemperature = Celsius(16d)) @@ -133,6 +135,7 @@ class ThermalGridWithHouseAndStorageSpec val gridDemand = thermalGrid.energyDemand( tick, testGridAmbientTemperature, + testGridAmbientTemperature, startingState.copy(houseState = startingState.houseState.map( _.copy(innerTemperature = Celsius(3d)) 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..0a782c222d 100644 --- a/src/test/scala/edu/ie3/simona/model/thermal/ThermalGridWithHouseOnlySpec.scala +++ b/src/test/scala/edu/ie3/simona/model/thermal/ThermalGridWithHouseOnlySpec.scala @@ -83,6 +83,7 @@ class ThermalGridWithHouseOnlySpec extends UnitSpec with ThermalHouseTestData { val gridDemand = thermalGrid.energyDemand( tick, testGridAmbientTemperature, + testGridAmbientTemperature, ThermalGrid.startingState(thermalGrid), ) 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..fd05aae5d3 100644 --- a/src/test/scala/edu/ie3/simona/model/thermal/ThermalGridWithStorageOnlySpec.scala +++ b/src/test/scala/edu/ie3/simona/model/thermal/ThermalGridWithStorageOnlySpec.scala @@ -83,6 +83,7 @@ class ThermalGridWithStorageOnlySpec val gridDemand = thermalGrid.energyDemand( tick, testGridAmbientTemperature, + testGridAmbientTemperature, ThermalGrid.startingState(thermalGrid), ) @@ -97,6 +98,7 @@ class ThermalGridWithStorageOnlySpec val gridDemand = thermalGrid.energyDemand( tick, testGridAmbientTemperature, + testGridAmbientTemperature, ThermalGridState( None, Some(ThermalStorageState(0L, KilowattHours(575d), Kilowatts(0d))),