MLBEDSW-3403 Generate supported op report

A new CLI has been added that allows the generation of a report
containing a summary table of all TFLite ops that can be placed on the
NPU, and what the constraints are for that operator to be successfully
scheduled on the NPU.
This option will generate a new file, SUPPORTED_OPS.md containing this
information, in the current working directory.

Signed-off-by: Michael McGeagh <michael.mcgeagh@arm.com>
Change-Id: I6a7e2a49f251b76b2ea1168fff78e00da1910b25
diff --git a/ethosu/vela/vela.py b/ethosu/vela/vela.py
index 5df20d2..5df21f5 100644
--- a/ethosu/vela/vela.py
+++ b/ethosu/vela/vela.py
@@ -36,8 +36,11 @@
 from .nn_graph import PassPlacement
 from .nn_graph import TensorAllocator
 from .scheduler import ParetoMetric
+from .supported_operators import SupportedOperators
 from .tensor import MemArea
 from .tensor import Tensor
+from .tflite_mapping import builtin_operator_map
+from .tflite_mapping import builtin_type_name
 
 
 def process(input_name, enable_debug_db, arch, model_reader_options, compiler_options, scheduler_options):
@@ -119,17 +122,83 @@
     print("   Maximum Subgraph Size = {0} KiB".format(max_sg_size))
 
 
+def generate_supported_ops():
+    lines = [
+        "# Supported Ops",
+        "",
+        "This file was automatically generated by Vela using the `--supported-ops-report` parameter.  ",
+        f"Vela version: `{__version__}`",
+        "",
+        "This file complies with [**CommonMark.**](https://commonmark.org)",
+        "",
+        "## Summary Table",
+        "",
+        "The table below contains TFLite operators that can be placed on the Ethos-U NPU.  ",
+        "If the constraints are not met, then that operator will be scheduled on the CPU instead.  ",
+        "For any other TFLite operator not listed, will be left untouched and scheduled on the CPU.  ",
+        "Please check the supported operator list for your chosen runtime for further information.",
+        "",
+        "| Operator | Constraints |",
+        "| - | - |",
+    ]
+    supported = SupportedOperators()
+    op_constraint_links = []
+    op_list = sorted(((op, builtin_type_name(op)) for op in builtin_operator_map), key=lambda x: x[1])
+    for op, name in op_list:
+        internal_op = builtin_operator_map[op][0]
+        if internal_op in SupportedOperators.supported_operators:
+            links = "[Generic](#generic-constraints)"
+            if internal_op in supported.specific_constraints:
+                links += f", [Specific](#{name.lower()}-constraints)"
+                op_constraint_links.append((internal_op, name))
+            lines.append(f"| {name} | {links} |")
+    lines += [
+        "",
+        "## Generic Constraints",
+        "",
+        "This is a list of constraints that all NPU operators must satisfy in order to be scheduled on the NPU.",
+        "",
+    ]
+    for constraint in supported.generic_constraints:
+        # Markdown needs two spaces at the end of a line to render it as a separate line
+        reason = constraint.__doc__.replace("\n", "  \n")
+        lines.append(f"- {reason}")
+    for op, name in op_constraint_links:
+        lines += [
+            "",
+            f"## {name} Constraints",
+            "",
+            f"This is a list of constraints that the {name} operator must satisfy in order to be scheduled on the NPU.",
+            "",
+        ]
+        for constraint in supported.specific_constraints[op]:
+            # Markdown needs two spaces at the end of a line to render it as a separate line
+            reason = constraint.__doc__.replace("\n", "  \n")
+            lines.append(f"- {reason}")
+
+    # Note. this will generate the file in the CWD
+    filepath = os.path.join(os.getcwd(), "SUPPORTED_OPS.md")
+    with open(filepath, "wt") as md:
+        md.writelines(line + "\n" for line in lines)
+        print(f"Report file: {filepath}")
+
+
 def main(args=None):
     if args is None:
         args = sys.argv[1:]
 
     parser = argparse.ArgumentParser(prog="vela", description="Neural network model compiler for Ethos-U55")
-
+    parser.add_argument("--version", action="version", version=__version__)
     parser.add_argument(
-        "network", metavar="NETWORK", type=str, default=None, nargs=None, help="Filename of network to process"
+        "--supported-ops-report",
+        action="store_true",
+        help="Generate the SUPPORTED_OPS.md file in the current working directory and exits.",
     )
 
-    parser.add_argument("--version", action="version", version=__version__)
+    parser.add_argument(
+        "network", metavar="NETWORK", type=str, default=None, nargs="?", help="Filename of network to process"
+    )
+
     parser.add_argument(
         "--output-dir", type=str, default="output", help="Output directory to write files to (default: %(default)s)"
     )
@@ -279,6 +348,11 @@
             config = configparser.ConfigParser()
             config.read_file(f)
 
+    # Generate the supported ops report and exit
+    if args.supported_ops_report:
+        generate_supported_ops()
+        return 0
+
     if args.network is None:
         parser.error("the following argument is required: NETWORK")