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