Comparing FES/Darwin and PERTH/Doodson Engines#

PyFES implements two prediction engines that share the same harmonic method but differ in how they name their constituents and compute nodal corrections.

Both engines solve the same prediction equation:

\[h(t) = H_0 + \sum_k f_k H_k \cos(\omega_k t + V_k + u_k - G_k)\]

What changes between engines is how constituents are catalogued and how the nodal factors \(f_k\) and \(u_k\) are obtained.

Note

To run actual predictions you need tidal atlas files downloaded from AVISO (for FES) or NASA GSFC (for GOT models). This example only inspects the constituent catalogues and settings; it does not require any data files.

from __future__ import annotations

from IPython.display import HTML
import markdown
import numpy as np

import pyfes


def md_to_html(md_string: str) -> HTML:
    """Convert a markdown string to HTML for display in Jupyter."""
    return HTML(markdown.markdown(md_string, extensions=['tables']))

Constituent catalogues#

Each engine defines its own catalogue of tidal constituents. Let’s retrieve them and see how they compare.

darwin_wt = pyfes.wave_table_factory(pyfes.DARWIN)
perth_wt = pyfes.wave_table_factory(pyfes.DOODSON)

darwin_names = {w.name for w in darwin_wt}
perth_names = {w.name for w in perth_wt}
common = sorted(darwin_names & perth_names)
darwin_only = sorted(darwin_names - perth_names)
perth_only = sorted(perth_names - darwin_names)

print(f'Darwin catalogue :  {len(darwin_wt)} constituents')
print(f'Doodson catalogue:  {len(perth_wt)} constituents')
print(f'Common to both   :  {len(common)}')
print(f'Darwin-only      :  {len(darwin_only)}')
print(f'Doodson-only     :  {len(perth_only)}')
Darwin catalogue :  99 constituents
Doodson catalogue:  80 constituents
Common to both   :  69
Darwin-only      :  30
Doodson-only     :  11

Both catalogues cover the same major constituents. The differences are mostly in the higher-order compound tides.

print('\nDarwin-only constituents:')
print(', '.join(darwin_only))

print('\nDoodson-only constituents:')
print(', '.join(perth_only))
Darwin-only constituents:
2MK2, 2MNS4, 2MP5, 2MSN4, 2NM6, 2NS2, 2SMu2, 3MS4, 3MS8, A5, M0, M11, M12, ML4, MNK6, MNS2, MNu4, MNuS2, MP1, MSK2, Mf1, Mf2, Mm1, Mm2, NK4, NKM2, OQ2, SK3, SKM2, SO3

Doodson-only constituents:
Alpha2, Beta1, Beta2, Delta2, Gamma2, MSm, MStm, Mqm, Node, Tau1, Ups1

Notation: Darwin vs Doodson#

Both notations encode the same six integer coefficients that define a constituent’s astronomical argument:

\[V_k = n_1 \tau + n_2 s + n_3 h + n_4 p + n_5 N' + n_6 p_1\]

Darwin notation gives each constituent a traditional name (M₂, K₁, …) and lists the six integers explicitly.

Doodson notation packs them into a six-digit number with an offset of +5 on every digit except the first:

\[\text{Doodson number} = n_1 \; (n_2{+}5)(n_3{+}5).(n_4{+}5)(n_5{+}5)(n_6{+}5)\]

PyFES also exposes the XDO alphabetical encoding used internally. Let’s compare the representations for a few well-known constituents.

sample = ['M2', 'S2', 'K1', 'O1', 'Mf', 'M4']

lines = ['| Name | Speed (°/h) | Doodson | XDO |']
lines.append('| :--- | ---: | :---: | :---: |')
for name in sample:
    w = darwin_wt[name]
    lines.append(
        f'| {name} '
        f'| {w.frequency(pyfes.DEGREE_PER_HOUR):.7f} '
        f'| {w.xdo_numerical()} '
        f'| {w.xdo_alphabetical()} |'
    )

md_to_html('\n'.join(lines))
Name Speed (°/h) Doodson XDO
M2 28.9841042 2555555 BZZZZZZ
S2 30.0000000 2735555 BBXZZZZ
K1 15.0410686 1655556 AAZZZZA
O1 13.9430356 1455554 AYZZZZY
Mf 1.0980330 0755555 ZBZZZZZ
M4 57.9682085 4555555 DZZZZZZ


Default settings for each engine#

The two settings classes differ only in their defaults.

fes_settings = pyfes.FESSettings()
perth_settings = pyfes.PerthSettings()

lines = [
    '| Setting | FESSettings | PerthSettings |',
    '| :--- | :---: | :---: |',
    f'| Engine type | {fes_settings.engine_type.name} '
    f'| {perth_settings.engine_type.name} |',
    f'| Inference | {fes_settings.inference_type.name} '
    f'| {perth_settings.inference_type.name} |',
    f'| Formulae | {fes_settings.astronomic_formulae.name} '
    f'| {perth_settings.astronomic_formulae.name} |',
    f'| Group modulations | {fes_settings.group_modulations} '
    f'| {perth_settings.group_modulations} |',
    f'| Long-period equilibrium | '
    f'{fes_settings.compute_long_period_equilibrium} '
    f'| {perth_settings.compute_long_period_equilibrium} |',
]

md_to_html('\n'.join(lines))
Setting FESSettings PerthSettings
Engine type DARWIN DOODSON
Inference SPLINE LINEAR
Formulae SCHUREMAN_ORDER_1 IERS
Group modulations False False
Long-period equilibrium True False


Note that group modulations are disabled by default on both engines. They can be enabled explicitly when needed:

settings = pyfes.PerthSettings().with_group_modulations(True)

Nodal corrections: individual vs group#

Every constituent has a slowly varying amplitude factor f and phase correction u that account for the 18.61-year lunar nodal cycle.

  • Individual corrections (both engines, default): each constituent gets its own f and u computed from Schureman’s formulae. This is the classical approach.

  • Group modulations (optional, PERTH engine only): closely related constituents sharing the same first two Doodson digits are modulated together, summing over satellite frequencies within the group.

Let’s compute the individual nodal corrections for a sample date and compare the two engines.

date = np.datetime64('2024-07-01T00:00:00')

f_darwin, vu_darwin = darwin_wt.compute_nodal_modulations(
    np.array([date]),
    formulae=pyfes.Formulae.SCHUREMAN_ORDER_1,
)

f_perth, vu_perth = perth_wt.compute_nodal_modulations(
    np.array([date]),
    formulae=pyfes.Formulae.IERS,
)

Display nodal factors for major shared constituents.

lines = ['| Constituent | f (Darwin) | f (Doodson) |']
lines.append('| :--- | ---: | ---: |')

for name in ['M2', 'S2', 'K1', 'O1', 'N2', 'K2', 'Mf', 'Mm']:
    idx_d = darwin_wt.constituents.index(name)
    idx_p = perth_wt.constituents.index(name)
    lines.append(
        f'| {name} | {f_darwin[idx_d, 0]:.6f} | {f_perth[idx_p, 0]:.6f} |'
    )

md_to_html('\n'.join(lines))
Constituent f (Darwin) f (Doodson)
M2 0.963920 0.963912
S2 1.000000 1.002207
K1 1.111175 1.111373
O1 1.180081 1.173631
N2 0.963920 0.963912
K2 1.310282 1.311796
Mf 1.444214 1.488810
Mm 0.873869 0.796820


The amplitude factors are nearly identical because both engines implement the same Schureman obliquity formulae; the small differences come from the different astronomic angle polynomials (SCHUREMAN_ORDER_1 vs IERS). Solar constituents like S₂ always have f = 1 regardless of engine.

Enabling group modulations#

Group modulations are an optional feature of the PERTH engine. When enabled, related constituents within the same tidal group are modulated together instead of individually. Let’s compare.

f_perth_grp, vu_perth_grp = perth_wt.compute_nodal_modulations(
    np.array([date]),
    formulae=pyfes.Formulae.IERS,
    group_modulations=True,
)

lines = ['| Constituent | f (individual) | f (group) |']
lines.append('| :--- | ---: | ---: |')
for name in ['M2', 'S2', 'K1', 'O1', 'N2', 'K2', 'Mf', 'Mm']:
    idx = perth_wt.constituents.index(name)
    lines.append(
        f'| {name} | {f_perth[idx, 0]:.6f} | {f_perth_grp[idx, 0]:.6f} |'
    )

md_to_html('\n'.join(lines))
Constituent f (individual) f (group)
M2 0.963912 0.965649
S2 1.002207 0.612739
K1 1.111373 1.373624
O1 1.173631 1.186137
N2 0.963912 0.791494
K2 1.311796 1.311796
Mf 1.488810 1.404428
Mm 0.796820 0.630888


Inference modes#

Both engines support the same four inference modes for estimating minor constituents that are absent from the atlas. The mode is set independently of the engine choice.

for mode in [pyfes.ZERO, pyfes.LINEAR, pyfes.SPLINE, pyfes.FOURIER]:
    print(f'  {mode.name:8s}  -  available on both engines')
ZERO      -  available on both engines
LINEAR    -  available on both engines
SPLINE    -  available on both engines
FOURIER   -  available on both engines

The recommended defaults are:

  • SPLINE for FES atlases (default of FESSettings)

  • LINEAR for GOT atlases (default of PerthSettings)

Generating constituent summary tables#

pyfes.generate_markdown_table() produces a complete summary of the engine settings and shows which constituents are modeled (provided by the atlas) versus inferred.

modeled = ['M2', 'S2', 'N2', 'K2', 'K1', 'O1', 'P1', 'Q1']

md_to_html(
    pyfes.generate_markdown_table(
        fes_settings,
        modeled_constituents=modeled,
    )
)
Setting Value
Engine Type Darwin
Astronomic Formulae Schureman Order 1
Inference Type Spline
Time Tolerance (s) 0.000000
Group Modulations N/A
Compute Long Period Equilibrium Yes
Number of Threads 0
Constituent Speed (Deg/hr) XDO Modeled Inferred
2Q1 12.854286 AWZBZZY No Yes
${\sigma}1$ 12.927140 AWBZZZY No Yes
Q1 13.398661 AXZAZZY Yes No
${\rho}1$ 13.471515 AXBYZZY No Yes
O1 13.943036 AYZZZZY Yes No
M11 14.487410 AZZYZZA No Yes
M12 14.496694 AZZAZZA No Yes
${\chi}1$ 14.569548 AZBYZZA No Yes
${\pi}1$ 14.917865 AAWZZAY No Yes
P1 14.958931 AAXZZZY Yes No
K1 15.041069 AAZZZZA Yes No
${\phi}1$ 15.123206 AABZZZA No Yes
${\theta}1$ 15.512590 ABXAZZA No Yes
J1 15.585443 ABZYZZA No Yes
OO1 16.139102 ACZZZZA No Yes
${\epsilon}2$ 27.423834 BWBAZZZ No Yes
2N2 27.895355 BXZBZZZ No Yes
${\mu}2$ 27.968208 BXBZZZZ No Yes
N2 28.439730 BYZAZZZ Yes No
${\nu}2$ 28.512583 BYBYZZZ No Yes
M2 28.984104 BZZZZZZ Yes No
${\lambda}2$ 29.455625 BAXAZZB No Yes
L2 29.528479 BAZYZZB No Yes
T2 29.958933 BBWZZAZ No Yes
S2 30.000000 BBXZZZZ Yes No
K2 30.082137 BBZZZZZ Yes No
${\eta}2$ 30.626512 BCZYZZZ No Yes


The same call with the PERTH engine settings:

md_to_html(
    pyfes.generate_markdown_table(
        perth_settings,
        modeled_constituents=modeled,
    )
)
Setting Value
Engine Type Doodson
Astronomic Formulae IERS
Inference Type Linear
Time Tolerance (s) 0.000000
Group Modulations No
Compute Long Period Equilibrium No
Number of Threads 0
Constituent Speed (Deg/hr) XDO Modeled Inferred
Node 0.002206 ZZZZAZB No Yes
Sa1 0.041067 ZZAZZYZ No Yes
Ssa 0.082137 ZZBZZZZ No Yes
Sta 0.123206 ZZCZZZZ No Yes
MSm 0.471521 ZAXAZZZ No Yes
Mm 0.544375 ZAZYZZZ No Yes
MSf 1.015896 ZBXZZZZ No Yes
Mf 1.098033 ZBZZZZZ No Yes
MStm 1.569554 ZCXAZZZ No Yes
Mtm 1.642408 ZCZYZZZ No Yes
MSqm 2.113929 ZDXZZZZ No Yes
Mqm 2.186782 ZDZXZZZ No Yes
2Q1 12.854286 AWZBZZC No Yes
${\sigma}1$ 12.927140 AWBZZZC No Yes
Q1 13.398661 AXZAZZC Yes No
${\rho}1$ 13.471515 AXBYZZC No Yes
O1 13.943036 AYZZZZC Yes No
${\tau}1$ 14.025173 AYBZZZA No Yes
${\beta}1$ 14.414557 AZXAZZA No Yes
M1 14.496694 AZZAZZA No Yes
${\chi}1$ 14.569548 AZBYZZA No Yes
${\pi}1$ 14.917865 AAWZZAC No Yes
P1 14.958931 AAXZZZC Yes No
K1 15.041069 AAZZZZA Yes No
${\psi}1$ 15.082135 AAAZZYA No Yes
${\phi}1$ 15.123206 AABZZZA No Yes
${\theta}1$ 15.512590 ABXAZZA No Yes
J1 15.585443 ABZYZZA No Yes
SO1 16.056964 ACXZZZA No Yes
OO1 16.139102 ACZZZZA No Yes
${\upsilon}1$ 16.683476 ADZYZZA No Yes
${\epsilon}2$ 27.423834 BWBAZZZ No Yes
2N2 27.895355 BXZBZZZ No Yes
${\mu}2$ 27.968208 BXBZZZZ No Yes
N2 28.439730 BYZAZZZ Yes No
${\nu}2$ 28.512583 BYBYZZZ No Yes
${\gamma}2$ 28.911251 BZXBZZB No Yes
${\alpha}2$ 28.943038 BZYZZAB No Yes
M2 28.984104 BZZZZZZ Yes No
${\beta}2$ 29.025171 BZAZZYZ No Yes
${\delta}2$ 29.066242 BZBZZZZ No Yes
${\lambda}2$ 29.455625 BAXAZZB No Yes
L2 29.528479 BAZYZZB No Yes
T2 29.958933 BBWZZAZ No Yes
S2 30.000000 BBXZZZZ Yes No
R2 30.041067 BBYZZYB No Yes
K2 30.082137 BBZZZZZ Yes No
${\eta}2$ 30.626512 BCZYZZZ No Yes


You can also inspect the wave table directly. The table lists every constituent with its frequency, Doodson number and XDO encoding.

wt = pyfes.wave_table_factory(
    pyfes.DARWIN,
    ['M2', 'S2', 'N2', 'K2', 'K1', 'O1', 'P1', 'Q1', 'Mf', 'Mm'],
)
md_to_html(wt.generate_markdown_table())
Constituent Speed (Deg/hr) XDO
Mm 0.544375 ZAZYZZZ
Mf 1.098033 ZBZZZZZ
Q1 13.398661 AXZAZZY
O1 13.943036 AYZZZZY
P1 14.958931 AAXZZZY
K1 15.041069 AAZZZZA
N2 28.439730 BYZAZZZ
M2 28.984104 BZZZZZZ
S2 30.000000 BBXZZZZ
K2 30.082137 BBZZZZZ


Choosing an engine#

The engine is determined by the tidal atlas you use. Set it once in the YAML configuration file; the Python prediction code is identical.

  • engine: darwin → FES atlases (FES2014, FES2022)

  • engine: perth → GOT atlases (GOT4.10, GOT5.5, GOT5.6)

# Exactly the same code regardless of engine
config = pyfes.config.load('my_atlas.yaml')
tide, lp, flags = pyfes.evaluate_tide(
    config.models['tide'], dates, lons, lats,
    settings=config.settings,
)

Total running time of the script: (0 minutes 0.019 seconds)

Gallery generated by Sphinx-Gallery