diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ff86ccca2..11a9a6b80b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Enhanced Newton-Raphson-PowerFlow failures with more information [#815](https://github.com/ie3-institute/simona/issues/815) - Update RTD references and bibliography [#868](https://github.com/ie3-institute/simona/issues/868) - Add gradle application plugin for command line execution with gradle run [#890](https://github.com/ie3-institute/simona/issues/890) +- Additional tests to check flexibility options of thermal house and storage [#729](https://github.com/ie3-institute/simona/issues/729) ### Changed - Adapted to changed data source in PSDM [#435](https://github.com/ie3-institute/simona/issues/435) @@ -76,6 +77,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Updated AUTHORS.md [#904](https://github.com/ie3-institute/simona/issues/904) - 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) ### Fixed - Removed a repeated line in the documentation of vn_simona config [#658](https://github.com/ie3-institute/simona/issues/658) @@ -100,6 +102,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed FixedFeedModelSpec [#861](https://github.com/ie3-institute/simona/issues/861) - Fixing duration calculation in result events [#801](https://github.com/ie3-institute/simona/issues/801) - Handle MobSim requests for current prices [#892](https://github.com/ie3-institute/simona/issues/892) +- Fixed Hp results leading to overheating house and other effects [#827](https://github.com/ie3-institute/simona/issues/827) +- Fixed thermal storage getting recharged when empty [#827](https://github.com/ie3-institute/simona/issues/827) ## [3.0.0] - 2023-08-07 diff --git a/docs/readthedocs/models.md b/docs/readthedocs/models.md index 46fcd2e991..c9a1f870f2 100644 --- a/docs/readthedocs/models.md +++ b/docs/readthedocs/models.md @@ -16,6 +16,7 @@ models/two_winding_transformer_model models/three_winding_transformer_model models/reference_system models/thermal_grid_model +models/thermal_house_model ``` ## System Participant Related Models diff --git a/docs/readthedocs/models/thermal_grid_model.md b/docs/readthedocs/models/thermal_grid_model.md index fc16159552..48b5371b99 100644 --- a/docs/readthedocs/models/thermal_grid_model.md +++ b/docs/readthedocs/models/thermal_grid_model.md @@ -3,6 +3,8 @@ The Thermal Grid Model introduces a coupling point to thermal system, equivalent to an electrical node. It can be used to interconnect thermal units to a thermal heat network. +A Thermal Grid Model consists of a {ref}`thermal_house_model` and / or a {ref}`cts_model` which are supplied by a thermal source like a {ref}`hp_model`. + ## Attributes, Units and Remarks Please refer to {doc}`PowerSystemDataModel - Thermal Bus ` for Attributes and Units used in this Model. diff --git a/docs/readthedocs/models/thermal_house_model.md b/docs/readthedocs/models/thermal_house_model.md new file mode 100644 index 0000000000..7914270c7f --- /dev/null +++ b/docs/readthedocs/models/thermal_house_model.md @@ -0,0 +1,13 @@ +(thermal_house_model)= + +# Thermal House Model + +This page documents the functionality of the thermal house available in SIMONA. + +## Behaviour + +This house model represents the thermal behaviour of a building. This reflects a simple shoe box with a thermal capacity and with transmission losses. + +## Attributes, Units and Remarks + +Please refer to {doc}`PowerSystemDataModel - Thermal House Model ` for Attributes and Units used in this Model. 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/agent/participant/hp/HpAgentFundamentals.scala b/src/main/scala/edu/ie3/simona/agent/participant/hp/HpAgentFundamentals.scala index 9add5d8ddc..1d6506c714 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 @@ -261,8 +261,26 @@ trait HpAgentFundamentals currentTick, updatedState, ) - val updatedBaseStateData = - baseStateData.copy(stateDataStore = updatedStateDataStore) + + val updatedBaseStateData = { + updatedState.maybeThermalThreshold match { + case Some(nextThreshold) + if baseStateData.foreseenDataTicks.headOption.exists { + case (_, Some(tick)) => nextThreshold.tick < tick + case _ => false + } => + baseStateData.copy( + stateDataStore = updatedStateDataStore, + additionalActivationTicks = + baseStateData.additionalActivationTicks + nextThreshold.tick, + ) + case _ => + baseStateData.copy( + stateDataStore = updatedStateDataStore + ) + } + } + updateValueStoresInformListenersAndGoToIdleWithUpdatedBaseStateData( scheduler, updatedBaseStateData, @@ -293,7 +311,11 @@ trait HpAgentFundamentals calcRelevantData: HpRelevantData, nodalVoltage: squants.Dimensionless, model: HpModel, - ): HpState = model.determineState(modelState, calcRelevantData) + ): HpState = { + val (canOperate, canBeOutOfOperation, state) = + model.determineState(modelState, calcRelevantData) + state + } /** Abstract definition, individual implementations found in individual agent * fundamental classes @@ -385,25 +407,44 @@ trait HpAgentFundamentals tick: Long, ): HpRelevantData = { /* extract weather data from secondary data, which should have been requested and received before */ - val weatherData = - baseStateData.receivedSecondaryDataStore + val weatherData = { + val currentWeatherData = baseStateData.receivedSecondaryDataStore .last(tick) .flatMap { case (receivedTick, receivedValues) => - if (receivedTick != tick) - log.debug( - s"The model ${baseStateData.model.getUuid} needs to do calculations with values received " + - s"in tick $receivedTick, as no weather data has been received in tick $tick." - ) - receivedValues.collectFirst { - // filter secondary data for weather data - case (_, data: WeatherData) => data + if (receivedTick == tick) { + receivedValues.collectFirst { case (_, data: WeatherData) => + data + } + } else None + } + + // If current weather data is not found, fallback to the latest entry where the map is not empty + currentWeatherData + .orElse { + val latestEntry = + baseStateData.receivedSecondaryDataStore.store.toSeq.findLast { + case (_, receivedValues) => receivedValues.nonEmpty + } + + latestEntry.flatMap { case (receivedTick, receivedValues) => + if (tick - receivedTick > 3600) { + log.warning( + s"The model ${baseStateData.model.getUuid} is using weather data from tick $receivedTick, " + + s"but there is a discrepancy of ${tick - receivedTick} seconds compared to the current tick $tick." + ) + } + + receivedValues.collectFirst { case (_, data: WeatherData) => + data + } } } - .getOrElse( + .getOrElse { throw new InconsistentStateException( s"The model ${baseStateData.model} was not provided with needed weather data." ) - ) + } + } HpRelevantData( tick, 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 7af9279611..770f6e3637 100644 --- a/src/main/scala/edu/ie3/simona/model/participant/HpModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant/HpModel.scala @@ -11,15 +11,17 @@ 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.energy.{Energy, KilowattHours, Kilowatts} import squants.{Power, Temperature} import java.time.ZonedDateTime @@ -116,19 +118,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 +148,42 @@ 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, ThermalEnergyDemand, ThermalEnergyDemand) = { + val (demandHouse, demandStorage, updatedState) = + thermalGrid.energyDemandAndUpdatedState( + relevantData.currentTick, + relevantData.ambientTemperature, + state.thermalGridState, + ) + implicit val tolerance: Energy = KilowattHours(1e-3) + val noStorageOrStorageIsEmpty: Boolean = + updatedState.storageState.isEmpty || updatedState.storageState.exists( + _.storedEnergy =~ zeroKWH + ) + + val turnHpOn: Boolean = { + (demandHouse.hasRequiredDemand && noStorageOrStorageIsEmpty) || demandStorage.hasRequiredDemand || (state.isRunning && demandHouse.hasAdditionalDemand) || (state.isRunning && demandStorage.hasAdditionalDemand) + } + + val canOperate = + demandHouse.hasRequiredDemand || demandHouse.hasAdditionalDemand || + demandStorage.hasRequiredDemand || demandStorage.hasAdditionalDemand + val canBeOutOfOperation = + !(demandHouse.hasRequiredDemand && noStorageOrStorageIsEmpty) + + ( + turnHpOn, + canOperate, + canBeOutOfOperation, + demandHouse, + demandStorage, ) - demand.hasRequiredDemand || (state.isRunning && demand.hasAdditionalDemand) } /** Calculate state depending on whether heat pump is needed or not. Also @@ -166,6 +196,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]] */ @@ -173,11 +207,19 @@ final case class HpModel( state: HpState, relevantData: HpRelevantData, isRunning: Boolean, + houseDemand: ThermalEnergyDemand, + storageDemand: ThermalEnergyDemand, ): 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) = @@ -186,6 +228,8 @@ final case class HpModel( state.thermalGridState, state.ambientTemperature.getOrElse(relevantData.ambientTemperature), newThermalPower, + houseDemand, + storageDemand, ) HpState( @@ -204,23 +248,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 @@ -229,7 +264,7 @@ final case class HpModel( ProvideMinMaxFlexOptions( uuid, - updatedState.activePower, + updatedHpState.activePower, lowerBoundary, upperBoundary, ) @@ -259,13 +294,32 @@ 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, + data.ambientTemperature, + lastState.thermalGridState, + ) + + val updatedHpState: HpState = + calcState( + lastState, + data, + turnOn, + thermalEnergyDemandHouse, + thermalEnergyDemandStorage, + ) ( - 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 85b41b35ff..4780c6401c 100644 --- a/src/main/scala/edu/ie3/simona/model/thermal/ThermalGrid.scala +++ b/src/main/scala/edu/ie3/simona/model/thermal/ThermalGrid.scala @@ -13,6 +13,7 @@ import edu.ie3.datamodel.models.result.thermal.{ CylindricalStorageResult, ThermalHouseResult, } +import edu.ie3.simona.exceptions.InvalidParameterException import edu.ie3.simona.exceptions.agent.InconsistentStateException import edu.ie3.simona.model.thermal.ThermalGrid.{ ThermalEnergyDemand, @@ -43,7 +44,8 @@ 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 ambientTemperature @@ -51,56 +53,96 @@ final case class ThermalGrid( * @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, 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, + ambientTemperature, + lastHouseState.qDot, + ) + if ( + updatedHouseState.innerTemperature < thermalHouse.targetTemperature + ) { + ( + 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), ) } /** Update the current state of the grid + * * @param tick * Instance in time * @param state @@ -109,6 +151,10 @@ final case class ThermalGrid( * Ambient temperature * @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 */ @@ -117,13 +163,23 @@ final case class ThermalGrid( state: ThermalGridState, ambientTemperature: Temperature, qDot: Power, + houseDemand: ThermalEnergyDemand, + storageDemand: ThermalEnergyDemand, ): (ThermalGridState, Option[ThermalThreshold]) = if (qDot > zeroKW) - handleInfeed(tick, ambientTemperature, state, qDot) + handleInfeed( + tick, + ambientTemperature, + state, + qDot, + houseDemand, + storageDemand, + ) else handleConsumption(tick, ambientTemperature, state, 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 ambientTemperature @@ -132,6 +188,10 @@ final case class ThermalGrid( * Current state of the houses * @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 */ @@ -140,39 +200,142 @@ final case class ThermalGrid( ambientTemperature: Temperature, state: ThermalGridState, 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: ThermalEnergyDemand, + storageDemand: ThermalEnergyDemand, + ): (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." + ) + } - val (updatedHouseState, maybeHouseThreshold) = - thermalHouse.determineState( + if ( + (qDotHouseLastState > zeroKW && qDotHouseLastState == qDot) | qDotStorageLastState > zeroKW + ) { + val (updatedHouseState, thermalHouseThreshold, remainingQDotHouse) = + handleInfeedHouse(tick, ambientTemperature, state, qDotHouseLastState) + val (updatedStorageState, thermalStorageThreshold) = + if ( + qDotStorageLastState >= zeroKW && remainingQDotHouse > qDotStorageLastState + ) { + handleInfeedStorage( + tick, + ambientTemperature, + state, + remainingQDotHouse, + ) + } else { + handleInfeedStorage( tick, - lastHouseState, ambientTemperature, - qDot, + state, + qDotStorageLastState, + ) + } + + val nextThreshold = determineMostRecentThreshold( + thermalHouseThreshold, + thermalStorageThreshold, + ) + ( + state.copy( + houseState = updatedHouseState, + storageState = updatedStorageState, + ), + nextThreshold, + ) + } else { + ( + state, + houseDemand.hasAdditionalDemand, + storageDemand.hasAdditionalDemand, + ) match { + case (_, true, _) => + val (updatedHouseState, thermalHouseThreshold, remainingQDotHouse) = + handleInfeedHouse(tick, ambientTemperature, state, qDot) + val (updatedStorageState, thermalStorageThreshold) = + handleInfeedStorage(tick, ambientTemperature, state, zeroKW) + val nextThreshold = determineMostRecentThreshold( + thermalHouseThreshold, + thermalStorageThreshold, + ) + ( + state.copy( + houseState = updatedHouseState, + storageState = updatedStorageState, + ), + nextThreshold, ) + case (_, _, true) => + val (updatedHouseState, thermalHouseThreshold, remainingQDotHouse) = + handleInfeedHouse(tick, ambientTemperature, state, zeroKW) + val (updatedStorageState, thermalStorageThreshold) = + handleInfeedStorage(tick, ambientTemperature, state, qDot) + val nextThreshold = determineMostRecentThreshold( + thermalHouseThreshold, + thermalStorageThreshold, + ) + ( + state.copy( + houseState = updatedHouseState, + storageState = updatedStorageState, + ), + nextThreshold, + ) + + case _ => + throw new RuntimeException( + "Heat pump is running but neither house nor storage has additional demand. This should not happen." + ) + } + + } + } + + /** Handles the case, when the house has heat demand and will be heated up + * here. + * + * @param tick + * Current tick + * @param ambientTemperature + * 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, + 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, + 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, @@ -180,54 +343,45 @@ 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, + ambientTemperature: Temperature, + 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], @@ -386,6 +540,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 { @@ -394,7 +586,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, @@ -410,7 +602,7 @@ final case class ThermalGrid( ThermalStorageState(tick, storedEnergy, qDot), ) => houseResults :+ new CylindricalStorageResult( - tick.toDateTime, + actualResultTick.toDateTime, storage.uuid, storedEnergy.toMegawattHours.asMegaWattHour, qDot.toMegawatts.asMegaWatt, @@ -483,13 +675,12 @@ object ThermalGrid { def hasRequiredDemand: Boolean = required > zeroMWH - def hasAdditionalDemand: Boolean = possible > required + def hasAdditionalDemand: Boolean = possible > zeroMWH } object ThermalEnergyDemand { /** Builds a new instance of [[ThermalEnergyDemand]]. If the possible energy - * is less than the required energy, this is considered to be a bad state - * and the required energy is curtailed to the possible energy. + * is less than the required energy, this is considered to be a bad state. * @param required * The absolutely required energy to reach target state * @param possible @@ -501,8 +692,12 @@ object ThermalGrid { required: Energy, possible: Energy, ): ThermalEnergyDemand = { - if (possible < required) - new ThermalEnergyDemand(possible, possible) + if ( + math.abs(possible.toKilowattHours) < math.abs(required.toKilowattHours) + ) + throw new InvalidParameterException( + s"The possible amount of energy {$possible} is smaller than the required amount of energy {$required}. This is not supported." + ) else new ThermalEnergyDemand(required, possible) } 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 f803502bfb..1765d5dffc 100644 --- a/src/test/scala/edu/ie3/simona/model/participant/HpModelSpec.scala +++ b/src/test/scala/edu/ie3/simona/model/participant/HpModelSpec.scala @@ -18,7 +18,7 @@ import edu.ie3.simona.ontology.messages.flex.MinMaxFlexibilityMessage.ProvideMin import edu.ie3.simona.test.common.UnitSpec import edu.ie3.simona.test.common.input.HpInputTestData import edu.ie3.util.scala.quantities.WattsPerKelvin -import org.scalatest.prop.TableDrivenPropertyChecks +import org.scalatest.prop.{TableDrivenPropertyChecks, TableFor3} import squants.energy.{KilowattHours, Kilowatts, Watts} import squants.thermal.Celsius import squants.{Kelvin, Power, Temperature} @@ -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)) @@ -232,55 +236,494 @@ class HpModelSpec } } - "determining the flexibility options" when { - "the house is heated up and storage has space" should { - "deliver positive flexibility" in { - val house = thermalHouse(18, 22) - .copy(ethLosses = WattsPerKelvin(200)) - val grid = thermalGrid(house, Some(thermalStorage)) - val hp = hpModel(grid) - // Tick, at which the house is heated up - val relevantData = hpData.copy(currentTick = 2763L) - val thermalState = ThermalGridState( - Some( - ThermalHouseState( - 0L, - Celsius(21), - Kilowatts(80), - ) + "determining the flexibility options for different states" should { + "deliver correct flexibility options" in { + val testCases + : TableFor3[ThermalGridState, HpState, (Double, Double, Double)] = + 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), ), - Some( - ThermalStorageState( - 0L, - KilowattHours(20), - Kilowatts(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))), + ), + 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(0), Kilowatts(0)) + ), + ), + None, + ), + (95.0, 95.0, 95.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(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))), + 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, + ), + (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), + ), + // 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))), + 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(19), Kilowatts(0))), + Some( + ThermalStorageState(0L, KilowattHours(20), Kilowatts(0)) + ), + ), + None, + ), + (0.0, 0.0, 95.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), + ), + // 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(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)), + ), + (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(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))), + ), + 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, + ), + (0.0, 0.0, 0.0), ), - ) - val lastState = HpState( - isRunning = true, - 0, - Some(hpData.ambientTemperature), - Kilowatts(95.0), - Kilowatts(80.0), - thermalState, - Some(HouseTemperatureUpperBoundaryReached(7995L)), ) - hp.determineFlexOptions(relevantData, lastState) match { - case ProvideMinMaxFlexOptions( - modelUuid, - referencePower, - minPower, - maxPower, - ) => - modelUuid shouldBe hp.uuid - referencePower should approximate(Kilowatts(95.0)) - minPower should approximate(Kilowatts(0.0)) - maxPower should approximate(Kilowatts(95.0)) - } + // Run the test cases + forAll(testCases) { + ( + thermalState: ThermalGridState, + lastState: HpState, + expectedValues: (Double, Double, Double), + ) => + val (expectedReferencePower, expectedMinPower, expectedMaxPower) = + expectedValues + + // Initialize the house and grid models + val house = + thermalHouse(18, 22).copy(ethLosses = WattsPerKelvin(200)) + val grid = thermalGrid(house, Some(thermalStorage)) + val hp = hpModel(grid) + + // Create relevant data for the current test + // As we are only testing flexOptions here, we can use tick 0 + // which is also the tick of the lastState. + // This should not happen in the simulation! + // This can be simplified once the transitoryData is introduced + val relevantData = hpData.copy(currentTick = + thermalState.houseState.map(_.tick).getOrElse(0L) + ) + + // Invoke determineFlexOptions and match the results + hp.determineFlexOptions(relevantData, lastState) match { + case ProvideMinMaxFlexOptions( + modelUuid, + referencePower, + minPower, + maxPower, + ) => + modelUuid shouldBe hp.uuid + referencePower should approximate( + Kilowatts(expectedReferencePower) + ) + minPower should approximate(Kilowatts(expectedMinPower)) + maxPower should approximate(Kilowatts(expectedMaxPower)) + } } } } + } } diff --git a/src/test/scala/edu/ie3/simona/model/thermal/ThermalGridSpec.scala b/src/test/scala/edu/ie3/simona/model/thermal/ThermalGridSpec.scala index e4c1c14c70..9d279ede07 100644 --- a/src/test/scala/edu/ie3/simona/model/thermal/ThermalGridSpec.scala +++ b/src/test/scala/edu/ie3/simona/model/thermal/ThermalGridSpec.scala @@ -6,6 +6,7 @@ package edu.ie3.simona.model.thermal +import edu.ie3.simona.exceptions.InvalidParameterException import edu.ie3.simona.model.thermal.ThermalGrid.ThermalEnergyDemand import edu.ie3.simona.test.common.UnitSpec import squants.energy.{MegawattHours, WattHours, Watts} @@ -20,14 +21,22 @@ class ThermalGridSpec extends UnitSpec { "Testing the thermal energy demand" when { "instantiating it from given values" should { - "correct non-sensible input" in { + "throw exception for non-sensible input (positive)" in { val possible = MegawattHours(40d) val required = MegawattHours(42d) - val energyDemand = ThermalEnergyDemand(required, possible) + intercept[InvalidParameterException] { + ThermalEnergyDemand(required, possible) + }.getMessage shouldBe s"The possible amount of energy {$possible} is smaller than the required amount of energy {$required}. This is not supported." + } - energyDemand.required should approximate(possible) - energyDemand.possible should approximate(possible) + "throw exception for non-sensible input (negative)" in { + val possible = MegawattHours(-40d) + val required = MegawattHours(-42d) + + intercept[InvalidParameterException] { + ThermalEnergyDemand(required, possible) + }.getMessage shouldBe s"The possible amount of energy {$possible} is smaller than the required amount of energy {$required}. This is not supported." } "set the correct values, if they are sensible" in { @@ -51,22 +60,30 @@ class ThermalGridSpec extends UnitSpec { } "checking for required and additional demand" should { - "return proper information, if no required but additional demand is apparent" in { + "return proper information, if no required and no additional demand is apparent" in { val required = MegawattHours(0d) - val possible = MegawattHours(45d) + val possible = MegawattHours(0d) val energyDemand = ThermalEnergyDemand(required, possible) energyDemand.hasRequiredDemand shouldBe false - energyDemand.hasAdditionalDemand shouldBe true + energyDemand.hasAdditionalDemand shouldBe false } - "return proper information, if required but no additional demand is apparent" in { - val required = MegawattHours(45d) + "return proper information, if no required but additional demand is apparent" in { + val required = MegawattHours(0d) val possible = MegawattHours(45d) val energyDemand = ThermalEnergyDemand(required, possible) - energyDemand.hasRequiredDemand shouldBe true - energyDemand.hasAdditionalDemand shouldBe false + energyDemand.hasRequiredDemand shouldBe false + energyDemand.hasAdditionalDemand shouldBe true + } + + "throw exception, if required demand is higher than possible demand" in { + val required = MegawattHours(1d) + val possible = MegawattHours(0d) + intercept[InvalidParameterException] { + ThermalEnergyDemand(required, possible) + }.getMessage shouldBe s"The possible amount of energy {$possible} is smaller than the required amount of energy {$required}. This is not supported." } "return proper information, if required and additional demand is apparent" in { 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..58dc68b2d8 100644 --- a/src/test/scala/edu/ie3/simona/model/thermal/ThermalGridTestData.scala +++ b/src/test/scala/edu/ie3/simona/model/thermal/ThermalGridTestData.scala @@ -9,7 +9,8 @@ package edu.ie3.simona.model.thermal import edu.ie3.datamodel.models.OperationTime import edu.ie3.datamodel.models.input.OperatorInput import edu.ie3.datamodel.models.input.thermal.ThermalBusInput -import squants.energy.{Kilowatts, Power} +import edu.ie3.simona.model.thermal.ThermalGrid.ThermalEnergyDemand +import squants.energy.{KilowattHours, Kilowatts, Power} import squants.thermal.{Celsius, Temperature} import java.util.UUID @@ -25,4 +26,8 @@ trait ThermalGridTestData { protected val testGridQDotInfeed: Power = Kilowatts(15d) protected val testGridQDotConsumption: Power = Kilowatts(-42d) protected val testGridQDotConsumptionHigh: Power = Kilowatts(-200d) + protected val noThermalDemand: ThermalEnergyDemand = + ThermalEnergyDemand(KilowattHours(0d), KilowattHours(0d)) + protected val thermalDemand: ThermalEnergyDemand = + ThermalEnergyDemand(KilowattHours(1d), KilowattHours(2d)) } 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 e90edcb0a3..53e026962b 100644 --- a/src/test/scala/edu/ie3/simona/model/thermal/ThermalGridWithHouseAndStorageSpec.scala +++ b/src/test/scala/edu/ie3/simona/model/thermal/ThermalGridWithHouseAndStorageSpec.scala @@ -96,51 +96,49 @@ 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, + 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 + 920) + 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(0d)) - gridDemand.possible should approximate(KilowattHours(1041.200111111)) - } - - "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, + 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)) + ) + updatedThermalGridState.storageState shouldBe Some( + ThermalStorageState(10800, KilowattHours(0d), Kilowatts(0d)) ) - gridDemand.required should approximate(KilowattHours(8.64987499999)) - gridDemand.possible should approximate(KilowattHours(1418.64987499999)) } } @@ -189,7 +187,7 @@ class ThermalGridWithHouseAndStorageSpec "take energy from storage, if there is actual consumption" in { val tick = 0L val initialGridState = ThermalGrid.startingState(thermalGrid) - val initialLoading = KilowattHours(430d) + val initialLoading = KilowattHours(200d) val gridState = initialGridState.copy(storageState = initialGridState.storageState.map(storageState => storageState.copy(storedEnergy = initialLoading) @@ -366,7 +364,7 @@ class ThermalGridWithHouseAndStorageSpec ( ThermalStorageState( tick, - KilowattHours(50d), + KilowattHours(0d), testGridQDotInfeed, ), Some(StorageEmpty(tick)), @@ -408,7 +406,7 @@ class ThermalGridWithHouseAndStorageSpec ( ThermalStorageState( tick, - KilowattHours(250d), + KilowattHours(20d), testGridQDotInfeed, ), None, @@ -429,7 +427,7 @@ class ThermalGridWithHouseAndStorageSpec val formerStorageState = Some( ThermalStorageState( 0L, - KilowattHours(300d), + KilowattHours(70d), Kilowatts(-50d), ) ) @@ -447,7 +445,7 @@ class ThermalGridWithHouseAndStorageSpec Some( ( ThermalHouseState(houseTick, _, revisedQDotHouse), - Some(HouseTemperatureUpperBoundaryReached(houseColdTick)), + Some(HouseTemperatureUpperBoundaryReached(houseWarmTick)), ) ), Some( @@ -465,8 +463,8 @@ class ThermalGridWithHouseAndStorageSpec thermalStorage.chargingPower * (-1) ) - houseColdTick shouldBe 3718L - storageEmptyTick shouldBe 3678L + houseWarmTick shouldBe 3695L + storageEmptyTick shouldBe 3663L case _ => fail("Revision of states failed") } } @@ -489,6 +487,8 @@ class ThermalGridWithHouseAndStorageSpec testGridAmbientTemperature, initialGridState, externalQDot, + thermalDemand, + noThermalDemand, ) updatedGridState match { @@ -502,7 +502,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) @@ -534,6 +534,8 @@ class ThermalGridWithHouseAndStorageSpec testGridAmbientTemperature, gridState, externalQDot, + noThermalDemand, + thermalDemand, ) updatedGridState match { @@ -557,7 +559,7 @@ class ThermalGridWithHouseAndStorageSpec case _ => fail("Thermal grid state has been calculated wrong.") } reachedThreshold shouldBe Some( - StorageFull(220800L) + StorageFull(276000L) ) } } 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 99ed500efb..20c785d1a8 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,28 @@ 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, + 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 } } @@ -126,7 +133,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) = @@ -169,6 +176,8 @@ class ThermalGridWithHouseOnlySpec extends UnitSpec with ThermalHouseTestData { testGridAmbientTemperature, gridState, testGridQDotInfeed, + thermalDemand, + noThermalDemand, ) updatedGridState match { @@ -194,6 +203,8 @@ class ThermalGridWithHouseOnlySpec extends UnitSpec with ThermalHouseTestData { ThermalGrid.startingState(thermalGrid), testGridAmbientTemperature, testGridQDotInfeed, + thermalDemand, + noThermalDemand, ) match { case ( ThermalGridState( @@ -216,6 +227,8 @@ class ThermalGridWithHouseOnlySpec extends UnitSpec with ThermalHouseTestData { ThermalGrid.startingState(thermalGrid), testGridAmbientTemperature, testGridQDotConsumption, + thermalDemand, + noThermalDemand, ) match { case ( ThermalGridState( @@ -238,6 +251,8 @@ class ThermalGridWithHouseOnlySpec extends UnitSpec with ThermalHouseTestData { ThermalGrid.startingState(thermalGrid), testGridAmbientTemperature, 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 7455f282ae..37e8cb423b 100644 --- a/src/test/scala/edu/ie3/simona/model/thermal/ThermalGridWithStorageOnlySpec.scala +++ b/src/test/scala/edu/ie3/simona/model/thermal/ThermalGridWithStorageOnlySpec.scala @@ -80,14 +80,44 @@ class ThermalGridWithStorageOnlySpec "deliver the capabilities of the 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, + 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)) ) + } - gridDemand.required should approximate(MegawattHours(0d)) - gridDemand.possible should approximate(MegawattHours(0.92d)) + "deliver the capabilities of a half full storage" in { + val tick = 10800 // after three hours + + val (houseDemand, storageDemand, updatedThermalGridState) = + thermalGrid.energyDemandAndUpdatedState( + tick, + testGridAmbientTemperature, + ThermalGridState( + None, + Some(ThermalStorageState(0L, KilowattHours(575d), Kilowatts(0d))), + ), + ) + + 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)) + ) } } @@ -105,7 +135,7 @@ class ThermalGridWithStorageOnlySpec Some( ThermalStorageState( 0L, - KilowattHours(430d), + KilowattHours(200d), Kilowatts(0d), ) ) @@ -125,7 +155,7 @@ class ThermalGridWithStorageOnlySpec Some(ThermalStorageState(tick, storedEnergy, qDot)), ) => tick shouldBe 0L - storedEnergy should approximate(KilowattHours(430d)) + storedEnergy should approximate(KilowattHours(200d)) qDot should approximate(testGridQDotConsumptionHigh) case _ => fail("Thermal grid state has been calculated wrong.") } @@ -149,6 +179,8 @@ class ThermalGridWithStorageOnlySpec testGridAmbientTemperature, gridState, testGridQDotInfeed, + noThermalDemand, + thermalDemand, ) updatedGridState match { @@ -157,11 +189,11 @@ class ThermalGridWithStorageOnlySpec Some(ThermalStorageState(tick, storedEnergy, qDot)), ) => tick shouldBe 0L - storedEnergy should approximate(KilowattHours(230d)) + storedEnergy should approximate(KilowattHours(0d)) qDot should approximate(testGridQDotInfeed) case _ => fail("Thermal grid state has been calculated wrong.") } - reachedThreshold shouldBe Some(StorageFull(220800L)) + reachedThreshold shouldBe Some(StorageFull(276000L)) } } @@ -172,9 +204,11 @@ class ThermalGridWithStorageOnlySpec ThermalGrid.startingState(thermalGrid), testGridAmbientTemperature, testGridQDotInfeed, + noThermalDemand, + thermalDemand, ) - nextThreshold shouldBe Some(StorageFull(220800L)) + nextThreshold shouldBe Some(StorageFull(276000L)) updatedState match { case ThermalGridState( @@ -182,7 +216,7 @@ class ThermalGridWithStorageOnlySpec Some(ThermalStorageState(tick, storedEnergy, qDot)), ) => tick shouldBe 0L - storedEnergy should approximate(KilowattHours(230d)) + storedEnergy should approximate(KilowattHours(0d)) qDot should approximate(testGridQDotInfeed) case _ => fail("Thermal grid state updated failed") } @@ -197,13 +231,15 @@ class ThermalGridWithStorageOnlySpec Some( ThermalStorageState( 0L, - KilowattHours(430d), + KilowattHours(200d), Kilowatts(0d), ) ) ), testGridAmbientTemperature, testGridQDotConsumptionHigh, + thermalDemand, + noThermalDemand, ) match { case ( ThermalGridState( @@ -213,7 +249,7 @@ class ThermalGridWithStorageOnlySpec Some(StorageEmpty(thresholdTick)), ) => tick shouldBe 0L - storedEnergy should approximate(KilowattHours(430d)) + storedEnergy should approximate(KilowattHours(200d)) qDot should approximate(testGridQDotConsumptionHigh) thresholdTick shouldBe 3600L case _ => fail("Thermal grid state updated failed") @@ -226,6 +262,8 @@ class ThermalGridWithStorageOnlySpec ThermalGrid.startingState(thermalGrid), testGridAmbientTemperature, Kilowatts(0d), + noThermalDemand, + noThermalDemand, ) updatedState match { case ( @@ -236,7 +274,7 @@ class ThermalGridWithStorageOnlySpec None, ) => tick shouldBe 0L - storedEnergy should approximate(KilowattHours(230d)) + storedEnergy should approximate(KilowattHours(0d)) qDot should approximate(Megawatts(0d)) case _ => fail("Thermal grid state updated failed") diff --git a/src/test/scala/edu/ie3/simona/model/thermal/ThermalStorageTestData.scala b/src/test/scala/edu/ie3/simona/model/thermal/ThermalStorageTestData.scala index b5434bbb17..2360fb6efc 100644 --- a/src/test/scala/edu/ie3/simona/model/thermal/ThermalStorageTestData.scala +++ b/src/test/scala/edu/ie3/simona/model/thermal/ThermalStorageTestData.scala @@ -25,7 +25,7 @@ trait ThermalStorageTestData extends ThermalGridTestData { "TestThermalBus", ), getQuantity(100, StandardUnits.VOLUME), - getQuantity(20, StandardUnits.VOLUME), + getQuantity(0, StandardUnits.VOLUME), getQuantity(30, StandardUnits.TEMPERATURE), getQuantity(40, StandardUnits.TEMPERATURE), getQuantity(1.15, StandardUnits.SPECIFIC_HEAT_CAPACITY), diff --git a/src/test/scala/edu/ie3/simona/test/common/input/HpInputTestData.scala b/src/test/scala/edu/ie3/simona/test/common/input/HpInputTestData.scala index 4c5a48dfd1..c2f5fed9ea 100644 --- a/src/test/scala/edu/ie3/simona/test/common/input/HpInputTestData.scala +++ b/src/test/scala/edu/ie3/simona/test/common/input/HpInputTestData.scala @@ -117,7 +117,7 @@ trait HpInputTestData extends NodeInputTestData with ThermalGridTestData { OperatorInput.NO_OPERATOR_ASSIGNED, OperationTime.notLimited(), thermalBusInput, - KilowattHours(20d), + KilowattHours(0d), KilowattHours(500d), Kilowatts(10d), KilowattHours(0d),