Atmospheric Modeling

This example demonstrates NRLMSISE-00 high-fidelity atmosphere 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

NRLMSISE-00
  • Naval Research Laboratory Mass Spectrometer and Incoherent Scatter Radar

  • High-fidelity empirical model from sea level to 1000+ km

  • Accounts for solar activity (F10.7) and geomagnetic storms (Ap)

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 NRLMSISE00()

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

Running the Example

python examples/atmospheric_modeling.py

See Also

  • orbital_mechanics - Orbital propagation

  • relativity_demo - Relativistic corrections

  • ephemeris_demo - Planetary positions