diff --git a/CHANGELOG.md b/CHANGELOG.md index 81ae41cd37..59fd2e1e6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - EmAgents should be able to handle initialization [#945](https://github.com/ie3-institute/simona/issues/945) - Added option to directly zip the output files [#793](https://github.com/ie3-institute/simona/issues/793) - Added weatherData HowTo for Copernicus ERA5 data [#967](https://github.com/ie3-institute/simona/issues/967) +- Add some quote to 'printGoodbye' [#997](https://github.com/ie3-institute/simona/issues/997) ### Changed - Adapted to changed data source in PSDM [#435](https://github.com/ie3-institute/simona/issues/435) @@ -87,6 +88,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Renamed `ActivityStartTrigger`, `ScheduleTriggerMessage`, `CompletionMessage` in UML Diagrams[#675](https://github.com/ie3-institute/simona/issues/675) - Simplifying quantity integration in QuantityUtil [#973](https://github.com/ie3-institute/simona/issues/973) - 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) - Move compression of output files into `ResultEventListener`[#965](https://github.com/ie3-institute/simona/issues/965) ### Fixed @@ -118,6 +121,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Improve code quality in fixedloadmodelspec and other tests [#919](https://github.com/ie3-institute/simona/issues/919) - Fix power flow calculation with em agents [#962](https://github.com/ie3-institute/simona/issues/962) - Fix scheduling at Evcs with more than one Ev at a time without Em [#787](https://github.com/ie3-institute/simona/issues/787) +- Fix CheckWindow duration [#921](https://github.com/ie3-institute/simona/issues/921) ## [3.0.0] - 2023-08-07 diff --git a/build.gradle b/build.gradle index 0101467b78..c4fcda0a94 100644 --- a/build.gradle +++ b/build.gradle @@ -98,7 +98,7 @@ dependencies { /* logging */ implementation "com.typesafe.scala-logging:scala-logging_${scalaVersion}:3.9.5" // pekko scala logging - implementation "ch.qos.logback:logback-classic:1.5.9" + implementation "ch.qos.logback:logback-classic:1.5.10" /* testing */ testImplementation 'org.spockframework:spock-core:2.3-groovy-4.0' diff --git a/docs/readthedocs/requirements.txt b/docs/readthedocs/requirements.txt index 382b7eaf88..5ed7a8bf17 100644 --- a/docs/readthedocs/requirements.txt +++ b/docs/readthedocs/requirements.txt @@ -1,4 +1,4 @@ -Sphinx==8.1.0 +Sphinx==8.1.3 sphinx-rtd-theme==3.0.1 sphinxcontrib-plantuml==0.30 myst-parser==4.0.0 diff --git a/src/main/scala/edu/ie3/simona/io/runtime/RuntimeEventLogSink.scala b/src/main/scala/edu/ie3/simona/io/runtime/RuntimeEventLogSink.scala index b46ccc06b0..86c731a3ed 100644 --- a/src/main/scala/edu/ie3/simona/io/runtime/RuntimeEventLogSink.scala +++ b/src/main/scala/edu/ie3/simona/io/runtime/RuntimeEventLogSink.scala @@ -27,7 +27,6 @@ import scala.concurrent.duration.DurationLong final case class RuntimeEventLogSink( simulationStartDate: ZonedDateTime, log: Logger, - private var last: Long = 0L, ) extends RuntimeEventSink { override def handleRuntimeEvent( @@ -45,15 +44,13 @@ final case class RuntimeEventLogSink( case CheckWindowPassed(tick, duration) => log.info( - s"******* Simulation until ${calcTime(tick)} completed. ${durationAndMemoryString(duration - last)} ******" + s"******* Simulation until ${calcTime(tick)} completed. ${durationAndMemoryString(duration)} ******" ) - last = duration case Ready(tick, duration) => log.info( - s"******* Switched from 'Simulating' to 'Ready'. Last simulated time: ${calcTime(tick)}. ${durationAndMemoryString(duration - last)} ******" + s"******* Switched from 'Simulating' to 'Ready'. Last simulated time: ${calcTime(tick)}. ${durationAndMemoryString(duration)} ******" ) - last = duration case Simulating(startTick, endTick) => log.info( diff --git a/src/main/scala/edu/ie3/simona/main/RunSimona.scala b/src/main/scala/edu/ie3/simona/main/RunSimona.scala index 8265ed0cdb..c255990a05 100644 --- a/src/main/scala/edu/ie3/simona/main/RunSimona.scala +++ b/src/main/scala/edu/ie3/simona/main/RunSimona.scala @@ -66,6 +66,7 @@ trait RunSimona[T <: SimonaSetup] extends LazyLogging { "\"Ich bin der Anfang, das Ende, die Eine, die Viele ist. Ich bin die Borg.\" - Borg-Königin (in Star Trek: Der erste Kontakt)", "\"A horse! A horse! My kingdom for a horse!\" - King Richard III (in Shakespeare's Richard III, 1594)", "\"Und wenn du lange in einen Abgrund blickst, blickt der Abgrund auch in dich hinein\" - F. Nietzsche", + "\"Before anything else, preparation is the key to success.\" - Alexander Graham Bell", ) val rand = new Random 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 b6e5d9916d..fedea75204 100644 --- a/src/main/scala/edu/ie3/simona/model/thermal/CylindricalThermalStorage.scala +++ b/src/main/scala/edu/ie3/simona/model/thermal/CylindricalThermalStorage.scala @@ -63,7 +63,7 @@ final case class CylindricalThermalStorage( minEnergyThreshold: Energy, maxEnergyThreshold: Energy, chargingPower: Power, - override protected var _storedEnergy: Energy, + override var _storedEnergy: Energy, ) extends ThermalStorage( uuid, id, diff --git a/src/main/scala/edu/ie3/simona/scheduler/RuntimeNotifier.scala b/src/main/scala/edu/ie3/simona/scheduler/RuntimeNotifier.scala index 844021c530..73ec02a4af 100644 --- a/src/main/scala/edu/ie3/simona/scheduler/RuntimeNotifier.scala +++ b/src/main/scala/edu/ie3/simona/scheduler/RuntimeNotifier.scala @@ -111,8 +111,12 @@ final case class RuntimeNotifier( val completedWindows = (adjustedLastCheck + checkWindow) to completedTick by checkWindow - completedWindows.foreach { tick => - notify(CheckWindowPassed(tick, duration(lastStartTime, nowTime))) + completedWindows.foldLeft(lastCheckWindowTime) { + case (lastTime, tick) => + notify( + CheckWindowPassed(tick, duration(lastTime, nowTime)) + ) + None } completedWindows.lastOption diff --git a/src/test/groovy/edu/ie3/simona/model/participant/ChpModelTest.groovy b/src/test/groovy/edu/ie3/simona/model/participant/ChpModelTest.groovy deleted file mode 100644 index a07273bb27..0000000000 --- a/src/test/groovy/edu/ie3/simona/model/participant/ChpModelTest.groovy +++ /dev/null @@ -1,243 +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.participant - -import static edu.ie3.util.quantities.PowerSystemUnits.* -import static tech.units.indriya.quantity.Quantities.getQuantity -import static tech.units.indriya.unit.Units.PERCENT - -import edu.ie3.datamodel.models.OperationTime -import edu.ie3.datamodel.models.StandardUnits -import edu.ie3.datamodel.models.input.OperatorInput -import edu.ie3.datamodel.models.input.system.ChpInput -import edu.ie3.datamodel.models.input.system.characteristic.CosPhiFixed -import edu.ie3.datamodel.models.input.system.type.ChpTypeInput -import edu.ie3.datamodel.models.input.thermal.CylindricalStorageInput -import edu.ie3.datamodel.models.input.thermal.ThermalBusInput -import edu.ie3.datamodel.models.voltagelevels.GermanVoltageLevelUtils -import edu.ie3.simona.model.participant.ChpModel.ChpState -import edu.ie3.simona.model.thermal.CylindricalThermalStorage -import edu.ie3.util.scala.quantities.KilowattHoursPerKelvinCubicMeters$ -import edu.ie3.util.TimeUtil -import edu.ie3.util.scala.quantities.Sq -import spock.lang.Shared -import spock.lang.Specification -import spock.lang.Unroll -import squants.energy.KilowattHours$ -import squants.energy.Kilowatts$ -import squants.space.CubicMeters$ -import squants.thermal.Celsius$ -import testutils.TestObjectFactory - -class ChpModelTest extends Specification { - - @Shared - static final Double TOLERANCE = 0.0001d - @Shared - ChpState chpStateNotRunning = new ChpState(false, 0, Sq.create(0, Kilowatts$.MODULE$), Sq.create(0, KilowattHours$.MODULE$)) - @Shared - ChpState chpStateRunning = new ChpState(true, 0, Sq.create(0, Kilowatts$.MODULE$), Sq.create(0, KilowattHours$.MODULE$)) - @Shared - CylindricalStorageInput storageInput - @Shared - ChpInput chpInput - - def setupSpec() { - def thermalBus = new ThermalBusInput(UUID.randomUUID(), "thermal bus") - - storageInput = new CylindricalStorageInput( - UUID.randomUUID(), - "ThermalStorage", - thermalBus, - getQuantity(100, StandardUnits.VOLUME), - getQuantity(20, StandardUnits.VOLUME), - getQuantity(30, StandardUnits.TEMPERATURE), - getQuantity(40, StandardUnits.TEMPERATURE), - getQuantity(1.15, StandardUnits.SPECIFIC_HEAT_CAPACITY)) - - def chpTypeInput = new ChpTypeInput( - UUID.randomUUID(), - "ChpTypeInput", - getQuantity(10000d, EURO), - getQuantity(200d, EURO_PER_MEGAWATTHOUR), - getQuantity(19, PERCENT), - getQuantity(76, PERCENT), - getQuantity(100, KILOVOLTAMPERE), - 0.95, - getQuantity(50, KILOWATT), - getQuantity(0, KILOWATT)) - - chpInput = new ChpInput( - UUID.randomUUID(), - "ChpInput", - OperatorInput.NO_OPERATOR_ASSIGNED, - OperationTime.notLimited(), - TestObjectFactory.buildNodeInput(false, GermanVoltageLevelUtils.MV_10KV, 0), - thermalBus, - new CosPhiFixed("cosPhiFixed:{(0.0,0.95)}"), - null, - chpTypeInput, - null, - false) - } - - static def buildChpModel(CylindricalThermalStorage thermalStorage) { - return new ChpModel( - UUID.randomUUID(), - "ChpModel", - null, - null, - Sq.create(100, Kilowatts$.MODULE$), - 0.95, - Sq.create(50, Kilowatts$.MODULE$), - thermalStorage) - } - - static def buildChpRelevantData(ChpState chpState, Double heatDemand) { - return new ChpModel.ChpRelevantData(chpState, Sq.create(heatDemand, KilowattHours$.MODULE$), 7200) - } - - static def buildThermalStorage(CylindricalStorageInput storageInput, Double storageLvl) { - def storedEnergy = CylindricalThermalStorage.volumeToEnergy( - Sq.create(storageLvl, CubicMeters$.MODULE$), - Sq.create(storageInput.c.value.toDouble(), 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 - } - - @Unroll - def "Check active power after calculating next state with #chpState and heat demand #heatDemand kWh:"() { - given: - def chpData = buildChpRelevantData(chpState, heatDemand) - def thermalStorage = buildThermalStorage(storageInput, storageLvl) - def chpModel = buildChpModel(thermalStorage) - - when: - def activePower = chpModel.calculateNextState(chpData).activePower() - - then: - activePower.toKilowatts() == expectedActivePower - - where: - chpState | storageLvl | heatDemand || expectedActivePower - chpStateNotRunning | 90 | 0 || 0 // tests case (false, false, true) - chpStateNotRunning | 90 | 8 * 115 || 95 // tests case (false, true, false) - chpStateNotRunning | 90 | 10 || 0 // tests case (false, true, true) - chpStateRunning | 90 | 0 || 95 // tests case (true, false, true) - chpStateRunning | 90 | 8 * 115 || 95 // tests case (true, true, false) - chpStateRunning | 90 | 10 || 95 // tests case (true, true, true) - chpStateRunning | 90 | 7 * 115 + 1 || 95 // test case (_, true, false) and demand covered together with chp - chpStateRunning | 90 | 9 * 115 || 95 // test case (_, true, false) and demand not covered together with chp - chpStateRunning | 92 | 1 || 95 // test case (true, true, true) and storage volume exceeds maximum - /* The following tests do not exist: (false, false, false), (true, false, false) */ - } - - @Unroll - def "Check total energy after calculating next state with #chpState and heat demand #heatDemand kWh:"() { - given: - def chpData = buildChpRelevantData(chpState, heatDemand) - def thermalStorage = buildThermalStorage(storageInput, storageLvl) - def chpModel = buildChpModel(thermalStorage) - - when: - def nextState = chpModel.calculateNextState(chpData) - def thermalEnergy = nextState.thermalEnergy() - - then: - Math.abs(thermalEnergy.toKilowattHours() - expectedTotalEnergy) < TOLERANCE - - where: - chpState | storageLvl | heatDemand || expectedTotalEnergy - chpStateNotRunning | 90 | 0 || 0 // tests case (false, false, true) - chpStateNotRunning | 90 | 8 * 115 || 100 // tests case (false, true, false) - chpStateNotRunning | 90 | 10 || 0 // tests case (false, true, true) - chpStateRunning | 90 | 0 || 100 // tests case (true, false, true) - chpStateRunning | 90 | 8 * 115 || 100 // tests case (true, true, false) - chpStateRunning | 90 | 10 || 100 // tests case (true, true, true) - chpStateRunning | 90 | 7 * 115 + 1 || 100 // test case (_, true, false) and demand covered together with chp - chpStateRunning | 90 | 9 * 115 || 100 // test case (_, true, false) and demand not covered together with chp - chpStateRunning | 92 | 1 || 93 // test case (true, true, true) and storage volume exceeds maximum - /* The following tests do not exist: (false, false, false), (true, false, false) */ - } - - def "Check storage level after calculating next state with #chpState and heat demand #heatDemand kWh:"() { - given: - def chpData = buildChpRelevantData(chpState, heatDemand) - def thermalStorage = buildThermalStorage(storageInput, storageLvl) - def chpModel = buildChpModel(thermalStorage) - - when: - chpModel.calculateNextState(chpData) - - then: - thermalStorage._storedEnergy() =~ expectedStoredEnergy - - where: - chpState | storageLvl | heatDemand | expectedStoredEnergy - chpStateNotRunning | 90d | 0d || 1035d // tests case (false, false, true) - chpStateNotRunning | 90d | 8d * 115d || 230d // tests case (false, true, false) - chpStateNotRunning | 90d | 10d || 1025d // tests case (false, true, true) - chpStateRunning | 90d | 0d || 1135d // tests case (true, false, true) - chpStateRunning | 90d | 8d * 115d || 230d // tests case (true, true, false) - chpStateRunning | 90d | 10d || 1125d // tests case (true, true, true) - chpStateRunning | 90d | 806d || 329d // test case (_, true, false) and demand covered together with chp - chpStateRunning | 90d | 9d * 115d || 230d // test case (_, true, false) and demand not covered together with chp - chpStateRunning | 92d | 1d || 1150d // test case (true, true, true) and storage volume exceeds maximum - /* The following tests do not exist: (false, false, false), (true, false, false) */ - } - - def "Check time tick and running status after calculating next state with #chpState and heat demand #heatDemand kWh:"() { - given: - def chpData = buildChpRelevantData(chpState, heatDemand) - def thermalStorage = buildThermalStorage(storageInput, storageLvl) - def chpModel = buildChpModel(thermalStorage) - - when: - def nextState = chpModel.calculateNextState(chpData) - - then: - nextState.lastTimeTick() == expectedTimeTick - nextState.isRunning() == expectedRunningStatus - - where: - chpState | storageLvl | heatDemand | expectedTimeTick | expectedRunningStatus - chpStateNotRunning | 90 | 0 || 7200 | false // tests case (false, false, true) - chpStateNotRunning | 90 | 8 * 115 || 7200 | true // tests case (false, true, false) - chpStateNotRunning | 90 | 10 || 7200 | false // tests case (false, true, true) - chpStateRunning | 90 | 0 || 7200 | true // tests case (true, false, true) - chpStateRunning | 90 | 8 * 115 || 7200 | true // tests case (true, true, false) - chpStateRunning | 90 | 10 || 7200 | true // tests case (true, true, true) - chpStateRunning | 90 | 806 || 7200 | true // test case (_, true, false) and demand covered together with chp - chpStateRunning | 90 | 9 * 115 || 7200 | true // test case (_, true, false) and demand not covered together with chp - chpStateRunning | 92 | 1 || 7200 | false // test case (true, true, true) and storage volume exceeds maximum - /* The following tests do not exist: (false, false, false), (true, false, false) */ - } - - def "Check apply, validation and build method:"() { - when: - def thermalStorage = buildThermalStorage(storageInput, 90) - def chpModelCaseClass = buildChpModel(thermalStorage) - def startDate = TimeUtil.withDefaults.toZonedDateTime("2021-01-01T00:00:00Z") - def endDate = startDate.plusSeconds(86400L) - def chpModelCaseObject = ChpModel.apply( - chpInput, - startDate, - endDate, - null, - 1.0, - thermalStorage) - - then: - chpModelCaseClass.sRated() == chpModelCaseObject.sRated() - chpModelCaseClass.cosPhiRated() == chpModelCaseObject.cosPhiRated() - chpModelCaseClass.pThermal() == chpModelCaseObject.pThermal() - chpModelCaseClass.storage() == chpModelCaseObject.storage() - } -} diff --git a/src/test/groovy/edu/ie3/simona/model/participant/SystemParticipantTest.groovy b/src/test/groovy/edu/ie3/simona/model/participant/SystemParticipantTest.groovy deleted file mode 100644 index d13782be0c..0000000000 --- a/src/test/groovy/edu/ie3/simona/model/participant/SystemParticipantTest.groovy +++ /dev/null @@ -1,242 +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.participant - -import edu.ie3.datamodel.models.input.system.characteristic.CosPhiFixed -import edu.ie3.datamodel.models.input.system.characteristic.CosPhiP -import edu.ie3.datamodel.models.input.system.characteristic.QV -import edu.ie3.simona.model.participant.control.QControl -import edu.ie3.simona.test.common.model.MockParticipant -import edu.ie3.util.scala.OperationInterval -import edu.ie3.util.scala.quantities.Sq -import spock.lang.Specification -import squants.* -import squants.energy.* - -class SystemParticipantTest extends Specification { - - def "Test calculateQ for a load or generation unit with fixed cosphi"() { - given: "the mocked system participant model with a q_v characteristic" - - def loadMock = new MockParticipant( - UUID.fromString("b69f6675-5284-4e28-add5-b76952ec1ec2"), - "System participant calculateQ Test", - OperationInterval.apply(0L, 86400L), - QControl.apply(new CosPhiFixed(varCharacteristicString)), - Sq.create(200, Kilowatts$.MODULE$), - 1d) - Dimensionless adjustedVoltage = Sq.create(1, Each$.MODULE$) // needed for method call but not applicable for cosphi_p - - when: "the reactive power is calculated" - Power power = Sq.create(pVal, Kilowatts$.MODULE$) - def qCalc = loadMock.calculateReactivePower(power, adjustedVoltage) - - then: "compare the results in watt" - Math.abs(qCalc.toKilovars() - qSol.doubleValue()) < 0.0001 - - where: - varCharacteristicString | pVal || qSol - "cosPhiFixed:{(0.0,0.9)}" | 0 || 0 - "cosPhiFixed:{(0.0,0.9)}" | 50 || 24.216105241892627000 - "cosPhiFixed:{(0.0,0.9)}" | 100 || 48.432210483785254000 - "cosPhiFixed:{(0.0,0.9)}" | 200 || 0 - "cosPhiFixed:{(0.0,0.9)}" | -50 || -24.216105241892627000 - "cosPhiFixed:{(0.0,0.9)}" | -100 || -48.432210483785254000 - "cosPhiFixed:{(0.0,0.9)}" | -200 || 0 - "cosPhiFixed:{(0.0,1.0)}" | 100 || 0 - } - - def "Test calculateQ for a load unit with cosphi_p"() { - given: "the mocked load model" - - def loadMock = new MockParticipant( - UUID.fromString("3d28b9f7-929a-48e3-8696-ad2330a04225"), - "Load calculateQ Test", - OperationInterval.apply(0L, 86400L), - QControl.apply(new CosPhiP(varCharacteristicString)), - Sq.create(102, Kilowatts$.MODULE$), - 1d) - - Dimensionless adjustedVoltage = Sq.create(1, Each$.MODULE$) // needed for method call but not applicable for cosphi_p - - when: "the reactive power is calculated" - Power power = Sq.create(p, Kilowatts$.MODULE$) - def qCalc = loadMock.calculateReactivePower(power, adjustedVoltage) - - then: "compare the results in watt" - Math.abs(qCalc.toKilovars() - qSol.doubleValue()) < 0.0001 - - where: // explained below - varCharacteristicString | p || qSol - "cosPhiP:{(0,1),(0.05,1),(0.1,1),(0.15,1),(0.2,1),(0.25,1),(0.3,1),(0.35,1),(0.4,1),(0.45,1),(0.5,1),(0.55,0.99),(0.6,0.98),(0.65,0.97),(0.7,0.96),(0.75,0.95),(0.8,0.94),(0.85,0.93),(0.9,0.92),(0.95,0.91),(1,0.9)}" | 100.0d || 20.09975124224169d - "cosPhiP:{(0,-1),(0.05,-1),(0.1,-1),(0.15,-1),(0.2,-1),(0.25,-1),(0.3,-1),(0.35,-1),(0.4,-1),(0.45,-1),(0.5,-1),(0.55,-0.99),(0.6,-0.98),(0.65,-0.97),(0.7,-0.96),(0.75,-0.95),(0.8,-0.94),(0.85,-0.93),(0.9,-0.92),(0.95,-0.91),(1,-0.9)}" | 100.0d || -20.09975124224169d - - // first line is "with P" -> positive Q (influence on voltage level: decrease) is expected - // second line is "against P" -> negative Q (influence on voltage level: increase) is expected - } - - def "Test calculateQ for a generation unit with cosphi_p"() { - given: "the mocked generation model" - - def loadMock = new MockParticipant( - UUID.fromString("30f84d97-83b4-4b71-9c2d-dbc7ebb1127c"), - "Generation calculateQ Test", - OperationInterval.apply(0L, 86400L), - QControl.apply(new CosPhiP(varCharacteristicString)), - Sq.create(101, Kilowatts$.MODULE$), - 1d) - - Dimensionless adjustedVoltage = Sq.create(1, Each$.MODULE$) // needed for method call but not applicable for cosphi_p - - when: "the reactive power is calculated" - Power power = Sq.create(p, Kilowatts$.MODULE$) - def qCalc = loadMock.calculateReactivePower(power, adjustedVoltage) - - then: "compare the results in watt" - Math.abs(qCalc.toKilovars() - qSol.doubleValue()) < 0.0001 - - where: // explained below - varCharacteristicString | p || qSol - "cosPhiP:{(-1,0.9),(-0.95,0.91),(-0.9,0.92),(-0.85,0.93),(-0.8,0.94),(-0.75,0.95),(-0.7,0.96),(-0.65,0.97),(-0.6,0.98),(-0.55,0.99),(-0.5,1),(-0.45,1),(-0.4,1),(-0.35,1),(-0.3,1),(-0.25,1),(-0.2,1),(-0.15,1),(-0.1,1),(-0.05,1),(0,1)}" | -100.0d || -14.177446878757818d - "cosPhiP:{(-1,-0.9),(-0.95,-0.91),(-0.9,-0.92),(-0.85,-0.93),(-0.8,-0.94),(-0.75,-0.95),(-0.7,-0.96),(-0.65,-0.97),(-0.6,-0.98),(-0.55,-0.99),(-0.5,-1),(-0.45,-1),(-0.4,-1),(-0.35,-1),(-0.3,-1),(-0.25,-1),(-0.2,-1),(-0.15,-1),(-0.1,-1),(-0.05,-1),(0,-1)}" | -100.0d || 14.177446878757818d - - // first line is "with P" -> negative Q (influence on voltage level: increase) is expected - // second line is "against P" -> positive Q (influence on voltage level: decrease) is expected - } - - def "Test calculateQ for a standard q_v characteristic"() { - given: "the mocked system participant model with a q_v characteristic" - - Power p = Sq.create(42, Kilowatts$.MODULE$) - - def loadMock = new MockParticipant( - UUID.fromString("d8461624-d142-4360-8e02-c21965ec555e"), - "System participant calculateQ Test", - OperationInterval.apply(0L, 86400L), - QControl.apply(new QV("qV:{(0.93,-1),(0.97,0),(1,0),(1.03,0),(1.07,1)}")), - Sq.create(200, Kilowatts$.MODULE$), - 0.98) - - when: "the reactive power is calculated" - Dimensionless adjustedVoltage = Sq.create(adjustedVoltageVal.doubleValue(), Each$.MODULE$) - def qCalc = loadMock.calculateReactivePower(p, adjustedVoltage) - - then: "compare the results in watt" - Math.abs(qCalc.toKilovars() - qSol.doubleValue()) < 0.0001 - - where: - adjustedVoltageVal || qSol - 0.9 || -39.79949748426482 - 0.93 || -39.79949748426482 - 0.95 || -19.89974874213241 - 0.97 || 0 - 1.00 || 0 - 1.03 || 0 - 1.05 || 19.89974874213241 - 1.07 || 39.79949748426482 - 1.1 || 39.79949748426482 - } - - def "Test calculateQ for a standard q_v characteristic if active power is zero and cosPhiRated 1"() { - given: "the mocked system participant model with a q_v characteristic" - - Power p = Sq.create(0, Kilowatts$.MODULE$) - - def loadMock = new MockParticipant( - UUID.fromString("d8461624-d142-4360-8e02-c21965ec555e"), - "System participant calculateQ Test", - OperationInterval.apply(0L, 86400L), - QControl.apply(new QV("qV:{(0.93,-1),(0.97,0),(1,0),(1.03,0),(1.07,1)}")), - Sq.create(200, Kilowatts$.MODULE$), - 1d) - - when: "the reactive power is calculated" - Dimensionless adjustedVoltage = Sq.create(adjustedVoltageVal.doubleValue(), Each$.MODULE$) - def qCalc = loadMock.calculateReactivePower(p, adjustedVoltage) - - then: "compare the results in watt" - Math.abs(qCalc.toKilovars() - qSol.doubleValue()) < 0.0001 - - where: - adjustedVoltageVal || qSol - 0.9 || 0 - 0.93 || 0 - 0.95 || 0 - 0.97 || 0 - 1.00 || 0 - 1.03 || 0 - 1.05 || 0 - 1.07 || 0 - 1.1 || 0 - } - - def "Test calculateQ for a standard q_v characteristic if active power is not zero and cosPhiRated 0.95"() { - given: "the mocked system participant model with a q_v characteristic" - - Power p = Sq.create(100, Kilowatts$.MODULE$) - - def loadMock = new MockParticipant( - UUID.fromString("d8461624-d142-4360-8e02-c21965ec555e"), - "System participant calculateQ Test", - OperationInterval.apply(0L, 86400L), - QControl.apply(new QV("qV:{(0.93,-1),(0.97,0),(1,0),(1.03,0),(1.07,1)}")), - Sq.create(200, Kilowatts$.MODULE$), - 0.95) - - when: "the reactive power is calculated" - Dimensionless adjustedVoltage = Sq.create(adjustedVoltageVal.doubleValue(), Each$.MODULE$) - def qCalc = loadMock.calculateReactivePower(p, adjustedVoltage) - - then: "compare the results in watt" - Math.abs(qCalc.toKilovars() - qSol.doubleValue()) < 0.0001 - - where: - adjustedVoltageVal || qSol - 0.9 || -62.449979983984 - 0.93 || -62.449979983984 - 0.95 || -31.224989991992 - 0.97 || 0 - 1.00 || 0 - 1.03 || 0 - 1.05 || 31.224989991992 - 1.07 || 62.449979983984 - 1.1 || 62.449979983984 - } - - def "Test calculateQ for a standard q_v characteristic if active power is 195 and cosPhiRated 0.95"() { - given: "the mocked system participant model with a q_v characteristic" - - Power p = Sq.create(195, Kilowatts$.MODULE$) - - def loadMock = new MockParticipant( - UUID.fromString("d8461624-d142-4360-8e02-c21965ec555e"), - "System participant calculateQ Test", - OperationInterval.apply(0L, 86400L), - QControl.apply(new QV("qV:{(0.93,-1),(0.97,0),(1,0),(1.03,0),(1.07,1)}")), - Sq.create(200, Kilowatts$.MODULE$), - 0.95) - - when: "the reactive power is calculated" - Dimensionless adjustedVoltage = Sq.create(adjustedVoltageVal.doubleValue(), Each$.MODULE$) - def qCalc = loadMock.calculateReactivePower(p, adjustedVoltage) - - then: "compare the results in watt" - Math.abs(qCalc.toKilovars() - qSol.doubleValue()) < 0.0001 - - where: - adjustedVoltageVal || qSol - 0.9 || -44.440972086578 - 0.93 || -44.440972086578 - 0.95 || -31.224989991992 - 0.97 || 0 - 1.00 || 0 - 1.03 || 0 - 1.05 || 31.224989991992 - 1.07 || 44.440972086578 - 1.1 || 44.440972086578 - } -} diff --git a/src/test/groovy/edu/ie3/simona/test/common/model/MockParticipant.groovy b/src/test/groovy/edu/ie3/simona/test/common/model/MockParticipant.groovy deleted file mode 100644 index acfc06fa14..0000000000 --- a/src/test/groovy/edu/ie3/simona/test/common/model/MockParticipant.groovy +++ /dev/null @@ -1,60 +0,0 @@ -/* - * © 2022. TU Dortmund University, - * Institute of Energy Systems, Energy Efficiency and Energy Economics, - * Research group Distribution grid planning and operation - */ - -package edu.ie3.simona.test.common.model - -import edu.ie3.simona.agent.participant.data.Data -import edu.ie3.simona.model.participant.CalcRelevantData -import edu.ie3.simona.model.participant.ModelState -import edu.ie3.simona.model.participant.SystemParticipant -import edu.ie3.simona.model.participant.control.QControl -import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage -import edu.ie3.util.scala.OperationInterval -import edu.ie3.util.scala.quantities.Sq -import scala.Tuple2 -import squants.Dimensionless -import squants.energy.* - -class MockParticipant extends SystemParticipant { - - MockParticipant( - UUID uuid, - String id, - OperationInterval operationInterval, - QControl qControl, - Power sRated, - Double cosPhiRated - ) { - super( - uuid, - id, - operationInterval, - qControl, - sRated, - cosPhiRated - ) - } - - @Override - Data.PrimaryData.ApparentPower calculatePower(long tick, Dimensionless voltage, ModelState state, CalcRelevantData data) { - return super.calculateApparentPower(tick, voltage, state, data) - } - - @Override - Power calculateActivePower(ModelState maybeModelState, CalcRelevantData data) { - return Sq.create(0, Megawatts$.MODULE$) - } - - @Override - FlexibilityMessage.ProvideFlexOptions determineFlexOptions(CalcRelevantData data, ModelState lastState) { - return null - } - - @Override - Tuple2 handleControlledPowerChange(CalcRelevantData data, ModelState lastState, Power setPower) { - return null - } -} diff --git a/src/test/scala/edu/ie3/simona/model/participant/ChpModelSpec.scala b/src/test/scala/edu/ie3/simona/model/participant/ChpModelSpec.scala new file mode 100644 index 0000000000..bf1bb717ef --- /dev/null +++ b/src/test/scala/edu/ie3/simona/model/participant/ChpModelSpec.scala @@ -0,0 +1,381 @@ +/* + * © 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.participant + +import edu.ie3.datamodel.models.input.system.ChpInput +import edu.ie3.datamodel.models.input.system.`type`.ChpTypeInput +import edu.ie3.datamodel.models.input.system.characteristic.CosPhiFixed +import edu.ie3.datamodel.models.input.thermal.{ + CylindricalStorageInput, + ThermalBusInput, +} +import edu.ie3.datamodel.models.voltagelevels.GermanVoltageLevelUtils +import edu.ie3.datamodel.models.{OperationTime, StandardUnits} +import edu.ie3.simona.model.participant.ChpModel.{ChpRelevantData, ChpState} +import edu.ie3.simona.model.thermal.CylindricalThermalStorage +import edu.ie3.simona.test.common.{DefaultTestData, UnitSpec} +import edu.ie3.util.TimeUtil +import edu.ie3.util.quantities.PowerSystemUnits +import edu.ie3.util.quantities.PowerSystemUnits.{ + EURO, + EURO_PER_MEGAWATTHOUR, + KILOVOLTAMPERE, + KILOWATT, +} +import edu.ie3.util.scala.quantities._ +import org.scalatest.BeforeAndAfterAll +import org.scalatest.prop.TableDrivenPropertyChecks +import squants.energy.{KilowattHours, Kilowatts} +import squants.space.CubicMeters +import squants.thermal.Celsius +import tech.units.indriya.quantity.Quantities.getQuantity +import tech.units.indriya.unit.Units +import tech.units.indriya.unit.Units.PERCENT +import testutils.TestObjectFactory + +import java.util.UUID + +class ChpModelSpec + extends UnitSpec + with BeforeAndAfterAll + with TableDrivenPropertyChecks + with DefaultTestData { + + implicit val Tolerance: Double = 1e-12 + val chpStateNotRunning: ChpState = + ChpState(isRunning = false, 0, Kilowatts(0), KilowattHours(0)) + val chpStateRunning: ChpState = + ChpState(isRunning = true, 0, Kilowatts(0), KilowattHours(0)) + var storageInput: CylindricalStorageInput = _ + var chpInput: ChpInput = _ + + override def beforeAll(): Unit = { + setupSpec() + } + + def setupSpec(): Unit = { + val thermalBus = new ThermalBusInput(UUID.randomUUID(), "thermal bus") + + storageInput = new CylindricalStorageInput( + UUID.randomUUID(), + "ThermalStorage", + thermalBus, + getQuantity(100, StandardUnits.VOLUME), + getQuantity(20, StandardUnits.VOLUME), + getQuantity(30, StandardUnits.TEMPERATURE), + getQuantity(40, StandardUnits.TEMPERATURE), + getQuantity(1.15, StandardUnits.SPECIFIC_HEAT_CAPACITY), + ) + + val chpTypeInput = new ChpTypeInput( + UUID.randomUUID(), + "ChpTypeInput", + getQuantity(10000d, EURO), + getQuantity(200, EURO_PER_MEGAWATTHOUR), + getQuantity(19, PERCENT), + getQuantity(76, PERCENT), + getQuantity(100, KILOVOLTAMPERE), + 0.95, + getQuantity(50d, KILOWATT), + getQuantity(0, KILOWATT), + ) + + chpInput = new ChpInput( + UUID.randomUUID(), + "ChpInput", + null, + OperationTime.notLimited(), + TestObjectFactory + .buildNodeInput(false, GermanVoltageLevelUtils.MV_10KV, 0), + thermalBus, + new CosPhiFixed("cosPhiFixed:{(0.0,0.95)}"), + null, + chpTypeInput, + null, + false, + ) + } + + def buildChpModel(thermalStorage: CylindricalThermalStorage): ChpModel = { + ChpModel( + UUID.randomUUID(), + "ChpModel", + null, + null, + Kilowatts(100), + 0.95, + Kilowatts(50), + thermalStorage, + ) + } + + def buildChpRelevantData( + chpState: ChpState, + heatDemand: Double, + ): ChpRelevantData = { + ChpRelevantData(chpState, KilowattHours(heatDemand), 7200) + } + + def buildThermalStorage( + storageInput: CylindricalStorageInput, + volume: Double, + ): CylindricalThermalStorage = { + val storedEnergy = CylindricalThermalStorage.volumeToEnergy( + CubicMeters(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) + } + + "A ChpModel" should { + "Check active power after calculating next state with #chpState and heat demand #heatDemand kWh:" in { + val testCases = Table( + ("chpState", "storageLvl", "heatDemand", "expectedActivePower"), + (chpStateNotRunning, 90, 0, 0), // tests case (false, false, true) + ( + chpStateNotRunning, + 90, + 8 * 115, + 95, + ), // tests case (false, true, false) + (chpStateNotRunning, 90, 10, 0), // tests case (false, true, true) + (chpStateRunning, 90, 0, 95), // tests case (true, false, true) + (chpStateRunning, 90, 8 * 115, 95), // tests case (true, true, false) + (chpStateRunning, 90, 10, 95), // tests case (true, true, true) + ( + chpStateRunning, + 90, + 7 * 115 + 1, + 95, + ), // test case (_, true, false) and demand covered together with chp + ( + chpStateRunning, + 90, + 9 * 115, + 95, + ), // test case (_, true, false) and demand not covered together with chp + ( + chpStateRunning, + 92, + 1, + 95, + ), // test case (true, true, true) and storage volume exceeds maximum + ) + + forAll(testCases) { + (chpState, storageLvl, heatDemand, expectedActivePower) => + val chpData = buildChpRelevantData(chpState, heatDemand) + val thermalStorage = buildThermalStorage(storageInput, storageLvl) + val chpModel = buildChpModel(thermalStorage) + + val activePower = chpModel.calculateNextState(chpData).activePower + activePower.toKilowatts shouldEqual expectedActivePower + } + } + + "Check total energy after calculating next state with #chpState and heat demand #heatDemand kWh:" in { + val testCases = Table( + ("chpState", "storageLvl", "heatDemand", "expectedTotalEnergy"), + (chpStateNotRunning, 90, 0, 0), // tests case (false, false, true) + ( + chpStateNotRunning, + 90, + 8 * 115, + 100, + ), // tests case (false, true, false) + (chpStateNotRunning, 90, 10, 0), // tests case (false, true, true) + (chpStateRunning, 90, 0, 100), // tests case (true, false, true) + (chpStateRunning, 90, 8 * 115, 100), // tests case (true, true, false) + (chpStateRunning, 90, 10, 100), // tests case (true, true, true) + ( + chpStateRunning, + 90, + 7 * 115 + 1, + 100, + ), // test case (_, true, false) and demand covered together with chp + ( + chpStateRunning, + 90, + 9 * 115, + 100, + ), // test case (_, true, false) and demand not covered together with chp + ( + chpStateRunning, + 92, + 1, + 93, + ), // test case (true, true, true) and storage volume exceeds maximum + ) + + forAll(testCases) { + (chpState, storageLvl, heatDemand, expectedTotalEnergy) => + val chpData = buildChpRelevantData(chpState, heatDemand) + val thermalStorage = buildThermalStorage(storageInput, storageLvl) + val chpModel = buildChpModel(thermalStorage) + + val nextState = chpModel.calculateNextState(chpData) + val thermalEnergy = nextState.thermalEnergy + thermalEnergy.toKilowattHours shouldEqual expectedTotalEnergy + } + } + + "Check storage level after calculating next state with #chpState and heat demand #heatDemand kWh:" in { + val testCases = Table( + ("chpState", "storageLvl", "heatDemand", "expectedStoredEnergy"), + (chpStateNotRunning, 90, 0, 1035), // tests case (false, false, true) + ( + chpStateNotRunning, + 90, + 8 * 115, + 230, + ), // tests case (false, true, false) + (chpStateNotRunning, 90, 10, 1025), // tests case (false, true, true) + (chpStateRunning, 90, 0, 1135), // tests case (true, false, true) + (chpStateRunning, 90, 8 * 115, 230), // tests case (true, true, false) + (chpStateRunning, 90, 10, 1125), // tests case (true, true, true) + ( + chpStateRunning, + 90, + 806, + 329, + ), // test case (_, true, false) and demand covered together with chp + ( + chpStateRunning, + 90, + 9 * 115, + 230, + ), // test case (_, true, false) and demand not covered together with chp + ( + chpStateRunning, + 92, + 1, + 1150, + ), // test case (true, true, true) and storage volume exceeds maximum + ) + + forAll(testCases) { + (chpState, storageLvl, heatDemand, expectedStoredEnergy) => + val chpData = buildChpRelevantData(chpState, heatDemand) + val thermalStorage = buildThermalStorage(storageInput, storageLvl) + val chpModel = buildChpModel(thermalStorage) + + chpModel.calculateNextState(chpData) + thermalStorage._storedEnergy.toKilowattHours should be( + expectedStoredEnergy + ) + } + } + + "Check time tick and running status after calculating next state with #chpState and heat demand #heatDemand kWh:" in { + val testCases = Seq( + // (ChpState, Storage Level, Heat Demand, Expected Time Tick, Expected Running Status) + ( + chpStateNotRunning, + 90, + 0, + 7200, + false, + ), // Test case (false, false, true) + ( + chpStateNotRunning, + 90, + 8 * 115, + 7200, + true, + ), // Test case (false, true, false) + ( + chpStateNotRunning, + 90, + 10, + 7200, + false, + ), // Test case (false, true, true) + (chpStateRunning, 90, 0, 7200, true), // Test case (true, false, true) + ( + chpStateRunning, + 90, + 8 * 115, + 7200, + true, + ), // Test case (true, true, false) + (chpStateRunning, 90, 10, 7200, true), // Test case (true, true, true) + ( + chpStateRunning, + 90, + 806, + 7200, + true, + ), // Test case (_, true, false) and demand covered together with chp + ( + chpStateRunning, + 90, + 9 * 115, + 7200, + true, + ), // Test case (_, true, false) and demand not covered together with chp + ( + chpStateRunning, + 92, + 1, + 7200, + false, + ), // Test case (true, true, true) and storage volume exceeds maximum + ) + + for ( + ( + chpState, + storageLvl, + heatDemand, + expectedTimeTick, + expectedRunningStatus, + ) <- testCases + ) { + val chpData = buildChpRelevantData(chpState, heatDemand) + val thermalStorage = buildThermalStorage(storageInput, storageLvl) + val chpModel = buildChpModel(thermalStorage) + + val nextState = chpModel.calculateNextState(chpData) + + nextState.lastTimeTick shouldEqual expectedTimeTick + nextState.isRunning shouldEqual expectedRunningStatus + } + } + + "apply, validate, and build correctly" in { + val thermalStorage = buildThermalStorage(storageInput, 90) + val chpModelCaseClass = buildChpModel(thermalStorage) + val startDate = + TimeUtil.withDefaults.toZonedDateTime("2021-01-01T00:00:00Z") + val endDate = startDate.plusSeconds(86400L) + val chpModelCaseObject = ChpModel( + chpInput, + startDate, + endDate, + null, + 1.0, + thermalStorage, + ) + + chpModelCaseClass.sRated shouldEqual chpModelCaseObject.sRated + chpModelCaseClass.cosPhiRated shouldEqual chpModelCaseObject.cosPhiRated + chpModelCaseClass.pThermal shouldEqual chpModelCaseObject.pThermal + chpModelCaseClass.storage shouldEqual chpModelCaseObject.storage + } + } +} diff --git a/src/test/scala/edu/ie3/simona/model/participant/SystemParticipantSpec.scala b/src/test/scala/edu/ie3/simona/model/participant/SystemParticipantSpec.scala new file mode 100644 index 0000000000..0c36be3e9c --- /dev/null +++ b/src/test/scala/edu/ie3/simona/model/participant/SystemParticipantSpec.scala @@ -0,0 +1,266 @@ +/* + * © 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.participant + +import edu.ie3.datamodel.models.input.system.characteristic.{ + CosPhiFixed, + CosPhiP, + QV, +} +import edu.ie3.simona.model.participant.control.QControl +import edu.ie3.simona.test.common.UnitSpec +import edu.ie3.simona.test.common.model.MockParticipant +import edu.ie3.util.scala.OperationInterval +import edu.ie3.util.scala.quantities.{Kilovars, Megavars, ReactivePower} +import org.scalatest.matchers.should.Matchers +import squants._ +import squants.energy._ + +import java.util.UUID +import scala.language.postfixOps + +class SystemParticipantSpec extends UnitSpec with Matchers { + + private implicit val tolerance: ReactivePower = Megavars( + 1e-5 + ) + + "SystemParticipant" should { + "calculate reactive power correctly for fixed cos phi" in { + val adjustedVoltage = + Each(1) // not applicable for cos phi_fixed but required + + val testCases = Table( + ("varCharacteristicString", "pVal", "qSol"), + ("cosPhiFixed:{(0.0,0.9)}", 0, Kilovars(0)), + ("cosPhiFixed:{(0.0,0.9)}", 50, Kilovars(24.216)), + ("cosPhiFixed:{(0.0,0.9)}", 100, Kilovars(48.432)), + ("cosPhiFixed:{(0.0,0.9)}", 200, Kilovars(0)), + ("cosPhiFixed:{(0.0,0.9)}", -50, Kilovars(-24.216)), + ("cosPhiFixed:{(0.0,0.9)}", -100, Kilovars(-48.432)), + ("cosPhiFixed:{(0.0,0.9)}", -200, Kilovars(0)), + ("cosPhiFixed:{(0.0,1.0)}", 100, Kilovars(0)), + ) + + forAll(testCases) { (varCharacteristicString, pVal, qSol) => + val loadMock = new MockParticipant( + UUID.fromString("b69f6675-5284-4e28-add5-b76952ec1ec2"), + "System participant calculateQ Test", + OperationInterval(0L, 86400L), + QControl(new CosPhiFixed(varCharacteristicString)), + Kilowatts(200), + 1d, + ) + val power = Kilowatts(pVal) + val qCalc = loadMock.calculateReactivePower(power, adjustedVoltage) + qCalc should approximate(qSol) + } + } + } + + "calculate reactive power correctly for cosphi_p" in { + + val adjustedVoltage = + Each(1) // needed for method call but not applicable for cos phi_p + + val testCases = Table( + ("varCharacteristicString", "pVal", "qSol"), + ( + "cosPhiP:{(0,1),(0.05,1),(0.1,1),(0.15,1),(0.2,1),(0.25,1),(0.3,1),(0.35,1),(0.4,1),(0.45,1),(0.5,1),(0.55,0.99),(0.6,0.98),(0.65,0.97),(0.7,0.96),(0.75,0.95),(0.8,0.94),(0.85,0.93),(0.9,0.92),(0.95,0.91),(1,0.9)}", + 100, + Kilovars(20.099), + ), + ( + "cosPhiP:{(0,-1),(0.05,-1),(0.1,-1),(0.15,-1),(0.2,-1),(0.25,-1),(0.3,-1),(0.35,-1),(0.4,-1),(0.45,-1),(0.5,-1),(0.55,-0.99),(0.6,-0.98),(0.65,-0.97),(0.7,-0.96),(0.75,-0.95),(0.8,-0.94),(0.85,-0.93),(0.9,-0.92),(0.95,-0.91),(1,-0.9)}", + 100, + Kilovars(-20.099), + ), + ) + + // first line is "with P" -> negative Q (influence on voltage level: increase) is expected + // second line is "against P" -> positive Q (influence on voltage level: decrease) is expected + + forAll(testCases) { (varCharacteristicString, pVal, qSol) => + val loadMock = new MockParticipant( + UUID.fromString("30f84d97-83b4-4b71-9c2d-dbc7ebb1127c"), + "Generation calculateQ Test", + OperationInterval(0L, 86400L), + QControl( + new CosPhiP(varCharacteristicString) + ), + Kilowatts(102), + 1d, + ) + val power = Kilowatts(pVal) + val qCalc = loadMock.calculateReactivePower(power, adjustedVoltage) + qCalc should approximate(qSol) + } + } + + "calculate reactive power correctly for generation unit with cosphi_p" in { + val adjustedVoltage = + Each(1) // needed for method call but not applicable for cos phi_p + + val testCases = Table( + ("varCharacteristicString", "pVal", "qSol"), + ( + "cosPhiP:{(-1,0.9),(-0.95,0.91),(-0.9,0.92),(-0.85,0.93),(-0.8,0.94),(-0.75,0.95),(-0.7,0.96),(-0.65,0.97),(-0.6,0.98),(-0.55,0.99),(-0.5,1),(-0.45,1),(-0.4,1),(-0.35,1),(-0.3,1),(-0.25,1),(-0.2,1),(-0.15,1),(-0.1,1),(-0.05,1),(0,1)}", + -100, + Kilovars(-14.177), + ), + ( + "cosPhiP:{(-1,-0.9),(-0.95,-0.91),(-0.9,-0.92),(-0.85,-0.93),(-0.8,-0.94),(-0.75,-0.95),(-0.7,-0.96),(-0.65,-0.97),(-0.6,-0.98),(-0.55,-0.99),(-0.5,-1),(-0.45,-1),(-0.4,-1),(-0.35,-1),(-0.3,-1),(-0.25,-1),(-0.2,-1),(-0.15,-1),(-0.1,-1),(-0.05,-1),(0,-1)}", + -100, + Kilovars(14.177), + ), + ) + + // first line is "with P" -> negative Q (influence on voltage level: increase) is expected + // second line is "against P" -> positive Q (influence on voltage level: decrease) is expected + + forAll(testCases) { (varCharacteristicString, pVal, qSol) => + val loadMock = new MockParticipant( + UUID.fromString("30f84d97-83b4-4b71-9c2d-dbc7ebb1127c"), + "Generation calculateQ Test", + OperationInterval(0L, 86400L), + QControl( + new CosPhiP(varCharacteristicString) + ), + Kilowatts(101), + 1d, + ) + val power = Kilowatts(pVal) + val qCalc = loadMock.calculateReactivePower(power, adjustedVoltage) + qCalc should approximate(qSol) + + } + } + + "calculate reactive power correctly for a standard q_v characteristic" in { + val loadMock = new MockParticipant( + UUID.fromString("d8461624-d142-4360-8e02-c21965ec555e"), + "System participant calculateQ Test", + OperationInterval(0L, 86400L), + QControl(new QV("qV:{(0.93,-1),(0.97,0),(1,0),(1.03,0),(1.07,1)}")), + Kilowatts(200), + 0.98, + ) + + val testCases = Table( + ("adjustedVoltageVal", "qSol"), + (0.9, Kilovars(-39.799)), + (0.93, Kilovars(-39.799)), + (0.95, Kilovars(-19.899)), + (0.97, Kilovars(0)), + (1.00, Kilovars(0)), + (1.03, Kilovars(0)), + (1.05, Kilovars(19.899)), + (1.07, Kilovars(39.799)), + (1.1, Kilovars(39.799)), + ) + + forAll(testCases) { (adjustedVoltageVal, qSol) => + val adjustedVoltage = Each(adjustedVoltageVal) + val p = Kilowatts(42) + val qCalc = loadMock.calculateReactivePower(p, adjustedVoltage) + qCalc should approximate(qSol) + } + } + + "calculate reactive power correctly for q_v characteristic if active power is zero and cosPhiRated is 1" in { + val loadMock = new MockParticipant( + UUID.fromString("d8461624-d142-4360-8e02-c21965ec555e"), + "System participant calculateQ Test", + OperationInterval(0L, 86400L), + QControl(new QV("qV:{(0.93,-1),(0.97,0),(1,0),(1.03,0),(1.07,1)}")), + Kilowatts(200), + 1d, + ) + + val testCases = Table( + ("adjustedVoltageVal", "qSol"), + (0.9, Kilovars(0)), + (0.93, Kilovars(0)), + (0.95, Kilovars(0)), + (0.97, Kilovars(0)), + (1.00, Kilovars(0)), + (1.03, Kilovars(0)), + (1.05, Kilovars(0)), + (1.07, Kilovars(0)), + (1.1, Kilovars(0)), + ) + + forAll(testCases) { (adjustedVoltageVal, qSol) => + val adjustedVoltage = Each(adjustedVoltageVal) + val p = Kilowatts(0) + val qCalc = loadMock.calculateReactivePower(p, adjustedVoltage) + qCalc should approximate(qSol) + } + } + + "calculate reactive power correctly for q_v characteristic if active power is not zero and cosPhiRated is 0.95" in { + val loadMock = new MockParticipant( + UUID.fromString("d8461624-d142-4360-8e02-c21965ec555e"), + "System participant calculateQ Test", + OperationInterval(0L, 86400L), + QControl(new QV("qV:{(0.93,-1),(0.97,0),(1,0),(1.03,0),(1.07,1)}")), + Kilowatts(200), + 0.95, + ) + + val testCases = Table( + ("adjustedVoltageVal", "qSol"), + (0.9, Kilovars(-62.44)), + (0.93, Kilovars(-62.44)), + (0.95, Kilovars(-31.22)), + (0.97, Kilovars(0)), + (1.00, Kilovars(0)), + (1.03, Kilovars(0)), + (1.05, Kilovars(31.22)), + (1.07, Kilovars(62.44)), + (1.1, Kilovars(62.44)), + ) + + forAll(testCases) { (adjustedVoltageVal, expectedQ) => + val adjustedVoltage = Each(adjustedVoltageVal) + val p = Kilowatts(100) + val qCalc = loadMock.calculateReactivePower(p, adjustedVoltage) + qCalc should approximate(expectedQ) + } + } + + "calculate reactive power correctly for a standard q_v characteristic if active power is 195 and cosPhiRated is 0.95" in { + val activePower: Power = Kilowatts(195) + val loadMock = new MockParticipant( + UUID.fromString("d8461624-d142-4360-8e02-c21965ec555e"), + "System participant calculateQ Test", + OperationInterval(0L, 86400L), + QControl(new QV("qV:{(0.93,-1),(0.97,0),(1,0),(1.03,0),(1.07,1)}")), + Kilowatts(200), + 0.95, + ) + + val testCases = Table( + ("adjustedVoltageVal", "qSol"), + (0.9, Kilovars(-44.44)), + (0.93, Kilovars(-44.44)), + (0.95, Kilovars(-31.22)), + (0.97, Kilovars(0)), + (1.00, Kilovars(0)), + (1.03, Kilovars(0)), + (1.05, Kilovars(31.22)), + (1.07, Kilovars(44.44)), + (1.1, Kilovars(44.44)), + ) + + forAll(testCases) { (adjustedVoltageVal, qSol) => + val adjustedVoltage: Dimensionless = Each(adjustedVoltageVal) + val qCalc = loadMock.calculateReactivePower(activePower, adjustedVoltage) + qCalc should approximate(qSol) + } + } +} diff --git a/src/test/scala/edu/ie3/simona/test/common/model/MockParticipant.scala b/src/test/scala/edu/ie3/simona/test/common/model/MockParticipant.scala new file mode 100644 index 0000000000..ad407dbfb4 --- /dev/null +++ b/src/test/scala/edu/ie3/simona/test/common/model/MockParticipant.scala @@ -0,0 +1,74 @@ +/* + * © 2022. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.test.common.model + +import edu.ie3.simona.agent.participant.data.Data +import edu.ie3.simona.model.participant.control.QControl +import edu.ie3.simona.model.participant.{ + CalcRelevantData, + FlexChangeIndicator, + ModelState, + SystemParticipant, +} +import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage +import edu.ie3.util.scala.OperationInterval +import squants.Dimensionless +import squants.energy._ + +import java.util.UUID + +class MockParticipant( + uuid: UUID, + id: String, + operationInterval: OperationInterval, + qControl: QControl, + sRated: Power, + cosPhiRated: Double, +) extends SystemParticipant[ + CalcRelevantData, + Data.PrimaryData.ApparentPower, + ModelState, + ]( + uuid, + id, + operationInterval, + qControl, + sRated, + cosPhiRated, + ) { + + override def calculatePower( + tick: Long, + voltage: Dimensionless, + state: ModelState, + data: CalcRelevantData, + ): Data.PrimaryData.ApparentPower = { + super.calculateApparentPower(tick, voltage, state, data) + } + + override def calculateActivePower( + maybeModelState: ModelState, + data: CalcRelevantData, + ): Power = { + Kilowatts(0) + } + + override def determineFlexOptions( + data: CalcRelevantData, + lastState: ModelState, + ): FlexibilityMessage.ProvideFlexOptions = { + null + } + + override def handleControlledPowerChange( + data: CalcRelevantData, + lastState: ModelState, + setPower: Power, + ): (ModelState, FlexChangeIndicator) = { + (lastState, FlexChangeIndicator(changesAtTick = None)) + } +}