Muqcs (pronounced mucks) is McGuffin's Useless Quantum Circuit Simulator. It is written in JavaScript, and allows one to simulate circuits programmatically or from a command line. It has no graphical front end, does not leverage the GPU for computations, and makes almost no use of external libraries (mathjs is used in only one subroutine), making it easier for others to understand the core algorithms. On many personal computers, it can simulate circuits of 20+ qubits, and can compute several statistics.
The code is contained entirely in a single file, and defines a small class for complex numbers, a class for complex matrices (i.e., matrices storing complex numbers), and a few utility classes like Sim (for Simulator). These classes take up about 2000 lines of code. The rest of the code consists of a regression test (in the function performRegressionTest()). Having a relatively small amount of source code means that the code is easier to study and understand by others.
Unlike other javascript quantum circuit simulators, Muqcs implements partial trace and can compute arbitrary reduced density matrices, from which several statistics can be computed: the phase, probability, purity, linear entropy, and von Neumann entropy (to quantify mixedness), and Bloch sphere coordinates of individual qubits; the concurrence (to quantify entanglement), correlation, purity, and von Neumann entropy of pairs of qubits; and the stabilizer Rényi entropy (to quantify magic) of a set of qubits.
On my 2022 laptop, running inside Chrome, Muqcs can simulate circuits on N=20 qubits at a speed of less than 100ms per gate, and can also compute all N(N-1)/2 4×4 2-qubit partial traces and all N 2×2 1-qubit partial traces in under 22 seconds, all without ever computing any explicit
To run the code, load the html file into a browser like Chrome, and then open a console (in Chrome, this is done by selecting 'Developer Tools'). From the console prompt, you can call functions in the code and see output printed to the console.
Muqcs is named in allusion to mush, Moser's Useless SHell, written by Derrick Moser (dtmoser).
Creating and Manipulating Matrices
To create some matrices and print out their contents, we can do
let m0 = new CMatrix(2,3); // CMatrix means 'complex matrix', a matrix with complex entries
console.log("A 2x3 matrix filled with zeros:\n" + m0.toString());
let m1 = CMatrix.create([[10,20],[30,40]]);
console.log("A 2x2 matrix:\n" + m1.toString());
let c0 = CMatrix.createColVector([5,10,15]);
console.log("The transpose of a column vector is a row vector:\n" + c0.transpose().toString());
which produces this output:
A 2x3 matrix filled with zeros:
[_,_,_]
[_,_,_]
A 2x2 matrix:
[10,20]
[30,40]
The transpose of a column vector is a row vector:
[5,10,15]
Notice that the toString() method returns a string containing newline characters. In the source code, this is referred to as a 'multiline string'. Next, we create a matrix containing complex numbers, add two matrices together, and print out the matrices and their sum:
let m2 = CMatrix.create([[new Complex(0,1),new Complex(2,3)],[new Complex(5,7),new Complex(-1,-3)]]);
let m3 = CMatrix.sum(m1,m2);
console.log(StringUtil.concatMultiline(m1.toString()," + ",m2.toString()," = ",m3.toString()));
which produces this output:
[10,20] + [1i ,2+3i ] = [10+1i,22+3i]
[30,40] [5+7i,-1-3i] [35+7i,39-3i]
Similarly, there are static methods in the CMatrix class for subtracting matrices (diff(m1,m2)), multiplying matrices (mult(m1,m2) and naryMult([m1,m2,...])), and for computing their tensor product (tensor(m1,m2) and naryTensor([m1,m2,...])). There are also some predefined vectors and matrices. For example,
console.log(Sim.ketOne.toString());
prints the column vector
[_]
[1]
and
console.log(Sim.CX.toString());
prints the 4×4 matrix for the CNOT (also called CX) gate:
[1,_,_,_]
[_,_,_,1]
[_,_,1,_]
[_,1,_,_]
Notice that zeros are replaced with underscores to make it easier for a human to read sparse matrices (to change this behavior, you can call toString({suppressZeros:false}) or toString({charToReplaceSuppressedZero:'.'})). You might also notice that the matrix for the CNOT gate looks different from the way it is usually presented in textbooks or other sources. This is related to the ordering of bits and ordering of tensor products. Search for the usingTextbookConvention flag in the source code for comments that explain this, and set that flag to true if you prefer the textbook ordering. We can also call a method on a matrix (or a vector) to change its ordering:
console.log(Sim.CX.reverseEndianness().toString());
prints the 4×4 matrix for the CNOT gate in its more usual form:
[1,_,_,_]
[_,1,_,_]
[_,_,_,1]
[_,_,1,_]
Simulating a Quantum Circuit
To simulate a circuit, one approach is to compute an explicit matrix for each layer (or step or stage) of the circuit. In a circuit with
The input state vector
psi_0 = CMatrix.tensor( Sim.ketZero, CMatrix.tensor( Sim.ketZero, Sim.ketZero ) );
console.log( psi_0.toString() );
psi_0 = CMatrix.naryTensor( [ Sim.ketZero /*q2*/, Sim.ketZero /*q1*/, Sim.ketZero /*q0*/ ] );
console.log( psi_0.toString() );
psi_0 = CMatrix.tensorPower( Sim.ketZero, 3 );
console.log( psi_0.toString() );
The quantum logic gates for
let L_1 = CMatrix.naryTensor( [ Sim.X, Sim.H, Sim.I ] ); // these are ordered q2, q1, q0
console.log( L_1.toString() );
Layers 2 and 4 involve gates with control bits.
The CX (also called controlled-X or controlled-NOT or CNOT) gate in layer 4 is described by a particular
// View this circuit in Quirk with
// https://algassert.com/quirk#circuit={%22cols%22:[[1,%22H%22,%22X%22],[%22X%22,%22%E2%80%A2%22],[%22Z%22],[1,%22%E2%80%A2%22,%22X%22]]}
let n = 3; // number of qubits
let psi_0 = CMatrix.tensorPower(Sim.ketZero,n); // initialization: ψ = |0> ⊗ |0> ⊗ |0> = |0>^(⊗3)
let L_1 = CMatrix.naryTensor( [ Sim.X, Sim.H, Sim.I ] );
let L_2 = CMatrix.tensor( Sim.I, Sim.CX.reverseEndianness() );
// This would also work:
// let L_2 = Sim.expand4x4ForNWires( Sim.CX, 1, 0, n );
let L_3 = CMatrix.naryTensor( [ Sim.I, Sim.I, Sim.Z ] );
let L_4 = CMatrix.tensor( Sim.CX, Sim.I );
// This would also work:
// let L_4 = Sim.expand4x4ForNWires( Sim.CX, 1, 2, n );
let psi_f = CMatrix.naryMult( [ L_4, L_3, L_2, L_1, psi_0 ] );
// Print the output state vector:
//console.log( psi_f.toString({binaryPrefixes:true}) );
// Print a summarized description of the computation:
console.log(StringUtil.concatMultiline(
L_4.toString(),
" * ", L_3.toString(),
" * ", "...", // L_2.toString(),
" * ", "...", // L_1.toString({decimalPrecision:1}),
" * ", psi_0.toString(), // input
" = ", psi_f.toString({binaryPrefixes:true}) // output
));
Each L_i matrix takes up
Notice that in a call to toString() above, we can optionally pass in {binaryPrefixes:true}; this causes bit strings like |000> to be printed in front of the matrix, as a reminder of the association between base states and matrix rows. The output of the above code is:
[1,_,_,_,_,_,_,_] [1,0 ,0,0 ,0,0 ,0,0 ] [1] |000>[0 ]
[_,1,_,_,_,_,_,_] [0,-1,0,0 ,0,0 ,0,0 ] [_] |001>[0 ]
[_,_,_,_,_,_,1,_] [0,0 ,1,0 ,0,0 ,0,0 ] [_] |010>[0 ]
[_,_,_,_,_,_,_,1] * [0,0 ,0,-1,0,0 ,0,0 ] * ... * ... * [_] = |011>[-0.707]
[_,_,_,_,1,_,_,_] [0,0 ,0,0 ,1,0 ,0,0 ] [_] |100>[0.707 ]
[_,_,_,_,_,1,_,_] [0,0 ,0,0 ,0,-1,0,0 ] [_] |101>[0 ]
[_,_,1,_,_,_,_,_] [0,0 ,0,0 ,0,0 ,1,0 ] [_] |110>[0 ]
[_,_,_,1,_,_,_,_] [0,0 ,0,0 ,0,0 ,0,-1] [_] |111>[0 ]
A second approach to simulating the same circuit is to not compute any explicit matrices of size
let n = 3; // number of qubits
let psi_0 = CMatrix.tensorPower(Sim.ketZero,n); // initialization: ψ = |0> ⊗ |0> ⊗ |0> = |0>^(⊗3)
let step1 = Sim.qubitWiseMultiply(Sim.H,1,n,psi_0);
step1 = Sim.qubitWiseMultiply(Sim.X,2,n,step1);
let step2 = Sim.qubitWiseMultiply(Sim.X,0,n,step1,[[1,true]] /*one control bit on wire 1*/ );
let step3 = Sim.qubitWiseMultiply(Sim.Z,0,n,step2);
let psi_f = Sim.qubitWiseMultiply(Sim.X,2,n,step3,[[1,true]] /*one control bit on wire 1*/ );
// Print the output state vector:
console.log( psi_f.toString({binaryPrefixes:true}) );
// Print a summarized description of the computation:
console.log(StringUtil.concatMultiline(
psi_0.toString(), // input
" -> ", step1.toString(),
" -> ", step2.toString(),
" -> ", step3.toString(),
" -> ", psi_f.toString({binaryPrefixes:true}) // output
));
In this second approach, the space and time requirements of each layer (or step) of the circuit are
To implement SWAP gates, consider another example circuit, containing a SWAP gate and a controlled SWAP (or Fredkin) gate:
We can compute its output with this:
// View this circuit in Quirk with
// https://algassert.com/quirk#circuit={%22cols%22:[[%22H%22],[%22Swap%22,1,%22Swap%22],[1,%22X%22,%22%E2%97%A6%22],[%22X%22,%22%E2%80%A2%22],[%22Y%22],[%22%E2%80%A2%22,%22Swap%22,%22Swap%22],[1,%22Z%22]]}
n = 3; // number of qubits
let ψ = CMatrix.tensorPower(Sim.ketZero,n); // initialization: ψ = |0> ⊗ |0> ⊗ |0> = |0>^(⊗3)
ψ = Sim.qubitWiseMultiply(Sim.H,0,n,ψ);
ψ = Sim.applySwap(0,2,n,ψ);
ψ = Sim.qubitWiseMultiply(Sim.X,1,n,ψ,[[2,false]] /*one anti-control bit on wire 2*/ );
ψ = Sim.qubitWiseMultiply(Sim.X,0,n,ψ,[[1,true]] /*one control bit on wire 1*/ );
ψ = Sim.qubitWiseMultiply(Sim.Y,0,n,ψ);
ψ = Sim.applySwap(1,2,n,ψ,[[0,true]] /*one control bit on wire 0*/ );
ψ = Sim.qubitWiseMultiply(Sim.Z,1,n,ψ);
console.log( ψ.toString({binaryPrefixes:true}) );
Consider another example, where we will compute partial traces. To simulate this circuit...
... we can use the following code snippet:
// View this circuit in Quirk with
// https://algassert.com/quirk#circuit={%22cols%22:[[%22H%22,1,%22H%22],[%22%E2%80%A2%22,%22X%22]]}
n = 3; // number of qubits
let ψ = CMatrix.tensorPower(Sim.ketZero,n); // initialization: ψ = |0> ⊗ |0> ⊗ |0> = |0>^(⊗3)
ψ = Sim.qubitWiseMultiply(Sim.H,0,n,ψ);
ψ = Sim.qubitWiseMultiply(Sim.H,2,n,ψ);
ψ = Sim.qubitWiseMultiply(Sim.X,1,n,ψ,[[0,true]] /*one control bit on wire 0*/ );
console.log( ψ.toString({binaryPrefixes:true}) );
Let rho_210 be the
let rho_210 = Sim.computeDensityMatrix( n, ψ );
let rho_0 = Sim.partialTrace( n, rho_210, true, null, [1,2], true /* the preceding list says which qubits to trace out */ );
let rho_1 = Sim.partialTrace( n, rho_210, true, null, [0,2], true );
let rho_2 = Sim.partialTrace( n, rho_210, true, null, [0,1], true );
let rho_10 = Sim.partialTrace( n, rho_210, true, null, [2], true );
let rho_20 = Sim.partialTrace( n, rho_210, true, null, [1], true );
let rho_21 = Sim.partialTrace( n, rho_210, true, null, [0], true );
Rather than passing in a list of qubits to trace out, we can instead pass in a list of qubits to keep, which can simplify things, e.g.
let rho_0 = Sim.partialTrace( n, rho_210, true, null, [0], false /* the preceding list says which qubits to keep */ );
In the above examples, we explicitly compute the full density matrix rho_210. However, computing rho_210 from the state vector requires
let rho_0 = Sim.partialTrace( n, null, true, ψ, [0], false );
let rho_1 = Sim.partialTrace( n, null, true, ψ, [1], false );
let rho_2 = Sim.partialTrace( n, null, true, ψ, [2], false );
let rho_10 = Sim.partialTrace( n, null, true, ψ, [2], true );
let rho_20 = Sim.partialTrace( n, null, true, ψ, [1], true );
let rho_21 = Sim.partialTrace( n, null, true, ψ, [0], true );
Printing the reduced density matrices can be done with statements like
console.log(StringUtil.concatMultiline(
"rho_2 = ", rho_2.toString(),
", rho_10 = ", rho_10.toString()
));
console.log(StringUtil.concatMultiline(
"rho_0 = ", rho_0.toString(),
", rho_21 = ", rho_21.toString()
));
console.log( "rho_20 =\n" + rho_20.toString() );
console.log( "rho_1 =\n" + rho_1.toString() );
Given the
Given the
We can also compute the Second Stabilizer Rényi Entropy (SSRE) to quantify magic of a set of qubits.
We do this by finding the
Circuit and Qubit Statistics
Here we consider in more detail a particular circuit and the statistics that can be computed for it, and compare these to the output of IBM Quantum Composer and of Quirk, to provide convincing evidence that Muqcs correctly computes these statistics.
From the amplitudes of an output state vector, we can easily find the probability of each computational basis state.
In addition, muqcs can compute the (
let N = 4; // total qubits
input = CMatrix.tensorPower(Sim.ketZero,N);
step1 = CMatrix.naryTensor( [ Sim.RY(45) /*q3*/, Sim.RX_90deg /*q2*/,
Sim.RX_90deg /*q1*/, Sim.RX(45) /*q0*/ ] );
step2 = CMatrix.naryTensor( [ Sim.RX(45) /*q3*/, Sim.RZ(120) /*q2*/,
Sim.RZ(100) /*q1*/, Sim.I /*q0*/ ] );
output = CMatrix.naryMult([ step2, step1, input ]);
output = Sim.qubitWiseMultiply(Sim.RZ_90deg,2,N,output,[[1,true]]/*list of control qubits*/);
output = Sim.qubitWiseMultiply(Sim.RY(45),2,N,output,[[3,true]]/*list of control qubits*/);
baseStateProbabilities = new CMatrix( output._rows, 1 );
for ( let i=0; i < output._rows; ++i ) baseStateProbabilities.set( i, 0, output.get(i,0).mag()**2 );
console.log(StringUtil.concatMultiline(
"Output: ", output.toString({binaryPrefixes:true}), ", Probabilities: ", baseStateProbabilities.toString()
));
Sim.printAnalysisOfEachQubit(N,output);
... and now the same circuit in IBM Quantum Composer:
// Copy-paste the below instructions into IBM’s website at https://quantum-computing.ibm.com/composer to recreate the circuit
OPENQASM 2.0;
include "qelib1.inc";
qreg q[4];
rx(pi / 2) q[1];
rx(pi / 2) q[2];
rx(pi/4) q[0];
ry(pi/4) q[3];
rz(1.7453292519943295) q[1];
rz(2.0943951023931953) q[2];
rx(pi/4) q[3];
crz(pi / 2) q[1], q[2];
cry(pi/4) q[3], q[2];
... and the same circuit in Quirk:
Conventions
In a circuit with N qubits, the wires are numbered 0 for the top wire to (N-1) for the bottom wire. The top wire encodes the Least-Significant Bit (LSB).
Limitations
There is currently no support for measurement gates.
The code depends on mathjs, but only in one subroutine (Sim.eigendecomposition()) which is used to compute concurrence and von Neumann entropy.
Background Notes
Think of bra (
Some predefined basis vectors:
Common names | Muqcs code | Size (rows x columns) | Matrix | Notes |
---|---|---|---|---|
Sim.braZero |
1x2 | [ 1 0 ] |
||
Sim.ketZero |
2x1 | [ 1 ] |
||
Sim.braOne |
1x2 | [ 0 1 ] |
||
Sim.ketOne |
2x1 | [ 0 ] |
||
Sim.braPlus |
1x2 | (1/sqrt(2)) [ 1 1 ] |
||
Sim.ketPlus |
2x1 | (1/sqrt(2)) [ 1 ] |
||
Sim.braMinus |
1x2 | 1/sqrt(2) [ 1 -1 ] |
||
Sim.ketMinus |
2x1 | (1/sqrt(2)) [ 1 ] |
||
Sim.braPlusI |
1x2 | (1/sqrt(2)) [ 1 -i ] |
||
Sim.ketPlusI |
2x1 | (1/sqrt(2)) [ 1 ] |
||
Sim.braMinusI |
1x2 | (1/sqrt(2)) [ 1 i ] |
||
Sim.ketMinusI |
2x1 | (1/sqrt(2)) [ 1 ] |
Consider a circuit of
A matrix
A matrix
A matrix
Any two of the above properties implies the third. All valid quantum gates are described by matrices that are unitary. Some of them (like I, X, Y, Z, H, CX, SWAP) are described by matrices that are additionally hermitian and involutory.
A density matrix is always hermitian, and its diagonal elements are real-valued and sum to 1.
Circuits consisting only of Clifford gates (which includes I, H, X, Y, Z, SX, SY, SZ, CX, SWAP) can be simulated in polynomial time on a classical computer, by the Gottesman-Knill theorem. Thus, mere superposition (which can be created with H gates) and entanglement (CX gates) are not sufficient to explain the speedup offered by quantum computers.
Matrices encoding the effect of a quantum gate:
Common names | Muqcs code | Qubits | Size | Notes |
---|---|---|---|---|
zero, 0 | Sim.ZERO |
1 | 2x2 | not unitary |
identity, I | Sim.I |
1 | 2x2 | no-op I = Phase(0) |
Hadamard, H | Sim.H |
1 | 2x2 | |
Pauli X, NOT | Sim.X |
1 | 2x2 | bit flip X = -iYZ = iZY = HZH |
Pauli Y | Sim.Y |
1 | 2x2 | Y = iXZ = -iZX = |
Pauli Z, Phase( |
Sim.Z or Sim.Phase(180)
|
1 | 2x2 | phase flip Z = Phase(180) Z = -iXY = iYX = HXH |
|
Sim.SX |
1 | 2x2 | The name SX means 'Square root of X' |
|
Sim.SY |
1 | 2x2 |
|
|
Sim.SZ or Sim.Phase(90)
|
1 | 2x2 | SZ = Phase(90) |
Sim.SSX |
1 | 2x2 | The name SSX means 'Square root of Square root of X' |
|
Sim.SSY |
1 | 2x2 |
|
|
|
Sim.SSZ or Sim.Phase(45)
|
1 | 2x2 | SSZ = Phase(45) |
global phase shift | Sim.GlobalPhase (angleInDegrees) |
1 | 2x2 | Has the same effect regardless of which qubit it is applied to; causes an equal phase shift in all amplitudes. Cannot be physically measured. |
phase shift | Sim.Phase (angleInDegrees) |
1 | 2x2 | Z = Phase(180) |
Sim.RX (angleInDegrees) |
1 | 2x2 | RX(a) = RZ(-90) RY(a) RZ(90) RX(a) = RY(90) RZ(a) RY(-90) |
|
Sim.RY (angleInDegrees) |
1 | 2x2 | RY(a) = RX(-90) RZ(a) RX(90) | |
Sim.RZ (angleInDegrees) |
1 | 2x2 | Phase(angle) * GlobalPhase( -angle/2 ) = RZ( angle ) Phase(angle) = RZ( angle ) * GlobalPhase( angle/2 ) Z = Phase(180) = RZ(180) * GlobalPhase(90) |
|
Sim.RotFreeAxis (ax,ay,az) |
1 | 2x2 | The angle, in radians, is encoded in the length of the given vector | |
Sim.RotFreeAxisAngle (ax,ay,az, angleInDegrees) |
1 | 2x2 | ||
Sim.SWAP_2 |
2 | 4x4 | ||
Sim.SWAP(i,j,n) |
2 | |||
CNOT, CX, XOR | Sim.CX |
2 | 4x4 |
zero, 0, Sim.ZERO
identity, I, Sim.I
Hadamard, H, Sim.H
Pauli X, NOT, Sim.X
Pauli Y, Sim.Y
Pauli Z, Phase(Sim.Z
, Sim.Phase(180)
Sim.SX
Sim.invSX
Sim.SY
Sim.invSY
Sim.SZ
, Sim.Phase(90)
Sim.invSZ
, Sim.Phase(-90)
Sim.SSX
Sim.invSSX
Sim.SSY
Sim.invSSY
Sim.SSZ
, Sim.Phase(45)
Sim.invSSZ
, Sim.Phase(-45)
global phase shift, Sim.GlobalPhase(angleInDegrees)
phase shift, Sim.Phase(angleInDegrees)
Sim.RX(angleInDegrees)
Sim.RY(angleInDegrees)
Sim.RZ(angleInDegrees)
Sim.RotFreeAxis(ax,ay,az)
(see definition in source code)
Sim.RotFreeAxisAngle(ax,ay,az, angleInDegrees)
(see definition in source code)
Sim.XE(k)
,
Sim.YE(k)
,
Sim.ZE(k)
,
Sim.Z_G(angle1InDegrees, angle2InDegrees)
Sim.Y_G(angle1InDegrees, angle2InDegrees)
Sim.H_G(angle1InDegrees, angle2InDegrees)
It's useful to be able to check matrix math using a symbolic math package like Mathematica. Here is some code to get started:
(* identity *)
(* id = {{1, 0}, {0, 1}}; *)
id = IdentityMatrix[2];
(* Hadamard *)
H = ((1/(2^0.5))*{{1, 1}, {1, -1}});
(* Pauli *)
X={{0,1},{1, 0}};
Y={{0,-I},{I, 0}};
Z={{1,0},{0, -1}};
(* Square roots of Pauli: X^0.5, Y^0.5, Z^0.5
SX, SY, SZ mean Square root of X, Y, Z
*)
SX = (0.5 * {{1+I, 1-I}, {1-I, 1+I}});
SY = (0.5 * {{1+I, -1-I}, {1+I, 1+I}});
SZ = ({{1, 0}, {0, I}});
(* inverses X^-0.5, Y^-0.5, Z^-0.25 *)
invSX = (0.5 * {{1-I, 1+I}, {1+I, 1-I}});
invSY = (0.5 * {{1-I, 1-I}, {-1+I, 1-I}}) ;
invSZ = ({{1, 0}, {0, -I}});
(* fourth roots and inverses *)
SSX = (0.5 * {{1+E^(I*Pi/4),1-E^(I*Pi/4)},{1-E^(I*Pi/4),1+E^(I*Pi/4)}});
SSY = (0.5 * {{1+E^(I*Pi/4),I*(E^(I*Pi/4)-1)},{I*(1-E^(I*Pi/4)),1+E^(I*Pi/4)}});
SSZ = ({{1,0},{0,E^(I*Pi/4)}});
invSSX = {{(2+2^0.5)/4-I/(2*2^0.5),(2-2^0.5)/4+I/(2*2^0.5)},{(2-2^0.5)/4+I/(2*2^0.5),(2+2^0.5)/4-I/(2*2^0.5)}};
invSSY = {{(2+2^0.5)/4-I/(2*2^0.5),1/(2*2^0.5)-I*(2-2^0.5)/4},{-1/(2*2^0.5)+I*(2-2^0.5)/4,(2+2^0.5)/4-I/(2*2^0.5)}};
invSSZ = ({{1,0},{0,E^(-I*Pi/4)}});
GlobalPhase[\[Theta]_] := E^(I*\[Theta]) * IdentityMatrix[2];
Phase[\[Theta]_] := {{1,0},{0,E^(I*\[Theta])}};
(* Pauli exponentials X^k, Y^k, Z^k *)
XE[k_] := (0.5 * {{1+E^(I*k*Pi), 1-E^(I*k*Pi)}, {1-E^(I*k*Pi), 1+E^(I*k*Pi)}});
YE[k_] := (0.5 * {{1+E^(I*k*Pi), I*(E^(I*k*Pi)-1)}, {I*(1-E^(I*k*Pi)), 1+E^(I*k*Pi)}});
ZE[k_] := ({{1, 0}, {0,E^(I*k*Pi)}});
(* rotation *)
RX[\[Theta]_] := {{Cos[\[Theta]/2],-I*Sin[\[Theta]/2]},{-I*Sin[\[Theta]/2],Cos[\[Theta]/2]}};
RY[\[Theta]_] := {{Cos[\[Theta]/2],-Sin[\[Theta]/2]},{Sin[\[Theta]/2],Cos[\[Theta]/2]}};
RZ[\[Theta]_] := {{E^(-I*\[Theta]/2),0},{0,E^(I*\[Theta]/2)}};
(* alternative definitions:
RXalt[\[Theta]_] := XE[\[Theta]/Pi] . GlobalPhase[-\[Theta]/2];
RYalt[\[Theta]_] := YE[\[Theta]/Pi] . GlobalPhase[-\[Theta]/2];
RZalt[\[Theta]_] := ZE[\[Theta]/Pi] . GlobalPhase[-\[Theta]/2];
*)
(* Hadamard exponential H^k *)
HE[k_] := {
{ (2+2^0.5)/4 + (E^(I*k*Pi))*(2-2^0.5)/4 , 1/(2*2^0.5) - (E^(I*k*Pi))/(2*2^0.5) },
{ 1/(2*2^0.5) - (E^(I*k*Pi))/(2*2^0.5) , (2-2^0.5)/4 + (E^(I*k*Pi))*(2+2^0.5)/4 }
};
(* generalized gates *)
Zgeneralized[a_,b_] := {{E^(I*a),0},{0,E^(I*b)}};
Ygeneralized[a_,b_] := {{0,E^(I*b)},{E^(I*a),0}};
Hgeneralized[a_,b_] := ((1/2^0.5)*{{E^(I*a),E^(I*b)},{E^(I*a),-E^(I*b)}});
(* Example computation: we test if X == HZH, by checking if the below expression yields zero *)
X - H.Z.H // Chop
(* Another example computation: we test if H^k == (Y^-0.25) (X^k) (Y^0.25), by checking if the below expression yields zero *)
HE[k] - invSSY . XE[k] . SSY // FullSimplify // Chop