Created
September 5, 2025 15:27
-
-
Save aavogt/601da0964e613d5962b58943ac214b43 to your computer and use it in GitHub Desktop.
garbage pickup bin asymmetry
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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