From 807edf92d7f5d6f34715fff9d21614d77cd096d3 Mon Sep 17 00:00:00 2001 From: Daniel Puzzuoli Date: Fri, 9 Jun 2023 14:50:11 -0700 Subject: [PATCH] Preparing 0.4.1 (#217) * Temporary upper bound on JAX version <=0.4.6 and update to DynamicsBackend tutorial (#210) * Add links of API reference to tutorials and userguides (#212) * Fix multiset ordering bug in perturbation module (#211) Co-authored-by: Ian Hincks * Bump Sphinx Theme to 1.11 (#215) * Bump Sphinx Theme to 1.11 * Also activate jquery * Bug fix: Measurement properties automatic padding for DynamicsBackend initialization (#209) Co-authored-by: Daniel Puzzuoli * bounding ipython version for compatibility with python 3.8 (#216) * Update deploy_documentation.sh to deploy in ecosystem (#219) * Bound Diffrax version (#226) * Bounding diffrax and equinox versions. The latest versions require the latest version of JAX, but due to an unresolved bug in JAX, Dynamics is only compatible with jax<=0.4.6. This commit also adds a release note stating exactly what versions of these packages will work with the latest version of dynamics. * Upgrade to qiskit_sphinx_theme 1.12 (#224) * Update links to repo and documentation (#227) * updating minor comments in pulse sim tutorial to remove confusion (#228) * adding max step size argument to dynamics_backend tutorial, with explanation (#229) * fixing typo --------- Co-authored-by: Kento Ueda <38037695+to24toro@users.noreply.github.com> Co-authored-by: Ian Hincks Co-authored-by: Eric Arellano <14852634+Eric-Arellano@users.noreply.github.com> Co-authored-by: Arthur Strauss <56998701+arthurostrauss@users.noreply.github.com> Co-authored-by: Luciano Bello --- .github/workflows/docs.yml | 1 + CITATION.bib | 2 +- CONTRIBUTING.md | 2 +- README.md | 67 ++++++------ docs/conf.py | 9 +- docs/index.rst | 2 +- .../Lindblad_dynamics_simulation.rst | 6 +- docs/tutorials/Rabi_oscillations.rst | 15 +-- docs/tutorials/dynamics_backend.rst | 17 ++- docs/tutorials/qiskit_pulse.rst | 33 +++--- .../how_to_configure_simulations.rst | 12 +-- docs/userguide/how_to_use_jax.rst | 18 ++-- docs/userguide/perturbative_solvers.rst | 2 +- pyproject.toml | 3 +- qiskit_dynamics/VERSION.txt | 2 +- qiskit_dynamics/backend/dynamics_backend.py | 34 +++++- qiskit_dynamics/perturbation/dyson_magnus.py | 23 ++-- .../perturbation/multiset_utils.py | 52 +++++++-- .../notes/diffrax-bound-0bd80c01b7f4b48f.yaml | 5 + ...ent_property_bug_fix-12461088823a943c.yaml | 11 ++ .../notes/move-repo-c0b48ba3b0ced8db.yaml | 9 ++ ...ltiset-order-bug-fix-1f1603ee1e230cba.yaml | 6 ++ .../notes/patch-0.4.1-d339aa8669341341.yaml | 4 + requirements-dev.txt | 5 +- setup.py | 12 +-- .../dynamics/backend/test_dynamics_backend.py | 102 +++++++++++++++++- .../perturbation/test_dyson_magnus.py | 62 ++++++++++- .../perturbation/test_multiset_utils.py | 9 ++ .../test_solve_lmde_perturbation.py | 75 +++++++++++++ tools/deploy_documentation.sh | 11 +- tox.ini | 29 +++-- 31 files changed, 493 insertions(+), 147 deletions(-) create mode 100644 releasenotes/notes/diffrax-bound-0bd80c01b7f4b48f.yaml create mode 100755 releasenotes/notes/measurement_property_bug_fix-12461088823a943c.yaml create mode 100644 releasenotes/notes/move-repo-c0b48ba3b0ced8db.yaml create mode 100644 releasenotes/notes/multiset-order-bug-fix-1f1603ee1e230cba.yaml create mode 100644 releasenotes/notes/patch-0.4.1-d339aa8669341341.yaml diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index ad99f9030..4490fce6a 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -1,5 +1,6 @@ name: Docs Publish on: + workflow_dispatch: push: tags: - "*" diff --git a/CITATION.bib b/CITATION.bib index 7b4688214..934523e67 100644 --- a/CITATION.bib +++ b/CITATION.bib @@ -4,5 +4,5 @@ @misc{qiskit_dynamics_2021 year = {2021}, publisher = {GitHub}, journal = {GitHub repository}, - url = {https://github.com/Qiskit/qiskit-dynamics} + url = {https://github.com/Qiskit-Extensions/qiskit-dynamics} } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 47165cb2c..f3ecca87c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -152,7 +152,7 @@ as: fixes: - | Fixes a race condition in the function ``foo()``. Refer to - `#12345 ` for more + `#12345 ` for more details. ``` diff --git a/README.md b/README.md index 88b96fde8..e0f424a29 100644 --- a/README.md +++ b/README.md @@ -4,26 +4,26 @@ **This repo is still in the early stages of development, there will be breaking API changes** -Qiskit Dynamics is an open-source project for building, transforming, and solving -time-dependent quantum systems in Qiskit. - -The goal of Qiskit Dynamics is to provide access to different numerical -methods for solving differential equations, and to automate common processes typically performed by hand, -e.g. applying frame transformations or rotating wave approximations to system and control Hamiltonians. - -Qiskit Dynamics can be configured to use either -[NumPy](https://github.com/numpy/numpy) or [JAX](https://github.com/google/jax) -as the backend for array operations. [NumPy](https://github.com/numpy/numpy) is the default, -and [JAX](https://github.com/google/jax) is an optional dependency. -[JAX](https://github.com/google/jax) provides just-in-time compilation, automatic differentiation, -and GPU execution, and therefore is well-suited to tasks involving repeated -evaluation of functions with different parameters; E.g. simulating a model of a quantum system -over a range of parameter values, or optimizing the parameters of control sequence. - -Reference documentation may be found [here](https://qiskit.org/documentation/dynamics/), -including [tutorials](https://qiskit.org/documentation/dynamics/tutorials), -[user guide](https://qiskit.org/documentation/dynamics/userguide), -and [API reference](https://qiskit.org/documentation/dynamics/apidocs). +Qiskit Dynamics is an open-source project for building, transforming, and solving time-dependent +quantum systems in Qiskit. + +The goal of Qiskit Dynamics is to provide access to different numerical methods for solving +differential equations, and to automate common processes typically performed by hand, e.g. applying +frame transformations or rotating wave approximations to system and control Hamiltonians. + +Qiskit Dynamics can be configured to use either [NumPy](https://github.com/numpy/numpy) or +[JAX](https://github.com/google/jax) as the backend for array operations. +[NumPy](https://github.com/numpy/numpy) is the default, and [JAX](https://github.com/google/jax) is +an optional dependency. [JAX](https://github.com/google/jax) provides just-in-time compilation, +automatic differentiation, and GPU execution, and therefore is well-suited to tasks involving +repeated evaluation of functions with different parameters; E.g. simulating a model of a quantum +system over a range of parameter values, or optimizing the parameters of control sequence. + +Reference documentation may be found [here](https://qiskit.org/ecosystem/dynamics/), including +[tutorials](https://qiskit.org/ecosystem/dynamics/tutorials/index.html), +[user guide](https://qiskit.org/ecosystem/dynamics/userguide/index.html), +[API reference](https://qiskit.org/ecosystem/dynamics/apidocs/index.html), and +[Discussions](https://qiskit.org/ecosystem/dynamics/discussions/index.html). ## Installation @@ -46,25 +46,22 @@ Installing JAX with GPU support must be done manually, for instructions refer to ## Contribution Guidelines -If you'd like to contribute to Qiskit Dynamics, please take a look at our -[contribution guidelines](CONTRIBUTING.md). This project adheres to Qiskit's -[code of conduct](CODE_OF_CONDUCT.md). By participating, you are expected to -uphold this code. - -We use [GitHub issues](https://github.com/Qiskit/qiskit-dynamics/issues) for -tracking requests and bugs. Please -[join the Qiskit Slack community](https://qisk.it/join-slack) -and use our [#qiskit-dynamics](https://qiskit.slack.com/archives/C03E7UVCDEV) channel for discussion and -simple questions. -For questions that are more suited for a forum we use the Qiskit tag in the +If you'd like to contribute to Qiskit Dynamics, please take a look at our +[contribution guidelines](CONTRIBUTING.md). This project adheres to Qiskit's +[code of conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. + +We use [GitHub issues](https://github.com/Qiskit-Extensions/qiskit-dynamics/issues) for tracking +requests and bugs. Please [join the Qiskit Slack community](https://qisk.it/join-slack) and use our +[#qiskit-dynamics](https://qiskit.slack.com/archives/C03E7UVCDEV) channel for discussion and simple +questions. For questions that are more suited for a forum we use the Qiskit tag in the [Stack Exchange](https://quantumcomputing.stackexchange.com/questions/tagged/qiskit). ## Authors and Citation -Qiskit Dynamics is the work of -[many people](https://github.com/Qiskit/qiskit-dynamics/graphs/contributors) who contribute -to the project at different levels. If you use Qiskit, please cite as per the included -[BibTeX file](https://github.com/Qiskit/qiskit-dynamics/blob/main/CITATION.bib). +Qiskit Dynamics is the work of +[many people](https://github.com/Qiskit-Extensions/qiskit-dynamics/graphs/contributors) who +contribute to the project at different levels. If you use Qiskit, please cite as per the included +[BibTeX file](https://github.com/Qiskit-Extensions/qiskit-dynamics/blob/main/CITATION.bib). ## License diff --git a/docs/conf.py b/docs/conf.py index f5a946aff..ab137a43f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -12,10 +12,6 @@ # pylint: disable=invalid-name -""" -Sphinx documentation builder -""" - # General options: project = 'Qiskit Dynamics' @@ -25,7 +21,7 @@ # The short X.Y version version = '' # The full version, including alpha/beta/rc tags -release = '0.4.0' +release = '0.4.1' extensions = [ 'sphinx.ext.napoleon', @@ -39,7 +35,8 @@ 'reno.sphinxext', 'sphinx.ext.intersphinx', 'nbsphinx', - 'sphinxcontrib.bibtex' + 'sphinxcontrib.bibtex', + "qiskit_sphinx_theme", ] templates_path = ["_templates"] diff --git a/docs/index.rst b/docs/index.rst index 1f9ce685e..b9cb37812 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -20,7 +20,7 @@ just-in-time compilation, automatic differentiation, and GPU execution of Qiskit This package is still in the early stages of development and it is very likely that there will be breaking API changes in future releases. If you encounter any bugs please open an issue on - `Github `_ + `Github `_ .. toctree:: diff --git a/docs/tutorials/Lindblad_dynamics_simulation.rst b/docs/tutorials/Lindblad_dynamics_simulation.rst index 34bcd4925..1d17caf88 100644 --- a/docs/tutorials/Lindblad_dynamics_simulation.rst +++ b/docs/tutorials/Lindblad_dynamics_simulation.rst @@ -14,7 +14,7 @@ In the sections below we define a model, solve the dynamics and plot some observables using the following steps: 1. Define the number of qubits and precompute some matrix operators. -2. Define all relevant parameters and setup a ``Solver`` instance with the model of the system, +2. Define all relevant parameters and setup a :class:`Solver` instance with the model of the system, consisting of the Hamiltonian and the jump operators of the Lindblad dissipator. 3. Define the initial state and other parameters for the initial value problem, and evolve the system state. @@ -110,7 +110,7 @@ used in the rest of this tutorial. 2. Setup the solver ------------------- -In this section we setup a ``Solver`` class that stores and manipulates +In this section we setup a :class:`Solver` class that stores and manipulates the model to be solved. In the following, we will set :math:`\hbar=1` and set the driving amplitude to be :math:`\nu_x \equiv 1`. This sets the time units, with the other frequency @@ -119,7 +119,7 @@ these free parameters, and then create the Hamiltonian matrix and the list of dissipator operators. We build the full Hamiltonian matrix by summing all single-qubit and two-qubit terms. Since there are no time-dependent terms, and we do not plan to take partial derivatives of -parameters, we do not use the ``Signal`` class in this tutorial. See the other tutorials for various +parameters, we do not use the :class:`Signal` class in this tutorial. See the other tutorials for various generalizations of this approach supported with ``qiskit-dynamics``. .. jupyter-execute:: diff --git a/docs/tutorials/Rabi_oscillations.rst b/docs/tutorials/Rabi_oscillations.rst index 7c38b956d..6346b444e 100644 --- a/docs/tutorials/Rabi_oscillations.rst +++ b/docs/tutorials/Rabi_oscillations.rst @@ -10,8 +10,7 @@ decoherence terms modeled by using a Lindblad master equation. In the sections below we define a model, solve the dynamics and plot the qubit oscillations using the following steps: -1. Define all relevant parameters and setup a ``Solver`` instance with the Hamiltonian model of - the system. +1. Setup a :class:`.Solver` with the Hamiltonian model 2. Define the initial state and simulation times, and evolve the system state. 3. Plot the qubit state as a function of time and discuss the results. 4. Solve again the the model with jump operators for the Lindblad dissipator, and plot the results. @@ -20,7 +19,9 @@ In the first step below, we model the time evolution of a qubit’s state taken as a two-level system, using the Schrödinger equation with a Hamiltonian containing a diagonal term of frequency :math:`\nu_z` and a transverse term of amplitude :math:`\nu_x` and harmonic driving -frequency :math:`\nu_d`, +frequency :math:`\nu_d` (see how the Hamiltonians are derived on +`Qiskit Textbook page on Introduction to Transmon Physics +`_), .. math:: H = \frac{1}{2} \times 2 \pi \nu_z {Z} + 2 \pi \nu_x \cos(2 \pi \nu_d t){X}, @@ -33,8 +34,8 @@ where :math:`\{X,Y,Z\}` are the Pauli matrices (also written as In the following, we will set :math:`\hbar=1` and fix some arbitrary time units, with all frequency parameters scaled accordingly. Below, we first set a few values for these frequency parameters, and then setup the -``Solver`` class instance that stores and manipulates the model to be -solved, using matrices and ``Signal`` instances. For the +:class:`.Solver` class instance that stores and manipulates the model to be +solved, using matrices and :class:`.Signal` instances. For the time-independent :math:`z` term we set the signal to a constant, while for the trasverse driving term we setup a harmonic signal. @@ -144,8 +145,8 @@ particular this is a realization of the :math:`X` gate. plot_qubit_dynamics(sol, t_eval, X, Y, Z) -4. Redefine the model with damping and decoherence. ---------------------------------------------------- +4. Redefine the model with damping and decoherence +-------------------------------------------------- Now we add to our simulation an environment modeled as a memory-less (Markovian) bath, solving the Lindblad master equation with the same diff --git a/docs/tutorials/dynamics_backend.rst b/docs/tutorials/dynamics_backend.rst index aca14ba86..c9b6f2c27 100644 --- a/docs/tutorials/dynamics_backend.rst +++ b/docs/tutorials/dynamics_backend.rst @@ -143,13 +143,16 @@ differential equation. The full list of allowable ``solver_options`` are the arg :func:`.solve_ode`. Note that, to enable the internal automatic jit-compilation, we choose a JAX integration method. +Furthermore, note that in the solver options we set the max step size to the pulse sample width +``dt`` via the ``"hmax"`` argument for the method ``"jax_odeint"``. This is important for preventing +variable step solvers from accidentally stepping over pulses in schedules with long idle times. .. jupyter-execute:: from qiskit_dynamics import DynamicsBackend # Consistent solver option to use throughout notebook - solver_options = {"method": "jax_odeint", "atol": 1e-6, "rtol": 1e-8} + solver_options = {"method": "jax_odeint", "atol": 1e-6, "rtol": 1e-8, "hmax": dt} backend = DynamicsBackend( solver=solver, @@ -318,8 +321,9 @@ To enable running of the single qubit experiments, we add the following to the ` backend. - Add definitions of ``RZ`` gates as phase shifts. These instructions control the phase of the drive channels, as well as any control channels acting on a given qubit. -- Add a ``CX`` gate which applies to all qubits. While this tutorial will not be utilizing it, this - ensures that validation steps checking that the device is fully connected will pass. +- Add a ``CX`` gate between qubits :math:`(0, 1)` and :math:`(1, 0)`. While this tutorial will not + be utilizing it, this ensures that validation steps checking that the device is fully connected + will pass. .. jupyter-execute:: @@ -336,7 +340,7 @@ To enable running of the single qubit experiments, we add the following to the ` target.add_instruction(XGate(), properties={(0,): None, (1,): None}) target.add_instruction(SXGate(), properties={(0,): None, (1,): None}) - target.add_instruction(CXGate()) + target.add_instruction(CXGate(), properties={(0, 1): None, (1, 0): None}) # Add RZ instruction as phase shift for drag cal phi = Parameter("phi") @@ -472,11 +476,6 @@ values for the single qubit gates calibrated above. from qiskit_experiments.library import CrossResonanceHamiltonian - backend.target.add_instruction( - instruction=CrossResonanceHamiltonian.CRPulseGate(width=Parameter("width")), - properties={(0, 1): None, (1, 0): None} - ) - cr_ham_experiment = CrossResonanceHamiltonian( qubits=(0, 1), flat_top_widths=np.linspace(0, 5000, 17), diff --git a/docs/tutorials/qiskit_pulse.rst b/docs/tutorials/qiskit_pulse.rst index 07f009937..2725c5a88 100644 --- a/docs/tutorials/qiskit_pulse.rst +++ b/docs/tutorials/qiskit_pulse.rst @@ -25,7 +25,7 @@ ground state we expect that this second pulse will not have any effect on the qubit. This situation is simulated with the following steps: 1. Create the pulse schedule -2. Converting pulse schedules to Signals +2. Converting pulse schedules to a :class:`.Signal` 3. Create the system model, configured to simulate pulse schedules 4. Simulate the pulse schedule using the model @@ -48,30 +48,27 @@ First, we use the pulse module in Qiskit to create a pulse schedule. # Sample rate of the backend in ns. dt = 1 / 4.5 - # Define gaussian envelope function to have a pi rotation. - amp = 1. - area = 1 - sig = area*0.399128/r/amp + # Define gaussian envelope function to approximately implement an sx gate. + amp = 1. / 1.75 + sig = 0.6985/r/amp T = 4*sig duration = int(T / dt) beta = 2.0 - # The 1.75 factor is used to approximately get a sx gate. - # Further "calibration" could be done to refine the pulse amplitude. - with pulse.build(name="sx-sy schedule") as xp: - pulse.play(pulse.Drag(duration, amp / 1.75, sig / dt, beta), pulse.DriveChannel(0)) + with pulse.build(name="sx-sy schedule") as sxp: + pulse.play(pulse.Drag(duration, amp, sig / dt, beta), pulse.DriveChannel(0)) pulse.shift_phase(np.pi/2, pulse.DriveChannel(0)) - pulse.play(pulse.Drag(duration, amp / 1.75, sig / dt, beta), pulse.DriveChannel(0)) + pulse.play(pulse.Drag(duration, amp, sig / dt, beta), pulse.DriveChannel(0)) - xp.draw() + sxp.draw() -2. Convert the pulse schedule to a ``Signal`` ---------------------------------------------- +2. Convert the pulse schedule to a :class:`.Signal` +--------------------------------------------------- Qiskit Dynamics has functionality for converting pulse schedule to instances -of ``Signal``. This is done using the pulse instruction to signal -converter ``InstructionToSignals``. This converter needs to know the +of :class:`.Signal`. This is done using the pulse instruction to signal +converter :class:`.InstructionToSignals`. This converter needs to know the sample rate of the arbitrary waveform generators creating the signals, i.e. ``dt``, as well as the carrier frequency of the signals, i.e. ``w``. The plot below shows the envelopes and the signals resulting @@ -87,7 +84,7 @@ virtual ``Z`` gate is applied. converter = InstructionToSignals(dt, carriers={"d0": w}) - signals = converter.get_signals(xp) + signals = converter.get_signals(sxp) fig, axs = plt.subplots(1, 2, figsize=(14, 4.5)) for ax, title in zip(axs, ["envelope", "signal"]): signals[0].draw(0, 2*T, 2000, title, axis=ax) @@ -101,7 +98,7 @@ virtual ``Z`` gate is applied. 3. Create the system model -------------------------- -We now setup a ``Solver`` instance with the desired Hamiltonian information, +We now setup a :class:`.Solver` instance with the desired Hamiltonian information, and configure it to simulate pulse schedules. This requires specifying which channels act on which operators, channel carrier frequencies, and sample width ``dt``. Additionally, we setup this solver in the rotating frame and perform the @@ -146,7 +143,7 @@ and in this case should produce identical behavior. # Start the qubit in its ground state. y0 = Statevector([1., 0.]) - %time sol = hamiltonian_solver.solve(t_span=[0., 2*T], y0=y0, signals=xp, atol=1e-8, rtol=1e-8) + %time sol = hamiltonian_solver.solve(t_span=[0., 2*T], y0=y0, signals=sxp, atol=1e-8, rtol=1e-8) .. jupyter-execute:: diff --git a/docs/userguide/how_to_configure_simulations.rst b/docs/userguide/how_to_configure_simulations.rst index d1c072c5f..64b0babb6 100644 --- a/docs/userguide/how_to_configure_simulations.rst +++ b/docs/userguide/how_to_configure_simulations.rst @@ -20,7 +20,7 @@ Here we walk through some of these options, covering: rotating frame to preserve sparsity -Throughout this guide we work at the level of the ``Solver`` interface, +Throughout this guide we work at the level of the :class:`.Solver` interface, and consider Hamiltonian dynamics for simplicity, however all of the considerations have their analogs for Lindblad dynamics. @@ -28,7 +28,7 @@ considerations have their analogs for Lindblad dynamics. ----------------------------------------------------------------------------- Here we show how to perform a simulation in a rotating frame by setting the -optional ``rotating_frame`` argument when instantiating a ``Solver``, and demonstrate how a +optional ``rotating_frame`` argument when instantiating a :class:`.Solver`, and demonstrate how a well-chosen frame operator :math:`F = -iH_0` can reduce solving time. See the :ref:`Rotating frames section of the Models API documentation ` for details on rotating frames. @@ -74,7 +74,7 @@ First, construct the components of the model: # total simulation time T = 1. / r -Construct a ``Solver`` for the model as stated, without entering a rotating frame, and solve, +Construct a :class:`.Solver` for the model as stated, without entering a rotating frame, and solve, timing the solver. .. jupyter-execute:: @@ -87,7 +87,7 @@ timing the solver. y0 = np.eye(dim, dtype=complex) %time results = solver.solve(t_span=[0., T], y0=y0, signals=[drive_signal], atol=1e-10, rtol=1e-10) -Next, define a ``Solver`` in the rotating frame of the static +Next, define a :class:`.Solver` in the rotating frame of the static Hamiltonian by setting the ``rotating_frame`` kwarg, and solve, again timing the solver. .. jupyter-execute:: @@ -151,7 +151,7 @@ reducing the number of RHS calls required to solve with a given accuracy. --------------------------------------------------------------------------- Next we show how to perform a simulation with the rotating wave approximation (RWA) -by setting the ``rwa_cutoff_freq`` argument at ``Solver`` instantiation, and show +by setting the ``rwa_cutoff_freq`` argument at :class:`.Solver` instantiation, and show how it results in further speed ups at the expense of solution accuracy. See the API documentation for the :meth:`~qiskit_dynamics.models.rotating_wave_approximation` function for specific details about the RWA. @@ -201,7 +201,7 @@ with extra emphasis on the following: :ref:`evaluation modes section of the Models API documentation `, when using a sparse evaluation mode, to preserve sparsity, it is recommended to only use *diagonal* rotating frames, which can be specified as a 1d array to the - ``rotating_frame`` kwarg of ``Solver`` instantiation. + ``rotating_frame`` kwarg of :class:`.Solver` instantiation. For this section we use JAX as it is more performant. See the :ref:`userguide on using JAX ` for a more detailed diff --git a/docs/userguide/how_to_use_jax.rst b/docs/userguide/how_to_use_jax.rst index 5aadb7205..905013e0a 100644 --- a/docs/userguide/how_to_use_jax.rst +++ b/docs/userguide/how_to_use_jax.rst @@ -5,7 +5,7 @@ How-to use JAX with ``qiskit-dynamics`` JAX enables just-in-time compilation, automatic differentation, and GPU execution. JAX is integrated into ``qiskit-dynamics`` via the -``Array`` class, which allows most parts of the package to be +:class:`.Array` class, which allows most parts of the package to be executed with either ``numpy`` or ``jax.numpy``. This guide addresses the following topics: @@ -20,7 +20,7 @@ This guide addresses the following topics: 1. How do I configure dynamics to run with JAX? ----------------------------------------------- -The ``Array`` class provides a means of controlling whether array +The :class:`.Array` class provides a means of controlling whether array operations are performed using ``numpy`` or ``jax.numpy``. In many cases, the “default backend” is used to determine which of the two options is used. @@ -50,8 +50,8 @@ The default backend can be observed via: The ``Array`` class wraps both ``numpy`` and ``jax.numpy`` arrays. The particular type is indicated by the ``backend`` property, -and ``numpy`` functions called on an ``Array`` will automatically be -dispatched to ``numpy`` or ``jax.numpy`` based on the ``Array``\ ’s +and ``numpy`` functions called on an :class:`.Array` will automatically be +dispatched to ``numpy`` or ``jax.numpy`` based on the :class:`.Array`\ ’s backend. See the API documentation for ``qiskit_dynamics.array`` for details. @@ -67,7 +67,7 @@ JAX-transformable functions must be: - Pure, in the sense that they have no side-effects. The previous section shows how to handle the first two points using -``Array``. The last point further restricts the type of +:class:`.Array`. The last point further restricts the type of code that can be safely transformed. Qiskit Dynamics uses various objects which can be updated by setting properties (models, solvers). If a function to be transformed requires updating an already-constructed object of this @@ -90,7 +90,7 @@ functions built using Qiskit Dynamics can be just-in-time compiled, resulting in faster simulation times. For convenience, the ``wrap`` function can be used to transform -``jax.jit`` to also work on functions that have ``Array`` objects as +``jax.jit`` to also work on functions that have :class:`.Array` objects as inputs and outputs. .. jupyter-execute:: @@ -99,7 +99,7 @@ inputs and outputs. jit = wrap(jax.jit, decorator=True) -Construct a ``Solver`` instance with a model that will be used to solve. +Construct a :class:`.Solver` instance with a model that will be used to solve. .. jupyter-execute:: @@ -234,8 +234,8 @@ To get dynamics to run with JAX, it is necessary to configure dynamics to run with JAX *before* building any objects or running any functions. The internal behaviour of some objects is modified by what the default backend is *at the time of instantiation*. For example, at instantiation -the operators in a model or ``Solver`` instance will be wrapped in an -``Array`` whose backend is the current default backend, and changing the +the operators in a model or :class:`.Solver` instance will be wrapped in an +:class:`.Array` whose backend is the current default backend, and changing the default backend after building the object won’t change this. 4.2 Running Dynamics with JAX on CPU vs GPU diff --git a/docs/userguide/perturbative_solvers.rst b/docs/userguide/perturbative_solvers.rst index f8d082e29..df9c15417 100644 --- a/docs/userguide/perturbative_solvers.rst +++ b/docs/userguide/perturbative_solvers.rst @@ -126,7 +126,7 @@ along with the structure of the differential equation: - To compute the truncated perturbative expansion, the signal envelopes are approximated as a linear combination of Chebyshev polynomials. - The order of the Chebyshev approximations, along with central carrier frequencies - for defining the “envelope” of each ``Signal``, must be provided at instantiation. + for defining the “envelope” of each :class:`.Signal`, must be provided at instantiation. See the :class:`.DysonSolver` API docs for more details. diff --git a/pyproject.toml b/pyproject.toml index 8af40f764..f9728a2be 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,6 @@ [build-system] -requires = ["setuptools", "wheel"] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" [tool.black] line-length = 100 diff --git a/qiskit_dynamics/VERSION.txt b/qiskit_dynamics/VERSION.txt index 1d0ba9ea1..267577d47 100644 --- a/qiskit_dynamics/VERSION.txt +++ b/qiskit_dynamics/VERSION.txt @@ -1 +1 @@ -0.4.0 +0.4.1 diff --git a/qiskit_dynamics/backend/dynamics_backend.py b/qiskit_dynamics/backend/dynamics_backend.py index c4307138d..02e4d1603 100644 --- a/qiskit_dynamics/backend/dynamics_backend.py +++ b/qiskit_dynamics/backend/dynamics_backend.py @@ -203,17 +203,18 @@ def __init__( # add default simulator measure instructions measure_properties = {} + instruction_schedule_map = target.instruction_schedule_map() for qubit in self.options.subsystem_labels: - instruction_schedule_map = target.instruction_schedule_map() if not instruction_schedule_map.has(instruction="measure", qubits=qubit): with pulse.build() as meas_sched: pulse.acquire( duration=1, qubit_or_channel=qubit, register=pulse.MemorySlot(qubit) ) - measure_properties[(qubit,)] = InstructionProperties(calibration=meas_sched) + measure_properties[(qubit,)] = InstructionProperties(calibration=meas_sched) - target.add_instruction(Measure(), measure_properties) + if bool(measure_properties): + target.add_instruction(Measure(), measure_properties) target.dt = solver._dt @@ -682,6 +683,31 @@ def from_backend( channels=hamiltonian_channels, ) + # Add control_channel_map from backend (only if not specified before by user) + if "control_channel_map" not in options: + if hasattr(backend, "control_channels"): + control_channel_map_backend = { + qubits: backend.control_channels[qubits][0].index + for qubits in backend.control_channels + } + + elif hasattr(backend.configuration(), "control_channels"): + control_channel_map_backend = { + qubits: backend.configuration().control_channels[qubits][0].index + for qubits in backend.configuration().control_channels + } + + else: + control_channel_map_backend = {} + + # Reduce control_channel_map based on which channels are in the model + if bool(control_channel_map_backend): + control_channel_map = {} + for label, idx in control_channel_map_backend.items(): + if f"u{idx}" in hamiltonian_channels: + control_channel_map[label] = idx + options["control_channel_map"] = control_channel_map + # build the solver if rotating_frame == "auto": if "dense" in evaluation_mode: @@ -867,7 +893,7 @@ def _get_acquire_instruction_timings( ) -> 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 + 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``. diff --git a/qiskit_dynamics/perturbation/dyson_magnus.py b/qiskit_dynamics/perturbation/dyson_magnus.py index b6de8338a..7642bcd25 100644 --- a/qiskit_dynamics/perturbation/dyson_magnus.py +++ b/qiskit_dynamics/perturbation/dyson_magnus.py @@ -804,13 +804,13 @@ def _get_q_term_list(complete_index_multisets: List[Multiset]) -> List: def _get_dyson_lmult_rule( complete_index_multisets: List[Multiset], perturbation_labels: Optional[List[Multiset]] = None ) -> List: - """Given a complete list of index multisets, return - the lmult rule in the format required for ``CustomProduct``. - Note, the generator :math:`G(t)` is encoded as index ``-1``, as - it will be prepended to the list of A matrices. + """Given a complete list of index multisets, return the lmult rule in the format required for + ``CustomProduct``. Note, the generator :math:`G(t)` is encoded as index ``-1``, as it will be + prepended to the list of A matrices. Similarly the index of the solution to the LMDE with + generator :math:`G(t)` is encoded as ``-1``. - While not required within the logic of this function, the input - should be canonically ordered according to ``_get_all_submultisets``. + While not required within the logic of this function, the input should be canonically ordered + according to ``_get_all_submultisets``. Args: complete_index_multisets: List of complete multisets. @@ -834,8 +834,15 @@ def _get_dyson_lmult_rule( lmult_rule = [(np.array([1.0]), np.array([[-1, -1]]))] for term_idx, term in enumerate(complete_index_multisets): - if len(term) == 1: - lmult_rule.append((np.array([1.0, 1.0]), np.array([[-1, term_idx], [term_idx, -1]]))) + if len(term) == 1 and term in perturbation_labels: + # if term not in perturbation_labels for len(term) == 1 the corresponding Dyson integral + # will always be 0 + lmult_rule.append( + ( + np.array([1.0, 1.0]), + np.array([[-1, term_idx], [perturbation_labels.index(term), -1]]), + ) + ) else: # self multiplied by base generator lmult_indices = [[-1, term_idx]] diff --git a/qiskit_dynamics/perturbation/multiset_utils.py b/qiskit_dynamics/perturbation/multiset_utils.py index 79f83db18..04fa1b235 100644 --- a/qiskit_dynamics/perturbation/multiset_utils.py +++ b/qiskit_dynamics/perturbation/multiset_utils.py @@ -15,7 +15,7 @@ Utility functions for working with multisets. """ -from typing import List, Optional, Tuple +from typing import List, Optional, Tuple, Iterable import itertools from multiset import Multiset @@ -46,15 +46,51 @@ def _multiset_to_sorted_list(multiset: Multiset) -> List: return sorted_list -def _sorted_multisets(multisets: List[Multiset]) -> List[Multiset]: - """Sort in non-decreasing order according to: - - ms1 <= ms2 if len(ms1) < len(ms2), or if - len(ms1) == len(ms2) and if - str(_multiset_to_sorted_list(ms1)) <= str(_multiset_to_sorted_list(ms2)). +class _MultisetSortKey: + """Dummy class for usage as a key when sorting Multiset instances. This assumes the elements + of the multisets can themselves be sorted. """ - return sorted(multisets, key=lambda x: str(len(x)) + ", " + str(_multiset_to_sorted_list(x))) + __slots__ = ("multiset",) + + def __init__(self, multiset: Multiset): + self.multiset = multiset + + def __lt__(self, other: Multiset) -> bool: + """Implements an ordering on multisets. + + This orders first according to length (the number of elements in each multiset). If ``self`` + and ``other`` are the same length, ``self < other`` if, when written as fully expanded and + sorted lists, ``self < other`` in lexicographic ordering. E.g. it holds that ``Multiset({0: + 2, 1: 1}) < Multiset({0: 1, 1: 2})``, as the list versions are ``x = [0, 0, 1]``, and ``y = + [0, 1, 1]``. Here ``x[0] == y[0]``, but ``x[1] < y[1]``, and hence ``x < y`` in this + ordering. + """ + if len(self.multiset) < len(other.multiset): + return True + + if len(other.multiset) < len(self.multiset): + return False + + unique_entries = set(self.multiset.distinct_elements()) + unique_entries.update(other.multiset.distinct_elements()) + unique_entries = sorted(unique_entries) + + for element in unique_entries: + self_count = self.multiset[element] + other_count = other.multiset[element] + + if self_count != other_count: + return self_count > other_count + + return False + + +def _sorted_multisets(multisets: Iterable[Multiset]) -> List[Multiset]: + """Sort in non-decreasing order according to the ordering described in the dummy class + _MultisetSort. + """ + return sorted(multisets, key=_MultisetSortKey) def _clean_multisets(multisets: List[Multiset]) -> List[Multiset]: diff --git a/releasenotes/notes/diffrax-bound-0bd80c01b7f4b48f.yaml b/releasenotes/notes/diffrax-bound-0bd80c01b7f4b48f.yaml new file mode 100644 index 000000000..293d6966f --- /dev/null +++ b/releasenotes/notes/diffrax-bound-0bd80c01b7f4b48f.yaml @@ -0,0 +1,5 @@ +--- +issues: + - | + 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. \ No newline at end of file diff --git a/releasenotes/notes/measurement_property_bug_fix-12461088823a943c.yaml b/releasenotes/notes/measurement_property_bug_fix-12461088823a943c.yaml new file mode 100755 index 000000000..c4c69601b --- /dev/null +++ b/releasenotes/notes/measurement_property_bug_fix-12461088823a943c.yaml @@ -0,0 +1,11 @@ +--- +features: + - | + 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. + +fixes: + - | + A bug in :meth:`DynamicsBackend.__init__` causing existing measurement instructions for a + user-supplied :class:`Target` to be overwritten has been fixed. + diff --git a/releasenotes/notes/move-repo-c0b48ba3b0ced8db.yaml b/releasenotes/notes/move-repo-c0b48ba3b0ced8db.yaml new file mode 100644 index 000000000..f03b8fbc9 --- /dev/null +++ b/releasenotes/notes/move-repo-c0b48ba3b0ced8db.yaml @@ -0,0 +1,9 @@ +--- +other: + - | + 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/releasenotes/notes/multiset-order-bug-fix-1f1603ee1e230cba.yaml b/releasenotes/notes/multiset-order-bug-fix-1f1603ee1e230cba.yaml new file mode 100644 index 000000000..eb1110ce1 --- /dev/null +++ b/releasenotes/notes/multiset-order-bug-fix-1f1603ee1e230cba.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + 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. diff --git a/releasenotes/notes/patch-0.4.1-d339aa8669341341.yaml b/releasenotes/notes/patch-0.4.1-d339aa8669341341.yaml new file mode 100644 index 000000000..ea9b5d835 --- /dev/null +++ b/releasenotes/notes/patch-0.4.1-d339aa8669341341.yaml @@ -0,0 +1,4 @@ +--- +prelude: > + Qiskit Dynamics 0.4.1 is an incremental release with minor bug fixes, documentation updates, + and usability features. \ No newline at end of file diff --git a/requirements-dev.txt b/requirements-dev.txt index 99dec6a4d..a1aa26bf7 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,7 +2,7 @@ stestr>=3.0.0 astroid==2.9.3 pylint==2.12.2 black~=22.0 -qiskit-sphinx-theme~=1.10 +qiskit-sphinx-theme~=1.12.0 sphinx-autodoc-typehints jupyter-sphinx pygments>=2.4 @@ -14,4 +14,5 @@ ddt~=1.4.2 matplotlib>=3.3.0 qiskit-experiments pandas # for docs only -pylatexenc>=1.4 # for docs only \ No newline at end of file +pylatexenc>=1.4 # for docs only +ipython<8.13.0 # for docs build with python 3.8 \ No newline at end of file diff --git a/setup.py b/setup.py index 35c9f48de..54064a497 100644 --- a/setup.py +++ b/setup.py @@ -23,8 +23,8 @@ "multiset>=3.0.1", ] -jax_extras = ['jax>=0.2.26', - 'jaxlib>=0.1.75'] +jax_extras = ['jax>=0.2.26, <= 0.4.6', + 'jaxlib>=0.1.75, <= 0.4.6'] PACKAGES = setuptools.find_packages(exclude=['test*']) @@ -47,7 +47,7 @@ description="Qiskit ODE solver", long_description=README, long_description_content_type='text/markdown', - url="https://github.com/Qiskit/qiskit-dynamics", + url="https://github.com/Qiskit-Extensions/qiskit-dynamics", author="Qiskit Development Team", author_email="qiskit@qiskit.org", license="Apache 2.0", @@ -68,9 +68,9 @@ ], keywords="qiskit sdk quantum", project_urls={ - "Bug Tracker": "https://github.com/Qiskit/qiskit-dynamics/issues", - "Source Code": "https://github.com/Qiskit/qiskit-dynamics", - "Documentation": "https://qiskit.org/documentation/dynamics", + "Bug Tracker": "https://github.com/Qiskit-Extensions/qiskit-dynamics/issues", + "Source Code": "https://github.com/Qiskit-Extensions/qiskit-dynamics", + "Documentation": "https://qiskit.org/ecosystem/dynamics/", }, install_requires=requirements, include_package_data=True, diff --git a/test/dynamics/backend/test_dynamics_backend.py b/test/dynamics/backend/test_dynamics_backend.py index fcedb414e..6dff0211e 100644 --- a/test/dynamics/backend/test_dynamics_backend.py +++ b/test/dynamics/backend/test_dynamics_backend.py @@ -22,7 +22,7 @@ from scipy.sparse import csr_matrix from qiskit import QiskitError, pulse, QuantumCircuit -from qiskit.circuit.library import XGate +from qiskit.circuit.library import XGate, Measure from qiskit.transpiler import Target, InstructionProperties from qiskit.quantum_info import Statevector, DensityMatrix from qiskit.result.models import ExperimentResult, ExperimentResultData @@ -264,6 +264,7 @@ def setUp(self): dt=0.1, rotating_frame=static_ham_2q, ) + self.solver_2q = solver_2q self.backend_2q = DynamicsBackend(solver=solver_2q, subsystem_dims=[2, 2]) # function to discriminate 0 and 1 for default centers. @@ -549,6 +550,55 @@ def test_metadata_transfer(self): self.assertDictEqual(res.get_counts(1), {"2": 1024}) self.assertDictEqual(res.results[1].header.metadata, {"key1": "value1"}) + def test_valid_measurement_properties(self): + """Test that DynamicsBackend instantiation always carries measurement instructions.""" + + # Case where no measurement instruction is added manually + instruction_schedule_map = self.backend_2q.target.instruction_schedule_map() + for q in range(self.simple_backend.num_qubits): + self.assertTrue(instruction_schedule_map.has(instruction="measure", qubits=q)) + self.assertTrue( + isinstance( + instruction_schedule_map.get("measure", q).instructions[0][1], pulse.Acquire + ) + ) + self.assertEqual(len(instruction_schedule_map.get("measure", q).instructions), 1) + + # Case where measurement instruction is added manually + custom_meas_duration = 3 + with pulse.build() as meas_sched0: + pulse.acquire( + duration=custom_meas_duration, qubit_or_channel=0, register=pulse.MemorySlot(0) + ) + + with pulse.build() as meas_sched1: + pulse.acquire( + duration=custom_meas_duration, qubit_or_channel=1, register=pulse.MemorySlot(1) + ) + + measure_properties = { + (0,): InstructionProperties(calibration=meas_sched0), + (1,): InstructionProperties(calibration=meas_sched1), + } + target = Target() + target.add_instruction(Measure(), measure_properties) + custom_meas_backend = DynamicsBackend( + solver=self.solver_2q, target=target, subsystem_dims=[2, 2] + ) + instruction_schedule_map = custom_meas_backend.target.instruction_schedule_map() + for q in range(self.simple_backend.num_qubits): + self.assertTrue(instruction_schedule_map.has(instruction="measure", qubits=q)) + self.assertTrue( + isinstance( + instruction_schedule_map.get("measure", q).instructions[0][1], pulse.Acquire + ) + ) + self.assertEqual(len(instruction_schedule_map.get("measure", q).instructions), 1) + self.assertEqual( + instruction_schedule_map.get("measure", q).instructions[0][1].duration, + custom_meas_duration, + ) + class TestDynamicsBackend_from_backend(QiskitDynamicsTestCase): """Test class for DynamicsBackend.from_backend and resulting DynamicsBackend instances.""" @@ -617,6 +667,17 @@ def setUp(self): [UchannelLO(3, (1 + 0j))], ] + configuration.control_channels = { + (0, 1): [pulse.ControlChannel(0)], + (1, 0): [pulse.ControlChannel(1)], + (1, 2): [pulse.ControlChannel(2)], + (2, 1): [pulse.ControlChannel(3)], + (1, 3): [pulse.ControlChannel(4)], + (3, 1): [pulse.ControlChannel(5)], + (3, 4): [pulse.ControlChannel(6)], + (4, 3): [pulse.ControlChannel(7)], + } + defaults = SimpleNamespace() defaults.qubit_freq_est = [ 5175383639.513607, @@ -630,6 +691,7 @@ def setUp(self): backend = SimpleNamespace() backend.configuration = lambda: configuration backend.defaults = lambda: defaults + backend.control_channels = backend.configuration().control_channels self.valid_backend = backend @@ -827,6 +889,44 @@ def test_building_model_case2(self): ) self.assertAllClose(expected_operators / 1e9, solver.model.operators / 1e9) + def test_setting_control_channel_map(self): + """Test automatic padding of control_channel_map in DynamicsBackend + options from original backend.""" + + # Check that manual setting of the map overrides the one from original backend + control_channel_map = {(0, 1): 4} + backend = DynamicsBackend.from_backend( + self.valid_backend, control_channel_map=control_channel_map + ) + self.assertDictEqual(backend.options.control_channel_map, {(0, 1): 4}) + + # Check that control_channel_map from original backend is set in DynamicsBackend.options + backend = DynamicsBackend.from_backend(self.valid_backend) + self.assertDictEqual( + backend.options.control_channel_map, + { + (0, 1): 0, + (1, 0): 1, + (1, 2): 2, + (2, 1): 3, + (1, 3): 4, + (3, 1): 5, + (3, 4): 6, + (4, 3): 7, + }, + ) + + # Check that reduction to subsystem_list is correct + backend = DynamicsBackend.from_backend(self.valid_backend, subsystem_list=[0, 1, 2]) + self.assertDictEqual( + backend.options.control_channel_map, + {(0, 1): 0, (1, 0): 1, (1, 2): 2, (2, 1): 3, (1, 3): 4}, + ) + + # Check that manually setting the option after the declaration overwrites the previous map + backend.set_options(control_channel_map={(0, 1): 3}) + self.assertDictEqual(backend.options.control_channel_map, {(0, 1): 3}) + class Test_default_experiment_result_function(QiskitDynamicsTestCase): """Test default_experiment_result_function.""" diff --git a/test/dynamics/perturbation/test_dyson_magnus.py b/test/dynamics/perturbation/test_dyson_magnus.py index dd0943b26..3d812cba4 100644 --- a/test/dynamics/perturbation/test_dyson_magnus.py +++ b/test/dynamics/perturbation/test_dyson_magnus.py @@ -555,6 +555,31 @@ def test__get_dyson_lmult_rule_power_series_case1(self): perturbation_labels=perturbation_labels, ) + def test__get_dyson_lmult_rule_power_series_case1_missing(self): + """Test _get_dyson_lmult_rule for higher order terms in the generator + decomposition case 1 where there is no corresponding perturbation_label term for + a desired expansion term. + """ + + expansion_labels = [[0], [1], [0, 1], [1, 1], [0, 1, 1]] + expansion_labels = [Multiset(label) for label in expansion_labels] + perturbation_labels = [[0], [0, 1], [1, 1]] + perturbation_labels = [Multiset(label) for label in perturbation_labels] + expected_lmult_rule = [ + (np.ones(1, dtype=float), np.array([[-1, -1]])), + (np.ones(2, dtype=float), np.array([[-1, 0], [0, -1]])), + (np.ones(1, dtype=float), np.array([[-1, 1]])), + (np.ones(3, dtype=float), np.array([[-1, 2], [0, 1], [1, -1]])), + (np.ones(2, dtype=float), np.array([[-1, 3], [2, -1]])), + (np.ones(4, dtype=float), np.array([[-1, 4], [0, 3], [1, 1], [2, 0]])), + ] + + self._test__get_dyson_lmult_rule( + expansion_labels, + expected_lmult_rule, + perturbation_labels=perturbation_labels, + ) + def test__get_dyson_lmult_rule_power_series_case2(self): """Test _get_dyson_lmult_rule for higher order terms in the generator decomposition case 2. @@ -579,6 +604,36 @@ def test__get_dyson_lmult_rule_power_series_case2(self): perturbation_labels=perturbation_labels, ) + def test__get_dyson_lmult_rule_power_series_case2_unordered(self): + """Test _get_dyson_lmult_rule for higher order terms in the generator + decomposition case 2, with the perturbation_labels being non-canonically ordered. + + Note that the conversion of case2 to case2_unordered requires relabelling 1 <-> 3 + in expected_lmult_rule, but also changing the ordering of the pairs in the matrix-product + description of expected_lmult_rule, as the rule is constructed by iterating through the + entries of perturbation_labels. The matrix-product description must be ordered by the first + index. + """ + + expansion_labels = [[0], [1], [0, 1], [1, 1], [0, 1, 1]] + expansion_labels = [Multiset(label) for label in expansion_labels] + perturbation_labels = [[0], [0, 1], [2], [1]] + perturbation_labels = [Multiset(label) for label in perturbation_labels] + expected_lmult_rule = [ + (np.ones(1, dtype=float), np.array([[-1, -1]])), + (np.ones(2, dtype=float), np.array([[-1, 0], [0, -1]])), + (np.ones(2, dtype=float), np.array([[-1, 1], [3, -1]])), + (np.ones(4, dtype=float), np.array([[-1, 2], [0, 1], [1, -1], [3, 0]])), + (np.ones(2, dtype=float), np.array([[-1, 3], [3, 1]])), + (np.ones(4, dtype=float), np.array([[-1, 4], [0, 3], [1, 1], [3, 2]])), + ] + + self._test__get_dyson_lmult_rule( + expansion_labels, + expected_lmult_rule, + perturbation_labels=perturbation_labels, + ) + def test__get_dyson_lmult_rule_case1(self): """Test __get_dyson_lmult_rule case 1.""" expansion_labels = [[0], [1], [0, 1], [1, 1], [0, 1, 1]] @@ -652,11 +707,10 @@ def test__get_dyson_lmult_rule_case2(self): self._test__get_dyson_lmult_rule(expansion_labels, expected_lmult_rule) def _test__get_dyson_lmult_rule( - self, complete_symmetric_dyson_term_list, expected, perturbation_labels=None + self, complete_index_multisets, expected, perturbation_labels=None ): - """Run a test case for _get_symmetric_dyson_mult_rules.""" - lmult_rule = _get_dyson_lmult_rule(complete_symmetric_dyson_term_list, perturbation_labels) - + """Run a test case for _get_dyson_lmult_rule.""" + lmult_rule = _get_dyson_lmult_rule(complete_index_multisets, perturbation_labels) self.assertMultRulesEqual(lmult_rule, expected) def assertMultRulesEqual(self, rule1, rule2): diff --git a/test/dynamics/perturbation/test_multiset_utils.py b/test/dynamics/perturbation/test_multiset_utils.py index 8be47b188..38526a6e6 100644 --- a/test/dynamics/perturbation/test_multiset_utils.py +++ b/test/dynamics/perturbation/test_multiset_utils.py @@ -59,6 +59,15 @@ def test_case1(self): expected = [Multiset([1]), Multiset([0, 2]), Multiset([0, 0, 1]), Multiset([0, 1, 1])] self.assertTrue(output == expected) + def test_case2(self): + """Test case which would have caught a bug introduced by a previous implementation of the + ordering. + """ + multisets = [Multiset([10]), Multiset([1])] + output = _sorted_multisets(multisets) + expected = [Multiset([1]), Multiset([10])] + self.assertTrue(output == expected) + class TestToSortedList(QiskitDynamicsTestCase): """Test conversion to sorted list.""" diff --git a/test/dynamics/perturbation/test_solve_lmde_perturbation.py b/test/dynamics/perturbation/test_solve_lmde_perturbation.py index 724061619..1e70a556d 100644 --- a/test/dynamics/perturbation/test_solve_lmde_perturbation.py +++ b/test/dynamics/perturbation/test_solve_lmde_perturbation.py @@ -501,6 +501,81 @@ def A1(t): self.assertAllClose(expected_D0001, results.perturbation_data.get_item([0, 0, 0, 1])[-1]) self.assertAllClose(expected_D0011, results.perturbation_data.get_item([0, 0, 1, 1])[-1]) + def test_dyson_analytic_case1_1d_relabeled(self): + """This is the same numerical test as test_dyson_analytic_case1_1d, however it relabels + the perturbation indices as 0 -> 1, and 1 -> 10. This is an integration test that would + have caught multiset sorting bug that broke solve_lmde_perturbation results computation. + """ + + def generator(t): + return Array([[1, 0], [0, 1]], dtype=complex).data + + def A0(t): + return Array([[0, t], [t**2, 0]], dtype=complex).data + + def A1(t): + return Array([[t, 0], [0, t**2]], dtype=complex).data + + T = np.pi * 1.2341 + + results = solve_lmde_perturbation( + perturbations=[A0, A1], + t_span=[0, T], + generator=generator, + y0=np.array([0.0, 1.0], dtype=complex), + expansion_method="dyson", + expansion_order=2, + expansion_labels=[[1, 1, 10], [1, 1, 1, 10], [1, 1, 10, 10]], + perturbation_labels=[[1], [10]], + dyson_in_frame=False, + integration_method=self.integration_method, + atol=1e-13, + rtol=1e-13, + ) + + T2 = T**2 + T3 = T * T2 + T4 = T * T3 + T5 = T * T4 + T6 = T * T5 + T7 = T * T6 + T8 = T * T7 + T9 = T * T8 + T10 = T * T9 + T11 = T * T10 + + U = expm(np.array(generator(0)) * T) + + expected_D0 = U @ np.array([[T2 / 2], [0]], dtype=complex) + expected_D1 = U @ np.array([[0], [T3 / 3]], dtype=complex) + expected_D00 = U @ np.array([[0], [T5 / 10]], dtype=complex) + expected_D01 = U @ ( + np.array([[T5 / 15], [0]], dtype=complex) + np.array([[T4 / 8], [0]], dtype=complex) + ) + expected_D11 = U @ np.array([[0], [T6 / 18]], dtype=complex) + expected_D001 = U @ ( + np.array([[0], [T8 / 120]], dtype=complex) + + np.array([[0], [T7 / 56]], dtype=complex) + + np.array([[0], [T8 / 80]], dtype=complex) + ) + expected_D0001 = U @ np.array([[(T10 / 480) + (T9 / 280)], [0]], dtype=complex) + expected_D0011 = U @ np.array( + [ + [0], + [(T11 / 396) + 23 * (T10 / 8400) + (T9 / 432)], + ], + dtype=complex, + ) + + self.assertAllClose(expected_D0, results.perturbation_data.get_item([1])[-1]) + self.assertAllClose(expected_D1, results.perturbation_data.get_item([10])[-1]) + self.assertAllClose(expected_D00, results.perturbation_data.get_item([1, 1])[-1]) + self.assertAllClose(expected_D01, results.perturbation_data.get_item([1, 10])[-1]) + self.assertAllClose(expected_D11, results.perturbation_data.get_item([10, 10])[-1]) + self.assertAllClose(expected_D001, results.perturbation_data.get_item([1, 1, 10])[-1]) + self.assertAllClose(expected_D0001, results.perturbation_data.get_item([1, 1, 1, 10])[-1]) + self.assertAllClose(expected_D0011, results.perturbation_data.get_item([1, 1, 10, 10])[-1]) + def test_dyson_analytic_case1(self): """Analytic test of computing dyson terms. diff --git a/tools/deploy_documentation.sh b/tools/deploy_documentation.sh index c56a133c1..d9d158800 100755 --- a/tools/deploy_documentation.sh +++ b/tools/deploy_documentation.sh @@ -2,7 +2,7 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2018, 2019. +# (C) Copyright IBM 2018, 2023. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory @@ -12,7 +12,7 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -# Script for pushing the documentation to the qiskit.org repository. +# Script for pushing the documentation to the qiskit.org/ecosystem. set -e curl https://downloads.rclone.org/rclone-current-linux-amd64.deb -o rclone.deb @@ -26,7 +26,10 @@ tox -edocs echo "show current dir: " pwd -# Push to qiskit.org website +# Push to qiskit.org/ecosystem openssl aes-256-cbc -K $encrypted_rclone_key -iv $encrypted_rclone_iv -in tools/rclone.conf.enc -out $RCLONE_CONFIG_PATH -d -echo "Pushing built docs to website" +echo "Pushing built docs to qiskit.org/ecosystem" +rclone sync --progress ./docs/_build/html IBMCOS:qiskit-org-web-resources/ecosystem/dynamics + +# Push to qiskit.org/documentation rclone sync --progress ./docs/_build/html IBMCOS:qiskit-org-web-resources/documentation/dynamics diff --git a/tox.ini b/tox.ini index 6e65c3521..1efa34cc5 100644 --- a/tox.ini +++ b/tox.ini @@ -15,16 +15,18 @@ commands = stestr run {posargs} [testenv:jax] deps = -r{toxinidir}/requirements-dev.txt - jax - jaxlib - diffrax + jax<=0.4.6 + jaxlib<=0.4.6 + equinox<=0.10.3 + diffrax<=0.3.1 [testenv:lint] deps = -r{toxinidir}/requirements-dev.txt - jax - jaxlib - diffrax + jax<=0.4.6 + jaxlib<=0.4.6 + equinox<=0.10.3 + diffrax<=0.3.1 commands = black --check {posargs} qiskit_dynamics test pylint -rn -j 0 --rcfile={toxinidir}/.pylintrc qiskit_dynamics/ test/ @@ -35,13 +37,18 @@ commands = black {posargs} qiskit_dynamics test [testenv:docs] +# Editable mode breaks macOS: https://github.com/sphinx-doc/sphinx/issues/10943 +usedevelop = False deps = -r{toxinidir}/requirements-dev.txt - jax - jaxlib + jax<=0.4.6 + jaxlib<=0.4.6 diffrax commands = - sphinx-build -b html -W {posargs} docs/ docs/_build/html + sphinx-build -j auto -b html -W {posargs} docs/ docs/_build/html -[pycodestyle] -max-line-length = 100 +[testenv:docs-clean] +skip_install = true +deps = +allowlist_externals = rm +commands = rm -rf {toxinidir}/docs/stubs/ {toxinidir}/docs/_build