///|
/// One completed continuation stage, in caller-supplied schedule order.
/// `result.iterations` counts full row/column updates at this epsilon only.
pub(all) struct SinkhornContinuationStage {
  options : SinkhornOptions
  result : SinkhornResult
}

///|
/// Work completed before a solver error. The failing stage is not included.
/// `last_seed` is the last completed stage's seed, or the caller's initial seed
/// when no stage completed. Iterations performed inside a failing solve are not
/// available from the underlying solver and are not counted.
pub(all) struct SinkhornContinuationProgress {
  stages : Array[SinkhornContinuationStage]
  total_iterations : Int64
  last_seed : SinkhornWarmStart?
}

///|
/// Completed schedule, including stages that reached their iteration limits.
/// `termination` is exclusively the final stage's termination, not an aggregate.
/// The final transport plan is `stages[stages.length() - 1].result`.
pub(all) struct SinkhornContinuationResult {
  stages : Array[SinkhornContinuationStage]
  total_iterations : Int64
  last_seed : SinkhornWarmStart
  termination : SinkhornTermination
}

///|
/// Schedule validation errors and lossless, stage-indexed solver errors.
/// All indices are zero-based. Validation completes before any solver runs.
/// `NonDecreasingEpsilon(i)` compares stage i to stage i - 1.
pub(all) enum SinkhornContinuationError {
  EmptySchedule
  InvalidEpsilon(Int)
  NonDecreasingEpsilon(Int)
  InvalidTolerance(Int)
  InvalidIterationLimit(Int)
  StageFailed(Int, SinkhornError, SinkhornContinuationProgress)
}

///|
/// Solve balanced transport along a strictly decreasing epsilon schedule.
///
/// Each schedule entry supplies a finite positive epsilon and absolute marginal
/// tolerance, plus a positive iteration limit for that stage (not a shared
/// budget). A single entry is valid. Entire-schedule validation precedes input
/// validation by the solver. At each index, epsilon, decreasing order, tolerance,
/// then iteration limit are checked, in that order.
///
/// Every stage runs `balanced_sinkhorn_with_warm_start`; the returned opaque
/// cost-unit seed is passed unchanged to the next stage. An intermediate
/// IterationLimit is recorded and does not stop continuation. Only the final
/// stage determines convergence. Solver errors stop immediately and preserve
/// both their original cause and the completed progress. No input or seed is
/// mutated, and no schedule is generated, sorted, or normalized.
pub fn balanced_sinkhorn_continuation(
  source : Array[Double],
  target : Array[Double],
  cost : Array[Array[Double]],
  schedule : Array[SinkhornOptions],
  warm_start : SinkhornWarmStart?,
) -> Result[SinkhornContinuationResult, SinkhornContinuationError] {
  if schedule.is_empty() {
    return Err(SinkhornContinuationError::EmptySchedule)
  }
  for index in 0.. 0 && options.epsilon >= schedule[index - 1].epsilon {
      return Err(SinkhornContinuationError::NonDecreasingEpsilon(index))
    }
    if !is_finite(options.tolerance) || options.tolerance <= 0.0 {
      return Err(SinkhornContinuationError::InvalidTolerance(index))
    }
    if options.max_iterations <= 0 {
      return Err(SinkhornContinuationError::InvalidIterationLimit(index))
    }
  }
  let stages : Array[SinkhornContinuationStage] = []
  let (first_result, first_seed) = match
    balanced_sinkhorn_with_warm_start(
      source,
      target,
      cost,
      schedule[0],
      warm_start,
    ) {
    Err(error) =>
      return Err(
        SinkhornContinuationError::StageFailed(0, error, {
          stages,
          total_iterations: 0L,
          last_seed: warm_start,
        }),
      )
    Ok(value) => value
  }
  stages.push({ options: schedule[0], result: first_result, })
  let mut total_iterations = first_result.iterations.to_int64()
  let mut last_seed = first_seed
  let mut termination = first_result.termination
  for index in 1..
        return Err(
          SinkhornContinuationError::StageFailed(index, error, {
            stages,
            total_iterations,
            last_seed: Some(last_seed),
          }),
        )
      Ok(value) => value
    }
    stages.push({ options: schedule[index], result, })
    total_iterations = total_iterations + result.iterations.to_int64()
    last_seed = seed
    termination = result.termination
  }
  Ok({ stages, total_iterations, last_seed, termination, })
}