""" Task 1 Sensitivity Analysis for Moon Colony Logistics This module performs comprehensive sensitivity analysis on key model parameters: 1. Rocket payload capacity (100-150 tons, as specified in problem) 2. Elevator capacity (±20%) 3. Launch frequency (0.5-2 launches/day) 4. Engine technology (Isp variations) 5. Structural coefficient α Author: MCM 2026 Team """ 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 import warnings warnings.filterwarnings('ignore') # 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 # Baseline Mission parameters TOTAL_PAYLOAD = 100e6 # 100 million metric tons # Baseline Space Elevator parameters BASELINE_NUM_ELEVATORS = 3 BASELINE_ELEVATOR_CAPACITY_PER_YEAR = 179000 # metric tons per elevator per year BASELINE_ELEVATOR_SPECIFIC_ENERGY = 157.2e9 # J per metric ton # Baseline Rocket parameters BASELINE_PAYLOAD_PER_LAUNCH = 125 # metric tons (range: 100-150) BASELINE_ISP = 363 # seconds (LOX/CH4) BASELINE_SPECIFIC_FUEL_ENERGY = 11.9e6 # J/kg BASELINE_ALPHA = 0.10 # structural coefficient BASELINE_NUM_STAGES = 3 BASELINE_DELTA_V = 13300 # m/s # Baseline Launch frequency BASELINE_LAUNCHES_PER_DAY = 1 # per site # ============== Launch Sites ============== @dataclass class LaunchSite: name: str short_name: str latitude: float @property def abs_latitude(self) -> float: return abs(self.latitude) def delta_v_loss(self, omega=OMEGA_EARTH, r=R_EARTH) -> float: v_equator = omega * r v_site = omega * r * np.cos(np.radians(self.abs_latitude)) return v_equator - v_site LAUNCH_SITES = [ LaunchSite("Kourou", "Kourou", 5.2), LaunchSite("SDSC", "SDSC", 13.7), LaunchSite("Texas", "Texas", 26.0), LaunchSite("Florida", "Florida", 28.5), LaunchSite("California", "California", 34.7), LaunchSite("Virginia", "Virginia", 37.8), LaunchSite("Taiyuan", "Taiyuan", 38.8), LaunchSite("Mahia", "Mahia", 39.3), LaunchSite("Baikonur", "Baikonur", 45.6), LaunchSite("Alaska", "Alaska", 57.4), ] LAUNCH_SITES = sorted(LAUNCH_SITES, key=lambda x: x.abs_latitude) NUM_SITES = len(LAUNCH_SITES) # ============== Core Calculation Functions ============== def fuel_ratio_multistage(delta_v: float, isp: float, alpha: float, num_stages: int) -> float: """Calculate 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 calculate_scenario( completion_years: float, payload_per_launch: float = BASELINE_PAYLOAD_PER_LAUNCH, elevator_capacity: float = BASELINE_NUM_ELEVATORS * BASELINE_ELEVATOR_CAPACITY_PER_YEAR, launches_per_day: float = BASELINE_LAUNCHES_PER_DAY, isp: float = BASELINE_ISP, alpha: float = BASELINE_ALPHA, num_stages: int = BASELINE_NUM_STAGES, delta_v_base: float = BASELINE_DELTA_V, specific_fuel_energy: float = BASELINE_SPECIFIC_FUEL_ENERGY, ) -> Optional[Dict]: """Calculate scenario for given parameters""" # Space elevator transport elevator_payload = min(elevator_capacity * completion_years, TOTAL_PAYLOAD) elevator_energy = elevator_payload * BASELINE_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, 'total_energy_PJ': elevator_energy / 1e15, 'rocket_launches': 0, 'elevator_fraction': 1.0, 'feasible': True, } # Rocket launches needed rocket_launches_needed = int(np.ceil(remaining_payload / payload_per_launch)) # Check feasibility days_available = completion_years * 365 max_launches_per_site = int(days_available * launches_per_day) total_rocket_capacity = NUM_SITES * max_launches_per_site * payload_per_launch if remaining_payload > total_rocket_capacity: return { 'years': completion_years, 'feasible': False, 'shortage': remaining_payload - total_rocket_capacity, } # Calculate rocket energy (low-latitude priority) rocket_energy = 0 remaining_launches = rocket_launches_needed for site in LAUNCH_SITES: if remaining_launches <= 0: break allocated = min(remaining_launches, max_launches_per_site) # Calculate fuel ratio for this site total_dv = delta_v_base + site.delta_v_loss() k = fuel_ratio_multistage(total_dv, isp, alpha, num_stages) fuel_per_ton = k * 1000 # kg fuel per metric ton payload energy_per_ton = fuel_per_ton * specific_fuel_energy rocket_energy += energy_per_ton * payload_per_launch * allocated remaining_launches -= allocated total_energy = elevator_energy + rocket_energy return { 'years': completion_years, 'elevator_payload': elevator_payload, 'rocket_payload': remaining_payload, 'total_energy_PJ': total_energy / 1e15, 'rocket_launches': rocket_launches_needed, 'elevator_fraction': elevator_payload / TOTAL_PAYLOAD, 'feasible': True, } def find_minimum_timeline( payload_per_launch: float = BASELINE_PAYLOAD_PER_LAUNCH, elevator_capacity: float = BASELINE_NUM_ELEVATORS * BASELINE_ELEVATOR_CAPACITY_PER_YEAR, launches_per_day: float = BASELINE_LAUNCHES_PER_DAY, **kwargs ) -> float: """Find minimum feasible timeline""" for years in np.linspace(50, 300, 500): result = calculate_scenario( years, payload_per_launch, elevator_capacity, launches_per_day, **kwargs ) if result and result.get('feasible', False): return years return 300 # fallback def generate_tradeoff_curve( year_min: float = None, year_max: float = 250, num_points: int = 200, **kwargs ) -> pd.DataFrame: """Generate trade-off curve for given parameters""" if year_min is None: year_min = find_minimum_timeline(**kwargs) years_range = np.linspace(year_min, year_max, num_points) results = [] for years in years_range: scenario = calculate_scenario(years, **kwargs) if scenario and scenario.get('feasible', False): results.append({ 'years': years, 'energy_PJ': scenario['total_energy_PJ'], 'elevator_fraction': scenario['elevator_fraction'], 'rocket_launches': scenario.get('rocket_launches', 0), }) return pd.DataFrame(results) def find_optimal_point(df: pd.DataFrame, lambda_cost: float) -> Dict: """Find optimal point for given λ""" if df.empty: return {'years': np.nan, 'energy_PJ': np.nan} total_cost = df['energy_PJ'].values + lambda_cost * df['years'].values opt_idx = np.argmin(total_cost) return { 'years': df['years'].iloc[opt_idx], 'energy_PJ': df['energy_PJ'].iloc[opt_idx], 'elevator_fraction': df['elevator_fraction'].iloc[opt_idx], } # ============== Sensitivity Analysis Functions ============== def sensitivity_payload_capacity(): """Analyze sensitivity to rocket payload capacity (100-150 tons)""" print("\n[1] Analyzing Rocket Payload Capacity Sensitivity (100-150 tons)...") payload_range = np.linspace(100, 150, 11) results = [] for payload in payload_range: elevator_cap = BASELINE_NUM_ELEVATORS * BASELINE_ELEVATOR_CAPACITY_PER_YEAR t_min = find_minimum_timeline(payload_per_launch=payload, elevator_capacity=elevator_cap) t_elev = TOTAL_PAYLOAD / elevator_cap df = generate_tradeoff_curve( year_min=t_min, year_max=250, payload_per_launch=payload, elevator_capacity=elevator_cap ) if not df.empty: opt_504 = find_optimal_point(df, 504) energy_at_139 = df[df['years'] >= 139]['energy_PJ'].iloc[0] if any(df['years'] >= 139) else np.nan results.append({ 'payload_tons': payload, 'min_timeline': t_min, 'elevator_only_timeline': t_elev, 'optimal_T_lambda504': opt_504['years'], 'optimal_E_lambda504': opt_504['energy_PJ'], 'energy_at_139y': energy_at_139, }) return pd.DataFrame(results) def sensitivity_elevator_capacity(): """Analyze sensitivity to elevator capacity (±20%)""" print("\n[2] Analyzing Elevator Capacity Sensitivity (±20%)...") baseline_cap = BASELINE_NUM_ELEVATORS * BASELINE_ELEVATOR_CAPACITY_PER_YEAR capacity_factors = np.linspace(0.8, 1.2, 9) results = [] for factor in capacity_factors: elevator_cap = baseline_cap * factor t_min = find_minimum_timeline(elevator_capacity=elevator_cap) t_elev = TOTAL_PAYLOAD / elevator_cap df = generate_tradeoff_curve( year_min=t_min, year_max=300, elevator_capacity=elevator_cap ) if not df.empty: # Find critical λ lambda_crit = None for lam in np.linspace(300, 700, 200): opt = find_optimal_point(df, lam) if opt['years'] < t_elev - 5: lambda_crit = lam break opt_504 = find_optimal_point(df, 504) results.append({ 'capacity_factor': factor, 'capacity_tons_per_year': elevator_cap, 'min_timeline': t_min, 'elevator_only_timeline': t_elev, 'critical_lambda': lambda_crit, 'optimal_T_lambda504': opt_504['years'], 'optimal_E_lambda504': opt_504['energy_PJ'], }) return pd.DataFrame(results) def sensitivity_launch_frequency(): """Analyze sensitivity to launch frequency""" print("\n[3] Analyzing Launch Frequency Sensitivity (0.5-2 launches/day)...") frequency_range = np.linspace(0.5, 2.0, 7) results = [] for freq in frequency_range: elevator_cap = BASELINE_NUM_ELEVATORS * BASELINE_ELEVATOR_CAPACITY_PER_YEAR t_min = find_minimum_timeline(launches_per_day=freq, elevator_capacity=elevator_cap) t_elev = TOTAL_PAYLOAD / elevator_cap df = generate_tradeoff_curve( year_min=t_min, year_max=250, launches_per_day=freq, elevator_capacity=elevator_cap ) if not df.empty: opt_504 = find_optimal_point(df, 504) results.append({ 'launches_per_day': freq, 'min_timeline': t_min, 'elevator_only_timeline': t_elev, 'optimal_T_lambda504': opt_504['years'], 'optimal_E_lambda504': opt_504['energy_PJ'], 'rocket_capacity_tons_per_year': freq * 365 * NUM_SITES * BASELINE_PAYLOAD_PER_LAUNCH, }) return pd.DataFrame(results) def sensitivity_engine_technology(): """Analyze sensitivity to engine technology (Isp)""" print("\n[4] Analyzing Engine Technology Sensitivity (Isp)...") engines = [ ('Solid', 280, 5.0e6), ('LOX/Kerosene', 350, 10.3e6), ('LOX/Methane', 363, 11.9e6), ('LOX/Hydrogen', 450, 15.5e6), ] results = [] for name, isp, specific_energy in engines: elevator_cap = BASELINE_NUM_ELEVATORS * BASELINE_ELEVATOR_CAPACITY_PER_YEAR t_min = find_minimum_timeline( isp=isp, specific_fuel_energy=specific_energy, elevator_capacity=elevator_cap ) df = generate_tradeoff_curve( year_min=t_min, year_max=250, isp=isp, specific_fuel_energy=specific_energy, elevator_capacity=elevator_cap ) if not df.empty: opt_504 = find_optimal_point(df, 504) # Calculate fuel ratio at equator k = fuel_ratio_multistage(BASELINE_DELTA_V, isp, BASELINE_ALPHA, BASELINE_NUM_STAGES) results.append({ 'engine_type': name, 'isp_seconds': isp, 'specific_energy_MJ_kg': specific_energy / 1e6, 'fuel_ratio': k, 'min_timeline': t_min, 'optimal_T_lambda504': opt_504['years'], 'optimal_E_lambda504': opt_504['energy_PJ'], }) return pd.DataFrame(results) def sensitivity_structural_coefficient(): """Analyze sensitivity to structural coefficient α""" print("\n[5] Analyzing Structural Coefficient Sensitivity (α)...") alpha_range = np.linspace(0.06, 0.14, 9) results = [] for alpha in alpha_range: elevator_cap = BASELINE_NUM_ELEVATORS * BASELINE_ELEVATOR_CAPACITY_PER_YEAR t_min = find_minimum_timeline(alpha=alpha, elevator_capacity=elevator_cap) df = generate_tradeoff_curve( year_min=t_min, year_max=250, alpha=alpha, elevator_capacity=elevator_cap ) if not df.empty: opt_504 = find_optimal_point(df, 504) k = fuel_ratio_multistage(BASELINE_DELTA_V, BASELINE_ISP, alpha, BASELINE_NUM_STAGES) results.append({ 'alpha': alpha, 'fuel_ratio': k, 'min_timeline': t_min, 'optimal_T_lambda504': opt_504['years'], 'optimal_E_lambda504': opt_504['energy_PJ'], }) return pd.DataFrame(results) # ============== Visualization ============== def plot_sensitivity_analysis( payload_df: pd.DataFrame, elevator_df: pd.DataFrame, frequency_df: pd.DataFrame, engine_df: pd.DataFrame, alpha_df: pd.DataFrame, save_path: str = '/Volumes/Files/code/mm/20260130_b/p1/sensitivity_analysis.png' ): """Create comprehensive sensitivity analysis visualization""" fig, axes = plt.subplots(2, 3, figsize=(18, 12)) # ========== Plot 1: Rocket Payload Capacity ========== ax1 = axes[0, 0] ax1.plot(payload_df['payload_tons'], payload_df['min_timeline'], 'b-o', linewidth=2, markersize=8, label='Minimum Timeline') ax1.plot(payload_df['payload_tons'], payload_df['optimal_T_lambda504'], 'r-s', linewidth=2, markersize=8, label='Optimal T* (λ=504)') ax1.axhline(y=186.2, color='green', linestyle='--', alpha=0.7, label='Elevator-only') ax1.axvline(x=125, color='gray', linestyle=':', alpha=0.7, label='Baseline (125t)') ax1.fill_betweenx([80, 220], 100, 150, alpha=0.1, color='blue', label='Problem range') ax1.set_xlabel('Rocket Payload Capacity (metric tons)', fontsize=11) ax1.set_ylabel('Timeline (years)', fontsize=11) ax1.set_title('(a) Sensitivity to Rocket Payload Capacity\n(Problem specifies 100-150 tons)', fontsize=12) ax1.legend(loc='upper right', fontsize=9) ax1.grid(True, alpha=0.3) ax1.set_xlim(95, 155) ax1.set_ylim(80, 200) # ========== Plot 2: Elevator Capacity ========== ax2 = axes[0, 1] ax2.plot(elevator_df['capacity_factor'] * 100, elevator_df['elevator_only_timeline'], 'g-o', linewidth=2, markersize=8, label='Elevator-only Timeline') ax2.plot(elevator_df['capacity_factor'] * 100, elevator_df['optimal_T_lambda504'], 'r-s', linewidth=2, markersize=8, label='Optimal T* (λ=504)') ax2.plot(elevator_df['capacity_factor'] * 100, elevator_df['min_timeline'], 'b-^', linewidth=2, markersize=8, label='Minimum Timeline') ax2.axvline(x=100, color='gray', linestyle=':', alpha=0.7, label='Baseline') ax2.set_xlabel('Elevator Capacity (% of baseline)', fontsize=11) ax2.set_ylabel('Timeline (years)', fontsize=11) ax2.set_title('(b) Sensitivity to Elevator Capacity (±20%)', fontsize=12) ax2.legend(loc='upper right', fontsize=9) ax2.grid(True, alpha=0.3) ax2.set_xlim(75, 125) # ========== Plot 3: Launch Frequency ========== ax3 = axes[0, 2] ax3.plot(frequency_df['launches_per_day'], frequency_df['min_timeline'], 'b-o', linewidth=2, markersize=8, label='Minimum Timeline') ax3.plot(frequency_df['launches_per_day'], frequency_df['optimal_T_lambda504'], 'r-s', linewidth=2, markersize=8, label='Optimal T* (λ=504)') ax3.axhline(y=186.2, color='green', linestyle='--', alpha=0.7, label='Elevator-only') ax3.axvline(x=1.0, color='gray', linestyle=':', alpha=0.7, label='Baseline (1/day)') ax3.set_xlabel('Launch Frequency (launches/site/day)', fontsize=11) ax3.set_ylabel('Timeline (years)', fontsize=11) ax3.set_title('(c) Sensitivity to Launch Frequency', fontsize=12) ax3.legend(loc='upper right', fontsize=9) ax3.grid(True, alpha=0.3) # ========== Plot 4: Engine Technology ========== ax4 = axes[1, 0] x_pos = np.arange(len(engine_df)) width = 0.35 bars1 = ax4.bar(x_pos - width/2, engine_df['optimal_E_lambda504'] / 1000, width, label='Energy at T* (×10³ PJ)', color='steelblue', alpha=0.8) ax4_twin = ax4.twinx() bars2 = ax4_twin.bar(x_pos + width/2, engine_df['fuel_ratio'], width, label='Fuel Ratio k', color='coral', alpha=0.8) ax4.set_xlabel('Engine Type', fontsize=11) ax4.set_ylabel('Energy (×10³ PJ)', fontsize=11, color='steelblue') ax4_twin.set_ylabel('Fuel Ratio k', fontsize=11, color='coral') ax4.set_title('(d) Sensitivity to Engine Technology', fontsize=12) ax4.set_xticks(x_pos) ax4.set_xticklabels(engine_df['engine_type'], rotation=15) ax4.legend(loc='upper left', fontsize=9) ax4_twin.legend(loc='upper right', fontsize=9) ax4.grid(True, alpha=0.3, axis='y') # ========== Plot 5: Structural Coefficient ========== ax5 = axes[1, 1] ax5.plot(alpha_df['alpha'], alpha_df['fuel_ratio'], 'g-o', linewidth=2, markersize=8, label='Fuel Ratio k') ax5_twin = ax5.twinx() ax5_twin.plot(alpha_df['alpha'], alpha_df['optimal_E_lambda504'] / 1000, 'r-s', linewidth=2, markersize=8, label='Energy at T* (×10³ PJ)') ax5.axvline(x=0.10, color='gray', linestyle=':', alpha=0.7, label='Baseline') ax5.set_xlabel('Structural Coefficient α', fontsize=11) ax5.set_ylabel('Fuel Ratio k', fontsize=11, color='green') ax5_twin.set_ylabel('Energy (×10³ PJ)', fontsize=11, color='red') ax5.set_title('(e) Sensitivity to Structural Coefficient α', fontsize=12) ax5.legend(loc='upper left', fontsize=9) ax5_twin.legend(loc='upper right', fontsize=9) ax5.grid(True, alpha=0.3) # ========== Plot 6: Summary Tornado Chart ========== ax6 = axes[1, 2] # Calculate sensitivity indices (% change in T* for parameter variation) baseline_T = 139 # baseline optimal timeline sensitivities = [ ('Payload\n(100→150t)', (payload_df['optimal_T_lambda504'].iloc[0] - baseline_T) / baseline_T * 100, (payload_df['optimal_T_lambda504'].iloc[-1] - baseline_T) / baseline_T * 100), ('Elevator Cap.\n(80%→120%)', (elevator_df['optimal_T_lambda504'].iloc[0] - baseline_T) / baseline_T * 100, (elevator_df['optimal_T_lambda504'].iloc[-1] - baseline_T) / baseline_T * 100), ('Launch Freq.\n(0.5→2/day)', (frequency_df['optimal_T_lambda504'].iloc[0] - baseline_T) / baseline_T * 100, (frequency_df['optimal_T_lambda504'].iloc[-1] - baseline_T) / baseline_T * 100), ('Struct. Coef.\n(0.06→0.14)', (alpha_df['optimal_T_lambda504'].iloc[0] - baseline_T) / baseline_T * 100, (alpha_df['optimal_T_lambda504'].iloc[-1] - baseline_T) / baseline_T * 100), ] y_pos = np.arange(len(sensitivities)) for i, (name, low, high) in enumerate(sensitivities): ax6.barh(i, high - low, left=min(low, high), height=0.6, color='steelblue' if high > low else 'coral', alpha=0.7) ax6.plot([low, high], [i, i], 'k-', linewidth=2) ax6.plot(low, i, 'ko', markersize=8) ax6.plot(high, i, 'k^', markersize=8) ax6.axvline(x=0, color='black', linestyle='-', linewidth=1) ax6.set_yticks(y_pos) ax6.set_yticklabels([s[0] for s in sensitivities]) ax6.set_xlabel('Change in Optimal Timeline T* (%)', fontsize=11) ax6.set_title('(f) Sensitivity Summary (Tornado Chart)', fontsize=12) ax6.grid(True, alpha=0.3, axis='x') plt.tight_layout() plt.savefig(save_path, dpi=150, bbox_inches='tight') print(f"\nSensitivity analysis plot saved to: {save_path}") return fig def plot_tornado_chart_standalone( payload_df: pd.DataFrame, elevator_df: pd.DataFrame, frequency_df: pd.DataFrame, alpha_df: pd.DataFrame, save_path: str = '/Volumes/Files/code/mm/20260130_b/p1/tornado_chart.png' ): """ 单独绘制 Tornado Chart(敏感性汇总图) - XY轴反转(垂直柱状图) - 长宽比 0.618 - 无标题 - 有图例 - 低饱和度色系 """ # 使用0.618黄金比例 fig_width = 8 fig_height = fig_width * 0.618 fig, ax = plt.subplots(figsize=(fig_width, fig_height)) # 计算敏感性指标 baseline_T = 139 sensitivities = [ ('Payload\n(100→150t)', (payload_df['optimal_T_lambda504'].iloc[0] - baseline_T) / baseline_T * 100, (payload_df['optimal_T_lambda504'].iloc[-1] - baseline_T) / baseline_T * 100), ('Elevator Cap.\n(80%→120%)', (elevator_df['optimal_T_lambda504'].iloc[0] - baseline_T) / baseline_T * 100, (elevator_df['optimal_T_lambda504'].iloc[-1] - baseline_T) / baseline_T * 100), ('Launch Freq.\n(0.5→2/day)', (frequency_df['optimal_T_lambda504'].iloc[0] - baseline_T) / baseline_T * 100, (frequency_df['optimal_T_lambda504'].iloc[-1] - baseline_T) / baseline_T * 100), ('Struct. Coef.\n(0.06→0.14)', (alpha_df['optimal_T_lambda504'].iloc[0] - baseline_T) / baseline_T * 100, (alpha_df['optimal_T_lambda504'].iloc[-1] - baseline_T) / baseline_T * 100), ] # 低饱和度配色 color_positive = '#7A9CBF' # 灰蓝色(参数增加导致T*增加) color_negative = '#C4816B' # 灰橙红色(参数增加导致T*减少) x_pos = np.arange(len(sensitivities)) bar_width = 0.6 # 绘制柱状图(XY反转,使用垂直柱状图) for i, (name, low, high) in enumerate(sensitivities): bar_color = color_positive if high > low else color_negative bar_height = high - low bar_bottom = min(low, high) ax.bar(i, bar_height, bottom=bar_bottom, width=bar_width, color=bar_color, alpha=0.8, edgecolor='#555555', linewidth=0.8) # 标记端点 ax.plot(i, low, 'o', color='#444444', markersize=7, zorder=5) ax.plot(i, high, '^', color='#444444', markersize=7, zorder=5) ax.plot([i, i], [low, high], '-', color='#444444', linewidth=1.5, zorder=4) # 基准线 ax.axhline(y=0, color='#333333', linestyle='-', linewidth=1.2) # 设置轴 ax.set_xticks(x_pos) ax.set_xticklabels([s[0] for s in sensitivities], fontsize=10) ax.set_ylabel('Change in Optimal Timeline T* (%)', fontsize=11) ax.grid(True, alpha=0.3, axis='y') # 添加图例 from matplotlib.patches import Patch from matplotlib.lines import Line2D legend_elements = [ Patch(facecolor=color_negative, edgecolor='#555555', alpha=0.8, label='Parameter ↑ → T* ↓'), Patch(facecolor=color_positive, edgecolor='#555555', alpha=0.8, label='Parameter ↑ → T* ↑'), Line2D([0], [0], marker='o', color='#444444', linestyle='None', markersize=6, label='Low value'), Line2D([0], [0], marker='^', color='#444444', linestyle='None', markersize=6, label='High value'), ] ax.legend(handles=legend_elements, loc='upper right', fontsize=9, framealpha=0.9) plt.tight_layout() plt.savefig(save_path, dpi=150, bbox_inches='tight') print(f"Tornado chart saved to: {save_path}") return fig def plot_tradeoff_comparison( save_path: str = '/Volumes/Files/code/mm/20260130_b/p1/sensitivity_tradeoff.png' ): """Plot trade-off curves for different parameter values""" fig, axes = plt.subplots(2, 2, figsize=(14, 12)) elevator_cap = BASELINE_NUM_ELEVATORS * BASELINE_ELEVATOR_CAPACITY_PER_YEAR # ========== Plot 1: Different Payload Capacities ========== ax1 = axes[0, 0] for payload, color in [(100, 'red'), (125, 'blue'), (150, 'green')]: df = generate_tradeoff_curve(payload_per_launch=payload, elevator_capacity=elevator_cap) if not df.empty: ax1.plot(df['years'], df['energy_PJ'] / 1000, color=color, linewidth=2, label=f'{payload} tons') ax1.set_xlabel('Timeline (years)', fontsize=11) ax1.set_ylabel('Energy (×10³ PJ)', fontsize=11) ax1.set_title('Trade-off Curves: Rocket Payload Capacity', fontsize=12) ax1.legend(title='Payload') ax1.grid(True, alpha=0.3) ax1.set_xlim(95, 200) # ========== Plot 2: Different Elevator Capacities ========== ax2 = axes[0, 1] for factor, color in [(0.8, 'red'), (1.0, 'blue'), (1.2, 'green')]: df = generate_tradeoff_curve(elevator_capacity=elevator_cap * factor) if not df.empty: ax2.plot(df['years'], df['energy_PJ'] / 1000, color=color, linewidth=2, label=f'{factor*100:.0f}%') ax2.set_xlabel('Timeline (years)', fontsize=11) ax2.set_ylabel('Energy (×10³ PJ)', fontsize=11) ax2.set_title('Trade-off Curves: Elevator Capacity', fontsize=12) ax2.legend(title='Capacity') ax2.grid(True, alpha=0.3) ax2.set_xlim(95, 250) # ========== Plot 3: Different Launch Frequencies ========== ax3 = axes[1, 0] for freq, color in [(0.5, 'red'), (1.0, 'blue'), (2.0, 'green')]: df = generate_tradeoff_curve(launches_per_day=freq, elevator_capacity=elevator_cap) if not df.empty: ax3.plot(df['years'], df['energy_PJ'] / 1000, color=color, linewidth=2, label=f'{freq}/day') ax3.set_xlabel('Timeline (years)', fontsize=11) ax3.set_ylabel('Energy (×10³ PJ)', fontsize=11) ax3.set_title('Trade-off Curves: Launch Frequency', fontsize=12) ax3.legend(title='Frequency') ax3.grid(True, alpha=0.3) ax3.set_xlim(45, 200) # ========== Plot 4: Different Engine Types ========== ax4 = axes[1, 1] engines = [ ('LOX/Kerosene', 350, 10.3e6, 'red'), ('LOX/Methane', 363, 11.9e6, 'blue'), ('LOX/Hydrogen', 450, 15.5e6, 'green'), ] for name, isp, se, color in engines: df = generate_tradeoff_curve(isp=isp, specific_fuel_energy=se, elevator_capacity=elevator_cap) if not df.empty: ax4.plot(df['years'], df['energy_PJ'] / 1000, color=color, linewidth=2, label=f'{name}') ax4.set_xlabel('Timeline (years)', fontsize=11) ax4.set_ylabel('Energy (×10³ PJ)', fontsize=11) ax4.set_title('Trade-off Curves: Engine Technology', fontsize=12) ax4.legend(title='Engine') 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"Trade-off comparison plot saved to: {save_path}") return fig # ============== Main ============== def main(): print("=" * 70) print("Task 1 Sensitivity Analysis for Moon Colony Logistics") print("=" * 70) # Run all sensitivity analyses payload_df = sensitivity_payload_capacity() elevator_df = sensitivity_elevator_capacity() frequency_df = sensitivity_launch_frequency() engine_df = sensitivity_engine_technology() alpha_df = sensitivity_structural_coefficient() # Print summary tables print("\n" + "=" * 70) print("SENSITIVITY ANALYSIS RESULTS") print("=" * 70) print("\n[1] Rocket Payload Capacity (Problem range: 100-150 tons)") print(payload_df.to_string(index=False)) print("\n[2] Elevator Capacity (±20%)") print(elevator_df.to_string(index=False)) print("\n[3] Launch Frequency") print(frequency_df.to_string(index=False)) print("\n[4] Engine Technology") print(engine_df.to_string(index=False)) print("\n[5] Structural Coefficient α") print(alpha_df.to_string(index=False)) # Generate plots print("\n" + "=" * 70) print("Generating Visualization...") print("=" * 70) plot_sensitivity_analysis(payload_df, elevator_df, frequency_df, engine_df, alpha_df) plot_tradeoff_comparison() # Save data payload_df.to_csv('/Volumes/Files/code/mm/20260130_b/p1/sensitivity_payload.csv', index=False) elevator_df.to_csv('/Volumes/Files/code/mm/20260130_b/p1/sensitivity_elevator.csv', index=False) frequency_df.to_csv('/Volumes/Files/code/mm/20260130_b/p1/sensitivity_frequency.csv', index=False) engine_df.to_csv('/Volumes/Files/code/mm/20260130_b/p1/sensitivity_engine.csv', index=False) alpha_df.to_csv('/Volumes/Files/code/mm/20260130_b/p1/sensitivity_alpha.csv', index=False) print("\nData saved to CSV files.") # Summary print("\n" + "=" * 70) print("KEY FINDINGS") print("=" * 70) print(""" 1. ROCKET PAYLOAD CAPACITY (100-150 tons): - Higher capacity reduces minimum timeline significantly - Limited impact on energy when elevator dominates - Baseline 125 tons is a reasonable middle estimate 2. ELEVATOR CAPACITY (±20%): - Most sensitive parameter for timeline - ±20% capacity changes elevator-only timeline by ~37 years - Critical for long-term planning 3. LAUNCH FREQUENCY: - Doubling frequency halves minimum rocket-only timeline - Essential for time-constrained scenarios - Less impact when elevator provides majority of transport 4. ENGINE TECHNOLOGY: - Primarily affects energy, not timeline - LOX/Hydrogen saves ~30% energy vs LOX/Methane - But LOX/Methane has practical advantages (storage, cost) 5. STRUCTURAL COEFFICIENT α: - Lower α (better technology) improves fuel efficiency - Moderate impact on total energy - Progress from 0.10 to 0.08 saves ~8% energy """) print("=" * 70) print("Analysis Complete!") print("=" * 70) return payload_df, elevator_df, frequency_df, engine_df, alpha_df if __name__ == "__main__": results = main()