Skip to content

parallelized MPO equivalence checker#448

Merged
aaronleesander merged 28 commits into
mainfrom
ec-update
Jun 2, 2026
Merged

parallelized MPO equivalence checker#448
aaronleesander merged 28 commits into
mainfrom
ec-update

Conversation

@aaronleesander
Copy link
Copy Markdown
Member

Description

This PR parallelizes the MPO equivalence checking method such that disjoint temporal zones are applied at once.
This significantly speeds up equivalence checking for larger systems, although this method scales favorably even in serial.

Parallelization only kicks in above 12 qubits since the thread overhead below this is too high to justify using it based on experiments.

Checklist

  • The pull request only contains commits that are focused and relevant to this change.
  • I have added appropriate tests that cover the new/changed functionality.
  • I have updated the documentation to reflect these changes.
  • I have added entries to the changelog for any noteworthy additions, changes, fixes, or removals.
  • I have added migration instructions to the upgrade guide (if needed).
  • The changes follow the project's style guidelines and introduce no new warnings.
  • The changes are fully tested and pass the CI checks.
  • I have reviewed my own code changes.

If PR contains AI-assisted content:

  • I have disclosed the use of AI tools in the PR description as per our AI Usage Guidelines.
  • AI-assisted commits include an Assisted-by: [Model Name] via [Tool Name] footer.
  • I confirm that I have personally reviewed and understood all AI-generated content, and accept full responsibility for it.

@aaronleesander aaronleesander added the enhancement New feature or request label Jun 2, 2026
@aaronleesander
Copy link
Copy Markdown
Member Author

@CodeRabbit review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 2, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 2, 2026

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

Release Notes

  • New Features

    • Added matrix-based circuit equivalence backend alongside existing MPO implementation
    • Introduced automatic backend selection ("auto", "matrix", "mpo") with intelligent cutover based on qubit count
    • Enabled parallel execution for MPO equivalence checking with configurable worker threads
  • Documentation

    • Expanded equivalence checking guide with backend selection criteria, parameter documentation, and performance comparisons
    • Updated simulator initialization documentation for clarity on parallel execution settings

Walkthrough

This PR introduces dual-backend quantum circuit equivalence checking (matrix and MPO) with automatic backend selection, parallel MPO execution via thread pools, shared parallel utilities, refactored MPO identity construction, and comprehensive tests and documentation covering backend behavior, parallelism, and auto-selection.

Changes

Dual-backend equivalence checking with parallel MPO

Layer / File(s) Summary
Parallel utilities module extraction and integration
src/mqt/yaqs/parallel_utils.py, src/mqt/yaqs/simulator.py, docs/examples/simulator_initialization.md
New parallel_utils.py centralizes CPU-count resolution (available_cpus), multiprocessing context selection (get_parallel_context), and thread limiting (limit_worker_threads). Simulator imports and uses these shared helpers instead of local implementations, with a backward-compatible _get_parallel_context alias.
DAG utilities and gate scheduling
src/mqt/yaqs/digital/utils/dag_utils.py, src/mqt/yaqs/digital/utils/scheduler_utils.py, tests/digital/utils/test_scheduler_utils.py
extract_temporal_zone builds temporal-zone DAG non-destructively; get_temporal_zone remains mutating. New gate-batching helpers (gates_have_disjoint_sites, partition_disjoint_gate_batches) enable deterministic scheduling of commuting gates for matrix and parallel MPO backends.
Dense matrix equivalence backend
src/mqt/yaqs/digital/utils/matrix_utils.py
New backend constructs composed operator tensor W = U2† U1 via layer-by-layer gate application using einsum contractions (with full-matrix fallback) and checks identity via normalized trace overlap. Handles circuit preprocessing, rejects mid-circuit measurements, strips final measurements.
MPO identity constructor refactoring
src/mqt/yaqs/core/data_structures/mpo.py, tests/analog/test_lindblad.py, tests/analog/test_mcwf.py, tests/core/data_structures/test_mpo.py, tests/digital/utils/test_mpo_utils.py
MPO.identity(...) is now a classmethod returning a new instance; init_identity(...) retains in-place behavior. All test and usage sites updated to use the new classmethod constructor.
MPO parallel checkerboard sweeps
src/mqt/yaqs/digital/utils/mpo_utils.py, tests/digital/utils/test_mpo_utils.py
compute_pair_update centralizes neighbor-pair tensor updates. _apply_layer_sweep routes pair work through a thread pool for parallel execution or falls back to serial. iterate conditionally creates ThreadPoolExecutor for parallel checkerboard sweeps, delegating long-range updates based on gate distances.
Backend-selecting EquivalenceChecker
src/mqt/yaqs/equivalence_checker.py, src/mqt/yaqs/__init__.py
EquivalenceChecker accepts representation ("auto" | "matrix" | "mpo"), matrix_max_qubits, parallel, max_workers, and mp_context. Runtime backend selection via _resolve_representation applies cutover logic for "auto". Result dict includes "representation" field. Exports DEFAULT_MATRIX_MAX_QUBITS and Representation type.
Matrix backend test coverage
tests/digital/utils/test_matrix_utils.py
Comprehensive test suite validates input validation, circuit preprocessing, single/two-qubit equivalence, global-phase invariance, long-range interactions, and operator tensor properties.
Parallel MPO and backend selection tests
tests/digital/utils/test_mpo_utils.py, tests/test_equivalence_checker.py
Tests validate serial/parallel MPO agreement, backend selection cutover logic, matrix_max_qubits override, max_workers input validation, and cross-backend equivalence on parameterized circuit pairs.
Documentation and changelog
CHANGELOG.md, docs/examples/equivalence_checking.md, docs/examples/simulator_initialization.md
CHANGELOG documents the parallelized MPO checker. Equivalence-checking example clarifies global-phase equivalence, documents backend selection, shows parallel configuration, and expands parameter descriptions. Simulator initialization docs reference the refactored parallel utilities.

Sequence Diagram(s)

(Diagrams embedded in the hidden review stack artifact above show backend selection flow, parallel MPO execution, and matrix operator computation path.)

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

The PR introduces substantial new functionality across multiple modules: a new dense-matrix backend with tensor contractions, parallel MPO execution with thread-pool orchestration, refactored parallel utilities, and dual-backend orchestration in EquivalenceChecker. The matrix backend has high logic density (einsum vs embedding trade-offs, DAG layer consumption, operator composition), and the parallel MPO layer adds concurrency patterns with thread pool management. However, changes follow consistent patterns (e.g., all MPO initialization sites follow the same refactoring), which reduces per-file review burden.

Possibly related PRs

  • munich-quantum-toolkit/yaqs#430: Continuation that expands EquivalenceChecker from simple MPO-only to dual-backend support with "auto" | "matrix" | "mpo" modes and new parallel controls.
  • munich-quantum-toolkit/yaqs#337: Overlaps on simulator parallel context selection and thread limiting refactoring, now delegated to the shared parallel_utils module.

Suggested reviewers

  • burgholzer

Poem

🐰 Hark, the equivalence checker blooms in parallel haste,
Two backends dancing: matrices swift and MPOs vast,
Thread pools weave their tapestry with care,
Global phases fade, yet circuits still compare!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'parallelized MPO equivalence checker' directly summarizes the main change: adding parallelization to the MPO equivalence checking method.
Description check ✅ Passed The PR description includes a clear summary of the change, explains the performance benefit and the 12-qubit threshold decision, and confirms completion of all required checklist items including tests, documentation, changelog, and AI disclosure.
Docstring Coverage ✅ Passed Docstring coverage is 92.44% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch ec-update

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 12

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/examples/equivalence_checking.md`:
- Around line 203-205: Update the wording to match the implemented cutoff: state
that MPO parallelization (when parallel=True) is enabled for systems with 13+
qubits and that 12 qubits and below remain serial; replace the phrases "12+
qubits" and "Below 12 qubits" with "13+ qubits" and "12 qubits and below" and
mention `parallel=True`/MPO parallelization accordingly so the docs match the PR
behavior.
- Around line 82-86: The Sphinx {func} cross-reference points to the moved
helper; update or remove it: replace
`{func}`~mqt.yaqs.core.parallel.available_cpus with the correct symbol in its
new module (e.g., the function in parallel_utils, such as
`mqt.yaqs.core.parallel_utils.available_cpus`) if that helper is part of the
public API, or remove the cross-reference and use plain text (e.g., "defaults to
the machine CPU count") if the helper is internal/not documented; ensure the
final docs build and linkcheck pass.

In `@src/mqt/yaqs/digital/utils/matrix_utils.py`:
- Around line 40-58: Update the helper docstrings to Google style by adding an
Args: section for each function (e.g., _circuit_without_measurements,
_identity_operator_tensor, _embed_unitary and the other helpers referenced at
lines 67-79, 97-110, 144-155, 169-174, 194-198, 211-215, 222-226), and keep the
existing one-line summary and Returns: blocks; for example document parameter
names, types and short descriptions (e.g., circuit: QuantumCircuit – the circuit
to copy; num_qubits: int – number of qubits; local: NDArray[np.complex128] –
k-qubit unitary; sites: list[int] – target qubit indices), making each docstring
follow the repository’s Google-style format.
- Around line 267-287: The check_operator_is_identity function incorrectly
rounds np.abs(trace) before normalizing, causing false positives; remove the
pre-comparison rounding and compare the raw normalized absolute trace against
fidelity. Update check_operator_is_identity to compute normalized =
np.abs(trace) / hilbert_dim and return normalized >= fidelity (or equivalently
keep the existing boolean logic but without np.round) so Rz-like near-identity
gates are evaluated correctly.
- Around line 235-255: The function build_composed_operator_tensor currently
assumes both circuits have the same number of qubits but does not enforce it;
add an explicit check comparing circuit1.num_qubits and circuit2.num_qubits at
the start of the function (after reading num_qubits) and raise a ValueError with
a clear message (similar to EquivalenceChecker.check) if they differ so callers
get a deterministic error instead of implicit shape/index failures; locate this
in build_composed_operator_tensor near where num_qubits is obtained and before
any tensor/dag construction.
- Around line 169-184: The current _pop_front_layer_gates silently removes
DAGOpNode measurement nodes (node.op.name == "measure"), which hides mid-circuit
measurements and turns non-unitary circuits into unitary ones; instead, detect
and reject non-final measurements: when you see a "measure" DAGOpNode in
_pop_front_layer_gates, determine whether it is a final measurement (no
subsequent operations use the measured classical/quantum wire) and if not, raise
a clear exception (e.g., ValueError) describing that mid-circuit measurements
are unsupported; only allow removal of truly final measurement nodes (otherwise
keep existing dag.remove_op_node behavior for final measurements) so callers
like equivalence_checker no longer get false positives.

In `@src/mqt/yaqs/digital/utils/mpo_utils.py`:
- Around line 607-609: The ThreadPoolExecutor is currently passed
parallel_worker_init (designed for process pools) which calls process-wide setup
like limit_worker_threads; remove the initializer when creating the thread pool
to avoid repeated process-global changes: modify the ThreadPoolExecutor(...)
call that currently uses initializer=parallel_worker_init (the block that sets
workers = max_workers if max_workers is not None else available_cpus() and then
calls with ThreadPoolExecutor(... ) as pool) to omit the initializer argument
and leave thread-local setup out, or if thread-local initialization is actually
required, replace parallel_worker_init with a new thread-specific initializer
function and use that instead; ensure _consume_dags(pool) still receives the
pool unchanged.

In `@src/mqt/yaqs/equivalence_checker.py`:
- Around line 39-54: Add Google-style docstrings to the new validator and
resolver functions: _validate_representation, _validate_matrix_max_qubits, and
_resolve_representation; each docstring should briefly describe the function
purpose, list parameters (name and type), describe the return type, and document
raised exceptions (e.g., ValueError, TypeError) and conditions under which
they're raised so the API/validation behavior is discoverable and consistent
with the module.

In `@src/mqt/yaqs/parallel_utils.py`:
- Around line 142-150: Add a Google-style "Args" section to the _run_on_executor
docstring describing the parameters: executor (ProcessPoolExecutor used to
submit tasks), tasks (list[TArg] of inputs to process), and worker_fn
(Callable[[TArg], TRes] - the function executed for each task), keeping the
existing Returns section intact; place the Args section directly under the short
description and before Returns in the _run_on_executor docstring so it follows
Google-style formatting.
- Around line 152-160: The results list is initialized with None placeholders
and then filtered before return, which hides missing results and mismatches the
declared return type list[TRes]; update the logic in the function that uses
results/future_to_index/worker_fn to either (A) assert that all entries in
results are non-None after the wait loop (raise an exception or log and fail if
any None remains) and then return results cast to list[TRes], or (B) change the
function signature and return type to list[Optional[TRes]] to reflect possible
partial results; pick one approach and apply it consistently (adjust type hints
and any callers if you choose B, or add an explicit check and clear error
handling if you choose A).

In `@tests/digital/utils/test_equivalence_parallel.py`:
- Around line 78-117: The tests currently set parallel=True on small circuits so
the MPO parallel path in EquivalenceChecker is never exercised; update the test
file to include at least one test that triggers the real parallel code by
creating a circuit with >=13 qubits (e.g., build qc1 and qc2 with 13+ qubits and
a nontrivial gate pattern) and call EquivalenceChecker(representation="mpo",
parallel=True, max_workers=2).check(qc1, qc2) asserting equivalence, or
alternatively mock/override the MPO parallel cutoff used by EquivalenceChecker
(identify and patch the cutoff constant or method inside the EquivalenceChecker
class) for these tests so the current small circuits exercise the parallel
thread-pool code path.

In `@tests/test_equivalence_checker.py`:
- Around line 171-183: The test currently only verifies global-phase equivalence
for representation="matrix" (test_global_phase_equivalence_matrix); update it to
run for both backends by parameterizing the test over representations ["matrix",
"mpo"] (e.g., use pytest.mark.parametrize on a parameter like representation)
and construct EquivalenceChecker(representation=representation, fidelity=1 -
1e-13) inside the test; keep the same circuit setup and assertions
(result["equivalent"] is True and result["representation"] == representation) so
the public contract is validated for both implementations.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4e4c16c6-7a58-40da-a437-af3f363ab359

📥 Commits

Reviewing files that changed from the base of the PR and between b909ec4 and d727cf1.

📒 Files selected for processing (19)
  • CHANGELOG.md
  • docs/examples/equivalence_checking.md
  • docs/examples/simulator_initialization.md
  • src/mqt/yaqs/__init__.py
  • src/mqt/yaqs/core/data_structures/mpo.py
  • src/mqt/yaqs/digital/utils/dag_utils.py
  • src/mqt/yaqs/digital/utils/equivalence_parallel.py
  • src/mqt/yaqs/digital/utils/equivalence_schedule.py
  • src/mqt/yaqs/digital/utils/matrix_utils.py
  • src/mqt/yaqs/digital/utils/mpo_utils.py
  • src/mqt/yaqs/equivalence_checker.py
  • src/mqt/yaqs/parallel_utils.py
  • src/mqt/yaqs/simulator.py
  • tests/analog/test_lindblad.py
  • tests/analog/test_mcwf.py
  • tests/core/data_structures/test_mpo.py
  • tests/digital/utils/test_equivalence_parallel.py
  • tests/digital/utils/test_mpo_utils.py
  • tests/test_equivalence_checker.py

Comment thread docs/examples/equivalence_checking.md Outdated
Comment thread docs/examples/equivalence_checking.md
Comment thread src/mqt/yaqs/digital/utils/matrix_utils.py
Comment thread src/mqt/yaqs/digital/utils/matrix_utils.py
Comment thread src/mqt/yaqs/digital/utils/matrix_utils.py
Comment thread src/mqt/yaqs/equivalence_checker.py Outdated
Comment thread src/mqt/yaqs/parallel_utils.py Outdated
Comment thread src/mqt/yaqs/parallel_utils.py Outdated
Comment thread tests/digital/utils/test_equivalence_parallel.py Outdated
Comment thread tests/test_equivalence_checker.py Outdated
@codecov
Copy link
Copy Markdown

codecov Bot commented Jun 2, 2026

Codecov Report

❌ Patch coverage is 91.75532% with 31 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/mqt/yaqs/digital/utils/matrix_utils.py 91.5% 11 Missing ⚠️
src/mqt/yaqs/parallel_utils.py 84.0% 11 Missing ⚠️
src/mqt/yaqs/equivalence_checker.py 84.6% 8 Missing ⚠️
src/mqt/yaqs/digital/utils/dag_utils.py 75.0% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@aaronleesander
Copy link
Copy Markdown
Member Author

@CodeRabbit review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 2, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (6)
docs/examples/equivalence_checking.md (1)

57-59: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Document the matrix measurement behavior precisely.

This currently says measurements are ignored on the matrix path, but the new matrix tests only permit final measurements to be stripped; mid-circuit measurements are rejected. Please tighten this wording so users do not expect measured circuits to always be accepted.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/examples/equivalence_checking.md` around lines 57 - 59, Update the
wording around the "matrix path" and `fidelity` to state precisely that only
final measurements are stripped before building the unitary U and that
mid-circuit (non-final) measurements are rejected by the matrix tests; keep the
note that barriers are ignored for the matrix path and retain the MPO algorithm
reference (`MPO algorithm`, `sander2025_EquivalenceChecking`) but clarify that
the DAG-based MPO processing only applies when no mid-circuit measurements are
present.
src/mqt/yaqs/parallel_utils.py (2)

84-90: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Document mp_context in Google style.

get_parallel_context() is a new public helper, but its docstring only has a summary. Add Args: and Returns: so the API docs stay consistent with the rest of the Python surface. As per coding guidelines, **/*.py: Use Google-style docstrings in Python code.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/mqt/yaqs/parallel_utils.py` around lines 84 - 90, Add a Google-style
docstring to get_parallel_context(mp_context: MPContext = "auto") describing the
argument and return value: under "Args:" document mp_context (accepted values
like "auto" or a multiprocessing context name and behavior when "auto" chooses
"fork" on linux or "spawn" elsewhere), and under "Returns:" state that the
function returns a multiprocessing.context.BaseContext used for worker
processes; keep the existing one-line summary and ensure types and behavior
match the function implementation and MPContext typing.

103-106: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Overwrite the thread-limit env vars instead of using setdefault.

If the parent process already exported OMP_NUM_THREADS, MKL_NUM_THREADS, or similar, this helper leaves those higher values untouched, so the MPO parallel path can still oversubscribe cores under SLURM/CI shells. That defeats the point of calling limit_worker_threads(1) before creating workers.

Suggested fix
-    for key in THREAD_ENV_VARS:
-        os.environ.setdefault(key, str(n_threads))
-    os.environ.setdefault("OMP_DYNAMIC", "FALSE")
-    os.environ.setdefault("MKL_DYNAMIC", "FALSE")
+    for key in THREAD_ENV_VARS:
+        os.environ[key] = str(n_threads)
+    os.environ["OMP_DYNAMIC"] = "FALSE"
+    os.environ["MKL_DYNAMIC"] = "FALSE"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/mqt/yaqs/parallel_utils.py` around lines 103 - 106, The loop in
parallel_utils.py uses os.environ.setdefault for THREAD_ENV_VARS and for
"OMP_DYNAMIC"/"MKL_DYNAMIC", which leaves existing parent-exported thread limits
in place; change these to overwrite the environment variables (use
os.environ[key] = str(n_threads] for each key in THREAD_ENV_VARS and
os.environ["OMP_DYNAMIC"] = "FALSE", os.environ["MKL_DYNAMIC"] = "FALSE") so
calling limit_worker_threads(1) actually enforces the limits before spawning
workers (refer to THREAD_ENV_VARS and the surrounding code that calls
limit_worker_threads(1) to locate and update the assignments).
src/mqt/yaqs/digital/utils/mpo_utils.py (2)

150-165: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Expand the new helper docstring to Google style.

apply_temporal_zone_gates() takes multiple parameters but only documents the return value. Please add Args: for theta, gates, qubits, and conjugate so this public helper follows the repo standard. As per coding guidelines, **/*.py: Use Google-style docstrings in Python code.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/mqt/yaqs/digital/utils/mpo_utils.py` around lines 150 - 165, The
docstring for apply_temporal_zone_gates is incomplete: update the Google-style
docstring for the function apply_temporal_zone_gates to include an Args: section
documenting theta (NDArray[np.complex128], the local tensor to be updated),
gates (list[BaseGate], the pre-extracted gates to apply), qubits (list[int], the
local qubit indices, using qubits[0] as n), and conjugate (bool, optional,
whether to apply the conjugate), and keep/adjust the existing Returns: block to
describe the returned NDArray; follow the repository's Google-style wording and
type conventions used elsewhere in mpo_utils.py.

273-278: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Always conjugate dag2 zones in nearest-neighbor updates.

Both call sites currently derive apply_conjugate_on_second from bool(gates1). That means a pair with only dag2 work is applied as U2, not U2†, even though apply_long_range_layer() always conjugates the second circuit. The nearest-neighbor MPO path can therefore return a different equivalence result purely because the same gates were scheduled into different zones.

Suggested fix
         work_items.append(
             _PairUpdateWork(
                 site=n,
                 gates1=tuple(gates1),
                 gates2=tuple(gates2),
-                apply_conjugate_on_second=bool(gates1),
+                apply_conjugate_on_second=bool(gates2),
             )
         )
-    apply_conjugate = bool(gates1)
+    apply_conjugate = bool(gates2)
     mpo.tensors[n], mpo.tensors[n + 1] = compute_pair_update(
         mpo.tensors[n],
         mpo.tensors[n + 1],
         gates1,
         gates2,

Also applies to: 336-344

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/mqt/yaqs/digital/utils/mpo_utils.py` around lines 273 - 278, The code
sets apply_conjugate_on_second based on bool(gates1) which causes zones
containing only dag2 to be applied without conjugation; change the logic to
always conjugate the second zone by using apply_conjugate_on_second=bool(gates2)
(replace occurrences in the _PairUpdateWork calls around the shown block and the
other instance at the 336-344 region) so the second circuit segment is
conjugated consistently with apply_long_range_layer().
src/mqt/yaqs/equivalence_checker.py (1)

97-126: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate max_workers eagerly in EquivalenceChecker.__init__

ThreadPoolExecutor requires max_workers > 0; passing 0, a negative integer, or False (treated as 0) raises ValueError when the MPO backend creates the executor. Reject these values during construction so the public API fails fast with a clear error.

Suggested fix
+def _validate_max_workers(max_workers: int | None) -> int | None:
+    """Validate the MPO thread-pool worker cap.
+
+    Args:
+        max_workers: Requested worker count, or ``None`` for automatic sizing.
+
+    Returns:
+        The validated worker count or ``None``.
+
+    Raises:
+        TypeError: If ``max_workers`` is neither ``None`` nor an ``int``.
+        ValueError: If ``max_workers`` is less than 1.
+    """
+    if max_workers is None:
+        return None
+    if isinstance(max_workers, bool) or not isinstance(max_workers, int):
+        msg = f"max_workers must be int or None, got {type(max_workers).__name__}."
+        raise TypeError(msg)
+    if max_workers < 1:
+        msg = f"max_workers must be positive, got {max_workers}."
+        raise ValueError(msg)
+    return max_workers
+
 ...
-        self.max_workers = max_workers
+        self.max_workers = _validate_max_workers(max_workers)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/mqt/yaqs/equivalence_checker.py` around lines 97 - 126,
EquivalenceChecker.__init__ must validate the max_workers parameter eagerly
because ThreadPoolExecutor requires max_workers > 0; add an early check in
__init__ that if max_workers is not None and (not an int or <= 0) then raise a
ValueError with a clear message; ensure the check also rejects False (which
equals 0) so pass-through booleans/invalid types are disallowed, and keep the
existing attributes (self.max_workers, self.parallel, etc.) unchanged otherwise.
♻️ Duplicate comments (1)
docs/examples/equivalence_checking.md (1)

200-202: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

The parallel cutoff wording is still off by one.

The runtime contract in this PR is “above 12 qubits,” but this still documents “12+ qubits” and “Below 12 qubits.” Please change this to 13+ qubits and 12 qubits and below so the docs match the implementation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/examples/equivalence_checking.md` around lines 200 - 202, Update the
wording to match the runtime cutoff: change "12+ qubits" to "13+ qubits" and
change "Below 12 qubits" to "12 qubits and below" in the equivalence_checking.md
text that references the parallel cutoff (the sentence mentioning
`parallel=True` and thread overhead); ensure both occurrences in that paragraph
are updated so the docs reflect the implementation's "above 12 qubits" contract.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@docs/examples/equivalence_checking.md`:
- Around line 57-59: Update the wording around the "matrix path" and `fidelity`
to state precisely that only final measurements are stripped before building the
unitary U and that mid-circuit (non-final) measurements are rejected by the
matrix tests; keep the note that barriers are ignored for the matrix path and
retain the MPO algorithm reference (`MPO algorithm`,
`sander2025_EquivalenceChecking`) but clarify that the DAG-based MPO processing
only applies when no mid-circuit measurements are present.

In `@src/mqt/yaqs/digital/utils/mpo_utils.py`:
- Around line 150-165: The docstring for apply_temporal_zone_gates is
incomplete: update the Google-style docstring for the function
apply_temporal_zone_gates to include an Args: section documenting theta
(NDArray[np.complex128], the local tensor to be updated), gates (list[BaseGate],
the pre-extracted gates to apply), qubits (list[int], the local qubit indices,
using qubits[0] as n), and conjugate (bool, optional, whether to apply the
conjugate), and keep/adjust the existing Returns: block to describe the returned
NDArray; follow the repository's Google-style wording and type conventions used
elsewhere in mpo_utils.py.
- Around line 273-278: The code sets apply_conjugate_on_second based on
bool(gates1) which causes zones containing only dag2 to be applied without
conjugation; change the logic to always conjugate the second zone by using
apply_conjugate_on_second=bool(gates2) (replace occurrences in the
_PairUpdateWork calls around the shown block and the other instance at the
336-344 region) so the second circuit segment is conjugated consistently with
apply_long_range_layer().

In `@src/mqt/yaqs/equivalence_checker.py`:
- Around line 97-126: EquivalenceChecker.__init__ must validate the max_workers
parameter eagerly because ThreadPoolExecutor requires max_workers > 0; add an
early check in __init__ that if max_workers is not None and (not an int or <= 0)
then raise a ValueError with a clear message; ensure the check also rejects
False (which equals 0) so pass-through booleans/invalid types are disallowed,
and keep the existing attributes (self.max_workers, self.parallel, etc.)
unchanged otherwise.

In `@src/mqt/yaqs/parallel_utils.py`:
- Around line 84-90: Add a Google-style docstring to
get_parallel_context(mp_context: MPContext = "auto") describing the argument and
return value: under "Args:" document mp_context (accepted values like "auto" or
a multiprocessing context name and behavior when "auto" chooses "fork" on linux
or "spawn" elsewhere), and under "Returns:" state that the function returns a
multiprocessing.context.BaseContext used for worker processes; keep the existing
one-line summary and ensure types and behavior match the function implementation
and MPContext typing.
- Around line 103-106: The loop in parallel_utils.py uses os.environ.setdefault
for THREAD_ENV_VARS and for "OMP_DYNAMIC"/"MKL_DYNAMIC", which leaves existing
parent-exported thread limits in place; change these to overwrite the
environment variables (use os.environ[key] = str(n_threads] for each key in
THREAD_ENV_VARS and os.environ["OMP_DYNAMIC"] = "FALSE",
os.environ["MKL_DYNAMIC"] = "FALSE") so calling limit_worker_threads(1) actually
enforces the limits before spawning workers (refer to THREAD_ENV_VARS and the
surrounding code that calls limit_worker_threads(1) to locate and update the
assignments).

---

Duplicate comments:
In `@docs/examples/equivalence_checking.md`:
- Around line 200-202: Update the wording to match the runtime cutoff: change
"12+ qubits" to "13+ qubits" and change "Below 12 qubits" to "12 qubits and
below" in the equivalence_checking.md text that references the parallel cutoff
(the sentence mentioning `parallel=True` and thread overhead); ensure both
occurrences in that paragraph are updated so the docs reflect the
implementation's "above 12 qubits" contract.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 94cd86a0-4e02-4f4e-a905-7a9e308cbb45

📥 Commits

Reviewing files that changed from the base of the PR and between d727cf1 and aef7907.

📒 Files selected for processing (8)
  • docs/examples/equivalence_checking.md
  • src/mqt/yaqs/digital/utils/matrix_utils.py
  • src/mqt/yaqs/digital/utils/mpo_utils.py
  • src/mqt/yaqs/equivalence_checker.py
  • src/mqt/yaqs/parallel_utils.py
  • tests/digital/utils/test_equivalence_parallel.py
  • tests/digital/utils/test_matrix_utils.py
  • tests/test_equivalence_checker.py

@aaronleesander
Copy link
Copy Markdown
Member Author

@CodeRabbit review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 2, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/mqt/yaqs/digital/utils/mpo_utils.py (1)

584-585: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Stale docstring: thread pool, not process pool.

iterate now uses a ThreadPoolExecutor, but the docstring still describes max_workers as "Worker process count" and mp_context as the context "for the process pool". Align the wording with the thread-based implementation (and note mp_context is currently unused/reserved, as in apply_layer).

📝 Proposed doc fix
-        parallel: If True, parallelize tensor updates within checkerboard sweeps.
-        max_workers: Worker process count when ``parallel`` is True.
-        mp_context: Multiprocessing context for the process pool.
+        parallel: If True, parallelize tensor updates within checkerboard sweeps.
+        max_workers: Worker thread count when ``parallel`` is True.
+        mp_context: Reserved (a thread pool is used for MPO parallelism).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/mqt/yaqs/digital/utils/mpo_utils.py` around lines 584 - 585, The
docstring for iterate is stale: update the descriptions for max_workers and
mp_context to reflect that iterate uses a ThreadPoolExecutor (thread pool) not a
process pool; change "Worker process count" to "Worker thread count (max_workers
for ThreadPoolExecutor)" and change "mp_context: Multiprocessing context for the
process pool" to note that mp_context is unused/reserved (like in apply_layer)
or removed if not used, and ensure the docstring references the iterate function
and the max_workers and mp_context parameters accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/mqt/yaqs/digital/utils/mpo_utils.py`:
- Around line 584-585: The docstring for iterate is stale: update the
descriptions for max_workers and mp_context to reflect that iterate uses a
ThreadPoolExecutor (thread pool) not a process pool; change "Worker process
count" to "Worker thread count (max_workers for ThreadPoolExecutor)" and change
"mp_context: Multiprocessing context for the process pool" to note that
mp_context is unused/reserved (like in apply_layer) or removed if not used, and
ensure the docstring references the iterate function and the max_workers and
mp_context parameters accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3b824516-afb4-4936-b54f-43346efb118c

📥 Commits

Reviewing files that changed from the base of the PR and between aef7907 and 3b44c1e.

📒 Files selected for processing (10)
  • docs/examples/equivalence_checking.md
  • src/mqt/yaqs/digital/utils/matrix_utils.py
  • src/mqt/yaqs/digital/utils/mpo_utils.py
  • src/mqt/yaqs/digital/utils/scheduler_utils.py
  • src/mqt/yaqs/equivalence_checker.py
  • src/mqt/yaqs/parallel_utils.py
  • tests/digital/utils/test_matrix_utils.py
  • tests/digital/utils/test_mpo_utils.py
  • tests/digital/utils/test_scheduler_utils.py
  • tests/test_equivalence_checker.py
💤 Files with no reviewable changes (1)
  • src/mqt/yaqs/digital/utils/scheduler_utils.py

@aaronleesander aaronleesander merged commit 80836e9 into main Jun 2, 2026
13 checks passed
@aaronleesander aaronleesander deleted the ec-update branch June 2, 2026 20:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant