> ## Documentation Index
> Fetch the complete documentation index at: https://dragonwingdocs.qualcomm.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Model Preparation

# 10.1	Model Preparation

This phase covers building the example 1D-CNN model, exporting it to int8 TFLite format, generating quantized input data, and producing the C byte arrays embedded in the MCU firmware.

The source files for this phase (`model_keras.py`, `export_tflite.py`, `gen_input_data.py`) are provided alongside this document. Install dependencies on Host Machine once before running any script:

```bash theme={null}
python3 -m venv .venv && source .venv/bin/activate
pip install numpy tensorflow
```

***

## 10.1.1	Reference Model Architecture

The example model is a minimal 1D-CNN with random weights.

| Parameter | Value |
| - | - |
| Architecture | Conv2D + Mean (GlobalAvgPool) + FullyConnected |
| TFLM ops | 3: `AddConv2D`, `AddMean`, `AddFullyConnected` |
| Framework | Keras (TensorFlow 2.x), exported via `TFLiteConverter` |
| Input shape | `[1, 1, 200, 1]` (batch, height, length, channels) |
| Input dtype | int8 — 200 bytes per inference window |
| Output shape | `[1, 2]` — 2-class logits |
| Model file size | **2,096 bytes** (`cnn1d_minimal_int8.tflite`) |
| Tensor arena | `TFLI_ARENA_KB = 4` (measure `g_tfli_arena_used` on-target to tune) |

### 10.1.1.1	Model Graph

BatchNormalization is folded into the Conv2D weights during export — it does not appear as a separate op in the quantized graph. The int8 model has exactly **3 operators**.

```
              input
                |
     [1 x 1 x 200 x 1]  int8
                |
                v
   +--------------------------------+
   |            Conv2D              |
   |  filter   [4 x 1 x 3 x 1]     |
   |  bias     [4]                  |
   |  padding  SAME                 |
   |  BatchNorm folded into weights |
   +--------------------------------+
   |             ReLU               |
   +--------------------------------+
                |
     [1 x 1 x 200 x 4]
                |
                v
   +--------------------------------+
   |             Mean               |
   |  axis  [2]  (global avg pool)  |
   +--------------------------------+
                |
            [1 x 4]
                |
                v
   +--------------------------------+
   |       FullyConnected           |
   |  weights  [2 x 4]              |
   +--------------------------------+
                |
            [1 x 2]
                |
                v
            output_0
```

***

## 10.1.2	Source Files

### 10.1.2.1	`model_keras.py` — Model Definition

Defines the `Keras` model architecture.

```python theme={null}
"""Minimal Keras CNN1D for TFLM MCU demo.

Architecture: 1 x Conv2D(4 filters, k=1x3, same) + BN + ReLU
             + GlobalAveragePool2D + Dense(num_classes)

TFLM ops needed: Conv2D, Mean, FullyConnected  (3 ops, no MaxPool/Reshape)
"""

import tensorflow as tf

INPUT_HEIGHT   = 1
INPUT_LENGTH   = 200
INPUT_CHANNELS = 1


def build_model(num_classes: int = 2, channels: int = 4) -> tf.keras.Model:
    inputs = tf.keras.Input(
        batch_shape=(1, INPUT_HEIGHT, INPUT_LENGTH, INPUT_CHANNELS), name="input")
    x = tf.keras.layers.Conv2D(
        channels, kernel_size=(1, 3), padding="same")(inputs)
    x = tf.keras.layers.BatchNormalization()(x)
    x = tf.keras.layers.ReLU()(x)
    x = tf.keras.layers.GlobalAveragePooling2D()(x)
    outputs = tf.keras.layers.Dense(num_classes, name="output")(x)
    return tf.keras.Model(inputs, outputs, name="CNN1D_minimal")


if __name__ == "__main__":
    model = build_model()
    model.summary()
    n = sum(tf.size(w).numpy() for w in model.trainable_weights)
    print(f"trainable parameters: {n}")
```

### 10.1.2.2	`export_tflite.py` — Export Float32 and Int8 TFLite

Builds the model from `model_keras.py`, runs full-integer quantization with a random representative dataset, and validates both outputs.

```python theme={null}
"""Export minimal CNN1D to TFLite float32 and full-integer int8.

Outputs
-------
  model_files/cnn1d_minimal_float.tflite  -- float32 (reference)
  model_files/cnn1d_minimal_int8.tflite   -- int8, deploy on MCU
"""

import argparse
import os
import numpy as np
import tensorflow as tf
from model_keras import build_model, INPUT_HEIGHT, INPUT_LENGTH, INPUT_CHANNELS

REPR_SAMPLES = 100


def representative_dataset():
    rng = np.random.default_rng(42)
    for _ in range(REPR_SAMPLES):
        sample = rng.standard_normal(
            (1, INPUT_HEIGHT, INPUT_LENGTH, INPUT_CHANNELS)).astype(np.float32)
        yield [sample]


def export_float(model, path):
    converter = tf.lite.TFLiteConverter.from_keras_model(model)
    data = converter.convert()
    with open(path, "wb") as f:
        f.write(data)
    print(f"  wrote {path}  ({len(data):,} bytes)")
    return data


def export_int8(model, path):
    converter = tf.lite.TFLiteConverter.from_keras_model(model)
    converter.optimizations = [tf.lite.Optimize.DEFAULT]
    converter.representative_dataset = representative_dataset
    converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS_INT8]
    converter.inference_input_type  = tf.int8
    converter.inference_output_type = tf.int8
    data = converter.convert()
    with open(path, "wb") as f:
        f.write(data)
    print(f"  wrote {path}  ({len(data):,} bytes)")
    return data


def validate_and_report(tflite_bytes, dtype, label):
    interp = tf.lite.Interpreter(model_content=tflite_bytes)
    interp.allocate_tensors()
    inp = interp.get_input_details()[0]
    out = interp.get_output_details()[0]
    print(f"  [{label}] input  shape={inp['shape']}  dtype={inp['dtype'].__name__}"
          f"  scale={inp['quantization'][0]:.6f}  zero_pt={inp['quantization'][1]}")
    print(f"  [{label}] output shape={out['shape']}  dtype={out['dtype'].__name__}"
          f"  scale={out['quantization'][0]:.6f}  zero_pt={out['quantization'][1]}")
    x = np.zeros((1, INPUT_HEIGHT, INPUT_LENGTH, INPUT_CHANNELS), dtype=dtype)
    interp.set_tensor(inp["index"], x)
    interp.invoke()
    result = interp.get_tensor(out["index"])
    print(f"  [{label}] zero-input output: {result}")
    try:
        ops = interp._get_ops_details()
        for op in ops:
            print(f"    op_index={op['index']}  name={op['op_name']}")
    except AttributeError:
        print("    (op detail API not available in this TF version)")
    return inp['quantization']


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--num-classes", type=int, default=2)
    ap.add_argument("--channels",    type=int, default=4)
    ap.add_argument("--out-dir",     default="model_files")
    args = ap.parse_args()

    os.makedirs(args.out_dir, exist_ok=True)
    model = build_model(num_classes=args.num_classes, channels=args.channels)

    float_path = os.path.join(args.out_dir, "cnn1d_minimal_float.tflite")
    int8_path  = os.path.join(args.out_dir, "cnn1d_minimal_int8.tflite")

    print("--- float32 ---")
    fb = export_float(model, float_path)
    validate_and_report(fb, np.float32, "float32")

    print("\n--- int8 ---")
    ib = export_int8(model, int8_path)
    scale, zero_pt = validate_and_report(ib, np.int8, "int8")

    print(f"\nInput quantization: scale={scale:.6f}  zero_point={zero_pt}")
    print(f"  float->int8:  q = clip(round(x / {scale:.6f}) + {zero_pt}, -128, 127)")
    print(f"\nDeploy: {int8_path}")


if __name__ == "__main__":
    main()
```

**Run:**

```bash theme={null}
python export_tflite.py
```

**Outputs:**

| File | Purpose |
| - | - |
| `model_files/cnn1d_minimal_float.tflite` | Float32 model — reference only |
| `model_files/cnn1d_minimal_int8.tflite` | Int8 model — embed in MCU firmware |

The script also prints the input quantization parameters (`scale`, `zero_point`) used internally by `gen_input_data.py`.

### 10.1.2.3	`gen_input_data.py` — Generate Quantized Input Files

Reads quantization parameters directly from the exported int8 model, then generates 200-sample int8 windows from a real dataset or synthetic data.

```python theme={null}
"""Generate int8 and float32 input files for the minimal CNN1D model.

Outputs
-------
  inputs/input_0.bin        -- 200 int8 bytes, ready for MCU g_cnn_input[]
  inputs/input_0_f32.raw    -- 200 float32 = 800 bytes, for reference
  inputs/input_list.txt     -- list of .bin paths for batch testing
"""

import argparse
import os
import numpy as np
import tensorflow as tf

WINDOW_LEN  = 200
DATASET_LEN = 1696
DTYPE_F32   = np.dtype("<f4")


def quantize(data_f32, scale, zero_pt):
    q = np.round(data_f32 / scale).astype(np.int32) + zero_pt
    return np.clip(q, -128, 127).astype(np.int8)


def get_quant_params(model_path):
    interp = tf.lite.Interpreter(model_path=model_path)
    interp.allocate_tensors()
    inp = interp.get_input_details()[0]
    scale, zero_pt = inp["quantization"]
    if scale == 0.0:
        raise ValueError(f"{model_path}: input tensor has scale=0 -- not an int8 model")
    return float(scale), int(zero_pt)


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--model",       default="model_files/cnn1d_minimal_int8.tflite")
    ap.add_argument("--dataset",     default=None,
                    help="Raw float32 LE dataset (1,696 values). Omit for synthetic data.")
    ap.add_argument("--num-windows", type=int, default=8)
    ap.add_argument("--out-dir",     default="inputs")
    args = ap.parse_args()

    os.makedirs(args.out_dir, exist_ok=True)

    if os.path.exists(args.model):
        scale, zero_pt = get_quant_params(args.model)
        print(f"quant params from {args.model}: scale={scale:.6f}  zero_point={zero_pt}")
    else:
        scale, zero_pt = 1.0 / 128.0, 0
        print("model not found -- using default quant params: scale=1/128  zero_point=0")

    if args.dataset:
        data = np.fromfile(args.dataset, dtype=DTYPE_F32)
        print(f"loaded dataset: {args.dataset}  ({data.size} values)")
    else:
        rng  = np.random.default_rng(0)
        data = rng.standard_normal(DATASET_LEN).astype(DTYPE_F32)
        print(f"synthesized dataset: {data.size} values")

    bin_files = []
    for i in range(args.num_windows):
        start      = (i * WINDOW_LEN) % (len(data) - WINDOW_LEN + 1)
        window_f32 = data[start : start + WINDOW_LEN]
        window_i8  = quantize(window_f32, scale, zero_pt)

        bin_path = os.path.join(args.out_dir, f"input_{i}.bin")
        window_i8.tofile(bin_path)
        bin_files.append(bin_path)

        f32_path = os.path.join(args.out_dir, f"input_{i}_f32.raw")
        window_f32.tofile(f32_path)

        print(f"  window {i}: start={start}  "
              f"f32=[{window_f32.min():.3f}, {window_f32.max():.3f}]  "
              f"i8=[{window_i8.min()}, {window_i8.max()}]  -> {bin_path}")

    list_path = os.path.join(args.out_dir, "input_list.txt")
    with open(list_path, "w") as f:
        for p in bin_files:
            f.write(os.path.abspath(p) + "\n")
    print(f"\nwrote {list_path}  ({len(bin_files)} entries)")
    print(f"Primary MCU input file: {os.path.abspath(bin_files[0])}")
    print(f"  use as g_cnn_input[] in input_data.cpp (200 int8 bytes)")


if __name__ == "__main__":
    main()
```

**Run:**

```bash theme={null}
python gen_input_data.py --num-windows 8
# Pass --dataset /path/to/data.raw to use a real 1,696-value float32 LE dataset
```

**Outputs (in `inputs/` folder):**

| File | Size | Purpose |
| - | - | - |
| `input_0.bin` ... `input_7.bin` | 200 bytes each | Pre-quantized int8 windows — embed `input_0.bin` in firmware |
| `input_0_f32.raw` ... `input_7_f32.raw` | 800 bytes each | Float32 originals — host verification only |
| `input_list.txt` | — | Absolute paths for batch testing tools |

***

## 10.1.3	Verify on Host

Before integrating model with MCU Firmware, confirm the model runs correctly with the generated input:

```python theme={null}
import tensorflow as tf
import numpy as np

interp = tf.lite.Interpreter("model_files/cnn1d_minimal_int8.tflite")
interp.allocate_tensors()
inp = interp.get_input_details()[0]
out = interp.get_output_details()[0]

print(f"Input:  shape={inp['shape']}  dtype={inp['dtype']}  "
      f"scale={inp['quantization'][0]:.6f}  zp={inp['quantization'][1]}")
print(f"Output: shape={out['shape']}  dtype={out['dtype']}  "
      f"scale={out['quantization'][0]:.6f}  zp={out['quantization'][1]}")

x = np.fromfile("inputs/input_0.bin", dtype=np.int8).reshape(inp['shape'])
interp.set_tensor(inp['index'], x)
interp.invoke()
y = interp.get_tensor(out['index'])
print(f"Output logits (int8): {y}")
print(f"Predicted class: {np.argmax(y)}")
```

Expected shape and type (values depend on random weights):

```
Input:  shape=[1 1 200 1]  dtype=int8  scale=0.007812  zp=0
Output: shape=[1 2]  dtype=int8  scale=...  zp=...
Output logits (int8): [[...]]
Predicted class: 0 or 1
```
