p1: sen
This commit is contained in:
844
p1/lambda_cost_analysis.py
Normal file
844
p1/lambda_cost_analysis.py
Normal file
@@ -0,0 +1,844 @@
|
||||
"""
|
||||
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
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
df, results = main()
|
||||
Reference in New Issue
Block a user