# -*- coding: utf-8 -*-
import logging
import os
from re import search
from typing import List, Optional, Tuple, Union
import pandas as pd
from addict import Dict
from qiskit_metal.toolbox_metal.parsing import FALSE_STR, TRUE_STR
[docs]
class LayerStackHandler:
"""Use DataFrame to hold information for multiple chips.
For all the chips, do NOT reuse the layer-number. However, within the dataframe,
we can repeat layer numbers since a layer can have multiple datatypes.
"""
Col_Names = [
"chip_name",
"layer",
"datatype",
"material",
"thickness",
"z_coord",
"fill",
]
def __init__(
self, multi_planar_design: "MultiPlanar", fname: Optional[str] = None
) -> None:
"""Use the information in MultiPlanar design to parse_value
and get the name of filename for layer stack.
Args:
multi_planar_design (MultiPlanar): Class that is child of QDesign.
"""
self.multi_planar_design = multi_planar_design
self.logger = self.multi_planar_design.logger # type: logging.Logger
self.filename_csv_df = None
if (
hasattr(self.multi_planar_design, "ls_filename")
and self.multi_planar_design.ls_filename
):
# populate pandas table from data in this file.
self.filename_csv_df = self.multi_planar_design.ls_filename
elif fname:
self.filename_csv_df = fname
self.ls_df = None
# self.column_names = [
# 'chip_name', 'layer', 'datatype', 'material', 'thickness',
# 'z_coord', 'fill'
# ]
self.layer_stack_default = Dict(
chip_name=["main", "main"],
layer=[1, 3],
datatype=[0, 0],
material=["pec", "silicon"],
thickness=["2um", "-750um"],
z_coord=["0um", "0um"],
fill=["true", "true"],
)
self._init_dataframe()
self.is_layer_data_unique()
def _init_dataframe(self) -> None:
"""Must check if filename for layerstack is valid before trying to import to a pandas table."""
if self.filename_csv_df:
# populate pandas table from data in this file.
abs_path = os.path.abspath(self.filename_csv_df)
if os.path.isfile(abs_path):
self._read_csv_df(abs_path)
else:
self.logger.error(
f"Not able to read file."
f"File:{abs_path} not read. Check the name and path."
)
else:
# enter very basic default data for pandas table.
self.ls_df = pd.DataFrame(data=self.layer_stack_default)
[docs]
def get_layer_datatype_when_fill_is_true(self) -> Optional[dict]:
"""Return layer/datatype rows where ``fill`` is True.
Returns:
dict | None: Mapping of (layer, datatype) -> dict of parsed column
values, or None if no rows have ``fill=True``.
"""
if self.ls_df is None:
abs_path = os.path.abspath(self.filename_csv_df)
self.logger.error(
f"Not able to read file."
f"File:{abs_path} not read. Check the name and path."
)
mask = (
self.ls_df["fill"]
.astype(str)
.str.replace("[']", "", regex=True)
.isin(TRUE_STR)
)
search_result_df = self.ls_df[mask]
if len(search_result_df) > 0:
try:
layer_datatype_fill = dict()
for row in search_result_df.itertuples():
a_key = (row.layer, row.datatype)
layer_datatype_fill[a_key] = dict()
layer_datatype_fill[a_key]["layer"] = row.layer
layer_datatype_fill[a_key]["datatype"] = row.datatype
layer_datatype_fill[a_key]["thickness"] = (
self.multi_planar_design.parse_value(row.thickness.strip("'"))
)
layer_datatype_fill[a_key]["z_coord"] = (
self.multi_planar_design.parse_value(row.z_coord.strip("'"))
)
layer_datatype_fill[a_key]["material"] = row.material.strip("'")
layer_datatype_fill[a_key]["chip_name"] = row.chip_name.strip("'")
return layer_datatype_fill
except Exception as ex:
self.logger.error(
f"User is not using LayerStackHandler.get_layer_datatype_when_fill_is_true correctly. Check your input file."
f"\nERROR:{ex}"
)
return None
[docs]
def get_properties_for_layer_datatype(
self, properties: List[str], layer_number: int, datatype: int = 0
) -> Optional[Tuple[Union[float, str, bool]]]:
"""When user provides a layer and datatype, they can get properties
from the layer_stack file. The allowed options for properties must
be in Col_Names. If any of the properties are not in Col_Names,
None will be returned. Otherwise a Tuple will be returned with properties
in the same order as provided in input variable properties.
Args:
properties (List[str]): The column(s) within the layer stack that you want
for a row based on layer, and datatype.
layer_number (int): The layer number within the column denoted by layer.
datatype (int, optional): The datatype within the column denoted
by datatype. Defaults to 0.
Returns:
Optional[Tuple[Union[float, str, bool]]]: If the search data provided
in the arguments are not in the layer_stack file,
None will be returned. If the search values are found in
layer_stack file, then a Tuple will be returned with the
requested properties in the same order as provided in
input variable denoted by properties.
"""
if not properties:
return None
# Check if parameter is not a subset if Col_Names T.
if not set(properties).issubset(set(self.Col_Names)):
self._warning_properties(properties)
return None
props = Dict()
thickness = 0.0
z_coord = 0.0
material = None
fill_value = None
chip_name = None
if self.ls_df is None:
abs_path = os.path.abspath(self.filename_csv_df)
self.logger.error(
f"Not able to read file."
f"File:{abs_path} not read. Check the name and path."
)
mask = (self.ls_df["layer"] == layer_number) & (
self.ls_df["datatype"] == datatype
)
search_result_df = self.ls_df[mask]
if len(search_result_df) > 0:
try:
thickness = self.multi_planar_design.parse_value(
search_result_df.thickness.iloc[0].strip("'")
)
z_coord = self.multi_planar_design.parse_value(
search_result_df.z_coord.iloc[0].strip("'")
)
material = search_result_df.material.iloc[0].strip("'")
chip_name = search_result_df.chip_name.iloc[0].strip("'")
value = search_result_df.fill.iloc[0].strip("'")
if value in TRUE_STR:
fill_value = True
elif value in FALSE_STR:
fill_value = False
else:
self.logger.warning(
f'The "fill" value is neither True nor False.'
f"You have:{value}. "
f"Will return NULL for fill value."
)
props["thickness"] = thickness
props["z_coord"] = z_coord
props["material"] = material
props["fill"] = fill_value
props["chip_name"] = chip_name
except Exception as ex:
self._warning_search_minus_chip(layer_number, datatype, ex)
result = list()
for item in properties:
result.append(props[item])
return tuple(result)
[docs]
def is_layer_data_unique(self) -> bool:
"""For each layer number make sure the datatypes are unique. A layers can
#have multiple datatypes.
Returns:
bool: True if empty dataframe, True if for each layer, there is ONLY one datatype. Otherwise, False.
"""
layer_nums = self.get_unique_layer_ints()
if layer_nums:
for num in layer_nums:
mask = self.ls_df["layer"] == num
search_result_num = self.ls_df[mask]
if not search_result_num.datatype.is_unique:
self.logger.warning(
f"There WILL BE PROBLEMS since layer {num} does not have unique datatypes."
)
return False
return True
def _read_csv_df(self, abs_path: str) -> None:
# ASSUME that self.filename_csv_df is valid file path and name.
# https://best-excel-tutorial.com/59-tips-and-tricks/485-how-to-display-a-single-quote-in-a-cell
try:
self.ls_df = pd.read_csv(abs_path)
except BaseException as error:
self.logger.warning(
f"Not able to create pandas dataframe."
f"File:{abs_path} not imported. Expected a CSV formatted file."
)
self.logger.error(f"The exception: {error}")
[docs]
def get_unique_chip_names(self) -> set:
"""Get a set of unique names used in the layer_stack dataframe.
Returns:
set: Unique names used in the layer stack used as either default or provided by user.
"""
names = self.ls_df["chip_name"]
result = set(names.str.strip("'"))
return result
[docs]
def get_unique_layer_ints(self) -> set:
"""Get a set of unique layer ints used in the layer_stack dataframe.
Returns:
set: Unique layer numbers used in the layer stack used as either default or provided by user.
"""
layers = self.ls_df["layer"]
return set(layers.unique())
def _warning_properties(self, properties: list):
"""_Give warning if the properties is
Args:
parameters (list): _description_
"""
self.logger.warning(
f"The list for properties: {properties} is not a"
f"subset of expected column names: {self.Col_Names}"
)
def _warning_search(
self, chip_name: str, layer_number: int, data_type: int, ex: Exception
):
"""Give warning when the layerstack pandas table doesn't
have requested chip_name, layer, and datatype information.
Args:
chip_name (str): Name of chip which has the layer of interest.
layer_number (int): The unique layer number through out all chips.
The same layer number can not be used on multiple chips.
datatype (int, optional): Datatype of the layer number.
"""
self.logger.warning(
f"\nERROR: {ex}"
f"\nThere is an error searching in layer_stack dataframe using "
f"chip_name={chip_name}, layer={layer_number}, datatype={data_type}"
)
def _warning_search_minus_chip(
self, layer_number: int, data_type: int, ex: Exception
):
"""Give warning when the layerstack pandas table doesn't
have requested layer, and datatype information.
Args:
layer_number (int): The unique layer number through out all chips.
The same layer number can not be used on multiple chips.
datatype (int, optional): Datatype of the layer number.
"""
self.logger.warning(
f"\nERROR: {ex}"
f"\nThere is an error searching in layer_stack dataframe using "
f" layer={layer_number}, datatype={data_type}"
)
[docs]
def layer_stack_handler_pilot_error(self):
"""The handler will return None if incorrect arguments are passed."""
self.logger.error(
"User is not using LayerStackHandler.get_properties_for_chip_layer_datatype correctly."
)