Managing Optimizations and Solutions
Luna simplifies the process of creating, storing, and managing optimizations, allowing you to efficiently solve complex problems. By leveraging Luna's intuitive interface and powerful tools, you can seamlessly define optimization problems, execute them on classical, quantum, or hybrid hardware, and analyze the results. Additionally, Luna provides robust mechanisms for managing solutions, enabling you to store, visualize and refine them to achieve optimal outcomes. This streamlined approach enhances your workflow and ensures that you can focus on finding the best solutions for your problems.
Upon completing this tutorial, you will be able to:
- Define and Configure Optimization Problems in Luna.
- Effortlessly execute and manage solutions using a wide array of algorithms.
Managing Optimizations
First, you'll need to upload an optimization problem to Luna. In this example, we'll use a simple optimization in the BQM format.
from luna_sdk.schemas.optimization_formats.bqm import BQMSchema
# Define your QUBO problem as a BQM
bqm_data = {
"linear": {"0": 4.0, "1": -2.0, "2": 6.0, "3": 2.0, "4": 5.0},
"quadratic": {("3", "2"): -6.0, ("4", "0"): -4.0},
"vartype": "BINARY"
}
bqm = BQMSchema(**bqm_data)
# Upload your BQM to LunaSolve
optimization = ls.optimization.create_from_bqm(name="My BQM", bqm=bqm)
Luna makes it easy to manage all your stored optimizations. You can retrieve, edit, and delete them.
# Retrieve an optimization
optimization = ls.optimization.get(optimization_id=optimization.id)
print(optimization)
Output:
quadratic:
('3', '2'): -6.0
('4', '0'): -4.0
linear:
2: 6.0
3: 2.0
0: 4.0
4: 5.0
1: -2.0
vartype: BQMVarType.BINARY
offset: 0.0
id: 664e1452acaf90d0a917a4e3
name: My BQM
created_date: 2024-05-22 17:50:42.070000
created_by: 66291cab401df553a5f5ccd6
modified_date: None
modified_by: None
use_case_name: None
params: None
Furthermore, you can retrieve ally our created optimization problems:
from luna_sdk.schemas.enums.timeframe import TimeframeEnum
all_optimizations = luna.optimization.get_all(
timeframe=TimeframeEnum.this_week, #Timeframe when solution was created
input_type=InputType.qubo, #Type of Problem
limit=10 #Limit of optimizations returned, default is 50.
offset=0, #Pagination
)
# Rename an optimization
updated_optimization = ls.optimization.rename(optimization_id=optimization.id, name="My Updated BQM")
print(updated_optimization)
Output:
id: 664e1452acaf90d0a917a4e3
name: My Updated BQM
created_date: 2024-05-22 17:50:42.070000
created_by: 66291cab401df553a5f5ccd6
modified_date: None
modified_by: 66291cab401df553a5f5ccd6
use_case_name: None
params: None
# Delete an optimization
ls.optimization.delete(optimization_id=optimization.id)
Managing Solutions
First, you'll need to create a solution for an optimization via Luna. We'll use the same BQM from earlier and generate a simple solution using the Simulated Annealing algorithm.
# Define your QUBO problem as a BQM
bqm_data = {
"linear": {"0": 4.0, "1": -2.0, "2": 6.0, "3": 2.0, "4": 5.0},
"quadratic": {("3", "2"): -6.0, ("4", "0"): -4.0},
"vartype": "BINARY"
}
bqm = BQMSchema(**bqm_data)
# Upload your BQM to LunaSolve
optimization = ls.optimization.create_from_bqm(name="My BQM", bqm=bqm)
# Create a solution for the BQM using SA
job = ls.solution.create(
optimization_id=optimization.id,
solver_name="SA",
provider="dwave"
)
Similar to optimizations, Luna provides the same features for managing solutions. Users can easily print solution details, obtaining all necessary information for their problem. Luna provides two options for solution visualization. Using print(solution)
displays all problem solutions, print(solution.head)
highlights 5 solutions among all the availabe possibilities. If you only need to verify that the solution process was successful and don't require all solutions to be displayed, you can use print(solution.head)
. This will provide the necessary information about the solution object without displaying every single solution.
# Retrieve an optimization
solution = ls.solution.get(solution_id=job.id)
print(solution.head)
Output:
--------------------------------------------------------------------------------
META DATA:
--------------------------------------------------------------------------------
id: 664e1574ca894b28dc87b1e0
name: Your_Solution_Name
created_date: 2024-05-22 17:55:32.998000
created_by: 66291cab401df553a5f5ccd6
modified_date: 2024-05-22 17:55:33.116000
modified_by: None
error_message: None
params:
num_reads: None
num_sweeps: 1000
beta_range: None
beta_schedule_type: geometric
initial_states_generator: random
num_sweeps_per_beta: 1
seed: None
interrupt_function: None
beta_schedule: None
initial_states: None
randomize_order: False
proposal_acceptance_criteria: Metropolis
runtime:
total: 0.005857708998519229
qpu: None
sense: SenseEnum.MIN
provider: dwave
status: StatusEnum.DONE
optimization:
id: 664e1574ca894b28dc87b1df
name: My BQM
created_date: 2024-05-22 17:55:32.366000
created_by: 66291cab401df553a5f5ccd6
modified_date: None
modified_by: None
use_case_name: None
params: None
representation: None
--------------------------------------------------------------------------------
RESULTS:
--------------------------------------------------------------------------------
1 results found. Displaying first 5 results.
Result 1:
{'sample': {'0': 0.0, '1': 1.0, '2': 0.0, '3': 0.0, '4': 0.0}, 'obj_value': -2.0, 'feasible': True, 'constraints': {}}
--------------------------------------------------------------------------------
DWAVE META DATA:
--------------------------------------------------------------------------------
beta_range: [0.11552453009332421, 4.605170185988092]
beta_schedule_type: geometric
timing:
preprocessing_ns: 5021833
sampling_ns: 638458
postprocessing_ns: 189417
# Retrieve all solutions available to you
all_solutions = ls.solution.get_all(
timeframe=TimeframeEnum.this_week, #Timeframe when solution was created
limit=10 # Limit of solutions returned, default is 50.
offset=0, #Pagination
)
# Delete a solution
ls.solution.delete(solution_id=job.id)
Because some algorithms return multiple possible solutions, Luna allows you to retrieve only the best result from any created solutions.
ls.solution.get_best_result(solution)
When you create a solution through our use case library, Luna also offers the option to directly access the best result within the use case representation.
import networkx as nx
from luna_sdk.schemas import TravellingSalesmanProblem
# Create an empty graph using networkx
graph = nx.Graph()
# Define the cities we want to visit
cities = ['Berlin', 'Hamburg', 'Munich']
# Add the cities as nodes in our graph
graph.add_nodes_from(cities)
# Define the distances between each city
edges = [('Berlin', 'Hamburg', 289),
('Berlin', 'Munich', 586),
('Hamburg', 'Munich', 796)]
# Add the distances as the edges in our graph
graph.add_weighted_edges_from(edges)
# Convert the networkx graph to a digestible format for Luna
graph_data = nx.to_dict_of_dicts(graph)
# Define that we want to create a TSP with the corresponding graph data
tsp = TravellingSalesmanProblem(graph=graph_data)
# Create a TSP instance and retrieve the corresponding ID
optimization = ls.optimization.create_from_use_case(name="My TSP", use_case=tsp)
# Create a solution for the TSP using SA
job = ls.solution.create(
optimization_id=optimization.id,
solver_name="SA",
provider="dwave"
)
# Instead of retrieving the raw solution, get it in the format fitting for your problem formulation
use_case_solution = ls.solution.get_use_case_representation(job.id)
# Finally, get the best result directly in the use case representation
result = ls.solution.get_best_use_case_result(use_case_solution)
print(result)
Output:
representation=[['Hamburg'], ['Munich'], ['Berlin']] obj_value=-3111.0
Interpreting Solution Runtimes
Each solution you create via Luna contains a Runtime
object, which itself has the fields total
and qpu
.
The total
field holds a value we measured ourselves: it is the time in seconds that elapsed from between calling a solver and getting back a result from the solver.
The qpu
field only holds a value when the solver that created the solution runs on a backend other than our own servers.
It reports the runtime in seconds on the hardware of our providers. Note that it also contains a value for non-quantum hardware, e.g., when solving with Fujitsu's Digital Annealer.
In the past, the overhead
field was used to report overhead times, e.g., for preprocessing an optimization to be in a suitable format. It was removed as a single float value was not enough to reasonably express the different kinds of overheads during the optimization process. You can now find relevant overhead values in the solution metadata.
For example, if you had created the solution in the previous section with DWave's quantum annealer, you could expect the solution runtime to be of the following format:
print(solution.runtime)
Output:
total=3.362961308001104 qpu=0.01587886
Depending on the provider and the specific solver, solutions may also have timing information in the solution's metadata field. These metadata differ from provider to provider, and we mostly just pass on the values we obtain from the provider. The expected timing metadata per provider are summarized below.
DWave
For a general overview over which runtime metadata to expect from DWave solvers, check out DWave's website.
When you create a solution
with DWave's quantum annealer, the metadata may look like this:
print(solution)
Output:
<...>
--------------------------------------------------------------------------------
DWAVE META DATA:
--------------------------------------------------------------------------------
timing:
qpu_sampling_time: 94.5
qpu_anneal_time_per_sample: 20
qpu_readout_time_per_sample: 53.92
qpu_access_time: 15878.86
qpu_access_overhead_time: 1.14
qpu_programming_time: 15784.36
qpu_delay_time_per_sample: 20.58
total_post_processing_time: 1
post_processing_overhead_time: 1
problem_id: "75464a4b-64aa-495a-bdd6-b08344b1e9f8"
When you create a solution with the RRQA or the QAGA solver, we will make several calls to DWave's quantum annealer, and therefore obtain runtime metadata several times. We will aggregate these runtime metadata on our side, so that you can obtain them in the same way as for the other solvers. If you happen to need runtimes for the single jobs, you can get them via the dashboard of your DWave Leap account.
Fujitsu
When you create a solution with Fujitsu's Digital Annealer, e.g., DA V2, you can expect runtime metadata to be of the following format. Note that metadata that are not runtime-related have been left out for conciseness.
print(solution)
Output:
<...>
--------------------------------------------------------------------------------
FUJITSU META DATA:
--------------------------------------------------------------------------------
Elapsed time (from - to): "2024-06-24 22:13:04.492955 - 2024-06-24 22:13:16.020305"
Elapsed time: 11.52735
Reduce degree time (from - to): "2024-06-24 22:13:04.493147 - 2024-06-24 22:13:04.493193"
Reduce degree time: 0.000046
Tuning time (from - to): "2024-06-24 22:13:04.493219 - 2024-06-24 22:13:04.493232"
Tuning time: 0.000013
Prepare QUBO time (from - to): "2024-06-24 22:13:04.493255 - 2024-06-24 22:13:04.493487"
Prepare QUBO time: 0.000232
Account occupied time (from - to): "2024-06-24 22:13:04.493523 - 2024-06-24 22:13:08.060695"
Account occupied time: 3.567172
Prepare request time (from - to): "2024-06-24 22:13:08.060750 - 2024-06-24 22:13:08.061170"
Prepare request time: 0.00042
Send request time (from - to): "2024-06-24 22:13:08.061197 - 2024-06-24 22:13:11.073558"
Send request time: 3.012361
Execution time: 0.198
Queue time: 0.001
Solve time: 0.197
CPU time: 0.15
Anneal time: 0.047
Waiting time (from - to): "2024-06-24 22:13:11.073558 - 2024-06-24 22:13:12.500559"
Waiting time: 1.427001
Receive response time (from - to): "2024-06-24 22:13:13.927612 - 2024-06-24 22:13:16.019925"
Receive response time: 2.092313
Parse response time (from - to): "2024-06-24 22:13:16.019963 - 2024-06-24 22:13:16.020278"
Parse response time: 0.000315
Fujitsu runtimes of type float
always represent the respective runtime in seconds.