Skip to content

Commit 91c5729

Browse files
huaweil-nvsacpis
andauthored
Fix segfault in EvolveResult.final_expectation_values() when no observables provided (#4065)
<!-- Thanks for helping us improve CUDA-Q! ⚠️ The pull request title should be concise and understandable for all. ⚠️ If your pull request fixes an open issue, please link to the issue. Checklist: - [ ] I have added tests to cover my changes. - [ ] I have updated the documentation accordingly. - [ ] I have read the CONTRIBUTING document. --> ### Description <!-- Include relevant issues here, describe what changed and why --> EvolveResult.final_expectation_values() and EvolveResult.final_state() crash with a segfault when the underlying std::optional fields are nullopt. This happens when evolve() is called without observables — expectation_values is never populated, but the accessor directly dereferences it without checking. The method's own docstring states "This value will be None if no observables were specified in the call", but the actual behavior is a segfault. Users can see this docstring in Python via: ``` >>> help(cudaq.EvolveResult.final_expectation_values) ``` **How to reproduce**: ``` import cudaq from cudaq.operators import * from cudaq.dynamics import * import numpy as np cudaq.set_target('density-matrix-cpu') hamiltonian = 2 * np.pi * 0.1 * spin.x(0) dimensions = {0: 2} rho0 = cudaq.State.from_data( np.array([[1.0, 0.0], [0.0, 0.0]], dtype=np.complex128)) steps = np.linspace(0, 10, 11) schedule = Schedule(steps, ["time"]) result = cudaq.evolve(hamiltonian, dimensions, schedule, rho0) result.final_expectation_values() # Segmentation fault ``` Fix: Add has_value() and empty() checks in py_EvolveResult.cpp for both final_expectation_values() and final_state(). Return py::none() when the data is not available, consistent with the documented behavior. Signed-off-by: huaweil <huaweil@nvidia.com> Co-authored-by: Sachin Pisal <spisal@nvidia.com>
1 parent 3cb2034 commit 91c5729

File tree

2 files changed

+42
-3
lines changed

2 files changed

+42
-3
lines changed

python/runtime/common/py_EvolveResult.cpp

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,15 @@ void bindEvolveResult(py::module &mod) {
3535
.def(py::init<std::vector<state>, std::vector<std::vector<double>>>())
3636
.def(
3737
"final_state",
38-
[](evolve_result &self) { return self.states->back(); },
38+
[](evolve_result &self) -> py::object {
39+
if (!self.states.has_value() || self.states->empty())
40+
return py::none();
41+
return py::cast(self.states->back());
42+
},
3943
"Stores the final state produced by a call to :func:`evolve`. "
4044
"Represent the state of a quantum system after time evolution under "
4145
"a set of operators, see the :func:`evolve` documentation for more "
42-
"detail.\n")
46+
"detail. Returns None if no states are available.\n")
4347
.def(
4448
"intermediate_states",
4549
[](evolve_result &self) { return self.states; },
@@ -50,7 +54,12 @@ void bindEvolveResult(py::module &mod) {
5054
":func:`evolve`.\n")
5155
.def(
5256
"final_expectation_values",
53-
[](evolve_result &self) { return self.expectation_values->back(); },
57+
[](evolve_result &self) -> py::object {
58+
if (!self.expectation_values.has_value() ||
59+
self.expectation_values->empty())
60+
return py::none();
61+
return py::cast(self.expectation_values->back());
62+
},
5463
"Stores the final expectation values, that is the results produced "
5564
"by "
5665
"calls to :func:`observe`, triggered by a call to :func:`evolve`. "

python/tests/dynamics/test_evolve_simulators.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,36 @@ def test_evolve_no_intermediate_results():
461461
assert final_exp_decay[0][1].expectation() != final_exp[0][1].expectation()
462462

463463

464+
def test_final_expectation_values_without_observables():
465+
"""Test that final_expectation_values returns None instead of crashing
466+
when evolve is called without observables."""
467+
468+
hamiltonian = 2 * np.pi * 0.1 * spin.x(0)
469+
dimensions = {0: 2}
470+
rho0 = cudaq.State.from_data(
471+
np.array([[1.0, 0.0], [0.0, 0.0]], dtype=np.complex128))
472+
473+
steps = np.linspace(0, 10, 11)
474+
schedule = Schedule(steps, ["time"])
475+
476+
# Evolve without observables
477+
result = cudaq.evolve(
478+
hamiltonian,
479+
dimensions,
480+
schedule,
481+
rho0,
482+
store_intermediate_results=cudaq.IntermediateResultSave.NONE)
483+
484+
# final_expectation_values should return None, not segfault
485+
assert result.final_expectation_values() is None
486+
487+
# expectation_values should also be None
488+
assert result.expectation_values() is None
489+
490+
# final_state should still work
491+
assert result.final_state() is not None
492+
493+
464494
# leave for gdb debugging
465495
if __name__ == "__main__":
466496
loc = os.path.abspath(__file__)

0 commit comments

Comments
 (0)