From 30a0e00e200aa2df1a36b72a5eb5fdf29e637506 Mon Sep 17 00:00:00 2001 From: huaweil Date: Tue, 25 Nov 2025 06:12:09 +0000 Subject: [PATCH] Fix evolve_async missing return and incorrect condition check Bug fixes in evolution.py: - Fix missing return statement in evolve_single_async when collapse_operators is provided with store_intermediate_results=NONE mode - Fix condition check: use 'store_intermediate_results != NONE' instead of boolean check on enum value Test improvements: - Fix expectation_values() access pattern in test to correctly handle nested list structure - Fix intermediate_states() assertion: NONE mode returns 1 (final state only) - Add test_evolve_no_intermediate_results for sync version coverage Signed-off-by: huaweil --- python/cudaq/dynamics/evolution.py | 14 +- .../tests/dynamics/test_evolve_simulators.py | 128 ++++++++++++++++++ 2 files changed, 135 insertions(+), 7 deletions(-) diff --git a/python/cudaq/dynamics/evolution.py b/python/cudaq/dynamics/evolution.py index 974f2b5035a..1bd4a0fb85c 100644 --- a/python/cudaq/dynamics/evolution.py +++ b/python/cudaq/dynamics/evolution.py @@ -544,7 +544,7 @@ def evolve_single_async( step_parameters, dt) if shots_count is None: shots_count = -1 - if store_intermediate_results: + if store_intermediate_results != IntermediateResultSave.NONE: evolution = _evolution_kernel( num_qubits, compute_step_matrix, @@ -579,12 +579,12 @@ def evolve_single_async( return cudaq_runtime.evolve_async(initial_state, kernel) # FIXME: permit to compute expectation values for operators defined as matrix if len(collapse_operators) > 0: - cudaq_runtime.evolve_async(initial_state, - kernel, - parameters[-1], - observable_spinops, - noise_model=noise, - shots_count=shots_count) + return cudaq_runtime.evolve_async(initial_state, + kernel, + parameters[-1], + observable_spinops, + noise_model=noise, + shots_count=shots_count) return cudaq_runtime.evolve_async(initial_state, kernel, parameters[-1], diff --git a/python/tests/dynamics/test_evolve_simulators.py b/python/tests/dynamics/test_evolve_simulators.py index a12eb69be51..58265357025 100644 --- a/python/tests/dynamics/test_evolve_simulators.py +++ b/python/tests/dynamics/test_evolve_simulators.py @@ -331,6 +331,134 @@ def test_evolve_async(): atol=0.1) +def test_evolve_no_intermediate_results(): + """Test evolve with store_intermediate_results=NONE + to verify the else branch in evolve_single is working.""" + + # Qubit Hamiltonian + hamiltonian = 2 * np.pi * 0.1 * spin.x(0) + + # Dimensions + dimensions = {0: 2} + + # Initial state + rho0 = cudaq.State.from_data( + np.array([[1.0, 0.0], [0.0, 0.0]], dtype=np.complex128)) + + # Schedule + steps = np.linspace(0, 10, 101) + schedule = Schedule(steps, ["time"]) + + # Test 1: NONE without observables + evolution_result = cudaq.evolve( + hamiltonian, + dimensions, + schedule, + rho0, + store_intermediate_results=cudaq.IntermediateResultSave.NONE) + + # NONE mode: only final state is saved, no intermediate states + assert len(evolution_result.intermediate_states()) == 1 + + # Test 2: NONE with observables + schedule.reset() + evolution_result = cudaq.evolve( + hamiltonian, + dimensions, + schedule, + rho0, + observables=[spin.y(0), spin.z(0)], + store_intermediate_results=cudaq.IntermediateResultSave.NONE) + + # Verify final expectation value is reasonable + final_exp = evolution_result.expectation_values() + assert final_exp is not None + + # Test 3: NONE with collapse_operators (tests the missing return bug) + schedule.reset() + evolution_result_decay = cudaq.evolve( + hamiltonian, + dimensions, + schedule, + rho0, + observables=[spin.y(0), spin.z(0)], + collapse_operators=[np.sqrt(0.05) * spin.x(0)], + store_intermediate_results=cudaq.IntermediateResultSave.NONE) + + # Results with decay should differ from ideal (noise should have effect) + # This test would fail if the noise_model is ignored (the return bug) + final_exp_decay = evolution_result_decay.expectation_values() + assert final_exp_decay is not None + # expectation_values() returns [[ObserveResult, ...]] - outer list is time steps, + # inner list is observables. With NONE mode, there's only one time step (final). + assert final_exp_decay[0][0].expectation() != final_exp[0][0].expectation() + assert final_exp_decay[0][1].expectation() != final_exp[0][1].expectation() + + +def test_evolve_async_no_intermediate_results(): + """Test evolve_async with store_intermediate_results=NONE + to verify the else branch in evolve_single_async is working.""" + + # Qubit Hamiltonian + hamiltonian = 2 * np.pi * 0.1 * spin.x(0) + + # Dimensions + dimensions = {0: 2} + + # Initial state + rho0 = cudaq.State.from_data( + np.array([[1.0, 0.0], [0.0, 0.0]], dtype=np.complex128)) + + # Schedule + steps = np.linspace(0, 10, 101) + schedule = Schedule(steps, ["time"]) + + # Test 1: NONE without observables + evolution_result = cudaq.evolve_async( + hamiltonian, + dimensions, + schedule, + rho0, + store_intermediate_results=cudaq.IntermediateResultSave.NONE).get() + + # NONE mode: only final state is saved, no intermediate states + assert len(evolution_result.intermediate_states()) == 1 + + # Test 2: NONE with observables + schedule.reset() + evolution_result = cudaq.evolve_async( + hamiltonian, + dimensions, + schedule, + rho0, + observables=[spin.y(0), spin.z(0)], + store_intermediate_results=cudaq.IntermediateResultSave.NONE).get() + + # Verify final expectation value is reasonable + final_exp = evolution_result.expectation_values() + assert final_exp is not None + + # Test 3: NONE with collapse_operators (tests the missing return bug) + schedule.reset() + evolution_result_decay = cudaq.evolve_async( + hamiltonian, + dimensions, + schedule, + rho0, + observables=[spin.y(0), spin.z(0)], + collapse_operators=[np.sqrt(0.05) * spin.x(0)], + store_intermediate_results=cudaq.IntermediateResultSave.NONE).get() + + # Results with decay should differ from ideal (noise should have effect) + # This test would fail if the noise_model is ignored (the return bug) + final_exp_decay = evolution_result_decay.expectation_values() + assert final_exp_decay is not None + # expectation_values() returns [[ObserveResult, ...]] - outer list is time steps, + # inner list is observables. With NONE mode, there's only one time step (final). + assert final_exp_decay[0][0].expectation() != final_exp[0][0].expectation() + assert final_exp_decay[0][1].expectation() != final_exp[0][1].expectation() + + # leave for gdb debugging if __name__ == "__main__": loc = os.path.abspath(__file__)