Skip to content

Commit

Permalink
Support setting classical and quantum random seeds (#1053)
Browse files Browse the repository at this point in the history
This adds new Python APIs `qsharp.set_quantum_seed` and
`qsharp.set_classical_seed` that allow users to fix the seed and
sequence of random quantum measurements and/or random classical numbers
generated from simulating Q#.

Fixes #1013
  • Loading branch information
swernli authored Jan 20, 2024
1 parent b4ed65f commit 285f73f
Show file tree
Hide file tree
Showing 12 changed files with 161 additions and 9 deletions.
33 changes: 31 additions & 2 deletions compiler/qsc/src/interpret.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,12 @@ pub struct Interpreter {
source_package: PackageId,
/// The default simulator backend.
sim: SparseSim,
/// The quantum seed, if any. This is cached here so that it can be used in calls to
/// `run_internal` which use a passed instance of the simulator instead of the one above.
quantum_seed: Option<u64>,
/// The classical seed, if any. This needs to be passed to the evaluator for use in intrinsic
/// calls that produce classical random numbers.
classical_seed: Option<u64>,
/// The evaluator environment.
env: Env,
/// The current state of the evaluator.
Expand Down Expand Up @@ -141,12 +147,23 @@ impl Interpreter {
lowerer,
env: Env::default(),
sim: SparseSim::new(),
state: State::new(map_hir_package_to_fir(source_package_id)),
quantum_seed: None,
classical_seed: None,
state: State::new(map_hir_package_to_fir(source_package_id), None),
package: map_hir_package_to_fir(package_id),
source_package: map_hir_package_to_fir(source_package_id),
})
}

pub fn set_quantum_seed(&mut self, seed: Option<u64>) {
self.quantum_seed = seed;
self.sim.set_seed(seed);
}

pub fn set_classical_seed(&mut self, seed: Option<u64>) {
self.classical_seed = seed;
}

/// Loads the entry expression to the top of the evaluation stack.
/// This is needed for debugging so that when begging to debug with
/// a step action the system is already in the correct state.
Expand Down Expand Up @@ -193,6 +210,7 @@ impl Interpreter {
let expr = self.get_entry_expr()?;
eval(
self.source_package,
self.classical_seed,
expr.into(),
self.compiler.package_store(),
&self.fir_store,
Expand All @@ -210,8 +228,12 @@ impl Interpreter {
receiver: &mut impl Receiver,
) -> Result<Value, Vec<Error>> {
let expr = self.get_entry_expr()?;
if self.quantum_seed.is_some() {
sim.set_seed(self.quantum_seed);
}
eval(
self.source_package,
self.classical_seed,
expr.into(),
self.compiler.package_store(),
&self.fir_store,
Expand Down Expand Up @@ -263,6 +285,7 @@ impl Interpreter {
for stmt_id in stmts {
result = eval(
self.package,
self.classical_seed,
stmt_id.into(),
self.compiler.package_store(),
&self.fir_store,
Expand Down Expand Up @@ -315,9 +338,13 @@ impl Interpreter {
expr: &str,
) -> Result<InterpretResult, Vec<Error>> {
let stmt_id = self.compile_expr_to_stmt(expr)?;
if self.quantum_seed.is_some() {
sim.set_seed(self.quantum_seed);
}

Ok(eval(
self.package,
self.classical_seed,
stmt_id.into(),
self.compiler.package_store(),
&self.fir_store,
Expand Down Expand Up @@ -448,16 +475,18 @@ impl Interpreter {
}

/// Wrapper function for `qsc_eval::eval` that handles error conversion.
#[allow(clippy::too_many_arguments)]
fn eval(
package: PackageId,
classical_seed: Option<u64>,
id: EvalId,
package_store: &PackageStore,
fir_store: &fir::PackageStore,
env: &mut Env,
sim: &mut impl Backend<ResultType = impl Into<val::Result>>,
receiver: &mut impl Receiver,
) -> InterpretResult {
qsc_eval::eval(package, id, fir_store, env, sim, receiver)
qsc_eval::eval(package, classical_seed, id, fir_store, env, sim, receiver)
.map_err(|(error, call_stack)| eval_error(package_store, fir_store, call_stack, error))
}

Expand Down
1 change: 1 addition & 0 deletions compiler/qsc_codegen/src/qir_base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ pub fn generate_qir(
let mut out = GenericReceiver::new(&mut stdout);
let result = eval(
package,
None,
entry_expr.into(),
&fir_store,
&mut Env::default(),
Expand Down
10 changes: 10 additions & 0 deletions compiler/qsc_eval/src/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
use num_bigint::BigUint;
use num_complex::Complex;
use quantum_sparse_sim::QuantumSim;
use rand::RngCore;

use crate::val::Value;

Expand Down Expand Up @@ -42,6 +43,8 @@ pub trait Backend {
fn custom_intrinsic(&mut self, _name: &str, _arg: Value) -> Option<Result<Value, String>> {
None
}

fn set_seed(&mut self, _seed: Option<u64>) {}
}

/// Default backend used when targeting sparse simulation.
Expand Down Expand Up @@ -203,4 +206,11 @@ impl Backend for SparseSim {
_ => None,
}
}

fn set_seed(&mut self, seed: Option<u64>) {
match seed {
Some(seed) => self.sim.set_rng_seed(seed),
None => self.sim.set_rng_seed(rand::thread_rng().next_u64()),
}
}
}
7 changes: 4 additions & 3 deletions compiler/qsc_eval/src/intrinsic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use crate::{
Error,
};
use num_bigint::BigInt;
use rand::Rng;
use rand::{rngs::StdRng, Rng};
use std::array;

#[allow(clippy::too_many_lines)]
Expand All @@ -22,6 +22,7 @@ pub(crate) fn call(
arg: Value,
arg_span: PackageSpan,
sim: &mut dyn Backend<ResultType = impl Into<val::Result>>,
rng: &mut StdRng,
out: &mut dyn Receiver,
) -> Result<Value, Error> {
match name {
Expand Down Expand Up @@ -66,7 +67,7 @@ pub(crate) fn call(
if lo > hi {
Err(Error::EmptyRange(arg_span))
} else {
Ok(Value::Int(rand::thread_rng().gen_range(lo..=hi)))
Ok(Value::Int(rng.gen_range(lo..=hi)))
}
}
"DrawRandomDouble" => {
Expand All @@ -76,7 +77,7 @@ pub(crate) fn call(
if lo > hi {
Err(Error::EmptyRange(arg_span))
} else {
Ok(Value::Double(rand::thread_rng().gen_range(lo..=hi)))
Ok(Value::Double(rng.gen_range(lo..=hi)))
}
}
#[allow(clippy::cast_possible_truncation)]
Expand Down
23 changes: 20 additions & 3 deletions compiler/qsc_eval/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,10 @@ use qsc_fir::fir::{
StmtKind, StoreItemId, StringComponent, UnOp,
};
use qsc_fir::ty::Ty;
use rand::{rngs::StdRng, SeedableRng};
use rustc_hash::FxHashMap;
use std::{
cell::RefCell,
collections::hash_map::Entry,
fmt::{self, Display, Formatter, Write},
iter,
Expand Down Expand Up @@ -196,13 +198,14 @@ impl From<StmtId> for EvalId {
/// On internal error where no result is returned.
pub fn eval(
package: PackageId,
seed: Option<u64>,
id: EvalId,
globals: &impl PackageStoreLookup,
env: &mut Env,
sim: &mut impl Backend<ResultType = impl Into<val::Result>>,
receiver: &mut impl Receiver,
) -> Result<Value, (Error, Vec<Frame>)> {
let mut state = State::new(package);
let mut state = State::new(package, seed);
match id {
EvalId::Expr(expr) => state.push_expr(expr),
EvalId::Stmt(stmt) => state.push_stmt(stmt),
Expand Down Expand Up @@ -430,17 +433,23 @@ pub struct State {
package: PackageId,
call_stack: CallStack,
current_span: Span,
rng: RefCell<StdRng>,
}

impl State {
#[must_use]
pub fn new(package: PackageId) -> Self {
pub fn new(package: PackageId, classical_seed: Option<u64>) -> Self {
let rng = match classical_seed {
Some(seed) => RefCell::new(StdRng::seed_from_u64(seed)),
None => RefCell::new(StdRng::from_entropy()),
};
Self {
stack: Vec::new(),
vals: Vec::new(),
package,
call_stack: CallStack::default(),
current_span: Span::default(),
rng,
}
}

Expand Down Expand Up @@ -1089,7 +1098,15 @@ impl State {
match &callee.implementation {
CallableImpl::Intrinsic => {
let name = &callee.name.name;
let val = intrinsic::call(name, callee_span, arg, arg_span, sim, out)?;
let val = intrinsic::call(
name,
callee_span,
arg,
arg_span,
sim,
&mut self.rng.borrow_mut(),
out,
)?;
self.push_val(val);
Ok(())
}
Expand Down
2 changes: 1 addition & 1 deletion compiler/qsc_eval/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ pub(super) fn eval_expr(
package: PackageId,
out: &mut impl Receiver,
) -> Result<Value, (Error, Vec<Frame>)> {
let mut state = State::new(package);
let mut state = State::new(package, None);
let mut env = Env::default();
state.push_expr(expr);
let StepResult::Return(value) =
Expand Down
4 changes: 4 additions & 0 deletions pip/qsharp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
run,
compile,
estimate,
set_quantum_seed,
set_classical_seed,
dump_machine,
)

Expand All @@ -27,6 +29,8 @@
"init",
"eval",
"run",
"set_quantum_seed",
"set_classical_seed",
"dump_machine",
"compile",
"estimate",
Expand Down
16 changes: 16 additions & 0 deletions pip/qsharp/_native.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,22 @@ class Interpreter:
:returns resources: The estimated resources.
"""
...
def set_quantum_seed(self, seed: Optional[int]) -> None:
"""
Sets the seed for the quantum random number generator.
:param seed: The seed to use for the quantum random number generator. If None,
the seed will be generated from entropy.
"""
...
def set_classical_seed(self, seed: Optional[int]) -> None:
"""
Sets the seed for the classical random number generator.
:param seed: The seed to use for the classical random number generator. If None,
the seed will be generated from entropy.
"""
...
def dump_machine(self) -> StateDump:
"""
Returns the sparse state vector of the simulator as a StateDump object.
Expand Down
20 changes: 20 additions & 0 deletions pip/qsharp/_qsharp.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,26 @@ def estimate(
json.loads(get_interpreter().estimate(entry_expr, json.dumps(params)))
)

def set_quantum_seed(seed: Optional[int]) -> None:
"""
Sets the seed for the random number generator used for quantum measurements.
This applies to all Q# code executed, compiled, or estimated.
:param seed: The seed to use for the quantum random number generator.
If None, the seed will be generated from entropy.
"""
get_interpreter().set_quantum_seed(seed)

def set_classical_seed(seed: Optional[int]) -> None:
"""
Sets the seed for the random number generator used for standard
library classical random number operations.
This applies to all Q# code executed, compiled, or estimated.
:param seed: The seed to use for the classical random number generator.
If None, the seed will be generated from entropy.
"""
get_interpreter().set_classical_seed(seed)

def dump_machine() -> StateDump:
"""
Expand Down
10 changes: 10 additions & 0 deletions pip/src/interpreter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,16 @@ impl Interpreter {
}
}

/// Sets the quantum seed for the interpreter.
fn set_quantum_seed(&mut self, seed: Option<u64>) {
self.interpreter.set_quantum_seed(seed);
}

/// Sets the classical seed for the interpreter.
fn set_classical_seed(&mut self, seed: Option<u64>) {
self.interpreter.set_classical_seed(seed);
}

/// Dumps the quantum state of the interpreter.
/// Returns a tuple of (amplitudes, num_qubits), where amplitudes is a dictionary from integer indices to
/// pairs of real and imaginary amplitudes.
Expand Down
18 changes: 18 additions & 0 deletions pip/tests/test_interpreter.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,24 @@ def callback(output):
)
assert called

def test_quantum_seed() -> None:
e = Interpreter(TargetProfile.Unrestricted)
e.set_quantum_seed(42)
value1 = e.interpret("{ use qs = Qubit[16]; for q in qs { H(q); }; Microsoft.Quantum.Measurement.MResetEachZ(qs) }")
e = Interpreter(TargetProfile.Unrestricted)
e.set_quantum_seed(42)
value2 = e.interpret("{ use qs = Qubit[16]; for q in qs { H(q); }; Microsoft.Quantum.Measurement.MResetEachZ(qs) }")
assert value1 == value2

def test_classical_seed() -> None:
e = Interpreter(TargetProfile.Unrestricted)
e.set_classical_seed(42)
value1 = e.interpret("{ mutable res = []; for _ in 0..15{ set res += [Microsoft.Quantum.Random.DrawRandomInt(0, 100)]; }; res }")
e = Interpreter(TargetProfile.Unrestricted)
e.set_classical_seed(42)
value2 = e.interpret("{ mutable res = []; for _ in 0..15{ set res += [Microsoft.Quantum.Random.DrawRandomInt(0, 100)]; }; res }")
assert value1 == value2


def test_dump_machine() -> None:
e = Interpreter(TargetProfile.Unrestricted)
Expand Down
26 changes: 26 additions & 0 deletions pip/tests/test_qsharp.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,32 @@ def test_stdout_multiple_lines() -> None:

assert f.getvalue() == "STATE:\n|0⟩: 1.0000+0.0000𝑖\nHello!\n"

def test_quantum_seed() -> None:
qsharp.init(target_profile=qsharp.TargetProfile.Unrestricted)
qsharp.set_quantum_seed(42)
value1 = qsharp.eval("{ use qs = Qubit[32]; for q in qs { H(q); }; Microsoft.Quantum.Measurement.MResetEachZ(qs) }")
qsharp.init(target_profile=qsharp.TargetProfile.Unrestricted)
qsharp.set_quantum_seed(42)
value2 = qsharp.eval("{ use qs = Qubit[32]; for q in qs { H(q); }; Microsoft.Quantum.Measurement.MResetEachZ(qs) }")
assert value1 == value2
qsharp.set_quantum_seed(None)
value3 = qsharp.eval("{ use qs = Qubit[32]; for q in qs { H(q); }; Microsoft.Quantum.Measurement.MResetEachZ(qs) }")
assert value1 != value3


def test_classical_seed() -> None:
qsharp.init(target_profile=qsharp.TargetProfile.Unrestricted)
qsharp.set_classical_seed(42)
value1 = qsharp.eval("{ mutable res = []; for _ in 0..15{ set res += [(Microsoft.Quantum.Random.DrawRandomInt(0, 100), Microsoft.Quantum.Random.DrawRandomDouble(0.0, 1.0))]; }; res }")
qsharp.init(target_profile=qsharp.TargetProfile.Unrestricted)
qsharp.set_classical_seed(42)
value2 = qsharp.eval("{ mutable res = []; for _ in 0..15{ set res += [(Microsoft.Quantum.Random.DrawRandomInt(0, 100), Microsoft.Quantum.Random.DrawRandomDouble(0.0, 1.0))]; }; res }")
assert value1 == value2
qsharp.init(target_profile=qsharp.TargetProfile.Unrestricted)
qsharp.set_classical_seed(None)
value3 = qsharp.eval("{ mutable res = []; for _ in 0..15{ set res += [(Microsoft.Quantum.Random.DrawRandomInt(0, 100), Microsoft.Quantum.Random.DrawRandomDouble(0.0, 1.0))]; }; res }")
assert value1 != value3


def test_dump_machine() -> None:
qsharp.init(target_profile=qsharp.TargetProfile.Unrestricted)
Expand Down

0 comments on commit 285f73f

Please sign in to comment.