From a0d34cc6e6948f76b339e9bbc316cdc943f3a223 Mon Sep 17 00:00:00 2001 From: Salman Husain Date: Wed, 10 Jun 2026 16:41:20 -0600 Subject: [PATCH 1/5] build(linux): prefer Chrono's bundled yaml-cpp and adapt to Chrono 10 API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two small build fixes that together get SEA-Stack compiling cleanly on Linux against Chrono 10.0.0. * adapters/chrono/CMakeLists.txt — prefer Chrono::yaml-cpp over a system yaml-cpp when both are visible. Mixing Chrono's bundled yaml-cpp headers (from chrono_thirdparty/yaml-cpp/include) with a system libyaml-cpp.so triggers link errors against internal symbols such as YAML::FpToString and YAML::Emitter::Write. * apps/seastack/single_run.cpp — Chrono 10 moved the protected m_script_directory member of ChParserMbsYAML into an internal file handler; the script directory must now be set via m_file_handler.SetReferenceDirectory(dir). --- adapters/chrono/CMakeLists.txt | 28 +++++++++++++++++----------- apps/seastack/single_run.cpp | 2 +- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/adapters/chrono/CMakeLists.txt b/adapters/chrono/CMakeLists.txt index 46f30a5..ae95378 100644 --- a/adapters/chrono/CMakeLists.txt +++ b/adapters/chrono/CMakeLists.txt @@ -24,19 +24,25 @@ target_include_directories(seastack_chrono_adapter # Chrono MBS YAML registers TSDA/RSDA force functors without copying damping onto the link; # simulation_export reads damping_coefficient from model YAML when GetDampingCoefficient()==0. -find_package(yaml-cpp CONFIG QUIET) -if(NOT yaml-cpp_FOUND) - find_package(yaml-cpp QUIET) -endif() -if(TARGET yaml-cpp::yaml-cpp) - set(_seastack_yaml_cpp_target yaml-cpp::yaml-cpp) -elseif(TARGET yaml-cpp) - set(_seastack_yaml_cpp_target yaml-cpp) -elseif(TARGET Chrono::yaml-cpp) - # Chrono's config often provides yaml-cpp as an imported target without a system package. +# +# Prefer Chrono's bundled yaml-cpp when available so that the yaml-cpp headers +# (which come from Chrono's chrono_thirdparty/yaml-cpp/include via Chrono::Chrono_core) +# match the linked library. Mixing a system libyaml-cpp.so with Chrono's headers can +# produce link errors against internal symbols (e.g. YAML::FpToString, YAML::Emitter::Write). +if(TARGET Chrono::yaml-cpp) set(_seastack_yaml_cpp_target Chrono::yaml-cpp) else() - message(FATAL_ERROR "yaml-cpp not found (required for Chrono adapter).") + find_package(yaml-cpp CONFIG QUIET) + if(NOT yaml-cpp_FOUND) + find_package(yaml-cpp QUIET) + endif() + if(TARGET yaml-cpp::yaml-cpp) + set(_seastack_yaml_cpp_target yaml-cpp::yaml-cpp) + elseif(TARGET yaml-cpp) + set(_seastack_yaml_cpp_target yaml-cpp) + else() + message(FATAL_ERROR "yaml-cpp not found (required for Chrono adapter).") + endif() endif() target_link_libraries(seastack_chrono_adapter diff --git a/apps/seastack/single_run.cpp b/apps/seastack/single_run.cpp index 3c3b36b..56d8918 100644 --- a/apps/seastack/single_run.cpp +++ b/apps/seastack/single_run.cpp @@ -123,7 +123,7 @@ namespace { class HCParser : public ::chrono::parsers::ChParserMbsYAML { public: HCParser() : ChParserMbsYAML() {} - void SetScriptDir(const std::string& dir) { m_script_directory = dir; } + void SetScriptDir(const std::string& dir) { m_file_handler.SetReferenceDirectory(dir); } }; static std::shared_ptr<::chrono::ChBody> FindBodyByName(::chrono::ChSystem& system, From 4b12d7db31f752974b6de8beba65fc00b59ffbf2 Mon Sep 17 00:00:00 2001 From: Salman Husain Date: Wed, 10 Jun 2026 16:41:20 -0600 Subject: [PATCH 2/5] fix(export): log joint reactions for all ChLink subclasses The generic joint reaction extraction in SimulationExporter::RecordStep was guarded by `dynamic_cast`, which silently fell through to an "all zeros" fallback for every other ChLink subclass -- including ChLinkUniversal (used by the 5SA articulated WEC demos), ChLinkRevolute, ChLinkRevoluteSpherical, and others. GetReaction1, GetReaction2, GetFrame1Abs, and GetFrame2Abs are virtual on ChLinkBase, so they resolve uniformly for any concrete joint type. Calling them on the base ChLink pointer fixes the export for all current and future joint types. Validation (5SA bimodal, 600s): Before: joint_12..45 max |F| = 0.000 N for entire run After: joint_12 max |F| = 192,471 N max |T| = 158,075 N*m joint_23 max |F| = 211,687 N max |T| = 220,279 N*m joint_34 max |F| = 201,941 N max |T| = 311,303 N*m joint_45 max |F| = 169,830 N max |T| = 401,657 N*m Internal consistency: reaction1_force == -reaction2_force to machine precision (Newton's third law). --- adapters/chrono/src/simulation_export.cpp | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/adapters/chrono/src/simulation_export.cpp b/adapters/chrono/src/simulation_export.cpp index 5e7a60c..c9e55c2 100644 --- a/adapters/chrono/src/simulation_export.cpp +++ b/adapters/chrono/src/simulation_export.cpp @@ -1082,13 +1082,18 @@ void SimulationExporter::RecordStep(::chrono::ChSystem* system) { } // ── Generic joint reactions (level-gated) ─────────────────────────────── + // GetReaction1/2 and GetFrame1Abs/2Abs are virtual on ChLinkBase, so they + // work for every concrete joint type (ChLinkLock, ChLinkUniversal, + // ChLinkRevoluteSpherical, ...). Previously this code dynamic_cast<>'d to + // ChLinkLock* only, silently writing zeros for every other joint type -- + // including the ChLinkUniversal joints used by the 5SA articulated WEC demos. for (auto& j : impl_->joints) { auto* L = j.link; - if (auto* lock = dynamic_cast<::chrono::ChLinkLock*>(L)) { + if (L) { try { - auto w1 = lock->GetReaction1(); - auto F1 = lock->GetFrame1Abs().TransformDirectionLocalToParent(w1.force); - auto T1 = lock->GetFrame1Abs().TransformDirectionLocalToParent(w1.torque); + auto w1 = L->GetReaction1(); + auto F1 = L->GetFrame1Abs().TransformDirectionLocalToParent(w1.force); + auto T1 = L->GetFrame1Abs().TransformDirectionLocalToParent(w1.torque); j.react_force_b1.insert(j.react_force_b1.end(), {F1.x(), F1.y(), F1.z()}); j.react_torque_b1.insert(j.react_torque_b1.end(), {T1.x(), T1.y(), T1.z()}); } catch (const std::exception& ex) { @@ -1098,9 +1103,9 @@ void SimulationExporter::RecordStep(::chrono::ChSystem* system) { } if (!is_compact) { try { - auto w2 = lock->GetReaction2(); - auto F2 = lock->GetFrame2Abs().TransformDirectionLocalToParent(w2.force); - auto T2 = lock->GetFrame2Abs().TransformDirectionLocalToParent(w2.torque); + auto w2 = L->GetReaction2(); + auto F2 = L->GetFrame2Abs().TransformDirectionLocalToParent(w2.force); + auto T2 = L->GetFrame2Abs().TransformDirectionLocalToParent(w2.torque); j.react_force_b2.insert(j.react_force_b2.end(), {F2.x(), F2.y(), F2.z()}); j.react_torque_b2.insert(j.react_torque_b2.end(), {T2.x(), T2.y(), T2.z()}); } catch (const std::exception& ex) { From f85e175c5dd48f536335879efdbe6b8abcb12d9d Mon Sep 17 00:00:00 2001 From: Salman Husain Date: Wed, 10 Jun 2026 16:41:20 -0600 Subject: [PATCH 3/5] fix(5sa/bimodal): add heave/pitch/yaw damping and frequency-domain excitation The bimodal case had linear_damping = [0, 100000, 0, 500000, 0, 0] (zero linear damping in surge, heave, pitch, and yaw) and no quadratic damping at all. The bimodal swell + wind-sea spectrum contains energy near the chain's rigid-body heave natural frequency, and with no restoring damping in heave the mode integrated unboundedly: at t=198.86s body3 crossed 5 m/s, and by t=280s body z-positions exceeded +10 m above SWL with the entire 144-m chain out of the water. The fix mirrors the damping already present in the spreading case (a near-identical multi-body configuration that runs to completion): linear_damping: [20000, 80000, 80000, 500000, 200000, 200000] quadratic_damping: [10000, 40000, 40000, 250000, 100000, 100000] It also adds an explicit `excitation: frequency_domain` block, again mirroring spreading. The bimodal swell and wind-sea have different headings, so excitation must be evaluated in the frequency domain -- this is auto-selected by the loader when wave components span multiple headings, but stating it explicitly matches the parity with spreading and makes intent clear. Validation (run_seastack 600s, no MoorDyn): Before: body3 max |v| = diverged past 5.0 m/s at t=198.86s body3 max z = +10.98 m at t=280s (chain in the air) run terminated at 280.7s After: body3 max |v| = 3.92 m/s at t=592.99s (bounded) body3 max z = within +/- 2 m throughout run completed full 600s, clean exit, 2m wallclock --- .../5sa/bimodal/5sa_bimodal.hydro.yaml | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/data/demos/run_seastack/5sa/bimodal/5sa_bimodal.hydro.yaml b/data/demos/run_seastack/5sa/bimodal/5sa_bimodal.hydro.yaml index 32569c1..1159c59 100644 --- a/data/demos/run_seastack/5sa/bimodal/5sa_bimodal.hydro.yaml +++ b/data/demos/run_seastack/5sa/bimodal/5sa_bimodal.hydro.yaml @@ -1,20 +1,31 @@ hydrodynamics: bodies: + # Surge/sway/heave/roll/pitch/yaw damping mirrored from the spreading + # case. The bimodal swell + wind sea combination contains spectral + # energy near the chain's heave natural frequency, and with zero + # linear and quadratic damping in heave/pitch/yaw the rigid-body + # heave mode runs away after ~200 s of accumulation (body z-position + # exceeded 10 m above SWL by t=280 s in the previous configuration). - name: body1 h5_file: ../assets/hydroData/5sa_directional.h5 - linear_damping: [0, 100000, 0, 500000, 0, 0] + linear_damping: [20000, 80000, 80000, 500000, 200000, 200000] + quadratic_damping: [10000, 40000, 40000, 250000, 100000, 100000] - name: body2 h5_file: ../assets/hydroData/5sa_directional.h5 - linear_damping: [0, 100000, 0, 500000, 0, 0] + linear_damping: [20000, 80000, 80000, 500000, 200000, 200000] + quadratic_damping: [10000, 40000, 40000, 250000, 100000, 100000] - name: body3 h5_file: ../assets/hydroData/5sa_directional.h5 - linear_damping: [0, 100000, 0, 500000, 0, 0] + linear_damping: [20000, 80000, 80000, 500000, 200000, 200000] + quadratic_damping: [10000, 40000, 40000, 250000, 100000, 100000] - name: body4 h5_file: ../assets/hydroData/5sa_directional.h5 - linear_damping: [0, 100000, 0, 500000, 0, 0] + linear_damping: [20000, 80000, 80000, 500000, 200000, 200000] + quadratic_damping: [10000, 40000, 40000, 250000, 100000, 100000] - name: body5 h5_file: ../assets/hydroData/5sa_directional.h5 - linear_damping: [0, 100000, 0, 500000, 0, 0] + linear_damping: [20000, 80000, 80000, 500000, 200000, 200000] + quadratic_damping: [10000, 40000, 40000, 250000, 100000, 100000] # Bimodal sea state: swell along +X, wind sea from beam (90 deg). waves: @@ -42,6 +53,11 @@ hydrodynamics: type: cos2s s: 8 + # Bimodal sea: swell and wind-sea have different headings, so excitation + # must be evaluated in the frequency domain (matches spreading case). + excitation: + method: frequency_domain + radiation: method: rirf_convolution smoothing: From 2172d0bc9133a71ef01ea8f9812f03afd3e020d9 Mon Sep 17 00:00:00 2001 From: Salman Husain Date: Wed, 10 Jun 2026 16:41:21 -0600 Subject: [PATCH 4/5] fix(gui/vsg): use opaque water surface to avoid runtime DepthSorted crash When the animated water surface is added to the VSG scene graph after ChVisualSystemVSG::Initialize() -- which happens because the wave model is set up at runtime, not pre-bind -- Chrono's wrapIfTransparent helper wraps the new node in a vsg::DepthSorted set to bin 10 if the material opacity is less than 1.0. The first record traversal that encounters this node then segfaults inside vsg::Bin::add() because the bin and its pipeline state were not present when Initialize() finalized the view's render path. Stack at the crash: #0 vsg::Bin::add(State*, double, Node const*) #1 vsg::RecordTraversal::apply(MatrixTransform const&) #2 vsg::Group::traverse(RecordTraversal&) ... #N chrono::vsg3d::ChVisualSystemVSG::Render() Water surface (kWaterOpacity=0.55) and wireframe overlay (SetOpacity(0.35)) both hit this. Setting both to 1.0 keeps the nodes out of the bin-10 DepthSorted path, so neither demos nor run_seastack crash on first render, including with the in-GUI wireframe toggle. Tradeoff: the water is opaque so submerged geometry is no longer visible through it. The proper fix is to compile any runtime-added transparent subtree against the live viewer (Chrono's ChShapeBuilderVSG owns an m_compileTraversal for this purpose but it is not exposed via SEA-Stack's public API surface). Tracking as a follow-up. --- apps/seastack/gui/vsg_config.h | 9 ++++++++- apps/seastack/gui/vsg_water_surface.cpp | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/apps/seastack/gui/vsg_config.h b/apps/seastack/gui/vsg_config.h index 99d237d..bfe5bad 100644 --- a/apps/seastack/gui/vsg_config.h +++ b/apps/seastack/gui/vsg_config.h @@ -25,7 +25,14 @@ inline constexpr int kColorVariationCycle = 8; inline constexpr float kWaterR = 0.01f; inline constexpr float kWaterG = 0.20f; inline constexpr float kWaterB = 0.35f; -inline constexpr float kWaterOpacity = 0.55f; +// NOTE: Material opacity must stay at 1.0 on Linux. +// With opacity < 1.0, Chrono's wrapIfTransparent() wraps the runtime-added +// water node in vsg::DepthSorted (bin 10), which crashes the VSG renderer +// at vsg::Bin::add during the first record traversal. The animated water +// surface is added to the scene after pVis->Initialize(), bypassing the +// BindAll() compile path that built-in Chrono demos rely on. +// TODO: enable proper translucency via a runtime compile traversal. +inline constexpr float kWaterOpacity = 1.0f; inline constexpr float kWaterSpecular = 0.6f; inline constexpr float kWaterRoughness = 0.05f; inline constexpr float kWaterMetallic = 0.0f; diff --git a/apps/seastack/gui/vsg_water_surface.cpp b/apps/seastack/gui/vsg_water_surface.cpp index ecc097a..6c86e50 100644 --- a/apps/seastack/gui/vsg_water_surface.cpp +++ b/apps/seastack/gui/vsg_water_surface.cpp @@ -501,7 +501,7 @@ void AnimatedWaterSurface::InitializeWireframe() { // Faint blue-gray material. auto wire_material = ::chrono_types::make_shared<::chrono::ChVisualMaterial>(); wire_material->SetDiffuseColor(::chrono::ChColor(0.1f, 0.2f, 0.3f)); - wire_material->SetOpacity(0.35f); + wire_material->SetOpacity(1.0f); wire_material->SetRoughness(0.9f); wire_material->SetMetallic(0.0f); From 5c2b908b332aa1b27868d52c9df7c6219f9cfbfa Mon Sep 17 00:00:00 2001 From: Salman Husain Date: Wed, 10 Jun 2026 16:41:21 -0600 Subject: [PATCH 5/5] chore(scripts): mark unix shell scripts as executable Each script under scripts/unix/ was checked in with mode 100644 (no executable bit), so `./scripts/unix/build.sh` fails on a fresh Linux clone. Setting +x on all of them makes the standard invocation pattern work as documented. --- scripts/unix/build.sh | 0 scripts/unix/ctest_suite.sh | 0 scripts/unix/run_benchmarks.sh | 0 scripts/unix/run_chrono_free_tests.sh | 0 scripts/unix/run_comparison_tests.sh | 0 scripts/unix/run_regression_tests.sh | 0 scripts/unix/run_seastack_demo_smoke.sh | 0 scripts/unix/run_unit_tests.sh | 0 scripts/unix/run_verification_tests.sh | 0 9 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 scripts/unix/build.sh mode change 100644 => 100755 scripts/unix/ctest_suite.sh mode change 100644 => 100755 scripts/unix/run_benchmarks.sh mode change 100644 => 100755 scripts/unix/run_chrono_free_tests.sh mode change 100644 => 100755 scripts/unix/run_comparison_tests.sh mode change 100644 => 100755 scripts/unix/run_regression_tests.sh mode change 100644 => 100755 scripts/unix/run_seastack_demo_smoke.sh mode change 100644 => 100755 scripts/unix/run_unit_tests.sh mode change 100644 => 100755 scripts/unix/run_verification_tests.sh diff --git a/scripts/unix/build.sh b/scripts/unix/build.sh old mode 100644 new mode 100755 diff --git a/scripts/unix/ctest_suite.sh b/scripts/unix/ctest_suite.sh old mode 100644 new mode 100755 diff --git a/scripts/unix/run_benchmarks.sh b/scripts/unix/run_benchmarks.sh old mode 100644 new mode 100755 diff --git a/scripts/unix/run_chrono_free_tests.sh b/scripts/unix/run_chrono_free_tests.sh old mode 100644 new mode 100755 diff --git a/scripts/unix/run_comparison_tests.sh b/scripts/unix/run_comparison_tests.sh old mode 100644 new mode 100755 diff --git a/scripts/unix/run_regression_tests.sh b/scripts/unix/run_regression_tests.sh old mode 100644 new mode 100755 diff --git a/scripts/unix/run_seastack_demo_smoke.sh b/scripts/unix/run_seastack_demo_smoke.sh old mode 100644 new mode 100755 diff --git a/scripts/unix/run_unit_tests.sh b/scripts/unix/run_unit_tests.sh old mode 100644 new mode 100755 diff --git a/scripts/unix/run_verification_tests.sh b/scripts/unix/run_verification_tests.sh old mode 100644 new mode 100755