1"""
2A class for representing invertible functions.
3"""
4
5from functools import partial
6from typing import Callable, Optional
7
8from qugrad.pulses import compose
9
[docs]
10class InvertibleFunction():
11 """A class representing an invertible function."""
12
13 _inverse: Optional["InvertibleFunction"] = None
14 """
15 The inverse of the function
16
17 Parameters
18 ----------
19 input
20 The input to the inverse function
21 *args
22 Any additional positional arguments to pass to the inverse function.
23 **kwargs
24 Keyword arguments to pass to the inverse function.
25
26 Returns
27 -------
28 Any
29 The result of the inverse function call.
30
31 Note
32 ----
33 `_inverse` should satisfy the following assertions::
34
35 assert self._inverse(self(x, *args, **kwargs), *args, **kwargs) == x
36 assert self(self._inverse(x, *args, **kwargs), *args, **kwargs) == x
37
38 for all inputs ``x``.
39 """
40
[docs]
41 def __init__(self, func: Callable):
42 """
43 Wraps the a function in an InvertibleFunction object so that an inverse
44 can be associated with it.
45
46 Parameters
47 ----------
48 func : Callable
49 The function to be wrapped
50 """
51 self._func = func
52 def __call__(self, input, *args, **kwargs):
53 """
54 Call the function with the given arguments.
55
56 Parameters
57 ----------
58 input
59 The input to the function
60 *args
61 Any additional positional arguments to pass to the function.
62 **kwargs
63 Keyword arguments to pass to the function.
64
65 Returns
66 -------
67 Any
68 The result of the function call.
69 """
70 return self._func(input, *args, **kwargs)
71 @property
72 def inverse(self):
73 """
74 The inverse of the function
75
76 Parameters
77 ----------
78 input
79 The input to the inverse function
80 *args
81 Any additional positional arguments to pass to the inverse function.
82 **kwargs
83 Keyword arguments to pass to the inverse function.
84
85 Returns
86 -------
87 Any
88 The result of the inverse function call.
89
90 Note
91 ----
92 `inverse` should satisfy the following assertions::
93
94 assert self.inverse(self(x, *args, **kwargs), *args, **kwargs) == x
95 assert self(self.inverse(x, *args, **kwargs), *args, **kwargs) == x
96
97 for all inputs ``x``.
98 """
99 if self._inverse is not None:
100 return self._inverse
101 raise NotImplementedError("This function has no inverse.")
[docs]
102 def set_inverse(self, inverse_func: Callable):
103 """
104 Sets the inverse of the function.
105
106 Parameters
107 ----------
108 inverse_func : Callable
109 The inverse function
110
111 Note
112 ----
113 ``inverse_func`` should satisfy the following assertions::
114
115 assert inverse_func(self(x, *args, **kwargs), *args, **kwargs) == x
116 assert self(inverse_func(x, *args, **kwargs), *args, **kwargs) == x
117
118 for all inputs ``x``.
119 """
120 self._inverse = InvertibleFunction(inverse_func)
121 self.inverse._inverse = self
[docs]
122 def specify_parameters(self, **kwargs) -> "InvertibleFunction":
123 """
124 Allows keyword parameters for the function and inverse function to be
125 pre-specified. This generates a new :class:`InvertibleFunction` without
126 the specified parameters in the call signatures.
127
128 Parameters
129 ----------
130 **kwargs
131 Keyword arguments to pre-specify for the function and inverse
132 function.
133
134 Returns
135 -------
136 InvertibleFunction
137 A new :class:`InvertibleFunction` with the specified parameters
138 pre-specified.
139 """
140 func = partial(self.__call__, **kwargs)
141 inverse = partial(self.inverse.__call__, **kwargs)
142 new_invertible_function = InvertibleFunction(func)
143 new_invertible_function.set_inverse(inverse)
144 return new_invertible_function
[docs]
145 def compose(self,
146 inner_invertible_function: "InvertibleFunction",
147 *args,
148 **kwargs
149 ) -> "InvertibleFunction":
150 """
151 Composes the :class:`InvertibleFunction` with another
152 :class:`InvertibleFunction` to create a new :class:`InvertibleFunction`
153 along with the composed inverse. That is the following assertions should
154 hold::
155
156 assert f.compose(g, *f_args, **f_kwargs)(x, *g_args, **g_kwargs) \
157 == f(g(x, *g_args, **g_kwargs), *f_args, **f_kwargs)
158
159 for all inputs ``x``.
160
161 Parameters
162 ----------
163 inner_invertible_function : InvertibleFunction
164 The :class:`InvertibleFunction` be be called first. The output of
165 this :class:`InvertibleFunction` will be passed to the current
166 :class:`InvertibleFunction`.
167 *args
168 Any additional positional arguments to pass to the function.
169
170 **kwargs
171 Keyword arguments to pass to the function.
172
173 Note
174 ----
175 Enough positional arguments and keyword arguments need to be passed that
176 the calling :class:`InvertibleFunction` has only one remaining parameter
177 (``input``).
178
179 Returns
180 -------
181 InvertibleFunction
182 A new :class:`InvertibleFunction` that is the composition of the two
183 functions.
184 """
185 new_invertible_function = InvertibleFunction(compose(self.__call__,
186 inner_invertible_function.__call__,
187 *args,
188 **kwargs))
189 if (inner_invertible_function._inverse is not None
190 and self._inverse is not None):
191 inverse = compose(inner_invertible_function.inverse.__call__,
192 self.inverse.__call__,
193 *args,
194 **kwargs)
195 new_invertible_function.set_inverse(inverse)
196 return new_invertible_function