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    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