text: # -*- 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.
  """This is the main module that defines what an element is in Qiskit Metal.
  
  See the docstring of `QGeometryTables`
  """
  
  import inspect
  import logging
  import pandas as pd
  import shapely
  
  from typing import TYPE_CHECKING
  from typing import Dict as Dict_
  from typing import List, Tuple, Union, Any, Iterable
  from geopandas import GeoDataFrame, GeoSeries
  
  from .. import Dict
  from ..draw import BaseGeometry
  from qiskit_metal.draw.utility import round_coordinate_sequence
  
  from shapely.geometry.multipolygon import MultiPolygon  #to avoid MultiPolygons
  from .. import config
  if not config.is_building_docs():
      from qiskit_metal.toolbox_python.utility_functions import get_range_of_vertex_to_not_fillet, data_frame_empty_typed
  
  if TYPE_CHECKING:
      from ..qlibrary.core import QComponent
      from ..designs import QDesign
  
  __all__ = ['is_qgeometry_table', 'QGeometryTables']  # , 'ElementTypes']
  
  # from collections import OrderedDict
  # dict are ordered in Python 3.6+ by default, this is for backward compatibility
  
  # class ElementTypes:
  #     """
  #     Types of qgeometry
  #         positive : qgeometry that are positive mask
  #         negative : qgeometry that will be subtracted from the chip ground plane
  #         helper   : qgeometry that are only used in a helper capacity,
  #                    such as labels or mesh rectangles
  #     """
  #     positive = 0
  #     negative = 1
  #     helper   = 2
  
  
  def is_qgeometry_table(obj):
      """Check if an object is a Metal BaseElementTable, i.e., an instance of
      `QGeometryTables`.
  
      The problem is that the `isinstance` built-in method fails
      when this module is reloaded.
  
      Args:
          obj (object): Test this object
  
      Returns:
          bool: True if is a Metal element
      """
      if isinstance(obj, Dict):
          return False
  
      return hasattr(obj, '__i_am_qgeometry_table__')
  
  
  #############################################################################
  #
  # Dictionary that specifies the column names of various element tables.
  #
  
  # TODO: implement data types in construction of tables?
  # TODO: when a copy over the table and manipulate ehtmee
  # i seem to loose the assignments to bool etc.
  
  ELEMENT_COLUMNS = dict(
  
      ################################################
      # DO NOT MODIFY THE base DICTIONARY.
      # This is for Metal API use only.
      # To add a new element type, add a new key below.
      base=dict(
          component=str,  # Unique ID of the component to which the element belongs
          name=str,  # name of the element
          geometry=object,  # shapely object
          layer=int,  # gds type of layer
          subtract=bool,  # do we subtract from the ground place of the chip
          helper=bool,  # helper or not
          chip=str,  # chip name
          # type=str,  # metal, helper.   poly=10 or path=11
          __renderers__=dict(
              # ADD specific renderers here, all renderes must register here.
              # hfss = dict( ... ) # pick names as hfss_name
              # hfss=dict(
              #     boundary_name=str,
              #     material=str,
              #     perfectE=bool
              # ),
              # gds=dict(
              #     type=str,
              #     color=str,
              # )
          )),
  
      ################################################
      # Specifies a path, such as a CPW.
      # Ideas: chamfer
      path=dict(
          width=float,
          fillet=object,  # TODO: not decided yet how to represent this
          __renderers__=dict()),
  
      ################################################
      # Specifies a polygon
      # Ideas: chamfer
      poly=dict(
          fillet=object,  # TODO: not decided yet how to represent this
          __renderers__=dict()),
      ################################################
      # Specifies a junction as a 2 point line and width
      # This should provide enough information so as to
      # - render a sheet with inductance (from renderer options) + a vector for EPR
      # - generate ports (edge ports?) for Z analysis
      # - provice bounding box dimensions for the p-cell of ebeam junction layout
      #       for GDS renderer
      junction=dict(width=float, __renderers__=dict()),
      ################################################
      # Specifies a curved object, such as a circle. Perhaps as a buffered point
      # Not yet implemented
      # curved = dict(
      # __renderers__= dict(
      # )
      # )
  )
  """Dictionary that specifies the column names of various element tables."""
  
  #############################################################################
  #
  # Class to create, store, and handle element tables.
  #
  TRUE_BOOLS = [True, 'True', 'true', 'Yes', 'yes', '1', 1]
  
  
  class QGeometryTables(object):
      """Class to create, store, and handle element tables.
  
      A regular user would not need to create tables themselves.
      This is handled automatically by the design creation and plugins.
  
      Structure:
          A component, such as a qubit, is a collection of qgeometry.
          For example, an element includes a rectangle, a cpw path, or a more general polygon.
  
          An element is a row in a table.
  
          All qgeometry of a type (Path or Polygon, or otherwise) are stored in a
          single table of their element type.
  
          All qgeometry of the same kind are stored in a table.
          A renderer has to know how to handle all types of qgeometry in order to render them.
  
      For plugin developers:
          In the following, we provide an example that illustrates for plugin developers how
          to add custom qgeometry and custom element properties. For example, we will add, for a renderer
          called hfss, a string property called 'boundary', a bool property called 'perfectE', and a property called 'material'.
  
      For plugin developers, example use:
  
          .. code-block:: python
              :linenos:
              :emphasize-lines: 4,6
  
              import qiskit_metal as metal
  
              design = metal.designs.DesignPlanar()
              design.qgeometry = metal.QGeometryTables(design)
  
              # return the path table - give access to ..
              design.qgeometry['path']
              design.qgeometry.table['path']
  
              # Define interfaces
              design.qgeometry.get_component(
                      component_name,
                      element_name,
                      columns=all or geom or list) # get all elements for components
  
              >>> component	name	geometry	layer	type	chip	subtract	fillet	color	width
  
  
      Now, if we want to add custom qgeometry through two fake renderers called hfss and gds:
  
          .. code-block:: python
              :linenos:
              :emphasize-lines: 1-15
  
              metal.QGeometryTables.add_renderer_extension('hfss', dict(
                  base=dict(
                      boundary=str,
                      perfectE=bool,
                      material=str,
                      )
                  ))
  
              metal.QGeometryTables.add_renderer_extension('gds', dict(
                  path=dict(
                      color=str,
                      pcell=bool,
                      )
                  ))
  
              design = metal.designs.DesignPlanar()
              qgeometry = metal.QGeometryTables(design)
  
              qgeometry.tables['path']
              >>> component	name	geometry	layer	type	chip	subtract	fillet	color	width	hfss_boundary	hfss_perfectE	hfss_material	gds_color	gds_pcell
      """
  
      # Dummy private attribute used to check if an instanciated object is
      # indeed a element table class. The problem is that the `isinstance`
      # built-in method fails when this module is reloaded.
      # Used by `is_element` to check.
      __i_am_qgeometry_table__ = True
  
      ELEMENT_COLUMNS = ELEMENT_COLUMNS
      """
      Table column names to use to create.
      This dict should be updated by renderers.
      """
  
      # For creating names of columns of renderer properties
      name_delimiter = '_'
      """ Delimiter to use when creating names of columns of renderer properties. """
  
      def __init__(self, design: 'QDesign'):
          """The constructor for the `QGeometryTables` class.
  
          Args:
              design: Design in use
          """
          self._design = design
  
          self._tables = Dict()
  
          # Need to call after columns are added by add_renderer_extension is run by all the renderers.
          # self.create_tables()
  
      @property
      def design(self) -> 'QDesign':
          """Return a reference to the parent design object."""
          return self._design
  
      @property
      def logger(self) -> logging.Logger:
          """Return the logger."""
          return self._design.logger
  
      @property
      def tables(self) -> Dict_[str, GeoDataFrame]:
          """The dictionary of tables containing qgeometry.
  
          Returns:
              Dict_[str, GeoDataFrame]: The keys of this dictionary are
              also obtained from `self.get_element_types()`
          """
          return self._tables
  
      @classmethod
      def add_renderer_extension(cls, renderer_name: str, qgeometry: dict):
          """Add renderer element extension to ELEMENT_COLUMNS. Called when the
          load function of a renderer is called.
  
          Args:
              renderer_name (str): Name of renderer
              qgeometry (dict):  dict of dict.  Keys give element type names,
                                such as base, poly, path, etc.
          """
          # Make sure that the base and all other element kinds have this renderer registered
          for element_key in cls.ELEMENT_COLUMNS:
              if not renderer_name in cls.ELEMENT_COLUMNS[element_key][
                      '__renderers__']:
                  cls.ELEMENT_COLUMNS[element_key]['__renderers__'][
                      renderer_name] = dict()
  
          # Now update the dictionaries with all qgeometry that the renderer may have
          for element_key, element_column_ext_dict in qgeometry.items():
  
              # The element the render is specifying is not in the specified qgeometry;
              # then add it. This shouldn't really happen.
              # The rest of the renderer dict keys in __renderers__  are missing for
              # the created type. Avoid doing, else hope it works.
              if not element_key in cls.ELEMENT_COLUMNS:
                  cls.ELEMENT_COLUMNS[element_key] = dict(__renderers__=dict())
  
              # Now add qgeometry
              cls.ELEMENT_COLUMNS[element_key]['__renderers__'][
                  renderer_name].update(element_column_ext_dict)
  
      # could use weakref memorization
      # https://stackoverflow.com/questions/33672412/python-functools-lru-cache-with-class-methods-release-object
      @classmethod
      def get_element_types(cls) -> List[str]:
          """Return the names of the available qgeometry to create. This does not
          include 'base', but is rather such as poly and path.
  
          Returns:
              list(str) : A list of name in self.ELEMENT_COLUMNS
          """
          # TODO: I should probably make this a variable and memorize, only change when qgeometry are added and removed
          # can be slow for performance to look up each time and recalculate, since may call this often
          names = list(cls.ELEMENT_COLUMNS.keys())
          names.remove('base')
          return names
  
      def create_tables(self):
          """Creates the default tables once. Populates the dict 'tables' of
          GeoDataFrame, each with columns corresponding to the types of qgeometry
          defined in ELEMENT_COLUMNS.
  
          Should only be done once when a new design is created.
          """
          self.logger.debug('Creating Element Tables.')
  
          for table_name in self.get_element_types():
              # Create GeoDataFrame with correct columns and d types
              assert isinstance(table_name, str)
              assert table_name.isidentifier()
  
              # Get column names
              # Base names, add concrete names, then add renderer names
  
              # Base names
              columns_base = self.ELEMENT_COLUMNS['base'].copy()
              columns_base_renderers = columns_base.pop('__renderers__')
  
              # Concrete names
              columns_concrete = self.ELEMENT_COLUMNS[table_name].copy()
              columns_concrete_renderer = columns_concrete.pop('__renderers__')
  
              assert isinstance(columns_base_renderers, dict) and\
                  isinstance(columns_concrete_renderer, dict),\
                  "Please make sure that all qgeometry types have __renderers__\
                       which is a dictionary."
  
              # Combine all base names and renderer names
              columns = columns_base
              columns.update(columns_concrete)
              # add renderer columns: base and then concrete
              for renderer_key in columns_base_renderers:
                  columns.update(
                      self._prepend_renderer_names(table_name, renderer_key,
                                                   columns_base_renderers))
                  columns.update(
                      self._prepend_renderer_names(table_name, renderer_key,
                                                   columns_concrete_renderer))
  
              # Validate -- Throws an error if not valid
              self._validate_column_dictionary(table_name, columns)
  
              # Create df with correct column names
              table = GeoDataFrame(data_frame_empty_typed(columns))
              # not used elsewhere, also the name becomes "name" for some reason
              table.name = table_name
  
              # Assign
              self.tables[table_name] = table
  
      def _validate_column_dictionary(self, table_name: str, column_dict: dict):
          """Validate A possible error here is if the user did not pass a valid
          data type.
  
          Throws an error if not valid.
  
          Args:
              table_name (str): Name of element table (e.g., 'poly')
              column_dict (dict): Dictionary to check
  
          Raises:
              TypeError: Data type '' not understood
          """
          __pre = 'ERROR CREATING ELEMENT TABLE FOR DESIGN: \
              \n  ELEMENT_TABLE_NAME = {table_name}\
              \n  KEY                = {k} \
              \n  VALUE              = {v}\n '
  
          # Are these assertions still holding true?
          for k, v in column_dict.items():
              assert isinstance(k, str), __pre.format(**locals()) +\
                  ' Key needs to be a string!'
              assert k.isidentifier(), __pre.format(**locals()) +\
                  ' Key needs to be a valid string identifier!'
              assert inspect.isclass(v), __pre.format(**locals()) +\
                  ' Value needs to be a class!'
  
      def get_rname(self, renderer_name: str, key: str) -> str:
          """Get name for renderer property.
  
          Args:
              renderer_name (str): Name of the renderer
              key (str): Key to get the name for
  
          Returns:
              str: The unique named used as a column in the table
          """
          return renderer_name + self.name_delimiter + key
  
      def _prepend_renderer_names(self, table_name: str, renderer_key: str,
                                  rdict: dict):
          """Prepare all the renderer names.
  
          Args:
              table_name (str): Unused
              renderer_key (str): Key to check for
              rdict (dict): Renderer dictionary
  
          Returns:
              dict: Prepared dictionary
  
          TODO:
              This function has arguments that are unused, fix the function or ditch the unused args
          """
          return {
              self.get_rname(renderer_key, k): v
              for k, v in rdict.get(renderer_key, {}).items()
          }
  
      def add_qgeometry(
              self,
              kind: str,
              component_name: str,
              geometry: dict,
              subtract: bool = False,
              helper: bool = False,
              layer: Union[int, str] = 1,  # chip will be here
              chip: str = 'main',
              **other_options):
          """Main interface to add qgeometries.
  
          Args:
              kind (str): Must be in get_element_types ('path', 'poly', etc.).
              component_name (str): Component name.
              geometry (dict): Dict of shapely geometry.
              subtract (bool): Subtract - passed through.  Defaults to False.
              helper (bool): Helper - passed through.  Defaults to False.
              layer (Union[int, str]): Layer - passed through.  Defaults to 1.
              chip (str): Chip name - passed through.  Defaults to 'main'.
              **other_options (object): Other_options - passed through.
          """
          # TODO: Add unit test
  
          # ensure correct types
          if not isinstance(subtract, bool):
              subtract = subtract in TRUE_BOOLS
          if not isinstance(helper, bool):
              helper = helper in TRUE_BOOLS
  
          if not (kind in self.get_element_types()):
              self.logger.error(
                  f'Creator user error: Unknown element kind=`{kind}`'
                  f'Kind must be in {self.get_element_types()}. This failed for component'
                  f'name = `{component_name}`.\n'
                  f' The call was with subtract={subtract} and helper={helper}'
                  f' and layer={layer}, and options={other_options}')
  
          #Checks if (any) of the geometry are MultiPolygons, and breaks them up into
          #individual polygons. Rounds the coordinate sequences of those values to avoid
          #numerical errors.
          rounding_val = self.design.template_options['PRECISION']
          new_dict = Dict()
          for key, item in geometry.items():
              if isinstance(geometry[key], MultiPolygon):
                  temp_multi = geometry[key]
                  shape_count = 0
                  for shape_temp in temp_multi.geoms:
                      new_dict[key + '_' +
                               str(shape_count)] = round_coordinate_sequence(
                                   shape_temp, rounding_val)
                      shape_count += 1
              else:
                  new_dict[key] = round_coordinate_sequence(item, rounding_val)
  
          geometry = new_dict
  
          # Create options TODO: Might want to modify this (component_name -> component_id)
          # Give warning if length is to be fillet's and not long enough.
          self.check_lengths(geometry, kind, component_name, layer, chip,
                             **other_options)
  
          # Create options
          options = dict(component=component_name,
                         subtract=subtract,
                         helper=helper,
                         layer=int(layer),
                         chip=chip,
                         **other_options)
  
          #replaces line above to generate the options.
          #for keyC in design.qgeometry.tables[kind].columns:
          #    if keyC != 'geometry':
          #        options[keyC] = ???[keyC] -> alternative manner to pass options to the add_qgeometry function?
          #                                       instead have the add_qeometry in baseComponent generate the dict?
  
          #Could we just append rather than make a new table each time? This seems slow
          table = self.tables[kind]
  
          # assert that all names in options are in table columns! TODO: New approach will not be wanting
          #to do this (maybe check that all columns are in options?)
          df = GeoDataFrame.from_dict(geometry,
                                      orient='index',
                                      columns=['geometry'])
          df.index.name = 'name'
          df = df.reset_index()
  
          df = df.assign(**options)
  
          # Set new table. Unfortunately, this creates a new instance. Can just direct append
          self.tables[kind] = table.append(df, sort=False, ignore_index=True)
          # concat([table,df], axis=0, join='outer', ignore_index=True,sort=False,
          #          verify_integrity=False, copy=False)
  
      def check_lengths(self, geometry: shapely.geometry.base.BaseGeometry,
                        kind: str, component_name: str, layer: Union[int, str],
                        chip: str, **other_options):
          """If user wants to fillet, check the line-segments to see if it is too
          short for fillet.
  
          Args:
              geometry (shapely.geometry.base.BaseGeometry): The LineString to investigate.
              kind (str): Name of table, i.e. 'path', 'poly', 'junction, etc
              component_name (str): Is an integer id.
              layer (Union[int, str]): Should be int, but getting a float, will cast to int when used.
              chip (str): Name of chip, i.e. 'main'.
          """
  
          if 'fillet' in other_options.keys():
              # fillet_scalar = 2.0    #Depreciated, moved to toolbox_python.utility_functions
              # fillet_comparison_precision = 9  # used for np.round #Depreciated, moved to toolbox_python.utility_functions
  
              # For now, don't let front end user edit this.
              # if 'fillet_comparison_precision' in other_options.keys():
              #     # The parse_value converts all ints to floats.
              #     fillet_comparison_precision = int(self.parse_value(
              #         other_options['fillet_comparison_precision']))
  
              # Depreciated, moved to toolbox_python.utility_functions
              # if 'fillet_scalar' in other_options.keys():
              #     fillet_scalar = self.parse_value(
              #         other_options['fillet_scalar'])
  
              fillet = other_options['fillet']
  
              for key, geom in geometry.items():
                  if isinstance(geom, shapely.geometry.LineString):
                      coords = list(geom.coords)
                      qdesign_precision = self.design.template_options.PRECISION
                      range_vertex_of_short_segments = get_range_of_vertex_to_not_fillet(
                          coords, fillet, qdesign_precision, add_endpoints=False)
  
                      if len(range_vertex_of_short_segments) > 0:
                          range_string = ""
                          for item in range_vertex_of_short_segments:
  
                              range_string += f'({ item[0]}-{item[1]}) '
                          text_id = self.design._components[component_name]._name
                          self.logger.warning(
                              f'For {kind} table, component={text_id}, key={key}'
                              f' has short segments that could cause issues with fillet. Values in {range_string} '
                              f'are index(es) in shapley geometry.')
  
      def parse_value(self, value: Union[Any, List, Dict, Iterable]) -> Any:
          """Same as design.parse_value. See design for help.
  
          Returns:
              Parsed value of input.
          """
          return self.design.parse_value(value)
  
      def clear_all_tables(self):
          """Clear all the internal tables and all else.
  
          Use when clearing a design and starting from scratch.
          """
          self.tables.clear()
          self.create_tables()  # remake all tables
  
      def delete_component(self, name: str):
          """Delete component by name.
  
          Args:
              name (str): Name of component (case sensitive)
          """
          # TODO: Add unit test
          # TODO: is this the best way to do this, or is there a faster way?
          a_comp = self.design.components[name]
          if a_comp is not None:
              for table_name in self.tables:
                  df = self.tables[table_name]
                  self.tables[table_name] = df[df['component'] != a_comp.id]
  
      def delete_component_id(self, component_id: int):
          """Drop the components within the qgeometry.tables.
  
          Args:
              component_id (int): Unique number to describe the component.
          """
          for table_name in self.tables:
              df_table_name = self.tables[table_name]
              # self.tables[table_name] = df_table_name.drop(df_table_name[df_table_name['component'] == component_id].index)
              self.tables[table_name] = df_table_name[
                  df_table_name['component'] != component_id]
  
      def get_component(
          self,
          name: str,
          table_name: str = 'all'
      ) -> Union[GeoDataFrame, Dict_[str, GeoDataFrame]]:
          """Return the table for just a given component. If all, returns a
          dictionary with kets as table names and tables of components as values.
  
          Args:
              name (str): Name of component (case sensitive).  Defaults to 'all'.
              table_name (str): Element table name ('poly', 'path', etc.).  Defaults to {'all'}.
  
          Returns:
              Union[GeoDataFrame, Dict_[str, GeoDataFrame]]: Either a GeoDataFrame or a dict or GeoDataFrame.
  
          Example usage:
              ```table = pd.concat(qgeometry.get_component('Q1')) # , axis=0```
          """
  
          if table_name == 'all':
              tables = {}
              for table_name in self.get_element_types():
                  tables[table_name] = self.get_component(name, table_name)
              return tables
          else:
              df = self.tables[table_name]
              a_comp = self.design.components[name]
              if a_comp is None:
                  # Component not found.
                  return None
              else:
                  return df[df.component == a_comp.id]
  
              # comp_id = self.design.components[name].id
              # return df[df.component == comp_id]
  
      def get_component_bounds(self,
                               name: str) -> Tuple[float, float, float, float]:
          """Returns a tuple containing minx, miny, maxx, maxy values for the
          bounds of the component as a whole.
  
          Args:
              name (str): Component name
  
          Returns:
              Geometry: Bare element geometry
          """
          gs = self.get_component_geometry(name)  # Pandas GeoSeries
          if len(gs) == 0:
              return (0, 0, 0, 0)
          else:
              return gs.total_bounds
  
      def rename_component(self, component_id: int, new_name: str):
          """Rename component by ID (integer) cast to string format.
  
          Args:
              component_id (int) : ID of component (case sensitive)
              new_name (str) : The new name of the component (case sensitive)
          """
  
          # comp_id = self.design.components[name].id
          component_int_id = int(component_id)
          a_comp = self.design._components[component_int_id]
          if a_comp is None:
              return None
          else:
              # TODO: is this the best way to do this, or is there a faster way?
              for table_name in self.tables:
                  table = self.tables[table_name]
                  table.component[table.component == a_comp.id] = new_name
  
      def get_component_geometry_list(self,
                                      name: str,
                                      table_name: str = 'all'
                                     ) -> List[BaseGeometry]:
          """Return just the bare element geometry (shapely geometry objects) as
          a list, for the selected component.
  
          Args:
              name (str) : Name of component (case sensitive)
              table_name (str) : Element type ('poly', 'path', etc.).
                                 Can also be 'all' to return all. This is the default.
  
          Returns:
              list: List of shapley.geometry objects
          """
          if table_name == 'all':
              qgeometry = []
              for table in self.get_element_types():
                  qgeometry += self.get_component_geometry_list(name, table)
  
          else:
              table = self.tables[table_name]
              comp_id = self.design.components[name].id
              qgeometry = table.geometry[table.component == comp_id].to_list()
  
          return qgeometry
  
      def get_component_geometry(self, name: str) -> GeoSeries:
          """Returns geometry of a given component.
  
          Args:
              name (str) : Name of component (case sensitive)
  
          Returns:
              GeoSeries : Geometry of the component
          """
          comp_id = self.design.components[name].id
          qgeometry = {}
          for table_name in self.get_element_types():
              table = self.tables[table_name]
              qgeometry[table_name] = table.geometry[table.component == comp_id]
          qgeometry = pd.concat(qgeometry)
  
          # when concatenating empty GeoSeries, returns Series (ugly fix)
          if not isinstance(qgeometry, GeoSeries):
              qgeometry = GeoSeries(qgeometry)
  
          return qgeometry
  
      def get_component_geometry_dict(self,
                                      name: str,
                                      table_name: str = 'all'
                                     ) -> List[BaseGeometry]:
          """Return just the bare element geometry (shapely geometry objects) as
          a dict, with key being the names of the qgeometry and the values as the
          shapely geometry, for the selected component.
  
          Args:
              name (str) : Name of component (case sensitive)
              table_name (str) : Element type ('poly', 'path', etc.). Defaults to 'all'.
  
          Returns:
              dict: Bare element geometry
          """
          if table_name == 'all':
              qgeometry = Dict()
              for table in self.get_element_types():
                  qgeometry[table] = self.get_component_geometry_list(name, table)
              return qgeometry  # return pd.concat(qgeometry, axis=0)
  
          else:
              table = self.tables[table_name]
  
              # mask the rows nad get only 2 columns
              comp_id = self.design.components[name].id
              df_comp_id = table.loc[table.component == comp_id,
                                     ['name', 'geometry']]
              df_geometry = df_comp_id.geometry
              df_geometry.index = df_comp_id.name
              return df_geometry.to_dict()
  
      def check_element_type(self,
                             table_name: str,
                             log_issue: bool = True) -> bool:
          """Check if the name `table_name` is in the element tables.
  
          Args:
              table_name (str): Element type ('poly', 'path', etc.) or 'all'
              log_issue (bool): Throw an error in the log if name missing.  Defaults to True.
  
          Returns:
              bool: True if the name is valid, else False
          """
          if not table_name in self.get_element_types() or table_name in 'all':
              if log_issue:
                  self.logger.error(
                      f'Element Tables: Tried to access non-existing element table: `{table_name}`'
                  )
              return False
          else:
              return True
  
      def get_all_unique_layers(self, chip_name: str) -> list:
          """Returns a lit of unique layers for the given chip names.
  
          Args:
              chip_name (str): Name of the chip
  
          Returns:
              list: List of unique layers
          """
          unique_layers = list()
          for table_name in self.design.qgeometry.get_element_types():
              table = self.design.qgeometry.tables[table_name]
              temp = table[table['chip'] == chip_name]
              layers = temp['layer'].unique().tolist()
              unique_layers += layers
          unique_layers = list(set(unique_layers))
  
          return unique_layers
  