GTorlai / PastaQ.jl

Package for Simulation, Tomography and Analysis of Quantum Computers
Apache License 2.0
142 stars 23 forks source link

Unify `PastaQ` `state` and `gate` with ITensors `state` and `op` #247

Closed mtfishman closed 2 years ago

mtfishman commented 2 years ago

Internally, we should unify the state and gate interface in PastaQ with the state, op, and OpSum interface in ITensors.

Part of this would be to have a hierarchy of types representing gates or operators which would be used internally by both ITensors and PastaQ, to simplify parsing gate lists and to have a common format for making gate lists, Trotter circuits, and MPOs.

The starting point would be something like this:

struct SimpleOp{N}
  name::String
  sites::NTuple{N,Int}
  params::NamedTuple
end

This is essentially just the SiteOp type used by OpSum: https://github.com/ITensor/ITensors.jl/blob/v0.2.9/src/physics/autompo.jl#L10-L14 with a new name (since SiteOp doesn't make much sense, since it can by multi-site). So this is just a "simple" gate representation like:

("CRz", (1, 2), (ϕ=0.1,))
("CRz", 1, 2, (ϕ=0.1,))

Then, these would get put together into a product (or sum of products) to make an operator Op, like this:

struct Op{F<:Function,T<:Number,N}
  f::F
  coefficient::T
  opsum::Vector{Vector{SimpleOp{N}}}
end

This is similar to the MPOTerm type used by OpSum (https://github.com/ITensor/ITensors.jl/blob/v0.2.9/src/physics/autompo.jl#L90-L93). This would be the internal representation for more sophisticated (sums of) products of operators as well as function applications, like:

(2.0, "CX", 1, 2)
(exp, "CX", 1, 2)
(exp, 0.1im, "a†", 1, "a", 2)
(exp, 0.1im, "a† * a", 1, 2)
(exp, 0.1im, "a† * a + a * a†", 1, 2)

The reason it is a Vector{Vector{SimpleOp}} is that the inner Vector stores a product of SimpleOp and the outer Vector stores a sum of those products, so (exp, 0.1im, "a† * a + a * a†", 1, 2) would get parsed into a structure like:

f = exp
coefficient = 0.1im
opsum = [
  [SimpleOp("a†", (1,), (;)), SimpleOp("a", (2,), (;))],
  [SimpleOp("a", (1,), (;)), SimpleOp("a†", (2,), (;))]
]

In principle the field opsum could be a general lazy representation for adding and multiplying gates, but I think adding sums of gates is sufficient. Finally, I think we can restrict all of the terms in the sum to have support on the same sites, so maybe something like this:

struct Op{F<:Function,T<:Number,N}
  f::F
  coefficient::T
  opsum::Vector{Vector{SimpleOp{N}}}
  sites::NTuple{N,Int}
end

where sites is the same as the sites of all of the Vector{SimpleOp{N}} terms. The internal constructor could check that the sites of all of the Vector{SimpleOp{N}} are all the same and then store them in the sites field.

A question would be how to represent products of on-site terms as well. One proposal would be:

(exp, 0.1im, "X * Y + Y * X", 1, 1)

in which case it would fit into the same data structure as above. To me this makes sense since you can interpret the above as exp(0.1im (X₁ Y₁ + Y₁ X₁)). There would be a question about how to do mixtures of on-site and multi-site products. In that case you could do:

(exp, 0.1im, "X * Y * Z + Y * X * Z", 1, 1, 2)

If you don't have a product somewhere, you could use an identity:

(exp, 0.1im, "X * I * Z + Y * X * Z", 1, 1, 2)

I think we could make "I" interpreted as a universal identity I (Julia already has that defined) so it would be a no-op. In principle we could get pretty fancy and have a syntax like this:

(exp, 0.1im, "X(1) * Z(2) + Y(1) * X(1) * Z(2)")

where the sites in the parentheses get extracted when parsing.

Then, the ITensors.jl OpSum could be defined as:

struct OpSum
  ops::Vector{Op}
end

so simply a sum of Ops.

Additionally, the PastaQ/ITensor operator or gate lists for gate evolution can be converted to a Vector{Op} internally. We could even consider defining something like:

struct Circuit
  ops::Vector{Op}
end

or maybe OpList, GateList, OpProd, GateProd, etc. as an internal representation for a circuit. In that way, we can define functions like inv, dag, etc. of a circuit. We could have fields inv and dag in Op or SimpleOp to store the fact that an inverse or dagger needs to be performed (and maybe other standard fields, like isunitary, isorthogonal, grad, etc.).

This would help with unifying gate and op, since there could be a single functionality for converting an Op into a Matrix or ITensor (i.e. parsing the opname strings to get sums of products of gates that get turned into an Op which then gets turned into the correct Matrix/ITensor). So basically all of the complexity for converting an Op to a Matrix/ITensor would be in functions like op(::Op, ::SiteType).

Ultimately I think gate could just be an alias for op (or a wrapper), since their functionality is very similar. The actual definitions of the gates for the Qubit/Qudit site types could be in either PastaQ or ITensors (core standard ones in ITensors and more complicated quantum-computing specific ones in PastaQ).

Finally, a small piece is unifying the state implementation. I think we can basically just import state from ITensors and remove the PastaQ implementation.

mtfishman commented 2 years ago

An additional generalization would be to allow a Matrix in the SimpleOp struct instead of a string for the opname, like is proposed here: https://github.com/ITensor/ITensors.jl/issues/722

That could be done by extending SimpleOp to:

struct SimpleOp{N,T<:Union{String,Matrix}}
  name::T
  sites::NTuple{N,Int}
  params::NamedTuple
end

(and maybe rename the field name to something more general like op).

mtfishman commented 2 years ago

Another thing to consider is that we could expose the Op and Circuit types externally to allow operations like:

exp(Op("X", 1)) -> Op(exp, "X", 1)
2 * Op("X", 1) -> Op(2, "X", 1)
Op("X", 1) * Op("Y", 2)
Op("X", 1) * Op("Y", 2) + Op("Y", 1) * Op("X", 2)

but that is more of an interface enhancement that would iterate on the plan above.