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