Quick Start =========== In this chapter, the basic behaviour of QuBricks is demonstrated in the context of some simple problems. For specific documentation on methods and their usage, please refer to the API documentation is subsequent chapters. In the following, we assume that the following has been run in your Python environment. >>> from qubricks import * >>> from qubricks.wall import * >>> import numpy as np .. note:: QuBricks current only works in Python 2, since it depends on python-parameters which in turn is compatible only with Python 2. Getting Started --------------- In this section, we motivate the use of QuBricks in simple quantum systems. We will make use of the submodule qubricks.wall, which brings together many of the “bricks†that make up QuBricks into usable standalone tools. Consider a single isolated electron in a magnetic field. Suppose that this magnetic field was composed of stable magnetic field along the Z axis :math:`B_z`, and a noisy magnetic field along the X axis :math:`B_x(t)`. The Hamiltonian describing the mechanics is then: .. math:: \begin{aligned} H & = & \mu_B \left(\begin{array}{cc} B_z & B_x(t)\\ B_x(t) & -B_z \end{array}\right),\end{aligned} where :math:`B_z` is the magnetic field along :math:`z` and :math:`B_x(t) = \bar{B}_{x} + \tilde{B}_x(t)` is a noise field along :math:`x` centred on some nominal value :math:`\bar{B}_{x}` with a time-dependent noisy component :math:`\tilde{B_x}(t)`. Let us assume that the noise in :math:`B_x(t)` is white, so that: .. math:: \left< \tilde{B}_x(t) \tilde{B}_x(t^\prime) \right> = D\delta(t-t^\prime), We can model such white noise using a Lindblad superoperator. This is a simple system which can be analytically solved. Evolution under this Hamiltonian will lead to the electron's spin gyrating around an axis between Z and X (i.e. at an angle :math:`\theta = \tan^{-1}(B_z/J_x)` from the x-axis) at a frequency of :math:`2\sqrt{B_z^2 + B_x^2}`. The effect of high frequency noise in :math:`B_x` is to progressively increase the mixedness in the Z quadrature until such time as measurements of Z are unbiased. For example, when :math:`B_z=0`, the return probability for an initially up state is given by: :math:`p = \frac{1}{2}(1+\cos{2B_{x}t})`. Since :math:`\left< \tilde{B_{x}}^2 \right>_{\textrm{avg}} = D/t`, we find by taylor expanding that: :math:`\left<p\right> = 1 - Dt`. A more careful analysis would have found that: .. math:: \left<p\right> = \frac{1}{2}(1+exp(-2Dt)) . It is possible to find approximations for a general :math:`\theta`, but we leave that as an exercise. Alternatively, you can take advantage of QuBricks to simulate these results for us. .. _two-level: .. figure:: _static/twolevel.pdf Dynamics of a single electron spin under a magnetic field aligned 45 degrees in the XZ plane. For example, suppose we wanted to examine the case where :math:`B_z = B_x`, as shown in figure :num:`two-level`. This can be simulated using the following QuBricks code: .. literalinclude:: _static/twolevel.py :linenos: :lines: 4-24 We could then plot the results using `matplotlib`: .. literalinclude:: _static/twolevel.py :linenos: :lines: 26-48 This would result in the following plot: .. image:: _static/results.pdf The above code takes advantage of several attributes and methods of `QuantumSystem` instances which may not be entirely clear. At this point, you can look them up in the API reference in subsequent chapters. Advanced Usage -------------- For more fine-grained control, one can subclass `QuantumSystem`, `Measurement`, `StateOperator` and `Basis` as necessary. For more information about which methods are available on these objects, refer to the API documentation below. The templates for subclassing are shown below. QuantumSystem ~~~~~~~~~~~~~ .. literalinclude:: ../templates/quantum_system.py :linenos: Measurement ~~~~~~~~~~~ .. literalinclude:: ../templates/measurement.py :linenos: StateOperator ~~~~~~~~~~~~~ .. literalinclude:: ../templates/state_operator.py :linenos: Basis ~~~~~ .. literalinclude:: ../templates/basis.py :linenos: Integrator ~~~~~~~~~~ In rare circumstances, you may find it necessary to subclass the `Integrator` class. Refer to the API documentation for more details on how to do this. .. literalinclude:: ../templates/integrator.py :linenos: Operator Basics --------------- One class that is worth discussing in more detail is `Operator`, which is among the most important "bricks" in the QuBricks library. It represents all of the two-dimensional linear operators used in QuBricks. The Operator object is neither directly a symbolic or numeric representation of an operator; but can be used to generate both. Consider a simple example: >>> op = Operator([[1,2],[3,4]]) >>> op <Operator with shape (2,2)> To generate a matrix representation of this object for inspection, we have two options depending upon whether we want a symbolic or numeric representation. >>> op() # Numeric representation as a NumPy array array([[ 1.+0.j, 2.+0.j], [ 3.+0.j, 4.+0.j]]) >>> op.symbolic() # Symbolic representation as a SymPy matrix Matrix([ [1, 2], [3, 4]]) In this case, there is not much difference, of course, since there are no symbolic parameters used. Creating an Operator object with named parameters can be done in two ways. Either you must create a dictionary relating parameter names to matrix forms, or you can create a SymPy symbolic matrix. In both cases, one then passes this to the Operator constructor. For example: >>> op = Operator('B':[[1,0],[0,-1]], 'J':[[0,1],[1,0]]) >>> op.symbolic() Matrix([ [B, J], [J, -B]]) >>> op.symbolic(J=10) Matrix([ [ B, 10], [10, -B]]) >>> op() ValueError: Operator requires use of Parameters object; but none specified. When representing Operator objects symbolically, we can override some parameters and perform parameter substitution. We see that attempting to generate a numeric representation of the Operator object failed, because it did not know how to assign a value to :math:`B` and :math:`J`. Normally, Operator objects will have a reference to a `Parameters` instance (from `python-parameters`) passed to it in the constructor phase, for which these parameters can be extracted. This will in most cases be handled for you by QuantumSystem (see `QuantumSystem` in the API chapters), but for completeness there are two keyword arguments you can pass to Operator instances: `parameters`, which shold be a reference to an existing Parameters instance, and `basis`, which should be a reference to an existing Basis object or None (see `Basis` in the API chapters). For now, let us manually add it for demonstration purposes. >>> from parameters import Parameters >>> p = Parameters() >>> p(B=2,J=1) < Parameters with 2 definitions > >>> op = Operator('B':[[1,0],[0,-1]], 'J':[[0,1],[1,0]],parameters=p) >>> op() array([[ 2.+0.j, 1.+0.j], [ 1.+0.j, -2.+0.j]]) >>> op(J=10,B=1) array([[ 1.+0.j, 10.+0.j], [ 10.+0.j, -1.+0.j]]) We see in the above that we can take advantage of temporary parameter overrides for numeric representations too [note that a parameters instance is still necessary for this]. The `Parameters` instance allows one to have parameters which are functions of one another, which allows for time and/or context dependent operators. Operator objects support basic arithmetic: addition, subtraction, and multiplication using the standard python syntax. The inverse operation can be performed using the inverse method: >>> op.inverse() The Kronecker tensor product can be applied using the tensor method: >>> op.tensor(other_op) To apply an Operator object to a vector, you can either use the standard inbuilt multiplication operations, or use the slightly more optimised apply method. If you are only interested in how certain parameters affect the operator, then to improve performance you can "collapse" the Operator down to only include variables which depend upon those variables. >>> op.collapse('t',J=1) The result of the above command would substitute all variables (with a parameter override of :math:`J=1`) that do not depend upon :math:`t` with their numerical value, and then perform various optimisations to make further substitutions more efficient. This is used, for example, by the integrator. The last set of key methods of the Operator object are the connected and restrict methods. Operator.connected will return the set of all indicies (of the basis vectors in which the Operator is represented) that are connected by non-zero matrix elements, subject to the provided parameter substitution. Note that this comparison is done with the numerical values of the parameters. >>> op = Operator('B':[[1,0],[0,-1]], 'J':[[0,1],[1,0]],parameters=p) >>> op.connected(0) 0,1 >>> op.connected(0, J=0) 0 The restrict method returns a new Operator object which keeps only the entries in the old Operator object which correspond to the basis elements indicated by the indicies. >>> op = Operator('B':[[1,0],[0,-1]], 'J':[[0,1],[1,0]],parameters=p) >>> op.restrict(0) <Operator with shape (1, 1)> >>> op.symbolic() Matrix([[B]]) For more detail, please refer to the API documentation for `Operator`.