From c97bb892d44666e4bc4826f19fc2be44a0d121ba Mon Sep 17 00:00:00 2001 From: Dima Fedoriaka Date: Tue, 9 Jun 2026 15:05:33 -0700 Subject: [PATCH 1/4] Add OperationTestHelper.get_action_on_state --- source/qdk_package/qdk/__init__.py | 1 + source/qdk_package/qdk/_test_helper.py | 51 ++++++++ source/qdk_package/tests/test_test_helper.py | 128 +++++++++++++++++++ 3 files changed, 180 insertions(+) create mode 100644 source/qdk_package/qdk/_test_helper.py create mode 100644 source/qdk_package/tests/test_test_helper.py diff --git a/source/qdk_package/qdk/__init__.py b/source/qdk_package/qdk/__init__.py index ea6a704e20..416efb7a32 100644 --- a/source/qdk_package/qdk/__init__.py +++ b/source/qdk_package/qdk/__init__.py @@ -53,6 +53,7 @@ set_quantum_seed, ) from ._native import Result, TargetProfile +from ._test_helper import OperationTestHelper from ._types import ( BitFlipNoise, DepolarizingNoise, diff --git a/source/qdk_package/qdk/_test_helper.py b/source/qdk_package/qdk/_test_helper.py new file mode 100644 index 0000000000..26771cd6f0 --- /dev/null +++ b/source/qdk_package/qdk/_test_helper.py @@ -0,0 +1,51 @@ +from typing import Any + +from qdk._interpreter import _get_default_context + +from ._context import Context + + +class OperationTestHelper: + """Helper for testing Q# operations.""" + + def __init__(self, context: Context | None = None): + self.context = context or _get_default_context() + + def get_action_on_state(self, op: Any, num_qubits: int) -> list[complex]: + """Returns the state vector after applying an operation to the zero state. + + Uses big-endian convention for basis-state numbering. + + Args: + op: Q# callable from ``Context.code`` or a string that evaluates to + a Q# callable. The callable must have signature + ``(Qubit[] => Unit)``. + num_qubits: Number of qubits the operation acts on. + + Returns: + The state vector as a list of ``2**num_qubits`` complex numbers. + """ + + if type(op) is str: + op = self.context.eval(op) + + self.context.eval(""" + operation _GetActionOnZeroState(num_qubits: Int, op: (Qubit[] => Unit)) : Unit { + use qubits = Qubit[num_qubits]; + op(qubits); + Microsoft.Quantum.Diagnostics.DumpRegister(qubits); + ResetAll(qubits); + } + """) + result = self.context.run( + self.context.code._GetActionOnZeroState, + 1, + num_qubits, + op, + save_events=True, + )[0] + state = result["events"][-1].state_dump().get_dict() + result = [0.0] * (2**num_qubits) + for key, value in state.items(): + result[key] = value + return result diff --git a/source/qdk_package/tests/test_test_helper.py b/source/qdk_package/tests/test_test_helper.py new file mode 100644 index 0000000000..cce46e4c1b --- /dev/null +++ b/source/qdk_package/tests/test_test_helper.py @@ -0,0 +1,128 @@ +import math + +import pytest +from qdk import Context, OperationTestHelper + + +@pytest.fixture(scope="session") +def ctx(): + yield Context() + + +def _assert_states_close(state1: list[complex], state2: list[complex]): + assert len(state1) == len(state2) + for i in range(len(state1)): + assert abs(state1[i] - state2[i]) < 1e-9 + + +def test_get_action_on_state(ctx): + ctx.eval(""" + operation MyOp1(q: Qubit[]) : Unit { + H(q[0]); + CNOT(q[0], q[1]); + Z(q[1]); + } + """) + helper = OperationTestHelper(ctx) + + vector = helper.get_action_on_state(ctx.code.MyOp1, num_qubits=2) + s = 0.5**0.5 + _assert_states_close(vector, [s, 0, 0, -s]) + + +def test_get_action_on_state_with_two_registers(ctx): + ctx.eval(""" + operation MyOp2(q1: Qubit[], q2: Qubit[]) : Unit { + H(q1[0]); + CNOT(q1[0], q2[0]); + } + + operation MyOp2_TestHelper(q: Qubit[]) : Unit { + let n = Length(q); + MyOp2(q[0..n/2-1], q[n/2..n-1]); + } + """) + helper = OperationTestHelper(ctx) + + vector = helper.get_action_on_state(ctx.code.MyOp2_TestHelper, num_qubits=4) + s = 0.5**0.5 + _assert_states_close(vector, [s, 0, 0, 0, 0, 0, 0, 0, 0, 0, s, 0, 0, 0, 0, 0]) + + +def test_get_action_on_state_with_partial_trace(ctx): + ctx.eval(""" + operation MyOp3(q1: Qubit[], q2: Qubit[]) : Unit { + H(q1[0]); + H(q2[0]); + } + + operation MyOp3_TestHelper(q: Qubit[]) : Unit { + use q2 = Qubit[2]; + MyOp3(q, q2); + ResetAll(q2); + } + """) + helper = OperationTestHelper(ctx) + + vector = helper.get_action_on_state(ctx.code.MyOp3_TestHelper, num_qubits=2) + s = 0.5**0.5 + _assert_states_close(vector, [s, 0, s, 0]) + + +def test_get_action_on_state_with_non_zero_initial_state(ctx): + ctx.eval(""" + operation MyOp4(q: Qubit[]) : Unit is Adj { + CNOT(q[0], q[1]); + H(q[0]); + } + + operation MyOp4_TestHelper(initial_state: Double[]) : (Qubit[] => Unit) { + return q => { + Std.StatePreparation.PreparePureStateD(initial_state, q); + MyOp4(q); + } + } + """) + helper = OperationTestHelper(ctx) + + s = 0.5**0.5 + vector = helper.get_action_on_state( + ctx.code.MyOp4_TestHelper([s, 0, 0, s]), num_qubits=2 + ) + _assert_states_close(vector, [1, 0, 0, 0]) + + +def test_get_action_on_state_with_parameters(ctx): + ctx.eval(""" + operation MyOp5(qs: Qubit[], angle: Double) : Unit is Adj { + for q in qs { + Rx(angle, q); + } + } + + operation MyOp5_TestHelper(angle: Double) : (Qubit[] => Unit) { + MyOp5(_, angle) + } + """) + helper = OperationTestHelper(ctx) + + vector = helper.get_action_on_state(ctx.code.MyOp5_TestHelper(0.3), num_qubits=2) + c = math.cos(0.3 / 2) + s = math.sin(0.3 / 2) + _assert_states_close(vector, [c * c, -1j * c * s, -1j * c * s, -(s * s)]) + + +def test_get_action_on_state_with_parameterized_callable(ctx): + ctx.eval(""" + operation MyOp5(qs: Qubit[], angle: Double) : Unit is Adj { + for q in qs { + Rx(angle, q); + } + } + """) + helper = OperationTestHelper(ctx) + + vector = helper.get_action_on_state(ctx.eval("MyOp5(_, 0.3)"), num_qubits=2) + c = math.cos(0.3 / 2) + s = math.sin(0.3 / 2) + _assert_states_close(vector, [c * c, -1j * c * s, -1j * c * s, -(s * s)]) From bc0a19f22b02f44a3fa2d1dec66b1635854e7f6e Mon Sep 17 00:00:00 2001 From: Dima Fedoriaka Date: Tue, 9 Jun 2026 15:11:02 -0700 Subject: [PATCH 2/4] add tests when op is string --- source/qdk_package/tests/test_test_helper.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/source/qdk_package/tests/test_test_helper.py b/source/qdk_package/tests/test_test_helper.py index cce46e4c1b..422821de31 100644 --- a/source/qdk_package/tests/test_test_helper.py +++ b/source/qdk_package/tests/test_test_helper.py @@ -29,6 +29,9 @@ def test_get_action_on_state(ctx): s = 0.5**0.5 _assert_states_close(vector, [s, 0, 0, -s]) + vector = helper.get_action_on_state("MyOp1", num_qubits=2) + _assert_states_close(vector, [s, 0, 0, -s]) + def test_get_action_on_state_with_two_registers(ctx): ctx.eval(""" @@ -122,7 +125,7 @@ def test_get_action_on_state_with_parameterized_callable(ctx): """) helper = OperationTestHelper(ctx) - vector = helper.get_action_on_state(ctx.eval("MyOp5(_, 0.3)"), num_qubits=2) + vector = helper.get_action_on_state("MyOp5(_, 0.3)", num_qubits=2) c = math.cos(0.3 / 2) s = math.sin(0.3 / 2) _assert_states_close(vector, [c * c, -1j * c * s, -1j * c * s, -(s * s)]) From db21f633c2179bb542dd4f04e2daf795b5085711 Mon Sep 17 00:00:00 2001 From: Dima Fedoriaka Date: Fri, 12 Jun 2026 13:08:33 -0700 Subject: [PATCH 3/4] Change API --- library/std/src/Std/Diagnostics.qs | 26 +++++++- source/qdk_package/qdk/__init__.py | 1 - source/qdk_package/qdk/_interpreter.py | 9 +++ source/qdk_package/qdk/_test_helper.py | 51 --------------- source/qdk_package/qdk/test_utils.py | 53 ++++++++++++++++ ...test_test_helper.py => test_test_utils.py} | 63 +++++++------------ 6 files changed, 111 insertions(+), 92 deletions(-) delete mode 100644 source/qdk_package/qdk/_test_helper.py create mode 100644 source/qdk_package/qdk/test_utils.py rename source/qdk_package/tests/{test_test_helper.py => test_test_utils.py} (55%) diff --git a/library/std/src/Std/Diagnostics.qs b/library/std/src/Std/Diagnostics.qs index de68095b74..c922040be6 100644 --- a/library/std/src/Std/Diagnostics.qs +++ b/library/std/src/Std/Diagnostics.qs @@ -480,6 +480,29 @@ operation PostSelectZ(res : Result, qubit : Qubit) : Unit { body intrinsic; } +/// # Summary +/// Dumps statevector after applying operation to the given state. +/// +/// # Input +/// ## op +/// The operation to apply. +/// ## num_qubits +/// The number of qubits in the register on which the operation is applied. +/// ## initial_state +operation DumpOperationOnState( + op : (Qubit[] => Unit), + num_qubits : Int, + initial_state : Double[] +) : Unit { + use qubits = Qubit[num_qubits]; + if (Length(initial_state) > 1) { + Std.StatePreparation.PreparePureStateD(initial_state, qubits); + } + op(qubits); + DumpRegister(qubits); + ResetAll(qubits); +} + export DumpMachine, DumpRegister, @@ -501,4 +524,5 @@ export PhaseFlipNoise, DepolarizingNoise, NoNoise, - PostSelectZ; + PostSelectZ, + DumpOperationOnState; diff --git a/source/qdk_package/qdk/__init__.py b/source/qdk_package/qdk/__init__.py index 386fbcc9a5..4f1d497cee 100644 --- a/source/qdk_package/qdk/__init__.py +++ b/source/qdk_package/qdk/__init__.py @@ -53,7 +53,6 @@ set_quantum_seed, ) from ._native import ProgramType, Result, TargetProfile -from ._test_helper import OperationTestHelper from ._types import ( BitFlipNoise, DepolarizingNoise, diff --git a/source/qdk_package/qdk/_interpreter.py b/source/qdk_package/qdk/_interpreter.py index 01561f5065..e60c475e6c 100644 --- a/source/qdk_package/qdk/_interpreter.py +++ b/source/qdk_package/qdk/_interpreter.py @@ -159,6 +159,15 @@ def _get_default_context() -> Context: return _default_context +def _get_context_or_default(obj: Any) -> Context: + """Returns context associated with given object, if available. + Otherwise falls back to the default context. + """ + if hasattr(obj, "_qdk_context"): + return getattr(obj, "_qdk_context") + return _get_default_context() + + # --------------------------------------------------------------------------- # Functions accessing global context, for compatibility. # --------------------------------------------------------------------------- diff --git a/source/qdk_package/qdk/_test_helper.py b/source/qdk_package/qdk/_test_helper.py deleted file mode 100644 index 26771cd6f0..0000000000 --- a/source/qdk_package/qdk/_test_helper.py +++ /dev/null @@ -1,51 +0,0 @@ -from typing import Any - -from qdk._interpreter import _get_default_context - -from ._context import Context - - -class OperationTestHelper: - """Helper for testing Q# operations.""" - - def __init__(self, context: Context | None = None): - self.context = context or _get_default_context() - - def get_action_on_state(self, op: Any, num_qubits: int) -> list[complex]: - """Returns the state vector after applying an operation to the zero state. - - Uses big-endian convention for basis-state numbering. - - Args: - op: Q# callable from ``Context.code`` or a string that evaluates to - a Q# callable. The callable must have signature - ``(Qubit[] => Unit)``. - num_qubits: Number of qubits the operation acts on. - - Returns: - The state vector as a list of ``2**num_qubits`` complex numbers. - """ - - if type(op) is str: - op = self.context.eval(op) - - self.context.eval(""" - operation _GetActionOnZeroState(num_qubits: Int, op: (Qubit[] => Unit)) : Unit { - use qubits = Qubit[num_qubits]; - op(qubits); - Microsoft.Quantum.Diagnostics.DumpRegister(qubits); - ResetAll(qubits); - } - """) - result = self.context.run( - self.context.code._GetActionOnZeroState, - 1, - num_qubits, - op, - save_events=True, - )[0] - state = result["events"][-1].state_dump().get_dict() - result = [0.0] * (2**num_qubits) - for key, value in state.items(): - result[key] = value - return result diff --git a/source/qdk_package/qdk/test_utils.py b/source/qdk_package/qdk/test_utils.py new file mode 100644 index 0000000000..f9e74a41e4 --- /dev/null +++ b/source/qdk_package/qdk/test_utils.py @@ -0,0 +1,53 @@ +"""Helper functions for testing Q# code.""" + +from typing import Any + +from qdk._interpreter import _get_context_or_default + +from ._context import Context + + +def dump_operation_on_state( + op: Any, + num_qubits: int, + initial_state: list[float] | None = None, + context: Context | None = None, +) -> list[complex]: + """Returns statevector after applying operation to the given state. + + Uses big-endian convention for basis-state numbering. + + Args: + op: Q# callable from ``Context.code`` or a string that evaluates to + a Q# callable. The callable must have signature + ``(Qubit[] => Unit)``. + num_qubits: Number of qubits the operation acts on. + initial_state: Initial state given by list of `2**num_qubits` real amplitudes. + If the list is shorter, it will be padded with zeros. + If not provided, the initial state is zero state (|00..0>). + context: `qdk.Context` from which the operation was created (optional). If + not provided, will attempt to infer it from `op` and then fall back to + default context. + + Returns: + The state vector as a list of `2**num_qubits` complex numbers. + """ + context = context or _get_context_or_default(op) + if initial_state is None: + initial_state = [1.0] # |00..0> state. + if type(op) is str: + op = context.eval(op) + + result = context.run( + context.eval("Std.Diagnostics.DumpOperationOnState"), + 1, # shots + op, + num_qubits, + initial_state, + save_events=True, + )[0] + state = result["events"][-1].state_dump().get_dict() + statevector = [0.0] * (2**num_qubits) + for index, amplitude in state.items(): + statevector[index] = amplitude + return statevector diff --git a/source/qdk_package/tests/test_test_helper.py b/source/qdk_package/tests/test_test_utils.py similarity index 55% rename from source/qdk_package/tests/test_test_helper.py rename to source/qdk_package/tests/test_test_utils.py index 422821de31..957692e660 100644 --- a/source/qdk_package/tests/test_test_helper.py +++ b/source/qdk_package/tests/test_test_utils.py @@ -1,12 +1,7 @@ import math -import pytest -from qdk import Context, OperationTestHelper - - -@pytest.fixture(scope="session") -def ctx(): - yield Context() +from qdk import code, qsharp, Context +from qdk.test_utils import dump_operation_on_state def _assert_states_close(state1: list[complex], state2: list[complex]): @@ -15,26 +10,25 @@ def _assert_states_close(state1: list[complex], state2: list[complex]): assert abs(state1[i] - state2[i]) < 1e-9 -def test_get_action_on_state(ctx): - ctx.eval(""" +def test_dump_operation_on_state(): + qsharp.eval(""" operation MyOp1(q: Qubit[]) : Unit { H(q[0]); CNOT(q[0], q[1]); Z(q[1]); } """) - helper = OperationTestHelper(ctx) - vector = helper.get_action_on_state(ctx.code.MyOp1, num_qubits=2) + vector = dump_operation_on_state(code.MyOp1, num_qubits=2) s = 0.5**0.5 _assert_states_close(vector, [s, 0, 0, -s]) - vector = helper.get_action_on_state("MyOp1", num_qubits=2) + vector = dump_operation_on_state("MyOp1", num_qubits=2) _assert_states_close(vector, [s, 0, 0, -s]) -def test_get_action_on_state_with_two_registers(ctx): - ctx.eval(""" +def test_dump_operation_on_state_with_two_registers(): + qsharp.eval(""" operation MyOp2(q1: Qubit[], q2: Qubit[]) : Unit { H(q1[0]); CNOT(q1[0], q2[0]); @@ -45,15 +39,14 @@ def test_get_action_on_state_with_two_registers(ctx): MyOp2(q[0..n/2-1], q[n/2..n-1]); } """) - helper = OperationTestHelper(ctx) - vector = helper.get_action_on_state(ctx.code.MyOp2_TestHelper, num_qubits=4) + vector = dump_operation_on_state(code.MyOp2_TestHelper, num_qubits=4) s = 0.5**0.5 _assert_states_close(vector, [s, 0, 0, 0, 0, 0, 0, 0, 0, 0, s, 0, 0, 0, 0, 0]) -def test_get_action_on_state_with_partial_trace(ctx): - ctx.eval(""" +def test_dump_operation_on_state_with_partial_trace(): + qsharp.eval(""" operation MyOp3(q1: Qubit[], q2: Qubit[]) : Unit { H(q1[0]); H(q2[0]); @@ -65,37 +58,29 @@ def test_get_action_on_state_with_partial_trace(ctx): ResetAll(q2); } """) - helper = OperationTestHelper(ctx) - vector = helper.get_action_on_state(ctx.code.MyOp3_TestHelper, num_qubits=2) + vector = dump_operation_on_state(code.MyOp3_TestHelper, num_qubits=2) s = 0.5**0.5 _assert_states_close(vector, [s, 0, s, 0]) -def test_get_action_on_state_with_non_zero_initial_state(ctx): - ctx.eval(""" +def test_dump_operation_on_state_with_initial_state(): + qsharp.eval(""" operation MyOp4(q: Qubit[]) : Unit is Adj { CNOT(q[0], q[1]); H(q[0]); } - - operation MyOp4_TestHelper(initial_state: Double[]) : (Qubit[] => Unit) { - return q => { - Std.StatePreparation.PreparePureStateD(initial_state, q); - MyOp4(q); - } - } """) - helper = OperationTestHelper(ctx) s = 0.5**0.5 - vector = helper.get_action_on_state( - ctx.code.MyOp4_TestHelper([s, 0, 0, s]), num_qubits=2 + vector = dump_operation_on_state( + code.MyOp4, num_qubits=2, initial_state=[s, 0, 0, s] ) _assert_states_close(vector, [1, 0, 0, 0]) -def test_get_action_on_state_with_parameters(ctx): +def test_dump_operation_on_state_with_parameters(): + ctx = Context() ctx.eval(""" operation MyOp5(qs: Qubit[], angle: Double) : Unit is Adj { for q in qs { @@ -107,25 +92,25 @@ def test_get_action_on_state_with_parameters(ctx): MyOp5(_, angle) } """) - helper = OperationTestHelper(ctx) - vector = helper.get_action_on_state(ctx.code.MyOp5_TestHelper(0.3), num_qubits=2) + vector = dump_operation_on_state( + ctx.code.MyOp5_TestHelper(0.3), num_qubits=2, context=ctx + ) c = math.cos(0.3 / 2) s = math.sin(0.3 / 2) _assert_states_close(vector, [c * c, -1j * c * s, -1j * c * s, -(s * s)]) -def test_get_action_on_state_with_parameterized_callable(ctx): - ctx.eval(""" +def test_dump_operation_on_state_with_parameterized_callable(): + qsharp.eval(""" operation MyOp5(qs: Qubit[], angle: Double) : Unit is Adj { for q in qs { Rx(angle, q); } } """) - helper = OperationTestHelper(ctx) - vector = helper.get_action_on_state("MyOp5(_, 0.3)", num_qubits=2) + vector = dump_operation_on_state("MyOp5(_, 0.3)", num_qubits=2) c = math.cos(0.3 / 2) s = math.sin(0.3 / 2) _assert_states_close(vector, [c * c, -1j * c * s, -1j * c * s, -(s * s)]) From 3bc21849fba91d10eb750d4f98772d31ee100603 Mon Sep 17 00:00:00 2001 From: Dima Fedoriaka Date: Fri, 12 Jun 2026 14:12:45 -0700 Subject: [PATCH 4/4] Pull Q# code back into Python --- library/std/src/Std/Diagnostics.qs | 26 +------------------------- source/qdk_package/qdk/test_utils.py | 19 ++++++++++++++++++- 2 files changed, 19 insertions(+), 26 deletions(-) diff --git a/library/std/src/Std/Diagnostics.qs b/library/std/src/Std/Diagnostics.qs index c922040be6..de68095b74 100644 --- a/library/std/src/Std/Diagnostics.qs +++ b/library/std/src/Std/Diagnostics.qs @@ -480,29 +480,6 @@ operation PostSelectZ(res : Result, qubit : Qubit) : Unit { body intrinsic; } -/// # Summary -/// Dumps statevector after applying operation to the given state. -/// -/// # Input -/// ## op -/// The operation to apply. -/// ## num_qubits -/// The number of qubits in the register on which the operation is applied. -/// ## initial_state -operation DumpOperationOnState( - op : (Qubit[] => Unit), - num_qubits : Int, - initial_state : Double[] -) : Unit { - use qubits = Qubit[num_qubits]; - if (Length(initial_state) > 1) { - Std.StatePreparation.PreparePureStateD(initial_state, qubits); - } - op(qubits); - DumpRegister(qubits); - ResetAll(qubits); -} - export DumpMachine, DumpRegister, @@ -524,5 +501,4 @@ export PhaseFlipNoise, DepolarizingNoise, NoNoise, - PostSelectZ, - DumpOperationOnState; + PostSelectZ; diff --git a/source/qdk_package/qdk/test_utils.py b/source/qdk_package/qdk/test_utils.py index f9e74a41e4..02d83f86c7 100644 --- a/source/qdk_package/qdk/test_utils.py +++ b/source/qdk_package/qdk/test_utils.py @@ -38,8 +38,25 @@ def dump_operation_on_state( if type(op) is str: op = context.eval(op) + if not hasattr(context.code, "_DumpOperationOnState"): + context.eval(""" + operation _DumpOperationOnState( + op : (Qubit[] => Unit), + num_qubits : Int, + initial_state : Double[] + ) : Unit { + use qubits = Qubit[num_qubits]; + if (Length(initial_state) > 1) { + Std.StatePreparation.PreparePureStateD(initial_state, qubits); + } + op(qubits); + Std.Diagnostics.DumpRegister(qubits); + ResetAll(qubits); + } + """) + result = context.run( - context.eval("Std.Diagnostics.DumpOperationOnState"), + context.code._DumpOperationOnState, 1, # shots op, num_qubits,