#
# Copyright 2022 Keysight Technologies Inc.
#
"""
Simulator: Introduction
=======================
"""
#%%
# |True-Q| offers a highly versatile built-in simulator that allows
# users to simulate arbitrary circuits with a wide variety of noise models and options
# for noise model customization.
#
# In this example, we go through the basic features of the |True-Q| simulator.
# We begin by instantiating a circuit to play with:
import matplotlib.pyplot as plt
import numpy as np
import trueq as tq
import trueq.simulation as tqs
circuit = tq.Circuit([{0: tq.Gate.x, 1: tq.Gate.y}, {(0, 1): tq.Gate.cz}])
circuit.measure_all()
circuit.draw()
#%%
# Simulator Basics
# ----------------
#%%
# In its simplest configuration, a (noiseless) simulator can be instantiated from
# |True-Q|'s :py:class:`~trueq.Simulator` class as follows:
sim = tq.Simulator()
#%%
# We can use it to simulate the final state of the circuit, with all initial states
# prepared as :math:`|0\rangle` by default. Since the simulator is noiseless, the
# output state is a pure state; a noisy simulator will generally return a density matrix
# instead.
sim.state(circuit).mat()
#%%
# If we want to force the output to be a density matrix, we can use:
sim.state(circuit).upgrade().mat()
#%%
# We can also use it to compute the overall action of the circuit. Since the simulator
# is currently noiseless, the output is a unitary matrix; a noisy simulator will return
# a superoperator (in the rowstacked basis).
tq.plot_mat(sim.operator(circuit).mat())
#%%
# Finally, we can use a simulator to populate the :py:attr:`~trueq.Circuit.results`
# attribute of the circuit. Currently, this attribute is just an empty dictionary:
circuit.results
#%%
# But after calling the :py:meth:`~trueq.Simulator.run` method, it has
# :py:attr:`~trueq.Circuit.results` that are randomly sampled bitstrings from the final
# state of a state simulation:
sim.run(circuit, n_shots=100)
circuit.results
#%%
# In this case, all 100 shots end in the ``11`` state because the final state is a
# computational eigenstate.
#
# When re-running the simulation, it will overwrite the existing
# :py:attr:`~trueq.Circuit.results` unless
# otherwise specified. Another option is to call the simulator's
# :py:meth:`~trueq.Simulator.sample` method which returns a :py:class:`~trueq.Results`
# object directly without affecting the circuit's :py:attr:`~trueq.Circuit.results`
# attribute:
results = sim.sample(circuit, n_shots=100)
results
#%%
# Adding Noise Sources
# --------------------
#%%
# We construct a noisy simulator by appending noise sources to a noiseless simulator.
# The example below demonstrates this using two of |True-Q|\'s built-in noise sources:
# Add an overrotation noise, which causes single qubit gates to be simulated as U^1.02
sim.add_overrotation(single_sys=0.02)
# Add a depolarizing noise source at a rate of 0.8% per acted-on qubit per cycle
sim.add_depolarizing(p=0.008)
# Note that noisy simulators can be constructed as one-liners
other_sim = tq.Simulator().add_overrotation(single_sys=0.02).add_depolarizing(p=0.008)
#%%
# .. note::
#
# Note that simulation is cycle-based.
# Each noise source is called to add noise to the quantum state (or to the
# superoperator if :py:meth:`~trueq.Simulator.operator` is called) for each cycle
# in a circuit. The order in which noise sources are applied is dictated by the
# order in which they were added to the simulator.
#%%
# Now, since the simulator applies a depolarizing noise source, we get a density
# operator rather than a pure state:
tq.plot_mat(sim.state(circuit).mat())
#%%
# After calling the :py:meth:`~trueq.Simulator.run` method (overwriting the
# :py:attr:`~trueq.Circuit.results` from above), we end up with noisy outcomes:
sim.run(circuit, n_shots=100, overwrite=True)
circuit.results
#%%
# We can also specify infinite shots to get the expectation values of each bitstring:
sim.run(circuit, n_shots=np.inf, overwrite=True)
circuit.results
#%%
# Built-in Noise Sources
# ----------------------
#
# The simulator supports a series of common noise models, such as:
#
# #. :py:meth:`~trueq.Simulator.add_depolarizing`\: This method adds depolarizing noise
# to every location where a gate acts. It takes the depolarizing parameter ``p`` as
# an argument.
# #. :py:meth:`~trueq.Simulator.add_stochastic_pauli`\: This method adds stochastic
# Pauli noise to every location where a gate acts. It takes as parameters the
# probabilities of each Pauli error for the noise channel, ``px``, ``py`` and
# ``pz``.
# #. :py:meth:`~trueq.Simulator.add_overrotation`\: This method adds an overrotation to
# every single- and/or two-qubit gate. It takes as parameters the ``single_sys``
# angle which specifies how much the single-qudit gates are under/overrotated, and
# the ``multi_sys`` parameter, which specifies how much the two-qudit gates are
# under/overrotated.
# #. :py:meth:`~trueq.Simulator.add_relaxation`\: This method adds amplitude damping
# (:math:`T1`\) and/or dephasing (:math:`T2`\) to every location where a gate acts.
# This method takes as arguments the noise parameters ``t1`` and ``t2`` and the
# amount of time ``t_single`` (``t_multi``) a single- (multi-)qubit gate takes.
# #. :py:meth:`~trueq.Simulator.add_kraus`\: This method allows for the creation of a
# custom noise source specified through Kraus operators and takes as input a list
# of operators specifed in matrix-form.
#
# For a full list of built-in noise sources, check out the :py:class:`~trueq.Simulator`
# API reference.
#%%
# Restricting Noise via Conditional Noise Sources
# -----------------------------------------------
#%%
# Every noise source has a property called match, which can be used to create a
# conditional noise source. A match can specify specific qubits, operations,
# cycles, or even more specific properties. Refer to the children of
# :py:class:`~trueq.simulation.match.Match` for more details.
# initialize a simulator with different dephasing rates on the qubits
bitflip = lambda p: [np.sqrt(1 - p) * np.eye(2), np.sqrt(p) * np.fliplr(np.eye(2))]
sim0 = tq.Simulator()
sim0.add_kraus(bitflip(0.05), match=tqs.LabelMatch(0))
sim0.add_kraus(bitflip(0.09), match=tqs.LabelMatch(1))
# initialize a simulator that targets only a specific gate
xmatch = tqs.GateMatch(tq.Gate.x)
sim1 = tq.Simulator().add_kraus(bitflip(0.15), match=xmatch)
# initialize a simulator that targets only specific gates on specific labels
sim2 = tq.Simulator()
gate_label_match = tqs.LabelMatch((1, 2)) & tqs.GateMatch([tq.Gate.y, tq.Gate.s])
sim2.add_kraus(bitflip(0.1), match=gate_label_match)
# plot the final states
plt.figure(figsize=(10, 3))
tq.plot_mat(sim0.state(circuit).mat(), ax=plt.subplot(131))
tq.plot_mat(sim1.state(circuit).mat(), ax=plt.subplot(132))
tq.plot_mat(sim2.state(circuit).mat(), ax=plt.subplot(133))