1import logging
2import pickle
3from abc import ABC, abstractmethod
4from dataclasses import dataclass
5from typing import TYPE_CHECKING, Any, Generic, Literal, TypeVar
6
7from qlauncher.base.models import Model
8
9if TYPE_CHECKING:
10 from collections.abc import Callable
11
12AVAILABLE_FORMATS = Literal['hamiltonian', 'qubo', 'bqm', 'none', 'fn', 'tabular_ml']
13
14_Model = TypeVar('_Model', bound=Model)
15_Backends = TypeVar('_Backends', bound='Backend')
16
17
[docs]
18@dataclass
19class Result:
20 best_bitstring: str
21 best_energy: float
22 most_common_bitstring: str
23 most_common_bitstring_energy: float
24 distribution: dict
25 energies: dict
26 num_of_samples: int
27 average_energy: float
28 energy_std: float
29 result: Any
30
31 def __str__(self):
32 return f'Result(bitstring={self.best_bitstring}, energy={self.best_energy})'
33
34 def __repr__(self):
35 return str(self)
36
[docs]
37 def best(self):
38 return self.best_bitstring, self.best_energy
39
[docs]
40 def most_common(self):
41 return self.most_common_bitstring, self.most_common_bitstring_energy
42
[docs]
43 @staticmethod
44 def from_counts_energies(bitstring_counts: dict[str, int], energies: dict[str, float], result: Any = None) -> 'Result':
45 """
46 Constructs the Result object from Dictionary with bitstring to num of occurrences,
47 dictionary mapping bitstring to energy and optional result (rest)
48 """
49 best_bitstring = min(energies, key=energies.get)
50 best_energy = energies[best_bitstring]
51 most_common_bitstring = max(bitstring_counts, key=bitstring_counts.get)
52 most_common_bitstring_energy = energies[most_common_bitstring]
53 num_of_samples = int(sum(bitstring_counts.values()))
54
55 mean_value = sum(energies[bitstring] * occ for bitstring, occ in bitstring_counts.items()) / num_of_samples
56 std = 0
57 for bitstring, occ in bitstring_counts.items():
58 std += occ * ((energies[bitstring] - mean_value) ** 2)
59 std = (std / (num_of_samples - 1)) ** 0.5
60 return Result(
61 best_bitstring,
62 best_energy,
63 most_common_bitstring,
64 most_common_bitstring_energy,
65 {k: v / num_of_samples for k, v in bitstring_counts.items()},
66 energies,
67 num_of_samples,
68 mean_value,
69 std,
70 result,
71 )
72
73
[docs]
74class Backend:
75 """
76 Abstract class representing a backend for quantum computing.
77
78 Attributes:
79 name (str): The name of the backend.
80 path (str | None): The path to the backend (optional).
81 parameters (list): A list of parameters for the backend (optional).
82
83 """
84
85 def __init__(self, name: str, parameters: list | None = None) -> None:
86 self.name: str = name
87 self.is_device = name == 'device'
88 self.path: str | None = None
89 self.parameters = parameters if parameters is not None else []
90 self.logger: logging.Logger | None = None
91
[docs]
92 def set_logger(self, logger: logging.Logger) -> None:
93 self.logger = logger
94
95 def _get_path(self) -> str:
96 return f'{self.name}'
97
98
[docs]
99class Problem:
100 """
101 Abstract class for defining Problems.
102
103 Attributes:
104 variant (str): The variant of the problem. The default variant is "Optimization".
105 path (str | None): The path to the problem.
106 name (str): The name of the problem.
107 instance_name (str): The name of the instance.
108 instance (any): An instance of the problem.
109
110 """
111
112 _all_problems: dict[str, type['Problem']] = {}
113
114 def __init__(self, instance: Any, instance_name: str = 'unnamed') -> None:
115 """
116 Initializes a Problem instance.
117
118 Params:
119 instance (any): An instance of the problem.
120 instance_name (str | None): The name of the instance.
121
122 Returns:
123 None
124 """
125 self.instance: Any = instance
126 self.instance_name = instance_name
127 self.variant: Literal['decision', 'optimization'] = 'optimization'
128 self.path: str | None = None
129 self.name = self.__class__.__name__.lower()
130
[docs]
131 @classmethod
132 def from_file(cls: type['Problem'], path: str) -> 'Problem':
133 with open(path, 'rb') as f:
134 instance = pickle.load(f)
135 return cls(instance)
136
[docs]
137 @staticmethod
138 def from_preset(instance_name, **kwargs) -> 'Problem':
139 raise NotImplementedError()
140
141 def __init_subclass__(cls) -> None:
142 if Problem not in cls.__bases__:
143 return
144 Problem._all_problems[cls.__name__] = cls
145 cls._mapping: dict[type[Model], Callable[[], Model]] = {}
146 for method_name in cls.__dict__:
147 if method_name.startswith('to_'):
148 method = cls.__dict__[method_name]
149 cls._mapping[method.__annotations__['return']] = method
150
[docs]
151 def read_result(self, exp, log_path):
152 """
153 Reads a result from a file.
154
155 Args:
156 exp: The experiment.
157 log_path: The path to the log file.
158
159 Returns:
160 The result.
161 """
162 exp += exp # ?: this is perplexing
163 with open(log_path, 'rb') as file:
164 return pickle.load(file)
165
[docs]
166 def analyze_result(self, result) -> Any:
167 """
168 Analyzes the result.
169
170 Args:
171 result: The result.
172
173 """
174 raise NotImplementedError()
175
[docs]
176 def to(self, problem_type: type[Model]) -> Model:
177 name = problem_type.__name__.lower()
178 if hasattr(self, f'to_{name}'):
179 return getattr(self, f'to_{name}')()
180 raise TypeError
181
182
[docs]
183class OptimizationAlgorithm: ...
184
185
[docs]
186class Algorithm(ABC, Generic[_Model, _Backends]):
187 """
188 Abstract class for Algorithms.
189
190 Attributes:
191 name (str): The name of the algorithm, derived from the class name in lowercase.
192 path (str | None): The path to the algorithm, if applicable.
193 parameters (list): A list of parameters for the algorithm.
194 alg_kwargs (dict): Additional keyword arguments for the algorithm.
195
196 Abstract methods:
197 __init__(self, **alg_kwargs): Initializes the Algorithm object.
198 _get_path(self) -> str: Returns the common path for the algorithm.
199 run(self, problem: Problem, backend: Backend): Runs the algorithm on a specific problem using a backend.
200 """
201
202 def __init__(self, **alg_kwargs) -> None:
203 self.name: str = self.__class__.__name__.lower()
204 self.path: str | None = None
205 self.parameters: list = []
206 self.alg_kwargs = alg_kwargs
207
[docs]
208 def parse_result_to_json(self, o: object) -> dict:
209 """Parses results so that they can be saved as a JSON file.
210
211 Args:
212 o (object): The result object to be parsed.
213
214 Returns:
215 dict: The parsed result as a dictionary.
216 """
217 print('Algorithm does not have the parse_result_to_json method implemented')
218 return o.__dict__
219
223
226
[docs]
227 @abstractmethod
228 def run(self, problem: _Model, backend: _Backends) -> Result:
229 """Runs the algorithm on a specific problem using a backend.
230
231 Args:
232 problem (Problem): The problem to be solved.
233 backend (Backend): The backend to be used for execution.
234 """