diff --git a/CHANGELOG.md b/CHANGELOG.md index 80e8e5acb3..86a0a0be0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -72,6 +72,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Adapted to changes of EvcsInput in PSDM [#377](https://github.com/ie3-institute/simona/pull/377) - Fix breaking SIMONA caused by changes in simonaAPI [#384] (https://github.com/ie3-institute/simona/issues/384) - Fixed awaiting departed EVs in ExtEvDataService [#392](https://github.com/ie3-institute/simona/issues/392) +- Fixed missing ModelBaseStateData generation for random load profiles [#399](https://github.com/ie3-institute/simona/issues/399) +- Fixed non-random first days of random load profiles [#401](https://github.com/ie3-institute/simona/issues/401) ### Removed - Remove workaround for tscfg tmp directory [#178](https://github.com/ie3-institute/simona/issues/178) diff --git a/build.gradle b/build.gradle index 49691c72f9..76fc204a55 100644 --- a/build.gradle +++ b/build.gradle @@ -13,7 +13,7 @@ plugins { id "de.undercouch.download" version "5.3.0" // downloads plugin id "kr.motd.sphinx" version "2.10.1" // documentation generation id "com.github.johnrengelman.shadow" version "7.1.2" // fat jar - id "org.sonarqube" version "3.4.0.2513" // sonarqube + id "org.sonarqube" version "3.5.0.2730" // sonarqube id "org.scoverage" version "7.0.1" // scala code coverage scoverage id "com.github.maiflai.scalatest" version "0.32" // run scalatest without specific spec task id 'org.hidetake.ssh' version '2.10.1' diff --git a/src/main/scala/edu/ie3/simona/agent/participant/load/LoadAgentFundamentals.scala b/src/main/scala/edu/ie3/simona/agent/participant/load/LoadAgentFundamentals.scala index 1fd1a6ee6f..4844f5f181 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant/load/LoadAgentFundamentals.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant/load/LoadAgentFundamentals.scala @@ -41,7 +41,10 @@ import edu.ie3.simona.model.participant.load.profile.{ LoadProfileStore, ProfileLoadModel } -import edu.ie3.simona.model.participant.load.random.RandomLoadModel +import edu.ie3.simona.model.participant.load.random.{ + RandomLoadModel, + RandomLoadParamStore +} import edu.ie3.simona.model.participant.load.random.RandomLoadModel.RandomRelevantData import edu.ie3.simona.model.participant.load.{ FixedLoadModel, @@ -149,6 +152,13 @@ protected trait LoadAgentFundamentals[LD <: LoadRelevantData, LM <: LoadModel[ profileLoadModel.operationInterval.start, profileLoadModel.operationInterval.end ) + case randomLoadModel: RandomLoadModel => + activationTicksInOperationTime( + simulationStartDate, + RandomLoadParamStore.resolution.getSeconds, + randomLoadModel.operationInterval.start, + randomLoadModel.operationInterval.end + ) case _ => Array.emptyLongArray } diff --git a/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala b/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala index 2dbd83fdfe..095e331eba 100644 --- a/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala @@ -194,6 +194,7 @@ final case class StorageModel( // don't allow under- or overcharge e.g. due to tick rounding error // allow charges below dod though since batteries can start at 0 kWh + // TODO don't allow SOCs below dod zeroKWH.max(eStorage.min(newEnergy)) } @@ -258,6 +259,7 @@ object StorageModel { inputModel.getType.getDod, initialSoc ) + // TODO initialSoc >= dod must be true // TODO include activePowerGradient, lifeTime, lifeCycle ? model.enable() diff --git a/src/main/scala/edu/ie3/simona/model/participant/evcs/EvcsModel.scala b/src/main/scala/edu/ie3/simona/model/participant/evcs/EvcsModel.scala index 761d660a09..b371c8d882 100644 --- a/src/main/scala/edu/ie3/simona/model/participant/evcs/EvcsModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant/evcs/EvcsModel.scala @@ -265,7 +265,7 @@ final case class EvcsModel( /* Determine the energy charged within this slice of the schedule and accumulate it */ accumulatedEnergy.add( - chargedEnergyInScheduleSlice(trimmedEntry) + chargedEnergyInScheduleEntry(trimmedEntry) ) } /* Update EV with the charged energy during the charging interval */ @@ -341,7 +341,7 @@ final case class EvcsModel( _ -> ChargingSchedule.Entry(lastTick, lastTick, 0d.asKiloWatt) } - val (_, _, evResults, evcsResults) = + val (currentEvs, currentSchedules, evResults, evcsResults) = startAndStopTicks.foldLeft( lastEvMap, startingSchedules, @@ -388,7 +388,7 @@ final case class EvcsModel( // update EV val newEvStoredEnergy = ev.getStoredEnergy.add( - chargedEnergyInScheduleSlice(entry) + chargedEnergyInScheduleEntry(entry) ) val newEv = ev.copyWith(newEvStoredEnergy) @@ -425,7 +425,26 @@ final case class EvcsModel( ) } - (evResults, evcsResults) + // special case: also add EVs that are departing at current tick + // because they won't be included when the next results are created + val departingEvResults = currentSchedules + .map { case evUuid -> _ => + currentEvs(evUuid) + } + .filter { + // only take those that are departing now + _.getDepartureTick.toLong == currentTick + } + .map { + createEvResult( + _, + currentTick, + 0d.asKiloWatt, + voltageMagnitude + ) + } + + (evResults ++ departingEvResults, evcsResults) } private def createEvResult( @@ -480,11 +499,11 @@ final case class EvcsModel( * interval. * * @param scheduleEntry - * Definition of the schedule slice + * The schedule entry * @return - * The energy charged during this slice + * The energy charged during the time interval of the schedule entry */ - private def chargedEnergyInScheduleSlice( + private def chargedEnergyInScheduleEntry( scheduleEntry: ChargingSchedule.Entry ): ComparableQuantity[Energy] = scheduleEntry.chargingPower @@ -700,9 +719,9 @@ final case class EvcsModel( sum.add(power) } - val (maxCharging, maxDischarging) = - preferredScheduling.foldLeft((zeroKW, zeroKW)) { - case ((chargingSum, dischargingSum), (ev, _)) => + val (maxCharging, forcedCharging, maxDischarging) = + preferredScheduling.foldLeft((zeroKW, zeroKW, zeroKW)) { + case ((chargingSum, forcedSum, dischargingSum), (ev, _)) => val maxPower = getMaxAvailableChargingPower(ev) val maxCharging = @@ -711,19 +730,36 @@ final case class EvcsModel( else zeroKW + val forcedCharging = + if (isEmpty(ev) && !isInLowerMargin(ev)) + maxPower // TODO maybe use preferred power instead + else + zeroKW + val maxDischarging = if (!isEmpty(ev) && vehicle2grid) maxPower.multiply(-1) else zeroKW - (chargingSum.add(maxCharging), dischargingSum.add(maxDischarging)) + ( + chargingSum.add(maxCharging), + forcedSum.add(forcedCharging), + dischargingSum.add(maxDischarging) + ) } + // if we need to charge at least one EV, we cannot discharge any other + val (adaptedMin, adaptedPreferred) = + if (forcedCharging.isGreaterThan(zeroKW)) + (forcedCharging, preferredPower.max(forcedCharging)) + else + (maxDischarging, preferredPower) + ProvideMinMaxFlexOptions( uuid, - preferredPower, - maxDischarging, + adaptedPreferred, + adaptedMin, maxCharging ) } @@ -755,22 +791,40 @@ final case class EvcsModel( !isEmpty(ev) } - val chargingSchedule = - createScheduleWithSetPower(data.tick, applicableEvs, setPower) + val (forcedChargingEvs, regularChargingEvs) = + if (setPower.isGreaterThan(zeroKW)) + // lower margin is excluded since charging is not required here anymore + applicableEvs.partition { ev => + isEmpty(ev) && !isInLowerMargin(ev) + } + else + (Set.empty[EvModel], applicableEvs) - val scheduleAtNextActivation = chargingSchedule - .map { case (_, _, _, scheduleAtNext) => - scheduleAtNext - } + val (forcedSchedules, remainingPower) = + createScheduleWithSetPower(data.tick, forcedChargingEvs, setPower) + + val (regularSchedules, _) = + createScheduleWithSetPower(data.tick, regularChargingEvs, remainingPower) + + val combinedSchedules = forcedSchedules ++ regularSchedules + + val schedulesOnly = combinedSchedules.flatMap { case (_, scheduleOpt) => + scheduleOpt + } + + val scheduleAtNextActivation = schedulesOnly + .map { case (_, _, scheduleAtNext) => scheduleAtNext } .reduceOption(_ || _) .getOrElse(false) - val nextScheduledTick = chargingSchedule.map { case (_, _, endTick, _) => + val nextScheduledTick = schedulesOnly.map { case (_, endTick, _) => endTick }.minOption - val allSchedules = chargingSchedule.map { case (ev, schedule, _, _) => - ev -> Some(schedule) + val allSchedules = combinedSchedules.map { + case (ev, Some((schedule, _, _))) => + ev -> Some(schedule) + case (ev, None) => ev -> None }.toMap ++ otherEvs.map(_ -> None).toMap ( @@ -786,13 +840,32 @@ final case class EvcsModel( ) } + /** @param currentTick + * The current tick + * @param evs + * The collection of EVs to assign charging power to + * @param setPower + * The remaining power to assign to given EVs + * @return + * A set of EV model and possibly charging schedule and activation + * indicators, as well as the remaining power that could not be assigned to + * given EVs + */ private def createScheduleWithSetPower( currentTick: Long, evs: Set[EvModel], setPower: ComparableQuantity[Power] - ): Set[(EvModel, ChargingSchedule, Long, Boolean)] = { + ): ( + Set[(EvModel, Option[(ChargingSchedule, Long, Boolean)])], + ComparableQuantity[Power] + ) = { + + if (evs.isEmpty) return (Set.empty, setPower) - if (evs.isEmpty) return Set.empty + if (QuantityUtil.isEquivalentAbs(setPower, zeroKW, 0d)) { + // No power left. Rest is not charging + return (evs.map { _ -> None }, setPower) + } val proposedPower = setPower.divide(evs.size) @@ -806,20 +879,24 @@ final case class EvcsModel( if (exceedingPowerEvs.isEmpty) { // end of recursion, rest of charging power fits to all - fittingPowerEvs.map { ev => + val results = fittingPowerEvs.map { ev => val chargingTicks = calculateChargingDuration(ev, proposedPower) val endTick = Math.min(currentTick + chargingTicks, ev.getDepartureTick) ( ev, - ChargingSchedule( - ev, - Seq(ChargingSchedule.Entry(currentTick, endTick, proposedPower)) - ), - endTick, - isFull(ev) || isEmpty(ev) + Some( + ChargingSchedule( + ev, + Seq(ChargingSchedule.Entry(currentTick, endTick, proposedPower)) + ), + endTick, + isFull(ev) || isEmpty(ev) || isInLowerMargin(ev) + ) ) - } + }: Set[(EvModel, Option[(ChargingSchedule, Long, Boolean)])] + + (results, zeroKW) } else { // not all evs can be charged with proposed power @@ -838,34 +915,37 @@ final case class EvcsModel( (ev, power, endTick) } - // if there's evs left whose max power has not been exceeded, go on with the recursion - val nextIterationResults = if (fittingPowerEvs.nonEmpty) { + // sum up allocated power + val chargingPowerSum = maxCharged.foldLeft(zeroKW) { + case (powerSum, (_, chargingPower, _)) => + powerSum.add(chargingPower) + } - // sum up allocated power - val chargingPowerSum = maxCharged.foldLeft(zeroKW) { - case (powerSum, (_, chargingPower, _)) => - powerSum.add(chargingPower) - } + val remainingAfterAllocation = setPower.subtract(chargingPowerSum) - // go into the next recursion step with the remaining power + // go into the next recursion step with the remaining power + val (nextIterationResults, remainingAfterRecursion) = createScheduleWithSetPower( currentTick, fittingPowerEvs, - setPower.subtract(chargingPowerSum) + remainingAfterAllocation ) - } else Set.empty - maxCharged.map { case (ev, power, endTick) => + val combinedResults = maxCharged.map { case (ev, power, endTick) => ( ev, - ChargingSchedule( - ev, - Seq(ChargingSchedule.Entry(currentTick, endTick, power)) - ), - endTick, - isFull(ev) || isEmpty(ev) + Some( + ChargingSchedule( + ev, + Seq(ChargingSchedule.Entry(currentTick, endTick, power)) + ), + endTick, + isFull(ev) || isEmpty(ev) || isInLowerMargin(ev) + ) ) } ++ nextIterationResults + + (combinedResults, remainingAfterRecursion) } } @@ -875,12 +955,20 @@ final case class EvcsModel( power: ComparableQuantity[Power] ): Long = { val timeUntilFullOrEmpty = - if (power.isGreaterThan(zeroKW)) - ev.getEStorage + if (power.isGreaterThan(zeroKW)) { + + // if we're below lowest SOC, flex options will change at that point + val targetEnergy = + if (isEmpty(ev) && !isInLowerMargin(ev)) + ev.getEStorage.multiply(lowestEvSoc) + else + ev.getEStorage + + targetEnergy .subtract(ev.getStoredEnergy) .divide(power) .asType(classOf[Time]) - else + } else ev.getStoredEnergy .subtract(ev.getEStorage.multiply(lowestEvSoc)) .divide(power.multiply(-1)) @@ -913,6 +1001,23 @@ final case class EvcsModel( ev.getEStorage.multiply(lowestEvSoc).add(calcToleranceMargin(ev)) ) + /** @param ev + * the ev whose stored energy is to be checked + * @return + * whether the given ev's stored energy is within +- tolerance of the + * minimal charged energy allowed + */ + private def isInLowerMargin(ev: EvModel): Boolean = { + val toleranceMargin = calcToleranceMargin(ev) + val lowestSoc = ev.getEStorage.multiply(lowestEvSoc) + + ev.getStoredEnergy.isLessThanOrEqualTo( + lowestSoc.add(toleranceMargin) + ) && ev.getStoredEnergy.isGreaterThanOrEqualTo( + lowestSoc.subtract(toleranceMargin) + ) + } + private def calcToleranceMargin(ev: EvModel): ComparableQuantity[Energy] = getMaxAvailableChargingPower(ev) .multiply(Quantities.getQuantity(1, SECOND)) @@ -932,6 +1037,8 @@ final case class EvcsModel( data: EvcsRelevantData, lastState: EvcsState ): Set[EvModel] = { + // TODO use Seq instead of Set as return value + // if last state is from before current tick, determine current state val currentEVs = if (lastState.tick < data.tick) diff --git a/src/main/scala/edu/ie3/simona/model/participant/load/random/RandomLoadModel.scala b/src/main/scala/edu/ie3/simona/model/participant/load/random/RandomLoadModel.scala index 3f5033510e..255c45a77e 100644 --- a/src/main/scala/edu/ie3/simona/model/participant/load/random/RandomLoadModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant/load/random/RandomLoadModel.scala @@ -26,6 +26,7 @@ import java.util.UUID import javax.measure.quantity.{Dimensionless, Power} import scala.annotation.tailrec import scala.collection.mutable +import scala.util.Random /** A load model consuming energy followed by time resolved probability. The * referencing to rated active power maps the output's 95 % quantile to this @@ -79,7 +80,6 @@ final case class RandomLoadModel( ) } - private val randomFactory = RandomFactory.get(430431L) private val randomLoadParamStore = RandomLoadParamStore() type GevKey = (DayType.Value, Int) @@ -146,6 +146,7 @@ final case class RandomLoadModel( case Some(foundIt) => foundIt case None => /* Instantiate new gev distribution, put it to storage and return it */ + val randomFactory = RandomFactory.get(Random.nextLong()) val gevParameters = randomLoadParamStore.parameters(dateTime) val newGev = new GeneralizedExtremeValueDistribution( gevParameters.my, diff --git a/src/main/scala/edu/ie3/simona/model/participant/load/random/RandomLoadParamStore.scala b/src/main/scala/edu/ie3/simona/model/participant/load/random/RandomLoadParamStore.scala index af779e2439..44f481eafe 100644 --- a/src/main/scala/edu/ie3/simona/model/participant/load/random/RandomLoadParamStore.scala +++ b/src/main/scala/edu/ie3/simona/model/participant/load/random/RandomLoadParamStore.scala @@ -7,8 +7,7 @@ package edu.ie3.simona.model.participant.load.random import java.io.{InputStreamReader, Reader} -import java.time.ZonedDateTime - +import java.time.{Duration, ZonedDateTime} import com.typesafe.scalalogging.LazyLogging import edu.ie3.simona.exceptions.FileIOException import edu.ie3.simona.model.participant.load.DayType @@ -44,6 +43,7 @@ final case class RandomLoadParamStore private (reader: Reader) { } case object RandomLoadParamStore extends LazyLogging { + val resolution: Duration = Duration.ofMinutes(15) /** Default value store, that uses information from a file * 'random_load_parameters.csv' placed in the resources folder of the project diff --git a/src/test/scala/edu/ie3/simona/agent/participant/EvcsAgentModelCalculationSpec.scala b/src/test/scala/edu/ie3/simona/agent/participant/EvcsAgentModelCalculationSpec.scala index 1a4c43af50..6fb8129e9b 100644 --- a/src/test/scala/edu/ie3/simona/agent/participant/EvcsAgentModelCalculationSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/participant/EvcsAgentModelCalculationSpec.scala @@ -1447,7 +1447,7 @@ class EvcsAgentModelCalculationSpec /* TICK 900 - ev 900 arrives - - charging with 9 kW + - charging with 11 kW */ val ev900 = evA.copyWithDeparture(4500L) @@ -1478,7 +1478,7 @@ class EvcsAgentModelCalculationSpec ) => modelUuid shouldBe evcsInputModel.getUuid referencePower shouldBe ev900.getSRatedAC - minPower shouldBe 0d.asKiloWatt // battery is empty + minPower shouldBe ev900.getSRatedAC // battery is empty maxPower shouldBe ev900.getSRatedAC } @@ -1486,11 +1486,11 @@ class EvcsAgentModelCalculationSpec flexResult.getInputModel shouldBe evcsInputModel.getUuid flexResult.getTime shouldBe 900L.toDateTime flexResult.getpRef should beEquivalentTo(ev900.getSRatedAC) - flexResult.getpMin should beEquivalentTo(0d.asKiloWatt) + flexResult.getpMin should beEquivalentTo(ev900.getSRatedAC) flexResult.getpMax should beEquivalentTo(ev900.getSRatedAC) } - emAgent.send(evcsAgent, IssuePowerCtrl(900L, 9.asKiloWatt)) + emAgent.send(evcsAgent, IssueNoCtrl(900L)) // at 4500 ev is departing emAgent.expectMsg( @@ -1504,7 +1504,7 @@ class EvcsAgentModelCalculationSpec emAgent.expectMsgType[ParticipantResultEvent] match { case result => result.systemParticipantResult.getP should equalWithTolerance( - 9.asKiloWatt, + ev900.getSRatedAC, testingTolerance ) result.systemParticipantResult.getQ should equalWithTolerance( @@ -1545,27 +1545,37 @@ class EvcsAgentModelCalculationSpec evs.headOption.foreach { ev => ev.getUuid shouldBe ev900.getUuid ev.getStoredEnergy should equalWithTolerance( - 9.asKiloWattHour, + 11.asKiloWattHour, testingTolerance ) } } // results arrive right after departure request - resultListener.expectMsgPF() { - case ParticipantResultEvent(result: EvResult) => - result.getInputModel shouldBe ev900.getUuid - result.getTime shouldBe 900L.toDateTime - result.getP should beEquivalentTo(9d.asKiloWatt) - result.getQ should beEquivalentTo(0d.asMegaVar) - result.getSoc should beEquivalentTo(0d.asPercent) - } + Range(0, 2) + .map { _ => + resultListener.expectMsgType[ParticipantResultEvent] + } + .foreach { + case ParticipantResultEvent(result: EvResult) + if result.getTime.equals(900L.toDateTime) => + result.getInputModel shouldBe ev900.getUuid + result.getP should beEquivalentTo(ev900.getSRatedAC) + result.getQ should beEquivalentTo(0d.asMegaVar) + result.getSoc should beEquivalentTo(0d.asPercent) + case ParticipantResultEvent(result: EvResult) + if result.getTime.equals(4500L.toDateTime) => + result.getInputModel shouldBe ev900.getUuid + result.getP should beEquivalentTo(0d.asKiloWatt) + result.getQ should beEquivalentTo(0d.asMegaVar) + result.getSoc should beEquivalentTo(18.96551724137931d.asPercent) + } resultListener.expectMsgPF() { case ParticipantResultEvent(result: EvcsResult) => result.getInputModel shouldBe evcsInputModel.getUuid result.getTime shouldBe 900L.toDateTime - result.getP should beEquivalentTo(9d.asKiloWatt) + result.getP should beEquivalentTo(ev900.getSRatedAC) result.getQ should beEquivalentTo(0d.asMegaVar) } @@ -1601,7 +1611,7 @@ class EvcsAgentModelCalculationSpec ) => modelUuid shouldBe evcsInputModel.getUuid referencePower shouldBe ev4500.getSRatedAC - minPower shouldBe 0d.asKiloWatt // battery is empty + minPower shouldBe ev900.getSRatedAC // battery is empty maxPower shouldBe ev4500.getSRatedAC } @@ -1609,20 +1619,20 @@ class EvcsAgentModelCalculationSpec flexResult.getInputModel shouldBe evcsInputModel.getUuid flexResult.getTime shouldBe 4500L.toDateTime flexResult.getpRef should beEquivalentTo(ev4500.getSRatedAC) - flexResult.getpMin should beEquivalentTo(0d.asKiloWatt) + flexResult.getpMin should beEquivalentTo(ev4500.getSRatedAC) flexResult.getpMax should beEquivalentTo(ev4500.getSRatedAC) } emAgent.send(evcsAgent, IssueNoCtrl(4500L)) // we currently have an empty battery in ev4500 - // time to charge fully ~= 7.2727273h = 26182 ticks (rounded) from now - // current tick is 4500, thus: 4500 + 26182 = 30682 + // time to charge to minimal soc ~= 1.45454545455h = 5236 ticks (rounded) from now + // current tick is 4500, thus: 4500 + 5236 = 9736 emAgent.expectMsg( FlexCtrlCompletion( modelUuid = evcsInputModel.getUuid, requestAtNextActivation = true, - requestAtTick = Some(30682L) + requestAtTick = Some(9736L) ) ) @@ -1643,13 +1653,13 @@ class EvcsAgentModelCalculationSpec // already sent out after EV departed resultListener.expectNoMessage() - /* TICK 8100 + /* TICK 9736 - flex control changes - charging with 10 kW */ // sending flex request at very next activated tick - emAgent.send(evcsAgent, RequestFlexOptions(8100L)) + emAgent.send(evcsAgent, RequestFlexOptions(9736L)) emAgent.expectMsgType[ProvideFlexOptions] match { case ProvideMinMaxFlexOptions( @@ -1660,30 +1670,29 @@ class EvcsAgentModelCalculationSpec ) => modelUuid shouldBe evcsInputModel.getUuid referencePower shouldBe ev4500.getSRatedAC - minPower shouldBe 0d.asKiloWatt // battery is still below lowest soc for discharging + minPower shouldBe 0d.asKiloWatt // battery is exactly at margin maxPower shouldBe ev4500.getSRatedAC } resultListener.expectMsgPF() { case FlexOptionsResultEvent(flexResult) => flexResult.getInputModel shouldBe evcsInputModel.getUuid - flexResult.getTime shouldBe 8100L.toDateTime + flexResult.getTime shouldBe 9736L.toDateTime flexResult.getpRef should beEquivalentTo(ev4500.getSRatedAC) flexResult.getpMin should beEquivalentTo(0d.asKiloWatt) flexResult.getpMax should beEquivalentTo(ev4500.getSRatedAC) } - emAgent.send(evcsAgent, IssuePowerCtrl(8100L, 10.asKiloWatt)) + emAgent.send(evcsAgent, IssuePowerCtrl(9736L, 10.asKiloWatt)) evService.expectNoMessage() - // ev4500 is now at 11 kWh - // time to charge fully = 6.9 h = 24840 ticks from now - // current tick is 8100, thus: 8100 + 24840 = 32940 + // ev4500 is now at 16 kWh + // time to charge fully = 6.4 h = 23040 ticks from now + // current tick is 9736, thus: 9736 + 23040 = 32776 emAgent.expectMsg( FlexCtrlCompletion( modelUuid = evcsInputModel.getUuid, - requestAtTick = Some(32940L), - revokeRequestAtTick = Some(30682L), + requestAtTick = Some(32776L), requestAtNextActivation = true // since battery is still below lowest soc, it's still considered empty ) @@ -1723,7 +1732,8 @@ class EvcsAgentModelCalculationSpec - charging with 16 kW */ - val ev11700 = evA.copyWithDeparture(36000L) + // with stored energy right at minimal SOC + val ev11700 = evA.copyWithDeparture(36000L).copyWith(11.6d.asKiloWattHour) val activation3 = 3L @@ -1780,16 +1790,15 @@ class EvcsAgentModelCalculationSpec // no departing evs here evService.expectNoMessage() - // ev4500 is now at 21 kWh, ev11700 just arrived - // ev4500: time to charge fully = 7.375 h = 26550 ticks from now - // ev11700: time to charge fully = 7.25 h = 26100 ticks from now - // current tick is 11700, thus: 11700 + 26100 = 37800 - // BUT: departing tick 36000 is earlier + // ev4500 is now at ~ 21.45555556 kWh, ev11700 just arrived with 11.6 kWh + // ev4500: time to charge fully ~= 7.3180556 h = 26345 ticks from now + // ev11700: time to charge fully = 5.8 h = 20880 ticks from now + // current tick is 11700, thus: 11700 + 20880 = 32580 emAgent.expectMsg( FlexCtrlCompletion( modelUuid = evcsInputModel.getUuid, - requestAtTick = Some(36000L), - revokeRequestAtTick = Some(32940L), + requestAtTick = Some(32580L), + revokeRequestAtTick = Some(32776L), requestAtNextActivation = true // since battery is still below lowest soc, it's still considered empty ) @@ -1812,16 +1821,16 @@ class EvcsAgentModelCalculationSpec resultListener.expectMsgPF() { case ParticipantResultEvent(result: EvResult) => result.getInputModel shouldBe ev4500.getUuid - result.getTime shouldBe 8100L.toDateTime + result.getTime shouldBe 9736L.toDateTime result.getP should beEquivalentTo(10d.asKiloWatt) result.getQ should beEquivalentTo(0d.asMegaVar) - result.getSoc should beEquivalentTo(13.75d.asPercent) + result.getSoc should beEquivalentTo(20d.asPercent, 1e-2) } resultListener.expectMsgPF() { case ParticipantResultEvent(result: EvcsResult) => result.getInputModel shouldBe evcsInputModel.getUuid - result.getTime shouldBe 8100L.toDateTime + result.getTime shouldBe 9736L.toDateTime result.getP should beEquivalentTo(10d.asKiloWatt) result.getQ should beEquivalentTo(0d.asMegaVar) } @@ -1866,15 +1875,15 @@ class EvcsAgentModelCalculationSpec // no departing evs here evService.expectNoMessage() - // ev4500 is now at 35 kWh, ev11700 at 14 kWh - // ev4500: time to discharge to lowest soc = 1.9 h = 6840 ticks from now - // ev11700: time to discharge fully = 0.24 h = 864 ticks from now - // current tick is 18000, thus: 18000 + 864 = 18864 + // ev4500 is now at ~ 35.455556 kWh, ev11700 at 25.6 kWh + // ev4500: time to discharge to lowest soc ~= 1.9455556 h = 7004 ticks from now + // ev11700: time to discharge to lowest soc ~= 1.4 h = 5040 ticks from now + // current tick is 18000, thus: 18000 + 5040 = 23040 emAgent.expectMsg( FlexCtrlCompletion( modelUuid = evcsInputModel.getUuid, - requestAtTick = Some(18864L), - revokeRequestAtTick = Some(36000L) + requestAtTick = Some(23040L), + revokeRequestAtTick = Some(32580L) ) ) @@ -1900,13 +1909,13 @@ class EvcsAgentModelCalculationSpec result.getTime shouldBe 11700L.toDateTime result.getP should beEquivalentTo(8d.asKiloWatt) result.getQ should beEquivalentTo(0d.asMegaVar) - result.getSoc should beEquivalentTo(26.25d.asPercent) + result.getSoc should beEquivalentTo(26.819d.asPercent, 1e-2) case ParticipantResultEvent(result: EvResult) if result.getInputModel == ev11700.getUuid => result.getTime shouldBe 11700L.toDateTime result.getP should beEquivalentTo(8d.asKiloWatt) result.getQ should beEquivalentTo(0d.asMegaVar) - result.getSoc should beEquivalentTo(0d.asPercent) + result.getSoc should beEquivalentTo(20.0d.asPercent) } resultListener.expectMsgPF() { @@ -1917,12 +1926,12 @@ class EvcsAgentModelCalculationSpec result.getQ should beEquivalentTo(0d.asMegaVar) } - /* TICK 18864 + /* TICK 23040 - ev11700 at lowest soc - discharging with 10 kW */ - emAgent.send(evcsAgent, RequestFlexOptions(18864L)) + emAgent.send(evcsAgent, RequestFlexOptions(23040L)) emAgent.expectMsgType[ProvideFlexOptions] match { case ProvideMinMaxFlexOptions( @@ -1941,7 +1950,7 @@ class EvcsAgentModelCalculationSpec resultListener.expectMsgPF() { case FlexOptionsResultEvent(flexResult) => flexResult.getInputModel shouldBe evcsInputModel.getUuid - flexResult.getTime shouldBe 18864L.toDateTime + flexResult.getTime shouldBe 23040L.toDateTime flexResult.getpRef should beEquivalentTo(combinedChargingPower) flexResult.getpMin should beEquivalentTo( ev4500.getSRatedAC.multiply( @@ -1951,18 +1960,18 @@ class EvcsAgentModelCalculationSpec flexResult.getpMax should beEquivalentTo(combinedChargingPower) } - emAgent.send(evcsAgent, IssuePowerCtrl(18864L, (-10).asKiloWatt)) + emAgent.send(evcsAgent, IssuePowerCtrl(23040L, (-10).asKiloWatt)) // no departing evs here evService.expectNoMessage() - // ev4500 is now at 32.6 kWh, ev11700 at 11.6 kWh (lowest soc) - // ev4500: time to discharge to lowest soc = 1.66 h = 5976 ticks from now - // current tick is 18864, thus: 18864 + 5976 = 24840 + // ev4500 is now at 21.455556 kWh, ev11700 at 11.6 kWh (lowest soc) + // ev4500: time to discharge to lowest soc = 0.5455556 h = 1964 ticks from now + // current tick is 18864, thus: 23040 + 1964 = 25004 emAgent.expectMsg( FlexCtrlCompletion( modelUuid = evcsInputModel.getUuid, - requestAtTick = Some(24840L) + requestAtTick = Some(25004L) ) ) @@ -1988,13 +1997,13 @@ class EvcsAgentModelCalculationSpec result.getTime shouldBe 18000L.toDateTime result.getP should beEquivalentTo((-10d).asKiloWatt) result.getQ should beEquivalentTo(0d.asMegaVar) - result.getSoc should beEquivalentTo(43.75d.asPercent) + result.getSoc should beEquivalentTo(44.3194d.asPercent, 1e-2) case ParticipantResultEvent(result: EvResult) if result.getInputModel == ev11700.getUuid => result.getTime shouldBe 18000L.toDateTime result.getP should beEquivalentTo((-10d).asKiloWatt) result.getQ should beEquivalentTo(0d.asMegaVar) - result.getSoc should beEquivalentTo(24.137931034483d.asPercent) + result.getSoc should beEquivalentTo(44.137931034d.asPercent, 1e-6) } resultListener.expectMsgPF() { @@ -2005,72 +2014,12 @@ class EvcsAgentModelCalculationSpec result.getQ should beEquivalentTo(0d.asMegaVar) } - /* TICK 21600 - - changing flex control - - discharging with 9 kW - */ - - emAgent.send(evcsAgent, IssuePowerCtrl(21600L, (-9).asKiloWatt)) - - // no departing evs here - evService.expectNoMessage() - - // ev4500 is now at 25 kWh - // time to discharge to lowest soc = 1 h = 3600 ticks from now - // current tick is 18864, thus: 21600 + 3600 = 25200 - emAgent.expectMsg( - FlexCtrlCompletion( - modelUuid = evcsInputModel.getUuid, - requestAtTick = Some(25200L), - revokeRequestAtTick = Some(24840L) - ) - ) - - emAgent.expectMsgType[ParticipantResultEvent] match { - case result => - result.systemParticipantResult.getP should equalWithTolerance( - (-9).asKiloWatt, - testingTolerance - ) - result.systemParticipantResult.getQ should equalWithTolerance( - 0.asMegaVar, - testingTolerance - ) - } - - Range(0, 2) - .map { _ => - resultListener.expectMsgType[ParticipantResultEvent] - } - .foreach { - case ParticipantResultEvent(result: EvResult) - if result.getInputModel == ev4500.getUuid => - result.getTime shouldBe 18864L.toDateTime - result.getP should beEquivalentTo((-10d).asKiloWatt) - result.getQ should beEquivalentTo(0d.asMegaVar) - result.getSoc should beEquivalentTo(40.75d.asPercent) - case ParticipantResultEvent(result: EvResult) - if result.getInputModel == ev11700.getUuid => - result.getTime shouldBe 18864L.toDateTime - result.getP should beEquivalentTo(0d.asKiloWatt) - result.getQ should beEquivalentTo(0d.asMegaVar) - result.getSoc should beEquivalentTo(20d.asPercent) - } - - resultListener.expectMsgPF() { - case ParticipantResultEvent(result: EvcsResult) => - result.getInputModel shouldBe evcsInputModel.getUuid - result.getTime shouldBe 18864L.toDateTime - result.getP should beEquivalentTo((-10d).asKiloWatt) - result.getQ should beEquivalentTo(0d.asMegaVar) - } - - /* TICK 25200 + /* TICK 25004L - both evs at lowest soc - no power */ - emAgent.send(evcsAgent, RequestFlexOptions(25200L)) + emAgent.send(evcsAgent, RequestFlexOptions(25004L)) emAgent.expectMsgType[ProvideFlexOptions] match { case ProvideMinMaxFlexOptions( @@ -2087,13 +2036,13 @@ class EvcsAgentModelCalculationSpec resultListener.expectMsgPF() { case FlexOptionsResultEvent(flexResult) => flexResult.getInputModel shouldBe evcsInputModel.getUuid - flexResult.getTime shouldBe 25200L.toDateTime + flexResult.getTime shouldBe 25004L.toDateTime flexResult.getpRef should beEquivalentTo(combinedChargingPower) flexResult.getpMin should beEquivalentTo(0.asKiloWatt) flexResult.getpMax should beEquivalentTo(combinedChargingPower) } - emAgent.send(evcsAgent, IssuePowerCtrl(25200L, 0.asKiloWatt)) + emAgent.send(evcsAgent, IssuePowerCtrl(25004L, 0.asKiloWatt)) // no departing evs here evService.expectNoMessage() @@ -2124,13 +2073,13 @@ class EvcsAgentModelCalculationSpec .foreach { case ParticipantResultEvent(result: EvResult) if result.getInputModel == ev4500.getUuid => - result.getTime shouldBe 21600L.toDateTime - result.getP should beEquivalentTo((-9d).asKiloWatt) + result.getTime shouldBe 23040L.toDateTime + result.getP should beEquivalentTo((-10d).asKiloWatt) result.getQ should beEquivalentTo(0d.asMegaVar) - result.getSoc should beEquivalentTo(31.25d.asPercent) + result.getSoc should beEquivalentTo(26.819445d.asPercent, 1e-2) case ParticipantResultEvent(result: EvResult) if result.getInputModel == ev11700.getUuid => - result.getTime shouldBe 21600L.toDateTime + result.getTime shouldBe 23040L.toDateTime result.getP should beEquivalentTo(0d.asKiloWatt) result.getQ should beEquivalentTo(0d.asMegaVar) result.getSoc should beEquivalentTo(20d.asPercent) @@ -2139,8 +2088,8 @@ class EvcsAgentModelCalculationSpec resultListener.expectMsgPF() { case ParticipantResultEvent(result: EvcsResult) => result.getInputModel shouldBe evcsInputModel.getUuid - result.getTime shouldBe 21600L.toDateTime - result.getP should beEquivalentTo((-9d).asKiloWatt) + result.getTime shouldBe 23040L.toDateTime + result.getP should beEquivalentTo((-10d).asKiloWatt) result.getQ should beEquivalentTo(0d.asMegaVar) } @@ -2178,13 +2127,13 @@ class EvcsAgentModelCalculationSpec .foreach { case ParticipantResultEvent(result: EvResult) if result.getInputModel == ev4500.getUuid => - result.getTime shouldBe 25200L.toDateTime + result.getTime shouldBe 25004L.toDateTime result.getP should beEquivalentTo(0d.asKiloWatt) result.getQ should beEquivalentTo(0d.asMegaVar) - result.getSoc should beEquivalentTo(20d.asPercent) + result.getSoc should beEquivalentTo(20d.asPercent, 1e-2) case ParticipantResultEvent(result: EvResult) if result.getInputModel == ev11700.getUuid => - result.getTime shouldBe 25200L.toDateTime + result.getTime shouldBe 25004L.toDateTime result.getP should beEquivalentTo(0d.asKiloWatt) result.getQ should beEquivalentTo(0d.asMegaVar) result.getSoc should beEquivalentTo(20d.asPercent) @@ -2193,7 +2142,7 @@ class EvcsAgentModelCalculationSpec resultListener.expectMsgPF() { case ParticipantResultEvent(result: EvcsResult) => result.getInputModel shouldBe evcsInputModel.getUuid - result.getTime shouldBe 25200L.toDateTime + result.getTime shouldBe 25004L.toDateTime result.getP should beEquivalentTo(0d.asKiloWatt) result.getQ should beEquivalentTo(0d.asMegaVar) } diff --git a/src/test/scala/edu/ie3/simona/model/participant/evcs/EvcsModelSpec.scala b/src/test/scala/edu/ie3/simona/model/participant/evcs/EvcsModelSpec.scala index b28b4ad0b8..05e186989f 100644 --- a/src/test/scala/edu/ie3/simona/model/participant/evcs/EvcsModelSpec.scala +++ b/src/test/scala/edu/ie3/simona/model/participant/evcs/EvcsModelSpec.scala @@ -207,7 +207,7 @@ class EvcsModelSpec 10.0.asKiloWatt, 10.0.asKiloWattHour, 0d.asKiloWattHour, - 7200L // is ignored here + 10800L ) val schedule = ChargingSchedule( @@ -302,7 +302,7 @@ class EvcsModelSpec 10.0.asKiloWatt, 10.0.asKiloWattHour, 0d.asKiloWattHour, - 7200L // is ignored here + 18000L ) val ev2 = new MockEvModel( @@ -312,7 +312,7 @@ class EvcsModelSpec 10.0.asKiloWatt, 10.0.asKiloWattHour, 0d.asKiloWattHour, - 7200L // is ignored here + 18000L ) val schedule1 = ChargingSchedule( @@ -407,6 +407,79 @@ class EvcsModelSpec } } + "EV is departing at current tick" in { + + val ev = new MockEvModel( + UUID.randomUUID(), + "TestEv", + 5.0.asKiloWatt, // using AC charging here + 10.0.asKiloWatt, + 10.0.asKiloWattHour, + 0d.asKiloWattHour, + 7200L // equals the current tick + ) + + val schedule = ChargingSchedule( + ev, + Seq( + Entry(3600L, 7200L, 2d.asKiloWatt) + ) + ) + + val lastTick = 1800L + val currentTick = 7200L + + val lastState = EvcsState( + Set(ev), + Map(ev -> Some(schedule)), + lastTick + ) + + val (actualEvResults, actualEvcsResults) = + evcsStandardModel.createResults( + lastState, + currentTick, + 1d.asPu + ) + + // tick, p in kW, soc in % + val expectedEvResults = + Seq( + (1800L, 0d, 0d), + (3600L, 2d, 0d), + // this result normally does not appear + // if EV does not depart at current tick + (7200L, 0d, 20d) + ) + + // tick, p in kW + val expectedEvcsResults = + Seq( + (1800L, 0d), + (3600L, 2d) + ) + + actualEvResults should have size expectedEvResults.size + + actualEvResults should have size expectedEvResults.size + actualEvResults.zip(expectedEvResults).foreach { + case (actual, (startTick, p, soc)) => + actual.getTime shouldBe startTick.toDateTime(simulationStart) + actual.getP should beEquivalentTo(p.asKiloWatt) + actual.getQ should beEquivalentTo(zeroKW) + actual.getSoc should beEquivalentTo(soc.asPercent) + } + + actualEvcsResults should have size expectedEvcsResults.size + actualEvcsResults.zip(expectedEvcsResults).foreach { + case (actual, (startTick, p)) => + actual.getTime shouldBe startTick.toDateTime(simulationStart) + actual.getInputModel shouldBe evcsStandardModel.getUuid + actual.getP should beEquivalentTo(p.asKiloWatt) + actual.getQ should beEquivalentTo(zeroKW) + } + } + } "handle flexibility correctly" when { @@ -434,19 +507,35 @@ class EvcsModelSpec /* 1: empty */ // 2: empty - (0.0, 0.0, 0.0, 15.0, 0.0, 15.0), + (0.0, 0.0, 0.0, 15.0, 15.0, 15.0), + // 2: at lower margin + (0.0, 3.0, 0.0, 15.0, 10.0, 15.0), + // 2: mid-way full (charged to 7.5 kWh) + (0.0, 0.0, 5.0, 15.0, 10.0, 15.0), + // 2: mid-way full (set to 7.5 kWh) + (0.0, 7.5, 0.0, 15.0, 10.0, 15.0), + // 2: almost full (12.5 kWh) + (0.0, 5.0, 5.0, 12.5, 10.0, 15.0), + // 2: full (set) + (0.0, 15.0, 0.0, 10.0, 10.0, 10.0), + + /* 1: at lower margin (set to 2 kWh) */ + // 2: empty + (2.0, 0.0, 0.0, 13.0, 5.0, 15.0), + // 2: at lower margin + (2.0, 3.0, 0.0, 13.0, 0.0, 15.0), // 2: mid-way full (charged to 7.5 kWh) - (0.0, 0.0, 5.0, 15.0, -5.0, 15.0), + (2.0, 0.0, 5.0, 13.0, -5.0, 15.0), // 2: mid-way full (set to 7.5 kWh) - (0.0, 7.5, 0.0, 15.0, -5.0, 15.0), + (2.0, 7.5, 0.0, 13.0, -5.0, 15.0), // 2: almost full (12.5 kWh) - (0.0, 5.0, 5.0, 12.5, -5.0, 15.0), + (2.0, 5.0, 5.0, 10.5, -5.0, 15.0), // 2: full (set) - (0.0, 15.0, 0.0, 10.0, -5.0, 10.0), + (2.0, 15.0, 0.0, 8.0, -5.0, 10.0), /* 1: mid-way full (set to 5 kWh) */ // 2: empty - (5.0, 0.0, 0.0, 10.0, -10.0, 15.0), + (5.0, 0.0, 0.0, 10.0, 5.0, 15.0), // 2: mid-way full (charged to 7.5 kWh) (5.0, 0.0, 5.0, 10.0, -15.0, 15.0), // 2: mid-way full (set to 7.5 kWh) @@ -458,7 +547,7 @@ class EvcsModelSpec /* 1: full (set to 10 kWh) */ // 2: empty - (10.0, 0.0, 0.0, 5.0, -10.0, 5.0), + (10.0, 0.0, 0.0, 5.0, 5.0, 5.0), // 2: mid-way full (charged to 7.5 kWh) (10.0, 0.0, 5.0, 5.0, -15.0, 5.0), // 2: mid-way full (set to 7.5 kWh) @@ -609,21 +698,21 @@ class EvcsModelSpec (10.0, 15.0, 0.0, N, N, false, N), /* setPower is positive (charging) */ - (0.0, 0.0, 4.0, S(2.0, 7200L), S(2.0, 10800L), true, S(7200L)), - (5.0, 0.0, 4.0, S(2.0, 7200L), S(2.0, 10800L), true, S(7200L)), - (0.0, 7.5, 4.0, S(2.0, 7200L), S(2.0, 10800L), true, S(7200L)), - (9.0, 0.0, 4.0, S(2.0, 5400L), S(2.0, 10800L), true, S(5400L)), + (0.0, 0.0, 4.0, S(2.0, 7200L), S(2.0, 9000L), true, S(7200L)), + (5.0, 0.0, 4.0, N, S(4.0, 6300L), true, S(6300L)), + (0.0, 7.5, 4.0, S(4.0, 5400L), N, true, S(5400L)), + (9.0, 0.0, 4.0, N, S(4.0, 6300L), true, S(6300L)), (5.0, 14.0, 4.0, S(2.0, 7200L), S(2.0, 5400L), false, S(5400L)), (9.0, 14.0, 4.0, S(2.0, 5400L), S(2.0, 5400L), false, S(5400L)), (10.0, 14.0, 4.0, N, S(4.0, 4500L), false, S(4500L)), (6.0, 15.0, 4.0, S(4.0, 7200L), N, false, S(7200L)), /* setPower is set to > (ev2 * 2) (charging) */ - (0.0, 0.0, 13.0, S(8.0, 7200L), S(5.0, 10800L), true, S(7200L)), - (7.0, 0.0, 11.0, S(6.0, 5400L), S(5.0, 10800L), true, S(5400L)), - (0.0, 5.0, 15.0, S(10.0, 7200L), S(5.0, 10800L), true, S(7200L)), - (0.0, 12.5, 15.0, S(10.0, 7200L), S(5.0, 5400L), true, S(5400L)), - (0.0, 0.0, 15.0, S(10.0, 7200L), S(5.0, 10800L), true, S(7200L)), + (0.0, 0.0, 13.0, S(8.0, 4500L), S(5.0, 5760L), true, S(4500L)), + (7.0, 0.0, 11.0, S(6.0, 5400L), S(5.0, 5760L), true, S(5400L)), + (0.0, 5.0, 15.0, S(10.0, 4320L), S(5.0, 10800L), true, S(4320L)), + (0.0, 12.5, 15.0, S(10.0, 4320L), S(5.0, 5400L), true, S(4320L)), + (0.0, 0.0, 15.0, S(10.0, 4320L), S(5.0, 5760L), true, S(4320L)), (5.0, 7.5, 15.0, S(10.0, 5400L), S(5.0, 9000L), false, S(5400L)), /* setPower is negative (discharging) */