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 249699c7f5..6f6656c9ad 100644 --- a/src/main/scala/edu/ie3/simona/model/thermal/ThermalGrid.scala +++ b/src/main/scala/edu/ie3/simona/model/thermal/ThermalGrid.scala @@ -40,6 +40,7 @@ import scala.jdk.CollectionConverters.SetHasAsScala final case class ThermalGrid( house: Option[ThermalHouse], storage: Option[ThermalStorage], + domesticHotWaterStorage: Option[ThermalStorage], ) extends LazyLogging { /** Determine the energy demand of the total grid at the given instance in @@ -76,7 +77,7 @@ final case class ThermalGrid( house .zip(state.houseState) .map { case (house, state) => - house.energyDemand( + house.energyDemandHeating( tick, ambientTemperature, state, @@ -131,6 +132,54 @@ final case class ThermalGrid( ) } + /** Determines if the domestic hot water storage has energy demand or not. + * + * @param tick + * Questionable tick + * @param state + * most recent state, that is valid for this model + * @return + * the needed energy for heating in the questioned tick + */ + + def energyDemandDomesticHotWaterStorage( + tick: Long, + state: ThermalGridState, + ): ThermalEnergyDemand = { + + val domesticHotWaterStorageDemand = { + domesticHotWaterStorage + .zip(state.domesticHotWaterStorageState) + .map { case (hotWaterStorage, state) => + val updatedhotWaterStorageState = + hotWaterStorage.updateState(tick, state.qDot, state)._1 + val storedEnergy = updatedhotWaterStorageState.storedEnergy + val soc = storedEnergy / hotWaterStorage.getMaxEnergyThreshold + val hotWaterStorageRequired = + if (soc < 0.5) { + hotWaterStorage.getMaxEnergyThreshold * 0.5 - storedEnergy + } else { + zeroMWH + } + + val hotWaterStoragePossible = + hotWaterStorage.getMaxEnergyThreshold - storedEnergy + ThermalEnergyDemand( + hotWaterStorageRequired, + hotWaterStoragePossible, + ) + } + .getOrElse( + ThermalEnergyDemand(zeroMWH, zeroMWH) + ) + } + + ThermalEnergyDemand( + domesticHotWaterStorageDemand.required, + domesticHotWaterStorageDemand.possible, + ) + } + /** Update the current state of the grid * * @param tick @@ -196,12 +245,25 @@ final case class ThermalGrid( // 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) + val ( + qDotHouseLastState, + qDotStorageLastState, + qDotDomesticWaterStorageLastState, + ) = state match { + case ThermalGridState( + Some(houseState), + Some(storageState), + Some(domesticWaterState), + ) => + (houseState.qDot, storageState.qDot, domesticWaterState.qDot) + case ThermalGridState(Some(houseState), Some(storageState), None) => + (houseState.qDot, storageState.qDot, zeroKW) + case ThermalGridState(Some(houseState), None, Some(domesticWaterState)) => + (houseState.qDot, zeroKW, domesticWaterState.qDot) + case ThermalGridState(Some(houseState), None, None) => + (houseState.qDot, zeroKW, zeroKW) + case ThermalGridState(None, Some(storageState), None) => + (zeroKW, storageState.qDot, zeroKW) case _ => throw new InconsistentStateException( "There should be at least a house or a storage state." @@ -608,9 +670,23 @@ object ThermalGrid { case _ => None } .toSet + val domesticHotWaterStorage: Set[ThermalStorage] = input + .storages() + .asScala + .flatMap { + + // FIXME + // case domesticHotWaterInput: DomesticHotWaterStorageInput => + // Some(DomesticHotWaterStorageInput(domesticHotWaterInput)) + case cylindricalInput: CylindricalStorageInput => + Some(CylindricalThermalStorage(cylindricalInput)) + case _ => None + } + .toSet new ThermalGrid( houses.headOption, storages.headOption, + domesticHotWaterStorage.headOption, ) } @@ -623,12 +699,14 @@ object ThermalGrid { final case class ThermalGridState( houseState: Option[ThermalHouseState], storageState: Option[ThermalStorageState], + domesticHotWaterStorageState: Option[ThermalStorageState], ) def startingState(thermalGrid: ThermalGrid): ThermalGridState = ThermalGridState( thermalGrid.house.map(house => ThermalHouse.startingState(house)), thermalGrid.storage.map(_.startingState), + thermalGrid.domesticHotWaterStorage.map(_.startingState), ) /** Defines the thermal energy demand of a thermal grid. It comprises the diff --git a/src/main/scala/edu/ie3/simona/model/thermal/ThermalHouse.scala b/src/main/scala/edu/ie3/simona/model/thermal/ThermalHouse.scala index c08a6a3114..31f86bb98f 100644 --- a/src/main/scala/edu/ie3/simona/model/thermal/ThermalHouse.scala +++ b/src/main/scala/edu/ie3/simona/model/thermal/ThermalHouse.scala @@ -25,12 +25,15 @@ import edu.ie3.util.quantities.PowerSystemUnits import edu.ie3.util.scala.quantities.DefaultQuantities._ import edu.ie3.util.scala.quantities.{ThermalConductance, WattsPerKelvin} import squants.energy.KilowattHours -import squants.thermal.{Kelvin, ThermalCapacity} +import squants.space.Litres +import squants.thermal.{Celsius, Kelvin, ThermalCapacity} import squants.time.{Hours, Seconds} -import squants.{Energy, Power, Temperature, Time} +import squants.{Energy, Power, Temperature, Time, Volume} import tech.units.indriya.unit.Units +import java.time.ZonedDateTime import java.util.UUID +import java.time.Duration /** A thermal house model * @@ -74,10 +77,10 @@ final case class ThermalHouse( bus, ) { - /** Calculate the energy demand at the instance in question. If the inner - * temperature is at or above the lower boundary temperature, there is no - * demand. If it is below the target temperature, the demand is the energy - * needed to heat up the house to the maximum temperature. The current + /** Calculate the energy demand for heating at the instance in question. If + * the inner temperature is at or above the lower boundary temperature, there + * is no demand. If it is below the target temperature, the demand is the + * energy needed to heat up the house to the maximum temperature. The current * (external) thermal infeed is not accounted for, as we assume, that after * determining the thermal demand, a change in external infeed will take * place. @@ -89,9 +92,9 @@ final case class ThermalHouse( * @param state * most recent state, that is valid for this model * @return - * the needed energy in the questioned tick + * the needed energy for heating in the questioned tick */ - def energyDemand( + def energyDemandHeating( tick: Long, ambientTemperature: Temperature, state: ThermalHouseState, @@ -135,6 +138,158 @@ final case class ThermalHouse( ThermalEnergyDemand(requiredEnergy, possibleEnergy) } + /** Calculate the energy demand for warm water at the instance in question. + * + * @param tick + * Questionable tick + * @param state + * most recent state, that is valid for this model + * @return + * the needed energy for heating in the questioned tick + */ + + def energyDemandWater( + tick: Long, + state: ThermalHouseState, + simulationStart: ZonedDateTime, + ): ThermalEnergyDemand = { + + // Fixme: + val noPersonsInHoushold = 2 + + val lastStateTime: ZonedDateTime = simulationStart.plusSeconds(state.tick) + val actualStateTime: ZonedDateTime = simulationStart.plusSeconds(tick) + val timeDiff: Duration = Duration.between(lastStateTime, actualStateTime) + + // if we have been triggered this hour there the water Demand is already covered + if (timeDiff.toSeconds > 3600) { + + // FIXME + // the lastStates hour is already considered. Need to start lastState.getHour + 1 until tick.getHour + + ThermalEnergyDemand(zeroKWH, zeroKWH) + } else if (actualStateTime.getHour != lastStateTime.getHour) { + // determine demand for the actual hour + + val waterDemand = + waterDemandOfHour(tick, noPersonsInHoushold, simulationStart) + + val thermalDemandWater: Energy = + thermalEnergyDemandWater(waterDemand, Celsius(10d), Celsius(55d)) + ThermalEnergyDemand(thermalDemandWater, thermalDemandWater) + + } else ThermalEnergyDemand(zeroKWH, zeroKWH) + + } + + /** Calculate the energy required to heat up a given volume of water from a + * start to an end temperature + * + * @param waterDemand + * water volume to get heated up + * @param startTemperature + * starting Temperature + * @param endTemperature + * end Temperature + * @return + * the needed energy + */ + + private def thermalEnergyDemandWater( + waterDemand: Volume, + startTemperature: Temperature, + endTemperature: Temperature, + ): Energy = { + if (endTemperature < startTemperature) + throw new RuntimeException( + s"End temperature of $endTemperature is lower than the start temperature $startTemperature for the water heating system" + ) + + waterDemand.toLitres * KilowattHours( + 1.16 + ) * (endTemperature.toCelsiusDegrees - startTemperature.toCelsiusDegrees) + } + + def waterDemandOfHour( + tick: Long, + noPersonsInHoushold: Int, + simulationStart: ZonedDateTime, + ): Volume = { + + // Volume => VDI 2067 Blatt 12 + // Time series relative => DIN EN 12831-3 Table B.2 for single family houses and for flats + val waterVolumeRelativeHouse: Map[Int, Double] = Map( + 0 -> 0.018, + 1 -> 0.01, + 2 -> 0.006, + 3 -> 0.003, + 4 -> 0.004, + 5 -> 0.006, + 6 -> 0.024, + 7 -> 0.047, + 8 -> 0.068, + 9 -> 0.057, + 10 -> 0.061, + 11 -> 0.061, + 12 -> 0.063, + 13 -> 0.064, + 14 -> 0.051, + 15 -> 0.044, + 16 -> 0.043, + 17 -> 0.047, + 18 -> 0.057, + 19 -> 0.065, + 20 -> 0.066, + 21 -> 0.058, + 22 -> 0.045, + 23 -> 0.031, + ) + val waterVolumeRelativeFlat: Map[Int, Double] = Map( + 0 -> 0.01, + 1 -> 0.01, + 2 -> 0.01, + 3 -> 0.0, + 4 -> 0.0, + 5 -> 0.1, + 6 -> 0.03, + 7 -> 0.06, + 8 -> 0.08, + 9 -> 0.06, + 10 -> 0.05, + 11 -> 0.05, + 12 -> 0.06, + 13 -> 0.06, + 14 -> 0.05, + 15 -> 0.04, + 16 -> 0.04, + 17 -> 0.05, + 18 -> 0.06, + 19 -> 0.07, + 20 -> 0.07, + 21 -> 0.06, + 22 -> 0.05, + 23 -> 0.02, + ) + + val waterDemandVolumePerPersonYear = + // Shower and Bath + washbasin + dish washing per hand (also dish washer in the building) + Litres(8600 + 4200 + 300) + + val isHouse = true + + val currentHour: Int = simulationStart.plusSeconds(tick).getHour + + val waterVolumeRelative: Map[Int, Double] = + if (isHouse) waterVolumeRelativeHouse else waterVolumeRelativeFlat + + waterVolumeRelative.getOrElse( + currentHour, + throw new RuntimeException( + "Couldn't get the actual hour to determine water demand" + ), + ) * noPersonsInHoushold * waterDemandVolumePerPersonYear / 365 + } + /** Calculate the needed energy to change from start temperature to target * temperature * 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 6dec2fc25c..283b2f4171 100644 --- a/src/test/scala/edu/ie3/simona/model/participant/HpModelSpec.scala +++ b/src/test/scala/edu/ie3/simona/model/participant/HpModelSpec.scala @@ -213,7 +213,7 @@ class HpModelSpec _, activePower, _, - ThermalGridState(Some(thermalHouseState), _), + ThermalGridState(Some(thermalHouseState), _, _), maybeThreshold, ) => isRunning shouldBe expectedRunningState @@ -255,6 +255,7 @@ class HpModelSpec Kilowatts(0), ) ), + None, ) val lastState = HpState( isRunning = true, diff --git a/src/test/scala/edu/ie3/simona/model/participant/HpModelTestData.scala b/src/test/scala/edu/ie3/simona/model/participant/HpModelTestData.scala index cbbe51ba9e..9e846e3eb5 100644 --- a/src/test/scala/edu/ie3/simona/model/participant/HpModelTestData.scala +++ b/src/test/scala/edu/ie3/simona/model/participant/HpModelTestData.scala @@ -91,6 +91,7 @@ trait HpModelTestData { ThermalGrid( Some(thermalHouse), thermalStorage, + None, ) private val thermHouseUuid: UUID = @@ -142,6 +143,7 @@ trait HpModelTestData { ) ), None, + None, ) protected def hpData: HpRelevantData = 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 34cc9f5b0e..0b756af29c 100644 --- a/src/test/scala/edu/ie3/simona/model/thermal/ThermalGridWithHouseAndStorageSpec.scala +++ b/src/test/scala/edu/ie3/simona/model/thermal/ThermalGridWithHouseAndStorageSpec.scala @@ -48,6 +48,7 @@ class ThermalGridWithHouseAndStorageSpec case ThermalGrid( Some(thermalHouseGenerated), Some(thermalStorageGenerated), + None, ) => thermalHouseGenerated shouldBe thermalHouse thermalStorageGenerated shouldBe thermalStorage @@ -74,6 +75,7 @@ class ThermalGridWithHouseAndStorageSpec Some( ThermalStorageState(storageTick, storedEnergy, qDotStorage) ), + None, ) => houseTick shouldBe expectedHouseStartingState.tick storageTick shouldBe expectedHouseStartingState.tick @@ -160,6 +162,7 @@ class ThermalGridWithHouseAndStorageSpec Some( ThermalStorageState(storageTick, storedEnergy, qDotStorage) ), + None, ) => storageTick shouldBe 0L storedEnergy should approximate(initialLoading) @@ -196,6 +199,7 @@ class ThermalGridWithHouseAndStorageSpec Some( ThermalStorageState(storageTick, storedEnergy, qDotStorage) ), + None, ) => houseTick shouldBe 0L innerTemperature should approximate(Celsius(18.9999d)) @@ -484,6 +488,7 @@ class ThermalGridWithHouseAndStorageSpec Some( ThermalStorageState(storageTick, storedEnergy, qDotStorage) ), + None, ) => houseTick shouldBe 0L innerTemperature should approximate(Celsius(18.9999d)) @@ -531,6 +536,7 @@ class ThermalGridWithHouseAndStorageSpec Some( ThermalStorageState(storageTick, storedEnergy, qDotStorage) ), + None, ) => houseTick shouldBe 0L innerTemperature should approximate(Celsius(20.99999167d)) 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 0dbe5f9625..9522039839 100644 --- a/src/test/scala/edu/ie3/simona/model/thermal/ThermalGridWithHouseOnlySpec.scala +++ b/src/test/scala/edu/ie3/simona/model/thermal/ThermalGridWithHouseOnlySpec.scala @@ -36,7 +36,7 @@ class ThermalGridWithHouseOnlySpec extends UnitSpec with ThermalHouseTestData { ) ThermalGrid(thermalGridInput) match { - case ThermalGrid(Some(thermalHouseGenerated), None) => + case ThermalGrid(Some(thermalHouseGenerated), None, None) => thermalHouseGenerated shouldBe thermalHouse case _ => fail("Generation of thermal grid from thermal input grid failed.") @@ -59,6 +59,7 @@ class ThermalGridWithHouseOnlySpec extends UnitSpec with ThermalHouseTestData { case ThermalGridState( Some(ThermalHouseState(tick, innerTemperature, thermalInfeed)), None, + None, ) => tick shouldBe expectedHouseStartingState.tick innerTemperature should approximate( @@ -74,7 +75,7 @@ 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 expectedHouseDemand = thermalHouse.energyDemand( + val expectedHouseDemand = thermalHouse.energyDemandHeating( tick, testGridambientTemperature, expectedHouseStartingState, @@ -116,6 +117,7 @@ class ThermalGridWithHouseOnlySpec extends UnitSpec with ThermalHouseTestData { case ThermalGridState( Some(ThermalHouseState(tick, innerTemperature, qDot)), None, + None, ) => tick shouldBe 0L innerTemperature should approximate(Celsius(18.9999d)) @@ -143,6 +145,7 @@ class ThermalGridWithHouseOnlySpec extends UnitSpec with ThermalHouseTestData { case ThermalGridState( Some(ThermalHouseState(tick, innerTemperature, qDot)), None, + None, ) => tick shouldBe 0L innerTemperature should approximate(Celsius(18.9999d)) @@ -179,6 +182,7 @@ class ThermalGridWithHouseOnlySpec extends UnitSpec with ThermalHouseTestData { case ThermalGridState( Some(ThermalHouseState(tick, innerTemperature, qDot)), None, + None, ) => tick shouldBe 0L innerTemperature should approximate(Celsius(18.9999d)) @@ -205,6 +209,7 @@ class ThermalGridWithHouseOnlySpec extends UnitSpec with ThermalHouseTestData { ThermalGridState( Some(ThermalHouseState(tick, innerTemperature, qDot)), None, + None, ), Some(HouseTemperatureUpperBoundaryReached(thresholdTick)), ) => @@ -229,6 +234,7 @@ class ThermalGridWithHouseOnlySpec extends UnitSpec with ThermalHouseTestData { ThermalGridState( Some(ThermalHouseState(tick, innerTemperature, qDot)), None, + None, ), Some(HouseTemperatureLowerBoundaryReached(thresholdTick)), ) => @@ -253,6 +259,7 @@ class ThermalGridWithHouseOnlySpec extends UnitSpec with ThermalHouseTestData { ThermalGridState( Some(ThermalHouseState(tick, innerTemperature, qDot)), None, + None, ), Some(HouseTemperatureLowerBoundaryReached(thresholdTick)), ) => 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 ada5195e33..1592e38c7a 100644 --- a/src/test/scala/edu/ie3/simona/model/thermal/ThermalGridWithStorageOnlySpec.scala +++ b/src/test/scala/edu/ie3/simona/model/thermal/ThermalGridWithStorageOnlySpec.scala @@ -41,7 +41,7 @@ class ThermalGridWithStorageOnlySpec ) ThermalGrid(thermalGridInput) match { - case ThermalGrid(None, Some(thermalStorageGenerated)) => + case ThermalGrid(None, Some(thermalStorageGenerated), None) => thermalStorageGenerated shouldBe thermalStorage case _ => fail("Generation of thermal grid from thermal input grid failed.") @@ -64,6 +64,7 @@ class ThermalGridWithStorageOnlySpec case ThermalGridState( None, Some(ThermalStorageState(tick, storedEnergy, qDot)), + None, ) => tick shouldBe expectedStorageStartingState.tick storedEnergy should approximate( @@ -125,6 +126,7 @@ class ThermalGridWithStorageOnlySpec case ThermalGridState( None, Some(ThermalStorageState(tick, storedEnergy, qDot)), + None, ) => tick shouldBe 0L storedEnergy should approximate(KilowattHours(430d)) @@ -159,6 +161,7 @@ class ThermalGridWithStorageOnlySpec case ThermalGridState( None, Some(ThermalStorageState(tick, storedEnergy, qDot)), + None, ) => tick shouldBe 0L storedEnergy should approximate(KilowattHours(230d)) @@ -186,6 +189,7 @@ class ThermalGridWithStorageOnlySpec case ThermalGridState( None, Some(ThermalStorageState(tick, storedEnergy, qDot)), + None, ) => tick shouldBe 0L storedEnergy should approximate(KilowattHours(230d)) @@ -217,6 +221,7 @@ class ThermalGridWithStorageOnlySpec ThermalGridState( None, Some(ThermalStorageState(tick, storedEnergy, qDot)), + None, ), Some(StorageEmpty(thresholdTick)), ) => @@ -242,6 +247,7 @@ class ThermalGridWithStorageOnlySpec ThermalGridState( None, Some(ThermalStorageState(tick, storedEnergy, qDot)), + None, ), None, ) =>