// spatio_temporal.mbt — 5D [T, B, C, H, W] tensor for spiking-CNN
// time-series input (v0.14.1).
//
// Layout convention (matching SNN.jl / Conv-STDP / most time-major
// spiking CNN literature):
//   - `t` (outermost): time step index, 0.. STImage {
  { data: Array::make(t * b * c * h * w, 0.0F), t, b, c, h, w }
}

///|
/// Total element count (T * B * C * H * W).
pub fn STImage::numel(self : STImage) -> Int {
  self.data.length()
}

///|
/// Shape as `(t, b, c, h, w)`.
pub fn STImage::shape(self : STImage) -> (Int, Int, Int, Int, Int) {
  (self.t, self.b, self.c, self.h, self.w)
}

///|
/// Stride helper: number of elements per [B, C, H, W] "frame block".
pub fn STImage::frame_block(self : STImage) -> Int {
  self.b * self.c * self.h * self.w
}

///|
/// Stride helper: number of elements per [C, H, W] "batch slice".
pub fn STImage::batch_slice(self : STImage) -> Int {
  self.c * self.h * self.w
}

///|
/// Flat offset of `[t, b, c, h, w]` in `data`.
pub fn STImage::offset(
  self : STImage,
  t : Int,
  b : Int,
  c : Int,
  h : Int,
  w : Int,
) -> Int {
  t * self.frame_block() + b * self.batch_slice() + c * (self.h * self.w) +
  h * self.w +
  w
}

///|
/// Read `[t, b, c, h, w]`.
pub fn STImage::get(
  self : STImage,
  t : Int,
  b : Int,
  c : Int,
  h : Int,
  w : Int,
) -> Float {
  self.data[self.offset(t, b, c, h, w)]
}

///|
/// Write `[t, b, c, h, w]`.
pub fn STImage::set(
  self : STImage,
  t : Int,
  b : Int,
  c : Int,
  h : Int,
  w : Int,
  v : Float,
) -> Unit {
  self.data[self.offset(t, b, c, h, w)] = v
}

///|
/// Extract a single time step `t` as a 4D `Image { n: 1, c, h, w }`.
/// Returns a fresh `Image` (data is copied, not aliased).
pub fn STImage::get_frame(self : STImage, t : Int) -> Image {
  let block = self.frame_block()
  let plane = self.c * self.h * self.w
  let out : Array[Float] = Array::make(plane, 0.0F)
  let src_off = t * block
  for i in 0.. Image {
  let slice = self.batch_slice()
  let out : Array[Float] = Array::make(self.t * slice, 0.0F)
  for t_idx in 0.. STImage raise Failure {
  if frames.length() == 0 { raise Failure::Failure("from_frames: empty array") }
  let f0 = frames[0]
  let (_, c, h, w) = f0.shape()
  let t = frames.length()
  let plane = c * h * w
  let out : Array[Float] = Array::make(t * plane, 0.0F)
  for t_idx in 0.. STImage raise Failure {
  if a.b != b.b || a.c != b.c || a.h != b.h || a.w != b.w {
    raise Failure::Failure(
      "st_image_concat_t: shape mismatch (\{a.b},\{a.c},\{a.h},\{a.w}) vs (\{b.b},\{b.c},\{b.h},\{b.w})",
    )
  }
  let t = a.t + b.t
  let slice = a.batch_slice()
  let out : Array[Float] = Array::make(t * a.b * slice, 0.0F)
  let frame_block_a = a.frame_block()
  let frame_block_b = b.frame_block()
  for t_idx in 0.. STImage {
  let (_, c, h, w) = img.shape()
  let plane = c * h * w
  let out : Array[Float] = Array::make(t * plane, 0.0F)
  for t_idx in 0..