# Adding Custom Algorithms and Metrics to the Benchmark Pipeline

In this tutorial, we're diving into the advanced capabilities of LunaBench, highlighting how you can extend its functionality with custom algorithms and metrics. This tutorial is built for advanced users who want to take full advantage of all customization possibilities LunaBench offers. From integrating custom algorithms to defining unique metrics for evaluating results, we cover a spectrum of advanced customizations.

Before we begin, ensure you have installed the LunaBench Python SDK along with additional dependencies:

```
pip install numpy
```

*Please note that LunaBench is provided through a separate SDK from the luna-quantum SDK and is currently available only with academic and commercial plans.*

Upon completing this tutorial, you will be able to:

- Integrate and utilize your own algorithms within the LunaBench framework.
- Apply custom metrics for a personalized evaluation of your results.
- Navigate through the benchmarking process, adjusting it to fit your unique project needs.

## 1. Creating the Dataset

For this tutorial, we'll be working with a scenario from our use case library—the Binary Paint Shop Problem. If you're interested in a deeper dive into this and other use cases, we encourage you to explore our dedicated tutorial on the use case library.

```
# Define the problem type
problem_name = "bpsp"
# Define the dataset
dataset = {
"id_00": {"sequence": [1, 2, 0, 1, 2, 0]},
"id_01": {"sequence": [1, 0, 0, 1, 2, 2]},
"id_02": {"sequence": [0, 1, 0, 1, 2, 2]},
}
```

## 2. Integrating Custom Algorithms

LunaBench simplifies the addition of custom algorithms with the `@solver`

decorator. Attaching this decorator to your solving function enables LunaBench to process your dataset with the specified custom algorithm. For LunaBench to correctly track and allocate results, it's crucial to provide an identifier for your algorithm, like so: `@solver(algorithm_id="YOUR_ALGORITHM")`

.

For a seamless integration and processing of your dataset, the solving or minimizing function should include specific input arguments. These are `qubo`

for QUBO problems, formatted as a list of lists or a 2D numpy array; `circuit`

for quantum circuits, either as a string or a QuantumCircuit object; and `lp`

for linear programming problems, as a string.

The output from your custom algorithm should be a well-structured dictionary containing at least the following information:

`"solution"`

: This should map to the list-formatted binary solution vector.`"energy"`

: This value represents the solution value, usually in the form of (f(x) = xᵀQx), although it can be None if not applicable.`"runtime"`

: This indicated the time it took for your algorithm to arrive at the solution.

```
import time
import numpy as np
from lunabench import solver
# Use the @solver decorator to define your custom algorithm
@solver(algorithm_id="random")
def random_solve(qubo):
# Log the runtime
start_time = time.perf_counter()
# Get a random solution
np_solution = np.random.randint(2, size=len(qubo))
# Stop the runtime
end_time = time.perf_counter()
# Calculate the overall time in seconds that the algorithm took
solve_time = end_time - start_time
# Calculate the energy
energy = float((np_solution.T @ qubo @ np_solution))
solution = np_solution.tolist()
# Return a dict containing "solution", "energy" and "runtime"
result = {"solution": solution, "energy": energy, "runtime": solve_time}
return result
```

## 3. Solving the Dataset

Once you've tailored your algorithm to fit the unique characteristics of your problem instances, including their specific input and output requirements, you're ready to put it to the test. With LunaBench, running your custom algorithm against your dataset is straightforward. All you need to do is add your algorithm's name to the `solve_algorithms`

list or dictionary within the `solve_dataset`

function. This inclusion tells LunaBench which algorithms you want to apply to your dataset, integrating your custom solution seamlessly into the benchmarking process.

```
from lunabench import solve_dataset
# Specify the algorithms
algorithms = ["sa", "random"]
# Define the number of runs for each algorithm
n_runs = 2
# Solve the complete dataset
solved, result_id = solve_dataset(
problem_name=problem_name,
dataset=dataset,
solve_algorithms=algorithms,
n_runs=n_runs,
)
```

## 4. Adding Custom Metrics

Just as you can integrate custom algorithms, LunaBench also supports the creation of custom metrics through the `@metric`

decorator. This feature enables you to tailor the evaluation phase to your specific needs. By defining a custom metric with `@metric(metric_id="YOUR_METRIC")`

, you guarantee that LunaBench accurately records and categorizes the outcomes of your benchmarks according to your defined criteria.

For your custom metric to function correctly, it's essential that its input parameters mirror the corresponding column names found in the `solutions.csv`

file.

```
from lunabench import metric
# Use the @metric decorator to define your custom metric
@metric(metric_id="n_cars")
def count_cars(sequence, best_solution):
# Compute the number of total cars
if 2 * len(best_solution) == len(sequence):
return len(sequence)
return 2 * len(best_solution)
```

## 5. Evaluating the Results

Following the integration of your custom metrics, you can incorporate these metrics into the evaluation phase by including their names in the `metrics_config`

. This inclusion directs LunaBench to utilize your specified custom metrics for analyzing the solutions or results.

```
from lunabench import evaluate_results
# Define the metrics
metrics = ["n_cars"]
# Run the evaluation
eval_data, plot_outs = evaluate_results(result_id=result_id, dataset=dataset, metrics_config=metrics)
eval_data
```

id | solver | n_cars | |
---|---|---|---|

0 | id_00 | sa | 6 |

1 | id_00 | random | 6 |

2 | id_01 | sa | 6 |

3 | id_01 | random | 6 |

4 | id_02 | sa | 6 |

5 | id_02 | random | 6 |