From 6f46fc1c15163aaeaa187003b50f916adeb5a28a Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Tue, 13 Jun 2023 16:09:37 +0200 Subject: [PATCH 1/2] Adapting WecModel and test --- .../wec/WecAgentFundamentals.scala | 23 +++- .../model/participant/SystemParticipant.scala | 7 +- .../simona/model/participant/WecModel.scala | 100 +++++++++--------- .../simona/model/system/Characteristic.scala | 4 +- .../model/participant/WecModelTest.groovy | 36 ++++--- .../participant/CharacteristicTestData.scala | 15 ++- 6 files changed, 103 insertions(+), 82 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/agent/participant/wec/WecAgentFundamentals.scala b/src/main/scala/edu/ie3/simona/agent/participant/wec/WecAgentFundamentals.scala index aaa4097420..9d17103555 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant/wec/WecAgentFundamentals.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant/wec/WecAgentFundamentals.scala @@ -8,21 +8,34 @@ package edu.ie3.simona.agent.participant.wec import akka.actor.{ActorRef, FSM} import edu.ie3.datamodel.models.input.system.WecInput -import edu.ie3.datamodel.models.result.system.{SystemParticipantResult, WecResult} +import edu.ie3.datamodel.models.result.system.{ + SystemParticipantResult, + WecResult +} import edu.ie3.simona.agent.ValueStore import edu.ie3.simona.agent.participant.ParticipantAgent._ import edu.ie3.simona.agent.participant.ParticipantAgentFundamentals -import edu.ie3.simona.agent.participant.data.Data.PrimaryData.{ApparentPower, ZERO_POWER} +import edu.ie3.simona.agent.participant.data.Data.PrimaryData.{ + ApparentPower, + ZERO_POWER +} import edu.ie3.simona.agent.participant.data.Data.SecondaryData import edu.ie3.simona.agent.participant.data.secondary.SecondaryDataService import edu.ie3.simona.agent.participant.statedata.BaseStateData._ -import edu.ie3.simona.agent.participant.statedata.{DataCollectionStateData, ParticipantStateData} +import edu.ie3.simona.agent.participant.statedata.{ + DataCollectionStateData, + ParticipantStateData +} import edu.ie3.simona.agent.participant.wec.WecAgent.neededServices import edu.ie3.simona.agent.state.AgentState import edu.ie3.simona.agent.state.AgentState.Idle import edu.ie3.simona.config.SimonaConfig.WecRuntimeConfig import edu.ie3.simona.event.notifier.ParticipantNotifierConfig -import edu.ie3.simona.exceptions.agent.{AgentInitializationException, InconsistentStateException, InvalidRequestException} +import edu.ie3.simona.exceptions.agent.{ + AgentInitializationException, + InconsistentStateException, + InvalidRequestException +} import edu.ie3.simona.model.participant.WecModel import edu.ie3.simona.model.participant.WecModel.WecRelevantData import edu.ie3.simona.ontology.messages.services.WeatherMessage.WeatherData @@ -215,7 +228,7 @@ protected trait WecAgentFundamentals WecRelevantData( weatherData.windVel, weatherData.temp, - Pascals(0d) // weather data does not support air pressure + None // weather data does not support air pressure ) val power = wecModel.calculatePower( diff --git a/src/main/scala/edu/ie3/simona/model/participant/SystemParticipant.scala b/src/main/scala/edu/ie3/simona/model/participant/SystemParticipant.scala index 0939e0f906..8c58ea58b2 100644 --- a/src/main/scala/edu/ie3/simona/model/participant/SystemParticipant.scala +++ b/src/main/scala/edu/ie3/simona/model/participant/SystemParticipant.scala @@ -9,9 +9,12 @@ package edu.ie3.simona.model.participant import edu.ie3.simona.agent.participant.data.Data.PrimaryData.ApparentPower import edu.ie3.simona.model.SystemComponent import edu.ie3.simona.model.participant.control.QControl -import edu.ie3.util.quantities.PowerSystemUnits._ import edu.ie3.util.scala.OperationInterval -import edu.ie3.util.scala.quantities.{DefaultQuantities, Megavars, ReactivePower} +import edu.ie3.util.scala.quantities.{ + DefaultQuantities, + Megavars, + ReactivePower +} import squants.energy.Kilowatts import java.util.UUID 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 44d1e7a489..bf352c3b61 100644 --- a/src/main/scala/edu/ie3/simona/model/participant/WecModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant/WecModel.scala @@ -16,26 +16,18 @@ import edu.ie3.simona.model.participant.WecModel.{ import edu.ie3.simona.model.participant.control.QControl import edu.ie3.simona.model.system.Characteristic import edu.ie3.simona.model.system.Characteristic.XYPair -import edu.ie3.util.quantities.EmptyQuantity -import edu.ie3.util.quantities.PowerSystemUnits.{ - KILOGRAM_PER_CUBIC_METRE, - KILOWATT, - MEGAWATT -} -import edu.ie3.util.quantities.QuantityUtils.RichQuantityDouble - +import edu.ie3.util.quantities.PowerSystemUnits._ import edu.ie3.util.scala.OperationInterval -import squants.Velocity -import squants.energy.{Kilowatts, Megawatts} +import squants.energy.{Kilowatts, Watts} import squants.mass.{Kilograms, KilogramsPerCubicMeter} +import squants.motion.MetersPerSecond +import squants.space.SquareMeters import squants.thermal.JoulesPerKelvin -import tech.units.indriya.ComparableQuantity - +import squants.{Each, Velocity} import tech.units.indriya.unit.Units._ import java.time.ZonedDateTime import java.util.UUID -import javax.measure.quantity._ import scala.collection.SortedSet /** A wind energy converter model used for calculating output power of a wind @@ -66,9 +58,9 @@ final case class WecModel( operationInterval: OperationInterval, scalingFactor: Double, qControl: QControl, - sRated: ComparableQuantity[Power], + sRated: squants.Power, cosPhiRated: Double, - rotorArea: ComparableQuantity[Area], + rotorArea: squants.Area, betzCurve: WecCharacteristic ) extends SystemParticipant[WecRelevantData]( uuid, @@ -76,7 +68,7 @@ final case class WecModel( operationInterval, scalingFactor, qControl, - Kilowatts(sRated.to(KILOWATT).getValue.doubleValue), + sRated, cosPhiRated ) { @@ -96,22 +88,20 @@ final case class WecModel( override protected def calculateActivePower( wecData: WecRelevantData ): squants.Power = { - val activePower = determinePower(wecData).to(MEGAWATT) - val pMax = sMax.toKilowatts.asKiloWatt.multiply(cosPhiRated).to(MEGAWATT) + val activePower = determinePower(wecData) + val pMax = sMax * cosPhiRated - Megawatts( - (if (activePower.isGreaterThan(pMax)) { - logger.warn( - "The fed in active power is higher than the estimated maximum active power of this plant ({} > {}). " + - "Did you provide wrong weather input data?", - activePower, - pMax - ) + (if (activePower > pMax) { + logger.warn( + "The fed in active power is higher than the estimated maximum active power of this plant ({} > {}). " + + "Did you provide wrong weather input data?", + activePower, pMax - } else { - activePower - }).multiply(-1).to(MEGAWATT).getValue.doubleValue - ) + ) + pMax + } else { + activePower + }) * (-1) } /** Determine the turbine output power with the air density ρ, the wind @@ -127,19 +117,27 @@ final case class WecModel( */ private def determinePower( wecData: WecRelevantData - ): ComparableQuantity[Power] = { + ): squants.Power = { val betzCoefficient = determineBetzCoefficient(wecData.windVelocity) + + /** air density in kg/m³ + */ val airDensity = - calculateAirDensity(wecData.temperature, wecData.airPressure) - val v = wecData.windVelocity - val cubedVelocity = v.multiply(v).multiply(v) + calculateAirDensity( + wecData.temperature, + wecData.airPressure + ).toKilogramsPerCubicMeter + + val v = wecData.windVelocity.toMetersPerSecond - cubedVelocity - .multiply(0.5) - .multiply(betzCoefficient) - .multiply(airDensity.to(KILOGRAM_PER_CUBIC_METRE)) - .multiply(rotorArea.to(SQUARE_METRE)) - .asType(classOf[Power]) + /** cubed velocity in m³/s³ + */ + val cubedVelocity = v * v * v + + // Combined, we get (kg * m²)/s³, which is Watts + Watts( + cubedVelocity * 0.5 * betzCoefficient.toEach * airDensity * rotorArea.toSquareMeters + ) } /** The coefficient is dependent on the wind velocity v. Therefore use v to @@ -172,15 +170,13 @@ final case class WecModel( */ private def calculateAirDensity( temperature: squants.Temperature, - airPressure: squants.motion.Pressure + airPressure: Option[squants.motion.Pressure] ): squants.Density = { airPressure match { - // OPTIONAL TODO DF - case _: EmptyQuantity[Pressure] => + case None => KilogramsPerCubicMeter(1.2041d) - case pressure => - AIR_MOLAR_MASS * (pressure.toPascals) - .divide(R * temperature.toKelvinScale) + case Some(pressure) => + AIR_MOLAR_MASS * pressure.toPascals / (R * temperature.toKelvinScale) } } } @@ -210,8 +206,8 @@ object WecModel { .SortedSet[XYPair[Velocity, squants.Dimensionless]]() ++ input.getPoints.asScala.map(p => XYPair[Velocity, squants.Dimensionless]( - p.getX.to(METRE_PER_SECOND), - p.getY + MetersPerSecond(p.getX.to(METRE_PER_SECOND).getValue.doubleValue), + Each(p.getY.to(PU).getValue.doubleValue) ) ) ) @@ -230,7 +226,7 @@ object WecModel { final case class WecRelevantData( windVelocity: squants.Velocity, temperature: squants.Temperature, - airPressure: squants.motion.Pressure + airPressure: Option[squants.motion.Pressure] ) extends CalcRelevantData def apply( @@ -251,9 +247,11 @@ object WecModel { operationInterval, scalingFactor, QControl(inputModel.getqCharacteristics), - inputModel.getType.getsRated, + Kilowatts(inputModel.getType.getsRated.to(KILOWATT).getValue.doubleValue), inputModel.getType.getCosPhiRated, - inputModel.getType.getRotorArea, + SquareMeters( + inputModel.getType.getRotorArea.to(SQUARE_METRE).getValue.doubleValue + ), WecCharacteristic(inputModel.getType.getCpCharacteristic) ) diff --git a/src/main/scala/edu/ie3/simona/model/system/Characteristic.scala b/src/main/scala/edu/ie3/simona/model/system/Characteristic.scala index dabb72a172..2be558d535 100644 --- a/src/main/scala/edu/ie3/simona/model/system/Characteristic.scala +++ b/src/main/scala/edu/ie3/simona/model/system/Characteristic.scala @@ -74,8 +74,8 @@ trait Characteristic[A <: Quantity[A], O <: Quantity[O]] { object Characteristic { final case class XYPair[A <: Quantity[A], O <: Quantity[O]]( - x: A, - y: O + x: A, + y: O ) extends Ordered[XYPair[A, O]] { /** The pairs are ordered by their x value first. If two pairs have the same diff --git a/src/test/groovy/edu/ie3/simona/model/participant/WecModelTest.groovy b/src/test/groovy/edu/ie3/simona/model/participant/WecModelTest.groovy index 3112a9d9ac..71d8dae6b5 100644 --- a/src/test/groovy/edu/ie3/simona/model/participant/WecModelTest.groovy +++ b/src/test/groovy/edu/ie3/simona/model/participant/WecModelTest.groovy @@ -15,18 +15,19 @@ import edu.ie3.datamodel.models.input.system.characteristic.WecCharacteristicInp 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.quantities.EmptyQuantity +import edu.ie3.util.scala.quantities.Sq +import scala.Option import spock.lang.Shared import spock.lang.Specification import spock.lang.Unroll -import squants.energy.Watts$ - +import squants.Each$ +import squants.motion.MetersPerSecond$ +import squants.motion.Pascals$ +import squants.thermal.Celsius$ -import edu.ie3.util.scala.quantities.Sq import static edu.ie3.datamodel.models.StandardUnits.* import static edu.ie3.simona.model.participant.WecModel.WecRelevantData import static edu.ie3.util.quantities.PowerSystemUnits.* -import static edu.ie3.util.quantities.QuantityUtil.equals import static tech.units.indriya.quantity.Quantities.getQuantity class WecModelTest extends Specification { @@ -98,9 +99,9 @@ class WecModelTest extends Specification { wecModel.uuid() == inputModel.getUuid() wecModel.id() == inputModel.getId() wecModel.scalingFactor() == 1 - wecModel.sRated() == inputModel.getType().getsRated() + wecModel.sRated() == Kilowatts(inputModel.getType.getsRated.to(KILOWATT).getValue.doubleValue) wecModel.cosPhiRated() == inputModel.getType().getCosPhiRated() - wecModel.rotorArea() == inputModel.getType().getRotorArea() + wecModel.rotorArea() == SquareMeters(inputModel.getType.getRotorArea.to(SQUARE_METRE).getValue.doubleValue) wecModel.betzCurve() == new WecModel.WecCharacteristic$().apply(inputModel.getType().getCpCharacteristic()) } @@ -108,8 +109,10 @@ class WecModelTest extends Specification { def "Check active power output depending on velocity #velocity m/s"() { given: def wecModel = buildWecModel() - def wecData = new WecRelevantData(getQuantity(velocity, METRE_PER_SECOND), - getQuantity(20, CELSIUS), getQuantity(101325, PASCAL)) + def wecData = new WecRelevantData( + Sq.create(velocity, MetersPerSecond$.MODULE$), + Sq.create(20, Celsius$.MODULE$), + Sq.create(101325, Pascals$.MODULE$)) when: def result = wecModel.calculateActivePower(wecData) @@ -157,11 +160,11 @@ class WecModelTest extends Specification { def "Check determineBetzCoefficient method with wind velocity #velocity m/s:"() { given: def wecModel = buildWecModel() - def windVel = getQuantity(velocity, WIND_VELOCITY) + def windVel = Sq.create(velocity, MetersPerSecond$.MODULE$) when: def betzFactor = wecModel.determineBetzCoefficient(windVel) - def expected = getQuantity(betzResult, PU) + def expected = Sq.create(betzResult, Each$.MODULE$) then: betzFactor == expected @@ -180,18 +183,17 @@ class WecModelTest extends Specification { def "Check calculateAirDensity method with temperature #temperature degrees Celsius and air pressure #pressure Pascal:"() { given: def wecModel = buildWecModel() - def temperatureV = getQuantity(temperature, CELSIUS) - def pressureV = EmptyQuantity.of(PASCAL) + def temperatureV = Sq.create(temperature, Celsius$.MODULE$) + def pressureV = Option.empty() when: if (pressure > 0) { - pressureV = getQuantity(pressure, PASCAL) + pressureV = Some(Sq.create(pressure, Pascals$.MODULE$)) } - def airDensity = wecModel.calculateAirDensity(temperatureV, pressureV).to(KILOGRAM_PER_CUBIC_METRE) - def expected = getQuantity(densityResult, KILOGRAM_PER_CUBIC_METRE) + def airDensity = wecModel.calculateAirDensity(temperatureV, pressureV).toKilogramsPerCubicMeter() then: - equals(airDensity, expected, TOLERANCE) + Math.abs(airDensity - densityResult) < TOLERANCE where: temperature | pressure || densityResult diff --git a/src/test/scala/edu/ie3/simona/test/common/model/participant/CharacteristicTestData.scala b/src/test/scala/edu/ie3/simona/test/common/model/participant/CharacteristicTestData.scala index 39b11b0a66..be3c09de15 100644 --- a/src/test/scala/edu/ie3/simona/test/common/model/participant/CharacteristicTestData.scala +++ b/src/test/scala/edu/ie3/simona/test/common/model/participant/CharacteristicTestData.scala @@ -13,11 +13,16 @@ import squants.{Dimensionless, Each} import scala.collection.SortedSet trait CharacteristicTestData { - protected val xy1: XYPair[Dimensionless, Dimensionless] = XYPair(Each(1), Each(2)) - protected val xy2: XYPair[Dimensionless, Dimensionless] = XYPair(Each(2), Each(4)) - protected val xy3: XYPair[Dimensionless, Dimensionless] = XYPair(Each(3), Each(8)) - protected val xy4: XYPair[Dimensionless, Dimensionless] = XYPair(Each(4), Each(16)) - protected val xy5: XYPair[Dimensionless, Dimensionless] = XYPair(Each(5), Each(32)) + protected val xy1: XYPair[Dimensionless, Dimensionless] = + XYPair(Each(1), Each(2)) + protected val xy2: XYPair[Dimensionless, Dimensionless] = + XYPair(Each(2), Each(4)) + protected val xy3: XYPair[Dimensionless, Dimensionless] = + XYPair(Each(3), Each(8)) + protected val xy4: XYPair[Dimensionless, Dimensionless] = + XYPair(Each(4), Each(16)) + protected val xy5: XYPair[Dimensionless, Dimensionless] = + XYPair(Each(5), Each(32)) object TestCharacteristic extends Characteristic[Dimensionless, Dimensionless] { From b17a5170cdec6b707964430259b6cf6564c8a549 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Tue, 13 Jun 2023 16:36:59 +0200 Subject: [PATCH 2/2] Fixing air density calculation in WecModel --- .../edu/ie3/simona/model/participant/WecModel.scala | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) 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 bf352c3b61..a6f3802747 100644 --- a/src/main/scala/edu/ie3/simona/model/participant/WecModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant/WecModel.scala @@ -72,9 +72,12 @@ final case class WecModel( cosPhiRated ) { - /** Universal gas constant + /** Universal gas constant, actually in J/(K * mol) */ private val R = JoulesPerKelvin(8.31446261815324d) + + /** Molar mass of air, actually in kg/mol + */ private val AIR_MOLAR_MASS = Kilograms(0.0289647d) /** Calculate the active power output of the [[WecModel]]. First determine the @@ -176,7 +179,11 @@ final case class WecModel( case None => KilogramsPerCubicMeter(1.2041d) case Some(pressure) => - AIR_MOLAR_MASS * pressure.toPascals / (R * temperature.toKelvinScale) + // kg * mol^-1 * J * m^-3 * J^-1 * K * mol * K^-1 + // = kg * m^-3 + KilogramsPerCubicMeter( + AIR_MOLAR_MASS.toKilograms * pressure.toPascals / (R.toJoulesPerKelvin * temperature.toKelvinScale) + ) } } }