Mismatch between numerical and MPS Schmidt-rank-1 projection overlap

How do I use this algorithm? What does that parameter do?
Post Reply
Jonas_K
Posts: 2
Joined: 13 Oct 2025, 12:02

Mismatch between numerical and MPS Schmidt-rank-1 projection overlap

Post by Jonas_K »

Hello,

I am testing a simple 1D spin-1 chain (L = 4, open BCs) and have encountered trouble projecting my MPS onto the leading Schmidt component (around bond 2).
I have two wavefunctions \(\psi\) and \(\psi_0\).
Conceptually, my code should:
1 ) Time-evolve both states.
2) Project \(\psi\) onto its leading Schmidt component across the middle bond.
3) Compute the overlap between the projected \(\psi\) and \(\psi_0\).
I verified numerically (without TeNPy) that this procedure works as expected.
In TeNPy, I attempt the projection like this:

Python: Select all

Ss = psi.get_SL(i = L // 2)
Ss_new = np.zeros_like(Ss)
Ss_new[0] = 1.0
psi.set_SL(i = L // 2, S = Ss_new)
When I print the singular values, all but the first are zero, as expected. However, when I compute the overlap

Python: Select all

psi.overlap(psi_0)
the result does not match the numerical value / reference.
If I extract the full wavefunction using

Python: Select all

wf_full = get_full_wavefunction(psi.copy())
and perform an SVD on wf_full, I still see the old (unprojected) singular values.

So my question is:
Does modifying psi._S (or using set_SL) not update the underlying tensors? and if not, how then can I update them based on the new singular values?

Below is a code excerpt:

Python: Select all

# Time evolve numeric
H = get_numpy_Hamiltonian( model = model ).real
def time_evo( ts , H , wf0 ):
        egvr, egvk = np.linalg.eigh( H )        
        wf = egvk.conj().T @ wf0
        wf_time_evolution = lambda t: egvk @ ( np.exp( - 1j * egvr * t ) *  wf )
        wf_ts = [ wf_time_evolution(t) for t in ts ]
        return wf_ts

def Eng_time_evo( dt , N_steps ):
    TDVPEng.evolve( N_steps = N_steps , dt = dt )
    TDVPEng_0.evolve( N_steps = N_steps , dt = dt )

def projection( psi ):
    Ss = psi.get_SL( i = L // 2 )
    Ss_new = np.zeros_like( a = Ss )
    Ss_new[0] = 1.
    psi.set_SL( i = L // 2 , S = Ss_new )

### PARAMETERS ###
dt = 0.1
N_steps = 20
ts = [ N_steps *  dt ]

### TIME EVOLUTION AND PROJECTION ###
# TENPY # 
Eng_time_evo( dt = dt , N_steps = N_steps ) 
print('Singular values, before p:' , psi._S)
projection( psi = psi )
print('Singular values, after p:' , psi._S)
wf_full = get_full_wavefunction( psi = psi.copy() )


# NUMERIC #
wf_t = time_evo( ts = ts , H = H , wf0 = psi_init )[0]
wf_0_t = time_evo( ts = ts , H = H , wf0 = psi_0_init )[0]
psi_p_schmidt = wf_t.reshape( ( dim_A , dim_B ) )
U , s , Vh = np.linalg.svd( psi_p_schmidt , full_matrices = False )
print('Numeric, acurate, singular values', s)
psi_proj = np.kron( U[ : , 0 ] , Vh[ 0 , : ] )

### OVERLAPS ###
# TENPY #
ov_tenpy = psi.overlap( psi_0 )
print( 'ov_tenpy' , round( ov_tenpy , 3 ) )

# TENPY NUMERIC #
ov_tenpy_numeric = np.abs( np.vdot( wf_full , wf_0_t ) )
print( 'ov_tenpy_numeric' , round( ov_tenpy_numeric , 3 ) )

# NUMERIC #
ov_numeric = np.vdot( psi_proj , wf_0_t )
print( 'ov_numeric' , round( ov_numeric , 3 ) )
Jonas_K
Posts: 2
Joined: 13 Oct 2025, 12:02

Re: Mismatch between numerical and MPS Schmidt-rank-1 projection overlap

Post by Jonas_K »

I ended up getting this to work by altering the tensor instead of the singular values at bond i.
That is by:

Python: Select all

 
= psi.get_B( idx_cut )
legs = B.legs
labels = B.get_leg_labels()
new_B = npc.zeros( legs , labels = labels , dtype = B.dtype )
new_B[0, :, :] = B[0, :, :]
psi.set_B(idx_cut, new_B)
psi.canonical_form()
How exactly one is supposed to use the set._S function I am not sure (and I would be interested in hearing the idea), but an answer is not strictly necessary now:)
User avatar
Johannes
Site Admin
Posts: 473
Joined: 21 Jul 2018, 12:52
Location: TU Munich

Re: Mismatch between numerical and MPS Schmidt-rank-1 projection overlap

Post by Johannes »

Indeed, psi.set_SL(...) and psi.set_SR(...) just updates the singular values, not the underlying B tensors (as you can easily see by checking the code...), so this is expected. It is meant to be a more low-level function and used by other MPS methods where you additionally also call psi.set_B(...) to update the state... If you directly modify entries of tensors or singular values, we expect you to know what you do and how things work in TeNPy, and keep things in a consistent state.

What you do is a projection that cannot be written as a unitary, which changes singular values on all bonds. That means to get the MPS back into a consistent canonical from, you need to change the singular values and A/B tensors on *all* sites. This is why the final psi.canonical_form() in your last code is vital in this case, as that does a left-right sweep through the whole MPS, updating the B and S on all the sites consistently.
Post Reply