ariadnetor_algorithms/dmrg/wrapper.rs
1//! High-level 2-site DMRG entry point.
2//!
3//! [`dmrg_2site`] hides [`DmrgEnvs`] construction and canonical-form
4//! management from non-expert callers, layered on top of the
5//! layout-generic low-level driver [`super::sweep_2site`]. The
6//! low-level driver intentionally rejects MPS not in `Right` or
7//! `Mixed { center: 0 }` form so that a caller-supplied env is never
8//! silently invalidated by an internal canonicalize; this wrapper
9//! defensively clones the input MPS, canonicalizes the clone to
10//! `Mixed { center: 0 }`, builds a fresh [`DmrgEnvs`] against it, and
11//! then invokes the driver. The caller's `psi0` is left untouched.
12//!
13//! Naming convention: `dmrg_2site` is the pure 2-site entry point.
14//! The bare name `dmrg` is intentionally reserved for a future
15//! mixed-strategy entry point (a single call that varies the number
16//! of sites optimized per sweep, mirroring ITensorMPS.jl's `dmrg`
17//! with `nsite` keyword). Pure 1-site DMRG with subspace expansion,
18//! when it lands, will sit alongside as `dmrg_1site`.
19//!
20//! # Generic surface
21//!
22//! `dmrg_2site` is generic over the `Mps<St, L>` chain
23//! (`Mps<St, L>: super::DmrgOps<T>` + `Clone`), so the same entry point
24//! covers both the Dense and BlockSparse / U(1) paths. The `Clone` bounds
25//! (on the storage and layout) are required because the wrapper
26//! defensively clones the input `Mps` before canonicalizing it. Both
27//! concrete chains satisfy `Clone`, so the bound is met for every concrete
28//! chain in the workspace. DMRG is host-pinned in the
29//! CPU-only Stage B scope, so the boundary supplies the [`Host`]
30//! substrate rather than an arbitrary backend.
31//!
32//! # Errors
33//!
34//! See [`DmrgError`] for the full set. Two failure modes are caught
35//! by the wrapper itself before the lower layers can panic or repeat
36//! the check:
37//!
38//! - [`DmrgError::EmptyMps`] — `ariadnetor_mps::canonicalize` asserts
39//! `center < n` and would panic on an empty chain.
40//! - [`DmrgError::LengthMismatch`] — surfaced eagerly so callers see
41//! one failure mode for the same bug regardless of whether the
42//! build or the sweep would have caught it.
43//!
44//! Underlying [`DmrgEnvError`] (e.g. BlockSparse edge-bond validation)
45//! and [`DmrgSweepError`] (param validation, local-eigensolver / SVD
46//! failure, `TooFewSites`, etc.) are forwarded as
47//! [`DmrgError::Env`] / [`DmrgError::Sweep`] respectively; the
48//! `MpsNotRightCanonical` and downstream `LengthMismatch` variants of
49//! [`DmrgSweepError`] are unreachable through the wrapper but kept
50//! visible as defense-in-depth.
51
52use ariadnetor_core::Scalar;
53use ariadnetor_mps::{Mpo, Mps, MpsOps, TensorChain};
54use ariadnetor_tensor::{Host, OpsFor, Storage, StorageFor, TensorLayout};
55
56use super::dispatch::DmrgOps;
57use super::env::{DmrgEnvError, DmrgEnvOps, DmrgEnvs};
58use super::sweep::{DmrgResult, DmrgSweepError, DmrgSweepParams, sweep_2site};
59
60/// Errors raised by [`dmrg_2site`].
61#[derive(Debug, thiserror::Error)]
62#[non_exhaustive]
63pub enum DmrgError {
64 /// Input MPS had zero sites. Surfaced before the wrapper would
65 /// invoke `canonicalize`, which asserts `center < n` and would
66 /// otherwise panic.
67 #[error("input MPS has zero sites")]
68 EmptyMps,
69 /// MPO and MPS chain lengths disagreed.
70 #[error("chain length mismatch: mps = {mps}, mpo = {mpo}")]
71 LengthMismatch {
72 /// Site count reported by the MPS.
73 mps: usize,
74 /// Site count reported by the MPO.
75 mpo: usize,
76 },
77 /// `DmrgEnvs::build` failed (e.g. BlockSparse edge-bond
78 /// validation).
79 #[error("DMRG environment build failed")]
80 Env(#[from] DmrgEnvError),
81 /// The underlying low-level sweep driver returned an error.
82 #[error("DMRG sweep driver failed")]
83 Sweep(#[from] DmrgSweepError),
84}
85
86/// Output of [`dmrg_2site`]: the diagnostic [`DmrgResult`] paired with the
87/// optimized MPS, or a [`DmrgError`].
88type Dmrg2SiteOutput<R, St, L> = Result<(DmrgResult<R>, Mps<St, L>), DmrgError>;
89
90/// Run a 2-site DMRG calculation with caller-friendly defaults for
91/// canonical-form management and environment construction.
92///
93/// The caller's `psi0` is **defensively cloned**; the input MPS is
94/// not mutated regardless of outcome. The clone is canonicalized to
95/// `Mixed { center: 0 }`, paired with a freshly built [`DmrgEnvs`],
96/// and handed to [`super::sweep_2site`]. The optimized MPS is
97/// returned alongside the diagnostic [`DmrgResult`].
98///
99/// The returned MPS ends in `CanonicalForm::Mixed { center: 0 }`
100/// (the orthogonality center sits at site 0 because the driver runs
101/// R→L last); see [`super::sweep_2site`] for details.
102///
103/// # Errors
104///
105/// Returns [`DmrgError::EmptyMps`] when `psi0.len() == 0`,
106/// [`DmrgError::LengthMismatch`] when MPO and MPS lengths disagree,
107/// [`DmrgError::Env`] on environment-build failure, and
108/// [`DmrgError::Sweep`] on driver failure.
109pub fn dmrg_2site<T, St, L>(
110 mpo: &Mpo<St, L>,
111 psi0: &Mps<St, L>,
112 params: &DmrgSweepParams,
113) -> Dmrg2SiteOutput<T::Real, St, L>
114where
115 T: Scalar,
116 T::Real: Scalar<Real = T::Real>,
117 St: Storage + StorageFor<L> + Clone,
118 L: TensorLayout + Clone,
119 Mps<St, L>: DmrgOps<T> + MpsOps<T, Storage = St, Layout = L>,
120 DmrgEnvs<St, L>: DmrgEnvOps<T, Storage = St, Layout = L>,
121 // Host-pinned: the host backend supplies every kernel, so it must declare
122 // capability for this chain's storage (satisfied by Dense / BlockSparse).
123 Host: OpsFor<St>,
124{
125 if psi0.len() == 0 {
126 return Err(DmrgError::EmptyMps);
127 }
128 if mpo.len() != psi0.len() {
129 return Err(DmrgError::LengthMismatch {
130 mps: psi0.len(),
131 mpo: mpo.len(),
132 });
133 }
134
135 let mut psi = psi0.clone();
136 psi.canonicalize(Host::shared().as_ref(), 0);
137 let mut envs = DmrgEnvs::<St, L>::build::<T>(&psi, mpo)?;
138 let result = sweep_2site::<T, St, L>(&mut envs, &mut psi, mpo, params)?;
139 Ok((result, psi))
140}