Skip to content

Commit

Permalink
use trip instance when adding trip to rotation
Browse files Browse the repository at this point in the history
Instead of generating trip from dict.
Move rotation charge type setting/checking to schedule.
Change optimization.recombination to use trip instances instead of dictionaries.
Also remove optimizer_util.get_buffer_time, use util.get_buffer_time instead.
  • Loading branch information
stefan.schirmeister committed Dec 5, 2024
1 parent 866b8b3 commit bf97d89
Show file tree
Hide file tree
Showing 7 changed files with 114 additions and 141 deletions.
130 changes: 62 additions & 68 deletions simba/optimization.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,107 +276,101 @@ def recombination(schedule, args, trips, depot_trips):
rotation = None # new rotation is generated during first trip
rot_name = f"{rot_id}_r" # new rotation name, if it needs to be split a counter is added
rot_counter = 0 # into how many rotations has the original already been split?
soc = None # remaining soc of new rotation
last_depot_trip = None # keep track of last possible depot trip
# find upper limit of single rotation:
# consumption calculates energy difference, so get vehicle capacity
vt = original_rotation.vehicle_type
ct = original_rotation.charging_type
v_info = schedule.vehicle_types[vt][ct]
allowed_consumption = args.desired_soc_deps * v_info["capacity"]

while trip_list:
# probe next trip
trip = trip_list.pop(0)
if rotation is None:
# first trip of new rotation: generate new rotation
rotation = Rotation(
id=rot_name,
vehicle_type=trip.rotation.vehicle_type,
schedule=schedule,
)
charging_type = trip.rotation.charging_type
rotation.set_charging_type(charging_type)
rotation = Rotation(id=rot_name, vehicle_type=vt, schedule=schedule)
rotation.allow_opp_charging_for_oppb = original_rotation.allow_opp_charging_for_oppb
rotation.vehicle_id = original_rotation.vehicle_id
rotation.set_charging_type(ct)

# begin rotation in depot: add initial trip
depot_trip = generate_depot_trip_data_dict(
trip.departure_name, depot_name, depot_trips,
args.default_depot_distance, args.default_mean_speed)
height_difference = schedule.get_height_difference(
depot_name, trip.departure_name)
rotation.add_trip({
"departure_time": trip.departure_time - depot_trip["travel_time"],
"departure_name": depot_name,
"arrival_time": trip.departure_time,
"arrival_name": trip.departure_name,
"distance": depot_trip["distance"],
"line": trip.line,
"charging_type": charging_type,
"temperature": trip.temperature,
"height_difference": height_difference,
"level_of_loading": 0, # no passengers from depot
"mean_speed": depot_trip["mean_speed"],
"station_data": schedule.station_data,
})

# calculate consumption for initial trip
soc = args.desired_soc_deps # vehicle leaves depot with this soc
schedule.calculate_rotation_consumption(rotation)
soc += rotation.trips[0].delta_soc # new soc after initial trip
trip_dict = generate_depot_trip_data_dict(
trip.departure_name, depot_name, depot_trips,
args.default_depot_distance, args.default_mean_speed)
initial_trip = Trip(
rotation=rotation,
departure_time=trip.departure_time - trip_dict["travel_time"],
departure_name=depot_name,
arrival_time=trip.departure_time,
arrival_name=trip.departure_name,
distance=trip_dict["distance"],
line=trip.line,
temperature=trip.temperature,
height_difference=height_difference,
level_of_loading=0, # no passengers from depot
mean_speed=trip_dict["mean_speed"],
)
rotation.add_trip(initial_trip)

if rot_counter > 0:
logging.info(
f'Rotation {rot_id}: New Einsetzfahrt '
f'{trip.departure_time - depot_trip["travel_time"]} '
f'{trip.departure_time - trip_dict["travel_time"]} '
f'{depot_name} - {trip.departure_name} '
f'({depot_trip["travel_time"].total_seconds() / 60} min, '
f'{depot_trip["distance"] / 1000} km)')
f'({trip_dict["travel_time"].total_seconds() / 60} min, '
f'{trip_dict["distance"] / 1000} km)')

# can trip be completed while having enough energy to reach depot again?
# probe return trip
height_difference = schedule.get_height_difference(trip.arrival_name, depot_name)
depot_trip = generate_depot_trip_data_dict(
trip.arrival_name, depot_name, depot_trips,
args.default_depot_distance, args.default_mean_speed)
height_difference = schedule.get_height_difference(trip.arrival_name,
depot_name)
depot_trip = {
"departure_time": trip.arrival_time,
"departure_name": trip.arrival_name,
"arrival_time": trip.arrival_time + depot_trip["travel_time"],
"arrival_name": depot_name,
"distance": depot_trip["distance"],
"line": trip.line,
"charging_type": charging_type,
"temperature": trip.temperature,
"height_difference": height_difference,
"level_of_loading": 0,
"mean_speed": depot_trip["mean_speed"],
"station_data": schedule.station_data,
}
# rotation.add_trip needs dict, but consumption calculation is better done on Trip obj:
# create temporary depot trip object for consumption calculation
tmp_trip = Trip(rotation, **depot_trip)
# Sets tmp_trip.delta_soc and tmp_trip.consumption
schedule.calculate_trip_consumption(tmp_trip)
if soc >= -(trip.delta_soc + tmp_trip.delta_soc):
# next trip is possible: add trip, use info from original trip
trip_dict = vars(trip)
del trip_dict["rotation"]
trip_dict["charging_type"] = charging_type
rotation.add_trip(trip_dict)
depot_trip = Trip(
rotation=rotation,
departure_time=trip.arrival_time,
departure_name=trip.arrival_name,
arrival_time=trip.arrival_time + depot_trip["travel_time"],
arrival_name=depot_name,
distance=depot_trip["distance"],
line=trip.line,
temperature=trip.temperature,
height_difference=height_difference,
level_of_loading=0,
mean_speed=depot_trip["mean_speed"],
)

# check: can next trip be done with return to depot?
# create temp rotation (clone) to facilitate rollback
tmp_rot = deepcopy(rotation)
tmp_rot.add_trip(trip)
tmp_rot.add_trip(depot_trip)
schedule.calculate_rotation_consumption(tmp_rot)
if tmp_rot.consumption <= allowed_consumption:
# next trip is possible: add trip to rotation
rotation.add_trip(trip)
last_depot_trip = depot_trip
soc += trip.delta_soc
else:
# next trip is not safely possible
if last_depot_trip is None:
# this was initial trip: discard rotation
logging.info(f"Trip of line {trip.line} between {trip.departure_name} and "
f"{trip.arrival_name} too long with added depot trips, discarded")
logging.info(
f"Trip of line {trip.line} between {trip.departure_name} and "
f"{trip.arrival_name} too long with added depot trips, discarded")
else:
# not initial trip: add last possible depot trip
rotation.add_trip(last_depot_trip)
assert last_depot_trip["arrival_name"] == depot_name
travel_time = last_depot_trip["arrival_time"]-last_depot_trip["departure_time"]
assert last_depot_trip.arrival_name == depot_name
travel_time = last_depot_trip.arrival_time-last_depot_trip.departure_time
logging.info(
f'Rotation {rot_id}: New Aussetzfahrt '
f'{last_depot_trip["departure_time"]} '
f'{last_depot_trip["departure_name"]} - {last_depot_trip["arrival_name"]} '
f'{last_depot_trip.departure_time} '
f'{last_depot_trip.departure_name} - {last_depot_trip.arrival_name} '
f'({travel_time.total_seconds() / 60} min, '
f'{last_depot_trip["distance"] / 1000} km)')
f'{last_depot_trip.distance / 1000} km)')
schedule.rotations[rotation.id] = rotation
last_depot_trip = None
rot_counter += 1
Expand Down
19 changes: 4 additions & 15 deletions simba/optimizer_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
if typing.TYPE_CHECKING:
from simba.station_optimizer import StationOptimizer

from simba.util import get_buffer_time as get_buffer_time_util
from simba.util import get_buffer_time
from spice_ev.report import generate_soc_timeseries


Expand Down Expand Up @@ -229,7 +229,7 @@ def get_charging_time(trip1, trip2, args):
:rtype: float
"""
standing_time_min = (trip2.departure_time - trip1.arrival_time) / timedelta(minutes=1)
buffer_time = (get_buffer_time(trip1, args.default_buffer_time_opps) / timedelta(minutes=1))
buffer_time = get_buffer_time(trip1, args.default_buffer_time_opps)
standing_time_min -= buffer_time

if args.min_charging_time > standing_time_min:
Expand All @@ -249,19 +249,8 @@ def get_charging_start(trip1, args):
:return: First possible charging time as datetime object
"""
buffer_time = get_buffer_time(trip1, args.default_buffer_time_opps)
return trip1.arrival_time+buffer_time


def get_buffer_time(trip, default_buffer_time_opps):
""" Return the buffer time as timedelta object.
:param trip: trip to be checked
:type trip: simba.trip.Trip
:param default_buffer_time_opps: the default buffer time at opps charging stations
:return: buffer time
:rtype: datetime.timedelta
"""
return timedelta(minutes=get_buffer_time_util(trip, default_buffer_time_opps))
buffer_timedelta = timedelta(minutes=buffer_time)
return trip1.arrival_time + buffer_timedelta


def get_index_by_time(scenario, search_time):
Expand Down
51 changes: 15 additions & 36 deletions simba/rotation.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import datetime
import logging

from simba.trip import Trip


class Rotation:

Expand Down Expand Up @@ -32,53 +30,34 @@ def __init__(self, id, vehicle_type, schedule) -> None:
def add_trip(self, trip):
""" Create a trip object and append to rotations trip set.
:param trip: Information on trip to be added to rotation
:type trip: dict
:raises Exception: if charging type of trip and rotation differ
:param trip: trip to be added to rotation
"""
new_trip = Trip(self, **trip)

self.distance += new_trip.distance
if new_trip.line:
self.lines.add(new_trip.line)
self.distance += trip.distance
if trip.line:
self.lines.add(trip.line)

if self.departure_time is None and self.arrival_time is None:
# first trip added
self.departure_time = new_trip.departure_time
self.departure_name = new_trip.departure_name
self.arrival_time = new_trip.arrival_time
self.arrival_name = new_trip.arrival_name
self.departure_time = trip.departure_time
self.departure_name = trip.departure_name
self.arrival_time = trip.arrival_time
self.arrival_name = trip.arrival_name
else:
if self.departure_time > new_trip.departure_time:
if self.departure_time > trip.departure_time:
# first of rotation found (for now)
self.departure_time = new_trip.departure_time
self.departure_name = new_trip.departure_name
self.departure_time = trip.departure_time
self.departure_name = trip.departure_name
# '<=' instead of '<' since last trip of rotation has no duration
# in the sample data. The trips are however chronologically
# sorted which is why this approach works for sample data.
# Will also work if one only relies on timestamps!
elif self.arrival_time <= new_trip.arrival_time:
elif self.arrival_time <= trip.arrival_time:
# last trip of rotation (for now)
self.arrival_time = new_trip.arrival_time
self.arrival_name = new_trip.arrival_name
self.arrival_time = trip.arrival_time
self.arrival_name = trip.arrival_name

# set charging type if given
charging_type = trip.get('charging_type')
self.trips.append(new_trip)
if charging_type in ['depb', 'oppb']:
assert self.schedule.vehicle_types.get(
self.vehicle_type, {}).get(charging_type) is not None, (
f"The required vehicle type {self.vehicle_type}({charging_type}) "
"is not given in the vehicle_types.json file.")
if self.charging_type is None:
# set CT for whole rotation
self.set_charging_type(charging_type)
elif self.charging_type == charging_type:
# same CT as other trips: just add trip consumption
self.consumption += self.schedule.calculate_trip_consumption(new_trip)
else:
# different CT than rotation: error
raise Exception(f"Two trips of rotation {self.id} have distinct charging types")
self.trips.append(trip)

def set_charging_type(self, ct):
""" Change charging type of either all or specified rotations.
Expand Down
14 changes: 13 additions & 1 deletion simba/schedule.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,18 @@ def from_datacontainer(cls, data: DataContainer, args):
rotation_id: Rotation(id=rotation_id,
vehicle_type=trip['vehicle_type'],
schedule=schedule)})
schedule.rotations[rotation_id].add_trip(trip)
rotation = schedule.rotations[rotation_id]
charging_type = trip.get("charging_type")
trip = Trip(rotation, **trip)
rotation.add_trip(trip)
if charging_type in ['depb', 'oppb']:
if rotation.charging_type is None:
# set CT for whole rotation
rotation.set_charging_type(charging_type)
elif rotation.charging_type != charging_type:
# different CT than rotation: error
raise Exception(
f"Two trips of rotation {rotation.id} have differing charging types")

# Set charging type for all rotations without explicitly specified charging type.
# Charging type may have been set previously if a trip had specified a charging type.
Expand All @@ -167,6 +178,7 @@ def from_datacontainer(cls, data: DataContainer, args):
logging.warning("Option skip_inconsistent_rotations ignored, "
"as check_rotation_consistency is not set to 'true'")

schedule.calculate_consumption()
return schedule

@classmethod
Expand Down
16 changes: 7 additions & 9 deletions simba/station_optimizer.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
""" Optimizer class which implements the optimizer object and methods needed"""

import logging
import pickle
from copy import deepcopy, copy
from datetime import datetime, timedelta
import logging
import numpy as np
from pathlib import Path
import pickle
from typing import Iterable

import numpy as np

import simba.optimizer_util as opt_util
from spice_ev import scenario
from simba import rotation, schedule
from simba import rotation, schedule, util


class StationOptimizer:
Expand Down Expand Up @@ -531,10 +530,9 @@ def timeseries_calc(self, electrified_stations: set, rotations=None) -> dict:

# Add the charge as linear interpolation during the charge time, but only start
# after the buffer time
buffer_idx = (int(opt_util.get_buffer_time(
trip,
self.args.default_buffer_time_opps) / self.scenario.interval))
delta_idx = int(timedelta(minutes=standing_time_min)/self.scenario.interval + 1)
buffer_minutes = util.get_buffer_time(trip, self.args.default_buffer_time_opps)
buffer_idx = int(timedelta(minutes=buffer_minutes) / self.scenario.interval)
delta_idx = int(timedelta(minutes=standing_time_min) / self.scenario.interval + 1)
soc[idx + buffer_idx:idx + buffer_idx + delta_idx] += np.linspace(0, d_soc,
delta_idx)
# Keep track of the last SoC as starting point for the next trip
Expand Down
8 changes: 4 additions & 4 deletions tests/test_optimization.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,14 +112,14 @@ def test_recombination(self):
new_rot_name = f"{rot_id}_r_{counter}"
assert len(trip_list) == trip_counter

# make all trips except one easily possible
# make all trips except one easily possible (short distance)
schedule = original_schedule
trips = deepcopy(original_trips)
for trips_ in trips.values():
for t in trips_:
t.delta_soc = 0.0000001
# make one trip impossible
trips["1"][0].delta_soc = -2
t.distance = 1
# make one trip impossible (great distance)
trips["1"][0].distance = 1e9

recombined_schedule = optimization.recombination(schedule, args, trips, depot_trips)
# add Ein/Aussetzfahrt, subtract one impossible trip
Expand Down
Loading

0 comments on commit bf97d89

Please sign in to comment.