Skip to content

Commit

Permalink
Return methods to original order
Browse files Browse the repository at this point in the history
  • Loading branch information
ElePT committed Jan 9, 2024
1 parent a36a6e6 commit 56ecc8d
Show file tree
Hide file tree
Showing 2 changed files with 167 additions and 169 deletions.
332 changes: 166 additions & 166 deletions qiskit/providers/basic_provider/basic_simulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,38 +239,23 @@ def _default_options(cls) -> Options:
parameter_binds=None,
)

def _set_options(
self, qobj_config: QasmQobjConfig | None = None, backend_options: dict | None = None
) -> None:
"""Set the backend options for all experiments in a qobj"""
# Reset default options
self._initial_statevector = self.options.get("initial_statevector")
self._chop_threshold = self.options.get("chop_threshold")
if "backend_options" in backend_options and backend_options["backend_options"]:
backend_options = backend_options["backend_options"]
def _add_unitary(self, gate: np.ndarray, qubits: list[int]) -> None:
"""Apply an N-qubit unitary matrix.
# Check for custom initial statevector in backend_options first,
# then config second
if (
"initial_statevector" in backend_options
and backend_options["initial_statevector"] is not None
):
self._initial_statevector = np.array(
backend_options["initial_statevector"], dtype=complex
)
elif hasattr(qobj_config, "initial_statevector"):
self._initial_statevector = np.array(qobj_config.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")
# Check for custom chop threshold
# Replace with custom options
if "chop_threshold" in backend_options:
self._chop_threshold = backend_options["chop_threshold"]
elif hasattr(qobj_config, "chop_threshold"):
self._chop_threshold = qobj_config.chop_threshold
Args:
gate (matrix_like): an N-qubit unitary matrix
qubits (list): the list of N-qubits.
"""
# Get the number of qubits
num_qubits = len(qubits)
# Compute einsum index string for 1-qubit matrix multiplication
indexes = einsum_vecmul_index(qubits, self._number_of_qubits)
# Convert to complex rank-2N tensor
gate_tensor = np.reshape(np.array(gate, dtype=complex), num_qubits * [2, 2])
# Apply matrix multiplication
self._statevector = np.einsum(
indexes, gate_tensor, self._statevector, dtype=complex, casting="no"
)

def _get_measure_outcome(self, qubit: int) -> tuple[str, int]:
"""Simulate the outcome of measurement of a qubit.
Expand All @@ -293,6 +278,51 @@ def _get_measure_outcome(self, qubit: int) -> tuple[str, int]:
# Else outcome was '1'
return "1", probabilities[1]

def _add_sample_measure(
self, measure_params: list[list[int, int]], num_samples: int
) -> list[hex]:
"""Generate memory samples from current statevector.
Args:
measure_params (list): List of (qubit, cmembit) values for
measure instructions to sample.
num_samples (int): The number of memory samples to generate.
Returns:
list: A list of memory values in hex format.
"""
# Get unique qubits that are actually measured and sort in
# ascending order
measured_qubits = sorted({qubit for qubit, cmembit in measure_params})
num_measured = len(measured_qubits)
# We use the axis kwarg for numpy.sum to compute probabilities
# this sums over all non-measured qubits to return a vector
# of measure probabilities for the measured qubits
axis = list(range(self._number_of_qubits))
for qubit in reversed(measured_qubits):
# Remove from largest qubit to smallest so list position is correct
# with respect to position from end of the list
axis.remove(self._number_of_qubits - 1 - qubit)
probabilities = np.reshape(
np.sum(np.abs(self._statevector) ** 2, axis=tuple(axis)), 2**num_measured
)
# 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)
# Convert the ints to bitstrings
memory = []
for sample in samples:
classical_memory = self._classical_memory
for qubit, cmembit in measure_params:
pos = measured_qubits.index(qubit)
qubit_outcome = int((sample & (1 << pos)) >> pos)
membit = 1 << cmembit
classical_memory = (classical_memory & (~membit)) | (qubit_outcome << cmembit)
value = bin(classical_memory)[2:]
memory.append(hex(int(value, 2)))
return memory

def _add_measure(self, qubit: int, cmembit: int, cregbit: int | None = None) -> None:
"""Apply a measure instruction to a qubit.
Expand Down Expand Up @@ -321,24 +351,6 @@ def _add_measure(self, qubit: int, cmembit: int, cregbit: int | None = None) ->
# update classical state
self._add_unitary(update_diag, [qubit])

def _add_unitary(self, gate: np.ndarray, qubits: list[int]) -> None:
"""Apply an N-qubit unitary matrix.
Args:
gate (matrix_like): an N-qubit unitary matrix
qubits (list): the list of N-qubits.
"""
# Get the number of qubits
num_qubits = len(qubits)
# Compute einsum index string for 1-qubit matrix multiplication
indexes = einsum_vecmul_index(qubits, self._number_of_qubits)
# Convert to complex rank-2N tensor
gate_tensor = np.reshape(np.array(gate, dtype=complex), num_qubits * [2, 2])
# Apply matrix multiplication
self._statevector = np.einsum(
indexes, gate_tensor, self._statevector, dtype=complex, casting="no"
)

def _add_reset(self, qubit: int) -> None:
"""Apply a reset instruction to a qubit.
Expand All @@ -359,62 +371,6 @@ def _add_reset(self, qubit: int) -> None:
update = [[0, 1 / np.sqrt(probability)], [0, 0]]
self._add_unitary(update, [qubit])

def _add_sample_measure(
self, measure_params: list[list[int, int]], num_samples: int
) -> list[hex]:
"""Generate memory samples from current statevector.
Args:
measure_params (list): List of (qubit, cmembit) values for
measure instructions to sample.
num_samples (int): The number of memory samples to generate.
Returns:
list: A list of memory values in hex format.
"""
# Get unique qubits that are actually measured and sort in
# ascending order
measured_qubits = sorted({qubit for qubit, cmembit in measure_params})
num_measured = len(measured_qubits)
# We use the axis kwarg for numpy.sum to compute probabilities
# this sums over all non-measured qubits to return a vector
# of measure probabilities for the measured qubits
axis = list(range(self._number_of_qubits))
for qubit in reversed(measured_qubits):
# Remove from largest qubit to smallest so list position is correct
# with respect to position from end of the list
axis.remove(self._number_of_qubits - 1 - qubit)
probabilities = np.reshape(
np.sum(np.abs(self._statevector) ** 2, axis=tuple(axis)), 2**num_measured
)
# 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)
# Convert the ints to bitstrings
memory = []
for sample in samples:
classical_memory = self._classical_memory
for qubit, cmembit in measure_params:
pos = measured_qubits.index(qubit)
qubit_outcome = int((sample & (1 << pos)) >> pos)
membit = 1 << cmembit
classical_memory = (classical_memory & (~membit)) | (qubit_outcome << cmembit)
value = bin(classical_memory)[2:]
memory.append(hex(int(value, 2)))
return memory

def _initialize_statevector(self) -> None:
"""Set the initial statevector for simulation"""
if self._initial_statevector is None:
# Set to default state of all qubits in |0>
self._statevector = np.zeros(2**self._number_of_qubits, dtype=complex)
self._statevector[0] = 1
else:
self._statevector = self._initial_statevector.copy()
# Reshape to rank-N tensor
self._statevector = np.reshape(self._statevector, self._number_of_qubits * [2])

def _validate_initial_statevector(self) -> None:
"""Validate an initial statevector"""
# If initial statevector isn't set we don't need to validate
Expand All @@ -428,26 +384,49 @@ def _validate_initial_statevector(self) -> None:
f"initial statevector is incorrect length: {length} != {required_dim}"
)

def _validate(self, qobj: QasmQobj) -> None:
"""Semantic validations of the qobj which cannot be done via schemas."""
n_qubits = qobj.config.n_qubits
max_qubits = self.configuration().n_qubits
if n_qubits > max_qubits:
raise BasicProviderError(
f"Number of qubits {n_qubits} is greater than maximum ({max_qubits}) "
f'for "{self.name}".'
def _set_options(
self, qobj_config: QasmQobjConfig | None = None, backend_options: dict | None = None
) -> None:
"""Set the backend options for all experiments in a qobj"""
# Reset default options
self._initial_statevector = self.options.get("initial_statevector")
self._chop_threshold = self.options.get("chop_threshold")
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 (
"initial_statevector" in backend_options
and backend_options["initial_statevector"] is not None
):
self._initial_statevector = np.array(
backend_options["initial_statevector"], dtype=complex
)
for experiment in qobj.experiments:
name = experiment.header.name
if experiment.config.memory_slots == 0:
logger.warning(
'No classical registers in circuit "%s", counts will be empty.', name
)
elif "measure" not in [op.name for op in experiment.instructions]:
logger.warning(
'No measurements in circuit "%s", classical register will remain all zeros.',
name,
)
elif hasattr(qobj_config, "initial_statevector"):
self._initial_statevector = np.array(qobj_config.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")
# Check for custom chop threshold
# Replace with custom options
if "chop_threshold" in backend_options:
self._chop_threshold = backend_options["chop_threshold"]
elif hasattr(qobj_config, "chop_threshold"):
self._chop_threshold = qobj_config.chop_threshold

def _initialize_statevector(self) -> None:
"""Set the initial statevector for simulation"""
if self._initial_statevector is None:
# Set to default state of all qubits in |0>
self._statevector = np.zeros(2**self._number_of_qubits, dtype=complex)
self._statevector[0] = 1
else:
self._statevector = self._initial_statevector.copy()
# Reshape to rank-N tensor
self._statevector = np.reshape(self._statevector, self._number_of_qubits * [2])

def _validate_measure_sampling(self, experiment: QasmQobjExperiment) -> None:
"""Determine if measure sampling is allowed for an experiment
Expand Down Expand Up @@ -489,6 +468,50 @@ def _validate_measure_sampling(self, experiment: QasmQobjExperiment) -> None:
# measure sampling is allowed
self._sample_measure = True

def run(
self, run_input: QuantumCircuit | list[QuantumCircuit], **backend_options
) -> BasicProviderJob:
"""Run on the backend.
Args:
run_input (QuantumCircuit or list): payload of the experiment
backend_options (dict): 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
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.
Example::
backend_options = {
"initial_statevector": np.array([1, 0, 0, 1j]) / np.sqrt(2),
}
"""
from qiskit.compiler import assemble

out_options = {}
for key in backend_options:
if not hasattr(self.options, key):
warnings.warn(
"Option %s is not used by this backend" % key, UserWarning, stacklevel=2
)
else:
out_options[key] = backend_options[key]
qobj = assemble(run_input, self, **out_options)
qobj_options = qobj.config
self._set_options(qobj_config=qobj_options, backend_options=backend_options)
job_id = str(uuid.uuid4())
job = BasicProviderJob(self, job_id, self._run_job(job_id, qobj))
return job

def _run_job(self, job_id: str, qobj: QasmQobj) -> Result:
"""Run experiments in qobj
Expand Down Expand Up @@ -711,46 +734,23 @@ def run_experiment(self, experiment: QasmQobjExperiment) -> dict[str, ...]:
"header": experiment.header.to_dict(),
}

def run(
self, run_input: QuantumCircuit | list[QuantumCircuit], **backend_options
) -> BasicProviderJob:
"""Run on the backend.
Args:
run_input (QuantumCircuit or list): payload of the experiment
backend_options (dict): 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
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.
Example::
backend_options = {
"initial_statevector": np.array([1, 0, 0, 1j]) / np.sqrt(2),
}
"""
from qiskit.compiler import assemble

out_options = {}
for key in backend_options:
if not hasattr(self.options, key):
warnings.warn(
"Option %s is not used by this backend" % key, UserWarning, stacklevel=2
def _validate(self, qobj: QasmQobj) -> None:
"""Semantic validations of the qobj which cannot be done via schemas."""
n_qubits = qobj.config.n_qubits
max_qubits = self.configuration().n_qubits
if n_qubits > max_qubits:
raise BasicProviderError(
f"Number of qubits {n_qubits} is greater than maximum ({max_qubits}) "
f'for "{self.name}".'
)
for experiment in qobj.experiments:
name = experiment.header.name
if experiment.config.memory_slots == 0:
logger.warning(
'No classical registers in circuit "%s", counts will be empty.', name
)
elif "measure" not in [op.name for op in experiment.instructions]:
logger.warning(
'No measurements in circuit "%s", classical register will remain all zeros.',
name,
)
else:
out_options[key] = backend_options[key]
qobj = assemble(run_input, self, **out_options)
qobj_options = qobj.config
self._set_options(qobj_config=qobj_options, backend_options=backend_options)
job_id = str(uuid.uuid4())
job = BasicProviderJob(self, job_id, self._run_job(job_id, qobj))
return job
4 changes: 1 addition & 3 deletions test/python/circuit/test_parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -1084,9 +1084,7 @@ def test_executing_parameterized_instruction_bound_early(self, target_type):
bound_qc = unbound_qc.assign_parameters({theta: numpy.pi / 2})

shots = 1024
job = execute(
bound_qc, backend=BasicProvider.get_backend("basic_simulator"), shots=shots
)
job = execute(bound_qc, backend=BasicProvider.get_backend("basic_simulator"), shots=shots)
self.assertDictAlmostEqual(job.result().get_counts(), {"1": shots}, 0.05 * shots)

def test_num_parameters(self):
Expand Down

0 comments on commit 56ecc8d

Please sign in to comment.