Source code for squadds.core.analysis

import time
from typing import Any

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd

from squadds.calcs.transmon_cross import TransmonCrossHamiltonian
from squadds.core.analysis_enrichment import (
    extract_coupler_options,
    extract_cpw_options,
    extract_qubit_options,
    fix_cavity_claw_dataframe,
)
from squadds.core.analysis_plotting import build_closest_design_hspace_plot
from squadds.core.analysis_search import (
    SUPPORTED_METRICS,
    filter_df_by_target_params,
    get_H_param_keys_for_system,
    outside_bounds,
    rank_closest_indices,
    remove_resonator_type_from_target_params,
    resolve_metric_strategy,
)
from squadds.core.metrics import MetricStrategy
from squadds.core.processing import merge_dfs
from squadds.core.utils import create_unified_design_options

"""
=====================================================================================
HELPER FUNCTIONS
=====================================================================================
"""


# Helper function to scale values with 'um' in them
[docs] def scale_value(value, ratio): """ Scales the given value by the specified ratio. Args: - value (str): The value to be scaled, in the format 'Xum' where X is a number. - ratio (float): The scaling ratio. Returns: scaled_value (str): The scaled value in the format 'Xum' where X is the scaled number. """ scaled_value = str(float(value.replace("um", "")) * ratio) + "um" return scaled_value
""" ===================================================================================== Analyzer ===================================================================================== """
[docs] class Analyzer: """ The Analyzer class is responsible for analyzing designs and finding the closest designs based on target parameters. Methods: _add_target_params_columns(): Adds target parameter columns to the dataframe based on the selected system. _fix_cavity_claw_df(): Fixes the cavity claw DataFrame by renaming columns and updating values. _get_H_param_keys(): Gets the parameter keys for the Hamiltonian based on the selected system. target_param_keys(): Returns the target parameter keys. set_metric_strategy(strategy: MetricStrategy): Sets the metric strategy to use for calculating the distance metric. _outside_bounds(df: pd.DataFrame, params: dict, display=True) -> bool: Checks if entered parameters are outside the bounds of a dataframe. find_closest(target_params: dict, num_top: int, metric: str = 'Euclidean', display: bool = True): Finds the closest designs in the library based on the target parameters. get_interpolated_design(target_params: dict, metric: str = 'Euclidean', display: bool = True): Gets the interpolated design based on the target parameters. get_design(df): Extracts the design parameters from the dataframe and returns a dict. """ __supported_metrics__ = SUPPORTED_METRICS __supported_estimation_methods__ = ["Interpolation"] def __init__(self, db=None): """ Initializes an instance of the Analysis class. Parameters: - db: The database object. Attributes: - db: The database object. - selected_component_name: The name of the selected component. - selected_component: The selected component. - selected_data_type: The selected data type. - selected_confg: The selected configuration. - selected_qubit: The selected qubit. - selected_cavity: The selected cavity. - selected_coupler: The selected coupler. - selected_system: The selected system. - df: The selected dataframe. - closest_df_entry: The closest dataframe entry. - closest_design: The closest design. - presimmed_closest_cpw_design: The presimmed closest CPW design. - presimmed_closest_qubit_design: The presimmed closest qubit design. - presimmed_closest_coupler_design: The presimmed closest coupler design. - interpolated_design: The interpolated design. - metric_strategy: The metric strategy (will be set dynamically). - custom_metric_func: The custom metric function. - metric_weights: The metric weights. - target_params: The target parameters. - H_param_keys: The H parameter keys. """ from squadds.core.db import SQuADDS_DB self.db = db if db is not None else SQuADDS_DB() self.reload_db() def _initialize_attributes(self): self.selected_component_name = self.db.selected_component_name self.selected_component = self.db.selected_component self.selected_data_type = self.db.selected_data_type self.selected_confg = self.db.selected_confg self.selected_qubit = self.db.selected_qubit self.selected_cavity = self.db.selected_cavity self.selected_resonator_type = self.db.selected_resonator_type self.selected_coupler = self.db.selected_coupler self.selected_system = self.db.selected_system self.df = self.db.selected_df self.qubit_df = self.db.qubit_df self.cavity_df = self.db.cavity_df self.coupler_df = self.db.coupler_df self.closest_df_entry = None self.closest_design = None self.closest_df = None self.presimmed_closest_cpw_design = None self.presimmed_closest_qubit_design = None self.presimmed_closest_coupler_design = None self.interpolated_design = None self.closest_design_found = False self.params_computed = False self.metric_strategy = None # Will be set dynamically self.custom_metric_func = None self.metric_weights = None self.target_params = None self.H_param_keys = self._get_H_param_keys()
[docs] def reload_db(self): """ Reload the Analyzer with the current singleton SQuADDS_DB object. """ self._initialize_attributes()
[docs] def _add_target_params_columns(self): """ Adds target parameter columns to the dataframe based on the selected system. If the selected system is "qubit", it adds qubit Hamiltonian parameters to the dataframe. If the selected system is "cavity_claw", it fixes the dataframe for the cavity_claw system. If the selected system is "coupler", it does nothing. If the selected system is ["qubit", "cavity_claw"] or ["cavity_claw", "qubit"], it fixes the dataframe for the cavity_claw system and adds cavity-coupled Hamiltonian parameters to the dataframe. Raises: a ValueError if the selected system is invalid. """ self.params_computed = True #! TODO: make this more general and read the param keys from the database if self.selected_system == "qubit": qubit_H = TransmonCrossHamiltonian(self) qubit_H.add_qubit_H_params() self.df = qubit_H.df elif self.selected_system == "cavity_claw": self._fix_cavity_claw_df() elif self.selected_system == "coupler": pass elif (self.selected_system == ["qubit", "cavity_claw"]) or (self.selected_system == ["cavity_claw", "qubit"]): self._fix_cavity_claw_df() qubit_H = TransmonCrossHamiltonian(self) start = time.time() qubit_H.add_cavity_coupled_H_params() end = time.time() print(f"Time taken to add the coupled H params: {end - start} seconds") self.df = qubit_H.df else: raise ValueError("Invalid system.")
[docs] def _fix_cavity_claw_df(self): """ Fix the cavity claw DataFrame by renaming columns and updating values. If the columns 'cavity_frequency' or 'kappa' exist in the DataFrame, they will be renamed to 'cavity_frequency_GHz' and 'kappa_kHz' respectively. The values in these columns will also be updated by multiplying them with appropriate conversion factors. Args: None Returns: None """ self.df = fix_cavity_claw_dataframe(self.df)
[docs] def _get_H_param_keys(self): """ Get the parameter keys for the Hamiltonian (H) based on the selected system. Returns: list: A list of parameter keys for the Hamiltonian. Raises: ValueError: If the selected system is invalid. """ self.H_param_keys = get_H_param_keys_for_system(self.selected_system) return self.H_param_keys
[docs] def target_param_keys(self): """ Returns: list: The target parameter keys. """ return self.H_param_keys
[docs] def set_metric_strategy(self, strategy: MetricStrategy): """ Sets the metric strategy to use for calculating the distance metric. Args: strategy (MetricStrategy): The strategy to use for calculating the distance metric. Raises: ValueError: If the specified metric is not supported. """ self.metric_strategy = strategy
def _outside_bounds(self, df: pd.DataFrame, params: dict, display=True) -> bool: """ Check if entered parameters are outside the bounds of a dataframe. Args: df (pd.DataFrame): Dataframe to give warning. params (dict): Keys are column names of `df`. Values are values to check for bounds. Returns: bool: True if any value is outside of bounds. False if all values are inside bounds. """ return outside_bounds(df, params, display=display)
[docs] def get_complete_df(self, target_params: dict, metric: str = "Euclidean", display: bool = True): """ Returns the complete DataFrame (design + Hamiltonian parameters) sourced using the target parameters. Args: - target_params (dict): A dictionary containing the target parameters. - metric (str, optional): The distance metric to use for calculating distances. Defaults to 'Euclidean'. - display (bool, optional): Whether to display warnings for parameters outside of the library bounds. Defaults to True. Returns: - complete_df (DataFrame): A DataFrame containing all designs and Hamiltonian parameters. Raises: - ValueError: If the specified metric is not supported or if num_top is bigger than the size of the library. - ValueError: If the metric is invalid. """ ### Checks # Check for supported metric if metric not in self.__supported_metrics__: raise ValueError(f"`metric` must be one of the following: {self.__supported_metrics__}") self.target_params = target_params remove_resonator_type_from_target_params( self.target_params, self.selected_resonator_type, missing_ok=False, ) if not self.params_computed: self._add_target_params_columns() else: print("Target parameters have already been computed.") return self.df
[docs] def find_closest( self, target_params: dict, num_top: int, metric: str = "Euclidean", display: bool = True, parallel: bool = False, num_cpu: str = "auto", skip_df_gen: bool = False, ): """ Find the closest designs in the library based on the target parameters. Args: - target_params (dict): A dictionary containing the target parameters. - num_top (int): The number of closest designs to retrieve. - metric (str, optional): The distance metric to use for calculating distances. Defaults to 'Euclidean'. - display (bool, optional): Whether to display warnings for parameters outside of the library bounds. Defaults to True. - parallell (bool, optional): Whether to run metric calculation in a parallelized way - num_cpu (str/int, optional): The number of CPUs to run a job over - skip_df_gen (bool, optional): Whether to generate the df or run from memory Returns: - closest_df (DataFrame): A DataFrame containing the closest designs. Raises: - ValueError: If the specified metric is not supported or if num_top is bigger than the size of the library. - ValueError: If the metric is invalid. """ ### Checks # Check for supported metric if metric not in self.__supported_metrics__: raise ValueError(f"`metric` must be one of the following: {self.__supported_metrics__}") self.target_params = target_params remove_resonator_type_from_target_params( self.target_params, self.selected_resonator_type, missing_ok=True, ) if (skip_df_gen) or (not self.params_computed): self._add_target_params_columns() elif self.selected_resonator_type == "quarter": pass else: print( "Either `skip_df_gen` flag is set to True or all target params have been precomputed at an earlier step. Using `df` from memory.\nPlease set this to False if `target_parameters` have changed." ) target_params_list = list(self.target_params.keys()) filtered_df = self.df[target_params_list] self._outside_bounds(df=filtered_df, params=target_params, display=display) self.set_metric_strategy(resolve_metric_strategy(metric, self.metric_weights, self.custom_metric_func)) # Main logic # Filter DataFrame based on target parameters that are string filtered_df = filter_df_by_target_params(filtered_df, self.target_params) # if the filtered_df is empty, raise a User input error if filtered_df.empty: raise ValueError( f"No geometries found with the specified parameters:\n{target_params}\nPlease double-check your targets (especially ``resonator_type``) and try again." ) # Calculate distances # Use vectorized calculation (much faster and avoids joblib overhead) if parallel: print( "Using vectorized calculation for speed (ignoring num_cpu parameter as it's not needed for vectorization)." ) sorted_indices = rank_closest_indices(filtered_df, self.target_params, self.metric_strategy, num_top) # Sort distances and get the closest ones self.closest_df = self.df.loc[sorted_indices] # set the closest design found flag self.closest_design_found = True if self.selected_resonator_type == "quarter": # store the best design self.closest_df_entry = self.closest_df.iloc[0] self.closest_design = self.closest_df.iloc[0]["design_options"] if len(self.selected_system) == 2: #! TODO: make this more general self.presimmed_closest_cpw_design = self.closest_df_entry["design_options_cavity_claw"] self.presimmed_closest_qubit_design = self.closest_df_entry["design_options_qubit"] elif self.selected_resonator_type == "half": # retrieve the best designs self.closest_qubit = self.qubit_df.iloc[self.closest_df.index_qc].copy() self.closest_coupler = self.coupler_df.iloc[self.closest_df.index_cplr] self.closest_cavity = self.get_closest_cavity() for merger_term in self.db.claw_merger_terms: self.closest_qubit[merger_term] = self.closest_qubit["design_options"].map( lambda x, mt=merger_term: x["connection_pads"]["readout"].get(mt) ) # Create a unified design options column merged_df = merge_dfs(self.closest_qubit, self.closest_cavity, self.db.claw_merger_terms) # Add a temporary key column for cross join self.closest_df["_temp_key"] = 1 merged_df["_temp_key"] = 1 # Perform the cross join self.closest_df = pd.merge( self.closest_df, merged_df, on="_temp_key", how="inner", suffixes=("_closest", "_merged") ).drop("_temp_key", axis=1) # Create the unified design options column self.closest_df["design_options"] = self.closest_df.apply(create_unified_design_options, axis=1) self.closest_df_entry = self.closest_df.iloc[0] return self.closest_df
[docs] def get_closest_cavity(self): """ Returns the closest cavity design. Returns: pd.Series: The closest cavity design. """ # Extract the values you're looking for closest_index_cc = self.closest_df.index_cc.values[0] closest_index_cplr = self.closest_df.index_cplr.values[0] # Use np.where for fast boolean indexing mask = np.where( (self.cavity_df["index_cc"].values == closest_index_cc) & (self.cavity_df["index_cplr"].values == closest_index_cplr) )[0] # Get the index from the DataFrame index = self.cavity_df.index[mask] return self.cavity_df.loc[index]
[docs] def compute_metric_distances(self, row): return self.metric_strategy.calculate(self.target_params, row)
[docs] def get_interpolated_design(self, target_params: dict, metric: str = "Euclidean", display: bool = True): """ """ raise NotImplementedError
[docs] def get_design(self, df): """ Extracts the design parameters from the dataframe and returns a dict. Returns: dict: A dict containing the design parameters. """ return df["design_options"].to_dict()[0]
[docs] def get_param(self, design, param): """ Extracts a specific parameter from the design dict. """ raise NotImplementedError
[docs] def closest_design_in_H_space(self): """Plots a scatter plot of the closest design in the H-space. This method creates a scatter plot with two subplots. The first subplot shows the relationship between 'cavity_frequency_GHz' and 'kappa_kHz', while the second subplot shows the relationship between 'anharmonicity_MHz' and 'g_MHz'. The scatter plot includes pre-simulated data, target data, and the closest design entry from the database. Returns: None """ build_closest_design_hspace_plot( self.df, self.target_params, self.closest_df_entry, self.selected_resonator_type, ) plt.show()
[docs] def get_qubit_options(self, df: pd.DataFrame) -> dict[str, list[Any]]: """ Extracts qubit design options from the dataframe. Parameters: df (pd.DataFrame): The dataframe containing design options. Returns: Dict[str, List[Any]]: A dictionary containing lists of the extracted qubit options. """ return extract_qubit_options(df)
[docs] def get_cpw_options(self, df: pd.DataFrame) -> dict[str, list[Any]]: """ Extracts CPW options from the dataframe. Parameters: df (pd.DataFrame): The dataframe containing design options. Returns: Dict[str, List[Any]]: A dictionary containing lists of the extracted CPW options. """ return extract_cpw_options(df)
[docs] def get_coupler_options(self, df: pd.DataFrame) -> dict[str, list[Any]]: """ Extracts coupler options from the dataframe. Parameters: df (pd.DataFrame): The dataframe containing design options. Returns: Dict[str, List[Any]]: A dictionary containing lists of the extracted coupler options. """ return extract_coupler_options(df)
[docs] def get_Ljs(self, df: pd.DataFrame): """ Extracts the EJ values from the dataframe. Converts them to Josephson inductance values using pyEPR Parameters: df (pd.DataFrame): The dataframe containing design options. Returns: np.array: An array of Josephson inductance values. """ from pyEPR.calcs import Convert # EJ values are stored in a column named 'EJ' in GHz EJ_values = df["EJ"].values # Convert EJ (in MHz) to Josephson inductance (Lj) in nH Ljs = np.array([Convert.Lj_from_Ej(Ej, units_in="GHz", units_out="nH") for Ej in EJ_values]) return Ljs