// ddpg.mbt �?Deep Deterministic Policy Gradient (Lillicrap et al. 2015).
//
// DDPG learns a deterministic policy μ(s; θ^μ) that maps states to a
// continuous action, plus a critic Q(s, a; θ^Q) for off-policy TD
// learning. Like SAC/DSAC, it uses twin critics to mitigate Q-value
// overestimation and Polyak-averaged target networks for stability.
//
// Differences vs SAC/DSAC:
//   - The actor is deterministic (no reparameterisation trick needed).
//     Exploration comes from additive Gaussian noise during training.
//   - Critic takes [state, action] as input and outputs a single scalar.
//   - No entropy bonus in the actor loss.
//
// Differences vs TD3 (next version):
//   - DDPG uses a single critic (we use twin for stability).
//   - DDPG does NOT smooth the target policy or delay actor updates.
//
// References:
//   - Lillicrap et al. 2015, "Continuous Control with Deep Reinforcement
//     Learning" (ICLR).
//   - Plappert et al. 2018 (twin-critic variant).

// =========================================================================
// DeterministicPolicy: state -> continuous action
// =========================================================================

///|
/// Deterministic policy network. One hidden layer with ReLU activation;
/// output is squashed via tanh to `[action_low, action_high]` per dim.
pub struct DeterministicPolicy {
  state_dim : Int
  action_dim : Int
  hidden : Int
  // W1[hidden, state_dim], b1[hidden]
  w1 : Array[Array[Float]]
  b1 : Array[Float]
  // W2[action_dim, hidden], b2[action_dim]
  w2 : Array[Array[Float]]
  b2 : Array[Float]
  action_low : Float
  action_high : Float
}

///|
/// Construct a DeterministicPolicy. Hidden size defaults to 64.
pub fn DeterministicPolicy::new(
  state_dim : Int,
  action_dim : Int,
  hidden : Int,
  action_low : Float,
  action_high : Float,
  seed : UInt64,
) -> DeterministicPolicy {
  let rng = Xoshiro::from_state(seed, seed + 7UL, seed + 13UL, seed + 17UL)
  let bound_w1 = Float::from_double(@math.pow(1.0 / Float::from_int(state_dim).to_double(), 0.5))
  let bound_w2 = Float::from_double(@math.pow(1.0 / Float::from_int(hidden).to_double(), 0.5))
  let w1 : Array[Array[Float]] = Array::make(hidden, [])
  let b1 : Array[Float] = Array::make(hidden, 0.0F)
  for h in 0.. continuous action (in [action_low, action_high]).
/// `hidden_state_out` (optional) is filled with the post-ReLU hidden layer
/// for use in the critic's gradient computation.
pub fn deterministic_policy_forward(
  net : DeterministicPolicy,
  state : Array[Float],
  hidden_state_out : Array[Float],
) -> Array[Float] {
  // h = relu(W1 · state + b1)
  for h in 0.. 0.0F { s } else { 0.0F }
    ignore(hidden_state_out.set(h, relu))
  }
  // a = tanh(W2 · h + b2), then rescale to [action_low, action_high]
  let action : Array[Float] = Array::make(net.action_dim, 0.0F)
  let range = (net.action_high - net.action_low) * 0.5F
  let mid = (net.action_high + net.action_low) * 0.5F
  for a in 0.. Unit {
  for h in 0.. scalar Q
// =========================================================================

///|
/// Continuous Q-network: takes state + action as input, outputs scalar Q.
/// One hidden layer with ReLU; linear output.
pub struct QNetworkContinuous {
  state_dim : Int
  action_dim : Int
  hidden : Int
  // W1[hidden, state_dim + action_dim], b1[hidden]
  w1 : Array[Array[Float]]
  b1 : Array[Float]
  // W2[1, hidden], b2[1]
  w2 : Array[Float]
  mut b2 : Float
}

///|
/// Construct a continuous Q-network.
pub fn QNetworkContinuous::new(
  state_dim : Int,
  action_dim : Int,
  hidden : Int,
  seed : UInt64,
) -> QNetworkContinuous {
  let rng = Xoshiro::from_state(seed, seed + 11UL, seed + 23UL, seed + 37UL)
  let in_dim = state_dim + action_dim
  let bound_w1 = Float::from_double(@math.pow(1.0 / Float::from_int(in_dim).to_double(), 0.5))
  let bound_w2 = Float::from_double(@math.pow(1.0 / Float::from_int(hidden).to_double(), 0.5))
  let w1 : Array[Array[Float]] = Array::make(hidden, [])
  let b1 : Array[Float] = Array::make(hidden, 0.0F)
  for h in 0.. scalar Q. `hidden_out` is filled with
/// the post-ReLU hidden state for use in critic gradient computation.
pub fn qnet_continuous_forward(
  net : QNetworkContinuous,
  state : Array[Float],
  action : Array[Float],
  hidden_out : Array[Float],
) -> Float {
  let in_dim = net.state_dim + net.action_dim
  // h = relu(W1 · [state; action] + b1)
  for h in 0.. 0.0F { s } else { 0.0F }
    ignore(hidden_out.set(h, relu))
  }
  // q = W2 · h + b2
  let mut q = net.b2
  for h in 0.. Unit {
  for h in 0..