From e68be97135d2ed414116e34ea4d73bc86a8f477d Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Wed, 28 Aug 2024 18:41:30 +0200 Subject: [PATCH 01/77] Initial commit --- .../main/participants/ParticipantStates.puml | 35 + .../agent/participant2/ParticipantAgent.scala | 131 +++ .../participant2/ParticipantAgentInit.scala | 197 ++++ .../participant2/ParticipantDataCore.scala | 51 ++ .../edu/ie3/simona/api/ExtSimAdapter.scala | 2 +- .../participant2/ParticipantFlexibility.scala | 66 ++ .../model/participant2/ParticipantModel.scala | 85 ++ .../participant2/ParticipantModelInit.scala | 26 + .../participant2/ParticipantModelShell.scala | 34 + .../simona/model/participant2/PvModel.scala | 845 ++++++++++++++++++ .../messages/services/ServiceMessage.scala | 9 +- .../edu/ie3/simona/service/ServiceType.scala | 18 + .../ie3/simona/service/SimonaService.scala | 10 +- .../ie3/simona/api/ExtSimAdapterSpec.scala | 2 +- 14 files changed, 1501 insertions(+), 10 deletions(-) create mode 100644 docs/uml/main/participants/ParticipantStates.puml create mode 100644 src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgent.scala create mode 100644 src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgentInit.scala create mode 100644 src/main/scala/edu/ie3/simona/agent/participant2/ParticipantDataCore.scala create mode 100644 src/main/scala/edu/ie3/simona/model/participant2/ParticipantFlexibility.scala create mode 100644 src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala create mode 100644 src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelInit.scala create mode 100644 src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala create mode 100644 src/main/scala/edu/ie3/simona/model/participant2/PvModel.scala create mode 100644 src/main/scala/edu/ie3/simona/service/ServiceType.scala diff --git a/docs/uml/main/participants/ParticipantStates.puml b/docs/uml/main/participants/ParticipantStates.puml new file mode 100644 index 0000000000..bd83f9fba6 --- /dev/null +++ b/docs/uml/main/participants/ParticipantStates.puml @@ -0,0 +1,35 @@ +@startuml +'https://plantuml.com/state-diagram + +' clock "Cock 0" as C0 with period 50 +skinparam componentStyle rectangle + +package "t0" { + component "State (t0)" as s0 + component "OperatingPoint (t0 - ?)" as op0 + component "OperationRelevantData (t0)" as or0 + +} + +package t1 { + component "State (t1)" as s1 +} + +package t2 { + component "State (t2)" as s2 +} + + +s0 -> op0 +or0 -> op0 + +s0 -> s1 +op0 -> s1 + +s1 -> s2 + +' Layouting: +s0 -[hidden]-> or0 +'States --> OperatingPoints + +@enduml \ No newline at end of file diff --git a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgent.scala b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgent.scala new file mode 100644 index 0000000000..7e49bc7a20 --- /dev/null +++ b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgent.scala @@ -0,0 +1,131 @@ +/* + * © 2024. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.agent.participant2 + +import edu.ie3.simona.agent.participant.data.Data.SecondaryData +import edu.ie3.simona.exceptions.CriticalFailureException +import edu.ie3.simona.model.participant2.ParticipantModel +import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage.{ + FlexRequest, + FlexResponse, + ProvideFlexOptions, +} +import edu.ie3.simona.ontology.messages.{Activation, SchedulerMessage} +import org.apache.pekko.actor.typed.scaladsl.Behaviors +import org.apache.pekko.actor.typed.{ActorRef, Behavior} +import org.apache.pekko.actor.{ActorRef => ClassicRef} + +object ParticipantAgent { + + trait Request + + /** Extended by all requests that activate an [[ParticipantAgent]], i.e. + * activations, flex requests and control messages + */ + private[participant2] sealed trait ActivationRequest extends Request { + val tick: Long + } + + /** Wrapper for an [[Activation]] for usage by an adapter. Activations can + * only be received if this agent is not EM-controlled. + * + * @param tick + * The tick to activate + */ + private[participant2] final case class ParticipantActivation( + override val tick: Long + ) extends ActivationRequest + + /** Wrapper for [[FlexRequest]] messages for usage by an adapter (if this + * [[ParticipantAgent]] is EM-controlled itself) + * + * @param msg + * The wrapped flex request + */ + private[participant2] final case class Flex(msg: FlexRequest) + extends ActivationRequest { + override val tick: Long = msg.tick + } + + sealed trait RegistrationResponseMessage extends Request { + val serviceRef: ClassicRef + } + + /** Message, that is used to confirm a successful registration + */ + final case class RegistrationSuccessfulMessage( + override val serviceRef: ClassicRef, + nextDataTick: Long, + ) extends RegistrationResponseMessage + + /** Message, that is used to announce a failed registration + */ + final case class RegistrationFailedMessage( + override val serviceRef: ClassicRef + ) extends RegistrationResponseMessage + + /** The existence of this data object indicates that the corresponding agent + * is not EM-controlled, but activated by a + * [[edu.ie3.simona.scheduler.Scheduler]] + * + * @param scheduler + * The scheduler that is activating this agent + * @param activationAdapter + * The activation adapter handling [[Activation]] messages + */ + final case class SchedulerData( + scheduler: ActorRef[SchedulerMessage], + activationAdapter: ActorRef[Activation], + ) + + /** The existence of this data object indicates that the corresponding agent + * is EM-controlled (by [[emAgent]]). + * + * @param emAgent + * The parent EmAgent that is controlling this agent. + * @param flexAdapter + * The flex adapter handling [[FlexRequest]] messages + * @param lastFlexOptions + * Last flex options that have been calculated for this agent. + */ + final case class FlexControlledData( + emAgent: ActorRef[FlexResponse], + flexAdapter: ActorRef[FlexRequest], + lastFlexOptions: Option[ProvideFlexOptions] = None, + ) + + def apply( + model: ParticipantModel[_, _, _], + dataCore: ParticipantDataCore, + parentData: Either[SchedulerData, FlexControlledData], + ): Behavior[Request] = + Behaviors.receivePartial { case (ctx, activation: ActivationRequest) => + // handle issueControl differently? + val updatedCore = dataCore.handleActivation(activation.tick) + + if (dataCore.isComplete) { + + val receivedData = dataCore.getData.map { + case data: SecondaryData => data + case other => + throw new CriticalFailureException( + s"Received unexpected data $other" + ) + } + val relevantData = + model.createRelevantData(receivedData, activation.tick) + model.calcState() + } + + ParticipantAgent(model, updatedCore, parentData) + } + + private def primaryData(): Behavior[Request] = Behaviors.receivePartial { + case _ => Behaviors.same + } + +} diff --git a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgentInit.scala b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgentInit.scala new file mode 100644 index 0000000000..09ef304479 --- /dev/null +++ b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgentInit.scala @@ -0,0 +1,197 @@ +/* + * © 2024. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.agent.participant2 + +import edu.ie3.datamodel.models.input.system.SystemParticipantInput +import edu.ie3.simona.agent.participant2.ParticipantAgent._ +import edu.ie3.simona.config.SimonaConfig.BaseRuntimeConfig +import edu.ie3.simona.exceptions.CriticalFailureException +import edu.ie3.simona.model.participant2.{ + ParticipantModel, + ParticipantModelInit, +} +import edu.ie3.simona.ontology.messages.{Activation, SchedulerMessage} +import edu.ie3.simona.ontology.messages.SchedulerMessage.{ + Completion, + ScheduleActivation, +} +import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage.{ + FlexCompletion, + FlexRequest, + FlexResponse, + ProvideFlexOptions, + RegisterParticipant, + ScheduleFlexRequest, +} +import edu.ie3.simona.ontology.messages.services.ServiceMessage.PrimaryServiceRegistrationMessage +import edu.ie3.simona.util.SimonaConstants.INIT_SIM_TICK +import org.apache.pekko.actor.typed.{ActorRef, Behavior} +import org.apache.pekko.actor.typed.scaladsl.Behaviors +import org.apache.pekko.actor.{ActorRef => ClassicRef} + +import java.time.ZonedDateTime + +object ParticipantAgentInit { + + def apply( + participantInput: SystemParticipantInput, + config: BaseRuntimeConfig, + primaryServiceProxy: ClassicRef, + simulationStartDate: ZonedDateTime, + simulationEndDate: ZonedDateTime, + parent: Either[ActorRef[SchedulerMessage], ActorRef[FlexResponse]], + ): Behavior[Request] = Behaviors.setup { ctx => + val parentData = parent + .map { parentEm => + val flexAdapter = ctx.messageAdapter[FlexRequest](Flex) + + parentEm ! RegisterParticipant( + participantInput.getUuid, + flexAdapter, + participantInput, + ) + + parentEm ! ScheduleFlexRequest( + participantInput.getUuid, + INIT_SIM_TICK, + ) + + FlexControlledData(parentEm, flexAdapter) + } + .left + .map { scheduler => + { + val activationAdapter = ctx.messageAdapter[Activation] { msg => + ParticipantActivation(msg.tick) + } + + scheduler ! ScheduleActivation( + activationAdapter, + INIT_SIM_TICK, + ) + + SchedulerData(scheduler, activationAdapter) + } + } + + uninitialized( + participantInput, + config, + primaryServiceProxy, + simulationStartDate, + simulationEndDate, + parentData, + ) + } + + private def uninitialized( + participantInput: SystemParticipantInput, + config: BaseRuntimeConfig, + primaryServiceProxy: ClassicRef, + simulationStartDate: ZonedDateTime, + simulationEndDate: ZonedDateTime, + parentData: Either[SchedulerData, FlexControlledData], + ): Behavior[Request] = Behaviors.receivePartial { + case (ctx, activation: ActivationRequest) + if activation.tick == INIT_SIM_TICK => + primaryServiceProxy ! PrimaryServiceRegistrationMessage( + participantInput.getUuid + ) + waitingForProxy( + participantInput, + config, + simulationStartDate, + simulationEndDate, + parentData, + ) + } + + private def waitingForProxy( + participantInput: SystemParticipantInput, + config: BaseRuntimeConfig, + simulationStartDate: ZonedDateTime, + simulationEndDate: ZonedDateTime, + parentData: Either[SchedulerData, FlexControlledData], + ): Behavior[Request] = Behaviors.receivePartial { + + case (_, RegistrationSuccessfulMessage(serviceRef, nextDataTick)) => + parentData.fold( + schedulerData => + schedulerData.scheduler ! Completion( + schedulerData.activationAdapter, + Some(nextDataTick), + ), + _.emAgent ! FlexCompletion( + participantInput.getUuid, + requestAtNextActivation = false, + Some(nextDataTick), + ), + ) + + primaryData() + + case (_, RegistrationFailedMessage(serviceRef)) => + val model = ParticipantModelInit.createModel( + participantInput, + config.scaling, + simulationStartDate, + simulationEndDate, + ) + val requiredServices = model.getRequiredServices.toSeq + if (requiredServices.isEmpty) { + ParticipantAgent(model) + } else { + waitingForServices(model) + } + } + + private def waitingForServices( + model: ParticipantModel[_, _, _], + expectedRegistrations: Set[ClassicRef], + expectedFirstData: Map[ClassicRef, Long], + parentData: Either[SchedulerData, FlexControlledData], + ): Behavior[Request] = + Behaviors.receivePartial { + case (_, RegistrationSuccessfulMessage(serviceRef, nextDataTick)) => + if (!expectedRegistrations.contains(serviceRef)) + throw new CriticalFailureException( + s"Registration response from $serviceRef was not expected!" + ) + + val newExpectedRegistrations = expectedRegistrations.excl(serviceRef) + val newExpectedFirstData = + expectedFirstData + (serviceRef, nextDataTick) + + if (newExpectedRegistrations.isEmpty) { + val earliestNextTick = expectedFirstData.map { case (_, nextTick) => + nextTick + }.minOption + + parentData.fold( + schedulerData => + schedulerData.scheduler ! Completion( + schedulerData.activationAdapter, + earliestNextTick, + ), + _.emAgent ! FlexCompletion( + model.uuid, + requestAtNextActivation = false, + earliestNextTick, + ), + ) + + ParticipantAgent(model, newExpectedFirstData, parentData) + } else + waitingForServices( + model, + newExpectedRegistrations, + newExpectedFirstData, + parentData, + ) + } + +} diff --git a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantDataCore.scala b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantDataCore.scala new file mode 100644 index 0000000000..a62a23b6fa --- /dev/null +++ b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantDataCore.scala @@ -0,0 +1,51 @@ +/* + * © 2024. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.agent.participant2 + +import org.apache.pekko.actor.{ActorRef => ClassicRef} +import edu.ie3.simona.agent.participant.data.Data +import edu.ie3.simona.ontology.messages.services.ServiceMessage.ProvisionMessage + +case class ParticipantDataCore( + expectedData: Map[ClassicRef, Long], + receivedData: Map[ClassicRef, Option[_ <: Data]], + activeTick: Option[Long], +) { + + // holds active tick and received data, + // knows what data is expected and can thus decide whether everything is complete + + // holds results as well? or no? + + def handleActivation(tick: Long): ParticipantDataCore = { + // TODO + this + } + + def handleDataProvision( + msg: ProvisionMessage[_ <: Data] + ): ParticipantDataCore = { + val updatedReceivedData = receivedData + (msg.serviceRef -> Some(msg.data)) + val updatedExpectedData = msg.nextDataTick + .map { nextTick => + expectedData + (msg.serviceRef -> nextTick) + } + .getOrElse { + expectedData - msg.serviceRef + } + + copy(expectedData = updatedExpectedData, receivedData = updatedReceivedData) + } + + def isComplete: Boolean = activeTick.nonEmpty && receivedData.forall { + case (_, data) => data.nonEmpty + } + + def getData: Seq[Data] = + receivedData.values.flatten.toSeq + +} diff --git a/src/main/scala/edu/ie3/simona/api/ExtSimAdapter.scala b/src/main/scala/edu/ie3/simona/api/ExtSimAdapter.scala index b71adf4a30..0093442a83 100644 --- a/src/main/scala/edu/ie3/simona/api/ExtSimAdapter.scala +++ b/src/main/scala/edu/ie3/simona/api/ExtSimAdapter.scala @@ -22,7 +22,7 @@ import edu.ie3.simona.ontology.messages.SchedulerMessage.{ Completion, ScheduleActivation, } -import edu.ie3.simona.ontology.messages.services.ServiceMessage.RegistrationResponseMessage.ScheduleServiceActivation +import edu.ie3.simona.ontology.messages.services.ServiceMessage.ScheduleServiceActivation import edu.ie3.simona.ontology.messages.Activation import edu.ie3.simona.scheduler.ScheduleLock import edu.ie3.simona.scheduler.ScheduleLock.ScheduleKey diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantFlexibility.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantFlexibility.scala new file mode 100644 index 0000000000..596be8b7a7 --- /dev/null +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantFlexibility.scala @@ -0,0 +1,66 @@ +/* + * © 2024. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.model.participant2 + +import edu.ie3.simona.model.participant2.ParticipantFlexibility.FlexChangeIndicator +import edu.ie3.simona.model.participant2.ParticipantModel.{ + ActivePowerOperatingPoint, + ModelState, + OperatingPoint, + OperationRelevantData, +} +import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage.ProvideFlexOptions +import edu.ie3.simona.ontology.messages.flex.MinMaxFlexibilityMessage.ProvideMinMaxFlexOptions +import edu.ie3.util.scala.quantities.DefaultQuantities +import squants.energy.Power + +trait ParticipantFlexibility[ + OP <: OperatingPoint, + S <: ModelState, + OR <: OperationRelevantData, +] { + + this: ParticipantModel[OP, S, OR] => + + def calcFlexOptions(state: S, relevantData: OR): ProvideFlexOptions + + def handlePowerControl( + flexOptions: ProvideFlexOptions, + setPower: Power, + ): (OP, FlexChangeIndicator) + +} + +object ParticipantFlexibility { + + final case class FlexChangeIndicator( + changesAtNextActivation: Boolean = false, + changesAtTick: Option[Long] = None, + ) + + trait ParticipantSimpleFlexibility[ + S <: ModelState, + OR <: OperationRelevantData, + ] extends ParticipantFlexibility[ActivePowerOperatingPoint, S, OR] { + this: ParticipantModel[ActivePowerOperatingPoint, S, OR] => + + def calcFlexOptions(state: S, relevantData: OR): ProvideFlexOptions = { + val (operatingPoint, _) = calcOperatingPoint(state, relevantData) + val power = operatingPoint.activePower + + ProvideMinMaxFlexOptions(uuid, power, power, DefaultQuantities.zeroKW) + } + + def handlePowerControl( + flexOptions: ProvideFlexOptions, + setPower: Power, + ): (ActivePowerOperatingPoint, FlexChangeIndicator) = { + (ActivePowerOperatingPoint(setPower), FlexChangeIndicator()) + } + } + +} diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala new file mode 100644 index 0000000000..0b50700c5c --- /dev/null +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala @@ -0,0 +1,85 @@ +/* + * © 2024. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.model.participant2 + +import edu.ie3.datamodel.models.result.system.SystemParticipantResult +import edu.ie3.simona.agent.participant.data.Data.PrimaryData.ApparentPower +import edu.ie3.simona.agent.participant.data.Data.SecondaryData +import edu.ie3.simona.model.participant2.ParticipantModel.{ + ModelState, + OperatingPoint, + OperationRelevantData, + ResultsContainer, +} +import edu.ie3.simona.agent.participant2.ParticipantAgent +import edu.ie3.simona.service.ServiceType +import org.apache.pekko.actor.typed.javadsl.ActorContext +import squants.energy.Power + +import java.time.ZonedDateTime +import java.util.UUID + +abstract class ParticipantModel[ + OP <: OperatingPoint, + S <: ModelState, + OR <: OperationRelevantData, +](val uuid: UUID) { + + def calcOperatingPoint(state: S, relevantData: OR): (OP, Option[Long]) + + def calcState(lastState: S, operatingPoint: OP, currentTick: Long): S + + def calcResults( + state: S, + operatingPoint: OP, + complexPower: ApparentPower, + dateTime: ZonedDateTime, + ): ResultsContainer + + // todo split off the following to ParticipantModelMeta? + def getRequiredServices: Iterable[ServiceType] + + /** @param receivedData + * @throws CriticalFailureException + * if unexpected type of data was provided + * @return + */ + def createRelevantData(receivedData: Seq[SecondaryData], tick: Long): OR + + /** Handling requests that are not part of the standard participant protocol + * + * @param ctx + * The actor context that can be used to send replies + * @param msg + * The received request TODO create interface + * @return + * An updated state + */ + def handleRequest(ctx: ActorContext[ParticipantAgent.Request], msg: Any): S = + throw new NotImplementedError(s"Method not implemented by $getClass") + +} + +object ParticipantModel { + + trait OperationRelevantData + + trait OperatingPoint + + case class ActivePowerOperatingPoint(activePower: Power) + extends OperatingPoint + + trait ModelState + + case object ConstantState extends ModelState + + final case class ResultsContainer( + power: Power, + modelResults: Seq[SystemParticipantResult], + ) + +} diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelInit.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelInit.scala new file mode 100644 index 0000000000..689af3083b --- /dev/null +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelInit.scala @@ -0,0 +1,26 @@ +/* + * © 2024. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.model.participant2 + +import edu.ie3.datamodel.models.input.system.{PvInput, SystemParticipantInput} + +import java.time.ZonedDateTime + +object ParticipantModelInit { + + def createModel( + participantInput: SystemParticipantInput, + scalingFactor: Double, + simulationStartDate: ZonedDateTime, + simulationEndDate: ZonedDateTime, + ): ParticipantModel[_, _, _] = + participantInput match { + case pvInput: PvInput => + PvModel(pvInput, scalingFactor, simulationStartDate, simulationEndDate) + } + +} diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala new file mode 100644 index 0000000000..0dec87713a --- /dev/null +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala @@ -0,0 +1,34 @@ +/* + * © 2024. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.model.participant2 +import edu.ie3.simona.exceptions.CriticalFailureException +import edu.ie3.simona.model.participant2.ParticipantModel.OperationRelevantData + +import scala.reflect._ + +/** Takes care of: + * - activating/deactivating model + * - holding id information + */ +class ParticipantModelShell { + + def handleReceivedData[ + OR <: OperationRelevantData: ClassTag, + M <: ParticipantModel[_, _, OR], + ](receivedData: OperationRelevantData, model: M) = { + + receivedData match { + case _: M => + + case unexpected => + throw new CriticalFailureException( + s"Received unexpected operation relevant data $unexpected" + ) + } + + } +} diff --git a/src/main/scala/edu/ie3/simona/model/participant2/PvModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/PvModel.scala new file mode 100644 index 0000000000..a83d40d52b --- /dev/null +++ b/src/main/scala/edu/ie3/simona/model/participant2/PvModel.scala @@ -0,0 +1,845 @@ +/* + * © 2020. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.model.participant2 + +import com.typesafe.scalalogging.LazyLogging +import edu.ie3.datamodel.models.input.system.PvInput +import edu.ie3.datamodel.models.result.system.PvResult +import edu.ie3.simona.agent.participant.data.Data.PrimaryData.ApparentPower +import edu.ie3.simona.agent.participant.data.Data.SecondaryData +import edu.ie3.simona.exceptions.CriticalFailureException +import edu.ie3.simona.model.SystemComponent +import edu.ie3.simona.model.participant.control.QControl +import edu.ie3.simona.model.participant2.ParticipantFlexibility.ParticipantSimpleFlexibility +import edu.ie3.simona.model.participant2.ParticipantModel.{ + ActivePowerOperatingPoint, + ConstantState, + OperationRelevantData, + ResultsContainer, +} +import edu.ie3.simona.model.participant2.PvModel.PvRelevantData +import edu.ie3.simona.ontology.messages.services.WeatherMessage.WeatherData +import edu.ie3.simona.service.ServiceType +import edu.ie3.util.quantities.PowerSystemUnits +import edu.ie3.util.quantities.QuantityUtils.RichQuantityDouble +import edu.ie3.util.scala.OperationInterval +import edu.ie3.util.scala.quantities._ +import squants._ +import squants.energy.Kilowatts +import squants.space.{Degrees, SquareMeters} +import squants.time.Minutes +import tech.units.indriya.unit.Units._ + +import java.time.ZonedDateTime +import java.util.UUID +import java.util.stream.IntStream +import scala.math._ + +final class PvModel private ( + uuid: UUID, + sRated: Power, + cosPhiRated: Double, + private val lat: Angle, + private val lon: Angle, + private val albedo: Double, + private val etaConv: Dimensionless, + private val alphaE: Angle, + private val gammaE: Angle, + private val moduleSurface: Area = SquareMeters(1d), +) extends ParticipantModel[ + ActivePowerOperatingPoint, + ConstantState.type, + PvRelevantData, + ](uuid) + with ParticipantSimpleFlexibility[ConstantState.type, PvRelevantData] + with LazyLogging { + + /** 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 + */ + val sMax: Power = sRated * 1.1 + + /** Permissible maximum active power feed in (therefore negative) */ + protected val pMax: Power = sMax * cosPhiRated * -1d + + /** Reference yield at standard testing conditions (STC) */ + private val yieldSTC = WattsPerSquareMeter(1000d) + + private val activationThreshold = sRated * cosPhiRated * 0.001 * -1d + + /** Calculate the active power behaviour of the model + * + * @param data + * Further needed, secondary data + * @return + * Active power + */ + override def calcOperatingPoint( + modelState: ConstantState.type, + data: PvRelevantData, + ): (ActivePowerOperatingPoint, Option[Long]) = { + // === 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 === // + val angleJ = calcAngleJ(data.dateTime) + val delta = calcSunDeclinationDelta(angleJ) + + val omega = calcHourAngleOmega(data.dateTime, angleJ, lon) + + val omegaSS = calcSunsetAngleOmegaSS(lat, delta) + val omegaSR = calcSunriseAngleOmegaSR(omegaSS) + + val alphaS = calcSolarAltitudeAngleAlphaS(omega, delta, lat) + val thetaG = + calcAngleOfIncidenceThetaG(delta, lat, gammaE, alphaE, omega) + + val omegas = calculateBeamOmegas(thetaG, omega, omegaSS, omegaSR) + + // === Beam Radiation ===// + val eBeamS = calcBeamRadiationOnSlopedSurface( + eBeamH, + omegas, + delta, + lat, + gammaE, + alphaE, + ) + + // === Diffuse Radiation Parameters ===// + val thetaZ = calcZenithAngleThetaZ(alphaS) + val airMass = calcAirMass(thetaZ) + val extraterrestrialRadiationI0 = calcExtraterrestrialRadiationI0(angleJ) + + // === Diffuse Radiation ===// + val eDifS = calcDiffuseRadiationOnSlopedSurfacePerez( + eDifH, + eBeamH, + airMass, + extraterrestrialRadiationI0, + thetaZ, + thetaG, + gammaE, + ) + + // === Reflected Radiation ===// + val eRefS = + calcReflectedRadiationOnSlopedSurface(eBeamH, eDifH, gammaE, albedo) + + // === Total Radiation ===// + val eTotal = eDifS + eBeamS + eRefS + + val irraditionSTC = yieldSTC * duration + val power = calcOutput( + eTotal, + data.dateTime, + irraditionSTC, + ) + + (ActivePowerOperatingPoint(power), None) + } + + /** Calculates the position of the earth in relation to the sun (day angle) + * for the provided time + * + * @param time + * the time + * @return + * day angle J + */ + def calcAngleJ(time: ZonedDateTime): Angle = { + val day = time.getDayOfYear // day of the year + val j = 2d * Math.PI * ((day - 1d) / 365) + Radians(j) + } + + /** Calculates the declination angle delta of the sun at solar noon (i.e., + * when the sun is on the local meridian) with respect to the plane of the + * equator. Formula taken from Spencer, J.W. "Fourier series representation + * of the position of the sun". Appl. Opt. 1971, 10, 2569–2571 + * + * @param angleJ + * day angle J + * @return + * declination angle + */ + def calcSunDeclinationDelta( + angleJ: Angle + ): Angle = { + val jInRad = angleJ.toRadians + Radians( + 0.006918 - + 0.399912 * cos(jInRad) + + 0.070257 * sin(jInRad) - + 0.006758 * cos(2d * jInRad) + + 0.000907 * sin(2d * jInRad) - + 0.002697 * cos(3d * jInRad) + + 0.00148 * sin(3d * jInRad) + ) + } + + /** Calculates the hour angle omega which represents the angular displacement + * of the sun east or west of the local meridian due to rotation of the earth + * on its axis at 15◦ per hour; morning negative, afternoon positive. + * + * @param time + * the requested time (which is transformed to solar time) + * @param angleJ + * day angle J + * @param longitude + * longitude of the position + * @return + * hour angle omega + */ + def calcHourAngleOmega( + time: ZonedDateTime, + angleJ: Angle, + longitude: Angle, + ): Angle = { + val jInRad = angleJ.toRadians + val lambda = longitude.toDegrees + val et = Minutes( + 0.0066 + 7.3525 * cos(jInRad + 1.4992378274631293) + 9.9359 * cos( + 2d * jInRad + 1.9006635554218247 + ) + 0.3387 * cos(3d * jInRad + 1.8360863730980346) + ) + + val lmt = Minutes(time.getHour * 60d + time.getMinute - 4d * (15d - lambda)) + val st = lmt + et + + Radians((st.toHours - 12).toRadians * 15d) + } + + /** Calculates the sunset hour angle omegaSS which represents the omega value + * when the sun sets. The sunrise hour angle omegaSR is the negative of + * omegaSS. + * + * @param latitude + * latitude of the position + * @param delta + * sun declination angle + * @return + * sunset angle omegaSS + */ + def calcSunsetAngleOmegaSS( + latitude: Angle, + delta: Angle, + ): Angle = { + val latInRad = latitude.toRadians + val deltaInRad = delta.toRadians + + Radians(acos(-tan(latInRad) * tan(deltaInRad))) + } + + /** Calculates the sunrise hour angle omegaSR given omegaSS. + */ + private val calcSunriseAngleOmegaSR = + (omegaSS: Angle) => omegaSS * (-1) + + /** Calculates the solar altitude angle alphaS which represents the angle + * between the horizontal and the line to the sun, that is, the complement of + * the zenith angle. + * + * @param omega + * hour angle + * @param delta + * sun declination angle + * @param latitude + * latitude of the position + * @return + * solar altitude angle alphaS + */ + def calcSolarAltitudeAngleAlphaS( + omega: Angle, + delta: Angle, + latitude: Angle, + ): Angle = { + val latInRad = latitude.toRadians + val deltaInRad = delta.toRadians + val omegaInRad = omega.toRadians + val sinAlphaS = + min( + max( + cos(omegaInRad) * cos(latInRad) * cos(deltaInRad) + + sin(latInRad) * sin(deltaInRad), + -1, + ), + 1, + ) + Radians(asin(sinAlphaS)) + } + + /** 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. + * + * @param alphaS + * sun altitude angle + * @return + * the zenith angle + */ + def calcZenithAngleThetaZ( + alphaS: Angle + ): Angle = { + val alphaSInRad = alphaS.toRadians + + // the zenith angle is defined as 90° - gammaS in Radian + Radians(Pi / 2 - abs(alphaSInRad)) + } + + /** 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 + * zenith (i.e., directly overhead). + * + * @param thetaZ + * zenith angle + * @return + * air mass + */ + def calcAirMass(thetaZ: Angle): Double = { + val thetaZInRad = thetaZ.toRadians + + // radius of the earth in kilometers + val re = 6371d + // approx. effective height of the atmosphere + val yAtm = 9d + + // Ratio re / yAtm between the earth radius and the atmosphere height + val airMassRatio = re / yAtm + sqrt( + pow(airMassRatio * cos(thetaZInRad), 2d) + 2d * airMassRatio + 1d + ) - airMassRatio * cos(thetaZInRad) + } + + /** Calculates the extraterrestrial radiation, that is, the radiation that + * would be received in the absence of the atmosphere. + * + * @param angleJ + * day angle J + * @return + * extraterrestrial radiation I0 + */ + def calcExtraterrestrialRadiationI0( + angleJ: Angle + ): Irradiation = { + val jInRad = angleJ.toRadians + + // eccentricity correction factor + val e0 = 1.000110 + + 0.034221 * cos(jInRad) + + 0.001280 * sin(jInRad) + + 0.000719 * cos(2d * jInRad) + + 0.000077 * sin(2d * jInRad) + + // solar constant in W/m2 + val Gsc = WattHoursPerSquareMeter(1367) // solar constant + Gsc * e0 + } + + /** Calculates the angle of incidence thetaG of beam radiation on a surface + * + * @param delta + * sun declination angle + * @param latitude + * latitude of the position + * @param gammaE + * 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) + * @param omega + * hour angle + * @return + * angle of incidence thetaG + */ + def calcAngleOfIncidenceThetaG( + delta: Angle, + latitude: Angle, + gammaE: Angle, + alphaE: Angle, + omega: Angle, + ): Angle = { + val deltaInRad = delta.toRadians + val omegaInRad = omega.toRadians + val gammaInRad = gammaE.toRadians + val alphaEInRad = alphaE.toRadians + val latInRad = latitude.toRadians + + Radians( + acos( + sin(deltaInRad) * sin(latInRad) * cos(gammaInRad) - + sin(deltaInRad) * cos(latInRad) * sin(gammaInRad) * cos(alphaEInRad) + + cos(deltaInRad) * cos(latInRad) * cos(gammaInRad) * cos(omegaInRad) + + cos(deltaInRad) * sin(latInRad) * sin(gammaInRad) * + cos(alphaEInRad) * cos(omegaInRad) + + cos(deltaInRad) * sin(gammaInRad) * sin(alphaEInRad) * sin(omegaInRad) + ) + ) + } + + /** Calculates omega1 and omega2, which are parameters for + * calcBeamRadiationOnSlopedSurface + * + * @param thetaG + * angle of incidence + * @param omega + * hour angle + * @param omegaSS + * sunset angle + * @param omegaSR + * sunrise angle + * @return + * omega1 and omega encapsulated in an Option, if applicable. None + * otherwise + */ + def calculateBeamOmegas( + thetaG: Angle, + omega: Angle, + omegaSS: Angle, + omegaSR: Angle, + ): Option[(Angle, Angle)] = { + val thetaGInRad = thetaG.toRadians + val omegaSSInRad = omegaSS.toRadians + val omegaSRInRad = omegaSR.toRadians + + val omegaOneHour = toRadians(15d) + val omegaHalfHour = omegaOneHour / 2d + + val omega1InRad = omega.toRadians // requested hour + 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 + if ( + thetaGInRad < toRadians(90) + // omega1 and omega2: sun has risen and has not set yet + && omega2InRad > omegaSRInRad + omegaHalfHour + && omega1InRad < omegaSSInRad - omegaHalfHour + ) { + + val (finalOmega1, finalOmega2) = + if (omega1InRad < omegaSRInRad) { + // requested time earlier than sunrise + (omegaSRInRad, omegaSRInRad + omegaOneHour) + } else if (omega2InRad > omegaSSInRad) { + // sunset earlier than requested time + (omegaSSInRad - omegaOneHour, omegaSSInRad) + } else { + (omega1InRad, omega2InRad) + } + + Some(Radians(finalOmega1), Radians(finalOmega2)) + } else + None + } + + /** Calculates the beam radiation on a sloped surface + * + * @param eBeamH + * beam radiation on a horizontal surface + * @param omegas + * omega1 and omega2 + * @param delta + * sun declination angle + * @param latitude + * latitude of the position + * @param gammaE + * 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) + * @return + * the beam radiation on the sloped surface + */ + def calcBeamRadiationOnSlopedSurface( + eBeamH: Irradiation, + omegas: Option[(Angle, Angle)], + delta: Angle, + latitude: Angle, + gammaE: Angle, + alphaE: Angle, + ): Irradiation = { + + omegas match { + case Some((omega1, omega2)) => + val deltaInRad = delta.toRadians + val gammaEInRad = gammaE.toRadians + val alphaEInRad = alphaE.toRadians + val latInRad = latitude.toRadians + + val omega1InRad = omega1.toRadians + val omega2InRad = omega2.toRadians + + val a = ((sin(deltaInRad) * sin(latInRad) * cos(gammaEInRad) + - sin(deltaInRad) * cos(latInRad) * sin(gammaEInRad) * cos( + alphaEInRad + )) + * (omega2InRad - omega1InRad) + + (cos(deltaInRad) * cos(latInRad) * cos(gammaEInRad) + + cos(deltaInRad) * sin(latInRad) * sin(gammaEInRad) * cos( + alphaEInRad + )) + * (sin(omega2InRad) - sin(omega1InRad)) + - (cos(deltaInRad) * sin(gammaEInRad) * sin(alphaEInRad)) + * (cos(omega2InRad) - cos(omega1InRad))) + + val b = ((cos(latInRad) * cos(deltaInRad)) * (sin(omega2InRad) - sin( + omega1InRad + )) + + (sin(latInRad) * sin(deltaInRad)) * (omega2InRad - omega1InRad)) + + // 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) + } + } + + /** Calculates the diffuse radiation 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 airMass + * the air mass + * @param extraterrestrialRadiationI0 + * extraterrestrial radiation + * @param thetaZ + * zenith angle + * @param thetaG + * angle of incidence + * @param gammaE + * slope angle (the angle between the plane of the surface in question and + * the horizontal) + * @return + * the diffuse radiation on the sloped surface + */ + def calcDiffuseRadiationOnSlopedSurfacePerez( + eDifH: Irradiation, + eBeamH: Irradiation, + airMass: Double, + extraterrestrialRadiationI0: Irradiation, + thetaZ: Angle, + thetaG: Angle, + gammaE: Angle, + ): Irradiation = { + val thetaZInRad = thetaZ.toRadians + val thetaGInRad = thetaG.toRadians + val gammaEInRad = gammaE.toRadians + + // == brightness index beta ==// + val beta = eDifH * airMass / extraterrestrialRadiationI0 + + // == cloud index epsilon ==// + // if we have no clouds, the epsilon bin is 8, as epsilon bin for an epsilon in [6.2, inf.[ = 8 + var x = 8 + + if (eDifH.value.doubleValue > 0) { + // if we have diffuse radiation on horizontal surface we have to check if we have another epsilon due to clouds get the epsilon + var epsilon = ((eDifH + eBeamH) / eDifH + + (5.535d * 1.0e-6) * pow( + thetaZInRad, + 3, + )) / (1d + (5.535d * 1.0e-6) * pow( + thetaZInRad, + 3, + )) + + // get the corresponding bin if epsilon is smaller than 6.2 + if (epsilon < 6.2) { // define the bins based on Perez + val discreteSkyClearnessCategories = Array( + Array(1, 1.065), + Array(1.065, 1.230), + Array(1.230, 1.500), + Array(1.500, 1.950), + Array(1.950, 2.800), + Array(2.800, 4.500), + Array(4.500, 6.200), + ) + // adapt the epsilon as we have no bin < 1 + epsilon = max(1, epsilon) + + // get the corresponding bin + val finalEpsilon = epsilon + + x = IntStream + .range(0, discreteSkyClearnessCategories.length) + .filter((i: Int) => + (finalEpsilon - discreteSkyClearnessCategories(i)( + 0 + ) >= 0) && (finalEpsilon - discreteSkyClearnessCategories( + i + )(1) < 0) + ) + .findFirst + .getAsInt + 1 + } + } + + // calculate the f_ij components based on the epsilon bin + val f11 = -0.0161 * pow(x, 3) + 0.1840 * pow(x, 2) - 0.3806 * x + 0.2324 + val f12 = 0.0134 * pow(x, 4) - 0.1938 * pow(x, 3) + 0.8410 * pow( + x, + 2, + ) - 1.4018 * x + 1.3579 + val f13 = 0.0032 * pow(x, 3) - 0.0280 * pow(x, 2) - 0.0056 * x - 0.0385 + val f21 = -0.0048 * pow(x, 3) + 0.0536 * pow(x, 2) - 0.1049 * x + 0.0034 + val f22 = 0.0012 * pow(x, 3) - 0.0067 * pow(x, 2) + 0.0091 * x - 0.0269 + val f23 = 0.0052 * pow(x, 3) - 0.0971 * pow(x, 2) + 0.2856 * x - 0.1389 + + // calculate circuumsolar brightness coefficient f1 and horizon brightness coefficient f2 + val f1 = max(0, f11 + f12 * beta + f13 * thetaZInRad) + val f2 = f21 + f22 * beta + f23 * thetaZInRad + val aPerez = max(0, cos(thetaGInRad)) + val bPerez = max(cos(1.4835298641951802), cos(thetaZInRad)) + + // finally calculate the diffuse radiation on an inclined surface + eDifH * ( + ((1 + cos( + gammaEInRad + )) / 2) * (1 - f1) + (f1 * (aPerez / bPerez)) + (f2 * sin( + gammaEInRad + )) + ) + } + + /** Calculates the reflected radiation on a sloped surface + * + * @param eBeamH + * beam radiation on a horizontal surface + * @param eDifH + * diffuse radiation on a horizontal surface + * @param gammaE + * slope angle (the angle between the plane of the surface in question and + * the horizontal) + * @param albedo + * albedo / "composite" ground reflection + * @return + * the reflected radiation on the sloped surface eRefS + */ + def calcReflectedRadiationOnSlopedSurface( + eBeamH: Irradiation, + eDifH: Irradiation, + gammaE: Angle, + albedo: Double, + ): Irradiation = { + val gammaEInRad = gammaE.toRadians + (eBeamH + eDifH) * (albedo * 0.5 * (1 - cos(gammaEInRad))) + } + + private def generatorCorrectionFactor( + time: ZonedDateTime, + gammaE: Angle, + ): Double = { + val gammaEValInDeg = gammaE.toDegrees + + val genCorr = new Array[Array[Double]](4) + genCorr(0) = Array(0.69, 0.73, 0.81, 0.83, 0.84, 0.84, 0.9, 0.84, 0.84, + 0.82, 0.75, 0.66) // 30° + genCorr(1) = Array(0.8, 0.83, 0.84, 0.85, 0.86, 0.86, 0.86, 0.86, 0.86, + 0.84, 0.82, 0.77) // 45° + genCorr(2) = Array(0.84, 0.85, 0.86, 0.86, 0.85, 0.85, 0.85, 0.85, 0.86, + 0.86, 0.85, 0.84) // 60° + genCorr(3) = Array(0.86, 0.86, 0.85, 0.84, 0.82, 0.81, 0.81, 0.82, 0.84, + 0.85, 0.86, 0.86) // 90° + + val genCorrKey: Int = gammaEValInDeg match { + case gamma if gamma < 38 => 0 + case gamma if gamma < 53 => 1 + case gamma if gamma < 75 => 2 + case _ => 3 + } + + genCorr(genCorrKey)(time.getMonth.getValue - 1) + } + + private def temperatureCorrectionFactor(time: ZonedDateTime): Double = { + val tempCorr = + Array(1.06, 1.04, 1.01, 0.98, 0.94, 0.93, 0.94, 0.93, 0.96, 1, 1.04, 1.06) + + tempCorr(time.getMonth.getValue - 1) + } + + private def calcOutput( + eTotalInWhPerSM: Irradiation, + time: ZonedDateTime, + irradiationSTC: Irradiation, + ): Power = { + val genCorr = generatorCorrectionFactor(time, gammaE) + val tempCorr = temperatureCorrectionFactor(time) + /* The actual yield of this sum of available panels. As the solar irradiance summed up over the total panel surface + * 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) + + /* Calculate the foreseen active power output without boundary condition adaptions */ + val proposal = sRated * (-1) * ( + actYield / irradiationSTC + ) * cosPhiRated + + /* Do sanity check, if the proposed feed in is above the estimated maximum to be apparent active power of the plant */ + if (proposal < pMax) + logger.warn( + "The fed in active power is higher than the estimated maximum active power of this plant ({} < {}). " + + "Did you provide wrong weather input data?", + proposal, + pMax, + ) + + /* If the output is marginally small, suppress the output, as we are likely to be in night and then only produce incorrect output */ + if (proposal.compareTo(activationThreshold) > 0) + DefaultQuantities.zeroMW + else proposal + } + + override def calcState( + lastState: ParticipantModel.ConstantState.type, + operatingPoint: ActivePowerOperatingPoint, + currentTick: Long, + ): ParticipantModel.ConstantState.type = ConstantState + + override def calcResults( + state: ParticipantModel.ConstantState.type, + operatingPoint: ActivePowerOperatingPoint, + complexPower: ApparentPower, + dateTime: ZonedDateTime, + ): ResultsContainer = { + ResultsContainer( + operatingPoint.activePower, + Seq( + new PvResult( + dateTime, + uuid, + complexPower.p.toMegawatts.asMegaWatt, + complexPower.q.toMegavars.asMegaVar, + ) + ), + ) + } + + def getRequiredServices: Iterable[ServiceType] = + Iterable(ServiceType.WeatherService) + + def createRelevantData( + receivedData: Seq[SecondaryData], + tick: Long, + ): PvRelevantData = { + receivedData + .collectFirst { case weatherData: WeatherData => + PvRelevantData(weatherData.diffIrr, weatherData.dirIrr) + } + .getOrElse { + throw new CriticalFailureException( + s"Expected WeatherData, got $receivedData" + ) + } + } + +} + +object PvModel { + + /** 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 + * @param diffIrradiance + * diffuse solar irradiance + * @param dirIrradiance + * direct solar irradiance + */ + final case class PvRelevantData( + dateTime: ZonedDateTime, + weatherDataFrameLength: Long, + diffIrradiance: Irradiance, + dirIrradiance: Irradiance, + ) extends OperationRelevantData + + def apply( + inputModel: PvInput, + scalingFactor: Double, + simulationStartDate: ZonedDateTime, + simulationEndDate: ZonedDateTime, + ): PvModel = { + + val scaledInput = inputModel.copy().scale(scalingFactor).build() + + /* Determine the operation interval */ + val operationInterval: OperationInterval = + SystemComponent.determineOperationInterval( + simulationStartDate, + simulationEndDate, + scaledInput.getOperationTime, + ) + + // moduleSurface and yieldSTC are left out for now + val model = apply( + scaledInput.getUuid, + scaledInput.getId, + operationInterval, + QControl(scaledInput.getqCharacteristics), + Kilowatts( + scaledInput.getsRated + .to(PowerSystemUnits.KILOWATT) + .getValue + .doubleValue + ), + scaledInput.getCosPhiRated, + Degrees(scaledInput.getNode.getGeoPosition.getY), + Degrees(scaledInput.getNode.getGeoPosition.getX), + scaledInput.getAlbedo, + Each( + scaledInput.getEtaConv + .to(PowerSystemUnits.PU) + .getValue + .doubleValue + ), + Radians( + scaledInput.getAzimuth + .to(RADIAN) + .getValue + .doubleValue + ), + Radians( + scaledInput.getElevationAngle + .to(RADIAN) + .getValue + .doubleValue + ), + ) + + model.enable() + + model + } + +} diff --git a/src/main/scala/edu/ie3/simona/ontology/messages/services/ServiceMessage.scala b/src/main/scala/edu/ie3/simona/ontology/messages/services/ServiceMessage.scala index d7444454fd..26d338ad61 100644 --- a/src/main/scala/edu/ie3/simona/ontology/messages/services/ServiceMessage.scala +++ b/src/main/scala/edu/ie3/simona/ontology/messages/services/ServiceMessage.scala @@ -60,12 +60,13 @@ object ServiceMessage { override val serviceRef: ActorRef ) extends RegistrationResponseMessage - final case class ScheduleServiceActivation( - tick: Long, - unlockKey: ScheduleKey, - ) } + final case class ScheduleServiceActivation( + tick: Long, + unlockKey: ScheduleKey, + ) + /** Actual provision of data * * @tparam D diff --git a/src/main/scala/edu/ie3/simona/service/ServiceType.scala b/src/main/scala/edu/ie3/simona/service/ServiceType.scala new file mode 100644 index 0000000000..a1796cea89 --- /dev/null +++ b/src/main/scala/edu/ie3/simona/service/ServiceType.scala @@ -0,0 +1,18 @@ +/* + * © 2024. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.service + +sealed trait ServiceType + +object ServiceType { + + case object WeatherService extends ServiceType + + case object PriceService extends ServiceType + + case object EvMovementService extends ServiceType +} diff --git a/src/main/scala/edu/ie3/simona/service/SimonaService.scala b/src/main/scala/edu/ie3/simona/service/SimonaService.scala index 1b41b30400..b72fa2ca56 100644 --- a/src/main/scala/edu/ie3/simona/service/SimonaService.scala +++ b/src/main/scala/edu/ie3/simona/service/SimonaService.scala @@ -6,16 +6,16 @@ package edu.ie3.simona.service -import org.apache.pekko.actor.typed.scaladsl.adapter.ClassicActorRefOps -import org.apache.pekko.actor.{Actor, ActorContext, ActorRef, Stash} import edu.ie3.simona.logging.SimonaActorLogging import edu.ie3.simona.ontology.messages.Activation import edu.ie3.simona.ontology.messages.SchedulerMessage.{ Completion, ScheduleActivation, } -import edu.ie3.simona.ontology.messages.services.ServiceMessage.RegistrationResponseMessage.ScheduleServiceActivation -import edu.ie3.simona.ontology.messages.services.ServiceMessage.ServiceRegistrationMessage +import edu.ie3.simona.ontology.messages.services.ServiceMessage.{ + ScheduleServiceActivation, + ServiceRegistrationMessage, +} import edu.ie3.simona.scheduler.ScheduleLock.ScheduleKey import edu.ie3.simona.service.ServiceStateData.{ InitializeServiceStateData, @@ -23,6 +23,8 @@ import edu.ie3.simona.service.ServiceStateData.{ } import edu.ie3.simona.service.SimonaService.Create import edu.ie3.simona.util.SimonaConstants.INIT_SIM_TICK +import org.apache.pekko.actor.typed.scaladsl.adapter.ClassicActorRefOps +import org.apache.pekko.actor.{Actor, ActorContext, ActorRef, Stash} import scala.util.{Failure, Success, Try} diff --git a/src/test/scala/edu/ie3/simona/api/ExtSimAdapterSpec.scala b/src/test/scala/edu/ie3/simona/api/ExtSimAdapterSpec.scala index 0a4dab2d51..b1d5d34028 100644 --- a/src/test/scala/edu/ie3/simona/api/ExtSimAdapterSpec.scala +++ b/src/test/scala/edu/ie3/simona/api/ExtSimAdapterSpec.scala @@ -21,7 +21,7 @@ import edu.ie3.simona.ontology.messages.SchedulerMessage.{ Completion, ScheduleActivation, } -import edu.ie3.simona.ontology.messages.services.ServiceMessage.RegistrationResponseMessage.ScheduleServiceActivation +import edu.ie3.simona.ontology.messages.services.ServiceMessage.ScheduleServiceActivation import edu.ie3.simona.scheduler.ScheduleLock.ScheduleKey import edu.ie3.simona.test.common.{TestKitWithShutdown, TestSpawnerClassic} import edu.ie3.simona.util.SimonaConstants.INIT_SIM_TICK From 5397f2bfe1c0cb28237d988555d86a0a33730ae7 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Thu, 19 Sep 2024 10:18:17 +0200 Subject: [PATCH 02/77] Further dev Signed-off-by: Sebastian Peter --- .../agent/participant2/ParticipantAgent.scala | 31 ++++-- .../participant2/ParticipantDataCore.scala | 1 + .../participant2/ParticipantFlexibility.scala | 2 +- .../model/participant2/ParticipantModel.scala | 38 ++++--- .../participant2/ParticipantModelShell.scala | 98 ++++++++++++++++--- .../simona/model/participant2/PvModel.scala | 8 +- 6 files changed, 137 insertions(+), 41 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgent.scala b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgent.scala index 7e49bc7a20..5fc89a48b5 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgent.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgent.scala @@ -8,7 +8,7 @@ package edu.ie3.simona.agent.participant2 import edu.ie3.simona.agent.participant.data.Data.SecondaryData import edu.ie3.simona.exceptions.CriticalFailureException -import edu.ie3.simona.model.participant2.ParticipantModel +import edu.ie3.simona.model.participant2.ParticipantModelShell import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage.{ FlexRequest, FlexResponse, @@ -21,7 +21,7 @@ import org.apache.pekko.actor.{ActorRef => ClassicRef} object ParticipantAgent { - trait Request + sealed trait Request /** Extended by all requests that activate an [[ParticipantAgent]], i.e. * activations, flex requests and control messages @@ -98,8 +98,15 @@ object ParticipantAgent { lastFlexOptions: Option[ProvideFlexOptions] = None, ) + /** A request to the participant agent that is not covered by the standard + * ways of interacting with the agent + */ + trait ParticipantRequest extends Request { + val tick: Long + } + def apply( - model: ParticipantModel[_, _, _], + modelShell: ParticipantModelShell[_, _, _], dataCore: ParticipantDataCore, parentData: Either[SchedulerData, FlexControlledData], ): Behavior[Request] = @@ -113,15 +120,23 @@ object ParticipantAgent { case data: SecondaryData => data case other => throw new CriticalFailureException( - s"Received unexpected data $other" + s"Received unexpected data $other, should be secondary data" ) } - val relevantData = - model.createRelevantData(receivedData, activation.tick) - model.calcState() + + modelShell.determineRelevantData(receivedData, activation.tick) + + parentData match { + case Left(schedulerData) => + val updatedModel = + modelShell.determineOperatingPoint(activation.tick) + + case Right(flexData) => + } + } - ParticipantAgent(model, updatedCore, parentData) + ParticipantAgent(modelShell, updatedCore, parentData) } private def primaryData(): Behavior[Request] = Behaviors.receivePartial { diff --git a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantDataCore.scala b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantDataCore.scala index a62a23b6fa..0b5682e843 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantDataCore.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantDataCore.scala @@ -10,6 +10,7 @@ import org.apache.pekko.actor.{ActorRef => ClassicRef} import edu.ie3.simona.agent.participant.data.Data import edu.ie3.simona.ontology.messages.services.ServiceMessage.ProvisionMessage +/** todo rather call ParticipantInputHandler? */ case class ParticipantDataCore( expectedData: Map[ClassicRef, Long], receivedData: Map[ClassicRef, Option[_ <: Data]], diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantFlexibility.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantFlexibility.scala index 596be8b7a7..653a1b065d 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantFlexibility.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantFlexibility.scala @@ -49,7 +49,7 @@ object ParticipantFlexibility { this: ParticipantModel[ActivePowerOperatingPoint, S, OR] => def calcFlexOptions(state: S, relevantData: OR): ProvideFlexOptions = { - val (operatingPoint, _) = calcOperatingPoint(state, relevantData) + val (operatingPoint, _) = determineOperatingPoint(state, relevantData) val power = operatingPoint.activePower ProvideMinMaxFlexOptions(uuid, power, power, DefaultQuantities.zeroKW) diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala index 0b50700c5c..667f872d95 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala @@ -16,6 +16,7 @@ import edu.ie3.simona.model.participant2.ParticipantModel.{ ResultsContainer, } import edu.ie3.simona.agent.participant2.ParticipantAgent +import edu.ie3.simona.agent.participant2.ParticipantAgent.ParticipantRequest import edu.ie3.simona.service.ServiceType import org.apache.pekko.actor.typed.javadsl.ActorContext import squants.energy.Power @@ -27,14 +28,15 @@ abstract class ParticipantModel[ OP <: OperatingPoint, S <: ModelState, OR <: OperationRelevantData, -](val uuid: UUID) { +](val uuid: UUID) + extends ParticipantFlexibility[OP, S, OR] { - def calcOperatingPoint(state: S, relevantData: OR): (OP, Option[Long]) + def determineOperatingPoint(state: S, relevantData: OR): (OP, Option[Long]) - def calcState(lastState: S, operatingPoint: OP, currentTick: Long): S + def determineState(lastState: S, operatingPoint: OP, currentTick: Long): S - def calcResults( - state: S, + def createResults( + lastState: S, operatingPoint: OP, complexPower: ApparentPower, dateTime: ZonedDateTime, @@ -52,14 +54,20 @@ abstract class ParticipantModel[ /** Handling requests that are not part of the standard participant protocol * + * @param state + * The current state * @param ctx * The actor context that can be used to send replies * @param msg - * The received request TODO create interface + * The received request * @return - * An updated state + * An updated state, or the same state provided as parameter */ - def handleRequest(ctx: ActorContext[ParticipantAgent.Request], msg: Any): S = + def handleRequest( + state: S, + ctx: ActorContext[ParticipantAgent.Request], + msg: ParticipantRequest, + ): S = throw new NotImplementedError(s"Method not implemented by $getClass") } @@ -68,14 +76,20 @@ object ParticipantModel { trait OperationRelevantData - trait OperatingPoint + trait OperatingPoint { + val activePower: Power + } - case class ActivePowerOperatingPoint(activePower: Power) + case class ActivePowerOperatingPoint(override val activePower: Power) extends OperatingPoint - trait ModelState + trait ModelState { + val tick: Long + } - case object ConstantState extends ModelState + case object ConstantState extends ModelState { + override val tick = -1 // is there a better way? + } final case class ResultsContainer( power: Power, diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala index 0dec87713a..4d9867ce2c 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala @@ -5,30 +5,96 @@ */ package edu.ie3.simona.model.participant2 -import edu.ie3.simona.exceptions.CriticalFailureException -import edu.ie3.simona.model.participant2.ParticipantModel.OperationRelevantData - -import scala.reflect._ +import edu.ie3.simona.agent.em.FlexCorrespondenceStore.WithTime +import edu.ie3.simona.agent.participant.data.Data.PrimaryData.ApparentPower +import edu.ie3.simona.agent.participant.data.Data.SecondaryData +import edu.ie3.simona.agent.participant2.ParticipantAgent +import edu.ie3.simona.agent.participant2.ParticipantAgent.ParticipantRequest +import edu.ie3.simona.model.participant2.ParticipantModel.{ + ModelState, + OperatingPoint, + OperationRelevantData, +} +import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage.ProvideFlexOptions +import org.apache.pekko.actor.typed.javadsl.ActorContext /** Takes care of: * - activating/deactivating model * - holding id information + * - storing: + * - states (only current needed) + * - operating points (only current needed) + * - operation relevant data (only current needed) + * - flex options? (only current needed) + * - results? also needs to handle power request from grid */ -class ParticipantModelShell { +final case class ParticipantModelShell[ + OP <: OperatingPoint, + S <: ModelState, + OR <: OperationRelevantData, +]( + model: ParticipantModel[OP, S, OR] with ParticipantFlexibility[OP, S, OR], + state: S, + relevantData: OR, + operatingPoint: OP, + flexOptions: ProvideFlexOptions, +) { + + def determineRelevantData(receivedData: Seq[SecondaryData], tick: Long) = { + model.createRelevantData(receivedData, tick) + } + + private def determineCurrentState(currentTick: Long): S = { + if (state.tick < currentTick) + model.determineState(state, operatingPoint, currentTick) + else + state + } + + def determineOperatingPoint( + currentTick: Long + ): ParticipantModelShell[OP, S, OR] = { + val currentState = determineCurrentState(currentTick) + + val (newOperatingPoint, maybeNextTick) = + model.determineOperatingPoint(state, relevantData) + + val activePower = newOperatingPoint.activePower - def handleReceivedData[ - OR <: OperationRelevantData: ClassTag, - M <: ParticipantModel[_, _, OR], - ](receivedData: OperationRelevantData, model: M) = { + // todo where store the reactive power? + val reactivePower = ??? - receivedData match { - case _: M => + // todo store results here as well? Or separate module for avg power calculation? + val results = model.createResults( + state, + newOperatingPoint, + ApparentPower(activePower, reactivePower), + ???, + ) - case unexpected => - throw new CriticalFailureException( - s"Received unexpected operation relevant data $unexpected" - ) - } + copy(state = currentState, operatingPoint = newOperatingPoint) + } + + def calcFlexOptions(): ParticipantModelShell[OP, S, OR] = { + val flexOptions = model.calcFlexOptions(state, relevantData) + + copy(flexOptions = flexOptions) + } + + def handleFlexControl(): ParticipantModelShell[OP, S, OR] = { + // todo pretty similar to determineOperatingPoint + this } + + def handleRequest( + ctx: ActorContext[ParticipantAgent.Request], + msg: ParticipantRequest, + ): ParticipantModelShell[OP, S, OR] = { + val currentState = determineCurrentState(msg.tick) + val updatedState = model.handleRequest(currentState, ctx, msg) + + copy(state = updatedState) + } + } diff --git a/src/main/scala/edu/ie3/simona/model/participant2/PvModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/PvModel.scala index a83d40d52b..debaa8816b 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/PvModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/PvModel.scala @@ -78,7 +78,7 @@ final class PvModel private ( * @return * Active power */ - override def calcOperatingPoint( + override def determineOperatingPoint( modelState: ConstantState.type, data: PvRelevantData, ): (ActivePowerOperatingPoint, Option[Long]) = { @@ -718,14 +718,14 @@ final class PvModel private ( else proposal } - override def calcState( + override def determineState( lastState: ParticipantModel.ConstantState.type, operatingPoint: ActivePowerOperatingPoint, currentTick: Long, ): ParticipantModel.ConstantState.type = ConstantState - override def calcResults( - state: ParticipantModel.ConstantState.type, + override def createResults( + lastState: ParticipantModel.ConstantState.type, operatingPoint: ActivePowerOperatingPoint, complexPower: ApparentPower, dateTime: ZonedDateTime, From fb8848ce8d9ba31a49a4eb782607defb3f4d4be2 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Thu, 19 Sep 2024 21:42:49 +0200 Subject: [PATCH 03/77] Further dev Signed-off-by: Sebastian Peter --- .../agent/participant2/ParticipantAgent.scala | 112 +++++++++++++----- .../participant2/ParticipantFlexibility.scala | 13 +- .../model/participant2/ParticipantModel.scala | 12 +- .../participant2/ParticipantModelShell.scala | 85 ++++++++----- 4 files changed, 157 insertions(+), 65 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgent.scala b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgent.scala index 5fc89a48b5..0ce04c6939 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgent.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgent.scala @@ -9,12 +9,17 @@ package edu.ie3.simona.agent.participant2 import edu.ie3.simona.agent.participant.data.Data.SecondaryData import edu.ie3.simona.exceptions.CriticalFailureException import edu.ie3.simona.model.participant2.ParticipantModelShell +import edu.ie3.simona.ontology.messages.SchedulerMessage.Completion import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage.{ + FlexActivation, + FlexCompletion, FlexRequest, FlexResponse, + IssueFlexControl, ProvideFlexOptions, } import edu.ie3.simona.ontology.messages.{Activation, SchedulerMessage} +import edu.ie3.util.scala.Scope import org.apache.pekko.actor.typed.scaladsl.Behaviors import org.apache.pekko.actor.typed.{ActorRef, Behavior} import org.apache.pekko.actor.{ActorRef => ClassicRef} @@ -110,33 +115,86 @@ object ParticipantAgent { dataCore: ParticipantDataCore, parentData: Either[SchedulerData, FlexControlledData], ): Behavior[Request] = - Behaviors.receivePartial { case (ctx, activation: ActivationRequest) => - // handle issueControl differently? - val updatedCore = dataCore.handleActivation(activation.tick) - - if (dataCore.isComplete) { - - val receivedData = dataCore.getData.map { - case data: SecondaryData => data - case other => - throw new CriticalFailureException( - s"Received unexpected data $other, should be secondary data" - ) - } - - modelShell.determineRelevantData(receivedData, activation.tick) - - parentData match { - case Left(schedulerData) => - val updatedModel = - modelShell.determineOperatingPoint(activation.tick) - - case Right(flexData) => - } - - } - - ParticipantAgent(modelShell, updatedCore, parentData) + Behaviors.receivePartial { + case (ctx, request: ParticipantRequest) => + val updatedShell = modelShell.handleRequest(ctx, request) + + ParticipantAgent(updatedShell, dataCore, parentData) + + case (ctx, activation: ActivationRequest) => + // handle issueControl differently? + val updatedCore = dataCore.handleActivation(activation.tick) + + val updatedShell = if (dataCore.isComplete) { + + val receivedData = dataCore.getData.map { + case data: SecondaryData => data + case other => + throw new CriticalFailureException( + s"Received unexpected data $other, should be secondary data" + ) + } + + Scope(modelShell) + .map(_.updateRelevantData(receivedData, activation.tick)) + .map { shell => + activation match { + case ParticipantActivation(tick) => + val modelWithOP = shell.updateOperatingPoint(tick) + + // todo results + + parentData.fold( + schedulerData => + schedulerData.scheduler ! Completion( + schedulerData.activationAdapter, + modelWithOP.modelChange.changesAtTick, + ), + _ => + throw new CriticalFailureException( + "Received activation while controlled by EM" + ), + ) + modelWithOP + + case Flex(FlexActivation(tick)) => + val modelWithFlex = shell.updateFlexOptions(tick) + + parentData.fold( + _ => + throw new CriticalFailureException( + "Received flex activation while not controlled by EM" + ), + _.emAgent ! modelWithFlex.flexOptions, + ) + + modelWithFlex + + case Flex(flexControl: IssueFlexControl) => + val modelWithOP = shell.updateOperatingPoint(flexControl) + + // todo results + + parentData.fold( + _ => + throw new CriticalFailureException( + "Received issue flex control while not controlled by EM" + ), + _.emAgent ! FlexCompletion( + shell.model.uuid, + shell.modelChange.changesAtNextActivation, + shell.modelChange.changesAtTick, + ), + ) + + modelWithOP + } + } + .get + } else + modelShell + + ParticipantAgent(updatedShell, updatedCore, parentData) } private def primaryData(): Behavior[Request] = Behaviors.receivePartial { diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantFlexibility.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantFlexibility.scala index 653a1b065d..06cf160b4e 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantFlexibility.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantFlexibility.scala @@ -6,9 +6,9 @@ package edu.ie3.simona.model.participant2 -import edu.ie3.simona.model.participant2.ParticipantFlexibility.FlexChangeIndicator import edu.ie3.simona.model.participant2.ParticipantModel.{ ActivePowerOperatingPoint, + ModelChangeIndicator, ModelState, OperatingPoint, OperationRelevantData, @@ -31,17 +31,12 @@ trait ParticipantFlexibility[ def handlePowerControl( flexOptions: ProvideFlexOptions, setPower: Power, - ): (OP, FlexChangeIndicator) + ): (OP, ModelChangeIndicator) } object ParticipantFlexibility { - final case class FlexChangeIndicator( - changesAtNextActivation: Boolean = false, - changesAtTick: Option[Long] = None, - ) - trait ParticipantSimpleFlexibility[ S <: ModelState, OR <: OperationRelevantData, @@ -58,8 +53,8 @@ object ParticipantFlexibility { def handlePowerControl( flexOptions: ProvideFlexOptions, setPower: Power, - ): (ActivePowerOperatingPoint, FlexChangeIndicator) = { - (ActivePowerOperatingPoint(setPower), FlexChangeIndicator()) + ): (ActivePowerOperatingPoint, ModelChangeIndicator) = { + (ActivePowerOperatingPoint(setPower), ModelChangeIndicator()) } } diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala index 667f872d95..967378bb1e 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala @@ -18,7 +18,7 @@ import edu.ie3.simona.model.participant2.ParticipantModel.{ import edu.ie3.simona.agent.participant2.ParticipantAgent import edu.ie3.simona.agent.participant2.ParticipantAgent.ParticipantRequest import edu.ie3.simona.service.ServiceType -import org.apache.pekko.actor.typed.javadsl.ActorContext +import org.apache.pekko.actor.typed.scaladsl.ActorContext import squants.energy.Power import java.time.ZonedDateTime @@ -91,6 +91,16 @@ object ParticipantModel { override val tick = -1 // is there a better way? } + /** Indicates when either flex options change (when em-controlled) or the + * operating point must change (when not em-controlled). + * @param changesAtNextActivation + * @param changesAtTick + */ + final case class ModelChangeIndicator( + changesAtNextActivation: Boolean = false, + changesAtTick: Option[Long] = None, + ) + final case class ResultsContainer( power: Power, modelResults: Seq[SystemParticipantResult], diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala index 4d9867ce2c..8547b318c2 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala @@ -5,28 +5,32 @@ */ package edu.ie3.simona.model.participant2 -import edu.ie3.simona.agent.em.FlexCorrespondenceStore.WithTime import edu.ie3.simona.agent.participant.data.Data.PrimaryData.ApparentPower import edu.ie3.simona.agent.participant.data.Data.SecondaryData import edu.ie3.simona.agent.participant2.ParticipantAgent import edu.ie3.simona.agent.participant2.ParticipantAgent.ParticipantRequest +import edu.ie3.simona.model.em.EmTools import edu.ie3.simona.model.participant2.ParticipantModel.{ + ModelChangeIndicator, ModelState, OperatingPoint, OperationRelevantData, } -import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage.ProvideFlexOptions -import org.apache.pekko.actor.typed.javadsl.ActorContext +import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage.{ + IssueFlexControl, + ProvideFlexOptions, +} +import org.apache.pekko.actor.typed.scaladsl.ActorContext /** Takes care of: * - activating/deactivating model * - holding id information - * - storing: - * - states (only current needed) - * - operating points (only current needed) - * - operation relevant data (only current needed) - * - flex options? (only current needed) - * - results? also needs to handle power request from grid + * - storing: + * - states (only current needed) + * - operating points (only current needed) + * - operation relevant data (only current needed) + * - flex options? (only current needed) + * - results? also needs to handle power request from grid */ final case class ParticipantModelShell[ OP <: OperatingPoint, @@ -38,20 +42,19 @@ final case class ParticipantModelShell[ relevantData: OR, operatingPoint: OP, flexOptions: ProvideFlexOptions, + modelChange: ModelChangeIndicator, ) { - def determineRelevantData(receivedData: Seq[SecondaryData], tick: Long) = { - model.createRelevantData(receivedData, tick) - } + def updateRelevantData( + receivedData: Seq[SecondaryData], + tick: Long, + ): ParticipantModelShell[OP, S, OR] = { + val updatedRelevantData = model.createRelevantData(receivedData, tick) - private def determineCurrentState(currentTick: Long): S = { - if (state.tick < currentTick) - model.determineState(state, operatingPoint, currentTick) - else - state + copy(relevantData = updatedRelevantData) } - def determineOperatingPoint( + def updateOperatingPoint( currentTick: Long ): ParticipantModelShell[OP, S, OR] = { val currentState = determineCurrentState(currentTick) @@ -72,29 +75,55 @@ final case class ParticipantModelShell[ ???, ) - copy(state = currentState, operatingPoint = newOperatingPoint) + copy( + state = currentState, + operatingPoint = newOperatingPoint, + modelChange = ModelChangeIndicator(changesAtTick = maybeNextTick), + ) } - def calcFlexOptions(): ParticipantModelShell[OP, S, OR] = { - val flexOptions = model.calcFlexOptions(state, relevantData) + def updateFlexOptions(currentTick: Long): ParticipantModelShell[OP, S, OR] = { + val currentState = determineCurrentState(currentTick) + val flexOptions = model.calcFlexOptions(currentState, relevantData) - copy(flexOptions = flexOptions) + copy(state = currentState, flexOptions = flexOptions) } - def handleFlexControl(): ParticipantModelShell[OP, S, OR] = { - // todo pretty similar to determineOperatingPoint + def updateOperatingPoint( + flexControl: IssueFlexControl + ): ParticipantModelShell[OP, S, OR] = { + val currentState = determineCurrentState(flexControl.tick) + + val setPointActivePower = EmTools.determineFlexPower( + flexOptions, + flexControl, + ) + + val (newOperatingPoint, modelChange) = + model.handlePowerControl(flexOptions, setPointActivePower) - this + copy( + state = currentState, + operatingPoint = newOperatingPoint, + modelChange = modelChange, + ) } def handleRequest( ctx: ActorContext[ParticipantAgent.Request], - msg: ParticipantRequest, + request: ParticipantRequest, ): ParticipantModelShell[OP, S, OR] = { - val currentState = determineCurrentState(msg.tick) - val updatedState = model.handleRequest(currentState, ctx, msg) + val currentState = determineCurrentState(request.tick) + val updatedState = model.handleRequest(currentState, ctx, request) copy(state = updatedState) } + private def determineCurrentState(currentTick: Long): S = { + if (state.tick < currentTick) + model.determineState(state, operatingPoint, currentTick) + else + state + } + } From 2aaa970618e06caa76bbbd40b01e5f0b22deef40 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Thu, 26 Sep 2024 16:33:39 +0200 Subject: [PATCH 04/77] Further dev Signed-off-by: Sebastian Peter --- .../agent/participant2/ParticipantAgent.scala | 216 ++++++++++++------ .../participant2/ParticipantAgentInit.scala | 4 + .../participant2/ParticipantDataCore.scala | 14 +- .../participant2/ParticipantGridAdapter.scala | 145 ++++++++++++ .../model/participant2/ParticipantModel.scala | 30 ++- .../participant2/ParticipantModelShell.scala | 57 +++-- .../simona/model/participant2/PvModel.scala | 17 +- 7 files changed, 380 insertions(+), 103 deletions(-) create mode 100644 src/main/scala/edu/ie3/simona/agent/participant2/ParticipantGridAdapter.scala diff --git a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgent.scala b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgent.scala index 0ce04c6939..68cf162e41 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgent.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgent.scala @@ -7,6 +7,7 @@ package edu.ie3.simona.agent.participant2 import edu.ie3.simona.agent.participant.data.Data.SecondaryData +import edu.ie3.simona.agent.participant.data.Data import edu.ie3.simona.exceptions.CriticalFailureException import edu.ie3.simona.model.participant2.ParticipantModelShell import edu.ie3.simona.ontology.messages.SchedulerMessage.Completion @@ -18,11 +19,13 @@ import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage.{ IssueFlexControl, ProvideFlexOptions, } +import edu.ie3.simona.ontology.messages.services.ServiceMessage.ProvisionMessage import edu.ie3.simona.ontology.messages.{Activation, SchedulerMessage} import edu.ie3.util.scala.Scope import org.apache.pekko.actor.typed.scaladsl.Behaviors import org.apache.pekko.actor.typed.{ActorRef, Behavior} import org.apache.pekko.actor.{ActorRef => ClassicRef} +import squants.Dimensionless object ParticipantAgent { @@ -73,6 +76,22 @@ object ParticipantAgent { override val serviceRef: ClassicRef ) extends RegistrationResponseMessage + /** Request the power values for the requested tick from an AssetAgent and + * provide the latest nodal voltage + * + * @param currentTick + * The tick that power values are requested for + * @param eInPu + * Real part of the complex, dimensionless nodal voltage + * @param fInPu + * Imaginary part of the complex, dimensionless nodal voltage + */ + final case class RequestAssetPowerMessage( + currentTick: Long, + eInPu: Dimensionless, + fInPu: Dimensionless, + ) extends Request + /** The existence of this data object indicates that the corresponding agent * is not EM-controlled, but activated by a * [[edu.ie3.simona.scheduler.Scheduler]] @@ -113,90 +132,147 @@ object ParticipantAgent { def apply( modelShell: ParticipantModelShell[_, _, _], dataCore: ParticipantDataCore, + gridAdapter: ParticipantGridAdapter, parentData: Either[SchedulerData, FlexControlledData], ): Behavior[Request] = Behaviors.receivePartial { case (ctx, request: ParticipantRequest) => val updatedShell = modelShell.handleRequest(ctx, request) - ParticipantAgent(updatedShell, dataCore, parentData) + ParticipantAgent(updatedShell, dataCore, gridAdapter, parentData) case (ctx, activation: ActivationRequest) => - // handle issueControl differently? - val updatedCore = dataCore.handleActivation(activation.tick) + val coreWithActivation = dataCore.handleActivation(activation) - val updatedShell = if (dataCore.isComplete) { + val (updatedShell, updatedCore, updatedGridAdapter) = + maybeCalculate( + modelShell, + coreWithActivation, + gridAdapter, + parentData, + ) - val receivedData = dataCore.getData.map { - case data: SecondaryData => data - case other => - throw new CriticalFailureException( - s"Received unexpected data $other, should be secondary data" - ) - } + ParticipantAgent( + updatedShell, + updatedCore, + updatedGridAdapter, + parentData, + ) - Scope(modelShell) - .map(_.updateRelevantData(receivedData, activation.tick)) - .map { shell => - activation match { - case ParticipantActivation(tick) => - val modelWithOP = shell.updateOperatingPoint(tick) - - // todo results - - parentData.fold( - schedulerData => - schedulerData.scheduler ! Completion( - schedulerData.activationAdapter, - modelWithOP.modelChange.changesAtTick, - ), - _ => - throw new CriticalFailureException( - "Received activation while controlled by EM" - ), - ) - modelWithOP - - case Flex(FlexActivation(tick)) => - val modelWithFlex = shell.updateFlexOptions(tick) - - parentData.fold( - _ => - throw new CriticalFailureException( - "Received flex activation while not controlled by EM" - ), - _.emAgent ! modelWithFlex.flexOptions, - ) - - modelWithFlex - - case Flex(flexControl: IssueFlexControl) => - val modelWithOP = shell.updateOperatingPoint(flexControl) - - // todo results - - parentData.fold( - _ => - throw new CriticalFailureException( - "Received issue flex control while not controlled by EM" - ), - _.emAgent ! FlexCompletion( - shell.model.uuid, - shell.modelChange.changesAtNextActivation, - shell.modelChange.changesAtTick, - ), - ) - - modelWithOP - } - } - .get - } else - modelShell + case (ctx, msg: ProvisionMessage[Data]) => + val coreWithData = dataCore.handleDataProvision(msg) + + val (updatedShell, updatedCore, updatedGridAdapter) = + maybeCalculate(modelShell, coreWithData, gridAdapter, parentData) + + ParticipantAgent( + updatedShell, + updatedCore, + updatedGridAdapter, + parentData, + ) + + case (ctx, msg: RequestAssetPowerMessage) => + // activeToReactivePowerFunc - ParticipantAgent(updatedShell, updatedCore, parentData) + gridAdapter.updateAveragePower(msg.currentTick, ctx.log) + + Behaviors.same } + private def maybeCalculate( + modelShell: ParticipantModelShell[_, _, _], + dataCore: ParticipantDataCore, + gridAdapter: ParticipantGridAdapter, + parentData: Either[SchedulerData, FlexControlledData], + ): ( + ParticipantModelShell[_, _, _], + ParticipantDataCore, + ParticipantGridAdapter, + ) = { + if (dataCore.isComplete) { + + val activation = dataCore.activation.getOrElse( + throw new CriticalFailureException( + "Activation should be present when data collection is complete" + ) + ) + + val receivedData = dataCore.getData.map { + case data: SecondaryData => data + case other => + throw new CriticalFailureException( + s"Received unexpected data $other, should be secondary data" + ) + } + + val updatedShell = Scope(modelShell) + .map(_.updateRelevantData(receivedData, activation.tick)) + .map { shell => + activation match { + case ParticipantActivation(tick) => + val modelWithOP = shell.updateOperatingPoint(tick) + + if (!gridAdapter.isPowerRequestExpected(tick)) { + // we don't expect a power request that could change voltage, + // so we can go ahead and calculate results + val results = shell.determineResults(tick, ???) + + } + + parentData.fold( + schedulerData => + schedulerData.scheduler ! Completion( + schedulerData.activationAdapter, + modelWithOP.modelChange.changesAtTick, + ), + _ => + throw new CriticalFailureException( + "Received activation while controlled by EM" + ), + ) + modelWithOP + + case Flex(FlexActivation(tick)) => + val modelWithFlex = shell.updateFlexOptions(tick) + + parentData.fold( + _ => + throw new CriticalFailureException( + "Received flex activation while not controlled by EM" + ), + _.emAgent ! modelWithFlex.flexOptions, + ) + + modelWithFlex + + case Flex(flexControl: IssueFlexControl) => + val modelWithOP = shell.updateOperatingPoint(flexControl) + + // todo results + + parentData.fold( + _ => + throw new CriticalFailureException( + "Received issue flex control while not controlled by EM" + ), + _.emAgent ! FlexCompletion( + shell.model.uuid, + shell.modelChange.changesAtNextActivation, + shell.modelChange.changesAtTick, + ), + ) + + modelWithOP + } + } + .get + + (updatedShell, dataCore.completeActivity(), ???) + } else + (modelShell, dataCore, gridAdapter) + } + private def primaryData(): Behavior[Request] = Behaviors.receivePartial { case _ => Behaviors.same } diff --git a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgentInit.scala b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgentInit.scala index 09ef304479..b8baf8f96c 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgentInit.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgentInit.scala @@ -37,6 +37,10 @@ import java.time.ZonedDateTime object ParticipantAgentInit { + // todo also register with GridAgent, + // wait for reply and then create + // GridAdapter + def apply( participantInput: SystemParticipantInput, config: BaseRuntimeConfig, diff --git a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantDataCore.scala b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantDataCore.scala index 0b5682e843..b378616a36 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantDataCore.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantDataCore.scala @@ -8,13 +8,14 @@ package edu.ie3.simona.agent.participant2 import org.apache.pekko.actor.{ActorRef => ClassicRef} import edu.ie3.simona.agent.participant.data.Data +import edu.ie3.simona.agent.participant2.ParticipantAgent.ActivationRequest import edu.ie3.simona.ontology.messages.services.ServiceMessage.ProvisionMessage /** todo rather call ParticipantInputHandler? */ case class ParticipantDataCore( expectedData: Map[ClassicRef, Long], receivedData: Map[ClassicRef, Option[_ <: Data]], - activeTick: Option[Long], + activation: Option[ActivationRequest], ) { // holds active tick and received data, @@ -22,9 +23,12 @@ case class ParticipantDataCore( // holds results as well? or no? - def handleActivation(tick: Long): ParticipantDataCore = { - // TODO - this + def handleActivation(activation: ActivationRequest): ParticipantDataCore = { + copy(activation = Some(activation)) + } + + def completeActivity(): ParticipantDataCore = { + copy(activation = None) } def handleDataProvision( @@ -42,7 +46,7 @@ case class ParticipantDataCore( copy(expectedData = updatedExpectedData, receivedData = updatedReceivedData) } - def isComplete: Boolean = activeTick.nonEmpty && receivedData.forall { + def isComplete: Boolean = activation.nonEmpty && receivedData.forall { case (_, data) => data.nonEmpty } diff --git a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantGridAdapter.scala b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantGridAdapter.scala new file mode 100644 index 0000000000..04d159e7cc --- /dev/null +++ b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantGridAdapter.scala @@ -0,0 +1,145 @@ +/* + * © 2024. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.agent.participant2 + +import edu.ie3.simona.agent.em.FlexCorrespondenceStore.WithTime +import edu.ie3.simona.agent.grid.GridAgent +import edu.ie3.simona.agent.participant.data.Data.PrimaryData.ApparentPower +import edu.ie3.simona.agent.participant2.ParticipantGridAdapter.averageApparentPower +import edu.ie3.util.scala.quantities.DefaultQuantities.{zeroMVAr, zeroMW} +import edu.ie3.util.scala.quantities.{Megavars, QuantityUtil, ReactivePower} +import org.apache.pekko.actor.typed.ActorRef +import org.apache.pekko.event.LoggingAdapter +import org.slf4j.Logger +import squants.energy.Megawatts +import squants.{Dimensionless, Energy, Power} + +import scala.collection.immutable.SortedMap +import scala.util.{Failure, Success} + +/** Provides (average) power values to grid agent + * + * @param gridAgent + * @param expectedRequestTick + * Tick at which next power request is expected + * @param voltage + * @param lastRequestTick + * Tick of the last request + * @param tickToPower + * power values + * @param currentAvgPower + */ +final case class ParticipantGridAdapter( + gridAgent: ActorRef[GridAgent.Request], + expectedRequestTick: Long, + voltage: Dimensionless, + lastRequestTick: Long = 0, + tickToPower: SortedMap[Long, ApparentPower], + currentAvgPower: WithTime[ApparentPower], +) { + + def isPowerRequestExpected(currentTick: Long): Boolean = { + expectedRequestTick == currentTick + } + + def storePowerValue( + power: ApparentPower, + tick: Long, + ): ParticipantGridAdapter = + copy(tickToPower = tickToPower + (tick, power)) + // power of the current tick is irrelevant + + def updateAveragePower( + currentTick: Long, + log: Logger, + ): ParticipantGridAdapter = { + val averagePower = + averageApparentPower(tickToPower, lastRequestTick, currentTick, ???, log) + + // keep the last entry because we do not know + // if the next entry will necessarily be at the + // current tick + val lastTickAndPower = tickToPower.maxByOption { case (tick, _) => + tick + } + + copy( + currentAvgPower = WithTime(averagePower, currentTick), + tickToPower = SortedMap.from(lastTickAndPower), + lastRequestTick = currentTick, + ) + } + +} + +object ParticipantGridAdapter { + + /** Determine the average apparent power within the given tick window + * + * @param tickToPower + * Mapping from data tick to actual data + * @param windowStart + * First, included tick of the time window + * @param windowEnd + * Last, included tick of the time window + * @param activeToReactivePowerFuncOpt + * An Option on a function, that transfers the active into reactive power + * @return + * The averaged apparent power + */ + def averageApparentPower( + tickToPower: Map[Long, ApparentPower], + windowStart: Long, + windowEnd: Long, + activeToReactivePowerFuncOpt: Option[ + Power => ReactivePower + ] = None, + log: Logger, + ): ApparentPower = { + val p = QuantityUtil.average[Power, Energy]( + tickToPower.map { case (tick, pd) => + tick -> pd.p + }, + windowStart, + windowEnd, + ) match { + case Success(pSuccess) => + pSuccess + case Failure(exception) => + log.warn( + "Unable to determine average active power. Apply 0 instead.", + exception, + ) + zeroMW + } + + val q = QuantityUtil.average[Power, Energy]( + tickToPower.map { case (tick, pd) => + activeToReactivePowerFuncOpt match { + case Some(qFunc) => + // NOTE: The type conversion to Megawatts is done to satisfy the methods type constraints + // and is undone after unpacking the results + tick -> Megawatts(qFunc(pd.toApparentPower.p).toMegavars) + case None => tick -> Megawatts(pd.toApparentPower.q.toMegavars) + } + }, + windowStart, + windowEnd, + ) match { + case Success(pSuccess) => + Megavars(pSuccess.toMegawatts) + case Failure(exception) => + log.warn( + "Unable to determine average reactive power. Apply 0 instead.", + exception, + ) + zeroMVAr + } + + ApparentPower(p, q) + } +} diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala index 967378bb1e..ab490f84e0 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala @@ -17,8 +17,11 @@ import edu.ie3.simona.model.participant2.ParticipantModel.{ } import edu.ie3.simona.agent.participant2.ParticipantAgent import edu.ie3.simona.agent.participant2.ParticipantAgent.ParticipantRequest +import edu.ie3.simona.model.participant.control.QControl import edu.ie3.simona.service.ServiceType +import edu.ie3.util.scala.quantities.ReactivePower import org.apache.pekko.actor.typed.scaladsl.ActorContext +import squants.Dimensionless import squants.energy.Power import java.time.ZonedDateTime @@ -28,8 +31,29 @@ abstract class ParticipantModel[ OP <: OperatingPoint, S <: ModelState, OR <: OperationRelevantData, -](val uuid: UUID) - extends ParticipantFlexibility[OP, S, OR] { +] extends ParticipantFlexibility[OP, S, OR] { + + val uuid: UUID + val sRated: Power + val cosPhiRated: Double + val qControl: QControl + + /** Get a partial function, that transfers the current active into reactive + * power based on the participants properties and the given nodal voltage + * + * @param nodalVoltage + * The currently given nodal voltage + * @return + * A [[PartialFunction]] from [[Power]] to [[ReactivePower]] + */ + def activeToReactivePowerFunc( + nodalVoltage: Dimensionless + ): Power => ReactivePower = + qControl.activeToReactivePowerFunc( + sRated, + cosPhiRated, + nodalVoltage, + ) def determineOperatingPoint(state: S, relevantData: OR): (OP, Option[Long]) @@ -102,7 +126,7 @@ object ParticipantModel { ) final case class ResultsContainer( - power: Power, + totalPower: ApparentPower, modelResults: Seq[SystemParticipantResult], ) diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala index 8547b318c2..d4098f280e 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala @@ -9,18 +9,21 @@ import edu.ie3.simona.agent.participant.data.Data.PrimaryData.ApparentPower import edu.ie3.simona.agent.participant.data.Data.SecondaryData import edu.ie3.simona.agent.participant2.ParticipantAgent import edu.ie3.simona.agent.participant2.ParticipantAgent.ParticipantRequest +import edu.ie3.simona.exceptions.CriticalFailureException import edu.ie3.simona.model.em.EmTools import edu.ie3.simona.model.participant2.ParticipantModel.{ ModelChangeIndicator, ModelState, OperatingPoint, OperationRelevantData, + ResultsContainer, } import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage.{ IssueFlexControl, ProvideFlexOptions, } import org.apache.pekko.actor.typed.scaladsl.ActorContext +import squants.Dimensionless /** Takes care of: * - activating/deactivating model @@ -59,27 +62,36 @@ final case class ParticipantModelShell[ ): ParticipantModelShell[OP, S, OR] = { val currentState = determineCurrentState(currentTick) + if (currentState.tick != currentTick) + throw new CriticalFailureException( + s"New state $currentState is not set to current tick $currentTick" + ) + val (newOperatingPoint, maybeNextTick) = model.determineOperatingPoint(state, relevantData) - val activePower = newOperatingPoint.activePower + copy( + state = currentState, + operatingPoint = newOperatingPoint, + modelChange = ModelChangeIndicator(changesAtTick = maybeNextTick), + ) + } - // todo where store the reactive power? + def determineResults( + currentTick: Long, + nodalVoltage: Dimensionless, + ): ResultsContainer = { + val activePower = operatingPoint.activePower + + // todo where store the reactive power? where voltage? val reactivePower = ??? - // todo store results here as well? Or separate module for avg power calculation? - val results = model.createResults( + model.createResults( state, - newOperatingPoint, + operatingPoint, ApparentPower(activePower, reactivePower), ???, ) - - copy( - state = currentState, - operatingPoint = newOperatingPoint, - modelChange = ModelChangeIndicator(changesAtTick = maybeNextTick), - ) } def updateFlexOptions(currentTick: Long): ParticipantModelShell[OP, S, OR] = { @@ -102,10 +114,25 @@ final case class ParticipantModelShell[ val (newOperatingPoint, modelChange) = model.handlePowerControl(flexOptions, setPointActivePower) - copy( - state = currentState, - operatingPoint = newOperatingPoint, - modelChange = modelChange, + val activePower = newOperatingPoint.activePower + + // todo where store the reactive power? + val reactivePower = ??? + + val results = model.createResults( + state, + newOperatingPoint, + ApparentPower(activePower, reactivePower), + ???, + ) + + ( + copy( + state = currentState, + operatingPoint = newOperatingPoint, + modelChange = modelChange, + ), + results, ) } diff --git a/src/main/scala/edu/ie3/simona/model/participant2/PvModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/PvModel.scala index debaa8816b..9ba5c9ee6c 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/PvModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/PvModel.scala @@ -40,9 +40,10 @@ import java.util.stream.IntStream import scala.math._ final class PvModel private ( - uuid: UUID, - sRated: Power, - cosPhiRated: Double, + override val uuid: UUID, + override val sRated: Power, + override val cosPhiRated: Double, + override val qControl: QControl, private val lat: Angle, private val lon: Angle, private val albedo: Double, @@ -54,7 +55,7 @@ final class PvModel private ( ActivePowerOperatingPoint, ConstantState.type, PvRelevantData, - ](uuid) + ] with ParticipantSimpleFlexibility[ConstantState.type, PvRelevantData] with LazyLogging { @@ -731,7 +732,7 @@ final class PvModel private ( dateTime: ZonedDateTime, ): ResultsContainer = { ResultsContainer( - operatingPoint.activePower, + complexPower, Seq( new PvResult( dateTime, @@ -802,7 +803,7 @@ object PvModel { ) // moduleSurface and yieldSTC are left out for now - val model = apply( + new PvModel( scaledInput.getUuid, scaledInput.getId, operationInterval, @@ -836,10 +837,6 @@ object PvModel { .doubleValue ), ) - - model.enable() - - model } } From 6db21bd092a88fe614ee79b6e14760908a8844bd Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Fri, 27 Sep 2024 10:45:03 +0200 Subject: [PATCH 05/77] Further dev Signed-off-by: Sebastian Peter --- .../simona/agent/grid/GridAgentMessages.scala | 4 +- .../agent/participant2/ParticipantAgent.scala | 33 ++++++- .../participant2/ParticipantGridAdapter.scala | 98 ++++++++++++++----- .../model/participant2/ParticipantModel.scala | 20 ++-- .../participant2/ParticipantModelShell.scala | 5 + 5 files changed, 118 insertions(+), 42 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/agent/grid/GridAgentMessages.scala b/src/main/scala/edu/ie3/simona/agent/grid/GridAgentMessages.scala index b0d98be1e9..727c31a5c4 100644 --- a/src/main/scala/edu/ie3/simona/agent/grid/GridAgentMessages.scala +++ b/src/main/scala/edu/ie3/simona/agent/grid/GridAgentMessages.scala @@ -186,8 +186,8 @@ object GridAgentMessages { /** Provide values as a reply to a * [[edu.ie3.simona.agent.participant.ParticipantAgent.RequestAssetPowerMessage]]. * In contrast to [[AssetPowerChangedMessage]], this message indicates that - * the same values for [[p]] and [[q]] has been send again as in the previous - * request + * the same values for [[p]] and [[q]] as to the previous request have been + * sent again * * @param p * Active power from the previous request diff --git a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgent.scala b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgent.scala index 68cf162e41..b6a5a96981 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgent.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgent.scala @@ -6,6 +6,8 @@ package edu.ie3.simona.agent.participant2 +import breeze.numerics.{pow, sqrt} +import edu.ie3.simona.agent.grid.GridAgentMessages.AssetPowerChangedMessage import edu.ie3.simona.agent.participant.data.Data.SecondaryData import edu.ie3.simona.agent.participant.data.Data import edu.ie3.simona.exceptions.CriticalFailureException @@ -25,7 +27,7 @@ import edu.ie3.util.scala.Scope import org.apache.pekko.actor.typed.scaladsl.Behaviors import org.apache.pekko.actor.typed.{ActorRef, Behavior} import org.apache.pekko.actor.{ActorRef => ClassicRef} -import squants.Dimensionless +import squants.{Dimensionless, Each} object ParticipantAgent { @@ -172,12 +174,33 @@ object ParticipantAgent { parentData, ) - case (ctx, msg: RequestAssetPowerMessage) => - // activeToReactivePowerFunc + case (ctx, RequestAssetPowerMessage(currentTick, eInPu, fInPu)) => + val activeToReactivePowerFunc = modelShell.activeToReactivePowerFunc - gridAdapter.updateAveragePower(msg.currentTick, ctx.log) + val nodalVoltage = Each( + sqrt( + pow(eInPu.toEach, 2) + + pow(fInPu.toEach, 2) + ) + ) + + val updatedGridAdapter = gridAdapter + .updateNodalVoltage(nodalVoltage) + .updateAveragePower( + currentTick, + Some(activeToReactivePowerFunc), + ctx.log, + ) + + val avgPower = updatedGridAdapter.avgPowerCache.get + gridAdapter.gridAgent ! AssetPowerChangedMessage(avgPower.p, avgPower.q) - Behaviors.same + ParticipantAgent( + modelShell, + dataCore, + updatedGridAdapter, + parentData, + ) } private def maybeCalculate( diff --git a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantGridAdapter.scala b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantGridAdapter.scala index 04d159e7cc..a82fdaff9c 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantGridAdapter.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantGridAdapter.scala @@ -6,17 +6,15 @@ package edu.ie3.simona.agent.participant2 -import edu.ie3.simona.agent.em.FlexCorrespondenceStore.WithTime import edu.ie3.simona.agent.grid.GridAgent import edu.ie3.simona.agent.participant.data.Data.PrimaryData.ApparentPower -import edu.ie3.simona.agent.participant2.ParticipantGridAdapter.averageApparentPower +import edu.ie3.simona.agent.participant2.ParticipantGridAdapter._ import edu.ie3.util.scala.quantities.DefaultQuantities.{zeroMVAr, zeroMW} import edu.ie3.util.scala.quantities.{Megavars, QuantityUtil, ReactivePower} import org.apache.pekko.actor.typed.ActorRef -import org.apache.pekko.event.LoggingAdapter import org.slf4j.Logger import squants.energy.Megawatts -import squants.{Dimensionless, Energy, Power} +import squants.{Dimensionless, Each, Energy, Power} import scala.collection.immutable.SortedMap import scala.util.{Failure, Success} @@ -26,20 +24,17 @@ import scala.util.{Failure, Success} * @param gridAgent * @param expectedRequestTick * Tick at which next power request is expected - * @param voltage - * @param lastRequestTick - * Tick of the last request + * @param nodalVoltage * @param tickToPower * power values - * @param currentAvgPower + * @param avgPowerCache */ final case class ParticipantGridAdapter( gridAgent: ActorRef[GridAgent.Request], + nodalVoltage: Dimensionless, expectedRequestTick: Long, - voltage: Dimensionless, - lastRequestTick: Long = 0, tickToPower: SortedMap[Long, ApparentPower], - currentAvgPower: WithTime[ApparentPower], + avgPowerCache: Option[AvgPowerResult], ) { def isPowerRequestExpected(currentTick: Long): Boolean = { @@ -51,26 +46,59 @@ final case class ParticipantGridAdapter( tick: Long, ): ParticipantGridAdapter = copy(tickToPower = tickToPower + (tick, power)) + // power of the current tick is irrelevant + def updateNodalVoltage(voltage: Dimensionless): ParticipantGridAdapter = + copy(nodalVoltage = voltage) + def updateAveragePower( currentTick: Long, + activeToReactivePowerFuncOpt: Option[ + Dimensionless => Power => ReactivePower + ], log: Logger, ): ParticipantGridAdapter = { - val averagePower = - averageApparentPower(tickToPower, lastRequestTick, currentTick, ???, log) - - // keep the last entry because we do not know - // if the next entry will necessarily be at the - // current tick - val lastTickAndPower = tickToPower.maxByOption { case (tick, _) => - tick - } + implicit val voltageTolerance: Dimensionless = Each( + 1e-3 + ) // todo requestVoltageDeviationThreshold + + val result = (avgPowerCache match { + case Some(cache @ AvgPowerResult(windowStart, windowEnd, voltage, _)) + if windowEnd == currentTick => + // Results have been calculated for the same tick... + if (voltage =~ nodalVoltage) { + // ... and same voltage, return cached result + Left(cache) + } else { + // ... and different voltage, results have to be re-calculated with same params + Right(windowStart, windowEnd) + } + case Some(AvgPowerResult(_, windowEnd, _, _)) => + // Results have been calculated for a former tick, take former windowEnd as the new windowStart + Right(windowEnd, currentTick) + case None => + // No results have been calculated whatsoever, calculate from simulation start (0) + Right(0, currentTick) + }).fold( + cachedResult => cachedResult, + { case (windowStart, windowEnd) => + val avgPower = averageApparentPower( + tickToPower, + windowStart, + windowEnd, + activeToReactivePowerFuncOpt.map(_.apply(nodalVoltage)), + log, + ) + AvgPowerResult(windowStart, windowEnd, nodalVoltage, avgPower) + }, + ) + + val reducedMap = reduceTickToPowerMap(tickToPower, result.windowStart) copy( - currentAvgPower = WithTime(averagePower, currentTick), - tickToPower = SortedMap.from(lastTickAndPower), - lastRequestTick = currentTick, + avgPowerCache = Some(result), + tickToPower = reducedMap, ) } @@ -78,6 +106,28 @@ final case class ParticipantGridAdapter( object ParticipantGridAdapter { + final case class AvgPowerResult( + windowStart: Long, + windowEnd: Long, + voltage: Dimensionless, + avgPower: ApparentPower, + ) + + private def reduceTickToPowerMap( + tickToPower: SortedMap[Long, ApparentPower], + windowStart: Long, + ): SortedMap[Long, ApparentPower] = { + // keep the last entry at or before windowStart + val lastTickBeforeWindowStart = + tickToPower.rangeUntil(windowStart + 1).lastOption + + // throw out all entries before or at windowStart + val reducedMap = tickToPower.rangeFrom(windowStart + 1) + + // combine both + lastTickBeforeWindowStart.map(reducedMap + _).getOrElse(reducedMap) + } + /** Determine the average apparent power within the given tick window * * @param tickToPower @@ -91,7 +141,7 @@ object ParticipantGridAdapter { * @return * The averaged apparent power */ - def averageApparentPower( + private def averageApparentPower( tickToPower: Map[Long, ApparentPower], windowStart: Long, windowEnd: Long, diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala index ab490f84e0..1f1e4ee891 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala @@ -41,19 +41,17 @@ abstract class ParticipantModel[ /** Get a partial function, that transfers the current active into reactive * power based on the participants properties and the given nodal voltage * - * @param nodalVoltage - * The currently given nodal voltage * @return - * A [[PartialFunction]] from [[Power]] to [[ReactivePower]] + * A [[PartialFunction]] from [[Power]] and voltage ([[Dimensionless]]) to + * [[ReactivePower]] */ - def activeToReactivePowerFunc( - nodalVoltage: Dimensionless - ): Power => ReactivePower = - qControl.activeToReactivePowerFunc( - sRated, - cosPhiRated, - nodalVoltage, - ) + def activeToReactivePowerFunc: Dimensionless => Power => ReactivePower = + nodalVoltage => + qControl.activeToReactivePowerFunc( + sRated, + cosPhiRated, + nodalVoltage, + ) def determineOperatingPoint(state: S, relevantData: OR): (OP, Option[Long]) diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala index d4098f280e..94079fcee7 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala @@ -22,8 +22,10 @@ import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage.{ IssueFlexControl, ProvideFlexOptions, } +import edu.ie3.util.scala.quantities.ReactivePower import org.apache.pekko.actor.typed.scaladsl.ActorContext import squants.Dimensionless +import squants.energy.Power /** Takes care of: * - activating/deactivating model @@ -77,6 +79,9 @@ final case class ParticipantModelShell[ ) } + def activeToReactivePowerFunc: Dimensionless => Power => ReactivePower = + model.activeToReactivePowerFunc + def determineResults( currentTick: Long, nodalVoltage: Dimensionless, From cdea5e9a1f0cf4706845ee9280fbbba33749d7e0 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Mon, 30 Sep 2024 14:31:34 +0200 Subject: [PATCH 06/77] Further dev, connection to init Signed-off-by: Sebastian Peter --- .../simona/agent/participant/data/Data.scala | 8 +- .../agent/participant2/ParticipantAgent.scala | 147 +++++++++++++----- .../participant2/ParticipantAgentInit.scala | 6 +- .../participant2/ParticipantGridAdapter.scala | 57 +++++-- ...re.scala => ParticipantInputHandler.scala} | 27 +++- .../model/participant2/ParticipantModel.scala | 44 ++++-- .../participant2/ParticipantModelShell.scala | 92 +++++++---- .../simona/model/participant2/PvModel.scala | 9 +- 8 files changed, 275 insertions(+), 115 deletions(-) rename src/main/scala/edu/ie3/simona/agent/participant2/{ParticipantDataCore.scala => ParticipantInputHandler.scala} (68%) diff --git a/src/main/scala/edu/ie3/simona/agent/participant/data/Data.scala b/src/main/scala/edu/ie3/simona/agent/participant/data/Data.scala index cb56e1af92..7acd5b9b08 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant/data/Data.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant/data/Data.scala @@ -10,12 +10,11 @@ import edu.ie3.datamodel.models.value._ import edu.ie3.simona.agent.participant.data.Data.PrimaryData.ApparentPower import edu.ie3.util.quantities.PowerSystemUnits import edu.ie3.util.quantities.interfaces.EnergyPrice -import edu.ie3.util.scala.quantities.{Kilovars, Megavars, ReactivePower} -import squants.energy.{Power, Kilowatts, Megawatts} -import tech.units.indriya.ComparableQuantity import edu.ie3.util.scala.quantities.DefaultQuantities._ +import edu.ie3.util.scala.quantities.{Kilovars, ReactivePower} +import squants.energy.{Kilowatts, Power} +import tech.units.indriya.ComparableQuantity -import java.time.ZonedDateTime import scala.jdk.OptionConverters.RichOptional import scala.util.{Failure, Success, Try} @@ -241,7 +240,6 @@ object Data { */ trait SecondaryData extends Data object SecondaryData { - final case class DateTime(dateTime: ZonedDateTime) extends SecondaryData final case class WholesalePrice(price: ComparableQuantity[EnergyPrice]) extends SecondaryData } diff --git a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgent.scala b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgent.scala index b6a5a96981..25c1392983 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgent.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgent.scala @@ -7,20 +7,20 @@ package edu.ie3.simona.agent.participant2 import breeze.numerics.{pow, sqrt} -import edu.ie3.simona.agent.grid.GridAgentMessages.AssetPowerChangedMessage -import edu.ie3.simona.agent.participant.data.Data.SecondaryData +import edu.ie3.simona.agent.grid.GridAgent +import edu.ie3.simona.agent.grid.GridAgentMessages.{ + AssetPowerChangedMessage, + AssetPowerUnchangedMessage, +} import edu.ie3.simona.agent.participant.data.Data +import edu.ie3.simona.agent.participant.data.Data.SecondaryData import edu.ie3.simona.exceptions.CriticalFailureException -import edu.ie3.simona.model.participant2.ParticipantModelShell -import edu.ie3.simona.ontology.messages.SchedulerMessage.Completion -import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage.{ - FlexActivation, - FlexCompletion, - FlexRequest, - FlexResponse, - IssueFlexControl, - ProvideFlexOptions, +import edu.ie3.simona.model.participant2.{ + ParticipantModel, + ParticipantModelShell, } +import edu.ie3.simona.ontology.messages.SchedulerMessage.Completion +import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage._ import edu.ie3.simona.ontology.messages.services.ServiceMessage.ProvisionMessage import edu.ie3.simona.ontology.messages.{Activation, SchedulerMessage} import edu.ie3.util.scala.Scope @@ -94,6 +94,14 @@ object ParticipantAgent { fInPu: Dimensionless, ) extends Request + /** @param currentTick + * @param nextRequestTick + */ + final case class FinishParticipantSimulation( + currentTick: Long, + nextRequestTick: Long, + ) extends Request + /** The existence of this data object indicates that the corresponding agent * is not EM-controlled, but activated by a * [[edu.ie3.simona.scheduler.Scheduler]] @@ -131,9 +139,24 @@ object ParticipantAgent { val tick: Long } + def apply( + model: ParticipantModel[_, _, _], + expectedData: Map[ClassicRef, Long], + gridAgentRef: ActorRef[GridAgent.Request], + expectedPowerRequestTick: Long, + parentData: Either[SchedulerData, FlexControlledData], + ): Behavior[Request] = { + ParticipantAgent( + ParticipantModelShell(model), + ParticipantInputHandler(expectedData), + ParticipantGridAdapter(gridAgentRef, expectedPowerRequestTick), + parentData, + ) + } + def apply( modelShell: ParticipantModelShell[_, _, _], - dataCore: ParticipantDataCore, + inputHandler: ParticipantInputHandler, gridAdapter: ParticipantGridAdapter, parentData: Either[SchedulerData, FlexControlledData], ): Behavior[Request] = @@ -141,10 +164,10 @@ object ParticipantAgent { case (ctx, request: ParticipantRequest) => val updatedShell = modelShell.handleRequest(ctx, request) - ParticipantAgent(updatedShell, dataCore, gridAdapter, parentData) + ParticipantAgent(updatedShell, inputHandler, gridAdapter, parentData) case (ctx, activation: ActivationRequest) => - val coreWithActivation = dataCore.handleActivation(activation) + val coreWithActivation = inputHandler.handleActivation(activation) val (updatedShell, updatedCore, updatedGridAdapter) = maybeCalculate( @@ -162,7 +185,7 @@ object ParticipantAgent { ) case (ctx, msg: ProvisionMessage[Data]) => - val coreWithData = dataCore.handleDataProvision(msg) + val coreWithData = inputHandler.handleDataProvision(msg) val (updatedShell, updatedCore, updatedGridAdapter) = maybeCalculate(modelShell, coreWithData, gridAdapter, parentData) @@ -175,6 +198,9 @@ object ParticipantAgent { ) case (ctx, RequestAssetPowerMessage(currentTick, eInPu, fInPu)) => + // we do not have to wait for the resulting power of the current tick, + // since the current power is irrelevant for the average power up until now + val activeToReactivePowerFunc = modelShell.activeToReactivePowerFunc val nodalVoltage = Each( @@ -185,19 +211,41 @@ object ParticipantAgent { ) val updatedGridAdapter = gridAdapter - .updateNodalVoltage(nodalVoltage) - .updateAveragePower( + .handlePowerRequest( + nodalVoltage, currentTick, Some(activeToReactivePowerFunc), ctx.log, ) - val avgPower = updatedGridAdapter.avgPowerCache.get - gridAdapter.gridAgent ! AssetPowerChangedMessage(avgPower.p, avgPower.q) + val result = updatedGridAdapter.avgPowerResult.get + gridAdapter.gridAgent ! + (if (result.newResult) { + AssetPowerChangedMessage( + result.avgPower.p, + result.avgPower.q, + ) + } else { + AssetPowerUnchangedMessage( + result.avgPower.p, + result.avgPower.q, + ) + }) ParticipantAgent( modelShell, - dataCore, + inputHandler, + updatedGridAdapter, + parentData, + ) + + case (ctx, FinishParticipantSimulation(_, nextRequestTick)) => + val updatedGridAdapter = + gridAdapter.updateNextRequestTick(nextRequestTick) + + ParticipantAgent( + modelShell, + inputHandler, updatedGridAdapter, parentData, ) @@ -205,23 +253,23 @@ object ParticipantAgent { private def maybeCalculate( modelShell: ParticipantModelShell[_, _, _], - dataCore: ParticipantDataCore, + inputHandler: ParticipantInputHandler, gridAdapter: ParticipantGridAdapter, parentData: Either[SchedulerData, FlexControlledData], ): ( ParticipantModelShell[_, _, _], - ParticipantDataCore, + ParticipantInputHandler, ParticipantGridAdapter, ) = { - if (dataCore.isComplete) { + if (isDataComplete(inputHandler, gridAdapter)) { - val activation = dataCore.activation.getOrElse( + val activation = inputHandler.activation.getOrElse( throw new CriticalFailureException( "Activation should be present when data collection is complete" ) ) - val receivedData = dataCore.getData.map { + val receivedData = inputHandler.getData.map { case data: SecondaryData => data case other => throw new CriticalFailureException( @@ -229,20 +277,28 @@ object ParticipantAgent { ) } - val updatedShell = Scope(modelShell) - .map(_.updateRelevantData(receivedData, activation.tick)) + val (updatedShell, updatedGridAdapter) = Scope(modelShell) + .map( + _.updateRelevantData( + receivedData, + gridAdapter.nodalVoltage, + activation.tick, + ) + ) .map { shell => activation match { case ParticipantActivation(tick) => val modelWithOP = shell.updateOperatingPoint(tick) - if (!gridAdapter.isPowerRequestExpected(tick)) { - // we don't expect a power request that could change voltage, - // so we can go ahead and calculate results - val results = shell.determineResults(tick, ???) + val results = + modelWithOP.determineResults(tick, gridAdapter.nodalVoltage) + results.modelResults.foreach { res => // todo send out results } + val gridAdapterWithResult = + gridAdapter.storePowerValue(results.totalPower, tick) + parentData.fold( schedulerData => schedulerData.scheduler ! Completion( @@ -254,7 +310,7 @@ object ParticipantAgent { "Received activation while controlled by EM" ), ) - modelWithOP + (modelWithOP, gridAdapterWithResult) case Flex(FlexActivation(tick)) => val modelWithFlex = shell.updateFlexOptions(tick) @@ -264,10 +320,14 @@ object ParticipantAgent { throw new CriticalFailureException( "Received flex activation while not controlled by EM" ), - _.emAgent ! modelWithFlex.flexOptions, + _.emAgent ! modelWithFlex.flexOptions.getOrElse( + throw new CriticalFailureException( + "Flex options have not been calculated!" + ) + ), ) - modelWithFlex + (modelWithFlex, gridAdapter) case Flex(flexControl: IssueFlexControl) => val modelWithOP = shell.updateOperatingPoint(flexControl) @@ -291,11 +351,26 @@ object ParticipantAgent { } .get - (updatedShell, dataCore.completeActivity(), ???) + (updatedShell, inputHandler.completeActivity(), updatedGridAdapter) } else - (modelShell, dataCore, gridAdapter) + (modelShell, inputHandler, gridAdapter) } + def isDataComplete( + inputHandler: ParticipantInputHandler, + gridAdapter: ParticipantGridAdapter, + ): Boolean = + if (inputHandler.isComplete) { + val activation = inputHandler.activation.getOrElse( + throw new CriticalFailureException( + "Activation should be present when data collection is complete" + ) + ) + + !gridAdapter.isPowerRequestExpected(activation.tick) + } else + false + private def primaryData(): Behavior[Request] = Behaviors.receivePartial { case _ => Behaviors.same } diff --git a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgentInit.scala b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgentInit.scala index b8baf8f96c..7e4061d256 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgentInit.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgentInit.scala @@ -147,7 +147,7 @@ object ParticipantAgentInit { ) val requiredServices = model.getRequiredServices.toSeq if (requiredServices.isEmpty) { - ParticipantAgent(model) + ParticipantAgent(model, Map.empty, ???, ???, parentData) } else { waitingForServices(model) } @@ -168,7 +168,7 @@ object ParticipantAgentInit { val newExpectedRegistrations = expectedRegistrations.excl(serviceRef) val newExpectedFirstData = - expectedFirstData + (serviceRef, nextDataTick) + expectedFirstData.updated(serviceRef, nextDataTick) if (newExpectedRegistrations.isEmpty) { val earliestNextTick = expectedFirstData.map { case (_, nextTick) => @@ -188,7 +188,7 @@ object ParticipantAgentInit { ), ) - ParticipantAgent(model, newExpectedFirstData, parentData) + ParticipantAgent(model, newExpectedFirstData, ???, ???, parentData) } else waitingForServices( model, diff --git a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantGridAdapter.scala b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantGridAdapter.scala index a82fdaff9c..cab4556f12 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantGridAdapter.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantGridAdapter.scala @@ -9,6 +9,7 @@ package edu.ie3.simona.agent.participant2 import edu.ie3.simona.agent.grid.GridAgent import edu.ie3.simona.agent.participant.data.Data.PrimaryData.ApparentPower import edu.ie3.simona.agent.participant2.ParticipantGridAdapter._ +import edu.ie3.simona.exceptions.CriticalFailureException import edu.ie3.util.scala.quantities.DefaultQuantities.{zeroMVAr, zeroMW} import edu.ie3.util.scala.quantities.{Megavars, QuantityUtil, ReactivePower} import org.apache.pekko.actor.typed.ActorRef @@ -27,14 +28,14 @@ import scala.util.{Failure, Success} * @param nodalVoltage * @param tickToPower * power values - * @param avgPowerCache + * @param avgPowerResult */ final case class ParticipantGridAdapter( gridAgent: ActorRef[GridAgent.Request], nodalVoltage: Dimensionless, expectedRequestTick: Long, tickToPower: SortedMap[Long, ApparentPower], - avgPowerCache: Option[AvgPowerResult], + avgPowerResult: Option[AvgPowerResult], ) { def isPowerRequestExpected(currentTick: Long): Boolean = { @@ -47,61 +48,72 @@ final case class ParticipantGridAdapter( ): ParticipantGridAdapter = copy(tickToPower = tickToPower + (tick, power)) - // power of the current tick is irrelevant - - def updateNodalVoltage(voltage: Dimensionless): ParticipantGridAdapter = - copy(nodalVoltage = voltage) - - def updateAveragePower( + def handlePowerRequest( + newVoltage: Dimensionless, currentTick: Long, activeToReactivePowerFuncOpt: Option[ Dimensionless => Power => ReactivePower ], log: Logger, ): ParticipantGridAdapter = { + if (currentTick != expectedRequestTick) + throw new CriticalFailureException( + s"Power request expected for $expectedRequestTick, but not for current tick $currentTick" + ) + implicit val voltageTolerance: Dimensionless = Each( 1e-3 ) // todo requestVoltageDeviationThreshold - val result = (avgPowerCache match { - case Some(cache @ AvgPowerResult(windowStart, windowEnd, voltage, _)) + val result = (avgPowerResult match { + case Some(cache @ AvgPowerResult(windowStart, windowEnd, voltage, _, _)) if windowEnd == currentTick => // Results have been calculated for the same tick... - if (voltage =~ nodalVoltage) { + if (voltage =~ newVoltage) { // ... and same voltage, return cached result Left(cache) } else { // ... and different voltage, results have to be re-calculated with same params Right(windowStart, windowEnd) } - case Some(AvgPowerResult(_, windowEnd, _, _)) => + case Some(AvgPowerResult(_, windowEnd, _, _, _)) => // Results have been calculated for a former tick, take former windowEnd as the new windowStart Right(windowEnd, currentTick) case None => // No results have been calculated whatsoever, calculate from simulation start (0) Right(0, currentTick) }).fold( - cachedResult => cachedResult, + cachedResult => cachedResult.copy(newResult = false), { case (windowStart, windowEnd) => val avgPower = averageApparentPower( tickToPower, windowStart, windowEnd, - activeToReactivePowerFuncOpt.map(_.apply(nodalVoltage)), + activeToReactivePowerFuncOpt.map(_.apply(newVoltage)), log, ) - AvgPowerResult(windowStart, windowEnd, nodalVoltage, avgPower) + AvgPowerResult( + windowStart, + windowEnd, + newVoltage, + avgPower, + newResult = true, + ) }, ) val reducedMap = reduceTickToPowerMap(tickToPower, result.windowStart) copy( - avgPowerCache = Some(result), + nodalVoltage = newVoltage, tickToPower = reducedMap, + avgPowerResult = Some(result), ) } + def updateNextRequestTick(nextRequestTick: Long): ParticipantGridAdapter = + copy(expectedRequestTick = nextRequestTick) + } object ParticipantGridAdapter { @@ -111,8 +123,21 @@ object ParticipantGridAdapter { windowEnd: Long, voltage: Dimensionless, avgPower: ApparentPower, + newResult: Boolean, ) + def apply( + gridAgentRef: ActorRef[GridAgent.Request], + expectedRequestTick: Long, + ): ParticipantGridAdapter = + new ParticipantGridAdapter( + gridAgent = gridAgentRef, + nodalVoltage = Each(1d), + expectedRequestTick = expectedRequestTick, + tickToPower = SortedMap.empty, + avgPowerResult = None, + ) + private def reduceTickToPowerMap( tickToPower: SortedMap[Long, ApparentPower], windowStart: Long, diff --git a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantDataCore.scala b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantInputHandler.scala similarity index 68% rename from src/main/scala/edu/ie3/simona/agent/participant2/ParticipantDataCore.scala rename to src/main/scala/edu/ie3/simona/agent/participant2/ParticipantInputHandler.scala index b378616a36..a79b5795c7 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantDataCore.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantInputHandler.scala @@ -11,8 +11,7 @@ import edu.ie3.simona.agent.participant.data.Data import edu.ie3.simona.agent.participant2.ParticipantAgent.ActivationRequest import edu.ie3.simona.ontology.messages.services.ServiceMessage.ProvisionMessage -/** todo rather call ParticipantInputHandler? */ -case class ParticipantDataCore( +final case class ParticipantInputHandler( expectedData: Map[ClassicRef, Long], receivedData: Map[ClassicRef, Option[_ <: Data]], activation: Option[ActivationRequest], @@ -23,17 +22,19 @@ case class ParticipantDataCore( // holds results as well? or no? - def handleActivation(activation: ActivationRequest): ParticipantDataCore = { + def handleActivation( + activation: ActivationRequest + ): ParticipantInputHandler = { copy(activation = Some(activation)) } - def completeActivity(): ParticipantDataCore = { + def completeActivity(): ParticipantInputHandler = { copy(activation = None) } def handleDataProvision( msg: ProvisionMessage[_ <: Data] - ): ParticipantDataCore = { + ): ParticipantInputHandler = { val updatedReceivedData = receivedData + (msg.serviceRef -> Some(msg.data)) val updatedExpectedData = msg.nextDataTick .map { nextTick => @@ -46,11 +47,23 @@ case class ParticipantDataCore( copy(expectedData = updatedExpectedData, receivedData = updatedReceivedData) } - def isComplete: Boolean = activation.nonEmpty && receivedData.forall { - case (_, data) => data.nonEmpty + def isComplete: Boolean = activation.exists { activationMsg => + expectedData.forall { case (_, nextTick) => + nextTick > activationMsg.tick + } } def getData: Seq[Data] = receivedData.values.flatten.toSeq } + +object ParticipantInputHandler { + + def apply(expectedData: Map[ClassicRef, Long]): ParticipantInputHandler = + new ParticipantInputHandler( + expectedData = expectedData, + receivedData = Map.empty, + activation = None, + ) +} diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala index 1f1e4ee891..40e35cd06f 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala @@ -64,16 +64,6 @@ abstract class ParticipantModel[ dateTime: ZonedDateTime, ): ResultsContainer - // todo split off the following to ParticipantModelMeta? - def getRequiredServices: Iterable[ServiceType] - - /** @param receivedData - * @throws CriticalFailureException - * if unexpected type of data was provided - * @return - */ - def createRelevantData(receivedData: Seq[SecondaryData], tick: Long): OR - /** Handling requests that are not part of the standard participant protocol * * @param state @@ -92,6 +82,22 @@ abstract class ParticipantModel[ ): S = throw new NotImplementedError(s"Method not implemented by $getClass") + // todo split off the following to ParticipantModelMeta? + def getRequiredServices: Iterable[ServiceType] + + def getInitialState(): S + + /** @param receivedData + * @throws CriticalFailureException + * if unexpected type of data was provided + * @return + */ + def createRelevantData( + receivedData: Seq[SecondaryData], + nodalVoltage: Dimensionless, + tick: Long, + ): OR + } object ParticipantModel { @@ -110,7 +116,23 @@ object ParticipantModel { } case object ConstantState extends ModelState { - override val tick = -1 // is there a better way? + override val tick: Long = -1 // is there a better way? + } + + trait ParticipantConstantModel[ + OP <: OperatingPoint, + OR <: OperationRelevantData, + ] { + this: ParticipantModel[OP, ConstantState.type, OR] => + + override def getInitialState(): ConstantState.type = ConstantState + + override def determineState( + lastState: ConstantState.type, + operatingPoint: OP, + currentTick: Long, + ): ConstantState.type = ConstantState + } /** Indicates when either flex options change (when em-controlled) or the diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala index 94079fcee7..6b8fdb7725 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala @@ -5,6 +5,7 @@ */ package edu.ie3.simona.model.participant2 + import edu.ie3.simona.agent.participant.data.Data.PrimaryData.ApparentPower import edu.ie3.simona.agent.participant.data.Data.SecondaryData import edu.ie3.simona.agent.participant2.ParticipantAgent @@ -28,35 +29,36 @@ import squants.Dimensionless import squants.energy.Power /** Takes care of: - * - activating/deactivating model * - holding id information * - storing: * - states (only current needed) * - operating points (only current needed) * - operation relevant data (only current needed) * - flex options? (only current needed) - * - results? also needs to handle power request from grid */ final case class ParticipantModelShell[ OP <: OperatingPoint, S <: ModelState, OR <: OperationRelevantData, ]( - model: ParticipantModel[OP, S, OR] with ParticipantFlexibility[OP, S, OR], + model: ParticipantModel[OP, S, OR] + with ParticipantFlexibility[OP, S, OR], // todo primary replay model? state: S, - relevantData: OR, - operatingPoint: OP, - flexOptions: ProvideFlexOptions, + relevantData: Option[OR], + flexOptions: Option[ProvideFlexOptions], + operatingPoint: Option[OP], modelChange: ModelChangeIndicator, ) { def updateRelevantData( receivedData: Seq[SecondaryData], + nodalVoltage: Dimensionless, tick: Long, ): ParticipantModelShell[OP, S, OR] = { - val updatedRelevantData = model.createRelevantData(receivedData, tick) + val updatedRelevantData = + model.createRelevantData(receivedData, nodalVoltage, tick) - copy(relevantData = updatedRelevantData) + copy(relevantData = Some(updatedRelevantData)) } def updateOperatingPoint( @@ -70,11 +72,14 @@ final case class ParticipantModelShell[ ) val (newOperatingPoint, maybeNextTick) = - model.determineOperatingPoint(state, relevantData) + model.determineOperatingPoint( + state, + relevantData.getOrElse("No relevant data available!"), + ) copy( state = currentState, - operatingPoint = newOperatingPoint, + operatingPoint = Some(newOperatingPoint), modelChange = ModelChangeIndicator(changesAtTick = maybeNextTick), ) } @@ -86,14 +91,17 @@ final case class ParticipantModelShell[ currentTick: Long, nodalVoltage: Dimensionless, ): ResultsContainer = { - val activePower = operatingPoint.activePower + val op = operatingPoint + .getOrElse( + throw new CriticalFailureException("No operating point available!") + ) - // todo where store the reactive power? where voltage? - val reactivePower = ??? + val activePower = op.activePower + val reactivePower = activeToReactivePowerFunc(nodalVoltage)(activePower) model.createResults( state, - operatingPoint, + op, ApparentPower(activePower, reactivePower), ???, ) @@ -101,23 +109,30 @@ final case class ParticipantModelShell[ def updateFlexOptions(currentTick: Long): ParticipantModelShell[OP, S, OR] = { val currentState = determineCurrentState(currentTick) - val flexOptions = model.calcFlexOptions(currentState, relevantData) + val flexOptions = model.calcFlexOptions( + currentState, + relevantData.getOrElse("No relevant data available!"), + ) - copy(state = currentState, flexOptions = flexOptions) + copy(state = currentState, flexOptions = Some(flexOptions)) } def updateOperatingPoint( flexControl: IssueFlexControl ): ParticipantModelShell[OP, S, OR] = { + val fo = flexOptions.getOrElse( + throw new CriticalFailureException("No flex options available!") + ) + val currentState = determineCurrentState(flexControl.tick) val setPointActivePower = EmTools.determineFlexPower( - flexOptions, + fo, flexControl, ) val (newOperatingPoint, modelChange) = - model.handlePowerControl(flexOptions, setPointActivePower) + model.handlePowerControl(fo, setPointActivePower) val activePower = newOperatingPoint.activePower @@ -131,13 +146,10 @@ final case class ParticipantModelShell[ ???, ) - ( - copy( - state = currentState, - operatingPoint = newOperatingPoint, - modelChange = modelChange, - ), - results, + copy( + state = currentState, + operatingPoint = Some(newOperatingPoint), + modelChange = modelChange, ) } @@ -151,11 +163,29 @@ final case class ParticipantModelShell[ copy(state = updatedState) } - private def determineCurrentState(currentTick: Long): S = { - if (state.tick < currentTick) - model.determineState(state, operatingPoint, currentTick) - else - state - } + private def determineCurrentState(currentTick: Long): S = + operatingPoint + .flatMap { op => + Option.when(state.tick < currentTick) { + model.determineState(state, op, currentTick) + } + } + .getOrElse(state) + +} + +object ParticipantModelShell { + + def apply( + model: ParticipantModel[_, _, _] + ): ParticipantModelShell[_, _, _] = + new ParticipantModelShell( + model = model, + state = model.getInitialState(), + relevantData = None, + flexOptions = None, + operatingPoint = None, + modelChange = ModelChangeIndicator(), + ) } diff --git a/src/main/scala/edu/ie3/simona/model/participant2/PvModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/PvModel.scala index 9ba5c9ee6c..827a2c6c6c 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/PvModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/PvModel.scala @@ -19,6 +19,7 @@ import edu.ie3.simona.model.participant2.ParticipantModel.{ ActivePowerOperatingPoint, ConstantState, OperationRelevantData, + ParticipantConstantModel, ResultsContainer, } import edu.ie3.simona.model.participant2.PvModel.PvRelevantData @@ -57,6 +58,7 @@ final class PvModel private ( PvRelevantData, ] with ParticipantSimpleFlexibility[ConstantState.type, PvRelevantData] + with ParticipantConstantModel[ActivePowerOperatingPoint, PvRelevantData] with LazyLogging { /** Override sMax as the power output of a pv unit could become easily up to @@ -719,12 +721,6 @@ final class PvModel private ( else proposal } - override def determineState( - lastState: ParticipantModel.ConstantState.type, - operatingPoint: ActivePowerOperatingPoint, - currentTick: Long, - ): ParticipantModel.ConstantState.type = ConstantState - override def createResults( lastState: ParticipantModel.ConstantState.type, operatingPoint: ActivePowerOperatingPoint, @@ -749,6 +745,7 @@ final class PvModel private ( def createRelevantData( receivedData: Seq[SecondaryData], + nodalVoltage: Dimensionless, tick: Long, ): PvRelevantData = { receivedData From d6431c2f05515c0cc815a22a8c34fa071ad9b6fb Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Wed, 2 Oct 2024 12:23:03 +0200 Subject: [PATCH 07/77] Further dev Signed-off-by: Sebastian Peter --- .../model/participant2/ParticipantModel.scala | 15 +- .../participant2/ParticipantModelShell.scala | 4 +- .../model/participant2/PrimaryDataModel.scala | 128 ++++++++++++++++++ .../simona/model/participant2/PvModel.scala | 10 +- 4 files changed, 147 insertions(+), 10 deletions(-) create mode 100644 src/main/scala/edu/ie3/simona/model/participant2/PrimaryDataModel.scala diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala index 40e35cd06f..d4027488c6 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala @@ -8,7 +8,7 @@ package edu.ie3.simona.model.participant2 import edu.ie3.datamodel.models.result.system.SystemParticipantResult import edu.ie3.simona.agent.participant.data.Data.PrimaryData.ApparentPower -import edu.ie3.simona.agent.participant.data.Data.SecondaryData +import edu.ie3.simona.agent.participant.data.Data import edu.ie3.simona.model.participant2.ParticipantModel.{ ModelState, OperatingPoint, @@ -93,7 +93,7 @@ abstract class ParticipantModel[ * @return */ def createRelevantData( - receivedData: Seq[SecondaryData], + receivedData: Seq[Data], nodalVoltage: Dimensionless, tick: Long, ): OR @@ -106,10 +106,17 @@ object ParticipantModel { trait OperatingPoint { val activePower: Power + + /** Reactive power can be overridden by the model itself. If this is None, + * the active-to-reactive-power function is used. + */ + val reactivePower: Option[ReactivePower] } - case class ActivePowerOperatingPoint(override val activePower: Power) - extends OperatingPoint + final case class ActivePowerOperatingPoint(override val activePower: Power) + extends OperatingPoint { + override val reactivePower: Option[ReactivePower] = None + } trait ModelState { val tick: Long diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala index 6b8fdb7725..5383f3227f 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala @@ -97,7 +97,9 @@ final case class ParticipantModelShell[ ) val activePower = op.activePower - val reactivePower = activeToReactivePowerFunc(nodalVoltage)(activePower) + val reactivePower = op.reactivePower.getOrElse( + activeToReactivePowerFunc(nodalVoltage)(activePower) + ) model.createResults( state, diff --git a/src/main/scala/edu/ie3/simona/model/participant2/PrimaryDataModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/PrimaryDataModel.scala new file mode 100644 index 0000000000..bbe638120e --- /dev/null +++ b/src/main/scala/edu/ie3/simona/model/participant2/PrimaryDataModel.scala @@ -0,0 +1,128 @@ +/* + * © 2024. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.model.participant2 + +import edu.ie3.simona.agent.participant.data.Data +import edu.ie3.simona.agent.participant.data.Data.PrimaryData +import edu.ie3.simona.agent.participant.data.Data.PrimaryData.{ + ActivePower, + PrimaryDataWithApparentPower, +} +import edu.ie3.simona.exceptions.CriticalFailureException +import edu.ie3.simona.model.participant.control.QControl +import edu.ie3.simona.model.participant2.ParticipantModel.{ + ConstantState, + OperatingPoint, + OperationRelevantData, + ParticipantConstantModel, +} +import edu.ie3.simona.model.participant2.PrimaryDataModel.PrimaryOperationRelevantData +import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage +import edu.ie3.simona.service.ServiceType +import edu.ie3.util.scala.quantities.ReactivePower +import squants.{Dimensionless, Power} + +import java.time.ZonedDateTime +import java.util.UUID +import scala.reflect.ClassTag + +/** Just "replaying" primary data + */ +final case class PrimaryDataModel[T <: PrimaryData: ClassTag]( + override val uuid: UUID, + override val sRated: Power, + override val cosPhiRated: Double, + override val qControl: QControl, +) extends ParticipantModel[ + OperatingPoint, + ConstantState.type, + PrimaryOperationRelevantData[T], + ] + with ParticipantConstantModel[OperatingPoint, PrimaryOperationRelevantData[ + T + ]] { + + override def determineOperatingPoint( + state: ParticipantModel.ConstantState.type, + relevantData: PrimaryOperationRelevantData[T], + ): (OperatingPoint, Option[Long]) = ??? + + override def createResults( + lastState: ParticipantModel.ConstantState.type, + operatingPoint: OperatingPoint, + complexPower: PrimaryData.ApparentPower, + dateTime: ZonedDateTime, + ): ParticipantModel.ResultsContainer = ??? + + override def getRequiredServices: Iterable[ServiceType] = ??? + + /** @param receivedData + * @throws CriticalFailureException + * if unexpected type of data was provided + * @return + */ + override def createRelevantData( + receivedData: Seq[Data], + nodalVoltage: Dimensionless, + tick: Long, + ): PrimaryOperationRelevantData[T] = + receivedData + .collectFirst { case data: T => + PrimaryOperationRelevantData(data) + } + .getOrElse { + throw new CriticalFailureException( + s"Expected WeatherData, got $receivedData" + ) + } + + override def calcFlexOptions( + state: ParticipantModel.ConstantState.type, + relevantData: PrimaryOperationRelevantData[T], + ): FlexibilityMessage.ProvideFlexOptions = ??? + + override def handlePowerControl( + flexOptions: FlexibilityMessage.ProvideFlexOptions, + setPower: Power, + ): (OperatingPoint, ParticipantModel.ModelChangeIndicator) = ??? +} + +object PrimaryDataModel { + + final case class PrimaryOperationRelevantData[+T <: PrimaryData](data: T) + extends OperationRelevantData + + trait PrimaryOperatingPoint[+T <: PrimaryData] extends OperatingPoint { + val data: T + + override val activePower: Power = data.p + + } + + object PrimaryOperatingPoint { + def apply[T <: PrimaryData: ClassTag](data: T): PrimaryOperatingPoint[T] = + data match { + case apparentPowerData: PrimaryDataWithApparentPower[_] => + PrimaryApparentPowerOperatingPoint(apparentPowerData) + case other => + PrimaryActivePowerOperatingPoint(other) + } + } + + private final case class PrimaryApparentPowerOperatingPoint[ + T <: PrimaryDataWithApparentPower[T] + ](override val data: T) + extends PrimaryOperatingPoint[T] { + override val reactivePower: Option[ReactivePower] = Some(data.q) + } + + private final case class PrimaryActivePowerOperatingPoint[+T <: PrimaryData]( + override val data: T + ) extends PrimaryOperatingPoint[T] { + override val reactivePower: Option[ReactivePower] = None + } +} diff --git a/src/main/scala/edu/ie3/simona/model/participant2/PvModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/PvModel.scala index 827a2c6c6c..0d824d3cdb 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/PvModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/PvModel.scala @@ -10,7 +10,7 @@ import com.typesafe.scalalogging.LazyLogging import edu.ie3.datamodel.models.input.system.PvInput import edu.ie3.datamodel.models.result.system.PvResult import edu.ie3.simona.agent.participant.data.Data.PrimaryData.ApparentPower -import edu.ie3.simona.agent.participant.data.Data.SecondaryData +import edu.ie3.simona.agent.participant.data.Data import edu.ie3.simona.exceptions.CriticalFailureException import edu.ie3.simona.model.SystemComponent import edu.ie3.simona.model.participant.control.QControl @@ -57,8 +57,8 @@ final class PvModel private ( ConstantState.type, PvRelevantData, ] - with ParticipantSimpleFlexibility[ConstantState.type, PvRelevantData] with ParticipantConstantModel[ActivePowerOperatingPoint, PvRelevantData] + with ParticipantSimpleFlexibility[ConstantState.type, PvRelevantData] with LazyLogging { /** Override sMax as the power output of a pv unit could become easily up to @@ -740,11 +740,11 @@ final class PvModel private ( ) } - def getRequiredServices: Iterable[ServiceType] = + override def getRequiredServices: Iterable[ServiceType] = Iterable(ServiceType.WeatherService) - def createRelevantData( - receivedData: Seq[SecondaryData], + override def createRelevantData( + receivedData: Seq[Data], nodalVoltage: Dimensionless, tick: Long, ): PvRelevantData = { From 2ac274d85aea6f0d5e57422bdced5ad73ca449fe Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Tue, 15 Oct 2024 18:27:52 +0200 Subject: [PATCH 08/77] Introducing PrimaryDataParticipantModel --- .../agent/participant2/ParticipantAgent.scala | 6 +---- .../participant2/ParticipantAgentInit.scala | 27 ++++++++++++++----- .../participant2/ParticipantModelInit.scala | 22 +++++++++++++++ ...cala => PrimaryDataParticipantModel.scala} | 27 ++++++++++--------- 4 files changed, 59 insertions(+), 23 deletions(-) rename src/main/scala/edu/ie3/simona/model/participant2/{PrimaryDataModel.scala => PrimaryDataParticipantModel.scala} (83%) diff --git a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgent.scala b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgent.scala index 25c1392983..c94c25c495 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgent.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgent.scala @@ -154,7 +154,7 @@ object ParticipantAgent { ) } - def apply( + private def apply( modelShell: ParticipantModelShell[_, _, _], inputHandler: ParticipantInputHandler, gridAdapter: ParticipantGridAdapter, @@ -371,8 +371,4 @@ object ParticipantAgent { } else false - private def primaryData(): Behavior[Request] = Behaviors.receivePartial { - case _ => Behaviors.same - } - } diff --git a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgentInit.scala b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgentInit.scala index 7e4061d256..fd2caad497 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgentInit.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgentInit.scala @@ -23,7 +23,6 @@ import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage.{ FlexCompletion, FlexRequest, FlexResponse, - ProvideFlexOptions, RegisterParticipant, ScheduleFlexRequest, } @@ -105,7 +104,7 @@ object ParticipantAgentInit { primaryServiceProxy ! PrimaryServiceRegistrationMessage( participantInput.getUuid ) - waitingForProxy( + waitingForPrimaryProxy( participantInput, config, simulationStartDate, @@ -114,7 +113,7 @@ object ParticipantAgentInit { ) } - private def waitingForProxy( + private def waitingForPrimaryProxy( participantInput: SystemParticipantInput, config: BaseRuntimeConfig, simulationStartDate: ZonedDateTime, @@ -136,7 +135,23 @@ object ParticipantAgentInit { ), ) - primaryData() + // todo T parameter, receive from primary proxy + val model = ParticipantModelInit.createPrimaryModel( + participantInput, + config.scaling, + simulationStartDate, + simulationEndDate, + ) + + val expectedFirstData = Map(serviceRef -> nextDataTick) + + ParticipantAgent( + model, + expectedFirstData, + ???, + ???, + parentData, + ) case (_, RegistrationFailedMessage(serviceRef)) => val model = ParticipantModelInit.createModel( @@ -149,14 +164,14 @@ object ParticipantAgentInit { if (requiredServices.isEmpty) { ParticipantAgent(model, Map.empty, ???, ???, parentData) } else { - waitingForServices(model) + waitingForServices(model, requiredServices, parentData = parentData) } } private def waitingForServices( model: ParticipantModel[_, _, _], expectedRegistrations: Set[ClassicRef], - expectedFirstData: Map[ClassicRef, Long], + expectedFirstData: Map[ClassicRef, Long] = Map.empty, parentData: Either[SchedulerData, FlexControlledData], ): Behavior[Request] = Behaviors.receivePartial { diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelInit.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelInit.scala index 689af3083b..248f745724 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelInit.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelInit.scala @@ -7,6 +7,7 @@ package edu.ie3.simona.model.participant2 import edu.ie3.datamodel.models.input.system.{PvInput, SystemParticipantInput} +import edu.ie3.simona.agent.participant.data.Data.PrimaryData import java.time.ZonedDateTime @@ -23,4 +24,25 @@ object ParticipantModelInit { PvModel(pvInput, scalingFactor, simulationStartDate, simulationEndDate) } + def createPrimaryModel[T <: PrimaryData]( + participantInput: SystemParticipantInput, + scalingFactor: Double, + simulationStartDate: ZonedDateTime, + simulationEndDate: ZonedDateTime, + ): PrimaryDataParticipantModel[T] = { + // Create a fitting physical model to extract parameters from + val physicalModel = createModel( + participantInput, + scalingFactor, + simulationStartDate, + simulationEndDate, + ) + + new PrimaryDataParticipantModel[T]( + physicalModel.uuid, + physicalModel.sRated, + physicalModel.cosPhiRated, + physicalModel.qControl, + ) + } } diff --git a/src/main/scala/edu/ie3/simona/model/participant2/PrimaryDataModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/PrimaryDataParticipantModel.scala similarity index 83% rename from src/main/scala/edu/ie3/simona/model/participant2/PrimaryDataModel.scala rename to src/main/scala/edu/ie3/simona/model/participant2/PrimaryDataParticipantModel.scala index bbe638120e..6bf0f4b27b 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/PrimaryDataModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/PrimaryDataParticipantModel.scala @@ -8,10 +8,7 @@ package edu.ie3.simona.model.participant2 import edu.ie3.simona.agent.participant.data.Data import edu.ie3.simona.agent.participant.data.Data.PrimaryData -import edu.ie3.simona.agent.participant.data.Data.PrimaryData.{ - ActivePower, - PrimaryDataWithApparentPower, -} +import edu.ie3.simona.agent.participant.data.Data.PrimaryData.PrimaryDataWithApparentPower import edu.ie3.simona.exceptions.CriticalFailureException import edu.ie3.simona.model.participant.control.QControl import edu.ie3.simona.model.participant2.ParticipantModel.{ @@ -20,7 +17,10 @@ import edu.ie3.simona.model.participant2.ParticipantModel.{ OperationRelevantData, ParticipantConstantModel, } -import edu.ie3.simona.model.participant2.PrimaryDataModel.PrimaryOperationRelevantData +import edu.ie3.simona.model.participant2.PrimaryDataParticipantModel.{ + PrimaryOperatingPoint, + PrimaryOperationRelevantData, +} import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage import edu.ie3.simona.service.ServiceType import edu.ie3.util.scala.quantities.ReactivePower @@ -32,28 +32,31 @@ import scala.reflect.ClassTag /** Just "replaying" primary data */ -final case class PrimaryDataModel[T <: PrimaryData: ClassTag]( +final case class PrimaryDataParticipantModel[T <: PrimaryData: ClassTag]( override val uuid: UUID, override val sRated: Power, override val cosPhiRated: Double, override val qControl: QControl, ) extends ParticipantModel[ - OperatingPoint, + PrimaryOperatingPoint[T], ConstantState.type, PrimaryOperationRelevantData[T], ] - with ParticipantConstantModel[OperatingPoint, PrimaryOperationRelevantData[ + with ParticipantConstantModel[PrimaryOperatingPoint[ + T + ], PrimaryOperationRelevantData[ T ]] { override def determineOperatingPoint( state: ParticipantModel.ConstantState.type, relevantData: PrimaryOperationRelevantData[T], - ): (OperatingPoint, Option[Long]) = ??? + ): (PrimaryOperatingPoint[T], Option[Long]) = + (PrimaryOperatingPoint(relevantData.data), None) override def createResults( lastState: ParticipantModel.ConstantState.type, - operatingPoint: OperatingPoint, + operatingPoint: PrimaryOperatingPoint[T], complexPower: PrimaryData.ApparentPower, dateTime: ZonedDateTime, ): ParticipantModel.ResultsContainer = ??? @@ -88,10 +91,10 @@ final case class PrimaryDataModel[T <: PrimaryData: ClassTag]( override def handlePowerControl( flexOptions: FlexibilityMessage.ProvideFlexOptions, setPower: Power, - ): (OperatingPoint, ParticipantModel.ModelChangeIndicator) = ??? + ): (PrimaryOperatingPoint[T], ParticipantModel.ModelChangeIndicator) = ??? } -object PrimaryDataModel { +object PrimaryDataParticipantModel { final case class PrimaryOperationRelevantData[+T <: PrimaryData](data: T) extends OperationRelevantData From d2e8babbcef0bd6702103b2e2abcef7a6c296863 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Tue, 22 Oct 2024 13:53:40 +0200 Subject: [PATCH 09/77] Backup --- .../participant2/ParticipantFlexibility.scala | 9 +- .../model/participant2/ParticipantModel.scala | 43 ++- .../participant2/ParticipantModelInit.scala | 11 + .../participant2/ParticipantModelShell.scala | 10 +- .../PrimaryDataParticipantModel.scala | 55 ++- .../simona/model/participant2/PvModel.scala | 44 ++- .../model/participant2/StorageModel.scala | 345 ++++++++++++++++++ .../evcs/EvcsChargingProperties.scala | 24 ++ .../model/participant2/evcs/EvcsModel.scala | 334 +++++++++++++++++ .../evcs/MaximumPowerStrategy.scala | 39 ++ 10 files changed, 884 insertions(+), 30 deletions(-) create mode 100644 src/main/scala/edu/ie3/simona/model/participant2/StorageModel.scala create mode 100644 src/main/scala/edu/ie3/simona/model/participant2/evcs/EvcsChargingProperties.scala create mode 100644 src/main/scala/edu/ie3/simona/model/participant2/evcs/EvcsModel.scala create mode 100644 src/main/scala/edu/ie3/simona/model/participant2/evcs/MaximumPowerStrategy.scala diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantFlexibility.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantFlexibility.scala index 06cf160b4e..0a96f08c90 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantFlexibility.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantFlexibility.scala @@ -29,6 +29,7 @@ trait ParticipantFlexibility[ def calcFlexOptions(state: S, relevantData: OR): ProvideFlexOptions def handlePowerControl( + state: S, flexOptions: ProvideFlexOptions, setPower: Power, ): (OP, ModelChangeIndicator) @@ -43,14 +44,18 @@ object ParticipantFlexibility { ] extends ParticipantFlexibility[ActivePowerOperatingPoint, S, OR] { this: ParticipantModel[ActivePowerOperatingPoint, S, OR] => - def calcFlexOptions(state: S, relevantData: OR): ProvideFlexOptions = { + override def calcFlexOptions( + state: S, + relevantData: OR, + ): ProvideFlexOptions = { val (operatingPoint, _) = determineOperatingPoint(state, relevantData) val power = operatingPoint.activePower ProvideMinMaxFlexOptions(uuid, power, power, DefaultQuantities.zeroKW) } - def handlePowerControl( + override def handlePowerControl( + state: S, flexOptions: ProvideFlexOptions, setPower: Power, ): (ActivePowerOperatingPoint, ModelChangeIndicator) = { diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala index d4027488c6..67e759c7ea 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala @@ -7,7 +7,10 @@ package edu.ie3.simona.model.participant2 import edu.ie3.datamodel.models.result.system.SystemParticipantResult -import edu.ie3.simona.agent.participant.data.Data.PrimaryData.ApparentPower +import edu.ie3.simona.agent.participant.data.Data.PrimaryData.{ + ApparentPower, + PrimaryDataWithApparentPower, +} import edu.ie3.simona.agent.participant.data.Data import edu.ie3.simona.model.participant2.ParticipantModel.{ ModelState, @@ -53,16 +56,48 @@ abstract class ParticipantModel[ nodalVoltage, ) + /** With the given current state and the given relevant data, determines the + * operating point that is currently valid until the next operating point is + * determined. Also, optionally returns a tick at which the state will change + * unless the operating point changes beforehand. + * + * This method is only called if the participant is *not* em-controlled. + * + * @param state + * the current state + * @param relevantData + * the relevant data for the current tick + * @return + * the operating point and optionally a next activation tick + */ def determineOperatingPoint(state: S, relevantData: OR): (OP, Option[Long]) + /** Determines the current state given the last state and the operating point + * that has been valid from the last state up until now. + * + * @param lastState + * the last state + * @param operatingPoint + * the operating point valid from the simulation time of the last state up + * until now + * @param currentTick + * the current tick + * @return + * the current state + */ def determineState(lastState: S, operatingPoint: OP, currentTick: Long): S def createResults( - lastState: S, + state: S, operatingPoint: OP, complexPower: ApparentPower, dateTime: ZonedDateTime, - ): ResultsContainer + ): Iterable[SystemParticipantResult] + + def createPrimaryDataResult( + data: PrimaryDataWithApparentPower[_], + dateTime: ZonedDateTime, + ): SystemParticipantResult /** Handling requests that are not part of the standard participant protocol * @@ -154,7 +189,7 @@ object ParticipantModel { final case class ResultsContainer( totalPower: ApparentPower, - modelResults: Seq[SystemParticipantResult], + modelResults: Iterable[SystemParticipantResult], ) } diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelInit.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelInit.scala index 248f745724..142a3ab161 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelInit.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelInit.scala @@ -7,7 +7,9 @@ package edu.ie3.simona.model.participant2 import edu.ie3.datamodel.models.input.system.{PvInput, SystemParticipantInput} +import edu.ie3.datamodel.models.result.system.SystemParticipantResult import edu.ie3.simona.agent.participant.data.Data.PrimaryData +import edu.ie3.simona.model.participant2.PrimaryDataParticipantModel.PrimaryResultFunc import java.time.ZonedDateTime @@ -38,11 +40,20 @@ object ParticipantModelInit { simulationEndDate, ) + val primaryResultFunc = new PrimaryResultFunc[T] { + override def createResult( + data: T with PrimaryData.PrimaryDataWithApparentPower[_], + dateTime: ZonedDateTime, + ): SystemParticipantResult = + physicalModel.createPrimaryDataResult(data) + } + new PrimaryDataParticipantModel[T]( physicalModel.uuid, physicalModel.sRated, physicalModel.cosPhiRated, physicalModel.qControl, + primaryResultFunc, ) } } diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala index 5383f3227f..af71c65685 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala @@ -100,13 +100,19 @@ final case class ParticipantModelShell[ val reactivePower = op.reactivePower.getOrElse( activeToReactivePowerFunc(nodalVoltage)(activePower) ) + val complexPower = ApparentPower(activePower, reactivePower) - model.createResults( + val participantResults = model.createResults( state, op, - ApparentPower(activePower, reactivePower), + complexPower, ???, ) + + ResultsContainer( + complexPower, + participantResults, + ) } def updateFlexOptions(currentTick: Long): ParticipantModelShell[OP, S, OR] = { diff --git a/src/main/scala/edu/ie3/simona/model/participant2/PrimaryDataParticipantModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/PrimaryDataParticipantModel.scala index 6bf0f4b27b..1ab3611c78 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/PrimaryDataParticipantModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/PrimaryDataParticipantModel.scala @@ -6,9 +6,13 @@ package edu.ie3.simona.model.participant2 +import edu.ie3.datamodel.models.result.system.SystemParticipantResult import edu.ie3.simona.agent.participant.data.Data import edu.ie3.simona.agent.participant.data.Data.PrimaryData -import edu.ie3.simona.agent.participant.data.Data.PrimaryData.PrimaryDataWithApparentPower +import edu.ie3.simona.agent.participant.data.Data.PrimaryData.{ + EnrichableData, + PrimaryDataWithApparentPower, +} import edu.ie3.simona.exceptions.CriticalFailureException import edu.ie3.simona.model.participant.control.QControl import edu.ie3.simona.model.participant2.ParticipantModel.{ @@ -18,8 +22,11 @@ import edu.ie3.simona.model.participant2.ParticipantModel.{ ParticipantConstantModel, } import edu.ie3.simona.model.participant2.PrimaryDataParticipantModel.{ + PrimaryActivePowerOperatingPoint, + PrimaryApparentPowerOperatingPoint, PrimaryOperatingPoint, PrimaryOperationRelevantData, + PrimaryResultFunc, } import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage import edu.ie3.simona.service.ServiceType @@ -37,6 +44,7 @@ final case class PrimaryDataParticipantModel[T <: PrimaryData: ClassTag]( override val sRated: Power, override val cosPhiRated: Double, override val qControl: QControl, + primaryDataResultFunc: PrimaryResultFunc[T], ) extends ParticipantModel[ PrimaryOperatingPoint[T], ConstantState.type, @@ -55,13 +63,26 @@ final case class PrimaryDataParticipantModel[T <: PrimaryData: ClassTag]( (PrimaryOperatingPoint(relevantData.data), None) override def createResults( - lastState: ParticipantModel.ConstantState.type, + state: ParticipantModel.ConstantState.type, operatingPoint: PrimaryOperatingPoint[T], complexPower: PrimaryData.ApparentPower, dateTime: ZonedDateTime, - ): ParticipantModel.ResultsContainer = ??? + ): Iterable[SystemParticipantResult] = { + val primaryDataWithApparentPower = operatingPoint match { + case PrimaryApparentPowerOperatingPoint(data) => + data + case PrimaryActivePowerOperatingPoint(data) => + data.add(complexPower.q) + } + Iterable( + primaryDataResultFunc.createResult(primaryDataWithApparentPower, dateTime) + ) + } - override def getRequiredServices: Iterable[ServiceType] = ??? + override def getRequiredServices: Iterable[ServiceType] = { + // primary service should not be specified here + Iterable.empty + } /** @param receivedData * @throws CriticalFailureException @@ -89,9 +110,17 @@ final case class PrimaryDataParticipantModel[T <: PrimaryData: ClassTag]( ): FlexibilityMessage.ProvideFlexOptions = ??? override def handlePowerControl( + state: ParticipantModel.ConstantState.type, flexOptions: FlexibilityMessage.ProvideFlexOptions, setPower: Power, ): (PrimaryOperatingPoint[T], ParticipantModel.ModelChangeIndicator) = ??? + + override def createPrimaryDataResult( + data: PrimaryDataWithApparentPower[_], + dateTime: ZonedDateTime, + ): SystemParticipantResult = throw new CriticalFailureException( + "Method not implemented by this model." + ) } object PrimaryDataParticipantModel { @@ -123,9 +152,25 @@ object PrimaryDataParticipantModel { override val reactivePower: Option[ReactivePower] = Some(data.q) } - private final case class PrimaryActivePowerOperatingPoint[+T <: PrimaryData]( + private final case class PrimaryActivePowerOperatingPoint[ + +T <: PrimaryData with EnrichableData[T2], + T2 <: T with PrimaryDataWithApparentPower[T2], + ]( override val data: T ) extends PrimaryOperatingPoint[T] { override val reactivePower: Option[ReactivePower] = None } + + /** Function needs to be packaged to be store it in a val + * @tparam T + */ + trait PrimaryResultFunc[ + T <: PrimaryData + ] { + def createResult( + data: T with PrimaryDataWithApparentPower[_], + dateTime: ZonedDateTime, + ): SystemParticipantResult + } + } diff --git a/src/main/scala/edu/ie3/simona/model/participant2/PvModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/PvModel.scala index 0d824d3cdb..e2904c6d3d 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/PvModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/PvModel.scala @@ -8,9 +8,13 @@ package edu.ie3.simona.model.participant2 import com.typesafe.scalalogging.LazyLogging import edu.ie3.datamodel.models.input.system.PvInput -import edu.ie3.datamodel.models.result.system.PvResult +import edu.ie3.datamodel.models.result.system.{ + PvResult, + SystemParticipantResult, +} import edu.ie3.simona.agent.participant.data.Data.PrimaryData.ApparentPower import edu.ie3.simona.agent.participant.data.Data +import edu.ie3.simona.agent.participant.data.Data.PrimaryData import edu.ie3.simona.exceptions.CriticalFailureException import edu.ie3.simona.model.SystemComponent import edu.ie3.simona.model.participant.control.QControl @@ -20,7 +24,6 @@ import edu.ie3.simona.model.participant2.ParticipantModel.{ ConstantState, OperationRelevantData, ParticipantConstantModel, - ResultsContainer, } import edu.ie3.simona.model.participant2.PvModel.PvRelevantData import edu.ie3.simona.ontology.messages.services.WeatherMessage.WeatherData @@ -40,7 +43,7 @@ import java.util.UUID import java.util.stream.IntStream import scala.math._ -final class PvModel private ( +class PvModel private ( override val uuid: UUID, override val sRated: Power, override val cosPhiRated: Double, @@ -722,23 +725,30 @@ final class PvModel private ( } override def createResults( - lastState: ParticipantModel.ConstantState.type, + state: ParticipantModel.ConstantState.type, operatingPoint: ActivePowerOperatingPoint, complexPower: ApparentPower, dateTime: ZonedDateTime, - ): ResultsContainer = { - ResultsContainer( - complexPower, - Seq( - new PvResult( - dateTime, - uuid, - complexPower.p.toMegawatts.asMegaWatt, - complexPower.q.toMegavars.asMegaVar, - ) - ), + ): Iterable[SystemParticipantResult] = + Iterable( + new PvResult( + dateTime, + uuid, + complexPower.p.toMegawatts.asMegaWatt, + complexPower.q.toMegavars.asMegaVar, + ) + ) + + override def createPrimaryDataResult( + data: PrimaryData.PrimaryDataWithApparentPower[_], + dateTime: ZonedDateTime, + ): SystemParticipantResult = + new PvResult( + dateTime, + uuid, + data.p.toMegawatts.asMegaWatt, + data.q.toMegavars.asMegaVar, ) - } override def getRequiredServices: Iterable[ServiceType] = Iterable(ServiceType.WeatherService) @@ -750,7 +760,7 @@ final class PvModel private ( ): PvRelevantData = { receivedData .collectFirst { case weatherData: WeatherData => - PvRelevantData(weatherData.diffIrr, weatherData.dirIrr) + PvRelevantData(???, ???, weatherData.diffIrr, weatherData.dirIrr) } .getOrElse { throw new CriticalFailureException( diff --git a/src/main/scala/edu/ie3/simona/model/participant2/StorageModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/StorageModel.scala new file mode 100644 index 0000000000..eaf64fe7a8 --- /dev/null +++ b/src/main/scala/edu/ie3/simona/model/participant2/StorageModel.scala @@ -0,0 +1,345 @@ +/* + * © 2024. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.model.participant2 + +import edu.ie3.datamodel.models.result.system.{ + StorageResult, + SystemParticipantResult, +} +import edu.ie3.simona.agent.participant.data.Data +import edu.ie3.simona.agent.participant.data.Data.PrimaryData +import edu.ie3.simona.exceptions.CriticalFailureException +import edu.ie3.simona.model.participant.StorageModel.RefTargetSocParams +import edu.ie3.simona.model.participant.control.QControl +import edu.ie3.simona.model.participant2.ParticipantModel.{ + ActivePowerOperatingPoint, + ModelState, + OperationRelevantData, +} +import edu.ie3.simona.model.participant2.StorageModel.{ + StorageRelevantData, + StorageState, +} +import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage +import edu.ie3.simona.ontology.messages.flex.MinMaxFlexibilityMessage.ProvideMinMaxFlexOptions +import edu.ie3.simona.service.ServiceType +import edu.ie3.util.quantities.QuantityUtils.RichQuantityDouble +import edu.ie3.util.scala.quantities.DefaultQuantities.{zeroKW, zeroKWH} +import squants.{Dimensionless, Energy, Power, Seconds} + +import java.time.ZonedDateTime +import java.util.UUID + +class StorageModel private ( + override val uuid: UUID, + override val sRated: Power, + override val cosPhiRated: Double, + override val qControl: QControl, + eStorage: Energy, + pMax: Power, + eta: Dimensionless, + targetSoc: Option[Double], +) extends ParticipantModel[ + ActivePowerOperatingPoint, + StorageState, + StorageRelevantData, + ] { + + private val minEnergy = zeroKWH + + /** Tolerance for power comparisons. With very small (dis-)charging powers, + * problems can occur when calculating the future tick at which storage is + * full or empty. For sufficiently large time frames, the maximum Long value + * ([[Long.MaxValue]]) can be exceeded, thus the Long value overflows and we + * get undefined behavior. + * + * Thus, small (dis-)charging powers compared to storage capacity have to be + * set to zero. The given tolerance value below amounts to 1 W for 1 GWh + * storage capacity and is sufficient in preventing Long overflows. + */ + private implicit val powerTolerance: Power = eStorage / Seconds(1) / 3.6e12 + + /** In order to avoid faulty behavior of storages, we want to avoid offering + * charging/discharging when storage is very close to full, to empty or to a + * target. + * + * In particular, we want to avoid offering the option to (dis-)charge if + * that operation could last less than our smallest possible time step, which + * is one second. Thus, we establish a safety margin of the energy + * (dis-)charged with maximum power in one second. + */ + private val toleranceMargin: Energy = pMax * Seconds(1d) + + /** Minimal allowed energy with tolerance margin added + */ + private val minEnergyWithMargin: Energy = + minEnergy + (toleranceMargin / eta.toEach) + + /** Maximum allowed energy with tolerance margin added + */ + private val maxEnergyWithMargin: Energy = + eStorage - (toleranceMargin * eta.toEach) + + private val refTargetSoc: Option[RefTargetSocParams] = targetSoc.map { + target => + val targetEnergy = eStorage * target + + val targetWithPosMargin = + targetEnergy + (toleranceMargin / eta.toEach) + + val targetWithNegMargin = + targetEnergy - (toleranceMargin * eta.toEach) + + RefTargetSocParams( + targetEnergy, + targetWithPosMargin, + targetWithNegMargin, + ) + } + + override def determineOperatingPoint( + state: StorageState, + relevantData: StorageRelevantData, + ): (ActivePowerOperatingPoint, Option[Long]) = + throw new CriticalFailureException( + "Storage model cannot calculate operation point without flexibility control." + ) + + override def determineState( + lastState: StorageState, + operatingPoint: ActivePowerOperatingPoint, + currentTick: Long, + ): StorageState = { + val timespan = Seconds(currentTick - lastState.tick) + val netPower = calcNetPower(operatingPoint.activePower) + val energyChange = netPower * timespan + + // don't allow under- or overcharge e.g. due to tick rounding error + val currentEnergy = + minEnergy.max(eStorage.min(lastState.storedEnergy + energyChange)) + + StorageState(currentEnergy, currentTick) + } + + override def createResults( + state: StorageState, + operatingPoint: ActivePowerOperatingPoint, + complexPower: PrimaryData.ApparentPower, + dateTime: ZonedDateTime, + ): Iterable[SystemParticipantResult] = + Iterable( + new StorageResult( + dateTime, + uuid, + complexPower.p.toMegawatts.asMegaWatt, + complexPower.q.toMegavars.asMegaVar, + (state.storedEnergy / eStorage).asPu, + ) + ) + + override def createPrimaryDataResult( + data: PrimaryData.PrimaryDataWithApparentPower[_], + dateTime: ZonedDateTime, + ): SystemParticipantResult = + new StorageResult( + dateTime, + uuid, + data.p.toMegawatts.asMegaWatt, + data.q.toMegavars.asMegaVar, + (-1).asPu, // FIXME currently not supported + ) + + override def getRequiredServices: Iterable[ServiceType] = Iterable.empty + + override def getInitialState(): StorageState = + StorageState(storedEnergy = zeroKWH, -1L) + + override def createRelevantData( + receivedData: Seq[Data], + nodalVoltage: Dimensionless, + tick: Long, + ): StorageRelevantData = { + if (receivedData.nonEmpty) { + throw new CriticalFailureException( + s"Expected no received data, got $receivedData" + ) + } + + StorageRelevantData(tick) + } + + override def calcFlexOptions( + state: StorageState, + relevantData: StorageRelevantData, + ): FlexibilityMessage.ProvideFlexOptions = { + + val chargingPossible = !isFull(state.storedEnergy) + val dischargingPossible = !isEmpty(state.storedEnergy) + + val refPower = refTargetSoc + .map { targetParams => + if (state.storedEnergy <= targetParams.targetWithPosMargin) { + if (state.storedEnergy >= targetParams.targetWithNegMargin) { + // is within target +/- margin, no charging needed + zeroKW + } else { + // below target - margin, charge up to target + pMax + } + } else { + // above target + margin, discharge to target + pMax * (-1d) + } + } + .getOrElse { + // no target set + zeroKW + } + + ProvideMinMaxFlexOptions( + uuid, + refPower, + if (dischargingPossible) pMax * (-1) else zeroKW, + if (chargingPossible) pMax else zeroKW, + ) + } + + override def handlePowerControl( + state: StorageState, + flexOptions: FlexibilityMessage.ProvideFlexOptions, + setPower: Power, + ): (ActivePowerOperatingPoint, ParticipantModel.ModelChangeIndicator) = { + val adaptedSetPower = + if ( + // if power is close to zero, set it to zero + (setPower ~= zeroKW) + // do not keep charging if we're already full (including safety margin) + || (setPower > zeroKW && isFull(state.storedEnergy)) + // do not keep discharging if we're already empty (including safety margin) + || (setPower < zeroKW && isEmpty(state.storedEnergy)) + ) + zeroKW + else + setPower + + // net power after considering efficiency + val netPower = calcNetPower(adaptedSetPower) + + // if the storage is at minimum or maximum charged energy AND we are charging + // or discharging, flex options will be different at the next activation + val isEmptyOrFull = + isEmpty(state.storedEnergy) || isFull(state.storedEnergy) + // if target soc is enabled, we can also be at that exact point + val isAtTarget = refTargetSoc.exists { targetParams => + state.storedEnergy <= targetParams.targetWithPosMargin && + state.storedEnergy >= targetParams.targetWithNegMargin + } + val isChargingOrDischarging = netPower != zeroKW + // 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. + // Similarly, if the ref target margin area is hit before hitting target SOC, we want + // to refresh flex options. + val hasObsoleteFlexOptions = + (isFull(state.storedEnergy) && setPower > zeroKW) || + (isEmpty(state.storedEnergy) && setPower < zeroKW) || + (isAtTarget && setPower != zeroKW) + + val activateAtNextTick = + ((isEmptyOrFull || isAtTarget) && isChargingOrDischarging) || hasObsoleteFlexOptions + + // calculate the time span until we're full or empty, if applicable + val maybeTimeSpan = + if (!isChargingOrDischarging) { + // we're at 0 kW, do nothing + None + } else if (netPower > zeroKW) { + // we're charging, calculate time until we're full or at target energy + + val closestEnergyTarget = refTargetSoc + .flatMap { targetParams => + Option.when( + state.storedEnergy <= targetParams.targetWithNegMargin + )(targetParams.targetSoc) + } + .getOrElse(eStorage) + + val energyToFull = closestEnergyTarget - state.storedEnergy + Some(energyToFull / netPower) + } else { + // we're discharging, calculate time until we're at lowest energy allowed or at target energy + + val closestEnergyTarget = refTargetSoc + .flatMap { targetParams => + Option.when( + state.storedEnergy >= targetParams.targetWithPosMargin + )(targetParams.targetSoc) + } + .getOrElse(minEnergy) + + val energyToEmpty = state.storedEnergy - closestEnergyTarget + Some(energyToEmpty / (netPower * (-1))) + } + + // calculate the tick from time span + val maybeNextTick = maybeTimeSpan.map { timeSpan => + val timeSpanTicks = Math.round(timeSpan.toSeconds) + state.tick + timeSpanTicks + } + + ( + ActivePowerOperatingPoint(adaptedSetPower), + ParticipantModel.ModelChangeIndicator(activateAtNextTick, maybeNextTick), + ) + } + + private def calcNetPower(setPower: Power): Power = + if (setPower > zeroKW) { + // multiply eta if we're charging + setPower * eta.toEach + } else { + // divide by eta if we're discharging + // (draining the battery more than we get as output) + setPower / eta.toEach + } + + /** @param storedEnergy + * the stored energy amount to check + * @return + * whether the given stored energy is greater than the maximum charged + * energy allowed (minus a tolerance margin) + */ + private def isFull(storedEnergy: Energy): Boolean = + storedEnergy >= maxEnergyWithMargin + + /** @param storedEnergy + * the stored energy amount to check + * @return + * whether the given stored energy is less than the minimal charged energy + * allowed (plus a tolerance margin) + */ + private def isEmpty(storedEnergy: Energy): Boolean = + storedEnergy <= minEnergyWithMargin + +} + +object StorageModel { + final case class StorageRelevantData( + currentTick: Long + ) extends OperationRelevantData + + /** @param storedEnergy + * The amount of currently stored energy + * @param tick + * The tick at which this state is valid + */ + final case class StorageState( + storedEnergy: Energy, + tick: Long, + ) extends ModelState +} diff --git a/src/main/scala/edu/ie3/simona/model/participant2/evcs/EvcsChargingProperties.scala b/src/main/scala/edu/ie3/simona/model/participant2/evcs/EvcsChargingProperties.scala new file mode 100644 index 0000000000..c609f51c7a --- /dev/null +++ b/src/main/scala/edu/ie3/simona/model/participant2/evcs/EvcsChargingProperties.scala @@ -0,0 +1,24 @@ +/* + * © 2024. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.model.participant2.evcs + +import edu.ie3.datamodel.models.ElectricCurrentType +import edu.ie3.simona.model.participant.evcs.EvModelWrapper +import squants.Power + +trait EvcsChargingProperties { + + /** Charging station rated power + */ + val sRated: Power + + val currentType: ElectricCurrentType + + val lowestEvSoc: Double + + def getMaxAvailableChargingPower(ev: EvModelWrapper): Power +} diff --git a/src/main/scala/edu/ie3/simona/model/participant2/evcs/EvcsModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/evcs/EvcsModel.scala new file mode 100644 index 0000000000..a191da6a05 --- /dev/null +++ b/src/main/scala/edu/ie3/simona/model/participant2/evcs/EvcsModel.scala @@ -0,0 +1,334 @@ +/* + * © 2024. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.model.participant2.evcs + +import edu.ie3.datamodel.models.ElectricCurrentType +import edu.ie3.datamodel.models.result.system.{ + EvcsResult, + SystemParticipantResult, +} +import edu.ie3.simona.agent.participant.data.Data +import edu.ie3.simona.agent.participant.data.Data.PrimaryData +import edu.ie3.simona.exceptions.CriticalFailureException +import edu.ie3.simona.model.participant.control.QControl +import edu.ie3.simona.model.participant.evcs.EvModelWrapper +import edu.ie3.simona.model.participant2.ParticipantModel +import edu.ie3.simona.model.participant2.ParticipantModel.{ + ModelState, + OperatingPoint, + OperationRelevantData, +} +import edu.ie3.simona.model.participant2.evcs.EvcsModel.{ + ChargingStrategy, + EvcsOperatingPoint, + EvcsRelevantData, + EvcsState, +} +import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage +import edu.ie3.simona.ontology.messages.flex.MinMaxFlexibilityMessage.ProvideMinMaxFlexOptions +import edu.ie3.simona.ontology.messages.services.EvMessage.ArrivingEvs +import edu.ie3.simona.service.ServiceType +import edu.ie3.util.quantities.QuantityUtils.RichQuantityDouble +import edu.ie3.util.scala.quantities.DefaultQuantities._ +import edu.ie3.util.scala.quantities.ReactivePower +import squants.energy.Watts +import squants.time.Seconds +import squants.{Dimensionless, Energy, Power} + +import java.time.ZonedDateTime +import java.util.UUID + +class EvcsModel private ( + override val uuid: UUID, + override val sRated: Power, + override val cosPhiRated: Double, + override val qControl: QControl, + strategy: ChargingStrategy, + override val currentType: ElectricCurrentType, + override val lowestEvSoc: Double, + vehicle2grid: Boolean, +) extends ParticipantModel[ + EvcsOperatingPoint, + EvcsState, + EvcsRelevantData, + ] + with EvcsChargingProperties { + override def determineOperatingPoint( + state: EvcsState, + relevantData: EvcsRelevantData, + ): (EvcsOperatingPoint, Option[Long]) = { + val chargingPowers = + strategy.determineChargingPowers(state.evs.values, state.tick, this) + + val nextEvent = chargingPowers.flatMap { case (uuid, power) => + val ev = state.evs.getOrElse( + uuid, + throw new CriticalFailureException( + s"Charging strategy ${strategy.getClass.getSimpleName} returned a charging power for unknown UUID $uuid" + ), + ) + + determineNextEvent(ev, power) + }.minOption + + (EvcsOperatingPoint(chargingPowers), nextEvent) + } + + private def determineNextEvent( + ev: EvModelWrapper, + chargingPower: Power, + ): Option[Long] = { + implicit val tolerance: Power = Watts(1e-3) + if (chargingPower ~= zeroKW) + None + else { + val timeUntilFullOrEmpty = + if (chargingPower > zeroKW) { + + // if we're below lowest SOC, flex options will change at that point + val targetEnergy = + if (isEmpty(ev) && !isInLowerMargin(ev)) + ev.eStorage * lowestEvSoc + else + ev.eStorage + + (targetEnergy - ev.storedEnergy) / chargingPower + } else + (ev.storedEnergy - (ev.eStorage * lowestEvSoc)) / (chargingPower * (-1)) + + Some(Math.round(timeUntilFullOrEmpty.toSeconds)) + } + } + + override def determineState( + lastState: EvcsState, + operatingPoint: EvcsOperatingPoint, + currentTick: Long, + ): EvcsState = { + + val updatedEvs = lastState.evs.map { case (uuid, ev) => + uuid -> + operatingPoint.evOperatingPoints + .get(uuid) + .map { chargingPower => + val newStoredEnergy = ev.storedEnergy + + chargingPower * Seconds( + currentTick - lastState.tick + ) + ev.copy(storedEnergy = newStoredEnergy) + } + .getOrElse(ev) + } + + EvcsState(updatedEvs, currentTick) + } + + override def createResults( + state: EvcsState, + operatingPoint: EvcsOperatingPoint, + complexPower: PrimaryData.ApparentPower, + dateTime: ZonedDateTime, + ): Iterable[SystemParticipantResult] = ??? + + override def createPrimaryDataResult( + data: PrimaryData.PrimaryDataWithApparentPower[_], + dateTime: ZonedDateTime, + ): SystemParticipantResult = + new EvcsResult( + dateTime, + uuid, + data.p.toMegawatts.asMegaWatt, + data.q.toMegavars.asMegaVar, + ) + + override def getRequiredServices: Iterable[ServiceType] = + Iterable( + ServiceType.EvMovementService + ) + + override def getInitialState(): EvcsState = EvcsState(Map.empty, -1) + + override def createRelevantData( + receivedData: Seq[Data], + nodalVoltage: Dimensionless, + tick: Long, + ): EvcsRelevantData = { + receivedData + .collectFirst { case evData: ArrivingEvs => + EvcsRelevantData(tick, evData.arrivals) + } + .getOrElse { + throw new CriticalFailureException( + s"Expected ArrivingEvs, got $receivedData" + ) + } + } + + override def calcFlexOptions( + state: EvcsState, + relevantData: EvcsRelevantData, + ): FlexibilityMessage.ProvideFlexOptions = { + + val preferredPowers = + strategy.determineChargingPowers(state.evs.values, state.tick, this) + + val (maxCharging, preferredPower, forcedCharging, maxDischarging) = + state.evs.foldLeft( + (zeroKW, zeroKW, zeroKW, zeroKW) + ) { + case ( + (chargingSum, preferredSum, forcedSum, dischargingSum), + (uuid, ev), + ) => + val maxPower = getMaxAvailableChargingPower(ev) + + val preferredPower = preferredPowers.get(uuid) + + val maxCharging = + if (!isFull(ev)) + maxPower + else + zeroKW + + val forced = + if (isEmpty(ev) && !isInLowerMargin(ev)) + preferredPower.getOrElse(maxPower) + else + zeroKW + + val maxDischarging = + if (!isEmpty(ev) && vehicle2grid) + maxPower * -1 + else + zeroKW + + ( + chargingSum + maxCharging, + preferredSum + preferredPower.getOrElse(zeroKW), + forcedSum + forced, + dischargingSum + maxDischarging, + ) + } + + // if we need to charge at least one EV, we cannot discharge any other + val (adaptedMaxDischarging, adaptedPreferred) = + if (forcedCharging > zeroKW) + (forcedCharging, preferredPower.max(forcedCharging)) + else + (maxDischarging, preferredPower) + + ProvideMinMaxFlexOptions( + uuid, + adaptedPreferred, + adaptedMaxDischarging, + maxCharging, + ) + } + + override def handlePowerControl( + state: EvcsState, + flexOptions: FlexibilityMessage.ProvideFlexOptions, + setPower: Power, + ): (EvcsOperatingPoint, ParticipantModel.ModelChangeIndicator) = ??? + + /** @param ev + * the ev whose stored energy is to be checked + * @return + * whether the given ev's stored energy is greater than the maximum charged + * energy allowed (minus a tolerance margin) + */ + private def isFull(ev: EvModelWrapper): Boolean = + ev.storedEnergy >= (ev.eStorage - calcToleranceMargin(ev)) + + /** @param ev + * the ev whose stored energy is to be checked + * @return + * whether the given ev's stored energy is less than the minimal charged + * energy allowed (plus a tolerance margin) + */ + private def isEmpty(ev: EvModelWrapper): Boolean = + ev.storedEnergy <= ( + ev.eStorage * lowestEvSoc + 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: EvModelWrapper): Boolean = { + val toleranceMargin = calcToleranceMargin(ev) + val lowestSoc = ev.eStorage * lowestEvSoc + + ev.storedEnergy <= ( + lowestSoc + toleranceMargin + ) && ev.storedEnergy >= ( + lowestSoc - toleranceMargin + ) + } + + private def calcToleranceMargin(ev: EvModelWrapper): Energy = + getMaxAvailableChargingPower(ev) * Seconds(1) + + /** Returns the maximum available charging power for an EV, which depends on + * ev and charging station limits for AC and DC current + * + * @param ev + * ev for which the max charging power should be returned + * @return + * maximum charging power for the EV at this charging station + */ + override def getMaxAvailableChargingPower( + ev: EvModelWrapper + ): Power = { + val evPower = currentType match { + case ElectricCurrentType.AC => + ev.sRatedAc + case ElectricCurrentType.DC => + ev.sRatedDc + } + /* Limit the charging power to the minimum of ev's and evcs' permissible power */ + evPower.min(sRated) + } + +} + +object EvcsModel { + + final case class EvcsOperatingPoint(evOperatingPoints: Map[UUID, Power]) + extends OperatingPoint { + + override val activePower: Power = + evOperatingPoints.values.reduceOption(_ + _).getOrElse(zeroKW) + + override val reactivePower: Option[ReactivePower] = ??? + } + + /** @param evs + * TODO also save starting tick, so that results are not cluttered + * @param tick + */ + final case class EvcsState( + evs: Map[UUID, EvModelWrapper], + override val tick: Long, + ) extends ModelState + + final case class EvcsRelevantData( + tick: Long, + arrivals: Seq[EvModelWrapper], + ) extends OperationRelevantData + + trait ChargingStrategy { + def determineChargingPowers( + evs: Iterable[EvModelWrapper], + currentTick: Long, + chargingProps: EvcsChargingProperties, + ): Map[UUID, Power] + } + +} diff --git a/src/main/scala/edu/ie3/simona/model/participant2/evcs/MaximumPowerStrategy.scala b/src/main/scala/edu/ie3/simona/model/participant2/evcs/MaximumPowerStrategy.scala new file mode 100644 index 0000000000..0283c2888f --- /dev/null +++ b/src/main/scala/edu/ie3/simona/model/participant2/evcs/MaximumPowerStrategy.scala @@ -0,0 +1,39 @@ +/* + * © 2024. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.model.participant2.evcs + +import edu.ie3.simona.model.participant.evcs.EvModelWrapper +import edu.ie3.simona.model.participant2.evcs.EvcsModel.ChargingStrategy +import squants.Power + +import java.util.UUID + +object MaximumPowerStrategy extends ChargingStrategy { + + /** Determine scheduling for charging the EVs currently parked at the charging + * station until their departure. In this case, each EV is charged with + * maximum power from current time until it reaches either 100% SoC or its + * departure time. + * + * @param currentTick + * current tick + * @param evs + * currently parked evs at the charging station + * @return + * scheduling for charging the EVs + */ + def determineChargingPowers( + evs: Iterable[EvModelWrapper], + currentTick: Long, + chargingProps: EvcsChargingProperties, + ): Map[UUID, Power] = evs + .filter(ev => ev.storedEnergy < ev.eStorage) + .map { ev => + ev.uuid -> chargingProps.getMaxAvailableChargingPower(ev) + } + .toMap +} From 857dba5b4e38523b55cf8c5be1d2fa55c259b361 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Tue, 22 Oct 2024 18:27:25 +0200 Subject: [PATCH 10/77] Further dev --- .../agent/participant2/ParticipantAgent.scala | 38 ++++----- .../participant2/ParticipantAgentInit.scala | 73 ++++++++-------- .../participant2/ParticipantModelShell.scala | 84 ++++++++++++++----- .../PrimaryDataParticipantModel.scala | 35 ++++---- .../simona/model/participant2/PvModel.scala | 44 +++------- .../model/participant2/StorageModel.scala | 38 ++++++++- 6 files changed, 189 insertions(+), 123 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgent.scala b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgent.scala index c94c25c495..13fc5e94b8 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgent.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgent.scala @@ -7,7 +7,6 @@ package edu.ie3.simona.agent.participant2 import breeze.numerics.{pow, sqrt} -import edu.ie3.simona.agent.grid.GridAgent import edu.ie3.simona.agent.grid.GridAgentMessages.{ AssetPowerChangedMessage, AssetPowerUnchangedMessage, @@ -15,10 +14,7 @@ import edu.ie3.simona.agent.grid.GridAgentMessages.{ import edu.ie3.simona.agent.participant.data.Data import edu.ie3.simona.agent.participant.data.Data.SecondaryData import edu.ie3.simona.exceptions.CriticalFailureException -import edu.ie3.simona.model.participant2.{ - ParticipantModel, - ParticipantModelShell, -} +import edu.ie3.simona.model.participant2.ParticipantModelShell import edu.ie3.simona.ontology.messages.SchedulerMessage.Completion import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage._ import edu.ie3.simona.ontology.messages.services.ServiceMessage.ProvisionMessage @@ -140,21 +136,6 @@ object ParticipantAgent { } def apply( - model: ParticipantModel[_, _, _], - expectedData: Map[ClassicRef, Long], - gridAgentRef: ActorRef[GridAgent.Request], - expectedPowerRequestTick: Long, - parentData: Either[SchedulerData, FlexControlledData], - ): Behavior[Request] = { - ParticipantAgent( - ParticipantModelShell(model), - ParticipantInputHandler(expectedData), - ParticipantGridAdapter(gridAgentRef, expectedPowerRequestTick), - parentData, - ) - } - - private def apply( modelShell: ParticipantModelShell[_, _, _], inputHandler: ParticipantInputHandler, gridAdapter: ParticipantGridAdapter, @@ -332,7 +313,20 @@ object ParticipantAgent { case Flex(flexControl: IssueFlexControl) => val modelWithOP = shell.updateOperatingPoint(flexControl) - // todo results + val results = + modelWithOP.determineResults( + flexControl.tick, + gridAdapter.nodalVoltage, + ) + + results.modelResults.foreach { res => // todo send out results + } + + val gridAdapterWithResult = + gridAdapter.storePowerValue( + results.totalPower, + flexControl.tick, + ) parentData.fold( _ => @@ -346,7 +340,7 @@ object ParticipantAgent { ), ) - modelWithOP + (modelWithOP, gridAdapterWithResult) } } .get diff --git a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgentInit.scala b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgentInit.scala index fd2caad497..803007ba4a 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgentInit.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgentInit.scala @@ -7,29 +7,21 @@ package edu.ie3.simona.agent.participant2 import edu.ie3.datamodel.models.input.system.SystemParticipantInput +import edu.ie3.simona.agent.grid.GridAgent import edu.ie3.simona.agent.participant2.ParticipantAgent._ import edu.ie3.simona.config.SimonaConfig.BaseRuntimeConfig import edu.ie3.simona.exceptions.CriticalFailureException -import edu.ie3.simona.model.participant2.{ - ParticipantModel, - ParticipantModelInit, -} -import edu.ie3.simona.ontology.messages.{Activation, SchedulerMessage} +import edu.ie3.simona.model.participant2.ParticipantModelShell import edu.ie3.simona.ontology.messages.SchedulerMessage.{ Completion, ScheduleActivation, } -import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage.{ - FlexCompletion, - FlexRequest, - FlexResponse, - RegisterParticipant, - ScheduleFlexRequest, -} +import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage._ import edu.ie3.simona.ontology.messages.services.ServiceMessage.PrimaryServiceRegistrationMessage +import edu.ie3.simona.ontology.messages.{Activation, SchedulerMessage} import edu.ie3.simona.util.SimonaConstants.INIT_SIM_TICK -import org.apache.pekko.actor.typed.{ActorRef, Behavior} import org.apache.pekko.actor.typed.scaladsl.Behaviors +import org.apache.pekko.actor.typed.{ActorRef, Behavior} import org.apache.pekko.actor.{ActorRef => ClassicRef} import java.time.ZonedDateTime @@ -135,41 +127,43 @@ object ParticipantAgentInit { ), ) - // todo T parameter, receive from primary proxy - val model = ParticipantModelInit.createPrimaryModel( - participantInput, - config.scaling, - simulationStartDate, - simulationEndDate, - ) - val expectedFirstData = Map(serviceRef -> nextDataTick) - ParticipantAgent( - model, + createAgent( + ParticipantModelShell.createForPrimaryData( + participantInput, + config, + simulationStartDate, + simulationEndDate, + ), expectedFirstData, ???, ???, parentData, ) - case (_, RegistrationFailedMessage(serviceRef)) => - val model = ParticipantModelInit.createModel( + case (_, RegistrationFailedMessage(_)) => + val modelShell = ParticipantModelShell.createForModel( participantInput, - config.scaling, + config, simulationStartDate, simulationEndDate, ) - val requiredServices = model.getRequiredServices.toSeq + + val requiredServices = modelShell.model.getRequiredServices.toSeq if (requiredServices.isEmpty) { - ParticipantAgent(model, Map.empty, ???, ???, parentData) + createAgent(modelShell, Map.empty, ???, ???, parentData) } else { - waitingForServices(model, requiredServices, parentData = parentData) + waitingForServices( + modelShell, + requiredServices, + parentData = parentData, + ) } } private def waitingForServices( - model: ParticipantModel[_, _, _], + modelShell: ParticipantModelShell[_, _, _], expectedRegistrations: Set[ClassicRef], expectedFirstData: Map[ClassicRef, Long] = Map.empty, parentData: Either[SchedulerData, FlexControlledData], @@ -197,20 +191,33 @@ object ParticipantAgentInit { earliestNextTick, ), _.emAgent ! FlexCompletion( - model.uuid, + modelShell.model.uuid, requestAtNextActivation = false, earliestNextTick, ), ) - ParticipantAgent(model, newExpectedFirstData, ???, ???, parentData) + createAgent(modelShell, newExpectedFirstData, ???, ???, parentData) } else waitingForServices( - model, + modelShell, newExpectedRegistrations, newExpectedFirstData, parentData, ) } + def createAgent( + modelShell: ParticipantModelShell[_, _, _], + expectedData: Map[ClassicRef, Long], + gridAgentRef: ActorRef[GridAgent.Request], + expectedPowerRequestTick: Long, + parentData: Either[SchedulerData, FlexControlledData], + ): Behavior[Request] = + ParticipantAgent( + modelShell, + ParticipantInputHandler(expectedData), + ParticipantGridAdapter(gridAgentRef, expectedPowerRequestTick), + parentData, + ) } diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala index af71c65685..92cc47c202 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala @@ -6,11 +6,14 @@ package edu.ie3.simona.model.participant2 +import edu.ie3.datamodel.models.input.system.SystemParticipantInput import edu.ie3.simona.agent.participant.data.Data.PrimaryData.ApparentPower import edu.ie3.simona.agent.participant.data.Data.SecondaryData import edu.ie3.simona.agent.participant2.ParticipantAgent import edu.ie3.simona.agent.participant2.ParticipantAgent.ParticipantRequest +import edu.ie3.simona.config.SimonaConfig.BaseRuntimeConfig import edu.ie3.simona.exceptions.CriticalFailureException +import edu.ie3.simona.model.SystemComponent import edu.ie3.simona.model.em.EmTools import edu.ie3.simona.model.participant2.ParticipantModel.{ ModelChangeIndicator, @@ -23,11 +26,15 @@ import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage.{ IssueFlexControl, ProvideFlexOptions, } +import edu.ie3.simona.util.TickUtil.TickLong +import edu.ie3.util.scala.OperationInterval import edu.ie3.util.scala.quantities.ReactivePower import org.apache.pekko.actor.typed.scaladsl.ActorContext import squants.Dimensionless import squants.energy.Power +import java.time.ZonedDateTime + /** Takes care of: * - holding id information * - storing: @@ -41,8 +48,9 @@ final case class ParticipantModelShell[ S <: ModelState, OR <: OperationRelevantData, ]( - model: ParticipantModel[OP, S, OR] - with ParticipantFlexibility[OP, S, OR], // todo primary replay model? + model: ParticipantModel[OP, S, OR] with ParticipantFlexibility[OP, S, OR], + operationInterval: OperationInterval, + simulationStartDate: ZonedDateTime, state: S, relevantData: Option[OR], flexOptions: Option[ProvideFlexOptions], @@ -64,6 +72,8 @@ final case class ParticipantModelShell[ def updateOperatingPoint( currentTick: Long ): ParticipantModelShell[OP, S, OR] = { + // todo consider operationInterval + val currentState = determineCurrentState(currentTick) if (currentState.tick != currentTick) @@ -106,7 +116,7 @@ final case class ParticipantModelShell[ state, op, complexPower, - ???, + currentTick.toDateTime(simulationStartDate), ) ResultsContainer( @@ -140,19 +150,7 @@ final case class ParticipantModelShell[ ) val (newOperatingPoint, modelChange) = - model.handlePowerControl(fo, setPointActivePower) - - val activePower = newOperatingPoint.activePower - - // todo where store the reactive power? - val reactivePower = ??? - - val results = model.createResults( - state, - newOperatingPoint, - ApparentPower(activePower, reactivePower), - ???, - ) + model.handlePowerControl(currentState, fo, setPointActivePower) copy( state = currentState, @@ -184,16 +182,64 @@ final case class ParticipantModelShell[ object ParticipantModelShell { - def apply( - model: ParticipantModel[_, _, _] - ): ParticipantModelShell[_, _, _] = + def createForPrimaryData( + participantInput: SystemParticipantInput, + config: BaseRuntimeConfig, + simulationStartDate: ZonedDateTime, + simulationEndDate: ZonedDateTime, + ): ParticipantModelShell[_, _, _] = { + // todo T parameter, receive from primary proxy + val model = ParticipantModelInit.createPrimaryModel( + participantInput, + config, + ) + val operationInterval: OperationInterval = + SystemComponent.determineOperationInterval( + simulationStartDate, + simulationEndDate, + participantInput.getOperationTime, + ) + new ParticipantModelShell( model = model, + operationInterval = operationInterval, + simulationStartDate = simulationStartDate, state = model.getInitialState(), relevantData = None, flexOptions = None, operatingPoint = None, modelChange = ModelChangeIndicator(), ) + } + + def createForModel( + participantInput: SystemParticipantInput, + config: BaseRuntimeConfig, + simulationStartDate: ZonedDateTime, + simulationEndDate: ZonedDateTime, + ): ParticipantModelShell[_, _, _] = { + + val model = ParticipantModelInit.createModel( + participantInput, + config, + ) + val operationInterval: OperationInterval = + SystemComponent.determineOperationInterval( + simulationStartDate, + simulationEndDate, + participantInput.getOperationTime, + ) + + new ParticipantModelShell( + model = model, + operationInterval = operationInterval, + simulationStartDate = simulationStartDate, + state = model.getInitialState(), + relevantData = None, + flexOptions = None, + operatingPoint = None, + modelChange = ModelChangeIndicator(), + ) + } } diff --git a/src/main/scala/edu/ie3/simona/model/participant2/PrimaryDataParticipantModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/PrimaryDataParticipantModel.scala index 1ab3611c78..25d47b8eb2 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/PrimaryDataParticipantModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/PrimaryDataParticipantModel.scala @@ -21,14 +21,9 @@ import edu.ie3.simona.model.participant2.ParticipantModel.{ OperationRelevantData, ParticipantConstantModel, } -import edu.ie3.simona.model.participant2.PrimaryDataParticipantModel.{ - PrimaryActivePowerOperatingPoint, - PrimaryApparentPowerOperatingPoint, - PrimaryOperatingPoint, - PrimaryOperationRelevantData, - PrimaryResultFunc, -} +import edu.ie3.simona.model.participant2.PrimaryDataParticipantModel._ import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage +import edu.ie3.simona.ontology.messages.flex.MinMaxFlexibilityMessage.ProvideMinMaxFlexOptions import edu.ie3.simona.service.ServiceType import edu.ie3.util.scala.quantities.ReactivePower import squants.{Dimensionless, Power} @@ -79,8 +74,15 @@ final case class PrimaryDataParticipantModel[T <: PrimaryData: ClassTag]( ) } + override def createPrimaryDataResult( + data: PrimaryDataWithApparentPower[_], + dateTime: ZonedDateTime, + ): SystemParticipantResult = throw new CriticalFailureException( + "Method not implemented by this model." + ) + override def getRequiredServices: Iterable[ServiceType] = { - // primary service should not be specified here + // only secondary services should be specified here Iterable.empty } @@ -107,20 +109,21 @@ final case class PrimaryDataParticipantModel[T <: PrimaryData: ClassTag]( override def calcFlexOptions( state: ParticipantModel.ConstantState.type, relevantData: PrimaryOperationRelevantData[T], - ): FlexibilityMessage.ProvideFlexOptions = ??? + ): FlexibilityMessage.ProvideFlexOptions = { + val (operatingPoint, _) = determineOperatingPoint(state, relevantData) + val power = operatingPoint.activePower + + ProvideMinMaxFlexOptions.noFlexOption(uuid, power) + } override def handlePowerControl( state: ParticipantModel.ConstantState.type, flexOptions: FlexibilityMessage.ProvideFlexOptions, setPower: Power, - ): (PrimaryOperatingPoint[T], ParticipantModel.ModelChangeIndicator) = ??? + ): (PrimaryOperatingPoint[T], ParticipantModel.ModelChangeIndicator) = { + ??? // fixme hmmm + } - override def createPrimaryDataResult( - data: PrimaryDataWithApparentPower[_], - dateTime: ZonedDateTime, - ): SystemParticipantResult = throw new CriticalFailureException( - "Method not implemented by this model." - ) } object PrimaryDataParticipantModel { diff --git a/src/main/scala/edu/ie3/simona/model/participant2/PvModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/PvModel.scala index e2904c6d3d..a8b437b920 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/PvModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/PvModel.scala @@ -16,7 +16,6 @@ import edu.ie3.simona.agent.participant.data.Data.PrimaryData.ApparentPower import edu.ie3.simona.agent.participant.data.Data import edu.ie3.simona.agent.participant.data.Data.PrimaryData import edu.ie3.simona.exceptions.CriticalFailureException -import edu.ie3.simona.model.SystemComponent import edu.ie3.simona.model.participant.control.QControl import edu.ie3.simona.model.participant2.ParticipantFlexibility.ParticipantSimpleFlexibility import edu.ie3.simona.model.participant2.ParticipantModel.{ @@ -30,7 +29,6 @@ import edu.ie3.simona.ontology.messages.services.WeatherMessage.WeatherData import edu.ie3.simona.service.ServiceType import edu.ie3.util.quantities.PowerSystemUnits import edu.ie3.util.quantities.QuantityUtils.RichQuantityDouble -import edu.ie3.util.scala.OperationInterval import edu.ie3.util.scala.quantities._ import squants._ import squants.energy.Kilowatts @@ -793,57 +791,39 @@ object PvModel { ) extends OperationRelevantData def apply( - inputModel: PvInput, - scalingFactor: Double, - simulationStartDate: ZonedDateTime, - simulationEndDate: ZonedDateTime, - ): PvModel = { - - val scaledInput = inputModel.copy().scale(scalingFactor).build() - - /* Determine the operation interval */ - val operationInterval: OperationInterval = - SystemComponent.determineOperationInterval( - simulationStartDate, - simulationEndDate, - scaledInput.getOperationTime, - ) - - // moduleSurface and yieldSTC are left out for now + inputModel: PvInput + ): PvModel = new PvModel( - scaledInput.getUuid, - scaledInput.getId, - operationInterval, - QControl(scaledInput.getqCharacteristics), + inputModel.getUuid, Kilowatts( - scaledInput.getsRated + inputModel.getsRated .to(PowerSystemUnits.KILOWATT) .getValue .doubleValue ), - scaledInput.getCosPhiRated, - Degrees(scaledInput.getNode.getGeoPosition.getY), - Degrees(scaledInput.getNode.getGeoPosition.getX), - scaledInput.getAlbedo, + inputModel.getCosPhiRated, + QControl(inputModel.getqCharacteristics), + Degrees(inputModel.getNode.getGeoPosition.getY), + Degrees(inputModel.getNode.getGeoPosition.getX), + inputModel.getAlbedo, Each( - scaledInput.getEtaConv + inputModel.getEtaConv .to(PowerSystemUnits.PU) .getValue .doubleValue ), Radians( - scaledInput.getAzimuth + inputModel.getAzimuth .to(RADIAN) .getValue .doubleValue ), Radians( - scaledInput.getElevationAngle + inputModel.getElevationAngle .to(RADIAN) .getValue .doubleValue ), ) - } } diff --git a/src/main/scala/edu/ie3/simona/model/participant2/StorageModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/StorageModel.scala index eaf64fe7a8..b7f5b5d184 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/StorageModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/StorageModel.scala @@ -6,12 +6,14 @@ package edu.ie3.simona.model.participant2 +import edu.ie3.datamodel.models.input.system.StorageInput import edu.ie3.datamodel.models.result.system.{ StorageResult, SystemParticipantResult, } import edu.ie3.simona.agent.participant.data.Data import edu.ie3.simona.agent.participant.data.Data.PrimaryData +import edu.ie3.simona.config.SimonaConfig.StorageRuntimeConfig import edu.ie3.simona.exceptions.CriticalFailureException import edu.ie3.simona.model.participant.StorageModel.RefTargetSocParams import edu.ie3.simona.model.participant.control.QControl @@ -27,9 +29,11 @@ import edu.ie3.simona.model.participant2.StorageModel.{ import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage import edu.ie3.simona.ontology.messages.flex.MinMaxFlexibilityMessage.ProvideMinMaxFlexOptions import edu.ie3.simona.service.ServiceType +import edu.ie3.util.quantities.PowerSystemUnits import edu.ie3.util.quantities.QuantityUtils.RichQuantityDouble import edu.ie3.util.scala.quantities.DefaultQuantities.{zeroKW, zeroKWH} -import squants.{Dimensionless, Energy, Power, Seconds} +import squants.energy.{KilowattHours, Kilowatts} +import squants.{Dimensionless, Each, Energy, Power, Seconds} import java.time.ZonedDateTime import java.util.UUID @@ -342,4 +346,36 @@ object StorageModel { storedEnergy: Energy, tick: Long, ) extends ModelState + + def apply( + inputModel: StorageInput, + config: StorageRuntimeConfig, + ): StorageModel = + new StorageModel( + inputModel.getUuid, + Kilowatts( + inputModel.getType.getsRated + .to(PowerSystemUnits.KILOWATT) + .getValue + .doubleValue + ), + inputModel.getType.getCosPhiRated, + QControl.apply(inputModel.getqCharacteristics), + KilowattHours( + inputModel.getType.geteStorage + .to(PowerSystemUnits.KILOWATTHOUR) + .getValue + .doubleValue + ), + Kilowatts( + inputModel.getType.getpMax + .to(PowerSystemUnits.KILOWATT) + .getValue + .doubleValue + ), + Each( + inputModel.getType.getEta.to(PowerSystemUnits.PU).getValue.doubleValue + ), + config.targetSoc, + ) } From 24afead7bf300f3442b2c46fa0f8959701146205 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Fri, 25 Oct 2024 13:09:16 +0200 Subject: [PATCH 11/77] Further dev --- .../participant2/ParticipantFlexibility.scala | 2 +- .../model/participant2/ParticipantModel.scala | 22 ++++-- .../participant2/ParticipantModelInit.scala | 77 ++++++++++++++----- .../participant2/ParticipantModelShell.scala | 63 ++++++++------- .../model/participant2/StorageModel.scala | 7 +- .../model/participant2/evcs/EvcsModel.scala | 6 +- 6 files changed, 116 insertions(+), 61 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantFlexibility.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantFlexibility.scala index 0a96f08c90..fcc8917259 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantFlexibility.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantFlexibility.scala @@ -19,7 +19,7 @@ import edu.ie3.util.scala.quantities.DefaultQuantities import squants.energy.Power trait ParticipantFlexibility[ - OP <: OperatingPoint, + OP <: OperatingPoint[_], S <: ModelState, OR <: OperationRelevantData, ] { diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala index 67e759c7ea..3c8c425323 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala @@ -16,12 +16,12 @@ import edu.ie3.simona.model.participant2.ParticipantModel.{ ModelState, OperatingPoint, OperationRelevantData, - ResultsContainer, } import edu.ie3.simona.agent.participant2.ParticipantAgent import edu.ie3.simona.agent.participant2.ParticipantAgent.ParticipantRequest import edu.ie3.simona.model.participant.control.QControl import edu.ie3.simona.service.ServiceType +import edu.ie3.util.scala.quantities.DefaultQuantities.zeroKW import edu.ie3.util.scala.quantities.ReactivePower import org.apache.pekko.actor.typed.scaladsl.ActorContext import squants.Dimensionless @@ -31,7 +31,7 @@ import java.time.ZonedDateTime import java.util.UUID abstract class ParticipantModel[ - OP <: OperatingPoint, + OP <: OperatingPoint[_], S <: ModelState, OR <: OperationRelevantData, ] extends ParticipantFlexibility[OP, S, OR] { @@ -120,8 +120,6 @@ abstract class ParticipantModel[ // todo split off the following to ParticipantModelMeta? def getRequiredServices: Iterable[ServiceType] - def getInitialState(): S - /** @param receivedData * @throws CriticalFailureException * if unexpected type of data was provided @@ -139,18 +137,26 @@ object ParticipantModel { trait OperationRelevantData - trait OperatingPoint { + trait OperatingPoint[OP <: OperatingPoint[OP]] { + this: OP => + val activePower: Power /** Reactive power can be overridden by the model itself. If this is None, * the active-to-reactive-power function is used. */ val reactivePower: Option[ReactivePower] + + def zero: OP } final case class ActivePowerOperatingPoint(override val activePower: Power) - extends OperatingPoint { + extends OperatingPoint[ActivePowerOperatingPoint] { override val reactivePower: Option[ReactivePower] = None + + override def zero: ActivePowerOperatingPoint = ActivePowerOperatingPoint( + zeroKW + ) } trait ModelState { @@ -162,12 +168,12 @@ object ParticipantModel { } trait ParticipantConstantModel[ - OP <: OperatingPoint, + OP <: OperatingPoint[_], OR <: OperationRelevantData, ] { this: ParticipantModel[OP, ConstantState.type, OR] => - override def getInitialState(): ConstantState.type = ConstantState + def getInitialState: ConstantState.type = ConstantState override def determineState( lastState: ConstantState.type, diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelInit.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelInit.scala index 142a3ab161..cea6616be5 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelInit.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelInit.scala @@ -6,54 +6,93 @@ package edu.ie3.simona.model.participant2 -import edu.ie3.datamodel.models.input.system.{PvInput, SystemParticipantInput} +import edu.ie3.datamodel.models.input.system.SystemParticipantInput.SystemParticipantInputCopyBuilder +import edu.ie3.datamodel.models.input.system.{ + PvInput, + StorageInput, + SystemParticipantInput, +} import edu.ie3.datamodel.models.result.system.SystemParticipantResult import edu.ie3.simona.agent.participant.data.Data.PrimaryData +import edu.ie3.simona.config.SimonaConfig.{ + BaseRuntimeConfig, + StorageRuntimeConfig, +} +import edu.ie3.simona.exceptions.CriticalFailureException +import edu.ie3.simona.model.participant2.ParticipantModel.ModelState import edu.ie3.simona.model.participant2.PrimaryDataParticipantModel.PrimaryResultFunc import java.time.ZonedDateTime object ParticipantModelInit { - def createModel( + def createModel[S <: ModelState]( participantInput: SystemParticipantInput, - scalingFactor: Double, - simulationStartDate: ZonedDateTime, - simulationEndDate: ZonedDateTime, - ): ParticipantModel[_, _, _] = - participantInput match { - case pvInput: PvInput => - PvModel(pvInput, scalingFactor, simulationStartDate, simulationEndDate) + modelConfig: BaseRuntimeConfig, + ): ParticipantModelInitContainer[_] = { + + // function needed because Scala does not recognize Java type parameter + def scale[B <: SystemParticipantInputCopyBuilder[B]]( + builder: B + ): Double => B = + factor => builder.scale(factor) + + val scaledParticipantInput = + scale(participantInput.copy)(modelConfig.scaling).build() + + (scaledParticipantInput, modelConfig) match { + case (input: PvInput, _) => + val model = PvModel(input) + val state = model.getInitialState + ParticipantModelInitContainer(model, state) + case (input: StorageInput, config: StorageRuntimeConfig) => + val model = StorageModel(input, config) + val state = model.getInitialState(config) + ParticipantModelInitContainer(model, state) + case (input, config) => + throw new CriticalFailureException( + s"Handling the input model ${input.getClass.getSimpleName} or " + + "the combination of the input model with model config " + + s"${config.getClass.getSimpleName} is not implemented." + ) } + } def createPrimaryModel[T <: PrimaryData]( participantInput: SystemParticipantInput, - scalingFactor: Double, - simulationStartDate: ZonedDateTime, - simulationEndDate: ZonedDateTime, - ): PrimaryDataParticipantModel[T] = { + modelConfig: BaseRuntimeConfig, + ): ParticipantModelInitContainer[_] = { // Create a fitting physical model to extract parameters from - val physicalModel = createModel( + val modelContainer = createModel( participantInput, - scalingFactor, - simulationStartDate, - simulationEndDate, + modelConfig, ) + val physicalModel = modelContainer.model val primaryResultFunc = new PrimaryResultFunc[T] { override def createResult( data: T with PrimaryData.PrimaryDataWithApparentPower[_], dateTime: ZonedDateTime, ): SystemParticipantResult = - physicalModel.createPrimaryDataResult(data) + physicalModel.createPrimaryDataResult(data, dateTime) } - new PrimaryDataParticipantModel[T]( + val primaryDataModel = new PrimaryDataParticipantModel[T]( physicalModel.uuid, physicalModel.sRated, physicalModel.cosPhiRated, physicalModel.qControl, primaryResultFunc, ) + + ParticipantModelInitContainer( + primaryDataModel, + primaryDataModel.getInitialState, + ) } + + final case class ParticipantModelInitContainer[S <: ModelState]( + model: ParticipantModel[_, S, _], + initialState: S, + ) } diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala index 92cc47c202..644bfd375b 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala @@ -44,7 +44,7 @@ import java.time.ZonedDateTime * - flex options? (only current needed) */ final case class ParticipantModelShell[ - OP <: OperatingPoint, + OP <: OperatingPoint[_], S <: ModelState, OR <: OperationRelevantData, ]( @@ -72,8 +72,6 @@ final case class ParticipantModelShell[ def updateOperatingPoint( currentTick: Long ): ParticipantModelShell[OP, S, OR] = { - // todo consider operationInterval - val currentState = determineCurrentState(currentTick) if (currentState.tick != currentTick) @@ -81,16 +79,24 @@ final case class ParticipantModelShell[ s"New state $currentState is not set to current tick $currentTick" ) - val (newOperatingPoint, maybeNextTick) = + val (newOperatingPoint, newNextTick) = model.determineOperatingPoint( state, relevantData.getOrElse("No relevant data available!"), ) + val (adaptedOperatingPoint, adaptedNextTick) = + if (!operationInterval.includes(currentTick)) { + // Current tick is outside of operation interval. + // Set operating point to "zero" + (newOperatingPoint.zero, None) + } else + (newOperatingPoint, newNextTick) + copy( state = currentState, - operatingPoint = Some(newOperatingPoint), - modelChange = ModelChangeIndicator(changesAtTick = maybeNextTick), + operatingPoint = Some(adaptedOperatingPoint), + modelChange = ModelChangeIndicator(changesAtTick = adaptedNextTick), ) } @@ -189,26 +195,15 @@ object ParticipantModelShell { simulationEndDate: ZonedDateTime, ): ParticipantModelShell[_, _, _] = { // todo T parameter, receive from primary proxy - val model = ParticipantModelInit.createPrimaryModel( + val modelContainer = ParticipantModelInit.createPrimaryModel( participantInput, config, ) - val operationInterval: OperationInterval = - SystemComponent.determineOperationInterval( - simulationStartDate, - simulationEndDate, - participantInput.getOperationTime, - ) - - new ParticipantModelShell( - model = model, - operationInterval = operationInterval, - simulationStartDate = simulationStartDate, - state = model.getInitialState(), - relevantData = None, - flexOptions = None, - operatingPoint = None, - modelChange = ModelChangeIndicator(), + createShell( + modelContainer, + participantInput, + simulationEndDate, + simulationStartDate, ) } @@ -218,11 +213,24 @@ object ParticipantModelShell { simulationStartDate: ZonedDateTime, simulationEndDate: ZonedDateTime, ): ParticipantModelShell[_, _, _] = { - - val model = ParticipantModelInit.createModel( + val modelContainer = ParticipantModelInit.createModel( participantInput, config, ) + createShell( + modelContainer, + participantInput, + simulationEndDate, + simulationStartDate, + ) + } + + private def createShell( + modelContainer: ParticipantModelInit.ParticipantModelInitContainer[_], + participantInput: SystemParticipantInput, + simulationEndDate: ZonedDateTime, + simulationStartDate: ZonedDateTime, + ): ParticipantModelShell[_, _, _] = { val operationInterval: OperationInterval = SystemComponent.determineOperationInterval( simulationStartDate, @@ -231,15 +239,14 @@ object ParticipantModelShell { ) new ParticipantModelShell( - model = model, + model = modelContainer.model, operationInterval = operationInterval, simulationStartDate = simulationStartDate, - state = model.getInitialState(), + state = modelContainer.initialState, relevantData = None, flexOptions = None, operatingPoint = None, modelChange = ModelChangeIndicator(), ) } - } diff --git a/src/main/scala/edu/ie3/simona/model/participant2/StorageModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/StorageModel.scala index b7f5b5d184..6479990a86 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/StorageModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/StorageModel.scala @@ -159,9 +159,6 @@ class StorageModel private ( override def getRequiredServices: Iterable[ServiceType] = Iterable.empty - override def getInitialState(): StorageState = - StorageState(storedEnergy = zeroKWH, -1L) - override def createRelevantData( receivedData: Seq[Data], nodalVoltage: Dimensionless, @@ -330,6 +327,10 @@ class StorageModel private ( private def isEmpty(storedEnergy: Energy): Boolean = storedEnergy <= minEnergyWithMargin + def getInitialState(config: StorageRuntimeConfig): StorageState = { + val initialStorage = eStorage * config.initialSoc + StorageState(storedEnergy = initialStorage, -1L) + } } object StorageModel { diff --git a/src/main/scala/edu/ie3/simona/model/participant2/evcs/EvcsModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/evcs/EvcsModel.scala index a191da6a05..99a498eb75 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/evcs/EvcsModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/evcs/EvcsModel.scala @@ -150,7 +150,7 @@ class EvcsModel private ( ServiceType.EvMovementService ) - override def getInitialState(): EvcsState = EvcsState(Map.empty, -1) + def getInitialState: EvcsState = EvcsState(Map.empty, -1) override def createRelevantData( receivedData: Seq[Data], @@ -301,12 +301,14 @@ class EvcsModel private ( object EvcsModel { final case class EvcsOperatingPoint(evOperatingPoints: Map[UUID, Power]) - extends OperatingPoint { + extends OperatingPoint[EvcsOperatingPoint] { override val activePower: Power = evOperatingPoints.values.reduceOption(_ + _).getOrElse(zeroKW) override val reactivePower: Option[ReactivePower] = ??? + + override def zero: EvcsOperatingPoint = EvcsOperatingPoint(Map.empty) } /** @param evs From 5338fa3ea44f727b5bc279aeb94ff6c8b5489a93 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Fri, 25 Oct 2024 15:26:07 +0200 Subject: [PATCH 12/77] Filling in some missing parts Signed-off-by: Sebastian Peter --- .../participant2/ParticipantAgentInit.scala | 52 +++++++++++++++---- .../model/participant2/ParticipantModel.scala | 14 +++-- .../PrimaryDataParticipantModel.scala | 1 + .../simona/model/participant2/PvModel.scala | 8 ++- .../model/participant2/StorageModel.scala | 1 + .../model/participant2/evcs/EvcsModel.scala | 1 + 6 files changed, 64 insertions(+), 13 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgentInit.scala b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgentInit.scala index 803007ba4a..0084a265cd 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgentInit.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgentInit.scala @@ -36,6 +36,8 @@ object ParticipantAgentInit { participantInput: SystemParticipantInput, config: BaseRuntimeConfig, primaryServiceProxy: ClassicRef, + gridAgentRef: ActorRef[GridAgent.Request], + expectedPowerRequestTick: Long, simulationStartDate: ZonedDateTime, simulationEndDate: ZonedDateTime, parent: Either[ActorRef[SchedulerMessage], ActorRef[FlexResponse]], @@ -77,6 +79,8 @@ object ParticipantAgentInit { participantInput, config, primaryServiceProxy, + gridAgentRef, + expectedPowerRequestTick, simulationStartDate, simulationEndDate, parentData, @@ -87,27 +91,35 @@ object ParticipantAgentInit { participantInput: SystemParticipantInput, config: BaseRuntimeConfig, primaryServiceProxy: ClassicRef, + gridAgentRef: ActorRef[GridAgent.Request], + expectedPowerRequestTick: Long, simulationStartDate: ZonedDateTime, simulationEndDate: ZonedDateTime, parentData: Either[SchedulerData, FlexControlledData], - ): Behavior[Request] = Behaviors.receivePartial { - case (ctx, activation: ActivationRequest) - if activation.tick == INIT_SIM_TICK => + ): Behavior[Request] = Behaviors.receiveMessagePartial { + + case activation: ActivationRequest if activation.tick == INIT_SIM_TICK => primaryServiceProxy ! PrimaryServiceRegistrationMessage( participantInput.getUuid ) + waitingForPrimaryProxy( participantInput, config, + gridAgentRef, + expectedPowerRequestTick, simulationStartDate, simulationEndDate, parentData, ) + } private def waitingForPrimaryProxy( participantInput: SystemParticipantInput, config: BaseRuntimeConfig, + gridAgentRef: ActorRef[GridAgent.Request], + expectedPowerRequestTick: Long, simulationStartDate: ZonedDateTime, simulationEndDate: ZonedDateTime, parentData: Either[SchedulerData, FlexControlledData], @@ -137,8 +149,8 @@ object ParticipantAgentInit { simulationEndDate, ), expectedFirstData, - ???, - ???, + gridAgentRef, + expectedPowerRequestTick, parentData, ) @@ -150,12 +162,24 @@ object ParticipantAgentInit { simulationEndDate, ) - val requiredServices = modelShell.model.getRequiredServices.toSeq - if (requiredServices.isEmpty) { - createAgent(modelShell, Map.empty, ???, ???, parentData) + val requiredServiceTypes = modelShell.model.getRequiredServices.toSeq + + if (requiredServiceTypes.isEmpty) { + createAgent( + modelShell, + Map.empty, + gridAgentRef, + expectedPowerRequestTick, + parentData, + ) } else { + // TODO request service actorrefs + val requiredServices = ??? + waitingForServices( modelShell, + gridAgentRef, + expectedPowerRequestTick, requiredServices, parentData = parentData, ) @@ -164,6 +188,8 @@ object ParticipantAgentInit { private def waitingForServices( modelShell: ParticipantModelShell[_, _, _], + gridAgentRef: ActorRef[GridAgent.Request], + expectedPowerRequestTick: Long, expectedRegistrations: Set[ClassicRef], expectedFirstData: Map[ClassicRef, Long] = Map.empty, parentData: Either[SchedulerData, FlexControlledData], @@ -197,10 +223,18 @@ object ParticipantAgentInit { ), ) - createAgent(modelShell, newExpectedFirstData, ???, ???, parentData) + createAgent( + modelShell, + newExpectedFirstData, + gridAgentRef, + expectedPowerRequestTick, + parentData, + ) } else waitingForServices( modelShell, + gridAgentRef, + expectedPowerRequestTick, newExpectedRegistrations, newExpectedFirstData, parentData, diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala index 3c8c425323..7c5e7a52f5 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala @@ -121,16 +121,24 @@ abstract class ParticipantModel[ def getRequiredServices: Iterable[ServiceType] /** @param receivedData - * @throws CriticalFailureException - * if unexpected type of data was provided + * The received primary or secondary data + * @param nodalVoltage + * The voltage at the node that we're connected to + * @param tick + * The current tick + * @param simulationTime + * The current simulation time (matches the tick) * @return + * The operation relevant date for the current point in simulation time + * @throws edu.ie3.simona.exceptions.CriticalFailureException + * if unexpected type of data was provided */ def createRelevantData( receivedData: Seq[Data], nodalVoltage: Dimensionless, tick: Long, + simulationTime: ZonedDateTime, ): OR - } object ParticipantModel { diff --git a/src/main/scala/edu/ie3/simona/model/participant2/PrimaryDataParticipantModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/PrimaryDataParticipantModel.scala index 25d47b8eb2..1cbbe1a36d 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/PrimaryDataParticipantModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/PrimaryDataParticipantModel.scala @@ -95,6 +95,7 @@ final case class PrimaryDataParticipantModel[T <: PrimaryData: ClassTag]( receivedData: Seq[Data], nodalVoltage: Dimensionless, tick: Long, + simulationTime: ZonedDateTime, ): PrimaryOperationRelevantData[T] = receivedData .collectFirst { case data: T => diff --git a/src/main/scala/edu/ie3/simona/model/participant2/PvModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/PvModel.scala index a8b437b920..10b8711434 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/PvModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/PvModel.scala @@ -755,10 +755,16 @@ class PvModel private ( receivedData: Seq[Data], nodalVoltage: Dimensionless, tick: Long, + simulationTime: ZonedDateTime, ): PvRelevantData = { receivedData .collectFirst { case weatherData: WeatherData => - PvRelevantData(???, ???, weatherData.diffIrr, weatherData.dirIrr) + PvRelevantData( + simulationTime, + ???, + weatherData.diffIrr, + weatherData.dirIrr, + ) } .getOrElse { throw new CriticalFailureException( diff --git a/src/main/scala/edu/ie3/simona/model/participant2/StorageModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/StorageModel.scala index 6479990a86..d7f945c662 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/StorageModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/StorageModel.scala @@ -163,6 +163,7 @@ class StorageModel private ( receivedData: Seq[Data], nodalVoltage: Dimensionless, tick: Long, + simulationTime: ZonedDateTime, ): StorageRelevantData = { if (receivedData.nonEmpty) { throw new CriticalFailureException( diff --git a/src/main/scala/edu/ie3/simona/model/participant2/evcs/EvcsModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/evcs/EvcsModel.scala index 99a498eb75..aefce7ff11 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/evcs/EvcsModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/evcs/EvcsModel.scala @@ -156,6 +156,7 @@ class EvcsModel private ( receivedData: Seq[Data], nodalVoltage: Dimensionless, tick: Long, + simulationTime: ZonedDateTime, ): EvcsRelevantData = { receivedData .collectFirst { case evData: ArrivingEvs => From 0787e73de6f105281786c4b166fa0bf06009ca26 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Fri, 25 Oct 2024 18:10:07 +0200 Subject: [PATCH 13/77] Some shenanigans with self-types of primary data and more Signed-off-by: Sebastian Peter --- .../agent/participant/ParticipantAgent.scala | 2 +- .../ParticipantAgentFundamentals.scala | 8 +- .../simona/agent/participant/data/Data.scala | 28 +++++-- .../data/primary/PrimaryDataService.scala | 2 +- .../statedata/InitializeStateData.scala | 5 +- .../statedata/ParticipantStateData.scala | 18 ++--- .../statedata/UninitializedStateData.scala | 2 +- .../participant2/ParticipantGridAdapter.scala | 4 +- .../model/participant2/ParticipantModel.scala | 24 ++++-- .../participant2/ParticipantModelInit.scala | 11 +-- .../participant2/ParticipantModelShell.scala | 29 +++++++- .../PrimaryDataParticipantModel.scala | 74 +++++++++++-------- .../simona/model/participant2/PvModel.scala | 3 +- .../model/participant2/StorageModel.scala | 3 +- .../model/participant2/evcs/EvcsModel.scala | 59 ++++++++++++--- .../primary/PrimaryServiceWorker.scala | 6 +- .../agent/participant/RichValueSpec.scala | 2 +- 17 files changed, 194 insertions(+), 86 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/agent/participant/ParticipantAgent.scala b/src/main/scala/edu/ie3/simona/agent/participant/ParticipantAgent.scala index 52e3a53a59..0d44b92d3d 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant/ParticipantAgent.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant/ParticipantAgent.scala @@ -888,7 +888,7 @@ object ParticipantAgent { * nodal voltage */ def getAndCheckNodalVoltage( - baseStateData: BaseStateData[_ <: PrimaryData], + baseStateData: BaseStateData[_ <: PrimaryData[_]], currentTick: Long, ): Dimensionless = { baseStateData.voltageValueStore.last(currentTick) match { diff --git a/src/main/scala/edu/ie3/simona/agent/participant/ParticipantAgentFundamentals.scala b/src/main/scala/edu/ie3/simona/agent/participant/ParticipantAgentFundamentals.scala index 0b0ad206e0..ddb61b57d7 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant/ParticipantAgentFundamentals.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant/ParticipantAgentFundamentals.scala @@ -993,17 +993,17 @@ protected trait ParticipantAgentFundamentals[ .flatMap { case (_, maybeData) => maybeData } - .fold[Try[PrimaryData]] { + .fold[Try[PrimaryData[_]]] { Failure( new IllegalStateException( "Not able to determine the most recent result, although it should have been sent." ) ) } { - case result: PrimaryData + case result: PrimaryData[_] if pdClassTag.runtimeClass.equals(result.getClass) => Success(result) - case primaryData: PrimaryData => + case primaryData: PrimaryData[_] => primaryData match { case pd: EnrichableData[_] => val q = @@ -1981,7 +1981,7 @@ object ParticipantAgentFundamentals { * @tparam PD * Type of primary data, that is relevant for the next calculation */ - final case class RelevantResultValues[+PD <: PrimaryData]( + final case class RelevantResultValues[+PD <: PrimaryData[_]]( windowStart: Long, windowEnd: Long, relevantData: Map[Long, PD], diff --git a/src/main/scala/edu/ie3/simona/agent/participant/data/Data.scala b/src/main/scala/edu/ie3/simona/agent/participant/data/Data.scala index 7acd5b9b08..1470c012ef 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant/data/Data.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant/data/Data.scala @@ -33,9 +33,11 @@ object Data { * model invocation. Anyway, primary data has to have at least active power * given */ - sealed trait PrimaryData extends Data { + sealed trait PrimaryData[+T <: PrimaryData[T]] extends Data { val p: Power def toApparentPower: ApparentPower + + def scale(factor: Double): T } object PrimaryData { @@ -48,7 +50,7 @@ object Data { */ sealed trait PrimaryDataWithApparentPower[ +T <: PrimaryDataWithApparentPower[T] - ] extends PrimaryData { + ] extends PrimaryData[T] { val q: ReactivePower def withReactivePower(q: ReactivePower): T @@ -68,7 +70,7 @@ object Data { * Active power */ final case class ActivePower(override val p: Power) - extends PrimaryData + extends PrimaryData[ActivePower] with EnrichableData[ApparentPower] { override def toApparentPower: ApparentPower = ApparentPower( @@ -78,6 +80,9 @@ object Data { override def add(q: ReactivePower): ApparentPower = ApparentPower(p, q) + + override def scale(factor: Double): ActivePower = + ActivePower(p = p * factor) } /** Active and Reactive power as participant simulation result @@ -95,6 +100,9 @@ object Data { override def withReactivePower(q: ReactivePower): ApparentPower = copy(q = q) + + override def scale(factor: Double): ApparentPower = + ApparentPower(p = p * factor, q = q * factor) } /** Active power and heat demand as participant simulation result @@ -107,7 +115,7 @@ object Data { final case class ActivePowerAndHeat( override val p: Power, override val qDot: Power, - ) extends PrimaryData + ) extends PrimaryData[ActivePowerAndHeat] with Heat with EnrichableData[ApparentPowerAndHeat] { override def toApparentPower: ApparentPower = @@ -118,6 +126,9 @@ object Data { override def add(q: ReactivePower): ApparentPowerAndHeat = ApparentPowerAndHeat(p, q, qDot) + + override def scale(factor: Double): ActivePowerAndHeat = + ActivePowerAndHeat(p = p * factor, qDot = qDot * factor) } /** Apparent power and heat demand as participant simulation result @@ -140,10 +151,17 @@ object Data { override def withReactivePower(q: ReactivePower): ApparentPowerAndHeat = copy(q = q) + + override def scale(factor: Double): ApparentPowerAndHeat = + ApparentPowerAndHeat( + p = p * factor, + q = q * factor, + qDot = qDot * factor, + ) } implicit class RichValue(private val value: Value) { - def toPrimaryData: Try[PrimaryData] = + def toPrimaryData: Try[PrimaryData[_]] = value match { case hs: HeatAndSValue => (hs.getP.toScala, hs.getQ.toScala, hs.getHeatDemand.toScala) match { diff --git a/src/main/scala/edu/ie3/simona/agent/participant/data/primary/PrimaryDataService.scala b/src/main/scala/edu/ie3/simona/agent/participant/data/primary/PrimaryDataService.scala index 33cb4b7519..1365277b52 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant/data/primary/PrimaryDataService.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant/data/primary/PrimaryDataService.scala @@ -13,7 +13,7 @@ import edu.ie3.simona.agent.participant.data.DataService /** Enum-like trait to denote possible external data sources for systems */ -sealed trait PrimaryDataService[+D <: PrimaryData] extends DataService[D] +sealed trait PrimaryDataService[+D <: PrimaryData[_]] extends DataService[D] object PrimaryDataService { diff --git a/src/main/scala/edu/ie3/simona/agent/participant/statedata/InitializeStateData.scala b/src/main/scala/edu/ie3/simona/agent/participant/statedata/InitializeStateData.scala index de01009850..c049b13472 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant/statedata/InitializeStateData.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant/statedata/InitializeStateData.scala @@ -13,7 +13,8 @@ import edu.ie3.simona.event.notifier.NotifierConfig * information needed to initialize a * [[edu.ie3.simona.agent.participant.ParticipantAgent]] */ -trait InitializeStateData[+PD <: PrimaryData] extends ParticipantStateData[PD] { +trait InitializeStateData[+PD <: PrimaryData[_]] + extends ParticipantStateData[PD] { /** Config for the output behaviour of simulation results */ @@ -21,7 +22,7 @@ trait InitializeStateData[+PD <: PrimaryData] extends ParticipantStateData[PD] { } object InitializeStateData { - final case class TrivialInitializeStateData[+PD <: PrimaryData]( + final case class TrivialInitializeStateData[+PD <: PrimaryData[_]]( resultEventEmitter: String ) extends InitializeStateData[PD] { val outputConfig: NotifierConfig = NotifierConfig( diff --git a/src/main/scala/edu/ie3/simona/agent/participant/statedata/ParticipantStateData.scala b/src/main/scala/edu/ie3/simona/agent/participant/statedata/ParticipantStateData.scala index 95d8df3fcf..2e66b932b5 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant/statedata/ParticipantStateData.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant/statedata/ParticipantStateData.scala @@ -22,18 +22,18 @@ import java.time.ZonedDateTime /** Trait to denote all common forms of state data related to each participant * agent */ -trait ParticipantStateData[+PD <: PrimaryData] +trait ParticipantStateData[+PD <: PrimaryData[_]] object ParticipantStateData { /** Data for the state, in which the agent is not initialized, yet. *

IMPORTANT: Needs to be an empty case class due to typing

*/ - final class ParticipantUninitializedStateData[+PD <: PrimaryData]() + final class ParticipantUninitializedStateData[+PD <: PrimaryData[_]]() extends UninitializedStateData[PD] object ParticipantUninitializedStateData { - def apply[PD <: PrimaryData](): ParticipantUninitializedStateData[PD] = + def apply[PD <: PrimaryData[_]](): ParticipantUninitializedStateData[PD] = new ParticipantUninitializedStateData() } @@ -67,7 +67,7 @@ object ParticipantStateData { final case class ParticipantInitializingStateData[ I <: SystemParticipantInput, C <: SimonaConfig.BaseRuntimeConfig, - PD <: PrimaryData, + PD <: PrimaryData[_], ]( inputModel: InputModelContainer[I], modelConfig: C, @@ -111,7 +111,7 @@ object ParticipantStateData { final case class ParticipantInitializeStateData[ I <: SystemParticipantInput, C <: SimonaConfig.BaseRuntimeConfig, - PD <: PrimaryData, + PD <: PrimaryData[_], ]( inputModel: InputModelContainer[I], modelConfig: C, @@ -130,7 +130,7 @@ object ParticipantStateData { def apply[ I <: SystemParticipantInput, C <: SimonaConfig.BaseRuntimeConfig, - PD <: PrimaryData, + PD <: PrimaryData[_], ]( inputModel: I, modelConfig: C, @@ -159,7 +159,7 @@ object ParticipantStateData { def apply[ I <: SystemParticipantInput, C <: SimonaConfig.BaseRuntimeConfig, - PD <: PrimaryData, + PD <: PrimaryData[_], ]( inputModel: I, modelConfig: C, @@ -190,7 +190,7 @@ object ParticipantStateData { def apply[ I <: SystemParticipantInput, C <: SimonaConfig.BaseRuntimeConfig, - PD <: PrimaryData, + PD <: PrimaryData[_], ]( inputModel: I, thermalGrid: ThermalGrid, @@ -221,7 +221,7 @@ object ParticipantStateData { def apply[ I <: SystemParticipantInput, C <: SimonaConfig.BaseRuntimeConfig, - PD <: PrimaryData, + PD <: PrimaryData[_], ]( inputModel: I, thermalGrid: ThermalGrid, diff --git a/src/main/scala/edu/ie3/simona/agent/participant/statedata/UninitializedStateData.scala b/src/main/scala/edu/ie3/simona/agent/participant/statedata/UninitializedStateData.scala index 85700927c2..5bfda74961 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant/statedata/UninitializedStateData.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant/statedata/UninitializedStateData.scala @@ -10,5 +10,5 @@ import edu.ie3.simona.agent.participant.data.Data.PrimaryData /** Properties common to all participant agents not yet initialized */ -trait UninitializedStateData[+PD <: PrimaryData] +trait UninitializedStateData[+PD <: PrimaryData[_]] extends ParticipantStateData[PD] diff --git a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantGridAdapter.scala b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantGridAdapter.scala index cab4556f12..cd9f0e9b4a 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantGridAdapter.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantGridAdapter.scala @@ -46,7 +46,7 @@ final case class ParticipantGridAdapter( power: ApparentPower, tick: Long, ): ParticipantGridAdapter = - copy(tickToPower = tickToPower + (tick, power)) + copy(tickToPower = tickToPower.updated(tick, power)) def handlePowerRequest( newVoltage: Dimensionless, @@ -84,7 +84,7 @@ final case class ParticipantGridAdapter( Right(0, currentTick) }).fold( cachedResult => cachedResult.copy(newResult = false), - { case (windowStart, windowEnd) => + { case (windowStart: Long, windowEnd: Long) => val avgPower = averageApparentPower( tickToPower, windowStart, diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala index 7c5e7a52f5..2ebfa11ad6 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala @@ -87,9 +87,24 @@ abstract class ParticipantModel[ */ def determineState(lastState: S, operatingPoint: OP, currentTick: Long): S + /** @param state + * the current state + * @param lastOperatingPoint + * the last operating point before the current one, i.e. the one valid up + * until the last state, if applicable + * @param currentOperatingPoint + * the operating point valid from the simulation time of the last state up + * until now + * @param complexPower + * the total complex power derived from the current operating point + * @param dateTime + * the current simulation date and time + * @return + */ def createResults( state: S, - operatingPoint: OP, + lastOperatingPoint: Option[OP], + currentOperatingPoint: OP, complexPower: ApparentPower, dateTime: ZonedDateTime, ): Iterable[SystemParticipantResult] @@ -145,7 +160,7 @@ object ParticipantModel { trait OperationRelevantData - trait OperatingPoint[OP <: OperatingPoint[OP]] { + trait OperatingPoint[+OP <: OperatingPoint[OP]] { this: OP => val activePower: Power @@ -201,9 +216,4 @@ object ParticipantModel { changesAtTick: Option[Long] = None, ) - final case class ResultsContainer( - totalPower: ApparentPower, - modelResults: Iterable[SystemParticipantResult], - ) - } diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelInit.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelInit.scala index cea6616be5..97ab103159 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelInit.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelInit.scala @@ -23,6 +23,7 @@ import edu.ie3.simona.model.participant2.ParticipantModel.ModelState import edu.ie3.simona.model.participant2.PrimaryDataParticipantModel.PrimaryResultFunc import java.time.ZonedDateTime +import scala.reflect.ClassTag object ParticipantModelInit { @@ -34,7 +35,7 @@ object ParticipantModelInit { // function needed because Scala does not recognize Java type parameter def scale[B <: SystemParticipantInputCopyBuilder[B]]( builder: B - ): Double => B = + ): Double => SystemParticipantInputCopyBuilder[B] = factor => builder.scale(factor) val scaledParticipantInput = @@ -58,7 +59,7 @@ object ParticipantModelInit { } } - def createPrimaryModel[T <: PrimaryData]( + def createPrimaryModel[P <: PrimaryData[_]: ClassTag]( participantInput: SystemParticipantInput, modelConfig: BaseRuntimeConfig, ): ParticipantModelInitContainer[_] = { @@ -69,15 +70,15 @@ object ParticipantModelInit { ) val physicalModel = modelContainer.model - val primaryResultFunc = new PrimaryResultFunc[T] { + val primaryResultFunc = new PrimaryResultFunc[P] { override def createResult( - data: T with PrimaryData.PrimaryDataWithApparentPower[_], + data: P with PrimaryData.PrimaryDataWithApparentPower[_], dateTime: ZonedDateTime, ): SystemParticipantResult = physicalModel.createPrimaryDataResult(data, dateTime) } - val primaryDataModel = new PrimaryDataParticipantModel[T]( + val primaryDataModel = new PrimaryDataParticipantModel[P]( physicalModel.uuid, physicalModel.sRated, physicalModel.cosPhiRated, diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala index 644bfd375b..b16e561dea 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala @@ -7,6 +7,7 @@ package edu.ie3.simona.model.participant2 import edu.ie3.datamodel.models.input.system.SystemParticipantInput +import edu.ie3.datamodel.models.result.system.SystemParticipantResult import edu.ie3.simona.agent.participant.data.Data.PrimaryData.ApparentPower import edu.ie3.simona.agent.participant.data.Data.SecondaryData import edu.ie3.simona.agent.participant2.ParticipantAgent @@ -20,8 +21,8 @@ import edu.ie3.simona.model.participant2.ParticipantModel.{ ModelState, OperatingPoint, OperationRelevantData, - ResultsContainer, } +import edu.ie3.simona.model.participant2.ParticipantModelShell.ResultsContainer import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage.{ IssueFlexControl, ProvideFlexOptions, @@ -54,6 +55,7 @@ final case class ParticipantModelShell[ state: S, relevantData: Option[OR], flexOptions: Option[ProvideFlexOptions], + lastOperatingPoint: Option[OP], operatingPoint: Option[OP], modelChange: ModelChangeIndicator, ) { @@ -63,8 +65,14 @@ final case class ParticipantModelShell[ nodalVoltage: Dimensionless, tick: Long, ): ParticipantModelShell[OP, S, OR] = { + val currentSimulationTime = tick.toDateTime(simulationStartDate) val updatedRelevantData = - model.createRelevantData(receivedData, nodalVoltage, tick) + model.createRelevantData( + receivedData, + nodalVoltage, + tick, + currentSimulationTime, + ) copy(relevantData = Some(updatedRelevantData)) } @@ -82,7 +90,9 @@ final case class ParticipantModelShell[ val (newOperatingPoint, newNextTick) = model.determineOperatingPoint( state, - relevantData.getOrElse("No relevant data available!"), + relevantData.getOrElse( + throw new CriticalFailureException("No relevant data available!") + ), ) val (adaptedOperatingPoint, adaptedNextTick) = @@ -95,6 +105,7 @@ final case class ParticipantModelShell[ copy( state = currentState, + lastOperatingPoint = operatingPoint, operatingPoint = Some(adaptedOperatingPoint), modelChange = ModelChangeIndicator(changesAtTick = adaptedNextTick), ) @@ -120,6 +131,7 @@ final case class ParticipantModelShell[ val participantResults = model.createResults( state, + lastOperatingPoint, op, complexPower, currentTick.toDateTime(simulationStartDate), @@ -135,7 +147,9 @@ final case class ParticipantModelShell[ val currentState = determineCurrentState(currentTick) val flexOptions = model.calcFlexOptions( currentState, - relevantData.getOrElse("No relevant data available!"), + relevantData.getOrElse( + throw new CriticalFailureException("No relevant data available!") + ), ) copy(state = currentState, flexOptions = Some(flexOptions)) @@ -160,6 +174,7 @@ final case class ParticipantModelShell[ copy( state = currentState, + lastOperatingPoint = operatingPoint, operatingPoint = Some(newOperatingPoint), modelChange = modelChange, ) @@ -188,6 +203,11 @@ final case class ParticipantModelShell[ object ParticipantModelShell { + final case class ResultsContainer( + totalPower: ApparentPower, + modelResults: Iterable[SystemParticipantResult], + ) + def createForPrimaryData( participantInput: SystemParticipantInput, config: BaseRuntimeConfig, @@ -245,6 +265,7 @@ object ParticipantModelShell { state = modelContainer.initialState, relevantData = None, flexOptions = None, + lastOperatingPoint = None, operatingPoint = None, modelChange = ModelChangeIndicator(), ) diff --git a/src/main/scala/edu/ie3/simona/model/participant2/PrimaryDataParticipantModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/PrimaryDataParticipantModel.scala index 1cbbe1a36d..8dde3e5547 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/PrimaryDataParticipantModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/PrimaryDataParticipantModel.scala @@ -34,36 +34,38 @@ import scala.reflect.ClassTag /** Just "replaying" primary data */ -final case class PrimaryDataParticipantModel[T <: PrimaryData: ClassTag]( +final case class PrimaryDataParticipantModel[P <: PrimaryData[_]: ClassTag]( override val uuid: UUID, override val sRated: Power, override val cosPhiRated: Double, override val qControl: QControl, - primaryDataResultFunc: PrimaryResultFunc[T], + primaryDataResultFunc: PrimaryResultFunc[P], ) extends ParticipantModel[ - PrimaryOperatingPoint[T], + PrimaryOperatingPoint[_, P], ConstantState.type, - PrimaryOperationRelevantData[T], + PrimaryOperationRelevantData[P], ] with ParticipantConstantModel[PrimaryOperatingPoint[ - T + _, + P, ], PrimaryOperationRelevantData[ - T + P ]] { override def determineOperatingPoint( state: ParticipantModel.ConstantState.type, - relevantData: PrimaryOperationRelevantData[T], - ): (PrimaryOperatingPoint[T], Option[Long]) = + relevantData: PrimaryOperationRelevantData[P], + ): (PrimaryOperatingPoint[_, P], Option[Long]) = (PrimaryOperatingPoint(relevantData.data), None) override def createResults( state: ParticipantModel.ConstantState.type, - operatingPoint: PrimaryOperatingPoint[T], + lastOperatingPoint: Option[PrimaryOperatingPoint[_, P]], + currentOperatingPoint: PrimaryOperatingPoint[_, P], complexPower: PrimaryData.ApparentPower, dateTime: ZonedDateTime, ): Iterable[SystemParticipantResult] = { - val primaryDataWithApparentPower = operatingPoint match { + val primaryDataWithApparentPower = currentOperatingPoint match { case PrimaryApparentPowerOperatingPoint(data) => data case PrimaryActivePowerOperatingPoint(data) => @@ -96,9 +98,9 @@ final case class PrimaryDataParticipantModel[T <: PrimaryData: ClassTag]( nodalVoltage: Dimensionless, tick: Long, simulationTime: ZonedDateTime, - ): PrimaryOperationRelevantData[T] = + ): PrimaryOperationRelevantData[P] = receivedData - .collectFirst { case data: T => + .collectFirst { case data: P => PrimaryOperationRelevantData(data) } .getOrElse { @@ -109,7 +111,7 @@ final case class PrimaryDataParticipantModel[T <: PrimaryData: ClassTag]( override def calcFlexOptions( state: ParticipantModel.ConstantState.type, - relevantData: PrimaryOperationRelevantData[T], + relevantData: PrimaryOperationRelevantData[P], ): FlexibilityMessage.ProvideFlexOptions = { val (operatingPoint, _) = determineOperatingPoint(state, relevantData) val power = operatingPoint.activePower @@ -121,26 +123,32 @@ final case class PrimaryDataParticipantModel[T <: PrimaryData: ClassTag]( state: ParticipantModel.ConstantState.type, flexOptions: FlexibilityMessage.ProvideFlexOptions, setPower: Power, - ): (PrimaryOperatingPoint[T], ParticipantModel.ModelChangeIndicator) = { - ??? // fixme hmmm + // todo relevant data needed + ): (PrimaryOperatingPoint[_, P], ParticipantModel.ModelChangeIndicator) = { + ??? // fixme hmmm. scale by amount of setPower in relation to active power } } object PrimaryDataParticipantModel { - final case class PrimaryOperationRelevantData[+T <: PrimaryData](data: T) + final case class PrimaryOperationRelevantData[+P <: PrimaryData[_]](data: P) extends OperationRelevantData - trait PrimaryOperatingPoint[+T <: PrimaryData] extends OperatingPoint { - val data: T + trait PrimaryOperatingPoint[T <: PrimaryOperatingPoint[ + T, + P, + ], +P <: PrimaryData[_]] + extends OperatingPoint[PrimaryOperatingPoint[T, P]] { + val data: P override val activePower: Power = data.p - } object PrimaryOperatingPoint { - def apply[T <: PrimaryData: ClassTag](data: T): PrimaryOperatingPoint[T] = + def apply[P <: PrimaryData[_]: ClassTag]( + data: P + ): PrimaryOperatingPoint[_, P] = data match { case apparentPowerData: PrimaryDataWithApparentPower[_] => PrimaryApparentPowerOperatingPoint(apparentPowerData) @@ -150,29 +158,35 @@ object PrimaryDataParticipantModel { } private final case class PrimaryApparentPowerOperatingPoint[ - T <: PrimaryDataWithApparentPower[T] - ](override val data: T) - extends PrimaryOperatingPoint[T] { + +P <: PrimaryDataWithApparentPower[P] + ](override val data: P) + extends PrimaryOperatingPoint[PrimaryApparentPowerOperatingPoint[_], P] { override val reactivePower: Option[ReactivePower] = Some(data.q) + + override def zero: PrimaryApparentPowerOperatingPoint[P] = + copy(data = data.scale(0d)) } private final case class PrimaryActivePowerOperatingPoint[ - +T <: PrimaryData with EnrichableData[T2], - T2 <: T with PrimaryDataWithApparentPower[T2], + +P <: PrimaryData[P] with EnrichableData[P2], + P2 <: P with PrimaryDataWithApparentPower[P2], ]( - override val data: T - ) extends PrimaryOperatingPoint[T] { + override val data: P + ) extends PrimaryOperatingPoint[PrimaryActivePowerOperatingPoint[_, P2], P] { override val reactivePower: Option[ReactivePower] = None + + override def zero: PrimaryActivePowerOperatingPoint[P, P2] = + copy(data = data.scale(0d)) } /** Function needs to be packaged to be store it in a val - * @tparam T + * @tparam P */ trait PrimaryResultFunc[ - T <: PrimaryData + P <: PrimaryData[_] ] { def createResult( - data: T with PrimaryDataWithApparentPower[_], + data: P with PrimaryDataWithApparentPower[_], dateTime: ZonedDateTime, ): SystemParticipantResult } diff --git a/src/main/scala/edu/ie3/simona/model/participant2/PvModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/PvModel.scala index 10b8711434..06decc317d 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/PvModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/PvModel.scala @@ -724,7 +724,8 @@ class PvModel private ( override def createResults( state: ParticipantModel.ConstantState.type, - operatingPoint: ActivePowerOperatingPoint, + lastOperatingPoint: Option[ActivePowerOperatingPoint], + currentOperatingPoint: ActivePowerOperatingPoint, complexPower: ApparentPower, dateTime: ZonedDateTime, ): Iterable[SystemParticipantResult] = diff --git a/src/main/scala/edu/ie3/simona/model/participant2/StorageModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/StorageModel.scala index d7f945c662..197d9d321e 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/StorageModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/StorageModel.scala @@ -131,7 +131,8 @@ class StorageModel private ( override def createResults( state: StorageState, - operatingPoint: ActivePowerOperatingPoint, + lastOperatingPoint: Option[ActivePowerOperatingPoint], + currentOperatingPoint: ActivePowerOperatingPoint, complexPower: PrimaryData.ApparentPower, dateTime: ZonedDateTime, ): Iterable[SystemParticipantResult] = diff --git a/src/main/scala/edu/ie3/simona/model/participant2/evcs/EvcsModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/evcs/EvcsModel.scala index aefce7ff11..cc1c7258c6 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/evcs/EvcsModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/evcs/EvcsModel.scala @@ -8,6 +8,7 @@ package edu.ie3.simona.model.participant2.evcs import edu.ie3.datamodel.models.ElectricCurrentType import edu.ie3.datamodel.models.result.system.{ + EvResult, EvcsResult, SystemParticipantResult, } @@ -38,6 +39,7 @@ import edu.ie3.util.scala.quantities.ReactivePower import squants.energy.Watts import squants.time.Seconds import squants.{Dimensionless, Energy, Power} +import tech.units.indriya.unit.Units.PERCENT import java.time.ZonedDateTime import java.util.UUID @@ -57,6 +59,7 @@ class EvcsModel private ( EvcsRelevantData, ] with EvcsChargingProperties { + override def determineOperatingPoint( state: EvcsState, relevantData: EvcsRelevantData, @@ -129,10 +132,53 @@ class EvcsModel private ( override def createResults( state: EvcsState, - operatingPoint: EvcsOperatingPoint, + lastOperatingPoint: Option[EvcsOperatingPoint], + currentOperatingPoint: EvcsOperatingPoint, complexPower: PrimaryData.ApparentPower, dateTime: ZonedDateTime, - ): Iterable[SystemParticipantResult] = ??? + ): Iterable[SystemParticipantResult] = { + val evResults = state.evs.flatMap { case (uuid, ev) => + val lastOp = lastOperatingPoint.flatMap(_.evOperatingPoints.get(uuid)) + val currentOp = currentOperatingPoint.evOperatingPoints.get(uuid) + + val currentPower = currentOp.getOrElse(zeroKW) + + val resultPower = + // only take results that are different from last time + if (!lastOp.contains(currentPower)) + Some(currentPower) + // create 0 kW results for EVs that are not charging anymore + else if (lastOp.isDefined && currentOp.isEmpty) + Some(zeroKW) + else + None + + resultPower.map { activePower => + // assume that reactive power is proportional to active power + val reactivePower = complexPower.q * (activePower / complexPower.p) + + val soc = (ev.storedEnergy / ev.eStorage).asPu + .to(PERCENT) + + new EvResult( + dateTime, + uuid, + activePower.toMegawatts.asMegaWatt, + reactivePower.toMegavars.asMegaVar, + soc, + ) + } + } + + val evcsResult = new EvcsResult( + dateTime, + uuid, + complexPower.p.toMegawatts.asMegaWatt, + complexPower.q.toMegavars.asMegaVar, + ) + + evResults ++ Iterable(evcsResult) + } override def createPrimaryDataResult( data: PrimaryData.PrimaryDataWithApparentPower[_], @@ -150,8 +196,6 @@ class EvcsModel private ( ServiceType.EvMovementService ) - def getInitialState: EvcsState = EvcsState(Map.empty, -1) - override def createRelevantData( receivedData: Seq[Data], nodalVoltage: Dimensionless, @@ -297,6 +341,7 @@ class EvcsModel private ( evPower.min(sRated) } + def getInitialState: EvcsState = EvcsState(Map.empty, -1) } object EvcsModel { @@ -307,15 +352,11 @@ object EvcsModel { override val activePower: Power = evOperatingPoints.values.reduceOption(_ + _).getOrElse(zeroKW) - override val reactivePower: Option[ReactivePower] = ??? + override val reactivePower: Option[ReactivePower] = None override def zero: EvcsOperatingPoint = EvcsOperatingPoint(Map.empty) } - /** @param evs - * TODO also save starting tick, so that results are not cluttered - * @param tick - */ final case class EvcsState( evs: Map[UUID, EvModelWrapper], override val tick: Long, diff --git a/src/main/scala/edu/ie3/simona/service/primary/PrimaryServiceWorker.scala b/src/main/scala/edu/ie3/simona/service/primary/PrimaryServiceWorker.scala index 7ff05ccf1a..5b301f6d1c 100644 --- a/src/main/scala/edu/ie3/simona/service/primary/PrimaryServiceWorker.scala +++ b/src/main/scala/edu/ie3/simona/service/primary/PrimaryServiceWorker.scala @@ -299,7 +299,7 @@ final case class PrimaryServiceWorker[V <: Value]( */ private def announcePrimaryData( tick: Long, - primaryData: PrimaryData, + primaryData: PrimaryData[_], serviceBaseStateData: PrimaryServiceInitializedStateData[V], ): ( PrimaryServiceInitializedStateData[V], @@ -434,7 +434,7 @@ object PrimaryServiceWorker { final case class ProvidePrimaryDataMessage( override val tick: Long, override val serviceRef: ActorRef, - override val data: PrimaryData, + override val data: PrimaryData[_], override val nextDataTick: Option[Long], - ) extends ServiceMessage.ProvisionMessage[PrimaryData] + ) extends ServiceMessage.ProvisionMessage[PrimaryData[_]] } diff --git a/src/test/scala/edu/ie3/simona/agent/participant/RichValueSpec.scala b/src/test/scala/edu/ie3/simona/agent/participant/RichValueSpec.scala index 5710c15417..fea1a8a155 100644 --- a/src/test/scala/edu/ie3/simona/agent/participant/RichValueSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/participant/RichValueSpec.scala @@ -135,7 +135,7 @@ class RichValueSpec extends UnitSpec with TableDrivenPropertyChecks { ), ) - forAll(table)({ case (value: Value, primaryData: PrimaryData) => + forAll(table)({ case (value: Value, primaryData: PrimaryData[_]) => value.toPrimaryData match { case Success(actualPrimaryData) => actualPrimaryData shouldBe primaryData From 284786c0e4b7d1f596465f532bafca58fb0aa8b8 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Sat, 26 Oct 2024 22:00:55 +0200 Subject: [PATCH 14/77] Some fixes concerning types Signed-off-by: Sebastian Peter --- .../simona/agent/participant/data/Data.scala | 11 +++- .../participant2/ParticipantFlexibility.scala | 4 +- .../model/participant2/ParticipantModel.scala | 21 +++--- .../participant2/ParticipantModelInit.scala | 40 ++++++++---- .../participant2/ParticipantModelShell.scala | 48 ++++++++------ .../PrimaryDataParticipantModel.scala | 64 +++++++++---------- .../simona/model/participant2/PvModel.scala | 3 + .../model/participant2/StorageModel.scala | 4 ++ .../model/participant2/evcs/EvcsModel.scala | 10 ++- 9 files changed, 124 insertions(+), 81 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/agent/participant/data/Data.scala b/src/main/scala/edu/ie3/simona/agent/participant/data/Data.scala index 1470c012ef..e04b25d613 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant/data/Data.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant/data/Data.scala @@ -33,11 +33,16 @@ object Data { * model invocation. Anyway, primary data has to have at least active power * given */ - sealed trait PrimaryData[+T <: PrimaryData[T]] extends Data { + sealed trait PrimaryData[T <: PrimaryData[T]] extends Data { + val p: Power def toApparentPower: ApparentPower - def scale(factor: Double): T + def scale(factor: Double): this.type + } + + sealed trait PrimaryDataMeta[T <: PrimaryData[_]] { + def zero: T } object PrimaryData { @@ -49,7 +54,7 @@ object Data { /** Denoting all primary data, that carry apparent power */ sealed trait PrimaryDataWithApparentPower[ - +T <: PrimaryDataWithApparentPower[T] + T <: PrimaryDataWithApparentPower[T] ] extends PrimaryData[T] { val q: ReactivePower diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantFlexibility.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantFlexibility.scala index fcc8917259..e1fbf59515 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantFlexibility.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantFlexibility.scala @@ -19,7 +19,7 @@ import edu.ie3.util.scala.quantities.DefaultQuantities import squants.energy.Power trait ParticipantFlexibility[ - OP <: OperatingPoint[_], + OP <: OperatingPoint, S <: ModelState, OR <: OperationRelevantData, ] { @@ -30,6 +30,7 @@ trait ParticipantFlexibility[ def handlePowerControl( state: S, + relevantData: OR, flexOptions: ProvideFlexOptions, setPower: Power, ): (OP, ModelChangeIndicator) @@ -56,6 +57,7 @@ object ParticipantFlexibility { override def handlePowerControl( state: S, + relevantData: OR, flexOptions: ProvideFlexOptions, setPower: Power, ): (ActivePowerOperatingPoint, ModelChangeIndicator) = { diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala index 2ebfa11ad6..b42f97ebb4 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala @@ -31,7 +31,7 @@ import java.time.ZonedDateTime import java.util.UUID abstract class ParticipantModel[ - OP <: OperatingPoint[_], + OP <: OperatingPoint, S <: ModelState, OR <: OperationRelevantData, ] extends ParticipantFlexibility[OP, S, OR] { @@ -72,6 +72,8 @@ abstract class ParticipantModel[ */ def determineOperatingPoint(state: S, relevantData: OR): (OP, Option[Long]) + def zeroPowerOperatingPoint: OP + /** Determines the current state given the last state and the operating point * that has been valid from the last state up until now. * @@ -160,8 +162,7 @@ object ParticipantModel { trait OperationRelevantData - trait OperatingPoint[+OP <: OperatingPoint[OP]] { - this: OP => + trait OperatingPoint { val activePower: Power @@ -169,17 +170,19 @@ object ParticipantModel { * the active-to-reactive-power function is used. */ val reactivePower: Option[ReactivePower] + } - def zero: OP + object OperatingPoint { + def a: String = "a" } final case class ActivePowerOperatingPoint(override val activePower: Power) - extends OperatingPoint[ActivePowerOperatingPoint] { + extends OperatingPoint { override val reactivePower: Option[ReactivePower] = None + } - override def zero: ActivePowerOperatingPoint = ActivePowerOperatingPoint( - zeroKW - ) + object ActivePowerOperatingPoint { + def zero: ActivePowerOperatingPoint = ActivePowerOperatingPoint(zeroKW) } trait ModelState { @@ -191,7 +194,7 @@ object ParticipantModel { } trait ParticipantConstantModel[ - OP <: OperatingPoint[_], + OP <: OperatingPoint, OR <: OperationRelevantData, ] { this: ParticipantModel[OP, ConstantState.type, OR] => diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelInit.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelInit.scala index 97ab103159..64d0dd7879 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelInit.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelInit.scala @@ -19,7 +19,11 @@ import edu.ie3.simona.config.SimonaConfig.{ StorageRuntimeConfig, } import edu.ie3.simona.exceptions.CriticalFailureException -import edu.ie3.simona.model.participant2.ParticipantModel.ModelState +import edu.ie3.simona.model.participant2.ParticipantModel.{ + ModelState, + OperatingPoint, + OperationRelevantData, +} import edu.ie3.simona.model.participant2.PrimaryDataParticipantModel.PrimaryResultFunc import java.time.ZonedDateTime @@ -27,19 +31,20 @@ import scala.reflect.ClassTag object ParticipantModelInit { - def createModel[S <: ModelState]( + def createModel( participantInput: SystemParticipantInput, modelConfig: BaseRuntimeConfig, - ): ParticipantModelInitContainer[_] = { - - // function needed because Scala does not recognize Java type parameter - def scale[B <: SystemParticipantInputCopyBuilder[B]]( - builder: B - ): Double => SystemParticipantInputCopyBuilder[B] = - factor => builder.scale(factor) + ): ParticipantModelInitContainer[ + _ <: OperatingPoint, + _ <: ModelState, + _ <: OperationRelevantData, + ] = { val scaledParticipantInput = - scale(participantInput.copy)(modelConfig.scaling).build() + (participantInput.copy().scale(modelConfig.scaling) match { + // matching needed because Scala has trouble recognizing the Java type parameter + case copyBuilder: SystemParticipantInputCopyBuilder[_] => copyBuilder + }).build() (scaledParticipantInput, modelConfig) match { case (input: PvInput, _) => @@ -62,7 +67,11 @@ object ParticipantModelInit { def createPrimaryModel[P <: PrimaryData[_]: ClassTag]( participantInput: SystemParticipantInput, modelConfig: BaseRuntimeConfig, - ): ParticipantModelInitContainer[_] = { + ): ParticipantModelInitContainer[ + _ <: OperatingPoint, + _ <: ModelState, + _ <: OperationRelevantData, + ] = { // Create a fitting physical model to extract parameters from val modelContainer = createModel( participantInput, @@ -84,6 +93,7 @@ object ParticipantModelInit { physicalModel.cosPhiRated, physicalModel.qControl, primaryResultFunc, + ???, // todo needs to be provided by primary data service? ) ParticipantModelInitContainer( @@ -92,8 +102,12 @@ object ParticipantModelInit { ) } - final case class ParticipantModelInitContainer[S <: ModelState]( - model: ParticipantModel[_, S, _], + final case class ParticipantModelInitContainer[ + OP <: OperatingPoint, + S <: ModelState, + OR <: OperationRelevantData, + ]( + model: ParticipantModel[OP, S, OR] with ParticipantFlexibility[OP, S, OR], initialState: S, ) } diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala index b16e561dea..ff776bcea0 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala @@ -22,6 +22,7 @@ import edu.ie3.simona.model.participant2.ParticipantModel.{ OperatingPoint, OperationRelevantData, } +import edu.ie3.simona.model.participant2.ParticipantModelInit.ParticipantModelInitContainer import edu.ie3.simona.model.participant2.ParticipantModelShell.ResultsContainer import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage.{ IssueFlexControl, @@ -45,7 +46,7 @@ import java.time.ZonedDateTime * - flex options? (only current needed) */ final case class ParticipantModelShell[ - OP <: OperatingPoint[_], + OP <: OperatingPoint, S <: ModelState, OR <: OperationRelevantData, ]( @@ -88,26 +89,23 @@ final case class ParticipantModelShell[ ) val (newOperatingPoint, newNextTick) = - model.determineOperatingPoint( - state, - relevantData.getOrElse( - throw new CriticalFailureException("No relevant data available!") - ), - ) - - val (adaptedOperatingPoint, adaptedNextTick) = - if (!operationInterval.includes(currentTick)) { + if (!operationInterval.includes(currentTick)) // Current tick is outside of operation interval. // Set operating point to "zero" - (newOperatingPoint.zero, None) - } else - (newOperatingPoint, newNextTick) + (model.zeroPowerOperatingPoint, None) + else + model.determineOperatingPoint( + state, + relevantData.getOrElse( + throw new CriticalFailureException("No relevant data available!") + ), + ) copy( state = currentState, lastOperatingPoint = operatingPoint, - operatingPoint = Some(adaptedOperatingPoint), - modelChange = ModelChangeIndicator(changesAtTick = adaptedNextTick), + operatingPoint = Some(newOperatingPoint), + modelChange = ModelChangeIndicator(changesAtTick = newNextTick), ) } @@ -170,7 +168,14 @@ final case class ParticipantModelShell[ ) val (newOperatingPoint, modelChange) = - model.handlePowerControl(currentState, fo, setPointActivePower) + model.handlePowerControl( + currentState, + relevantData.getOrElse( + throw new CriticalFailureException("No relevant data available!") + ), + fo, + setPointActivePower, + ) copy( state = currentState, @@ -245,12 +250,17 @@ object ParticipantModelShell { ) } - private def createShell( - modelContainer: ParticipantModelInit.ParticipantModelInitContainer[_], + private def createShell[ + OP <: OperatingPoint, + S <: ModelState, + OR <: OperationRelevantData, + ]( + modelContainer: ParticipantModelInitContainer[OP, S, OR], participantInput: SystemParticipantInput, simulationEndDate: ZonedDateTime, simulationStartDate: ZonedDateTime, - ): ParticipantModelShell[_, _, _] = { + ): ParticipantModelShell[OP, S, OR] = { + val operationInterval: OperationInterval = SystemComponent.determineOperationInterval( simulationStartDate, diff --git a/src/main/scala/edu/ie3/simona/model/participant2/PrimaryDataParticipantModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/PrimaryDataParticipantModel.scala index 8dde3e5547..3ca12143ed 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/PrimaryDataParticipantModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/PrimaryDataParticipantModel.scala @@ -8,7 +8,7 @@ package edu.ie3.simona.model.participant2 import edu.ie3.datamodel.models.result.system.SystemParticipantResult import edu.ie3.simona.agent.participant.data.Data -import edu.ie3.simona.agent.participant.data.Data.PrimaryData +import edu.ie3.simona.agent.participant.data.Data.{PrimaryData, PrimaryDataMeta} import edu.ie3.simona.agent.participant.data.Data.PrimaryData.{ EnrichableData, PrimaryDataWithApparentPower, @@ -17,6 +17,7 @@ import edu.ie3.simona.exceptions.CriticalFailureException import edu.ie3.simona.model.participant.control.QControl import edu.ie3.simona.model.participant2.ParticipantModel.{ ConstantState, + ModelChangeIndicator, OperatingPoint, OperationRelevantData, ParticipantConstantModel, @@ -40,28 +41,30 @@ final case class PrimaryDataParticipantModel[P <: PrimaryData[_]: ClassTag]( override val cosPhiRated: Double, override val qControl: QControl, primaryDataResultFunc: PrimaryResultFunc[P], + primaryDataMeta: PrimaryDataMeta[P], ) extends ParticipantModel[ - PrimaryOperatingPoint[_, P], + PrimaryOperatingPoint[P], ConstantState.type, PrimaryOperationRelevantData[P], ] - with ParticipantConstantModel[PrimaryOperatingPoint[ - _, - P, - ], PrimaryOperationRelevantData[ - P - ]] { + with ParticipantConstantModel[ + PrimaryOperatingPoint[P], + PrimaryOperationRelevantData[P], + ] { override def determineOperatingPoint( - state: ParticipantModel.ConstantState.type, + state: ConstantState.type, relevantData: PrimaryOperationRelevantData[P], - ): (PrimaryOperatingPoint[_, P], Option[Long]) = + ): (PrimaryOperatingPoint[P], Option[Long]) = (PrimaryOperatingPoint(relevantData.data), None) + override def zeroPowerOperatingPoint: PrimaryOperatingPoint[P] = + PrimaryOperatingPoint(primaryDataMeta.zero) + override def createResults( - state: ParticipantModel.ConstantState.type, - lastOperatingPoint: Option[PrimaryOperatingPoint[_, P]], - currentOperatingPoint: PrimaryOperatingPoint[_, P], + state: ConstantState.type, + lastOperatingPoint: Option[PrimaryOperatingPoint[P]], + currentOperatingPoint: PrimaryOperatingPoint[P], complexPower: PrimaryData.ApparentPower, dateTime: ZonedDateTime, ): Iterable[SystemParticipantResult] = { @@ -110,7 +113,7 @@ final case class PrimaryDataParticipantModel[P <: PrimaryData[_]: ClassTag]( } override def calcFlexOptions( - state: ParticipantModel.ConstantState.type, + state: ConstantState.type, relevantData: PrimaryOperationRelevantData[P], ): FlexibilityMessage.ProvideFlexOptions = { val (operatingPoint, _) = determineOperatingPoint(state, relevantData) @@ -120,12 +123,15 @@ final case class PrimaryDataParticipantModel[P <: PrimaryData[_]: ClassTag]( } override def handlePowerControl( - state: ParticipantModel.ConstantState.type, + state: ConstantState.type, + relevantData: PrimaryOperationRelevantData[P], flexOptions: FlexibilityMessage.ProvideFlexOptions, setPower: Power, - // todo relevant data needed - ): (PrimaryOperatingPoint[_, P], ParticipantModel.ModelChangeIndicator) = { - ??? // fixme hmmm. scale by amount of setPower in relation to active power + ): (PrimaryOperatingPoint[P], ModelChangeIndicator) = { + val factor = relevantData.data.p / setPower + val scaledData = relevantData.data.scale(factor) + + (PrimaryOperatingPoint(scaledData), ModelChangeIndicator()) } } @@ -135,11 +141,7 @@ object PrimaryDataParticipantModel { final case class PrimaryOperationRelevantData[+P <: PrimaryData[_]](data: P) extends OperationRelevantData - trait PrimaryOperatingPoint[T <: PrimaryOperatingPoint[ - T, - P, - ], +P <: PrimaryData[_]] - extends OperatingPoint[PrimaryOperatingPoint[T, P]] { + trait PrimaryOperatingPoint[+P <: PrimaryData[_]] extends OperatingPoint { val data: P override val activePower: Power = data.p @@ -148,7 +150,7 @@ object PrimaryDataParticipantModel { object PrimaryOperatingPoint { def apply[P <: PrimaryData[_]: ClassTag]( data: P - ): PrimaryOperatingPoint[_, P] = + ): PrimaryOperatingPoint[P] = data match { case apparentPowerData: PrimaryDataWithApparentPower[_] => PrimaryApparentPowerOperatingPoint(apparentPowerData) @@ -158,25 +160,19 @@ object PrimaryDataParticipantModel { } private final case class PrimaryApparentPowerOperatingPoint[ - +P <: PrimaryDataWithApparentPower[P] + P <: PrimaryDataWithApparentPower[_] ](override val data: P) - extends PrimaryOperatingPoint[PrimaryApparentPowerOperatingPoint[_], P] { + extends PrimaryOperatingPoint[P] { override val reactivePower: Option[ReactivePower] = Some(data.q) - - override def zero: PrimaryApparentPowerOperatingPoint[P] = - copy(data = data.scale(0d)) } private final case class PrimaryActivePowerOperatingPoint[ - +P <: PrimaryData[P] with EnrichableData[P2], + P <: PrimaryData[_] with EnrichableData[P2], P2 <: P with PrimaryDataWithApparentPower[P2], ]( override val data: P - ) extends PrimaryOperatingPoint[PrimaryActivePowerOperatingPoint[_, P2], P] { + ) extends PrimaryOperatingPoint[P] { override val reactivePower: Option[ReactivePower] = None - - override def zero: PrimaryActivePowerOperatingPoint[P, P2] = - copy(data = data.scale(0d)) } /** Function needs to be packaged to be store it in a val diff --git a/src/main/scala/edu/ie3/simona/model/participant2/PvModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/PvModel.scala index 06decc317d..73ba677c9e 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/PvModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/PvModel.scala @@ -158,6 +158,9 @@ class PvModel private ( (ActivePowerOperatingPoint(power), None) } + override def zeroPowerOperatingPoint: ActivePowerOperatingPoint = + ActivePowerOperatingPoint.zero + /** Calculates the position of the earth in relation to the sun (day angle) * for the provided time * diff --git a/src/main/scala/edu/ie3/simona/model/participant2/StorageModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/StorageModel.scala index 197d9d321e..32a2575af7 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/StorageModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/StorageModel.scala @@ -113,6 +113,9 @@ class StorageModel private ( "Storage model cannot calculate operation point without flexibility control." ) + override def zeroPowerOperatingPoint: ActivePowerOperatingPoint = + ActivePowerOperatingPoint.zero + override def determineState( lastState: StorageState, operatingPoint: ActivePowerOperatingPoint, @@ -213,6 +216,7 @@ class StorageModel private ( override def handlePowerControl( state: StorageState, + relevantData: StorageRelevantData, flexOptions: FlexibilityMessage.ProvideFlexOptions, setPower: Power, ): (ActivePowerOperatingPoint, ParticipantModel.ModelChangeIndicator) = { diff --git a/src/main/scala/edu/ie3/simona/model/participant2/evcs/EvcsModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/evcs/EvcsModel.scala index cc1c7258c6..13ce6cd967 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/evcs/EvcsModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/evcs/EvcsModel.scala @@ -81,6 +81,9 @@ class EvcsModel private ( (EvcsOperatingPoint(chargingPowers), nextEvent) } + override def zeroPowerOperatingPoint: EvcsOperatingPoint = + EvcsOperatingPoint.zero + private def determineNextEvent( ev: EvModelWrapper, chargingPower: Power, @@ -276,6 +279,7 @@ class EvcsModel private ( override def handlePowerControl( state: EvcsState, + relevantData: EvcsRelevantData, flexOptions: FlexibilityMessage.ProvideFlexOptions, setPower: Power, ): (EvcsOperatingPoint, ParticipantModel.ModelChangeIndicator) = ??? @@ -347,14 +351,16 @@ class EvcsModel private ( object EvcsModel { final case class EvcsOperatingPoint(evOperatingPoints: Map[UUID, Power]) - extends OperatingPoint[EvcsOperatingPoint] { + extends OperatingPoint { override val activePower: Power = evOperatingPoints.values.reduceOption(_ + _).getOrElse(zeroKW) override val reactivePower: Option[ReactivePower] = None + } - override def zero: EvcsOperatingPoint = EvcsOperatingPoint(Map.empty) + object EvcsOperatingPoint { + def zero: EvcsOperatingPoint = EvcsOperatingPoint(Map.empty) } final case class EvcsState( From ec5620f23bba46e09c178f6cf34a0fff7fc9101e Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Mon, 28 Oct 2024 11:18:37 +0100 Subject: [PATCH 15/77] Fixing type problems in PrimaryDataParticipantModel Signed-off-by: Sebastian Peter --- .../PrimaryDataParticipantModel.scala | 30 ++++++++----------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/model/participant2/PrimaryDataParticipantModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/PrimaryDataParticipantModel.scala index 3ca12143ed..196a7d1d17 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/PrimaryDataParticipantModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/PrimaryDataParticipantModel.scala @@ -40,7 +40,7 @@ final case class PrimaryDataParticipantModel[P <: PrimaryData[_]: ClassTag]( override val sRated: Power, override val cosPhiRated: Double, override val qControl: QControl, - primaryDataResultFunc: PrimaryResultFunc[P], + primaryDataResultFunc: PrimaryResultFunc, primaryDataMeta: PrimaryDataMeta[P], ) extends ParticipantModel[ PrimaryOperatingPoint[P], @@ -68,11 +68,11 @@ final case class PrimaryDataParticipantModel[P <: PrimaryData[_]: ClassTag]( complexPower: PrimaryData.ApparentPower, dateTime: ZonedDateTime, ): Iterable[SystemParticipantResult] = { - val primaryDataWithApparentPower = currentOperatingPoint match { - case PrimaryApparentPowerOperatingPoint(data) => - data - case PrimaryActivePowerOperatingPoint(data) => - data.add(complexPower.q) + val primaryDataWithApparentPower = currentOperatingPoint.data match { + case primaryDataWithApparentPower: PrimaryDataWithApparentPower[_] => + primaryDataWithApparentPower + case enrichableData: EnrichableData[_] => + enrichableData.add(complexPower.q): PrimaryDataWithApparentPower[_] } Iterable( primaryDataResultFunc.createResult(primaryDataWithApparentPower, dateTime) @@ -152,9 +152,9 @@ object PrimaryDataParticipantModel { data: P ): PrimaryOperatingPoint[P] = data match { - case apparentPowerData: PrimaryDataWithApparentPower[_] => + case apparentPowerData: P with PrimaryDataWithApparentPower[_] => PrimaryApparentPowerOperatingPoint(apparentPowerData) - case other => + case other: P with EnrichableData[_] => PrimaryActivePowerOperatingPoint(other) } } @@ -167,22 +167,18 @@ object PrimaryDataParticipantModel { } private final case class PrimaryActivePowerOperatingPoint[ - P <: PrimaryData[_] with EnrichableData[P2], - P2 <: P with PrimaryDataWithApparentPower[P2], + PE <: PrimaryData[_] with EnrichableData[_]: ClassTag ]( - override val data: P - ) extends PrimaryOperatingPoint[P] { + override val data: PE + ) extends PrimaryOperatingPoint[PE] { override val reactivePower: Option[ReactivePower] = None } /** Function needs to be packaged to be store it in a val - * @tparam P */ - trait PrimaryResultFunc[ - P <: PrimaryData[_] - ] { + trait PrimaryResultFunc { def createResult( - data: P with PrimaryDataWithApparentPower[_], + data: PrimaryDataWithApparentPower[_], dateTime: ZonedDateTime, ): SystemParticipantResult } From 084a389ffc12b6060da2f98c40512be275892e9c Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Mon, 28 Oct 2024 11:19:05 +0100 Subject: [PATCH 16/77] Scapegoat fixes Signed-off-by: Sebastian Peter --- .../scala/edu/ie3/simona/agent/participant/data/Data.scala | 4 ++++ .../ie3/simona/agent/participant2/ParticipantAgent.scala | 6 +++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main/scala/edu/ie3/simona/agent/participant/data/Data.scala b/src/main/scala/edu/ie3/simona/agent/participant/data/Data.scala index e04b25d613..6c6c64add0 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant/data/Data.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant/data/Data.scala @@ -90,6 +90,10 @@ object Data { ActivePower(p = p * factor) } + object ActivePowerMeta extends PrimaryDataMeta[ActivePower] { + override def zero: ActivePower = ActivePower(zeroKW) + } + /** Active and Reactive power as participant simulation result * * @param p diff --git a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgent.scala b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgent.scala index 13fc5e94b8..ed3a4ba106 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgent.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgent.scala @@ -199,7 +199,11 @@ object ParticipantAgent { ctx.log, ) - val result = updatedGridAdapter.avgPowerResult.get + val result = updatedGridAdapter.avgPowerResult.getOrElse( + throw new CriticalFailureException( + "Power result has not been calculated" + ) + ) gridAdapter.gridAgent ! (if (result.newResult) { AssetPowerChangedMessage( From 0fe4c589d3b1ac556cbae7317a5a3cb3f02d94b4 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Mon, 28 Oct 2024 11:42:25 +0100 Subject: [PATCH 17/77] More type fixes Signed-off-by: Sebastian Peter --- .../simona/agent/participant/data/Data.scala | 51 +++++++++++++------ .../participant2/ParticipantModelInit.scala | 4 +- .../PrimaryDataParticipantModel.scala | 4 +- 3 files changed, 39 insertions(+), 20 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/agent/participant/data/Data.scala b/src/main/scala/edu/ie3/simona/agent/participant/data/Data.scala index 6c6c64add0..38f6e1fd9e 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant/data/Data.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant/data/Data.scala @@ -33,16 +33,15 @@ object Data { * model invocation. Anyway, primary data has to have at least active power * given */ - sealed trait PrimaryData[T <: PrimaryData[T]] extends Data { - + sealed trait PrimaryData[+T <: PrimaryData[T]] extends Data { val p: Power def toApparentPower: ApparentPower - - def scale(factor: Double): this.type } sealed trait PrimaryDataMeta[T <: PrimaryData[_]] { def zero: T + + def scale(data: T, factor: Double): T } object PrimaryData { @@ -54,7 +53,7 @@ object Data { /** Denoting all primary data, that carry apparent power */ sealed trait PrimaryDataWithApparentPower[ - T <: PrimaryDataWithApparentPower[T] + +T <: PrimaryDataWithApparentPower[T] ] extends PrimaryData[T] { val q: ReactivePower @@ -85,13 +84,13 @@ object Data { override def add(q: ReactivePower): ApparentPower = ApparentPower(p, q) - - override def scale(factor: Double): ActivePower = - ActivePower(p = p * factor) } object ActivePowerMeta extends PrimaryDataMeta[ActivePower] { override def zero: ActivePower = ActivePower(zeroKW) + + override def scale(data: ActivePower, factor: Double): ActivePower = + ActivePower(data.p * factor) } /** Active and Reactive power as participant simulation result @@ -109,9 +108,13 @@ object Data { override def withReactivePower(q: ReactivePower): ApparentPower = copy(q = q) + } - override def scale(factor: Double): ApparentPower = - ApparentPower(p = p * factor, q = q * factor) + object ApparentPowerMeta extends PrimaryDataMeta[ApparentPower] { + override def zero: ApparentPower = ApparentPower(zeroKW, zeroKVAr) + + override def scale(data: ApparentPower, factor: Double): ApparentPower = + ApparentPower(data.p * factor, data.q * factor) } /** Active power and heat demand as participant simulation result @@ -135,9 +138,16 @@ object Data { override def add(q: ReactivePower): ApparentPowerAndHeat = ApparentPowerAndHeat(p, q, qDot) + } + + object ActivePowerAndHeatMeta extends PrimaryDataMeta[ActivePowerAndHeat] { + override def zero: ActivePowerAndHeat = ActivePowerAndHeat(zeroKW, zeroKW) - override def scale(factor: Double): ActivePowerAndHeat = - ActivePowerAndHeat(p = p * factor, qDot = qDot * factor) + override def scale( + data: ActivePowerAndHeat, + factor: Double, + ): ActivePowerAndHeat = + ActivePowerAndHeat(data.p * factor, data.qDot * factor) } /** Apparent power and heat demand as participant simulation result @@ -160,12 +170,21 @@ object Data { override def withReactivePower(q: ReactivePower): ApparentPowerAndHeat = copy(q = q) + } + + object ApparentPowerAndHeatMeta + extends PrimaryDataMeta[ApparentPowerAndHeat] { + override def zero: ApparentPowerAndHeat = + ApparentPowerAndHeat(zeroKW, zeroKVAr, zeroKW) - override def scale(factor: Double): ApparentPowerAndHeat = + override def scale( + data: ApparentPowerAndHeat, + factor: Double, + ): ApparentPowerAndHeat = ApparentPowerAndHeat( - p = p * factor, - q = q * factor, - qDot = qDot * factor, + data.p * factor, + data.q * factor, + data.qDot * factor, ) } diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelInit.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelInit.scala index 64d0dd7879..4489c17f0a 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelInit.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelInit.scala @@ -79,9 +79,9 @@ object ParticipantModelInit { ) val physicalModel = modelContainer.model - val primaryResultFunc = new PrimaryResultFunc[P] { + val primaryResultFunc = new PrimaryResultFunc { override def createResult( - data: P with PrimaryData.PrimaryDataWithApparentPower[_], + data: PrimaryData.PrimaryDataWithApparentPower[_], dateTime: ZonedDateTime, ): SystemParticipantResult = physicalModel.createPrimaryDataResult(data, dateTime) diff --git a/src/main/scala/edu/ie3/simona/model/participant2/PrimaryDataParticipantModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/PrimaryDataParticipantModel.scala index 196a7d1d17..d5655c392f 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/PrimaryDataParticipantModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/PrimaryDataParticipantModel.scala @@ -72,7 +72,7 @@ final case class PrimaryDataParticipantModel[P <: PrimaryData[_]: ClassTag]( case primaryDataWithApparentPower: PrimaryDataWithApparentPower[_] => primaryDataWithApparentPower case enrichableData: EnrichableData[_] => - enrichableData.add(complexPower.q): PrimaryDataWithApparentPower[_] + enrichableData.add(complexPower.q) } Iterable( primaryDataResultFunc.createResult(primaryDataWithApparentPower, dateTime) @@ -129,7 +129,7 @@ final case class PrimaryDataParticipantModel[P <: PrimaryData[_]: ClassTag]( setPower: Power, ): (PrimaryOperatingPoint[P], ModelChangeIndicator) = { val factor = relevantData.data.p / setPower - val scaledData = relevantData.data.scale(factor) + val scaledData: P = primaryDataMeta.scale(relevantData.data, factor) (PrimaryOperatingPoint(scaledData), ModelChangeIndicator()) } From f73887bce16dc8063f459903e73e0cbe50582f97 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Mon, 28 Oct 2024 11:56:44 +0100 Subject: [PATCH 18/77] Reverting self type of primary data Signed-off-by: Sebastian Peter --- .../agent/participant/ParticipantAgent.scala | 2 +- .../ParticipantAgentFundamentals.scala | 8 ++++---- .../simona/agent/participant/data/Data.scala | 12 ++++++------ .../data/primary/PrimaryDataService.scala | 2 +- .../statedata/InitializeStateData.scala | 5 ++--- .../statedata/ParticipantStateData.scala | 18 +++++++++--------- .../statedata/UninitializedStateData.scala | 2 +- .../participant2/ParticipantModelInit.scala | 2 +- .../PrimaryDataParticipantModel.scala | 10 +++++----- .../service/primary/PrimaryServiceWorker.scala | 6 +++--- .../agent/participant/RichValueSpec.scala | 2 +- 11 files changed, 34 insertions(+), 35 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/agent/participant/ParticipantAgent.scala b/src/main/scala/edu/ie3/simona/agent/participant/ParticipantAgent.scala index 0d44b92d3d..52e3a53a59 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant/ParticipantAgent.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant/ParticipantAgent.scala @@ -888,7 +888,7 @@ object ParticipantAgent { * nodal voltage */ def getAndCheckNodalVoltage( - baseStateData: BaseStateData[_ <: PrimaryData[_]], + baseStateData: BaseStateData[_ <: PrimaryData], currentTick: Long, ): Dimensionless = { baseStateData.voltageValueStore.last(currentTick) match { diff --git a/src/main/scala/edu/ie3/simona/agent/participant/ParticipantAgentFundamentals.scala b/src/main/scala/edu/ie3/simona/agent/participant/ParticipantAgentFundamentals.scala index ddb61b57d7..0b0ad206e0 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant/ParticipantAgentFundamentals.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant/ParticipantAgentFundamentals.scala @@ -993,17 +993,17 @@ protected trait ParticipantAgentFundamentals[ .flatMap { case (_, maybeData) => maybeData } - .fold[Try[PrimaryData[_]]] { + .fold[Try[PrimaryData]] { Failure( new IllegalStateException( "Not able to determine the most recent result, although it should have been sent." ) ) } { - case result: PrimaryData[_] + case result: PrimaryData if pdClassTag.runtimeClass.equals(result.getClass) => Success(result) - case primaryData: PrimaryData[_] => + case primaryData: PrimaryData => primaryData match { case pd: EnrichableData[_] => val q = @@ -1981,7 +1981,7 @@ object ParticipantAgentFundamentals { * @tparam PD * Type of primary data, that is relevant for the next calculation */ - final case class RelevantResultValues[+PD <: PrimaryData[_]]( + final case class RelevantResultValues[+PD <: PrimaryData]( windowStart: Long, windowEnd: Long, relevantData: Map[Long, PD], diff --git a/src/main/scala/edu/ie3/simona/agent/participant/data/Data.scala b/src/main/scala/edu/ie3/simona/agent/participant/data/Data.scala index 38f6e1fd9e..8a3fd0fb88 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant/data/Data.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant/data/Data.scala @@ -33,12 +33,12 @@ object Data { * model invocation. Anyway, primary data has to have at least active power * given */ - sealed trait PrimaryData[+T <: PrimaryData[T]] extends Data { + sealed trait PrimaryData extends Data { val p: Power def toApparentPower: ApparentPower } - sealed trait PrimaryDataMeta[T <: PrimaryData[_]] { + sealed trait PrimaryDataMeta[T <: PrimaryData] { def zero: T def scale(data: T, factor: Double): T @@ -54,7 +54,7 @@ object Data { */ sealed trait PrimaryDataWithApparentPower[ +T <: PrimaryDataWithApparentPower[T] - ] extends PrimaryData[T] { + ] extends PrimaryData { val q: ReactivePower def withReactivePower(q: ReactivePower): T @@ -74,7 +74,7 @@ object Data { * Active power */ final case class ActivePower(override val p: Power) - extends PrimaryData[ActivePower] + extends PrimaryData with EnrichableData[ApparentPower] { override def toApparentPower: ApparentPower = ApparentPower( @@ -127,7 +127,7 @@ object Data { final case class ActivePowerAndHeat( override val p: Power, override val qDot: Power, - ) extends PrimaryData[ActivePowerAndHeat] + ) extends PrimaryData with Heat with EnrichableData[ApparentPowerAndHeat] { override def toApparentPower: ApparentPower = @@ -189,7 +189,7 @@ object Data { } implicit class RichValue(private val value: Value) { - def toPrimaryData: Try[PrimaryData[_]] = + def toPrimaryData: Try[PrimaryData] = value match { case hs: HeatAndSValue => (hs.getP.toScala, hs.getQ.toScala, hs.getHeatDemand.toScala) match { diff --git a/src/main/scala/edu/ie3/simona/agent/participant/data/primary/PrimaryDataService.scala b/src/main/scala/edu/ie3/simona/agent/participant/data/primary/PrimaryDataService.scala index 1365277b52..33cb4b7519 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant/data/primary/PrimaryDataService.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant/data/primary/PrimaryDataService.scala @@ -13,7 +13,7 @@ import edu.ie3.simona.agent.participant.data.DataService /** Enum-like trait to denote possible external data sources for systems */ -sealed trait PrimaryDataService[+D <: PrimaryData[_]] extends DataService[D] +sealed trait PrimaryDataService[+D <: PrimaryData] extends DataService[D] object PrimaryDataService { diff --git a/src/main/scala/edu/ie3/simona/agent/participant/statedata/InitializeStateData.scala b/src/main/scala/edu/ie3/simona/agent/participant/statedata/InitializeStateData.scala index c049b13472..de01009850 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant/statedata/InitializeStateData.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant/statedata/InitializeStateData.scala @@ -13,8 +13,7 @@ import edu.ie3.simona.event.notifier.NotifierConfig * information needed to initialize a * [[edu.ie3.simona.agent.participant.ParticipantAgent]] */ -trait InitializeStateData[+PD <: PrimaryData[_]] - extends ParticipantStateData[PD] { +trait InitializeStateData[+PD <: PrimaryData] extends ParticipantStateData[PD] { /** Config for the output behaviour of simulation results */ @@ -22,7 +21,7 @@ trait InitializeStateData[+PD <: PrimaryData[_]] } object InitializeStateData { - final case class TrivialInitializeStateData[+PD <: PrimaryData[_]]( + final case class TrivialInitializeStateData[+PD <: PrimaryData]( resultEventEmitter: String ) extends InitializeStateData[PD] { val outputConfig: NotifierConfig = NotifierConfig( diff --git a/src/main/scala/edu/ie3/simona/agent/participant/statedata/ParticipantStateData.scala b/src/main/scala/edu/ie3/simona/agent/participant/statedata/ParticipantStateData.scala index 2e66b932b5..95d8df3fcf 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant/statedata/ParticipantStateData.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant/statedata/ParticipantStateData.scala @@ -22,18 +22,18 @@ import java.time.ZonedDateTime /** Trait to denote all common forms of state data related to each participant * agent */ -trait ParticipantStateData[+PD <: PrimaryData[_]] +trait ParticipantStateData[+PD <: PrimaryData] object ParticipantStateData { /** Data for the state, in which the agent is not initialized, yet. *

IMPORTANT: Needs to be an empty case class due to typing

*/ - final class ParticipantUninitializedStateData[+PD <: PrimaryData[_]]() + final class ParticipantUninitializedStateData[+PD <: PrimaryData]() extends UninitializedStateData[PD] object ParticipantUninitializedStateData { - def apply[PD <: PrimaryData[_]](): ParticipantUninitializedStateData[PD] = + def apply[PD <: PrimaryData](): ParticipantUninitializedStateData[PD] = new ParticipantUninitializedStateData() } @@ -67,7 +67,7 @@ object ParticipantStateData { final case class ParticipantInitializingStateData[ I <: SystemParticipantInput, C <: SimonaConfig.BaseRuntimeConfig, - PD <: PrimaryData[_], + PD <: PrimaryData, ]( inputModel: InputModelContainer[I], modelConfig: C, @@ -111,7 +111,7 @@ object ParticipantStateData { final case class ParticipantInitializeStateData[ I <: SystemParticipantInput, C <: SimonaConfig.BaseRuntimeConfig, - PD <: PrimaryData[_], + PD <: PrimaryData, ]( inputModel: InputModelContainer[I], modelConfig: C, @@ -130,7 +130,7 @@ object ParticipantStateData { def apply[ I <: SystemParticipantInput, C <: SimonaConfig.BaseRuntimeConfig, - PD <: PrimaryData[_], + PD <: PrimaryData, ]( inputModel: I, modelConfig: C, @@ -159,7 +159,7 @@ object ParticipantStateData { def apply[ I <: SystemParticipantInput, C <: SimonaConfig.BaseRuntimeConfig, - PD <: PrimaryData[_], + PD <: PrimaryData, ]( inputModel: I, modelConfig: C, @@ -190,7 +190,7 @@ object ParticipantStateData { def apply[ I <: SystemParticipantInput, C <: SimonaConfig.BaseRuntimeConfig, - PD <: PrimaryData[_], + PD <: PrimaryData, ]( inputModel: I, thermalGrid: ThermalGrid, @@ -221,7 +221,7 @@ object ParticipantStateData { def apply[ I <: SystemParticipantInput, C <: SimonaConfig.BaseRuntimeConfig, - PD <: PrimaryData[_], + PD <: PrimaryData, ]( inputModel: I, thermalGrid: ThermalGrid, diff --git a/src/main/scala/edu/ie3/simona/agent/participant/statedata/UninitializedStateData.scala b/src/main/scala/edu/ie3/simona/agent/participant/statedata/UninitializedStateData.scala index 5bfda74961..85700927c2 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant/statedata/UninitializedStateData.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant/statedata/UninitializedStateData.scala @@ -10,5 +10,5 @@ import edu.ie3.simona.agent.participant.data.Data.PrimaryData /** Properties common to all participant agents not yet initialized */ -trait UninitializedStateData[+PD <: PrimaryData[_]] +trait UninitializedStateData[+PD <: PrimaryData] extends ParticipantStateData[PD] diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelInit.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelInit.scala index 4489c17f0a..37ffdb034f 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelInit.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelInit.scala @@ -64,7 +64,7 @@ object ParticipantModelInit { } } - def createPrimaryModel[P <: PrimaryData[_]: ClassTag]( + def createPrimaryModel[P <: PrimaryData: ClassTag]( participantInput: SystemParticipantInput, modelConfig: BaseRuntimeConfig, ): ParticipantModelInitContainer[ diff --git a/src/main/scala/edu/ie3/simona/model/participant2/PrimaryDataParticipantModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/PrimaryDataParticipantModel.scala index d5655c392f..4e03762c81 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/PrimaryDataParticipantModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/PrimaryDataParticipantModel.scala @@ -35,7 +35,7 @@ import scala.reflect.ClassTag /** Just "replaying" primary data */ -final case class PrimaryDataParticipantModel[P <: PrimaryData[_]: ClassTag]( +final case class PrimaryDataParticipantModel[P <: PrimaryData: ClassTag]( override val uuid: UUID, override val sRated: Power, override val cosPhiRated: Double, @@ -138,17 +138,17 @@ final case class PrimaryDataParticipantModel[P <: PrimaryData[_]: ClassTag]( object PrimaryDataParticipantModel { - final case class PrimaryOperationRelevantData[+P <: PrimaryData[_]](data: P) + final case class PrimaryOperationRelevantData[+P <: PrimaryData](data: P) extends OperationRelevantData - trait PrimaryOperatingPoint[+P <: PrimaryData[_]] extends OperatingPoint { + trait PrimaryOperatingPoint[+P <: PrimaryData] extends OperatingPoint { val data: P override val activePower: Power = data.p } object PrimaryOperatingPoint { - def apply[P <: PrimaryData[_]: ClassTag]( + def apply[P <: PrimaryData: ClassTag]( data: P ): PrimaryOperatingPoint[P] = data match { @@ -167,7 +167,7 @@ object PrimaryDataParticipantModel { } private final case class PrimaryActivePowerOperatingPoint[ - PE <: PrimaryData[_] with EnrichableData[_]: ClassTag + PE <: PrimaryData with EnrichableData[_]: ClassTag ]( override val data: PE ) extends PrimaryOperatingPoint[PE] { diff --git a/src/main/scala/edu/ie3/simona/service/primary/PrimaryServiceWorker.scala b/src/main/scala/edu/ie3/simona/service/primary/PrimaryServiceWorker.scala index 5b301f6d1c..7ff05ccf1a 100644 --- a/src/main/scala/edu/ie3/simona/service/primary/PrimaryServiceWorker.scala +++ b/src/main/scala/edu/ie3/simona/service/primary/PrimaryServiceWorker.scala @@ -299,7 +299,7 @@ final case class PrimaryServiceWorker[V <: Value]( */ private def announcePrimaryData( tick: Long, - primaryData: PrimaryData[_], + primaryData: PrimaryData, serviceBaseStateData: PrimaryServiceInitializedStateData[V], ): ( PrimaryServiceInitializedStateData[V], @@ -434,7 +434,7 @@ object PrimaryServiceWorker { final case class ProvidePrimaryDataMessage( override val tick: Long, override val serviceRef: ActorRef, - override val data: PrimaryData[_], + override val data: PrimaryData, override val nextDataTick: Option[Long], - ) extends ServiceMessage.ProvisionMessage[PrimaryData[_]] + ) extends ServiceMessage.ProvisionMessage[PrimaryData] } diff --git a/src/test/scala/edu/ie3/simona/agent/participant/RichValueSpec.scala b/src/test/scala/edu/ie3/simona/agent/participant/RichValueSpec.scala index fea1a8a155..5710c15417 100644 --- a/src/test/scala/edu/ie3/simona/agent/participant/RichValueSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/participant/RichValueSpec.scala @@ -135,7 +135,7 @@ class RichValueSpec extends UnitSpec with TableDrivenPropertyChecks { ), ) - forAll(table)({ case (value: Value, primaryData: PrimaryData[_]) => + forAll(table)({ case (value: Value, primaryData: PrimaryData) => value.toPrimaryData match { case Success(actualPrimaryData) => actualPrimaryData shouldBe primaryData From df800bfc656d38aaf72bd02630f4272c2e0b9e0d Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Mon, 28 Oct 2024 13:08:52 +0100 Subject: [PATCH 19/77] Adding power ctrl handling of EvcsModel Signed-off-by: Sebastian Peter --- .../model/participant2/ParticipantModel.scala | 19 +- .../model/participant2/evcs/EvcsModel.scala | 258 +++++++++++++++--- 2 files changed, 243 insertions(+), 34 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala index b42f97ebb4..67b5567e5c 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala @@ -217,6 +217,23 @@ object ParticipantModel { final case class ModelChangeIndicator( changesAtNextActivation: Boolean = false, changesAtTick: Option[Long] = None, - ) + ) { + + /** Combines two ModelChangeIndicators by aggregating + * changesAtNextActivation via OR function and picking the earlier (or any) + * of both changesAtTick values. + * + * @param otherIndicator + * The other ModelChangeIndicator to combine with this one + * @return + * An aggregated ModelChangeIndicator + */ + def |(otherIndicator: ModelChangeIndicator): ModelChangeIndicator = { + ModelChangeIndicator( + changesAtNextActivation || otherIndicator.changesAtNextActivation, + Seq(changesAtTick, otherIndicator.changesAtTick).flatten.minOption, + ) + } + } } diff --git a/src/main/scala/edu/ie3/simona/model/participant2/evcs/EvcsModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/evcs/EvcsModel.scala index 13ce6cd967..43b6b07ffd 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/evcs/EvcsModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/evcs/EvcsModel.scala @@ -19,6 +19,7 @@ import edu.ie3.simona.model.participant.control.QControl import edu.ie3.simona.model.participant.evcs.EvModelWrapper import edu.ie3.simona.model.participant2.ParticipantModel import edu.ie3.simona.model.participant2.ParticipantModel.{ + ModelChangeIndicator, ModelState, OperatingPoint, OperationRelevantData, @@ -36,7 +37,7 @@ import edu.ie3.simona.service.ServiceType import edu.ie3.util.quantities.QuantityUtils.RichQuantityDouble import edu.ie3.util.scala.quantities.DefaultQuantities._ import edu.ie3.util.scala.quantities.ReactivePower -import squants.energy.Watts +import squants.energy.{Kilowatts, Watts} import squants.time.Seconds import squants.{Dimensionless, Energy, Power} import tech.units.indriya.unit.Units.PERCENT @@ -65,18 +66,16 @@ class EvcsModel private ( relevantData: EvcsRelevantData, ): (EvcsOperatingPoint, Option[Long]) = { val chargingPowers = - strategy.determineChargingPowers(state.evs.values, state.tick, this) + strategy.determineChargingPowers(state.evs, state.tick, this) - val nextEvent = chargingPowers.flatMap { case (uuid, power) => - val ev = state.evs.getOrElse( - uuid, - throw new CriticalFailureException( - s"Charging strategy ${strategy.getClass.getSimpleName} returned a charging power for unknown UUID $uuid" - ), - ) - - determineNextEvent(ev, power) - }.minOption + val nextEvent = state.evs + .flatMap { ev => + chargingPowers.get(ev.uuid).map((ev, _)) + } + .flatMap { case (ev, power) => + determineNextEvent(ev, power) + } + .minOption (EvcsOperatingPoint(chargingPowers), nextEvent) } @@ -116,18 +115,17 @@ class EvcsModel private ( currentTick: Long, ): EvcsState = { - val updatedEvs = lastState.evs.map { case (uuid, ev) => - uuid -> - operatingPoint.evOperatingPoints - .get(uuid) - .map { chargingPower => - val newStoredEnergy = ev.storedEnergy + - chargingPower * Seconds( - currentTick - lastState.tick - ) - ev.copy(storedEnergy = newStoredEnergy) - } - .getOrElse(ev) + val updatedEvs = lastState.evs.map { ev => + operatingPoint.evOperatingPoints + .get(ev.uuid) + .map { chargingPower => + val newStoredEnergy = ev.storedEnergy + + chargingPower * Seconds( + currentTick - lastState.tick + ) + ev.copy(storedEnergy = newStoredEnergy) + } + .getOrElse(ev) } EvcsState(updatedEvs, currentTick) @@ -140,9 +138,9 @@ class EvcsModel private ( complexPower: PrimaryData.ApparentPower, dateTime: ZonedDateTime, ): Iterable[SystemParticipantResult] = { - val evResults = state.evs.flatMap { case (uuid, ev) => - val lastOp = lastOperatingPoint.flatMap(_.evOperatingPoints.get(uuid)) - val currentOp = currentOperatingPoint.evOperatingPoints.get(uuid) + val evResults = state.evs.flatMap { ev => + val lastOp = lastOperatingPoint.flatMap(_.evOperatingPoints.get(ev.uuid)) + val currentOp = currentOperatingPoint.evOperatingPoints.get(ev.uuid) val currentPower = currentOp.getOrElse(zeroKW) @@ -165,7 +163,7 @@ class EvcsModel private ( new EvResult( dateTime, - uuid, + ev.uuid, activePower.toMegawatts.asMegaWatt, reactivePower.toMegavars.asMegaVar, soc, @@ -222,7 +220,7 @@ class EvcsModel private ( ): FlexibilityMessage.ProvideFlexOptions = { val preferredPowers = - strategy.determineChargingPowers(state.evs.values, state.tick, this) + strategy.determineChargingPowers(state.evs, state.tick, this) val (maxCharging, preferredPower, forcedCharging, maxDischarging) = state.evs.foldLeft( @@ -230,7 +228,7 @@ class EvcsModel private ( ) { case ( (chargingSum, preferredSum, forcedSum, dischargingSum), - (uuid, ev), + ev, ) => val maxPower = getMaxAvailableChargingPower(ev) @@ -282,7 +280,201 @@ class EvcsModel private ( relevantData: EvcsRelevantData, flexOptions: FlexibilityMessage.ProvideFlexOptions, setPower: Power, - ): (EvcsOperatingPoint, ParticipantModel.ModelChangeIndicator) = ??? + ): (EvcsOperatingPoint, ModelChangeIndicator) = { + if (setPower == zeroKW) + return ( + EvcsOperatingPoint(Map.empty), + ModelChangeIndicator(), + ) + + // applicable evs can be charged/discharged, other evs cannot + val applicableEvs = state.evs.filter { ev => + if (setPower > zeroKW) + !isFull(ev) + else + !isEmpty(ev) + } + + val (forcedChargingEvs, regularChargingEvs) = + if (setPower > zeroKW) + // lower margin is excluded since charging is not required here anymore + applicableEvs.partition { ev => + isEmpty(ev) && !isInLowerMargin(ev) + } + else + (Seq.empty, applicableEvs) + + val (forcedSchedules, remainingPower) = + createScheduleWithSetPower(state.tick, forcedChargingEvs, setPower) + + val (regularSchedules, _) = + createScheduleWithSetPower(state.tick, regularChargingEvs, remainingPower) + + val combinedSchedules = forcedSchedules ++ regularSchedules + + val allSchedules = combinedSchedules.map { case (ev, (power, _)) => + ev -> power + }.toMap + + val aggregatedChangeIndicator = combinedSchedules + .map { case (_, (_, indicator)) => indicator } + .foldLeft(ModelChangeIndicator()) { + case (aggregateIndicator, otherIndicator) => + aggregateIndicator | otherIndicator + } + + ( + EvcsOperatingPoint(allSchedules), + aggregatedChangeIndicator, + ) + } + + /** Distributes some set power value across given EVs, taking into + * consideration the maximum charging power of EVs and the charging station + * + * @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: Seq[EvModelWrapper], + setPower: Power, + ): ( + Seq[(UUID, (Power, ModelChangeIndicator))], + Power, + ) = { + + if (evs.isEmpty) return (Seq.empty, setPower) + + if (setPower.~=(zeroKW)(Kilowatts(1e-6))) { + // No power left. Rest is not charging + return (Seq.empty, zeroKW) + } + + val proposedPower = setPower.divide(evs.size) + + val (exceedingPowerEvs, fittingPowerEvs) = evs.partition { ev => + if (setPower > zeroKW) + proposedPower > getMaxAvailableChargingPower(ev) + else + proposedPower < (getMaxAvailableChargingPower(ev) * -1) + } + + if (exceedingPowerEvs.isEmpty) { + // end of recursion, rest of charging power fits to all + + val results = fittingPowerEvs.map { ev => + val chargingTicks = calcFlexOptionsChange(ev, proposedPower) + val endTick = Math.min(currentTick + chargingTicks, ev.departureTick) + + ( + ev.uuid, + ( + proposedPower, + ModelChangeIndicator( + changesAtNextActivation = + isFull(ev) || isEmpty(ev) || isInLowerMargin(ev), + changesAtTick = Some(endTick), + ), + ), + ) + } + + (results, zeroKW) + } else { + // not all evs can be charged with proposed power + + // charge all exceeded evs with their respective maximum power + val maxCharged = exceedingPowerEvs.map { ev => + val maxPower = getMaxAvailableChargingPower(ev) + val power = + if (setPower > zeroKW) + maxPower + else + maxPower * (-1) + + val chargingTicks = calcFlexOptionsChange(ev, power) + val endTick = Math.min(currentTick + chargingTicks, ev.departureTick) + + (ev, power, endTick) + } + + val maxChargedResults = maxCharged.map { case (ev, power, endTick) => + ( + ev.uuid, + ( + power, + ModelChangeIndicator( + changesAtNextActivation = + isFull(ev) || isEmpty(ev) || isInLowerMargin(ev), + changesAtTick = Some(endTick), + ), + ), + ) + } + + // sum up allocated power + val chargingPowerSum = maxCharged.foldLeft(zeroKW) { + case (powerSum, (_, chargingPower, _)) => + powerSum + chargingPower + } + + val remainingAfterAllocation = setPower - chargingPowerSum + + // go into the next recursion step with the remaining power + val (nextIterationResults, remainingAfterRecursion) = + createScheduleWithSetPower( + currentTick, + fittingPowerEvs, + remainingAfterAllocation, + ) + + val combinedResults = maxChargedResults ++ nextIterationResults + + (combinedResults, remainingAfterRecursion) + } + + } + + /** Calculates the duration (in ticks) until the flex options will change + * next, which could be the battery being fully charged or discharged or the + * minimum SOC requirement being reached + * + * @param ev + * The EV to charge/discharge + * @param power + * The charging/discharging power + * @return + * The tick at which flex options will change + */ + private def calcFlexOptionsChange( + ev: EvModelWrapper, + power: Power, + ): Long = { + val timeUntilFullOrEmpty = + if (power > zeroKW) { + + // if we're below lowest SOC, flex options will change at that point + val targetEnergy = + if (isEmpty(ev) && !isInLowerMargin(ev)) + ev.eStorage * lowestEvSoc + else + ev.eStorage + + (targetEnergy - ev.storedEnergy) / power + } else + (ev.storedEnergy - (ev.eStorage * lowestEvSoc)) / (power * (-1)) + + Math.round(timeUntilFullOrEmpty.toSeconds) + } /** @param ev * the ev whose stored energy is to be checked @@ -345,7 +537,7 @@ class EvcsModel private ( evPower.min(sRated) } - def getInitialState: EvcsState = EvcsState(Map.empty, -1) + def getInitialState: EvcsState = EvcsState(Seq.empty, -1) } object EvcsModel { @@ -364,7 +556,7 @@ object EvcsModel { } final case class EvcsState( - evs: Map[UUID, EvModelWrapper], + evs: Seq[EvModelWrapper], override val tick: Long, ) extends ModelState From 8d159aa79cd5cd702dcfd8eb7ef4f6c62af6c96a Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Mon, 28 Oct 2024 13:13:23 +0100 Subject: [PATCH 20/77] Refactoring getRequiredServices Signed-off-by: Sebastian Peter --- .../simona/agent/participant2/ParticipantAgentInit.scala | 3 ++- .../ie3/simona/model/participant2/ParticipantModel.scala | 7 +++++-- .../model/participant2/PrimaryDataParticipantModel.scala | 2 +- .../scala/edu/ie3/simona/model/participant2/PvModel.scala | 2 +- .../edu/ie3/simona/model/participant2/StorageModel.scala | 3 ++- .../edu/ie3/simona/model/participant2/evcs/EvcsModel.scala | 2 +- 6 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgentInit.scala b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgentInit.scala index 0084a265cd..705552ec64 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgentInit.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgentInit.scala @@ -162,7 +162,8 @@ object ParticipantAgentInit { simulationEndDate, ) - val requiredServiceTypes = modelShell.model.getRequiredServices.toSeq + val requiredServiceTypes = + modelShell.model.getRequiredSecondaryServices.toSeq if (requiredServiceTypes.isEmpty) { createAgent( diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala index 67b5567e5c..ad54a116a4 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala @@ -134,8 +134,11 @@ abstract class ParticipantModel[ ): S = throw new NotImplementedError(s"Method not implemented by $getClass") - // todo split off the following to ParticipantModelMeta? - def getRequiredServices: Iterable[ServiceType] + /** @return + * All secondary services required by the model for creating operation + * relevant data [[OR]] + */ + def getRequiredSecondaryServices: Iterable[ServiceType] /** @param receivedData * The received primary or secondary data diff --git a/src/main/scala/edu/ie3/simona/model/participant2/PrimaryDataParticipantModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/PrimaryDataParticipantModel.scala index 4e03762c81..5bb4ccdf8e 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/PrimaryDataParticipantModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/PrimaryDataParticipantModel.scala @@ -86,7 +86,7 @@ final case class PrimaryDataParticipantModel[P <: PrimaryData: ClassTag]( "Method not implemented by this model." ) - override def getRequiredServices: Iterable[ServiceType] = { + override def getRequiredSecondaryServices: Iterable[ServiceType] = { // only secondary services should be specified here Iterable.empty } diff --git a/src/main/scala/edu/ie3/simona/model/participant2/PvModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/PvModel.scala index 73ba677c9e..b5c7bb03f9 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/PvModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/PvModel.scala @@ -752,7 +752,7 @@ class PvModel private ( data.q.toMegavars.asMegaVar, ) - override def getRequiredServices: Iterable[ServiceType] = + override def getRequiredSecondaryServices: Iterable[ServiceType] = Iterable(ServiceType.WeatherService) override def createRelevantData( diff --git a/src/main/scala/edu/ie3/simona/model/participant2/StorageModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/StorageModel.scala index 32a2575af7..8770aa217a 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/StorageModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/StorageModel.scala @@ -161,7 +161,8 @@ class StorageModel private ( (-1).asPu, // FIXME currently not supported ) - override def getRequiredServices: Iterable[ServiceType] = Iterable.empty + override def getRequiredSecondaryServices: Iterable[ServiceType] = + Iterable.empty override def createRelevantData( receivedData: Seq[Data], diff --git a/src/main/scala/edu/ie3/simona/model/participant2/evcs/EvcsModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/evcs/EvcsModel.scala index 43b6b07ffd..7dba3e2277 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/evcs/EvcsModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/evcs/EvcsModel.scala @@ -192,7 +192,7 @@ class EvcsModel private ( data.q.toMegavars.asMegaVar, ) - override def getRequiredServices: Iterable[ServiceType] = + override def getRequiredSecondaryServices: Iterable[ServiceType] = Iterable( ServiceType.EvMovementService ) From 647dff11b317ed116fd2cc3f7c7e14803c8c69dc Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Mon, 28 Oct 2024 13:39:30 +0100 Subject: [PATCH 21/77] Adding WecModel Signed-off-by: Sebastian Peter --- .../participant2/ParticipantModelInit.scala | 5 + .../simona/model/participant2/WecModel.scala | 272 ++++++++++++++++++ 2 files changed, 277 insertions(+) create mode 100644 src/main/scala/edu/ie3/simona/model/participant2/WecModel.scala diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelInit.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelInit.scala index 37ffdb034f..b5b1f8fbd6 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelInit.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelInit.scala @@ -11,6 +11,7 @@ import edu.ie3.datamodel.models.input.system.{ PvInput, StorageInput, SystemParticipantInput, + WecInput, } import edu.ie3.datamodel.models.result.system.SystemParticipantResult import edu.ie3.simona.agent.participant.data.Data.PrimaryData @@ -51,6 +52,10 @@ object ParticipantModelInit { val model = PvModel(input) val state = model.getInitialState ParticipantModelInitContainer(model, state) + case (input: WecInput, _) => + val model = WecModel(input) + val state = model.getInitialState + ParticipantModelInitContainer(model, state) case (input: StorageInput, config: StorageRuntimeConfig) => val model = StorageModel(input, config) val state = model.getInitialState(config) diff --git a/src/main/scala/edu/ie3/simona/model/participant2/WecModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/WecModel.scala new file mode 100644 index 0000000000..38fe633bb9 --- /dev/null +++ b/src/main/scala/edu/ie3/simona/model/participant2/WecModel.scala @@ -0,0 +1,272 @@ +/* + * © 2020. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.model.participant2 + +import com.typesafe.scalalogging.LazyLogging +import edu.ie3.datamodel.models.input.system.WecInput +import edu.ie3.datamodel.models.input.system.characteristic.WecCharacteristicInput +import edu.ie3.datamodel.models.result.system.{ + SystemParticipantResult, + WecResult, +} +import edu.ie3.simona.agent.participant.data.Data +import edu.ie3.simona.agent.participant.data.Data.PrimaryData +import edu.ie3.simona.agent.participant.data.Data.PrimaryData.ApparentPower +import edu.ie3.simona.exceptions.CriticalFailureException +import edu.ie3.simona.model.participant.control.QControl +import edu.ie3.simona.model.participant2.ParticipantFlexibility.ParticipantSimpleFlexibility +import edu.ie3.simona.model.participant2.ParticipantModel.{ + ActivePowerOperatingPoint, + ConstantState, + OperationRelevantData, + ParticipantConstantModel, +} +import edu.ie3.simona.model.participant2.WecModel.{ + WecCharacteristic, + WecRelevantData, + molarMassAir, + universalGasConstantR, +} +import edu.ie3.simona.model.system.Characteristic +import edu.ie3.simona.model.system.Characteristic.XYPair +import edu.ie3.simona.ontology.messages.services.WeatherMessage.WeatherData +import edu.ie3.simona.service.ServiceType +import edu.ie3.util.quantities.PowerSystemUnits.{KILOWATT, PU} +import edu.ie3.util.quantities.QuantityUtils.RichQuantityDouble +import squants._ +import squants.energy.{Kilowatts, Watts} +import squants.mass.{Kilograms, KilogramsPerCubicMeter} +import squants.motion.{MetersPerSecond, Pressure} +import squants.space.SquareMeters +import squants.thermal.JoulesPerKelvin +import tech.units.indriya.unit.Units._ + +import java.time.ZonedDateTime +import java.util.UUID +import scala.collection.SortedSet + +class WecModel private ( + override val uuid: UUID, + override val sRated: Power, + override val cosPhiRated: Double, + override val qControl: QControl, + private val rotorArea: Area, + private val betzCurve: WecCharacteristic, +) extends ParticipantModel[ + ActivePowerOperatingPoint, + ConstantState.type, + WecRelevantData, + ] + with ParticipantConstantModel[ActivePowerOperatingPoint, WecRelevantData] + with ParticipantSimpleFlexibility[ConstantState.type, WecRelevantData] + with LazyLogging { + + /** Calculate the active power behaviour of the model + * + * @param data + * Further needed, secondary data + * @return + * Active power + */ + override def determineOperatingPoint( + modelState: ConstantState.type, + data: WecRelevantData, + ): (ActivePowerOperatingPoint, Option[Long]) = { + val betzCoefficient = determineBetzCoefficient(data.windVelocity) + + /** air density in kg/m³ + */ + val airDensity = + calculateAirDensity( + data.temperature, + data.airPressure, + ).toKilogramsPerCubicMeter + + val v = data.windVelocity.toMetersPerSecond + + /** cubed velocity in m³/s³ + */ + val cubedVelocity = v * v * v + + // Combined, we get (kg * m²)/s³, which is Watts + val power = Watts( + cubedVelocity * 0.5 * betzCoefficient.toEach * airDensity * rotorArea.toSquareMeters + ) + + (ActivePowerOperatingPoint(power), None) + } + + /** The coefficient is dependent on the wind velocity v. Therefore use v to + * determine the betz coefficient cₚ. + * + * @param windVelocity + * current wind velocity + * @return + * betz coefficient cₚ + */ + def determineBetzCoefficient( + windVelocity: Velocity + ): Dimensionless = { + betzCurve.interpolateXy(windVelocity) match { + case (_, cp) => cp + } + } + + /** Calculate the correct air density, dependent on the current temperature + * and air pressure. + * + * If no air pressure is given, the default density 1.2041 is returned (air + * density for 20 degrees Celsius at sea level) + * + * @param temperature + * current temperature + * @param airPressure + * current air pressure + * @return + */ + def calculateAirDensity( + temperature: Temperature, + airPressure: Option[Pressure], + ): Density = { + airPressure match { + case None => + KilogramsPerCubicMeter(1.2041d) + case Some(pressure) => + // kg * mol^-1 * J * m^-3 * J^-1 * K * mol * K^-1 + // = kg * m^-3 + KilogramsPerCubicMeter( + molarMassAir.toKilograms * pressure.toPascals / (universalGasConstantR.toJoulesPerKelvin * temperature.toKelvinScale) + ) + } + } + + override def zeroPowerOperatingPoint: ActivePowerOperatingPoint = + ActivePowerOperatingPoint.zero + + override def createResults( + state: ParticipantModel.ConstantState.type, + lastOperatingPoint: Option[ActivePowerOperatingPoint], + currentOperatingPoint: ActivePowerOperatingPoint, + complexPower: ApparentPower, + dateTime: ZonedDateTime, + ): Iterable[SystemParticipantResult] = + Iterable( + new WecResult( + dateTime, + uuid, + complexPower.p.toMegawatts.asMegaWatt, + complexPower.q.toMegavars.asMegaVar, + ) + ) + + override def createPrimaryDataResult( + data: PrimaryData.PrimaryDataWithApparentPower[_], + dateTime: ZonedDateTime, + ): SystemParticipantResult = + new WecResult( + dateTime, + uuid, + data.p.toMegawatts.asMegaWatt, + data.q.toMegavars.asMegaVar, + ) + + override def getRequiredSecondaryServices: Iterable[ServiceType] = + Iterable(ServiceType.WeatherService) + + override def createRelevantData( + receivedData: Seq[Data], + nodalVoltage: Dimensionless, + tick: Long, + simulationTime: ZonedDateTime, + ): WecRelevantData = { + receivedData + .collectFirst { case weatherData: WeatherData => + WecRelevantData( + weatherData.windVel, + weatherData.temp, + None, + ) + } + .getOrElse { + throw new CriticalFailureException( + s"Expected WeatherData, got $receivedData" + ) + } + } + +} + +object WecModel { + + /** Universal gas constant + */ + private val universalGasConstantR = JoulesPerKelvin(8.31446261815324d) + + /** Molar mass of air, actually in kg/mol + */ + private val molarMassAir = Kilograms(0.0289647d) + + /** Class that holds all relevant data for wec model calculation + * + * @param windVelocity + * current wind velocity + * @param temperature + * current temperature + * @param airPressure + * current air pressure + */ + final case class WecRelevantData( + windVelocity: Velocity, + temperature: Temperature, + airPressure: Option[Pressure], + ) extends OperationRelevantData + + /** This class is initialized with a [[WecCharacteristicInput]], which + * contains the needed betz curve. + */ + final case class WecCharacteristic( + override val xyCoordinates: SortedSet[ + XYPair[Velocity, Dimensionless] + ] + ) extends Characteristic[Velocity, Dimensionless] + + object WecCharacteristic { + import scala.jdk.CollectionConverters._ + + /** Transform the inputs points from [[java.util.SortedSet]] to + * [[scala.collection.SortedSet]], which is fed into [[WecCharacteristic]]. + */ + def apply(input: WecCharacteristicInput): WecCharacteristic = + new WecCharacteristic( + collection.immutable + .SortedSet[XYPair[Velocity, Dimensionless]]() ++ + input.getPoints.asScala.map(p => + XYPair[Velocity, Dimensionless]( + MetersPerSecond(p.getX.to(METRE_PER_SECOND).getValue.doubleValue), + Each(p.getY.to(PU).getValue.doubleValue), + ) + ) + ) + } + + def apply( + inputModel: WecInput + ): WecModel = + new WecModel( + inputModel.getUuid, + Kilowatts( + inputModel.getType.getsRated.to(KILOWATT).getValue.doubleValue + ), + inputModel.getType.getCosPhiRated, + QControl(inputModel.getqCharacteristics), + SquareMeters( + inputModel.getType.getRotorArea.to(SQUARE_METRE).getValue.doubleValue + ), + WecCharacteristic(inputModel.getType.getCpCharacteristic), + ) + +} From 06db12ee7726ccdbea8c260a6c2fe325e127f0c4 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Mon, 28 Oct 2024 14:00:12 +0100 Subject: [PATCH 22/77] Adding FixedFeedInModel Signed-off-by: Sebastian Peter --- .../model/participant2/FixedFeedInModel.scala | 118 ++++++++++++++++++ .../model/participant2/ParticipantModel.scala | 5 + 2 files changed, 123 insertions(+) create mode 100644 src/main/scala/edu/ie3/simona/model/participant2/FixedFeedInModel.scala diff --git a/src/main/scala/edu/ie3/simona/model/participant2/FixedFeedInModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/FixedFeedInModel.scala new file mode 100644 index 0000000000..44b4c96e84 --- /dev/null +++ b/src/main/scala/edu/ie3/simona/model/participant2/FixedFeedInModel.scala @@ -0,0 +1,118 @@ +/* + * © 2024. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.model.participant2 + +import edu.ie3.datamodel.models.input.system.FixedFeedInInput +import edu.ie3.datamodel.models.result.system.{ + FixedFeedInResult, + SystemParticipantResult, +} +import edu.ie3.simona.agent.participant.data.Data +import edu.ie3.simona.agent.participant.data.Data.PrimaryData +import edu.ie3.simona.model.participant.control.QControl +import edu.ie3.simona.model.participant2.ParticipantFlexibility.ParticipantSimpleFlexibility +import edu.ie3.simona.model.participant2.ParticipantModel.{ + ActivePowerOperatingPoint, + ConstantState, + FixedRelevantData, + ParticipantConstantModel, +} +import edu.ie3.simona.service.ServiceType +import edu.ie3.util.quantities.PowerSystemUnits +import edu.ie3.util.quantities.QuantityUtils.RichQuantityDouble +import squants.energy.Kilowatts +import squants.{Dimensionless, Power} + +import java.time.ZonedDateTime +import java.util.UUID + +class FixedFeedInModel( + override val uuid: UUID, + override val sRated: Power, + override val cosPhiRated: Double, + override val qControl: QControl, +) extends ParticipantModel[ + ActivePowerOperatingPoint, + ConstantState.type, + FixedRelevantData.type, + ] + with ParticipantConstantModel[ + ActivePowerOperatingPoint, + FixedRelevantData.type, + ] + with ParticipantSimpleFlexibility[ + ConstantState.type, + FixedRelevantData.type, + ] { + + override def determineOperatingPoint( + state: ParticipantModel.ConstantState.type, + relevantData: ParticipantModel.FixedRelevantData.type, + ): (ActivePowerOperatingPoint, Option[Long]) = { + val power = sRated * (-1) * cosPhiRated + + (ActivePowerOperatingPoint(power), None) + } + + override def zeroPowerOperatingPoint: ActivePowerOperatingPoint = + ActivePowerOperatingPoint.zero + + override def createResults( + state: ParticipantModel.ConstantState.type, + lastOperatingPoint: Option[ActivePowerOperatingPoint], + currentOperatingPoint: ActivePowerOperatingPoint, + complexPower: PrimaryData.ApparentPower, + dateTime: ZonedDateTime, + ): Iterable[SystemParticipantResult] = + Iterable( + new FixedFeedInResult( + dateTime, + uuid, + complexPower.p.toMegawatts.asMegaWatt, + complexPower.q.toMegavars.asMegaVar, + ) + ) + + override def createPrimaryDataResult( + data: PrimaryData.PrimaryDataWithApparentPower[_], + dateTime: ZonedDateTime, + ): SystemParticipantResult = + new FixedFeedInResult( + dateTime, + uuid, + data.p.toMegawatts.asMegaWatt, + data.q.toMegavars.asMegaVar, + ) + + override def getRequiredSecondaryServices: Iterable[ServiceType] = + Iterable.empty + + override def createRelevantData( + receivedData: Seq[Data], + nodalVoltage: Dimensionless, + tick: Long, + simulationTime: ZonedDateTime, + ): FixedRelevantData.type = FixedRelevantData +} + +object FixedFeedInModel { + def apply( + inputModel: FixedFeedInInput + ): FixedFeedInModel = { + new FixedFeedInModel( + inputModel.getUuid, + Kilowatts( + inputModel.getsRated + .to(PowerSystemUnits.KILOWATT) + .getValue + .doubleValue + ), + inputModel.getCosPhiRated, + QControl.apply(inputModel.getqCharacteristics), + ) + } +} diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala index ad54a116a4..bc2a90f16d 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala @@ -165,6 +165,11 @@ object ParticipantModel { trait OperationRelevantData + /** Passed to model calculation classes for each participant when no secondary + * data is required + */ + case object FixedRelevantData extends OperationRelevantData + trait OperatingPoint { val activePower: Power From 48d5da8b882159d8bfb62f2fff901aa41f53eb2c Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Mon, 28 Oct 2024 18:46:47 +0100 Subject: [PATCH 23/77] Added LoadModels Signed-off-by: Sebastian Peter --- .../model/participant2/ParticipantModel.scala | 6 + .../participant2/ParticipantModelInit.scala | 18 +- .../participant2/load/FixedLoadModel.scala | 81 +++++++++ .../model/participant2/load/LoadModel.scala | 169 ++++++++++++++++++ .../participant2/load/LoadReference.scala | 101 +++++++++++ .../participant2/load/ProfileLoadModel.scala | 98 ++++++++++ .../participant2/load/RandomLoadModel.scala | 164 +++++++++++++++++ 7 files changed, 631 insertions(+), 6 deletions(-) create mode 100644 src/main/scala/edu/ie3/simona/model/participant2/load/FixedLoadModel.scala create mode 100644 src/main/scala/edu/ie3/simona/model/participant2/load/LoadModel.scala create mode 100644 src/main/scala/edu/ie3/simona/model/participant2/load/LoadReference.scala create mode 100644 src/main/scala/edu/ie3/simona/model/participant2/load/ProfileLoadModel.scala create mode 100644 src/main/scala/edu/ie3/simona/model/participant2/load/RandomLoadModel.scala diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala index bc2a90f16d..4c1ee12f4a 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala @@ -170,6 +170,12 @@ object ParticipantModel { */ case object FixedRelevantData extends OperationRelevantData + /** OperationRelevantData that just transports the current datetime + * @param dateTime + * The current datetime + */ + case class DateTimeData(dateTime: ZonedDateTime) extends OperationRelevantData + trait OperatingPoint { val activePower: Power diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelInit.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelInit.scala index b5b1f8fbd6..b1faa5b109 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelInit.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelInit.scala @@ -7,16 +7,12 @@ package edu.ie3.simona.model.participant2 import edu.ie3.datamodel.models.input.system.SystemParticipantInput.SystemParticipantInputCopyBuilder -import edu.ie3.datamodel.models.input.system.{ - PvInput, - StorageInput, - SystemParticipantInput, - WecInput, -} +import edu.ie3.datamodel.models.input.system._ import edu.ie3.datamodel.models.result.system.SystemParticipantResult import edu.ie3.simona.agent.participant.data.Data.PrimaryData import edu.ie3.simona.config.SimonaConfig.{ BaseRuntimeConfig, + LoadRuntimeConfig, StorageRuntimeConfig, } import edu.ie3.simona.exceptions.CriticalFailureException @@ -26,6 +22,7 @@ import edu.ie3.simona.model.participant2.ParticipantModel.{ OperationRelevantData, } import edu.ie3.simona.model.participant2.PrimaryDataParticipantModel.PrimaryResultFunc +import edu.ie3.simona.model.participant2.load.LoadModel import java.time.ZonedDateTime import scala.reflect.ClassTag @@ -48,6 +45,15 @@ object ParticipantModelInit { }).build() (scaledParticipantInput, modelConfig) match { + // fixme ticks not scheduled for fixed feed-in/load models + case (input: FixedFeedInInput, _) => + val model = FixedFeedInModel(input) + val state = model.getInitialState + ParticipantModelInitContainer(model, state) + case (input: LoadInput, config: LoadRuntimeConfig) => + val model = LoadModel(input, config) + val state = model.getInitialState + ParticipantModelInitContainer(model, state) case (input: PvInput, _) => val model = PvModel(input) val state = model.getInitialState diff --git a/src/main/scala/edu/ie3/simona/model/participant2/load/FixedLoadModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/load/FixedLoadModel.scala new file mode 100644 index 0000000000..dd42342980 --- /dev/null +++ b/src/main/scala/edu/ie3/simona/model/participant2/load/FixedLoadModel.scala @@ -0,0 +1,81 @@ +/* + * © 2024. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.model.participant2.load + +import edu.ie3.datamodel.models.input.system.LoadInput +import edu.ie3.simona.agent.participant.data.Data +import edu.ie3.simona.config.SimonaConfig.LoadRuntimeConfig +import edu.ie3.simona.model.participant.control.QControl +import edu.ie3.simona.model.participant.load.LoadReference +import edu.ie3.simona.model.participant.load.LoadReference.{ + ActivePower, + EnergyConsumption, +} +import edu.ie3.simona.model.participant2.ParticipantModel +import edu.ie3.simona.model.participant2.ParticipantModel.{ + ActivePowerOperatingPoint, + FixedRelevantData, +} +import edu.ie3.util.quantities.PowerSystemUnits +import squants.energy.Kilowatts +import squants.time.Days +import squants.{Dimensionless, Power} + +import java.time.ZonedDateTime +import java.util.UUID + +class FixedLoadModel( + override val uuid: UUID, + override val sRated: Power, + override val cosPhiRated: Double, + override val qControl: QControl, + private val activePower: Power, +) extends LoadModel[FixedRelevantData.type] { + + override def determineOperatingPoint( + state: ParticipantModel.ConstantState.type, + relevantData: ParticipantModel.FixedRelevantData.type, + ): (ActivePowerOperatingPoint, Option[Long]) = + (ActivePowerOperatingPoint(activePower), None) + + override def createRelevantData( + receivedData: Seq[Data], + nodalVoltage: Dimensionless, + tick: Long, + simulationTime: ZonedDateTime, + ): ParticipantModel.FixedRelevantData.type = FixedRelevantData + +} + +object FixedLoadModel { + def apply( + inputModel: LoadInput, + config: LoadRuntimeConfig, + ): FixedLoadModel = { + val reference = LoadReference(inputModel, config) + + val activePower: Power = reference match { + case ActivePower(power) => power + case EnergyConsumption(energyConsumption) => + val duration = Days(365d) + energyConsumption / duration + } + + new FixedLoadModel( + inputModel.getUuid, + Kilowatts( + inputModel.getsRated + .to(PowerSystemUnits.KILOWATT) + .getValue + .doubleValue + ), + inputModel.getCosPhiRated, + QControl.apply(inputModel.getqCharacteristics), + activePower, + ) + } +} diff --git a/src/main/scala/edu/ie3/simona/model/participant2/load/LoadModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/load/LoadModel.scala new file mode 100644 index 0000000000..6fb36cbfc0 --- /dev/null +++ b/src/main/scala/edu/ie3/simona/model/participant2/load/LoadModel.scala @@ -0,0 +1,169 @@ +/* + * © 2024. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.model.participant2.load + +import edu.ie3.datamodel.models.input.system.LoadInput +import edu.ie3.datamodel.models.result.system.{ + LoadResult, + SystemParticipantResult, +} +import edu.ie3.simona.agent.participant.data.Data.PrimaryData +import edu.ie3.simona.config.SimonaConfig.LoadRuntimeConfig +import edu.ie3.simona.model.participant.load.LoadModelBehaviour +import edu.ie3.simona.model.participant2.ParticipantFlexibility.ParticipantSimpleFlexibility +import edu.ie3.simona.model.participant2.ParticipantModel +import edu.ie3.simona.model.participant2.ParticipantModel.{ + ActivePowerOperatingPoint, + ConstantState, + OperationRelevantData, + ParticipantConstantModel, +} +import edu.ie3.simona.service.ServiceType +import edu.ie3.util.quantities.PowerSystemUnits +import edu.ie3.util.quantities.QuantityUtils.RichQuantityDouble +import squants.energy.Kilowatts +import squants.{Energy, Power} + +import java.time.ZonedDateTime + +abstract class LoadModel[OR <: OperationRelevantData] + extends ParticipantModel[ + ActivePowerOperatingPoint, + ConstantState.type, + OR, + ] + with ParticipantConstantModel[ + ActivePowerOperatingPoint, + OR, + ] + with ParticipantSimpleFlexibility[ + ConstantState.type, + OR, + ] { + + override def zeroPowerOperatingPoint: ActivePowerOperatingPoint = + ActivePowerOperatingPoint.zero + + override def createResults( + state: ParticipantModel.ConstantState.type, + lastOperatingPoint: Option[ActivePowerOperatingPoint], + currentOperatingPoint: ActivePowerOperatingPoint, + complexPower: PrimaryData.ApparentPower, + dateTime: ZonedDateTime, + ): Iterable[SystemParticipantResult] = + Iterable( + new LoadResult( + dateTime, + uuid, + complexPower.p.toMegawatts.asMegaWatt, + complexPower.q.toMegavars.asMegaVar, + ) + ) + + override def createPrimaryDataResult( + data: PrimaryData.PrimaryDataWithApparentPower[_], + dateTime: ZonedDateTime, + ): SystemParticipantResult = + new LoadResult( + dateTime, + uuid, + data.p.toMegawatts.asMegaWatt, + data.q.toMegavars.asMegaVar, + ) + + override def getRequiredSecondaryServices: Iterable[ServiceType] = + Iterable.empty + +} + +object LoadModel { + + /** Scale profile based load models' sRated based on a provided active power + * value + * + * When the load is scaled to the active power value, the models' sRated is + * multiplied by the ratio of the provided active power value and the active + * power value of the model (activePowerVal / (input.sRated*input.cosPhi) + * + * @param inputModel + * the input model instance + * @param activePower + * the active power value sRated should be scaled to + * @param safetyFactor + * a safety factor to address potential higher sRated values than the + * original scaling would provide (e.g. when using unrestricted probability + * functions) + * @return + * the inputs model sRated scaled to the provided active power + */ + def scaleSRatedActivePower( + inputModel: LoadInput, + activePower: Power, + safetyFactor: Double = 1d, + ): Power = { + val sRated = Kilowatts( + inputModel.getsRated + .to(PowerSystemUnits.KILOWATT) + .getValue + .doubleValue + ) + val pRated = sRated * inputModel.getCosPhiRated + val referenceScalingFactor = activePower / pRated + sRated * referenceScalingFactor * safetyFactor + } + + /** Scale profile based load model's sRated based on the provided yearly + * energy consumption + * + * When the load is scaled based on the consumed energy per year, the + * installed sRated capacity is not usable anymore instead, the load's rated + * apparent power is scaled on the maximum power occurring in the specified + * load profile multiplied by the ratio of the annual consumption and the + * standard load profile scale + * + * @param inputModel + * the input model instance + * @param energyConsumption + * the yearly energy consumption the models' sRated should be scaled to + * @param profileMaxPower + * the maximum power value of the profile + * @param profileEnergyScaling + * the energy scaling factor of the profile (= amount of yearly energy the + * profile is scaled to) + * @param safetyFactor + * a safety factor to address potential higher sRated values than the + * original scaling would provide (e.g. when using unrestricted probability + * functions) + * @return + * the inputs model sRated scaled to the provided energy consumption + */ + def scaleSRatedEnergy( + inputModel: LoadInput, + energyConsumption: Energy, + profileMaxPower: Power, + profileEnergyScaling: Energy, + safetyFactor: Double = 1d, + ): Power = { + (profileMaxPower / inputModel.getCosPhiRated) * ( + energyConsumption / profileEnergyScaling + ) * safetyFactor + } + + def apply( + input: LoadInput, + config: LoadRuntimeConfig, + ): LoadModel[_ <: OperationRelevantData] = { + LoadModelBehaviour(config.modelBehaviour) match { + case LoadModelBehaviour.FIX => + FixedLoadModel(input, config) + case LoadModelBehaviour.PROFILE => + ProfileLoadModel(input, config) + case LoadModelBehaviour.RANDOM => + RandomLoadModel(input, config) + } + } +} diff --git a/src/main/scala/edu/ie3/simona/model/participant2/load/LoadReference.scala b/src/main/scala/edu/ie3/simona/model/participant2/load/LoadReference.scala new file mode 100644 index 0000000000..1dcb22705a --- /dev/null +++ b/src/main/scala/edu/ie3/simona/model/participant2/load/LoadReference.scala @@ -0,0 +1,101 @@ +/* + * © 2020. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.model.participant2.load + +import edu.ie3.datamodel.models.input.system.LoadInput +import edu.ie3.simona.config.SimonaConfig +import edu.ie3.util.StringUtils +import edu.ie3.util.quantities.PowerSystemUnits.{MEGAWATT, MEGAWATTHOUR} +import squants.energy.{MegawattHours, Megawatts} +import squants.{Energy, Power} + +/** Denoting difference referencing scenarios for scaling load model output + */ +sealed trait LoadReference { + val key: String + + def getKey: String = key + + def scale(factor: Double): LoadReference +} +object LoadReference { + + /** Scale the load model behaviour to reach the given active power in max + * + * @param power + * Foreseen active power + */ + final case class ActivePower(power: Power) extends LoadReference { + override val key: String = "power" + + override def scale(factor: Double): ActivePower = + copy(power = power * factor) + } + + /** Scale the load model behaviour to reach the given annual energy + * consumption + * + * @param energyConsumption + * Annual energy consumption to reach + */ + final case class EnergyConsumption( + energyConsumption: Energy + ) extends LoadReference { + override val key: String = "energy" + + override def scale(factor: Double): LoadReference = + copy(energyConsumption = energyConsumption * factor) + } + + def isEligibleKey(key: String): Boolean = { + Set("power", "energy").contains(key) + } + + /** Build a reference object, that denotes, to which reference a load model + * behaviour might be scaled. If the behaviour is meant to be scaled to + * energy consumption and no annual energy consumption is given, an + * [[IllegalArgumentException]] is thrown + * + * @param inputModel + * [[LoadInput]] to derive energy information from + * @param modelConfig + * Configuration of model behaviour + * @return + * A [[LoadReference]] for use in [[LoadModel]] + */ + def apply( + inputModel: LoadInput, + modelConfig: SimonaConfig.LoadRuntimeConfig, + ): LoadReference = + StringUtils.cleanString(modelConfig.reference).toLowerCase match { + case "power" => + val activePower = Megawatts( + inputModel + .getsRated() + .to(MEGAWATT) + .getValue + .doubleValue + ) * + inputModel.getCosPhiRated + LoadReference.ActivePower(activePower) + case "energy" => + Option(inputModel.geteConsAnnual()) match { + case Some(consumption) => + LoadReference.EnergyConsumption( + MegawattHours(consumption.to(MEGAWATTHOUR).getValue.doubleValue) + ) + case None => + throw new IllegalArgumentException( + s"Load model with uuid ${inputModel.getUuid} is meant to be scaled to annual energy consumption, but the energy is not provided." + ) + } + case unsupported => + throw new IllegalArgumentException( + s"Load model with uuid ${inputModel.getUuid} is meant to be scaled to unsupported reference '$unsupported'." + ) + } +} diff --git a/src/main/scala/edu/ie3/simona/model/participant2/load/ProfileLoadModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/load/ProfileLoadModel.scala new file mode 100644 index 0000000000..010dea4675 --- /dev/null +++ b/src/main/scala/edu/ie3/simona/model/participant2/load/ProfileLoadModel.scala @@ -0,0 +1,98 @@ +/* + * © 2024. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.model.participant2.load + +import edu.ie3.datamodel.models.input.system.LoadInput +import edu.ie3.datamodel.models.profile.StandardLoadProfile +import edu.ie3.simona.agent.participant.data.Data +import edu.ie3.simona.config.SimonaConfig.LoadRuntimeConfig +import edu.ie3.simona.model.participant.control.QControl +import edu.ie3.simona.model.participant.load.profile.LoadProfileStore +import edu.ie3.simona.model.participant2.ParticipantModel +import edu.ie3.simona.model.participant2.ParticipantModel.{ + ActivePowerOperatingPoint, + DateTimeData, +} +import squants.{Dimensionless, Power} + +import java.time.ZonedDateTime +import java.util.UUID + +class ProfileLoadModel( + override val uuid: UUID, + override val sRated: Power, + override val cosPhiRated: Double, + override val qControl: QControl, + private val loadProfileStore: LoadProfileStore, + private val loadProfile: StandardLoadProfile, + private val referenceScalingFactor: Double, +) extends LoadModel[DateTimeData] { + + override def determineOperatingPoint( + state: ParticipantModel.ConstantState.type, + relevantData: DateTimeData, + ): (ParticipantModel.ActivePowerOperatingPoint, Option[Long]) = { + val averagePower = + loadProfileStore.entry(relevantData.dateTime, loadProfile) + + (ActivePowerOperatingPoint(averagePower * referenceScalingFactor), None) + } + + override def createRelevantData( + receivedData: Seq[Data], + nodalVoltage: Dimensionless, + tick: Long, + simulationTime: ZonedDateTime, + ): DateTimeData = DateTimeData(simulationTime) + +} + +object ProfileLoadModel { + + def apply(input: LoadInput, config: LoadRuntimeConfig): ProfileLoadModel = { + + val loadProfileStore: LoadProfileStore = LoadProfileStore() + + val loadProfile = input.getLoadProfile.asInstanceOf[StandardLoadProfile] + val loadProfileMax = loadProfileStore.maxPower(loadProfile) + + val reference = LoadReference(input, config) + + val referenceScalingFactor = + reference match { + case LoadReference.ActivePower(power) => + power / loadProfileMax + case LoadReference.EnergyConsumption(energyConsumption) => + energyConsumption / LoadProfileStore.defaultLoadProfileEnergyScaling + } + + // todo maybe this does not need to be so complicated, referenceScalingFactor is already calculated + val scaledSRated = reference match { + case LoadReference.ActivePower(power) => + LoadModel.scaleSRatedActivePower(input, power) + + case LoadReference.EnergyConsumption(energyConsumption) => + LoadModel.scaleSRatedEnergy( + input, + energyConsumption, + loadProfileMax, + LoadProfileStore.defaultLoadProfileEnergyScaling, + ) + } + + new ProfileLoadModel( + input.getUuid, + scaledSRated, + input.getCosPhiRated, + QControl.apply(input.getqCharacteristics()), + loadProfileStore, + loadProfile, + referenceScalingFactor, + ) + } + +} diff --git a/src/main/scala/edu/ie3/simona/model/participant2/load/RandomLoadModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/load/RandomLoadModel.scala new file mode 100644 index 0000000000..8e30bea64e --- /dev/null +++ b/src/main/scala/edu/ie3/simona/model/participant2/load/RandomLoadModel.scala @@ -0,0 +1,164 @@ +/* + * © 2024. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.model.participant2.load + +import de.lmu.ifi.dbs.elki.math.statistics.distribution.GeneralizedExtremeValueDistribution +import de.lmu.ifi.dbs.elki.utilities.random.RandomFactory +import edu.ie3.datamodel.models.input.system.LoadInput +import edu.ie3.simona.agent.participant.data.Data +import edu.ie3.simona.config.SimonaConfig.LoadRuntimeConfig +import edu.ie3.simona.model.participant.control.QControl +import edu.ie3.simona.model.participant.load.DayType +import edu.ie3.simona.model.participant.load.random.RandomLoadParamStore +import edu.ie3.simona.model.participant2.ParticipantModel +import edu.ie3.simona.model.participant2.ParticipantModel.{ + ActivePowerOperatingPoint, + DateTimeData, +} +import edu.ie3.util.TimeUtil +import squants.energy.{KilowattHours, Kilowatts, Watts} +import squants.{Dimensionless, Power} + +import java.time.ZonedDateTime +import java.util.UUID +import scala.collection.mutable +import scala.util.Random + +class RandomLoadModel( + override val uuid: UUID, + override val sRated: Power, + override val cosPhiRated: Double, + override val qControl: QControl, + private val referenceScalingFactor: Double, +) extends LoadModel[DateTimeData] { + + private val randomLoadParamStore = RandomLoadParamStore() + + private type GevKey = (DayType.Value, Int) + private val gevStorage = + mutable.Map.empty[GevKey, GeneralizedExtremeValueDistribution] + + override def determineOperatingPoint( + state: ParticipantModel.ConstantState.type, + relevantData: DateTimeData, + ): (ParticipantModel.ActivePowerOperatingPoint, Option[Long]) = { + val gev = getGevDistribution(relevantData.dateTime) + + /* Get a next random power (in kW) */ + val randomPower = gev.nextRandom() + if (randomPower < 0) + determineOperatingPoint(state, relevantData) + else { + ( + ActivePowerOperatingPoint( + Kilowatts(randomPower) * referenceScalingFactor + ), + None, + ) + } + } + + /** Get the needed generalized extreme value distribution from the store or + * instantiate a new one and put it to the store. + * + * @param dateTime + * Questioned date time + * @return + * The needed generalized extreme value distribution + */ + private def getGevDistribution( + dateTime: ZonedDateTime + ): GeneralizedExtremeValueDistribution = { + /* Determine identifying key for a distinct generalized extreme value distribution and look it up. If it is not + * available, yet, instantiate one. */ + val key: GevKey = ( + DayType(dateTime.getDayOfWeek), + TimeUtil.withDefaults.getQuarterHourOfDay(dateTime), + ) + gevStorage.get(key) match { + 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, + gevParameters.sigma, + gevParameters.k, + randomFactory, + ) + gevStorage += (key -> newGev) + newGev + } + } + + override def createRelevantData( + receivedData: Seq[Data], + nodalVoltage: Dimensionless, + tick: Long, + simulationTime: ZonedDateTime, + ): DateTimeData = DateTimeData(simulationTime) +} + +object RandomLoadModel { + + /** The profile energy scaling factor, the random profile is scaled to. + * + * It is said in 'Kays - Agent-based simulation environment for improving the + * planning of distribution grids', that the Generalized Extreme Value + * distribution's parameters are sampled from input data, that is normalized + * to 1,000 kWh annual energy consumption. However, due to inaccuracies in + * random data reproduction, the sampled values will lead to an average + * annual energy consumption of approx. this value. It has been found by + * 1,000 evaluations of the year 2019. + */ + private val randomProfileEnergyScaling = KilowattHours(716.5416966513656) + + /** This is the 95 % quantile resulting from 10,000 evaluations of the year + * 2019. It is only needed, when the load is meant to be scaled to rated + * active power. + * + * @return + * Reference power to use for later model calculations + */ + private val randomMaxPower: Power = Watts(159d) + + def apply(input: LoadInput, config: LoadRuntimeConfig): RandomLoadModel = { + + val reference = LoadReference(input, config) + + val referenceScalingFactor = reference match { + case LoadReference.ActivePower(power) => + power / randomMaxPower + case LoadReference.EnergyConsumption(energyConsumption) => + energyConsumption / randomProfileEnergyScaling + } + + val scaledSRated = reference match { + case LoadReference.ActivePower(power) => + LoadModel.scaleSRatedActivePower(input, power, 1.1) + + case LoadReference.EnergyConsumption(energyConsumption) => + LoadModel.scaleSRatedEnergy( + input, + energyConsumption, + randomMaxPower, + randomProfileEnergyScaling, + 1.1, + ) + } + + new RandomLoadModel( + input.getUuid, + scaledSRated, + input.getCosPhiRated, + QControl.apply(input.getqCharacteristics()), + referenceScalingFactor, + ) + } + +} From 9df5aceda3e3e9024db6ec39fc82cbaa7b179300 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Tue, 29 Oct 2024 14:16:34 +0100 Subject: [PATCH 24/77] Unifying init Signed-off-by: Sebastian Peter --- .../agent/participant2/ParticipantAgent.scala | 2 +- .../participant2/ParticipantAgentInit.scala | 83 +++++++++++-------- 2 files changed, 48 insertions(+), 37 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgent.scala b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgent.scala index ed3a4ba106..6d2dd30f24 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgent.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgent.scala @@ -65,7 +65,7 @@ object ParticipantAgent { */ final case class RegistrationSuccessfulMessage( override val serviceRef: ClassicRef, - nextDataTick: Long, + firstDataTick: Long, ) extends RegistrationResponseMessage /** Message, that is used to announce a failed registration diff --git a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgentInit.scala b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgentInit.scala index 705552ec64..d504dad793 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgentInit.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgentInit.scala @@ -125,23 +125,10 @@ object ParticipantAgentInit { parentData: Either[SchedulerData, FlexControlledData], ): Behavior[Request] = Behaviors.receivePartial { - case (_, RegistrationSuccessfulMessage(serviceRef, nextDataTick)) => - parentData.fold( - schedulerData => - schedulerData.scheduler ! Completion( - schedulerData.activationAdapter, - Some(nextDataTick), - ), - _.emAgent ! FlexCompletion( - participantInput.getUuid, - requestAtNextActivation = false, - Some(nextDataTick), - ), - ) - - val expectedFirstData = Map(serviceRef -> nextDataTick) + case (_, RegistrationSuccessfulMessage(serviceRef, firstDataTick)) => + val expectedFirstData = Map(serviceRef -> firstDataTick) - createAgent( + completeInitialization( ParticipantModelShell.createForPrimaryData( participantInput, config, @@ -152,6 +139,7 @@ object ParticipantAgentInit { gridAgentRef, expectedPowerRequestTick, parentData, + firstDataTick, ) case (_, RegistrationFailedMessage(_)) => @@ -166,12 +154,15 @@ object ParticipantAgentInit { modelShell.model.getRequiredSecondaryServices.toSeq if (requiredServiceTypes.isEmpty) { - createAgent( + val firstTick = ??? + + completeInitialization( modelShell, Map.empty, gridAgentRef, expectedPowerRequestTick, parentData, + firstTick, ) } else { // TODO request service actorrefs @@ -207,29 +198,22 @@ object ParticipantAgentInit { expectedFirstData.updated(serviceRef, nextDataTick) if (newExpectedRegistrations.isEmpty) { - val earliestNextTick = expectedFirstData.map { case (_, nextTick) => - nextTick - }.minOption - - parentData.fold( - schedulerData => - schedulerData.scheduler ! Completion( - schedulerData.activationAdapter, - earliestNextTick, - ), - _.emAgent ! FlexCompletion( - modelShell.model.uuid, - requestAtNextActivation = false, - earliestNextTick, - ), - ) + val firstTick = expectedFirstData + .map { case (_, nextTick) => + nextTick + } + .minOption + .getOrElse( + throw new CriticalFailureException("No expected data registered.") + ) - createAgent( + completeInitialization( modelShell, newExpectedFirstData, gridAgentRef, expectedPowerRequestTick, parentData, + firstTick, ) } else waitingForServices( @@ -242,17 +226,44 @@ object ParticipantAgentInit { ) } - def createAgent( + /** Completes initialization activation and creates actual + * [[ParticipantAgent]] + * + * @param modelShell + * @param expectedData + * @param gridAgentRef + * @param expectedPowerRequestTick + * @param parentData + * @param firstTick + * @return + */ + private def completeInitialization( modelShell: ParticipantModelShell[_, _, _], expectedData: Map[ClassicRef, Long], gridAgentRef: ActorRef[GridAgent.Request], expectedPowerRequestTick: Long, parentData: Either[SchedulerData, FlexControlledData], - ): Behavior[Request] = + firstTick: Long, + ): Behavior[Request] = { + + parentData.fold( + schedulerData => + schedulerData.scheduler ! Completion( + schedulerData.activationAdapter, + Some(firstTick), + ), + _.emAgent ! FlexCompletion( + modelShell.model.uuid, + requestAtNextActivation = false, + Some(firstTick), + ), + ) + ParticipantAgent( modelShell, ParticipantInputHandler(expectedData), ParticipantGridAdapter(gridAgentRef, expectedPowerRequestTick), parentData, ) + } } From dfce915ef3de5e0589d96c6a9ebdbbd558c69af3 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Thu, 31 Oct 2024 22:28:35 +0100 Subject: [PATCH 25/77] Returning proper next tick Signed-off-by: Sebastian Peter --- .../model/participant2/ParticipantModel.scala | 9 ++-- .../participant2/ParticipantModelShell.scala | 1 + .../participant2/load/ProfileLoadModel.scala | 21 ++++++-- .../participant2/load/RandomLoadModel.scala | 16 ++++-- .../scala/edu/ie3/simona/util/TickUtil.scala | 32 ++++++++++++ .../edu/ie3/simona/util/TickUtilSpec.scala | 51 +++++++++++++++++++ 6 files changed, 120 insertions(+), 10 deletions(-) create mode 100644 src/test/scala/edu/ie3/simona/util/TickUtilSpec.scala diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala index 4c1ee12f4a..24ba2cc3d5 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala @@ -170,11 +170,14 @@ object ParticipantModel { */ case object FixedRelevantData extends OperationRelevantData - /** OperationRelevantData that just transports the current datetime + /** OperationRelevantData that just transports the current datetime and tick + * @param tick + * The current tick * @param dateTime - * The current datetime + * The current datetime, corresponding to the current tick */ - case class DateTimeData(dateTime: ZonedDateTime) extends OperationRelevantData + case class DateTimeData(tick: Long, dateTime: ZonedDateTime) + extends OperationRelevantData trait OperatingPoint { diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala index ff776bcea0..d54d618410 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala @@ -88,6 +88,7 @@ final case class ParticipantModelShell[ s"New state $currentState is not set to current tick $currentTick" ) + // todo also include operation start and end as ticks val (newOperatingPoint, newNextTick) = if (!operationInterval.includes(currentTick)) // Current tick is outside of operation interval. diff --git a/src/main/scala/edu/ie3/simona/model/participant2/load/ProfileLoadModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/load/ProfileLoadModel.scala index 010dea4675..36fbb11849 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/load/ProfileLoadModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/load/ProfileLoadModel.scala @@ -12,11 +12,13 @@ import edu.ie3.simona.agent.participant.data.Data import edu.ie3.simona.config.SimonaConfig.LoadRuntimeConfig import edu.ie3.simona.model.participant.control.QControl import edu.ie3.simona.model.participant.load.profile.LoadProfileStore +import edu.ie3.simona.model.participant.load.random.RandomLoadParamStore import edu.ie3.simona.model.participant2.ParticipantModel import edu.ie3.simona.model.participant2.ParticipantModel.{ ActivePowerOperatingPoint, DateTimeData, } +import edu.ie3.simona.util.TickUtil import squants.{Dimensionless, Power} import java.time.ZonedDateTime @@ -36,10 +38,21 @@ class ProfileLoadModel( state: ParticipantModel.ConstantState.type, relevantData: DateTimeData, ): (ParticipantModel.ActivePowerOperatingPoint, Option[Long]) = { - val averagePower = - loadProfileStore.entry(relevantData.dateTime, loadProfile) + val resolution = RandomLoadParamStore.resolution.getSeconds - (ActivePowerOperatingPoint(averagePower * referenceScalingFactor), None) + val (modelTick, modelDateTime) = TickUtil.roundToResolution( + relevantData.tick, + relevantData.dateTime, + resolution.toInt, + ) + + val averagePower = loadProfileStore.entry(modelDateTime, loadProfile) + val nextTick = modelTick + resolution + + ( + ActivePowerOperatingPoint(averagePower * referenceScalingFactor), + Some(nextTick), + ) } override def createRelevantData( @@ -47,7 +60,7 @@ class ProfileLoadModel( nodalVoltage: Dimensionless, tick: Long, simulationTime: ZonedDateTime, - ): DateTimeData = DateTimeData(simulationTime) + ): DateTimeData = DateTimeData(tick, simulationTime) } diff --git a/src/main/scala/edu/ie3/simona/model/participant2/load/RandomLoadModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/load/RandomLoadModel.scala index 8e30bea64e..3935fa9b45 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/load/RandomLoadModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/load/RandomLoadModel.scala @@ -19,6 +19,7 @@ import edu.ie3.simona.model.participant2.ParticipantModel.{ ActivePowerOperatingPoint, DateTimeData, } +import edu.ie3.simona.util.TickUtil import edu.ie3.util.TimeUtil import squants.energy.{KilowattHours, Kilowatts, Watts} import squants.{Dimensionless, Power} @@ -46,18 +47,27 @@ class RandomLoadModel( state: ParticipantModel.ConstantState.type, relevantData: DateTimeData, ): (ParticipantModel.ActivePowerOperatingPoint, Option[Long]) = { - val gev = getGevDistribution(relevantData.dateTime) + val resolution = RandomLoadParamStore.resolution.getSeconds + + val (modelTick, modelDateTime) = TickUtil.roundToResolution( + relevantData.tick, + relevantData.dateTime, + resolution.toInt, + ) + + val gev = getGevDistribution(modelDateTime) /* Get a next random power (in kW) */ val randomPower = gev.nextRandom() if (randomPower < 0) determineOperatingPoint(state, relevantData) else { + val nextTick = modelTick + resolution ( ActivePowerOperatingPoint( Kilowatts(randomPower) * referenceScalingFactor ), - None, + Some(nextTick), ) } } @@ -101,7 +111,7 @@ class RandomLoadModel( nodalVoltage: Dimensionless, tick: Long, simulationTime: ZonedDateTime, - ): DateTimeData = DateTimeData(simulationTime) + ): DateTimeData = DateTimeData(tick, simulationTime) } object RandomLoadModel { diff --git a/src/main/scala/edu/ie3/simona/util/TickUtil.scala b/src/main/scala/edu/ie3/simona/util/TickUtil.scala index 34f16d8e6e..b15d2e4c35 100644 --- a/src/main/scala/edu/ie3/simona/util/TickUtil.scala +++ b/src/main/scala/edu/ie3/simona/util/TickUtil.scala @@ -75,4 +75,36 @@ object TickUtil { (firstFullHourTick to lastAvailableTick by resolution.intValue).toArray } + /** Rounds given tick and datetime with regard to their (implicit) minutes and + * seconds. + * + * @param tick + * The given tick to round + * @param dateTime + * The given date and time to round + * @param resolution + * Resolution in seconds. Should divide 3600 without remainder, i.e. + * {{{3600 % resolution == 0}}} + */ + def roundToResolution( + tick: Long, + dateTime: ZonedDateTime, + resolution: Int, + ): (Long, ZonedDateTime) = { + + val givenHourSeconds = dateTime.getMinute * 60 + dateTime.getSecond + + val adaptedHourSeconds = givenHourSeconds / resolution * resolution + val adaptedMinute = adaptedHourSeconds / 60 + val adaptedSecond = adaptedHourSeconds % 60 + + val adaptedDateTime = + dateTime.withMinute(adaptedMinute).withSecond(adaptedSecond).withNano(0) + + val tickDifference = givenHourSeconds - adaptedHourSeconds + + val adaptedTick = tick - tickDifference + + (adaptedTick, adaptedDateTime) + } } diff --git a/src/test/scala/edu/ie3/simona/util/TickUtilSpec.scala b/src/test/scala/edu/ie3/simona/util/TickUtilSpec.scala new file mode 100644 index 0000000000..7b26ee47be --- /dev/null +++ b/src/test/scala/edu/ie3/simona/util/TickUtilSpec.scala @@ -0,0 +1,51 @@ +/* + * © 2024. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.util + +import edu.ie3.simona.test.common.UnitSpec +import edu.ie3.util.TimeUtil + +class TickUtilSpec extends UnitSpec { + "TimeUtil" should { + "round ticks and date/times correctly" in { + val cases = Table( + ("tick", "dateTime", "resolution", "expectedTick", "expectedDateTime"), + // base scenario + (0, "2024-01-02T00:00:00Z", 900, 0, "2024-01-02T00:00:00Z"), + // unusual tick + (1, "2024-01-02T00:00:00Z", 900, 1, "2024-01-02T00:00:00Z"), + // uneven second + (3600, "2024-01-02T00:00:10Z", 900, 3590, "2024-01-02T00:00:00Z"), + // uneven minute + (3600, "2024-01-02T00:07:00Z", 900, 3180, "2024-01-02T00:00:00Z"), + // uneven minute and second + (3600, "2024-01-02T00:07:07Z", 900, 3173, "2024-01-02T00:00:00Z"), + // second-sized resolution, base scenario + (3600, "2024-01-02T00:00:00Z", 15, 3600, "2024-01-02T00:00:00Z"), + // second-sized resolution, uneven second + (3603, "2024-01-02T00:00:18Z", 15, 3600, "2024-01-02T00:00:15Z"), + // second-sized resolution, uneven minute + (3600, "2024-01-02T00:05:00Z", 15, 3600, "2024-01-02T00:05:00Z"), + // second-sized resolution, uneven minute and second + (3607, "2024-01-02T00:05:07Z", 15, 3600, "2024-01-02T00:05:00Z"), + ) + + forAll(cases) { + case (tick, dateTime, resolution, expectedTick, expectedDateTime) => + val (actualTick, actualDateTime) = TickUtil.roundToResolution( + tick, + TimeUtil.withDefaults.toZonedDateTime(dateTime), + resolution, + ) + + actualTick should equal(expectedTick) + TimeUtil.withDefaults.toString(actualDateTime) should + equal(expectedDateTime) + } + } + } +} From ed7fd8fc7d135714da90b4ffa1f5ae5f7ae2f72b Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Thu, 31 Oct 2024 23:52:43 +0100 Subject: [PATCH 26/77] Init models without secondary data Signed-off-by: Sebastian Peter --- .../ie3/simona/agent/participant2/ParticipantAgentInit.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgentInit.scala b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgentInit.scala index d504dad793..9f8c1797c0 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgentInit.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgentInit.scala @@ -154,7 +154,8 @@ object ParticipantAgentInit { modelShell.model.getRequiredSecondaryServices.toSeq if (requiredServiceTypes.isEmpty) { - val firstTick = ??? + // Models that do not use secondary data always start at tick 0 + val firstTick = 0L completeInitialization( modelShell, From 1424c55706395488fb73fdac68f850f9388cb23a Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Fri, 1 Nov 2024 12:30:11 +0100 Subject: [PATCH 27/77] Properly implementing operation interval consideration Signed-off-by: Sebastian Peter --- .../model/participant2/ParticipantModel.scala | 18 ++- .../participant2/ParticipantModelShell.scala | 115 +++++++++++++----- .../ie3/util/scala/OperationInterval.scala | 23 +--- 3 files changed, 100 insertions(+), 56 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala index 24ba2cc3d5..f1f7406736 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala @@ -56,12 +56,20 @@ abstract class ParticipantModel[ nodalVoltage, ) - /** With the given current state and the given relevant data, determines the - * operating point that is currently valid until the next operating point is - * determined. Also, optionally returns a tick at which the state will change - * unless the operating point changes beforehand. + /** With the given current state and the given operation-relevant data, + * determines the operating point that is currently valid until the next + * operating point is determined. Also, optionally returns a tick at which + * the state will change unless the operating point changes due to external + * influences beforehand. * - * This method is only called if the participant is *not* em-controlled. + * This method should be able to handle calls at arbitrary points in + * simulation time (i.e. ticks), which are situated after the tick of the + * last state. + * + * This method is only called if the participant is *not* em-controlled. If + * the participant *is* em-controlled, + * [[ParticipantFlexibility.handlePowerControl()]] determines the operating + * point instead. * * @param state * the current state diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala index d54d618410..2703a15572 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala @@ -28,8 +28,10 @@ import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage.{ IssueFlexControl, ProvideFlexOptions, } +import edu.ie3.simona.ontology.messages.flex.MinMaxFlexibilityMessage.ProvideMinMaxFlexOptions import edu.ie3.simona.util.TickUtil.TickLong import edu.ie3.util.scala.OperationInterval +import edu.ie3.util.scala.quantities.DefaultQuantities._ import edu.ie3.util.scala.quantities.ReactivePower import org.apache.pekko.actor.typed.scaladsl.ActorContext import squants.Dimensionless @@ -78,6 +80,11 @@ final case class ParticipantModelShell[ copy(relevantData = Some(updatedRelevantData)) } + /** Update operating point when the model is '''not''' em-controlled. + * + * @param currentTick + * @return + */ def updateOperatingPoint( currentTick: Long ): ParticipantModelShell[OP, S, OR] = { @@ -88,25 +95,25 @@ final case class ParticipantModelShell[ s"New state $currentState is not set to current tick $currentTick" ) - // todo also include operation start and end as ticks - val (newOperatingPoint, newNextTick) = - if (!operationInterval.includes(currentTick)) - // Current tick is outside of operation interval. - // Set operating point to "zero" - (model.zeroPowerOperatingPoint, None) - else - model.determineOperatingPoint( - state, - relevantData.getOrElse( - throw new CriticalFailureException("No relevant data available!") - ), - ) + def modelOperatingPoint() = { + val (modelOp, modelNextTick) = model.determineOperatingPoint( + state, + relevantData.getOrElse( + throw new CriticalFailureException("No relevant data available!") + ), + ) + val modelIndicator = ModelChangeIndicator(changesAtTick = modelNextTick) + (modelOp, modelIndicator) + } + + val (newOperatingPoint, newChangeIndicator) = + determineOperatingPointInInterval(modelOperatingPoint, currentTick) copy( state = currentState, lastOperatingPoint = operatingPoint, operatingPoint = Some(newOperatingPoint), - modelChange = ModelChangeIndicator(changesAtTick = newNextTick), + modelChange = newChangeIndicator, ) } @@ -144,31 +151,46 @@ final case class ParticipantModelShell[ def updateFlexOptions(currentTick: Long): ParticipantModelShell[OP, S, OR] = { val currentState = determineCurrentState(currentTick) - val flexOptions = model.calcFlexOptions( - currentState, - relevantData.getOrElse( - throw new CriticalFailureException("No relevant data available!") - ), - ) + + val flexOptions = + if (operationInterval.includes(currentTick)) { + model.calcFlexOptions( + currentState, + relevantData.getOrElse( + throw new CriticalFailureException("No relevant data available!") + ), + ) + } else { + // Out of operation, there's no way to operate besides 0 kW + ProvideMinMaxFlexOptions.noFlexOption(model.uuid, zeroKW) + } copy(state = currentState, flexOptions = Some(flexOptions)) } + /** Update operating point on receiving [[IssueFlexControl]], i.e. when the + * model is em-controlled. + * + * @param flexControl + * @return + */ def updateOperatingPoint( flexControl: IssueFlexControl ): ParticipantModelShell[OP, S, OR] = { - val fo = flexOptions.getOrElse( - throw new CriticalFailureException("No flex options available!") - ) - val currentState = determineCurrentState(flexControl.tick) - val setPointActivePower = EmTools.determineFlexPower( - fo, - flexControl, - ) + val currentTick = flexControl.tick + + def modelOperatingPoint() = { + val fo = flexOptions.getOrElse( + throw new CriticalFailureException("No flex options available!") + ) + + val setPointActivePower = EmTools.determineFlexPower( + fo, + flexControl, + ) - val (newOperatingPoint, modelChange) = model.handlePowerControl( currentState, relevantData.getOrElse( @@ -177,15 +199,48 @@ final case class ParticipantModelShell[ fo, setPointActivePower, ) + } + + val (newOperatingPoint, newChangeIndicator) = + determineOperatingPointInInterval(modelOperatingPoint, currentTick) copy( state = currentState, lastOperatingPoint = operatingPoint, operatingPoint = Some(newOperatingPoint), - modelChange = modelChange, + modelChange = newChangeIndicator, ) } + private def determineOperatingPointInInterval( + modelOperatingPoint: () => (OP, ModelChangeIndicator), + currentTick: Long, + ): (OP, ModelChangeIndicator) = { + if (operationInterval.includes(currentTick)) { + val (modelOp, modelIndicator) = modelOperatingPoint() + + // Check if the end of the operation interval is *before* the next tick calculated by the model + val adaptedNextTick = + Seq( + modelIndicator.changesAtTick, + Option(operationInterval.end), + ).flatten.minOption + + (modelOp, modelIndicator.copy(changesAtTick = adaptedNextTick)) + } else { + // Current tick is outside of operation interval. + // Set operating point to "zero" + val op = model.zeroPowerOperatingPoint + + // If the model is not active *yet*, schedule the operation start + val nextTick = Option.when(operationInterval.start < currentTick)( + operationInterval.start + ) + + (op, ModelChangeIndicator(changesAtTick = nextTick)) + } + } + def handleRequest( ctx: ActorContext[ParticipantAgent.Request], request: ParticipantRequest, diff --git a/src/main/scala/edu/ie3/util/scala/OperationInterval.scala b/src/main/scala/edu/ie3/util/scala/OperationInterval.scala index 880f441460..65eb4ff7db 100644 --- a/src/main/scala/edu/ie3/util/scala/OperationInterval.scala +++ b/src/main/scala/edu/ie3/util/scala/OperationInterval.scala @@ -16,24 +16,5 @@ import edu.ie3.util.interval.ClosedInterval * @param end * End of operation period (included) */ -final case class OperationInterval(start: java.lang.Long, end: java.lang.Long) - extends ClosedInterval[java.lang.Long](start, end) { - - /** Get the first tick, in which the operation starts - * - * @return - * Tick, in which operation starts - */ - def getStart: Long = getLower - - /** Get the last tick, in which the operation end - * - * @return - * Tick, in which operation end - */ - def getEnd: Long = getUpper -} - -object OperationInterval { - def apply(start: Long, end: Long) = new OperationInterval(start, end) -} +final case class OperationInterval(start: Long, end: Long) + extends ClosedInterval[java.lang.Long](start, end) From da6a9fffe5e2d652155e17c27ecce7058a74f498 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Fri, 1 Nov 2024 12:33:57 +0100 Subject: [PATCH 28/77] ScalaDoc fix Signed-off-by: Sebastian Peter --- .../edu/ie3/simona/model/participant2/ParticipantModel.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala index f1f7406736..05a411230d 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala @@ -66,8 +66,8 @@ abstract class ParticipantModel[ * simulation time (i.e. ticks), which are situated after the tick of the * last state. * - * This method is only called if the participant is *not* em-controlled. If - * the participant *is* em-controlled, + * This method is only called if the participant is '''not''' em-controlled. + * If the participant '''is''' em-controlled, * [[ParticipantFlexibility.handlePowerControl()]] determines the operating * point instead. * From a3a1fc0d957cce094538adb4f1cd13cf33ce6d82 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Fri, 1 Nov 2024 13:43:24 +0100 Subject: [PATCH 29/77] Small improvements Signed-off-by: Sebastian Peter --- .../agent/participant2/ParticipantAgent.scala | 28 ++++++++++--------- .../participant2/ParticipantGridAdapter.scala | 13 ++++++++- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgent.scala b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgent.scala index 6d2dd30f24..5ddcd43cc3 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgent.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgent.scala @@ -132,6 +132,9 @@ object ParticipantAgent { * ways of interacting with the agent */ trait ParticipantRequest extends Request { + + /** The tick for which the request is valid, which is the current tick + */ val tick: Long } @@ -147,7 +150,7 @@ object ParticipantAgent { ParticipantAgent(updatedShell, inputHandler, gridAdapter, parentData) - case (ctx, activation: ActivationRequest) => + case (_, activation: ActivationRequest) => val coreWithActivation = inputHandler.handleActivation(activation) val (updatedShell, updatedCore, updatedGridAdapter) = @@ -165,7 +168,7 @@ object ParticipantAgent { parentData, ) - case (ctx, msg: ProvisionMessage[Data]) => + case (_, msg: ProvisionMessage[Data]) => val coreWithData = inputHandler.handleDataProvision(msg) val (updatedShell, updatedCore, updatedGridAdapter) = @@ -224,7 +227,7 @@ object ParticipantAgent { parentData, ) - case (ctx, FinishParticipantSimulation(_, nextRequestTick)) => + case (_, FinishParticipantSimulation(_, nextRequestTick)) => val updatedGridAdapter = gridAdapter.updateNextRequestTick(nextRequestTick) @@ -354,19 +357,18 @@ object ParticipantAgent { (modelShell, inputHandler, gridAdapter) } - def isDataComplete( + private def isDataComplete( inputHandler: ParticipantInputHandler, gridAdapter: ParticipantGridAdapter, - ): Boolean = - if (inputHandler.isComplete) { - val activation = inputHandler.activation.getOrElse( - throw new CriticalFailureException( - "Activation should be present when data collection is complete" - ) + ): Boolean = { + lazy val activation = inputHandler.activation.getOrElse( + throw new CriticalFailureException( + "Activation should be present when data collection is complete" ) + ) - !gridAdapter.isPowerRequestExpected(activation.tick) - } else - false + inputHandler.isComplete && + !gridAdapter.isPowerRequestAwaited(activation.tick) + } } diff --git a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantGridAdapter.scala b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantGridAdapter.scala index cd9f0e9b4a..ae1904e543 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantGridAdapter.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantGridAdapter.scala @@ -23,12 +23,15 @@ import scala.util.{Failure, Success} /** Provides (average) power values to grid agent * * @param gridAgent + * The ActorRef for the [[GridAgent]] * @param expectedRequestTick * Tick at which next power request is expected * @param nodalVoltage + * The currently valid nodal voltage in p.u. * @param tickToPower * power values * @param avgPowerResult + * The calculated average power for the current request window */ final case class ParticipantGridAdapter( gridAgent: ActorRef[GridAgent.Request], @@ -38,7 +41,15 @@ final case class ParticipantGridAdapter( avgPowerResult: Option[AvgPowerResult], ) { - def isPowerRequestExpected(currentTick: Long): Boolean = { + /** Whether a power request is expected and has not yet arrived, thus is + * awaited, for the given tick. + * + * @param currentTick + * The current tick + * @return + * Whether a power request is awaited for the given tick + */ + def isPowerRequestAwaited(currentTick: Long): Boolean = { expectedRequestTick == currentTick } From 33e5a9567241800432cacb797369df458ff5dbae Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Fri, 1 Nov 2024 17:24:34 +0100 Subject: [PATCH 30/77] Sending results Signed-off-by: Sebastian Peter --- .../agent/participant2/ParticipantAgent.scala | 31 ++++++++++++++++--- .../participant2/ParticipantAgentInit.scala | 1 + .../participant2/ParticipantModelInit.scala | 1 - 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgent.scala b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgent.scala index 5ddcd43cc3..54e279e1bd 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgent.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgent.scala @@ -13,6 +13,8 @@ import edu.ie3.simona.agent.grid.GridAgentMessages.{ } import edu.ie3.simona.agent.participant.data.Data import edu.ie3.simona.agent.participant.data.Data.SecondaryData +import edu.ie3.simona.event.ResultEvent +import edu.ie3.simona.event.ResultEvent.ParticipantResultEvent import edu.ie3.simona.exceptions.CriticalFailureException import edu.ie3.simona.model.participant2.ParticipantModelShell import edu.ie3.simona.ontology.messages.SchedulerMessage.Completion @@ -142,13 +144,20 @@ object ParticipantAgent { modelShell: ParticipantModelShell[_, _, _], inputHandler: ParticipantInputHandler, gridAdapter: ParticipantGridAdapter, + resultListener: Iterable[ActorRef[ResultEvent]], parentData: Either[SchedulerData, FlexControlledData], ): Behavior[Request] = Behaviors.receivePartial { case (ctx, request: ParticipantRequest) => val updatedShell = modelShell.handleRequest(ctx, request) - ParticipantAgent(updatedShell, inputHandler, gridAdapter, parentData) + ParticipantAgent( + updatedShell, + inputHandler, + gridAdapter, + resultListener, + parentData, + ) case (_, activation: ActivationRequest) => val coreWithActivation = inputHandler.handleActivation(activation) @@ -158,6 +167,7 @@ object ParticipantAgent { modelShell, coreWithActivation, gridAdapter, + resultListener, parentData, ) @@ -165,6 +175,7 @@ object ParticipantAgent { updatedShell, updatedCore, updatedGridAdapter, + resultListener, parentData, ) @@ -172,12 +183,19 @@ object ParticipantAgent { val coreWithData = inputHandler.handleDataProvision(msg) val (updatedShell, updatedCore, updatedGridAdapter) = - maybeCalculate(modelShell, coreWithData, gridAdapter, parentData) + maybeCalculate( + modelShell, + coreWithData, + gridAdapter, + resultListener, + parentData, + ) ParticipantAgent( updatedShell, updatedCore, updatedGridAdapter, + resultListener, parentData, ) @@ -224,6 +242,7 @@ object ParticipantAgent { modelShell, inputHandler, updatedGridAdapter, + resultListener, parentData, ) @@ -235,6 +254,7 @@ object ParticipantAgent { modelShell, inputHandler, updatedGridAdapter, + resultListener, parentData, ) } @@ -243,6 +263,7 @@ object ParticipantAgent { modelShell: ParticipantModelShell[_, _, _], inputHandler: ParticipantInputHandler, gridAdapter: ParticipantGridAdapter, + listener: Iterable[ActorRef[ResultEvent]], parentData: Either[SchedulerData, FlexControlledData], ): ( ParticipantModelShell[_, _, _], @@ -281,7 +302,8 @@ object ParticipantAgent { val results = modelWithOP.determineResults(tick, gridAdapter.nodalVoltage) - results.modelResults.foreach { res => // todo send out results + results.modelResults.foreach { res => + listener.foreach(_ ! ParticipantResultEvent(res)) } val gridAdapterWithResult = @@ -326,7 +348,8 @@ object ParticipantAgent { gridAdapter.nodalVoltage, ) - results.modelResults.foreach { res => // todo send out results + results.modelResults.foreach { res => + listener.foreach(_ ! ParticipantResultEvent(res)) } val gridAdapterWithResult = diff --git a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgentInit.scala b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgentInit.scala index 9f8c1797c0..c7c30932e6 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgentInit.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgentInit.scala @@ -264,6 +264,7 @@ object ParticipantAgentInit { modelShell, ParticipantInputHandler(expectedData), ParticipantGridAdapter(gridAgentRef, expectedPowerRequestTick), + ???, parentData, ) } diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelInit.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelInit.scala index b1faa5b109..09351ad782 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelInit.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelInit.scala @@ -45,7 +45,6 @@ object ParticipantModelInit { }).build() (scaledParticipantInput, modelConfig) match { - // fixme ticks not scheduled for fixed feed-in/load models case (input: FixedFeedInInput, _) => val model = FixedFeedInModel(input) val state = model.getInitialState From d6eeb252ad2978afa21eccf0f7230b53805abe79 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Wed, 13 Nov 2024 20:34:14 +0100 Subject: [PATCH 31/77] Some fixes Signed-off-by: Sebastian Peter --- .../participant2/ParticipantFlexibility.scala | 2 +- .../model/participant2/ParticipantModel.scala | 2 ++ .../simona/model/participant2/WecModel.scala | 25 +++++++++++++++---- 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantFlexibility.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantFlexibility.scala index e1fbf59515..2ee34d6456 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantFlexibility.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantFlexibility.scala @@ -31,7 +31,7 @@ trait ParticipantFlexibility[ def handlePowerControl( state: S, relevantData: OR, - flexOptions: ProvideFlexOptions, + flexOptions: ProvideFlexOptions, // TODO is this needed? setPower: Power, ): (OP, ModelChangeIndicator) diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala index 05a411230d..768c94c6f3 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala @@ -41,6 +41,8 @@ abstract class ParticipantModel[ val cosPhiRated: Double val qControl: QControl + protected val pRated: Power = sRated * cosPhiRated + /** Get a partial function, that transfers the current active into reactive * power based on the participants properties and the given nodal voltage * diff --git a/src/main/scala/edu/ie3/simona/model/participant2/WecModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/WecModel.scala index 38fe633bb9..b3a07e771d 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/WecModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/WecModel.scala @@ -37,6 +37,7 @@ import edu.ie3.simona.ontology.messages.services.WeatherMessage.WeatherData import edu.ie3.simona.service.ServiceType import edu.ie3.util.quantities.PowerSystemUnits.{KILOWATT, PU} import edu.ie3.util.quantities.QuantityUtils.RichQuantityDouble +import edu.ie3.util.scala.Scope import squants._ import squants.energy.{Kilowatts, Watts} import squants.mass.{Kilograms, KilogramsPerCubicMeter} @@ -92,12 +93,26 @@ class WecModel private ( */ val cubedVelocity = v * v * v - // Combined, we get (kg * m²)/s³, which is Watts - val power = Watts( - cubedVelocity * 0.5 * betzCoefficient.toEach * airDensity * rotorArea.toSquareMeters - ) + val activePower = Scope( + // Combined, we get (kg * m²)/s³, which is Watts + Watts( + cubedVelocity * 0.5 * betzCoefficient.toEach * airDensity * rotorArea.toSquareMeters + ) + ).map { power => + if (power > pRated) { + logger.warn( + "The fed in active power is higher than the estimated maximum active power of this plant ({} > {}). " + + "Did you provide wrong weather input data?", + power, + pRated, + ) + pRated + } else + power + }.map(_ * -1) + .get - (ActivePowerOperatingPoint(power), None) + (ActivePowerOperatingPoint(activePower), None) } /** The coefficient is dependent on the wind velocity v. Therefore use v to From 50654c93c18c0f01fad862ab51eac2e8e0799854 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Wed, 13 Nov 2024 20:34:30 +0100 Subject: [PATCH 32/77] Adapting first tests for new models Signed-off-by: Sebastian Peter --- .../model/participant2/PvModelSpec.scala | 792 ++++++++++++++++++ .../model/participant2/StorageModelSpec.scala | 428 ++++++++++ .../model/participant2/WecModelSpec.scala | 199 +++++ 3 files changed, 1419 insertions(+) create mode 100644 src/test/scala/edu/ie3/simona/model/participant2/PvModelSpec.scala create mode 100644 src/test/scala/edu/ie3/simona/model/participant2/StorageModelSpec.scala create mode 100644 src/test/scala/edu/ie3/simona/model/participant2/WecModelSpec.scala diff --git a/src/test/scala/edu/ie3/simona/model/participant2/PvModelSpec.scala b/src/test/scala/edu/ie3/simona/model/participant2/PvModelSpec.scala new file mode 100644 index 0000000000..7930a01e6c --- /dev/null +++ b/src/test/scala/edu/ie3/simona/model/participant2/PvModelSpec.scala @@ -0,0 +1,792 @@ +/* + * © 2020. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.model.participant2 + +import edu.ie3.datamodel.models.OperationTime +import edu.ie3.datamodel.models.input.system.PvInput +import edu.ie3.datamodel.models.input.system.characteristic.CosPhiFixed +import edu.ie3.datamodel.models.input.{NodeInput, OperatorInput} +import edu.ie3.datamodel.models.voltagelevels.GermanVoltageLevelUtils +import edu.ie3.simona.test.common.{DefaultTestData, UnitSpec} +import edu.ie3.util.quantities.PowerSystemUnits._ +import edu.ie3.util.scala.quantities.{Irradiation, WattHoursPerSquareMeter} +import org.locationtech.jts.geom.{Coordinate, GeometryFactory, Point} +import org.scalatest.GivenWhenThen +import squants.energy.{Kilowatts, Power} +import squants.space.{Angle, Degrees, Radians} +import tech.units.indriya.quantity.Quantities.getQuantity +import tech.units.indriya.unit.Units._ + +import java.time.ZonedDateTime +import java.util.UUID +import scala.math.toRadians + +/** Test class that tries to cover all special cases of the current + * implementation of the PvModel + * + * Some of these test cases are taken from the examples of Duffie, J. A., & + * Beckman, W. A. (2013). Solar engineering of thermal processes (4th ed.). + * Hoboken, N.J.: John Wiley & Sons. + * + * The page examples can be found using the page number provided in each test. + * Results may differ slightly from the book as we sometimes use different + * formulas. Furthermore, sometimes the example might be slightly adapted to + * fit our needs. + */ +class PvModelSpec extends UnitSpec with GivenWhenThen with DefaultTestData { + + // build the NodeInputModel (which defines the location of the pv input model) + // the NodeInputModel needs a GeoReference for the Pv to work + val geometryFactory = new GeometryFactory() + val p: Point = geometryFactory.createPoint(new Coordinate(13.2491, 53.457909)) + val nodeInput = new NodeInput( + UUID.fromString("85f8b517-8a2d-4c20-86c6-3ff3c5823e6d"), + "NodeInputModel for PvModel Test", + OperatorInput.NO_OPERATOR_ASSIGNED, + OperationTime.notLimited(), + getQuantity(1, PU), + false, + p, + GermanVoltageLevelUtils.MV_20KV, + 11, + ) + + // build the PvInputModel + val pvInput = new PvInput( + UUID.fromString("adb4eb23-1dd6-4406-a5e7-02e1e4c9dead"), + "Pv Model Test", + OperatorInput.NO_OPERATOR_ASSIGNED, + OperationTime.notLimited, + nodeInput, + new CosPhiFixed("cosPhiFixed:{(0.0,0.9)}"), + null, + 0.20000000298023224, + getQuantity(-8.926613807678223, DEGREE_GEOM), + getQuantity(97, PERCENT), + getQuantity(41.01871871948242, DEGREE_GEOM), + 0.8999999761581421, + 1, + false, + getQuantity(10, KILOVOLTAMPERE), + 0.8999999761581421, + ) + + // build the PvModel + val pvModel: PvModel = PvModel( + pvInput + ) + + private implicit val angleTolerance: Angle = Radians(1e-10) + private implicit val irradiationTolerance: Irradiation = + WattHoursPerSquareMeter(1e-10) + private implicit val powerTolerance: Power = Kilowatts(1e-10) + + "A PV Model" should { + "have sMax set to be 10% higher than its sRated" in { + When("sMax is calculated") + val actualSMax = pvModel.sMax + val expectedSMax = pvModel.sRated * 1.1 + + Then("result should match the test data") + actualSMax should approximate(expectedSMax) + } + + "calculate the day angle J correctly" in { + val testCases = Table( + ("time", "jSol"), + ("2019-01-05T05:15:00+01:00", 0.06885682528415985d), + ( + "2016-10-31T12:15:00+01:00", + 5.23311872159614873d, + ), // leap year => day = 305 + ( + "2017-10-31T12:15:00+01:00", + 5.21590451527510877d, + ), // regular year => day = 304 + ( + "2011-06-21T13:00:00+02:00", + 2.9436292808978335d, + ), // regular year => day = 172 + ( + "2011-04-05T16:00:00+02:00", + 1.6181353941777565d, + ), // regular year => day = 95 + ( + "2011-09-21T00:00:00+02:00", + 4.5273362624335105d, + ), // regular year => day = 264 + ( + "2011-03-21T00:00:00+01:00", + 1.359922299362157d, + ), // regular year => day = 80 + ) + + forAll(testCases) { (time, jSol) => + When("the day angle is calculated") + val jCalc = pvModel.calcAngleJ(ZonedDateTime.parse(time)) + + Then("result should match the test data") + jCalc should approximate(Radians(jSol)) + } + } + + "calculate the declination angle delta correctly" in { + val testCases = Table( + ("j", "deltaSol"), + (0d, -0.402449d), // Jan 1st + (2.943629280897834d, 0.40931542032971796d), // Jun 21 (maximum: 23.45°) + (6.024972212363987d, -0.4069636112528855d), // Dec 21 (minimum: -23.45°) + (4.52733626243351d, 0.01790836361732633d), // Sep 21 (0°) + (1.359922299362157d, -0.0011505915019577827d), // Mar 21 (0°) + ) + + forAll(testCases) { (j, deltaSol) => + When("the declination angle is calculated") + val deltaCalc = pvModel.calcSunDeclinationDelta(Radians(j)) + + Then("result should match the test data") + deltaCalc should approximate(Radians(deltaSol)) + } + } + + "calculate the hour angle omega correctly" in { + val testCases = Table( + ("time", "j", "longitude", "omegaSol"), + ( + "2019-01-01T05:00:00+01:00", + 0d, + 0.16d, + -1.9465030168609223d, + ), // long: ~9.17°E + ( + "2019-01-01T10:05:00+01:00", + 0d, + 0.16d, + -0.6156894622152458d, + ), // different time: 10:05 + ( + "2019-01-01T12:00:00+01:00", + 0d, + 0.16d, + -0.11390730226687622d, + ), // 12:00 + ( + "2019-01-01T14:00:00+01:00", + 0d, + 0.16d, + 0.40969147333142264d, + ), // 14:00 + ( + "2019-01-01T17:30:00+01:00", + 0d, + 0.16d, + 1.3259893306284447d, + ), // 17:30 + ( + "2019-03-21T05:00:00+01:00", + 1.359922299362157d, + 0.16d, + -1.9677750757840207d, + ), // different j (different date) + ( + "2019-01-01T05:00:00+01:00", + 0d, + 0.175d, + -1.9315030168609224d, + ), // different long, ~10°E + ( + "2011-06-21T11:00:00+02:00", + 2.9436292808978d, + 0.2337d, + -0.2960273936975511d, + ), // long of Berlin (13.39E) + ( + "2011-06-21T12:00:00+02:00", + 2.9436292808978d, + 0.2337d, + -0.034228005898401644d, + ), // long of Berlin (13.39E) + ( + "2011-06-21T13:00:00+02:00", + 2.9436292808978d, + 0.2337d, + 0.2275713819007478d, + ), // long of Berlin (13.39E) + ( + "2011-06-21T14:00:00+02:00", + 2.9436292808978d, + 0.2337d, + 0.4893707696998972d, + ), // long of Berlin (13.39E) + ( + "2011-06-21T15:00:00+02:00", + 2.9436292808978d, + 0.2337d, + 0.7511701574990467d, + ), // long of Berlin (13.39E) + ( + "2011-04-05T16:00:00+02:00", + 1.6181353941777565d, + 0.2337d, + 1.0062695999127786d, + ), // long of Berlin (13.39E) + ( + "2011-06-21T12:00:00+02:00", + 2.9436292808978d, + 0.5449d, + 0.2769719941015987d, + ), // long of Cairo (31.22E) + ) + + forAll(testCases) { (time, j, longitude, omegaSol) => + When("the hour angle is calculated") + val omegaCalc = pvModel.calcHourAngleOmega( + ZonedDateTime.parse(time), + Radians(j), + Radians(longitude), + ) + + Then("result should match the test data") + omegaCalc should approximate(Radians(omegaSol)) + } + } + + "calculate the sunset angle omegaSS correctly" in { + val testCases = Table( + ("latitude", "delta", "omegaSSSol"), + (0.9d, -0.402449d, 1.0045975406286176d), // lat: ~51.57°N + (0.935d, -0.402449d, 0.956011693657339d), // different lat: ~53.57°N + (0.9d, 0.017908d, 1.5933675693198284d), // different delta + ( + 0.157952297d, + 0.384670567d, + 1.635323424114512d, + ), // Example 2.2 Goswami Principles of Solar Engineering + ) + + forAll(testCases) { (latitude, delta, omegaSSSol) => + When("the sunset angle is calculated") + val omegaSSCalc = + pvModel.calcSunsetAngleOmegaSS(Radians(latitude), Radians(delta)) + + Then("result should match the test data") + omegaSSCalc should approximate(Radians(omegaSSSol)) + } + } + + "calculate the solar altitude angle alphaS correctly" in { + val testCases = Table( + ("omega", "delta", "latitude", "alphaSSol"), + ( + 1.946503016860923d, + -0.402449d, + 0.9d, + -0.5429594681352444d, + ), // Jan 1st, lat: ~51.57°N + ( + 1.967775075784021d, + -0.001150591501958d, + 0.9d, + -0.24363984335678648d, + ), // March 21st + ( + 1.946503016860923d, + -0.402449d, + 0.935d, + -0.5417322854819461d, + ), // Jan 1st, lat: ~53.57°N + ( + 1.256637061d, + -0.402449d, + 0.698d, + -0.033897520990303694d, + ), // omega: 82°, Jan 1st, lat: ~39.99°N + ( + 0.409691473331422d, + -0.402449d, + 0.9d, + 0.21956610107293822d, + ), // omega: 14:00, Jan 1st + ( + -0.85019406d, + -0.00720875d, + 0.9128072d, + 0.40911138927659646d, + ), // omega: -48.71° = 09:00, March 21st, lat: Berlin + ( + 0.22425484d, + 0.40899596d, + 0.9128072d, + 1.0386092658376944d, + ), // omega: +12.84° = 14:00 MESZ = 13:00 MEZ, June 21st, lat: Berlin + ( + -0.81703281d, + -0.00720875d, + 0.54628806d, + 0.619982384489836d, + ), // omega: -36.9809° = 09:00, March 21st, lat: Cairo + ( + -0.00438329d, + 0.40899596d, + 0.54628806d, + 1.4334492081530734d, + ), // omega: -0.25° = 12:00, June 21st, lat: Cairo + ( + 0.0126074d, + -0.40842934d, + 0.54628806d, + 0.6160025701438165d, + ), // omega: +0.7223° = 12:00, Dez 21st, lat: Cairo + ( + -0.78639785d, + 0.1549651d, + 0.54628806d, + 0.7430566034615067d, + ), // omega: -45.05° = 09:00, Sep 1st, lat: Cairo + ( + 1.04619786d, + 0.1549651d, + 0.54628806d, + 0.5270965151470974d, + ), // omega: +59.943° = 16:00, Sep 1st, lat: Cairo + ( + 0d, + -0.305432619d, + 0.518013722d, + 0.7473499857948969d, + ), // omega: 0 = Solar Noon, Feb 01st, lat/lon: Gainsville (29.68 N, 82.27 W) + ( + -1.374970385d, + 0.380755678d, + 0.157952297d, + 0.2391202791125743d, + ), // omega: -78.78° = 7:00 a.m., June 01st, lat/lon: Tocumen Panama (9.05 N, 79.37 W) + ( + 0d, + -0.268780705d, + -0.616101226d, + 1.2234758057948967d, + ), // omega: 0° = Solar noon., Nov 01st, lat/lon: Canberra Australia (35.3 S, 149.1 E) + ( + toRadians(-37.5d), + toRadians(-14d), + toRadians(43d), + toRadians(23.4529893659531784299686037109330117049955654837550d), + ), // '2011-02-13T09:30:00' from Duffie + ( + toRadians(97.5d), + toRadians(23.1d), + toRadians(43d), + toRadians(10.356151317506402829742934977890382350725031728508d), + ), // '2011-07-01T06:30:00' from Duffie + // Reference: Quaschning, Regenerative Energiesysteme figure 2.15 and figure 2.16 // gammaS@Quaschning = alphaS@SIMONA ! + ( + toRadians(-47.15114406), + toRadians(23.4337425d), + toRadians(52.3d), + toRadians(44.12595614280154d), + ), // Berlin (13.2E 52.3N) '2011-06-21T09:00:00' MEZ + ( + toRadians(-32.15114394d), + toRadians(23.4337425d), + toRadians(52.3d), + toRadians(52.15790489243239d), + ), // Berlin (13.2E 52.3N) '2011-06-21T10:00:00' MEZ + ( + toRadians(-17.15114381d), + toRadians(23.4337425d), + toRadians(52.3d), + toRadians(58.29851278388936d), + ), // Berlin (13.2E 52.3N) '2011-06-21T11:00:00' MEZ + ( + toRadians(-2.151143686d), + toRadians(23.4337425d), + toRadians(52.3d), + toRadians(61.086849596117524d), + ), // Berlin (13.2E 52.3N) '2011-06-21T12:00:00' MEZ + ( + toRadians(12.84885587d), + toRadians(23.4337425d), + toRadians(52.3d), + toRadians(59.50792770681503d), + ), // Berlin (13.2E 52.3N) '2011-06-21T13:00:00' MEZ + ( + toRadians(27.84885599d), + toRadians(23.4337425d), + toRadians(52.3d), + toRadians(54.170777340509574d), + ), // Berlin (13.2E 52.3N) '2011-06-21T14:00:00' MEZ + ( + toRadians(58.28178946d), + toRadians(7.79402247d), + toRadians(52.3d), + toRadians(25.203526133755485d), + ), // Berlin (13.2E 52.3N) '2011-09-04T16:00:00' MEZ + ( + toRadians(0.948855924d), + toRadians(23.4337425d), + toRadians(30.1d), + toRadians(83.28023248078853d), + ), // Cairo (31.3E 30.1N) '2011-06-21T12:00:00' MEZ+1h + ) + + forAll(testCases) { (omega, delta, latitude, alphaSSol) => + When("the solar altitude angle is calculated") + val alphaSCalc = pvModel.calcSolarAltitudeAngleAlphaS( + Radians(omega), + Radians(delta), + Radians(latitude), + ) + + Then("result should match the test data") + alphaSCalc should approximate(Radians(alphaSSol)) + } + } + + "calculate the zenith angle thetaZ correctly" in { + val testCases = Table( + ("alphaS", "thetaZSol"), + (0d, 1.5707963267948966d), // 0° + (0.785398163397448d, 0.7853981633974486d), // 45° + (1.570796326794897d, -4.440892098500626e-16d), // 90° + ) + + forAll(testCases) { (alphaS, thetaZSol) => + When("the zenith angle is calculated") + val thetaZCalc = pvModel.calcZenithAngleThetaZ(Radians(alphaS)) + + Then("result should match the test data") + thetaZCalc should approximate(Radians(thetaZSol)) + } + } + + "calculate the air mass correctly" in { + val testCases = Table( + ("thetaZ", "airMassSol"), + (0d, 1d), // 0° + (0.785398163397448d, 1.41321748045965d), // 45° + (1.570796326794897d, 37.640108631323025d), // 90° + ) + + forAll(testCases) { (thetaZ, airMassSol) => + When("the air mass is calculated") + val airMassCalc = pvModel.calcAirMass(Radians(thetaZ)) + + Then("result should match the test data") + airMassCalc shouldEqual airMassSol +- 1e-10 // the "approximate" function does not work for doubles, therefore the "shouldEqual" function is used + } + } + + "calculate the extraterrestrial radiation I0 correctly" in { + val testCases = Table( + ("j", "I0Sol"), + (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)) + + Then("result should match the test data") + I0Calc should approximate(WattHoursPerSquareMeter(I0Sol)) + } + } + + "calculate the angle of incidence thetaG correctly" in { + val testCases = Table( + ( + "latitudeDeg", + "deltaDeg", + "omegaDeg", + "gammaEDeg", + "alphaEDeg", + "thetaGOut", + ), + (43d, -14d, -22.5d, 45d, 15d, + 35.176193345578606393727080835951995075234213360724d), // Duffie + ( + 51.516667d, + +18.4557514d, + -15.00225713d, + 30d, + +0d, + 14.420271449960715d, + ), // Iqbal + ( + 51.516667d, + +18.4557514d, + -15.00225713d, + 90d, + +0d, + 58.65287310017624d, + ), // Iqbal + ( + 35.0d, + +23.2320597d, + +30.00053311d, + 45d, + 10d, + 39.62841449023577d, + ), // Kalogirou - Solar Energy Engineering Example 2.7 ISBN 978-0-12-374501-9; DOI https://doi.org/10.1016/B978-0-12-374501-9.X0001-5 + ( + 35.0d, + +23.2320597d, + +30.00053311d, + 45d, + 90d, + 18.946300807438607d, + ), // Kalogirou - Solar Energy Engineering Example 2.7 changed to 90° panel azimuth to WEST + ( + 35.0d, + +23.2320597d, + +74.648850625d, + 45d, + 90d, + 21.95480347380729d, + ), // Kalogirou - Solar Energy Engineering Example 2.7 90° panel azimuth to WEST at 17:00 + ( + 35.0d, + +23.2320597d, + +74.648850625d, + 45d, + -90d, + 109.00780288303966d, + ), // Kalogirou - Solar Energy Engineering Example 2.7 90° panel azimuth to EAST at 17:00 + ( + 27.96d, + -17.51d, + -11.1d, + 30d, + +10d, + 22.384603601536398d, + ), // Goswami Principles of Solar Engineering Example 2.7a + ( + -35.3d, + -17.51d, + -4.2d, + 30d, + +170d, + 14.882390116876563d, + ), // Goswami Principles of Solar Engineering Example 2.7b + (40d, -11.6d, 82.5d, 60d, 0d, 79.11011928744357d), + (40d, -11.6d, -82.5d, 60d, 0d, + 79.11011928744357d), // inverse hour angle + (40d, -11.6d, 78.0d, 60d, 0d, 74.92072065185143d), + (40d, -11.6d, -78.0d, 60d, 0d, + 74.92072065185143d), // inverse hour angle + (45d, -7.15d, -82.5d, 0d, 0d, + 89.79565474295107d), // Duffie Solar Engineering of Thermal Processes example 2.14.1 + + ) + + /** Calculate the angle of incidence of beam radiation 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. + */ + + forAll(testCases) { + (latitudeDeg, deltaDeg, omegaDeg, gammaEDeg, alphaEDeg, thetaGOut) => + Given("using the input data") + + When("the angle of incidence is calculated") + val thetaG = pvModel.calcAngleOfIncidenceThetaG( + Degrees(deltaDeg), + Degrees(latitudeDeg), + Degrees(gammaEDeg), + Degrees(alphaEDeg), + Degrees(omegaDeg), + ) + + Then("result should match the test data") + thetaG should approximate(Degrees(thetaGOut)) + } + } + + "deliver correct angles of incidence at different latitudes and slope angles" in { + val testCases = Table( + ( + "latitudeDeg", + "deltaDeg", + "omegaDeg", + "gammaEDeg", + "alphaEDeg", + "thetaGOut", + ), + (45d, -7.15d, -82.5d, 60d, 0d, 80.94904834048776d), // thetaG + (15d, -7.15, -82.5d, 30d, 0d, + 80.94904834048776d), // same test but 30° South with 30° less sloped surface + (0d, -7.15, -82.5d, 15d, 0d, + 80.94904834048776d), // same test but 15° South with 15° less sloped surface + (-15d, -7.15, -82.5d, 0d, 0d, + 80.94904834048776d), // same test but 15° South with 15° less sloped surface + (-30d, -7.15, -82.5d, 15d, 180d, + 80.94904834048776d), // same test but 15° South with 15° more sloped surface (Surface is now facing north, since it is in the southern hemisphere, therefore the surface azimuth is 180°) + (52.3d, 23.4337425, 2.15114395d, 0d, 0d, + 28.91315041538251d), // Berlin 21.06. 12:00 => thetaG = 90 - alphaS + (70.3d, 23.4337425, 2.15114395d, 18d, 0d, + 28.91315041538251d), // same test but 18° North with 18° sloped surface + + ) + + /** Iqbal Figure 1.6.2 - the angle of incidence of a surface sloped by + * angle beta (gammaE) at latitude phi should be same as the angle of + * incidence of an "unsloped" (horizontal) surface (where the angle of + * incidence is equal to the zenith angle of the sun) positioned at + * latitude phi - beta. Note that this is only true if the surface is + * facing directly north or south. + */ + forAll(testCases) { + (latitudeDeg, deltaDeg, omegaDeg, gammaEDeg, alphaEDeg, thetaGOut) => + Given("using pre-calculated parameters") + + When("the angle of incidence is calculated") + val thetaG = pvModel.calcAngleOfIncidenceThetaG( + Degrees(deltaDeg), + Degrees(latitudeDeg), + Degrees(gammaEDeg), + Degrees(alphaEDeg), + Degrees(omegaDeg), + ) + + Then("the result should match the pre-calculated data") + thetaG should approximate(Degrees(thetaGOut)) + } + } + + "calculate the estimate beam radiation eBeamS correctly" in { + val testCases = Table( + ( + "latitudeDeg", + "gammaEDeg", + "alphaEDeg", + "deltaDeg", + "omegaDeg", + "thetaGDeg", + "eBeamSSol", + ), + (40d, 0d, 0d, -11.6d, -37.5d, 37.0d, + 67.777778d), // flat surface => eBeamS = eBeamH + (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 + (40d, 60d, 0d, -11.6d, 62.0d, 76.0d, 199.16566536224116d), // sunset + (40d, 60d, 0d, -11.6d, 69.0d, 89.9d, + 245.77637766673405d), // sunset, cut off + (40d, 60d, 0d, -11.6d, 75.0d, 89.9d, 0d), // no sun + (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 + * 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 + */ + forAll(testCases) { + ( + latitudeDeg, + gammaEDeg, + alphaEDeg, + deltaDeg, + omegaDeg, + thetaGDeg, + eBeamSSol, + ) => + 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 + val omegaSS = pvModel.calcSunsetAngleOmegaSS( + Degrees(latitudeDeg), + Degrees(deltaDeg), + ) // Sunset angle + val omegaSR = -omegaSS // Sunrise angle + val omegas = pvModel.calculateBeamOmegas( + Degrees(thetaGDeg), + Degrees(omegaDeg), + omegaSS, + omegaSR, + ) // omega1 and omega2 + + When("the beam radiation is calculated") + val eBeamSCalc = pvModel.calcBeamRadiationOnSlopedSurface( + WattHoursPerSquareMeter(eBeamH), + omegas, + Degrees(deltaDeg), + Degrees(latitudeDeg), + Degrees(gammaEDeg), + Degrees(alphaEDeg), + ) + + Then("result should match the test data") + eBeamSCalc should approximate(WattHoursPerSquareMeter(eBeamSSol)) + } + } + + "calculate the estimate diffuse radiation eDifS correctly" in { + val testCases = Table( + ("thetaGDeg", "thetaZDeg", "gammaEDeg", "airMass", "I0", "eDifSSol"), + (37.0d, 62.2d, 60d, 2.13873080095658d, 1399.0077631849722d, + 216.46615469650982d), + ) + + forAll(testCases) { + (thetaGDeg, thetaZDeg, gammaEDeg, airMass, I0, eDifSSol) => + // 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 + 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 + + When("the diffuse radiation is calculated") + val eDifSCalc = pvModel.calcDiffuseRadiationOnSlopedSurfacePerez( + WattHoursPerSquareMeter(eDifH), + WattHoursPerSquareMeter(eBeamH), + airMass, + WattHoursPerSquareMeter(I0), + Degrees(thetaZDeg), + Degrees(thetaGDeg), + Degrees(gammaEDeg), + ) + + Then("result should match the test data") + eDifSCalc should approximate(WattHoursPerSquareMeter(eDifSSol)) + } + } + + "calculate the ground reflection eRefS" in { + val testCases = Table( + ("gammaEDeg", "albedo", "eRefSSol"), + (60d, 0.60d, 42.20833319999999155833336d), // '2011-02-20T09:00:00' + ) + + forAll(testCases) { (gammaEDeg, albedo, eRefSSol) => + 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 + + When("the ground reflection is calculated") + val eRefSCalc = pvModel.calcReflectedRadiationOnSlopedSurface( + WattHoursPerSquareMeter(eBeamH), + WattHoursPerSquareMeter(eDifH), + Degrees(gammaEDeg), + albedo, + ) + + Then("result should match the test data") + eRefSCalc should approximate(WattHoursPerSquareMeter(eRefSSol)) + } + } + } +} diff --git a/src/test/scala/edu/ie3/simona/model/participant2/StorageModelSpec.scala b/src/test/scala/edu/ie3/simona/model/participant2/StorageModelSpec.scala new file mode 100644 index 0000000000..0013f89db9 --- /dev/null +++ b/src/test/scala/edu/ie3/simona/model/participant2/StorageModelSpec.scala @@ -0,0 +1,428 @@ +/* + * © 2022. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.model.participant2 + +import edu.ie3.datamodel.models.OperationTime +import edu.ie3.datamodel.models.input.system.StorageInput +import edu.ie3.datamodel.models.input.system.`type`.StorageTypeInput +import edu.ie3.datamodel.models.input.system.characteristic.CosPhiFixed +import edu.ie3.datamodel.models.input.{NodeInput, OperatorInput} +import edu.ie3.datamodel.models.voltagelevels.GermanVoltageLevelUtils +import edu.ie3.simona.config.SimonaConfig.StorageRuntimeConfig +import edu.ie3.simona.ontology.messages.flex.MinMaxFlexibilityMessage.ProvideMinMaxFlexOptions +import edu.ie3.simona.test.common.UnitSpec +import edu.ie3.util.quantities.PowerSystemUnits +import edu.ie3.util.quantities.PowerSystemUnits._ +import edu.ie3.util.scala.quantities.DefaultQuantities.zeroKW +import org.scalatest.matchers.should.Matchers +import squants.energy.{KilowattHours, Kilowatts} +import squants.{Energy, Power} +import tech.units.indriya.quantity.Quantities +import tech.units.indriya.quantity.Quantities.getQuantity + +import java.util.UUID + +class StorageModelSpec extends UnitSpec with Matchers { + + final val inputModel: StorageInput = createStorageInput() + implicit val powerTolerance: Power = Kilowatts(1e-10) + implicit val energyTolerance: Energy = KilowattHours(1e-10) + + def createStorageInput(): StorageInput = { + val nodeInput = new NodeInput( + UUID.fromString("ad39d0b9-5ad6-4588-8d92-74c7d7de9ace"), + "NodeInput", + OperatorInput.NO_OPERATOR_ASSIGNED, + OperationTime.notLimited(), + Quantities.getQuantity(1, PowerSystemUnits.PU), + false, + NodeInput.DEFAULT_GEO_POSITION, + GermanVoltageLevelUtils.LV, + -1, + ) + + val typeInput = new StorageTypeInput( + UUID.fromString("fbee4995-24dd-45e4-9c85-7d986fe99ff3"), + "Test_StorageTypeInput", + Quantities.getQuantity(10000d, EURO), + getQuantity(0.05d, EURO_PER_MEGAWATTHOUR), + Quantities.getQuantity(100d, KILOWATTHOUR), + getQuantity(13d, KILOVOLTAMPERE), + 0.997, + getQuantity(10d, KILOWATT), + getQuantity(0.03, PU_PER_HOUR), + getQuantity(0.9, PU), + ) + + new StorageInput( + UUID.randomUUID(), + "Test_StorageInput", + new OperatorInput(UUID.randomUUID(), "NO_OPERATOR"), + OperationTime.notLimited(), + nodeInput, + CosPhiFixed.CONSTANT_CHARACTERISTIC, + null, + typeInput, + ) + } + + def buildStorageModel( + targetSoc: Option[Double] = Option.empty + ): StorageModel = { + val runtimeConfig = new StorageRuntimeConfig( + calculateMissingReactivePowerWithModel = false, + scaling = 1d, + uuids = List.empty, // not used + initialSoc = 0d, // not used + targetSoc = targetSoc, + ) + + StorageModel.apply( + inputModel, + runtimeConfig, + ) + } + + "StorageModel" should { + + "Calculate flex options" in { + val storageModel = buildStorageModel() + val tick = 3600L + // not used in calculation + val data = StorageModel.StorageRelevantData(tick) + + val testCases = Table( + ("storedEnergy", "pRef", "pMin", "pMax"), + // completely empty + (0.0, 0.0, 0.0, 10.0), + // at a tiny bit above empty + (0.011, 0.0, -10.0, 10.0), + // at mid-level charge + (60.0, 0.0, -10.0, 10.0), + // almost fully charged + (99.989, 0.0, -10.0, 10.0), + // fully charged + (100.0, 0.0, -10.0, 0.0), + ) + + forAll(testCases) { + (storedEnergy: Double, pRef: Double, pMin: Double, pMax: Double) => + val state = StorageModel.StorageState( + KilowattHours(storedEnergy), + tick, + ) + + storageModel.calcFlexOptions(state, data) match { + case result: ProvideMinMaxFlexOptions => + result.ref should approximate(Kilowatts(pRef)) + result.min should approximate(Kilowatts(pMin)) + result.max should approximate(Kilowatts(pMax)) + case _ => + fail("Expected result of type ProvideMinMaxFlexOptions") + } + } + } + + "Calculate flex options with target SOC" in { + val storageModel = buildStorageModel(Some(0.5d)) + val tick = 3600L + // not used in calculation + val data = StorageModel.StorageRelevantData(tick) + + val testCases = Table( + ("storedEnergy", "pRef", "pMin", "pMax"), + // completely empty + (0.0, 10.0, 0.0, 10.0), + // below margin of ref power target + (49.9974, 10.0, -10.0, 10.0), + // within margin below ref power target + (49.9976, 0.0, -10.0, 10.0), + // exactly at ref power target + (50.0, 0.0, -10.0, 10.0), + // within margin above ref power target + (50.0030, 0.0, -10.0, 10.0), + // above margin of ref power target + (50.0031, -10.0, -10.0, 10.0), + // at mid-level charge + (60.0, -10.0, -10.0, 10.0), + // fully charged + (100.0, -10.0, -10.0, 0.0), + ) + + forAll(testCases) { + (storedEnergy: Double, pRef: Double, pMin: Double, pMax: Double) => + val state = StorageModel.StorageState( + KilowattHours(storedEnergy), + tick, + ) + + storageModel.calcFlexOptions(state, data) match { + case result: ProvideMinMaxFlexOptions => + result.ref should approximate(Kilowatts(pRef)) + result.min should approximate(Kilowatts(pMin)) + result.max should approximate(Kilowatts(pMax)) + case _ => + fail("Expected result of type ProvideMinMaxFlexOptions") + } + } + } + + "Handle controlled power change" in { + val storageModel = buildStorageModel() + val tick = 3600L + // not used in calculation + val data = StorageModel.StorageRelevantData(tick) + val flexOptions = + ProvideMinMaxFlexOptions.noFlexOption(inputModel.getUuid, zeroKW) + + val testCases = Table( + ( + "storedEnergy", + "setPower", + "expPower", + "expActiveNext", + "expScheduled", + "expDelta", + ), + // no power + (0.0, 0.0, 0.0, false, false, 0.0), + (50.0, 0.0, 0.0, false, false, 0.0), + (100.0, 0.0, 0.0, false, false, 0.0), + // charging on empty + (0.0, 1.0, 1.0, true, true, 100 * 3600 / 0.9), + (0.0, 2.5, 2.5, true, true, 40 * 3600 / 0.9), + (0.0, 5.0, 5.0, true, true, 20 * 3600 / 0.9), + (0.0, 10.0, 10.0, true, true, 10 * 3600 / 0.9), + // charging on half full + (50.0, 5.0, 5.0, false, true, 10 * 3600 / 0.9), + (50.0, 10.0, 10.0, false, true, 5 * 3600 / 0.9), + // discharging on half full + (50.0, -4.5, -4.5, false, true, 10 * 3600.0), + (50.0, -9.0, -9.0, false, true, 5 * 3600.0), + // discharging on full + (100.0, -4.5, -4.5, true, true, 20 * 3600.0), + (100.0, -9.0, -9.0, true, true, 10 * 3600.0), + ) + + forAll(testCases) { + ( + storedEnergy: Double, + setPower: Double, + expPower: Double, + expActiveNext: Boolean, + expScheduled: Boolean, + expDelta: Double, + ) => + val state = StorageModel.StorageState( + KilowattHours(storedEnergy), + tick, + ) + + val (operatingPoint, changeIndicator) = + storageModel.handlePowerControl( + state, + data, + flexOptions, + Kilowatts(setPower), + ) + + operatingPoint.activePower should approximate(Kilowatts(expPower)) + + changeIndicator.changesAtTick.isDefined shouldBe expScheduled + changeIndicator.changesAtTick.forall( + _ == (tick + expDelta) + ) shouldBe true + changeIndicator.changesAtNextActivation shouldBe expActiveNext + } + } + + "Handle controlled power change with ref target SOC" in { + val storageModel = buildStorageModel(Some(0.5d)) + val tick = 3600L + // not used in calculation + val data = StorageModel.StorageRelevantData(tick) + val flexOptions = + ProvideMinMaxFlexOptions.noFlexOption(inputModel.getUuid, zeroKW) + + val testCases = Table( + ( + "storedEnergy", + "setPower", + "expPower", + "expActiveNext", + "expScheduled", + "expDelta", + ), + // no power + (0.0, 0.0, 0.0, false, false, 0.0), + (50.0, 0.0, 0.0, false, false, 0.0), + (100.0, 0.0, 0.0, false, false, 0.0), + // charging on empty + (0.0, 1.0, 1.0, true, true, 50 * 3600 / 0.9), + (0.0, 2.5, 2.5, true, true, 20 * 3600 / 0.9), + (0.0, 5.0, 5.0, true, true, 10 * 3600 / 0.9), + (0.0, 10.0, 10.0, true, true, 5 * 3600 / 0.9), + // charging on target ref + (50.0, 5.0, 5.0, true, true, 10 * 3600 / 0.9), + (50.0, 10.0, 10.0, true, true, 5 * 3600 / 0.9), + // discharging on target ref + (50.0, -4.5, -4.5, true, true, 10 * 3600.0), + (50.0, -9.0, -9.0, true, true, 5 * 3600.0), + // discharging on full + (100.0, -4.5, -4.5, true, true, 10 * 3600.0), + (100.0, -9.0, -9.0, true, true, 5 * 3600.0), + ) + + forAll(testCases) { + ( + storedEnergy: Double, + setPower: Double, + expPower: Double, + expActiveNext: Boolean, + expScheduled: Boolean, + expDelta: Double, + ) => + val state = StorageModel.StorageState( + KilowattHours(storedEnergy), + tick, + ) + + val (operatingPoint, changeIndicator) = + storageModel.handlePowerControl( + state, + data, + flexOptions, + Kilowatts(setPower), + ) + + operatingPoint.activePower should approximate(Kilowatts(expPower)) + + changeIndicator.changesAtTick.isDefined shouldBe expScheduled + changeIndicator.changesAtTick.forall( + _ == (tick + expDelta) + ) shouldBe true + changeIndicator.changesAtNextActivation shouldBe expActiveNext + } + } + + "Handle the edge case of discharging in tolerance margins" in { + val storageModel = buildStorageModel() + val tick = 1800L + // not used in calculation + val data = StorageModel.StorageRelevantData(tick) + val flexOptions = + ProvideMinMaxFlexOptions.noFlexOption(inputModel.getUuid, zeroKW) + + // margin is at ~ 0.0030864 kWh + val state = StorageModel.StorageState( + KilowattHours(0.002d), + tick, + ) + + val (operatingPoint, changeIndicator) = + storageModel.handlePowerControl( + state, + data, + flexOptions, + Kilowatts(-5d), + ) + + operatingPoint.activePower should approximate(zeroKW) + + changeIndicator.changesAtTick.isDefined shouldBe false + changeIndicator.changesAtNextActivation shouldBe true + } + + "Handle the edge case of charging in tolerance margins" in { + val storageModel = buildStorageModel() + val tick = 1800L + // not used in calculation + val data = StorageModel.StorageRelevantData(tick) + val flexOptions = + ProvideMinMaxFlexOptions.noFlexOption(inputModel.getUuid, zeroKW) + + // margin is at ~ 99.9975 kWh + val state = StorageModel.StorageState( + KilowattHours(99.999d), + tick, + ) + + val (operatingPoint, changeIndicator) = + storageModel.handlePowerControl( + state, + data, + flexOptions, + Kilowatts(9d), + ) + + operatingPoint.activePower should approximate(zeroKW) + + changeIndicator.changesAtTick.isDefined shouldBe false + changeIndicator.changesAtNextActivation shouldBe true + } + + "Handle the edge case of discharging in positive target margin" in { + val storageModel = buildStorageModel(Some(0.3d)) + val tick = 1800L + // not used in calculation + val data = StorageModel.StorageRelevantData(tick) + val flexOptions = + ProvideMinMaxFlexOptions.noFlexOption(inputModel.getUuid, zeroKW) + + // margin is at ~ 30.0025 kWh + val state = StorageModel.StorageState( + KilowattHours(30.0024d), + tick, + ) + + val (operatingPoint, changeIndicator) = + storageModel.handlePowerControl( + state, + data, + flexOptions, + Kilowatts(-9d), + ) + + operatingPoint.activePower should approximate(Kilowatts(-9d)) + + changeIndicator.changesAtTick should be( + Some(tick + 10801L) + ) + changeIndicator.changesAtNextActivation shouldBe true + } + + "Handle the edge case of charging in negative target margin" in { + val storageModel = buildStorageModel(Some(0.4d)) + val tick = 1800L + // not used in calculation + val data = StorageModel.StorageRelevantData(tick) + val flexOptions = + ProvideMinMaxFlexOptions.noFlexOption(inputModel.getUuid, zeroKW) + + // margin is at ~ 39.9975 kWh + val state = StorageModel.StorageState( + KilowattHours(39.998d), + tick, + ) + + val (operatingPoint, changeIndicator) = + storageModel.handlePowerControl( + state, + data, + flexOptions, + Kilowatts(5d), + ) + + operatingPoint.activePower should approximate(Kilowatts(5d)) + + changeIndicator.changesAtTick should be( + Some(tick + 48002L) + ) + changeIndicator.changesAtNextActivation shouldBe true + } + } +} diff --git a/src/test/scala/edu/ie3/simona/model/participant2/WecModelSpec.scala b/src/test/scala/edu/ie3/simona/model/participant2/WecModelSpec.scala new file mode 100644 index 0000000000..9937c142e5 --- /dev/null +++ b/src/test/scala/edu/ie3/simona/model/participant2/WecModelSpec.scala @@ -0,0 +1,199 @@ +/* + * © 2020. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.model.participant2 + +import edu.ie3.datamodel.models.OperationTime +import edu.ie3.datamodel.models.input.system.WecInput +import edu.ie3.datamodel.models.input.system.`type`.WecTypeInput +import edu.ie3.datamodel.models.input.system.characteristic.{ + ReactivePowerCharacteristic, + WecCharacteristicInput, +} +import edu.ie3.datamodel.models.input.{NodeInput, OperatorInput} +import edu.ie3.datamodel.models.voltagelevels.GermanVoltageLevelUtils +import edu.ie3.simona.test.common.{DefaultTestData, UnitSpec} +import edu.ie3.util.quantities.PowerSystemUnits +import squants.Each +import squants.energy.Watts +import squants.motion.{MetersPerSecond, Pascals} +import squants.thermal.Celsius +import tech.units.indriya.quantity.Quantities +import tech.units.indriya.unit.Units.{METRE, PERCENT, SQUARE_METRE} + +import java.util.UUID + +class WecModelSpec extends UnitSpec with DefaultTestData { + + val nodeInput = new NodeInput( + UUID.fromString("ad39d0b9-5ad6-4588-8d92-74c7d7de9ace"), + "NodeInput", + OperatorInput.NO_OPERATOR_ASSIGNED, + OperationTime.notLimited(), + Quantities.getQuantity(1, PowerSystemUnits.PU), + false, + NodeInput.DEFAULT_GEO_POSITION, + GermanVoltageLevelUtils.LV, + -1, + ) + + val typeInput = new WecTypeInput( + UUID.randomUUID(), + "WecTypeInput", + Quantities.getQuantity(1000, PowerSystemUnits.EURO), + Quantities.getQuantity(1000, PowerSystemUnits.EURO_PER_MEGAWATTHOUR), + Quantities.getQuantity(1200.0, PowerSystemUnits.KILOVOLTAMPERE), + 0.95, + new WecCharacteristicInput( + "cP:{(0.0,0.0), (1.0,0.0), (2.0,0.115933516), (3.0,0.286255595), (4.0,0.39610618), " + + "(5.0,0.430345211), (6.0,0.45944023), (7.0,0.479507331), (8.0,0.492113623), " + + "(9.0,0.500417188), (10.0,0.488466547), (11.0,0.420415054), (12.0,0.354241299), " + + "(13.0,0.288470591), (14.0,0.230965702), (15.0,0.18778367), (16.0,0.154728976), " + + "(17.0,0.128998552), (18.0,0.108671106), (19.0,0.09239975), (20.0,0.079221236), " + + "(21.0,0.068434282), (22.0,0.059520087), (23.0,0.052089249), (24.0,0.045845623), " + + "(25.0,0.040561273), (26.0,0.036058824), (27.0,0.032198846), (28.0,0.016618264), " + + "(29.0,0.010330976), (30.0,0.006091519), (31.0,0.003331177), (32.0,0.001641637), " + + "(33.0,0.000705423), (34.0,0.000196644), (35.0,0.0), (36.0,0.0), (37.0,0.0), (38.0,0.0), " + + "(39.0,0.0), (40.0,0.0), (41.0,0.0), (42.0,0.0), (43.0,0.0), (44.0,0.0), (45.0,0.0), " + + "(46.0,0.0), (47.0,0.0), (48.0,0.0), (49.0,0.0), (50.0,0.0)}" + ), + Quantities.getQuantity(15, PERCENT), + Quantities.getQuantity(5281.0, SQUARE_METRE), + Quantities.getQuantity(20, METRE), + ) + + val inputModel = new WecInput( + UUID.randomUUID(), + "WecTypeInput", + nodeInput, + ReactivePowerCharacteristic.parse("cosPhiFixed:{(0.00,0.95)}"), + null, + typeInput, + false, + ) + + "WecModel" should { + + "check build method of companion object" in { + val wecModel = WecModel.apply(inputModel) + wecModel.uuid shouldBe inputModel.getUuid + wecModel.cosPhiRated shouldBe typeInput.getCosPhiRated + wecModel.sRated.toWatts shouldBe (typeInput.getsRated.toSystemUnit.getValue + .doubleValue() +- 1e-5) + } + + "determine Betz coefficient correctly" in { + val wecModel = WecModel.apply(inputModel) + + val testCases = Table( + ("velocity", "expectedBetzResult"), + (2.0, 0.115933516), + (2.5, 0.2010945555), + (18.0, 0.108671106), + (27.0, 0.032198846), + (34.0, 0.000196644), + (40.0, 0.0), + ) + + forAll(testCases) { case (velocity: Double, expectedBetzResult: Double) => + val windVel = MetersPerSecond(velocity) + val betzFactor = wecModel.determineBetzCoefficient(windVel) + + betzFactor shouldEqual Each(expectedBetzResult) + } + } + + "calculate active power output depending on velocity" in { + val wecModel = WecModel.apply(inputModel) + val testCases = Table( + ("velocity", "expectedPower"), + (1.0, 0.0), + (2.0, -2948.8095851378266), + (3.0, -24573.41320418286), + (7.0, -522922.2325710509), + (9.0, -1140000.0), + (13.0, -1140000.0), + (15.0, -1140000.0), + (19.0, -1140000.0), + (23.0, -1140000.0), + (27.0, -1140000.0), + (34.0, -24573.39638823692), + (40.0, 0.0), + ) + + forAll(testCases) { (velocity: Double, expectedPower: Double) => + val wecData = WecModel.WecRelevantData( + MetersPerSecond(velocity), + Celsius(20), + Some(Pascals(101325d)), + ) + val (operatingPoint, nextTick) = + wecModel.determineOperatingPoint( + ParticipantModel.ConstantState, + wecData, + ) + + operatingPoint.activePower shouldBe Watts(expectedPower) + nextTick shouldBe None + } + } + + "calculate air density correctly" in { + val wecModel = WecModel.apply(inputModel) + val testCases = Seq( + (-15.0, 100129.44, 1.3512151548083537), + (-5.0, 99535.96, 1.2931147269065832), + (0.0, 99535.96, 1.2694443127219486), + (5.0, 100129.44, 1.25405785444464), + (20.0, 100129.44, 1.1898897909390296), + (25.0, 100427.25, 1.1734149214121568), + (37.0, 100427.25, 1.1280143763309192), + // test cases where no air pressure is given + (0.0, -1.0, 1.2041), + (5.0, -1.0, 1.2041), + (40.0, -1.0, 1.2041), + ) + + testCases.foreach { case (temperature, pressure, densityResult) => + val temperatureV = Celsius(temperature) + val pressureV = + if (pressure > 0) Some(Pascals(pressure)) else Option.empty + + val airDensity = wecModel + .calculateAirDensity(temperatureV, pressureV) + .toKilogramsPerCubicMeter + + airDensity should be(densityResult) + } + } + + "calculate active power output depending on temperature" in { + val wecModel = WecModel.apply(inputModel) + val testCases = Table( + ("temperature", "expectedPower"), + (35.0, -23377.23862017266), + (20.0, -24573.41320418286), + (-25.0, -29029.60338829823), + ) + + forAll(testCases) { (temperature: Double, expectedPower: Double) => + val wecData = WecModel.WecRelevantData( + MetersPerSecond(3.0), + Celsius(temperature), + Some(Pascals(101325d)), + ) + val (operatingPoint, nextTick) = + wecModel.determineOperatingPoint( + ParticipantModel.ConstantState, + wecData, + ) + + operatingPoint.activePower shouldBe Watts(expectedPower) + nextTick shouldBe None + } + } + } +} From 5298be2140c1434ea041208ca20bb66b318b045d Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Thu, 14 Nov 2024 18:59:06 +0100 Subject: [PATCH 33/77] Adding test for determineState Signed-off-by: Sebastian Peter --- .../model/participant2/StorageModelSpec.scala | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/src/test/scala/edu/ie3/simona/model/participant2/StorageModelSpec.scala b/src/test/scala/edu/ie3/simona/model/participant2/StorageModelSpec.scala index 0013f89db9..38b99a0621 100644 --- a/src/test/scala/edu/ie3/simona/model/participant2/StorageModelSpec.scala +++ b/src/test/scala/edu/ie3/simona/model/participant2/StorageModelSpec.scala @@ -13,6 +13,7 @@ import edu.ie3.datamodel.models.input.system.characteristic.CosPhiFixed import edu.ie3.datamodel.models.input.{NodeInput, OperatorInput} import edu.ie3.datamodel.models.voltagelevels.GermanVoltageLevelUtils import edu.ie3.simona.config.SimonaConfig.StorageRuntimeConfig +import edu.ie3.simona.model.participant2.ParticipantModel.ActivePowerOperatingPoint import edu.ie3.simona.ontology.messages.flex.MinMaxFlexibilityMessage.ProvideMinMaxFlexOptions import edu.ie3.simona.test.common.UnitSpec import edu.ie3.util.quantities.PowerSystemUnits @@ -89,6 +90,93 @@ class StorageModelSpec extends UnitSpec with Matchers { "StorageModel" should { + "Determine the current state" in { + val storageModel = buildStorageModel() + + val lastTick = 3600L + + val testCases = Table( + ("lastEnergy", "power", "duration", "expEnergy"), + /* empty storage */ + // zero power + (0.0, 0.0, 3600, 0.0), + // zero duration + (0.0, 5.0, 0, 0.0), + // charging a tiny bit + (0.0, 1.0, 1, 0.00025), + // charging until half + (0.0, 10.0, 20000, 50.0), + // charging until almost full + (0.0, 10.0, 39999, 99.9975), + // charging until full + (0.0, 10.0, 40000, 100.0), + // overcharging a tiny bit + (0.0, 10.0, 40001, 100.0), + // discharging + (0.0, -10.0, 3600, 0.0), + /* half full storage */ + // zero power + (50.0, 0.0, 3600, 50.0), + // zero duration + (50.0, 5.0, 0, 50.0), + // charging a tiny bit + (50.0, 1.0, 1, 50.00025), + // charging until almost full + (50.0, 10.0, 19999, 99.9975), + // charging until full + (50.0, 10.0, 20000, 100.0), + // overcharging a tiny bit + (50.0, 10.0, 20001, 100.0), + // discharging a tiny bit + (50.0, -0.81, 1, 49.99975), + // discharging until almost empty + (50.0, -8.1, 19999, 0.0025), + // discharging until empty + (50.0, -8.1, 20000, 0.0), + // undercharging a tiny bit + (50.0, -8.1, 20001, 0.0), + /* full storage */ + // zero power + (100.0, 0.0, 3600, 100.0), + // zero duration + (100.0, -5.0, 0, 100.0), + // discharging a tiny bit + (100.0, -0.81, 1, 99.99975), + // discharging until half + (100.0, -8.1, 20000, 50.0), + // discharging until almost empty + (100.0, -8.1, 39999, 0.0025), + // discharging until empty + (100.0, -8.1, 40000, 0.0), + // undercharging a tiny bit + (100.0, -8.1, 40001, 0.0), + // charging + (100.0, 10.0, 3600, 100.0), + ) + + forAll(testCases) { + (lastEnergy: Double, power: Double, duration: Int, expEnergy: Double) => + val lastState = StorageModel.StorageState( + KilowattHours(lastEnergy), + lastTick, + ) + + val operatingPoint = + ActivePowerOperatingPoint(Kilowatts(power)) + + val currentTick = lastTick + duration + + val newState = storageModel.determineState( + lastState, + operatingPoint, + currentTick, + ) + + newState.tick shouldBe currentTick + newState.storedEnergy should approximate(KilowattHours(expEnergy)) + } + } + "Calculate flex options" in { val storageModel = buildStorageModel() val tick = 3600L From de60e0cbba4a4344886a02e3b65a770921914a38 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Fri, 15 Nov 2024 16:34:52 +0100 Subject: [PATCH 34/77] Enhancing charging strats of evcs Signed-off-by: Sebastian Peter --- .../evcs/ConstantPowerCharging.scala | 43 +++++++++++++++++++ .../model/participant2/evcs/EvcsModel.scala | 13 ++++++ ...ategy.scala => MaximumPowerCharging.scala} | 19 +++----- 3 files changed, 62 insertions(+), 13 deletions(-) create mode 100644 src/main/scala/edu/ie3/simona/model/participant2/evcs/ConstantPowerCharging.scala rename src/main/scala/edu/ie3/simona/model/participant2/evcs/{MaximumPowerStrategy.scala => MaximumPowerCharging.scala} (59%) diff --git a/src/main/scala/edu/ie3/simona/model/participant2/evcs/ConstantPowerCharging.scala b/src/main/scala/edu/ie3/simona/model/participant2/evcs/ConstantPowerCharging.scala new file mode 100644 index 0000000000..1873270527 --- /dev/null +++ b/src/main/scala/edu/ie3/simona/model/participant2/evcs/ConstantPowerCharging.scala @@ -0,0 +1,43 @@ +/* + * © 2021. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.model.participant2.evcs + +import edu.ie3.simona.model.participant.evcs.EvModelWrapper +import edu.ie3.simona.model.participant2.evcs.EvcsModel.ChargingStrategy +import squants.{Power, Seconds} + +import java.util.UUID + +/** Determine scheduling for charging the EVs currently parked at the charging + * station by charging with constant power from current time until departure. + * If less than the maximum power is required to reach 100% SoC, the power is + * reduced accordingly. + */ +object ConstantPowerCharging extends ChargingStrategy { + + override def determineChargingPowers( + evs: Iterable[EvModelWrapper], + currentTick: Long, + chargingProps: EvcsChargingProperties, + ): Map[UUID, Power] = evs + .filter(ev => ev.storedEnergy < ev.eStorage) + .map { ev => + val maxChargingPower = chargingProps.getMaxAvailableChargingPower(ev) + val remainingParkingTime = Seconds(ev.departureTick - currentTick) + + val requiredEnergyUntilFull = ev.eStorage - ev.storedEnergy + val maxChargedEnergyUntilDeparture = + maxChargingPower * remainingParkingTime + val actualChargedEnergy = + requiredEnergyUntilFull.min(maxChargedEnergyUntilDeparture) + + val chargingPower = actualChargedEnergy / remainingParkingTime + + ev.uuid -> chargingPower + } + .toMap +} diff --git a/src/main/scala/edu/ie3/simona/model/participant2/evcs/EvcsModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/evcs/EvcsModel.scala index 7dba3e2277..2af1d0019e 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/evcs/EvcsModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/evcs/EvcsModel.scala @@ -566,6 +566,19 @@ object EvcsModel { ) extends OperationRelevantData trait ChargingStrategy { + + /** Determine scheduling for charging the EVs currently parked at the + * charging station until their departure. + * + * @param evs + * currently parked evs at the charging station + * @param currentTick + * current tick + * @param chargingProps + * interface that provides information on charging station + * @return + * scheduling for charging the EVs + */ def determineChargingPowers( evs: Iterable[EvModelWrapper], currentTick: Long, diff --git a/src/main/scala/edu/ie3/simona/model/participant2/evcs/MaximumPowerStrategy.scala b/src/main/scala/edu/ie3/simona/model/participant2/evcs/MaximumPowerCharging.scala similarity index 59% rename from src/main/scala/edu/ie3/simona/model/participant2/evcs/MaximumPowerStrategy.scala rename to src/main/scala/edu/ie3/simona/model/participant2/evcs/MaximumPowerCharging.scala index 0283c2888f..65490b0571 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/evcs/MaximumPowerStrategy.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/evcs/MaximumPowerCharging.scala @@ -12,20 +12,13 @@ import squants.Power import java.util.UUID -object MaximumPowerStrategy extends ChargingStrategy { +/** Determine scheduling for charging the EVs currently parked at the charging + * station until their departure. In this case, each EV is charged with maximum + * power from current time until it reaches either 100% SoC or its departure + * time. + */ +object MaximumPowerCharging extends ChargingStrategy { - /** Determine scheduling for charging the EVs currently parked at the charging - * station until their departure. In this case, each EV is charged with - * maximum power from current time until it reaches either 100% SoC or its - * departure time. - * - * @param currentTick - * current tick - * @param evs - * currently parked evs at the charging station - * @return - * scheduling for charging the EVs - */ def determineChargingPowers( evs: Iterable[EvModelWrapper], currentTick: Long, From 244dd4dffd3531e731d51fc611625ea0d3bb1cfe Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Fri, 15 Nov 2024 16:37:22 +0100 Subject: [PATCH 35/77] Adapting ScalaDoc Signed-off-by: Sebastian Peter --- .../model/participant2/evcs/MaximumPowerCharging.scala | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/model/participant2/evcs/MaximumPowerCharging.scala b/src/main/scala/edu/ie3/simona/model/participant2/evcs/MaximumPowerCharging.scala index 65490b0571..223838c22b 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/evcs/MaximumPowerCharging.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/evcs/MaximumPowerCharging.scala @@ -13,9 +13,8 @@ import squants.Power import java.util.UUID /** Determine scheduling for charging the EVs currently parked at the charging - * station until their departure. In this case, each EV is charged with maximum - * power from current time until it reaches either 100% SoC or its departure - * time. + * station by charging with maximum power from current time until it reaches + * either 100% SoC or its departure time. */ object MaximumPowerCharging extends ChargingStrategy { From d7a5f867d1394e1bd02675eea658e84d4368db6e Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Fri, 15 Nov 2024 17:03:39 +0100 Subject: [PATCH 36/77] Fix compilation error Signed-off-by: Sebastian Peter --- .../edu/ie3/simona/model/participant2/StorageModel.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/model/participant2/StorageModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/StorageModel.scala index 8770aa217a..4698e435db 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/StorageModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/StorageModel.scala @@ -31,7 +31,7 @@ import edu.ie3.simona.ontology.messages.flex.MinMaxFlexibilityMessage.ProvideMin import edu.ie3.simona.service.ServiceType import edu.ie3.util.quantities.PowerSystemUnits import edu.ie3.util.quantities.QuantityUtils.RichQuantityDouble -import edu.ie3.util.scala.quantities.DefaultQuantities.{zeroKW, zeroKWH} +import edu.ie3.util.scala.quantities.DefaultQuantities.{zeroKW, zeroKWh} import squants.energy.{KilowattHours, Kilowatts} import squants.{Dimensionless, Each, Energy, Power, Seconds} @@ -53,7 +53,7 @@ class StorageModel private ( StorageRelevantData, ] { - private val minEnergy = zeroKWH + private val minEnergy = zeroKWh /** Tolerance for power comparisons. With very small (dis-)charging powers, * problems can occur when calculating the future tick at which storage is From 4346981d1b03add95733660aee7f25d0139f04bb Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Fri, 15 Nov 2024 17:03:56 +0100 Subject: [PATCH 37/77] Method inheritance Signed-off-by: Sebastian Peter --- .../evcs/EvcsChargingProperties.scala | 21 ++++++++++++++++++- .../model/participant2/evcs/EvcsModel.scala | 21 ------------------- 2 files changed, 20 insertions(+), 22 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/model/participant2/evcs/EvcsChargingProperties.scala b/src/main/scala/edu/ie3/simona/model/participant2/evcs/EvcsChargingProperties.scala index c609f51c7a..c7e752d932 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/evcs/EvcsChargingProperties.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/evcs/EvcsChargingProperties.scala @@ -20,5 +20,24 @@ trait EvcsChargingProperties { val lowestEvSoc: Double - def getMaxAvailableChargingPower(ev: EvModelWrapper): Power + /** Returns the maximum available charging power for an EV, which depends on + * ev and charging station limits for AC and DC current + * + * @param ev + * ev for which the max charging power should be returned + * @return + * maximum charging power for the EV at this charging station + */ + def getMaxAvailableChargingPower( + ev: EvModelWrapper + ): Power = { + val evPower = currentType match { + case ElectricCurrentType.AC => + ev.sRatedAc + case ElectricCurrentType.DC => + ev.sRatedDc + } + /* Limit the charging power to the minimum of ev's and evcs' permissible power */ + evPower.min(sRated) + } } diff --git a/src/main/scala/edu/ie3/simona/model/participant2/evcs/EvcsModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/evcs/EvcsModel.scala index 2af1d0019e..179edbb1fe 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/evcs/EvcsModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/evcs/EvcsModel.scala @@ -516,27 +516,6 @@ class EvcsModel private ( private def calcToleranceMargin(ev: EvModelWrapper): Energy = getMaxAvailableChargingPower(ev) * Seconds(1) - /** Returns the maximum available charging power for an EV, which depends on - * ev and charging station limits for AC and DC current - * - * @param ev - * ev for which the max charging power should be returned - * @return - * maximum charging power for the EV at this charging station - */ - override def getMaxAvailableChargingPower( - ev: EvModelWrapper - ): Power = { - val evPower = currentType match { - case ElectricCurrentType.AC => - ev.sRatedAc - case ElectricCurrentType.DC => - ev.sRatedDc - } - /* Limit the charging power to the minimum of ev's and evcs' permissible power */ - evPower.min(sRated) - } - def getInitialState: EvcsState = EvcsState(Seq.empty, -1) } From 25e7195921b0307bd4c21de6a27c5be6279702b0 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Fri, 15 Nov 2024 17:11:27 +0100 Subject: [PATCH 38/77] EVCS charging strat tests Signed-off-by: Sebastian Peter --- .../evcs/ConstantPowerChargingSpec.scala | 146 ++++++++++++++++++ .../evcs/MaximumPowerChargingSpec.scala | 139 +++++++++++++++++ .../evcs/MockEvcsChargingProperties.scala | 18 +++ 3 files changed, 303 insertions(+) create mode 100644 src/test/scala/edu/ie3/simona/model/participant2/evcs/ConstantPowerChargingSpec.scala create mode 100644 src/test/scala/edu/ie3/simona/model/participant2/evcs/MaximumPowerChargingSpec.scala create mode 100644 src/test/scala/edu/ie3/simona/model/participant2/evcs/MockEvcsChargingProperties.scala diff --git a/src/test/scala/edu/ie3/simona/model/participant2/evcs/ConstantPowerChargingSpec.scala b/src/test/scala/edu/ie3/simona/model/participant2/evcs/ConstantPowerChargingSpec.scala new file mode 100644 index 0000000000..ddb19ecb1b --- /dev/null +++ b/src/test/scala/edu/ie3/simona/model/participant2/evcs/ConstantPowerChargingSpec.scala @@ -0,0 +1,146 @@ +/* + * © 2022. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.model.participant2.evcs + +import edu.ie3.simona.model.participant.evcs.EvModelWrapper +import edu.ie3.simona.test.common.UnitSpec +import edu.ie3.simona.test.common.model.MockEvModel +import edu.ie3.util.quantities.QuantityUtils.RichQuantityDouble +import org.scalatest.prop.TableDrivenPropertyChecks +import squants.energy.Kilowatts + +import java.util.UUID + +class ConstantPowerChargingSpec + extends UnitSpec + with TableDrivenPropertyChecks { + + "Calculating constant power charging schedules" should { + + "not charge evs if they are fully charged" in { + val ev = EvModelWrapper( + new MockEvModel( + UUID.randomUUID(), + "Test EV", + 5.0.asKiloWatt, + 10.0.asKiloWatt, + 20.0.asKiloWattHour, + 20.0.asKiloWattHour, + 3600L, + ) + ) + + val actualSchedule = ConstantPowerCharging.determineChargingPowers( + Seq(ev), + 1800L, + MockEvcsChargingProperties, + ) + + actualSchedule shouldBe Map.empty + } + + "work correctly with one ev" in { + val offset = 1800L + + val cases = Table( + ("stayingTicks", "storedEnergy", "expectedPower"), + // empty battery + (3600L, 0.0, 5.0), // more than max power, limited + (7200L, 0.0, 5.0), // exactly max power + (14400L, 0.0, 2.5), // less than max power + (360000L, 0.0, 0.1), // long stay: 100 hours + // half full battery + (1800L, 5.0, 5.0), // more than max power, limited + (3600L, 5.0, 5.0), // exactly max power + (7200L, 5.0, 2.5), // less than max power + (180000L, 5.0, 0.1), // long stay: 100 hours + ) + + forAll(cases) { (stayingTicks, storedEnergy, expectedPower) => + val ev = EvModelWrapper( + new MockEvModel( + UUID.randomUUID(), + "Test EV", + 5.0.asKiloWatt, // using AC charging here + 10.0.asKiloWatt, + 10.0.asKiloWattHour, + storedEnergy.asKiloWattHour, + offset + stayingTicks, + ) + ) + + val chargingMap = ConstantPowerCharging.determineChargingPowers( + Seq(ev), + offset, + MockEvcsChargingProperties, + ) + + chargingMap shouldBe Map( + ev.uuid -> Kilowatts(expectedPower) + ) + } + + } + + "work correctly with two evs" in { + val offset = 3600L + + val cases = Table( + ("stayingTicks", "storedEnergy", "expectedPower"), + // empty battery + (3600L, 0.0, 5.0), // more than max power, limited + (7200L, 0.0, 5.0), // exactly max power + (14400L, 0.0, 2.5), // less than max power + (360000L, 0.0, 0.1), // long stay: 100 hours + // half full battery + (1800L, 5.0, 5.0), // more than max power, limited + (3600L, 5.0, 5.0), // exactly max power + (7200L, 5.0, 2.5), // less than max power + (180000L, 5.0, 0.1), // long stay: 100 hours + ) + + forAll(cases) { (stayingTicks, storedEnergy, expectedPower) => + val givenEv = EvModelWrapper( + new MockEvModel( + UUID.randomUUID(), + "First EV", + 5.0.asKiloWatt, // using AC charging here + 10.0.asKiloWatt, + 10.0.asKiloWattHour, + 5.0.asKiloWattHour, + offset + 3600L, + ) + ) + + val ev = EvModelWrapper( + new MockEvModel( + UUID.randomUUID(), + "Test EV", + 5.0.asKiloWatt, // using AC charging here + 10.0.asKiloWatt, + 10.0.asKiloWattHour, + storedEnergy.asKiloWattHour, + offset + stayingTicks, + ) + ) + + val chargingMap = ConstantPowerCharging.determineChargingPowers( + Seq(givenEv, ev), + offset, + MockEvcsChargingProperties, + ) + + chargingMap shouldBe Map( + givenEv.uuid -> Kilowatts(5.0), + ev.uuid -> Kilowatts(expectedPower), + ) + } + + } + } + +} diff --git a/src/test/scala/edu/ie3/simona/model/participant2/evcs/MaximumPowerChargingSpec.scala b/src/test/scala/edu/ie3/simona/model/participant2/evcs/MaximumPowerChargingSpec.scala new file mode 100644 index 0000000000..92fb49009e --- /dev/null +++ b/src/test/scala/edu/ie3/simona/model/participant2/evcs/MaximumPowerChargingSpec.scala @@ -0,0 +1,139 @@ +/* + * © 2022. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.model.participant2.evcs + +import edu.ie3.simona.model.participant.evcs.EvModelWrapper +import edu.ie3.simona.test.common.UnitSpec +import edu.ie3.simona.test.common.model.MockEvModel +import edu.ie3.util.quantities.QuantityUtils.RichQuantityDouble +import org.scalatest.prop.TableDrivenPropertyChecks +import squants.energy.Kilowatts + +import java.util.UUID + +class MaximumPowerChargingSpec extends UnitSpec with TableDrivenPropertyChecks { + + "Calculating maximum power charging schedules" should { + + "not charge evs if they are fully charged" in { + val ev = EvModelWrapper( + new MockEvModel( + UUID.randomUUID(), + "Test EV", + 5.0.asKiloWatt, + 10.0.asKiloWatt, + 20.0.asKiloWattHour, + 20.0.asKiloWattHour, + 3600, + ) + ) + + val actualSchedule = MaximumPowerCharging.determineChargingPowers( + Seq(ev), + 1800L, + MockEvcsChargingProperties, + ) + + actualSchedule shouldBe Map.empty + } + + "work correctly with one ev" in { + val offset = 1800L + + val cases = Table( + ("stayingTicks", "storedEnergy"), + // empty battery + (3600L, 0.0), // stay shorter than full + (7200L, 0.0), // exactly full + (14400L, 0.0), // full before end of stay + // half full battery + (1800L, 5.0), // stay shorter than full + (3600L, 5.0), // exactly full + (14400L, 5.0), // full before end of stay + ) + + forAll(cases) { (stayingTicks, storedEnergy) => + val ev = EvModelWrapper( + new MockEvModel( + UUID.randomUUID(), + "Test EV", + 5.0.asKiloWatt, // using AC charging here + 10.0.asKiloWatt, + 10.0.asKiloWattHour, + storedEnergy.asKiloWattHour, + offset + stayingTicks, + ) + ) + + val chargingMap = MaximumPowerCharging.determineChargingPowers( + Seq(ev), + offset, + MockEvcsChargingProperties, + ) + + chargingMap shouldBe Map( + ev.uuid -> ev.sRatedAc + ) + } + + } + + "work correctly with two evs" in { + val offset = 3600L + + val cases = Table( + ("stayingTicks", "storedEnergy"), + // empty battery + (3600L, 0.0), // stay shorter than full + (7200L, 0.0), // exactly full + (14400L, 0.0), // full before end of stay + // half full battery + (1800L, 5.0), // stay shorter than full + (3600L, 5.0), // exactly full + (14400L, 5.0), // full before end of stay + ) + + forAll(cases) { (stayingTicks, storedEnergy) => + val givenEv = EvModelWrapper( + new MockEvModel( + UUID.randomUUID(), + "First EV", + 5.0.asKiloWatt, // using AC charging here + 10.0.asKiloWatt, + 10.0.asKiloWattHour, + 5.0.asKiloWattHour, + offset + 3600L, + ) + ) + + val ev = EvModelWrapper( + new MockEvModel( + UUID.randomUUID(), + "Test EV", + 5.0.asKiloWatt, // using AC charging here + 10.0.asKiloWatt, + 10.0.asKiloWattHour, + storedEnergy.asKiloWattHour, + offset + stayingTicks, + ) + ) + + val chargingMap = MaximumPowerCharging.determineChargingPowers( + Seq(givenEv, ev), + offset, + MockEvcsChargingProperties, + ) + + chargingMap shouldBe Map( + givenEv.uuid -> Kilowatts(5.0), + ev.uuid -> Kilowatts(5.0), + ) + } + + } + } +} diff --git a/src/test/scala/edu/ie3/simona/model/participant2/evcs/MockEvcsChargingProperties.scala b/src/test/scala/edu/ie3/simona/model/participant2/evcs/MockEvcsChargingProperties.scala new file mode 100644 index 0000000000..25469a4cd0 --- /dev/null +++ b/src/test/scala/edu/ie3/simona/model/participant2/evcs/MockEvcsChargingProperties.scala @@ -0,0 +1,18 @@ +/* + * © 2024. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.model.participant2.evcs +import edu.ie3.datamodel.models.ElectricCurrentType +import squants.Power +import squants.energy.Kilowatts + +object MockEvcsChargingProperties extends EvcsChargingProperties { + + override val sRated: Power = Kilowatts(43) + override val currentType: ElectricCurrentType = ElectricCurrentType.AC + override val lowestEvSoc: Double = 0.2 + +} From 9070090041acc6df34df0b56f7b8af41dc463e53 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Fri, 15 Nov 2024 19:43:30 +0100 Subject: [PATCH 39/77] Some additions to EVCS Signed-off-by: Sebastian Peter --- .../evcs/ConstantPowerCharging.scala | 4 +- .../evcs/EvcsChargingStrategy.scala | 49 +++++++++++++ .../model/participant2/evcs/EvcsModel.scala | 68 +++++++++++++------ .../evcs/MaximumPowerCharging.scala | 4 +- 4 files changed, 99 insertions(+), 26 deletions(-) create mode 100644 src/main/scala/edu/ie3/simona/model/participant2/evcs/EvcsChargingStrategy.scala diff --git a/src/main/scala/edu/ie3/simona/model/participant2/evcs/ConstantPowerCharging.scala b/src/main/scala/edu/ie3/simona/model/participant2/evcs/ConstantPowerCharging.scala index 1873270527..decf1c1a5b 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/evcs/ConstantPowerCharging.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/evcs/ConstantPowerCharging.scala @@ -7,7 +7,6 @@ package edu.ie3.simona.model.participant2.evcs import edu.ie3.simona.model.participant.evcs.EvModelWrapper -import edu.ie3.simona.model.participant2.evcs.EvcsModel.ChargingStrategy import squants.{Power, Seconds} import java.util.UUID @@ -17,7 +16,7 @@ import java.util.UUID * If less than the maximum power is required to reach 100% SoC, the power is * reduced accordingly. */ -object ConstantPowerCharging extends ChargingStrategy { +object ConstantPowerCharging extends EvcsChargingStrategy { override def determineChargingPowers( evs: Iterable[EvModelWrapper], @@ -40,4 +39,5 @@ object ConstantPowerCharging extends ChargingStrategy { ev.uuid -> chargingPower } .toMap + } diff --git a/src/main/scala/edu/ie3/simona/model/participant2/evcs/EvcsChargingStrategy.scala b/src/main/scala/edu/ie3/simona/model/participant2/evcs/EvcsChargingStrategy.scala new file mode 100644 index 0000000000..12edcefd5b --- /dev/null +++ b/src/main/scala/edu/ie3/simona/model/participant2/evcs/EvcsChargingStrategy.scala @@ -0,0 +1,49 @@ +/* + * © 2024. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.model.participant2.evcs + +import edu.ie3.simona.exceptions.CriticalFailureException +import edu.ie3.simona.model.participant.evcs.EvModelWrapper +import squants.Power + +import java.util.UUID + +trait EvcsChargingStrategy { + + /** Determine scheduling for charging the EVs currently parked at the charging + * station until their departure. + * + * @param evs + * currently parked evs at the charging station + * @param currentTick + * current tick + * @param chargingProps + * interface that provides information on charging station + * @return + * scheduling for charging the EVs + */ + def determineChargingPowers( + evs: Iterable[EvModelWrapper], + currentTick: Long, + chargingProps: EvcsChargingProperties, + ): Map[UUID, Power] + +} + +object EvcsChargingStrategy { + + def apply(token: String): EvcsChargingStrategy = + "[-_]".r.replaceAllIn(token.trim.toLowerCase, "") match { + case "maxpower" => MaximumPowerCharging + case "constantpower" => ConstantPowerCharging + case unknown => + throw new CriticalFailureException( + s"The token '$unknown' cannot be parsed to charging strategy." + ) + } + +} diff --git a/src/main/scala/edu/ie3/simona/model/participant2/evcs/EvcsModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/evcs/EvcsModel.scala index 179edbb1fe..3d7c54e53b 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/evcs/EvcsModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/evcs/EvcsModel.scala @@ -7,6 +7,7 @@ package edu.ie3.simona.model.participant2.evcs import edu.ie3.datamodel.models.ElectricCurrentType +import edu.ie3.datamodel.models.input.system.EvcsInput import edu.ie3.datamodel.models.result.system.{ EvResult, EvcsResult, @@ -14,6 +15,9 @@ import edu.ie3.datamodel.models.result.system.{ } import edu.ie3.simona.agent.participant.data.Data import edu.ie3.simona.agent.participant.data.Data.PrimaryData +import edu.ie3.simona.agent.participant2.ParticipantAgent +import edu.ie3.simona.agent.participant2.ParticipantAgent.ParticipantRequest +import edu.ie3.simona.config.SimonaConfig.EvcsRuntimeConfig import edu.ie3.simona.exceptions.CriticalFailureException import edu.ie3.simona.model.participant.control.QControl import edu.ie3.simona.model.participant.evcs.EvModelWrapper @@ -25,7 +29,6 @@ import edu.ie3.simona.model.participant2.ParticipantModel.{ OperationRelevantData, } import edu.ie3.simona.model.participant2.evcs.EvcsModel.{ - ChargingStrategy, EvcsOperatingPoint, EvcsRelevantData, EvcsState, @@ -34,9 +37,11 @@ import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage import edu.ie3.simona.ontology.messages.flex.MinMaxFlexibilityMessage.ProvideMinMaxFlexOptions import edu.ie3.simona.ontology.messages.services.EvMessage.ArrivingEvs import edu.ie3.simona.service.ServiceType +import edu.ie3.util.quantities.PowerSystemUnits.KILOWATT import edu.ie3.util.quantities.QuantityUtils.RichQuantityDouble import edu.ie3.util.scala.quantities.DefaultQuantities._ import edu.ie3.util.scala.quantities.ReactivePower +import org.apache.pekko.actor.typed.scaladsl.ActorContext import squants.energy.{Kilowatts, Watts} import squants.time.Seconds import squants.{Dimensionless, Energy, Power} @@ -50,9 +55,10 @@ class EvcsModel private ( override val sRated: Power, override val cosPhiRated: Double, override val qControl: QControl, - strategy: ChargingStrategy, + strategy: EvcsChargingStrategy, override val currentType: ElectricCurrentType, override val lowestEvSoc: Double, + chargingPoints: Int, vehicle2grid: Boolean, ) extends ParticipantModel[ EvcsOperatingPoint, @@ -476,6 +482,27 @@ class EvcsModel private ( Math.round(timeUntilFullOrEmpty.toSeconds) } + /** Handling requests that are not part of the standard participant protocol + * + * @param state + * The current state + * @param ctx + * The actor context that can be used to send replies + * @param msg + * The received request + * @return + * An updated state, or the same state provided as parameter + */ + override def handleRequest( + state: EvcsState, + ctx: ActorContext[ParticipantAgent.Request], + msg: ParticipantRequest, + ): EvcsState = { + ??? // todo + } + + /* HELPER METHODS */ + /** @param ev * the ev whose stored energy is to be checked * @return @@ -544,25 +571,22 @@ object EvcsModel { arrivals: Seq[EvModelWrapper], ) extends OperationRelevantData - trait ChargingStrategy { - - /** Determine scheduling for charging the EVs currently parked at the - * charging station until their departure. - * - * @param evs - * currently parked evs at the charging station - * @param currentTick - * current tick - * @param chargingProps - * interface that provides information on charging station - * @return - * scheduling for charging the EVs - */ - def determineChargingPowers( - evs: Iterable[EvModelWrapper], - currentTick: Long, - chargingProps: EvcsChargingProperties, - ): Map[UUID, Power] - } + def apply( + inputModel: EvcsInput, + modelConfig: EvcsRuntimeConfig, + ): EvcsModel = + new EvcsModel( + inputModel.getUuid, + Kilowatts( + inputModel.getType.getsRated.to(KILOWATT).getValue.doubleValue + ), + inputModel.getCosPhiRated, + QControl(inputModel.getqCharacteristics), + EvcsChargingStrategy(modelConfig.chargingStrategy), + inputModel.getType.getElectricCurrentType, + modelConfig.lowestEvSoc, + inputModel.getChargingPoints, + inputModel.getV2gSupport, + ) } diff --git a/src/main/scala/edu/ie3/simona/model/participant2/evcs/MaximumPowerCharging.scala b/src/main/scala/edu/ie3/simona/model/participant2/evcs/MaximumPowerCharging.scala index 223838c22b..941617bda2 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/evcs/MaximumPowerCharging.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/evcs/MaximumPowerCharging.scala @@ -7,7 +7,6 @@ package edu.ie3.simona.model.participant2.evcs import edu.ie3.simona.model.participant.evcs.EvModelWrapper -import edu.ie3.simona.model.participant2.evcs.EvcsModel.ChargingStrategy import squants.Power import java.util.UUID @@ -16,7 +15,7 @@ import java.util.UUID * station by charging with maximum power from current time until it reaches * either 100% SoC or its departure time. */ -object MaximumPowerCharging extends ChargingStrategy { +object MaximumPowerCharging extends EvcsChargingStrategy { def determineChargingPowers( evs: Iterable[EvModelWrapper], @@ -28,4 +27,5 @@ object MaximumPowerCharging extends ChargingStrategy { ev.uuid -> chargingProps.getMaxAvailableChargingPower(ev) } .toMap + } From a615d438894516f2ecc9ad42de86f8818c7c2857 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Thu, 21 Nov 2024 19:07:21 +0100 Subject: [PATCH 40/77] Lots of fixes in EVCS Abstracting functionality from EVCS and storage Adapting/introducing tests Signed-off-by: Sebastian Peter --- .../model/participant2/ChargingHelper.scala | 77 ++ .../model/participant2/StorageModel.scala | 94 +- .../model/participant2/evcs/EvcsModel.scala | 128 ++- .../participant2/ChargingHelperSpec.scala | 72 ++ .../participant2/evcs/EvcsModelSpec.scala | 802 ++++++++++++++++++ 5 files changed, 1048 insertions(+), 125 deletions(-) create mode 100644 src/main/scala/edu/ie3/simona/model/participant2/ChargingHelper.scala create mode 100644 src/test/scala/edu/ie3/simona/model/participant2/ChargingHelperSpec.scala create mode 100644 src/test/scala/edu/ie3/simona/model/participant2/evcs/EvcsModelSpec.scala diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ChargingHelper.scala b/src/main/scala/edu/ie3/simona/model/participant2/ChargingHelper.scala new file mode 100644 index 0000000000..d13da64a46 --- /dev/null +++ b/src/main/scala/edu/ie3/simona/model/participant2/ChargingHelper.scala @@ -0,0 +1,77 @@ +/* + * © 2024. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.model.participant2 + +import edu.ie3.util.scala.quantities.DefaultQuantities.{zeroKW, zeroKWh} +import squants.{Dimensionless, Each, Energy, Power, Seconds} + +object ChargingHelper { + + def calcEnergy( + storedEnergy: Energy, + power: Power, + startTick: Long, + endTick: Long, + maxEnergy: Energy, + minEnergy: Energy = zeroKWh, + eta: Dimensionless = Each(1), + ): Energy = { + val timespan = Seconds(endTick - startTick) + val energyChange = calcNetPower(power, eta) * timespan + + // don't allow under- or overcharge e.g. due to tick rounding error + minEnergy.max(maxEnergy.min(storedEnergy + energyChange)) + } + + def calcNextEventTick( + storedEnergy: Energy, + power: Power, + currentTick: Long, + chargingEnergyTarget: () => Energy, + dischargingEnergyTarget: () => Energy, + eta: Dimensionless = Each(1), + )(implicit tolerance: Power): Option[Long] = { + val netPower = calcNetPower(power, eta) + + val maybeTimeSpan = + if (netPower ~= zeroKW) { + // we're at 0 kW, do nothing + None + } else if (netPower > zeroKW) { + val energyToFull = chargingEnergyTarget() - storedEnergy + Some(energyToFull / netPower) + } else { + val energyToEmpty = storedEnergy - dischargingEnergyTarget() + Some(energyToEmpty / (netPower * -1)) + } + + // calculate the tick from time span + maybeTimeSpan.map { timeSpan => + val timeSpanTicks = Math.round(timeSpan.toSeconds) + currentTick + timeSpanTicks + } + } + + /** Calculate net power (after considering efficiency eta). + * + * @param setPower + * The gross power + * @param eta + * The efficiency + * @return + * The net power + */ + private def calcNetPower(setPower: Power, eta: Dimensionless): Power = + if (setPower > zeroKW) { + // multiply eta if we're charging + setPower * eta.toEach + } else { + // divide by eta if we're discharging + // (draining the battery more than we get as output) + setPower / eta.toEach + } +} diff --git a/src/main/scala/edu/ie3/simona/model/participant2/StorageModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/StorageModel.scala index 4698e435db..32c17ec66c 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/StorageModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/StorageModel.scala @@ -121,13 +121,15 @@ class StorageModel private ( operatingPoint: ActivePowerOperatingPoint, currentTick: Long, ): StorageState = { - val timespan = Seconds(currentTick - lastState.tick) - val netPower = calcNetPower(operatingPoint.activePower) - val energyChange = netPower * timespan - - // don't allow under- or overcharge e.g. due to tick rounding error - val currentEnergy = - minEnergy.max(eStorage.min(lastState.storedEnergy + energyChange)) + val currentEnergy = ChargingHelper.calcEnergy( + lastState.storedEnergy, + operatingPoint.activePower, + lastState.tick, + currentTick, + eStorage, + minEnergy, + eta, + ) StorageState(currentEnergy, currentTick) } @@ -158,7 +160,7 @@ class StorageModel private ( uuid, data.p.toMegawatts.asMegaWatt, data.q.toMegavars.asMegaVar, - (-1).asPu, // FIXME currently not supported + -1.asPu, // FIXME currently not supported ) override def getRequiredSecondaryServices: Iterable[ServiceType] = @@ -199,7 +201,7 @@ class StorageModel private ( } } else { // above target + margin, discharge to target - pMax * (-1d) + pMax * -1d } } .getOrElse { @@ -210,7 +212,7 @@ class StorageModel private ( ProvideMinMaxFlexOptions( uuid, refPower, - if (dischargingPossible) pMax * (-1) else zeroKW, + if (dischargingPossible) pMax * -1 else zeroKW, if (chargingPossible) pMax else zeroKW, ) } @@ -234,9 +236,6 @@ class StorageModel private ( else setPower - // net power after considering efficiency - val netPower = calcNetPower(adaptedSetPower) - // if the storage is at minimum or maximum charged energy AND we are charging // or discharging, flex options will be different at the next activation val isEmptyOrFull = @@ -246,10 +245,10 @@ class StorageModel private ( state.storedEnergy <= targetParams.targetWithPosMargin && state.storedEnergy >= targetParams.targetWithNegMargin } - val isChargingOrDischarging = netPower != zeroKW + val isChargingOrDischarging = adaptedSetPower != zeroKW // 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 + // we already set the power to zero (see above) and also want to refresh flex options // at the next activation. // Similarly, if the ref target margin area is hit before hitting target SOC, we want // to refresh flex options. @@ -261,44 +260,29 @@ class StorageModel private ( val activateAtNextTick = ((isEmptyOrFull || isAtTarget) && isChargingOrDischarging) || hasObsoleteFlexOptions - // calculate the time span until we're full or empty, if applicable - val maybeTimeSpan = - if (!isChargingOrDischarging) { - // we're at 0 kW, do nothing - None - } else if (netPower > zeroKW) { - // we're charging, calculate time until we're full or at target energy - - val closestEnergyTarget = refTargetSoc - .flatMap { targetParams => - Option.when( - state.storedEnergy <= targetParams.targetWithNegMargin - )(targetParams.targetSoc) - } - .getOrElse(eStorage) - - val energyToFull = closestEnergyTarget - state.storedEnergy - Some(energyToFull / netPower) - } else { - // we're discharging, calculate time until we're at lowest energy allowed or at target energy - - val closestEnergyTarget = refTargetSoc - .flatMap { targetParams => - Option.when( - state.storedEnergy >= targetParams.targetWithPosMargin - )(targetParams.targetSoc) - } - .getOrElse(minEnergy) + // when charging, calculate time until we're full or at target energy + val chargingEnergyTarget = () => + refTargetSoc + .filter(_.targetWithNegMargin >= state.storedEnergy) + .map(_.targetSoc) + .getOrElse(eStorage) - val energyToEmpty = state.storedEnergy - closestEnergyTarget - Some(energyToEmpty / (netPower * (-1))) - } + // when discharging, calculate time until we're at lowest energy allowed or at target energy + val dischargingEnergyTarget = () => + refTargetSoc + .filter(_.targetWithPosMargin <= state.storedEnergy) + .map(_.targetSoc) + .getOrElse(minEnergy) // calculate the tick from time span - val maybeNextTick = maybeTimeSpan.map { timeSpan => - val timeSpanTicks = Math.round(timeSpan.toSeconds) - state.tick + timeSpanTicks - } + val maybeNextTick = ChargingHelper.calcNextEventTick( + state.storedEnergy, + adaptedSetPower, + state.tick, + chargingEnergyTarget, + dischargingEnergyTarget, + eta, + ) ( ActivePowerOperatingPoint(adaptedSetPower), @@ -306,16 +290,6 @@ class StorageModel private ( ) } - private def calcNetPower(setPower: Power): Power = - if (setPower > zeroKW) { - // multiply eta if we're charging - setPower * eta.toEach - } else { - // divide by eta if we're discharging - // (draining the battery more than we get as output) - setPower / eta.toEach - } - /** @param storedEnergy * the stored energy amount to check * @return diff --git a/src/main/scala/edu/ie3/simona/model/participant2/evcs/EvcsModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/evcs/EvcsModel.scala index 3d7c54e53b..c8b516570e 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/evcs/EvcsModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/evcs/EvcsModel.scala @@ -21,7 +21,7 @@ import edu.ie3.simona.config.SimonaConfig.EvcsRuntimeConfig import edu.ie3.simona.exceptions.CriticalFailureException import edu.ie3.simona.model.participant.control.QControl import edu.ie3.simona.model.participant.evcs.EvModelWrapper -import edu.ie3.simona.model.participant2.ParticipantModel +import edu.ie3.simona.model.participant2.{ChargingHelper, ParticipantModel} import edu.ie3.simona.model.participant2.ParticipantModel.{ ModelChangeIndicator, ModelState, @@ -79,7 +79,11 @@ class EvcsModel private ( chargingPowers.get(ev.uuid).map((ev, _)) } .flatMap { case (ev, power) => - determineNextEvent(ev, power) + determineNextEvent( + ev, + power, + relevantData.tick, + ) } .minOption @@ -89,32 +93,6 @@ class EvcsModel private ( override def zeroPowerOperatingPoint: EvcsOperatingPoint = EvcsOperatingPoint.zero - private def determineNextEvent( - ev: EvModelWrapper, - chargingPower: Power, - ): Option[Long] = { - implicit val tolerance: Power = Watts(1e-3) - if (chargingPower ~= zeroKW) - None - else { - val timeUntilFullOrEmpty = - if (chargingPower > zeroKW) { - - // if we're below lowest SOC, flex options will change at that point - val targetEnergy = - if (isEmpty(ev) && !isInLowerMargin(ev)) - ev.eStorage * lowestEvSoc - else - ev.eStorage - - (targetEnergy - ev.storedEnergy) / chargingPower - } else - (ev.storedEnergy - (ev.eStorage * lowestEvSoc)) / (chargingPower * (-1)) - - Some(Math.round(timeUntilFullOrEmpty.toSeconds)) - } - } - override def determineState( lastState: EvcsState, operatingPoint: EvcsOperatingPoint, @@ -125,11 +103,15 @@ class EvcsModel private ( operatingPoint.evOperatingPoints .get(ev.uuid) .map { chargingPower => - val newStoredEnergy = ev.storedEnergy + - chargingPower * Seconds( - currentTick - lastState.tick - ) - ev.copy(storedEnergy = newStoredEnergy) + val currentEnergy = ChargingHelper.calcEnergy( + ev.storedEnergy, + chargingPower, + lastState.tick, + currentTick, + ev.eStorage, + ) + + ev.copy(storedEnergy = currentEnergy) } .getOrElse(ev) } @@ -177,14 +159,24 @@ class EvcsModel private ( } } - val evcsResult = new EvcsResult( - dateTime, - uuid, - complexPower.p.toMegawatts.asMegaWatt, - complexPower.q.toMegavars.asMegaVar, + val powerDifferent = lastOperatingPoint.forall( + _.activePower != complexPower.p ) - evResults ++ Iterable(evcsResult) + val evcsResult = + if (powerDifferent) + Iterable( + new EvcsResult( + dateTime, + uuid, + complexPower.p.toMegawatts.asMegaWatt, + complexPower.q.toMegavars.asMegaVar, + ) + ) + else + Iterable.empty + + evResults ++ evcsResult } override def createPrimaryDataResult( @@ -238,7 +230,7 @@ class EvcsModel private ( ) => val maxPower = getMaxAvailableChargingPower(ev) - val preferredPower = preferredPowers.get(uuid) + val preferredPower = preferredPowers.get(ev.uuid) val maxCharging = if (!isFull(ev)) @@ -378,8 +370,9 @@ class EvcsModel private ( // end of recursion, rest of charging power fits to all val results = fittingPowerEvs.map { ev => - val chargingTicks = calcFlexOptionsChange(ev, proposedPower) - val endTick = Math.min(currentTick + chargingTicks, ev.departureTick) + val endTick = determineNextEvent(ev, proposedPower, currentTick) + .map(math.min(_, ev.departureTick)) + .getOrElse(ev.departureTick) ( ev.uuid, @@ -405,10 +398,11 @@ class EvcsModel private ( if (setPower > zeroKW) maxPower else - maxPower * (-1) + maxPower * -1 - val chargingTicks = calcFlexOptionsChange(ev, power) - val endTick = Math.min(currentTick + chargingTicks, ev.departureTick) + val endTick = determineNextEvent(ev, power, currentTick) + .map(math.min(_, ev.departureTick)) + .getOrElse(ev.departureTick) (ev, power, endTick) } @@ -447,39 +441,43 @@ class EvcsModel private ( (combinedResults, remainingAfterRecursion) } - } - /** Calculates the duration (in ticks) until the flex options will change - * next, which could be the battery being fully charged or discharged or the - * minimum SOC requirement being reached + /** Calculates the tick at which the target energy (e.g. full on charging or + * empty on discharging) is reached. * * @param ev * The EV to charge/discharge * @param power * The charging/discharging power + * @param currentTick + * The current simulation tick * @return - * The tick at which flex options will change + * The tick wat which the target is reached */ - private def calcFlexOptionsChange( + private def determineNextEvent( ev: EvModelWrapper, power: Power, - ): Long = { - val timeUntilFullOrEmpty = - if (power > zeroKW) { - - // if we're below lowest SOC, flex options will change at that point - val targetEnergy = - if (isEmpty(ev) && !isInLowerMargin(ev)) - ev.eStorage * lowestEvSoc - else - ev.eStorage + currentTick: Long, + ): Option[Long] = { + // TODO adapt like in StorageModel: dependent tolerance + implicit val tolerance: Power = Watts(1e-3) - (targetEnergy - ev.storedEnergy) / power - } else - (ev.storedEnergy - (ev.eStorage * lowestEvSoc)) / (power * (-1)) + val chargingEnergyTarget = () => + if (isEmpty(ev) && !isInLowerMargin(ev)) + ev.eStorage * lowestEvSoc + else + ev.eStorage + + val dischargingEnergyTarget = () => ev.eStorage * lowestEvSoc - Math.round(timeUntilFullOrEmpty.toSeconds) + ChargingHelper.calcNextEventTick( + ev.storedEnergy, + power, + currentTick, + chargingEnergyTarget, + dischargingEnergyTarget, + ) } /** Handling requests that are not part of the standard participant protocol diff --git a/src/test/scala/edu/ie3/simona/model/participant2/ChargingHelperSpec.scala b/src/test/scala/edu/ie3/simona/model/participant2/ChargingHelperSpec.scala new file mode 100644 index 0000000000..55b3033e30 --- /dev/null +++ b/src/test/scala/edu/ie3/simona/model/participant2/ChargingHelperSpec.scala @@ -0,0 +1,72 @@ +/* + * © 2024. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.model.participant2 + +import edu.ie3.simona.test.common.UnitSpec +import squants.energy.{KilowattHours, Kilowatts} + +class ChargingHelperSpec extends UnitSpec { + + private implicit val energyTolerance: squants.Energy = KilowattHours(1e-10) + + "A ChargingHelper" should { + + "calculate charged energy correctly" in { + + val cases = Table( + ( + "storedEnergy", + "lastStateTick", + "currentTick", + "power", + "expectedStored", + ), + // empty battery + (0.0, 0L, 3600L, 5.0, 5.0), + (0.0, 0L, 3600L, 2.5, 2.5), + (0.0, 0L, 1800L, 5.0, 2.5), + (0.0, 900L, 2700L, 5.0, 2.5), + (0.0, 0L, 3600L, -5.0, 0.0), + // half full battery + (5.0, 0L, 3600L, 5.0, 10.0), + (5.0, 0L, 3600L, 2.5, 7.5), + (5.0, 0L, 1800L, 5.0, 7.5), + (5.0, 900L, 2700L, 5.0, 7.5), + (5.0, 0L, 3600L, -5.0, 0.0), + (5.0, 0L, 3600L, -2.5, 2.5), + (5.0, 0L, 1800L, -5.0, 2.5), + (5.0, 900L, 2700L, -5.0, 2.5), + // full battery + (10.0, 0L, 3600L, -5.0, 5.0), + (10.0, 0L, 3600L, -2.5, 7.5), + (10.0, 0L, 1800L, -5.0, 7.5), + (10.0, 900L, 2700L, -5.0, 7.5), + (10.0, 0L, 3600L, 5.0, 10.0), + ) + + forAll(cases) { + ( + storedEnergy, + startTick, + endTick, + power, + expectedEnergy, + ) => + val newEnergy = ChargingHelper.calcEnergy( + KilowattHours(storedEnergy), + Kilowatts(power), + startTick, + endTick, + KilowattHours(10.0), + ) + + newEnergy should approximate(KilowattHours(expectedEnergy)) + } + + } + } +} diff --git a/src/test/scala/edu/ie3/simona/model/participant2/evcs/EvcsModelSpec.scala b/src/test/scala/edu/ie3/simona/model/participant2/evcs/EvcsModelSpec.scala new file mode 100644 index 0000000000..4cf40adceb --- /dev/null +++ b/src/test/scala/edu/ie3/simona/model/participant2/evcs/EvcsModelSpec.scala @@ -0,0 +1,802 @@ +/* + * © 2022. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.model.participant2.evcs + +import edu.ie3.datamodel.models.result.system.{EvResult, EvcsResult} +import edu.ie3.simona.agent.participant.data.Data.PrimaryData.ApparentPower +import edu.ie3.simona.config.SimonaConfig.EvcsRuntimeConfig +import edu.ie3.simona.model.participant.evcs.EvModelWrapper +import edu.ie3.simona.model.participant2.ParticipantModel.ModelChangeIndicator +import edu.ie3.simona.model.participant2.evcs.EvcsModel.{ + EvcsOperatingPoint, + EvcsRelevantData, + EvcsState, +} +import edu.ie3.simona.ontology.messages.flex.MinMaxFlexibilityMessage.ProvideMinMaxFlexOptions +import edu.ie3.simona.test.common.UnitSpec +import edu.ie3.simona.test.common.input.EvcsInputTestData +import edu.ie3.simona.test.common.model.MockEvModel +import edu.ie3.simona.test.helper.TableDrivenHelper +import edu.ie3.util.TimeUtil +import edu.ie3.util.quantities.QuantityUtils.RichQuantityDouble +import edu.ie3.util.scala.quantities.DefaultQuantities.zeroKW +import edu.ie3.util.scala.quantities.Kilovars +import squants.energy.{KilowattHours, Kilowatts} + +import java.time.ZonedDateTime +import java.util.UUID + +class EvcsModelSpec + extends UnitSpec + with TableDrivenHelper + with EvcsInputTestData { + + private implicit val energyTolerance: squants.Energy = KilowattHours(1e-10) + private implicit val powerTolerance: squants.Power = Kilowatts(1e-10) + + private val resultDateTime: ZonedDateTime = + TimeUtil.withDefaults.toZonedDateTime("2020-01-02T03:04:05Z") + + private def createModel( + chargingStrategy: String, + vehicle2Grid: Boolean = true, + ): EvcsModel = + EvcsModel( + evcsInputModel.copy().v2gSupport(vehicle2Grid).build(), + EvcsRuntimeConfig( + calculateMissingReactivePowerWithModel = false, + scaling = 1.0, + uuids = List.empty, + chargingStrategy = chargingStrategy, + lowestEvSoc = 0.2, + ), + ) + + "An EVCS model" should { + + "calculate new schedules correctly" when { + + "configured with max power charging" in { + val evcsModel = createModel("maxpower") + + val evModel = EvModelWrapper( + new MockEvModel( + UUID.randomUUID(), + "Mock EV", + 10.0.asKiloWatt, // AC is relevant, + 20.0.asKiloWatt, // DC is not + 20.0.asKiloWattHour, + 5.0.asKiloWattHour, + 10800L, + ) + ) + + val (operatingPoint, nextEvent) = evcsModel.determineOperatingPoint( + EvcsState( + Seq(evModel), + 3600L, + ), + EvcsRelevantData( + 3600L, + Seq.empty, + ), + ) + + operatingPoint.evOperatingPoints shouldBe Map( + evModel.uuid -> + // ending early at 9000 because of max power charging + Kilowatts(10.0) + ) + + nextEvent shouldBe Some(9000L) + } + + "configured with constant power charging" in { + val evcsModel = createModel("constantpower") + + val evModel = EvModelWrapper( + new MockEvModel( + UUID.randomUUID(), + "Mock EV", + 10.0.asKiloWatt, // AC is relevant, + 20.0.asKiloWatt, // DC is not + 20.0.asKiloWattHour, + 15.0.asKiloWattHour, + 10800L, + ) + ) + + val (operatingPoint, nextEvent) = evcsModel.determineOperatingPoint( + EvcsState( + Seq(evModel), + 3600L, + ), + EvcsRelevantData( + 3600L, + Seq.empty, + ), + ) + + operatingPoint.evOperatingPoints shouldBe Map( + evModel.uuid -> + // using 2.5 kW with constant power charging + Kilowatts(2.5) + ) + nextEvent shouldBe Some(10800L) + } + } + + "determining current state correctly" when { + + "being provided with a ChargingSchedule consisting of one entry" in { + val evcsModel = createModel("constantpower") + + val cases = Table( + ( + "storedEnergy", + "lastStateTick", + "currentTick", + "power", + "expectedStored", + ), + // empty battery + (0.0, 900L, 2700L, 5.0, 2.5), + (0.0, 0L, 3600L, -5.0, 0.0), + // half full battery + (5.0, 0L, 3600L, 5.0, 10.0), + (5.0, 900L, 2700L, 5.0, 7.5), + (5.0, 0L, 3600L, -5.0, 0.0), + (5.0, 900L, 2700L, -5.0, 2.5), + // full battery + (10.0, 900L, 2700L, -5.0, 7.5), + (10.0, 0L, 3600L, 5.0, 10.0), + ) + + forAll(cases) { + ( + storedEnergy, + lastStateTick, + currentTick, + power, + expectedStored, + ) => + val ev = EvModelWrapper( + new MockEvModel( + UUID.randomUUID(), + "TestEv1", + 5.0.asKiloWatt, // using AC charging here + 10.0.asKiloWatt, + 10.0.asKiloWattHour, + storedEnergy.asKiloWattHour, + 7200L, // is ignored here + ) + ) + + val state = EvcsState( + Seq(ev), + lastStateTick, + ) + + val operatingPoint = EvcsOperatingPoint( + Map(ev.uuid -> Kilowatts(power)) + ) + + val newState = evcsModel.determineState( + state, + operatingPoint, + currentTick, + ) + + newState.evs should have size 1 + newState.tick shouldBe currentTick + + val actualEv = newState.evs.headOption.getOrElse( + fail("No charging schedule provided.") + ) + + actualEv.uuid shouldBe ev.uuid + actualEv.id shouldBe ev.id + actualEv.sRatedAc shouldBe ev.sRatedAc + actualEv.sRatedDc shouldBe ev.sRatedDc + actualEv.eStorage shouldBe ev.eStorage + actualEv.storedEnergy should approximate( + KilowattHours(expectedStored) + ) + actualEv.departureTick shouldBe ev.departureTick + + } + + } + } + + "calculate results correctly" when { + val evcsModel = createModel("constantpower") + + val ev1 = EvModelWrapper( + new MockEvModel( + UUID.randomUUID(), + "TestEv1", + 5.0.asKiloWatt, + 10.0.asKiloWatt, + 10.0.asKiloWattHour, + 5.0.asKiloWattHour, + 18000L, + ) + ) + + val ev2 = EvModelWrapper( + new MockEvModel( + UUID.randomUUID(), + "TestEv2", + 5.0.asKiloWatt, + 10.0.asKiloWatt, + 10.0.asKiloWattHour, + 7.5.asKiloWattHour, + 18000L, + ) + ) + + "two EVs are parked and charging without last operating point" in { + + val currentOperatingPoint = EvcsOperatingPoint( + Map(ev1.uuid -> Kilowatts(3.0), ev2.uuid -> Kilowatts(2.0)) + ) + + val state = EvcsState( + Seq(ev1, ev2), + 10800L, + ) + + val results = evcsModel.createResults( + state, + None, + currentOperatingPoint, + ApparentPower(Kilowatts(5), Kilovars(0.5)), + resultDateTime, + ) + + results should have size 3 + + results + .find(_.getInputModel == ev1.uuid) + .getOrElse(fail(s"No results for EV ${ev1.uuid}.")) match { + case evResult: EvResult => + evResult.getTime shouldBe resultDateTime + evResult.getP should beEquivalentTo(3.0.asKiloWatt) + evResult.getQ should beEquivalentTo(0.3.asKiloVar) + evResult.getSoc should beEquivalentTo(50.0.asPercent) + case unexpected => + fail(s"Unexpected result $unexpected was found.") + } + + results + .find(_.getInputModel == ev2.uuid) + .getOrElse(fail(s"No results for EV ${ev2.uuid}.")) match { + case evResult: EvResult => + evResult.getTime shouldBe resultDateTime + evResult.getP should beEquivalentTo(2.0.asKiloWatt) + evResult.getQ should beEquivalentTo(0.2.asKiloVar) + evResult.getSoc should beEquivalentTo(75.0.asPercent) + case unexpected => + fail(s"Unexpected result $unexpected was found.") + } + + results + .find(_.getInputModel == evcsModel.uuid) + .getOrElse(fail(s"No results for EVCS.")) match { + case evcsResult: EvcsResult => + evcsResult.getTime shouldBe resultDateTime + evcsResult.getP should beEquivalentTo(5.0.asKiloWatt) + evcsResult.getQ should beEquivalentTo(0.5.asKiloVar) + case unexpected => + fail(s"Unexpected result $unexpected was found.") + } + + } + + "two EVs are parked and charging with given last operating point" in { + + val lastOperatingPoint = EvcsOperatingPoint( + Map(ev1.uuid -> Kilowatts(3.0), ev2.uuid -> Kilowatts(2.0)) + ) + + val state = EvcsState( + Seq(ev1, ev2), + 10800L, + ) + + val cases = Table( + ("ev1P", "ev2P", "ev1Res", "ev2Res", "evcsRes"), + (4.0, 3.0, true, true, true), + (4.0, 1.0, true, true, false), + (3.0, 1.0, false, true, true), + (3.0, 2.0, false, false, false), + ) + + forAll(cases) { (ev1P, ev2P, ev1Res, ev2Res, evcsRes) => + val evcsP = ev1P + ev2P + val evcsQ = evcsP / 10 + + val currentOperatingPoint = EvcsOperatingPoint( + Map(ev1.uuid -> Kilowatts(ev1P), ev2.uuid -> Kilowatts(ev2P)) + ) + + val results = evcsModel.createResults( + state, + Some(lastOperatingPoint), + currentOperatingPoint, + ApparentPower(Kilowatts(evcsP), Kilovars(evcsQ)), + resultDateTime, + ) + + val expectedResults = Iterable(ev1Res, ev2Res, evcsRes).map { + if (_) 1 else 0 + }.sum + + results should have size expectedResults + + val actualEv1Result = results.find(_.getInputModel == ev1.uuid) + actualEv1Result.isDefined shouldBe ev1Res + actualEv1Result.foreach { + case evResult: EvResult => + evResult.getTime shouldBe resultDateTime + evResult.getP should beEquivalentTo(ev1P.asKiloWatt) + evResult.getQ should beEquivalentTo((ev1P / 10).asKiloVar) + evResult.getSoc should beEquivalentTo(50.0.asPercent) + case unexpected => + fail(s"Unexpected result $unexpected was found.") + } + + val actualEv2Result = results.find(_.getInputModel == ev2.uuid) + actualEv2Result.isDefined shouldBe ev2Res + actualEv2Result.foreach { + case evResult: EvResult => + evResult.getTime shouldBe resultDateTime + evResult.getP should beEquivalentTo(ev2P.asKiloWatt) + evResult.getQ should beEquivalentTo((ev2P / 10).asKiloVar) + evResult.getSoc should beEquivalentTo(75.0.asPercent) + case unexpected => + fail(s"Unexpected result $unexpected was found.") + } + + val actualEvcsResult = results.find(_.getInputModel == evcsModel.uuid) + actualEvcsResult.isDefined shouldBe evcsRes + actualEvcsResult.foreach { + case evcsResult: EvcsResult => + evcsResult.getTime shouldBe resultDateTime + evcsResult.getP should beEquivalentTo(evcsP.asKiloWatt) + evcsResult.getQ should beEquivalentTo(evcsQ.asKiloVar) + case unexpected => + fail(s"Unexpected result $unexpected was found.") + } + } + } + + } + + "calculate flex options correctly" when { + + "charging with constant power and allowing v2g" in { + val evcsModel = createModel("constantpower") + + val currentTick = 7200L + + val data = EvcsRelevantData( + currentTick, + Seq.empty, + ) + + val cases = Table( + ( + "stored1", + "stored2", + "expectedPRef", + "expectedPMin", + "expectedPMax", + ), + + /* 1: empty */ + // 2: empty + (0.0, 0.0, 15.0, 15.0, 15.0), + // 2: at lower margin + (0.0, 3.0, 15.0, 10.0, 15.0), + // 2: mid-way full, forced charging + (0.0, 7.5, 13.75, 10.0, 15.0), + // 2: almost full, forced charging + (0.0, 12.5, 11.25, 10.0, 15.0), + // 2: full, forced charging + (0.0, 15.0, 10.0, 10.0, 10.0), + + /* 1: at lower margin (set to 2 kWh) */ + // 2: empty + (2.0, 0.0, 13.0, 5.0, 15.0), + // 2: at lower margin + (2.0, 3.0, 13.0, 0.0, 15.0), + // 2: mid-way full (set to 7.5 kWh) + (2.0, 7.5, 11.75, -5.0, 15.0), + // 2: almost full + (2.0, 12.5, 9.25, -5.0, 15.0), + // 2: full + (2.0, 15.0, 8.0, -5.0, 10.0), + + /* 1: mid-way full (set to 5 kWh) */ + // 2: empty, forced charging + (5.0, 0.0, 10.0, 5.0, 15.0), + // 2: mid-way full (set to 7.5 kWh) + (5.0, 7.5, 8.75, -15.0, 15.0), + // 2: almost full + (5.0, 12.5, 6.25, -15.0, 15.0), + // 2: full + (5.0, 15.0, 5.0, -15.0, 10.0), + + /* 1: full (set to 10 kWh) */ + // 2: empty, forced charging + (10.0, 0.0, 5.0, 5.0, 5.0), + // 2: mid-way full + (10.0, 7.5, 3.75, -15.0, 5.0), + // 2: almost full + (10.0, 12.5, 1.25, -15.0, 5.0), + // 2: full + (10.0, 15.0, 0.0, -15.0, 0.0), + ) + + forAll(cases) { + ( + stored1, + stored2, + expectedPRef, + expectedPMin, + expectedPMax, + ) => + // stays one more hour + val ev1 = EvModelWrapper( + new MockEvModel( + UUID.randomUUID(), + "Mock EV 1", + 10.0.asKiloWatt, // AC is relevant, + 20.0.asKiloWatt, // DC is not + 10.0.asKiloWattHour, + stored1.asKiloWattHour, + 10800L, + ) + ) + + // stays two more hours + val ev2 = EvModelWrapper( + new MockEvModel( + UUID.randomUUID(), + "Mock EV 2", + 5.0.asKiloWatt, // AC is relevant, + 10.0.asKiloWatt, // DC is not + 15.0.asKiloWattHour, + stored2.asKiloWattHour, + 14400L, + ) + ) + + evcsModel.calcFlexOptions( + EvcsState( + Seq(ev1, ev2), + currentTick, + ), + data, + ) match { + case ProvideMinMaxFlexOptions( + modelUuid, + refPower, + minPower, + maxPower, + ) => + modelUuid shouldBe evcsModel.uuid + refPower should approximate(Kilowatts(expectedPRef)) + minPower should approximate(Kilowatts(expectedPMin)) + maxPower should approximate(Kilowatts(expectedPMax)) + } + } + + } + + "charging with maximum power and allowing v2g" in { + val evcsModel = createModel("maxpower") + + val currentTick = 7200L + + val data = EvcsRelevantData( + currentTick, + Seq.empty, + ) + + val cases = Table( + ( + "stored1", + "stored2", + "expectedPRef", + "expectedPMin", + "expectedPMax", + ), + + /* 1: empty */ + // 2: empty + (0.0, 0.0, 15.0, 15.0, 15.0), + // 2: at lower margin + (0.0, 3.0, 15.0, 10.0, 15.0), + // 2: mid-way full, forced charging + (0.0, 7.5, 15.0, 10.0, 15.0), + // 2: almost full, forced charging + (0.0, 12.5, 15.0, 10.0, 15.0), + // 2: full + (0.0, 15.0, 10.0, 10.0, 10.0), + + /* 1: at lower margin (set to 2 kWh) */ + // 2: empty + (2.0, 0.0, 15.0, 5.0, 15.0), + // 2: at lower margin + (2.0, 3.0, 15.0, 0.0, 15.0), + // 2: mid-way full + (2.0, 7.5, 15.0, -5.0, 15.0), + // 2: almost full + (2.0, 12.5, 15.0, -5.0, 15.0), + // 2: full + (2.0, 15.0, 10.0, -5.0, 10.0), + + /* 1: mid-way full (set to 5 kWh) */ + // 2: empty, forced charging + (5.0, 0.0, 15.0, 5.0, 15.0), + // 2: mid-way full + (5.0, 7.5, 15.0, -15.0, 15.0), + // 2: almost full + (5.0, 12.5, 15.0, -15.0, 15.0), + // 2: full + (5.0, 15.0, 10.0, -15.0, 10.0), + + /* 1: full (set to 10 kWh) */ + // 2: empty, forced charging + (10.0, 0.0, 5.0, 5.0, 5.0), + // 2: mid-way full + (10.0, 7.5, 5.0, -15.0, 5.0), + // 2: almost full + (10.0, 12.5, 5.0, -15.0, 5.0), + // 2: full + (10.0, 15.0, 0.0, -15.0, 0.0), + ) + + forAll(cases) { + ( + stored1, + stored2, + expectedPRef, + expectedPMin, + expectedPMax, + ) => + val ev1 = EvModelWrapper( + new MockEvModel( + UUID.randomUUID(), + "Mock EV 1", + 10.0.asKiloWatt, // AC is relevant, + 20.0.asKiloWatt, // DC is not + 10.0.asKiloWattHour, + stored1.asKiloWattHour, + 10800L, + ) + ) + + val ev2 = EvModelWrapper( + new MockEvModel( + UUID.randomUUID(), + "Mock EV 2", + 5.0.asKiloWatt, // AC is relevant, + 10.0.asKiloWatt, // DC is not + 15.0.asKiloWattHour, + stored2.asKiloWattHour, + 10800L, + ) + ) + + evcsModel.calcFlexOptions( + EvcsState( + Seq(ev1, ev2), + currentTick, + ), + data, + ) match { + case ProvideMinMaxFlexOptions( + modelUuid, + refPower, + minPower, + maxPower, + ) => + modelUuid shouldBe evcsModel.uuid + refPower should approximate(Kilowatts(expectedPRef)) + minPower should approximate(Kilowatts(expectedPMin)) + maxPower should approximate(Kilowatts(expectedPMax)) + } + } + + } + + "disallowing v2g" in { + val evcsModel = createModel("constantpower", vehicle2Grid = false) + + val currentTick = 7200L + + val data = EvcsRelevantData( + currentTick, + Seq.empty, + ) + + val ev1 = EvModelWrapper( + new MockEvModel( + UUID.randomUUID(), + "Mock EV 1", + 10.0.asKiloWatt, // AC is relevant, + 20.0.asKiloWatt, // DC is not + 10.0.asKiloWattHour, + 5.0.asKiloWattHour, + 10800L, + ) + ) + + evcsModel.calcFlexOptions( + EvcsState( + Seq(ev1), + currentTick, + ), + data, + ) match { + case ProvideMinMaxFlexOptions( + modelUuid, + refPower, + minPower, + maxPower, + ) => + modelUuid shouldBe evcsModel.uuid + refPower should approximate(Kilowatts(5.0)) // one hour left + minPower should approximate(Kilowatts(0d)) // no v2g allowed! + maxPower should approximate(ev1.sRatedAc) + } + + } + + } + + "handle power control correctly" when { + val evcsModel = createModel("constantpower") + + "dealing with two evs" in { + val currentTick = 3600L + + val data = EvcsRelevantData( + currentTick, + Seq.empty, + ) + + // not used + val mockFlexOptions = + ProvideMinMaxFlexOptions.noFlexOption(evcsModel.uuid, zeroKW) + + val cases = Table( + ( + "stored1", + "stored2", + "setPower", + "expPower1", + "expPower2", + "expNextActivation", + "expNextTick", + ), + + /* setPower is 0 kWh */ + (0.0, 0.0, 0.0, N, N, false, N), + (10.0, 5.0, 0.0, N, N, false, N), + (5.0, 15.0, 0.0, N, N, false, N), + (10.0, 15.0, 0.0, N, N, false, N), + + /* setPower is positive (charging) */ + (0.0, 0.0, 4.0, S(2.0), S(2.0), true, S(7200L)), + (5.0, 0.0, 4.0, N, S(4.0), true, S(6300L)), + (0.0, 7.5, 4.0, S(4.0), N, true, S(5400L)), + (9.0, 0.0, 4.0, N, S(4.0), true, S(6300L)), + (5.0, 14.0, 4.0, S(2.0), S(2.0), false, S(5400L)), + (9.0, 14.0, 4.0, S(2.0), S(2.0), false, S(5400L)), + (10.0, 14.0, 4.0, N, S(4.0), false, S(4500L)), + (6.0, 15.0, 4.0, S(4.0), N, false, S(7200L)), + + /* setPower is set to > (ev2 * 2) (charging) */ + (0.0, 0.0, 13.0, S(8.0), S(5.0), true, S(4500L)), + (7.0, 0.0, 11.0, S(6.0), S(5.0), true, S(5400L)), + (0.0, 5.0, 15.0, S(10.0), S(5.0), true, S(4320L)), + (0.0, 12.5, 15.0, S(10.0), S(5.0), true, S(4320L)), + (0.0, 0.0, 15.0, S(10.0), S(5.0), true, S(4320L)), + (5.0, 7.5, 15.0, S(10.0), S(5.0), false, S(5400L)), + + /* setPower is negative (discharging) */ + (10.0, 15.0, -4.0, S(-2.0), S(-2.0), true, S(7200L)), + (5.0, 15.0, -4.0, S(-2.0), S(-2.0), true, S(7200L)), + (10.0, 7.5, -4.0, S(-2.0), S(-2.0), true, S(7200L)), + (3.0, 15.0, -4.0, S(-2.0), S(-2.0), true, S(5400L)), + (5.0, 4.0, -4.0, S(-2.0), S(-2.0), false, S(5400L)), + (3.0, 4.0, -4.0, S(-2.0), S(-2.0), false, S(5400L)), + (0.0, 4.0, -4.0, N, S(-4.0), false, S(4500L)), + (6.0, 0.0, -4.0, S(-4.0), N, false, S(7200L)), + + /* setPower is set to > (ev2 * 2) (discharging) */ + (10.0, 15.0, -13.0, S(-8.0), S(-5.0), true, S(7200L)), + (5.0, 15.0, -11.0, S(-6.0), S(-5.0), true, S(5400L)), + (10.0, 8.0, -15.0, S(-10.0), S(-5.0), true, S(6480L)), + (10.0, 5.5, -15.0, S(-10.0), S(-5.0), true, S(5400L)), + (10.0, 15.0, -15.0, S(-10.0), S(-5.0), true, S(6480L)), + (7.0, 10.5, -15.0, S(-10.0), S(-5.0), false, S(5400L)), + ) + + forAll(cases) { + ( + stored1: Double, + stored2: Double, + setPower: Double, + expPower1: Option[Double], + expPower2: Option[Double], + expNextActivation: Boolean, + expNextTick: Option[Long], + ) => + val ev1 = EvModelWrapper( + new MockEvModel( + UUID.randomUUID(), + "Mock EV 1", + 10.0.asKiloWatt, // AC is relevant, + 20.0.asKiloWatt, // DC is not + 10.0.asKiloWattHour, + stored1.asKiloWattHour, + 7200L, + ) + ) + + val ev2 = EvModelWrapper( + new MockEvModel( + UUID.randomUUID(), + "Mock EV 2", + 5.0.asKiloWatt, // AC is relevant, + 10.0.asKiloWatt, // DC is not + 15.0.asKiloWattHour, + stored2.asKiloWattHour, + 10800L, + ) + ) + + evcsModel.handlePowerControl( + EvcsState( + Seq(ev1, ev2), + currentTick, + ), + data, + mockFlexOptions, + Kilowatts(setPower), + ) match { + case ( + EvcsOperatingPoint(evOperatingPoints), + ModelChangeIndicator(actualNextActivation, actualNextTick), + ) => + evOperatingPoints + .get(ev1.uuid) + .map(_.toKilowatts) shouldBe expPower1 + evOperatingPoints + .get(ev2.uuid) + .map(_.toKilowatts) shouldBe expPower2 + + actualNextActivation shouldBe expNextActivation + actualNextTick shouldBe expNextTick + } + } + + } + + } + + } + + // TODO testing requests + +} From 74ba115b0dc5b7f7df1459a9054e302831e3d649 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Thu, 21 Nov 2024 20:01:46 +0100 Subject: [PATCH 41/77] Adapting to changes from dev Signed-off-by: Sebastian Peter --- .../agent/participant/ParticipantAgent.scala | 4 +-- .../ParticipantAgentFundamentals.scala | 4 +-- .../participant/ServiceRegistration.scala | 4 +-- .../simona/agent/participant/data/Data.scala | 34 +++++++++---------- .../participant/statedata/BaseStateData.scala | 16 ++++----- .../statedata/DataCollectionStateData.scala | 6 ++-- .../statedata/ParticipantStateData.scala | 6 ++-- .../participant2/ParticipantGridAdapter.scala | 22 ++++++------ .../result/AccompaniedSimulationResult.scala | 7 ++-- .../model/participant/SystemParticipant.scala | 4 +-- .../model/participant2/FixedFeedInModel.scala | 21 +++++++----- .../model/participant2/ParticipantModel.scala | 14 ++++---- .../participant2/ParticipantModelInit.scala | 2 +- .../participant2/ParticipantModelShell.scala | 6 ++-- .../PrimaryDataParticipantModel.scala | 19 ++++++----- .../simona/model/participant2/PvModel.scala | 27 ++++++++------- .../model/participant2/StorageModel.scala | 16 +++++---- .../simona/model/participant2/WecModel.scala | 21 +++++++----- .../evcs/EvcsChargingProperties.scala | 8 ++--- .../model/participant2/evcs/EvcsModel.scala | 21 +++++++----- .../participant2/load/FixedLoadModel.scala | 8 ++--- .../model/participant2/load/LoadModel.scala | 29 +++++++++------- .../participant2/load/ProfileLoadModel.scala | 3 +- .../participant2/load/RandomLoadModel.scala | 3 +- .../participant2/ChargingHelperSpec.scala | 4 +-- .../model/participant2/PvModelSpec.scala | 12 +++++-- .../model/participant2/WecModelSpec.scala | 2 +- .../participant2/evcs/EvcsModelSpec.scala | 12 +++---- .../evcs/MaximumPowerChargingSpec.scala | 2 +- .../evcs/MockEvcsChargingProperties.scala | 2 +- 30 files changed, 185 insertions(+), 154 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/agent/participant/ParticipantAgent.scala b/src/main/scala/edu/ie3/simona/agent/participant/ParticipantAgent.scala index 19eff90211..33089d72a8 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant/ParticipantAgent.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant/ParticipantAgent.scala @@ -14,7 +14,7 @@ import edu.ie3.simona.agent.participant.ParticipantAgent.{ getAndCheckNodalVoltage, } import edu.ie3.simona.agent.participant.data.Data -import edu.ie3.simona.agent.participant.data.Data.PrimaryData.PrimaryDataWithApparentPower +import edu.ie3.simona.agent.participant.data.Data.PrimaryData.PrimaryDataWithComplexPower import edu.ie3.simona.agent.participant.data.Data.{PrimaryData, SecondaryData} import edu.ie3.simona.agent.participant.data.secondary.SecondaryDataService import edu.ie3.simona.agent.participant.statedata.BaseStateData.{ @@ -89,7 +89,7 @@ import scala.reflect.ClassTag * @since 2019-07-04 */ abstract class ParticipantAgent[ - PD <: PrimaryDataWithApparentPower[PD], + PD <: PrimaryDataWithComplexPower[PD], CD <: CalcRelevantData, MS <: ModelState, D <: ParticipantStateData[PD], diff --git a/src/main/scala/edu/ie3/simona/agent/participant/ParticipantAgentFundamentals.scala b/src/main/scala/edu/ie3/simona/agent/participant/ParticipantAgentFundamentals.scala index 12b0e9657c..b6aa36694e 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant/ParticipantAgentFundamentals.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant/ParticipantAgentFundamentals.scala @@ -26,7 +26,7 @@ import edu.ie3.simona.agent.participant.data.Data.PrimaryData.{ ComplexPower, ComplexPowerAndHeat, EnrichableData, - PrimaryDataWithApparentPower, + PrimaryDataWithComplexPower, } import edu.ie3.simona.agent.participant.data.Data.{PrimaryData, SecondaryData} import edu.ie3.simona.agent.participant.data.secondary.SecondaryDataService @@ -103,7 +103,7 @@ import scala.util.{Failure, Success, Try} /** Useful functions to use in [[ParticipantAgent]] s */ protected trait ParticipantAgentFundamentals[ - PD <: PrimaryDataWithApparentPower[PD], + PD <: PrimaryDataWithComplexPower[PD], CD <: CalcRelevantData, MS <: ModelState, D <: ParticipantStateData[PD], diff --git a/src/main/scala/edu/ie3/simona/agent/participant/ServiceRegistration.scala b/src/main/scala/edu/ie3/simona/agent/participant/ServiceRegistration.scala index 5308b4e768..b07a96484e 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant/ServiceRegistration.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant/ServiceRegistration.scala @@ -8,7 +8,7 @@ package edu.ie3.simona.agent.participant import org.apache.pekko.actor.ActorRef import edu.ie3.datamodel.models.input.system.{EvcsInput, SystemParticipantInput} -import edu.ie3.simona.agent.participant.data.Data.PrimaryData.PrimaryDataWithApparentPower +import edu.ie3.simona.agent.participant.data.Data.PrimaryData.PrimaryDataWithComplexPower import edu.ie3.simona.agent.participant.data.Data.SecondaryData import edu.ie3.simona.agent.participant.data.secondary.SecondaryDataService import edu.ie3.simona.agent.participant.data.secondary.SecondaryDataService.{ @@ -28,7 +28,7 @@ import edu.ie3.simona.ontology.messages.services.EvMessage.RegisterForEvDataMess import edu.ie3.simona.ontology.messages.services.WeatherMessage.RegisterForWeatherMessage trait ServiceRegistration[ - PD <: PrimaryDataWithApparentPower[PD], + PD <: PrimaryDataWithComplexPower[PD], CD <: CalcRelevantData, MS <: ModelState, D <: ParticipantStateData[PD], diff --git a/src/main/scala/edu/ie3/simona/agent/participant/data/Data.scala b/src/main/scala/edu/ie3/simona/agent/participant/data/Data.scala index 24e32cf58e..ee0d792e8f 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant/data/Data.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant/data/Data.scala @@ -46,14 +46,14 @@ object Data { object PrimaryData { - sealed trait EnrichableData[E <: PrimaryDataWithApparentPower[E]] { + sealed trait EnrichableData[E <: PrimaryDataWithComplexPower[E]] { def add(q: ReactivePower): E } - /** Denoting all primary data, that carry apparent power + /** Denoting all primary data, that carry complex power */ - sealed trait PrimaryDataWithApparentPower[ - +T <: PrimaryDataWithApparentPower[T] + sealed trait PrimaryDataWithComplexPower[ + +T <: PrimaryDataWithComplexPower[T] ] extends PrimaryData { val q: ReactivePower @@ -103,18 +103,18 @@ object Data { final case class ComplexPower( override val p: Power, override val q: ReactivePower, - ) extends PrimaryDataWithApparentPower[ComplexPower] { + ) extends PrimaryDataWithComplexPower[ComplexPower] { override def toComplexPower: ComplexPower = this override def withReactivePower(q: ReactivePower): ComplexPower = copy(q = q) } - object ApparentPowerMeta extends PrimaryDataMeta[ApparentPower] { - override def zero: ApparentPower = ApparentPower(zeroKW, zeroKVAr) + object ComplexPowerMeta extends PrimaryDataMeta[ComplexPower] { + override def zero: ComplexPower = ComplexPower(zeroKW, zeroKVAr) - override def scale(data: ApparentPower, factor: Double): ApparentPower = - ApparentPower(data.p * factor, data.q * factor) + override def scale(data: ComplexPower, factor: Double): ComplexPower = + ComplexPower(data.p * factor, data.q * factor) } /** Active power and heat demand as participant simulation result @@ -163,7 +163,7 @@ object Data { override val p: Power, override val q: ReactivePower, override val qDot: Power, - ) extends PrimaryDataWithApparentPower[ComplexPowerAndHeat] + ) extends PrimaryDataWithComplexPower[ComplexPowerAndHeat] with Heat { override def toComplexPower: ComplexPower = ComplexPower(p, q) @@ -172,16 +172,16 @@ object Data { copy(q = q) } - object ApparentPowerAndHeatMeta - extends PrimaryDataMeta[ApparentPowerAndHeat] { - override def zero: ApparentPowerAndHeat = - ApparentPowerAndHeat(zeroKW, zeroKVAr, zeroKW) + object ComplexPowerAndHeatMeta + extends PrimaryDataMeta[ComplexPowerAndHeat] { + override def zero: ComplexPowerAndHeat = + ComplexPowerAndHeat(zeroKW, zeroKVAr, zeroKW) override def scale( - data: ApparentPowerAndHeat, + data: ComplexPowerAndHeat, factor: Double, - ): ApparentPowerAndHeat = - ApparentPowerAndHeat( + ): ComplexPowerAndHeat = + ComplexPowerAndHeat( data.p * factor, data.q * factor, data.qDot * factor, diff --git a/src/main/scala/edu/ie3/simona/agent/participant/statedata/BaseStateData.scala b/src/main/scala/edu/ie3/simona/agent/participant/statedata/BaseStateData.scala index b6ebd99841..ce6d65575f 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant/statedata/BaseStateData.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant/statedata/BaseStateData.scala @@ -7,7 +7,7 @@ package edu.ie3.simona.agent.participant.statedata import edu.ie3.simona.agent.ValueStore -import edu.ie3.simona.agent.participant.data.Data.PrimaryData.PrimaryDataWithApparentPower +import edu.ie3.simona.agent.participant.data.Data.PrimaryData.PrimaryDataWithComplexPower import edu.ie3.simona.agent.participant.data.Data.SecondaryData import edu.ie3.simona.agent.participant.data.secondary.SecondaryDataService import edu.ie3.simona.event.notifier.NotifierConfig @@ -33,10 +33,10 @@ import scala.collection.SortedSet * agents * * @tparam PD - * Type of [[PrimaryDataWithApparentPower]], that the represented Participant + * Type of [[PrimaryDataWithComplexPower]], that the represented Participant * produces */ -trait BaseStateData[+PD <: PrimaryDataWithApparentPower[PD]] +trait BaseStateData[+PD <: PrimaryDataWithComplexPower[PD]] extends ParticipantStateData[PD] { /** The date, that fits the tick 0 @@ -90,7 +90,7 @@ object BaseStateData { /** The agent is supposed to carry out model calculations * * @tparam PD - * Type of [[PrimaryDataWithApparentPower]], that the represented + * Type of [[PrimaryDataWithComplexPower]], that the represented * Participant produces * @tparam CD * Type of [[CalcRelevantData]], that is required by the included model @@ -98,7 +98,7 @@ object BaseStateData { * Restricting the model to a certain class */ trait ModelBaseStateData[ - +PD <: PrimaryDataWithApparentPower[PD], + +PD <: PrimaryDataWithComplexPower[PD], CD <: CalcRelevantData, MS <: ModelState, +M <: SystemParticipant[_ <: CalcRelevantData, PD, MS], @@ -156,7 +156,7 @@ object BaseStateData { _ <: CalcRelevantData, P, _, - ], +P <: PrimaryDataWithApparentPower[P]]( + ], +P <: PrimaryDataWithComplexPower[P]]( model: M, override val startDate: ZonedDateTime, override val endDate: ZonedDateTime, @@ -207,7 +207,7 @@ object BaseStateData { * Type of model, the base state data is attached to */ final case class ParticipantModelBaseStateData[ - +PD <: PrimaryDataWithApparentPower[PD], + +PD <: PrimaryDataWithComplexPower[PD], CD <: CalcRelevantData, MS <: ModelState, M <: SystemParticipant[_ <: CalcRelevantData, PD, MS], @@ -272,7 +272,7 @@ object BaseStateData { * @return * A copy of the base data with updated value stores */ - def updateBaseStateData[PD <: PrimaryDataWithApparentPower[PD]]( + def updateBaseStateData[PD <: PrimaryDataWithComplexPower[PD]]( baseStateData: BaseStateData[PD], updatedResultValueStore: ValueStore[PD], updatedRequestValueStore: ValueStore[PD], diff --git a/src/main/scala/edu/ie3/simona/agent/participant/statedata/DataCollectionStateData.scala b/src/main/scala/edu/ie3/simona/agent/participant/statedata/DataCollectionStateData.scala index 1ce8cf9352..fafb1e5775 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant/statedata/DataCollectionStateData.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant/statedata/DataCollectionStateData.scala @@ -8,7 +8,7 @@ package edu.ie3.simona.agent.participant.statedata import org.apache.pekko.actor.ActorRef import edu.ie3.simona.agent.participant.data.Data -import edu.ie3.simona.agent.participant.data.Data.PrimaryData.PrimaryDataWithApparentPower +import edu.ie3.simona.agent.participant.data.Data.PrimaryData.PrimaryDataWithComplexPower import scala.reflect.{ClassTag, classTag} @@ -24,10 +24,10 @@ import scala.reflect.{ClassTag, classTag} * True, if an [[edu.ie3.simona.ontology.messages.Activation]] has already * arrived * @tparam PD - * Type of the [[PrimaryDataWithApparentPower]], that the model will produce + * Type of the [[PrimaryDataWithComplexPower]], that the model will produce * or receive as primary data */ -final case class DataCollectionStateData[+PD <: PrimaryDataWithApparentPower[ +final case class DataCollectionStateData[+PD <: PrimaryDataWithComplexPower[ PD ]]( baseStateData: BaseStateData[PD], diff --git a/src/main/scala/edu/ie3/simona/agent/participant/statedata/ParticipantStateData.scala b/src/main/scala/edu/ie3/simona/agent/participant/statedata/ParticipantStateData.scala index 786caf63b0..dab7ff586b 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant/statedata/ParticipantStateData.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant/statedata/ParticipantStateData.scala @@ -8,7 +8,7 @@ package edu.ie3.simona.agent.participant.statedata import edu.ie3.datamodel.models.input.container.ThermalGrid import edu.ie3.datamodel.models.input.system.SystemParticipantInput -import edu.ie3.simona.agent.participant.data.Data.PrimaryData.PrimaryDataWithApparentPower +import edu.ie3.simona.agent.participant.data.Data.PrimaryData.PrimaryDataWithComplexPower import edu.ie3.simona.agent.participant.data.Data.{PrimaryData, SecondaryData} import edu.ie3.simona.agent.participant.data.secondary.SecondaryDataService import edu.ie3.simona.config.SimonaConfig @@ -261,11 +261,11 @@ object ParticipantStateData { * Mapping from service provider to foreseen next tick, it will send new * data * @tparam PD - * Type of [[PrimaryDataWithApparentPower]], that is covered by given + * Type of [[PrimaryDataWithComplexPower]], that is covered by given * [[BaseStateData]] */ final case class CollectRegistrationConfirmMessages[ - +PD <: PrimaryDataWithApparentPower[PD] + +PD <: PrimaryDataWithComplexPower[PD] ]( baseStateData: BaseStateData[PD], pendingResponses: Iterable[ClassicActorRef], diff --git a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantGridAdapter.scala b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantGridAdapter.scala index ae1904e543..7fba50ae9b 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantGridAdapter.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantGridAdapter.scala @@ -7,7 +7,7 @@ package edu.ie3.simona.agent.participant2 import edu.ie3.simona.agent.grid.GridAgent -import edu.ie3.simona.agent.participant.data.Data.PrimaryData.ApparentPower +import edu.ie3.simona.agent.participant.data.Data.PrimaryData.ComplexPower import edu.ie3.simona.agent.participant2.ParticipantGridAdapter._ import edu.ie3.simona.exceptions.CriticalFailureException import edu.ie3.util.scala.quantities.DefaultQuantities.{zeroMVAr, zeroMW} @@ -37,7 +37,7 @@ final case class ParticipantGridAdapter( gridAgent: ActorRef[GridAgent.Request], nodalVoltage: Dimensionless, expectedRequestTick: Long, - tickToPower: SortedMap[Long, ApparentPower], + tickToPower: SortedMap[Long, ComplexPower], avgPowerResult: Option[AvgPowerResult], ) { @@ -54,7 +54,7 @@ final case class ParticipantGridAdapter( } def storePowerValue( - power: ApparentPower, + power: ComplexPower, tick: Long, ): ParticipantGridAdapter = copy(tickToPower = tickToPower.updated(tick, power)) @@ -133,7 +133,7 @@ object ParticipantGridAdapter { windowStart: Long, windowEnd: Long, voltage: Dimensionless, - avgPower: ApparentPower, + avgPower: ComplexPower, newResult: Boolean, ) @@ -150,9 +150,9 @@ object ParticipantGridAdapter { ) private def reduceTickToPowerMap( - tickToPower: SortedMap[Long, ApparentPower], + tickToPower: SortedMap[Long, ComplexPower], windowStart: Long, - ): SortedMap[Long, ApparentPower] = { + ): SortedMap[Long, ComplexPower] = { // keep the last entry at or before windowStart val lastTickBeforeWindowStart = tickToPower.rangeUntil(windowStart + 1).lastOption @@ -178,14 +178,14 @@ object ParticipantGridAdapter { * The averaged apparent power */ private def averageApparentPower( - tickToPower: Map[Long, ApparentPower], + tickToPower: Map[Long, ComplexPower], windowStart: Long, windowEnd: Long, activeToReactivePowerFuncOpt: Option[ Power => ReactivePower ] = None, log: Logger, - ): ApparentPower = { + ): ComplexPower = { val p = QuantityUtil.average[Power, Energy]( tickToPower.map { case (tick, pd) => tick -> pd.p @@ -209,8 +209,8 @@ object ParticipantGridAdapter { case Some(qFunc) => // NOTE: The type conversion to Megawatts is done to satisfy the methods type constraints // and is undone after unpacking the results - tick -> Megawatts(qFunc(pd.toApparentPower.p).toMegavars) - case None => tick -> Megawatts(pd.toApparentPower.q.toMegavars) + tick -> Megawatts(qFunc(pd.p).toMegavars) + case None => tick -> Megawatts(pd.q.toMegavars) } }, windowStart, @@ -226,6 +226,6 @@ object ParticipantGridAdapter { zeroMVAr } - ApparentPower(p, q) + ComplexPower(p, q) } } diff --git a/src/main/scala/edu/ie3/simona/io/result/AccompaniedSimulationResult.scala b/src/main/scala/edu/ie3/simona/io/result/AccompaniedSimulationResult.scala index a73a854de4..92c8b491ce 100644 --- a/src/main/scala/edu/ie3/simona/io/result/AccompaniedSimulationResult.scala +++ b/src/main/scala/edu/ie3/simona/io/result/AccompaniedSimulationResult.scala @@ -7,16 +7,17 @@ package edu.ie3.simona.io.result import edu.ie3.datamodel.models.result.ResultEntity -import edu.ie3.simona.agent.participant.data.Data.PrimaryData.PrimaryDataWithApparentPower +import edu.ie3.simona.agent.participant.data.Data.PrimaryData.PrimaryDataWithComplexPower /** A class to offer means to transport accompanying results alongside of - * [[PrimaryDataWithApparentPower]], e.g. heat results obtained during a + * [[PrimaryDataWithComplexPower]], e.g. heat results obtained during a * simulation + * * @param primaryData * The original primary data of the electrical asset * @tparam PD * Type of primary data, that is carried */ -final case class AccompaniedSimulationResult[PD <: PrimaryDataWithApparentPower[ +final case class AccompaniedSimulationResult[PD <: PrimaryDataWithComplexPower[ PD ]](primaryData: PD, accompanyingResults: Seq[ResultEntity] = Seq.empty) diff --git a/src/main/scala/edu/ie3/simona/model/participant/SystemParticipant.scala b/src/main/scala/edu/ie3/simona/model/participant/SystemParticipant.scala index 1aa4377f8e..7fee381e11 100644 --- a/src/main/scala/edu/ie3/simona/model/participant/SystemParticipant.scala +++ b/src/main/scala/edu/ie3/simona/model/participant/SystemParticipant.scala @@ -8,7 +8,7 @@ package edu.ie3.simona.model.participant import edu.ie3.simona.agent.participant.data.Data.PrimaryData.{ ComplexPower, - PrimaryDataWithApparentPower, + PrimaryDataWithComplexPower, } import edu.ie3.simona.model.SystemComponent import edu.ie3.simona.model.participant.control.QControl @@ -44,7 +44,7 @@ import java.util.UUID */ abstract class SystemParticipant[ CD <: CalcRelevantData, - +PD <: PrimaryDataWithApparentPower[PD], + +PD <: PrimaryDataWithComplexPower[PD], MS <: ModelState, ]( uuid: UUID, diff --git a/src/main/scala/edu/ie3/simona/model/participant2/FixedFeedInModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/FixedFeedInModel.scala index 44b4c96e84..5724aa204f 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/FixedFeedInModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/FixedFeedInModel.scala @@ -12,7 +12,10 @@ import edu.ie3.datamodel.models.result.system.{ SystemParticipantResult, } import edu.ie3.simona.agent.participant.data.Data -import edu.ie3.simona.agent.participant.data.Data.PrimaryData +import edu.ie3.simona.agent.participant.data.Data.PrimaryData.{ + ComplexPower, + PrimaryDataWithComplexPower, +} import edu.ie3.simona.model.participant.control.QControl import edu.ie3.simona.model.participant2.ParticipantFlexibility.ParticipantSimpleFlexibility import edu.ie3.simona.model.participant2.ParticipantModel.{ @@ -24,15 +27,15 @@ import edu.ie3.simona.model.participant2.ParticipantModel.{ import edu.ie3.simona.service.ServiceType import edu.ie3.util.quantities.PowerSystemUnits import edu.ie3.util.quantities.QuantityUtils.RichQuantityDouble -import squants.energy.Kilowatts -import squants.{Dimensionless, Power} +import edu.ie3.util.scala.quantities.{ApparentPower, Kilovoltamperes} +import squants.Dimensionless import java.time.ZonedDateTime import java.util.UUID class FixedFeedInModel( override val uuid: UUID, - override val sRated: Power, + override val sRated: ApparentPower, override val cosPhiRated: Double, override val qControl: QControl, ) extends ParticipantModel[ @@ -53,7 +56,7 @@ class FixedFeedInModel( state: ParticipantModel.ConstantState.type, relevantData: ParticipantModel.FixedRelevantData.type, ): (ActivePowerOperatingPoint, Option[Long]) = { - val power = sRated * (-1) * cosPhiRated + val power = pRated * -1 (ActivePowerOperatingPoint(power), None) } @@ -65,7 +68,7 @@ class FixedFeedInModel( state: ParticipantModel.ConstantState.type, lastOperatingPoint: Option[ActivePowerOperatingPoint], currentOperatingPoint: ActivePowerOperatingPoint, - complexPower: PrimaryData.ApparentPower, + complexPower: ComplexPower, dateTime: ZonedDateTime, ): Iterable[SystemParticipantResult] = Iterable( @@ -78,7 +81,7 @@ class FixedFeedInModel( ) override def createPrimaryDataResult( - data: PrimaryData.PrimaryDataWithApparentPower[_], + data: PrimaryDataWithComplexPower[_], dateTime: ZonedDateTime, ): SystemParticipantResult = new FixedFeedInResult( @@ -105,9 +108,9 @@ object FixedFeedInModel { ): FixedFeedInModel = { new FixedFeedInModel( inputModel.getUuid, - Kilowatts( + Kilovoltamperes( inputModel.getsRated - .to(PowerSystemUnits.KILOWATT) + .to(PowerSystemUnits.KILOVOLTAMPERE) .getValue .doubleValue ), diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala index 768c94c6f3..4fcbd38afb 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala @@ -8,8 +8,8 @@ package edu.ie3.simona.model.participant2 import edu.ie3.datamodel.models.result.system.SystemParticipantResult import edu.ie3.simona.agent.participant.data.Data.PrimaryData.{ - ApparentPower, - PrimaryDataWithApparentPower, + ComplexPower, + PrimaryDataWithComplexPower, } import edu.ie3.simona.agent.participant.data.Data import edu.ie3.simona.model.participant2.ParticipantModel.{ @@ -22,7 +22,7 @@ import edu.ie3.simona.agent.participant2.ParticipantAgent.ParticipantRequest import edu.ie3.simona.model.participant.control.QControl import edu.ie3.simona.service.ServiceType import edu.ie3.util.scala.quantities.DefaultQuantities.zeroKW -import edu.ie3.util.scala.quantities.ReactivePower +import edu.ie3.util.scala.quantities.{ApparentPower, ReactivePower} import org.apache.pekko.actor.typed.scaladsl.ActorContext import squants.Dimensionless import squants.energy.Power @@ -37,11 +37,11 @@ abstract class ParticipantModel[ ] extends ParticipantFlexibility[OP, S, OR] { val uuid: UUID - val sRated: Power + val sRated: ApparentPower val cosPhiRated: Double val qControl: QControl - protected val pRated: Power = sRated * cosPhiRated + protected val pRated: Power = sRated.toActivePower(cosPhiRated) /** Get a partial function, that transfers the current active into reactive * power based on the participants properties and the given nodal voltage @@ -117,12 +117,12 @@ abstract class ParticipantModel[ state: S, lastOperatingPoint: Option[OP], currentOperatingPoint: OP, - complexPower: ApparentPower, + complexPower: ComplexPower, dateTime: ZonedDateTime, ): Iterable[SystemParticipantResult] def createPrimaryDataResult( - data: PrimaryDataWithApparentPower[_], + data: PrimaryDataWithComplexPower[_], dateTime: ZonedDateTime, ): SystemParticipantResult diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelInit.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelInit.scala index 09351ad782..b2e83d487a 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelInit.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelInit.scala @@ -91,7 +91,7 @@ object ParticipantModelInit { val primaryResultFunc = new PrimaryResultFunc { override def createResult( - data: PrimaryData.PrimaryDataWithApparentPower[_], + data: PrimaryData.PrimaryDataWithComplexPower[_], dateTime: ZonedDateTime, ): SystemParticipantResult = physicalModel.createPrimaryDataResult(data, dateTime) diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala index 2703a15572..cb711df812 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala @@ -8,7 +8,7 @@ package edu.ie3.simona.model.participant2 import edu.ie3.datamodel.models.input.system.SystemParticipantInput import edu.ie3.datamodel.models.result.system.SystemParticipantResult -import edu.ie3.simona.agent.participant.data.Data.PrimaryData.ApparentPower +import edu.ie3.simona.agent.participant.data.Data.PrimaryData.ComplexPower import edu.ie3.simona.agent.participant.data.Data.SecondaryData import edu.ie3.simona.agent.participant2.ParticipantAgent import edu.ie3.simona.agent.participant2.ParticipantAgent.ParticipantRequest @@ -133,7 +133,7 @@ final case class ParticipantModelShell[ val reactivePower = op.reactivePower.getOrElse( activeToReactivePowerFunc(nodalVoltage)(activePower) ) - val complexPower = ApparentPower(activePower, reactivePower) + val complexPower = ComplexPower(activePower, reactivePower) val participantResults = model.createResults( state, @@ -265,7 +265,7 @@ final case class ParticipantModelShell[ object ParticipantModelShell { final case class ResultsContainer( - totalPower: ApparentPower, + totalPower: ComplexPower, modelResults: Iterable[SystemParticipantResult], ) diff --git a/src/main/scala/edu/ie3/simona/model/participant2/PrimaryDataParticipantModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/PrimaryDataParticipantModel.scala index 5bb4ccdf8e..681e16f383 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/PrimaryDataParticipantModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/PrimaryDataParticipantModel.scala @@ -10,8 +10,9 @@ import edu.ie3.datamodel.models.result.system.SystemParticipantResult import edu.ie3.simona.agent.participant.data.Data import edu.ie3.simona.agent.participant.data.Data.{PrimaryData, PrimaryDataMeta} import edu.ie3.simona.agent.participant.data.Data.PrimaryData.{ + ComplexPower, EnrichableData, - PrimaryDataWithApparentPower, + PrimaryDataWithComplexPower, } import edu.ie3.simona.exceptions.CriticalFailureException import edu.ie3.simona.model.participant.control.QControl @@ -26,7 +27,7 @@ import edu.ie3.simona.model.participant2.PrimaryDataParticipantModel._ import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage import edu.ie3.simona.ontology.messages.flex.MinMaxFlexibilityMessage.ProvideMinMaxFlexOptions import edu.ie3.simona.service.ServiceType -import edu.ie3.util.scala.quantities.ReactivePower +import edu.ie3.util.scala.quantities.{ApparentPower, ReactivePower} import squants.{Dimensionless, Power} import java.time.ZonedDateTime @@ -37,7 +38,7 @@ import scala.reflect.ClassTag */ final case class PrimaryDataParticipantModel[P <: PrimaryData: ClassTag]( override val uuid: UUID, - override val sRated: Power, + override val sRated: ApparentPower, override val cosPhiRated: Double, override val qControl: QControl, primaryDataResultFunc: PrimaryResultFunc, @@ -65,11 +66,11 @@ final case class PrimaryDataParticipantModel[P <: PrimaryData: ClassTag]( state: ConstantState.type, lastOperatingPoint: Option[PrimaryOperatingPoint[P]], currentOperatingPoint: PrimaryOperatingPoint[P], - complexPower: PrimaryData.ApparentPower, + complexPower: ComplexPower, dateTime: ZonedDateTime, ): Iterable[SystemParticipantResult] = { val primaryDataWithApparentPower = currentOperatingPoint.data match { - case primaryDataWithApparentPower: PrimaryDataWithApparentPower[_] => + case primaryDataWithApparentPower: PrimaryDataWithComplexPower[_] => primaryDataWithApparentPower case enrichableData: EnrichableData[_] => enrichableData.add(complexPower.q) @@ -80,7 +81,7 @@ final case class PrimaryDataParticipantModel[P <: PrimaryData: ClassTag]( } override def createPrimaryDataResult( - data: PrimaryDataWithApparentPower[_], + data: PrimaryDataWithComplexPower[_], dateTime: ZonedDateTime, ): SystemParticipantResult = throw new CriticalFailureException( "Method not implemented by this model." @@ -152,7 +153,7 @@ object PrimaryDataParticipantModel { data: P ): PrimaryOperatingPoint[P] = data match { - case apparentPowerData: P with PrimaryDataWithApparentPower[_] => + case apparentPowerData: P with PrimaryDataWithComplexPower[_] => PrimaryApparentPowerOperatingPoint(apparentPowerData) case other: P with EnrichableData[_] => PrimaryActivePowerOperatingPoint(other) @@ -160,7 +161,7 @@ object PrimaryDataParticipantModel { } private final case class PrimaryApparentPowerOperatingPoint[ - P <: PrimaryDataWithApparentPower[_] + P <: PrimaryDataWithComplexPower[_] ](override val data: P) extends PrimaryOperatingPoint[P] { override val reactivePower: Option[ReactivePower] = Some(data.q) @@ -178,7 +179,7 @@ object PrimaryDataParticipantModel { */ trait PrimaryResultFunc { def createResult( - data: PrimaryDataWithApparentPower[_], + data: PrimaryDataWithComplexPower[_], dateTime: ZonedDateTime, ): SystemParticipantResult } diff --git a/src/main/scala/edu/ie3/simona/model/participant2/PvModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/PvModel.scala index b5c7bb03f9..14fcd60b68 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/PvModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/PvModel.scala @@ -12,9 +12,11 @@ import edu.ie3.datamodel.models.result.system.{ PvResult, SystemParticipantResult, } -import edu.ie3.simona.agent.participant.data.Data.PrimaryData.ApparentPower +import edu.ie3.simona.agent.participant.data.Data.PrimaryData.{ + ComplexPower, + PrimaryDataWithComplexPower, +} import edu.ie3.simona.agent.participant.data.Data -import edu.ie3.simona.agent.participant.data.Data.PrimaryData import edu.ie3.simona.exceptions.CriticalFailureException import edu.ie3.simona.model.participant.control.QControl import edu.ie3.simona.model.participant2.ParticipantFlexibility.ParticipantSimpleFlexibility @@ -31,7 +33,6 @@ import edu.ie3.util.quantities.PowerSystemUnits import edu.ie3.util.quantities.QuantityUtils.RichQuantityDouble import edu.ie3.util.scala.quantities._ import squants._ -import squants.energy.Kilowatts import squants.space.{Degrees, SquareMeters} import squants.time.Minutes import tech.units.indriya.unit.Units._ @@ -43,7 +44,7 @@ import scala.math._ class PvModel private ( override val uuid: UUID, - override val sRated: Power, + override val sRated: ApparentPower, override val cosPhiRated: Double, override val qControl: QControl, private val lat: Angle, @@ -65,15 +66,15 @@ class PvModel private ( /** 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 */ - val sMax: Power = sRated * 1.1 + val sMax: ApparentPower = sRated * 1.1 /** Permissible maximum active power feed in (therefore negative) */ - protected val pMax: Power = sMax * cosPhiRated * -1d + protected val pMax: Power = sMax.toActivePower(cosPhiRated) * -1 /** Reference yield at standard testing conditions (STC) */ private val yieldSTC = WattsPerSquareMeter(1000d) - private val activationThreshold = sRated * cosPhiRated * 0.001 * -1d + private val activationThreshold = pMax * 0.001 * -1 /** Calculate the active power behaviour of the model * @@ -706,9 +707,9 @@ class PvModel private ( eTotalInWhPerSM * moduleSurface.toSquareMeters * etaConv.toEach * (genCorr * tempCorr) /* Calculate the foreseen active power output without boundary condition adaptions */ - val proposal = sRated * (-1) * ( + val proposal = pRated * -1 * ( actYield / irradiationSTC - ) * cosPhiRated + ) /* Do sanity check, if the proposed feed in is above the estimated maximum to be apparent active power of the plant */ if (proposal < pMax) @@ -729,7 +730,7 @@ class PvModel private ( state: ParticipantModel.ConstantState.type, lastOperatingPoint: Option[ActivePowerOperatingPoint], currentOperatingPoint: ActivePowerOperatingPoint, - complexPower: ApparentPower, + complexPower: ComplexPower, dateTime: ZonedDateTime, ): Iterable[SystemParticipantResult] = Iterable( @@ -742,7 +743,7 @@ class PvModel private ( ) override def createPrimaryDataResult( - data: PrimaryData.PrimaryDataWithApparentPower[_], + data: PrimaryDataWithComplexPower[_], dateTime: ZonedDateTime, ): SystemParticipantResult = new PvResult( @@ -805,9 +806,9 @@ object PvModel { ): PvModel = new PvModel( inputModel.getUuid, - Kilowatts( + Kilovoltamperes( inputModel.getsRated - .to(PowerSystemUnits.KILOWATT) + .to(PowerSystemUnits.KILOVOLTAMPERE) .getValue .doubleValue ), diff --git a/src/main/scala/edu/ie3/simona/model/participant2/StorageModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/StorageModel.scala index 32c17ec66c..747cf26b1b 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/StorageModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/StorageModel.scala @@ -12,7 +12,10 @@ import edu.ie3.datamodel.models.result.system.{ SystemParticipantResult, } import edu.ie3.simona.agent.participant.data.Data -import edu.ie3.simona.agent.participant.data.Data.PrimaryData +import edu.ie3.simona.agent.participant.data.Data.PrimaryData.{ + ComplexPower, + PrimaryDataWithComplexPower, +} import edu.ie3.simona.config.SimonaConfig.StorageRuntimeConfig import edu.ie3.simona.exceptions.CriticalFailureException import edu.ie3.simona.model.participant.StorageModel.RefTargetSocParams @@ -31,6 +34,7 @@ import edu.ie3.simona.ontology.messages.flex.MinMaxFlexibilityMessage.ProvideMin import edu.ie3.simona.service.ServiceType import edu.ie3.util.quantities.PowerSystemUnits import edu.ie3.util.quantities.QuantityUtils.RichQuantityDouble +import edu.ie3.util.scala.quantities.{ApparentPower, Kilovoltamperes} import edu.ie3.util.scala.quantities.DefaultQuantities.{zeroKW, zeroKWh} import squants.energy.{KilowattHours, Kilowatts} import squants.{Dimensionless, Each, Energy, Power, Seconds} @@ -40,7 +44,7 @@ import java.util.UUID class StorageModel private ( override val uuid: UUID, - override val sRated: Power, + override val sRated: ApparentPower, override val cosPhiRated: Double, override val qControl: QControl, eStorage: Energy, @@ -138,7 +142,7 @@ class StorageModel private ( state: StorageState, lastOperatingPoint: Option[ActivePowerOperatingPoint], currentOperatingPoint: ActivePowerOperatingPoint, - complexPower: PrimaryData.ApparentPower, + complexPower: ComplexPower, dateTime: ZonedDateTime, ): Iterable[SystemParticipantResult] = Iterable( @@ -152,7 +156,7 @@ class StorageModel private ( ) override def createPrimaryDataResult( - data: PrimaryData.PrimaryDataWithApparentPower[_], + data: PrimaryDataWithComplexPower[_], dateTime: ZonedDateTime, ): SystemParticipantResult = new StorageResult( @@ -335,9 +339,9 @@ object StorageModel { ): StorageModel = new StorageModel( inputModel.getUuid, - Kilowatts( + Kilovoltamperes( inputModel.getType.getsRated - .to(PowerSystemUnits.KILOWATT) + .to(PowerSystemUnits.KILOVOLTAMPERE) .getValue .doubleValue ), diff --git a/src/main/scala/edu/ie3/simona/model/participant2/WecModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/WecModel.scala index b3a07e771d..9eabe275b7 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/WecModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/WecModel.scala @@ -14,8 +14,10 @@ import edu.ie3.datamodel.models.result.system.{ WecResult, } import edu.ie3.simona.agent.participant.data.Data -import edu.ie3.simona.agent.participant.data.Data.PrimaryData -import edu.ie3.simona.agent.participant.data.Data.PrimaryData.ApparentPower +import edu.ie3.simona.agent.participant.data.Data.PrimaryData.{ + ComplexPower, + PrimaryDataWithComplexPower, +} import edu.ie3.simona.exceptions.CriticalFailureException import edu.ie3.simona.model.participant.control.QControl import edu.ie3.simona.model.participant2.ParticipantFlexibility.ParticipantSimpleFlexibility @@ -35,11 +37,12 @@ import edu.ie3.simona.model.system.Characteristic import edu.ie3.simona.model.system.Characteristic.XYPair import edu.ie3.simona.ontology.messages.services.WeatherMessage.WeatherData import edu.ie3.simona.service.ServiceType -import edu.ie3.util.quantities.PowerSystemUnits.{KILOWATT, PU} +import edu.ie3.util.quantities.PowerSystemUnits.{KILOVOLTAMPERE, PU} import edu.ie3.util.quantities.QuantityUtils.RichQuantityDouble import edu.ie3.util.scala.Scope +import edu.ie3.util.scala.quantities.{ApparentPower, Kilovoltamperes} import squants._ -import squants.energy.{Kilowatts, Watts} +import squants.energy.Watts import squants.mass.{Kilograms, KilogramsPerCubicMeter} import squants.motion.{MetersPerSecond, Pressure} import squants.space.SquareMeters @@ -52,7 +55,7 @@ import scala.collection.SortedSet class WecModel private ( override val uuid: UUID, - override val sRated: Power, + override val sRated: ApparentPower, override val cosPhiRated: Double, override val qControl: QControl, private val rotorArea: Area, @@ -166,7 +169,7 @@ class WecModel private ( state: ParticipantModel.ConstantState.type, lastOperatingPoint: Option[ActivePowerOperatingPoint], currentOperatingPoint: ActivePowerOperatingPoint, - complexPower: ApparentPower, + complexPower: ComplexPower, dateTime: ZonedDateTime, ): Iterable[SystemParticipantResult] = Iterable( @@ -179,7 +182,7 @@ class WecModel private ( ) override def createPrimaryDataResult( - data: PrimaryData.PrimaryDataWithApparentPower[_], + data: PrimaryDataWithComplexPower[_], dateTime: ZonedDateTime, ): SystemParticipantResult = new WecResult( @@ -273,8 +276,8 @@ object WecModel { ): WecModel = new WecModel( inputModel.getUuid, - Kilowatts( - inputModel.getType.getsRated.to(KILOWATT).getValue.doubleValue + Kilovoltamperes( + inputModel.getType.getsRated.to(KILOVOLTAMPERE).getValue.doubleValue ), inputModel.getType.getCosPhiRated, QControl(inputModel.getqCharacteristics), diff --git a/src/main/scala/edu/ie3/simona/model/participant2/evcs/EvcsChargingProperties.scala b/src/main/scala/edu/ie3/simona/model/participant2/evcs/EvcsChargingProperties.scala index c7e752d932..b79965c541 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/evcs/EvcsChargingProperties.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/evcs/EvcsChargingProperties.scala @@ -14,7 +14,7 @@ trait EvcsChargingProperties { /** Charging station rated power */ - val sRated: Power + protected val pRated: Power val currentType: ElectricCurrentType @@ -33,11 +33,11 @@ trait EvcsChargingProperties { ): Power = { val evPower = currentType match { case ElectricCurrentType.AC => - ev.sRatedAc + ev.pRatedAc case ElectricCurrentType.DC => - ev.sRatedDc + ev.pRatedDc } /* Limit the charging power to the minimum of ev's and evcs' permissible power */ - evPower.min(sRated) + evPower.min(pRated) } } diff --git a/src/main/scala/edu/ie3/simona/model/participant2/evcs/EvcsModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/evcs/EvcsModel.scala index c8b516570e..bf55446b02 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/evcs/EvcsModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/evcs/EvcsModel.scala @@ -15,13 +15,13 @@ import edu.ie3.datamodel.models.result.system.{ } import edu.ie3.simona.agent.participant.data.Data import edu.ie3.simona.agent.participant.data.Data.PrimaryData +import edu.ie3.simona.agent.participant.data.Data.PrimaryData.ComplexPower import edu.ie3.simona.agent.participant2.ParticipantAgent import edu.ie3.simona.agent.participant2.ParticipantAgent.ParticipantRequest import edu.ie3.simona.config.SimonaConfig.EvcsRuntimeConfig import edu.ie3.simona.exceptions.CriticalFailureException import edu.ie3.simona.model.participant.control.QControl import edu.ie3.simona.model.participant.evcs.EvModelWrapper -import edu.ie3.simona.model.participant2.{ChargingHelper, ParticipantModel} import edu.ie3.simona.model.participant2.ParticipantModel.{ ModelChangeIndicator, ModelState, @@ -33,14 +33,19 @@ import edu.ie3.simona.model.participant2.evcs.EvcsModel.{ EvcsRelevantData, EvcsState, } +import edu.ie3.simona.model.participant2.{ChargingHelper, ParticipantModel} import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage import edu.ie3.simona.ontology.messages.flex.MinMaxFlexibilityMessage.ProvideMinMaxFlexOptions import edu.ie3.simona.ontology.messages.services.EvMessage.ArrivingEvs import edu.ie3.simona.service.ServiceType -import edu.ie3.util.quantities.PowerSystemUnits.KILOWATT +import edu.ie3.util.quantities.PowerSystemUnits.KILOVOLTAMPERE import edu.ie3.util.quantities.QuantityUtils.RichQuantityDouble import edu.ie3.util.scala.quantities.DefaultQuantities._ -import edu.ie3.util.scala.quantities.ReactivePower +import edu.ie3.util.scala.quantities.{ + ApparentPower, + Kilovoltamperes, + ReactivePower, +} import org.apache.pekko.actor.typed.scaladsl.ActorContext import squants.energy.{Kilowatts, Watts} import squants.time.Seconds @@ -52,7 +57,7 @@ import java.util.UUID class EvcsModel private ( override val uuid: UUID, - override val sRated: Power, + override val sRated: ApparentPower, override val cosPhiRated: Double, override val qControl: QControl, strategy: EvcsChargingStrategy, @@ -123,7 +128,7 @@ class EvcsModel private ( state: EvcsState, lastOperatingPoint: Option[EvcsOperatingPoint], currentOperatingPoint: EvcsOperatingPoint, - complexPower: PrimaryData.ApparentPower, + complexPower: ComplexPower, dateTime: ZonedDateTime, ): Iterable[SystemParticipantResult] = { val evResults = state.evs.flatMap { ev => @@ -180,7 +185,7 @@ class EvcsModel private ( } override def createPrimaryDataResult( - data: PrimaryData.PrimaryDataWithApparentPower[_], + data: PrimaryData.PrimaryDataWithComplexPower[_], dateTime: ZonedDateTime, ): SystemParticipantResult = new EvcsResult( @@ -575,8 +580,8 @@ object EvcsModel { ): EvcsModel = new EvcsModel( inputModel.getUuid, - Kilowatts( - inputModel.getType.getsRated.to(KILOWATT).getValue.doubleValue + Kilovoltamperes( + inputModel.getType.getsRated.to(KILOVOLTAMPERE).getValue.doubleValue ), inputModel.getCosPhiRated, QControl(inputModel.getqCharacteristics), diff --git a/src/main/scala/edu/ie3/simona/model/participant2/load/FixedLoadModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/load/FixedLoadModel.scala index dd42342980..8c68cebd2e 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/load/FixedLoadModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/load/FixedLoadModel.scala @@ -21,7 +21,7 @@ import edu.ie3.simona.model.participant2.ParticipantModel.{ FixedRelevantData, } import edu.ie3.util.quantities.PowerSystemUnits -import squants.energy.Kilowatts +import edu.ie3.util.scala.quantities.{ApparentPower, Kilovoltamperes} import squants.time.Days import squants.{Dimensionless, Power} @@ -30,7 +30,7 @@ import java.util.UUID class FixedLoadModel( override val uuid: UUID, - override val sRated: Power, + override val sRated: ApparentPower, override val cosPhiRated: Double, override val qControl: QControl, private val activePower: Power, @@ -67,9 +67,9 @@ object FixedLoadModel { new FixedLoadModel( inputModel.getUuid, - Kilowatts( + Kilovoltamperes( inputModel.getsRated - .to(PowerSystemUnits.KILOWATT) + .to(PowerSystemUnits.KILOVOLTAMPERE) .getValue .doubleValue ), diff --git a/src/main/scala/edu/ie3/simona/model/participant2/load/LoadModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/load/LoadModel.scala index 6fb36cbfc0..4ee0cffe09 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/load/LoadModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/load/LoadModel.scala @@ -11,7 +11,10 @@ import edu.ie3.datamodel.models.result.system.{ LoadResult, SystemParticipantResult, } -import edu.ie3.simona.agent.participant.data.Data.PrimaryData +import edu.ie3.simona.agent.participant.data.Data.PrimaryData.{ + ComplexPower, + PrimaryDataWithComplexPower, +} import edu.ie3.simona.config.SimonaConfig.LoadRuntimeConfig import edu.ie3.simona.model.participant.load.LoadModelBehaviour import edu.ie3.simona.model.participant2.ParticipantFlexibility.ParticipantSimpleFlexibility @@ -25,7 +28,7 @@ import edu.ie3.simona.model.participant2.ParticipantModel.{ import edu.ie3.simona.service.ServiceType import edu.ie3.util.quantities.PowerSystemUnits import edu.ie3.util.quantities.QuantityUtils.RichQuantityDouble -import squants.energy.Kilowatts +import edu.ie3.util.scala.quantities.{ApparentPower, Kilovoltamperes} import squants.{Energy, Power} import java.time.ZonedDateTime @@ -52,7 +55,7 @@ abstract class LoadModel[OR <: OperationRelevantData] state: ParticipantModel.ConstantState.type, lastOperatingPoint: Option[ActivePowerOperatingPoint], currentOperatingPoint: ActivePowerOperatingPoint, - complexPower: PrimaryData.ApparentPower, + complexPower: ComplexPower, dateTime: ZonedDateTime, ): Iterable[SystemParticipantResult] = Iterable( @@ -65,7 +68,7 @@ abstract class LoadModel[OR <: OperationRelevantData] ) override def createPrimaryDataResult( - data: PrimaryData.PrimaryDataWithApparentPower[_], + data: PrimaryDataWithComplexPower[_], dateTime: ZonedDateTime, ): SystemParticipantResult = new LoadResult( @@ -104,14 +107,14 @@ object LoadModel { inputModel: LoadInput, activePower: Power, safetyFactor: Double = 1d, - ): Power = { - val sRated = Kilowatts( + ): ApparentPower = { + val sRated = Kilovoltamperes( inputModel.getsRated - .to(PowerSystemUnits.KILOWATT) + .to(PowerSystemUnits.KILOVOLTAMPERE) .getValue .doubleValue ) - val pRated = sRated * inputModel.getCosPhiRated + val pRated = sRated.toActivePower(inputModel.getCosPhiRated) val referenceScalingFactor = activePower / pRated sRated * referenceScalingFactor * safetyFactor } @@ -147,10 +150,12 @@ object LoadModel { profileMaxPower: Power, profileEnergyScaling: Energy, safetyFactor: Double = 1d, - ): Power = { - (profileMaxPower / inputModel.getCosPhiRated) * ( - energyConsumption / profileEnergyScaling - ) * safetyFactor + ): ApparentPower = { + val profileMaxApparentPower = Kilovoltamperes( + profileMaxPower.toKilowatts / inputModel.getCosPhiRated + ) + + profileMaxApparentPower * (energyConsumption / profileEnergyScaling) * safetyFactor } def apply( diff --git a/src/main/scala/edu/ie3/simona/model/participant2/load/ProfileLoadModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/load/ProfileLoadModel.scala index 36fbb11849..2797c300c5 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/load/ProfileLoadModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/load/ProfileLoadModel.scala @@ -19,6 +19,7 @@ import edu.ie3.simona.model.participant2.ParticipantModel.{ DateTimeData, } import edu.ie3.simona.util.TickUtil +import edu.ie3.util.scala.quantities.ApparentPower import squants.{Dimensionless, Power} import java.time.ZonedDateTime @@ -26,7 +27,7 @@ import java.util.UUID class ProfileLoadModel( override val uuid: UUID, - override val sRated: Power, + override val sRated: ApparentPower, override val cosPhiRated: Double, override val qControl: QControl, private val loadProfileStore: LoadProfileStore, diff --git a/src/main/scala/edu/ie3/simona/model/participant2/load/RandomLoadModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/load/RandomLoadModel.scala index 3935fa9b45..3735d82d0f 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/load/RandomLoadModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/load/RandomLoadModel.scala @@ -21,6 +21,7 @@ import edu.ie3.simona.model.participant2.ParticipantModel.{ } import edu.ie3.simona.util.TickUtil import edu.ie3.util.TimeUtil +import edu.ie3.util.scala.quantities.ApparentPower import squants.energy.{KilowattHours, Kilowatts, Watts} import squants.{Dimensionless, Power} @@ -31,7 +32,7 @@ import scala.util.Random class RandomLoadModel( override val uuid: UUID, - override val sRated: Power, + override val sRated: ApparentPower, override val cosPhiRated: Double, override val qControl: QControl, private val referenceScalingFactor: Double, diff --git a/src/test/scala/edu/ie3/simona/model/participant2/ChargingHelperSpec.scala b/src/test/scala/edu/ie3/simona/model/participant2/ChargingHelperSpec.scala index 55b3033e30..d48a8399ed 100644 --- a/src/test/scala/edu/ie3/simona/model/participant2/ChargingHelperSpec.scala +++ b/src/test/scala/edu/ie3/simona/model/participant2/ChargingHelperSpec.scala @@ -7,11 +7,11 @@ package edu.ie3.simona.model.participant2 import edu.ie3.simona.test.common.UnitSpec -import squants.energy.{KilowattHours, Kilowatts} +import squants.energy.{Energy, KilowattHours, Kilowatts} class ChargingHelperSpec extends UnitSpec { - private implicit val energyTolerance: squants.Energy = KilowattHours(1e-10) + private implicit val energyTolerance: Energy = KilowattHours(1e-10) "A ChargingHelper" should { diff --git a/src/test/scala/edu/ie3/simona/model/participant2/PvModelSpec.scala b/src/test/scala/edu/ie3/simona/model/participant2/PvModelSpec.scala index 7930a01e6c..b2175e852e 100644 --- a/src/test/scala/edu/ie3/simona/model/participant2/PvModelSpec.scala +++ b/src/test/scala/edu/ie3/simona/model/participant2/PvModelSpec.scala @@ -13,10 +13,14 @@ import edu.ie3.datamodel.models.input.{NodeInput, OperatorInput} import edu.ie3.datamodel.models.voltagelevels.GermanVoltageLevelUtils import edu.ie3.simona.test.common.{DefaultTestData, UnitSpec} import edu.ie3.util.quantities.PowerSystemUnits._ -import edu.ie3.util.scala.quantities.{Irradiation, WattHoursPerSquareMeter} +import edu.ie3.util.scala.quantities.{ + ApparentPower, + Irradiation, + Kilovoltamperes, + WattHoursPerSquareMeter, +} import org.locationtech.jts.geom.{Coordinate, GeometryFactory, Point} import org.scalatest.GivenWhenThen -import squants.energy.{Kilowatts, Power} import squants.space.{Angle, Degrees, Radians} import tech.units.indriya.quantity.Quantities.getQuantity import tech.units.indriya.unit.Units._ @@ -83,7 +87,9 @@ 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 powerTolerance: Power = Kilowatts(1e-10) + private implicit val apparentPowerTolerance: ApparentPower = Kilovoltamperes( + 1e-10 + ) "A PV Model" should { "have sMax set to be 10% higher than its sRated" in { diff --git a/src/test/scala/edu/ie3/simona/model/participant2/WecModelSpec.scala b/src/test/scala/edu/ie3/simona/model/participant2/WecModelSpec.scala index 9937c142e5..abdf886a87 100644 --- a/src/test/scala/edu/ie3/simona/model/participant2/WecModelSpec.scala +++ b/src/test/scala/edu/ie3/simona/model/participant2/WecModelSpec.scala @@ -81,7 +81,7 @@ class WecModelSpec extends UnitSpec with DefaultTestData { val wecModel = WecModel.apply(inputModel) wecModel.uuid shouldBe inputModel.getUuid wecModel.cosPhiRated shouldBe typeInput.getCosPhiRated - wecModel.sRated.toWatts shouldBe (typeInput.getsRated.toSystemUnit.getValue + wecModel.sRated.toVoltamperes shouldBe (typeInput.getsRated.toSystemUnit.getValue .doubleValue() +- 1e-5) } diff --git a/src/test/scala/edu/ie3/simona/model/participant2/evcs/EvcsModelSpec.scala b/src/test/scala/edu/ie3/simona/model/participant2/evcs/EvcsModelSpec.scala index 4cf40adceb..1e2dee5c62 100644 --- a/src/test/scala/edu/ie3/simona/model/participant2/evcs/EvcsModelSpec.scala +++ b/src/test/scala/edu/ie3/simona/model/participant2/evcs/EvcsModelSpec.scala @@ -7,7 +7,7 @@ package edu.ie3.simona.model.participant2.evcs import edu.ie3.datamodel.models.result.system.{EvResult, EvcsResult} -import edu.ie3.simona.agent.participant.data.Data.PrimaryData.ApparentPower +import edu.ie3.simona.agent.participant.data.Data.PrimaryData.ComplexPower import edu.ie3.simona.config.SimonaConfig.EvcsRuntimeConfig import edu.ie3.simona.model.participant.evcs.EvModelWrapper import edu.ie3.simona.model.participant2.ParticipantModel.ModelChangeIndicator @@ -200,8 +200,8 @@ class EvcsModelSpec actualEv.uuid shouldBe ev.uuid actualEv.id shouldBe ev.id - actualEv.sRatedAc shouldBe ev.sRatedAc - actualEv.sRatedDc shouldBe ev.sRatedDc + actualEv.pRatedAc shouldBe ev.pRatedAc + actualEv.pRatedDc shouldBe ev.pRatedDc actualEv.eStorage shouldBe ev.eStorage actualEv.storedEnergy should approximate( KilowattHours(expectedStored) @@ -255,7 +255,7 @@ class EvcsModelSpec state, None, currentOperatingPoint, - ApparentPower(Kilowatts(5), Kilovars(0.5)), + ComplexPower(Kilowatts(5), Kilovars(0.5)), resultDateTime, ) @@ -329,7 +329,7 @@ class EvcsModelSpec state, Some(lastOperatingPoint), currentOperatingPoint, - ApparentPower(Kilowatts(evcsP), Kilovars(evcsQ)), + ComplexPower(Kilowatts(evcsP), Kilovars(evcsQ)), resultDateTime, ) @@ -656,7 +656,7 @@ class EvcsModelSpec modelUuid shouldBe evcsModel.uuid refPower should approximate(Kilowatts(5.0)) // one hour left minPower should approximate(Kilowatts(0d)) // no v2g allowed! - maxPower should approximate(ev1.sRatedAc) + maxPower should approximate(ev1.pRatedAc) } } diff --git a/src/test/scala/edu/ie3/simona/model/participant2/evcs/MaximumPowerChargingSpec.scala b/src/test/scala/edu/ie3/simona/model/participant2/evcs/MaximumPowerChargingSpec.scala index 92fb49009e..85876fafac 100644 --- a/src/test/scala/edu/ie3/simona/model/participant2/evcs/MaximumPowerChargingSpec.scala +++ b/src/test/scala/edu/ie3/simona/model/participant2/evcs/MaximumPowerChargingSpec.scala @@ -76,7 +76,7 @@ class MaximumPowerChargingSpec extends UnitSpec with TableDrivenPropertyChecks { ) chargingMap shouldBe Map( - ev.uuid -> ev.sRatedAc + ev.uuid -> ev.pRatedAc ) } diff --git a/src/test/scala/edu/ie3/simona/model/participant2/evcs/MockEvcsChargingProperties.scala b/src/test/scala/edu/ie3/simona/model/participant2/evcs/MockEvcsChargingProperties.scala index 25469a4cd0..1d393e6c15 100644 --- a/src/test/scala/edu/ie3/simona/model/participant2/evcs/MockEvcsChargingProperties.scala +++ b/src/test/scala/edu/ie3/simona/model/participant2/evcs/MockEvcsChargingProperties.scala @@ -11,7 +11,7 @@ import squants.energy.Kilowatts object MockEvcsChargingProperties extends EvcsChargingProperties { - override val sRated: Power = Kilowatts(43) + override protected val pRated: Power = Kilowatts(43) override val currentType: ElectricCurrentType = ElectricCurrentType.AC override val lowestEvSoc: Double = 0.2 From ba69944fa64ccb55ca4a476810a31ed9d2f34884 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Fri, 22 Nov 2024 14:01:16 +0100 Subject: [PATCH 42/77] EVCS: Implement and test request handling Signed-off-by: Sebastian Peter --- .../agent/participant/evcs/EvcsAgent.scala | 8 +- .../model/participant2/evcs/EvcsModel.scala | 43 +++++++- .../messages/services/EvMessage.scala | 14 ++- .../simona/service/ev/ExtEvDataService.scala | 4 +- .../EvcsAgentModelCalculationSpec.scala | 18 ++-- .../participant2/evcs/EvcsModelSpec.scala | 98 ++++++++++++++++++- .../service/ev/ExtEvDataServiceSpec.scala | 8 +- 7 files changed, 166 insertions(+), 27 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/agent/participant/evcs/EvcsAgent.scala b/src/main/scala/edu/ie3/simona/agent/participant/evcs/EvcsAgent.scala index 5b37ae70e3..069906d402 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant/evcs/EvcsAgent.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant/evcs/EvcsAgent.scala @@ -85,7 +85,7 @@ class EvcsAgent( when(Idle) { case Event( - EvFreeLotsRequest(tick), + EvFreeLotsRequest(tick, _), modelBaseStateData: ParticipantModelBaseStateData[ ComplexPower, EvcsRelevantData, @@ -97,7 +97,7 @@ class EvcsAgent( stay() case Event( - DepartingEvsRequest(tick, departingEvs), + DepartingEvsRequest(tick, departingEvs, _), modelBaseStateData: ParticipantModelBaseStateData[ ComplexPower, EvcsRelevantData, @@ -115,7 +115,7 @@ class EvcsAgent( // in case the activation has arrived first case Event( - EvFreeLotsRequest(tick), + EvFreeLotsRequest(tick, _), stateData: DataCollectionStateData[ComplexPower], ) => stateData.baseStateData match { @@ -134,7 +134,7 @@ class EvcsAgent( } case Event( - DepartingEvsRequest(tick, departingEvs), + DepartingEvsRequest(tick, departingEvs, _), stateData: DataCollectionStateData[ComplexPower], ) => stateData.baseStateData match { diff --git a/src/main/scala/edu/ie3/simona/model/participant2/evcs/EvcsModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/evcs/EvcsModel.scala index bf55446b02..a4506feee7 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/evcs/EvcsModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/evcs/EvcsModel.scala @@ -36,7 +36,13 @@ import edu.ie3.simona.model.participant2.evcs.EvcsModel.{ import edu.ie3.simona.model.participant2.{ChargingHelper, ParticipantModel} import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage import edu.ie3.simona.ontology.messages.flex.MinMaxFlexibilityMessage.ProvideMinMaxFlexOptions -import edu.ie3.simona.ontology.messages.services.EvMessage.ArrivingEvs +import edu.ie3.simona.ontology.messages.services.EvMessage.{ + ArrivingEvs, + DepartingEvsRequest, + DepartingEvsResponse, + EvFreeLotsRequest, + FreeLotsResponse, +} import edu.ie3.simona.service.ServiceType import edu.ie3.util.quantities.PowerSystemUnits.KILOVOLTAMPERE import edu.ie3.util.quantities.QuantityUtils.RichQuantityDouble @@ -500,8 +506,39 @@ class EvcsModel private ( state: EvcsState, ctx: ActorContext[ParticipantAgent.Request], msg: ParticipantRequest, - ): EvcsState = { - ??? // todo + ): EvcsState = msg match { + case freeLotsRequest: EvFreeLotsRequest => + val stayingEvsCount = + state.evs.count(_.departureTick > freeLotsRequest.tick) + + freeLotsRequest.replyTo ! FreeLotsResponse( + uuid, + chargingPoints - stayingEvsCount, + ) + + state + + case departingEvsRequest: DepartingEvsRequest => + // create a set for faster containment checking + val requestedEvs = departingEvsRequest.departingEvs.toSet + + val (departingEvs, stayingEvs) = state.evs.partition { ev => + requestedEvs.contains(ev.uuid) + } + + if (departingEvs.size != requestedEvs.size) { + requestedEvs.foreach { requestedUuid => + if (!departingEvs.exists(_.uuid == requestedUuid)) + ctx.log.warn( + s"EV $requestedUuid should depart from this station (according to external simulation), but has not been parked here." + ) + } + } + + departingEvsRequest.replyTo ! DepartingEvsResponse(uuid, departingEvs) + + state.copy(evs = stayingEvs) + } /* HELPER METHODS */ diff --git a/src/main/scala/edu/ie3/simona/ontology/messages/services/EvMessage.scala b/src/main/scala/edu/ie3/simona/ontology/messages/services/EvMessage.scala index 1e8ae341a9..dde0b0b422 100644 --- a/src/main/scala/edu/ie3/simona/ontology/messages/services/EvMessage.scala +++ b/src/main/scala/edu/ie3/simona/ontology/messages/services/EvMessage.scala @@ -7,6 +7,7 @@ package edu.ie3.simona.ontology.messages.services import edu.ie3.simona.agent.participant.data.Data.SecondaryData +import edu.ie3.simona.agent.participant2.ParticipantAgent.ParticipantRequest import edu.ie3.simona.model.participant.evcs.EvModelWrapper import edu.ie3.simona.ontology.messages.services.ServiceMessage.{ ProvisionMessage, @@ -55,8 +56,11 @@ object EvMessage { * * @param tick * The latest tick that the data is requested for + * @param replyTo + * The actor to receive the response */ - final case class EvFreeLotsRequest(tick: Long) + final case class EvFreeLotsRequest(tick: Long, replyTo: ActorRef) + extends ParticipantRequest /** Requests EV models of departing EVs with given UUIDs * @@ -64,8 +68,14 @@ object EvMessage { * The latest tick that the data is requested for * @param departingEvs * The UUIDs of EVs that are requested + * @param replyTo + * The actor to receive the response */ - final case class DepartingEvsRequest(tick: Long, departingEvs: Seq[UUID]) + final case class DepartingEvsRequest( + tick: Long, + departingEvs: Seq[UUID], + replyTo: ActorRef, + ) extends ParticipantRequest /** Holds arrivals for one charging station * diff --git a/src/main/scala/edu/ie3/simona/service/ev/ExtEvDataService.scala b/src/main/scala/edu/ie3/simona/service/ev/ExtEvDataService.scala index 680c6050b1..27ce041205 100644 --- a/src/main/scala/edu/ie3/simona/service/ev/ExtEvDataService.scala +++ b/src/main/scala/edu/ie3/simona/service/ev/ExtEvDataService.scala @@ -229,7 +229,7 @@ class ExtEvDataService(override val scheduler: ActorRef) serviceStateData: ExtEvStateData ): (ExtEvStateData, Option[Long]) = { serviceStateData.uuidToActorRef.foreach { case (_, evcsActor) => - evcsActor ! EvFreeLotsRequest(tick) + evcsActor ! EvFreeLotsRequest(tick, context.self) } val freeLots = @@ -261,7 +261,7 @@ class ExtEvDataService(override val scheduler: ActorRef) requestedDepartingEvs.flatMap { case (evcs, departingEvs) => serviceStateData.uuidToActorRef.get(evcs) match { case Some(evcsActor) => - evcsActor ! DepartingEvsRequest(tick, departingEvs) + evcsActor ! DepartingEvsRequest(tick, departingEvs, context.self) Some(evcs) 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 4d3c444156..087fa24e2e 100644 --- a/src/test/scala/edu/ie3/simona/agent/participant/EvcsAgentModelCalculationSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/participant/EvcsAgentModelCalculationSpec.scala @@ -756,7 +756,7 @@ class EvcsAgentModelCalculationSpec /* Send out public evcs request */ evService.send( evcsAgent, - EvFreeLotsRequest(0L), + EvFreeLotsRequest(0L, evService.ref), ) evService.expectMsg( @@ -788,7 +788,7 @@ class EvcsAgentModelCalculationSpec /* Ask for public evcs lot count again with a later tick */ evService.send( evcsAgent, - EvFreeLotsRequest(3600), + EvFreeLotsRequest(3600, evService.ref), ) // this time, only one is still free @@ -933,7 +933,7 @@ class EvcsAgentModelCalculationSpec // departures first evService.send( evcsAgent, - DepartingEvsRequest(3600, Seq(evA.getUuid)), + DepartingEvsRequest(3600, Seq(evA.getUuid), evService.ref), ) evService.expectMsgType[DepartingEvsResponse] match { case DepartingEvsResponse(evcs, evModels) => @@ -968,7 +968,7 @@ class EvcsAgentModelCalculationSpec // departures first evService.send( evcsAgent, - DepartingEvsRequest(7200, Seq(evB.getUuid)), + DepartingEvsRequest(7200, Seq(evB.getUuid), evService.ref), ) evService.expectMsgType[DepartingEvsResponse] match { case DepartingEvsResponse(evcs, evModels) => @@ -1410,7 +1410,7 @@ class EvcsAgentModelCalculationSpec // departure first evService.send( evcsAgent, - DepartingEvsRequest(4500, Seq(ev900.uuid)), + DepartingEvsRequest(4500, Seq(ev900.uuid), evService.ref), ) evService.expectMsgPF() { case DepartingEvsResponse(uuid, evs) => @@ -1907,7 +1907,7 @@ class EvcsAgentModelCalculationSpec // departure first evService.send( evcsAgent, - DepartingEvsRequest(36000, Seq(ev900.uuid)), + DepartingEvsRequest(36000, Seq(ev900.uuid), evService.ref), ) evService.expectMsgPF() { case DepartingEvsResponse(uuid, evs) => @@ -2228,7 +2228,7 @@ class EvcsAgentModelCalculationSpec // TICK 3600: ev900 leaves evService.send( evcsAgent, - DepartingEvsRequest(3600, Seq(ev900.uuid)), + DepartingEvsRequest(3600, Seq(ev900.uuid), evService.ref), ) evService.expectMsgType[DepartingEvsResponse] match { @@ -2282,7 +2282,7 @@ class EvcsAgentModelCalculationSpec evService.send( evcsAgent, - DepartingEvsRequest(4500, Seq(ev1800.uuid)), + DepartingEvsRequest(4500, Seq(ev1800.uuid), evService.ref), ) evService.expectMsgType[DepartingEvsResponse] match { @@ -2331,7 +2331,7 @@ class EvcsAgentModelCalculationSpec evService.send( evcsAgent, - DepartingEvsRequest(5400, Seq(ev2700.uuid)), + DepartingEvsRequest(5400, Seq(ev2700.uuid), evService.ref), ) evService.expectMsgType[DepartingEvsResponse] match { diff --git a/src/test/scala/edu/ie3/simona/model/participant2/evcs/EvcsModelSpec.scala b/src/test/scala/edu/ie3/simona/model/participant2/evcs/EvcsModelSpec.scala index 1e2dee5c62..9344aaa545 100644 --- a/src/test/scala/edu/ie3/simona/model/participant2/evcs/EvcsModelSpec.scala +++ b/src/test/scala/edu/ie3/simona/model/participant2/evcs/EvcsModelSpec.scala @@ -8,6 +8,8 @@ package edu.ie3.simona.model.participant2.evcs import edu.ie3.datamodel.models.result.system.{EvResult, EvcsResult} import edu.ie3.simona.agent.participant.data.Data.PrimaryData.ComplexPower +import edu.ie3.simona.agent.participant2.ParticipantAgent +import edu.ie3.simona.agent.participant2.ParticipantAgent.ParticipantRequest import edu.ie3.simona.config.SimonaConfig.EvcsRuntimeConfig import edu.ie3.simona.model.participant.evcs.EvModelWrapper import edu.ie3.simona.model.participant2.ParticipantModel.ModelChangeIndicator @@ -17,6 +19,13 @@ import edu.ie3.simona.model.participant2.evcs.EvcsModel.{ EvcsState, } import edu.ie3.simona.ontology.messages.flex.MinMaxFlexibilityMessage.ProvideMinMaxFlexOptions +import edu.ie3.simona.ontology.messages.services.EvMessage.{ + DepartingEvsRequest, + DepartingEvsResponse, + EvFreeLotsRequest, + EvResponseMessage, + FreeLotsResponse, +} import edu.ie3.simona.test.common.UnitSpec import edu.ie3.simona.test.common.input.EvcsInputTestData import edu.ie3.simona.test.common.model.MockEvModel @@ -25,13 +34,18 @@ import edu.ie3.util.TimeUtil import edu.ie3.util.quantities.QuantityUtils.RichQuantityDouble import edu.ie3.util.scala.quantities.DefaultQuantities.zeroKW import edu.ie3.util.scala.quantities.Kilovars +import org.apache.pekko.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit +import org.apache.pekko.actor.typed.Behavior +import org.apache.pekko.actor.typed.scaladsl.Behaviors +import org.apache.pekko.actor.typed.scaladsl.adapter.TypedActorRefOps import squants.energy.{KilowattHours, Kilowatts} import java.time.ZonedDateTime import java.util.UUID class EvcsModelSpec - extends UnitSpec + extends ScalaTestWithActorTestKit + with UnitSpec with TableDrivenHelper with EvcsInputTestData { @@ -795,8 +809,86 @@ class EvcsModelSpec } - } + "reply to requests" when { + val evcsModel = createModel("constantpower") + + val evModel = EvModelWrapper( + new MockEvModel( + UUID.randomUUID(), + "Mock EV", + 10.0.asKiloWatt, // AC is relevant, + 20.0.asKiloWatt, // DC is not + 20.0.asKiloWattHour, + 5.0.asKiloWattHour, + 10800L, + ) + ) - // TODO testing requests + def testAgent( + model: EvcsModel, + state: EvcsState, + ): Behavior[ParticipantAgent.Request] = Behaviors.receivePartial { + case (ctx, request: ParticipantRequest) => + val newState = model.handleRequest( + state, + ctx, + request, + ) + + testAgent(model, newState) + } + + "no EVs are parked" in { + val service = createTestProbe[EvResponseMessage]() + val currentTick = 0L + + val startingState = EvcsState(Seq.empty, currentTick) + val agent = spawn(testAgent(evcsModel, startingState)) + + agent ! EvFreeLotsRequest(currentTick, service.ref.toClassic) + service.expectMessage(FreeLotsResponse(evcsModel.uuid, 2)) + } + + "one EV is parked, departing later" in { + val service = createTestProbe[EvResponseMessage]() + val currentTick = 0L + + val startingState = EvcsState(Seq(evModel), currentTick) + val agent = spawn(testAgent(evcsModel, startingState)) + + agent ! EvFreeLotsRequest(currentTick, service.ref.toClassic) + service.expectMessage(FreeLotsResponse(evcsModel.uuid, 1)) + + // ev is supposed to be departing later, but we collect it here for testing purposes + agent ! DepartingEvsRequest( + currentTick, + Seq(evModel.uuid), + service.ref.toClassic, + ) + service.expectMessage( + DepartingEvsResponse(evcsModel.uuid, Seq(evModel)) + ) + + agent ! EvFreeLotsRequest(currentTick, service.ref.toClassic) + // now, ev should be gone + service.expectMessage(FreeLotsResponse(evcsModel.uuid, 2)) + } + + "one EV is parked, departing now" in { + val service = createTestProbe[EvResponseMessage]() + // ev is supposed to be departing at this tick + val currentTick = 10800L + + val startingState = EvcsState(Seq(evModel), currentTick) + val agent = spawn(testAgent(evcsModel, startingState)) + + agent ! EvFreeLotsRequest(currentTick, service.ref.toClassic) + // ev should not count, since it is departing now + service.expectMessage(FreeLotsResponse(evcsModel.uuid, 2)) + } + + } + + } } diff --git a/src/test/scala/edu/ie3/simona/service/ev/ExtEvDataServiceSpec.scala b/src/test/scala/edu/ie3/simona/service/ev/ExtEvDataServiceSpec.scala index 8e6cf9e07d..6bb66dcbd3 100644 --- a/src/test/scala/edu/ie3/simona/service/ev/ExtEvDataServiceSpec.scala +++ b/src/test/scala/edu/ie3/simona/service/ev/ExtEvDataServiceSpec.scala @@ -255,11 +255,11 @@ class ExtEvDataServiceSpec scheduler.send(evService, Activation(tick)) evcs1.expectMsg( - EvFreeLotsRequest(tick) + EvFreeLotsRequest(tick, evService.ref) ) evcs2.expectMsg( - EvFreeLotsRequest(tick) + EvFreeLotsRequest(tick, evService.ref) ) scheduler.expectMsg(Completion(evService.toTyped)) @@ -479,10 +479,10 @@ class ExtEvDataServiceSpec scheduler.send(evService, Activation(tick)) evcs1.expectMsg( - DepartingEvsRequest(tick, scala.collection.immutable.Seq(evA.getUuid)) + DepartingEvsRequest(tick, Seq(evA.getUuid), evService.ref) ) evcs2.expectMsg( - DepartingEvsRequest(tick, scala.collection.immutable.Seq(evB.getUuid)) + DepartingEvsRequest(tick, Seq(evB.getUuid), evService.ref) ) scheduler.expectMsg(Completion(evService.toTyped)) From 0277acec92a6ff7fd2f279db37fc0b8ed0cc5450 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Fri, 22 Nov 2024 14:23:16 +0100 Subject: [PATCH 43/77] Introducing participant refs Signed-off-by: Sebastian Peter --- .../participant2/ParticipantAgentInit.scala | 77 ++++++++++++------- 1 file changed, 48 insertions(+), 29 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgentInit.scala b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgentInit.scala index c7c30932e6..dbce3f9cac 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgentInit.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgentInit.scala @@ -10,6 +10,7 @@ import edu.ie3.datamodel.models.input.system.SystemParticipantInput import edu.ie3.simona.agent.grid.GridAgent import edu.ie3.simona.agent.participant2.ParticipantAgent._ import edu.ie3.simona.config.SimonaConfig.BaseRuntimeConfig +import edu.ie3.simona.event.ResultEvent import edu.ie3.simona.exceptions.CriticalFailureException import edu.ie3.simona.model.participant2.ParticipantModelShell import edu.ie3.simona.ontology.messages.SchedulerMessage.{ @@ -19,6 +20,7 @@ import edu.ie3.simona.ontology.messages.SchedulerMessage.{ import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage._ import edu.ie3.simona.ontology.messages.services.ServiceMessage.PrimaryServiceRegistrationMessage import edu.ie3.simona.ontology.messages.{Activation, SchedulerMessage} +import edu.ie3.simona.service.ServiceType import edu.ie3.simona.util.SimonaConstants.INIT_SIM_TICK import org.apache.pekko.actor.typed.scaladsl.Behaviors import org.apache.pekko.actor.typed.{ActorRef, Behavior} @@ -32,11 +34,29 @@ object ParticipantAgentInit { // wait for reply and then create // GridAdapter + /** Container class, that gather together reference to relevant entities, that + * represent the environment in the simulation + * + * @param gridAgent + * Reference to the grid agent + * @param primaryServiceProxy + * Reference to the primary service proxy + * @param services + * References to services by service type + * @param resultListener + * Reference to the result listeners + */ + final case class ParticipantRefs( + gridAgent: ActorRef[GridAgent.Request], + primaryServiceProxy: ClassicRef, + services: Map[ServiceType, ClassicRef], + resultListener: Iterable[ActorRef[ResultEvent]], + ) + def apply( participantInput: SystemParticipantInput, config: BaseRuntimeConfig, - primaryServiceProxy: ClassicRef, - gridAgentRef: ActorRef[GridAgent.Request], + participantRefs: ParticipantRefs, expectedPowerRequestTick: Long, simulationStartDate: ZonedDateTime, simulationEndDate: ZonedDateTime, @@ -78,8 +98,7 @@ object ParticipantAgentInit { uninitialized( participantInput, config, - primaryServiceProxy, - gridAgentRef, + participantRefs, expectedPowerRequestTick, simulationStartDate, simulationEndDate, @@ -90,8 +109,7 @@ object ParticipantAgentInit { private def uninitialized( participantInput: SystemParticipantInput, config: BaseRuntimeConfig, - primaryServiceProxy: ClassicRef, - gridAgentRef: ActorRef[GridAgent.Request], + participantRefs: ParticipantRefs, expectedPowerRequestTick: Long, simulationStartDate: ZonedDateTime, simulationEndDate: ZonedDateTime, @@ -99,14 +117,14 @@ object ParticipantAgentInit { ): Behavior[Request] = Behaviors.receiveMessagePartial { case activation: ActivationRequest if activation.tick == INIT_SIM_TICK => - primaryServiceProxy ! PrimaryServiceRegistrationMessage( + participantRefs.primaryServiceProxy ! PrimaryServiceRegistrationMessage( participantInput.getUuid ) waitingForPrimaryProxy( participantInput, config, - gridAgentRef, + participantRefs, expectedPowerRequestTick, simulationStartDate, simulationEndDate, @@ -118,7 +136,7 @@ object ParticipantAgentInit { private def waitingForPrimaryProxy( participantInput: SystemParticipantInput, config: BaseRuntimeConfig, - gridAgentRef: ActorRef[GridAgent.Request], + participantRefs: ParticipantRefs, expectedPowerRequestTick: Long, simulationStartDate: ZonedDateTime, simulationEndDate: ZonedDateTime, @@ -136,7 +154,7 @@ object ParticipantAgentInit { simulationEndDate, ), expectedFirstData, - gridAgentRef, + participantRefs, expectedPowerRequestTick, parentData, firstDataTick, @@ -151,7 +169,7 @@ object ParticipantAgentInit { ) val requiredServiceTypes = - modelShell.model.getRequiredSecondaryServices.toSeq + modelShell.model.getRequiredSecondaryServices.toSet if (requiredServiceTypes.isEmpty) { // Models that do not use secondary data always start at tick 0 @@ -160,18 +178,24 @@ object ParticipantAgentInit { completeInitialization( modelShell, Map.empty, - gridAgentRef, + participantRefs, expectedPowerRequestTick, parentData, firstTick, ) } else { - // TODO request service actorrefs - val requiredServices = ??? + val requiredServices = requiredServiceTypes.map(serviceType => + participantRefs.services.getOrElse( + serviceType, + throw new CriticalFailureException( + s"Service of type $serviceType is not available." + ), + ) + ) waitingForServices( modelShell, - gridAgentRef, + participantRefs, expectedPowerRequestTick, requiredServices, parentData = parentData, @@ -181,7 +205,7 @@ object ParticipantAgentInit { private def waitingForServices( modelShell: ParticipantModelShell[_, _, _], - gridAgentRef: ActorRef[GridAgent.Request], + participantRefs: ParticipantRefs, expectedPowerRequestTick: Long, expectedRegistrations: Set[ClassicRef], expectedFirstData: Map[ClassicRef, Long] = Map.empty, @@ -211,7 +235,7 @@ object ParticipantAgentInit { completeInitialization( modelShell, newExpectedFirstData, - gridAgentRef, + participantRefs, expectedPowerRequestTick, parentData, firstTick, @@ -219,7 +243,7 @@ object ParticipantAgentInit { } else waitingForServices( modelShell, - gridAgentRef, + participantRefs, expectedPowerRequestTick, newExpectedRegistrations, newExpectedFirstData, @@ -229,19 +253,11 @@ object ParticipantAgentInit { /** Completes initialization activation and creates actual * [[ParticipantAgent]] - * - * @param modelShell - * @param expectedData - * @param gridAgentRef - * @param expectedPowerRequestTick - * @param parentData - * @param firstTick - * @return */ private def completeInitialization( modelShell: ParticipantModelShell[_, _, _], expectedData: Map[ClassicRef, Long], - gridAgentRef: ActorRef[GridAgent.Request], + participantRefs: ParticipantRefs, expectedPowerRequestTick: Long, parentData: Either[SchedulerData, FlexControlledData], firstTick: Long, @@ -263,8 +279,11 @@ object ParticipantAgentInit { ParticipantAgent( modelShell, ParticipantInputHandler(expectedData), - ParticipantGridAdapter(gridAgentRef, expectedPowerRequestTick), - ???, + ParticipantGridAdapter( + participantRefs.gridAgent, + expectedPowerRequestTick, + ), + participantRefs.resultListener, parentData, ) } From ff447737310d180ecb4c93f9aac05fceeaa4f4c9 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Fri, 22 Nov 2024 14:31:50 +0100 Subject: [PATCH 44/77] Adding EVCS to init Signed-off-by: Sebastian Peter --- .../participant2/ParticipantModelInit.scala | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelInit.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelInit.scala index b2e83d487a..df8550a1df 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelInit.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelInit.scala @@ -10,18 +10,11 @@ import edu.ie3.datamodel.models.input.system.SystemParticipantInput.SystemPartic import edu.ie3.datamodel.models.input.system._ import edu.ie3.datamodel.models.result.system.SystemParticipantResult import edu.ie3.simona.agent.participant.data.Data.PrimaryData -import edu.ie3.simona.config.SimonaConfig.{ - BaseRuntimeConfig, - LoadRuntimeConfig, - StorageRuntimeConfig, -} +import edu.ie3.simona.config.SimonaConfig.{BaseRuntimeConfig, EvcsRuntimeConfig, LoadRuntimeConfig, StorageRuntimeConfig} import edu.ie3.simona.exceptions.CriticalFailureException -import edu.ie3.simona.model.participant2.ParticipantModel.{ - ModelState, - OperatingPoint, - OperationRelevantData, -} +import edu.ie3.simona.model.participant2.ParticipantModel.{ModelState, OperatingPoint, OperationRelevantData} import edu.ie3.simona.model.participant2.PrimaryDataParticipantModel.PrimaryResultFunc +import edu.ie3.simona.model.participant2.evcs.EvcsModel import edu.ie3.simona.model.participant2.load.LoadModel import java.time.ZonedDateTime @@ -65,6 +58,10 @@ object ParticipantModelInit { val model = StorageModel(input, config) val state = model.getInitialState(config) ParticipantModelInitContainer(model, state) + case (input: EvcsInput, config: EvcsRuntimeConfig) => + val model = EvcsModel(input, config) + val state = model.getInitialState + ParticipantModelInitContainer(model, state) case (input, config) => throw new CriticalFailureException( s"Handling the input model ${input.getClass.getSimpleName} or " + From 120f1f39fc815d10e9dcb19f6ffa86f81a99ec7c Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Fri, 22 Nov 2024 15:38:49 +0100 Subject: [PATCH 45/77] Providing PrimaryDataMeta Signed-off-by: Sebastian Peter --- .../agent/participant2/ParticipantAgent.scala | 19 +++++++++++++++++- .../participant2/ParticipantAgentInit.scala | 10 +++++++++- .../participant2/ParticipantModelInit.scala | 20 ++++++++++++++----- .../participant2/ParticipantModelShell.scala | 12 ++++++++--- 4 files changed, 51 insertions(+), 10 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgent.scala b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgent.scala index 54e279e1bd..501fb9d76f 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgent.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgent.scala @@ -12,7 +12,11 @@ import edu.ie3.simona.agent.grid.GridAgentMessages.{ AssetPowerUnchangedMessage, } import edu.ie3.simona.agent.participant.data.Data -import edu.ie3.simona.agent.participant.data.Data.SecondaryData +import edu.ie3.simona.agent.participant.data.Data.{ + PrimaryData, + PrimaryDataMeta, + SecondaryData, +} import edu.ie3.simona.event.ResultEvent import edu.ie3.simona.event.ResultEvent.ParticipantResultEvent import edu.ie3.simona.exceptions.CriticalFailureException @@ -27,6 +31,8 @@ import org.apache.pekko.actor.typed.{ActorRef, Behavior} import org.apache.pekko.actor.{ActorRef => ClassicRef} import squants.{Dimensionless, Each} +import scala.reflect.ClassTag + object ParticipantAgent { sealed trait Request @@ -70,6 +76,17 @@ object ParticipantAgent { firstDataTick: Long, ) extends RegistrationResponseMessage + /** Message, that is used to confirm a successful registration with primary + * service + */ + final case class PrimaryRegistrationSuccessfulMessage[ + P <: PrimaryData: ClassTag + ]( + override val serviceRef: ClassicRef, + firstDataTick: Long, + primaryDataMeta: PrimaryDataMeta[P], + ) extends RegistrationResponseMessage + /** Message, that is used to announce a failed registration */ final case class RegistrationFailedMessage( diff --git a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgentInit.scala b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgentInit.scala index dbce3f9cac..9eea078cbb 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgentInit.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgentInit.scala @@ -143,13 +143,21 @@ object ParticipantAgentInit { parentData: Either[SchedulerData, FlexControlledData], ): Behavior[Request] = Behaviors.receivePartial { - case (_, RegistrationSuccessfulMessage(serviceRef, firstDataTick)) => + case ( + _, + PrimaryRegistrationSuccessfulMessage( + serviceRef, + firstDataTick, + primaryDataMeta, + ), + ) => val expectedFirstData = Map(serviceRef -> firstDataTick) completeInitialization( ParticipantModelShell.createForPrimaryData( participantInput, config, + primaryDataMeta, simulationStartDate, simulationEndDate, ), diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelInit.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelInit.scala index df8550a1df..f25bc44693 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelInit.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelInit.scala @@ -9,10 +9,19 @@ package edu.ie3.simona.model.participant2 import edu.ie3.datamodel.models.input.system.SystemParticipantInput.SystemParticipantInputCopyBuilder import edu.ie3.datamodel.models.input.system._ import edu.ie3.datamodel.models.result.system.SystemParticipantResult -import edu.ie3.simona.agent.participant.data.Data.PrimaryData -import edu.ie3.simona.config.SimonaConfig.{BaseRuntimeConfig, EvcsRuntimeConfig, LoadRuntimeConfig, StorageRuntimeConfig} +import edu.ie3.simona.agent.participant.data.Data.{PrimaryData, PrimaryDataMeta} +import edu.ie3.simona.config.SimonaConfig.{ + BaseRuntimeConfig, + EvcsRuntimeConfig, + LoadRuntimeConfig, + StorageRuntimeConfig, +} import edu.ie3.simona.exceptions.CriticalFailureException -import edu.ie3.simona.model.participant2.ParticipantModel.{ModelState, OperatingPoint, OperationRelevantData} +import edu.ie3.simona.model.participant2.ParticipantModel.{ + ModelState, + OperatingPoint, + OperationRelevantData, +} import edu.ie3.simona.model.participant2.PrimaryDataParticipantModel.PrimaryResultFunc import edu.ie3.simona.model.participant2.evcs.EvcsModel import edu.ie3.simona.model.participant2.load.LoadModel @@ -74,6 +83,7 @@ object ParticipantModelInit { def createPrimaryModel[P <: PrimaryData: ClassTag]( participantInput: SystemParticipantInput, modelConfig: BaseRuntimeConfig, + primaryDataMeta: PrimaryDataMeta[P], ): ParticipantModelInitContainer[ _ <: OperatingPoint, _ <: ModelState, @@ -94,13 +104,13 @@ object ParticipantModelInit { physicalModel.createPrimaryDataResult(data, dateTime) } - val primaryDataModel = new PrimaryDataParticipantModel[P]( + val primaryDataModel = new PrimaryDataParticipantModel( physicalModel.uuid, physicalModel.sRated, physicalModel.cosPhiRated, physicalModel.qControl, primaryResultFunc, - ???, // todo needs to be provided by primary data service? + primaryDataMeta, ) ParticipantModelInitContainer( diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala index cb711df812..7e22a31316 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala @@ -9,7 +9,11 @@ package edu.ie3.simona.model.participant2 import edu.ie3.datamodel.models.input.system.SystemParticipantInput import edu.ie3.datamodel.models.result.system.SystemParticipantResult import edu.ie3.simona.agent.participant.data.Data.PrimaryData.ComplexPower -import edu.ie3.simona.agent.participant.data.Data.SecondaryData +import edu.ie3.simona.agent.participant.data.Data.{ + PrimaryData, + PrimaryDataMeta, + SecondaryData, +} import edu.ie3.simona.agent.participant2.ParticipantAgent import edu.ie3.simona.agent.participant2.ParticipantAgent.ParticipantRequest import edu.ie3.simona.config.SimonaConfig.BaseRuntimeConfig @@ -38,6 +42,7 @@ import squants.Dimensionless import squants.energy.Power import java.time.ZonedDateTime +import scala.reflect.ClassTag /** Takes care of: * - holding id information @@ -269,16 +274,17 @@ object ParticipantModelShell { modelResults: Iterable[SystemParticipantResult], ) - def createForPrimaryData( + def createForPrimaryData[P <: PrimaryData: ClassTag]( participantInput: SystemParticipantInput, config: BaseRuntimeConfig, + primaryDataMeta: PrimaryDataMeta[P], simulationStartDate: ZonedDateTime, simulationEndDate: ZonedDateTime, ): ParticipantModelShell[_, _, _] = { - // todo T parameter, receive from primary proxy val modelContainer = ParticipantModelInit.createPrimaryModel( participantInput, config, + primaryDataMeta, ) createShell( modelContainer, From b63c2e795b3f76afc12cd11a6267fa98121bd214 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Fri, 22 Nov 2024 20:23:55 +0100 Subject: [PATCH 46/77] Adapting initialization Signed-off-by: Sebastian Peter --- .../participant2/ParticipantModelShell.scala | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala index 7e22a31316..be16da1dae 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala @@ -61,11 +61,11 @@ final case class ParticipantModelShell[ operationInterval: OperationInterval, simulationStartDate: ZonedDateTime, state: S, - relevantData: Option[OR], - flexOptions: Option[ProvideFlexOptions], - lastOperatingPoint: Option[OP], - operatingPoint: Option[OP], - modelChange: ModelChangeIndicator, + relevantData: Option[OR] = None, + flexOptions: Option[ProvideFlexOptions] = None, + lastOperatingPoint: Option[OP] = None, + operatingPoint: Option[OP] = None, + modelChange: ModelChangeIndicator = ModelChangeIndicator(), ) { def updateRelevantData( @@ -335,11 +335,6 @@ object ParticipantModelShell { operationInterval = operationInterval, simulationStartDate = simulationStartDate, state = modelContainer.initialState, - relevantData = None, - flexOptions = None, - lastOperatingPoint = None, - operatingPoint = None, - modelChange = ModelChangeIndicator(), ) } } From df1521120dfe2ec7c28135ad709fbef18574608d Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Mon, 25 Nov 2024 15:12:49 +0100 Subject: [PATCH 47/77] Some improvements and new todos Signed-off-by: Sebastian Peter --- .../participant2/ParticipantAgentInit.scala | 8 +- .../model/participant2/FixedFeedInModel.scala | 14 +-- .../model/participant2/ParticipantModel.scala | 14 ++- .../participant2/ParticipantModelShell.scala | 2 + .../PrimaryDataParticipantModel.scala | 16 ++-- .../simona/model/participant2/PvModel.scala | 14 +-- .../simona/model/participant2/WecModel.scala | 14 +-- .../participant2/load/FixedLoadModel.scala | 2 +- .../model/participant2/load/LoadModel.scala | 12 +-- .../participant2/load/ProfileLoadModel.scala | 2 +- .../participant2/load/RandomLoadModel.scala | 2 +- .../participant2/MockParticipantModel.scala | 89 +++++++++++++++++++ .../ParticipantAgentMockFactory.scala | 65 ++++++++++++++ .../participant2/ParticipantAgentSpec.scala | 64 +++++++++++++ .../model/participant2/WecModelSpec.scala | 4 +- 15 files changed, 270 insertions(+), 52 deletions(-) create mode 100644 src/test/scala/edu/ie3/simona/agent/participant2/MockParticipantModel.scala create mode 100644 src/test/scala/edu/ie3/simona/agent/participant2/ParticipantAgentMockFactory.scala create mode 100644 src/test/scala/edu/ie3/simona/agent/participant2/ParticipantAgentSpec.scala diff --git a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgentInit.scala b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgentInit.scala index 9eea078cbb..7d367e3e78 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgentInit.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgentInit.scala @@ -63,21 +63,21 @@ object ParticipantAgentInit { parent: Either[ActorRef[SchedulerMessage], ActorRef[FlexResponse]], ): Behavior[Request] = Behaviors.setup { ctx => val parentData = parent - .map { parentEm => + .map { em => val flexAdapter = ctx.messageAdapter[FlexRequest](Flex) - parentEm ! RegisterParticipant( + em ! RegisterParticipant( participantInput.getUuid, flexAdapter, participantInput, ) - parentEm ! ScheduleFlexRequest( + em ! ScheduleFlexRequest( participantInput.getUuid, INIT_SIM_TICK, ) - FlexControlledData(parentEm, flexAdapter) + FlexControlledData(em, flexAdapter) } .left .map { scheduler => diff --git a/src/main/scala/edu/ie3/simona/model/participant2/FixedFeedInModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/FixedFeedInModel.scala index 5724aa204f..f4da6afa02 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/FixedFeedInModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/FixedFeedInModel.scala @@ -20,9 +20,9 @@ import edu.ie3.simona.model.participant.control.QControl import edu.ie3.simona.model.participant2.ParticipantFlexibility.ParticipantSimpleFlexibility import edu.ie3.simona.model.participant2.ParticipantModel.{ ActivePowerOperatingPoint, - ConstantState, + FixedState, FixedRelevantData, - ParticipantConstantModel, + ParticipantFixedState, } import edu.ie3.simona.service.ServiceType import edu.ie3.util.quantities.PowerSystemUnits @@ -40,20 +40,20 @@ class FixedFeedInModel( override val qControl: QControl, ) extends ParticipantModel[ ActivePowerOperatingPoint, - ConstantState.type, + FixedState, FixedRelevantData.type, ] - with ParticipantConstantModel[ + with ParticipantFixedState[ ActivePowerOperatingPoint, FixedRelevantData.type, ] with ParticipantSimpleFlexibility[ - ConstantState.type, + FixedState, FixedRelevantData.type, ] { override def determineOperatingPoint( - state: ParticipantModel.ConstantState.type, + state: ParticipantModel.FixedState, relevantData: ParticipantModel.FixedRelevantData.type, ): (ActivePowerOperatingPoint, Option[Long]) = { val power = pRated * -1 @@ -65,7 +65,7 @@ class FixedFeedInModel( ActivePowerOperatingPoint.zero override def createResults( - state: ParticipantModel.ConstantState.type, + state: ParticipantModel.FixedState, lastOperatingPoint: Option[ActivePowerOperatingPoint], currentOperatingPoint: ActivePowerOperatingPoint, complexPower: ComplexPower, diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala index 4fcbd38afb..3830679950 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala @@ -216,23 +216,21 @@ object ParticipantModel { val tick: Long } - case object ConstantState extends ModelState { - override val tick: Long = -1 // is there a better way? - } + final case class FixedState(override val tick: Long) extends ModelState - trait ParticipantConstantModel[ + trait ParticipantFixedState[ OP <: OperatingPoint, OR <: OperationRelevantData, ] { - this: ParticipantModel[OP, ConstantState.type, OR] => + this: ParticipantModel[OP, FixedState, OR] => - def getInitialState: ConstantState.type = ConstantState + def getInitialState: FixedState = FixedState(-1) override def determineState( - lastState: ConstantState.type, + lastState: FixedState, operatingPoint: OP, currentTick: Long, - ): ConstantState.type = ConstantState + ): FixedState = FixedState(currentTick) } diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala index be16da1dae..4392d1f27d 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala @@ -95,6 +95,8 @@ final case class ParticipantModelShell[ ): ParticipantModelShell[OP, S, OR] = { val currentState = determineCurrentState(currentTick) + // FIXME this does not work. chicken and egg problem: state with current tick or operating point first? + // method for creating initial state with specific tick? if (currentState.tick != currentTick) throw new CriticalFailureException( s"New state $currentState is not set to current tick $currentTick" diff --git a/src/main/scala/edu/ie3/simona/model/participant2/PrimaryDataParticipantModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/PrimaryDataParticipantModel.scala index 681e16f383..bacf4b6fe0 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/PrimaryDataParticipantModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/PrimaryDataParticipantModel.scala @@ -17,11 +17,11 @@ import edu.ie3.simona.agent.participant.data.Data.PrimaryData.{ import edu.ie3.simona.exceptions.CriticalFailureException import edu.ie3.simona.model.participant.control.QControl import edu.ie3.simona.model.participant2.ParticipantModel.{ - ConstantState, + FixedState, ModelChangeIndicator, OperatingPoint, OperationRelevantData, - ParticipantConstantModel, + ParticipantFixedState, } import edu.ie3.simona.model.participant2.PrimaryDataParticipantModel._ import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage @@ -45,16 +45,16 @@ final case class PrimaryDataParticipantModel[P <: PrimaryData: ClassTag]( primaryDataMeta: PrimaryDataMeta[P], ) extends ParticipantModel[ PrimaryOperatingPoint[P], - ConstantState.type, + FixedState, PrimaryOperationRelevantData[P], ] - with ParticipantConstantModel[ + with ParticipantFixedState[ PrimaryOperatingPoint[P], PrimaryOperationRelevantData[P], ] { override def determineOperatingPoint( - state: ConstantState.type, + state: FixedState, relevantData: PrimaryOperationRelevantData[P], ): (PrimaryOperatingPoint[P], Option[Long]) = (PrimaryOperatingPoint(relevantData.data), None) @@ -63,7 +63,7 @@ final case class PrimaryDataParticipantModel[P <: PrimaryData: ClassTag]( PrimaryOperatingPoint(primaryDataMeta.zero) override def createResults( - state: ConstantState.type, + state: FixedState, lastOperatingPoint: Option[PrimaryOperatingPoint[P]], currentOperatingPoint: PrimaryOperatingPoint[P], complexPower: ComplexPower, @@ -114,7 +114,7 @@ final case class PrimaryDataParticipantModel[P <: PrimaryData: ClassTag]( } override def calcFlexOptions( - state: ConstantState.type, + state: FixedState, relevantData: PrimaryOperationRelevantData[P], ): FlexibilityMessage.ProvideFlexOptions = { val (operatingPoint, _) = determineOperatingPoint(state, relevantData) @@ -124,7 +124,7 @@ final case class PrimaryDataParticipantModel[P <: PrimaryData: ClassTag]( } override def handlePowerControl( - state: ConstantState.type, + state: FixedState, relevantData: PrimaryOperationRelevantData[P], flexOptions: FlexibilityMessage.ProvideFlexOptions, setPower: Power, diff --git a/src/main/scala/edu/ie3/simona/model/participant2/PvModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/PvModel.scala index 14fcd60b68..010fb5f3f9 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/PvModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/PvModel.scala @@ -22,9 +22,9 @@ import edu.ie3.simona.model.participant.control.QControl import edu.ie3.simona.model.participant2.ParticipantFlexibility.ParticipantSimpleFlexibility import edu.ie3.simona.model.participant2.ParticipantModel.{ ActivePowerOperatingPoint, - ConstantState, + FixedState, OperationRelevantData, - ParticipantConstantModel, + ParticipantFixedState, } import edu.ie3.simona.model.participant2.PvModel.PvRelevantData import edu.ie3.simona.ontology.messages.services.WeatherMessage.WeatherData @@ -56,11 +56,11 @@ class PvModel private ( private val moduleSurface: Area = SquareMeters(1d), ) extends ParticipantModel[ ActivePowerOperatingPoint, - ConstantState.type, + FixedState, PvRelevantData, ] - with ParticipantConstantModel[ActivePowerOperatingPoint, PvRelevantData] - with ParticipantSimpleFlexibility[ConstantState.type, PvRelevantData] + with ParticipantFixedState[ActivePowerOperatingPoint, PvRelevantData] + with ParticipantSimpleFlexibility[FixedState, PvRelevantData] with LazyLogging { /** Override sMax as the power output of a pv unit could become easily up to @@ -84,7 +84,7 @@ class PvModel private ( * Active power */ override def determineOperatingPoint( - modelState: ConstantState.type, + modelState: FixedState, data: PvRelevantData, ): (ActivePowerOperatingPoint, Option[Long]) = { // === Weather Base Data === // @@ -727,7 +727,7 @@ class PvModel private ( } override def createResults( - state: ParticipantModel.ConstantState.type, + state: ParticipantModel.FixedState, lastOperatingPoint: Option[ActivePowerOperatingPoint], currentOperatingPoint: ActivePowerOperatingPoint, complexPower: ComplexPower, diff --git a/src/main/scala/edu/ie3/simona/model/participant2/WecModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/WecModel.scala index 9eabe275b7..77d273d034 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/WecModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/WecModel.scala @@ -23,9 +23,9 @@ import edu.ie3.simona.model.participant.control.QControl import edu.ie3.simona.model.participant2.ParticipantFlexibility.ParticipantSimpleFlexibility import edu.ie3.simona.model.participant2.ParticipantModel.{ ActivePowerOperatingPoint, - ConstantState, + FixedState, OperationRelevantData, - ParticipantConstantModel, + ParticipantFixedState, } import edu.ie3.simona.model.participant2.WecModel.{ WecCharacteristic, @@ -62,11 +62,11 @@ class WecModel private ( private val betzCurve: WecCharacteristic, ) extends ParticipantModel[ ActivePowerOperatingPoint, - ConstantState.type, + FixedState, WecRelevantData, ] - with ParticipantConstantModel[ActivePowerOperatingPoint, WecRelevantData] - with ParticipantSimpleFlexibility[ConstantState.type, WecRelevantData] + with ParticipantFixedState[ActivePowerOperatingPoint, WecRelevantData] + with ParticipantSimpleFlexibility[FixedState, WecRelevantData] with LazyLogging { /** Calculate the active power behaviour of the model @@ -77,7 +77,7 @@ class WecModel private ( * Active power */ override def determineOperatingPoint( - modelState: ConstantState.type, + modelState: FixedState, data: WecRelevantData, ): (ActivePowerOperatingPoint, Option[Long]) = { val betzCoefficient = determineBetzCoefficient(data.windVelocity) @@ -166,7 +166,7 @@ class WecModel private ( ActivePowerOperatingPoint.zero override def createResults( - state: ParticipantModel.ConstantState.type, + state: ParticipantModel.FixedState, lastOperatingPoint: Option[ActivePowerOperatingPoint], currentOperatingPoint: ActivePowerOperatingPoint, complexPower: ComplexPower, diff --git a/src/main/scala/edu/ie3/simona/model/participant2/load/FixedLoadModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/load/FixedLoadModel.scala index 8c68cebd2e..21649b9b26 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/load/FixedLoadModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/load/FixedLoadModel.scala @@ -37,7 +37,7 @@ class FixedLoadModel( ) extends LoadModel[FixedRelevantData.type] { override def determineOperatingPoint( - state: ParticipantModel.ConstantState.type, + state: ParticipantModel.FixedState, relevantData: ParticipantModel.FixedRelevantData.type, ): (ActivePowerOperatingPoint, Option[Long]) = (ActivePowerOperatingPoint(activePower), None) diff --git a/src/main/scala/edu/ie3/simona/model/participant2/load/LoadModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/load/LoadModel.scala index 4ee0cffe09..19215e47d6 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/load/LoadModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/load/LoadModel.scala @@ -21,9 +21,9 @@ import edu.ie3.simona.model.participant2.ParticipantFlexibility.ParticipantSimpl import edu.ie3.simona.model.participant2.ParticipantModel import edu.ie3.simona.model.participant2.ParticipantModel.{ ActivePowerOperatingPoint, - ConstantState, + FixedState, OperationRelevantData, - ParticipantConstantModel, + ParticipantFixedState, } import edu.ie3.simona.service.ServiceType import edu.ie3.util.quantities.PowerSystemUnits @@ -36,15 +36,15 @@ import java.time.ZonedDateTime abstract class LoadModel[OR <: OperationRelevantData] extends ParticipantModel[ ActivePowerOperatingPoint, - ConstantState.type, + FixedState, OR, ] - with ParticipantConstantModel[ + with ParticipantFixedState[ ActivePowerOperatingPoint, OR, ] with ParticipantSimpleFlexibility[ - ConstantState.type, + FixedState, OR, ] { @@ -52,7 +52,7 @@ abstract class LoadModel[OR <: OperationRelevantData] ActivePowerOperatingPoint.zero override def createResults( - state: ParticipantModel.ConstantState.type, + state: ParticipantModel.FixedState, lastOperatingPoint: Option[ActivePowerOperatingPoint], currentOperatingPoint: ActivePowerOperatingPoint, complexPower: ComplexPower, diff --git a/src/main/scala/edu/ie3/simona/model/participant2/load/ProfileLoadModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/load/ProfileLoadModel.scala index 2797c300c5..aa155c0aaf 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/load/ProfileLoadModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/load/ProfileLoadModel.scala @@ -36,7 +36,7 @@ class ProfileLoadModel( ) extends LoadModel[DateTimeData] { override def determineOperatingPoint( - state: ParticipantModel.ConstantState.type, + state: ParticipantModel.FixedState, relevantData: DateTimeData, ): (ParticipantModel.ActivePowerOperatingPoint, Option[Long]) = { val resolution = RandomLoadParamStore.resolution.getSeconds diff --git a/src/main/scala/edu/ie3/simona/model/participant2/load/RandomLoadModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/load/RandomLoadModel.scala index 3735d82d0f..410222e2de 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/load/RandomLoadModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/load/RandomLoadModel.scala @@ -45,7 +45,7 @@ class RandomLoadModel( mutable.Map.empty[GevKey, GeneralizedExtremeValueDistribution] override def determineOperatingPoint( - state: ParticipantModel.ConstantState.type, + state: ParticipantModel.FixedState, relevantData: DateTimeData, ): (ParticipantModel.ActivePowerOperatingPoint, Option[Long]) = { val resolution = RandomLoadParamStore.resolution.getSeconds diff --git a/src/test/scala/edu/ie3/simona/agent/participant2/MockParticipantModel.scala b/src/test/scala/edu/ie3/simona/agent/participant2/MockParticipantModel.scala new file mode 100644 index 0000000000..90888e76d7 --- /dev/null +++ b/src/test/scala/edu/ie3/simona/agent/participant2/MockParticipantModel.scala @@ -0,0 +1,89 @@ +/* + * © 2024. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.agent.participant2 + +import edu.ie3.datamodel.models.result.system.SystemParticipantResult +import edu.ie3.simona.agent.participant.data.Data +import edu.ie3.simona.agent.participant.data.Data.PrimaryData +import edu.ie3.simona.model.participant.control.QControl +import edu.ie3.simona.model.participant.control.QControl.CosPhiFixed +import edu.ie3.simona.model.participant2.ParticipantModel +import edu.ie3.simona.model.participant2.ParticipantModel.{ + ActivePowerOperatingPoint, + FixedRelevantData, + FixedState, + ModelChangeIndicator, + ParticipantFixedState, +} +import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage +import edu.ie3.simona.service.ServiceType +import edu.ie3.util.scala.quantities.{ApparentPower, Kilovoltamperes} +import squants.Dimensionless +import squants.energy.{Kilowatts, Power} + +import java.time.ZonedDateTime +import java.util.UUID + +class MockParticipantModel( + override val uuid: UUID = UUID.fromString("0-0-0-0-1"), + override val sRated: ApparentPower = Kilovoltamperes(10), + override val cosPhiRated: Double = 0.9, + override val qControl: QControl = CosPhiFixed(0.9), +) extends ParticipantModel[ + ActivePowerOperatingPoint, + FixedState, + FixedRelevantData.type, + ] + with ParticipantFixedState[ + ActivePowerOperatingPoint, + FixedRelevantData.type, + ] { + + override def determineOperatingPoint( + state: FixedState, + relevantData: FixedRelevantData.type, + ): (ActivePowerOperatingPoint, Option[Long]) = + (ActivePowerOperatingPoint(Kilowatts(5)), None) + + override def zeroPowerOperatingPoint: ActivePowerOperatingPoint = + ActivePowerOperatingPoint.zero + + override def createResults( + state: FixedState, + lastOperatingPoint: Option[ActivePowerOperatingPoint], + currentOperatingPoint: ActivePowerOperatingPoint, + complexPower: PrimaryData.ComplexPower, + dateTime: ZonedDateTime, + ): Iterable[SystemParticipantResult] = ??? + + override def createPrimaryDataResult( + data: PrimaryData.PrimaryDataWithComplexPower[_], + dateTime: ZonedDateTime, + ): SystemParticipantResult = ??? + + override def getRequiredSecondaryServices: Iterable[ServiceType] = + Iterable.empty + + override def createRelevantData( + receivedData: Seq[Data], + nodalVoltage: Dimensionless, + tick: Long, + simulationTime: ZonedDateTime, + ): FixedRelevantData.type = FixedRelevantData + + override def calcFlexOptions( + state: FixedState, + relevantData: FixedRelevantData.type, + ): FlexibilityMessage.ProvideFlexOptions = ??? + + override def handlePowerControl( + state: FixedState, + relevantData: FixedRelevantData.type, + flexOptions: FlexibilityMessage.ProvideFlexOptions, + setPower: Power, + ): (ActivePowerOperatingPoint, ModelChangeIndicator) = ??? +} diff --git a/src/test/scala/edu/ie3/simona/agent/participant2/ParticipantAgentMockFactory.scala b/src/test/scala/edu/ie3/simona/agent/participant2/ParticipantAgentMockFactory.scala new file mode 100644 index 0000000000..a352f63eda --- /dev/null +++ b/src/test/scala/edu/ie3/simona/agent/participant2/ParticipantAgentMockFactory.scala @@ -0,0 +1,65 @@ +/* + * © 2024. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.agent.participant2 + +import edu.ie3.simona.agent.participant2.ParticipantAgent.{ + Flex, + FlexControlledData, + ParticipantActivation, + Request, + SchedulerData, +} +import edu.ie3.simona.event.ResultEvent +import edu.ie3.simona.model.participant2.ParticipantModelShell +import edu.ie3.simona.ontology.messages.{Activation, SchedulerMessage} +import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage.{ + FlexRequest, + FlexResponse, +} +import org.apache.pekko.actor.typed.{ActorRef, Behavior} +import org.apache.pekko.actor.typed.scaladsl.Behaviors + +object ParticipantAgentMockFactory { + + /** Needed because activation adapter needs to be created and communicated + */ + def create( + modelShell: ParticipantModelShell[_, _, _], + inputHandler: ParticipantInputHandler, + gridAdapter: ParticipantGridAdapter, + resultListener: Iterable[ActorRef[ResultEvent]], + parent: Either[ + (ActorRef[SchedulerMessage], ActorRef[ActorRef[Activation]]), + (ActorRef[FlexResponse], ActorRef[ActorRef[FlexRequest]]), + ], + ): Behavior[Request] = Behaviors.setup { ctx => + val parentData = parent + .map { case (em, adapterReply) => + val flexAdapter = ctx.messageAdapter[FlexRequest](Flex) + adapterReply ! flexAdapter + FlexControlledData(em, flexAdapter) + } + .left + .map { + case (scheduler, adapterReply) => { + val activationAdapter = ctx.messageAdapter[Activation] { msg => + ParticipantActivation(msg.tick) + } + adapterReply ! activationAdapter + SchedulerData(scheduler, activationAdapter) + } + } + + ParticipantAgent( + modelShell, + inputHandler, + gridAdapter, + resultListener, + parentData, + ) + } +} diff --git a/src/test/scala/edu/ie3/simona/agent/participant2/ParticipantAgentSpec.scala b/src/test/scala/edu/ie3/simona/agent/participant2/ParticipantAgentSpec.scala new file mode 100644 index 0000000000..0eb8a69564 --- /dev/null +++ b/src/test/scala/edu/ie3/simona/agent/participant2/ParticipantAgentSpec.scala @@ -0,0 +1,64 @@ +/* + * © 2024. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.agent.participant2 + +import edu.ie3.simona.agent.grid.GridAgent +import edu.ie3.simona.event.ResultEvent +import edu.ie3.simona.model.participant2.ParticipantModel.FixedState +import edu.ie3.simona.model.participant2.ParticipantModelShell +import edu.ie3.simona.ontology.messages.SchedulerMessage.Completion +import edu.ie3.simona.ontology.messages.{Activation, SchedulerMessage} +import edu.ie3.simona.test.common.UnitSpec +import edu.ie3.util.TimeUtil +import edu.ie3.util.scala.OperationInterval +import org.apache.pekko.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit +import org.apache.pekko.actor.typed.ActorRef + +import java.time.ZonedDateTime + +class ParticipantAgentSpec extends ScalaTestWithActorTestKit with UnitSpec { + + private val simulationStartDate: ZonedDateTime = + TimeUtil.withDefaults.toZonedDateTime("2020-01-01T00:00:00Z") + + "A ParticipantAgent without services" in { + + val scheduler = createTestProbe[SchedulerMessage]() + val gridAgent = createTestProbe[GridAgent.Request]() + val resultListener = createTestProbe[ResultEvent]() + + val receiveAdapter = createTestProbe[ActorRef[Activation]]() + + val participantAgent = spawn( + ParticipantAgentMockFactory.create( + ParticipantModelShell( + new MockParticipantModel(), + OperationInterval(0, 24 * 60 * 60), + simulationStartDate, + FixedState(-1), + ), + ParticipantInputHandler( + Map.empty + ), + ParticipantGridAdapter( + gridAgent.ref, + 3600, + ), + Iterable(resultListener.ref), + Left(scheduler.ref, receiveAdapter.ref), + ) + ) + val activationRef = receiveAdapter.expectMessageType[ActorRef[Activation]] + + activationRef ! Activation(0) + + // fixme + // scheduler.expectMessage(Completion(activationRef)) + + } + +} diff --git a/src/test/scala/edu/ie3/simona/model/participant2/WecModelSpec.scala b/src/test/scala/edu/ie3/simona/model/participant2/WecModelSpec.scala index abdf886a87..7da74750fb 100644 --- a/src/test/scala/edu/ie3/simona/model/participant2/WecModelSpec.scala +++ b/src/test/scala/edu/ie3/simona/model/participant2/WecModelSpec.scala @@ -132,7 +132,7 @@ class WecModelSpec extends UnitSpec with DefaultTestData { ) val (operatingPoint, nextTick) = wecModel.determineOperatingPoint( - ParticipantModel.ConstantState, + ParticipantModel.FixedState(0), wecData, ) @@ -187,7 +187,7 @@ class WecModelSpec extends UnitSpec with DefaultTestData { ) val (operatingPoint, nextTick) = wecModel.determineOperatingPoint( - ParticipantModel.ConstantState, + ParticipantModel.FixedState(0), wecData, ) From 13cb38d055d247c1a1a4375fe6405233c986439c Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Mon, 25 Nov 2024 18:20:02 +0100 Subject: [PATCH 48/77] Some adaptations regarding state and first successful test Signed-off-by: Sebastian Peter --- .../model/participant2/ParticipantModel.scala | 6 +- .../participant2/ParticipantModelInit.scala | 46 +++-------- .../participant2/ParticipantModelShell.scala | 59 +++++++------- .../model/participant2/StorageModel.scala | 30 ++++--- .../model/participant2/evcs/EvcsModel.scala | 4 +- .../participant2/MockParticipantModel.scala | 25 +++++- .../participant2/ParticipantAgentSpec.scala | 80 ++++++++++++------- 7 files changed, 140 insertions(+), 110 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala index 3830679950..728275b3bc 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala @@ -41,6 +41,8 @@ abstract class ParticipantModel[ val cosPhiRated: Double val qControl: QControl + val initialState: Long => S + protected val pRated: Power = sRated.toActivePower(cosPhiRated) /** Get a partial function, that transfers the current active into reactive @@ -186,7 +188,7 @@ object ParticipantModel { * @param dateTime * The current datetime, corresponding to the current tick */ - case class DateTimeData(tick: Long, dateTime: ZonedDateTime) + final case class DateTimeData(tick: Long, dateTime: ZonedDateTime) extends OperationRelevantData trait OperatingPoint { @@ -224,7 +226,7 @@ object ParticipantModel { ] { this: ParticipantModel[OP, FixedState, OR] => - def getInitialState: FixedState = FixedState(-1) + override val initialState: Long => FixedState = tick => FixedState(tick) override def determineState( lastState: FixedState, diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelInit.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelInit.scala index f25bc44693..4b448df033 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelInit.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelInit.scala @@ -34,7 +34,7 @@ object ParticipantModelInit { def createModel( participantInput: SystemParticipantInput, modelConfig: BaseRuntimeConfig, - ): ParticipantModelInitContainer[ + ): ParticipantModel[ _ <: OperatingPoint, _ <: ModelState, _ <: OperationRelevantData, @@ -48,29 +48,17 @@ object ParticipantModelInit { (scaledParticipantInput, modelConfig) match { case (input: FixedFeedInInput, _) => - val model = FixedFeedInModel(input) - val state = model.getInitialState - ParticipantModelInitContainer(model, state) + FixedFeedInModel(input) case (input: LoadInput, config: LoadRuntimeConfig) => - val model = LoadModel(input, config) - val state = model.getInitialState - ParticipantModelInitContainer(model, state) + LoadModel(input, config) case (input: PvInput, _) => - val model = PvModel(input) - val state = model.getInitialState - ParticipantModelInitContainer(model, state) + PvModel(input) case (input: WecInput, _) => - val model = WecModel(input) - val state = model.getInitialState - ParticipantModelInitContainer(model, state) + WecModel(input) case (input: StorageInput, config: StorageRuntimeConfig) => - val model = StorageModel(input, config) - val state = model.getInitialState(config) - ParticipantModelInitContainer(model, state) + StorageModel(input, config) case (input: EvcsInput, config: EvcsRuntimeConfig) => - val model = EvcsModel(input, config) - val state = model.getInitialState - ParticipantModelInitContainer(model, state) + EvcsModel(input, config) case (input, config) => throw new CriticalFailureException( s"Handling the input model ${input.getClass.getSimpleName} or " + @@ -84,17 +72,16 @@ object ParticipantModelInit { participantInput: SystemParticipantInput, modelConfig: BaseRuntimeConfig, primaryDataMeta: PrimaryDataMeta[P], - ): ParticipantModelInitContainer[ + ): ParticipantModel[ _ <: OperatingPoint, _ <: ModelState, _ <: OperationRelevantData, ] = { // Create a fitting physical model to extract parameters from - val modelContainer = createModel( + val physicalModel = createModel( participantInput, modelConfig, ) - val physicalModel = modelContainer.model val primaryResultFunc = new PrimaryResultFunc { override def createResult( @@ -104,7 +91,7 @@ object ParticipantModelInit { physicalModel.createPrimaryDataResult(data, dateTime) } - val primaryDataModel = new PrimaryDataParticipantModel( + new PrimaryDataParticipantModel( physicalModel.uuid, physicalModel.sRated, physicalModel.cosPhiRated, @@ -112,19 +99,6 @@ object ParticipantModelInit { primaryResultFunc, primaryDataMeta, ) - - ParticipantModelInitContainer( - primaryDataModel, - primaryDataModel.getInitialState, - ) } - final case class ParticipantModelInitContainer[ - OP <: OperatingPoint, - S <: ModelState, - OR <: OperationRelevantData, - ]( - model: ParticipantModel[OP, S, OR] with ParticipantFlexibility[OP, S, OR], - initialState: S, - ) } diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala index 4392d1f27d..5d6bc3c269 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala @@ -26,7 +26,6 @@ import edu.ie3.simona.model.participant2.ParticipantModel.{ OperatingPoint, OperationRelevantData, } -import edu.ie3.simona.model.participant2.ParticipantModelInit.ParticipantModelInitContainer import edu.ie3.simona.model.participant2.ParticipantModelShell.ResultsContainer import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage.{ IssueFlexControl, @@ -60,7 +59,7 @@ final case class ParticipantModelShell[ model: ParticipantModel[OP, S, OR] with ParticipantFlexibility[OP, S, OR], operationInterval: OperationInterval, simulationStartDate: ZonedDateTime, - state: S, + state: Option[S] = None, relevantData: Option[OR] = None, flexOptions: Option[ProvideFlexOptions] = None, lastOperatingPoint: Option[OP] = None, @@ -95,16 +94,9 @@ final case class ParticipantModelShell[ ): ParticipantModelShell[OP, S, OR] = { val currentState = determineCurrentState(currentTick) - // FIXME this does not work. chicken and egg problem: state with current tick or operating point first? - // method for creating initial state with specific tick? - if (currentState.tick != currentTick) - throw new CriticalFailureException( - s"New state $currentState is not set to current tick $currentTick" - ) - def modelOperatingPoint() = { val (modelOp, modelNextTick) = model.determineOperatingPoint( - state, + currentState, relevantData.getOrElse( throw new CriticalFailureException("No relevant data available!") ), @@ -117,7 +109,7 @@ final case class ParticipantModelShell[ determineOperatingPointInInterval(modelOperatingPoint, currentTick) copy( - state = currentState, + state = Some(currentState), lastOperatingPoint = operatingPoint, operatingPoint = Some(newOperatingPoint), modelChange = newChangeIndicator, @@ -143,7 +135,9 @@ final case class ParticipantModelShell[ val complexPower = ComplexPower(activePower, reactivePower) val participantResults = model.createResults( - state, + state.getOrElse( + throw new CriticalFailureException("No model state available!") + ), lastOperatingPoint, op, complexPower, @@ -172,7 +166,7 @@ final case class ParticipantModelShell[ ProvideMinMaxFlexOptions.noFlexOption(model.uuid, zeroKW) } - copy(state = currentState, flexOptions = Some(flexOptions)) + copy(state = Some(currentState), flexOptions = Some(flexOptions)) } /** Update operating point on receiving [[IssueFlexControl]], i.e. when the @@ -212,7 +206,7 @@ final case class ParticipantModelShell[ determineOperatingPointInInterval(modelOperatingPoint, currentTick) copy( - state = currentState, + state = Some(currentState), lastOperatingPoint = operatingPoint, operatingPoint = Some(newOperatingPoint), modelChange = newChangeIndicator, @@ -255,17 +249,27 @@ final case class ParticipantModelShell[ val currentState = determineCurrentState(request.tick) val updatedState = model.handleRequest(currentState, ctx, request) - copy(state = updatedState) + copy(state = Some(updatedState)) } - private def determineCurrentState(currentTick: Long): S = - operatingPoint - .flatMap { op => - Option.when(state.tick < currentTick) { - model.determineState(state, op, currentTick) + private def determineCurrentState(currentTick: Long): S = { + // new state is only calculated if there's an old state and an operating point + val currentState = state + .zip(operatingPoint) + .flatMap { case (st, op) => + Option.when(st.tick < currentTick) { + model.determineState(st, op, currentTick) } } - .getOrElse(state) + .getOrElse(model.initialState(currentTick)) + + if (currentState.tick != currentTick) + throw new CriticalFailureException( + s"New state $currentState is not set to current tick $currentTick" + ) + + currentState + } } @@ -283,13 +287,13 @@ object ParticipantModelShell { simulationStartDate: ZonedDateTime, simulationEndDate: ZonedDateTime, ): ParticipantModelShell[_, _, _] = { - val modelContainer = ParticipantModelInit.createPrimaryModel( + val model = ParticipantModelInit.createPrimaryModel( participantInput, config, primaryDataMeta, ) createShell( - modelContainer, + model, participantInput, simulationEndDate, simulationStartDate, @@ -302,12 +306,12 @@ object ParticipantModelShell { simulationStartDate: ZonedDateTime, simulationEndDate: ZonedDateTime, ): ParticipantModelShell[_, _, _] = { - val modelContainer = ParticipantModelInit.createModel( + val model = ParticipantModelInit.createModel( participantInput, config, ) createShell( - modelContainer, + model, participantInput, simulationEndDate, simulationStartDate, @@ -319,7 +323,7 @@ object ParticipantModelShell { S <: ModelState, OR <: OperationRelevantData, ]( - modelContainer: ParticipantModelInitContainer[OP, S, OR], + model: ParticipantModel[OP, S, OR], participantInput: SystemParticipantInput, simulationEndDate: ZonedDateTime, simulationStartDate: ZonedDateTime, @@ -333,10 +337,9 @@ object ParticipantModelShell { ) new ParticipantModelShell( - model = modelContainer.model, + model = model, operationInterval = operationInterval, simulationStartDate = simulationStartDate, - state = modelContainer.initialState, ) } } diff --git a/src/main/scala/edu/ie3/simona/model/participant2/StorageModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/StorageModel.scala index 747cf26b1b..860806b41c 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/StorageModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/StorageModel.scala @@ -47,6 +47,7 @@ class StorageModel private ( override val sRated: ApparentPower, override val cosPhiRated: Double, override val qControl: QControl, + override val initialState: Long => StorageState, eStorage: Energy, pMax: Power, eta: Dimensionless, @@ -312,10 +313,6 @@ class StorageModel private ( private def isEmpty(storedEnergy: Energy): Boolean = storedEnergy <= minEnergyWithMargin - def getInitialState(config: StorageRuntimeConfig): StorageState = { - val initialStorage = eStorage * config.initialSoc - StorageState(storedEnergy = initialStorage, -1L) - } } object StorageModel { @@ -336,7 +333,20 @@ object StorageModel { def apply( inputModel: StorageInput, config: StorageRuntimeConfig, - ): StorageModel = + ): StorageModel = { + val eStorage = KilowattHours( + inputModel.getType.geteStorage + .to(PowerSystemUnits.KILOWATTHOUR) + .getValue + .doubleValue + ) + def getInitialState(eStorage: Energy, config: StorageRuntimeConfig)( + tick: Long + ): StorageState = { + val initialStorage = eStorage * config.initialSoc + StorageState(storedEnergy = initialStorage, tick) + } + new StorageModel( inputModel.getUuid, Kilovoltamperes( @@ -347,12 +357,8 @@ object StorageModel { ), inputModel.getType.getCosPhiRated, QControl.apply(inputModel.getqCharacteristics), - KilowattHours( - inputModel.getType.geteStorage - .to(PowerSystemUnits.KILOWATTHOUR) - .getValue - .doubleValue - ), + getInitialState(eStorage, config), + eStorage, Kilowatts( inputModel.getType.getpMax .to(PowerSystemUnits.KILOWATT) @@ -364,4 +370,6 @@ object StorageModel { ), config.targetSoc, ) + } + } diff --git a/src/main/scala/edu/ie3/simona/model/participant2/evcs/EvcsModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/evcs/EvcsModel.scala index a4506feee7..93059b8630 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/evcs/EvcsModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/evcs/EvcsModel.scala @@ -78,6 +78,9 @@ class EvcsModel private ( ] with EvcsChargingProperties { + override val initialState: Long => EvcsState = tick => + EvcsState(Seq.empty, tick) + override def determineOperatingPoint( state: EvcsState, relevantData: EvcsRelevantData, @@ -583,7 +586,6 @@ class EvcsModel private ( private def calcToleranceMargin(ev: EvModelWrapper): Energy = getMaxAvailableChargingPower(ev) * Seconds(1) - def getInitialState: EvcsState = EvcsState(Seq.empty, -1) } object EvcsModel { diff --git a/src/test/scala/edu/ie3/simona/agent/participant2/MockParticipantModel.scala b/src/test/scala/edu/ie3/simona/agent/participant2/MockParticipantModel.scala index 90888e76d7..6a88e24bb0 100644 --- a/src/test/scala/edu/ie3/simona/agent/participant2/MockParticipantModel.scala +++ b/src/test/scala/edu/ie3/simona/agent/participant2/MockParticipantModel.scala @@ -9,6 +9,7 @@ package edu.ie3.simona.agent.participant2 import edu.ie3.datamodel.models.result.system.SystemParticipantResult import edu.ie3.simona.agent.participant.data.Data import edu.ie3.simona.agent.participant.data.Data.PrimaryData +import edu.ie3.simona.agent.participant2.MockParticipantModel.MockResult import edu.ie3.simona.model.participant.control.QControl import edu.ie3.simona.model.participant.control.QControl.CosPhiFixed import edu.ie3.simona.model.participant2.ParticipantModel @@ -24,7 +25,10 @@ import edu.ie3.simona.service.ServiceType import edu.ie3.util.scala.quantities.{ApparentPower, Kilovoltamperes} import squants.Dimensionless import squants.energy.{Kilowatts, Power} +import tech.units.indriya.ComparableQuantity +import edu.ie3.util.quantities.QuantityUtils.RichQuantityDouble +import javax.measure.quantity.{Power => QuantPower} import java.time.ZonedDateTime import java.util.UUID @@ -58,7 +62,15 @@ class MockParticipantModel( currentOperatingPoint: ActivePowerOperatingPoint, complexPower: PrimaryData.ComplexPower, dateTime: ZonedDateTime, - ): Iterable[SystemParticipantResult] = ??? + ): Iterable[SystemParticipantResult] = + Iterable( + MockResult( + dateTime, + uuid, + complexPower.p.toMegawatts.asMegaWatt, + complexPower.q.toMegavars.asMegaVar, + ) + ) override def createPrimaryDataResult( data: PrimaryData.PrimaryDataWithComplexPower[_], @@ -87,3 +99,14 @@ class MockParticipantModel( setPower: Power, ): (ActivePowerOperatingPoint, ModelChangeIndicator) = ??? } + +object MockParticipantModel { + + final case class MockResult( + time: ZonedDateTime, + inputModel: UUID, + p: ComparableQuantity[QuantPower], + q: ComparableQuantity[QuantPower], + ) extends SystemParticipantResult(time, inputModel, p, q) + +} diff --git a/src/test/scala/edu/ie3/simona/agent/participant2/ParticipantAgentSpec.scala b/src/test/scala/edu/ie3/simona/agent/participant2/ParticipantAgentSpec.scala index 0eb8a69564..77ab4bb535 100644 --- a/src/test/scala/edu/ie3/simona/agent/participant2/ParticipantAgentSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/participant2/ParticipantAgentSpec.scala @@ -7,13 +7,15 @@ package edu.ie3.simona.agent.participant2 import edu.ie3.simona.agent.grid.GridAgent +import edu.ie3.simona.agent.participant2.MockParticipantModel.MockResult import edu.ie3.simona.event.ResultEvent -import edu.ie3.simona.model.participant2.ParticipantModel.FixedState +import edu.ie3.simona.event.ResultEvent.ParticipantResultEvent import edu.ie3.simona.model.participant2.ParticipantModelShell import edu.ie3.simona.ontology.messages.SchedulerMessage.Completion import edu.ie3.simona.ontology.messages.{Activation, SchedulerMessage} import edu.ie3.simona.test.common.UnitSpec import edu.ie3.util.TimeUtil +import edu.ie3.util.quantities.QuantityUtils.RichQuantityDouble import edu.ie3.util.scala.OperationInterval import org.apache.pekko.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit import org.apache.pekko.actor.typed.ActorRef @@ -25,39 +27,55 @@ class ParticipantAgentSpec extends ScalaTestWithActorTestKit with UnitSpec { private val simulationStartDate: ZonedDateTime = TimeUtil.withDefaults.toZonedDateTime("2020-01-01T00:00:00Z") - "A ParticipantAgent without services" in { - - val scheduler = createTestProbe[SchedulerMessage]() - val gridAgent = createTestProbe[GridAgent.Request]() - val resultListener = createTestProbe[ResultEvent]() - - val receiveAdapter = createTestProbe[ActorRef[Activation]]() - - val participantAgent = spawn( - ParticipantAgentMockFactory.create( - ParticipantModelShell( - new MockParticipantModel(), - OperationInterval(0, 24 * 60 * 60), - simulationStartDate, - FixedState(-1), - ), - ParticipantInputHandler( - Map.empty - ), - ParticipantGridAdapter( - gridAgent.ref, - 3600, - ), - Iterable(resultListener.ref), - Left(scheduler.ref, receiveAdapter.ref), + "A ParticipantAgent without services" should { + + "calculate operating point and results correctly" in { + + val scheduler = createTestProbe[SchedulerMessage]() + val gridAgent = createTestProbe[GridAgent.Request]() + val resultListener = createTestProbe[ResultEvent]() + + // receiving the activation adapter + val receiveAdapter = createTestProbe[ActorRef[Activation]]() + + val model = new MockParticipantModel() + val operationInterval = OperationInterval(0, 24 * 60 * 60) + + spawn( + ParticipantAgentMockFactory.create( + ParticipantModelShell( + model, + operationInterval, + simulationStartDate, + ), + ParticipantInputHandler( + Map.empty + ), + ParticipantGridAdapter( + gridAgent.ref, + 3600, + ), + Iterable(resultListener.ref), + Left(scheduler.ref, receiveAdapter.ref), + ) ) - ) - val activationRef = receiveAdapter.expectMessageType[ActorRef[Activation]] + val activationRef = receiveAdapter.expectMessageType[ActorRef[Activation]] + + activationRef ! Activation(0) - activationRef ! Activation(0) + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: MockResult) => + result.getInputModel shouldBe model.uuid + result.getTime shouldBe simulationStartDate + result.getP should equalWithTolerance(0.005.asMegaWatt) + result.getQ should equalWithTolerance(0.002421610524.asMegaVar) + } + + scheduler.expectMessage( + Completion(activationRef, Some(operationInterval.end)) + ) - // fixme - // scheduler.expectMessage(Completion(activationRef)) + } } From b8c4e4e31fbe6c7ca50515e9da150bc310e64597 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Mon, 25 Nov 2024 21:29:30 +0100 Subject: [PATCH 49/77] Fixing starting tick scheduling Signed-off-by: Sebastian Peter --- .../ie3/simona/model/participant2/ParticipantModelShell.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala index 5d6bc3c269..ff5fbc451a 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala @@ -234,7 +234,7 @@ final case class ParticipantModelShell[ val op = model.zeroPowerOperatingPoint // If the model is not active *yet*, schedule the operation start - val nextTick = Option.when(operationInterval.start < currentTick)( + val nextTick = Option.when(operationInterval.start > currentTick)( operationInterval.start ) From 79308f25a5eb81d0c7ad8caa47e51857f518ca12 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Mon, 25 Nov 2024 21:43:52 +0100 Subject: [PATCH 50/77] OperationInterval is now right-open Signed-off-by: Sebastian Peter --- src/main/scala/edu/ie3/util/scala/OperationInterval.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/scala/edu/ie3/util/scala/OperationInterval.scala b/src/main/scala/edu/ie3/util/scala/OperationInterval.scala index 65eb4ff7db..9e4fd7bd0f 100644 --- a/src/main/scala/edu/ie3/util/scala/OperationInterval.scala +++ b/src/main/scala/edu/ie3/util/scala/OperationInterval.scala @@ -6,10 +6,10 @@ package edu.ie3.util.scala -import edu.ie3.util.interval.ClosedInterval +import edu.ie3.util.interval.{ClosedInterval, RightOpenInterval} /** Wrapper class for an operation interval, as the superclass - * [[ClosedInterval]] only accepts [[java.lang.Long]] as type parameter + * [[RightOpenInterval]] only accepts [[java.lang.Long]] as type parameter * * @param start * Start of operation period (included) @@ -17,4 +17,4 @@ import edu.ie3.util.interval.ClosedInterval * End of operation period (included) */ final case class OperationInterval(start: Long, end: Long) - extends ClosedInterval[java.lang.Long](start, end) + extends RightOpenInterval[java.lang.Long](start, end) From 673b933e2d2479f8586ec97d1a5d1c22ccd775a7 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Mon, 25 Nov 2024 22:05:31 +0100 Subject: [PATCH 51/77] Enhanced ParticipantAgent tests Signed-off-by: Sebastian Peter --- .../participant2/MockParticipantModel.scala | 9 +- .../participant2/ParticipantAgentSpec.scala | 147 +++++++++++++++++- 2 files changed, 149 insertions(+), 7 deletions(-) diff --git a/src/test/scala/edu/ie3/simona/agent/participant2/MockParticipantModel.scala b/src/test/scala/edu/ie3/simona/agent/participant2/MockParticipantModel.scala index 6a88e24bb0..011a759c8e 100644 --- a/src/test/scala/edu/ie3/simona/agent/participant2/MockParticipantModel.scala +++ b/src/test/scala/edu/ie3/simona/agent/participant2/MockParticipantModel.scala @@ -37,6 +37,7 @@ class MockParticipantModel( override val sRated: ApparentPower = Kilovoltamperes(10), override val cosPhiRated: Double = 0.9, override val qControl: QControl = CosPhiFixed(0.9), + mockActivationTicks: Map[Long, Long], ) extends ParticipantModel[ ActivePowerOperatingPoint, FixedState, @@ -50,8 +51,12 @@ class MockParticipantModel( override def determineOperatingPoint( state: FixedState, relevantData: FixedRelevantData.type, - ): (ActivePowerOperatingPoint, Option[Long]) = - (ActivePowerOperatingPoint(Kilowatts(5)), None) + ): (ActivePowerOperatingPoint, Option[Long]) = { + ( + ActivePowerOperatingPoint(Kilowatts(5)), + mockActivationTicks.get(state.tick), + ) + } override def zeroPowerOperatingPoint: ActivePowerOperatingPoint = ActivePowerOperatingPoint.zero diff --git a/src/test/scala/edu/ie3/simona/agent/participant2/ParticipantAgentSpec.scala b/src/test/scala/edu/ie3/simona/agent/participant2/ParticipantAgentSpec.scala index 77ab4bb535..0a418c9f63 100644 --- a/src/test/scala/edu/ie3/simona/agent/participant2/ParticipantAgentSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/participant2/ParticipantAgentSpec.scala @@ -14,6 +14,7 @@ import edu.ie3.simona.model.participant2.ParticipantModelShell import edu.ie3.simona.ontology.messages.SchedulerMessage.Completion import edu.ie3.simona.ontology.messages.{Activation, SchedulerMessage} import edu.ie3.simona.test.common.UnitSpec +import edu.ie3.simona.util.TickUtil.TickLong import edu.ie3.util.TimeUtil import edu.ie3.util.quantities.QuantityUtils.RichQuantityDouble import edu.ie3.util.scala.OperationInterval @@ -24,12 +25,12 @@ import java.time.ZonedDateTime class ParticipantAgentSpec extends ScalaTestWithActorTestKit with UnitSpec { - private val simulationStartDate: ZonedDateTime = + private implicit val simulationStartDate: ZonedDateTime = TimeUtil.withDefaults.toZonedDateTime("2020-01-01T00:00:00Z") - "A ParticipantAgent without services" should { + "A ParticipantAgent without secondary services" should { - "calculate operating point and results correctly" in { + "calculate operating point and results correctly with no additional ticks" in { val scheduler = createTestProbe[SchedulerMessage]() val gridAgent = createTestProbe[GridAgent.Request]() @@ -38,8 +39,9 @@ class ParticipantAgentSpec extends ScalaTestWithActorTestKit with UnitSpec { // receiving the activation adapter val receiveAdapter = createTestProbe[ActorRef[Activation]]() - val model = new MockParticipantModel() - val operationInterval = OperationInterval(0, 24 * 60 * 60) + // no additional activation ticks + val model = new MockParticipantModel(mockActivationTicks = Map.empty) + val operationInterval = OperationInterval(8 * 3600, 16 * 3600) spawn( ParticipantAgentMockFactory.create( @@ -61,12 +63,30 @@ class ParticipantAgentSpec extends ScalaTestWithActorTestKit with UnitSpec { ) val activationRef = receiveAdapter.expectMessageType[ActorRef[Activation]] + // TICK 0: Outside of operation interval + activationRef ! Activation(0) resultListener.expectMessageType[ParticipantResultEvent] match { case ParticipantResultEvent(result: MockResult) => result.getInputModel shouldBe model.uuid result.getTime shouldBe simulationStartDate + result.getP should equalWithTolerance(0.0.asMegaWatt) + result.getQ should equalWithTolerance(0.0.asMegaVar) + } + + scheduler.expectMessage( + Completion(activationRef, Some(operationInterval.start)) + ) + + // TICK 8 * 3600: Start of operation interval + + activationRef ! Activation(operationInterval.start) + + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: MockResult) => + result.getInputModel shouldBe model.uuid + result.getTime shouldBe operationInterval.start.toDateTime result.getP should equalWithTolerance(0.005.asMegaWatt) result.getQ should equalWithTolerance(0.002421610524.asMegaVar) } @@ -75,6 +95,123 @@ class ParticipantAgentSpec extends ScalaTestWithActorTestKit with UnitSpec { Completion(activationRef, Some(operationInterval.end)) ) + // TICK 16 * 3600: Outside of operation interval (last tick) + + activationRef ! Activation(operationInterval.end) + + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: MockResult) => + result.getInputModel shouldBe model.uuid + result.getTime shouldBe operationInterval.end.toDateTime + result.getP should equalWithTolerance(0.0.asMegaWatt) + result.getQ should equalWithTolerance(0.0.asMegaVar) + } + + scheduler.expectMessage(Completion(activationRef)) + + } + + "calculate operating point and results correctly with additional ticks" in { + + val scheduler = createTestProbe[SchedulerMessage]() + val gridAgent = createTestProbe[GridAgent.Request]() + val resultListener = createTestProbe[ResultEvent]() + + // receiving the activation adapter + val receiveAdapter = createTestProbe[ActorRef[Activation]]() + + // no additional activation ticks + val model = new MockParticipantModel(mockActivationTicks = + Map( + 0 * 3600L -> 4 * 3600L, // still before operation, is ignored + 8 * 3600L -> 12 * 3600L, // middle of operation + 12 * 3600L -> 20 * 3600L, // after operation, is ignored + ) + ) + val operationInterval = OperationInterval(8 * 3600, 16 * 3600) + + spawn( + ParticipantAgentMockFactory.create( + ParticipantModelShell( + model, + operationInterval, + simulationStartDate, + ), + ParticipantInputHandler( + Map.empty + ), + ParticipantGridAdapter( + gridAgent.ref, + 3600, + ), + Iterable(resultListener.ref), + Left(scheduler.ref, receiveAdapter.ref), + ) + ) + val activationRef = receiveAdapter.expectMessageType[ActorRef[Activation]] + + // TICK 0: Outside of operation interval + + activationRef ! Activation(0) + + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: MockResult) => + result.getInputModel shouldBe model.uuid + result.getTime shouldBe simulationStartDate + result.getP should equalWithTolerance(0.0.asMegaWatt) + result.getQ should equalWithTolerance(0.0.asMegaVar) + } + + scheduler.expectMessage( + Completion(activationRef, Some(operationInterval.start)) + ) + + // TICK 8 * 3600: Start of operation interval + + activationRef ! Activation(operationInterval.start) + + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: MockResult) => + result.getInputModel shouldBe model.uuid + result.getTime shouldBe operationInterval.start.toDateTime + result.getP should equalWithTolerance(0.005.asMegaWatt) + result.getQ should equalWithTolerance(0.002421610524.asMegaVar) + } + + scheduler.expectMessage( + Completion(activationRef, Some(12 * 3600)) + ) + + // TICK 12 * 3600: Middle of operation interval + + activationRef ! Activation(12 * 3600) + + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: MockResult) => + result.getInputModel shouldBe model.uuid + result.getTime shouldBe (12 * 3600).toDateTime + result.getP should equalWithTolerance(0.005.asMegaWatt) + result.getQ should equalWithTolerance(0.002421610524.asMegaVar) + } + + scheduler.expectMessage( + Completion(activationRef, Some(operationInterval.end)) + ) + + // TICK 16 * 3600: Outside of operation interval (last tick) + + activationRef ! Activation(operationInterval.end) + + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: MockResult) => + result.getInputModel shouldBe model.uuid + result.getTime shouldBe operationInterval.end.toDateTime + result.getP should equalWithTolerance(0.0.asMegaWatt) + result.getQ should equalWithTolerance(0.0.asMegaVar) + } + + scheduler.expectMessage(Completion(activationRef)) + } } From 044209b80d3e8d02c3e102796d81c52412598955 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Mon, 25 Nov 2024 22:06:38 +0100 Subject: [PATCH 52/77] Removing unnecessary imports Signed-off-by: Sebastian Peter --- src/main/scala/edu/ie3/util/scala/OperationInterval.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/edu/ie3/util/scala/OperationInterval.scala b/src/main/scala/edu/ie3/util/scala/OperationInterval.scala index 9e4fd7bd0f..fe80870ac0 100644 --- a/src/main/scala/edu/ie3/util/scala/OperationInterval.scala +++ b/src/main/scala/edu/ie3/util/scala/OperationInterval.scala @@ -6,7 +6,7 @@ package edu.ie3.util.scala -import edu.ie3.util.interval.{ClosedInterval, RightOpenInterval} +import edu.ie3.util.interval.RightOpenInterval /** Wrapper class for an operation interval, as the superclass * [[RightOpenInterval]] only accepts [[java.lang.Long]] as type parameter From 2043308307f77f6dde431ed9c8e7edd35160e0ae Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Tue, 26 Nov 2024 17:23:16 +0100 Subject: [PATCH 53/77] Some fixes and improvements to ParticipantAgent Signed-off-by: Sebastian Peter --- .../agent/participant2/ParticipantAgent.scala | 61 ++++++++++++------- .../participant2/ParticipantGridAdapter.scala | 2 +- 2 files changed, 41 insertions(+), 22 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgent.scala b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgent.scala index 501fb9d76f..0dfa9167b3 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgent.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgent.scala @@ -179,7 +179,7 @@ object ParticipantAgent { case (_, activation: ActivationRequest) => val coreWithActivation = inputHandler.handleActivation(activation) - val (updatedShell, updatedCore, updatedGridAdapter) = + val (updatedShell, updatedInputHandler, updatedGridAdapter) = maybeCalculate( modelShell, coreWithActivation, @@ -190,19 +190,19 @@ object ParticipantAgent { ParticipantAgent( updatedShell, - updatedCore, + updatedInputHandler, updatedGridAdapter, resultListener, parentData, ) case (_, msg: ProvisionMessage[Data]) => - val coreWithData = inputHandler.handleDataProvision(msg) + val inputHandlerWithData = inputHandler.handleDataProvision(msg) - val (updatedShell, updatedCore, updatedGridAdapter) = + val (updatedShell, updatedInputHandler, updatedGridAdapter) = maybeCalculate( modelShell, - coreWithData, + inputHandlerWithData, gridAdapter, resultListener, parentData, @@ -210,7 +210,7 @@ object ParticipantAgent { ParticipantAgent( updatedShell, - updatedCore, + updatedInputHandler, updatedGridAdapter, resultListener, parentData, @@ -264,12 +264,22 @@ object ParticipantAgent { ) case (_, FinishParticipantSimulation(_, nextRequestTick)) => - val updatedGridAdapter = + val gridAdapterFinished = gridAdapter.updateNextRequestTick(nextRequestTick) + // Possibly start simulation if we've been activated + val (updatedShell, updatedInputHandler, updatedGridAdapter) = + maybeCalculate( + modelShell, + inputHandler, + gridAdapterFinished, + resultListener, + parentData, + ) + ParticipantAgent( - modelShell, - inputHandler, + updatedShell, + updatedInputHandler, updatedGridAdapter, resultListener, parentData, @@ -287,7 +297,7 @@ object ParticipantAgent { ParticipantInputHandler, ParticipantGridAdapter, ) = { - if (isDataComplete(inputHandler, gridAdapter)) { + if (isReadyForCalculation(inputHandler, gridAdapter)) { val activation = inputHandler.activation.getOrElse( throw new CriticalFailureException( @@ -381,9 +391,9 @@ object ParticipantAgent { "Received issue flex control while not controlled by EM" ), _.emAgent ! FlexCompletion( - shell.model.uuid, - shell.modelChange.changesAtNextActivation, - shell.modelChange.changesAtTick, + modelWithOP.model.uuid, + modelWithOP.modelChange.changesAtNextActivation, + modelWithOP.modelChange.changesAtTick, ), ) @@ -397,18 +407,27 @@ object ParticipantAgent { (modelShell, inputHandler, gridAdapter) } - private def isDataComplete( + /** Checks if all requirements for calculation have been met. These are: + * - agent is activated (activation has been received and not completed + * yet) + * - all required data has been received + * - the grid adapter is not waiting for power requests + * + * @param inputHandler + * The participant input handler + * @param gridAdapter + * The participant grid adapter + * @return + * Whether power can be calculated or not. + */ + private def isReadyForCalculation( inputHandler: ParticipantInputHandler, gridAdapter: ParticipantGridAdapter, ): Boolean = { - lazy val activation = inputHandler.activation.getOrElse( - throw new CriticalFailureException( - "Activation should be present when data collection is complete" - ) - ) - inputHandler.isComplete && - !gridAdapter.isPowerRequestAwaited(activation.tick) + inputHandler.activation.exists(activation => + !gridAdapter.isPowerRequestAwaited(activation.tick) + ) } } diff --git a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantGridAdapter.scala b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantGridAdapter.scala index 7fba50ae9b..8f9c3e7d6b 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantGridAdapter.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantGridAdapter.scala @@ -92,7 +92,7 @@ final case class ParticipantGridAdapter( Right(windowEnd, currentTick) case None => // No results have been calculated whatsoever, calculate from simulation start (0) - Right(0, currentTick) + Right(0L, currentTick) }).fold( cachedResult => cachedResult.copy(newResult = false), { case (windowStart: Long, windowEnd: Long) => From 9c5dad7f7e9e08151971a8b4f7dee7bf24266d03 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Tue, 26 Nov 2024 17:23:27 +0100 Subject: [PATCH 54/77] Enhancing test Signed-off-by: Sebastian Peter --- .../participant2/MockParticipantModel.scala | 14 +- .../participant2/ParticipantAgentSpec.scala | 741 ++++++++++++++---- 2 files changed, 595 insertions(+), 160 deletions(-) diff --git a/src/test/scala/edu/ie3/simona/agent/participant2/MockParticipantModel.scala b/src/test/scala/edu/ie3/simona/agent/participant2/MockParticipantModel.scala index 011a759c8e..b994a38119 100644 --- a/src/test/scala/edu/ie3/simona/agent/participant2/MockParticipantModel.scala +++ b/src/test/scala/edu/ie3/simona/agent/participant2/MockParticipantModel.scala @@ -21,6 +21,7 @@ import edu.ie3.simona.model.participant2.ParticipantModel.{ ParticipantFixedState, } import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage +import edu.ie3.simona.ontology.messages.flex.MinMaxFlexibilityMessage.ProvideMinMaxFlexOptions import edu.ie3.simona.service.ServiceType import edu.ie3.util.scala.quantities.{ApparentPower, Kilovoltamperes} import squants.Dimensionless @@ -53,7 +54,7 @@ class MockParticipantModel( relevantData: FixedRelevantData.type, ): (ActivePowerOperatingPoint, Option[Long]) = { ( - ActivePowerOperatingPoint(Kilowatts(5)), + ActivePowerOperatingPoint(Kilowatts(6)), mockActivationTicks.get(state.tick), ) } @@ -80,7 +81,7 @@ class MockParticipantModel( override def createPrimaryDataResult( data: PrimaryData.PrimaryDataWithComplexPower[_], dateTime: ZonedDateTime, - ): SystemParticipantResult = ??? + ): SystemParticipantResult = throw new NotImplementedError() // Not tested override def getRequiredSecondaryServices: Iterable[ServiceType] = Iterable.empty @@ -95,14 +96,19 @@ class MockParticipantModel( override def calcFlexOptions( state: FixedState, relevantData: FixedRelevantData.type, - ): FlexibilityMessage.ProvideFlexOptions = ??? + ): FlexibilityMessage.ProvideFlexOptions = + ProvideMinMaxFlexOptions(uuid, Kilowatts(1), Kilowatts(-1), Kilowatts(3)) override def handlePowerControl( state: FixedState, relevantData: FixedRelevantData.type, flexOptions: FlexibilityMessage.ProvideFlexOptions, setPower: Power, - ): (ActivePowerOperatingPoint, ModelChangeIndicator) = ??? + ): (ActivePowerOperatingPoint, ModelChangeIndicator) = + ( + ActivePowerOperatingPoint(setPower), + ModelChangeIndicator(changesAtTick = mockActivationTicks.get(state.tick)), + ) } object MockParticipantModel { diff --git a/src/test/scala/edu/ie3/simona/agent/participant2/ParticipantAgentSpec.scala b/src/test/scala/edu/ie3/simona/agent/participant2/ParticipantAgentSpec.scala index 0a418c9f63..699d2e070d 100644 --- a/src/test/scala/edu/ie3/simona/agent/participant2/ParticipantAgentSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/participant2/ParticipantAgentSpec.scala @@ -7,210 +7,639 @@ package edu.ie3.simona.agent.participant2 import edu.ie3.simona.agent.grid.GridAgent +import edu.ie3.simona.agent.grid.GridAgentMessages.{ + AssetPowerChangedMessage, + AssetPowerUnchangedMessage, +} import edu.ie3.simona.agent.participant2.MockParticipantModel.MockResult +import edu.ie3.simona.agent.participant2.ParticipantAgent.{ + FinishParticipantSimulation, + RequestAssetPowerMessage, +} import edu.ie3.simona.event.ResultEvent import edu.ie3.simona.event.ResultEvent.ParticipantResultEvent import edu.ie3.simona.model.participant2.ParticipantModelShell import edu.ie3.simona.ontology.messages.SchedulerMessage.Completion +import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage.{ + FlexActivation, + FlexCompletion, + FlexRequest, + FlexResponse, + IssueNoControl, + IssuePowerControl, +} +import edu.ie3.simona.ontology.messages.flex.MinMaxFlexibilityMessage.ProvideMinMaxFlexOptions import edu.ie3.simona.ontology.messages.{Activation, SchedulerMessage} import edu.ie3.simona.test.common.UnitSpec import edu.ie3.simona.util.TickUtil.TickLong import edu.ie3.util.TimeUtil import edu.ie3.util.quantities.QuantityUtils.RichQuantityDouble import edu.ie3.util.scala.OperationInterval +import edu.ie3.util.scala.quantities.{Kilovars, ReactivePower} import org.apache.pekko.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit import org.apache.pekko.actor.typed.ActorRef +import squants.{Each, Power} +import squants.energy.Kilowatts import java.time.ZonedDateTime +/** Test for [[ParticipantAgent]] and [[ParticipantModelShell]] using a mock + * participant [[MockParticipantModel]] + */ class ParticipantAgentSpec extends ScalaTestWithActorTestKit with UnitSpec { private implicit val simulationStartDate: ZonedDateTime = TimeUtil.withDefaults.toZonedDateTime("2020-01-01T00:00:00Z") - "A ParticipantAgent without secondary services" should { - - "calculate operating point and results correctly with no additional ticks" in { - - val scheduler = createTestProbe[SchedulerMessage]() - val gridAgent = createTestProbe[GridAgent.Request]() - val resultListener = createTestProbe[ResultEvent]() - - // receiving the activation adapter - val receiveAdapter = createTestProbe[ActorRef[Activation]]() - - // no additional activation ticks - val model = new MockParticipantModel(mockActivationTicks = Map.empty) - val operationInterval = OperationInterval(8 * 3600, 16 * 3600) - - spawn( - ParticipantAgentMockFactory.create( - ParticipantModelShell( - model, - operationInterval, - simulationStartDate, - ), - ParticipantInputHandler( - Map.empty - ), - ParticipantGridAdapter( - gridAgent.ref, - 3600, - ), - Iterable(resultListener.ref), - Left(scheduler.ref, receiveAdapter.ref), - ) - ) - val activationRef = receiveAdapter.expectMessageType[ActorRef[Activation]] - - // TICK 0: Outside of operation interval - - activationRef ! Activation(0) - - resultListener.expectMessageType[ParticipantResultEvent] match { - case ParticipantResultEvent(result: MockResult) => - result.getInputModel shouldBe model.uuid - result.getTime shouldBe simulationStartDate - result.getP should equalWithTolerance(0.0.asMegaWatt) - result.getQ should equalWithTolerance(0.0.asMegaVar) - } + private implicit val activePowerTolerance: Power = Kilowatts(1e-10) + private implicit val reactivePowerTolerance: ReactivePower = Kilovars(1e-10) + + "A ParticipantAgent without secondary services" when { + + "not flex-controlled" should { + + "calculate operating point and results correctly with no additional ticks" in { + + val scheduler = createTestProbe[SchedulerMessage]() + val gridAgent = createTestProbe[GridAgent.Request]() + val resultListener = createTestProbe[ResultEvent]() + + // receiving the activation adapter + val receiveAdapter = createTestProbe[ActorRef[Activation]]() + + // no additional activation ticks + val model = new MockParticipantModel(mockActivationTicks = Map.empty) + val operationInterval = OperationInterval(8 * 3600, 20 * 3600) + + val participantAgent = spawn( + ParticipantAgentMockFactory.create( + ParticipantModelShell( + model, + operationInterval, + simulationStartDate, + ), + ParticipantInputHandler( + Map.empty + ), + ParticipantGridAdapter( + gridAgent.ref, + 12 * 3600, + ), + Iterable(resultListener.ref), + Left(scheduler.ref, receiveAdapter.ref), + ) + ) + val activationRef = + receiveAdapter.expectMessageType[ActorRef[Activation]] - scheduler.expectMessage( - Completion(activationRef, Some(operationInterval.start)) - ) + // TICK 0: Outside of operation interval - // TICK 8 * 3600: Start of operation interval + activationRef ! Activation(0) - activationRef ! Activation(operationInterval.start) + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: MockResult) => + result.getInputModel shouldBe model.uuid + result.getTime shouldBe simulationStartDate + result.getP should equalWithTolerance(0.0.asMegaWatt) + result.getQ should equalWithTolerance(0.0.asMegaVar) + } - resultListener.expectMessageType[ParticipantResultEvent] match { - case ParticipantResultEvent(result: MockResult) => - result.getInputModel shouldBe model.uuid - result.getTime shouldBe operationInterval.start.toDateTime - result.getP should equalWithTolerance(0.005.asMegaWatt) - result.getQ should equalWithTolerance(0.002421610524.asMegaVar) - } + scheduler.expectMessage( + Completion(activationRef, Some(operationInterval.start)) + ) - scheduler.expectMessage( - Completion(activationRef, Some(operationInterval.end)) - ) + // TICK 8 * 3600: Start of operation interval - // TICK 16 * 3600: Outside of operation interval (last tick) + activationRef ! Activation(operationInterval.start) - activationRef ! Activation(operationInterval.end) + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: MockResult) => + result.getInputModel shouldBe model.uuid + result.getTime shouldBe operationInterval.start.toDateTime + result.getP should equalWithTolerance(0.006.asMegaWatt) + result.getQ should equalWithTolerance(0.00290593262.asMegaVar) + } - resultListener.expectMessageType[ParticipantResultEvent] match { - case ParticipantResultEvent(result: MockResult) => - result.getInputModel shouldBe model.uuid - result.getTime shouldBe operationInterval.end.toDateTime - result.getP should equalWithTolerance(0.0.asMegaWatt) - result.getQ should equalWithTolerance(0.0.asMegaVar) - } + scheduler.expectMessage( + Completion(activationRef, Some(operationInterval.end)) + ) - scheduler.expectMessage(Completion(activationRef)) + // TICK 12 * 3600: GridAgent requests power - } + // first request + participantAgent ! RequestAssetPowerMessage(12 * 3600, Each(1), Each(0)) - "calculate operating point and results correctly with additional ticks" in { - - val scheduler = createTestProbe[SchedulerMessage]() - val gridAgent = createTestProbe[GridAgent.Request]() - val resultListener = createTestProbe[ResultEvent]() - - // receiving the activation adapter - val receiveAdapter = createTestProbe[ActorRef[Activation]]() - - // no additional activation ticks - val model = new MockParticipantModel(mockActivationTicks = - Map( - 0 * 3600L -> 4 * 3600L, // still before operation, is ignored - 8 * 3600L -> 12 * 3600L, // middle of operation - 12 * 3600L -> 20 * 3600L, // after operation, is ignored - ) - ) - val operationInterval = OperationInterval(8 * 3600, 16 * 3600) - - spawn( - ParticipantAgentMockFactory.create( - ParticipantModelShell( - model, - operationInterval, - simulationStartDate, - ), - ParticipantInputHandler( - Map.empty - ), - ParticipantGridAdapter( - gridAgent.ref, - 3600, - ), - Iterable(resultListener.ref), - Left(scheduler.ref, receiveAdapter.ref), - ) - ) - val activationRef = receiveAdapter.expectMessageType[ActorRef[Activation]] - - // TICK 0: Outside of operation interval - - activationRef ! Activation(0) - - resultListener.expectMessageType[ParticipantResultEvent] match { - case ParticipantResultEvent(result: MockResult) => - result.getInputModel shouldBe model.uuid - result.getTime shouldBe simulationStartDate - result.getP should equalWithTolerance(0.0.asMegaWatt) - result.getQ should equalWithTolerance(0.0.asMegaVar) - } + gridAgent.expectMessageType[AssetPowerChangedMessage] match { + case AssetPowerChangedMessage(p, q) => + p should approximate(Kilowatts(2)) + q should approximate(Kilovars(0.968644209676)) + } + + // second request with same voltage + participantAgent ! RequestAssetPowerMessage(12 * 3600, Each(1), Each(0)) + + gridAgent.expectMessageType[AssetPowerUnchangedMessage] match { + case AssetPowerUnchangedMessage(p, q) => + p should approximate(Kilowatts(2)) + q should approximate(Kilovars(0.968644209676)) + } - scheduler.expectMessage( - Completion(activationRef, Some(operationInterval.start)) - ) + // third request with different voltage + participantAgent ! RequestAssetPowerMessage( + 12 * 3600, + Each(0.98), + Each(0), + ) + + gridAgent.expectMessageType[AssetPowerChangedMessage] match { + case AssetPowerChangedMessage(p, q) => + p should approximate(Kilowatts(2)) + // not voltage dependent + q should approximate(Kilovars(0.968644209676)) + } + + participantAgent ! FinishParticipantSimulation(12 * 3600, 24 * 3600) + + // TICK 20 * 3600: Outside of operation interval (last tick) + + activationRef ! Activation(operationInterval.end) + + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: MockResult) => + result.getInputModel shouldBe model.uuid + result.getTime shouldBe operationInterval.end.toDateTime + result.getP should equalWithTolerance(0.0.asMegaWatt) + result.getQ should equalWithTolerance(0.0.asMegaVar) + } + + scheduler.expectMessage(Completion(activationRef)) - // TICK 8 * 3600: Start of operation interval + // TICK 24 * 3600: GridAgent requests power - activationRef ! Activation(operationInterval.start) + participantAgent ! RequestAssetPowerMessage(24 * 3600, Each(1), Each(0)) + + gridAgent.expectMessageType[AssetPowerChangedMessage] match { + case AssetPowerChangedMessage(p, q) => + p should approximate(Kilowatts(4)) + q should approximate(Kilovars(1.93728841935)) + } + + participantAgent ! FinishParticipantSimulation(24 * 3600, 36 * 3600) - resultListener.expectMessageType[ParticipantResultEvent] match { - case ParticipantResultEvent(result: MockResult) => - result.getInputModel shouldBe model.uuid - result.getTime shouldBe operationInterval.start.toDateTime - result.getP should equalWithTolerance(0.005.asMegaWatt) - result.getQ should equalWithTolerance(0.002421610524.asMegaVar) } - scheduler.expectMessage( - Completion(activationRef, Some(12 * 3600)) - ) + "calculate operating point and results correctly with additional ticks" in { + + val scheduler = createTestProbe[SchedulerMessage]() + val gridAgent = createTestProbe[GridAgent.Request]() + val resultListener = createTestProbe[ResultEvent]() + + // receiving the activation adapter + val receiveAdapter = createTestProbe[ActorRef[Activation]]() + + // no additional activation ticks + val model = new MockParticipantModel(mockActivationTicks = + Map( + 0 * 3600L -> 4 * 3600L, // still before operation, is ignored + 8 * 3600L -> 12 * 3600L, // middle of operation + 12 * 3600L -> 22 * 3600L, // after operation, is ignored + ) + ) + val operationInterval = OperationInterval(8 * 3600, 20 * 3600) + + val participantAgent = spawn( + ParticipantAgentMockFactory.create( + ParticipantModelShell( + model, + operationInterval, + simulationStartDate, + ), + ParticipantInputHandler( + Map.empty + ), + ParticipantGridAdapter( + gridAgent.ref, + 12 * 3600, + ), + Iterable(resultListener.ref), + Left(scheduler.ref, receiveAdapter.ref), + ) + ) + val activationRef = + receiveAdapter.expectMessageType[ActorRef[Activation]] + + // TICK 0: Outside of operation interval + + activationRef ! Activation(0) + + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: MockResult) => + result.getInputModel shouldBe model.uuid + result.getTime shouldBe simulationStartDate + result.getP should equalWithTolerance(0.0.asMegaWatt) + result.getQ should equalWithTolerance(0.0.asMegaVar) + } + + scheduler.expectMessage( + Completion(activationRef, Some(operationInterval.start)) + ) + + // TICK 8 * 3600: Start of operation interval + + activationRef ! Activation(operationInterval.start) + + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: MockResult) => + result.getInputModel shouldBe model.uuid + result.getTime shouldBe operationInterval.start.toDateTime + result.getP should equalWithTolerance(0.006.asMegaWatt) + result.getQ should equalWithTolerance(0.00290593262.asMegaVar) + } + + scheduler.expectMessage( + Completion(activationRef, Some(12 * 3600)) + ) + + // TICK 12 * 3600: Middle of operation interval and GridAgent requests power + + activationRef ! Activation(12 * 3600) + + participantAgent ! RequestAssetPowerMessage(12 * 3600, Each(1), Each(0)) + + gridAgent.expectMessageType[AssetPowerChangedMessage] match { + case AssetPowerChangedMessage(p, q) => + p should approximate(Kilowatts(2)) + q should approximate(Kilovars(0.968644209676)) + } + + resultListener.expectNoMessage() + scheduler.expectNoMessage() + + participantAgent ! FinishParticipantSimulation(12 * 3600, 24 * 3600) + + // calculation should start now + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: MockResult) => + result.getInputModel shouldBe model.uuid + result.getTime shouldBe (12 * 3600).toDateTime + result.getP should equalWithTolerance(0.006.asMegaWatt) + result.getQ should equalWithTolerance(0.00290593262.asMegaVar) + } + + scheduler.expectMessage( + Completion(activationRef, Some(operationInterval.end)) + ) + + // TICK 20 * 3600: Outside of operation interval (last tick) + + activationRef ! Activation(operationInterval.end) + + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: MockResult) => + result.getInputModel shouldBe model.uuid + result.getTime shouldBe operationInterval.end.toDateTime + result.getP should equalWithTolerance(0.0.asMegaWatt) + result.getQ should equalWithTolerance(0.0.asMegaVar) + } + + scheduler.expectMessage(Completion(activationRef)) + + // TICK 24 * 3600: GridAgent requests power - // TICK 12 * 3600: Middle of operation interval + participantAgent ! RequestAssetPowerMessage(24 * 3600, Each(1), Each(0)) - activationRef ! Activation(12 * 3600) + gridAgent.expectMessageType[AssetPowerChangedMessage] match { + case AssetPowerChangedMessage(p, q) => + p should approximate(Kilowatts(4)) + q should approximate(Kilovars(1.93728841935)) + } + + participantAgent ! FinishParticipantSimulation(24 * 3600, 36 * 3600) - resultListener.expectMessageType[ParticipantResultEvent] match { - case ParticipantResultEvent(result: MockResult) => - result.getInputModel shouldBe model.uuid - result.getTime shouldBe (12 * 3600).toDateTime - result.getP should equalWithTolerance(0.005.asMegaWatt) - result.getQ should equalWithTolerance(0.002421610524.asMegaVar) } - scheduler.expectMessage( - Completion(activationRef, Some(operationInterval.end)) - ) + } + + "flex-controlled" should { + + "calculate operating point and results correctly with no additional ticks" in { + + val em = createTestProbe[FlexResponse]() + val gridAgent = createTestProbe[GridAgent.Request]() + val resultListener = createTestProbe[ResultEvent]() + + // receiving the activation adapter + val receiveAdapter = createTestProbe[ActorRef[FlexRequest]]() + + // no additional activation ticks + val model = new MockParticipantModel(mockActivationTicks = Map.empty) + val operationInterval = OperationInterval(8 * 3600, 20 * 3600) + + val participantAgent = spawn( + ParticipantAgentMockFactory.create( + ParticipantModelShell( + model, + operationInterval, + simulationStartDate, + ), + ParticipantInputHandler( + Map.empty + ), + ParticipantGridAdapter( + gridAgent.ref, + 12 * 3600, + ), + Iterable(resultListener.ref), + Right(em.ref, receiveAdapter.ref), + ) + ) + val flexRef = receiveAdapter.expectMessageType[ActorRef[FlexRequest]] + + // TICK 0: Outside of operation interval + + flexRef ! FlexActivation(0) + + em.expectMessageType[ProvideMinMaxFlexOptions] match { + case ProvideMinMaxFlexOptions(modelUuid, ref, min, max) => + modelUuid shouldBe model.uuid + ref should approximate(Kilowatts(0)) + min should approximate(Kilowatts(0)) + max should approximate(Kilowatts(0)) + } + + flexRef ! IssueNoControl(0) + + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: MockResult) => + result.getInputModel shouldBe model.uuid + result.getTime shouldBe simulationStartDate + result.getP should equalWithTolerance(0.0.asMegaWatt) + result.getQ should equalWithTolerance(0.0.asMegaVar) + } + + em.expectMessage( + FlexCompletion( + model.uuid, + requestAtTick = Some(operationInterval.start), + ) + ) + + // TICK 8 * 3600: Start of operation interval + + flexRef ! FlexActivation(operationInterval.start) + + em.expectMessageType[ProvideMinMaxFlexOptions] match { + case ProvideMinMaxFlexOptions(modelUuid, ref, min, max) => + modelUuid shouldBe model.uuid + ref should approximate(Kilowatts(1)) + min should approximate(Kilowatts(-1)) + max should approximate(Kilowatts(3)) + } + + flexRef ! IssuePowerControl(operationInterval.start, Kilowatts(3)) + + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: MockResult) => + result.getInputModel shouldBe model.uuid + result.getTime shouldBe operationInterval.start.toDateTime + result.getP should equalWithTolerance(0.003.asMegaWatt) + result.getQ should equalWithTolerance(0.0014529663.asMegaVar) + } + + em.expectMessage( + FlexCompletion( + model.uuid, + requestAtTick = Some(operationInterval.end), + ) + ) + + // TICK 12 * 3600: GridAgent requests power + + participantAgent ! RequestAssetPowerMessage(12 * 3600, Each(1), Each(0)) + + gridAgent.expectMessageType[AssetPowerChangedMessage] match { + case AssetPowerChangedMessage(p, q) => + p should approximate(Kilowatts(1)) + q should approximate(Kilovars(0.48432210483)) + } + + participantAgent ! FinishParticipantSimulation(12 * 3600, 24 * 3600) + + // TICK 20 * 3600: Outside of operation interval (last tick) + + flexRef ! FlexActivation(operationInterval.end) - // TICK 16 * 3600: Outside of operation interval (last tick) + em.expectMessageType[ProvideMinMaxFlexOptions] match { + case ProvideMinMaxFlexOptions(modelUuid, ref, min, max) => + modelUuid shouldBe model.uuid + ref should approximate(Kilowatts(0)) + min should approximate(Kilowatts(0)) + max should approximate(Kilowatts(0)) + } - activationRef ! Activation(operationInterval.end) + flexRef ! IssueNoControl(operationInterval.end) + + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: MockResult) => + result.getInputModel shouldBe model.uuid + result.getTime shouldBe operationInterval.end.toDateTime + result.getP should equalWithTolerance(0.0.asMegaWatt) + result.getQ should equalWithTolerance(0.0.asMegaVar) + } + + em.expectMessage(FlexCompletion(model.uuid)) + + // TICK 24 * 3600: GridAgent requests power + + participantAgent ! RequestAssetPowerMessage(24 * 3600, Each(1), Each(0)) + + gridAgent.expectMessageType[AssetPowerChangedMessage] match { + case AssetPowerChangedMessage(p, q) => + p should approximate(Kilowatts(2)) + q should approximate(Kilovars(0.96864420966)) + } + + participantAgent ! FinishParticipantSimulation(24 * 3600, 36 * 3600) - resultListener.expectMessageType[ParticipantResultEvent] match { - case ParticipantResultEvent(result: MockResult) => - result.getInputModel shouldBe model.uuid - result.getTime shouldBe operationInterval.end.toDateTime - result.getP should equalWithTolerance(0.0.asMegaWatt) - result.getQ should equalWithTolerance(0.0.asMegaVar) } - scheduler.expectMessage(Completion(activationRef)) + "calculate operating point and results correctly with additional ticks" in { + + val em = createTestProbe[FlexResponse]() + val gridAgent = createTestProbe[GridAgent.Request]() + val resultListener = createTestProbe[ResultEvent]() + + // receiving the activation adapter + val receiveAdapter = createTestProbe[ActorRef[FlexRequest]]() + + // no additional activation ticks + val model = new MockParticipantModel(mockActivationTicks = + Map( + 0 * 3600L -> 4 * 3600L, // still before operation, is ignored + 8 * 3600L -> 12 * 3600L, // middle of operation + 12 * 3600L -> 22 * 3600L, // after operation, is ignored + ) + ) + val operationInterval = OperationInterval(8 * 3600, 20 * 3600) + + val participantAgent = spawn( + ParticipantAgentMockFactory.create( + ParticipantModelShell( + model, + operationInterval, + simulationStartDate, + ), + ParticipantInputHandler( + Map.empty + ), + ParticipantGridAdapter( + gridAgent.ref, + 12 * 3600, + ), + Iterable(resultListener.ref), + Right(em.ref, receiveAdapter.ref), + ) + ) + val flexRef = receiveAdapter.expectMessageType[ActorRef[FlexRequest]] + + // TICK 0: Outside of operation interval + + flexRef ! FlexActivation(0) + + em.expectMessageType[ProvideMinMaxFlexOptions] match { + case ProvideMinMaxFlexOptions(modelUuid, ref, min, max) => + modelUuid shouldBe model.uuid + ref should approximate(Kilowatts(0)) + min should approximate(Kilowatts(0)) + max should approximate(Kilowatts(0)) + } + + flexRef ! IssueNoControl(0) + + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: MockResult) => + result.getInputModel shouldBe model.uuid + result.getTime shouldBe simulationStartDate + result.getP should equalWithTolerance(0.0.asMegaWatt) + result.getQ should equalWithTolerance(0.0.asMegaVar) + } + + em.expectMessage( + FlexCompletion( + model.uuid, + requestAtTick = Some(operationInterval.start), + ) + ) + + // TICK 8 * 3600: Start of operation interval + + flexRef ! FlexActivation(operationInterval.start) + + em.expectMessageType[ProvideMinMaxFlexOptions] match { + case ProvideMinMaxFlexOptions(modelUuid, ref, min, max) => + modelUuid shouldBe model.uuid + ref should approximate(Kilowatts(1)) + min should approximate(Kilowatts(-1)) + max should approximate(Kilowatts(3)) + } + + flexRef ! IssuePowerControl(operationInterval.start, Kilowatts(3)) + + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: MockResult) => + result.getInputModel shouldBe model.uuid + result.getTime shouldBe operationInterval.start.toDateTime + result.getP should equalWithTolerance(0.003.asMegaWatt) + result.getQ should equalWithTolerance(0.0014529663.asMegaVar) + } + + em.expectMessage( + FlexCompletion( + model.uuid, + requestAtTick = Some(12 * 3600), + ) + ) + + // TICK 12 * 3600: Middle of operation interval and GridAgent requests power + + flexRef ! FlexActivation(12 * 3600) + + participantAgent ! RequestAssetPowerMessage(12 * 3600, Each(1), Each(0)) + + gridAgent.expectMessageType[AssetPowerChangedMessage] match { + case AssetPowerChangedMessage(p, q) => + p should approximate(Kilowatts(1)) + q should approximate(Kilovars(0.48432210483)) + } + + resultListener.expectNoMessage() + em.expectNoMessage() + + participantAgent ! FinishParticipantSimulation(12 * 3600, 24 * 3600) + + // calculation should start now + em.expectMessageType[ProvideMinMaxFlexOptions] match { + case ProvideMinMaxFlexOptions(modelUuid, ref, min, max) => + modelUuid shouldBe model.uuid + ref should approximate(Kilowatts(1)) + min should approximate(Kilowatts(-1)) + max should approximate(Kilowatts(3)) + } + + flexRef ! IssueNoControl(12 * 3600) + + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: MockResult) => + result.getInputModel shouldBe model.uuid + result.getTime shouldBe (12 * 3600).toDateTime + result.getP should equalWithTolerance(0.001.asMegaWatt) + result.getQ should equalWithTolerance(0.0004843221.asMegaVar) + } + + em.expectMessage( + FlexCompletion( + model.uuid, + requestAtTick = Some(operationInterval.end), + ) + ) + + // TICK 20 * 3600: Outside of operation interval (last tick) + + flexRef ! FlexActivation(operationInterval.end) + + em.expectMessageType[ProvideMinMaxFlexOptions] match { + case ProvideMinMaxFlexOptions(modelUuid, ref, min, max) => + modelUuid shouldBe model.uuid + ref should approximate(Kilowatts(0)) + min should approximate(Kilowatts(0)) + max should approximate(Kilowatts(0)) + } + + flexRef ! IssueNoControl(operationInterval.end) + + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: MockResult) => + result.getInputModel shouldBe model.uuid + result.getTime shouldBe operationInterval.end.toDateTime + result.getP should equalWithTolerance(0.0.asMegaWatt) + result.getQ should equalWithTolerance(0.0.asMegaVar) + } + + em.expectMessage(FlexCompletion(model.uuid)) + + // TICK 24 * 3600: GridAgent requests power + + participantAgent ! RequestAssetPowerMessage(24 * 3600, Each(1), Each(0)) + + gridAgent.expectMessageType[AssetPowerChangedMessage] match { + case AssetPowerChangedMessage(p, q) => + p should approximate(Kilowatts(0.6666666667)) + q should approximate(Kilovars(0.32288140322)) + } + + participantAgent ! FinishParticipantSimulation(24 * 3600, 36 * 3600) + + } } From 45897ff9e733600aa5d489274506e18e7a87cb3b Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Tue, 26 Nov 2024 17:25:51 +0100 Subject: [PATCH 55/77] Removing dummy code Signed-off-by: Sebastian Peter --- .../edu/ie3/simona/model/participant2/ParticipantModel.scala | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala index 728275b3bc..4582512366 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala @@ -201,10 +201,6 @@ object ParticipantModel { val reactivePower: Option[ReactivePower] } - object OperatingPoint { - def a: String = "a" - } - final case class ActivePowerOperatingPoint(override val activePower: Power) extends OperatingPoint { override val reactivePower: Option[ReactivePower] = None From f2ad723520254cb6730d541d3b77aba0b11f710e Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Tue, 26 Nov 2024 17:26:10 +0100 Subject: [PATCH 56/77] Adding todo Signed-off-by: Sebastian Peter --- .../ie3/simona/agent/participant2/ParticipantAgentSpec.scala | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/test/scala/edu/ie3/simona/agent/participant2/ParticipantAgentSpec.scala b/src/test/scala/edu/ie3/simona/agent/participant2/ParticipantAgentSpec.scala index 699d2e070d..167dc96090 100644 --- a/src/test/scala/edu/ie3/simona/agent/participant2/ParticipantAgentSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/participant2/ParticipantAgentSpec.scala @@ -643,6 +643,8 @@ class ParticipantAgentSpec extends ScalaTestWithActorTestKit with UnitSpec { } + // todo testing handleRequest + } } From bdc36e00416458a5e840a17fbbde2d05609a2b31 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Tue, 26 Nov 2024 20:37:49 +0100 Subject: [PATCH 57/77] Testing participant requests Signed-off-by: Sebastian Peter --- .../messages/services/EvMessage.scala | 4 +-- .../participant2/MockParticipantModel.scala | 29 ++++++++++++++++++- .../participant2/ParticipantAgentSpec.scala | 24 +++++++++++++-- 3 files changed, 51 insertions(+), 6 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/ontology/messages/services/EvMessage.scala b/src/main/scala/edu/ie3/simona/ontology/messages/services/EvMessage.scala index dde0b0b422..fe832cfe07 100644 --- a/src/main/scala/edu/ie3/simona/ontology/messages/services/EvMessage.scala +++ b/src/main/scala/edu/ie3/simona/ontology/messages/services/EvMessage.scala @@ -59,7 +59,7 @@ object EvMessage { * @param replyTo * The actor to receive the response */ - final case class EvFreeLotsRequest(tick: Long, replyTo: ActorRef) + final case class EvFreeLotsRequest(override val tick: Long, replyTo: ActorRef) extends ParticipantRequest /** Requests EV models of departing EVs with given UUIDs @@ -72,7 +72,7 @@ object EvMessage { * The actor to receive the response */ final case class DepartingEvsRequest( - tick: Long, + override val tick: Long, departingEvs: Seq[UUID], replyTo: ActorRef, ) extends ParticipantRequest diff --git a/src/test/scala/edu/ie3/simona/agent/participant2/MockParticipantModel.scala b/src/test/scala/edu/ie3/simona/agent/participant2/MockParticipantModel.scala index b994a38119..ef164f055a 100644 --- a/src/test/scala/edu/ie3/simona/agent/participant2/MockParticipantModel.scala +++ b/src/test/scala/edu/ie3/simona/agent/participant2/MockParticipantModel.scala @@ -9,7 +9,12 @@ package edu.ie3.simona.agent.participant2 import edu.ie3.datamodel.models.result.system.SystemParticipantResult import edu.ie3.simona.agent.participant.data.Data import edu.ie3.simona.agent.participant.data.Data.PrimaryData -import edu.ie3.simona.agent.participant2.MockParticipantModel.MockResult +import edu.ie3.simona.agent.participant2.MockParticipantModel.{ + MockRequestMessage, + MockResponseMessage, + MockResult, +} +import edu.ie3.simona.agent.participant2.ParticipantAgent.ParticipantRequest import edu.ie3.simona.model.participant.control.QControl import edu.ie3.simona.model.participant.control.QControl.CosPhiFixed import edu.ie3.simona.model.participant2.ParticipantModel @@ -28,6 +33,8 @@ import squants.Dimensionless import squants.energy.{Kilowatts, Power} import tech.units.indriya.ComparableQuantity import edu.ie3.util.quantities.QuantityUtils.RichQuantityDouble +import org.apache.pekko.actor.typed.ActorRef +import org.apache.pekko.actor.typed.scaladsl.ActorContext import javax.measure.quantity.{Power => QuantPower} import java.time.ZonedDateTime @@ -109,6 +116,19 @@ class MockParticipantModel( ActivePowerOperatingPoint(setPower), ModelChangeIndicator(changesAtTick = mockActivationTicks.get(state.tick)), ) + + override def handleRequest( + state: FixedState, + ctx: ActorContext[ParticipantAgent.Request], + msg: ParticipantRequest, + ): FixedState = { + msg match { + case MockRequestMessage(_, replyTo) => + replyTo ! MockResponseMessage + } + + state + } } object MockParticipantModel { @@ -120,4 +140,11 @@ object MockParticipantModel { q: ComparableQuantity[QuantPower], ) extends SystemParticipantResult(time, inputModel, p, q) + final case class MockRequestMessage( + override val tick: Long, + replyTo: ActorRef[MockResponseMessage.type], + ) extends ParticipantRequest + + case object MockResponseMessage + } diff --git a/src/test/scala/edu/ie3/simona/agent/participant2/ParticipantAgentSpec.scala b/src/test/scala/edu/ie3/simona/agent/participant2/ParticipantAgentSpec.scala index 167dc96090..564084a08d 100644 --- a/src/test/scala/edu/ie3/simona/agent/participant2/ParticipantAgentSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/participant2/ParticipantAgentSpec.scala @@ -11,7 +11,11 @@ import edu.ie3.simona.agent.grid.GridAgentMessages.{ AssetPowerChangedMessage, AssetPowerUnchangedMessage, } -import edu.ie3.simona.agent.participant2.MockParticipantModel.MockResult +import edu.ie3.simona.agent.participant2.MockParticipantModel.{ + MockRequestMessage, + MockResponseMessage, + MockResult, +} import edu.ie3.simona.agent.participant2.ParticipantAgent.{ FinishParticipantSimulation, RequestAssetPowerMessage, @@ -63,6 +67,7 @@ class ParticipantAgentSpec extends ScalaTestWithActorTestKit with UnitSpec { val scheduler = createTestProbe[SchedulerMessage]() val gridAgent = createTestProbe[GridAgent.Request]() val resultListener = createTestProbe[ResultEvent]() + val responseReceiver = createTestProbe[MockResponseMessage.type]() // receiving the activation adapter val receiveAdapter = createTestProbe[ActorRef[Activation]]() @@ -94,6 +99,9 @@ class ParticipantAgentSpec extends ScalaTestWithActorTestKit with UnitSpec { // TICK 0: Outside of operation interval + participantAgent ! MockRequestMessage(0, responseReceiver.ref) + responseReceiver.expectMessage(MockResponseMessage) + activationRef ! Activation(0) resultListener.expectMessageType[ParticipantResultEvent] match { @@ -110,6 +118,9 @@ class ParticipantAgentSpec extends ScalaTestWithActorTestKit with UnitSpec { // TICK 8 * 3600: Start of operation interval + participantAgent ! MockRequestMessage(0, responseReceiver.ref) + responseReceiver.expectMessage(MockResponseMessage) + activationRef ! Activation(operationInterval.start) resultListener.expectMessageType[ParticipantResultEvent] match { @@ -126,6 +137,9 @@ class ParticipantAgentSpec extends ScalaTestWithActorTestKit with UnitSpec { // TICK 12 * 3600: GridAgent requests power + participantAgent ! MockRequestMessage(0, responseReceiver.ref) + responseReceiver.expectMessage(MockResponseMessage) + // first request participantAgent ! RequestAssetPowerMessage(12 * 3600, Each(1), Each(0)) @@ -162,6 +176,9 @@ class ParticipantAgentSpec extends ScalaTestWithActorTestKit with UnitSpec { // TICK 20 * 3600: Outside of operation interval (last tick) + participantAgent ! MockRequestMessage(0, responseReceiver.ref) + responseReceiver.expectMessage(MockResponseMessage) + activationRef ! Activation(operationInterval.end) resultListener.expectMessageType[ParticipantResultEvent] match { @@ -176,6 +193,9 @@ class ParticipantAgentSpec extends ScalaTestWithActorTestKit with UnitSpec { // TICK 24 * 3600: GridAgent requests power + participantAgent ! MockRequestMessage(0, responseReceiver.ref) + responseReceiver.expectMessage(MockResponseMessage) + participantAgent ! RequestAssetPowerMessage(24 * 3600, Each(1), Each(0)) gridAgent.expectMessageType[AssetPowerChangedMessage] match { @@ -643,8 +663,6 @@ class ParticipantAgentSpec extends ScalaTestWithActorTestKit with UnitSpec { } - // todo testing handleRequest - } } From f65249c351413378e6bba7b3f67b23285f9ae147 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Tue, 26 Nov 2024 21:01:07 +0100 Subject: [PATCH 58/77] Fixing participant for primary data Signed-off-by: Sebastian Peter --- .../agent/participant2/ParticipantAgent.scala | 16 ++-------------- .../participant2/ParticipantModelShell.scala | 9 +++------ 2 files changed, 5 insertions(+), 20 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgent.scala b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgent.scala index 0dfa9167b3..116dece612 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgent.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgent.scala @@ -12,11 +12,7 @@ import edu.ie3.simona.agent.grid.GridAgentMessages.{ AssetPowerUnchangedMessage, } import edu.ie3.simona.agent.participant.data.Data -import edu.ie3.simona.agent.participant.data.Data.{ - PrimaryData, - PrimaryDataMeta, - SecondaryData, -} +import edu.ie3.simona.agent.participant.data.Data.{PrimaryData, PrimaryDataMeta} import edu.ie3.simona.event.ResultEvent import edu.ie3.simona.event.ResultEvent.ParticipantResultEvent import edu.ie3.simona.exceptions.CriticalFailureException @@ -305,18 +301,10 @@ object ParticipantAgent { ) ) - val receivedData = inputHandler.getData.map { - case data: SecondaryData => data - case other => - throw new CriticalFailureException( - s"Received unexpected data $other, should be secondary data" - ) - } - val (updatedShell, updatedGridAdapter) = Scope(modelShell) .map( _.updateRelevantData( - receivedData, + inputHandler.getData, gridAdapter.nodalVoltage, activation.tick, ) diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala index ff5fbc451a..16091cf53a 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala @@ -8,12 +8,9 @@ package edu.ie3.simona.model.participant2 import edu.ie3.datamodel.models.input.system.SystemParticipantInput import edu.ie3.datamodel.models.result.system.SystemParticipantResult +import edu.ie3.simona.agent.participant.data.Data import edu.ie3.simona.agent.participant.data.Data.PrimaryData.ComplexPower -import edu.ie3.simona.agent.participant.data.Data.{ - PrimaryData, - PrimaryDataMeta, - SecondaryData, -} +import edu.ie3.simona.agent.participant.data.Data.{PrimaryData, PrimaryDataMeta} import edu.ie3.simona.agent.participant2.ParticipantAgent import edu.ie3.simona.agent.participant2.ParticipantAgent.ParticipantRequest import edu.ie3.simona.config.SimonaConfig.BaseRuntimeConfig @@ -68,7 +65,7 @@ final case class ParticipantModelShell[ ) { def updateRelevantData( - receivedData: Seq[SecondaryData], + receivedData: Seq[Data], nodalVoltage: Dimensionless, tick: Long, ): ParticipantModelShell[OP, S, OR] = { From 885ce45beca473a44fcb67c3125436917418e1e7 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Tue, 26 Nov 2024 21:08:55 +0100 Subject: [PATCH 59/77] Preparations for further participant tests Signed-off-by: Sebastian Peter --- .../participant2/MockParticipantModel.scala | 30 +++++++++++++------ .../participant2/ParticipantAgentSpec.scala | 26 ++++++++++++++-- 2 files changed, 44 insertions(+), 12 deletions(-) diff --git a/src/test/scala/edu/ie3/simona/agent/participant2/MockParticipantModel.scala b/src/test/scala/edu/ie3/simona/agent/participant2/MockParticipantModel.scala index ef164f055a..b31dd834fd 100644 --- a/src/test/scala/edu/ie3/simona/agent/participant2/MockParticipantModel.scala +++ b/src/test/scala/edu/ie3/simona/agent/participant2/MockParticipantModel.scala @@ -8,11 +8,13 @@ package edu.ie3.simona.agent.participant2 import edu.ie3.datamodel.models.result.system.SystemParticipantResult import edu.ie3.simona.agent.participant.data.Data -import edu.ie3.simona.agent.participant.data.Data.PrimaryData +import edu.ie3.simona.agent.participant.data.Data.{PrimaryData, SecondaryData} import edu.ie3.simona.agent.participant2.MockParticipantModel.{ + MockRelevantData, MockRequestMessage, MockResponseMessage, MockResult, + MockSecondaryData, } import edu.ie3.simona.agent.participant2.ParticipantAgent.ParticipantRequest import edu.ie3.simona.model.participant.control.QControl @@ -20,9 +22,9 @@ import edu.ie3.simona.model.participant.control.QControl.CosPhiFixed import edu.ie3.simona.model.participant2.ParticipantModel import edu.ie3.simona.model.participant2.ParticipantModel.{ ActivePowerOperatingPoint, - FixedRelevantData, FixedState, ModelChangeIndicator, + OperationRelevantData, ParticipantFixedState, } import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage @@ -49,16 +51,16 @@ class MockParticipantModel( ) extends ParticipantModel[ ActivePowerOperatingPoint, FixedState, - FixedRelevantData.type, + MockRelevantData, ] with ParticipantFixedState[ ActivePowerOperatingPoint, - FixedRelevantData.type, + MockRelevantData, ] { override def determineOperatingPoint( state: FixedState, - relevantData: FixedRelevantData.type, + relevantData: MockRelevantData, ): (ActivePowerOperatingPoint, Option[Long]) = { ( ActivePowerOperatingPoint(Kilowatts(6)), @@ -91,24 +93,29 @@ class MockParticipantModel( ): SystemParticipantResult = throw new NotImplementedError() // Not tested override def getRequiredSecondaryServices: Iterable[ServiceType] = - Iterable.empty + throw new NotImplementedError() // Not tested override def createRelevantData( receivedData: Seq[Data], nodalVoltage: Dimensionless, tick: Long, simulationTime: ZonedDateTime, - ): FixedRelevantData.type = FixedRelevantData + ): MockRelevantData = + MockRelevantData( + receivedData.collectFirst { case data: MockSecondaryData => + data + } + ) override def calcFlexOptions( state: FixedState, - relevantData: FixedRelevantData.type, + relevantData: MockRelevantData, ): FlexibilityMessage.ProvideFlexOptions = ProvideMinMaxFlexOptions(uuid, Kilowatts(1), Kilowatts(-1), Kilowatts(3)) override def handlePowerControl( state: FixedState, - relevantData: FixedRelevantData.type, + relevantData: MockRelevantData, flexOptions: FlexibilityMessage.ProvideFlexOptions, setPower: Power, ): (ActivePowerOperatingPoint, ModelChangeIndicator) = @@ -147,4 +154,9 @@ object MockParticipantModel { case object MockResponseMessage + final case class MockSecondaryData(payload: String) extends SecondaryData + + final case class MockRelevantData(data: Option[MockSecondaryData]) + extends OperationRelevantData + } diff --git a/src/test/scala/edu/ie3/simona/agent/participant2/ParticipantAgentSpec.scala b/src/test/scala/edu/ie3/simona/agent/participant2/ParticipantAgentSpec.scala index 564084a08d..3630ae401b 100644 --- a/src/test/scala/edu/ie3/simona/agent/participant2/ParticipantAgentSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/participant2/ParticipantAgentSpec.scala @@ -58,9 +58,9 @@ class ParticipantAgentSpec extends ScalaTestWithActorTestKit with UnitSpec { private implicit val activePowerTolerance: Power = Kilowatts(1e-10) private implicit val reactivePowerTolerance: ReactivePower = Kilovars(1e-10) - "A ParticipantAgent without secondary services" when { + "A ParticipantAgent that is not controlled by EM" when { - "not flex-controlled" should { + "not depending on external services" should { "calculate operating point and results correctly with no additional ticks" in { @@ -340,7 +340,19 @@ class ParticipantAgentSpec extends ScalaTestWithActorTestKit with UnitSpec { } - "flex-controlled" should { + "depending on secondary data" should { + // todo + } + + "depending on primary data" should { + // todo + } + + } + + "A ParticipantAgent that is controlled by EM" when { + + "not depending on external services" should { "calculate operating point and results correctly with no additional ticks" in { @@ -663,6 +675,14 @@ class ParticipantAgentSpec extends ScalaTestWithActorTestKit with UnitSpec { } + "depending on secondary data" should { + // todo + } + + "depending on primary data" should { + // todo + } + } } From 34e18344f189f4e2a656b067bd8b40506636683d Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Thu, 28 Nov 2024 12:06:28 +0100 Subject: [PATCH 60/77] Fixing next tick with secondary data, adding test Signed-off-by: Sebastian Peter --- .../agent/participant2/ParticipantAgent.scala | 51 +- .../ParticipantInputHandler.scala | 11 +- .../model/participant2/ParticipantModel.scala | 1 + .../participant2/ParticipantModelShell.scala | 42 +- .../participant2/MockParticipantModel.scala | 53 +- .../participant2/ParticipantAgentSpec.scala | 474 +++++++++++++++++- 6 files changed, 575 insertions(+), 57 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgent.scala b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgent.scala index 116dece612..40f90c6ff5 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgent.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgent.scala @@ -19,7 +19,6 @@ import edu.ie3.simona.exceptions.CriticalFailureException import edu.ie3.simona.model.participant2.ParticipantModelShell import edu.ie3.simona.ontology.messages.SchedulerMessage.Completion import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage._ -import edu.ie3.simona.ontology.messages.services.ServiceMessage.ProvisionMessage import edu.ie3.simona.ontology.messages.{Activation, SchedulerMessage} import edu.ie3.util.scala.Scope import org.apache.pekko.actor.typed.scaladsl.Behaviors @@ -89,6 +88,24 @@ object ParticipantAgent { override val serviceRef: ClassicRef ) extends RegistrationResponseMessage + /** @param tick + * @param serviceRef + * @param data + * @param nextDataTick + * Next tick at which data could arrive. If None, no data is expected for + * the rest of the simulation + * + * @tparam D + * + * TODO this should suffice as secondary data provision message + */ + final case class ProvideData[D <: Data]( + tick: Long, + serviceRef: ClassicRef, + data: D, + nextDataTick: Option[Long], + ) extends Request + /** Request the power values for the requested tick from an AssetAgent and * provide the latest nodal voltage * @@ -192,7 +209,7 @@ object ParticipantAgent { parentData, ) - case (_, msg: ProvisionMessage[Data]) => + case (_, msg: ProvideData[Data]) => val inputHandlerWithData = inputHandler.handleDataProvision(msg) val (updatedShell, updatedInputHandler, updatedGridAdapter) = @@ -312,10 +329,10 @@ object ParticipantAgent { .map { shell => activation match { case ParticipantActivation(tick) => - val modelWithOP = shell.updateOperatingPoint(tick) + val shellWithOP = shell.updateOperatingPoint(tick) val results = - modelWithOP.determineResults(tick, gridAdapter.nodalVoltage) + shellWithOP.determineResults(tick, gridAdapter.nodalVoltage) results.modelResults.foreach { res => listener.foreach(_ ! ParticipantResultEvent(res)) @@ -324,18 +341,23 @@ object ParticipantAgent { val gridAdapterWithResult = gridAdapter.storePowerValue(results.totalPower, tick) + val changeIndicator = shellWithOP.getChangeIndicator( + tick, + inputHandler.getNextActivationTick, + ) + parentData.fold( schedulerData => schedulerData.scheduler ! Completion( schedulerData.activationAdapter, - modelWithOP.modelChange.changesAtTick, + changeIndicator.changesAtTick, ), _ => throw new CriticalFailureException( "Received activation while controlled by EM" ), ) - (modelWithOP, gridAdapterWithResult) + (shellWithOP, gridAdapterWithResult) case Flex(FlexActivation(tick)) => val modelWithFlex = shell.updateFlexOptions(tick) @@ -355,10 +377,10 @@ object ParticipantAgent { (modelWithFlex, gridAdapter) case Flex(flexControl: IssueFlexControl) => - val modelWithOP = shell.updateOperatingPoint(flexControl) + val shellWithOP = shell.updateOperatingPoint(flexControl) val results = - modelWithOP.determineResults( + shellWithOP.determineResults( flexControl.tick, gridAdapter.nodalVoltage, ) @@ -373,19 +395,24 @@ object ParticipantAgent { flexControl.tick, ) + val changeIndicator = shellWithOP.getChangeIndicator( + flexControl.tick, + inputHandler.getNextActivationTick, + ) + parentData.fold( _ => throw new CriticalFailureException( "Received issue flex control while not controlled by EM" ), _.emAgent ! FlexCompletion( - modelWithOP.model.uuid, - modelWithOP.modelChange.changesAtNextActivation, - modelWithOP.modelChange.changesAtTick, + shellWithOP.model.uuid, + changeIndicator.changesAtNextActivation, + changeIndicator.changesAtTick, ), ) - (modelWithOP, gridAdapterWithResult) + (shellWithOP, gridAdapterWithResult) } } .get diff --git a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantInputHandler.scala b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantInputHandler.scala index a79b5795c7..8052d3e290 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantInputHandler.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantInputHandler.scala @@ -8,8 +8,10 @@ package edu.ie3.simona.agent.participant2 import org.apache.pekko.actor.{ActorRef => ClassicRef} import edu.ie3.simona.agent.participant.data.Data -import edu.ie3.simona.agent.participant2.ParticipantAgent.ActivationRequest -import edu.ie3.simona.ontology.messages.services.ServiceMessage.ProvisionMessage +import edu.ie3.simona.agent.participant2.ParticipantAgent.{ + ActivationRequest, + ProvideData, +} final case class ParticipantInputHandler( expectedData: Map[ClassicRef, Long], @@ -33,7 +35,7 @@ final case class ParticipantInputHandler( } def handleDataProvision( - msg: ProvisionMessage[_ <: Data] + msg: ProvideData[_ <: Data] ): ParticipantInputHandler = { val updatedReceivedData = receivedData + (msg.serviceRef -> Some(msg.data)) val updatedExpectedData = msg.nextDataTick @@ -53,6 +55,9 @@ final case class ParticipantInputHandler( } } + def getNextActivationTick: Option[Long] = + expectedData.values.minOption + def getData: Seq[Data] = receivedData.values.flatten.toSeq diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala index 4582512366..5b416751e1 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala @@ -237,6 +237,7 @@ object ParticipantModel { * @param changesAtNextActivation * @param changesAtTick */ + // todo rename to OperationChangeIndicator final case class ModelChangeIndicator( changesAtNextActivation: Boolean = false, changesAtTick: Option[Long] = None, diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala index 16091cf53a..23f5d52f9b 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala @@ -61,7 +61,7 @@ final case class ParticipantModelShell[ flexOptions: Option[ProvideFlexOptions] = None, lastOperatingPoint: Option[OP] = None, operatingPoint: Option[OP] = None, - modelChange: ModelChangeIndicator = ModelChangeIndicator(), + private val modelChange: ModelChangeIndicator = ModelChangeIndicator(), ) { def updateRelevantData( @@ -103,7 +103,7 @@ final case class ParticipantModelShell[ } val (newOperatingPoint, newChangeIndicator) = - determineOperatingPointInInterval(modelOperatingPoint, currentTick) + determineOperatingPoint(modelOperatingPoint, currentTick) copy( state = Some(currentState), @@ -200,7 +200,7 @@ final case class ParticipantModelShell[ } val (newOperatingPoint, newChangeIndicator) = - determineOperatingPointInInterval(modelOperatingPoint, currentTick) + determineOperatingPoint(modelOperatingPoint, currentTick) copy( state = Some(currentState), @@ -210,32 +210,46 @@ final case class ParticipantModelShell[ ) } - private def determineOperatingPointInInterval( + private def determineOperatingPoint( modelOperatingPoint: () => (OP, ModelChangeIndicator), currentTick: Long, ): (OP, ModelChangeIndicator) = { if (operationInterval.includes(currentTick)) { - val (modelOp, modelIndicator) = modelOperatingPoint() + modelOperatingPoint() + } else { + // Current tick is outside of operation interval. + // Set operating point to "zero" + (model.zeroPowerOperatingPoint, ModelChangeIndicator()) + } + } - // Check if the end of the operation interval is *before* the next tick calculated by the model + /** Determines and returns the next activation tick considering the operating + * interval and given next data tick. + */ + def getChangeIndicator( + currentTick: Long, + nextDataTick: Option[Long], + ): ModelChangeIndicator = { + if (operationInterval.includes(currentTick)) { + // The next activation tick should be the earliest of + // the next tick request by the model, the next data tick and + // the end of the operation interval val adaptedNextTick = Seq( - modelIndicator.changesAtTick, + modelChange.changesAtTick, + nextDataTick, Option(operationInterval.end), ).flatten.minOption - (modelOp, modelIndicator.copy(changesAtTick = adaptedNextTick)) + modelChange.copy(changesAtTick = adaptedNextTick) } else { - // Current tick is outside of operation interval. - // Set operating point to "zero" - val op = model.zeroPowerOperatingPoint - - // If the model is not active *yet*, schedule the operation start + // If the model is not active, all activation ticks are ignored besides + // potentially the operation start val nextTick = Option.when(operationInterval.start > currentTick)( operationInterval.start ) - (op, ModelChangeIndicator(changesAtTick = nextTick)) + ModelChangeIndicator(changesAtTick = nextTick) } } diff --git a/src/test/scala/edu/ie3/simona/agent/participant2/MockParticipantModel.scala b/src/test/scala/edu/ie3/simona/agent/participant2/MockParticipantModel.scala index b31dd834fd..d716eeb664 100644 --- a/src/test/scala/edu/ie3/simona/agent/participant2/MockParticipantModel.scala +++ b/src/test/scala/edu/ie3/simona/agent/participant2/MockParticipantModel.scala @@ -9,38 +9,27 @@ package edu.ie3.simona.agent.participant2 import edu.ie3.datamodel.models.result.system.SystemParticipantResult import edu.ie3.simona.agent.participant.data.Data import edu.ie3.simona.agent.participant.data.Data.{PrimaryData, SecondaryData} -import edu.ie3.simona.agent.participant2.MockParticipantModel.{ - MockRelevantData, - MockRequestMessage, - MockResponseMessage, - MockResult, - MockSecondaryData, -} +import edu.ie3.simona.agent.participant2.MockParticipantModel._ import edu.ie3.simona.agent.participant2.ParticipantAgent.ParticipantRequest import edu.ie3.simona.model.participant.control.QControl import edu.ie3.simona.model.participant.control.QControl.CosPhiFixed import edu.ie3.simona.model.participant2.ParticipantModel -import edu.ie3.simona.model.participant2.ParticipantModel.{ - ActivePowerOperatingPoint, - FixedState, - ModelChangeIndicator, - OperationRelevantData, - ParticipantFixedState, -} +import edu.ie3.simona.model.participant2.ParticipantModel._ import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage import edu.ie3.simona.ontology.messages.flex.MinMaxFlexibilityMessage.ProvideMinMaxFlexOptions import edu.ie3.simona.service.ServiceType +import edu.ie3.util.quantities.QuantityUtils.RichQuantityDouble +import edu.ie3.util.scala.quantities.DefaultQuantities._ import edu.ie3.util.scala.quantities.{ApparentPower, Kilovoltamperes} +import org.apache.pekko.actor.typed.ActorRef +import org.apache.pekko.actor.typed.scaladsl.ActorContext import squants.Dimensionless import squants.energy.{Kilowatts, Power} import tech.units.indriya.ComparableQuantity -import edu.ie3.util.quantities.QuantityUtils.RichQuantityDouble -import org.apache.pekko.actor.typed.ActorRef -import org.apache.pekko.actor.typed.scaladsl.ActorContext -import javax.measure.quantity.{Power => QuantPower} import java.time.ZonedDateTime import java.util.UUID +import javax.measure.quantity.{Power => QuantPower} class MockParticipantModel( override val uuid: UUID = UUID.fromString("0-0-0-0-1"), @@ -63,7 +52,9 @@ class MockParticipantModel( relevantData: MockRelevantData, ): (ActivePowerOperatingPoint, Option[Long]) = { ( - ActivePowerOperatingPoint(Kilowatts(6)), + ActivePowerOperatingPoint( + Kilowatts(6) + relevantData.additionalP.getOrElse(zeroKW) + ), mockActivationTicks.get(state.tick), ) } @@ -103,15 +94,22 @@ class MockParticipantModel( ): MockRelevantData = MockRelevantData( receivedData.collectFirst { case data: MockSecondaryData => - data + data.additionalP } ) override def calcFlexOptions( state: FixedState, relevantData: MockRelevantData, - ): FlexibilityMessage.ProvideFlexOptions = - ProvideMinMaxFlexOptions(uuid, Kilowatts(1), Kilowatts(-1), Kilowatts(3)) + ): FlexibilityMessage.ProvideFlexOptions = { + val additionalP = relevantData.additionalP.getOrElse(zeroKW) + ProvideMinMaxFlexOptions( + uuid, + Kilowatts(1) + additionalP, + Kilowatts(-1) + additionalP, + Kilowatts(3) + additionalP, + ) + } override def handlePowerControl( state: FixedState, @@ -154,9 +152,16 @@ object MockParticipantModel { case object MockResponseMessage - final case class MockSecondaryData(payload: String) extends SecondaryData + final case class MockSecondaryData(additionalP: Power) extends SecondaryData - final case class MockRelevantData(data: Option[MockSecondaryData]) + /** Simple [[OperationRelevantData]] to test its usage in operation point + * calculations + * + * @param additionalP + * Power value that is added to the power or flex options power for testing + * purposes + */ + final case class MockRelevantData(additionalP: Option[Power]) extends OperationRelevantData } diff --git a/src/test/scala/edu/ie3/simona/agent/participant2/ParticipantAgentSpec.scala b/src/test/scala/edu/ie3/simona/agent/participant2/ParticipantAgentSpec.scala index 3630ae401b..f1314d23fe 100644 --- a/src/test/scala/edu/ie3/simona/agent/participant2/ParticipantAgentSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/participant2/ParticipantAgentSpec.scala @@ -15,9 +15,11 @@ import edu.ie3.simona.agent.participant2.MockParticipantModel.{ MockRequestMessage, MockResponseMessage, MockResult, + MockSecondaryData, } import edu.ie3.simona.agent.participant2.ParticipantAgent.{ FinishParticipantSimulation, + ProvideData, RequestAssetPowerMessage, } import edu.ie3.simona.event.ResultEvent @@ -44,6 +46,7 @@ import org.apache.pekko.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit import org.apache.pekko.actor.typed.ActorRef import squants.{Each, Power} import squants.energy.Kilowatts +import org.apache.pekko.actor.typed.scaladsl.adapter._ import java.time.ZonedDateTime @@ -280,7 +283,7 @@ class ParticipantAgentSpec extends ScalaTestWithActorTestKit with UnitSpec { Completion(activationRef, Some(12 * 3600)) ) - // TICK 12 * 3600: Middle of operation interval and GridAgent requests power + // TICK 12 * 3600: Inside of operation interval and GridAgent requests power activationRef ! Activation(12 * 3600) @@ -341,7 +344,208 @@ class ParticipantAgentSpec extends ScalaTestWithActorTestKit with UnitSpec { } "depending on secondary data" should { - // todo + + "calculate operating point and results correctly with additional ticks" in { + + val scheduler = createTestProbe[SchedulerMessage]() + val gridAgent = createTestProbe[GridAgent.Request]() + val resultListener = createTestProbe[ResultEvent]() + val service = createTestProbe() + + // receiving the activation adapter + val receiveAdapter = createTestProbe[ActorRef[Activation]]() + + // no additional activation ticks + val model = new MockParticipantModel(mockActivationTicks = + Map( + 0 * 3600L -> 4 * 3600L, // still before operation, is ignored + 8 * 3600L -> 12 * 3600L, // middle of operation + 12 * 3600L -> 22 * 3600L, // after operation, is ignored + ) + ) + val operationInterval = OperationInterval(8 * 3600, 20 * 3600) + + val participantAgent = spawn( + ParticipantAgentMockFactory.create( + ParticipantModelShell( + model, + operationInterval, + simulationStartDate, + ), + ParticipantInputHandler( + Map(service.ref.toClassic -> 0) + ), + ParticipantGridAdapter( + gridAgent.ref, + 12 * 3600, + ), + Iterable(resultListener.ref), + Left(scheduler.ref, receiveAdapter.ref), + ) + ) + val activationRef = + receiveAdapter.expectMessageType[ActorRef[Activation]] + + // TICK 0: Outside of operation interval + + activationRef ! Activation(0) + + // nothing should happen, still waiting for secondary data... + resultListener.expectNoMessage() + scheduler.expectNoMessage() + + participantAgent ! ProvideData( + 0, + service.ref.toClassic, + MockSecondaryData(Kilowatts(1)), + Some(6 * 3600), + ) + + // outside of operation interval, 0 MW + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: MockResult) => + result.getInputModel shouldBe model.uuid + result.getTime shouldBe simulationStartDate + result.getP should equalWithTolerance(0.0.asMegaWatt) + result.getQ should equalWithTolerance(0.0.asMegaVar) + } + + // next model tick and next data tick are ignored, + // because we are outside of operation interval + scheduler.expectMessage( + Completion(activationRef, Some(operationInterval.start)) + ) + + // TICK 6 * 3600: Outside of operation interval, only data expected, no activation + + participantAgent ! ProvideData( + 6 * 3600, + service.ref.toClassic, + MockSecondaryData(Kilowatts(3)), + Some(12 * 3600), + ) + + resultListener.expectNoMessage() + scheduler.expectNoMessage() + + // TICK 8 * 3600: Start of operation interval + + activationRef ! Activation(operationInterval.start) + + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: MockResult) => + result.getInputModel shouldBe model.uuid + result.getTime shouldBe operationInterval.start.toDateTime + result.getP should equalWithTolerance(0.009.asMegaWatt) + result.getQ should equalWithTolerance(0.00435889893.asMegaVar) + } + + // next model tick and next data tick are both hour 12 + scheduler.expectMessage( + Completion(activationRef, Some(12 * 3600)) + ) + + // TICK 12 * 3600: Inside of operation interval, GridAgent requests power + + activationRef ! Activation(12 * 3600) + + participantAgent ! RequestAssetPowerMessage(12 * 3600, Each(1), Each(0)) + + // 8 hours of 0 kW, 4 hours of 6+3=9 kW + gridAgent.expectMessageType[AssetPowerChangedMessage] match { + case AssetPowerChangedMessage(p, q) => + p should approximate(Kilowatts(3)) + q should approximate(Kilovars(1.452966314514)) + } + + participantAgent ! FinishParticipantSimulation(12 * 3600, 24 * 3600) + + // nothing should happen, still waiting for secondary data... + resultListener.expectNoMessage() + scheduler.expectNoMessage() + + participantAgent ! ProvideData( + 12 * 3600, + service.ref.toClassic, + MockSecondaryData(Kilowatts(6)), + Some(18 * 3600), + ) + + // calculation should start now + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: MockResult) => + result.getInputModel shouldBe model.uuid + result.getTime shouldBe (12 * 3600).toDateTime + result.getP should equalWithTolerance(0.012.asMegaWatt) + result.getQ should equalWithTolerance(0.005811865258.asMegaVar) + } + + // new data is expected at 18 hours + scheduler.expectMessage( + Completion(activationRef, Some(18 * 3600)) + ) + + // TICK 18 * 3600: Inside of operation interval because of expected secondary data + + activationRef ! Activation(18 * 3600) + + // nothing should happen, still waiting for secondary data... + resultListener.expectNoMessage() + scheduler.expectNoMessage() + + participantAgent ! ProvideData( + 18 * 3600, + service.ref.toClassic, + MockSecondaryData(Kilowatts(9)), + Some(24 * 3600), + ) + + // calculation should start now + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: MockResult) => + result.getInputModel shouldBe model.uuid + result.getTime shouldBe (18 * 3600).toDateTime + result.getP should equalWithTolerance(0.015.asMegaWatt) + result.getQ should equalWithTolerance(0.00726483157257.asMegaVar) + } + + scheduler.expectMessage( + Completion(activationRef, Some(operationInterval.end)) + ) + + // TICK 20 * 3600: Outside of operation interval (last tick) + + activationRef ! Activation(operationInterval.end) + + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: MockResult) => + result.getInputModel shouldBe model.uuid + result.getTime shouldBe operationInterval.end.toDateTime + result.getP should equalWithTolerance(0.0.asMegaWatt) + result.getQ should equalWithTolerance(0.0.asMegaVar) + } + + // Since we left the operation interval, there are no more ticks to activate + scheduler.expectMessage(Completion(activationRef)) + + // TICK 24 * 3600: GridAgent requests power + + participantAgent ! RequestAssetPowerMessage(24 * 3600, Each(1), Each(0)) + + // 6 hours of 6+6=12 kW, 2 hours of 6+9=15 kW, 4 hours of 0 kW + gridAgent.expectMessageType[AssetPowerChangedMessage] match { + case AssetPowerChangedMessage(p, q) => + p should approximate(Kilowatts(8.5)) + q should approximate(Kilovars(4.116737891123)) + } + + participantAgent ! FinishParticipantSimulation(24 * 3600, 36 * 3600) + + resultListener.expectNoMessage() + scheduler.expectNoMessage() + + } + } "depending on primary data" should { @@ -556,6 +760,7 @@ class ParticipantAgentSpec extends ScalaTestWithActorTestKit with UnitSpec { result.getQ should equalWithTolerance(0.0.asMegaVar) } + // next model tick is ignored because we are outside of operation interval em.expectMessage( FlexCompletion( model.uuid, @@ -592,7 +797,7 @@ class ParticipantAgentSpec extends ScalaTestWithActorTestKit with UnitSpec { ) ) - // TICK 12 * 3600: Middle of operation interval and GridAgent requests power + // TICK 12 * 3600: Inside of operation interval and GridAgent requests power flexRef ! FlexActivation(12 * 3600) @@ -676,7 +881,268 @@ class ParticipantAgentSpec extends ScalaTestWithActorTestKit with UnitSpec { } "depending on secondary data" should { - // todo + + "calculate operating point and results correctly with additional ticks" in { + // todo test changesAtNextActivation as well + + val em = createTestProbe[FlexResponse]() + val gridAgent = createTestProbe[GridAgent.Request]() + val resultListener = createTestProbe[ResultEvent]() + val service = createTestProbe() + + // receiving the activation adapter + val receiveAdapter = createTestProbe[ActorRef[FlexRequest]]() + + // no additional activation ticks + val model = new MockParticipantModel(mockActivationTicks = + Map( + 0 * 3600L -> 4 * 3600L, // still before operation, is ignored + 8 * 3600L -> 12 * 3600L, // middle of operation + 12 * 3600L -> 22 * 3600L, // after operation, is ignored + ) + ) + val operationInterval = OperationInterval(8 * 3600, 20 * 3600) + + val participantAgent = spawn( + ParticipantAgentMockFactory.create( + ParticipantModelShell( + model, + operationInterval, + simulationStartDate, + ), + ParticipantInputHandler( + Map(service.ref.toClassic -> 0) + ), + ParticipantGridAdapter( + gridAgent.ref, + 12 * 3600, + ), + Iterable(resultListener.ref), + Right(em.ref, receiveAdapter.ref), + ) + ) + val flexRef = receiveAdapter.expectMessageType[ActorRef[FlexRequest]] + + // TICK 0: Outside of operation interval + + flexRef ! FlexActivation(0) + + // nothing should happen, still waiting for secondary data... + resultListener.expectNoMessage() + em.expectNoMessage() + + participantAgent ! ProvideData( + 0, + service.ref.toClassic, + MockSecondaryData(Kilowatts(1)), + Some(6 * 3600), + ) + + em.expectMessageType[ProvideMinMaxFlexOptions] match { + case ProvideMinMaxFlexOptions(modelUuid, ref, min, max) => + modelUuid shouldBe model.uuid + ref should approximate(Kilowatts(0)) + min should approximate(Kilowatts(0)) + max should approximate(Kilowatts(0)) + } + + flexRef ! IssueNoControl(0) + + // outside of operation interval, 0 MW + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: MockResult) => + result.getInputModel shouldBe model.uuid + result.getTime shouldBe simulationStartDate + result.getP should equalWithTolerance(0.0.asMegaWatt) + result.getQ should equalWithTolerance(0.0.asMegaVar) + } + + // next model tick and next data tick are ignored, + // because we are outside of operation interval + em.expectMessage( + FlexCompletion( + model.uuid, + requestAtTick = Some(operationInterval.start), + ) + ) + + // TICK 6 * 3600: Outside of operation interval, only data expected, no activation + + participantAgent ! ProvideData( + 6 * 3600, + service.ref.toClassic, + MockSecondaryData(Kilowatts(1)), + Some(12 * 3600), + ) + + resultListener.expectNoMessage() + em.expectNoMessage() + + // TICK 8 * 3600: Start of operation interval + + flexRef ! FlexActivation(operationInterval.start) + + em.expectMessageType[ProvideMinMaxFlexOptions] match { + case ProvideMinMaxFlexOptions(modelUuid, ref, min, max) => + modelUuid shouldBe model.uuid + ref should approximate(Kilowatts(2)) + min should approximate(Kilowatts(0)) + max should approximate(Kilowatts(4)) + } + + flexRef ! IssuePowerControl(operationInterval.start, Kilowatts(3)) + + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: MockResult) => + result.getInputModel shouldBe model.uuid + result.getTime shouldBe operationInterval.start.toDateTime + result.getP should equalWithTolerance(0.003.asMegaWatt) + result.getQ should equalWithTolerance(0.0014529663.asMegaVar) + } + + // next model tick and next data tick are both hour 12 + em.expectMessage( + FlexCompletion( + model.uuid, + requestAtTick = Some(12 * 3600), + ) + ) + + // TICK 12 * 3600: Inside of operation interval, GridAgent requests power + + flexRef ! FlexActivation(12 * 3600) + + participantAgent ! RequestAssetPowerMessage(12 * 3600, Each(1), Each(0)) + + gridAgent.expectMessageType[AssetPowerChangedMessage] match { + case AssetPowerChangedMessage(p, q) => + p should approximate(Kilowatts(1)) + q should approximate(Kilovars(0.48432210483)) + } + + participantAgent ! FinishParticipantSimulation(12 * 3600, 24 * 3600) + + // nothing should happen, still waiting for secondary data... + resultListener.expectNoMessage() + em.expectNoMessage() + + participantAgent ! ProvideData( + 12 * 3600, + service.ref.toClassic, + MockSecondaryData(Kilowatts(2)), + Some(18 * 3600), + ) + + // calculation should start now + em.expectMessageType[ProvideMinMaxFlexOptions] match { + case ProvideMinMaxFlexOptions(modelUuid, ref, min, max) => + modelUuid shouldBe model.uuid + ref should approximate(Kilowatts(3)) + min should approximate(Kilowatts(1)) + max should approximate(Kilowatts(5)) + } + + flexRef ! IssueNoControl(12 * 3600) + + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: MockResult) => + result.getInputModel shouldBe model.uuid + result.getTime shouldBe (12 * 3600).toDateTime + result.getP should equalWithTolerance(0.003.asMegaWatt) + result.getQ should equalWithTolerance(0.001452966315.asMegaVar) + } + + em.expectMessage( + FlexCompletion( + model.uuid, + requestAtTick = Some(18 * 3600), + ) + ) + + // TICK 18 * 3600: Inside of operation interval because of expected secondary data + + flexRef ! FlexActivation(18 * 3600) + + // nothing should happen, still waiting for secondary data... + resultListener.expectNoMessage() + em.expectNoMessage() + + participantAgent ! ProvideData( + 18 * 3600, + service.ref.toClassic, + MockSecondaryData(Kilowatts(5)), + Some(24 * 3600), + ) + + // calculation should start now + em.expectMessageType[ProvideMinMaxFlexOptions] match { + case ProvideMinMaxFlexOptions(modelUuid, ref, min, max) => + modelUuid shouldBe model.uuid + ref should approximate(Kilowatts(6)) + min should approximate(Kilowatts(4)) + max should approximate(Kilowatts(8)) + } + + flexRef ! IssueNoControl(18 * 3600) + + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: MockResult) => + result.getInputModel shouldBe model.uuid + result.getTime shouldBe (18 * 3600).toDateTime + result.getP should equalWithTolerance(0.006.asMegaWatt) + result.getQ should equalWithTolerance(0.002905932629.asMegaVar) + } + + em.expectMessage( + FlexCompletion( + model.uuid, + requestAtTick = Some(operationInterval.end), + ) + ) + + // TICK 20 * 3600: Outside of operation interval (last tick) + + flexRef ! FlexActivation(operationInterval.end) + + em.expectMessageType[ProvideMinMaxFlexOptions] match { + case ProvideMinMaxFlexOptions(modelUuid, ref, min, max) => + modelUuid shouldBe model.uuid + ref should approximate(Kilowatts(0)) + min should approximate(Kilowatts(0)) + max should approximate(Kilowatts(0)) + } + + flexRef ! IssueNoControl(operationInterval.end) + + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: MockResult) => + result.getInputModel shouldBe model.uuid + result.getTime shouldBe operationInterval.end.toDateTime + result.getP should equalWithTolerance(0.0.asMegaWatt) + result.getQ should equalWithTolerance(0.0.asMegaVar) + } + + // Since we left the operation interval, there are no more ticks to activate + em.expectMessage(FlexCompletion(model.uuid)) + + // TICK 24 * 3600: GridAgent requests power + + participantAgent ! RequestAssetPowerMessage(24 * 3600, Each(1), Each(0)) + + // 6 hours of 3 kW, 2 hours of 6 kW, 4 hours of 0 kW + gridAgent.expectMessageType[AssetPowerChangedMessage] match { + case AssetPowerChangedMessage(p, q) => + p should approximate(Kilowatts(2.5)) + q should approximate(Kilovars(1.210805262)) + } + + participantAgent ! FinishParticipantSimulation(24 * 3600, 36 * 3600) + + resultListener.expectNoMessage() + em.expectNoMessage() + + } + } "depending on primary data" should { From d32a81b617a973aa9b47465b15e490832c5567b8 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Thu, 28 Nov 2024 12:12:11 +0100 Subject: [PATCH 61/77] Renaming ModelChangeIndicator Signed-off-by: Sebastian Peter --- .../participant2/ParticipantFlexibility.scala | 8 ++++---- .../model/participant2/ParticipantModel.scala | 15 ++++++++------- .../participant2/ParticipantModelShell.scala | 18 ++++++++++-------- .../PrimaryDataParticipantModel.scala | 6 +++--- .../model/participant2/StorageModel.scala | 7 +++++-- .../model/participant2/evcs/EvcsModel.scala | 14 +++++++------- .../participant2/MockParticipantModel.scala | 6 ++++-- .../participant2/evcs/EvcsModelSpec.scala | 7 +++++-- 8 files changed, 46 insertions(+), 35 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantFlexibility.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantFlexibility.scala index 2ee34d6456..5170e1e43d 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantFlexibility.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantFlexibility.scala @@ -8,7 +8,7 @@ package edu.ie3.simona.model.participant2 import edu.ie3.simona.model.participant2.ParticipantModel.{ ActivePowerOperatingPoint, - ModelChangeIndicator, + OperationChangeIndicator, ModelState, OperatingPoint, OperationRelevantData, @@ -33,7 +33,7 @@ trait ParticipantFlexibility[ relevantData: OR, flexOptions: ProvideFlexOptions, // TODO is this needed? setPower: Power, - ): (OP, ModelChangeIndicator) + ): (OP, OperationChangeIndicator) } @@ -60,8 +60,8 @@ object ParticipantFlexibility { relevantData: OR, flexOptions: ProvideFlexOptions, setPower: Power, - ): (ActivePowerOperatingPoint, ModelChangeIndicator) = { - (ActivePowerOperatingPoint(setPower), ModelChangeIndicator()) + ): (ActivePowerOperatingPoint, OperationChangeIndicator) = { + (ActivePowerOperatingPoint(setPower), OperationChangeIndicator()) } } diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala index 5b416751e1..01f4f7f171 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala @@ -237,23 +237,24 @@ object ParticipantModel { * @param changesAtNextActivation * @param changesAtTick */ - // todo rename to OperationChangeIndicator - final case class ModelChangeIndicator( + final case class OperationChangeIndicator( changesAtNextActivation: Boolean = false, changesAtTick: Option[Long] = None, ) { - /** Combines two ModelChangeIndicators by aggregating + /** Combines two [[OperationChangeIndicator]]s by aggregating * changesAtNextActivation via OR function and picking the earlier (or any) * of both changesAtTick values. * * @param otherIndicator - * The other ModelChangeIndicator to combine with this one + * The other [[OperationChangeIndicator]] to combine with this one * @return - * An aggregated ModelChangeIndicator + * An aggregated [[OperationChangeIndicator]] */ - def |(otherIndicator: ModelChangeIndicator): ModelChangeIndicator = { - ModelChangeIndicator( + def |( + otherIndicator: OperationChangeIndicator + ): OperationChangeIndicator = { + OperationChangeIndicator( changesAtNextActivation || otherIndicator.changesAtNextActivation, Seq(changesAtTick, otherIndicator.changesAtTick).flatten.minOption, ) diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala index 23f5d52f9b..8f7cbb925e 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala @@ -18,7 +18,7 @@ import edu.ie3.simona.exceptions.CriticalFailureException import edu.ie3.simona.model.SystemComponent import edu.ie3.simona.model.em.EmTools import edu.ie3.simona.model.participant2.ParticipantModel.{ - ModelChangeIndicator, + OperationChangeIndicator, ModelState, OperatingPoint, OperationRelevantData, @@ -61,7 +61,8 @@ final case class ParticipantModelShell[ flexOptions: Option[ProvideFlexOptions] = None, lastOperatingPoint: Option[OP] = None, operatingPoint: Option[OP] = None, - private val modelChange: ModelChangeIndicator = ModelChangeIndicator(), + private val modelChange: OperationChangeIndicator = + OperationChangeIndicator(), ) { def updateRelevantData( @@ -98,7 +99,8 @@ final case class ParticipantModelShell[ throw new CriticalFailureException("No relevant data available!") ), ) - val modelIndicator = ModelChangeIndicator(changesAtTick = modelNextTick) + val modelIndicator = + OperationChangeIndicator(changesAtTick = modelNextTick) (modelOp, modelIndicator) } @@ -211,15 +213,15 @@ final case class ParticipantModelShell[ } private def determineOperatingPoint( - modelOperatingPoint: () => (OP, ModelChangeIndicator), + modelOperatingPoint: () => (OP, OperationChangeIndicator), currentTick: Long, - ): (OP, ModelChangeIndicator) = { + ): (OP, OperationChangeIndicator) = { if (operationInterval.includes(currentTick)) { modelOperatingPoint() } else { // Current tick is outside of operation interval. // Set operating point to "zero" - (model.zeroPowerOperatingPoint, ModelChangeIndicator()) + (model.zeroPowerOperatingPoint, OperationChangeIndicator()) } } @@ -229,7 +231,7 @@ final case class ParticipantModelShell[ def getChangeIndicator( currentTick: Long, nextDataTick: Option[Long], - ): ModelChangeIndicator = { + ): OperationChangeIndicator = { if (operationInterval.includes(currentTick)) { // The next activation tick should be the earliest of // the next tick request by the model, the next data tick and @@ -249,7 +251,7 @@ final case class ParticipantModelShell[ operationInterval.start ) - ModelChangeIndicator(changesAtTick = nextTick) + OperationChangeIndicator(changesAtTick = nextTick) } } diff --git a/src/main/scala/edu/ie3/simona/model/participant2/PrimaryDataParticipantModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/PrimaryDataParticipantModel.scala index bacf4b6fe0..872b31c514 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/PrimaryDataParticipantModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/PrimaryDataParticipantModel.scala @@ -18,7 +18,7 @@ import edu.ie3.simona.exceptions.CriticalFailureException import edu.ie3.simona.model.participant.control.QControl import edu.ie3.simona.model.participant2.ParticipantModel.{ FixedState, - ModelChangeIndicator, + OperationChangeIndicator, OperatingPoint, OperationRelevantData, ParticipantFixedState, @@ -128,11 +128,11 @@ final case class PrimaryDataParticipantModel[P <: PrimaryData: ClassTag]( relevantData: PrimaryOperationRelevantData[P], flexOptions: FlexibilityMessage.ProvideFlexOptions, setPower: Power, - ): (PrimaryOperatingPoint[P], ModelChangeIndicator) = { + ): (PrimaryOperatingPoint[P], OperationChangeIndicator) = { val factor = relevantData.data.p / setPower val scaledData: P = primaryDataMeta.scale(relevantData.data, factor) - (PrimaryOperatingPoint(scaledData), ModelChangeIndicator()) + (PrimaryOperatingPoint(scaledData), OperationChangeIndicator()) } } diff --git a/src/main/scala/edu/ie3/simona/model/participant2/StorageModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/StorageModel.scala index 860806b41c..fd4c5f2fab 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/StorageModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/StorageModel.scala @@ -227,7 +227,7 @@ class StorageModel private ( relevantData: StorageRelevantData, flexOptions: FlexibilityMessage.ProvideFlexOptions, setPower: Power, - ): (ActivePowerOperatingPoint, ParticipantModel.ModelChangeIndicator) = { + ): (ActivePowerOperatingPoint, ParticipantModel.OperationChangeIndicator) = { val adaptedSetPower = if ( // if power is close to zero, set it to zero @@ -291,7 +291,10 @@ class StorageModel private ( ( ActivePowerOperatingPoint(adaptedSetPower), - ParticipantModel.ModelChangeIndicator(activateAtNextTick, maybeNextTick), + ParticipantModel.OperationChangeIndicator( + activateAtNextTick, + maybeNextTick, + ), ) } diff --git a/src/main/scala/edu/ie3/simona/model/participant2/evcs/EvcsModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/evcs/EvcsModel.scala index 93059b8630..8ee7b9db94 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/evcs/EvcsModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/evcs/EvcsModel.scala @@ -23,7 +23,7 @@ import edu.ie3.simona.exceptions.CriticalFailureException import edu.ie3.simona.model.participant.control.QControl import edu.ie3.simona.model.participant.evcs.EvModelWrapper import edu.ie3.simona.model.participant2.ParticipantModel.{ - ModelChangeIndicator, + OperationChangeIndicator, ModelState, OperatingPoint, OperationRelevantData, @@ -292,11 +292,11 @@ class EvcsModel private ( relevantData: EvcsRelevantData, flexOptions: FlexibilityMessage.ProvideFlexOptions, setPower: Power, - ): (EvcsOperatingPoint, ModelChangeIndicator) = { + ): (EvcsOperatingPoint, OperationChangeIndicator) = { if (setPower == zeroKW) return ( EvcsOperatingPoint(Map.empty), - ModelChangeIndicator(), + OperationChangeIndicator(), ) // applicable evs can be charged/discharged, other evs cannot @@ -330,7 +330,7 @@ class EvcsModel private ( val aggregatedChangeIndicator = combinedSchedules .map { case (_, (_, indicator)) => indicator } - .foldLeft(ModelChangeIndicator()) { + .foldLeft(OperationChangeIndicator()) { case (aggregateIndicator, otherIndicator) => aggregateIndicator | otherIndicator } @@ -360,7 +360,7 @@ class EvcsModel private ( evs: Seq[EvModelWrapper], setPower: Power, ): ( - Seq[(UUID, (Power, ModelChangeIndicator))], + Seq[(UUID, (Power, OperationChangeIndicator))], Power, ) = { @@ -392,7 +392,7 @@ class EvcsModel private ( ev.uuid, ( proposedPower, - ModelChangeIndicator( + OperationChangeIndicator( changesAtNextActivation = isFull(ev) || isEmpty(ev) || isInLowerMargin(ev), changesAtTick = Some(endTick), @@ -426,7 +426,7 @@ class EvcsModel private ( ev.uuid, ( power, - ModelChangeIndicator( + OperationChangeIndicator( changesAtNextActivation = isFull(ev) || isEmpty(ev) || isInLowerMargin(ev), changesAtTick = Some(endTick), diff --git a/src/test/scala/edu/ie3/simona/agent/participant2/MockParticipantModel.scala b/src/test/scala/edu/ie3/simona/agent/participant2/MockParticipantModel.scala index d716eeb664..580569cf19 100644 --- a/src/test/scala/edu/ie3/simona/agent/participant2/MockParticipantModel.scala +++ b/src/test/scala/edu/ie3/simona/agent/participant2/MockParticipantModel.scala @@ -116,10 +116,12 @@ class MockParticipantModel( relevantData: MockRelevantData, flexOptions: FlexibilityMessage.ProvideFlexOptions, setPower: Power, - ): (ActivePowerOperatingPoint, ModelChangeIndicator) = + ): (ActivePowerOperatingPoint, OperationChangeIndicator) = ( ActivePowerOperatingPoint(setPower), - ModelChangeIndicator(changesAtTick = mockActivationTicks.get(state.tick)), + OperationChangeIndicator(changesAtTick = + mockActivationTicks.get(state.tick) + ), ) override def handleRequest( diff --git a/src/test/scala/edu/ie3/simona/model/participant2/evcs/EvcsModelSpec.scala b/src/test/scala/edu/ie3/simona/model/participant2/evcs/EvcsModelSpec.scala index 9344aaa545..d27422a27f 100644 --- a/src/test/scala/edu/ie3/simona/model/participant2/evcs/EvcsModelSpec.scala +++ b/src/test/scala/edu/ie3/simona/model/participant2/evcs/EvcsModelSpec.scala @@ -12,7 +12,7 @@ import edu.ie3.simona.agent.participant2.ParticipantAgent import edu.ie3.simona.agent.participant2.ParticipantAgent.ParticipantRequest import edu.ie3.simona.config.SimonaConfig.EvcsRuntimeConfig import edu.ie3.simona.model.participant.evcs.EvModelWrapper -import edu.ie3.simona.model.participant2.ParticipantModel.ModelChangeIndicator +import edu.ie3.simona.model.participant2.ParticipantModel.OperationChangeIndicator import edu.ie3.simona.model.participant2.evcs.EvcsModel.{ EvcsOperatingPoint, EvcsRelevantData, @@ -791,7 +791,10 @@ class EvcsModelSpec ) match { case ( EvcsOperatingPoint(evOperatingPoints), - ModelChangeIndicator(actualNextActivation, actualNextTick), + OperationChangeIndicator( + actualNextActivation, + actualNextTick, + ), ) => evOperatingPoints .get(ev1.uuid) From cb49df5c89257e12db5049a018d7293cdf6a8409 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Thu, 28 Nov 2024 12:15:10 +0100 Subject: [PATCH 62/77] Adding ScalaDoc Signed-off-by: Sebastian Peter --- .../model/participant2/ParticipantModel.scala | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala index 01f4f7f171..cdd88c00ec 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModel.scala @@ -232,10 +232,20 @@ object ParticipantModel { } - /** Indicates when either flex options change (when em-controlled) or the - * operating point must change (when not em-controlled). + /** Indicates when either flex options (when em-controlled) or the operating + * point are going to change (when not em-controlled). + * + * A change of flex options or operating point might occur due to various + * reasons, including expected data arrival, internal expected model changes + * and operating interval limits. + * * @param changesAtNextActivation + * Indicates whether flex options change at the very next tick that EM is + * activated, due to e.g. storage limits being reached. Not applicable for + * not-em-controlled models. * @param changesAtTick + * The next tick at which a change of flex options or the operating point + * is expected. */ final case class OperationChangeIndicator( changesAtNextActivation: Boolean = false, From 7eaf563a0977b87f6713e7b9a9fb7aa4e963e082 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Thu, 28 Nov 2024 13:04:53 +0100 Subject: [PATCH 63/77] Testing changesAtNext Signed-off-by: Sebastian Peter --- .../participant2/MockParticipantModel.scala | 8 ++-- .../participant2/ParticipantAgentSpec.scala | 37 ++++++++++++------- 2 files changed, 29 insertions(+), 16 deletions(-) diff --git a/src/test/scala/edu/ie3/simona/agent/participant2/MockParticipantModel.scala b/src/test/scala/edu/ie3/simona/agent/participant2/MockParticipantModel.scala index 580569cf19..73353362dc 100644 --- a/src/test/scala/edu/ie3/simona/agent/participant2/MockParticipantModel.scala +++ b/src/test/scala/edu/ie3/simona/agent/participant2/MockParticipantModel.scala @@ -36,7 +36,8 @@ class MockParticipantModel( override val sRated: ApparentPower = Kilovoltamperes(10), override val cosPhiRated: Double = 0.9, override val qControl: QControl = CosPhiFixed(0.9), - mockActivationTicks: Map[Long, Long], + mockActivationTicks: Map[Long, Long] = Map.empty, + mockChangeAtNext: Set[Long] = Set.empty, ) extends ParticipantModel[ ActivePowerOperatingPoint, FixedState, @@ -119,8 +120,9 @@ class MockParticipantModel( ): (ActivePowerOperatingPoint, OperationChangeIndicator) = ( ActivePowerOperatingPoint(setPower), - OperationChangeIndicator(changesAtTick = - mockActivationTicks.get(state.tick) + OperationChangeIndicator( + changesAtNextActivation = mockChangeAtNext.contains(state.tick), + changesAtTick = mockActivationTicks.get(state.tick), ), ) diff --git a/src/test/scala/edu/ie3/simona/agent/participant2/ParticipantAgentSpec.scala b/src/test/scala/edu/ie3/simona/agent/participant2/ParticipantAgentSpec.scala index f1314d23fe..69ac314294 100644 --- a/src/test/scala/edu/ie3/simona/agent/participant2/ParticipantAgentSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/participant2/ParticipantAgentSpec.scala @@ -709,12 +709,17 @@ class ParticipantAgentSpec extends ScalaTestWithActorTestKit with UnitSpec { val receiveAdapter = createTestProbe[ActorRef[FlexRequest]]() // no additional activation ticks - val model = new MockParticipantModel(mockActivationTicks = - Map( - 0 * 3600L -> 4 * 3600L, // still before operation, is ignored - 8 * 3600L -> 12 * 3600L, // middle of operation - 12 * 3600L -> 22 * 3600L, // after operation, is ignored - ) + val model = new MockParticipantModel( + mockActivationTicks = Map( + 0 * 3600L -> 4 * 3600L, // out of operation, is ignored + 8 * 3600L -> 12 * 3600L, // in operation + 12 * 3600L -> 22 * 3600L, // out of operation, is ignored + ), + mockChangeAtNext = Set( + 0, // out of operation, is ignored + 12 * 3600, // in operation + 20 * 3600, // out of operation, is ignored + ), ) val operationInterval = OperationInterval(8 * 3600, 20 * 3600) @@ -836,6 +841,7 @@ class ParticipantAgentSpec extends ScalaTestWithActorTestKit with UnitSpec { em.expectMessage( FlexCompletion( model.uuid, + requestAtNextActivation = true, requestAtTick = Some(operationInterval.end), ) ) @@ -883,7 +889,6 @@ class ParticipantAgentSpec extends ScalaTestWithActorTestKit with UnitSpec { "depending on secondary data" should { "calculate operating point and results correctly with additional ticks" in { - // todo test changesAtNextActivation as well val em = createTestProbe[FlexResponse]() val gridAgent = createTestProbe[GridAgent.Request]() @@ -894,12 +899,17 @@ class ParticipantAgentSpec extends ScalaTestWithActorTestKit with UnitSpec { val receiveAdapter = createTestProbe[ActorRef[FlexRequest]]() // no additional activation ticks - val model = new MockParticipantModel(mockActivationTicks = - Map( - 0 * 3600L -> 4 * 3600L, // still before operation, is ignored - 8 * 3600L -> 12 * 3600L, // middle of operation - 12 * 3600L -> 22 * 3600L, // after operation, is ignored - ) + val model = new MockParticipantModel( + mockActivationTicks = Map( + 0 * 3600L -> 4 * 3600L, // out of operation, is ignored + 8 * 3600L -> 12 * 3600L, // in operation + 12 * 3600L -> 22 * 3600L, // out of operation, is ignored + ), + mockChangeAtNext = Set( + 0, // out of operation, is ignored + 18 * 3600, // in operation + 20 * 3600, // out of operation, is ignored + ), ) val operationInterval = OperationInterval(8 * 3600, 20 * 3600) @@ -1096,6 +1106,7 @@ class ParticipantAgentSpec extends ScalaTestWithActorTestKit with UnitSpec { em.expectMessage( FlexCompletion( model.uuid, + requestAtNextActivation = true, requestAtTick = Some(operationInterval.end), ) ) From c37b25a4c5a75210a1d62b2614304c547479b4bc Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Sun, 1 Dec 2024 18:18:56 +0100 Subject: [PATCH 64/77] Improving primary data model Signed-off-by: Sebastian Peter --- .../participant2/PrimaryDataParticipantModel.scala | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/model/participant2/PrimaryDataParticipantModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/PrimaryDataParticipantModel.scala index 872b31c514..508b5fc28e 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/PrimaryDataParticipantModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/PrimaryDataParticipantModel.scala @@ -92,11 +92,6 @@ final case class PrimaryDataParticipantModel[P <: PrimaryData: ClassTag]( Iterable.empty } - /** @param receivedData - * @throws CriticalFailureException - * if unexpected type of data was provided - * @return - */ override def createRelevantData( receivedData: Seq[Data], nodalVoltage: Dimensionless, @@ -109,7 +104,9 @@ final case class PrimaryDataParticipantModel[P <: PrimaryData: ClassTag]( } .getOrElse { throw new CriticalFailureException( - s"Expected WeatherData, got $receivedData" + "Expected primary data of type " + + s"${implicitly[ClassTag[P]].runtimeClass.getSimpleName}, " + + s"got $receivedData" ) } @@ -118,9 +115,8 @@ final case class PrimaryDataParticipantModel[P <: PrimaryData: ClassTag]( relevantData: PrimaryOperationRelevantData[P], ): FlexibilityMessage.ProvideFlexOptions = { val (operatingPoint, _) = determineOperatingPoint(state, relevantData) - val power = operatingPoint.activePower - ProvideMinMaxFlexOptions.noFlexOption(uuid, power) + ProvideMinMaxFlexOptions.noFlexOption(uuid, operatingPoint.activePower) } override def handlePowerControl( @@ -148,7 +144,7 @@ object PrimaryDataParticipantModel { override val activePower: Power = data.p } - object PrimaryOperatingPoint { + private object PrimaryOperatingPoint { def apply[P <: PrimaryData: ClassTag]( data: P ): PrimaryOperatingPoint[P] = From b296350cc2c1807587fa2fe81ac4eb5e4b147c0f Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Sun, 1 Dec 2024 18:20:53 +0100 Subject: [PATCH 65/77] Adding test for primary data participant agent Signed-off-by: Sebastian Peter --- .../participant2/ParticipantModelInit.scala | 14 + .../participant2/MockParticipantModel.scala | 9 +- .../participant2/ParticipantAgentSpec.scala | 512 +++++++++++++++++- 3 files changed, 511 insertions(+), 24 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelInit.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelInit.scala index 4b448df033..32630944b8 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelInit.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelInit.scala @@ -83,6 +83,20 @@ object ParticipantModelInit { modelConfig, ) + createPrimaryModel( + physicalModel, + primaryDataMeta, + ) + } + + def createPrimaryModel[P <: PrimaryData: ClassTag]( + physicalModel: ParticipantModel[_, _, _], + primaryDataMeta: PrimaryDataMeta[P], + ): ParticipantModel[ + _ <: OperatingPoint, + _ <: ModelState, + _ <: OperationRelevantData, + ] = { val primaryResultFunc = new PrimaryResultFunc { override def createResult( data: PrimaryData.PrimaryDataWithComplexPower[_], diff --git a/src/test/scala/edu/ie3/simona/agent/participant2/MockParticipantModel.scala b/src/test/scala/edu/ie3/simona/agent/participant2/MockParticipantModel.scala index 73353362dc..22dd2174b9 100644 --- a/src/test/scala/edu/ie3/simona/agent/participant2/MockParticipantModel.scala +++ b/src/test/scala/edu/ie3/simona/agent/participant2/MockParticipantModel.scala @@ -82,7 +82,14 @@ class MockParticipantModel( override def createPrimaryDataResult( data: PrimaryData.PrimaryDataWithComplexPower[_], dateTime: ZonedDateTime, - ): SystemParticipantResult = throw new NotImplementedError() // Not tested + ): SystemParticipantResult = { + MockResult( + dateTime, + uuid, + data.p.toMegawatts.asMegaWatt, + data.q.toMegavars.asMegaVar, + ) + } override def getRequiredSecondaryServices: Iterable[ServiceType] = throw new NotImplementedError() // Not tested diff --git a/src/test/scala/edu/ie3/simona/agent/participant2/ParticipantAgentSpec.scala b/src/test/scala/edu/ie3/simona/agent/participant2/ParticipantAgentSpec.scala index 69ac314294..62a2124f6b 100644 --- a/src/test/scala/edu/ie3/simona/agent/participant2/ParticipantAgentSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/participant2/ParticipantAgentSpec.scala @@ -11,6 +11,10 @@ import edu.ie3.simona.agent.grid.GridAgentMessages.{ AssetPowerChangedMessage, AssetPowerUnchangedMessage, } +import edu.ie3.simona.agent.participant.data.Data.PrimaryData.{ + ActivePower, + ActivePowerMeta, +} import edu.ie3.simona.agent.participant2.MockParticipantModel.{ MockRequestMessage, MockResponseMessage, @@ -24,16 +28,12 @@ import edu.ie3.simona.agent.participant2.ParticipantAgent.{ } import edu.ie3.simona.event.ResultEvent import edu.ie3.simona.event.ResultEvent.ParticipantResultEvent -import edu.ie3.simona.model.participant2.ParticipantModelShell -import edu.ie3.simona.ontology.messages.SchedulerMessage.Completion -import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage.{ - FlexActivation, - FlexCompletion, - FlexRequest, - FlexResponse, - IssueNoControl, - IssuePowerControl, +import edu.ie3.simona.model.participant2.{ + ParticipantModelInit, + ParticipantModelShell, } +import edu.ie3.simona.ontology.messages.SchedulerMessage.Completion +import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage._ import edu.ie3.simona.ontology.messages.flex.MinMaxFlexibilityMessage.ProvideMinMaxFlexOptions import edu.ie3.simona.ontology.messages.{Activation, SchedulerMessage} import edu.ie3.simona.test.common.UnitSpec @@ -44,9 +44,9 @@ import edu.ie3.util.scala.OperationInterval import edu.ie3.util.scala.quantities.{Kilovars, ReactivePower} import org.apache.pekko.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit import org.apache.pekko.actor.typed.ActorRef -import squants.{Each, Power} -import squants.energy.Kilowatts import org.apache.pekko.actor.typed.scaladsl.adapter._ +import squants.energy.Kilowatts +import squants.{Each, Power} import java.time.ZonedDateTime @@ -76,7 +76,7 @@ class ParticipantAgentSpec extends ScalaTestWithActorTestKit with UnitSpec { val receiveAdapter = createTestProbe[ActorRef[Activation]]() // no additional activation ticks - val model = new MockParticipantModel(mockActivationTicks = Map.empty) + val model = new MockParticipantModel() val operationInterval = OperationInterval(8 * 3600, 20 * 3600) val participantAgent = spawn( @@ -146,6 +146,7 @@ class ParticipantAgentSpec extends ScalaTestWithActorTestKit with UnitSpec { // first request participantAgent ! RequestAssetPowerMessage(12 * 3600, Each(1), Each(0)) + // 8 hours of 0 kW, 4 hours of 6 kW gridAgent.expectMessageType[AssetPowerChangedMessage] match { case AssetPowerChangedMessage(p, q) => p should approximate(Kilowatts(2)) @@ -220,7 +221,7 @@ class ParticipantAgentSpec extends ScalaTestWithActorTestKit with UnitSpec { // receiving the activation adapter val receiveAdapter = createTestProbe[ActorRef[Activation]]() - // no additional activation ticks + // with additional activation ticks val model = new MockParticipantModel(mockActivationTicks = Map( 0 * 3600L -> 4 * 3600L, // still before operation, is ignored @@ -289,6 +290,7 @@ class ParticipantAgentSpec extends ScalaTestWithActorTestKit with UnitSpec { participantAgent ! RequestAssetPowerMessage(12 * 3600, Each(1), Each(0)) + // 8 hours of 0 kW, 4 hours of 6 kW gridAgent.expectMessageType[AssetPowerChangedMessage] match { case AssetPowerChangedMessage(p, q) => p should approximate(Kilowatts(2)) @@ -355,7 +357,7 @@ class ParticipantAgentSpec extends ScalaTestWithActorTestKit with UnitSpec { // receiving the activation adapter val receiveAdapter = createTestProbe[ActorRef[Activation]]() - // no additional activation ticks + // with additional activation ticks val model = new MockParticipantModel(mockActivationTicks = Map( 0 * 3600L -> 4 * 3600L, // still before operation, is ignored @@ -549,7 +551,207 @@ class ParticipantAgentSpec extends ScalaTestWithActorTestKit with UnitSpec { } "depending on primary data" should { - // todo + + "calculate operating point and results correctly" in { + + val scheduler = createTestProbe[SchedulerMessage]() + val gridAgent = createTestProbe[GridAgent.Request]() + val resultListener = createTestProbe[ResultEvent]() + val service = createTestProbe() + + // receiving the activation adapter + val receiveAdapter = createTestProbe[ActorRef[Activation]]() + + // no additional activation ticks + val physicalModel = new MockParticipantModel() + + val model = ParticipantModelInit.createPrimaryModel( + physicalModel, + ActivePowerMeta, + ) + val operationInterval = OperationInterval(8 * 3600, 20 * 3600) + + val participantAgent = spawn( + ParticipantAgentMockFactory.create( + ParticipantModelShell( + model, + operationInterval, + simulationStartDate, + ), + ParticipantInputHandler( + Map(service.ref.toClassic -> 0) + ), + ParticipantGridAdapter( + gridAgent.ref, + 12 * 3600, + ), + Iterable(resultListener.ref), + Left(scheduler.ref, receiveAdapter.ref), + ) + ) + val activationRef = + receiveAdapter.expectMessageType[ActorRef[Activation]] + + // TICK 0: Outside of operation interval + + activationRef ! Activation(0) + + // nothing should happen, still waiting for primary data... + resultListener.expectNoMessage() + scheduler.expectNoMessage() + + participantAgent ! ProvideData( + 0, + service.ref.toClassic, + ActivePower(Kilowatts(1)), + Some(6 * 3600), + ) + + // outside of operation interval, 0 MW + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: MockResult) => + result.getInputModel shouldBe model.uuid + result.getTime shouldBe simulationStartDate + result.getP should equalWithTolerance(0.0.asMegaWatt) + result.getQ should equalWithTolerance(0.0.asMegaVar) + } + + // next model tick and next data tick are ignored, + // because we are outside of operation interval + scheduler.expectMessage( + Completion(activationRef, Some(operationInterval.start)) + ) + + // TICK 6 * 3600: Outside of operation interval, only data expected, no activation + + participantAgent ! ProvideData( + 6 * 3600, + service.ref.toClassic, + ActivePower(Kilowatts(3)), + Some(12 * 3600), + ) + + resultListener.expectNoMessage() + scheduler.expectNoMessage() + + // TICK 8 * 3600: Start of operation interval + + activationRef ! Activation(operationInterval.start) + + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: MockResult) => + result.getInputModel shouldBe model.uuid + result.getTime shouldBe operationInterval.start.toDateTime + result.getP should equalWithTolerance(0.003.asMegaWatt) + result.getQ should equalWithTolerance(0.00145296631.asMegaVar) + } + + // next data tick is hour 12 + scheduler.expectMessage( + Completion(activationRef, Some(12 * 3600)) + ) + + // TICK 12 * 3600: Inside of operation interval, GridAgent requests power + + activationRef ! Activation(12 * 3600) + + participantAgent ! RequestAssetPowerMessage(12 * 3600, Each(1), Each(0)) + + // 8 hours of 0 kW, 4 hours of 3 kW + gridAgent.expectMessageType[AssetPowerChangedMessage] match { + case AssetPowerChangedMessage(p, q) => + p should approximate(Kilowatts(1)) + q should approximate(Kilovars(0.48432210484)) + } + + participantAgent ! FinishParticipantSimulation(12 * 3600, 24 * 3600) + + // nothing should happen, still waiting for primary data... + resultListener.expectNoMessage() + scheduler.expectNoMessage() + + participantAgent ! ProvideData( + 12 * 3600, + service.ref.toClassic, + ActivePower(Kilowatts(6)), + Some(18 * 3600), + ) + + // calculation should start now + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: MockResult) => + result.getInputModel shouldBe model.uuid + result.getTime shouldBe (12 * 3600).toDateTime + result.getP should equalWithTolerance(0.006.asMegaWatt) + result.getQ should equalWithTolerance(0.00290593263.asMegaVar) + } + + // new data is expected at 18 hours + scheduler.expectMessage( + Completion(activationRef, Some(18 * 3600)) + ) + + // TICK 18 * 3600: Inside of operation interval because of expected primary data + + activationRef ! Activation(18 * 3600) + + // nothing should happen, still waiting for primary data... + resultListener.expectNoMessage() + scheduler.expectNoMessage() + + participantAgent ! ProvideData( + 18 * 3600, + service.ref.toClassic, + ActivePower(Kilowatts(3)), + Some(24 * 3600), + ) + + // calculation should start now + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: MockResult) => + result.getInputModel shouldBe model.uuid + result.getTime shouldBe (18 * 3600).toDateTime + result.getP should equalWithTolerance(0.003.asMegaWatt) + result.getQ should equalWithTolerance(0.00145296631.asMegaVar) + } + + scheduler.expectMessage( + Completion(activationRef, Some(operationInterval.end)) + ) + + // TICK 20 * 3600: Outside of operation interval (last tick) + + activationRef ! Activation(operationInterval.end) + + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: MockResult) => + result.getInputModel shouldBe model.uuid + result.getTime shouldBe operationInterval.end.toDateTime + result.getP should equalWithTolerance(0.0.asMegaWatt) + result.getQ should equalWithTolerance(0.0.asMegaVar) + } + + // Since we left the operation interval, there are no more ticks to activate + scheduler.expectMessage(Completion(activationRef)) + + // TICK 24 * 3600: GridAgent requests power + + participantAgent ! RequestAssetPowerMessage(24 * 3600, Each(1), Each(0)) + + // 6 hours of 6 kW, 2 hours of 3 kW, 4 hours of 0 kW + gridAgent.expectMessageType[AssetPowerChangedMessage] match { + case AssetPowerChangedMessage(p, q) => + p should approximate(Kilowatts(3.5)) + q should approximate(Kilovars(1.695127366932)) + } + + participantAgent ! FinishParticipantSimulation(24 * 3600, 36 * 3600) + + resultListener.expectNoMessage() + scheduler.expectNoMessage() + + } + } } @@ -568,7 +770,7 @@ class ParticipantAgentSpec extends ScalaTestWithActorTestKit with UnitSpec { val receiveAdapter = createTestProbe[ActorRef[FlexRequest]]() // no additional activation ticks - val model = new MockParticipantModel(mockActivationTicks = Map.empty) + val model = new MockParticipantModel() val operationInterval = OperationInterval(8 * 3600, 20 * 3600) val participantAgent = spawn( @@ -639,7 +841,7 @@ class ParticipantAgentSpec extends ScalaTestWithActorTestKit with UnitSpec { result.getInputModel shouldBe model.uuid result.getTime shouldBe operationInterval.start.toDateTime result.getP should equalWithTolerance(0.003.asMegaWatt) - result.getQ should equalWithTolerance(0.0014529663.asMegaVar) + result.getQ should equalWithTolerance(0.00145296631.asMegaVar) } em.expectMessage( @@ -653,6 +855,7 @@ class ParticipantAgentSpec extends ScalaTestWithActorTestKit with UnitSpec { participantAgent ! RequestAssetPowerMessage(12 * 3600, Each(1), Each(0)) + // 8 hours of 0 kW, 4 hours of 3 kW gridAgent.expectMessageType[AssetPowerChangedMessage] match { case AssetPowerChangedMessage(p, q) => p should approximate(Kilowatts(1)) @@ -689,6 +892,7 @@ class ParticipantAgentSpec extends ScalaTestWithActorTestKit with UnitSpec { participantAgent ! RequestAssetPowerMessage(24 * 3600, Each(1), Each(0)) + // 8 hours of 3 kW, 4 hours of 0 kW gridAgent.expectMessageType[AssetPowerChangedMessage] match { case AssetPowerChangedMessage(p, q) => p should approximate(Kilowatts(2)) @@ -708,7 +912,7 @@ class ParticipantAgentSpec extends ScalaTestWithActorTestKit with UnitSpec { // receiving the activation adapter val receiveAdapter = createTestProbe[ActorRef[FlexRequest]]() - // no additional activation ticks + // with additional activation ticks val model = new MockParticipantModel( mockActivationTicks = Map( 0 * 3600L -> 4 * 3600L, // out of operation, is ignored @@ -792,7 +996,7 @@ class ParticipantAgentSpec extends ScalaTestWithActorTestKit with UnitSpec { result.getInputModel shouldBe model.uuid result.getTime shouldBe operationInterval.start.toDateTime result.getP should equalWithTolerance(0.003.asMegaWatt) - result.getQ should equalWithTolerance(0.0014529663.asMegaVar) + result.getQ should equalWithTolerance(0.00145296631.asMegaVar) } em.expectMessage( @@ -874,6 +1078,7 @@ class ParticipantAgentSpec extends ScalaTestWithActorTestKit with UnitSpec { participantAgent ! RequestAssetPowerMessage(24 * 3600, Each(1), Each(0)) + // 8 hours of 1 kW, 4 hours of 0 kW gridAgent.expectMessageType[AssetPowerChangedMessage] match { case AssetPowerChangedMessage(p, q) => p should approximate(Kilowatts(0.6666666667)) @@ -898,7 +1103,7 @@ class ParticipantAgentSpec extends ScalaTestWithActorTestKit with UnitSpec { // receiving the activation adapter val receiveAdapter = createTestProbe[ActorRef[FlexRequest]]() - // no additional activation ticks + // with additional activation ticks val model = new MockParticipantModel( mockActivationTicks = Map( 0 * 3600L -> 4 * 3600L, // out of operation, is ignored @@ -1007,7 +1212,7 @@ class ParticipantAgentSpec extends ScalaTestWithActorTestKit with UnitSpec { result.getInputModel shouldBe model.uuid result.getTime shouldBe operationInterval.start.toDateTime result.getP should equalWithTolerance(0.003.asMegaWatt) - result.getQ should equalWithTolerance(0.0014529663.asMegaVar) + result.getQ should equalWithTolerance(0.00145296631.asMegaVar) } // next model tick and next data tick are both hour 12 @@ -1024,6 +1229,7 @@ class ParticipantAgentSpec extends ScalaTestWithActorTestKit with UnitSpec { participantAgent ! RequestAssetPowerMessage(12 * 3600, Each(1), Each(0)) + // 8 hours of 0 kW, 4 hours of 3 kW gridAgent.expectMessageType[AssetPowerChangedMessage] match { case AssetPowerChangedMessage(p, q) => p should approximate(Kilowatts(1)) @@ -1059,7 +1265,7 @@ class ParticipantAgentSpec extends ScalaTestWithActorTestKit with UnitSpec { result.getInputModel shouldBe model.uuid result.getTime shouldBe (12 * 3600).toDateTime result.getP should equalWithTolerance(0.003.asMegaWatt) - result.getQ should equalWithTolerance(0.001452966315.asMegaVar) + result.getQ should equalWithTolerance(0.00145296631.asMegaVar) } em.expectMessage( @@ -1157,7 +1363,267 @@ class ParticipantAgentSpec extends ScalaTestWithActorTestKit with UnitSpec { } "depending on primary data" should { - // todo + + "calculate operating point and results correctly" in { + + val em = createTestProbe[FlexResponse]() + val gridAgent = createTestProbe[GridAgent.Request]() + val resultListener = createTestProbe[ResultEvent]() + val service = createTestProbe() + + // receiving the activation adapter + val receiveAdapter = createTestProbe[ActorRef[FlexRequest]]() + + // no additional activation ticks + val physicalModel = new MockParticipantModel() + + val model = ParticipantModelInit.createPrimaryModel( + physicalModel, + ActivePowerMeta, + ) + val operationInterval = OperationInterval(8 * 3600, 20 * 3600) + + val participantAgent = spawn( + ParticipantAgentMockFactory.create( + ParticipantModelShell( + model, + operationInterval, + simulationStartDate, + ), + ParticipantInputHandler( + Map(service.ref.toClassic -> 0) + ), + ParticipantGridAdapter( + gridAgent.ref, + 12 * 3600, + ), + Iterable(resultListener.ref), + Right(em.ref, receiveAdapter.ref), + ) + ) + val flexRef = receiveAdapter.expectMessageType[ActorRef[FlexRequest]] + + // TICK 0: Outside of operation interval + + flexRef ! FlexActivation(0) + + // nothing should happen, still waiting for primary data... + resultListener.expectNoMessage() + em.expectNoMessage() + + participantAgent ! ProvideData( + 0, + service.ref.toClassic, + ActivePower(Kilowatts(1)), + Some(6 * 3600), + ) + + em.expectMessageType[ProvideMinMaxFlexOptions] match { + case ProvideMinMaxFlexOptions(modelUuid, ref, min, max) => + modelUuid shouldBe model.uuid + ref should approximate(Kilowatts(0)) + min should approximate(Kilowatts(0)) + max should approximate(Kilowatts(0)) + } + + flexRef ! IssueNoControl(0) + + // outside of operation interval, 0 MW + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: MockResult) => + result.getInputModel shouldBe model.uuid + result.getTime shouldBe simulationStartDate + result.getP should equalWithTolerance(0.0.asMegaWatt) + result.getQ should equalWithTolerance(0.0.asMegaVar) + } + + // next model tick and next data tick are ignored, + // because we are outside of operation interval + em.expectMessage( + FlexCompletion( + model.uuid, + requestAtTick = Some(operationInterval.start), + ) + ) + + // TICK 6 * 3600: Outside of operation interval, only data expected, no activation + + participantAgent ! ProvideData( + 6 * 3600, + service.ref.toClassic, + ActivePower(Kilowatts(3)), + Some(12 * 3600), + ) + + resultListener.expectNoMessage() + em.expectNoMessage() + + // TICK 8 * 3600: Start of operation interval + + flexRef ! FlexActivation(operationInterval.start) + + em.expectMessageType[ProvideMinMaxFlexOptions] match { + case ProvideMinMaxFlexOptions(modelUuid, ref, min, max) => + modelUuid shouldBe model.uuid + ref should approximate(Kilowatts(3)) + min should approximate(Kilowatts(3)) + max should approximate(Kilowatts(3)) + } + + flexRef ! IssuePowerControl(operationInterval.start, Kilowatts(3)) + + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: MockResult) => + result.getInputModel shouldBe model.uuid + result.getTime shouldBe operationInterval.start.toDateTime + result.getP should equalWithTolerance(0.003.asMegaWatt) + result.getQ should equalWithTolerance(0.00145296631.asMegaVar) + } + + // next data tick is hour 12 + em.expectMessage( + FlexCompletion( + model.uuid, + requestAtTick = Some(12 * 3600), + ) + ) + + // TICK 12 * 3600: Inside of operation interval, GridAgent requests power + + flexRef ! FlexActivation(12 * 3600) + + participantAgent ! RequestAssetPowerMessage(12 * 3600, Each(1), Each(0)) + + // 8 hours of 0 kW, 4 hours of 3 kW + gridAgent.expectMessageType[AssetPowerChangedMessage] match { + case AssetPowerChangedMessage(p, q) => + p should approximate(Kilowatts(1)) + q should approximate(Kilovars(0.48432210483)) + } + + participantAgent ! FinishParticipantSimulation(12 * 3600, 24 * 3600) + + // nothing should happen, still waiting for primary data... + resultListener.expectNoMessage() + em.expectNoMessage() + + participantAgent ! ProvideData( + 12 * 3600, + service.ref.toClassic, + ActivePower(Kilowatts(6)), + Some(18 * 3600), + ) + + // calculation should start now + em.expectMessageType[ProvideMinMaxFlexOptions] match { + case ProvideMinMaxFlexOptions(modelUuid, ref, min, max) => + modelUuid shouldBe model.uuid + ref should approximate(Kilowatts(6)) + min should approximate(Kilowatts(6)) + max should approximate(Kilowatts(6)) + } + + flexRef ! IssueNoControl(12 * 3600) + + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: MockResult) => + result.getInputModel shouldBe model.uuid + result.getTime shouldBe (12 * 3600).toDateTime + result.getP should equalWithTolerance(0.006.asMegaWatt) + result.getQ should equalWithTolerance(0.00290593263.asMegaVar) + } + + em.expectMessage( + FlexCompletion( + model.uuid, + requestAtTick = Some(18 * 3600), + ) + ) + + // TICK 18 * 3600: Inside of operation interval because of expected primary data + + flexRef ! FlexActivation(18 * 3600) + + // nothing should happen, still waiting for primary data... + resultListener.expectNoMessage() + em.expectNoMessage() + + participantAgent ! ProvideData( + 18 * 3600, + service.ref.toClassic, + ActivePower(Kilowatts(3)), + Some(24 * 3600), + ) + + // calculation should start now + em.expectMessageType[ProvideMinMaxFlexOptions] match { + case ProvideMinMaxFlexOptions(modelUuid, ref, min, max) => + modelUuid shouldBe model.uuid + ref should approximate(Kilowatts(3)) + min should approximate(Kilowatts(3)) + max should approximate(Kilowatts(3)) + } + + flexRef ! IssueNoControl(18 * 3600) + + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: MockResult) => + result.getInputModel shouldBe model.uuid + result.getTime shouldBe (18 * 3600).toDateTime + result.getP should equalWithTolerance(0.003.asMegaWatt) + result.getQ should equalWithTolerance(0.00145296631.asMegaVar) + } + + em.expectMessage( + FlexCompletion( + model.uuid, + requestAtTick = Some(operationInterval.end), + ) + ) + + // TICK 20 * 3600: Outside of operation interval (last tick) + + flexRef ! FlexActivation(operationInterval.end) + + em.expectMessageType[ProvideMinMaxFlexOptions] match { + case ProvideMinMaxFlexOptions(modelUuid, ref, min, max) => + modelUuid shouldBe model.uuid + ref should approximate(Kilowatts(0)) + min should approximate(Kilowatts(0)) + max should approximate(Kilowatts(0)) + } + + flexRef ! IssueNoControl(operationInterval.end) + + resultListener.expectMessageType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: MockResult) => + result.getInputModel shouldBe model.uuid + result.getTime shouldBe operationInterval.end.toDateTime + result.getP should equalWithTolerance(0.0.asMegaWatt) + result.getQ should equalWithTolerance(0.0.asMegaVar) + } + + // Since we left the operation interval, there are no more ticks to activate + em.expectMessage(FlexCompletion(model.uuid)) + + // TICK 24 * 3600: GridAgent requests power + + participantAgent ! RequestAssetPowerMessage(24 * 3600, Each(1), Each(0)) + + // 6 hours of 6 kW, 2 hours of 3 kW, 4 hours of 0 kW + gridAgent.expectMessageType[AssetPowerChangedMessage] match { + case AssetPowerChangedMessage(p, q) => + p should approximate(Kilowatts(3.5)) + q should approximate(Kilovars(1.695127366932)) + } + + participantAgent ! FinishParticipantSimulation(24 * 3600, 36 * 3600) + + resultListener.expectNoMessage() + em.expectNoMessage() + + } + } } From d49fc8afbf07d34c680aea6a7485587e151634bd Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Mon, 2 Dec 2024 16:01:28 +0100 Subject: [PATCH 66/77] Testing primary data meta functions Signed-off-by: Sebastian Peter --- .../agent/participant/data/DataSpec.scala | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 src/test/scala/edu/ie3/simona/agent/participant/data/DataSpec.scala diff --git a/src/test/scala/edu/ie3/simona/agent/participant/data/DataSpec.scala b/src/test/scala/edu/ie3/simona/agent/participant/data/DataSpec.scala new file mode 100644 index 0000000000..0f33a9c84c --- /dev/null +++ b/src/test/scala/edu/ie3/simona/agent/participant/data/DataSpec.scala @@ -0,0 +1,52 @@ +/* + * © 2024. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.agent.participant.data + +import edu.ie3.simona.agent.participant.data.Data.PrimaryData._ +import edu.ie3.simona.test.common.UnitSpec +import edu.ie3.util.scala.quantities.DefaultQuantities._ +import edu.ie3.util.scala.quantities.Kilovars +import squants.energy.Kilowatts + +class DataSpec extends UnitSpec { + + "Meta functions for active power should work as expected" in { + ActivePowerMeta.zero shouldBe ActivePower(zeroKW) + + ActivePowerMeta.scale(ActivePower(Kilowatts(5)), 2.5) shouldBe ActivePower( + Kilowatts(12.5) + ) + } + + "Meta functions for complex power should work as expected" in { + ComplexPowerMeta.zero shouldBe ComplexPower(zeroKW, zeroKVAr) + + ComplexPowerMeta.scale( + ComplexPower(Kilowatts(5), Kilovars(2)), + 1.5, + ) shouldBe ComplexPower(Kilowatts(7.5), Kilovars(3)) + } + + "Meta functions for active power and heat should work as expected" in { + ActivePowerAndHeatMeta.zero shouldBe ActivePowerAndHeat(zeroKW, zeroKW) + + ActivePowerAndHeatMeta.scale( + ActivePowerAndHeat(Kilowatts(5), Kilowatts(2)), + 2, + ) shouldBe ActivePowerAndHeat(Kilowatts(10), Kilowatts(4)) + } + + "Meta functions for complex power and heat should work as expected" in { + ComplexPowerAndHeatMeta.zero shouldBe ComplexPowerAndHeat(zeroKW, zeroKVAr, zeroKW) + + ComplexPowerAndHeatMeta.scale( + ComplexPowerAndHeat(Kilowatts(5), Kilovars(1), Kilowatts(2)), + 3, + ) shouldBe ComplexPowerAndHeat(Kilowatts(15), Kilovars(3), Kilowatts(6)) + } + +} From 465bdcf29673d002ae9867cc3943ff6105487081 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Mon, 2 Dec 2024 16:09:10 +0100 Subject: [PATCH 67/77] spotless Signed-off-by: Sebastian Peter --- .../scala/edu/ie3/simona/agent/participant/data/DataSpec.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/test/scala/edu/ie3/simona/agent/participant/data/DataSpec.scala b/src/test/scala/edu/ie3/simona/agent/participant/data/DataSpec.scala index 0f33a9c84c..5ca7e50fda 100644 --- a/src/test/scala/edu/ie3/simona/agent/participant/data/DataSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/participant/data/DataSpec.scala @@ -41,7 +41,8 @@ class DataSpec extends UnitSpec { } "Meta functions for complex power and heat should work as expected" in { - ComplexPowerAndHeatMeta.zero shouldBe ComplexPowerAndHeat(zeroKW, zeroKVAr, zeroKW) + ComplexPowerAndHeatMeta.zero shouldBe + ComplexPowerAndHeat(zeroKW, zeroKVAr, zeroKW) ComplexPowerAndHeatMeta.scale( ComplexPowerAndHeat(Kilowatts(5), Kilovars(1), Kilowatts(2)), From b029006bd0a96c23eca1e16dd6c90eac1b04c74f Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Mon, 2 Dec 2024 17:19:44 +0100 Subject: [PATCH 68/77] Fixing some code smells and simplifying code Signed-off-by: Sebastian Peter --- .../agent/participant2/ParticipantAgent.scala | 8 +- .../participant2/ParticipantAgentInit.scala | 5 +- .../participant2/ParticipantGridAdapter.scala | 2 +- .../participant2/ParticipantModelShell.scala | 150 ++++++++++++------ 4 files changed, 103 insertions(+), 62 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgent.scala b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgent.scala index 40f90c6ff5..5afb2e9d09 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgent.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgent.scala @@ -367,11 +367,7 @@ object ParticipantAgent { throw new CriticalFailureException( "Received flex activation while not controlled by EM" ), - _.emAgent ! modelWithFlex.flexOptions.getOrElse( - throw new CriticalFailureException( - "Flex options have not been calculated!" - ) - ), + _.emAgent ! modelWithFlex.flexOptions, ) (modelWithFlex, gridAdapter) @@ -406,7 +402,7 @@ object ParticipantAgent { "Received issue flex control while not controlled by EM" ), _.emAgent ! FlexCompletion( - shellWithOP.model.uuid, + shellWithOP.uuid, changeIndicator.changesAtNextActivation, changeIndicator.changesAtTick, ), diff --git a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgentInit.scala b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgentInit.scala index 7d367e3e78..4af474a47f 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgentInit.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantAgentInit.scala @@ -176,8 +176,7 @@ object ParticipantAgentInit { simulationEndDate, ) - val requiredServiceTypes = - modelShell.model.getRequiredSecondaryServices.toSet + val requiredServiceTypes = modelShell.requiredServices.toSet if (requiredServiceTypes.isEmpty) { // Models that do not use secondary data always start at tick 0 @@ -278,7 +277,7 @@ object ParticipantAgentInit { Some(firstTick), ), _.emAgent ! FlexCompletion( - modelShell.model.uuid, + modelShell.uuid, requestAtNextActivation = false, Some(firstTick), ), diff --git a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantGridAdapter.scala b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantGridAdapter.scala index 8f9c3e7d6b..6dd5e50f9d 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantGridAdapter.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant2/ParticipantGridAdapter.scala @@ -157,7 +157,7 @@ object ParticipantGridAdapter { val lastTickBeforeWindowStart = tickToPower.rangeUntil(windowStart + 1).lastOption - // throw out all entries before or at windowStart + // remove all entries before or at windowStart val reducedMap = tickToPower.rangeFrom(windowStart + 1) // combine both diff --git a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala index 8f7cbb925e..c6fdf49a31 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/ParticipantModelShell.scala @@ -18,9 +18,9 @@ import edu.ie3.simona.exceptions.CriticalFailureException import edu.ie3.simona.model.SystemComponent import edu.ie3.simona.model.em.EmTools import edu.ie3.simona.model.participant2.ParticipantModel.{ - OperationChangeIndicator, ModelState, OperatingPoint, + OperationChangeIndicator, OperationRelevantData, } import edu.ie3.simona.model.participant2.ParticipantModelShell.ResultsContainer @@ -29,6 +29,7 @@ import edu.ie3.simona.ontology.messages.flex.FlexibilityMessage.{ ProvideFlexOptions, } import edu.ie3.simona.ontology.messages.flex.MinMaxFlexibilityMessage.ProvideMinMaxFlexOptions +import edu.ie3.simona.service.ServiceType import edu.ie3.simona.util.TickUtil.TickLong import edu.ie3.util.scala.OperationInterval import edu.ie3.util.scala.quantities.DefaultQuantities._ @@ -38,6 +39,7 @@ import squants.Dimensionless import squants.energy.Power import java.time.ZonedDateTime +import java.util.UUID import scala.reflect.ClassTag /** Takes care of: @@ -53,18 +55,75 @@ final case class ParticipantModelShell[ S <: ModelState, OR <: OperationRelevantData, ]( - model: ParticipantModel[OP, S, OR] with ParticipantFlexibility[OP, S, OR], - operationInterval: OperationInterval, - simulationStartDate: ZonedDateTime, - state: Option[S] = None, - relevantData: Option[OR] = None, - flexOptions: Option[ProvideFlexOptions] = None, - lastOperatingPoint: Option[OP] = None, - operatingPoint: Option[OP] = None, - private val modelChange: OperationChangeIndicator = + private val model: ParticipantModel[OP, S, OR] + with ParticipantFlexibility[OP, S, OR], + private val operationInterval: OperationInterval, + private val simulationStartDate: ZonedDateTime, + private val _state: Option[S] = None, + private val _relevantData: Option[OR] = None, + private val _flexOptions: Option[ProvideFlexOptions] = None, + private val _lastOperatingPoint: Option[OP] = None, + private val _operatingPoint: Option[OP] = None, + private val _modelChange: OperationChangeIndicator = OperationChangeIndicator(), ) { + /** Returns the model UUID. + * + * @return + * the UUID of the model + */ + def uuid: UUID = model.uuid + + /** Returns the types of required secondary services for the model to + * function. + * + * @return + * the types of secondary services required + */ + def requiredServices: Iterable[ServiceType] = + model.getRequiredSecondaryServices + + /** Returns the current relevant data, if present, or throws a + * [[CriticalFailureException]]. Only call this if you are certain the + * operation relevant data has been set. + * + * @return + * the operation relevant data + */ + private def getRelevantData: OR = + _relevantData.getOrElse( + throw new CriticalFailureException("No relevant data available!") + ) + + /** Returns the current operating point, if present, or throws a + * [[CriticalFailureException]]. Only call this if you are certain the + * operating point has been set. + * + * @return + * the operating point + */ + private def operatingPoint: OP = { + _operatingPoint + .getOrElse( + throw new CriticalFailureException("No operating point available!") + ) + } + + /** Returns the current flex options, if present, or throws a + * [[CriticalFailureException]]. Only call this if you are certain the flex + * options have been set. + * + * @return + * the flex options + */ + def flexOptions: ProvideFlexOptions = + _flexOptions.getOrElse( + throw new CriticalFailureException( + "Flex options have not been calculated!" + ) + ) + def updateRelevantData( receivedData: Seq[Data], nodalVoltage: Dimensionless, @@ -79,7 +138,7 @@ final case class ParticipantModelShell[ currentSimulationTime, ) - copy(relevantData = Some(updatedRelevantData)) + copy(_relevantData = Some(updatedRelevantData)) } /** Update operating point when the model is '''not''' em-controlled. @@ -92,12 +151,10 @@ final case class ParticipantModelShell[ ): ParticipantModelShell[OP, S, OR] = { val currentState = determineCurrentState(currentTick) - def modelOperatingPoint() = { + def modelOperatingPoint(): (OP, OperationChangeIndicator) = { val (modelOp, modelNextTick) = model.determineOperatingPoint( currentState, - relevantData.getOrElse( - throw new CriticalFailureException("No relevant data available!") - ), + getRelevantData, ) val modelIndicator = OperationChangeIndicator(changesAtTick = modelNextTick) @@ -108,10 +165,10 @@ final case class ParticipantModelShell[ determineOperatingPoint(modelOperatingPoint, currentTick) copy( - state = Some(currentState), - lastOperatingPoint = operatingPoint, - operatingPoint = Some(newOperatingPoint), - modelChange = newChangeIndicator, + _state = Some(currentState), + _lastOperatingPoint = _operatingPoint, + _operatingPoint = Some(newOperatingPoint), + _modelChange = newChangeIndicator, ) } @@ -122,23 +179,16 @@ final case class ParticipantModelShell[ currentTick: Long, nodalVoltage: Dimensionless, ): ResultsContainer = { - val op = operatingPoint - .getOrElse( - throw new CriticalFailureException("No operating point available!") - ) - - val activePower = op.activePower - val reactivePower = op.reactivePower.getOrElse( + val activePower = operatingPoint.activePower + val reactivePower = operatingPoint.reactivePower.getOrElse( activeToReactivePowerFunc(nodalVoltage)(activePower) ) val complexPower = ComplexPower(activePower, reactivePower) val participantResults = model.createResults( - state.getOrElse( - throw new CriticalFailureException("No model state available!") - ), - lastOperatingPoint, - op, + determineCurrentState(currentTick), + _lastOperatingPoint, + operatingPoint, complexPower, currentTick.toDateTime(simulationStartDate), ) @@ -156,16 +206,14 @@ final case class ParticipantModelShell[ if (operationInterval.includes(currentTick)) { model.calcFlexOptions( currentState, - relevantData.getOrElse( - throw new CriticalFailureException("No relevant data available!") - ), + getRelevantData, ) } else { // Out of operation, there's no way to operate besides 0 kW ProvideMinMaxFlexOptions.noFlexOption(model.uuid, zeroKW) } - copy(state = Some(currentState), flexOptions = Some(flexOptions)) + copy(_state = Some(currentState), _flexOptions = Some(flexOptions)) } /** Update operating point on receiving [[IssueFlexControl]], i.e. when the @@ -181,8 +229,8 @@ final case class ParticipantModelShell[ val currentTick = flexControl.tick - def modelOperatingPoint() = { - val fo = flexOptions.getOrElse( + def modelOperatingPoint(): (OP, OperationChangeIndicator) = { + val fo = _flexOptions.getOrElse( throw new CriticalFailureException("No flex options available!") ) @@ -193,9 +241,7 @@ final case class ParticipantModelShell[ model.handlePowerControl( currentState, - relevantData.getOrElse( - throw new CriticalFailureException("No relevant data available!") - ), + getRelevantData, fo, setPointActivePower, ) @@ -205,10 +251,10 @@ final case class ParticipantModelShell[ determineOperatingPoint(modelOperatingPoint, currentTick) copy( - state = Some(currentState), - lastOperatingPoint = operatingPoint, - operatingPoint = Some(newOperatingPoint), - modelChange = newChangeIndicator, + _state = Some(currentState), + _lastOperatingPoint = _operatingPoint, + _operatingPoint = Some(newOperatingPoint), + _modelChange = newChangeIndicator, ) } @@ -238,12 +284,12 @@ final case class ParticipantModelShell[ // the end of the operation interval val adaptedNextTick = Seq( - modelChange.changesAtTick, + _modelChange.changesAtTick, nextDataTick, Option(operationInterval.end), ).flatten.minOption - modelChange.copy(changesAtTick = adaptedNextTick) + _modelChange.copy(changesAtTick = adaptedNextTick) } else { // If the model is not active, all activation ticks are ignored besides // potentially the operation start @@ -262,13 +308,13 @@ final case class ParticipantModelShell[ val currentState = determineCurrentState(request.tick) val updatedState = model.handleRequest(currentState, ctx, request) - copy(state = Some(updatedState)) + copy(_state = Some(updatedState)) } private def determineCurrentState(currentTick: Long): S = { // new state is only calculated if there's an old state and an operating point - val currentState = state - .zip(operatingPoint) + val state = _state + .zip(_operatingPoint) .flatMap { case (st, op) => Option.when(st.tick < currentTick) { model.determineState(st, op, currentTick) @@ -276,12 +322,12 @@ final case class ParticipantModelShell[ } .getOrElse(model.initialState(currentTick)) - if (currentState.tick != currentTick) + if (state.tick != currentTick) throw new CriticalFailureException( - s"New state $currentState is not set to current tick $currentTick" + s"New state $state is not set to current tick $currentTick" ) - currentState + state } } From bcfa9b42d8063bc1acdae02855c05db02c685672 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Wed, 4 Dec 2024 12:55:28 +0100 Subject: [PATCH 69/77] Fixing code smell Signed-off-by: Sebastian Peter --- .../model/participant2/load/ProfileLoadModel.scala | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/model/participant2/load/ProfileLoadModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/load/ProfileLoadModel.scala index aa155c0aaf..adac300b10 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/load/ProfileLoadModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/load/ProfileLoadModel.scala @@ -10,6 +10,7 @@ import edu.ie3.datamodel.models.input.system.LoadInput import edu.ie3.datamodel.models.profile.StandardLoadProfile import edu.ie3.simona.agent.participant.data.Data import edu.ie3.simona.config.SimonaConfig.LoadRuntimeConfig +import edu.ie3.simona.exceptions.CriticalFailureException import edu.ie3.simona.model.participant.control.QControl import edu.ie3.simona.model.participant.load.profile.LoadProfileStore import edu.ie3.simona.model.participant.load.random.RandomLoadParamStore @@ -20,7 +21,7 @@ import edu.ie3.simona.model.participant2.ParticipantModel.{ } import edu.ie3.simona.util.TickUtil import edu.ie3.util.scala.quantities.ApparentPower -import squants.{Dimensionless, Power} +import squants.Dimensionless import java.time.ZonedDateTime import java.util.UUID @@ -71,7 +72,15 @@ object ProfileLoadModel { val loadProfileStore: LoadProfileStore = LoadProfileStore() - val loadProfile = input.getLoadProfile.asInstanceOf[StandardLoadProfile] + val loadProfile = input.getLoadProfile match { + case slp: StandardLoadProfile => + slp + case other => + throw new CriticalFailureException( + s"Expected a standard load profile type, got ${other.getClass}" + ) + } + val loadProfileMax = loadProfileStore.maxPower(loadProfile) val reference = LoadReference(input, config) From ec2fde6379ab9fbb2116e063bf83140f1e117d12 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Thu, 5 Dec 2024 16:17:47 +0100 Subject: [PATCH 70/77] Simplifying LoadModel setup Signed-off-by: Sebastian Peter --- .../load/profile/LoadProfileStore.scala | 7 +- .../load/profile/ProfileLoadModel.scala | 4 +- .../model/participant2/load/LoadModel.scala | 106 +++++++----------- .../participant2/load/LoadReference.scala | 101 ----------------- .../participant2/load/LoadReferenceType.scala | 49 ++++++++ .../participant2/load/ProfileLoadModel.scala | 36 ++---- .../participant2/load/RandomLoadModel.scala | 45 +++----- 7 files changed, 124 insertions(+), 224 deletions(-) delete mode 100644 src/main/scala/edu/ie3/simona/model/participant2/load/LoadReference.scala create mode 100644 src/main/scala/edu/ie3/simona/model/participant2/load/LoadReferenceType.scala diff --git a/src/main/scala/edu/ie3/simona/model/participant/load/profile/LoadProfileStore.scala b/src/main/scala/edu/ie3/simona/model/participant/load/profile/LoadProfileStore.scala index 209d9349c8..f759678899 100644 --- a/src/main/scala/edu/ie3/simona/model/participant/load/profile/LoadProfileStore.scala +++ b/src/main/scala/edu/ie3/simona/model/participant/load/profile/LoadProfileStore.scala @@ -18,8 +18,7 @@ import edu.ie3.simona.model.participant.load.profile.LoadProfileStore.{ } import edu.ie3.simona.model.participant.load.{DayType, profile} import org.apache.commons.csv.CSVFormat -import squants.Power -import squants.energy.{KilowattHours, Watts} +import squants.energy.{Energy, KilowattHours, Power, Watts} import java.io.{InputStreamReader, Reader} import java.time.{Duration, ZonedDateTime} @@ -58,7 +57,7 @@ class LoadProfileStore private (val reader: Reader) { def entry( time: ZonedDateTime, loadProfile: StandardLoadProfile, - ): squants.Power = { + ): Power = { val key = LoadProfileKey(loadProfile, time) profileMap.get(key) match { case Some(typeDayValues) => @@ -112,7 +111,7 @@ object LoadProfileStore extends LazyLogging { /** Default standard load profile energy scaling */ - val defaultLoadProfileEnergyScaling: squants.Energy = KilowattHours(1000d) + val profileReferenceEnergy: Energy = KilowattHours(1000d) /** Default entry point to get the default implementation with the provided * default standard load profiles diff --git a/src/main/scala/edu/ie3/simona/model/participant/load/profile/ProfileLoadModel.scala b/src/main/scala/edu/ie3/simona/model/participant/load/profile/ProfileLoadModel.scala index e30390bb74..c0ecbd6baf 100644 --- a/src/main/scala/edu/ie3/simona/model/participant/load/profile/ProfileLoadModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant/load/profile/ProfileLoadModel.scala @@ -67,7 +67,7 @@ final case class ProfileLoadModel( private lazy val energyReferenceScalingFactor = reference match { case EnergyConsumption(energyConsumption) => - energyConsumption / LoadProfileStore.defaultLoadProfileEnergyScaling + energyConsumption / LoadProfileStore.profileReferenceEnergy case _ => throw new IllegalArgumentException( s"Applying energy reference scaling factor for reference mode '$reference' is not supported!" @@ -129,7 +129,7 @@ object ProfileLoadModel { scaledInput, energyConsumption, loadProfileMax, - LoadProfileStore.defaultLoadProfileEnergyScaling, + LoadProfileStore.profileReferenceEnergy, ) } diff --git a/src/main/scala/edu/ie3/simona/model/participant2/load/LoadModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/load/LoadModel.scala index 19215e47d6..b367703432 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/load/LoadModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/load/LoadModel.scala @@ -26,9 +26,10 @@ import edu.ie3.simona.model.participant2.ParticipantModel.{ ParticipantFixedState, } import edu.ie3.simona.service.ServiceType -import edu.ie3.util.quantities.PowerSystemUnits +import edu.ie3.util.quantities.PowerSystemUnits.{KILOVOLTAMPERE, KILOWATTHOUR} import edu.ie3.util.quantities.QuantityUtils.RichQuantityDouble import edu.ie3.util.scala.quantities.{ApparentPower, Kilovoltamperes} +import squants.energy.KilowattHours import squants.{Energy, Power} import java.time.ZonedDateTime @@ -85,77 +86,56 @@ abstract class LoadModel[OR <: OperationRelevantData] object LoadModel { - /** Scale profile based load models' sRated based on a provided active power - * value + /** Calculates the scaling factor and scaled rated apparent power according to + * the reference type * - * When the load is scaled to the active power value, the models' sRated is - * multiplied by the ratio of the provided active power value and the active - * power value of the model (activePowerVal / (input.sRated*input.cosPhi) - * - * @param inputModel - * the input model instance - * @param activePower - * the active power value sRated should be scaled to - * @param safetyFactor - * a safety factor to address potential higher sRated values than the - * original scaling would provide (e.g. when using unrestricted probability - * functions) + * @param referenceType + * The type of reference according to which scaling is calculated + * @param input + * The [[LoadInput]] of the model + * @param maxPower + * The maximum power consumption possible for the model + * @param referenceEnergy + * The (annual) reference energy relevant to the load model * @return - * the inputs model sRated scaled to the provided active power + * the reference scaling factor used for calculation of specific power + * consumption values and the scaled rated apparent power */ - def scaleSRatedActivePower( - inputModel: LoadInput, - activePower: Power, - safetyFactor: Double = 1d, - ): ApparentPower = { + def scaleToReference( + referenceType: LoadReferenceType, + input: LoadInput, + maxPower: Power, + referenceEnergy: Energy, + ): (Double, ApparentPower) = { val sRated = Kilovoltamperes( - inputModel.getsRated - .to(PowerSystemUnits.KILOVOLTAMPERE) + input.getsRated + .to(KILOVOLTAMPERE) .getValue .doubleValue ) - val pRated = sRated.toActivePower(inputModel.getCosPhiRated) - val referenceScalingFactor = activePower / pRated - sRated * referenceScalingFactor * safetyFactor - } - - /** Scale profile based load model's sRated based on the provided yearly - * energy consumption - * - * When the load is scaled based on the consumed energy per year, the - * installed sRated capacity is not usable anymore instead, the load's rated - * apparent power is scaled on the maximum power occurring in the specified - * load profile multiplied by the ratio of the annual consumption and the - * standard load profile scale - * - * @param inputModel - * the input model instance - * @param energyConsumption - * the yearly energy consumption the models' sRated should be scaled to - * @param profileMaxPower - * the maximum power value of the profile - * @param profileEnergyScaling - * the energy scaling factor of the profile (= amount of yearly energy the - * profile is scaled to) - * @param safetyFactor - * a safety factor to address potential higher sRated values than the - * original scaling would provide (e.g. when using unrestricted probability - * functions) - * @return - * the inputs model sRated scaled to the provided energy consumption - */ - def scaleSRatedEnergy( - inputModel: LoadInput, - energyConsumption: Energy, - profileMaxPower: Power, - profileEnergyScaling: Energy, - safetyFactor: Double = 1d, - ): ApparentPower = { - val profileMaxApparentPower = Kilovoltamperes( - profileMaxPower.toKilowatts / inputModel.getCosPhiRated + val eConsAnnual = KilowattHours( + input.geteConsAnnual().to(KILOWATTHOUR).getValue.doubleValue ) - profileMaxApparentPower * (energyConsumption / profileEnergyScaling) * safetyFactor + val referenceScalingFactor = referenceType match { + case LoadReferenceType.ActivePower => + val pRated = sRated.toActivePower(input.getCosPhiRated) + pRated / maxPower + case LoadReferenceType.EnergyConsumption => + eConsAnnual / referenceEnergy + } + + val scaledSRated = referenceType match { + case LoadReferenceType.ActivePower => + sRated + case LoadReferenceType.EnergyConsumption => + val maxApparentPower = Kilovoltamperes( + maxPower.toKilowatts / input.getCosPhiRated + ) + maxApparentPower * referenceScalingFactor + } + + (referenceScalingFactor, scaledSRated) } def apply( diff --git a/src/main/scala/edu/ie3/simona/model/participant2/load/LoadReference.scala b/src/main/scala/edu/ie3/simona/model/participant2/load/LoadReference.scala deleted file mode 100644 index 1dcb22705a..0000000000 --- a/src/main/scala/edu/ie3/simona/model/participant2/load/LoadReference.scala +++ /dev/null @@ -1,101 +0,0 @@ -/* - * © 2020. TU Dortmund University, - * Institute of Energy Systems, Energy Efficiency and Energy Economics, - * Research group Distribution grid planning and operation - */ - -package edu.ie3.simona.model.participant2.load - -import edu.ie3.datamodel.models.input.system.LoadInput -import edu.ie3.simona.config.SimonaConfig -import edu.ie3.util.StringUtils -import edu.ie3.util.quantities.PowerSystemUnits.{MEGAWATT, MEGAWATTHOUR} -import squants.energy.{MegawattHours, Megawatts} -import squants.{Energy, Power} - -/** Denoting difference referencing scenarios for scaling load model output - */ -sealed trait LoadReference { - val key: String - - def getKey: String = key - - def scale(factor: Double): LoadReference -} -object LoadReference { - - /** Scale the load model behaviour to reach the given active power in max - * - * @param power - * Foreseen active power - */ - final case class ActivePower(power: Power) extends LoadReference { - override val key: String = "power" - - override def scale(factor: Double): ActivePower = - copy(power = power * factor) - } - - /** Scale the load model behaviour to reach the given annual energy - * consumption - * - * @param energyConsumption - * Annual energy consumption to reach - */ - final case class EnergyConsumption( - energyConsumption: Energy - ) extends LoadReference { - override val key: String = "energy" - - override def scale(factor: Double): LoadReference = - copy(energyConsumption = energyConsumption * factor) - } - - def isEligibleKey(key: String): Boolean = { - Set("power", "energy").contains(key) - } - - /** Build a reference object, that denotes, to which reference a load model - * behaviour might be scaled. If the behaviour is meant to be scaled to - * energy consumption and no annual energy consumption is given, an - * [[IllegalArgumentException]] is thrown - * - * @param inputModel - * [[LoadInput]] to derive energy information from - * @param modelConfig - * Configuration of model behaviour - * @return - * A [[LoadReference]] for use in [[LoadModel]] - */ - def apply( - inputModel: LoadInput, - modelConfig: SimonaConfig.LoadRuntimeConfig, - ): LoadReference = - StringUtils.cleanString(modelConfig.reference).toLowerCase match { - case "power" => - val activePower = Megawatts( - inputModel - .getsRated() - .to(MEGAWATT) - .getValue - .doubleValue - ) * - inputModel.getCosPhiRated - LoadReference.ActivePower(activePower) - case "energy" => - Option(inputModel.geteConsAnnual()) match { - case Some(consumption) => - LoadReference.EnergyConsumption( - MegawattHours(consumption.to(MEGAWATTHOUR).getValue.doubleValue) - ) - case None => - throw new IllegalArgumentException( - s"Load model with uuid ${inputModel.getUuid} is meant to be scaled to annual energy consumption, but the energy is not provided." - ) - } - case unsupported => - throw new IllegalArgumentException( - s"Load model with uuid ${inputModel.getUuid} is meant to be scaled to unsupported reference '$unsupported'." - ) - } -} diff --git a/src/main/scala/edu/ie3/simona/model/participant2/load/LoadReferenceType.scala b/src/main/scala/edu/ie3/simona/model/participant2/load/LoadReferenceType.scala new file mode 100644 index 0000000000..f1917160aa --- /dev/null +++ b/src/main/scala/edu/ie3/simona/model/participant2/load/LoadReferenceType.scala @@ -0,0 +1,49 @@ +/* + * © 2020. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.model.participant2.load + +import edu.ie3.simona.config.SimonaConfig +import edu.ie3.util.StringUtils + +/** Denoting difference referencing scenarios for scaling load model output + */ +sealed trait LoadReferenceType + +object LoadReferenceType { + + /** Scale the load model behaviour so that the rated power of the load model + * serves as the maximum power consumption + */ + case object ActivePower extends LoadReferenceType + + /** Scale the load model behaviour so that the aggregate annual energy + * consumption corresponds to the energy set by the model input + */ + case object EnergyConsumption extends LoadReferenceType + + /** Build a reference type, that denotes to which type of reference a load + * model behaviour might be scaled. + * + * @param modelConfig + * Configuration of model behaviour + * @return + * A [[LoadReferenceType]] for use in [[LoadModel]] + */ + def apply( + modelConfig: SimonaConfig.LoadRuntimeConfig + ): LoadReferenceType = + StringUtils.cleanString(modelConfig.reference).toLowerCase match { + case "power" => + LoadReferenceType.ActivePower + case "energy" => + LoadReferenceType.EnergyConsumption + case unsupported => + throw new IllegalArgumentException( + s"Unsupported load reference type '$unsupported'." + ) + } +} diff --git a/src/main/scala/edu/ie3/simona/model/participant2/load/ProfileLoadModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/load/ProfileLoadModel.scala index adac300b10..e1f80fd16b 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/load/ProfileLoadModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/load/ProfileLoadModel.scala @@ -33,7 +33,7 @@ class ProfileLoadModel( override val qControl: QControl, private val loadProfileStore: LoadProfileStore, private val loadProfile: StandardLoadProfile, - private val referenceScalingFactor: Double, + val referenceScalingFactor: Double, ) extends LoadModel[DateTimeData] { override def determineOperatingPoint( @@ -81,31 +81,15 @@ object ProfileLoadModel { ) } - val loadProfileMax = loadProfileStore.maxPower(loadProfile) - - val reference = LoadReference(input, config) - - val referenceScalingFactor = - reference match { - case LoadReference.ActivePower(power) => - power / loadProfileMax - case LoadReference.EnergyConsumption(energyConsumption) => - energyConsumption / LoadProfileStore.defaultLoadProfileEnergyScaling - } - - // todo maybe this does not need to be so complicated, referenceScalingFactor is already calculated - val scaledSRated = reference match { - case LoadReference.ActivePower(power) => - LoadModel.scaleSRatedActivePower(input, power) - - case LoadReference.EnergyConsumption(energyConsumption) => - LoadModel.scaleSRatedEnergy( - input, - energyConsumption, - loadProfileMax, - LoadProfileStore.defaultLoadProfileEnergyScaling, - ) - } + val referenceType = LoadReferenceType(config) + + val (referenceScalingFactor, scaledSRated) = + LoadModel.scaleToReference( + referenceType, + input, + loadProfileStore.maxPower(loadProfile), + LoadProfileStore.profileReferenceEnergy, + ) new ProfileLoadModel( input.getUuid, diff --git a/src/main/scala/edu/ie3/simona/model/participant2/load/RandomLoadModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/load/RandomLoadModel.scala index 410222e2de..e8cdc8285d 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/load/RandomLoadModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/load/RandomLoadModel.scala @@ -35,7 +35,7 @@ class RandomLoadModel( override val sRated: ApparentPower, override val cosPhiRated: Double, override val qControl: QControl, - private val referenceScalingFactor: Double, + val referenceScalingFactor: Double, ) extends LoadModel[DateTimeData] { private val randomLoadParamStore = RandomLoadParamStore() @@ -117,7 +117,7 @@ class RandomLoadModel( object RandomLoadModel { - /** The profile energy scaling factor, the random profile is scaled to. + /** The annual energy consumption that the random profile is scaled to. * * It is said in 'Kays - Agent-based simulation environment for improving the * planning of distribution grids', that the Generalized Extreme Value @@ -127,45 +127,34 @@ object RandomLoadModel { * annual energy consumption of approx. this value. It has been found by * 1,000 evaluations of the year 2019. */ - private val randomProfileEnergyScaling = KilowattHours(716.5416966513656) + private val profileReferenceEnergy = KilowattHours(716.5416966513656) /** This is the 95 % quantile resulting from 10,000 evaluations of the year * 2019. It is only needed, when the load is meant to be scaled to rated * active power. - * - * @return - * Reference power to use for later model calculations */ - private val randomMaxPower: Power = Watts(159d) + private val maxPower: Power = Watts(159d) def apply(input: LoadInput, config: LoadRuntimeConfig): RandomLoadModel = { - val reference = LoadReference(input, config) + val referenceType = LoadReferenceType(config) - val referenceScalingFactor = reference match { - case LoadReference.ActivePower(power) => - power / randomMaxPower - case LoadReference.EnergyConsumption(energyConsumption) => - energyConsumption / randomProfileEnergyScaling - } + val (referenceScalingFactor, scaledSRated) = + LoadModel.scaleToReference( + referenceType, + input, + maxPower, + profileReferenceEnergy, + ) - val scaledSRated = reference match { - case LoadReference.ActivePower(power) => - LoadModel.scaleSRatedActivePower(input, power, 1.1) - - case LoadReference.EnergyConsumption(energyConsumption) => - LoadModel.scaleSRatedEnergy( - input, - energyConsumption, - randomMaxPower, - randomProfileEnergyScaling, - 1.1, - ) - } + /** Safety factor to address potential higher sRated values when using + * unrestricted probability functions + */ + val safetyFactor = 1.1 new RandomLoadModel( input.getUuid, - scaledSRated, + scaledSRated * safetyFactor, input.getCosPhiRated, QControl.apply(input.getqCharacteristics()), referenceScalingFactor, From 426f513c4d2655ca19a02615401e11a24556f181 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Thu, 5 Dec 2024 16:18:40 +0100 Subject: [PATCH 71/77] Tests for profile and load models Signed-off-by: Sebastian Peter --- .../load/ProfileLoadModelSpec.scala | 130 ++++++++++++++ .../load/RandomLoadModelSpec.scala | 168 ++++++++++++++++++ .../edu/ie3/simona/test/common/UnitSpec.scala | 7 +- .../simona/test/matchers/DoubleMatchers.scala | 28 +++ 4 files changed, 332 insertions(+), 1 deletion(-) create mode 100644 src/test/scala/edu/ie3/simona/model/participant2/load/ProfileLoadModelSpec.scala create mode 100644 src/test/scala/edu/ie3/simona/model/participant2/load/RandomLoadModelSpec.scala create mode 100644 src/test/scala/edu/ie3/simona/test/matchers/DoubleMatchers.scala diff --git a/src/test/scala/edu/ie3/simona/model/participant2/load/ProfileLoadModelSpec.scala b/src/test/scala/edu/ie3/simona/model/participant2/load/ProfileLoadModelSpec.scala new file mode 100644 index 0000000000..712a262581 --- /dev/null +++ b/src/test/scala/edu/ie3/simona/model/participant2/load/ProfileLoadModelSpec.scala @@ -0,0 +1,130 @@ +/* + * © 2021. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.model.participant2.load + +import edu.ie3.datamodel.models.OperationTime +import edu.ie3.datamodel.models.input.system.LoadInput +import edu.ie3.datamodel.models.input.system.characteristic.CosPhiFixed +import edu.ie3.datamodel.models.input.{NodeInput, OperatorInput} +import edu.ie3.datamodel.models.profile.BdewStandardLoadProfile._ +import edu.ie3.datamodel.models.voltagelevels.GermanVoltageLevelUtils +import edu.ie3.simona.config.SimonaConfig.LoadRuntimeConfig +import edu.ie3.simona.test.common.UnitSpec +import edu.ie3.simona.test.matchers.DoubleMatchers +import edu.ie3.util.quantities.PowerSystemUnits +import edu.ie3.util.scala.quantities.{ApparentPower, Voltamperes} +import tech.units.indriya.quantity.Quantities + +import java.util.UUID + +class ProfileLoadModelSpec extends UnitSpec with DoubleMatchers { + + private implicit val powerTolerance: ApparentPower = Voltamperes(1e-2) + private implicit val doubleTolerance: Double = 1e-6 + + "A profile load model" should { + + val loadInput = new LoadInput( + UUID.fromString("4eeaf76a-ec17-4fc3-872d-34b7d6004b03"), + "testLoad", + OperatorInput.NO_OPERATOR_ASSIGNED, + OperationTime.notLimited(), + new NodeInput( + UUID.fromString("e5c1cde5-c161-4a4f-997f-fcf31fecbf57"), + "TestNodeInputModel", + OperatorInput.NO_OPERATOR_ASSIGNED, + OperationTime.notLimited(), + Quantities.getQuantity(1d, PowerSystemUnits.PU), + false, + NodeInput.DEFAULT_GEO_POSITION, + GermanVoltageLevelUtils.LV, + -1, + ), + new CosPhiFixed("cosPhiFixed:{(0.0,0.95)}"), + null, + H0, + false, + Quantities.getQuantity(3000, PowerSystemUnits.KILOWATTHOUR), + Quantities.getQuantity(282.74, PowerSystemUnits.VOLTAMPERE), + 0.95, + ) + + "be instantiated correctly with power reference" in { + + forAll( + Table( + ("profile", "sRated", "expectedScalingFactor"), + (H0, 282.736842, 1.0), + (H0, 1000.0, 3.536858), + (L0, 253.052632, 1.0), + (L0, 1000.0, 3.951747), + (G0, 253.052632, 1.0), + (G0, 1000.0, 3.951747), + ) + ) { (profile, sRated, expectedScalingFactor) => + val config = LoadRuntimeConfig( + calculateMissingReactivePowerWithModel = false, + scaling = 1.0, + uuids = List.empty, + modelBehaviour = "profile", + reference = "power", + ) + val model = ProfileLoadModel( + loadInput + .copy() + .loadprofile(profile) + .sRated(Quantities.getQuantity(sRated, PowerSystemUnits.VOLTAMPERE)) + .build(), + config, + ) + + model.referenceScalingFactor should approximate(expectedScalingFactor) + + } + + } + + "be instantiated correctly with energy reference" in { + + forAll( + Table( + ("profile", "eConsAnnual", "expectedScalingFactor", "expectedSRated"), + (H0, 1000.0, 1.0, 282.74), + (H0, 3000.0, 3.0, 848.22), + (L0, 1000.0, 1.0, 253.053), + (L0, 3000.0, 3.0, 759.158), + (G0, 1000.0, 1.0, 253.053), + (G0, 3000.0, 3.0, 759.158), + ) + ) { (profile, eConsAnnual, expectedScalingFactor, expectedSRated) => + val config = LoadRuntimeConfig( + calculateMissingReactivePowerWithModel = false, + scaling = 1.0, + uuids = List.empty, + modelBehaviour = "profile", + reference = "energy", + ) + val model = ProfileLoadModel( + loadInput + .copy() + .loadprofile(profile) + .eConsAnnual( + Quantities.getQuantity(eConsAnnual, PowerSystemUnits.KILOWATTHOUR) + ) + .build(), + config, + ) + + model.referenceScalingFactor should approximate(expectedScalingFactor) + model.sRated should approximate(Voltamperes(expectedSRated)) + + } + + } + + } +} diff --git a/src/test/scala/edu/ie3/simona/model/participant2/load/RandomLoadModelSpec.scala b/src/test/scala/edu/ie3/simona/model/participant2/load/RandomLoadModelSpec.scala new file mode 100644 index 0000000000..fb12b6de31 --- /dev/null +++ b/src/test/scala/edu/ie3/simona/model/participant2/load/RandomLoadModelSpec.scala @@ -0,0 +1,168 @@ +/* + * © 2021. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.model.participant2.load + +import de.lmu.ifi.dbs.elki.math.statistics.distribution.GeneralizedExtremeValueDistribution +import edu.ie3.datamodel.models.OperationTime +import edu.ie3.datamodel.models.input.system.LoadInput +import edu.ie3.datamodel.models.input.system.characteristic.CosPhiFixed +import edu.ie3.datamodel.models.input.{NodeInput, OperatorInput} +import edu.ie3.datamodel.models.profile.BdewStandardLoadProfile +import edu.ie3.datamodel.models.voltagelevels.GermanVoltageLevelUtils +import edu.ie3.simona.config.SimonaConfig.LoadRuntimeConfig +import edu.ie3.simona.model.participant.load.random.RandomLoadParameters +import edu.ie3.simona.test.common.UnitSpec +import edu.ie3.util.TimeUtil +import edu.ie3.util.quantities.PowerSystemUnits +import edu.ie3.util.scala.quantities.{ApparentPower, Voltamperes} +import tech.units.indriya.quantity.Quantities + +import java.util.UUID + +class RandomLoadModelSpec extends UnitSpec { + + implicit val powerTolerance: ApparentPower = Voltamperes(1e-2) + private implicit val doubleTolerance: Double = 1e-6 + + "A random load model" should { + + val loadInput = new LoadInput( + UUID.fromString("4eeaf76a-ec17-4fc3-872d-34b7d6004b03"), + "testLoad", + OperatorInput.NO_OPERATOR_ASSIGNED, + OperationTime.notLimited(), + new NodeInput( + UUID.fromString("e5c1cde5-c161-4a4f-997f-fcf31fecbf57"), + "TestNodeInputModel", + OperatorInput.NO_OPERATOR_ASSIGNED, + OperationTime.notLimited(), + Quantities.getQuantity(1d, PowerSystemUnits.PU), + false, + NodeInput.DEFAULT_GEO_POSITION, + GermanVoltageLevelUtils.LV, + -1, + ), + new CosPhiFixed("cosPhiFixed:{(0.0,0.95)}"), + null, + BdewStandardLoadProfile.H0, + false, + Quantities.getQuantity(3000d, PowerSystemUnits.KILOWATTHOUR), + Quantities.getQuantity(282.74d, PowerSystemUnits.VOLTAMPERE), + 0.95, + ) + + "be instantiated correctly with power reference" in { + + forAll( + Table( + ("sRated", "expectedScalingFactor"), + (167.368421, 1.0), + (1000.0, 5.9748428), + ) + ) { (sRated, expectedScalingFactor) => + val config = LoadRuntimeConfig( + calculateMissingReactivePowerWithModel = false, + scaling = 1.0, + uuids = List.empty, + modelBehaviour = "random", + reference = "power", + ) + val model = RandomLoadModel( + loadInput + .copy() + .sRated(Quantities.getQuantity(sRated, PowerSystemUnits.VOLTAMPERE)) + .build(), + config, + ) + + model.referenceScalingFactor should approximate(expectedScalingFactor) + + } + + } + + "be instantiated correctly with energy reference" in { + + forAll( + Table( + ("eConsAnnual", "expectedScalingFactor", "expectedSRated"), + (1000.0, 1.3955921, 256.936), + (2000.0, 2.7911842, 513.8717), + (3000.0, 4.1867763, 770.808), + ) + ) { (eConsAnnual, expectedScalingFactor, expectedSRated) => + val config = LoadRuntimeConfig( + calculateMissingReactivePowerWithModel = false, + scaling = 1.0, + uuids = List.empty, + modelBehaviour = "random", + reference = "energy", + ) + val model = RandomLoadModel( + loadInput + .copy() + .eConsAnnual( + Quantities.getQuantity(eConsAnnual, PowerSystemUnits.KILOWATTHOUR) + ) + .build(), + config, + ) + + model.referenceScalingFactor should approximate(expectedScalingFactor) + model.sRated should approximate(Voltamperes(expectedSRated)) + + } + + } + + "deliver the correct distribution on request" in { + val model = RandomLoadModel( + loadInput, + LoadRuntimeConfig( + calculateMissingReactivePowerWithModel = false, + scaling = 1.0, + uuids = List.empty, + modelBehaviour = "random", + reference = "energy", + ), + ) + + /* Working day, 61st quarter-hour */ + val queryDate = + TimeUtil.withDefaults.toZonedDateTime("2019-07-19T15:21:00Z") + val expectedParams = new RandomLoadParameters( + 0.405802458524704, + 0.0671483352780342, + 0.0417016632854939, + ) + + /* First query leeds to generation of distribution */ + val getGevDistribution = + PrivateMethod[GeneralizedExtremeValueDistribution]( + Symbol("getGevDistribution") + ) + + def firstHit = model invokePrivate getGevDistribution(queryDate) + + firstHit.getK shouldBe expectedParams.k + firstHit.getMu shouldBe expectedParams.my + firstHit.getSigma shouldBe expectedParams.sigma + + /* Second query is only look up in storage */ + def secondHit = model invokePrivate getGevDistribution(queryDate) + + secondHit shouldBe firstHit + } + + } +} + +object RandomLoadModelSpec { + def get95Quantile[V](sortedArray: Array[V]): V = sortedArray( + (sortedArray.length * 0.95).toInt + ) +} diff --git a/src/test/scala/edu/ie3/simona/test/common/UnitSpec.scala b/src/test/scala/edu/ie3/simona/test/common/UnitSpec.scala index 5cbf725051..b9ad362047 100644 --- a/src/test/scala/edu/ie3/simona/test/common/UnitSpec.scala +++ b/src/test/scala/edu/ie3/simona/test/common/UnitSpec.scala @@ -7,7 +7,11 @@ package edu.ie3.simona.test.common import com.typesafe.scalalogging.LazyLogging -import edu.ie3.simona.test.matchers.{QuantityMatchers, SquantsMatchers} +import edu.ie3.simona.test.matchers.{ + DoubleMatchers, + QuantityMatchers, + SquantsMatchers, +} import edu.ie3.util.scala.quantities.{QuantityUtil => PSQuantityUtil} import org.apache.pekko.actor.testkit.typed.scaladsl.LogCapturing import org.scalatest._ @@ -29,6 +33,7 @@ trait UnitSpec extends should.Matchers with QuantityMatchers with SquantsMatchers + with DoubleMatchers with AnyWordSpecLike with LogCapturing with OptionValues diff --git a/src/test/scala/edu/ie3/simona/test/matchers/DoubleMatchers.scala b/src/test/scala/edu/ie3/simona/test/matchers/DoubleMatchers.scala new file mode 100644 index 0000000000..fe9b7a2b16 --- /dev/null +++ b/src/test/scala/edu/ie3/simona/test/matchers/DoubleMatchers.scala @@ -0,0 +1,28 @@ +/* + * © 2024. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.test.matchers + +import org.scalactic.TolerantNumerics +import org.scalatest.matchers.{MatchResult, Matcher} + +trait DoubleMatchers extends TolerantNumerics { + + class DoubleMatcher(right: Double, implicit val tolerance: Double) + extends Matcher[Double] { + private val equality = tolerantDoubleEquality(tolerance) + + override def apply(left: Double): MatchResult = MatchResult( + equality.areEqual(left, right), + s"The values $left and $right differ more than $tolerance in value", + s"The values $left and $right differ less than $tolerance in value", + ) + } + + def approximate(right: Double)(implicit tolerance: Double) = + new DoubleMatcher(right, tolerance) + +} From d0777d42ee6036dc37138fe19604fae68efbd84c Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Wed, 11 Dec 2024 14:30:39 +0100 Subject: [PATCH 72/77] Simplifying maximum calculation of load profiles Signed-off-by: Sebastian Peter --- .../load/profile/LoadProfileStore.scala | 54 +++++++++---------- 1 file changed, 25 insertions(+), 29 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/model/participant/load/profile/LoadProfileStore.scala b/src/main/scala/edu/ie3/simona/model/participant/load/profile/LoadProfileStore.scala index f759678899..31e997b215 100644 --- a/src/main/scala/edu/ie3/simona/model/participant/load/profile/LoadProfileStore.scala +++ b/src/main/scala/edu/ie3/simona/model/participant/load/profile/LoadProfileStore.scala @@ -199,35 +199,31 @@ object LoadProfileStore extends LazyLogging { val knownLoadProfiles: Set[StandardLoadProfile] = profileMap.keySet.map(key => key.standardLoadProfile) - knownLoadProfiles - .flatMap(loadProfile => { - (loadProfile match { - case BdewStandardLoadProfile.H0 => - // max load for h0 is expected to be exclusively found in winter, - // thus we only search there. - DayType.values - .map(dayType => { - val key = - profile.LoadProfileKey(loadProfile, Season.winter, dayType) - // maximum dynamization factor is on day 366 (leap year) or day 365 (regular year). - // The difference between day 365 and day 366 is negligible, thus pick 366 - profileMap - .get(key) - .map(typeDay => dynamization(typeDay.getMaxValue, 366)) - .getOrElse(0d) - }) - .maxOption - case _ => - (for (season <- Season.values; dayType <- DayType.values) yield { - val key = profile.LoadProfileKey(loadProfile, season, dayType) - profileMap.get(key) match { - case Some(value) => Option(value.getMaxValue) - case None => None - } - }).flatten.maxOption - }).map(maxConsumption => loadProfile -> maxConsumption) - }) - .toMap + knownLoadProfiles.flatMap { loadProfile => + val dyn = loadProfile match { + case BdewStandardLoadProfile.H0 => + // Pick the maximum dynamization factor per season, including leap years and non-leap years + (season: Season.Value, value: Double) => { + val day = season match { + case Season.winter => 366 // leap year + case Season.transition => 80 // non-leap year + case Season.summer => 135 // non-leap year + } + dynamization(value, day) + } + + case _ => + (_: Season.Value, value: Double) => value + } + + (for (season <- Season.values; dayType <- DayType.values) yield { + val key = + profile.LoadProfileKey(loadProfile, season, dayType) + profileMap + .get(key) + .map(typeDay => dyn(season, typeDay.getMaxValue)) + }).flatten.maxOption.map(maxConsumption => loadProfile -> maxConsumption) + }.toMap } /** @return From 95891c35650e9f9111289fb61e358928d8f83747 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Wed, 11 Dec 2024 14:30:58 +0100 Subject: [PATCH 73/77] Using the correct param store Signed-off-by: Sebastian Peter --- .../ie3/simona/model/participant2/load/ProfileLoadModel.scala | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/scala/edu/ie3/simona/model/participant2/load/ProfileLoadModel.scala b/src/main/scala/edu/ie3/simona/model/participant2/load/ProfileLoadModel.scala index e1f80fd16b..1754e4080f 100644 --- a/src/main/scala/edu/ie3/simona/model/participant2/load/ProfileLoadModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant2/load/ProfileLoadModel.scala @@ -13,7 +13,6 @@ import edu.ie3.simona.config.SimonaConfig.LoadRuntimeConfig import edu.ie3.simona.exceptions.CriticalFailureException import edu.ie3.simona.model.participant.control.QControl import edu.ie3.simona.model.participant.load.profile.LoadProfileStore -import edu.ie3.simona.model.participant.load.random.RandomLoadParamStore import edu.ie3.simona.model.participant2.ParticipantModel import edu.ie3.simona.model.participant2.ParticipantModel.{ ActivePowerOperatingPoint, @@ -40,7 +39,7 @@ class ProfileLoadModel( state: ParticipantModel.FixedState, relevantData: DateTimeData, ): (ParticipantModel.ActivePowerOperatingPoint, Option[Long]) = { - val resolution = RandomLoadParamStore.resolution.getSeconds + val resolution = LoadProfileStore.resolution.getSeconds val (modelTick, modelDateTime) = TickUtil.roundToResolution( relevantData.tick, From 0f1127da33e48a39351b85ae88a0e5192280d9f7 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Wed, 11 Dec 2024 14:31:39 +0100 Subject: [PATCH 74/77] Introducing test for FixedLoadModel Signed-off-by: Sebastian Peter --- .../load/FixedLoadModelSpec.scala | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 src/test/scala/edu/ie3/simona/model/participant2/load/FixedLoadModelSpec.scala diff --git a/src/test/scala/edu/ie3/simona/model/participant2/load/FixedLoadModelSpec.scala b/src/test/scala/edu/ie3/simona/model/participant2/load/FixedLoadModelSpec.scala new file mode 100644 index 0000000000..ec992ae412 --- /dev/null +++ b/src/test/scala/edu/ie3/simona/model/participant2/load/FixedLoadModelSpec.scala @@ -0,0 +1,62 @@ +/* + * © 2024. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.model.participant2.load + +import edu.ie3.simona.config.SimonaConfig.LoadRuntimeConfig +import edu.ie3.simona.model.participant2.ParticipantModel.{ + FixedRelevantData, + FixedState, +} +import edu.ie3.simona.test.common.input.LoadInputTestData +import edu.ie3.simona.test.common.UnitSpec +import squants.Power +import squants.energy.Watts + +class FixedLoadModelSpec extends UnitSpec with LoadInputTestData { + + private implicit val tolerance: Power = Watts(1e-2) + + "A fixed load model" should { + + "return the desired power in 1,000 calculations" in { + + val cases = Table( + ("reference", "expectedPower"), + ("power", Watts(268.6)), + ("energy", Watts(342.47)), + ) + + forAll(cases) { (reference, expectedPower) => + val config = LoadRuntimeConfig( + calculateMissingReactivePowerWithModel = false, + scaling = 1.0, + uuids = List.empty, + modelBehaviour = "fixed", + reference = reference, + ) + + val model = FixedLoadModel( + loadInput, + config, + ) + + (0 until 1000).foreach { tick => + val (operatingPoint, nextTick) = model.determineOperatingPoint( + FixedState(tick), + FixedRelevantData, + ) + + operatingPoint.activePower should approximate(expectedPower) + operatingPoint.reactivePower shouldBe None + nextTick shouldBe None + } + + } + } + + } +} From 55104d2720b2bc77e328addcf6162270e5fbe933 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Wed, 11 Dec 2024 14:38:00 +0100 Subject: [PATCH 75/77] Transferring aggregation tests Signed-off-by: Sebastian Peter --- .../load/LoadModelTestHelper.scala | 74 +++++++++++++++ .../load/ProfileLoadModelSpec.scala | 91 ++++++++++++++++++- 2 files changed, 163 insertions(+), 2 deletions(-) create mode 100644 src/test/scala/edu/ie3/simona/model/participant2/load/LoadModelTestHelper.scala diff --git a/src/test/scala/edu/ie3/simona/model/participant2/load/LoadModelTestHelper.scala b/src/test/scala/edu/ie3/simona/model/participant2/load/LoadModelTestHelper.scala new file mode 100644 index 0000000000..11d72e3aef --- /dev/null +++ b/src/test/scala/edu/ie3/simona/model/participant2/load/LoadModelTestHelper.scala @@ -0,0 +1,74 @@ +/* + * © 2024. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.model.participant2.load + +import edu.ie3.simona.model.participant2.ParticipantModel.{ + ActivePowerOperatingPoint, + DateTimeData, + FixedState, +} +import squants.{Dimensionless, Each, Energy, Power, Quantity} +import squants.energy.KilowattHours +import squants.time.Minutes + +import java.time.ZonedDateTime +import java.time.temporal.ChronoUnit + +trait LoadModelTestHelper { + + protected def calculateEnergyDiffForYear( + model: LoadModel[DateTimeData], + simulationStartDate: ZonedDateTime, + expectedEnergy: Energy, + ): Dimensionless = { + val duration = Minutes(15d) + + val avgEnergy = calculatePowerForYear( + model, + simulationStartDate, + ).foldLeft(KilowattHours(0)) { case (energySum, power) => + energySum + (power * duration) + } + + getRelativeDifference( + avgEnergy, + expectedEnergy, + ) + } + + protected def calculatePowerForYear( + model: LoadModel[DateTimeData], + simulationStartDate: ZonedDateTime, + ): Iterable[Power] = { + val quarterHoursInYear = 365L * 96L + + (0L until quarterHoursInYear) + .map { quarterHour => + val tick = quarterHour * 15 * 60 + val relevantData = DateTimeData( + tick, + simulationStartDate.plus(quarterHour * 15, ChronoUnit.MINUTES), + ) + + model + .determineOperatingPoint( + FixedState(tick), + relevantData, + ) match { + case (ActivePowerOperatingPoint(p), _) => + p + } + } + } + + protected def getRelativeDifference[Q <: Quantity[Q]]( + actualResult: Q, + expectedResult: Q, + ): Dimensionless = + Each((expectedResult - actualResult).abs / expectedResult) + +} diff --git a/src/test/scala/edu/ie3/simona/model/participant2/load/ProfileLoadModelSpec.scala b/src/test/scala/edu/ie3/simona/model/participant2/load/ProfileLoadModelSpec.scala index 712a262581..82fcc04d0a 100644 --- a/src/test/scala/edu/ie3/simona/model/participant2/load/ProfileLoadModelSpec.scala +++ b/src/test/scala/edu/ie3/simona/model/participant2/load/ProfileLoadModelSpec.scala @@ -15,17 +15,30 @@ import edu.ie3.datamodel.models.voltagelevels.GermanVoltageLevelUtils import edu.ie3.simona.config.SimonaConfig.LoadRuntimeConfig import edu.ie3.simona.test.common.UnitSpec import edu.ie3.simona.test.matchers.DoubleMatchers +import edu.ie3.util.TimeUtil import edu.ie3.util.quantities.PowerSystemUnits -import edu.ie3.util.scala.quantities.{ApparentPower, Voltamperes} +import edu.ie3.util.scala.quantities.{ + ApparentPower, + Kilovoltamperes, + Voltamperes, +} +import squants.Percent +import squants.energy.{KilowattHours, Power, Watts} import tech.units.indriya.quantity.Quantities import java.util.UUID -class ProfileLoadModelSpec extends UnitSpec with DoubleMatchers { +class ProfileLoadModelSpec + extends UnitSpec + with DoubleMatchers + with LoadModelTestHelper { private implicit val powerTolerance: ApparentPower = Voltamperes(1e-2) private implicit val doubleTolerance: Double = 1e-6 + private val simulationStartDate = + TimeUtil.withDefaults.toZonedDateTime("2022-01-01T00:00:00Z") + "A profile load model" should { val loadInput = new LoadInput( @@ -126,5 +139,79 @@ class ProfileLoadModelSpec extends UnitSpec with DoubleMatchers { } + "reach the targeted annual energy consumption" in { + forAll( + Table("profile", H0, L0, G0) + ) { profile => + val input = loadInput.copy().loadprofile(profile).build() + + val config = LoadRuntimeConfig( + calculateMissingReactivePowerWithModel = false, + scaling = 1.0, + uuids = List.empty, + modelBehaviour = "profile", + reference = "energy", + ) + + val targetEnergyConsumption = KilowattHours( + loadInput + .geteConsAnnual() + .to(PowerSystemUnits.KILOWATTHOUR) + .getValue + .doubleValue + ) + + val model = ProfileLoadModel(input, config) + + /* Test against a permissible deviation of 2 %. As per official documentation of the bdew load profiles + * [https://www.bdew.de/media/documents/2000131_Anwendung-repraesentativen_Lastprofile-Step-by-step.pdf], 1.5 % + * are officially permissible. But, as we currently do not take (bank) holidays into account, we cannot reach + * this accuracy. */ + + calculateEnergyDiffForYear( + model, + simulationStartDate, + targetEnergyConsumption, + ) should be < Percent(2) + } + } + + "approximately reach the maximum power in a simulated year" in { + forAll( + Table("profile", H0, L0, G0) + ) { profile => + val input = loadInput.copy().loadprofile(profile).build() + + val config = LoadRuntimeConfig( + calculateMissingReactivePowerWithModel = false, + scaling = 1.0, + uuids = List.empty, + modelBehaviour = "profile", + reference = "power", + ) + + val model = ProfileLoadModel(input, config) + + val targetMaximumPower = Kilovoltamperes( + input + .getsRated() + .to(PowerSystemUnits.KILOVOLTAMPERE) + .getValue + .doubleValue + ).toActivePower(input.getCosPhiRated) + + val maximumPower = calculatePowerForYear( + model, + simulationStartDate, + ).maxOption.value + + // the maximum value depends on the year of the simulation, + // since the maximum value for h0 will be reached on Saturdays in the winter + // and since the dynamization function reaches its maximum on day 366 (leap year) + implicit val tolerance: Power = Watts(1) + maximumPower should approximate(targetMaximumPower) + } + } + } } From 5e6044721f6f17662724544291c189d1120114f9 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Wed, 11 Dec 2024 14:57:18 +0100 Subject: [PATCH 76/77] Transferring aggregation tests for random model Signed-off-by: Sebastian Peter --- .../load/LoadModelTestHelper.scala | 4 + .../load/ProfileLoadModelSpec.scala | 2 +- .../load/RandomLoadModelSpec.scala | 83 +++++++++++++++++-- 3 files changed, 80 insertions(+), 9 deletions(-) diff --git a/src/test/scala/edu/ie3/simona/model/participant2/load/LoadModelTestHelper.scala b/src/test/scala/edu/ie3/simona/model/participant2/load/LoadModelTestHelper.scala index 11d72e3aef..8761973aa5 100644 --- a/src/test/scala/edu/ie3/simona/model/participant2/load/LoadModelTestHelper.scala +++ b/src/test/scala/edu/ie3/simona/model/participant2/load/LoadModelTestHelper.scala @@ -71,4 +71,8 @@ trait LoadModelTestHelper { ): Dimensionless = Each((expectedResult - actualResult).abs / expectedResult) + protected def get95Quantile[V](sortedArray: Array[V]): V = sortedArray( + (sortedArray.length * 0.95).toInt + ) + } diff --git a/src/test/scala/edu/ie3/simona/model/participant2/load/ProfileLoadModelSpec.scala b/src/test/scala/edu/ie3/simona/model/participant2/load/ProfileLoadModelSpec.scala index 82fcc04d0a..faa41a67a9 100644 --- a/src/test/scala/edu/ie3/simona/model/participant2/load/ProfileLoadModelSpec.scala +++ b/src/test/scala/edu/ie3/simona/model/participant2/load/ProfileLoadModelSpec.scala @@ -139,7 +139,7 @@ class ProfileLoadModelSpec } - "reach the targeted annual energy consumption" in { + "reach the targeted annual energy consumption in a simulated year" in { forAll( Table("profile", H0, L0, G0) ) { profile => diff --git a/src/test/scala/edu/ie3/simona/model/participant2/load/RandomLoadModelSpec.scala b/src/test/scala/edu/ie3/simona/model/participant2/load/RandomLoadModelSpec.scala index fb12b6de31..b52227152d 100644 --- a/src/test/scala/edu/ie3/simona/model/participant2/load/RandomLoadModelSpec.scala +++ b/src/test/scala/edu/ie3/simona/model/participant2/load/RandomLoadModelSpec.scala @@ -18,16 +18,25 @@ import edu.ie3.simona.model.participant.load.random.RandomLoadParameters import edu.ie3.simona.test.common.UnitSpec import edu.ie3.util.TimeUtil import edu.ie3.util.quantities.PowerSystemUnits -import edu.ie3.util.scala.quantities.{ApparentPower, Voltamperes} +import edu.ie3.util.scala.quantities.{ + ApparentPower, + Kilovoltamperes, + Voltamperes, +} +import squants.Percent +import squants.energy.KilowattHours import tech.units.indriya.quantity.Quantities import java.util.UUID -class RandomLoadModelSpec extends UnitSpec { +class RandomLoadModelSpec extends UnitSpec with LoadModelTestHelper { implicit val powerTolerance: ApparentPower = Voltamperes(1e-2) private implicit val doubleTolerance: Double = 1e-6 + private val simulationStartDate = + TimeUtil.withDefaults.toZonedDateTime("2019-01-01T00:00:00Z") + "A random load model" should { val loadInput = new LoadInput( @@ -158,11 +167,69 @@ class RandomLoadModelSpec extends UnitSpec { secondHit shouldBe firstHit } - } -} + "reach the targeted annual energy consumption in a simulated year" in { + val config = LoadRuntimeConfig( + calculateMissingReactivePowerWithModel = false, + scaling = 1.0, + uuids = List.empty, + modelBehaviour = "random", + reference = "energy", + ) + + val model = RandomLoadModel( + loadInput, + config, + ) + + val targetEnergyConsumption = KilowattHours( + loadInput + .geteConsAnnual() + .to(PowerSystemUnits.KILOWATTHOUR) + .getValue + .doubleValue + ) -object RandomLoadModelSpec { - def get95Quantile[V](sortedArray: Array[V]): V = sortedArray( - (sortedArray.length * 0.95).toInt - ) + calculateEnergyDiffForYear( + model, + simulationStartDate, + targetEnergyConsumption, + ) should be < Percent(1d) + } + + "approximately reach the maximum power in a simulated year" in { + val config = LoadRuntimeConfig( + calculateMissingReactivePowerWithModel = false, + scaling = 1.0, + uuids = List.empty, + modelBehaviour = "random", + reference = "power", + ) + + val model = RandomLoadModel( + loadInput, + config, + ) + + val targetMaximumPower = Kilovoltamperes( + loadInput + .getsRated() + .to(PowerSystemUnits.KILOVOLTAMPERE) + .getValue + .doubleValue + ).toActivePower(loadInput.getCosPhiRated) + + val powers = calculatePowerForYear( + model, + simulationStartDate, + ).toIndexedSeq.sorted.toArray + + val quantile95 = get95Quantile(powers) + + getRelativeDifference( + quantile95, + targetMaximumPower, + ) should be < Percent(2d) + } + + } } From ffd0fc2ce2e1165c4b3b1fa0a65f90321933c986 Mon Sep 17 00:00:00 2001 From: Sebastian Peter Date: Wed, 11 Dec 2024 18:21:40 +0100 Subject: [PATCH 77/77] Test for FixedFeedInModel Signed-off-by: Sebastian Peter --- .../participant2/FixedFeedInModelSpec.scala | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 src/test/scala/edu/ie3/simona/model/participant2/FixedFeedInModelSpec.scala diff --git a/src/test/scala/edu/ie3/simona/model/participant2/FixedFeedInModelSpec.scala b/src/test/scala/edu/ie3/simona/model/participant2/FixedFeedInModelSpec.scala new file mode 100644 index 0000000000..efa7ad5605 --- /dev/null +++ b/src/test/scala/edu/ie3/simona/model/participant2/FixedFeedInModelSpec.scala @@ -0,0 +1,60 @@ +/* + * © 2020. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.model.participant2 + +import edu.ie3.simona.model.participant.control.QControl +import edu.ie3.simona.model.participant2.ParticipantModel.{ + FixedRelevantData, + FixedState, +} +import edu.ie3.simona.test.common.UnitSpec +import edu.ie3.simona.test.common.input.FixedFeedInputTestData +import edu.ie3.util.quantities.PowerSystemUnits +import edu.ie3.util.quantities.PowerSystemUnits.MEGAVOLTAMPERE +import edu.ie3.util.scala.quantities.{Kilovoltamperes, Megavoltamperes} + +class FixedFeedInModelSpec extends UnitSpec with FixedFeedInputTestData { + + "The fixed feed in model" should { + + "build a correct FixedFeedModel from valid input" in { + + val model = FixedFeedInModel(fixedFeedInput) + + model.uuid shouldBe fixedFeedInput.getUuid + model.sRated shouldBe Megavoltamperes( + fixedFeedInput.getsRated().to(MEGAVOLTAMPERE).getValue.doubleValue + ) + model.cosPhiRated shouldBe fixedFeedInput.getCosPhiRated + model.qControl shouldBe QControl(fixedFeedInput.getqCharacteristics) + + } + + "return approximately correct power calculations" in { + + val model = FixedFeedInModel(fixedFeedInput) + + val expectedPower = Kilovoltamperes( + fixedFeedInput + .getsRated() + .to(PowerSystemUnits.KILOWATT) + .getValue + .doubleValue * -1 + ).toActivePower(fixedFeedInput.getCosPhiRated) + + val (operatingPoint, nextTick) = model.determineOperatingPoint( + FixedState(0), + FixedRelevantData, + ) + operatingPoint.activePower shouldBe expectedPower + nextTick shouldBe None + + } + + } + +}