From 699095b15a6e9bcee66f97054ff32f3de705749a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20H=C3=B6rl?= Date: Sat, 7 Dec 2024 12:01:22 +0100 Subject: [PATCH] feat(ev): strategic electric vehicle charging --- contribs/ev/README.md | 124 +- contribs/ev/docs/sevc/durations.png | Bin 0 -> 63891 bytes contribs/ev/docs/sevc/revenues.png | Bin 0 -> 63144 bytes contribs/ev/docs/sevc/score_distribution.png | Bin 0 -> 67173 bytes contribs/ev/docs/sevc/scores.png | Bin 0 -> 50222 bytes .../reservation/ChargerReservationModule.java | 17 +- .../CriticalAlternativeProvider.java | 158 ++ .../org/matsim/contrib/ev/strategic/README.md | 147 ++ .../StrategicChargingAlternativeProvider.java | 213 +++ .../StrategicChargingConfigGroup.java | 150 ++ .../ev/strategic/StrategicChargingModule.java | 216 +++ .../StrategicChargingQSimModule.java | 69 + .../StrategicChargingSlotProvider.java | 93 + .../ev/strategic/StrategicChargingUtils.java | 316 ++++ .../ev/strategic/access/AnyChargerAccess.java | 22 + .../access/AttributeBasedChargerAccess.java | 41 + .../ev/strategic/access/ChargerAccess.java | 16 + .../access/SubscriptionRegistry.java | 109 ++ .../analysis/ChargerTypeAnalysisListener.java | 220 +++ .../analysis/ChargingPlanScoringListener.java | 96 + .../AttributeBasedChargingCostCalculator.java | 103 + ...AttributeBasedChargingCostsParameters.java | 17 + .../costs/ChargingCostCalculator.java | 15 + .../strategic/costs/ChargingCostModule.java | 51 + .../costs/ChargingCostsParameters.java | 5 + .../costs/DefaultChargingCostCalculator.java | 28 + .../costs/DefaultChargingCostsParameters.java | 32 + .../costs/NoChargingCostCalculator.java | 17 + .../TariffBasedChargingCostCalculator.java | 124 ++ .../TariffBasedChargingCostsParameters.java | 77 + .../AbstractChargerProviderModule.java | 25 + .../infrastructure/ChargerProvider.java | 47 + .../CompositeChargerProvider.java | 34 + .../DefaultChargerProvidersModule.java | 53 + .../FacilityChargerProvider.java | 130 ++ .../infrastructure/PersonChargerProvider.java | 123 ++ .../infrastructure/PublicChargerProvider.java | 92 + .../ev/strategic/plan/ChargingPlan.java | 52 + .../strategic/plan/ChargingPlanActivity.java | 128 ++ .../ev/strategic/plan/ChargingPlans.java | 121 ++ .../plan/ChargingPlansConverter.java | 35 + .../StrategicChargingReplanningAlgorithm.java | 62 + .../StrategicChargingReplanningModule.java | 27 + .../StrategicChargingReplanningStrategy.java | 34 + .../innovator/ChargingPlanInnovator.java | 17 + .../innovator/EmptyChargingPlanInnovator.java | 18 + .../RandomChargingPlanInnovator.java | 170 ++ .../selector/BestChargingPlanSelector.java | 20 + .../selector/ChargingPlanSelector.java | 16 + .../ExponentialChargingPlanSelector.java | 51 + .../selector/RandomChargingPlanSelector.java | 29 + .../scoring/ChargingPlanScoring.java | 555 ++++++ .../ChargingPlanScoringParameters.java | 55 + .../ev/strategic/scoring/ScoringTracker.java | 71 + .../StrategicChargingScoringFunction.java | 57 + .../ev/withinday/ChargingAlternative.java | 22 + .../ChargingAlternativeProvider.java | 42 + .../ev/withinday/ChargingScheduler.java | 573 ++++++ .../contrib/ev/withinday/ChargingSlot.java | 32 + .../ev/withinday/ChargingSlotFinder.java | 196 ++ .../ev/withinday/ChargingSlotProvider.java | 24 + .../withinday/WithinDayChargingStrategy.java | 80 + .../ev/withinday/WithinDayEvConfigGroup.java | 60 + .../ev/withinday/WithinDayEvEngine.java | 938 ++++++++++ .../ev/withinday/WithinDayEvModule.java | 37 + .../ev/withinday/WithinDayEvQSimModule.java | 75 + .../ev/withinday/WithinDayEvUtils.java | 49 + .../WithinDayChargingAnalysisHandler.java | 312 ++++ .../WithinDayChargingAnalysisListener.java | 139 ++ .../events/AbortChargingAttemptEvent.java | 43 + .../AbortChargingAttemptEventHandler.java | 7 + .../events/AbortChargingProcessEvent.java | 43 + .../AbortChargingProcessEventHandler.java | 7 + .../events/FinishChargingAttemptEvent.java | 43 + .../FinishChargingAttemptEventHandler.java | 7 + .../events/FinishChargingProcessEvent.java | 43 + .../FinishChargingProcessEventHandler.java | 7 + .../events/StartChargingAttemptEvent.java | 102 + .../StartChargingAttemptEventHandler.java | 7 + .../events/StartChargingProcessEvent.java | 58 + .../StartChargingProcessEventHandler.java | 7 + .../events/UpdateChargingAttemptEvent.java | 81 + .../UpdateChargingAttemptEventHandler.java | 7 + .../strategic/ChargingPlansConverterTest.java | 34 + .../ev/strategic/StrategicChargingTest.java | 162 ++ .../strategic/utils/TestScenarioBuilder.java | 699 +++++++ .../contrib/ev/withinday/WithinDayEvTest.java | 1649 +++++++++++++++++ .../utils/ActivityLegChangeProvider.java | 106 ++ .../ChangeDurationAlternativeProvider.java | 37 + .../utils/FirstActivitySlotProvider.java | 31 + .../withinday/utils/FirstLegSlotProvider.java | 46 + .../utils/LastActivitySlotProvider.java | 31 + .../utils/OrderedAlternativeProvider.java | 61 + .../utils/ReservationAlternativeProvider.java | 35 + .../utils/SpontaneousChargingProvider.java | 73 + .../SwitchChargerAlternativeProvider.java | 45 + ...itchEnrouteChargerAlternativeProvider.java | 44 + .../withinday/utils/WholeDaySlotProvider.java | 34 + .../utils/WorkActivitySlotProvider.java | 53 + 99 files changed, 10675 insertions(+), 4 deletions(-) create mode 100644 contribs/ev/docs/sevc/durations.png create mode 100644 contribs/ev/docs/sevc/revenues.png create mode 100644 contribs/ev/docs/sevc/score_distribution.png create mode 100644 contribs/ev/docs/sevc/scores.png create mode 100644 contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/CriticalAlternativeProvider.java create mode 100644 contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/README.md create mode 100644 contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/StrategicChargingAlternativeProvider.java create mode 100644 contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/StrategicChargingConfigGroup.java create mode 100644 contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/StrategicChargingModule.java create mode 100644 contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/StrategicChargingQSimModule.java create mode 100644 contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/StrategicChargingSlotProvider.java create mode 100644 contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/StrategicChargingUtils.java create mode 100644 contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/access/AnyChargerAccess.java create mode 100644 contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/access/AttributeBasedChargerAccess.java create mode 100644 contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/access/ChargerAccess.java create mode 100644 contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/access/SubscriptionRegistry.java create mode 100644 contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/analysis/ChargerTypeAnalysisListener.java create mode 100644 contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/analysis/ChargingPlanScoringListener.java create mode 100644 contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/costs/AttributeBasedChargingCostCalculator.java create mode 100644 contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/costs/AttributeBasedChargingCostsParameters.java create mode 100644 contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/costs/ChargingCostCalculator.java create mode 100644 contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/costs/ChargingCostModule.java create mode 100644 contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/costs/ChargingCostsParameters.java create mode 100644 contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/costs/DefaultChargingCostCalculator.java create mode 100644 contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/costs/DefaultChargingCostsParameters.java create mode 100644 contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/costs/NoChargingCostCalculator.java create mode 100644 contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/costs/TariffBasedChargingCostCalculator.java create mode 100644 contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/costs/TariffBasedChargingCostsParameters.java create mode 100644 contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/infrastructure/AbstractChargerProviderModule.java create mode 100644 contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/infrastructure/ChargerProvider.java create mode 100644 contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/infrastructure/CompositeChargerProvider.java create mode 100644 contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/infrastructure/DefaultChargerProvidersModule.java create mode 100644 contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/infrastructure/FacilityChargerProvider.java create mode 100644 contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/infrastructure/PersonChargerProvider.java create mode 100644 contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/infrastructure/PublicChargerProvider.java create mode 100644 contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/plan/ChargingPlan.java create mode 100644 contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/plan/ChargingPlanActivity.java create mode 100644 contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/plan/ChargingPlans.java create mode 100644 contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/plan/ChargingPlansConverter.java create mode 100644 contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/replanning/StrategicChargingReplanningAlgorithm.java create mode 100644 contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/replanning/StrategicChargingReplanningModule.java create mode 100644 contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/replanning/StrategicChargingReplanningStrategy.java create mode 100644 contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/replanning/innovator/ChargingPlanInnovator.java create mode 100644 contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/replanning/innovator/EmptyChargingPlanInnovator.java create mode 100644 contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/replanning/innovator/RandomChargingPlanInnovator.java create mode 100644 contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/replanning/selector/BestChargingPlanSelector.java create mode 100644 contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/replanning/selector/ChargingPlanSelector.java create mode 100644 contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/replanning/selector/ExponentialChargingPlanSelector.java create mode 100644 contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/replanning/selector/RandomChargingPlanSelector.java create mode 100644 contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/scoring/ChargingPlanScoring.java create mode 100644 contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/scoring/ChargingPlanScoringParameters.java create mode 100644 contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/scoring/ScoringTracker.java create mode 100644 contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/scoring/StrategicChargingScoringFunction.java create mode 100644 contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/ChargingAlternative.java create mode 100644 contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/ChargingAlternativeProvider.java create mode 100644 contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/ChargingScheduler.java create mode 100644 contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/ChargingSlot.java create mode 100644 contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/ChargingSlotFinder.java create mode 100644 contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/ChargingSlotProvider.java create mode 100644 contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/WithinDayChargingStrategy.java create mode 100644 contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/WithinDayEvConfigGroup.java create mode 100644 contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/WithinDayEvEngine.java create mode 100644 contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/WithinDayEvModule.java create mode 100644 contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/WithinDayEvQSimModule.java create mode 100644 contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/WithinDayEvUtils.java create mode 100644 contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/analysis/WithinDayChargingAnalysisHandler.java create mode 100644 contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/analysis/WithinDayChargingAnalysisListener.java create mode 100644 contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/events/AbortChargingAttemptEvent.java create mode 100644 contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/events/AbortChargingAttemptEventHandler.java create mode 100644 contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/events/AbortChargingProcessEvent.java create mode 100644 contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/events/AbortChargingProcessEventHandler.java create mode 100644 contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/events/FinishChargingAttemptEvent.java create mode 100644 contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/events/FinishChargingAttemptEventHandler.java create mode 100644 contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/events/FinishChargingProcessEvent.java create mode 100644 contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/events/FinishChargingProcessEventHandler.java create mode 100644 contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/events/StartChargingAttemptEvent.java create mode 100644 contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/events/StartChargingAttemptEventHandler.java create mode 100644 contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/events/StartChargingProcessEvent.java create mode 100644 contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/events/StartChargingProcessEventHandler.java create mode 100644 contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/events/UpdateChargingAttemptEvent.java create mode 100644 contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/events/UpdateChargingAttemptEventHandler.java create mode 100644 contribs/ev/src/test/java/org/matsim/contrib/ev/strategic/ChargingPlansConverterTest.java create mode 100644 contribs/ev/src/test/java/org/matsim/contrib/ev/strategic/StrategicChargingTest.java create mode 100644 contribs/ev/src/test/java/org/matsim/contrib/ev/strategic/utils/TestScenarioBuilder.java create mode 100644 contribs/ev/src/test/java/org/matsim/contrib/ev/withinday/WithinDayEvTest.java create mode 100644 contribs/ev/src/test/java/org/matsim/contrib/ev/withinday/utils/ActivityLegChangeProvider.java create mode 100644 contribs/ev/src/test/java/org/matsim/contrib/ev/withinday/utils/ChangeDurationAlternativeProvider.java create mode 100644 contribs/ev/src/test/java/org/matsim/contrib/ev/withinday/utils/FirstActivitySlotProvider.java create mode 100644 contribs/ev/src/test/java/org/matsim/contrib/ev/withinday/utils/FirstLegSlotProvider.java create mode 100644 contribs/ev/src/test/java/org/matsim/contrib/ev/withinday/utils/LastActivitySlotProvider.java create mode 100644 contribs/ev/src/test/java/org/matsim/contrib/ev/withinday/utils/OrderedAlternativeProvider.java create mode 100644 contribs/ev/src/test/java/org/matsim/contrib/ev/withinday/utils/ReservationAlternativeProvider.java create mode 100644 contribs/ev/src/test/java/org/matsim/contrib/ev/withinday/utils/SpontaneousChargingProvider.java create mode 100644 contribs/ev/src/test/java/org/matsim/contrib/ev/withinday/utils/SwitchChargerAlternativeProvider.java create mode 100644 contribs/ev/src/test/java/org/matsim/contrib/ev/withinday/utils/SwitchEnrouteChargerAlternativeProvider.java create mode 100644 contribs/ev/src/test/java/org/matsim/contrib/ev/withinday/utils/WholeDaySlotProvider.java create mode 100644 contribs/ev/src/test/java/org/matsim/contrib/ev/withinday/utils/WorkActivitySlotProvider.java diff --git a/contribs/ev/README.md b/contribs/ev/README.md index f17ff0fee51..66768013583 100644 --- a/contribs/ev/README.md +++ b/contribs/ev/README.md @@ -1,3 +1,123 @@ +# EV: Electric Vehicle functionality for MATSim -# EV -Electric Vehicle functionality for MATSim +## Strategic electric vehicle charging (SEVC) + +This PR introduces two major packages into the `ev` contrib: + +- *Within-day electric vehicle charging (WEVC)* provides a comprehensive framework for letting MATSim agents charge during the simulation. On the one hand, it allows users to implement planned charging activities into the schedules at the beginning of the day, on the other hand, it allows to define a custom logic regarding how agents change their plans during the day, receact to occupied chargers, etc. The package is meant as a backing package that required substantial code-based configuration. + +- *Strategic electric vehicle charging (SEVC)* provides high-level functionality for simulating rich charging behavior of agents. It adds a second replanning layer (with dedicated *charging plans*) on top of MATSim's standard replanning, which optimizes charging decisions (when, where, ...) in an evolutionary way. Charging plans are scored like regular MATSim plans, reacting to zero SoC events, minimum reached SoCs, charging costs, and other components. The package provides rich functionality to analyze in detail the scoring components, define access rules (subscriptions) between persons and chargers, and detailed tariffs for chargers. + +### WEVC: Within-day electric vehicle charging + +The core of WEVC is the `WithinDayEvEngine` which manages the charging behavior of agents. The system is based on the concept of *charging slots*. There are *activity-based* charging slots and *leg-based* charging slots. An *activity-based* charging slot is defined by a sequence of (main) activities during which the agent intends to charge. The engine will make sure that the agent drives to the selected charger before starting the first main activity in the sequence, then walks to the main activity, performs the sequence of (or only one) main activities, walks back to the charger, and picks up the vehicle again. The *leg-based* charging slots divert the initially planned leg such that a period of charging is inserted during a specified period. + +Internally, *WEVC* works by rewriting the within-day agent plan at specific moments during the simulation. At start-up, all planned charging activities are inserted by the means of a *plug* activities to which the engine responds later on. During such an activity, the engine makes the agent try plug his vehicle. If this succeeds the plan is further rewritten to represent the logic above. For an activity-based charging slot, the return journey to the charger is inserted after the last main activity of the sequence, including an *unplug* activity. During that activity, the agent unplugs the vehicle from the charger and then continues the journey. For leg-based charging slots, the agent enters a *wait* activity after the *plug* activity for a predefined duration and then performs the *unplug*, followed by another diversion to the next main activity. + +On top of the planned charging activities, *WEVC* allows to define online behaviors. They define what happens in several cases: + +- If a certain queue time is passed at a charger without success, the agent will attempt to charge elsewhere. The engien then makes sure that the *plug* activity is ended, and a new one at dynamically chosen charger is inserted. +- If an agent goes on a leg that leads to / contains a charging slot, an online decision can be made. This is useful, for instance, for simulation behaviors in which the agent checks charger availability on a map and updates his initial choice of charger. +- Optionally (since more time consuming), *spntaneous charging* can be enabled which will allow an agent to dynamically decide when starting any *vehicle leg* whether the agent wants to charge along that leg or not. + +Code-wise *WEVC* is based on two main interfaces that need to be implemented: + +- The `ChargingSlotProvider` is used to obtain a list of planned charging slots at the beginning of a simulated day. A `ChargingSlot` consists of a *start activity*, an *end activity*, and a *charger* for activity-based charging, and a *leg*, a *charger*, and a *duration* for leg-based charging. + +- The `ChargingAlternativeProvider` is used for any dynamic decision. In the default case, it provides a new *charger* given a `ChargingSlot`. When updating a leg-based charging slot, also a new duration can be given. For every decision, the trace of all previously visited alternatives in the current search process is given to avoid visiting two chargers twice. For activity-based charging slots *enroute* decisions can be made when approaching the first main activity. In that case, providing a *duration* can also transform an initially activity-based slot into a leg-based charging activity. + +**Events** + +When following the life cycle of the charging agents, they generate `StartChargingProcessEvent`s when they start a search process. Inside each charging process, multiple `StartChargingAttemptEvent`s can be observed with each *attempt* representing one try to make use of a charger. An *attempt* can either fail which is indicated by a `AbortChargingAttemptEvent`. At that point, either a new decision is made using the `ChargingAlternativeProvider` and a new *attempt* will start, or a `AbortChargingProcessEvent` is raised. In that case, *WEVC* can be configured to let the agent get *stuck* or simply follow the daily plan. In case an online decision is made when approaching a charging slot before the first *attempt*, an `UpdateChargingAttemptEvent` is generate to document the change. A standard event sequence (including the standard `ev` events) is, hence: + +``` +StartChargingProcessEvent > StartChargingAttemptEvent > ChargingStartEvent > ChargingEndEvent > EndChargingAttemptEvent > EndChargingProcessEvent +``` + +**Analysis** + +*WEVC* provides some standard *CSV* analysis output that tracks all the charging processes and attempts in detail (start time, queue time, plug time, charged energy, ...). Furthermore, it is thoroughly tested with ~25 unit tests covering the different scheduling situations that can arise. + +**Usage** + +To use *WEVC*, the `WithinDayEvModule` needs to be added to the controler. It must be configured using the `WithinDayEvConfigGroup`, which allows, for instance, to choose the mode that is covered by the charging logic. Furthermore, every agent that is supposed to take part in witin-day event charging needs be activated by setting the `wevc:active` attribute to `true`. Some convenience functions are provided in `WithinDayEvUtls` that allow to easily set up a scenario, in particular `WithinDayEvUtils.activate(Person)`. + +When implementing `ChargingSlotProvider` and `ChargingAlternativeProvider`, it is advise to make use of `ChargingSlotFinder`, which is a convenience class that allows to identify all viable activity-based and leg-based charging slots along an agent's plan. A viable activity-based charging slot is any sequence of main activities with a leading and following vehicular leg (or start/end of day) that does not contain an intermediate vehicular leg. A viable leg-based charging slot is any vehicular leg. Note that both types may conflict, so `ChargingSlotFinder` also provides the functionaltiy to remove items of either type from a list of slot candidates, given on a set of already selected candidates from the other type. Various examples are given in in the unit tests. An example will be provided in *matsim-code-examples*. + +### SEVC: Strategic electric vehicle charging + +The purpose of *SEVC* is to provide an easy-to-use and feature-rich package for simulating the charging behavior of the population. The focus lies on different charger types and on exploring how and when persons would use those chargers. + +*SEVC* is based on the concept of *charging plans*. Each regular MATSim plan of a *WEVC*-enabled agent contains a memory of such *charging plans*. As in regular MATSim replanning, charging plans can be selected from memory and innovated. For instance, the standard innovation approach in *SEVC* is to randomly select charging slots along the plan of an agent using the `ChargingSlotFinder` and then finding viable chargers (see below). The selection part is handled, as in regular MATSim, on the basis of a *charging score* that is applied to each *charging plan*. The standard selection strategy for the charging plans is a logit-style approach like it is mostly common in regular MATSim. Technically, the selection and innovation of charging plans is triggered by executing the `StrategicChargingReplanningStrategy` that is implemented as a standard replanning strategy of MATSim. + +A charging plan describes the slots that, at some point in time, were used by the agent. For instance, activity-based charging slots are described by indices over the main activities. The `ChargingSlotProvider` of *SEVC* then examines the saved charging slots of the selected charging plan and compares them to the viable slots of the current main plan (in which modes may have changed due to mode choice and other strategies meanwhile). So if a recorded charging slot is still valid for the current plan configuration, it is implemented in the beginning of the day. + +During the simulation, everything that has to do with charging is scored separately from the regular scoring to establish a *charging score* for the selected *charging plan*. This scroring (see below for the individual components) penalizes reaching zero SoC, reaching a minimal SoC, and other events. + +Furthermore, *SEVC* defines a standardized `ChargingAlternativeProvider` which provides alternative chargers if the planned one is already occupied. In particular, different overall planning scenarios can be defined via configuration, which (1) let agents go and queue at charger naively with a maximum queue time, (2) check occupancy in an anticipatory way, or (3) resere the initial (or an alternative) charger when approaching the planned charging activity. + +**Charger selection** + +When constructing new *charging plan*s, the default approach randomly selects viable charging slot candidates (activity-based and leg-based). For each candidate, a charger needs to be selected. This task is handled by a list of `ChargerProvider`s in *SEVC*. All providers try to find chargers that are close enough to the planned activity/leg and fulfill certain other criteria. For performance reasons, it does not make sense to always test all chargers in the vicinity of every agent. Additionally, it may make sense to only propose specific chargers in certain situations. By default, chargers are provided simultaneously by three strategies in *SEVC*: + +- *Person* chargers are assigned to a specific list of persons. This means, that they are only proposed when a charger for a specific person (by ID) is searched and if the charger is close enough to the search location. Technically, relevant chargers should have the `sevc:persons` attribute, which can also be set using `StrategicChargingUtils.setChargerPersons()`. +- *Facility* chargers are assigned to a spefici list of facilities. Whenever a charger is searched, the provider will examine the facility of the start activity of the slot and check whether there are chargers that have been assigned to that facility. This allows, for instance, to define chargers that belong to a specific workplace or supermarket. The relevant chargers should have the `sevc:facilities` attribute, which can also be set using `StrategicChargingUtils.setChargerFacilities`. +- *Public* chargers are the most general chargers that are considered by *SEVC*. They are queried using a spatial index based on the search location. They are identified by having their charger attribute `sevc:public` set to `true`. + +**Charger access** + +*SEVC* provides flexible interfaces to control in detail the access of individual persons to chargers. The default implementation makes use of *subscriptions*. Each charger can have none, one or multiple required subscriptions. Analogously, persons can have a list of available subscriptions. While chargers without required subscriptions are avaialble to all agents, only agents that have the right subscription are allowed at those chargers that required them. + +Subscriptions are managed per-person and per-charger using the `sevc:subscriptions` attributes, which can also conveniently be set using the respective helper methods in `StrategicChargingUtils`. + +**Charging costs and tariffs** + +*SEVC* provides various ways of defining charging costs that can be set up using configuration. Charging costs can be set (1) globally by defining per-kWh, per-minute, per-use costs, (2) in an attribute-based way per charger, or (3) using a detailed tariff system. + +When working with the tariff-based cost calculator, the framework user defines a list of named tariffs with specific cost structures (kWh, minutes, ...) in configuration. Each charger then needs to have a list of tariffs defined through their `sevc:tariffs` attribute. Each tariff can also be restricted to a list of subscriptions. Whenever an agent uses a charger, the cost calculator then identifies the tariffs that are available to that person and selects the best. + +**Scoring** + +The charging score tracks various events and observations during a simulation. Via configuration, their individual contribution to the overall charging score can be configured and a detailed analysis covering all scoring components is generated in CSV format. The components are the following: + +- Reaching zero SoC +- Reaching a minimum SoC defined as a per-agent attribute +- Observing a SoC at the end of the day that is below a per-person minimum end-of-day SoC +- Charging costs +- Charging duration +- Wait/queue duration +- Detour distance and detour travel time compared to a plan without charging +- Experiencing a failed charging attempt +- Experiencing a failed charging process + +Other dimensions could be added in the future. + +**Analysis** + +*SEVC* provides information on the replanning process by writing out the maximum, minimum, mean and selected charging plan scores over the population as well as a detailed listing of all scoring components in each iteration. + +Furthermore, SEVC provides various high-level indicators by default. Each charger can be assigned one or more analysis types through their `sevc:analysisTypes` attribute. For each analysis type, the consumed energy, the total charging duration, the number of users, and other indicators are written out per iteration. This way, one can quickly set up and obtain relevant scenario indicators. + +**Usage** + +To use *SEVC*, *WEVC* needs to be installed as well as the `StrategicChargingModule`. All configuration is contained in the `StrategicChargingConfigGroup`. It contains individual parameter sets for `scoring` and for `costs` along a range of different configuration options for which sensible default values have been chosen. A good entry point for preparing sceanrio data is `StrategicChargingUtils` which covers the main values that need to be set of persons and chargers. + +Note that *SEVC* can be used in two ways. The config option `chargingScoreWeight` allows to feed back the *charging score* into the regular score of the MATSil plan, weighted by the defined value. This way, penalties that are observed with respect to charging can influence the general replanning behavior of MATSim, including mode choice. It is, hence, perfectly possible to set up a simulation in which agents switch away from mode because charging is inconvenient or too expenseive. + +On the other hand, MATSim can be configured such that only the *SEVC* replanning strategy (and thus only charging plan selection and innovation) is used. This means that in an already stabilized MATSim simulation, one can explore different deployment or pricing scenarios for electric charging infrastructure and observe how the (activated) car users respond to those scenarios. Such simulations are called *standalone SEVC* simulations and can be set up conventiently using `StrategicChargingUtils.configureStandalone`. + +**Example** + +The following results are taken from a standalone SEVC simulation. The first plot shows the progression of charging scores over 100 iterations. One can see how the selected score increases step by step, meaning that agents find more beneficial charging plan configurations for their daily plans: + +![Scores over iterations](docs/sevc/scores.png "Scores over iterations") + +Based on the SEVC output, we can plot in detail the individual components that consitute the charging score in each iteration. We can see how zero-SoC and minimum-SoC situations are reduced and how agents select chargers that minimize their charging costs: + +![Distribution of scores](docs/sevc/score_distribution.png "Distribution of scores") + +Individual high-level indicators can be obtained, such as the charging duration and revenues by the four different charger types that have been set up in the simulation: + +![Charging duration](docs/sevc/durations.png "Charging duration") + +![Revenues](docs/sevc/revenues.png "Revenues") diff --git a/contribs/ev/docs/sevc/durations.png b/contribs/ev/docs/sevc/durations.png new file mode 100644 index 0000000000000000000000000000000000000000..d013d9b5c633105a0927472337bb1aabe0908b4e GIT binary patch literal 63891 zcmeFZRZv{p`Y(zkK(Ii9yCgt@1rP4-O_1OYf#B}$5Hz?3?cnb24xzE&P8yfSH^Y<^Br#BlP~qUKMhgRv%VV%`iKfUgamiuBO$e{|GpRa96FdM(my{=s-y3P64a^%@w3-IzxY)) z`=0^+d*1$M2LGS-V2LiYR+E(Th%Zpk znY?d0?GhW0%x{E{@5Js;m-$j2UO3Kx$$syRRS1P1lZ_nt^g+PI*8@C6r8;Bb$lJ

ByxW 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 z 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 Gd;bG^)im1x literal 0 HcmV?d00001 diff --git a/contribs/ev/docs/sevc/score_distribution.png b/contribs/ev/docs/sevc/score_distribution.png new file mode 100644 index 0000000000000000000000000000000000000000..e2bff515e2db56e567ac787eb7c4904ae47e471b GIT binary patch literal 67173 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 trace) { + throw new UnsupportedOperationException(); + } + + @Override + @Nullable + public ChargingAlternative findEnrouteAlternative(double now, Person person, Plan plan, + ElectricVehicle electricVehicle, + @Nullable ChargingSlot slot) { + Preconditions.checkArgument(slot == null); + + Double criticalSoc = getCriticalSoc(person); + if (criticalSoc != null) { + // we must be on a leg when finding an enroute alternative + Leg leg = (Leg) WithinDayAgentUtils.getCurrentPlanElement(qsim.getAgents().get(person.getId())); + + double initialCharge = electricVehicle.getBattery().getCharge(); + double targetCharge = criticalSoc * electricVehicle.getBattery().getCapacity(); + + // a priori no additional charge needed right now if zero + double deltaCharge = Math.max(0.0, targetCharge - initialCharge); + + if (deltaCharge == 0.0) { + // Can we obtain the vehicle parameter from somewhere here? For time + // calculation? + double consumption = calculateConsumption(now, person, leg, electricVehicle, null); + double finalCharge = initialCharge - consumption; + + // maybe not enough at the end + deltaCharge = Math.max(0.0, targetCharge - finalCharge); + } + + if (deltaCharge > 0.0) { + // we need to charge, otherwise we don't get to the next activity + BatteryCharging calculator = (BatteryCharging) electricVehicle.getChargingPower(); + + Collection chargers = chargerProvider.findChargers(person, plan, + new ChargerRequest(leg)); + + if (chargers.size() > 0) { + final double fixedDeltaCharge = deltaCharge; + + ChargerSpecification selected = chargers.stream().sorted((a, b) -> { + double durationA = calculator.calcChargingTime(a, fixedDeltaCharge); + double durationB = calculator.calcChargingTime(b, fixedDeltaCharge); + return Double.compare(durationA, durationB); + }).findFirst().get(); + + double duration = calculator.calcChargingTime(selected, fixedDeltaCharge); + duration = Math.max(minimumDuration, duration); + + return new ChargingAlternative(infrastructure.getChargers().get(selected.getId()), duration); + } + } + } + + return null; + } + + private double calculateConsumption(double now, Person person, Leg leg, ElectricVehicle electricVehicle, + Vehicle vehicle) { + double consumption = 0.0; + + DriveEnergyConsumption calculator = electricVehicle.getDriveEnergyConsumption(); + for (Id linkId : ((NetworkRoute) leg.getRoute()).getLinkIds()) { + Link link = network.getLinks().get(linkId); + double linkTravelTime = travelTime.getLinkTravelTime(link, now, person, vehicle); + + consumption += calculator.calcEnergyConsumption(link, linkTravelTime, now); + now += linkTravelTime; + } + + return consumption; + } + + /** + * Sets the critical SoC for an agent. + */ + static public void setCriticalSoC(Person person, double criticalSoc) { + person.getAttributes().putAttribute(CRITICAL_SOC_PERSON_ATTRIBUTE, criticalSoc); + } + + /** + * Retrieves the critical SoC from an agent. + */ + static public Double getCriticalSoc(Person person) { + return (Double) person.getAttributes().getAttribute(CRITICAL_SOC_PERSON_ATTRIBUTE); + } +} diff --git a/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/README.md b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/README.md new file mode 100644 index 00000000000..748eb59d0b7 --- /dev/null +++ b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/README.md @@ -0,0 +1,147 @@ +# Strategic electric vehicle charging (SEVC) + +## Configuration parameters + +A range of configuration options control the generation of new charging plans: + +- `minimumActivityChargingDuration` (*Double*, default `900`, seconds): defines the shortest possible activity or chain of activities during which charging may be planned + +- `maximumActivityChargingDuration` (*Double*, default `Inf`, seconds): defines the longest possible activity or chain of activities during which charging may be planned + +- `minimumEnrouteDriveTime` (*Double*, default `3600`, seconds): defines the shortest possible drive during which enroute charging may be planned + +- `minimumEnrouteChargingDuration` (*Double*, default `900`, seconds): defines the shortest possible charging slot that may be generated enroute + +- `maximumEnrouteChargingDuration` (*Double*, default `3600`, seconds): defines the shortest possible charging slot that may be generated enroute + +- `chargerSearchRadius` (*Double*, default `1000`, meters): defines the radius within which chargers are searched + +The following parmeters control the selection / innovation process for charging plans: + +- `selectionProbability` (*Double*, default `0.8`): defines the frequency with which charging plans are switched rather than newly generawted + +- `selectionStrategy` (*Exponential|Random|Best*, default `Exponential`): defines the way that plans are selected from the plan memory, based on their charging score + +- `maximumChargingPlans` (*Integer*, default `4`): size of the charging plan memory + +The following parameters control scoring of charging plans: + +- `chargingScoreWeight` (*Double*, default `0.0`): defines with which factor the calculated charging score for a plan is fed back into the score of the regular plan + +- `scoreTrackingInterval` (*Integer*, default `0`): defines how often detailed information on the scoring process is written out (`0` means never, any other value produces surely an output in the last iteration) + +The following parmeters control the online search process during the simulation: + +- `maximumAlternatives` (*Integer*, default `2`): maximum attempts (including the initial one) that an agent tries to find a charger before the search is aborted + +- `maximumQueueTime` (*Double*, default `300`): maximum queue time at a charger until an alternative charger is searched + +- `useProactiveAlternativeSearch` (*Boolean*, default `true`): defines if agents consider potential alternatives proactively upon starting the leg that leads to a planned charging activity + +- `alternativeSearchStrategy`: defines the way agents make decisions for alternative chargers during the simulation + +- *Naive*: agents select a random alternative charger +- *OccupancyBased*: agents select a random alternative charger that has a free spot +- *ReservationBased*: agents select a random alternative charger and reserves it for the planned duration + +- `alternativeCacheSize` (*Integer*, default `-1`): defines whether to precompute viable charger alternatives for each charging activity planned in an agent's plan. In that case the search process is restricted to one alternative attempt. + +## Scoring parameters + +- `cost` (*Doule*, default `-1.0`): weight the monetary cost units incurred from charging + +- `waitTime_min` (*Double*, default `0.0`): weight the time waited in a queue for charging + +- `detourTime_min` (*Double*, default `0.0`): weighs the travel time reduced by the planned travel time of the base plan. Evaluation is mostly relevant compared to the detour component of another charging plan. + +- `detourDistance_km` (*Double*, default `0.0`): weighs the travel time reduced by the planned travel time of the base plan. Evaluation is mostly relevant compared to the detour component of another charging plan. + +- `zeroSoc` (*Double*, default `-100.0`): penalty added when the SoC of the agent drops to zero + +- `failedChargingAttempt` (*Double*, default `-10.0`): penalty added when a charging attempt of the agent fails (probably followed by an alternative attempt) + +- `failedChargingProcess` (*Double*, default `-100.0`): penalty added when a charging process fails (no new alternative is found) + +- `belowMinimumSoc` (*Double*, default `0.0`): penalty added when the SoC of a person falls below an attributable value (see person attributes) + +- `belowMinimumEndSoc` (*Double*, default `0.0`): penalty added when the SoC of the person at the end of the simulation is below an attributable value (see person attributes) + +## Cost models + +Two cost models can be selected by default. + +The *DefaultCostModel* defines one cost parameter: + +- `costPerEnergy_kWh` (*Double*, default `0.28`): cost registered for charging energy +- `costPerDuration_min` (*Double*, default `0.0`): cost registered for charging duration + +The *AttributeCostModel* defines no parameters, but obtains the cost from the charger (see charger attributes). + + +## Charger and person attributes + +### Chargers + +A couple of attributes define the chargers search process. One option would be to save all chargers in a large global spatial index, but then filtering out which chargers are aimed at a specific purpose is costly. Hence, we define a couple of categories in which each charger needs to be sorted in to speed up identification of viable chargers for each desired enroute or activtiy-based charging activity: + +- `sevc:facilities` (*String*, optional): Chargers that have this attribute are proposed during charger search when the activity during which an agent wants to charge takes place at one of the listed facilities. + +- `sevc:persons` (*String*, optional): Chargers that have this attribute are proposed during charger search when the agent is in the provided list of persons. + +- `sevc:public` (*Boolean*, default `false`): By default, all chargers are not public and only found if they fall in one of the previous categories. Only chargers for which is attribute is `true` are provided in a spatial search around the planned enroute or fixed charging activities of an agent, additionally to the categories above. + +For public chargers (but, potentially, also for workplace chargers and others) an additional layer of access verification is performed. Each charger can have a list of subscriptions of which a user must have one in order to be allowed to charge: + +- `sevc:subscriptions` (*String*, comma-separated, optional): describes the subscriptions that are necessary to use this charger. If no attribute is give, the charger is accessible to everyone. + +Additional attributes: + +- `sevc:operator` (*String*, optional): describes the name of the operator of the charger. The atrtibutes is used for per-operator analysis. + +- `secv:costPerEnergy_kWh` (*Double*, default `0.0`): describes the cost in monetary units (for instance, EUR) for charging one kWh at the charger. Used if the `AttributeBasedChargingCost` is chosen. + +- `secv:costPerDuration_min` (*Double*, default `0.0`): describes the cost for the plugging duration at the charger. Used if the `AttributeBasedChargingCost` is chosen. + +**Some attributes need clarification: @ Olly** + +- `plugType`: What is meant? + +### Persons + +For a person to be covered by the SEVC contrib, it needs to be activated. Also, access to chargers can be defined: + +- `sevc:active` (*Boolean*, default `false`): only if set to true, an agent is simulated in SEVC, otherwise they are ignored + +- `sevc:subscriptions` (*String*, comma-separated, optional): a list of subscription that the persons owns. In case a charger requires a subscription, it must be present in the list for the person to be eligible. + +- `sevc:activityTypes` (*String*, comma-separated, optional): if given, the agent will only attempt at activities of the listed types + +Some parameters relevant for the simulation dynamics: + +- `sevc:criticalSoc` (*Double*, default `0.0`): if the agents departs on a leg and detects that the SoC will fall below that value along the way, a spontaneous charging activity will be created. Only used if spontaneous charging has been activated via configuration. + +- `sevc:maximumQueueTime` (*Double*, default from config): the time the agent will wait at a charger to be queued before the alternative search process starts. If no value is given, the default value from the main config group is used. + +Some parameters are relevant for scoring of the charging plans: + +- `sevc:minimumSoc` (*Double*, default `0.2`): if the SoC of the vehicle falls below this threshold, SEVC scoring will add a configurable penalty + +- `sevc:minimumEndSoc` (*Double*, default `0.8`): if the SoC of the vehicle is below this threshold at the end of the simulation, SEVC scoring will add a configurable penalty + +### Vehicles + +The maximum SoC per vehicle should per vehicle: + +- `sevc:maximumSoc` (*Double*): defines the maximum SoC until a vehicle will be charged at a charger + +**Some things that are not handled by the SEVC contrib: @ Olly** + +- Battery capacity is already defined in `VehicleType` > `EngineInformation` attributes in vehicles file +- Initial SoC is already defined in `Vehicle` attributes in vehicles file + +**Some things that need clarification / are obsolete @ Olly** + +- `simEndSoc` is a result of the simulation and can be handled via scoring (see above) +- `chargingTypeIndex` not sure what is meant +- `chargingType` not sure what is meant +- `chargerIds` is rather handled in the chargers file, to be more consistent across different types diff --git a/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/StrategicChargingAlternativeProvider.java b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/StrategicChargingAlternativeProvider.java new file mode 100644 index 00000000000..14c523aa5a3 --- /dev/null +++ b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/StrategicChargingAlternativeProvider.java @@ -0,0 +1,213 @@ +package org.matsim.contrib.ev.strategic; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import javax.annotation.Nullable; + +import org.matsim.api.core.v01.Coord; +import org.matsim.api.core.v01.Scenario; +import org.matsim.api.core.v01.population.Person; +import org.matsim.api.core.v01.population.Plan; +import org.matsim.contrib.ev.fleet.ElectricVehicle; +import org.matsim.contrib.ev.infrastructure.Charger; +import org.matsim.contrib.ev.infrastructure.ChargerSpecification; +import org.matsim.contrib.ev.infrastructure.ChargingInfrastructure; +import org.matsim.contrib.ev.reservation.ChargerReservationManager; +import org.matsim.contrib.ev.strategic.StrategicChargingConfigGroup.AlternativeSearchStrategy; +import org.matsim.contrib.ev.strategic.access.ChargerAccess; +import org.matsim.contrib.ev.strategic.infrastructure.ChargerProvider; +import org.matsim.contrib.ev.strategic.infrastructure.ChargerProvider.ChargerRequest; +import org.matsim.contrib.ev.withinday.ChargingAlternative; +import org.matsim.contrib.ev.withinday.ChargingAlternativeProvider; +import org.matsim.contrib.ev.withinday.ChargingSlot; +import org.matsim.core.population.PopulationUtils; +import org.matsim.core.utils.geometry.CoordUtils; +import org.matsim.core.utils.timing.TimeInterpretation; +import org.matsim.core.utils.timing.TimeTracker; + +import com.google.common.base.Verify; + +/** + * This is the ChargingAlternativeProvider of the strategic charging package. + * Whenever an agent tries to find a new charger, the ChargingProvider logic is + * used to find and select viable locations. + * + * @author Sebastian Hörl (sebhoerl), IRT SystemX + */ +public class StrategicChargingAlternativeProvider implements ChargingAlternativeProvider { + private final Scenario scenario; + private final ChargerProvider chargerProvider; + private final ChargerAccess access; + private final ChargingInfrastructure infrastruture; + + private final ChargerReservationManager reservationManager; + private final TimeInterpretation timeInterpretation; + + private final AlternativeSearchStrategy onlineSearchStrategy; + private final boolean useProactiveOnlineSearch; + + private final CriticalAlternativeProvider criticalProvider; + private final int maximumAlternatives; + + public StrategicChargingAlternativeProvider(Scenario scenario, ChargerProvider chargerProvider, + ChargingInfrastructure infrastructure, + ChargerAccess access, + AlternativeSearchStrategy onlineSearchStrategy, boolean useProactiveOnlineSearch, + TimeInterpretation timeInterpretation, + @Nullable ChargerReservationManager reservationManager, CriticalAlternativeProvider criticalProvider, + int maximumAlternatives) { + this.maximumAlternatives = maximumAlternatives; + this.chargerProvider = chargerProvider; + this.infrastruture = infrastructure; + this.access = access; + this.scenario = scenario; + this.reservationManager = reservationManager; + this.timeInterpretation = timeInterpretation; + this.onlineSearchStrategy = onlineSearchStrategy; + this.useProactiveOnlineSearch = useProactiveOnlineSearch; + this.criticalProvider = criticalProvider; + } + + @Override + @Nullable + public ChargingAlternative findAlternative(double now, Person person, Plan plan, ElectricVehicle vehicle, + ChargingSlot slot, List trace) { + if (trace.size() >= maximumAlternatives) { + return null; // search limit has been reached + } + + // obtain possible other chargers within the search radius + Coord initialLocation = slot.isLegBased() ? slot.charger().getCoord() + : PopulationUtils.decideOnCoordForActivity(slot.startActivity(), scenario); + + Collection candidates = chargerProvider.findChargers(person, plan, + new ChargerRequest(slot.startActivity(), slot.endActivity(), slot.leg(), slot.duration())).stream() + .map(ChargerSpecification::getId).map(infrastruture.getChargers()::get).collect(Collectors.toList()); + + // remove chargers that have already been visited + candidates.remove(slot.charger()); + trace.forEach(s -> { + candidates.remove(s.charger()); + }); + + // remove chargers to which the person has no access + candidates.removeIf(candidate -> !access.hasAccess(person, candidate)); + + // remove chargers that have no free spots + if (onlineSearchStrategy.equals(AlternativeSearchStrategy.OccupancyBased)) { + candidates.removeIf(candidate -> { + return candidate.getLogic().getPluggedVehicles().size() == candidate.getPlugCount(); + }); + } + + // remove chargers for which no reservation can be made + double reservationStartTime = now; + double reservationEndTime = estimateReservationEndTime(reservationStartTime, plan, slot); + + if (onlineSearchStrategy.equals(AlternativeSearchStrategy.ReservationBased)) { + candidates.removeIf(candidate -> { + return !reservationManager.isAvailable(candidate.getSpecification(), vehicle, reservationStartTime, + reservationEndTime); + }); + } + + // perform a random selection + if (candidates.size() > 0) { + Charger selected = candidates.stream().sorted((a, b) -> { + double distanceA = CoordUtils.calcEuclideanDistance(initialLocation, a.getCoord()); + double distanceB = CoordUtils.calcEuclideanDistance(initialLocation, b.getCoord()); + return Double.compare(distanceA, distanceB); + }).findFirst().get(); + + // send the reservation if requested + if (onlineSearchStrategy.equals(AlternativeSearchStrategy.ReservationBased)) { + Verify.verify( + reservationManager.addReservation(selected.getSpecification(), vehicle, reservationStartTime, + reservationEndTime) != null); + } + + double duration = slot.isLegBased() ? slot.duration() : 0.0; + return new ChargingAlternative(selected, duration); + } + + // no new candidate found + return null; + } + + @Override + @Nullable + public ChargingAlternative findEnrouteAlternative(double now, Person person, Plan plan, + ElectricVehicle vehicle, + @Nullable ChargingSlot slot) { + if (slot == null) { + // no activity-based or leg-based charging planned, but we may add a critical + // charge + + if (criticalProvider != null) { + return criticalProvider.findEnrouteAlternative(now, person, plan, vehicle, slot); + } else { + return null; // no change + } + } + + // only if proactive search is enabled + if (!useProactiveOnlineSearch) { + return null; + } + + boolean updateRequired = false; + + // reserve upon approaching + if (onlineSearchStrategy.equals(AlternativeSearchStrategy.ReservationBased)) { + double reservationStartTime = now; + double reservationEndTime = estimateReservationEndTime(reservationStartTime, plan, slot); + + // if a reservation can be made now, keep the initial slot + if (reservationManager.isAvailable(slot.charger().getSpecification(), vehicle, reservationStartTime, + reservationEndTime)) { + Verify.verifyNotNull(reservationManager.addReservation(slot.charger().getSpecification(), + vehicle, reservationStartTime, reservationEndTime)); + return null; + } else { + updateRequired = true; + } + } + + // proactively react if planned charger is occupied + if (onlineSearchStrategy.equals(AlternativeSearchStrategy.OccupancyBased)) { + updateRequired |= slot.charger().getPlugCount() == slot.charger().getLogic().getPluggedVehicles() + .size(); + } + + if (updateRequired) { + // use logic from above to find a new charger, excluding the planned attempt + return findAlternative(now, person, plan, vehicle, + slot, Collections.emptyList()); + } else { + // keep initial slot + return null; + } + } + + private double estimateReservationEndTime(double startTime, Plan plan, ChargingSlot slot) { + TimeTracker timeTracker = new TimeTracker(timeInterpretation); + timeTracker.setTime(startTime); + + if (!slot.isLegBased()) { + int startIndex = plan.getPlanElements().indexOf(slot.startActivity()); + int endIndex = plan.getPlanElements().indexOf(slot.endActivity()); + + for (int i = startIndex; i <= endIndex; i++) { + timeTracker.addElement(plan.getPlanElements().get(i)); + } + } else { + // TODO: estimate drive-to time + timeTracker.addDuration(slot.duration()); + } + + return timeTracker.getTime().orElse(Double.POSITIVE_INFINITY); + } +} diff --git a/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/StrategicChargingConfigGroup.java b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/StrategicChargingConfigGroup.java new file mode 100644 index 00000000000..5b45a6f7d60 --- /dev/null +++ b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/StrategicChargingConfigGroup.java @@ -0,0 +1,150 @@ +package org.matsim.contrib.ev.strategic; + +import org.matsim.contrib.common.util.ReflectiveConfigGroupWithConfigurableParameterSets; +import org.matsim.contrib.ev.strategic.costs.AttributeBasedChargingCostsParameters; +import org.matsim.contrib.ev.strategic.costs.ChargingCostsParameters; +import org.matsim.contrib.ev.strategic.costs.DefaultChargingCostsParameters; +import org.matsim.contrib.ev.strategic.scoring.ChargingPlanScoringParameters; +import org.matsim.core.config.Config; + +import com.google.common.base.Verify; + +import jakarta.validation.constraints.DecimalMax; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.PositiveOrZero; + +/** + * Configuration for the Strategic Electric Vehicle Charging package (SEVC). + * + * @author Sebastian Hörl (sebhoerl), IRT SystemX + */ +public class StrategicChargingConfigGroup extends ReflectiveConfigGroupWithConfigurableParameterSets { + public static final String GROUP_NAME = "startegic_charging"; + + public static StrategicChargingConfigGroup get(Config config) { + return (StrategicChargingConfigGroup) config.getModules().get(GROUP_NAME); + } + + public StrategicChargingConfigGroup() { + super(GROUP_NAME); + + addDefinition(ChargingPlanScoringParameters.GROUP_NAME, // + ChargingPlanScoringParameters::new, // + () -> scoring, // + s -> { + scoring = (ChargingPlanScoringParameters) s; + }); + + addDefinition(DefaultChargingCostsParameters.SET_NAME, // + DefaultChargingCostsParameters::new, // + () -> (DefaultChargingCostsParameters) costs, // + c -> { + costs = (DefaultChargingCostsParameters) c; + }); + + addDefinition(AttributeBasedChargingCostsParameters.SET_NAME, // + AttributeBasedChargingCostsParameters::new, // + () -> (AttributeBasedChargingCostsParameters) costs, // + c -> { + costs = (AttributeBasedChargingCostsParameters) c; + }); + } + + public ChargingPlanScoringParameters scoring; + public ChargingCostsParameters costs; + + @Parameter + @Comment("Minimum duration of the activity-based charging slots") + @PositiveOrZero + public double minimumActivityChargingDuration = 900.0; + + @Parameter + @Comment("Maximum duration of the activity-based charging slots") + @PositiveOrZero + public double maximumActivityChargingDuration = Double.POSITIVE_INFINITY; + + @Parameter + @Comment("Minimum drive duration at which charging along the route is considered") + @PositiveOrZero + public double minimumEnrouteDriveTime = 3600.0;; + + @Parameter + @Comment("Minimum duration of enroute charging. A random value between the minimum and this value is sampled.") + @PositiveOrZero + public double minimumEnrouteChargingDuration = 3600.0; + + @Parameter + @Comment("Maximum duration of enroute charging. A random value between the minimum and this value is sampled.") + @PositiveOrZero + public double maximumEnrouteChargingDuration = 3600.0; + + @Parameter + @Comment("Euclidean search radius to find candidates for charging") + @PositiveOrZero + public double chargerSearchRadius = 1000.0; + + @Parameter + @Comment("Defines the probability with which a charging plan is selected among the existing ones versus creating a new charging plan") + @DecimalMin("0.0") + @DecimalMax("1.0") + public double selectionProbability = 0.8; + + @Parameter + @Comment("Defines how many charging plans per regular plan can exist") + @Positive + public int maximumChargingPlans = 4; + + @Parameter + @Comment("Defines the weight with which the charging score is added to the standard plan score") + public double chargingScoreWeight = 0.0; + + public enum SelectionStrategy { + Best, Exponential, Random + } + + @Parameter + @Comment("Defines the selection strategy for the charging plans") + public SelectionStrategy selectionStrategy = SelectionStrategy.Exponential; + + public enum AlternativeSearchStrategy { + Naive, OccupancyBased, ReservationBased + } + + @Parameter + @Comment("Defines the scaling factor for the exponential charging plan selection strategy") + public double exponentialSelectionBeta = 0.1; + + @Parameter + @Comment("Defines what to do when planned charger is not available. Naive: Select any other eligible charger nearby. Occupancy: Check whether there is at least a free spot upon departure. Reservation: Select among chargers that can be prebooked for the planned charging slot.") + public AlternativeSearchStrategy onlineSearchStrategy = AlternativeSearchStrategy.Naive; + + @Parameter + @Comment("Defines whether online search is applied proactively (when approaching a planned charging activity)") + public boolean useProactiveOnlineSearch = true; + + @Parameter + @Comment("Defines how often to write out detailed information about charging scoring") + public int scoreTrackingInterval = 0; + + @Parameter + @Comment("Maximum attempts (excluding the initial one) that an agent tries to find a charger before the search is aborted") + int maximumAlternatives = 2; + + @Parameter + @Comment("Defines whether to precompute viable charger alternatives for each charging activity planned in an agent's plan. A value of -1 means no caching.") + int alternativeCacheSize = -1; + + @Override + protected void checkConsistency(Config config) { + super.checkConsistency(config); + + Verify.verify(maximumEnrouteChargingDuration >= minimumEnrouteChargingDuration); + Verify.verifyNotNull(scoring, "Charging scoring parameters not found"); + + if (alternativeCacheSize > -1) { + Verify.verify(maximumAlternatives == 1, + "When using alternative caching, only one alternative can be chosen, so maximumAlternatives must be set to 1!"); + } + } +} diff --git a/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/StrategicChargingModule.java b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/StrategicChargingModule.java new file mode 100644 index 00000000000..00aaeab2b6c --- /dev/null +++ b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/StrategicChargingModule.java @@ -0,0 +1,216 @@ +package org.matsim.contrib.ev.strategic; + +import org.matsim.api.core.v01.Scenario; +import org.matsim.api.core.v01.network.Network; +import org.matsim.api.core.v01.population.Population; +import org.matsim.contrib.ev.fleet.ElectricFleetSpecification; +import org.matsim.contrib.ev.infrastructure.ChargingInfrastructureSpecification; +import org.matsim.contrib.ev.reservation.ChargerReservationModule; +import org.matsim.contrib.ev.strategic.StrategicChargingConfigGroup.AlternativeSearchStrategy; +import org.matsim.contrib.ev.strategic.access.AnyChargerAccess; +import org.matsim.contrib.ev.strategic.access.AttributeBasedChargerAccess; +import org.matsim.contrib.ev.strategic.access.ChargerAccess; +import org.matsim.contrib.ev.strategic.access.SubscriptionRegistry; +import org.matsim.contrib.ev.strategic.analysis.ChargerTypeAnalysisListener; +import org.matsim.contrib.ev.strategic.analysis.ChargingPlanScoringListener; +import org.matsim.contrib.ev.strategic.costs.ChargingCostCalculator; +import org.matsim.contrib.ev.strategic.costs.ChargingCostModule; +import org.matsim.contrib.ev.strategic.infrastructure.ChargerProvider; +import org.matsim.contrib.ev.strategic.infrastructure.DefaultChargerProvidersModule; +import org.matsim.contrib.ev.strategic.plan.ChargingPlans; +import org.matsim.contrib.ev.strategic.plan.ChargingPlansConverter; +import org.matsim.contrib.ev.strategic.replanning.StrategicChargingReplanningAlgorithm; +import org.matsim.contrib.ev.strategic.replanning.StrategicChargingReplanningStrategy; +import org.matsim.contrib.ev.strategic.replanning.innovator.ChargingPlanInnovator; +import org.matsim.contrib.ev.strategic.replanning.innovator.EmptyChargingPlanInnovator; +import org.matsim.contrib.ev.strategic.replanning.innovator.RandomChargingPlanInnovator; +import org.matsim.contrib.ev.strategic.replanning.selector.BestChargingPlanSelector; +import org.matsim.contrib.ev.strategic.replanning.selector.ChargingPlanSelector; +import org.matsim.contrib.ev.strategic.replanning.selector.ExponentialChargingPlanSelector; +import org.matsim.contrib.ev.strategic.replanning.selector.RandomChargingPlanSelector; +import org.matsim.contrib.ev.strategic.scoring.ChargingPlanScoring; +import org.matsim.contrib.ev.strategic.scoring.ScoringTracker; +import org.matsim.contrib.ev.strategic.scoring.StrategicChargingScoringFunction; +import org.matsim.contrib.ev.withinday.ChargingSlotFinder; +import org.matsim.contrib.ev.withinday.WithinDayEvConfigGroup; +import org.matsim.contrib.ev.withinday.analysis.WithinDayChargingAnalysisHandler; +import org.matsim.core.api.experimental.events.EventsManager; +import org.matsim.core.controler.AbstractModule; +import org.matsim.core.controler.OutputDirectoryHierarchy; +import org.matsim.core.router.util.TravelTime; +import org.matsim.core.scoring.ScoringFunctionFactory; +import org.matsim.core.scoring.functions.CharyparNagelScoringFunctionFactory; +import org.matsim.core.utils.timing.TimeInterpretation; + +import com.google.inject.Key; +import com.google.inject.Provider; +import com.google.inject.Provides; +import com.google.inject.Singleton; +import com.google.inject.name.Names; + +/** + * Main entry-point for startegic electric vehicle charging. + * + * @author Sebastian Hörl (sebhoerl), IRT SystemX + */ +public class StrategicChargingModule extends AbstractModule { + static public final String MODE_BINDING = "ev:strategic"; + + @Override + public void install() { + WithinDayEvConfigGroup withinDayConfig = WithinDayEvConfigGroup.get(getConfig()); + + install(new DefaultChargerProvidersModule()); + installOverridingQSimModule(new StrategicChargingQSimModule()); + + addPlanStrategyBinding(StrategicChargingReplanningStrategy.STRATEGY) + .toProvider(StrategicChargingReplanningStrategy.class); + + addControlerListenerBinding().to(ChargerTypeAnalysisListener.class); + addEventHandlerBinding().to(ChargerTypeAnalysisListener.class); + + bind(Key.get(TravelTime.class, Names.named(MODE_BINDING))) + .to(Key.get(TravelTime.class, Names.named(withinDayConfig.carMode))); + + StrategicChargingConfigGroup chargingConfig = StrategicChargingConfigGroup.get(getConfig()); + + switch (chargingConfig.selectionStrategy) { + case Best: + bind(ChargingPlanSelector.class).to(BestChargingPlanSelector.class); + break; + case Random: + bind(ChargingPlanSelector.class).to(RandomChargingPlanSelector.class); + break; + case Exponential: + bind(ChargingPlanSelector.class).to(ExponentialChargingPlanSelector.class); + break; + default: + throw new IllegalStateException(); + } + + // bind(ChargingPlanCreator.class).to(EmptyChargingPlanCreator.class); + bind(ChargingPlanInnovator.class).to(RandomChargingPlanInnovator.class); + + addControlerListenerBinding().to(ChargingPlanScoring.class); + addControlerListenerBinding().to(ChargingPlanScoringListener.class); + + if (chargingConfig.chargingScoreWeight != 0.0) { + bind(ScoringFunctionFactory.class).to(StrategicChargingScoringFunction.Factory.class).in(Singleton.class); + } + + install(new ChargingCostModule()); + + addAttributeConverterBinding(ChargingPlans.class).to(ChargingPlansConverter.class); + + bind(ChargerAccess.class).to(AttributeBasedChargerAccess.class); + + install(new ChargerReservationModule( + chargingConfig.onlineSearchStrategy.equals(AlternativeSearchStrategy.ReservationBased))); + } + + @Provides + @Singleton + ChargingPlansConverter provideChargingPlansConverter() { + return new ChargingPlansConverter(); + } + + @Provides + @Singleton + ChargingPlanScoring provideChargingPlanScoring(EventsManager eventsManager, Population population, Network network, + ElectricFleetSpecification fleet, ChargingCostCalculator costCalculator, + StrategicChargingConfigGroup scConfig, WithinDayEvConfigGroup withinConfig, ScoringTracker tracker) { + return new ChargingPlanScoring(eventsManager, population, network, fleet, costCalculator, scConfig.scoring, + withinConfig.carMode, tracker); + } + + @Provides + @Singleton + ScoringTracker provideScoringTracker(OutputDirectoryHierarchy outputHierarchy, + StrategicChargingConfigGroup config) { + return new ScoringTracker(outputHierarchy, config.scoreTrackingInterval); + } + + @Provides + StrategicChargingReplanningStrategy provideChargingPlanStrategy( + Provider algorithmProvider) { + return new StrategicChargingReplanningStrategy(getConfig().global(), algorithmProvider); + } + + @Provides + StrategicChargingReplanningAlgorithm provideStrategicReplanningAlgorithm(ChargingPlanSelector selector, + ChargingPlanInnovator creator, StrategicChargingConfigGroup config) { + return new StrategicChargingReplanningAlgorithm(selector, creator, config.selectionProbability, + config.maximumChargingPlans); + } + + @Provides + BestChargingPlanSelector provideBestChargingPlanSelector() { + return new BestChargingPlanSelector(); + } + + @Provides + RandomChargingPlanSelector provideRandomChargingPlanSelector() { + return new RandomChargingPlanSelector(); + } + + @Provides + EmptyChargingPlanInnovator provideEmptyChargingPlanCreator() { + return new EmptyChargingPlanInnovator(); + } + + @Provides + ExponentialChargingPlanSelector provideExponentialPlanSelector(StrategicChargingConfigGroup config) { + return new ExponentialChargingPlanSelector(config.exponentialSelectionBeta); + } + + @Provides + RandomChargingPlanInnovator provideRandomChargingPlanCreator(ChargerProvider chargerProvider, + Scenario scenario, StrategicChargingConfigGroup config, WithinDayEvConfigGroup withinConfig, + TimeInterpretation timeInterpretation) { + ChargingSlotFinder candidateFinder = new ChargingSlotFinder(scenario, withinConfig.carMode); + return new RandomChargingPlanInnovator(chargerProvider, candidateFinder, timeInterpretation, config); + } + + @Provides + @Singleton + ChargerTypeAnalysisListener provideChargerTypeAnalysisListener(OutputDirectoryHierarchy outputHierarchy, + ChargingInfrastructureSpecification infrastructure, EventsManager eventsManager, + WithinDayChargingAnalysisHandler analysisHandler) { + return new ChargerTypeAnalysisListener(outputHierarchy, infrastructure, analysisHandler, eventsManager); + } + + @Provides + @Singleton + ChargingPlanScoringListener provideChargingPlanScoringListener(Population population, + OutputDirectoryHierarchy outputHierarchy) { + return new ChargingPlanScoringListener(population, outputHierarchy); + } + + @Provides + @Singleton + StrategicChargingScoringFunction.Factory provideStrategicChargingScoringFunctiony(Scenario scenario, + WithinDayEvConfigGroup withinDayConfig, StrategicChargingConfigGroup chargingConfig, + ChargingPlanScoring chargingScoring) { + CharyparNagelScoringFunctionFactory delegate = new CharyparNagelScoringFunctionFactory(scenario); + return new StrategicChargingScoringFunction.Factory(delegate, chargingScoring, + chargingConfig.chargingScoreWeight); + } + + @Provides + @Singleton + AnyChargerAccess provideAnyChargerAccess() { + return new AnyChargerAccess(); + } + + @Provides + @Singleton + AttributeBasedChargerAccess provideAttributeBasedChargerAccess(SubscriptionRegistry subscriptionRegistry) { + return new AttributeBasedChargerAccess(subscriptionRegistry); + } + + @Provides + @Singleton + SubscriptionRegistry provideSubscriptionRegistry() { + return new SubscriptionRegistry(); + } +} diff --git a/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/StrategicChargingQSimModule.java b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/StrategicChargingQSimModule.java new file mode 100644 index 00000000000..75c1bd4beba --- /dev/null +++ b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/StrategicChargingQSimModule.java @@ -0,0 +1,69 @@ +package org.matsim.contrib.ev.strategic; + +import org.matsim.api.core.v01.Scenario; +import org.matsim.api.core.v01.network.Network; +import org.matsim.contrib.ev.EvModule; +import org.matsim.contrib.ev.infrastructure.ChargingInfrastructure; +import org.matsim.contrib.ev.reservation.ChargerReservationManager; +import org.matsim.contrib.ev.strategic.access.ChargerAccess; +import org.matsim.contrib.ev.strategic.infrastructure.ChargerProvider; +import org.matsim.contrib.ev.strategic.scoring.ChargingPlanScoring; +import org.matsim.contrib.ev.withinday.ChargingAlternativeProvider; +import org.matsim.contrib.ev.withinday.ChargingSlotFinder; +import org.matsim.contrib.ev.withinday.ChargingSlotProvider; +import org.matsim.contrib.ev.withinday.WithinDayEvConfigGroup; +import org.matsim.core.mobsim.qsim.AbstractQSimModule; +import org.matsim.core.mobsim.qsim.QSim; +import org.matsim.core.router.util.TravelTime; +import org.matsim.core.utils.timing.TimeInterpretation; + +import com.google.inject.Provides; +import com.google.inject.Singleton; +import com.google.inject.name.Named; + +/** + * The QSim components for startegic electric vehicle charging (SEVC). + * + * @author Sebastian Hörl (sebhoerl), IRT SystemX + */ +public class StrategicChargingQSimModule extends AbstractQSimModule { + public StrategicChargingQSimModule() { + super(); + } + + @Override + protected void configureQSim() { + bind(ChargingSlotProvider.class).to(StrategicChargingSlotProvider.class); + bind(ChargingAlternativeProvider.class).to(StrategicChargingAlternativeProvider.class); + + addQSimComponentBinding(EvModule.EV_COMPONENT).to(ChargingPlanScoring.class); + } + + @Provides + @Singleton + StrategicChargingSlotProvider provideStrategicOfflineSlotProvider(ChargingInfrastructure infrastructure, + TimeInterpretation timeInterpretation, Scenario scenario, WithinDayEvConfigGroup config) { + return new StrategicChargingSlotProvider(infrastructure, + new ChargingSlotFinder(scenario, config.carMode)); + } + + @Provides + StrategicChargingAlternativeProvider providePublicOnlineSlotProvider(ChargingInfrastructure infrastructure, + ChargerProvider chargerProvider, Scenario scenario, StrategicChargingConfigGroup chargingConfig, + ChargerAccess access, + ChargerReservationManager reservationManager, TimeInterpretation timeInterpretation, + CriticalAlternativeProvider criticalProvider) { + return new StrategicChargingAlternativeProvider(scenario, chargerProvider, infrastructure, access, + chargingConfig.onlineSearchStrategy, + chargingConfig.useProactiveOnlineSearch, timeInterpretation, reservationManager, criticalProvider, + chargingConfig.maximumAlternatives); + } + + @Provides + CriticalAlternativeProvider provideCriticalAlternativeProvider(QSim qsim, Network network, + @Named("car") TravelTime travelTime, + ChargerProvider chargerProvider, ChargingInfrastructure infrastructure, + StrategicChargingConfigGroup config) { + return new CriticalAlternativeProvider(qsim, network, travelTime, chargerProvider, infrastructure, config); + } +} diff --git a/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/StrategicChargingSlotProvider.java b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/StrategicChargingSlotProvider.java new file mode 100644 index 00000000000..d79d7278ce3 --- /dev/null +++ b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/StrategicChargingSlotProvider.java @@ -0,0 +1,93 @@ +package org.matsim.contrib.ev.strategic; + +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; + +import org.matsim.api.core.v01.population.Activity; +import org.matsim.api.core.v01.population.Person; +import org.matsim.api.core.v01.population.Plan; +import org.matsim.contrib.ev.fleet.ElectricVehicle; +import org.matsim.contrib.ev.infrastructure.Charger; +import org.matsim.contrib.ev.infrastructure.ChargingInfrastructure; +import org.matsim.contrib.ev.strategic.plan.ChargingPlan; +import org.matsim.contrib.ev.strategic.plan.ChargingPlanActivity; +import org.matsim.contrib.ev.strategic.plan.ChargingPlans; +import org.matsim.contrib.ev.withinday.ChargingSlot; +import org.matsim.contrib.ev.withinday.ChargingSlotFinder; +import org.matsim.contrib.ev.withinday.ChargingSlotFinder.ActivityBasedCandidate; +import org.matsim.contrib.ev.withinday.ChargingSlotFinder.LegBasedCandidate; +import org.matsim.contrib.ev.withinday.ChargingSlotProvider; +import org.matsim.core.router.TripStructureUtils; +import org.matsim.core.router.TripStructureUtils.StageActivityHandling; + +import com.google.common.base.Preconditions; + +/** + * This is the ChargingSlotProvider implementation of the startegic charging + * package. It examines an agent's plan and tries to find the selected "charging + * plan". If the charging plan can be found, the respective charging slots (if + * still avaialble after mode choice, etc.) are generated throghout the day. + * + * @author Sebastian Hörl (sebhoerl), IRT SystemX + */ +public class StrategicChargingSlotProvider implements ChargingSlotProvider { + private final ChargingInfrastructure infrastructure; + private final ChargingSlotFinder candidateFinder; + + public StrategicChargingSlotProvider(ChargingInfrastructure infrastructure, ChargingSlotFinder candidateFinder) { + this.infrastructure = infrastructure; + this.candidateFinder = candidateFinder; + } + + @Override + public List findSlots(Person person, Plan plan, ElectricVehicle vehicle) { + ChargingPlans chargingPlans = ChargingPlans.get(plan); + ChargingPlan selectedPlan = chargingPlans.getSelectedPlan(); + + if (selectedPlan == null) { + return Collections.emptyList(); + } + + // find the charging activities that are compatible with the possible slots of + // the current plan configuration + List slots = new LinkedList<>(); + + List activityBased = candidateFinder.findActivityBased(person, plan); + List legBased = candidateFinder.findLegBased(person, plan); + + List activities = TripStructureUtils.getActivities(plan, + StageActivityHandling.ExcludeStageActivities); + + for (ChargingPlanActivity chargingActivity : selectedPlan.getChargingActivities()) { + if (!chargingActivity.isEnroute()) { + for (ActivityBasedCandidate candidate : activityBased) { + if (activities.indexOf(candidate.startActivity()) == chargingActivity.getStartActivityIndex()) { + if (activities.indexOf(candidate.endActivity()) == chargingActivity.getEndActivityIndex()) { + // we found a matching candidate + Charger charger = infrastructure.getChargers().get(chargingActivity.getChargerId()); + slots.add(new ChargingSlot(candidate.startActivity(), + candidate.endActivity(), charger)); + } + } + } + } else { + boolean foundMatch = false; + + for (LegBasedCandidate candidate : legBased) { + if (activities.indexOf(candidate.followingActivity()) == chargingActivity + .getFollowingActivityIndex()) { + // we found a matching candidate + Charger charger = infrastructure.getChargers().get(chargingActivity.getChargerId()); + slots.add(new ChargingSlot(candidate.leg(), chargingActivity.getDuration(), charger)); + + Preconditions.checkState(!foundMatch); + foundMatch = true; + } + } + } + } + + return slots; + } +} diff --git a/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/StrategicChargingUtils.java b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/StrategicChargingUtils.java new file mode 100644 index 00000000000..30b5a990387 --- /dev/null +++ b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/StrategicChargingUtils.java @@ -0,0 +1,316 @@ +package org.matsim.contrib.ev.strategic; + +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; +import java.util.stream.Collectors; + +import org.matsim.api.core.v01.Id; +import org.matsim.api.core.v01.population.Person; +import org.matsim.contrib.ev.infrastructure.ChargerSpecification; +import org.matsim.contrib.ev.strategic.access.SubscriptionRegistry; +import org.matsim.contrib.ev.strategic.analysis.ChargerTypeAnalysisListener; +import org.matsim.contrib.ev.strategic.costs.AttributeBasedChargingCostCalculator; +import org.matsim.contrib.ev.strategic.costs.TariffBasedChargingCostCalculator; +import org.matsim.contrib.ev.strategic.infrastructure.FacilityChargerProvider; +import org.matsim.contrib.ev.strategic.infrastructure.PersonChargerProvider; +import org.matsim.contrib.ev.strategic.infrastructure.PublicChargerProvider; +import org.matsim.contrib.ev.strategic.replanning.StrategicChargingReplanningStrategy; +import org.matsim.contrib.ev.strategic.scoring.ChargingPlanScoring; +import org.matsim.contrib.ev.withinday.WithinDayChargingStrategy; +import org.matsim.contrib.ev.withinday.WithinDayEvConfigGroup; +import org.matsim.contrib.ev.withinday.WithinDayEvEngine; +import org.matsim.contrib.ev.withinday.WithinDayEvModule; +import org.matsim.core.config.Config; +import org.matsim.core.config.groups.ReplanningConfigGroup.StrategySettings; +import org.matsim.core.config.groups.ScoringConfigGroup.ActivityParams; +import org.matsim.core.controler.Controler; +import org.matsim.facilities.ActivityFacility; +import org.matsim.utils.objectattributes.attributable.Attributable; +import org.matsim.vehicles.Vehicle; + +/** + * This is a convenience class that gives a central access point to attributes + * that may need to be prepared in the scenario when using strategic charging. + * + * @author Sebastian Hörl (sebhoerl), IRT SystemX + */ +public class StrategicChargingUtils { + private StrategicChargingUtils() { + } + + /** + * Adds a subscription to the person attributes. To be used with + * AttributeBasedChargerAccess. + */ + public void addSubscription(Person person, String subscription) { + SubscriptionRegistry.addSubscription(person, subscription); + } + + /** + * Returns the subscriptions of a person + */ + static public Set getSubscriptions(Person person) { + return SubscriptionRegistry.getSubscriptions(person); + } + + /** + * Adds a subscription to the charger attributes. To be used with + * AttributeBasedChargerAccess. + */ + public void addSubscription(ChargerSpecification charger, String subscription) { + SubscriptionRegistry.addSubscription(charger, subscription); + } + + /** + * Returns the required subscriptions for a charger + */ + static public Set getSubscriptions(ChargerSpecification charger) { + return SubscriptionRegistry.getSubscriptions(charger); + } + + /** + * Adds an analysis type to a charger + */ + static public void addAnalysisType(ChargerSpecification charger, String analysisType) { + ChargerTypeAnalysisListener.addAnalysisType(charger, analysisType); + } + + /** + * Returns the list of analysis types of a charger + */ + static public Set getAnalysisTypes(ChargerSpecification charger) { + return ChargerTypeAnalysisListener.getAnalysisTypes(charger); + } + + /** + * Sets the cost structure for the charger. + */ + static public void setChargingCosts(ChargerSpecification charger, double costPerUse, double costPerEnergy_kWh, + double costPerDuration_kWh) { + AttributeBasedChargingCostCalculator.setChargingCosts(charger, costPerUse, costPerEnergy_kWh, + costPerDuration_kWh); + } + + /** + * Sets the cost structure for the charger. The blocking costs are charged + * additionally for any duration that exceeds the blocking duration. + */ + static public void setChargingCosts(ChargerSpecification charger, double costPerUse, double costPerEnergy_kWh, + double costPerDuration_min, double costPerBlockingDuration_min, double blockingDuration_min) { + AttributeBasedChargingCostCalculator.setChargingCosts(charger, costPerUse, costPerEnergy_kWh, + costPerDuration_min, costPerBlockingDuration_min, blockingDuration_min); + } + + /** + * Adds a tariff to a charger. + */ + static public void addTariff(ChargerSpecification charger, String tariff) { + TariffBasedChargingCostCalculator.addTariff(charger, tariff); + } + + /** + * Retrieve the list of tariffs for a charger. + */ + static public Set getTariffs(ChargerSpecification charger) { + return TariffBasedChargingCostCalculator.getTariffs(charger); + } + + /** + * Sets the minimum SoC under which a person doesn't want to fall. + */ + static public void setMinimumSoc(Person person, double minimumSoc) { + ChargingPlanScoring.setMinimumSoc(person, minimumSoc); + } + + /** + * Returns the minimum SoC under which a person doesn't want to fall. + */ + static public Double getMinimumSoc(Person person) { + return ChargingPlanScoring.getMinimumSoc(person); + } + + /** + * Sets the minimum SoC under which a person doesn't want to be at the end of + * the day. + */ + static public void setMinimumEndSoc(Person person, double minimumEndSoc) { + ChargingPlanScoring.setMinimumEndSoc(person, minimumEndSoc); + } + + /** + * Returns the minimum SoC under which a person doesn't want to be at the end of + * the day. + */ + static public Double getMinimumEndSoc(Person person) { + return ChargingPlanScoring.getMinimumEndSoc(person); + } + + /** + * Returns whether a charger is assigned to at least one facility. + */ + static public boolean isFacilityCharger(ChargerSpecification charger) { + return FacilityChargerProvider.isFacilityCharger(charger); + } + + /** + * Sets the facilities to which a charger is assigned. + */ + static public void assignChargerFacilities(ChargerSpecification charger, + Collection> facilityIds) { + FacilityChargerProvider.setFacilityIds(charger, facilityIds); + } + + /** + * Returns the facilities to which a charger is assigned. + */ + static public Set> getChargerFacilities(ChargerSpecification charger) { + return FacilityChargerProvider.getFacilityIds(charger); + } + + /** + * Returns whether a charger is assigned to at least one person. + */ + static public boolean isPersonCharger(ChargerSpecification charger) { + return PersonChargerProvider.isPersonCharger(charger); + } + + /** + * Sets the persons to which a charger is assigned. + */ + static public void assignChargerPersons(ChargerSpecification charger, Collection> personIds) { + PersonChargerProvider.setPersonIds(charger, personIds); + } + + /** + * Returns the persons to which a charger is assigned. + */ + static public Set> getChargerPersons(ChargerSpecification charger) { + return PersonChargerProvider.getPersonIds(charger); + } + + /** + * Return whether a charger is public. + */ + static public boolean isPublicCharger(ChargerSpecification charger) { + return PublicChargerProvider.isPublicCharger(charger); + } + + /** + * Sets a charge public or not. + */ + static public void assignPublic(ChargerSpecification charger, boolean isPublic) { + PublicChargerProvider.setPublic(charger, isPublic); + } + + /** + * Sets the critical SoC for an agent. + */ + static public void setCriticalSoC(Person person, double criticalSoc) { + CriticalAlternativeProvider.setCriticalSoC(person, criticalSoc); + } + + /** + * Retrieves the critical SoC from an agent. + */ + static public Double getCriticalSoc(Person person) { + return CriticalAlternativeProvider.getCriticalSoc(person); + } + + /** + * Returns the maximum SoC of a vehicle. + */ + static public Double getMaximumSoc(Vehicle vehicle) { + return WithinDayChargingStrategy.getMaximumSoc(vehicle); + } + + /** + * Sets the maximum SoC of a vehicle. + */ + static public void setMaximumSoc(Vehicle vehicle, double maximumSoc) { + WithinDayChargingStrategy.setMaximumSoc(vehicle, maximumSoc); + } + + /** + * Sets up scoring for a SEVC simulation + */ + static public void configureScoring(Config config) { + for (String activityType : Arrays.asList(WithinDayEvEngine.PLUG_ACTIVITY_TYPE, + WithinDayEvEngine.UNPLUG_ACTIVITY_TYPE, WithinDayEvEngine.ACCESS_ACTIVITY_TYPE, + WithinDayEvEngine.WAIT_ACTIVITY_TYPE)) { + ActivityParams activityParams = new ActivityParams(activityType); + activityParams.setScoringThisActivityAtAll(false); + config.scoring().addActivityParams(activityParams); + } + } + + /** + * Sets up configuration for a SEVC simulation + * + *

+ */ + static public void configure(Config config) { + WithinDayEvConfigGroup wdevConfig = new WithinDayEvConfigGroup(); + config.addModule(wdevConfig); + + StrategicChargingConfigGroup sevcConfig = new StrategicChargingConfigGroup(); + config.addModule(sevcConfig); + + StrategySettings sevcStrategy = new StrategySettings(); + sevcStrategy.setStrategyName(StrategicChargingReplanningStrategy.STRATEGY); + sevcStrategy.setWeight(0.05); + config.replanning().addStrategySettings(sevcStrategy); + + configureScoring(config); + } + + /** + * Sets up configuration for a standalone SEVC simulation + * + *
    + *
  • Agents still need to be activated using the `activate` method
  • + *
+ */ + static public void configureStanadlone(Config config) { + configure(config); + + config.replanning().setMaxAgentPlanMemorySize(1); + config.replanning().clearStrategySettings(); + + StrategySettings sevcStrategy = new StrategySettings(); + sevcStrategy.setStrategyName(StrategicChargingReplanningStrategy.STRATEGY); + sevcStrategy.setWeight(1.0); + config.replanning().addStrategySettings(sevcStrategy); + } + + /** + * Sets up the controller + */ + static public void configureController(Controler controller) { + controller.addOverridingModule(new WithinDayEvModule()); + controller.addOverridingModule(new StrategicChargingModule()); + } + + /** + * Helper function that reads a list of string items from an attribute. + */ + public static Set readList(Attributable source, String attribute) { + String value = (String) source.getAttributes().getAttribute(attribute); + + if (value == null) { + return new HashSet<>(); + } else { + return new HashSet<>(Arrays.stream(attribute.split(",")).collect(Collectors.toSet())); + } + } + + /** + * Helper function that writes a list of string items to an attribute. + */ + public static void writeList(Attributable target, String attribute, Set items) { + target.getAttributes().putAttribute(attribute, String.join(",", items)); + } +} diff --git a/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/access/AnyChargerAccess.java b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/access/AnyChargerAccess.java new file mode 100644 index 00000000000..ff85265cdc2 --- /dev/null +++ b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/access/AnyChargerAccess.java @@ -0,0 +1,22 @@ +package org.matsim.contrib.ev.strategic.access; + +import org.matsim.api.core.v01.population.Person; +import org.matsim.contrib.ev.infrastructure.Charger; +import org.matsim.contrib.ev.infrastructure.ChargerSpecification; + +/** + * This implementation gives access to every person to every charger. + * + * @author Sebastian Hörl (sebhoerl), IRT SystemX + */ +public class AnyChargerAccess implements ChargerAccess { + @Override + public boolean hasAccess(Person person, Charger charger) { + return true; + } + + @Override + public boolean hasAccess(Person person, ChargerSpecification charger) { + return true; + } +} diff --git a/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/access/AttributeBasedChargerAccess.java b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/access/AttributeBasedChargerAccess.java new file mode 100644 index 00000000000..c69d5f4c4cb --- /dev/null +++ b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/access/AttributeBasedChargerAccess.java @@ -0,0 +1,41 @@ +package org.matsim.contrib.ev.strategic.access; + +import java.util.Set; + +import org.matsim.api.core.v01.population.Person; +import org.matsim.contrib.ev.infrastructure.Charger; +import org.matsim.contrib.ev.infrastructure.ChargerSpecification; + +import com.google.common.collect.Sets; + +/** + * This implementation checks the subscriptions of each person and the required + * subscriptions to access the chargers. If a person has the required + * subscription, the charger can be used. + * + * @author Sebastian Hörl (sebhoerl), IRT SystemX + */ +public class AttributeBasedChargerAccess implements ChargerAccess { + private final SubscriptionRegistry subscriptions; + + public AttributeBasedChargerAccess(SubscriptionRegistry subscriptions) { + this.subscriptions = subscriptions; + } + + @Override + public boolean hasAccess(Person person, ChargerSpecification charger) { + Set chargerSubscriptions = subscriptions.getChargerSubscriptions(charger); + + if (chargerSubscriptions.size() == 0) { + return true; + } + + Set personSubscriptions = subscriptions.getPersonSubscriptions(person); + return Sets.union(personSubscriptions, chargerSubscriptions).size() > 0; + } + + @Override + public boolean hasAccess(Person person, Charger charger) { + return hasAccess(person, charger.getSpecification()); + } +} diff --git a/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/access/ChargerAccess.java b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/access/ChargerAccess.java new file mode 100644 index 00000000000..05587e87b20 --- /dev/null +++ b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/access/ChargerAccess.java @@ -0,0 +1,16 @@ +package org.matsim.contrib.ev.strategic.access; + +import org.matsim.api.core.v01.population.Person; +import org.matsim.contrib.ev.infrastructure.Charger; +import org.matsim.contrib.ev.infrastructure.ChargerSpecification; + +/** + * This interface decides whether a person has access to a specific charger. + * + * @author Sebastian Hörl (sebhoerl), IRT SystemX + */ +public interface ChargerAccess { + boolean hasAccess(Person person, Charger charger); + + boolean hasAccess(Person person, ChargerSpecification charger); +} diff --git a/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/access/SubscriptionRegistry.java b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/access/SubscriptionRegistry.java new file mode 100644 index 00000000000..1748ce7addf --- /dev/null +++ b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/access/SubscriptionRegistry.java @@ -0,0 +1,109 @@ +package org.matsim.contrib.ev.strategic.access; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import org.matsim.api.core.v01.IdMap; +import org.matsim.api.core.v01.population.Person; +import org.matsim.contrib.ev.infrastructure.Charger; +import org.matsim.contrib.ev.infrastructure.ChargerSpecification; +import org.matsim.contrib.ev.strategic.StrategicChargingUtils; + +/** + * Utility service which is used to cache which person has which charging + * subscriptions and which charger required which subscriptions. + * + * @author Sebastian Hörl (sebhoerl), IRT SystemX + */ +public class SubscriptionRegistry { + static public final String SUBSCRIPTIONS_ATTRIBUTE = "sevc:subscriptions"; + + private final IdMap> personCache = new IdMap<>(Person.class); + + public Set getPersonSubscriptions(Person person) { + Set subscriptions = personCache.get(person.getId()); + + if (subscriptions == null) { + String rawSubscriptions = (String) person.getAttributes().getAttribute(SUBSCRIPTIONS_ATTRIBUTE); + + if (rawSubscriptions != null) { + subscriptions = new HashSet<>(); + + for (String subscription : rawSubscriptions.split(",")) { + subscriptions.add(subscription.trim()); + } + + if (subscriptions.size() == 1) { + subscriptions = Collections.singleton(subscriptions.iterator().next()); + } + } else { + subscriptions = Collections.emptySet(); + } + + personCache.put(person.getId(), subscriptions); + } + + return subscriptions; + } + + private final IdMap> chargerCache = new IdMap<>(Charger.class); + + public Set getChargerSubscriptions(ChargerSpecification charger) { + Set subscriptions = chargerCache.get(charger.getId()); + + if (subscriptions == null) { + String rawSubscriptions = (String) charger.getAttributes().getAttribute(SUBSCRIPTIONS_ATTRIBUTE); + + if (rawSubscriptions != null) { + subscriptions = new HashSet<>(); + + for (String subscription : rawSubscriptions.split(",")) { + subscriptions.add(subscription.trim()); + } + + if (subscriptions.size() == 1) { + subscriptions = Collections.singleton(subscriptions.iterator().next()); + } + } else { + subscriptions = Collections.emptySet(); + } + + chargerCache.put(charger.getId(), subscriptions); + } + + return subscriptions; + } + + /** + * Adds a subscription for a person + */ + static public void addSubscription(Person person, String subscription) { + Set subscriptions = StrategicChargingUtils.readList(person, SUBSCRIPTIONS_ATTRIBUTE); + subscriptions.add(subscription); + StrategicChargingUtils.writeList(person, SUBSCRIPTIONS_ATTRIBUTE, subscriptions); + } + + /** + * Returns the subscriptions of a person + */ + static public Set getSubscriptions(Person person) { + return StrategicChargingUtils.readList(person, SUBSCRIPTIONS_ATTRIBUTE); + } + + /** + * Adds a required subscription for a charger + */ + static public void addSubscription(ChargerSpecification person, String subscription) { + Set subscriptions = StrategicChargingUtils.readList(person, SUBSCRIPTIONS_ATTRIBUTE); + subscriptions.add(subscription); + StrategicChargingUtils.writeList(person, SUBSCRIPTIONS_ATTRIBUTE, subscriptions); + } + + /** + * Returns the required subscriptions for a charger + */ + static public Set getSubscriptions(ChargerSpecification charger) { + return StrategicChargingUtils.readList(charger, SUBSCRIPTIONS_ATTRIBUTE); + } +} diff --git a/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/analysis/ChargerTypeAnalysisListener.java b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/analysis/ChargerTypeAnalysisListener.java new file mode 100644 index 00000000000..1a81267db75 --- /dev/null +++ b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/analysis/ChargerTypeAnalysisListener.java @@ -0,0 +1,220 @@ +package org.matsim.contrib.ev.strategic.analysis; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.IOException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; + +import org.matsim.api.core.v01.Id; +import org.matsim.api.core.v01.IdMap; +import org.matsim.api.core.v01.IdSet; +import org.matsim.api.core.v01.events.PersonMoneyEvent; +import org.matsim.api.core.v01.events.handler.PersonMoneyEventHandler; +import org.matsim.api.core.v01.population.Person; +import org.matsim.contrib.ev.infrastructure.Charger; +import org.matsim.contrib.ev.infrastructure.ChargerSpecification; +import org.matsim.contrib.ev.infrastructure.ChargingInfrastructureSpecification; +import org.matsim.contrib.ev.strategic.StrategicChargingUtils; +import org.matsim.contrib.ev.strategic.scoring.ChargingPlanScoring; +import org.matsim.contrib.ev.withinday.analysis.WithinDayChargingAnalysisHandler; +import org.matsim.core.api.experimental.events.EventsManager; +import org.matsim.core.controler.OutputDirectoryHierarchy; +import org.matsim.core.controler.events.IterationEndsEvent; +import org.matsim.core.controler.events.IterationStartsEvent; +import org.matsim.core.controler.listener.IterationEndsListener; +import org.matsim.core.controler.listener.IterationStartsListener; +import org.matsim.core.utils.io.IOUtils; + +import com.google.common.util.concurrent.AtomicDouble; + +/** + * Analysis class that writes out high-level indicators for chargers. The + * analysis is performed based on a the sevc:analysisTypes attribute of the + * chargers. All chargers having the same anaylsis type are aggregated in the + * analysis (on the number of users, consumed kWh, ...). + * + * @author Sebastian Hörl (sebhoerl), IRT SystemX + */ +public class ChargerTypeAnalysisListener + implements IterationStartsListener, IterationEndsListener, PersonMoneyEventHandler { + static public final String OUTPUT_PATH = "sevc_analysis.csv"; + static public final String ANALYSIS_TYPE_CHARGER_ATTRIBUTE = "sevc:analysisTypes"; + + private final EventsManager eventsManager; + private final WithinDayChargingAnalysisHandler handler; + private final String outputPath; + + private final IdMap> analysisMap = new IdMap<>(Charger.class); + private final Set analysisTypes = new HashSet<>(); + + public ChargerTypeAnalysisListener(OutputDirectoryHierarchy outputHierarchy, + ChargingInfrastructureSpecification infrastructure, WithinDayChargingAnalysisHandler handler, + EventsManager eventsManager) { + this.outputPath = outputHierarchy.getOutputFilename(OUTPUT_PATH); + this.handler = handler; + this.eventsManager = eventsManager; + + for (ChargerSpecification charger : infrastructure.getChargerSpecifications().values()) { + String rawTypes = (String) charger.getAttributes().getAttribute(ANALYSIS_TYPE_CHARGER_ATTRIBUTE); + Set analysisTypes = new HashSet<>(); + + if (rawTypes != null) { + for (String analysisType : rawTypes.split(",")) { + analysisTypes.add(analysisType.trim()); + this.analysisTypes.add(analysisType.trim()); + } + } + + analysisMap.put(charger.getId(), analysisTypes); + } + } + + static private final List HEADER = Arrays.asList( // + "iteration", // + "analysis_type", // + "successful_attempts", // + "failed_attempts", // + "energy_kWh", // + "charging_duration_min", // + "idle_duration_min", // + "wait_duration_min", // + "revenue", // + "users"); + + private class AnalysisItem { + AtomicInteger successfulAttempts = new AtomicInteger(); + AtomicInteger failedAttempts = new AtomicInteger(); + AtomicDouble energy_kWh = new AtomicDouble(); + AtomicDouble chargingDuration_min = new AtomicDouble(); + AtomicDouble idleDuration_min = new AtomicDouble(); + AtomicDouble waitDuration_min = new AtomicDouble(); + AtomicDouble revenue = new AtomicDouble(); + } + + private IdMap revenueTracker = new IdMap<>(Charger.class); + + @Override + public void notifyIterationStarts(IterationStartsEvent event) { + eventsManager.addHandler(this); + } + + @Override + public void handleEvent(PersonMoneyEvent event) { + if (event.getPurpose().equals(ChargingPlanScoring.MONEY_EVENT_PURPOSE)) { + Id chargerId = Id.create(event.getTransactionPartner(), Charger.class); + revenueTracker.computeIfAbsent(chargerId, id -> new AtomicDouble()).addAndGet(event.getAmount()); + } + } + + @Override + public void notifyIterationEnds(IterationEndsEvent event) { + eventsManager.removeHandler(this); + + try { + boolean writeHeader = !(new File(outputPath).exists()); + BufferedWriter writer = IOUtils.getAppendingBufferedWriter(outputPath); + + if (writeHeader) { + writer.write(String.join(";", HEADER) + "\n"); + } + + Map analysisItems = new HashMap<>(); + Map> users = new HashMap<>(); + + for (String analysisType : analysisTypes) { + analysisItems.put(analysisType, new AnalysisItem()); + users.put(analysisType, new IdSet<>(Person.class)); + } + + for (var attempt : handler.getChargingAttemptItems()) { + for (String analysisType : analysisMap.get(attempt.chargerId())) { + var item = analysisItems.get(analysisType); + + if (attempt.successful()) { + item.successfulAttempts.incrementAndGet(); + users.get(analysisType).add(attempt.personId()); + + double chargingDuration_min = attempt.chargingEndTime() - attempt.chargingStartTime(); + chargingDuration_min /= 60.0; + + double idleDuration_min = attempt.endTime() - attempt.chargingEndTime(); + idleDuration_min /= 60.0; + + double waitDuration_min = attempt.queueingEndTime() - attempt.queueingStartTime(); + waitDuration_min /= 60.0; + + if (Double.isFinite(chargingDuration_min)) { + item.chargingDuration_min.addAndGet(chargingDuration_min); + } + + if (Double.isFinite(idleDuration_min)) { + item.idleDuration_min.addAndGet(idleDuration_min); + } + + if (Double.isFinite(waitDuration_min)) { + item.waitDuration_min.addAndGet(waitDuration_min); + } + + item.energy_kWh.addAndGet(attempt.energy_kWh()); + } else { + item.failedAttempts.incrementAndGet(); + } + } + } + + for (var entry : revenueTracker.entrySet()) { + for (String analysisType : analysisMap.get(entry.getKey())) { + var item = analysisItems.get(analysisType); + item.revenue.addAndGet(entry.getValue().get()); + } + } + + revenueTracker.clear(); + + for (String analysisType : analysisTypes) { + int usersValue = users.get(analysisType).size(); + var item = analysisItems.get(analysisType); + + writer.write(String.join(";", new String[] { + String.valueOf(event.getIteration()), + analysisType, + String.valueOf(item.successfulAttempts.get()), // + String.valueOf(item.failedAttempts.get()), // + String.valueOf(item.energy_kWh.get()), + String.valueOf(item.chargingDuration_min.get()), // + String.valueOf(item.idleDuration_min.get()), // + String.valueOf(item.waitDuration_min.get()), // + String.valueOf(item.revenue.get()), // + String.valueOf(usersValue) // + }) + "\n"); + + } + + writer.close(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * Adds an analysis type to a charger + */ + static public void addAnalysisType(ChargerSpecification charger, String analysisType) { + Set analysisTypes = StrategicChargingUtils.readList(charger, ANALYSIS_TYPE_CHARGER_ATTRIBUTE); + analysisTypes.add(analysisType); + StrategicChargingUtils.writeList(charger, ANALYSIS_TYPE_CHARGER_ATTRIBUTE, analysisTypes); + } + + /** + * Returns the list of analysis types of a charger + */ + static public Set getAnalysisTypes(ChargerSpecification charger) { + return StrategicChargingUtils.readList(charger, ANALYSIS_TYPE_CHARGER_ATTRIBUTE); + } +} diff --git a/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/analysis/ChargingPlanScoringListener.java b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/analysis/ChargingPlanScoringListener.java new file mode 100644 index 00000000000..2e7d830c1af --- /dev/null +++ b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/analysis/ChargingPlanScoringListener.java @@ -0,0 +1,96 @@ +package org.matsim.contrib.ev.strategic.analysis; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.IOException; +import java.util.LinkedList; +import java.util.List; + +import org.matsim.api.core.v01.population.Person; +import org.matsim.api.core.v01.population.Population; +import org.matsim.contrib.ev.strategic.plan.ChargingPlan; +import org.matsim.contrib.ev.strategic.plan.ChargingPlans; +import org.matsim.contrib.ev.withinday.WithinDayEvEngine; +import org.matsim.core.controler.OutputDirectoryHierarchy; +import org.matsim.core.controler.events.IterationEndsEvent; +import org.matsim.core.controler.listener.IterationEndsListener; +import org.matsim.core.utils.io.IOUtils; + +/** + * Analysis class that writes out the minimum, maximum, mean, and selected + * charging scores per selected agent plan. + * + * @author Sebastian Hörl (sebhoerl), IRT SystemX + */ +public class ChargingPlanScoringListener implements IterationEndsListener { + static public final String SCORING_PATH = "sevc_scores.csv"; + + private final Population population; + private final String outputPath; + + public ChargingPlanScoringListener(Population population, OutputDirectoryHierarchy outputHierarchy) { + this.population = population; + this.outputPath = outputHierarchy.getOutputFilename(SCORING_PATH); + } + + @Override + public void notifyIterationEnds(IterationEndsEvent event) { + try { + boolean writeHeader = !(new File(outputPath).exists()); + BufferedWriter writer = IOUtils.getAppendingBufferedWriter(outputPath); + + if (writeHeader) { + List header = new LinkedList<>(); + header.add("iteration"); + header.add("min_mean"); + header.add("max_mean"); + header.add("mean_mean"); + header.add("selected_mean"); + header.add("persons"); + + writer.write(String.join(";", header) + "\n"); + } + + List row = new LinkedList<>(); + row.add(String.valueOf(event.getIteration())); + + int count = 0; + + double min = 0.0; + double max = 0.0; + double mean = 0.0; + double selected = 0.0; + + for (Person person : population.getPersons().values()) { + if (WithinDayEvEngine.isActive(person)) { + ChargingPlans chargingPlans = ChargingPlans.get(person.getSelectedPlan()); + + if (chargingPlans.getChargingPlans().size() > 0) { + min += chargingPlans.getChargingPlans().stream().mapToDouble(ChargingPlan::getScore).min() + .getAsDouble(); + max += chargingPlans.getChargingPlans().stream().mapToDouble(ChargingPlan::getScore).max() + .getAsDouble(); + mean += chargingPlans.getChargingPlans().stream().mapToDouble(ChargingPlan::getScore).average() + .getAsDouble(); + selected += chargingPlans.getSelectedPlan().getScore(); + + count++; + } + } + } + + row.add(String.valueOf(min / count)); + row.add(String.valueOf(max / count)); + row.add(String.valueOf(mean / count)); + row.add(String.valueOf(selected / count)); + row.add(String.valueOf(count)); + + writer.write(String.join(";", row) + "\n"); + + writer.flush(); + writer.close(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/costs/AttributeBasedChargingCostCalculator.java b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/costs/AttributeBasedChargingCostCalculator.java new file mode 100644 index 00000000000..70159421b64 --- /dev/null +++ b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/costs/AttributeBasedChargingCostCalculator.java @@ -0,0 +1,103 @@ +package org.matsim.contrib.ev.strategic.costs; + +import org.matsim.api.core.v01.Id; +import org.matsim.api.core.v01.IdMap; +import org.matsim.api.core.v01.population.Person; +import org.matsim.contrib.ev.EvUnits; +import org.matsim.contrib.ev.infrastructure.Charger; +import org.matsim.contrib.ev.infrastructure.ChargerSpecification; +import org.matsim.contrib.ev.infrastructure.ChargingInfrastructureSpecification; + +/** + * This cost calculator implementation makes use of the charger attributes to + * obtain prices per duration, kWh, etc. + * + * @author Sebastian Hörl (sebhoerl), IRT SystemX + */ +public class AttributeBasedChargingCostCalculator implements ChargingCostCalculator { + static public final String USE_COST_CHARGER_ATTRIBUTE = "secv:costPerUse"; + static public final String ENERGY_COST_CHARGER_ATTRIBUTE = "secv:costPerEnergy_kWh"; + static public final String DURATION_COST_CHARGER_ATTRIBUTE = "secv:costPerDuration_min"; + static public final String BLOCKING_DURATION_COST_CHARGER_ATTRIBUTE = "secv:costPerBlockingDuration_min"; + static public final String BLOCKING_DURATION_CHARGER_ATTRIBUTE = "secv:blockingDuration_min"; + + private final ChargingInfrastructureSpecification infrastructure; + + private final IdMap cache = new IdMap<>(Charger.class); + + private record Item(double use, double duration_min, double energy_kWh, double costPerBlockingDuration_min, + double blockingDuration_min) { + } + + public AttributeBasedChargingCostCalculator(ChargingInfrastructureSpecification infrastructure) { + this.infrastructure = infrastructure; + } + + @Override + public double calculateChargingCost(Id personId, Id chargerId, double duration, double energy) { + Item item = cache.get(chargerId); + if (item == null) { + ChargerSpecification charger = infrastructure.getChargerSpecifications().get(chargerId); + + double costPerUse = getCostPerUse(charger); + double costPerDuration_min = getCostPerDuration_min(charger); + double costPerEnergy_kWh = getCostPerEnergy_kWh(charger); + double costPerBlockingDuration_min = getCostPerBlockingDuration_min(charger); + double blockingDuration_min = getBlockingDuration_min(charger); + + item = new Item(costPerUse, costPerDuration_min, costPerEnergy_kWh, costPerBlockingDuration_min, + blockingDuration_min); + cache.put(chargerId, item); + } + + double blockingDuration_min = Math.max(duration / 60.0 - item.blockingDuration_min, 0.0); + return duration / 60.0 * item.duration_min + blockingDuration_min * item.costPerBlockingDuration_min + + EvUnits.J_to_kWh(energy) * item.energy_kWh + item.use; + } + + /** + * Sets the cost structure for the charger. + */ + static public void setChargingCosts(ChargerSpecification charger, double costPerUse, double costPerEnergy_kWh, + double costPerDuration_kWh) { + setChargingCosts(charger, costPerUse, costPerEnergy_kWh, costPerDuration_kWh, 0.0, 0.0); + } + + /** + * Sets the cost structure for the charger. The blocking costs are charged + * additionally for any duration that exceeds the blocking duration. + */ + static public void setChargingCosts(ChargerSpecification charger, double costPerUse, double costPerEnergy_kWh, + double costPerDuration_min, double costPerBlockingDuration_min, double blockingDuration_min) { + charger.getAttributes().putAttribute(USE_COST_CHARGER_ATTRIBUTE, costPerUse); + charger.getAttributes().putAttribute(ENERGY_COST_CHARGER_ATTRIBUTE, costPerEnergy_kWh); + charger.getAttributes().putAttribute(DURATION_COST_CHARGER_ATTRIBUTE, costPerDuration_min); + charger.getAttributes().putAttribute(BLOCKING_DURATION_COST_CHARGER_ATTRIBUTE, costPerBlockingDuration_min); + charger.getAttributes().putAttribute(BLOCKING_DURATION_CHARGER_ATTRIBUTE, blockingDuration_min); + } + + static public double getCostPerUse(ChargerSpecification charger) { + Double value = (Double) charger.getAttributes().getAttribute(USE_COST_CHARGER_ATTRIBUTE); + return value == null ? 0.0 : value; + } + + static public double getCostPerDuration_min(ChargerSpecification charger) { + Double value = (Double) charger.getAttributes().getAttribute(DURATION_COST_CHARGER_ATTRIBUTE); + return value == null ? 0.0 : value; + } + + static public double getCostPerEnergy_kWh(ChargerSpecification charger) { + Double value = (Double) charger.getAttributes().getAttribute(ENERGY_COST_CHARGER_ATTRIBUTE); + return value == null ? 0.0 : value; + } + + static public double getCostPerBlockingDuration_min(ChargerSpecification charger) { + Double value = (Double) charger.getAttributes().getAttribute(BLOCKING_DURATION_COST_CHARGER_ATTRIBUTE); + return value == null ? 0.0 : value; + } + + static public double getBlockingDuration_min(ChargerSpecification charger) { + Double value = (Double) charger.getAttributes().getAttribute(BLOCKING_DURATION_CHARGER_ATTRIBUTE); + return value == null ? 0.0 : value; + } +} diff --git a/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/costs/AttributeBasedChargingCostsParameters.java b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/costs/AttributeBasedChargingCostsParameters.java new file mode 100644 index 00000000000..455e4736fe3 --- /dev/null +++ b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/costs/AttributeBasedChargingCostsParameters.java @@ -0,0 +1,17 @@ +package org.matsim.contrib.ev.strategic.costs; + +import org.matsim.core.config.ReflectiveConfigGroup; + +/** + * These cost parameters should be set in order to retrieve the charging cost + * structure from the charger attributes. + * + * @author Sebastian Hörl (sebhoerl), IRT SystemX + */ +public class AttributeBasedChargingCostsParameters extends ReflectiveConfigGroup implements ChargingCostsParameters { + static public final String SET_NAME = "costs:attribute_based"; + + public AttributeBasedChargingCostsParameters() { + super(SET_NAME); + } +} diff --git a/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/costs/ChargingCostCalculator.java b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/costs/ChargingCostCalculator.java new file mode 100644 index 00000000000..4f402e091d8 --- /dev/null +++ b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/costs/ChargingCostCalculator.java @@ -0,0 +1,15 @@ +package org.matsim.contrib.ev.strategic.costs; + +import org.matsim.api.core.v01.Id; +import org.matsim.api.core.v01.population.Person; +import org.matsim.contrib.ev.infrastructure.Charger; + +/** + * This interface calculates the cost for charging. + * + * @author Sebastian Hörl (sebhoerl), IRT SystemX + */ +public interface ChargingCostCalculator { + double calculateChargingCost(Id personId, Id charger, double duration, + double energy); +} diff --git a/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/costs/ChargingCostModule.java b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/costs/ChargingCostModule.java new file mode 100644 index 00000000000..5e04c0a1d8b --- /dev/null +++ b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/costs/ChargingCostModule.java @@ -0,0 +1,51 @@ +package org.matsim.contrib.ev.strategic.costs; + +import org.matsim.api.core.v01.population.Population; +import org.matsim.contrib.ev.infrastructure.ChargingInfrastructureSpecification; +import org.matsim.contrib.ev.strategic.StrategicChargingConfigGroup; +import org.matsim.contrib.ev.strategic.access.SubscriptionRegistry; +import org.matsim.core.controler.AbstractModule; + +import com.google.inject.Provides; +import com.google.inject.Singleton; + +public class ChargingCostModule extends AbstractModule { + @Override + public void install() { + StrategicChargingConfigGroup config = StrategicChargingConfigGroup.get(getConfig()); + + if (config != null) { + if (config.costs == null) { + bind(ChargingCostCalculator.class).toInstance(new NoChargingCostCalculator()); + } else if (config.costs instanceof DefaultChargingCostsParameters) { + bind(ChargingCostCalculator.class).to(DefaultChargingCostCalculator.class); + } else if (config.costs instanceof AttributeBasedChargingCostsParameters) { + bind(ChargingCostCalculator.class).to(AttributeBasedChargingCostCalculator.class); + } else if (config.costs instanceof TariffBasedChargingCostsParameters) { + bind(ChargingCostCalculator.class).to(TariffBasedChargingCostCalculator.class); + } + } + } + + @Singleton + @Provides + DefaultChargingCostCalculator provideDefaultChargingCostCalculator(StrategicChargingConfigGroup config) { + return new DefaultChargingCostCalculator((DefaultChargingCostsParameters) config.costs); + } + + @Singleton + @Provides + AttributeBasedChargingCostCalculator provideDefaultChargingCostCalculator( + ChargingInfrastructureSpecification infrastructure) { + return new AttributeBasedChargingCostCalculator(infrastructure); + } + + @Singleton + @Provides + TariffBasedChargingCostCalculator provideTariffBasedChargingCostCalculator( + ChargingInfrastructureSpecification infrastructure, Population population, + SubscriptionRegistry subscriptions, StrategicChargingConfigGroup config) { + TariffBasedChargingCostsParameters parameters = (TariffBasedChargingCostsParameters) config.costs; + return new TariffBasedChargingCostCalculator(parameters, infrastructure, population, subscriptions); + } +} diff --git a/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/costs/ChargingCostsParameters.java b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/costs/ChargingCostsParameters.java new file mode 100644 index 00000000000..ccee975a312 --- /dev/null +++ b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/costs/ChargingCostsParameters.java @@ -0,0 +1,5 @@ +package org.matsim.contrib.ev.strategic.costs; + +public interface ChargingCostsParameters { + // just a tag interface +} diff --git a/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/costs/DefaultChargingCostCalculator.java b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/costs/DefaultChargingCostCalculator.java new file mode 100644 index 00000000000..493bb6cc252 --- /dev/null +++ b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/costs/DefaultChargingCostCalculator.java @@ -0,0 +1,28 @@ +package org.matsim.contrib.ev.strategic.costs; + +import org.matsim.api.core.v01.Id; +import org.matsim.api.core.v01.population.Person; +import org.matsim.contrib.ev.EvUnits; +import org.matsim.contrib.ev.infrastructure.Charger; + +/** + * This cost calculator implementation retrieves the cost structure from the + * configuration. + * + * @author Sebastian Hörl (sebhoerl), IRT SystemX + */ +public class DefaultChargingCostCalculator implements ChargingCostCalculator { + private final DefaultChargingCostsParameters parameters; + + public DefaultChargingCostCalculator(DefaultChargingCostsParameters parameters) { + this.parameters = parameters; + } + + @Override + public double calculateChargingCost(Id personId, Id charger, double duration, double energy) { + double blockingDuration_min = Math.max(duration / 60.0 - parameters.blockingDuration_min, 0.0); + return duration / 60.0 * parameters.costPerDuration_min + + blockingDuration_min * parameters.costPerBlockingDuration_min + + +EvUnits.J_to_kWh(energy) * parameters.costPerEnergy_kWh + parameters.costPerUse; + } +} diff --git a/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/costs/DefaultChargingCostsParameters.java b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/costs/DefaultChargingCostsParameters.java new file mode 100644 index 00000000000..b3bb60b78d8 --- /dev/null +++ b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/costs/DefaultChargingCostsParameters.java @@ -0,0 +1,32 @@ +package org.matsim.contrib.ev.strategic.costs; + +import org.matsim.core.config.ReflectiveConfigGroup; + +/** + * Set these cost parameters to set the charging cost struture globally for all + * chargers based on the configuration. + * + * @author Sebastian Hörl (sebhoerl), IRT SystemX + */ +public class DefaultChargingCostsParameters extends ReflectiveConfigGroup implements ChargingCostsParameters { + static public final String SET_NAME = "costs:default"; + + public DefaultChargingCostsParameters() { + super(SET_NAME); + } + + @Parameter + public double costPerUse = 0.0; + + @Parameter + public double costPerDuration_min = 0.0; + + @Parameter + public double costPerEnergy_kWh = 0.0; + + @Parameter + public double costPerBlockingDuration_min = 0.0; + + @Parameter + public double blockingDuration_min = 0.0; +} diff --git a/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/costs/NoChargingCostCalculator.java b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/costs/NoChargingCostCalculator.java new file mode 100644 index 00000000000..ec94d121a93 --- /dev/null +++ b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/costs/NoChargingCostCalculator.java @@ -0,0 +1,17 @@ +package org.matsim.contrib.ev.strategic.costs; + +import org.matsim.api.core.v01.Id; +import org.matsim.api.core.v01.population.Person; +import org.matsim.contrib.ev.infrastructure.Charger; + +/** + * This cost calculator implementation always returns zero costs. + * + * @author Sebastian Hörl (sebhoerl), IRT SystemX + */ +public class NoChargingCostCalculator implements ChargingCostCalculator { + @Override + public double calculateChargingCost(Id personId, Id charger, double duration, double energy) { + return 0; + } +} diff --git a/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/costs/TariffBasedChargingCostCalculator.java b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/costs/TariffBasedChargingCostCalculator.java new file mode 100644 index 00000000000..83cf6b0a5dd --- /dev/null +++ b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/costs/TariffBasedChargingCostCalculator.java @@ -0,0 +1,124 @@ +package org.matsim.contrib.ev.strategic.costs; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.matsim.api.core.v01.Id; +import org.matsim.api.core.v01.IdMap; +import org.matsim.api.core.v01.population.Person; +import org.matsim.api.core.v01.population.Population; +import org.matsim.contrib.ev.EvUnits; +import org.matsim.contrib.ev.infrastructure.Charger; +import org.matsim.contrib.ev.infrastructure.ChargerSpecification; +import org.matsim.contrib.ev.infrastructure.ChargingInfrastructureSpecification; +import org.matsim.contrib.ev.strategic.StrategicChargingUtils; +import org.matsim.contrib.ev.strategic.access.SubscriptionRegistry; +import org.matsim.contrib.ev.strategic.costs.TariffBasedChargingCostsParameters.TariffParameters; + +import com.google.common.base.Preconditions; +import com.google.common.collect.Sets; + +/** + * This cost calculator implementation calculates charging costs based on a + * configured list of tariffs. Each charger can have one or more tariffs, which, + * in turn, are either available to everybody or only to persons with specific + * subscriptions. Among the tariffs available at a charger, a person will always + * choose the cheapest available option. + * + * @author Sebastian Hörl (sebhoerl), IRT SystemX + */ +public class TariffBasedChargingCostCalculator implements ChargingCostCalculator { + static public final String TARIFFS_CHARGER_ATTRIBUTE = "sevc:tariffs"; + + private final Map parameters; + private final IdMap> cache = new IdMap<>(Charger.class); + + private final Population population; + private final ChargingInfrastructureSpecification infrastructure; + private final SubscriptionRegistry subscriptions; + + public TariffBasedChargingCostCalculator(TariffBasedChargingCostsParameters parameters, + ChargingInfrastructureSpecification infrastructure, Population population, + SubscriptionRegistry subscriptions) { + this.parameters = parameters.getTariffParameters(); + this.infrastructure = infrastructure; + this.subscriptions = subscriptions; + this.population = population; + } + + @Override + public double calculateChargingCost(Id personId, Id chargerId, double duration, double energy) { + Person person = population.getPersons().get(personId); + + List tariffs = cache.get(chargerId); + if (tariffs == null) { + tariffs = getChargerTariffs(chargerId); + cache.put(chargerId, tariffs); + } + + double best = Double.POSITIVE_INFINITY; + for (TariffParameters tariff : tariffs) { + if (tariff.subscriptions.size() == 0 || Sets + .intersection(tariff.subscriptions, subscriptions.getPersonSubscriptions(person)).size() > 0) { + double blockingDuration_min = Math.max(duration / 60.0 - tariff.blockingDuration_min, 0.0); + + best = Math.min(best, + duration / 60.0 * tariff.costPerDuration_min + + blockingDuration_min * tariff.costPerBlockingDuration_min + + +EvUnits.J_to_kWh(energy) * tariff.costPerEnergy_kWh + tariff.costPerUse); + } + } + + Preconditions.checkState(Double.isFinite(best), + String.format("No viable tariff found for person %s at charger %s", personId, chargerId)); + + return best; + } + + private List getChargerTariffs(Id chargerId) { + String raw = (String) infrastructure.getChargerSpecifications().get(chargerId).getAttributes() + .getAttribute(TARIFFS_CHARGER_ATTRIBUTE); + + if (raw == null) { + return Collections.emptyList(); + } else { + List tariffs = new ArrayList<>(); + + for (String name : raw.split(",")) { + TariffParameters tariff = parameters.get(name.trim()); + + if (tariff == null) { + throw new IllegalStateException( + String.format("Tariff %s of charger %s has not been defined", name, chargerId.toString())); + } else { + tariffs.add(tariff); + } + } + + if (tariffs.size() == 0) { + return Collections.emptyList(); + } else { + return tariffs; + } + } + } + + /** + * Adds a tariff to a charger. + */ + static public void addTariff(ChargerSpecification charger, String tariff) { + Set tariffs = StrategicChargingUtils.readList(charger, TARIFFS_CHARGER_ATTRIBUTE); + tariffs.add(tariff); + StrategicChargingUtils.writeList(charger, TARIFFS_CHARGER_ATTRIBUTE, tariffs); + } + + /** + * Retrieve the list of tariffs for a charger. + */ + static public Set getTariffs(ChargerSpecification charger) { + return StrategicChargingUtils.readList(charger, TARIFFS_CHARGER_ATTRIBUTE); + } +} diff --git a/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/costs/TariffBasedChargingCostsParameters.java b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/costs/TariffBasedChargingCostsParameters.java new file mode 100644 index 00000000000..550a5d23337 --- /dev/null +++ b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/costs/TariffBasedChargingCostsParameters.java @@ -0,0 +1,77 @@ +package org.matsim.contrib.ev.strategic.costs; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.matsim.core.config.ConfigGroup; +import org.matsim.core.config.ReflectiveConfigGroup; + +import jakarta.validation.constraints.NotEmpty; + +/** + * When this parameter set is selected to set up the costs, charging costs are + * calculated per tariff at the chargers. Each charger can have one or more + * tariffs, which, in turn, are either available to everybody or only to persons + * with specific subscriptions. Among the tariffs available at a charger, a + * person will always choose the cheapest available option. + * + * Each tariff can be added as an individual parameter set of type + * TariffParameters. + * + * @author Sebastian Hörl (sebhoerl), IRT SystemX + */ +public class TariffBasedChargingCostsParameters extends ReflectiveConfigGroup implements ChargingCostsParameters { + static public final String SET_NAME = "costs:tariff_based"; + + public TariffBasedChargingCostsParameters() { + super(SET_NAME); + } + + /** + * This parameter set describes a tariff for a charger. A list of subscriptions + * can be defined so that only persons with the respective subscription can make + * use of the tariff. + */ + static public class TariffParameters extends ReflectiveConfigGroup { + static public final String SET_NAME = "tariff"; + + public TariffParameters() { + super(SET_NAME); + } + + @Parameter + @NotEmpty + public String name; + + @Parameter + public Set subscriptions = new HashSet<>(); + + @Parameter + public double costPerUse = 0.0; + + @Parameter + public double costPerDuration_min = 0.0; + + @Parameter + public double costPerEnergy_kWh = 0.0; + + @Parameter + public double costPerBlockingDuration_min = 0.0; + + @Parameter + public double blockingDuration_min = 0.0; + } + + public Map getTariffParameters() { + Map tariffs = new HashMap<>(); + + for (ConfigGroup item : getParameterSets(TariffParameters.SET_NAME)) { + TariffParameters tariff = (TariffParameters) item; + tariffs.put(tariff.name, tariff); + } + + return tariffs; + } +} diff --git a/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/infrastructure/AbstractChargerProviderModule.java b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/infrastructure/AbstractChargerProviderModule.java new file mode 100644 index 00000000000..d8b442cb502 --- /dev/null +++ b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/infrastructure/AbstractChargerProviderModule.java @@ -0,0 +1,25 @@ +package org.matsim.contrib.ev.strategic.infrastructure; + +import org.matsim.core.controler.AbstractModule; + +import com.google.inject.binder.LinkedBindingBuilder; +import com.google.inject.multibindings.Multibinder; + +/** + * This class is used to bind new charger sources for startegic charging. + */ +public abstract class AbstractChargerProviderModule extends AbstractModule { + private Multibinder chargerProviderBinder; + + @Override + public void install() { + this.chargerProviderBinder = Multibinder.newSetBinder(binder(), ChargerProvider.class); + configureChargingStrategies(); + } + + protected final LinkedBindingBuilder bindChargerProvider() { + return chargerProviderBinder.addBinding(); + } + + abstract protected void configureChargingStrategies(); +} diff --git a/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/infrastructure/ChargerProvider.java b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/infrastructure/ChargerProvider.java new file mode 100644 index 00000000000..e3f82834133 --- /dev/null +++ b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/infrastructure/ChargerProvider.java @@ -0,0 +1,47 @@ +package org.matsim.contrib.ev.strategic.infrastructure; + +import java.util.Collection; + +import org.matsim.api.core.v01.population.Activity; +import org.matsim.api.core.v01.population.Leg; +import org.matsim.api.core.v01.population.Person; +import org.matsim.api.core.v01.population.Plan; +import org.matsim.contrib.ev.infrastructure.ChargerSpecification; + +/** + * The charger provider interface contains the functionality to identify viable + * chargers for a potential charging process. + * + * @author Sebastian Hörl (sebhoerl), IRT SystemX + */ +public interface ChargerProvider { + /** + * Return relevant chargers that can be used for the given charging request and + * the given person. + */ + Collection findChargers(Person person, Plan plan, ChargerRequest request); + + /** + * This class represents a request that is made to a ChargingProvider. A + * charging request can either be for a leg-based charging slot (along a ride) + * or an activity-based charging slot (when an agent chargers during a sequence + * of activities). + */ + public record ChargerRequest(Activity startActivity, Activity endActivity, Leg leg, double duration) { + public boolean isLegBased() { + return leg != null; + } + + public ChargerRequest(Activity startActivity, Activity endActivity) { + this(startActivity, endActivity, null, 0.0); + } + + public ChargerRequest(Leg leg, double duration) { + this(null, null, leg, duration); + } + + public ChargerRequest(Leg leg) { + this(null, null, leg, Double.NaN); + } + } +} diff --git a/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/infrastructure/CompositeChargerProvider.java b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/infrastructure/CompositeChargerProvider.java new file mode 100644 index 00000000000..89dc760dd40 --- /dev/null +++ b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/infrastructure/CompositeChargerProvider.java @@ -0,0 +1,34 @@ +package org.matsim.contrib.ev.strategic.infrastructure; + +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; + +import org.matsim.api.core.v01.population.Person; +import org.matsim.api.core.v01.population.Plan; +import org.matsim.contrib.ev.infrastructure.ChargerSpecification; + +/** + * An implementation of the ChargerProvider that delegates to several other + * providers. + * + * @author Sebastian Hörl (sebhoerl), IRT SystemX + */ +public class CompositeChargerProvider implements ChargerProvider { + private final Collection delegates; + + public CompositeChargerProvider(Collection delegates) { + this.delegates = delegates; + } + + @Override + public Collection findChargers(Person person, Plan plan, ChargerRequest request) { + List chargers = new LinkedList<>(); + + for (ChargerProvider delegate : delegates) { + chargers.addAll(delegate.findChargers(person, plan, request)); + } + + return chargers; + } +} diff --git a/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/infrastructure/DefaultChargerProvidersModule.java b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/infrastructure/DefaultChargerProvidersModule.java new file mode 100644 index 00000000000..7f8016048aa --- /dev/null +++ b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/infrastructure/DefaultChargerProvidersModule.java @@ -0,0 +1,53 @@ +package org.matsim.contrib.ev.strategic.infrastructure; + +import java.util.Set; + +import org.matsim.api.core.v01.Scenario; +import org.matsim.contrib.ev.infrastructure.ChargingInfrastructureSpecification; +import org.matsim.contrib.ev.strategic.StrategicChargingConfigGroup; +import org.matsim.contrib.ev.strategic.access.ChargerAccess; + +import com.google.inject.Provides; +import com.google.inject.Singleton; + +/** + * This module configures the standard ChargerProviders that come with the + * package. + * + * @author Sebastian Hörl (sebhoerl), IRT SystemX + */ +public class DefaultChargerProvidersModule extends AbstractChargerProviderModule { + @Override + protected void configureChargingStrategies() { + bind(ChargerProvider.class).to(CompositeChargerProvider.class); + + bindChargerProvider().to(PersonChargerProvider.class); + bindChargerProvider().to(FacilityChargerProvider.class); + bindChargerProvider().to(PublicChargerProvider.class); + } + + @Provides + @Singleton + CompositeChargerProvider provideChargerProvider(Set delegates) { + return new CompositeChargerProvider(delegates); + } + + @Provides + public PersonChargerProvider provideHomeChargerProvider(ChargingInfrastructureSpecification infrastructure, + StrategicChargingConfigGroup config, Scenario scenario, ChargerAccess access) { + return PersonChargerProvider.build(infrastructure, config.chargerSearchRadius, scenario, access); + } + + @Provides + public FacilityChargerProvider provideWorkChargerProvider(ChargingInfrastructureSpecification infrastructure, + StrategicChargingConfigGroup config, Scenario scenario, ChargerAccess access) { + return FacilityChargerProvider.build(infrastructure, config.chargerSearchRadius, scenario, access); + } + + @Provides + public PublicChargerProvider providePublicChargerProvider(Scenario scenario, + ChargingInfrastructureSpecification infrastructure, StrategicChargingConfigGroup config, + ChargerAccess access) { + return PublicChargerProvider.create(scenario, infrastructure, access, config.chargerSearchRadius); + } +} diff --git a/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/infrastructure/FacilityChargerProvider.java b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/infrastructure/FacilityChargerProvider.java new file mode 100644 index 00000000000..b9384235dcb --- /dev/null +++ b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/infrastructure/FacilityChargerProvider.java @@ -0,0 +1,130 @@ +package org.matsim.contrib.ev.strategic.infrastructure; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import org.matsim.api.core.v01.Coord; +import org.matsim.api.core.v01.Id; +import org.matsim.api.core.v01.IdMap; +import org.matsim.api.core.v01.Scenario; +import org.matsim.api.core.v01.population.Person; +import org.matsim.api.core.v01.population.Plan; +import org.matsim.contrib.ev.infrastructure.ChargerSpecification; +import org.matsim.contrib.ev.infrastructure.ChargingInfrastructureSpecification; +import org.matsim.contrib.ev.strategic.access.ChargerAccess; +import org.matsim.core.population.PopulationUtils; +import org.matsim.core.utils.geometry.CoordUtils; +import org.matsim.facilities.ActivityFacility; + +/** + * This charger provider examines the activity at which an activity-based + * charging slot is potentially supposed to happen. It keeps a list of + * facilities to which chargers are assigned and returns the chargers that are + * assigned to the facility at which the charging activity is taking place. The + * standard selection criteria (person charger access; search radius) are + * evaluated. + * + * Chargers can be assigned to facilities by setting the sevc:facilities + * attribute of the charger, which should contain a list of facility IDs. + * + * @author Sebastian Hörl (sebhoerl), IRT SystemX + */ +public class FacilityChargerProvider implements ChargerProvider { + static public final String FACILITIES_CHARGER_ATTRIBUTE = "sevc:facilities"; + + private final IdMap> chargers; + + private final double searchRadius; + private final Scenario scenario; + + private final ChargerAccess access; + + public FacilityChargerProvider(IdMap> chargers, double searchRadius, + Scenario scenario, ChargerAccess access) { + this.chargers = chargers; + this.searchRadius = searchRadius; + this.scenario = scenario; + this.access = access; + } + + @Override + public Collection findChargers(Person person, Plan plan, ChargerRequest request) { + if (!request.isLegBased()) { + List candidates = new ArrayList<>( + chargers.getOrDefault(request.startActivity().getFacilityId(), Collections.emptyList())); + + Coord activityLocation = PopulationUtils.decideOnCoordForActivity(request.startActivity(), + scenario); + + candidates.removeIf(charger -> { + return !access.hasAccess(person, charger); + }); + + candidates.removeIf(charger -> { + Coord chargerLocation = scenario.getNetwork().getLinks().get(charger.getLinkId()).getCoord(); + double distance = CoordUtils.calcEuclideanDistance(activityLocation, chargerLocation); + return distance > searchRadius; + }); + + return candidates; + } + + return Collections.emptySet(); + } + + static public FacilityChargerProvider build(ChargingInfrastructureSpecification infrastructure, double searchRadius, + Scenario scenario, ChargerAccess access) { + IdMap> chargers = new IdMap<>(ActivityFacility.class); + + for (ChargerSpecification charger : infrastructure.getChargerSpecifications().values()) { + String raw = (String) charger.getAttributes().getAttribute(FACILITIES_CHARGER_ATTRIBUTE); + + if (raw != null) { + for (String rawFacilityId : raw.split(",")) { + Id facilityId = Id.create(rawFacilityId.trim(), ActivityFacility.class); + chargers.computeIfAbsent(facilityId, id -> new LinkedList<>()).add(charger); + } + } + } + + return new FacilityChargerProvider(chargers, searchRadius, scenario, access); + } + + /** + * Returns whether a charger is assigned to at least one facility. + */ + static public boolean isFacilityCharger(ChargerSpecification charger) { + return getFacilityIds(charger).size() > 0; + } + + /** + * Sets the facilities to which a charger is assigned. + */ + static public void setFacilityIds(ChargerSpecification charger, Collection> facilityIds) { + charger.getAttributes().putAttribute(FACILITIES_CHARGER_ATTRIBUTE, + facilityIds.stream().map(Id::toString).collect(Collectors.joining(","))); + } + + /** + * Returns the facilities to which a charger is assigned. + */ + static public Set> getFacilityIds(ChargerSpecification charger) { + String raw = (String) charger.getAttributes().getAttribute(FACILITIES_CHARGER_ATTRIBUTE); + Set> facilityIds = new HashSet<>(); + + if (raw != null) { + for (String rawFacilityIds : raw.split(",")) { + Id facilityId = Id.create(rawFacilityIds.trim(), ActivityFacility.class); + facilityIds.add(facilityId); + } + } + + return facilityIds; + } +} diff --git a/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/infrastructure/PersonChargerProvider.java b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/infrastructure/PersonChargerProvider.java new file mode 100644 index 00000000000..cdbbd981b17 --- /dev/null +++ b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/infrastructure/PersonChargerProvider.java @@ -0,0 +1,123 @@ +package org.matsim.contrib.ev.strategic.infrastructure; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import org.matsim.api.core.v01.Coord; +import org.matsim.api.core.v01.Id; +import org.matsim.api.core.v01.IdMap; +import org.matsim.api.core.v01.Scenario; +import org.matsim.api.core.v01.population.Person; +import org.matsim.api.core.v01.population.Plan; +import org.matsim.contrib.ev.infrastructure.ChargerSpecification; +import org.matsim.contrib.ev.infrastructure.ChargingInfrastructureSpecification; +import org.matsim.contrib.ev.strategic.access.ChargerAccess; +import org.matsim.core.population.PopulationUtils; +import org.matsim.core.utils.geometry.CoordUtils; + +/** + * This charger provider keeps a registry of chargers that are assigned to a + * specific person. Whenever the person examines charging locations and the + * standard conditions (subscriptions, search radius) are fulfilled, the + * respective chargers are returned for that person. + * + * Chargers can be assigned to persons by setting the sevc:persons + * attribute of the charger, which should contain a list of person IDs. + * + * @author Sebastian Hörl (sebhoerl), IRT SystemX + */ +public class PersonChargerProvider implements ChargerProvider { + static public final String PERSONS_CHARGER_ATTRIBUTE = "sevc:persons"; + + private final IdMap> chargers; + + private final Scenario scenario; + private final double searchRadius; + + private final ChargerAccess access; + + public PersonChargerProvider(IdMap> chargers, double searchRadius, + Scenario scenario, + ChargerAccess access) { + this.chargers = chargers; + this.searchRadius = searchRadius; + this.scenario = scenario; + this.access = access; + } + + @Override + public Collection findChargers(Person person, Plan plan, ChargerRequest request) { + if (!request.isLegBased()) { + List candidates = new ArrayList<>( + chargers.getOrDefault(person.getId(), Collections.emptyList())); + + Coord activityLocation = PopulationUtils.decideOnCoordForActivity(request.startActivity(), + scenario); + + candidates.removeIf(charger -> { + return !access.hasAccess(person, charger); + }); + + candidates.removeIf(charger -> { + Coord chargerLocation = scenario.getNetwork().getLinks().get(charger.getLinkId()).getCoord(); + double distance = CoordUtils.calcEuclideanDistance(activityLocation, chargerLocation); + return distance > searchRadius; + }); + + return candidates; + } + + return Collections.emptySet(); + } + + static public PersonChargerProvider build(ChargingInfrastructureSpecification infrastructure, double searchRadius, + Scenario scenario, ChargerAccess access) { + IdMap> chargers = new IdMap<>(Person.class); + + for (ChargerSpecification charger : infrastructure.getChargerSpecifications().values()) { + for (Id personId : getPersonIds(charger)) { + chargers.computeIfAbsent(personId, id -> new LinkedList<>()).add(charger); + } + } + + return new PersonChargerProvider(chargers, searchRadius, scenario, access); + } + + /** + * Returns whether a charger is assigned to at least one person. + */ + static public boolean isPersonCharger(ChargerSpecification charger) { + return getPersonIds(charger).size() > 0; + } + + /** + * Sets the persons to which a charger is assigned. + */ + static public void setPersonIds(ChargerSpecification charger, Collection> personIds) { + charger.getAttributes().putAttribute(PERSONS_CHARGER_ATTRIBUTE, + personIds.stream().map(Id::toString).collect(Collectors.joining(","))); + } + + /** + * Returns the persons to which a charger is assigned. + */ + static public Set> getPersonIds(ChargerSpecification charger) { + String raw = (String) charger.getAttributes().getAttribute(PERSONS_CHARGER_ATTRIBUTE); + Set> personIds = new HashSet<>(); + + if (raw != null) { + for (String rawPersonId : raw.split(",")) { + Id personId = Id.createPersonId(rawPersonId.trim()); + personIds.add(personId); + } + } + + return personIds; + } +} diff --git a/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/infrastructure/PublicChargerProvider.java b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/infrastructure/PublicChargerProvider.java new file mode 100644 index 00000000000..3b6983f56ad --- /dev/null +++ b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/infrastructure/PublicChargerProvider.java @@ -0,0 +1,92 @@ +package org.matsim.contrib.ev.strategic.infrastructure; + +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; + +import org.matsim.api.core.v01.Coord; +import org.matsim.api.core.v01.Scenario; +import org.matsim.api.core.v01.network.Link; +import org.matsim.api.core.v01.network.Network; +import org.matsim.api.core.v01.population.Person; +import org.matsim.api.core.v01.population.Plan; +import org.matsim.contrib.ev.infrastructure.ChargerSpecification; +import org.matsim.contrib.ev.infrastructure.ChargingInfrastructureSpecification; +import org.matsim.contrib.ev.strategic.access.ChargerAccess; +import org.matsim.core.population.PopulationUtils; +import org.matsim.core.utils.collections.QuadTree; +import org.matsim.core.utils.collections.QuadTrees; + +/** + * This charger provider keeps a spatial index of chargers that are tagged as + * "public". When searching for a charger, it returns candidates within the + * configured search radius and based on the subscriptions of the + * charger/persons. + * + * A charger can be tagged as public by setting its sevc:public charger + * attribute to true. + * + * @author Sebastian Hörl (sebhoerl), IRT SystemX + */ +public class PublicChargerProvider implements ChargerProvider { + static public final String PUBLIC_CHARGER_ATTRIBUTE = "sevc:public"; + + private final Scenario scenario; + private final QuadTree index; + private final double radius; + + private final ChargerAccess access; + + PublicChargerProvider(Scenario scenario, QuadTree index, ChargerAccess access, + double radius) { + this.scenario = scenario; + this.index = index; + this.radius = radius; + this.access = access; + } + + @Override + public Collection findChargers(Person person, Plan plan, ChargerRequest request) { + Coord location = request.isLegBased() + ? scenario.getNetwork().getLinks().get(request.leg().getRoute().getEndLinkId()).getCoord() + : PopulationUtils.decideOnCoordForActivity(request.startActivity(), scenario); + + return index.getDisk(location.getX(), location.getY(), radius).stream() + .filter(charger -> access.hasAccess(person, charger)).toList(); + } + + static public PublicChargerProvider create(Scenario scenario, ChargingInfrastructureSpecification infrastrcuture, + ChargerAccess access, double radius) { + Network network = scenario.getNetwork(); + List chargers = new LinkedList<>(); + + for (ChargerSpecification charger : infrastrcuture.getChargerSpecifications().values()) { + Boolean isPublic = (Boolean) charger.getAttributes().getAttribute(PUBLIC_CHARGER_ATTRIBUTE); + + if (isPublic != null && isPublic) { + chargers.add(charger); + } + } + + return new PublicChargerProvider(scenario, + chargers.isEmpty() ? new QuadTree<>(0.0, 0.0, 0.0, 0.0) : QuadTrees.createQuadTree(chargers, c -> { + Link link = network.getLinks().get(c.getLinkId()); + return link.getCoord(); + }, 0.0), access, radius); + } + + /** + * Return whether a charger is public. + */ + static public boolean isPublicCharger(ChargerSpecification charger) { + Boolean isPublic = (Boolean) charger.getAttributes().getAttribute(PUBLIC_CHARGER_ATTRIBUTE); + return isPublic != null && isPublic; + } + + /** + * Sets a charge public or not. + */ + static public void setPublic(ChargerSpecification charger, boolean isPublic) { + charger.getAttributes().putAttribute(PUBLIC_CHARGER_ATTRIBUTE, isPublic); + } +} diff --git a/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/plan/ChargingPlan.java b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/plan/ChargingPlan.java new file mode 100644 index 00000000000..669ac0a794d --- /dev/null +++ b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/plan/ChargingPlan.java @@ -0,0 +1,52 @@ +package org.matsim.contrib.ev.strategic.plan; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * This class represents a charging plan. It allows to track the score of a + * charging plan and indicates the individual charging activities that are to be + * implemented throghout the day. + * + * @author Sebastian Hörl (sebhoerl), IRT SystemX + */ +public class ChargingPlan { + @JsonProperty("activities") + private final List activities = new ArrayList<>(); + + @JsonProperty("score") + private double score = Double.NaN; + + public void setScore(double score) { + this.score = score; + } + + public double getScore() { + return score; + } + + @JsonIgnore + public List getChargingActivities() { + return Collections.unmodifiableList(activities); + } + + public void addChargingActivity(ChargingPlanActivity activity) { + activities.add(activity); + } + + ChargingPlan createCopy() { + ChargingPlan copyPlan = new ChargingPlan(); + copyPlan.setScore(score); + + for (ChargingPlanActivity activity : activities) { + ChargingPlanActivity copyActivity = activity.createCopy(); + copyPlan.addChargingActivity(copyActivity); + } + + return copyPlan; + } +} diff --git a/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/plan/ChargingPlanActivity.java b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/plan/ChargingPlanActivity.java new file mode 100644 index 00000000000..9902e6ebb37 --- /dev/null +++ b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/plan/ChargingPlanActivity.java @@ -0,0 +1,128 @@ +package org.matsim.contrib.ev.strategic.plan; + +import java.io.IOException; + +import org.matsim.api.core.v01.Id; +import org.matsim.contrib.ev.infrastructure.Charger; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JacksonException; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.google.common.base.Preconditions; + +/** + * This class represents the individual charging activities that are to be + * implemented throughout a day. A charging activity can either be leg-based in + * which case the leg along which the agent intends to charge is saved. + * Alternatively, a charging activity can be activity-based in which case the + * activity sequence during which the vehicle is charged is given. + * + * @author Sebastian Hörl (sebhoerl), IRT SystemX + */ +public class ChargingPlanActivity { + @JsonProperty + private int startActivityIndex = -1; + + @JsonProperty + private int endActivityIndex = -1; + + @JsonProperty + private int followingActivityIndex = -1; + + @JsonProperty + private double duration = 0.0; + + @JsonProperty + @JsonSerialize(using = IdSerializer.class) + @JsonDeserialize(using = IdDeserializer.class) + private Id chargerId; + + ChargingPlanActivity() { + } + + public ChargingPlanActivity(int startActivityIndex, int endActivityIndex, Id chargerId) { + Preconditions.checkArgument(startActivityIndex >= 0); + Preconditions.checkArgument(endActivityIndex >= startActivityIndex); + Preconditions.checkNotNull(chargerId); + + this.startActivityIndex = startActivityIndex; + this.endActivityIndex = endActivityIndex; + this.chargerId = chargerId; + } + + public ChargingPlanActivity(int followingActivityIndex, double duration, Id chargerId) { + Preconditions.checkArgument(followingActivityIndex >= 0); + Preconditions.checkArgument(duration > 0.0); + Preconditions.checkNotNull(chargerId); + + this.followingActivityIndex = followingActivityIndex; + this.duration = duration; + this.chargerId = chargerId; + } + + public int getStartActivityIndex() { + return startActivityIndex; + } + + public int getEndActivityIndex() { + return endActivityIndex; + } + + public int getFollowingActivityIndex() { + return followingActivityIndex; + } + + public double getDuration() { + return duration; + } + + public Id getChargerId() { + return chargerId; + } + + // Convenience accessors + + @JsonIgnore + public boolean isEnroute() { + return followingActivityIndex >= 0; + } + + // (de)serialization + + static public class IdSerializer extends JsonSerializer> { + @Override + public void serialize(Id value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeString(value.toString()); + } + } + + static public class IdDeserializer extends JsonDeserializer> { + @Override + public Id deserialize(JsonParser p, DeserializationContext context) + throws IOException, JacksonException { + return Id.create(p.readValueAs(String.class), Charger.class); + } + } + + // Copy + + ChargingPlanActivity createCopy() { + ChargingPlanActivity copy = new ChargingPlanActivity(); + + copy.startActivityIndex = startActivityIndex; + copy.endActivityIndex = endActivityIndex; + copy.followingActivityIndex = followingActivityIndex; + copy.duration = duration; + copy.chargerId = chargerId; + + return copy; + } +} diff --git a/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/plan/ChargingPlans.java b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/plan/ChargingPlans.java new file mode 100644 index 00000000000..cb3d275d622 --- /dev/null +++ b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/plan/ChargingPlans.java @@ -0,0 +1,121 @@ +package org.matsim.contrib.ev.strategic.plan; + +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; + +import org.matsim.api.core.v01.population.Plan; +import org.matsim.contrib.ev.withinday.WithinDayEvEngine; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.Preconditions; +import com.google.common.base.Verify; + +/** + * This class is a container for the charging plans that an agent may accumulate + * during charging replaninng. Each regular MATSim plan contains one of these + * objects indicating the underlying charging plans. + * + * @author Sebastian Hörl (sebhoerl), IRT SystemX + */ +public class ChargingPlans { + static public final String ATTRIBUTE = "charging"; + + @JsonProperty("plans") + private final List chargingPlans = new LinkedList<>(); + + @JsonProperty("selected") + private int selectedIndex = -1; + + @JsonIgnore + private Plan ownerPlan = null; + + public List getChargingPlans() { + return Collections.unmodifiableList(chargingPlans); + } + + public void addChargingPlan(ChargingPlan chargingPlan) { + if (selectedIndex == -1) { + selectedIndex = 0; + } + + chargingPlans.add(chargingPlan); + } + + public void removeChargingPlan(ChargingPlan chargingPlan) { + int removeIndex = chargingPlans.indexOf(chargingPlan); + + if (removeIndex >= 0) { + if (removeIndex == selectedIndex) { + selectedIndex = -1; + } else if (removeIndex < selectedIndex) { + selectedIndex--; + } + + chargingPlans.remove(removeIndex); + } + } + + public void setSelectedPlan(ChargingPlan chargingPlan) { + selectedIndex = chargingPlans.indexOf(chargingPlan); + Verify.verify(selectedIndex >= 0, "Plan does not exist"); + } + + @JsonIgnore + public ChargingPlan getSelectedPlan() { + return selectedIndex >= 0 ? chargingPlans.get(selectedIndex) : null; + } + + private ChargingPlans createCopy() { + ChargingPlans copyPlans = new ChargingPlans(); + + for (ChargingPlan plan : chargingPlans) { + ChargingPlan copyPlan = plan.createCopy(); + copyPlans.addChargingPlan(copyPlan); + + if (getSelectedPlan() == plan) { + copyPlans.setSelectedPlan(copyPlan); + } + } + + return copyPlans; + } + + static public ChargingPlans get(Plan plan) { + /* + * The regular replanning proces copies plans, including their attributes. + * However, this means that the reference to the ChargingPlans object is copied + * when a new regular plan is created. So the ChargingPlans of the new regular + * plan will point to the same object as for the initial plan. However, we want + * that a completely new object is used. Therefore, plans should always be + * obtained using the present function. Besides retrieving the attribute, it + * checks the ownerPlan variable that indicates the regular plan to which a + * ChargingPlans belongs. If a ChargingPlans object is retrieved from a regular + * plan, but it doesn't indicate that regular plan as its "ownerPlan", a deep + * copy of the ChargingPlans object is created and assigned to the regular plan + * in question. + */ + Preconditions.checkState(WithinDayEvEngine.isActive(plan.getPerson()), + "Attempting to obtain charging plans for an agent that is not enabled."); + + ChargingPlans chargingPlans = (ChargingPlans) plan.getAttributes().getAttribute(ChargingPlans.ATTRIBUTE); + + if (chargingPlans == null) { + chargingPlans = new ChargingPlans(); + plan.getAttributes().putAttribute(ChargingPlans.ATTRIBUTE, chargingPlans); + chargingPlans.ownerPlan = plan; + } + + // manage deep copy of charging plans after creating new regular plans + if (chargingPlans.ownerPlan == null) { + chargingPlans.ownerPlan = plan; + } else if (chargingPlans.ownerPlan != plan) { + chargingPlans = chargingPlans.createCopy(); + plan.getAttributes().putAttribute(ChargingPlans.ATTRIBUTE, chargingPlans); + chargingPlans.ownerPlan = plan; + } + + return chargingPlans; + } +} diff --git a/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/plan/ChargingPlansConverter.java b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/plan/ChargingPlansConverter.java new file mode 100644 index 00000000000..efbf2102bfd --- /dev/null +++ b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/plan/ChargingPlansConverter.java @@ -0,0 +1,35 @@ +package org.matsim.contrib.ev.strategic.plan; + +import java.io.IOException; + +import org.matsim.utils.objectattributes.AttributeConverter; + +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * This class is used to serialize and deserialize the charging plans of an + * agent which are saved as an attribute of regular plans. + * + * @author Sebastian Hörl (sebhoerl), IRT SystemX + */ +public class ChargingPlansConverter implements AttributeConverter { + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Override + public ChargingPlans convert(String value) { + try { + return objectMapper.readValue(value, ChargingPlans.class); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public String convertToString(Object o) { + try { + return objectMapper.writeValueAsString((ChargingPlans) o); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/replanning/StrategicChargingReplanningAlgorithm.java b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/replanning/StrategicChargingReplanningAlgorithm.java new file mode 100644 index 00000000000..ed8b665c7ce --- /dev/null +++ b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/replanning/StrategicChargingReplanningAlgorithm.java @@ -0,0 +1,62 @@ +package org.matsim.contrib.ev.strategic.replanning; + +import java.util.Random; + +import org.matsim.api.core.v01.population.Plan; +import org.matsim.contrib.ev.strategic.plan.ChargingPlan; +import org.matsim.contrib.ev.strategic.plan.ChargingPlans; +import org.matsim.contrib.ev.strategic.replanning.innovator.ChargingPlanInnovator; +import org.matsim.contrib.ev.strategic.replanning.selector.ChargingPlanSelector; +import org.matsim.contrib.ev.withinday.WithinDayEvEngine; +import org.matsim.core.gbl.MatsimRandom; +import org.matsim.core.population.algorithms.PlanAlgorithm; + +/** + * This class is a PlanAlgorithm that manages the selection and innovation of + * charging plans for a regular MATSim plan. + * + * @author Sebastian Hörl (sebhoerl), IRT SystemX + */ +public class StrategicChargingReplanningAlgorithm implements PlanAlgorithm { + private final ChargingPlanSelector selector; + private final ChargingPlanInnovator creator; + + private final Random random; + private final double selectionProbability; + private final int maximumPlans; + + public StrategicChargingReplanningAlgorithm(ChargingPlanSelector selector, ChargingPlanInnovator creator, + double selectionProbability, int maximumPlans) { + this.selector = selector; + this.creator = creator; + this.random = MatsimRandom.getLocalInstance(); + this.selectionProbability = selectionProbability; + this.maximumPlans = maximumPlans; + } + + @Override + public void run(Plan plan) { + if (WithinDayEvEngine.isActive(plan.getPerson())) { + ChargingPlans chargingPlans = ChargingPlans.get(plan); + + if (chargingPlans.getChargingPlans().size() > 0 && random.nextDouble() <= selectionProbability) { + chargingPlans.setSelectedPlan(selector.select(plan.getPerson(), plan, chargingPlans)); + } else { + while (chargingPlans.getChargingPlans().size() >= maximumPlans) { + removeWorst(chargingPlans); + } + + ChargingPlan chargingPlan = creator.createChargingPlan(plan.getPerson(), plan, chargingPlans); + + chargingPlans.addChargingPlan(chargingPlan); + chargingPlans.setSelectedPlan(chargingPlan); + } + } + } + + private void removeWorst(ChargingPlans chargingPlans) { + ChargingPlan removal = chargingPlans.getChargingPlans().stream() + .sorted((a, b) -> Double.compare(a.getScore(), b.getScore())).findFirst().get(); + chargingPlans.removeChargingPlan(removal); + } +} diff --git a/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/replanning/StrategicChargingReplanningModule.java b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/replanning/StrategicChargingReplanningModule.java new file mode 100644 index 00000000000..c8a7192d4b8 --- /dev/null +++ b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/replanning/StrategicChargingReplanningModule.java @@ -0,0 +1,27 @@ +package org.matsim.contrib.ev.strategic.replanning; + +import org.matsim.core.config.groups.GlobalConfigGroup; +import org.matsim.core.population.algorithms.PlanAlgorithm; +import org.matsim.core.replanning.modules.AbstractMultithreadedModule; + +import com.google.inject.Provider; + +/** + * This class registers the replanning strategy for strategic charging. + * + * @author Sebastian Hörl (sebhoerl), IRT SystemX + */ +public class StrategicChargingReplanningModule extends AbstractMultithreadedModule { + private final Provider algorithmProvider; + + StrategicChargingReplanningModule(GlobalConfigGroup globalConfigGroup, + Provider algorithmProvider) { + super(globalConfigGroup); + this.algorithmProvider = algorithmProvider; + } + + @Override + public PlanAlgorithm getPlanAlgoInstance() { + return algorithmProvider.get(); + } +} diff --git a/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/replanning/StrategicChargingReplanningStrategy.java b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/replanning/StrategicChargingReplanningStrategy.java new file mode 100644 index 00000000000..11a8bc86b62 --- /dev/null +++ b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/replanning/StrategicChargingReplanningStrategy.java @@ -0,0 +1,34 @@ +package org.matsim.contrib.ev.strategic.replanning; + +import org.matsim.core.config.groups.GlobalConfigGroup; +import org.matsim.core.replanning.PlanStrategy; +import org.matsim.core.replanning.PlanStrategyImpl; +import org.matsim.core.replanning.PlanStrategyImpl.Builder; +import org.matsim.core.replanning.selectors.RandomPlanSelector; + +import com.google.inject.Provider; + +/** + * This class registers the replanning strategy for strategic charging. + * + * @author Sebastian Hörl (sebhoerl), IRT SystemX + */ +public class StrategicChargingReplanningStrategy implements Provider { + static public final String STRATEGY = "strategic_charging"; + + private final GlobalConfigGroup globalConfigGroup; + private final Provider algorithmProvider; + + public StrategicChargingReplanningStrategy(GlobalConfigGroup globalConfigGroup, + Provider algorithmProvider) { + this.globalConfigGroup = globalConfigGroup; + this.algorithmProvider = algorithmProvider; + } + + @Override + public PlanStrategy get() { + PlanStrategyImpl.Builder builder = new Builder(new RandomPlanSelector<>()); + builder.addStrategyModule(new StrategicChargingReplanningModule(globalConfigGroup, algorithmProvider)); + return builder.build(); + } +} diff --git a/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/replanning/innovator/ChargingPlanInnovator.java b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/replanning/innovator/ChargingPlanInnovator.java new file mode 100644 index 00000000000..ed5d0c2a31f --- /dev/null +++ b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/replanning/innovator/ChargingPlanInnovator.java @@ -0,0 +1,17 @@ +package org.matsim.contrib.ev.strategic.replanning.innovator; + +import org.matsim.api.core.v01.population.Person; +import org.matsim.api.core.v01.population.Plan; +import org.matsim.contrib.ev.strategic.plan.ChargingPlan; +import org.matsim.contrib.ev.strategic.plan.ChargingPlans; + +/** + * This interface represents an innovator for charging plans. It obtains the + * current charging plan (or null if none) and returns a new charging plan that + * is updated or created from scratch. + * + * @author Sebastian Hörl (sebhoerl), IRT SystemX + */ +public interface ChargingPlanInnovator { + ChargingPlan createChargingPlan(Person person, Plan plan, ChargingPlans chargingPlans); +} diff --git a/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/replanning/innovator/EmptyChargingPlanInnovator.java b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/replanning/innovator/EmptyChargingPlanInnovator.java new file mode 100644 index 00000000000..1c33b539be9 --- /dev/null +++ b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/replanning/innovator/EmptyChargingPlanInnovator.java @@ -0,0 +1,18 @@ +package org.matsim.contrib.ev.strategic.replanning.innovator; + +import org.matsim.api.core.v01.population.Person; +import org.matsim.api.core.v01.population.Plan; +import org.matsim.contrib.ev.strategic.plan.ChargingPlan; +import org.matsim.contrib.ev.strategic.plan.ChargingPlans; + +/** + * This implementation creates empty charging plans (= no charging at all). + * + * @author Sebastian Hörl (sebhoerl), IRT SystemX + */ +public class EmptyChargingPlanInnovator implements ChargingPlanInnovator { + @Override + public ChargingPlan createChargingPlan(Person person, Plan plan, ChargingPlans chargingPlans) { + return new ChargingPlan(); + } +} diff --git a/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/replanning/innovator/RandomChargingPlanInnovator.java b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/replanning/innovator/RandomChargingPlanInnovator.java new file mode 100644 index 00000000000..381f4fab2d5 --- /dev/null +++ b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/replanning/innovator/RandomChargingPlanInnovator.java @@ -0,0 +1,170 @@ +package org.matsim.contrib.ev.strategic.replanning.innovator; + +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Random; + +import org.matsim.api.core.v01.population.Activity; +import org.matsim.api.core.v01.population.Person; +import org.matsim.api.core.v01.population.Plan; +import org.matsim.api.core.v01.population.PlanElement; +import org.matsim.contrib.ev.infrastructure.ChargerSpecification; +import org.matsim.contrib.ev.strategic.StrategicChargingConfigGroup; +import org.matsim.contrib.ev.strategic.infrastructure.ChargerProvider; +import org.matsim.contrib.ev.strategic.infrastructure.ChargerProvider.ChargerRequest; +import org.matsim.contrib.ev.strategic.plan.ChargingPlan; +import org.matsim.contrib.ev.strategic.plan.ChargingPlanActivity; +import org.matsim.contrib.ev.strategic.plan.ChargingPlans; +import org.matsim.contrib.ev.withinday.ChargingSlotFinder; +import org.matsim.contrib.ev.withinday.ChargingSlotFinder.ActivityBasedCandidate; +import org.matsim.contrib.ev.withinday.ChargingSlotFinder.LegBasedCandidate; +import org.matsim.core.gbl.MatsimRandom; +import org.matsim.core.router.TripStructureUtils; +import org.matsim.core.router.TripStructureUtils.StageActivityHandling; +import org.matsim.core.utils.timing.TimeInterpretation; +import org.matsim.core.utils.timing.TimeTracker; + +/** + * This is the current default implementation of the charging plan innovator. It + * traverses an agent's regular plan and finds all potential slots where the + * agent may charge along a leg or during a sequence of activities. For each of + * the viable slots, a charging activity is created with probability 50%. For + * the generated slots, the ChargingProvider is then used to find a viable + * charger. Those charging plans are created from scratch, i.e., there is no + * dependence on the existing charging plan. + * + * @author Sebastian Hörl (sebhoerl), IRT SystemX + */ +public class RandomChargingPlanInnovator implements ChargingPlanInnovator { + private final ChargerProvider chargerProvider; + private final ChargingSlotFinder candidateFinder; + private final TimeInterpretation timeInterpretation; + + private final Random random; + + private final double minimumActivityChargingDuration; + private final double maximumActivityChargingDuration; + + private final double minimumEnrouteDriveTime; + private final double minimumEnrouteChargingDuration; + private final double maximumEnrouteChargingDuration; + + public RandomChargingPlanInnovator(ChargerProvider chargerProvider, ChargingSlotFinder candidateFinder, + TimeInterpretation timeInterpretation, StrategicChargingConfigGroup config) { + this.chargerProvider = chargerProvider; + this.timeInterpretation = timeInterpretation; + this.minimumActivityChargingDuration = config.minimumActivityChargingDuration; + this.maximumActivityChargingDuration = config.maximumActivityChargingDuration; + this.minimumEnrouteDriveTime = config.minimumEnrouteDriveTime; + this.minimumEnrouteChargingDuration = config.minimumEnrouteChargingDuration; + this.maximumEnrouteChargingDuration = config.maximumEnrouteChargingDuration; + this.candidateFinder = candidateFinder; + this.random = MatsimRandom.getLocalInstance(); + } + + @Override + public ChargingPlan createChargingPlan(Person person, Plan plan, ChargingPlans chargingPlans) { + ChargingPlan chargingPlan = new ChargingPlan(); + + // set up some lookups + List activities = TripStructureUtils.getActivities(plan.getPlanElements(), + StageActivityHandling.ExcludeStageActivities); + Map startTimes = new HashMap<>(); + Map endTimes = new HashMap<>(); + + TimeTracker timeTracker = new TimeTracker(timeInterpretation); + for (PlanElement element : plan.getPlanElements()) { + double startTime = timeTracker.getTime().seconds(); + timeTracker.addElement(element); + + if (element instanceof Activity activity) { + if (!TripStructureUtils.isStageActivityType(activity.getType())) { + if (activity == activities.get(0)) { + startTime = Double.NEGATIVE_INFINITY; + } + + final double endTime; + if (activity == activities.get(activities.size() - 1)) { + endTime = Double.POSITIVE_INFINITY; + } else { + endTime = timeTracker.getTime().orElse(Double.POSITIVE_INFINITY); + } + + startTimes.put(activity, startTime); + endTimes.put(activity, endTime); + } + } + } + + // first, select activity-based slots + List activityBased = candidateFinder.findActivityBased(person, plan); + + // remove slots that are too short + activityBased.removeIf(candidate -> { + double duration = endTimes.get(candidate.endActivity()) - startTimes.get(candidate.startActivity()); + return duration < minimumActivityChargingDuration || duration > maximumActivityChargingDuration; + }); + + // track which ones are selected + List selectedActivityBased = new LinkedList<>(); + + // construct activities + for (ActivityBasedCandidate candidate : activityBased) { + if (random.nextBoolean()) { + // find chargers + List chargers = new LinkedList<>( + chargerProvider.findChargers(plan.getPerson(), plan, + new ChargerRequest(candidate.startActivity(), candidate.endActivity()))); + + if (chargers.size() > 0) { + ChargerSpecification charger = chargers.get(random.nextInt(chargers.size())); + + int startActivityIndex = activities.indexOf(candidate.startActivity()); + int endActivityIndex = activities.indexOf(candidate.endActivity()); + + chargingPlan.addChargingActivity(new ChargingPlanActivity(startActivityIndex, endActivityIndex, + charger.getId())); + + selectedActivityBased.add(candidate); + } + } + } + + // second, find leg-based slots + List legBased = candidateFinder.findLegBased(person, plan); + + // remove if too short + legBased.removeIf(candidate -> { + return candidate.leg().getTravelTime().seconds() < minimumEnrouteDriveTime; + }); + + // reduce slots that are incompatible with the selected activity-based ones + candidateFinder.reduceLegBased(legBased, selectedActivityBased, plan.getPlanElements()); + + // construct legs + for (LegBasedCandidate candidate : legBased) { + if (random.nextBoolean()) { + double duration = minimumEnrouteChargingDuration; + duration += (maximumEnrouteChargingDuration - minimumEnrouteChargingDuration) * random.nextDouble(); + + // find chargers + List chargers = new LinkedList<>( + chargerProvider.findChargers(plan.getPerson(), plan, + new ChargerRequest(candidate.leg(), duration))); + + if (chargers.size() > 0) { + ChargerSpecification charger = chargers.get(random.nextInt(chargers.size())); + int followingActivityIndex = activities.indexOf(candidate.followingActivity()); + + chargingPlan.addChargingActivity(new ChargingPlanActivity(followingActivityIndex, + duration, + charger.getId())); + } + } + } + + return chargingPlan; + } +} diff --git a/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/replanning/selector/BestChargingPlanSelector.java b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/replanning/selector/BestChargingPlanSelector.java new file mode 100644 index 00000000000..237d8f52318 --- /dev/null +++ b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/replanning/selector/BestChargingPlanSelector.java @@ -0,0 +1,20 @@ +package org.matsim.contrib.ev.strategic.replanning.selector; + +import org.matsim.api.core.v01.population.Person; +import org.matsim.api.core.v01.population.Plan; +import org.matsim.contrib.ev.strategic.plan.ChargingPlan; +import org.matsim.contrib.ev.strategic.plan.ChargingPlans; + +/** + * This selector implementation selects the charging plan that has achieved the + * highest charging score from the charging plan memory of the agent. + * + * @author Sebastian Hörl (sebhoerl), IRT SystemX + */ +public class BestChargingPlanSelector implements ChargingPlanSelector { + @Override + public ChargingPlan select(Person person, Plan plan, ChargingPlans chargingPlans) { + return chargingPlans.getChargingPlans().stream().sorted((a, b) -> -Double.compare(a.getScore(), b.getScore())) + .findFirst().get(); + } +} diff --git a/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/replanning/selector/ChargingPlanSelector.java b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/replanning/selector/ChargingPlanSelector.java new file mode 100644 index 00000000000..10a0187c697 --- /dev/null +++ b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/replanning/selector/ChargingPlanSelector.java @@ -0,0 +1,16 @@ +package org.matsim.contrib.ev.strategic.replanning.selector; + +import org.matsim.api.core.v01.population.Person; +import org.matsim.api.core.v01.population.Plan; +import org.matsim.contrib.ev.strategic.plan.ChargingPlan; +import org.matsim.contrib.ev.strategic.plan.ChargingPlans; + +/** + * This interface looks at the current memory of charging plans of an agent and + * selects one among the existing ones. + * + * @author Sebastian Hörl (sebhoerl), IRT SystemX + */ +public interface ChargingPlanSelector { + ChargingPlan select(Person person, Plan plan, ChargingPlans chargingPlans); +} diff --git a/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/replanning/selector/ExponentialChargingPlanSelector.java b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/replanning/selector/ExponentialChargingPlanSelector.java new file mode 100644 index 00000000000..f15268803ad --- /dev/null +++ b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/replanning/selector/ExponentialChargingPlanSelector.java @@ -0,0 +1,51 @@ +package org.matsim.contrib.ev.strategic.replanning.selector; + +import java.util.Random; + +import org.matsim.api.core.v01.population.Person; +import org.matsim.api.core.v01.population.Plan; +import org.matsim.contrib.common.util.WeightedRandomSelection; +import org.matsim.contrib.ev.strategic.plan.ChargingPlan; +import org.matsim.contrib.ev.strategic.plan.ChargingPlans; +import org.matsim.core.gbl.MatsimRandom; + +/** + * This selector implementation selects an existing charging plan from an + * agent's charging plan memory based on the plans' scores and the logit + * formula. Plans with a higher score, will, hence, be selected with higher + * probability. + * + * @author Sebastian Hörl (sebhoerl), IRT SystemX + */ +public class ExponentialChargingPlanSelector implements ChargingPlanSelector { + private final double beta; + private final Random random; + + public ExponentialChargingPlanSelector(double beta) { + this.beta = beta; + this.random = MatsimRandom.getLocalInstance(); + } + + @Override + public ChargingPlan select(Person person, Plan plan, ChargingPlans chargingPlans) { + double maximum = Double.NEGATIVE_INFINITY; + + for (ChargingPlan chargingPlan : chargingPlans.getChargingPlans()) { + if (Double.isFinite(chargingPlan.getScore())) { + maximum = Math.max(maximum, chargingPlan.getScore()); + } + } + + if (Double.isFinite(maximum)) { + WeightedRandomSelection selector = new WeightedRandomSelection<>(random); + + for (ChargingPlan chargingPlan : chargingPlans.getChargingPlans()) { + selector.add(chargingPlan, Math.exp(beta * (chargingPlan.getScore() - maximum))); + } + + return selector.select(); + } else { + return new RandomChargingPlanSelector().select(person, plan, chargingPlans); + } + } +} diff --git a/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/replanning/selector/RandomChargingPlanSelector.java b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/replanning/selector/RandomChargingPlanSelector.java new file mode 100644 index 00000000000..d030ba877a3 --- /dev/null +++ b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/replanning/selector/RandomChargingPlanSelector.java @@ -0,0 +1,29 @@ +package org.matsim.contrib.ev.strategic.replanning.selector; + +import java.util.Random; + +import org.matsim.api.core.v01.population.Person; +import org.matsim.api.core.v01.population.Plan; +import org.matsim.contrib.ev.strategic.plan.ChargingPlan; +import org.matsim.contrib.ev.strategic.plan.ChargingPlans; +import org.matsim.core.gbl.MatsimRandom; + +/** + * This selector implementation selects a random charging plan from the charging + * plan memory of the agent. + * + * @author Sebastian Hörl (sebhoerl), IRT SystemX + */ +public class RandomChargingPlanSelector implements ChargingPlanSelector { + private final Random random; + + public RandomChargingPlanSelector() { + this.random = MatsimRandom.getLocalInstance(); + } + + @Override + public ChargingPlan select(Person person, Plan plan, ChargingPlans chargingPlans) { + int planIndex = random.nextInt(chargingPlans.getChargingPlans().size()); + return chargingPlans.getChargingPlans().get(planIndex); + } +} diff --git a/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/scoring/ChargingPlanScoring.java b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/scoring/ChargingPlanScoring.java new file mode 100644 index 00000000000..b795eb3aca0 --- /dev/null +++ b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/scoring/ChargingPlanScoring.java @@ -0,0 +1,555 @@ +package org.matsim.contrib.ev.strategic.scoring; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; + +import org.matsim.api.core.v01.Id; +import org.matsim.api.core.v01.IdMap; +import org.matsim.api.core.v01.events.LinkEnterEvent; +import org.matsim.api.core.v01.events.LinkLeaveEvent; +import org.matsim.api.core.v01.events.PersonMoneyEvent; +import org.matsim.api.core.v01.events.VehicleLeavesTrafficEvent; +import org.matsim.api.core.v01.events.handler.LinkEnterEventHandler; +import org.matsim.api.core.v01.events.handler.LinkLeaveEventHandler; +import org.matsim.api.core.v01.events.handler.VehicleLeavesTrafficEventHandler; +import org.matsim.api.core.v01.network.Network; +import org.matsim.api.core.v01.population.Leg; +import org.matsim.api.core.v01.population.Person; +import org.matsim.api.core.v01.population.Population; +import org.matsim.contrib.ev.charging.ChargingEndEvent; +import org.matsim.contrib.ev.charging.ChargingEndEventHandler; +import org.matsim.contrib.ev.charging.ChargingStartEvent; +import org.matsim.contrib.ev.charging.ChargingStartEventHandler; +import org.matsim.contrib.ev.charging.EnergyChargedEvent; +import org.matsim.contrib.ev.charging.EnergyChargedEventHandler; +import org.matsim.contrib.ev.charging.QueuedAtChargerEvent; +import org.matsim.contrib.ev.charging.QueuedAtChargerEventHandler; +import org.matsim.contrib.ev.charging.QuitQueueAtChargerEvent; +import org.matsim.contrib.ev.charging.QuitQueueAtChargerEventHandler; +import org.matsim.contrib.ev.discharging.DrivingEnergyConsumptionEvent; +import org.matsim.contrib.ev.discharging.DrivingEnergyConsumptionEventHandler; +import org.matsim.contrib.ev.discharging.IdlingEnergyConsumptionEvent; +import org.matsim.contrib.ev.discharging.IdlingEnergyConsumptionEventHandler; +import org.matsim.contrib.ev.fleet.ElectricFleetSpecification; +import org.matsim.contrib.ev.fleet.ElectricVehicleSpecification; +import org.matsim.contrib.ev.infrastructure.Charger; +import org.matsim.contrib.ev.strategic.costs.ChargingCostCalculator; +import org.matsim.contrib.ev.strategic.plan.ChargingPlans; +import org.matsim.contrib.ev.withinday.WithinDayEvEngine; +import org.matsim.contrib.ev.withinday.events.AbortChargingAttemptEvent; +import org.matsim.contrib.ev.withinday.events.AbortChargingAttemptEventHandler; +import org.matsim.contrib.ev.withinday.events.AbortChargingProcessEvent; +import org.matsim.contrib.ev.withinday.events.AbortChargingProcessEventHandler; +import org.matsim.core.api.experimental.events.EventsManager; +import org.matsim.core.controler.events.IterationStartsEvent; +import org.matsim.core.controler.events.ScoringEvent; +import org.matsim.core.controler.listener.IterationStartsListener; +import org.matsim.core.controler.listener.ScoringListener; +import org.matsim.core.mobsim.qsim.InternalInterface; +import org.matsim.core.mobsim.qsim.interfaces.MobsimEngine; +import org.matsim.core.router.TripStructureUtils; +import org.matsim.vehicles.Vehicle; +import org.matsim.vehicles.VehicleUtils; + +import com.google.common.util.concurrent.AtomicDouble; + +/** + * This class manages the scoring of charging plans. See the documentation of + * the package or the respective ChargingPlanScoringParameters for more + * information on the individaul scoring dimensions. + * + * @author Sebastian Hörl (sebhoerl), IRT SystemX + */ +public class ChargingPlanScoring implements IterationStartsListener, ScoringListener, AbortChargingProcessEventHandler, + AbortChargingAttemptEventHandler, DrivingEnergyConsumptionEventHandler, IdlingEnergyConsumptionEventHandler, + ChargingEndEventHandler, QueuedAtChargerEventHandler, QuitQueueAtChargerEventHandler, ChargingStartEventHandler, + LinkEnterEventHandler, LinkLeaveEventHandler, VehicleLeavesTrafficEventHandler, EnergyChargedEventHandler, + MobsimEngine { + static public final String MINIMUM_SOC_PERSON_ATTRIBUTE = "sevc:minimumSoc"; + static public final String MINIMUM_END_SOC_PERSON_ATTRIBUTE = "sevc:minimumEndSoc"; + + static public final String MONEY_EVENT_PURPOSE = "strategic charging"; + + private final EventsManager eventsManager; + + private final Population population; + private final Network network; + private final ElectricFleetSpecification fleet; + + private final ChargingPlanScoringParameters parameters; + private final ChargingCostCalculator costCalculator; + + private final String chargingMode; + + private final ScoringTracker tracker; + + public ChargingPlanScoring(EventsManager eventsManager, Population population, Network network, + ElectricFleetSpecification fleet, ChargingCostCalculator costCalculator, + ChargingPlanScoringParameters parameters, String chargingMode, ScoringTracker tracker) { + this.eventsManager = eventsManager; + this.population = population; + this.network = network; + this.fleet = fleet; + this.costCalculator = costCalculator; + this.parameters = parameters; + this.chargingMode = chargingMode; + this.tracker = tracker; + + initializePersons(); + } + + // PERSONS for vehicles: vehicle-related scoring is added to the score of the + // owning person + + private final List activePersons = new ArrayList<>(); + private final IdMap> vehiclePersons = new IdMap<>(Vehicle.class); + + private void initializePersons() { + for (Person person : population.getPersons().values()) { + if (WithinDayEvEngine.isActive(person) && VehicleUtils.hasVehicleId(person, chargingMode)) { + Id vehicleId = VehicleUtils.getVehicleId(person, chargingMode); + Id previousId = vehiclePersons.put(vehicleId, person.getId()); + activePersons.add(person); + + if (previousId != null) { + throw new IllegalStateException( + "Vehicle " + vehicleId + " is attached to multiple persons (at least " + person.getId() + + " and " + previousId + ")"); + } + } + } + } + + private Id getPerson(Id vehicleId) { + return vehiclePersons.get(vehicleId); + } + + // SCORING handling + + private final IdMap scores = new IdMap<>(Person.class); + private boolean finalized = false; + + @Override + public void notifyIterationStarts(IterationStartsEvent event) { + tracker.start(event.getIteration(), event.isLastIteration()); + + // scoring per day starts here + + initializeEnergy(); + initializeCosts(); + initializeWaiting(); + initializeDetours(); + + finalized = false; + scores.clear(); + + for (Person person : activePersons) { + scores.put(person.getId(), new AtomicDouble(0.0)); + } + + eventsManager.addHandler(this); + } + + private void addScoreForPerson(Id personId, double score) { + if (score != 0.0) { + AtomicDouble item = scores.get(personId); + + if (item != null) { + item.addAndGet(score); + } + } + } + + private void trackScoreForPerson(double time, Id personId, String dimension, double score, Double value) { + if (score != 0.0) { + tracker.trackScore(time, personId, dimension, score, value); + } + } + + private void addScoreForVehicle(Id vehicleId, double score) { + Id personId = getPerson(vehicleId); + + if (personId != null) { + addScoreForPerson(personId, score); + } + } + + private void trackScoreForVehicle(double time, Id vehicleId, String dimension, double score, + Double value) { + Id personId = getPerson(vehicleId); + + if (personId != null) { + trackScoreForPerson(time, personId, dimension, score, value); + } + } + + void finalizeScoring() { + // this may be called by standard scoring or directly by notifyScoring, + // depending + // on whether charging scoring is fed back to standard scoring + + if (finalized) { + return; + } + + finalized = true; + + // scoring per day ends here + + finalizeSoc(simStepTime); + finalizeDetours(simStepTime); + + eventsManager.removeHandler(this); + tracker.finish(); + + for (Person person : activePersons) { + AtomicDouble score = scores.get(person.getId()); + ChargingPlans chargingPlans = ChargingPlans.get(person.getSelectedPlan()); + + if (chargingPlans.getChargingPlans().size() > 0) { + chargingPlans.getSelectedPlan().setScore(score.get()); + } + } + } + + @Override + public void notifyScoring(ScoringEvent event) { + finalizeScoring(); + } + + double getScore(Id personId) { + return scores.getOrDefault(personId, new AtomicDouble(0.0)).get(); + } + + // SOC TRACKING + + private class EnergyEntry { + double total; + double current; + } + + private final IdMap energy = new IdMap<>(Vehicle.class); + private final IdMap minimumSoc = new IdMap<>(Person.class); + + private void initializeEnergy() { + energy.clear(); + minimumSoc.clear(); + + for (ElectricVehicleSpecification vehicle : fleet.getVehicleSpecifications().values()) { + EnergyEntry entry = new EnergyEntry(); + entry.total = vehicle.getBatteryCapacity(); + entry.current = vehicle.getInitialCharge(); + energy.put(vehicle.getId(), entry); + } + + for (Person person : activePersons) { + Double minimumSoc = getMinimumSoc(person); + + if (minimumSoc != null) { + this.minimumSoc.put(person.getId(), minimumSoc); + } + } + } + + private void handleEnergy(double now, Id vehicleId, double endCharge) { + EnergyEntry entry = this.energy.get(vehicleId); + double initialSoc = entry.current / entry.total; + + if (entry != null) { + entry.current = endCharge; + } + + double finalSoc = entry.current / entry.total; + handleChangeSoc(now, vehicleId, initialSoc, finalSoc); + } + + @Override + public void handleEvent(IdlingEnergyConsumptionEvent event) { + handleEnergy(event.getTime(), event.getVehicleId(), event.getEndCharge()); + } + + @Override + public void handleEvent(DrivingEnergyConsumptionEvent event) { + handleEnergy(event.getTime(), event.getVehicleId(), event.getEndCharge()); + } + + @Override + public void handleEvent(EnergyChargedEvent event) { + handleEnergy(event.getTime(), event.getVehicleId(), event.getEndCharge()); + } + + @Override + public void handleEvent(ChargingEndEvent event) { + handleEnergy(event.getTime(), event.getVehicleId(), event.getCharge()); + handleFinishCharging(event); + } + + // SOC: handle soc-related scoring + + private void handleChangeSoc(double now, Id vehicleId, double initialSoc, double finalSoc) { + if (parameters.zeroSoc != 0.0) { + if (initialSoc > 0.0 && finalSoc <= 0.0) { + addScoreForVehicle(vehicleId, parameters.zeroSoc); + trackScoreForVehicle(now, vehicleId, "zero_soc", parameters.zeroSoc, null); + } + } + + if (parameters.belowMinimumSoc != 0.0) { + Id personId = getPerson(vehicleId); + if (personId != null && minimumSoc.containsKey(personId)) { + double personMinimumSoc = minimumSoc.get(personId); + + if (initialSoc >= personMinimumSoc && finalSoc < personMinimumSoc) { + addScoreForPerson(personId, parameters.belowMinimumSoc); + trackScoreForPerson(now, personId, "minimum_soc", parameters.belowMinimumSoc, finalSoc); + } + } + } + } + + private void finalizeSoc(double now) { + if (parameters.belowMinimumEndSoc != 0.0) { + for (Person person : activePersons) { + Double minimumEndOfDaySoc = getMinimumEndSoc(person); + + if (minimumEndOfDaySoc != null) { + Id vehicleId = VehicleUtils.getVehicleId(person, chargingMode); + + if (energy.containsKey(vehicleId)) { + EnergyEntry entry = energy.get(vehicleId); + + if (entry.current / entry.total < minimumEndOfDaySoc) { + addScoreForPerson(person.getId(), parameters.belowMinimumEndSoc); + trackScoreForPerson(now, person.getId(), "minimum_end_soc", parameters.belowMinimumEndSoc, + null); + } + } + } + } + } + } + + // CHARGING PROCESS: handle failed attempts + + @Override + public void handleEvent(AbortChargingAttemptEvent event) { + // handles an unsuccessful charging attempt, but the agent tries another one + addScoreForPerson(event.getPersonId(), parameters.failedChargingAttempt); + trackScoreForPerson(event.getTime(), event.getPersonId(), "failed_attempt", parameters.failedChargingAttempt, + null); + } + + @Override + public void handleEvent(AbortChargingProcessEvent event) { + // handles an unsuccessful charging process after trying several chargers + addScoreForPerson(event.getPersonId(), parameters.failedChargingProcess); + trackScoreForPerson(event.getTime(), event.getPersonId(), "failed_process", parameters.failedChargingProcess, + null); + } + + // WAITING scoring + + private final IdMap enterQueueTimes = new IdMap<>(Vehicle.class); + + private void initializeWaiting() { + enterQueueTimes.clear(); + } + + @Override + public void handleEvent(QueuedAtChargerEvent event) { + enterQueueTimes.put(event.getVehicleId(), event.getTime()); + } + + private void handleQuitQueue(Id vehicleId, double quitTime) { + Double enterTime = enterQueueTimes.remove(vehicleId); + + if (enterTime != null && parameters.waitTime_min != 0.0) { + double waitTime_min = (quitTime - enterTime) / 60.0; + addScoreForVehicle(vehicleId, parameters.waitTime_min * waitTime_min); + trackScoreForVehicle(quitTime, vehicleId, "wait_time_min", parameters.waitTime_min * waitTime_min, + waitTime_min); + } + } + + @Override + public void handleEvent(QuitQueueAtChargerEvent event) { + handleQuitQueue(event.getVehicleId(), event.getTime()); + } + + @Override + public void handleEvent(ChargingStartEvent event) { + handleQuitQueue(event.getVehicleId(), event.getTime()); + handleStartCharging(event); + } + + // COST scoring + + private record ChargingStartState(double time, double charge) { + } + + private final IdMap chargingStartStates = new IdMap<>(Vehicle.class); + + private record MoneyRecord(Id personId, Id chargerId, double amount) { + } + + private final List moneyEvents = Collections.synchronizedList(new LinkedList<>()); + + private void initializeCosts() { + chargingStartStates.clear(); + moneyEvents.clear(); + } + + private void handleStartCharging(ChargingStartEvent event) { + chargingStartStates.put(event.getVehicleId(), new ChargingStartState(event.getTime(), event.getCharge())); + } + + private void handleFinishCharging(ChargingEndEvent event) { + ChargingStartState start = chargingStartStates.remove(event.getVehicleId()); + + double duration = event.getTime() - start.time; + double energy = event.getCharge() - start.charge; + + Id personId = getPerson(event.getVehicleId()); + + if (personId != null) { + double cost = costCalculator.calculateChargingCost(personId, event.getChargerId(), duration, energy); + addScoreForVehicle(event.getVehicleId(), cost * parameters.cost); + trackScoreForVehicle(event.getTime(), event.getVehicleId(), "cost", cost * parameters.cost, cost); + + if (cost != 0.0) { + moneyEvents.add(new MoneyRecord(personId, event.getChargerId(), cost)); + } + } + } + + // DETOUR scoring : works by calculating the travel time according to schedule + // and comparing the recorded travel time + + private record DetourPair(AtomicDouble travelTime, AtomicDouble travelDistance) { + } + + private final IdMap detours = new IdMap<>(Vehicle.class); + private final IdMap linkEnterTimes = new IdMap<>(Vehicle.class); + + private void initializeDetours() { + linkEnterTimes.clear(); + detours.clear(); + + for (Person person : activePersons) { + Id vehicleId = VehicleUtils.getVehicleId(person, chargingMode); + + AtomicDouble travelTime = new AtomicDouble(0.0); + AtomicDouble travelDistance = new AtomicDouble(0.0); + + for (Leg leg : TripStructureUtils.getLegs(person.getSelectedPlan())) { + travelTime.addAndGet(-leg.getTravelTime().seconds()); + travelDistance.addAndGet(-leg.getRoute().getDistance()); + } + + detours.put(vehicleId, new DetourPair(travelTime, travelDistance)); + } + } + + @Override + public void handleEvent(LinkEnterEvent event) { + if (detours.containsKey(event.getVehicleId())) { + linkEnterTimes.put(event.getVehicleId(), event.getTime()); + } + } + + @Override + public void handleEvent(LinkLeaveEvent event) { + Double enterTime = linkEnterTimes.remove(event.getVehicleId()); + + if (enterTime != null) { + DetourPair pair = detours.get(event.getVehicleId()); + pair.travelTime.addAndGet(event.getTime() - enterTime); + pair.travelDistance.addAndGet(network.getLinks().get(event.getLinkId()).getLength()); + } + } + + @Override + public void handleEvent(VehicleLeavesTrafficEvent event) { + linkEnterTimes.remove(event.getVehicleId()); + } + + private void finalizeDetours(double now) { + for (var entry : detours.entrySet()) { + double detourTravelTime_min = Math.max(0.0, entry.getValue().travelTime().get()) / 60.0; + double detourTravelDistance_km = Math.max(0.0, entry.getValue().travelTime().get()) * 1e-3; + + if (detourTravelTime_min * parameters.detourTime_min != 0.0) { + addScoreForVehicle(entry.getKey(), detourTravelTime_min * parameters.detourTime_min); + trackScoreForVehicle(now, entry.getKey(), "detour_time_min", + detourTravelTime_min * parameters.detourTime_min, + detourTravelTime_min); + } + + if (detourTravelDistance_km * parameters.detourDistance_km != 0.0) { + addScoreForVehicle(entry.getKey(), detourTravelDistance_km * parameters.detourDistance_km); + trackScoreForVehicle(now, entry.getKey(), "detour_distance_km", + detourTravelDistance_km * parameters.detourDistance_km, detourTravelDistance_km); + } + } + } + + private double simStepTime = 0.0; + + @Override + public void doSimStep(double time) { + simStepTime = time; + + for (MoneyRecord moneyRecord : moneyEvents) { + eventsManager.processEvent( + new PersonMoneyEvent(time, moneyRecord.personId, moneyRecord.amount, MONEY_EVENT_PURPOSE, + moneyRecord.chargerId.toString(), + null)); + } + + moneyEvents.clear(); + } + + @Override + public void onPrepareSim() { + } + + @Override + public void afterSim() { + } + + @Override + public void setInternalInterface(InternalInterface internalInterface) { + } + + /** + * Sets the minimum SoC under which a person doesn't want to fall. + */ + static public void setMinimumSoc(Person person, double minimumSoc) { + person.getAttributes().putAttribute(MINIMUM_SOC_PERSON_ATTRIBUTE, minimumSoc); + } + + /** + * Returns the minimum SoC under which a person doesn't want to fall. + */ + static public Double getMinimumSoc(Person person) { + return (Double) person.getAttributes().getAttribute(MINIMUM_SOC_PERSON_ATTRIBUTE); + } + + /** + * Sets the minimum SoC under which a person doesn't want to be at the end of + * the day. + */ + static public void setMinimumEndSoc(Person person, double minimumEndSoc) { + person.getAttributes().putAttribute(MINIMUM_END_SOC_PERSON_ATTRIBUTE, minimumEndSoc); + } + + /** + * Returns the minimum SoC under which a person doesn't want to be at the end of + * the day. + */ + static public Double getMinimumEndSoc(Person person) { + return (Double) person.getAttributes().getAttribute(MINIMUM_END_SOC_PERSON_ATTRIBUTE); + } +} diff --git a/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/scoring/ChargingPlanScoringParameters.java b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/scoring/ChargingPlanScoringParameters.java new file mode 100644 index 00000000000..a598363e54a --- /dev/null +++ b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/scoring/ChargingPlanScoringParameters.java @@ -0,0 +1,55 @@ +package org.matsim.contrib.ev.strategic.scoring; + +import org.matsim.core.config.ReflectiveConfigGroup; + +/** + * This parameter set represents the weights that are used in charging plan + * scoring. Some defaults are given. + * + * @author Sebastian Hörl (sebhoerl), IRT SystemX + */ +public class ChargingPlanScoringParameters extends ReflectiveConfigGroup { + static public final String GROUP_NAME = "scoring"; + + public ChargingPlanScoringParameters() { + super(GROUP_NAME); + } + + @Parameter + @Comment("scoring utility applied per money paid") + public double cost = -1.0; + + @Parameter + @Comment("scoring utility applied per minute waited") + public double waitTime_min = 0.0; + + @Parameter + @Comment("scoring utility applied per detour minutes induced for charging (during routing)") + public double detourTime_min = 0.0; + + @Parameter + @Comment("scoring utility applied per detour kilometres induced for charging (during routing)") + public double detourDistance_km = 0.0; + + @Parameter + @Comment("scorign utility applied every time the SoC goes to zero") + public double zeroSoc = -100.0; + + @Parameter + @Comment("scoring utility applied every time a charging attempt is unsuccessful (going to next charger)") + public double failedChargingAttempt = -10.0; + + @Parameter + @Comment("scoring utility applied every time a charging process (multiple retries) is unsuccessful") + public double failedChargingProcess = -100.0; + + @Parameter + @Comment("scoring utility applied every time the SoC goes from above to below the per-person minium soc (person attriute " + + ChargingPlanScoring.MINIMUM_SOC_PERSON_ATTRIBUTE + ")") + public double belowMinimumSoc = 0.0; + + @Parameter + @Comment("scoring utility applied at the end of the day if the SoC is below the per-person requirement (person attriute " + + ChargingPlanScoring.MINIMUM_END_SOC_PERSON_ATTRIBUTE + ")") + public double belowMinimumEndSoc = 0.0; +} diff --git a/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/scoring/ScoringTracker.java b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/scoring/ScoringTracker.java new file mode 100644 index 00000000000..aaa5998ee2e --- /dev/null +++ b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/scoring/ScoringTracker.java @@ -0,0 +1,71 @@ +package org.matsim.contrib.ev.strategic.scoring; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.UncheckedIOException; + +import org.matsim.api.core.v01.Id; +import org.matsim.api.core.v01.population.Person; +import org.matsim.core.controler.OutputDirectoryHierarchy; +import org.matsim.core.utils.io.IOUtils; + +/** + * This class tracks the charging scorign procses and writes out all scoring + * contributions to an analysis file. + * + * @author Sebastian Hörl (sebhoerl), IRT SystemX + */ +public class ScoringTracker { + static public final String OUTPUT_NAME = "sevc_scores.csv.gz"; + + private final int interval; + private final OutputDirectoryHierarchy outputHierarchy; + + private BufferedWriter writer = null; + + public ScoringTracker(OutputDirectoryHierarchy outputHierarchy, int interval) { + this.outputHierarchy = outputHierarchy; + this.interval = interval; + } + + public void start(int iteration, boolean isLastIteration) { + if (interval > 0 && (iteration % interval == 0 || isLastIteration)) { + String path = outputHierarchy.getIterationFilename(iteration, OUTPUT_NAME); + writer = IOUtils.getBufferedWriter(path); + + try { + writer.write(String.join(";", new String[] { + "time", "person_id", "score", "cause", "value" + }) + "\n"); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + } + + public void trackScore(double time, Id personId, String dimension, double score, Double value) { + if (writer != null) { + try { + writer.write(String.join(";", new String[] { + String.valueOf(time), + personId.toString(), String.valueOf(score), dimension, + value == null ? "" : String.valueOf(value) + }) + "\n"); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + } + + public void finish() { + if (writer != null) { + try { + writer.close(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + + writer = null; + } + } +} diff --git a/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/scoring/StrategicChargingScoringFunction.java b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/scoring/StrategicChargingScoringFunction.java new file mode 100644 index 00000000000..8cc6addecf1 --- /dev/null +++ b/contribs/ev/src/main/java/org/matsim/contrib/ev/strategic/scoring/StrategicChargingScoringFunction.java @@ -0,0 +1,57 @@ +package org.matsim.contrib.ev.strategic.scoring; + +import org.matsim.api.core.v01.Id; +import org.matsim.api.core.v01.population.Person; +import org.matsim.core.scoring.ScoringFunction; +import org.matsim.core.scoring.ScoringFunctionFactory; +import org.matsim.core.scoring.SumScoringFunction; +import org.matsim.core.scoring.SumScoringFunction.BasicScoring; + +/** + * This is a MATSim scoring function that integrates the score obtained from + * charging scoring with a factor. + * + * @author Sebastian Hörl (sebhoerl), IRT SystemX + */ +public class StrategicChargingScoringFunction implements BasicScoring { + private final Id personId; + private final double weight; + + private final ChargingPlanScoring scoring; + + public StrategicChargingScoringFunction(ChargingPlanScoring scoring, Id personId, double weight) { + this.scoring = scoring; + this.personId = personId; + this.weight = weight; + } + + @Override + public void finish() { + scoring.finalizeScoring(); + } + + @Override + public double getScore() { + return weight * scoring.getScore(personId); + } + + static public class Factory implements ScoringFunctionFactory { + private final ScoringFunctionFactory delegate; + private final ChargingPlanScoring chargingScoring; + + private final double weight; + + public Factory(ScoringFunctionFactory delegate, ChargingPlanScoring chargingScoring, double weight) { + this.delegate = delegate; + this.chargingScoring = chargingScoring; + this.weight = weight; + } + + @Override + public ScoringFunction createNewScoringFunction(Person person) { + SumScoringFunction function = (SumScoringFunction) delegate.createNewScoringFunction(person); + function.addScoringFunction(new StrategicChargingScoringFunction(chargingScoring, person.getId(), weight)); + return function; + } + } +} diff --git a/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/ChargingAlternative.java b/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/ChargingAlternative.java new file mode 100644 index 00000000000..dba4a05efe2 --- /dev/null +++ b/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/ChargingAlternative.java @@ -0,0 +1,22 @@ +package org.matsim.contrib.ev.withinday; + +import org.matsim.contrib.ev.infrastructure.Charger; + +/** + * A charging alternative is mainly used to encode an alternative charger that + * can be used for a planned charging activity during the day. However, for + * leg-based charging, also the planne duration can be changed. Furthermore, by + * providing a duration, an initially activity-based charging slot can be + * transformed into a leg-based charging slot. + * + * @author Sebastian Hörl (sebhoerl), IRT SystemX + */ +public record ChargingAlternative(Charger charger, double duration) { + public ChargingAlternative(Charger charger) { + this(charger, 0.0); + } + + public boolean isLegBased() { + return duration > 0.0; + } +} diff --git a/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/ChargingAlternativeProvider.java b/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/ChargingAlternativeProvider.java new file mode 100644 index 00000000000..6f4fb81662e --- /dev/null +++ b/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/ChargingAlternativeProvider.java @@ -0,0 +1,42 @@ +package org.matsim.contrib.ev.withinday; + +import java.util.List; + +import javax.annotation.Nullable; + +import org.matsim.api.core.v01.population.Person; +import org.matsim.api.core.v01.population.Plan; +import org.matsim.contrib.ev.fleet.ElectricVehicle; + +/** + * This interface provides alternative charging configurations online during the + * day, for instance, if the initial planned charger is occupied. In most cases, + * this interface is used to provide an alternative charger to the agent. + * + * @author Sebastian Hörl (sebhoerl), IRT SystemX + */ +public interface ChargingAlternativeProvider { + @Nullable + ChargingAlternative findAlternative(double now, Person person, Plan plan, ElectricVehicle vehicle, + ChargingSlot slot, List trace); + + @Nullable + ChargingAlternative findEnrouteAlternative(double now, Person person, Plan plan, ElectricVehicle vehicle, + @Nullable ChargingSlot slot); + + static public final ChargingAlternativeProvider NOOP = new ChargingAlternativeProvider() { + @Override + @Nullable + public ChargingAlternative findAlternative(double now, Person person, Plan plan, ElectricVehicle vehicle, + ChargingSlot slot, List trace) { + return null; + } + + @Override + @Nullable + public ChargingAlternative findEnrouteAlternative(double now, Person person, Plan plan, ElectricVehicle vehicle, + @Nullable ChargingSlot slot) { + return null; + } + }; +} diff --git a/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/ChargingScheduler.java b/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/ChargingScheduler.java new file mode 100644 index 00000000000..432d80fab49 --- /dev/null +++ b/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/ChargingScheduler.java @@ -0,0 +1,573 @@ +package org.matsim.contrib.ev.withinday; + +import java.util.LinkedList; +import java.util.List; + +import org.matsim.api.core.v01.Id; +import org.matsim.api.core.v01.network.Link; +import org.matsim.api.core.v01.network.Network; +import org.matsim.api.core.v01.population.Activity; +import org.matsim.api.core.v01.population.Leg; +import org.matsim.api.core.v01.population.Plan; +import org.matsim.api.core.v01.population.PlanElement; +import org.matsim.api.core.v01.population.PopulationFactory; +import org.matsim.contrib.ev.infrastructure.Charger; +import org.matsim.core.mobsim.framework.MobsimAgent; +import org.matsim.core.mobsim.qsim.agents.WithinDayAgentUtils; +import org.matsim.core.population.routes.NetworkRoute; +import org.matsim.core.router.DefaultRoutingRequest; +import org.matsim.core.router.RoutingModule; +import org.matsim.core.router.TripStructureUtils; +import org.matsim.core.utils.timing.TimeInterpretation; +import org.matsim.core.utils.timing.TimeTracker; +import org.matsim.facilities.ActivityFacilities; +import org.matsim.facilities.FacilitiesUtils; +import org.matsim.facilities.Facility; +import org.matsim.utils.objectattributes.attributable.AttributesImpl; +import org.matsim.utils.objectattributes.attributable.AttributesUtils; + +import com.google.common.base.Preconditions; + +/** + * This is an internal utility class that manages the rewriting of agent plans + * for everything that has to do with charging activities that are either + * planned in the beginning of the day or online throughout the simulation. + * + * @author Sebastian Hörl (sebhoerl), IRT SystemX + */ +class ChargingScheduler { + private final PopulationFactory populationFactory; + private final TimeInterpretation timeInterpretation; + private final ActivityFacilities facilities; + private final RoutingModule roadRoutingModule; + private final RoutingModule walkRoutingModule; + private final Network network; + + public ChargingScheduler(PopulationFactory populationFactory, TimeInterpretation timeInterpretation, + ActivityFacilities facilities, RoutingModule roadRoutingModule, RoutingModule walkRoutingModule, + Network network) { + this.populationFactory = populationFactory; + this.timeInterpretation = timeInterpretation; + this.facilities = facilities; + this.roadRoutingModule = roadRoutingModule; + this.walkRoutingModule = walkRoutingModule; + this.network = network; + } + + private int findPrecedingActivityIndex(List elements, int index) { + index--; + + while (index >= 0) { + PlanElement element = elements.get(index); + + if (element instanceof Activity) { + Activity activity = (Activity) element; + + if (!TripStructureUtils.isStageActivityType(activity.getType()) + || WithinDayEvEngine.isManagedActivityType(activity.getType())) { + return index; + } + } + + index--; + } + + throw new IllegalStateException(); + } + + private int findFollowingActivityIndex(List elements, int index) { + index++; + + while (index < elements.size()) { + PlanElement element = elements.get(index); + + if (element instanceof Activity) { + Activity activity = (Activity) element; + + if (!TripStructureUtils.isStageActivityType(activity.getType()) + || WithinDayEvEngine.isManagedActivityType(activity.getType())) { + return index; + } + } + + index++; + } + + throw new IllegalStateException(); + } + + public Activity scheduleInitialPlugActivity(MobsimAgent agent, Activity startActivity, Charger charger) { + return schedulePlugActivity(agent, startActivity, charger, null); + } + + public Activity scheduleSubsequentPlugActivity(MobsimAgent agent, Activity currentPlugActivity, Charger charger, + double departureTime) { + Preconditions.checkArgument(currentPlugActivity.getType().equals(WithinDayEvEngine.PLUG_ACTIVITY_TYPE)); + + Plan plan = WithinDayAgentUtils.getModifiablePlan(agent); + int currentIndex = plan.getPlanElements().indexOf(currentPlugActivity); + Preconditions.checkState(currentIndex >= 0); + + int nextActivityIndex = findFollowingActivityIndex(plan.getPlanElements(), currentIndex); + Activity nextActivity = (Activity) plan.getPlanElements().get(nextActivityIndex); + + return schedulePlugActivity(agent, nextActivity, charger, departureTime); + } + + /* + * This method schedules a ev:plug activity into the schedule. This happens at + * the beginning of the simulation to insert those plug activities as triggers + * for the charging procesess. + */ + private Activity schedulePlugActivity(MobsimAgent agent, Activity startActivity, Charger charger, + Double departureTime) { + Plan plan = WithinDayAgentUtils.getModifiablePlan(agent); + List planElements = plan.getPlanElements(); + + int startActivityIndex = plan.getPlanElements().indexOf(startActivity); + Preconditions.checkState(startActivityIndex >= 0); + int precedingActivityIndex = findPrecedingActivityIndex(planElements, startActivityIndex); + + Activity precedingActivity = (Activity) planElements.get(precedingActivityIndex); + Preconditions.checkState(!TripStructureUtils.isStageActivityType(startActivity.getType()) + || WithinDayEvEngine.isManagedActivityType(startActivity.getType())); + Preconditions.checkState(!TripStructureUtils.isStageActivityType(precedingActivity.getType()) + || WithinDayEvEngine.isManagedActivityType(precedingActivity.getType())); + + // Find departure time + TimeTracker timeTracker = new TimeTracker(timeInterpretation); + if (departureTime == null) { + addTimeElements(timeTracker, planElements.subList(0, precedingActivityIndex + 1)); + departureTime = timeTracker.getTime().seconds(); + } else { + timeTracker.setTime(departureTime); + Preconditions.checkState(precedingActivity.getType().equals(WithinDayEvEngine.PLUG_ACTIVITY_TYPE)); + } + + // Remove existing trip + planElements.removeAll(planElements.subList(precedingActivityIndex + 1, startActivityIndex)); + + // insert drive to charger + Facility precedingFacility = FacilitiesUtils.toFacility(precedingActivity, facilities); + + List driveToChargeElements = WithinDayAgentUtils + .convertInteractionActivities(roadRoutingModule.calcRoute( + DefaultRoutingRequest.of(precedingFacility, FacilitiesUtils.wrapLink(charger.getLink()), + departureTime, plan.getPerson(), precedingActivity.getAttributes()))); + + timeTracker.addElements(driveToChargeElements); + + int insertionIndex = precedingActivityIndex + 1; + planElements.addAll(insertionIndex, driveToChargeElements); + + // insert plug activity + Activity plugActivity = populationFactory.createActivityFromLinkId(WithinDayEvEngine.PLUG_ACTIVITY_TYPE, + charger.getLink().getId()); + AttributesUtils.copyAttributesFromTo(startActivity, plugActivity); + plugActivity.setStartTime(timeTracker.getTime().seconds()); + plugActivity.setMaximumDuration(Double.MAX_VALUE); + + insertionIndex += driveToChargeElements.size(); + planElements.add(insertionIndex, plugActivity); + + return plugActivity; + } + + public Activity scheduleOnroutePlugActivity(MobsimAgent agent, Leg leg, Charger charger) { + Plan plan = WithinDayAgentUtils.getModifiablePlan(agent); + List planElements = plan.getPlanElements(); + + int legIndex = plan.getPlanElements().indexOf(leg); + Preconditions.checkState(legIndex >= 0); + int precedingActivityIndex = findPrecedingActivityIndex(planElements, legIndex); + + Activity precedingActivity = (Activity) planElements.get(precedingActivityIndex); + Preconditions.checkState(!TripStructureUtils.isStageActivityType(precedingActivity.getType()) + || WithinDayEvEngine.isManagedActivityType(precedingActivity.getType())); + + // Find departure time + TimeTracker timeTracker = new TimeTracker(timeInterpretation); + addTimeElements(timeTracker, planElements.subList(0, precedingActivityIndex + 1)); + double departureTime = timeTracker.getTime().seconds(); + + // Remove existing trip + planElements.removeAll(planElements.subList(precedingActivityIndex + 1, legIndex + 1)); + + // insert drive to charger + Facility precedingFacility = FacilitiesUtils.toFacility(precedingActivity, facilities); + + List driveToChargeElements = WithinDayAgentUtils + .convertInteractionActivities(roadRoutingModule.calcRoute( + DefaultRoutingRequest.of(precedingFacility, FacilitiesUtils.wrapLink(charger.getLink()), + departureTime, plan.getPerson(), precedingActivity.getAttributes()))); + + timeTracker.addElements(driveToChargeElements); + + int insertionIndex = precedingActivityIndex + 1; + planElements.addAll(insertionIndex, driveToChargeElements); + + // insert plug activity + Activity plugActivity = populationFactory.createActivityFromLinkId(WithinDayEvEngine.PLUG_ACTIVITY_TYPE, + charger.getLink().getId()); + AttributesUtils.copyAttributesFromTo(precedingActivity, plugActivity); + plugActivity.setStartTime(timeTracker.getTime().seconds()); + plugActivity.setMaximumDuration(Double.MAX_VALUE); + + insertionIndex += driveToChargeElements.size(); + planElements.add(insertionIndex, plugActivity); + + return plugActivity; + } + + /** + * This method lets an agent drive to the next main activity in the schedule. + * This happens when + * + * (1) we are at an unplug activity, then charging process has + * finished successfully, and we now need to guide the agent from the unplug + * activity to the next planned main activity; and + * + * (2) when a charging process + * is unsuccessful, but the agent should still continue, then we need to send + * him from the current charger to the initially planned main activity for + * charging. + */ + public void scheduleDriveToNextActivity(MobsimAgent agent) { + Plan plan = WithinDayAgentUtils.getModifiablePlan(agent); + List planElements = plan.getPlanElements(); + + int currentActivityIndex = WithinDayAgentUtils.getCurrentPlanElementIndex(agent); + Activity currentActivity = (Activity) planElements.get(currentActivityIndex); + + Preconditions.checkState(currentActivity.getType().equals(WithinDayEvEngine.PLUG_ACTIVITY_TYPE) + || currentActivity.getType().equals(WithinDayEvEngine.UNPLUG_ACTIVITY_TYPE)); + + int mainActivityIndex = findFollowingActivityIndex(planElements, currentActivityIndex); + Activity mainActivity = (Activity) planElements.get(mainActivityIndex); + + double departureTime = currentActivity.getEndTime().seconds(); + + // remove intermediate elements + planElements.removeAll(planElements.subList(currentActivityIndex + 1, mainActivityIndex)); + + // insert drive to activity + Facility currentFacility = FacilitiesUtils.toFacility(currentActivity, facilities); + Facility mainFacility = FacilitiesUtils.toFacility(mainActivity, facilities); + + List driveToActivityElements = WithinDayAgentUtils + .convertInteractionActivities(roadRoutingModule.calcRoute(DefaultRoutingRequest.of(currentFacility, + mainFacility, departureTime, plan.getPerson(), currentActivity.getAttributes()))); + + int insertionIndex = currentActivityIndex + 1; + planElements.addAll(insertionIndex, driveToActivityElements); + } + + /** + * This method is called when a person is at a plug activity and the vehicle has + * been plugged successfully. We need to schedule the walk to the main activity, + * then skip everything until the end activity of the charging slot and insert + * another walk to the unplug activity. + */ + public Activity scheduleUntilUnplugActivity(MobsimAgent agent, Activity startActivity, Activity endActivity) { + Plan plan = WithinDayAgentUtils.getModifiablePlan(agent); + List planElements = plan.getPlanElements(); + + // GOING TO THE MAIN ACTIVITY + + int plugActivityIndex = WithinDayAgentUtils.getCurrentPlanElementIndex(agent); + Activity plugActivity = (Activity) planElements.get(plugActivityIndex); + Preconditions.checkState(plugActivity.getType().equals(WithinDayEvEngine.PLUG_ACTIVITY_TYPE)); + + int nextActivityIndex = findFollowingActivityIndex(planElements, plugActivityIndex); + int startActivityIndex = planElements.indexOf(startActivity); + Preconditions.checkState(nextActivityIndex == startActivityIndex); + + TimeTracker timeTracker = new TimeTracker(timeInterpretation); + timeTracker.setTime(plugActivity.getEndTime().seconds()); + + // insert walk to activity + Facility plugFacility = FacilitiesUtils.toFacility(plugActivity, facilities); + Facility startFacility = FacilitiesUtils.toFacility(startActivity, facilities); + + List walkToStartElements = WithinDayAgentUtils.convertInteractionActivities( + walkRoutingModule.calcRoute(DefaultRoutingRequest.of(plugFacility, startFacility, + timeTracker.getTime().seconds(), plan.getPerson(), plugActivity.getAttributes()))); + + timeTracker.addElements(walkToStartElements); + timeTracker.addElement(startActivity); + + int insertionIndex = plugActivityIndex + 1; + planElements.addAll(insertionIndex, walkToStartElements); + + // GOING TO THE UNPLUG ACTIVITY + + int endActivityIndex = planElements.indexOf(endActivity); + Preconditions.checkState(endActivityIndex >= 0); + Facility endFacility = FacilitiesUtils.toFacility(endActivity, facilities); + + if (endActivity == planElements.get(planElements.size() - 1)) { + // the end activity is the last one of the schedule, so we don't actually + // schedule an unplug activity + return null; + } + + for (int i = plugActivityIndex + 1; i <= endActivityIndex; i++) { + timeTracker.addElement(planElements.get(i)); + } + + // insert walk from activity + List walkToChargerElements = WithinDayAgentUtils.convertInteractionActivities( + walkRoutingModule.calcRoute(DefaultRoutingRequest.of(endFacility, plugFacility, + timeTracker.getTime().seconds(), plan.getPerson(), endActivity.getAttributes()))); + + insertionIndex = endActivityIndex + 1; + + planElements.addAll(insertionIndex, walkToChargerElements); + timeTracker.addElements(walkToChargerElements); + + // insert unplug activity + Activity unplugActivity = populationFactory.createActivityFromLinkId(WithinDayEvEngine.UNPLUG_ACTIVITY_TYPE, + plugActivity.getLinkId()); + AttributesUtils.copyAttributesFromTo(endActivity, unplugActivity); + unplugActivity.setStartTime(timeTracker.getTime().seconds()); + unplugActivity.setEndTime(Double.MAX_VALUE); + + insertionIndex += walkToChargerElements.size(); + planElements.add(insertionIndex, unplugActivity); + timeTracker.addActivity(unplugActivity); + + // DRIVE TO NEXT MAIN ACTIVITY IN PLAN + + // find following activity + int followingActivityIndex = findFollowingActivityIndex(planElements, insertionIndex + 1); + Activity followingActivity = (Activity) planElements.get(followingActivityIndex); + + // remove deprecated drive after main activity + planElements.removeAll(planElements.subList(insertionIndex + 1, followingActivityIndex)); + + // replace with a new chain + Facility unplugFacility = FacilitiesUtils.toFacility(unplugActivity, facilities); + Facility followingFacility = FacilitiesUtils.toFacility(followingActivity, facilities); + + List driveToFollowingElements = WithinDayAgentUtils.convertInteractionActivities( + roadRoutingModule.calcRoute(DefaultRoutingRequest.of(unplugFacility, followingFacility, + timeTracker.getTime().seconds(), plan.getPerson(), plugActivity.getAttributes()))); + + insertionIndex++; + planElements.addAll(insertionIndex, driveToFollowingElements); + + return unplugActivity; + } + + /** + * This method schedules an unplug activity after a main activity that is the + * first after which the charging mode is used. This means that the vehicle was + * plugged overnight, so the person needs to walk from the main activity to the + * unplug activity and then we need to reroute the agent. + */ + public Activity scheduleUnplugActivityAfterOvernightCharge(MobsimAgent agent, Activity endActivity, + Charger charger) { + Plan plan = WithinDayAgentUtils.getModifiablePlan(agent); + List planElements = plan.getPlanElements(); + + int endActivityIndex = planElements.indexOf(endActivity); + Preconditions.checkState(endActivityIndex >= 0); + + TimeTracker timeTracker = new TimeTracker(timeInterpretation); + addTimeElements(timeTracker, planElements.subList(0, endActivityIndex + 1)); + + // create unplug activity + Activity unplugActivity = populationFactory.createActivityFromLinkId(WithinDayEvEngine.UNPLUG_ACTIVITY_TYPE, + charger.getLink().getId()); + AttributesUtils.copyAttributesFromTo(endActivity, unplugActivity); + + // insert walk to unplug activity + Facility endFacility = FacilitiesUtils.toFacility(endActivity, facilities); + Facility unplugFacility = FacilitiesUtils.toFacility(unplugActivity, facilities); + + List walkElements = WithinDayAgentUtils.convertInteractionActivities( + walkRoutingModule.calcRoute(DefaultRoutingRequest.of(endFacility, unplugFacility, + timeTracker.getTime().seconds(), plan.getPerson(), endActivity.getAttributes()))); + + int insertionIndex = endActivityIndex + 1; + planElements.addAll(insertionIndex, walkElements); + timeTracker.addElements(walkElements); + + // insert unplug activity + insertionIndex += walkElements.size(); + unplugActivity.setStartTime(timeTracker.getTime().seconds()); + unplugActivity.setEndTime(Double.MAX_VALUE); + planElements.add(insertionIndex, unplugActivity); + + return unplugActivity; + } + + /** + * This method is called when a person successfully plugs at a charger in an + * on-route slot. The person should stay at the charger and stay there until the + * requested time has elapsed. + */ + public Activity scheduleUnplugActivityAtCharger(MobsimAgent agent, double duration) { + Plan plan = WithinDayAgentUtils.getModifiablePlan(agent); + List planElements = plan.getPlanElements(); + + int plugActivityIndex = WithinDayAgentUtils.getCurrentPlanElementIndex(agent); + Activity plugActivity = (Activity) planElements.get(plugActivityIndex); + Preconditions.checkState(plugActivity.getType().equals(WithinDayEvEngine.PLUG_ACTIVITY_TYPE)); + + Activity waitActivity = populationFactory.createActivityFromLinkId(WithinDayEvEngine.WAIT_ACTIVITY_TYPE, + plugActivity.getLinkId()); + waitActivity.setMaximumDuration(duration); + + Activity unplugActivity = populationFactory.createActivityFromLinkId(WithinDayEvEngine.UNPLUG_ACTIVITY_TYPE, + plugActivity.getLinkId()); + unplugActivity.setEndTime(Double.MAX_VALUE); + + int insertionIndex = plugActivityIndex + 1; + planElements.add(insertionIndex, waitActivity); + + insertionIndex++; + planElements.add(insertionIndex, unplugActivity); + + return unplugActivity; + } + + /** + * This method is called if charging at the first main activity which is + * followed by the charging mode is not successful. This means that an overnight + * charging did not succeed. The agent, hence, needs to walk to the charger + * location and pick up the car from there. + */ + public void scheduleAccessAfterOvernightCharge(MobsimAgent agent, Activity endActivity, Charger charger) { + Plan plan = WithinDayAgentUtils.getModifiablePlan(agent); + List planElements = plan.getPlanElements(); + + int endActivityIndex = planElements.indexOf(endActivity); + Preconditions.checkState(endActivityIndex >= 0); + + TimeTracker timeTracker = new TimeTracker(timeInterpretation); + addTimeElements(timeTracker, planElements.subList(0, endActivityIndex + 1)); + + // create access activity + Activity accessActivity = populationFactory.createActivityFromLinkId( + WithinDayEvEngine.ACCESS_ACTIVITY_TYPE, + charger.getLink().getId()); + AttributesUtils.copyAttributesFromTo(endActivity, accessActivity); + + // insert walk to access activity + Facility endFacility = FacilitiesUtils.toFacility(endActivity, facilities); + Facility accessFacility = FacilitiesUtils.toFacility(accessActivity, facilities); + + List walkElements = WithinDayAgentUtils.convertInteractionActivities( + walkRoutingModule.calcRoute(DefaultRoutingRequest.of(endFacility, accessFacility, + timeTracker.getTime().seconds(), plan.getPerson(), endActivity.getAttributes()))); + + int insertionIndex = endActivityIndex + 1; + planElements.addAll(insertionIndex, walkElements); + timeTracker.addElements(walkElements); + + // insert access activity + insertionIndex += walkElements.size(); + accessActivity.setStartTime(timeTracker.getTime().seconds()); + accessActivity.setEndTime(timeTracker.getTime().seconds()); + planElements.add(insertionIndex, accessActivity); + + int nextActivityIndex = findFollowingActivityIndex(planElements, insertionIndex); + Activity nextActivity = (Activity) planElements.get(nextActivityIndex); + + double departureTime = timeTracker.getTime().seconds(); + + // remove intermediate elements + planElements.removeAll(planElements.subList(insertionIndex + 1, nextActivityIndex)); + + // insert drive to next activity + Facility nextFacility = FacilitiesUtils.toFacility(nextActivity, facilities); + + List driveToActivityElements = WithinDayAgentUtils + .convertInteractionActivities(roadRoutingModule.calcRoute(DefaultRoutingRequest.of(accessFacility, + nextFacility, departureTime, plan.getPerson(), endActivity.getAttributes()))); + + insertionIndex++; + planElements.addAll(insertionIndex, driveToActivityElements); + } + + /** + * This function is called when the plug activity is changed from one place to + * another, for instance, when an enroute change happens while approaching a + * planned plug activity. + */ + public Activity changePlugActivity(MobsimAgent agent, Activity currentPlugActivity, Charger charger, + double now) { + Plan plan = WithinDayAgentUtils.getModifiablePlan(agent); + int currentLegIndex = WithinDayAgentUtils.getCurrentPlanElementIndex(agent); + Leg currentLeg = (Leg) plan.getPlanElements().get(currentLegIndex); + + Id currentLinkId = agent.getCurrentLinkId(); + Link currentLink = network.getLinks().get(currentLinkId); + Facility currentFacility = FacilitiesUtils.wrapLink(currentLink); + Facility plugFacility = FacilitiesUtils.wrapLink(charger.getLink()); + + List driveToChargeElements = WithinDayAgentUtils + .convertInteractionActivities(roadRoutingModule.calcRoute(DefaultRoutingRequest.of(currentFacility, + plugFacility, now, plan.getPerson(), new AttributesImpl()))); + + Preconditions.checkState(driveToChargeElements.size() == 1); + Leg updatedLeg = (Leg) driveToChargeElements.get(0); + Preconditions.checkState(updatedLeg.getMode().equals(currentLeg.getMode())); + + NetworkRoute currentRoute = (NetworkRoute) currentLeg.getRoute(); + NetworkRoute followingRoute = (NetworkRoute) updatedLeg.getRoute(); + + int currentLinkIndex = currentRoute.getLinkIds().indexOf(currentLinkId); + + List> updatedSequence = new LinkedList<>(); + updatedSequence.addAll(currentRoute.getLinkIds().subList(0, currentLinkIndex + 1)); + updatedSequence.addAll(followingRoute.getLinkIds()); + + currentRoute.setLinkIds(currentRoute.getStartLinkId(), updatedSequence, followingRoute.getEndLinkId()); + + if (currentPlugActivity != null) { + Preconditions.checkState(currentPlugActivity.getType().equals(WithinDayEvEngine.PLUG_ACTIVITY_TYPE)); + Preconditions.checkState(plan.getPlanElements().get(currentLegIndex + 1) == currentPlugActivity); + plan.getPlanElements().remove(currentLegIndex + 1); // remove plug activity + } + + TimeTracker timeTracker = new TimeTracker(timeInterpretation); + timeTracker.setTime(now); + timeTracker.addElements(driveToChargeElements); + + // insert new plug activity + Activity plugActivity = populationFactory.createActivityFromLinkId(WithinDayEvEngine.PLUG_ACTIVITY_TYPE, + charger.getLink().getId()); + + if (currentPlugActivity != null) { + AttributesUtils.copyAttributesFromTo(currentPlugActivity, plugActivity); + } + + plugActivity.setStartTime(timeTracker.getTime().seconds()); + plugActivity.setMaximumDuration(Double.MAX_VALUE); + plan.getPlanElements().add(currentLegIndex + 1, plugActivity); + + return plugActivity; + } + + /** + * Called when an agent starts a charging process on a leg, but there is no + * planned activity-based nor leg-based charging slot. This is spontaneous + * charging. + */ + public Activity insertPlugActivity(MobsimAgent agent, Charger charger, + double now) { + return changePlugActivity(agent, null, charger, now); + } + + private void addTimeElements(TimeTracker timeTracker, List elements) { + for (PlanElement element : elements) { + if (element instanceof Activity activity && WithinDayEvEngine.isManagedActivityType(activity.getType())) { + continue; // ignore our inifinite duration marker activities + } + + timeTracker.addElement(element); + } + } +} diff --git a/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/ChargingSlot.java b/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/ChargingSlot.java new file mode 100644 index 00000000000..700fc3d46e5 --- /dev/null +++ b/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/ChargingSlot.java @@ -0,0 +1,32 @@ +package org.matsim.contrib.ev.withinday; + +import javax.annotation.Nullable; + +import org.matsim.api.core.v01.population.Activity; +import org.matsim.api.core.v01.population.Leg; +import org.matsim.contrib.ev.infrastructure.Charger; + +import com.google.common.base.Preconditions; + +/** + * A charging slot represents when an agent intends to charge during the day. + * Charging slots can be leg-based and activity-based. See ChargingSlotFinder + * for more information. + * + * @author Sebastian Hörl (sebhoerl), IRT SystemX + */ +public record ChargingSlot(@Nullable Activity startActivity, @Nullable Activity endActivity, @Nullable Leg leg, + double duration, Charger charger) { + public boolean isLegBased() { + return duration > 0.0; + } + + public ChargingSlot(Activity startActivity, Activity endActivity, Charger charger) { + this(startActivity, endActivity, null, 0.0, charger); + } + + public ChargingSlot(Leg leg, double duration, Charger charger) { + this(null, null, leg, duration, charger); + Preconditions.checkArgument(duration > 0.0); + } +} diff --git a/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/ChargingSlotFinder.java b/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/ChargingSlotFinder.java new file mode 100644 index 00000000000..050555bfc2e --- /dev/null +++ b/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/ChargingSlotFinder.java @@ -0,0 +1,196 @@ +package org.matsim.contrib.ev.withinday; + +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +import org.matsim.api.core.v01.Id; +import org.matsim.api.core.v01.Scenario; +import org.matsim.api.core.v01.network.Link; +import org.matsim.api.core.v01.population.Activity; +import org.matsim.api.core.v01.population.Leg; +import org.matsim.api.core.v01.population.Person; +import org.matsim.api.core.v01.population.Plan; +import org.matsim.api.core.v01.population.PlanElement; +import org.matsim.core.population.PopulationUtils; +import org.matsim.core.router.TripStructureUtils; +import org.matsim.core.router.TripStructureUtils.Trip; + +/** + * This is a convenience class that helps identifying viable charging slots + * throughout a day. + * + * A viable charging slot for leg-based charging is a leg of the given transport + * mode. + * + * A viable charging slot for activity-based charging is a sequence of + * activities with specific conditions. The first condtion is that the sequence + * is preceded by a leg of the specified mode and that the sequence is left by a + * leg of that mode. Furthermore, no leg of that mode should take place between + * the first and the last activity in the sequence. + * + * @author Sebastian Hörl (sebhoerl), IRT SystemX + */ +public class ChargingSlotFinder { + private final Scenario scenario; + private final String chargingMode; + + public ChargingSlotFinder(Scenario scenario, String chargingMode) { + this.scenario = scenario; + this.chargingMode = chargingMode; + } + + public record ActivityBasedCandidate(Activity startActivity, Activity endActivity) { + } + + public record LegBasedCandidate(Leg leg, Activity followingActivity) { + } + + /** + * Finds all candidates for leg-based charging along the agent plan. + */ + public List findLegBased(Person person, Plan plan) { + List candidates = new LinkedList<>(); + for (Trip trip : TripStructureUtils.getTrips(plan)) { + for (Leg leg : trip.getLegsOnly()) { + if (leg.getRoutingMode().equals(chargingMode)) { + if (leg.getMode().equals(chargingMode)) { + candidates.add(new LegBasedCandidate(leg, trip.getDestinationActivity())); + } + } + } + } + + return candidates; + } + + /** + * Finds all candidates for activity-based charging along an agent plan. + */ + public List findActivityBased(Person person, Plan plan) { + // find trips that change the location of the vehicle + List breakpoints = new LinkedList<>(); + for (Trip trip : TripStructureUtils.getTrips(plan)) { + String routingMode = TripStructureUtils.getRoutingModeIdentifier().identifyMainMode(trip.getTripElements()); + + if (routingMode.equals(chargingMode)) { + Id originLinkId = PopulationUtils.decideOnLinkIdForActivity(trip.getOriginActivity(), scenario); + Id destinationLinkId = PopulationUtils.decideOnLinkIdForActivity(trip.getDestinationActivity(), + scenario); + + if (originLinkId != destinationLinkId) { + breakpoints.add(trip); + } + } + } + + // find candidates for charging slots + List candidates = new LinkedList<>(); + + if (breakpoints.size() == 0) { + Activity startActivity = (Activity) plan.getPlanElements().get(0); + Activity endActivity = (Activity) plan.getPlanElements().get(plan.getPlanElements().size() - 1); + + candidates.add(new ActivityBasedCandidate(startActivity, endActivity)); + } else { + { + // First slot + Activity startActivity = (Activity) plan.getPlanElements().get(0); + Activity endActivity = breakpoints.getFirst().getOriginActivity(); + + candidates.add( + new ActivityBasedCandidate(startActivity, endActivity)); + } + + List elements = plan.getPlanElements(); + for (int i = 1; i < breakpoints.size(); i++) { + // Intermediate slot + + Trip startTrip = breakpoints.get(i - 1); + Trip endTrip = breakpoints.get(i); + + Activity startActivity = startTrip.getDestinationActivity(); + Activity endActivity = endTrip.getOriginActivity(); + + // check if there are intermediate uses of the car + boolean isValid = true; + + for (int k = elements.indexOf(startActivity); k < elements.indexOf(endActivity); k++) { + if (elements.get(k) instanceof Leg leg) { + if (leg.getMode().equals(chargingMode)) { + // we found antoher car leg that has not generated a breakpoint, meaning that + // the car is used along the way to go from a place to itself + isValid = false; + break; + } + } + } + + if (isValid) { + candidates.add( + new ActivityBasedCandidate(startActivity, endActivity)); + } + } + + { + // Last slot + Activity startActivity = breakpoints.getLast().getDestinationActivity(); + Activity endActivity = (Activity) plan.getPlanElements().get(plan.getPlanElements().size() - 1); + + candidates.add( + new ActivityBasedCandidate(startActivity, endActivity)); + } + } + + return candidates; + } + + /** + * Some activity-based and leg-based slot candidates are incompatible. This + * method removes from a list of activity-based candiates those that are + * incompatible with the provided list of leg-based candidates. + */ + public void reduceActivityBased(List activityBased, List legBased, + List elements) { + Iterator iterator = activityBased.iterator(); + + while (iterator.hasNext()) { + ActivityBasedCandidate activity = iterator.next(); + + int startIndex = elements.indexOf(activity.startActivity); + int endIndex = elements.indexOf(activity.endActivity); + + for (LegBasedCandidate leg : legBased) { + int followingActivityIndex = elements.indexOf(leg.followingActivity); + + if (followingActivityIndex >= startIndex && followingActivityIndex <= endIndex) { + iterator.remove(); + } + } + } + } + + /** + * Some activity-based and leg-based slot candidates are incompatible. This + * method removes from a list of leg-based candiates those that are + * incompatible with the provided list of activity-based candidates. + */ + public void reduceLegBased(List legBased, List activityBased, + List elements) { + Iterator iterator = legBased.iterator(); + + while (iterator.hasNext()) { + LegBasedCandidate leg = iterator.next(); + int followingActivityIndex = elements.indexOf(leg.followingActivity); + + for (ActivityBasedCandidate activity : activityBased) { + int startIndex = elements.indexOf(activity.startActivity); + int endIndex = elements.indexOf(activity.endActivity); + + if (followingActivityIndex >= startIndex && followingActivityIndex <= endIndex) { + iterator.remove(); + } + } + } + } +} diff --git a/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/ChargingSlotProvider.java b/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/ChargingSlotProvider.java new file mode 100644 index 00000000000..3854c321bf3 --- /dev/null +++ b/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/ChargingSlotProvider.java @@ -0,0 +1,24 @@ +package org.matsim.contrib.ev.withinday; + +import java.util.Collections; +import java.util.List; + +import org.matsim.api.core.v01.population.Person; +import org.matsim.api.core.v01.population.Plan; +import org.matsim.contrib.ev.fleet.ElectricVehicle; + +/** + * A charging slot provider is called in the beginning of the day. It is + * supposed to return all the planned charging slots for an agent. Those can be + * leg-based charging slots (where an agent stops and charges along a leg) or + * activity-based charging slots where the agent plugs the vehicle before going + * to an activity, and comes back after that or another acitvity to unplug the + * vehicle. + * + * @author Sebastian Hörl (sebhoerl), IRT SystemX + */ +public interface ChargingSlotProvider { + List findSlots(Person person, Plan plan, ElectricVehicle vehicle); + + static public final ChargingSlotProvider NOOP = (person, plan, vehicle) -> Collections.emptyList(); +} diff --git a/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/WithinDayChargingStrategy.java b/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/WithinDayChargingStrategy.java new file mode 100644 index 00000000000..5c972f52cd1 --- /dev/null +++ b/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/WithinDayChargingStrategy.java @@ -0,0 +1,80 @@ +package org.matsim.contrib.ev.withinday; + +import org.matsim.contrib.ev.charging.BatteryCharging; +import org.matsim.contrib.ev.charging.ChargingStrategy; +import org.matsim.contrib.ev.fleet.Battery; +import org.matsim.contrib.ev.fleet.ElectricVehicle; +import org.matsim.contrib.ev.infrastructure.ChargerSpecification; +import org.matsim.vehicles.Vehicle; + +/** + * This charging strategy makes vehicles *not* unplug automatically at a + * charger. Instead, the vehicles remain plugged until they get unplugged by the + * owner. Furthermore, vehicles are only charged up to a maximum SoC that can be + * configured per vehicle. + * + * @author Sebastian Hörl (sebhoerl), IRT SystemX + */ +public class WithinDayChargingStrategy implements ChargingStrategy { + static public final String MAXIMUM_SOC_VEHICLE_ATTRIBUTE = "wevc:maximumSoc"; + + private final ChargerSpecification charger; + private final ElectricVehicle ev; + + public WithinDayChargingStrategy(ChargerSpecification charger, ElectricVehicle ev) { + this.charger = charger; + this.ev = ev; + } + + @Override + public double calcRemainingEnergyToCharge() { + Double maximumSoc = getMaximumSoc(ev); + + if (maximumSoc == null) { + maximumSoc = 1.0; + } + + Battery battery = ev.getBattery(); + return maximumSoc * battery.getCapacity() - battery.getCharge(); + } + + @Override + public double calcRemainingTimeToCharge() { + return ((BatteryCharging) ev.getChargingPower()).calcChargingTime(charger, calcRemainingEnergyToCharge()); + } + + @Override + public boolean isChargingCompleted() { + if (getMaximumSoc(ev) != null) { + return false; + } else { + return calcRemainingEnergyToCharge() <= 0; + } + } + + private Double getMaximumSoc(ElectricVehicle ev) { + return getMaximumSoc(ev.getVehicleSpecification().getMatsimVehicle()); + } + + /** + * Returns the maximum SoC of a vehicle. + */ + static public Double getMaximumSoc(Vehicle vehicle) { + return (Double) vehicle.getAttributes() + .getAttribute(MAXIMUM_SOC_VEHICLE_ATTRIBUTE); + } + + /** + * Sets the maximum SoC of a vehicle. + */ + static public void setMaximumSoc(Vehicle vehicle, double maximumSoc) { + vehicle.getAttributes().putAttribute(MAXIMUM_SOC_VEHICLE_ATTRIBUTE, maximumSoc); + } + + public static class Factory implements ChargingStrategy.Factory { + @Override + public ChargingStrategy createStrategy(ChargerSpecification charger, ElectricVehicle ev) { + return new WithinDayChargingStrategy(charger, ev); + } + } +} \ No newline at end of file diff --git a/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/WithinDayEvConfigGroup.java b/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/WithinDayEvConfigGroup.java new file mode 100644 index 00000000000..9f8331ad129 --- /dev/null +++ b/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/WithinDayEvConfigGroup.java @@ -0,0 +1,60 @@ +package org.matsim.contrib.ev.withinday; + +import org.matsim.api.core.v01.TransportMode; +import org.matsim.contrib.ev.EvConfigGroup; +import org.matsim.core.config.Config; +import org.matsim.core.config.ReflectiveConfigGroup; + +import com.google.common.base.Verify; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.PositiveOrZero; + +/** + * Configuration options for within-day electric vehicle charging + * + * @author Sebastian Hörl (sebhoerl), IRT SystemX + */ +public class WithinDayEvConfigGroup extends ReflectiveConfigGroup { + public static final String GROUP_NAME = "withinDayEv"; + + public static WithinDayEvConfigGroup get(Config config) { + return (WithinDayEvConfigGroup) config.getModules().get(GROUP_NAME); + } + + public WithinDayEvConfigGroup() { + super(GROUP_NAME); + } + + @Parameter + @Comment("Mode for which charging of electric vehicles is simulated. Persons need to have vehicles of that type.") + @NotBlank + public String carMode = TransportMode.car; + + @Parameter + @Comment("Mode that is used to move between chargers and main activities") + @NotBlank + public String walkMode = TransportMode.walk; + + @Parameter + @Comment("Defines whether agents abort if no charger can be found (scoring is event is generated in any case)") + public boolean abortAgents = false; + + @Parameter + @Comment("Defines how long an agent will wait during a charging attempt for the vehicle to be plugged, otherwise find new charger") + @PositiveOrZero + public double maximumQueueTime = 300.0; + + @Parameter + @Comment("Defines whether spontaneous charging is allowed when going on a car leg that is neither followed by an activity-based charging slot, nor already has a leg-based charging slot.") + public boolean allowSpoantaneousCharging = false; + + @Override + protected void checkConsistency(Config config) { + super.checkConsistency(config); + + EvConfigGroup evConfig = EvConfigGroup.get(config); + Verify.verify(maximumQueueTime > evConfig.chargeTimeStep, + "Maximum queue time should be longer than the charging engine time step, to make sure we catch an event where a vehicle gets queued or plugged"); + } +} diff --git a/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/WithinDayEvEngine.java b/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/WithinDayEvEngine.java new file mode 100644 index 00000000000..f2d0f70147f --- /dev/null +++ b/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/WithinDayEvEngine.java @@ -0,0 +1,938 @@ +package org.matsim.contrib.ev.withinday; + +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.matsim.api.core.v01.Id; +import org.matsim.api.core.v01.IdMap; +import org.matsim.api.core.v01.IdSet; +import org.matsim.api.core.v01.Scenario; +import org.matsim.api.core.v01.events.ActivityStartEvent; +import org.matsim.api.core.v01.events.PersonDepartureEvent; +import org.matsim.api.core.v01.events.handler.ActivityStartEventHandler; +import org.matsim.api.core.v01.events.handler.PersonDepartureEventHandler; +import org.matsim.api.core.v01.network.Link; +import org.matsim.api.core.v01.population.Activity; +import org.matsim.api.core.v01.population.Leg; +import org.matsim.api.core.v01.population.Person; +import org.matsim.api.core.v01.population.Plan; +import org.matsim.api.core.v01.population.PlanElement; +import org.matsim.contrib.ev.charging.ChargingStartEvent; +import org.matsim.contrib.ev.charging.ChargingStartEventHandler; +import org.matsim.contrib.ev.charging.ChargingStrategy; +import org.matsim.contrib.ev.charging.QueuedAtChargerEvent; +import org.matsim.contrib.ev.charging.QueuedAtChargerEventHandler; +import org.matsim.contrib.ev.fleet.ElectricFleet; +import org.matsim.contrib.ev.fleet.ElectricVehicle; +import org.matsim.contrib.ev.withinday.events.AbortChargingAttemptEvent; +import org.matsim.contrib.ev.withinday.events.AbortChargingProcessEvent; +import org.matsim.contrib.ev.withinday.events.FinishChargingAttemptEvent; +import org.matsim.contrib.ev.withinday.events.FinishChargingProcessEvent; +import org.matsim.contrib.ev.withinday.events.StartChargingAttemptEvent; +import org.matsim.contrib.ev.withinday.events.StartChargingProcessEvent; +import org.matsim.contrib.ev.withinday.events.UpdateChargingAttemptEvent; +import org.matsim.core.api.experimental.events.EventsManager; +import org.matsim.core.events.MobsimScopeEventHandler; +import org.matsim.core.mobsim.framework.MobsimAgent; +import org.matsim.core.mobsim.qsim.InternalInterface; +import org.matsim.core.mobsim.qsim.QSim; +import org.matsim.core.mobsim.qsim.agents.HasModifiablePlan; +import org.matsim.core.mobsim.qsim.agents.WithinDayAgentUtils; +import org.matsim.core.mobsim.qsim.interfaces.MobsimEngine; +import org.matsim.core.mobsim.qsim.interfaces.MobsimVehicle; +import org.matsim.core.mobsim.qsim.qnetsimengine.QLinkI; +import org.matsim.core.mobsim.qsim.qnetsimengine.QVehicle; +import org.matsim.core.mobsim.qsim.qnetsimengine.QVehicleFactory; +import org.matsim.core.population.PopulationUtils; +import org.matsim.core.router.TripStructureUtils; +import org.matsim.core.router.TripStructureUtils.Trip; +import org.matsim.vehicles.Vehicle; +import org.matsim.vehicles.VehicleUtils; +import org.matsim.vehicles.Vehicles; + +import com.google.common.base.Preconditions; + +/** + * This engine is the core of the within-day electric vehicle charging package. + * It mainly works with two interfaces, ChargingSlotProvider and + * ChargingAlternativeProvider. In the beginning of the day, the + * ChargingSlotProvider is called to integate planned charging activities into + * the schedule of each viable agent. The ChargingAlternativeProvider is called + * throghout the day, for instance, to change the charger when an agent notices + * that a planned charger is blocked. The engine maanges the successive + * adaptation of the plan to the actions intended by the agent (pluggin the car, + * unpluggin the car, ...). + * + * @author Sebastian Hörl (sebhoerl), IRT SystemX + */ +public class WithinDayEvEngine implements MobsimEngine, ActivityStartEventHandler, ChargingStartEventHandler, + QueuedAtChargerEventHandler, PersonDepartureEventHandler, MobsimScopeEventHandler { + static public final String ACTIVE_PERSON_ATTRIBUTE = "wevc:active"; + static public final String MAXIMUM_QUEUE_TIME_PERSON_ATTRIBUTE = "wevc:maximumQueueTime"; + + static public final String PLUG_ACTIVITY_TYPE = "ev:plug interaction"; + static public final String UNPLUG_ACTIVITY_TYPE = "ev:unplug interaction"; + static public final String WAIT_ACTIVITY_TYPE = "ev:wait interaction"; + + // used when charging at the first acitivty fails and the person needs to + // recover the vehicle + static public final String ACCESS_ACTIVITY_TYPE = "ev:access interaction"; + + static public final String CHARGING_SLOT_ATTRIBUTE = "ev:chargingSlot"; + static public final String CHARGING_PROCESS_ATTRIBUTE = "ev:chargingProcess"; + static private final String INITIAL_ACTIVITY_END_TIME_ATTRIBUTE = "ev:initialActivityEndTime"; + + private final String chargingMode; + private final QSim qsim; + + private final Vehicles vehicles; + private final QVehicleFactory qVehicleFactory; + private final Scenario scenario; + private final WithinDayChargingStrategy.Factory chargingStrategyFactory; + + private final ElectricFleet electricFleet; + private final ChargingAlternativeProvider alternativeProvider; + private final ChargingSlotProvider slotProvider; + private final EventsManager eventsManager; + private ChargingScheduler chargingScheduler; + + private final boolean performAbort; + private final double maximumQueueWaitTime; + private final boolean allowSpontaneousCharging; + + private final Logger logger = LogManager.getLogger(WithinDayEvEngine.class); + + public WithinDayEvEngine(WithinDayEvConfigGroup config, QSim qsim, ElectricFleet electricFleet, + ChargingAlternativeProvider onlineSlotProvider, ChargingSlotProvider offlineSlotProvider, + EventsManager eventsManager, + ChargingScheduler chargingScheduler, Vehicles vehicles, QVehicleFactory qVehicleFactory, + Scenario scenario, WithinDayChargingStrategy.Factory chargingStrategyFactory) { + this.qsim = qsim; + this.electricFleet = electricFleet; + this.alternativeProvider = onlineSlotProvider; + this.slotProvider = offlineSlotProvider; + this.eventsManager = eventsManager; + this.chargingScheduler = chargingScheduler; + this.vehicles = vehicles; + this.qVehicleFactory = qVehicleFactory; + this.scenario = scenario; + this.chargingStrategyFactory = chargingStrategyFactory; + + this.chargingMode = config.carMode; + this.performAbort = config.abortAgents; + this.maximumQueueWaitTime = config.maximumQueueTime; + this.allowSpontaneousCharging = config.allowSpoantaneousCharging; + } + + // INITIALIZATION + + private final IdSet relevantPersons = new IdSet<>(Person.class); + private final IdSet relevantVehicles = new IdSet<>(Vehicle.class); + + @Override + public void onPrepareSim() { + logger.info("Implementing charging slots .."); + + int activityBasedCount = 0; + int legBasedCount = 0; + int overnightCount = 0; + int wholeDayCount = 0; + + for (MobsimAgent agent : qsim.getAgents().values()) { + if (agent instanceof HasModifiablePlan) { + Plan plan = WithinDayAgentUtils.getModifiablePlan(agent); + + // only active agents + // only those agents that actually have a proper vehicle + if (isActive(plan.getPerson()) && VehicleUtils.hasVehicleId(plan.getPerson(), chargingMode)) { + relevantPersons.add(plan.getPerson().getId()); + + Id vehicleId = VehicleUtils.getVehicleId(plan.getPerson(), chargingMode); + ElectricVehicle vehicle = electricFleet.getElectricVehicles().get(vehicleId); + relevantVehicles.add(vehicleId); + + Activity firstActivity = (Activity) plan.getPlanElements().get(0); + Activity lastActivity = (Activity) plan.getPlanElements().get(plan.getPlanElements().size() - 1); + Activity overnightActivity = findOvernightActivity(plan); + + ChargingSlot overnightSlot = null; + ChargingSlot wholeDaySlot = null; + boolean foundRegularSlot = false; + + List slots = slotProvider.findSlots(plan.getPerson(), plan, vehicle); + Collections.sort(slots, (first, second) -> { + int firstIndex = plan.getPlanElements() + .indexOf(first.isLegBased() ? first.leg() : first.startActivity()); + + int secondIndex = plan.getPlanElements() + .indexOf(second.isLegBased() ? second.leg() : second.startActivity()); + + return Integer.compare(firstIndex, secondIndex); + }); + + for (ChargingSlot slot : slots) { + if (slot.startActivity() == firstActivity && slot.endActivity() == lastActivity) { + // special case: this is a slot spanning the whole plan + Preconditions.checkState(slot.leg() == null); + Preconditions.checkState(!foundRegularSlot); + Preconditions.checkState(overnightSlot == null); + Preconditions.checkState(wholeDaySlot == null); + wholeDaySlot = slot; + wholeDayCount++; + } else if (slot.startActivity() == firstActivity && slot.endActivity() == overnightActivity) { + // special case: this slot has started on the "previous day" and the vehicle + // only needs to be unplugged after the overnight activity. In order to simplify + // time calculation for scheduling, we treat this slot last + Preconditions.checkState(slot.leg() == null); + Preconditions.checkState(overnightSlot == null); + Preconditions.checkState(wholeDaySlot == null); + overnightSlot = slot; + overnightCount++; + } else if (slot.startActivity() != null && slot.endActivity() != null) { + // standard case: schedule a plug activity along the plan + Preconditions.checkState(slot.leg() == null); + Preconditions.checkState(wholeDaySlot == null); + Activity plugActivity = chargingScheduler.scheduleInitialPlugActivity(agent, + slot.startActivity(), slot.charger()); + plugActivity.getAttributes().putAttribute(CHARGING_SLOT_ATTRIBUTE, slot); + activityBasedCount++; + } else { + // leg case: schedule a plug activity along a leg + Preconditions.checkState(slot.startActivity() == null); + Preconditions.checkState(slot.endActivity() == null); + Preconditions.checkState(slot.leg() != null); + + Activity plugActivity = chargingScheduler.scheduleOnroutePlugActivity(agent, slot.leg(), + slot.charger()); + plugActivity.getAttributes().putAttribute(CHARGING_SLOT_ATTRIBUTE, slot); + legBasedCount++; + } + } + + if (overnightSlot != null) { + startOvernightCharging(agent, overnightSlot); + updateInitialVehicleLocation(plan, vehicleId, overnightSlot); + } + + if (wholeDaySlot != null) { + startWholeDayCharging(agent, wholeDaySlot); + updateInitialVehicleLocation(plan, vehicleId, wholeDaySlot); + } + } + } + } + + logger.info(String.format(" activity: %d, leg: %d, overnight: %d, whole day: %d", activityBasedCount, + legBasedCount, overnightCount, wholeDayCount)); + } + + private Activity findOvernightActivity(Plan plan) { + for (Trip trip : TripStructureUtils.getTrips(plan)) { + String mode = TripStructureUtils.getRoutingModeIdentifier().identifyMainMode(trip.getTripElements()); + + if (mode.equals(chargingMode)) { + Id originLinkId = PopulationUtils.decideOnLinkIdForActivity(trip.getOriginActivity(), scenario); + Id destinationLinkId = PopulationUtils.decideOnLinkIdForActivity(trip.getDestinationActivity(), + scenario); + + if (!originLinkId.equals(destinationLinkId)) { + return trip.getOriginActivity(); + } + } + } + + return null; + } + + private void updateInitialVehicleLocation(Plan plan, Id vehicleId, ChargingSlot slot) { + MobsimVehicle vehicle = qsim.getVehicles().get(vehicleId); + + if (vehicle == null) { + Vehicle vehicleData = vehicles.getVehicles().get(vehicleId); + vehicle = qVehicleFactory.createQVehicle(vehicleData); + qsim.addParkedVehicle(vehicle, slot.charger().getLink().getId()); + } + + Id initialLinkId = vehicle.getCurrentLink().getId(); + + QLinkI originalLink = (QLinkI) qsim.getNetsimNetwork().getNetsimLink(initialLinkId); + QVehicle qVehicle = originalLink.removeParkedVehicle(vehicleId); + Preconditions.checkNotNull(qVehicle); + + QLinkI updatedLink = (QLinkI) qsim.getNetsimNetwork().getNetsimLink(slot.charger().getLink().getId()); + updatedLink.addParkedVehicle(qVehicle); + } + + private void startOvernightCharging(MobsimAgent agent, ChargingSlot slot) { + Activity endActivity = slot.endActivity(); + endActivity.getAttributes().putAttribute(CHARGING_SLOT_ATTRIBUTE, slot); + + double now = internalInterface.getMobsim().getSimTimer().getSimStartTime(); + ChargingProcess process = createChargingProcess(agent.getId(), now, slot, null, false); + process.isOvernight = true; + + // Handle end time from here + + Preconditions.checkState(endActivity.getEndTime().isDefined()); + double endTime = endActivity.getEndTime().seconds(); + + endActivity.getAttributes().putAttribute(INITIAL_ACTIVITY_END_TIME_ATTRIBUTE, endTime); + endActivity.setEndTime(Double.MAX_VALUE); + + WithinDayAgentUtils.resetCaches(agent); + + /* + * We would usually include the following line, but we must not call it here + * because it is called automatically by the QSim at simulation startup. + * Otherwise, the agent will appear twice in the agent queue: + * + * WithinDayAgentUtils.rescheduleActivityEnd(agent, qsim); + */ + + plugging.put(process.vehicle.getId(), process); + } + + private void startWholeDayCharging(MobsimAgent agent, ChargingSlot slot) { + double now = internalInterface.getMobsim().getSimTimer().getSimStartTime(); + ChargingProcess process = createChargingProcess(agent.getId(), now, slot, null, false); + process.isWholeDay = true; + + plugging.put(process.vehicle.getId(), process); + } + + // EVENT COLLECTION + + private final List personDepartureEvents = new LinkedList<>(); + private final List activityStartEvents = new LinkedList<>(); + private final List queuedAtChargerEvents = new LinkedList<>(); + private final List chargingStartEvents = new LinkedList<>(); + + @Override + public void handleEvent(PersonDepartureEvent event) { + if (event.getLegMode().equals(chargingMode) && relevantPersons.contains(event.getPersonId())) { + synchronized (personDepartureEvents) { + personDepartureEvents.add(event); + } + } + } + + @Override + public void handleEvent(ActivityStartEvent event) { + if (event.getActType().equals(PLUG_ACTIVITY_TYPE) || event.getActType().equals(UNPLUG_ACTIVITY_TYPE)) { + synchronized (activityStartEvents) { + activityStartEvents.add(event); + } + } + } + + @Override + public void handleEvent(QueuedAtChargerEvent event) { + if (relevantVehicles.contains(event.getVehicleId())) { + synchronized (queuedAtChargerEvents) { + queuedAtChargerEvents.add(event); + } + } + } + + @Override + public void handleEvent(ChargingStartEvent event) { + if (relevantVehicles.contains(event.getVehicleId())) { + synchronized (chargingStartEvents) { + chargingStartEvents.add(event); + } + } + } + + // MANAGING CHARGING PROCESSES + + private class ChargingProcess { + MobsimAgent agent; + ElectricVehicle vehicle; + + // search process + boolean isFirstAttempt = true; + int attemptIndex = 0; + int processIndex; + + // plugging process + double latestPlugTime; + + // charging slots + ChargingSlot initialSlot; + ChargingSlot currentSlot; + List trace = new LinkedList<>(); + + // state variables trigger by events + boolean isSubmitted = false; + boolean isQueued = false; + boolean isPlugged = false; + + // markers for special cases + boolean isOvernight = false; + boolean isWholeDay = false; + } + + private ChargingProcess createChargingProcessFromPlugActivity(Id personId, double now) { + MobsimAgent agent = qsim.getAgents().get(personId); + Plan plan = WithinDayAgentUtils.getModifiablePlan(agent); + + int plugActivityIndex = WithinDayAgentUtils.getCurrentPlanElementIndex(agent); + Activity plugActivity = (Activity) plan.getPlanElements().get(plugActivityIndex); + + return createChargingProcessFromPlugActivity(personId, now, plugActivity, false); + } + + private ChargingProcess createChargingProcessFromLeg(Id personId, double now) { + MobsimAgent agent = qsim.getAgents().get(personId); + Plan plan = WithinDayAgentUtils.getModifiablePlan(agent); + + int legIndex = WithinDayAgentUtils.getCurrentPlanElementIndex(agent); + Leg leg = (Leg) plan.getPlanElements().get(legIndex); + Preconditions.checkState(leg.getMode().equals(chargingMode)); + + Activity plugActivity = findFollowingPlugActivity(agent, plan); + if (plugActivity == null) { + // can only happening when creating charging process from a leg + return null; + } + + return createChargingProcessFromPlugActivity(personId, now, plugActivity, false); + } + + private ChargingProcess createChargingProcessFromPlugActivity(Id personId, double now, + Activity plugActivity, + boolean isSpontaneous) { + Preconditions.checkState(plugActivity.getType().equals(PLUG_ACTIVITY_TYPE)); + + ChargingProcess chargingProcess = (ChargingProcess) plugActivity.getAttributes() + .getAttribute(CHARGING_PROCESS_ATTRIBUTE); + if (chargingProcess != null) { + // we are continuing an ongoing search process + chargingProcess.isFirstAttempt = false; + return chargingProcess; + } + + ChargingSlot slot = (ChargingSlot) plugActivity.getAttributes().getAttribute(CHARGING_SLOT_ATTRIBUTE); + return createChargingProcess(personId, now, slot, plugActivity, isSpontaneous); + } + + private final IdMap chargingProcessIndex = new IdMap<>(Person.class); + + private ChargingProcess createChargingProcess(Id personId, double now, ChargingSlot slot, + Activity plugActivity, + boolean isSpontaneous) { + MobsimAgent agent = qsim.getAgents().get(personId); + Plan plan = WithinDayAgentUtils.getModifiablePlan(agent); + + Id vehicleId = VehicleUtils.getVehicleId(plan.getPerson(), chargingMode); + ElectricVehicle vehicle = electricFleet.getElectricVehicles().get(vehicleId); + + ChargingProcess process = new ChargingProcess(); + process.processIndex = chargingProcessIndex.compute(personId, (id, value) -> { + return value == null ? 0 : value + 1; + }); + process.agent = agent; + process.vehicle = vehicle; + process.currentSlot = slot; + process.initialSlot = slot; + + eventsManager.processEvent(new StartChargingProcessEvent(now, process.agent.getId(), process.vehicle.getId(), + process.processIndex)); + eventsManager.processEvent( + new StartChargingAttemptEvent(now, personId, vehicleId, process.currentSlot.charger().getId(), + process.attemptIndex, process.processIndex, process.currentSlot.isLegBased(), isSpontaneous, + process.currentSlot.duration())); + + if (plugActivity != null) { + plugActivity.getAttributes().putAttribute(CHARGING_PROCESS_ATTRIBUTE, process); + } + + return process; + } + + private Activity findFollowingPlugActivity(MobsimAgent agent, Plan plan) { + int currentIndex = WithinDayAgentUtils.getCurrentPlanElementIndex(agent); + PlanElement currentElement = plan.getPlanElements().get(currentIndex); + Preconditions.checkState(currentElement instanceof Leg); + + for (int k = currentIndex + 1; k < plan.getPlanElements().size(); k++) { + PlanElement element = plan.getPlanElements().get(k); + + if (element instanceof Activity activity) { + if (!TripStructureUtils.isStageActivityType(activity.getType()) + || isManagedActivityType(activity.getType())) { + if (activity.getType().equals(PLUG_ACTIVITY_TYPE)) { + return activity; + } else { + return null; // there is no plug activity between here and next main activity + } + } + } + } + + return null; + } + + // ENGINE LOGIC + + @Override + public void doSimStep(double time) { + // first process collected events + processPersonDepartureEvents(time); + processActivityStartEvents(time); + processQueuedAtChargerEvents(time); + processChargingStartEvents(time); + + // next advance logic + processApproachingProcesses(time); + processPluggingProcesses(time); + processUnpluggingProcesses(time); + } + + private IdSet approaching = new IdSet<>(Person.class); + private IdMap plugging = new IdMap<>(Vehicle.class); + private IdMap active = new IdMap<>(Person.class); + private IdMap unplugging = new IdMap<>(Vehicle.class); + + private void processPersonDepartureEvents(double now) { + synchronized (personDepartureEvents) { + var iterator = personDepartureEvents.iterator(); + + while (iterator.hasNext()) { + PersonDepartureEvent event = iterator.next(); + + if (event.getTime() < now) { + iterator.remove(); + approaching.add(event.getPersonId()); + } + } + } + } + + private void processActivityStartEvents(double now) { + synchronized (activityStartEvents) { + var iterator = activityStartEvents.iterator(); + + while (iterator.hasNext()) { + ActivityStartEvent event = iterator.next(); + + if (event.getTime() < now) { + iterator.remove(); + + if (event.getActType().equals(PLUG_ACTIVITY_TYPE)) { + ChargingProcess process = createChargingProcessFromPlugActivity(event.getPersonId(), now); + plugging.put(process.vehicle.getId(), process); + } else if (event.getActType().equals(UNPLUG_ACTIVITY_TYPE)) { + ChargingProcess process = active.remove(event.getPersonId()); + Preconditions.checkNotNull(process); + unplugging.put(process.vehicle.getId(), process); + } + } + } + } + } + + private void processQueuedAtChargerEvents(double now) { + synchronized (queuedAtChargerEvents) { + var iterator = queuedAtChargerEvents.iterator(); + + while (iterator.hasNext()) { + QueuedAtChargerEvent event = iterator.next(); + + if (event.getTime() < now - 1.0) { // -1.0 because event is generated in afterSimStepListener + iterator.remove(); + + ChargingProcess process = plugging.get(event.getVehicleId()); + + if (process != null) { + process.isQueued = true; + } + } + } + } + } + + private void processChargingStartEvents(double now) { + synchronized (chargingStartEvents) { + var iterator = chargingStartEvents.iterator(); + + while (iterator.hasNext()) { + ChargingStartEvent event = iterator.next(); + + if (event.getTime() < now - 1.0) { // -1.0 because event is generated in afterSimStepListener + iterator.remove(); + + ChargingProcess process = plugging.get(event.getVehicleId()); + + if (process != null) { + process.isPlugged = true; + } + } + } + } + } + + private void processApproachingProcesses(double time) { + for (Id personId : approaching) { + MobsimAgent agent = qsim.getAgents().get(personId); + Plan plan = WithinDayAgentUtils.getModifiablePlan(agent); + int currentIndex = WithinDayAgentUtils.getCurrentPlanElementIndex(agent); + + if (plan.getPlanElements().get(currentIndex) instanceof Leg leg) { + if (leg.getMode().equals(chargingMode)) { + ChargingProcess process = createChargingProcessFromLeg(personId, time); + + if (process != null && process.isFirstAttempt) { + // a plug activity was found and we may implement an alternative proposal + + ChargingAlternative alternative = alternativeProvider.findEnrouteAlternative(time, + plan.getPerson(), + plan, + process.vehicle, process.currentSlot); + + if (alternative != null) { + if (process.currentSlot.isLegBased() && !alternative.isLegBased()) { + throw new IllegalStateException( + "Cannot switch from a leg-based charging slot to an activity-based alternative because activities are not known"); + } + + if (alternative.charger() != process.currentSlot.charger()) { + Activity followingPlugActivity = findFollowingPlugActivity(agent, plan); + + // drive to different charger and schedule a plug activity + Activity plugActivity = chargingScheduler.changePlugActivity(process.agent, + followingPlugActivity, alternative.charger(), + time); + plugActivity.getAttributes().putAttribute(CHARGING_PROCESS_ATTRIBUTE, process); + + // update slot + process.currentSlot = new ChargingSlot(process.currentSlot.startActivity(), + process.currentSlot.endActivity(), + process.currentSlot.leg(), alternative.duration(), + alternative.charger()); + + // send event for scoring + eventsManager.processEvent(new UpdateChargingAttemptEvent(time, process.agent.getId(), + process.vehicle.getId(), alternative.charger().getId(), + alternative.isLegBased(), alternative.duration())); + } else if (alternative.duration() != process.currentSlot.duration()) { + // update slot with custom duration (either switch between leg- and + // activity-based slot, or change of duration) + process.currentSlot = new ChargingSlot(process.currentSlot.startActivity(), + process.currentSlot.endActivity(), + process.currentSlot.leg(), alternative.duration(), + process.currentSlot.charger()); + + // send event for scoring + eventsManager.processEvent(new UpdateChargingAttemptEvent(time, process.agent.getId(), + process.vehicle.getId(), alternative.charger().getId(), + alternative.isLegBased(), alternative.duration())); + } + } + } else if (process == null && allowSpontaneousCharging) { + // no upcoming plug activity is found, this is a completely spantaneous charging + // attempt + + Id vehicleId = VehicleUtils.getVehicleId(plan.getPerson(), chargingMode); + ElectricVehicle vehicle = electricFleet.getElectricVehicles().get(vehicleId); + + ChargingAlternative alternative = alternativeProvider.findEnrouteAlternative(time, + plan.getPerson(), plan, vehicle, null); + + if (alternative != null) { + ChargingSlot slot = new ChargingSlot(leg, alternative.duration(), + alternative.charger()); + + Activity plugActivity = chargingScheduler.insertPlugActivity(agent, + alternative.charger(), time); + plugActivity.getAttributes().putAttribute(CHARGING_SLOT_ATTRIBUTE, slot); + + createChargingProcessFromPlugActivity(personId, time, plugActivity, true); + } + } + } + } + } + + approaching.clear(); + } + + private void processPluggingProcesses(double now) { + Iterator iterator = plugging.values().iterator(); + + while (iterator.hasNext()) { + ChargingProcess process = iterator.next(); + + if (!process.isSubmitted) { + Double personMaximumQueueWaitTime = getMaximumQueueTime( + ((HasModifiablePlan) process.agent).getModifiablePlan() + .getPerson()); + + if (personMaximumQueueWaitTime == null) { + personMaximumQueueWaitTime = maximumQueueWaitTime; + } + + // add vehicle to charger, it will be either queued or plugged + ChargingStrategy chargingStrategy = chargingStrategyFactory + .createStrategy(process.currentSlot.charger().getSpecification(), process.vehicle); + process.currentSlot.charger().getLogic().addVehicle(process.vehicle, chargingStrategy, now); + process.latestPlugTime = now + personMaximumQueueWaitTime; + process.isSubmitted = true; + } else if (process.isPlugged) { + // vehicle has been plugged -> continue to the main activity + + if (process.isWholeDay) { + // do nothing, vehicle stays plugged the whole day + } else if (process.isOvernight) { + // vehicle was plugged overnight, reset end time of the first activity after + // which the vehilce will be picked up and schedule the pickup walk + double plannedEndTime = (Double) process.currentSlot.endActivity().getAttributes() + .getAttribute(INITIAL_ACTIVITY_END_TIME_ATTRIBUTE); + process.currentSlot.endActivity().setEndTime(Math.max(now, plannedEndTime)); + + // following only necessary if this is the very first activity of the day + WithinDayAgentUtils.resetCaches(process.agent); + WithinDayAgentUtils.rescheduleActivityEnd(process.agent, qsim); + + chargingScheduler.scheduleUnplugActivityAfterOvernightCharge(process.agent, + process.currentSlot.endActivity(), process.currentSlot.charger()); + } else { + // stadard case, we are in a plug activity, need to end it, and let agent to to + // main activity + Activity plugActivity = (Activity) WithinDayAgentUtils.getCurrentPlanElement(process.agent); + Preconditions.checkState(plugActivity.getType().equals(PLUG_ACTIVITY_TYPE)); + + // end activity + plugActivity.setEndTime(now); + WithinDayAgentUtils.resetCaches(process.agent); + WithinDayAgentUtils.rescheduleActivityEnd(process.agent, qsim); + + if (process.currentSlot.isLegBased()) { + // schedule unplug at the charger then continue to main activity + chargingScheduler.scheduleUnplugActivityAtCharger(process.agent, + process.currentSlot.duration()); + } else { + // walk to main activity, perform it, walk back to charger and unplug + chargingScheduler.scheduleUntilUnplugActivity(process.agent, + process.currentSlot.startActivity(), + process.currentSlot.endActivity()); + } + } + + active.put(process.agent.getId(), process); + iterator.remove(); + } else if (process.isQueued) { + if (now > process.latestPlugTime) { + // remove vehicle from charger + process.currentSlot.charger().getLogic().removeVehicle(process.vehicle, now); + + // remove from plugging processes + iterator.remove(); + + eventsManager + .processEvent( + new AbortChargingAttemptEvent(now, process.agent.getId(), process.vehicle.getId())); + + if (process.isWholeDay || process.isOvernight) { + // did not succeed charging overnight + // agent may be in any potential state along the plan + + // send event for scoring + eventsManager.processEvent( + new AbortChargingProcessEvent(now, process.agent.getId(), process.vehicle.getId())); + + if (performAbort) { + // abort the agent + process.currentSlot.endActivity().setEndTime(Double.POSITIVE_INFINITY); + WithinDayAgentUtils.resetCaches(process.agent); + WithinDayAgentUtils.rescheduleActivityEnd(process.agent, qsim); + + process.agent.setStateToAbort(now); + internalInterface.arrangeNextAgentState(process.agent); + } else if (process.isOvernight) { + Activity endActivity = process.currentSlot.endActivity(); + double initialEndTime = (Double) endActivity.getAttributes() + .getAttribute(INITIAL_ACTIVITY_END_TIME_ATTRIBUTE); + + // end current plug activity + endActivity.setEndTime(Math.max(now, initialEndTime)); + WithinDayAgentUtils.resetCaches(process.agent); + WithinDayAgentUtils.rescheduleActivityEnd(process.agent, qsim); + + chargingScheduler.scheduleAccessAfterOvernightCharge(process.agent, + process.currentSlot.endActivity(), + process.currentSlot.charger()); + } + } else { + // stadnard case + Activity plugActivity = (Activity) WithinDayAgentUtils + .getCurrentPlanElement(process.agent); + Preconditions.checkState(plugActivity.getType().equals(PLUG_ACTIVITY_TYPE)); + + // reset charging process + process.attemptIndex++; + + // try to find next charger + Plan plan = WithinDayAgentUtils.getModifiablePlan(process.agent); + ChargingAlternative alternative = alternativeProvider.findAlternative(now, + plan.getPerson(), + plan, + process.vehicle, process.initialSlot, process.trace); + + if (alternative != null) { + // found an alternative charger + if (process.currentSlot.isLegBased() && !alternative.isLegBased()) { + throw new IllegalStateException( + "Cannot switch from a leg-based charging slot to an activity-based alternative because activities are not known"); + } + + // end current plug activity + plugActivity.setEndTime(now); + WithinDayAgentUtils.resetCaches(process.agent); + WithinDayAgentUtils.rescheduleActivityEnd(process.agent, qsim); + + // drive to the next charger and schedule a plug activity + plugActivity = chargingScheduler.scheduleSubsequentPlugActivity(process.agent, + plugActivity, alternative.charger(), now); + plugActivity.getAttributes().putAttribute(CHARGING_PROCESS_ATTRIBUTE, process); + + // reset process for next attempt + process.currentSlot = new ChargingSlot(process.currentSlot.startActivity(), + process.currentSlot.endActivity(), + process.currentSlot.leg(), alternative.duration(), + alternative.charger()); + process.isSubmitted = false; + process.isPlugged = false; + process.isQueued = false; + process.trace.add(alternative); + + // send event for scoring + eventsManager.processEvent( + new StartChargingAttemptEvent(now, process.agent.getId(), process.vehicle.getId(), + alternative.charger().getId(), process.attemptIndex, process.processIndex, + alternative.isLegBased(), false, alternative.duration())); + } else { + // send event for scoring + eventsManager.processEvent( + new AbortChargingProcessEvent(now, process.agent.getId(), process.vehicle.getId())); + + if (performAbort) { + // we abort the agent + plugActivity.setEndTime(Double.POSITIVE_INFINITY); + WithinDayAgentUtils.resetCaches(process.agent); + WithinDayAgentUtils.rescheduleActivityEnd(process.agent, qsim); + + process.agent.setStateToAbort(now); + internalInterface.arrangeNextAgentState(process.agent); + } else { + // end current plug activity + plugActivity.setEndTime(now); + WithinDayAgentUtils.resetCaches(process.agent); + WithinDayAgentUtils.rescheduleActivityEnd(process.agent, qsim); + + chargingScheduler.scheduleDriveToNextActivity(process.agent); + } + } + } + } + } // else: we are waiting to be queued or plugged, don't do anything + } + + } + + private void processUnpluggingProcesses(double now) { + Iterator iterator = unplugging.values().iterator(); + + while (iterator.hasNext()) { + ChargingProcess process = iterator.next(); + + // remove vehicle from charger, but may already be done + if (process.currentSlot.charger().getLogic().getPluggedVehicles().contains(process.vehicle)) { + process.currentSlot.charger().getLogic().removeVehicle(process.vehicle, now); + } else { + logger.warn(String.format( + "Agent %s tried to unplug vehicle %s at charger %s, but was already unplugged. Is the correct ChargingStrategy configured?", + process.agent.getId().toString(), process.vehicle.getId().toString(), + process.currentSlot.charger().getId().toString())); + } + + Activity unplugActivity = (Activity) WithinDayAgentUtils.getCurrentPlanElement(process.agent); + Preconditions.checkState(unplugActivity.getType().equals(UNPLUG_ACTIVITY_TYPE)); + + unplugActivity.setEndTime(now); + WithinDayAgentUtils.resetCaches(process.agent); + WithinDayAgentUtils.rescheduleActivityEnd(process.agent, qsim); + + chargingScheduler.scheduleDriveToNextActivity(process.agent); + + eventsManager + .processEvent(new FinishChargingAttemptEvent(now, process.agent.getId(), process.vehicle.getId())); + eventsManager + .processEvent(new FinishChargingProcessEvent(now, process.agent.getId(), process.vehicle.getId())); + + iterator.remove(); + } + } + + // BOILERPLATE + + @Override + public void afterSim() { + } + + private InternalInterface internalInterface; + + @Override + public void setInternalInterface(InternalInterface internalInterface) { + this.internalInterface = internalInterface; + } + + /** + * Checks whether a person is managed by within-day electric vehicle charging + */ + static public boolean isActive(Person person) { + Boolean isActive = (Boolean) person.getAttributes().getAttribute(ACTIVE_PERSON_ATTRIBUTE); + return isActive != null && isActive; + } + + /** + * Sets a person to be active in within-day electric vehicle charging or not + */ + static public void setActive(Person person, boolean isActive) { + person.getAttributes().putAttribute(ACTIVE_PERSON_ATTRIBUTE, isActive); + } + + /** + * Activates a person for within-day electric vehicle charging + */ + static public void activate(Person person) { + setActive(person, true); + } + + /** + * Retrieves the maximum queue time for a person before an attempt is aborted + */ + static public Double getMaximumQueueTime(Person person) { + return (Double) person.getAttributes().getAttribute(MAXIMUM_QUEUE_TIME_PERSON_ATTRIBUTE); + } + + /** + * Sets the maximum queue time for a person before an attempt is aborted + */ + static public void setMaximumQueueTime(Person person, double maximumQueueTime) { + person.getAttributes().putAttribute(MAXIMUM_QUEUE_TIME_PERSON_ATTRIBUTE, maximumQueueTime); + } + + /** + * Determines whether an activity type is managed in a special way by within-day + * electric vehicle charging + */ + static public boolean isManagedActivityType(String activityType) { + return activityType.equals(PLUG_ACTIVITY_TYPE) || activityType.equals(UNPLUG_ACTIVITY_TYPE) + || activityType.equals(WAIT_ACTIVITY_TYPE) || activityType.equals(ACCESS_ACTIVITY_TYPE); + } +} diff --git a/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/WithinDayEvModule.java b/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/WithinDayEvModule.java new file mode 100644 index 00000000000..b8629a12e91 --- /dev/null +++ b/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/WithinDayEvModule.java @@ -0,0 +1,37 @@ +package org.matsim.contrib.ev.withinday; + +import org.matsim.contrib.ev.withinday.analysis.WithinDayChargingAnalysisHandler; +import org.matsim.contrib.ev.withinday.analysis.WithinDayChargingAnalysisListener; +import org.matsim.core.controler.AbstractModule; +import org.matsim.core.controler.OutputDirectoryHierarchy; + +import com.google.inject.Provides; +import com.google.inject.Singleton; + +/** + * This module is the main entry point for within-day electric vehicle charging + * (WEVC). + * + * @author Sebastian Hörl (sebhoerl), IRT SystemX + */ +public class WithinDayEvModule extends AbstractModule { + @Override + public void install() { + installQSimModule(new WithinDayEvQSimModule()); + addControlerListenerBinding().to(WithinDayChargingAnalysisListener.class); + addEventHandlerBinding().to(WithinDayChargingAnalysisHandler.class); + } + + @Provides + @Singleton + WithinDayChargingAnalysisListener provideWithinDayChargingAnalysisListener(WithinDayChargingAnalysisHandler handler, + OutputDirectoryHierarchy outputDirectoryHierarchy) { + return new WithinDayChargingAnalysisListener(handler, outputDirectoryHierarchy); + } + + @Provides + @Singleton + WithinDayChargingAnalysisHandler provideWithinDayChargingAnalysisHandler() { + return new WithinDayChargingAnalysisHandler(); + } +} diff --git a/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/WithinDayEvQSimModule.java b/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/WithinDayEvQSimModule.java new file mode 100644 index 00000000000..d36cc8e33df --- /dev/null +++ b/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/WithinDayEvQSimModule.java @@ -0,0 +1,75 @@ +package org.matsim.contrib.ev.withinday; + +import org.matsim.api.core.v01.Scenario; +import org.matsim.api.core.v01.network.Network; +import org.matsim.api.core.v01.population.Population; +import org.matsim.contrib.ev.EvModule; +import org.matsim.contrib.ev.fleet.ElectricFleet; +import org.matsim.core.api.experimental.events.EventsManager; +import org.matsim.core.mobsim.qsim.AbstractQSimModule; +import org.matsim.core.mobsim.qsim.QSim; +import org.matsim.core.mobsim.qsim.qnetsimengine.QVehicleFactory; +import org.matsim.core.router.RoutingModule; +import org.matsim.core.utils.timing.TimeInterpretation; +import org.matsim.facilities.ActivityFacilities; +import org.matsim.vehicles.Vehicles; + +import com.google.inject.Key; +import com.google.inject.Provides; +import com.google.inject.Singleton; +import com.google.inject.name.Named; +import com.google.inject.name.Names; + +/** + * This module manages the QSim components for within-day electric vehicle + * charging (WEVC). + * + * @author Sebastian Hörl (sebhoerl), IRT SystemX + */ +public class WithinDayEvQSimModule extends AbstractQSimModule { + static public final String ROAD_MODE_BINDING = "ev:road"; + static public final String WALK_MODE_BINDING = "ev:walk"; + + @Override + protected void configureQSim() { + WithinDayEvConfigGroup config = WithinDayEvConfigGroup.get(getConfig()); + + addQSimComponentBinding(EvModule.EV_COMPONENT).to(WithinDayEvEngine.class); + addMobsimScopeEventHandlerBinding().to(WithinDayEvEngine.class); + + bind(Key.get(RoutingModule.class, Names.named(ROAD_MODE_BINDING))) + .to(Key.get(RoutingModule.class, Names.named(config.carMode))); + bind(Key.get(RoutingModule.class, Names.named(WALK_MODE_BINDING))) + .to(Key.get(RoutingModule.class, Names.named(config.walkMode))); + + bind(ChargingSlotProvider.class).toInstance(ChargingSlotProvider.NOOP); + bind(ChargingAlternativeProvider.class).toInstance(ChargingAlternativeProvider.NOOP); + } + + @Provides + @Singleton + WithinDayEvEngine provideEvPlanningEngine(QSim qsim, ElectricFleet electricFleet, + ChargingAlternativeProvider alternativeProvider, ChargingSlotProvider slotProvider, + EventsManager eventsManager, + ChargingScheduler chargingScheduler, WithinDayEvConfigGroup config, Vehicles vehicles, + QVehicleFactory qVehicleFactory, Scenario scenario, + WithinDayChargingStrategy.Factory chargingStrategyFactory) { + return new WithinDayEvEngine(config, qsim, electricFleet, alternativeProvider, slotProvider, + eventsManager, chargingScheduler, vehicles, qVehicleFactory, scenario, chargingStrategyFactory); + } + + @Provides + @Singleton + ChargingScheduler provideChargingScheduler(Population population, TimeInterpretation timeInterpretation, + ActivityFacilities facilities, @Named(ROAD_MODE_BINDING) RoutingModule roadRoutingModule, + @Named(WALK_MODE_BINDING) RoutingModule walkRoutingModule, Network network) { + return new ChargingScheduler(population.getFactory(), timeInterpretation, facilities, roadRoutingModule, + walkRoutingModule, network); + } + + @Provides + @Singleton + WithinDayChargingStrategy.Factory provideWithinDayChargingStrategyFactory() { + return new WithinDayChargingStrategy.Factory(); + } +} diff --git a/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/WithinDayEvUtils.java b/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/WithinDayEvUtils.java new file mode 100644 index 00000000000..23e2d3c07e8 --- /dev/null +++ b/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/WithinDayEvUtils.java @@ -0,0 +1,49 @@ +package org.matsim.contrib.ev.withinday; + +import org.matsim.api.core.v01.population.Person; + +/** + * This is a convenience class that helps preapring the relevant scenario + * attributes for within-day electric vehicle charging. + * + * @author Sebastian Hörl (sebhoerl), IRT SystemX + */ +public class WithinDayEvUtils { + private WithinDayEvUtils() { + } + + /** + * Checks whether a person is managed by within-day electric vehicle charging + */ + static public boolean isActive(Person person) { + return WithinDayEvEngine.isActive(person); + } + + /** + * Sets a person to be active in within-day electric vehicle charging or not + */ + static public void setActive(Person person, boolean isActive) { + WithinDayEvEngine.setActive(person, isActive); + } + + /** + * Activates a person for within-day electric vehicle charging + */ + static public void activate(Person person) { + WithinDayEvEngine.activate(person); + } + + /** + * Retrieves the maximum queue time for a person before an attempt is aborted + */ + static public Double getMaximumQueueTime(Person person) { + return WithinDayEvEngine.getMaximumQueueTime(person); + } + + /** + * Sets the maximum queue time for a person before an attempt is aborted + */ + static public void setMaximumQueueTime(Person person, double maximumQueueTime) { + WithinDayEvEngine.setMaximumQueueTime(person, maximumQueueTime); + } +} diff --git a/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/analysis/WithinDayChargingAnalysisHandler.java b/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/analysis/WithinDayChargingAnalysisHandler.java new file mode 100644 index 00000000000..a05cc776c13 --- /dev/null +++ b/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/analysis/WithinDayChargingAnalysisHandler.java @@ -0,0 +1,312 @@ +package org.matsim.contrib.ev.withinday.analysis; + +import java.util.LinkedList; +import java.util.List; + +import org.matsim.api.core.v01.Id; +import org.matsim.api.core.v01.IdMap; +import org.matsim.api.core.v01.population.Person; +import org.matsim.contrib.ev.EvUnits; +import org.matsim.contrib.ev.charging.ChargingEndEvent; +import org.matsim.contrib.ev.charging.ChargingEndEventHandler; +import org.matsim.contrib.ev.charging.ChargingStartEvent; +import org.matsim.contrib.ev.charging.ChargingStartEventHandler; +import org.matsim.contrib.ev.charging.EnergyChargedEvent; +import org.matsim.contrib.ev.charging.EnergyChargedEventHandler; +import org.matsim.contrib.ev.charging.QueuedAtChargerEvent; +import org.matsim.contrib.ev.charging.QueuedAtChargerEventHandler; +import org.matsim.contrib.ev.charging.QuitQueueAtChargerEvent; +import org.matsim.contrib.ev.charging.QuitQueueAtChargerEventHandler; +import org.matsim.contrib.ev.infrastructure.Charger; +import org.matsim.contrib.ev.withinday.events.AbortChargingAttemptEvent; +import org.matsim.contrib.ev.withinday.events.AbortChargingAttemptEventHandler; +import org.matsim.contrib.ev.withinday.events.AbortChargingProcessEvent; +import org.matsim.contrib.ev.withinday.events.AbortChargingProcessEventHandler; +import org.matsim.contrib.ev.withinday.events.FinishChargingAttemptEvent; +import org.matsim.contrib.ev.withinday.events.FinishChargingAttemptEventHandler; +import org.matsim.contrib.ev.withinday.events.FinishChargingProcessEvent; +import org.matsim.contrib.ev.withinday.events.FinishChargingProcessEventHandler; +import org.matsim.contrib.ev.withinday.events.StartChargingAttemptEvent; +import org.matsim.contrib.ev.withinday.events.StartChargingAttemptEventHandler; +import org.matsim.contrib.ev.withinday.events.StartChargingProcessEvent; +import org.matsim.contrib.ev.withinday.events.StartChargingProcessEventHandler; +import org.matsim.contrib.ev.withinday.events.UpdateChargingAttemptEvent; +import org.matsim.contrib.ev.withinday.events.UpdateChargingAttemptEventHandler; +import org.matsim.vehicles.Vehicle; + +import com.google.common.base.Preconditions; + +/** + * Tracks detailed information on the electric vehilce charging processes and + * attempts. + * + * @author Sebastian Hörl (sebhoerl), IRT SystemX + */ +public class WithinDayChargingAnalysisHandler implements // + StartChargingProcessEventHandler, + AbortChargingProcessEventHandler, FinishChargingProcessEventHandler, // + StartChargingAttemptEventHandler, UpdateChargingAttemptEventHandler, FinishChargingAttemptEventHandler, + AbortChargingAttemptEventHandler, // + ChargingStartEventHandler, + ChargingEndEventHandler, QueuedAtChargerEventHandler, QuitQueueAtChargerEventHandler, + EnergyChargedEventHandler { + private final List chargingItems = new LinkedList<>(); + private final List chargingAttemptItems = new LinkedList<>(); + private boolean finished = false; + + private IdMap active = new IdMap<>(Vehicle.class); + + @Override + public void reset(int iteration) { + chargingItems.clear(); + chargingAttemptItems.clear(); + finished = false; + active.clear(); + } + + @Override + public void handleEvent(StartChargingProcessEvent event) { + Preconditions.checkState(!active.containsKey(event.getVehicleId())); + active.put(event.getVehicleId(), new ChargingProcessTracker(event)); + } + + @Override + public void handleEvent(AbortChargingProcessEvent event) { + ChargingProcessTracker process = active.remove(event.getVehicleId()); + process.abort = event; + registerProcess(process); + } + + @Override + public void handleEvent(FinishChargingProcessEvent event) { + ChargingProcessTracker process = active.remove(event.getVehicleId()); + process.finish = event; + registerProcess(process); + } + + @Override + public void handleEvent(StartChargingAttemptEvent event) { + active.get(event.getVehicleId()).attempts.add(new ChargingAttemptTracker(event)); + } + + @Override + public void handleEvent(UpdateChargingAttemptEvent event) { + ChargingAttemptTracker attempt = active.get(event.getVehicleId()).attempts.getLast(); + attempt.update = event; + } + + @Override + public void handleEvent(AbortChargingAttemptEvent event) { + ChargingAttemptTracker attempt = active.get(event.getVehicleId()).attempts.getLast(); + attempt.abort = event; + } + + @Override + public void handleEvent(FinishChargingAttemptEvent event) { + ChargingAttemptTracker attempt = active.get(event.getVehicleId()).attempts.getLast(); + attempt.finish = event; + } + + private class ChargingProcessTracker { + ChargingProcessTracker(StartChargingProcessEvent start) { + this.start = start; + } + + final StartChargingProcessEvent start; + AbortChargingProcessEvent abort; + FinishChargingProcessEvent finish; + + LinkedList attempts = new LinkedList<>(); + } + + private class ChargingAttemptTracker { + ChargingAttemptTracker(StartChargingAttemptEvent start) { + this.start = start; + } + + final StartChargingAttemptEvent start; + UpdateChargingAttemptEvent update; + AbortChargingAttemptEvent abort; + FinishChargingAttemptEvent finish; + + QueuedAtChargerEvent queued; + QuitQueueAtChargerEvent quit; + + ChargingStartEvent plug; + ChargingEndEvent unplug; + + EnergyChargedEvent lastEnergyEvent; + } + + @Override + public void handleEvent(QueuedAtChargerEvent event) { + var item = active.get(event.getVehicleId()); + + if (item != null) { + item.attempts.getLast().queued = event; + } + } + + @Override + public void handleEvent(QuitQueueAtChargerEvent event) { + var item = active.get(event.getVehicleId()); + + if (item != null) { + item.attempts.getLast().quit = event; + } + } + + @Override + public void handleEvent(ChargingStartEvent event) { + var item = active.get(event.getVehicleId()); + + if (item != null) { + item.attempts.getLast().plug = event; + } + } + + @Override + public void handleEvent(EnergyChargedEvent event) { + var item = active.get(event.getVehicleId()); + + if (item != null) { + item.attempts.getLast().lastEnergyEvent = event; + } + } + + @Override + public void handleEvent(ChargingEndEvent event) { + var item = active.get(event.getVehicleId()); + + if (item != null) { + item.attempts.getLast().unplug = event; + } + } + + static public record ChargingProcessItem( // + Id personId, Id vehicleId, // + int processIndex, int attempts, boolean successful, // + double startTime, double endTime) { + } + + static public record ChargingAttemptItem( // + Id personId, Id vehicleId, // + int processIndex, int attemptIndex, boolean successful, // + double startTime, double updateTime, double endTime, // + double queueingStartTime, double queueingEndTime, boolean queued, // + double chargingStartTime, double chargingEndTime, boolean charged, // + Id chargerId, Id initialChargerId, boolean enroute, // + boolean spontaneous, double energy_kWh) { + } + + private void registerProcess(ChargingProcessTracker tracker) { + Preconditions.checkState(!finished); + + double endTime = Double.NaN; + if (tracker.finish != null) { + endTime = tracker.finish.getTime(); + } else if (tracker.abort != null) { + endTime = tracker.abort.getTime(); + } // otherwise was still ongoing + + ChargingProcessItem chargingItem = new ChargingProcessItem(tracker.start.getPersonId(), + tracker.start.getVehicleId(), tracker.start.getProcessIndex(), tracker.attempts.size(), + tracker.attempts.getLast().start != null, tracker.start.getTime(), endTime); + + synchronized (chargingItems) { + chargingItems.add(chargingItem); + } + + for (ChargingAttemptTracker attemptTracker : tracker.attempts) { + double attemptStartTime = chargingItem.startTime; + if (attemptTracker.start != null) { + attemptStartTime = attemptTracker.start.getTime(); + } + + boolean isEnroute = attemptTracker.start.isEnroute(); + boolean isSponaneous = attemptTracker.start.isSpontaneous(); + + double attemptUpdateTime = Double.NaN; + if (attemptTracker.update != null) { + attemptUpdateTime = attemptTracker.update.getTime(); + } + + double attemptEndTime = Double.NaN; + if (attemptTracker.finish != null) { + attemptEndTime = attemptTracker.finish.getTime(); + } else if (attemptTracker.abort != null) { + attemptEndTime = attemptTracker.abort.getTime(); + } + + double queueingStartTime = Double.NaN; + if (attemptTracker.queued != null) { + queueingStartTime = attemptTracker.queued.getTime(); + } + + double queueingEndTime = Double.NaN; + if (attemptTracker.quit != null) { + queueingEndTime = attemptTracker.quit.getTime(); + } else if (attemptTracker.queued != null && attemptTracker.plug != null) { + queueingEndTime = attemptTracker.plug.getTime(); + } + + double chargingStartTime = Double.NaN; + if (attemptTracker.plug != null) { + chargingStartTime = attemptTracker.plug.getTime(); + } + + double chargingEndTime = Double.NaN; + if (attemptTracker.unplug != null && attemptTracker.lastEnergyEvent != null) { + // this is not when we unplug, but the last time energy is added + chargingEndTime = attemptTracker.lastEnergyEvent.getTime(); + } + + Id chargerId = attemptTracker.start.getChargerId(); + Id initialChargerId = attemptTracker.start.getChargerId(); + if (attemptTracker.update != null) { + chargerId = attemptTracker.update.getChargerId(); + } + + double energy_kWh = 0.0; + if (attemptTracker.plug != null && attemptTracker.lastEnergyEvent != null) { + energy_kWh = EvUnits + .J_to_kWh(attemptTracker.lastEnergyEvent.getEndCharge() - attemptTracker.plug.getCharge()); + } + + ChargingAttemptItem attemptItem = new ChargingAttemptItem(tracker.start.getPersonId(), + tracker.start.getVehicleId(), tracker.start.getProcessIndex(), + attemptTracker.start.getAttemptIndex(), attemptTracker.plug != null, + attemptStartTime, attemptUpdateTime, attemptEndTime, queueingStartTime, queueingEndTime, + attemptTracker.queued != null, chargingStartTime, chargingEndTime, + attemptTracker.plug != null, chargerId, initialChargerId, isEnroute, isSponaneous, energy_kWh); + + synchronized (chargingAttemptItems) { + chargingAttemptItems.add(attemptItem); + } + } + } + + public void processRemainingEvents() { + for (var process : active.values()) { + registerProcess(process); + } + + finished = true; + } + + public List getChargingProcessItems() { + if (!finished) { + processRemainingEvents(); + } + + return chargingItems; + } + + public List getChargingAttemptItems() { + if (!finished) { + processRemainingEvents(); + } + + return chargingAttemptItems; + } +} diff --git a/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/analysis/WithinDayChargingAnalysisListener.java b/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/analysis/WithinDayChargingAnalysisListener.java new file mode 100644 index 00000000000..ae91618d9d6 --- /dev/null +++ b/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/analysis/WithinDayChargingAnalysisListener.java @@ -0,0 +1,139 @@ +package org.matsim.contrib.ev.withinday.analysis; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.IOException; +import java.util.Arrays; + +import org.matsim.api.core.v01.IdSet; +import org.matsim.api.core.v01.population.Person; +import org.matsim.core.controler.OutputDirectoryHierarchy; +import org.matsim.core.controler.events.IterationEndsEvent; +import org.matsim.core.controler.listener.IterationEndsListener; +import org.matsim.core.utils.io.IOUtils; + +/** + * Tracks detailed and aggregated information on the electric vehilce charging + * processes and attempts. + * + * @author Sebastian Hörl (sebhoerl), IRT SystemX + */ +public class WithinDayChargingAnalysisListener implements IterationEndsListener { + static public final String PROCESSES_FILE = "wevc_charging_processes.csv"; + static public final String ATTEMPTS_FILE = "wevc_charging_attempts.csv"; + static public final String AGGREGATED_FILE = "wevc_analysis.csv"; + + private final OutputDirectoryHierarchy outputHierarchy; + private final WithinDayChargingAnalysisHandler handler; + + public WithinDayChargingAnalysisListener(WithinDayChargingAnalysisHandler handler, + OutputDirectoryHierarchy outputHierarchy) { + this.outputHierarchy = outputHierarchy; + this.handler = handler; + } + + @Override + public void notifyIterationEnds(IterationEndsEvent event) { + try { + String chargingPath = outputHierarchy.getIterationFilename(event.getIteration(), PROCESSES_FILE); + BufferedWriter chargingWriter = IOUtils.getBufferedWriter(chargingPath); + + chargingWriter.write(String.join(";", Arrays.asList( // + "person_id", "vehicle_id", "process_index", "attempts", "successful", "start_time", "end_time")) + + "\n"); + + for (var item : handler.getChargingProcessItems()) { + chargingWriter.write(String.join(";", Arrays.asList( // + item.personId().toString(), item.vehicleId().toString(), String.valueOf(item.processIndex()), + String.valueOf(item.attempts()), String.valueOf(item.successful()), + String.valueOf(item.startTime()), String.valueOf(item.endTime()))) + "\n"); + } + + chargingWriter.close(); + + String attemptsPath = outputHierarchy.getIterationFilename(event.getIteration(), ATTEMPTS_FILE); + BufferedWriter attemptsWriter = IOUtils.getBufferedWriter(attemptsPath); + + attemptsWriter.write(String.join(";", Arrays.asList( // + "person_id", "vehicle_id", "process_index", "attempt_index", "successful", "start_time", + "update_time", "end_time", + "queue_start_time", "queue_end_time", "queued", "charging_start_time", "charging_end_time", + "charged", "charger_id", "initial_charger_id", "enroute", "spontaneous", "energy_kWh")) + "\n"); + + for (var item : handler.getChargingAttemptItems()) { + attemptsWriter.write(String.join(";", Arrays.asList( // + item.personId().toString(), item.vehicleId().toString(), String.valueOf(item.processIndex()), + String.valueOf(item.attemptIndex()), String.valueOf(item.successful()), + String.valueOf(item.startTime()), String.valueOf(item.updateTime()), + String.valueOf(item.endTime()), + String.valueOf(item.queueingStartTime()), String.valueOf(item.queueingEndTime()), + String.valueOf(item.queued()), String.valueOf(item.chargingStartTime()), + String.valueOf(item.chargingEndTime()), String.valueOf(item.charged()), + item.chargerId().toString(), item.initialChargerId().toString(), + String.valueOf(item.enroute()), + String.valueOf(item.spontaneous()), + String.valueOf(item.energy_kWh()))) + "\n"); + } + + attemptsWriter.close(); + + writeAggregatedStatistics(event.getIteration()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private void writeAggregatedStatistics(int iteration) throws IOException { + int chargingPersonsCount = 0; + int failedPersonsCount = 0; + + int chargingItemsCount = 0; + int failedItemsCount = 0; + + int chargingAttempsCount = 0; + int failedAttemptsCount = 0; + + IdSet personIds = new IdSet<>(Person.class); + IdSet failedIds = new IdSet<>(Person.class); + + for (var item : handler.getChargingProcessItems()) { + chargingItemsCount++; + personIds.add(item.personId()); + + if (!item.successful()) { + failedItemsCount++; + failedIds.add(item.personId()); + } + } + + chargingPersonsCount = personIds.size(); + failedPersonsCount = failedIds.size(); + + for (var item : handler.getChargingAttemptItems()) { + chargingAttempsCount++; + + if (!item.successful()) { + failedAttemptsCount++; + } + } + + File outputFile = new File(outputHierarchy.getOutputFilename(AGGREGATED_FILE)); + boolean writeHeader = !outputFile.exists(); + + BufferedWriter writer = IOUtils.getAppendingBufferedWriter(outputFile.toString()); + + if (writeHeader) { + writer.write(String.join(";", Arrays.asList( // + "iteration", "persons", "failed_persons", "processes", "failed_processes", "attempts", + "failed_attempts")) + + "\n"); + } + + writer.write(String.join(";", Arrays.asList( // + String.valueOf(iteration), String.valueOf(chargingPersonsCount), String.valueOf(failedPersonsCount), + String.valueOf(chargingItemsCount), String.valueOf(failedItemsCount), + String.valueOf(chargingAttempsCount), String.valueOf(failedAttemptsCount))) + "\n"); + + writer.close(); + } +} diff --git a/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/events/AbortChargingAttemptEvent.java b/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/events/AbortChargingAttemptEvent.java new file mode 100644 index 00000000000..13dfcc5e424 --- /dev/null +++ b/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/events/AbortChargingAttemptEvent.java @@ -0,0 +1,43 @@ +package org.matsim.contrib.ev.withinday.events; + +import org.matsim.api.core.v01.Id; +import org.matsim.api.core.v01.events.Event; +import org.matsim.api.core.v01.events.HasPersonId; +import org.matsim.api.core.v01.events.HasVehicleId; +import org.matsim.api.core.v01.population.Person; +import org.matsim.vehicles.Vehicle; + +/** + * Generated when a charging attempt is aborted, for instance, because the agent + * has waited for too long in the queue. + * + * @author Sebastian Hörl (sebhoerl), IRT SystemX + */ +public class AbortChargingAttemptEvent extends Event implements HasPersonId, HasVehicleId { + static public final String EVENT_TYPE = "abort charging attempt"; + + private final Id personId; + private final Id vehicleId; + + public AbortChargingAttemptEvent(double time, Id personId, Id vehicleId) { + super(time); + + this.personId = personId; + this.vehicleId = vehicleId; + } + + @Override + public String getEventType() { + return EVENT_TYPE; + } + + @Override + public Id getPersonId() { + return personId; + } + + @Override + public Id getVehicleId() { + return vehicleId; + } +} diff --git a/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/events/AbortChargingAttemptEventHandler.java b/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/events/AbortChargingAttemptEventHandler.java new file mode 100644 index 00000000000..0f7a568310e --- /dev/null +++ b/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/events/AbortChargingAttemptEventHandler.java @@ -0,0 +1,7 @@ +package org.matsim.contrib.ev.withinday.events; + +import org.matsim.core.events.handler.EventHandler; + +public interface AbortChargingAttemptEventHandler extends EventHandler { + void handleEvent(AbortChargingAttemptEvent event); +} diff --git a/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/events/AbortChargingProcessEvent.java b/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/events/AbortChargingProcessEvent.java new file mode 100644 index 00000000000..78955c223f9 --- /dev/null +++ b/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/events/AbortChargingProcessEvent.java @@ -0,0 +1,43 @@ +package org.matsim.contrib.ev.withinday.events; + +import org.matsim.api.core.v01.Id; +import org.matsim.api.core.v01.events.Event; +import org.matsim.api.core.v01.events.HasPersonId; +import org.matsim.api.core.v01.events.HasVehicleId; +import org.matsim.api.core.v01.population.Person; +import org.matsim.vehicles.Vehicle; + +/** + * Generated when a charging process is aborted, for instance, because the agent + * has already tried to many chargers that were all occupied. + * + * @author Sebastian Hörl (sebhoerl), IRT SystemX + */ +public class AbortChargingProcessEvent extends Event implements HasPersonId, HasVehicleId { + static public final String EVENT_TYPE = "abort charging process"; + + private final Id personId; + private final Id vehicleId; + + public AbortChargingProcessEvent(double time, Id personId, Id vehicleId) { + super(time); + + this.personId = personId; + this.vehicleId = vehicleId; + } + + @Override + public String getEventType() { + return EVENT_TYPE; + } + + @Override + public Id getPersonId() { + return personId; + } + + @Override + public Id getVehicleId() { + return vehicleId; + } +} diff --git a/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/events/AbortChargingProcessEventHandler.java b/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/events/AbortChargingProcessEventHandler.java new file mode 100644 index 00000000000..8362145b3d3 --- /dev/null +++ b/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/events/AbortChargingProcessEventHandler.java @@ -0,0 +1,7 @@ +package org.matsim.contrib.ev.withinday.events; + +import org.matsim.core.events.handler.EventHandler; + +public interface AbortChargingProcessEventHandler extends EventHandler { + void handleEvent(AbortChargingProcessEvent event); +} diff --git a/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/events/FinishChargingAttemptEvent.java b/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/events/FinishChargingAttemptEvent.java new file mode 100644 index 00000000000..c6ae65aee3b --- /dev/null +++ b/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/events/FinishChargingAttemptEvent.java @@ -0,0 +1,43 @@ +package org.matsim.contrib.ev.withinday.events; + +import org.matsim.api.core.v01.Id; +import org.matsim.api.core.v01.events.Event; +import org.matsim.api.core.v01.events.HasPersonId; +import org.matsim.api.core.v01.events.HasVehicleId; +import org.matsim.api.core.v01.population.Person; +import org.matsim.vehicles.Vehicle; + +/** + * Generated when a charging attempt is finished succesfully, i.e., the agent + * managed to plug the vehicle and let it charge. + * + * @author Sebastian Hörl (sebhoerl), IRT SystemX + */ +public class FinishChargingAttemptEvent extends Event implements HasPersonId, HasVehicleId { + static public final String EVENT_TYPE = "finish charging attempt"; + + private final Id personId; + private final Id vehicleId; + + public FinishChargingAttemptEvent(double time, Id personId, Id vehicleId) { + super(time); + + this.personId = personId; + this.vehicleId = vehicleId; + } + + @Override + public String getEventType() { + return EVENT_TYPE; + } + + @Override + public Id getPersonId() { + return personId; + } + + @Override + public Id getVehicleId() { + return vehicleId; + } +} diff --git a/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/events/FinishChargingAttemptEventHandler.java b/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/events/FinishChargingAttemptEventHandler.java new file mode 100644 index 00000000000..10492957e5c --- /dev/null +++ b/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/events/FinishChargingAttemptEventHandler.java @@ -0,0 +1,7 @@ +package org.matsim.contrib.ev.withinday.events; + +import org.matsim.core.events.handler.EventHandler; + +public interface FinishChargingAttemptEventHandler extends EventHandler { + void handleEvent(FinishChargingAttemptEvent event); +} diff --git a/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/events/FinishChargingProcessEvent.java b/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/events/FinishChargingProcessEvent.java new file mode 100644 index 00000000000..76b245b8119 --- /dev/null +++ b/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/events/FinishChargingProcessEvent.java @@ -0,0 +1,43 @@ +package org.matsim.contrib.ev.withinday.events; + +import org.matsim.api.core.v01.Id; +import org.matsim.api.core.v01.events.Event; +import org.matsim.api.core.v01.events.HasPersonId; +import org.matsim.api.core.v01.events.HasVehicleId; +import org.matsim.api.core.v01.population.Person; +import org.matsim.vehicles.Vehicle; + +/** + * Generated when a charging process is finished succesfully, i.e., the agent + * managed to plug the vehicle and let it charge. + * + * @author Sebastian Hörl (sebhoerl), IRT SystemX + */ +public class FinishChargingProcessEvent extends Event implements HasPersonId, HasVehicleId { + static public final String EVENT_TYPE = "finish charging process"; + + private final Id personId; + private final Id vehicleId; + + public FinishChargingProcessEvent(double time, Id personId, Id vehicleId) { + super(time); + + this.personId = personId; + this.vehicleId = vehicleId; + } + + @Override + public String getEventType() { + return EVENT_TYPE; + } + + @Override + public Id getPersonId() { + return personId; + } + + @Override + public Id getVehicleId() { + return vehicleId; + } +} diff --git a/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/events/FinishChargingProcessEventHandler.java b/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/events/FinishChargingProcessEventHandler.java new file mode 100644 index 00000000000..49bc40197f3 --- /dev/null +++ b/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/events/FinishChargingProcessEventHandler.java @@ -0,0 +1,7 @@ +package org.matsim.contrib.ev.withinday.events; + +import org.matsim.core.events.handler.EventHandler; + +public interface FinishChargingProcessEventHandler extends EventHandler { + void handleEvent(FinishChargingProcessEvent event); +} diff --git a/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/events/StartChargingAttemptEvent.java b/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/events/StartChargingAttemptEvent.java new file mode 100644 index 00000000000..4436d44b189 --- /dev/null +++ b/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/events/StartChargingAttemptEvent.java @@ -0,0 +1,102 @@ +package org.matsim.contrib.ev.withinday.events; + +import java.util.Map; + +import org.matsim.api.core.v01.Id; +import org.matsim.api.core.v01.events.Event; +import org.matsim.api.core.v01.events.HasPersonId; +import org.matsim.api.core.v01.events.HasVehicleId; +import org.matsim.api.core.v01.population.Person; +import org.matsim.contrib.ev.infrastructure.Charger; +import org.matsim.vehicles.Vehicle; + +/** + * Generated when an agent starts a charging attempt, i.e., he has arrived at + * the charger and now starts to queue / plug the vehicle. + * + * @author Sebastian Hörl (sebhoerl), IRT SystemX + */ +public class StartChargingAttemptEvent extends Event implements HasPersonId, HasVehicleId { + static public final String EVENT_TYPE = "start charging attempt"; + + private final Id personId; + private final Id vehicleId; + private final Id chargerId; + + private final boolean isEnroute; + private final boolean isSpontaneous; + + private final int attemptIndex; + private final int processIndex; + + private final double duration; + + public StartChargingAttemptEvent(double time, Id personId, Id vehicleId, Id chargerId, + int attemptIndex, int processIndex, boolean isEnroute, boolean isSpontaneous, double duration) { + super(time); + + this.personId = personId; + this.vehicleId = vehicleId; + this.chargerId = chargerId; + this.isEnroute = isEnroute; + this.isSpontaneous = isSpontaneous; + this.attemptIndex = attemptIndex; + this.processIndex = processIndex; + this.duration = duration; + } + + @Override + public String getEventType() { + return EVENT_TYPE; + } + + @Override + public Id getPersonId() { + return personId; + } + + @Override + public Id getVehicleId() { + return vehicleId; + } + + public Id getChargerId() { + return chargerId; + } + + public boolean isEnroute() { + return isEnroute; + } + + public boolean isSpontaneous() { + return isSpontaneous; + } + + public int getAttemptIndex() { + return attemptIndex; + } + + public int getProcessIndex() { + return processIndex; + } + + public double getDuration() { + return duration; + } + + @Override + public Map getAttributes() { + Map attributes = super.getAttributes(); + attributes.put("charger", chargerId.toString()); + attributes.put("enroute", String.valueOf(isEnroute)); + attributes.put("spontaneous", String.valueOf(isSpontaneous)); + attributes.put("attemptIndex", String.valueOf(attemptIndex)); + attributes.put("processIndex", String.valueOf(processIndex)); + + if (duration > 0.0) { + attributes.put("duration", String.valueOf(duration)); + } + + return attributes; + } +} diff --git a/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/events/StartChargingAttemptEventHandler.java b/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/events/StartChargingAttemptEventHandler.java new file mode 100644 index 00000000000..e4be9acb3cd --- /dev/null +++ b/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/events/StartChargingAttemptEventHandler.java @@ -0,0 +1,7 @@ +package org.matsim.contrib.ev.withinday.events; + +import org.matsim.core.events.handler.EventHandler; + +public interface StartChargingAttemptEventHandler extends EventHandler { + void handleEvent(StartChargingAttemptEvent event); +} diff --git a/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/events/StartChargingProcessEvent.java b/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/events/StartChargingProcessEvent.java new file mode 100644 index 00000000000..34c5ac5db21 --- /dev/null +++ b/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/events/StartChargingProcessEvent.java @@ -0,0 +1,58 @@ +package org.matsim.contrib.ev.withinday.events; + +import java.util.Map; + +import org.matsim.api.core.v01.Id; +import org.matsim.api.core.v01.events.Event; +import org.matsim.api.core.v01.events.HasPersonId; +import org.matsim.api.core.v01.events.HasVehicleId; +import org.matsim.api.core.v01.population.Person; +import org.matsim.vehicles.Vehicle; + +/** + * Generated when an agent starts a charging process, i.e., he makes the + * decision at which charger to perform a charging attempt. + * + * @author Sebastian Hörl (sebhoerl), IRT SystemX + */ +public class StartChargingProcessEvent extends Event implements HasPersonId, HasVehicleId { + static public final String EVENT_TYPE = "start charging process"; + + private final Id personId; + private final Id vehicleId; + private final int processIndex; + + public StartChargingProcessEvent(double time, Id personId, Id vehicleId, int processIndex) { + super(time); + + this.personId = personId; + this.vehicleId = vehicleId; + this.processIndex = processIndex; + } + + @Override + public String getEventType() { + return EVENT_TYPE; + } + + @Override + public Id getPersonId() { + return personId; + } + + @Override + public Id getVehicleId() { + return vehicleId; + } + + public int getProcessIndex() { + return processIndex; + } + + @Override + public Map getAttributes() { + Map attributes = super.getAttributes(); + attributes.put("processIndex", String.valueOf(processIndex)); + return attributes; + } +} diff --git a/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/events/StartChargingProcessEventHandler.java b/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/events/StartChargingProcessEventHandler.java new file mode 100644 index 00000000000..3667d50e3c8 --- /dev/null +++ b/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/events/StartChargingProcessEventHandler.java @@ -0,0 +1,7 @@ +package org.matsim.contrib.ev.withinday.events; + +import org.matsim.core.events.handler.EventHandler; + +public interface StartChargingProcessEventHandler extends EventHandler { + void handleEvent(StartChargingProcessEvent event); +} diff --git a/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/events/UpdateChargingAttemptEvent.java b/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/events/UpdateChargingAttemptEvent.java new file mode 100644 index 00000000000..7a1e2afc9ee --- /dev/null +++ b/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/events/UpdateChargingAttemptEvent.java @@ -0,0 +1,81 @@ +package org.matsim.contrib.ev.withinday.events; + +import java.util.Map; + +import org.matsim.api.core.v01.Id; +import org.matsim.api.core.v01.events.Event; +import org.matsim.api.core.v01.events.HasPersonId; +import org.matsim.api.core.v01.events.HasVehicleId; +import org.matsim.api.core.v01.population.Person; +import org.matsim.contrib.ev.infrastructure.Charger; +import org.matsim.vehicles.Vehicle; + +/** + * Generated when an agent updates a charging attempt at the beginning of the + * charging process. This means that even before starting the first attempt, the + * selected charger or other characteristics are updated on-the-fly compared to + * the initially planned configuration. + * + * @author Sebastian Hörl (sebhoerl), IRT SystemX + */ +public class UpdateChargingAttemptEvent extends Event implements HasPersonId, HasVehicleId { + static public final String EVENT_TYPE = "update charging attempt"; + + private final Id personId; + private final Id vehicleId; + private final Id chargerId; + + private boolean isEnroute; + private double duration; + + public UpdateChargingAttemptEvent(double time, Id personId, Id vehicleId, Id chargerId, + boolean isEnroute, double duration) { + super(time); + + this.personId = personId; + this.vehicleId = vehicleId; + this.chargerId = chargerId; + this.isEnroute = isEnroute; + this.duration = duration; + } + + @Override + public String getEventType() { + return EVENT_TYPE; + } + + @Override + public Id getPersonId() { + return personId; + } + + @Override + public Id getVehicleId() { + return vehicleId; + } + + public Id getChargerId() { + return chargerId; + } + + public boolean isEnroute() { + return isEnroute; + } + + public double getDuration() { + return duration; + } + + @Override + public Map getAttributes() { + Map attributes = super.getAttributes(); + attributes.put("charger", chargerId.toString()); + attributes.put("enroute", String.valueOf(isEnroute)); + + if (duration > 0.0) { + attributes.put("duration", String.valueOf(duration)); + } + + return attributes; + } +} diff --git a/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/events/UpdateChargingAttemptEventHandler.java b/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/events/UpdateChargingAttemptEventHandler.java new file mode 100644 index 00000000000..5f5a75d6880 --- /dev/null +++ b/contribs/ev/src/main/java/org/matsim/contrib/ev/withinday/events/UpdateChargingAttemptEventHandler.java @@ -0,0 +1,7 @@ +package org.matsim.contrib.ev.withinday.events; + +import org.matsim.core.events.handler.EventHandler; + +public interface UpdateChargingAttemptEventHandler extends EventHandler { + void handleEvent(UpdateChargingAttemptEvent event); +} diff --git a/contribs/ev/src/test/java/org/matsim/contrib/ev/strategic/ChargingPlansConverterTest.java b/contribs/ev/src/test/java/org/matsim/contrib/ev/strategic/ChargingPlansConverterTest.java new file mode 100644 index 00000000000..543bfd1be89 --- /dev/null +++ b/contribs/ev/src/test/java/org/matsim/contrib/ev/strategic/ChargingPlansConverterTest.java @@ -0,0 +1,34 @@ +package org.matsim.contrib.ev.strategic; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; +import org.matsim.api.core.v01.Id; +import org.matsim.contrib.ev.infrastructure.Charger; +import org.matsim.contrib.ev.strategic.plan.ChargingPlan; +import org.matsim.contrib.ev.strategic.plan.ChargingPlanActivity; +import org.matsim.contrib.ev.strategic.plan.ChargingPlans; +import org.matsim.contrib.ev.strategic.plan.ChargingPlansConverter; + +public class ChargingPlansConverterTest { + @Test + public void testCharingPlansConverter() { + ChargingPlans chargingPlans = new ChargingPlans(); + + ChargingPlan chargingPlan = new ChargingPlan(); + chargingPlans.addChargingPlan(chargingPlan); + + ChargingPlanActivity activity = new ChargingPlanActivity(5, 6, Id.create("charger", Charger.class)); + chargingPlan.addChargingActivity(activity); + + ChargingPlansConverter converter = new ChargingPlansConverter(); + String representation = converter.convertToString(chargingPlans); + + ChargingPlans restored = converter.convert(representation); + assertEquals(5, restored.getChargingPlans().get(0).getChargingActivities().get(0).getStartActivityIndex()); + assertEquals(6, restored.getChargingPlans().get(0).getChargingActivities().get(0).getEndActivityIndex()); + assertEquals("charger", + restored.getChargingPlans().get(0).getChargingActivities().get(0).getChargerId().toString()); + + } +} diff --git a/contribs/ev/src/test/java/org/matsim/contrib/ev/strategic/StrategicChargingTest.java b/contribs/ev/src/test/java/org/matsim/contrib/ev/strategic/StrategicChargingTest.java new file mode 100644 index 00000000000..b6b0af8da60 --- /dev/null +++ b/contribs/ev/src/test/java/org/matsim/contrib/ev/strategic/StrategicChargingTest.java @@ -0,0 +1,162 @@ +package org.matsim.contrib.ev.strategic; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.matsim.api.core.v01.Id; +import org.matsim.api.core.v01.Scenario; +import org.matsim.contrib.ev.strategic.plan.ChargingPlans; +import org.matsim.contrib.ev.strategic.plan.ChargingPlansConverter; +import org.matsim.contrib.ev.strategic.utils.TestScenarioBuilder; +import org.matsim.contrib.ev.strategic.utils.TestScenarioBuilder.TestScenario; +import org.matsim.contrib.ev.withinday.WithinDayEvConfigGroup; +import org.matsim.core.config.Config; +import org.matsim.core.config.ConfigUtils; +import org.matsim.core.controler.Controler; +import org.matsim.core.population.io.PopulationReader; +import org.matsim.core.scenario.ScenarioUtils; +import org.matsim.testcases.MatsimTestUtils; + +public class StrategicChargingTest { + @RegisterExtension + public MatsimTestUtils utils = new MatsimTestUtils(); + + @Test + public void testChargingAtActivity() { + TestScenario scenario = new TestScenarioBuilder(utils) // + .enableStrategicCharging(1) // + .addWorkCharger(8, 8, 1, 1.0, "default") // + .setElectricVehicleRange(10000.0) // + .addPerson("person", 0.5) // SoC goes to zero after leaving from work + .addActivity("home", 0, 0, 10.0 * 3600.0) // + .addActivity("work", 8, 8, 18.0 * 3600.0) // + .addActivity("home", 0, 0) // + .build(); + + StrategicChargingConfigGroup config = StrategicChargingConfigGroup.get(scenario.config()); + config.scoreTrackingInterval = 1; + config.scoring.zeroSoc = -1000.0; // incentivize agent to charge at work + + // motivate agent to charge at activity + config.minimumEnrouteDriveTime = Double.POSITIVE_INFINITY; + + Controler controller = scenario.controller(); + controller.run(); + + assertEquals(1, scenario.tracker().startChargingProcessEvents.size()); + assertEquals(1, scenario.tracker().finishChargingProcessEvents.size()); + assertEquals(0, scenario.tracker().abortCharingProcessEvents.size()); + + assertEquals(1, scenario.tracker().startChargingAttemptEvents.size()); + assertEquals(0, scenario.tracker().updateChargingAttemptEvents.size()); + assertEquals(1, scenario.tracker().finishChargingAttemptEvents.size()); + assertEquals(0, scenario.tracker().abortCharingAttemptEvents.size()); + + assertFalse(scenario.tracker().startChargingAttemptEvents.getFirst().isEnroute()); + + assertEquals(1, scenario.tracker().chargingStartEvents.size()); + assertEquals(1, scenario.tracker().chargingEndEvents.size()); + + // test output of charging plans + Config outputConfig = ConfigUtils.createConfig(); + Scenario ouptutScenario = ScenarioUtils.createScenario(outputConfig); + + PopulationReader reader = new PopulationReader(ouptutScenario); + reader.putAttributeConverter(ChargingPlans.class, new ChargingPlansConverter()); + reader.readFile(utils.getOutputDirectory() + "/output_plans.xml.gz"); + + Object value = ouptutScenario.getPopulation().getPersons().values().iterator().next().getSelectedPlan() + .getAttributes().getAttribute(ChargingPlans.ATTRIBUTE); + + assertTrue(value != null); + assertTrue(value instanceof ChargingPlans); + } + + @Test + public void testChargingEnroute() { + TestScenario scenario = new TestScenarioBuilder(utils) // + .enableStrategicCharging(3) // + .addPublicCharger("charger", 6, 6, 1, 1.0, "default") // + .setElectricVehicleRange(10000.0) // + .addPerson("person", 0.5) // SoC goes to zero after leaving from work + .addActivity("home", 0, 0, 10.0 * 3600.0) // + .addActivity("work", 8, 8, 18.0 * 3600.0) // + .addActivity("home", 0, 0) // + .build(); + + StrategicChargingConfigGroup config = StrategicChargingConfigGroup.get(scenario.config()); + config.scoring.zeroSoc = -1000.0; // incentivize agent to charge + + // motivate agent to charge enroute + config.maximumActivityChargingDuration = 0.0; + config.minimumEnrouteDriveTime = 0; + + // define charging duration + config.minimumEnrouteChargingDuration = 1800.0; + config.maximumEnrouteChargingDuration = 1800.0; + + Controler controller = scenario.controller(); + controller.run(); + + assertEquals(1, scenario.tracker().startChargingProcessEvents.size()); + assertEquals(1, scenario.tracker().finishChargingProcessEvents.size()); + assertEquals(0, scenario.tracker().abortCharingProcessEvents.size()); + + assertEquals(1, scenario.tracker().startChargingAttemptEvents.size()); + assertEquals(0, scenario.tracker().updateChargingAttemptEvents.size()); + assertEquals(1, scenario.tracker().finishChargingAttemptEvents.size()); + assertEquals(0, scenario.tracker().abortCharingAttemptEvents.size()); + + assertTrue(scenario.tracker().startChargingAttemptEvents.getFirst().isEnroute()); + + assertEquals(1, scenario.tracker().chargingStartEvents.size()); + assertEquals(1, scenario.tracker().chargingEndEvents.size()); + } + + @Test + public void testCriticalCharging() { + TestScenario scenario = new TestScenarioBuilder(utils) // + .enableStrategicCharging(0) // + .addPublicCharger("charger", 2, 2, 1, 1.0, "default") // + .setElectricVehicleRange(10000.0) // + .addPerson("person", 0.5) // SoC goes to zero after leaving from work + .addActivity("home", 0, 0, 10.0 * 3600.0) // + .addActivity("work", 8, 8, 18.0 * 3600.0) // + .addActivity("home", 0, 0) // + .build(); + + WithinDayEvConfigGroup wdConfig = WithinDayEvConfigGroup.get(scenario.config()); + wdConfig.allowSpoantaneousCharging = true; + + StrategicChargingConfigGroup config = StrategicChargingConfigGroup.get(scenario.config()); + config.scoring.zeroSoc = -1000.0; // incentivize agent to charge + + // disallow enroute and activity charging + config.minimumEnrouteDriveTime = Double.POSITIVE_INFINITY; + config.minimumActivityChargingDuration = Double.POSITIVE_INFINITY; + + // set critical soc + scenario.scenario().getPopulation().getPersons().get(Id.createPersonId("person")).getAttributes() + .putAttribute(CriticalAlternativeProvider.CRITICAL_SOC_PERSON_ATTRIBUTE, 0.1); + + Controler controller = scenario.controller(); + controller.run(); + + assertEquals(1, scenario.tracker().startChargingProcessEvents.size()); + assertEquals(1, scenario.tracker().finishChargingProcessEvents.size()); + assertEquals(0, scenario.tracker().abortCharingProcessEvents.size()); + + assertEquals(1, scenario.tracker().startChargingAttemptEvents.size()); + assertEquals(0, scenario.tracker().updateChargingAttemptEvents.size()); + assertEquals(1, scenario.tracker().finishChargingAttemptEvents.size()); + assertEquals(0, scenario.tracker().abortCharingAttemptEvents.size()); + + assertTrue(scenario.tracker().startChargingAttemptEvents.getFirst().isEnroute()); + + assertEquals(1, scenario.tracker().chargingStartEvents.size()); + assertEquals(1, scenario.tracker().chargingEndEvents.size()); + } +} \ No newline at end of file diff --git a/contribs/ev/src/test/java/org/matsim/contrib/ev/strategic/utils/TestScenarioBuilder.java b/contribs/ev/src/test/java/org/matsim/contrib/ev/strategic/utils/TestScenarioBuilder.java new file mode 100644 index 00000000000..3ea7cb2b930 --- /dev/null +++ b/contribs/ev/src/test/java/org/matsim/contrib/ev/strategic/utils/TestScenarioBuilder.java @@ -0,0 +1,699 @@ +package org.matsim.contrib.ev.strategic.utils; + +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +import org.matsim.api.core.v01.Coord; +import org.matsim.api.core.v01.Id; +import org.matsim.api.core.v01.IdMap; +import org.matsim.api.core.v01.Scenario; +import org.matsim.api.core.v01.events.ActivityEndEvent; +import org.matsim.api.core.v01.events.ActivityStartEvent; +import org.matsim.api.core.v01.events.PersonDepartureEvent; +import org.matsim.api.core.v01.events.PersonStuckEvent; +import org.matsim.api.core.v01.events.handler.ActivityEndEventHandler; +import org.matsim.api.core.v01.events.handler.ActivityStartEventHandler; +import org.matsim.api.core.v01.events.handler.PersonDepartureEventHandler; +import org.matsim.api.core.v01.events.handler.PersonStuckEventHandler; +import org.matsim.api.core.v01.network.Link; +import org.matsim.api.core.v01.network.Network; +import org.matsim.api.core.v01.network.NetworkFactory; +import org.matsim.api.core.v01.network.Node; +import org.matsim.api.core.v01.population.Activity; +import org.matsim.api.core.v01.population.Leg; +import org.matsim.api.core.v01.population.Person; +import org.matsim.api.core.v01.population.Plan; +import org.matsim.api.core.v01.population.Population; +import org.matsim.api.core.v01.population.PopulationFactory; +import org.matsim.contrib.ev.EvConfigGroup; +import org.matsim.contrib.ev.EvModule; +import org.matsim.contrib.ev.EvUnits; +import org.matsim.contrib.ev.charging.ChargingEndEvent; +import org.matsim.contrib.ev.charging.ChargingEndEventHandler; +import org.matsim.contrib.ev.charging.ChargingStartEvent; +import org.matsim.contrib.ev.charging.ChargingStartEventHandler; +import org.matsim.contrib.ev.charging.QueuedAtChargerEvent; +import org.matsim.contrib.ev.charging.QueuedAtChargerEventHandler; +import org.matsim.contrib.ev.charging.QuitQueueAtChargerEvent; +import org.matsim.contrib.ev.charging.QuitQueueAtChargerEventHandler; +import org.matsim.contrib.ev.discharging.AuxEnergyConsumption; +import org.matsim.contrib.ev.discharging.DriveEnergyConsumption; +import org.matsim.contrib.ev.fleet.ElectricFleetUtils; +import org.matsim.contrib.ev.infrastructure.Charger; +import org.matsim.contrib.ev.infrastructure.ChargerSpecification; +import org.matsim.contrib.ev.infrastructure.ChargingInfrastructureSpecification; +import org.matsim.contrib.ev.infrastructure.ChargingInfrastructureSpecificationDefaultImpl; +import org.matsim.contrib.ev.infrastructure.ImmutableChargerSpecification; +import org.matsim.contrib.ev.strategic.StrategicChargingConfigGroup; +import org.matsim.contrib.ev.strategic.StrategicChargingConfigGroup.SelectionStrategy; +import org.matsim.contrib.ev.strategic.StrategicChargingModule; +import org.matsim.contrib.ev.strategic.infrastructure.FacilityChargerProvider; +import org.matsim.contrib.ev.strategic.infrastructure.PersonChargerProvider; +import org.matsim.contrib.ev.strategic.infrastructure.PublicChargerProvider; +import org.matsim.contrib.ev.strategic.replanning.StrategicChargingReplanningStrategy; +import org.matsim.contrib.ev.strategic.scoring.ChargingPlanScoringParameters; +import org.matsim.contrib.ev.withinday.WithinDayEvConfigGroup; +import org.matsim.contrib.ev.withinday.WithinDayEvEngine; +import org.matsim.contrib.ev.withinday.WithinDayEvModule; +import org.matsim.contrib.ev.withinday.events.AbortChargingAttemptEvent; +import org.matsim.contrib.ev.withinday.events.AbortChargingAttemptEventHandler; +import org.matsim.contrib.ev.withinday.events.AbortChargingProcessEvent; +import org.matsim.contrib.ev.withinday.events.AbortChargingProcessEventHandler; +import org.matsim.contrib.ev.withinday.events.FinishChargingAttemptEvent; +import org.matsim.contrib.ev.withinday.events.FinishChargingAttemptEventHandler; +import org.matsim.contrib.ev.withinday.events.FinishChargingProcessEvent; +import org.matsim.contrib.ev.withinday.events.FinishChargingProcessEventHandler; +import org.matsim.contrib.ev.withinday.events.StartChargingAttemptEvent; +import org.matsim.contrib.ev.withinday.events.StartChargingAttemptEventHandler; +import org.matsim.contrib.ev.withinday.events.StartChargingProcessEvent; +import org.matsim.contrib.ev.withinday.events.StartChargingProcessEventHandler; +import org.matsim.contrib.ev.withinday.events.UpdateChargingAttemptEvent; +import org.matsim.contrib.ev.withinday.events.UpdateChargingAttemptEventHandler; +import org.matsim.core.config.Config; +import org.matsim.core.config.ConfigUtils; +import org.matsim.core.config.groups.QSimConfigGroup.EndtimeInterpretation; +import org.matsim.core.config.groups.QSimConfigGroup.StarttimeInterpretation; +import org.matsim.core.config.groups.QSimConfigGroup.VehicleBehavior; +import org.matsim.core.config.groups.QSimConfigGroup.VehiclesSource; +import org.matsim.core.config.groups.ReplanningConfigGroup.StrategySettings; +import org.matsim.core.config.groups.RoutingConfigGroup.AccessEgressType; +import org.matsim.core.config.groups.ScoringConfigGroup.ActivityParams; +import org.matsim.core.controler.AbstractModule; +import org.matsim.core.controler.Controler; +import org.matsim.core.controler.OutputDirectoryHierarchy.OverwriteFileSetting; +import org.matsim.core.router.TripStructureUtils; +import org.matsim.core.scenario.ScenarioUtils; +import org.matsim.facilities.ActivityFacilities; +import org.matsim.facilities.ActivityFacilitiesFactory; +import org.matsim.facilities.ActivityFacility; +import org.matsim.testcases.MatsimTestUtils; +import org.matsim.vehicles.Vehicle; +import org.matsim.vehicles.VehicleType; +import org.matsim.vehicles.VehicleUtils; +import org.matsim.vehicles.Vehicles; +import org.matsim.vehicles.VehiclesFactory; + +import com.google.common.base.Preconditions; +import com.google.common.base.Verify; + +public class TestScenarioBuilder { + private final MatsimTestUtils utils; + + public TestScenarioBuilder(MatsimTestUtils utils) { + this.utils = utils; + } + + // NETWORK PART + + private Integer networkSize = 10; // nodes + private Double linkLength = 200.0; // meters + private Double linkSpeed = 1.0; // meters per second + + public TestScenarioBuilder setNetworkSize(int networkSize) { + this.networkSize = networkSize; + return this; + } + + public TestScenarioBuilder setLinkLength(double linkLength) { + this.linkLength = linkLength; + return this; + } + + private void prepareNetwork(Scenario scenario) { + Network network = scenario.getNetwork(); + NetworkFactory networkFactory = network.getFactory(); + + Node[][] nodes = new Node[networkSize][networkSize]; + for (int i = 0; i < networkSize; i++) { + for (int j = 0; j < networkSize; j++) { + nodes[i][j] = networkFactory.createNode(Id.createNodeId(i + ":" + j), + new Coord(i * linkLength, j * linkLength)); + network.addNode(nodes[i][j]); + } + } + + List links = new LinkedList<>(); + + for (int i = 0; i < networkSize; i++) { + for (int j = 0; j < networkSize - 1; j++) { + Node firstNode = nodes[i][j]; + Node secondNode = nodes[i][j + 1]; + + links.add(networkFactory.createLink( + Id.createLinkId(firstNode.getId().toString() + "::" + secondNode.getId().toString()), firstNode, + secondNode)); + + links.add(networkFactory.createLink( + Id.createLinkId(secondNode.getId().toString() + "::" + firstNode.getId().toString()), + secondNode, firstNode)); + } + } + + for (int j = 0; j < networkSize; j++) { + for (int i = 0; i < networkSize - 1; i++) { + Node firstNode = nodes[i][j]; + Node secondNode = nodes[i + 1][j]; + + links.add(networkFactory.createLink( + Id.createLinkId(firstNode.getId().toString() + "::" + secondNode.getId().toString()), firstNode, + secondNode)); + + links.add(networkFactory.createLink( + Id.createLinkId(secondNode.getId().toString() + "::" + firstNode.getId().toString()), + secondNode, firstNode)); + } + } + + for (Link link : links) { + link.setCapacity(1e9); + link.setNumberOfLanes(1); + link.setAllowedModes(Collections.singleton("car")); + link.setLength(linkLength); + link.setFreespeed(linkSpeed); + network.addLink(link); + } + + ActivityFacilities facilities = scenario.getActivityFacilities(); + ActivityFacilitiesFactory facilitiesFactory = facilities.getFactory(); + + for (Link link : links) { + ActivityFacility facility = facilitiesFactory.createActivityFacility( + Id.create(link.getId(), ActivityFacility.class), link.getCoord(), link.getId()); + facilities.addActivityFacility(facility); + } + } + + // INFRASTRUCTURE PART + + record ChargerItem(String identifier, int x, int y, int plugCount, double plugPower, Set> personIds, + Set> facilityIds, boolean isPublic) { + } + + private List chargers = new LinkedList<>(); + + public TestScenarioBuilder addCharger(String identifier, int x, int y, int plugCount, double plugPower) { + chargers.add(new ChargerItem(identifier, x, y, plugCount, plugPower, Collections.emptySet(), + Collections.emptySet(), false)); + return this; + } + + public TestScenarioBuilder addCharger(String identifier, int x, int y, int plugCount, double plugPower, + Set> personIds, + Set> facilityIds, boolean isPublic) { + chargers.add(new ChargerItem(identifier, x, y, plugCount, plugPower, personIds, facilityIds, isPublic)); + return this; + } + + public TestScenarioBuilder addHomeCharger(String personId, int x, int y, int plugCount, double plugPower, + String type) { + String chargerId = "charger:person:" + personId; + return addCharger(chargerId, x, y, plugCount, plugPower, Collections.singleton(Id.createPersonId(personId)), + Collections.emptySet(), false); + } + + public TestScenarioBuilder addWorkCharger(int x, int y, int plugCount, double plugPower, + String type) { + Id linkId = Id.createLinkId(x + ":" + y + "::" + (x + 1) + ":" + y); + String chargerId = "charger:facility:" + linkId.toString(); + return addCharger(chargerId, x, y, plugCount, plugPower, Collections.emptySet(), + Collections.singleton(Id.create(linkId.toString(), ActivityFacility.class)), false); + } + + public TestScenarioBuilder addPublicCharger(String chargerId, int x, int y, int plugCount, double plugPower, + String type) { + String publicChargerId = "charger:public" + chargerId; + return addCharger(publicChargerId, x, y, plugCount, plugPower, Collections.emptySet(), + Collections.emptySet(), true); + } + + private void prepareInfrastructure(Controler controller) { + ChargingInfrastructureSpecification infrastructure = new ChargingInfrastructureSpecificationDefaultImpl(); + + for (ChargerItem item : chargers) { + Verify.verify(item.x >= 0 && item.x < networkSize - 1, "Invalid depot for vehicle " + item.identifier); + Verify.verify(item.y >= 0 && item.y < networkSize - 1, "Invalid depot for vehicle " + item.identifier); + + Id linkId = Id.createLinkId(item.x + ":" + item.y + "::" + (item.x + 1) + ":" + item.y); + + ChargerSpecification specification = ImmutableChargerSpecification.newBuilder() // + .id(Id.create(item.identifier, Charger.class)) // , + .linkId(linkId) // + .chargerType("default") // + .plugPower(item.plugPower) // + .plugCount(item.plugCount) // + .build(); + + if (item.personIds.size() > 0) { + PersonChargerProvider.setPersonIds(specification, item.personIds); + } + + if (item.facilityIds.size() > 0) { + FacilityChargerProvider.setFacilityIds(specification, item.facilityIds); + } + + if (item.isPublic) { + PublicChargerProvider.setPublic(specification, true); + } + + infrastructure.addChargerSpecification(specification); + } + + controller.addOverridingModule(new AbstractModule() { + @Override + public void install() { + bind(ChargingInfrastructureSpecification.class).toInstance(infrastructure); + } + }); + } + + // DEMAND PART + + record ActivityItem(String type, int x, int y, double endTime, String previousMode) { + + } + + record PersonItem(String identifer, List activities, double initialSoc) { + } + + private LinkedList persons = new LinkedList<>(); + + public TestScenarioBuilder addPerson(String identifier, double initialSoc) { + PersonItem item = new PersonItem(identifier, new LinkedList<>(), initialSoc); + persons.add(item); + return this; + } + + public TestScenarioBuilder addActivity(String type, int x, int y, double endTime) { + Preconditions.checkState(persons.size() > 0); + persons.getLast().activities.add(new ActivityItem(type, x, y, endTime, "car")); + return this; + } + + public TestScenarioBuilder addActivity(String type, int x, int y) { + return addActivity(type, x, y, Double.NaN); + } + + public TestScenarioBuilder addActivity(String type, int x, int y, double endTime, String previousMode) { + Preconditions.checkState(persons.size() > 0); + persons.getLast().activities.add(new ActivityItem(type, x, y, endTime, previousMode)); + return this; + } + + public TestScenarioBuilder addActivity(String type, int x, int y, String previousMode) { + return addActivity(type, x, y, Double.NaN, previousMode); + } + + private void preparePopulation(Scenario scenario) { + Population population = scenario.getPopulation(); + PopulationFactory populationFactory = population.getFactory(); + + Vehicles vehicles = scenario.getVehicles(); + VehiclesFactory vehiclesFactory = vehicles.getFactory(); + + VehicleType electricVehicleType = vehicles.getVehicleTypes().get(Id.create("electric", VehicleType.class)); + + for (var item : persons) { + Person person = populationFactory.createPerson(Id.createPersonId(item.identifer)); + WithinDayEvEngine.activate(person); + population.addPerson(person); + + Plan plan = populationFactory.createPlan(); + person.addPlan(plan); + + List activities = new LinkedList<>(); + + for (var activityItem : item.activities) { + Verify.verify(activityItem.x >= 0 && activityItem.y < networkSize - 1, + "Invalid location for person " + item.identifer); + + Id linkId = Id.createLinkId( + activityItem.x + ":" + activityItem.y + "::" + (activityItem.x + 1) + ":" + activityItem.y); + Activity activity = populationFactory.createActivityFromLinkId(activityItem.type, linkId); + activity.setFacilityId(Id.create(linkId, ActivityFacility.class)); + + if (!Double.isNaN(activityItem.endTime)) { + activity.setEndTime(activityItem.endTime); + } + + activities.add(activity); + } + + for (int i = 0; i < activities.size(); i++) { + if (i > 0) { + String mode = item.activities.get(i).previousMode; + Leg leg = populationFactory.createLeg(mode); + leg.setRoutingMode(mode); + plan.addLeg(leg); + } + + plan.addActivity(activities.get(i)); + } + + Vehicle vehicle = vehiclesFactory.createVehicle(Id.createVehicleId(item.identifer), electricVehicleType); + vehicles.addVehicle(vehicle); + + ElectricFleetUtils.setInitialSoc(vehicle, item.initialSoc); + + VehicleUtils.insertVehicleIdsIntoPersonAttributes(person, Collections.singletonMap("car", vehicle.getId())); + } + } + + private double range_m = 10 * 1e3; + + public TestScenarioBuilder setElectricVehicleRange(double range_m) { + this.range_m = range_m; + return this; + } + + private void prepareVehicles(Scenario scenario) { + Vehicles vehicles = scenario.getVehicles(); + VehiclesFactory vehiclesFactory = vehicles.getFactory(); + + VehicleType electricVehicleType = vehiclesFactory.createVehicleType(Id.create("electric", VehicleType.class)); + vehicles.addVehicleType(electricVehicleType); + + VehicleUtils.setEnergyCapacity(electricVehicleType.getEngineInformation(), EvUnits.J_to_kWh(range_m)); + + VehicleUtils.setHbefaTechnology(electricVehicleType.getEngineInformation(), + ElectricFleetUtils.EV_ENGINE_HBEFA_TECHNOLOGY); + } + + // GENERAL PART + + private double simulationStartTime = 0.0; + private double simulationEndTime = 24.0 * 3600.0; + + public TestScenarioBuilder setSimulationStartTime(double simulationStartTime) { + this.simulationStartTime = simulationStartTime; + return this; + } + + public TestScenarioBuilder setSimulationEndTime(double simulationEndTime) { + this.simulationEndTime = simulationEndTime; + return this; + } + + private boolean enableStrategicCharging = false; + private int iterations = 0; + + public TestScenarioBuilder enableStrategicCharging(int iterations) { + this.enableStrategicCharging = true; + this.iterations = iterations; + return this; + } + + private Config prepareConfig() { + Config config = ConfigUtils.createConfig(); + + config.controller().setOutputDirectory(utils.getOutputDirectory()); + config.controller().setOverwriteFileSetting(OverwriteFileSetting.deleteDirectoryIfExists); + + ActivityParams genericActivity = new ActivityParams("generic"); + genericActivity.setScoringThisActivityAtAll(false); + config.scoring().addActivityParams(genericActivity); + + config.controller().setLastIteration(iterations); + + config.qsim().setStartTime(simulationStartTime); + config.qsim().setSimStarttimeInterpretation(StarttimeInterpretation.onlyUseStarttime); + + config.qsim().setEndTime(simulationEndTime); + config.qsim().setSimEndtimeInterpretation(EndtimeInterpretation.onlyUseEndtime); + + config.qsim().setFlowCapFactor(1e9); + config.qsim().setStorageCapFactor(1e9); + config.qsim().setVehicleBehavior(VehicleBehavior.exception); + + config.routing().setAccessEgressType(AccessEgressType.accessEgressModeToLink); + + ActivityParams homeParams = new ActivityParams("home"); + homeParams.setScoringThisActivityAtAll(false); + config.scoring().addActivityParams(homeParams); + + ActivityParams workParams = new ActivityParams("work"); + workParams.setScoringThisActivityAtAll(false); + config.scoring().addActivityParams(workParams); + + ActivityParams plugParams = new ActivityParams(WithinDayEvEngine.PLUG_ACTIVITY_TYPE); + plugParams.setScoringThisActivityAtAll(false); + config.scoring().addActivityParams(plugParams); + + ActivityParams unplugParams = new ActivityParams(WithinDayEvEngine.UNPLUG_ACTIVITY_TYPE); + unplugParams.setScoringThisActivityAtAll(false); + config.scoring().addActivityParams(unplugParams); + + ActivityParams accessParams = new ActivityParams(WithinDayEvEngine.ACCESS_ACTIVITY_TYPE); + accessParams.setScoringThisActivityAtAll(false); + config.scoring().addActivityParams(accessParams); + + ActivityParams waitParams = new ActivityParams(WithinDayEvEngine.WAIT_ACTIVITY_TYPE); + waitParams.setScoringThisActivityAtAll(false); + config.scoring().addActivityParams(waitParams); + + config.qsim().setVehiclesSource(VehiclesSource.fromVehiclesData); + + EvConfigGroup evConfigGroup = new EvConfigGroup(); + config.addModule(evConfigGroup); + evConfigGroup.chargersFile = "none"; + + WithinDayEvConfigGroup withinDayConfig = new WithinDayEvConfigGroup(); + config.addModule(withinDayConfig); + withinDayConfig.carMode = "car"; + + if (enableStrategicCharging) { + StrategicChargingConfigGroup strategicConfig = new StrategicChargingConfigGroup(); + strategicConfig.selectionStrategy = SelectionStrategy.Best; // to have a strong effect + config.addModule(strategicConfig); + + ChargingPlanScoringParameters scoringParameters = new ChargingPlanScoringParameters(); + strategicConfig.addParameterSet(scoringParameters); + + // only strategic charging + StrategySettings chargingStrategy = new StrategySettings(); + chargingStrategy.setStrategyName(StrategicChargingReplanningStrategy.STRATEGY); + chargingStrategy.setWeight(1.0); + config.replanning().addStrategySettings(chargingStrategy); + } + + return config; + } + + private Scenario prepareScenario(Config config) { + Scenario scenario = ScenarioUtils.createScenario(config); + prepareNetwork(scenario); + prepareVehicles(scenario); + preparePopulation(scenario); + return scenario; + } + + private Controler prepareController(Scenario scenario) { + Controler controller = new Controler(scenario); + + controller.addOverridingModule(new EvModule()); + controller.addOverridingModule(new AbstractModule() { + @Override + public void install() { + bind(DriveEnergyConsumption.Factory.class).toInstance(vehicle -> { + return (link, travelTime, linkEnterTime) -> link.getLength(); + }); + + bind(AuxEnergyConsumption.Factory.class).toInstance(vehicle -> { + return (beginTime, duration, linkId) -> 0.0; + }); + } + }); + + prepareInfrastructure(controller); + + controller.addOverridingModule(new WithinDayEvModule()); + + if (enableStrategicCharging) { + controller.addOverridingModule(new StrategicChargingModule()); + } + + return controller; + } + + public TestScenario build() { + Config config = prepareConfig(); + Scenario scenario = prepareScenario(config); + Controler controller = prepareController(scenario); + + Tracker tracker = prepareTracker(controller); + + return new TestScenario(config, scenario, controller, tracker); + } + + public record TestScenario(Config config, Scenario scenario, Controler controller, Tracker tracker) { + } + + private Tracker prepareTracker(Controler controller) { + Tracker tracker = new Tracker(); + + controller.addOverridingModule(new AbstractModule() { + @Override + public void install() { + addEventHandlerBinding().toInstance(tracker); + } + }); + + return tracker; + } + + public class Tracker + implements ActivityStartEventHandler, ActivityEndEventHandler, // + StartChargingProcessEventHandler, AbortChargingProcessEventHandler, + FinishChargingProcessEventHandler, // + StartChargingAttemptEventHandler, UpdateChargingAttemptEventHandler, AbortChargingAttemptEventHandler, + FinishChargingAttemptEventHandler, // + ChargingStartEventHandler, + ChargingEndEventHandler, QueuedAtChargerEventHandler, QuitQueueAtChargerEventHandler, + PersonStuckEventHandler, PersonDepartureEventHandler { + public final LinkedList activityStartEvents = new LinkedList<>(); + public final LinkedList plugActivityEvents = new LinkedList<>(); + public final LinkedList unplugActivityEvents = new LinkedList<>(); + public final LinkedList unplugActivityEndEvents = new LinkedList<>(); + + public final LinkedList startChargingProcessEvents = new LinkedList<>(); + public final LinkedList abortCharingProcessEvents = new LinkedList<>(); + public final LinkedList finishChargingProcessEvents = new LinkedList<>(); + + public final LinkedList startChargingAttemptEvents = new LinkedList<>(); + public final LinkedList updateChargingAttemptEvents = new LinkedList<>(); + public final LinkedList abortCharingAttemptEvents = new LinkedList<>(); + public final LinkedList finishChargingAttemptEvents = new LinkedList<>(); + + public final LinkedList chargingStartEvents = new LinkedList<>(); + public final LinkedList chargingEndEvents = new LinkedList<>(); + public final LinkedList queuedAtChargerEvents = new LinkedList<>(); + public final LinkedList quitQueueAtChargerEvents = new LinkedList<>(); + public final LinkedList personStuckEvents = new LinkedList<>(); + + public final IdMap> sequences = new IdMap<>(Person.class); + + @Override + synchronized public void handleEvent(ActivityStartEvent event) { + if (!TripStructureUtils.isStageActivityType(event.getActType()) || WithinDayEvEngine.isManagedActivityType(event.getActType())) { + activityStartEvents.add(event); + + if (event.getActType().equals(WithinDayEvEngine.PLUG_ACTIVITY_TYPE)) { + plugActivityEvents.add(event); + } else if (event.getActType().equals(WithinDayEvEngine.UNPLUG_ACTIVITY_TYPE)) { + unplugActivityEvents.add(event); + } + } + + synchronized (sequences) { + if (!sequences.containsKey(event.getPersonId())) { + sequences.put(event.getPersonId(), new LinkedList<>()); + } + + sequences.get(event.getPersonId()).add("activity:" + event.getActType()); + } + } + + @Override + synchronized public void handleEvent(ActivityEndEvent event) { + if (event.getActType().equals(WithinDayEvEngine.UNPLUG_ACTIVITY_TYPE)) { + unplugActivityEndEvents.add(event); + } + } + + @Override + synchronized public void handleEvent(QuitQueueAtChargerEvent event) { + quitQueueAtChargerEvents.add(event); + } + + @Override + synchronized public void handleEvent(QueuedAtChargerEvent event) { + queuedAtChargerEvents.add(event); + } + + @Override + synchronized public void handleEvent(ChargingEndEvent event) { + chargingEndEvents.add(event); + } + + @Override + synchronized public void handleEvent(ChargingStartEvent event) { + chargingStartEvents.add(event); + } + + @Override + synchronized public void handleEvent(StartChargingProcessEvent event) { + startChargingProcessEvents.add(event); + } + + @Override + synchronized public void handleEvent(AbortChargingProcessEvent event) { + abortCharingProcessEvents.add(event); + } + + @Override + synchronized public void handleEvent(FinishChargingProcessEvent event) { + finishChargingProcessEvents.add(event); + } + + @Override + synchronized public void handleEvent(StartChargingAttemptEvent event) { + startChargingAttemptEvents.add(event); + } + + @Override + synchronized public void handleEvent(UpdateChargingAttemptEvent event) { + updateChargingAttemptEvents.add(event); + } + + @Override + synchronized public void handleEvent(AbortChargingAttemptEvent event) { + abortCharingAttemptEvents.add(event); + } + + @Override + synchronized public void handleEvent(FinishChargingAttemptEvent event) { + finishChargingAttemptEvents.add(event); + } + + @Override + synchronized public void handleEvent(PersonStuckEvent event) { + personStuckEvents.add(event); + } + + @Override + public void handleEvent(PersonDepartureEvent event) { + synchronized (sequences) { + if (!sequences.containsKey(event.getPersonId())) { + sequences.put(event.getPersonId(), new LinkedList<>()); + } + + sequences.get(event.getPersonId()).add("leg:" + event.getLegMode()); + } + } + + @Override + public void reset(int iteration) { + activityStartEvents.clear(); + plugActivityEvents.clear(); + unplugActivityEvents.clear(); + + startChargingProcessEvents.clear(); + abortCharingProcessEvents.clear(); + finishChargingProcessEvents.clear(); + + startChargingAttemptEvents.clear(); + updateChargingAttemptEvents.clear(); + abortCharingAttemptEvents.clear(); + finishChargingAttemptEvents.clear(); + + chargingStartEvents.clear(); + chargingEndEvents.clear(); + queuedAtChargerEvents.clear(); + quitQueueAtChargerEvents.clear(); + personStuckEvents.clear(); + + sequences.clear(); + } + } +} diff --git a/contribs/ev/src/test/java/org/matsim/contrib/ev/withinday/WithinDayEvTest.java b/contribs/ev/src/test/java/org/matsim/contrib/ev/withinday/WithinDayEvTest.java new file mode 100644 index 00000000000..c697744956f --- /dev/null +++ b/contribs/ev/src/test/java/org/matsim/contrib/ev/withinday/WithinDayEvTest.java @@ -0,0 +1,1649 @@ +package org.matsim.contrib.ev.withinday; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import org.assertj.core.util.Arrays; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.matsim.api.core.v01.Id; +import org.matsim.contrib.ev.reservation.ChargerReservationModule; +import org.matsim.contrib.ev.strategic.utils.TestScenarioBuilder; +import org.matsim.contrib.ev.strategic.utils.TestScenarioBuilder.TestScenario; +import org.matsim.contrib.ev.withinday.utils.ActivityLegChangeProvider; +import org.matsim.contrib.ev.withinday.utils.ChangeDurationAlternativeProvider; +import org.matsim.contrib.ev.withinday.utils.FirstActivitySlotProvider; +import org.matsim.contrib.ev.withinday.utils.FirstLegSlotProvider; +import org.matsim.contrib.ev.withinday.utils.LastActivitySlotProvider; +import org.matsim.contrib.ev.withinday.utils.OrderedAlternativeProvider; +import org.matsim.contrib.ev.withinday.utils.ReservationAlternativeProvider; +import org.matsim.contrib.ev.withinday.utils.SpontaneousChargingProvider; +import org.matsim.contrib.ev.withinday.utils.SwitchChargerAlternativeProvider; +import org.matsim.contrib.ev.withinday.utils.WholeDaySlotProvider; +import org.matsim.contrib.ev.withinday.utils.WorkActivitySlotProvider; +import org.matsim.core.controler.Controler; +import org.matsim.core.mobsim.qsim.AbstractQSimModule; +import org.matsim.testcases.MatsimTestUtils; + +public class WithinDayEvTest { + @RegisterExtension + public MatsimTestUtils utils = new MatsimTestUtils(); + + @Test + public void testWithoutProviders() { + TestScenario scenario = new TestScenarioBuilder(utils) // + .addPerson("person", 1.0) // + .addActivity("home", 0, 0, 10.0 * 3600.0) // + .addActivity("work", 8, 8, 18.0 * 3600.0) // + .addActivity("home", 0, 0) // + .build(); + + Controler controller = scenario.controller(); + controller.run(); + + assertEquals("work", scenario.tracker().activityStartEvents.get(0).getActType()); + assertEquals(39217.0, scenario.tracker().activityStartEvents.get(0).getTime()); + assertEquals("home", scenario.tracker().activityStartEvents.get(1).getActType()); + assertEquals(68419.0, scenario.tracker().activityStartEvents.get(1).getTime()); + + assertEquals(Arrays.asList(Arrays.array( + // "activity:home", + "leg:walk", + "activity:car interaction", + "leg:car", + "activity:car interaction", + "leg:walk", + "activity:work", + "leg:walk", + "activity:car interaction", + "leg:car", + "activity:car interaction", + "leg:walk", + "activity:home")), scenario.tracker().sequences.get(Id.createPersonId("person"))); + } + + @Test + public void testChargeAtFirstTry() { + TestScenario scenario = new TestScenarioBuilder(utils) // + .addCharger("charger", 8, 8, 1, 1.0) // + .addPerson("person", 1.0) // + .addActivity("home", 0, 0, 10.0 * 3600.0) // + .addActivity("work", 8, 8, 18.0 * 3600.0) // + .addActivity("home", 0, 0) // + .build(); + + Controler controller = scenario.controller(); + + controller.addOverridingQSimModule(new AbstractQSimModule() { + @Override + protected void configureQSim() { + bind(ChargingSlotProvider.class).to(WorkActivitySlotProvider.class); + } + }); + + controller.run(); + + // check arrival at home + assertEquals("home", scenario.tracker().activityStartEvents.getLast().getActType()); + assertEquals(68576.0, scenario.tracker().activityStartEvents.getLast().getTime()); + + // check charging process + assertEquals(1, scenario.tracker().startChargingProcessEvents.size()); + assertEquals(1, scenario.tracker().finishChargingProcessEvents.size()); + assertEquals(0, scenario.tracker().abortCharingProcessEvents.size()); + + assertEquals(1, scenario.tracker().startChargingAttemptEvents.size()); + assertEquals(0, scenario.tracker().updateChargingAttemptEvents.size()); + assertEquals(1, scenario.tracker().finishChargingAttemptEvents.size()); + assertEquals(0, scenario.tracker().abortCharingAttemptEvents.size()); + + // check charger interaction + assertEquals(1, scenario.tracker().chargingStartEvents.size()); + assertEquals(1, scenario.tracker().chargingEndEvents.size()); + assertEquals(0, scenario.tracker().queuedAtChargerEvents.size()); + assertEquals(0, scenario.tracker().quitQueueAtChargerEvents.size()); + + assertEquals("charger", scenario.tracker().chargingStartEvents.getFirst().getChargerId().toString()); + + // check engine logic + assertEquals(1, scenario.tracker().plugActivityEvents.size()); + assertEquals(1, scenario.tracker().unplugActivityEvents.size()); + + assertEquals(39217.0, scenario.tracker().plugActivityEvents.getFirst().getTime()); + assertEquals(64956.0, scenario.tracker().unplugActivityEvents.getFirst().getTime()); + + assertEquals(Arrays.asList(Arrays.array( + // "activity:home", + "leg:walk", + "activity:car interaction", + "leg:car", + "activity:ev:plug interaction", + "leg:walk", + "activity:work", + "leg:walk", + "activity:ev:unplug interaction", + "leg:car", + "activity:car interaction", + "leg:walk", + "activity:home")), scenario.tracker().sequences.get(Id.createPersonId("person"))); + } + + @Test + public void testChargeAtFirstTryWithLongerDistance() { + TestScenario scenario = new TestScenarioBuilder(utils) // + .addCharger("charger", 5, 5, 1, 1.0) // charger is further away from work + .addPerson("person", 1.0) // + .addActivity("home", 0, 0, 10.0 * 3600.0) // + .addActivity("work", 8, 8, 18.0 * 3600.0) // + .addActivity("home", 0, 0) // + .build(); + + Controler controller = scenario.controller(); + + controller.addOverridingQSimModule(new AbstractQSimModule() { + @Override + protected void configureQSim() { + bind(ChargingSlotProvider.class).to(WorkActivitySlotProvider.class); + } + }); + + controller.run(); + + // check arrival at home + assertEquals("home", scenario.tracker().activityStartEvents.getLast().getActType()); + assertEquals(68432.0, scenario.tracker().activityStartEvents.getLast().getTime()); + + // check charging process + assertEquals(1, scenario.tracker().startChargingProcessEvents.size()); + assertEquals(1, scenario.tracker().finishChargingProcessEvents.size()); + assertEquals(0, scenario.tracker().abortCharingProcessEvents.size()); + + assertEquals(1, scenario.tracker().startChargingAttemptEvents.size()); + assertEquals(0, scenario.tracker().updateChargingAttemptEvents.size()); + assertEquals(1, scenario.tracker().finishChargingAttemptEvents.size()); + assertEquals(0, scenario.tracker().abortCharingAttemptEvents.size()); + + // check charger interaction + assertEquals(1, scenario.tracker().chargingStartEvents.size()); + assertEquals(1, scenario.tracker().chargingEndEvents.size()); + assertEquals(0, scenario.tracker().queuedAtChargerEvents.size()); + assertEquals(0, scenario.tracker().quitQueueAtChargerEvents.size()); + + assertEquals("charger", scenario.tracker().chargingStartEvents.getFirst().getChargerId().toString()); + + // check engine logic + assertEquals(1, scenario.tracker().plugActivityEvents.size()); + assertEquals(1, scenario.tracker().unplugActivityEvents.size()); + + assertEquals(38011.0, scenario.tracker().plugActivityEvents.getFirst().getTime()); + assertEquals(66018.0, scenario.tracker().unplugActivityEvents.getFirst().getTime()); + + assertEquals(Arrays.asList(Arrays.array( + // "activity:home", + "leg:walk", + "activity:car interaction", + "leg:car", + "activity:ev:plug interaction", + "leg:walk", + "activity:work", + "leg:walk", + "activity:ev:unplug interaction", + "leg:car", + "activity:car interaction", + "leg:walk", + "activity:home")), scenario.tracker().sequences.get(Id.createPersonId("person"))); + } + + @Test + public void testChargeAtFirstActivitySameLocation() { + TestScenario scenario = new TestScenarioBuilder(utils) // + .addCharger("charger", 0, 0, 1, 1.0) // located at home + .addPerson("person", 1.0) // + .addActivity("home", 0, 0, 10.0 * 3600.0) // + .addActivity("work", 8, 8, 18.0 * 3600.0) // + .addActivity("home", 0, 0) // + .build(); + + Controler controller = scenario.controller(); + + controller.addOverridingQSimModule(new AbstractQSimModule() { + @Override + protected void configureQSim() { + bind(ChargingSlotProvider.class).to(FirstActivitySlotProvider.class); + } + }); + + controller.run(); + + // check arrival at work + assertEquals("work", scenario.tracker().activityStartEvents.get(1).getActType()); + assertEquals(39374.0, scenario.tracker().activityStartEvents.get(1).getTime()); + + // check arrival at home + assertEquals("home", scenario.tracker().activityStartEvents.getLast().getActType()); + assertEquals(68419.0, scenario.tracker().activityStartEvents.getLast().getTime()); + + // check charging process + assertEquals(1, scenario.tracker().startChargingProcessEvents.size()); + assertEquals(1, scenario.tracker().finishChargingProcessEvents.size()); + assertEquals(0, scenario.tracker().abortCharingProcessEvents.size()); + + assertEquals(1, scenario.tracker().startChargingAttemptEvents.size()); + assertEquals(0, scenario.tracker().updateChargingAttemptEvents.size()); + assertEquals(1, scenario.tracker().finishChargingAttemptEvents.size()); + assertEquals(0, scenario.tracker().abortCharingAttemptEvents.size()); + + // check charger interaction + assertEquals(1, scenario.tracker().chargingStartEvents.size()); + assertEquals(1, scenario.tracker().chargingEndEvents.size()); + assertEquals(0, scenario.tracker().queuedAtChargerEvents.size()); + assertEquals(0, scenario.tracker().quitQueueAtChargerEvents.size()); + + assertEquals("charger", scenario.tracker().chargingStartEvents.getFirst().getChargerId().toString()); + + // check engine logic + assertEquals(0, scenario.tracker().plugActivityEvents.size()); + assertEquals(1, scenario.tracker().unplugActivityEvents.size()); + + assertEquals(36156.0, scenario.tracker().unplugActivityEvents.getFirst().getTime()); + + assertEquals(Arrays.asList(Arrays.array( + // "activity:home", + "leg:walk", + "activity:ev:unplug interaction", + "leg:car", + "activity:car interaction", + "leg:walk", + "activity:work", + "leg:walk", + "activity:car interaction", + "leg:car", + "activity:car interaction", + "leg:walk", + "activity:home")), scenario.tracker().sequences.get(Id.createPersonId("person"))); + } + + @Test + public void testChargeAtFirstActivityOtherLocation() { + TestScenario scenario = new TestScenarioBuilder(utils) // + .addCharger("charger", 1, 1, 1, 1.0) // located at home + .addPerson("person", 1.0) // + .addActivity("home", 0, 0, 10.0 * 3600.0) // + .addActivity("work", 8, 8, 18.0 * 3600.0) // + .addActivity("home", 0, 0) // + .build(); + + Controler controller = scenario.controller(); + + controller.addOverridingQSimModule(new AbstractQSimModule() { + @Override + protected void configureQSim() { + bind(ChargingSlotProvider.class).to(FirstActivitySlotProvider.class); + } + }); + + controller.run(); + + // check arrival at home + assertEquals("home", scenario.tracker().activityStartEvents.getLast().getActType()); + assertEquals(68419.0, scenario.tracker().activityStartEvents.getLast().getTime()); + + // check charging process + assertEquals(1, scenario.tracker().startChargingProcessEvents.size()); + assertEquals(1, scenario.tracker().finishChargingProcessEvents.size()); + assertEquals(0, scenario.tracker().abortCharingProcessEvents.size()); + + assertEquals(1, scenario.tracker().startChargingAttemptEvents.size()); + assertEquals(0, scenario.tracker().updateChargingAttemptEvents.size()); + assertEquals(1, scenario.tracker().finishChargingAttemptEvents.size()); + assertEquals(0, scenario.tracker().abortCharingAttemptEvents.size()); + + // check charger interaction + assertEquals(1, scenario.tracker().chargingStartEvents.size()); + assertEquals(1, scenario.tracker().chargingEndEvents.size()); + assertEquals(0, scenario.tracker().queuedAtChargerEvents.size()); + assertEquals(0, scenario.tracker().quitQueueAtChargerEvents.size()); + + assertEquals("charger", scenario.tracker().chargingStartEvents.getFirst().getChargerId().toString()); + + // check engine logic + assertEquals(0, scenario.tracker().plugActivityEvents.size()); + assertEquals(1, scenario.tracker().unplugActivityEvents.size()); + + assertEquals(36562.0, scenario.tracker().unplugActivityEvents.getFirst().getTime()); + + assertEquals(Arrays.asList(Arrays.array( + // "activity:home", + "leg:walk", + "activity:ev:unplug interaction", + "leg:car", + "activity:car interaction", + "leg:walk", + "activity:work", + "leg:walk", + "activity:car interaction", + "leg:car", + "activity:car interaction", + "leg:walk", + "activity:home")), scenario.tracker().sequences.get(Id.createPersonId("person"))); + } + + @Test + public void testFailAtFirstActivitySameLocation() { + TestScenario scenario = new TestScenarioBuilder(utils) // + .addCharger("charger", 0, 0, 0, 1.0) // located at home + .addPerson("person", 1.0) // + .addActivity("home", 0, 0, 10.0 * 3600.0) // + .addActivity("work", 8, 8, 18.0 * 3600.0) // + .addActivity("home", 0, 0) // + .build(); + + Controler controller = scenario.controller(); + + controller.addOverridingQSimModule(new AbstractQSimModule() { + @Override + protected void configureQSim() { + bind(ChargingSlotProvider.class).to(FirstActivitySlotProvider.class); + } + }); + + controller.run(); + + // check arrival at work + assertEquals("work", scenario.tracker().activityStartEvents.get(1).getActType()); + assertEquals(39373.0, scenario.tracker().activityStartEvents.get(1).getTime()); + + // check arrival at home + assertEquals("home", scenario.tracker().activityStartEvents.getLast().getActType()); + assertEquals(68419.0, scenario.tracker().activityStartEvents.getLast().getTime()); + + // check charging process + assertEquals(1, scenario.tracker().startChargingProcessEvents.size()); + assertEquals(0, scenario.tracker().finishChargingProcessEvents.size()); + assertEquals(1, scenario.tracker().abortCharingProcessEvents.size()); + + assertEquals(1, scenario.tracker().startChargingAttemptEvents.size()); + assertEquals(0, scenario.tracker().updateChargingAttemptEvents.size()); + assertEquals(0, scenario.tracker().finishChargingAttemptEvents.size()); + assertEquals(1, scenario.tracker().abortCharingAttemptEvents.size()); + + // check charger interaction + assertEquals(0, scenario.tracker().chargingStartEvents.size()); + assertEquals(0, scenario.tracker().chargingEndEvents.size()); + assertEquals(1, scenario.tracker().queuedAtChargerEvents.size()); + assertEquals(1, scenario.tracker().quitQueueAtChargerEvents.size()); + + // check engine logic + assertEquals(0, scenario.tracker().plugActivityEvents.size()); + assertEquals(0, scenario.tracker().unplugActivityEvents.size()); + + assertEquals(Arrays.asList(Arrays.array( + // "activity:home", + "leg:walk", + "activity:ev:access interaction", + "leg:car", + "activity:car interaction", + "leg:walk", + "activity:work", + "leg:walk", + "activity:car interaction", + "leg:car", + "activity:car interaction", + "leg:walk", + "activity:home")), scenario.tracker().sequences.get(Id.createPersonId("person"))); + } + + @Test + public void testFailAtFirstActivityOtherLocation() { + TestScenario scenario = new TestScenarioBuilder(utils) // + .addCharger("charger", 1, 1, 0, 1.0) // located at home + .addPerson("person", 1.0) // + .addActivity("home", 0, 0, 10.0 * 3600.0) // + .addActivity("work", 8, 8, 18.0 * 3600.0) // + .addActivity("home", 0, 0) // + .build(); + + Controler controller = scenario.controller(); + + controller.addOverridingQSimModule(new AbstractQSimModule() { + @Override + protected void configureQSim() { + bind(ChargingSlotProvider.class).to(FirstActivitySlotProvider.class); + } + }); + + controller.run(); + + // check arrival at work + assertEquals("work", scenario.tracker().activityStartEvents.get(1).getActType()); + assertEquals(39377.0, scenario.tracker().activityStartEvents.get(1).getTime()); + + // check arrival at home + assertEquals("home", scenario.tracker().activityStartEvents.getLast().getActType()); + assertEquals(68419.0, scenario.tracker().activityStartEvents.getLast().getTime()); + + // check charging process + assertEquals(1, scenario.tracker().startChargingProcessEvents.size()); + assertEquals(0, scenario.tracker().finishChargingProcessEvents.size()); + assertEquals(1, scenario.tracker().abortCharingProcessEvents.size()); + + assertEquals(1, scenario.tracker().startChargingAttemptEvents.size()); + assertEquals(0, scenario.tracker().updateChargingAttemptEvents.size()); + assertEquals(0, scenario.tracker().finishChargingAttemptEvents.size()); + assertEquals(1, scenario.tracker().abortCharingAttemptEvents.size()); + + // check charger interaction + assertEquals(0, scenario.tracker().chargingStartEvents.size()); + assertEquals(0, scenario.tracker().chargingEndEvents.size()); + assertEquals(1, scenario.tracker().queuedAtChargerEvents.size()); + assertEquals(1, scenario.tracker().quitQueueAtChargerEvents.size()); + + // check engine logic + assertEquals(0, scenario.tracker().plugActivityEvents.size()); + assertEquals(0, scenario.tracker().unplugActivityEvents.size()); + + assertEquals(Arrays.asList(Arrays.array( + // "activity:home", + "leg:walk", + "activity:ev:access interaction", + "leg:car", + "activity:car interaction", + "leg:walk", + "activity:work", + "leg:walk", + "activity:car interaction", + "leg:car", + "activity:car interaction", + "leg:walk", + "activity:home")), scenario.tracker().sequences.get(Id.createPersonId("person"))); + } + + @Test + public void testChargeAtLastActivity() { + TestScenario scenario = new TestScenarioBuilder(utils) // + .addCharger("charger", 0, 0, 1, 1.0) // at home + .addPerson("person", 1.0) // + .addActivity("home", 0, 0, 10.0 * 3600.0) // + .addActivity("work", 8, 8, 18.0 * 3600.0) // + .addActivity("home", 0, 0) // + .build(); + + Controler controller = scenario.controller(); + + controller.addOverridingQSimModule(new AbstractQSimModule() { + @Override + protected void configureQSim() { + bind(ChargingSlotProvider.class).to(LastActivitySlotProvider.class); + } + }); + + controller.run(); + + // check arrival at home + assertEquals("home", scenario.tracker().activityStartEvents.getLast().getActType()); + assertEquals(68588.0, scenario.tracker().activityStartEvents.getLast().getTime()); + + // check charging process + assertEquals(1, scenario.tracker().startChargingProcessEvents.size()); + assertEquals(0, scenario.tracker().finishChargingProcessEvents.size()); + assertEquals(0, scenario.tracker().abortCharingProcessEvents.size()); + + assertEquals(1, scenario.tracker().startChargingAttemptEvents.size()); + assertEquals(0, scenario.tracker().updateChargingAttemptEvents.size()); + assertEquals(0, scenario.tracker().finishChargingAttemptEvents.size()); + assertEquals(0, scenario.tracker().abortCharingAttemptEvents.size()); + + // check charger interaction + assertEquals(1, scenario.tracker().chargingStartEvents.size()); + assertEquals(1, scenario.tracker().chargingEndEvents.size()); + assertEquals(0, scenario.tracker().queuedAtChargerEvents.size()); + assertEquals(0, scenario.tracker().quitQueueAtChargerEvents.size()); + + assertEquals("charger", scenario.tracker().chargingStartEvents.getFirst().getChargerId().toString()); + + // check engine logic + assertEquals(1, scenario.tracker().plugActivityEvents.size()); + assertEquals(0, scenario.tracker().unplugActivityEvents.size()); + + assertEquals(68419.0, scenario.tracker().plugActivityEvents.getFirst().getTime()); + + assertEquals(Arrays.asList(Arrays.array( + // "activity:home", + "leg:walk", + "activity:car interaction", + "leg:car", + "activity:car interaction", + "leg:walk", + "activity:work", + "leg:walk", + "activity:car interaction", + "leg:car", + "activity:ev:plug interaction", + "leg:walk", + "activity:home")), scenario.tracker().sequences.get(Id.createPersonId("person"))); + } + + @Test + public void testChargeAtSecondTry() { + TestScenario scenario = new TestScenarioBuilder(utils) // + .addCharger("charger1", 8, 8, 0, 1.0) // plug count zero + .addCharger("charger2", 7, 7, 1, 1.0) // + .addPerson("person", 1.0) // + .addActivity("home", 0, 0, 10.0 * 3600.0) // + .addActivity("work", 8, 8, 18.0 * 3600.0) // + .addActivity("home", 0, 0) // + .build(); + + Controler controller = scenario.controller(); + + controller.addOverridingQSimModule(new AbstractQSimModule() { + @Override + protected void configureQSim() { + bind(ChargingSlotProvider.class).to(WorkActivitySlotProvider.class); + bind(ChargingAlternativeProvider.class).to(OrderedAlternativeProvider.class); + } + }); + + controller.run(); + + // check arrival at home + assertEquals("home", scenario.tracker().activityStartEvents.getLast().getActType()); + assertEquals(68366.0, scenario.tracker().activityStartEvents.getLast().getTime()); + + // check charging process + assertEquals(1, scenario.tracker().startChargingProcessEvents.size()); + assertEquals(1, scenario.tracker().finishChargingProcessEvents.size()); + assertEquals(0, scenario.tracker().abortCharingProcessEvents.size()); + + assertEquals(2, scenario.tracker().startChargingAttemptEvents.size()); + assertEquals(0, scenario.tracker().updateChargingAttemptEvents.size()); + assertEquals(1, scenario.tracker().finishChargingAttemptEvents.size()); + assertEquals(1, scenario.tracker().abortCharingAttemptEvents.size()); + + // check charger interaction + assertEquals(1, scenario.tracker().chargingStartEvents.size()); + assertEquals(1, scenario.tracker().chargingEndEvents.size()); + assertEquals(1, scenario.tracker().queuedAtChargerEvents.size()); + assertEquals(1, scenario.tracker().quitQueueAtChargerEvents.size()); + + assertEquals("charger1", scenario.tracker().queuedAtChargerEvents.getFirst().getChargerId().toString()); + assertEquals("charger2", scenario.tracker().chargingStartEvents.getFirst().getChargerId().toString()); + + // check engine logic + assertEquals(2, scenario.tracker().plugActivityEvents.size()); + assertEquals(1, scenario.tracker().unplugActivityEvents.size()); + + assertEquals(39217.0, scenario.tracker().plugActivityEvents.getFirst().getTime()); + assertEquals(40324.0, scenario.tracker().plugActivityEvents.get(1).getTime()); + assertEquals(65148.0, scenario.tracker().unplugActivityEvents.getFirst().getTime()); + + assertEquals(Arrays.asList(Arrays.array( + // "activity:home", + "leg:walk", + "activity:car interaction", + "leg:car", + "activity:ev:plug interaction", + "leg:car", + "activity:ev:plug interaction", + "leg:walk", + "activity:work", + "leg:walk", + "activity:ev:unplug interaction", + "leg:car", + "activity:car interaction", + "leg:walk", + "activity:home")), scenario.tracker().sequences.get(Id.createPersonId("person"))); + } + + @Test + public void testFailAfterSecondTry() { + TestScenario scenario = new TestScenarioBuilder(utils) // + .addCharger("charger1", 8, 8, 0, 1.0) // plug count zero + .addCharger("charger2", 7, 7, 0, 1.0) // plug count zero + .addPerson("person", 1.0) // + .addActivity("home", 0, 0, 10.0 * 3600.0) // + .addActivity("work", 8, 8, 18.0 * 3600.0) // + .addActivity("home", 0, 0) // + .build(); + + Controler controller = scenario.controller(); + + controller.addOverridingQSimModule(new AbstractQSimModule() { + @Override + protected void configureQSim() { + bind(ChargingSlotProvider.class).to(WorkActivitySlotProvider.class); + bind(ChargingAlternativeProvider.class).to(OrderedAlternativeProvider.class); + } + }); + + controller.run(); + + // check arrival at home + assertEquals("home", scenario.tracker().activityStartEvents.getLast().getActType()); + assertEquals(68419.0, scenario.tracker().activityStartEvents.getLast().getTime()); + + // check charging process + assertEquals(1, scenario.tracker().startChargingProcessEvents.size()); + assertEquals(0, scenario.tracker().finishChargingProcessEvents.size()); + assertEquals(1, scenario.tracker().abortCharingProcessEvents.size()); + + assertEquals(2, scenario.tracker().startChargingAttemptEvents.size()); + assertEquals(0, scenario.tracker().updateChargingAttemptEvents.size()); + assertEquals(0, scenario.tracker().finishChargingAttemptEvents.size()); + assertEquals(2, scenario.tracker().abortCharingAttemptEvents.size()); + + // check charger interaction + assertEquals(0, scenario.tracker().chargingStartEvents.size()); + assertEquals(0, scenario.tracker().chargingEndEvents.size()); + assertEquals(2, scenario.tracker().queuedAtChargerEvents.size()); + assertEquals(2, scenario.tracker().quitQueueAtChargerEvents.size()); + + assertEquals("charger1", scenario.tracker().queuedAtChargerEvents.getFirst().getChargerId().toString()); + assertEquals("charger2", scenario.tracker().queuedAtChargerEvents.get(1).getChargerId().toString()); + + // check engine logic + assertEquals(2, scenario.tracker().plugActivityEvents.size()); + assertEquals(0, scenario.tracker().unplugActivityEvents.size()); + + assertEquals(39217.0, scenario.tracker().plugActivityEvents.getFirst().getTime()); + assertEquals(40324.0, scenario.tracker().plugActivityEvents.get(1).getTime()); + + assertEquals(Arrays.asList(Arrays.array( + // "activity:home", + "leg:walk", + "activity:car interaction", + "leg:car", + "activity:ev:plug interaction", + "leg:car", + "activity:ev:plug interaction", + "leg:car", + "activity:car interaction", + "leg:walk", + "activity:work", + "leg:walk", + "activity:car interaction", + "leg:car", + "activity:car interaction", + "leg:walk", + "activity:home")), scenario.tracker().sequences.get(Id.createPersonId("person"))); + } + + @Test + public void testFailAfterSecondTryAndStuck() { + TestScenario scenario = new TestScenarioBuilder(utils) // + .addCharger("charger1", 8, 8, 0, 1.0) // plug count zero + .addCharger("charger2", 7, 7, 0, 1.0) // plug count zero + .addPerson("person", 1.0) // + .addActivity("home", 0, 0, 10.0 * 3600.0) // + .addActivity("work", 8, 8, 18.0 * 3600.0) // + .addActivity("home", 0, 0) // + .build(); + + WithinDayEvConfigGroup config = WithinDayEvConfigGroup.get(scenario.config()); + config.abortAgents = true; + + Controler controller = scenario.controller(); + + controller.addOverridingQSimModule(new AbstractQSimModule() { + @Override + protected void configureQSim() { + bind(ChargingSlotProvider.class).to(WorkActivitySlotProvider.class); + bind(ChargingAlternativeProvider.class).to(OrderedAlternativeProvider.class); + } + }); + + controller.run(); + + // check arrival at home + assertEquals(40626.0, scenario.tracker().personStuckEvents.getFirst().getTime()); + + // check charging process + assertEquals(1, scenario.tracker().startChargingProcessEvents.size()); + assertEquals(0, scenario.tracker().finishChargingProcessEvents.size()); + assertEquals(1, scenario.tracker().abortCharingProcessEvents.size()); + + assertEquals(2, scenario.tracker().startChargingAttemptEvents.size()); + assertEquals(0, scenario.tracker().updateChargingAttemptEvents.size()); + assertEquals(0, scenario.tracker().finishChargingAttemptEvents.size()); + assertEquals(2, scenario.tracker().abortCharingAttemptEvents.size()); + + // check charger interaction + assertEquals(0, scenario.tracker().chargingStartEvents.size()); + assertEquals(0, scenario.tracker().chargingEndEvents.size()); + assertEquals(2, scenario.tracker().queuedAtChargerEvents.size()); + assertEquals(2, scenario.tracker().quitQueueAtChargerEvents.size()); + + assertEquals("charger1", scenario.tracker().queuedAtChargerEvents.getFirst().getChargerId().toString()); + assertEquals("charger2", scenario.tracker().queuedAtChargerEvents.get(1).getChargerId().toString()); + + // check engine logic + assertEquals(2, scenario.tracker().plugActivityEvents.size()); + assertEquals(0, scenario.tracker().unplugActivityEvents.size()); + + assertEquals(39217.0, scenario.tracker().plugActivityEvents.getFirst().getTime()); + assertEquals(40324.0, scenario.tracker().plugActivityEvents.get(1).getTime()); + + assertEquals(Arrays.asList(Arrays.array( + // "activity:home", + "leg:walk", + "activity:car interaction", + "leg:car", + "activity:ev:plug interaction", + "leg:car", + "activity:ev:plug interaction")), scenario.tracker().sequences.get(Id.createPersonId("person"))); + } + + @Test + public void testChangeWhileApproaching() { + TestScenario scenario = new TestScenarioBuilder(utils) // + .addCharger("charger1", 8, 8, 1, 1.0) + .addCharger("charger2", 7, 7, 1, 1.0) + .addPerson("person", 1.0) // + .addActivity("home", 0, 0, 10.0 * 3600.0) // + .addActivity("work", 8, 8, 18.0 * 3600.0) // + .addActivity("home", 0, 0) // + .build(); + + WithinDayEvConfigGroup config = WithinDayEvConfigGroup.get(scenario.config()); + config.abortAgents = true; + + Controler controller = scenario.controller(); + + controller.addOverridingQSimModule(new AbstractQSimModule() { + @Override + protected void configureQSim() { + bind(ChargingSlotProvider.class).to(WorkActivitySlotProvider.class); + bind(ChargingAlternativeProvider.class).to(SwitchChargerAlternativeProvider.class); + } + }); + + controller.run(); + + // check arrival at home + assertEquals("home", scenario.tracker().activityStartEvents.getLast().getActType()); + assertEquals(68366.0, scenario.tracker().activityStartEvents.getLast().getTime()); + + // check charging process + assertEquals(1, scenario.tracker().startChargingProcessEvents.size()); + assertEquals(1, scenario.tracker().finishChargingProcessEvents.size()); + assertEquals(0, scenario.tracker().abortCharingProcessEvents.size()); + + assertEquals(1, scenario.tracker().startChargingAttemptEvents.size()); + assertEquals(1, scenario.tracker().updateChargingAttemptEvents.size()); + assertEquals(1, scenario.tracker().finishChargingAttemptEvents.size()); + assertEquals(0, scenario.tracker().abortCharingAttemptEvents.size()); + + // check charger interaction + assertEquals(1, scenario.tracker().chargingStartEvents.size()); + assertEquals(1, scenario.tracker().chargingEndEvents.size()); + assertEquals(0, scenario.tracker().queuedAtChargerEvents.size()); + assertEquals(0, scenario.tracker().quitQueueAtChargerEvents.size()); + + assertEquals("charger2", scenario.tracker().chargingStartEvents.getFirst().getChargerId().toString()); + + // check engine logic + assertEquals(1, scenario.tracker().plugActivityEvents.size()); + assertEquals(1, scenario.tracker().unplugActivityEvents.size()); + + assertEquals(38815.0, scenario.tracker().plugActivityEvents.getFirst().getTime()); + assertEquals(65148.0, scenario.tracker().unplugActivityEvents.getFirst().getTime()); + + assertEquals(Arrays.asList(Arrays.array( + // "activity:home", + "leg:walk", + "activity:car interaction", + "leg:car", + "activity:ev:plug interaction", + "leg:walk", + "activity:work", + "leg:walk", + "activity:ev:unplug interaction", + "leg:car", + "activity:car interaction", + "leg:walk", + "activity:home")), scenario.tracker().sequences.get(Id.createPersonId("person"))); + } + + @Test + public void testTwoAgentsCompetition() { + /* + * Two agents want to use the same charger at work, but there is only one plug. + * The first agent therefore occupies the plug, so the second one that is + * leaving 30s later must fall back to the other charger using online search. + */ + TestScenario scenario = new TestScenarioBuilder(utils) // + .addCharger("charger1", 8, 8, 1, 1.0) // plug count zero + .addCharger("charger2", 7, 7, 1, 1.0) // + .addPerson("person1", 1.0) // + /**/.addActivity("home", 0, 0, 10.0 * 3600.0) // + /**/.addActivity("work", 8, 8, 18.0 * 3600.0) // + /**/.addActivity("home", 0, 0) // + .addPerson("person2", 1.0) // + /**/.addActivity("home", 0, 0, 10.0 * 3600.0 + 30.0) // 30 seconds later + /**/.addActivity("work", 8, 8, 18.0 * 3600.0) // + /**/.addActivity("home", 0, 0) // + .build(); + + Controler controller = scenario.controller(); + + controller.addOverridingQSimModule(new AbstractQSimModule() { + @Override + protected void configureQSim() { + bind(ChargingSlotProvider.class).to(WorkActivitySlotProvider.class); + bind(ChargingAlternativeProvider.class).to(OrderedAlternativeProvider.class); + } + }); + + controller.run(); + + // check charging process + assertEquals(2, scenario.tracker().startChargingProcessEvents.size()); + assertEquals(2, scenario.tracker().finishChargingProcessEvents.size()); + + assertEquals(3, scenario.tracker().startChargingAttemptEvents.size()); + assertEquals(0, scenario.tracker().updateChargingAttemptEvents.size()); + assertEquals(2, scenario.tracker().finishChargingAttemptEvents.size()); + assertEquals(1, scenario.tracker().abortCharingAttemptEvents.size()); + + // check charger interaction + assertEquals(2, scenario.tracker().chargingStartEvents.size()); + assertEquals(2, scenario.tracker().chargingEndEvents.size()); + assertEquals(1, scenario.tracker().queuedAtChargerEvents.size()); + assertEquals(1, scenario.tracker().quitQueueAtChargerEvents.size()); + + assertEquals("person1", scenario.tracker().chargingStartEvents.get(0).getVehicleId().toString()); + assertEquals("charger1", scenario.tracker().chargingStartEvents.get(0).getChargerId().toString()); + assertEquals(39224.0, scenario.tracker().chargingStartEvents.get(0).getTime()); + + assertEquals("person2", scenario.tracker().chargingStartEvents.get(1).getVehicleId().toString()); + assertEquals("charger2", scenario.tracker().chargingStartEvents.get(1).getChargerId().toString()); + assertEquals(40364.0, scenario.tracker().chargingStartEvents.get(1).getTime()); + + assertEquals("person2", scenario.tracker().queuedAtChargerEvents.get(0).getVehicleId().toString()); + assertEquals(39254.0, scenario.tracker().queuedAtChargerEvents.get(0).getTime()); + + assertEquals("person2", scenario.tracker().quitQueueAtChargerEvents.get(0).getVehicleId().toString()); + assertEquals(39549.0, scenario.tracker().quitQueueAtChargerEvents.get(0).getTime()); + } + + @Test + public void testTwoAgentsCompetitionWithReservation() { + /* + * Two agents want to use the same charger at work. When departing, the second + * agent is allowed to reserve the charger. Hence, it is the first agent that + * will be queued at the charger and then falls back to the other one. + */ + TestScenario scenario = new TestScenarioBuilder(utils) // + .addCharger("charger1", 8, 8, 1, 1.0) // plug count zero + .addCharger("charger2", 7, 7, 1, 1.0) // + .addPerson("person1", 1.0) // + /**/.addActivity("home", 0, 0, 10.0 * 3600.0) // + /**/.addActivity("work", 8, 8, 18.0 * 3600.0) // + /**/.addActivity("home", 0, 0) // + .addPerson("person2", 1.0) // + /**/.addActivity("home", 0, 0, 10.0 * 3600.0 + 30.0) // 30 seconds later + /**/.addActivity("work", 8, 8, 18.0 * 3600.0) // + /**/.addActivity("home", 0, 0) // + .build(); + + Controler controller = scenario.controller(); + controller.addOverridingModule(new ChargerReservationModule()); + + controller.addOverridingQSimModule(new AbstractQSimModule() { + @Override + protected void configureQSim() { + bind(ChargingSlotProvider.class).to(WorkActivitySlotProvider.class); + bind(ChargingAlternativeProvider.class).to(ReservationAlternativeProvider.class); + } + }); + + controller.run(); + + // check charging process + assertEquals(2, scenario.tracker().startChargingProcessEvents.size()); + assertEquals(2, scenario.tracker().finishChargingProcessEvents.size()); + + assertEquals(3, scenario.tracker().startChargingAttemptEvents.size()); + assertEquals(0, scenario.tracker().updateChargingAttemptEvents.size()); + assertEquals(2, scenario.tracker().finishChargingAttemptEvents.size()); + assertEquals(1, scenario.tracker().abortCharingAttemptEvents.size()); + + // check charger interaction + assertEquals(2, scenario.tracker().chargingStartEvents.size()); + assertEquals(2, scenario.tracker().chargingEndEvents.size()); + assertEquals(1, scenario.tracker().queuedAtChargerEvents.size()); + assertEquals(1, scenario.tracker().quitQueueAtChargerEvents.size()); + + assertEquals("person2", scenario.tracker().chargingStartEvents.get(0).getVehicleId().toString()); + assertEquals("charger1", scenario.tracker().chargingStartEvents.get(0).getChargerId().toString()); + assertEquals(39254.0, scenario.tracker().chargingStartEvents.get(0).getTime()); + + assertEquals("person1", scenario.tracker().chargingStartEvents.get(1).getVehicleId().toString()); + assertEquals("charger2", scenario.tracker().chargingStartEvents.get(1).getChargerId().toString()); + assertEquals(40334.0, scenario.tracker().chargingStartEvents.get(1).getTime()); + + assertEquals("person1", scenario.tracker().queuedAtChargerEvents.get(0).getVehicleId().toString()); + assertEquals(39224.0, scenario.tracker().queuedAtChargerEvents.get(0).getTime()); + + assertEquals("person1", scenario.tracker().quitQueueAtChargerEvents.get(0).getVehicleId().toString()); + assertEquals(39519.0, scenario.tracker().quitQueueAtChargerEvents.get(0).getTime()); + } + + @Test + public void testChargeOverMultipleActivities() { + TestScenario scenario = new TestScenarioBuilder(utils) // + .addCharger("charger", 8, 8, 1, 1.0) // + .addPerson("person", 1.0) // + .addActivity("home", 0, 0, 10.0 * 3600.0) // + .addActivity("work", 8, 8, 14.0 * 3600.0) // + .addActivity("work", 8, 8, 16.0 * 3600.0, "walk") // + .addActivity("work", 8, 8, 18.0 * 3600.0, "walk") // + .addActivity("home", 0, 0) // + .build(); + + Controler controller = scenario.controller(); + + controller.addOverridingQSimModule(new AbstractQSimModule() { + @Override + protected void configureQSim() { + bind(ChargingSlotProvider.class).to(WorkActivitySlotProvider.class); + } + }); + + controller.run(); + + // check arrival at home + assertEquals("home", scenario.tracker().activityStartEvents.getLast().getActType()); + assertEquals(68576.0, scenario.tracker().activityStartEvents.getLast().getTime()); + + // check charging process + assertEquals(1, scenario.tracker().startChargingProcessEvents.size()); + assertEquals(1, scenario.tracker().finishChargingProcessEvents.size()); + assertEquals(0, scenario.tracker().abortCharingProcessEvents.size()); + + assertEquals(1, scenario.tracker().startChargingAttemptEvents.size()); + assertEquals(0, scenario.tracker().updateChargingAttemptEvents.size()); + assertEquals(1, scenario.tracker().finishChargingAttemptEvents.size()); + assertEquals(0, scenario.tracker().abortCharingAttemptEvents.size()); + + // check charger interaction + assertEquals(1, scenario.tracker().chargingStartEvents.size()); + assertEquals(1, scenario.tracker().chargingEndEvents.size()); + assertEquals(0, scenario.tracker().queuedAtChargerEvents.size()); + assertEquals(0, scenario.tracker().quitQueueAtChargerEvents.size()); + + assertEquals("charger", scenario.tracker().chargingStartEvents.getFirst().getChargerId().toString()); + + // check engine logic + assertEquals(1, scenario.tracker().plugActivityEvents.size()); + assertEquals(1, scenario.tracker().unplugActivityEvents.size()); + + assertEquals(39217.0, scenario.tracker().plugActivityEvents.getFirst().getTime()); + assertEquals(64956.0, scenario.tracker().unplugActivityEvents.getFirst().getTime()); + + assertEquals(Arrays.asList(Arrays.array( + // "activity:home", + "leg:walk", + "activity:car interaction", + "leg:car", + "activity:ev:plug interaction", + "leg:walk", + "activity:work", + "leg:walk", + "activity:work", + "leg:walk", + "activity:work", + "leg:walk", + "activity:ev:unplug interaction", + "leg:car", + "activity:car interaction", + "leg:walk", + "activity:home")), scenario.tracker().sequences.get(Id.createPersonId("person"))); + } + + @Test + public void testChargeUntilSecondActivity() { + TestScenario scenario = new TestScenarioBuilder(utils) // + .addCharger("charger", 8, 8, 1, 1.0) // + .addPerson("person", 1.0) // + .addActivity("home", 0, 0, 10.0 * 3600.0, "walk") // unplugging first at work + .addActivity("work", 8, 8, 18.0 * 3600.0, "walk") // same as home + .addActivity("home", 0, 0, "car") // + .build(); + + Controler controller = scenario.controller(); + + controller.addOverridingQSimModule(new AbstractQSimModule() { + @Override + protected void configureQSim() { + bind(ChargingSlotProvider.class).to(WorkActivitySlotProvider.class); + } + }); + + controller.run(); + + // check arrival at home + assertEquals("home", scenario.tracker().activityStartEvents.getLast().getActType()); + assertEquals(68576.0, scenario.tracker().activityStartEvents.getLast().getTime()); + + // check charging process + assertEquals(1, scenario.tracker().startChargingProcessEvents.size()); + assertEquals(1, scenario.tracker().finishChargingProcessEvents.size()); + assertEquals(0, scenario.tracker().abortCharingProcessEvents.size()); + + assertEquals(1, scenario.tracker().startChargingAttemptEvents.size()); + assertEquals(0, scenario.tracker().updateChargingAttemptEvents.size()); + assertEquals(1, scenario.tracker().finishChargingAttemptEvents.size()); + assertEquals(0, scenario.tracker().abortCharingAttemptEvents.size()); + + // check charger interaction + assertEquals(1, scenario.tracker().chargingStartEvents.size()); + assertEquals(1, scenario.tracker().chargingEndEvents.size()); + assertEquals(0, scenario.tracker().queuedAtChargerEvents.size()); + assertEquals(0, scenario.tracker().quitQueueAtChargerEvents.size()); + + assertEquals("charger", scenario.tracker().chargingStartEvents.getFirst().getChargerId().toString()); + + // check engine logic + assertEquals(0, scenario.tracker().plugActivityEvents.size()); + assertEquals(1, scenario.tracker().unplugActivityEvents.size()); + + assertEquals(64956.0, scenario.tracker().unplugActivityEvents.getFirst().getTime()); + + assertEquals(Arrays.asList(Arrays.array( + // "activity:home", + "leg:walk", + "activity:work", + "leg:walk", + "activity:ev:unplug interaction", + "leg:car", + "activity:car interaction", + "leg:walk", + "activity:home")), scenario.tracker().sequences.get(Id.createPersonId("person"))); + } + + @Test + public void testChargeWholeDay() { + TestScenario scenario = new TestScenarioBuilder(utils) // + .addCharger("charger", 8, 8, 1, 1.0) // + .addPerson("person", 1.0) // + .addActivity("home", 0, 0, 10.0 * 3600.0) // + .addActivity("work", 0, 0, 18.0 * 3600.0, "walk") // same as home + .addActivity("home", 0, 0, "walk") // + .build(); + + Controler controller = scenario.controller(); + + controller.addOverridingQSimModule(new AbstractQSimModule() { + @Override + protected void configureQSim() { + bind(ChargingSlotProvider.class).to(WholeDaySlotProvider.class); + } + }); + + controller.run(); + + // check arrival at home + assertEquals("home", scenario.tracker().activityStartEvents.getLast().getActType()); + // assertEquals(68576.0, + // scenario.tracker().activityStartEvents.getLast().getTime()); + + // check charging process + assertEquals(1, scenario.tracker().startChargingProcessEvents.size()); + assertEquals(0, scenario.tracker().finishChargingProcessEvents.size()); + assertEquals(0, scenario.tracker().abortCharingProcessEvents.size()); + + assertEquals(1, scenario.tracker().startChargingAttemptEvents.size()); + assertEquals(0, scenario.tracker().updateChargingAttemptEvents.size()); + assertEquals(0, scenario.tracker().finishChargingAttemptEvents.size()); + assertEquals(0, scenario.tracker().abortCharingAttemptEvents.size()); + + // check charger interaction + assertEquals(1, scenario.tracker().chargingStartEvents.size()); + assertEquals(1, scenario.tracker().chargingEndEvents.size()); + assertEquals(0, scenario.tracker().queuedAtChargerEvents.size()); + assertEquals(0, scenario.tracker().quitQueueAtChargerEvents.size()); + + assertEquals("charger", scenario.tracker().chargingStartEvents.getFirst().getChargerId().toString()); + + // check engine logic + assertEquals(0, scenario.tracker().plugActivityEvents.size()); + assertEquals(0, scenario.tracker().unplugActivityEvents.size()); + + assertEquals(Arrays.asList(Arrays.array( + // "activity:home", + "leg:walk", + "activity:work", + "leg:walk", + "activity:home")), scenario.tracker().sequences.get(Id.createPersonId("person"))); + } + + @Test + public void testChargeOnRoute() { + TestScenario scenario = new TestScenarioBuilder(utils) // + .addCharger("charger", 4, 4, 1, 1.0) // + .addPerson("person", 0.0) // + .addActivity("home", 0, 0, 10.0 * 3600.0) // + .addActivity("work", 8, 8, 18.0 * 3600.0) // + .addActivity("home", 0, 0) // + .build(); + + Controler controller = scenario.controller(); + + controller.addOverridingQSimModule(new AbstractQSimModule() { + @Override + protected void configureQSim() { + bind(ChargingSlotProvider.class).to(FirstLegSlotProvider.class); + } + }); + + controller.run(); + + // check arrival at home + assertEquals("home", scenario.tracker().activityStartEvents.getLast().getActType()); + assertEquals(68419.0, scenario.tracker().activityStartEvents.getLast().getTime()); + + // check charging process + assertEquals(1, scenario.tracker().startChargingProcessEvents.size()); + assertEquals(1, scenario.tracker().finishChargingProcessEvents.size()); + assertEquals(0, scenario.tracker().abortCharingProcessEvents.size()); + + assertEquals(1, scenario.tracker().startChargingAttemptEvents.size()); + assertEquals(0, scenario.tracker().updateChargingAttemptEvents.size()); + assertEquals(1, scenario.tracker().finishChargingAttemptEvents.size()); + assertEquals(0, scenario.tracker().abortCharingAttemptEvents.size()); + + // check charger interaction + assertEquals(1, scenario.tracker().chargingStartEvents.size()); + assertEquals(1, scenario.tracker().chargingEndEvents.size()); + assertEquals(0, scenario.tracker().queuedAtChargerEvents.size()); + assertEquals(0, scenario.tracker().quitQueueAtChargerEvents.size()); + + assertEquals(37619.0, scenario.tracker().chargingStartEvents.getFirst().getTime()); + assertEquals(49229.0, scenario.tracker().chargingEndEvents.getFirst().getTime()); + + assertEquals("charger", scenario.tracker().chargingStartEvents.getFirst().getChargerId().toString()); + + // check engine logic + assertEquals(1, scenario.tracker().plugActivityEvents.size()); + assertEquals(1, scenario.tracker().unplugActivityEvents.size()); + + assertEquals(37609.0, scenario.tracker().plugActivityEvents.getFirst().getTime()); + assertEquals(41222.0, scenario.tracker().unplugActivityEvents.getFirst().getTime()); + assertEquals(41224.0, scenario.tracker().unplugActivityEndEvents.getFirst().getTime()); + + assertEquals(Arrays.asList(Arrays.array( + // "activity:home", + "leg:walk", + "activity:car interaction", + "leg:car", + "activity:ev:plug interaction", + "activity:ev:wait interaction", + "activity:ev:unplug interaction", + "leg:car", + "activity:car interaction", + "leg:walk", + "activity:work", + "leg:walk", + "activity:car interaction", + "leg:car", + "activity:car interaction", + "leg:walk", + "activity:home")), scenario.tracker().sequences.get(Id.createPersonId("person"))); + } + + @Test + public void testChargeOnRouteWithChange() { + TestScenario scenario = new TestScenarioBuilder(utils) // + .addCharger("charger1", 4, 4, 1, 1.0) // + .addCharger("charger2", 5, 5, 1, 1.0) // + .addPerson("person", 0.0) // + .addActivity("home", 0, 0, 10.0 * 3600.0) // + .addActivity("work", 8, 8, 18.0 * 3600.0) // + .addActivity("home", 0, 0) // + .build(); + + Controler controller = scenario.controller(); + + controller.addOverridingQSimModule(new AbstractQSimModule() { + @Override + protected void configureQSim() { + bind(ChargingSlotProvider.class).to(FirstLegSlotProvider.class); + bind(ChargingAlternativeProvider.class).to(SwitchChargerAlternativeProvider.class); + } + }); + + controller.run(); + + // check arrival at home + assertEquals("home", scenario.tracker().activityStartEvents.getLast().getActType()); + assertEquals(68419.0, scenario.tracker().activityStartEvents.getLast().getTime()); + + // check charging process + assertEquals(1, scenario.tracker().startChargingProcessEvents.size()); + assertEquals(1, scenario.tracker().finishChargingProcessEvents.size()); + assertEquals(0, scenario.tracker().abortCharingProcessEvents.size()); + + assertEquals(1, scenario.tracker().startChargingAttemptEvents.size()); + assertEquals(1, scenario.tracker().updateChargingAttemptEvents.size()); + assertEquals(1, scenario.tracker().finishChargingAttemptEvents.size()); + assertEquals(0, scenario.tracker().abortCharingAttemptEvents.size()); + + // check charger interaction + assertEquals(1, scenario.tracker().chargingStartEvents.size()); + assertEquals(1, scenario.tracker().chargingEndEvents.size()); + assertEquals(0, scenario.tracker().queuedAtChargerEvents.size()); + assertEquals(0, scenario.tracker().quitQueueAtChargerEvents.size()); + + assertEquals(38024.0, scenario.tracker().chargingStartEvents.getFirst().getTime()); + assertEquals(49229.0, scenario.tracker().chargingEndEvents.getFirst().getTime()); + + assertEquals("charger2", scenario.tracker().chargingStartEvents.getFirst().getChargerId().toString()); + + // check engine logic + assertEquals(1, scenario.tracker().plugActivityEvents.size()); + assertEquals(1, scenario.tracker().unplugActivityEvents.size()); + + assertEquals(38011.0, scenario.tracker().plugActivityEvents.getFirst().getTime()); + assertEquals(41627.0, scenario.tracker().unplugActivityEvents.getFirst().getTime()); + assertEquals(41629.0, scenario.tracker().unplugActivityEndEvents.getFirst().getTime()); + + assertEquals(Arrays.asList(Arrays.array( + // "activity:home", + "leg:walk", + "activity:car interaction", + "leg:car", + "activity:ev:plug interaction", + "activity:ev:wait interaction", + "activity:ev:unplug interaction", + "leg:car", + "activity:car interaction", + "leg:walk", + "activity:work", + "leg:walk", + "activity:car interaction", + "leg:car", + "activity:car interaction", + "leg:walk", + "activity:home")), scenario.tracker().sequences.get(Id.createPersonId("person"))); + } + + @Test + public void testChargeOnRouteWithTwoAttempts() { + TestScenario scenario = new TestScenarioBuilder(utils) // + .addCharger("charger1", 4, 4, 0, 1.0) // blocked + .addCharger("charger2", 5, 5, 1, 1.0) // + .addPerson("person", 0.0) // + .addActivity("home", 0, 0, 10.0 * 3600.0) // + .addActivity("work", 8, 8, 18.0 * 3600.0) // + .addActivity("home", 0, 0) // + .build(); + + Controler controller = scenario.controller(); + + controller.addOverridingQSimModule(new AbstractQSimModule() { + @Override + protected void configureQSim() { + bind(ChargingSlotProvider.class).to(FirstLegSlotProvider.class); + bind(ChargingAlternativeProvider.class).to(OrderedAlternativeProvider.class); + } + }); + + controller.run(); + + // check arrival at home + assertEquals("home", scenario.tracker().activityStartEvents.getLast().getActType()); + assertEquals(68419.0, scenario.tracker().activityStartEvents.getLast().getTime()); + + // check charging process + assertEquals(1, scenario.tracker().startChargingProcessEvents.size()); + assertEquals(1, scenario.tracker().finishChargingProcessEvents.size()); + assertEquals(0, scenario.tracker().abortCharingProcessEvents.size()); + + assertEquals(2, scenario.tracker().startChargingAttemptEvents.size()); + assertEquals(0, scenario.tracker().updateChargingAttemptEvents.size()); + assertEquals(1, scenario.tracker().finishChargingAttemptEvents.size()); + assertEquals(1, scenario.tracker().abortCharingAttemptEvents.size()); + + // check charger interaction + assertEquals(1, scenario.tracker().chargingStartEvents.size()); + assertEquals(1, scenario.tracker().chargingEndEvents.size()); + assertEquals(1, scenario.tracker().queuedAtChargerEvents.size()); + assertEquals(1, scenario.tracker().quitQueueAtChargerEvents.size()); + + assertEquals(38324.0, scenario.tracker().chargingStartEvents.getFirst().getTime()); + assertEquals(49529.0, scenario.tracker().chargingEndEvents.getFirst().getTime()); + + assertEquals("charger2", scenario.tracker().chargingStartEvents.getFirst().getChargerId().toString()); + + // check engine logic + assertEquals(2, scenario.tracker().plugActivityEvents.size()); + assertEquals(1, scenario.tracker().unplugActivityEvents.size()); + + assertEquals(37609.0, scenario.tracker().plugActivityEvents.getFirst().getTime()); + assertEquals(38314.0, scenario.tracker().plugActivityEvents.getLast().getTime()); + assertEquals(41927.0, scenario.tracker().unplugActivityEvents.getFirst().getTime()); + assertEquals(41929.0, scenario.tracker().unplugActivityEndEvents.getFirst().getTime()); + + assertEquals(Arrays.asList(Arrays.array( + // "activity:home", + "leg:walk", + "activity:car interaction", + "leg:car", + "activity:ev:plug interaction", + "leg:car", + "activity:ev:plug interaction", + "activity:ev:wait interaction", + "activity:ev:unplug interaction", + "leg:car", + "activity:car interaction", + "leg:walk", + "activity:work", + "leg:walk", + "activity:car interaction", + "leg:car", + "activity:car interaction", + "leg:walk", + "activity:home")), scenario.tracker().sequences.get(Id.createPersonId("person"))); + } + + @Test + public void testChangeActivityToOnRoute() { + TestScenario scenario = new TestScenarioBuilder(utils) // + .addCharger("charger1", 4, 4, 1, 1.0) // + .addCharger("charger2", 5, 5, 1, 1.0) // + .addPerson("person", 1.0) // + .addActivity("home", 0, 0, 10.0 * 3600.0) // + .addActivity("work", 8, 8, 18.0 * 3600.0) // + .addActivity("home", 0, 0) // + .build(); + + Controler controller = scenario.controller(); + + controller.addOverridingQSimModule(new AbstractQSimModule() { + @Override + protected void configureQSim() { + bind(ChargingSlotProvider.class).toProvider(ActivityLegChangeProvider.createProvider(true)); + bind(ChargingAlternativeProvider.class).toProvider(ActivityLegChangeProvider.createProvider(true)); + } + }); + + controller.run(); + + // check arrival at home + assertEquals("home", scenario.tracker().activityStartEvents.getLast().getActType()); + assertEquals(68419.0, scenario.tracker().activityStartEvents.getLast().getTime()); + + // check charging process + assertEquals(1, scenario.tracker().startChargingProcessEvents.size()); + assertEquals(1, scenario.tracker().finishChargingProcessEvents.size()); + assertEquals(0, scenario.tracker().abortCharingProcessEvents.size()); + + assertEquals(1, scenario.tracker().startChargingAttemptEvents.size()); + assertEquals(1, scenario.tracker().updateChargingAttemptEvents.size()); + assertEquals(1, scenario.tracker().finishChargingAttemptEvents.size()); + assertEquals(0, scenario.tracker().abortCharingAttemptEvents.size()); + + // check charger interaction + assertEquals(1, scenario.tracker().chargingStartEvents.size()); + assertEquals(1, scenario.tracker().chargingEndEvents.size()); + assertEquals(0, scenario.tracker().queuedAtChargerEvents.size()); + assertEquals(0, scenario.tracker().quitQueueAtChargerEvents.size()); + + assertEquals("charger2", scenario.tracker().chargingStartEvents.getFirst().getChargerId().toString()); + + // check engine logic + assertEquals(1, scenario.tracker().plugActivityEvents.size()); + assertEquals(1, scenario.tracker().unplugActivityEvents.size()); + + assertEquals(38011.0, scenario.tracker().plugActivityEvents.getFirst().getTime()); + assertEquals(41627.0, scenario.tracker().unplugActivityEvents.getFirst().getTime()); + assertEquals(41629.0, scenario.tracker().unplugActivityEndEvents.getFirst().getTime()); + + assertEquals(Arrays.asList(Arrays.array( + // "activity:home", + "leg:walk", + "activity:car interaction", + "leg:car", + "activity:ev:plug interaction", + "activity:ev:wait interaction", + "activity:ev:unplug interaction", + "leg:car", + "activity:car interaction", + "leg:walk", + "activity:work", + "leg:walk", + "activity:car interaction", + "leg:car", + "activity:car interaction", + "leg:walk", + "activity:home")), scenario.tracker().sequences.get(Id.createPersonId("person"))); + } + + @Test + public void testChangeOnRouteToActivityBased() { + TestScenario scenario = new TestScenarioBuilder(utils) // + .addCharger("charger1", 4, 4, 1, 1.0) // + .addCharger("charger2", 5, 5, 1, 1.0) // + .addPerson("person", 1.0) // + .addActivity("home", 0, 0, 10.0 * 3600.0) // + .addActivity("work", 8, 8, 18.0 * 3600.0) // + .addActivity("home", 0, 0) // + .build(); + + Controler controller = scenario.controller(); + + controller.addOverridingQSimModule(new AbstractQSimModule() { + @Override + protected void configureQSim() { + bind(ChargingSlotProvider.class).toProvider(ActivityLegChangeProvider.createProvider(false)); + bind(ChargingAlternativeProvider.class).toProvider(ActivityLegChangeProvider.createProvider(false)); + } + }); + + try { + controller.run(); + } catch (IllegalStateException e) { + assertTrue(e.getMessage().startsWith("Cannot switch from a leg-based")); + return; + } + + fail(); + } + + @Test + public void testChangeOnrouteDuration() { + TestScenario scenario = new TestScenarioBuilder(utils) // + .addCharger("charger", 4, 4, 1, 1.0) // + .addPerson("person", 0.0) // + .addActivity("home", 0, 0, 10.0 * 3600.0) // + .addActivity("work", 8, 8, 18.0 * 3600.0) // + .addActivity("home", 0, 0) // + .build(); + + Controler controller = scenario.controller(); + + controller.addOverridingQSimModule(new AbstractQSimModule() { + @Override + protected void configureQSim() { + bind(ChargingSlotProvider.class).to(FirstLegSlotProvider.class); + bind(ChargingAlternativeProvider.class).to(ChangeDurationAlternativeProvider.class); + } + }); + + controller.run(); + + // check arrival at home + assertEquals("home", scenario.tracker().activityStartEvents.getLast().getActType()); + assertEquals(68419.0, scenario.tracker().activityStartEvents.getLast().getTime()); + + // check charging process + assertEquals(1, scenario.tracker().startChargingProcessEvents.size()); + assertEquals(1, scenario.tracker().finishChargingProcessEvents.size()); + assertEquals(0, scenario.tracker().abortCharingProcessEvents.size()); + + assertEquals(1, scenario.tracker().startChargingAttemptEvents.size()); + assertEquals(1, scenario.tracker().updateChargingAttemptEvents.size()); + assertEquals(1, scenario.tracker().finishChargingAttemptEvents.size()); + assertEquals(0, scenario.tracker().abortCharingAttemptEvents.size()); + + // check charger interaction + assertEquals(1, scenario.tracker().chargingStartEvents.size()); + assertEquals(1, scenario.tracker().chargingEndEvents.size()); + assertEquals(0, scenario.tracker().queuedAtChargerEvents.size()); + assertEquals(0, scenario.tracker().quitQueueAtChargerEvents.size()); + + assertEquals(37619.0, scenario.tracker().chargingStartEvents.getFirst().getTime()); + assertEquals(49229.0, scenario.tracker().chargingEndEvents.getFirst().getTime()); + + assertEquals("charger", scenario.tracker().chargingStartEvents.getFirst().getChargerId().toString()); + + // check engine logic + assertEquals(1, scenario.tracker().plugActivityEvents.size()); + assertEquals(1, scenario.tracker().unplugActivityEvents.size()); + + assertEquals(37609.0, scenario.tracker().plugActivityEvents.getFirst().getTime()); + assertEquals(39422.0, scenario.tracker().unplugActivityEvents.getFirst().getTime()); + assertEquals(39424.0, scenario.tracker().unplugActivityEndEvents.getFirst().getTime()); + } + + @Test + public void testSpntaneousCharging() { + TestScenario scenario = new TestScenarioBuilder(utils) // + .addCharger("charger", 4, 4, 1, 1.0) // + .addPerson("person", 1.0) // + .addActivity("home", 0, 0, 10.0 * 3600.0) // + .addActivity("work", 8, 8, 18.0 * 3600.0) // + .addActivity("home", 0, 0) // + .build(); + + WithinDayEvConfigGroup config = WithinDayEvConfigGroup.get(scenario.config()); + config.allowSpoantaneousCharging = true; + + Controler controller = scenario.controller(); + + controller.addOverridingQSimModule(new AbstractQSimModule() { + @Override + protected void configureQSim() { + bind(ChargingAlternativeProvider.class).to(SpontaneousChargingProvider.class); + } + }); + + controller.run(); + + // check arrival at home + assertEquals("home", scenario.tracker().activityStartEvents.getLast().getActType()); + assertEquals(68419.0, scenario.tracker().activityStartEvents.getLast().getTime()); + + // check charging process + assertEquals(1, scenario.tracker().startChargingProcessEvents.size()); + assertEquals(1, scenario.tracker().finishChargingProcessEvents.size()); + assertEquals(0, scenario.tracker().abortCharingProcessEvents.size()); + + assertEquals(1, scenario.tracker().startChargingAttemptEvents.size()); + assertEquals(0, scenario.tracker().updateChargingAttemptEvents.size()); + assertEquals(1, scenario.tracker().finishChargingAttemptEvents.size()); + assertEquals(0, scenario.tracker().abortCharingAttemptEvents.size()); + + // check charger interaction + assertEquals(1, scenario.tracker().chargingStartEvents.size()); + assertEquals(1, scenario.tracker().chargingEndEvents.size()); + assertEquals(0, scenario.tracker().queuedAtChargerEvents.size()); + assertEquals(0, scenario.tracker().quitQueueAtChargerEvents.size()); + + assertEquals("charger", scenario.tracker().chargingStartEvents.getFirst().getChargerId().toString()); + + // check engine logic + assertEquals(1, scenario.tracker().plugActivityEvents.size()); + assertEquals(1, scenario.tracker().unplugActivityEvents.size()); + + assertEquals(37609.0, scenario.tracker().plugActivityEvents.getFirst().getTime()); + assertEquals(41222.0, scenario.tracker().unplugActivityEvents.getFirst().getTime()); + assertEquals(41224.0, scenario.tracker().unplugActivityEndEvents.getFirst().getTime()); + } + + @Test + public void testTwoSlots() { + TestScenario scenario = new TestScenarioBuilder(utils) // + .addCharger("charger", 8, 8, 1, 1.0) // + .addPerson("person", 1.0) // + .addActivity("home", 0, 0, 10.0 * 3600.0) // + .addActivity("work", 8, 8, 14.0 * 3600.0) // + .addActivity("home", 0, 0, 15.0 * 3600.0) // + .addActivity("work", 8, 8, 18.0 * 3600.0) // + .addActivity("home", 0, 0) // + .build(); + + Controler controller = scenario.controller(); + + controller.addOverridingQSimModule(new AbstractQSimModule() { + @Override + protected void configureQSim() { + bind(ChargingSlotProvider.class).to(WorkActivitySlotProvider.class); + } + }); + + controller.run(); + + // check arrival at home + assertEquals("home", scenario.tracker().activityStartEvents.getLast().getActType()); + assertEquals(68576.0, scenario.tracker().activityStartEvents.getLast().getTime()); + + // check charging process + assertEquals(2, scenario.tracker().startChargingProcessEvents.size()); + assertEquals(2, scenario.tracker().finishChargingProcessEvents.size()); + assertEquals(0, scenario.tracker().abortCharingProcessEvents.size()); + + assertEquals(2, scenario.tracker().startChargingAttemptEvents.size()); + assertEquals(0, scenario.tracker().updateChargingAttemptEvents.size()); + assertEquals(2, scenario.tracker().finishChargingAttemptEvents.size()); + assertEquals(0, scenario.tracker().abortCharingAttemptEvents.size()); + + // check charger interaction + assertEquals(2, scenario.tracker().chargingStartEvents.size()); + assertEquals(2, scenario.tracker().chargingEndEvents.size()); + assertEquals(0, scenario.tracker().queuedAtChargerEvents.size()); + assertEquals(0, scenario.tracker().quitQueueAtChargerEvents.size()); + + assertEquals("charger", scenario.tracker().chargingStartEvents.getFirst().getChargerId().toString()); + assertEquals("charger", scenario.tracker().chargingStartEvents.getLast().getChargerId().toString()); + + // check engine logic + assertEquals(2, scenario.tracker().plugActivityEvents.size()); + assertEquals(2, scenario.tracker().unplugActivityEvents.size()); + + assertEquals(39217.0, scenario.tracker().plugActivityEvents.getFirst().getTime()); + assertEquals(50556.0, scenario.tracker().unplugActivityEvents.getFirst().getTime()); + + assertEquals(57393.0, scenario.tracker().plugActivityEvents.getLast().getTime()); + assertEquals(64956.0, scenario.tracker().unplugActivityEvents.getLast().getTime()); + + assertEquals(Arrays.asList(Arrays.array( + // "activity:home", + "leg:walk", + "activity:car interaction", + "leg:car", + "activity:ev:plug interaction", + "leg:walk", + "activity:work", + "leg:walk", + "activity:ev:unplug interaction", + "leg:car", + "activity:car interaction", + "leg:walk", + "activity:home", + "leg:walk", + "activity:car interaction", + "leg:car", + "activity:ev:plug interaction", + "leg:walk", + "activity:work", + "leg:walk", + "activity:ev:unplug interaction", + "leg:car", + "activity:car interaction", + "leg:walk", + "activity:home")), scenario.tracker().sequences.get(Id.createPersonId("person"))); + } +} diff --git a/contribs/ev/src/test/java/org/matsim/contrib/ev/withinday/utils/ActivityLegChangeProvider.java b/contribs/ev/src/test/java/org/matsim/contrib/ev/withinday/utils/ActivityLegChangeProvider.java new file mode 100644 index 00000000000..3fae6715d4b --- /dev/null +++ b/contribs/ev/src/test/java/org/matsim/contrib/ev/withinday/utils/ActivityLegChangeProvider.java @@ -0,0 +1,106 @@ +package org.matsim.contrib.ev.withinday.utils; + +import java.util.Collections; +import java.util.List; + +import javax.annotation.Nullable; + +import org.matsim.api.core.v01.Id; +import org.matsim.api.core.v01.population.Activity; +import org.matsim.api.core.v01.population.Leg; +import org.matsim.api.core.v01.population.Person; +import org.matsim.api.core.v01.population.Plan; +import org.matsim.api.core.v01.population.PlanElement; +import org.matsim.contrib.ev.fleet.ElectricVehicle; +import org.matsim.contrib.ev.infrastructure.Charger; +import org.matsim.contrib.ev.infrastructure.ChargingInfrastructure; +import org.matsim.contrib.ev.withinday.ChargingAlternative; +import org.matsim.contrib.ev.withinday.ChargingAlternativeProvider; +import org.matsim.contrib.ev.withinday.ChargingSlot; +import org.matsim.contrib.ev.withinday.ChargingSlotProvider; +import org.matsim.core.router.TripStructureUtils; +import org.matsim.core.router.TripStructureUtils.Trip; + +import com.google.inject.Inject; +import com.google.inject.Provider; + +public class ActivityLegChangeProvider implements ChargingAlternativeProvider, ChargingSlotProvider { + private boolean useFirstActivityBased; + private ChargingInfrastructure infrastructure; + + private Activity chargingActivity = null; + private Leg chargingLeg = null; + + ActivityLegChangeProvider(ChargingInfrastructure infrasturcutre, boolean useFirstActivityBased) { + this.infrastructure = infrasturcutre; + this.useFirstActivityBased = useFirstActivityBased; + } + + private void prepare(Plan plan) { + for (PlanElement element : plan.getPlanElements()) { + if (element instanceof Activity activity && activity.getType().equals("work")) { + chargingActivity = activity; + break; + } + } + + for (Trip trip : TripStructureUtils.getTrips(plan)) { + if (trip.getDestinationActivity().getType().equals("work")) { + for (Leg leg : trip.getLegsOnly()) { + if (leg.getMode().equals("car")) { + chargingLeg = leg; + break; + } + } + } + } + } + + @Override + public List findSlots(Person person, Plan plan, ElectricVehicle vehicle) { + Charger charger = infrastructure.getChargers().get(Id.create("charger1", Charger.class)); + prepare(plan); + + if (useFirstActivityBased) { + return Collections.singletonList(new ChargingSlot(chargingActivity, chargingActivity, null, 0, charger)); + } else { + return Collections.singletonList(new ChargingSlot(null, null, chargingLeg, 3600.0, charger)); + } + } + + @Override + @Nullable + public ChargingAlternative findEnrouteAlternative(double now, Person person, Plan plan, + ElectricVehicle vehicle, + @Nullable ChargingSlot initialSlot) { + Charger charger = infrastructure.getChargers().get(Id.create("charger2", Charger.class)); + prepare(plan); + + if (useFirstActivityBased) { + // now switch to leg-based + return new ChargingAlternative(charger, 3600.0); + } else { + // now switch to activity-based + return new ChargingAlternative(charger); + } + } + + @Override + @Nullable + public ChargingAlternative findAlternative(double now, Person person, Plan plan, ElectricVehicle vehicle, + @Nullable ChargingSlot slot, List trace) { + return null; + } + + static public Provider createProvider(boolean useFirstActivityBased) { + return new Provider<>() { + @Inject + ChargingInfrastructure infrastructure; + + @Override + public ActivityLegChangeProvider get() { + return new ActivityLegChangeProvider(infrastructure, useFirstActivityBased); + } + }; + } +} diff --git a/contribs/ev/src/test/java/org/matsim/contrib/ev/withinday/utils/ChangeDurationAlternativeProvider.java b/contribs/ev/src/test/java/org/matsim/contrib/ev/withinday/utils/ChangeDurationAlternativeProvider.java new file mode 100644 index 00000000000..1685649499b --- /dev/null +++ b/contribs/ev/src/test/java/org/matsim/contrib/ev/withinday/utils/ChangeDurationAlternativeProvider.java @@ -0,0 +1,37 @@ +package org.matsim.contrib.ev.withinday.utils; + +import java.util.List; + +import javax.annotation.Nullable; + +import org.matsim.api.core.v01.population.Person; +import org.matsim.api.core.v01.population.Plan; +import org.matsim.contrib.ev.fleet.ElectricVehicle; +import org.matsim.contrib.ev.infrastructure.ChargingInfrastructure; +import org.matsim.contrib.ev.withinday.ChargingAlternative; +import org.matsim.contrib.ev.withinday.ChargingAlternativeProvider; +import org.matsim.contrib.ev.withinday.ChargingSlot; + +import com.google.inject.Inject; +import com.google.inject.Singleton; + +@Singleton +public class ChangeDurationAlternativeProvider implements ChargingAlternativeProvider { + @Inject + ChargingInfrastructure infrastructure; + + @SuppressWarnings("null") + @Override + public ChargingAlternative findEnrouteAlternative(double now, Person person, Plan plan, + ElectricVehicle vehicle, + @Nullable ChargingSlot initialSlot) { + return new ChargingAlternative(initialSlot.charger(), 1800.0); + } + + @Override + public ChargingAlternative findAlternative(double now, Person person, Plan plan, ElectricVehicle vehicle, + @Nullable ChargingSlot slot, List trace) { + return null; + } + +} diff --git a/contribs/ev/src/test/java/org/matsim/contrib/ev/withinday/utils/FirstActivitySlotProvider.java b/contribs/ev/src/test/java/org/matsim/contrib/ev/withinday/utils/FirstActivitySlotProvider.java new file mode 100644 index 00000000000..ce4c8e1b8cd --- /dev/null +++ b/contribs/ev/src/test/java/org/matsim/contrib/ev/withinday/utils/FirstActivitySlotProvider.java @@ -0,0 +1,31 @@ +package org.matsim.contrib.ev.withinday.utils; + +import java.util.Collections; +import java.util.List; + +import org.matsim.api.core.v01.population.Activity; +import org.matsim.api.core.v01.population.Person; +import org.matsim.api.core.v01.population.Plan; +import org.matsim.api.core.v01.population.PlanElement; +import org.matsim.contrib.ev.fleet.ElectricVehicle; +import org.matsim.contrib.ev.infrastructure.Charger; +import org.matsim.contrib.ev.infrastructure.ChargingInfrastructure; +import org.matsim.contrib.ev.withinday.ChargingSlot; +import org.matsim.contrib.ev.withinday.ChargingSlotProvider; + +import com.google.inject.Inject; +import com.google.inject.Singleton; + +@Singleton +public class FirstActivitySlotProvider implements ChargingSlotProvider { + @Inject + private ChargingInfrastructure infrastructure; + + @Override + public List findSlots(Person person, Plan plan, ElectricVehicle vehicle) { + List elements = plan.getPlanElements(); + Activity activity = (Activity) elements.get(0); + Charger charger = infrastructure.getChargers().values().iterator().next(); + return Collections.singletonList(new ChargingSlot(activity, activity, null, 0.0, charger)); + } +} diff --git a/contribs/ev/src/test/java/org/matsim/contrib/ev/withinday/utils/FirstLegSlotProvider.java b/contribs/ev/src/test/java/org/matsim/contrib/ev/withinday/utils/FirstLegSlotProvider.java new file mode 100644 index 00000000000..4e1a6689fcc --- /dev/null +++ b/contribs/ev/src/test/java/org/matsim/contrib/ev/withinday/utils/FirstLegSlotProvider.java @@ -0,0 +1,46 @@ +package org.matsim.contrib.ev.withinday.utils; + +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; + +import org.matsim.api.core.v01.population.Leg; +import org.matsim.api.core.v01.population.Person; +import org.matsim.api.core.v01.population.Plan; +import org.matsim.api.core.v01.population.PlanElement; +import org.matsim.contrib.ev.fleet.ElectricVehicle; +import org.matsim.contrib.ev.infrastructure.Charger; +import org.matsim.contrib.ev.infrastructure.ChargingInfrastructure; +import org.matsim.contrib.ev.withinday.ChargingSlot; +import org.matsim.contrib.ev.withinday.ChargingSlotProvider; + +import com.google.inject.Inject; +import com.google.inject.Singleton; + +@Singleton +public class FirstLegSlotProvider implements ChargingSlotProvider { + @Inject + private ChargingInfrastructure infrastructure; + + private final static double DURATION = 3600.0; + + @Override + public List findSlots(Person person, Plan plan, ElectricVehicle vehicle) { + List chargers = new LinkedList<>(); + chargers.addAll(infrastructure.getChargers().values()); + + Collections.sort(chargers, (a, b) -> { + return String.CASE_INSENSITIVE_ORDER.compare(a.getId().toString(), b.getId().toString()); + }); + + Charger charger = chargers.get(0); + + for (PlanElement element : plan.getPlanElements()) { + if (element instanceof Leg leg) { + return Collections.singletonList(new ChargingSlot(null, null, leg, DURATION, charger)); + } + } + + return Collections.emptyList(); + } +} diff --git a/contribs/ev/src/test/java/org/matsim/contrib/ev/withinday/utils/LastActivitySlotProvider.java b/contribs/ev/src/test/java/org/matsim/contrib/ev/withinday/utils/LastActivitySlotProvider.java new file mode 100644 index 00000000000..7279ab43ae2 --- /dev/null +++ b/contribs/ev/src/test/java/org/matsim/contrib/ev/withinday/utils/LastActivitySlotProvider.java @@ -0,0 +1,31 @@ +package org.matsim.contrib.ev.withinday.utils; + +import java.util.Collections; +import java.util.List; + +import org.matsim.api.core.v01.population.Activity; +import org.matsim.api.core.v01.population.Person; +import org.matsim.api.core.v01.population.Plan; +import org.matsim.api.core.v01.population.PlanElement; +import org.matsim.contrib.ev.fleet.ElectricVehicle; +import org.matsim.contrib.ev.infrastructure.Charger; +import org.matsim.contrib.ev.infrastructure.ChargingInfrastructure; +import org.matsim.contrib.ev.withinday.ChargingSlot; +import org.matsim.contrib.ev.withinday.ChargingSlotProvider; + +import com.google.inject.Inject; +import com.google.inject.Singleton; + +@Singleton +public class LastActivitySlotProvider implements ChargingSlotProvider { + @Inject + private ChargingInfrastructure infrastructure; + + @Override + public List findSlots(Person person, Plan plan, ElectricVehicle vehicle) { + List elements = plan.getPlanElements(); + Activity activity = (Activity) elements.get(elements.size() - 1); + Charger charger = infrastructure.getChargers().values().iterator().next(); + return Collections.singletonList(new ChargingSlot(activity, activity, null, 0.0, charger)); + } +} diff --git a/contribs/ev/src/test/java/org/matsim/contrib/ev/withinday/utils/OrderedAlternativeProvider.java b/contribs/ev/src/test/java/org/matsim/contrib/ev/withinday/utils/OrderedAlternativeProvider.java new file mode 100644 index 00000000000..be122c0c923 --- /dev/null +++ b/contribs/ev/src/test/java/org/matsim/contrib/ev/withinday/utils/OrderedAlternativeProvider.java @@ -0,0 +1,61 @@ +package org.matsim.contrib.ev.withinday.utils; + +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; + +import javax.annotation.Nullable; + +import org.matsim.api.core.v01.population.Person; +import org.matsim.api.core.v01.population.Plan; +import org.matsim.contrib.ev.fleet.ElectricVehicle; +import org.matsim.contrib.ev.infrastructure.Charger; +import org.matsim.contrib.ev.infrastructure.ChargingInfrastructure; +import org.matsim.contrib.ev.withinday.ChargingAlternative; +import org.matsim.contrib.ev.withinday.ChargingAlternativeProvider; +import org.matsim.contrib.ev.withinday.ChargingSlot; + +import com.google.inject.Inject; +import com.google.inject.Singleton; + +@Singleton +public class OrderedAlternativeProvider implements ChargingAlternativeProvider { + @Inject + ChargingInfrastructure infrastructure; + + @Override + public ChargingAlternative findEnrouteAlternative(double now, Person person, Plan plan, + ElectricVehicle vehicle, + @Nullable ChargingSlot initialSlot) { + return null; + } + + @SuppressWarnings("null") + @Override + @Nullable + public ChargingAlternative findAlternative(double now, Person person, Plan plan, ElectricVehicle vehicle, + @Nullable ChargingSlot slot, List trace) { + List chargers = new LinkedList<>(); + chargers.addAll(infrastructure.getChargers().values()); + + chargers.remove(slot.charger()); + for (ChargingAlternative s : trace) { + chargers.remove(s.charger()); + } + + Collections.sort(chargers, (a, b) -> { + return String.CASE_INSENSITIVE_ORDER.compare(a.getId().toString(), b.getId().toString()); + }); + + if (chargers.size() > 0) { + if (!slot.isLegBased()) { + return new ChargingAlternative(chargers.get(0)); + } else { + return new ChargingAlternative(chargers.get(0), slot.duration()); + } + } + + return null; + } + +} diff --git a/contribs/ev/src/test/java/org/matsim/contrib/ev/withinday/utils/ReservationAlternativeProvider.java b/contribs/ev/src/test/java/org/matsim/contrib/ev/withinday/utils/ReservationAlternativeProvider.java new file mode 100644 index 00000000000..71981a440f0 --- /dev/null +++ b/contribs/ev/src/test/java/org/matsim/contrib/ev/withinday/utils/ReservationAlternativeProvider.java @@ -0,0 +1,35 @@ +package org.matsim.contrib.ev.withinday.utils; + +import javax.annotation.Nullable; + +import org.matsim.api.core.v01.population.Person; +import org.matsim.api.core.v01.population.Plan; +import org.matsim.contrib.ev.fleet.ElectricVehicle; +import org.matsim.contrib.ev.infrastructure.ChargingInfrastructure; +import org.matsim.contrib.ev.reservation.ChargerReservationManager; +import org.matsim.contrib.ev.withinday.ChargingAlternative; +import org.matsim.contrib.ev.withinday.ChargingSlot; + +import com.google.inject.Inject; +import com.google.inject.Singleton; + +@Singleton +public class ReservationAlternativeProvider extends OrderedAlternativeProvider { + @Inject + ChargingInfrastructure infrastructure; + + @Inject + ChargerReservationManager manager; + + @SuppressWarnings("null") + @Override + @Nullable + public ChargingAlternative findEnrouteAlternative(double now, Person person, Plan plan, + ElectricVehicle vehicle, @Nullable ChargingSlot initialSlot) { + if (person.getId().toString().equals("person2")) { + manager.addReservation(initialSlot.charger().getSpecification(), vehicle, now, Double.POSITIVE_INFINITY); + } + + return null; + } +} diff --git a/contribs/ev/src/test/java/org/matsim/contrib/ev/withinday/utils/SpontaneousChargingProvider.java b/contribs/ev/src/test/java/org/matsim/contrib/ev/withinday/utils/SpontaneousChargingProvider.java new file mode 100644 index 00000000000..0e5a38297be --- /dev/null +++ b/contribs/ev/src/test/java/org/matsim/contrib/ev/withinday/utils/SpontaneousChargingProvider.java @@ -0,0 +1,73 @@ +package org.matsim.contrib.ev.withinday.utils; + +import java.util.List; + +import javax.annotation.Nullable; + +import org.matsim.api.core.v01.population.Activity; +import org.matsim.api.core.v01.population.Person; +import org.matsim.api.core.v01.population.Plan; +import org.matsim.api.core.v01.population.PlanElement; +import org.matsim.contrib.ev.fleet.ElectricVehicle; +import org.matsim.contrib.ev.infrastructure.Charger; +import org.matsim.contrib.ev.infrastructure.ChargingInfrastructure; +import org.matsim.contrib.ev.withinday.ChargingAlternative; +import org.matsim.contrib.ev.withinday.ChargingAlternativeProvider; +import org.matsim.contrib.ev.withinday.ChargingSlot; +import org.matsim.core.mobsim.framework.MobsimAgent; +import org.matsim.core.mobsim.qsim.QSim; +import org.matsim.core.mobsim.qsim.agents.WithinDayAgentUtils; +import org.matsim.core.router.TripStructureUtils; + +import com.google.inject.Inject; +import com.google.inject.Singleton; + +@Singleton +public class SpontaneousChargingProvider implements ChargingAlternativeProvider { + @Inject + ChargingInfrastructure infrastructure; + + @Inject + QSim qsim; + + private boolean hasCharged = false; + + @Override + public ChargingAlternative findEnrouteAlternative(double now, Person person, Plan plan, + ElectricVehicle vehicle, + @Nullable ChargingSlot initialSlot) { + if (hasCharged) { + return null; + } + + MobsimAgent agent = qsim.getAgents().get(person.getId()); + int currentIndex = WithinDayAgentUtils.getCurrentPlanElementIndex(agent); + + Activity nextActivity = null; + for (int index = currentIndex; index < plan.getPlanElements().size(); index++) { + PlanElement element = plan.getPlanElements().get(index); + + if (element instanceof Activity activity) { + if (!TripStructureUtils.isStageActivityType(activity.getType())) { + nextActivity = activity; + break; + } + } + } + + if (nextActivity != null && nextActivity.getType().equals("work")) { + Charger charger = infrastructure.getChargers().values().iterator().next(); + hasCharged = true; + return new ChargingAlternative(charger, 3600.0); + } + + return null; + } + + @Override + @Nullable + public ChargingAlternative findAlternative(double now, Person person, Plan plan, ElectricVehicle vehicle, + ChargingSlot slot, List trace) { + return null; + } +} diff --git a/contribs/ev/src/test/java/org/matsim/contrib/ev/withinday/utils/SwitchChargerAlternativeProvider.java b/contribs/ev/src/test/java/org/matsim/contrib/ev/withinday/utils/SwitchChargerAlternativeProvider.java new file mode 100644 index 00000000000..487c19bdee8 --- /dev/null +++ b/contribs/ev/src/test/java/org/matsim/contrib/ev/withinday/utils/SwitchChargerAlternativeProvider.java @@ -0,0 +1,45 @@ +package org.matsim.contrib.ev.withinday.utils; + +import java.util.List; + +import javax.annotation.Nullable; + +import org.matsim.api.core.v01.population.Person; +import org.matsim.api.core.v01.population.Plan; +import org.matsim.contrib.ev.fleet.ElectricVehicle; +import org.matsim.contrib.ev.infrastructure.Charger; +import org.matsim.contrib.ev.infrastructure.ChargingInfrastructure; +import org.matsim.contrib.ev.withinday.ChargingAlternative; +import org.matsim.contrib.ev.withinday.ChargingAlternativeProvider; +import org.matsim.contrib.ev.withinday.ChargingSlot; + +import com.google.inject.Inject; +import com.google.inject.Singleton; + +@Singleton +public class SwitchChargerAlternativeProvider implements ChargingAlternativeProvider { + @Inject + ChargingInfrastructure infrastructure; + + @SuppressWarnings("null") + @Override + @Nullable + public ChargingAlternative findEnrouteAlternative(double now, Person person, Plan plan, + ElectricVehicle vehicle, @Nullable ChargingSlot initialSlot) { + for (Charger charger : infrastructure.getChargers().values()) { + if (charger != initialSlot.charger()) { + return new ChargingAlternative(charger, initialSlot.duration()); + } + } + + return null; + } + + @Override + @Nullable + public ChargingAlternative findAlternative(double now, Person person, Plan plan, ElectricVehicle vehicle, + ChargingSlot slot, List trace) { + return null; + } + +} diff --git a/contribs/ev/src/test/java/org/matsim/contrib/ev/withinday/utils/SwitchEnrouteChargerAlternativeProvider.java b/contribs/ev/src/test/java/org/matsim/contrib/ev/withinday/utils/SwitchEnrouteChargerAlternativeProvider.java new file mode 100644 index 00000000000..0f82d05563b --- /dev/null +++ b/contribs/ev/src/test/java/org/matsim/contrib/ev/withinday/utils/SwitchEnrouteChargerAlternativeProvider.java @@ -0,0 +1,44 @@ +package org.matsim.contrib.ev.withinday.utils; + +import java.util.List; + +import javax.annotation.Nullable; + +import org.matsim.api.core.v01.population.Person; +import org.matsim.api.core.v01.population.Plan; +import org.matsim.contrib.ev.fleet.ElectricVehicle; +import org.matsim.contrib.ev.infrastructure.Charger; +import org.matsim.contrib.ev.infrastructure.ChargingInfrastructure; +import org.matsim.contrib.ev.withinday.ChargingAlternative; +import org.matsim.contrib.ev.withinday.ChargingAlternativeProvider; +import org.matsim.contrib.ev.withinday.ChargingSlot; + +import com.google.inject.Inject; +import com.google.inject.Singleton; + +@Singleton +public class SwitchEnrouteChargerAlternativeProvider implements ChargingAlternativeProvider { + @Inject + ChargingInfrastructure infrastructure; + + @SuppressWarnings("null") + @Override + @Nullable + public ChargingAlternative findEnrouteAlternative(double now, Person person, Plan plan, + ElectricVehicle vehicle, @Nullable ChargingSlot initialSlot) { + for (Charger charger : infrastructure.getChargers().values()) { + if (charger != initialSlot.charger()) { + return new ChargingAlternative(charger, initialSlot.duration()); + } + } + + return null; + } + + @Override + @Nullable + public ChargingAlternative findAlternative(double now, Person person, Plan plan, ElectricVehicle vehicle, + ChargingSlot slot, List trace) { + return null; + } +} diff --git a/contribs/ev/src/test/java/org/matsim/contrib/ev/withinday/utils/WholeDaySlotProvider.java b/contribs/ev/src/test/java/org/matsim/contrib/ev/withinday/utils/WholeDaySlotProvider.java new file mode 100644 index 00000000000..446bc36d561 --- /dev/null +++ b/contribs/ev/src/test/java/org/matsim/contrib/ev/withinday/utils/WholeDaySlotProvider.java @@ -0,0 +1,34 @@ +package org.matsim.contrib.ev.withinday.utils; + +import java.util.Collections; +import java.util.List; + +import org.matsim.api.core.v01.population.Activity; +import org.matsim.api.core.v01.population.Person; +import org.matsim.api.core.v01.population.Plan; +import org.matsim.api.core.v01.population.PlanElement; +import org.matsim.contrib.ev.fleet.ElectricVehicle; +import org.matsim.contrib.ev.infrastructure.Charger; +import org.matsim.contrib.ev.infrastructure.ChargingInfrastructure; +import org.matsim.contrib.ev.withinday.ChargingSlot; +import org.matsim.contrib.ev.withinday.ChargingSlotProvider; + +import com.google.inject.Inject; +import com.google.inject.Singleton; + +@Singleton +public class WholeDaySlotProvider implements ChargingSlotProvider { + @Inject + private ChargingInfrastructure infrastructure; + + @Override + public List findSlots(Person person, Plan plan, ElectricVehicle vehicle) { + List elements = plan.getPlanElements(); + + Activity startActivity = (Activity) elements.get(0); + Activity endActivity = (Activity) elements.get(elements.size() - 1); + + Charger charger = infrastructure.getChargers().values().iterator().next(); + return Collections.singletonList(new ChargingSlot(startActivity, endActivity, null, 0.0, charger)); + } +} diff --git a/contribs/ev/src/test/java/org/matsim/contrib/ev/withinday/utils/WorkActivitySlotProvider.java b/contribs/ev/src/test/java/org/matsim/contrib/ev/withinday/utils/WorkActivitySlotProvider.java new file mode 100644 index 00000000000..f1544b2b4a1 --- /dev/null +++ b/contribs/ev/src/test/java/org/matsim/contrib/ev/withinday/utils/WorkActivitySlotProvider.java @@ -0,0 +1,53 @@ +package org.matsim.contrib.ev.withinday.utils; + +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; + +import org.matsim.api.core.v01.Scenario; +import org.matsim.api.core.v01.population.Person; +import org.matsim.api.core.v01.population.Plan; +import org.matsim.contrib.ev.fleet.ElectricVehicle; +import org.matsim.contrib.ev.infrastructure.Charger; +import org.matsim.contrib.ev.infrastructure.ChargingInfrastructure; +import org.matsim.contrib.ev.withinday.ChargingSlot; +import org.matsim.contrib.ev.withinday.ChargingSlotFinder; +import org.matsim.contrib.ev.withinday.ChargingSlotFinder.ActivityBasedCandidate; +import org.matsim.contrib.ev.withinday.ChargingSlotProvider; + +import com.google.inject.Inject; +import com.google.inject.Singleton; + +@Singleton +public class WorkActivitySlotProvider implements ChargingSlotProvider { + @Inject + ChargingInfrastructure infrastructure; + + @Inject + Scenario scenario; + + @Override + public List findSlots(Person person, Plan plan, ElectricVehicle vehicle) { + ChargingSlotFinder finder = new ChargingSlotFinder(scenario, "car"); + + List chargers = new LinkedList<>(); + chargers.addAll(infrastructure.getChargers().values()); + + Collections.sort(chargers, (a, b) -> { + return String.CASE_INSENSITIVE_ORDER.compare(a.getId().toString(), b.getId().toString()); + }); + + Charger charger = chargers.get(0); + + List slots = new LinkedList<>(); + + for (ActivityBasedCandidate candidate : finder.findActivityBased(person, plan)) { + if (candidate.startActivity().getType().startsWith("work") + || candidate.endActivity().getType().startsWith("work")) { + slots.add(new ChargingSlot(candidate.startActivity(), candidate.endActivity(), null, 0.0, charger)); + } + } + + return slots; + } +}