diff --git a/backend/src/lib.rs b/backend/src/lib.rs index b02b92c4..8d265088 100644 --- a/backend/src/lib.rs +++ b/backend/src/lib.rs @@ -61,7 +61,9 @@ pub fn set_rng_seed(seed: u64) { pub extern "C" fn __quantum__rt__initialize(_: *mut c_char) { SIM_STATE.with(|sim_state| { let state = &mut *sim_state.borrow_mut(); - state.sim = QuantumSim::default(); + // in order to continue using the same RNG, we need to reset the simulator + // and keep the same RNG + state.sim = QuantumSim::new(Some(state.sim.take_rng())); state.res = bitvec![]; state.max_qubit_id = 0; }); diff --git a/runner/tests/resources/random-bit.bc b/runner/tests/resources/random-bit.bc new file mode 100644 index 00000000..b255b657 Binary files /dev/null and b/runner/tests/resources/random-bit.bc differ diff --git a/runner/tests/tests.rs b/runner/tests/tests.rs index cc782080..dd39f491 100644 --- a/runner/tests/tests.rs +++ b/runner/tests/tests.rs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use runner::{run_bitcode, run_file}; +use runner::{run_bitcode, run_file, OUTPUT}; // This group of tests verifies the behavior of QIR execution with a series of quantum gate checks based on the Choi–Jamiołkowski Isomorphism. // They will verify the behavior of body, adjoint, controlled, and controlled adjoint specializations of each gate against decompositions thereof, @@ -278,6 +278,46 @@ fn run_file_errors_on_missing_file() { ); } +#[test] +fn run_file_random_seed_is_applied_and_persists_across_shots() { + OUTPUT.with(|output| { + let mut output = output.borrow_mut(); + output.use_std_out(false); + }); + + let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join("tests") + .join("resources") + .join("random-bit.bc"); + + // running 100 random shots with the same seed should be enough to ensure + // that the output is deterministic + let shots = 100; + let rngseed = 42; + + let mut first_output = vec![]; + let result = run_file(path.clone(), None, shots, Some(rngseed), &mut first_output); + assert!(result.is_ok()); + + let mut second_output = vec![]; + let result = run_file(path, None, shots, Some(rngseed), &mut second_output); + assert!(result.is_ok()); + + let first_result = String::from_utf8(first_output).expect("output should be valid utf8"); + let second_result = String::from_utf8(second_output).expect("output should be valid utf8"); + + // sanity check that the output has been captured + // if the output recording is accidentally set to stdout, + // this will fail. + assert!( + first_result.contains("OUTPUT RESULT 1"), + "Ensure global output has use_std_out set to false" + ); + + // the output should be the same for each set of shots + assert_eq!(first_result, second_result); +} + #[test] fn run_file_errors_on_missing_binding() { let bitcode = include_bytes!("resources/missing-intrinsic.bc"); diff --git a/sparsesim/src/lib.rs b/sparsesim/src/lib.rs index a945e7c9..c6421d8c 100644 --- a/sparsesim/src/lib.rs +++ b/sparsesim/src/lib.rs @@ -57,7 +57,7 @@ pub(crate) enum FlushLevel { impl Default for QuantumSim { fn default() -> Self { - Self::new() + Self::new(None) } } @@ -65,14 +65,14 @@ impl Default for QuantumSim { impl QuantumSim { /// Creates a new sparse state quantum simulator object with empty initial state (no qubits allocated, no operations buffered). #[must_use] - pub fn new() -> Self { + pub fn new(rng: Option) -> Self { let mut initial_state = SparseState::default(); initial_state.insert(BigUint::zero(), Complex64::one()); QuantumSim { state: initial_state, id_map: FxHashMap::default(), - rng: RefCell::new(StdRng::from_entropy()), + rng: RefCell::new(rng.unwrap_or_else(StdRng::from_entropy)), h_flag: BigUint::zero(), rx_queue: FxHashMap::default(), ry_queue: FxHashMap::default(), @@ -84,6 +84,10 @@ impl QuantumSim { self.rng.replace(StdRng::seed_from_u64(seed)); } + pub fn take_rng(&mut self) -> StdRng { + self.rng.replace(StdRng::from_entropy()) + } + /// Returns a sorted copy of the current sparse state as a vector of pairs of indices and complex numbers, along with /// the total number of currently allocated qubits to help in interpreting the sparse state. #[allow(clippy::missing_panics_doc)] // reason="Panics can only occur if the keys are not present in the map, which should not happen." @@ -1288,7 +1292,7 @@ mod tests { #[test] #[should_panic(expected = "Duplicate qubit id '0' found in application.")] fn test_duplicate_target() { - let mut sim = QuantumSim::new(); + let mut sim = QuantumSim::new(None); let q = sim.allocate(); sim.mcx(&[q], q); } @@ -1297,7 +1301,7 @@ mod tests { #[test] #[should_panic(expected = "Duplicate qubit id '1' found in application.")] fn test_duplicate_control() { - let mut sim = QuantumSim::new(); + let mut sim = QuantumSim::new(None); let q = sim.allocate(); let c = sim.allocate(); sim.mcx(&[c, c], q); @@ -1307,7 +1311,7 @@ mod tests { #[test] #[should_panic(expected = "Duplicate qubit id '0' found in application.")] fn test_target_in_control() { - let mut sim = QuantumSim::new(); + let mut sim = QuantumSim::new(None); let q = sim.allocate(); let c = sim.allocate(); sim.mcx(&[c, q], q); @@ -1316,7 +1320,7 @@ mod tests { /// Large, entangled state handling. #[test] fn test_large_state() { - let mut sim = QuantumSim::new(); + let mut sim = QuantumSim::new(None); let ctl = sim.allocate(); sim.h(ctl); for _ in 0..4999 { @@ -1332,7 +1336,7 @@ mod tests { /// Verify seeded RNG is predictable. #[test] fn test_seeded_rng() { - let mut sim = QuantumSim::new(); + let mut sim = QuantumSim::new(None); sim.set_rng_seed(42); let q = sim.allocate(); let mut val1 = 0_u64; @@ -1342,7 +1346,7 @@ mod tests { val1 += 1 << i; } } - let mut sim = QuantumSim::new(); + let mut sim = QuantumSim::new(None); sim.set_rng_seed(42); let q = sim.allocate(); let mut val2 = 0_u64; @@ -1358,7 +1362,7 @@ mod tests { /// Verify that dump after swap on released qubits doesn't crash. #[test] fn test_swap_dump() { - let mut sim = QuantumSim::new(); + let mut sim = QuantumSim::new(None); let q = sim.allocate(); let inner_q = sim.allocate(); sim.swap_qubit_ids(q, inner_q); @@ -1369,7 +1373,7 @@ mod tests { /// Verify that swap preserves queued rotations. #[test] fn test_swap_rotations() { - let mut sim = QuantumSim::new(); + let mut sim = QuantumSim::new(None); let (q1, q2) = (sim.allocate(), sim.allocate()); sim.rx(PI / 7.0, q1); sim.ry(PI / 7.0, q2); @@ -1384,7 +1388,7 @@ mod tests { /// a no-op. #[test] fn test_rx_queue_nearly_zero() { - let mut sim = QuantumSim::new(); + let mut sim = QuantumSim::new(None); let q = sim.allocate(); sim.rx(PI / 4.0, q); assert_eq!(sim.state.len(), 1); @@ -1397,7 +1401,7 @@ mod tests { /// a no-op. #[test] fn test_ry_queue_nearly_zero() { - let mut sim = QuantumSim::new(); + let mut sim = QuantumSim::new(None); let q = sim.allocate(); sim.ry(PI / 4.0, q); assert_eq!(sim.state.len(), 1);