Skip to content

Conversation

@Yue-Zhengyuan
Copy link
Member

@Yue-Zhengyuan Yue-Zhengyuan commented Oct 23, 2025

This PR provides an improved interface for current and upcoming (Trotter-based) time evolution algorithms.

Time evolution is now performed by an iterator, to allow easier access and finer control of the intermediate steps.

mutable struct TimeEvolver{TE <: TimeEvolution, G, S, N <: Number}
    "Time evolution algorithm (currently supported: `SimpleUpdate`)"
    alg::TE
    "Trotter time step"
    dt::N
    "The number of iteration steps"
    nstep::Int
    "Trotter gates"
    gate::G
    "PEPS/PEPO and its environment"
    state::S
end
  • The internal state state stores the evolution outcome after each step, and will change during the evolution.
  • dt can be any real or complex number or any precision.
  • The 2-site (nearest neighbor only) and 3-site (up to next nearest neighbor) version of the update are dispatched based on the type of gate.

For simple update, state contains the following data:

struct SUState{S <: InfiniteState, E <: SUWeight, N <: Number}
    "number of performed iterations"
    iter::Int
    "evolved time"
    t::N
    "PEPS/PEPO"
    psi::S
    "SUWeight environment"
    env::E
end

The upper-level interface of time evolution are the following functions:

# expert mode: directly dealing with the iterator
## creating the iterator (can be used with for-loop syntax)
TimeEvolver(psi0, H, dt, nstep, alg, env0; t0 = 0.0)
## one step of iteration on an externally specified state (wrapper of Base.iterate(it, state))
MPSKit.timestep(it, psi, env) -> (ψ, env, info)
## evolve to the end without interruption
MPSKit.time_evolve(it; tol = 0.0, kwargs...) -> (ψ, env, info)

# normal usage: the iterator is handled internally (wrapper of time_evolve(it))
MPSKit.time_evolve(psi0, H, dt, nstep, alg, env0; tol = 0.0, t0 = 0.0, kwargs...) -> (ψ, env, info)
  • t0 records the initial time of the input state. The time of the output state is recorded in info.t.
  • Real and imaginary time evolution are distinguished by imaginary_time::Bool in the SimpleUpdate algorithm struct. The default value is true (different from MPSKit).
  • Convergence check is enabled by setting tol > 0:
    • For simple update, convergence is implied by weight difference between two steps < tol.
    • For full update, convergence is implied by energy difference between two steps < tol.
  • I still don't quite want to create a separate ground_state_search function. (1) In practice one set the nstep to a very large number (of order 1e+5 to 1e+6), instead of letting it evolve indefinitely, since sometimes things don't converge well. (2) Except the need to check convergence, it is not too different from other usages. (3) It's usually not actually the ground state, and only serve as initialization for more sophisticated optimizations.

To do:

  • Polish the docstring.
  • Update the examples with the new time evolution interface. (Docs will be re-generated in a separate PR.)

@codecov
Copy link

codecov bot commented Oct 23, 2025

Codecov Report

❌ Patch coverage is 93.87755% with 6 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/algorithms/time_evolution/simpleupdate.jl 95.77% 3 Missing ⚠️
src/algorithms/time_evolution/evoltools.jl 75.00% 2 Missing ⚠️
src/algorithms/time_evolution/time_evolve.jl 91.66% 1 Missing ⚠️
Files with missing lines Coverage Δ
src/PEPSKit.jl 100.00% <ø> (ø)
src/algorithms/time_evolution/simpleupdate3site.jl 100.00% <100.00%> (ø)
src/algorithms/truncation/bond_truncation.jl 97.01% <ø> (ø)
src/operators/infinitepepo.jl 77.00% <ø> (+5.57%) ⬆️
src/algorithms/time_evolution/time_evolve.jl 91.66% <91.66%> (ø)
src/algorithms/time_evolution/evoltools.jl 95.04% <75.00%> (-1.73%) ⬇️
src/algorithms/time_evolution/simpleupdate.jl 97.69% <95.77%> (-2.31%) ⬇️

... and 10 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@Yue-Zhengyuan Yue-Zhengyuan mentioned this pull request Oct 24, 2025
1 task
@Yue-Zhengyuan Yue-Zhengyuan marked this pull request as draft October 26, 2025 00:58
@Yue-Zhengyuan
Copy link
Member Author

I'm going to try making time evolution an iterator (the approach of YASTN) to allow easier user access to the intermediate states.

@Yue-Zhengyuan Yue-Zhengyuan marked this pull request as ready for review October 28, 2025 13:39
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.

I definitely like the idea of having an iterator approach, I agree that this is better to allow more control over intermediate states etc.

For this particular implementation, there are some suggestions for organizational changes that I think make a slightly more robust (and type-stable) framework though.

The first thing has to do with the organization of the structs. To be more in line with the rest, and also to keep things well-separated, I think it would be nice to distinguish a bit more the iterator, the algorithm and the state.

What I mean with this is something like this:

struct SimpleUpdate <: MPSKit.Algorithm
    maxiter
    dt
    trscheme
    ...
end

struct SUIterator
    alg::SimpleUpdate
    operator
    state
end

For more inspiration, see e.g. https://github.com/QuantumKitHub/MPSKit.jl/blob/main/src/states/ortho.jl, or even the current implementation of VUMPS.

I am trying to formalize these notions into a package, which additionally has the purpose of generalizing the stopping criteria and the logging, but that is still a bit WIP. Linking it here though: https://github.com/JuliaManifolds/AlgorithmsInterface.jl
There already is some information in the discussions of that repository, which I think can be interesting to go over. In the long run, I would definitely like to switch over to that style anyways.


With this in mind, I also don't think I like not implementing timestep, which should really be equivalent to iterate(iterator, state) up to some reshuffling of algorithms.
I don't particularly mind whether we implement the first in terms of the second or the other way around, but it seems like it could be useful to have both.


Somewhat related, as the cost of a single SU step is lower than the typical CTMRG ones, it would be nice to ensure that we guarantee some form of type stability. This would mean making sure that iterate is inferrable, which probably means putting some type parameters into the different structs.


Do let me know what you think about most of these points, I'm happy to discuss further and obtain more viewpoints on this.


As a somewhat unrelated note, may I please ask for the future to try and avoid having both formatting/naming changes and algorithm changes in the same PR? I'm not against reformatting or changing names, and I understand that it is inconvenient to separate everything out, but it makes reviewing PRs a lot more involved if 80% of the lines that are showed as "changed" don't have anything to do with the actual logic changes.

@Yue-Zhengyuan
Copy link
Member Author

@lkdvos Is it a good idea to first let copilot generate a draft based on your suggestion? (I haven't tried copilot yet by myself)

@lkdvos
Copy link
Member

lkdvos commented Nov 3, 2025

I have no issue with you trying this out, we can always ignore the changes it proposes if it turns out to not work the way we like

@Yue-Zhengyuan
Copy link
Member Author

I may describe an additional complication in full update that iterate may need to handle.

In SU, apart form convergence checking, all iterations are the same. But in (fast) full update, the CTMRGEnv is only carefully re-converged (with leading_boundary) every n iterations of FU, regardless of whether convergence checking is turned on. So some iterations have this additional step compared to the rest. Unlike convergence checking, this re-converging is essential to the result of FU that possibly should not be moved to an upper level.

My current design is that each iterate of FU actually performs n iterations + re-converging CTMRGEnv at the end, but honestly I don't quite like it...

@lkdvos
Copy link
Member

lkdvos commented Nov 10, 2025

For the logging:

LoggingExtras.withlevel(f; it.verbosity)

runs f() -> y in a scope with the modified logger, and returns the output y of that function.
The syntax

LoggingExtras.withlevel(; it.verbosity) do
# body
end

is syntactically equivalent to

function f()
# body
end

LoggingExtras.withlevel(f; it.verbosity)

Therefore, if you don't return anything from within the do block of the withlevel, this follows the same syntax rules as regular function definitions and anonymous functions in Julia: the result of the last executed statement is returned. Since this is often a bit subtle to actually figure out what is being returned, we tend to favor explicitly adding the return statement, even though this is technically not required by the language: See also this docs page.


I'm happy to exclude a bunch of these functions from the docs if you like, but I'd rather do that by restricting the docs, while keeping the docstrings. I find these actually very useful, and fundamentally disagree with the idea that a private internal function cannot have a docstring. In an organization where multiple people work on different parts of the code, it seems to me almost required to be able to have this.


For the FU, it is a bit harder without having an actual look at it, but perhaps something like this could work?

function Base.iterate(it::FUIterator, state = it.state)
    if state.iteration % it.environment_frequency == 0
        leading_boundary
    end
    
    # actual iterator implementation
end

@Yue-Zhengyuan
Copy link
Member Author

Yue-Zhengyuan commented Nov 11, 2025

Latest changes:

  • Docstrings are restored.
  • Logging and convergence checking of simple update are moved to the "auto-pilot" function time_evolve. The functions iterate and timestep for finer control no longer do these tasks.
  • verbosity option is removed to make output messages completely irrelevant to TimeEvolver.

@Yue-Zhengyuan Yue-Zhengyuan requested a review from lkdvos November 11, 2025 12:30
@Yue-Zhengyuan Yue-Zhengyuan self-assigned this Nov 12, 2025
@Yue-Zhengyuan
Copy link
Member Author

@lkdvos Ready for (hopefully) final review

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.

I left a couple small comments, but otherwise I think this looks more or less ready to merge for me!

@Yue-Zhengyuan Yue-Zhengyuan requested a review from lkdvos November 15, 2025 02:50
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.

For me this looks good to go, great work!

@Yue-Zhengyuan Yue-Zhengyuan merged commit 552d6ad into QuantumKitHub:master Nov 15, 2025
51 checks passed
@pbrehmer
Copy link
Collaborator

Thanks for the effort, this is really nice! I can regenerate the docs examples tomorrow (unless someone does it before me :)) so that we can tag a new release.

@Yue-Zhengyuan Yue-Zhengyuan deleted the timeevol-interface branch November 16, 2025 00:12
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.

3 participants