Skip to content

Commit c712887

Browse files
authored
Merge pull request #71 from idriss-hamadi/feature/combined-ising-visualization-58-59
feat: Add Ising analysis and atomic visualization features (#58 #59)
2 parents eecf112 + 376968a commit c712887

9 files changed

Lines changed: 999 additions & 3 deletions

File tree

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ classifiers = [
3737
dependencies = [
3838
"pydantic>=2.10.6",
3939
"oqd-compiler-infrastructure@git+https://github.com/openquantumdesign/oqd-compiler-infrastructure",
40-
"numpy",
40+
"numpy>=2.2.3",
41+
"matplotlib",
4142
]
4243

4344
[project.optional-dependencies]

src/oqd_core/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,6 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
from . import backend, compiler, interface
15+
from . import backend, compiler, interface, visualizations
1616

17-
__all__ = ["interface", "compiler", "backend"]
17+
__all__ = ["interface", "compiler", "backend", "visualizations"]

src/oqd_core/compiler/analog/passes/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,13 @@
1515
from .analysis import analysis_canonical_hamiltonian_dim, analysis_term_index
1616
from .assign import assign_analog_circuit_dim, verify_analog_args_dim
1717
from .canonicalize import analog_operator_canonicalization
18+
from .ising_analysis import analyze_ising_gate
1819

1920
__all__ = [
2021
"assign_analog_circuit_dim",
2122
"verify_analog_args_dim",
2223
"analog_operator_canonicalization",
2324
"analysis_canonical_hamiltonian_dim",
2425
"analysis_term_index",
26+
"analyze_ising_gate",
2527
]
Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
# Copyright 2024-2025 Open Quantum Design
2+
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from __future__ import annotations
16+
17+
from typing import Dict, List, Set, Tuple
18+
19+
from oqd_compiler_infrastructure import ConversionRule, Post
20+
21+
from oqd_core.interface.analog.operation import AnalogGate
22+
from oqd_core.interface.analog.operator import (
23+
Annihilation,
24+
Creation,
25+
Identity,
26+
Ladder,
27+
Operator,
28+
OperatorAdd,
29+
OperatorKron,
30+
OperatorScalarMul,
31+
OperatorSub,
32+
PauliI,
33+
PauliX,
34+
PauliY,
35+
PauliZ,
36+
)
37+
from oqd_core.interface.math import MathExpr, MathImag, MathMul, MathNum
38+
39+
from .ising_types import (
40+
IsingAnalysisResult,
41+
IsingCouplingMatrices,
42+
IsingValidationError,
43+
PauliString,
44+
)
45+
46+
__all__ = [
47+
"IsingAnalysisPass",
48+
"analyze_ising_gate"
49+
]
50+
51+
52+
class IsingAnalysisPass(ConversionRule):
53+
"""
54+
Compiler pass to analyze if an AnalogGate implements an Ising-like Hamiltonian.
55+
56+
This pass walks the Hamiltonian operator tree and:
57+
1. Validates Ising-like properties (qubit-only, weight-2 Pauli strings, time-independent)
58+
2. Extracts coupling matrices for different Pauli operator pairs
59+
3. Returns structured analysis results
60+
"""
61+
62+
def __init__(self):
63+
super().__init__()
64+
self.pauli_strings: List[PauliString] = []
65+
self.errors: List[IsingValidationError] = []
66+
self.qubit_indices: Set[int] = set()
67+
self.current_coefficient = 1.0
68+
self.current_pauli_ops: List[Tuple[int, str]] = []
69+
self.current_qubit_index = 0
70+
71+
def map_AnalogGate(self, model: AnalogGate, operands: Dict) -> IsingAnalysisResult:
72+
"""Analyze an AnalogGate for Ising-like properties."""
73+
# Reset state
74+
self.pauli_strings = []
75+
self.errors = []
76+
self.qubit_indices = set()
77+
78+
# Walk the Hamiltonian
79+
self._analyze_operator(model.hamiltonian)
80+
81+
# Determine if Ising-like
82+
is_ising_like = len(self.errors) == 0 and self._validate_ising_properties()
83+
84+
# Extract coupling matrices if valid
85+
coupling_matrices = None
86+
n_qubits = max(self.qubit_indices) + 1 if self.qubit_indices else 0
87+
88+
if is_ising_like and n_qubits > 0:
89+
coupling_matrices = self._extract_coupling_matrices(n_qubits)
90+
91+
return IsingAnalysisResult(
92+
is_ising_like=is_ising_like,
93+
coupling_matrices=coupling_matrices,
94+
pauli_strings=self.pauli_strings,
95+
errors=self.errors,
96+
n_qubits=n_qubits
97+
)
98+
99+
def _analyze_operator(self, op: Operator, coefficient: complex = 1.0):
100+
"""Recursively analyze an operator."""
101+
if isinstance(op, OperatorAdd):
102+
self._analyze_operator(op.op1, coefficient)
103+
self._analyze_operator(op.op2, coefficient)
104+
105+
elif isinstance(op, OperatorSub):
106+
self._analyze_operator(op.op1, coefficient)
107+
self._analyze_operator(op.op2, -coefficient)
108+
109+
elif isinstance(op, OperatorScalarMul):
110+
# Extract coefficient and check if time-independent
111+
scalar_coeff = self._extract_coefficient(op.expr)
112+
if scalar_coeff is None:
113+
self.errors.append(IsingValidationError(
114+
error_type="time_dependent_coefficient",
115+
message="Coefficient contains time-dependent terms",
116+
operator=op
117+
))
118+
return
119+
self._analyze_operator(op.op, coefficient * scalar_coeff)
120+
121+
elif isinstance(op, OperatorKron):
122+
# Analyze Kronecker product (tensor product)
123+
self._analyze_kronecker_product(op, coefficient)
124+
125+
elif isinstance(op, (PauliI, PauliX, PauliY, PauliZ)):
126+
# Single Pauli operator
127+
pauli_type = self._get_pauli_type(op)
128+
pauli_string = PauliString(
129+
pauli_ops=[(0, pauli_type)],
130+
coefficient=coefficient
131+
)
132+
self.pauli_strings.append(pauli_string)
133+
self.qubit_indices.add(0)
134+
135+
elif isinstance(op, (Creation, Annihilation, Identity, Ladder)):
136+
# Bosonic operators - not allowed in Ising Hamiltonians
137+
self.errors.append(IsingValidationError(
138+
error_type="bosonic_operator",
139+
message=f"Found bosonic operator {type(op).__name__} - not allowed in Ising Hamiltonians",
140+
operator=op
141+
))
142+
143+
def _analyze_kronecker_product(self, op: OperatorKron, coefficient: complex):
144+
"""Analyze a Kronecker product to extract Pauli string."""
145+
pauli_ops = []
146+
qubit_index = 0
147+
148+
# Flatten the Kronecker product and extract Pauli operators
149+
ops_to_process = [op.op1, op.op2]
150+
151+
for sub_op in ops_to_process:
152+
if isinstance(sub_op, (PauliI, PauliX, PauliY, PauliZ)):
153+
pauli_type = self._get_pauli_type(sub_op)
154+
pauli_ops.append((qubit_index, pauli_type))
155+
self.qubit_indices.add(qubit_index)
156+
qubit_index += 1
157+
elif isinstance(sub_op, OperatorKron):
158+
# Nested Kronecker product - recursively flatten
159+
self._flatten_kronecker(sub_op, pauli_ops, qubit_index)
160+
qubit_index = len(pauli_ops)
161+
elif isinstance(sub_op, (Creation, Annihilation, Identity, Ladder)):
162+
self.errors.append(IsingValidationError(
163+
error_type="bosonic_operator",
164+
message=f"Found bosonic operator {type(sub_op).__name__} in Kronecker product",
165+
operator=sub_op
166+
))
167+
return
168+
else:
169+
self.errors.append(IsingValidationError(
170+
error_type="unsupported_operator",
171+
message=f"Unsupported operator type {type(sub_op).__name__} in Kronecker product",
172+
operator=sub_op
173+
))
174+
return
175+
176+
if pauli_ops:
177+
pauli_string = PauliString(
178+
pauli_ops=pauli_ops,
179+
coefficient=coefficient
180+
)
181+
self.pauli_strings.append(pauli_string)
182+
183+
def _flatten_kronecker(self, op: OperatorKron, pauli_ops: List[Tuple[int, str]], start_index: int):
184+
"""Recursively flatten nested Kronecker products."""
185+
# This is a simplified version - full implementation would handle all nesting levels
186+
if isinstance(op.op1, (PauliI, PauliX, PauliY, PauliZ)):
187+
pauli_type = self._get_pauli_type(op.op1)
188+
pauli_ops.append((start_index, pauli_type))
189+
self.qubit_indices.add(start_index)
190+
191+
if isinstance(op.op2, (PauliI, PauliX, PauliY, PauliZ)):
192+
pauli_type = self._get_pauli_type(op.op2)
193+
pauli_ops.append((start_index + 1, pauli_type))
194+
self.qubit_indices.add(start_index + 1)
195+
196+
def _get_pauli_type(self, op) -> str:
197+
"""Get the Pauli type as a string."""
198+
if isinstance(op, PauliI):
199+
return 'I'
200+
elif isinstance(op, PauliX):
201+
return 'X'
202+
elif isinstance(op, PauliY):
203+
return 'Y'
204+
elif isinstance(op, PauliZ):
205+
return 'Z'
206+
else:
207+
raise ValueError(f"Unknown Pauli operator: {type(op)}")
208+
209+
def _extract_coefficient(self, expr: MathExpr) -> complex:
210+
"""Extract coefficient from MathExpr and check if time-independent."""
211+
if isinstance(expr, MathNum):
212+
return complex(expr.value)
213+
elif isinstance(expr, MathImag):
214+
return 1j
215+
elif isinstance(expr, MathMul):
216+
# For simplicity, assume multiplication of numbers and imaginary unit
217+
left = self._extract_coefficient(expr.expr1)
218+
right = self._extract_coefficient(expr.expr2)
219+
if left is not None and right is not None:
220+
return left * right
221+
222+
# If we can't extract a simple coefficient, assume time-dependent
223+
return None
224+
225+
def _validate_ising_properties(self) -> bool:
226+
"""Validate that all Pauli strings satisfy Ising properties."""
227+
for pauli_string in self.pauli_strings:
228+
# Check weight-2 constraint
229+
if pauli_string.weight > 2:
230+
self.errors.append(IsingValidationError(
231+
error_type="high_weight_pauli",
232+
message=f"Found Pauli string with weight {pauli_string.weight} > 2"
233+
))
234+
return False
235+
236+
# Check time-independence
237+
if not pauli_string.is_time_independent:
238+
self.errors.append(IsingValidationError(
239+
error_type="time_dependent_coefficient",
240+
message="Found time-dependent coefficient"
241+
))
242+
return False
243+
244+
return True
245+
246+
def _extract_coupling_matrices(self, n_qubits: int) -> IsingCouplingMatrices:
247+
"""Extract coupling matrices from validated Pauli strings."""
248+
matrices = IsingCouplingMatrices.zeros(n_qubits)
249+
250+
for pauli_string in self.pauli_strings:
251+
if pauli_string.weight == 2:
252+
# Get the two non-identity qubits and their Pauli types
253+
non_identity = [(idx, pauli) for idx, pauli in pauli_string.pauli_ops if pauli != 'I']
254+
if len(non_identity) == 2:
255+
(i, pauli_i), (j, pauli_j) = non_identity
256+
coefficient = pauli_string.coefficient.real # Should be real for Ising
257+
258+
# Determine interaction type and update corresponding matrix
259+
interaction_type = ''.join(sorted([pauli_i, pauli_j]))
260+
261+
if interaction_type == 'XX':
262+
matrices.XX[i, j] = matrices.XX[j, i] = coefficient
263+
elif interaction_type == 'YY':
264+
matrices.YY[i, j] = matrices.YY[j, i] = coefficient
265+
elif interaction_type == 'ZZ':
266+
matrices.ZZ[i, j] = matrices.ZZ[j, i] = coefficient
267+
elif interaction_type == 'XY':
268+
matrices.XY[i, j] = matrices.XY[j, i] = coefficient
269+
elif interaction_type == 'XZ':
270+
matrices.XZ[i, j] = matrices.XZ[j, i] = coefficient
271+
elif interaction_type == 'YZ':
272+
matrices.YZ[i, j] = matrices.YZ[j, i] = coefficient
273+
274+
return matrices
275+
276+
277+
def analyze_ising_gate(gate: AnalogGate) -> IsingAnalysisResult:
278+
"""
279+
Convenience function to analyze an AnalogGate for Ising-like properties.
280+
281+
Args:
282+
gate: AnalogGate to analyze
283+
284+
Returns:
285+
IsingAnalysisResult containing analysis results and coupling matrices
286+
"""
287+
analysis_pass = Post(IsingAnalysisPass())
288+
return analysis_pass(gate)

0 commit comments

Comments
 (0)