Skip to content

Commit

Permalink
Merge branch 'sp/#000-em-combined' into sp/#339-em-it
Browse files Browse the repository at this point in the history
  • Loading branch information
sebastian-peter committed Nov 4, 2022
2 parents 7cdacba + 41b203e commit e156655
Show file tree
Hide file tree
Showing 2 changed files with 110 additions and 18 deletions.
72 changes: 54 additions & 18 deletions src/main/scala/edu/ie3/simona/model/participant/StorageModel.scala
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,31 @@ final case class StorageModel(
cosPhiRated
) {

private val lowestEnergy = eStorage.multiply(dod).asType(classOf[Energy])
private val minEnergy = eStorage
.multiply(dod)
.asType(classOf[Energy])
.to(PowerSystemUnits.KILOWATTHOUR)

/** In order to avoid faulty flexibility options, we want to avoid offering
* charging/discharging that could last less than one second.
*/
private val toleranceMargin = pMax
.multiply(1d.asSecond)
.asType(classOf[Energy])
.to(PowerSystemUnits.KILOWATTHOUR)

/** Minimal allowed energy with tolerance margin added
*/
private val minEnergyWithMargin = minEnergy
.add(toleranceMargin.divide(eta).asType(classOf[Energy]))
.to(PowerSystemUnits.KILOWATTHOUR)

/** Maximum allowed energy with tolerance margin added
*/
private val maxEnergyWithMargin =
eStorage
.subtract(toleranceMargin.multiply(eta).asType(classOf[Energy]))
.to(PowerSystemUnits.KILOWATTHOUR)

override protected def calculateActivePower(
data: StorageRelevantData
Expand Down Expand Up @@ -94,18 +111,30 @@ final case class StorageModel(
determineCurrentState(lastState, data.currentTick)

// net power after considering efficiency
val netPower = {
val proposal = setPower
.multiply(eta)
.asType(classOf[Power])
.to(PowerSystemUnits.KILOWATT)

// if it's close to zero, set it to zero
if (QuantityUtil.isEquivalentAbs(zeroKW, proposal, 1e-9))
val netPower =
if (QuantityUtil.isEquivalentAbs(zeroKW, setPower, 1e-9)) {
// if power is close to zero, set it to zero
zeroKW
else
proposal
}
} else if (setPower.isGreaterThan(zeroKW)) {
if (isFull(currentStoredEnergy))
zeroKW // do not keep charging if we're already full
else
// multiply eta if we're charging
setPower
.multiply(eta)
.asType(classOf[Power])
.to(PowerSystemUnits.KILOWATT)
} else {
if (isEmpty(currentStoredEnergy))
zeroKW // do not keep discharging if we're already empty
else
// divide eta if we're discharging
// (draining the battery more than we get as output)
setPower
.multiply(eta) // FIXME this should be division
.asType(classOf[Power])
.to(PowerSystemUnits.KILOWATT)
}

val currentState =
StorageState(currentStoredEnergy, netPower, data.currentTick)
Expand All @@ -116,8 +145,16 @@ final case class StorageModel(
isEmpty(currentStoredEnergy) || isFull(currentStoredEnergy)
val isChargingOrDischarging =
!QuantityUtil.isEquivalentAbs(zeroKW, netPower, 0)
// if we've been triggered just before we hit the minimum or maximum energy,
// and we're still discharging or charging respectively (happens in edge cases),
// we already set netPower to zero (see above) and also want to refresh flex options
// at the next activation
val hasObsoleteFlexOptions =
(isFull(currentStoredEnergy) && setPower.isGreaterThan(zeroKW)) ||
(isEmpty(currentStoredEnergy) && setPower.isLessThan(zeroKW))

val activateAtNextTick = isEmptyOrFull && isChargingOrDischarging
val activateAtNextTick =
(isEmptyOrFull && isChargingOrDischarging) || hasObsoleteFlexOptions

// calculate the time span until we're full or empty, if applicable
val maybeTimeSpan =
Expand All @@ -130,14 +167,14 @@ final case class StorageModel(
Some(energyToFull.divide(netPower).asType(classOf[Time]))
} else {
// we're discharging, calculate time until we're at lowest energy allowed
val energyToEmpty = currentStoredEnergy.subtract(lowestEnergy)
val energyToEmpty = currentStoredEnergy.subtract(minEnergy)
Some(energyToEmpty.divide(netPower.multiply(-1)).asType(classOf[Time]))
}

// calculate the tick from time span
val maybeNextTick = maybeTimeSpan.map { timeSpan =>
val ticksToEmpty =
Math.round(timeSpan.to(Units.SECOND).getValue.doubleValue())
Math.round(timeSpan.to(Units.SECOND).getValue.doubleValue)
data.currentTick + ticksToEmpty
}

Expand Down Expand Up @@ -167,7 +204,7 @@ final case class StorageModel(
* energy allowed (minus a tolerance margin)
*/
private def isFull(storedEnergy: ComparableQuantity[Energy]): Boolean =
storedEnergy.isGreaterThanOrEqualTo(eStorage.subtract(toleranceMargin))
storedEnergy.isGreaterThanOrEqualTo(maxEnergyWithMargin)

/** @param storedEnergy
* the stored energy amount to check
Expand All @@ -176,8 +213,7 @@ final case class StorageModel(
* allowed (plus a tolerance margin)
*/
private def isEmpty(storedEnergy: ComparableQuantity[Energy]): Boolean =
storedEnergy.isLessThanOrEqualTo(lowestEnergy.add(toleranceMargin))

storedEnergy.isLessThanOrEqualTo(minEnergyWithMargin)
}

object StorageModel {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,4 +167,60 @@ class StorageModelTest extends Specification {
100 | -10 || -9 | true | true | 8*3600/0.9
}

def "Handle the edge case of charging in tolerance margins"() {
given:
def storageModel = buildStorageModel()
def startTick = 1800L
def data = new StorageModel.StorageRelevantData(startTick + 1)
// margin is at ~ 20.0030864 kWh
def oldState = new StorageModel.StorageState(
getQuantity(20.001d, KILOWATTHOUR),
getQuantity(0d, KILOWATT),
startTick
)

when:
def result = storageModel.handleControlledPowerChange(
data,
oldState,
getQuantity(-5d, KILOWATT)
)

then:
equals(result._1.chargingPower(), getQuantity(0d, KILOWATT), TOLERANCE)
result._1.tick() == startTick + 1
equals(result._1.storedEnergy(), oldState.storedEnergy(), TOLERANCE)
def flexChangeIndication = result._2
!flexChangeIndication.changesAtTick().defined
flexChangeIndication.changesAtNextActivation()
}

def "Handle the edge case of discharging in tolerance margins"() {
given:
def storageModel = buildStorageModel()
def startTick = 1800L
def data = new StorageModel.StorageRelevantData(startTick + 1)
// margin is at ~ 99.9975 kWh
def oldState = new StorageModel.StorageState(
getQuantity(99.9990, KILOWATTHOUR),
getQuantity(0d, KILOWATT),
startTick
)

when:
def result = storageModel.handleControlledPowerChange(
data,
oldState,
getQuantity(9d, KILOWATT)
)

then:
equals(result._1.chargingPower(), getQuantity(0d, KILOWATT), TOLERANCE)
result._1.tick() == startTick + 1
equals(result._1.storedEnergy(), oldState.storedEnergy(), TOLERANCE)
def flexChangeIndication = result._2
!flexChangeIndication.changesAtTick().defined
flexChangeIndication.changesAtNextActivation()
}

}

0 comments on commit e156655

Please sign in to comment.