155 lines
5.2 KiB
Python
155 lines
5.2 KiB
Python
|
|
#!/usr/bin/env python3
|
|||
|
|
# -*- coding: utf-8 -*-
|
|||
|
|
"""
|
|||
|
|
Richards S-Curve Fit for 2010-2025 Data with K=4298
|
|||
|
|
|
|||
|
|
Fits Richards model to 2010-2025 launch data with carrying capacity
|
|||
|
|
constrained to K=4298 (close to physical limit of 3650).
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
import pandas as pd
|
|||
|
|
import numpy as np
|
|||
|
|
from scipy.optimize import curve_fit
|
|||
|
|
import matplotlib
|
|||
|
|
matplotlib.use('Agg')
|
|||
|
|
import matplotlib.pyplot as plt
|
|||
|
|
import warnings
|
|||
|
|
warnings.filterwarnings('ignore')
|
|||
|
|
|
|||
|
|
plt.rcParams['font.sans-serif'] = ['Arial Unicode MS', 'SimHei', 'DejaVu Sans']
|
|||
|
|
plt.rcParams['axes.unicode_minus'] = False
|
|||
|
|
|
|||
|
|
|
|||
|
|
def richards(t, K, r, t0, v):
|
|||
|
|
"""Richards curve (generalized logistic model)"""
|
|||
|
|
exp_term = np.exp(-r * (t - t0))
|
|||
|
|
exp_term = np.clip(exp_term, 1e-10, 1e10)
|
|||
|
|
return K / np.power(1 + exp_term, 1/v)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def richards_fixed_K(t, r, t0, v):
|
|||
|
|
"""Richards curve with fixed K=4298"""
|
|||
|
|
K = 4298
|
|||
|
|
exp_term = np.exp(-r * (t - t0))
|
|||
|
|
exp_term = np.clip(exp_term, 1e-10, 1e10)
|
|||
|
|
return K / np.power(1 + exp_term, 1/v)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def load_data(filepath="rocket_launch_counts.csv"):
|
|||
|
|
"""Load rocket launch data"""
|
|||
|
|
df = pd.read_csv(filepath)
|
|||
|
|
df = df.rename(columns={"YDate": "year", "Total": "launches"})
|
|||
|
|
df["year"] = pd.to_numeric(df["year"], errors="coerce")
|
|||
|
|
df["launches"] = pd.to_numeric(df["launches"], errors="coerce")
|
|||
|
|
df = df.dropna(subset=["year", "launches"])
|
|||
|
|
df = df[(df["year"] >= 1957) & (df["year"] <= 2025)]
|
|||
|
|
df = df.astype({"year": int, "launches": int})
|
|||
|
|
df = df.sort_values("year").reset_index(drop=True)
|
|||
|
|
return df
|
|||
|
|
|
|||
|
|
|
|||
|
|
def main():
|
|||
|
|
print("=" * 60)
|
|||
|
|
print("Richards S-Curve Fit (2010-2025 Data, K=4298)")
|
|||
|
|
print("=" * 60)
|
|||
|
|
|
|||
|
|
# Load data
|
|||
|
|
df = load_data()
|
|||
|
|
|
|||
|
|
# Filter 2010-2025
|
|||
|
|
start_year = 2010
|
|||
|
|
end_year = 2025
|
|||
|
|
data = df[(df["year"] >= start_year) & (df["year"] <= end_year)].copy()
|
|||
|
|
years = data["year"].values
|
|||
|
|
launches = data["launches"].values
|
|||
|
|
|
|||
|
|
print(f"Data range: {start_year} - {end_year}")
|
|||
|
|
print(f"Data points: {len(data)}")
|
|||
|
|
print(f"\nHistorical data:")
|
|||
|
|
for y, l in zip(years, launches):
|
|||
|
|
print(f" {y}: {l} launches")
|
|||
|
|
|
|||
|
|
# Fit with fixed K=4298
|
|||
|
|
K_fixed = 4298
|
|||
|
|
t = (years - start_year).astype(float)
|
|||
|
|
|
|||
|
|
p0 = [0.2, 20.0, 2.0] # r, t0, v
|
|||
|
|
bounds = ([0.01, 5, 0.5], [1.0, 100, 10.0])
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
popt, pcov = curve_fit(richards_fixed_K, t, launches, p0=p0, bounds=bounds, maxfev=100000)
|
|||
|
|
r, t0, v = popt
|
|||
|
|
|
|||
|
|
# Calculate R²
|
|||
|
|
y_pred = richards_fixed_K(t, *popt)
|
|||
|
|
ss_res = np.sum((launches - y_pred) ** 2)
|
|||
|
|
ss_tot = np.sum((launches - np.mean(launches)) ** 2)
|
|||
|
|
r_squared = 1 - (ss_res / ss_tot)
|
|||
|
|
|
|||
|
|
print(f"\nFitted Parameters (K fixed at {K_fixed}):")
|
|||
|
|
print(f" K (carrying capacity) = {K_fixed} launches/year (FIXED)")
|
|||
|
|
print(f" r (growth rate) = {r:.4f}")
|
|||
|
|
print(f" t0 (inflection point) = {start_year + t0:.1f}")
|
|||
|
|
print(f" v (shape parameter) = {v:.3f}")
|
|||
|
|
print(f" R² = {r_squared:.4f}")
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f"Fitting error: {e}")
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
# Physical limit
|
|||
|
|
physical_max = 3650
|
|||
|
|
print(f"\nPhysical limit: {physical_max} (10 sites × 365 days)")
|
|||
|
|
print(f"K / Physical limit = {K_fixed/physical_max:.2f}x")
|
|||
|
|
|
|||
|
|
# ========== Create Visualization ==========
|
|||
|
|
fig, ax = plt.subplots(figsize=(12, 7))
|
|||
|
|
|
|||
|
|
# Historical data points
|
|||
|
|
ax.scatter(years, launches, color='#2C3E50', s=100, alpha=0.9,
|
|||
|
|
label='Historical Data (2010-2025)', zorder=4, edgecolor='white', linewidth=1)
|
|||
|
|
|
|||
|
|
# Generate smooth S-curve
|
|||
|
|
years_smooth = np.linspace(start_year, 2080, 500)
|
|||
|
|
t_smooth = years_smooth - start_year
|
|||
|
|
pred_smooth = richards(t_smooth, K_fixed, r, t0, v)
|
|||
|
|
|
|||
|
|
# S-curve prediction
|
|||
|
|
ax.plot(years_smooth, pred_smooth, color='#27AE60', lw=3,
|
|||
|
|
label=f'Richards Model (K={K_fixed}, R²={r_squared:.3f})', zorder=2)
|
|||
|
|
|
|||
|
|
# K=4298 saturation line
|
|||
|
|
ax.axhline(K_fixed, color='#27AE60', ls=':', lw=2, alpha=0.7,
|
|||
|
|
label=f'K = {K_fixed} (Model Saturation)')
|
|||
|
|
|
|||
|
|
# Mark 2050 line only
|
|||
|
|
ax.axvline(2050, color='#3498DB', ls=':', lw=2, alpha=0.8)
|
|||
|
|
ax.text(2051, K_fixed*0.85, '2050\n(Target)', fontsize=10, color='#3498DB')
|
|||
|
|
|
|||
|
|
# Only show 2050 prediction point
|
|||
|
|
t_2050 = 2050 - start_year
|
|||
|
|
pred_2050 = richards(t_2050, K_fixed, r, t0, v)
|
|||
|
|
ax.scatter([2050], [pred_2050], color='#3498DB', s=80, marker='D', zorder=4)
|
|||
|
|
ax.annotate(f'{pred_2050:.0f}', xy=(2050, pred_2050),
|
|||
|
|
xytext=(2051, pred_2050+150),
|
|||
|
|
fontsize=9, color='#3498DB', fontweight='bold')
|
|||
|
|
|
|||
|
|
# Formatting
|
|||
|
|
ax.set_xlabel('Year', fontsize=12)
|
|||
|
|
ax.set_ylabel('Annual Launches', fontsize=12)
|
|||
|
|
ax.legend(loc='upper left', fontsize=10)
|
|||
|
|
ax.grid(True, alpha=0.3)
|
|||
|
|
ax.set_xlim(2008, 2080)
|
|||
|
|
ax.set_ylim(0, K_fixed * 1.15)
|
|||
|
|
|
|||
|
|
plt.tight_layout()
|
|||
|
|
plt.savefig('richards_curve_2010_K4298.png', dpi=150, bbox_inches='tight')
|
|||
|
|
plt.close()
|
|||
|
|
|
|||
|
|
print("\nPlot saved: richards_curve_2010_K4298.png")
|
|||
|
|
print("=" * 60)
|
|||
|
|
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
main()
|