Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add CTMRG support for regular 2D partition functions #111

Open
wants to merge 48 commits into
base: master
Choose a base branch
from

Conversation

leburgel
Copy link
Collaborator

@leburgel leburgel commented Jan 3, 2025

Rebased from @Sander-De-Meyer's initial work here.

Some todos left:

  • Agree on struct names
  • Change indexing convention for local partition function tensor to W ⊗ S ← N ⊗ E and document
  • Reorganize partition function code a bit (i.e. try not to mix in with PEPS code in the same files if possible)

@leburgel leburgel self-assigned this Jan 3, 2025
@leburgel leburgel marked this pull request as draft January 3, 2025 15:34
Copy link

codecov bot commented Jan 3, 2025

Copy link
Member

@lkdvos lkdvos left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Starting to look good!
I left some remarks alreayd, but some general remarks here:

For the docstrings, I don't think, it makes sense to have separate docstrings for both the partition function and PEPS (and eventually PEPO) implementations. Rather, we could use A in the figure, and then specify that A might be either bra-ket, partition function or bra-pepo-ket combinations. As a result, we should probably also agree on a style of drawings (let's do this in a separate PR) that uses the same character for drawing tensor connections, and maybe either never draw boxes around tensors or always draw boxes.
(I am assuming that you left some of the docstrings unattached to functions on purposes as this is still a WIP port?)

For naming things, InfinitePartitionFunction seems like the best we can do, since I really cannot come up with anything better. Given that it is rather long, how would you feel about InfinitePF, similar to InfinitePEPS? (Along with PFTensor etc)

src/algorithms/contractions/ctmrg_contractions.jl Outdated Show resolved Hide resolved
src/algorithms/contractions/ctmrg_contractions.jl Outdated Show resolved Hide resolved
src/algorithms/contractions/ctmrg_contractions.jl Outdated Show resolved Hide resolved
src/algorithms/contractions/ctmrg_contractions.jl Outdated Show resolved Hide resolved
@@ -57,6 +57,46 @@ function LinearAlgebra.norm(peps::InfinitePEPS, env::CTMRGEnv)
return total
end

function LinearAlgebra.norm(partfunc::InfinitePartitionFunction, env::CTMRGEnv)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not really convinced that this should overload norm for this purpose, given that it's not really a state, and there is no overlap or conj etc...

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair. Any other name would be fine for me as well, but I'm having a bit of trouble thinking of a suitable one. Just value might be the most 'correct' since the network just represents a number in the end?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I settled on value for now and added a corresponding export, let me know if you prefer something else.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, this didn't quite work as expected since norm is called inside the CTMRG leading_boundary to give the current PEPS norm in the logging output. For one, it seems a bit wasteful to do the norm contraction every iteration regardless of the logging verbosity. It would probably be good to make the computation conditional on the logging level, no?

While I can then make the partition function tests pass by setting verbosity=0, this is clearly not a fix. So we should probably come up with a name that makes sense for any network type and use that in the CTMRG logging. Does value still make sense in the more general case?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the cleaner way is to redefine the ctmrg_log... functions to take in the state instead, and specialize on what kind it is to determine what should be printed? (along with hiding this behind a verbosity check to avoid unnecessary computations)

Copy link
Collaborator Author

@leburgel leburgel Jan 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, but to do this I would need to be able to access the current verbosity in from the ctmrg_log... functions inside the withlevel block, and I don't immediately see how to do that. I could spend some more time, but since I'm getting warnings that the verbosity macros from LoggingExtras.jl have been deprecated maybe it would be better to reconsider the logging more thoroughly anyway?

To patch this for now I added a network-dependent ctmrg_objective and made its computation verbosity dependent. I'm very annoyed at the explicit verbosity level integers inside the leading_boundary body now, but I don't immediately see a way around this unless I can manage to figure out the above issue with accessing the verbosity.

Do you think this is sufficient for this PR where we address the remaining issues in a follow-up?

src/states/abstractpeps.jl Outdated Show resolved Hide resolved
src/states/infinitepartitionfunction.jl Outdated Show resolved Hide resolved
src/states/infinitepartitionfunction.jl Outdated Show resolved Hide resolved
test/ctmrg/partition_function.jl Outdated Show resolved Hide resolved
@leburgel
Copy link
Collaborator Author

leburgel commented Jan 7, 2025

For the docstrings, I don't think, it makes sense to have separate docstrings for both the partition function and PEPS (and eventually PEPO) implementations. Rather, we could use A in the figure, and then specify that A might be either bra-ket, partition function or bra-pepo-ket combinations.

Sounds good, I'll try to standardize as much as I can here. I agree we should be more systematic about the diagrams, I'm happy to put some effort into that soon.

For naming things, InfinitePartitionFunction seems like the best we can do, since I really cannot come up with anything better. Given that it is rather long, how would you feel about InfinitePF, similar to InfinitePEPS? (Along with PFTensor etc)

I don't really like the 'PF' acronym, for the simple reason that no one really uses it so there's no way of knowing what it means without actually reading the docstring. This is quite different for 'PEPS' I think. I'd be happy to use the shorter names as internal aliases, but I wouldn't like to export InfinitePF like that. Makes sense?

leburgel and others added 3 commits January 7, 2025 09:26
Add internal convenience aliases for different flavors of edge tensors

Co-authored-by: Lukas Devos <[email protected]>
@pbrehmer
Copy link
Collaborator

pbrehmer commented Jan 7, 2025

Thanks a lot for the effort! To me the PR mostly looks quite nice already.

On the topic of naming things: I am a bit confused by calling the AbstractTensorMap{S,2,2} a partition function at all. Wouldn't a more generic name be MPOTensor (and InfiniteMPO)? There could probably be applications where you would want to contract or optimize a generic MPO which doesn't have to represent a partition function physically, so calling it PartitionFunctionTensor might be too specific. What do you think?

Also: let me know if I can contribute or finish up something! I anyway wanted to add 2D partition function support at some point so I'm happy to help.

@leburgel
Copy link
Collaborator Author

leburgel commented Jan 7, 2025

On the topic of naming things: I am a bit confused by calling the AbstractTensorMap{S,2,2} a partition function at all. Wouldn't a more generic name be MPOTensor (and InfiniteMPO)?

I agree using 'partition function' might imply some additional physical properties rather than just a network structure, which is really only what we mean here. The reason we didn't just reuse MPSKit.MPOTensor and MPSKit.InfiniteMPO is that these are really one-dimensional structures with some additional assumptions as well.

I would personally be okay with using the term 'partition function' for things with no stat mech origin, or at least more than I would be with using 'MPO' for 2D network structures. We could also go for SquareTensor and InfiniteGrid if we want to be more generic?

@pbrehmer
Copy link
Collaborator

pbrehmer commented Jan 7, 2025

Well, in principle partition functions could also be a 1D or 3D thing, right? So I feel a bit icky about using PartitionFunctionTensor in that way.

I do think it could make sense to use the MPSKit.MPOTensor alias because that does not yet imply a 1D structure. For InfiniteMPO I agree, we would need a different name (also to avoid a clash with MPSKit). What do you think about InfiniteMPOGrid or InfiniteMPONetwork?

@lkdvos
Copy link
Member

lkdvos commented Jan 7, 2025

To be fair, matrix product operator really does not make too much sense either in the 2D grid case...
The only other thing I can come up with is Projected Entangled Pair Function (PEPF), which might make some sense.
Ultimately, given that there is no term for this anywhere, we should just pick something that's practical for us though, PEPSKit.InfinitePartitionFunction does have the undertone of being 2D given that it is in PEPSKit. If you really want, we could also just add a type parameter N to give the dimensionality, which is reflected in the array type

@lkdvos
Copy link
Member

lkdvos commented Jan 8, 2025

I think my preference for the drawings is unicode lines, without arrows. In order to add the missing information about which spaces are domain-codomain, we would then have to specify this in the docs of the individual tensor types, and dedicate a page in the docs for this

Comment on lines 94 to 106
function local_contraction(
O::AbstractTensorMap{S,2,2}, env::CTMRGEnv{C,<:CTMRG_PF_EdgeTensor}
) where {S,C}
return @autoopt @tensor env.corners[NORTHWEST, 1, 1][C_WNW; C_NNW] *
env.edges[NORTH, 1, 1][C_NNW D_N; C_NNE] *
env.corners[NORTHEAST, 1, 1][C_NNE; C_ENE] *
env.edges[EAST, 1, 1][C_ENE D_E; C_ESE] *
env.corners[SOUTHEAST, 1, 1][C_ESE; C_SSE] *
env.edges[SOUTH, 1, 1][C_SSE D_S; C_SSW] *
env.corners[SOUTHWEST, 1, 1][C_SSW; C_WSW] *
env.edges[WEST, 1, 1][C_WSW D_W; C_WNW] *
O[D_W D_S; D_N D_E]
end
Copy link
Collaborator

@pbrehmer pbrehmer Jan 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a small detail, but in order for @autoopt to work here, the index labels need to follow our convention: the environment indices have to start with χ.

Also, instead of defining this contraction manually, it would probably pay of to adapt contract_localnorm to work for InfinitePartitionFunctions. That would make it much easier to work with unit cells or more complicated operators.

I can get that in since I'm anyway going the through the code right now.

Copy link
Collaborator Author

@leburgel leburgel Jan 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was just in the middle of generalizing and adding this to src. Didn't know about the @autoopt convention, thanks for that!

I added a generic contraction for a single local tensor (AbstractTensorMap{S,2,2}) at a given site in a partition function. Unit cells should be fine, more complicated operators not at this point. It's not immediately clear to me how to do this, since the 'operator' here is contracted into some set of virtual indices instead of being applied to physical indices. For the simplest case it's obvious what this means, but for more complicated cases you would need some conventions for the index ordering on the bigger local tensor.

Copy link
Collaborator

@pbrehmer pbrehmer Jan 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, the operator in principle is just a InfinitePartitionFunction again, right? Just looking at the contraction, local_contraction is the same as contract_localnorm. The only difference is the object that is contracted inside the environment. So in my head, the user should construct an operator as a InfinitePartitionFunction and then just call expectation_value which only uses contract_localnorm under the hood.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think I agree with interpreting O as an InfinitePartitionFunction, it's just a single different local tensor surrounded by some given partition function on all sides. I don't pass the surrounding partition function here since it's not needed in the contraction, but it is really inserting a single (potentially) different O tensor at a given site surrounded by the original partition function. In other words, the environment given is not necessarily an environment of a network of all Os, so I passing the operator as an InfinitePartitionFunction does not make sense I think. It's quite different from the local norm in spirit even though the contraction is the same. Does that make sense?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes that does make sense to me. My point was more that given a unit cell of AbstractTensorMap{S,2,2} operators (which corresponds to our InfinitePartitionFunction) and some environment, the correct contraction to obtain the expectation value can be performed only using contract_localnorm. I just wanted to reuse that structure since we already have that. Making a special case for a single operator via local_contraction perhaps seems too specific given that we already have all this cool generic machinery that should be easy to adapt to partition functions.

Maybe we could make a method for expectation_value that dispatches on Matrix{AbstractTensorMap{S,2,2}} and even a special method that dispatches on a single AbstractTensorMap{S,2,2} operator and then internally use contract_localnorm?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just adding on to this: replacing a single tensor is fundamentally different from InfinitePartitionFunction, since that one repeats the unitcell over the entire grid, while here you really want to change a single site in the entire grid.
I'm definitely happy with sharing some of the contraction code between the implementation of contract_localnorm, but this should probably be decoupled from the actual API functions, since the interpretation really is different.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as API goes, expectation_value calling contract_local_tensor sounds fairly 'correct' to me in the current implementation, given that it's really contracting in a single different local tensor. If we want more general 'expectation values', I agree that it would be nice if we were able to reuse the implementation of contract_localnorm. However:

  • To be able to reuse the actual code, we would need to split out the part of contract_localnorm that handles indices and conjugates for the edges and the 'effective MPO part' of the contraction inside the environment. This is basically everything besides the corners. While reusing the machinery in spirit is of course what we want, this will still require separate dedicated code to handle contraction expressions for different types of network stacks, right? Of course this is not a real issue at all, except that
  • I really don't know how useful an expectation_value for Matrix{AbstractTensorMap{S,2,2}} actually is. Usually there's two purposes for 'local' contractions in partition functions, namely really local expectation values or correlation functions. If it's a simple local expectation value, it's a single AbstractTensorMap{S,2,2} being contracted which is currently covered. If it's a correlation function, this generically involves two tensor which each have an extra index being contracted across the pair (when imposing symmetries, this is almost always the case). Even for more involved local observables, this would usually involve a single higher-rank tensor (e.g. a plaquette observable) rather than a collection of AbstractTensorMap{S,2,2}s. None of these really require or can even be written in terms of contracting a generic Matrix{AbstractTensorMap{S,2,2}} inside a given environment.

In summary, while I would be fine with adding support for expectation_values of Matrix{AbstractTensorMap{S,2,2}} and then reuse the existing machinery to implement this, I'm really not sure this is what we actually need.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright, I wasn't sure if there is some use to the Matrix{AbstractTensorMap{S,2,2}} contraction since I've not yet used 2D partition function PEPS much. And of course I agree that the interpretation of inserting a single operator is different from InfinitePF. Then let's keep it the way it is right now.

But if I understood correctly we can't yet contract correlation functions or operator strings with contract_local_tensor, right? In that case we would need a version of contract_localnorm (with adjusted edge/MPO indices) to expand the environment in the corresponding directions.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, contract_local_tensor doesn't support correlation functions,. For that case we would definitely need the logic of contract_localnorm, but adapted for MPO-style indices and with an extra contraction for the additional stringy indices. All very doable, but a bit tricky so I was trying to procrastinate this until someone explicitly needed it :)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay I'll give it a try :)

@leburgel leburgel marked this pull request as ready for review January 9, 2025 15:16
@leburgel leburgel requested a review from lkdvos January 9, 2025 15:17
@pbrehmer
Copy link
Collaborator

pbrehmer commented Jan 9, 2025

The InfiniteSquareNetwork interface already reduces the code duplication among InfinitePEPS, InfinitePEPO and InfinitePF quite a bit. I'm not yet sure, however, how to best incorporate the abstract type in sparse_environments.jl and ctmrg_contractions.jl. My plan would be to reduce the redundancies by having methods which could dispatch on either InfinitePF, Tuple{InfinitePEPS,InfinitePEPS} or eventually also Tuple{InfinitePEPS,InfinitePEPO,InfinitePEPS}. The goal is that only the methods that really contain the @tensor contractions specialize on the underlying tensor type - everything else (i.e. the methods that select the right coordinates) should be generic enough to only require one method per contraction, if that makes sense...

I'll finish up the rest tomorrow :-)

@pbrehmer
Copy link
Collaborator

There is a design decision to be made if we want to generalize our CTMRG code to also support different ket and bras, as well as PEPOs, and I'm not sure what the best way forward is. Right now, all CTMRG contractions support different ket and bra but there is no way for the user to pass this onto the CTMRG algorithm. And in principle our code is generic enough to take any state (which in principle should be a InfiniteSquareNetwork) which is then passed onto EnlargedCorner and the CTMRG contractions.

The problem is that there is no InfinitePEPSKetBra type which contains both the ket and the bra InfinitePEPS which the user can pass to leading_boundary. Similarly, if we want to support PEPOs, we would need a InfiniteKetPEPOBra type which contains the ket, bra as InfinitePEPSs and the InfinitePEPO. Should we implement such types (names are up for discussion :P) and subtype as InfiniteSquareNetwork or is that redundant somehow? Of course this is not strictly necessary to support 2D partition functions (the code runs as is) but I thought it would pay off to invest some time into generalizing this interface while we're already at it. I hope that makes sense!

@leburgel
Copy link
Collaborator Author

There is a design decision to be made if we want to generalize our CTMRG code to also support different ket and bras, as well as PEPOs, and I'm not sure what the best way forward is. Right now, all CTMRG contractions support different ket and bra but there is no way for the user to pass this onto the CTMRG algorithm. And in principle our code is generic enough to take any state (which in principle should be a InfiniteSquareNetwork) which is then passed onto EnlargedCorner and the CTMRG contractions.

I think it makes sense to have all the lowest level methods for things like leading_boundary and EnlargedCorner to work on a 'fully specified' network to be contracted. By this I mean either a partition function, a real pair of ket and bra PEPS, and so on. Passing just an InfiniteSquareNetwork to contains extra assumptions on the other layer in the network. This is fine, but it seems natural to make this a layer around the core routines, which always take fully specified disctinct layers. That also solver the issue of not being able to pass distinct ket and bra PEPS, since you could then do this by just skipping this first layer and directly calling the core routines.

The problem is that there is no InfinitePEPSKetBra type which contains both the ket and the bra InfinitePEPS which the user can pass to leading_boundary. Similarly, if we want to support PEPOs, we would need a InfiniteKetPEPOBra type which contains the ket, bra as InfinitePEPSs and the InfinitePEPO. Should we implement such types (names are up for discussion :P) and subtype as InfiniteSquareNetwork or is that redundant somehow?

I don't know if having these as actual types is necessary, it might be but it's not clear to me. Just a union type of tuples for all different possible sandwiches would do the job to start. I certainly don't think it makes sense to make these potential full sandwich types subtypes of InfiniteSquareNetwork, since then the hierarchy would become a bit circular I think. At least with the current meaning of InfiniteSquareNetwork these new types don't really naturally fall under it I think.

Of course this is not strictly necessary to support 2D partition functions (the code runs as is) but I thought it would pay off to invest some time into generalizing this interface while we're already at it. I hope that makes sense!

I'm a bit biased of course since I want to start using this as soon as I can, but purely in terms of compartmentalizing features I think it would make sense to just restrict this PR to working partition functions. Just to keep additions from growing bigger and bigger I would break out additional discussions as issues or follow up PRs. Of course I'm not in that much of a rush, but it seems we're moving away from the initial purpose a bit already.

@lkdvos
Copy link
Member

lkdvos commented Jan 10, 2025

I'm certainly not against that. I know we previously have done so, and called it PEPSSandwich, and PEPOSandwich. I do quite like those names 😉

[edit]
After reading @leburgel answer, I guess we really are straying away a bit too far from the PRs purpose, and are definitely cramming in way too many changes at once.

@pbrehmer
Copy link
Collaborator

I don't know if having these as actual types is necessary, it might be but it's not clear to me. Just a union type of tuples for all different possible sandwiches would do the job to start. I certainly don't think it makes sense to make these potential full sandwich types subtypes of InfiniteSquareNetwork, since then the hierarchy would become a bit circular I think. At least with the current meaning of InfiniteSquareNetwork these new types don't really naturally fall under it I think.

I have also considered this but the problem with tuples as sandwiches is that you need to be able to index the sandwich. But I do agree that it doesn't make sense to subtype the sandwiches as InfiniteSquareNetworks as well since it would create a circular hierarchy. Maybe it's fine to just add the corresponding indexing methods for tuples, e.g. Tuple{InfinitePEPS,InfinitePEPS}, but I'm not sure it will be pretty.

Anyways, I agree that we're putting too many additions into this PR and should probably generalize the CTMRG interface in another PR - I will open up an issue! I will finish up the contraction business and then the PR is good to go for me.

@pbrehmer
Copy link
Collaborator

Alright, I'd be fine with merging this as is :-) (once the lights turn green)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants