diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6d58167b6c..d11126c218 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,15 +30,29 @@ jobs: with: fetch-depth: 0 + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Check Branch + run: | + if [ "${{ github.event_name }}" == "pull_request" ]; then + branchName="${{ github.head_ref }}" + else + branchName="${{ github.ref_name }}" + fi + + if [[ "$branchName" == refs/heads/* ]]; then + branchName="${branchName#refs/heads/}" + fi + + ./gradlew checkBranchName -PbranchName="$branchName" + - name: Setup Java uses: actions/setup-java@v4 with: distribution: 'temurin' java-version: 17 - - name: Setup Gradle - uses: gradle/actions/setup-gradle@v4 - - name: Build Project run: ./gradlew --refresh-dependencies clean assemble spotlessCheck diff --git a/CHANGELOG.md b/CHANGELOG.md index 96de72a529..8cef0ae02e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add unapply method for ThermalHouseResults [#934](https://github.com/ie3-institute/simona/issues/934) - 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) - Integration test for thermal grids [#878](https://github.com/ie3-institute/simona/issues/878) - Integration test for thermal grids [#878](https://github.com/ie3-institute/simona/issues/878) @@ -106,6 +108,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Refactor `ResultFileHierarchy` [#1031](https://github.com/ie3-institute/simona/issues/1031) - Removing logs in `logs/simona` [#1017](https://github.com/ie3-institute/simona/issues/1017) - Fix implausible test cases of HpModelSpec [#1042](https://github.com/ie3-institute/simona/issues/1042) +- Refactoring to only use 'lastHpState' and 'relevantData' for 'ThermalGrid' calculations [#916](https://github.com/ie3-institute/simona/issues/916) ### Fixed - Fix rendering of references in documentation [#505](https://github.com/ie3-institute/simona/issues/505) diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 0000000000..0008300e9f --- /dev/null +++ b/CITATION.cff @@ -0,0 +1,41 @@ +cff-version: 1.2.0 +title: SIMONA +message: "If you use this software, please cite it as below." +type: software +authors: + - family-names: Hiry + given-names: Johannes + orcid: https://orcid.org/0000-0002-1447-0607 + - family-names: Kittl + given-names: Chris + orcid: https://orcid.org/0000-0002-1187-0568 + - family-names: Sen Sarma + given-names: Debopama + orcid: https://orcid.org/0000-0003-3311-3020 + - family-names: Oberließen + given-names: Thomas + orcid: https://orcid.org/0000-0001-5805-5408 + - family-names: Peter + given-names: Sebastian + orcid: https://orcid.org/0000-0001-6311-6113 + - family-names: Feismann + given-names: Daniel + orcid: https://orcid.org/0000-0002-3531-9025 + - family-names: Bao + given-names: Johannes + orcid: https://orcid.org/0009-0008-3641-6469 + - family-names: Hohmann + given-names: Julian + - family-names: Staudt + given-names: Marius +repository-code: https://github.com/ie3-institute/simona +url: https://simona.ie3.e-technik.tu-dortmund.de +repository-artifact: https://central.sonatype.com/artifact/com.github.ie3-institute/simona +keywords: + - agent-based + - discrete-event simulation + - powerflow + - electricity distribution grid +license: BSD-3-Clause +version: 3.0.0 +date-released: 2023-08-07 diff --git a/build.gradle b/build.gradle index a69e01e63d..3125cfe07e 100644 --- a/build.gradle +++ b/build.gradle @@ -54,6 +54,7 @@ apply from: scriptsLocation + 'scoverage.gradle' // scoverage scala code coverag apply from: scriptsLocation + 'deploy.gradle' apply from: scriptsLocation + 'semVer.gradle' apply from: scriptsLocation + 'mavenCentralPublish.gradle' +apply from: scriptsLocation + 'branchName.gradle' configurations { scalaCompilerPlugin diff --git a/gradle/scripts/branchName.gradle b/gradle/scripts/branchName.gradle new file mode 100644 index 0000000000..b1357b16f1 --- /dev/null +++ b/gradle/scripts/branchName.gradle @@ -0,0 +1,26 @@ +tasks.register('checkBranchName') { + doLast { + if (!project.hasProperty('branchName')) { + throw new GradleException("Error: Missing required property 'branchName'.") + } + + def branchName = project.property('branchName') + + def patterns = [ + ~/^(developer|develop|dev)$/, + ~/.*rel\/.*/, + ~/^dependabot\/.*$/, + ~/.*hotfix\/\pL{2}\/#\d+.*/, + ~/.*main/, + ~/^[a-z]{2}\/#[0-9]+(?:-.+)?$/ + ] + + def isValid = patterns.any { pattern -> branchName ==~ pattern } + + if (!isValid) { + throw new GradleException("Error: Check Branch name format (e.g., ps/#1337-FeatureName).") + } + + println "Branch name is $branchName" + } +} 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 801ff1b9db..6d18b2034a 100644 --- a/src/main/scala/edu/ie3/simona/model/participant/HpModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant/HpModel.scala @@ -112,7 +112,12 @@ final case class HpModel( tick: Long, currentState: HpState, data: HpRelevantData, - ): Power = currentState.qDot + ): Power = + // only if Hp is running qDot will be from Hp, else qDot results from other source, e.g. some storage + if (currentState.isRunning) + currentState.qDot + else + zeroKW /** Given a [[HpRelevantData]] object and the last [[HpState]], this function * calculates the heat pump's next state to get the actual active power of @@ -134,12 +139,8 @@ final case class HpModel( // Use lastHpState and relevantData to update state of thermalGrid to the current tick val (thermalDemandWrapper, currentThermalGridState) = thermalGrid.energyDemandAndUpdatedState( - relevantData.currentTick, - lastHpState.ambientTemperature.getOrElse( - relevantData.ambientTemperature - ), - relevantData.ambientTemperature, - lastHpState.thermalGridState, + relevantData, + lastHpState, relevantData.simulationStart, relevantData.houseInhabitants, ) @@ -190,10 +191,8 @@ final case class HpModel( val demandDomesticHotWaterStorage = thermalDemands.domesticHotWaterStorageDemand - val noThermalStorageOrThermalStorageIsEmpty = determineThermalStorageStatus( - lastState, - currentThermalGridState, - ) + val noThermalStorageOrThermalStorageIsEmpty = + currentThermalGridState.isThermalStorageEmpty val turnHpOn = (demandHouse.hasRequiredDemand && noThermalStorageOrThermalStorageIsEmpty) || @@ -217,31 +216,6 @@ final case class HpModel( ) } - /** Determines the actual status of heat pump and the status of the heat - * storage if there is one - * - * @param hpState - * Current state of the heat pump - * @param updatedGridState - * The updated state of the [[ThermalGrid]] - * @return - * boolean which is true, if there is no thermalStorage, or it's empty. - */ - - private def determineThermalStorageStatus( - lastHpState: HpState, - updatedGridState: ThermalGridState, - ): Boolean = { - implicit val tolerance: Energy = KilowattHours(1e-3) - val noThermalStorageOrThermalStorageIsEmpty: Boolean = - updatedGridState.storageState.isEmpty || updatedGridState.storageState - .exists( - _.storedEnergy =~ zeroKWh - ) - - noThermalStorageOrThermalStorageIsEmpty - } - /** Calculate state depending on whether heat pump is needed or not. Also * calculate inner temperature change of thermal house and update its inner * temperature. @@ -339,7 +313,7 @@ final case class HpModel( * operating state and give back the next tick in which something will * change. * - * @param data + * @param relevantData * Relevant data for model calculation * @param lastState * The last known model state @@ -350,7 +324,7 @@ final case class HpModel( * options will change next */ override def handleControlledPowerChange( - data: HpRelevantData, + relevantData: HpRelevantData, lastState: HpState, setPower: Power, ): (HpState, FlexChangeIndicator) = { @@ -362,17 +336,15 @@ final case class HpModel( _, ) = thermalGrid.energyDemandAndUpdatedState( - data.currentTick, - lastState.ambientTemperature.getOrElse(data.ambientTemperature), - data.ambientTemperature, - lastState.thermalGridState, + relevantData, + lastState, data.simulationStart, data.houseInhabitants, ) val updatedHpState = calcState( lastState, - data, + relevantData, turnOn, thermalDemands, ) diff --git a/src/main/scala/edu/ie3/simona/model/thermal/CylindricalThermalStorage.scala b/src/main/scala/edu/ie3/simona/model/thermal/CylindricalThermalStorage.scala index 8c0d011a8d..f77282a47e 100644 --- a/src/main/scala/edu/ie3/simona/model/thermal/CylindricalThermalStorage.scala +++ b/src/main/scala/edu/ie3/simona/model/thermal/CylindricalThermalStorage.scala @@ -184,7 +184,6 @@ object CylindricalThermalStorage extends ThermalStorageCalculations { input: CylindricalStorageInput, initialStoredEnergy: Energy = zeroKWh, ): CylindricalThermalStorage = { - val maxEnergyThreshold = volumeToEnergy( CubicMeters( input.getStorageVolumeLvl.to(Units.CUBIC_METRE).getValue.doubleValue 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 f11e5d1c96..656a35eab3 100644 --- a/src/main/scala/edu/ie3/simona/model/thermal/ThermalGrid.scala +++ b/src/main/scala/edu/ie3/simona/model/thermal/ThermalGrid.scala @@ -19,7 +19,7 @@ import edu.ie3.datamodel.models.result.thermal.{ } import edu.ie3.simona.exceptions.InvalidParameterException import edu.ie3.simona.exceptions.agent.InconsistentStateException -import edu.ie3.simona.model.participant.HpModel.HpRelevantData +import edu.ie3.simona.model.participant.HpModel.{HpRelevantData, HpState} import edu.ie3.simona.model.thermal.ThermalGrid.{ ThermalDemandWrapper, ThermalEnergyDemand, @@ -55,42 +55,31 @@ final case class ThermalGrid( /** Determine the energy demand of the total grid at the given instance in * time and returns it including the updatedState * - * @param tick - * Questioned instance in time - * @param lastAmbientTemperature - * Ambient temperature until this tick - * @param ambientTemperature - * Current ambient temperature in the instance in question - * @param state - * Currently applicable state of the thermal grid - * @param simulationStartTime - * simulationStartDate as ZonedDateTime - * @param houseInhabitants - * number of people living in the building + * @param lastHpState + * Last state of the heat pump + * @param relevantData + * data of heat pump including * @return * The total energy demand of the house and the storage and an updated * [[ThermalGridState]] */ def energyDemandAndUpdatedState( - tick: Long, - // FIXME this is also in state - lastAmbientTemperature: Temperature, - ambientTemperature: Temperature, - state: ThermalGridState, - simulationStartTime: ZonedDateTime, - houseInhabitants: Double, + relevantData: HpRelevantData, + lastHpState: HpState, ): (ThermalDemandWrapper, ThermalGridState) = { /* First get the energy demand of the houses but only if inner temperature is below target temperature */ - val (houseDemand, updatedHouseState, demandHotDomesticWater) = - house.zip(state.houseState) match { - case Some((thermalHouse, lastHouseState)) => { - val (updatedHouseState, updatedStorageState) = + val (houseDemand, updatedHouseState) = + house.zip(lastHpState.thermalGridState.houseState) match { + case Some((thermalHouse, lastHouseState)) => + val (updatedHouseState, _) = thermalHouse.determineState( - tick, + relevantData.currentTick, lastHouseState, - lastAmbientTemperature, - ambientTemperature, + lastHpState.ambientTemperature.getOrElse( + relevantData.ambientTemperature + ), + relevantData.ambientTemperature, lastHouseState.qDot, ) val (heatDemand, newHouseState) = if ( @@ -98,8 +87,8 @@ final case class ThermalGrid( ) { ( thermalHouse.energyDemandHeating( - tick, - ambientTemperature, + relevantData.currentTick, + relevantData.ambientTemperature, updatedHouseState, ), Some(updatedHouseState), @@ -127,10 +116,10 @@ final case class ThermalGrid( val (storageDemand, updatedStorageState) = { heatStorage - .zip(state.storageState) + .zip(lastHpState.thermalGridState.storageState) .map { case (storage, state) => val (updatedStorageState, _) = - storage.updateState(tick, state.qDot, state) + storage.updateState(relevantData.currentTick, state.qDot, state) val storedEnergy = updatedStorageState.storedEnergy val soc = storedEnergy / storage.getMaxEnergyThreshold val storageRequired = { @@ -1484,8 +1473,24 @@ object ThermalGrid { final case class ThermalGridState( houseState: Option[ThermalHouseState], storageState: Option[ThermalStorageState], - domesticHotWaterStorageState: Option[ThermalStorageState], - ) + domesticHotWaterStorageState: 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( 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 9d279ede07..cb09b2bb80 100644 --- a/src/test/scala/edu/ie3/simona/model/thermal/ThermalGridSpec.scala +++ b/src/test/scala/edu/ie3/simona/model/thermal/ThermalGridSpec.scala @@ -6,14 +6,20 @@ package edu.ie3.simona.model.thermal +import edu.ie3.datamodel.models.input.thermal.ThermalStorageInput 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} +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) @@ -21,7 +27,7 @@ class ThermalGridSpec extends UnitSpec { "Testing the thermal energy demand" when { "instantiating it from given values" should { - "throw exception for non-sensible input (positive)" in { + "throw exception for non-plausible input (positive)" in { val possible = MegawattHours(40d) val required = MegawattHours(42d) @@ -114,4 +120,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/ThermalGridWithHouseAndStorageSpec.scala b/src/test/scala/edu/ie3/simona/model/thermal/ThermalGridWithHouseAndStorageSpec.scala index 3c1c03dbce..9d6c2ee77d 100644 --- a/src/test/scala/edu/ie3/simona/model/thermal/ThermalGridWithHouseAndStorageSpec.scala +++ b/src/test/scala/edu/ie3/simona/model/thermal/ThermalGridWithHouseAndStorageSpec.scala @@ -14,15 +14,9 @@ import edu.ie3.simona.model.thermal.ThermalGrid.{ ThermalGridState, } import edu.ie3.simona.model.thermal.ThermalHouse.ThermalHouseState -import edu.ie3.simona.model.thermal.ThermalHouse.ThermalHouseThreshold.{ - HouseTemperatureLowerBoundaryReached, - HouseTemperatureUpperBoundaryReached, -} +import edu.ie3.simona.model.thermal.ThermalHouse.ThermalHouseThreshold.{HouseTemperatureLowerBoundaryReached, HouseTemperatureUpperBoundaryReached} import edu.ie3.simona.model.thermal.ThermalStorage.ThermalStorageState -import edu.ie3.simona.model.thermal.ThermalStorage.ThermalStorageThreshold.{ - StorageEmpty, - StorageFull, -} +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._ @@ -127,6 +121,20 @@ class ThermalGridWithHouseAndStorageSpec // hot water demand will be excluded here for test reasons val tick = 10800 // after three hours + val relevantData = HpRelevantData( + 10800, // after three hours + testGridAmbientTemperature, + ) + val lastHpState = HpState( + true, + relevantData.currentTick, + Some(testGridAmbientTemperature), + Kilowatts(42), + Kilowatts(42), + ThermalGrid.startingState(thermalGrid), + None, + ) + val (thermalDemands, updatedThermalGridState) = thermalGrid.energyDemandAndUpdatedState( tick, @@ -206,6 +214,24 @@ class ThermalGridWithHouseAndStorageSpec "deliver the correct house and storage demand" in { val tick = 10800 // after three hours + val relevantData = HpRelevantData( + 10800, // after three hours + testGridAmbientTemperature, + ) + val startingState = ThermalGrid.startingState(thermalGrid) + val lastHpState = HpState( + true, + relevantData.currentTick, + Some(testGridAmbientTemperature), + Kilowatts(42), + Kilowatts(42), + startingState.copy(houseState = + startingState.houseState.map( + _.copy(innerTemperature = Celsius(16d)) + ) + ), + None, + ) val startingState = ThermalGrid.startingState(thermalGrid) val (thermalDemands, updatedThermalGridState) = 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 582339c44a..b4ef49a8e2 100644 --- a/src/test/scala/edu/ie3/simona/model/thermal/ThermalGridWithHouseOnlySpec.scala +++ b/src/test/scala/edu/ie3/simona/model/thermal/ThermalGridWithHouseOnlySpec.scala @@ -112,9 +112,21 @@ class ThermalGridWithHouseOnlySpec "determining the energy demand for heating and domestic hot water" should { "exactly be the thermal demand for heating of the house" in { - val tick = 10800 // after three hours + val relevantData = HpRelevantData( + 10800, // after three hours + testGridAmbientTemperature, + ) + val lastHpState = HpState( + true, + relevantData.currentTick, + Some(testGridAmbientTemperature), + Kilowatts(42), + Kilowatts(42), + ThermalGrid.startingState(thermalGrid), + None, + ) val expectedHouseDemand = thermalHouse.energyDemandHeating( - tick, + relevantData.currentTick, testGridAmbientTemperature, expectedHouseStartingState, ) @@ -124,10 +136,8 @@ class ThermalGridWithHouseOnlySpec updatedThermalGridState, ) = thermalGrid.energyDemandAndUpdatedState( - tick, - testGridAmbientTemperature, - testGridAmbientTemperature, - ThermalGrid.startingState(thermalGrid), + relevantData, + lastHpState, defaultSimulationStart, houseInhabitants, ) 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 0d76074252..5b48d39374 100644 --- a/src/test/scala/edu/ie3/simona/model/thermal/ThermalGridWithStorageOnlySpec.scala +++ b/src/test/scala/edu/ie3/simona/model/thermal/ThermalGridWithStorageOnlySpec.scala @@ -6,22 +6,12 @@ package edu.ie3.simona.model.thermal -import edu.ie3.datamodel.models.input.thermal.{ - ThermalHouseInput, - ThermalStorageInput, -} -import edu.ie3.simona.model.participant.HpModel.HpRelevantData -import edu.ie3.simona.model.thermal.ThermalGrid.{ - ThermalDemandWrapper, - ThermalEnergyDemand, - ThermalGridState, -} +import edu.ie3.datamodel.models.input.thermal.{ThermalHouseInput, ThermalStorageInput} +import edu.ie3.simona.model.participant.HpModel.{HpRelevantData, HpState} +import edu.ie3.simona.model.thermal.ThermalGrid.{ThermalDemandWrapper, ThermalEnergyDemand, ThermalGridState} import edu.ie3.simona.model.thermal.ThermalHouse.ThermalHouseState import edu.ie3.simona.model.thermal.ThermalStorage.ThermalStorageState -import edu.ie3.simona.model.thermal.ThermalStorage.ThermalStorageThreshold.{ - StorageEmpty, - StorageFull, -} +import edu.ie3.simona.model.thermal.ThermalStorage.ThermalStorageThreshold.{StorageEmpty, StorageFull} import edu.ie3.simona.test.common.{DefaultTestData, UnitSpec} import edu.ie3.util.scala.quantities.DefaultQuantities.{zeroKW, zeroKWh} import squants.energy._ @@ -94,6 +84,26 @@ class ThermalGridWithStorageOnlySpec "deliver the capabilities of the storage" in { val tick = 10800 // after three hours + val relevantData = HpRelevantData( + 10800, // after three hours + testGridAmbientTemperature, + ) + val lastHpState = HpState( + true, + relevantData.currentTick, + Some(testGridAmbientTemperature), + Kilowatts(42), + Kilowatts(42), + ThermalGrid.startingState(thermalGrid), + None, + ) + + val (thermalDemands, updatedThermalGridState) = + thermalGrid.energyDemandAndUpdatedState( + relevantData, + lastHpState, + ) + val ( thermalDemands, updatedThermalGridState, @@ -126,6 +136,29 @@ class ThermalGridWithStorageOnlySpec "deliver the capabilities of a half full storage" in { val tick = 10800 // after three hours + val relevantData = HpRelevantData( + 10800, // after three hours + testGridAmbientTemperature, + ) + val lastHpState = HpState( + true, + relevantData.currentTick, + Some(testGridAmbientTemperature), + Kilowatts(42), + Kilowatts(42), + ThermalGridState( + None, + Some(ThermalStorageState(0L, KilowattHours(575d), zeroKW)), + ), + None, + ) + + val (thermalDemands, updatedThermalGridState) = + thermalGrid.energyDemandAndUpdatedState( + relevantData, + lastHpState, + ) + val ( thermalDemands, updatedThermalGridState,