diff --git a/CHANGELOG.md b/CHANGELOG.md index fc1a1c810d..48a0e0933c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -140,6 +140,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Replaced Java Durations with Scala Durations [#1068](https://github.com/ie3-institute/simona/issues/1068) - Typo and format of `ThermalGrid` and `ThermalHouse` ScalaDocs [#1196](https://github.com/ie3-institute/simona/issues/1196) - Refactor `EmRuntimeConfig` [#1181](https://github.com/ie3-institute/simona/issues/1181) +- Based `PvModel` calculations on irradiance (power per area) instead of irradiation (energy per area) [#1212](https://github.com/ie3-institute/simona/issues/1212) ### Fixed - Fix rendering of references in documentation [#505](https://github.com/ie3-institute/simona/issues/505) diff --git a/docs/readthedocs/models/pv_model.md b/docs/readthedocs/models/pv_model.md index da64e2067a..9a9e3c5802 100644 --- a/docs/readthedocs/models/pv_model.md +++ b/docs/readthedocs/models/pv_model.md @@ -4,7 +4,8 @@ This page documents the functionality of the PV model available in SIMONA. -The initial parts of the model are presented in the paper [Agent based approach to model photovoltaic feed-in in distribution network planning](https://ieeexplore.ieee.org/abstract/document/7038345). Since then several adaptions has been made that are documented as follows. +The initial parts of the model are presented in the paper _Agent based approach to model photovoltaic feed-in in distribution network planning_ by {cite:cts}`Seack.2014`. +Since then several adaptions has been made that are documented as follows. The PV Model is part of the SIMONA Simulation framework and represented by an agent. @@ -20,21 +21,29 @@ Please refer to {doc}`PowerSystemDataModel - PV Model - tick - dataTick - case _ => - /* At the first tick, we are not able to determine the tick interval from last tick - * (since there is none). Then we use a fallback pv stem distance. */ - FALLBACK_WEATHER_STEM_DISTANCE - } - // take the last weather data, not necessarily the one for the current tick: // we might receive flex control messages for irregular ticks val (_, secondaryData) = baseStateData.receivedSecondaryDataStore @@ -240,7 +228,6 @@ protected trait PvAgentFundamentals PvRelevantData( dateTime, - tickInterval, weatherData.diffIrr, weatherData.dirIrr, ) diff --git a/src/main/scala/edu/ie3/simona/model/participant/PvModel.scala b/src/main/scala/edu/ie3/simona/model/participant/PvModel.scala index 98f2162ed2..d399892cfa 100644 --- a/src/main/scala/edu/ie3/simona/model/participant/PvModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant/PvModel.scala @@ -51,45 +51,35 @@ final case class PvModel private ( with ApparentPowerParticipant[PvRelevantData, ConstantState.type] { /** Override sMax as the power output of a pv unit could become easily up to - * 10% higher than the sRated value found in the technical sheets + * 10% higher than the sRated value found in the technical sheets. */ override val sMax: ApparentPower = sRated * 1.1 - /** Permissible maximum active power feed in (therefore negative) */ + /** Permissible maximum active power feed in (therefore negative). */ protected val pMax: Power = sMax.toActivePower(cosPhiRated) * -1d - /** Reference yield at standard testing conditions (STC) */ + /** Reference yield at standard testing conditions (STC). */ private val yieldSTC = WattsPerSquareMeter(1000d) private val activationThreshold = sRated.toActivePower(cosPhiRated) * 0.001 * -1d - /** Calculate the active power behaviour of the model + /** Calculate the active power behaviour of the model. * * @param data - * Further needed, secondary data + * Further needed, secondary data. * @return - * Active power + * THe Active power. */ override protected def calculateActivePower( modelState: ConstantState.type, data: PvRelevantData, ): Power = { - // === Weather Base Data === // - /* The pv model calculates the power in-feed based on the solar irradiance that is received over a specific - * time frame (which actually is the solar irradiation). Hence, a multiplication with the time frame within - * this irradiance is received is required. */ - val duration: Time = Seconds(data.weatherDataFrameLength) - - // eBeamH and eDifH needs to be extract to their double values in some places - // hence a conversion to watt-hour per square meter is required, to avoid - // invalid double value extraction! - val eBeamH = - data.dirIrradiance * duration - val eDifH = - data.diffIrradiance * duration - - // === Beam Radiation Parameters === // + // Irradiance on a horizontal surface + val gBeamH = data.dirIrradiance + val gDifH = data.diffIrradiance + + // === Beam irradiance parameters === // val angleJ = calcAngleJ(data.dateTime) val delta = calcSunDeclinationDelta(angleJ) @@ -104,9 +94,9 @@ final case class PvModel private ( val omegas = calculateBeamOmegas(thetaG, omega, omegaSS, omegaSR) - // === Beam Radiation ===// - val eBeamS = calcBeamRadiationOnSlopedSurface( - eBeamH, + // === Beam irradiance ===// + val gBeamS = calcBeamIrradianceOnSlopedSurface( + gBeamH, omegas, delta, lat, @@ -114,44 +104,43 @@ final case class PvModel private ( alphaE, ) - // === Diffuse Radiation Parameters ===// + // === Diffuse irradiance parameters ===// val thetaZ = calcZenithAngleThetaZ(alphaS) val airMass = calcAirMass(thetaZ) - val extraterrestrialRadiationI0 = calcExtraterrestrialRadiationI0(angleJ) + val g0 = calcExtraterrestrialRadianceG0(angleJ) - // === Diffuse Radiation ===// - val eDifS = calcDiffuseRadiationOnSlopedSurfacePerez( - eDifH, - eBeamH, + // === Diffuse irradiance ===// + val gDifS = calcDiffuseIrradianceOnSlopedSurfacePerez( + gDifH, + gBeamH, airMass, - extraterrestrialRadiationI0, + g0, thetaZ, thetaG, gammaE, ) - // === Reflected Radiation ===// - val eRefS = - calcReflectedRadiationOnSlopedSurface(eBeamH, eDifH, gammaE, albedo) + // === Reflected irradiance ===// + val gRefS = + calcReflectedIrradianceOnSlopedSurface(gBeamH, gDifH, gammaE, albedo) - // === Total Radiation ===// - val eTotal = eDifS + eBeamS + eRefS + // === Total irradiance ===// + val gTotal = gDifS + gBeamS + gRefS - val irraditionSTC = yieldSTC * duration calcOutput( - eTotal, + gTotal, data.dateTime, - irraditionSTC, + yieldSTC, ) } /** Calculates the position of the earth in relation to the sun (day angle) - * for the provided time + * for the provided time. * * @param time - * the time + * The time. * @return - * day angle J + * Day angle J. */ def calcAngleJ(time: ZonedDateTime): Angle = { val day = time.getDayOfYear // day of the year @@ -165,9 +154,9 @@ final case class PvModel private ( * of the position of the sun". Appl. Opt. 1971, 10, 2569–2571 * * @param angleJ - * day angle J + * The day angle J. * @return - * declination angle + * The declination angle. */ def calcSunDeclinationDelta( angleJ: Angle @@ -189,13 +178,13 @@ final case class PvModel private ( * on its axis at 15◦ per hour; morning negative, afternoon positive. * * @param time - * the requested time (which is transformed to solar time) + * The requested time (which is transformed to solar time). * @param angleJ - * day angle J + * The day angle J. * @param longitude - * longitude of the position + * The longitude of the position. * @return - * hour angle omega + * The hour angle omega. */ def calcHourAngleOmega( time: ZonedDateTime, @@ -221,11 +210,11 @@ final case class PvModel private ( * omegaSS. * * @param latitude - * latitude of the position + * The latitude of the position. * @param delta - * sun declination angle + * The sun declination angle. * @return - * sunset angle omegaSS + * The sunset angle omegaSS. */ def calcSunsetAngleOmegaSS( latitude: Angle, @@ -247,13 +236,13 @@ final case class PvModel private ( * the zenith angle. * * @param omega - * hour angle + * The hour angle. * @param delta - * sun declination angle + * The sun declination angle. * @param latitude - * latitude of the position + * The latitude of the position. * @return - * solar altitude angle alphaS + * The solar altitude angle alphaS. */ def calcSolarAltitudeAngleAlphaS( omega: Angle, @@ -277,7 +266,7 @@ final case class PvModel private ( /** Calculates the zenith angle thetaG which represents the angle between the * vertical and the line to the sun, that is, the angle of incidence of beam - * radiation on a horizontal surface. + * irradiance on a horizontal surface. * * @param alphaS * sun altitude angle @@ -294,13 +283,13 @@ final case class PvModel private ( } /** Calculates the ratio of the mass of atmosphere through which beam - * radiation passes to the mass it would pass through if the sun were at the + * irradiance passes to the mass it would pass through if the sun were at the * zenith (i.e., directly overhead). * * @param thetaZ - * zenith angle + * The zenith angle. * @return - * air mass + * The air mass. */ def calcAirMass(thetaZ: Angle): Double = { val thetaZInRad = thetaZ.toRadians @@ -317,17 +306,17 @@ final case class PvModel private ( ) - airMassRatio * cos(thetaZInRad) } - /** Calculates the extraterrestrial radiation, that is, the radiation that + /** Calculates the extraterrestrial irradiance, that is, the irradiance that * would be received in the absence of the atmosphere. * * @param angleJ - * day angle J + * The day angle J. * @return - * extraterrestrial radiation I0 + * The extraterrestrial irradiance G0. */ - def calcExtraterrestrialRadiationI0( + def calcExtraterrestrialRadianceG0( angleJ: Angle - ): Irradiation = { + ): Irradiance = { val jInRad = angleJ.toRadians // eccentricity correction factor @@ -338,27 +327,27 @@ final case class PvModel private ( 0.000077 * sin(2d * jInRad) // solar constant in W/m2 - val Gsc = WattHoursPerSquareMeter(1367) // solar constant - Gsc * e0 + val gSc = WattsPerSquareMeter(1367) // solar constant + gSc * e0 } - /** Calculates the angle of incidence thetaG of beam radiation on a surface + /** Calculates the angle of incidence thetaG of beam irradiance on a surface. * * @param delta - * sun declination angle + * The sun declination angle. * @param latitude - * latitude of the position + * The latitude of the position. * @param gammaE - * slope angle (the angle between the plane of the surface in question and - * the horizontal) + * The slope angle (the angle between the plane of the surface in question + * and the horizontal). * @param alphaE - * surface azimuth angle (the deviation of the projection on a horizontal - * plane of the normal to the surface from the local meridian, with zero - * due south, east negative, and west positive) + * The surface azimuth angle (the deviation of the projection on a + * horizontal plane of the normal to the surface from the local meridian, + * with zero due south, east negative, and west positive). * @param omega - * hour angle + * The hour angle. * @return - * angle of incidence thetaG + * The angle of incidence thetaG. */ def calcAngleOfIncidenceThetaG( delta: Angle, @@ -386,19 +375,19 @@ final case class PvModel private ( } /** Calculates omega1 and omega2, which are parameters for - * calcBeamRadiationOnSlopedSurface + * calcBeamIrradianceOnSlopedSurface * * @param thetaG - * angle of incidence + * The angle of incidence. * @param omega - * hour angle + * The hour angle. * @param omegaSS - * sunset angle + * The sunset angle. * @param omegaSR - * sunrise angle + * The sunrise angle. * @return - * omega1 and omega encapsulated in an Option, if applicable. None - * otherwise + * The omega1 and omega encapsulated in an Option, if applicable. None + * otherwise. */ def calculateBeamOmegas( thetaG: Angle, @@ -417,7 +406,7 @@ final case class PvModel private ( val omega2InRad = omega1InRad + omegaOneHour // requested hour plus 1 hour // (thetaG < 90°): sun is visible - // (thetaG > 90°), otherwise: sun is behind the surface -> no direct radiation + // (thetaG > 90°), otherwise: sun is behind the surface -> no direct irradiance if ( thetaGInRad < toRadians(90) // omega1 and omega2: sun has risen and has not set yet @@ -441,34 +430,34 @@ final case class PvModel private ( None } - /** Calculates the beam radiation on a sloped surface + /** Calculates the beam irradiance on a sloped surface. * - * @param eBeamH - * beam radiation on a horizontal surface + * @param gBeamH + * The beam irradiance on a horizontal surface. * @param omegas - * omega1 and omega2 + * Omega1 and omega2. * @param delta - * sun declination angle + * The sun declination angle. * @param latitude - * latitude of the position + * The latitude of the position. * @param gammaE - * slope angle (the angle between the plane of the surface in question and - * the horizontal) + * The slope angle (the angle between the plane of the surface in question + * and the horizontal). * @param alphaE - * surface azimuth angle (the deviation of the projection on a horizontal - * plane of the normal to the surface from the local meridian, with zero - * due south, east negative, and west positive) + * The surface azimuth angle (the deviation of the projection on a + * horizontal plane of the normal to the surface from the local meridian, + * with zero due south, east negative, and west positive). * @return - * the beam radiation on the sloped surface + * The beam irradiance on the sloped surface. */ - def calcBeamRadiationOnSlopedSurface( - eBeamH: Irradiation, + def calcBeamIrradianceOnSlopedSurface( + gBeamH: Irradiance, omegas: Option[(Angle, Angle)], delta: Angle, latitude: Angle, gammaE: Angle, alphaE: Angle, - ): Irradiation = { + ): Irradiance = { omegas match { case Some((omega1, omega2)) => @@ -500,60 +489,60 @@ final case class PvModel private ( // in rare cases (close to sunrise) r can become negative (although very small) val r = max(a / b, 0d) - eBeamH * r - case None => WattHoursPerSquareMeter(0d) + gBeamH * r + case None => WattsPerSquareMeter(0d) } } - /** Calculates the diffuse radiation on a sloped surface based on the model of - * Perez et al. + /** Calculates the diffuse irradiance on a sloped surface based on the model + * of Perez et al. * *

Formula taken from Perez, R., P. Ineichen, R. Seals, J. Michalsky, and * R. Stewart, "Modeling Daylight Availability and Irradiance Components from * Direct and Global Irradiance". Solar Energy, 44, 271 (1990). * - * @param eDifH - * diffuse radiation on a horizontal surface - * @param eBeamH - * beam radiation on a horizontal surface + * @param gDifH + * The diffuse irradiance on a horizontal surface. + * @param gBeamH + * The beam irradiance on a horizontal surface. * @param airMass - * the air mass - * @param extraterrestrialRadiationI0 - * extraterrestrial radiation + * The air mass. + * @param extraterrestrialRadianceG0 + * The extraterrestrial irradiance. * @param thetaZ - * zenith angle + * The zenith angle. * @param thetaG - * angle of incidence + * The angle of incidence. * @param gammaE - * slope angle (the angle between the plane of the surface in question and - * the horizontal) + * The slope angle (the angle between the plane of the surface in question + * and the horizontal). * @return - * the diffuse radiation on the sloped surface + * The diffuse irradiance on the sloped surface. */ - def calcDiffuseRadiationOnSlopedSurfacePerez( - eDifH: Irradiation, - eBeamH: Irradiation, + def calcDiffuseIrradianceOnSlopedSurfacePerez( + gDifH: Irradiance, + gBeamH: Irradiance, airMass: Double, - extraterrestrialRadiationI0: Irradiation, + extraterrestrialRadianceG0: Irradiance, thetaZ: Angle, thetaG: Angle, gammaE: Angle, - ): Irradiation = { + ): Irradiance = { val thetaZInRad = thetaZ.toRadians val thetaGInRad = thetaG.toRadians val gammaEInRad = gammaE.toRadians // == brightness index beta ==// - val delta = eDifH * airMass / extraterrestrialRadiationI0 + val delta = gDifH * airMass / extraterrestrialRadianceG0 // == cloud index epsilon ==// - val x = if (eDifH.value.doubleValue > 0) { - // if we have diffuse radiation on horizontal surface we have to consider + val x = if (gDifH.value.doubleValue > 0) { + // if we have diffuse irradiance on horizontal surface we have to consider // the clearness parameter epsilon, which then gives us an epsilon bin x - // Beam radiation is required on a plane normal to the beam direction (normal incidence), + // Beam irradiance is required on a plane normal to the beam direction (normal incidence), // thus dividing by cos theta_z - var epsilon = ((eDifH + eBeamH / cos(thetaZInRad)) / eDifH + + var epsilon = ((gDifH + gBeamH / cos(thetaZInRad)) / gDifH + (5.535d * 1.0e-6) * pow( thetaZ.toDegrees, 3, @@ -615,36 +604,36 @@ final case class PvModel private ( val aPerez = max(0, cos(thetaGInRad)) val bPerez = max(cos(1.4835298641951802), cos(thetaZInRad)) - // finally calculate the diffuse radiation on an inclined surface - eDifH * ( + // finally calculate the diffuse irradiance on an inclined surface + gDifH * ( ((1 + cos(gammaEInRad)) / 2) * (1 - f1) + (f1 * (aPerez / bPerez)) + (f2 * sin(gammaEInRad)) ) } - /** Calculates the reflected radiation on a sloped surface + /** Calculates the reflected irradiance on a sloped surface. * - * @param eBeamH - * beam radiation on a horizontal surface - * @param eDifH - * diffuse radiation on a horizontal surface + * @param gBeamH + * The beam irradiance on a horizontal surface. + * @param gDifH + * The diffuse irradiance on a horizontal surface. * @param gammaE - * slope angle (the angle between the plane of the surface in question and - * the horizontal) + * The slope angle (the angle between the plane of the surface in question + * and the horizontal). * @param albedo - * albedo / "composite" ground reflection + * The albedo / "composite" ground reflection. * @return - * the reflected radiation on the sloped surface eRefS + * The reflected irradiance on the sloped surface eRefS. */ - def calcReflectedRadiationOnSlopedSurface( - eBeamH: Irradiation, - eDifH: Irradiation, + def calcReflectedIrradianceOnSlopedSurface( + gBeamH: Irradiance, + gDifH: Irradiance, gammaE: Angle, albedo: Double, - ): Irradiation = { + ): Irradiance = { val gammaEInRad = gammaE.toRadians - (eBeamH + eDifH) * (albedo * 0.5 * (1 - cos(gammaEInRad))) + (gBeamH + gDifH) * albedo * 0.5 * (1 - cos(gammaEInRad)) } private def generatorCorrectionFactor( @@ -681,9 +670,9 @@ final case class PvModel private ( } private def calcOutput( - eTotalInWhPerSM: Irradiation, + gTotal: Irradiance, time: ZonedDateTime, - irradiationSTC: Irradiation, + irradianceSTC: Irradiance, ): Power = { val genCorr = generatorCorrectionFactor(time, gammaE) val tempCorr = temperatureCorrectionFactor(time) @@ -691,11 +680,11 @@ final case class PvModel private ( * area. The yield also takes care of generator and temperature correction factors as well as the converter's * efficiency */ val actYield = - eTotalInWhPerSM * moduleSurface.toSquareMeters * etaConv.toEach * (genCorr * tempCorr) + gTotal * moduleSurface.toSquareMeters * etaConv.toEach * (genCorr * tempCorr) /* Calculate the foreseen active power output without boundary condition adaptions */ val proposal = - sRated.toActivePower(cosPhiRated) * -1 * (actYield / irradiationSTC) + sRated.toActivePower(cosPhiRated) * -1 * (actYield / irradianceSTC) /* Do sanity check, if the proposed feed in is above the estimated maximum to be apparent active power of the plant */ if (proposal < pMax) @@ -731,21 +720,17 @@ final case class PvModel private ( object PvModel { - /** Class that holds all relevant data for a pv model calculation + /** Class that holds all relevant data for a pv model calculation. * * @param dateTime - * date and time of the ending of time frame to calculate - * @param weatherDataFrameLength - * the duration in ticks (= seconds) the provided irradiance is received by - * the pv panel + * Date and time of the ending of time frame to calculate. * @param diffIrradiance - * diffuse solar irradiance + * The diffuse solar irradiance on a horizontal surface. * @param dirIrradiance - * direct solar irradiance + * The direct solar irradiance on a horizontal surface. */ final case class PvRelevantData( dateTime: ZonedDateTime, - weatherDataFrameLength: Long, diffIrradiance: Irradiance, dirIrradiance: Irradiance, ) extends CalcRelevantData diff --git a/src/main/scala/edu/ie3/simona/service/weather/WeatherService.scala b/src/main/scala/edu/ie3/simona/service/weather/WeatherService.scala index d1838ab56d..f323238f4a 100644 --- a/src/main/scala/edu/ie3/simona/service/weather/WeatherService.scala +++ b/src/main/scala/edu/ie3/simona/service/weather/WeatherService.scala @@ -90,7 +90,6 @@ object WeatherService { sourceDefinition: SimonaConfig.Simona.Input.Weather.Datasource ) extends InitializeServiceStateData - val FALLBACK_WEATHER_STEM_DISTANCE = 3600L } /** Weather Service is responsible to register other actors that require weather diff --git a/src/test/scala/edu/ie3/simona/agent/em/EmAgentIT.scala b/src/test/scala/edu/ie3/simona/agent/em/EmAgentIT.scala index 6ace95c335..dfdad66a6f 100644 --- a/src/test/scala/edu/ie3/simona/agent/em/EmAgentIT.scala +++ b/src/test/scala/edu/ie3/simona/agent/em/EmAgentIT.scala @@ -288,7 +288,7 @@ class EmAgentIT /* TICK 7200 LOAD: 0.269 kW (unchanged) - PV: -3.791 kW + PV: -3.715 kW STORAGE: SOC 63.3 % -> charge with 3.522 kW -> remaining 0 kW @@ -300,8 +300,8 @@ class EmAgentIT 7200, weatherService.ref.toClassic, WeatherData( - WattsPerSquareMeter(50d), - WattsPerSquareMeter(150d), + WattsPerSquareMeter(45d), + WattsPerSquareMeter(140d), Celsius(0d), MetersPerSecond(0d), ), @@ -318,24 +318,24 @@ class EmAgentIT emResult.getQ should equalWithTolerance(0.0000882855367.asMegaVar) } - scheduler.expectMessage(Completion(emAgentActivation, Some(13115))) + scheduler.expectMessage(Completion(emAgentActivation, Some(13247))) - /* TICK 13115 + /* TICK 13247 LOAD: 0.269 kW (unchanged) - PV: -3.791 kW (unchanged) + PV: -3.715 kW (unchanged) STORAGE: SOC 100 % -> charge with 0 kW - -> remaining -3.522 kW + -> remaining -3.447 kW */ - emAgentActivation ! Activation(13115) + emAgentActivation ! Activation(13247) resultListener.expectMessageType[ParticipantResultEvent] match { case ParticipantResultEvent(emResult: EmResult) => emResult.getInputModel shouldBe emInput.getUuid - emResult.getTime shouldBe 13115L.toDateTime + emResult.getTime shouldBe 13247.toDateTime emResult.getP should equalWithTolerance( - -0.0035233186089842434.asMegaWatt + -0.0034468567291.asMegaWatt ) emResult.getQ should equalWithTolerance(0.0000882855367.asMegaVar) } @@ -344,9 +344,9 @@ class EmAgentIT /* TICK 14400 LOAD: 0.269 kW (unchanged) - PV: -0.069 kW + PV: -0.07 kW STORAGE: SOC 100 % - -> discharge with 0.2 kW + -> discharge with 0.199 kW -> remaining 0.0 kW */ @@ -580,10 +580,10 @@ class EmAgentIT /* TICK 7200 LOAD: 0.269 kW (unchanged) - PV: -3.791 kW + PV: -3.715 kW Heat pump: running (turned on from last request), can also be turned off -> set point ~3.5 kW (bigger than 50 % rated apparent power): stays turned on with unchanged state - -> remaining 1.327 kW + -> remaining 1.403 kW */ emAgentActivation ! Activation(7200) @@ -593,8 +593,8 @@ class EmAgentIT 7200, weatherService.ref.toClassic, WeatherData( - WattsPerSquareMeter(50d), - WattsPerSquareMeter(150d), + WattsPerSquareMeter(45d), + WattsPerSquareMeter(140d), Celsius(0d), MetersPerSecond(0d), ), @@ -607,10 +607,10 @@ class EmAgentIT emResult.getInputModel shouldBe emInput.getUuid emResult.getTime shouldBe 7200.toDateTime emResult.getP should equalWithTolerance( - 0.0013266813910157566.asMegaWatt + 0.0014031432709.asMegaWatt ) emResult.getQ should equalWithTolerance( - 0.0010731200407782782.asMegaVar + 0.0010731200408.asMegaVar ) } @@ -644,12 +644,12 @@ class EmAgentIT resultListener.expectMessageType[ParticipantResultEvent] match { case ParticipantResultEvent(emResult: EmResult) => emResult.getInputModel shouldBe emInput.getUuid - emResult.getTime shouldBe 14400L.toDateTime + emResult.getTime shouldBe 14400.toDateTime emResult.getP should equalWithTolerance( - 0.00019892577822992104.asMegaWatt + 0.0001988993578.asMegaWatt ) emResult.getQ should equalWithTolerance( - 0.0000882855367033582.asMegaVar + 0.0000882855367.asMegaVar ) } @@ -657,10 +657,10 @@ class EmAgentIT /* TICK 21600 LOAD: 0.269 kW (unchanged) - PV: -0.023 kW + PV: -0.024 kW Heat pump: Is not running, can run or stay off -> flex signal is 0 MW: Heat pump is turned off - -> remaining 0.245 kW + -> remaining 0.244 kW */ emAgentActivation ! Activation(21600) @@ -685,10 +685,10 @@ class EmAgentIT emResult.getInputModel shouldBe emInput.getUuid emResult.getTime shouldBe 21600.toDateTime emResult.getP should equalWithTolerance( - 0.0002450436827011999.asMegaWatt + 0.0002442471208.asMegaWatt ) emResult.getQ should equalWithTolerance( - 0.0000882855367033582.asMegaVar + 0.0000882855367.asMegaVar ) } diff --git a/src/test/scala/edu/ie3/simona/model/participant/PvModelITSpec.scala b/src/test/scala/edu/ie3/simona/model/participant/PvModelITSpec.scala index 170a040aab..632a3a2423 100644 --- a/src/test/scala/edu/ie3/simona/model/participant/PvModelITSpec.scala +++ b/src/test/scala/edu/ie3/simona/model/participant/PvModelITSpec.scala @@ -38,7 +38,6 @@ class PvModelITSpec extends Matchers with UnitSpec with PvModelITHelper { val weather = modelToWeatherMap(modelId) val neededData = PvModel.PvRelevantData( dateTime, - 3600L, weather.diffIrr, weather.dirIrr, ) diff --git a/src/test/scala/edu/ie3/simona/model/participant/PvModelSpec.scala b/src/test/scala/edu/ie3/simona/model/participant/PvModelSpec.scala index c7aef19b1c..0e7fc3b93d 100644 --- a/src/test/scala/edu/ie3/simona/model/participant/PvModelSpec.scala +++ b/src/test/scala/edu/ie3/simona/model/participant/PvModelSpec.scala @@ -17,7 +17,7 @@ import edu.ie3.util.scala.quantities._ import org.locationtech.jts.geom.{Coordinate, GeometryFactory, Point} import org.scalatest.GivenWhenThen import squants.Each -import squants.energy.Kilowatts +import squants.energy.{Kilowatts, Megajoules} import squants.space.{Angle, Degrees, Radians} import tech.units.indriya.quantity.Quantities.getQuantity import tech.units.indriya.unit.Units._ @@ -85,11 +85,10 @@ class PvModelSpec extends UnitSpec with GivenWhenThen with DefaultTestData { ) private implicit val angleTolerance: Angle = Radians(1e-10) - private implicit val irradiationTolerance: Irradiation = - WattHoursPerSquareMeter(1e-10) - private implicit val apparentPowerTolerance: ApparentPower = Kilovoltamperes( - 1e-10 - ) + private implicit val irradianceTolerance: Irradiance = + WattsPerSquareMeter(1e-10) + private implicit val apparentPowerTolerance: ApparentPower = + Kilovoltamperes(1e-10) private implicit val reactivePowerTolerance: ReactivePower = Megavars(1e-10) "A PV Model" should { @@ -508,20 +507,20 @@ class PvModelSpec extends UnitSpec with GivenWhenThen with DefaultTestData { } } - "calculate the extraterrestrial radiation I0 correctly" in { + "calculate the extraterrestrial radiance g0 correctly" in { val testCases = Table( - ("j", "I0Sol"), + ("j", "g0Sol"), (0d, 1414.91335d), // Jan 1st (2.943629280897834d, 1322.494291080537598d), // Jun 21st (4.52733626243351d, 1355.944773587800003d), // Sep 21st ) - forAll(testCases) { (j, I0Sol) => - When("the extraterrestrial radiation is calculated") - val I0Calc = pvModel.calcExtraterrestrialRadiationI0(Radians(j)) + forAll(testCases) { (j, g0Sol) => + When("the extraterrestrial radiance is calculated") + val g0Calc = pvModel.calcExtraterrestrialRadianceG0(Radians(j)) Then("result should match the test data") - I0Calc should approximate(WattHoursPerSquareMeter(I0Sol)) + g0Calc should approximate(WattsPerSquareMeter(g0Sol)) } } @@ -612,7 +611,7 @@ class PvModelSpec extends UnitSpec with GivenWhenThen with DefaultTestData { ) - /** Calculate the angle of incidence of beam radiation on a surface + /** Calculate the angle of incidence of beam irradiance on a surface * located at a latitude at a certain hour angle (solar time) on a given * declination (date) if the surface is tilted by a certain slope from * the horizontal and pointed to a certain panel azimuth west of south. @@ -687,7 +686,7 @@ class PvModelSpec extends UnitSpec with GivenWhenThen with DefaultTestData { } } - "calculate the estimate beam radiation eBeamS correctly" in { + "calculate the estimate beam irradiance gBeamS correctly" in { val testCases = Table( ( "latitudeDeg", @@ -696,10 +695,10 @@ class PvModelSpec extends UnitSpec with GivenWhenThen with DefaultTestData { "deltaDeg", "omegaDeg", "thetaGDeg", - "eBeamSSol", + "gBeamSSol", ), (40d, 0d, 0d, -11.6d, -37.5d, 37.0d, - 67.777778d), // flat surface => eBeamS = eBeamH + 67.777778d), // flat surface => gBeamS = gBeamH (40d, 60d, 0d, -11.6d, -37.5d, 37.0d, 112.84217113154841369d), // 2011-02-20T09:00:00 (40d, 60d, 0d, -11.6d, -78.0d, 75.0d, 210.97937494450755d), // sunrise @@ -710,7 +709,7 @@ class PvModelSpec extends UnitSpec with GivenWhenThen with DefaultTestData { (40d, 60d, -90.0d, -11.6d, 60.0d, 91.0d, 0d), // no direct beam ) - /** For a given hour angle, the estimate beam radiation on a sloped + /** For a given hour angle, the estimated beam irradiance on a sloped * surface is calculated for the next 60 minutes. Reference p.95 * https://www.sku.ac.ir/Datafiles/BookLibrary/45/John%20A.%20Duffie,%20William%20A.%20Beckman(auth.)-Solar%20Engineering%20of%20Thermal%20Processes,%20Fourth%20Edition%20(2013).pdf */ @@ -722,12 +721,12 @@ class PvModelSpec extends UnitSpec with GivenWhenThen with DefaultTestData { deltaDeg, omegaDeg, thetaGDeg, - eBeamSSol, + gBeamSSol, ) => Given("using the input data") - // Beam Radiation on a horizontal surface - val eBeamH = - 67.777778d // 1 MJ/m^2 = 277,778 Wh/m^2 -> 0.244 MJ/m^2 = 67.777778 Wh/m^2 + // Beam irradiance on a horizontal surface + // 1 MJ/m^2 = 277,778 Wh/m^2 -> 0.244 MJ/m^2 = 67.777778 Wh/m^2 + val gBeamH = 67.777778d val omegaSS = pvModel.calcSunsetAngleOmegaSS( Degrees(latitudeDeg), Degrees(deltaDeg), @@ -740,9 +739,9 @@ class PvModelSpec extends UnitSpec with GivenWhenThen with DefaultTestData { omegaSR, ) // omega1 and omega2 - When("the beam radiation is calculated") - val eBeamSCalc = pvModel.calcBeamRadiationOnSlopedSurface( - WattHoursPerSquareMeter(eBeamH), + When("the beam irradiance is calculated") + val gBeamSCalc = pvModel.calcBeamIrradianceOnSlopedSurface( + WattsPerSquareMeter(gBeamH), omegas, Degrees(deltaDeg), Degrees(latitudeDeg), @@ -751,82 +750,82 @@ class PvModelSpec extends UnitSpec with GivenWhenThen with DefaultTestData { ) Then("result should match the test data") - eBeamSCalc should approximate(WattHoursPerSquareMeter(eBeamSSol)) + gBeamSCalc should approximate(WattsPerSquareMeter(gBeamSSol)) } } - "calculate the estimated diffuse radiation eDifS correctly" in { - def megaJoule2WattHours(megajoule: Double): Double = { - megajoule / (3.6 / 1000) - } + "calculate the estimated diffuse irradiance gDifS correctly" in { val testCases = Table( - ("thetaGDeg", "thetaZDeg", "gammaEDeg", "airMass", "I0", "eDifSSol"), + ("thetaGDeg", "thetaZDeg", "gammaEDeg", "airMass", "g0", "gDifSSol"), // Reference Duffie 4th ed., p.95 // I_0 = 5.025 MJ * 277.778 Wh/MJ = 1395.83445 Wh - // eDifSSol is 0.79607 MJ (0.444 + 0.348 + 0.003) if one only calculates the relevant terms + // gDifSSol is 0.79607 MJ (0.444 + 0.348 + 0.003) if one only calculates the relevant terms // from I_T on p. 96, but Duffie uses fixed f values, so the inaccuracy is fine (approx. 4.5 Wh/m^2 or 0.016 MJ/m^2) + // g0 and gDifSol are energies (radiations) in Duffie, but we interpret them as powers (radiances). ( 37.0d, 62.2d, 60d, 2.144d, - megaJoule2WattHours(5.025), - megaJoule2WattHours(0.812140993078191252), + Megajoules(5.025).toWattHours, + Megajoules(0.812140993078191252).toWattHours, ), ) forAll(testCases) { - (thetaGDeg, thetaZDeg, gammaEDeg, airMass, I0, eDifSSol) => + (thetaGDeg, thetaZDeg, gammaEDeg, airMass, g0, gDifSSol) => // Reference Duffie 4th ed., p.95 + // gBeamH and gDifH are energies (radiations) in Duffie, but we interpret them as powers (radiances). Given("using the input data") - // Beam Radiation on horizontal surface - val eBeamH = - megaJoule2WattHours(0.244) // 0.244 MJ/m^2 = 67.777778 Wh/m^2 - // Diffuse Radiation on a horizontal surface - val eDifH = - megaJoule2WattHours(0.796) // 0.796 MJ/m^2 = 221.111111 Wh/m^2 - - When("the diffuse radiation is calculated") - val eDifSCalc = pvModel.calcDiffuseRadiationOnSlopedSurfacePerez( - WattHoursPerSquareMeter(eDifH), - WattHoursPerSquareMeter(eBeamH), + // Beam irradiance on horizontal surface given a radiation for one hour + // 0.244 MJ/m^2 = 67.777778 Wh/m^2 + val gBeamH = Megajoules(0.244).toWattHours + // Diffuse irradiance on a horizontal surface given a radiation for one hour + // 0.796 MJ/m^2 = 221.111111 Wh/m^2 + val gDifH = Megajoules(0.796).toWattHours + + When("the diffuse irradiance is calculated") + val gDifSCalc = pvModel.calcDiffuseIrradianceOnSlopedSurfacePerez( + WattsPerSquareMeter(gDifH), + WattsPerSquareMeter(gBeamH), airMass, - WattHoursPerSquareMeter(I0), + WattsPerSquareMeter(g0), Degrees(thetaZDeg), Degrees(thetaGDeg), Degrees(gammaEDeg), ) Then("result should match the test data") - eDifSCalc should approximate(WattHoursPerSquareMeter(eDifSSol)) + gDifSCalc should approximate(WattsPerSquareMeter(gDifSSol)) } } "calculate the ground reflection eRefS" in { val testCases = Table( - ("gammaEDeg", "albedo", "eRefSSol"), + ("gammaEDeg", "albedo", "gRefSSol"), (60d, 0.60d, 42.20833319999999155833336d), // '2011-02-20T09:00:00' ) - forAll(testCases) { (gammaEDeg, albedo, eRefSSol) => + forAll(testCases) { (gammaEDeg, albedo, gRefSSol) => Given("using the input data") - // Beam Radiation on horizontal surface - val eBeamH = - 67.777778d // 1 MJ/m^2 = 277,778 Wh/m^2 -> 0.244 MJ/m^2 = 67.777778 Wh/m^2 - // Diffuse Radiation on a horizontal surface - val eDifH = 213.61111d // 0.769 MJ/m^2 = 213,61111 Wh/m^2 + // Beam irradiance on horizontal surface given a radiation for one hour + // 1 MJ/m^2 = 277,778 Wh/m^2 -> 0.244 MJ/m^2 = 67.777778 Wh/m^2 + val gBeamH = 67.777778d + // Diffuse irradiance on a horizontal surface given a radiation for one hour + // 0.769 MJ/m^2 = 213,61111 Wh/m^2 + val gDifH = 213.61111d When("the ground reflection is calculated") - val eRefSCalc = pvModel.calcReflectedRadiationOnSlopedSurface( - WattHoursPerSquareMeter(eBeamH), - WattHoursPerSquareMeter(eDifH), + val gRefSCalc = pvModel.calcReflectedIrradianceOnSlopedSurface( + WattsPerSquareMeter(gBeamH), + WattsPerSquareMeter(gDifH), Degrees(gammaEDeg), albedo, ) Then("result should match the test data") - eRefSCalc should approximate(WattHoursPerSquareMeter(eRefSSol)) + gRefSCalc should approximate(WattsPerSquareMeter(gRefSSol)) } } }