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() and igrf()

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

Running the Example

python examples/geophysical_models.py

See Also