//  [python]
def local_encoder_pullback_zt(
        self, sample, timestep, encoder_hidden_states=None, op=None, block_idx=None,
        pca_rank=50, chunk_size=25, min_iter=10, max_iter=100, convergence_threshold=1e-3,
    ):
    '''
    Args
        - sample : zt
        - op : ['down', 'mid', 'up']
        - block_idx : op == down, up : [0,1,2,3], op == mid : [0]
        - pooling : ['pixel-sum', 'channel-sum', 'single-channel', 'multiple-channel']
    Returns
        - h : hidden feature
    '''
    # get h samples
    time_s = time.time()

    # necessary variables
    h_shape = self.get_h(
        sample, timestep=timestep, encoder_hidden_states=encoder_hidden_states,
        op=op, block_idx=block_idx, 
    ).shape

    c_i, w_i, h_i = sample.size(1), sample.size(2), sample.size(3)
    c_o, w_o, h_o = h_shape[1], h_shape[2], h_shape[3]

    a = torch.tensor(0., device=sample.device, dtype=sample.dtype)

    # Algorithm 1
    vT = torch.randn(c_i*w_i*h_i, pca_rank, device=sample.device, dtype=torch.float)
    q, r = torch.qr(vT)
    vT = q
    v = vT.T
    v = v.view(-1, c_i, w_i, h_i)

    time_s = time.time()
    for i in range(max_iter):
        v = v.to(device=sample.device, dtype=sample.dtype)
        v_prev = v.detach().cpu().clone()
        
        u = []
        if v.size(0) // chunk_size != 0:
            v_buffer = list(v.chunk(v.size(0) // chunk_size))
        else:
            v_buffer = [v]

        for vi in v_buffer:
            g = lambda a : self.get_h(
                sample + a*vi, timestep=timestep, encoder_hidden_states=encoder_hidden_states.repeat(vi.size(0), 1, 1),
                op=op, block_idx=block_idx, 
            )

            ui = torch.func.jacfwd(g, argnums=0, has_aux=False, randomness='error')(a)
            u.append(ui.detach().cpu().clone())
        u = torch.cat(u, dim=0)
        u = u.to(sample.device, sample.dtype)
        
        # if v.size(0) == 1:
        #     g = lambda a : self.get_h(
        #         sample + a*v, timestep=timestep, encoder_hidden_states=encoder_hidden_states.repeat(v.size(0), 1, 1),
        #         op=op, block_idx=block_idx, 
        #     )
        #     u = torch.func.jacfwd(g, argnums=0, has_aux=False, randomness='error')(a)
            
        # time_s = time.time()
        # g = lambda a : get_h(x + a*v)
        # u = torch.func.jacfwd(g, argnums=0, has_aux=False, randomness='error')(a)
        # time_e = time.time()
        # print('single vi jacfwd t ==', time_e - time_s)

        g = lambda sample : einsum(
            u, self.get_h(
                sample, timestep=timestep, encoder_hidden_states=encoder_hidden_states,
                op=op, block_idx=block_idx, 
            ), 'b c w h, i c w h -> b'
        )
        v_ = torch.autograd.functional.jacobian(g, sample)
        v_ = v_.view(-1, c_i*w_i*h_i)

        _, s, v = torch.linalg.svd(v_, full_matrices=False)
        v = v.view(-1, c_i, w_i, h_i)
        u = u.view(-1, c_o, w_o, h_o)
        
        convergence = torch.dist(v_prev, v.detach().cpu()).item()
        print(f'power method : {i}-th step convergence : ', convergence)
        
        if torch.allclose(v_prev, v.detach().cpu(), atol=convergence_threshold) and (i > min_iter):
            print('reach convergence threshold : ', convergence)
            break

    u, s, vT = u.view(-1, c_o*w_o*h_o).T.detach(), s.sqrt().detach(), v.view(-1, c_i*w_i*h_i).detach()
    # u, s, vT = u.reshape(-1, c_o*w_o*h_o).T.detach(), s.sqrt().detach(), v.reshape(-1, c_i*w_i*h_i).detach()

    time_e = time.time()
    print('power method runtime ==', time_e - time_s)

    return u, s, vT