diff --git a/CHANGELOG.md b/CHANGELOG.md index 59fd2e1e6a..324156a8e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -90,6 +90,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Reorganized Jenkins pipeline to separate build and test stages for better efficiency [#938](https://github.com/ie3-institute/simona/issues/938) - Rewrote SystemParticipantTest and MockParticipant from groovy to scala [#646](https://github.com/ie3-institute/simona/issues/646) - Rewrote ChpModelTest from groovy to scala [#646](https://github.com/ie3-institute/simona/issues/646) +- Rewrote CylindricalThermalStorageTest Test from groovy to scala [#646](https://github.com/ie3-institute/simona/issues/646) - Move compression of output files into `ResultEventListener`[#965](https://github.com/ie3-institute/simona/issues/965) ### Fixed diff --git a/build.gradle b/build.gradle index c4fcda0a94..117893dbdd 100644 --- a/build.gradle +++ b/build.gradle @@ -30,7 +30,7 @@ ext { jtsVersion = '1.20.0' confluentKafkaVersion = '7.4.0' tscfgVersion = '1.1.3' - scapegoatVersion = '3.0.3' + scapegoatVersion = '3.1.1' testContainerVersion = '0.41.4' @@ -98,12 +98,12 @@ dependencies { /* logging */ implementation "com.typesafe.scala-logging:scala-logging_${scalaVersion}:3.9.5" // pekko scala logging - implementation "ch.qos.logback:logback-classic:1.5.10" + implementation "ch.qos.logback:logback-classic:1.5.11" /* testing */ testImplementation 'org.spockframework:spock-core:2.3-groovy-4.0' testImplementation 'org.scalatestplus:mockito-3-4_2.13:3.2.10.0' - testImplementation 'org.mockito:mockito-core:5.14.1' // mocking framework + testImplementation 'org.mockito:mockito-core:5.14.2' // mocking framework testImplementation "org.scalatest:scalatest_${scalaVersion}:3.2.19" testRuntimeOnly 'com.vladsch.flexmark:flexmark-all:0.64.8' //scalatest html output testImplementation group: 'org.pegdown', name: 'pegdown', version: '1.6.0' diff --git a/src/test/groovy/edu/ie3/simona/model/thermal/CylindricalThermalStorageTest.groovy b/src/test/groovy/edu/ie3/simona/model/thermal/CylindricalThermalStorageTest.groovy deleted file mode 100644 index af5ffef8e5..0000000000 --- a/src/test/groovy/edu/ie3/simona/model/thermal/CylindricalThermalStorageTest.groovy +++ /dev/null @@ -1,150 +0,0 @@ -/* - * © 2020. TU Dortmund University, - * Institute of Energy Systems, Energy Efficiency and Energy Economics, - * Research group Distribution grid planning and operation - */ - -package edu.ie3.simona.model.thermal - -import static edu.ie3.util.quantities.PowerSystemUnits.KILOWATTHOUR -import static tech.units.indriya.quantity.Quantities.getQuantity - -import edu.ie3.datamodel.models.StandardUnits -import edu.ie3.datamodel.models.input.thermal.CylindricalStorageInput -import edu.ie3.util.scala.quantities.KilowattHoursPerKelvinCubicMeters$ -import edu.ie3.util.scala.quantities.Sq -import spock.lang.Shared -import spock.lang.Specification -import squants.energy.KilowattHours$ -import squants.energy.Kilowatts$ -import squants.space.CubicMeters$ -import squants.thermal.Celsius$ - -class CylindricalThermalStorageTest extends Specification { - - static final double TESTING_TOLERANCE = 1e-10 - - @Shared - CylindricalStorageInput storageInput - - def setupSpec() { - storageInput = new CylindricalStorageInput( - UUID.randomUUID(), - "ThermalStorage", - null, - getQuantity(100, StandardUnits.VOLUME), - getQuantity(20, StandardUnits.VOLUME), - getQuantity(30, StandardUnits.TEMPERATURE), - getQuantity(40, StandardUnits.TEMPERATURE), - getQuantity(1.15, StandardUnits.SPECIFIC_HEAT_CAPACITY)) - } - - static def buildThermalStorage(CylindricalStorageInput storageInput, Double volume) { - def storedEnergy = CylindricalThermalStorage.volumeToEnergy(Sq.create(volume, CubicMeters$.MODULE$), - Sq.create(storageInput.c.value.doubleValue(), KilowattHoursPerKelvinCubicMeters$.MODULE$), - Sq.create(storageInput.inletTemp.value.doubleValue(), Celsius$.MODULE$), - Sq.create(storageInput.returnTemp.value.doubleValue(), Celsius$.MODULE$)) - def thermalStorage = CylindricalThermalStorage.apply(storageInput, storedEnergy) - return thermalStorage - } - - def vol2Energy(Double volume) { - return CylindricalThermalStorage.volumeToEnergy( // FIXME below: get values in units with to..() - Sq.create(volume, CubicMeters$.MODULE$), - Sq.create(storageInput.c.value.doubleValue(), KilowattHoursPerKelvinCubicMeters$.MODULE$), - Sq.create(storageInput.inletTemp.value.doubleValue(), Celsius$.MODULE$), - Sq.create(storageInput.returnTemp.value.doubleValue(), Celsius$.MODULE$)) - } - - def "Check storage level operations:"() { - given: - def storage = buildThermalStorage(storageInput, 70) - - when: - def initialLevel = - getQuantity(storage._storedEnergy().toKilowattHours(), KILOWATTHOUR) - storage._storedEnergy_$eq(vol2Energy(50d),) - def newLevel1 = getQuantity(storage._storedEnergy().toKilowattHours(), KILOWATTHOUR) - def surplus = storage.tryToStoreAndReturnRemainder( - vol2Energy(55d)) - def newLevel2 = getQuantity(storage._storedEnergy().toKilowattHours(), KILOWATTHOUR) - def isCovering = storage.isDemandCoveredByStorage(Sq.create(5, KilowattHours$.MODULE$)) - def lack = - storage.tryToTakeAndReturnLack( - vol2Energy(95d) - ) - def newLevel3 = getQuantity(storage._storedEnergy().toKilowattHours(), KILOWATTHOUR) - def notCovering = storage.isDemandCoveredByStorage(Sq.create(1, KilowattHours$.MODULE$)) - - then: - initialLevel.value.doubleValue() =~ vol2Energy(70d).toKilowattHours() - newLevel1.value.doubleValue() =~ vol2Energy(50d).toKilowattHours() - surplus =~ vol2Energy(5d) - newLevel2.value.doubleValue() =~ vol2Energy(100d).toKilowattHours() - lack =~ vol2Energy(15d) - newLevel3.value.doubleValue() =~ vol2Energy(20d).toKilowattHours() - isCovering - !notCovering - } - - def "Check converting methods:"() { - given: - def storage = buildThermalStorage(storageInput, 70) - - when: - def usableThermalEnergy = storage.usableThermalEnergy() - - then: - Math.abs(usableThermalEnergy.toKilowattHours() - 5 * 115) < TESTING_TOLERANCE - } - - def "Check apply, validation and build method:"() { - when: - def storage = buildThermalStorage(storageInput, 70) - - then: - storage.uuid() == storageInput.uuid - storage.id() == storageInput.id - storage.operatorInput() == storageInput.operator - storage.operationTime() == storageInput.operationTime - storage.bus() == storageInput.thermalBus - } - - def "Check mutable state update:"() { - when: - def storage = buildThermalStorage(storageInput, 70) - def lastState = new ThermalStorage.ThermalStorageState(tick, Sq.create(storedEnergy, KilowattHours$.MODULE$), Sq.create(qDot, Kilowatts$.MODULE$)) - def result = storage.updateState(newTick, Sq.create(newQDot, Kilowatts$.MODULE$), lastState) - - then: - Math.abs(result._1().storedEnergy().toKilowattHours() - expectedStoredEnergy.doubleValue()) < TESTING_TOLERANCE - result._2.defined - result._2.get() == expectedThreshold - - where: - tick | storedEnergy | qDot | newTick | newQDot || expectedStoredEnergy | expectedThreshold - 0L | 250.0d | 10.0d | 3600L | 42.0d || 260.0d | new ThermalStorage.ThermalStorageThreshold.StorageFull(79886L) - 0L | 250.0d | 10.0d | 3600L | -42.0d || 260.0d | new ThermalStorage.ThermalStorageThreshold.StorageEmpty(6171L) - 0L | 250.0d | -10.0d | 3600L | 42.0d || 240.0d | new ThermalStorage.ThermalStorageThreshold.StorageFull(81600L) - 0L | 250.0d | -10.0d | 3600L | -42.0d || 240.0d | new ThermalStorage.ThermalStorageThreshold.StorageEmpty(4457L) - 0L | 250.0d | -10.0d | 3600L | -42.0d || 240.0d | new ThermalStorage.ThermalStorageThreshold.StorageEmpty(4457L) - 0L | 1000.0d | 149.0d | 3600L | 5000.0d || 1149.0d | new ThermalStorage.ThermalStorageThreshold.StorageFull(3601L) - 0L | 240.0d | -9.0d | 3600L | -5000.0d || 231.0d | new ThermalStorage.ThermalStorageThreshold.StorageEmpty(3601L) - } - - def "Check mutable state update, if no threshold is reached:"() { - when: - def storage = buildThermalStorage(storageInput, 70) - def lastState = new ThermalStorage.ThermalStorageState(tick, Sq.create(storedEnergy, KilowattHours$.MODULE$), Sq.create(qDot, Kilowatts$.MODULE$)) - def result = storage.updateState(newTick, Sq.create(newQDot, Kilowatts$.MODULE$), lastState) - - then: - Math.abs(result._1().storedEnergy().toKilowattHours() - expectedStoredEnergy.doubleValue()) < TESTING_TOLERANCE - result._2.empty - - where: - tick | storedEnergy | qDot | newTick | newQDot || expectedStoredEnergy - 0L | 250.0d | 10.0d | 3600L | 0.0d || 260.0d - 0L | 250.0d | -10.0d | 3600L | 0.0d || 240.0d - } -} diff --git a/src/test/scala/edu/ie3/simona/model/thermal/CylindricalThermalStorageSpec.scala b/src/test/scala/edu/ie3/simona/model/thermal/CylindricalThermalStorageSpec.scala new file mode 100644 index 0000000000..52cbb30f79 --- /dev/null +++ b/src/test/scala/edu/ie3/simona/model/thermal/CylindricalThermalStorageSpec.scala @@ -0,0 +1,261 @@ +/* + * © 2020. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.model.thermal + +import edu.ie3.datamodel.models.StandardUnits +import edu.ie3.datamodel.models.input.thermal.CylindricalStorageInput +import edu.ie3.simona.test.common.UnitSpec +import edu.ie3.util.quantities.PowerSystemUnits +import edu.ie3.util.scala.quantities.KilowattHoursPerKelvinCubicMeters +import org.scalatest.BeforeAndAfterAll +import org.scalatest.matchers.should.Matchers +import squants.Energy +import squants.energy.{KilowattHours, Kilowatts} +import squants.space.{CubicMeters, Volume} +import squants.thermal.Celsius +import tech.units.indriya.quantity.Quantities.getQuantity +import tech.units.indriya.unit.Units + +import java.util.UUID + +class CylindricalThermalStorageSpec + extends UnitSpec + with Matchers + with BeforeAndAfterAll { + + implicit val tolerance: Energy = KilowattHours(1e-10) + + lazy val storageInput = new CylindricalStorageInput( + UUID.randomUUID(), + "ThermalStorage", + null, + getQuantity(100, StandardUnits.VOLUME), + getQuantity(20, StandardUnits.VOLUME), + getQuantity(30, StandardUnits.TEMPERATURE), + getQuantity(40, StandardUnits.TEMPERATURE), + getQuantity(1.15, StandardUnits.SPECIFIC_HEAT_CAPACITY), + ) + + def buildThermalStorage( + storageInput: CylindricalStorageInput, + volume: Volume, + ): CylindricalThermalStorage = { + val storedEnergy = CylindricalThermalStorage.volumeToEnergy( + volume, + KilowattHoursPerKelvinCubicMeters( + storageInput.getC + .to(PowerSystemUnits.KILOWATTHOUR_PER_KELVIN_TIMES_CUBICMETRE) + .getValue + .doubleValue + ), + Celsius( + storageInput.getInletTemp.to(Units.CELSIUS).getValue.doubleValue() + ), + Celsius( + storageInput.getReturnTemp.to(Units.CELSIUS).getValue.doubleValue() + ), + ) + CylindricalThermalStorage(storageInput, storedEnergy) + } + + def vol2Energy(volume: Volume): Energy = { + CylindricalThermalStorage.volumeToEnergy( + volume, + KilowattHoursPerKelvinCubicMeters( + storageInput.getC + .to(PowerSystemUnits.KILOWATTHOUR_PER_KELVIN_TIMES_CUBICMETRE) + .getValue + .doubleValue + ), + Celsius( + storageInput.getInletTemp.to(Units.CELSIUS).getValue.doubleValue() + ), + Celsius( + storageInput.getReturnTemp.to(Units.CELSIUS).getValue.doubleValue() + ), + ) + } + + "CylindricalThermalStorage Model" should { + + "Check storage level operations:" in { + val storage = buildThermalStorage(storageInput, CubicMeters(70)) + + val initialLevel = storage._storedEnergy + storage._storedEnergy_=(vol2Energy(CubicMeters(50))) + val newLevel1 = storage._storedEnergy + val surplus = + storage.tryToStoreAndReturnRemainder(vol2Energy(CubicMeters(55))) + val newLevel2 = storage._storedEnergy + val isCovering = storage.isDemandCoveredByStorage(KilowattHours(5)) + val lack = storage.tryToTakeAndReturnLack(vol2Energy(CubicMeters(95))) + val newLevel3 = storage._storedEnergy + val notCovering = storage.isDemandCoveredByStorage(KilowattHours(1)) + + initialLevel should approximate(vol2Energy(CubicMeters(70))) + newLevel1 should approximate(vol2Energy(CubicMeters(50))) + surplus.value shouldBe vol2Energy(CubicMeters(5)) + newLevel2 should approximate(vol2Energy(CubicMeters(100))) + lack.value shouldBe vol2Energy(CubicMeters(15)) + newLevel3 should approximate(vol2Energy(CubicMeters(20))) + isCovering shouldBe true + notCovering shouldBe false + } + + "Converting methods work correctly" in { + val storage = buildThermalStorage(storageInput, CubicMeters(70)) + + val usableThermalEnergy = storage.usableThermalEnergy + usableThermalEnergy should approximate(KilowattHours(5 * 115)) + } + + "Apply, validation, and build method work correctly" in { + val storage = buildThermalStorage(storageInput, CubicMeters(70)) + + storage.uuid shouldBe storageInput.getUuid + storage.id shouldBe storageInput.getId + storage.operatorInput shouldBe storageInput.getOperator + storage.operationTime shouldBe storageInput.getOperationTime + storage.bus shouldBe storageInput.getThermalBus + } + + "Check mutable state update correctly update the state with thresholds" in { + val cases = Table( + ( + "tick", + "storedEnergy", + "qDot", + "newTick", + "newQDot", + "expectedStoredEnergy", + "expectedThreshold", + ), + ( + 0L, + 250.0, + 10.0, + 3600L, + 42.0, + 260.0, + ThermalStorage.ThermalStorageThreshold.StorageFull(79886L), + ), + ( + 0L, + 250.0, + 10.0, + 3600L, + -42.0, + 260.0, + ThermalStorage.ThermalStorageThreshold.StorageEmpty(6171L), + ), + ( + 0L, + 250.0, + -10.0, + 3600L, + 42.0, + 240.0, + ThermalStorage.ThermalStorageThreshold.StorageFull(81600L), + ), + ( + 0L, + 250.0, + -10.0, + 3600L, + -42.0, + 240.0, + ThermalStorage.ThermalStorageThreshold.StorageEmpty(4457L), + ), + ( + 0L, + 1000.0, + 149.0, + 3600L, + 5000.0, + 1149.0, + ThermalStorage.ThermalStorageThreshold.StorageFull(3601L), + ), + ( + 0L, + 240.0, + -9.0, + 3600L, + -5000.0, + 231.0, + ThermalStorage.ThermalStorageThreshold.StorageEmpty(3601L), + ), + ) + + forAll(cases) { + ( + tick, + storedEnergy, + qDot, + newTick, + newQDot, + expectedStoredEnergy, + expectedThreshold, + ) => + val storage = buildThermalStorage(storageInput, CubicMeters(70)) + val lastState = ThermalStorage.ThermalStorageState( + tick, + KilowattHours(storedEnergy), + Kilowatts(qDot), + ) + val result = + storage.updateState(newTick, Kilowatts(newQDot), lastState) + + result._1.storedEnergy should approximate( + KilowattHours(expectedStoredEnergy) + ) + + result._2 match { + case Some(threshold) => threshold shouldBe expectedThreshold + case None => fail("Expected a threshold but got None") + } + } + + } + + "Check mutable state update, if no threshold is reached update state without hitting a threshold" in { + val cases = Table( + ( + "tick", + "storedEnergy", + "qDot", + "newTick", + "newQDot", + "expectedStoredEnergy", + ), + (0L, 250.0, 10.0, 3600L, 0.0, 260.0), + (0L, 250.0, -10.0, 3600L, 0.0, 240.0), + ) + + forAll(cases) { + (tick, storedEnergy, qDot, newTick, newQDot, expectedStoredEnergy) => + val storage = buildThermalStorage(storageInput, CubicMeters(70)) + val lastState = ThermalStorage.ThermalStorageState( + tick, + KilowattHours(storedEnergy), + Kilowatts(qDot), + ) + val result = + storage.updateState(newTick, Kilowatts(newQDot), lastState) + + result._1.storedEnergy should approximate( + KilowattHours(expectedStoredEnergy) + ) + + result._2 match { + case Some(threshold) => + fail(s"Expected no threshold, but got: $threshold") + case None => succeed + } + } + } + } +}