Atmospheric Modeling

This example demonstrates the simplified thermosphere model and drag calculations for analyzing satellite orbital decay and atmospheric interactions.

Overview

Atmospheric density models are essential for:

  • LEO satellite operations: Predicting orbital decay and drag

  • Reentry analysis: Determining vehicle heating and trajectory

  • Space weather effects: Understanding solar activity impacts

  • Mission planning: Fuel requirements for station-keeping

Key Scenarios

ISS Altitude Profile

Atmospheric density varies significantly with solar activity level, affecting ISS orbital decay rate and reboost requirements.

Satellite Drag

Drag coefficients depend on satellite geometry and atmospheric composition at orbital altitudes.

Temperature Profiles

The thermosphere temperature increases dramatically with altitude and varies with solar flux (F10.7) and geomagnetic activity (Ap).

Composition Transitions

The atmosphere transitions from molecular (N2, O2) to atomic (O, He, H) species with increasing altitude.

Orbital Decay: Atmospheric drag causes LEO satellites to gradually lose altitude, with decay rate depending on solar activity levels.

Models Covered

Simplified Thermosphere
  • Barometric per-species model with solar-activity (F10.7) and geomagnetic (Ap) inputs

  • Usable above ~200 km (within ~2x of published NRLMSISE-00 values); below ~86 km use the US Standard Atmosphere instead (gh-79)

US Standard Atmosphere 1976
  • Reference atmosphere up to 85 km

  • Useful for comparison and validation

Code Highlights

The example demonstrates:

  • Density profiles across altitude with SimplifiedThermosphere()

  • Species composition (N2, O2, O, He, H, Ar, N) vs altitude

  • Temperature profiles under various solar activity levels

  • Solar flux (F10.7) effects on ISS-altitude conditions

  • Comparison with US Standard Atmosphere 1976

Source Code

  1"""
  2Atmospheric Modeling and Orbital Decay Analysis
  3
  4Demonstrates the simplified thermosphere model and drag calculations
  5for analyzing satellite orbital decay and atmospheric interactions.
  6
  7Key scenarios:
  81. ISS altitude profile across different solar activity levels
  92. Satellite drag coefficient database
 103. Orbit decay simulation (LEO satellite)
 114. Temperature profile comparison across altitude range
 12"""
 13
 14import os
 15from pathlib import Path
 16
 17import numpy as np
 18import plotly.graph_objects as go
 19import plotly.subplots as sp
 20
 21from pytcl.atmosphere import (
 22    SimplifiedThermosphere,
 23    us_standard_atmosphere_1976,
 24)
 25
 26
 27def plot_density_vs_altitude():
 28    """
 29    Plot atmospheric density from the simplified thermosphere across altitude range.
 30
 31    Compares quiet and active solar activity conditions.
 32    """
 33    # Altitude range from sea level to 1000 km
 34    altitudes_km = np.concatenate(
 35        [
 36            np.linspace(0, 100, 50),  # Lower atmosphere (detailed)
 37            np.linspace(100, 1000, 100),  # Upper atmosphere
 38        ]
 39    )
 40    altitudes_m = altitudes_km * 1000
 41
 42    # Quiet solar activity (F107=70, Ap=0)
 43    model = SimplifiedThermosphere()
 44    output_quiet = model(
 45        latitude=np.radians(45) * np.ones_like(altitudes_m),
 46        longitude=np.radians(-75) * np.ones_like(altitudes_m),
 47        altitude=altitudes_m,
 48        year=2024,
 49        day_of_year=100,
 50        seconds_in_day=43200,
 51        f107=70,
 52        f107a=70,
 53        ap=0,
 54    )
 55
 56    # Active solar activity (F107=200, Ap=50)
 57    output_active = model(
 58        latitude=np.radians(45) * np.ones_like(altitudes_m),
 59        longitude=np.radians(-75) * np.ones_like(altitudes_m),
 60        altitude=altitudes_m,
 61        year=2024,
 62        day_of_year=100,
 63        seconds_in_day=43200,
 64        f107=200,
 65        f107a=200,
 66        ap=50,
 67    )
 68
 69    # US Standard Atmosphere for comparison (up to 85 km only)
 70    altitudes_short = altitudes_km[altitudes_km <= 85]
 71    output_us76 = np.array(
 72        [us_standard_atmosphere_1976(h * 1000).density for h in altitudes_short]
 73    )
 74
 75    fig = sp.make_subplots(rows=1, cols=1, specs=[[{"secondary_y": True}]])
 76
 77    # Log-scale density plot
 78    fig.add_trace(
 79        go.Scatter(
 80            x=altitudes_km,
 81            y=np.maximum(output_quiet.density, 1e-20),
 82            name="Quiet (F107=70, Ap=0)",
 83            mode="lines",
 84            line=dict(color="blue", width=2),
 85            hovertemplate="<b>Density (Quiet)</b><br>Alt: %{x:.1f} km<br>rho: %{y:.2e} kg/m³",
 86        ),
 87        secondary_y=False,
 88    )
 89
 90    fig.add_trace(
 91        go.Scatter(
 92            x=altitudes_km,
 93            y=np.maximum(output_active.density, 1e-20),
 94            name="Active (F107=200, Ap=50)",
 95            mode="lines",
 96            line=dict(color="red", width=2, dash="dash"),
 97            hovertemplate="<b>Density (Active)</b><br>Alt: %{x:.1f} km<br>rho: %{y:.2e} kg/m³",
 98        ),
 99        secondary_y=False,
100    )
101
102    # US 76 for reference
103    fig.add_trace(
104        go.Scatter(
105            x=altitudes_short,
106            y=np.maximum(output_us76, 1e-20),
107            name="US Standard 1976",
108            mode="lines",
109            line=dict(color="green", width=2, dash="dot"),
110            hovertemplate="<b>Density (US76)</b><br>Alt: %{x:.1f} km<br>rho: %{y:.2e} kg/m³",
111        ),
112        secondary_y=False,
113    )
114
115    fig.update_yaxes(type="log", title_text="Density (kg/m³)", secondary_y=False)
116
117    fig.update_xaxes(title_text="Altitude (km)", range=[0, 1000])
118
119    fig.update_layout(
120        title="Atmospheric Density vs. Altitude<br><sub>Simplified Thermosphere (Quiet vs. Active Solar Activity)</sub>",
121        hovermode="x unified",
122        height=600,
123        template="plotly_white",
124    )
125
126    return fig
127
128
129def plot_composition_profile():
130    """
131    Plot atmospheric composition (species densities) vs. altitude.
132    """
133    # Altitude range
134    altitudes_km = np.linspace(80, 500, 200)
135    altitudes_m = altitudes_km * 1000
136
137    model = SimplifiedThermosphere()
138    output = model(
139        latitude=np.zeros_like(altitudes_m),
140        longitude=np.zeros_like(altitudes_m),
141        altitude=altitudes_m,
142        year=2024,
143        day_of_year=1,
144        seconds_in_day=0,
145        f107=150,
146        f107a=150,
147        ap=5,
148    )
149
150    fig = go.Figure()
151
152    # Plot each species
153    species = [
154        ("N2", output.n2_density, "blue"),
155        ("O2", output.o2_density, "green"),
156        ("O", output.o_density, "red"),
157        ("He", output.he_density, "purple"),
158        ("H", output.h_density, "orange"),
159        ("Ar", output.ar_density, "brown"),
160        ("N", output.n_density, "pink"),
161    ]
162
163    for name, density, color in species:
164        fig.add_trace(
165            go.Scatter(
166                x=altitudes_km,
167                y=np.maximum(density, 1e8),
168                name=name,
169                mode="lines",
170                line=dict(color=color, width=2.5),
171                hovertemplate=f"<b>{name}</b><br>Alt: %{{x:.1f}} km<br>n: %{{y:.2e}} m⁻³",
172            )
173        )
174
175    fig.update_yaxes(type="log", title_text="Number Density (m^-³)")
176
177    fig.update_xaxes(title_text="Altitude (km)")
178
179    fig.update_layout(
180        title="Atmospheric Composition vs. Altitude<br><sub>Simplified Thermosphere at F107=150, Ap=5</sub>",
181        height=600,
182        hovermode="x unified",
183        template="plotly_white",
184    )
185
186    return fig
187
188
189def plot_temperature_profile():
190    """
191    Plot temperature vs. altitude across full range.
192    """
193    altitudes_km = np.concatenate(
194        [np.linspace(0, 100, 50), np.linspace(100, 1000, 100)]
195    )
196    altitudes_m = altitudes_km * 1000
197
198    model = SimplifiedThermosphere()
199
200    # Different solar activity levels
201    conditions = [
202        ("Quiet (F107=70)", 70, 70, 0, "blue"),
203        ("Moderate (F107=150)", 150, 150, 5, "green"),
204        ("Active (F107=200)", 200, 200, 50, "orange"),
205        ("Storm (F107=150, Ap=300)", 150, 150, 300, "red"),
206    ]
207
208    fig = go.Figure()
209
210    for label, f107, f107a, ap, color in conditions:
211        output = model(
212            latitude=np.zeros_like(altitudes_m),
213            longitude=np.zeros_like(altitudes_m),
214            altitude=altitudes_m,
215            year=2024,
216            day_of_year=1,
217            seconds_in_day=0,
218            f107=f107,
219            f107a=f107a,
220            ap=ap,
221        )
222
223        fig.add_trace(
224            go.Scatter(
225                x=output.temperature,
226                y=altitudes_km,
227                name=label,
228                mode="lines",
229                line=dict(color=color, width=2.5),
230                hovertemplate="<b>"
231                + label
232                + "</b><br>T: %{x:.0f} K<br>Alt: %{y:.1f} km",
233            )
234        )
235
236    fig.update_yaxes(title_text="Altitude (km)", range=[0, 500])
237
238    fig.update_xaxes(title_text="Temperature (K)", range=[150, 1100])
239
240    fig.update_layout(
241        title="Temperature Profile vs. Altitude<br><sub>Simplified Thermosphere Under Various Solar Activity Levels</sub>",
242        height=600,
243        hovermode="x unified",
244        template="plotly_white",
245    )
246
247    return fig
248
249
250def plot_solar_activity_effect():
251    """
252    Plot density response to varying solar activity index (F107).
253    """
254    # Fixed altitude ISS orbit
255    iss_altitude = 408_000  # meters
256
257    # Vary F107 from quiet to stormy
258    f107_range = np.linspace(50, 300, 50)
259
260    model = SimplifiedThermosphere()
261    densities = []
262    temperatures = []
263
264    for f107 in f107_range:
265        output = model(
266            latitude=np.radians(51.6),  # ISS inclination
267            longitude=np.radians(0),
268            altitude=iss_altitude,
269            year=2024,
270            day_of_year=1,
271            seconds_in_day=0,
272            f107=f107,
273            f107a=f107,
274            ap=5,
275        )
276        densities.append(output.density)
277        temperatures.append(output.temperature)
278
279    fig = sp.make_subplots(
280        rows=1, cols=2, specs=[[{"secondary_y": False}, {"secondary_y": False}]]
281    )
282
283    # Density vs F107
284    fig.add_trace(
285        go.Scatter(
286            x=f107_range,
287            y=densities,
288            name="Density",
289            mode="lines+markers",
290            line=dict(color="blue", width=2),
291            marker=dict(size=4),
292            hovertemplate="F107: %{x:.0f} SFU<br>rho: %{y:.2e} kg/m³",
293        ),
294        row=1,
295        col=1,
296    )
297
298    # Temperature vs F107
299    fig.add_trace(
300        go.Scatter(
301            x=f107_range,
302            y=temperatures,
303            name="Temperature",
304            mode="lines+markers",
305            line=dict(color="red", width=2),
306            marker=dict(size=4),
307            hovertemplate="F107: %{x:.0f} SFU<br>T: %{y:.0f} K",
308        ),
309        row=1,
310        col=2,
311    )
312
313    fig.update_xaxes(title_text="Solar Flux F107 (SFU)", row=1, col=1)
314    fig.update_xaxes(title_text="Solar Flux F107 (SFU)", row=1, col=2)
315    fig.update_yaxes(title_text="Density at 408 km (kg/m³)", type="log", row=1, col=1)
316    fig.update_yaxes(title_text="Temperature at 408 km (K)", row=1, col=2)
317
318    fig.update_layout(
319        title_text="Effect of Solar Activity on Atmospheric Conditions at ISS Altitude",
320        height=500,
321        hovermode="x unified",
322        template="plotly_white",
323    )
324
325    return fig
326
327
328def plot_composition_transitions():
329    """
330    Plot composition transition from molecular to atomic atmosphere.
331    """
332    altitudes_km = np.linspace(70, 300, 300)
333    altitudes_m = altitudes_km * 1000
334
335    model = SimplifiedThermosphere()
336    output = model(
337        latitude=np.zeros_like(altitudes_m),
338        longitude=np.zeros_like(altitudes_m),
339        altitude=altitudes_m,
340        year=2024,
341        day_of_year=1,
342        seconds_in_day=0,
343        f107=150,
344        f107a=150,
345        ap=5,
346    )
347
348    # Calculate composition percentages
349    total_dens = (
350        output.n2_density
351        + output.o2_density
352        + output.o_density
353        + output.he_density
354        + output.h_density
355        + output.ar_density
356        + output.n_density
357    )
358
359    fig = go.Figure()
360
361    # Stacked area chart (as percentages)
362    species_data = [
363        ("N2", output.n2_density / total_dens * 100, "blue"),
364        ("O2", output.o2_density / total_dens * 100, "green"),
365        ("O", output.o_density / total_dens * 100, "red"),
366        ("He", output.he_density / total_dens * 100, "purple"),
367        ("H", output.h_density / total_dens * 100, "orange"),
368        ("Other", (output.ar_density + output.n_density) / total_dens * 100, "gray"),
369    ]
370
371    for name, fractions, color in species_data:
372        fig.add_trace(
373            go.Scatter(
374                x=altitudes_km,
375                y=fractions,
376                name=name,
377                mode="lines",
378                line=dict(width=0.5, color=color),
379                fillcolor=color,
380                fill="tonexty" if name != "N2" else "tozeroy",
381                hovertemplate=f"<b>{name}</b><br>Alt: %{{x:.1f}} km<br>Fraction: %{{y:.1f}}%",
382            )
383        )
384
385    fig.update_yaxes(title_text="Composition (%)", range=[0, 100])
386
387    fig.update_xaxes(title_text="Altitude (km)")
388
389    fig.update_layout(
390        title="Atmospheric Composition Transition<br><sub>Molecular -> Atomic Atmosphere (70-300 km)</sub>",
391        height=600,
392        hovermode="x unified",
393        template="plotly_white",
394    )
395
396    return fig
397
398
399if __name__ == "__main__":
400    print("Generating thermosphere modeling visualizations...")
401
402    # Create output directory
403    output_dir = os.path.join(os.path.dirname(__file__), "output")
404    os.makedirs(output_dir, exist_ok=True)
405
406    # Generate all plots
407    fig1 = plot_density_vs_altitude()
408    output_path1 = os.path.join(output_dir, "thermosphere_density.html")
409    fig1.write_html(
410        output_path1, include_plotlyjs="cdn", div_id=Path(output_path1).stem
411    )
412    print(f"OK Saved: {output_path1}")
413
414    fig2 = plot_composition_profile()
415    output_path2 = os.path.join(output_dir, "thermosphere_composition.html")
416    fig2.write_html(
417        output_path2, include_plotlyjs="cdn", div_id=Path(output_path2).stem
418    )
419    print(f"OK Saved: {output_path2}")
420
421    fig3 = plot_temperature_profile()
422    output_path3 = os.path.join(output_dir, "thermosphere_temperature.html")
423    fig3.write_html(
424        output_path3, include_plotlyjs="cdn", div_id=Path(output_path3).stem
425    )
426    print(f"OK Saved: {output_path3}")
427
428    fig4 = plot_solar_activity_effect()
429    output_path4 = os.path.join(output_dir, "thermosphere_solar_activity.html")
430    fig4.write_html(
431        output_path4, include_plotlyjs="cdn", div_id=Path(output_path4).stem
432    )
433    print(f"OK Saved: {output_path4}")
434
435    fig5 = plot_composition_transitions()
436    output_path5 = os.path.join(output_dir, "thermosphere_composition_transition.html")
437    fig5.write_html(
438        output_path5, include_plotlyjs="cdn", div_id=Path(output_path5).stem
439    )
440    print(f"OK Saved: {output_path5}")
441
442    print("\nAll visualizations complete!")
443    print("View the HTML files in a browser to interact with the plots.")

Running the Example

python examples/atmospheric_modeling.py

See Also