Skip to content

Noise Swap#396

Open
aleramos119 wants to merge 21 commits into
munich-quantum-toolkit:mainfrom
aleramos119:noise_swap
Open

Noise Swap#396
aleramos119 wants to merge 21 commits into
munich-quantum-toolkit:mainfrom
aleramos119:noise_swap

Conversation

@aleramos119
Copy link
Copy Markdown
Contributor

Description

When constructing a NoiseModel class, internally the constructor swapped the indexes if they were not provided in ascending order, however didn't swapped the corresponding operators.

The construction of the NoiseModel accepts directly a BaseGate class in the "name" keywords instead of only a string with the name.

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.

aleramos119 and others added 10 commits April 21, 2026 14:55
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
NoiseModel.proccess["name"] accepts directly the BaseGate class and also a string with the name
- Restructure NoiseModel.__init__ process loop: handle 1-site first,
  then adjacent/non-adjacent 2-site with Crosstalk branching
- Move BaseGate to runtime import; get_operator short-circuits on
  BaseGate instances
- Add longrange_crosstalk_* aliases to GateLibrary and _PAULI_STRINGS
- Extend is_pauli to handle BaseGate instances via isinstance check
- Fix pre-commit issues: split compound asserts, add __init__ docstrings
  to Crosstalk subclasses, update test docstring
- Add .claude/ to .gitignore

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add local_dim parameter to BaseGate.__init__ (default 2 preserves
  qubit behaviour); interaction is now computed as log(n)/log(local_dim)
  and validated to be an integer
- Remove power-of-2 restriction from Destroy and Create
- Crosstalk inherits local_dim from constituent gates and raises
  ValueError if they differ

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Forward local_dim in all BaseGate methods that return a new gate
  (dag, conj, trans, __add__, __sub__, __mul__, __matmul__)
- Replace PauliX/Y/Z.matrix with X()/Y()/Z().matrix in tests,
  removing the noise_library import

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 22, 2026

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added Zero gate and Crosstalk gate family (including variants like CrosstalkXX, CrosstalkYY, etc.) to the gate library.
    • Extended gate library exports with additional operator aliases (raising, lowering, pauli_x/y/z, dephasing, zero, and crosstalk variants).
  • Refactor

    • Updated operator resolution to use gate library instead of legacy approach.
  • Chores

    • Updated .gitignore to exclude local configuration directory.

Walkthrough

Noise/operator sourcing moved from NoiseLibrary to GateLibrary: BaseGate gained local_dim, new Zero and Crosstalk gates and factories were added, NoiseModel now resolves and consumes BaseGate instances (extracting matrices and special-crosstalk handling), dissipation detection and tests updated to match the new gate semantics.

Changes

Config

Layer / File(s) Summary
Ignore assistant cache
.gitignore
Added .claude/ to ignored paths.

Gate Library

Layer / File(s) Summary
BaseGate local_dim and factories
src/mqt/yaqs/core/libraries/gate_library.py
Introduce local_dim on BaseGate, preserve it across ops, add BaseGate.crosstalk() and BaseGate.zero() factories, update ladder operators to pass local_dim, and add Zero gate.
Crosstalk classes & exports
src/mqt/yaqs/core/libraries/gate_library.py
Add Crosstalk and many Crosstalk* subclasses, compute swapped_matrix, extend GateLibrary exports and noise-model naming aliases, and update related docstrings.

Noise Model

Layer / File(s) Summary
Operator resolution & normalization
src/mqt/yaqs/core/data_structures/noise_model.py
NoiseModel.get_operator now returns BaseGate (accepting a gate or name); initialization and normalization derive matrix from gates, handle adjacent vs long-range processes, and extract/reorder factors for Crosstalk gates.

Dissipation Logic

Layer / File(s) Summary
Pauli/crosstalk detection
src/mqt/yaqs/core/methods/dissipation.py
is_pauli() recognizes Pauli and Crosstalk processes via expanded string aliases and GateLibrary gate classes.

Tests — Noise / Lindblad

Layer / File(s) Summary
Noise/Lindblad tests updates
tests/analog/test_lindblad.py, tests/core/data_structures/test_noise_model.py
Switch tests to gate constructors (X(), Y(), Z(), XX()), update expectations for adjacent/long-range factor ordering, simplify error assertions, and add tests for non-crosstalk cases and get_operator returning gate instances.

Tests — Gate Library

Layer / File(s) Summary
Gate library tests expanded
tests/core/libraries/test_gate_library.py
Add tests for Zero, Crosstalk*, XX/YY/ZZ, projectors, local_dim/matrix-size validation, @ operator, set_sites list-input behavior, and tensor transpose expectations.

Sequence Diagram(s)

(Skipped — changes modify library APIs and normalization but do not introduce a new multi-actor sequential runtime flow requiring a diagram.)

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • munich-quantum-toolkit/yaqs#319: Changes NoiseModel scheduled-jump/operator resolution paths overlapping with this PR's NoiseModel/operator-resolution updates.

Suggested labels

feature

Poem

🐰 I hopped from library old to new,
With Crosstalk stitched and local_dim true,
Gates pair up, matrices align,
Tests updated, the code refines,
A little rabbit cheers — nibble a clue. 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'Noise Swap' is vague and does not clearly convey what the pull request accomplishes; it lacks specificity about the nature of the changes. Use a more descriptive title such as 'Fix NoiseModel operator ordering with site index swaps' or 'Support BaseGate instances in NoiseModel' to clearly indicate the main changes.
✅ Passed checks (4 passed)
Check name Status Explanation
Description check ✅ Passed The description includes a summary of changes and a completed checklist, meeting the template requirements; all major sections are addressed.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% 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

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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: 7

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/mqt/yaqs/core/data_structures/noise_model.py`:
- Around line 202-217: get_operator currently calls operator_class() without
arguments which will raise a confusing TypeError for parameterized GateLibrary
entries (e.g., rx, ry, rz, u, u2, cp, p, pvm, destroy/create); update
get_operator to either catch TypeError when instantiating operator_class and
raise a clear ValueError describing that the named gate requires parameters
(include the gate name) or validate against a whitelist of parameter-free gate
names before instantiation; also fix the docstring return description to state
it returns a BaseGate (not a matrix) and adjust the docstring summary
accordingly.
- Around line 102-123: The code only adjusts ordering for Crosstalk but leaves
non-Crosstalk 2-site operators misaligned: when you detect swapped =
sorted(sites) != sites you must also reorder the operator data to match the new
site order. For adjacent non-Crosstalk cases (where proc["matrix"] is set),
either replace proc["matrix"] with the matrix conjugated by a SWAP on the two
sites (i.e. apply the index-swap transform) or explicitly raise if the provided
BaseGate is asymmetric and cannot be reordered; reference the variables proc,
name_op, swapped and where proc["matrix"] is assigned. For long-range cases
where proc["factors"] is provided, when swapped is True reorder the factors
(e.g. proc["factors"] = tuple(reversed(proc["factors"])) ) so factor↔site
alignment is preserved. Add a unit test that constructs a non-Crosstalk
asymmetric 2-site gate (e.g. CX-like) with unsorted sites and asserts the stored
proc["sites"] and proc operator data remain correctly aligned after
normalization.

In `@src/mqt/yaqs/core/libraries/gate_library.py`:
- Around line 141-166: The constructor BaseGate.__init__ currently computes
interaction = np.log(n) / np.log(local_dim) which misbehaves for local_dim <= 1;
add an explicit guard near the start of __init__ to validate local_dim >= 2 and
raise a ValueError with a clear message (e.g. "local_dim must be >= 2") before
computing logs; then proceed with the existing square-check and interaction
computation, leaving variables mat, n, interaction, self.local_dim, self.matrix,
self.tensor, and self.interaction unchanged.
- Around line 1534-1559: The constructor of Crosstalk must validate that both
operands are single-site gates: in Crosstalk.__init__ check gate1.interaction ==
1 and gate2.interaction == 1 (or equivalently that their interaction attribute
equals 1) before computing matrix1/matrix2/swapped_matrix and calling
super().__init__; if either operand is not single-site, raise a ValueError with
a clear message mentioning gate1.name/gate2.name and their interaction values so
callers can't pass multi-site gates like XX or CX that would produce invalid
factors for NoiseModel.

In `@src/mqt/yaqs/core/methods/dissipation.py`:
- Around line 61-103: PAULI fast-path misses common aliases and the Crosstalk
base class; update _PAULI_STRINGS and _PAULI_CLASSES and is_pauli to derive
aliases and classes programmatically: query GateLibrary for aliases that map to
Pauli operators (e.g. "x","y","z","dephasing") and add those names into
_PAULI_STRINGS, and include the Crosstalk base class (Crosstalk) in
_PAULI_CLASSES (or build the tuple from the Pauli classes plus Crosstalk) so
is_pauli(proc) (which inspects proc["name"]) recognizes both alias strings and
composite Crosstalk instances via isinstance(..., _PAULI_CLASSES).

In `@tests/analog/test_lindblad.py`:
- Around line 140-143: The test defines noise_processes with name="dephasing"
which is not in _PAULI_STRINGS so dissipation.is_pauli(proc) returns False and
the Pauli fast path is not exercised; update the test to either use a
Pauli-designated name (e.g., change the process name to one present in
_PAULI_STRINGS) or add an explicit assertion that dissipation.is_pauli(proc) is
True for the created proc (or add a sibling test) so the fast-path in
dissipation.py (is_pauli, _PAULI_STRINGS) is actually executed for the sigma_z
operator referenced in noise_processes.

In `@tests/core/data_structures/test_noise_model.py`:
- Around line 171-178: The test
test_longrange_unknown_label_without_factors_raises should stop catching broad
exception types and instead assert the specific error and message from failing
to resolve a long-range label via GateLibrary/get_operator; change the
pytest.raises call to expect a specific exception (e.g., ValueError or KeyError
raised by NoiseModel.__init__/GateLibrary.get_operator) and include a match
argument that checks for the stable substring like "unknown gate" or "could not
resolve 'foo_bar'" so the test only passes when the name-resolution failure
occurs rather than any unrelated AttributeError.
🪄 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: b279b9cd-ac62-4c69-90dd-39415d16f219

📥 Commits

Reviewing files that changed from the base of the PR and between a3cedd3 and 9fc8844.

📒 Files selected for processing (6)
  • .gitignore
  • src/mqt/yaqs/core/data_structures/noise_model.py
  • src/mqt/yaqs/core/libraries/gate_library.py
  • src/mqt/yaqs/core/methods/dissipation.py
  • tests/analog/test_lindblad.py
  • tests/core/data_structures/test_noise_model.py

Comment on lines +102 to +123
else: # Two-site: normalize site ordering
sorted_sites = sorted(sites)
if sorted_sites != sites:
swapped = sorted_sites != sites
if swapped:
proc["sites"] = sorted_sites
i, j = proc["sites"]
is_adjacent = abs(j - i) == 1

# Adjacent two-site: use full matrix
if is_adjacent:
if str(name).startswith("crosstalk_"):
# infer matrix from suffix ab
suffix = str(name).rsplit("_", 1)[-1]
assert len(suffix) == 2, "Invalid crosstalk label. Expected 'crosstalk_ab' with a,b in {x,y,z}."
assert all(c in "xyz" for c in suffix), (
"Invalid crosstalk label. Expected 'crosstalk_ab' with a,b in {x,y,z}."
)
a, b = suffix[0], suffix[1]
proc["matrix"] = np.kron(PAULI_MAP[a], PAULI_MAP[b])
elif "matrix" not in proc:
proc["matrix"] = NoiseModel.get_operator(name)
filled_processes.append(proc)
continue

# Non-adjacent two-site: attach per-site factors for crosstalk labels
if str(name).startswith("crosstalk_"):
if "factors" not in proc:
suffix = str(name).rsplit("_", 1)[-1]
assert len(suffix) == 2, "Invalid crosstalk label. Expected 'crosstalk_ab' with a,b in {x,y,z}."
assert all(c in "xyz" for c in suffix), (
"Invalid crosstalk label. Expected 'crosstalk_ab' with a,b in {x,y,z}."
)
a, b = suffix[0], suffix[1]
proc["factors"] = (PAULI_MAP[a], PAULI_MAP[b])
filled_processes.append(proc)
continue

# Long-range two-site with canonical legacy label
if str(name).startswith(CROSSTALK_PREFIX):
if "factors" not in proc:
suffix = str(name).rsplit("_", 1)[-1]
assert len(suffix) == 2, (
f"Invalid crosstalk label. Expected '{CROSSTALK_PREFIX}ab' with a,b in {{x,y,z}}."
)
assert all(c in "xyz" for c in suffix), (
f"Invalid crosstalk label. Expected '{CROSSTALK_PREFIX}ab' with a,b in {{x,y,z}}."
)
a, b = suffix[0], suffix[1]
proc["factors"] = (PAULI_MAP[a], PAULI_MAP[b])
filled_processes.append(proc)
continue

# Other long-range two-site: require explicit factors
assert "factors" in proc, (
"Non-adjacent 2-site processes must specify 'factors' unless named 'crosstalk_{ab}'."
)
filled_processes.append(proc)
continue

# One-site: ensure matrix
if "matrix" not in proc:
proc["matrix"] = NoiseModel.get_operator(name)

if abs(j - i) == 1: # Adjacent: store full matrix
if isinstance(name_op, Crosstalk):
proc["matrix"] = name_op.swapped_matrix if swapped else name_op.matrix
else:
proc["matrix"] = name_op.matrix

elif isinstance(name_op, Crosstalk):
proc["factors"] = (
(name_op.matrix2, name_op.matrix1) if swapped else (name_op.matrix1, name_op.matrix2)
)
else:
assert "factors" in proc, (
"Non-adjacent 2-site processes must specify 'factors' unless a Crosstalk gate is provided."
)

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.

⚠️ Potential issue | 🟠 Major

Swap fix only covers Crosstalk — non-Crosstalk 2-site gates still hit the original bug.

The PR description states the constructor "reordered indices into ascending order but did not reorder the corresponding operators accordingly." The new code fixes this only when name_op is a Crosstalk instance:

  • Line 113 (adjacent, non-Crosstalk): proc["matrix"] = name_op.matrix is stored without reordering when sites were swapped. For any asymmetric 2-site BaseGate passed by name or as a BaseGate instance (e.g., a custom gate, or CX), this reproduces exactly the bug this PR claims to fix.
  • Lines 119–122 (long-range, non-Crosstalk): when the user supplies their own "factors" with unsorted sites, the factors are not swapped either — proc["sites"] becomes ascending but proc["factors"] keeps the original order, so factor ↔ site alignment is broken.

At minimum the long-range explicit-factors case must reorder factors when swapped; the adjacent non-Crosstalk case should either swap via SWAP @ M @ SWAP or reject asymmetric 2-site operators here.

🛡️ Proposed fix for the explicit-factors case
                 elif isinstance(name_op, Crosstalk):
                     proc["factors"] = (
                         (name_op.matrix2, name_op.matrix1) if swapped else (name_op.matrix1, name_op.matrix2)
                     )
                 else:
                     assert "factors" in proc, (
                         "Non-adjacent 2-site processes must specify 'factors' unless a Crosstalk gate is provided."
                     )
+                    if swapped:
+                        f0, f1 = proc["factors"]
+                        proc["factors"] = (f1, f0)

Please also add a test covering a non-Crosstalk, asymmetric adjacent 2-site operator with unsorted sites to lock this in.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/mqt/yaqs/core/data_structures/noise_model.py` around lines 102 - 123, The
code only adjusts ordering for Crosstalk but leaves non-Crosstalk 2-site
operators misaligned: when you detect swapped = sorted(sites) != sites you must
also reorder the operator data to match the new site order. For adjacent
non-Crosstalk cases (where proc["matrix"] is set), either replace proc["matrix"]
with the matrix conjugated by a SWAP on the two sites (i.e. apply the index-swap
transform) or explicitly raise if the provided BaseGate is asymmetric and cannot
be reordered; reference the variables proc, name_op, swapped and where
proc["matrix"] is assigned. For long-range cases where proc["factors"] is
provided, when swapped is True reorder the factors (e.g. proc["factors"] =
tuple(reversed(proc["factors"])) ) so factor↔site alignment is preserved. Add a
unit test that constructs a non-Crosstalk asymmetric 2-site gate (e.g. CX-like)
with unsorted sites and asserts the stored proc["sites"] and proc operator data
remain correctly aligned after normalization.

Comment on lines 202 to +217
@staticmethod
def get_operator(name: str) -> NDArray[np.complex128]:
"""Retrieve the operator from NoiseLibrary, possibly as a tensor product if needed.
def get_operator(name: str | BaseGate) -> BaseGate:
"""Retrieve the operator from GateLibrary, possibly as a tensor product if needed.

Args:
name: Name of the noise process (e.g., 'xx', 'zz').

Returns:
np.ndarray: The matrix representation of the operator.
BaseGate: The matrix representation of the operator.
"""
if name in PAULI_MAP:
return PAULI_MAP[name]
operator_class = getattr(NoiseLibrary, name)
if isinstance(name, BaseGate):
return name

operator_class = getattr(GateLibrary, name)

operator: BaseGate = operator_class()
return operator.matrix
return operator_class()
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.

⚠️ Potential issue | 🟡 Minor

get_operator will fail for parameterized / arg-taking gate classes.

operator_class() unconditionally instantiates with no arguments. GateLibrary contains classes that require arguments (e.g., rx, ry, rz, u, u2, cp, p, pvm) as well as destroy/create which have optional d. If a user ever passes one of those names (or a future alias like raising_three) they will get a confusing TypeError rather than a clear diagnostic. Consider either catching the TypeError and raising a more descriptive error, or restricting get_operator to names known to be parameter-free.

Also, the docstring’s Returns: BaseGate: The matrix representation of the operator. is stale — it now returns a BaseGate, not a matrix. As per coding guidelines (Google-style docstrings in Python code), please correct the description.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/mqt/yaqs/core/data_structures/noise_model.py` around lines 202 - 217,
get_operator currently calls operator_class() without arguments which will raise
a confusing TypeError for parameterized GateLibrary entries (e.g., rx, ry, rz,
u, u2, cp, p, pvm, destroy/create); update get_operator to either catch
TypeError when instantiating operator_class and raise a clear ValueError
describing that the named gate requires parameters (include the gate name) or
validate against a whitelist of parameter-free gate names before instantiation;
also fix the docstring return description to state it returns a BaseGate (not a
matrix) and adjust the docstring summary accordingly.

Comment on lines +141 to +166
def __init__(self, mat: NDArray[np.complex128], local_dim: int = 2) -> None:
"""Initializes a BaseGate instance with the given matrix.

Args:
mat: The matrix representation of the gate.
local_dim: Local Hilbert space dimension per site (e.g. 2 for qubits, 3 for qutrits).

Raises:
ValueError: If the matrix is not square.
ValueError: If the matrix size is not a power of 2.
ValueError: If the matrix size is not a power of local_dim.
"""
if mat.shape[0] != mat.shape[1]:
msg = "Matrix must be square"
raise ValueError(msg)

log = np.log2(mat.shape[0])
n = mat.shape[0]

interaction = np.log(n) / np.log(local_dim)
if not np.isclose(interaction, round(interaction)):
msg = f"Matrix size {n} is not a power of {local_dim}"
raise ValueError(msg)

self.local_dim = local_dim
self.matrix = mat
self.tensor = mat
self.interaction = int(log)
self.interaction = round(interaction)
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.

⚠️ Potential issue | 🟡 Minor

Validate local_dim >= 2 to avoid NaN/division-by-zero.

np.log(local_dim) is 0 when local_dim == 1 and undefined for local_dim <= 0, so the ratio becomes NaN/inf, bypasses the np.isclose check incorrectly or yields the confusing message "Matrix size N is not a power of 1" for any matrix. Add an explicit guard so the error is clear and deterministic.

🛡️ Proposed fix
         if mat.shape[0] != mat.shape[1]:
             msg = "Matrix must be square"
             raise ValueError(msg)
 
+        if local_dim < 2:
+            msg = f"local_dim must be >= 2, got {local_dim}"
+            raise ValueError(msg)
+
         n = mat.shape[0]
 
         interaction = np.log(n) / np.log(local_dim)

Note: Destroy/Create assert d > 0 (lines 758, 786) then pass local_dim=d, so d=1 currently produces a 1×1 zero matrix and a misleading error inside BaseGate. Tightening to d >= 2 (or converting the assertion to a ValueError, since asserts are stripped under python -O) is recommended.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/mqt/yaqs/core/libraries/gate_library.py` around lines 141 - 166, The
constructor BaseGate.__init__ currently computes interaction = np.log(n) /
np.log(local_dim) which misbehaves for local_dim <= 1; add an explicit guard
near the start of __init__ to validate local_dim >= 2 and raise a ValueError
with a clear message (e.g. "local_dim must be >= 2") before computing logs; then
proceed with the existing square-check and interaction computation, leaving
variables mat, n, interaction, self.local_dim, self.matrix, self.tensor, and
self.interaction unchanged.

Comment on lines +1534 to +1559
class Crosstalk(BaseGate):
"""Class representing the Kronecker product of two single-site gates."""

name = "crosstalk"

def __init__(self, gate1: BaseGate, gate2: BaseGate) -> None:
"""Initializes the Kronecker product gate.

Args:
gate1: Left operand gate.
gate2: Right operand gate.

Raises:
ValueError: If the gates have different local dimensions.
"""
if gate1.local_dim != gate2.local_dim:
msg = f"Crosstalk gates must have the same local dimension, got {gate1.local_dim} and {gate2.local_dim}"
raise ValueError(msg)

self.name = f"crosstalk_{gate1.name}_{gate2.name}"
self.matrix1 = gate1.matrix
self.matrix2 = gate2.matrix
self.swapped_matrix = np.kron(gate2.matrix, gate1.matrix).astype(np.complex128)

mat = np.kron(gate1.matrix, gate2.matrix).astype(np.complex128)
super().__init__(mat, local_dim=gate1.local_dim)
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.

⚠️ Potential issue | 🟡 Minor

Validate inputs to Crosstalk.

Crosstalk is meant for Kronecker products of single-site gates, but there is no check that gate1.interaction == 1 and gate2.interaction == 1. Passing a 2-site gate (e.g., XX, CX) silently stores a 4×4 matrix in matrix1/matrix2, producing invalid "factors" when NoiseModel later destructures them for long-range processes, and a 16×16 matrix that will still pass the local_dim power check.

🛡️ Proposed validation
         if gate1.local_dim != gate2.local_dim:
             msg = f"Crosstalk gates must have the same local dimension, got {gate1.local_dim} and {gate2.local_dim}"
             raise ValueError(msg)
 
+        if gate1.interaction != 1 or gate2.interaction != 1:
+            msg = (
+                f"Crosstalk expects two single-site gates (interaction=1), "
+                f"got interaction={gate1.interaction} and {gate2.interaction}"
+            )
+            raise ValueError(msg)
+
         self.name = f"crosstalk_{gate1.name}_{gate2.name}"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/mqt/yaqs/core/libraries/gate_library.py` around lines 1534 - 1559, The
constructor of Crosstalk must validate that both operands are single-site gates:
in Crosstalk.__init__ check gate1.interaction == 1 and gate2.interaction == 1
(or equivalently that their interaction attribute equals 1) before computing
matrix1/matrix2/swapped_matrix and calling super().__init__; if either operand
is not single-site, raise a ValueError with a clear message mentioning
gate1.name/gate2.name and their interaction values so callers can't pass
multi-site gates like XX or CX that would produce invalid factors for
NoiseModel.

Comment on lines +61 to +103
_PAULI_STRINGS = {
"pauli_x",
"pauli_y",
"pauli_z",
"crosstalk_xx",
"crosstalk_yy",
"crosstalk_zz",
"crosstalk_xy",
"crosstalk_yx",
"crosstalk_zy",
"crosstalk_zx",
"crosstalk_yz",
"crosstalk_xz",
"longrange_crosstalk_xx",
"longrange_crosstalk_yy",
"longrange_crosstalk_zz",
"longrange_crosstalk_xy",
"longrange_crosstalk_yx",
"longrange_crosstalk_zy",
"longrange_crosstalk_zx",
"longrange_crosstalk_yz",
"longrange_crosstalk_xz",
}
_PAULI_CLASSES = (
X,
Y,
Z,
CrosstalkXX,
CrosstalkYY,
CrosstalkZZ,
CrosstalkXY,
CrosstalkYX,
CrosstalkZY,
CrosstalkZX,
CrosstalkYZ,
CrosstalkXZ,
)


def is_pauli(proc: dict[str, Any]) -> bool:
"""Return True if the process is a Pauli process."""
return bool(
proc["name"]
in {
"pauli_x",
"pauli_y",
"pauli_z",
"crosstalk_xx",
"crosstalk_yy",
"crosstalk_zz",
"crosstalk_xy",
"crosstalk_yx",
"crosstalk_zy",
"crosstalk_zx",
"crosstalk_yz",
"crosstalk_xz",
}
)
name = proc["name"]
return name in _PAULI_STRINGS or isinstance(name, _PAULI_CLASSES)
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.

⚠️ Potential issue | 🟠 Major

_PAULI_STRINGS and _PAULI_CLASSES are incomplete — Pauli fast path silently bypassed for common aliases.

GateLibrary exposes additional aliases that resolve to Pauli operators: x, y, z, and dephasing (= Z). None of these are in _PAULI_STRINGS, so is_pauli(proc) returns False for them and the dissipator falls through to the generic expm(-0.5 * dt * γ * A†A) branch. Mathematically this still happens to produce the correct result (because σ†σ = I for Pauli), but:

  • test_lindblad_dephasing_both_qubits in tests/analog/test_lindblad.py uses "name": "dephasing" — the intended fast path is not exercised at all.
  • The behavior is inconsistent: "pauli_x" uses the fast path but "x" does not, even though both resolve to the same gate class.
  • _PAULI_CLASSES also omits the Crosstalk base class, so a user-constructed Crosstalk(X(), Y()) instance passed as proc["name"] (now supported by NoiseModel.get_operator) won’t be recognized even though its concrete subclasses would be.

Consider deriving the set programmatically from GateLibrary aliases and including Crosstalk in the class tuple.

🛠️ Suggested consolidation
+from ..libraries.gate_library import Crosstalk
 from ..libraries.gate_library import (
     CrosstalkXX,
     ...
     X, Y, Z,
 )
 ...
 _PAULI_STRINGS = {
-    "pauli_x", "pauli_y", "pauli_z",
+    "pauli_x", "pauli_y", "pauli_z",
+    "x", "y", "z", "dephasing",
     "crosstalk_xx", ...
 }
 _PAULI_CLASSES = (
-    X, Y, Z,
+    X, Y, Z, Crosstalk,
     CrosstalkXX, ...
 )

(Adding Crosstalk makes the concrete-subclass entries redundant but harmless.)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/mqt/yaqs/core/methods/dissipation.py` around lines 61 - 103, PAULI
fast-path misses common aliases and the Crosstalk base class; update
_PAULI_STRINGS and _PAULI_CLASSES and is_pauli to derive aliases and classes
programmatically: query GateLibrary for aliases that map to Pauli operators
(e.g. "x","y","z","dephasing") and add those names into _PAULI_STRINGS, and
include the Crosstalk base class (Crosstalk) in _PAULI_CLASSES (or build the
tuple from the Pauli classes plus Crosstalk) so is_pauli(proc) (which inspects
proc["name"]) recognizes both alias strings and composite Crosstalk instances
via isinstance(..., _PAULI_CLASSES).

Comment on lines 140 to 143
noise_processes = [
{"name": "dephasing_0", "sites": [0], "strength": gamma, "matrix": sigma_z},
{"name": "dephasing_1", "sites": [1], "strength": gamma, "matrix": sigma_z},
{"name": "dephasing", "sites": [0], "strength": gamma, "matrix": sigma_z},
{"name": "dephasing", "sites": [1], "strength": gamma, "matrix": sigma_z},
]
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.

⚠️ Potential issue | 🟡 Minor

Test does not exercise the Pauli fast path despite using a dephasing operator.

With name="dephasing", is_pauli(proc) returns False (see comment in dissipation.py"dephasing" is not in _PAULI_STRINGS), so the dissipator goes through the generic expm branch. Numerically this still matches because Z²=I, but the test is not validating what its name implies. Once _PAULI_STRINGS is extended, this test will transparently start covering the fast path; until then, consider adding an assertion (or a sibling test) that pins which code path is exercised.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/analog/test_lindblad.py` around lines 140 - 143, The test defines
noise_processes with name="dephasing" which is not in _PAULI_STRINGS so
dissipation.is_pauli(proc) returns False and the Pauli fast path is not
exercised; update the test to either use a Pauli-designated name (e.g., change
the process name to one present in _PAULI_STRINGS) or add an explicit assertion
that dissipation.is_pauli(proc) is True for the created proc (or add a sibling
test) so the fast-path in dissipation.py (is_pauli, _PAULI_STRINGS) is actually
executed for the sigma_z operator referenced in noise_processes.

Comment on lines 171 to 178
def test_longrange_unknown_label_without_factors_raises() -> None:
"""Test that unknown long-range labels without 'factors' raise.

If the name is not 'longrange_crosstalk_{ab}' and no factors are provided,
If the name is not a recognized GateLibrary alias and no factors are provided,
initialization must fail to avoid guessing operators.

Raises:
AssertionError: If the model accepts an unknown long-range label without factors.
"""
try:
# Name is not a recognized non-adjacent 'crosstalk_{ab}' and no factors provided
with pytest.raises((AttributeError, AssertionError)):
_ = NoiseModel([{"name": "foo_bar", "sites": [0, 2], "strength": 0.1}])
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.

🧹 Nitpick | 🔵 Trivial

Broadened exception expectation can hide regressions.

pytest.raises((AttributeError, AssertionError)) will pass for any AttributeError raised anywhere in NoiseModel.__init__ — including an unrelated bug (e.g., a renamed attribute access). Since the intent is "unknown name cannot be resolved by GateLibrary", consider matching on a substring to lock the contract:

-    with pytest.raises((AttributeError, AssertionError)):
+    with pytest.raises((AttributeError, AssertionError), match="foo_bar|factors"):
         _ = NoiseModel([{"name": "foo_bar", "sites": [0, 2], "strength": 0.1}])

Longer-term, having get_operator raise a dedicated KeyError/ValueError with a stable message would make this assertion more robust.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/core/data_structures/test_noise_model.py` around lines 171 - 178, The
test test_longrange_unknown_label_without_factors_raises should stop catching
broad exception types and instead assert the specific error and message from
failing to resolve a long-range label via GateLibrary/get_operator; change the
pytest.raises call to expect a specific exception (e.g., ValueError or KeyError
raised by NoiseModel.__init__/GateLibrary.get_operator) and include a match
argument that checks for the stable substring like "unknown gate" or "could not
resolve 'foo_bar'" so the test only passes when the name-resolution failure
occurs rather than any unrelated AttributeError.

- Correct Destroy/Create matrix size from 2x2 to d×d
- Fix XX/YY/ZZ: replace stale `mpo` attribute with `mpo_tensors` note
- Add full Attributes section to Crosstalk class
- Correct CZ generator comment (I-X → I-Z)
- Expand set_sites docstrings in two-qubit gates to describe full effect
- Add missing aliases to GateLibrary Attributes section

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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: 3

♻️ Duplicate comments (2)
src/mqt/yaqs/core/libraries/gate_library.py (2)

141-166: ⚠️ Potential issue | 🟡 Minor

local_dim < 2 still lets matrices pass with a confusing error.

When local_dim == 1, np.log(local_dim) == 0 causes a divide-by-zero → the ratio becomes inf/NaN, so np.isclose(interaction, round(interaction)) evaluates unpredictably and the subsequent "Matrix size N is not a power of 1" message is misleading. local_dim <= 0 is also not rejected. Since Destroy/Create only assert d > 0 (stripped under python -O) and forward local_dim=d, a caller using d=1 lands here instead of getting a clean error. A guard if local_dim < 2: raise ValueError(...) before the log is worth adding.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/mqt/yaqs/core/libraries/gate_library.py` around lines 141 - 166, Add a
guard at the start of BaseGate.__init__ to reject invalid local_dim values:
check if local_dim < 2 and raise ValueError with a clear message (e.g.
"local_dim must be an integer >= 2"); this prevents divide-by-zero when
computing interaction = np.log(n)/np.log(local_dim) and avoids misleading "power
of" errors when callers pass 1 or non-positive values (the relevant symbols are
BaseGate.__init__, local_dim, interaction).

1546-1582: ⚠️ Potential issue | 🟡 Minor

Missing interaction == 1 validation for Crosstalk operands.

Crosstalk is documented as a Kronecker product of two single-site gates, but only local_dim equality is checked. Passing a 2-site gate (e.g., XX, CX) silently stores 4×4 factors in matrix1/matrix2 and produces a 16×16 matrix that still passes the local_dim-power check in BaseGate — which downstream NoiseModel code would then mis-destructure into "factors". Add a guard that both operands have interaction == 1.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/mqt/yaqs/core/libraries/gate_library.py` around lines 1546 - 1582,
Crosstalk currently only validates local_dim but must also ensure both operands
are single-site; in the Crosstalk.__init__ add a guard that gate1.interaction ==
1 and gate2.interaction == 1 and raise a ValueError with a clear message if not;
reference the Crosstalk class and its attributes matrix1/matrix2 as the place to
validate, so we never accept multi-site gates (e.g., XX, CX) that would produce
mis-sized Kronecker products downstream.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/mqt/yaqs/core/libraries/gate_library.py`:
- Around line 1657-1686: The Crosstalk ladder-operator aliases hardcode local
dimension d=2; update CrosstalkDestroyDestroy, CrosstalkCreateCreate,
CrosstalkDestroyCreate, and CrosstalkCreateDestroy to accept a local-dimension
parameter (e.g., def __init__(self, d: int = 2)) and pass that d into the
underlying Destroy(d) / Create(d) constructors instead of using the default, so
these GateLibrary aliases work for higher local dimensions; keep the existing
default of 2 to preserve backwards compatibility and update the constructor
docstrings to note the d parameter.
- Line 741: Replace the Unicode multiplication sign (×) used in the docstrings
with an ASCII "x" to satisfy ruff RUF002: update the docstring for the Destroy,
Create, and Crosstalk docstrings so "d×d" becomes "d x d" (and "d²×d²" becomes
"d^2 x d^2" or "d² x d²" using an ASCII "x"), modifying the docstring text in
the Destroy, Create, and Crosstalk definitions to use the ASCII x character
consistently.
- Around line 814-834: The Zero gate's matrix in Zero.__init__ is created as an
integer ndarray (mat = np.array([[0,0],[0,0]])) which is inconsistent with other
complex-typed placeholder/measurement gates; change the mat creation to
explicitly use a complex dtype (e.g., use np.zeros((2,2), dtype=complex) or
np.array([...], dtype=complex)) before calling super().__init__(mat) so the
gate's matrix dtype matches other gates and avoids subtle promotion issues.

---

Duplicate comments:
In `@src/mqt/yaqs/core/libraries/gate_library.py`:
- Around line 141-166: Add a guard at the start of BaseGate.__init__ to reject
invalid local_dim values: check if local_dim < 2 and raise ValueError with a
clear message (e.g. "local_dim must be an integer >= 2"); this prevents
divide-by-zero when computing interaction = np.log(n)/np.log(local_dim) and
avoids misleading "power of" errors when callers pass 1 or non-positive values
(the relevant symbols are BaseGate.__init__, local_dim, interaction).
- Around line 1546-1582: Crosstalk currently only validates local_dim but must
also ensure both operands are single-site; in the Crosstalk.__init__ add a guard
that gate1.interaction == 1 and gate2.interaction == 1 and raise a ValueError
with a clear message if not; reference the Crosstalk class and its attributes
matrix1/matrix2 as the place to validate, so we never accept multi-site gates
(e.g., XX, CX) that would produce mis-sized Kronecker products downstream.
🪄 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: 6e951ac0-3c8c-4fe1-a646-859fcd9f0016

📥 Commits

Reviewing files that changed from the base of the PR and between 9fc8844 and 645c1b5.

📒 Files selected for processing (1)
  • src/mqt/yaqs/core/libraries/gate_library.py

Comment thread src/mqt/yaqs/core/libraries/gate_library.py Outdated
Comment on lines +814 to +834
class Zero(BaseGate):
"""Class representing the Zero gate.

Attributes:
name: The name of the gate ("zero").
matrix: The 2x2 zero matrix.
interaction: The interaction level (1 for single-qubit gates).
tensor: The tensor representation of the gate (same as the matrix).

Methods:
set_sites(*sites: int) -> None:
Sets the site(s) where the gate is applied.
"""

name = "zero"

def __init__(self) -> None:
"""Initializes the Zero gate."""
mat = np.array([[0, 0], [0, 0]])
super().__init__(mat)

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.

🧹 Nitpick | 🔵 Trivial

Zero matrix has integer dtype.

np.array([[0, 0], [0, 0]]) produces an int64 matrix, unlike most other 2×2 gates here that are fine with int but unlike the complex-typed placeholders (P0, P1, RuntimeCost, ...). If Zero is used as a noise operator and later combined with complex matrices (e.g., in np.kron, dissipator terms, or Crosstalk), NumPy will promote correctly, but dtype inconsistency can cause subtle surprises (e.g., when callers set elements in-place, or when comparing dtypes). Consider dtype=complex for consistency with the measurement/placeholder gates.

♻️ Proposed fix
-        mat = np.array([[0, 0], [0, 0]])
+        mat = np.array([[0, 0], [0, 0]], dtype=complex)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/mqt/yaqs/core/libraries/gate_library.py` around lines 814 - 834, The Zero
gate's matrix in Zero.__init__ is created as an integer ndarray (mat =
np.array([[0,0],[0,0]])) which is inconsistent with other complex-typed
placeholder/measurement gates; change the mat creation to explicitly use a
complex dtype (e.g., use np.zeros((2,2), dtype=complex) or np.array([...],
dtype=complex)) before calling super().__init__(mat) so the gate's matrix dtype
matches other gates and avoids subtle promotion issues.

Comment on lines +1657 to +1686
class CrosstalkDestroyDestroy(Crosstalk):
"""Crosstalk Destroy ⊗ Destroy."""

def __init__(self) -> None:
"""Initializes Destroy ⊗ Destroy crosstalk."""
super().__init__(Destroy(), Destroy())


class CrosstalkCreateCreate(Crosstalk):
"""Crosstalk Create ⊗ Create."""

def __init__(self) -> None:
"""Initializes Create ⊗ Create crosstalk."""
super().__init__(Create(), Create())


class CrosstalkDestroyCreate(Crosstalk):
"""Crosstalk Destroy ⊗ Create."""

def __init__(self) -> None:
"""Initializes Destroy ⊗ Create crosstalk."""
super().__init__(Destroy(), Create())


class CrosstalkCreateDestroy(Crosstalk):
"""Crosstalk Create ⊗ Destroy."""

def __init__(self) -> None:
"""Initializes Create ⊗ Destroy crosstalk."""
super().__init__(Create(), Destroy())
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.

⚠️ Potential issue | 🟡 Minor

Ladder-operator Crosstalk aliases hardcode d=2.

CrosstalkDestroyDestroy, CrosstalkCreateCreate, CrosstalkDestroyCreate, and CrosstalkCreateDestroy instantiate Destroy()/Create() with the default d=2, so the raising_two/lowering_two GateLibrary aliases cannot be reused for higher local dimensions (qutrits, bosonic modes). Since the rest of the changes make local_dim first-class throughout BaseGate, consider parametrizing these subclasses by d (e.g., def __init__(self, d: int = 2): super().__init__(Destroy(d), Destroy(d))), or document the limitation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/mqt/yaqs/core/libraries/gate_library.py` around lines 1657 - 1686, The
Crosstalk ladder-operator aliases hardcode local dimension d=2; update
CrosstalkDestroyDestroy, CrosstalkCreateCreate, CrosstalkDestroyCreate, and
CrosstalkCreateDestroy to accept a local-dimension parameter (e.g., def
__init__(self, d: int = 2)) and pass that d into the underlying Destroy(d) /
Create(d) constructors instead of using the default, so these GateLibrary
aliases work for higher local dimensions; keep the existing default of 2 to
preserve backwards compatibility and update the constructor docstrings to note
the d parameter.

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 23, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

aleramos119 and others added 4 commits April 23, 2026 13:10
Ruff RUF002 flags U+00D7 (×) as ambiguous. Replace d×d and d²×d²
with dxd and (d^2)x(d^2) in Destroy, Create, and Crosstalk docstrings.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New tests cover:
- BaseGate: non-power-of-local_dim ValueError, list-input set_sites,
  @ operator, factory classmethods (destroy, zero, crosstalk, p0, p1, pvm)
- mpo_tensors property AttributeError before set_sites is called
- Zero, XX, YY, ZZ, P0, P1, PVM gate constructors
- Crosstalk: matrix/swapped_matrix, mismatched-local_dim ValueError,
  all 13 named subclasses
- Two-qubit gates (cx, cz, swap, rxx, ryy, rzz, cp): list-input set_sites,
  wrong-site-count ValueError, CX reversed-order tensor transpose
- Entropy/SchmidtSpectrum set_sites list-input branch

Remaining 3% (lines 99-114) is dead code in extend_gate's
elif len(tensors) == 3 branch, which split_tensor never triggers.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…00%)

Three new tests target the three previously uncovered branches:
- Adjacent two-site non-Crosstalk gate uses name_op.matrix directly (line 113)
- Non-adjacent non-Crosstalk process with pre-supplied factors passes assert (line 120)
- get_operator returns a BaseGate instance unchanged when passed directly (line 213)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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/core/libraries/gate_library.py (1)

192-266: ⚠️ Potential issue | 🟠 Major

Apply the same compatibility checks to @ as to gate multiplication.

Line 266 allows any shape-compatible matrices, so a 4x4 single-site qudit gate (local_dim=4, interaction=1) can be @-multiplied with a 4x4 two-qubit gate (local_dim=2, interaction=2) and silently returned as a two-qubit gate. Check both interaction and local_dim before all binary gate operations.

🛡️ Proposed compatibility helper
+    def _ensure_compatible_gate(self, other: BaseGate, operation: str) -> None:
+        if self.interaction != other.interaction or self.local_dim != other.local_dim:
+            msg = (
+                f"Cannot {operation} gates with different interaction/local_dim "
+                f"({self.interaction}, {self.local_dim}) and ({other.interaction}, {other.local_dim})"
+            )
+            raise ValueError(msg)
+
     def __add__(self, other: BaseGate) -> BaseGate:
@@
-        if self.interaction != other.interaction:
-            msg = "Cannot add gates with different interaction"
-            raise ValueError(msg)
+        self._ensure_compatible_gate(other, "add")
         return BaseGate(self.matrix + other.matrix, local_dim=self.local_dim)
@@
-        if self.interaction != other.interaction:
-            msg = "Cannot subtract gates with different interaction"
-            raise ValueError(msg)
+        self._ensure_compatible_gate(other, "subtract")
         return BaseGate(self.matrix - other.matrix, local_dim=self.local_dim)
@@
         if isinstance(other, BaseGate):
-            if self.interaction != other.interaction:
-                msg = "Cannot multiply gates with different interaction"
-                raise ValueError(msg)
+            self._ensure_compatible_gate(other, "multiply")
             return BaseGate(self.matrix @ other.matrix, local_dim=self.local_dim)
@@
     def __matmul__(self, other: BaseGate) -> BaseGate:
@@
-        return BaseGate(self.matrix @ other.matrix, local_dim=self.local_dim)
+        self._ensure_compatible_gate(other, "matrix-multiply")
+        return BaseGate(self.matrix @ other.matrix, local_dim=self.local_dim)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/mqt/yaqs/core/libraries/gate_library.py` around lines 192 - 266, The
__matmul__ implementation currently does no compatibility checks and can combine
gates with mismatched interaction or local_dim (e.g., BaseGate objects with
differing interaction or local_dim), so update __matmul__ to mirror the checks
used in __mul__/__add__/__sub__: verify self.interaction == other.interaction
and self.local_dim == other.local_dim before performing self.matrix @
other.matrix, and raise a ValueError with a clear message (similar to "Cannot
multiply gates with different interaction" / "Cannot multiply gates with
different local_dim") if they don't match; do this in BaseGate.__matmul__ (and
optionally factor the checks into a small helper used by __add__, __sub__,
__mul__, and __matmul__ to avoid duplication).
♻️ Duplicate comments (3)
src/mqt/yaqs/core/libraries/gate_library.py (2)

141-166: ⚠️ Potential issue | 🟡 Minor

Validate local_dim before deriving interaction.

local_dim <= 1 reaches the logarithm-based power check and produces unclear failures; this also makes Destroy(1)/Create(1) fail indirectly instead of with the documented constructor validation.

🛡️ Proposed guard
     def __init__(self, mat: NDArray[np.complex128], local_dim: int = 2) -> None:
         """Initializes a BaseGate instance with the given matrix.
@@
         if mat.shape[0] != mat.shape[1]:
             msg = "Matrix must be square"
             raise ValueError(msg)
 
+        if local_dim < 2:
+            msg = f"local_dim must be >= 2, got {local_dim}"
+            raise ValueError(msg)
+
         n = mat.shape[0]
 
         interaction = np.log(n) / np.log(local_dim)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/mqt/yaqs/core/libraries/gate_library.py` around lines 141 - 166, The
constructor BaseGate.__init__ computes interaction using log(local_dim) without
first validating local_dim; add a guard at the start of __init__ to validate
local_dim is an integer greater than 1 (e.g., local_dim >= 2) and raise a clear
ValueError if not, before computing interaction, so subsequent np.log-based
checks (and uses in Destroy/Create) fail with a descriptive constructor-level
error instead of unclear math errors.

1546-1582: ⚠️ Potential issue | 🟠 Major

Reject multi-site operands in Crosstalk.

The class contract says gate1 and gate2 are single-site gates, but Line 1576 accepts multi-site gates like XX(). That makes matrix1/matrix2 invalid per-site factors and can corrupt long-range NoiseModel factor construction.

🛡️ Proposed validation
         if gate1.local_dim != gate2.local_dim:
             msg = f"Crosstalk gates must have the same local dimension, got {gate1.local_dim} and {gate2.local_dim}"
             raise ValueError(msg)
 
+        if gate1.interaction != 1 or gate2.interaction != 1:
+            msg = (
+                "Crosstalk expects two single-site gates, "
+                f"got {gate1.name} with interaction {gate1.interaction} and "
+                f"{gate2.name} with interaction {gate2.interaction}"
+            )
+            raise ValueError(msg)
+
         self.name = f"crosstalk_{gate1.name}_{gate2.name}"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/mqt/yaqs/core/libraries/gate_library.py` around lines 1546 - 1582, The
Crosstalk constructor currently accepts multi-site gates; add a validation that
both operands are single-site by checking gate1.interaction == 1 and
gate2.interaction == 1 (or equivalent single-site property) and raise a
ValueError if not, before assigning matrix1/matrix2 and computing kron products;
update the error message to mention the requirement for single-site gates and
include gate1.name/gate2.name for clarity so matrix1/matrix2 remain valid
per-site factors used elsewhere.
tests/core/data_structures/test_noise_model.py (1)

171-178: ⚠️ Potential issue | 🟡 Minor

Tighten this exception assertion to the intended name-resolution failure.

Line 177 will pass for unrelated AttributeError/AssertionError raised anywhere in NoiseModel.__init__. Match the stable failure message so this test only covers the unknown-label path.

🧪 Proposed test tightening
-    with pytest.raises((AttributeError, AssertionError)):
+    with pytest.raises((AttributeError, AssertionError), match="foo_bar|unknown|resolve|factors"):
         _ = NoiseModel([{"name": "foo_bar", "sites": [0, 2], "strength": 0.1}])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/core/data_structures/test_noise_model.py` around lines 171 - 178,
Tighten the test in test_longrange_unknown_label_without_factors_raises so it
only catches the specific name-resolution failure from NoiseModel.__init__:
replace the broad pytest.raises((AttributeError, AssertionError)) with
pytest.raises(ValueError, match="unrecognized long-range label") (or the exact
stable message emitted by NoiseModel.__init__ for unknown labels) so the test
targets the unknown-label path in NoiseModel rather than any
AttributeError/AssertionError raised elsewhere.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/mqt/yaqs/core/libraries/gate_library.py`:
- Around line 192-266: The __matmul__ implementation currently does no
compatibility checks and can combine gates with mismatched interaction or
local_dim (e.g., BaseGate objects with differing interaction or local_dim), so
update __matmul__ to mirror the checks used in __mul__/__add__/__sub__: verify
self.interaction == other.interaction and self.local_dim == other.local_dim
before performing self.matrix @ other.matrix, and raise a ValueError with a
clear message (similar to "Cannot multiply gates with different interaction" /
"Cannot multiply gates with different local_dim") if they don't match; do this
in BaseGate.__matmul__ (and optionally factor the checks into a small helper
used by __add__, __sub__, __mul__, and __matmul__ to avoid duplication).

---

Duplicate comments:
In `@src/mqt/yaqs/core/libraries/gate_library.py`:
- Around line 141-166: The constructor BaseGate.__init__ computes interaction
using log(local_dim) without first validating local_dim; add a guard at the
start of __init__ to validate local_dim is an integer greater than 1 (e.g.,
local_dim >= 2) and raise a clear ValueError if not, before computing
interaction, so subsequent np.log-based checks (and uses in Destroy/Create) fail
with a descriptive constructor-level error instead of unclear math errors.
- Around line 1546-1582: The Crosstalk constructor currently accepts multi-site
gates; add a validation that both operands are single-site by checking
gate1.interaction == 1 and gate2.interaction == 1 (or equivalent single-site
property) and raise a ValueError if not, before assigning matrix1/matrix2 and
computing kron products; update the error message to mention the requirement for
single-site gates and include gate1.name/gate2.name for clarity so
matrix1/matrix2 remain valid per-site factors used elsewhere.

In `@tests/core/data_structures/test_noise_model.py`:
- Around line 171-178: Tighten the test in
test_longrange_unknown_label_without_factors_raises so it only catches the
specific name-resolution failure from NoiseModel.__init__: replace the broad
pytest.raises((AttributeError, AssertionError)) with pytest.raises(ValueError,
match="unrecognized long-range label") (or the exact stable message emitted by
NoiseModel.__init__ for unknown labels) so the test targets the unknown-label
path in NoiseModel rather than any AttributeError/AssertionError raised
elsewhere.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 048ca944-e44b-46b5-a234-903da185ac3d

📥 Commits

Reviewing files that changed from the base of the PR and between 645c1b5 and 43a22c2.

📒 Files selected for processing (3)
  • src/mqt/yaqs/core/libraries/gate_library.py
  • tests/core/data_structures/test_noise_model.py
  • tests/core/libraries/test_gate_library.py

aleramos119 and others added 6 commits April 23, 2026 14:33
Covers project overview, source layout, key abstractions (BaseGate,
NoiseModel, MPS/MPO), development conventions, and useful commands.
Complements AGENTS.md without duplicating it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace bare Any with Callable[..., BaseGate] and dict[str, list[float]]
  to satisfy ruff ANN401 (no bare Any) and ty call-non-callable checks
- Move Callable import under TYPE_CHECKING per ruff TC003

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Annotations were dropped by a previous hook run. Reapply
Callable[..., BaseGate] and dict[str, list[float]] for gate_factory
and kwargs, with Callable imported under TYPE_CHECKING.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@aaronleesander
Copy link
Copy Markdown
Member

@CodeRabbit full review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 15, 2026

✅ Actions performed

Full review triggered.

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/core/libraries/gate_library.py (1)

257-267: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

__matmul__ bypasses interaction compatibility checks.

__mul__ rejects mixed interaction levels, but __matmul__ does not. This allows composing gates that are shape-compatible but physically incompatible (e.g., different interaction semantics), producing misleading results.

Suggested fix
 def __matmul__(self, other: BaseGate) -> BaseGate:
     """Matrix multiplication using @ operator.
@@
-    return BaseGate(self.matrix @ other.matrix, local_dim=self.local_dim)
+    if self.interaction != other.interaction:
+        msg = "Cannot multiply gates with different interaction"
+        raise ValueError(msg)
+    return BaseGate(self.matrix @ other.matrix, local_dim=self.local_dim)
🤖 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/core/libraries/gate_library.py` around lines 257 - 267, The
__matmul__ method currently constructs a new BaseGate from self.matrix @
other.matrix without enforcing the same interaction-level compatibility checks
that __mul__ performs, allowing physically incompatible gates to be combined;
update __matmul__ (the BaseGate.__matmul__ method) to perform the same
interaction compatibility validation as BaseGate.__mul__ (or call into the same
helper used by __mul__), and raise the same error (e.g., ValueError) when
interaction semantics differ, then only return BaseGate(self.matrix @
other.matrix, local_dim=self.local_dim) when the compatibility check passes.
🤖 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/core/libraries/gate_library.py`:
- Around line 257-267: The __matmul__ method currently constructs a new BaseGate
from self.matrix @ other.matrix without enforcing the same interaction-level
compatibility checks that __mul__ performs, allowing physically incompatible
gates to be combined; update __matmul__ (the BaseGate.__matmul__ method) to
perform the same interaction compatibility validation as BaseGate.__mul__ (or
call into the same helper used by __mul__), and raise the same error (e.g.,
ValueError) when interaction semantics differ, then only return
BaseGate(self.matrix @ other.matrix, local_dim=self.local_dim) when the
compatibility check passes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3d72f1e3-8ca5-4a40-aa4a-e71b0a2206d5

📥 Commits

Reviewing files that changed from the base of the PR and between 93f6b9e and dc3d919.

📒 Files selected for processing (7)
  • .gitignore
  • src/mqt/yaqs/core/data_structures/noise_model.py
  • src/mqt/yaqs/core/libraries/gate_library.py
  • src/mqt/yaqs/core/methods/dissipation.py
  • tests/analog/test_lindblad.py
  • tests/core/data_structures/test_noise_model.py
  • tests/core/libraries/test_gate_library.py

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