diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 8e4e82db3a046e454373c2f0b58d55865cda9c5b..c513c5493d674b067b82fdae9e675d7f9b6eb024 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -27,7 +27,8 @@
 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
 exclude: '^docs/conf.py'
-
+default_language_version:
+    python: python3
 repos:
 # black
 - repo: https://github.com/ambv/black
diff --git a/run-docker.sh b/run-docker.sh
index e010733080b7cae205119e2bc136cff836f71fa5..f5c9f64b7d89e7def72c5b39131f37c22fcf57bf 100755
--- a/run-docker.sh
+++ b/run-docker.sh
@@ -89,7 +89,7 @@ git clone $EXAMPLES_REPO $EXAMPLES_LOCAL ||  git -C "$EXAMPLES_LOCAL" checkout f
 git clone $CNPY_REPO $CNPY_LOCAL ||  git -C "$CNPY_LOCAL" pull
 git clone $FINN_HLS_REPO $FINN_HLS_LOCAL ||  git -C "$FINN_HLS_LOCAL" checkout master; git -C "$FINN_HLS_LOCAL" pull
 git clone $PYVERILATOR_REPO $PYVERILATOR_LOCAL ||  git -C "$PYVERILATOR_LOCAL" pull
-git clone $PYNQSHELL_REPO $PYNQSHELL_LOCAL ||  git -C "$PYNQSHELL_LOCAL" pull
+git clone $PYNQSHELL_REPO $PYNQSHELL_LOCAL ||  git -C "$PYNQSHELL_LOCAL" checkout feature/synth_rpt; git -C "$PYNQSHELL_LOCAL" pull
 
 # ensure build dir exists locally
 mkdir -p $BUILD_LOCAL
diff --git a/src/finn/analysis/fpgadataflow/hls_synth_res_estimation.py b/src/finn/analysis/fpgadataflow/hls_synth_res_estimation.py
index 0334c316b80a5c0628d00b75eb40776436cb8434..c7db5b1d9d22ea89740f4c82633c96746a6fa5ee 100644
--- a/src/finn/analysis/fpgadataflow/hls_synth_res_estimation.py
+++ b/src/finn/analysis/fpgadataflow/hls_synth_res_estimation.py
@@ -34,9 +34,9 @@ import finn.util.basic as util
 
 
 def hls_synth_res_estimation(model):
-    """Extracts the results from the vivado synthesis.
+    """Extracts the FPGA resource results from the Vivado HLS synthesis estimates.
 
-    Returns {node name : resource estimation}."""
+    Returns {node name : resources_dict}."""
 
     res_dict = {}
     for node in model.graph.node:
@@ -60,14 +60,12 @@ def hls_synth_res_estimation(model):
                     )
 
                     if os.path.isfile(xmlfile):
-                        res_dict[node.name] = []
+                        res_dict[node.name] = dict()
                         tree = ET.parse(xmlfile)
                         root = tree.getroot()
                         for item in root.findall("AreaEstimates/Resources"):
                             for child in item:
-                                res_dict[node.name].append(
-                                    ["{} : {}".format(child.tag, child.text)]
-                                )
+                                res_dict[node.name][child.tag] = child.text
                     else:
                         raise Exception(
                             """Please run "HLSSynth_IPGen" first
diff --git a/src/finn/analysis/fpgadataflow/post_synth_res.py b/src/finn/analysis/fpgadataflow/post_synth_res.py
new file mode 100644
index 0000000000000000000000000000000000000000..508c34aaed50f2935f4915cdcea29a3e92641b3c
--- /dev/null
+++ b/src/finn/analysis/fpgadataflow/post_synth_res.py
@@ -0,0 +1,80 @@
+# Copyright (c) 2020, Xilinx
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# * Redistributions of source code must retain the above copyright notice, this
+#   list of conditions and the following disclaimer.
+#
+# * Redistributions in binary form must reproduce the above copyright notice,
+#   this list of conditions and the following disclaimer in the documentation
+#   and/or other materials provided with the distribution.
+#
+# * Neither the name of FINN nor the names of its
+#   contributors may be used to endorse or promote products derived from
+#   this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import os
+import xml.etree.ElementTree as ET
+
+from finn.transformation.move_reshape import _is_fpgadataflow_node
+
+
+def post_synth_res(model):
+    """Extracts the FPGA resource results from the Vivado synthesis.
+
+    Returns {node name : resources_dict}."""
+
+    res_dict = {}
+    synth_report_filename = model.get_metadata_prop("vivado_synth_rpt")
+    if os.path.isfile(synth_report_filename):
+        tree = ET.parse(synth_report_filename)
+        root = tree.getroot()
+        all_cells = root.findall(".//tablecell")
+        # strip all whitespace from table cell contents
+        for cell in all_cells:
+            cell.attrib["contents"] = cell.attrib["contents"].strip()
+    else:
+        raise Exception("Please run synthesis first")
+
+    for node in model.graph.node:
+        if _is_fpgadataflow_node(node):
+            row = root.findall(".//*[@contents='%s']/.." % node.name)
+            if row != []:
+                node_dict = {}
+                row = row[0].getchildren()
+                """ Expected XML structure:
+<tablerow class="" suppressoutput="0" wordwrap="0">
+    <tableheader class="" contents="Instance" halign="3" width="-1"/>
+    <tableheader class="" contents="Module" halign="3" width="-1"/>
+    <tableheader class="" contents="Total LUTs" halign="3" width="-1"/>
+    <tableheader class="" contents="Logic LUTs" halign="3" width="-1"/>
+    <tableheader class="" contents="LUTRAMs" halign="3" width="-1"/>
+    <tableheader class="" contents="SRLs" halign="3" width="-1"/>
+    <tableheader class="" contents="FFs" halign="3" width="-1"/>
+    <tableheader class="" contents="RAMB36" halign="3" width="-1"/>
+    <tableheader class="" contents="RAMB18" halign="3" width="-1"/>
+    <tableheader class="" contents="DSP48 Blocks" halign="3" width="-1"/>
+</tablerow>
+                """
+                node_dict["LUT"] = int(row[2].attrib["contents"])
+                node_dict["SRL"] = int(row[5].attrib["contents"])
+                node_dict["FF"] = int(row[6].attrib["contents"])
+                node_dict["BRAM_36K"] = int(row[7].attrib["contents"])
+                node_dict["BRAM_18K"] = int(row[8].attrib["contents"])
+                node_dict["DSP48"] = int(row[9].attrib["contents"])
+                res_dict[node.name] = node_dict
+
+    return res_dict
diff --git a/src/finn/custom_op/fpgadataflow/__init__.py b/src/finn/custom_op/fpgadataflow/__init__.py
index 10f8b7feedf7584afb66a7fad8f1ee20745bf67d..4231be7c523a5a510de89fb1202dc7bbcf30d39f 100644
--- a/src/finn/custom_op/fpgadataflow/__init__.py
+++ b/src/finn/custom_op/fpgadataflow/__init__.py
@@ -73,15 +73,18 @@ class HLSCustomOp(CustomOp):
             "exec_mode": ("s", False, ""),
             "sim_cycles": ("i", False, 0),
             "rtlsim_trace": ("s", False, ""),
+            "res_estimate": ("s", False, ""),
+            "res_hls": ("s", False, ""),
+            "res_synth": ("s", False, ""),
         }
 
     def node_res_estimation(self):
         """Returns summarized resource estimation of BRAMs and LUTs
-        of the node."""
-        resources = []
-        resources.append("BRAMs: " + str(self.bram_estimation()))
-        resources.append("LUTs: " + str(self.lut_estimation()))
-        return resources
+        of the node as a dictionary."""
+        ret = dict()
+        ret["BRAM_18K"] = self.bram_estimation()
+        ret["LUT"] = self.lut_estimation()
+        return ret
 
     def bram_estimation(self):
         """Function for BRAM resource estimation, is member function of
@@ -99,6 +102,7 @@ class HLSCustomOp(CustomOp):
 
         # generate top cpp file for ip generation
         path = self.get_nodeattr("code_gen_dir_ipgen")
+        self.code_gen_dict["$AP_INT_MAX_W$"] = [str(self.get_ap_int_max_w())]
         self.generate_params(model, path)
         self.global_includes()
         self.defines("ipgen")
@@ -156,6 +160,7 @@ class HLSCustomOp(CustomOp):
         """Generates c++ code for simulation (npysim)."""
         node = self.onnx_node
         path = self.get_nodeattr("code_gen_dir_npysim")
+        self.code_gen_dict["$AP_INT_MAX_W$"] = [str(self.get_ap_int_max_w())]
         self.generate_params(model, path)
         self.global_includes()
         self.defines("npysim")
@@ -429,3 +434,8 @@ compilation transformations?
     def get_outstream_width(self):
         """Returns output stream width, if implemented."""
         raise Exception("get_outstream_width not implemented for this op")
+
+    def get_ap_int_max_w(self):
+        instream = self.get_instream_width()
+        outstream = self.get_outstream_width()
+        return max([instream, outstream])
diff --git a/src/finn/custom_op/fpgadataflow/convolutioninputgenerator.py b/src/finn/custom_op/fpgadataflow/convolutioninputgenerator.py
index e2a2f90b85a790a6d4fc7053d0e742329a7a1012..2ef5d350fb972e448b9a3745eb8c98197ab87d94 100644
--- a/src/finn/custom_op/fpgadataflow/convolutioninputgenerator.py
+++ b/src/finn/custom_op/fpgadataflow/convolutioninputgenerator.py
@@ -29,7 +29,11 @@
 import os
 
 import numpy as np
-from pyverilator import PyVerilator
+
+try:
+    from pyverilator import PyVerilator
+except ModuleNotFoundError:
+    PyVerilator = None
 
 from finn.core.datatype import DataType
 from finn.custom_op.fpgadataflow import HLSCustomOp
@@ -73,6 +77,10 @@ class ConvolutionInputGenerator(HLSCustomOp):
         ishape = (1, ifm_dim, ifm_dim, ifm_ch)
         return ishape
 
+    def get_folded_input_shape(self):
+        """Assumption: No folding on input"""
+        return self.get_normal_input_shape()
+
     def get_normal_output_shape(self):
         k = self.get_nodeattr("ConvKernelDim")
         ifm_dim = self.get_nodeattr("IFMDim")
@@ -124,12 +132,6 @@ class ConvolutionInputGenerator(HLSCustomOp):
     def verify_node(self):
         pass
 
-    def bram_estimation(self):
-        pass
-
-    def lut_estimation(self):
-        pass
-
     def get_input_datatype(self):
         """Returns FINN DataType of input."""
         return DataType[self.get_nodeattr("inputDataType")]
@@ -206,6 +208,9 @@ class ConvolutionInputGenerator(HLSCustomOp):
             did not produce expected ofolded utput shape"
             context[node.output[0]] = context[node.output[0]].reshape(*exp_oshape)
         elif mode == "rtlsim":
+            if PyVerilator is None:
+                raise ImportError("Installation of PyVerilator is required.")
+
             prefixed_top_name = "%s_%s" % (node.name, node.name)
             # check if needed file exists
             verilog_file = "{}/project_{}/sol1/impl/verilog/{}.v".format(
@@ -272,9 +277,10 @@ class ConvolutionInputGenerator(HLSCustomOp):
     def defines(self, var):
         numReps = 1
         self.code_gen_dict["$DEFINES$"] = [
-            """#define ConvKernelDim1 {}\n #define IFMChannels1 {}
-            #define Input_precision1 {}\n #define IFMDim1 {}\n #define OFMDim1 {}
-            #define SIMD1 {}\n #define Stride1 {}\n #define numReps {}""".format(
+            """#define ConvKernelDim1 {}\n #define IFMChannels1 {}\n
+            #define Input_precision1 {}\n #define IFMDim1 {}\n
+            #define OFMDim1 {}\n #define SIMD1 {}\n
+            #define Stride1 {}\n #define numReps {}""".format(
                 self.get_nodeattr("ConvKernelDim"),
                 self.get_nodeattr("IFMChannels"),
                 self.get_input_datatype().bitwidth(),
diff --git a/src/finn/custom_op/fpgadataflow/streamingdatawidthconverter_batch.py b/src/finn/custom_op/fpgadataflow/streamingdatawidthconverter_batch.py
index ce135e91088d2bfabe0259e1cc6873bb54884198..5e4c99aa41216b05f66da8341870269c620c6c40 100644
--- a/src/finn/custom_op/fpgadataflow/streamingdatawidthconverter_batch.py
+++ b/src/finn/custom_op/fpgadataflow/streamingdatawidthconverter_batch.py
@@ -28,7 +28,11 @@
 
 import os
 import numpy as np
-from pyverilator import PyVerilator
+
+try:
+    from pyverilator import PyVerilator
+except ModuleNotFoundError:
+    PyVerilator = None
 from finn.custom_op.fpgadataflow import HLSCustomOp
 from finn.core.datatype import DataType
 from onnx import TensorProto, helper
@@ -206,12 +210,6 @@ class StreamingDataWidthConverter_Batch(HLSCustomOp):
 
         return info_messages
 
-    def bram_estimation(self):
-        pass
-
-    def lut_estimation(self):
-        pass
-
     def global_includes(self):
         self.code_gen_dict["$GLOBALS$"] = ['#include "streamtools.h"']
 
@@ -353,6 +351,9 @@ class StreamingDataWidthConverter_Batch(HLSCustomOp):
             context[node.output[0]] = output
 
         elif mode == "rtlsim":
+            if PyVerilator is None:
+                raise ImportError("Installation of PyVerilator is required.")
+
             prefixed_top_name = "%s_%s" % (node.name, node.name)
             # check if needed file exists
             verilog_file = "{}/project_{}/sol1/impl/verilog/{}.v".format(
diff --git a/src/finn/custom_op/fpgadataflow/streamingfclayer_batch.py b/src/finn/custom_op/fpgadataflow/streamingfclayer_batch.py
index 00b8287a312fc82425b508ffef66f5187d074617..567a6cc984293c1db79657ce6ac8d186aa2fa1f3 100644
--- a/src/finn/custom_op/fpgadataflow/streamingfclayer_batch.py
+++ b/src/finn/custom_op/fpgadataflow/streamingfclayer_batch.py
@@ -32,7 +32,11 @@ import subprocess
 from shutil import copy
 
 import numpy as np
-from pyverilator import PyVerilator
+
+try:
+    from pyverilator import PyVerilator
+except ModuleNotFoundError:
+    PyVerilator = None
 from onnx import TensorProto, helper
 from finn.core.datatype import DataType
 from finn.custom_op.fpgadataflow import HLSCustomOp
@@ -270,6 +274,11 @@ class StreamingFCLayer_Batch(HLSCustomOp):
         wp = self.get_weight_datatype().bitwidth()
         return pe * simd * wp
 
+    def get_ap_int_max_w(self):
+        temp_value = super().get_ap_int_max_w()
+        weightstream = self.get_weightstream_width()
+        return max([weightstream, temp_value])
+
     def get_folded_input_shape(self):
         mw = self.get_nodeattr("MW")
         simd = self.get_nodeattr("SIMD")
@@ -631,6 +640,9 @@ class StreamingFCLayer_Batch(HLSCustomOp):
             oshape = self.get_normal_output_shape()
             context[node.output[0]] = context[node.output[0]].reshape(*oshape)
         elif mode == "rtlsim":
+            if PyVerilator is None:
+                raise ImportError("Installation of PyVerilator is required.")
+
             # set top name depending on mem_mode
             mem_mode = self.get_nodeattr("mem_mode")
             if mem_mode == "const":
@@ -714,9 +726,9 @@ class StreamingFCLayer_Batch(HLSCustomOp):
         numInputVectors = list(self.get_nodeattr("numInputVectors"))
         numReps = np.prod(numInputVectors)
         self.code_gen_dict["$DEFINES$"] = [
-            """#define MW1 {}\n #define MH1 {}\n #define SIMD1 {}\n
-            #define PE1 {}\n #define WMEM1 {}\n #define TMEM1 {}\n
-            #define numReps {}""".format(
+            """#define MW1 {}\n #define MH1 {}\n
+            #define SIMD1 {}\n #define PE1 {}\n #define WMEM1 {}\n
+            #define TMEM1 {}\n #define numReps {}""".format(
                 self.get_nodeattr("MW"),
                 self.get_nodeattr("MH"),
                 self.get_nodeattr("SIMD"),
diff --git a/src/finn/custom_op/fpgadataflow/streamingmaxpool_batch.py b/src/finn/custom_op/fpgadataflow/streamingmaxpool_batch.py
index 804da50f5a2c2de7c920975de4e082851a627c4e..a7c2d5166b6af41327abcfeaa5cb5ae25fd23856 100644
--- a/src/finn/custom_op/fpgadataflow/streamingmaxpool_batch.py
+++ b/src/finn/custom_op/fpgadataflow/streamingmaxpool_batch.py
@@ -28,7 +28,11 @@
 
 import os
 import numpy as np
-from pyverilator import PyVerilator
+
+try:
+    from pyverilator import PyVerilator
+except ModuleNotFoundError:
+    PyVerilator = None
 from finn.custom_op.fpgadataflow import HLSCustomOp
 from finn.custom_op.im2col import compute_conv_output_dim
 from finn.core.datatype import DataType
@@ -64,6 +68,9 @@ class StreamingMaxPool_Batch(HLSCustomOp):
         ishape = (1, ifm_dim, ifm_dim, ifm_ch)
         return ishape
 
+    def get_folded_input_shape(self):
+        return self.get_normal_input_shape()
+
     def get_normal_output_shape(self):
         k = self.get_nodeattr("PoolDim")
         ifm_dim = self.get_nodeattr("ImgDim")
@@ -143,12 +150,6 @@ class StreamingMaxPool_Batch(HLSCustomOp):
 
         return info_messages
 
-    def bram_estimation(self):
-        pass
-
-    def lut_estimation(self):
-        pass
-
     def global_includes(self):
         self.code_gen_dict["$GLOBALS$"] = ['#include "maxpool.h"']
 
@@ -301,6 +302,9 @@ class StreamingMaxPool_Batch(HLSCustomOp):
             did not produce expected ofolded utput shape"
             context[node.output[0]] = context[node.output[0]].reshape(*exp_oshape)
         elif mode == "rtlsim":
+            if PyVerilator is None:
+                raise ImportError("Installation of PyVerilator is required.")
+
             prefixed_top_name = "%s_%s" % (node.name, node.name)
             # check if needed file exists
             verilog_file = "{}/project_{}/sol1/impl/verilog/{}.v".format(
diff --git a/src/finn/custom_op/fpgadataflow/templates.py b/src/finn/custom_op/fpgadataflow/templates.py
index 98035a6b7927e5d7eb0ba24bd69b37ddb3b15f94..b869efbbc92ae45fd48085e4495fad3d178ff129 100644
--- a/src/finn/custom_op/fpgadataflow/templates.py
+++ b/src/finn/custom_op/fpgadataflow/templates.py
@@ -29,11 +29,12 @@
 
 # template for single node execution
 docompute_template = """
-#define AP_INT_MAX_W 16384
+#define AP_INT_MAX_W $AP_INT_MAX_W$
 #include "cnpy.h"
 #include "npy2apintstream.hpp"
 #include <vector>
 #include "bnn-library.h"
+
 // includes for network parameters
 $GLOBALS$
 
@@ -60,8 +61,10 @@ $SAVEASCNPY$
 
 # cpp file
 ipgen_template = """
-#define AP_INT_MAX_W 4096
+#define AP_INT_MAX_W $AP_INT_MAX_W$
+
 #include "bnn-library.h"
+
 // includes for network parameters
 $GLOBALS$
 
@@ -134,15 +137,15 @@ wire [31:0] config_q0;
 
 //multiple wire AXI Streams
 reg m_axis_0_afull = 0;
-reg m_axis_0_tready;
+wire m_axis_0_tready;
 wire m_axis_0_tvalid;
 wire $WEIGHT_RANGE$ m_axis_0_tdata;
 
-reg m_axis_0_tready_q;
+wire m_axis_0_tready_q;
 wire m_axis_0_tvalid_q;
 wire $WEIGHT_RANGE$ m_axis_0_tdata_q;
 
-reg m_axis_0_tready_q2;
+wire m_axis_0_tready_q2;
 wire m_axis_0_tvalid_q2;
 wire $WEIGHT_RANGE$ m_axis_0_tdata_q2;
 
diff --git a/src/finn/transformation/fpgadataflow/annotate_resources.py b/src/finn/transformation/fpgadataflow/annotate_resources.py
new file mode 100644
index 0000000000000000000000000000000000000000..d192372a7d9c1f6ee2f088c6a058b994d21f6c99
--- /dev/null
+++ b/src/finn/transformation/fpgadataflow/annotate_resources.py
@@ -0,0 +1,78 @@
+# Copyright (c) 2020, Xilinx
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# * Redistributions of source code must retain the above copyright notice, this
+#   list of conditions and the following disclaimer.
+#
+# * Redistributions in binary form must reproduce the above copyright notice,
+#   this list of conditions and the following disclaimer in the documentation
+#   and/or other materials provided with the distribution.
+#
+# * Neither the name of FINN nor the names of its
+#   contributors may be used to endorse or promote products derived from
+#   this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import finn.custom_op.registry as registry
+from finn.transformation import Transformation
+from finn.transformation.move_reshape import _is_fpgadataflow_node
+from finn.analysis.fpgadataflow.res_estimation import res_estimation
+from finn.analysis.fpgadataflow.hls_synth_res_estimation import hls_synth_res_estimation
+from finn.analysis.fpgadataflow.post_synth_res import post_synth_res
+
+
+class AnnotateResources(Transformation):
+    """Annotate the amount of FPGA resources taken by each fpgadataflow
+    node as an attribute on the node, depending on the mode parameter:
+    * 'estimate' -- use the analytical estimation model
+    * 'hls' -- use results from the HLS synthesis report
+
+    No annotations can be provided unless the relevant transformation for the
+    chosen mode (e.g. HLSSynth_IPGen for hls) was previously run.
+    """
+
+    def __init__(self, mode):
+        super().__init__()
+        self.mode = mode
+
+    def apply(self, model):
+        graph = model.graph
+        if self.mode == "estimate":
+            res_fxn = res_estimation
+        elif self.mode == "hls":
+            res_fxn = hls_synth_res_estimation
+        elif self.mode == "synth":
+            res_fxn = post_synth_res
+        else:
+            raise Exception("Unrecognized mode for AnnotateResources")
+        res_dict = model.analysis(res_fxn)
+        total_dict = {}
+        for lname in res_dict.keys():
+            layer_res_dict = res_dict[lname]
+            for r_type in layer_res_dict.keys():
+                r_amount = layer_res_dict[r_type]
+                r_amount = float(r_amount)
+                if r_type in total_dict.keys():
+                    total_dict[r_type] += r_amount
+                else:
+                    total_dict[r_type] = r_amount
+        model.set_metadata_prop("res_total_" + self.mode, str(total_dict))
+        for node in graph.node:
+            if _is_fpgadataflow_node(node) and node.name in res_dict.keys():
+                op_inst = registry.getCustomOp(node)
+                op_inst.set_nodeattr("res_" + self.mode, str(res_dict[node.name]))
+
+        return (model, False)
diff --git a/src/finn/transformation/fpgadataflow/codegen_ipgen.py b/src/finn/transformation/fpgadataflow/codegen_ipgen.py
index ab3b4e820e04ff1f0a02b6b95254b5fe8b45de91..45db9db04f17325cafe04aad1016580054daf554 100644
--- a/src/finn/transformation/fpgadataflow/codegen_ipgen.py
+++ b/src/finn/transformation/fpgadataflow/codegen_ipgen.py
@@ -46,7 +46,7 @@ def _codegen_single_node(node, model, fpgapart, clk):
         # ensure that there is a directory
         if code_gen_dir == "" or not os.path.isdir(code_gen_dir):
             code_gen_dir = make_build_dir(
-                prefix="code_gen_ipgen_" + str(node.op_type) + "_"
+                prefix="code_gen_ipgen_" + str(node.name) + "_"
             )
             inst.set_nodeattr("code_gen_dir_ipgen", code_gen_dir)
         # ensure that there is generated code inside the dir
diff --git a/src/finn/transformation/fpgadataflow/codegen_ipstitch.py b/src/finn/transformation/fpgadataflow/codegen_ipstitch.py
index 0fbd83199d88ec68cbf11c6ded5af33fdd4d91a3..f482db793018933883a068bb16fd99ece671064b 100644
--- a/src/finn/transformation/fpgadataflow/codegen_ipstitch.py
+++ b/src/finn/transformation/fpgadataflow/codegen_ipstitch.py
@@ -176,7 +176,8 @@ class CodeGen_ipstitch(Transformation):
         tcl.append("set all_v_files [get_files -filter {FILE_TYPE == Verilog}]")
         v_file_list = "%s/all_verilog_srcs.txt" % vivado_stitch_proj_dir
         tcl.append("set fp [open %s w]" % v_file_list)
-        tcl.append("puts $fp $all_v_files")
+        # write each verilog filename to all_verilog_srcs.txt
+        tcl.append("foreach vf $all_v_files {puts $fp $vf}")
         tcl.append("close $fp")
         # write the project creator tcl script
         tcl_string = "\n".join(tcl) + "\n"
diff --git a/src/finn/transformation/fpgadataflow/codegen_npysim.py b/src/finn/transformation/fpgadataflow/codegen_npysim.py
index d2862d82cf76f62bc236ace9d44c607dd2ab86ff..fe758ec28d67bd2f46edc864574d2edddfe6e3a3 100644
--- a/src/finn/transformation/fpgadataflow/codegen_npysim.py
+++ b/src/finn/transformation/fpgadataflow/codegen_npysim.py
@@ -46,7 +46,7 @@ def _codegen_single_node(node, model):
         # ensure that there is a directory
         if code_gen_dir == "" or not os.path.isdir(code_gen_dir):
             code_gen_dir = make_build_dir(
-                prefix="code_gen_npysim_" + str(node.op_type) + "_"
+                prefix="code_gen_npysim_" + str(node.name) + "_"
             )
             inst.set_nodeattr("code_gen_dir_npysim", code_gen_dir)
         # ensure that there is generated code inside the dir
diff --git a/src/finn/transformation/fpgadataflow/make_pynq_proj.py b/src/finn/transformation/fpgadataflow/make_pynq_proj.py
index c2c3802635ba8b1be9bf7f0c71e48ad13b79771f..9921ce7caf2aaffd197f9bc863ab77502a963647 100644
--- a/src/finn/transformation/fpgadataflow/make_pynq_proj.py
+++ b/src/finn/transformation/fpgadataflow/make_pynq_proj.py
@@ -113,11 +113,15 @@ class MakePYNQProject(Transformation):
         # create a temporary folder for the project
         vivado_pynq_proj_dir = make_build_dir(prefix="vivado_pynq_proj_")
         model.set_metadata_prop("vivado_pynq_proj", vivado_pynq_proj_dir)
+        # filename for the synth utilization report
+        synth_report_filename = vivado_pynq_proj_dir + "/synth_report.xml"
+        model.set_metadata_prop("vivado_synth_rpt", synth_report_filename)
 
         ip_config_tcl = templates.ip_config_tcl_template % (
             vivado_pynq_proj_dir,
             ip_dirs_str,
             vivado_pynq_proj_dir,
+            synth_report_filename,
             vivado_stitch_vlnv,
             in_bytes,
             out_bytes,
diff --git a/src/finn/transformation/fpgadataflow/templates.py b/src/finn/transformation/fpgadataflow/templates.py
index edbf28c4e9d49129d22da12985f3b8c003e3d745..81cb954bb4503c8daf18bad5881661018e9d17b7 100644
--- a/src/finn/transformation/fpgadataflow/templates.py
+++ b/src/finn/transformation/fpgadataflow/templates.py
@@ -38,6 +38,7 @@ variable config_ip_use_axilite
 variable config_ip_project_dir
 variable config_output_products_dir
 variable config_remote_cache
+variable config_util_report_filename
 
 # for arguments involving paths below: use absolute paths or relative to the
 # platform/overlay/bitstream folder
@@ -47,6 +48,8 @@ set config_ip_project_dir %s
 set config_ip_repo %s
 # where the produced bitfile and .hwh file will be placed
 set config_output_products_dir %s
+# where the synth util XML report will be written
+set config_util_report_filename %s
 
 # non-path arguments
 # VLNV of the IP block
diff --git a/src/finn/transformation/move_reshape.py b/src/finn/transformation/move_reshape.py
new file mode 100644
index 0000000000000000000000000000000000000000..6a30fd93cc0bdc322b6ec7d892d42d3c3ca96fd6
--- /dev/null
+++ b/src/finn/transformation/move_reshape.py
@@ -0,0 +1,40 @@
+from finn.transformation import Transformation
+from finn.util.basic import get_by_name
+
+
+def _is_fpgadataflow_node(node):
+    if node is not None:
+        if node.domain == "finn":
+            n_backend = get_by_name(node.attribute, "backend")
+            if n_backend is None:
+                return False
+            backend_value = n_backend.s.decode("UTF-8")
+            if backend_value == "fpgadataflow":
+                return True
+        else:
+            return False
+    else:
+        return False
+
+
+class MoveReshape(Transformation):
+    """Removes a node that implements a (1, -1) reshape if it is
+    between two fpgadataflow nodes"""
+
+    def apply(self, model):
+
+        graph = model.graph
+        graph_modified = False
+        for n in graph.node:
+            if n.op_type == "Reshape":
+                graph_modified = True
+                shape = model.get_initializer(n.input[1])
+                if (shape == [1, -1]).all():
+                    producer = model.find_producer(n.input[0])
+                    if _is_fpgadataflow_node(producer) is True:
+                        consumer = model.find_consumer(n.output[0])
+                        if _is_fpgadataflow_node(consumer) is True:
+                            consumer.input[0] = n.input[0]
+                            graph.node.remove(n)
+
+        return (model, graph_modified)
diff --git a/src/finn/util/fpgadataflow.py b/src/finn/util/fpgadataflow.py
index 29607b002bd7d7748d450c84f816606d18fded81..5b29ddbcd1dcc4f771dbc4eb633bf7c1ecb6b3aa 100644
--- a/src/finn/util/fpgadataflow.py
+++ b/src/finn/util/fpgadataflow.py
@@ -29,7 +29,11 @@
 import os
 import subprocess
 
-from pyverilator import PyVerilator
+try:
+    from pyverilator import PyVerilator
+except ModuleNotFoundError:
+    PyVerilator = None
+from finn.util.basic import get_by_name
 
 
 class IPGenBuilder:
@@ -69,6 +73,9 @@ class IPGenBuilder:
 
 def pyverilate_stitched_ip(model):
     "Given a model with stitched IP, return a PyVerilator sim object."
+    if PyVerilator is None:
+        raise ImportError("Installation of PyVerilator is required.")
+
     vivado_stitch_proj_dir = model.get_metadata_prop("vivado_stitch_proj")
     with open(vivado_stitch_proj_dir + "/all_verilog_srcs.txt", "r") as f:
         all_verilog_srcs = f.read().split()
@@ -87,3 +94,16 @@ def pyverilate_get_liveness_threshold_cycles():
     the simulation is not finishing and throwing an exception."""
 
     return int(os.getenv("LIVENESS_THRESHOLD", 10000))
+
+
+def is_fpgadataflow_node(node):
+    is_node = False
+    if node is not None:
+        if node.domain == "finn":
+            n_backend = get_by_name(node.attribute, "backend")
+            if n_backend is not None:
+                backend_value = n_backend.s.decode("UTF-8")
+                if backend_value == "fpgadataflow":
+                    is_node = True
+
+    return is_node
diff --git a/tests/end2end/test_end2end_cnv_w1a1.py b/tests/end2end/test_end2end_cnv_w1a1.py
new file mode 100644
index 0000000000000000000000000000000000000000..1a59191a085616d08d0910b28a9e62cb6596b7c4
--- /dev/null
+++ b/tests/end2end/test_end2end_cnv_w1a1.py
@@ -0,0 +1,329 @@
+# Copyright (c) 2020, Xilinx
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# * Redistributions of source code must retain the above copyright notice, this
+#   list of conditions and the following disclaimer.
+#
+# * Redistributions in binary form must reproduce the above copyright notice,
+#   this list of conditions and the following disclaimer in the documentation
+#   and/or other materials provided with the distribution.
+#
+# * Neither the name of FINN nor the names of its
+#   contributors may be used to endorse or promote products derived from
+#   this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import os
+
+import numpy as np
+
+# as of Feb'20 there is a bug that segfaults ONNX shape inference if we
+# import pytorch before onnx, so we make sure to import onnx first
+import onnx  # NOQA
+
+import pytest
+import pkg_resources as pk
+from finn.core.modelwrapper import ModelWrapper
+from finn.custom_op.registry import getCustomOp
+from finn.core.onnx_exec import execute_onnx
+from finn.transformation.double_to_single_float import DoubleToSingleFloat
+from finn.transformation.infer_shapes import InferShapes
+from finn.transformation.move_reshape import MoveReshape
+from finn.transformation.fold_constants import FoldConstants
+from finn.transformation.general import GiveReadableTensorNames, GiveUniqueNodeNames
+from finn.transformation.streamline import Streamline
+from finn.transformation.lower_convs_to_matmul import LowerConvsToMatMul
+from finn.transformation.bipolar_to_xnor import ConvertBipolarMatMulToXnorPopcount
+import finn.transformation.streamline.absorb as absorb
+from finn.transformation.streamline.reorder import MakeMaxPoolNHWC
+import finn.transformation.fpgadataflow.convert_to_hls_layers as to_hls
+from finn.transformation.fpgadataflow.create_dataflow_partition import (
+    CreateDataflowPartition,
+)
+from finn.transformation.fpgadataflow.insert_dwc import InsertDWC
+from finn.transformation.fpgadataflow.insert_tlastmarker import InsertTLastMarker
+from finn.transformation.fpgadataflow.codegen_ipgen import CodeGen_ipgen
+from finn.transformation.fpgadataflow.hlssynth_ipgen import HLSSynth_IPGen
+from finn.transformation.fpgadataflow.replace_verilog_relpaths import (
+    ReplaceVerilogRelPaths,
+)
+from finn.transformation.fpgadataflow.codegen_ipstitch import CodeGen_ipstitch
+from finn.transformation.fpgadataflow.set_exec_mode import SetExecMode
+from finn.transformation.fpgadataflow.codegen_npysim import CodeGen_npysim
+from finn.transformation.fpgadataflow.compile import Compile
+from finn.transformation.fpgadataflow.make_pynq_driver import MakePYNQDriver
+from finn.transformation.fpgadataflow.make_pynq_proj import MakePYNQProject
+from finn.transformation.fpgadataflow.synth_pynq_proj import SynthPYNQProject
+from finn.transformation.fpgadataflow.make_deployment import DeployToPYNQ
+from finn.util.basic import pynq_part_map
+from finn.util.test import get_test_model_trained
+from finn.transformation.fpgadataflow.annotate_resources import AnnotateResources
+
+
+build_dir = "/tmp/" + os.environ["FINN_INST_NAME"]
+test_pynq_board = os.getenv("PYNQ_BOARD", default="Pynq-Z1")
+test_fpga_part = pynq_part_map[test_pynq_board]
+target_clk_ns = 5
+mem_mode = "const"
+
+
+def test_end2end_cnv_w1a1_export():
+    import brevitas.onnx as bo
+
+    cnv = get_test_model_trained("CNV", 1, 1)
+    bo.export_finn_onnx(
+        cnv, (1, 3, 32, 32), build_dir + "/end2end_cnv_w1a1_export.onnx"
+    )
+
+
+def test_end2end_cnv_w1a1_import_and_tidy():
+    model = ModelWrapper(build_dir + "/end2end_cnv_w1a1_export.onnx")
+    model = model.transform(DoubleToSingleFloat())
+    model = model.transform(InferShapes())
+    model = model.transform(FoldConstants())
+    model = model.transform(GiveUniqueNodeNames())
+    model = model.transform(GiveReadableTensorNames())
+    model.save(build_dir + "/end2end_cnv_w1a1_tidy.onnx")
+
+
+def test_end2end_cnv_w1a1_streamline():
+    model = ModelWrapper(build_dir + "/end2end_cnv_w1a1_tidy.onnx")
+    model = model.transform(Streamline())
+    model = model.transform(LowerConvsToMatMul())
+    model = model.transform(MakeMaxPoolNHWC())
+    model = model.transform(absorb.AbsorbTransposeIntoMultiThreshold())
+    model = model.transform(ConvertBipolarMatMulToXnorPopcount())
+    model = model.transform(Streamline())
+    model.save(build_dir + "/end2end_cnv_w1a1_streamlined.onnx")
+
+
+def test_end2end_cnv_w1a1_convert_to_hls_layers():
+    model = ModelWrapper(build_dir + "/end2end_cnv_w1a1_streamlined.onnx")
+    model = model.transform(to_hls.InferBinaryStreamingFCLayer())
+    model = model.transform(to_hls.InferQuantizedStreamingFCLayer())
+    model = model.transform(to_hls.InferConvInpGen())
+    model = model.transform(to_hls.InferStreamingMaxPool())
+    model = model.transform(MoveReshape())
+    model.save(build_dir + "/end2end_cnv_w1a1_hls_layers.onnx")
+
+
+def test_end2end_cnv_w1a1_create_dataflow_partition():
+    model = ModelWrapper(build_dir + "/end2end_cnv_w1a1_hls_layers.onnx")
+    parent_model = model.transform(CreateDataflowPartition())
+    parent_model.save(build_dir + "/end2end_cnv_w1a1_dataflow_parent.onnx")
+    sdp_node = parent_model.get_nodes_by_op_type("StreamingDataflowPartition")[0]
+    sdp_node = getCustomOp(sdp_node)
+    dataflow_model_filename = sdp_node.get_nodeattr("model")
+    dataflow_model = ModelWrapper(dataflow_model_filename)
+    dataflow_model.save(build_dir + "/end2end_cnv_w1a1_dataflow_model.onnx")
+
+
+def test_end2end_cnv_w1a1_fold_and_tlastmarker():
+    model = ModelWrapper(build_dir + "/end2end_cnv_w1a1_dataflow_model.onnx")
+    fc_layers = model.get_nodes_by_op_type("StreamingFCLayer_Batch")
+    fc0w = getCustomOp(fc_layers[0])
+    fc1w = getCustomOp(fc_layers[1])
+    fc2w = getCustomOp(fc_layers[2])
+    fc3w = getCustomOp(fc_layers[3])
+    fc4w = getCustomOp(fc_layers[4])
+    fc5w = getCustomOp(fc_layers[5])
+    fc6w = getCustomOp(fc_layers[6])
+    fc7w = getCustomOp(fc_layers[7])
+    fc8w = getCustomOp(fc_layers[8])
+    fc0w.set_nodeattr("SIMD", 27)
+    fc0w.set_nodeattr("PE", 8)
+    fc1w.set_nodeattr("SIMD", 32)
+    fc1w.set_nodeattr("PE", 8)
+    fc2w.set_nodeattr("SIMD", 32)
+    fc2w.set_nodeattr("PE", 16)
+    fc3w.set_nodeattr("SIMD", 32)
+    fc3w.set_nodeattr("PE", 16)
+    fc4w.set_nodeattr("SIMD", 32)
+    fc4w.set_nodeattr("PE", 32)
+    fc5w.set_nodeattr("SIMD", 64)
+    fc5w.set_nodeattr("PE", 16)
+    fc6w.set_nodeattr("SIMD", 32)
+    fc6w.set_nodeattr("PE", 16)
+    fc7w.set_nodeattr("SIMD", 64)
+    fc7w.set_nodeattr("PE", 8)
+    fc8w.set_nodeattr("SIMD", 16)
+    fc8w.set_nodeattr("PE", 10)
+
+    model = model.transform(InsertDWC())
+    model = model.transform(InsertTLastMarker())
+    model = model.transform(GiveUniqueNodeNames())
+    model = model.transform(AnnotateResources("estimate"))
+    model.save(build_dir + "/end2end_cnv_w1a1_folded.onnx")
+
+
+def test_end2end_cnv_w1a1_gen_hls_ip():
+    model = ModelWrapper(build_dir + "/end2end_cnv_w1a1_folded.onnx")
+    model = model.transform(CodeGen_ipgen(test_fpga_part, target_clk_ns))
+    model = model.transform(HLSSynth_IPGen())
+    model = model.transform(AnnotateResources("hls"))
+    model.save(build_dir + "/end2end_cnv_w1a1_ipgen.onnx")
+
+
+def test_end2end_cnv_w1a1_ip_stitch():
+    model = ModelWrapper(build_dir + "/end2end_cnv_w1a1_ipgen.onnx")
+    model = model.transform(ReplaceVerilogRelPaths())
+    model = model.transform(CodeGen_ipstitch(test_fpga_part))
+    model.save(build_dir + "/end2end_cnv_w1a1_ipstitch.onnx")
+
+
+def test_end2end_cnv_w1a1_verify_dataflow_part():
+    model = ModelWrapper(build_dir + "/end2end_cnv_w1a1_ipstitch.onnx")
+    x = np.zeros((1, 32, 32, 3), dtype=np.float32)
+    inp_name = model.graph.input[0].name
+    out_name = model.graph.output[0].name
+    inp_dict = {inp_name: x}
+    # npysim
+    model = model.transform(CodeGen_npysim())
+    model = model.transform(Compile())
+    model = model.transform(SetExecMode("npysim"))
+    model.save(build_dir + "/end2end_cnv_w1a1_ipgen_npysim.onnx")
+    ret_npysim = execute_onnx(model, inp_dict, True)
+    res_npysim = ret_npysim[out_name]
+    # node-by-node rtlsim
+    model = model.transform(SetExecMode("rtlsim"))
+    fc_layers = model.get_nodes_by_op_type("StreamingFCLayer_Batch")
+    for fcl in fc_layers:
+        getCustomOp(fcl).set_nodeattr("rtlsim_trace", "default")
+    model.save(build_dir + "/end2end_cnv_w1a1_ipgen_nodebynode_rtlsim.onnx")
+    ret_rtlsim_nodebynode = execute_onnx(model, inp_dict, True)
+    res_rtlsim_nodebynode = ret_rtlsim_nodebynode[out_name]
+    # whole-network (ip-stitched) rtlsim
+    model.set_metadata_prop("exec_mode", "rtlsim")
+    model.set_metadata_prop("rtlsim_trace", "whole_trace.vcd")
+    model.save(build_dir + "/end2end_cnv_w1a1_ipstitch_whole_rtlsim.onnx")
+    # this is a particularly long-running test, set liveness thr. to unlimited
+    os.environ["LIVENESS_THRESHOLD"] = "-1"
+    ret_rtlsim_whole = execute_onnx(model, inp_dict, True)
+    res_rtlsim_whole = ret_rtlsim_whole[out_name]
+    assert np.isclose(res_npysim, res_rtlsim_nodebynode).all()
+    assert np.isclose(res_npysim, res_rtlsim_whole).all()
+
+
+def test_end2end_cnv_w1a1_verify_all():
+    # use the streamlined model as the "golden" model for right answers
+    golden = ModelWrapper(build_dir + "/end2end_cnv_w1a1_streamlined.onnx")
+    iname = golden.graph.input[0].name
+    oname = golden.graph.output[0].name
+    # load one of the test vectors
+    fn = pk.resource_filename("finn", "data/cifar10/cifar10-test-data-class3.npz")
+    input_tensor = np.load(fn)["arr_0"].astype(np.float32)
+    assert input_tensor.shape == (1, 3, 32, 32)
+    x = input_tensor
+    # x = np.zeros(ishape, dtype=np.float32)
+    ret_golden = execute_onnx(golden, {iname: x}, True)
+    y_golden = ret_golden[oname]
+    # set up parent+child graph to test
+    # we'll use models from the previous step as the child model
+    parent_model = ModelWrapper(build_dir + "/end2end_cnv_w1a1_dataflow_parent.onnx")
+    iname = parent_model.graph.input[0].name
+    oname = parent_model.graph.output[0].name
+    # produce results with npysim
+    sdp_node = parent_model.get_nodes_by_op_type("StreamingDataflowPartition")[0]
+    sdp_node = getCustomOp(sdp_node)
+    sdp_node.set_nodeattr("model", build_dir + "/end2end_cnv_w1a1_ipgen_npysim.onnx")
+    ret_npysim = execute_onnx(parent_model, {iname: x}, True)
+    y_npysim = ret_npysim[oname]
+    # produce results with node-by-node rtlsim
+    sdp_node.set_nodeattr(
+        "model", build_dir + "/end2end_cnv_w1a1_ipgen_nodebynode_rtlsim.onnx"
+    )
+    ret_nodebynode_rtlsim = execute_onnx(parent_model, {iname: x}, True)
+    y_nodebynode_rtlsim = ret_nodebynode_rtlsim[oname]
+    # produce results with whole-network (stitched ip) rtlsim
+    sdp_node.set_nodeattr(
+        "model", build_dir + "/end2end_cnv_w1a1_ipstitch_whole_rtlsim.onnx"
+    )
+    # this is a particularly long-running test, set liveness thr. to unlimited
+    os.environ["LIVENESS_THRESHOLD"] = "-1"
+    ret_whole_rtlsim = execute_onnx(parent_model, {iname: x}, True)
+    y_whole_rtlsim = ret_whole_rtlsim[oname]
+    assert np.isclose(y_golden, y_npysim).all()
+    assert np.isclose(y_golden, y_nodebynode_rtlsim).all()
+    assert np.isclose(y_golden, y_whole_rtlsim).all()
+
+
+def test_end2end_cnv_w1a1_make_pynq_proj():
+    model = ModelWrapper(build_dir + "/end2end_cnv_w1a1_ipstitch.onnx")
+    model = model.transform(MakePYNQProject(test_pynq_board))
+    model.save(build_dir + "/end2end_cnv_w1a1_pynq_project.onnx")
+
+
+def test_end2end_cnv_w1a1_synth_pynq_project():
+    model = ModelWrapper(build_dir + "/end2end_cnv_w1a1_pynq_project.onnx")
+    model = model.transform(SynthPYNQProject())
+    model = model.transform(AnnotateResources("synth"))
+    model.save(build_dir + "/end2end_cnv_w1a1_synth.onnx")
+
+
+def test_end2end_cnv_w1a1_make_driver():
+    model = ModelWrapper(build_dir + "/end2end_cnv_w1a1_synth.onnx")
+    model = model.transform(MakePYNQDriver())
+    model.save(build_dir + "/end2end_cnv_w1a1_pynq_driver.onnx")
+
+
+def test_end2end_cnv_w1a1_deploy_on_pynq():
+    model = ModelWrapper(build_dir + "/end2end_cnv_w1a1_pynq_driver.onnx")
+    try:
+        ip = os.environ["PYNQ_IP"]  # no fault for this one; skip if not defined
+        if ip == "":
+            pytest.skip("PYNQ board IP address not specified")
+        username = os.getenv("PYNQ_USERNAME", "xilinx")
+        password = os.getenv("PYNQ_PASSWORD", "xilinx")
+        target_dir = os.getenv("PYNQ_TARGET_DIR", "/home/xilinx/finn")
+        model = model.transform(DeployToPYNQ(ip, username, password, target_dir))
+        # save the model to be able to link it to the parent
+        model.save(build_dir + "/end2end_cnv_w1a1_pynq_deploy.onnx")
+    except KeyError:
+        pytest.skip("PYNQ board IP address not specified")
+
+
+def test_end2end_cnv_w1a1_run_on_pynq():
+    # use the streamlined model as the "golden" model for right answers
+    golden = ModelWrapper(build_dir + "/end2end_cnv_w1a1_streamlined.onnx")
+    iname = golden.graph.input[0].name
+    oname = golden.graph.output[0].name
+    # load one of the test vectors
+    fn = pk.resource_filename("finn", "data/cifar10/cifar10-test-data-class3.npz")
+    input_tensor = np.load(fn)["arr_0"].astype(np.float32)
+    assert input_tensor.shape == (1, 3, 32, 32)
+    x = input_tensor
+    # run using FINN-based execution
+    ret_golden = execute_onnx(golden, {iname: x}, True)
+    y_golden = ret_golden[oname]
+    # set up parent+child graph to test
+    # we'll use models from the previous step as the child model
+    parent_model = ModelWrapper(build_dir + "/end2end_cnv_w1a1_dataflow_parent.onnx")
+    iname = parent_model.graph.input[0].name
+    oname = parent_model.graph.output[0].name
+    try:
+        ip = os.environ["PYNQ_IP"]  # NOQA
+        if ip == "":
+            pytest.skip("PYNQ board IP address not specified")
+        # produce results with npysim
+        sdp_node = parent_model.get_nodes_by_op_type("StreamingDataflowPartition")[0]
+        sdp_node = getCustomOp(sdp_node)
+        sdp_node.set_nodeattr("model", build_dir + "/end2end_cnv_w1a1_pynq_deploy.onnx")
+        ret = execute_onnx(parent_model, {iname: x}, True)
+        y = ret[oname]
+        assert np.isclose(y, y_golden).all()
+
+    except KeyError:
+        pytest.skip("PYNQ board IP address not specified")
diff --git a/tests/end2end/test_end2end_tfc_w1a1.py b/tests/end2end/test_end2end_tfc_w1a1.py
index 9cd338caa69913dbcd1a1b66758fd633b94260ad..03d6f92f1c148ce444f08fd65a867ad9390a18fd 100644
--- a/tests/end2end/test_end2end_tfc_w1a1.py
+++ b/tests/end2end/test_end2end_tfc_w1a1.py
@@ -70,6 +70,7 @@ from finn.transformation.streamline import Streamline
 from finn.transformation.streamline.round_thresholds import RoundAndClipThresholds
 from finn.util.basic import pynq_part_map
 from finn.util.test import get_test_model_trained
+from finn.transformation.fpgadataflow.annotate_resources import AnnotateResources
 
 build_dir = "/tmp/" + os.environ["FINN_INST_NAME"]
 test_pynq_board = os.getenv("PYNQ_BOARD", default="Pynq-Z1")
@@ -117,7 +118,8 @@ def test_end2end_tfc_w1a1_create_dataflow_partition():
     model = ModelWrapper(build_dir + "/end2end_tfc_w1a1_hls_layers.onnx")
     parent_model = model.transform(CreateDataflowPartition())
     parent_model.save(build_dir + "/end2end_tfc_w1a1_dataflow_parent.onnx")
-    sdp_node = getCustomOp(parent_model.graph.node[2])
+    sdp_node = parent_model.get_nodes_by_op_type("StreamingDataflowPartition")[0]
+    sdp_node = getCustomOp(sdp_node)
     dataflow_model_filename = sdp_node.get_nodeattr("model")
     dataflow_model = ModelWrapper(dataflow_model_filename)
     dataflow_model.save(build_dir + "/end2end_tfc_w1a1_dataflow_model.onnx")
@@ -125,14 +127,11 @@ def test_end2end_tfc_w1a1_create_dataflow_partition():
 
 def test_end2end_tfc_w1a1_fold_and_tlastmarker():
     model = ModelWrapper(build_dir + "/end2end_tfc_w1a1_dataflow_model.onnx")
-    fc0 = model.graph.node[0]
-    fc1 = model.graph.node[1]
-    fc2 = model.graph.node[2]
-    fc3 = model.graph.node[3]
-    fc0w = getCustomOp(fc0)
-    fc1w = getCustomOp(fc1)
-    fc2w = getCustomOp(fc2)
-    fc3w = getCustomOp(fc3)
+    fc_layers = model.get_nodes_by_op_type("StreamingFCLayer_Batch")
+    fc0w = getCustomOp(fc_layers[0])
+    fc1w = getCustomOp(fc_layers[1])
+    fc2w = getCustomOp(fc_layers[2])
+    fc3w = getCustomOp(fc_layers[3])
     fc0w.set_nodeattr("inFIFODepth", 50)
     fc0w.set_nodeattr("SIMD", 16)
     fc0w.set_nodeattr("PE", 16)
@@ -148,14 +147,16 @@ def test_end2end_tfc_w1a1_fold_and_tlastmarker():
     fc3w.set_nodeattr("outFIFODepth", 50)
     model = model.transform(InsertDWC())
     model = model.transform(InsertTLastMarker())
+    model = model.transform(GiveUniqueNodeNames())
+    model = model.transform(AnnotateResources("estimate"))
     model.save(build_dir + "/end2end_tfc_w1a1_folded.onnx")
 
 
 def test_end2end_tfc_w1a1_gen_hls_ip():
     model = ModelWrapper(build_dir + "/end2end_tfc_w1a1_folded.onnx")
-    model = model.transform(GiveUniqueNodeNames())
     model = model.transform(CodeGen_ipgen(test_fpga_part, target_clk_ns))
     model = model.transform(HLSSynth_IPGen())
+    model = model.transform(AnnotateResources("hls"))
     model.save(build_dir + "/end2end_tfc_w1a1_ipgen.onnx")
 
 
@@ -181,10 +182,9 @@ def test_end2end_tfc_w1a1_verify_dataflow_part():
     res_npysim = ret_npysim[out_name]
     # node-by-node rtlsim
     model = model.transform(SetExecMode("rtlsim"))
-    getCustomOp(model.graph.node[0]).set_nodeattr("rtlsim_trace", "default")
-    getCustomOp(model.graph.node[1]).set_nodeattr("rtlsim_trace", "default")
-    getCustomOp(model.graph.node[2]).set_nodeattr("rtlsim_trace", "default")
-    getCustomOp(model.graph.node[3]).set_nodeattr("rtlsim_trace", "default")
+    fc_layers = model.get_nodes_by_op_type("StreamingFCLayer_Batch")
+    for fcl in fc_layers:
+        getCustomOp(fcl).set_nodeattr("rtlsim_trace", "default")
     model.save(build_dir + "/end2end_tfc_w1a1_ipstitch_nodebynode_rtlsim.onnx")
     ret_rtlsim_nodebynode = execute_onnx(model, inp_dict, True)
     res_rtlsim_nodebynode = ret_rtlsim_nodebynode[out_name]
@@ -215,7 +215,8 @@ def test_end2end_tfc_w1a1_verify_all():
     iname = parent_model.graph.input[0].name
     oname = parent_model.graph.output[0].name
     # produce results with npysim
-    sdp_node = getCustomOp(parent_model.graph.node[2])
+    sdp_node = parent_model.get_nodes_by_op_type("StreamingDataflowPartition")[0]
+    sdp_node = getCustomOp(sdp_node)
     sdp_node.set_nodeattr("model", build_dir + "/end2end_tfc_w1a1_ipstitch_npysim.onnx")
     ret_npysim = execute_onnx(parent_model, {iname: x}, True)
     y_npysim = ret_npysim[oname]
@@ -245,6 +246,7 @@ def test_end2end_tfc_w1a1_make_pynq_proj():
 def test_end2end_tfc_w1a1_synth_pynq_project():
     model = ModelWrapper(build_dir + "/end2end_tfc_w1a1_pynq_project.onnx")
     model = model.transform(SynthPYNQProject())
+    model = model.transform(AnnotateResources("synth"))
     model.save(build_dir + "/end2end_tfc_w1a1_synth.onnx")
 
 
@@ -292,7 +294,8 @@ def test_end2end_tfc_w1a1_run_on_pynq():
         if ip == "":
             pytest.skip("PYNQ board IP address not specified")
         # produce results with npysim
-        sdp_node = getCustomOp(parent_model.graph.node[2])
+        sdp_node = parent_model.get_nodes_by_op_type("StreamingDataflowPartition")[0]
+        sdp_node = getCustomOp(sdp_node)
         sdp_node.set_nodeattr("model", build_dir + "/end2end_tfc_w1a1_pynq_deploy.onnx")
         ret = execute_onnx(parent_model, {iname: x}, True)
         y = ret[oname]
diff --git a/tests/end2end/test_end2end_tfc_w1a2.py b/tests/end2end/test_end2end_tfc_w1a2.py
index e3eead8454e901671ae27d62a3b1999c59f176a8..7fef331b99a78b43f8e808c8cdf978a5c8233f92 100644
--- a/tests/end2end/test_end2end_tfc_w1a2.py
+++ b/tests/end2end/test_end2end_tfc_w1a2.py
@@ -66,6 +66,7 @@ from finn.transformation.infer_shapes import InferShapes
 from finn.transformation.streamline import Streamline
 from finn.util.basic import pynq_part_map
 from finn.util.test import get_test_model_trained
+from finn.transformation.fpgadataflow.annotate_resources import AnnotateResources
 
 build_dir = "/tmp/" + os.environ["FINN_INST_NAME"]
 test_pynq_board = os.getenv("PYNQ_BOARD", default="Pynq-Z1")
@@ -109,7 +110,8 @@ def test_end2end_tfc_w1a2_create_dataflow_partition():
     model = ModelWrapper(build_dir + "/end2end_tfc_w1a2_hls_layers.onnx")
     parent_model = model.transform(CreateDataflowPartition())
     parent_model.save(build_dir + "/end2end_tfc_w1a2_dataflow_parent.onnx")
-    sdp_node = getCustomOp(parent_model.graph.node[2])
+    sdp_node = parent_model.get_nodes_by_op_type("StreamingDataflowPartition")[0]
+    sdp_node = getCustomOp(sdp_node)
     dataflow_model_filename = sdp_node.get_nodeattr("model")
     dataflow_model = ModelWrapper(dataflow_model_filename)
     dataflow_model.save(build_dir + "/end2end_tfc_w1a2_dataflow_model.onnx")
@@ -117,14 +119,11 @@ def test_end2end_tfc_w1a2_create_dataflow_partition():
 
 def test_end2end_tfc_w1a2_fold_and_tlastmarker():
     model = ModelWrapper(build_dir + "/end2end_tfc_w1a2_dataflow_model.onnx")
-    fc0 = model.graph.node[0]
-    fc1 = model.graph.node[1]
-    fc2 = model.graph.node[2]
-    fc3 = model.graph.node[3]
-    fc0w = getCustomOp(fc0)
-    fc1w = getCustomOp(fc1)
-    fc2w = getCustomOp(fc2)
-    fc3w = getCustomOp(fc3)
+    fc_layers = model.get_nodes_by_op_type("StreamingFCLayer_Batch")
+    fc0w = getCustomOp(fc_layers[0])
+    fc1w = getCustomOp(fc_layers[1])
+    fc2w = getCustomOp(fc_layers[2])
+    fc3w = getCustomOp(fc_layers[3])
     fc0w.set_nodeattr("inFIFODepth", 50)
     fc0w.set_nodeattr("SIMD", 8)
     fc0w.set_nodeattr("PE", 16)
@@ -139,14 +138,16 @@ def test_end2end_tfc_w1a2_fold_and_tlastmarker():
     fc3w.set_nodeattr("PE", 10)
     fc3w.set_nodeattr("outFIFODepth", 50)
     model = model.transform(InsertTLastMarker())
+    model = model.transform(GiveUniqueNodeNames())
+    model = model.transform(AnnotateResources("estimate"))
     model.save(build_dir + "/end2end_tfc_w1a2_folded.onnx")
 
 
 def test_end2end_tfc_w1a2_gen_hls_ip():
     model = ModelWrapper(build_dir + "/end2end_tfc_w1a2_folded.onnx")
-    model = model.transform(GiveUniqueNodeNames())
     model = model.transform(CodeGen_ipgen(test_fpga_part, target_clk_ns))
     model = model.transform(HLSSynth_IPGen())
+    model = model.transform(AnnotateResources("hls"))
     model.save(build_dir + "/end2end_tfc_w1a2_ipgen.onnx")
 
 
@@ -172,10 +173,9 @@ def test_end2end_tfc_w1a2_verify_dataflow_part():
     res_npysim = ret_npysim[out_name]
     # node-by-node rtlsim
     model = model.transform(SetExecMode("rtlsim"))
-    getCustomOp(model.graph.node[0]).set_nodeattr("rtlsim_trace", "default")
-    getCustomOp(model.graph.node[1]).set_nodeattr("rtlsim_trace", "default")
-    getCustomOp(model.graph.node[2]).set_nodeattr("rtlsim_trace", "default")
-    getCustomOp(model.graph.node[3]).set_nodeattr("rtlsim_trace", "default")
+    fc_layers = model.get_nodes_by_op_type("StreamingFCLayer_Batch")
+    for fcl in fc_layers:
+        getCustomOp(fcl).set_nodeattr("rtlsim_trace", "default")
     model.save(build_dir + "/end2end_tfc_w1a2_ipstitch_nodebynode_rtlsim.onnx")
     ret_rtlsim_nodebynode = execute_onnx(model, inp_dict, True)
     res_rtlsim_nodebynode = ret_rtlsim_nodebynode[out_name]
@@ -206,7 +206,8 @@ def test_end2end_tfc_w1a2_verify_all():
     iname = parent_model.graph.input[0].name
     oname = parent_model.graph.output[0].name
     # produce results with npysim
-    sdp_node = getCustomOp(parent_model.graph.node[2])
+    sdp_node = parent_model.get_nodes_by_op_type("StreamingDataflowPartition")[0]
+    sdp_node = getCustomOp(sdp_node)
     sdp_node.set_nodeattr("model", build_dir + "/end2end_tfc_w1a2_ipstitch_npysim.onnx")
     ret_npysim = execute_onnx(parent_model, {iname: x}, True)
     y_npysim = ret_npysim[oname]
@@ -236,6 +237,7 @@ def test_end2end_tfc_w1a2_make_pynq_proj():
 def test_end2end_tfc_w1a2_synth_pynq_project():
     model = ModelWrapper(build_dir + "/end2end_tfc_w1a2_pynq_project.onnx")
     model = model.transform(SynthPYNQProject())
+    model = model.transform(AnnotateResources("synth"))
     model.save(build_dir + "/end2end_tfc_w1a2_synth.onnx")
 
 
@@ -283,7 +285,8 @@ def test_end2end_tfc_w1a2_run_on_pynq():
         if ip == "":
             pytest.skip("PYNQ board IP address not specified")
         # produce results with npysim
-        sdp_node = getCustomOp(parent_model.graph.node[2])
+        sdp_node = parent_model.get_nodes_by_op_type("StreamingDataflowPartition")[0]
+        sdp_node = getCustomOp(sdp_node)
         sdp_node.set_nodeattr("model", build_dir + "/end2end_tfc_w1a2_pynq_deploy.onnx")
         ret = execute_onnx(parent_model, {iname: x}, True)
         y = ret[oname]
diff --git a/tests/end2end/test_end2end_tfc_w2a2.py b/tests/end2end/test_end2end_tfc_w2a2.py
index 84133bb6c3c32a81190ce0f8b7b4b5d3de64d079..c78be7b66fe2c2f84e6f9a1a520c3e22e769c82f 100644
--- a/tests/end2end/test_end2end_tfc_w2a2.py
+++ b/tests/end2end/test_end2end_tfc_w2a2.py
@@ -66,6 +66,7 @@ from finn.transformation.infer_shapes import InferShapes
 from finn.transformation.streamline import Streamline
 from finn.util.basic import pynq_part_map
 from finn.util.test import get_test_model_trained
+from finn.transformation.fpgadataflow.annotate_resources import AnnotateResources
 
 build_dir = "/tmp/" + os.environ["FINN_INST_NAME"]
 test_pynq_board = os.getenv("PYNQ_BOARD", default="Pynq-Z1")
@@ -109,7 +110,8 @@ def test_end2end_tfc_w2a2_create_dataflow_partition():
     model = ModelWrapper(build_dir + "/end2end_tfc_w2a2_hls_layers.onnx")
     parent_model = model.transform(CreateDataflowPartition())
     parent_model.save(build_dir + "/end2end_tfc_w2a2_dataflow_parent.onnx")
-    sdp_node = getCustomOp(parent_model.graph.node[2])
+    sdp_node = parent_model.get_nodes_by_op_type("StreamingDataflowPartition")[0]
+    sdp_node = getCustomOp(sdp_node)
     dataflow_model_filename = sdp_node.get_nodeattr("model")
     dataflow_model = ModelWrapper(dataflow_model_filename)
     dataflow_model.save(build_dir + "/end2end_tfc_w2a2_dataflow_model.onnx")
@@ -117,14 +119,11 @@ def test_end2end_tfc_w2a2_create_dataflow_partition():
 
 def test_end2end_tfc_w2a2_fold_and_tlastmarker():
     model = ModelWrapper(build_dir + "/end2end_tfc_w2a2_dataflow_model.onnx")
-    fc0 = model.graph.node[0]
-    fc1 = model.graph.node[1]
-    fc2 = model.graph.node[2]
-    fc3 = model.graph.node[3]
-    fc0w = getCustomOp(fc0)
-    fc1w = getCustomOp(fc1)
-    fc2w = getCustomOp(fc2)
-    fc3w = getCustomOp(fc3)
+    fc_layers = model.get_nodes_by_op_type("StreamingFCLayer_Batch")
+    fc0w = getCustomOp(fc_layers[0])
+    fc1w = getCustomOp(fc_layers[1])
+    fc2w = getCustomOp(fc_layers[2])
+    fc3w = getCustomOp(fc_layers[3])
     fc0w.set_nodeattr("inFIFODepth", 50)
     fc0w.set_nodeattr("SIMD", 8)
     fc0w.set_nodeattr("PE", 16)
@@ -139,14 +138,16 @@ def test_end2end_tfc_w2a2_fold_and_tlastmarker():
     fc3w.set_nodeattr("PE", 10)
     fc3w.set_nodeattr("outFIFODepth", 50)
     model = model.transform(InsertTLastMarker())
+    model = model.transform(GiveUniqueNodeNames())
+    model = model.transform(AnnotateResources("estimate"))
     model.save(build_dir + "/end2end_tfc_w2a2_folded.onnx")
 
 
 def test_end2end_tfc_w2a2_gen_hls_ip():
     model = ModelWrapper(build_dir + "/end2end_tfc_w2a2_folded.onnx")
-    model = model.transform(GiveUniqueNodeNames())
     model = model.transform(CodeGen_ipgen(test_fpga_part, target_clk_ns))
     model = model.transform(HLSSynth_IPGen())
+    model = model.transform(AnnotateResources("hls"))
     model.save(build_dir + "/end2end_tfc_w2a2_ipgen.onnx")
 
 
@@ -172,10 +173,9 @@ def test_end2end_tfc_w2a2_verify_dataflow_part():
     res_npysim = ret_npysim[out_name]
     # node-by-node rtlsim
     model = model.transform(SetExecMode("rtlsim"))
-    getCustomOp(model.graph.node[0]).set_nodeattr("rtlsim_trace", "default")
-    getCustomOp(model.graph.node[1]).set_nodeattr("rtlsim_trace", "default")
-    getCustomOp(model.graph.node[2]).set_nodeattr("rtlsim_trace", "default")
-    getCustomOp(model.graph.node[3]).set_nodeattr("rtlsim_trace", "default")
+    fc_layers = model.get_nodes_by_op_type("StreamingFCLayer_Batch")
+    for fcl in fc_layers:
+        getCustomOp(fcl).set_nodeattr("rtlsim_trace", "default")
     model.save(build_dir + "/end2end_tfc_w2a2_ipstitch_nodebynode_rtlsim.onnx")
     ret_rtlsim_nodebynode = execute_onnx(model, inp_dict, True)
     res_rtlsim_nodebynode = ret_rtlsim_nodebynode[out_name]
@@ -206,7 +206,8 @@ def test_end2end_tfc_w2a2_verify_all():
     iname = parent_model.graph.input[0].name
     oname = parent_model.graph.output[0].name
     # produce results with npysim
-    sdp_node = getCustomOp(parent_model.graph.node[2])
+    sdp_node = parent_model.get_nodes_by_op_type("StreamingDataflowPartition")[0]
+    sdp_node = getCustomOp(sdp_node)
     sdp_node.set_nodeattr("model", build_dir + "/end2end_tfc_w2a2_ipstitch_npysim.onnx")
     ret_npysim = execute_onnx(parent_model, {iname: x}, True)
     y_npysim = ret_npysim[oname]
@@ -236,6 +237,7 @@ def test_end2end_tfc_w2a2_make_pynq_proj():
 def test_end2end_tfc_w2a2_synth_pynq_project():
     model = ModelWrapper(build_dir + "/end2end_tfc_w2a2_pynq_project.onnx")
     model = model.transform(SynthPYNQProject())
+    model = model.transform(AnnotateResources("synth"))
     model.save(build_dir + "/end2end_tfc_w2a2_synth.onnx")
 
 
@@ -283,7 +285,8 @@ def test_end2end_tfc_w2a2_run_on_pynq():
         if ip == "":
             pytest.skip("PYNQ board IP address not specified")
         # produce results with npysim
-        sdp_node = getCustomOp(parent_model.graph.node[2])
+        sdp_node = parent_model.get_nodes_by_op_type("StreamingDataflowPartition")[0]
+        sdp_node = getCustomOp(sdp_node)
         sdp_node.set_nodeattr("model", build_dir + "/end2end_tfc_w2a2_pynq_deploy.onnx")
         ret = execute_onnx(parent_model, {iname: x}, True)
         y = ret[oname]
diff --git a/tests/fpgadataflow/test_fpgadataflow_ip_stitch.py b/tests/fpgadataflow/test_fpgadataflow_ip_stitch.py
index 4a81977d49d174f66e1a02140a7643bd352db7a2..1c5ae02e4c662f48be4f7f70b9de24a1f9f72ecf 100644
--- a/tests/fpgadataflow/test_fpgadataflow_ip_stitch.py
+++ b/tests/fpgadataflow/test_fpgadataflow_ip_stitch.py
@@ -286,13 +286,13 @@ def test_fpgadataflow_ipstitch_pynq_driver():
 
 
 def test_fpgadataflow_ipstitch_pynq_deployment_folder():
-    model = ModelWrapper(
-        ip_stitch_model_dir + "/test_fpgadataflow_ipstitch_pynq_driver.onnx"
-    )
     try:
         ip = os.environ["PYNQ_IP"]  # no default for this one; skip if not defined
         if ip == "":
             pytest.skip("PYNQ board IP address not specified")
+        model = ModelWrapper(
+            ip_stitch_model_dir + "/test_fpgadataflow_ipstitch_pynq_driver.onnx"
+        )
         username = os.getenv("PYNQ_USERNAME", "xilinx")
         password = os.getenv("PYNQ_PASSWORD", "xilinx")
         target_dir = os.getenv("PYNQ_TARGET_DIR", "/home/xilinx/finn")
@@ -319,13 +319,13 @@ def test_fpgadataflow_ipstitch_pynq_deployment_folder():
 
 
 def test_fpgadataflow_ipstitch_remote_execution():
-    model = ModelWrapper(
-        ip_stitch_model_dir + "/test_fpgadataflow_ipstitch_pynq_deployment.onnx"
-    )
     try:
         ip = os.environ["PYNQ_IP"]  # NOQA
         if ip == "":
             pytest.skip("PYNQ board IP address not specified")
+        model = ModelWrapper(
+            ip_stitch_model_dir + "/test_fpgadataflow_ipstitch_pynq_deployment.onnx"
+        )
         idt = DataType.INT2
         x = gen_finn_dt_tensor(idt, (1, 4))
         input_dict = {"inp": x}
diff --git a/tests/fpgadataflow/test_fpgadataflow_res_estimate.py b/tests/fpgadataflow/test_fpgadataflow_res_estimate.py
index 0dd3fd7a9fefaaad9777ac98a35806a9eaa35188..38f792ed3cdd52044b28b4c19ac0603da4e502e6 100644
--- a/tests/fpgadataflow/test_fpgadataflow_res_estimate.py
+++ b/tests/fpgadataflow/test_fpgadataflow_res_estimate.py
@@ -92,7 +92,7 @@ def test_res_estimate():
     model = model.transform(GiveUniqueNodeNames())
     prod_resource_estimation = model.analysis(res_estimation)
     expect_resource_estimation = {
-        "StreamingFCLayer_Batch_0": ["BRAMs: 1", "LUTs: 304.4"]
+        "StreamingFCLayer_Batch_0": {"BRAM_18K": 1, "LUT": 304.4}
     }
 
     assert check_two_dict_for_equality(