Status | Deprecated |
---|---|
RFC # | 0001 |
Authors | Julien Gacon ([email protected]) |
Submitted | 2020-01-06 |
Updated | 2023-05-10 |
Currently, creating a new variational form class requires a lot of copy+paste.
Remember: RYRZ
has all of that, too!
class RY(VariationalForm):
"""Layers of Y rotations followed by entangling gates."""
def __init__(self, num_qubits, depth=3, entangler_map=None, entanglement='full', initial_state=None,
entanglement_gate='cz', skip_unentangled_qubits=False, skip_final_ry=False):
self.validate(locals())
super().__init__()
""" 30 more lines ... """
self._bounds = [(-np.pi, np.pi)] * self._num_parameters
self._support_parameterized_circuit = True
def construct_circuit(self, parameters, q=None):
if len(parameters) != self._num_parameters:
raise ValueError('The number of parameters has to be {}'.format(self._num_parameters))
""" 39 more lines ... """
return circuit
Setting up a new variational form should be quick and easy, more like:
class RY(TwoQubitAnsatz):
def __init__(self, ...):
super.__init__(single_qubit_gate='ry', ...)
If a user wants to quickly try a new format, it would convenient to set up the variational form in just a single line. Not all ideas must become a complete, new class.
rxrycx = MultiQubitAnsatz(num_qubits, single_qubit_gate=['rx', 'ry'], two_qubit_gate='cx', depth=4)
The circuits that current variational forms build are not extensible. For algorithms, such as adaptive VQE, adding new layers to the variational forms is necessary.
varform.add(additional_gate, qubit_indices) # add `additional_gate` at `qubit_indices`
Other applications require the insertion of a new gate or layer not at the end of the variational form but somewhere in the middle. The position could e.g. be handled by the appearance of a certain Parameter
instance.
This can be used for the calculation of analytic gradients. By inserting a set of Paulis after a parameter and controlling the operation on an ancilla, we can compute the gradient with respect to that parameter (see this paper).
target_param = Parameter('θ') # this variable is also used in the circuit
varform.insert(target_param, instr_to_insert, qubit_indices)
The insertion might be difficult to implement, what would the position
be? This feature might be too specific and implementation of special cases may be left up to the users.
A variational form is essentially a gate preparing a parameterized state. For chemistry, this could be the preparation of certain excitation. If we want multiple excitations, we should be able to just combine the respective variational forms.
S0, S1 = single_excitation('θ0'), single_excitation('θ1')
D = double_excitation('θ2')
UCCSD = S0 + S1 + D
If some variational forms prepare subsystems, they should be able to be combined at certain qubit indices.
varform = sub_varform.append(other_varform, indices)
Possibly allow only combine
and not __add__
, since this is dangerously close to add(new_layer)
and might result in confusion of what types can be added.
Variational forms can be very diverse and for special problems, special structures may perform better than others. Therefore, users should be able to their own Ansaetze as variable as possible. For this we can support automatic repetition (with correct setting of the parameters).
The repetitions can either be specified by a single integer or layer indices.
varform = Ansatz([gate_1, gate_2], [indices_1, indices_2], reps=3)
varform = Ansatz([gate_1, gate_2], [indices_1, indices_2], reps=[0, 1, 0, 1, 0, 1])
Instead of gates we can also allow fancier options, e.g. provide a Callable that accepts a repetition number or similar.
What happens to the parameters in the gates if they get repeated? Do they just get repeated?
If we stored them in a list, the user could later on overwrite them in construct_circuit
.
We could also add some default parameters, since the user might not expect there to be the same parameters? If these default parameters haven't been substituted we could raise a warning.
Repeating the parameters is probably the more intuitive solution. If the user wants different parameters, they must be set afterwards -- as it is also the case with default parameters.
Several algorithms, such as QAOA, require an Operator-evolution type Ansatz, such as
Currently, this is cumbersome to implement. Similar to the ''DIY'' Ansaetze, we could support an OperatorAnsatz
based on Ansatz
:
varform = OperatorAnsatz([Op1, Op2], reps=4)
The concept of a variational form is broad. In support of a clear structure, we should group similar concepts. Then,
- users are less confused and see what concepts have the same base,
- we can reuse the same base functionalities,
- we ensure that similar objects in Aqua behave the same way.
Therefore, a common base for feature maps and variational forms would be sensible. Aqua hosts more facilities to construct circuits, as e.g. the InitialState
or the UncertaintyModel
, but as discussed towards the end of this doc these are best not derived from Ansatz.
However, it should be our goal that all objects producing circuits in Aqua behave the same way. Ideally, they also feel like objects from Terra. This means finding a similar base for
- variational forms
- feature maps
- initial states
- uncertainty models
- grover operators
- amplitude estimation
$\mathcal A$ operators
class MyFeatureMap(Ansatz):
# implement
class MyMultiQubitVarform(Ansatz):
# implement
Terra provides the Parameter
class for parameterized circuits. This can speedup the repeated execution of the same circuits with different parameters, as we can pre-transpile the circuit and substitute the parameters afterwards.
This significantly improves our performance since transpiling is costly!
By transpiling the circuit the first time construct_circuit
is called, the user benefits from pre-transpiling without specifying it manually.
def construct_circuit(self, transpiled=True, # set to False for untranspiled circuit
parameter_prefix=default_prefix)
Also, we could simplify the setting of parameters, if the user just quickly wants a circuit to look at.
circuit = varform.construct_circuit() # uses some default naming
circuit = varform.construct_circuit(parameter_prefix='θ')
Barriers can be very useful for visualization, but if a circuit is executed only and not looked at, we don't need them and they can even slow down transpilation. Why insert them by default?
circuit = varform.construct_circuit(insert_barriers=True) # False by default
For this feature we would have to store a has_barriers
variable and if the user changes back and forth between barriers and no barriers we would have to rebuild the circuit. (Workaround possible by storing both, but back-and-forth changing is not likely anyways.)
Let's summarise the benefits of having an Ansatz
class satisfying the requirements from the motivation.
- Beginner user
- flexibility: can get creative and easily assemble new variational forms
- performance: can
construct_circuit
without specifying parameters, and thereby benefits from potential speedup without even touching Terra'sParameter
class
- Advanced user
- needs less copy+paste to implement standard variational forms
- can also benefit from
Parameter
related speedup - has lots of lots of flexibility
- by adding and combining varforms
- inserting new layers
- wrapping parameterized gates
- gets less confused by the multitude of Ansatz-like objects in Aqua
The Ansatz class reconciles all classes that prepare an ansatz or wavefunction. Objects of the Ansatz type share the Ansatz interface and can be used as building blocks in larger circuits and algorithms. This has the key advantage, that the same concepts behave similarly and we avoid code duplication.
More specialized types, that can also provide an easy setup or special behaviour, are derived from the Ansatz.
See the next Figure.
Currently, Aqua (6.x) distinguishes between a VariationalForm
and a FeatureMap
, which are both abstract classes that specify interfaces. These interfaces, however, almost identical and could be merged. Further, to new users the terms "variational form" and "feature map" might be confusing, whereas the word "Ansatz" wraps both concepts.
Thus, by introducing the Ansatz
as joint class we can directly derive PauliExpansion
and TwoLocalGateAnsatz
without introducing new levels of inheritance, which might be sources of confusion.
The objects RY
/RYRZ
are displayed as objects derived from not Ansatz
but an intermediate type. This is discussed in the implementation section.
To be discussed in more detail.
- Ansatz
- Variational forms
- TwoLocalGateAnsatz
- Standard Forms
- Feature maps
- Pauli expansions
- First/Second/... order
- Initial state objects
Note: We know use the term Ansatz for the concept of a parameterized circuit preparing a parameterized state, not variational form.
- Take a list of gates and a number of repetitions and construct an Ansatz of desired depth
- Provide
construct_circuit(params=None, transpiled=True, insert_barriers=False)
. Note, thatparams
must supportlist[float | Parameter]
. - Combine two Ansatz objects at given indices
- Insert new gates at a specified place
- Backward compatibility with all previous
VariationalForm
objects
Code checklist:
def __init__(self, layers, indices, reps)
def construct_circuit(params=None, transpiled=True, parameter_prefix=default_prefix, insert_barriers=False)
def __add__(other) # easy appending of Ansaetze, and maybe for various kinds of RHS/LHS types?
def append(other, connection_indices=None) # for Ansatz + Ansatz at special indices
def add_layer(gate, indices=None, position=end) # to add a new gate/QuantumCircuit
def insert_at_param(target_param, gate, indices=None) # to insert after/before a certain parameter
Make an Ansatz essentially a QuantumCircuit
object.
- already provides:
__add__(QuantumCircuit/Ansatz)
andappend(Gate)
- gives the user a Terra-like feel for the Ansatz
- parameter setting by
list[float | Parameter]
not inherently supported - a circuit doesn't have
construct_circuit
-- but we can work without that - how to insert barriers or new gates at arbitrary positions? (use
data
member?)
We lose flexibility. What was the initial gate object the user gave us? Missing performance benefit of pre-transpilation.
- inserts another level of inheritance (or maybe just use
QuantumCircuit
as component?) construct_circuit
is required for backward compatibility and the method name is very misleading- an Ansatz fits more the concept of an Gate in the sense that it has no registers attached. Therefore it should probably not be a
QuantumCircuit
.
Make an Ansatz essentially a Gate
object. This could achieved by either deriving from a gate or wrapping it.
- intuitively that's what an Ansatz is: a (parameterized) gate
- provides some required functionalities: allows setting of parameters as list, can be controlled, repeated, etc.
- no registers bound to it
- stores parameters as
list
, setting parameters bylist[float | Parameter]
is straightforward - a
construct_circuit
method makes sense and is maybe missing anyways (orto_circuit
) - gives the user a Terra-like feel for the Ansatz
- how to insert barriers or new gates at arbitrary positions?
- need to implement the appending of new gates/instructions (use
definition
?) - exposes us to future changes of Terra's hierarchy
- inserts another level of inheritance (or maybe just use
Gate
as component?)
Base an Ansatz on a list of Gate
s or Instruction
s (each item with a set of qubit indices).
The parameters would be kept in a list.
- natural implementation of repeating layers
- not dependent on changes in Terra
- internal data structure reflects input parameters of the user
- appending new instructions is trivial
- adding two Ansaetze (or even other objects, e.g.
QuantumCircuit
orGate
) is straightforward: just add the respective lists of gates, the indices, reps, and the parameters - barriers are easily inserted in between the layers
- same issue as with
Gate
s to insert a new gate somewhere inside a layer
- this implements a parallel structure of functionalities that might go into Terra anyways
Ansatz as QuantumCircuit
is not suitable: not the same performance benefit as the other designs, loose information of user input, construct_circuit
is miselading.
Using Gate
s instead seems very convenient, conceptually and from an implementation point of view. If the Ansatz interally maintains a Gate
(vs. being derived from it) gives us better control of the user information and naturally gives a sense of layers.
Suggestion Use design #2, and implement it as the Ansatz internally maintaining a Gate
, but feeling like one.
Does Ansatz provide all functionalities of the current VariationalForm
?
Almost.
Missing:
num_parameters
setting(self)
parameter_bounds(self)
depth
Prints a formatted version of self.__dict__
. This can be replaced by implementing __rep__
and thus, setting
can be removed.
This is necessary since Terra doesn't allow to bind complex values to a Parameter
object. Once this is allowed, the support_parameterized_circuit
attribute can be removed.
The parameter_bounds
are supposed to be the valid ranges for the parameters of an Ansatz. They can be forwarded to an optimizer to limit the search range and, thus, improve performance.
An issue with allowing this attribute is that it suggests the Ansatz will enforce the parameter bounds to be respected. However, this is not correct.
Possible options:
- Keep the attribute and give a warning, that the bounds are not enforced. Here we have two choices:
- the bounds are an attribute of the Ansatz itself
- the bound on a parameter is an attribute of the parameter itself. The Ansatz forwards parameter bounds it gets to its parameters, and if asked for bounds, the Ansatz asks the parameters.
- Generate the parameter bounds on demand, based on the gates the Ansatz contains. If the standard gates knew their parameter range for optimization, this could be solved elegantly.
- Issue: the standard gates providing bounds is dangerous, as periodicity might be a feature we want to use, or the gate angle is subject to a coefficient invalidating the ranges.
This equals the reps
keyword of Ansatz
. Both VariationalForm
and FeatureMap
use the word depth
instead. Depth however, is ambiguous and could also refer to the circuit depth instead of the number of layers.
Using reps
instead of depth
in the future is therefore desirable. With a deprectation warning the attribute depth
can still be supported for a while.
Another possible use of depth
could be the following. One restriction on current hardware is the depth of the circuit to be simulated. To easily create Ansaetze of feasible circuit depth, the depth
attribute could specify the maximum depth of the Ansatz. Instead of repeating the input gates x times, we could repeat them until the maximum depth is reached.
Input: number of qubits, one qubit gate(s), two qubit gate(s), depth/reps, entanglement, skip_unentangled_qubits, skip_final_rotation, (initial state?)
Output: the specified Ansatz
In the initializer, build the rotation and entanglement gates from the single- and two-qubit gates and the entanglement information. From the depth/reps set up the correct repetition indices. In construct_circuit
, build the circuit. Theoretically, the construct_circuit
of the Ansatz
class can be used.
Hence derivation is sensible.
class TwoLocalGateAnsatz(Ansatz)
- Should the keywords
entanglement
andentangler_map
be merged toentanglement
? - Deprecate the
initial_state
keyword?
Should the keywords entanglement
and entangler_map
be merged to entanglement
?
- Having multiple keywords forces the code to decide which keyword overrules which. This can be opaque to the user. If we don't enforce a "keyword hierarchy", we could throw errors if multiple are set. (And who likes runtime errors?)
- Assume we add a new possible input type, e.g.
Callable
. Then we would require yet another keyword and choose which keyword is actually active if multiple are set. With one keyword only, we just have to add anisinstance
if-statement (which we anyways need). - The user doesn't need to think about which keyword he needs to use for the entanglement input, whether it is specified via string or pairs of qubits.
- The signature doesn't clearly show what input types of entanglement exist. (Then again, there is such a thing as docstrings.)
Conclusion Yes, merge entanglement
and entangler_map
in entanglement
.
Such forms as RY
or RYRZ
.
As subclass of TwoLocalGateAnsatz
.
- super easy to implement
- natural design: The standard Ansaetze are specifications of the two-local gate ansatz, obtained by fixing the single qubit gate and providing a default two qubit gate.
- adds another level of inheritance
RY(Ansatz)
is elegant to read, sinceRY
is just an Ansatz
As subclass of Ansatz
, using TwoLocalGateAnsatz
as component.
RY(Ansatz)
is intuitive to read- no new level of inheritance
- probably need to wrap all functions of
TwoLocalGateAnsatz
Inheritance is easier to implement and reading RY(TwoLocalGateAnsatz)
is still comprehensible.
Therefore, choose inheritance over composition.
class RY(TwoLocalGateAnsatz):
def __init__(self, ...): # repeat all keywords here to enable IDE detection
super().__init__(single_qubit_gate='ry', ...)
class RYRZ(TwoLocalGateAnsatz):
def __init__(self, ...):
super().__init__(single_qubit_gate=['ry', 'rz'], ...)
Does Ansatz provide all functionalities of the current FeatureMap
?
Yes -- except self.feature_dimension
.
Possible solutions include
-
New class
FeatureMap(Ansatz)
: An additional class for just one new method, that exists under another name, is not really justified. -
Add the method
feature_dimension
toAnsatz
: If Ansatz is not used as replacement of current feature maps this method remains unused. -
Deprecate
feature_dimension
, usenum_parameters
instead: Deprecatingfeature_dimension
is probably not a good idea, since it is a legitimate attribute of a feature map.
Conclusion The best solution seems to be adding, but deprecating, the feature_dimension
method.
The hierarchy within the "feature map" branch should be the same as for the "variational form" branch. Meaning, we implement a PauliExpression
and the specialized classes as derived objects. This design is also the current form.
However -- is the additional layer PauliZExpansion
truly necessary?
def __init__(self, feature_dimension, depth=2, entangler_map=None, entanglement='full', z_order=2,
data_map_func=self_product):
pauli_string = []
for i in range(1, z_order + 1):
pauli_string.append('Z' * i)
super().__init__(..., paulis=pauli_string)
Later on used in the definition of the first- and second-order expansions.
class FirstOrderExpansion(PauliZExpansion): # resp. SecondOrderExpansion
def __init__(self, feature_dimension, depth=2, data_map_func=self_product):
super().__init__(..., z_order=1) # resp. z_order=2
- simplifies the definition of Pauli Z expansion feature maps (such as
First/SecondOrderExpansion
)
- introduces an additional layer of inheritance
- if
First/SecondOrderExpansion
is the main motivation for this class, then the two minor changes they introduce is probably not worth introducing thePauliZExpansion
The InitialState
class allows to obtain circuits that prepare certain (fixed) states. Currently, these include a the zero-state, a uniform superposition, the preparation of a specified statevector, or the an Ansatz with certain parameters.
A fixed circuit is merely a special case of a Ansatz with zero parameters, thus, the initial states can be seen as Ansaetze.
However, all the parameter business and repeating is not needed for an initial state object and we could probably simplify the object. Also the initial state includes some functionalities (as statevector preparation) that an Ansatz doesn't. Thus, it might be less confusing for the user to keep the InitialState
object.
Uncertainty models (UM) encode their underlying probability distribution in the amplitude of the qubits, i.e. they perform the mapping
In this sense they could also fall in the category of an Ansatz with empty parameter set, i.e. an initial state.
However, to set up the UM for an arbitrary probability distribution they must be trained (e.g. via qGANs). To this end, they accept an Ansatz
Hence, they should not be an Ansatz, but rather use on as component.
Notetaker/Steering: Donny
- Kevin (@kdk)
- Terra can help in some areas, e.g. Barriers for print but not transpilation
- Should clarify what the Terra objects under the hood will be, or hide them, so as not to confuse users
- Should think harder about eager transpilation timing, so transpile only happens when we know about the backend
- Stefan (@stefan-woerner )
- What is the difference between the three types of terra circuits?
- Transpilation - there is a difference between optimizing the gate and transpiling for hardware
- Initialstate, UncertaintyModel - All overlapping, should be made the same
- Panos (@pbark )
- Transpilation - Not doable to do pre-transpilation, because it depends on the parameters. Would be hard if doable.
- Agrees with Stefan that more thought needs to go into organizing Featuremaps, InitialState, etc.
- Inserting parameters will be difficult and maybe overly complicated
- Christa (@Zoufalc )
- We need the gate insertion for gradients
- Initial state and uncertainty problem isn’t really an ansatz, it’s just a fixed circuit
- Ali (@ajavadia )
- Implementation - Should this just be derived from QuantumCircuit, or gate or something else, because it has a lot of overlapping functionality (append, etc.)?
- Difference between circuit, gate, instr - On our radar. Transpiling only one section of a gate should work by transpiling a circuit, and then calling to_gate(), and using that as is.
- Steve (@woodsp-ibm )
- Originally, Aqua came from a place where components were each pieces of circuits. We didn’t care what was under the hood, but want a consistent interface. Whatever we do here should be the same in the all the components.
- It’s good to have common classes, but not necessary to mush everything, e.g. InitialState, together into one class just to say we did.
- Parameters, transpile - if it’s not necessary to pass hardware information we shouldn’t. It would be better to just pass the user a circuit and have them transpile.
- Mutability - If I have an Ansatz and do an append, will the algorithm understand? If you want to build an ansatz, can you just stack some together, instead of building complicated functionality to modify?
- Donny (@dongreenberg )
- Like that the purpose of the Anastz as a thing that provides a service of repeating gates, and like Ali’s point that maybe this is just a sparse representation of a circuit. Context on transpile - when we got rid of caching, we started trying to do this eager transpile.
-
Julien (@Cryoris)
- Philosophy: Python is a language with lots of syntactic sugar and just works, and wanted to see that in the ansatz.
- Wanted things to be easy to understand across Aqua, where if you understand the Ansatz you realize that you understand the Featuremap and other too
- Backward compatibility must be maintained
- Combining things - InitialState, FeatureMap, etc. motivation is just to simplify these to make them familiar.
-
Questions
- Bounds - The Ansatz has bounds, but doesn’t enforce them. Should a gate have bounds?
- Unresolved, table for later
- Can we support a “parameterized_circuit” property in the gate?
- Kevin: yes
- Inherit or hold Circuit / Gate / Instruction?
- Table for later
- Bounds - The Ansatz has bounds, but doesn’t enforce them. Should a gate have bounds?
- All - Conditionally Approve, pending resolving Circuit inheritance vs. encapsulation discussion.
After follow up discussion, it was decided for the Ansatz to hold a gate or circuit, rather than inherit from Gate or QuantumCircuit, to allow for some flexibility as this design settles (inheritance introduces strong coupling which is hard to extricate later), and to allow for diverse Ansatze, such as linear combinations of circuits, not-gate.
Final rating: Approved
Here's a code skeleton of the Ansatz
class wrapping a gate, along with how this design would fit in the workflow of Aqua.
This is just an interface.
from qiskit import QuantumCircuit
from qiskit.circuit import Gate
class Ansatz:
def __init__(self, gates, qargs=None):
pass # include the setup functionalities as discussed in this document
@property
def num_qubits(self):
return self._gate.num_qubits
@property
def params(self):
return self._gate.params
@params.setter
def params(self, params):
self._gate.params = params
def control(self, num_ctrl_qubits=1, label=None):
return self._gate.control(num_ctrl_qubits, label)
def repeat(self, n):
return self._gate.repeat(n)
def inverse(self):
return self._gate.inverse()
def copy(self):
"""Get a copy of self. Can be used to append a copy of self to a QuantumCircuit."""
return copy.deepcopy(self)
def to_instruction(self):
"""To support QuantumCircuit.append(this)"""
return self._gate
# potentially
@property
def definition(self):
return self._gate.definition
Using the Gate
interface also for Amplitude estimation's
A, Q = AOperator(...), QOperator(...) # A and Q support all Gate functionality
m = 3
i_objective = m
ae = QuantumCircuit(m + 1)
# initial A operator
ae.append(A, qargs=[i_objective])
# Q operators
for mi in range(m):
ae.barrier()
ae.append(Q.repeat(2**mi).control(), qargs=[mi, i_objective])
# final fourier transform, this should also behave like a Gate
ae.append(Standard(m), qargs=list(range(m)))
Replacing the Gate
-interface leads to the following changes and replacements summarized in this table.
before | now |
---|---|
inherit from CircuitFactory |
inherit from Gate |
overwrite build(self, qc, q) |
overwrite _define(self) |
build_inverse() |
inverse() |
build_power(n) |
repeat(n) |
build_controlled() |
control() |
build_controlled_power(n) |
repeat(n).control() specialized doesn't exist yet: add controlled_power |
a_factory.build(qc, q) |
qc.append(a_operator, indices, []) or qc += a_operator.to_circuit() |
The following cell shows an example code skeleton for the Gate
methods, it is sensible to directly derive it from the Gate
class.
class AOperator(Gate):
def __init__(self):
pass
def value_to_estimation(self, value):
return value # not doing anything by default
class BernoulliAOperator(AOperator):
def __init__(self, probability):
self._probability = probability
def _define(self): # this was previously in the build method
circuit = QuantumCircuit(1)
circuit.ry(probability, 0)
self._definition = circuit.data
class LinearYRotation(Gate):
def __init__(self, slope, offset, num_state_qubits, i_state=None, i_target=None):
pass # define self.i_state, self.i_target
def _define(self):
circuit = QuantumCircuit(max([self.i_target] + self.i_state[:]))
circuit.ry(self.offset, i_target)
for power, i in enumerate(i_state):
circuit.cry(self.slope * 2**power, i, i_target)
self._definition = circuit.data
class MyFunction(AOperator):
def __init__(self, c_approx, num_state_qubits):
pass # define self.slope, self.offset, self.num_state_qubits
def _define(self):
circuit = QuantumCircuit(self.num_state_qubits + 1)
linear_y = LinearYRotation(self.slope, self.offset, self.num_state_qubits)
circuit.append(linear_y, list(range(self.num_state_qubits + 1)), [])
circuit.z(num_state_qubits)
circuit.append(linear_y.inverse(), list(range(self.num_state_qubits + 1)), [])
self._definition = circuit.data
Uncertainty models in Aqua allow us to load a discretized probability distribution in the amplitudes of qubits. Currently, they are implemented as CircuitFactory
, but should follow the same flow as the Ansatz and amplitude estimation's
class UnivariateDistribution(Gate):
def __init__(self, num_uncertainty_qubits):
self._definition = Custom(...).definition
@property
def low(self): # also implement: high, num_values, values, probabilities
pass
@staticmethod
def pdf_to_probabilities(self):
pass
class UnivariateVariationalDistribution(UnivariateDistribution, Ansatz): # use case? UVD as Ansatz
def __init__(self, ansatz):
self._definition = ansatz.definition # or ansatz.to_instruction().definition or such
def set_probabilities(self):
pass
Application of uncertainty models on QGANs:
qgan = QGAN(real_data, bounds, num_qubits)
distribution = UnivariateVariationalDistribution(some_ansatz)
qgan.set_generator(distribution)
Application to a circuit:
distribution = NormalDistribution(num_uncertainty_qubits=4, mu=0, sigma=1)
circuit.append(distribution, list(range(num_uncertainty_qubits)))
This is an example combining an ansatz, an uncertainty model plus the mapping of a linear function in an amplitude estimation experiment. Here, the ansatz can encode optimization variable and the uncertainty model could represent a probability distribution.
n = 3 # qubits for the uncertainty model
k = 3 # qubits for the Ansatz
prob = NormalDistribution(n, mu, sigma)
varform = RY(k, entanglement='linear', reps=2)
# either encapsulate the logic in a fixed class derived from AOperator
a_operator = MyAOperator(prob, varform)
# or do directly what would be done in MyAOperator
f_mapping = MyFunction(n, k)
a_circuit = QuantumCircuit(n + k + 1)
a_circuit.append(prob, prob_indices)
a_circuit.append(varform, varform_indices)
a_circuit.append(f_mapping, f_indices)
a_operator = AOperator(a_circuit)
# or do the application of the cost function more explicitely
a_circuit = QuantumCircuit(n + k + 1)
a_circuit.append(prob, prob_indices)
a_circuit.append(varform, varform_indices)
# apply the mapping F directly
for i in range(n):
a_circuit.ry(angle, i)
# etc.
a_operator = AOperator(a_circuit)
# or maybe even use the Ansatz constructor facilities?
f_mapping = MyFunction(n, k)
prob_indices, varform_indices, f_indices = list(range(n)), list(range(n, n + k)), list(range(n + k + 1))
a_as_ansatz = Ansatz([prob, varform, a_operator], [prob_indices, varform_indices, f_indices])
a_operator = AOperator(a_as_ansatz.to_circuit())
# then go about calling AE
ae = AmplitudeEstimation(m, a_operator)
Under the hood, not much would change in the VQE algorithm. We are more flexible in how to use the Ansatz though. Depending on if we implement a to_circuit
method (which seems very useful at the moment) the changes boil down to using ansatz.to_circuit(params)
instead of var_form.construct_circuit(params)
.
def VQAlgorithm(QuantumAlgorithm):
# no changes at all
pass
def VQE(VQAlgorithm):
def __init__(self, var_form: Ansatz):
# only change: either add a preferred_init_points method to Ansatz, or accept such a kwarg in VQE
def construct_circuit(self):
# we could do
wave_function = self._var_form.to_circuit(parameter)
# or
self._var_form.params = parameter
wave_function = self._var_form.to_circuit()
# or
self._var_form.params = parameter
wave_function = QuantumCircuit(self._var_form.num_qubits)
wave_function.append(self._varform, list(range(self._var_form.num_qubits)))
# rest of the implementation...
pass
def _eval_aux_ops(self):
if params is None:
params = self.optimal_params
self._var_form.params = parameter
wavefn_circuit = QuantumCircuit(self._var_form.num_qubits)
wavefn_circuit.append(self._varform, list(range(self._var_form.num_qubits)))
def _energy_evaluation(self, parameters):
# parameterized circuits should be supported completely (Terra will implement support for complex values)
# otherwise this is supported exactly the same as before
def get_optimal_circuit(self):
self._var_form.params = self._ret['opt_params']
circuit = QuantumCircuit(self._var_form.num_qubits)
circuit.append(self._varform, list(range(self._var_form.num_qubits)))
return circuit