Skip to content

Analyzing qubo solutions

QUBOAnalyzer example

To analyze the solutions from one or many QUBO solvers, we can instantiate a QUBOAnalyzer with solutions and labels as follows:

import torch
from qubosolver.data import QUBOSolution
from qubosolver.qubo_analyzer import QUBOAnalyzer

num_bitstrings=100
bit_length=3

costs = torch.randint(1, 20, (2**bit_length,), dtype=torch.float)

bitstrings = torch.randint(0, 2, (num_bitstrings, bit_length))
bitstrings,counts=bitstrings.unique(dim=0,return_counts=True)
solution1 = QUBOSolution(bitstrings, costs, counts)

bitstrings = torch.randint(0, 2, (num_bitstrings, bit_length))
bitstrings,counts=bitstrings.unique(dim=0,return_counts=True)
solution2 = QUBOSolution(bitstrings, costs, counts)

# Create the analyzer with our two solutions
analyzer = QUBOAnalyzer([solution1, solution2], labels=["sol1", "sol2"])
df = analyzer.df

This will generate a pandas dataframe internally (accessible via the df attribute) for several QUBOAnalyzer methods for plotting or comparing solutions, described below. More examples are demonstrated in the QUBOAnalyzer tutorial.

QUBOAnalyzer API description

QUBOAnalyzer(solutions, labels=None)

Analyzer for solutions to a Quadratic Unconstrained Binary Optimization (QUBO) problem.

Initializes the analyzer with one or a list of QUBOSolutions.

If a single QUBOSolution is provided, it is automatically wrapped into a list. Optionally, you can provide a list of labels corresponding to each QUBOSolution. If labels are not provided, they are assigned automatically as '0', '1', etc.

PARAMETER DESCRIPTION
solutions

A single QUBOSolution or a list of QUBOSolution instances.

TYPE: QUBOSolution | list[QUBOSolution]

labels

A list of labels for the QUBOSolutions. Must match the number of solutions.

TYPE: str | list[str] | None DEFAULT: None

RAISES DESCRIPTION
ValueError

If no solutions are provided or if the number of labels does not match the number of solutions.

TypeError

If any solution or label is not of the expected type.

Source code in qubosolver/qubo_analyzer.py
def __init__(
    self,
    solutions: QUBOSolution | list[QUBOSolution],
    labels: str | list[str] | None = None,
):
    """
    Analyzer for solutions to a Quadratic Unconstrained Binary Optimization (QUBO) problem.

    Initializes the analyzer with one or a list of QUBOSolutions.

    If a single QUBOSolution is provided, it is automatically wrapped into a list.
    Optionally, you can provide a list of labels corresponding to each QUBOSolution.
    If labels are not provided, they are assigned automatically as '0', '1', etc.

    Args:
        solutions (QUBOSolution | list[QUBOSolution]):
            A single QUBOSolution or a list of QUBOSolution instances.
        labels (str | list[str] | None):
            A list of labels for the QUBOSolutions. Must match the number of solutions.

    Raises:
        ValueError: If no solutions are provided or if the number of labels
                    does not match the number of solutions.
        TypeError: If any solution or label is not of the expected type.
    """
    # Recast solutions into a list if a single solution is provided.
    if not isinstance(solutions, list):
        solutions = [solutions]

    for sol in solutions:
        if not isinstance(sol, QUBOSolution):
            raise TypeError("Each solution must be a QUBOSolution instance.")

    self.solutions = solutions

    # Validate labels if provided.
    if labels is not None:
        # Recast labels into a list if a single solution is provided.
        if not isinstance(labels, list):
            labels = [labels]

        if len(labels) != len(solutions):
            raise ValueError(
                "The number of labels must equal the number of QUBOSolutions provided."
            )
        for label in labels:
            if not isinstance(label, str):
                raise TypeError("Each label must be a string.")
        self.labels = labels
    else:
        self.labels = [str(i) for i in range(len(solutions))]

    self.df = self._to_dataframe()

add_counts(counts)

Updates the DataFrame by adding the counts column.

If counts are provided at a later stage, this method will add the counts to the DataFrame and ensure that they match the number of bitstrings.

PARAMETER DESCRIPTION
counts

A list or tensor of counts.

TYPE: list[int] | Tensor

RAISES DESCRIPTION
ValueError

If the length of counts does not match the number of bitstrings.

Source code in qubosolver/qubo_analyzer.py
def add_counts(self, counts: list[int] | torch.Tensor) -> None:
    """
    Updates the DataFrame by adding the counts column.

    If counts are provided at a later stage, this method will add the counts
    to the DataFrame and ensure that they match the number of bitstrings.

    Args:
        counts (list[int] | torch.Tensor): A list or tensor of counts.

    Raises:
        ValueError: If the length of counts does not match the number of bitstrings.
    """
    if isinstance(counts, torch.Tensor):
        counts = counts.tolist()  # Convert tensor to list if necessary

    if len(counts) != len(self.df):
        raise ValueError(
            "The number of counts must match" " the number of bitstrings in the DataFrame."
        )

    if _PROBS in self.df.columns:
        # Check if the probabilities are consistent
        # with the counts (probs = counts / total_counts)
        total_counts = sum(self.df[_COUNTS])
        expected_counts = [probs * total_counts for probs in self.df[_PROBS]]
        if not all(abs(p - ep) < 1e-6 for p, ep in zip(counts, expected_counts)):
            raise ValueError("The provided counts do not match probabilities.")

    self.df[_COUNTS] = counts

add_probs(probs)

Updates the DataFrame by adding the probs column.

If probs are provided at a later stage, this method will add the probs to the DataFrame and ensure that they match the number of bitstrings.

PARAMETER DESCRIPTION
probs

A list or tensor of probabilities.

TYPE: list[float] | Tensor

RAISES DESCRIPTION
ValueError

If the length of probabilities does not match the number of bitstrings.

Source code in qubosolver/qubo_analyzer.py
def add_probs(self, probs: list[float] | torch.Tensor) -> None:
    """
    Updates the DataFrame by adding the probs column.

    If probs are provided at a later stage, this method will add the probs
    to the DataFrame and ensure that they match the number of bitstrings.

    Args:
        probs (list[float] | torch.Tensor): A list or tensor of probabilities.

    Raises:
        ValueError: If the length of probabilities does not match the number of bitstrings.
    """
    if isinstance(probs, torch.Tensor):
        probs = probs.tolist()

    if len(probs) != len(self.df):
        raise ValueError(
            "The number of counts must match" "the number of bitstrings in the DataFrame."
        )

    if _COUNTS in self.df.columns:
        # Check if the probabilities are consistent
        # with the counts (probs = counts / total_counts)
        total_counts = sum(self.df[_COUNTS])
        expected_probs = [count / total_counts for count in self.df[_COUNTS]]
        if not all(abs(p - ep) < 1e-6 for p, ep in zip(probs, expected_probs)):
            raise ValueError("The provided probabilities do not match counts.")

    self.df[_PROBS] = probs

average_cost(top_percent=1)

Calculates the average cost for the best top_percent of bitstrings (lowest cost) for each solution.

PARAMETER DESCRIPTION
top_percent

A fraction between 0 and 1 representing the percentage of lowest cost bitstrings to consider.

TYPE: float DEFAULT: 1

RETURNS DESCRIPTION
DataFrame

pd.DataFrame: A DataFrame with each solution label, the average cost over the best top_percent bitstrings, and the count of bitstrings used.

Source code in qubosolver/qubo_analyzer.py
def average_cost(self, top_percent: float = 1) -> pd.DataFrame:
    """
    Calculates the average cost for the best top_percent of bitstrings (lowest cost)
    for each solution.

    Args:
        top_percent (float): A fraction between 0 and 1 representing the percentage
                             of lowest cost bitstrings to consider.

    Returns:
        pd.DataFrame: A DataFrame with each solution label, the average cost over the
                      best top_percent bitstrings, and the count of bitstrings used.
    """
    df_top = self.filter_by_percentage(top_percent)
    results = []
    for label, group in df_top.groupby(_LABELS):
        avg_cost = group[_COSTS].mean()
        results.append(
            {
                _LABELS: label,
                "average cost": avg_cost,
                "bitstrings considered": len(group),
            }
        )

    return pd.DataFrame(results)

best_bitstrings()

Finds all unique bitstrings (with the best cost) in each solution's DataFrame.

RETURNS DESCRIPTION
DataFrame

pd.DataFrame: A DataFrame with all unique rows per solution (solution_label) that have the best (lowest) cost.

Source code in qubosolver/qubo_analyzer.py
def best_bitstrings(self) -> pd.DataFrame:
    """
    Finds all unique bitstrings (with the best cost) in each solution's DataFrame.

    Returns:
        pd.DataFrame: A DataFrame with all unique rows per solution (solution_label)
                      that have the best (lowest) cost.
    """
    best_list = []
    for label, sol in self.df.groupby(_LABELS):
        min_cost = sol[_COSTS].min()
        # Filter all rows with the cost equal to the minimum cost in this group
        best = sol[sol[_COSTS] == min_cost]
        # Optionally, drop duplicate bitstring entries (if bitstrings are duplicated)
        best = best.drop_duplicates(subset=[_BITSTRINGS])
        best_list.append(best)
    best_rows = pd.concat(best_list, ignore_index=True)
    return best_rows

bitstrings_to_tensor(bitstring_list) staticmethod

Converts a list of bitstring strings to a torch tensor of bitstrings.

Each bitstring in the list is assumed to be a string of '0's and '1's (e.g., "010101"). Converts each character to an integer and constructs a tensor of shape (num_bitstrings, bitstring_length).

PARAMETER DESCRIPTION
bitstring_list

A list of bitstring strings.

TYPE: list[str]

RETURNS DESCRIPTION
Tensor

torch.Tensor: A tensor with shape (num_bitstrings, bitstring_length) with integer elements.

RAISES DESCRIPTION
ValueError

If the list is empty or if the bitstrings are not of uniform length.

Source code in qubosolver/qubo_analyzer.py
@staticmethod
def bitstrings_to_tensor(bitstring_list: list[str]) -> torch.Tensor:
    """
    Converts a list of bitstring strings to a torch tensor of bitstrings.

    Each bitstring in the list is assumed to be a string of '0's and '1's (e.g., "010101").
    Converts each character to an integer and constructs a tensor of shape
    (num_bitstrings, bitstring_length).

    Args:
        bitstring_list (list[str]): A list of bitstring strings.

    Returns:
        torch.Tensor: A tensor with shape (num_bitstrings, bitstring_length)
                      with integer elements.

    Raises:
        ValueError: If the list is empty or if the bitstrings are not of uniform length.
    """
    if not bitstring_list:
        raise ValueError("The bitstring_list is empty.")
    bit_length = len(bitstring_list[0])
    for bitstr in bitstring_list:
        if len(bitstr) != bit_length:
            raise ValueError("All bitstrings must have the same length.")
    bit_lists = [[int(x) for x in bitstr] for bitstr in bitstring_list]
    return torch.tensor(bit_lists, dtype=torch.int)

calculate_costs(Q)

Calculates the cost for each bitstring using the provided Q QUBOInstance.

cost = x^T Q x

The computed cost is added as the columns _COSTS in the DataFrame.

PARAMETER DESCRIPTION
Q

QUBOInstance

TYPE: QUBOInstance

RETURNS DESCRIPTION
DataFrame

pd.DataFrame: The updated DataFrame including the _COSTS column.

RAISES DESCRIPTION
ValueError

If a bitstring's length does not match Q.shape[0].

Source code in qubosolver/qubo_analyzer.py
def calculate_costs(self, Q: QUBOInstance) -> pd.DataFrame:
    """
    Calculates the cost for each bitstring using the provided Q QUBOInstance.

        cost = x^T Q x

    The computed cost is added as the columns _COSTS in the DataFrame.

    Args:
        Q: QUBOInstance

    Returns:
        pd.DataFrame: The updated DataFrame including the _COSTS column.

    Raises:
        ValueError: If a bitstring's length does not match Q.shape[0].
    """

    self.df[_COSTS] = self.df[_BITSTRINGS].apply(Q.evaluate_solution)
    return self.df

calculate_gaps(opt_cost, Q=None)

Calculates the gaps for each bitstring using the provided optimal cost. If costs aren ot present calculates costs as

    cost = x^T Q x

The computed cost is added as the columns _COSTS in the DataFrame.

PARAMETER DESCRIPTION
Q

QUBOInstance

TYPE: QUBOInstance | None DEFAULT: None

RETURNS DESCRIPTION
DataFrame

pd.DataFrame: The updated DataFrame including the _COSTS column.

RAISES DESCRIPTION
ValueError

If a bitstring's length does not match Q.shape[0].

Source code in qubosolver/qubo_analyzer.py
def calculate_gaps(self, opt_cost: float, Q: QUBOInstance | None = None) -> pd.DataFrame:
    """
    Calculates the gaps for each bitstring using the provided optimal cost.
    If costs aren ot present calculates costs as

            cost = x^T Q x

    The computed cost is added as the columns _COSTS in the DataFrame.

    Args:
        Q: QUBOInstance

    Returns:
        pd.DataFrame: The updated DataFrame including the _COSTS column.

    Raises:
        ValueError: If a bitstring's length does not match Q.shape[0].
    """
    if _COSTS in self.df.columns:
        self.df[_GAPS] = abs((self.df[_COSTS] - opt_cost) / opt_cost)
    else:
        if Q is not None:
            self.df[_COSTS] = self.df[_BITSTRINGS].apply(Q.evaluate_solution)
        else:
            self.df[_GAPS] = abs((self.df[_COSTS] - opt_cost) / opt_cost)
    return self.df

compare_qubo_solutions(target_labels)

Compare two QUBOSolution objects and provide a statistical analysis of the differences, including degenerate solution matching and mismatch statistics.

PARAMETER DESCRIPTION
target_labels

The labels of the solutions to compare. If None, compares all solutions.

TYPE: list[str]

Source code in qubosolver/qubo_analyzer.py
def compare_qubo_solutions(
    self,
    target_labels: list[str],
) -> None:
    """
    Compare two `QUBOSolution` objects and provide a statistical analysis of the differences,
    including degenerate solution matching and mismatch statistics.

    Args:
        target_labels (list[str]): The labels of the solutions to compare. If None, compares
            all solutions.
    """

    def print_diff(
        diff: set[str],
        bs_set: set[str],
        main_label: str,
        compare_label: str,
    ) -> None:
        """
        Prints the differences between two sets of bitstrings.
        Args:
            diff (set[str]): The set of bitstrings that are in main_label but not in
                compare_label.
            bs_set (set[str]): The set of all unique bitstrings.
            main_label (str): The label of the solution being compared from.
            compare_label (str): The label of the solution being compared to.
        """
        if len(diff) > 0:
            print(f"\nBitstrings in {main_label} not present in {compare_label}:")
            for bs in diff:
                print("-", bs)
            print(
                f"\nRatio of different bitstrings: {len(diff)}/{len(bs_set)} = "
                + f"{(len(diff)/len(bs_set))*100:.0f}%"
            )

    # Validate target labels
    if len(target_labels) != 2:
        raise ValueError("Exactly two target labels must be provided for comparison.")
    if not all(label in self.labels for label in target_labels):
        raise ValueError("All target labels must be present in the QUBOAnalyzer's labels.")

    # Extract bitstrings for each target label
    bs_list1 = self.df[self.df["labels"] == target_labels[0]]["bitstrings"].tolist()
    bs_list2 = self.df[self.df["labels"] == target_labels[1]]["bitstrings"].tolist()

    # TODO: Once issue about duplicate bitstrings in QUBOSolution is fixed, this can be removed
    bs_set1 = set(bs_list1)
    bs_set2 = set(bs_list2)

    print(
        f"Comparing two lists of bitstrings:\n1. {target_labels[0]}: {len(bs_list1)} bitstrings"
        + f" ({len(bs_set1)} unique strings)\n2. {target_labels[1]}: {len(bs_list2)} bitstrings"
        + f" ({len(bs_set2)} unique strings)"
    )

    # Analyze differences
    diff1 = bs_set1 - bs_set2
    diff2 = bs_set2 - bs_set1

    if len(diff1) == 0 and len(diff2) == 0:
        print("\nThe lists contain exactly the same bitstrings.")
        return
    else:
        print_diff(diff1, bs_set1, target_labels[0], target_labels[1])
        print_diff(diff2, bs_set2, target_labels[1], target_labels[0])

filter_by_cost(max_cost, df=None)

Returns a DataFrame limited to bitstrings whose cost is smaller than the provided threshold.

PARAMETER DESCRIPTION
max_cost

Maximum cost threshold.

TYPE: float

df

DataFrame to filter.

TYPE: DataFrame | None DEFAULT: None

RETURNS DESCRIPTION
DataFrame

pd.DataFrame: The filtered DataFrame.

Source code in qubosolver/qubo_analyzer.py
def filter_by_cost(self, max_cost: float, df: pd.DataFrame | None = None) -> pd.DataFrame:
    """
    Returns a DataFrame limited to bitstrings whose cost
    is smaller than the provided threshold.

    Args:
        max_cost (float): Maximum cost threshold.
        df (pd.DataFrame | None): DataFrame to filter.

    Returns:
        pd.DataFrame: The filtered DataFrame.
    """

    if df is None:
        df = self.df

    if _COSTS not in df.columns:
        raise ValueError("No probabilities available in the DataFrame.")

    return df[df[_COSTS] < max_cost]

filter_by_percentage(top_percent=1.0, column=_COSTS, order='ascending')

Returns a DataFrame limited to the best bitstrings in a given column for each solution group, where "best" means that the cumulative probability (_PROBS) of the selected rows reaches at least top_percent. The sorting order is controlled by the order parameter: if "ascending", the group is sorted in ascending order (lower values are considered better); if "descending", sorted in descending order.

PARAMETER DESCRIPTION
top_percent

A threshold between 0 and 1 representing the fraction of cumulative probability. For example, 0.1 means select bitstrings until their cumulative probability is ≥ 10%.

TYPE: float DEFAULT: 1.0

column

The key (column) by which to sort the rows (e.g. _COSTS, _GAPS, or _PROBS). Defaults to _COSTS.

TYPE: str DEFAULT: _COSTS

order

Either "ascending" or "descending". If "ascending", rows are sorted in ascending order (lower values are better). If "descending", rows are sorted in descending order (higher values are better).

TYPE: str DEFAULT: 'ascending'

RETURNS DESCRIPTION
DataFrame

pd.DataFrame: The filtered DataFrame containing, for each solution group, the bitstrings whose cumulative probability (_PROBS) reaches the specified top_percent threshold.

RAISES DESCRIPTION
ValueError

If the specified column is not in the DataFrame, if top_percent is not in (0, 1], or if the order parameter is not "descending" or "ascending".

Source code in qubosolver/qubo_analyzer.py
def filter_by_percentage(
    self,
    top_percent: float = 1.0,
    column: str = _COSTS,
    order: str = "ascending",
) -> pd.DataFrame:
    """
    Returns a DataFrame limited to the best bitstrings
    in a given column for each solution group,
    where "best" means that the cumulative probability (_PROBS)
    of the selected rows reaches at least
    top_percent. The sorting order is controlled by the
    `order` parameter: if "ascending", the group is sorted
    in ascending order (lower values are considered better);
    if "descending", sorted in descending order.

    Args:
        top_percent (float): A threshold between 0 and 1 representing
                             the fraction of cumulative probability.
                             For example, 0.1 means select bitstrings
                             until their cumulative probability is ≥ 10%.
        column (str): The key (column) by which to sort the rows
                                (e.g. _COSTS, _GAPS, or _PROBS).
                                Defaults to _COSTS.
        order (str): Either "ascending" or "descending". If "ascending",
                     rows are sorted in ascending order (lower values are better).
                     If "descending", rows are sorted in descending order
                     (higher values are better).

    Returns:
        pd.DataFrame: The filtered DataFrame containing, for each solution group, the bitstrings
                      whose cumulative probability (_PROBS)
                    reaches the specified top_percent threshold.

    Raises:
        ValueError: If the specified column is not in the DataFrame,
                    if top_percent is not in (0, 1],
                    or if the order parameter is not "descending" or "ascending".
    """
    df = self.df
    if column not in df.columns:
        raise ValueError(
            f"{column} data is not available. \
                         Please add {column} before filtering."
        )

    if not (0 < top_percent <= 1):
        raise ValueError("top_percent must be a float between 0 and 1.")

    if order not in ("ascending", "descending"):
        raise ValueError("The keep parameter must be either 'ascending' or 'descending'.")

    filtered_list = []
    for label, group in df.groupby(_LABELS):
        # Sort the group based on the specified column using the desired order.
        sorted_group = group.sort_values(by=column, ascending=(order == "ascending"))
        cumulative = 0.0
        selected_indices = []
        # Use the _PROBS column to accumulate probability
        for idx, row in sorted_group.iterrows():
            cumulative += row[_PROBS]
            selected_indices.append(idx)
            if cumulative >= top_percent:
                break

        filtered_group = sorted_group.loc[selected_indices]
        filtered_list.append(filtered_group)
    return pd.concat(filtered_list, ignore_index=True)

filter_by_probability(min_probability, df=None)

Returns a DataFrame limited to bitstrings whose probability is greater than the provided threshold.

PARAMETER DESCRIPTION
min_probability

Minimum probability threshold.

TYPE: float

df

DataFrame to filter.

TYPE: DataFrame | None DEFAULT: None

RETURNS DESCRIPTION
DataFrame

pd.DataFrame: The filtered DataFrame.

RAISES DESCRIPTION
ValueError

If the 'probabilities' column is not present.

Source code in qubosolver/qubo_analyzer.py
def filter_by_probability(
    self, min_probability: float, df: pd.DataFrame | None = None
) -> pd.DataFrame:
    """
    Returns a DataFrame limited to bitstrings whose probability
    is greater than the provided threshold.

    Args:
        min_probability (float): Minimum probability threshold.
        df (pd.DataFrame | None): DataFrame to filter.

    Returns:
        pd.DataFrame: The filtered DataFrame.

    Raises:
        ValueError: If the 'probabilities' column is not present.
    """

    if df is None:
        df = self.df

    if _PROBS not in df.columns:
        raise ValueError("No probabilities available in the DataFrame.")
    return df[df[_PROBS] > min_probability]

plot(x_axis, y_axis, labels=None, sort_by=None, sort_order='ascending', probability_threshold=None, cost_threshold=None, top_percent=None, context='notebook')

A wrapper function that chooses between plotting costs, counts, or probabilities as a function of bitstrings or as a function of cost.

Source code in qubosolver/qubo_analyzer.py
def plot(
    self,
    x_axis: str,
    y_axis: str,
    labels: list[str] | None = None,
    sort_by: str | None = None,
    sort_order: str = "ascending",
    probability_threshold: float | None = None,
    cost_threshold: float | None = None,
    top_percent: float | None = None,
    context: str = "notebook",
) -> sns.axisgrid.FacetGrid:
    """
    A wrapper function that chooses between plotting costs, counts, or probabilities
    as a function of bitstrings or as a function of cost.
    """
    df = self.df.copy()

    if x_axis not in df.columns:
        raise ValueError(
            f"{x_axis} data is not available.\
                            Please add {x_axis} before plotting."
        )

    if labels:
        df = df[df[_LABELS].isin(labels)]

    if probability_threshold is not None:
        df = self.filter_by_probability(probability_threshold, df)

    if cost_threshold is not None:
        df = self.filter_by_cost(cost_threshold, df)

    if top_percent is not None:
        df = self.filter_by_percentage(top_percent)

    if x_axis == _BITSTRINGS:
        g = self.plot_vs_bitstrings(
            df=df,
            y_axis=y_axis,
            sort_by=sort_by,
            sort_order=sort_order,
            context=context,
        )
        return g
    else:
        g = self.plot_no_bitstrings(
            df=df,
            x_axis=x_axis,
            y_axis=y_axis,
            sort_by=sort_by,
            sort_order=sort_order,
            context=context,
        )
        return g

plot_no_bitstrings(df, x_axis, y_axis, sort_by=None, sort_order='ascending', context='notebook') staticmethod

Plots a bar chart of probabilities or counts as a function of cost.

PARAMETER DESCRIPTION
df

The DataFrame to plot. Defaults to None, that means uses self.df.

TYPE: DataFrame

x_axis

The column name to be plotted on the x-axis.

TYPE: str

y_axis

The column name to be plotted on the y-axis.

TYPE: str

sort_by

Defines the column by which to sort the costs. If None, no sorting is done.

TYPE: str | None DEFAULT: None

sort_order

Defines the sorting order. Accepts 'ascending' or 'descending'. Default is 'ascending'. Ignored if sort_by is None.

TYPE: str DEFAULT: 'ascending'

Source code in qubosolver/qubo_analyzer.py
@staticmethod
def plot_no_bitstrings(
    df: pd.DataFrame,
    x_axis: str,
    y_axis: str,
    sort_by: str | None = None,
    sort_order: str = "ascending",
    context: str = "notebook",
) -> sns.axisgrid.FacetGrid:
    """
    Plots a bar chart of probabilities or counts as a function of cost.

    Args:
        df (pd.DataFrame): The DataFrame to plot. Defaults to None,
                            that means uses self.df.
        x_axis (str): The column name to be plotted on the x-axis.
        y_axis (str): The column name to be plotted on the y-axis.
        sort_by (str | None): Defines the column by which to sort the costs.
                                 If None, no sorting is done.
        sort_order (str): Defines the sorting order. Accepts 'ascending' or 'descending'.
                          Default is 'ascending'. Ignored if `sort_by` is None.
    """
    if x_axis not in df.columns:
        raise ValueError(
            f"{x_axis} data is not available. Please add {x_axis} before plotting."
        )

    if y_axis not in df.columns:
        raise ValueError(
            f"{y_axis} data is not available. Please add {y_axis} before plotting."
        )

    if sort_by:
        if sort_by not in [x_axis, y_axis]:
            raise ValueError(f"{sort_by} is not a valid column for sorting.")

    df = df.groupby([_LABELS, x_axis], as_index=False).agg({y_axis: "sum"})
    df = df.pivot_table(
        index=x_axis,
        columns=_LABELS,
        values=y_axis,
        fill_value=0,
    ).reset_index()
    df = df.melt(id_vars=x_axis, var_name=_LABELS, value_name=y_axis)
    df = df.sort_values(by=sort_by, ascending=(sort_order == "ascending"))

    # Set color palette
    cmap = sns.color_palette("viridis", n_colors=len(df[_LABELS].unique().tolist()))

    with sns.plotting_context(context):
        g = sns.catplot(
            data=df,
            x=x_axis,
            y=y_axis,
            hue=_LABELS,
            kind="bar",
            order=df[x_axis].unique().tolist(),
            height=6,
            aspect=1.5,  # This ensures the bars are side by side
            palette=cmap,
        )

    # Set axis labels
    g.set_axis_labels(x_axis, y_axis)

    return g

plot_vs_bitstrings(df, y_axis, sort_by=None, sort_order='descending', context='notebook') staticmethod

Plots a bar chart of costs, counts, or probabilities as a function of bitstrings.

PARAMETER DESCRIPTION
df

The DataFrame to plot. Defaults to None, that means uses self.df.

TYPE: DataFrame

y_axis

The column name to be plotted on the y-axis.

TYPE: str

sort_by

Defines the column by which to sort the bitstrings. If None, no sorting is done.

TYPE: str | None DEFAULT: None

sort_order

Defines the sorting order. Accepts 'ascending' or 'descending'. Default is 'ascending'. Ignored if sort_by is None.

TYPE: str DEFAULT: 'descending'

Source code in qubosolver/qubo_analyzer.py
@staticmethod
def plot_vs_bitstrings(
    df: pd.DataFrame,
    y_axis: str,
    sort_by: str | None = None,
    sort_order: str = "descending",
    context: str = "notebook",
) -> sns.axisgrid.FacetGrid:
    """
    Plots a bar chart of costs, counts, or probabilities as a function of bitstrings.

    Args:
        df (pd.DataFrame): The DataFrame to plot. Defaults to None,
                            that means uses self.df.
        y_axis (str): The column name to be plotted on the y-axis.
        sort_by (str | None): Defines the column by which to sort the bitstrings.
                                 If None, no sorting is done.
        sort_order (str): Defines the sorting order. Accepts 'ascending' or 'descending'.
                          Default is 'ascending'. Ignored if `sort_by` is None.

    """
    # Check if the y_axis is available
    if y_axis not in df.columns:
        raise ValueError(
            f"{y_axis} data is not available.\
                          Please add {y_axis} before plotting."
        )
    if sort_by and sort_by not in df.columns:
        raise ValueError(f"{sort_by} is not a valid column for sorting.")

    if sort_by == y_axis:
        df = df.pivot_table(
            index=_BITSTRINGS,
            columns=_LABELS,
            values=y_axis,
            fill_value=0,
        ).reset_index()
        df = df.melt(id_vars=_BITSTRINGS, var_name=_LABELS, value_name=y_axis)
        df = df.sort_values(by=sort_by, ascending=(sort_order == "ascending"))
    else:
        df = df.pivot_table(
            index=[_BITSTRINGS, sort_by],
            columns=_LABELS,
            values=y_axis,
            fill_value=0,
        ).reset_index()
        df = df.melt(id_vars=[_BITSTRINGS, sort_by], var_name=_LABELS, value_name=y_axis)
        df = df.sort_values(by=sort_by, ascending=(sort_order == "ascending"))

    # Set color palette
    cmap = sns.color_palette("viridis", n_colors=len(df[_LABELS].unique().tolist()))

    with sns.plotting_context(context):
        g = sns.catplot(
            data=df,
            x=_BITSTRINGS,
            y=y_axis,
            hue=_LABELS,
            kind="bar",
            order=df[_BITSTRINGS].unique().tolist(),
            height=6,
            aspect=1.5,
            palette=cmap,
        )

    g.set_axis_labels(_BITSTRINGS, y_axis)

    g.set_xticklabels(rotation=90)
    return g

tensor_to_bitstrings(bitstring_tensor) staticmethod

Converts a torch tensor of bitstrings to a list of bitstring strings.

Each row in the tensor is assumed to be a bitstring (with integer elements 0 or 1), and is converted to a single string (e.g., a row [0, 1, 0, 1, 0, 1] becomes "010101").

PARAMETER DESCRIPTION
bitstring_tensor

Tensor of shape (num_bitstrings, bitstring_length) where each element is an integer (0 or 1).

TYPE: Tensor

RETURNS DESCRIPTION
list[str]

list[str]: A list of bitstring strings.

Source code in qubosolver/qubo_analyzer.py
@staticmethod
def tensor_to_bitstrings(bitstring_tensor: torch.Tensor) -> list[str]:
    """
    Converts a torch tensor of bitstrings to a list of bitstring strings.

    Each row in the tensor is assumed to be a bitstring (with integer elements 0 or 1),
    and is converted to a single string (e.g., a row [0, 1, 0, 1, 0, 1] becomes "010101").

    Args:
        bitstring_tensor (torch.Tensor): Tensor of shape (num_bitstrings, bitstring_length)
                                         where each element is an integer (0 or 1).

    Returns:
        list[str]: A list of bitstring strings.
    """
    return ["".join(map(str, row.tolist())) for row in bitstring_tensor]