Skip to content

Commit 5e2f4b4

Browse files
fixed mcwf/lindblad ordering not matching tjm (#481)
## Description This PR fixes a problem where the MCWF/Lindblad solvers had a different qubit ordering due to the Kronecker product. This has been fixed so that the qubit order remains consistent for all methods across the whole library. ## Checklist <!--- This checklist serves as a reminder of a couple of things that ensure your pull request will be merged swiftly. --> - [x] The pull request only contains commits that are focused and relevant to this change. - [x] I have added appropriate tests that cover the new/changed functionality. - [x] I have updated the documentation to reflect these changes. - [x] I have added entries to the changelog for any noteworthy additions, changes, fixes, or removals. - [x] I have added migration instructions to the upgrade guide (if needed). - [x] The changes follow the project's style guidelines and introduce no new warnings. - [x] The changes are fully tested and pass the CI checks. - [x] I have reviewed my own code changes. **If PR contains AI-assisted content:** - [x] I have disclosed the use of AI tools in the PR description as per our [AI Usage Guidelines](https://github.com/munich-quantum-toolkit/yaqs/blob/main/docs/ai_usage.md). - [x] AI-assisted commits include an `Assisted-by: [Model Name] via [Tool Name]` footer. - [x] I confirm that I have personally reviewed and understood all AI-generated content, and accept full responsibility for it.
2 parents 5d19a90 + 3d82f89 commit 5e2f4b4

12 files changed

Lines changed: 931 additions & 145 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ This project adheres to [Semantic Versioning], with the exception that minor rel
4848

4949
### Fixed
5050

51+
- fixed mcwf/lindblad ordering not matching tjm ([#481]) ([**@aaronleesander**])
5152
- added regression tests for tjm and mcwf jump probabilities ([#479]) ([**@aaronleesander**])
5253
- fixed NoiseModel factor order when two-site indices are normalized ([#396]) ([**@aleramos119**])
5354
- refactored observable handling in simulator to preserve user order ([#447]) ([**@aaronleesander**])
@@ -144,6 +145,7 @@ _📚 Refer to the [GitHub Release Notes](https://github.com/munich-quantum-tool
144145

145146
<!-- PR links -->
146147

148+
[#481]: https://github.com/munich-quantum-toolkit/yaqs/pull/481
147149
[#476]: https://github.com/munich-quantum-toolkit/yaqs/pull/476
148150
[#479]: https://github.com/munich-quantum-toolkit/yaqs/pull/479
149151
[#477]: https://github.com/munich-quantum-toolkit/yaqs/pull/477

UPGRADING.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,37 @@ For MPS-backed analog and strong-digital runs, `result.runtime_cost`, `result.ma
102102
`result.total_bond` are filled automatically (aligned with `result.times` or the strong-sim layer
103103
grid). MCWF, Lindblad, and weak digital runs leave these as `None`.
104104

105+
### MCWF / Lindblad operator ordering (dense backends)
106+
107+
MCWF (`State(..., representation="vector")`) and Lindblad (`representation="density_matrix"`)
108+
embed jump operators and observables on the full Hilbert space using the same **site-0 LSB**
109+
convention as MPS `to_vec`, Qiskit little-endian circuits, and the TJM (MPO) dissipation path.
110+
Before this release, those dense embeddings used a different Kronecker-product order, so jump
111+
probabilities, observables, and cross-solver comparisons could disagree with TJM even when the
112+
`NoiseModel` definition looked identical.
113+
114+
**What changed:** `_embed_operator_sparse` / `_embed_observable_sparse` (and their dense
115+
counterparts) now delegate to `state_utils.embed_*` helpers instead of building
116+
`left ⊗ op ⊗ right` with reversed tensor-leg order.
117+
118+
**Why it matters:** MCWF, Lindblad, and TJM now agree on how a local operator on `sites=[i]` or
119+
adjacent `sites=[i, i+1]` is placed in the full space. Regression tests compare TJM dissipative
120+
norm loss to MCWF jump probabilities under lowering noise.
121+
122+
**What you need to do:**
123+
124+
- If you only pass standard `NoiseModel` processes (`sites`, built-in names, or matrices authored
125+
for the listed site order), **no change is required**—results may shift slightly because the
126+
previous ordering was incorrect.
127+
- If you hand-built full-space jump operators or compared MCWF/Lindblad outputs to TJM using
128+
custom dense embeddings, rebuild those operators with
129+
`mqt.yaqs.core.data_structures.state_utils.embed_one_site_operator`,
130+
`embed_adjacent_two_site_operator`, or `embed_two_site_factors`, or pass the same local matrices
131+
through `NoiseModel` and let the solvers embed them.
132+
- For adjacent two-site **matrix** processes, list sites in ascending order `[i, i+1]` with the
133+
local matrix written for that pair order. If you pass reversed sites `[i+1, i]`, the matrix is
134+
transposed automatically to match the `(i, i+1)` leg order.
135+
105136
### Top-level public API
106137

107138
```python

src/mqt/yaqs/analog/lindblad.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ def preprocess_lindblad(
112112
raise ValueError(msg)
113113

114114
dim = math.prod(resolve_physical_dimensions(num_sites, physical_dimensions))
115+
site_dims = resolve_physical_dimensions(num_sites, physical_dimensions)
115116

116117
if dim > 2**10:
117118
msg = (
@@ -166,7 +167,7 @@ def preprocess_lindblad(
166167
strength = process["strength"]
167168
if strength <= 0:
168169
continue
169-
op_full = _embed_operator_sparse(process, num_sites)
170+
op_full = _embed_operator_sparse(process, num_sites, physical_dimensions=site_dims)
170171
jump_ops.append(np.sqrt(strength) * op_full)
171172

172173
is_unitary = len(jump_ops) == 0
@@ -184,7 +185,7 @@ def preprocess_lindblad(
184185
if obs.gate.name in {"entropy", "schmidt_spectrum"}:
185186
embedded_observables.append(None)
186187
else:
187-
embedded_observables.append(_embed_observable_sparse(obs, num_sites))
188+
embedded_observables.append(_embed_observable_sparse(obs, num_sites, physical_dimensions=site_dims))
188189

189190
# 6. Fixed-step propagator exp(L dt) when vec(rho) fits in memory (time-independent generator).
190191
step_propagator: NDArray[np.complex128] | None = None

src/mqt/yaqs/analog/mcwf.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ def preprocess_mcwf(
117117
raise ValueError(msg)
118118

119119
dim = math.prod(resolve_physical_dimensions(num_sites, physical_dimensions))
120+
site_dims = resolve_physical_dimensions(num_sites, physical_dimensions)
120121

121122
if dim > 2**14:
122123
msg = (
@@ -161,7 +162,7 @@ def preprocess_mcwf(
161162
strength = process["strength"]
162163
if strength <= 0:
163164
continue
164-
op_full = _embed_operator_sparse(process, num_sites)
165+
op_full = _embed_operator_sparse(process, num_sites, physical_dimensions=site_dims)
165166
jump_ops.append(np.sqrt(strength) * op_full)
166167

167168
is_unitary = len(jump_ops) == 0
@@ -190,7 +191,7 @@ def preprocess_mcwf(
190191
if obs.gate.name in {"entropy", "schmidt_spectrum"}:
191192
embedded_observables.append(None)
192193
else:
193-
op = _embed_observable_sparse(obs, num_sites)
194+
op = _embed_observable_sparse(obs, num_sites, physical_dimensions=site_dims)
194195
embedded_observables.append(op)
195196

196197
return MCWFContext(

0 commit comments

Comments
 (0)