eflips-depot 4.14.3__tar.gz → 4.15.9__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.
- {eflips_depot-4.14.3 → eflips_depot-4.15.9}/PKG-INFO +3 -3
- {eflips_depot-4.14.3 → eflips_depot-4.15.9}/README.md +1 -1
- {eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/api/__init__.py +19 -1
- {eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/api/private/consumption.py +65 -39
- {eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/api/private/depot.py +91 -25
- {eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/api/private/results_to_database.py +74 -24
- {eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/api/private/util.py +19 -6
- {eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/depot.py +2 -0
- {eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/processes.py +39 -3
- {eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/rating.py +1 -1
- {eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/simple_vehicle.py +12 -1
- {eflips_depot-4.14.3 → eflips_depot-4.15.9}/pyproject.toml +2 -2
- {eflips_depot-4.14.3 → eflips_depot-4.15.9}/LICENSE.md +0 -0
- {eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/__init__.py +0 -0
- {eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/api/defaults/default_settings.json +0 -0
- {eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/api/private/__init__.py +0 -0
- {eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/configuration.py +0 -0
- {eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/evaluation.py +0 -0
- {eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/filters.py +0 -0
- {eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/input_epex_power_price.py +0 -0
- {eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/layout_opt/__init__.py +0 -0
- {eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/layout_opt/doc/__init__.py +0 -0
- {eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/layout_opt/doc/direct_details.pdf +0 -0
- {eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/layout_opt/evaluation.py +0 -0
- {eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/layout_opt/opt_tools/__init__.py +0 -0
- {eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/layout_opt/opt_tools/crossover.py +0 -0
- {eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/layout_opt/opt_tools/fitness_c_urfd.py +0 -0
- {eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/layout_opt/opt_tools/fitness_util.py +0 -0
- {eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/layout_opt/opt_tools/init.py +0 -0
- {eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/layout_opt/opt_tools/mutation.py +0 -0
- {eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/layout_opt/optimize_c_urfd.py +0 -0
- {eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/layout_opt/packing.py +0 -0
- {eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/layout_opt/settings.py +0 -0
- {eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/layout_opt/template_creation.py +0 -0
- {eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/layout_opt/util.py +0 -0
- {eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/plots.py +0 -0
- {eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/resources.py +0 -0
- {eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/settings_config.py +0 -0
- {eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/simulation.py +0 -0
- {eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/smart_charging.py +0 -0
- {eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/standalone.py +0 -0
- {eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/validation.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: eflips-depot
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.15.9
|
|
4
4
|
Summary: Depot Simulation for eFLIPS
|
|
5
5
|
License: AGPL-3.0-or-later
|
|
6
6
|
License-File: LICENSE.md
|
|
@@ -14,7 +14,7 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.13
|
|
15
15
|
Requires-Dist: eflips (>=0.1.3,<0.2.0)
|
|
16
16
|
Requires-Dist: eflips-model (>=10.0.0,<11.0.0)
|
|
17
|
-
Requires-Dist: eflips-opt (>=0.3.
|
|
17
|
+
Requires-Dist: eflips-opt (>=0.3.6,<0.4.0)
|
|
18
18
|
Requires-Dist: pandas (>=2.2.0,<3.0.0)
|
|
19
19
|
Requires-Dist: scipy (>=1.14.0,<2.0.0)
|
|
20
20
|
Requires-Dist: simpy (>=4.0.1,<5.0.0)
|
|
@@ -131,5 +131,5 @@ This project is licensed under the AGPLv3 license - see the [LICENSE](LICENSE.md
|
|
|
131
131
|
|
|
132
132
|
## Funding Notice
|
|
133
133
|
|
|
134
|
-
This code was developed as part of the project [eBus2030+](
|
|
134
|
+
This code was developed as part of the project [eBus2030+](https://www.now-gmbh.de/projektfinder/e-bus-2030/) funded by the Federal German Ministry for Digital and Transport (BMDV) under grant number 03EMF0402.
|
|
135
135
|
|
|
@@ -104,4 +104,4 @@ This project is licensed under the AGPLv3 license - see the [LICENSE](LICENSE.md
|
|
|
104
104
|
|
|
105
105
|
## Funding Notice
|
|
106
106
|
|
|
107
|
-
This code was developed as part of the project [eBus2030+](
|
|
107
|
+
This code was developed as part of the project [eBus2030+](https://www.now-gmbh.de/projektfinder/e-bus-2030/) funded by the Federal German Ministry for Digital and Transport (BMDV) under grant number 03EMF0402.
|
|
@@ -532,6 +532,7 @@ def apply_even_smart_charging(
|
|
|
532
532
|
scenario: Union[Scenario, int, Any],
|
|
533
533
|
database_url: Optional[str] = None,
|
|
534
534
|
standby_departure_duration: timedelta = timedelta(minutes=5),
|
|
535
|
+
delete_existing_charging_timeseries: bool = False,
|
|
535
536
|
) -> None:
|
|
536
537
|
"""
|
|
537
538
|
Takes a scenario where depot simulation has been run and applies smart charging to the depot.
|
|
@@ -548,6 +549,8 @@ def apply_even_smart_charging(
|
|
|
548
549
|
:param standby_departure_duration: The duration of the STANDBY_DEPARTURE event. This is the time the vehicle is
|
|
549
550
|
allowed to wait at the depot before it has to leave. The default is 5 minutes.
|
|
550
551
|
|
|
552
|
+
:param delete_existing_timeseries: If True, the existing timeseries in the charging events will be deleted.
|
|
553
|
+
|
|
551
554
|
:return: None. The results are added to the database.
|
|
552
555
|
"""
|
|
553
556
|
logger = logging.getLogger(__name__)
|
|
@@ -564,6 +567,19 @@ def apply_even_smart_charging(
|
|
|
564
567
|
raise
|
|
565
568
|
|
|
566
569
|
with create_session(scenario, database_url) as (session, scenario):
|
|
570
|
+
if delete_existing_charging_timeseries is False:
|
|
571
|
+
raise ValueError(
|
|
572
|
+
"The existing timeseries of charging events needed to be deleted. Set "
|
|
573
|
+
"delete_existing_charging_timeseries=True to delete them."
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
# Delete existing timeseries in charging events
|
|
577
|
+
session.query(Event).filter(
|
|
578
|
+
Event.event_type == EventType.CHARGING_DEPOT,
|
|
579
|
+
Event.scenario_id == scenario.id,
|
|
580
|
+
).update({"timeseries": None}, synchronize_session=False)
|
|
581
|
+
session.expire_all()
|
|
582
|
+
|
|
567
583
|
depots = session.query(Depot).filter(Depot.scenario_id == scenario.id).all()
|
|
568
584
|
for depot in depots:
|
|
569
585
|
add_slack_time_to_events_of_depot(
|
|
@@ -669,7 +685,9 @@ def simulate_scenario(
|
|
|
669
685
|
case SmartChargingStrategy.NONE:
|
|
670
686
|
pass
|
|
671
687
|
case SmartChargingStrategy.EVEN:
|
|
672
|
-
apply_even_smart_charging(
|
|
688
|
+
apply_even_smart_charging(
|
|
689
|
+
scenario, database_url, delete_existing_charging_timeseries=True
|
|
690
|
+
)
|
|
673
691
|
case SmartChargingStrategy.MIN_PRICE:
|
|
674
692
|
raise NotImplementedError("MIN_PRICE strategy is not implemented yet.")
|
|
675
693
|
case _:
|
|
@@ -3,7 +3,7 @@ import warnings
|
|
|
3
3
|
from dataclasses import dataclass
|
|
4
4
|
from datetime import timedelta, datetime
|
|
5
5
|
from math import ceil
|
|
6
|
-
from typing import Tuple, List
|
|
6
|
+
from typing import Tuple, List, Optional
|
|
7
7
|
from zoneinfo import ZoneInfo
|
|
8
8
|
|
|
9
9
|
import numpy as np
|
|
@@ -88,11 +88,13 @@ class ConsumptionInformation:
|
|
|
88
88
|
"""
|
|
89
89
|
|
|
90
90
|
trip_id: int
|
|
91
|
-
consumption_lut: ConsumptionLut
|
|
91
|
+
consumption_lut: Optional[ConsumptionLut] # the LUT for the vehicle class
|
|
92
92
|
average_speed: float # the average speed of the trip in km/h
|
|
93
93
|
distance: float # the distance of the trip in km
|
|
94
|
-
temperature: float # The ambient temperature in °C
|
|
95
|
-
level_of_loading:
|
|
94
|
+
temperature: Optional[float] # The ambient temperature in °C
|
|
95
|
+
level_of_loading: Optional[
|
|
96
|
+
float
|
|
97
|
+
] # The level of loading of the vehicle as a fraction of its maximum payload
|
|
96
98
|
incline: float = 0.0 # The incline of the trip in 0.0-1.0
|
|
97
99
|
consumption: float = None # The consumption of the trip in kWh
|
|
98
100
|
consumption_per_km: float = None # The consumption per km in kWh
|
|
@@ -286,54 +288,78 @@ def extract_trip_information(
|
|
|
286
288
|
)
|
|
287
289
|
.one()
|
|
288
290
|
)
|
|
291
|
+
|
|
292
|
+
total_distance = trip.route.distance / 1000 # km
|
|
293
|
+
total_duration = (
|
|
294
|
+
trip.arrival_time - trip.departure_time
|
|
295
|
+
).total_seconds() / 3600
|
|
296
|
+
average_speed = total_distance / total_duration # km/h
|
|
297
|
+
|
|
289
298
|
# Check exactly one of the vehicle classes has a consumption LUT
|
|
290
299
|
all_consumption_luts = [
|
|
291
300
|
vehicle_class.consumption_lut
|
|
292
301
|
for vehicle_class in trip.rotation.vehicle_type.vehicle_classes
|
|
293
302
|
]
|
|
294
303
|
all_consumption_luts = [x for x in all_consumption_luts if x is not None]
|
|
295
|
-
if len(all_consumption_luts) != 1:
|
|
296
|
-
raise ValueError(
|
|
297
|
-
f"Expected exactly one consumption LUT, got {len(all_consumption_luts)}"
|
|
298
|
-
)
|
|
299
|
-
consumption_lut = all_consumption_luts[0]
|
|
300
|
-
# Disconnect the consumption LUT from the session to avoid loading the whole table
|
|
301
304
|
|
|
302
|
-
|
|
305
|
+
temperature = temperature_for_trip(trip_id, session)
|
|
303
306
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
307
|
+
if len(all_consumption_luts) == 1:
|
|
308
|
+
payload_mass = passenger_mass * passenger_count
|
|
309
|
+
assert (
|
|
310
|
+
trip.rotation.vehicle_type.allowed_mass is not None
|
|
311
|
+
), f"allowed_mass of vehicle {trip.rotation.vehicle_type} must be set"
|
|
309
312
|
|
|
310
|
-
|
|
313
|
+
assert (
|
|
314
|
+
trip.rotation.vehicle_type.empty_mass is not None
|
|
315
|
+
), f"empty_mass of vehicle {trip.rotation.vehicle_type} must be set"
|
|
311
316
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
317
|
+
full_payload = (
|
|
318
|
+
trip.rotation.vehicle_type.allowed_mass
|
|
319
|
+
- trip.rotation.vehicle_type.empty_mass
|
|
320
|
+
)
|
|
321
|
+
level_of_loading = payload_mass / full_payload
|
|
316
322
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
), f"empty_mass of vehicle {trip.rotation.vehicle_type} must be set"
|
|
323
|
+
consumption_lut = all_consumption_luts[0]
|
|
324
|
+
# Disconnect the consumption LUT from the session to avoid loading the whole table
|
|
320
325
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
326
|
+
del all_consumption_luts
|
|
327
|
+
|
|
328
|
+
info = ConsumptionInformation(
|
|
329
|
+
trip_id=trip.id,
|
|
330
|
+
consumption_lut=consumption_lut,
|
|
331
|
+
average_speed=average_speed,
|
|
332
|
+
distance=total_distance,
|
|
333
|
+
temperature=temperature,
|
|
334
|
+
level_of_loading=level_of_loading,
|
|
335
|
+
)
|
|
336
|
+
info.calculate()
|
|
337
|
+
elif len(all_consumption_luts) == 0:
|
|
338
|
+
warnings.warn(
|
|
339
|
+
f"No consumption LUT found for vehicle type {trip.rotation.vehicle_type}.",
|
|
340
|
+
ConsistencyWarning,
|
|
341
|
+
)
|
|
342
|
+
# Here, we fill out the condumption information without the LUT and LUT data, but with `consumption_per_km`
|
|
343
|
+
# set to the vehicle's `consumption` value.
|
|
344
|
+
if trip.rotation.vehicle_type.consumption is None:
|
|
345
|
+
raise ValueError(
|
|
346
|
+
f"Vehicle type {trip.rotation.vehicle_type} must have a consumption value set if no consumption LUT is available."
|
|
347
|
+
)
|
|
348
|
+
info = ConsumptionInformation(
|
|
349
|
+
trip_id=trip.id,
|
|
350
|
+
average_speed=average_speed,
|
|
351
|
+
distance=total_distance,
|
|
352
|
+
consumption_per_km=trip.rotation.vehicle_type.consumption,
|
|
353
|
+
consumption=trip.rotation.vehicle_type.consumption * total_distance,
|
|
354
|
+
consumption_lut=None,
|
|
355
|
+
temperature=temperature,
|
|
356
|
+
level_of_loading=None,
|
|
357
|
+
)
|
|
358
|
+
else:
|
|
359
|
+
raise ValueError(
|
|
360
|
+
f"Expected exactly one consumption LUT, got {len(all_consumption_luts)}"
|
|
361
|
+
)
|
|
335
362
|
|
|
336
|
-
info.calculate()
|
|
337
363
|
return info
|
|
338
364
|
|
|
339
365
|
|
|
@@ -126,20 +126,27 @@ class DepotConfigurationWish:
|
|
|
126
126
|
self.areas = areas
|
|
127
127
|
|
|
128
128
|
if self.auto_generate is True:
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
)
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
129
|
+
assert (
|
|
130
|
+
self.default_power is not None
|
|
131
|
+
), "default_power must be provided if auto_generate is True"
|
|
132
|
+
assert (
|
|
133
|
+
self.standard_block_length is not None
|
|
134
|
+
), "standard_block_length must be provided if auto_generate is True"
|
|
135
|
+
|
|
136
|
+
assert (
|
|
137
|
+
self.cleaning_slots is None
|
|
138
|
+
), "cleaning_slots cannot be provided if auto_generate is True"
|
|
139
|
+
assert (
|
|
140
|
+
self.cleaning_duration is None
|
|
141
|
+
), "cleaning_duration cannot be provided if auto_generate is True"
|
|
142
|
+
assert (
|
|
143
|
+
self.shunting_slots is None
|
|
144
|
+
), "shunting_slots cannot be provided if auto_generate is True"
|
|
145
|
+
assert (
|
|
146
|
+
self.shunting_duration is None
|
|
147
|
+
), "shunting_duration cannot be provided if auto_generate is True"
|
|
148
|
+
|
|
149
|
+
assert areas is None, "areas cannot be provided if auto_generate is True"
|
|
143
150
|
|
|
144
151
|
|
|
145
152
|
def delete_depots(scenario: Scenario, session: Session) -> None:
|
|
@@ -204,7 +211,7 @@ def depot_to_template(depot: Depot) -> Dict[str, str | Dict[str, str | int]]:
|
|
|
204
211
|
list_of_processes = []
|
|
205
212
|
|
|
206
213
|
# Get dictionary of each area
|
|
207
|
-
# For line areas, generate
|
|
214
|
+
# For line areas, generate a dictionary item for total areas, later it will be split into individual lines
|
|
208
215
|
for area in depot.areas:
|
|
209
216
|
area_name = str(area.id)
|
|
210
217
|
template["areas"][area_name] = {
|
|
@@ -226,6 +233,17 @@ def depot_to_template(depot: Depot) -> Dict[str, str | Dict[str, str | int]]:
|
|
|
226
233
|
"filter_names": ["vehicle_type"],
|
|
227
234
|
"vehicle_types": [str(area.vehicle_type_id)],
|
|
228
235
|
}
|
|
236
|
+
for processes_in_area in area.processes:
|
|
237
|
+
if process_type(processes_in_area) == ProcessType.CHARGING:
|
|
238
|
+
# Add the charging process for this vehicle type
|
|
239
|
+
template["areas"][area_name]["available_processes"].append(
|
|
240
|
+
str(processes_in_area.id) + "vt" + str(area.vehicle_type_id)
|
|
241
|
+
)
|
|
242
|
+
# Delete the original charging process
|
|
243
|
+
template["areas"][area_name]["available_processes"].remove(
|
|
244
|
+
str(processes_in_area.id)
|
|
245
|
+
)
|
|
246
|
+
|
|
229
247
|
else:
|
|
230
248
|
# If the vehicle type id is not set, the area is for all vehicle types
|
|
231
249
|
scenario = depot.scenario
|
|
@@ -236,6 +254,19 @@ def depot_to_template(depot: Depot) -> Dict[str, str | Dict[str, str | int]]:
|
|
|
236
254
|
"vehicle_types": all_vehicle_type_ids,
|
|
237
255
|
}
|
|
238
256
|
|
|
257
|
+
# if there are any charging areas, all a unique charging process for each vehicle type
|
|
258
|
+
for processes_in_area in area.processes:
|
|
259
|
+
if process_type(processes_in_area) == ProcessType.CHARGING:
|
|
260
|
+
# Add the charging process for this vehicle type
|
|
261
|
+
for vt in scenario.vehicle_types:
|
|
262
|
+
template["areas"][area_name]["available_processes"].append(
|
|
263
|
+
str(processes_in_area.id) + "vt" + str(vt.id)
|
|
264
|
+
)
|
|
265
|
+
# Delete the original charging process
|
|
266
|
+
template["areas"][area_name]["available_processes"].remove(
|
|
267
|
+
str(processes_in_area.id)
|
|
268
|
+
)
|
|
269
|
+
|
|
239
270
|
for process in area.processes:
|
|
240
271
|
# Add process into process list
|
|
241
272
|
list_of_processes.append(
|
|
@@ -285,7 +316,7 @@ def depot_to_template(depot: Depot) -> Dict[str, str | Dict[str, str | int]]:
|
|
|
285
316
|
for name in line_areas_to_delete:
|
|
286
317
|
del area_template[name]
|
|
287
318
|
|
|
288
|
-
# Fill in the dictionary of
|
|
319
|
+
# Fill in the dictionary of processes
|
|
289
320
|
for process in list_of_processes:
|
|
290
321
|
process_name = str(process.id)
|
|
291
322
|
# Shared template for all processes
|
|
@@ -358,8 +389,43 @@ def depot_to_template(depot: Depot) -> Dict[str, str | Dict[str, str | int]]:
|
|
|
358
389
|
] = list_of_breaks_in_seconds
|
|
359
390
|
|
|
360
391
|
case ProcessType.CHARGING:
|
|
361
|
-
|
|
362
|
-
|
|
392
|
+
all_vehicle_types = depot.scenario.vehicle_types
|
|
393
|
+
|
|
394
|
+
for vt in all_vehicle_types:
|
|
395
|
+
charging_curve = vt.charging_curve
|
|
396
|
+
|
|
397
|
+
charging_process_name = process_name + "vt" + str(vt.id)
|
|
398
|
+
template["processes"][charging_process_name] = template[
|
|
399
|
+
"processes"
|
|
400
|
+
][process_name].copy()
|
|
401
|
+
template["processes"][charging_process_name][
|
|
402
|
+
"typename"
|
|
403
|
+
] = "ChargeEquationSteps"
|
|
404
|
+
template["processes"][charging_process_name]["vehicle_filter"] = {
|
|
405
|
+
"filter_names": ["vehicle_type"],
|
|
406
|
+
"vehicle_types": [str(vt.id)],
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
template["processes"][charging_process_name][
|
|
410
|
+
"peq_name"
|
|
411
|
+
] = "charging_curve_power"
|
|
412
|
+
template["processes"][charging_process_name]["peq_params"] = {
|
|
413
|
+
"soc": [soc_power_pair[0] for soc_power_pair in charging_curve],
|
|
414
|
+
"power": [
|
|
415
|
+
soc_power_pair[1] for soc_power_pair in charging_curve
|
|
416
|
+
],
|
|
417
|
+
"precision": 0.01,
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
del template["processes"][charging_process_name]["dur"]
|
|
421
|
+
|
|
422
|
+
# delete the original process
|
|
423
|
+
del template["processes"][process_name]
|
|
424
|
+
|
|
425
|
+
# The original one
|
|
426
|
+
# template["processes"][process_name]["typename"] = "Charge"
|
|
427
|
+
|
|
428
|
+
# del template["processes"][process_name]["dur"]
|
|
363
429
|
|
|
364
430
|
case ProcessType.STANDBY | ProcessType.STANDBY_DEPARTURE:
|
|
365
431
|
template["processes"][process_name]["typename"] = "Standby"
|
|
@@ -1137,7 +1203,9 @@ def depot_smallest_possible_size(
|
|
|
1137
1203
|
|
|
1138
1204
|
# Simulate the depot
|
|
1139
1205
|
simulate_scenario(
|
|
1140
|
-
scenario,
|
|
1206
|
+
scenario,
|
|
1207
|
+
smart_charging_strategy=SmartChargingStrategy.NONE,
|
|
1208
|
+
ignore_unstable_simulation=True,
|
|
1141
1209
|
)
|
|
1142
1210
|
|
|
1143
1211
|
# Find the peak usage of the depot
|
|
@@ -1201,12 +1269,6 @@ def depot_smallest_possible_size(
|
|
|
1201
1269
|
|
|
1202
1270
|
except MissingVehicleDimensionError as e:
|
|
1203
1271
|
raise e
|
|
1204
|
-
except UnstableSimulationException as e:
|
|
1205
|
-
# This change is made after Unstable exception and delay exceptions are introduced
|
|
1206
|
-
logger.debug(
|
|
1207
|
-
f"Results are unstable, suggesting depot is too small."
|
|
1208
|
-
)
|
|
1209
|
-
continue
|
|
1210
1272
|
except DelayedTripException as e:
|
|
1211
1273
|
logger.debug(f"Trips are delayed, suggesting depot is too small.")
|
|
1212
1274
|
continue
|
|
@@ -1216,6 +1278,10 @@ def depot_smallest_possible_size(
|
|
|
1216
1278
|
# Identify the best configuration for each vehicle type
|
|
1217
1279
|
ret_val: Dict[VehicleType, Dict[AreaType, int]] = dict()
|
|
1218
1280
|
for vt in vts_and_rotations.keys():
|
|
1281
|
+
assert area_needed[vt] != {}, (
|
|
1282
|
+
f"No valid configurations found for {vt.name}, "
|
|
1283
|
+
f"please check if there are any delays after simulating scenario."
|
|
1284
|
+
)
|
|
1219
1285
|
best_config = min(area_needed[vt].keys(), key=lambda x: area_needed[vt][x])
|
|
1220
1286
|
ret_val[vt] = {
|
|
1221
1287
|
AreaType.LINE: best_config * standard_block_length,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import datetime
|
|
2
2
|
import itertools
|
|
3
3
|
import logging
|
|
4
|
+
import math
|
|
4
5
|
import warnings
|
|
5
6
|
from datetime import timedelta
|
|
6
7
|
from typing import List, Dict
|
|
@@ -225,8 +226,6 @@ def generate_vehicle_events(
|
|
|
225
226
|
"is_waiting": True,
|
|
226
227
|
}
|
|
227
228
|
|
|
228
|
-
# Create a list of battery log in order of time asc. Convenient for looking up corresponding soc
|
|
229
|
-
|
|
230
229
|
for time_stamp, process_log in current_vehicle.logger.loggedData[
|
|
231
230
|
"dwd.active_processes_copy"
|
|
232
231
|
].items():
|
|
@@ -276,13 +275,18 @@ def generate_vehicle_events(
|
|
|
276
275
|
)
|
|
277
276
|
start_this_event = dict_of_events[time_stamp]["end"]
|
|
278
277
|
else:
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
278
|
+
if len(process_log) > 1:
|
|
279
|
+
# This is for the case where the charging and standby_departure happen in the same area, and the standby_departure is the last process.
|
|
280
|
+
for other_process in process_log:
|
|
281
|
+
if (
|
|
282
|
+
other_process.ID != process.ID
|
|
283
|
+
and other_process.dur > 0
|
|
284
|
+
):
|
|
285
|
+
start_this_event = other_process.ends[0]
|
|
286
|
+
break
|
|
287
|
+
else:
|
|
288
|
+
# This is for the case where only standby_departure happens in the last area.
|
|
289
|
+
start_this_event = time_stamp
|
|
286
290
|
|
|
287
291
|
assert (
|
|
288
292
|
start_this_event is not None
|
|
@@ -395,7 +399,11 @@ def add_soc_to_events(dict_of_events, battery_log) -> None:
|
|
|
395
399
|
"""
|
|
396
400
|
battery_log_list = []
|
|
397
401
|
for log in battery_log:
|
|
398
|
-
|
|
402
|
+
# TODO this is a bypass of update events having lower energy_real than the event before. It happens in processes L 1304
|
|
403
|
+
if log.event_name == "update":
|
|
404
|
+
continue
|
|
405
|
+
|
|
406
|
+
battery_log_list.append((log.t, round(log.energy / log.energy_real, 4)))
|
|
399
407
|
|
|
400
408
|
time_keys = sorted(dict_of_events.keys())
|
|
401
409
|
|
|
@@ -408,15 +416,42 @@ def add_soc_to_events(dict_of_events, battery_log) -> None:
|
|
|
408
416
|
start_time = time_keys[i]
|
|
409
417
|
process_dict = dict_of_events[time_keys[i]]
|
|
410
418
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
419
|
+
match process_dict["type"]:
|
|
420
|
+
case "Charge" | "ChargeSteps" | "ChargeEquationSteps":
|
|
421
|
+
event_start = start_time
|
|
422
|
+
event_end = process_dict["end"]
|
|
423
|
+
start_time_index = battery_log_times.index(event_start)
|
|
424
|
+
end_time_index = battery_log_times.index(event_end)
|
|
425
|
+
time_series = {
|
|
426
|
+
"time": battery_log_times[start_time_index:end_time_index],
|
|
427
|
+
"soc": battery_log_socs[start_time_index:end_time_index],
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
# if there are repeated timestamps, we need to remove them
|
|
431
|
+
unique_indices = np.unique(
|
|
432
|
+
time_series["time"], return_index=True, return_inverse=False
|
|
433
|
+
)[1]
|
|
434
|
+
time_series["time"] = [
|
|
435
|
+
time_series["time"][index] for index in unique_indices
|
|
436
|
+
]
|
|
437
|
+
time_series["soc"] = [
|
|
438
|
+
time_series["soc"][index] for index in unique_indices
|
|
439
|
+
]
|
|
440
|
+
process_dict["timeseries"] = time_series
|
|
441
|
+
process_dict["soc_start"] = battery_log_socs[start_time_index]
|
|
442
|
+
process_dict["soc_end"] = battery_log_socs[end_time_index]
|
|
443
|
+
|
|
444
|
+
case "Serve" | "Standby":
|
|
445
|
+
soc_start = np.interp(start_time, battery_log_times, battery_log_socs)
|
|
446
|
+
process_dict["soc_start"] = min(float(soc_start), 1.0)
|
|
447
|
+
soc_end = np.interp(
|
|
448
|
+
process_dict["end"], battery_log_times, battery_log_socs
|
|
449
|
+
)
|
|
450
|
+
process_dict["soc_end"] = min(float(soc_end), 1.0)
|
|
451
|
+
case "Trip":
|
|
452
|
+
continue
|
|
453
|
+
case _:
|
|
454
|
+
raise NotImplementedError
|
|
420
455
|
|
|
421
456
|
|
|
422
457
|
def add_events_into_database(
|
|
@@ -446,7 +481,7 @@ def add_events_into_database(
|
|
|
446
481
|
match process_dict["type"]:
|
|
447
482
|
case "Serve":
|
|
448
483
|
event_type = EventType.SERVICE
|
|
449
|
-
case "Charge":
|
|
484
|
+
case "Charge" | "ChargeSteps" | "ChargeEquationSteps": # TODO that might be problematic
|
|
450
485
|
event_type = EventType.CHARGING_DEPOT
|
|
451
486
|
case "Standby":
|
|
452
487
|
if (
|
|
@@ -466,7 +501,7 @@ def add_events_into_database(
|
|
|
466
501
|
'"Standby", "Precondition"'
|
|
467
502
|
)
|
|
468
503
|
|
|
469
|
-
if process_dict["end"] == start_time:
|
|
504
|
+
if math.ceil(process_dict["end"]) == math.ceil(start_time):
|
|
470
505
|
logger.warning("Refusing to create an event with zero duration.")
|
|
471
506
|
continue
|
|
472
507
|
|
|
@@ -488,6 +523,20 @@ def add_events_into_database(
|
|
|
488
523
|
capacity_per_line = int(current_area.capacity / current_area.row_count)
|
|
489
524
|
process_dict["slot"] = capacity_per_line * row + process_dict["slot"] - 1
|
|
490
525
|
|
|
526
|
+
timeseries = {}
|
|
527
|
+
if "timeseries" in process_dict.keys():
|
|
528
|
+
# Convert "time" in timeseries to timedelta
|
|
529
|
+
timeseries["time"] = [
|
|
530
|
+
(timedelta(seconds=math.ceil(t)) + simulation_start_time).isoformat()
|
|
531
|
+
for t in process_dict["timeseries"]["time"]
|
|
532
|
+
]
|
|
533
|
+
timeseries["soc"] = process_dict["timeseries"]["soc"]
|
|
534
|
+
|
|
535
|
+
assert all(
|
|
536
|
+
timeseries["soc"][i] <= timeseries["soc"][i + 1]
|
|
537
|
+
for i in range(len(timeseries["soc"]) - 1)
|
|
538
|
+
), "SOC values in the timeseries should be non-decreasing."
|
|
539
|
+
|
|
491
540
|
current_event = Event(
|
|
492
541
|
scenario=scenario,
|
|
493
542
|
vehicle_type_id=db_vehicle.vehicle_type_id,
|
|
@@ -496,8 +545,9 @@ def add_events_into_database(
|
|
|
496
545
|
area_id=area_id,
|
|
497
546
|
subloc_no=process_dict["slot"] if "slot" in process_dict.keys() else 00,
|
|
498
547
|
trip_id=None,
|
|
499
|
-
time_start=timedelta(seconds=start_time) + simulation_start_time,
|
|
500
|
-
time_end=timedelta(seconds=process_dict["end"])
|
|
548
|
+
time_start=timedelta(seconds=math.ceil(start_time)) + simulation_start_time,
|
|
549
|
+
time_end=timedelta(seconds=math.ceil(process_dict["end"]))
|
|
550
|
+
+ simulation_start_time,
|
|
501
551
|
soc_start=process_dict["soc_start"]
|
|
502
552
|
if process_dict["soc_start"] is not None
|
|
503
553
|
else process_dict["soc_end"],
|
|
@@ -507,7 +557,7 @@ def add_events_into_database(
|
|
|
507
557
|
# then this is not an event with soc change
|
|
508
558
|
event_type=event_type,
|
|
509
559
|
description=process_dict["id"] if "id" in process_dict.keys() else None,
|
|
510
|
-
timeseries=None,
|
|
560
|
+
timeseries=timeseries if "timeseries" in process_dict.keys() else None,
|
|
511
561
|
)
|
|
512
562
|
|
|
513
563
|
session.add(current_event)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""This module contains miscellaneous utility functions for the eflips-depot API."""
|
|
2
2
|
import logging
|
|
3
3
|
import os
|
|
4
|
+
import warnings
|
|
4
5
|
from contextlib import contextmanager
|
|
5
6
|
from dataclasses import dataclass
|
|
6
7
|
from datetime import timedelta, datetime
|
|
@@ -17,9 +18,11 @@ from eflips.model import (
|
|
|
17
18
|
Trip,
|
|
18
19
|
Depot,
|
|
19
20
|
Temperatures,
|
|
21
|
+
ConsistencyWarning,
|
|
20
22
|
)
|
|
21
23
|
from eflips.model import create_engine
|
|
22
24
|
from sqlalchemy import inspect
|
|
25
|
+
from sqlalchemy.exc import NoResultFound
|
|
23
26
|
from sqlalchemy.orm import Session
|
|
24
27
|
|
|
25
28
|
from eflips.depot import SimpleTrip, Timetable as EflipsTimeTable
|
|
@@ -99,6 +102,7 @@ def vehicle_type_to_global_constants_dict(vt: VehicleType) -> Dict[str, float]:
|
|
|
99
102
|
"soc_max": 1.0,
|
|
100
103
|
"soc_init": 1.0,
|
|
101
104
|
"soh": 1.0,
|
|
105
|
+
"charging_efficiency": vt.charging_efficiency,
|
|
102
106
|
}
|
|
103
107
|
return the_dict
|
|
104
108
|
|
|
@@ -204,7 +208,9 @@ def check_depot_validity(depot: Depot) -> None:
|
|
|
204
208
|
|
|
205
209
|
def temperature_for_trip(trip_id: int, session: Session) -> float:
|
|
206
210
|
"""
|
|
207
|
-
Returns the temperature for a trip.
|
|
211
|
+
Returns the temperature for a trip.
|
|
212
|
+
|
|
213
|
+
Finds the temperature for the mid-point of the trip.
|
|
208
214
|
|
|
209
215
|
:param trip_id: The ID of the trip
|
|
210
216
|
:param session: The SQLAlchemy session
|
|
@@ -212,11 +218,18 @@ def temperature_for_trip(trip_id: int, session: Session) -> float:
|
|
|
212
218
|
"""
|
|
213
219
|
|
|
214
220
|
trip = session.query(Trip).filter(Trip.id == trip_id).one()
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
221
|
+
try:
|
|
222
|
+
temperatures = (
|
|
223
|
+
session.query(Temperatures)
|
|
224
|
+
.filter(Temperatures.scenario_id == trip.scenario_id)
|
|
225
|
+
.one()
|
|
226
|
+
)
|
|
227
|
+
except NoResultFound:
|
|
228
|
+
warnings.warn(
|
|
229
|
+
f"No temperatures found for scenario {trip.scenario_id}.",
|
|
230
|
+
ConsistencyWarning,
|
|
231
|
+
)
|
|
232
|
+
return None
|
|
220
233
|
|
|
221
234
|
# Find the mid-point of the trip
|
|
222
235
|
mid_time = trip.departure_time + (trip.arrival_time - trip.departure_time) / 2
|
|
@@ -1360,7 +1360,9 @@ class DepotControl:
|
|
|
1360
1360
|
for procID in area.available_processes
|
|
1361
1361
|
if self.depot.processes[procID]["type"].request_immediately
|
|
1362
1362
|
and (
|
|
1363
|
+
# TODO: Simplify this condition
|
|
1363
1364
|
self.depot.processes[procID]["kwargs"]["ismandatory"]
|
|
1365
|
+
and self.depot.processes[procID]["kwargs"]["vehicle_filter"](vehicle)
|
|
1364
1366
|
or not self.depot.processes[procID]["kwargs"]["ismandatory"]
|
|
1365
1367
|
and self.depot.processes[procID]["kwargs"]["vehicle_filter"](vehicle)
|
|
1366
1368
|
)
|
|
@@ -6,6 +6,7 @@ from abc import ABC, abstractmethod
|
|
|
6
6
|
from enum import auto, Enum
|
|
7
7
|
from warnings import warn
|
|
8
8
|
|
|
9
|
+
import numpy as np
|
|
9
10
|
import simpy
|
|
10
11
|
from eflips.helperFunctions import flexprint
|
|
11
12
|
from eflips.settings import globalConstants
|
|
@@ -672,7 +673,7 @@ class ChargeAbstract(VehicleProcess, ABC):
|
|
|
672
673
|
cancellable_for_dispatch=False,
|
|
673
674
|
efficiency=1,
|
|
674
675
|
*args,
|
|
675
|
-
**kwargs
|
|
676
|
+
**kwargs,
|
|
676
677
|
):
|
|
677
678
|
if required_resources is not None:
|
|
678
679
|
raise ValueError(
|
|
@@ -839,6 +840,7 @@ class Charge(ChargeAbstract):
|
|
|
839
840
|
|
|
840
841
|
@property
|
|
841
842
|
def power(self):
|
|
843
|
+
# TODO this doesn't consider vehicle charging power
|
|
842
844
|
return self.charging_interface.max_power
|
|
843
845
|
|
|
844
846
|
def _action(self, *args, **kwargs):
|
|
@@ -1239,6 +1241,8 @@ class ChargeEquationSteps(ChargeAbstract):
|
|
|
1239
1241
|
self.precision = precision
|
|
1240
1242
|
Charge.check_soc_target(soc_target, vehicle_filter)
|
|
1241
1243
|
self.soc_target = soc_target
|
|
1244
|
+
if "precision" in self.peq_params:
|
|
1245
|
+
self.precision = self.peq_params["precision"]
|
|
1242
1246
|
|
|
1243
1247
|
@property
|
|
1244
1248
|
def power(self):
|
|
@@ -1270,8 +1274,12 @@ class ChargeEquationSteps(ChargeAbstract):
|
|
|
1270
1274
|
)
|
|
1271
1275
|
soc_target_step = self.vehicle.battery.soc + soc_interval
|
|
1272
1276
|
amount = self.vehicle.battery.energy_real * soc_interval
|
|
1273
|
-
effective_power =
|
|
1274
|
-
|
|
1277
|
+
effective_power = (
|
|
1278
|
+
self.power
|
|
1279
|
+
* self.efficiency
|
|
1280
|
+
* self.vehicle.vehicle_type.charging_efficiency
|
|
1281
|
+
)
|
|
1282
|
+
self.dur = round(amount / effective_power * 3600, 12)
|
|
1275
1283
|
|
|
1276
1284
|
if self.dur == 0:
|
|
1277
1285
|
# amount is so small that dur is <1s. Reduce the precision
|
|
@@ -1294,6 +1302,7 @@ class ChargeEquationSteps(ChargeAbstract):
|
|
|
1294
1302
|
yield self.env.timeout(self.dur)
|
|
1295
1303
|
|
|
1296
1304
|
if soc_target_step < self.soc_target:
|
|
1305
|
+
# TODO why here we have energy higher than the step_soc_target?
|
|
1297
1306
|
# recalculate amount because update_battery may have been
|
|
1298
1307
|
# called in the meantime
|
|
1299
1308
|
amount_step = (
|
|
@@ -1428,6 +1437,33 @@ def exponential_power(vehicle, charging_interface, peq_params, *args, **kwargs):
|
|
|
1428
1437
|
)
|
|
1429
1438
|
|
|
1430
1439
|
|
|
1440
|
+
def charging_curve_power(vehicle, charging_interface, peq_params):
|
|
1441
|
+
"""
|
|
1442
|
+
Return power in kW for given peq_params in the format of dict[flaot, float]
|
|
1443
|
+
:param vehicle: SimpleVehicle
|
|
1444
|
+
:param charging_interface: DepotChargingInterface storing maximum power from charger
|
|
1445
|
+
:param peq_params: A dictionary with "soc" storing soc turning points and "power" for corresponding charging power
|
|
1446
|
+
:return: charging power in float for given SimpleVehicle
|
|
1447
|
+
"""
|
|
1448
|
+
|
|
1449
|
+
precision = peq_params.get("precision", 0.01)
|
|
1450
|
+
current_soc = vehicle.battery.soc
|
|
1451
|
+
p_max = charging_interface.max_power
|
|
1452
|
+
target_step_soc = min(vehicle.battery.soc + precision, vehicle.battery.soc_max)
|
|
1453
|
+
|
|
1454
|
+
power_current_soc = min(
|
|
1455
|
+
p_max, np.interp(current_soc, peq_params["soc"], peq_params["power"])
|
|
1456
|
+
)
|
|
1457
|
+
|
|
1458
|
+
power_target_soc = min(
|
|
1459
|
+
p_max, np.interp(target_step_soc, peq_params["soc"], peq_params["power"])
|
|
1460
|
+
)
|
|
1461
|
+
|
|
1462
|
+
current_power = min(power_current_soc, power_target_soc)
|
|
1463
|
+
|
|
1464
|
+
return float(current_power)
|
|
1465
|
+
|
|
1466
|
+
|
|
1431
1467
|
class Standby(VehicleProcess):
|
|
1432
1468
|
"""Process of mandatory waiting such as standby times."""
|
|
1433
1469
|
|
|
@@ -422,7 +422,7 @@ class RfdDiffDispatch:
|
|
|
422
422
|
diffs = []
|
|
423
423
|
for vehicle in area.vehicles:
|
|
424
424
|
etc = vehicle.dwd.etc_processes
|
|
425
|
-
if isinstance(etc, int):
|
|
425
|
+
if isinstance(etc, int) or isinstance(etc, float):
|
|
426
426
|
diff = etc - slot[0].env.now
|
|
427
427
|
diffs.append(diff)
|
|
428
428
|
# Convert EstimateValue to numerical rfddiff
|
|
@@ -36,7 +36,17 @@ class VehicleType:
|
|
|
36
36
|
|
|
37
37
|
"""
|
|
38
38
|
|
|
39
|
-
def __init__(
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
ID,
|
|
42
|
+
battery_capacity,
|
|
43
|
+
soc_min,
|
|
44
|
+
soc_max,
|
|
45
|
+
soc_init,
|
|
46
|
+
soh,
|
|
47
|
+
charging_efficiency=1.0,
|
|
48
|
+
CR=None,
|
|
49
|
+
):
|
|
40
50
|
self.ID = ID
|
|
41
51
|
self.battery_capacity = battery_capacity
|
|
42
52
|
self.soc_min = soc_min
|
|
@@ -45,6 +55,7 @@ class VehicleType:
|
|
|
45
55
|
self.soh = soh
|
|
46
56
|
self.CR = CR
|
|
47
57
|
self.group = None
|
|
58
|
+
self.charging_efficiency = charging_efficiency
|
|
48
59
|
|
|
49
60
|
self.count = {}
|
|
50
61
|
self.share = {}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "eflips-depot"
|
|
3
|
-
version = "4.
|
|
3
|
+
version = "4.15.9"
|
|
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",
|
|
@@ -23,7 +23,7 @@ pandas = "^2.2.0"
|
|
|
23
23
|
xlrd = "<=1.2.0"
|
|
24
24
|
scipy = "^1.14.0"
|
|
25
25
|
eflips-model = "^10.0.0"
|
|
26
|
-
eflips-opt = "^0.3.
|
|
26
|
+
eflips-opt = "^0.3.6"
|
|
27
27
|
|
|
28
28
|
|
|
29
29
|
[tool.pytest.ini_options]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/layout_opt/opt_tools/fitness_c_urfd.py
RENAMED
|
File without changes
|
{eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/layout_opt/opt_tools/fitness_util.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|