diff --git a/CHANGELOG.md b/CHANGELOG.md index 523347d6ff..e55515f198 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added `ApparentPower` to differentiate between different power types [#794](https://github.com/ie3-institute/simona/issues/794) - Update/enhance config documentation [#1013](https://github.com/ie3-institute/simona/issues/1013) - Create `CITATION.cff` [#1035](https://github.com/ie3-institute/simona/issues/1035) +- Introduce ThermalDemandWrapper [#1049](https://github.com/ie3-institute/simona/issues/1049) ### Changed - Adapted to changed data source in PSDM [#435](https://github.com/ie3-institute/simona/issues/435) 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 ed146cab9e..5911a9cade 100644 --- a/src/main/scala/edu/ie3/simona/model/participant/HpModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant/HpModel.scala @@ -12,7 +12,7 @@ import edu.ie3.simona.model.SystemComponent import edu.ie3.simona.model.participant.HpModel.{HpRelevantData, HpState} import edu.ie3.simona.model.participant.control.QControl import edu.ie3.simona.model.thermal.ThermalGrid.{ - ThermalEnergyDemand, + ThermalDemandWrapper, ThermalGridState, } import edu.ie3.simona.model.thermal.{ThermalGrid, ThermalThreshold} @@ -22,8 +22,8 @@ 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.{ApparentPower, Kilovoltamperes} -import squants.energy.{KilowattHours, Kilowatts} -import squants.{Energy, Power, Temperature} +import squants.energy.Kilowatts +import squants.{Power, Temperature} import java.time.ZonedDateTime import java.util.UUID @@ -132,7 +132,7 @@ final case class HpModel( ): (Boolean, Boolean, HpState) = { // Use lastHpState and relevantData to update state of thermalGrid to the current tick - val (demandHouse, demandThermalStorage, currentThermalGridState) = + val (thermalDemandWrapper, currentThermalGridState) = thermalGrid.energyDemandAndUpdatedState( relevantData, lastHpState, @@ -144,8 +144,7 @@ final case class HpModel( lastHpState, currentThermalGridState, relevantData, - demandHouse, - demandThermalStorage, + thermalDemandWrapper, ) // Updating the HpState @@ -166,10 +165,8 @@ final case class HpModel( * to current tick updated state of the thermalGrid * @param relevantData * Relevant (external) data - * @param demandHouse - * ThermalEnergyDemand of the house - * @param demandThermalStorage - * ThermalEnergyDemand of the thermal storage + * @param thermalDemands + * ThermalEnergyDemand of the house and the thermal storage * @return * boolean defining if heat pump runs in next time step, if it can be in * operation and can be out of operation @@ -178,23 +175,19 @@ final case class HpModel( lastState: HpState, currentThermalGridState: ThermalGridState, relevantData: HpRelevantData, - demandHouse: ThermalEnergyDemand, - demandThermalStorage: ThermalEnergyDemand, + thermalDemands: ThermalDemandWrapper, ): (Boolean, Boolean, Boolean) = { - val ( - houseHasDemand, - heatStorageHasDemand, - noThermalStorageOrThermalStorageIsEmpty, - ) = determineDemandBooleans( - lastState, - currentThermalGridState, - demandHouse, - demandThermalStorage, - ) + val demandHouse = thermalDemands.houseDemand + val demandThermalStorage = thermalDemands.heatStorageDemand + val noThermalStorageOrThermalStorageIsEmpty = + currentThermalGridState.isThermalStorageEmpty - val turnHpOn: Boolean = - houseHasDemand || heatStorageHasDemand + val turnHpOn = + (demandHouse.hasRequiredDemand && noThermalStorageOrThermalStorageIsEmpty) || + (demandHouse.hasAdditionalDemand && lastState.isRunning) || + demandThermalStorage.hasRequiredDemand || + (demandThermalStorage.hasAdditionalDemand && lastState.isRunning) val canOperate = demandHouse.hasRequiredDemand || demandHouse.hasAdditionalDemand || @@ -202,45 +195,11 @@ final case class HpModel( val canBeOutOfOperation = !(demandHouse.hasRequiredDemand && noThermalStorageOrThermalStorageIsEmpty) - (turnHpOn, canOperate, canBeOutOfOperation) - } - - /** This method will return booleans whether there is a heat demand of house - * or thermal storage as well as a boolean indicating if there is no thermal - * storage, or it is empty. - * - * @param lastHpState - * Current state of the heat pump - * @param updatedGridState - * The updated state of the [[ThermalGrid]] - * @param demandHouse - * heat demand of the thermal house - * @param demandThermalStorage - * heat demand of the thermal storage - * @return - * First boolean is true, if house has heat demand. Second boolean is true, - * if thermalStorage has heat demand. Third boolean is true, if there is no - * thermalStorage, or it's empty. - */ - - private def determineDemandBooleans( - lastHpState: HpState, - updatedGridState: ThermalGridState, - demandHouse: ThermalEnergyDemand, - demandThermalStorage: ThermalEnergyDemand, - ): (Boolean, Boolean, Boolean) = { - implicit val tolerance: Energy = KilowattHours(1e-3) - val noThermalStorageOrThermalStorageIsEmpty: Boolean = - updatedGridState.storageState.isEmpty || updatedGridState.storageState - .exists( - _.storedEnergy =~ zeroKWh - ) - - val houseDemand = - (demandHouse.hasRequiredDemand && noThermalStorageOrThermalStorageIsEmpty) || (lastHpState.isRunning && demandHouse.hasAdditionalDemand) - val heatStorageDemand = - demandThermalStorage.hasRequiredDemand || (lastHpState.isRunning && demandThermalStorage.hasAdditionalDemand) - (houseDemand, heatStorageDemand, noThermalStorageOrThermalStorageIsEmpty) + ( + turnHpOn, + canOperate, + canBeOutOfOperation, + ) } /** Calculate state depending on whether heat pump is needed or not. Also @@ -342,7 +301,7 @@ final case class HpModel( lastState: HpState, setPower: Power, ): (HpState, FlexChangeIndicator) = { - /* If the setpoint value is above 50 % of the electrical power, turn on the heat pump otherwise turn it off */ + /* If the set point value is above 50 % of the electrical power, turn on the heat pump otherwise turn it off */ val turnOn = setPower > (sRated.toActivePower(cosPhiRated) * 0.5) val updatedHpState = calcState( 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 e1d993f119..32e1870a03 100644 --- a/src/main/scala/edu/ie3/simona/model/thermal/ThermalGrid.scala +++ b/src/main/scala/edu/ie3/simona/model/thermal/ThermalGrid.scala @@ -16,6 +16,7 @@ import edu.ie3.datamodel.models.result.thermal.{ import edu.ie3.simona.exceptions.agent.InconsistentStateException import edu.ie3.simona.model.participant.HpModel.{HpRelevantData, HpState} import edu.ie3.simona.model.thermal.ThermalGrid.{ + ThermalDemandWrapper, ThermalEnergyDemand, ThermalGridState, } @@ -24,7 +25,7 @@ import edu.ie3.simona.model.thermal.ThermalStorage.ThermalStorageState 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.{KilowattHours, Kilowatts} import squants.{Energy, Power, Temperature} import java.time.ZonedDateTime @@ -57,7 +58,7 @@ final case class ThermalGrid( def energyDemandAndUpdatedState( relevantData: HpRelevantData, lastHpState: HpState, - ): (ThermalEnergyDemand, ThermalEnergyDemand, ThermalGridState) = { + ): (ThermalDemandWrapper, ThermalGridState) = { /* First get the energy demand of the houses but only if inner temperature is below target temperature */ val (houseDemand, updatedHouseState) = @@ -130,13 +131,15 @@ final case class ThermalGrid( } ( - ThermalEnergyDemand( - houseDemand.required, - houseDemand.possible, - ), - ThermalEnergyDemand( - storageDemand.required, - storageDemand.possible, + ThermalDemandWrapper( + ThermalEnergyDemand( + houseDemand.required, + houseDemand.possible, + ), + ThermalEnergyDemand( + storageDemand.required, + storageDemand.possible, + ), ), ThermalGridState(updatedHouseState, updatedStorageState), ) @@ -528,7 +531,23 @@ object ThermalGrid { final case class ThermalGridState( houseState: Option[ThermalHouseState], storageState: Option[ThermalStorageState], - ) + ) { + + /** This method will return booleans whether there is a heat demand of house + * or thermal storage as well as a boolean indicating if there is no + * thermal storage, or it is empty. + * + * @return + * boolean which is true, if there is no thermalStorage, or it's empty. + */ + def isThermalStorageEmpty: Boolean = { + implicit val tolerance: Energy = KilowattHours(1e-3) + storageState.isEmpty || storageState + .exists( + _.storedEnergy =~ zeroKWh + ) + } + } def startingState(thermalGrid: ThermalGrid): ThermalGridState = ThermalGridState( @@ -536,6 +555,18 @@ object ThermalGrid { thermalGrid.storage.map(_.startingState), ) + /** Wraps the demand of thermal units (thermal house, thermal storage). + * + * @param houseDemand + * the demand of the thermal house + * @param heatStorageDemand + * the demand of the thermal heat storage + */ + final case class ThermalDemandWrapper private ( + houseDemand: ThermalEnergyDemand, + heatStorageDemand: ThermalEnergyDemand, + ) + /** Defines the thermal energy demand of a thermal grid. It comprises the * absolutely required energy demand to reach the target state as well as an * energy, that can be handled. The possible energy always has to be greater 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..bb34a88682 100644 --- a/src/test/scala/edu/ie3/simona/model/thermal/ThermalGridSpec.scala +++ b/src/test/scala/edu/ie3/simona/model/thermal/ThermalGridSpec.scala @@ -6,13 +6,19 @@ package edu.ie3.simona.model.thermal +import edu.ie3.datamodel.models.input.thermal.ThermalStorageInput import edu.ie3.simona.model.thermal.ThermalGrid.ThermalEnergyDemand import edu.ie3.simona.test.common.UnitSpec -import squants.energy.{MegawattHours, WattHours, Watts} +import squants.energy.{KilowattHours, MegawattHours, WattHours, Watts} import squants.thermal.Celsius import squants.{Energy, Power, Temperature} -class ThermalGridSpec extends UnitSpec { +import scala.jdk.CollectionConverters._ + +class ThermalGridSpec + extends UnitSpec + with ThermalHouseTestData + with ThermalStorageTestData { implicit val tempTolerance: Temperature = Celsius(1e-3) implicit val powerTolerance: Power = Watts(1e-3) @@ -97,4 +103,44 @@ class ThermalGridSpec extends UnitSpec { } } } + "ThermalGridState" should { + val thermalGridOnlyHouse = ThermalGrid( + new edu.ie3.datamodel.models.input.container.ThermalGrid( + thermalBusInput, + Set(thermalHouseInput).asJava, + Set.empty[ThermalStorageInput].asJava, + ) + ) + + "return true when there is no storage" in { + val initialState = ThermalGrid.startingState(thermalGridOnlyHouse) + val result = initialState.isThermalStorageEmpty + result shouldBe true + } + + val thermalGrid = ThermalGrid( + new edu.ie3.datamodel.models.input.container.ThermalGrid( + thermalBusInput, + Set(thermalHouseInput).asJava, + Set[ThermalStorageInput](thermalStorageInput).asJava, + ) + ) + + "return true when all stored energy is effectively zero" in { + val initialState = ThermalGrid.startingState(thermalGrid) + val result = initialState.isThermalStorageEmpty + result shouldBe true + } + + "return false when storage is not empty" in { + val initialState = ThermalGrid.startingState(thermalGrid) + val gridState = initialState.copy(storageState = + initialState.storageState.map(storageState => + storageState.copy(storedEnergy = KilowattHours(1)) + ) + ) + val result = gridState.isThermalStorageEmpty + result shouldBe false + } + } } 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..df410786c5 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,12 @@ 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.{ + ThermalDemandWrapper, + ThermalEnergyDemand, +} +import edu.ie3.util.scala.quantities.DefaultQuantities.zeroKWh +import squants.energy.{KilowattHours, Kilowatts, Power} import squants.thermal.{Celsius, Temperature} import java.util.UUID @@ -25,4 +30,21 @@ trait ThermalGridTestData { protected val testGridQDotInfeed: Power = Kilowatts(15d) protected val testGridQDotConsumption: Power = Kilowatts(-42d) protected val testGridQDotConsumptionHigh: Power = Kilowatts(-200d) + protected val noThermalDemand: ThermalDemandWrapper = + ThermalDemandWrapper( + ThermalEnergyDemand(zeroKWh, zeroKWh), + ThermalEnergyDemand(zeroKWh, zeroKWh), + ) + protected val onlyThermalDemandOfHouse: ThermalDemandWrapper = + ThermalDemandWrapper( + ThermalEnergyDemand(KilowattHours(1), KilowattHours(2)), + ThermalEnergyDemand(zeroKWh, zeroKWh), + ) + protected val onlyThermalDemandOfHeatStorage: ThermalDemandWrapper = + ThermalDemandWrapper( + ThermalEnergyDemand(zeroKWh, zeroKWh), + ThermalEnergyDemand(KilowattHours(1), KilowattHours(2)), + ) + protected val isRunning: Boolean = true + protected val isNotRunning: Boolean = false } diff --git a/src/test/scala/edu/ie3/simona/model/thermal/ThermalGridWithHouseAndStorageSpec.scala b/src/test/scala/edu/ie3/simona/model/thermal/ThermalGridWithHouseAndStorageSpec.scala index aef9c642ea..5d19bfdd73 100644 --- a/src/test/scala/edu/ie3/simona/model/thermal/ThermalGridWithHouseAndStorageSpec.scala +++ b/src/test/scala/edu/ie3/simona/model/thermal/ThermalGridWithHouseAndStorageSpec.scala @@ -14,13 +14,13 @@ import edu.ie3.simona.model.thermal.ThermalHouse.ThermalHouseThreshold.{ HouseTemperatureLowerBoundaryReached, HouseTemperatureUpperBoundaryReached, } -import edu.ie3.util.scala.quantities.DefaultQuantities.{zeroKW, zeroKWh} import edu.ie3.simona.model.thermal.ThermalStorage.ThermalStorageState import edu.ie3.simona.model.thermal.ThermalStorage.ThermalStorageThreshold.{ StorageEmpty, StorageFull, } import edu.ie3.simona.test.common.UnitSpec +import edu.ie3.util.scala.quantities.DefaultQuantities.{zeroKW, zeroKWh} import squants.energy._ import squants.thermal.Celsius import squants.{Energy, Kelvin, Power, Temperature} @@ -109,11 +109,14 @@ class ThermalGridWithHouseAndStorageSpec ThermalGrid.startingState(thermalGrid), None, ) - val (houseDemand, storageDemand, updatedThermalGridState) = + val (thermalDemands, updatedThermalGridState) = thermalGrid.energyDemandAndUpdatedState( relevantData, lastHpState, ) + val houseDemand = thermalDemands.houseDemand + val storageDemand = thermalDemands.heatStorageDemand + houseDemand.required should approximate(zeroKWh) houseDemand.possible should approximate(KilowattHours(31.05009722d)) storageDemand.required should approximate(KilowattHours(1150d)) @@ -146,11 +149,13 @@ class ThermalGridWithHouseAndStorageSpec None, ) - val (houseDemand, storageDemand, updatedThermalGridState) = + val (thermalDemands, updatedThermalGridState) = thermalGrid.energyDemandAndUpdatedState( relevantData, lastHpState, ) + val houseDemand = thermalDemands.houseDemand + val storageDemand = thermalDemands.heatStorageDemand houseDemand.required should approximate(KilowattHours(45.6000555)) houseDemand.possible should approximate(KilowattHours(75.600055555)) 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 9d7e3e7a54..b89b649584 100644 --- a/src/test/scala/edu/ie3/simona/model/thermal/ThermalGridWithHouseOnlySpec.scala +++ b/src/test/scala/edu/ie3/simona/model/thermal/ThermalGridWithHouseOnlySpec.scala @@ -94,12 +94,15 @@ class ThermalGridWithHouseOnlySpec extends UnitSpec with ThermalHouseTestData { expectedHouseStartingState, ) - val (houseDemand, storageDemand, updatedThermalGridState) = + val (thermalDemands, updatedThermalGridState) = thermalGrid.energyDemandAndUpdatedState( relevantData, lastHpState, ) + val houseDemand = thermalDemands.houseDemand + val storageDemand = thermalDemands.heatStorageDemand + houseDemand.required should approximate(expectedHouseDemand.required) houseDemand.possible should approximate(expectedHouseDemand.possible) storageDemand.required should approximate(zeroKWh) 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 e6986edb7f..c251217f1a 100644 --- a/src/test/scala/edu/ie3/simona/model/thermal/ThermalGridWithStorageOnlySpec.scala +++ b/src/test/scala/edu/ie3/simona/model/thermal/ThermalGridWithStorageOnlySpec.scala @@ -94,11 +94,13 @@ class ThermalGridWithStorageOnlySpec None, ) - val (houseDemand, storageDemand, updatedThermalGridState) = + val (thermalDemands, updatedThermalGridState) = thermalGrid.energyDemandAndUpdatedState( relevantData, lastHpState, ) + val houseDemand = thermalDemands.houseDemand + val storageDemand = thermalDemands.heatStorageDemand houseDemand.required should approximate(zeroKWh) houseDemand.possible should approximate(zeroKWh) @@ -128,11 +130,13 @@ class ThermalGridWithStorageOnlySpec None, ) - val (houseDemand, storageDemand, updatedThermalGridState) = + val (thermalDemands, updatedThermalGridState) = thermalGrid.energyDemandAndUpdatedState( relevantData, lastHpState, ) + val houseDemand = thermalDemands.houseDemand + val storageDemand = thermalDemands.heatStorageDemand houseDemand.required should approximate(zeroKWh) houseDemand.possible should approximate(zeroKWh)