diff --git a/CHANGELOG.md b/CHANGELOG.md index dd9510d57d..fecbf6cdab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,6 +59,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Remove unnecessary dependency `pekko-connectors-csv` [#857](https://github.com/ie3-institute/simona/issues/857) - Rewrote RefSystemTest from groovy to scala [#646](https://github.com/ie3-institute/simona/issues/646) - Rewrote FixedFeedModelTest from groovy to scala [#646](https://github.com/ie3-institute/simona/issues/646) +- Rewrote WecModelTest from groovy to scala [#646](https://github.com/ie3-institute/simona/issues/646) ### Fixed - Removed a repeated line in the documentation of vn_simona config [#658](https://github.com/ie3-institute/simona/issues/658) diff --git a/src/main/scala/edu/ie3/simona/model/participant/WecModel.scala b/src/main/scala/edu/ie3/simona/model/participant/WecModel.scala index f2f34ac771..bffdd7fdae 100644 --- a/src/main/scala/edu/ie3/simona/model/participant/WecModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant/WecModel.scala @@ -90,7 +90,7 @@ final case class WecModel( * @return * active power output */ - override protected def calculateActivePower( + override def calculateActivePower( modelState: ConstantState.type, wecData: WecRelevantData, ): Power = { @@ -154,7 +154,7 @@ final case class WecModel( * @return * betz coefficient cₚ */ - private def determineBetzCoefficient( + def determineBetzCoefficient( windVelocity: Velocity ): Dimensionless = { betzCurve.interpolateXy(windVelocity) match { @@ -174,7 +174,7 @@ final case class WecModel( * current air pressure * @return */ - private def calculateAirDensity( + def calculateAirDensity( temperature: Temperature, airPressure: Option[Pressure], ): Density = { @@ -214,7 +214,7 @@ object WecModel { /** This class is initialized with a [[WecCharacteristicInput]], which * contains the needed betz curve. */ - final case class WecCharacteristic private ( + final case class WecCharacteristic( override val xyCoordinates: SortedSet[ XYPair[Velocity, Dimensionless] ] diff --git a/src/test/groovy/edu/ie3/simona/model/participant/WecModelTest.groovy b/src/test/groovy/edu/ie3/simona/model/participant/WecModelTest.groovy deleted file mode 100644 index 45c6a8ad0a..0000000000 --- a/src/test/groovy/edu/ie3/simona/model/participant/WecModelTest.groovy +++ /dev/null @@ -1,217 +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 edu.ie3.datamodel.models.StandardUnits.* -import static edu.ie3.simona.model.participant.WecModel.WecRelevantData -import static tech.units.indriya.quantity.Quantities.getQuantity - -import edu.ie3.datamodel.models.OperationTime -import edu.ie3.datamodel.models.input.NodeInput -import edu.ie3.datamodel.models.input.OperatorInput -import edu.ie3.datamodel.models.input.system.WecInput -import edu.ie3.datamodel.models.input.system.characteristic.CosPhiFixed -import edu.ie3.datamodel.models.input.system.characteristic.WecCharacteristicInput -import edu.ie3.datamodel.models.input.system.type.WecTypeInput -import edu.ie3.datamodel.models.voltagelevels.GermanVoltageLevelUtils -import edu.ie3.util.TimeUtil -import edu.ie3.util.scala.quantities.Sq -import scala.Option -import scala.Some -import spock.lang.Shared -import spock.lang.Specification -import spock.lang.Unroll -import squants.Each$ -import squants.motion.MetersPerSecond$ -import squants.motion.Pascals$ - -import squants.energy.Kilowatts$ -import squants.space.SquareMeters$ -import squants.thermal.Celsius$ - - -class WecModelTest extends Specification { - - @Shared - WecInput inputModel - @Shared - static final Double TOLERANCE = 1e-5 - - def setupSpec() { - def nodeInput = new NodeInput( - UUID.fromString("ad39d0b9-5ad6-4588-8d92-74c7d7de9ace"), - "NodeInput", - OperatorInput.NO_OPERATOR_ASSIGNED, - OperationTime.notLimited(), - getQuantity(1d, PU), - false, - NodeInput.DEFAULT_GEO_POSITION, - GermanVoltageLevelUtils.LV, - -1) - - def typeInput = new WecTypeInput( - UUID.randomUUID(), - "WecTypeInput", - getQuantity(1000, EURO), - getQuantity(1000, ENERGY_PRICE), - getQuantity(1200, S_RATED), - 0.95, - //Using the characteristics of the Enercon E-82 wind turbine. - new WecCharacteristicInput( - "cP:{(0.0,0.0), (1.0,0.0), (2.0,0.115933516), (3.0,0.286255595), (4.0,0.39610618), " + - "(5.0,0.430345211), (6.0,0.45944023), (7.0,0.479507331), (8.0,0.492113623), " + - "(9.0,0.500417188), (10.0,0.488466547), (11.0,0.420415054), (12.0,0.354241299), " + - "(13.0,0.288470591), (14.0,0.230965702), (15.0,0.18778367), (16.0,0.154728976), " + - "(17.0,0.128998552), (18.0,0.108671106), (19.0,0.09239975), (20.0,0.079221236), " + - "(21.0,0.068434282), (22.0,0.059520087), (23.0,0.052089249), (24.0,0.045845623), " + - "(25.0,0.040561273), (26.0,0.036058824), (27.0,0.032198846), (28.0,0.016618264), " + - "(29.0,0.010330976), (30.0,0.006091519), (31.0,0.003331177), (32.0,0.001641637), " + - "(33.0,0.000705423), (34.0,0.000196644), (35.0,0.0), (36.0,0.0), (37.0,0.0), (38.0,0.0), " + - "(39.0,0.0), (40.0,0.0), (41.0,0.0), (42.0,0.0), (43.0,0.0), (44.0,0.0), (45.0,0.0), " + - "(46.0,0.0), (47.0,0.0), (48.0,0.0), (49.0,0.0), (50.0,0.0)}" - ), - getQuantity(15, EFFICIENCY), - getQuantity(5281, ROTOR_AREA), - getQuantity(20, HUB_HEIGHT)) - - inputModel = new WecInput( - UUID.randomUUID(), - "WecInput", - new OperatorInput(UUID.randomUUID(), "NO_OPERATOR"), - OperationTime.notLimited(), - nodeInput, - CosPhiFixed.CONSTANT_CHARACTERISTIC, - null, - typeInput, - false) - } - - def buildWecModel() { - return WecModel.apply(inputModel, 1, - TimeUtil.withDefaults.toZonedDateTime("2020-01-01T00:00:00Z"), - TimeUtil.withDefaults.toZonedDateTime("2020-01-01T01:00:00Z")) - } - - @Unroll - def "Check build method of companion object"() { - when: - def wecModel = buildWecModel() - then: - wecModel.uuid() == inputModel.uuid - wecModel.id() == inputModel.id - wecModel.sRated() == Sq.create(inputModel.type.sRated.value.doubleValue(), Kilowatts$.MODULE$) - wecModel.cosPhiRated() == inputModel.type.cosPhiRated - wecModel.rotorArea() == Sq.create(inputModel.type.rotorArea.value.doubleValue(), SquareMeters$.MODULE$) - wecModel.betzCurve() == new WecModel.WecCharacteristic$().apply(inputModel.type.cpCharacteristic) - } - - @Unroll - def "Check active power output depending on velocity #velocity m/s"() { - given: - def wecModel = buildWecModel() - def wecData = new WecRelevantData( - Sq.create(velocity, MetersPerSecond$.MODULE$), - Sq.create(20, Celsius$.MODULE$), - new Some (Sq.create(101325d, Pascals$.MODULE$))) - - when: - def result = wecModel.calculateActivePower(ModelState.ConstantState$.MODULE$, wecData) - - then: - Math.abs((result.toWatts() - power.doubleValue())) < TOLERANCE - - where: - velocity || power - 1.0d || 0 - 2.0d || -2948.80958 - 3.0d || -24573.41320 - 7.0d || -522922.23257 - 9.0d || -1140000 - 13.0d || -1140000 - 15.0d || -1140000 - 19.0d || -1140000 - 23.0d || -1140000 - 27.0d || -1140000 - 34.0d || -24573.39638 - 40.0d || 0 - } - - @Unroll - def "Check active power output depending on temperature #temperature Celsius"() { - given: - def wecModel = buildWecModel() - def wecData = new WecRelevantData(Sq.create(3.0d, MetersPerSecond$.MODULE$), - Sq.create(temperature, Celsius$.MODULE$), new Some( Sq.create(101325d, Pascals$.MODULE$))) - - when: - def result = wecModel.calculateActivePower(ModelState.ConstantState$.MODULE$, wecData) - - then: - result.toWatts() =~ power - - where: - temperature || power - 35d || -23377.23862d - 20d || -24573.41320d - -25d || -29029.60338d - } - - @Unroll - def "Check determineBetzCoefficient method with wind velocity #velocity m/s:"() { - given: - def wecModel = buildWecModel() - def windVel = Sq.create(velocity, MetersPerSecond$.MODULE$) - - when: - def betzFactor = wecModel.determineBetzCoefficient(windVel) - def expected = Sq.create(betzResult, Each$.MODULE$) - - then: - betzFactor == expected - - where: - velocity || betzResult - 2d || 0.115933516d - 2.5d || 0.2010945555d - 18d || 0.108671106d - 27d || 0.032198846d - 34d || 0.000196644d - 40d || 0.0d - } - - @Unroll - def "Check calculateAirDensity method with temperature #temperature degrees Celsius and air pressure #pressure Pascal:"() { - given: - def wecModel = buildWecModel() - def temperatureV = Sq.create(temperature, Celsius$.MODULE$) - def pressureV = Option.empty() - - when: - if (pressure > 0) { - pressureV = new Some(Sq.create(pressure, Pascals$.MODULE$)) - } - def airDensity = wecModel.calculateAirDensity(temperatureV, pressureV).toKilogramsPerCubicMeter() - - then: - Math.abs(airDensity - densityResult) < TOLERANCE - - where: - temperature | pressure || densityResult - -15d | 100129.44d || 1.35121d - -5d | 99535.96d || 1.29311d - 0d | 99535.96d || 1.26944d - 5d | 100129.44d || 1.25405d - 20d | 100129.44d || 1.18988d - 25d | 100427.25d || 1.17341d - 37d | 100427.25d || 1.12801d - // test case, where no air pressure is given (see WecModel.calculateAirDensity) - 0d | -1.0d || 1.2041d - 5d | -1.0d || 1.2041d - 40d | -1.0d || 1.2041d - } -} diff --git a/src/test/scala/edu/ie3/simona/model/participant/WecModelSpec.scala b/src/test/scala/edu/ie3/simona/model/participant/WecModelSpec.scala new file mode 100644 index 0000000000..8933819773 --- /dev/null +++ b/src/test/scala/edu/ie3/simona/model/participant/WecModelSpec.scala @@ -0,0 +1,191 @@ +/* + * © 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.OperationTime +import edu.ie3.datamodel.models.input.system.WecInput +import edu.ie3.datamodel.models.input.system.`type`.WecTypeInput +import edu.ie3.datamodel.models.input.system.characteristic.{ + ReactivePowerCharacteristic, + WecCharacteristicInput, +} +import edu.ie3.datamodel.models.input.{NodeInput, OperatorInput} +import edu.ie3.datamodel.models.voltagelevels.GermanVoltageLevelUtils +import edu.ie3.simona.model.participant.WecModel.WecRelevantData +import edu.ie3.simona.test.common.{DefaultTestData, UnitSpec} +import edu.ie3.util.TimeUtil +import edu.ie3.util.quantities.PowerSystemUnits +import squants.energy.Watts +import squants.motion.{MetersPerSecond, Pascals} +import squants.thermal.Celsius +import squants.Each +import tech.units.indriya.quantity.Quantities +import tech.units.indriya.unit.Units.{METRE, PERCENT, SQUARE_METRE} + +import java.util.UUID + +class WecModelSpec extends UnitSpec with DefaultTestData { + + val nodeInput = new NodeInput( + UUID.fromString("ad39d0b9-5ad6-4588-8d92-74c7d7de9ace"), + "NodeInput", + OperatorInput.NO_OPERATOR_ASSIGNED, + OperationTime.notLimited(), + Quantities.getQuantity(1, PowerSystemUnits.PU), + false, + NodeInput.DEFAULT_GEO_POSITION, + GermanVoltageLevelUtils.LV, + -1, + ) + + val typeInput = new WecTypeInput( + UUID.randomUUID(), + "WecTypeInput", + Quantities.getQuantity(1000, PowerSystemUnits.EURO), + Quantities.getQuantity(1000, PowerSystemUnits.EURO_PER_MEGAWATTHOUR), + Quantities.getQuantity(1200.0, PowerSystemUnits.KILOVOLTAMPERE), + 0.95, + new WecCharacteristicInput( + "cP:{(0.0,0.0), (1.0,0.0), (2.0,0.115933516), (3.0,0.286255595), (4.0,0.39610618), " + + "(5.0,0.430345211), (6.0,0.45944023), (7.0,0.479507331), (8.0,0.492113623), " + + "(9.0,0.500417188), (10.0,0.488466547), (11.0,0.420415054), (12.0,0.354241299), " + + "(13.0,0.288470591), (14.0,0.230965702), (15.0,0.18778367), (16.0,0.154728976), " + + "(17.0,0.128998552), (18.0,0.108671106), (19.0,0.09239975), (20.0,0.079221236), " + + "(21.0,0.068434282), (22.0,0.059520087), (23.0,0.052089249), (24.0,0.045845623), " + + "(25.0,0.040561273), (26.0,0.036058824), (27.0,0.032198846), (28.0,0.016618264), " + + "(29.0,0.010330976), (30.0,0.006091519), (31.0,0.003331177), (32.0,0.001641637), " + + "(33.0,0.000705423), (34.0,0.000196644), (35.0,0.0), (36.0,0.0), (37.0,0.0), (38.0,0.0), " + + "(39.0,0.0), (40.0,0.0), (41.0,0.0), (42.0,0.0), (43.0,0.0), (44.0,0.0), (45.0,0.0), " + + "(46.0,0.0), (47.0,0.0), (48.0,0.0), (49.0,0.0), (50.0,0.0)}" + ), + Quantities.getQuantity(15, PERCENT), + Quantities.getQuantity(5281.0, SQUARE_METRE), + Quantities.getQuantity(20, METRE), + ) + + val inputModel = new WecInput( + UUID.randomUUID(), + "WecTypeInput", + nodeInput, + ReactivePowerCharacteristic.parse("cosPhiFixed:{(0.00,0.95)}"), + null, + typeInput, + false, + ) + + def buildWecModel(): WecModel = { + WecModel.apply( + inputModel, + 1, + TimeUtil.withDefaults.toZonedDateTime("2020-01-01T00:00:00Z"), + TimeUtil.withDefaults.toZonedDateTime("2020-01-01T01:00:00Z"), + ) + } + + "WecModel" should { + + "check build method of companion object" in { + val wecModel = buildWecModel() + wecModel.uuid shouldBe inputModel.getUuid + wecModel.id shouldBe inputModel.getId + wecModel.rotorArea.toSquareMeters shouldBe (typeInput.getRotorArea.toSystemUnit.getValue + .doubleValue() +- 1e-5) + wecModel.cosPhiRated shouldBe typeInput.getCosPhiRated + wecModel.sRated.toWatts shouldBe (typeInput.getsRated.toSystemUnit.getValue + .doubleValue() +- 1e-5) + wecModel.betzCurve shouldBe WecModel.WecCharacteristic.apply( + inputModel.getType.getCpCharacteristic + ) + } + + "determine Betz coefficient correctly" in { + val wecModel = buildWecModel() + val velocities = Seq(2.0, 2.5, 18.0, 27.0, 34.0, 40.0) + val expectedBetzResults = Seq(0.115933516, 0.2010945555, 0.108671106, + 0.032198846, 0.000196644, 0.0) + velocities.zip(expectedBetzResults).foreach { + case (velocity, betzResult) => + val windVel = MetersPerSecond(velocity) + val betzFactor = wecModel.determineBetzCoefficient(windVel) + val expected = Each(betzResult) + betzFactor shouldEqual expected + } + } + + "calculate active power output depending on velocity" in { + val wecModel = buildWecModel() + val velocities = + Seq(1.0, 2.0, 3.0, 7.0, 9.0, 13.0, 15.0, 19.0, 23.0, 27.0, 34.0, 40.0) + val expectedPowers = + Seq(0, -2948.8095851378266, -24573.41320418286, -522922.2325710509, + -1140000, -1140000, -1140000, -1140000, -1140000, -1140000, + -24573.39638823692, 0) + + velocities.zip(expectedPowers).foreach { case (velocity, power) => + val wecData = new WecRelevantData( + MetersPerSecond(velocity), + Celsius(20), + Some(Pascals(101325d)), + ) + val result = + wecModel.calculateActivePower(ModelState.ConstantState, wecData) + val expectedPower = Watts(power) + + result should be(expectedPower) + } + } + + "calculate air density correctly" in { + val wecModel = buildWecModel() + val testCases = Seq( + (-15.0, 100129.44, 1.3512151548083537), + (-5.0, 99535.96, 1.2931147269065832), + (0.0, 99535.96, 1.2694443127219486), + (5.0, 100129.44, 1.25405785444464), + (20.0, 100129.44, 1.1898897909390296), + (25.0, 100427.25, 1.1734149214121568), + (37.0, 100427.25, 1.1280143763309192), + // test cases where no air pressure is given + (0.0, -1.0, 1.2041), + (5.0, -1.0, 1.2041), + (40.0, -1.0, 1.2041), + ) + + testCases.foreach { case (temperature, pressure, densityResult) => + val temperatureV = Celsius(temperature) + val pressureV = + if (pressure > 0) Some(Pascals(pressure)) else Option.empty + + val airDensity = wecModel + .calculateAirDensity(temperatureV, pressureV) + .toKilogramsPerCubicMeter + + airDensity should be(densityResult) + } + } + + "calculate active power output depending on temperature" in { + val wecModel = buildWecModel() + val temperatures = Seq(35.0, 20.0, -25.0) + val expectedPowers = + Seq(-23377.23862017266, -24573.41320418286, -29029.60338829823) + + temperatures.zip(expectedPowers).foreach { case (temperature, power) => + val wecData = new WecRelevantData( + MetersPerSecond(3.0), + Celsius(temperature), + Some(Pascals(101325d)), + ) + val result = { + wecModel.calculateActivePower(ModelState.ConstantState, wecData) + } + val expectedPower = Watts(power) + result shouldBe expectedPower + } + } + } +}