Skip to content

Commit

Permalink
Merge branch 'sp/#332-evcs-flex' into sp/#000-em-combined
Browse files Browse the repository at this point in the history
  • Loading branch information
sebastian-peter committed Nov 7, 2022
2 parents d2645ce + a83a9bf commit ec5aff0
Show file tree
Hide file tree
Showing 8 changed files with 367 additions and 209 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
}
Expand Down
211 changes: 159 additions & 52 deletions src/main/scala/edu/ie3/simona/model/participant/evcs/EvcsModel.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -388,7 +388,7 @@ final case class EvcsModel(

// update EV
val newEvStoredEnergy = ev.getStoredEnergy.add(
chargedEnergyInScheduleSlice(entry)
chargedEnergyInScheduleEntry(entry)
)
val newEv = ev.copyWith(newEvStoredEnergy)

Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 =
Expand All @@ -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
)
}
Expand Down Expand Up @@ -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

(
Expand All @@ -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)

Expand All @@ -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

Expand All @@ -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)
}

}
Expand All @@ -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))
Expand Down Expand Up @@ -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))
Expand All @@ -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)
Expand Down
Loading

0 comments on commit ec5aff0

Please sign in to comment.