Source code for quantum_launcher.base.adapter_structure
1from collections import defaultdict
2from typing import Dict, Callable, Any
3from inspect import signature
4import warnings
5import networkx as nx
6from .base import Problem
7
8__QL_ADAPTERS: Dict[str, Dict[str, Callable]] = defaultdict(lambda: {}) # adapters[to][from]
9__QL_FORMATTERS: Dict[type[Problem] | None, Dict[str, Callable]] = defaultdict(lambda: {}) # formatters[problem][format]
10
11
12def _get_callable_name(c: Callable) -> str:
13 """
14 Gets name of input callable, made it because callable classes don't have a __name__
15 """
16 try:
17 return c.__name__
18 except AttributeError:
19 return str(c.__class__)
20
21
22def _merge_dicts(dicts: list[dict]) -> dict:
23 out = {}
24 for d in dicts:
25 out = out | d
26 return out
27
28
[docs]
29class ProblemFormatter:
30 """
31 Converts input problem to a given format (input and output types determined by formatter and adapters in __init__)
32
33 Probably shouldn't be constructed directly, call :py:func:`get_formatter()`
34 """
35
36 def __init__(self, formatter: Callable, adapters: list[Callable] | None = None):
37 self.formatter = formatter
38 self.adapters = [a['func'] for a in adapters] if adapters is not None else []
39 self.adapter_requirements = _merge_dicts([a['formatter_requirements'] for a in adapters] if adapters is not None else [])
40
41 self.formatter_sig = signature(self.formatter)
42
43 self.run_params = {}
44
45 def _formatter_call(self, run_params, *args, **kwargs):
46 params_used = {k: v for k, v in run_params.items() if k in self.formatter_sig.parameters}
47
48 unused_count = len(run_params) - len(params_used)
49 if unused_count > 0:
50 warnings.warn(
51 f"{unused_count} unused parameters. {_get_callable_name(self.formatter)} does not accept {[k for k in run_params if not k in params_used]}", Warning)
52 return self.formatter(*args, **kwargs, **params_used)
53
54 def __call__(self, *args, **kwargs):
55 # Reset bound params
56 curr_run_params = dict(self.run_params)
57 self.run_params = {}
58
59 common_params = set(curr_run_params.keys()).intersection(set(self.adapter_requirements.keys()))
60 if len(common_params) > 0:
61 warnings.warn(
62 f"Attempting to reassign parameter values required by one of the adapters: {common_params}, those params will not be set.")
63
64 curr_run_params = curr_run_params | self.adapter_requirements
65
66 out = self._formatter_call(curr_run_params, *args, **kwargs)
67 for a in self.adapters:
68 out = a(out)
69
70 return out
71
[docs]
72 def get_pipeline(self) -> str:
73 """
74 Returns:
75 String representing the conversion process: problem -> formatter -> adapters (if applicable)
76 """
77 return " -> ".join(
78 [str(list(self.formatter_sig.parameters.keys())[0])] +
79 [_get_callable_name(self.formatter)] +
80 [_get_callable_name(fn) for fn in self.adapters]
81 )
82
[docs]
83 def set_run_param(self, param: str, value: Any) -> None:
84 """
85 Sets a parameter to be used during next conversion.
86
87 Args:
88 param (str): parameter key
89 value (str): parameter value
90 """
91 self.run_params[param] = value
92
[docs]
93 def set_run_params(self, params: dict[str, Any]) -> None:
94 """
95 Sets multiple parameters to be used during next conversion.
96
97 Args:
98 params (dict[str, Any]): parameters to be set
99 """
100 for k, v in params.items():
101 self.set_run_param(k, v)
102
103
[docs]
104def adapter(translates_from: str, translates_to: str, **kwargs) -> Callable:
105 """
106 Register a function as an adapter from one problem format to another.
107
108 Args:
109 translates_from (str): Input format
110 translates_to (str): Output format
111
112 Returns:
113 Same function
114 """
115 def decorator(func):
116 if isinstance(func, type):
117 func = func()
118 __QL_ADAPTERS[translates_to][translates_from] = {'func': func, 'formatter_requirements': kwargs}
119 return func
120 return decorator
121
122
[docs]
123def formatter(problem: type[Problem] | None, alg_format: str):
124 """
125 Register a function as a formatter for a given problem type to a given format.
126
127 Args:
128 problem (type[Problem]): Input problem type
129 alg_format (str): Output format
130
131 Returns:
132 Same function
133 """
134 def decorator(func):
135 if isinstance(func, type):
136 func = func()
137 __QL_FORMATTERS[problem][alg_format] = func
138 return func
139 return decorator
140
141
142def _find_shortest_adapter_path(problem: type[Problem], alg_format: str) -> list[str] | None:
143 """
144 Creates directed graph of possible conversions between formats and finds shortest path of formats between problem and alg_format.
145
146 Returns:
147 List of formats or None if no path was found.
148 """
149 G = nx.DiGraph()
150 for problem_node in __QL_FORMATTERS[problem]:
151 G.add_edge("__problem__", problem_node)
152
153 for out_form in __QL_ADAPTERS:
154 for in_form in __QL_ADAPTERS[out_form]:
155 G.add_edge(in_form, out_form)
156
157 if not G.has_node(alg_format):
158 return None
159
160 path = nx.shortest_path(G, "__problem__", alg_format)
161 assert isinstance(path, list) or path is None, "Something went wrong in `nx.shortest_path`"
162 return path
163
164
[docs]
165def get_formatter(problem: type[Problem], alg_format: str) -> ProblemFormatter:
166 """
167 Creates a ProblemFormatter that converts a given Problem subclass into the requested format.
168
169 Args:
170 problem (type[Problem]): Input problem type
171 alg_format (str): Desired output format
172
173 Returns:
174 ProblemFormatter meeting the desired criteria.
175
176 Raises:
177 ValueError: If no combination of adapters can achieve conversion from problem to desired format.
178 """
179 formatter, adapters = None, None
180 available_problem_formats = set(__QL_FORMATTERS[problem].keys())
181 if alg_format in available_problem_formats:
182 formatter = __QL_FORMATTERS[problem][alg_format]
183 else:
184 path = _find_shortest_adapter_path(problem, alg_format)
185
186 if path is None:
187 formatter = default_formatter
188 else:
189 formatter = __QL_FORMATTERS[problem][path[1]]
190 adapters = []
191 for i in range(1, len(path)-1):
192 adapters.append(__QL_ADAPTERS[path[i+1]][path[i]])
193
194 return ProblemFormatter(formatter, adapters)
195
196
[docs]
197@formatter(None, 'none')
198def default_formatter(problem: Problem):
199 return problem.instance