eflips-depot 1.0.1__tar.gz → 1.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of eflips-depot might be problematic. Click here for more details.

Files changed (38) hide show
  1. {eflips_depot-1.0.1 → eflips_depot-1.1.1}/PKG-INFO +2 -2
  2. {eflips_depot-1.0.1 → eflips_depot-1.1.1}/eflips/depot/api/__init__.py +309 -47
  3. {eflips_depot-1.0.1 → eflips_depot-1.1.1}/eflips/depot/api/defaults/default_settings.json +2 -2
  4. {eflips_depot-1.0.1 → eflips_depot-1.1.1}/eflips/depot/api/private.py +1 -1
  5. {eflips_depot-1.0.1 → eflips_depot-1.1.1}/pyproject.toml +2 -2
  6. {eflips_depot-1.0.1 → eflips_depot-1.1.1}/LICENSE.md +0 -0
  7. {eflips_depot-1.0.1 → eflips_depot-1.1.1}/README.md +0 -0
  8. {eflips_depot-1.0.1 → eflips_depot-1.1.1}/eflips/depot/__init__.py +0 -0
  9. {eflips_depot-1.0.1 → eflips_depot-1.1.1}/eflips/depot/configuration.py +0 -0
  10. {eflips_depot-1.0.1 → eflips_depot-1.1.1}/eflips/depot/depot.py +0 -0
  11. {eflips_depot-1.0.1 → eflips_depot-1.1.1}/eflips/depot/evaluation.py +0 -0
  12. {eflips_depot-1.0.1 → eflips_depot-1.1.1}/eflips/depot/filters.py +0 -0
  13. {eflips_depot-1.0.1 → eflips_depot-1.1.1}/eflips/depot/input_epex_power_price.py +0 -0
  14. {eflips_depot-1.0.1 → eflips_depot-1.1.1}/eflips/depot/layout_opt/__init__.py +0 -0
  15. {eflips_depot-1.0.1 → eflips_depot-1.1.1}/eflips/depot/layout_opt/doc/__init__.py +0 -0
  16. {eflips_depot-1.0.1 → eflips_depot-1.1.1}/eflips/depot/layout_opt/doc/direct_details.pdf +0 -0
  17. {eflips_depot-1.0.1 → eflips_depot-1.1.1}/eflips/depot/layout_opt/evaluation.py +0 -0
  18. {eflips_depot-1.0.1 → eflips_depot-1.1.1}/eflips/depot/layout_opt/opt_tools/__init__.py +0 -0
  19. {eflips_depot-1.0.1 → eflips_depot-1.1.1}/eflips/depot/layout_opt/opt_tools/crossover.py +0 -0
  20. {eflips_depot-1.0.1 → eflips_depot-1.1.1}/eflips/depot/layout_opt/opt_tools/fitness_c_urfd.py +0 -0
  21. {eflips_depot-1.0.1 → eflips_depot-1.1.1}/eflips/depot/layout_opt/opt_tools/fitness_util.py +0 -0
  22. {eflips_depot-1.0.1 → eflips_depot-1.1.1}/eflips/depot/layout_opt/opt_tools/init.py +0 -0
  23. {eflips_depot-1.0.1 → eflips_depot-1.1.1}/eflips/depot/layout_opt/opt_tools/mutation.py +0 -0
  24. {eflips_depot-1.0.1 → eflips_depot-1.1.1}/eflips/depot/layout_opt/optimize_c_urfd.py +0 -0
  25. {eflips_depot-1.0.1 → eflips_depot-1.1.1}/eflips/depot/layout_opt/packing.py +0 -0
  26. {eflips_depot-1.0.1 → eflips_depot-1.1.1}/eflips/depot/layout_opt/settings.py +0 -0
  27. {eflips_depot-1.0.1 → eflips_depot-1.1.1}/eflips/depot/layout_opt/template_creation.py +0 -0
  28. {eflips_depot-1.0.1 → eflips_depot-1.1.1}/eflips/depot/layout_opt/util.py +0 -0
  29. {eflips_depot-1.0.1 → eflips_depot-1.1.1}/eflips/depot/plots.py +0 -0
  30. {eflips_depot-1.0.1 → eflips_depot-1.1.1}/eflips/depot/processes.py +0 -0
  31. {eflips_depot-1.0.1 → eflips_depot-1.1.1}/eflips/depot/rating.py +0 -0
  32. {eflips_depot-1.0.1 → eflips_depot-1.1.1}/eflips/depot/resources.py +0 -0
  33. {eflips_depot-1.0.1 → eflips_depot-1.1.1}/eflips/depot/settings_config.py +0 -0
  34. {eflips_depot-1.0.1 → eflips_depot-1.1.1}/eflips/depot/simple_vehicle.py +0 -0
  35. {eflips_depot-1.0.1 → eflips_depot-1.1.1}/eflips/depot/simulation.py +0 -0
  36. {eflips_depot-1.0.1 → eflips_depot-1.1.1}/eflips/depot/smart_charging.py +0 -0
  37. {eflips_depot-1.0.1 → eflips_depot-1.1.1}/eflips/depot/standalone.py +0 -0
  38. {eflips_depot-1.0.1 → eflips_depot-1.1.1}/eflips/depot/validation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: eflips-depot
3
- Version: 1.0.1
3
+ Version: 1.1.1
4
4
  Summary: Depot Simulation for eFLIPS
5
5
  License: AGPLv3
6
6
  Author: Enrico Lauth
@@ -11,7 +11,7 @@ Classifier: Programming Language :: Python :: 3
11
11
  Classifier: Programming Language :: Python :: 3.11
12
12
  Classifier: Programming Language :: Python :: 3.12
13
13
  Requires-Dist: eflips (>=0.1.0,<0.2.0)
14
- Requires-Dist: eflips-model (>=1.1.5,<2.0.0)
14
+ Requires-Dist: eflips-model (>=2.1.2,<3.0.0)
15
15
  Requires-Dist: pandas (>=2.1.4,<3.0.0)
16
16
  Requires-Dist: simpy (>=4.0.1,<5.0.0)
17
17
  Requires-Dist: xlrd (<=1.2.0)
@@ -8,12 +8,26 @@ the simulation, which can be added to the database using the :func:`eflips.depot
8
8
  function.
9
9
  """
10
10
  import os
11
+ from contextlib import contextmanager
11
12
  from datetime import timedelta
12
13
  from math import ceil
13
- from typing import Any, Dict, Optional, Union
14
+ from typing import Any, Dict, Optional, Union, Tuple
14
15
 
15
16
  import sqlalchemy.orm
16
- from eflips.model import Event, EventType, Rotation, Scenario, Vehicle
17
+ from eflips.model import (
18
+ Event,
19
+ EventType,
20
+ Rotation,
21
+ Scenario,
22
+ Vehicle,
23
+ Depot,
24
+ Plan,
25
+ Process,
26
+ AssocPlanProcess,
27
+ AssocAreaProcess,
28
+ Area,
29
+ AreaType,
30
+ )
17
31
  from sqlalchemy import create_engine, inspect
18
32
  from sqlalchemy.orm import Session
19
33
 
@@ -28,6 +42,272 @@ from eflips.depot.api.private import (
28
42
  )
29
43
 
30
44
 
45
+ @contextmanager
46
+ def create_session(
47
+ scenario: Union[Scenario, int, Any], database_url: Optional[str] = None
48
+ ) -> Tuple[Session, Scenario]:
49
+ """
50
+ This method takes a scenario, which can be either a :class:`eflips.model.Scenario` object, an integer specifying
51
+ the ID of a scenario in the database, or any other object that has an attribute `id` that is an integer. It then
52
+ creates a SQLAlchemy session and returns it. If the scenario is a :class:`eflips.model.Scenario` object, the
53
+ session is created and returned. If the scenario is an integer or an object with an `id` attribute, the session
54
+ is created, returned and closed after the context manager is exited.
55
+ :param scenario:
56
+ :return: Yield a Tuple of the session and the scenario.
57
+ """
58
+ if isinstance(scenario, Scenario):
59
+ session = inspect(scenario).session
60
+ do_close_session = False
61
+ elif isinstance(scenario, int) or hasattr(scenario, "id"):
62
+ do_close_session = True
63
+ if isinstance(scenario, int):
64
+ scenario_id = scenario
65
+ else:
66
+ scenario_id = scenario.id
67
+
68
+ if database_url is None:
69
+ if "DATABASE_URL" in os.environ:
70
+ database_url = os.environ.get("DATABASE_URL")
71
+ else:
72
+ raise ValueError("No database URL specified.")
73
+
74
+ engine = create_engine(database_url)
75
+ session = Session(engine)
76
+ scenario = session.query(Scenario).filter(Scenario.id == scenario_id).one()
77
+ else:
78
+ raise ValueError(
79
+ "The scenario parameter must be either a Scenario object, an integer or an object with an 'id' attribute."
80
+ )
81
+
82
+ try:
83
+ yield session, scenario
84
+ finally:
85
+ if do_close_session:
86
+ session.commit()
87
+ session.close()
88
+ engine.dispose()
89
+
90
+
91
+ def _delete_depot(scenario: Scenario, session: Session):
92
+ """This function deletes all depot-related data from the database for a given scenario. Used before a new depot
93
+ in this scenario is created.
94
+
95
+ :param scenario: The scenario to be simulated
96
+ :param session: The database session
97
+
98
+ :return: None. The depot-related data will be deleted from the database.
99
+ """
100
+
101
+ # Delete assocs
102
+ session.query(AssocPlanProcess).filter(
103
+ AssocPlanProcess.scenario_id == scenario.id
104
+ ).delete()
105
+ list_of_area = session.query(Area).filter(Area.scenario_id == scenario.id).all()
106
+
107
+ for area in list_of_area:
108
+ session.query(AssocAreaProcess).filter(
109
+ AssocAreaProcess.area_id == area.id
110
+ ).delete()
111
+ session.query(Event).filter(Event.area_id == area.id).delete()
112
+
113
+ # delete processes
114
+ session.query(Process).filter(Process.scenario_id == scenario.id).delete()
115
+
116
+ # delete areas
117
+ session.query(Area).filter(Area.scenario_id == scenario.id).delete()
118
+ # delete depot
119
+ session.query(Depot).filter(Depot.scenario_id == scenario.id).delete()
120
+ # delete plan
121
+ session.query(Plan).filter(Plan.scenario_id == scenario.id).delete()
122
+ # delete assoc_plan_process
123
+
124
+ session.commit()
125
+
126
+
127
+ def generate_depot_layout(
128
+ scenario: Union[Scenario, int, Any],
129
+ charging_power: float,
130
+ database_url: Optional[str] = None,
131
+ delete_existing_depot: bool = False,
132
+ capacity: Optional[int] = None,
133
+ ):
134
+ """
135
+ This function generates a simple depot layout according to the vehicle types and rotations in the scenario.
136
+
137
+ For each vehicle type, it generates 3 areas: arrival_area, charging_area, and standby-departure_area, all with type
138
+ DIRECT_ONESIDE. The capacity of each area can be specified by user, or generated according to number of
139
+ rotations. Each area has a list of available processes. When specified by user, the capacity of all areas will be
140
+ the same.
141
+
142
+ A default plan will also be generated, which includes the following default processes: standby_arrival, cleaning,
143
+ charging and standby_departure. Each vehicle will be processed with this exact order (stancby_arrival is optional
144
+ because it only happens if a vehicle needs to wait for the next process).
145
+
146
+ Using this function causes deleting the original depot in this scenario.
147
+
148
+
149
+ :param scenario: The scenario to be simulated
150
+ :param session: The database session
151
+ :param charging_power: the charging power of the charging area in kW
152
+ :param delete_existing_depot: if there is already a depot existing in this scenario, set True to delete this
153
+ existing depot. Set to False and a ValueError will be raised if there is a depot in this scenario.
154
+ :param capacity: capacity of each area. If not specified, the capacity will be generated according to the rotation.
155
+
156
+ :return: None. The depot layout will be added to the database.
157
+ """
158
+
159
+ with create_session(scenario, database_url) as (session, scenario):
160
+ # Handles existing depot
161
+ if session.query(Depot).filter(Depot.scenario_id == scenario.id).count() != 0:
162
+ if delete_existing_depot is False:
163
+ raise ValueError("Depot already exists.")
164
+ else:
165
+ _delete_depot(scenario, session)
166
+
167
+ # Create a simple depot
168
+ depot = Depot(scenario=scenario, name="Test Depot", name_short="TD")
169
+ session.add(depot)
170
+
171
+ # Create plan
172
+ plan = Plan(scenario=scenario, name="Test Plan")
173
+ session.add(plan)
174
+
175
+ depot.default_plan = plan
176
+
177
+ # Create processes
178
+ standby_arrival = Process(
179
+ name="Standby Arrival",
180
+ scenario=scenario,
181
+ dispatchable=False,
182
+ )
183
+ clean = Process(
184
+ name="Arrival Cleaning",
185
+ scenario=scenario,
186
+ dispatchable=False,
187
+ duration=timedelta(minutes=30),
188
+ )
189
+ charging = Process(
190
+ name="Charging",
191
+ scenario=scenario,
192
+ dispatchable=False,
193
+ electric_power=charging_power,
194
+ )
195
+ standby_departure = Process(
196
+ name="Standby Pre-departure",
197
+ scenario=scenario,
198
+ dispatchable=True,
199
+ )
200
+ session.add(standby_arrival)
201
+ session.add(clean)
202
+ session.add(charging)
203
+ session.add(standby_departure)
204
+
205
+ for vehicle_type in scenario.vehicle_types:
206
+ list_of_rotations = [
207
+ rotation
208
+ for rotation in scenario.rotations
209
+ if rotation.vehicle_type_id == vehicle_type.id
210
+ ]
211
+
212
+ max_vehicle_num = (
213
+ session.query(Rotation)
214
+ .filter(Rotation.vehicle_type_id == vehicle_type.id)
215
+ .filter(Rotation.scenario_id == scenario.id)
216
+ .count()
217
+ )
218
+
219
+ # Create areas
220
+ if max_vehicle_num != 0:
221
+ if capacity is None:
222
+ # Assuming each vehicle only be assigned to one rotation
223
+ parking_capacity = max_vehicle_num
224
+
225
+ else:
226
+ parking_capacity = capacity
227
+
228
+ # Create stand by arrival area
229
+ arrival_area = Area(
230
+ scenario=scenario,
231
+ name=f"Arrival for {vehicle_type.name_short}",
232
+ depot=depot,
233
+ area_type=AreaType.DIRECT_ONESIDE,
234
+ capacity=parking_capacity,
235
+ )
236
+ session.add(arrival_area)
237
+ arrival_area.vehicle_type = vehicle_type
238
+
239
+ # Create charging area
240
+ charging_area = Area(
241
+ scenario=scenario,
242
+ name=f"Direct Charging Area for {vehicle_type.name_short}",
243
+ depot=depot,
244
+ area_type=AreaType.DIRECT_ONESIDE,
245
+ capacity=parking_capacity,
246
+ )
247
+ session.add(charging_area)
248
+ charging_area.vehicle_type = vehicle_type
249
+
250
+ # Create cleaning area
251
+
252
+ list_of_rotations.sort(key=lambda x: x.trips[-1].arrival_time)
253
+
254
+ if capacity is None:
255
+ clean_capacity = 1
256
+
257
+ # Maximum number of vehicles that can park in the cleaning area according to rotation
258
+ for rot_idx in range(0, len(list_of_rotations)):
259
+ cleaning_interval_start = (
260
+ list_of_rotations[rot_idx].trips[-1].arrival_time
261
+ )
262
+
263
+ # Potential improvement: the "edge" between copy and non-copy schedules might need higher cleaning
264
+ # capacity than real, causing standby-arrival events. Considering adding repetition_period here
265
+ # This could be solved by implementing a sliging window that rolls over from e.g. sunday (last
266
+ # day to monday (first day) again, to calculate the load from both the beginning and end of the
267
+ # data.
268
+ for next_rot_idx in range(rot_idx + 1, len(list_of_rotations)):
269
+ arrival_time = (
270
+ list_of_rotations[next_rot_idx].trips[-1].arrival_time
271
+ )
272
+
273
+ if arrival_time > cleaning_interval_start + clean.duration:
274
+ clean_capacity = max(
275
+ clean_capacity, next_rot_idx - rot_idx
276
+ )
277
+ break
278
+
279
+ else:
280
+ clean_capacity = capacity
281
+
282
+ cleaning_area = Area(
283
+ scenario=scenario,
284
+ name=f"Cleaning Area for {vehicle_type.name_short}",
285
+ depot=depot,
286
+ area_type=AreaType.DIRECT_ONESIDE,
287
+ capacity=clean_capacity,
288
+ )
289
+
290
+ session.add(cleaning_area)
291
+ cleaning_area.vehicle_type = vehicle_type
292
+
293
+ arrival_area.processes.append(standby_arrival)
294
+ cleaning_area.processes.append(clean)
295
+ charging_area.processes.append(charging)
296
+ charging_area.processes.append(standby_departure)
297
+
298
+ assocs = [
299
+ AssocPlanProcess(
300
+ scenario=scenario, process=standby_arrival, plan=plan, ordinal=0
301
+ ),
302
+ AssocPlanProcess(scenario=scenario, process=clean, plan=plan, ordinal=1),
303
+ AssocPlanProcess(scenario=scenario, process=charging, plan=plan, ordinal=2),
304
+ AssocPlanProcess(
305
+ scenario=scenario, process=standby_departure, plan=plan, ordinal=3
306
+ ),
307
+ ]
308
+ session.add_all(assocs)
309
+
310
+
31
311
  def simulate_scenario(
32
312
  scenario: Union[Scenario, int, Any],
33
313
  simple_consumption_simulation: bool = False,
@@ -69,52 +349,26 @@ def simulate_scenario(
69
349
  """
70
350
 
71
351
  # Step 0: Load the scenario
72
- if isinstance(scenario, Scenario):
73
- session = inspect(scenario).session
74
- do_close_session = False
75
- elif isinstance(scenario, int) or hasattr(scenario, "id"):
76
- do_close_session = True
77
- if isinstance(scenario, int):
78
- scenario_id = scenario
79
- else:
80
- scenario_id = scenario.id
81
-
82
- if database_url is None:
83
- if "DATABASE_URL" in os.environ:
84
- database_url = os.environ.get("DATABASE_URL")
85
- else:
86
- raise ValueError("No database URL specified.")
87
-
88
- engine = create_engine(database_url)
89
- session = Session(engine)
90
- scenario = session.query(Scenario).filter(Scenario.id == scenario_id).one()
91
- else:
92
- raise ValueError(
93
- "The scenario parameter must be either a Scenario object, an integer or an object with an 'id' attribute."
94
- )
95
-
96
- simulation_host = _init_simulation(
97
- scenario=scenario,
98
- simple_consumption_simulation=simple_consumption_simulation,
99
- repetition_period=repetition_period,
100
- )
101
-
102
- ev = _run_simulation(simulation_host)
103
-
104
- if calculate_exact_vehicle_count:
105
- vehicle_counts = ev.nvehicles_used_calculation()
352
+ with create_session(scenario, database_url) as (session, scenario):
106
353
  simulation_host = _init_simulation(
107
354
  scenario=scenario,
108
355
  simple_consumption_simulation=simple_consumption_simulation,
109
356
  repetition_period=repetition_period,
110
- vehicle_count_dict=vehicle_counts,
111
357
  )
358
+
112
359
  ev = _run_simulation(simulation_host)
113
360
 
114
- _add_evaluation_to_database(scenario.id, ev, session)
361
+ if calculate_exact_vehicle_count:
362
+ vehicle_counts = ev.nvehicles_used_calculation()
363
+ simulation_host = _init_simulation(
364
+ scenario=scenario,
365
+ simple_consumption_simulation=simple_consumption_simulation,
366
+ repetition_period=repetition_period,
367
+ vehicle_count_dict=vehicle_counts,
368
+ )
369
+ ev = _run_simulation(simulation_host)
115
370
 
116
- if do_close_session:
117
- session.close()
371
+ _add_evaluation_to_database(scenario.id, ev, session)
118
372
 
119
373
 
120
374
  def _init_simulation(
@@ -324,6 +578,7 @@ def _add_evaluation_to_database(
324
578
  session.query(Event)
325
579
  .filter(Event.scenario_id == scenario_id)
326
580
  .filter(Event.event_type != EventType.DRIVING)
581
+ .filter(Event.area_id != None)
327
582
  )
328
583
  if non_driving_event_q.count() > 0:
329
584
  raise ValueError(
@@ -578,13 +833,20 @@ def _add_evaluation_to_database(
578
833
  f"Could not find Rotation {schedule_id} in scenario {scenario_id}."
579
834
  )
580
835
  else:
581
- rotation_q.update({"vehicle_id": vehicle_id})
582
- for trip in rotation_q.one().trips:
583
- for event in trip.events:
584
- assert event.vehicle_id is None
585
- assert event.event_type == EventType.DRIVING
586
- event.vehicle_id = vehicle_id
587
- event.time_end = event.time_end - timedelta(seconds=1)
836
+ old_vehicle_id = rotation_q.one().vehicle_id
837
+ if old_vehicle_id is None:
838
+ rotation_q.update({"vehicle_id": vehicle_id})
839
+ for trip in rotation_q.one().trips:
840
+ for event in trip.events:
841
+ event.vehicle_id = vehicle_id
842
+ else:
843
+ # If there already is a vehicle assigned to this rotation, we need to change all teh events by this
844
+ # vehicle to the new vehicle
845
+ event_q = session.query(Event).filter(
846
+ Event.vehicle_id == old_vehicle_id
847
+ )
848
+ event_q.update({"vehicle_id": vehicle_id})
849
+ rotation_q.update({"vehicle_id": vehicle_id})
588
850
 
589
851
  # Write Events
590
852
  session.add_all(list_of_events)
@@ -12,10 +12,10 @@
12
12
  ],
13
13
  "FLEXPRINT_SWITCHES": {
14
14
  "operations": false,
15
- "dispatch": true,
15
+ "dispatch": false,
16
16
  "objID": false,
17
17
  "timetable": false,
18
- "dispatch2": true,
18
+ "dispatch2": false,
19
19
  "processes": false,
20
20
  "res_break": false,
21
21
  "parking_full": false,
@@ -198,7 +198,7 @@ def depot_to_template(depot: Depot) -> Dict:
198
198
  "capacity": service_capacity,
199
199
  }
200
200
 
201
- if process.availability is not None:
201
+ if process.availability is not None and len(process.availability) > 0:
202
202
  template["resource_switches"]["service_switch"] = {
203
203
  "resource": "workers_service",
204
204
  "breaks": [],
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "eflips-depot"
3
- version = "1.0.1"
3
+ version = "1.1.1"
4
4
  description = "Depot Simulation for eFLIPS"
5
5
  authors = ["Enrico Lauth <enrico.lauth@tu-berlin.de>",
6
6
  "Ludger Heide <ludger.heide@tu-berlin.de",
@@ -12,7 +12,7 @@ packages = [{ include = "eflips/depot" }]
12
12
  [tool.poetry.dependencies]
13
13
  python = "^3.11"
14
14
  simpy = "^4.0.1"
15
- eflips-model = "^1.1.5"
15
+ eflips-model = "^2.1.2"
16
16
  # Legacy dependencies, which are still needed until we refactor the code
17
17
  eflips = "^0.1.0"
18
18
  xlsxwriter = "^3.1.9"
File without changes
File without changes