Skip to content

Commit

Permalink
Fix handling of backend options in both BasicSimulator and BackendV2.…
Browse files Browse the repository at this point in the history
… Add unit tests. Rename kwargs to unify use of options with aer simulator and update documentation to reflect actual usage.
  • Loading branch information
ElePT committed Jan 30, 2025
1 parent b4c197f commit 8370c17
Show file tree
Hide file tree
Showing 3 changed files with 120 additions and 53 deletions.
4 changes: 2 additions & 2 deletions qiskit/providers/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -361,9 +361,9 @@ def __init__(
self._provider = provider
if fields:
for field in fields:
if field not in self._options.data:
if field not in self._options:
raise AttributeError(f"Options field {field} is not valid for this backend")
self._options.update_config(**fields)
self._options.update_options(**fields)
self.name = name
"""Name of the backend."""
self.description = description
Expand Down
94 changes: 44 additions & 50 deletions qiskit/providers/basic_provider/basic_simulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,17 +100,13 @@ def __init__(
)

self._target = target

# Internal simulator variables
self._local_random = None
self._classical_memory = 0
self._statevector = 0
self._number_of_cmembits = 0
self._number_of_qubits = 0
self._shots = self.options.get("shots")
self._memory = self.options.get("memory")
self._initial_statevector = self.options.get("initial_statevector")
self._seed_simulator = self.options.get("seed_simulator")
self._sample_measure = False
self._local_rng = None

@property
def max_circuits(self) -> None:
Expand Down Expand Up @@ -212,11 +208,9 @@ def _build_basic_target(self) -> Target:
def _default_options(cls) -> Options:
return Options(
shots=1024,
memory=False,
memory=True,
initial_statevector=None,
allow_sample_measuring=True,
seed_simulator=None,
parameter_binds=None,
)

def _add_unitary(self, gate: np.ndarray, qubits: list[int]) -> None:
Expand Down Expand Up @@ -252,7 +246,7 @@ def _get_measure_outcome(self, qubit: int) -> tuple[str, int]:
axis.remove(self._number_of_qubits - 1 - qubit)
probabilities = np.sum(np.abs(self._statevector) ** 2, axis=tuple(axis))
# Compute einsum index string for 1-qubit matrix multiplication
random_number = self._local_random.random()
random_number = self._local_rng.random()
if random_number < probabilities[0]:
return "0", probabilities[0]
# Else outcome was '1'
Expand Down Expand Up @@ -289,7 +283,7 @@ def _add_sample_measure(
# Generate samples on measured qubits as ints with qubit
# position in the bit-string for each int given by the qubit
# position in the sorted measured_qubits list
samples = self._local_random.choice(range(2**num_measured), num_samples, p=probabilities)
samples = self._local_rng.choice(range(2**num_measured), num_samples, p=probabilities)
# Convert the ints to bitstrings
memory = []
for sample in samples:
Expand Down Expand Up @@ -357,36 +351,35 @@ def _validate_initial_statevector(self) -> None:
f"initial statevector is incorrect length: {length} != {required_dim}"
)

def _set_options(self, backend_options: dict | None = None) -> None:
"""Set the backend options for all circuits"""
# Reset default options
def _set_run_options(self, run_options: dict | None = None) -> None:
"""Set the backend run options for all circuits"""

# Reset internal variables every time "run" is called using saved options
self._shots = self.options.get("shots")
self._memory = self.options.get("memory")
self._initial_statevector = self.options.get("initial_statevector")
if "backend_options" in backend_options and backend_options["backend_options"]:
backend_options = backend_options["backend_options"]

# Check for custom initial statevector in backend_options first,
# then config second
if getattr(backend_options, "initial_statevector", None) is not None:
self._initial_statevector = np.array(
backend_options["initial_statevector"], dtype=complex
)
self._seed_simulator = self.options.get("seed_simulator")

# Apply custom run options
if run_options.get("initial_statevector", None) is not None:
self._initial_statevector = np.array(run_options["initial_statevector"], dtype=complex)
if self._initial_statevector is not None:
# Check the initial statevector is normalized
norm = np.linalg.norm(self._initial_statevector)
if round(norm, 12) != 1:
raise BasicProviderError(f"Initial statevector is not normalized: norm {norm} != 1")
if "shots" in backend_options:
self._shots = backend_options["shots"]
if "seed_simulator" in backend_options:
seed_simulator = backend_options["seed_simulator"]
elif getattr(self.options, "seed_simulator", None) is not None:
seed_simulator = self.options["seed_simulator"]
else:
if "shots" in run_options:
self._shots = run_options["shots"]
if "seed_simulator" in run_options:
self._seed_simulator = run_options["seed_simulator"]
elif self._seed_simulator is None:
# For compatibility on Windows force dtype to be int32
# and set the maximum value to be (2 ** 31) - 1
seed_simulator = np.random.randint(2147483647, dtype="int32")
self._seed_simulator = seed_simulator
self._local_random = np.random.default_rng(seed=seed_simulator)
self._seed_simulator = np.random.randint(2147483647, dtype="int32")
if "memory" in run_options:
self._memory = run_options["memory"]
# Set seed for local random number gen.
self._local_rng = np.random.default_rng(seed=self._seed_simulator)

def _initialize_statevector(self) -> None:
"""Set the initial statevector for simulation"""
Expand Down Expand Up @@ -424,41 +417,43 @@ def _validate_measure_sampling(self, circuit: QuantumCircuit) -> None:
self._sample_measure = measure_flag

def run(
self, run_input: QuantumCircuit | list[QuantumCircuit], **backend_options
self, run_input: QuantumCircuit | list[QuantumCircuit], **run_options
) -> BasicProviderJob:
"""Run on the backend.
Args:
run_input: payload of the experiment
backend_options: backend options
run_input (QuantumCircuit or list): the QuantumCircuit (or list
of QuantumCircuit objects) to run
run_options (kwargs): additional runtime backend options
Returns:
BasicProviderJob: derived from BaseJob
Additional Information:
backend_options: Is a dict of options for the backend. It may contain:
* "initial_statevector": vector_like
The "initial_statevector" option specifies a custom
initial statevector for the simulator to be used instead of the all
zero state. This size of this vector must be correct for the number
of qubits in ``run_input`` parameter.
* kwarg options specified in ``run_options`` will temporarily override
any set options of the same name for the current run. These may include:
* "initial_statevector": vector_like. The "initial_statevector" option specifies a custom
initial statevector for the simulator to be used instead of the all
zero state. This size of this vector must be correct for the number
of qubits in ``run_input`` parameter.
* "seed_simulator": int. This is the internal seed for sample generation.
* "shots": int. number of shots used in the simulation.
* "memory": bool. If True, the result will contained the results of every individual shot
simulation.
Example::
backend_options = {
"initial_statevector": np.array([1, 0, 0, 1j]) / math.sqrt(2),
}
backend.run(circuit, initial_statevector = np.array([1, 0, 0, 1j]) / math.sqrt(2))
"""
out_options = {}
for key, value in backend_options.items():
for key, value in run_options.items():
if not hasattr(self.options, key):
warnings.warn(
f"Option {key} is not used by this backend", UserWarning, stacklevel=2
)
else:
out_options[key] = value
self._set_options(backend_options=backend_options)
self._set_run_options(run_options=run_options)
job_id = str(uuid.uuid4())
job = BasicProviderJob(self, job_id, self._run_job(job_id, run_input))
return job
Expand All @@ -478,7 +473,6 @@ def _run_job(self, job_id: str, run_input) -> Result:

self._validate(run_input)
result_list = []
self._memory = True
start = time.time()
for circuit in run_input:
result_list.append(self._run_circuit(circuit))
Expand Down
75 changes: 74 additions & 1 deletion test/python/providers/basic_provider/test_basic_simulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ def setUp(self):
self.circuit = bell

self.seed = 88
self.backend = BasicSimulator()
qasm_dir = os.path.join(
os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "qasm"
)
Expand Down Expand Up @@ -215,6 +214,80 @@ def test_unitary(self):
counts = result.get_counts(0)
self.assertEqual(counts, target_counts)

def test_options(self):
"""Test setting custom backend options during init and run."""
init_statevector = np.zeros(2**2, dtype=complex)
init_statevector[2] = 1
in_options = {
"initial_statevector": init_statevector,
"seed_simulator": 42,
"shots": 100,
"memory": True,
}
backend = BasicSimulator()
backend_with_options = BasicSimulator(
initial_statevector=in_options["initial_statevector"],
seed_simulator=in_options["seed_simulator"],
shots=in_options["shots"],
memory=in_options["memory"],
)
bell = QuantumCircuit(2, 2)
bell.h(0)
bell.cx(0, 1)
bell.measure([0, 1], [0, 1])

with self.subTest(msg="Test init options"):
out_options = backend_with_options.options
for key in out_options:
if key != "initial_statevector":
self.assertEqual(getattr(out_options, key), in_options.get(key))
else:
np.testing.assert_array_equal(getattr(out_options, key), in_options.get(key))

with self.subTest(msg="Test run options"):
out_1 = backend_with_options.run(bell).result().get_counts()
out_2 = (
backend.run(
bell,
initial_statevector=in_options["initial_statevector"],
seed_simulator=in_options["seed_simulator"],
shots=in_options["shots"],
memory=in_options["memory"],
)
.result()
.get_counts()
)
self.assertEqual(out_1, out_2)

with self.subTest(msg="Test run options don't overwrite init"):
init_statevector = np.zeros(2**2, dtype=complex)
init_statevector[3] = 1
other_options = {
"initial_statevector": init_statevector,
"seed_simulator": 0,
"shots": 1000,
"memory": True,
}
out_1 = backend_with_options.run(bell).result().get_counts()
out_2 = (
backend_with_options.run(
bell,
initial_statevector=other_options["initial_statevector"],
seed_simulator=other_options["seed_simulator"],
shots=other_options["shots"],
memory=other_options["memory"],
)
.result()
.get_counts()
)
self.assertNotEqual(out_1, out_2)
out_options = backend_with_options.options
for key in out_options:
if key != "initial_statevector":
self.assertEqual(getattr(out_options, key), in_options.get(key))
else:
np.testing.assert_array_equal(getattr(out_options, key), in_options.get(key))


if __name__ == "__main__":
unittest.main()

0 comments on commit 8370c17

Please sign in to comment.