#!/usr/bin/python2
import math
# Customize me!
all_electric = False # Whether or not this is a Typ-1e (True) or a Typ-1h (false)
initial_charge = None # If you'd rather specify the initial charge by a number of watt-hours, enter it here. Otherwise, set it to "None".
initial_charge_percent = 1.00 # If you'd rather specify by a %, enter it here (50% = 0.50, 100% = 1.00, etc). If not, set this to "None".
target_speed = 80.0 # Miles per hour
vehicle_weight = 2000.0 # Pounds, loaded
route = "LA Reservoir to Grapevine, CA" # Name of route to take; see "routes" list below.
# Vehicle constants -- adjust as more info comes out
min_generator_charge = 1000.0 # Watt hours; generator starts up if charge drops below here
max_generator_charge = 1200.0 # Watt hours; generator shuts off if charge increases beyond here
max_typ1h_capacity = 6000.0 # Watt hours; Typ-1h battery pack can't hold more than this.
max_typ1e_capacity = 10000.0 # Watt hours; Typ-1e battery pack can't hold more than this.
regen_efficiency = 0.8 # Decimal percentage (0.8 = 80%)
generator_power = 12000.0 # Watts
# Vehicle power consumption curve
consumption_at_speed = [ # mph, Wh/mi
(55.0, 83.333),
(80.0, 142.857)
]
# Automatic speed limitation (turtling) at various SOCs
max_speed = [ #Wh, mph
(30.0, 45.0),
(60.0, 50.0),
(90.0, 55.0),
(120.0, 60.0),
(180.0, 65.0),
(880.0, 70.0),
(910.0, 75.0),
(940.0, 80.0),
(970.0, 85.0),
(1000.0, 90.0)
]
# Pre-defined routes
routes = { # Format: lat, lon, feet_alt
"LA Reservoir to Grapevine, CA": [
(34.2348, -118.4119, 865.0),
(34.2548, -118.4332, 944.0),
(34.2774, -118.4539, 1016.0),
(34.3003, -118.4771, 1173.0),
(34.3141, -118.4888, 1268.0),
(34.3269, -118.5032, 1366.0),
(34.3411, -118.5206, 1782.0),
(34.3494, -118.5399, 1500.0),
(34.3576, -118.5528, 1402.0),
(34.3945, -118.5712, 1399.0),
(34.4311, -118.5883, 1057.0),
(34.4604, -118.6152, 1059.0),
(34.4898, -118.6191, 1174.0),
(34.5155, -118.6353, 1456.0),
(34.5365, -118.6470, 1895.0),
(34.5574, -118.6759, 2290.0),
(34.5712, -118.6892, 2591.0),
(34.5897, -118.7115, 2841.0),
(34.6088, -118.7117, 2806.0),
(34.6313, -118.7271, 2663.0),
(34.6419, -118.7498, 2666.0),
(34.6699, -118.7651, 2774.0),
(34.6833, -118.7823, 2815.0),
(34.7040, -118.7943, 2864.0),
(34.7296, -118.7985, 3017.0),
(34.7594, -118.7959, 3252.0),
(34.7862, -118.8264, 3611.0),
(34.7957, -118.8569, 3882.0),
(34.8070, -118.8821, 4050.0),
(34.8476, -118.8688, 3422.0),
(34.8749, -118.8923, 3177.0),
(34.9038, -118.9232, 2351.0),
(34.9421, -118.9313, 1495.0)
],
"Grapevine, CA to LA Reservoir": [ # lat, lon, feet_alt
(34.9421, -118.9313, 1495.0),
(34.9038, -118.9232, 2351.0),
(34.8749, -118.8923, 3177.0),
(34.8476, -118.8688, 3422.0),
(34.8070, -118.8821, 4050.0),
(34.7957, -118.8569, 3882.0),
(34.7862, -118.8264, 3611.0),
(34.7594, -118.7959, 3252.0),
(34.7296, -118.7985, 3017.0),
(34.7040, -118.7943, 2864.0),
(34.6833, -118.7823, 2815.0),
(34.6699, -118.7651, 2774.0),
(34.6419, -118.7498, 2666.0),
(34.6313, -118.7271, 2663.0),
(34.6088, -118.7117, 2806.0),
(34.5897, -118.7115, 2841.0),
(34.5712, -118.6892, 2591.0),
(34.5574, -118.6759, 2290.0),
(34.5365, -118.6470, 1895.0),
(34.5155, -118.6353, 1456.0),
(34.4898, -118.6191, 1174.0),
(34.4604, -118.6152, 1059.0),
(34.4311, -118.5883, 1057.0),
(34.3945, -118.5712, 1399.0),
(34.3576, -118.5528, 1402.0),
(34.3494, -118.5399, 1500.0),
(34.3411, -118.5206, 1782.0),
(34.3269, -118.5032, 1366.0),
(34.3141, -118.4888, 1268.0),
(34.3003, -118.4771, 1173.0),
(34.2774, -118.4539, 1016.0),
(34.2548, -118.4332, 944.0),
(34.2348, -118.4119, 865.0)
]
}
max_capacity = max_typ1h_capacity
# If electric...
if all_electric == True:
generator_power = 0.0
max_capacity = max_typ1e_capacity
max_speed = [ (0.0, 90.0) ]
# If they want to set initial charge by a percentage
if initial_charge_percent != None:
initial_charge = max_capacity * initial_charge_percent
# Other constants
miles_to_meters_multiplier = 1609.334
feet_to_meters_multiplier = 0.3048
hours_to_seconds_multiplier = 3600.0
pounds_to_kilograms_multiplier = 0.45359237
watt_hours_to_joules_multiplier = 3600.0
earth_gravity_constant = 9.81
# Calculated constants
vehicle_mass = vehicle_weight * pounds_to_kilograms_multiplier
# Functions
def WhPerMi_at_speed(mph): # Slope-intercept
last_speed = 0
last_wh = 0
for entry in consumption_at_speed:
speed = entry[0]
wh = entry[1]
if mph < speed:
percent = (mph - last_speed) / (speed - last_speed)
return percent * wh + (1.0 - percent) * last_wh
last_speed = speed
last_wh = wh
#Fallthrough
last_speed = consumption_at_speed[-2][0]
last_wh = consumption_at_speed[-2][1]
speed = consumption_at_speed[-1][0]
wh = consumption_at_speed[-1][1]
percent = (mph - last_speed) / (speed - last_speed)
return percent * wh + (1.0 - percent) * last_wh
def max_speed_at_SOC(cur_charge):
last_charge = 0
last_mph = 0
for entry in max_speed:
charge = entry[0]
mph = entry[1]
if cur_charge < charge:
percent = (cur_charge - last_charge) / (charge - last_charge)
return percent * mph + (1.0 - percent) * last_mph
last_charge = charge
last_mph = mph
#Fallthrough
return max_speed[-1][1]
def coords_to_ground_distance(lat1, lon1, lat2, lon2):
R = 6371000.0
dLat = math.radians(lat2 - lat1)
dLon = math.radians(lon2 - lon1)
a = math.sin(dLat / 2) * math.sin(dLat / 2) + \
math.cos(math.radians(lat1)) * math.cos(math.radians(lat2)) * \
math.sin(dLon / 2) * math.sin(dLon / 2)
c = 2 * math.atan2(math.sqrt(a), math.sqrt(1-a))
d = R * c
return d
def energy_to_climb(meters):
return meters * vehicle_mass * earth_gravity_constant / watt_hours_to_joules_multiplier
def translate_route(route):
translated_route = []
last_lat = route[0][0]
last_lon = route[0][1]
cumulative_dist = 0
for entry in route:
lat = entry[0]
lon = entry[1]
alt = entry[2] * feet_to_meters_multiplier
dist = coords_to_ground_distance(last_lat, last_lon, lat, lon)
cumulative_dist += dist
translated_route.append( (cumulative_dist, alt) )
last_lat = lat
last_lon = lon
return translated_route
def simulate(route, starting_charge, target_speed, feet_per_cycle):
route = translate_route(route)
meters_per_cycle = feet_per_cycle * feet_to_meters_multiplier
dist = 0
generator_on = False
charge = starting_charge
last_alt = route[0][1]
route_last = 0
route_next = 1
while True:
dist += meters_per_cycle
if dist > route[-1][0]:
break
if dist > route[route_next][0]:
route_last += 1
route_next += 1
percent = (dist - route[route_last][0]) / (route[route_next][0] - route[route_last][0])
alt = percent * route[route_next][1] + (1.0 - percent) * route[route_last][1]
alt_change = alt - last_alt
last_alt = alt
#Try to find a speed that won't go negative in terms of charge.
speed = target_speed
max_speed = max_speed_at_SOC(charge)
if speed > max_speed:
speed = max_speed
while True:
temp_charge = charge
if alt_change >= 0:
temp_charge -= energy_to_climb(alt_change)
else:
temp_charge -= energy_to_climb(alt_change) * regen_efficiency
travel_dist = math.sqrt(alt_change * alt_change + meters_per_cycle * meters_per_cycle)
temp_charge -= (travel_dist / miles_to_meters_multiplier) * WhPerMi_at_speed(speed)
if temp_charge < min_generator_charge:
generator_on = True
elif temp_charge > max_generator_charge:
generator_on = False
if generator_on:
hours_passed = (travel_dist / miles_to_meters_multiplier) / speed
temp_charge += generator_power * hours_passed
if temp_charge > max_capacity:
temp_charge = max_capacity
if temp_charge < 0:
speed -= 1.0 #If we go below zero, try again, a touch slower.
continue
else:
break
charge = temp_charge
if all_electric == True:
print "Mile %.3f: Charge=%d Wh, Speed=%d mph" % (dist / miles_to_meters_multiplier, charge, speed)
else:
print "Mile %.3f: Charge=%d Wh, Speed=%d mph; Generator running: %s" % (dist / miles_to_meters_multiplier, charge, speed, generator_on)
# Runme
simulate(routes[route], initial_charge, target_speed, 100.0)