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
Orbital Mechanics - Orbital propagation
Relativistic Effects - Relativistic corrections
High-Precision Ephemeris - Planetary positions