Skip to content

Instantly share code, notes, and snippets.

@aavogt
Created September 5, 2025 15:27
Show Gist options
  • Select an option

  • Save aavogt/601da0964e613d5962b58943ac214b43 to your computer and use it in GitHub Desktop.

Select an option

Save aavogt/601da0964e613d5962b58943ac214b43 to your computer and use it in GitHub Desktop.
garbage pickup bin asymmetry
# diagram or explanation at http://aavogt.github.io/pickup.html
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.widgets import Slider, CheckButtons
import warnings
warnings.filterwarnings('ignore')
# Vehicle and fuel parameters
truck_mass = 15000 # kg (loaded garbage truck)
drag_coefficient = 0.8 # typical for box truck
frontal_area = 10 # m^2
air_density = 1.225 # kg/m^3
rolling_resistance = 0.015 # typical for truck tires
diesel_energy_density = 35.8e6 # J/L (diesel fuel)
engine_efficiency = 0.35 # 35% thermal efficiency (typical diesel)
transmission_efficiency = 0.90 # 90% mechanical efficiency
# Motion parameters
max_acceleration = 2.0 # m/s^2
max_deceleration = -3.0 # m/s^2
stop_time = 30 # seconds per bin pickup
# Distance conversions
ft_to_m = 0.3048
def calculate_fuel_consumption_detailed(distance, v_max, regen_efficiency=0.30):
"""Calculate fuel consumption based on energy balance"""
# Determine velocity profile
t_accel = v_max / max_acceleration
t_decel = v_max / abs(max_deceleration)
d_accel = 0.5 * max_acceleration * t_accel**2
d_decel = 0.5 * abs(max_deceleration) * t_decel**2
if d_accel + d_decel > distance:
# Triangular profile - can't reach v_max
ratio = max_acceleration / abs(max_deceleration)
t_accel = np.sqrt(2 * distance / (max_acceleration * (1 + ratio)))
t_decel = t_accel * ratio
v_peak = max_acceleration * t_accel
d_accel = distance * max_acceleration / (max_acceleration + abs(max_deceleration))
d_decel = distance - d_accel
d_cruise = 0
t_cruise = 0
else:
# Trapezoidal profile - reaches v_max
v_peak = v_max
d_cruise = distance - d_accel - d_decel
t_cruise = d_cruise / v_max
total_time = t_accel + t_cruise + t_decel
# Energy calculations
ke_gained = 0.5 * truck_mass * v_peak**2
ke_recovered = ke_gained * regen_efficiency
net_ke_cost = ke_gained - ke_recovered
# Work against rolling resistance
rolling_force = truck_mass * 9.81 * rolling_resistance
work_rolling = rolling_force * distance
# Work against aerodynamic drag
if t_accel > 0:
drag_work_accel = (0.5 * air_density * drag_coefficient * frontal_area *
max_acceleration**4 * t_accel**4 / 4)
else:
drag_work_accel = 0
if t_cruise > 0:
drag_force_cruise = 0.5 * air_density * drag_coefficient * frontal_area * v_peak**2
drag_work_cruise = drag_force_cruise * d_cruise
else:
drag_work_cruise = 0
if t_decel > 0:
drag_work_decel = (0.5 * air_density * drag_coefficient * frontal_area *
v_peak**4 * t_decel / 4)
else:
drag_work_decel = 0
total_drag_work = drag_work_accel + drag_work_cruise + drag_work_decel
total_work = net_ke_cost + work_rolling + total_drag_work
fuel_energy_needed = total_work / (engine_efficiency * transmission_efficiency)
fuel_volume = fuel_energy_needed / diesel_energy_density # Liters
return total_time, fuel_volume, v_peak
def route_performance(segments, v_max_values, regen_efficiency=0.30):
"""Calculate total route time and fuel for given max velocities per segment"""
total_time = 0
total_fuel = 0
for i, distance in enumerate(segments):
v_max = v_max_values[i] if isinstance(v_max_values, list) else v_max_values
time, fuel, _ = calculate_fuel_consumption_detailed(distance, v_max, regen_efficiency)
total_time += time + stop_time
total_fuel += fuel
return total_time / 60, total_fuel # time in minutes
def generate_performance_curves(segments, regen_efficiency=0.30):
"""Generate time-fuel trade-off curves by varying max velocities.
Accepts a single segment list or a list of segment-list variants;
if variants are provided, averages their performance to enforce symmetry."""
v_range = np.linspace(3, 25, 30) # Reduced points for faster computation
# Normalize to a list of variants
if isinstance(segments, (list, tuple)) and segments and isinstance(segments[0], (list, tuple, np.ndarray)):
variants = segments
else:
variants = [segments]
times = []
fuels = []
max_speeds = []
for v_max in v_range:
t_sum = 0.0
f_sum = 0.0
for segs in variants:
time_min, fuel_L = route_performance(segs, v_max, regen_efficiency)
t_sum += time_min
f_sum += fuel_L
times.append(t_sum / len(variants))
fuels.append(f_sum / len(variants))
max_speeds.append(v_max)
return np.array(times), np.array(fuels), np.array(max_speeds)
def create_segments(pitch_ft, asymmetry):
"""Create segment patterns based on pitch and asymmetry.
Returns equal segments and two asymmetric variants (starting with pair spacing vs pair gap)."""
pitch_m = pitch_ft * ft_to_m
# Equal spacing: 9 inter-bin segments for 10 pickups
equal_segments = [pitch_m] * 9
# Asymmetric spacing lengths
pair_gap = 2 * pitch_m * asymmetry
pair_spacing = 2 * pitch_m * (1 - asymmetry)
# Variant A: start with pair_spacing (counts: 5 spacings, 4 gaps)
asymmetric_A = []
for i in range(5): # 5 pairs
asymmetric_A.append(pair_spacing)
if i < 4:
asymmetric_A.append(pair_gap)
# Variant B: start with pair_gap (counts: 4 spacings, 5 gaps)
asymmetric_B = []
for i in range(5):
asymmetric_B.append(pair_gap)
if i < 4:
asymmetric_B.append(pair_spacing)
# Return both variants; downstream averages them to enforce symmetry
return equal_segments, [asymmetric_A, asymmetric_B]
# Create the interactive plot
fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, figsize=(16, 10))
plt.subplots_adjust(bottom=0.25)
# Initial parameters
initial_pitch = 50 # feet
initial_asymmetry = 0.8 # 0 = equal spacing, 1 = maximum asymmetry
initial_regen = True
# Create initial plots
equal_segments, asymmetric_segments = create_segments(initial_pitch, initial_asymmetry)
regen_eff = 0.30 if initial_regen else 0.0
equal_times, equal_fuels, equal_speeds = generate_performance_curves(equal_segments, regen_eff)
asym_times, asym_fuels, asym_speeds = generate_performance_curves(asymmetric_segments, regen_eff)
# Plot 1: Time-Fuel Trade-off
line1, = ax1.plot(equal_times, equal_fuels, 'b-', linewidth=2, label='Equal Spacing', alpha=0.7)
line2, = ax1.plot(asym_times, asym_fuels, 'r-', linewidth=2, label='Asymmetric Spacing', alpha=0.7)
ax1.set_xlabel('Travel Time (minutes)')
ax1.set_ylabel('Fuel Consumption (L)')
ax1.set_title('Time-Fuel Trade-off Curves')
ax1.grid(True, alpha=0.3)
ax1.legend()
# Plot 2: Time difference at equal fuel
fuel_min = max(min(equal_fuels), min(asym_fuels))
fuel_max = min(max(equal_fuels), max(asym_fuels))
fuel_levels = np.linspace(fuel_min, fuel_max, 15)
time_differences = []
for fuel_level in fuel_levels:
t1 = np.interp(fuel_level, equal_fuels, equal_times)
t2 = np.interp(fuel_level, asym_fuels, asym_times)
time_differences.append(t2 - t1)
line3, = ax2.plot(fuel_levels, time_differences, 'g-', linewidth=2, marker='o', markersize=3)
ax2.axhline(y=0, color='black', linestyle='--', alpha=0.5)
ax2.set_xlabel('Fuel Consumption (L)')
ax2.set_ylabel('Time Difference (Asym - Equal) [min]')
ax2.set_title('Time Penalty/Benefit at Equal Fuel')
ax2.grid(True, alpha=0.3)
# Plot 3: Fuel difference at equal time
time_min = max(min(equal_times), min(asym_times))
time_max = min(max(equal_times), max(asym_times))
time_levels = np.linspace(time_min, time_max, 15)
fuel_differences = []
for time_level in time_levels:
f1 = np.interp(time_level, equal_times[::-1], equal_fuels[::-1])
f2 = np.interp(time_level, asym_times[::-1], asym_fuels[::-1])
fuel_differences.append((f2 - f1) * 1000) # mL
line4, = ax3.plot(time_levels, fuel_differences, 'purple', linewidth=2, marker='s', markersize=3)
ax3.axhline(y=0, color='black', linestyle='--', alpha=0.5)
ax3.set_xlabel('Travel Time (minutes)')
ax3.set_ylabel('Fuel Difference (Asym - Equal) [mL]')
ax3.set_title('Fuel Penalty/Benefit at Equal Time')
ax3.grid(True, alpha=0.3)
# Plot 4: Speed requirements
line5, = ax4.plot(equal_times, equal_speeds * 2.237, 'b-', linewidth=2, label='Equal Spacing', alpha=0.7)
line6, = ax4.plot(asym_times, asym_speeds * 2.237, 'r-', linewidth=2, label='Asymmetric Spacing', alpha=0.7)
ax4.axhline(y=35, color='orange', linestyle='--', label='Typical Efficiency Speed')
ax4.set_xlabel('Travel Time (minutes)')
ax4.set_ylabel('Maximum Speed Allowed (mph)')
ax4.set_title('Speed Requirements vs Travel Time')
ax4.grid(True, alpha=0.3)
ax4.legend()
# Create sliders
ax_pitch = plt.axes([0.15, 0.15, 0.3, 0.03])
ax_asymmetry = plt.axes([0.55, 0.15, 0.3, 0.03])
ax_regen = plt.axes([0.15, 0.08, 0.15, 0.06])
slider_pitch = Slider(ax_pitch, 'Pitch (ft)', 20, 100, valinit=initial_pitch, valfmt='%.0f')
slider_asymmetry = Slider(ax_asymmetry, 'Asymmetry', 0.0, 0.95, valinit=initial_asymmetry, valfmt='%.2f')
check_regen = CheckButtons(ax_regen, ['Regen Braking'], [initial_regen])
# Status text
status_text = fig.text(0.15, 0.02, '', fontsize=10)
def update_plots(val=None):
"""Update all plots when sliders change"""
pitch = slider_pitch.val
asymmetry = slider_asymmetry.val
regen_enabled = check_regen.get_status()[0]
regen_eff = 0.30 if regen_enabled else 0.0
# Create new segments
equal_segments, asymmetric_segments = create_segments(pitch, asymmetry)
# Generate new curves
equal_times, equal_fuels, equal_speeds = generate_performance_curves(equal_segments, regen_eff)
asym_times, asym_fuels, asym_speeds = generate_performance_curves(asymmetric_segments, regen_eff)
# Update Plot 1
line1.set_data(equal_times, equal_fuels)
line2.set_data(asym_times, asym_fuels)
ax1.relim()
ax1.autoscale()
# Update Plot 2
fuel_min = max(min(equal_fuels), min(asym_fuels))
fuel_max = min(max(equal_fuels), max(asym_fuels))
if fuel_max > fuel_min:
fuel_levels = np.linspace(fuel_min, fuel_max, 15)
time_differences = []
for fuel_level in fuel_levels:
t1 = np.interp(fuel_level, equal_fuels, equal_times)
t2 = np.interp(fuel_level, asym_fuels, asym_times)
time_differences.append(t2 - t1)
line3.set_data(fuel_levels, time_differences)
ax2.relim()
ax2.autoscale()
# Update Plot 3
time_min = max(min(equal_times), min(asym_times))
time_max = min(max(equal_times), max(asym_times))
if time_max > time_min:
time_levels = np.linspace(time_min, time_max, 15)
fuel_differences = []
for time_level in time_levels:
f1 = np.interp(time_level, equal_times[::-1], equal_fuels[::-1])
f2 = np.interp(time_level, asym_times[::-1], asym_fuels[::-1])
fuel_differences.append((f2 - f1) * 1000)
line4.set_data(time_levels, fuel_differences)
ax3.relim()
ax3.autoscale()
# Update Plot 4
line5.set_data(equal_times, equal_speeds * 2.237)
line6.set_data(asym_times, asym_speeds * 2.237)
ax4.relim()
ax4.autoscale()
# Update status
pair_gap_ft = 2 * pitch * asymmetry
pair_spacing_ft = 2 * pitch * (1 - asymmetry)
status_text.set_text(f'Current: Pitch={pitch:.0f}ft, Asymmetry={asymmetry:.2f} → '
f'Gap={pair_gap_ft:.1f}ft, Pair={pair_spacing_ft:.1f}ft, '
f'Regen={"On" if regen_enabled else "Off"}')
plt.draw()
# Connect sliders to update function
slider_pitch.on_changed(update_plots)
slider_asymmetry.on_changed(update_plots)
check_regen.on_clicked(update_plots)
# Initial status update
update_plots()
plt.show()
print("=== INTERACTIVE SPACING ANALYSIS ===")
print("\nControls:")
print("• Pitch Slider: Base spacing between bins (20-100 ft)")
print("• Asymmetry Slider: 0.0 = equal spacing, 0.95 = maximum asymmetry")
print("• Regen Braking: Toggle regenerative braking on/off")
print("\nFormulas:")
print("• Equal spacing: pitch (constant)")
print("• Asymmetric spacing:")
print(" - Gap between pairs: 2 × pitch × asymmetry")
print(" - Spacing within pair: 2 × pitch × (1 - asymmetry)")
print("\nInterpretation:")
print("• Lower-left in Time-Fuel plot is better")
print("• Negative time difference = asymmetric is faster")
print("• Negative fuel difference = asymmetric uses less fuel")
print("• Watch how regenerative braking affects the trade-offs!")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment