Repository: qiskit-community/qiskit-metal (language: python)

Issue:
Update for 3D structure layer stack
<!-- ⚠️ Please abide by this template, otherwise you run the risk of the issue being closed -->
<!-- ⚠️ Make sure to browse the opened and closed issues to confirm this idea does not exist. -->

Please be as descriptive as possible, including: what is expected, why is this feature needed, what is the objective, etc.

### What is the feature being requested?
Update metal to allow for 3D structure layer stack.

After investigating what is needed, please made addition issues as needed in support of this issue.


Switch from travis to github actions
With travis's recent billing changes that went into effect this month: https://blog.travis-ci.com/2020-11-02-travis-ci-new-billing we're rapidly approach the qiskit project quota for the free trial accounts. This is problematic as both retworkx (for non-x86 jobs) and ibmq provider (for networking reasons) both rely on travis for their CI. We don't want to hit that quota and block development/testing on those projects. So we should try to remove our travis usage for things that dwe can run elsewhere to prioritize access to projects that really need it.

Github actions provides a good alternative that has all the necessary capabilities and native integration with github so we should migrate our CI jobs to use that instead. It also offers a much larger quota and enables things like storing artifacts from jobs (useful for saving docs build output etc). You can look at projects like [ignis](https://github.com/Qiskit/qiskit-ignis/tree/master/.github/workflows) and [aqua](https://github.com/Qiskit/qiskit-aqua/tree/master/.github/workflows) on how to configure it


Discussion / hints:

I am happy to work on this if no one else wants to.
Good by me thank you 

Sent from my iPhone

> On Nov 13, 2020, at 5:21 PM, willsALMANJ <notifications@github.com> wrote:
> 
> ﻿
> I am happy to work on this if no one else wants to.
> 
> —
> You are receiving this because you are subscribed to this thread.
> Reply to this email directly, view it on GitHub, or unsubscribe.

As discussed offline -- we are setup to use github actions once the repo goes to public.  There may be one or two minor hiccups but nothing to keep this issue open until March for.

Fix patch:
diff --git a/qiskit_metal/designs/__init__.py b/qiskit_metal/designs/__init__.py
index ea617cb97..7c3322129 100644
--- a/qiskit_metal/designs/__init__.py
+++ b/qiskit_metal/designs/__init__.py
@@ -11,6 +11,8 @@
 # Any modifications or derivative works of this code must retain this
 # copyright notice, and modified files need to carry a notice indicating
 # that they have been altered from the originals.
+
+# modified by Chalmers/SK 20210611 to add DesignFlipChip
 """
 =================================================
 Designs (:mod:`qiskit_metal.designs`)
@@ -39,6 +41,13 @@
 
     DesignPlanar
 
+DesignFlipChip
+---------------
+
+.. autosummary::
+    :toctree: ../stubs/
+
+    DesignFlipChip
 
 QNet
 ---------------
@@ -62,5 +71,6 @@
 from .. import is_design
 from .design_base import QDesign
 from .design_planar import DesignPlanar
+from .design_flipchip import DesignFlipChip
 from .net_info import QNet
 from .interface_components import Components
diff --git a/qiskit_metal/designs/design_flipchip.py b/qiskit_metal/designs/design_flipchip.py
new file mode 100644
index 000000000..5915c3914
--- /dev/null
+++ b/qiskit_metal/designs/design_flipchip.py
@@ -0,0 +1,157 @@
+# -*- coding: utf-8 -*-
+
+# This code is part of Qiskit.
+#
+# (C) Copyright IBM 2017, 2021.
+#
+# This code is licensed under the Apache License, Version 2.0. You may
+# obtain a copy of this license in the LICENSE.txt file in the root directory
+# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
+#
+# Any modifications or derivative works of this code must retain this
+# copyright notice, and modified files need to carry a notice indicating
+# that they have been altered from the originals.
+
+# The code is modified from DesignPlanar to implement flipchip-compatible QDesign class. (SK/Chalmers/20210610)
+"""Module containing Basic Qiskit Metal FlipChip design for CPW type
+geometry."""
+
+# from typing import TYPE_CHECKING
+from typing import Tuple
+
+# from typing import Dict as Dict_
+# from typing import List, Union
+
+from .design_base import QDesign, Dict
+
+__all__ = ["DesignFlipChip"]
+
+
+class DesignFlipChip(QDesign):
+    """Metal class for a flipchip design, consisting of two chips with their device sides facing each other.
+    Typically assumed to have some CPW geometires.
+
+    Inherits QDesign class.
+    """
+
+    def __init__(
+        self,
+        metadata: dict = None,
+        overwrite_enabled: bool = False,
+        enable_renderers: bool = True,
+    ):
+        """Pass metadata to QDesign.
+
+        Args:
+            metadata (dict, optional): Pass to QDesign. Defaults to {}.
+            overwrite_enabled (bool, optional): Passed to QDesign base class. Defaults to False.
+            enable_renderers (bool, optional): Passed to QDesign base class. Defaults to True.
+        """
+
+        super().__init__(
+            metadata=metadata,
+            overwrite_enabled=overwrite_enabled,
+            enable_renderers=enable_renderers,
+        )
+        self.variables[
+            "sample_holder_top"] = "890um"  # how tall is the vacuum above z=0
+        self.variables[
+            "sample_holder_bottom"] = "1650um"  # how tall is the vacuum below z=0
+        self.add_chip_info()
+
+    def add_chip_info(self):
+        """TODO How to get the values into self.chip. Will need to set up
+        parser for "self.p" for design base. For now, just hard code in
+        something.
+
+        # GDSPY is using numbers based on 1 meter unit.
+        # When the gds file is exported, data is converted to "user-selected" units.
+        # centered at (0,0) and 9 by 6 size.
+
+        NOTE: self._chips dict comes from QDesign base class.
+        """
+        # Reinitilise self._chips, then add two chips
+        self._chips = {}
+        self._chips["C_chip"] = Dict(material="silicon",
+                                     layer_start="0",
+                                     layer_end="2048")
+        self._chips["Q_chip"] = Dict(material="silicon",
+                                     layer_start="0",
+                                     layer_end="2048")
+
+        self._chips["C_chip"]["size"] = Dict(
+            center_x="0.0mm",
+            center_y="0.0mm",
+            center_z="0.0mm",
+            size_x="9mm",
+            size_y="9mm",
+            size_z="-280um",  # chip extends in negative z direction by 280 um
+            sample_holder_top=self.variables["sample_holder_top"],
+            sample_holder_bottom=self.variables["sample_holder_bottom"],
+        )
+
+        self._chips["Q_chip"]["size"] = Dict(
+            center_x="0.0mm",
+            center_y="0.0mm",
+            center_z="20 um",
+            size_x="9mm",
+            size_y="9mm",
+            size_z="280um",  # chip extends in positive z direction by 280 um
+            sample_holder_top=self.variables["sample_holder_top"],
+            sample_holder_bottom=self.variables["sample_holder_bottom"],
+        )
+
+    def get_x_y_for_chip(self, chip_name: str) -> Tuple[tuple, int]:
+        """
+        If the chip_name is in self.chips, along with entry for size
+        information then return a tuple=(minx, miny, maxx, maxy). Used for
+        subtraction while exporting design.
+
+        Args:
+            chip_name (str): Name of chip that you want the size of.
+
+        Returns:
+            Tuple[tuple, int]:
+            tuple: The exact placement on rectangle coordinate (minx, miny, maxx, maxy).
+            int: 0=all is good
+            1=chip_name not in self._chips
+            2=size information missing or no good
+        """
+        x_y_location = tuple()
+
+        if chip_name in self._chips:
+            if "size" in self._chips[chip_name]:
+
+                size = self.parse_value(self.chips[chip_name]["size"])
+                if ("center_x" in size and "center_y" in size and
+                        "size_x" in size and "size_y" in size):
+                    if (type(size.center_x) in [int, float] and
+                            type(size.center_y) in [int, float] and
+                            type(size.size_x) in [int, float] and
+                            type(size.size_y) in [int, float]):
+                        x_y_location = (
+                            size["center_x"] - (size["size_x"] / 2.0),
+                            size["center_y"] - (size["size_y"] / 2.0),
+                            size["center_x"] + (size["size_x"] / 2.0),
+                            size["center_y"] + (size["size_y"] / 2.0),
+                        )
+                        return x_y_location, 0
+
+                    self.logger.warning(
+                        f'Size information within self.chips[{chip_name}]["size"]'
+                        f" is NOT an int or float.")
+                    return x_y_location, 2
+
+                self.logger.warning("center_x or center_y or size_x or size_y "
+                                    f' NOT in self._chips[{chip_name}]["size"]')
+                return x_y_location, 2
+
+            self.logger.warning(
+                f"Information for size in NOT in self._chips[{chip_name}]"
+                ' dict. Return "None" in tuple.')
+            return x_y_location, 2
+
+        self.logger.warning(
+            f'Chip name "{chip_name}" is not in self._chips dict. Return "None" in tuple.'
+        )
+        return x_y_location, 1
diff --git a/qiskit_metal/qlibrary/qubits/transmon_cross.py b/qiskit_metal/qlibrary/qubits/transmon_cross.py
index ed701055c..fbe772d03 100644
--- a/qiskit_metal/qlibrary/qubits/transmon_cross.py
+++ b/qiskit_metal/qlibrary/qubits/transmon_cross.py
@@ -72,6 +72,7 @@ class TransmonCross(BaseQubit):  # pylint: disable=invalid-name
         cross_width='20um',
         cross_length='200um',
         cross_gap='20um',
+        chip='main',
         _default_connection_pads=Dict(
             connector_type='0',  # 0 = Claw type, 1 = gap type
             claw_length='30um',
@@ -110,6 +111,9 @@ def make_pocket(self):
         cross_length = p.cross_length
         cross_gap = p.cross_gap
 
+        # access to chip name
+        chip = p.chip
+
         # Creates the cross and the etch equivalent.
         cross_line = draw.shapely.ops.cascaded_union([
             draw.LineString([(0, cross_length), (0, -cross_length)]),
@@ -133,9 +137,15 @@ def make_pocket(self):
         [cross, cross_etch, rect_jj] = polys
 
         # generate qgeometry
-        self.add_qgeometry('poly', dict(cross=cross))
-        self.add_qgeometry('poly', dict(cross_etch=cross_etch), subtract=True)
-        self.add_qgeometry('junction', dict(rect_jj=rect_jj), width=cross_width)
+        self.add_qgeometry('poly', dict(cross=cross), chip=chip)
+        self.add_qgeometry('poly',
+                           dict(cross_etch=cross_etch),
+                           subtract=True,
+                           chip=chip)
+        self.add_qgeometry('junction',
+                           dict(rect_jj=rect_jj),
+                           width=cross_width,
+                           chip=chip)
 
 
 ############################CONNECTORS##################################################################################################
@@ -158,6 +168,9 @@ def make_connection_pad(self, name: str):
         cross_length = p.cross_length
         cross_gap = p.cross_gap
 
+        # access to chip name
+        chip = p.chip
+
         pc = self.p.connection_pads[name]  # parser on connector options
         c_g = pc.claw_gap
         c_l = pc.claw_length
@@ -206,9 +219,11 @@ def make_connection_pad(self, name: str):
         [connector_arm, connector_etcher, port_line] = polys
 
         # Generates qgeometry for the connector pads
-        self.add_qgeometry('poly', {f'{name}_connector_arm': connector_arm})
+        self.add_qgeometry('poly', {f'{name}_connector_arm': connector_arm},
+                           chip=chip)
         self.add_qgeometry('poly',
                            {f'{name}_connector_etcher': connector_etcher},
-                           subtract=True)
+                           subtract=True,
+                           chip=chip)
 
         self.add_pin(name, port_line.coords, c_w)
diff --git a/qiskit_metal/qlibrary/qubits/transmon_pocket.py b/qiskit_metal/qlibrary/qubits/transmon_pocket.py
index 6976ac56e..63a635145 100644
--- a/qiskit_metal/qlibrary/qubits/transmon_pocket.py
+++ b/qiskit_metal/qlibrary/qubits/transmon_pocket.py
@@ -153,6 +153,9 @@ def make_pocket(self):
         # self.p allows us to directly access parsed values (string -> numbers) form the user option
         p = self.p
 
+        # extract chip name
+        chip = p.chip
+
         # since we will reuse these options, parse them once and define them as varaibles
         pad_width = p.pad_width
         pad_height = p.pad_height
@@ -179,13 +182,19 @@ def make_pocket(self):
         [rect_jj, pad_top, pad_bot, rect_pk] = polys
 
         # Use the geometry to create Metal qgeometry
-        self.add_qgeometry('poly', dict(pad_top=pad_top, pad_bot=pad_bot))
-        self.add_qgeometry('poly', dict(rect_pk=rect_pk), subtract=True)
+        self.add_qgeometry('poly',
+                           dict(pad_top=pad_top, pad_bot=pad_bot),
+                           chip=chip)
+        self.add_qgeometry('poly',
+                           dict(rect_pk=rect_pk),
+                           subtract=True,
+                           chip=chip)
         # self.add_qgeometry('poly', dict(
         #     rect_jj=rect_jj), helper=True)
         self.add_qgeometry('junction',
                            dict(rect_jj=rect_jj),
-                           width=p.inductor_width)
+                           width=p.inductor_width,
+                           chip=chip)
 
     def make_connection_pads(self):
         """Makes standard transmon in a pocket."""
@@ -203,6 +212,9 @@ def make_connection_pad(self, name: str):
         p = self.p
         pc = self.p.connection_pads[name]  # parser on connector options
 
+        # extract chip name
+        chip = p.chip
+
         # define commonly used variables once
         cpw_width = pc.cpw_width
         cpw_extend = pc.cpw_extend
@@ -244,12 +256,15 @@ def make_connection_pad(self, name: str):
                                        [p.pos_x, p.pos_y])
         [connector_pad, connector_wire_path, connector_wire_CON] = objects
 
-        self.add_qgeometry('poly', {f'{name}_connector_pad': connector_pad})
+        self.add_qgeometry('poly', {f'{name}_connector_pad': connector_pad},
+                           chip=chip)
         self.add_qgeometry('path', {f'{name}_wire': connector_wire_path},
-                           width=cpw_width)
+                           width=cpw_width,
+                           chip=chip)
         self.add_qgeometry('path', {f'{name}_wire_sub': connector_wire_path},
                            width=cpw_width + 2 * pc.cpw_gap,
-                           subtract=True)
+                           subtract=True,
+                           chip=chip)
 
         ############################################################
 
@@ -258,4 +273,5 @@ def make_connection_pad(self, name: str):
         self.add_pin(name,
                      points=points[-2:],
                      width=cpw_width,
-                     input_as_norm=True)
+                     input_as_norm=True,
+                     chip=chip)
diff --git a/qiskit_metal/qlibrary/resonator/readoutres_fc.py b/qiskit_metal/qlibrary/resonator/readoutres_fc.py
new file mode 100644
index 000000000..2bd10ea9c
--- /dev/null
+++ b/qiskit_metal/qlibrary/resonator/readoutres_fc.py
@@ -0,0 +1,221 @@
+import numpy as np
+
+from qiskit_metal import draw, Dict
+from qiskit_metal.qlibrary.core import QComponent
+
+
+class ReadoutResFC(QComponent):
+    '''
+    The base ReadoutResFC class
+
+    Inherits the QComponent class
+
+    Readout resonator for the flipchip dev. Written for the flipchip tutorial.
+
+    The device consists of the following shapes combined together:
+        - a circle centered at ('pos_x', 'pos_y') with a radius of 'readout_radius', followed by
+        - a straight line (length= 'readout_l1') at a 45 degree angle, followed by
+        - a 45 degree arc, followed by
+        - a vertical line (length = 'readout_l2'), followed by
+        - a 90 degree arc, followed by
+        - a horizontal line (length = 'readout_l3'), followed by
+        - a 180 degree arc, followed by
+        - a horizontal line (length = 'readout_l4'), followed by
+        - 5 meandering horizontal lines (length = 'readout_l5') separated 180 degree arcs.
+
+    The arc has a bend radius of 'readout_cpw_turnradius',
+        it is measured from the center of the cpw to the center of rotation.
+    The lines and arcs will form a cpw with a signal linewidth of 'readout_cpw_width', and
+        signal-to-ground separation of 'readout_cpw_gap'.
+
+    One of the ways to adjust this design to your needs:
+    - change the coupling to the qubit by varying the 'readout_radius',
+    - couple the resonator to the feedthrough transmission line via
+        the horizontal section with this length 'readout_l3',
+    - adjust the frequency of the resonator by varying 'readout_l5'.
+
+    '''
+
+    default_options = Dict(pos_x='0 um',
+                           pos_y='0 um',
+                           readout_radius='50 um',
+                           readout_cpw_width='5 um',
+                           readout_cpw_gap='5 um',
+                           readout_cpw_turnradius='50 um',
+                           readout_l1='400 um',
+                           readout_l2='400 um',
+                           readout_l3='400 um',
+                           readout_l4='250 um',
+                           readout_l5='400 um',
+                           arc_step='1 um',
+                           orientation='0',
+                           layer='1',
+                           layer_subtract='2',
+                           subtract=True,
+                           chip='main',
+                           _default_connection_pads=Dict())
+    ''' Default drawing options ?? '''
+    component_metadata = Dict(short_name='readoutresfc',
+                              _qgeometry_table_poly='True',
+                              _qgeometry_table_junction='False')
+    ''' Component metadata '''
+
+    ##########################
+    def make(self):
+        ''' Make the head for readout res'''
+        self.make_ro()
+
+    ##########################
+
+    def make_ro(self):
+        '''
+        Create the head of the readout resonator.
+        Contains: the circular patch for coupling,
+            the 45 deg line,
+            the 45 deg arc,
+            a short straight segment (of length w) for smooth subtraction
+        '''
+        # access to parsed values from the user option
+        p = self.p
+
+        # access to chip name
+        chip = p.chip
+
+        # local variables
+        r = p.readout_radius
+        w = p.readout_cpw_width
+        g = p.readout_cpw_gap
+        turnradius = p.readout_cpw_turnradius
+        l_1 = p.readout_l1
+        l_2 = p.readout_l2
+        l_3 = p.readout_l3
+        l_4 = p.readout_l4
+        l_5 = p.readout_l5
+
+        # create the coupling patch in term of a circle
+        cppatch = draw.Point(0, 0).buffer(r)
+
+        # create the extended arm
+        ## useful coordinates
+        x_1, y_1 = l_1 * np.cos(np.pi / 4), -l_1 * np.sin(np.pi / 4)
+        x_2, y_2 = x_1 + turnradius * (
+            1 - np.cos(np.pi / 4)), y_1 - turnradius * np.sin(np.pi / 4)
+        coord_init = draw.Point(x_1, y_1)
+        coord_center = draw.Point(x_1 - turnradius * np.cos(np.pi / 4),
+                                  y_1 - turnradius * np.sin(np.pi / 4))
+        x_3, y_3 = x_2, y_2
+        x_4, y_4 = x_3, y_3 - l_2
+        x_5, y_5 = x_4 + turnradius, y_4
+        coord_init1 = draw.Point(x_4, y_4)
+        coord_center1 = draw.Point(x_5, y_5)
+        x_6, y_6 = x_5, y_5 - turnradius
+        x_7, y_7 = x_5 + l_3, y_6
+        x_8, y_8 = x_7, y_7 + turnradius
+        coord_init2 = draw.Point((x_7, y_7))
+        coord_center2 = draw.Point((x_8, y_8))
+        x_9, y_9 = x_8, y_8 + turnradius
+        x_10, y_10 = x_8 - l_4, y_9
+        x_11, y_11 = x_10, y_10 + turnradius
+        coord_init3 = draw.Point((x_10, y_10))
+        coord_center3 = draw.Point((x_11, y_11))
+        arc3 = self.arc(coord_init3, coord_center3, -np.pi)
+        x_12, y_12 = x_11, y_11 + turnradius
+        x_13, y_13 = x_12 + l_5, y_12
+        line12 = draw.LineString([(x_12, y_12), (x_13, y_13)])
+        x_14, y_14 = x_13, y_13 + turnradius
+        coord_init4 = draw.Point((x_13, y_13))
+        coord_center4 = draw.Point((x_14, y_14))
+        arc4 = self.arc(coord_init4, coord_center4, np.pi)
+        ## line containing the 45deg line, 45 deg arc,
+        ## and a short straight segment for smooth subtraction
+        cparm_line = draw.shapely.ops.unary_union([
+            draw.LineString([(0, 0), coord_init]),
+            self.arc(coord_init, coord_center, -np.pi / 4),
+            draw.LineString([(x_3, y_3), (x_4, y_4)]),
+            self.arc(coord_init1, coord_center1, np.pi / 2),
+            draw.LineString([(x_6, y_6), (x_7, y_7)]),
+            self.arc(coord_init2, coord_center2, np.pi),
+            draw.LineString([(x_9, y_9), (x_10, y_10)]), arc3, line12, arc4,
+            draw.translate(line12, 0, 2 * turnradius),
+            draw.translate(arc3, 0, 4 * turnradius),
+            draw.translate(line12, 0, 4 * turnradius),
+            draw.translate(arc4, 0, 4 * turnradius),
+            draw.translate(line12, 0, 6 * turnradius),
+            draw.translate(arc3, 0, 8 * turnradius),
+            draw.translate(line12, 0, 8 * turnradius)
+        ])
+        cparm = cparm_line.buffer(w / 2, cap_style=2, join_style=1)
+        ## fix the gap resulting from buffer
+        eps = 1e-3
+        cparm = draw.Polygon(cparm.exterior)
+        cparm = cparm.buffer(eps, join_style=2).buffer(-eps, join_style=2)
+
+        # create combined objects for the signal line and the etch
+        ro = draw.shapely.ops.unary_union([cppatch, cparm])
+        ro_etch = ro.buffer(g, cap_style=2, join_style=2)
+        x_15, y_15 = x_14, y_14 + 7 * turnradius
+        x_16, y_16 = x_15 + g / 2, y_15
+        port_line = draw.LineString([(x_15, y_15 + w / 2),
+                                     (x_15, y_15 - w / 2)])
+        subtract_patch = draw.LineString([(x_16, y_16 - w / 2 - g - eps),
+                                          (x_16, y_16 + w / 2 + g + eps)
+                                         ]).buffer(g / 2, cap_style=2)
+        ro_etch = ro_etch.difference(subtract_patch)
+
+        # rotate and translate
+        polys = [ro, ro_etch, port_line]
+        polys = draw.rotate(polys, p.orientation, origin=(0, 0))
+        polys = draw.translate(polys, p.pos_x, p.pos_y)
+
+        # update each object
+        [ro, ro_etch, port_line] = polys
+
+        # generate QGeometry
+        self.add_qgeometry('poly', dict(ro=ro), chip=chip, layer=p.layer)
+        self.add_qgeometry('poly',
+                           dict(ro_etch=ro_etch),
+                           chip=chip,
+                           layer=p.layer_subtract,
+                           subtract=p.subtract)
+
+        # generate pins
+        self.add_pin('readout', port_line.coords, width=w, gap=g, chip=chip)
+
+    def arc(self, coord_init, coord_center, angle):
+        '''
+        Generate x,y coordinates (in terms of shapely.geometry.Point()) of an arc with:
+        a specified initial point, rotation center, and
+        rotation direction (specified by angle in radian (float or integer), positive is ccw).
+
+        coord_init, and coord_center should be shapely.geometry.Point object
+        '''
+        # access to parse values from the user option
+        p = self.p
+
+        # local variable
+        r = p.readout_cpw_turnradius
+        step = p.arc_step
+
+        # determine step number
+        step_angle = step / r if angle >= 0 else -step / r
+        step_N = abs(int(angle / step_angle))
+        #laststep_flag = True if angle % step_angle != 0 else False
+        laststep_flag = bool(angle % step_angle != 0)
+
+        # generate coordinate
+        coord = [coord_init]
+        point = coord_init
+        for i in range(step_N):
+            point = draw.rotate(point,
+                                step_angle,
+                                origin=coord_center,
+                                use_radians=True)
+            coord.append(point)
+        if laststep_flag:
+            point = draw.rotate(coord_init,
+                                angle,
+                                origin=coord_center,
+                                use_radians=True)
+            coord.append(point)
+        coord = draw.LineString(coord)
+        return coord
diff --git a/qiskit_metal/renderers/renderer_ansys/ansys_renderer.py b/qiskit_metal/renderers/renderer_ansys/ansys_renderer.py
index 51b9a4ee1..9c9947f9c 100644
--- a/qiskit_metal/renderers/renderer_ansys/ansys_renderer.py
+++ b/qiskit_metal/renderers/renderer_ansys/ansys_renderer.py
@@ -39,8 +39,12 @@
 from qiskit_metal import Dict
 
 from .. import config
+
 if not config.is_building_docs():
-    from qiskit_metal.toolbox_python.utility_functions import toggle_numbers, bad_fillet_idxs
+    from qiskit_metal.toolbox_python.utility_functions import (
+        toggle_numbers,
+        bad_fillet_idxs,
+    )
 
 
 def good_fillet_idxs(coords: list,
@@ -82,15 +86,15 @@ def get_clean_name(name: str) -> str:
         str: Variable name consistent with Python naming conventions.
     """
     # Remove invalid characters
-    name = re.sub('[^0-9a-zA-Z_]', '', name)
+    name = re.sub("[^0-9a-zA-Z_]", "", name)
     # Remove leading characters until we find a letter or underscore
-    name = re.sub('^[^a-zA-Z_]+', '', name)
+    name = re.sub("^[^a-zA-Z_]+", "", name)
     return name
 
 
 class QAnsysRenderer(QRendererAnalysis):
     """Extends QRenderer to export designs to Ansys using pyEPR. The methods
-	which a user will need for Ansys export should be found within this class.
+        which a user will need for Ansys export should be found within this class.
 
     Default Options:
         * Lj: '10nH' -- Lj has units of nanoHenries (nH)
@@ -147,44 +151,50 @@ class QAnsysRenderer(QRendererAnalysis):
     """Default options"""
     # yapf: enable
 
-    NAME_DELIM = r'_'
+    NAME_DELIM = r"_"
     """Name delimiter"""
 
-    name = 'ansys'
+    name = "ansys"
     """Name"""
 
     default_setup = Dict(
-        drivenmodal=Dict(name="Setup",
-                         freq_ghz='5.0',
-                         max_delta_s='0.1',
-                         max_passes='10',
-                         min_passes='1',
-                         min_converged='1',
-                         pct_refinement='30',
-                         basis_order='1'),
-        eigenmode=Dict(name="Setup",
-                       min_freq_ghz='1',
-                       n_modes='1',
-                       max_delta_f='0.5',
-                       max_passes='10',
-                       min_passes='1',
-                       min_converged='1',
-                       pct_refinement='30',
-                       basis_order='-1'),
-        q3d=Dict(name='Setup',
-                 freq_ghz='5.0',
-                 save_fields='False',
-                 enabled='True',
-                 max_passes='15',
-                 min_passes='2',
-                 min_converged_passes='2',
-                 percent_error='0.5',
-                 percent_refinement='30',
-                 auto_increase_solution_order='True',
-                 solution_order='High',
-                 solver_type='Iterative'),
+        drivenmodal=Dict(
+            name="Setup",
+            freq_ghz="5.0",
+            max_delta_s="0.1",
+            max_passes="10",
+            min_passes="1",
+            min_converged="1",
+            pct_refinement="30",
+            basis_order="1",
+        ),
+        eigenmode=Dict(
+            name="Setup",
+            min_freq_ghz="1",
+            n_modes="1",
+            max_delta_f="0.5",
+            max_passes="10",
+            min_passes="1",
+            min_converged="1",
+            pct_refinement="30",
+            basis_order="-1",
+        ),
+        q3d=Dict(
+            name="Setup",
+            freq_ghz="5.0",
+            save_fields="False",
+            enabled="True",
+            max_passes="15",
+            min_passes="2",
+            min_converged_passes="2",
+            percent_error="0.5",
+            percent_refinement="30",
+            auto_increase_solution_order="True",
+            solution_order="High",
+            solver_type="Iterative",
+        ),
         port_inductor_gap=
-        '10um'  # spacing between port and inductor if junction is drawn both ways
+        "10um",  # spacing between port and inductor if junction is drawn both ways
     )
     """Default setup."""
 
@@ -204,16 +214,18 @@ class QAnsysRenderer(QRendererAnalysis):
     # Keeping this as a cls dict so could be edited before renderer is instantiated.
     # To update component.options junction table.
 
-    element_table_data = dict(path=dict(wire_bonds=False),
-                              junction=dict(
-                                  inductance=default_options['Lj'],
-                                  capacitance=default_options['Cj'],
-                                  resistance=default_options['_Rj'],
-                                  mesh_kw_jj=parse_units(
-                                      default_options['max_mesh_length_jj'])))
+    element_table_data = dict(
+        path=dict(wire_bonds=False),
+        junction=dict(
+            inductance=default_options["Lj"],
+            capacitance=default_options["Cj"],
+            resistance=default_options["_Rj"],
+            mesh_kw_jj=parse_units(default_options["max_mesh_length_jj"]),
+        ),
+    )
     """Element table data."""
 
-    def __init__(self, design: 'QDesign', initiate=True, options: Dict = None):
+    def __init__(self, design: "QDesign", initiate=True, options: Dict = None):
         """Create a QRenderer for Ansys.
 
         Args:
@@ -322,12 +334,14 @@ def close(self):
         """
         return self._close_renderer()
 
-    def open_ansys(self,
-                   path: str = None,
-                   executable: str = 'reg_ansysedt.exe',
-                   path_var: str = 'ANSYSEM_ROOT202'):
+    def open_ansys(
+        self,
+        path: str = None,
+        executable: str = "reg_ansysedt.exe",
+        path_var: str = "ANSYSEM_ROOT202",
+    ):
         """Alternative method to open an Ansys session that allows to specify
-		which version to use. Default is version 2020 R2, but can be overridden.
+                which version to use. Default is version 2020 R2, but can be overridden.
 
         Args:
             path (str): Path to the Ansys executable. Defaults to None
@@ -336,32 +350,35 @@ def open_ansys(self,
                             Ansys executable. Only used when path=None.
                             Defaults to 'ANSYSEM_ROOT202' (Ansys ver. 2020 R2)
         """
-        if not system() == 'Windows':
+        if not system() == "Windows":
             self.logger.warning(
-                'You are using %s, but this is a renderer to Ansys, which only runs on Windows. '
-                'Expect any sort of Errors if you try to work with this renderer beyond this point.'
+                "You are using %s, but this is a renderer to Ansys, which only runs on Windows. "
+                "Expect any sort of Errors if you try to work with this renderer beyond this point."
                 % system())
 
         import subprocess
+
         if path is None:
             try:
                 path = os.environ[path_var]
             except KeyError:
                 self.logger.error(
-                    'environment variable %s not found. Is Ansys 2020 R2 installed on this '
-                    'machine? If yes, then create said environment variable. If you have a '
-                    'different version of Ansys, then pass to open_ansys() the path to its '
-                    'binary, or the env var that stores it.' % path_var)
+                    "environment variable %s not found. Is Ansys 2020 R2 installed on this "
+                    "machine? If yes, then create said environment variable. If you have a "
+                    "different version of Ansys, then pass to open_ansys() the path to its "
+                    "binary, or the env var that stores it." % path_var)
                 raise
         else:
             path = os.path.abspath(path)
-        cmdlist = [os.path.sep.join([path, executable]), '-shortcut']
+        cmdlist = [os.path.sep.join([path, executable]), "-shortcut"]
         subprocess.call(cmdlist, cwd=path)
 
-    def connect_ansys(self,
-                      project_path: str = None,
-                      project_name: str = None,
-                      design_name: str = None):
+    def connect_ansys(
+        self,
+        project_path: str = None,
+        project_name: str = None,
+        design_name: str = None,
+    ):
         """If none of the optional parameters are provided: connects to the
         Ansys COM, then checks for, and grab if present, an active project,
         design, and design setup.
@@ -374,10 +391,10 @@ def connect_ansys(self,
             project_name (str, optional): File name (with or without extension)
             design_name (str, optional): Name of the default design to open from the project file
         """
-        if not system() == 'Windows':
+        if not system() == "Windows":
             self.logger.warning(
-                'You are using %s, but this is a renderer to Ansys, which only runs on Windows. '
-                'Expect any sort of Errors if you try to work with this renderer beyond this point.'
+                "You are using %s, but this is a renderer to Ansys, which only runs on Windows. "
+                "Expect any sort of Errors if you try to work with this renderer beyond this point."
                 % system())
 
         # pyEPR does not like extensions
@@ -385,19 +402,21 @@ def connect_ansys(self,
             project_name = project_name.replace(".aedt", "")
         # open connection through pyEPR
         import pythoncom
+
         try:
             self._pinfo = epr.ProjectInfo(
                 do_connect=True,
-                project_path=self._options['project_path']
+                project_path=self._options["project_path"]
                 if not project_path else project_path,
-                project_name=self._options['project_name']
+                project_name=self._options["project_name"]
                 if not project_name else project_name,
                 design_name=self._options['design_name']
                 if not design_name else design_name)
         except pythoncom.com_error as error:  # pylint: disable=no-member
             print("com_error: ", error)
             hr, msg, exc, arg = error.args
-            if msg == "Invalid class string":  # and hr == -2147221005 and exc is None and arg is None
+            if (msg == "Invalid class string"
+               ):  # and hr == -2147221005 and exc is None and arg is None
                 self.logger.error(
                     "pyEPR cannot find the Ansys COM. Ansys installation might not have registered it. "
                     "To verify if this is the problem, execute the following: "
@@ -413,7 +432,7 @@ def disconnect_ansys(self):
             self.pinfo.disconnect()
         else:
             self.logger.warning(
-                'This renderer appears to be already disconnected from Ansys')
+                "This renderer appears to be already disconnected from Ansys")
 
     def new_ansys_project(self):
         """Creates a new empty project in Ansys."""
@@ -432,7 +451,7 @@ def connect_ansys_design(self, design_name: str = None):
                 all_designs_names = self.pinfo.project.get_design_names()
                 if design_name not in all_designs_names:
                     self.logger.warning(
-                        f'The design_name={design_name} is not in project.  Connection did not happen.'
+                        f"The design_name={design_name} is not in project.  Connection did not happen."
                     )
                     return
 
@@ -441,17 +460,17 @@ def connect_ansys_design(self, design_name: str = None):
                     self.pinfo.connect_setup()
                 except AttributeError:
                     self.logger.error(
-                        'Please install a more recent version of pyEPR (>=0.8.4.3)'
+                        "Please install a more recent version of pyEPR (>=0.8.4.3)"
                     )
             else:
                 self.logger.warning(
-                    'Either you do not have a project loaded in Ansys, or you are not connected '
-                    'to it. Try executing hfss.connect_ansys(), or creating a new Ansys project. '
-                    'Also check the help file and other guide notebooks')
+                    "Either you do not have a project loaded in Ansys, or you are not connected "
+                    "to it. Try executing hfss.connect_ansys(), or creating a new Ansys project. "
+                    "Also check the help file and other guide notebooks")
         else:
             self.logger.warning(
-                'It does not look like you are connected to Ansys. Please use connect_ansys() '
-                'and make sure self.pinfo is set. There must be a project open in Ansys first.'
+                "It does not look like you are connected to Ansys. Please use connect_ansys() "
+                "and make sure self.pinfo is set. There must be a project open in Ansys first."
             )
 
     def get_active_design_name(self):
@@ -485,7 +504,7 @@ def plot_ansys_fields(self, *args, **kwargs):
         (deprecated) use plot_fields()
         """
         self.logger.warning(
-            'This method is deprecated. Change your scripts to use plot_fields()'
+            "This method is deprecated. Change your scripts to use plot_fields()"
         )
         return self.plot_fields(*args, **kwargs)
 
@@ -546,37 +565,38 @@ def plot_fields(
         """
         self.modeler._modeler.ShowWindow()
         if not self.pinfo:
-            self.logger.warning('pinfo is None.')
+            self.logger.warning("pinfo is None.")
             return
 
         if self.pinfo.design:
             if not self.pinfo.design._fields_calc:
-                self.logger.warning('The _fields_calc in design is None.')
+                self.logger.warning("The _fields_calc in design is None.")
                 return
             if not self.pinfo.design._modeler:
-                self.logger.warning('The _modeler in design is None.')
+                self.logger.warning("The _modeler in design is None.")
                 return
         else:
-            self.logger.warning('The design in pinfo is None.')
+            self.logger.warning("The design in pinfo is None.")
             return
 
         if not self.pinfo.setup:
-            self.logger.warning('The setup in pinfo is None.')
+            self.logger.warning("The setup in pinfo is None.")
             return
 
-        #TODO: This is just a prototype - should add features and flexibility.
-        oFieldsReport = self.pinfo.design._fields_calc  #design.GetModule("FieldsReporter")
-        oModeler = self.pinfo.design._modeler  #design.SetActiveEditor("3D Modeler")
+        # TODO: This is just a prototype - should add features and flexibility.
+        oFieldsReport = (self.pinfo.design._fields_calc
+                        )  # design.GetModule("FieldsReporter")
+        oModeler = self.pinfo.design._modeler  # design.SetActiveEditor("3D Modeler")
         setup = self.pinfo.setup
 
         # Object ID - use to plot on faces of
         object_id = oModeler.GetObjectIDByName(object_name)
         # Can also use hfss.pinfo.design._modeler.GetFaceIDs("main")
 
-        paf = self.options['plot_ansys_fields_options']
+        paf = self.options["plot_ansys_fields_options"]
 
         if not name:
-            name = self.parse_value(paf['name'])
+            name = self.parse_value(paf["name"])
 
         # Name of the solution setup and solution formatted as:"<SolveSetupName> : <WhichSolution>",
         # where <WhichSolution> can be "Adaptive_<n>", "LastAdaptive", or "PortOnly".
@@ -584,40 +604,43 @@ def plot_fields(
         # If it is missing, the plot will not be created.
         SolutionName = f"{setup.name} : LastAdaptive"
         if not UserSpecifyName:
-            UserSpecifyName = int(self.parse_value(paf['UserSpecifyName']))
+            UserSpecifyName = int(self.parse_value(paf["UserSpecifyName"]))
         if not UserSpecifyFolder:
-            UserSpecifyFolder = int(self.parse_value(paf['UserSpecifyFolder']))
+            UserSpecifyFolder = int(self.parse_value(paf["UserSpecifyFolder"]))
         if not QuantityName:
-            QuantityName = self.parse_value(paf['QuantityName'])
+            QuantityName = self.parse_value(paf["QuantityName"])
         if not PlotFolder:
-            PlotFolder = self.parse_value(paf['PlotFolder'])
+            PlotFolder = self.parse_value(paf["PlotFolder"])
         if not StreamlinePlot:
-            StreamlinePlot = is_true(self.parse_value(paf['StreamlinePlot']))
+            StreamlinePlot = is_true(self.parse_value(paf["StreamlinePlot"]))
         if not AdjacentSidePlot:
             AdjacentSidePlot = is_true(self.parse_value(
-                paf['AdjacentSidePlot']))
+                paf["AdjacentSidePlot"]))
         if not FullModelPlot:
-            FullModelPlot = is_true(self.parse_value(paf['FullModelPlot']))
+            FullModelPlot = is_true(self.parse_value(paf["FullModelPlot"]))
         if not IntrinsicVar:
-            IntrinsicVar = self.parse_value(paf['IntrinsicVar'])
+            IntrinsicVar = self.parse_value(paf["IntrinsicVar"])
         if not PlotGeomInfo_0:
-            PlotGeomInfo_0 = int(self.parse_value(paf['PlotGeomInfo_0']))
+            PlotGeomInfo_0 = int(self.parse_value(paf["PlotGeomInfo_0"]))
         if not PlotGeomInfo_1:
-            PlotGeomInfo_1 = self.parse_value(paf['PlotGeomInfo_1'])
+            PlotGeomInfo_1 = self.parse_value(paf["PlotGeomInfo_1"])
         if not PlotGeomInfo_2:
-            PlotGeomInfo_2 = self.parse_value(paf['PlotGeomInfo_2'])
+            PlotGeomInfo_2 = self.parse_value(paf["PlotGeomInfo_2"])
         if not PlotGeomInfo_3:
-            PlotGeomInfo_3 = int(self.parse_value(paf['PlotGeomInfo_3']))
+            PlotGeomInfo_3 = int(self.parse_value(paf["PlotGeomInfo_3"]))
 
         # used to pass to CreateFieldPlot
         # Copied from  pdf at http://www.ece.uprm.edu/~rafaelr/inel6068/HFSS/scripting.pdf
-        #<PlotGeomArray>Array(<NumGeomTypes>, <GeomTypeData>,<GeomTypeData>, ...)
+        # <PlotGeomArray>Array(<NumGeomTypes>, <GeomTypeData>,<GeomTypeData>, ...)
         # For example:
         # Array(4, "Volume", "ObjList", 1, "Box1","Surface", "FacesList", 1, "12", "Line", 1,"Polyline1",
         #       "Point", 2, "Point1", "Point2"
         PlotGeomInfo = [
-            PlotGeomInfo_0, PlotGeomInfo_1, PlotGeomInfo_2, PlotGeomInfo_3,
-            str(object_id)
+            PlotGeomInfo_0,
+            PlotGeomInfo_1,
+            PlotGeomInfo_2,
+            PlotGeomInfo_3,
+            str(object_id),
         ]
 
         # yapf: disable
@@ -634,7 +657,7 @@ def plot_fields(
                 "IntrinsicVar:="     , IntrinsicVar,
                 "PlotGeomInfo:="     , PlotGeomInfo,
             ]
-        #yapf: enable
+        # yapf: enable
         return oFieldsReport.CreateFieldPlot(args_list, "Field")
 
     def plot_ansys_delete(self, names: list):
@@ -642,7 +665,7 @@ def plot_ansys_delete(self, names: list):
         (deprecated) Use clear_fields()
         """
         self.logger.warning(
-            'This method is deprecated. Change your scripts to use clear_fields()'
+            "This method is deprecated. Change your scripts to use clear_fields()"
         )
         self.clear_fields(names)
 
@@ -685,14 +708,16 @@ def save_screenshot(self, path: str = None, show: bool = True):
             return self.pinfo.design.save_screenshot(path, show)
         except AttributeError:
             self.logger.error(
-                'Please install a more recent version of pyEPR (>=0.8.4.3)')
-
-    def execute_design(self,
-                       design_name: str,
-                       solution_type: str,
-                       vars_to_initialize: Dict,
-                       force_redraw: bool = False,
-                       **design_selection) -> str:
+                "Please install a more recent version of pyEPR (>=0.8.4.3)")
+
+    def execute_design(
+        self,
+        design_name: str,
+        solution_type: str,
+        vars_to_initialize: Dict,
+        force_redraw: bool = False,
+        **design_selection,
+    ) -> str:
         """It wraps the render_design() method to
         1. skip rendering if the "selection" of components is left empty (re-uses selected design)
         2. force design clearing and redraw if force_Redraw is set
@@ -708,8 +733,8 @@ def execute_design(self,
                 in case of conflicts)
         """
         # If a selection of components is not specified, use the active renderer-design
-        if 'selection' in design_selection:
-            if design_selection['selection'] is None:
+        if "selection" in design_selection:
+            if design_selection["selection"] is None:
                 try:
                     return self.pinfo.design.name
                 except AttributeError:
@@ -744,30 +769,32 @@ def new_ansys_design(self,
         """
         if self.pinfo:
             try:
-                if solution_type == 'capacitive':
+                if solution_type == "capacitive":
                     adesign = self.pinfo.project.new_q3d_design(design_name)
-                elif solution_type == 'eigenmode':
+                elif solution_type == "eigenmode":
                     adesign = self.pinfo.project.new_em_design(design_name)
-                elif solution_type == 'drivenmodal':
+                elif solution_type == "drivenmodal":
                     adesign = self.pinfo.project.new_dm_design(design_name)
                 else:
                     self.logger.error(
-                        f'The solution_type = {solution_type} is not supported by this renderer'
+                        f"The solution_type = {solution_type} is not supported by this renderer"
                     )
             except AttributeError:
                 if self.pinfo.project is None:
-                    self.logger.error('Project not found')
+                    self.logger.error("Project not found")
                 else:
                     self.logger.error(
-                        'Please install a more recent version of pyEPR (>=0.8.4.4)'
+                        "Please install a more recent version of pyEPR (>=0.8.4.4)"
                     )
                 raise
             if connect:
                 self.connect_ansys_design(adesign.name)
             return adesign
         else:
-            self.logger.info("You have to first connect to Ansys and to a project " \
-                            "before creating a new design. You can use renderer.connect_ansys()")
+            self.logger.info(
+                "You have to first connect to Ansys and to a project "
+                "before creating a new design. You can use renderer.connect_ansys()"
+            )
 
     def activate_ansys_design(self,
                               design_name: str,
@@ -785,40 +812,43 @@ def activate_ansys_design(self,
                     names_in_design = self.pinfo.project.get_design_names()
                 except AttributeError:
                     self.logger.error(
-                        'Please install a more recent version of pyEPR (>=0.8.4.5)'
+                        "Please install a more recent version of pyEPR (>=0.8.4.5)"
                     )
 
                 if design_name in names_in_design:
                     self.pinfo.connect_design(design_name)
-                    oDesktop = self.pinfo.design.parent.parent._desktop  # self.pinfo.design does not work
+                    oDesktop = (self.pinfo.design.parent.parent._desktop
+                               )  # self.pinfo.design does not work
                     oProject = oDesktop.SetActiveProject(
                         self.pinfo.project_name)
                     oDesign = oProject.SetActiveDesign(design_name)
                     current_solution_type = self.pinfo.design.solution_type.lower(
                     )
-                    if current_solution_type == 'q3d':
-                        current_solution_type = 'capacitive'
-                    if current_solution_type != solution_type and solution_type is not None:
+                    if current_solution_type == "q3d":
+                        current_solution_type = "capacitive"
+                    if (current_solution_type != solution_type and
+                            solution_type is not None):
                         self.logger.warning(
-                            f'The design_name={design_name} already exists, but it has solution_type=='
-                            f'{current_solution_type}, which is different from the requested=={solution_type}. '
-                            f'If you want a design with solution type=={solution_type}, please change the name '
-                            'requested for your design to one that does not exist. Alternatively, manually modify '
-                            f'the solution_type for design {design_name} from the Ansys GUI.'
+                            f"The design_name={design_name} already exists, but it has solution_type=="
+                            f"{current_solution_type}, which is different from the requested=={solution_type}. "
+                            f"If you want a design with solution type=={solution_type}, please change the name "
+                            "requested for your design to one that does not exist. Alternatively, manually modify "
+                            f"the solution_type for design {design_name} from the Ansys GUI."
                         )
                 else:
                     self.logger.warning(
-                        f'The design_name={design_name} was not in active project.  '
-                        f'Designs in active project are: \n{names_in_design}.  '
-                        'A new design will be added to the project.  ')
+                        f"The design_name={design_name} was not in active project.  "
+                        f"Designs in active project are: \n{names_in_design}.  "
+                        "A new design will be added to the project.  ")
                     if solution_type is not None:
                         adesign = self.new_ansys_design(
                             design_name=design_name,
                             solution_type=solution_type,
-                            connect=True)
+                            connect=True,
+                        )
                     else:
                         self.logger.error(
-                            'Please specify the solution_type, to determine what design to create'
+                            "Please specify the solution_type, to determine what design to create"
                         )
             else:
                 self.logger.warning(
@@ -838,19 +868,19 @@ def new_ansys_setup(self, name: str, **other_setup):
         Returns:
             pyEPR.ansys.HfssEMSetup: Pointer to the ansys setup object
         """
-        #TODO: only use activate_ansys_setup?
+        # TODO: only use activate_ansys_setup?
         if self.pinfo:
             if self.pinfo.design:
-                if 'reuse_setup' in other_setup:
-                    if other_setup['reuse_setup']:
+                if "reuse_setup" in other_setup:
+                    if other_setup["reuse_setup"]:
                         # delete_setup will check if setup exists, before deleting.
                         self.pinfo.design.delete_setup(name)
 
-                if self.pinfo.design.solution_type == 'Eigenmode':
+                if self.pinfo.design.solution_type == "Eigenmode":
                     setup = self.add_eigenmode_setup(name, **other_setup)
-                elif self.pinfo.design.solution_type == 'DrivenModal':
+                elif self.pinfo.design.solution_type == "DrivenModal":
                     setup = self.add_drivenmodal_setup(name, **other_setup)
-                elif self.pinfo.design.solution_type == 'Q3D':
+                elif self.pinfo.design.solution_type == "Q3D":
                     setup = self.add_q3d_setup(name, **other_setup)
         return setup
 
@@ -861,7 +891,7 @@ def initialize_cap_extract(self, **kwargs):
             str: Name of the setup that has been updated
         """
 
-        setup = self.new_ansys_setup(**kwargs)  #TODO: activate_ansys_setup?
+        setup = self.new_ansys_setup(**kwargs)  # TODO: activate_ansys_setup?
         return setup.name
 
     def initialize_eigenmode(self, vars: Dict = {}, **kwargs):
@@ -875,7 +905,7 @@ def initialize_eigenmode(self, vars: Dict = {}, **kwargs):
             str: Name of the setup that has been updated
         """
         self.set_variables(vars)
-        setup = self.new_ansys_setup(**kwargs)  #TODO: activate_ansys_setup?
+        setup = self.new_ansys_setup(**kwargs)  # TODO: activate_ansys_setup?
         return setup.name
 
     def initialize_drivenmodal(self,
@@ -893,7 +923,7 @@ def initialize_drivenmodal(self,
             str: Name of the setup that has been updated
         """
         self.set_variables(vars)
-        setup = self.new_ansys_setup(**kwargs)  #TODO: activate_ansys_setup?
+        setup = self.new_ansys_setup(**kwargs)  # TODO: activate_ansys_setup?
         sweep = self.add_sweep(setup.name, **sweep_setup)
         return setup.name, sweep.name
 
@@ -919,14 +949,14 @@ def activate_ansys_setup(self, setup_name: str):
                         else:
                             # When name is given, but not in design. So make a new setup with given name.
                             self.logger.warning(
-                                f'The setup_name={setup_name} was not in active design.  '
-                                f'Setups in active design are: \n{all_setup_names}.  '
-                                'A new setup will default values will be added to the design.  '
+                                f"The setup_name={setup_name} was not in active design.  "
+                                f"Setups in active design are: \n{all_setup_names}.  "
+                                "A new setup will default values will be added to the design.  "
                             )
                             self.pinfo.setup = self.new_ansys_setup(
                                 name=setup_name)
                     else:
-                        self.logger.warning(f'Please specify a setup_name.')
+                        self.logger.warning(f"Please specify a setup_name.")
                 else:
                     self.logger.warning(
                         "Design not found in selected project, have you opened a design?"
@@ -939,10 +969,12 @@ def activate_ansys_setup(self, setup_name: str):
                 "Have you run connect_ansys()?  Cannot find a reference to Ansys in QRenderer."
             )
 
-    def render_design(self,
-                      selection: Union[list, None] = None,
-                      open_pins: Union[list, None] = None,
-                      box_plus_buffer: bool = True):
+    def render_design(
+        self,
+        selection: Union[list, None] = None,
+        open_pins: Union[list, None] = None,
+        box_plus_buffer: bool = True,
+    ):
         """Initiate rendering of components in design contained in selection,
         assuming they're valid. Components are rendered before the chips they
         reside on, and subtraction of negative shapes is performed at the very
@@ -986,7 +1018,7 @@ def render_design(self,
 
         if self.case == 2:
             self.logger.warning(
-                'Unable to proceed with rendering. Please check selection.')
+                "Unable to proceed with rendering. Please check selection.")
             return
 
         self.chip_subtract_dict = defaultdict(set)
@@ -1011,7 +1043,7 @@ def render_tables(self, skip_junction: bool = False):
         Render components in design grouped by table type (path, poly, or junction).
         """
         for table_type in self.design.qgeometry.get_element_types():
-            if table_type != 'junction' or not skip_junction:
+            if table_type != "junction" or not skip_junction:
                 self.render_components(table_type)
 
     def render_components(self, table_type: str):
@@ -1024,13 +1056,13 @@ def render_components(self, table_type: str):
         table = self.design.qgeometry.tables[table_type]
 
         if self.case == 0:  # Render a subset of components using mask
-            mask = table['component'].isin(self.qcomp_ids)
+            mask = table["component"].isin(self.qcomp_ids)
             table = table[mask]
 
         for _, qgeom in table.iterrows():
-            self.render_element(qgeom, bool(table_type == 'junction'))
+            self.render_element(qgeom, bool(table_type == "junction"))
 
-        if table_type == 'path':
+        if table_type == "path":
             self.auto_wirebonds(table)
 
     def render_element(self, qgeom: pd.Series, is_junction: bool):
@@ -1064,13 +1096,13 @@ def render_element_junction(self, qgeom: pd.Series):
         """
         ansys_options = dict(transparency=0.0)
 
-        qc_name = 'Lj_' + str(qgeom['component'])
-        qc_elt = get_clean_name(qgeom['name'])
+        qc_name = "Lj_" + str(qgeom["component"])
+        qc_elt = get_clean_name(qgeom["name"])
         qc_shapely = qgeom.geometry
         qc_chip_z = parse_units(self.design.get_chip_z(qgeom.chip))
         qc_width = parse_units(qgeom.width)
 
-        name = f'{qc_name}{QAnsysRenderer.NAME_DELIM}{qc_elt}'
+        name = f"{qc_name}{QAnsysRenderer.NAME_DELIM}{qc_elt}"
 
         endpoints = parse_units(list(qc_shapely.coords))
         endpoints_3d = to_vec3D(endpoints, qc_chip_z)
@@ -1086,19 +1118,25 @@ def render_element_junction(self, qgeom: pd.Series):
             y_min, y_max = y0 - qc_width / 2, y0 + qc_width / 2
 
         # Draw rectangle
-        self.logger.debug(f'Drawing a rectangle: {name}')
-        poly_ansys = self.modeler.draw_rect_corner([x_min, y_min, qc_chip_z],
-                                                   x_max - x_min, y_max - y_min,
-                                                   qc_chip_z, **ansys_options)
-        axis = 'x' if abs(x1 - x0) > abs(y1 - y0) else 'y'
-        self.modeler.rename_obj(poly_ansys, 'JJ_rect_' + name)
-        self.assign_mesh.append('JJ_rect_' + name)
+        self.logger.debug(f"Drawing a rectangle: {name}")
+        poly_ansys = self.modeler.draw_rect_corner(
+            [x_min, y_min, qc_chip_z],
+            x_max - x_min,
+            y_max - y_min,
+            qc_chip_z,
+            **ansys_options,
+        )
+        axis = "x" if abs(x1 - x0) > abs(y1 - y0) else "y"
+        self.modeler.rename_obj(poly_ansys, "JJ_rect_" + name)
+        self.assign_mesh.append("JJ_rect_" + name)
 
         # Draw line
-        poly_jj = self.modeler.draw_polyline([endpoints_3d[0], endpoints_3d[1]],
-                                             closed=False,
-                                             **dict(color=(128, 0, 128)))
-        poly_jj = poly_jj.rename('JJ_' + name + '_')
+        poly_jj = self.modeler.draw_polyline(
+            [endpoints_3d[0], endpoints_3d[1]],
+            closed=False,
+            **dict(color=(128, 0, 128)),
+        )
+        poly_jj = poly_jj.rename("JJ_" + name + "_")
         poly_jj.show_direction = True
 
     def render_element_poly(self, qgeom: pd.Series):
@@ -1109,25 +1147,32 @@ def render_element_poly(self, qgeom: pd.Series):
         """
         ansys_options = dict(transparency=0.0)
 
-        qc_name = self.design._components[qgeom['component']].name
-        qc_elt = get_clean_name(qgeom['name'])
+        qc_name = self.design._components[qgeom["component"]].name
+        qc_elt = get_clean_name(qgeom["name"])
 
         qc_shapely = qgeom.geometry  # shapely geom
         qc_chip_z = parse_units(self.design.get_chip_z(qgeom.chip))
         qc_fillet = round(qgeom.fillet, 7)
 
-        name = f'{qc_elt}{QAnsysRenderer.NAME_DELIM}{qc_name}'
+        name = f"{qc_elt}{QAnsysRenderer.NAME_DELIM}{qc_name}"
 
         points = parse_units(list(
             qc_shapely.exterior.coords))  # list of 2d point tuples
         points_3d = to_vec3D(points, qc_chip_z)
 
         if is_rectangle(qc_shapely):  # Draw as rectangle
-            self.logger.debug(f'Drawing a rectangle: {name}')
+            self.logger.debug(f"Drawing a rectangle: {name}")
             x_min, y_min, x_max, y_max = qc_shapely.bounds
             poly_ansys = self.modeler.draw_rect_corner(
-                *parse_units([[x_min, y_min, qc_chip_z], x_max - x_min,
-                              y_max - y_min, qc_chip_z]), **ansys_options)
+                *parse_units([
+                    [x_min, y_min,
+                     self.design.get_chip_z(qgeom.chip)],
+                    x_max - x_min,
+                    y_max - y_min,
+                    0,
+                ]),
+                **ansys_options,
+            )
             self.modeler.rename_obj(poly_ansys, name)
 
         else:
@@ -1145,7 +1190,8 @@ def render_element_poly(self, qgeom: pd.Series):
                 points,
                 qc_fillet,
                 precision=self.design._template_options.PRECISION,
-                isclosed=True)
+                isclosed=True,
+            )
             if idxs_to_fillet:
                 self.modeler._fillet(qc_fillet, idxs_to_fillet, poly_ansys)
 
@@ -1162,11 +1208,11 @@ def render_element_poly(self, qgeom: pd.Series):
         if qgeom.chip not in self.chip_subtract_dict:
             self.chip_subtract_dict[qgeom.chip] = set()
 
-        if qgeom['subtract']:
+        if qgeom["subtract"]:
             self.chip_subtract_dict[qgeom.chip].add(name)
 
         # Potentially add to list of elements to metallize
-        elif not qgeom['helper']:
+        elif not qgeom["helper"]:
             self.assign_perfE.append(name)
 
     def render_element_path(self, qgeom: pd.Series):
@@ -1177,13 +1223,13 @@ def render_element_path(self, qgeom: pd.Series):
         """
         ansys_options = dict(transparency=0.0)
 
-        qc_name = self.design._components[qgeom['component']].name
-        qc_elt = get_clean_name(qgeom['name'])
+        qc_name = self.design._components[qgeom["component"]].name
+        qc_elt = get_clean_name(qgeom["name"])
 
         qc_shapely = qgeom.geometry  # shapely geom
         qc_chip_z = parse_units(self.design.get_chip_z(qgeom.chip))
 
-        name = f'{qc_elt}{QAnsysRenderer.NAME_DELIM}{qc_name}'
+        name = f"{qc_elt}{QAnsysRenderer.NAME_DELIM}{qc_name}"
 
         qc_width = parse_units(qgeom.width)
 
@@ -1197,7 +1243,7 @@ def render_element_path(self, qgeom: pd.Series):
         except AttributeError:
             if self.modeler is None:
                 self.logger.error(
-                    'No modeler was found. Are you connected to an active Ansys Design?'
+                    "No modeler was found. Are you connected to an active Ansys Design?"
                 )
             raise
 
@@ -1210,7 +1256,8 @@ def render_element_path(self, qgeom: pd.Series):
                 points,
                 qc_fillet,
                 precision=self.design._template_options.PRECISION,
-                isclosed=False)
+                isclosed=False,
+            )
             if idxs_to_fillet:
                 self.modeler._fillet(qc_fillet, idxs_to_fillet, poly_ansys)
 
@@ -1219,14 +1266,15 @@ def render_element_path(self, qgeom: pd.Series):
             x1, y1 = points[1]
             vlen = math.sqrt((x1 - x0)**2 + (y1 - y0)**2)
             p0 = np.array([
-                x0, y0, 0
+                x0, y0, qc_chip_z
             ]) + qc_width / (2 * vlen) * np.array([y0 - y1, x1 - x0, 0])
             p1 = np.array([
-                x0, y0, 0
+                x0, y0, qc_chip_z
             ]) + qc_width / (2 * vlen) * np.array([y1 - y0, x0 - x1, 0])
             shortline = self.modeler.draw_polyline([p0, p1],
                                                    closed=False)  # sweepline
             import pythoncom
+
             try:
                 self.modeler._sweep_along_path(shortline, poly_ansys)
             except pythoncom.com_error as error:
@@ -1244,10 +1292,10 @@ def render_element_path(self, qgeom: pd.Series):
         if qgeom.chip not in self.chip_subtract_dict:
             self.chip_subtract_dict[qgeom.chip] = set()
 
-        if qgeom['subtract']:
+        if qgeom["subtract"]:
             self.chip_subtract_dict[qgeom.chip].add(name)
 
-        elif qgeom['width'] and (not qgeom['helper']):
+        elif qgeom["width"] and (not qgeom["helper"]):
             self.assign_perfE.append(name)
 
     def add_endcaps(self, open_pins: Union[list, None] = None):
@@ -1263,12 +1311,14 @@ def add_endcaps(self, open_pins: Union[list, None] = None):
 
         for comp, pin in open_pins:
             pin_dict = self.design.components[comp].pins[pin]
-            width, gap = parse_units([pin_dict['width'], pin_dict['gap']])
-            mid, normal = parse_units(pin_dict['middle']), pin_dict['normal']
-            rect_mid = np.append(mid + normal * gap / 2, [0])
+            width, gap = parse_units([pin_dict["width"], pin_dict["gap"]])
+            mid, normal = parse_units(pin_dict["middle"]), pin_dict["normal"]
+            chip_name = self.design.components[comp].options.chip
+            qc_chip_z = parse_units(self.design.get_chip_z(chip_name))
+            rect_mid = np.append(mid + normal * gap / 2, [qc_chip_z])
             # Assumption: pins only point in x or y directions
             # If this assumption is not satisfied, draw_rect_center no longer works -> must use draw_polyline
-            endcap_name = f'endcap_{comp}_{pin}'
+            endcap_name = f"endcap_{comp}_{pin}"
             if abs(normal[0]) > abs(normal[1]):
                 self.modeler.draw_rect_center(rect_mid,
                                               x_size=gap,
@@ -1279,7 +1329,7 @@ def add_endcaps(self, open_pins: Union[list, None] = None):
                                               x_size=width + 2 * gap,
                                               y_size=gap,
                                               name=endcap_name)
-            self.chip_subtract_dict[pin_dict['chip']].add(endcap_name)
+            self.chip_subtract_dict[pin_dict["chip"]].add(endcap_name)
 
     def get_chip_names(self) -> List[str]:
         """
@@ -1289,28 +1339,28 @@ def get_chip_names(self) -> List[str]:
             List[str]: Chips to render.
         """
         if self.case == 2:  # One or more components not in QDesign.
-            self.logger.warning('One or more components not found.')
+            self.logger.warning("One or more components not found.")
             return []
         chip_names = set()
         if self.case == 1:  # All components rendered.
             comps = self.design.components
             for qcomp in comps:
-                if 'chip' not in comps[qcomp].options:
+                if "chip" not in comps[qcomp].options:
                     self.chip_designation_error()
                     return []
-                elif comps[qcomp].options.chip != 'main':
-                    self.chip_not_main()
-                    return []
+                # elif comps[qcomp].options.chip != 'main':
+                #    self.chip_not_main()
+                #    return []
                 chip_names.add(comps[qcomp].options.chip)
         else:  # Strict subset rendered.
             icomps = self.design._components
             for qcomp_id in self.qcomp_ids:
-                if 'chip' not in icomps[qcomp_id].options:
+                if "chip" not in icomps[qcomp_id].options:
                     self.chip_designation_error()
                     return []
-                elif icomps[qcomp_id].options.chip != 'main':
-                    self.chip_not_main()
-                    return []
+                # elif icomps[qcomp_id].options.chip != 'main':
+                #    self.chip_not_main()
+                #    return []
                 chip_names.add(icomps[qcomp_id].options.chip)
         return list(chip_names)
 
@@ -1342,10 +1392,10 @@ def get_min_bounding_box(self) -> Tuple[float]:
         Returns:
             Tuple[float]: min x, min y, max x, and max y coordinates of bounding box.
         """
-        min_x_main = min_y_main = float('inf')
-        max_x_main = max_y_main = float('-inf')
+        min_x_main = min_y_main = float("inf")
+        max_x_main = max_y_main = float("-inf")
         if self.case == 2:  # One or more components not in QDesign.
-            self.logger.warning('One or more components not found.')
+            self.logger.warning("One or more components not found.")
         elif self.case == 1:  # All components rendered.
             for qcomp in self.design.components:
                 min_x, min_y, max_x, max_y = self.design.components[
@@ -1375,25 +1425,73 @@ def render_chips(self,
             box_plus_buffer (bool, optional): Whether or not to use a box plus buffer. Defaults to True.
         """
         chip_list = self.get_chip_names()
-        if box_plus_buffer:  # Get bounding box of components first
-            min_x_main, min_y_main, max_x_main, max_y_main = parse_units(
-                self.get_min_bounding_box())
-            self.cw_x = max_x_main - min_x_main  # chip width along x
-            self.cw_y = max_y_main - min_y_main  # chip width along y
-            self.cw_x += 2 * parse_units(self._options['x_buffer_width_mm'])
-            self.cw_y += 2 * parse_units(self._options['y_buffer_width_mm'])
-            self.cc_x = (max_x_main + min_x_main) / 2  # x coord of chip center
-            self.cc_y = (max_y_main + min_y_main) / 2  # y coord of chip center
-        else:  # Adhere to chip placement and dimensions in QDesign
-            p = self.design.get_chip_size(
-                'main')  # x/y center/width same for all chips
-            self.cw_x, self.cw_y, _ = parse_units(
-                [p['size_x'], p['size_y'], p['size_z']])
-            self.cc_x, self.cc_y, _ = parse_units(
-                [p['center_x'], p['center_y'], p['center_z']])
+        # added this quick hack for the case of flipchip device.
+        # current self.get_chip_names only renders chips whose components are to be rendered.
+        # so if you happen to only draw a qubit only, then the other chip (C_chip) does not get rendered because get_chip_names only returns Q_chip
+        # I hope this gets a prettier fix in the future
+        if self.design._metadata.design_name == 'FlipChip_Device':
+            chip_list = self.design.chips.keys()
+        self.cw_x, self.cw_y = Dict(), Dict()
+        self.cc_x, self.cc_y = Dict(), Dict()
+
         for chip_name in chip_list:
-            self.render_chip(chip_name, draw_sample_holder and
-                             (chip_name == 'main'))
+            if box_plus_buffer:  # Get bounding box of components first
+                min_x_main, min_y_main, max_x_main, max_y_main = parse_units(
+                    self.get_min_bounding_box())
+                self.cw_x.update({chip_name: max_x_main - min_x_main
+                                 })  # chip width along x
+                self.cw_y.update({chip_name: max_y_main - min_y_main
+                                 })  # chip width along y
+                self.cw_x[chip_name] += 2 * parse_units(
+                    self._options["x_buffer_width_mm"])
+                self.cw_y[chip_name] += 2 * parse_units(
+                    self._options["y_buffer_width_mm"])
+                self.cc_x.update({chip_name:
+                    (max_x_main + min_x_main) / 2})  # x coord of chip center
+                self.cc_y.update({chip_name:
+                    (max_y_main + min_y_main) / 2})  # y coord of chip center
+            else:  # Adhere to chip placement and dimensions in QDesign
+                p = self.design.get_chip_size(
+                    chip_name)  # x/y center/width same for all chips
+                self.cw_x.update({chip_name: parse_units(p["size_x"])})
+                self.cw_y.update({chip_name: parse_units(p["size_y"])})
+                self.cc_x.update({chip_name: parse_units(p["center_x"])})
+                self.cc_y.update({chip_name: parse_units(p["center_y"])})
+                # self.cw_x, self.cw_y, _ = parse_units(
+                #    [p['size_x'], p['size_y'], p['size_z']])
+                # self.cc_x, self.cc_y, _ = parse_units(
+                #    [p['center_x'], p['center_y'], p['center_z']])
+            self.render_chip(chip_name, draw_sample_holder)
+
+        if draw_sample_holder:  # HFSS
+            if "sample_holder_top" in self.design.variables.keys():
+                p = self.design.variables
+            else:
+                p = self.design.get_chip_size(chip_list[0])
+            vac_height = parse_units(
+                [p["sample_holder_top"], p["sample_holder_bottom"]])
+            # very simple algorithm to build the vacuum box. It could be made better in the future
+            # assuming that both
+            cc_x = np.array([item for item in self.cc_x.values()])
+            cc_y = np.array([item for item in self.cc_y.values()])
+            cw_x = np.array([item for item in self.cw_x.values()])
+            cw_y = np.array([item for item in self.cw_y.values()])
+
+            cc_x_left, cc_x_right = np.min(cc_x - cw_x / 2), np.max(cc_x +
+                                                                    cw_x / 2)
+            cc_y_left, cc_y_right = np.min(cc_y - cw_y / 2), np.max(cc_y +
+                                                                    cw_y / 2)
+
+            cc_x = (cc_x_left + cc_x_right) / 2
+            cc_y = (cc_y_left + cc_y_right) / 2
+            cw_x = cc_x_right - cc_x_left
+            cw_y = cc_y_right - cc_y_left
+
+            vacuum_box = self.modeler.draw_box_center(
+                [cc_x, cc_y, (vac_height[0] - vac_height[1]) / 2],
+                [cw_x, cw_y, sum(vac_height)],
+                name="sample_holder",
+            )
 
     def render_chip(self, chip_name: str, draw_sample_holder: bool):
         """
@@ -1406,30 +1504,28 @@ def render_chip(self, chip_name: str, draw_sample_holder: bool):
         ansys_options = dict(transparency=0.0)
         ops = self.design._chips[chip_name]
         p = self.design.get_chip_size(chip_name)
-        z_coord, height = parse_units([p['center_z'], p['size_z']])
-        plane = self.modeler.draw_rect_center([self.cc_x, self.cc_y, z_coord],
-                                              x_size=self.cw_x,
-                                              y_size=self.cw_y,
-                                              name=f'ground_{chip_name}_plane',
-                                              **ansys_options)
+        z_coord, height = parse_units([p["center_z"], p["size_z"]])
+        plane = self.modeler.draw_rect_center(
+            [self.cc_x[chip_name], self.cc_y[chip_name], z_coord],
+            x_size=self.cw_x[chip_name],
+            y_size=self.cw_y[chip_name],
+            z_size=0,
+            name=f"ground_{chip_name}_plane",
+            **ansys_options,
+        )
         whole_chip = self.modeler.draw_box_center(
-            [self.cc_x, self.cc_y, height / 2], [self.cw_x, self.cw_y, -height],
+            [self.cc_x[chip_name], self.cc_y[chip_name], z_coord + height / 2],
+            [self.cw_x[chip_name], self.cw_y[chip_name], -height],
             name=chip_name,
-            material=ops['material'],
+            material=ops["material"],
             color=(186, 186, 205),
             transparency=0.2,
-            wireframe=False)
-        if draw_sample_holder:  # HFSS
-            vac_height = parse_units(
-                [p['sample_holder_top'], p['sample_holder_bottom']])
-            vacuum_box = self.modeler.draw_box_center(
-                [self.cc_x, self.cc_y, (vac_height[0] - vac_height[1]) / 2],
-                [self.cw_x, self.cw_y, sum(vac_height)],
-                name='sample_holder')
+            wireframe=False,
+        )
         if self.chip_subtract_dict[chip_name]:
             # Any layer which has subtract=True qgeometries will have a ground plane
             # TODO: Material property assignment may become layer-dependent.
-            self.assign_perfE.append(f'ground_{chip_name}_plane')
+            self.assign_perfE.append(f"ground_{chip_name}_plane")
 
     def subtract_from_ground(self):
         """For each chip, subtract all "negative" shapes residing on its
@@ -1437,8 +1533,9 @@ def subtract_from_ground(self):
         for chip, shapes in self.chip_subtract_dict.items():
             if shapes:
                 import pythoncom
+
                 try:
-                    self.modeler.subtract(f'ground_{chip}_plane', list(shapes))
+                    self.modeler.subtract(f"ground_{chip}_plane", list(shapes))
                 except pythoncom.com_error as error:
                     print("com_error: ", error)
                     hr, msg, exc, arg = error.args
@@ -1456,11 +1553,12 @@ def add_mesh(self):
         """Add mesh to all elements in self.assign_mesh."""
         if self.assign_mesh:
             self.modeler.mesh_length(
-                'small_mesh',
+                "small_mesh",
                 self.assign_mesh,
-                MaxLength=self._options['max_mesh_length_jj'])
+                MaxLength=self._options["max_mesh_length_jj"],
+            )
 
-    #Still implementing
+    # Still implementing
     def auto_wirebonds(self, table):
         """
         Adds wirebonds to the Ansys model for path elements where;
@@ -1475,48 +1573,49 @@ def auto_wirebonds(self, table):
         """
         norm_z = np.array([0, 0, 1])
 
-        wb_threshold = parse_units(self._options['wb_threshold'])
-        wb_offset = parse_units(self._options['wb_offset'])
+        wb_threshold = parse_units(self._options["wb_threshold"])
+        wb_offset = parse_units(self._options["wb_offset"])
 
-        #selecting only the qgeometry which meet criteria
-        wb_table = table.loc[table['hfss_wire_bonds'] == True]
-        wb_table2 = wb_table.loc[wb_table['subtract'] == True]
+        # selecting only the qgeometry which meet criteria
+        wb_table = table.loc[table["hfss_wire_bonds"] == True]
+        wb_table2 = wb_table.loc[wb_table["subtract"] == True]
 
-        #looping through each qgeometry
+        # looping through each qgeometry
         for _, row in wb_table2.iterrows():
-            geom = row['geometry']
-            width = row['width']
-            #looping through the linestring of the path to determine where WBs should be
+            geom = row["geometry"]
+            width = row["width"]
+            # looping through the linestring of the path to determine where WBs should be
             for index, i_p in enumerate(geom.coords[:-1], start=0):
                 j_p = np.asarray(geom.coords[:][index + 1])
                 vert_distance = parse_units(distance.euclidean(i_p, j_p))
                 if vert_distance > wb_threshold:
-                    #Gets number of wirebonds to fit in section of path
+                    # Gets number of wirebonds to fit in section of path
                     wb_count = int(vert_distance // wb_threshold)
-                    #finds the position vector
+                    # finds the position vector
                     wb_pos = (j_p - i_p) / (wb_count + 1)
-                    #gets the norm vector for finding the orthonormal of path
+                    # gets the norm vector for finding the orthonormal of path
                     wb_vec = wb_pos / np.linalg.norm(wb_pos)
-                    #finds the orthonormal (for orientation)
+                    # finds the orthonormal (for orientation)
                     wb_perp = np.cross(norm_z, wb_vec)[:2]
-                    #finds the first wirebond to place (rest are in the loop)
+                    # finds the first wirebond to place (rest are in the loop)
                     wb_pos_step = parse_units(wb_pos + i_p) + (wb_vec *
                                                                wb_offset)
-                    #Other input values could be modified, kept to minimal selection for automation
-                    #for the time being. Loops to place N wirebonds based on length of path section.
+                    # Other input values could be modified, kept to minimal selection for automation
+                    # for the time being. Loops to place N wirebonds based on length of path section.
                     for wb_i in range(wb_count):
                         self.modeler.draw_wirebond(
                             pos=wb_pos_step + parse_units(wb_pos * wb_i),
                             ori=wb_perp,
-                            width=parse_units(width * self._options['wb_size']),
+                            width=parse_units(width * self._options["wb_size"]),
                             height=parse_units(width *
-                                               self._options['wb_size']),
+                                               self._options["wb_size"]),
                             z=0,
-                            wire_diameter='0.015mm',
+                            wire_diameter="0.015mm",
                             NumSides=6,
-                            name='g_wb',
-                            material='pec',
-                            solve_inside=False)
+                            name="g_wb",
+                            material="pec",
+                            solve_inside=False,
+                        )
 
     def clean_active_design(self):
         """Remove all elements from Ansys Modeler."""
@@ -1524,7 +1623,7 @@ def clean_active_design(self):
             if self.pinfo.get_all_object_names():
                 project_name = self.pinfo.project_name
                 design_name = self.pinfo.design_name
-                select_all = ','.join(self.pinfo.get_all_object_names())
+                select_all = ",".join(self.pinfo.get_all_object_names())
 
                 # self.pinfo.design does not work, thus the following line
                 oDesktop = self.pinfo.design.parent.parent._desktop
@@ -1550,7 +1649,7 @@ def set_variables(self, variables: Dict):
                     self.pinfo.design.set_variable(k, v)
             else:
                 self.logger.warning(
-                    'Please create a design before setting variables, otherwise all variables will be set to 0 during rendering by default.'
+                    "Please create a design before setting variables, otherwise all variables will be set to 0 during rendering by default."
                 )
 
     # TODO: epr methods below should not be in the renderer, but in the analysis files.
@@ -1601,15 +1700,15 @@ def epr_get_stored_energy(self,
         elif self.epr_distributed_analysis is None:
             self.epr_start()
 
-        if self.pinfo.dissipative['dielectrics_bulk'] is not None:
+        if self.pinfo.dissipative["dielectrics_bulk"] is not None:
             eprd = self.epr_distributed_analysis
             energy_elec = eprd.calc_energy_electric()
             energy_elec_substrate = eprd.calc_energy_electric(
-                None, self.pinfo.dissipative['dielectrics_bulk'][0])
+                None, self.pinfo.dissipative["dielectrics_bulk"][0])
             energy_mag = eprd.calc_energy_magnetic()
 
             return energy_elec, energy_elec_substrate, energy_mag
-        self.logger.error('dielectrics_bulk needs to be defined')
+        self.logger.error("dielectrics_bulk needs to be defined")
 
     def epr_run_analysis(self,
                          junctions: dict = None,
@@ -1641,7 +1740,7 @@ def epr_spectrum_analysis(self, cos_trunc: int = 8, fock_trunc: int = 7):
                                                          fock_trunc=fock_trunc)
 
     def epr_report_hamiltonian(self,
-                               swp_variable: str = 'variation',
+                               swp_variable: str = "variation",
                                numeric=True):
         """Reports in a markdown friendly table the hamiltonian results.
 
diff --git a/qiskit_metal/renderers/renderer_ansys/hfss_renderer.py b/qiskit_metal/renderers/renderer_ansys/hfss_renderer.py
index d2170bb14..6406d302f 100644
--- a/qiskit_metal/renderers/renderer_ansys/hfss_renderer.py
+++ b/qiskit_metal/renderers/renderer_ansys/hfss_renderer.py
@@ -12,6 +12,8 @@
 # copyright notice, and modified files need to carry a notice indicating
 # that they have been altered from the originals.
 
+# modified by Chalmers/SK/20210621
+
 from collections import defaultdict
 from pathlib import Path
 from typing import Union, Tuple
@@ -368,7 +370,7 @@ def render_junction_inductor(self, qgeom: pd.Series, xmin: float,
         # Draw rectangle for inductor.
         self.logger.debug(f'Drawing a rectangle: {inductor_name}')
         poly_ansys = self.modeler.draw_rect_corner([xmin, ymin, z], xmax - xmin,
-                                                   ymax - ymin, z,
+                                                   ymax - ymin, 0,
                                                    **ansys_options)
         poly_ansys.make_rlc_boundary(axis,
                                      l=qgeom['hfss_inductance'],
diff --git a/tutorials/FlipChip_designtutorial.ipynb b/tutorials/FlipChip_designtutorial.ipynb
new file mode 100644
index 000000000..d3530c40b
--- /dev/null
+++ b/tutorials/FlipChip_designtutorial.ipynb
@@ -0,0 +1,401 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "id": "f96003c3",
+   "metadata": {},
+   "source": [
+    "# Flip-Chip Devices in Qiskit Metal\n",
+    "\n",
+    "In this notebook, we will construct a device made of a coupled qubit - resonator system in a flip-chip environment. After that, we proceed to analyse them using the EPR technique available in the qiskit metal analyses folder.\n",
+    "\n",
+    "The flip-chip device in this notebook is very basic, consisting of two chips separated by a specific distance (that we shall specify later). There are different layout flavours, one way is to place the resonator at the 'bottom' chip or so-called 'control' chip, and the qubit on the 'top' chip or so-called 'qubit' chip.\n"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "1ea0705e",
+   "metadata": {},
+   "source": [
+    "## 1. Building a Flip-Chip Device Design\n",
+    "We use another class called the <code>DesignFlipChip</code> class. The design workflow is very similar to the one for building planar devices (cf. <code>DesignPlanar</code>), except that there are two chips, and you need to specify which chip you want to draw the device on. At the moment, the two chips are called the 'C_chip' and the 'Q_chip'."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "0d41a1db",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "%reload_ext autoreload\n",
+    "%autoreload 2"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "1c02013c",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import qiskit_metal as metal\n",
+    "from qiskit_metal import designs, draw\n",
+    "from qiskit_metal import MetalGUI, Dict, Headings\n",
+    "from qiskit_metal.qlibrary.qubits.transmon_cross import TransmonCross\n",
+    "from qiskit_metal.qlibrary.resonator.readoutres_fc import ReadoutResFC\n",
+    "\n",
+    "# suppress deprecation warnings\n",
+    "import warnings\n",
+    "warnings.filterwarnings(\"ignore\", category=Warning)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "88f78db4",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Initialise design\n",
+    "design = designs.DesignFlipChip()\n",
+    "# Specify design name\n",
+    "design.metadata['design_name'] = 'FlipChip_Device'\n",
+    "# launch GUI\n",
+    "gui = MetalGUI(design)\n",
+    "# Allow running the same cell here multiple times to overwrite changes\n",
+    "design.overwrite_enabled = True"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "a689d06b",
+   "metadata": {},
+   "source": [
+    "Next, we draw an Xmon on the Q-chip, and a readout resonator on the C-chip."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "1a4c6ad6",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# delete existing components\n",
+    "design.delete_all_components()\n",
+    "\n",
+    "# draw an Xmon on the Q_chip. Notice that I have defined one more item called chip and set it to be the 'Q_chip'\n",
+    "options_cpads = Dict(\n",
+    "        connector_type = '0', # claw type\n",
+    "        claw_length = '30um',\n",
+    "        ground_spacing = '5um',\n",
+    "        claw_width = '10um',\n",
+    "        claw_gap = '6um')\n",
+    "options = Dict(\n",
+    "    chip = 'Q_chip',\n",
+    "    cross_width = '20um',\n",
+    "    cross_length = '150um',\n",
+    "    cross_gap = '20um',\n",
+    "    connection_pads = Dict(\n",
+    "        claw_west = Dict(connector_location= '180', **options_cpads)))\n",
+    "\n",
+    "# position the center of the Xmon at (0,0)\n",
+    "q1_x = '0.0 mm'\n",
+    "q1_y = '0.0 mm'\n",
+    "\n",
+    "# build the device, positioned at (q1_x, q1_y)\n",
+    "q1 = TransmonCross(design, 'Q1', options = Dict(pos_x=q1_x, pos_y=q1_y, **options))\n",
+    "\n",
+    "# rebuild in gui\n",
+    "gui.rebuild()\n",
+    "gui.autoscale()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "ddbc50d8",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# draw a readout resonator on the C_chip. Note that we specify the chip='C_chip'.\n",
+    "# The readout resonator can me made into any shape. Here I opted to use a lambda/4 cpw-based resonator.\n",
+    "# The open-ended end of the resonator has a circular shape with a radius of readout_radius. Change this radius to change the qubit-resonator coupling strength.\n",
+    "# The cpw_width and cpw_gap are the size of the signal line and the ground gap of the cpw transmission line.\n",
+    "# The parameters readout_l1 ... l5, are the lengths of various parts of the cpw.\n",
+    "# arc_step is the length of a line that approximates an arc. \n",
+    "options = Dict(\n",
+    "    chip = 'C_chip',\n",
+    "    readout_radius = '20 um',\n",
+    "    readout_cpw_width = '10 um',\n",
+    "    readout_cpw_gap = '10 um',\n",
+    "    readout_cpw_turnradius = '50 um',\n",
+    "    readout_l1 = '600 um',\n",
+    "    readout_l2 = '200 um',\n",
+    "    readout_l3 = '300 um',\n",
+    "    readout_l4 = '150 um',\n",
+    "    readout_l5 = '550 um',\n",
+    "    arc_step = '5 um')\n",
+    "\n",
+    "# the resonator is set to have its origin at the center of the circular patch.\n",
+    "# So we set the qubit and the resonator to share the same coordinate (q1_x, q1_y)\n",
+    "r1 = ReadoutResFC(design, 'R1', options = Dict(pos_x = q1_x, pos_y = q1_y, **options))\n",
+    "\n",
+    "# rebuild in gui\n",
+    "gui.rebuild()\n",
+    "gui.autoscale()\n"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "17affa51",
+   "metadata": {},
+   "source": [
+    "## 2. Preparation Prior to Rendering in ANSYS\n"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "1678915d",
+   "metadata": {},
+   "source": [
+    "As described before, we have specified two chips in the <code>FlipChipDesign</code> class, i.e. C_chip and Q_chip. This information is stored in the chips variables. "
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "b35cb7f9",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "design.chips"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "3d7929e4",
+   "metadata": {},
+   "source": [
+    "It is important to note that in terms of component rendering in ANSYS, the components are drawn on an XY plane with its Z-coordinate being defined by 'center_z'. For the design object we just created, C_chip components are drawn at z=0, and the Q_chip components are drawn at 20 um."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "f566f85f",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "design.chips['C_chip']['size']['center_z']"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "9007cb94",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "design.chips['Q_chip']['size']['center_z']"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "a05eeba7",
+   "metadata": {},
+   "source": [
+    "Next, we set the Q_chip to be at a distance of, say 10 um, from the C_chip."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "dd6060f0",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "design.chips['Q_chip']['size']['center_z'] = '10 um'\n",
+    "design.chips['Q_chip']['size']['center_z']"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "5f8e095c",
+   "metadata": {},
+   "source": [
+    "It is possible that you would like to assign a new material to the chip substrate. In this case, I'd usually define a new material in the library instead of modifying the existing one. If this is not of interest, feel free to skip this part.\n",
+    "\n",
+    "Say, we would like to have a Silicon substrate with a different dielectric constant (11.45 instead of the default value of 11.9). Let's call this new material 'Si_11.45'.\n",
+    "\n",
+    "To do this, first we add the new material to the ANSYS material library (either manually or scripted like what we did below), and then specify the new material in the design object."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "025e0bfa",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# new material properties\n",
+    "newMaterial_name = 'Si_11.45'\n",
+    "newMaterial_permittivity = '11.45'\n",
+    "newMaterial_losstangent = '1e-7'"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "6eb99015",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# add the new material via scripting\n",
+    "# uncomment if needed\n",
+    "from pyEPR import ansys, project_info\n",
+    "# get the handles to the ANSYS system\n",
+    "ans_prj = project_info.ProjectInfo()\n",
+    "oProject = ans_prj.project._project\n",
+    "defmanager = oProject.GetDefinitionManager()\n",
+    "# check if the material exists\n",
+    "if not defmanager.DoesMaterialExist(newMaterial_name):\n",
+    "    defmanager.AddMaterial(\n",
+    "        [\n",
+    "            \"NAME:%s\"%(newMaterial_name),\n",
+    "            \"CoordinateSystemType:=\", \"Cartesian\",\n",
+    "            \"BulkOrSurfaceType:=\", 1,\n",
+    "            [\n",
+    "                    \"NAME:PhysicsTypes\",\n",
+    "                    \"set:=\", [\"Electromagnetic\"]\n",
+    "            ],\n",
+    "            \"permittivity:=\", \"%s\"%(newMaterial_permittivity),\n",
+    "            \"dielectric_loss_tangent:=\", \"%s\"%(newMaterial_losstangent)\n",
+    "        ])\n",
+    "# release the handle to ANSYS\n",
+    "ans_prj.project.release()\n",
+    "ans_prj.desktop.release()\n",
+    "ans_prj.app.release()\n",
+    "ansys.release()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "1b1af6d6",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Specify the new material assigment for the chip\n",
+    "design.chips['C_chip']['material'] = newMaterial_name\n",
+    "design.chips['Q_chip']['material'] = newMaterial_name"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "2493e788",
+   "metadata": {},
+   "source": [
+    "## 3.  Run EPR Analysis\n",
+    "We proceed to run the EPR analysis. This follows closely tutorial 4.02 Eigenmode and EPR.\n",
+    "\n",
+    "First, we initialise an EPRanalysis class object, and update the ANSYS simulation setup."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "548873c3",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "from qiskit_metal.analyses.quantization import EPRanalysis\n",
+    "eig_fc = EPRanalysis(design, \"hfss\")\n",
+    "eig_fc.sim.setup"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "ac123d75",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# update setup setting\n",
+    "eig_fc.sim.setup.max_passes = 20\n",
+    "eig_fc.sim.setup.vars.Lj = '10 nH'\n",
+    "# since we are analysing both the qubit and the resonator, we set the number of modes to be 2\n",
+    "eig_fc.sim.setup.n_modes = 2\n",
+    "# if there is not enough computational power, you can set a less stringent max_delta_f\n",
+    "eig_fc.sim.setup.max_delta_f = 1 \n",
+    "eig_fc.sim.setup.min_freq_ghz = 1.5\n",
+    "# pass the name of the junction (for the EPR analysis purpose)\n",
+    "eig_fc.setup.junctions.jj.rect = 'JJ_rect_Lj_Q1_rect_jj'\n",
+    "eig_fc.setup.junctions.jj.line = 'JJ_Lj_Q1_rect_jj_'\n",
+    "\n",
+    "eig_fc.sim.setup"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "32351948",
+   "metadata": {},
+   "source": [
+    "Next, we run the ANSYS simulation and then the EPR analysis."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "491db648",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "eig_fc.sim.run(name=\"qubitres\", components=['Q1', 'R1'], open_terminations=[], box_plus_buffer = True)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "b973d2bc",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "eig_fc.setup.dissipatives.dielectrics_bulk = ['Q_chip', 'C_chip']\n",
+    "eig_fc.run_epr()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "6f56cc50",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "eig_fc.sim.close()"
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "pymetal",
+   "language": "python",
+   "name": "pymetal"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.9.7"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}


Test patch:
diff --git a/qiskit_metal/tests/test_qlibrary_2_options.py b/qiskit_metal/tests/test_qlibrary_2_options.py
index 5a2d168c5..c1db9fdd6 100644
--- a/qiskit_metal/tests/test_qlibrary_2_options.py
+++ b/qiskit_metal/tests/test_qlibrary_2_options.py
@@ -482,7 +482,7 @@ def test_qlibrary_transmon_cross_options(self):
         _options = _transmon_cross.default_options
 
         # Test all elements of the result data against expected data
-        self.assertEqual(len(_options), 4)
+        self.assertEqual(len(_options), 5)
         self.assertEqual(_options['cross_width'], '20um')
         self.assertEqual(_options['cross_length'], '200um')
         self.assertEqual(_options['cross_gap'], '20um')