Skip to content

How to add a solver#

Introduction#

In this article we will go through the process of adding a new Solver to the QFStudio platform.

First, there are some pre-requisites you need to have ready before creating a solver:

  • Enable the SDKs you want to use from the SDKs section.
  • Enable the harware providers you want to use from the Providers section.
  • Choose a use case or create your own use case and get familiar with the input and ouput formats (JSON) of the use case, because your solver must be compliant with them.

Now, this process of creating a new solver involves four main steps:

  1. Choose the Use case
  2. Add the solver to the platform
  3. Implement the solver using the solver template and store it in a Git repository
  4. Connect your repoisitory to the platform

Pre-requisites#

Enable SDKs#

Go to the SDKs section, browse the list of available SDKs and enable the ones you will be using in your solvers.

Enable hardware providers#

Go to the Providers section, browse the list of available providers and enable the ones you will be using in your solvers.

In this step, you will be asked if you have your own access token for each provider and if you want to use it or if you prefer to use QFStudio tokens.

Choose the use case and familiarize yourself with the input/output format#

Browse the Use Cases section to see the use cases already created in your organization and the Use Cases catalog to see other use cases publicly available to develop solvers for them.

Once you have chosen the use case, or created your own use case, visit it and check the Tehcnical details section.

Here’s an example of the Max Cut use case input and output specification in JSON format:

Max cut input/output formats
JSON input/output format for the Max cut use case

This would be an example of an input data file for the Max cut use case, following the input format:

{
    "adj_matrix": [ [0, 9, 5, 2, 4, 1], 
                    [9, 0, 3, 8, 7, 6], 
                    [5, 3, 0, 2, 8, 4], 
                    [2, 8, 2, 0, 9, 3], 
                    [4, 7, 8, 9, 0, 2], 
                    [1, 6, 4, 3, 2, 0] ],
    "num_nodes": 6
}

And this would be an example of output data generate by a solver:

{
    "partitions": [1, 0, 1, 0, 1, 1, 0, 1, 0, 0],
    "cut_value": 147
}

1. Choose the Use Case#

The first step is to choose the use case to be solved.

In the Use cases section you can find all the use cases available on the platform.

You can also create your own use case if you don’t find any suitable one already created (check Create a use case for instructions).

For this tutorial we will assume that we want to solve the Max Cut problem (a classic optimization problem).

2. Add the solver to the platform#

Now, you can register your solver int the Platform.

To add a new solver to the platform:

  1. Go to the section My solvers through the left side menu
  2. Then, on the top-right corner, click on the Add new solver button
  3. You will get the solver creation wizzard where you can fill all the information of your solver:
    • Select the use case you are solving.
    • Fill in your solver details, such as name and description.
  4. Click the Add solver button at the end of the wizzard to save the solver.

3. Implement the solver using the Solver Template#

Once the use case has been choosen and you have added it to the platform, you can start implementing the actual code or your solver.

Let’s see how we can implement the solver in a way that it can run on the QFStudio platform.

The easiest way to program your solvers is using Python.

First, we must prepare a Git source code repository with the following files:

  • solver-name.md (solver documentation file)
  • main.py
  • app.py
  • requirements.txt

solver-name.md (solver documentation file)#

You must include in the root of the repository a [solver-name].md file that will contain the documentation you want to include about your solver, explaining, if any, the parameters that can be inserted by the end user.

Continuing with our example, we would have created MyFirstSolver.md:

MyFirstSolver.md
## MyFirstSolver
Test documentation associated with my solver.
I don't have auxiliary parameters but I could define them like this:
- "parameter1": (int) This is what my first parameter does.

main.py#

This file will contain only a run function to which the parameters input_data, solver_params and extra_arguments will be passed:

main.py
import logging
logger = logging.getLogger(__name__)

from qiskit import QuantumCircuit, Aer, execute, IBMQ

def run(input_data:dict, solver_params:dict, extra_arguments:dict) -> dict:
    logger.info("Starting Solver...")

    # input_data contains the input data provided by the user.
    # It is a dictionary that follows the Use case input format.

    # This is your solver's code

    size = int(input_data['size'])
    backend = Aer.get_backend('qasm_simulator')

    qc = QuantumCircuit(1)
    qc.h(0)
    qc.measure_all()

    job = execute(qc, backend=backend, shots=size, memory=True)
    individual_shots = job.result().get_memory()

    logger.info("Ending Solver...")
    result = ''
    for i in individual_shots:
        result+=i

    # And this is the output it returns.
    # This output has to be dictionary and must follow the Use case output format.
    output = {"cut_value": result}

    return output

Tip

Make sure to comply with the Use case output format.

app.py#

The purpose of this file is for local testing only. In the platform it will be replaced with the platform’s own file that adds the necessary libraries and that calls the right hardward providers.

import main
result = main.run(problem_data, solver_params, extra_arguments)
print(result)

Caution

You should not modify this file to ensure that your solver works in both local and production environments.

As you can see, from this file the only thing that will be done is to call the function run that we have just created in the main.py file.

In order to test that everything works correctly locally without having to upload it to the platform, this is the ideal place to do it.

For this, we must be clear about the JSON input that will receive your solver, something like this:

input.json
{
    "adj_matrix": [ [0, 9, 5, 2, 4, 1], 
                    [9, 0, 3, 8, 7, 6], 
                    [5, 3, 0, 2, 8, 4], 
                    [2, 8, 2, 0, 9, 3], 
                    [4, 7, 8, 9, 0, 2], 
                    [1, 6, 4, 3, 2, 0] ],
    "num_nodes": 6
}

with the corresponding “solver_params” and “extra_arguments” if any. Finally, we would only have to create an input.json like the file we have just shown and modify app.py as follows:

app.py
input_file_name = "input.json"

# Input data loader. Container will get data from here

import json
with open(input_file_name) as f:
  dic = json.load(f)

# Optional extra parameters

if "extra_arguments" in dic:
    extra_arguments = dic['extra_arguments']
else:
    extra_arguments = {}

if "solver_params" in dic:
    solver_params = dic['solver_params']
else:
    solver_params = {}


import main
result = main.run(dic['data'], solver_params, extra_arguments)
print(result)

And when executing the app.py it will return, in this case, a string of 50 random zeros and ones (created with the qiskit simulator).

requirements.txt#

Finally we must create the requirements.txt file containing the libraries used in the solver along with their versions:

requirements.txt
qiskit==0.17.0

Any libraries from the standard approved ones in pip will be instaled. So make sure you add all your code dependencies.

When working locally, it is very useful to create a new environment (with VirtualEnv or Conda) to make sure you don’t have dependency mixes or you are not missing anything. Start with a completely new Python3.8 environment and add all the required modules in your requirements.txt file.

Handle exceptions#

Important

Do not catch exeptions globally in your solver. Let blocking exceptions to raise and to be handled by the platform for you, so the execution is stopped and the exception message is shown in the dashboard.

You can add limited scope exception handling in your solver code, but make sure to raise blocking errors as exceptions so the platform can capture them and stop the execution.

You can check this exceptions in the dashboard in the job details page after the execution.

Add logs to your solver#

You can add logs in your solver and see them in the platform web dashboard after every execution.

Tip

Add as many logs as you want, specially during the development, testing and experimentation phases to gather as much information as possible of your solver’s performance.

To do this, do the following in any of your solver files:

First, import Python’s native library for logging and initialize it:

import logging
logger = logging.getLogger(__name__)

Then, add as many log records as you want throughout your code:

logger.info("Starting solver execution...")

This is an example of a main.py file that includes log records:

main.py
import logging
logger = logging.getLogger(__name__)

def run(input_data, solver_params, extra_arguments):

    logger.info("Starting solver execution...")

    # This is your solver's code:

    input_param_1 = int(input_data['param_1'])

    logger.info(f"Input param_1: { input_param_1 }")

    output = {"output_param": "This is an output parameter"}

    # And this is the output it returns
    logger.info("End of solver execution.")

    return output

All these log records are stored and displayed in the web dashboard in the job details page, in the Execution logs section.

Measure execution time#

You can use this logging feature to measure your solver’s effective execution time by using the time library

Tip

You can measure the execution time of your solver or any subprocess within your solver.

Import Python’s native time library:

import time

Record the start and end time, substract them and send it to the execution log:

start_time = time.time()  # Start timing

# The process you want to measure

end_time = time.time()  # End timing

elapsed_time = end_time - start_time

logger.info(f"Elapsed time (in seconds): { elapsed_time }")

4. Connect your Git repository to the platform#

Now that you have your solver source code tracked in a repository, you can connect this repository to the platform. This needed to allow the platform to access the source code to run it.

Important

Make sure to follow all the steps in the process below, specially step 3.d, where you have to go to your Git server (GitHub, for example) and add the Deploy Key provided by the Platform.

To connect a repository to the platform:

  1. Go to the section Solvers > Repositories through the left side menu
  2. Then, on the top-right corner, click on the Connect a new repository button.
  3. You will get the repository connection wizzard where you can do all the required steps:
    1. Select the solver which is hosted in this repository.
    2. Fill the repository information: name and SSH URL (valid SSH URL in your git server).
    3. Generate an SSH key pair to ensure controlled and secure access to your code.
    4. Go to your Git server: Your repository > Settings > Deploy keys and add the generated key.
  4. Click the Connect button at the end of the wizzard to complete the process.

You will see your new repository marked as Connected when this is done.

At this moment, your repository is connected and the source-code Pull process has started to build your solver. Now, just wait until this process finishes. You can check the status bellow in the Repository pulls history section.

When the pull process has finished successfully, you’ll be able to run a job with this solver.

What’s next#

Run your solver

Edit or update this solver

Get started with the API and SDK