Defining Operators as Functions


Mathematically, one can represent an operator Ô with the following definitions:

Action of Ô on Ket:
Ô | i ⟩ = ∑ⱼ cᵢⱼ | j ⟩ 

Action of Ô on Bra:
⟨ u | Ô  = ∑ᵥ cᵤᵥ ⟨ v |

QuDirac allows us to define normal Julia functions that act like Ô using the @def_op macro:

# define "a" on Kets
julia> @def_op " a | n > = √n * | n-1 > "
a (generic function with 1 methods)

julia> d" a * | 42 > "
Ket{KroneckerDelta,1,Float64} with 1 state(s):
  6.48074069840786 | 41 ⟩

julia> d" a * (| 1 > + | 2 > - | 3 >) "
Ket{KroneckerDelta,1,Float64} with 3 state(s):
  -1.7320508075688772 | 2 ⟩
  1.0 | 0 ⟩
  1.4142135623730951 | 1 ⟩

Action of the operator's dual on Bras is well-defined by the definition of the operator on Kets, since < 1 | * Ô' == ( Ô * | 1 > )':

julia> d" < 2 | * a' "
Bra{KroneckerDelta,1,Float64} with 1 state(s):
  1.4142135623730951 ⟨ 1 |

To act the operator on a Bra (or its dual on a Ket), we need to define its action on the Bra:

# define "a" on Bras
julia> @def_op " < n | a = √(n+1) * < n+1 | "
a (generic function with 2 methods)

julia> d" < 5 | * a "
Bra{KroneckerDelta,1,Float64} with 1 state(s):
  2.449489742783178 ⟨ 6 |

julia> d" a' * (| 5 > + | 2 >)"
Ket{KroneckerDelta,1,Float64} with 2 state(s):
  1.7320508075688772 | 3 ⟩
  2.449489742783178 | 6 ⟩

julia> d" < 5 | * a * | 6 > "
2.449489742783178

The @def_op macro works for product bases as well:

julia> @def_op " a₂ | x,y,z >  = √y * | x,y-1,z > "
a₂ (generic function with 2 methods)

julia> d" a₂ * (| 0,10,8 > - | 12,31,838 >) "
Ket{KroneckerDelta,3,Float64} with 2 state(s):
  3.1622776601683795 | 0,9,8 ⟩
  -5.5677643628300215 | 12,30,838 ⟩

For an example of an operator that throws its basis Kets into superpositions, here's a function emulating a Hadamard operator:

julia> @def_op " h | n > = 1/√2 * ( | 0 > + (-1)^n *| 1 > )"
h (generic function with 1 methods)

julia> d" h * | 0 > "
Ket{KroneckerDelta,1,Float64} with 2 state(s):
  0.7071067811865475 | 0 ⟩
  0.7071067811865475 | 1 ⟩

julia> d" h * | 1 > "
Ket{KroneckerDelta,1,Float64} with 2 state(s):
  0.7071067811865475 | 0 ⟩
  -0.7071067811865475 | 1 ⟩

Grammar for the Definition String


The grammar of the string passed to @def_op is:

  1. Defining action on Kets:

    @def_op " $op_name | $label_args > = f($label_args...) "
    

    where f is an arbitrary expanded function that takes in the $label_args and returns a Ket.

  2. Defining action on Bras:

    @def_op " < $label_args | $op_name  = f($label_args...) "
    

    where f is an arbitrary expanded function that takes in the $label_args and returns a Bra.

Allowable syntax for the right-hand side of the equation is exactly the same syntax allowed by d"...".


Generating Operator Representations


The operator-functions described in the previous example are no doubt useful, but they are just normal Julia functions, and so are quite limited when it comes to mimicking the behavior of actual quantum operators. For example, they can't be added or be factors of a tensor product (this functionality may indeed be implemented in the future, however).

For those capabilities, we'll need to generate an OpSum representation in a basis. To do so, we can use the @rep_op macro. Here's a familiar example:

julia> @rep_op " a | n > = √n * | n-1 > " 1:10;

julia> a
OpSum{KroneckerDelta,1,Float64} with 10 operator(s):
  2.449489742783178 | 5 ⟩⟨ 6 |
  3.1622776601683795 | 9 ⟩⟨ 10 |
  2.23606797749979 | 4 ⟩⟨ 5 |
  2.8284271247461903 | 7 ⟩⟨ 8 |
  2.6457513110645907 | 6 ⟩⟨ 7 |
  2.0 | 3 ⟩⟨ 4 |
  1.0 | 0 ⟩⟨ 1 |
  3.0 | 8 ⟩⟨ 9 |
  1.4142135623730951 | 1 ⟩⟨ 2 |
  1.7320508075688772 | 2 ⟩⟨ 3 |

The @rep_op macro takes in a definition string, and an iterable of items to be used as basis labels. The grammar and allowable syntax of the definition string is exactly that of the definition string passed to @def_op. The only difference between the two is that the @rep_op macro feeds in the given basis labels to produce an OpSum.

To generate a representation on a product basis, one can provide multiple iterables to @rep_op. Their cartesian product will then be used as the basis for the representation:

# define P₁₃₂ by it's action on a Bra
julia> @rep_op " < i,j,k | P₁₃₂  = < i,k,j | "  1:10  'a':'f'  -(1:10)
OpSum{KroneckerDelta,3,Int64} with 600 operator(s):
  1 | 10,'e',-2 ⟩⟨ 10,-2,'e' |
  1 | 4,'c',-5 ⟩⟨ 4,-5,'c' |
  1 | 3,'c',-10 ⟩⟨ 3,-10,'c' |
  1 | 2,'e',-3 ⟩⟨ 2,-3,'e' |
  1 | 7,'c',-9 ⟩⟨ 7,-9,'c' |
  1 | 1,'f',-9 ⟩⟨ 1,-9,'f' |
  1 | 3,'f',-6 ⟩⟨ 3,-6,'f' |
  1 | 9,'b',-2 ⟩⟨ 9,-2,'b' |
  1 | 1,'c',-3 ⟩⟨ 1,-3,'c' |
  1 | 4,'d',-8 ⟩⟨ 4,-8,'d' |

julia> d" P₁₃₂ * | 10,-2,'e' > "
Ket{KroneckerDelta,3,Int64} with 1 state(s):
  1 | 10,'e',-2 ⟩

Let's say I already have an operator-function defined, and want to represent it in a basis. For example, take the Hadamard operator-function h, constructed in the previous section as:

julia> @def_op " h | n > = 1/√2 * ( | 0 > + (-1)^n *| 1 > )"
h (generic function with 1 methods)

I can easily generate a representation for this function by using @rep_op and calling h on the right-hand side:

julia> @rep_op " H | n > = h * | n > " 0:1
OpSum{KroneckerDelta,1,Float64} with 4 operator(s):
  0.7071067811865475 | 1 ⟩⟨ 0 |
  0.7071067811865475 | 0 ⟩⟨ 0 |
  0.7071067811865475 | 0 ⟩⟨ 1 |
  -0.7071067811865475 | 1 ⟩⟨ 1 |

The above strategy works to represent any operator-function. Just be aware that the function and the actual representation need to have unique names:

julia> @rep_op " h | n > = h * | n > " 0:1
ERROR: invalid redefinition of constant h
 in anonymous at no file:70