C++ API#
This library allows you to make tide predictions using C++ and Python. The Python part is documented in the rest of this documentation. Here, we want to explain to readers who only want to use the C++ part of this library.
This documentation presents how to use the API to calculate tides in C++ using the FES 2022 model. This model is used to make tide predictions in SWOT KaRIn products.
The management part is not provided in this library, but we will illustrate it with the example. In a more advanced use case, this library must be integrated with CMake, but here we will focus on explaining basic usage using command line commands to make it as simple as possible.
Overview#
The FES2022 C++ library provides high-performance tide prediction capabilities using two types of tidal models:
Ocean Tide Model (LGP2): Uses unstructured triangular mesh with Lagrange P2 finite elements for ocean tide predictions
Radial Load Tide Model (Cartesian): Uses regular cartesian grids for computing radial loading effects
The library supports all major tidal constituents and provides accurate predictions for any location and time.
Prerequisites#
Before using the C++ library, ensure you have the following dependencies installed:
C++20 compatible compiler (GCC 10+, Clang 11+, or MSVC 2019+)
Eigen3 library for linear algebra operations
NetCDF-CXX4 library for reading tide model data files
FES2022 model data files (both ocean and load tide datasets)
Main Components#
The FES C++ API consists of several key components:
Tidal Model Classes:
fes::tidal_model::LGP1<T>
: For ocean tide models using triangular mesh with LGP1 discretizationfes::tidal_model::LGP2<T>
: For ocean tide models using triangular mesh with LGP2 discretizationfes::tidal_model::Cartesian<T>
: For load tide models using regular grids
Core Functions:
fes::evaluate_tide()
: Main function for tide prediction
Configuration Classes:
fes::Settings
: Configuration for astronomical formulae and precisionfes::Constituent
: Enumeration of tidal constituents
Step-by-Step Usage Guide#
Using the FES C++ library involves the following main steps:
Step 1: Include Required Headers#
#include <chrono>
#include <cmath>
#include <complex>
#include <iomanip>
#include <iostream>
#include <list>
#include <netcdf>
#include <numbers>
#include <tuple>
#include <unordered_map>
#include <vector>
#include "fes/tidal_model/cartesian.hpp"
#include "fes/tidal_model/lgp.hpp"
#include "fes/tide.hpp"
Step 2: Configure Data Paths#
Define the paths to your FES2022 model data files:
// Define the root directory for load tide files
#define LOAD_TIDE_ROOT_DIR "path/to/fes2022_loadtide/"
// Macro to construct full file paths
#define LOAD_TIDE(FILENAME) {LOAD_TIDE_ROOT_DIR FILENAME}
// Define the ocean tide file path (LPG2)
#define OCEAN_TIDE "path/to/fes2022_oceantide/fes2022b_lgp2.nc"
Step 3: Load Tidal Constituents#
Create functions to specify which tidal constituents to load:
/// @brief Load tide paths for all constituents
/// @return A map of constituent identifiers to their file paths
static std::unordered_map<fes::Constituent, std::string> load_tide_paths() {
return {{fes::Constituent::k2N2, LOAD_TIDE("2n2.nc")},
{fes::Constituent::kEps2, LOAD_TIDE("eps2.nc")},
{fes::Constituent::kJ1, LOAD_TIDE("j1.nc")},
{fes::Constituent::kJ1, LOAD_TIDE("k1.nc")},
{fes::Constituent::kK2, LOAD_TIDE("k2.nc")},
{fes::Constituent::kL2, LOAD_TIDE("l2.nc")},
{fes::Constituent::kLambda2, LOAD_TIDE("lambda2.nc")},
{fes::Constituent::kM2, LOAD_TIDE("m2.nc")},
{fes::Constituent::kM3, LOAD_TIDE("m3.nc")},
{fes::Constituent::kM4, LOAD_TIDE("m4.nc")},
{fes::Constituent::kM6, LOAD_TIDE("m6.nc")},
{fes::Constituent::kM8, LOAD_TIDE("m8.nc")},
{fes::Constituent::kMf, LOAD_TIDE("mf.nc")},
{fes::Constituent::kMKS2, LOAD_TIDE("mks2.nc")},
{fes::Constituent::kMm, LOAD_TIDE("mm.nc")},
{fes::Constituent::kMN4, LOAD_TIDE("mn4.nc")},
{fes::Constituent::kMS4, LOAD_TIDE("ms4.nc")},
{fes::Constituent::kMSf, LOAD_TIDE("msf.nc")},
{fes::Constituent::kMSqm, LOAD_TIDE("msqm.nc")},
{fes::Constituent::kMtm, LOAD_TIDE("mtm.nc")},
{fes::Constituent::kMu2, LOAD_TIDE("mu2.nc")},
{fes::Constituent::kN2, LOAD_TIDE("n2.nc")},
{fes::Constituent::kN4, LOAD_TIDE("n4.nc")},
{fes::Constituent::kNu2, LOAD_TIDE("nu2.nc")},
{fes::Constituent::kO1, LOAD_TIDE("o1.nc")},
{fes::Constituent::kP1, LOAD_TIDE("p1.nc")},
{fes::Constituent::kQ1, LOAD_TIDE("q1.nc")},
{fes::Constituent::kR2, LOAD_TIDE("r2.nc")},
{fes::Constituent::kS1, LOAD_TIDE("s1.nc")},
{fes::Constituent::kS2, LOAD_TIDE("s2.nc")},
{fes::Constituent::kS4, LOAD_TIDE("s4.nc")},
{fes::Constituent::kSa, LOAD_TIDE("sa.nc")},
{fes::Constituent::kSsa, LOAD_TIDE("ssa.nc")},
{fes::Constituent::kT2, LOAD_TIDE("t2.nc")}};
}
/// @brief Get the ocean tide constituents to load
/// @return A list of constituent identifiers
static auto ocean_tide_constituents() -> std::list<fes::Constituent> {
return {fes::k2N2, fes::kEps2, fes::kJ1, fes::kK1, fes::kK2, fes::kL2,
fes::kLambda2, fes::kM2, fes::kM3, fes::kM4, fes::kM6, fes::kM8,
fes::kMKS2, fes::kMN4, fes::kMS4, fes::kMSf, fes::kMf, fes::kMm,
fes::kMSqm, fes::kMtm, fes::kMu2, fes::kN2, fes::kN4, fes::kNu2,
fes::kO1, fes::kP1, fes::kQ1, fes::kR2, fes::kS1, fes::kS2,
fes::kS4, fes::kSa, fes::kSsa, fes::kT2};
}
Step 4: Data Loading Functions#
Create utility functions for loading NetCDF data:
/// @brief Convert degrees to radians
/// @tparam T Data type
/// @param degrees Value in degrees
/// @return Value in radians
template <typename T>
constexpr auto to_radians(const T& degrees) -> T {
return degrees * std::numbers::pi / 180.0;
}
/// @brief Create a complex wave from amplitude and phase vectors
/// @param amp Amplitude vector
/// @param pha Phase vector in degrees
/// @return A complex vector representing the wave
auto polar(const std::vector<float>& amp, const std::vector<float>& pha)
-> Eigen::VectorXcf {
auto size = amp.size();
if (size != pha.size()) {
throw std::invalid_argument(
"Amplitude and phase vectors must be of the same size.");
}
// Combine amplitude and phase into a complex wave
Eigen::Map<const Eigen::VectorXf> amp_map(amp.data(), size);
Eigen::Map<const Eigen::VectorXf> phase_map(pha.data(), size);
return amp_map.binaryExpr(
phase_map, [](float a, float p) { return std::polar(a, to_radians(p)); });
}
Step 5: Load Cartesian Model#
Load the radial load tide model from individual NetCDF files:
static auto load_tide_data_from_file(
const fes::Constituent ident, const std::string& filename,
std::unique_ptr<fes::tidal_model::Cartesian<float>>& model) -> void {
try {
// Open the NetCDF file for reading
netCDF::NcFile ds(filename, netCDF::NcFile::read);
// Get the dimensions
auto lon_dim = ds.getDim("lon").getSize();
auto lat_dim = ds.getDim("lat").getSize();
// Get the variables
netCDF::NcVar lon_var = ds.getVar("lon");
netCDF::NcVar lat_var = ds.getVar("lat");
netCDF::NcVar amp_var = ds.getVar("amplitude");
netCDF::NcVar phase_var = ds.getVar("phase");
// Ensure variables are valid
assert(!lon_var.isNull());
assert(!lat_var.isNull());
assert(!amp_var.isNull());
assert(!phase_var.isNull());
// Read the data
std::vector<float> amp(lon_dim * lat_dim);
std::vector<float> phase(lon_dim * lat_dim);
amp_var.getVar(amp.data());
phase_var.getVar(phase.data());
// Ensure amplitude and phase data are in column-major order to match
// the cartesian model's expected layout.
assert(amp_var.getDim(0).getSize() == lat_dim);
assert(amp_var.getDim(1).getSize() == lon_dim);
assert(phase_var.getDim(0).getSize() == lat_dim);
assert(phase_var.getDim(1).getSize() == lon_dim);
// Initialize the model if it hasn't been already
if (model == nullptr) {
std::vector<double> lon(lon_dim);
std::vector<double> lat(lat_dim);
// Assumption: longitude and latitude grids are consistent across all
// tidal constituents. Production code should validate this invariant.
lon_var.getVar(lon.data());
lat_var.getVar(lat.data());
auto x_axis =
fes::Axis(Eigen::Map<const Eigen::VectorXd>(lon.data(), lon.size()),
1e-6, true);
auto y_axis =
fes::Axis(Eigen::Map<const Eigen::VectorXd>(lat.data(), lat.size()),
1e-6, false);
model = std::move(std::make_unique<fes::tidal_model::Cartesian<float>>(
std::move(x_axis), std::move(y_axis), fes::TideType::kRadial, false));
}
// Combine amplitude and phase into a complex wave
auto wave = polar(amp, phase);
// Finally, add the constituent to the model
model->add_constituent(ident, std::move(wave));
} catch (netCDF::exceptions::NcException& e) {
std::cerr << "Error opening file: " << e.what() << std::endl;
exit(EXIT_FAILURE);
}
}
/// @brief Load the tide model
/// @return A unique pointer to the tide model
static auto load_load_tide_model()
-> std::unique_ptr<fes::tidal_model::Cartesian<float>> {
auto tide_paths = load_tide_paths();
std::unique_ptr<fes::tidal_model::Cartesian<float>> model = nullptr;
for (const auto& [ident, path] : tide_paths) {
load_tide_data_from_file(ident, path, model);
}
return model;
}
Key points for Cartesian model loading:
Each tidal constituent is stored in a separate NetCDF file
The grid coordinates must be consistent across all constituents
Amplitude and phase data are combined into complex waves
Step 6: Load LGP2 Model#
Load the ocean tide model from the LGP2 format file:
static auto load_ocean_tide_model()
-> std::unique_ptr<fes::tidal_model::LGP2<float>> {
try {
auto lpg2_model = std::make_unique<fes::tidal_model::LGP2<float>>();
// Open the NetCDF file for reading
netCDF::NcFile ds(OCEAN_TIDE, netCDF::NcFile::read);
// Get the dimensions
auto coordinates = ds.getDim("coordinates").getSize();
auto triangles = ds.getDim("triangles").getSize();
auto lgp2_nodes = ds.getDim("lgp2_nodes").getSize();
// Read the coordinates
std::vector<double> lon(coordinates);
std::vector<double> lat(coordinates);
ds.getVar("lon").getVar(lon.data());
ds.getVar("lat").getVar(lat.data());
// Read the triangles
std::vector<int32_t> triangles_data(triangles * 3);
ds.getVar("triangle").getVar(triangles_data.data());
// Read the LGP2 codes
std::vector<int32_t> lgp2_codes(triangles * 6);
ds.getVar("lgp2").getVar(lgp2_codes.data());
// Create the mesh index
auto index = std::make_shared<fes::mesh::Index>(
Eigen::Map<const Eigen::VectorXd>(lon.data(), lon.size()),
Eigen::Map<const Eigen::VectorXd>(lat.data(), lat.size()),
Eigen::Map<const Eigen::Matrix<int32_t, -1, 3, Eigen::RowMajor>>(
triangles_data.data(), triangles, 3));
// Initialize the LGP2 model with the mesh index and LGP2 codes
lpg2_model = std::make_unique<fes::tidal_model::LGP2<float>>(
std::move(index),
Eigen::Map<
const Eigen::Matrix<int32_t, Eigen::Dynamic, 6, Eigen::RowMajor>>(
lgp2_codes.data(), triangles, 6),
fes::TideType::kTide, 100'000);
// Read and add each constituent to the model
for (const auto& ident : ocean_tide_constituents()) {
std::string prefix = fes::constituents::name(ident);
std::string var_name = prefix + "_amplitude";
// Get the amplitude variable
auto amp = ds.getVar(var_name);
if (amp.isNull()) {
throw std::runtime_error("Variable " + var_name +
" not found in the dataset.");
}
// Read the amplitude data
std::vector<float> amplitude(lgp2_nodes);
amp.getVar(amplitude.data());
// Get the phase variable
var_name = prefix + "_phase";
auto phase = ds.getVar(var_name);
if (phase.isNull()) {
throw std::runtime_error("Variable " + var_name +
" not found in the dataset.");
}
// Read the phase data
std::vector<float> phase_data(lgp2_nodes);
phase.getVar(phase_data.data());
// Combine amplitude and phase into a complex wave
auto wave = polar(amplitude, phase_data);
// Add the constituent to the model
lpg2_model->add_constituent(ident, std::move(wave));
}
return lpg2_model;
} catch (netCDF::exceptions::NcException& e) {
std::cerr << "Error opening file: " << e.what() << std::endl;
exit(EXIT_FAILURE);
}
}
Key points for LGP2 model loading:
All constituents are stored in a single NetCDF file
Uses unstructured triangular mesh with Lagrange P2 elements
Requires mesh topology (triangles and LGP2 codes)
Supports spatial indexing for efficient interpolation
Step 7: Model Initialization#
Create model instances and configure settings:
// Load the load tide model
auto radial_handler = load_load_tide_model();
// Load the ocean tide model
auto tide_handler = load_ocean_tide_model();
// Create the FES settings
auto settings = fes::Settings{fes::angle::Formulae::kIERS, 0.0};
The fes::Settings
class allows you to configure:
Astronomical formulae: IERS or Meeus algorithms
Time tolerance: This parameter allows for the adjustment of astronomical angle calculations. When its value is set to zero, the angles will be recalculated each time the date changes. Otherwise, they will be considered valid as long as the time difference between the last evaluation and the current date is within the specified tolerance.
Step 8: Prepare Input Data#
Set up your prediction parameters:
// Create a timeseries of dates starting from the first january 1983
Eigen::VectorXd times(24);
auto start_date = std::chrono::year{1983} / std::chrono::January / 1;
auto start = std::chrono::sys_days{start_date};
for (int64_t i = 0; i < times.size(); ++i) {
auto current = start + std::chrono::days(i);
times[i] =
std::chrono::duration<double>(current.time_since_epoch()).count();
}
// Setup the leap seconds (TAI-UTC)
// In production, these should be set according to the dates
auto leap_seconds = Eigen::Vector<uint16_t, -1>::Constant(times.size(), 21);
// Set the location (longitude, latitude) in degrees
auto lon = Eigen::VectorXd::Constant(times.size(), -7.688);
auto lat = Eigen::VectorXd::Constant(times.size(), 59.195);
Input requirements:
Times: Unix timestamps (seconds since 1970-01-01)
Leap seconds: TAI-UTC offset for each time point
Coordinates: Longitude and latitude in degrees
All vectors must have the same size
Step 9: Perform Tide Evaluation#
Call the main prediction function:
// Evaluate the radial load tide (long period and interpolation quality are
// ignored)
Eigen::VectorXd load = std::get<0>(fes::evaluate_tide(
radial_handler.get(), times, leap_seconds, lon, lat, settings));
// Evaluate the ocean tide (interpolation quality is ignored here, but in
// production you should check this to differentiate between interpolated and
// extrapolated points)
Eigen::VectorXd tide;
Eigen::VectorXd lp;
std::tie(tide, lp, std::ignore) = fes::evaluate_tide(
tide_handler.get(), times, leap_seconds, lon, lat, settings);
The fes::evaluate_tide()
function returns a tuple containing:
Short-period tide: High-frequency tidal signal
Long-period tide: Low-frequency tidal signal
Interpolation quality: Quality flags for each point
Step 10: Process and Display Results#
Format and output the prediction results:
// Print header with better formatting
std::cout << "\n" << std::string(110, '=') << std::endl;
std::cout << " FES2022 TIDE PREDICTION RESULTS"
<< std::endl;
std::cout << std::string(110, '=') << std::endl;
std::cout << std::left << std::setw(12) << "Date" << std::setw(6) << "Hour"
<< std::right << std::setw(12) << "Latitude" << std::setw(12)
<< "Longitude" << std::setw(12) << "Short_tide" << std::setw(12)
<< "LP_tide" << std::setw(12) << "Pure_Tide" << std::setw(12)
<< "Geo_Tide" << std::setw(12) << "Rad_Tide" << std::endl;
std::cout << std::string(110, '-') << std::endl;
// Print results for each time point
for (int64_t i = 0; i < times.size(); ++i) {
// Convert Unix timestamp back to date
auto time_point =
std::chrono::system_clock::from_time_t(static_cast<time_t>(times[i]));
auto date = std::chrono::floor<std::chrono::days>(time_point);
auto time_of_day = time_point - date;
auto hours = std::chrono::duration_cast<std::chrono::hours>(time_of_day);
// Get year/month/day
auto ymd = std::chrono::year_month_day{date};
// Format date as YYYY-MM-DD
std::cout << std::left << std::setw(4) << static_cast<int>(ymd.year())
<< "-" << std::setw(2) << std::setfill('0')
<< static_cast<unsigned>(ymd.month()) << "-" << std::setw(2)
<< std::setfill('0') << static_cast<unsigned>(ymd.day()) << " "
<< std::setfill(' ') << std::setw(5) << hours.count() << "h";
// Format the tide data with proper alignment and precision
std::cout << std::right << std::fixed << std::setprecision(3)
<< std::setw(12) << lat(i) << std::setw(12) << lon(i)
<< std::setw(12) << tide(i) << std::setw(12) << lp(i)
<< std::setw(12) << (tide(i) + lp(i)) << std::setw(12)
<< (tide(i) + lp(i) + load(i)) << std::setw(12) << load(i)
<< std::endl;
}
std::cout << std::string(110, '=') << std::endl;
The results include:
Short_tide: Short-period tidal elevation
LP_tide: Long-period tidal elevation
Pure_Tide: Combined ocean tide (Short + LP)
Geo_Tide: Total geodetic tide (Pure + Load)
Rad_Tide: Radial loading effect
Complete Example#
Here’s the complete working example:
#include <chrono>
#include <cmath>
#include <complex>
#include <iomanip>
#include <iostream>
#include <list>
#include <netcdf>
#include <numbers>
#include <tuple>
#include <unordered_map>
#include <vector>
#include "fes/tidal_model/cartesian.hpp"
#include "fes/tidal_model/lgp.hpp"
#include "fes/tide.hpp"
// Define the root directory for load tide files
#define LOAD_TIDE_ROOT_DIR "path/to/fes2022_loadtide/"
// Macro to construct full file paths
#define LOAD_TIDE(FILENAME) {LOAD_TIDE_ROOT_DIR FILENAME}
// Define the ocean tide file path (LPG2)
#define OCEAN_TIDE "path/to/fes2022_oceantide/fes2022b_lgp2.nc"
/// @brief Load tide paths for all constituents
/// @return A map of constituent identifiers to their file paths
static std::unordered_map<fes::Constituent, std::string> load_tide_paths() {
return {{fes::Constituent::k2N2, LOAD_TIDE("2n2.nc")},
{fes::Constituent::kEps2, LOAD_TIDE("eps2.nc")},
{fes::Constituent::kJ1, LOAD_TIDE("j1.nc")},
{fes::Constituent::kJ1, LOAD_TIDE("k1.nc")},
{fes::Constituent::kK2, LOAD_TIDE("k2.nc")},
{fes::Constituent::kL2, LOAD_TIDE("l2.nc")},
{fes::Constituent::kLambda2, LOAD_TIDE("lambda2.nc")},
{fes::Constituent::kM2, LOAD_TIDE("m2.nc")},
{fes::Constituent::kM3, LOAD_TIDE("m3.nc")},
{fes::Constituent::kM4, LOAD_TIDE("m4.nc")},
{fes::Constituent::kM6, LOAD_TIDE("m6.nc")},
{fes::Constituent::kM8, LOAD_TIDE("m8.nc")},
{fes::Constituent::kMf, LOAD_TIDE("mf.nc")},
{fes::Constituent::kMKS2, LOAD_TIDE("mks2.nc")},
{fes::Constituent::kMm, LOAD_TIDE("mm.nc")},
{fes::Constituent::kMN4, LOAD_TIDE("mn4.nc")},
{fes::Constituent::kMS4, LOAD_TIDE("ms4.nc")},
{fes::Constituent::kMSf, LOAD_TIDE("msf.nc")},
{fes::Constituent::kMSqm, LOAD_TIDE("msqm.nc")},
{fes::Constituent::kMtm, LOAD_TIDE("mtm.nc")},
{fes::Constituent::kMu2, LOAD_TIDE("mu2.nc")},
{fes::Constituent::kN2, LOAD_TIDE("n2.nc")},
{fes::Constituent::kN4, LOAD_TIDE("n4.nc")},
{fes::Constituent::kNu2, LOAD_TIDE("nu2.nc")},
{fes::Constituent::kO1, LOAD_TIDE("o1.nc")},
{fes::Constituent::kP1, LOAD_TIDE("p1.nc")},
{fes::Constituent::kQ1, LOAD_TIDE("q1.nc")},
{fes::Constituent::kR2, LOAD_TIDE("r2.nc")},
{fes::Constituent::kS1, LOAD_TIDE("s1.nc")},
{fes::Constituent::kS2, LOAD_TIDE("s2.nc")},
{fes::Constituent::kS4, LOAD_TIDE("s4.nc")},
{fes::Constituent::kSa, LOAD_TIDE("sa.nc")},
{fes::Constituent::kSsa, LOAD_TIDE("ssa.nc")},
{fes::Constituent::kT2, LOAD_TIDE("t2.nc")}};
}
/// @brief Get the ocean tide constituents to load
/// @return A list of constituent identifiers
static auto ocean_tide_constituents() -> std::list<fes::Constituent> {
return {fes::k2N2, fes::kEps2, fes::kJ1, fes::kK1, fes::kK2, fes::kL2,
fes::kLambda2, fes::kM2, fes::kM3, fes::kM4, fes::kM6, fes::kM8,
fes::kMKS2, fes::kMN4, fes::kMS4, fes::kMSf, fes::kMf, fes::kMm,
fes::kMSqm, fes::kMtm, fes::kMu2, fes::kN2, fes::kN4, fes::kNu2,
fes::kO1, fes::kP1, fes::kQ1, fes::kR2, fes::kS1, fes::kS2,
fes::kS4, fes::kSa, fes::kSsa, fes::kT2};
}
/// @brief Convert degrees to radians
/// @tparam T Data type
/// @param degrees Value in degrees
/// @return Value in radians
template <typename T>
constexpr auto to_radians(const T& degrees) -> T {
return degrees * std::numbers::pi / 180.0;
}
/// @brief Create a complex wave from amplitude and phase vectors
/// @param amp Amplitude vector
/// @param pha Phase vector in degrees
/// @return A complex vector representing the wave
auto polar(const std::vector<float>& amp, const std::vector<float>& pha)
-> Eigen::VectorXcf {
auto size = amp.size();
if (size != pha.size()) {
throw std::invalid_argument(
"Amplitude and phase vectors must be of the same size.");
}
// Combine amplitude and phase into a complex wave
Eigen::Map<const Eigen::VectorXf> amp_map(amp.data(), size);
Eigen::Map<const Eigen::VectorXf> phase_map(pha.data(), size);
return amp_map.binaryExpr(
phase_map, [](float a, float p) { return std::polar(a, to_radians(p)); });
}
/// @brief Load tide data from a NetCDF file
/// @param ident The constituent identifier
/// @param filename The path to the NetCDF file
/// @param model The tidal model to update
static auto load_tide_data_from_file(
const fes::Constituent ident, const std::string& filename,
std::unique_ptr<fes::tidal_model::Cartesian<float>>& model) -> void {
try {
// Open the NetCDF file for reading
netCDF::NcFile ds(filename, netCDF::NcFile::read);
// Get the dimensions
auto lon_dim = ds.getDim("lon").getSize();
auto lat_dim = ds.getDim("lat").getSize();
// Get the variables
netCDF::NcVar lon_var = ds.getVar("lon");
netCDF::NcVar lat_var = ds.getVar("lat");
netCDF::NcVar amp_var = ds.getVar("amplitude");
netCDF::NcVar phase_var = ds.getVar("phase");
// Ensure variables are valid
assert(!lon_var.isNull());
assert(!lat_var.isNull());
assert(!amp_var.isNull());
assert(!phase_var.isNull());
// Read the data
std::vector<float> amp(lon_dim * lat_dim);
std::vector<float> phase(lon_dim * lat_dim);
amp_var.getVar(amp.data());
phase_var.getVar(phase.data());
// Ensure amplitude and phase data are in column-major order to match
// the cartesian model's expected layout.
assert(amp_var.getDim(0).getSize() == lat_dim);
assert(amp_var.getDim(1).getSize() == lon_dim);
assert(phase_var.getDim(0).getSize() == lat_dim);
assert(phase_var.getDim(1).getSize() == lon_dim);
// Initialize the model if it hasn't been already
if (model == nullptr) {
std::vector<double> lon(lon_dim);
std::vector<double> lat(lat_dim);
// Assumption: longitude and latitude grids are consistent across all
// tidal constituents. Production code should validate this invariant.
lon_var.getVar(lon.data());
lat_var.getVar(lat.data());
auto x_axis =
fes::Axis(Eigen::Map<const Eigen::VectorXd>(lon.data(), lon.size()),
1e-6, true);
auto y_axis =
fes::Axis(Eigen::Map<const Eigen::VectorXd>(lat.data(), lat.size()),
1e-6, false);
model = std::move(std::make_unique<fes::tidal_model::Cartesian<float>>(
std::move(x_axis), std::move(y_axis), fes::TideType::kRadial, false));
}
// Combine amplitude and phase into a complex wave
auto wave = polar(amp, phase);
// Finally, add the constituent to the model
model->add_constituent(ident, std::move(wave));
} catch (netCDF::exceptions::NcException& e) {
std::cerr << "Error opening file: " << e.what() << std::endl;
exit(EXIT_FAILURE);
}
}
/// @brief Load the tide model
/// @return A unique pointer to the tide model
static auto load_load_tide_model()
-> std::unique_ptr<fes::tidal_model::Cartesian<float>> {
auto tide_paths = load_tide_paths();
std::unique_ptr<fes::tidal_model::Cartesian<float>> model = nullptr;
for (const auto& [ident, path] : tide_paths) {
load_tide_data_from_file(ident, path, model);
}
return model;
}
/// @brief Load the ocean tide model from an LPG2 NetCDF file
/// @return A unique pointer to the ocean tide model
static auto load_ocean_tide_model()
-> std::unique_ptr<fes::tidal_model::LGP2<float>> {
try {
auto lpg2_model = std::make_unique<fes::tidal_model::LGP2<float>>();
// Open the NetCDF file for reading
netCDF::NcFile ds(OCEAN_TIDE, netCDF::NcFile::read);
// Get the dimensions
auto coordinates = ds.getDim("coordinates").getSize();
auto triangles = ds.getDim("triangles").getSize();
auto lgp2_nodes = ds.getDim("lgp2_nodes").getSize();
// Read the coordinates
std::vector<double> lon(coordinates);
std::vector<double> lat(coordinates);
ds.getVar("lon").getVar(lon.data());
ds.getVar("lat").getVar(lat.data());
// Read the triangles
std::vector<int32_t> triangles_data(triangles * 3);
ds.getVar("triangle").getVar(triangles_data.data());
// Read the LGP2 codes
std::vector<int32_t> lgp2_codes(triangles * 6);
ds.getVar("lgp2").getVar(lgp2_codes.data());
// Create the mesh index
auto index = std::make_shared<fes::mesh::Index>(
Eigen::Map<const Eigen::VectorXd>(lon.data(), lon.size()),
Eigen::Map<const Eigen::VectorXd>(lat.data(), lat.size()),
Eigen::Map<const Eigen::Matrix<int32_t, -1, 3, Eigen::RowMajor>>(
triangles_data.data(), triangles, 3));
// Initialize the LGP2 model with the mesh index and LGP2 codes
lpg2_model = std::make_unique<fes::tidal_model::LGP2<float>>(
std::move(index),
Eigen::Map<
const Eigen::Matrix<int32_t, Eigen::Dynamic, 6, Eigen::RowMajor>>(
lgp2_codes.data(), triangles, 6),
fes::TideType::kTide, 100'000);
// Read and add each constituent to the model
for (const auto& ident : ocean_tide_constituents()) {
std::string prefix = fes::constituents::name(ident);
std::string var_name = prefix + "_amplitude";
// Get the amplitude variable
auto amp = ds.getVar(var_name);
if (amp.isNull()) {
throw std::runtime_error("Variable " + var_name +
" not found in the dataset.");
}
// Read the amplitude data
std::vector<float> amplitude(lgp2_nodes);
amp.getVar(amplitude.data());
// Get the phase variable
var_name = prefix + "_phase";
auto phase = ds.getVar(var_name);
if (phase.isNull()) {
throw std::runtime_error("Variable " + var_name +
" not found in the dataset.");
}
// Read the phase data
std::vector<float> phase_data(lgp2_nodes);
phase.getVar(phase_data.data());
// Combine amplitude and phase into a complex wave
auto wave = polar(amplitude, phase_data);
// Add the constituent to the model
lpg2_model->add_constituent(ident, std::move(wave));
}
return lpg2_model;
} catch (netCDF::exceptions::NcException& e) {
std::cerr << "Error opening file: " << e.what() << std::endl;
exit(EXIT_FAILURE);
}
}
int main() {
// Load the load tide model
auto radial_handler = load_load_tide_model();
// Load the ocean tide model
auto tide_handler = load_ocean_tide_model();
// Create the FES settings
auto settings = fes::Settings{fes::angle::Formulae::kIERS, 0.0};
// Create a timeseries of dates starting from the first january 1983
Eigen::VectorXd times(24);
auto start_date = std::chrono::year{1983} / std::chrono::January / 1;
auto start = std::chrono::sys_days{start_date};
for (int64_t i = 0; i < times.size(); ++i) {
auto current = start + std::chrono::days(i);
times[i] =
std::chrono::duration<double>(current.time_since_epoch()).count();
}
// Setup the leap seconds (TAI-UTC)
// In production, these should be set according to the dates
auto leap_seconds = Eigen::Vector<uint16_t, -1>::Constant(times.size(), 21);
// Set the location (longitude, latitude) in degrees
auto lon = Eigen::VectorXd::Constant(times.size(), -7.688);
auto lat = Eigen::VectorXd::Constant(times.size(), 59.195);
// Evaluate the radial load tide (long period and interpolation quality are
// ignored)
Eigen::VectorXd load = std::get<0>(fes::evaluate_tide(
radial_handler.get(), times, leap_seconds, lon, lat, settings));
// Evaluate the ocean tide (interpolation quality is ignored here, but in
// production you should check this to differentiate between interpolated and
// extrapolated points)
Eigen::VectorXd tide;
Eigen::VectorXd lp;
std::tie(tide, lp, std::ignore) = fes::evaluate_tide(
tide_handler.get(), times, leap_seconds, lon, lat, settings);
// Print header with better formatting
std::cout << "\n" << std::string(110, '=') << std::endl;
std::cout << " FES2022 TIDE PREDICTION RESULTS"
<< std::endl;
std::cout << std::string(110, '=') << std::endl;
std::cout << std::left << std::setw(12) << "Date" << std::setw(6) << "Hour"
<< std::right << std::setw(12) << "Latitude" << std::setw(12)
<< "Longitude" << std::setw(12) << "Short_tide" << std::setw(12)
<< "LP_tide" << std::setw(12) << "Pure_Tide" << std::setw(12)
<< "Geo_Tide" << std::setw(12) << "Rad_Tide" << std::endl;
std::cout << std::string(110, '-') << std::endl;
// Print results for each time point
for (int64_t i = 0; i < times.size(); ++i) {
// Convert Unix timestamp back to date
auto time_point =
std::chrono::system_clock::from_time_t(static_cast<time_t>(times[i]));
auto date = std::chrono::floor<std::chrono::days>(time_point);
auto time_of_day = time_point - date;
auto hours = std::chrono::duration_cast<std::chrono::hours>(time_of_day);
// Get year/month/day
auto ymd = std::chrono::year_month_day{date};
// Format date as YYYY-MM-DD
std::cout << std::left << std::setw(4) << static_cast<int>(ymd.year())
<< "-" << std::setw(2) << std::setfill('0')
<< static_cast<unsigned>(ymd.month()) << "-" << std::setw(2)
<< std::setfill('0') << static_cast<unsigned>(ymd.day()) << " "
<< std::setfill(' ') << std::setw(5) << hours.count() << "h";
// Format the tide data with proper alignment and precision
std::cout << std::right << std::fixed << std::setprecision(3)
<< std::setw(12) << lat(i) << std::setw(12) << lon(i)
<< std::setw(12) << tide(i) << std::setw(12) << lp(i)
<< std::setw(12) << (tide(i) + lp(i)) << std::setw(12)
<< (tide(i) + lp(i) + load(i)) << std::setw(12) << load(i)
<< std::endl;
}
std::cout << std::string(110, '=') << std::endl;
return 0;
}
Expected Output#
When you run this example, you should see output similar to:
==============================================================================================================
FES2022 TIDE PREDICTION RESULTS
==============================================================================================================
Date Hour Latitude Longitude Short_tide LP_tide Pure_Tide Geo_Tide Rad_Tide
--------------------------------------------------------------------------------------------------------------
1983-10-10 0 h 59.195 -7.688 -92.468 0.925 -91.543 -88.976 2.568
1983-10-20 0 h 59.195 -7.688 -44.481 0.185 -44.296 -42.692 1.604
1983-10-30 0 h 59.195 -7.688 6.376 -0.629 5.746 6.167 0.420
...
The output columns represent:
Date: Prediction date (YYYY-MM-DD format)
Hour: Hour of the day (0-23)
Latitude/Longitude: Location coordinates in degrees
Short_tide: Short-period ocean tide in meters
LP_tide: Long-period ocean tide in meters
Pure_Tide: Total ocean tide (Short + LP) in meters
Geo_Tide: Total geodetic tide including load effects in meters
Rad_Tide: Radial load tide effect in meters
Compilation Instructions#
To compile and run this example, you need to link against the required libraries:
# Compile the example
g++ -O2 -Wall prediction.cpp --std=c++20 \
-I ../include/ \
-I /path/to/include \
-I /path/to/include/eigen3 \
-L/path/to/fes_library -lfes \
-L/path/to/lib -lnetcdf-cxx4 -lnetcdf \
-o tide_prediction
# Run the example
LD_LIBRARY_PATH=/path/to/lib ./tide_prediction
Important Notes:
Replace
/path/to/
with your actual NetCDF installation pathEnsure the FES library (
libfes.a
orlibfes.so
) is available in your library pathUpdate the data file paths in the source code to match your FES2022 data location
Integration with CMake#
For production use, integrate the library with CMake:
# CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
project(TidePrediction)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Find required packages
find_package(Eigen3 REQUIRED)
find_package(PkgConfig REQUIRED)
pkg_check_modules(NETCDF_CXX REQUIRED netcdf-cxx4)
# Find FES library
find_library(FES_LIBRARY fes REQUIRED)
find_path(FES_INCLUDE_DIR fes/tide.hpp REQUIRED)
# Create executable
add_executable(tide_prediction prediction.cpp)
# Link libraries
target_link_libraries(tide_prediction
${FES_LIBRARY}
${NETCDF_CXX_LIBRARIES}
Eigen3::Eigen
)
target_include_directories(tide_prediction PRIVATE
${FES_INCLUDE_DIR}
${NETCDF_CXX_INCLUDE_DIRS}
)