Skip to content

Commit 3fa8886

Browse files
committed
improve TestHydro docs and indexing clarity
1 parent c964086 commit 3fa8886

8 files changed

Lines changed: 252 additions & 42 deletions

File tree

include/hydroc/hydro_forces.h

Lines changed: 101 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,12 @@ class ChronoHydroCoupler;
9090
class ForceFunc6d;
9191
class TestHydro;
9292

93+
// ─────────────────────────────────────────────────────────────────────────────
94+
// Degrees of Freedom Constant
95+
// ─────────────────────────────────────────────────────────────────────────────
96+
/// Number of degrees of freedom per rigid body: surge, sway, heave, roll, pitch, yaw.
97+
constexpr int kDofPerBody = 6;
98+
9399
/**
94100
* @brief Per-DOF force function for Chrono's ChForce system.
95101
*
@@ -193,9 +199,13 @@ class ForceFunc6d {
193199
void ApplyForceAndTorqueToBody();
194200

195201
std::shared_ptr<chrono::ChBody> body_; ///< Pointer to the body this force is applied to.
196-
int b_num_; ///< Body's index in the system. Currently 1-indexed.
197-
ComponentFunc forces_[6]; ///< Forces for each degree of freedom.
198-
std::shared_ptr<ComponentFunc> force_ptrs_[6]; ///< Pointers to the forces.
202+
203+
/// Body index in the system (1-indexed, parsed from Chrono body name "bodyN").
204+
/// This matches Chrono's body naming convention where bodies are named body1, body2, etc.
205+
int b_num_;
206+
207+
ComponentFunc forces_[kDofPerBody]; ///< Forces for each degree of freedom.
208+
std::shared_ptr<ComponentFunc> force_ptrs_[kDofPerBody]; ///< Pointers to the forces.
199209
std::shared_ptr<chrono::ChForce> chrono_force_; ///< Chrono force for the body.
200210
std::shared_ptr<chrono::ChForce> chrono_torque_; ///< Chrono torque for the body.
201211
TestHydro* all_hydro_forces_; ///< Pointer to TestHydro for calculations.
@@ -214,8 +224,65 @@ struct HydroProfileStats {
214224
int waves_calls = 0;
215225
};
216226

217-
// TODO: Rename TestHydro for clarity, perhaps to HydroForces?
218-
// TODO: Split TestHydro class from its helper classes for clearer code structure.
227+
// ═══════════════════════════════════════════════════════════════════════════════
228+
//
229+
// TestHydro — Primary Hydrodynamics Façade / Adapter
230+
//
231+
// ═══════════════════════════════════════════════════════════════════════════════
232+
/**
233+
* @brief Primary hydrodynamics façade for HydroChrono applications.
234+
*
235+
* TestHydro is the main user-facing object for attaching hydrodynamic forces to
236+
* Chrono bodies. Despite its name (a historical artifact), it is NOT a test-only
237+
* class — it is the production hydrodynamics adapter used by all workflows.
238+
*
239+
* @note The name "TestHydro" is retained for backward compatibility. New code
240+
* may use the alias `HydroForces` for clarity (see below).
241+
*
242+
* ## What It Does
243+
*
244+
* TestHydro bridges three layers:
245+
* 1. **Chrono bodies** — The physical bodies in the Chrono simulation.
246+
* 2. **HydroSystem (core)** — Chrono-free hydrodynamic force computation.
247+
* 3. **Chrono force callbacks** — ChForce functions that Chrono calls each timestep.
248+
*
249+
* ## Typical Usage
250+
*
251+
* @code{.cpp}
252+
* // Create Chrono bodies
253+
* auto body1 = chrono_types::make_shared<ChBody>();
254+
* body1->SetName("body1");
255+
* system.Add(body1);
256+
*
257+
* // Attach hydrodynamic forces (with or without waves)
258+
* std::vector<std::shared_ptr<ChBody>> bodies = {body1};
259+
* TestHydro hydro(bodies, "path/to/hydro_data.h5");
260+
* hydro.AddWaves(std::make_shared<RegularWave>(...));
261+
*
262+
* // Run simulation — Chrono automatically queries TestHydro for forces
263+
* @endcode
264+
*
265+
* ## Used By
266+
*
267+
* - **C++ tests and demos** — Directly instantiate TestHydro with bodies and H5 file.
268+
* - **YAML runner (hydrochrono.exe)** — SetupHydroFromYAML creates TestHydro internally.
269+
*
270+
* ## Architecture Notes
271+
*
272+
* - Internally delegates to HydroSystem + ChronoHydroCoupler for force computation.
273+
* - Maintains velocity history for radiation damping convolution.
274+
* - Caches forces per timestep to avoid redundant computation.
275+
*
276+
* ## Body Indexing Convention
277+
*
278+
* Chrono bodies must be named "body1", "body2", etc. The numeric suffix is parsed
279+
* to determine body order. Internally, body indices are **1-based** in the callback
280+
* interface (CoordinateFuncForBody) to match this naming convention.
281+
*
282+
* @see HydroForces (alias below)
283+
* @see HydroSystem (Chrono-free hydrodynamic core)
284+
* @see ChronoHydroCoupler (extracts state from Chrono bodies)
285+
*/
219286
class TestHydro {
220287
public:
221288
TestHydro() = delete;
@@ -328,13 +395,20 @@ class TestHydro {
328395
/**
329396
* @brief Calculates or retrieves the total force on a specific body in a particular degree of freedom.
330397
*
331-
* If the total force for the body and DOF was computed for the current timestep, it's retrieved.
332-
* Otherwise, the function calculates it. Note: Body index is 1-based here due to its origin from ForceFunc6d.
398+
* This is the main entry point called by Chrono's ChForce callbacks during simulation.
399+
* Forces are computed once per timestep and cached; subsequent calls within the same
400+
* timestep return the cached value.
401+
*
402+
* @note **1-Based Body Indexing**: The body index `b` is 1-based (body1 → b=1, body2 → b=2, etc.)
403+
* to match Chrono's body naming convention. This is intentional and matches how
404+
* ForceFunc6d parses body names like "body1", "body2", etc.
333405
*
334-
* @param b Body index (1-based due to source from ForceFunc6d).
335-
* @param i Degree of Freedom (DOF) index, ranging from [0,...5].
406+
* @param b Body index (1-based: valid range is [1, num_bodies]).
407+
* @param i Degree of Freedom (DOF) index (0-based: 0=surge, 1=sway, 2=heave, 3=roll, 4=pitch, 5=yaw).
336408
*
337-
* @return Component of the force vector for body 'b' and DOF 'i'.
409+
* @return Force component for body 'b' in DOF 'i' (Newtons for forces, Newton-meters for torques).
410+
*
411+
* @throws std::out_of_range if b or i are out of valid range.
338412
*/
339413
double CoordinateFuncForBody(int b, int i);
340414

@@ -459,4 +533,21 @@ class TestHydro {
459533
void EnsureHydroSystemAndCoupler();
460534
};
461535

536+
// ─────────────────────────────────────────────────────────────────────────────
537+
// Public Alias for Clarity
538+
// ─────────────────────────────────────────────────────────────────────────────
539+
/**
540+
* @brief Alias for TestHydro — the primary hydrodynamics façade.
541+
*
542+
* HydroForces is the recommended name for new code. The underlying class
543+
* is TestHydro (historical name retained for backward compatibility).
544+
*
545+
* Usage:
546+
* @code{.cpp}
547+
* HydroForces hydro(bodies, "hydro_data.h5");
548+
* hydro.AddWaves(my_waves);
549+
* @endcode
550+
*/
551+
using HydroForces = TestHydro;
552+
462553
#endif

src/hydro/chrono/chrono_hydro_coupler.cpp

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
#include <hydroc/coupling/chrono_coupler.h>
77
#include "chrono_state_utils.h"
88

9-
#include <cassert>
9+
#include <stdexcept>
1010

1111
namespace hydrochrono::hydro {
1212

@@ -15,8 +15,17 @@ ChronoHydroCoupler::ChronoHydroCoupler(
1515
std::vector<std::shared_ptr<chrono::ChBody>> bodies)
1616
: hydro_system_(hydro_system),
1717
bodies_(std::move(bodies)) {
18-
assert(hydro_system_ != nullptr);
19-
assert(!bodies_.empty());
18+
// HydroSystem is required for force evaluation.
19+
if (hydro_system_ == nullptr) {
20+
throw std::invalid_argument(
21+
"ChronoHydroCoupler: hydro_system pointer must not be null");
22+
}
23+
24+
// At least one body is required for hydrodynamic coupling.
25+
if (bodies_.empty()) {
26+
throw std::invalid_argument(
27+
"ChronoHydroCoupler: bodies vector must not be empty");
28+
}
2029
}
2130

2231
BodyForces ChronoHydroCoupler::Evaluate(double time) {

src/hydro/core/hydro_system.cpp

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55

66
#include <hydroc/core/hydro_system.h>
77

8-
#include <cassert>
98
#include <chrono>
9+
#include <stdexcept>
1010
#include <Eigen/Dense>
1111

1212
namespace hydrochrono::hydro {
@@ -17,7 +17,11 @@ HydroSystem::HydroSystem(int num_bodies,
1717
components_(std::move(components)),
1818
profile_stats_(),
1919
profiling_enabled_(false) {
20-
assert(num_bodies_ > 0);
20+
// Require at least one body; otherwise hydrodynamic forces are meaningless.
21+
if (num_bodies_ <= 0) {
22+
throw std::invalid_argument(
23+
"HydroSystem: num_bodies must be > 0 (got " + std::to_string(num_bodies_) + ")");
24+
}
2125
}
2226

2327
BodyForces HydroSystem::Evaluate(const SystemState& state, double time) {

src/hydro/force_components/excitation_component.cpp

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,35 @@
55

66
#include "excitation_component.h"
77

8-
#include <cassert>
8+
#include <stdexcept>
99
#include <Eigen/Dense>
1010

1111
namespace hydrochrono::hydro {
1212

1313
ExcitationComponent::ExcitationComponent(std::shared_ptr<WaveBase> waves, int num_bodies)
1414
: waves_(waves), num_bodies_(num_bodies) {
15-
assert(waves_ != nullptr);
16-
assert(num_bodies_ > 0);
15+
// Wave model is required for excitation force computation.
16+
if (waves_ == nullptr) {
17+
throw std::invalid_argument(
18+
"ExcitationComponent: waves pointer must not be null");
19+
}
20+
21+
// Require at least one body for wave excitation computation.
22+
if (num_bodies_ <= 0) {
23+
throw std::invalid_argument(
24+
"ExcitationComponent: num_bodies must be > 0 (got " + std::to_string(num_bodies_) + ")");
25+
}
1726
}
1827

1928
void ExcitationComponent::Compute(const SystemState& state,
2029
double time,
2130
BodyForces& inout_forces) {
22-
assert(static_cast<int>(inout_forces.size()) == num_bodies_);
31+
// Internal consistency check: forces must match expected body count.
32+
if (static_cast<int>(inout_forces.size()) != num_bodies_) {
33+
throw std::runtime_error(
34+
"ExcitationComponent::Compute: inout_forces.size() mismatch (expected " +
35+
std::to_string(num_bodies_) + ", got " + std::to_string(inout_forces.size()) + ")");
36+
}
2337

2438
// Get wave excitation forces from WaveBase (returns flat 6N vector)
2539
Eigen::VectorXd force_flat = waves_->GetForceAtTime(time);

src/hydro/force_components/hydrostatics_component.cpp

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
#include <hydroc/io/h5_reader.h>
88

99
#include <Eigen/Dense>
10-
#include <cassert>
10+
#include <stdexcept>
1111

1212
namespace hydrochrono::hydro {
1313

@@ -22,14 +22,46 @@ HydrostaticsComponent::HydrostaticsComponent(
2222
equilibrium_(equilibrium),
2323
cb_minus_cg_(cb_minus_cg),
2424
gravitational_acceleration_(gravitational_acceleration) {
25-
assert(num_bodies_ > 0);
25+
// Require at least one body for hydrostatic force computation.
26+
if (num_bodies_ <= 0) {
27+
throw std::invalid_argument(
28+
"HydrostaticsComponent: num_bodies must be > 0 (got " + std::to_string(num_bodies_) + ")");
29+
}
30+
31+
// Validate equilibrium array size: must have 6 DOF per body.
32+
const int expected_equilibrium_size = kDofPerBody * num_bodies_;
33+
if (static_cast<int>(equilibrium_.size()) != expected_equilibrium_size) {
34+
throw std::invalid_argument(
35+
"HydrostaticsComponent: equilibrium array size mismatch (expected " +
36+
std::to_string(expected_equilibrium_size) + ", got " +
37+
std::to_string(equilibrium_.size()) + ")");
38+
}
39+
40+
// Validate cb_minus_cg array size: must have 3 components per body.
41+
const int expected_cb_cg_size = kDofLinOrRot * num_bodies_;
42+
if (static_cast<int>(cb_minus_cg_.size()) != expected_cb_cg_size) {
43+
throw std::invalid_argument(
44+
"HydrostaticsComponent: cb_minus_cg array size mismatch (expected " +
45+
std::to_string(expected_cb_cg_size) + ", got " +
46+
std::to_string(cb_minus_cg_.size()) + ")");
47+
}
2648
}
2749

2850
void HydrostaticsComponent::Compute(const SystemState& state,
2951
double time,
3052
BodyForces& inout_forces) {
31-
assert(static_cast<int>(state.bodies.size()) == num_bodies_);
32-
assert(static_cast<int>(inout_forces.size()) == num_bodies_);
53+
// Internal consistency check: state and forces must match expected body count.
54+
// This can fail if HydroSystem is misused (integration bug, not user config error).
55+
if (static_cast<int>(state.bodies.size()) != num_bodies_) {
56+
throw std::runtime_error(
57+
"HydrostaticsComponent::Compute: state.bodies.size() mismatch (expected " +
58+
std::to_string(num_bodies_) + ", got " + std::to_string(state.bodies.size()) + ")");
59+
}
60+
if (static_cast<int>(inout_forces.size()) != num_bodies_) {
61+
throw std::runtime_error(
62+
"HydrostaticsComponent::Compute: inout_forces.size() mismatch (expected " +
63+
std::to_string(num_bodies_) + ", got " + std::to_string(inout_forces.size()) + ")");
64+
}
3365

3466
const double rho = file_info_.GetRhoVal();
3567
const double rho_times_g = rho * gravitational_acceleration_.norm();

src/hydro/force_components/radiation_component.cpp

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
#include <hydroc/io/h5_reader.h>
99

1010
#include <Eigen/Dense>
11-
#include <cassert>
1211
#include <algorithm>
1312
#include <stdexcept>
1413

@@ -33,7 +32,30 @@ RadiationComponent::RadiationComponent(
3332
rirf_time_vector_(rirf_time_vector),
3433
rirf_width_vector_(rirf_width_vector),
3534
rirf_processed_ready_(false) {
36-
assert(num_bodies_ > 0);
35+
// Require at least one body for radiation damping computation.
36+
if (num_bodies_ <= 0) {
37+
throw std::invalid_argument(
38+
"RadiationComponent: num_bodies must be > 0 (got " + std::to_string(num_bodies_) + ")");
39+
}
40+
41+
// Validate RIRF time series data from HDF5.
42+
if (rirf_steps <= 0) {
43+
throw std::invalid_argument(
44+
"RadiationComponent: rirf_steps must be > 0 (got " + std::to_string(rirf_steps) +
45+
"). Check that HDF5 file contains valid RIRF data.");
46+
}
47+
48+
// Validate RIRF time and width vectors match the expected size.
49+
if (static_cast<int>(rirf_time_vector_.size()) != rirf_steps) {
50+
throw std::invalid_argument(
51+
"RadiationComponent: rirf_time_vector size mismatch (expected " +
52+
std::to_string(rirf_steps) + ", got " + std::to_string(rirf_time_vector_.size()) + ")");
53+
}
54+
if (static_cast<int>(rirf_width_vector_.size()) != rirf_steps) {
55+
throw std::invalid_argument(
56+
"RadiationComponent: rirf_width_vector size mismatch (expected " +
57+
std::to_string(rirf_steps) + ", got " + std::to_string(rirf_width_vector_.size()) + ")");
58+
}
3759
}
3860

3961
void RadiationComponent::EnsureProcessedRIRF() {
@@ -78,13 +100,17 @@ double RadiationComponent::GetRIRFval(int row, int col, int st) {
78100
void RadiationComponent::Compute(const SystemState& state,
79101
double time,
80102
BodyForces& inout_forces) {
81-
assert(static_cast<int>(state.bodies.size()) == num_bodies_);
82-
assert(static_cast<int>(inout_forces.size()) == num_bodies_);
83-
84-
const int rirf_steps = file_info_.GetRIRFDims(2);
85-
const int total_dofs = kDofPerBody * num_bodies_;
86-
87-
assert(total_dofs > 0 && rirf_steps > 0);
103+
// Internal consistency check: state and forces must match expected body count.
104+
if (static_cast<int>(state.bodies.size()) != num_bodies_) {
105+
throw std::runtime_error(
106+
"RadiationComponent::Compute: state.bodies.size() mismatch (expected " +
107+
std::to_string(num_bodies_) + ", got " + std::to_string(state.bodies.size()) + ")");
108+
}
109+
if (static_cast<int>(inout_forces.size()) != num_bodies_) {
110+
throw std::runtime_error(
111+
"RadiationComponent::Compute: inout_forces.size() mismatch (expected " +
112+
std::to_string(num_bodies_) + ", got " + std::to_string(inout_forces.size()) + ")");
113+
}
88114

89115
// If using TaperedDirect, ensure processed kernel is ready
90116
if (convolution_mode_ == RadiationConvolutionMode::TaperedDirect) {

0 commit comments

Comments
 (0)