diff --git a/docs/conf.py b/docs/conf.py index ab137a43f..5a0811a89 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -21,7 +21,7 @@ # The short X.Y version version = '' # The full version, including alpha/beta/rc tags -release = '0.4.1' +release = '0.4.2' extensions = [ 'sphinx.ext.napoleon', @@ -82,3 +82,7 @@ nbsphinx_execute = 'always' nbsphinx_widgets_path = '' exclude_patterns = ['_build', '**.ipynb_checkpoints'] + +# this is tied to the temporary restriction to JAX versions <=0.4.6. See issue #190 +import os +os.environ["JAX_JIT_PJIT_API_MERGE"] = "0" \ No newline at end of file diff --git a/docs/release_notes.rst b/docs/release_notes.rst index ec77acc34..db234e106 100644 --- a/docs/release_notes.rst +++ b/docs/release_notes.rst @@ -1 +1,152 @@ -.. release-notes:: Release Notes +============= +Release Notes +============= + +.. _Release Notes_0.4.1-12: + +0.4.1-12 +======== + +.. _Release Notes_0.4.1-12_Prelude: + +Prelude +------- + +.. releasenotes/notes/patch-0.4.2-6a7c7bf380e54187.yaml @ None + +Qiskit Dynamics 0.4.2 is an incremental release with minor bug fixes and additional warnings to help guide users through issues. + + +.. _Release Notes_0.4.1-12_Upgrade Notes: + +Upgrade Notes +------------- + +.. releasenotes/notes/subsystem_labels-removal-9fcc71c310eff220.yaml @ b'cf256192ce1c0ef7c2f4c696d9be64234b48b68f' + +- The ``subsystem_labels`` option has been removed from the :class:`.DynamicsBackend`. This + removal impacts some technical aspects of the backend returned by + :meth:`.DynamicsBackend.from_backend` when the ``subsystem_list`` argument is used. Using the + ``subsystem_list`` argument with :meth:`.DynamicsBackend.from_backend` restricts the internally + constructed model to the qubits in ``subsystem_list``. When doing so previously, the option + ``subsystem_labels`` would be set to ``subsystem_labels``, and ``subsystem_dims`` would record + only the dimensions for the systems in ``subsystem_labels``. To account for the fact that + ``subsystem_labels`` no longer exists, :meth:`.DynamicsBackend.from_backend` now constructs + ``subsystem_dims`` to list a dimension for all of the qubits in the original backend, however + now the dimensions of the removed systems are given as 1 (i.e. they are treated as trivial + quantum systems with a single state). This change is made only for technical bookkeping + purposes, and has no impact on the core simulation behaviour. + + +.. _Release Notes_0.4.1-12_Bug Fixes: + +Bug Fixes +--------- + +.. releasenotes/notes/carrier-freq-0-19ad4362c874944f.yaml @ None + +- In the case that ``envelope`` is a constant, the :meth:`.Signal.__init__` method has been + updated to not attempt to evaluate ``carrier_freq == 0.0`` if ``carrier_freq`` is a JAX tracer. + In this case, it is not possible to determine if the :class:`.Signal` instance is constant. This + resolves an error that was being raised during JAX tracing if ``carrier_freq`` is abstract. + +.. releasenotes/notes/classical-registers-9bb117398a4d21d5.yaml @ None + +- Fixes bug in :meth:`.DynamicsBackend.run` that caused miscounting of the number of classical + registers in a :class:`~qiskit.circuit.QuantumCircuit` (issue #251). + +.. releasenotes/notes/normalize-probabilities-d729245bb3fe5f10.yaml @ b'6ede10a2bc8c61e8640db9085d4d1d9423341550' + +- ``DynamicsBackend.options.normalize_states`` now also controls whether or not the probability + distribution over outcomes is normalized before sampling outcomes. + + +.. _Release Notes_0.4.1-12_Other Notes: + +Other Notes +----------- + +.. releasenotes/notes/patch-0.4.2-6a7c7bf380e54187.yaml @ None + +- For users that have JAX installed, a warning has been added upon import of Qiskit Dynamics to + notify the user of issues with certain versions: JAX versions newer than ``0.4.6`` break the + ``perturbation`` module, and to use ``perturbation`` module with versions ``0.4.4``, ``0.4.5``, + or ``0.4.6``, it is necessary to set ``os.environ['JAX_JIT_PJIT_API_MERGE'] = '0'`` before + importing JAX or Dynamics. + +.. releasenotes/notes/patch-0.4.2-6a7c7bf380e54187.yaml @ None + +- A warning has been added to :class:`.InstructionToSignals` class when converting pulse schedules + to signals to notify the user if the usage of ``SetFrequency`` or ``ShiftFrequency`` commands + result in a digital carrier frequency larger than the Nyquist frequency of the envelope sample + size ``dt``. + + +.. _Release Notes_0.4.1: + +0.4.1 +===== + +.. _Release Notes_0.4.1_Prelude: + +Prelude +------- + +.. releasenotes/notes/0.4/patch-0.4.1-d339aa8669341341.yaml @ b'd6e280259d120d31723e0220a91cbd7dd8099298' + +Qiskit Dynamics 0.4.1 is an incremental release with minor bug fixes, documentation updates, and usability features. + +.. _Release Notes_0.4.1_New Features: + +New Features +------------ + +.. releasenotes/notes/measurement_property_bug_fix-12461088823a943c.yaml @ b'807edf92d7f5d6f34715fff9d21614d77cd096d3' + +- The :meth:`DynamicsBackend.from_backend` method has been updated to automatically populate the + ``control_channel_map`` option based on the supplied backend if the user does not supply one. + + +.. _Release Notes_0.4.1_Known Issues: + +Known Issues +------------ + +.. releasenotes/notes/0.4/diffrax-bound-0bd80c01b7f4b48f.yaml @ b'd6e280259d120d31723e0220a91cbd7dd8099298' + +- Due to a bug in JAX, Dynamics can only be used with jax<=0.4.6. As they depend on newer versions + of JAX, Dynamics is also now only compatible with diffrax<=0.3.1 and equinox<=0.10.3. + + +.. _Release Notes_0.4.1_Bug Fixes: + +Bug Fixes +--------- + +.. releasenotes/notes/0.4/multiset-order-bug-fix-1f1603ee1e230cba.yaml @ b'd6e280259d120d31723e0220a91cbd7dd8099298' + +- Fixes a bug in the perturbation module with internal sorting of ``Multiset`` instances, which + caused incorrect computation of perturbation theory terms when ``>10`` perturbations are + present. + +.. releasenotes/notes/measurement_property_bug_fix-12461088823a943c.yaml @ b'807edf92d7f5d6f34715fff9d21614d77cd096d3' + +- A bug in :meth:`DynamicsBackend.__init__` causing existing measurement instructions for a + user-supplied :class:`Target` to be overwritten has been fixed. + + +.. _Release Notes_0.4.1_Other Notes: + +Other Notes +----------- + +.. releasenotes/notes/0.4/move-repo-c0b48ba3b0ced8db.yaml @ b'd6e280259d120d31723e0220a91cbd7dd8099298' + +- The repository has been moved from + [github.com/Qiskit/qiskit-dynamics](https://github.com/Qiskit/qiskit-dynamics) to + [github.com/Qiskit-Extensions/qiskit-dynamics](https://github.com/Qiskit-Extensions/qiskit-dynamics), + and the documentation has been moved from + [qiskit.org/documentation/dynamics](https://qiskit.org/documentation/dynamics) to + [qiskit.org/ecosystem/dynamics](https://qiskit.org/ecosystem/dynamics/). + + diff --git a/qiskit_dynamics/VERSION.txt b/qiskit_dynamics/VERSION.txt index 267577d47..2b7c5ae01 100644 --- a/qiskit_dynamics/VERSION.txt +++ b/qiskit_dynamics/VERSION.txt @@ -1 +1 @@ -0.4.1 +0.4.2 diff --git a/qiskit_dynamics/backend/backend_string_parser/hamiltonian_string_parser.py b/qiskit_dynamics/backend/backend_string_parser/hamiltonian_string_parser.py index 9e94deaba..883c4b862 100644 --- a/qiskit_dynamics/backend/backend_string_parser/hamiltonian_string_parser.py +++ b/qiskit_dynamics/backend/backend_string_parser/hamiltonian_string_parser.py @@ -163,12 +163,12 @@ def parse_backend_hamiltonian_dict( # force keys in hamiltonian['qub'] to be ints qub_dict = {int(key): val for key, val in hamiltonian_dict["qub"].items()} - subsystem_dims = {int(qubit): qub_dict[int(qubit)] for qubit in subsystem_list} + subsystem_dims_dict = {int(qubit): qub_dict[int(qubit)] for qubit in subsystem_list} # Parse the Hamiltonian system = _regex_parser( operator_str=hamiltonian_dict["h_str"], - subsystem_dims=subsystem_dims, + subsystem_dims_dict=subsystem_dims_dict, subsystem_list=subsystem_list, ) @@ -227,7 +227,12 @@ def parse_backend_hamiltonian_dict( *sorted(zip(reduced_channels, hamiltonian_operators)) ) - return static_hamiltonian, list(hamiltonian_operators), list(reduced_channels), subsystem_dims + return ( + static_hamiltonian, + list(hamiltonian_operators), + list(reduced_channels), + subsystem_dims_dict, + ) def _hamiltonian_pre_parse_exceptions(hamiltonian_dict: dict): diff --git a/qiskit_dynamics/backend/backend_string_parser/operator_from_string.py b/qiskit_dynamics/backend/backend_string_parser/operator_from_string.py index ff96c2008..45ada7a4e 100644 --- a/qiskit_dynamics/backend/backend_string_parser/operator_from_string.py +++ b/qiskit_dynamics/backend/backend_string_parser/operator_from_string.py @@ -26,14 +26,14 @@ def _operator_from_string( - op_label: str, subsystem_label: int, subsystem_dims: Dict[int, int] + op_label: str, subsystem_label: int, subsystem_dims_dict: Dict[int, int] ) -> np.ndarray: r"""Generates a dense operator acting on a single subsystem, tensoring identities for remaining subsystems. The single system operator is specified via a string in ``op_label``, the list of subsystems and their corresponding dimensions are specified in the - dictionary ``subsystem_dims``, with system label being the keys specified as ``int``s, + dictionary ``subsystem_dims_dict``, with system label being the keys specified as ``int``s, and system dimensions the values also specified as ``int``s, and ``subsystem_label`` indicates which subsystem the operator specified by ``op_label`` acts on. @@ -61,7 +61,7 @@ def _operator_from_string( Args: op_label: The string labelling the single system operator. subsystem_label: Index of the subsystem to apply the operator. - subsystem_dims: Dictionary of subsystem labels and dimensions. + subsystem_dims_dict: Dictionary of subsystem labels and dimensions. Returns: np.ndarray corresponding to the specified operator. @@ -75,12 +75,12 @@ def _operator_from_string( if op_func is None: raise QiskitError(f"String {op_label} does not correspond to a known operator.") - dim = subsystem_dims[subsystem_label] + dim = subsystem_dims_dict[subsystem_label] out = qi.Operator(op_func(dim), input_dims=[dim], output_dims=[dim]) # sort subsystem labels and dimensions according to subsystem label sorted_subsystem_keys, sorted_subsystem_dims = zip( - *sorted(zip(subsystem_dims.keys(), subsystem_dims.values())) + *sorted(zip(subsystem_dims_dict.keys(), subsystem_dims_dict.values())) ) # get subsystem location in ordered list diff --git a/qiskit_dynamics/backend/backend_string_parser/regex_parser.py b/qiskit_dynamics/backend/backend_string_parser/regex_parser.py index b6b643ff8..624d4385e 100644 --- a/qiskit_dynamics/backend/backend_string_parser/regex_parser.py +++ b/qiskit_dynamics/backend/backend_string_parser/regex_parser.py @@ -29,20 +29,20 @@ def _regex_parser( - operator_str: List[str], subsystem_dims: Dict[int, int], subsystem_list: List[int] + operator_str: List[str], subsystem_dims_dict: Dict[int, int], subsystem_list: List[int] ) -> List[Tuple[np.array, str]]: """Function wrapper for regex parsing object. Args: operator_str: List of strings in accepted format as described in string_model_parser.parse_hamiltonian_dict. - subsystem_dims: Dictionary mapping subsystem labels to dimensions. + subsystem_dims_dict: Dictionary mapping subsystem labels to dimensions. subsystem_list: List of subsystems on which the operators are to be constructed. Returns: List of tuples containing pairs operators and their string coefficients. """ - return _HamiltonianParser(h_str=operator_str, subsystem_dims=subsystem_dims).parse( + return _HamiltonianParser(h_str=operator_str, subsystem_dims_dict=subsystem_dims_dict).parse( subsystem_list ) @@ -66,15 +66,17 @@ class _HamiltonianParser: BrkR=re.compile(r"\)"), ) - def __init__(self, h_str, subsystem_dims): + def __init__(self, h_str, subsystem_dims_dict): """Create new quantum operator generator Parameters: h_str (list): list of Hamiltonian string - subsystem_dims (dict): dimension of subsystems + subsystem_dims_dict (dict): dimension of subsystems """ self.h_str = h_str - self.subsystem_dims = {int(label): int(dim) for label, dim in subsystem_dims.items()} + self.subsystem_dims_dict = { + int(label): int(dim) for label, dim in subsystem_dims_dict.items() + } self.str2qopr = {} def parse(self, qubit_list=None): @@ -194,7 +196,7 @@ def _tokenizer(self, op_str, qubit_list=None): if qubit_list is not None and idx not in qubit_list: return 0, None name = p.group("opr") - opr = _operator_from_string(name, idx, self.subsystem_dims) + opr = _operator_from_string(name, idx, self.subsystem_dims_dict) self.str2qopr[p.group()] = opr elif key == "PrjOpr": _key = key @@ -202,7 +204,7 @@ def _tokenizer(self, op_str, qubit_list=None): if p.group() not in self.str2qopr: idx = int(p.group("idx")) name = "P" - opr = _operator_from_string(name, idx, self.subsystem_dims) + opr = _operator_from_string(name, idx, self.subsystem_dims_dict) self.str2qopr[p.group()] = opr elif key in ["Func", "Ext"]: _name = p.group("name") diff --git a/qiskit_dynamics/backend/backend_utils.py b/qiskit_dynamics/backend/backend_utils.py index 1c6f1a39b..95ccbab16 100644 --- a/qiskit_dynamics/backend/backend_utils.py +++ b/qiskit_dynamics/backend/backend_utils.py @@ -146,7 +146,10 @@ def _get_memory_slot_probabilities( def _sample_probability_dict( - probability_dict: Dict, shots: int, seed: Optional[int] = None + probability_dict: Dict, + shots: int, + normalize_probabilities: bool = True, + seed: Optional[int] = None, ) -> List[str]: """Sample outcomes based on probability dictionary. @@ -154,6 +157,8 @@ def _sample_probability_dict( probability_dict: Dictionary representing probability distribution, with keys being outcomes, values being probabilities. shots: Number of shots. + normalize_probabilities: Whether or not to normalize the probabilities to sum to 1 before + sampling. seed: Seed to use in rng construction. Return: @@ -161,6 +166,11 @@ def _sample_probability_dict( """ rng = np.random.default_rng(seed=seed) alphabet, probs = zip(*probability_dict.items()) + + if normalize_probabilities: + probs = np.array(probs) + probs = probs / probs.sum() + return rng.choice(alphabet, size=shots, replace=True, p=probs) diff --git a/qiskit_dynamics/backend/dynamics_backend.py b/qiskit_dynamics/backend/dynamics_backend.py index 02e4d1603..c22f8c9a4 100644 --- a/qiskit_dynamics/backend/dynamics_backend.py +++ b/qiskit_dynamics/backend/dynamics_backend.py @@ -19,6 +19,7 @@ import datetime import uuid +import warnings from typing import List, Optional, Union, Dict, Tuple import copy @@ -109,18 +110,16 @@ class DynamicsBackend(BackendV2): indicating solver methods and options. Defaults to the empty dictionary ``{}``. * ``subsystem_dims``: Dimensions of subsystems making up the system in ``solver``. Defaults to ``[solver.model.dim]``. - * ``subsystem_labels``: Integer labels for subsystems. Defaults to ``[0, ..., - len(subsystem_dims) - 1]``. - * ``meas_map``: Measurement map. Defaults to ``[[idx] for idx in subsystem_labels]``. + * ``meas_map``: Measurement map. Defaults to ``[[idx] for idx in range(len(subsystem_dims))]``. * ``control_channel_map``: A dictionary mapping control channel labels to indices, to be used for control channel index lookup in the :meth:`DynamicsBackend.control_channel` method. * ``initial_state``: Initial state for simulation, either the string ``"ground_state"``, indicating that the ground state for the system Hamiltonian should be used, or an arbitrary ``Statevector`` or ``DensityMatrix``. Defaults to ``"ground_state"``. * ``normalize_states``: Boolean indicating whether to normalize states before computing outcome - probabilities. Defaults to ``True``. Setting to ``False`` can result in errors if the solution - tolerance results in probabilities with significant numerical deviation from a proper - probability distribution. + probabilities, and normalize probablities before sampling. Defaults to ``True``. Setting to + ``False`` can result in errors if the solution tolerance results in probabilities with + significant numerical deviation from a proper probability distribution. * ``meas_level``: Form of measurement output. Supported values are ``1`` and ``2``. ``1`` returns IQ points and ``2`` returns counts. Defaults to ``meas_level == 2``. * ``meas_return``: Level of measurement data to return. For ``meas_level = 1`` ``"single"`` @@ -187,12 +186,8 @@ def __init__( # Set simulator options self.set_options(solver=solver, **options) - if self.options.subsystem_labels is None: - labels = list(range(len(self.options.subsystem_dims))) - self.set_options(subsystem_labels=labels) - if self.options.meas_map is None: - meas_map = [[idx] for idx in self.options.subsystem_labels] + meas_map = [[idx] for idx in range(len(self.options.subsystem_dims))] self.set_options(meas_map=meas_map) # self._target = target or Target() doesn't work as bool(target) can be False @@ -204,7 +199,7 @@ def __init__( # add default simulator measure instructions measure_properties = {} instruction_schedule_map = target.instruction_schedule_map() - for qubit in self.options.subsystem_labels: + for qubit in range(len(self.options.subsystem_dims)): if not instruction_schedule_map.has(instruction="measure", qubits=qubit): with pulse.build() as meas_sched: pulse.acquire( @@ -217,6 +212,7 @@ def __init__( target.add_instruction(Measure(), measure_properties) target.dt = solver._dt + target.num_qubits = len(self.options.subsystem_dims) self._target = target @@ -226,7 +222,6 @@ def _default_options(self): solver=None, solver_options={}, subsystem_dims=None, - subsystem_labels=None, meas_map=None, control_channel_map=None, normalize_states=True, @@ -383,7 +378,7 @@ def run( measurement_subsystems_list, memory_slot_indices_list, ) = _get_acquire_instruction_timings( - schedules, backend.options.subsystem_labels, backend.options.solver._dt + schedules, backend.options.subsystem_dims, backend.options.solver._dt ) # Build and submit job @@ -484,12 +479,10 @@ def _get_qubit_channel( self, qubit: int, ChannelClass: pulse.channels.Channel, method_name: str ): """Construct a channel instance for a given qubit.""" - if qubit in self.options.subsystem_labels: + if qubit < len(self.options.subsystem_dims): return ChannelClass(qubit) - raise QiskitError( - f"{method_name} requested for qubit {qubit} which is not in subsystem_list." - ) + raise QiskitError(f"{method_name} requested for qubit {qubit}, which is out of bounds.") def drive_channel(self, qubit: int) -> pulse.DriveChannel: """Return the drive channel for a given qubit.""" @@ -544,7 +537,7 @@ def defaults(self) -> PulseDefaults: @classmethod def from_backend( cls, - backend: Union[BackendV1, BackendV2], + backend: BackendV1, subsystem_list: Optional[List[int]] = None, rotating_frame: Optional[Union[Array, RotatingFrame, str]] = "auto", evaluation_mode: str = "dense", @@ -616,6 +609,11 @@ def from_backend( Args: backend: The ``Backend`` instance to build the :class:`.DynamicsBackend` from. + Note that while the type hint indicates that `backend` should be a + :class:`~qiskit.providers.backend.BackendV1` instance, this method also works for + :class:`~qiskit.providers.backend.BackendV2` instances that have been set up with + sufficiently populated ``configuration`` and ``defaults`` for backwards + compatibility. subsystem_list: The list of qubits in the backend to include in the model. rotating_frame: Rotating frame argument for the internal :class:`.Solver`. Defaults to ``"auto"``, allowing this method to pick a rotating frame. @@ -671,9 +669,9 @@ def from_backend( static_hamiltonian, hamiltonian_operators, hamiltonian_channels, - subsystem_dims, + subsystem_dims_dict, ) = parse_backend_hamiltonian_dict(backend_config.hamiltonian, subsystem_list) - subsystem_dims = [subsystem_dims[idx] for idx in subsystem_list] + subsystem_dims = [subsystem_dims_dict.get(idx, 1) for idx in range(backend_num_qubits)] # construct model frequencies dictionary from backend channel_freqs = _get_backend_channel_freqs( @@ -736,7 +734,6 @@ def from_backend( return cls( solver=solver, target=Target(dt=dt), - subsystem_labels=subsystem_list, subsystem_dims=subsystem_dims, **options, ) @@ -804,11 +801,6 @@ def default_experiment_result_function( if backend.options.normalize_states: yf = yf / np.diag(yf.data).sum() - # compute probabilities for measurement slot values - measurement_subsystems = [ - backend.options.subsystem_labels.index(x) for x in measurement_subsystems - ] - if backend.options.meas_level == MeasLevel.CLASSIFIED: memory_slot_probabilities = _get_memory_slot_probabilities( probability_dict=yf.probabilities_dict(qargs=measurement_subsystems), @@ -819,7 +811,10 @@ def default_experiment_result_function( # sample memory_samples = _sample_probability_dict( - memory_slot_probabilities, shots=backend.options.shots, seed=seed + memory_slot_probabilities, + shots=backend.options.shots, + normalize_probabilities=backend.options.normalize_states, + seed=seed, ) counts = _get_counts_from_samples(memory_samples) @@ -889,17 +884,18 @@ def _validate_run_input(run_input, accept_list=True): def _get_acquire_instruction_timings( - schedules: List[Schedule], valid_subsystem_labels: List[int], dt: float + schedules: List[Schedule], subsystem_dims: List[int], dt: float ) -> Tuple[List[List[float]], List[List[int]], List[List[int]]]: """Get the required data from the acquire commands in each schedule. Additionally validates that each schedule has Acquire instructions occurring at one time, at - least one memory slot is being listed, and all measured subsystems exist in - ``valid_subsystem_labels``. + least one memory slot is being listed, and all measured subsystem indices are less than + ``len(subsystem_dims)``. Additionally, a warning is raised if a 'trivial' subsystem is measured, + i.e. one with dimension 1. Args: schedules: A list of schedules. - valid_subsystem_labels: Valid acquire channel indices. + subsystem_dims: List of subsystem dimensions. dt: The sample size. Returns: A tuple of lists containing, for each schedule: the list of integration intervals required @@ -938,14 +934,16 @@ def _get_acquire_instruction_timings( measurement_subsystems = [] memory_slot_indices = [] for inst in schedule_acquires: - if inst.channel.index in valid_subsystem_labels: - measurement_subsystems.append(inst.channel.index) - else: + if not inst.channel.index < len(subsystem_dims): raise QiskitError( - f"Attempted to measure subsystem {inst.channel.index}, but it is not in " - "subsystem_list." + f"Attempted to measure out of bounds subsystem {inst.channel.index}." ) + if subsystem_dims[inst.channel.index] == 1: + warnings.warn(f"Measuring trivial subsystem {inst.channel.index} with dimension 1.") + + measurement_subsystems.append(inst.channel.index) + memory_slot_indices.append(inst.mem_slot.index) measurement_subsystems_list.append(measurement_subsystems) @@ -972,7 +970,7 @@ def _to_schedule_list( elif isinstance(sched, Schedule): schedules.append(sched) elif isinstance(sched, QuantumCircuit): - num_memslots[-1] = sched.cregs[0].size + num_memslots[-1] = sum(creg.size for creg in sched.cregs) schedules.append(build_schedule(sched, backend, dt=backend.options.solver._dt)) else: raise QiskitError(f"Type {type(sched)} cannot be converted to Schedule.") diff --git a/qiskit_dynamics/dispatch/backends/jax.py b/qiskit_dynamics/dispatch/backends/jax.py index 24a3eba09..ed104b9ef 100644 --- a/qiskit_dynamics/dispatch/backends/jax.py +++ b/qiskit_dynamics/dispatch/backends/jax.py @@ -17,28 +17,28 @@ try: import jax - from jax.interpreters.xla import DeviceArray + from jax import Array from jax.core import Tracer - from jax.interpreters.ad import JVPTracer - from jax.interpreters.partial_eval import JaxprTracer - JAX_TYPES = (DeviceArray, Tracer, JaxprTracer, JVPTracer) + # warning based on JAX version + from packaging import version + import warnings - try: - # This class was introduced in 0.4.0 - from jax import Array + if version.parse(jax.__version__) >= version.parse("0.4.4"): + import os - JAX_TYPES += (Array,) - except ImportError: - pass - - try: - # This class is not in older versions of Jax - from jax.interpreters.partial_eval import DynamicJaxprTracer + if ( + version.parse(jax.__version__) > version.parse("0.4.6") + or os.environ.get("JAX_JIT_PJIT_API_MERGE", None) != "0" + ): + warnings.warn( + "The functionality in the perturbation module of Qiskit Dynamics requires a JAX " + "version <= 0.4.6, due to a bug in JAX versions > 0.4.6. For versions 0.4.4, " + "0.4.5, and 0.4.6, using the perturbation module functionality requires setting " + "os.environ['JAX_JIT_PJIT_API_MERGE'] = '0' before importing JAX or Dynamics." + ) - JAX_TYPES += (DynamicJaxprTracer,) - except ImportError: - pass + JAX_TYPES = (Array, Tracer) from ..dispatch import Dispatch import numpy as np @@ -53,7 +53,7 @@ def _jax_asarray(array, dtype=None, order=None): """Wrapper for jax.numpy.asarray""" if ( - isinstance(array, DeviceArray) + isinstance(array, JAX_TYPES) and order is None and (dtype is None or dtype == array.dtype) ): diff --git a/qiskit_dynamics/pulse/pulse_to_signals.py b/qiskit_dynamics/pulse/pulse_to_signals.py index 1377d5941..9ac451295 100644 --- a/qiskit_dynamics/pulse/pulse_to_signals.py +++ b/qiskit_dynamics/pulse/pulse_to_signals.py @@ -16,6 +16,7 @@ from typing import Callable, Dict, List, Optional import functools +from warnings import warn import numpy as np import sympy as sym @@ -40,6 +41,12 @@ from qiskit_dynamics.array import Array from qiskit_dynamics.signals import DiscreteSignal +try: + import jax + import jax.numpy as jnp +except ImportError: + pass + class InstructionToSignals: """Converts pulse instructions to signals to be used in models. @@ -133,6 +140,9 @@ def get_signals(self, schedule: Schedule) -> List[DiscreteSignal]: Similarly to ``ShiftFrequency``, the shift rule for :math:`\phi_a` is defined to maintain carrier wave continuity. + If, at any sample point :math:`k`, :math:`\Delta\nu(k)` is larger than the Nyquist sampling + rate given by ``dt``, a warning will be raised. + Args: schedule: The schedule to represent in terms of signals. Instances of :class:`~qiskit.pulse.ScheduleBlock` must first be converted to @@ -188,14 +198,15 @@ def get_signals(self, schedule: Schedule) -> List[DiscreteSignal]: if isinstance(inst, ShiftPhase): phases[chan] += inst.phase + if isinstance(inst, SetPhase): + phases[chan] = inst.phase + if isinstance(inst, ShiftFrequency): frequency_shifts[chan] = frequency_shifts[chan] + Array(inst.frequency) phase_accumulations[chan] = ( phase_accumulations[chan] - inst.frequency * start_sample * self._dt ) - - if isinstance(inst, SetPhase): - phases[chan] = inst.phase + _nyquist_warn(frequency_shifts[chan], self._dt, chan) if isinstance(inst, SetFrequency): phase_accumulations[chan] = phase_accumulations[chan] - ( @@ -204,6 +215,7 @@ def get_signals(self, schedule: Schedule) -> List[DiscreteSignal]: * self._dt ) frequency_shifts[chan] = inst.frequency - signals[chan].carrier_freq + _nyquist_warn(frequency_shifts[chan], self._dt, chan) # ensure all signals have the same number of samples max_duration = 0 @@ -367,3 +379,17 @@ def _lru_cache_expr(expr: sym.Expr, backend) -> Callable: continue params.append(param) return sym.lambdify(params, expr, modules=backend) + + +def _nyquist_warn(frequency_shift: Array, dt: float, channel: str): + """Raise a warning if the frequency shift is above the Nyquist frequency given by ``dt``.""" + + if ( + Array(frequency_shift).backend != "jax" or not isinstance(jnp.array(0), jax.core.Tracer) + ) and np.abs(frequency_shift) > 0.5 / dt: + warn( + "Due to SetFrequency and ShiftFrequency instructions, the digital carrier frequency " + f"of channel {channel} is larger than the Nyquist frequency of the envelope sample " + "size dt. As shifts of the frequency from the analog frequency are handled digitally, " + "this will result in aliasing effects." + ) diff --git a/qiskit_dynamics/signals/signals.py b/qiskit_dynamics/signals/signals.py index efd118993..493702227 100644 --- a/qiskit_dynamics/signals/signals.py +++ b/qiskit_dynamics/signals/signals.py @@ -96,8 +96,12 @@ def __init__( if isinstance(envelope, Array): # if envelope is constant and the carrier is zero, this is a constant signal - if carrier_freq == 0.0: - self._is_constant = True + try: + # try block is for catching JAX tracer errors + if carrier_freq == 0.0: + self._is_constant = True + except Exception: # pylint: disable=broad-except + pass if envelope.backend == "jax": self._envelope = lambda t: envelope * jnp.ones_like(Array(t).data) diff --git a/releasenotes/notes/diffrax-bound-0bd80c01b7f4b48f.yaml b/releasenotes/notes/0.4/diffrax-bound-0bd80c01b7f4b48f.yaml similarity index 100% rename from releasenotes/notes/diffrax-bound-0bd80c01b7f4b48f.yaml rename to releasenotes/notes/0.4/diffrax-bound-0bd80c01b7f4b48f.yaml diff --git a/releasenotes/notes/move-repo-c0b48ba3b0ced8db.yaml b/releasenotes/notes/0.4/move-repo-c0b48ba3b0ced8db.yaml similarity index 100% rename from releasenotes/notes/move-repo-c0b48ba3b0ced8db.yaml rename to releasenotes/notes/0.4/move-repo-c0b48ba3b0ced8db.yaml diff --git a/releasenotes/notes/multiset-order-bug-fix-1f1603ee1e230cba.yaml b/releasenotes/notes/0.4/multiset-order-bug-fix-1f1603ee1e230cba.yaml similarity index 100% rename from releasenotes/notes/multiset-order-bug-fix-1f1603ee1e230cba.yaml rename to releasenotes/notes/0.4/multiset-order-bug-fix-1f1603ee1e230cba.yaml diff --git a/releasenotes/notes/patch-0.4.1-d339aa8669341341.yaml b/releasenotes/notes/0.4/patch-0.4.1-d339aa8669341341.yaml similarity index 100% rename from releasenotes/notes/patch-0.4.1-d339aa8669341341.yaml rename to releasenotes/notes/0.4/patch-0.4.1-d339aa8669341341.yaml diff --git a/releasenotes/notes/carrier-freq-0-19ad4362c874944f.yaml b/releasenotes/notes/carrier-freq-0-19ad4362c874944f.yaml new file mode 100644 index 000000000..318fc9afe --- /dev/null +++ b/releasenotes/notes/carrier-freq-0-19ad4362c874944f.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + In the case that ``envelope`` is a constant, the :meth:`.Signal.__init__` method has been + updated to not attempt to evaluate ``carrier_freq == 0.0`` if ``carrier_freq`` is a JAX tracer. + In this case, it is not possible to determine if the :class:`.Signal` instance is constant. This + resolves an error that was being raised during JAX tracing if ``carrier_freq`` is abstract. \ No newline at end of file diff --git a/releasenotes/notes/classical-registers-9bb117398a4d21d5.yaml b/releasenotes/notes/classical-registers-9bb117398a4d21d5.yaml new file mode 100644 index 000000000..2b96e4db9 --- /dev/null +++ b/releasenotes/notes/classical-registers-9bb117398a4d21d5.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + Fixes bug in :meth:`.DynamicsBackend.run` that caused miscounting of the number of classical + registers in a :class:`~qiskit.circuit.QuantumCircuit` (issue #251). \ No newline at end of file diff --git a/releasenotes/notes/normalize-probabilities-d729245bb3fe5f10.yaml b/releasenotes/notes/normalize-probabilities-d729245bb3fe5f10.yaml new file mode 100644 index 000000000..568d77eaf --- /dev/null +++ b/releasenotes/notes/normalize-probabilities-d729245bb3fe5f10.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + ``DynamicsBackend.options.normalize_states`` now also controls whether or not the probability + distribution over outcomes is normalized before sampling outcomes. diff --git a/releasenotes/notes/patch-0.4.2-6a7c7bf380e54187.yaml b/releasenotes/notes/patch-0.4.2-6a7c7bf380e54187.yaml new file mode 100644 index 000000000..23d5b804d --- /dev/null +++ b/releasenotes/notes/patch-0.4.2-6a7c7bf380e54187.yaml @@ -0,0 +1,16 @@ +--- +prelude: > + Qiskit Dynamics 0.4.2 is an incremental release with minor bug fixes and additional warnings to + help guide users through issues. +other: + - | + For users that have JAX installed, a warning has been added upon import of Qiskit Dynamics to + notify the user of issues with certain versions: JAX versions newer than ``0.4.6`` break the + ``perturbation`` module, and to use ``perturbation`` module with versions ``0.4.4``, ``0.4.5``, + or ``0.4.6``, it is necessary to set ``os.environ['JAX_JIT_PJIT_API_MERGE'] = '0'`` before + importing JAX or Dynamics. + - | + A warning has been added to :class:`.InstructionToSignals` class when converting pulse schedules + to signals to notify the user if the usage of ``SetFrequency`` or ``ShiftFrequency`` commands + result in a digital carrier frequency larger than the Nyquist frequency of the envelope sample + size ``dt``. \ No newline at end of file diff --git a/releasenotes/notes/subsystem_labels-removal-9fcc71c310eff220.yaml b/releasenotes/notes/subsystem_labels-removal-9fcc71c310eff220.yaml new file mode 100644 index 000000000..a444fbb94 --- /dev/null +++ b/releasenotes/notes/subsystem_labels-removal-9fcc71c310eff220.yaml @@ -0,0 +1,15 @@ +--- +upgrade: + - | + The ``subsystem_labels`` option has been removed from the :class:`.DynamicsBackend`. This + removal impacts some technical aspects of the backend returned by + :meth:`.DynamicsBackend.from_backend` when the ``subsystem_list`` argument is used. Using the + ``subsystem_list`` argument with :meth:`.DynamicsBackend.from_backend` restricts the internally + constructed model to the qubits in ``subsystem_list``. When doing so previously, the option + ``subsystem_labels`` would be set to ``subsystem_labels``, and ``subsystem_dims`` would record + only the dimensions for the systems in ``subsystem_labels``. To account for the fact that + ``subsystem_labels`` no longer exists, :meth:`.DynamicsBackend.from_backend` now constructs + ``subsystem_dims`` to list a dimension for all of the qubits in the original backend, however + now the dimensions of the removed systems are given as 1 (i.e. they are treated as trivial + quantum systems with a single state). This change is made only for technical bookkeping + purposes, and has no impact on the core simulation behaviour. \ No newline at end of file diff --git a/requirements-dev.txt b/requirements-dev.txt index a1aa26bf7..cdfea3f16 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -5,6 +5,9 @@ black~=22.0 qiskit-sphinx-theme~=1.12.0 sphinx-autodoc-typehints jupyter-sphinx +# The following line is needed until +# https://github.com/jupyter/jupyter-sphinx/pull/226 is resolved. +ipykernel>=4.5.1 pygments>=2.4 reno>=3.4.0 nbsphinx diff --git a/setup.py b/setup.py index 54064a497..d0b5d810a 100644 --- a/setup.py +++ b/setup.py @@ -21,10 +21,11 @@ "matplotlib>=3.0", "qiskit-terra>=0.23.0", "multiset>=3.0.1", + "sympy>=1.12" ] -jax_extras = ['jax>=0.2.26, <= 0.4.6', - 'jaxlib>=0.1.75, <= 0.4.6'] +jax_extras = ['jax>=0.4.0, <= 0.4.6', + 'jaxlib>=0.4.0, <= 0.4.6'] PACKAGES = setuptools.find_packages(exclude=['test*']) diff --git a/test/dynamics/backend/backend_string_parser/test_hamiltonian_string_parser.py b/test/dynamics/backend/backend_string_parser/test_hamiltonian_string_parser.py index 5c8943987..1bf26dd1e 100644 --- a/test/dynamics/backend/backend_string_parser/test_hamiltonian_string_parser.py +++ b/test/dynamics/backend/backend_string_parser/test_hamiltonian_string_parser.py @@ -123,11 +123,13 @@ def test_only_static_terms(self): """Test a basic system.""" ham_dict = {"h_str": ["v*np.pi*Z0"], "qub": {"0": 2}, "vars": {"v": 2.1}} - static_ham, ham_ops, channels, subsystem_dims = parse_backend_hamiltonian_dict(ham_dict) + static_ham, ham_ops, channels, subsystem_dims_dict = parse_backend_hamiltonian_dict( + ham_dict + ) self.assertAllClose(static_ham, 2.1 * np.pi * self.Z) self.assertTrue(not ham_ops) self.assertTrue(not channels) - self.assertTrue(subsystem_dims == {0: 2}) + self.assertTrue(subsystem_dims_dict == {0: 2}) def test_simple_single_q_system(self): """Test a basic system.""" @@ -137,12 +139,14 @@ def test_simple_single_q_system(self): "vars": {"v": 2.1}, } - static_ham, ham_ops, channels, subsystem_dims = parse_backend_hamiltonian_dict(ham_dict) + static_ham, ham_ops, channels, subsystem_dims_dict = parse_backend_hamiltonian_dict( + ham_dict + ) self.assertAllClose(static_ham, 2.1 * np.pi * self.Z) self.assertAllClose(to_array(ham_ops), [0.02 * np.pi * self.X]) self.assertTrue(channels == ["d0"]) - self.assertTrue(subsystem_dims == {0: 2}) + self.assertTrue(subsystem_dims_dict == {0: 2}) def test_simple_single_q_system_repeat_entries(self): """Test merging of terms with same channel or no channel.""" @@ -152,12 +156,14 @@ def test_simple_single_q_system_repeat_entries(self): "vars": {"v": 2.1}, } - static_ham, ham_ops, channels, subsystem_dims = parse_backend_hamiltonian_dict(ham_dict) + static_ham, ham_ops, channels, subsystem_dims_dict = parse_backend_hamiltonian_dict( + ham_dict + ) self.assertAllClose(static_ham, 2 * 2.1 * np.pi * self.Z) self.assertAllClose(to_array(ham_ops), [2 * 0.02 * np.pi * self.X]) self.assertTrue(channels == ["d0"]) - self.assertTrue(subsystem_dims == {0: 2}) + self.assertTrue(subsystem_dims_dict == {0: 2}) def test_simple_single_q_system_repeat_entries_different_case(self): """Test merging of terms with same channel or no channel, @@ -169,12 +175,14 @@ def test_simple_single_q_system_repeat_entries_different_case(self): "vars": {"v": 2.1}, } - static_ham, ham_ops, channels, subsystem_dims = parse_backend_hamiltonian_dict(ham_dict) + static_ham, ham_ops, channels, subsystem_dims_dict = parse_backend_hamiltonian_dict( + ham_dict + ) self.assertAllClose(static_ham, 2 * 2.1 * np.pi * self.Z) self.assertAllClose(to_array(ham_ops), [2 * 0.02 * np.pi * self.X]) self.assertTrue(channels == ["d0"]) - self.assertTrue(subsystem_dims == {0: 2}) + self.assertTrue(subsystem_dims_dict == {0: 2}) def test_simple_two_q_system(self): """Test a two qubit system.""" @@ -191,7 +199,9 @@ def test_simple_two_q_system(self): "vars": {"v0": 2.1, "v1": 2.0, "j": 0.02}, } - static_ham, ham_ops, channels, subsystem_dims = parse_backend_hamiltonian_dict(ham_dict) + static_ham, ham_ops, channels, subsystem_dims_dict = parse_backend_hamiltonian_dict( + ham_dict + ) ident = np.eye(2) self.assertAllClose( @@ -205,7 +215,7 @@ def test_simple_two_q_system(self): [0.02 * np.pi * np.kron(ident, self.X), 0.03 * np.pi * np.kron(self.X, ident)], ) self.assertTrue(channels == ["d0", "d1"]) - self.assertTrue(subsystem_dims == {0: 2, 1: 2}) + self.assertTrue(subsystem_dims_dict == {0: 2, 1: 2}) def test_simple_two_q_system_measurement_channel(self): """Test a two qubit system with a measurement-labelled channel.""" @@ -222,7 +232,9 @@ def test_simple_two_q_system_measurement_channel(self): "vars": {"v0": 2.1, "v1": 2.0, "j": 0.02}, } - static_ham, ham_ops, channels, subsystem_dims = parse_backend_hamiltonian_dict(ham_dict) + static_ham, ham_ops, channels, subsystem_dims_dict = parse_backend_hamiltonian_dict( + ham_dict + ) ident = np.eye(2) self.assertAllClose( @@ -236,7 +248,7 @@ def test_simple_two_q_system_measurement_channel(self): [0.02 * np.pi * np.kron(ident, self.X), 0.03 * np.pi * np.kron(self.X, ident)], ) self.assertTrue(channels == ["d0", "m1"]) - self.assertTrue(subsystem_dims == {0: 2, 1: 2}) + self.assertTrue(subsystem_dims_dict == {0: 2, 1: 2}) def test_single_oscillator_system(self): """Test single oscillator system.""" @@ -247,12 +259,14 @@ def test_single_oscillator_system(self): "vars": {"v": 2.1, "alpha": -0.33, "r": 0.02}, } - static_ham, ham_ops, channels, subsystem_dims = parse_backend_hamiltonian_dict(ham_dict) + static_ham, ham_ops, channels, subsystem_dims_dict = parse_backend_hamiltonian_dict( + ham_dict + ) self.assertAllClose(static_ham, 2.1 * np.pi * self.N - 0.33 * np.pi * self.N * self.N) self.assertAllClose(to_array(ham_ops), [0.02 * np.pi * (self.a + self.adag)]) self.assertTrue(channels == ["d0"]) - self.assertTrue(subsystem_dims == {0: 4}) + self.assertTrue(subsystem_dims_dict == {0: 4}) def test_two_oscillator_system(self): """Test a two qubit system.""" @@ -271,7 +285,9 @@ def test_two_oscillator_system(self): "vars": {"v0": 2.1, "v1": 2.0, "alpha0": -0.33, "alpha1": -0.33, "j": 0.02}, } - static_ham, ham_ops, channels, subsystem_dims = parse_backend_hamiltonian_dict(ham_dict) + static_ham, ham_ops, channels, subsystem_dims_dict = parse_backend_hamiltonian_dict( + ham_dict + ) ident = np.eye(4) @@ -291,7 +307,7 @@ def test_two_oscillator_system(self): ], ) self.assertTrue(channels == ["d0", "d1"]) - self.assertTrue(subsystem_dims == {0: 4, 1: 4}) + self.assertTrue(subsystem_dims_dict == {0: 4, 1: 4}) def test_single_q_high_dim(self): """Test single q system but higher dim.""" @@ -301,12 +317,14 @@ def test_single_q_high_dim(self): "vars": {"v": 2.1}, } - static_ham, ham_ops, channels, subsystem_dims = parse_backend_hamiltonian_dict(ham_dict) + static_ham, ham_ops, channels, subsystem_dims_dict = parse_backend_hamiltonian_dict( + ham_dict + ) self.assertAllClose(static_ham, 2.1 * np.pi * (np.eye(4) - 2 * self.N)) self.assertAllClose(to_array(ham_ops), [0.02 * np.pi * (self.a + self.adag)]) self.assertTrue(channels == ["d0"]) - self.assertTrue(subsystem_dims == {0: 4}) + self.assertTrue(subsystem_dims_dict == {0: 4}) def test_dagger(self): """Test correct parsing of dagger.""" @@ -397,13 +415,13 @@ def test_5q_hamiltonian_reduced(self): ) channels_expected = ["d0", "d1", "u0", "u1", "u2"] - static_ham, ham_ops, channels, subsystem_dims = parse_backend_hamiltonian_dict( + static_ham, ham_ops, channels, subsystem_dims_dict = parse_backend_hamiltonian_dict( ham_dict, subsystem_list=[0, 1] ) self.assertAllClose(static_ham, static_ham_expected) self.assertAllClose(ham_ops, ham_ops_expected) self.assertTrue(channels == channels_expected) - self.assertTrue(subsystem_dims == {0: 4, 1: 4}) + self.assertTrue(subsystem_dims_dict == {0: 4, 1: 4}) # test case for subsystems [3, 4] @@ -427,10 +445,10 @@ def test_5q_hamiltonian_reduced(self): ) channels_expected = ["d3", "d4", "u5", "u6", "u7"] - static_ham, ham_ops, channels, subsystem_dims = parse_backend_hamiltonian_dict( + static_ham, ham_ops, channels, subsystem_dims_dict = parse_backend_hamiltonian_dict( ham_dict, subsystem_list=[3, 4] ) self.assertAllClose(static_ham, static_ham_expected) self.assertAllClose(ham_ops, ham_ops_expected) self.assertTrue(channels == channels_expected) - self.assertTrue(subsystem_dims == {3: 4, 4: 4}) + self.assertTrue(subsystem_dims_dict == {3: 4, 4: 4}) diff --git a/test/dynamics/backend/test_dynamics_backend.py b/test/dynamics/backend/test_dynamics_backend.py index 6dff0211e..bb679c4de 100644 --- a/test/dynamics/backend/test_dynamics_backend.py +++ b/test/dynamics/backend/test_dynamics_backend.py @@ -21,7 +21,7 @@ from scipy.integrate._ivp.ivp import OdeResult from scipy.sparse import csr_matrix -from qiskit import QiskitError, pulse, QuantumCircuit +from qiskit import QiskitError, pulse, QuantumCircuit, QuantumRegister, ClassicalRegister from qiskit.circuit.library import XGate, Measure from qiskit.transpiler import Target, InstructionProperties from qiskit.quantum_info import Statevector, DensityMatrix @@ -127,7 +127,18 @@ def test_measure_nonexistant_subsystem(self): pulse.play(pulse.Waveform([0.5, 0.5, 0.5]), pulse.DriveChannel(0)) pulse.acquire(duration=1, qubit_or_channel=1, register=pulse.MemorySlot(0)) - with self.assertRaisesRegex(QiskitError, "Attempted to measure subsystem 1"): + with self.assertRaisesRegex(QiskitError, "Attempted to measure out of bounds subsystem 1."): + self.simple_backend.run(schedule) + + def test_measure_trivial_subsystem(self): + """Attempt to measure subsystem with dimension 1.""" + + with pulse.build() as schedule: + pulse.play(pulse.Waveform([0.5, 0.5, 0.5]), pulse.DriveChannel(0)) + pulse.acquire(duration=1, qubit_or_channel=1, register=pulse.MemorySlot(0)) + + self.simple_backend.set_options(subsystem_dims=[2, 1]) + with self.assertWarnsRegex(Warning, "Measuring trivial subsystem 1"): self.simple_backend.run(schedule) def test_invalid_initial_state(self): @@ -328,19 +339,6 @@ def test_pi_half_pulse_density_matrix(self): counts = self.iq_to_counts(result.get_memory()) self.assertDictEqual(counts, {"0": 510, "1": 514}) - def test_pi_half_pulse_relabelled(self): - """Test simulation of a pi/2 pulse with qubit relabelled.""" - - self.simple_backend.set_options(subsystem_labels=[1]) - - with pulse.build() as schedule: - with pulse.align_right(): - pulse.play(pulse.Waveform([1.0] * 50), pulse.DriveChannel(0)) - pulse.acquire(duration=1, qubit_or_channel=1, register=pulse.MemorySlot(1)) - - result = self.simple_backend.run(schedule, seed_simulator=398472).result() - self.assertDictEqual(result.get_counts(), {"00": 513, "10": 511}) - def test_circuit_with_pulse_defs(self): """Test simulating a circuit with pulse definitions.""" @@ -357,6 +355,22 @@ def test_circuit_with_pulse_defs(self): self.assertDictEqual(result.get_counts(), {"1": 1024}) self.assertTrue(result.get_memory() == ["1"] * 1024) + def test_circuit_with_multiple_classical_registers(self): + """Test simulating a circuit with pulse definitions and multiple classical registers.""" + + circ = QuantumCircuit(QuantumRegister(1), ClassicalRegister(1), ClassicalRegister(1)) + circ.x(0) + circ.measure([0], [1]) + + with pulse.build() as x_sched0: + pulse.play(pulse.Waveform([0.0]), pulse.DriveChannel(0)) + + circ.add_calibration("x", [0], x_sched0) + + result = self.simple_backend.run(circ, seed_simulator=1234567).result() + self.assertDictEqual(result.get_counts(), {"00": 1024}) + self.assertTrue(result.get_memory() == ["00"] * 1024) + def test_circuit_with_target_pulse_instructions(self): """Test running a circuit on a simulator with defined instructions.""" @@ -1128,7 +1142,7 @@ def test_correct_t_span(self): measurement_subsystems_list, memory_slot_indices_list, ) = _get_acquire_instruction_timings( - schedules=[schedule0, schedule1], valid_subsystem_labels=[0, 1], dt=dt + schedules=[schedule0, schedule1], subsystem_dims=[2, 2], dt=dt ) self.assertAllClose(np.array(t_span), np.array([[0.0, 104 * dt], [0.0, 100 * dt]])) diff --git a/test/dynamics/pulse/test_pulse_to_signals.py b/test/dynamics/pulse/test_pulse_to_signals.py index 8652ac60a..47822d68b 100644 --- a/test/dynamics/pulse/test_pulse_to_signals.py +++ b/test/dynamics/pulse/test_pulse_to_signals.py @@ -39,6 +39,19 @@ def setUp(self): # Typical length of samples in units of dt in IBM real backends is 1/4.5. self._dt = 1 / 4.5 + def test_nyquist_warning(self): + """Test Nyquist warning is raised.""" + converter = InstructionToSignals(dt=1, carriers={"d0": 0.0}) + + sched = Schedule(name="Schedule") + sched += pulse.SetFrequency(1.0, pulse.DriveChannel(0)) + sched += pulse.Play( + pulse.Drag(duration=20, amp=0.5, sigma=4, beta=0.5), pulse.DriveChannel(0) + ) + + with self.assertWarnsRegex(Warning, "Due to SetFrequency and ShiftFrequency"): + converter.get_signals(sched) + def test_pulse_to_signals(self): """Generic test.""" diff --git a/test/dynamics/signals/test_signals.py b/test/dynamics/signals/test_signals.py index e8991d34c..cd1b107a2 100644 --- a/test/dynamics/signals/test_signals.py +++ b/test/dynamics/signals/test_signals.py @@ -933,6 +933,35 @@ def eval_const(a): jit_grad_eval = jit(grad(eval_const)) self.assertAllClose(jit_grad_eval(3.0), 1.0) + # validate that is_constant is being properly set + def eval_const_conditional(a): + a = Array(a) + sig = Signal(a) + + if sig.is_constant: + return 5.0 + else: + return 3.0 + + jit_eval = jit(eval_const_conditional) + self.assertAllClose(jit_eval(1.0), 5.0) + + def test_jit_grad_carrier_freq_construct(self): + """Test jit/gradding through a function that constructs a signal and takes carrier frequency + as an argument. + """ + + def eval_sig(a, v, t): + a = Array(a) + v = Array(v) + return Array(Signal(a, v)(t)).data + + jit_eval = jit(eval_sig) + self.assertAllClose(jit_eval(1.0, 1.0, 1.0), 1.0) + + jit_grad_eval = jit(grad(eval_sig)) + self.assertAllClose(jit_grad_eval(1.0, 1.0, 1.0), 1.0) + def test_signal_list_jit_eval(self): """Test jit-compilation of SignalList evaluation.""" call_jit = jit(lambda t: Array(self.signal_list(t)).data)