diff --git a/.compozy/tasks/phase3/adrs/adr-001.md b/.compozy/tasks/phase3/adrs/adr-001.md new file mode 100644 index 0000000..87acbe2 --- /dev/null +++ b/.compozy/tasks/phase3/adrs/adr-001.md @@ -0,0 +1,91 @@ +# ADR-001: Visualization Session API MVP for Phase 3 + +## Status + +Accepted + +## Date + +2026-06-27 + +## Context + +Phase 3 must expose Ymir's `World` simulation state to external clients. The roadmap +identifies WebSocket + Protobuf, scenario loading, basic control commands, and real-time +state transmission as the intended control API. Codebase research shows that Phase 2 +already provides the required simulation foundation through `World::step(dt)`, +`World::snapshot()`, `Environment`, `NavalDomain`, and `CouplingRegistry`, while +`apps/server` remains an empty application target. + +The selected primary user is a visualization client. The MVP must provide body +kinematics, vessel operational state, and environment state. The client needs basic +session control: load scenario, start, pause, resume, stop, and observe state. The +preferred stream behavior is event-first updates rather than periodic full snapshots, +and the main success criterion is integration completeness. + +## Decision + +Phase 3 will use a Visualization Session API MVP. The product will let a visualization +client load a scenario, control the simulation session lifecycle, receive an initial +full session snapshot, and then receive event-first updates covering body kinematics, +vessel operational state, and environment state. + +The MVP will avoid live scenario editing and detailed operator controls. It will +prioritize one complete end-to-end integration path over breadth. + +## Alternatives Considered + +### Alternative 1: Control-Rich Operator API + +- **Description**: Include session control plus live changes to environment, waypoints, + vessel modes, rudder commands, and thruster commands. +- **Pros**: More useful for training operators and interactive maneuver experiments. +- **Cons**: Expands scope into operational control semantics before the visualization + contract is stable. +- **Why rejected**: It risks turning Phase 3 into a broad command product instead of + the smallest complete external integration layer. + +### Alternative 2: Stream-Only Viewer API + +- **Description**: Let external code create and run simulations while the server only + streams state/events to visualization clients. +- **Pros**: Fastest path to rendering live state. +- **Cons**: Does not let the client load or control a full World session. +- **Why rejected**: It fails the selected success criterion of integration completeness. + +## Consequences + +### Positive + +- Delivers a complete external client workflow with limited product scope. +- Gives visualization clients enough state to render bodies, vessel status, and environment. +- Establishes the message contract future operator controls can extend. +- Keeps Phase 3 aligned with the existing Phase 2 `World` snapshot model. + +### Negative + +- Does not yet support live operator editing of environment, waypoints, or actuator commands. +- Visualization clients may still need client-side interpolation when event cadence is lower + than render cadence. +- Event-first behavior requires clear baseline snapshot semantics so clients can recover + from missed or out-of-order updates. + +### Risks + +- **Risk**: Stakeholders may expect direct vessel control in Phase 3. + **Mitigation**: Document live scenario and vessel command editing as post-MVP scope. +- **Risk**: Event-first updates may be insufficient for early visualizers expecting + fixed-rate snapshots. + **Mitigation**: Require an initial full snapshot and allow clients to request current + state on demand. + +## Implementation Notes + +This ADR records product scope only. Implementation details belong in the Phase 3 +TechSpec. + +## References + +- `.prds/roadmap.md` — Phase 3 scope +- `.compozy/tasks/phase-02-world/_prd.md` — World snapshot and Phase 3 readiness +- `docs/architecture/data-flow.md` — real-time server data flow context diff --git a/cmake/Dependencies.cmake b/cmake/Dependencies.cmake index bce4484..5eb88ac 100644 --- a/cmake/Dependencies.cmake +++ b/cmake/Dependencies.cmake @@ -22,12 +22,6 @@ FetchContent_Declare( FetchContent_MakeAvailable(Catch2 nlohmann_json) -# --------------------------------------------------------------------------- -# SUNDIALS CVODE — ODE integrator (system package, required from M1) -# --------------------------------------------------------------------------- -find_package(SUNDIALS 6.0 REQUIRED) -message(STATUS "SUNDIALS found: ${SUNDIALS_VERSION}") - # --------------------------------------------------------------------------- # CGAL — mesh and geometry library (system package, required from M2) # --------------------------------------------------------------------------- diff --git a/default.profraw b/default.profraw new file mode 100644 index 0000000..ac1c10e Binary files /dev/null and b/default.profraw differ diff --git a/libs/physics/CMakeLists.txt b/libs/physics/CMakeLists.txt index adb4e2c..7e0ba30 100644 --- a/libs/physics/CMakeLists.txt +++ b/libs/physics/CMakeLists.txt @@ -3,7 +3,6 @@ add_library(ymir_physics STATIC src/naval_physics.cpp src/RigidBody6DOF.cpp src/NavalForceModel.cpp - src/integrator/CvodeIntegrator.cpp src/integrator/RK45Integrator.cpp src/forces/CurrentForces.cpp src/forces/DampingForces.cpp @@ -28,7 +27,6 @@ target_include_directories(ymir_physics target_link_libraries(ymir_physics PUBLIC ymir_common - SUNDIALS::cvode ) target_set_warnings(ymir_physics) diff --git a/libs/physics/include/ymir/physics/RigidBody6DOF.h b/libs/physics/include/ymir/physics/RigidBody6DOF.h index ae663a6..a3db8c9 100644 --- a/libs/physics/include/ymir/physics/RigidBody6DOF.h +++ b/libs/physics/include/ymir/physics/RigidBody6DOF.h @@ -3,7 +3,9 @@ #include #include #include -#include +#include +#include // backward compat alias +#include #include #include @@ -11,17 +13,11 @@ namespace ymir { -class CvodeIntegrator; -class RK45Integrator; - /** - * Concrete 6-DOF rigid body with CVODE/BDF integration. + * Concrete 6-DOF rigid body with RK45 adaptive integration. * * Owns its mass matrices, kinematic state, force models, and integrator. - * CVODE is initialized lazily on the first step() call. - * - * Non-copyable and non-movable: CvodeIntegrator::user_data holds an - * address-stable pointer to its internal RhsContext (see D-10). + * Integrator is initialized lazily on the first step() call. */ class RigidBody6DOF final : public AbstractBody { @@ -31,7 +27,7 @@ public: const Matrix6x6& addedMass, const Vector6& initialQ, const Vector6& initialQdot, - const CvodeConfig& config = {}); + const RK45Config& config = {}); ~RigidBody6DOF(); @@ -47,7 +43,7 @@ public: BodyState state() const noexcept override; int id() const noexcept override { return id_; } - /** Reset CVODE time to 0, reinitialise from current body state. */ + /** Reset integrator time to 0, reinitialise from current body state. */ void reset(); // Read-only access (used by CvodeIntegrator and tests) @@ -65,7 +61,6 @@ public: Vector6 computeAcceleration(const Forces& totalForce) const noexcept; private: - friend class CvodeIntegrator; friend class RK45Integrator; void setState(const Vector6& q, const Vector6& qdot) noexcept; @@ -85,7 +80,7 @@ private: std::vector> ownedModels_; std::vector modelPtrs_; - std::unique_ptr integrator_; + std::unique_ptr integrator_; double t_ = 0.0; bool initialized_ = false; }; diff --git a/libs/physics/include/ymir/physics/integrator/CvodeConfig.h b/libs/physics/include/ymir/physics/integrator/CvodeConfig.h index 2bcb4a0..8cab4fe 100644 --- a/libs/physics/include/ymir/physics/integrator/CvodeConfig.h +++ b/libs/physics/include/ymir/physics/integrator/CvodeConfig.h @@ -1,14 +1,10 @@ #pragma once +// CvodeConfig is now an alias for RK45Config. +// Kept for backward compatibility — tests and existing code can still use CvodeConfig. +#include + namespace ymir { - -struct CvodeConfig -{ - double reltol = 1e-8; - double abstol = 1e-10; - long maxSteps = 10000; - double maxStep = 0.0; // 0 = no limit (CVODE decides) -}; - +using CvodeConfig = RK45Config; } // namespace ymir diff --git a/libs/physics/include/ymir/physics/integrator/CvodeIntegrator.h b/libs/physics/include/ymir/physics/integrator/CvodeIntegrator.h deleted file mode 100644 index eaccc00..0000000 --- a/libs/physics/include/ymir/physics/integrator/CvodeIntegrator.h +++ /dev/null @@ -1,70 +0,0 @@ -#pragma once - -#include -#include - -#include -#include -#include -#include - -#include - -namespace ymir -{ - -/** - * CVODE/BDF integrator for 6-DOF rigid body dynamics. - * - * State vector (12 doubles): - * y[0..5] = q (position in inertial frame) - * y[6..11] = qdot (velocity in body frame) - * - * Non-copyable and non-movable: CVODE's user_data pointer references - * internal context by address (see D-10 in STATE.md). - */ -class CvodeIntegrator : public IIntegrator -{ -public: - explicit CvodeIntegrator(const CvodeConfig& config = {}); - ~CvodeIntegrator(); - - CvodeIntegrator(const CvodeIntegrator&) = delete; - CvodeIntegrator& operator=(const CvodeIntegrator&) = delete; - CvodeIntegrator(CvodeIntegrator&&) = delete; - CvodeIntegrator& operator=(CvodeIntegrator&&) = delete; - - /** Allocate CVODE memory and set initial conditions from body. */ - void initialize(RigidBody6DOF& body, std::vector& models) override; - - /** Advance body state by dt seconds. Throws on CVODE error. */ - void step(RigidBody6DOF& body, double dt) override; - - /** Reinitialize CVODE with the body's current state (no reallocation). */ - void reset(RigidBody6DOF& body) override; - - double time() const noexcept override { return t_; } - - // Public so the static rhs() callback can access it — not user API. - struct RhsContext - { - RigidBody6DOF* body = nullptr; - std::vector* models = nullptr; - }; - -private: - CvodeConfig config_; - double t_ = 0.0; - - SUNContext sunCtx_ = nullptr; - void* mem_ = nullptr; - N_Vector y_ = nullptr; - SUNMatrix A_ = nullptr; - SUNLinearSolver LS_ = nullptr; - - RhsContext ctx_{}; - - static int rhsCallback(sunrealtype t, N_Vector y, N_Vector ydot, void* user_data); -}; - -} // namespace ymir diff --git a/libs/physics/src/RigidBody6DOF.cpp b/libs/physics/src/RigidBody6DOF.cpp index 543c21e..99c7f5a 100644 --- a/libs/physics/src/RigidBody6DOF.cpp +++ b/libs/physics/src/RigidBody6DOF.cpp @@ -1,5 +1,5 @@ #include -#include +#include #include namespace ymir @@ -10,7 +10,7 @@ RigidBody6DOF::RigidBody6DOF(int id, const Matrix6x6& addedMass, const Vector6& initialQ, const Vector6& initialQdot, - const CvodeConfig& config) + const RK45Config& config) : id_(id) , massMatrix_(massMatrix) , addedMass_(addedMass) @@ -19,7 +19,7 @@ RigidBody6DOF::RigidBody6DOF(int id, , q_(initialQ) , qdot_(initialQdot) , qddot_{} - , integrator_(std::make_unique(config)) + , integrator_(std::make_unique(config)) { } diff --git a/libs/physics/src/integrator/CvodeIntegrator.cpp b/libs/physics/src/integrator/CvodeIntegrator.cpp deleted file mode 100644 index 47a9267..0000000 --- a/libs/physics/src/integrator/CvodeIntegrator.cpp +++ /dev/null @@ -1,167 +0,0 @@ -#include - -#include -#include -#include - -#include -#include -#include -#include -#include - -#include -#include -#include - -namespace ymir -{ - -// --------------------------------------------------------------------------- -// RHS — CVODE callback (static member → has friend access to RigidBody6DOF) -// --------------------------------------------------------------------------- - -int CvodeIntegrator::rhsCallback(sunrealtype t_rhs, N_Vector y, N_Vector ydot, void* user_data) -{ - auto* ctx = static_cast(user_data); - RigidBody6DOF& body = *ctx->body; - - // 1. Extract q and qdot from CVODE state vector - Vector6 q{}, qdot{}; - for (int i = 0; i < 6; ++i) - { - q[i] = NV_Ith_S(y, i); - qdot[i] = NV_Ith_S(y, i + 6); - } - - // 2. Update body state so ForceModels read current kinematics - body.setState(q, qdot); - - // 3. Sum all forces (pass t_rhs as the evaluation time, not body.time()) - BodyState state{q, qdot, static_cast(t_rhs), 0.0}; - Forces Ft = Forces::zero(); - for (ForceModel* model : *ctx->models) - Ft += model->compute(state); - - // 4. Compute acceleration: qddot = invM * Ft - Vector6 qddot = body.computeAcceleration(Ft); - body.setAcceleration(qddot); - - // 5. Kinematic transformation: ydot[0..5] (body → inertial) - const double psi = q[5]; // yaw - NV_Ith_S(ydot, 0) = std::cos(psi) * qdot[0] - std::sin(psi) * qdot[1]; // ẋ - NV_Ith_S(ydot, 1) = std::sin(psi) * qdot[0] + std::cos(psi) * qdot[1]; // ẏ - NV_Ith_S(ydot, 2) = qdot[2]; // ż - // ASSUMPTION: small-angle — Euler rates ≈ body angular rates - NV_Ith_S(ydot, 3) = qdot[3]; // φ̇ ≈ p - NV_Ith_S(ydot, 4) = qdot[4]; // θ̇ ≈ q - NV_Ith_S(ydot, 5) = qdot[5]; // ψ̇ = r - - // 6. ydot[6..11] = qddot - for (int i = 0; i < 6; ++i) - NV_Ith_S(ydot, 6 + i) = qddot[i]; - - return 0; -} - -// --------------------------------------------------------------------------- -// CvodeIntegrator -// --------------------------------------------------------------------------- - -CvodeIntegrator::CvodeIntegrator(const CvodeConfig& config) - : config_(config) -{ - if (SUNContext_Create(SUN_COMM_NULL, &sunCtx_) != 0) - throw std::runtime_error("CvodeIntegrator: SUNContext_Create failed"); - - mem_ = CVodeCreate(CV_BDF, sunCtx_); - if (!mem_) - throw std::runtime_error("CvodeIntegrator: CVodeCreate failed"); -} - -CvodeIntegrator::~CvodeIntegrator() -{ - if (LS_) SUNLinSolFree(LS_); - if (A_) SUNMatDestroy(A_); - if (y_) N_VDestroy(y_); - if (mem_) CVodeFree(&mem_); - if (sunCtx_) SUNContext_Free(&sunCtx_); -} - -void CvodeIntegrator::initialize(RigidBody6DOF& body, std::vector& models) -{ - ctx_ = RhsContext{&body, &models}; - t_ = 0.0; - - y_ = N_VNew_Serial(12, sunCtx_); - if (!y_) - throw std::runtime_error("CvodeIntegrator: N_VNew_Serial failed"); - - const Vector6& q = body.q(); - const Vector6& qdot = body.qdot(); - for (int i = 0; i < 6; ++i) - { - NV_Ith_S(y_, i) = q[i]; - NV_Ith_S(y_, i + 6) = qdot[i]; - } - - if (CVodeInit(mem_, CvodeIntegrator::rhsCallback, t_, y_) != CV_SUCCESS) - throw std::runtime_error("CvodeIntegrator: CVodeInit failed"); - - if (CVodeSStolerances(mem_, config_.reltol, config_.abstol) != CV_SUCCESS) - throw std::runtime_error("CvodeIntegrator: CVodeSStolerances failed"); - - if (CVodeSetMaxNumSteps(mem_, config_.maxSteps) != CV_SUCCESS) - throw std::runtime_error("CvodeIntegrator: CVodeSetMaxNumSteps failed"); - - if (config_.maxStep > 0.0) - CVodeSetMaxStep(mem_, config_.maxStep); - - CVodeSetUserData(mem_, &ctx_); - - A_ = SUNDenseMatrix(12, 12, sunCtx_); - if (!A_) - throw std::runtime_error("CvodeIntegrator: SUNDenseMatrix failed"); - - LS_ = SUNLinSol_Dense(y_, A_, sunCtx_); - if (!LS_) - throw std::runtime_error("CvodeIntegrator: SUNLinSol_Dense failed"); - - if (CVodeSetLinearSolver(mem_, LS_, A_) != CVLS_SUCCESS) - throw std::runtime_error("CvodeIntegrator: CVodeSetLinearSolver failed"); -} - -void CvodeIntegrator::step(RigidBody6DOF& body, double dt) -{ - double t_target = t_ + dt; - - int flag = CVode(mem_, t_target, y_, &t_, CV_NORMAL); - if (flag < 0) - { - throw std::runtime_error( - "CvodeIntegrator: CVode failed with flag " + std::to_string(flag)); - } - - Vector6 q{}, qdot{}; - for (int i = 0; i < 6; ++i) - { - q[i] = NV_Ith_S(y_, i); - qdot[i] = NV_Ith_S(y_, i + 6); - } - body.setState(q, qdot); -} - -void CvodeIntegrator::reset(RigidBody6DOF& body) -{ - const Vector6& q = body.q(); - const Vector6& qdot = body.qdot(); - for (int i = 0; i < 6; ++i) - { - NV_Ith_S(y_, i) = q[i]; - NV_Ith_S(y_, i + 6) = qdot[i]; - } - t_ = 0.0; - CVodeReInit(mem_, t_, y_); -} - -} // namespace ymir