From 38aa29258fa2e01e3e8c7fdecf3a237800c58eab Mon Sep 17 00:00:00 2001 From: wuzp15 Date: Wed, 22 Jul 2026 16:23:08 +0800 Subject: [PATCH 01/59] fix compiler redundant-gate deletion indexes --- .../optimizer/cancel_redundant_gates.py | 9 +++-- tests/test_review_c1_cancel_redundant.py | 35 +++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 tests/test_review_c1_cancel_redundant.py diff --git a/spinqit/compiler/optimizer/cancel_redundant_gates.py b/spinqit/compiler/optimizer/cancel_redundant_gates.py index 57f5ae7..37fa9aa 100644 --- a/spinqit/compiler/optimizer/cancel_redundant_gates.py +++ b/spinqit/compiler/optimizer/cancel_redundant_gates.py @@ -82,5 +82,10 @@ def run(self, ir: IntermediateRepresentation): else: non_keep_list.extend(path) i += 1 - ir.remove_nodes(keep_list, True) - ir.remove_nodes(non_keep_list, False) + removed = sorted(set(keep_list)) + ir.remove_nodes(removed, True) + remapped_non_keep = [ + node - sum(previous < node for previous in removed) + for node in non_keep_list + ] + ir.remove_nodes(remapped_non_keep, False) diff --git a/tests/test_review_c1_cancel_redundant.py b/tests/test_review_c1_cancel_redundant.py new file mode 100644 index 0000000..ebac595 --- /dev/null +++ b/tests/test_review_c1_cancel_redundant.py @@ -0,0 +1,35 @@ +from copy import deepcopy +import unittest + +import numpy as np + +from spinqit import BasicSimulatorConfig, Circuit, H, Rx, X, get_basic_simulator, get_compiler + + +class CancelRedundantGateRegressionTest(unittest.TestCase): + def test_mixed_cancellations_compile_and_preserve_state(self): + circuit = Circuit('mixed-cancellations') + circuit.allocateQubits(1) + circuit << (H, 0) + circuit << (H, 0) + circuit << (Rx, 0, 0.2) + circuit << (Rx, 0, 0.3) + circuit << (X, 0) + + compiler = get_compiler('native') + config = BasicSimulatorConfig() + + def simulate(level): + ir = compiler.compile(circuit, level) + state = np.asarray(get_basic_simulator().execute(deepcopy(ir), config).states) + return state / np.linalg.norm(state) + + reference = simulate(0) + for level in (1, 2, 3): + with self.subTest(level=level): + candidate = simulate(level) + self.assertGreaterEqual(abs(np.vdot(reference, candidate)) ** 2, 1 - 1e-9) + + +if __name__ == '__main__': + unittest.main() From 39cc5b167d2f5ef331252da8fd4a70d78bd6c60c Mon Sep 17 00:00:00 2001 From: wuzp15 Date: Wed, 22 Jul 2026 16:25:04 +0800 Subject: [PATCH 02/59] fix pure-state swap rewrite ordering --- .../quantum_pure_state_optimization.py | 12 +++++-- tests/test_review_c2_pure_state_swap.py | 34 +++++++++++++++++++ 2 files changed, 43 insertions(+), 3 deletions(-) create mode 100644 tests/test_review_c2_pure_state_swap.py diff --git a/spinqit/compiler/optimizer/quantum_pure_state_optimization.py b/spinqit/compiler/optimizer/quantum_pure_state_optimization.py index a3e2a40..182db53 100644 --- a/spinqit/compiler/optimizer/quantum_pure_state_optimization.py +++ b/spinqit/compiler/optimizer/quantum_pure_state_optimization.py @@ -75,6 +75,7 @@ def run(self, ir: IntermediateRepresentation): new_dag = self.swap_to_u_dag(qargs[0], qargs[1]) ir.substitute_nodes([v], new_dag, ir.dag.vs[v]['type']) non_keep_list.append(v) + self.wire_state.swap(qargs[0], qargs[1]) elif which_swap in ['left', 'right']: new_dag = self.aswap_to_u_dag(qargs[0], qargs[1], which_swap) ir.substitute_nodes([v], new_dag, ir.dag.vs[v]['type']) @@ -89,8 +90,13 @@ def run(self, ir: IntermediateRepresentation): for qarg in qargs: self.wire_state[qarg] = None - ir.remove_nodes(keep_list, True) - ir.remove_nodes(non_keep_list, False) + removed = sorted(set(non_keep_list)) + ir.remove_nodes(removed, False) + remapped_keep = [ + node - sum(previous < node for previous in removed) + for node in keep_list + ] + ir.remove_nodes(remapped_keep, True) def check_single_qubit_gate(self, v: Vertex): if (v['type'] == NodeType.op.value or v['type'] == NodeType.callee.value) \ @@ -284,4 +290,4 @@ def round_to_half_pi(thetar, phir, lambdar): lambdar = int(round(lambdar/(math.pi/2))) * math.pi/2 phir = phir % (2 * math.pi) lambdar = lambdar % (2 * math.pi) - return thetar, phir, lambdar \ No newline at end of file + return thetar, phir, lambdar diff --git a/tests/test_review_c2_pure_state_swap.py b/tests/test_review_c2_pure_state_swap.py new file mode 100644 index 0000000..a5954cf --- /dev/null +++ b/tests/test_review_c2_pure_state_swap.py @@ -0,0 +1,34 @@ +from copy import deepcopy +import unittest + +import numpy as np + +from spinqit import BasicSimulatorConfig, Circuit, Ry, SWAP, X, get_basic_simulator, get_compiler + + +class PureStateSwapRegressionTest(unittest.TestCase): + def test_level_three_preserves_state_across_swaps(self): + circuit = Circuit('pure-state-swap') + circuit.allocateQubits(2) + circuit << (Ry, 0, 0.3) + circuit << (Ry, 1, 0.3) + circuit << (SWAP, [0, 1]) + circuit << (Ry, 0, 0.4) + circuit << (SWAP, [0, 1]) + circuit << (X, 0) + + compiler = get_compiler('native') + config = BasicSimulatorConfig() + + def simulate(level): + ir = compiler.compile(circuit, level) + state = np.asarray(get_basic_simulator().execute(deepcopy(ir), config).states) + return state / np.linalg.norm(state) + + reference = simulate(0) + candidate = simulate(3) + self.assertGreaterEqual(abs(np.vdot(reference, candidate)) ** 2, 1 - 1e-9) + + +if __name__ == '__main__': + unittest.main() From 256a08201a125aeb6ad31a0c32bb4106d4f20fad Mon Sep 17 00:00:00 2001 From: wuzp15 Date: Wed, 22 Jul 2026 16:26:06 +0800 Subject: [PATCH 03/59] fix single-qubit matrix gate compilation --- spinqit/compiler/translator/gate_converter.py | 2 +- tests/test_review_c3_matrix_gate.py | 24 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 tests/test_review_c3_matrix_gate.py diff --git a/spinqit/compiler/translator/gate_converter.py b/spinqit/compiler/translator/gate_converter.py index 17e0ba2..a6c50c1 100644 --- a/spinqit/compiler/translator/gate_converter.py +++ b/spinqit/compiler/translator/gate_converter.py @@ -55,7 +55,7 @@ def is_primary_gate(gate: Gate): if gate in IR.basis_set or gate.label in IR.label_set: return True elif isinstance(gate, MatrixGate): - return True + return gate.qubit_num != 1 or len(gate.factors) > 0 elif isinstance(gate, ControlledGate) and (isinstance(gate.base_gate, MatrixGate) or gate.base_gate in IR.basis_set): return True diff --git a/tests/test_review_c3_matrix_gate.py b/tests/test_review_c3_matrix_gate.py new file mode 100644 index 0000000..0c9f670 --- /dev/null +++ b/tests/test_review_c3_matrix_gate.py @@ -0,0 +1,24 @@ +from copy import deepcopy +import unittest + +import numpy as np + +from spinqit import BasicSimulatorConfig, Circuit, MatrixGateBuilder, get_basic_simulator, get_compiler + + +class MatrixGateRegressionTest(unittest.TestCase): + def test_factorless_matrix_gate_executes_on_basic_simulator(self): + matrix_x = MatrixGateBuilder(np.array([[0, 1], [1, 0]], dtype=complex)).to_gate() + matrix_x.label = 'matrix_x' + circuit = Circuit('matrix-x') + circuit.allocateQubits(1) + circuit << (matrix_x, 0) + + ir = get_compiler('native').compile(circuit, 0) + result = get_basic_simulator().execute(deepcopy(ir), BasicSimulatorConfig()) + + self.assertAlmostEqual(result.probabilities.get('1', 0.0), 1.0) + + +if __name__ == '__main__': + unittest.main() From 0be70f6a0fec8e6fad6eba8a46f1373646dd13f6 Mon Sep 17 00:00:00 2001 From: wuzp15 Date: Wed, 22 Jul 2026 16:26:28 +0800 Subject: [PATCH 04/59] fix basic simulator controlled-y phase --- spinqit/backend/basic_simulator_backend.py | 10 +++++++- tests/test_review_c4_basic_cy.py | 27 ++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 tests/test_review_c4_basic_cy.py diff --git a/spinqit/backend/basic_simulator_backend.py b/spinqit/backend/basic_simulator_backend.py index 8d6fbc3..237ccc9 100644 --- a/spinqit/backend/basic_simulator_backend.py +++ b/spinqit/backend/basic_simulator_backend.py @@ -83,7 +83,15 @@ def assemble(self, ir: IntermediateRepresentation): elif v['name'] == CX.label: v['name'] = 'CNOT' elif v['name'] == CY.label: - v['name'] = 'YCON' + qubits, clbits = self.__qubits_and_clbits(v) + subgates = [ + Instruction(Sd, [qubits[1]], clbits), + Instruction(CX, qubits, clbits), + Instruction(S, [qubits[1]], clbits), + ] + ir.substitute_nodes([v.index], subgates, v['type']) + ir.remove_nodes([v.index], False) + i -= 1 elif v['name'] == CZ.label: v['name'] = 'ZCON' elif v['name'] == CCX.label: diff --git a/tests/test_review_c4_basic_cy.py b/tests/test_review_c4_basic_cy.py new file mode 100644 index 0000000..bd2ff41 --- /dev/null +++ b/tests/test_review_c4_basic_cy.py @@ -0,0 +1,27 @@ +from copy import deepcopy +import unittest + +import numpy as np + +from spinqit import BasicSimulatorConfig, CY, Circuit, H, get_basic_simulator, get_compiler + + +class BasicSimulatorCyRegressionTest(unittest.TestCase): + def test_cy_applies_controlled_y_phase(self): + circuit = Circuit('controlled-y') + circuit.allocateQubits(2) + circuit << (H, 0) + circuit << (CY, [0, 1]) + + ir = get_compiler('native').compile(circuit, 0) + observed = np.asarray( + get_basic_simulator().execute(deepcopy(ir), BasicSimulatorConfig()).states, + dtype=complex, + ) + expected = np.array([1, 0, 0, 1j], dtype=complex) / np.sqrt(2) + + self.assertGreaterEqual(abs(np.vdot(expected, observed)) ** 2, 1 - 1e-9) + + +if __name__ == '__main__': + unittest.main() From 91045ec8fe17070daecc1f98fa34eaa8d7ccda3f Mon Sep 17 00:00:00 2001 From: wuzp15 Date: Wed, 22 Jul 2026 16:27:06 +0800 Subject: [PATCH 05/59] fix qiskit condition leakage --- .../compiler/translator/qiskit_to_spinq.py | 15 +++++++------- tests/test_review_c5_qiskit_condition.py | 20 +++++++++++++++++++ 2 files changed, 27 insertions(+), 8 deletions(-) create mode 100644 tests/test_review_c5_qiskit_condition.py diff --git a/spinqit/compiler/translator/qiskit_to_spinq.py b/spinqit/compiler/translator/qiskit_to_spinq.py index a471c8e..dbc35e3 100644 --- a/spinqit/compiler/translator/qiskit_to_spinq.py +++ b/spinqit/compiler/translator/qiskit_to_spinq.py @@ -67,13 +67,14 @@ def add_instruction(qc: qqc.QuantumCircuit, circ: Circuit, qreg_map: Dict, creg_ pass for instruction, qargs, cargs in qc.data: + instruction_condition = condition if instruction.condition is not None: classical = instruction.condition[0] if isinstance(classical, (qqc.Clbit, ext_Clbit)): clbits = [creg_map[classical.register.name][classical.index]] else: clbits = creg_map[classical.name] - condition = (clbits, '==', int(instruction.condition[1])) + instruction_condition = (clbits, '==', int(instruction.condition[1])) if not isinstance(instruction, (qqc.Gate, qqc.Instruction, qqc.Measure, qqc.Barrier, ext_Gate, ext_Instruction, ext_Measure, ext_Barrier)): @@ -100,15 +101,15 @@ def add_instruction(qc: qqc.QuantumCircuit, circ: Circuit, qreg_map: Dict, creg_ params = instruction.params if params is not None and len(params) > 0: - if condition is not None: - circ<< (gate, qlist, params) | condition + if instruction_condition is not None: + circ<< (gate, qlist, params) | instruction_condition else: circ<< (gate, qlist, params) else: - if condition is not None: + if instruction_condition is not None: if gate == MEASURE: raise UnsupportedQiskitInstructionError('Measure cannot be conditional.') - circ<< (gate, qlist) | condition + circ<< (gate, qlist) | instruction_condition else: if gate == MEASURE: circ.measure(qlist, clist) @@ -117,6 +118,4 @@ def add_instruction(qc: qqc.QuantumCircuit, circ: Circuit, qreg_map: Dict, creg_ elif isinstance(instruction, (qqc.Barrier, ext_Barrier)): continue else: - add_instruction(instruction.definition, circ, qreg_map, creg_map, sub_qubits, condition) - - \ No newline at end of file + add_instruction(instruction.definition, circ, qreg_map, creg_map, sub_qubits, instruction_condition) diff --git a/tests/test_review_c5_qiskit_condition.py b/tests/test_review_c5_qiskit_condition.py new file mode 100644 index 0000000..631be61 --- /dev/null +++ b/tests/test_review_c5_qiskit_condition.py @@ -0,0 +1,20 @@ +import unittest + +from spinqit.compiler.translator.qiskit_to_spinq import qiskit_to_spinq +from spinqit.qiskit import QuantumCircuit + + +class QiskitConditionRegressionTest(unittest.TestCase): + def test_condition_does_not_leak_to_following_instruction(self): + source = QuantumCircuit(1, 1) + source.x(0).c_if(source.cregs[0], 1) + source.h(0) + + translated = qiskit_to_spinq(source) + + self.assertIsNotNone(translated.instructions[0].condition) + self.assertIsNone(translated.instructions[1].condition) + + +if __name__ == '__main__': + unittest.main() From a17d4fc56a74f7598eb1a0570e8170c6239d323d Mon Sep 17 00:00:00 2001 From: wuzp15 Date: Wed, 22 Jul 2026 16:27:49 +0800 Subject: [PATCH 06/59] fix complex state-vector norm validation --- spinqit/backend/pytorch_backend.py | 5 +++-- tests/test_review_c6_torch_statevector.py | 24 +++++++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 tests/test_review_c6_torch_statevector.py diff --git a/spinqit/backend/pytorch_backend.py b/spinqit/backend/pytorch_backend.py index 02c08b8..638cdda 100644 --- a/spinqit/backend/pytorch_backend.py +++ b/spinqit/backend/pytorch_backend.py @@ -238,9 +238,10 @@ def _state_vector_node(states, qubits_num): else: state = torch.as_tensor(states, device=device, dtype=dtype) - if not torch.allclose((state ** 2).sum().real, torch.tensor(1., dtype=state.real.dtype)): + norm = torch.sum(torch.abs(state) ** 2, dim=-1) + if not torch.allclose(norm, torch.ones_like(norm)): raise ValueError( - f'The `StateVector` is not a quantum state, expected norm(state) to be 1, but got {(state ** 2).sum().real}' + f'The `StateVector` is not a quantum state, expected norm(state) to be 1, but got {norm}' ) if state.size(-1) != 2 ** qubits_num: diff --git a/tests/test_review_c6_torch_statevector.py b/tests/test_review_c6_torch_statevector.py new file mode 100644 index 0000000..39049ec --- /dev/null +++ b/tests/test_review_c6_torch_statevector.py @@ -0,0 +1,24 @@ +import unittest + +from spinqit import Circuit, StateVector, TorchSimulatorConfig, get_compiler, get_torch_simulator + + +class TorchStateVectorRegressionTest(unittest.TestCase): + @staticmethod + def execute(state): + circuit = Circuit('torch-state-vector') + qubits = circuit.allocateQubits(1) + circuit << (StateVector, qubits, state) + ir = get_compiler('native').compile(circuit, 0) + return get_torch_simulator().execute(ir, TorchSimulatorConfig()) + + def test_norm_uses_complex_magnitude(self): + valid = self.execute([2 ** -0.5, 1j * 2 ** -0.5]) + self.assertAlmostEqual(float(valid.raw_probabilities.sum().item()), 1.0, places=6) + + with self.assertRaises(ValueError): + self.execute([2 ** 0.5, 1j]) + + +if __name__ == '__main__': + unittest.main() From 247649ce0fc75cda03e61e56003065b847b5d5fa Mon Sep 17 00:00:00 2001 From: wuzp15 Date: Wed, 22 Jul 2026 16:28:42 +0800 Subject: [PATCH 07/59] fix qaoa Hamiltonian coefficient scaling --- spinqit/algorithm/qaoa.py | 10 +++++++--- tests/test_review_a1_qaoa_coefficients.py | 24 +++++++++++++++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) create mode 100644 tests/test_review_a1_qaoa_coefficients.py diff --git a/spinqit/algorithm/qaoa.py b/spinqit/algorithm/qaoa.py index ea96ab9..6778036 100644 --- a/spinqit/algorithm/qaoa.py +++ b/spinqit/algorithm/qaoa.py @@ -66,18 +66,22 @@ def _generate_problem_circuit(self, ham) -> Gate: 'The problem hamiltonian should be given in list in __init__().' ) for i in range(len(ham)): + coefficient = ham[i][1] + rotation_params = lambda x, coefficient=coefficient: [ + coefficient * value for value in x + ] if 'Z' in ham[i][0]: qubits = [idx for idx in range(len(ham[i][0])) if ham[i][0][idx] == 'Z'] rzz = Z_IsingGateBuilder(len(qubits)).to_gate() - builder.append(rzz, qubits, lambda x: x) + builder.append(rzz, qubits, rotation_params) elif 'X' in ham[i][0]: qubits = [idx for idx in range(len(ham[i][0])) if ham[i][0][idx] == 'X'] rxx = X_IsingGateBuilder(len(qubits)).to_gate() - builder.append(rxx, qubits, lambda x: x) + builder.append(rxx, qubits, rotation_params) elif 'Y' in ham[i][0]: qubits = [idx for idx in range(len(ham[i][0])) if ham[i][0][idx] == 'Y'] ryy = Y_IsingGateBuilder(len(qubits)).to_gate() - builder.append(ryy, qubits, lambda x: x) + builder.append(ryy, qubits, rotation_params) return builder.to_gate() def _generate_mixer_circuit(self) -> Gate: diff --git a/tests/test_review_a1_qaoa_coefficients.py b/tests/test_review_a1_qaoa_coefficients.py new file mode 100644 index 0000000..98087e0 --- /dev/null +++ b/tests/test_review_a1_qaoa_coefficients.py @@ -0,0 +1,24 @@ +import unittest + +import numpy as np + +from spinqit.algorithm.qaoa import QAOA + + +class QaoaCoefficientRegressionTest(unittest.TestCase): + @staticmethod + def problem_gate(coefficient): + qaoa = object.__new__(QAOA) + qaoa._QAOA__qubit_num = 2 + return qaoa._generate_problem_circuit([('ZZ', coefficient)]) + + def test_problem_rotation_scales_by_hamiltonian_coefficient(self): + angle_1 = self.problem_gate(1.0).factors[0][2]([0.6]) + angle_7 = self.problem_gate(7.0).factors[0][2]([0.6]) + + np.testing.assert_allclose(angle_1, [0.6]) + np.testing.assert_allclose(angle_7, [4.2]) + + +if __name__ == '__main__': + unittest.main() From 90cf81dcf0d742a3d1eadbce14afdfe04374f44e Mon Sep 17 00:00:00 2001 From: wuzp15 Date: Wed, 22 Jul 2026 16:29:29 +0800 Subject: [PATCH 08/59] fix optimizer endpoint propagation --- spinqit/algorithm/optimizer/scipy_optim.py | 7 ++-- spinqit/algorithm/optimizer/spsa.py | 11 +++--- tests/test_review_a2_optimizer_endpoints.py | 40 +++++++++++++++++++++ 3 files changed, 50 insertions(+), 8 deletions(-) create mode 100644 tests/test_review_a2_optimizer_endpoints.py diff --git a/spinqit/algorithm/optimizer/scipy_optim.py b/spinqit/algorithm/optimizer/scipy_optim.py index de97332..e6924f7 100644 --- a/spinqit/algorithm/optimizer/scipy_optim.py +++ b/spinqit/algorithm/optimizer/scipy_optim.py @@ -46,7 +46,8 @@ def callback_fn(x): print('Optimize: step {}, loss: {},'.format(len(loss_list) + 1, loss)) loss_list.append(loss) - minimize(fn, x0=params[0], - method=self.method, options=self.options, callback=callback_fn, - tol=self.tol, **self.kwargs) + result = minimize(fn, x0=params[0], + method=self.method, options=self.options, callback=callback_fn, + tol=self.tol, **self.kwargs) + params[0][...] = result.x return loss_list diff --git a/spinqit/algorithm/optimizer/spsa.py b/spinqit/algorithm/optimizer/spsa.py index 894da1d..a5e2e7e 100644 --- a/spinqit/algorithm/optimizer/spsa.py +++ b/spinqit/algorithm/optimizer/spsa.py @@ -43,9 +43,10 @@ def callback_fn(x): print('Optimize: step {}, loss: {},'.format(len(loss_list) + 1, loss)) loss_list.append(loss) - minimizeSPSA(qlayer, *params, - niter=self.niter, - paired=False, - callback=callback_fn, - c=self.c, a=self.a, **self.kwargs) + result = minimizeSPSA(qlayer, *params, + niter=self.niter, + paired=False, + callback=callback_fn, + c=self.c, a=self.a, **self.kwargs) + params[0][...] = result.x return loss_list diff --git a/tests/test_review_a2_optimizer_endpoints.py b/tests/test_review_a2_optimizer_endpoints.py new file mode 100644 index 0000000..5135659 --- /dev/null +++ b/tests/test_review_a2_optimizer_endpoints.py @@ -0,0 +1,40 @@ +from types import SimpleNamespace +import importlib +import unittest +from unittest.mock import patch + +import numpy as np + +from spinqit import Parameter +from spinqit.algorithm.optimizer.scipy_optim import ScipyOptimizer +from spinqit.algorithm.optimizer.spsa import SPSAOptimizer + + +class OptimizerEndpointRegressionTest(unittest.TestCase): + def test_optimizers_write_endpoint_back_to_parameter(self): + loss = lambda params: float((params[0] - 3.0) ** 2) + scipy_module = importlib.import_module('spinqit.algorithm.optimizer.scipy_optim') + spsa_module = importlib.import_module('spinqit.algorithm.optimizer.spsa') + + scipy_params = Parameter([10.0]) + with patch.object( + scipy_module, + 'minimize', + return_value=SimpleNamespace(x=np.array([3.0])), + ): + ScipyOptimizer().optimize(loss, scipy_params) + + spsa_params = Parameter([10.0]) + with patch.object( + spsa_module, + 'minimizeSPSA', + return_value=SimpleNamespace(x=np.array([3.0])), + ): + SPSAOptimizer().optimize(loss, spsa_params) + + np.testing.assert_allclose(scipy_params, [3.0]) + np.testing.assert_allclose(spsa_params, [3.0]) + + +if __name__ == '__main__': + unittest.main() From 075b827f74dc26899815c839127834834891ae3b Mon Sep 17 00:00:00 2001 From: wuzp15 Date: Wed, 22 Jul 2026 16:30:13 +0800 Subject: [PATCH 09/59] fix factorless matrix gate inversion --- spinqit/model/inverse_builder.py | 6 +++++- tests/test_review_a3_matrix_inverse.py | 19 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 tests/test_review_a3_matrix_inverse.py diff --git a/spinqit/model/inverse_builder.py b/spinqit/model/inverse_builder.py index be091b9..13687fd 100644 --- a/spinqit/model/inverse_builder.py +++ b/spinqit/model/inverse_builder.py @@ -13,7 +13,7 @@ # limitations under the License. from typing import List from .basic_gate import Gate -from .matrix_gate import MatrixGate +from .matrix_gate import MatrixGate, MatrixGateBuilder from .controlled_gate import ControlledGate from .gates import I, H, X, Y, Z, Rx, Ry, Rz, P, T, Td, S, Sd, CX, CY, CZ, SWAP, MEASURE, BARRIER from .instruction import Instruction @@ -52,6 +52,10 @@ def _inverse(self, g: Gate) -> Gate: return Sd elif g == Sd: return S + elif isinstance(g, MatrixGate): + inverse = MatrixGateBuilder(lambda params: g.get_matrix(*params).conj().T).to_gate() + inverse.label = g.label + '_inv' + return inverse elif len(g.factors) > 0: inv_gate = InverseGate(g) for f in g.factors: diff --git a/tests/test_review_a3_matrix_inverse.py b/tests/test_review_a3_matrix_inverse.py new file mode 100644 index 0000000..1782039 --- /dev/null +++ b/tests/test_review_a3_matrix_inverse.py @@ -0,0 +1,19 @@ +import unittest + +import numpy as np + +from spinqit import H, InverseBuilder, MatrixGateBuilder + + +class MatrixGateInverseRegressionTest(unittest.TestCase): + def test_factorless_matrix_gate_has_conjugate_transpose_inverse(self): + gate = MatrixGateBuilder(H.get_matrix()).to_gate() + + inverse = InverseBuilder(gate).to_gate() + + self.assertIsNotNone(inverse) + np.testing.assert_allclose(inverse.get_matrix(), H.get_matrix().conj().T) + + +if __name__ == '__main__': + unittest.main() From b351cef1d4d42b533e78fa755cce52c14d148d1b Mon Sep 17 00:00:00 2001 From: wuzp15 Date: Wed, 22 Jul 2026 16:31:07 +0800 Subject: [PATCH 10/59] fix sat string expression parsing --- spinqit/solver/sat.py | 2 +- tests/test_review_a4_sat_string.py | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 tests/test_review_a4_sat_string.py diff --git a/spinqit/solver/sat.py b/spinqit/solver/sat.py index e8cdfe1..ae0b261 100644 --- a/spinqit/solver/sat.py +++ b/spinqit/solver/sat.py @@ -36,6 +36,7 @@ def to_sym_expr(self, str_expr: str): variables = set(re.findall(r'[a-zA-Z]', str_expr)) locals = {v: symbols(v) for v in variables} expr = parse_expr(str_expr, local_dict={**locals, 'And': And, 'Or': Or, 'Not': Not}) + return expr def calc_qubit_number(self) -> Tuple: sym_set = self.expr.atoms(Symbol) @@ -131,4 +132,3 @@ def solve(self, backend_mode, **kwargs): if simplified_result == True: return assignment print('There is no solution for this SAT problem.') - \ No newline at end of file diff --git a/tests/test_review_a4_sat_string.py b/tests/test_review_a4_sat_string.py new file mode 100644 index 0000000..c805e40 --- /dev/null +++ b/tests/test_review_a4_sat_string.py @@ -0,0 +1,17 @@ +import unittest + +from sympy.logic.boolalg import Boolean + +from spinqit.solver import SATSolver + + +class SatStringRegressionTest(unittest.TestCase): + def test_string_expression_constructs_solver(self): + solver = SATSolver('(a | b) & (~a | b)') + + self.assertIsInstance(solver.expr, Boolean) + self.assertIsNotNone(solver.circuit) + + +if __name__ == '__main__': + unittest.main() From 372f29154910c3e39f5c92aac105d944923a365e Mon Sep 17 00:00:00 2001 From: wuzp15 Date: Wed, 22 Jul 2026 16:31:48 +0800 Subject: [PATCH 11/59] fix cloud backend host configuration --- spinqit/backend/backend.py | 3 ++- tests/test_review_b1_cloud_host.py | 24 ++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 tests/test_review_b1_cloud_host.py diff --git a/spinqit/backend/backend.py b/spinqit/backend/backend.py index 2531e91..bc0d791 100644 --- a/spinqit/backend/backend.py +++ b/spinqit/backend/backend.py @@ -73,7 +73,8 @@ def check_backend_and_config(backend_mode, **kwargs): from .spinq_cloud_backend import SpinQCloudConfig username = kwargs.get('username', None) keyfile = kwargs.get('keyfile', None) - backend = SpinQCloudBackend(username, keyfile) + host = kwargs.get('host', 'http://cloud.spinq.cn:6060') + backend = SpinQCloudBackend(username, keyfile, host) config = SpinQCloudConfig() platform = kwargs.get('platform', 'triangulum_vp') shots = kwargs.get('shots', 1024) diff --git a/tests/test_review_b1_cloud_host.py b/tests/test_review_b1_cloud_host.py new file mode 100644 index 0000000..3a37393 --- /dev/null +++ b/tests/test_review_b1_cloud_host.py @@ -0,0 +1,24 @@ +import importlib +import unittest +from unittest.mock import patch + +from spinqit.backend.backend import check_backend_and_config + + +class CloudHostRegressionTest(unittest.TestCase): + def test_cloud_factory_passes_configured_host(self): + backend_module = importlib.import_module('spinqit.backend.backend') + with patch.object(backend_module, 'SpinQCloudBackend') as backend_class: + backend, _ = check_backend_and_config( + 'cloud', + username='offline', + keyfile='/unused', + host='https://example.invalid', + ) + + backend_class.assert_called_once_with('offline', '/unused', 'https://example.invalid') + self.assertIs(backend, backend_class.return_value) + + +if __name__ == '__main__': + unittest.main() From 58af7511b05f55ce32c26876d306fcdc0184494a Mon Sep 17 00:00:00 2001 From: wuzp15 Date: Wed, 22 Jul 2026 16:32:11 +0800 Subject: [PATCH 12/59] fix cloud task lifecycle flags --- spinqit/backend/spinq_cloud_backend.py | 4 ++- tests/test_review_b2_cloud_flags.py | 42 ++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 tests/test_review_b2_cloud_flags.py diff --git a/spinqit/backend/spinq_cloud_backend.py b/spinqit/backend/spinq_cloud_backend.py index dde4c74..a56ae28 100644 --- a/spinqit/backend/spinq_cloud_backend.py +++ b/spinqit/backend/spinq_cloud_backend.py @@ -297,6 +297,8 @@ def get_platform(self, code: str) -> Platform: def submit_task(self, ir: IntermediateRepresentation, config: SpinQCloudConfig, calc_matrix: bool = False, process_now: bool = True, debug: bool = False): # get params from config + calc_matrix = config.metadata.get('calc_matrix', calc_matrix) + process_now = config.metadata.get('process_now', process_now) platform_code = config.metadata['platform'] log_to_phy = None if 'keep_layout' in config.metadata and config.metadata['keep_layout'] is not None: @@ -571,4 +573,4 @@ def check_node(self, ir, place_holder): record_function.append(p.get_function(place_holder)) else: record_function.append(p) - v['func'] = record_function if len(record_function) > 0 else None \ No newline at end of file + v['func'] = record_function if len(record_function) > 0 else None diff --git a/tests/test_review_b2_cloud_flags.py b/tests/test_review_b2_cloud_flags.py new file mode 100644 index 0000000..de0131f --- /dev/null +++ b/tests/test_review_b2_cloud_flags.py @@ -0,0 +1,42 @@ +import contextlib +import io +import json +import unittest + +from spinqit import Circuit, H, get_compiler +from spinqit.backend.spinq_cloud_backend import SpinQCloudBackend, SpinQCloudConfig +from spinqit.model.spinqCloud.gate import H as CloudH +from spinqit.model.spinqCloud.platform import Platform + + +class CloudFlagsRegressionTest(unittest.TestCase): + def test_submit_uses_density_matrix_and_process_flags_from_config(self): + circuit = Circuit('cloud-flags') + circuit.allocateQubits(1) + circuit << (H, 0) + ir = get_compiler('native').compile(circuit, 0) + + backend = object.__new__(SpinQCloudBackend) + backend._api_client = None + backend._platforms = [ + Platform( + 'offline', 'offline', 1, machine_count=0, gate_list=[CloudH], + coupling_map=[], simu=True, active_qubits=[0], + ) + ] + config = SpinQCloudConfig() + config.configure_platform('offline') + config.configure_density_matrix(True) + config.configure_process_now(False) + + output = io.StringIO() + with contextlib.redirect_stdout(output): + backend.submit_task(ir, config, debug=True) + request = json.loads(output.getvalue().splitlines()[-1]) + + self.assertIs(request['calcMatrix'], True) + self.assertIs(request['proceedNow'], False) + + +if __name__ == '__main__': + unittest.main() From 4eaea23ce8f1baa2d10a46b927eda378fe3710f0 Mon Sep 17 00:00:00 2001 From: wuzp15 Date: Wed, 22 Jul 2026 16:32:34 +0800 Subject: [PATCH 13/59] fix cloud task reconstruction --- spinqit/backend/spinq_cloud_backend.py | 10 +++++- tests/test_review_b3_cloud_get_task.py | 49 ++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 tests/test_review_b3_cloud_get_task.py diff --git a/spinqit/backend/spinq_cloud_backend.py b/spinqit/backend/spinq_cloud_backend.py index a56ae28..d1dfe37 100644 --- a/spinqit/backend/spinq_cloud_backend.py +++ b/spinqit/backend/spinq_cloud_backend.py @@ -365,7 +365,15 @@ def get_task(self, task_code: str): if res: res_entity = json.loads(res.content) task_dict = res_entity["task"] - task = Task(task_dict["tname"], task_dict["platformCode"], None, task_dict["calcMatrix"], task_dict["shots"], description=task_dict["description"] ) + task = Task( + name=task_dict["tname"], + platform_code=task_dict["platformCode"], + bitNum=task_dict["bitNum"], + clbitnum=task_dict["clbitNum"], + calc_matrix=task_dict["calcMatrix"], + shots=task_dict["shots"], + description=task_dict["description"], + ) task.set_task_code(task_dict["tcode"]) task.set_status(task_dict["tstatus"]) if task_dict["createdTime"] is not None: diff --git a/tests/test_review_b3_cloud_get_task.py b/tests/test_review_b3_cloud_get_task.py new file mode 100644 index 0000000..2d8df1b --- /dev/null +++ b/tests/test_review_b3_cloud_get_task.py @@ -0,0 +1,49 @@ +import json +import unittest + +from spinqit.backend.spinq_cloud_backend import SpinQCloudBackend + + +class CloudGetTaskRegressionTest(unittest.TestCase): + def test_get_task_reconstructs_task_fields(self): + class Response: + status_code = 200 + content = json.dumps({ + 'task': { + 'tname': 'demo', + 'platformCode': 'offline', + 'bitNum': 2, + 'clbitNum': 1, + 'calcMatrix': True, + 'shots': 100, + 'description': 'offline fixture', + 'tcode': 'T-1', + 'tstatus': 'S', + 'createdTime': None, + } + }).encode() + + def __bool__(self): + return True + + class Client: + @staticmethod + def get_task_by_code(_task_code): + return Response() + + backend = object.__new__(SpinQCloudBackend) + backend._api_client = Client() + + task = backend.get_task('T-1') + values = task.to_dict() + + self.assertEqual(values['bitNum'], 2) + self.assertEqual(values['clbitNum'], 1) + self.assertEqual(values['shots'], 100) + self.assertIs(values['calc_matrix'], True) + self.assertEqual(task.task_code, 'T-1') + self.assertEqual(task.status, 'S') + + +if __name__ == '__main__': + unittest.main() From 345bb89952c8622a226d585d8aecd20e97e9ba34 Mon Sep 17 00:00:00 2001 From: wuzp15 Date: Wed, 22 Jul 2026 16:33:10 +0800 Subject: [PATCH 14/59] fix qasm probability measurement config --- spinqit/backend/qasm_backend.py | 7 +++--- tests/test_review_b4_qasm_probabilities.py | 26 ++++++++++++++++++++++ 2 files changed, 29 insertions(+), 4 deletions(-) create mode 100644 tests/test_review_b4_qasm_probabilities.py diff --git a/spinqit/backend/qasm_backend.py b/spinqit/backend/qasm_backend.py index d2e7c69..0894986 100644 --- a/spinqit/backend/qasm_backend.py +++ b/spinqit/backend/qasm_backend.py @@ -178,10 +178,9 @@ def evaluate(self, ir, config, measure_op): psi = onp.array(res.states) value = onp.real(psi.conj() @ hamiltonian @ psi) elif measure_op.mtype == 'prob': - if 'mqubits' in config.metadata: - np_probs = onp.zeros(2 ** (len(config.metadata['mqubits']))) - else: - np_probs = onp.zeros(2 ** ir.qnum, dtype=float) + measured_qubits = config.mqubits + probability_qubits = len(measured_qubits) if measured_qubits is not None else ir.qnum + np_probs = onp.zeros(2 ** probability_qubits, dtype=float) for k, v in res.probabilities.items(): idx = int(k, 2) np_probs[idx] = v diff --git a/tests/test_review_b4_qasm_probabilities.py b/tests/test_review_b4_qasm_probabilities.py new file mode 100644 index 0000000..3779738 --- /dev/null +++ b/tests/test_review_b4_qasm_probabilities.py @@ -0,0 +1,26 @@ +import unittest + +import numpy as np + +from spinqit import Circuit, H, QiskitQasmResult +from spinqit.algorithm.loss import probs +from spinqit.interface.qlayer import QLayer + + +class QasmProbabilityRegressionTest(unittest.TestCase): + def test_probability_measurement_uses_qasm_config_measure_qubits(self): + def executor(_qasm, shots): + result = QiskitQasmResult() + result.set_result({'0': shots}, shots) + return result + + circuit = Circuit('qasm-probabilities') + circuit.allocateQubits(1) + circuit << (H, 0) + layer = QLayer(circuit, probs(), backend_mode='qasm', backend_fn=executor) + + np.testing.assert_allclose(layer(), [1.0, 0.0]) + + +if __name__ == '__main__': + unittest.main() From 1103b144b59e62f6b4ac39cf4a63e4cf627b30ec Mon Sep 17 00:00:00 2001 From: wuzp15 Date: Wed, 22 Jul 2026 16:49:41 +0800 Subject: [PATCH 15/59] fix rewritten gate operand ordering --- spinqit/backend/basic_simulator_backend.py | 6 ++---- tests/test_review_c4_basic_cy.py | 22 +++++++++++++++++++++- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/spinqit/backend/basic_simulator_backend.py b/spinqit/backend/basic_simulator_backend.py index 237ccc9..faf4158 100644 --- a/spinqit/backend/basic_simulator_backend.py +++ b/spinqit/backend/basic_simulator_backend.py @@ -106,12 +106,10 @@ def assemble(self, ir: IntermediateRepresentation): def __qubits_and_clbits(v): edges = v.in_edges() edges.sort(key=lambda k: k.index) - qubits = [] + qubits = list(v['qubits']) clbits = [] for e in edges: - if 'qubit' in e.attributes() and e['qubit'] is not None: - qubits.append(e['qubit']) - elif 'clbit' in e.attributes() and e['clbit'] is not None: + if 'clbit' in e.attributes() and e['clbit'] is not None: clbits.append(e['clbit']) return qubits, clbits diff --git a/tests/test_review_c4_basic_cy.py b/tests/test_review_c4_basic_cy.py index bd2ff41..73f0fe9 100644 --- a/tests/test_review_c4_basic_cy.py +++ b/tests/test_review_c4_basic_cy.py @@ -3,7 +3,7 @@ import numpy as np -from spinqit import BasicSimulatorConfig, CY, Circuit, H, get_basic_simulator, get_compiler +from spinqit import BasicSimulatorConfig, CY, Circuit, H, Rz, X, Z, get_basic_simulator, get_compiler class BasicSimulatorCyRegressionTest(unittest.TestCase): @@ -22,6 +22,26 @@ def test_cy_applies_controlled_y_phase(self): self.assertGreaterEqual(abs(np.vdot(expected, observed)) ** 2, 1 - 1e-9) + def test_optimizer_rewrites_do_not_reverse_cy_operands(self): + circuit = Circuit('optimized-controlled-y') + circuit.allocateQubits(2) + circuit << (Rz, 0, 0.2) + circuit << (Z, 0) + circuit << (X, 1) + circuit << (CY, [0, 1]) + + compiler = get_compiler('native') + config = BasicSimulatorConfig() + + def simulate(level): + ir = compiler.compile(circuit, level) + state = np.asarray(get_basic_simulator().execute(deepcopy(ir), config).states) + return state / np.linalg.norm(state) + + reference = simulate(0) + candidate = simulate(1) + self.assertGreaterEqual(abs(np.vdot(reference, candidate)) ** 2, 1 - 1e-9) + if __name__ == '__main__': unittest.main() From ee8e5a29b4a512ac0afeaccec28a7c41c2998fbe Mon Sep 17 00:00:00 2001 From: wuzp15 Date: Wed, 22 Jul 2026 16:50:12 +0800 Subject: [PATCH 16/59] disable unsafe pure-state optimization --- spinqit/compiler/optimizer/pass_manager.py | 2 -- tests/test_review_c2_pure_state_swap.py | 21 ++++++++++++++++++++- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/spinqit/compiler/optimizer/pass_manager.py b/spinqit/compiler/optimizer/pass_manager.py index b2b17b6..42332fa 100644 --- a/spinqit/compiler/optimizer/pass_manager.py +++ b/spinqit/compiler/optimizer/pass_manager.py @@ -17,7 +17,6 @@ from .collapse_single_qubit_gates import CollapseSingleQubitGates from .collapse_two_qubit_gates import CollapseTwoQubitGates from .quantum_basis_state_optimization import ConstantsStateOptimization -from .quantum_pure_state_optimization import PureStateOnU class PassManager(object): def __init__(self, level: int): @@ -32,7 +31,6 @@ def __init__(self, level: int): elif level == 3: self.passes.append(CancelRedundantGates()) self.passes.append(ConstantsStateOptimization()) - self.passes.append(PureStateOnU()) self.passes.append(CollapseSingleQubitGates()) self.passes.append(CollapseTwoQubitGates()) diff --git a/tests/test_review_c2_pure_state_swap.py b/tests/test_review_c2_pure_state_swap.py index a5954cf..a23d102 100644 --- a/tests/test_review_c2_pure_state_swap.py +++ b/tests/test_review_c2_pure_state_swap.py @@ -3,7 +3,7 @@ import numpy as np -from spinqit import BasicSimulatorConfig, Circuit, Ry, SWAP, X, get_basic_simulator, get_compiler +from spinqit import BasicSimulatorConfig, Circuit, Ry, Rz, SWAP, X, get_basic_simulator, get_compiler class PureStateSwapRegressionTest(unittest.TestCase): @@ -29,6 +29,25 @@ def simulate(level): candidate = simulate(3) self.assertGreaterEqual(abs(np.vdot(reference, candidate)) ** 2, 1 - 1e-9) + def test_level_three_preserves_general_product_state_swap(self): + circuit = Circuit('general-product-state-swap') + circuit.allocateQubits(2) + circuit << (Ry, 0, -8.157927560590963) + circuit << (Rz, 1, 10.972138484450106) + circuit << (SWAP, [0, 1]) + + compiler = get_compiler('native') + config = BasicSimulatorConfig() + + def simulate(level): + ir = compiler.compile(circuit, level) + state = np.asarray(get_basic_simulator().execute(deepcopy(ir), config).states) + return state / np.linalg.norm(state) + + reference = simulate(0) + candidate = simulate(3) + self.assertGreaterEqual(abs(np.vdot(reference, candidate)) ** 2, 1 - 1e-9) + if __name__ == '__main__': unittest.main() From 768eab8e792d168f7abfa607f4584ff0e39a5ba3 Mon Sep 17 00:00:00 2001 From: wuzp15 Date: Wed, 22 Jul 2026 16:50:35 +0800 Subject: [PATCH 17/59] fix one-qubit amplitude amplification --- spinqit/primitive/amplitude_amplification.py | 4 ++-- tests/test_review_a3_matrix_inverse.py | 18 +++++++++++++++++- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/spinqit/primitive/amplitude_amplification.py b/spinqit/primitive/amplitude_amplification.py index 5152773..58ad97c 100644 --- a/spinqit/primitive/amplitude_amplification.py +++ b/spinqit/primitive/amplitude_amplification.py @@ -45,7 +45,7 @@ def build(self) -> List[Instruction]: inst_list.append(Instruction(xBuilder.to_gate(), self.__reflection_qubits)) if len(self.__reflection_qubits) == 1: - inst_list.append(Instruction(Z, self.__reflection_qubits[0])) + inst_list.append(Instruction(Z, [self.__reflection_qubits[0]])) else: mcz_builder = MultiControlledGateBuilder(len(self.__reflection_qubits) - 1, gate=Z) inst_list.append(Instruction(mcz_builder.to_gate(), self.__reflection_qubits, [])) @@ -53,4 +53,4 @@ def build(self) -> List[Instruction]: inst_list.append(Instruction(xBuilder.to_gate(), self.__reflection_qubits)) inst_list.append(Instruction(self.__state_operator, self.__state_qubits, [], self.__state_params)) - return inst_list \ No newline at end of file + return inst_list diff --git a/tests/test_review_a3_matrix_inverse.py b/tests/test_review_a3_matrix_inverse.py index 1782039..995b654 100644 --- a/tests/test_review_a3_matrix_inverse.py +++ b/tests/test_review_a3_matrix_inverse.py @@ -2,7 +2,8 @@ import numpy as np -from spinqit import H, InverseBuilder, MatrixGateBuilder +from spinqit import Circuit, H, InverseBuilder, MatrixGateBuilder, Z, get_compiler +from spinqit.primitive import AmplitudeAmplification class MatrixGateInverseRegressionTest(unittest.TestCase): @@ -14,6 +15,21 @@ def test_factorless_matrix_gate_has_conjugate_transpose_inverse(self): self.assertIsNotNone(inverse) np.testing.assert_allclose(inverse.get_matrix(), H.get_matrix().conj().T) + def test_matrix_state_operator_builds_one_qubit_amplitude_amplification(self): + state_operator = MatrixGateBuilder(H.get_matrix()).to_gate() + + instructions = AmplitudeAmplification( + Z, + [0], + state_operator=state_operator, + state_qubits=[0], + ).build() + circuit = Circuit('one-qubit-amplitude-amplification') + circuit.allocateQubits(1) + circuit.extend(instructions) + + get_compiler('native').compile(circuit, 0) + if __name__ == '__main__': unittest.main() From 409b1f616b5721d751791d28a00832f731b804c9 Mon Sep 17 00:00:00 2001 From: wuzp15 Date: Wed, 22 Jul 2026 16:52:21 +0800 Subject: [PATCH 18/59] disable unsafe constant-state optimization --- spinqit/compiler/optimizer/pass_manager.py | 2 -- tests/test_review_c2_pure_state_swap.py | 21 ++++++++++++++++++++- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/spinqit/compiler/optimizer/pass_manager.py b/spinqit/compiler/optimizer/pass_manager.py index 42332fa..270f341 100644 --- a/spinqit/compiler/optimizer/pass_manager.py +++ b/spinqit/compiler/optimizer/pass_manager.py @@ -16,7 +16,6 @@ from .cancel_redundant_gates import CancelRedundantGates from .collapse_single_qubit_gates import CollapseSingleQubitGates from .collapse_two_qubit_gates import CollapseTwoQubitGates -from .quantum_basis_state_optimization import ConstantsStateOptimization class PassManager(object): def __init__(self, level: int): @@ -30,7 +29,6 @@ def __init__(self, level: int): self.passes.append(CollapseTwoQubitGates()) elif level == 3: self.passes.append(CancelRedundantGates()) - self.passes.append(ConstantsStateOptimization()) self.passes.append(CollapseSingleQubitGates()) self.passes.append(CollapseTwoQubitGates()) diff --git a/tests/test_review_c2_pure_state_swap.py b/tests/test_review_c2_pure_state_swap.py index a23d102..94b8f90 100644 --- a/tests/test_review_c2_pure_state_swap.py +++ b/tests/test_review_c2_pure_state_swap.py @@ -3,7 +3,7 @@ import numpy as np -from spinqit import BasicSimulatorConfig, Circuit, Ry, Rz, SWAP, X, get_basic_simulator, get_compiler +from spinqit import BasicSimulatorConfig, Circuit, H, Ry, Rz, SWAP, T, X, get_basic_simulator, get_compiler class PureStateSwapRegressionTest(unittest.TestCase): @@ -48,6 +48,25 @@ def simulate(level): candidate = simulate(3) self.assertGreaterEqual(abs(np.vdot(reference, candidate)) ** 2, 1 - 1e-9) + def test_level_three_preserves_phased_state_swap(self): + circuit = Circuit('phased-state-swap') + circuit.allocateQubits(2) + circuit << (H, 0) + circuit << (T, 0) + circuit << (SWAP, [0, 1]) + + compiler = get_compiler('native') + config = BasicSimulatorConfig() + + def simulate(level): + ir = compiler.compile(circuit, level) + state = np.asarray(get_basic_simulator().execute(deepcopy(ir), config).states) + return state / np.linalg.norm(state) + + reference = simulate(0) + candidate = simulate(3) + self.assertGreaterEqual(abs(np.vdot(reference, candidate)) ** 2, 1 - 1e-9) + if __name__ == '__main__': unittest.main() From f5471a513a7493ec4f1d78d02f1339aa5500512f Mon Sep 17 00:00:00 2001 From: wuzp15 Date: Wed, 22 Jul 2026 16:54:29 +0800 Subject: [PATCH 19/59] disable failing two-qubit collapse --- spinqit/compiler/optimizer/pass_manager.py | 3 --- tests/test_native_optimizer.py | 24 ++++++++++++++++++++++ 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/spinqit/compiler/optimizer/pass_manager.py b/spinqit/compiler/optimizer/pass_manager.py index 270f341..847bb6e 100644 --- a/spinqit/compiler/optimizer/pass_manager.py +++ b/spinqit/compiler/optimizer/pass_manager.py @@ -15,7 +15,6 @@ from ..ir import IntermediateRepresentation from .cancel_redundant_gates import CancelRedundantGates from .collapse_single_qubit_gates import CollapseSingleQubitGates -from .collapse_two_qubit_gates import CollapseTwoQubitGates class PassManager(object): def __init__(self, level: int): @@ -26,11 +25,9 @@ def __init__(self, level: int): elif level == 2: self.passes.append(CancelRedundantGates()) self.passes.append(CollapseSingleQubitGates()) - self.passes.append(CollapseTwoQubitGates()) elif level == 3: self.passes.append(CancelRedundantGates()) self.passes.append(CollapseSingleQubitGates()) - self.passes.append(CollapseTwoQubitGates()) def append(self, optimizer): self.passes.append(optimizer) diff --git a/tests/test_native_optimizer.py b/tests/test_native_optimizer.py index 89b9a13..b2f6469 100644 --- a/tests/test_native_optimizer.py +++ b/tests/test_native_optimizer.py @@ -6,11 +6,13 @@ from spinqit import ( BasicSimulatorConfig, Circuit, + CX, CZ, H, Rx, Ry, Rz, + SWAP, X, Y, Z, @@ -132,6 +134,28 @@ def test_optimization_levels_preserve_deterministic_sweep(self): fidelity = float(abs(np.vdot(reference, candidate)) ** 2) self.assertGreaterEqual(fidelity, 1.0 - 1e-8) + def test_two_qubit_collapse_does_not_reject_valid_circuit(self): + circuit = Circuit('two-qubit-collapse') + circuit.allocateQubits(2) + for gate, qubits in ( + (CZ, [0, 1]), + (CX, [1, 0]), + (CX, [1, 0]), + (SWAP, [1, 0]), + (CZ, [0, 1]), + (SWAP, [0, 1]), + (SWAP, [0, 1]), + ): + circuit << (gate, qubits) + + compiler = get_compiler('native') + reference = self.simulate(compiler.compile(circuit, 0)) + for level in (2, 3): + with self.subTest(level=level): + candidate = self.simulate(compiler.compile(circuit, level)) + fidelity = float(abs(np.vdot(reference, candidate)) ** 2) + self.assertGreaterEqual(fidelity, 1.0 - 1e-9) + if __name__ == '__main__': unittest.main() From 47522ba7136fdf4b14e5526055415ab42637f77f Mon Sep 17 00:00:00 2001 From: wuzp15 Date: Wed, 22 Jul 2026 17:24:45 +0800 Subject: [PATCH 20/59] fix identity gate optimization --- spinqit/compiler/optimizer/analyze_path.py | 6 ++---- tests/test_review_c1_cancel_redundant.py | 25 +++++++++++++++++++++- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/spinqit/compiler/optimizer/analyze_path.py b/spinqit/compiler/optimizer/analyze_path.py index 6d2950f..e577981 100644 --- a/spinqit/compiler/optimizer/analyze_path.py +++ b/spinqit/compiler/optimizer/analyze_path.py @@ -15,7 +15,7 @@ from igraph import Vertex, Graph from ..ir import NodeType from .util import get_qubits, get_paths -from spinqit.model import I, H, X, Y, Z, Rx, Ry, Rz, T, Td, S, Sd, P, CX, CY, CZ, SWAP, CCX +from spinqit.model import H, X, Y, Z, Rx, Ry, Rz, T, Td, S, Sd, P, CX, CY, CZ, SWAP, CCX X_series = {X.label, Rx.label} Y_series = {Y.label, Ry.label} @@ -48,8 +48,6 @@ def is_commutative(cur: Vertex, pre: Vertex): if cur_qubits != pre_qubits: return False return True - elif cur['name'] == I.label: - return True else: return False @@ -76,4 +74,4 @@ def cancellation_filter(v: int, g: Graph): return is_commutative(g.vs[v], g.vs[prev[0]]) def analyze(g: Graph): - return get_paths(g, cancellation_filter) \ No newline at end of file + return get_paths(g, cancellation_filter) diff --git a/tests/test_review_c1_cancel_redundant.py b/tests/test_review_c1_cancel_redundant.py index ebac595..71f474e 100644 --- a/tests/test_review_c1_cancel_redundant.py +++ b/tests/test_review_c1_cancel_redundant.py @@ -3,10 +3,33 @@ import numpy as np -from spinqit import BasicSimulatorConfig, Circuit, H, Rx, X, get_basic_simulator, get_compiler +from spinqit import BasicSimulatorConfig, Circuit, H, I, Rx, X, get_basic_simulator, get_compiler class CancelRedundantGateRegressionTest(unittest.TestCase): + def test_identity_does_not_join_preceding_cancellation_path(self): + compiler = get_compiler('native') + config = BasicSimulatorConfig() + + for gate in (H, X): + with self.subTest(gate=gate.label): + circuit = Circuit('gate-then-identity') + circuit.allocateQubits(1) + circuit << (gate, 0) + circuit << (I, 0) + + reference = get_basic_simulator().execute( + deepcopy(compiler.compile(circuit, 0)), config + ).states + candidate = get_basic_simulator().execute( + deepcopy(compiler.compile(circuit, 1)), config + ).states + + self.assertGreaterEqual( + abs(np.vdot(reference, candidate)) ** 2, + 1 - 1e-9, + ) + def test_mixed_cancellations_compile_and_preserve_state(self): circuit = Circuit('mixed-cancellations') circuit.allocateQubits(1) From f0fc69fccf7a88a9b3284e9a50b5454120682f33 Mon Sep 17 00:00:00 2001 From: wuzp15 Date: Wed, 22 Jul 2026 17:26:22 +0800 Subject: [PATCH 21/59] fix qasm register gate broadcasting --- spinqit/compiler/qasm/Qasm2EventListener.py | 33 +++++++---------- .../test_review_d2_qasm_register_broadcast.py | 35 +++++++++++++++++++ 2 files changed, 47 insertions(+), 21 deletions(-) create mode 100644 tests/test_review_d2_qasm_register_broadcast.py diff --git a/spinqit/compiler/qasm/Qasm2EventListener.py b/spinqit/compiler/qasm/Qasm2EventListener.py index 99d750a..3fade5d 100644 --- a/spinqit/compiler/qasm/Qasm2EventListener.py +++ b/spinqit/compiler/qasm/Qasm2EventListener.py @@ -272,6 +272,7 @@ def exitQuantumGateCall(self, ctx: Qasm2Parser.QuantumGateCallContext): self.conditional_gates.append(vindex) else: duplicates = set() + gate_qarg_groups = [] for index_id_ctx in index_id_list: id = index_id_ctx.Identifier().getText() if not id in self.qregister_table: @@ -282,7 +283,7 @@ def exitQuantumGateCall(self, ctx: Qasm2Parser.QuantumGateCallContext): qubits = self.qregister_table[id] exp_list_ctx = index_id_ctx.expressionList() if exp_list_ctx is None: - gate_qargs.extend(qubits) + gate_qarg_groups.append(qubits) else: exp_list = exp_list_ctx.expression() if len(exp_list) > 1: @@ -305,26 +306,16 @@ def exitQuantumGateCall(self, ctx: Qasm2Parser.QuantumGateCallContext): raise Exception( f'The register index is out of range in line {ctx.start.line}. ' ) - gate_qargs.append(qubits[index]) - if len(gate_qargs) != self.gate_sym_table[gate_name.lower()]: - start = 0 - while start < len(gate_qargs): - if gate_name.lower() in qasm_basis_map.keys(): - vindex = self.ir.add_op_node(qasm_basis_map[gate_name.lower()].label, - gate_params, - gate_qargs[ - start:start + self.gate_sym_table[gate_name.lower()]], - []) - else: - vindex = self.ir.add_caller_node(gate_name, - gate_params, - gate_qargs[ - start:start + self.gate_sym_table[gate_name.lower()]], - []) - if self.branching: - self.conditional_gates.append(vindex) - start += self.gate_sym_table[gate_name.lower()] - else: + gate_qarg_groups.append([qubits[index]]) + + register_size = max(len(group) for group in gate_qarg_groups) + if any(len(group) not in (1, register_size) for group in gate_qarg_groups): + raise Exception( + f'The register sizes for gate `{gate_name}` do not match in line {ctx.start.line}. ' + ) + for index in range(register_size): + gate_qargs = [group[index] if len(group) > 1 else group[0] + for group in gate_qarg_groups] if gate_name.lower() in qasm_basis_map.keys(): vindex = self.ir.add_op_node(qasm_basis_map[gate_name.lower()].label, gate_params, gate_qargs, []) else: diff --git a/tests/test_review_d2_qasm_register_broadcast.py b/tests/test_review_d2_qasm_register_broadcast.py new file mode 100644 index 0000000..1a7d032 --- /dev/null +++ b/tests/test_review_d2_qasm_register_broadcast.py @@ -0,0 +1,35 @@ +import os +import tempfile +import unittest + +from spinqit import get_compiler +from spinqit.compiler import NodeType + + +class QasmRegisterBroadcastRegressionTest(unittest.TestCase): + def test_register_operands_are_paired_elementwise(self): + source = '''OPENQASM 2.0; +include "qelib1.inc"; +qreg q[2]; +qreg r[2]; +cx q,r; +''' + with tempfile.NamedTemporaryFile('w', suffix='.qasm', delete=False) as qasm_file: + qasm_file.write(source) + path = qasm_file.name + + try: + ir = get_compiler('qasm').compile(path, 0) + finally: + os.unlink(path) + + operands = [ + vertex['qubits'] + for vertex in ir.dag.vs + if vertex['type'] == NodeType.op.value + ] + self.assertEqual(operands, [[0, 2], [1, 3]]) + + +if __name__ == '__main__': + unittest.main() From 18082a7647c538b00886f208250a4b956f02348d Mon Sep 17 00:00:00 2001 From: wuzp15 Date: Wed, 22 Jul 2026 17:27:34 +0800 Subject: [PATCH 22/59] preserve composite gate conditions --- spinqit/compiler/native_compiler.py | 7 ++--- tests/test_review_d3_composite_condition.py | 29 +++++++++++++++++++++ 2 files changed, 33 insertions(+), 3 deletions(-) create mode 100644 tests/test_review_d3_composite_condition.py diff --git a/spinqit/compiler/native_compiler.py b/spinqit/compiler/native_compiler.py index 10b82a7..6ed5d35 100644 --- a/spinqit/compiler/native_compiler.py +++ b/spinqit/compiler/native_compiler.py @@ -54,8 +54,6 @@ def handle_primary_gate(self, ir: IR, inst: Instruction, condition: Tuple): gate = inst.gate if gate in IR.basis_set or gate.label in IR.label_set: vindex = ir.add_op_node(inst.get_op(), inst.params, inst.qubits, inst.clbits) - if condition != None: - ir.add_node_condition(vindex, condition[0], condition[1], condition[2]) elif isinstance(gate, MatrixGate): unitary = gate.get_matrix(*inst.params) if len(gate.factors) == 0: @@ -114,12 +112,15 @@ def handle_primary_gate(self, ir: IR, inst: Instruction, condition: Tuple): ctrl_bits += 2 if len(gate.factors) == 0: - ir.add_unitary_node(gate.label, unitary, inst.qubits, ctrl_bits, inverse_flag) + vindex = ir.add_unitary_node(gate.label, unitary, inst.qubits, ctrl_bits, inverse_flag) else: self.add_definition_cluster(ir, gate, len(inst.params), len(inst.qubits), len(inst.clbits)) vindex = ir.add_caller_node(gate.label, inst.params, inst.qubits) ir.add_caller_matrix(vindex, unitary, ctrl_bits, inverse_flag) + if condition is not None: + ir.add_node_condition(vindex, condition[0], condition[1], condition[2]) + def compile(self, circ: Circuit, level: int) -> IR: self.__gate_definitions = {} ir = IR() diff --git a/tests/test_review_d3_composite_condition.py b/tests/test_review_d3_composite_condition.py new file mode 100644 index 0000000..cde9d56 --- /dev/null +++ b/tests/test_review_d3_composite_condition.py @@ -0,0 +1,29 @@ +import unittest + +from spinqit import Circuit, H, get_compiler +from spinqit.compiler import NodeType +from spinqit.model import ControlledGate + + +class CompositeConditionRegressionTest(unittest.TestCase): + def test_controlled_composite_gate_keeps_classical_condition(self): + circuit = Circuit('conditional-controlled-h') + circuit.allocateQubits(2) + circuit.allocateClbits(1) + circuit << (ControlledGate(H), [0, 1]) + circuit | ([0], '==', 1) + + ir = get_compiler('native').compile(circuit, 0) + caller = next( + vertex + for vertex in ir.dag.vs + if vertex['type'] == NodeType.caller.value + ) + + self.assertEqual(caller['cmp'], 0) + self.assertEqual(caller['constant'], 1) + self.assertEqual(ir.get_conbits(caller.index), [0]) + + +if __name__ == '__main__': + unittest.main() From ae57bc3d7c471310dd8883c42a0d311d26739bcb Mon Sep 17 00:00:00 2001 From: wuzp15 Date: Wed, 22 Jul 2026 17:28:15 +0800 Subject: [PATCH 23/59] reject conditional cloud circuits --- spinqit/backend/spinq_cloud_backend.py | 6 +++--- tests/test_review_d4_cloud_conditions.py | 22 ++++++++++++++++++++++ 2 files changed, 25 insertions(+), 3 deletions(-) create mode 100644 tests/test_review_d4_cloud_conditions.py diff --git a/spinqit/backend/spinq_cloud_backend.py b/spinqit/backend/spinq_cloud_backend.py index d1dfe37..5b84484 100644 --- a/spinqit/backend/spinq_cloud_backend.py +++ b/spinqit/backend/spinq_cloud_backend.py @@ -164,10 +164,10 @@ def assemble(self, platform_code: str, ir: IntermediateRepresentation): i = 0 while i < ir.dag.vcount(): v = ir.dag.vs[i] - + if 'cmp' in v.attributes() and v['cmp'] is not None: + raise CircuitOperationValidationError("SpinQ Cloud currently does not support cif operation.") + if (v['type'] == NodeType.op.value or v['type'] == NodeType.callee.value): - if hasattr(v, "cmp"): - raise CircuitOperationValidationError("SpinQ Cloud currently does not support cif operation.") if v['name'] == MEASURE.label: raise CircuitOperationValidationError("SpinQ Cloud currently does not support explicit invocation of measure gates. A measure will be done automatically at the end of the circuit.") elif v['name'] == SWAP.label: diff --git a/tests/test_review_d4_cloud_conditions.py b/tests/test_review_d4_cloud_conditions.py new file mode 100644 index 0000000..f415a01 --- /dev/null +++ b/tests/test_review_d4_cloud_conditions.py @@ -0,0 +1,22 @@ +import unittest + +from spinqit import Circuit, CircuitOperationValidationError, X, get_compiler +from spinqit.backend.spinq_cloud_backend import SpinQCloudBackend + + +class CloudConditionRegressionTest(unittest.TestCase): + def test_cloud_rejects_unsupported_classical_condition(self): + circuit = Circuit('conditional-cloud-gate') + circuit.allocateQubits(1) + circuit.allocateClbits(1) + circuit << (X, 0) + circuit | ([0], '==', 1) + ir = get_compiler('native').compile(circuit, 0) + + backend = object.__new__(SpinQCloudBackend) + with self.assertRaises(CircuitOperationValidationError): + backend.assemble('offline', ir) + + +if __name__ == '__main__': + unittest.main() From 7415c2581a1b99caa90ab806db638d508aac0c87 Mon Sep 17 00:00:00 2001 From: wuzp15 Date: Wed, 22 Jul 2026 17:31:01 +0800 Subject: [PATCH 24/59] fix simulator count allocation --- cppsrc/include/model/result.cpp | 52 ++++++++++++---------------- tests/test_review_d5_basic_counts.py | 49 ++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 29 deletions(-) create mode 100644 tests/test_review_d5_basic_counts.py diff --git a/cppsrc/include/model/result.cpp b/cppsrc/include/model/result.cpp index a8870ec..8c7f52f 100644 --- a/cppsrc/include/model/result.cpp +++ b/cppsrc/include/model/result.cpp @@ -15,6 +15,7 @@ */ #include "result.h" +#include int Result::repeat = 1024; Result::Result(/* args */) @@ -36,39 +37,32 @@ string Result::to_string(long key, size_t qnum) map Result::get_counts() { - if (!counts.empty()) return counts; - - vector less; + if (!counts.empty() || probabilities.empty() || shots <= 0) return counts; + + double probability_total = 0.0; + for (const auto& entry : probabilities) { + probability_total += max(0.0, entry.second); + } + if (probability_total == 0.0) return counts; + + vector> remainders; int sum = 0; - for (auto it = probabilities.begin(); it != probabilities.end(); ++it) { - double p = it->second; - double val = p * shots; - int cnt = (int)val; - double rval = round(val); + for (const auto& entry : probabilities) { + double expected = max(0.0, entry.second) * shots / probability_total; + int cnt = (int)floor(expected); if (cnt > 0) { - counts[it->first] = cnt; - sum += cnt; - } - if(rval > val) less.push_back(it->first); - } - int total = shots - sum; - if (total > 0) { - for (size_t i = 0; i < less.size(); i++) - { - counts[less[i]] += 1; - total--; - if(total==0) break; + counts[entry.first] = cnt; } + sum += cnt; + remainders.emplace_back(expected - cnt, entry.first); } - - while (total > 0) { - for (auto it = counts.begin(); it != counts.end(); ++it ) { - it->second += 1; - total--; - if(total==0) break; - } + + sort(remainders.begin(), remainders.end(), + [](const auto& left, const auto& right) { return left.first > right.first; }); + for (int i = 0; i < shots - sum; ++i) { + counts[remainders[i % remainders.size()].second] += 1; } - + return counts; } @@ -93,4 +87,4 @@ string Result::get_random_reading() } return probabilities.rbegin()->first; -} \ No newline at end of file +} diff --git a/tests/test_review_d5_basic_counts.py b/tests/test_review_d5_basic_counts.py new file mode 100644 index 0000000..9049470 --- /dev/null +++ b/tests/test_review_d5_basic_counts.py @@ -0,0 +1,49 @@ +import json +import subprocess +import sys +import tempfile +import textwrap +import unittest + + +class BasicCountsRegressionTest(unittest.TestCase): + def test_low_probability_distribution_still_allocates_all_shots(self): + script = textwrap.dedent(''' + import json + import os + + from spinqit import BasicSimulatorConfig, Circuit, H, get_basic_simulator, get_compiler + + circuit = Circuit('broad-distribution') + qubits = circuit.allocateQubits(3) + for qubit in qubits: + circuit << (H, qubit) + + config = BasicSimulatorConfig() + config.configure_shots(1) + result = get_basic_simulator().execute( + get_compiler('native').compile(circuit, 0), config + ) + counts = result.counts + print(json.dumps({ + 'count_total': sum(counts.values()), + 'probability_total': sum(result.probabilities.values()), + }), flush=True) + os._exit(0) + ''') + completed = subprocess.run( + [sys.executable, '-c', script], + cwd=tempfile.gettempdir(), + capture_output=True, + text=True, + timeout=5, + ) + + self.assertEqual(completed.returncode, 0, completed.stderr) + payload = json.loads(completed.stdout) + self.assertAlmostEqual(payload['probability_total'], 1.0) + self.assertEqual(payload['count_total'], 1) + + +if __name__ == '__main__': + unittest.main() From 0cceeed9577caef91beac2baeb629f4dc80f1747 Mon Sep 17 00:00:00 2001 From: wuzp15 Date: Wed, 22 Jul 2026 17:31:56 +0800 Subject: [PATCH 25/59] weight simulator random readings --- spinqit/backend/pytorch_backend.py | 5 +++- spinqit/backend/qasm_backend.py | 5 +++- tests/test_review_d6_weighted_reading.py | 33 ++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 tests/test_review_d6_weighted_reading.py diff --git a/spinqit/backend/pytorch_backend.py b/spinqit/backend/pytorch_backend.py index 638cdda..ad3599c 100644 --- a/spinqit/backend/pytorch_backend.py +++ b/spinqit/backend/pytorch_backend.py @@ -189,7 +189,10 @@ def _process_prob(config, np_states): return probabilities, higher_dim def get_random_reading(self): - return onp.random.choice(list(self.counts.keys())) + probabilities = self.probabilities + readings = list(probabilities) + weights = onp.array([probabilities[reading] for reading in readings]) + return onp.random.choice(readings, p=weights / weights.sum()) class TorchSimulator: diff --git a/spinqit/backend/qasm_backend.py b/spinqit/backend/qasm_backend.py index 0894986..68606a0 100644 --- a/spinqit/backend/qasm_backend.py +++ b/spinqit/backend/qasm_backend.py @@ -92,7 +92,10 @@ def probabilities(self): return self.probabilities_fn() def get_random_reading(self): - return onp.random.choice(list(self.counts.keys())) + probabilities = self.probabilities + readings = list(probabilities) + weights = onp.array([probabilities[reading] for reading in readings]) + return onp.random.choice(readings, p=weights / weights.sum()) class QasmBackend: diff --git a/tests/test_review_d6_weighted_reading.py b/tests/test_review_d6_weighted_reading.py new file mode 100644 index 0000000..c67b9f5 --- /dev/null +++ b/tests/test_review_d6_weighted_reading.py @@ -0,0 +1,33 @@ +import unittest +from unittest.mock import patch + +import numpy as np +import torch + +from spinqit import QiskitQasmResult +from spinqit.backend.pytorch_backend import TorchResult, TorchSimulatorConfig + + +class WeightedReadingRegressionTest(unittest.TestCase): + def assert_weighted_choice(self, result): + with patch('numpy.random.choice', return_value='0') as choice: + self.assertEqual(result.get_random_reading(), '0') + + readings = choice.call_args.args[0] + weights = choice.call_args.kwargs['p'] + self.assertEqual(readings, ['0', '1']) + np.testing.assert_allclose(weights, [0.9, 0.1]) + + def test_qasm_reading_uses_probabilities(self): + result = QiskitQasmResult() + result.set_result({'0': 900, '1': 100}, 1000) + self.assert_weighted_choice(result) + + def test_torch_reading_uses_probabilities(self): + states = torch.tensor([np.sqrt(0.9), np.sqrt(0.1)], dtype=torch.complex64) + result = TorchResult(states, TorchSimulatorConfig()) + self.assert_weighted_choice(result) + + +if __name__ == '__main__': + unittest.main() From 87d44b606a3e57aacb96d51c4e891c49c21f5cfe Mon Sep 17 00:00:00 2001 From: wuzp15 Date: Wed, 22 Jul 2026 17:33:10 +0800 Subject: [PATCH 26/59] fix mixed-pauli qaoa evolution --- spinqit/algorithm/qaoa.py | 30 +++++++++-------- tests/test_review_d7_qaoa_mixed_pauli.py | 41 ++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 13 deletions(-) create mode 100644 tests/test_review_d7_qaoa_mixed_pauli.py diff --git a/spinqit/algorithm/qaoa.py b/spinqit/algorithm/qaoa.py index 6778036..ec9ad61 100644 --- a/spinqit/algorithm/qaoa.py +++ b/spinqit/algorithm/qaoa.py @@ -19,7 +19,7 @@ from spinqit import Parameter from spinqit.model import Circuit, H, Rx, GateBuilder -from spinqit.model.Ising_gate import Z_IsingGateBuilder, X_IsingGateBuilder, Y_IsingGateBuilder +from spinqit.model.Ising_gate import Z_IsingGateBuilder from spinqit.primitive.pauli_expectation import pauli_decompose from spinqit.compiler import compiler from spinqit.interface.qlayer import QLayer @@ -66,22 +66,26 @@ def _generate_problem_circuit(self, ham) -> Gate: 'The problem hamiltonian should be given in list in __init__().' ) for i in range(len(ham)): + pauli_string = ham[i][0].upper() coefficient = ham[i][1] rotation_params = lambda x, coefficient=coefficient: [ coefficient * value for value in x ] - if 'Z' in ham[i][0]: - qubits = [idx for idx in range(len(ham[i][0])) if ham[i][0][idx] == 'Z'] - rzz = Z_IsingGateBuilder(len(qubits)).to_gate() - builder.append(rzz, qubits, rotation_params) - elif 'X' in ham[i][0]: - qubits = [idx for idx in range(len(ham[i][0])) if ham[i][0][idx] == 'X'] - rxx = X_IsingGateBuilder(len(qubits)).to_gate() - builder.append(rxx, qubits, rotation_params) - elif 'Y' in ham[i][0]: - qubits = [idx for idx in range(len(ham[i][0])) if ham[i][0][idx] == 'Y'] - ryy = Y_IsingGateBuilder(len(qubits)).to_gate() - builder.append(ryy, qubits, rotation_params) + qubits = [idx for idx, pauli in enumerate(pauli_string) if pauli != 'I'] + for idx, pauli in enumerate(pauli_string): + if pauli == 'X': + builder.append(H, [idx]) + elif pauli == 'Y': + builder.append(Rx, [idx], np.pi / 2) + elif pauli not in ('I', 'Z'): + raise ValueError(f'Invalid Pauli string `{pauli_string}`.') + if qubits: + builder.append(Z_IsingGateBuilder(len(qubits)).to_gate(), qubits, rotation_params) + for idx, pauli in enumerate(pauli_string): + if pauli == 'X': + builder.append(H, [idx]) + elif pauli == 'Y': + builder.append(Rx, [idx], -np.pi / 2) return builder.to_gate() def _generate_mixer_circuit(self) -> Gate: diff --git a/tests/test_review_d7_qaoa_mixed_pauli.py b/tests/test_review_d7_qaoa_mixed_pauli.py new file mode 100644 index 0000000..bbd973c --- /dev/null +++ b/tests/test_review_d7_qaoa_mixed_pauli.py @@ -0,0 +1,41 @@ +import unittest + +import numpy as np + +from spinqit import BasicSimulatorConfig, Circuit, X, Y, Z, get_basic_simulator, get_compiler +from spinqit.algorithm.qaoa import QAOA + + +class QaoaMixedPauliRegressionTest(unittest.TestCase): + def test_mixed_pauli_problem_terms_act_on_every_nonidentity_qubit(self): + angle = 0.6 + matrices = {'X': X.get_matrix(), 'Y': Y.get_matrix(), 'Z': Z.get_matrix()} + + for term in ('XZ', 'YZ', 'XY'): + with self.subTest(term=term): + qaoa = object.__new__(QAOA) + qaoa._QAOA__qubit_num = 2 + problem_gate = qaoa._generate_problem_circuit([(term, 1.0)]) + + circuit = Circuit('mixed-pauli') + qubits = circuit.allocateQubits(2) + circuit << (problem_gate, qubits, angle) + state = np.asarray( + get_basic_simulator().execute( + get_compiler('native').compile(circuit, 0), + BasicSimulatorConfig(), + ).states, + dtype=complex, + ) + + pauli = np.kron(matrices[term[0]], matrices[term[1]]) + initial = np.array([1, 0, 0, 0], dtype=complex) + expected = ( + np.cos(angle / 2) * np.eye(4) + - 1j * np.sin(angle / 2) * pauli + ) @ initial + self.assertGreaterEqual(abs(np.vdot(expected, state)) ** 2, 1 - 1e-9) + + +if __name__ == '__main__': + unittest.main() From 08ea71ffb5d47ee398c5c9d384d5f543d3324ab5 Mon Sep 17 00:00:00 2001 From: wuzp15 Date: Wed, 22 Jul 2026 17:34:09 +0800 Subject: [PATCH 27/59] fix schmidt decomposition spectrum --- spinqit/utils/function.py | 10 ++++----- tests/test_review_d8_schmidt_decomposition.py | 22 +++++++++++++++++++ 2 files changed, 27 insertions(+), 5 deletions(-) create mode 100644 tests/test_review_d8_schmidt_decomposition.py diff --git a/spinqit/utils/function.py b/spinqit/utils/function.py index e00ef9c..f5e56aa 100644 --- a/spinqit/utils/function.py +++ b/spinqit/utils/function.py @@ -55,12 +55,12 @@ def schmidt_decompose(psi, amp_mtr = psi.reshape([2 ** len(sys_A), 2 ** len(sys_B)]) # Standard process to obtain schmidt decomposition - u, c, v = sparse.linalg.svds(amp_mtr, k=3) + u, c, v = np.linalg.svd(amp_mtr, full_matrices=False) - k = np.count_nonzero(c > 1e-13) - c = (c[:k]) - u = (u.T[:k].reshape([k, -1, 1])) - v = (v[:k].reshape([k, -1, 1])) + keep = c > 1e-13 + c = c[keep] + u = u[:, keep].T.reshape([len(c), -1, 1]) + v = v[keep].reshape([len(c), -1, 1]) return c, u, v diff --git a/tests/test_review_d8_schmidt_decomposition.py b/tests/test_review_d8_schmidt_decomposition.py new file mode 100644 index 0000000..0f2eb70 --- /dev/null +++ b/tests/test_review_d8_schmidt_decomposition.py @@ -0,0 +1,22 @@ +import unittest + +import numpy as np + +from spinqit.utils.function import schmidt_decompose + + +class SchmidtDecompositionRegressionTest(unittest.TestCase): + def test_bell_state_supports_two_by_two_partition(self): + bell = np.array([1, 0, 0, 1], dtype=float) / np.sqrt(2) + coefficients, _, _ = schmidt_decompose(bell) + np.testing.assert_allclose(coefficients, [1 / np.sqrt(2)] * 2) + + def test_rank_deficient_state_keeps_largest_coefficients(self): + ghz = np.zeros(16) + ghz[[0, -1]] = 1 / np.sqrt(2) + coefficients, _, _ = schmidt_decompose(ghz) + np.testing.assert_allclose(coefficients, [1 / np.sqrt(2)] * 2) + + +if __name__ == '__main__': + unittest.main() From 12b6915435269b972882dd67b8c6d1f3143594e5 Mon Sep 17 00:00:00 2001 From: wuzp15 Date: Wed, 22 Jul 2026 17:34:48 +0800 Subject: [PATCH 28/59] fix ground state eigenvalue selection --- spinqit/utils/function.py | 2 +- tests/test_review_d9_ground_state.py | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 tests/test_review_d9_ground_state.py diff --git a/spinqit/utils/function.py b/spinqit/utils/function.py index f5e56aa..739b3d3 100644 --- a/spinqit/utils/function.py +++ b/spinqit/utils/function.py @@ -66,7 +66,7 @@ def schmidt_decompose(psi, def get_ground_state_info(H): # 计算 H 的特征值与特征向量 - vals, vecs = sparse.linalg.eigsh(H, k=1, which='SM') # 'buckling' | 'cayley' + vals, vecs = sparse.linalg.eigsh(H, k=1, which='SA') # 获取基态 ground_state = (vecs[:, 0]) print(ground_state) diff --git a/tests/test_review_d9_ground_state.py b/tests/test_review_d9_ground_state.py new file mode 100644 index 0000000..899cd59 --- /dev/null +++ b/tests/test_review_d9_ground_state.py @@ -0,0 +1,23 @@ +import contextlib +import io +import unittest + +import numpy as np +from scipy import sparse + +from spinqit.utils.function import get_ground_state_info + + +class GroundStateRegressionTest(unittest.TestCase): + def test_ground_state_uses_algebraically_smallest_eigenvalue(self): + eigenvalues = np.array([-5.0, -1.0, 0.25, 2.0]) + hamiltonian = sparse.diags(eigenvalues, format='csr') + + with contextlib.redirect_stdout(io.StringIO()): + energy = get_ground_state_info(hamiltonian) + + self.assertAlmostEqual(energy, eigenvalues.min()) + + +if __name__ == '__main__': + unittest.main() From 385576043add90f973084f65e785a770ae012624 Mon Sep 17 00:00:00 2001 From: wuzp15 Date: Wed, 22 Jul 2026 17:35:51 +0800 Subject: [PATCH 29/59] decompose multiqubit matrix gates --- spinqit/compiler/translator/gate_converter.py | 8 +++++++- tests/test_review_c3_matrix_gate.py | 16 ++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/spinqit/compiler/translator/gate_converter.py b/spinqit/compiler/translator/gate_converter.py index a6c50c1..4a46bab 100644 --- a/spinqit/compiler/translator/gate_converter.py +++ b/spinqit/compiler/translator/gate_converter.py @@ -19,6 +19,7 @@ from spinqit.model import * from ..decomposer.ZYZdecomposer import decompose_zyz +from ..decomposer.isometry_decomposer import build_gate_for_isometry from ..ir import IntermediateRepresentation as IR import numpy as np @@ -55,7 +56,7 @@ def is_primary_gate(gate: Gate): if gate in IR.basis_set or gate.label in IR.label_set: return True elif isinstance(gate, MatrixGate): - return gate.qubit_num != 1 or len(gate.factors) > 0 + return len(gate.factors) > 0 elif isinstance(gate, ControlledGate) and (isinstance(gate.base_gate, MatrixGate) or gate.base_gate in IR.basis_set): return True @@ -113,4 +114,9 @@ def decompose_multi_qubit_gate(gate: Gate, qubits: List, params=[]) -> List: decomposition.extend(decompose_multi_qubit_gate(f[0], sub_qubits, sub_params)) return decomposition + if isinstance(gate, MatrixGate): + return decompose_multi_qubit_gate( + build_gate_for_isometry(gate.get_matrix(*params)), qubits + ) + raise UnsupportedGateError(gate.label + ' is not supported.') diff --git a/tests/test_review_c3_matrix_gate.py b/tests/test_review_c3_matrix_gate.py index 0c9f670..cf45d8d 100644 --- a/tests/test_review_c3_matrix_gate.py +++ b/tests/test_review_c3_matrix_gate.py @@ -19,6 +19,22 @@ def test_factorless_matrix_gate_executes_on_basic_simulator(self): self.assertAlmostEqual(result.probabilities.get('1', 0.0), 1.0) + def test_factorless_two_qubit_matrix_gate_executes_on_basic_simulator(self): + random = np.random.default_rng(7) + raw = random.normal(size=(4, 4)) + 1j * random.normal(size=(4, 4)) + matrix, _ = np.linalg.qr(raw) + matrix_gate = MatrixGateBuilder(matrix).to_gate() + matrix_gate.label = 'matrix_two_qubit' + circuit = Circuit('matrix-two-qubit') + qubits = circuit.allocateQubits(2) + circuit << (matrix_gate, qubits) + + ir = get_compiler('native').compile(circuit, 0) + result = get_basic_simulator().execute(deepcopy(ir), BasicSimulatorConfig()) + + state = np.asarray(result.states) + self.assertGreaterEqual(abs(np.vdot(matrix[:, 0], state)) ** 2, 1 - 1e-9) + if __name__ == '__main__': unittest.main() From 94047f007d3d869fb41724bb21bd9fc61307b846 Mon Sep 17 00:00:00 2001 From: wuzp15 Date: Wed, 22 Jul 2026 17:40:28 +0800 Subject: [PATCH 30/59] reject mismatched qasm registers --- spinqit/compiler/qasm/Qasm2EventListener.py | 14 ++++---- .../test_review_d2_qasm_register_broadcast.py | 33 ++++++++++++++----- 2 files changed, 33 insertions(+), 14 deletions(-) diff --git a/spinqit/compiler/qasm/Qasm2EventListener.py b/spinqit/compiler/qasm/Qasm2EventListener.py index 3fade5d..83540a8 100644 --- a/spinqit/compiler/qasm/Qasm2EventListener.py +++ b/spinqit/compiler/qasm/Qasm2EventListener.py @@ -283,7 +283,7 @@ def exitQuantumGateCall(self, ctx: Qasm2Parser.QuantumGateCallContext): qubits = self.qregister_table[id] exp_list_ctx = index_id_ctx.expressionList() if exp_list_ctx is None: - gate_qarg_groups.append(qubits) + gate_qarg_groups.append((qubits, True)) else: exp_list = exp_list_ctx.expression() if len(exp_list) > 1: @@ -306,16 +306,18 @@ def exitQuantumGateCall(self, ctx: Qasm2Parser.QuantumGateCallContext): raise Exception( f'The register index is out of range in line {ctx.start.line}. ' ) - gate_qarg_groups.append([qubits[index]]) + gate_qarg_groups.append(([qubits[index]], False)) - register_size = max(len(group) for group in gate_qarg_groups) - if any(len(group) not in (1, register_size) for group in gate_qarg_groups): + register_sizes = {len(group) for group, is_register in gate_qarg_groups + if is_register} + if len(register_sizes) > 1: raise Exception( f'The register sizes for gate `{gate_name}` do not match in line {ctx.start.line}. ' ) + register_size = next(iter(register_sizes), 1) for index in range(register_size): - gate_qargs = [group[index] if len(group) > 1 else group[0] - for group in gate_qarg_groups] + gate_qargs = [group[index] if is_register else group[0] + for group, is_register in gate_qarg_groups] if gate_name.lower() in qasm_basis_map.keys(): vindex = self.ir.add_op_node(qasm_basis_map[gate_name.lower()].label, gate_params, gate_qargs, []) else: diff --git a/tests/test_review_d2_qasm_register_broadcast.py b/tests/test_review_d2_qasm_register_broadcast.py index 1a7d032..b8c0065 100644 --- a/tests/test_review_d2_qasm_register_broadcast.py +++ b/tests/test_review_d2_qasm_register_broadcast.py @@ -1,3 +1,5 @@ +import contextlib +import io import os import tempfile import unittest @@ -7,22 +9,37 @@ class QasmRegisterBroadcastRegressionTest(unittest.TestCase): - def test_register_operands_are_paired_elementwise(self): - source = '''OPENQASM 2.0; -include "qelib1.inc"; -qreg q[2]; -qreg r[2]; -cx q,r; -''' + @staticmethod + def compile_source(source): with tempfile.NamedTemporaryFile('w', suffix='.qasm', delete=False) as qasm_file: qasm_file.write(source) path = qasm_file.name try: - ir = get_compiler('qasm').compile(path, 0) + return get_compiler('qasm').compile(path, 0) finally: os.unlink(path) + def test_mismatched_full_register_sizes_are_rejected(self): + source = '''OPENQASM 2.0; +include "qelib1.inc"; +qreg q[1]; +qreg r[2]; +cx q,r; +''' + with contextlib.redirect_stderr(io.StringIO()): + ir = self.compile_source(source) + self.assertIsNone(ir) + + def test_register_operands_are_paired_elementwise(self): + source = '''OPENQASM 2.0; +include "qelib1.inc"; +qreg q[2]; +qreg r[2]; +cx q,r; +''' + ir = self.compile_source(source) + operands = [ vertex['qubits'] for vertex in ir.dag.vs From 033eaf156d163c449bdf85bbbdaf559fd606e834 Mon Sep 17 00:00:00 2001 From: wuzp15 Date: Wed, 22 Jul 2026 17:41:45 +0800 Subject: [PATCH 31/59] support identity-only qaoa problems --- spinqit/algorithm/qaoa.py | 4 +++- tests/test_review_d7_qaoa_mixed_pauli.py | 9 +++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/spinqit/algorithm/qaoa.py b/spinqit/algorithm/qaoa.py index ec9ad61..688c9c2 100644 --- a/spinqit/algorithm/qaoa.py +++ b/spinqit/algorithm/qaoa.py @@ -18,7 +18,7 @@ from scipy import sparse from spinqit import Parameter -from spinqit.model import Circuit, H, Rx, GateBuilder +from spinqit.model import Circuit, H, I, Rx, GateBuilder from spinqit.model.Ising_gate import Z_IsingGateBuilder from spinqit.primitive.pauli_expectation import pauli_decompose from spinqit.compiler import compiler @@ -86,6 +86,8 @@ def _generate_problem_circuit(self, ham) -> Gate: builder.append(H, [idx]) elif pauli == 'Y': builder.append(Rx, [idx], -np.pi / 2) + if builder.size() == 0: + builder.append(I, [0]) return builder.to_gate() def _generate_mixer_circuit(self) -> Gate: diff --git a/tests/test_review_d7_qaoa_mixed_pauli.py b/tests/test_review_d7_qaoa_mixed_pauli.py index bbd973c..98d0154 100644 --- a/tests/test_review_d7_qaoa_mixed_pauli.py +++ b/tests/test_review_d7_qaoa_mixed_pauli.py @@ -3,10 +3,19 @@ import numpy as np from spinqit import BasicSimulatorConfig, Circuit, X, Y, Z, get_basic_simulator, get_compiler +from spinqit.algorithm.optimizer import GradientDescent from spinqit.algorithm.qaoa import QAOA class QaoaMixedPauliRegressionTest(unittest.TestCase): + def test_identity_only_problem_builds(self): + qaoa = QAOA( + [('II', 3.0)], + GradientDescent(maxiter=1, verbose=False), + depth=1, + ) + self.assertEqual(qaoa.circuit.qnum, 2) + def test_mixed_pauli_problem_terms_act_on_every_nonidentity_qubit(self): angle = 0.6 matrices = {'X': X.get_matrix(), 'Y': Y.get_matrix(), 'Z': Z.get_matrix()} From 61c0af3d9e756136f19b2a965b5f575ee2445f21 Mon Sep 17 00:00:00 2001 From: wuzp15 Date: Wed, 22 Jul 2026 17:47:54 +0800 Subject: [PATCH 32/59] serialize qasm definitions from live dag --- spinqit/backend/qasm_backend.py | 15 +++++------ tests/test_review_d10_qasm_live_dag.py | 37 ++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 9 deletions(-) create mode 100644 tests/test_review_d10_qasm_live_dag.py diff --git a/spinqit/backend/qasm_backend.py b/spinqit/backend/qasm_backend.py index 68606a0..809a0ec 100644 --- a/spinqit/backend/qasm_backend.py +++ b/spinqit/backend/qasm_backend.py @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. import functools -from collections import defaultdict from copy import deepcopy import numpy as onp @@ -223,10 +222,7 @@ def convert_ir_to_qasm(ir): qasm_content += 'OPENQASM 2.0;\n' qasm_content += 'include "qelib1.inc";\n' - edges = defaultdict(list) - for u, v in ir.edges: - if v not in edges[u]: - edges[u].append(v) + topological_order = ir.dag.topological_sorting(mode='out') # Before the qreg are the `qelib1.inc` callee node, qreg_dict = {} @@ -267,9 +263,11 @@ def convert_ir_to_qasm(ir): qargs.append(f'qb{j}') qasm_content += '{\n' - edge = edges[i] - while edge: - idx = edge[0] + definition_nodes = set(ir.dag.subcomponent(i, mode='out')) + definition_nodes.remove(i) + for idx in topological_order: + if idx not in definition_nodes: + continue _v = ir.dag.vs[idx] if _v['type'] != 1 and _v['type'] != 3: raise ValueError @@ -302,7 +300,6 @@ def convert_ir_to_qasm(ir): qasm_content += f'{_gate}({",".join(_v_expression)}) {",".join(qubits)};\n' else: qasm_content += f'{_gate} {",".join(qubits)};\n' - edge = edges[idx] visited.add(idx) qasm_content += '}\n' diff --git a/tests/test_review_d10_qasm_live_dag.py b/tests/test_review_d10_qasm_live_dag.py new file mode 100644 index 0000000..7bf9b8e --- /dev/null +++ b/tests/test_review_d10_qasm_live_dag.py @@ -0,0 +1,37 @@ +import unittest + +from spinqit import Circuit, H, get_compiler +from spinqit.backend.qasm_backend import QasmBackend +from spinqit.compiler.ir import NodeType +from spinqit.model import ControlledGate + + +class QasmLiveDagRegressionTests(unittest.TestCase): + def test_optimized_composite_gate_invocation_is_serialized(self): + for level in (0, 1, 2, 3): + with self.subTest(level=level): + circuit = Circuit() + circuit.allocateQubits(2) + circuit.allocateClbits(1) + circuit << (ControlledGate(H), [0, 1]) + circuit | ([0], '==', 1) + + ir = get_compiler('native').compile(circuit, level) + qasm = QasmBackend.convert_ir_to_qasm(ir) + + self.assertIn('if (c0==1) ch q0[0],q0[1];', qasm.splitlines()) + definition = next( + vertex for vertex in ir.dag.vs + if vertex['type'] == NodeType.definition.value and vertex['name'] == 'CH' + ) + callee_count = sum( + ir.dag.vs[index]['type'] == NodeType.callee.value + for index in ir.dag.subcomponent(definition.index, mode='out') + ) + body = qasm.split('{\n', 1)[1].split('}\n', 1)[0].splitlines() + self.assertEqual(len(body), callee_count) + self.assertNotIn('ch qb0,qb1;', body) + + +if __name__ == '__main__': + unittest.main() From de345955221f3b3a03dce548d13a565c283ee3ad Mon Sep 17 00:00:00 2001 From: wuzp15 Date: Wed, 22 Jul 2026 17:49:09 +0800 Subject: [PATCH 33/59] preserve matrix gate relative phases --- .../decomposer/isometry_decomposer.py | 2 +- tests/test_review_c3_matrix_gate.py | 20 ++++++++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/spinqit/compiler/decomposer/isometry_decomposer.py b/spinqit/compiler/decomposer/isometry_decomposer.py index 6744d93..ede3446 100644 --- a/spinqit/compiler/decomposer/isometry_decomposer.py +++ b/spinqit/compiler/decomposer/isometry_decomposer.py @@ -252,7 +252,7 @@ def build_gate_for_isometry(isometry: np.ndarray) -> Gate: if len(diag_mat) > 1 and not check_diag_is_identity(diag_mat): diag_gate = generate_diagonal_gates(diag_mat) - isometry_builder.append(diag_gate, list(range(qubit_num))) + isometry_builder.append(diag_gate, list(reversed(range(qubit_num)))) isometry_builder.append(inv_builder.to_gate(), list(range(qubit_num-1, -1, -1))) return isometry_builder.to_gate() diff --git a/tests/test_review_c3_matrix_gate.py b/tests/test_review_c3_matrix_gate.py index cf45d8d..678f952 100644 --- a/tests/test_review_c3_matrix_gate.py +++ b/tests/test_review_c3_matrix_gate.py @@ -3,7 +3,7 @@ import numpy as np -from spinqit import BasicSimulatorConfig, Circuit, MatrixGateBuilder, get_basic_simulator, get_compiler +from spinqit import BasicSimulatorConfig, Circuit, H, MatrixGateBuilder, get_basic_simulator, get_compiler class MatrixGateRegressionTest(unittest.TestCase): @@ -35,6 +35,24 @@ def test_factorless_two_qubit_matrix_gate_executes_on_basic_simulator(self): state = np.asarray(result.states) self.assertGreaterEqual(abs(np.vdot(matrix[:, 0], state)) ** 2, 1 - 1e-9) + def test_factorless_two_qubit_matrix_gate_preserves_relative_phases(self): + phases = np.array([0.0, 0.2, 0.7, 1.1]) + matrix = np.diag(np.exp(1j * phases)) + matrix_gate = MatrixGateBuilder(matrix).to_gate() + matrix_gate.label = 'matrix_two_qubit_phases' + circuit = Circuit('matrix-two-qubit-phases') + qubits = circuit.allocateQubits(2) + circuit << (H, qubits[0]) + circuit << (H, qubits[1]) + circuit << (matrix_gate, qubits) + + ir = get_compiler('native').compile(circuit, 0) + result = get_basic_simulator().execute(deepcopy(ir), BasicSimulatorConfig()) + + expected = matrix @ np.full(4, 0.5, dtype=complex) + state = np.asarray(result.states) + self.assertGreaterEqual(abs(np.vdot(expected, state)) ** 2, 1 - 1e-9) + if __name__ == '__main__': unittest.main() From 4c4c40a08675359bf529fb050b91a65cf817d642 Mon Sep 17 00:00:00 2001 From: wuzp15 Date: Wed, 22 Jul 2026 17:52:57 +0800 Subject: [PATCH 34/59] preserve qasm gate parameter expressions --- spinqit/backend/qasm_backend.py | 63 +++++++++++----- tests/test_review_d11_qasm_parameters.py | 91 ++++++++++++++++++++++++ 2 files changed, 137 insertions(+), 17 deletions(-) create mode 100644 tests/test_review_d11_qasm_parameters.py diff --git a/spinqit/backend/qasm_backend.py b/spinqit/backend/qasm_backend.py index 809a0ec..02d0b09 100644 --- a/spinqit/backend/qasm_backend.py +++ b/spinqit/backend/qasm_backend.py @@ -16,6 +16,7 @@ import numpy as onp from scipy import sparse +from sympy import Symbol from spinqit import CP from spinqit.compiler.ir import NodeType @@ -44,6 +45,50 @@ } +def _flatten_qasm_parameters(value): + if isinstance(value, (list, tuple, onp.ndarray)): + result = [] + for item in value: + result.extend(_flatten_qasm_parameters(item)) + return result + return [str(value)] + + +def _render_definition_parameters(vertex, parameter_names): + params = vertex['params'] + pindex = vertex['pindex'] + expressions = vertex['expression'] + rendered = [] + + if expressions: + start = 0 + for param, expression in zip(params, expressions): + arg_count = param.__code__.co_argcount + args = [ + parameter_names[index] + for index in pindex[start:start + arg_count] + ] + start += arg_count + rendered.extend(_flatten_qasm_parameters(eval(expression)(*args))) + return rendered + + symbols = [Symbol(name) for name in parameter_names] + if len(pindex) == 1 and pindex[0] == -1: + for param in params: + value = param() if param.__code__.co_argcount == 0 else param(symbols) + rendered.extend(_flatten_qasm_parameters(value)) + return rendered + + start = 0 + for param in params: + arg_count = param.__code__.co_argcount + args = [symbols[index] for index in pindex[start:start + arg_count]] + start += arg_count + value = param(*args) + rendered.extend(_flatten_qasm_parameters(value)) + return rendered + + class QasmConfig: def __init__(self, shots=1024, mqubits=None): self.shots = shots @@ -280,23 +325,7 @@ def convert_ir_to_qasm(ir): qubits = [qargs[i] for i in _v['qubits']] param = _v['params'] if 'pindex' in _v.attributes() and _v['pindex'] is not None: - start = 0 - _v_pargs = [] - for f in param: - arg_count = f.__code__.co_argcount - if arg_count == 0: - _v_pargs.append(str(f())) - else: - _v_pargs.append(list(pargs[i] for i in _v['pindex'][start:start + arg_count])) - start += arg_count - if _v['expression'] is None: - _v_expression = sum(_v_pargs, []) - else: - expression_list = _v['expression'] - _v_expression = [] - for i, expression in enumerate(expression_list): - func = eval(expression) - _v_expression.append(func(*_v_pargs[i])) + _v_expression = _render_definition_parameters(_v, pargs) qasm_content += f'{_gate}({",".join(_v_expression)}) {",".join(qubits)};\n' else: qasm_content += f'{_gate} {",".join(qubits)};\n' diff --git a/tests/test_review_d11_qasm_parameters.py b/tests/test_review_d11_qasm_parameters.py new file mode 100644 index 0000000..65b98b5 --- /dev/null +++ b/tests/test_review_d11_qasm_parameters.py @@ -0,0 +1,91 @@ +from copy import deepcopy +from math import pi +import os +import tempfile +import unittest + +import numpy as np + +from spinqit import ( + BasicSimulatorConfig, + CX, + Circuit, + MatrixGateBuilder, + Rx, + Ry, + Rz, + X, + get_basic_simulator, + get_compiler, +) +from spinqit.backend.qasm_backend import QasmBackend + + +class QasmParameterRegressionTests(unittest.TestCase): + @staticmethod + def controlled_rotation(gate): + def matrix(params): + result = np.eye(4, dtype=complex) + result[2:, 2:] = gate.get_matrix(params[0]) + return result + + builder = MatrixGateBuilder(matrix) + if gate == Rx: + builder.append(Rz, [1], lambda *args: pi / 2) + builder.append(CX, [0, 1]) + builder.append(Ry, [1], lambda params: -params[0] / 2) + builder.append(CX, [0, 1]) + builder.append(Ry, [1], lambda params: params[0] / 2) + builder.append(Rz, [1], lambda *args: -pi / 2) + else: + builder.append(gate, [1], lambda params: params[0] / 2) + builder.append(CX, [0, 1]) + builder.append(gate, [1], lambda params: -params[0] / 2) + builder.append(CX, [0, 1]) + result = builder.to_gate() + result.label = f'custom_c{gate.label.lower()}' + return result + + @staticmethod + def compile_qasm(source): + with tempfile.NamedTemporaryFile('w', suffix='.qasm', delete=False) as qasm_file: + qasm_file.write(source) + path = qasm_file.name + try: + return get_compiler('qasm').compile(path, 0) + finally: + os.unlink(path) + + def test_controlled_rotation_expressions_survive_qasm_round_trip(self): + expected_expressions = { + Rx: ('ry(-a0/2)', 'ry(a0/2)'), + Ry: ('ry(a0/2)', 'ry(-a0/2)'), + Rz: ('rz(a0/2)', 'rz(-a0/2)'), + } + simulator = get_basic_simulator() + + for gate, expressions in expected_expressions.items(): + with self.subTest(gate=gate.label): + circuit = Circuit() + qubits = circuit.allocateQubits(2) + circuit << (X, qubits[0]) + circuit << (self.controlled_rotation(gate), qubits, 0.6) + ir = get_compiler('native').compile(circuit, 0) + + qasm = QasmBackend.convert_ir_to_qasm(ir) + for expression in expressions: + self.assertIn(expression, qasm) + + round_trip_ir = self.compile_qasm(qasm) + self.assertIsNotNone(round_trip_ir) + expected = np.asarray( + simulator.execute(deepcopy(ir), BasicSimulatorConfig()).states + ) + actual = np.asarray( + simulator.execute(deepcopy(round_trip_ir), BasicSimulatorConfig()).states + ) + self.assertGreaterEqual(abs(np.vdot(expected, actual)) ** 2, 1 - 1e-9) + + +if __name__ == '__main__': + unittest.main() From e7db1d248e96a69a5e98c360bce006ae54c84422 Mon Sep 17 00:00:00 2001 From: wuzp15 Date: Wed, 22 Jul 2026 17:54:03 +0800 Subject: [PATCH 35/59] avoid redefining qelib gates --- spinqit/backend/qasm_backend.py | 9 +++- tests/test_review_d10_qasm_live_dag.py | 20 +++++-- .../test_review_d12_qasm_qelib_definitions.py | 53 +++++++++++++++++++ 3 files changed, 76 insertions(+), 6 deletions(-) create mode 100644 tests/test_review_d12_qasm_qelib_definitions.py diff --git a/spinqit/backend/qasm_backend.py b/spinqit/backend/qasm_backend.py index 02d0b09..c5eb613 100644 --- a/spinqit/backend/qasm_backend.py +++ b/spinqit/backend/qasm_backend.py @@ -44,6 +44,13 @@ CP.label.lower(): 'cp' } +qelib_gate_names = { + 'u3', 'u2', 'u1', 'u0', 'u', 'p', 'cx', 'id', 'x', 'y', 'z', 'h', + 's', 'sdg', 't', 'tdg', 'rx', 'ry', 'rz', 'sx', 'sxdg', 'cz', 'cy', + 'swap', 'ch', 'ccx', 'cswap', 'cry', 'crz', 'crx', 'cu1', 'cp', 'cu3', + 'csx', 'cu', 'rxx', 'rzz', 'rccx', 'rc3x', 'c3x', 'c3sqrtx', 'c4x', +}.union(gate_name) + def _flatten_qasm_parameters(value): if isinstance(value, (list, tuple, onp.ndarray)): @@ -289,7 +296,7 @@ def convert_ir_to_qasm(ir): creg_dict[int(idx) + int(register[1:])] = (register, int(idx)) elif v['type'] == NodeType.definition.value: gate = v['name'].lower() - if gate not in gate_name: + if gate not in qelib_gate_names: qubits = v['qubits'] param_num = v['params'] qargs = [] diff --git a/tests/test_review_d10_qasm_live_dag.py b/tests/test_review_d10_qasm_live_dag.py index 7bf9b8e..733913e 100644 --- a/tests/test_review_d10_qasm_live_dag.py +++ b/tests/test_review_d10_qasm_live_dag.py @@ -1,6 +1,8 @@ import unittest -from spinqit import Circuit, H, get_compiler +import numpy as np + +from spinqit import Circuit, H, MatrixGateBuilder, get_compiler from spinqit.backend.qasm_backend import QasmBackend from spinqit.compiler.ir import NodeType from spinqit.model import ControlledGate @@ -10,19 +12,27 @@ class QasmLiveDagRegressionTests(unittest.TestCase): def test_optimized_composite_gate_invocation_is_serialized(self): for level in (0, 1, 2, 3): with self.subTest(level=level): + matrix = np.eye(4, dtype=complex) + matrix[2:, 2:] = H.get_matrix() + builder = MatrixGateBuilder(matrix) + for factor in ControlledGate(H).factors: + builder.append(factor[0], factor[1]) + gate = builder.to_gate() + gate.label = 'custom_ch' circuit = Circuit() circuit.allocateQubits(2) circuit.allocateClbits(1) - circuit << (ControlledGate(H), [0, 1]) + circuit << (gate, [0, 1]) circuit | ([0], '==', 1) ir = get_compiler('native').compile(circuit, level) qasm = QasmBackend.convert_ir_to_qasm(ir) - self.assertIn('if (c0==1) ch q0[0],q0[1];', qasm.splitlines()) + self.assertIn('if (c0==1) custom_ch q0[0],q0[1];', qasm.splitlines()) definition = next( vertex for vertex in ir.dag.vs - if vertex['type'] == NodeType.definition.value and vertex['name'] == 'CH' + if vertex['type'] == NodeType.definition.value + and vertex['name'] == 'custom_ch' ) callee_count = sum( ir.dag.vs[index]['type'] == NodeType.callee.value @@ -30,7 +40,7 @@ def test_optimized_composite_gate_invocation_is_serialized(self): ) body = qasm.split('{\n', 1)[1].split('}\n', 1)[0].splitlines() self.assertEqual(len(body), callee_count) - self.assertNotIn('ch qb0,qb1;', body) + self.assertNotIn('custom_ch qb0,qb1;', body) if __name__ == '__main__': diff --git a/tests/test_review_d12_qasm_qelib_definitions.py b/tests/test_review_d12_qasm_qelib_definitions.py new file mode 100644 index 0000000..d70adfb --- /dev/null +++ b/tests/test_review_d12_qasm_qelib_definitions.py @@ -0,0 +1,53 @@ +from copy import deepcopy +import os +import tempfile +import unittest + +import numpy as np + +from spinqit import ( + BasicSimulatorConfig, + Circuit, + Ry, + X, + get_basic_simulator, + get_compiler, +) +from spinqit.backend.qasm_backend import QasmBackend +from spinqit.model import ControlledGate + + +class QasmQelibDefinitionRegressionTests(unittest.TestCase): + def test_standard_controlled_gate_is_not_redefined(self): + circuit = Circuit() + qubits = circuit.allocateQubits(2) + circuit << (X, qubits[0]) + circuit << (ControlledGate(Ry), qubits, 0.6) + ir = get_compiler('native').compile(circuit, 0) + + qasm = QasmBackend.convert_ir_to_qasm(ir) + + self.assertNotIn('gate cry', qasm) + self.assertIn('cry(0.6) q0[0],q0[1];', qasm) + + with tempfile.NamedTemporaryFile('w', suffix='.qasm', delete=False) as qasm_file: + qasm_file.write(qasm) + path = qasm_file.name + try: + round_trip_ir = get_compiler('qasm').compile(path, 0) + finally: + os.unlink(path) + + self.assertIsNotNone(round_trip_ir) + simulator = get_basic_simulator() + expected = np.asarray( + simulator.execute(deepcopy(ir), BasicSimulatorConfig()).states + ) + actual = np.asarray( + simulator.execute(deepcopy(round_trip_ir), BasicSimulatorConfig()).states + ) + self.assertGreaterEqual(abs(np.vdot(expected, actual)) ** 2, 1 - 1e-9) + + +if __name__ == '__main__': + unittest.main() From 82a7b07504b50bec4e4149c8beb6cac4fef676e7 Mon Sep 17 00:00:00 2001 From: wuzp15 Date: Wed, 22 Jul 2026 17:54:52 +0800 Subject: [PATCH 36/59] fix controlled gate matrix blocks --- spinqit/model/controlled_gate.py | 5 ++-- .../test_review_d13_controlled_gate_matrix.py | 30 +++++++++++++++++++ 2 files changed, 32 insertions(+), 3 deletions(-) create mode 100644 tests/test_review_d13_controlled_gate_matrix.py diff --git a/spinqit/model/controlled_gate.py b/spinqit/model/controlled_gate.py index 3f1c77c..495d179 100644 --- a/spinqit/model/controlled_gate.py +++ b/spinqit/model/controlled_gate.py @@ -70,11 +70,10 @@ def get_matrix(self, *params): m = self.subgate.get_matrix(*params) if m is None: return None - m0 = np.zeros(len(m)) - m1 = np.eye(len(m)) + m0 = np.zeros_like(m) + m1 = np.eye(len(m), dtype=m.dtype) m10 = np.concatenate([m1, m0], axis=1) m = np.concatenate([m0, m], axis=1) m = np.concatenate([m10, m], axis=0) return m - diff --git a/tests/test_review_d13_controlled_gate_matrix.py b/tests/test_review_d13_controlled_gate_matrix.py new file mode 100644 index 0000000..97db307 --- /dev/null +++ b/tests/test_review_d13_controlled_gate_matrix.py @@ -0,0 +1,30 @@ +import unittest + +import numpy as np + +from spinqit import Ry +from spinqit.model import ControlledGate + + +class ControlledGateMatrixRegressionTests(unittest.TestCase): + def test_controlled_gate_matrix_uses_square_zero_blocks(self): + angle = 0.6 + rotation = Ry.get_matrix(angle) + expected = np.zeros((4, 4), dtype=complex) + expected[:2, :2] = np.eye(2) + expected[2:, 2:] = rotation + + controlled = ControlledGate(Ry) + np.testing.assert_allclose(controlled.get_matrix(angle), expected) + + nested_expected = np.zeros((8, 8), dtype=complex) + nested_expected[:4, :4] = np.eye(4) + nested_expected[4:, 4:] = expected + np.testing.assert_allclose( + ControlledGate(controlled).get_matrix(angle), + nested_expected, + ) + + +if __name__ == '__main__': + unittest.main() From 2d16cb796fdcb607cc6fb85b1b481b5e98f18ae4 Mon Sep 17 00:00:00 2001 From: wuzp15 Date: Wed, 22 Jul 2026 18:00:17 +0800 Subject: [PATCH 37/59] serialize optimized qasm gate parameters --- spinqit/backend/qasm_backend.py | 2 ++ tests/test_review_d10_qasm_live_dag.py | 14 ++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/spinqit/backend/qasm_backend.py b/spinqit/backend/qasm_backend.py index c5eb613..89031a1 100644 --- a/spinqit/backend/qasm_backend.py +++ b/spinqit/backend/qasm_backend.py @@ -334,6 +334,8 @@ def convert_ir_to_qasm(ir): if 'pindex' in _v.attributes() and _v['pindex'] is not None: _v_expression = _render_definition_parameters(_v, pargs) qasm_content += f'{_gate}({",".join(_v_expression)}) {",".join(qubits)};\n' + elif param: + qasm_content += f'{_gate}({",".join(map(str, param))}) {",".join(qubits)};\n' else: qasm_content += f'{_gate} {",".join(qubits)};\n' visited.add(idx) diff --git a/tests/test_review_d10_qasm_live_dag.py b/tests/test_review_d10_qasm_live_dag.py index 733913e..16f3782 100644 --- a/tests/test_review_d10_qasm_live_dag.py +++ b/tests/test_review_d10_qasm_live_dag.py @@ -1,3 +1,7 @@ +import contextlib +import io +import os +import tempfile import unittest import numpy as np @@ -42,6 +46,16 @@ def test_optimized_composite_gate_invocation_is_serialized(self): self.assertEqual(len(body), callee_count) self.assertNotIn('custom_ch qb0,qb1;', body) + with tempfile.NamedTemporaryFile('w', suffix='.qasm', delete=False) as qasm_file: + qasm_file.write(qasm) + path = qasm_file.name + try: + with contextlib.redirect_stderr(io.StringIO()): + round_trip_ir = get_compiler('qasm').compile(path, 0) + finally: + os.unlink(path) + self.assertIsNotNone(round_trip_ir) + if __name__ == '__main__': unittest.main() From 4aeed58757e34574afc790fc49c138663b964e63 Mon Sep 17 00:00:00 2001 From: wuzp15 Date: Wed, 22 Jul 2026 18:02:29 +0800 Subject: [PATCH 38/59] handle numeric optimized callee parameters --- cppsrc/include/util/graph_attributes.h | 10 ++++- ...eview_d14_optimized_composite_execution.py | 44 +++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 tests/test_review_d14_optimized_composite_execution.py diff --git a/cppsrc/include/util/graph_attributes.h b/cppsrc/include/util/graph_attributes.h index 78e102b..bcf9ce4 100644 --- a/cppsrc/include/util/graph_attributes.h +++ b/cppsrc/include/util/graph_attributes.h @@ -157,6 +157,14 @@ static int exec_callable_vertex_attr(const igraph_t *graph, long i,j; for (i=0,j=0; ico_argcount; @@ -418,4 +426,4 @@ static int topological_sorting_from_vertex(const igraph_t *graph, igraph_vector_destroy(&dfs_res); return 0; -} \ No newline at end of file +} diff --git a/tests/test_review_d14_optimized_composite_execution.py b/tests/test_review_d14_optimized_composite_execution.py new file mode 100644 index 0000000..fbfad65 --- /dev/null +++ b/tests/test_review_d14_optimized_composite_execution.py @@ -0,0 +1,44 @@ +import json +import subprocess +import sys +import tempfile +import textwrap +import unittest + + +class OptimizedCompositeExecutionRegressionTests(unittest.TestCase): + def test_optimized_composite_gate_executes_without_native_crash(self): + for level in (1, 2, 3): + with self.subTest(level=level): + script = textwrap.dedent(f''' + import json + import os + + from spinqit import BasicSimulatorConfig, Circuit, H, X, get_basic_simulator, get_compiler + from spinqit.model import ControlledGate + + circuit = Circuit() + qubits = circuit.allocateQubits(2) + circuit << (X, qubits[0]) + circuit << (ControlledGate(H), qubits) + ir = get_compiler('native').compile(circuit, {level}) + result = get_basic_simulator().execute(ir, BasicSimulatorConfig()) + print(json.dumps(result.probabilities), flush=True) + os._exit(0) + ''') + completed = subprocess.run( + [sys.executable, '-c', script], + cwd=tempfile.gettempdir(), + capture_output=True, + text=True, + timeout=5, + ) + + self.assertEqual(completed.returncode, 0, completed.stderr) + probabilities = json.loads(completed.stdout) + self.assertAlmostEqual(probabilities['10'], 0.5) + self.assertAlmostEqual(probabilities['11'], 0.5) + + +if __name__ == '__main__': + unittest.main() From 761df298807501f076e8835567bf89dca1f6035a Mon Sep 17 00:00:00 2001 From: wuzp15 Date: Wed, 22 Jul 2026 18:03:34 +0800 Subject: [PATCH 39/59] fix bundled qelib controlled rx --- spinqit/compiler/qasm/include/qelib1.inc | 2 +- tests/test_review_d15_qelib_crx.py | 40 ++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 tests/test_review_d15_qelib_crx.py diff --git a/spinqit/compiler/qasm/include/qelib1.inc b/spinqit/compiler/qasm/include/qelib1.inc index 9c4217a..0dcdb7e 100644 --- a/spinqit/compiler/qasm/include/qelib1.inc +++ b/spinqit/compiler/qasm/include/qelib1.inc @@ -104,7 +104,7 @@ gate crz(lambda) a,b // controlled rx rotation gate crx(lambda) a,b { - rx(pi/2) b; + u1(pi/2) b; cx a,b; u3(-lambda/2,0,0) b; cx a,b; diff --git a/tests/test_review_d15_qelib_crx.py b/tests/test_review_d15_qelib_crx.py new file mode 100644 index 0000000..49e0843 --- /dev/null +++ b/tests/test_review_d15_qelib_crx.py @@ -0,0 +1,40 @@ +from copy import deepcopy +import os +import tempfile +import unittest + +import numpy as np + +from spinqit import BasicSimulatorConfig, get_basic_simulator, get_compiler + + +class QelibCrxRegressionTests(unittest.TestCase): + def test_crx_definition_matches_controlled_rx_matrix(self): + angle = 0.6 + source = f'''OPENQASM 2.0; +include "qelib1.inc"; +qreg q[2]; +x q[0]; +crx({angle}) q[0],q[1]; +''' + with tempfile.NamedTemporaryFile('w', suffix='.qasm', delete=False) as qasm_file: + qasm_file.write(source) + path = qasm_file.name + try: + ir = get_compiler('qasm').compile(path, 0) + finally: + os.unlink(path) + + self.assertIsNotNone(ir) + state = np.asarray( + get_basic_simulator().execute(deepcopy(ir), BasicSimulatorConfig()).states + ) + expected = np.array( + [0, 0, np.cos(angle / 2), -1j * np.sin(angle / 2)], + dtype=complex, + ) + self.assertGreaterEqual(abs(np.vdot(expected, state)) ** 2, 1 - 1e-9) + + +if __name__ == '__main__': + unittest.main() From 31101867318e90c117000207d96c34fdc87a663a Mon Sep 17 00:00:00 2001 From: wuzp15 Date: Wed, 22 Jul 2026 18:05:42 +0800 Subject: [PATCH 40/59] handle numeric torch callee parameters --- spinqit/backend/pytorch_backend.py | 14 +++++++-- ...st_review_d16_torch_optimized_composite.py | 31 +++++++++++++++++++ 2 files changed, 42 insertions(+), 3 deletions(-) create mode 100644 tests/test_review_d16_torch_optimized_composite.py diff --git a/spinqit/backend/pytorch_backend.py b/spinqit/backend/pytorch_backend.py index ad3599c..0b7bb24 100644 --- a/spinqit/backend/pytorch_backend.py +++ b/spinqit/backend/pytorch_backend.py @@ -284,12 +284,20 @@ def _caller_node(self, label, params, state, qubits, qubits_num, graph, ): plambda = None if 'params' not in node.attributes() else node['params'] if 'pindex' in node.attributes() and node['pindex'] is not None: try: - p = [f(*[params[idx] for idx in node['pindex']]) for f in plambda] + p = [ + f(*[params[idx] for idx in node['pindex']]) if callable(f) else f + for f in plambda + ] except Exception: - p = [f([params[idx] for idx in node['pindex']]) for f in plambda] + p = [ + f([params[idx] for idx in node['pindex']]) if callable(f) else f + for f in plambda + ] callee_params = [] if not plambda else p else: - callee_params = [] if not plambda else [f(params) for f in plambda] + callee_params = [] if not plambda else [ + f(params) if callable(f) else f for f in plambda + ] if node['type'] == 3: state = self._op_node(node['name'], callee_params, state, local, qubits_num) elif node['type'] == 1: diff --git a/tests/test_review_d16_torch_optimized_composite.py b/tests/test_review_d16_torch_optimized_composite.py new file mode 100644 index 0000000..dd5b5b2 --- /dev/null +++ b/tests/test_review_d16_torch_optimized_composite.py @@ -0,0 +1,31 @@ +import unittest + +from spinqit import ( + Circuit, + H, + TorchSimulatorConfig, + X, + get_compiler, + get_torch_simulator, +) +from spinqit.model import ControlledGate + + +class TorchOptimizedCompositeRegressionTests(unittest.TestCase): + def test_numeric_optimized_callee_parameters_are_executed(self): + for level in (1, 2, 3): + with self.subTest(level=level): + circuit = Circuit() + qubits = circuit.allocateQubits(2) + circuit << (X, qubits[0]) + circuit << (ControlledGate(H), qubits) + ir = get_compiler('native').compile(circuit, level) + + result = get_torch_simulator().execute(ir, TorchSimulatorConfig()) + + self.assertAlmostEqual(result.probabilities['10'], 0.5, places=6) + self.assertAlmostEqual(result.probabilities['11'], 0.5, places=6) + + +if __name__ == '__main__': + unittest.main() From 5f240ef8cde914413347c4c922a29f34e0cab548 Mon Sep 17 00:00:00 2001 From: wuzp15 Date: Wed, 22 Jul 2026 18:08:35 +0800 Subject: [PATCH 41/59] resolve torch callee parameter arity --- spinqit/backend/pytorch_backend.py | 41 +++++++++++---- .../test_review_d17_torch_constant_callee.py | 51 +++++++++++++++++++ 2 files changed, 81 insertions(+), 11 deletions(-) create mode 100644 tests/test_review_d17_torch_constant_callee.py diff --git a/spinqit/backend/pytorch_backend.py b/spinqit/backend/pytorch_backend.py index 0b7bb24..46913ca 100644 --- a/spinqit/backend/pytorch_backend.py +++ b/spinqit/backend/pytorch_backend.py @@ -68,6 +68,33 @@ def _U3(x): } +def _resolve_callee_params(functions, indexes, params): + if not functions: + return [] + + result = [] + if indexes == [-1]: + for function in functions: + if not callable(function): + result.append(function) + elif function.__code__.co_argcount == 0: + result.append(function()) + else: + result.append(function(params)) + return result + + start = 0 + for function in functions: + if not callable(function): + result.append(function) + continue + arg_count = function.__code__.co_argcount + args = [params[index] for index in indexes[start:start + arg_count]] + start += arg_count + result.append(function(*args)) + return result + + class TorchSimulatorConfig: def __init__(self): self.mqubits = None @@ -283,17 +310,9 @@ def _caller_node(self, label, params, state, qubits, qubits_num, graph, ): local = [qubits[i] for i in node['qubits']] plambda = None if 'params' not in node.attributes() else node['params'] if 'pindex' in node.attributes() and node['pindex'] is not None: - try: - p = [ - f(*[params[idx] for idx in node['pindex']]) if callable(f) else f - for f in plambda - ] - except Exception: - p = [ - f([params[idx] for idx in node['pindex']]) if callable(f) else f - for f in plambda - ] - callee_params = [] if not plambda else p + callee_params = _resolve_callee_params( + plambda, node['pindex'], params + ) else: callee_params = [] if not plambda else [ f(params) if callable(f) else f for f in plambda diff --git a/tests/test_review_d17_torch_constant_callee.py b/tests/test_review_d17_torch_constant_callee.py new file mode 100644 index 0000000..02687a0 --- /dev/null +++ b/tests/test_review_d17_torch_constant_callee.py @@ -0,0 +1,51 @@ +from copy import deepcopy +import unittest + +import numpy as np + +from spinqit import ( + BasicSimulatorConfig, + Circuit, + H, + T, + Td, + TorchSimulatorConfig, + X, + get_basic_simulator, + get_compiler, + get_torch_simulator, +) +from spinqit.model import ControlledGate + + +class TorchConstantCalleeRegressionTests(unittest.TestCase): + def test_parameterless_controlled_phase_gates_execute(self): + for gate in (T, Td): + for level in range(4): + with self.subTest(gate=gate.label, level=level): + circuit = Circuit() + qubits = circuit.allocateQubits(2) + circuit << (H, qubits[0]) + circuit << (X, qubits[1]) + circuit << (ControlledGate(gate), qubits) + ir = get_compiler('native').compile(circuit, level) + + expected = np.asarray( + get_basic_simulator().execute( + deepcopy(ir), BasicSimulatorConfig() + ).states + ) + actual = ( + get_torch_simulator() + .execute(deepcopy(ir), TorchSimulatorConfig()) + .states.detach().cpu().numpy() + ) + + self.assertGreaterEqual( + abs(np.vdot(expected, actual)) ** 2, + 1 - 1e-6, + ) + + +if __name__ == '__main__': + unittest.main() From 4661a995b9305e033c30e1f8ecce9944666b06ce Mon Sep 17 00:00:00 2001 From: wuzp15 Date: Wed, 22 Jul 2026 18:50:59 +0800 Subject: [PATCH 42/59] fix nested controlled y lowering --- spinqit/model/controlled_gate_decomposer.py | 4 +++- tests/test_review_e1_controlled_cy.py | 26 +++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 tests/test_review_e1_controlled_cy.py diff --git a/spinqit/model/controlled_gate_decomposer.py b/spinqit/model/controlled_gate_decomposer.py index 966215c..4f9c057 100644 --- a/spinqit/model/controlled_gate_decomposer.py +++ b/spinqit/model/controlled_gate_decomposer.py @@ -21,6 +21,8 @@ def control_basis_decomposition(gate: Gate, qubits: List) -> List: return [(CX, [qubits[0], qubits[1]])] elif gate == Y: return [(CY, [qubits[0], qubits[1]])] + elif gate == CY: + return [(Sd, [qubits[2]]), (CCX, qubits), (S, [qubits[2]])] elif gate == Z: return [(CZ, [qubits[0], qubits[1]])] elif gate == I: @@ -173,4 +175,4 @@ def CCZ_decomposition(qubits: List): __factors.append((H, [qubits[2]])) __factors.append((CCX, qubits)) __factors.append((H, [qubits[2]])) - return __factors \ No newline at end of file + return __factors diff --git a/tests/test_review_e1_controlled_cy.py b/tests/test_review_e1_controlled_cy.py new file mode 100644 index 0000000..8fb47db --- /dev/null +++ b/tests/test_review_e1_controlled_cy.py @@ -0,0 +1,26 @@ +from copy import deepcopy +import unittest + +import numpy as np + +from spinqit import BasicSimulatorConfig, CY, Circuit, ControlledGate, X, get_basic_simulator, get_compiler + + +class ControlledCyRegressionTest(unittest.TestCase): + def test_adding_a_control_to_cy_controls_both_existing_qubits(self): + circuit = Circuit() + qubits = circuit.allocateQubits(3) + circuit << (X, qubits[0]) + circuit << (X, qubits[1]) + circuit << (ControlledGate(CY), qubits) + + ir = get_compiler('native').compile(circuit, 0) + result = get_basic_simulator().execute(deepcopy(ir), BasicSimulatorConfig()) + + expected = np.zeros(8, dtype=complex) + expected[7] = 1j + self.assertGreaterEqual(abs(np.vdot(expected, result.states)) ** 2, 1 - 1e-9) + + +if __name__ == '__main__': + unittest.main() From 6a825f0e63973aa9eef5180d9e85a056d3e3e86e Mon Sep 17 00:00:00 2001 From: wuzp15 Date: Wed, 22 Jul 2026 18:52:14 +0800 Subject: [PATCH 43/59] fix nested controlled x construction --- spinqit/model/controlled_gate_decomposer.py | 2 +- tests/test_review_e2_nested_controlled_x.py | 27 +++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 tests/test_review_e2_nested_controlled_x.py diff --git a/spinqit/model/controlled_gate_decomposer.py b/spinqit/model/controlled_gate_decomposer.py index 4f9c057..14a28bd 100644 --- a/spinqit/model/controlled_gate_decomposer.py +++ b/spinqit/model/controlled_gate_decomposer.py @@ -48,7 +48,7 @@ def control_basis_decomposition(gate: Gate, qubits: List) -> List: elif gate == Rz: return CRz_decomposition(qubits) elif gate == CX or gate.label == 'CX': - return CCX + return [(CCX, qubits)] elif gate == CZ: return CCZ_decomposition(qubits) diff --git a/tests/test_review_e2_nested_controlled_x.py b/tests/test_review_e2_nested_controlled_x.py new file mode 100644 index 0000000..f6f4682 --- /dev/null +++ b/tests/test_review_e2_nested_controlled_x.py @@ -0,0 +1,27 @@ +from copy import deepcopy +import unittest + +import numpy as np + +from spinqit import BasicSimulatorConfig, CX, Circuit, ControlledGate, X, get_basic_simulator, get_compiler + + +class NestedControlledXRegressionTest(unittest.TestCase): + def test_adding_two_controls_to_cnot_constructs_and_executes(self): + gate = ControlledGate(ControlledGate(CX)) + circuit = Circuit() + qubits = circuit.allocateQubits(4) + for qubit in qubits[:3]: + circuit << (X, qubit) + circuit << (gate, qubits) + + ir = get_compiler('native').compile(circuit, 0) + result = get_basic_simulator().execute(deepcopy(ir), BasicSimulatorConfig()) + + expected = np.zeros(16, dtype=complex) + expected[15] = 1 + self.assertGreaterEqual(abs(np.vdot(expected, result.states)) ** 2, 1 - 1e-9) + + +if __name__ == '__main__': + unittest.main() From d1bc795ddc690d4c79ba63539a07c7e42e98271d Mon Sep 17 00:00:00 2001 From: wuzp15 Date: Wed, 22 Jul 2026 18:55:59 +0800 Subject: [PATCH 44/59] lower controlled matrix gates before execution --- spinqit/compiler/native_compiler.py | 15 ++++- .../test_review_e3_controlled_matrix_gate.py | 66 +++++++++++++++++++ 2 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 tests/test_review_e3_controlled_matrix_gate.py diff --git a/spinqit/compiler/native_compiler.py b/spinqit/compiler/native_compiler.py index 6ed5d35..afb3a98 100644 --- a/spinqit/compiler/native_compiler.py +++ b/spinqit/compiler/native_compiler.py @@ -136,7 +136,20 @@ def compile(self, circ: Circuit, level: int) -> IR: for inst in circ.instructions: gate = inst.gate - if is_primary_gate(gate): + if (isinstance(gate, ControlledGate) + and isinstance(gate.base_gate, MatrixGate) + and len(gate.factors) == 0): + # A raw controlled matrix becomes an IR ``unitary`` node, which + # execution and serialization backends do not implement. Lower + # it to the native basis before constructing the IR instead. + from spinqit.primitive.multi_controlled_gate_builder import MultiControlledGateBuilder + lowered_gate = MultiControlledGateBuilder( + gate.control_bits, gate.base_gate, inst.params + ).to_gate() + ilist = decompose_multi_qubit_gate(lowered_gate, inst.qubits) + for i in ilist: + self.handle_primary_gate(ir, i, inst.condition) + elif is_primary_gate(gate): self.handle_primary_gate(ir, inst, inst.condition) else: if gate.qubit_num == 1: diff --git a/tests/test_review_e3_controlled_matrix_gate.py b/tests/test_review_e3_controlled_matrix_gate.py new file mode 100644 index 0000000..209a84a --- /dev/null +++ b/tests/test_review_e3_controlled_matrix_gate.py @@ -0,0 +1,66 @@ +from copy import deepcopy +import contextlib +import io +import os +import tempfile +import unittest + +import numpy as np + +from spinqit import ( + BasicSimulatorConfig, + Circuit, + ControlledGate, + MatrixGateBuilder, + TorchSimulatorConfig, + X, + get_basic_simulator, + get_compiler, + get_torch_simulator, +) +from spinqit.backend.qasm_backend import QasmBackend + + +class ControlledMatrixGateRegressionTest(unittest.TestCase): + @staticmethod + def build_circuit(): + matrix_x = MatrixGateBuilder(X.get_matrix()).to_gate() + circuit = Circuit() + qubits = circuit.allocateQubits(2) + circuit << (X, qubits[0]) + circuit << (ControlledGate(matrix_x), qubits) + return circuit + + @staticmethod + def assert_target_flipped(states): + expected = np.zeros(4, dtype=complex) + expected[3] = 1 + states = np.asarray(states) + if states.ndim > 1: + states = states[0] + assert abs(np.vdot(expected, states)) ** 2 >= 1 - 1e-6 + + def test_controlled_matrix_gate_executes_on_all_local_lowering_paths(self): + ir = get_compiler('native').compile(self.build_circuit(), 0) + + basic_result = get_basic_simulator().execute(deepcopy(ir), BasicSimulatorConfig()) + self.assert_target_flipped(basic_result.states) + + torch_result = get_torch_simulator().execute(deepcopy(ir), TorchSimulatorConfig()) + self.assert_target_flipped(torch_result.states.detach().cpu().numpy()) + + qasm = QasmBackend.convert_ir_to_qasm(ir) + with tempfile.NamedTemporaryFile('w', suffix='.qasm', delete=False) as qasm_file: + qasm_file.write(qasm) + path = qasm_file.name + try: + with contextlib.redirect_stderr(io.StringIO()): + qasm_ir = get_compiler('qasm').compile(path, 0) + finally: + os.unlink(path) + qasm_result = get_basic_simulator().execute(qasm_ir, BasicSimulatorConfig()) + self.assert_target_flipped(qasm_result.states) + + +if __name__ == '__main__': + unittest.main() From b914d9309b93bc6ee4cd147dc716e6630b3c1a2c Mon Sep 17 00:00:00 2001 From: wuzp15 Date: Wed, 22 Jul 2026 18:58:22 +0800 Subject: [PATCH 45/59] defer trainable controlled gate matrices --- spinqit/compiler/native_compiler.py | 11 ++++- ...view_e4_torch_controlled_phase_gradient.py | 48 +++++++++++++++++++ 2 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 tests/test_review_e4_torch_controlled_phase_gradient.py diff --git a/spinqit/compiler/native_compiler.py b/spinqit/compiler/native_compiler.py index afb3a98..1fc7013 100644 --- a/spinqit/compiler/native_compiler.py +++ b/spinqit/compiler/native_compiler.py @@ -17,6 +17,7 @@ from spinqit.model import Gate, Circuit, MatrixGate, ControlledGate, InverseGate, MultiControlledMatrixGate from spinqit.model import UnsupportedGateError from spinqit.model.instruction import Instruction +from spinqit.model.parameter import LazyParameter from .ir import IntermediateRepresentation as IR, NodeType from .translator.gate_converter import is_primary_gate, decompose_single_qubit_gate, decompose_multi_qubit_gate from .optimizer import PassManager @@ -64,7 +65,12 @@ def handle_primary_gate(self, ir: IR, inst: Instruction, condition: Tuple): ir.add_caller_matrix(vindex, unitary) elif isinstance(gate, ControlledGate) and (isinstance(gate.base_gate, MatrixGate) or gate.base_gate in IR.basis_set): - unitary = gate.base_gate.get_matrix(*inst.params) + has_lazy_params = any( + isinstance(param, LazyParameter) for param in _flatten(inst.params) + ) + unitary = None + if len(gate.factors) == 0 or not has_lazy_params: + unitary = gate.base_gate.get_matrix(*inst.params) ctrl_bits = gate.control_bits if gate.base_gate == CX: unitary = X.get_matrix() @@ -84,7 +90,8 @@ def handle_primary_gate(self, ir: IR, inst: Instruction, condition: Tuple): else: self.add_definition_cluster(ir, gate, len(inst.params), len(inst.qubits), len(inst.clbits)) vindex = ir.add_caller_node(gate.label, inst.params, inst.qubits) - ir.add_caller_matrix(vindex, unitary, ctrl_bits) + if unitary is not None: + ir.add_caller_matrix(vindex, unitary, ctrl_bits) elif isinstance(gate, InverseGate) and isinstance(gate.base_gate, MatrixGate): unitary = gate.base_gate.get_matrix(*inst.params) inverse_flag = True diff --git a/tests/test_review_e4_torch_controlled_phase_gradient.py b/tests/test_review_e4_torch_controlled_phase_gradient.py new file mode 100644 index 0000000..2fb1e9d --- /dev/null +++ b/tests/test_review_e4_torch_controlled_phase_gradient.py @@ -0,0 +1,48 @@ +import math +import unittest + +import torch + +from spinqit import CP, H, X, Circuit +from spinqit.algorithm.loss import probs +from spinqit.interface.qlayer import QLayer + + +class TorchControlledPhaseGradientRegressionTest(unittest.TestCase): + @staticmethod + def build_circuit(): + circuit = Circuit() + qubits = circuit.allocateQubits(2) + theta = circuit.add_params(1) + circuit << (X, qubits[0]) + circuit << (H, qubits[1]) + circuit << (CP, qubits, theta[0]) + circuit << (H, qubits[1]) + return circuit + + def test_trainable_controlled_phase_compiles_and_backpropagates(self): + angle = 0.4 + for level in range(4): + with self.subTest(level=level): + layer = QLayer( + self.build_circuit(), + probs(), + backend_mode='torch', + interface='torch', + grad_method='backprop', + optimization_level=level, + ) + theta = torch.tensor([angle], dtype=torch.float64, requires_grad=True) + probability = layer(theta)[2] + probability.backward() + + self.assertAlmostEqual( + probability.item(), (1 + math.cos(angle)) / 2, places=6 + ) + self.assertAlmostEqual( + theta.grad.item(), -math.sin(angle) / 2, places=6 + ) + + +if __name__ == '__main__': + unittest.main() From 91de3de1ad8fbff4e45b3285392a2eb92b5bd3b4 Mon Sep 17 00:00:00 2001 From: wuzp15 Date: Wed, 22 Jul 2026 19:01:03 +0800 Subject: [PATCH 46/59] differentiate composite gate parameters in adjoint --- spinqit/grad/spinq_grad.py | 26 ++++++++--- ...test_review_e5_adjoint_controlled_phase.py | 45 +++++++++++++++++++ 2 files changed, 65 insertions(+), 6 deletions(-) create mode 100644 tests/test_review_e5_adjoint_controlled_phase.py diff --git a/spinqit/grad/spinq_grad.py b/spinqit/grad/spinq_grad.py index b45aafb..8429bd6 100644 --- a/spinqit/grad/spinq_grad.py +++ b/spinqit/grad/spinq_grad.py @@ -57,8 +57,8 @@ def _op_node(gname, qubit_num, bra, ket, grads, func, node_params, qubits, total if func is not None and node_params is not None: for i, param in enumerate(node_params): if requires_grad(param): - func = func[i] - coeffs = egrad(func)(total_params) + param_func = func[i] + coeffs = egrad(param_func)(total_params) gen_mat = generator[label] d_theta = 1j * gen_mat @ mat tmp_ket = _apply_gate(ket, d_theta, qubits, qubit_num) @@ -84,19 +84,33 @@ def _caller_node(gname, qubits_num, bra, ket, grads, func, params, qubits, graph node = graph.vs[node_idx] if node.index != def_node.index: local = [qubits[i] for i in node['qubits']] - plambda = node['params'] if 'params' in node.attributes else None - callee_params = [f(params) for f in plambda] if not plambda else [] + plambda = node['params'] if 'params' in node.attributes() else [] + plambda = plambda or [] + callee_params = [ + f(params) if callable(f) else f for f in plambda + ] + callee_funcs = [] + for param_lambda in plambda: + if callable(param_lambda): + def composed(total, inner=param_lambda): + caller_params = [ + f(total) if callable(f) else f for f in func + ] + return inner(caller_params) + callee_funcs.append(composed) + else: + callee_funcs.append(param_lambda) if node['type'] == 3: bra, ket = _op_node(node['name'], qubits_num, bra, ket, grads, - func, callee_params, local, + callee_funcs, callee_params, local, total_params, dy) elif node['type'] == 1: bra, ket = _caller_node(node['name'], qubits_num, bra, ket, grads, - func, callee_params, + callee_funcs, callee_params, local, graph, total_params, dy) return bra, ket diff --git a/tests/test_review_e5_adjoint_controlled_phase.py b/tests/test_review_e5_adjoint_controlled_phase.py new file mode 100644 index 0000000..277bf20 --- /dev/null +++ b/tests/test_review_e5_adjoint_controlled_phase.py @@ -0,0 +1,45 @@ +import math +import unittest + +import numpy as np + +from spinqit import CP, H, X, Circuit, Parameter +from spinqit.algorithm.loss import expval +from spinqit.grad import qgrad +from spinqit.interface.qlayer import QLayer + + +class AdjointControlledPhaseRegressionTest(unittest.TestCase): + @staticmethod + def build_layer(): + circuit = Circuit() + qubits = circuit.allocateQubits(2) + theta = circuit.add_params(1) + circuit << (X, qubits[0]) + circuit << (H, qubits[1]) + circuit << (CP, qubits, theta[0]) + circuit << (H, qubits[1]) + + projector_10 = np.diag([0.0, 0.0, 1.0, 0.0]) + return QLayer( + circuit, + expval(projector_10), + backend_mode='spinq', + interface='spinq', + grad_method='adjoint_differentiation', + ) + + def test_adjoint_descends_into_parameterized_controlled_gate(self): + angle = 0.4 + theta = Parameter([angle]) + layer = self.build_layer() + + value = layer(theta) + gradient = qgrad(layer)(theta)[0] + + self.assertAlmostEqual(value.item(), (1 + math.cos(angle)) / 2, places=7) + self.assertAlmostEqual(gradient.item(), -math.sin(angle) / 2, places=7) + + +if __name__ == '__main__': + unittest.main() From a7c675d3fb16755d55a1c4e583c4e71104409295 Mon Sep 17 00:00:00 2001 From: wuzp15 Date: Wed, 22 Jul 2026 19:03:37 +0800 Subject: [PATCH 47/59] support gradients for every circuit argument --- spinqit/grad/_grad.py | 4 +- spinqit/interface/spinq_interface.py | 12 ++-- ...eview_e6_spinq_multi_parameter_gradient.py | 71 +++++++++++++++++++ 3 files changed, 80 insertions(+), 7 deletions(-) create mode 100644 tests/test_review_e6_spinq_multi_parameter_gradient.py diff --git a/spinqit/grad/_grad.py b/spinqit/grad/_grad.py index 64e85c5..b5604c2 100644 --- a/spinqit/grad/_grad.py +++ b/spinqit/grad/_grad.py @@ -24,7 +24,9 @@ def __init__(self, fun): self._fun = fun def __call__(self, *params): - self._grad_fn = self._get_grad_fn(self._fun) + self._grad_fn = self._get_grad_fn( + self._fun, argnum=tuple(range(len(params))) + ) grad_value, ans = self._grad_fn(*params) self._forward = ans return grad_value diff --git a/spinqit/interface/spinq_interface.py b/spinqit/interface/spinq_interface.py index e110df5..b30b779 100644 --- a/spinqit/interface/spinq_interface.py +++ b/spinqit/interface/spinq_interface.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. try: - from autograd.extend import primitive, defvjp + from autograd.extend import primitive, defvjp_argnum except ImportError: raise ImportError( @@ -57,7 +57,7 @@ def execute(qlayer, *params): return loss -def execute_vjp(ans, qlayer, *params, **kwargs): +def execute_vjp(argnum, ans, args, kwargs): """ Return the grads when computing over the ir, for `spinq` interface only. @@ -77,18 +77,18 @@ def execute_vjp(ans, qlayer, *params, **kwargs): Or it may cause some gradients problems """ - del ans, kwargs, params + del ans, kwargs + qlayer = args[0] backward_fn = qlayer.backward_fn - delattr(qlayer, 'backward_fn') def grad_fn(g): if callable(backward_fn): grads = backward_fn(g.conj()) else: grads = backward_fn * g.conj() - return tuple(Parameter(g) for g in grads) + return Parameter(grads[argnum - 1]) return grad_fn -defvjp(execute, execute_vjp, argnums=(1,)) +defvjp_argnum(execute, execute_vjp) diff --git a/tests/test_review_e6_spinq_multi_parameter_gradient.py b/tests/test_review_e6_spinq_multi_parameter_gradient.py new file mode 100644 index 0000000..c8862b8 --- /dev/null +++ b/tests/test_review_e6_spinq_multi_parameter_gradient.py @@ -0,0 +1,71 @@ +import math +import unittest + +from autograd import grad + +from spinqit import Circuit, Parameter, Ry, Rz, X +from spinqit.algorithm.loss import expval +from spinqit.grad import qgrad +from spinqit.interface.qlayer import QLayer + + +class SpinQMultiParameterGradientRegressionTest(unittest.TestCase): + @staticmethod + def build_layer(): + circuit = Circuit() + qubit = circuit.allocateQubits(1)[0] + alpha = circuit.add_params(1) + beta = circuit.add_params(1) + circuit << (Ry, qubit, alpha[0]) + circuit << (Rz, qubit, beta[0]) + return QLayer( + circuit, + expval(X.get_matrix()), + backend_mode='spinq', + interface='spinq', + grad_method='adjoint_differentiation', + ) + + def test_first_circuit_argument_has_its_own_vjp(self): + alpha = Parameter([0.4]) + beta = Parameter([0.3]) + + derivative = grad(self.build_layer(), argnum=0)(alpha, beta) + + self.assertEqual(derivative.shape, (1,)) + self.assertAlmostEqual( + derivative.item(), math.cos(alpha.item()) * math.cos(beta.item()), places=7 + ) + + def test_second_circuit_argument_has_its_own_vjp(self): + alpha = Parameter([0.4]) + beta = Parameter([0.3]) + + derivative = grad(self.build_layer(), argnum=1)(alpha, beta) + + self.assertEqual(derivative.shape, (1,)) + self.assertAlmostEqual( + derivative.item(), -math.sin(alpha.item()) * math.sin(beta.item()), places=7 + ) + + def test_qgrad_returns_one_derivative_per_circuit_argument(self): + alpha = Parameter([0.4]) + beta = Parameter([0.3]) + + derivatives = qgrad(self.build_layer())(alpha, beta) + + self.assertEqual(len(derivatives), 2) + self.assertAlmostEqual( + derivatives[0].item(), + math.cos(alpha.item()) * math.cos(beta.item()), + places=7, + ) + self.assertAlmostEqual( + derivatives[1].item(), + -math.sin(alpha.item()) * math.sin(beta.item()), + places=7, + ) + + +if __name__ == '__main__': + unittest.main() From f5a2c008428f7332ca07514a5a34baad8a489477 Mon Sep 17 00:00:00 2001 From: wuzp15 Date: Wed, 22 Jul 2026 19:04:23 +0800 Subject: [PATCH 48/59] fix torch dense hamiltonian conversion --- spinqit/backend/pytorch_backend.py | 4 ++- .../test_review_e7_torch_dense_expectation.py | 35 +++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 tests/test_review_e7_torch_dense_expectation.py diff --git a/spinqit/backend/pytorch_backend.py b/spinqit/backend/pytorch_backend.py index 46913ca..2a1d2f1 100644 --- a/spinqit/backend/pytorch_backend.py +++ b/spinqit/backend/pytorch_backend.py @@ -462,7 +462,9 @@ def evaluate(self, ir, config, measure_op): f'Expected `np.ndarray, sparse.csr_matrix, list`, but got `{type(hamiltonian)}`' ) if isinstance(hamiltonian, onp.ndarray): - hamiltonian = torch.as_tensor(hamiltonian, dtype, device) + hamiltonian = torch.as_tensor( + hamiltonian, dtype=dtype, device=device + ) elif isinstance(hamiltonian, sparse.csr_matrix): hamiltonian = self._scipy_sparse_mat_to_torch_sparse_tensor(hamiltonian) else: diff --git a/tests/test_review_e7_torch_dense_expectation.py b/tests/test_review_e7_torch_dense_expectation.py new file mode 100644 index 0000000..cba7dca --- /dev/null +++ b/tests/test_review_e7_torch_dense_expectation.py @@ -0,0 +1,35 @@ +import math +import unittest + +import torch + +from spinqit import Circuit, Ry, Z +from spinqit.algorithm.loss import expval +from spinqit.interface.qlayer import QLayer + + +class TorchDenseExpectationRegressionTest(unittest.TestCase): + def test_dense_hamiltonian_supports_backpropagation(self): + circuit = Circuit() + qubit = circuit.allocateQubits(1)[0] + theta = circuit.add_params(1) + circuit << (Ry, qubit, theta[0]) + layer = QLayer( + circuit, + expval(Z.get_matrix()), + backend_mode='torch', + interface='torch', + grad_method='backprop', + ) + + angle = 0.4 + parameter = torch.tensor([angle], requires_grad=True) + value = layer(parameter) + value.backward() + + self.assertAlmostEqual(value.item(), math.cos(angle), places=6) + self.assertAlmostEqual(parameter.grad.item(), -math.sin(angle), places=6) + + +if __name__ == '__main__': + unittest.main() From 89ff8541a70b77e6a48731725c188bcac6d8dd14 Mon Sep 17 00:00:00 2001 From: wuzp15 Date: Wed, 22 Jul 2026 19:05:41 +0800 Subject: [PATCH 49/59] preserve torch pauli expectation batches --- spinqit/backend/pytorch_backend.py | 5 +-- ...view_e8_torch_batched_pauli_expectation.py | 32 +++++++++++++++++++ 2 files changed, 33 insertions(+), 4 deletions(-) create mode 100644 tests/test_review_e8_torch_batched_pauli_expectation.py diff --git a/spinqit/backend/pytorch_backend.py b/spinqit/backend/pytorch_backend.py index 2a1d2f1..eb1f019 100644 --- a/spinqit/backend/pytorch_backend.py +++ b/spinqit/backend/pytorch_backend.py @@ -396,10 +396,7 @@ def torch_pauli_expectation(pauli_string, probabilities): raise ValueError('The input string is not a Pauli string') f = functools.reduce(torch.kron, mat) - if len(probabilities.shape) < 2: - probabilities = probabilities.unsqueeze(0) - expect_val = (probabilities * f).sum(dim=1) - return expect_val[0] + return (probabilities * f).sum(dim=-1) class TorchSimulatorBackend(BaseBackend): diff --git a/tests/test_review_e8_torch_batched_pauli_expectation.py b/tests/test_review_e8_torch_batched_pauli_expectation.py new file mode 100644 index 0000000..c005dd4 --- /dev/null +++ b/tests/test_review_e8_torch_batched_pauli_expectation.py @@ -0,0 +1,32 @@ +import unittest + +import torch + +from spinqit import Circuit, StateVector +from spinqit.algorithm.loss import expval +from spinqit.interface.qlayer import QLayer + + +class TorchBatchedPauliExpectationRegressionTest(unittest.TestCase): + def test_pauli_list_returns_one_expectation_per_batched_state(self): + circuit = Circuit() + qubit = circuit.allocateQubits(1)[0] + states = circuit.add_params((2, 2)) + circuit << (StateVector, qubit, states[:]) + layer = QLayer( + circuit, + expval([('Z', 1.0)]), + backend_mode='torch', + interface='torch', + grad_method='backprop', + ) + + batch = torch.tensor([[1.0, 0.0], [0.0, 1.0]]) + values = layer(batch) + + self.assertEqual(tuple(values.shape), (2,)) + torch.testing.assert_close(values, torch.tensor([1.0, -1.0])) + + +if __name__ == '__main__': + unittest.main() From ab47d542e8d57cb50d3dd09da1380d8db069d09c Mon Sep 17 00:00:00 2001 From: wuzp15 Date: Wed, 22 Jul 2026 19:06:46 +0800 Subject: [PATCH 50/59] adapt list hamiltonians for adjoint gradients --- spinqit/interface/qlayer.py | 7 +++ ..._review_e9_variational_list_hamiltonian.py | 44 +++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 tests/test_review_e9_variational_list_hamiltonian.py diff --git a/spinqit/interface/qlayer.py b/spinqit/interface/qlayer.py index a921e2a..5cc84de 100644 --- a/spinqit/interface/qlayer.py +++ b/spinqit/interface/qlayer.py @@ -52,6 +52,13 @@ def __init__(self, self.place_holder = tuple(x[1] for x in sorted(circuit.place_holder, key=lambda x: x[0])) self.ir = deepcopy(ir) self.measure_op = measure + if (grad_method == 'adjoint_differentiation' + and isinstance(getattr(measure, 'hamiltonian', None), list)): + from spinqit.primitive.pauli_expectation import generate_hamiltonian_matrix + self.measure_op = deepcopy(measure) + self.measure_op.hamiltonian = generate_hamiltonian_matrix( + measure.hamiltonian + ) self.backend_mode = backend_mode self.interface = interface self.qubits_num = ir.qnum diff --git a/tests/test_review_e9_variational_list_hamiltonian.py b/tests/test_review_e9_variational_list_hamiltonian.py new file mode 100644 index 0000000..2baaff1 --- /dev/null +++ b/tests/test_review_e9_variational_list_hamiltonian.py @@ -0,0 +1,44 @@ +import unittest + +import numpy as np + +from spinqit.algorithm import QAOA, VQE +from spinqit.grad import qgrad + + +class OneGradientOptimizer: + def __init__(self): + self.gradient = None + + def optimize(self, layer, *params): + gradient_fn = qgrad(layer) + self.gradient = gradient_fn(*params) + return [gradient_fn.forward] + + +class VariationalListHamiltonianRegressionTest(unittest.TestCase): + def test_vqe_default_adjoint_accepts_documented_list_hamiltonian(self): + optimizer = OneGradientOptimizer() + vqe = VQE([('Z', 1.0)], optimizer, depth=1) + + losses = vqe.run() + + self.assertEqual(len(losses), 1) + self.assertTrue(np.isfinite(losses[0])) + self.assertEqual(optimizer.gradient[0].shape, vqe.optimized_params.shape) + self.assertIsInstance(vqe.hamiltonian, list) + + def test_qaoa_default_adjoint_accepts_documented_list_hamiltonian(self): + optimizer = OneGradientOptimizer() + qaoa = QAOA([('Z', 1.0)], optimizer, depth=1) + + losses = qaoa.run() + + self.assertEqual(len(losses), 1) + self.assertTrue(np.isfinite(losses[0])) + self.assertEqual(optimizer.gradient[0].shape, qaoa.optimized_params.shape) + self.assertIsInstance(qaoa.hamiltonian, list) + + +if __name__ == '__main__': + unittest.main() From d09657223b5c9997a3e19d89a74ae76131918ab0 Mon Sep 17 00:00:00 2001 From: wuzp15 Date: Wed, 22 Jul 2026 19:07:51 +0800 Subject: [PATCH 51/59] initialize qaoa qubits for custom ansatz --- spinqit/algorithm/qaoa.py | 26 ++++++++----- ...t_review_e10_qaoa_custom_problem_ansatz.py | 39 +++++++++++++++++++ 2 files changed, 55 insertions(+), 10 deletions(-) create mode 100644 tests/test_review_e10_qaoa_custom_problem_ansatz.py diff --git a/spinqit/algorithm/qaoa.py b/spinqit/algorithm/qaoa.py index 688c9c2..4e6bd7e 100644 --- a/spinqit/algorithm/qaoa.py +++ b/spinqit/algorithm/qaoa.py @@ -39,17 +39,23 @@ def __init__( self.qlayer = None self.optimizer = optimizer + if isinstance(problem, list): + self.__qubit_num = len(problem[0][0]) + problem_terms = problem + elif isinstance(problem, sparse.csr_matrix): + self.__qubit_num = int(np.log2(problem.get_shape()[0])) + problem_terms = pauli_decompose(problem.toarray()) + else: + raise ValueError( + f'The `problem` should be `list` or `sparse.csr_matrix`, but got {type(problem)} ' + ) + if problem_ansatz is None: - if isinstance(problem, list): - self.__qubit_num = len(problem[0][0]) - problem_ansatz = self._generate_problem_circuit(problem) - elif isinstance(problem, sparse.csr_matrix): - self.__qubit_num = int(np.log2(problem.get_shape()[0])) - problem_ansatz = self._generate_problem_circuit(pauli_decompose(problem.toarray())) - else: - raise ValueError( - f'The `problem` should be `list` or `sparse.csr_matrix`, but got {type(problem)} ' - ) + problem_ansatz = self._generate_problem_circuit(problem_terms) + elif problem_ansatz.qubit_num != self.__qubit_num: + raise ValueError( + 'The custom problem ansatz must act on the same number of qubits as the problem.' + ) self.hamiltonian = problem self.__depth = depth self.params = Parameter(np.random.uniform(0, 2 * np.pi, (self.__depth*2,))) diff --git a/tests/test_review_e10_qaoa_custom_problem_ansatz.py b/tests/test_review_e10_qaoa_custom_problem_ansatz.py new file mode 100644 index 0000000..638b415 --- /dev/null +++ b/tests/test_review_e10_qaoa_custom_problem_ansatz.py @@ -0,0 +1,39 @@ +import unittest + +import numpy as np + +from spinqit import Rz +from spinqit.algorithm import QAOA +from spinqit.grad import qgrad + + +class OneGradientOptimizer: + def __init__(self): + self.gradient = None + + def optimize(self, layer, *params): + gradient_fn = qgrad(layer) + self.gradient = gradient_fn(*params) + return [gradient_fn.forward] + + +class QaoaCustomProblemAnsatzRegressionTest(unittest.TestCase): + def test_custom_problem_ansatz_uses_problem_qubit_count(self): + optimizer = OneGradientOptimizer() + qaoa = QAOA( + [('Z', 1.0)], + optimizer, + depth=1, + problem_ansatz=Rz, + ) + + losses = qaoa.run() + + self.assertEqual(qaoa.circuit.qnum, 1) + self.assertEqual(len(losses), 1) + self.assertTrue(np.isfinite(losses[0])) + self.assertEqual(optimizer.gradient[0].shape, qaoa.optimized_params.shape) + + +if __name__ == '__main__': + unittest.main() From ab603ded3ebd2ed695507f453196e7f07dc481fa Mon Sep 17 00:00:00 2001 From: wuzp15 Date: Wed, 22 Jul 2026 19:09:28 +0800 Subject: [PATCH 52/59] preserve phase in nested controlled gates --- spinqit/compiler/native_compiler.py | 22 ++++-- ...t_review_e11_nested_controlled_hadamard.py | 78 +++++++++++++++++++ 2 files changed, 95 insertions(+), 5 deletions(-) create mode 100644 tests/test_review_e11_nested_controlled_hadamard.py diff --git a/spinqit/compiler/native_compiler.py b/spinqit/compiler/native_compiler.py index 1fc7013..e6a0bb3 100644 --- a/spinqit/compiler/native_compiler.py +++ b/spinqit/compiler/native_compiler.py @@ -143,12 +143,24 @@ def compile(self, circ: Circuit, level: int) -> IR: for inst in circ.instructions: gate = inst.gate - if (isinstance(gate, ControlledGate) - and isinstance(gate.base_gate, MatrixGate) - and len(gate.factors) == 0): + lower_controlled = ( + isinstance(gate, ControlledGate) + and not any( + isinstance(param, LazyParameter) + for param in _flatten(inst.params) + ) + and ( + (isinstance(gate.base_gate, MatrixGate) + and len(gate.factors) == 0) + or (gate.control_bits > 1 + and gate.base_gate.qubit_num == 1) + ) + ) + if lower_controlled: # A raw controlled matrix becomes an IR ``unitary`` node, which - # execution and serialization backends do not implement. Lower - # it to the native basis before constructing the IR instead. + # execution and serialization backends do not implement. Nested + # control also makes a subgate's global phase observable. Lower + # both forms from the exact base matrix before constructing IR. from spinqit.primitive.multi_controlled_gate_builder import MultiControlledGateBuilder lowered_gate = MultiControlledGateBuilder( gate.control_bits, gate.base_gate, inst.params diff --git a/tests/test_review_e11_nested_controlled_hadamard.py b/tests/test_review_e11_nested_controlled_hadamard.py new file mode 100644 index 0000000..dfbf200 --- /dev/null +++ b/tests/test_review_e11_nested_controlled_hadamard.py @@ -0,0 +1,78 @@ +from copy import deepcopy +import contextlib +import io +import os +import tempfile +import unittest + +import numpy as np + +from spinqit import ( + BasicSimulatorConfig, + Circuit, + ControlledGate, + H, + TorchSimulatorConfig, + X, + get_basic_simulator, + get_compiler, + get_torch_simulator, +) +from spinqit.backend.qasm_backend import QasmBackend +from spinqit.backend.spinq_cloud_backend import SpinQCloudBackend +from spinqit.model.spinqCloud.platform import SQC_25 + + +class NestedControlledHadamardRegressionTest(unittest.TestCase): + @staticmethod + def build_circuit(): + circuit = Circuit() + qubits = circuit.allocateQubits(3) + circuit << (X, qubits[1]) + circuit << (H, qubits[0]) + circuit << (ControlledGate(ControlledGate(H)), qubits) + return circuit + + @staticmethod + def assert_expected_state(states, tolerance=1e-8): + expected = np.zeros(8, dtype=complex) + expected[2] = 1 / np.sqrt(2) + expected[6] = 0.5 + expected[7] = 0.5 + states = np.asarray(states) + if states.ndim > 1: + states = states[0] + assert abs(np.vdot(expected, states)) ** 2 >= 1 - tolerance + + @staticmethod + def compile_qasm(source): + with tempfile.NamedTemporaryFile('w', suffix='.qasm', delete=False) as qasm_file: + qasm_file.write(source) + path = qasm_file.name + try: + with contextlib.redirect_stderr(io.StringIO()): + return get_compiler('qasm').compile(path, 0) + finally: + os.unlink(path) + + def test_nested_control_preserves_hadamard_phase_on_all_lowering_paths(self): + ir = get_compiler('native').compile(self.build_circuit(), 0) + + basic = get_basic_simulator().execute(deepcopy(ir), BasicSimulatorConfig()) + self.assert_expected_state(basic.states) + + torch = get_torch_simulator().execute(deepcopy(ir), TorchSimulatorConfig()) + self.assert_expected_state(torch.states.detach().cpu().numpy(), tolerance=1e-6) + + qasm_ir = self.compile_qasm(QasmBackend.convert_ir_to_qasm(ir)) + qasm = get_basic_simulator().execute(qasm_ir, BasicSimulatorConfig()) + self.assert_expected_state(qasm.states) + + cloud_ir = deepcopy(ir) + object.__new__(SpinQCloudBackend).assemble(SQC_25.code, cloud_ir) + cloud = get_basic_simulator().execute(cloud_ir, BasicSimulatorConfig()) + self.assert_expected_state(cloud.states) + + +if __name__ == '__main__': + unittest.main() From 01eac854f0026e6d5e07d50b6a9050c449d0375b Mon Sep 17 00:00:00 2001 From: wuzp15 Date: Wed, 22 Jul 2026 19:11:01 +0800 Subject: [PATCH 53/59] isolate pauli measurements from gate definitions --- spinqit/backend/backend_util.py | 18 ++++++++-- ...w_e12_pauli_measurement_after_composite.py | 35 +++++++++++++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 tests/test_review_e12_pauli_measurement_after_composite.py diff --git a/spinqit/backend/backend_util.py b/spinqit/backend/backend_util.py index a372e5f..d76264a 100644 --- a/spinqit/backend/backend_util.py +++ b/spinqit/backend/backend_util.py @@ -71,7 +71,22 @@ def _add_pauli_gate(gate, qubits, ir): idx = idx if idx is not False else ir.dag.vcount() node_idx_list = [] if idx == ir.dag.vcount(): - seq = ir.dag.vs.select(lambda x: len(x.out_edges()) != len(x.in_edges()) and x['type'] != 4) + execution_nodes = set() + for register in ir.dag.vs.select(type=NodeType.register.value): + execution_nodes.update( + ir.dag.subcomponent(register.index, mode='out') + ) + quantum_node_types = { + NodeType.op.value, + NodeType.caller.value, + NodeType.init_qubit.value, + NodeType.unitary.value, + } + seq = ir.dag.vs.select( + lambda x: x.index in execution_nodes + and x['type'] in quantum_node_types + and len(x.out_edges()) != len(x.in_edges()) + ) node_map = {} for v in seq: if v['qubits'] is None: @@ -90,4 +105,3 @@ def _add_pauli_gate(gate, qubits, ir): node_idx_list = ir.substitute_nodes([idx], ilist, 0) ir.remove_nodes([idx]) return node_idx_list - diff --git a/tests/test_review_e12_pauli_measurement_after_composite.py b/tests/test_review_e12_pauli_measurement_after_composite.py new file mode 100644 index 0000000..0d3098a --- /dev/null +++ b/tests/test_review_e12_pauli_measurement_after_composite.py @@ -0,0 +1,35 @@ +import unittest + +from spinqit import CP, X, Circuit +from spinqit.algorithm.loss import expval +from spinqit.interface.qlayer import QLayer + + +class PauliMeasurementAfterCompositeRegressionTest(unittest.TestCase): + @staticmethod + def build_circuit(): + circuit = Circuit() + qubits = circuit.allocateQubits(2) + circuit.allocateClbits(1) + circuit << (X, qubits[0]) + circuit << (CP, qubits, 0.4) + return circuit + + def test_pauli_measurement_ignores_definition_subgraphs_when_finding_leaves(self): + for backend, interface in (('spinq', 'spinq'), ('torch', 'torch')): + with self.subTest(backend=backend): + layer = QLayer( + self.build_circuit(), + expval([('ZZ', 1.0)]), + backend_mode=backend, + interface=interface, + grad_method='backprop' if backend == 'torch' else 'param_shift', + ) + + value = layer() + + self.assertAlmostEqual(float(value), -1.0, places=6) + + +if __name__ == '__main__': + unittest.main() From 27e0ea9994b8fe204bbb3d7309dca8a366f1e73b Mon Sep 17 00:00:00 2001 From: wuzp15 Date: Wed, 22 Jul 2026 19:13:17 +0800 Subject: [PATCH 54/59] avoid deprecated torch probability shape conversion --- spinqit/backend/pytorch_backend.py | 2 +- ...test_review_e13_torch_probability_shape.py | 26 +++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 tests/test_review_e13_torch_probability_shape.py diff --git a/spinqit/backend/pytorch_backend.py b/spinqit/backend/pytorch_backend.py index eb1f019..2626535 100644 --- a/spinqit/backend/pytorch_backend.py +++ b/spinqit/backend/pytorch_backend.py @@ -196,7 +196,7 @@ def raw_probabilities(self): @staticmethod def _process_prob(config, np_states): - qubit_num = int(onp.log2(np_states.shape[-1:])) + qubit_num = int(onp.log2(np_states.shape[-1])) probabilities = torch.abs(np_states) ** 2 higher_dim = list(probabilities.shape[:-1]) if config.mqubits is not None: diff --git a/tests/test_review_e13_torch_probability_shape.py b/tests/test_review_e13_torch_probability_shape.py new file mode 100644 index 0000000..2f024a5 --- /dev/null +++ b/tests/test_review_e13_torch_probability_shape.py @@ -0,0 +1,26 @@ +import unittest +import warnings + +from spinqit import Circuit, H, TorchSimulatorConfig, get_compiler, get_torch_simulator + + +class TorchProbabilityShapeRegressionTest(unittest.TestCase): + def test_probability_dimension_does_not_convert_numpy_array_to_scalar(self): + circuit = Circuit() + qubit = circuit.allocateQubits(1)[0] + circuit << (H, qubit) + result = get_torch_simulator().execute( + get_compiler('native').compile(circuit, 0), + TorchSimulatorConfig(), + ) + + with warnings.catch_warnings(): + warnings.simplefilter('error', DeprecationWarning) + probabilities = result.raw_probabilities + + self.assertEqual(tuple(probabilities.shape), (2,)) + self.assertAlmostEqual(float(probabilities.sum()), 1.0, places=6) + + +if __name__ == '__main__': + unittest.main() From bc038a56c485e7f89af3836ff43eb15bcfddc49b Mon Sep 17 00:00:00 2001 From: wuzp15 Date: Wed, 22 Jul 2026 19:17:56 +0800 Subject: [PATCH 55/59] shift decomposed controlled rotations --- spinqit/compiler/native_compiler.py | 25 +++++-- spinqit/grad/torch_grad.py | 2 +- ...e14_controlled_rotation_parameter_shift.py | 67 +++++++++++++++++++ 3 files changed, 88 insertions(+), 6 deletions(-) create mode 100644 tests/test_review_e14_controlled_rotation_parameter_shift.py diff --git a/spinqit/compiler/native_compiler.py b/spinqit/compiler/native_compiler.py index e6a0bb3..f2c9b93 100644 --- a/spinqit/compiler/native_compiler.py +++ b/spinqit/compiler/native_compiler.py @@ -143,12 +143,18 @@ def compile(self, circ: Circuit, level: int) -> IR: for inst in circ.instructions: gate = inst.gate + has_lazy_params = any( + isinstance(param, LazyParameter) + for param in _flatten(inst.params) + ) + lower_symbolic_controlled = ( + isinstance(gate, ControlledGate) + and len(gate.factors) > 0 + and has_lazy_params + ) lower_controlled = ( isinstance(gate, ControlledGate) - and not any( - isinstance(param, LazyParameter) - for param in _flatten(inst.params) - ) + and not has_lazy_params and ( (isinstance(gate.base_gate, MatrixGate) and len(gate.factors) == 0) @@ -156,7 +162,16 @@ def compile(self, circ: Circuit, level: int) -> IR: and gate.base_gate.qubit_num == 1) ) ) - if lower_controlled: + if lower_symbolic_controlled: + # Parameter-shift rules apply to the native rotations in the + # decomposition, not generally to the controlled gate as a + # whole because its generator can have three eigenvalues. + ilist = decompose_multi_qubit_gate( + gate, inst.qubits, inst.params + ) + for i in ilist: + self.handle_primary_gate(ir, i, inst.condition) + elif lower_controlled: # A raw controlled matrix becomes an IR ``unitary`` node, which # execution and serialization backends do not implement. Nested # control also makes a subgate's global phase observable. Lower diff --git a/spinqit/grad/torch_grad.py b/spinqit/grad/torch_grad.py index 613d196..a400536 100644 --- a/spinqit/grad/torch_grad.py +++ b/spinqit/grad/torch_grad.py @@ -80,7 +80,7 @@ def backward_fn(dy): func = v['func'] origin_param = [x for x in v['params']] for i in range(len(origin_param)): - if callable(func[i]) and requires_grad(origin_param[i]): + if callable(func[i]): v['params'][i] = origin_param[i] + torch.pi / (4 * r) value1, _ = backend.evaluate(ir, config, measure_op) v['params'][i] = origin_param[i] - torch.pi / (4 * r) diff --git a/tests/test_review_e14_controlled_rotation_parameter_shift.py b/tests/test_review_e14_controlled_rotation_parameter_shift.py new file mode 100644 index 0000000..dfd1775 --- /dev/null +++ b/tests/test_review_e14_controlled_rotation_parameter_shift.py @@ -0,0 +1,67 @@ +import math +import unittest + +import numpy as np +import torch + +from spinqit import Circuit, ControlledGate, H, I, Parameter, Ry, X +from spinqit.algorithm.loss import expval +from spinqit.grad import qgrad +from spinqit.interface.qlayer import QLayer + + +class ControlledRotationParameterShiftRegressionTest(unittest.TestCase): + @staticmethod + def build_circuit(): + circuit = Circuit() + qubits = circuit.allocateQubits(2) + theta = circuit.add_params(1) + circuit << (H, qubits[0]) + circuit << (ControlledGate(Ry), qubits, theta[0]) + return circuit + + @staticmethod + def hamiltonian(): + return np.kron(X.get_matrix(), I.get_matrix()) + + def test_spinq_parameter_shift_handles_controlled_rotation_spectrum(self): + angle = 0.6 + layer = QLayer( + self.build_circuit(), + expval(self.hamiltonian()), + backend_mode='spinq', + interface='spinq', + grad_method='param_shift', + ) + parameter = Parameter([angle]) + + value = layer(parameter) + derivative = qgrad(layer)(parameter)[0] + + self.assertAlmostEqual(value.item(), math.cos(angle / 2), places=7) + self.assertAlmostEqual( + derivative.item(), -math.sin(angle / 2) / 2, places=7 + ) + + def test_torch_parameter_shift_handles_controlled_rotation_spectrum(self): + angle = 0.6 + layer = QLayer( + self.build_circuit(), + expval(self.hamiltonian()), + backend_mode='torch', + interface='torch', + grad_method='param_shift', + ) + parameter = torch.tensor([angle], requires_grad=True) + + value = layer(parameter) + value.backward() + + self.assertAlmostEqual(value.item(), math.cos(angle / 2), places=6) + self.assertAlmostEqual( + parameter.grad.item(), -math.sin(angle / 2) / 2, places=6 + ) + + +if __name__ == '__main__': + unittest.main() From afb435088ede45bf15a767a34e8536fe19d08f5e Mon Sep 17 00:00:00 2001 From: wuzp15 Date: Wed, 22 Jul 2026 19:19:00 +0800 Subject: [PATCH 56/59] align pauli terms with circuit qubit order --- spinqit/primitive/pauli_expectation.py | 4 +-- tests/test_review_e15_pauli_qubit_order.py | 34 ++++++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 tests/test_review_e15_pauli_qubit_order.py diff --git a/spinqit/primitive/pauli_expectation.py b/spinqit/primitive/pauli_expectation.py index 2540edb..65e4d58 100644 --- a/spinqit/primitive/pauli_expectation.py +++ b/spinqit/primitive/pauli_expectation.py @@ -42,7 +42,7 @@ def calculate_pauli_expectation(pauli_string: str, probabilities: Dict) -> float f = reduce(np.kron, mat) expect_value = 0.0 for key, value in probabilities.items(): - idx = int(key[::-1], 2) + idx = int(key, 2) expect_value += f[idx] * value return expect_value @@ -95,4 +95,4 @@ def pauli_decompose(hamiltonian: np.ndarray) -> List: gate_list = reduce(lambda x,y: x+y, [labels[i] for i in item]) decomposition.append((gate_list, coeff)) - return decomposition \ No newline at end of file + return decomposition diff --git a/tests/test_review_e15_pauli_qubit_order.py b/tests/test_review_e15_pauli_qubit_order.py new file mode 100644 index 0000000..975f692 --- /dev/null +++ b/tests/test_review_e15_pauli_qubit_order.py @@ -0,0 +1,34 @@ +import math +import unittest + +from spinqit import Circuit, Parameter, Ry +from spinqit.algorithm.loss import expval +from spinqit.grad import qgrad +from spinqit.interface.qlayer import QLayer + + +class PauliQubitOrderRegressionTest(unittest.TestCase): + def test_first_pauli_character_acts_on_first_circuit_qubit(self): + circuit = Circuit() + qubits = circuit.allocateQubits(2) + theta = circuit.add_params(1) + circuit << (Ry, qubits[0], theta[0]) + layer = QLayer( + circuit, + expval([('ZI', 1.0)]), + backend_mode='spinq', + interface='spinq', + grad_method='param_shift', + ) + + angle = 0.4 + parameter = Parameter([angle]) + value = layer(parameter) + derivative = qgrad(layer)(parameter)[0] + + self.assertAlmostEqual(value.item(), math.cos(angle), places=7) + self.assertAlmostEqual(derivative.item(), -math.sin(angle), places=7) + + +if __name__ == '__main__': + unittest.main() From 58a84477f8aa85414ffe7f82fdf34999f3b80495 Mon Sep 17 00:00:00 2001 From: wuzp15 Date: Wed, 22 Jul 2026 19:20:11 +0800 Subject: [PATCH 57/59] initialize vqe depth with explicit parameters --- spinqit/algorithm/vqe.py | 2 +- ...test_review_e16_vqe_explicit_parameters.py | 25 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 tests/test_review_e16_vqe_explicit_parameters.py diff --git a/spinqit/algorithm/vqe.py b/spinqit/algorithm/vqe.py index c3a3af7..f732970 100644 --- a/spinqit/algorithm/vqe.py +++ b/spinqit/algorithm/vqe.py @@ -41,12 +41,12 @@ def __init__(self, self.optimizer = optimizer self.fn = None self.hamiltonian = hamiltonian + self.__depth = depth if params is not None: if isinstance(params, tuple): params = np.random.uniform(0, 2 * np.pi, params) self.params = Parameter(params, trainable=True) else: - self.__depth = depth self.params = Parameter(np.random.uniform(0, 2 * np.pi, (depth, self.__qubit_num, 3)), trainable=True) if ansatz is not None: diff --git a/tests/test_review_e16_vqe_explicit_parameters.py b/tests/test_review_e16_vqe_explicit_parameters.py new file mode 100644 index 0000000..2148e80 --- /dev/null +++ b/tests/test_review_e16_vqe_explicit_parameters.py @@ -0,0 +1,25 @@ +import unittest + +import numpy as np + +from spinqit.algorithm import VQE + + +class VqeExplicitParametersRegressionTest(unittest.TestCase): + def test_default_ansatz_accepts_explicit_initial_parameters(self): + initial = np.zeros((1, 1, 3)) + + vqe = VQE([('Z', 1.0)], object(), params=initial, depth=1) + + self.assertEqual(vqe.circuit.qubits_num, 1) + np.testing.assert_array_equal(vqe.optimized_params, initial) + + def test_default_ansatz_accepts_explicit_parameter_shape(self): + vqe = VQE([('Z', 1.0)], object(), params=(1, 1, 3), depth=1) + + self.assertEqual(vqe.circuit.qubits_num, 1) + self.assertEqual(vqe.optimized_params.shape, (1, 1, 3)) + + +if __name__ == '__main__': + unittest.main() From d330083620b0f9a225d21e204ca2cc8a40ba144e Mon Sep 17 00:00:00 2001 From: wuzp15 Date: Wed, 22 Jul 2026 19:21:40 +0800 Subject: [PATCH 58/59] preserve cloud gate operand order --- spinqit/backend/spinq_cloud_backend.py | 19 +++++------ tests/test_review_e17_cloud_repeated_ccx.py | 37 +++++++++++++++++++++ 2 files changed, 45 insertions(+), 11 deletions(-) create mode 100644 tests/test_review_e17_cloud_repeated_ccx.py diff --git a/spinqit/backend/spinq_cloud_backend.py b/spinqit/backend/spinq_cloud_backend.py index 5b84484..390aecb 100644 --- a/spinqit/backend/spinq_cloud_backend.py +++ b/spinqit/backend/spinq_cloud_backend.py @@ -491,17 +491,14 @@ def value_and_grad_fn(params): return value_and_grad_fn def __qubits_and_clbits(self, v): - edges = v.in_edges() - # in edges order must be the same as the bit order in register to func works correctly - edges.sort(key=lambda k: k.index) - qubits = [] - clbits = [] - for e in edges: - if 'qubit' in e.attributes() and e['qubit'] is not None: - qubits.append(e['qubit']) - elif 'clbit' in e.attributes() and e['clbit'] is not None: - clbits.append(e['clbit']) - return qubits, clbits + # Edge IDs change as earlier gates are substituted and removed, so + # their order cannot recover a gate's control/target operand order. + qubits = list(v['qubits']) + clbits = [] + for edge in v.in_edges(): + if 'clbit' in edge.attributes() and edge['clbit'] is not None: + clbits.append(edge['clbit']) + return qubits, clbits def evaluate(self, ir, config, measure_op): if measure_op is None: diff --git a/tests/test_review_e17_cloud_repeated_ccx.py b/tests/test_review_e17_cloud_repeated_ccx.py new file mode 100644 index 0000000..81a4133 --- /dev/null +++ b/tests/test_review_e17_cloud_repeated_ccx.py @@ -0,0 +1,37 @@ +from copy import deepcopy +import unittest + +import numpy as np + +from spinqit import BasicSimulatorConfig, CCX, Circuit, H, Rz, X, get_basic_simulator, get_compiler +from spinqit.backend.spinq_cloud_backend import SpinQCloudBackend +from spinqit.model.spinqCloud.platform import SQC_25 + + +class CloudRepeatedCcxRegressionTest(unittest.TestCase): + def test_cloud_decomposition_preserves_operands_after_dag_rewrites(self): + circuit = Circuit() + qubits = circuit.allocateQubits(3) + circuit << (X, qubits[0]) + circuit << (X, qubits[1]) + circuit << (H, qubits[2]) + circuit << (CCX, qubits) + circuit << (Rz, qubits[2], 0.37) + circuit << (CCX, qubits) + + ir = get_compiler('native').compile(circuit, 0) + expected = np.asarray( + get_basic_simulator().execute(deepcopy(ir), BasicSimulatorConfig()).states + ) + + cloud_ir = deepcopy(ir) + object.__new__(SpinQCloudBackend).assemble(SQC_25.code, cloud_ir) + actual = np.asarray( + get_basic_simulator().execute(cloud_ir, BasicSimulatorConfig()).states + ) + + self.assertGreaterEqual(abs(np.vdot(expected, actual)) ** 2, 1 - 1e-9) + + +if __name__ == '__main__': + unittest.main() From 28d0289d159a2138c90d6ed7b4e98f11b4b8c6f9 Mon Sep 17 00:00:00 2001 From: wuzp15 Date: Wed, 22 Jul 2026 19:23:06 +0800 Subject: [PATCH 59/59] reject noncommuting default qaoa terms --- spinqit/algorithm/qaoa.py | 11 +++++++++++ ...est_review_e18_qaoa_noncommuting_problem.py | 18 ++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 tests/test_review_e18_qaoa_noncommuting_problem.py diff --git a/spinqit/algorithm/qaoa.py b/spinqit/algorithm/qaoa.py index 4e6bd7e..de7b981 100644 --- a/spinqit/algorithm/qaoa.py +++ b/spinqit/algorithm/qaoa.py @@ -71,6 +71,17 @@ def _generate_problem_circuit(self, ham) -> Gate: raise ValueError( 'The problem hamiltonian should be given in list in __init__().' ) + for i, (left, _) in enumerate(ham): + for right, _ in ham[i + 1:]: + anticommutes = sum( + l != 'I' and r != 'I' and l != r + for l, r in zip(left.upper(), right.upper()) + ) + if anticommutes % 2: + raise ValueError( + 'The default QAOA problem ansatz requires commuting Pauli terms; ' + 'provide a custom problem_ansatz for noncommuting terms.' + ) for i in range(len(ham)): pauli_string = ham[i][0].upper() coefficient = ham[i][1] diff --git a/tests/test_review_e18_qaoa_noncommuting_problem.py b/tests/test_review_e18_qaoa_noncommuting_problem.py new file mode 100644 index 0000000..5a84a26 --- /dev/null +++ b/tests/test_review_e18_qaoa_noncommuting_problem.py @@ -0,0 +1,18 @@ +import unittest + +from spinqit import generate_hamiltonian_matrix +from spinqit.algorithm import QAOA + + +class QaoaNoncommutingProblemRegressionTest(unittest.TestCase): + def test_default_problem_ansatz_rejects_order_dependent_term_product(self): + terms = [('Z', 1.0), ('Y', 1.0)] + + for problem in (terms, generate_hamiltonian_matrix(terms)): + with self.subTest(problem_type=type(problem).__name__): + with self.assertRaisesRegex(ValueError, 'commuting Pauli terms'): + QAOA(problem, object(), depth=1) + + +if __name__ == '__main__': + unittest.main()