Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 123 additions & 0 deletions gz_waves_provider_gerstner/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
cmake_minimum_required(VERSION 3.16)
project(gz_waves_provider_gerstner)

if(NOT CMAKE_CXX_STANDARD)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
endif()

if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
add_compile_options(-Wall -Wextra -Wpedantic)
endif()

find_package(ament_cmake REQUIRED)
# gz_waves's exported library links gz-sim PUBLIC (the Wavefield component), so
# gz-sim::core must be resolvable before find_package(gz_waves). The core also
# provides WavesSystemBase, the base for the gerstner system plugin below.
find_package(gz_sim_vendor REQUIRED)
find_package(gz-sim REQUIRED)
find_package(gz_waves REQUIRED) # IWaveField + WaveParameters + base
find_package(gz_plugin_vendor REQUIRED)
find_package(gz-plugin REQUIRED COMPONENTS register)
find_package(gz_math_vendor REQUIRED)
find_package(gz-math REQUIRED) # gz::math vectors + GZ_PI
find_package(gz_common_vendor REQUIRED)
find_package(gz-common REQUIRED) # gzerr/gzwarn console

# --------------------------------------------------------------------------
# Gerstner wave-field engine (libgz-waves-provider-gerstner): a plain analytic
# IWaveField implementation. It is LINKED directly by the gerstner system
# plugin (server) and the water visual (GUI), not discovered by a runtime
# loader, so it carries no GZ_ADD_PLUGIN and is exported as a normal target.
# MakeGerstnerWaveField() is the factory consumers register under "gerstner".
# --------------------------------------------------------------------------
add_library(gz-waves-provider-gerstner SHARED
src/GerstnerWaveSimulation.cc
)
target_include_directories(gz-waves-provider-gerstner PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>)
target_include_directories(gz-waves-provider-gerstner PRIVATE
${gz_waves_INCLUDE_DIRS})
target_link_libraries(gz-waves-provider-gerstner
PUBLIC gz-math::gz-math
PRIVATE gz-common::gz-common)

# --------------------------------------------------------------------------
# Gerstner wave source system (gz-sim-waves-gerstner-system): a WavesSystemBase
# subclass that builds the Gerstner engine. This is the gz-plugin SDF loads by
# filename; it links the engine directly, so no runtime engine loader is used.
# --------------------------------------------------------------------------
add_library(gz-sim-waves-gerstner-system SHARED
src/GerstnerWavesSystem.cc
)
target_include_directories(gz-sim-waves-gerstner-system PRIVATE
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
${gz_waves_INCLUDE_DIRS})
target_link_libraries(gz-sim-waves-gerstner-system
PRIVATE
gz_waves::gz_waves
gz-waves-provider-gerstner
gz-sim::core
gz-plugin::register)

# --------------------------------------------------------------------------
# Gerstner GUI registrar (gz-sim-waves-gerstner-gui): loaded in the GUI process
# alongside WaterVisual. It registers the "gerstner" factory so the visual can
# rebuild its own thread-private engine from the replicated recipe via
# CreateWaveSimulation. This is what lets gz_waves_rendering stay
# provider-agnostic (core-only): the engine dependency lives here, the GUI-side
# mirror of gz-sim-waves-gerstner-system.
# --------------------------------------------------------------------------
add_library(gz-sim-waves-gerstner-gui SHARED
src/GerstnerWavesGui.cc
)
target_include_directories(gz-sim-waves-gerstner-gui PRIVATE
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
${gz_waves_INCLUDE_DIRS})
target_link_libraries(gz-sim-waves-gerstner-gui
PRIVATE
gz_waves::gz_waves
gz-waves-provider-gerstner
gz-sim::core
gz-plugin::register)

install(
TARGETS gz-waves-provider-gerstner
EXPORT export_gz_waves_provider_gerstner
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib
RUNTIME DESTINATION bin
)
install(
TARGETS gz-sim-waves-gerstner-system gz-sim-waves-gerstner-gui
LIBRARY DESTINATION lib
RUNTIME DESTINATION bin
)
install(DIRECTORY include/ DESTINATION include)

ament_export_targets(export_gz_waves_provider_gerstner HAS_LIBRARY_TARGET)
ament_export_include_directories(include)
ament_export_dependencies(gz_math_vendor)

if(BUILD_TESTING)
find_package(ament_lint_auto REQUIRED)
set(ament_cmake_copyright_FOUND TRUE)
# Gazebo (Allman) style, matching the upstream gz-sim source.
set(ament_cmake_cpplint_FOUND TRUE)
set(ament_cmake_uncrustify_FOUND TRUE)
ament_lint_auto_find_test_dependencies()

# Links the engine directly and registers its "gerstner" factory in-binary
# (see kGerstnerRegistered in the test), so the CreateWaveSimulation tests
# resolve via the registry — no dlopen, no GzPluginHook.
find_package(ament_cmake_gtest REQUIRED)
ament_add_gtest(gerstner_test test/gerstner_test.cc)
target_include_directories(gerstner_test PRIVATE
${gz_waves_INCLUDE_DIRS})
target_link_libraries(gerstner_test
gz_waves::gz_waves gz-waves-provider-gerstner)
endif()

ament_package()
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/*
* Copyright (C) 2026 Honu Robotics
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*/

#ifndef GZ_SIM_WAVES_GERSTNERWAVESIMULATION_HH_
#define GZ_SIM_WAVES_GERSTNERWAVESIMULATION_HH_

#include <memory>
#include <vector>

#include <gz/math/Vector2.hh>
#include <gz/math/Vector3.hh>

#include "gz/sim/waves/WaveSimulation.hh"
#include "gz/sim/waves/Wavefield.hh"

namespace gz::sim::waves
{

/// \brief Analytic sum-of-Gerstners wave model. Closed-form for each of up
/// to N component waves; deterministic; unbounded in space.
class GerstnerWaveSimulation final : public IWaveField
{
/// \brief Default-construct an unconfigured field. Call `SetParameters`
/// before sampling. Used by the engine factory (`MakeGerstnerWaveField`).
public: GerstnerWaveSimulation();

/// \brief Construct and sample the spectrum from `_params` (convenience;
/// equivalent to default-construct followed by `SetParameters`).
/// \param[in] _params Wave parameters to build the field from.
public: explicit GerstnerWaveSimulation(const WaveParameters &_params);

/// \brief Destructor.
public: ~GerstnerWaveSimulation() override = default;

// Documentation inherited
public: void SetParameters(const WaveParameters &_params) override;
// Documentation inherited
public: double Elevation(double _x, double _y, double _t) const override;
// Documentation inherited
public: gz::math::Vector3d ParticleVelocity(
double _x, double _y, double _t) const override;
// Documentation inherited
public: gz::math::Vector3d Normal(
double _x, double _y, double _t) const override;
// Documentation inherited
public: double Jacobian(double _x, double _y, double _t) const override;
// Documentation inherited
public: void Update(double _simTime) override;
// Documentation inherited
public: const WaveField2D *Field() const override;
// Documentation inherited
public: std::string_view Kind() const override { return "gerstner"; }

// ---- Backend-specific introspection accessors (exercised by the unit
// tests; not part of the IWaveField interface — WaterVisual consumes
// Field() instead) ----

/// \brief Per-component wave amplitudes [m].
public: const std::vector<double> &Amplitudes() const
{
return this->amplitudes;
}
/// \brief Per-component wavenumbers |k| [rad/m].
public: const std::vector<double> &Wavenumbers() const
{
return this->wavenumbers;
}
/// \brief Per-component angular frequencies ω [rad/s].
public: const std::vector<double> &AngularFrequencies() const
{
return this->angularFrequencies;
}
/// \brief Per-component Gerstner steepness in [0, 1].
public: const std::vector<double> &Steepnesses() const
{
return this->steepnesses;
}
/// \brief Per-component unit propagation directions.
public: const std::vector<gz::math::Vector2d> &Directions() const
{
return this->directions;
}

/// \brief Phase θ_i(x, y, t) = k_i·(d_i·(x, y)) − ω_i·t + φ of component `_i`
/// at world point (x, y) and time t. Shared by every per-component sum
/// (Elevation / ParticleVelocity / Normal / Jacobian / the render grid) so
/// the phase is computed one way only.
/// \param[in] _i Component index (< number of components).
/// \param[in] _x World-frame x coordinate [m].
/// \param[in] _y World-frame y coordinate [m].
/// \param[in] _t Simulation time [s].
/// \return The component's phase [rad].
private: double Phase(std::size_t _i, double _x, double _y, double _t) const;

/// \brief Per-component wave amplitudes [m].
private: std::vector<double> amplitudes;
/// \brief Per-component wavenumbers |k| [rad/m].
private: std::vector<double> wavenumbers;
/// \brief Per-component angular frequencies ω [rad/s].
private: std::vector<double> angularFrequencies;
/// \brief Per-component Gerstner steepness in [0, 1].
private: std::vector<double> steepnesses;
/// \brief Per-component unit propagation directions.
private: std::vector<gz::math::Vector2d> directions;
/// \brief Startup-ramp time constant τ [s].
private: double tau{2.0};
/// \brief Common phase offset φ [rad].
private: double phase{0.0};

// Render grid: the analytic field sampled onto an N×N tile each Update,
// exposed via Field() as the backend-agnostic rendering contract. Buffers
// are column-major; Update overwrites them in place, so field's data()
// pointers (bound in SetParameters) stay valid.

/// \brief Render-grid resolution per axis (N).
private: std::size_t fieldN{128};
/// \brief Render-grid tile extent [m].
private: double fieldTile{0.0};
/// \brief Sim time the render grid was last sampled at.
private: double currentTime{-1.0};
/// \brief Column-major vertical-displacement buffer for `field.dz`.
private: std::vector<double> dzBuf;
/// \brief Column-major x-chop buffer for `field.dx`.
private: std::vector<double> dxBuf;
/// \brief Column-major y-chop buffer for `field.dy`.
private: std::vector<double> dyBuf;
/// \brief Column-major folding/foam buffer for `field.foam`.
private: std::vector<double> foamBuf;
/// \brief The rendering contract returned by Field().
private: WaveField2D field;
};

/// \brief Factory: a default-constructed Gerstner wave-field engine (apply
/// `SetParameters` before use). Registered under the "gerstner" token so
/// `CreateWaveSimulation` can rebuild the engine from a serialized `Wavefield`
/// component on the GUI side.
std::shared_ptr<IWaveField> MakeGerstnerWaveField();

} // namespace gz::sim::waves

#endif // GZ_SIM_WAVES_GERSTNERWAVESIMULATION_HH_
30 changes: 30 additions & 0 deletions gz_waves_provider_gerstner/package.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3">
<name>gz_waves_provider_gerstner</name>
<version>0.0.0</version>
<description>
Analytic Gerstner (sum-of-sines) wave-field engine for gz_waves. A plain
IWaveField implementation registered in the in-process engine registry and
linked by the gz-sim-waves-gerstner-system plugin — and the simplest
template for writing your own provider package.
</description>
<maintainer email="cen.aguero@gmail.com">Carlos Agüero</maintainer>
<license>Apache-2.0</license>

<buildtool_depend>ament_cmake</buildtool_depend>

<depend>gz_waves</depend>
<depend>gz_sim_vendor</depend>
<depend>gz_math_vendor</depend>
<depend>gz_common_vendor</depend>
<depend>gz_plugin_vendor</depend>

<test_depend>ament_cmake_gtest</test_depend>
<test_depend>ament_lint_auto</test_depend>
<test_depend>ament_lint_common</test_depend>

<export>
<build_type>ament_cmake</build_type>
</export>
</package>
Loading