Skip to content
Merged
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
22 changes: 11 additions & 11 deletions config/vgoswec_0_exc_ff_pid.yaml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# vgoswec_0_exc_ff_pid.yaml
# Source: Ogden et al., ASME JOMAE 145(3):030905 (VGOSWEC-0 / VGM-0).
# VGM-0 free-decay reference: ωn = 1.07 rad/s, Ts = 5.86 s.
# Active control: Excitation-force feedforward + PID trajectory tracking.
# Active control: excitation-force feedforward + inner velocity PID.
#
# τ = α · F_exc,pitch(t) + PID( θ_ref(t) - θ(t) )
# τ = -(ff_gain · F_exc,pitch(t) + PID_vel(alpha · F_exc,pitch(t) - θ̇(t)))

simulation:
duration: 60.0
Expand Down Expand Up @@ -40,12 +40,12 @@ wave:
controller:
type: exc_ff_pid
exc_ff_pid:
alpha: 1.0 # feedforward gain on F_exc TODO: tune with tank-test data
theta_ref: 0.0 # [rad] reference angle (upright equilibrium)
pid:
kp: 0.5 # [N·m/rad] TODO: tune with tank-test data
ki: 0.05 # [N·m/(rad·s)] TODO: tune with tank-test data
kd: 0.05 # [N·m·s/rad] TODO: tune with tank-test data
tau_d: 0.02 # [s] derivative filter time constant (≈ 3× dt)
u_min: -5.0 # [N·m] output clamp
u_max: 5.0 # [N·m] output clamp
alpha: 0.05 # vel_ref = alpha * F_exc [(rad/s)/(N·m)] (~1/(2·B_eff), B_eff = effective hinge radiation damping); TODO tune
ff_gain: 0.5 # feedforward torque = ff_gain * F_exc [-] (default half-excitation feedforward, i.e. the Korde F_exc/2 term); TODO tune
vel_pid:
kp: 1.0 # [N·m per (rad/s)] TODO tune
ki: 0.0 # TODO tune
kd: 0.0 # TODO tune
tau_d: 0.02 # [s] derivative filter time constant
u_min: -5.0 # [N·m] output clamp
u_max: 5.0 # [N·m] output clamp
24 changes: 11 additions & 13 deletions config/vgoswec_45_exc_ff_pid.yaml
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
# vgoswec_45_exc_ff_pid.yaml
# Source: Ogden et al., ASME JOMAE 145(3):030905 (VGOSWEC-45 / VGM-45).
# VGM-45 free-decay reference: ωn = 1.84 rad/s, Ts = 3.42 s.
# Active control: Excitation-force feedforward + PID trajectory tracking.
# Active control: excitation-force feedforward + inner velocity PID.
#
# τ = α · F_exc,pitch(t) + PID( θ_ref(t) - θ(t) )
# τ = -(ff_gain · F_exc,pitch(t) + PID_vel(alpha · F_exc,pitch(t) - θ̇(t)))
#
# The feedforward term uses the actual wave excitation torque (pitch DOF)
# extracted from HydroForces::Evaluate per-component at each timestep.
# This implements active WEC phase-anticipation.
#
# Default θ_ref = 0 (regulate flap to upright; feedforward does anticipation).

simulation:
duration: 60.0
Expand Down Expand Up @@ -54,12 +52,12 @@ wave:
controller:
type: exc_ff_pid
exc_ff_pid:
alpha: 1.0 # feedforward gain on F_exc TODO: tune with tank-test data
theta_ref: 0.0 # [rad] reference angle (upright equilibrium)
pid:
kp: 0.5 # [N·m/rad] TODO: tune with tank-test data
ki: 0.05 # [N·m/(rad·s)] TODO: tune with tank-test data
kd: 0.05 # [N·m·s/rad] TODO: tune with tank-test data
tau_d: 0.02 # [s] derivative filter time constant (≈ 3× dt)
u_min: -5.0 # [N·m] output clamp
u_max: 5.0 # [N·m] output clamp
alpha: 0.05 # vel_ref = alpha * F_exc [(rad/s)/(N·m)] (~1/(2·B_eff), B_eff = effective hinge radiation damping); TODO tune
ff_gain: 0.5 # feedforward torque = ff_gain * F_exc [-] (default half-excitation feedforward, i.e. the Korde F_exc/2 term); TODO tune
vel_pid:
kp: 1.0 # [N·m per (rad/s)] TODO tune
ki: 0.0 # TODO tune
kd: 0.0 # TODO tune
tau_d: 0.02 # [s] derivative filter time constant
u_min: -5.0 # [N·m] output clamp
u_max: 5.0 # [N·m] output clamp
19 changes: 12 additions & 7 deletions docs/CONTROLLERS.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,23 +79,28 @@ CC control requires bidirectional power flow. A physical PTO must support reacti

---

## (D) ExcitationFeedforwardPID
## (D) ExcitationVelocityController (`exc_ff_pid`)

### Formula
```
τ = α · F_exc,pitch(t) + PID(θ_ref − θ)
vel_ref = α · F_exc,pitch(t)
τ_cmd = ff_gain · F_exc,pitch(t) + PID_vel(vel_ref − θ̇)
τ_pto = −τ_cmd
```

### Sub-components
- **Feedforward** (`α · F_exc`): uses real-time wave excitation torque from `ExcitationForceProvider`. Requires `HydroSystem::SetPerComponentCaptureEnabled(true)`.
- **PID** (`PID(θ_ref − θ)`): full PID with filtered derivative (time constant τ_d) and anti-windup back-calculation. Keeps flap near `θ_ref` (default: 0 = upright).
- **Velocity reference** (`α · F_exc`): sets a phase-aligned target velocity from real-time wave excitation torque.
- **Feedforward torque** (`ff_gain · F_exc`): uses real-time wave excitation torque from `ExcitationForceProvider`. Requires `HydroSystem::SetPerComponentCaptureEnabled(true)`.
- **Velocity PID** (`PID_vel(vel_ref − θ̇)`): full PID with filtered derivative (time constant τ_d) and anti-windup back-calculation. Regulates flap velocity, not flap position.

### PID parameters
| Name | Default | Notes |
|------|---------|-------|
| `kp` | 0.5 | N·m/rad |
| `ki` | 0.05 | N·m/(rad·s) |
| `kd` | 0.05 | N·m·s/rad |
| `alpha` | 0.05 | (rad/s)/(N·m) |
| `ff_gain` | 0.5 | direct feedforward torque scale |
| `kp` | 1.0 | N·m per (rad/s) |
| `ki` | 0.0 | integral gain on velocity error |
| `kd` | 0.0 | derivative gain on velocity error |
| `tau_d` | 0.02 s | ≈ 4× timestep |
| `u_min/u_max` | ±5 N·m | Saturation clamp |

Expand Down
4 changes: 2 additions & 2 deletions docs/MPC_TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Future extension: Model Predictive Control (MPC) for the VGOSWEC-45.

## Why MPC

The current ExcitationFeedforwardPID assumes the excitation force is known
The current ExcitationVelocityController assumes the excitation force is known
perfectly at each instant (from HydroForces::Evaluate) but uses it reactively.
True optimal WEC control requires **preview** of future excitation — MPC
provides this while respecting torque, angle, and power constraints.
Expand Down Expand Up @@ -50,7 +50,7 @@ Add `--controller mpc` to CLI and `vgoswec_45_mpc.yaml` config.
- [ ] Implement linear state-space discretization
- [ ] Implement causal excitation predictor (Kalman filter on wave elevation)
- [ ] Implement QP formulation and OSQP wrapper
- [ ] Validate against ExcitationFeedforwardPID in simulation
- [ ] Validate against ExcitationVelocityController in simulation
- [ ] Tank-test comparison

## References
Expand Down
28 changes: 14 additions & 14 deletions src/active_pto.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,25 +33,25 @@ double ComplexConjugateControl::ComputeForce(double disp, double vel, double /*t
return std::clamp(-K_r_ * disp - B_r_ * vel, -clip_, clip_);
}

// ─── (D) ExcitationFeedforwardPID ────────────────────────────────────────────
// ─── (D) ExcitationVelocityController ────────────────────────────────────────

ExcitationFeedforwardPID::ExcitationFeedforwardPID(
ExcitationVelocityController::ExcitationVelocityController(
std::shared_ptr<ExcitationForceProvider> src,
double alpha,
std::unique_ptr<PIDController> pid,
double theta_ref)
double ff_gain,
std::unique_ptr<PIDController> pid)
: f_exc_source_(std::move(src)),
alpha_(alpha),
pid_(std::move(pid)),
theta_ref_(theta_ref) {
pid_->SetSetpoint(theta_ref_);
}

double ExcitationFeedforwardPID::ComputeForce(double disp, double vel, double t) {
(void)vel;
const double tau_ff = alpha_ * f_exc_source_->GetLatestExcitationTorque();
const double tau_pid = pid_->Compute(disp, t);
return tau_ff + tau_pid;
ff_gain_(ff_gain),
pid_(std::move(pid)) {}

double ExcitationVelocityController::ComputeForce(double disp, double vel, double t) {
(void)disp; // Inner velocity loop only: no outer position/displacement regulation.
const double f_exc = f_exc_source_->GetLatestExcitationTorque();
pid_->SetSetpoint(alpha_ * f_exc);
const double tau_ff = ff_gain_ * f_exc;
const double tau_pid = pid_->Compute(vel, t);
return -(tau_ff + tau_pid);
}

} // namespace vgoswec
28 changes: 17 additions & 11 deletions src/active_pto.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
// │ returned value = PTO torque about hinge Y-axis [N·m] │
// │ Positive torque OPPOSES positive θ (restoring convention) │
// │ Absorbed power: P_abs = −τ_pto · ω (positive = extracted from waves)│
// │ Active vel control demand: ff_gain·F_exc + PID(alpha·F_exc − θ̇) │
// │ Returned active-control torque: τ_pto = −(demand) │
// └──────────────────────────────────────────────────────────────────────────┘
// =============================================================================
#pragma once
Expand Down Expand Up @@ -69,30 +71,34 @@ class ComplexConjugateControl : public seastack::pto::IPTOModel {
};

// =============================================================================
// (D) ExcitationFeedforwardPID — active WEC control
// τ = α · F_exc,pitch(t) + PID( θ_ref − θ )
// (D) ExcitationVelocityController — active WEC control
// vel_ref = α · F_exc,pitch(t)
// τ_cmd = ff_gain · F_exc,pitch(t) + PID( vel_ref − θ̇ )
// τ_pto = −τ_cmd
//
// The feedforward term uses the actual wave excitation torque broadcast by
// ExcitationForceProvider (updated from HydroForces::Evaluate per_component).
// The PID term regulates the flap toward θ_ref (default = 0, upright).
// The PID closes an inner velocity loop, driving flap velocity to track a
// phase-aligned reference proportional to excitation torque.
//
// This implements phase anticipation: α·F_exc advances the flap to absorb
// energy while the PID damps deviations from the reference trajectory.
// Under this file's sign convention, positive returned torque opposes
// positive θ, so the applied PTO torque is the negative of the active control
// effort above.
// =============================================================================
class ExcitationFeedforwardPID : public seastack::pto::IPTOModel {
class ExcitationVelocityController : public seastack::pto::IPTOModel {
public:
ExcitationFeedforwardPID(std::shared_ptr<ExcitationForceProvider> src,
double alpha,
std::unique_ptr<PIDController> pid,
double theta_ref = 0.0);
ExcitationVelocityController(std::shared_ptr<ExcitationForceProvider> src,
double alpha,
double ff_gain,
std::unique_ptr<PIDController> pid);

double ComputeForce(double disp, double vel, double t) override;

private:
std::shared_ptr<ExcitationForceProvider> f_exc_source_;
double alpha_;
double ff_gain_;
std::unique_ptr<PIDController> pid_;
double theta_ref_;
};

} // namespace vgoswec
Expand Down
12 changes: 6 additions & 6 deletions src/config_loader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,9 @@ BodyConfig ParseBody(const YAML::Node& n) {

PIDConfig ParsePID(const YAML::Node& n) {
PIDConfig p;
p.kp = ReadOpt<double>(n, "kp", 0.5);
p.ki = ReadOpt<double>(n, "ki", 0.05);
p.kd = ReadOpt<double>(n, "kd", 0.05);
p.kp = ReadOpt<double>(n, "kp", 1.0);
p.ki = ReadOpt<double>(n, "ki", 0.0);
p.kd = ReadOpt<double>(n, "kd", 0.0);
p.tau_d = ReadOpt<double>(n, "tau_d", 0.02);
p.u_min = ReadOpt<double>(n, "u_min", -5.0);
p.u_max = ReadOpt<double>(n, "u_max", 5.0);
Expand Down Expand Up @@ -77,9 +77,9 @@ ControllerConfig ParseController(const YAML::Node& n) {
}
if (n["exc_ff_pid"]) {
const auto& p = n["exc_ff_pid"];
ctrl.exc_ff_pid.alpha = ReadOpt<double>(p, "alpha", 1.0);
ctrl.exc_ff_pid.theta_ref = ReadOpt<double>(p, "theta_ref", 0.0);
if (p["pid"]) ctrl.exc_ff_pid.pid = ParsePID(p["pid"]);
ctrl.exc_ff_pid.alpha = ReadOpt<double>(p, "alpha", 0.05);
ctrl.exc_ff_pid.ff_gain = ReadOpt<double>(p, "ff_gain", 0.5);
if (p["vel_pid"]) ctrl.exc_ff_pid.vel_pid = ParsePID(p["vel_pid"]);
}
return ctrl;
}
Expand Down
12 changes: 6 additions & 6 deletions src/config_loader.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,18 +42,18 @@ struct CCConfig {
};

struct PIDConfig {
double kp{0.5};
double ki{0.05};
double kd{0.05};
double kp{1.0};
double ki{0.0};
double kd{0.0};
double tau_d{0.02}; ///< [s] derivative filter time constant
double u_min{-5.0}; ///< [N·m]
double u_max{5.0}; ///< [N·m]
};

struct ExcFFPIDConfig {
double alpha{1.0}; ///< feedforward gain on F_exc
double theta_ref{0.0}; ///< [rad] reference angle
PIDConfig pid;
double alpha{0.05}; ///< [(rad/s)/(N·m)] velocity reference gain, vel_ref = alpha * F_exc
double ff_gain{0.5}; ///< [-] direct feedforward torque gain, tau_ff = ff_gain * F_exc
PIDConfig vel_pid; ///< Velocity-loop PID gains/output clamp
};

struct ControllerConfig {
Expand Down
20 changes: 11 additions & 9 deletions src/demo_vgoswec.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -191,18 +191,20 @@ static std::shared_ptr<seastack::pto::IPTOModel> BuildController(

if (type == "exc_ff_pid") {
vgoswec::PIDParams p{};
p.kp = cfg.controller.exc_ff_pid.pid.kp;
p.ki = cfg.controller.exc_ff_pid.pid.ki;
p.kd = cfg.controller.exc_ff_pid.pid.kd;
p.tau_d = cfg.controller.exc_ff_pid.pid.tau_d;
p.u_min = cfg.controller.exc_ff_pid.pid.u_min;
p.u_max = cfg.controller.exc_ff_pid.pid.u_max;
p.kp = cfg.controller.exc_ff_pid.vel_pid.kp;
p.ki = cfg.controller.exc_ff_pid.vel_pid.ki;
p.kd = cfg.controller.exc_ff_pid.vel_pid.kd;
p.tau_d = cfg.controller.exc_ff_pid.vel_pid.tau_d;
p.u_min = cfg.controller.exc_ff_pid.vel_pid.u_min;
p.u_max = cfg.controller.exc_ff_pid.vel_pid.u_max;
p.dt_expected = cfg.timestep;

auto pid = std::make_unique<vgoswec::PIDController>(p);
return std::make_shared<vgoswec::ExcitationFeedforwardPID>(
exc_provider, cfg.controller.exc_ff_pid.alpha, std::move(pid),
cfg.controller.exc_ff_pid.theta_ref);
return std::make_shared<vgoswec::ExcitationVelocityController>(
exc_provider,
cfg.controller.exc_ff_pid.alpha,
cfg.controller.exc_ff_pid.ff_gain,
std::move(pid));
}

throw std::runtime_error("Unknown controller type: " + type);
Expand Down
2 changes: 1 addition & 1 deletion src/excitation_force_provider.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
// std::vector<seastack::hydro::ComponentForceRecord> per_comp;
// hydro_forces.Evaluate(state, time, &per_comp);
// exc_provider.Update(per_comp);
// // ... ExcitationFeedforwardPID reads GetLatestExcitationTorque()
// // ... ExcitationVelocityController reads GetLatestExcitationTorque()
//
// DOF convention (BEMIO / WEC-Sim):
// 0 = surge (Fx), 1 = sway (Fy), 2 = heave (Fz),
Expand Down
4 changes: 2 additions & 2 deletions src/pid_controller.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ struct PIDParams {
/// - Anti-windup via back-calculation (integrator only advances when unsaturated)
/// - Output clamp [u_min, u_max]
///
/// Units: measurement [rad], output [N·m] (rotational PTO use case).
/// Units: measurement/setpoint are application-defined, output is [N·m].
class PIDController : public seastack::control::IController {
public:
explicit PIDController(const PIDParams& params);
Expand All @@ -42,7 +42,7 @@ class PIDController : public seastack::control::IController {
double GetSetpoint() const { return setpoint_; }

/// Compute control output.
/// @param measurement Current flap angle θ [rad]
/// @param measurement Current measured process variable
/// @param time Current simulation time [s] (used to compute dt)
/// @return PTO torque command [N·m]
double Compute(double measurement, double time) override;
Expand Down
2 changes: 1 addition & 1 deletion src/rsda_pto_functor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ double RsdaPtoFunctor::evaluate(double time, double /*rest_angle*/, double angle
const ::chrono::ChLinkRSDA& /*link*/) {
// angle -> flap deviation theta [rad] (displacement); vel -> theta_dot [rad/s].
// rest_angle is unused: the controllers reference theta from the initialized-at-rest
// configuration (rest_angle = 0), matching theta_ref = 0 in the exc_ff_pid controller.
// configuration (rest_angle = 0); active controllers use angle/velocity directly.
return model_->ComputeForce(angle, vel, time);
}

Expand Down
Loading