Skip to content

Add C++ test coverage for tn_algo and fix the bugs it uncovers (#924) - #927

Closed
IvanaGyro wants to merge 11 commits into
masterfrom
claude/charming-hypatia-emgt59
Closed

Add C++ test coverage for tn_algo and fix the bugs it uncovers (#924)#927
IvanaGyro wants to merge 11 commits into
masterfrom
claude/charming-hypatia-emgt59

Conversation

@IvanaGyro

@IvanaGyro IvanaGyro commented Jun 22, 2026

Copy link
Copy Markdown
Member

Summary

Addresses the "add C++ test coverage" part of #924 for src/tn_algo, and fixes every bug the new tests uncovered (starting with #920). The previously untested C++ implementation of RegularMPS, MPO/RegularMPO, and DMRG is now exercised and corrected.

All 16 new tests pass on both the OpenBLAS (debug-openblas-cpu) and MKL (debug-mkl-cpu) CPU backends, with AddressSanitizer and leak detection enabled.

Tests added (tests/tn_algo_test/)

  • MPS (9): regular construction (regression for the tn_algo.MPS construction crashes in Into_Lortho: Svd reuses "_aux_L" producing duplicated labels #920 crash), metadata/open-boundary shapes, left-orthonormality after Init, normalization, non-uniform physical dimensions, Init_Msector, orthogonality-center sweeps via S_mvleft/S_mvright (norm invariance + right-orthonormality), and clone independence.
  • MPO (4): append/assign/get_op container behaviour, out-of-bound access, and a check that contracting the MPO down to a dense matrix reproduces a transverse-field Ising (TFIM) Hamiltonian assembled independently from Pauli operators.
  • DMRG (3): convergence to the exact ground-state energy (from dense diagonalization of the same TFIM Hamiltonian) for both sweep() and sweepv2(), at two field strengths.

A shared tfim_mpo.hpp helper builds the TFIM MPO tensors, the dense reference Hamiltonian, and its exact ground-state energy.

Bugs fixed

  1. RegularMPS::norm was broken for any virtual bond dimension > 1 (mismatched-dimension Tensordot); rewrote it as an explicit <ψ|ψ> transfer-matrix contraction that works for any bond dimension.
  2. tn_algo.MPS construction crashes in Into_Lortho: Svd reuses "_aux_L" producing duplicated labels #920 label collision in Into_Lortho (and the same class of bug in S_mvright/S_mvleft): linalg::Svd's fixed _aux_L/_aux_R bond labels survived into the next decomposition and tripped set_labels' duplicate check. Fixed by restoring the canonical per-site labels {2k, 2k+1, 2k+2} after each Svd. Also fixed a separate no-op where the const set_rowrank (returns a copy) was used instead of in-place set_rowrank_.
  3. DMRG was written against older cytnx APIs and crashed before completing a sweep: Launch(true) selected an unsupported Fermion network; the static Network::Contract().Launch() segfaults (unallocated contraction tree); the ; rowrank syntax in network strings is no longer parsed; set_rowrank no-ops; and Svd_truncate's _aux_* labels collided across sites. All fixed; DMRG now converges.

Notes / scope

  • Test the C++ tn_algo API and redesign its Python bindings #924 mentions SymMPS.cpp, but there is no SymMPS anywhere in the codebase — the C++ MPS only has RegularMPS and iMPS implementations. Coverage targets what actually exists.
  • The Python-binding redesign (part 3 of Test the C++ tn_algo API and redesign its Python bindings #924) is not included here; this PR is the C++ testing + bug-fix groundwork that part 3 depends on.
  • The full test suite has pre-existing, unrelated AddressSanitizer aborts in UniTensor::_Load (SVD-family data-loading tests) and OptimalTreeSolver (SearchTreeTest); these reproduce on the unmodified tree and are outside tn_algo, so they are left untouched.

🤖 Generated with Claude Code

IvanaGyro and others added 4 commits June 21, 2026 19:47
RegularMPS::norm() built the <psi|psi> contraction with a hard-coded
label scheme that only closed the trivial dim-1 boundary bonds: the first
site was contracted with its conjugate over the physical and right virtual
legs, and every subsequent site relabelled the running environment to "2"
and contracted it against the new site's left virtual leg. Those two legs
share the label "2" but have different dimensions whenever the virtual bond
is larger than one, so Tensordot aborts with a dimension-mismatch error.
The routine therefore only worked for a product state (chi == 1).

Rebuild the left environment transfer matrix explicitly instead: seed L as
the 1x1 identity on the trivial left boundary, then for each rank-3 site
[left, phys, right] contract the ket left leg onto L's "a", the bra left leg
onto "b", and the two physical legs onto each other, leaving the next
virtual bond open as the new {a, b}. The final 1x1 environment's trace is
<psi|psi>. This contracts correctly for any virtual bond dimension and is
independent of the site tensors' stored labels.

Co-authored-by: Claude <noreply@anthropic.com>
Constructing a regular MPS runs Into_Lortho(), which left-orthogonalises
the chain with a sweep of linalg::Svd calls. linalg::Svd always names the
new singular-value bond with the fixed labels "_aux_L"/"_aux_R". After the
S*vT factor is contracted into the next site, that site's virtual leg keeps
the generic "_aux_L" label, which then collides with the "_aux_L" the next
Svd reintroduces for its own bond; set_labels rejects the duplicate and the
construction aborts (issue #920). RegularMPS was effectively unusable for
the regular case as a result.

Restore the canonical per-site labels {2k, 2k+1, 2k+2} (the labels Init
assigns, where neighbours share their virtual-bond label) after every Svd in
Into_Lortho, S_mvright and S_mvleft. Overwriting the bond labels each step
stops the generic "_aux_*" labels from surviving into the next decomposition
and keeps the chain in a predictable labelling.

S_mvright/S_mvleft additionally never changed the orthogonality center: they
called the const UniTensor::set_rowrank, which returns a re-rowranked copy
and was discarded, instead of the in-place set_rowrank_. The subsequent Svd
then split the site at the wrong rowrank. Switch both to set_rowrank_.

Co-authored-by: Claude <noreply@anthropic.com>
The DMRG driver was written against older cytnx APIs and crashed before
completing a single sweep. Update it so DMRG runs and converges:

- Network::Launch no longer takes an "optimal order" bool; its argument now
  selects the network type, where 1 means Fermion. Every anet.Launch(true)
  was therefore selecting an unsupported network type and aborting. Use
  Launch() (regular network) throughout.

- The static Network::Contract(...).Launch() helper used in initialize()
  leaves the contraction tree's base nodes unallocated and segfaults on
  Launch. Replace it with the FromString + PutUniTensors pattern already used
  for the same L-A-M-A* environment elsewhere in this file.

- The current RegularNetwork parser splits per-tensor label lines on "," only
  and no longer accepts the ";" rowrank marker, so it folded "-4;1" into a
  single label and rejected the network. Drop the ";" markers from the tensor
  lines (keeping them on TOUT, where they are still honoured).

- set_rowrank is const and returns a copy; the discarded calls left psi and
  the boundary tensors at the wrong rowrank for the following Svd. Switch the
  in-place uses to set_rowrank_.

- Each Svd_truncate reintroduces the fixed "_aux_L"/"_aux_R" bond labels,
  which otherwise survive into the next site's decomposition and collide.
  Restore the canonical per-site labels {2k, 2k+1, 2k+2} after every
  decomposition (matching the standalone DMRG reference under example/DMRG).

Co-authored-by: Claude <noreply@anthropic.com>
The C++ implementation under src/tn_algo had no test coverage. Add a
tn_algo_test suite exercising the public API:

- MPS: regular construction (regression for the #920 label-collision crash),
  metadata and open-boundary shapes, left-orthonormality after Init,
  normalization, non-uniform physical dimensions, Init_Msector, orthogonality
  center sweeps via S_mvleft/S_mvright (norm invariance and right-
  orthonormality), and clone independence.

- MPO: append/assign/get_op container behaviour, out-of-bound access, and a
  check that contracting the matrix-product operator down to a dense matrix
  reproduces a transverse-field Ising Hamiltonian assembled independently
  from Pauli operators.

- DMRG: convergence to the exact ground-state energy (from dense
  diagonalization of the same TFIM Hamiltonian) for both sweep() and
  sweepv2(), at two field strengths.

A shared tfim_mpo.hpp helper builds the TFIM MPO tensors, the dense
reference Hamiltonian, and its exact ground-state energy.

Co-authored-by: Claude <noreply@anthropic.com>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request resolves issue #920 by ensuring canonical per-site labels are restored after SVD decompositions in both DMRG and RegularMPS, preventing label collisions during sweeps. It also refactors RegularMPS::norm() for a cleaner, site-by-site accumulation of the left environment, simplifies network contractions in DMRG initialization, and updates set_rowrank calls to set_rowrank_. Additionally, a comprehensive suite of unit tests for MPS, MPO, and DMRG has been added. The review feedback suggests improving GPU/CUDA compatibility in RegularMPS::norm() by matching the device of the environment tensor to the MPS tensors, and resolving potential narrowing conversion warnings in the test helper by using cytnx_uint64 instead of int.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread src/tn_algo/RegularMPS.cpp Outdated
Comment thread tests/tn_algo_test/tfim_mpo.hpp Outdated
IvanaGyro and others added 2 commits June 22, 2026 02:32
The rewritten norm() seeded the 1x1 identity environment with a default
(CPU) tensor, so contracting it against MPS tensors living on a GPU would
trigger cross-device errors or implicit host/device copies. Create the seed
on the same device as the site tensors, falling back to CPU when the chain
is empty.

Co-authored-by: Claude <noreply@anthropic.com>
Tensor::at takes a std::vector<cytnx_uint64>, so brace-initializing the
index list from int loop variables is a narrowing conversion that some
compilers reject. Use cytnx_uint64 for the index variables in the TfimW
helper.

Co-authored-by: Claude <noreply@anthropic.com>
@IvanaGyro
IvanaGyro marked this pull request as draft June 22, 2026 02:37
@codecov

codecov Bot commented Jun 22, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 5.47945% with 69 lines in your changes missing coverage. Please review.
✅ Project coverage is 30.65%. Comparing base (87c9fd5) to head (a050f63).
⚠️ Report is 57 commits behind head on master.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/tn_algo/DMRG.cpp 0.00% 0 Missing and 49 partials ⚠️
src/tn_algo/RegularMPS.cpp 14.28% 0 Missing and 18 partials ⚠️
include/tn_algo/MPS.hpp 33.33% 0 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #927      +/-   ##
==========================================
+ Coverage   29.49%   30.65%   +1.16%     
==========================================
  Files         241      241              
  Lines       35524    35517       -7     
  Branches    14780    14775       -5     
==========================================
+ Hits        10477    10888     +411     
+ Misses      17791    17028     -763     
- Partials     7256     7601     +345     
Flag Coverage Δ
cpp 30.28% <5.47%> (+1.19%) ⬆️
python 52.45% <ø> (-0.27%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Components Coverage Δ
C++ backend 32.07% <5.47%> (+1.33%) ⬆️
Python bindings 17.06% <ø> (-0.03%) ⬇️
Python package 52.45% <ø> (-0.27%) ⬇️
Files with missing lines Coverage Δ
include/tn_algo/MPS.hpp 51.64% <33.33%> (+51.64%) ⬆️
src/tn_algo/RegularMPS.cpp 46.45% <14.28%> (+46.45%) ⬆️
src/tn_algo/DMRG.cpp 30.76% <0.00%> (+30.76%) ⬆️

... and 30 files with indirect coverage changes


Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 87c9fd5...a050f63. Read the comment docs.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@ianmccul

Copy link
Copy Markdown
Collaborator

If there is no C++ user prepared to rewrite and maintain the tn_algo area then it should be removed. Cytnx public API is Python. The Python versions of these algorithms area sufficiently fast that there is no need for C++ here. Developer time would be much better spent fixing code that users are likely to use, such as the Python examples.

IvanaGyro and others added 2 commits June 22, 2026 07:50
Address maintainer review on the tn_algo bug-fix changes:

- Underscore-prefix the labels cytnx assigns to MPS site tensors. Init and
  Init_Msector now label every site (including site 0) {"_<2k>", "_<2k+1>",
  "_<2k+2>"} so the internal bond/physical labels cannot collide with labels a
  user might place on the exposed data() tensors.

- Replace the two identical per-file canonical-label helpers with a single
  shared CanonicalSiteLabels() in tn_algo/MPS.hpp, used by RegularMPS
  (Into_Lortho, S_mvright, S_mvleft) and DMRG. The name follows the Google
  style for free functions and takes the index by value.

- In RegularMPS::norm, use explicit UniTensor types instead of auto and
  annotate the UniTensor(Tensor, /*is_diag=*/false, /*rowrank=*/1) arguments.

- In DMRG, drop a leftover translation comment, construct the environment
  Network with an explicit type instead of auto, and read the local Lanczos
  input back into an explicit Tensor instead of auto.

Co-authored-by: Claude <noreply@anthropic.com>
- Move the per-file test fixtures into anonymous namespaces instead of named
  ones, and drop the unused `using namespace testing;`.
- Replace abbreviated identifiers (N, J, h, chi, Dw, ...) with descriptive
  names (num_sites, coupling, field, bond_dim, ...) across the tests and the
  shared TFIM helper, and rename the helper entry points accordingly
  (MakeMpoTensor, DenseHamiltonian).

Co-authored-by: Claude <noreply@anthropic.com>
IvanaGyro and others added 2 commits June 22, 2026 08:05
Google C++ style guide disallows using-directives. Drop the file-scope
using namespace cytnx; and using namespace cytnx::tn_algo; in
MPS_test.cpp, MPO_test.cpp, DMRG_test.cpp, and the function-local
using namespace cytnx; in tfim_mpo.hpp, qualifying every Tensor,
UniTensor, MPS, MPO, DMRG, and cytnx_uint64/cytnx_int64 use at the
call site instead.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
DMRG::sweep() and DMRG::sweepv2() carry excited-state penalty branches
gated on this->ortho_mps being non-empty (the loops over hLRs in
initialize(), sweep(), and sweepv2()). The existing ground-state-only
tests never populate ortho_mps, so those branches were unexercised.

Add TfimTest::SortedEigenvalues/ExactFirstExcitedEnergy and two DMRG
tests that compute a ground state, then feed it back in as ortho_mps
to drive both sweep paths toward the first excited state, checked
against the exact second-lowest eigenvalue of the dense Hamiltonian.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@IvanaGyro

Copy link
Copy Markdown
Member Author

If there is no C++ user prepared to rewrite and maintain the tn_algo area then it should be removed. Cytnx public API is Python. The Python versions of these algorithms area sufficiently fast that there is no need for C++ here. Developer time would be much better spent fixing code that users are likely to use, such as the Python examples.

Agree, we can let bot migrate them to python after the PR for stub (#912) is merged.

DMRG::sweep() and DMRG::sweepv2() guard their per-step progress output
behind `if (verbose)`. Every existing tn_algo test passes verbose=false,
so those logging branches were never executed.

Add DMRG.VerboseLoggingRuns, which drives a small TFIM system with
verbose=true through both sweep() and sweepv2(). std::cout is redirected
to a local buffer for the duration so the progress logging does not
pollute the test output, and the test asserts the buffer is non-empty to
confirm the logging path ran.

Co-authored-by: Claude <noreply@anthropic.com>
@IvanaGyro

Copy link
Copy Markdown
Member Author

C++ algorithm API was removed with #931.

@IvanaGyro IvanaGyro closed this Jul 9, 2026
@IvanaGyro
IvanaGyro deleted the claude/charming-hypatia-emgt59 branch July 30, 2026 17:56
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.

2 participants