Geophysical Models
This example demonstrates gravity and magnetic field models essential for high-precision navigation, geodesy, and aerospace applications.
Overview
Geophysical models are critical for:
Inertial navigation: Gravity compensation in INS
Geodesy: Height reference and surveying
Aerospace: Satellite orbit determination
Geophysics: Subsurface exploration
Gravity Models
- Normal Gravity (Somigliana)
Gravity variation with latitude
~0.5% increase from equator to poles
Due to Earth’s rotation and flattening
- WGS84 Gravity Model
Full gravity vector computation
Includes deflection of vertical
Essential for inertial navigation
- J2 Gravity Model
Simplified model using J2 oblateness term
Adequate for many applications
Faster computation than full models
- Geoid Height
Separation between geoid and ellipsoid
J2 approximation captures main flattening
Full models (EGM96/2008) for precision
- Gravity Anomalies
Free-air anomaly
Gravity disturbance
Used for geophysical exploration
- Tidal Effects
Solid Earth tide displacement (~30 cm)
Tidal gravity variations (~300 uGal)
Essential for precision gravimetry
Magnetic Field Models
- World Magnetic Model (WMM2020)
Standard model for navigation
Updated every 5 years
Valid 2020-2025
- IGRF-14 (IGRF-13 retained for reproducibility)
International Geomagnetic Reference Field
Historical (1900.0-2025.0) and predictive (2025-30) coefficients
Used for scientific applications
- Key Parameters
Declination: angle between true and magnetic north
Inclination: dip angle of field lines
Total intensity: field strength in nT
- Notable Features
South Atlantic Anomaly: weak field region
Magnetic poles: ~11° offset from geographic
Secular variation: field changes over time
Code Highlights
The example demonstrates:
Normal gravity with
normal_gravity_somigliana()WGS84 gravity with
gravity_wgs84()Geoid height with
geoid_height_j2()Free-air anomaly with
free_air_anomaly()Solid Earth tides with
solid_earth_tide_displacement()Magnetic field with
wmm()andigrf()Magnetic declination with
magnetic_declination()
Source Code
1"""
2Geophysical Models Example
3==========================
4
5This example demonstrates the geophysical models in PyTCL:
6
7Gravity Models:
8- Normal gravity (Somigliana formula)
9- WGS84 and J2 gravity models
10- Spherical harmonic expansions
11- Geoid height computation
12- Gravity anomalies and disturbances
13- Tidal effects (solid Earth, ocean loading)
14
15Magnetic Field Models:
16- World Magnetic Model (WMM2020)
17- International Geomagnetic Reference Field (IGRF-13)
18- Enhanced Magnetic Model (EMM)
19- Magnetic declination, inclination, and intensity
20
21These models are essential for high-precision navigation, geodesy,
22and aerospace applications.
23"""
24
25from pathlib import Path
26
27import numpy as np
28import plotly.graph_objects as go
29from plotly.subplots import make_subplots
30
31# Output directory for generated plots
32OUTPUT_DIR = Path(__file__).parent.parent / "docs" / "_static" / "images" / "examples"
33OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
34
35# Global flag to control plotting
36SHOW_PLOTS = True
37
38
39from pytcl.gravity import ( # Normal gravity; Gravity models; Geoid; Anomalies; Tidal effects; Constants
40 GRS80,
41 WGS84,
42 free_air_anomaly,
43 geoid_height,
44 geoid_height_j2,
45 gravity_anomaly,
46 gravity_disturbance,
47 gravity_j2,
48 gravity_wgs84,
49 normal_gravity,
50 normal_gravity_somigliana,
51 solid_earth_tide_displacement,
52 solid_earth_tide_gravity,
53 tidal_gravity_correction,
54)
55from pytcl.magnetism import ( # World Magnetic Model; IGRF
56 WMM2020,
57 WMM2025,
58 dipole_moment,
59 igrf,
60 igrf_declination,
61 igrf_inclination,
62 magnetic_declination,
63 magnetic_field_intensity,
64 magnetic_inclination,
65 magnetic_north_pole,
66 wmm,
67)
68
69
70def demo_normal_gravity():
71 """Demonstrate normal gravity computation."""
72 print("=" * 70)
73 print("Normal Gravity Demo")
74 print("=" * 70)
75
76 # Test locations at different latitudes
77 locations = [
78 ("Equator", 0.0, 0.0, 0.0),
79 ("Washington DC", 38.9, -77.0, 0.0),
80 ("North Pole", 90.0, 0.0, 0.0),
81 ("Mount Everest", 27.99, 86.93, 8848.0),
82 ("Dead Sea", 31.5, 35.5, -430.0),
83 ]
84
85 print("\nNormal gravity at various locations:")
86 print("-" * 60)
87 print(f"{'Location':<20} {'Lat':>8} {'Alt (m)':>10} {'g (m/s²)':>12}")
88 print("-" * 60)
89
90 for name, lat, lon, alt in locations:
91 # Normal gravity at sea level
92 g_somigliana = normal_gravity_somigliana(np.radians(lat))
93
94 # Gravity at altitude (free-air correction)
95 g_at_alt = normal_gravity(np.radians(lat), alt)
96
97 print(f"{name:<20} {lat:>8.2f} {alt:>10.1f} {g_at_alt:>12.6f}")
98
99 # Show latitude variation
100 print("\n--- Latitude Variation ---")
101 lats = np.array([0, 15, 30, 45, 60, 75, 90])
102 for lat in lats:
103 g = normal_gravity_somigliana(np.radians(lat))
104 print(f" {lat:>2}°: {g:.6f} m/s²")
105
106 print("\nNote: Gravity increases from equator to poles due to")
107 print("Earth's rotation (centrifugal) and flattening effects.")
108
109 # Plot gravity variation with latitude
110 if SHOW_PLOTS:
111 # High-resolution latitude array
112 lats_fine = np.linspace(0, 90, 181)
113 g_values = np.array(
114 [normal_gravity_somigliana(np.radians(lat)) for lat in lats_fine]
115 )
116
117 fig = make_subplots(
118 rows=1,
119 cols=2,
120 subplot_titles=(
121 "Normal Gravity vs Latitude (Somigliana)",
122 "Gravity Increase from Equator",
123 ),
124 )
125
126 # Left plot: Gravity vs latitude
127 fig.add_trace(
128 go.Scatter(
129 x=lats_fine,
130 y=g_values,
131 mode="lines",
132 name="Gravity",
133 line=dict(color="blue", width=2),
134 ),
135 row=1,
136 col=1,
137 )
138 # Mark equator and pole values
139 fig.add_hline(
140 y=g_values[0],
141 line_dash="dash",
142 line_color="red",
143 annotation_text=f"Equator: {g_values[0]:.4f}",
144 row=1,
145 col=1,
146 )
147 fig.add_hline(
148 y=g_values[-1],
149 line_dash="dash",
150 line_color="green",
151 annotation_text=f"Pole: {g_values[-1]:.4f}",
152 row=1,
153 col=1,
154 )
155
156 # Right plot: Gravity difference from equator
157 g_diff = (g_values - g_values[0]) * 1000 # mGal
158 fig.add_trace(
159 go.Scatter(
160 x=lats_fine,
161 y=g_diff,
162 mode="lines",
163 name="Δg",
164 line=dict(color="blue", width=2),
165 ),
166 row=1,
167 col=2,
168 )
169
170 fig.update_xaxes(title_text="Latitude (°)", range=[0, 90], row=1, col=1)
171 fig.update_yaxes(title_text="Normal Gravity (m/s²)", row=1, col=1)
172 fig.update_xaxes(title_text="Latitude (°)", range=[0, 90], row=1, col=2)
173 fig.update_yaxes(title_text="Δg from Equator (mGal)", row=1, col=2)
174
175 fig.update_layout(height=500, width=1200, showlegend=False)
176 fig.write_html(
177 str(OUTPUT_DIR / "geophysical_gravity_latitude.html"),
178 include_plotlyjs="cdn",
179 div_id="geophysical_gravity_latitude",
180 )
181 print("\n [Plot saved to geophysical_gravity_latitude.html]")
182
183
184def demo_gravity_models():
185 """Demonstrate different gravity models."""
186 print("\n" + "=" * 70)
187 print("Gravity Models Comparison Demo")
188 print("=" * 70)
189
190 # Test point: Washington DC
191 lat = np.radians(38.9)
192 lon = np.radians(-77.0)
193 alt = 0.0 # Sea level
194
195 print(f"\nLocation: Washington DC (38.9°N, 77.0°W)")
196 print("-" * 50)
197
198 # WGS84 gravity model
199 g_wgs84 = gravity_wgs84(lat, lon, alt)
200 print(f"\nWGS84 gravity model:")
201 print(f" Total gravity: {g_wgs84.magnitude:.6f} m/s²")
202 print(f" Down component: {g_wgs84.g_down:.6f} m/s²")
203 print(f" North component: {g_wgs84.g_north:.9f} m/s²")
204
205 # J2 gravity model (simpler, uses only J2 term)
206 g_j2 = gravity_j2(lat, lon, alt)
207 print(f"\nJ2 gravity model:")
208 print(f" Total gravity: {g_j2.magnitude:.6f} m/s²")
209 print(
210 f" Difference from WGS84: {(g_j2.magnitude - g_wgs84.magnitude) * 1e6:.2f} µGal"
211 )
212
213 # Compare at different altitudes
214 print("\n--- Gravity vs Altitude ---")
215 altitudes = [0, 1000, 10000, 100000, 400000] # meters
216 for alt in altitudes:
217 g = gravity_wgs84(lat, lon, alt)
218 print(f" {alt:>7} m: {g.magnitude:.6f} m/s²")
219
220 print("\nNote: Gravity decreases with altitude approximately as")
221 print("g(h) ~= g0(1 - 2h/R), about 3.1 mGal per meter at low altitudes.")
222
223 # Plot gravity vs altitude
224 if SHOW_PLOTS:
225 fig = make_subplots(
226 rows=1,
227 cols=2,
228 subplot_titles=(
229 "Gravity vs Altitude (WGS84)",
230 "Gravity Reduction with Altitude",
231 ),
232 )
233
234 # Altitude range from surface to ISS altitude
235 alts = np.linspace(0, 500000, 500) # 0 to 500 km
236 g_values = np.array([gravity_wgs84(lat, lon, a).magnitude for a in alts])
237
238 # Left plot: Gravity vs altitude
239 fig.add_trace(
240 go.Scatter(
241 x=alts / 1000,
242 y=g_values,
243 mode="lines",
244 name="Gravity",
245 line=dict(color="blue", width=2),
246 ),
247 row=1,
248 col=1,
249 )
250
251 # Right plot: Gravity reduction rate
252 g_reduction = (g_values[0] - g_values) / g_values[0] * 100 # percent
253 fig.add_trace(
254 go.Scatter(
255 x=alts / 1000,
256 y=g_reduction,
257 mode="lines",
258 name="Reduction",
259 line=dict(color="red", width=2),
260 ),
261 row=1,
262 col=2,
263 )
264
265 # Mark ISS at ~400 km
266 fig.add_hline(
267 y=g_reduction[400],
268 line_dash="dash",
269 line_color="green",
270 annotation_text=f"ISS (~400 km): {g_reduction[400]:.1f}% reduction",
271 row=1,
272 col=2,
273 )
274
275 fig.update_xaxes(title_text="Altitude (km)", row=1, col=1)
276 fig.update_yaxes(title_text="Gravity (m/s²)", row=1, col=1)
277 fig.update_xaxes(title_text="Altitude (km)", row=1, col=2)
278 fig.update_yaxes(title_text="Gravity Reduction (%)", row=1, col=2)
279
280 fig.update_layout(height=500, width=1200, showlegend=False)
281 fig.write_html(
282 str(OUTPUT_DIR / "geophysical_gravity_altitude.html"),
283 include_plotlyjs="cdn",
284 div_id="geophysical_gravity_altitude",
285 )
286 print("\n [Plot saved to geophysical_gravity_altitude.html]")
287
288
289def demo_geoid():
290 """Demonstrate geoid height computation."""
291 print("\n" + "=" * 70)
292 print("Geoid Height Demo")
293 print("=" * 70)
294
295 # Various latitudes (J2 geoid approximation is zonal - only latitude dependent)
296 locations = [
297 ("Equator", 0.0),
298 ("Mid-latitude (30N)", 30.0),
299 ("Mid-latitude (45N)", 45.0),
300 ("High latitude (60N)", 60.0),
301 ("Pole", 90.0),
302 ]
303
304 print("\nJ2 Geoid heights (zonal approximation - latitude dependent only):")
305 print("-" * 40)
306 print(f"{'Location':<25} {'Lat':>7} {'N (m)':>10}")
307 print("-" * 40)
308
309 for name, lat in locations:
310 # J2 geoid approximation - only depends on latitude
311 N = geoid_height_j2(np.radians(lat))
312 print(f"{name:<25} {lat:>7.1f} {N:>10.2f}")
313
314 print("\nNote: The J2 geoid model captures the main flattening effect.")
315 print("Real geoid variations are ±100m from the ellipsoid (need EGM96/2008).")
316
317
318def demo_gravity_anomalies():
319 """Demonstrate gravity anomaly computation."""
320 print("\n" + "=" * 70)
321 print("Gravity Anomalies Demo")
322 print("=" * 70)
323
324 # Simulated gravity survey
325 np.random.seed(42)
326
327 # Locations along a survey line
328 n_points = 5
329 lats = np.linspace(38.0, 39.0, n_points)
330 lons = np.full(n_points, -77.0)
331 alts = np.array([100, 150, 200, 180, 120]) # meters
332
333 # Simulated observed gravity (with anomaly)
334 g_normal = np.array(
335 [normal_gravity(np.radians(lat), alt) for lat, alt in zip(lats, alts)]
336 )
337 # Add a gravity anomaly (e.g., from subsurface density variation)
338 anomaly_true = np.array([0.0, 10.0, 25.0, 15.0, 5.0]) * 1e-5 # m/s²
339 g_observed = g_normal + anomaly_true
340
341 print("\nGravity survey along profile:")
342 print("-" * 65)
343 print(f"{'Point':>5} {'Lat':>7} {'Alt(m)':>7} {'g_obs':>12} {'FAA':>12}")
344 print("-" * 65)
345
346 for i in range(n_points):
347 lat_rad = np.radians(lats[i])
348 faa = free_air_anomaly(g_observed[i], lat_rad, alts[i])
349 print(
350 f"{i + 1:>5} {lats[i]:>7.2f} {alts[i]:>7.0f} "
351 f"{g_observed[i]:>12.6f} {faa * 1e5:>12.2f} mGal"
352 )
353
354 print("\nNote: Free-air anomaly removes the effect of elevation")
355 print("to reveal subsurface density variations.")
356
357
358def demo_tidal_effects():
359 """Demonstrate tidal effects on gravity and position."""
360 print("\n" + "=" * 70)
361 print("Tidal Effects Demo")
362 print("=" * 70)
363
364 # Observer location
365 lat = np.radians(38.9) # Washington DC
366 lon = np.radians(-77.0)
367
368 # Time (Julian date) - approximate
369 # Let's use a few different times
370 times = [
371 ("2025-01-01 00:00 UTC", 2460676.5),
372 ("2025-01-01 06:00 UTC", 2460676.75),
373 ("2025-01-01 12:00 UTC", 2460677.0),
374 ("2025-01-01 18:00 UTC", 2460677.25),
375 ]
376
377 print(f"\nLocation: Washington DC (38.9°N, 77.0°W)")
378 print("\nSolid Earth tide effects (displacement and gravity):")
379 print("-" * 60)
380
381 for time_str, jd in times:
382 # Solid Earth tide displacement
383 disp = solid_earth_tide_displacement(lat, lon, jd)
384
385 # Tidal gravity correction
386 dg = tidal_gravity_correction(lat, lon, jd)
387
388 print(f"\n{time_str}:")
389 print(
390 f" Displacement: ({disp[0] * 1000:.1f}, {disp[1] * 1000:.1f}, "
391 f"{disp[2] * 1000:.1f}) mm (N, E, Up)"
392 )
393 print(f" Gravity change: {dg * 1e8:.2f} µGal")
394
395 print("\nNote: Solid Earth tides cause displacements of ~30 cm")
396 print("and gravity changes of ~300 µGal peak-to-peak.")
397
398 # Plot tidal effects over 24 hours
399 if SHOW_PLOTS:
400 # Time array: 24 hours at 15-minute intervals
401 hours = np.linspace(0, 24, 97)
402 jd_start = 2460676.5 # 2025-01-01 00:00 UTC
403 jds = jd_start + hours / 24
404
405 # Compute displacements and gravity changes
406 disp_n = np.zeros_like(hours)
407 disp_e = np.zeros_like(hours)
408 disp_u = np.zeros_like(hours)
409 dg = np.zeros_like(hours)
410
411 for i, jd in enumerate(jds):
412 disp = solid_earth_tide_displacement(lat, lon, jd)
413 disp_n[i] = disp[0] * 1000 # mm
414 disp_e[i] = disp[1] * 1000
415 disp_u[i] = disp[2] * 1000
416 dg[i] = tidal_gravity_correction(lat, lon, jd) * 1e8 # µGal
417
418 fig = make_subplots(
419 rows=2,
420 cols=1,
421 subplot_titles=(
422 "Solid Earth Tide - Washington DC (2025-01-01)",
423 "Tidal Gravity Variation",
424 ),
425 shared_xaxes=True,
426 )
427
428 # Top plot: Displacement components
429 fig.add_trace(
430 go.Scatter(
431 x=hours,
432 y=disp_n,
433 mode="lines",
434 name="North",
435 line=dict(color="blue", width=1.5),
436 ),
437 row=1,
438 col=1,
439 )
440 fig.add_trace(
441 go.Scatter(
442 x=hours,
443 y=disp_e,
444 mode="lines",
445 name="East",
446 line=dict(color="green", width=1.5),
447 ),
448 row=1,
449 col=1,
450 )
451 fig.add_trace(
452 go.Scatter(
453 x=hours,
454 y=disp_u,
455 mode="lines",
456 name="Up",
457 line=dict(color="red", width=2),
458 ),
459 row=1,
460 col=1,
461 )
462 fig.add_hline(y=0, line_color="black", line_width=0.5, row=1, col=1)
463
464 # Bottom plot: Gravity change
465 fig.add_trace(
466 go.Scatter(
467 x=hours,
468 y=dg,
469 mode="lines",
470 name="Gravity",
471 line=dict(color="purple", width=2),
472 fill="tozeroy",
473 fillcolor="rgba(128,0,128,0.3)",
474 ),
475 row=2,
476 col=1,
477 )
478 fig.add_hline(y=0, line_color="black", line_width=0.5, row=2, col=1)
479
480 fig.update_xaxes(title_text="Hour (UTC)", range=[0, 24], row=2, col=1)
481 fig.update_yaxes(title_text="Displacement (mm)", row=1, col=1)
482 fig.update_yaxes(title_text="Gravity Change (µGal)", row=2, col=1)
483
484 fig.update_layout(height=600, width=1000)
485 fig.write_html(
486 str(OUTPUT_DIR / "geophysical_tides.html"),
487 include_plotlyjs="cdn",
488 div_id="geophysical_tides",
489 )
490 print("\n [Plot saved to geophysical_tides.html]")
491
492
493def demo_magnetic_field():
494 """Demonstrate magnetic field computation."""
495 print("\n" + "=" * 70)
496 print("Magnetic Field Models Demo")
497 print("=" * 70)
498
499 # Test locations
500 locations = [
501 ("Washington DC", 38.9, -77.0, 0.0),
502 ("London", 51.5, -0.1, 0.0),
503 ("Sydney", -33.9, 151.2, 0.0),
504 ("Magnetic North", 86.5, -175.3, 0.0),
505 ("Magnetic Equator", 0.0, 0.0, 0.0),
506 ("South Atlantic Anomaly", -25.0, -50.0, 0.0),
507 ]
508
509 # Use 2025 epoch
510 decimal_year = 2025.0
511
512 print(f"\nMagnetic field at various locations (WMM2020, {decimal_year}):")
513 print("-" * 75)
514 print(f"{'Location':<25} {'Dec':>8} {'Inc':>8} {'F (nT)':>10}")
515 print("-" * 75)
516
517 for name, lat, lon, alt in locations:
518 # Compute magnetic field using WMM
519 result = wmm(np.radians(lat), np.radians(lon), alt, decimal_year)
520
521 dec = np.degrees(result.D) # Declination
522 inc = np.degrees(result.I) # Inclination
523 F = result.F # Total intensity
524
525 print(f"{name:<25} {dec:>8.2f}° {inc:>8.2f}° {F:>10.0f}")
526
527 # Show magnetic declination map concept
528 print("\n--- Magnetic Declination Grid ---")
529 lats_grid = np.array([-60, -30, 0, 30, 60])
530 lons_grid = np.array([-120, -60, 0, 60, 120])
531
532 header = "Lat\\Lon"
533 print(f"{header:<8}", end="")
534 for lon in lons_grid:
535 print(f"{lon:>8}°", end="")
536 print()
537
538 for lat in lats_grid:
539 print(f"{lat:>6}° ", end="")
540 for lon in lons_grid:
541 dec = magnetic_declination(
542 np.radians(lat), np.radians(lon), 0.0, decimal_year
543 )
544 print(f"{np.degrees(dec):>8.1f}", end="")
545 print()
546
547 # Plot magnetic declination and field intensity maps
548 if SHOW_PLOTS:
549 # Create grid for plotting
550 lat_grid = np.linspace(-80, 80, 33)
551 lon_grid = np.linspace(-180, 180, 73)
552 LAT, LON = np.meshgrid(lat_grid, lon_grid)
553
554 # Compute declination and intensity on grid
555 DEC = np.zeros_like(LAT)
556 F = np.zeros_like(LAT)
557 for i in range(LAT.shape[0]):
558 for j in range(LAT.shape[1]):
559 result = wmm(
560 np.radians(LAT[i, j]), np.radians(LON[i, j]), 0.0, decimal_year
561 )
562 DEC[i, j] = np.degrees(result.D)
563 F[i, j] = result.F
564
565 fig = make_subplots(
566 rows=1,
567 cols=2,
568 subplot_titles=(
569 f"Magnetic Declination (WMM {decimal_year:.0f})",
570 f"Magnetic Field Intensity (WMM {decimal_year:.0f})",
571 ),
572 )
573
574 # Left plot: Magnetic declination
575 fig.add_trace(
576 go.Contour(
577 x=lon_grid,
578 y=lat_grid,
579 z=DEC.T,
580 colorscale="RdBu_r",
581 contours=dict(showlines=True),
582 colorbar=dict(title="Declination (°)", x=0.45),
583 ),
584 row=1,
585 col=1,
586 )
587
588 # Right plot: Total field intensity
589 fig.add_trace(
590 go.Contour(
591 x=lon_grid,
592 y=lat_grid,
593 z=F.T,
594 colorscale="Viridis",
595 colorbar=dict(title="Intensity (nT)", x=1.0),
596 ),
597 row=1,
598 col=2,
599 )
600
601 # Mark South Atlantic Anomaly region
602 fig.add_trace(
603 go.Scatter(
604 x=[-50],
605 y=[-25],
606 mode="markers",
607 marker=dict(symbol="star", size=15, color="red"),
608 name="South Atlantic Anomaly",
609 showlegend=True,
610 ),
611 row=1,
612 col=2,
613 )
614
615 fig.update_xaxes(title_text="Longitude (°)", range=[-180, 180], row=1, col=1)
616 fig.update_yaxes(title_text="Latitude (°)", range=[-80, 80], row=1, col=1)
617 fig.update_xaxes(title_text="Longitude (°)", range=[-180, 180], row=1, col=2)
618 fig.update_yaxes(title_text="Latitude (°)", range=[-80, 80], row=1, col=2)
619
620 fig.update_layout(height=500, width=1400)
621 fig.write_html(
622 str(OUTPUT_DIR / "geophysical_magnetic_field.html"),
623 include_plotlyjs="cdn",
624 div_id="geophysical_magnetic_field",
625 )
626 print("\n [Plot saved to geophysical_magnetic_field.html]")
627
628
629def demo_magnetic_properties():
630 """Demonstrate magnetic field properties and variations."""
631 print("\n" + "=" * 70)
632 print("Magnetic Field Properties Demo")
633 print("=" * 70)
634
635 decimal_year = 2025.0
636
637 # Magnetic pole location
638 pole_lat, pole_lon = magnetic_north_pole(decimal_year)
639 print(f"\nMagnetic North Pole location ({decimal_year}):")
640 print(f" Latitude: {np.degrees(pole_lat):.2f}°N")
641 print(f" Longitude: {np.degrees(pole_lon):.2f}°E")
642
643 # Earth's dipole moment (using WMM2020 coefficients)
644 moment = dipole_moment() # Uses default WMM2020 coefficients
645 print(f"\nEarth's dipole moment (WMM2020): {moment:.4e} A·m²")
646
647 # Compare WMM and IGRF at a test location
648 lat, lon, alt = np.radians(40.0), np.radians(-100.0), 0.0
649
650 wmm_result = wmm(lat, lon, alt, decimal_year)
651 igrf_result = igrf(lat, lon, alt, decimal_year)
652
653 print(f"\nModel comparison at (40°N, 100°W):")
654 print("-" * 40)
655 print(f"{'Component':<12} {'WMM':>12} {'IGRF':>12}")
656 print("-" * 40)
657 print(f"{'X (nT)':<12} {wmm_result.X:>12.1f} {igrf_result.X:>12.1f}")
658 print(f"{'Y (nT)':<12} {wmm_result.Y:>12.1f} {igrf_result.Y:>12.1f}")
659 print(f"{'Z (nT)':<12} {wmm_result.Z:>12.1f} {igrf_result.Z:>12.1f}")
660 print(f"{'F (nT)':<12} {wmm_result.F:>12.1f} {igrf_result.F:>12.1f}")
661
662 # Secular variation
663 print("\n--- Secular Variation ---")
664 print("Magnetic field changes over time due to core dynamics.")
665 years = [2020, 2022, 2024, 2025]
666 for year in years:
667 # WMM2025's window is 2025.0-2030.0; use WMM2020 (2020.0-2025.0)
668 # for the years before that so this demo stays warning-free.
669 coeffs = WMM2020 if year < 2025 else WMM2025
670 dec = magnetic_declination(lat, lon, alt, float(year), coeffs)
671 print(f" {year}: declination = {np.degrees(dec):.2f}°")
672
673
674def demo_navigation_application():
675 """Demonstrate application to navigation."""
676 print("\n" + "=" * 70)
677 print("Navigation Application Demo")
678 print("=" * 70)
679
680 # Aircraft navigation scenario
681 lat = np.radians(40.0)
682 lon = np.radians(-74.0)
683 alt = 10000.0 # meters (cruise altitude)
684 decimal_year = 2025.0
685
686 print("\nAircraft navigation correction scenario:")
687 print(f" Position: 40°N, 74°W")
688 print(f" Altitude: {alt:.0f} m")
689
690 # Magnetic declination for compass correction
691 dec = magnetic_declination(lat, lon, alt, decimal_year)
692 print(f"\n Magnetic declination: {np.degrees(dec):.2f}°")
693 print(f" -> Add {np.degrees(dec):.2f}° to magnetic heading for true heading")
694
695 # Gravity for inertial navigation
696 g_result = gravity_wgs84(lat, lon, alt)
697 print(f"\n Local gravity: {g_result.magnitude:.6f} m/s²")
698 print(f" -> Used for vertical channel in INS")
699
700 # Deflection of vertical (simplified)
701 print(f"\n Gravity deflection (N): {g_result.g_north * 1e6:.2f} µrad")
702 print(f" -> Correction for inertial alignment")
703
704 # Geoid for altitude reference (J2 approximation - latitude only)
705 N = geoid_height_j2(lat)
706 print(f"\n Geoid undulation (J2 approx): {N:.1f} m")
707 print(f" -> Ellipsoid alt = GPS alt, Orthometric alt = GPS alt - N")
708
709
710def main():
711 """Run all demonstrations."""
712 print("\n" + "#" * 70)
713 print("# PyTCL Geophysical Models Example")
714 print("#" * 70)
715
716 # Gravity models
717 demo_normal_gravity()
718 demo_gravity_models()
719 demo_geoid()
720 demo_gravity_anomalies()
721 demo_tidal_effects()
722
723 # Magnetic field models
724 demo_magnetic_field()
725 demo_magnetic_properties()
726
727 # Application
728 demo_navigation_application()
729
730 print("\n" + "=" * 70)
731 print("Example complete!")
732 if SHOW_PLOTS:
733 print("Plots saved: geophysical_gravity_latitude.html,")
734 print(" geophysical_gravity_altitude.html,")
735 print(" geophysical_tides.html,")
736 print(" geophysical_magnetic_field.html")
737 print("=" * 70)
738
739
740if __name__ == "__main__":
741 main()
Running the Example
python examples/geophysical_models.py
See Also
INS/GNSS Navigation - INS/GNSS integration
Navigation and Geodesy - Geodetic calculations
Magnetism Models - Magnetic field details