""" Lambda Time Cost Analysis for Moon Colony Logistics This module introduces the time opportunity cost (λ) to find the optimal operating point on the Energy-Time trade-off curve. Objective Function: J = E_total + λ × T where: - E_total: Total energy consumption (PJ) - T: Construction timeline (years) - λ: Time opportunity cost (PJ/year) The optimal point satisfies: dE/dT = -λ (marginal energy saving = time cost) """ import numpy as np import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt from matplotlib import rcParams import pandas as pd from typing import Dict, List, Tuple, Optional from dataclasses import dataclass # Font settings rcParams['font.sans-serif'] = ['Arial Unicode MS', 'DejaVu Sans', 'SimHei'] rcParams['axes.unicode_minus'] = False # ============== Physical Constants ============== G0 = 9.81 # m/s² OMEGA_EARTH = 7.27e-5 # rad/s R_EARTH = 6.371e6 # m # Mission parameters TOTAL_PAYLOAD = 100e6 # 100 million metric tons # Space Elevator parameters NUM_ELEVATORS = 3 ELEVATOR_CAPACITY_PER_YEAR = 179000 # metric tons per elevator per year TOTAL_ELEVATOR_CAPACITY = NUM_ELEVATORS * ELEVATOR_CAPACITY_PER_YEAR # 537,000 tons/year ELEVATOR_SPECIFIC_ENERGY = 157.2e9 # J per metric ton (157.2 MJ/kg × 1000) # Rocket parameters - LOX/CH4 (Raptor-class) PAYLOAD_PER_LAUNCH = 125 # metric tons per launch ISP = 363 # Specific impulse (s) SPECIFIC_FUEL_ENERGY = 11.9e6 # J/kg ALPHA = 0.10 # Structural coefficient NUM_STAGES = 3 DELTA_V_BASE = 13300 # m/s (LEO + TLI + LOI) # ============== Launch Site Definition ============== @dataclass class LaunchSite: name: str short_name: str latitude: float max_launches_per_day: int = 1 @property def abs_latitude(self) -> float: return abs(self.latitude) @property def delta_v_loss(self) -> float: v_equator = OMEGA_EARTH * R_EARTH v_site = OMEGA_EARTH * R_EARTH * np.cos(np.radians(self.abs_latitude)) return v_equator - v_site @property def total_delta_v(self) -> float: return DELTA_V_BASE + self.delta_v_loss LAUNCH_SITES = sorted([ LaunchSite("Kourou (French Guiana)", "Kourou", 5.2), LaunchSite("Satish Dhawan (India)", "SDSC", 13.7), LaunchSite("Boca Chica (Texas)", "Texas", 26.0), LaunchSite("Cape Canaveral (Florida)", "Florida", 28.5), LaunchSite("Vandenberg (California)", "California", 34.7), LaunchSite("Wallops (Virginia)", "Virginia", 37.8), LaunchSite("Taiyuan (China)", "Taiyuan", 38.8), LaunchSite("Mahia (New Zealand)", "Mahia", 39.3), LaunchSite("Baikonur (Kazakhstan)", "Baikonur", 45.6), LaunchSite("Kodiak (Alaska)", "Alaska", 57.4), ], key=lambda x: x.abs_latitude) # ============== Core Calculation Functions ============== def fuel_ratio_multistage(delta_v: float) -> float: """Multi-stage rocket fuel/payload ratio""" ve = ISP * G0 delta_v_per_stage = delta_v / NUM_STAGES R_stage = np.exp(delta_v_per_stage / ve) denominator = 1 - ALPHA * (R_stage - 1) if denominator <= 0: return np.inf k_stage = (R_stage - 1) / denominator total_fuel_ratio = 0 remaining_ratio = 1.0 for _ in range(NUM_STAGES): fuel_this_stage = remaining_ratio * k_stage total_fuel_ratio += fuel_this_stage remaining_ratio *= (1 + k_stage * (1 + ALPHA)) return total_fuel_ratio def rocket_energy_per_ton(site: LaunchSite) -> float: """Energy consumption per ton of payload for rocket launch (J/ton)""" k = fuel_ratio_multistage(site.total_delta_v) fuel_per_ton = k * 1000 # kg fuel per metric ton payload return fuel_per_ton * SPECIFIC_FUEL_ENERGY def calculate_scenario(completion_years: float) -> Optional[Dict]: """ Calculate optimal scenario for given completion timeline (elevator priority + low-latitude rockets) """ # Space elevator transport elevator_payload = min(TOTAL_ELEVATOR_CAPACITY * completion_years, TOTAL_PAYLOAD) elevator_energy = elevator_payload * ELEVATOR_SPECIFIC_ENERGY # Remaining payload for rockets remaining_payload = TOTAL_PAYLOAD - elevator_payload if remaining_payload <= 0: return { 'years': completion_years, 'elevator_payload': elevator_payload, 'rocket_payload': 0, 'elevator_energy_PJ': elevator_energy / 1e15, 'rocket_energy_PJ': 0, 'total_energy_PJ': elevator_energy / 1e15, 'rocket_launches': 0, 'sites_used': 0, 'elevator_fraction': 1.0, } # Rocket launches needed rocket_launches_needed = int(np.ceil(remaining_payload / PAYLOAD_PER_LAUNCH)) # Allocate by latitude priority days_available = completion_years * 365 max_launches_per_site = int(days_available) # Check feasibility total_rocket_capacity = len(LAUNCH_SITES) * max_launches_per_site * PAYLOAD_PER_LAUNCH if remaining_payload > total_rocket_capacity: return None rocket_energy = 0 sites_used = 0 remaining_launches = rocket_launches_needed for site in LAUNCH_SITES: if remaining_launches <= 0: break allocated = min(remaining_launches, max_launches_per_site) rocket_energy += rocket_energy_per_ton(site) * PAYLOAD_PER_LAUNCH * allocated remaining_launches -= allocated if allocated > 0: sites_used += 1 rocket_payload = rocket_launches_needed * PAYLOAD_PER_LAUNCH total_energy = elevator_energy + rocket_energy return { 'years': completion_years, 'elevator_payload': elevator_payload, 'rocket_payload': rocket_payload, 'elevator_energy_PJ': elevator_energy / 1e15, 'rocket_energy_PJ': rocket_energy / 1e15, 'total_energy_PJ': total_energy / 1e15, 'rocket_launches': rocket_launches_needed, 'sites_used': sites_used, 'elevator_fraction': elevator_payload / TOTAL_PAYLOAD, } # ============== Generate Trade-off Curve ============== def generate_tradeoff_curve( year_min: float = 100, year_max: float = 250, num_points: int = 500 ) -> pd.DataFrame: """Generate Energy-Time trade-off curve data""" years_range = np.linspace(year_min, year_max, num_points) results = [] for years in years_range: scenario = calculate_scenario(years) if scenario is not None: results.append({ 'years': years, 'energy_PJ': scenario['total_energy_PJ'], 'elevator_fraction': scenario['elevator_fraction'], 'sites_used': scenario['sites_used'], 'rocket_launches': scenario['rocket_launches'], }) return pd.DataFrame(results) # ============== Lambda Cost Analysis ============== def calculate_total_cost(df: pd.DataFrame, lambda_cost: float) -> np.ndarray: """ Calculate total cost J = E + λ × T Args: df: Trade-off curve data lambda_cost: Time opportunity cost (PJ/year) Returns: Total cost array """ return df['energy_PJ'].values + lambda_cost * df['years'].values def find_optimal_point(df: pd.DataFrame, lambda_cost: float) -> Dict: """ Find optimal point that minimizes J = E + λ × T Args: df: Trade-off curve data lambda_cost: Time opportunity cost (PJ/year) Returns: Optimal point information """ total_cost = calculate_total_cost(df, lambda_cost) opt_idx = np.argmin(total_cost) return { 'index': opt_idx, 'years': df['years'].iloc[opt_idx], 'energy_PJ': df['energy_PJ'].iloc[opt_idx], 'total_cost': total_cost[opt_idx], 'elevator_fraction': df['elevator_fraction'].iloc[opt_idx], 'lambda': lambda_cost, } def calculate_marginal_energy_saving(df: pd.DataFrame) -> np.ndarray: """ Calculate marginal energy saving rate: -dE/dT (PJ/year) This represents how much energy is saved per additional year of timeline. """ years = df['years'].values energy = df['energy_PJ'].values # Use central difference for interior points marginal = -np.gradient(energy, years) return marginal def sensitivity_analysis( df: pd.DataFrame, lambda_range: np.ndarray ) -> pd.DataFrame: """ Perform sensitivity analysis on λ parameter Args: df: Trade-off curve data lambda_range: Array of λ values to test Returns: DataFrame with optimal points for each λ """ results = [] for lam in lambda_range: opt = find_optimal_point(df, lam) results.append({ 'lambda_PJ_per_year': lam, 'optimal_years': opt['years'], 'optimal_energy_PJ': opt['energy_PJ'], 'total_cost_PJ': opt['total_cost'], 'elevator_fraction': opt['elevator_fraction'], }) return pd.DataFrame(results) # ============== Visualization Functions ============== def plot_lambda_analysis( df: pd.DataFrame, save_path: str = '/Volumes/Files/code/mm/20260130_b/p1/lambda_cost_analysis.png' ): """ Comprehensive visualization of λ time cost analysis Focus on critical range λ = 400-600 PJ/year """ fig, axes = plt.subplots(2, 2, figsize=(14, 12)) years = df['years'].values energy = df['energy_PJ'].values # Key boundaries T_min = years.min() # ~100.7 years (fastest) T_elev = TOTAL_PAYLOAD / TOTAL_ELEVATOR_CAPACITY # ~186 years (elevator only) E_min = energy[years >= T_elev].min() if any(years >= T_elev) else energy.min() # ========== Plot 1: Trade-off Curve with λ Iso-cost Lines ========== ax1 = axes[0, 0] # Main curve ax1.plot(years, energy, 'b-', linewidth=2.5, label='Energy-Time Trade-off') # Mark key points ax1.axvline(x=T_elev, color='green', linestyle='--', alpha=0.7, label=f'Elevator-only: {T_elev:.1f} years') ax1.axvline(x=T_min, color='red', linestyle='--', alpha=0.7, label=f'Minimum time: {T_min:.1f} years') # Draw iso-cost lines for different λ (focus on 400-600 range) lambda_values = [420, 480, 500, 550] colors = ['#2ecc71', '#e74c3c', '#9b59b6', '#3498db'] for lam, color in zip(lambda_values, colors): opt = find_optimal_point(df, lam) # Iso-cost line: E + λT = const → E = const - λT T_line = np.linspace(80, 220, 100) E_line = opt['total_cost'] - lam * T_line valid = (E_line > 0) & (E_line < 70000) ax1.plot(T_line[valid], E_line[valid], '--', color=color, alpha=0.6, linewidth=1.5) ax1.plot(opt['years'], opt['energy_PJ'], 'o', color=color, markersize=12, markeredgecolor='black', markeredgewidth=1.5, label=f'λ={lam}: T={opt["years"]:.1f}y, E={opt["energy_PJ"]:.0f}PJ') ax1.set_xlabel('Construction Timeline T (years)', fontsize=12) ax1.set_ylabel('Total Energy E (PJ)', fontsize=12) ax1.set_title('Energy-Time Trade-off with λ Iso-cost Lines (λ=400-600)\n$J = E + λT$', fontsize=13) ax1.legend(loc='upper right', fontsize=9) ax1.grid(True, alpha=0.3) ax1.set_xlim(95, 200) ax1.set_ylim(10000, 65000) # ========== Plot 2: Marginal Energy Saving Rate ========== ax2 = axes[0, 1] marginal = calculate_marginal_energy_saving(df) ax2.plot(years, marginal, 'r-', linewidth=2, label='Marginal Energy Saving -dE/dT') ax2.axhline(y=0, color='black', linestyle='-', alpha=0.3) # Mark critical λ values in 400-600 range for lam, color in zip([450, 480, 500, 550], ['#2ecc71', '#e74c3c', '#9b59b6', '#3498db']): ax2.axhline(y=lam, color=color, linestyle='--', alpha=0.7, label=f'λ = {lam} PJ/year') # Find intersection intersect_idx = np.argmin(np.abs(marginal - lam)) ax2.plot(years[intersect_idx], marginal[intersect_idx], 'o', color=color, markersize=8) ax2.set_xlabel('Construction Timeline T (years)', fontsize=12) ax2.set_ylabel('Marginal Energy Saving -dE/dT (PJ/year)', fontsize=12) ax2.set_title('Marginal Analysis: Optimal Condition is -dE/dT = λ', fontsize=13) ax2.legend(loc='upper right', fontsize=9) ax2.grid(True, alpha=0.3) ax2.set_xlim(95, 200) ax2.set_ylim(350, 620) # ========== Plot 3: Sensitivity Analysis (400-600 range) ========== ax3 = axes[1, 0] lambda_range = np.linspace(400, 600, 200) sensitivity_df = sensitivity_analysis(df, lambda_range) # Plot optimal years vs λ ax3.plot(sensitivity_df['lambda_PJ_per_year'], sensitivity_df['optimal_years'], 'b-', linewidth=2.5, label='Optimal Timeline T*') # Mark key regions ax3.axhline(y=T_elev, color='green', linestyle='--', alpha=0.7, label=f'Elevator-only: {T_elev:.1f}y') ax3.axhline(y=T_min, color='red', linestyle='--', alpha=0.7, label=f'Min timeline: {T_min:.1f}y') ax3.axhline(y=139, color='orange', linestyle=':', linewidth=2, alpha=0.8, label='Original knee: 139y') # Find λ corresponding to 139 years idx_139 = np.argmin(np.abs(sensitivity_df['optimal_years'] - 139)) if idx_139 < len(sensitivity_df): lambda_139 = sensitivity_df['lambda_PJ_per_year'].iloc[idx_139] ax3.axvline(x=lambda_139, color='orange', linestyle=':', linewidth=2, alpha=0.6) ax3.scatter([lambda_139], [139], s=150, c='orange', marker='*', zorder=5, edgecolors='black', linewidths=1.5, label=f'λ≈{lambda_139:.0f} for T*=139y') # Mark critical transition ax3.axvline(x=480, color='purple', linestyle='--', alpha=0.5) ax3.text(482, 175, 'Critical\nTransition', fontsize=9, color='purple') ax3.set_xlabel('Time Opportunity Cost λ (PJ/year)', fontsize=12) ax3.set_ylabel('Optimal Timeline T* (years)', fontsize=12) ax3.set_title('Sensitivity Analysis: Optimal Timeline vs λ (400-600 range)', fontsize=13) ax3.legend(loc='upper right', fontsize=9) ax3.grid(True, alpha=0.3) ax3.set_xlim(400, 600) ax3.set_ylim(95, 195) # ========== Plot 4: Total Cost Curves (400-600 range) ========== ax4 = axes[1, 1] for lam, color in [(450, '#2ecc71'), (480, '#e74c3c'), (500, '#9b59b6'), (550, '#3498db')]: total_cost = calculate_total_cost(df, lam) ax4.plot(years, total_cost / 1000, color=color, linestyle='-', linewidth=2, label=f'λ={lam} PJ/year') # Mark minimum opt = find_optimal_point(df, lam) ax4.plot(opt['years'], opt['total_cost'] / 1000, 'o', color=color, markersize=10, markeredgecolor='black', markeredgewidth=1.5) ax4.set_xlabel('Construction Timeline T (years)', fontsize=12) ax4.set_ylabel('Total Cost J = E + λT (×10³ PJ)', fontsize=12) ax4.set_title('Total Cost Function for λ = 400-600 PJ/year', fontsize=13) ax4.legend(loc='upper right', fontsize=10) ax4.grid(True, alpha=0.3) ax4.set_xlim(95, 200) plt.tight_layout() plt.savefig(save_path, dpi=150, bbox_inches='tight') print(f"Lambda analysis plot saved to: {save_path}") return fig def plot_decision_recommendation( df: pd.DataFrame, save_path: str = '/Volumes/Files/code/mm/20260130_b/p1/lambda_decision_map.png' ): """ Decision map showing optimal choice based on λ preference """ fig, ax = plt.subplots(figsize=(12, 8)) years = df['years'].values energy = df['energy_PJ'].values # Main trade-off curve ax.plot(years, energy, 'b-', linewidth=3, label='Feasible Trade-off Curve') # Key boundaries T_elev = TOTAL_PAYLOAD / TOTAL_ELEVATOR_CAPACITY T_min = years.min() # Shade decision regions # Region 1: Low λ (cost priority) → longer timeline ax.axvspan(160, 190, alpha=0.2, color='green', label='Low λ (<150): Cost Priority') # Region 2: Medium λ (balanced) → middle ground ax.axvspan(130, 160, alpha=0.2, color='yellow', label='Medium λ (150-250): Balanced') # Region 3: High λ (time priority) → shorter timeline ax.axvspan(100, 130, alpha=0.2, color='red', label='High λ (>250): Time Priority') # Mark specific strategy points strategies = [ {'name': 'A: Cost-Prioritized', 'years': 186, 'lambda': 0, 'color': 'green'}, {'name': 'C: Balanced (λ≈200)', 'years': 139, 'lambda': 200, 'color': 'orange'}, {'name': 'B: Time-Prioritized', 'years': 101, 'lambda': 500, 'color': 'red'}, ] for s in strategies: idx = np.argmin(np.abs(years - s['years'])) ax.plot(years[idx], energy[idx], 'o', color=s['color'], markersize=15, markeredgecolor='black', markeredgewidth=2) ax.annotate(s['name'], (years[idx], energy[idx]), textcoords="offset points", xytext=(10, 10), fontsize=11, fontweight='bold', color=s['color']) # Add decision guidance text textstr = '\n'.join([ 'Decision Guidance:', '─────────────────', 'λ < 150 PJ/year → Strategy A', ' (Long-term cost efficiency)', '', '150 ≤ λ ≤ 250 → Strategy C', ' (Balanced trade-off)', '', 'λ > 250 PJ/year → Strategy B', ' (Time-critical mission)', ]) props = dict(boxstyle='round', facecolor='wheat', alpha=0.8) ax.text(0.02, 0.98, textstr, transform=ax.transAxes, fontsize=10, verticalalignment='top', bbox=props, family='monospace') ax.set_xlabel('Construction Timeline T (years)', fontsize=13) ax.set_ylabel('Total Energy E (PJ)', fontsize=13) ax.set_title('Decision Map: Optimal Strategy Selection Based on Time Opportunity Cost λ', fontsize=14) ax.legend(loc='upper right', fontsize=10) ax.grid(True, alpha=0.3) ax.set_xlim(95, 200) ax.set_ylim(10000, 65000) plt.tight_layout() plt.savefig(save_path, dpi=150, bbox_inches='tight') print(f"Decision map saved to: {save_path}") return fig # ============== Critical Point Analysis ============== def analyze_curve_structure(df: pd.DataFrame) -> Dict: """ Analyze the structure of the trade-off curve to understand why certain λ values lead to specific optimal points. """ years = df['years'].values energy = df['energy_PJ'].values # Calculate marginal rates marginal = -np.gradient(energy, years) # Find the elevator-only boundary T_elev = TOTAL_PAYLOAD / TOTAL_ELEVATOR_CAPACITY idx_elev = np.argmin(np.abs(years - T_elev)) # Analyze marginal rate distribution # Region 1: T > T_elev (pure elevator, marginal ≈ 0) # Region 2: T < T_elev (need rockets, marginal > 0) # Find critical points where marginal rate changes significantly marginal_near_elev = marginal[idx_elev - 5:idx_elev + 5] marginal_at_139 = marginal[np.argmin(np.abs(years - 139))] marginal_at_101 = marginal[np.argmin(np.abs(years - 101))] return { 'T_elev': T_elev, 'idx_elev': idx_elev, 'marginal_at_elev': marginal[idx_elev], 'marginal_at_139': marginal_at_139, 'marginal_at_101': marginal_at_101, 'energy_at_elev': energy[idx_elev], 'energy_at_139': energy[np.argmin(np.abs(years - 139))], 'energy_at_101': energy[np.argmin(np.abs(years - 101))], } def find_critical_lambda(df: pd.DataFrame) -> Dict: """ Find the critical λ values where optimal point transitions occur. """ years = df['years'].values energy = df['energy_PJ'].values # Dense lambda scan lambda_range = np.linspace(1, 1000, 2000) transitions = [] prev_years = None for lam in lambda_range: opt = find_optimal_point(df, lam) if prev_years is not None and abs(opt['years'] - prev_years) > 5: transitions.append({ 'lambda': lam, 'from_years': prev_years, 'to_years': opt['years'], }) prev_years = opt['years'] return transitions def plot_comprehensive_analysis( df: pd.DataFrame, save_path: str = '/Volumes/Files/code/mm/20260130_b/p1/lambda_comprehensive.png' ): """ Comprehensive plot showing: 1. Trade-off curve with marginal rates 2. Critical λ transitions 3. Decision regions """ fig = plt.figure(figsize=(16, 14)) years = df['years'].values energy = df['energy_PJ'].values marginal = calculate_marginal_energy_saving(df) T_elev = TOTAL_PAYLOAD / TOTAL_ELEVATOR_CAPACITY T_min = years.min() # ========== Plot 1: Trade-off curve with annotations ========== ax1 = fig.add_subplot(2, 2, 1) ax1.plot(years, energy / 1000, 'b-', linewidth=2.5, label='Trade-off Curve') # Mark key points key_points = [ (T_min, 'Minimum Time\n(~101 years)', 'red'), (139, 'Original Knee\n(139 years)', 'orange'), (T_elev, 'Elevator-Only\n(~186 years)', 'green'), ] for t, label, color in key_points: idx = np.argmin(np.abs(years - t)) ax1.plot(years[idx], energy[idx] / 1000, 'o', color=color, markersize=12, markeredgecolor='black', markeredgewidth=2, zorder=5) ax1.annotate(label, (years[idx], energy[idx] / 1000), textcoords="offset points", xytext=(10, 10), fontsize=10, color=color, fontweight='bold') ax1.set_xlabel('Construction Timeline T (years)', fontsize=12) ax1.set_ylabel('Total Energy E (×10³ PJ)', fontsize=12) ax1.set_title('Energy-Time Trade-off Curve', fontsize=14) ax1.grid(True, alpha=0.3) ax1.set_xlim(95, 200) ax1.legend(loc='upper right') # ========== Plot 2: Marginal Rate Analysis ========== ax2 = fig.add_subplot(2, 2, 2) ax2.plot(years, marginal, 'r-', linewidth=2, label='Marginal Saving Rate') ax2.fill_between(years, 0, marginal, alpha=0.3, color='red') # Mark critical thresholds ax2.axhline(y=480, color='purple', linestyle='--', linewidth=2, label='Critical λ ≈ 480 PJ/year') ax2.axvline(x=T_elev, color='green', linestyle=':', alpha=0.7) # Annotate the jump at elevator boundary ax2.annotate('Marginal rate jumps\nat elevator capacity limit', xy=(T_elev, marginal[np.argmin(np.abs(years - T_elev))]), xytext=(150, 400), fontsize=10, arrowprops=dict(arrowstyle='->', color='black')) ax2.set_xlabel('Construction Timeline T (years)', fontsize=12) ax2.set_ylabel('Marginal Energy Saving -dE/dT (PJ/year)', fontsize=12) ax2.set_title('Marginal Analysis: Why 186 years is optimal for most λ', fontsize=14) ax2.grid(True, alpha=0.3) ax2.set_xlim(95, 200) ax2.set_ylim(-50, 600) ax2.legend(loc='upper right') # ========== Plot 3: λ Sensitivity with Phase Transitions (400-600 focus) ========== ax3 = fig.add_subplot(2, 2, 3) lambda_range = np.linspace(400, 600, 200) opt_years = [] for lam in lambda_range: opt = find_optimal_point(df, lam) opt_years.append(opt['years']) ax3.plot(lambda_range, opt_years, 'b-', linewidth=2.5) # Shade phases ax3.axhspan(180, 190, alpha=0.3, color='green', label='Phase 1: Elevator-Only (λ < 480)') ax3.axhspan(100, 145, alpha=0.3, color='red', label='Phase 2: Hybrid (λ > 480)') # Mark critical λ ax3.axvline(x=480, color='purple', linestyle='--', linewidth=2) ax3.annotate('Critical Transition\nλ ≈ 480 PJ/year', xy=(480, 160), fontsize=11, color='purple', fontweight='bold', ha='center') # Mark 139 year point ax3.axhline(y=139, color='orange', linestyle=':', linewidth=2, alpha=0.8) ax3.scatter([500], [139], s=200, c='orange', marker='*', zorder=5, edgecolors='black', linewidths=2, label='T*=139y at λ≈500') ax3.set_xlabel('Time Opportunity Cost λ (PJ/year)', fontsize=12) ax3.set_ylabel('Optimal Timeline T* (years)', fontsize=12) ax3.set_title('Phase Transition in Optimal Strategy (λ=400-600)', fontsize=14) ax3.grid(True, alpha=0.3) ax3.set_xlim(400, 600) ax3.set_ylim(95, 195) ax3.legend(loc='upper right') # ========== Plot 4: Decision Summary Table ========== ax4 = fig.add_subplot(2, 2, 4) ax4.axis('off') # Create summary table summary_text = """ ┌─────────────────────────────────────────────────────────────────┐ │ KEY FINDINGS FROM λ COST ANALYSIS │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ 1. CURVE STRUCTURE │ │ • Sharp discontinuity at T = 186 years (elevator capacity) │ │ • Marginal rate jumps from ~0 to ~480 PJ/year at boundary │ │ │ │ 2. OPTIMAL POINT SELECTION │ │ • λ < 480 PJ/year → T* = 186 years (elevator-only) │ │ • λ ≈ 500 PJ/year → T* = 139 years (original knee) │ │ • λ > 600 PJ/year → T* → 101 years (time-priority) │ │ │ │ 3. IMPLICATIONS FOR THE 139-YEAR KNEE POINT │ │ • Requires implicit assumption: λ ≈ 500 PJ/year │ │ • This means: 1 year delay costs ~500 PJ opportunity cost │ │ • Equivalent to: ~0.5% of total elevator energy per year │ │ │ │ 4. RECOMMENDATION │ │ • Either justify λ ≈ 500 PJ/year with physical reasoning │ │ • Or acknowledge 186 years as cost-optimal baseline │ │ • Present 139 years as a time-constrained scenario │ │ │ └─────────────────────────────────────────────────────────────────┘ """ ax4.text(0.05, 0.95, summary_text, transform=ax4.transAxes, fontsize=11, verticalalignment='top', family='monospace', bbox=dict(boxstyle='round', facecolor='lightyellow', alpha=0.8)) plt.tight_layout() plt.savefig(save_path, dpi=150, bbox_inches='tight') print(f"Comprehensive analysis saved to: {save_path}") return fig # ============== Main Analysis ============== def main(): print("=" * 70) print("Lambda Time Cost Analysis for Moon Colony Logistics") print("=" * 70) # Generate trade-off curve print("\n[1] Generating Energy-Time trade-off curve...") df = generate_tradeoff_curve(year_min=100, year_max=220, num_points=500) print(f" Generated {len(df)} data points") # Key statistics T_min = df['years'].min() T_max = df['years'].max() E_min = df['energy_PJ'].min() E_max = df['energy_PJ'].max() T_elev = TOTAL_PAYLOAD / TOTAL_ELEVATOR_CAPACITY print(f"\n[2] Trade-off Curve Statistics:") print(f" Timeline range: {T_min:.1f} - {T_max:.1f} years") print(f" Energy range: {E_min:.0f} - {E_max:.0f} PJ") print(f" Elevator-only timeline: {T_elev:.1f} years") # Curve structure analysis print("\n[3] Curve Structure Analysis:") structure = analyze_curve_structure(df) print(f" At elevator boundary (T={structure['T_elev']:.1f}y):") print(f" - Energy: {structure['energy_at_elev']:.0f} PJ") print(f" - Marginal rate: {structure['marginal_at_elev']:.1f} PJ/year") print(f" At T=139 years:") print(f" - Energy: {structure['energy_at_139']:.0f} PJ") print(f" - Marginal rate: {structure['marginal_at_139']:.1f} PJ/year") print(f" At T=101 years:") print(f" - Energy: {structure['energy_at_101']:.0f} PJ") print(f" - Marginal rate: {structure['marginal_at_101']:.1f} PJ/year") # Find critical transitions print("\n[4] Critical λ Transitions:") transitions = find_critical_lambda(df) for t in transitions: print(f" λ ≈ {t['lambda']:.0f} PJ/year: T* jumps from {t['from_years']:.0f}y to {t['to_years']:.0f}y") # Sensitivity analysis print("\n[5] Sensitivity Analysis Results:") print(" " + "-" * 55) print(f" {'λ (PJ/year)':<15} {'Optimal T (years)':<20} {'Energy (PJ)':<15}") print(" " + "-" * 55) test_lambdas = [100, 200, 300, 400, 450, 480, 500, 550, 600] sensitivity_results = [] for lam in test_lambdas: opt = find_optimal_point(df, lam) print(f" {lam:<15.0f} {opt['years']:<20.1f} {opt['energy_PJ']:<15.0f}") sensitivity_results.append(opt) # Find λ that gives 139 years print("\n[6] Original Knee Point Analysis (139 years):") lambda_for_139 = None for lam in np.linspace(400, 600, 1000): opt = find_optimal_point(df, lam) if abs(opt['years'] - 139) < 1: lambda_for_139 = lam print(f" λ ≈ {lam:.0f} PJ/year corresponds to T* ≈ 139 years") break if lambda_for_139: print(f"\n INTERPRETATION:") print(f" To justify 139-year knee point, one must argue that:") print(f" • Each year of delay costs ~{lambda_for_139:.0f} PJ in opportunity") print(f" • This is {lambda_for_139/E_min*100:.1f}% of minimum total energy per year") print(f" • Over 47 years (139→186), this amounts to {lambda_for_139*47:.0f} PJ") # Generate plots print("\n[7] Generating visualization plots...") plot_lambda_analysis(df) plot_decision_recommendation(df) plot_comprehensive_analysis(df) # Save sensitivity data sensitivity_df = sensitivity_analysis(df, np.linspace(50, 600, 120)) sensitivity_df.to_csv('/Volumes/Files/code/mm/20260130_b/p1/lambda_sensitivity.csv', index=False) print(" Data saved to: lambda_sensitivity.csv") # Paper modification recommendations print("\n" + "=" * 70) print("RECOMMENDATIONS FOR PAPER MODIFICATION") print("=" * 70) print(""" 1. REFRAME THE OPTIMIZATION PROBLEM Current: "Multi-objective Pareto optimization" Suggested: "Constrained optimization with time-cost trade-off" 2. INTRODUCE λ AS DECISION PARAMETER Add equation: J = E_total + λ × T where λ represents "time opportunity cost" (PJ/year) 3. JUSTIFY THE KNEE POINT SELECTION Option A: Argue λ ≈ 480-500 PJ/year is reasonable because: - Delayed lunar resource extraction - Extended Earth-side operational costs - Strategic/geopolitical value of early completion Option B: Present multiple scenarios: - "Cost-optimal" (λ < 480): T* = 186 years - "Balanced" (λ ≈ 500): T* = 139 years - "Time-critical" (λ > 600): T* → 101 years 4. ADD SENSITIVITY ANALYSIS FIGURE Show how optimal T* changes with λ (Figure generated) 5. ACKNOWLEDGE THE DISCONTINUITY Note that the trade-off curve has a sharp transition at T = 186 years due to elevator capacity constraints """) print("=" * 70) print("Analysis Complete!") print("=" * 70) return df, sensitivity_results def plot_total_cost_standalone( df: pd.DataFrame = None, save_path: str = '/Volumes/Files/code/mm/20260130_b/p1/total_cost_curves.png' ): """ 单独绘制总成本曲线图 - 长宽比 0.618 - 删去标题 - 删去 λ=500,添加目标 λ=504 - 低偏中饱和度色系 """ if df is None: df = generate_tradeoff_curve() # 使用0.618黄金比例 fig_width = 8 fig_height = fig_width * 0.618 fig, ax = plt.subplots(figsize=(fig_width, fig_height)) years = df['years'].values # 低偏中饱和度配色(删去500,添加504) lambda_colors = [ (450, '#6B9B78'), # 灰绿色 (480, '#B87B6B'), # 灰橙红色 (504, '#8B7BA8'), # 灰紫色(目标λ) (550, '#5B8FA8'), # 灰蓝色 ] for lam, color in lambda_colors: total_cost = calculate_total_cost(df, lam) linewidth = 2.5 if lam == 504 else 2 linestyle = '-' label = f'λ={lam} PJ/year' if lam != 504 else f'λ={lam} PJ/year (target)' ax.plot(years, total_cost / 1000, color=color, linestyle=linestyle, linewidth=linewidth, label=label) # 标记最小值点 opt = find_optimal_point(df, lam) markersize = 11 if lam == 504 else 9 ax.plot(opt['years'], opt['total_cost'] / 1000, 'o', color=color, markersize=markersize, markeredgecolor='#333333', markeredgewidth=1.5) ax.set_xlabel('Construction Timeline T (years)', fontsize=11) ax.set_ylabel('Total Cost J = E + λT (×10³ PJ)', fontsize=11) ax.legend(loc='upper right', fontsize=9) ax.grid(True, alpha=0.3) ax.set_xlim(95, 200) ax.set_ylim(95, 130) plt.tight_layout() plt.savefig(save_path, dpi=150, bbox_inches='tight') print(f"Total cost curves saved to: {save_path}") return fig if __name__ == "__main__": df, results = main()