Skip to content

Commit 2042b5a

Browse files
feat(decomp): add LQ decomposition (for MPS right canonicalization)
Implement LQ decomposition as the complement to existing QR decomposition: - T = L * Q where Q is right-orthonormal (Q * Q† = I) - L[left_indices, m(OUT)] * Q[m(IN), right_indices] Key features: - Consistent index direction convention with QR (L's last OUT, Q's first IN) - Divergence conservation: Div(L) + Div(Q) = Div(T) Implementation uses mathematical relationship: LQ(T) = Dag(QR(Dag(T_transposed))) Use case: MPS right canonicalization without Transpose workarounds Before: Transpose -> QR -> Transpose (hack) After: LQ(&tensor, rdims, qndiv, &L, &Q)
1 parent f5b8246 commit 2042b5a

File tree

4 files changed

+612
-0
lines changed

4 files changed

+612
-0
lines changed
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
// SPDX-License-Identifier: LGPL-3.0-only
2+
/*
3+
* Author: TensorToolkit Contributors
4+
* Creation Date: 2026-01-04
5+
*
6+
* Description: QuantumLiquids/tensor project. LQ decomposition for a symmetric QLTensor.
7+
*/
8+
9+
/**
10+
@file ten_lq.h
11+
@brief LQ decomposition for a symmetric QLTensor.
12+
13+
LQ decomposition factorizes a tensor T into L * Q:
14+
T[i0, ..., i_{n-k-1}, i_{n-k}, ..., i_{n-1}]
15+
= L[i0, ..., i_{n-k-1}, m(OUT)] * Q[m(IN), i_{n-k}, ..., i_{n-1}]
16+
17+
where:
18+
- rdims = k is the number of right indices that Q retains
19+
- Q is right-orthonormal (rows form an orthonormal set): Q * Q† = I
20+
- L's last index has direction OUT, Q's first index has direction IN
21+
- These two middle indices are InverseIndex of each other
22+
23+
This is complementary to QR decomposition which gives left-orthonormal Q:
24+
- QR: T = Q * R, where Q is left-orthonormal (columns orthonormal): Q† * Q = I
25+
- LQ: T = L * Q, where Q is right-orthonormal (rows orthonormal): Q * Q† = I
26+
27+
Implementation uses the mathematical relationship:
28+
If T† = Q' * R' (QR decomposition), then T = R'† * Q'† = L * Q
29+
*/
30+
#ifndef QLTEN_TENSOR_MANIPULATION_TEN_DECOMP_TEN_LQ_H
31+
#define QLTEN_TENSOR_MANIPULATION_TEN_DECOMP_TEN_LQ_H
32+
33+
#include "qlten/qltensor_all.h"
34+
#include "qlten/tensor_manipulation/basic_operations.h" // Dag
35+
#include "qlten/tensor_manipulation/ten_decomp/ten_qr.h" // QR
36+
37+
#include <vector>
38+
#include <numeric> // iota
39+
40+
#ifdef Release
41+
#define NDEBUG
42+
#endif
43+
#include <cassert>
44+
45+
46+
namespace qlten {
47+
48+
49+
/**
50+
Generate transpose axes to move the last rdims indices to the front.
51+
52+
@param rank Total number of indices.
53+
@param rdims Number of right indices to move to front.
54+
@return Transpose axes vector.
55+
*/
56+
inline std::vector<size_t> GenLQPreTransposeAxes(
57+
const size_t rank,
58+
const size_t rdims
59+
) {
60+
std::vector<size_t> axes(rank);
61+
size_t ldims = rank - rdims;
62+
// Put right indices first: [ldims, ldims+1, ..., rank-1, 0, 1, ..., ldims-1]
63+
for (size_t i = 0; i < rdims; ++i) {
64+
axes[i] = ldims + i;
65+
}
66+
for (size_t i = 0; i < ldims; ++i) {
67+
axes[rdims + i] = i;
68+
}
69+
return axes;
70+
}
71+
72+
73+
/**
74+
Generate transpose axes to restore Q from QR result.
75+
Q' has shape [right_indices..., m], need to get [m, right_indices...]
76+
77+
@param rdims Number of right indices (excluding m).
78+
@return Transpose axes vector.
79+
*/
80+
inline std::vector<size_t> GenLQQTransposeAxes(const size_t rdims) {
81+
std::vector<size_t> axes(rdims + 1);
82+
// Move last index (m) to first position
83+
axes[0] = rdims;
84+
for (size_t i = 0; i < rdims; ++i) {
85+
axes[i + 1] = i;
86+
}
87+
return axes;
88+
}
89+
90+
91+
/**
92+
Generate transpose axes to restore L from QR result.
93+
L' has shape [m, left_indices...], need to get [left_indices..., m]
94+
95+
@param ldims Number of left indices (excluding m).
96+
@return Transpose axes vector.
97+
*/
98+
inline std::vector<size_t> GenLQLTransposeAxes(const size_t ldims) {
99+
std::vector<size_t> axes(ldims + 1);
100+
// Move first index (m) to last position
101+
for (size_t i = 0; i < ldims; ++i) {
102+
axes[i] = i + 1;
103+
}
104+
axes[ldims] = 0;
105+
return axes;
106+
}
107+
108+
109+
/**
110+
LQ decomposition for a QLTensor.
111+
112+
Decomposes tensor T into L * Q where Q is right-orthonormal:
113+
T[i0, ..., i_{n-k-1}, i_{n-k}, ..., i_{n-1}]
114+
= L[i0, ..., i_{n-k-1}, m(OUT)] * Q[m(IN), i_{n-k}, ..., i_{n-1}]
115+
116+
@tparam TenElemT The element type of the tensors.
117+
@tparam QNT The quantum number type of the tensors.
118+
119+
@param pt A pointer to the to-be LQ decomposed tensor \f$ T \f$. The rank of
120+
\f$ T \f$ should be larger than 1.
121+
@param rdims Number of indices on the right hand side that Q retains.
122+
Must satisfy 0 < rdims < pt->Rank().
123+
@param rqndiv Quantum number divergence of the result \f$ Q \f$ tensor.
124+
@param pl A pointer to result \f$ L \f$ tensor (lower triangular factor).
125+
@param pq A pointer to result \f$ Q \f$ tensor (right-orthonormal factor).
126+
127+
@note The decomposition satisfies:
128+
- Contract(pl, pq, {{ldims}, {0}}) ≈ T (up to numerical precision)
129+
- Contract(pq, Dag(pq), {right_axes, right_axes}) ≈ Identity
130+
- L's last index is OUT, Q's first index is IN (they are InverseIndex)
131+
- Div(L) + Div(Q) = Div(T), and Div(Q) = rqndiv
132+
133+
@par Implementation
134+
Uses the mathematical relationship between LQ and QR:
135+
1. Transpose T to move right indices to front
136+
2. Compute Dag (invert directions, conjugate elements)
137+
3. Apply QR decomposition
138+
4. Dag both results to restore original structure
139+
5. Transpose to final index order
140+
141+
@par Example
142+
@code
143+
using qlten::special_qn::U1QN;
144+
QLTensor<QLTEN_Double, U1QN> T(...); // 3-index tensor [i, j, k]
145+
QLTensor<QLTEN_Double, U1QN> L, Q;
146+
147+
// Decompose T = L * Q where Q has 1 right index
148+
LQ(&T, 1, U1QN(0), &L, &Q);
149+
// Result: L[i, j, m], Q[m, k]
150+
151+
// Verify: L * Q ≈ T
152+
QLTensor<QLTEN_Double, U1QN> T_restored;
153+
Contract(&L, &Q, {{2}, {0}}, &T_restored);
154+
@endcode
155+
*/
156+
template <typename TenElemT, typename QNT>
157+
void LQ(
158+
const QLTensor<TenElemT, QNT> *pt,
159+
const size_t rdims,
160+
const QNT &rqndiv,
161+
QLTensor<TenElemT, QNT> *pl,
162+
QLTensor<TenElemT, QNT> *pq
163+
) {
164+
assert(pt->Rank() >= 2);
165+
assert(rdims > 0 && rdims < pt->Rank());
166+
assert(pl->IsDefault());
167+
assert(pq->IsDefault());
168+
169+
const size_t rank = pt->Rank();
170+
const size_t ldims = rank - rdims;
171+
172+
// Step 1: Transpose T to move right rdims indices to front
173+
// T[i0, ..., i_{ldims-1}, i_{ldims}, ..., i_{n-1}]
174+
// -> T'[i_{ldims}, ..., i_{n-1}, i0, ..., i_{ldims-1}]
175+
auto pre_transpose_axes = GenLQPreTransposeAxes(rank, rdims);
176+
QLTensor<TenElemT, QNT> t_transposed(*pt);
177+
t_transposed.Transpose(pre_transpose_axes);
178+
179+
// Step 2: Compute Dag of transposed tensor
180+
// This inverts all index directions and conjugates elements
181+
auto t_dag = Dag(t_transposed);
182+
183+
// Step 3: QR decomposition
184+
// T†[right†, left†] = Q'[right†, m(OUT)] * R'[m(IN), left†]
185+
//
186+
// The qndiv for QR: since Q_final = Dag(Q'), we have Div(Q) = -Div(Q')
187+
// We want Div(Q) = rqndiv, so Div(Q') = -rqndiv
188+
QLTensor<TenElemT, QNT> q_prime, r_prime;
189+
QNT lqndiv_for_qr = -rqndiv;
190+
QR(&t_dag, rdims, lqndiv_for_qr, &q_prime, &r_prime);
191+
192+
// Step 4: Dag both results to restore original index directions
193+
// Q'' = Dag(Q') has [right_indices, m(IN)]
194+
// L'' = Dag(R') has [m(OUT), left_indices]
195+
auto q_dag = Dag(q_prime);
196+
auto l_dag = Dag(r_prime);
197+
198+
// Step 5: Transpose Q to [m(IN), right_indices]
199+
auto q_transpose_axes = GenLQQTransposeAxes(rdims);
200+
q_dag.Transpose(q_transpose_axes);
201+
*pq = std::move(q_dag);
202+
203+
// Step 6: Transpose L to [left_indices, m(OUT)]
204+
auto l_transpose_axes = GenLQLTransposeAxes(ldims);
205+
l_dag.Transpose(l_transpose_axes);
206+
*pl = std::move(l_dag);
207+
}
208+
209+
210+
} /* qlten */
211+
#endif /* ifndef QLTEN_TENSOR_MANIPULATION_TEN_DECOMP_TEN_LQ_H */
212+

include/qlten/tensor_manipulation_all.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
#include "qlten/tensor_manipulation/ten_ctrct_based_mat_trans.h" // Contract
2121
#include "qlten/tensor_manipulation/ten_decomp/ten_svd.h" // SVD, TensorSVDExecutor
2222
#include "qlten/tensor_manipulation/ten_decomp/ten_qr.h" // QR, TensorQRExecutor
23+
#include "qlten/tensor_manipulation/ten_decomp/ten_lq.h" // LQ
2324
#include "qlten/tensor_manipulation/ten_expand.h" // Expand
2425
#include "qlten/tensor_manipulation/ten_fuse_index.h" // Fuse Index
2526
#include "qlten/tensor_manipulation/ten_block_expand.h"

tests/CMakeLists.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,11 @@ add_unittest(test_ten_qr
225225
"test_tensor_manipulation/test_ten_qr.cc"
226226
"${BLAS_INCLUDE_DIRS}" "" "${MATH_LIB_LINK_FLAGS}"
227227
)
228+
# Test tensor LQ.
229+
add_unittest(test_ten_lq
230+
"test_tensor_manipulation/test_ten_lq.cc"
231+
"${BLAS_INCLUDE_DIRS}" "" "${MATH_LIB_LINK_FLAGS}"
232+
)
228233
# Test tensor index combination.
229234
add_unittest(test_index_combine
230235
"test_tensor_manipulation/test_index_combine.cc"

0 commit comments

Comments
 (0)