Hello
I’d appreciate some feedback/opinions on the following.
For a few years now I have been developing a package built on top of ITensors. But I am always rethinking and brainstorming ways I could implement my code better, frequently refactoring code when I find better ways of doing things in other peoples code (like on ITensors github) or the Julia discourse forums.
To date I have basically used ITensors types directly. What I mean is that, I have my own types but they are built up using ITensors. For example, I am solving partial differential equations for fluid flow so I might have something like this:
struct FlowSimulation
u::MPS
v::MPS
ddx::MPO
ddy::MPO
etc...
end
I have some methods I have defined that only make sense for MPS representing flow fields. They make some assumptions about the ordering of indices and how MPS/MPO are created. So far I have what I think is a pretty naive implementation that looks like this.
function do_something_to_1D_MPS(mps::MPS)
# code here...
end
function do_something_to_2D_MPS(mps::MPS)
# code here...
end
function do_something_to_3D_MPS(mps::MPS)
# code here...
end
I don’t feel this is very idiomatic though. I’m considering the following two options but I am not sure which is better and for what reasons.
Here is approach one.
abstract type Abstract_MPS_ND end
mutable struct MPS_ND{dim} <: Abstract_MPS_ND
mps::MPS
indexing_scheme::String
end
Then I could define constructors for these that determine dim
(not shown below) and then I can dispatch my special methods on these types.
const MPS_1D = MPS_ND{1}
const MPS_2D = MPS_ND{2}
const MPS_3D = MPS_ND{3}
function do_something_to_MPS(mps::MPS_1D)
# code here...
end
function do_something_to_MPS(mps::MPS_2D)
# code here...
end
function do_something_to_MPS(mps::MPS_3D)
# code here...
end
And then I’d need to overload the ITensor methods to know how to use my type. So something along these lines. I would have to do this for every ITensor method that I needed to use.
ITensors.contract(a::T, b::V; kwargs...) where {T<:Abstract_MPS_ND, V<:Abstract_MPO_ND}
return MPS_ND{ndims(a)}(
contract(a.mps, b.mpo; kwargs...),
a.indexing_scheme
)
end
# in-place example
ITensors.truncate!(a::T; kwargs...) where {T<:Abstract_MPS_ND} = truncate!(a.mps; kwargs...)
I feel this is more elegant than what I have now. The only downside is I have to overload all the ITensor methods but that isn’t that much work in the grand scheme of things.
The other idea I had was to subtype AbstractMPS
from ITensors. I think this would require defining MPS_ND
to have the same fields as what is expected by AbstractMPS
. I would just attach one extra field (indexing_scheme
) that my own methods would use. This way I would not have to overload all the ITensor methods except where I needed special behavior. I think I’d also need to overload certain methods (like contract
) so they returned my type instead of an MPS
.
Does that make sense? If so, I think it would look something like this.
mutable struct MPS_ND{dim} <: AbstractMPS
data::Vector{ITensor}
llim::Int
rlim::Int
indexing_scheme::String
end
Of these two approaches, which do you think is better? Or is there a third I haven’t even thought of that is even better?