<jupyter_start><jupyter_text>Circuit Parameter Manipulation This tutorial covers the common use case of constructing a circuit with continuously variable parameters (today only in `Rx`, `Ry`, or `Rz` gates), evaluating the expectation value of a Hermitian operator supplied in sparse Pauli form using ideal statevector simulation, and then taking the gradient of the expectation value with respect to the values of the circuit rotation angle parameters. Such exercises are ubiquitous in variational quantum algorithms such as QAOA and VQE, e.g., in gradient-based algorithms to optimize the variational quantum circuit parameters. We have provided some utility functions to make these exercises easy to implement and rather fast to run (particularly when ideal statevector simulation in the infinite statistical sampling limit is invoked).<jupyter_code>import quasar
import time # We'll roughly time some quasar operations below<jupyter_output><empty_output><jupyter_text>Case-Study Circuit First, let's build a CIS state preparation circuit. We start by building a gadget to move amplitude from one singly-excited ket to another:<jupyter_code>gadget = quasar.Circuit().Ry(1).CZ(0,1).Ry(1).CX(1,0)
print(gadget)<jupyter_output><empty_output><jupyter_text>Then use the ``add_gates`` utility function to quickly build up the full CIS state preparation circuit:<jupyter_code>N = 10 # Increase this to get some more dramatic timing examples below
circuit = quasar.Circuit().X(0)
for I in range(N):
    circuit.add_gates(circuit=gadget, qubits=(I, I+1))
print(circuit)<jupyter_output><empty_output><jupyter_text>Parameter Manipulation Cool circuit, bro. So where are all the parameters, how do we figure out what they are, and how do we set their values? To get started with this, let's invoke the `parameter_str` utility property of `Circuit`:<jupyter_code>print(circuit.parameter_str)<jupyter_output><empty_output><jupyter_text>This provides a textual summary of all mutable parameters in `Circuit`, including a breakdown of the `times` and `qubits` key of the involved `Gate`, the name of that `Gate`, and the parameter name within that `Gate`, as well as the current value of the parameter. The parameters of a `Circuit` object have a strong lexical indexing order (referred to as the `parameter_index` order) determined first by the `times` and `qubits` key of the involved `Gate` (i.e., the underlying lexical `Gate` index order), followed by the definitional ordering of each parameter within the `Gate` (for multi-parameter `Gate` objects). For instance, the `10`-th `parameter_index` in our `Circuit` corresponds to the `theta` parameter of the `Ry` gates at `time` index `15` and `qubit` index `6` - its value is currently `0.0`. Before we start setting parameters, let's look at some more attributes that help us to locate and characterize these parameters. The `nparameter` property tells how many total parameters are in the `Circuit` object:<jupyter_code>print(circuit.nparameter)<jupyter_output><empty_output><jupyter_text>The `parameters` property is an `OrderedDict` of parameter (`times`, `qubits`, `parameter_key`) keys to parameter values. We'll print this out somewhat reasonably here:<jupyter_code># print(circuit.parameters)
for key, value in circuit.parameters.items():
    times, qubits, parameter_key = key
    print('%10r %10r %10r: %8.6f' % (times, qubits, parameter_key, value))<jupyter_output><empty_output><jupyter_text>If you don't want the whole `parameters` dictionary, you can also have either the `parameter_keys` or `parameter_values` in isolation:<jupyter_code>print(circuit.parameter_keys)
print(circuit.parameter_values)<jupyter_output><empty_output><jupyter_text>In another common use case, you have the `times` and `qubits` key of a `Gate` object, and want to know what parameter indices it covers in the `Circuit` object. For this, the `parameter_indices` property is provided:<jupyter_code># print(circuit.parameter_indices)
for key, parameter_indices in circuit.parameter_indices.items():
    times, qubits = key
    print('%10r %10r: %r' % (times, qubits, parameter_indices))<jupyter_output><empty_output><jupyter_text>Note how the `H`, `CX`, and `CZ` gates do not have any parameters, while the `Ry` gates have one each. The `parameter_indices` method can often be used in concert with automatic time placement in the `add_gate` method and the `return_key` optional argument to `add_gate` to determine where each `Gate` object is automatically added in time and from thence which lexical parameter indices correspond to that `Gate`. `return_key` falls through any gate addition sugar methods. For instance:<jupyter_code>circuit2 = quasar.Circuit().Rx(0).Ry(1)
# Where is the following Rz gate added? 
# (Note that time placement is determined automatically)
Rz_times, Rz_qubits = circuit2.Rz(0, return_key=True)
print(circuit2)
print(Rz_times, Rz_qubits)
# Maybe add more gates here
# ...
# At this point, what parameter indices are represented by the Rz gate?
print(circuit2.parameter_indices[(Rz_times, Rz_qubits)])<jupyter_output><empty_output><jupyter_text>Now to set the values of the parameters. You can set these one at a time with full knowledge of the parameter key via the `set_parameter` method:<jupyter_code>circuit.set_parameter(((23,), (8,), 'theta'), 1.0)
print(circuit.parameter_str)<jupyter_output><empty_output><jupyter_text>Or several at a time with the `set_parameters` method:<jupyter_code>circuit.set_parameters({
    ((20,), (7,), 'theta') : 0.2,
    ((21,), (8,), 'theta') : 0.3,
    })
print(circuit.parameter_str)<jupyter_output><empty_output><jupyter_text>However, full knowledge of the parameter key is somewhat verbose, and is entirely equivalent to knowledge of the integer parameter index. To set parameters instead in terms of parameter index, we provide the `set_parameter_values` method, which takes a `parameter_values` list and an optional `parameter_indices` list (needed if only a subset and/or a number of non-sequential parameters are set):<jupyter_code>circuit.set_parameter_values([0.4, 0.5], parameter_indices=[4,2])
print(circuit.parameter_str)<jupyter_output><empty_output><jupyter_text>Here's a quick example of how things look a little different if `Gate` objects have multiple parameters:<jupyter_code>circuit3 = quasar.Circuit().Ry(0).SO4(0,1)
print(circuit3)
print(circuit3.parameter_str)
for key, parameter_indices in circuit3.parameter_indices.items():
    times, qubits = key
    print('%10r %10r: %r' % (times, qubits, parameter_indices))<jupyter_output><empty_output><jupyter_text>Now back to our case-study CIS circuit. We'll set the parameters to something deterministic but sensible before proceeding to the rest of the exercise:<jupyter_code>parameter_values = []
for I in range(N):
    value = (1.0 - I / 17.0)
    parameter_values.append(+value)
    parameter_values.append(-value)
circuit.set_parameter_values(parameter_values)
print(circuit.parameter_str)<jupyter_output><empty_output><jupyter_text>Pauli Expectation Values and Gradients Let us define $|\Psi (\{ \theta_g \}) \rangle$ as the statevector generated by `circuit` at the current parameter values:<jupyter_code>print(circuit)
print(circuit.parameter_str)<jupyter_output><empty_output><jupyter_text>We can also define a Pauli-sparse Hermitian operator $\hat O$:<jupyter_code>I, X, Y, Z = quasar.Pauli.IXYZ()
pauli = quasar.Pauli.zero()
for k in range(N+1):
    pauli += (k + 1) / 10.0 * Z[k]
print(pauli)<jupyter_output><empty_output><jupyter_text>A common quantum primitive is to compute the total observable expectation value $O (\{ \theta_g \}) \equiv \langle \Psi (\{ \theta_g \} ) | \hat O | \Psi (\{ \theta_g \})\rangle$ at the current parameter set $\{ \theta_g \}$:<jupyter_code>backend1 = quasar.QuasarSimulatorBackend()
start = time.time()
print(backend1.run_pauli_expectation_value(
    circuit=circuit,
    pauli=pauli,
    ))
print('%11.3E s' % (time.time() - start))<jupyter_output><empty_output><jupyter_text>Another useful quantum primitive is to compute the gradient of the total observable expectation value with respect to the circuit parameters $\frac{\partial O}{\partial \theta_g}$. This can be done in either the ideal infinite sampling limit or with finite sampling by using the parameter shift method [e.g., $\frac{\partial O}{\partial \theta_g} = O (\theta_g + \pi/4) - O(\theta_g - \pi / 4)$ for $\hat R$ gates].<jupyter_code>backend1 = quasar.QuasarSimulatorBackend()
start = time.time()
print(backend1.run_pauli_expectation_value_gradient(
    circuit=circuit,
    pauli=pauli,
    ))
print('%11.3E s' % (time.time() - start))<jupyter_output><empty_output><jupyter_text>This can be restricted to user-desired parameter indices to lower the cost, using the `parameter_indices` optional argument:<jupyter_code>backend1 = quasar.QuasarSimulatorBackend()
start = time.time()
print(backend1.run_pauli_expectation_value_gradient(
    circuit=circuit,
    pauli=pauli,
    parameter_indices=[0,1,2,3]
    ))
print('%11.3E s' % (time.time() - start))<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.10025366714459027, 'fraction_numerical': 0.011029006286533583, 'mean_word_length': 6.051321928460342, 'pattern_counts': {'":': 0, '<': 77, '<?xml version=': 0, '>': 77, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '19', 'dir': 'jupyter-structured-clean-dedup', 'id': '192484', 'license': 'Apache-2.0', 'n_tokens_mistral': 2846, 'n_tokens_neox': 2690, 'n_words': 967, 'path': 'notebooks/quasar-parameters.ipynb', 'repo_name': 'qcware/quasar', 'size': '9067'}