Delay OP python extension (#2138)

Extends the Signal Library Delay OP to be usable from python.
Can test via `bazel run python/tflite_micro/signal:delay_op_test`

BUG=[287346710](http://b/287346710)
diff --git a/python/tflite_micro/signal/BUILD b/python/tflite_micro/signal/BUILD
index e516475..a0f9ae1 100644
--- a/python/tflite_micro/signal/BUILD
+++ b/python/tflite_micro/signal/BUILD
@@ -15,6 +15,7 @@
     name = "ops_lib",
     visibility = [":signal_friends"],
     deps = [
+        ":delay_op_cc",
         ":fft_ops_cc",
         ":window_op_cc",
     ],
@@ -29,12 +30,36 @@
     srcs_version = "PY3",
     visibility = ["//python/tflite_micro/signal/utils:__subpackages__"],
     deps = [
+        ":delay_op",
         ":fft_ops",
         ":window_op",
     ],
 )
 
 py_tflm_signal_library(
+    name = "delay_op",
+    srcs = ["ops/delay_op.py"],
+    cc_op_defs = ["//signal/tensorflow_core/ops:delay_op"],
+    cc_op_kernels = [
+        "//signal/tensorflow_core/kernels:delay_kernel",
+    ],
+)
+
+py_test(
+    name = "delay_op_test",
+    size = "small",
+    srcs = ["ops/delay_op_test.py"],
+    python_version = "PY3",
+    srcs_version = "PY3",
+    deps = [
+        ":delay_op",
+        "//python/tflite_micro/signal/utils:util",
+        requirement("numpy"),
+        requirement("tensorflow-cpu"),
+    ],
+)
+
+py_tflm_signal_library(
     name = "fft_ops",
     srcs = ["ops/fft_ops.py"],
     cc_op_defs = ["//signal/tensorflow_core/ops:fft_ops"],
diff --git a/python/tflite_micro/signal/ops/delay_op.py b/python/tflite_micro/signal/ops/delay_op.py
new file mode 100644
index 0000000..c750804
--- /dev/null
+++ b/python/tflite_micro/signal/ops/delay_op.py
@@ -0,0 +1,37 @@
+# Copyright 2023 The TensorFlow Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+# ==============================================================================
+"""Use overlap add op in python."""
+
+import tensorflow as tf
+from tflite_micro.python.tflite_micro.signal.utils import util
+
+gen_delay_op = util.load_custom_op('delay_op.so')
+
+
+def _delay_wrapper(delay_fn, default_name):
+  """Wrapper around gen_delay_op.delay*."""
+
+  def _delay(input_tensor, delay_length, name=default_name):
+    with tf.name_scope(name) as name:
+      input_tensor = tf.convert_to_tensor(input_tensor, dtype=tf.int16)
+      return delay_fn(input_tensor, delay_length=delay_length, name=name)
+
+  return _delay
+
+
+# TODO(b/286250473): change back name after name clash resolved
+delay = _delay_wrapper(gen_delay_op.signal_delay, "signal_delay")
+
+tf.no_gradient("signal_delay")
diff --git a/python/tflite_micro/signal/ops/delay_op_test.py b/python/tflite_micro/signal/ops/delay_op_test.py
new file mode 100644
index 0000000..66b033f
--- /dev/null
+++ b/python/tflite_micro/signal/ops/delay_op_test.py
@@ -0,0 +1,85 @@
+# Copyright 2023 The TensorFlow Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+# ==============================================================================
+"""Tests for delay op."""
+
+import numpy as np
+import tensorflow as tf
+
+from tflite_micro.python.tflite_micro.signal.ops import delay_op
+from tflite_micro.python.tflite_micro.signal.utils import util
+
+
+class DelayOpTest(tf.test.TestCase):
+
+  def TestHelper(self, input_signal, delay_length, frame_size):
+    inner_dim_size = input_signal.shape[-1]
+    input_signal_rank = len(input_signal.shape)
+    frame_num = int(np.ceil((inner_dim_size + delay_length) / frame_size))
+    # We need to continue feeding the op with zeros until the delay line is
+    # flushed. Pad the input signal to a multiple of frame_size.
+    padded_size = frame_num * frame_size
+    pad_size = int(padded_size - inner_dim_size)
+    # Axes to pass to np.pad. All axes have no padding except the innermost one.
+    pad_outer_axes = np.zeros([input_signal_rank - 1, 2], dtype=int)
+    pad_input_signal = np.vstack([pad_outer_axes, [0, pad_size]])
+    input_signal_padded = np.pad(input_signal, pad_input_signal)
+    delay_exp_signal = np.vstack(
+        [pad_outer_axes, [delay_length, pad_size - delay_length]])
+    delay_exp = np.pad(input_signal, delay_exp_signal)
+    delay_out = np.zeros(input_signal_padded.shape)
+
+    in_frame_shape = input_signal.shape[:-1] + (frame_size, )
+    func = tf.function(delay_op.delay)
+    concrete_function = func.get_concrete_function(tf.TensorSpec(
+        in_frame_shape, dtype=tf.int16),
+                                                   delay_length=delay_length)
+    interpreter = util.get_tflm_interpreter(concrete_function, func)
+
+    for i in range(frame_num):
+      in_frame = input_signal_padded[..., i * frame_size:(i + 1) * frame_size]
+      # TFLM
+      interpreter.set_input(in_frame, 0)
+      interpreter.invoke()
+      out_frame_tflm = interpreter.get_output(0)
+      # TF
+      out_frame = self.evaluate(
+          delay_op.delay(in_frame, delay_length=delay_length))
+      delay_out[..., i * frame_size:(i + 1) * frame_size] = out_frame
+      self.assertAllEqual(out_frame, out_frame_tflm)
+    self.assertAllEqual(delay_out, delay_exp)
+
+  def testFrameLargerThanDelay(self):
+    self.TestHelper(np.arange(0, 30, dtype=np.int16), 7, 10)
+
+  def testFrameSmallerThanDelay(self):
+    self.TestHelper(np.arange(0, 70, dtype=np.int16), 21, 3)
+
+  def testZeroDelay(self):
+    self.TestHelper(np.arange(0, 20, dtype=np.int16), 0, 3)
+
+  def testNegativeDelay(self):
+    with self.assertRaises((tf.errors.InvalidArgumentError, ValueError)):
+      self.TestHelper(np.arange(1, 20, dtype=np.int16), -21, 3)
+
+  def testMultiDimensionalDelay(self):
+    input_signal = np.reshape(np.arange(0, 120, dtype=np.int16), [2, 3, 20])
+    self.TestHelper(input_signal, 4, 6)
+    input_signal = np.reshape(np.arange(0, 72, dtype=np.int16),
+                              [2, 2, 3, 3, 2])
+    self.TestHelper(input_signal, 7, 3)
+
+
+if __name__ == '__main__':
+  tf.test.main()
diff --git a/signal/tensorflow_core/kernels/BUILD b/signal/tensorflow_core/kernels/BUILD
index 6745d9c..bea8eef 100644
--- a/signal/tensorflow_core/kernels/BUILD
+++ b/signal/tensorflow_core/kernels/BUILD
@@ -6,6 +6,15 @@
 )
 
 tflm_signal_kernel_library(
+    name = "delay_kernel",
+    srcs = ["delay_kernel.cc"],
+    deps = [
+        "//signal/src:circular_buffer",
+        "@tensorflow_cc_deps//:cc_library",
+    ],
+)
+
+tflm_signal_kernel_library(
     name = "fft_kernel",
     srcs = ["fft_kernels.cc"],
     deps = [
diff --git a/signal/tensorflow_core/kernels/delay_kernel.cc b/signal/tensorflow_core/kernels/delay_kernel.cc
new file mode 100644
index 0000000..8c5c505
--- /dev/null
+++ b/signal/tensorflow_core/kernels/delay_kernel.cc
@@ -0,0 +1,94 @@
+/* Copyright 2023 The TensorFlow Authors. All Rights Reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+==============================================================================*/
+
+#include <cstdint>
+
+#include "signal/src/circular_buffer.h"
+#include "tensorflow/core/framework/op_kernel.h"
+
+namespace tensorflow {
+namespace signal {
+
+class DelayOp : public tensorflow::OpKernel {
+ public:
+  explicit DelayOp(tensorflow::OpKernelConstruction* context)
+      : tensorflow::OpKernel(context) {
+    OP_REQUIRES_OK(context, context->GetAttr("delay_length", &delay_length_));
+    initialized_ = false;
+  }
+
+  ~DelayOp() {}
+
+  void Compute(tensorflow::OpKernelContext* context) override {
+    const tensorflow::Tensor& input_tensor = context->input(0);
+    if (!initialized_) {
+      frame_size_ = input_tensor.flat_inner_dims<int16_t>().dimensions().at(1);
+      outer_dims_ = input_tensor.flat_inner_dims<int16_t>().dimensions().at(0);
+
+      state_tensors_.resize(outer_dims_);
+      circular_buffers_.resize(outer_dims_);
+
+      // Calculate the capacity of the circular buffer.
+      size_t capacity = frame_size_ + delay_length_;
+      size_t state_size =
+          tflite::tflm_signal::CircularBufferGetNeededMemory(capacity);
+      for (int i = 0; i < outer_dims_; i++) {
+        OP_REQUIRES_OK(
+            context,
+            context->allocate_temp(
+                DT_INT8, TensorShape({static_cast<int32_t>(state_size)}),
+                &state_tensors_[i]));
+        int8_t* state_ = state_tensors_[i].flat<int8_t>().data();
+        circular_buffers_[i] = tflite::tflm_signal::CircularBufferInit(
+            capacity, state_, state_size);
+        tflite::tflm_signal::CircularBufferWriteZeros(circular_buffers_[i],
+                                                      delay_length_);
+      }
+      initialized_ = true;
+    }
+
+    TensorShape output_shape = input_tensor.shape();
+    tensorflow::Tensor* output_tensor = nullptr;
+    OP_REQUIRES_OK(context,
+                   context->allocate_output(0, output_shape, &output_tensor));
+
+    for (int dim_index = 0, sample_index = 0; dim_index < outer_dims_;
+         dim_index++, sample_index += frame_size_) {
+      tflite::tflm_signal::CircularBufferWrite(
+          circular_buffers_[dim_index],
+          &input_tensor.flat<int16_t>().data()[sample_index], frame_size_);
+      tflite::tflm_signal::CircularBufferGet(
+          circular_buffers_[dim_index], frame_size_,
+          &(reinterpret_cast<int16_t*>(output_tensor->data()))[sample_index]);
+      tflite::tflm_signal::CircularBufferDiscard(circular_buffers_[dim_index],
+                                                 frame_size_);
+    }
+  }
+
+ private:
+  bool initialized_;
+  int frame_size_;
+  int delay_length_;
+  int outer_dims_;
+  std::vector<Tensor> state_tensors_;
+  std::vector<struct tflite::tflm_signal::CircularBuffer*> circular_buffers_;
+};
+
+// TODO(b/286250473): change back name after name clash resolved
+REGISTER_KERNEL_BUILDER(Name("SignalDelay").Device(tensorflow::DEVICE_CPU),
+                        DelayOp);
+
+}  // namespace signal
+}  // namespace tensorflow
diff --git a/signal/tensorflow_core/ops/BUILD b/signal/tensorflow_core/ops/BUILD
index 8c24f22..436dde5 100644
--- a/signal/tensorflow_core/ops/BUILD
+++ b/signal/tensorflow_core/ops/BUILD
@@ -6,6 +6,14 @@
 )
 
 tflm_signal_kernel_library(
+    name = "delay_op",
+    srcs = ["delay_op.cc"],
+    deps = [
+        "@tensorflow_cc_deps//:cc_library",
+    ],
+)
+
+tflm_signal_kernel_library(
     name = "fft_ops",
     srcs = ["fft_ops.cc"],
     deps = [
diff --git a/signal/tensorflow_core/ops/delay_op.cc b/signal/tensorflow_core/ops/delay_op.cc
new file mode 100644
index 0000000..dfee3d2
--- /dev/null
+++ b/signal/tensorflow_core/ops/delay_op.cc
@@ -0,0 +1,58 @@
+/* Copyright 2023 The TensorFlow Authors. All Rights Reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+==============================================================================*/
+
+#include "tensorflow/core/framework/op.h"
+#include "tensorflow/core/framework/shape_inference.h"
+
+using tensorflow::shape_inference::InferenceContext;
+using tensorflow::shape_inference::ShapeHandle;
+
+namespace tensorflow {
+namespace signal {
+
+Status DelayShape(InferenceContext* c) {
+  ShapeHandle out;
+  TF_RETURN_IF_ERROR(c->WithRankAtLeast(c->input(0), 1, &out));
+  c->set_output(0, out);
+  return OkStatus();
+}
+
+// TODO(b/286250473): change back name after name clash resolved
+REGISTER_OP("SignalDelay")
+    .Attr("delay_length: int >= 0")
+    .Input("input: int16")
+    .Output("output: int16")
+    .SetShapeFn(DelayShape)
+    .Doc(R"doc(
+Delay the innermost dimension of input signal by delay_length samples.
+
+For example, assuming an input signal of 10 samples,
+[1 2 3 4 5 6 7 8 9 0]
+If we input the signal to a delay op configured with delay_length=3, the op
+will produce the following output:
+[0 0 0 1 2 3 4 5 6 7]
+To retrieve the remainder of the input signal, call the delay op again with
+zeros as input:
+[0 0 0 0 0 0 0 0 0 0]
+to get the output:
+[8 9 0 0 0 0 0 0 0 0]
+
+input: A multidimensional input signal.
+output: An output signal of the same shape as the input signal. The innermost
+        dimension is delayed by delay_length samples.
+)doc");
+
+}  // namespace signal
+}  // namespace tensorflow