From 595c3c8736c3f2bb953c698c0830de22f63bb9ed Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Wed, 20 Dec 2023 16:42:04 -0500 Subject: [PATCH 01/66] Start fixing links --- doc/guide/guide-basics.rst | 9 +++- doc/guide/guide-states.rst | 99 ++++++++++++++++++++------------------ 2 files changed, 60 insertions(+), 48 deletions(-) diff --git a/doc/guide/guide-basics.rst b/doc/guide/guide-basics.rst index 072b9a939d..fa86e506ab 100644 --- a/doc/guide/guide-basics.rst +++ b/doc/guide/guide-basics.rst @@ -179,6 +179,8 @@ Therefore, QuTiP includes predefined objects for a variety of states and operato +--------------------------+----------------------------+----------------------------------------+ | Identity | ``qeye(N)`` | N = number of levels in Hilbert space. | +--------------------------+----------------------------+----------------------------------------+ +| Identity-like | ``qeye_like(qobj)`` | qobj = Object to copy dimensions from. | ++--------------------------+----------------------------+----------------------------------------+ | Lowering (destruction) | ``destroy(N)`` | same as above | | operator | | | +--------------------------+----------------------------+----------------------------------------+ @@ -326,7 +328,8 @@ For the destruction operator above: -The data attribute returns a message stating that the data is a sparse matrix. All ``Qobj`` instances store their data as a sparse matrix to save memory. To access the underlying dense matrix one needs to use the :func:`qutip.Qobj.full` function as described below. +The data attribute returns a message stating that the data is a sparse matrix. All ``Qobj`` instances store their data as a sparse matrix to save memory. +To access the underlying dense matrix one needs to use the :meth:`.Qobj.full` function as described below. .. _basics-qobj-math: @@ -429,6 +432,8 @@ Like attributes, the quantum object class has defined functions (methods) that o +-----------------+-------------------------------+----------------------------------------+ | Groundstate | ``Q.groundstate()`` | Eigenval & eigket of Qobj groundstate. | +-----------------+-------------------------------+----------------------------------------+ +| Matrix inverse | ``Q.inv()`` | Matrix inverse of the Qobj. | ++-----------------+-------------------------------+----------------------------------------+ | Matrix Element | ``Q.matrix_element(bra,ket)`` | Matrix element | +-----------------+-------------------------------+----------------------------------------+ | Norm | ``Q.norm()`` | Returns L2 norm for states, | @@ -454,6 +459,8 @@ Like attributes, the quantum object class has defined functions (methods) that o +-----------------+-------------------------------+----------------------------------------+ | Trace | ``Q.tr()`` | Returns trace of quantum object. | +-----------------+-------------------------------+----------------------------------------+ +| Conversion | ``Q.to(dtype)`` | Convert the matrix format CSR / Dense. | ++-----------------+-------------------------------+----------------------------------------+ | Transform | ``Q.transform(inpt)`` | A basis transformation defined by | | | | matrix or list of kets 'inpt' . | +-----------------+-------------------------------+----------------------------------------+ diff --git a/doc/guide/guide-states.rst b/doc/guide/guide-states.rst index 3cabf6e020..4d4efc3c53 100644 --- a/doc/guide/guide-states.rst +++ b/doc/guide/guide-states.rst @@ -17,7 +17,7 @@ In the previous guide section :ref:`basics`, we saw how to create states and ope State Vectors (kets or bras) ============================== -Here we begin by creating a Fock :func:`qutip.states.basis` vacuum state vector :math:`\left|0\right>` with in a Hilbert space with 5 number states, from 0 to 4: +Here we begin by creating a Fock :func:`.basis` vacuum state vector :math:`\left|0\right>` with in a Hilbert space with 5 number states, from 0 to 4: .. testcode:: [states] @@ -41,7 +41,7 @@ Here we begin by creating a Fock :func:`qutip.states.basis` vacuum state vector -and then create a lowering operator :math:`\left(\hat{a}\right)` corresponding to 5 number states using the :func:`qutip.destroy` function: +and then create a lowering operator :math:`\left(\hat{a}\right)` corresponding to 5 number states using the :func:`.destroy` function: .. testcode:: [states] @@ -102,7 +102,8 @@ We see that, as expected, the vacuum is transformed to the zero vector. A more [0.] [0.]] -The raising operator has in indeed raised the state `vec` from the vacuum to the :math:`\left| 1\right>` state. Instead of using the dagger ``Qobj.dag()`` method to raise the state, we could have also used the built in :func:`qutip.create` function to make a raising operator: +The raising operator has in indeed raised the state `vec` from the vacuum to the :math:`\left| 1\right>` state. +Instead of using the dagger ``Qobj.dag()`` method to raise the state, we could have also used the built in :func:`.create` function to make a raising operator: .. testcode:: [states] @@ -237,7 +238,9 @@ Notice how in this last example, application of the number operator does not giv [0.] [0.]] -Since we are giving a demonstration of using states and operators, we have done a lot more work than we should have. For example, we do not need to operate on the vacuum state to generate a higher number Fock state. Instead we can use the :func:`qutip.states.basis` (or :func:`qutip.states.fock`) function to directly obtain the required state: +Since we are giving a demonstration of using states and operators, we have done a lot more work than we should have. +For example, we do not need to operate on the vacuum state to generate a higher number Fock state. +Instead we can use the :func:`.basis` (or :func:`.fock`) function to directly obtain the required state: .. testcode:: [states] @@ -258,7 +261,7 @@ Since we are giving a demonstration of using states and operators, we have done [0.] [0.]] -Notice how it is automatically normalized. We can also use the built in :func:`qutip.num` operator: +Notice how it is automatically normalized. We can also use the built in :func:`.num` operator: .. testcode:: [states] @@ -319,7 +322,7 @@ We can also create superpositions of states: [0. ] [0. ]] -where we have used the :func:`qutip.Qobj.unit` method to again normalize the state. Operating with the number function again: +where we have used the :meth:`.Qobj.unit` method to again normalize the state. Operating with the number function again: .. testcode:: [states] @@ -338,7 +341,7 @@ where we have used the :func:`qutip.Qobj.unit` method to again normalize the sta [0. ] [0. ]] -We can also create coherent states and squeezed states by applying the :func:`qutip.displace` and :func:`qutip.squeeze` functions to the vacuum state: +We can also create coherent states and squeezed states by applying the :func:`.displace` and :func:`.squeeze` functions to the vacuum state: .. testcode:: [states] @@ -380,7 +383,7 @@ We can also create coherent states and squeezed states by applying the :func:`qu [-0.02688063-0.23828775j] [ 0.26352814+0.11512178j]] -Of course, displacing the vacuum gives a coherent state, which can also be generated using the built in :func:`qutip.states.coherent` function. +Of course, displacing the vacuum gives a coherent state, which can also be generated using the built in :func:`.coherent` function. .. _states-dm: @@ -411,7 +414,7 @@ The simplest density matrix is created by forming the outer-product :math:`\left [0. 0. 0. 0. 0.] [0. 0. 0. 0. 0.]] -A similar task can also be accomplished via the :func:`qutip.states.fock_dm` or :func:`qutip.states.ket2dm` functions: +A similar task can also be accomplished via the :func:`.fock_dm` or :func:`.ket2dm` functions: .. testcode:: [states] @@ -466,7 +469,8 @@ If we want to create a density matrix with equal classical probability of being [0. 0. 0. 0. 0. ] [0. 0. 0. 0. 0.5]] -or use ``0.5 * fock_dm(5, 2) + 0.5 * fock_dm(5, 4)``. There are also several other built-in functions for creating predefined density matrices, for example :func:`qutip.states.coherent_dm` and :func:`qutip.states.thermal_dm` which create coherent state and thermal state density matrices, respectively. +or use ``0.5 * fock_dm(5, 2) + 0.5 * fock_dm(5, 4)``. +There are also several other built-in functions for creating predefined density matrices, for example :func:`.coherent_dm` and :func:`.thermal_dm` which create coherent state and thermal state density matrices, respectively. .. testcode:: [states] @@ -503,7 +507,8 @@ or use ``0.5 * fock_dm(5, 2) + 0.5 * fock_dm(5, 4)``. There are also several oth [0. 0. 0. 0.08046635 0. ] [0. 0. 0. 0. 0.04470353]] -QuTiP also provides a set of distance metrics for determining how close two density matrix distributions are to each other. Included are the trace distance :func:`qutip.core.metrics.tracedist`, fidelity :func:`qutip.core.metrics.fidelity`, Hilbert-Schmidt distance :func:`qutip.core.metrics.hilbert_dist`, Bures distance :func:`qutip.core.metrics.bures_dist`, Bures angle :func:`qutip.core.metrics.bures_angle`, and quantum Hellinger distance :func:`qutip.core.metrics.hellinger_dist`. +QuTiP also provides a set of distance metrics for determining how close two density matrix distributions are to each other. +Included are the trace distance :func:`.tracedist`, fidelity :func:`.fidelity`, Hilbert-Schmidt distance :func:`.hilbert_dist`, Bures distance :func:`.bures_dist`, Bures angle :func:`.bures_angle`, and quantum Hellinger distance :func:`.hellinger_dist`. .. testcode:: [states] @@ -534,7 +539,7 @@ For a pure state and a mixed state, :math:`1 - F^{2} \le T` which can also be ve Qubit (two-level) systems ========================= -Having spent a fair amount of time on basis states that represent harmonic oscillator states, we now move on to qubit, or two-level quantum systems (for example a spin-1/2). To create a state vector corresponding to a qubit system, we use the same :func:`qutip.states.basis`, or :func:`qutip.states.fock`, function with only two levels: +Having spent a fair amount of time on basis states that represent harmonic oscillator states, we now move on to qubit, or two-level quantum systems (for example a spin-1/2). To create a state vector corresponding to a qubit system, we use the same :func:`.basis`, or :func:`.fock`, function with only two levels: .. testcode:: [states] @@ -547,7 +552,7 @@ Now at this point one may ask how this state is different than that of a harmoni vac = basis(2, 0) -At this stage, there is no difference. This should not be surprising as we called the exact same function twice. The difference between the two comes from the action of the spin operators :func:`qutip.sigmax`, :func:`qutip.sigmay`, :func:`qutip.sigmaz`, :func:`qutip.sigmap`, and :func:`qutip.sigmam` on these two-level states. For example, if ``vac`` corresponds to the vacuum state of a harmonic oscillator, then, as we have already seen, we can use the raising operator to get the :math:`\left|1\right>` state: +At this stage, there is no difference. This should not be surprising as we called the exact same function twice. The difference between the two comes from the action of the spin operators :func:`.sigmax`, :func:`.sigmay`, :func:`.sigmaz`, :func:`.sigmap`, and :func:`.sigmam` on these two-level states. For example, if ``vac`` corresponds to the vacuum state of a harmonic oscillator, then, as we have already seen, we can use the raising operator to get the :math:`\left|1\right>` state: .. testcode:: [states] @@ -579,7 +584,7 @@ At this stage, there is no difference. This should not be surprising as we call [[0.] [1.]] -For a spin system, the operator analogous to the raising operator is the sigma-plus operator :func:`qutip.sigmap`. Operating on the ``spin`` state gives: +For a spin system, the operator analogous to the raising operator is the sigma-plus operator :func:`.sigmap`. Operating on the ``spin`` state gives: .. testcode:: [states] @@ -609,7 +614,7 @@ For a spin system, the operator analogous to the raising operator is the sigma-p [[0.] [0.]] -Now we see the difference! The :func:`qutip.sigmap` operator acting on the ``spin`` state returns the zero vector. Why is this? To see what happened, let us use the :func:`qutip.sigmaz` operator: +Now we see the difference! The :func:`.sigmap` operator acting on the ``spin`` state returns the zero vector. Why is this? To see what happened, let us use the :func:`.sigmaz` operator: .. testcode:: [states] @@ -669,7 +674,7 @@ Now we see the difference! The :func:`qutip.sigmap` operator acting on the ``sp [[ 0.] [-1.]] -The answer is now apparent. Since the QuTiP :func:`qutip.sigmaz` function uses the standard z-basis representation of the sigma-z spin operator, the ``spin`` state corresponds to the :math:`\left|\uparrow\right>` state of a two-level spin system while ``spin2`` gives the :math:`\left|\downarrow\right>` state. Therefore, in our previous example ``sigmap() * spin``, we raised the qubit state out of the truncated two-level Hilbert space resulting in the zero state. +The answer is now apparent. Since the QuTiP :func:`.sigmaz` function uses the standard z-basis representation of the sigma-z spin operator, the ``spin`` state corresponds to the :math:`\left|\uparrow\right>` state of a two-level spin system while ``spin2`` gives the :math:`\left|\downarrow\right>` state. Therefore, in our previous example ``sigmap() * spin``, we raised the qubit state out of the truncated two-level Hilbert space resulting in the zero state. While at first glance this convention might seem somewhat odd, it is in fact quite handy. For one, the spin operators remain in the conventional form. Second, when the spin system is in the :math:`\left|\uparrow\right>` state: @@ -689,14 +694,14 @@ While at first glance this convention might seem somewhat odd, it is in fact qui the non-zero component is the zeroth-element of the underlying matrix (remember that python uses c-indexing, and matrices start with the zeroth element). The :math:`\left|\downarrow\right>` state therefore has a non-zero entry in the first index position. This corresponds nicely with the quantum information definitions of qubit states, where the excited :math:`\left|\uparrow\right>` state is label as :math:`\left|0\right>`, and the :math:`\left|\downarrow\right>` state by :math:`\left|1\right>`. -If one wants to create spin operators for higher spin systems, then the :func:`qutip.jmat` function comes in handy. +If one wants to create spin operators for higher spin systems, then the :func:`.jmat` function comes in handy. .. _states-expect: Expectation values =================== -Some of the most important information about quantum systems comes from calculating the expectation value of operators, both Hermitian and non-Hermitian, as the state or density matrix of the system varies in time. Therefore, in this section we demonstrate the use of the :func:`qutip.expect` function. To begin: +Some of the most important information about quantum systems comes from calculating the expectation value of operators, both Hermitian and non-Hermitian, as the state or density matrix of the system varies in time. Therefore, in this section we demonstrate the use of the :func:`.expect` function. To begin: .. testcode:: [states] @@ -721,7 +726,7 @@ Some of the most important information about quantum systems comes from calculat np.testing.assert_almost_equal(expect(c, cat), 0.9999999999999998j) -The :func:`qutip.expect` function also accepts lists or arrays of state vectors or density matrices for the second input: +The :func:`.expect` function also accepts lists or arrays of state vectors or density matrices for the second input: .. testcode:: [states] @@ -749,9 +754,9 @@ The :func:`qutip.expect` function also accepts lists or arrays of state vectors [ 0.+0.j 0.+1.j -1.+0.j 0.-1.j] -Notice how in this last example, all of the return values are complex numbers. This is because the :func:`qutip.expect` function looks to see whether the operator is Hermitian or not. If the operator is Hermitian, then the output will always be real. In the case of non-Hermitian operators, the return values may be complex. Therefore, the :func:`qutip.expect` function will return an array of complex values for non-Hermitian operators when the input is a list/array of states or density matrices. +Notice how in this last example, all of the return values are complex numbers. This is because the :func:`.expect` function looks to see whether the operator is Hermitian or not. If the operator is Hermitian, then the output will always be real. In the case of non-Hermitian operators, the return values may be complex. Therefore, the :func:`.expect` function will return an array of complex values for non-Hermitian operators when the input is a list/array of states or density matrices. -Of course, the :func:`qutip.expect` function works for spin states and operators: +Of course, the :func:`.expect` function works for spin states and operators: .. testcode:: [states] @@ -797,8 +802,8 @@ in two copies of that Hilbert space, [Hav03]_, [Wat13]_. This isomorphism is implemented in QuTiP by the -:obj:`~qutip.superoperator.operator_to_vector` and -:obj:`~qutip.superoperator.vector_to_operator` functions: +:obj:`.operator_to_vector` and +:obj:`.vector_to_operator` functions: .. testcode:: [states] @@ -842,7 +847,7 @@ This isomorphism is implemented in QuTiP by the np.testing.assert_almost_equal((rho - rho2).norm(), 0) -The :attr:`~qutip.Qobj.type` attribute indicates whether a quantum object is +The :attr:`.Qobj.type` attribute indicates whether a quantum object is a vector corresponding to an operator (``operator-ket``), or its Hermitian conjugate (``operator-bra``). @@ -883,7 +888,7 @@ between :math:`\mathcal{L}(\mathcal{H})` and :math:`\mathcal{H} \otimes \mathcal Since :math:`\mathcal{H} \otimes \mathcal{H}` is a vector space, linear maps on this space can be represented as matrices, often called *superoperators*. -Using the :obj:`~qutip.Qobj`, the :obj:`~qutip.superoperator.spre` and :obj:`~qutip.superoperator.spost` functions, supermatrices +Using the :obj:`.Qobj`, the :obj:`.spre` and :obj:`.spost` functions, supermatrices corresponding to left- and right-multiplication respectively can be quickly constructed. @@ -893,7 +898,7 @@ constructed. S = spre(X) * spost(X.dag()) # Represents conjugation by X. -Note that this is done automatically by the :obj:`~qutip.superop_reps.to_super` function when given +Note that this is done automatically by the :obj:`.to_super` function when given ``type='oper'`` input. .. testcode:: [states] @@ -921,8 +926,8 @@ Quantum objects representing superoperators are denoted by ``type='super'``: [1. 0. 0. 0.]] Information about superoperators, such as whether they represent completely -positive maps, is exposed through the :attr:`~qutip.Qobj.iscp`, :attr:`~qutip.Qobj.istp` -and :attr:`~qutip.Qobj.iscptp` attributes: +positive maps, is exposed through the :attr:`.Qobj.iscp`, :attr:`.Qobj.istp` +and :attr:`.Qobj.iscptp` attributes: .. testcode:: [states] @@ -936,7 +941,7 @@ and :attr:`~qutip.Qobj.iscptp` attributes: True True True In addition, dynamical generators on this extended space, often called -*Liouvillian superoperators*, can be created using the :func:`~qutip.superoperator.liouvillian` function. Each of these takes a Hamiltonian along with +*Liouvillian superoperators*, can be created using the :func:`.liouvillian` function. Each of these takes a Hamiltonian along with a list of collapse operators, and returns a ``type="super"`` object that can be exponentiated to find the superoperator for that evolution. @@ -1001,8 +1006,8 @@ convention, J(\Lambda) = (\mathbb{1} \otimes \Lambda) [|\mathbb{1}\rangle\!\rangle \langle\!\langle \mathbb{1}|]. -In QuTiP, :math:`J(\Lambda)` can be found by calling the :func:`~qutip.superop_reps.to_choi` -function on a ``type="super"`` :obj:`~qutip.Qobj`. +In QuTiP, :math:`J(\Lambda)` can be found by calling the :func:`.to_choi` +function on a ``type="super"`` :obj:`.Qobj`. .. testcode:: [states] @@ -1042,7 +1047,7 @@ function on a ``type="super"`` :obj:`~qutip.Qobj`. [0. 0. 0. 0.] [1. 0. 0. 1.]] -If a :obj:`~qutip.Qobj` instance is already in the Choi :attr:`~qutip.Qobj.superrep`, then calling :func:`~qutip.superop_reps.to_choi` +If a :obj:`.Qobj` instance is already in the Choi :attr:`.Qobj.superrep`, then calling :func:`.to_choi` does nothing: .. testcode:: [states] @@ -1061,8 +1066,8 @@ does nothing: [0. 1. 1. 0.] [0. 0. 0. 0.]] -To get back to the superoperator representation, simply use the :func:`~qutip.superop_reps.to_super` function. -As with :func:`~qutip.superop_reps.to_choi`, :func:`~qutip.superop_reps.to_super` is idempotent: +To get back to the superoperator representation, simply use the :func:`.to_super` function. +As with :func:`.to_choi`, :func:`.to_super` is idempotent: .. testcode:: [states] @@ -1116,7 +1121,7 @@ we have that = \sum_i |A_i\rangle\!\rangle \langle\!\langle A_i| = J(\Lambda). The Kraus representation of a hermicity-preserving map can be found in QuTiP -using the :func:`~qutip.superop_reps.to_kraus` function. +using the :func:`.to_kraus` function. .. testcode:: [states] @@ -1219,9 +1224,9 @@ using the :func:`~qutip.superop_reps.to_kraus` function. [[0. 0. ] [0. 0.70710678]]] -As with the other representation conversion functions, :func:`~qutip.superop_reps.to_kraus` -checks the :attr:`~qutip.Qobj.superrep` attribute of its input, and chooses an appropriate -conversion method. Thus, in the above example, we can also call :func:`~qutip.superop_reps.to_kraus` +As with the other representation conversion functions, :func:`.to_kraus` +checks the :attr:`.Qobj.superrep` attribute of its input, and chooses an appropriate +conversion method. Thus, in the above example, we can also call :func:`.to_kraus` on ``J``. .. testcode:: [states] @@ -1285,7 +1290,7 @@ all operators :math:`X` acting on :math:`\mathcal{H}`, where the partial trace is over a new index that corresponds to the index in the Kraus summation. Conversion to Stinespring -is handled by the :func:`~qutip.superop_reps.to_stinespring` +is handled by the :func:`.to_stinespring` function. .. testcode:: [states] @@ -1373,7 +1378,7 @@ the :math:`\chi`-matrix representation, where :math:`\{B_\alpha\}` is a basis for the space of matrices acting on :math:`\mathcal{H}`. In QuTiP, this basis is taken to be the Pauli basis :math:`B_\alpha = \sigma_\alpha / \sqrt{2}`. Conversion to the -:math:`\chi` formalism is handled by the :func:`~qutip.superop_reps.to_chi` +:math:`\chi` formalism is handled by the :func:`.to_chi` function. .. testcode:: [states] @@ -1414,9 +1419,9 @@ the :math:`\chi_{00}` element: Here, the factor of 4 comes from the dimension of the underlying Hilbert space :math:`\mathcal{H}`. As with the superoperator and Choi representations, the :math:`\chi` representation is -denoted by the :attr:`~qutip.Qobj.superrep`, such that :func:`~qutip.superop_reps.to_super`, -:func:`~qutip.superop_reps.to_choi`, :func:`~qutip.superop_reps.to_kraus`, -:func:`~qutip.superop_reps.to_stinespring` and :func:`~qutip.superop_reps.to_chi` +denoted by the :attr:`.Qobj.superrep`, such that :func:`.to_super`, +:func:`.to_choi`, :func:`.to_kraus`, +:func:`.to_stinespring` and :func:`.to_chi` all convert from the :math:`\chi` representation appropriately. Properties of Quantum Maps @@ -1425,7 +1430,7 @@ Properties of Quantum Maps In addition to converting between the different representations of quantum maps, QuTiP also provides attributes to make it easy to check if a map is completely positive, trace preserving and/or hermicity preserving. Each of these attributes -uses :attr:`~qutip.Qobj.superrep` to automatically perform any needed conversions. +uses :attr:`.Qobj.superrep` to automatically perform any needed conversions. In particular, a quantum map is said to be positive (but not necessarily completely positive) if it maps all positive operators to positive operators. For instance, the @@ -1451,7 +1456,7 @@ with negative eigenvalues. Complete positivity addresses this by requiring that a map returns positive operators for all positive operators, and does so even under tensoring with another map. The Choi matrix is very useful here, as it can be shown that a map is completely positive if and only if its Choi matrix -is positive [Wat13]_. QuTiP implements this check with the :attr:`~qutip.Qobj.iscp` +is positive [Wat13]_. QuTiP implements this check with the :attr:`.Qobj.iscp` attribute. As an example, notice that the snippet above already calculates the Choi matrix of the transpose map by acting it on half of an entangled pair. We simply need to manually set the ``dims`` and ``superrep`` attributes to reflect the @@ -1477,7 +1482,7 @@ That is, :math:`\Lambda(\rho) = (\Lambda(\rho))^\dagger` for all :math:`\rho` su :math:`\rho = \rho^\dagger`. To see this, we note that :math:`(\rho^{\mathrm{T}})^\dagger = \rho^*`, the complex conjugate of :math:`\rho`. By assumption, :math:`\rho = \rho^\dagger = (\rho^*)^{\mathrm{T}}`, though, such that :math:`\Lambda(\rho) = \Lambda(\rho^\dagger) = \rho^*`. -We can confirm this by checking the :attr:`~qutip.Qobj.ishp` attribute: +We can confirm this by checking the :attr:`.Qobj.ishp` attribute: .. testcode:: [states] @@ -1492,7 +1497,7 @@ We can confirm this by checking the :attr:`~qutip.Qobj.ishp` attribute: Next, we note that the transpose map does preserve the trace of its inputs, such that :math:`\operatorname{Tr}(\Lambda[\rho]) = \operatorname{Tr}(\rho)` for all :math:`\rho`. -This can be confirmed by the :attr:`~qutip.Qobj.istp` attribute: +This can be confirmed by the :attr:`.Qobj.istp` attribute: .. testcode:: [states] From 1227bc2e7f86adabdb6804ccc13717a2361e719c Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Thu, 21 Dec 2023 15:01:52 -0500 Subject: [PATCH 02/66] Fix links to apidoc Remove reference to Bloch3D Remove page on parfor Add section about data layer Replace quantum control section with link to qutip-qtrl documentation --- doc/apidoc/classes.rst | 3 - doc/apidoc/functions.rst | 2 +- doc/biblio.rst | 19 ++-- doc/guide/figures/bloch3d+data.png | Bin 49982 -> 0 bytes doc/guide/figures/bloch3d+points.png | Bin 55538 -> 0 bytes doc/guide/figures/bloch3d-blank.png | Bin 46765 -> 0 bytes doc/guide/guide-basics.rst | 42 +++++++-- doc/guide/guide-bloch.rst | 110 ++---------------------- doc/guide/guide-control.rst | 66 +------------- doc/guide/guide-correlation.rst | 18 ++-- doc/guide/guide-measurement.rst | 8 +- doc/guide/guide-parfor.rst | 119 ------------------------- doc/guide/guide-piqs.rst | 8 +- doc/guide/guide-random.rst | 48 +++++------ doc/guide/guide-saving.rst | 9 +- doc/guide/guide-steady.rst | 12 +-- doc/guide/guide-super.rst | 17 ++-- doc/guide/guide-tensor.rst | 124 +++++---------------------- doc/guide/guide-visualization.rst | 7 +- doc/guide/guide.rst | 3 +- doc/guide/heom/intro.rst | 2 +- 21 files changed, 145 insertions(+), 472 deletions(-) delete mode 100644 doc/guide/figures/bloch3d+data.png delete mode 100644 doc/guide/figures/bloch3d+points.png delete mode 100644 doc/guide/figures/bloch3d-blank.png delete mode 100644 doc/guide/guide-parfor.rst diff --git a/doc/apidoc/classes.rst b/doc/apidoc/classes.rst index 19f0ed685b..ecd0cd7a97 100644 --- a/doc/apidoc/classes.rst +++ b/doc/apidoc/classes.rst @@ -31,9 +31,6 @@ Bloch sphere .. autoclass:: qutip.bloch.Bloch :members: -.. autoclass:: qutip.bloch3d.Bloch3d - :members: - Distributions ------------- diff --git a/doc/apidoc/functions.rst b/doc/apidoc/functions.rst index c54628f1c4..220ce0c62d 100644 --- a/doc/apidoc/functions.rst +++ b/doc/apidoc/functions.rst @@ -41,7 +41,7 @@ Random Operators and States --------------------------- .. automodule:: qutip.random_objects - :members: rand_dm, rand_herm, rand_ket, rand_stochastic, rand_unitary, rand_super, rand_super_bcsz + :members: rand_dm, rand_herm, rand_ket, rand_stochastic, rand_unitary, rand_super, rand_super_bcsz, rand_kraus_map Superoperators and Liouvillians diff --git a/doc/biblio.rst b/doc/biblio.rst index 248e492fa4..1bdabce891 100644 --- a/doc/biblio.rst +++ b/doc/biblio.rst @@ -1,5 +1,5 @@ .. _biblo: - + Bibliography ============ @@ -14,7 +14,7 @@ Bibliography .. The trick with |text|_ is to get an italic link, and is described in the Docutils FAQ at https://docutils.sourceforge.net/FAQ.html#is-nested-inline-markup-possible. - + .. |theory-qi| replace:: *Theory of Quantum Information* .. _theory-qi: https://cs.uwaterloo.ca/~watrous/TQI-notes/ @@ -39,10 +39,10 @@ Bibliography .. [WBC11] C. Wood, J. Biamonte, D. G. Cory, *Tensor networks and graphical calculus for open quantum systems*. :arxiv:`1111.6950` - + .. [dAless08] D. d’Alessandro, *Introduction to Quantum Control and Dynamics*, (Chapman & Hall/CRC, 2008). - + .. [Byrd95] R. H. Byrd, P. Lu, J. Nocedal, and C. Zhu, *A Limited Memory Algorithm for Bound Constrained Optimization*, SIAM J. Sci. Comput. **16**, 1190 (1995). :doi:`10.1137/0916069` @@ -51,19 +51,16 @@ Bibliography .. [Lloyd14] S. Lloyd and S. Montangero, *Information theoretical analysis of quantum optimal control*, Phys. Rev. Lett. **113**, 010502 (2014). :doi:`10.1103/PhysRevLett.113.010502` - + .. [Doria11] P. Doria, T. Calarco & S. Montangero, *Optimal Control Technique for Many-Body Quantum Dynamics*, Phys. Rev. Lett. **106**, 190501 (2011). :doi:`10.1103/PhysRevLett.106.190501` - + .. [Caneva11] T. Caneva, T. Calarco, & S. Montangero, *Chopped random-basis quantum optimization*, Phys. Rev. A **84**, 022326 (2011). :doi:`10.1103/PhysRevA.84.022326` - + .. [Rach15] N. Rach, M. M. Müller, T. Calarco, and S. Montangero, *Dressing the chopped-random-basis optimization: A bandwidth-limited access to the trap-free landscape*, Phys. Rev. A. **92**, 062343 (2015). :doi:`10.1103/PhysRevA.92.062343` -.. [DYNAMO] - S. Machnes, U. Sander, S. J. Glaser, P. De Fouquieres, A. Gruslys, S. Schirmer, and T. Schulte-Herbrueggen, *Comparing, Optimising and Benchmarking Quantum Control Algorithms in a Unifying Programming Framework*, Phys. Rev. A. **84**, 022305 (2010). :arxiv:`1011.4874` - .. [Wis09] Wiseman, H. M. & Milburn, G. J. *Quantum Measurement and Control*, (Cambridge University Press, 2009). @@ -76,4 +73,4 @@ Bibliography B. Donvil, P. Muratore-Ginanneschi, *Quantum trajectory framework for general time-local master equations*, Nat Commun **13**, 4140 (2022). :doi:`10.1038/s41467-022-31533-8`. .. [Abd19] - M. Abdelhafez, D. I. Schuster, J. Koch, *Gradient-based optimal control of open quantum systems using quantumtrajectories and automatic differentiation*, Phys. Rev. A **99**, 052327 (2019). :doi:`10.1103/PhysRevA.99.052327`. \ No newline at end of file + M. Abdelhafez, D. I. Schuster, J. Koch, *Gradient-based optimal control of open quantum systems using quantumtrajectories and automatic differentiation*, Phys. Rev. A **99**, 052327 (2019). :doi:`10.1103/PhysRevA.99.052327`. diff --git a/doc/guide/figures/bloch3d+data.png b/doc/guide/figures/bloch3d+data.png deleted file mode 100644 index 4214368d3b3121e9dab7fd055ec60a139437473a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 49982 zcmdpdg;!PG7cC6}N=ivyE+NvOAl=>4-6c}Oy>v={rW&XBo$(U6_e82)Si?<$oZnB_ooS`Cu)=-RiR1w-C&GBDi5xIc-{b|s%IV} zYQD}MTJ0|}Kg9seUFm~V8~(aGA+%_FrN>DTXB%9%f*URkufLi#nb8}+souV1@kW}F z5`!z78|#rD#{aI3C`LFsI(q2ScmJRNmOK8y#>mL~RP1u`ZG~oGyZc(0-A%?b1J}i7 zX*uwRp#A!Ri$HS>j2raIg#-FOv{+cIVFZ-iyz~w}hb`Ofx0^g?CZ8=jg8u&5xy#EA z>6DNf6yD7?wVzMF(j-aYUB)gamB}O#=5F@f{GQcuyO|X9rzDHp ztUGgn-hcnk&XmQ!(<~m-=FS(^nq5~Iigewcl)~I-fhLAeVsF8k<};rNxd!apFo*{J z71X@Mz)*Y?wuqgG4c3Z|!@yo!C=vL#gyHV@Xs!^C`8RguA7!dpoK2NGam;JwHD;wX>WF3qp(FX8PN7|hbi$W9MMNfY`PH;8cw z|8eolZ?~tz6V6zD1hkLwJSmCYd%$hr8~q7;bG1F4_wxf1tY3UWnbD;WF)+eCc7Cvs zNHV*BVU%IQ#`?Sd9jyGbX^Yp+53sJ7-ykGq<{=3oolO5rFMOOk2S_m8nL1kfZZrydnfIKR}Wsevz%DLYnQ07gZG!64FU7lZ^bozd=^KB4GN4 zN9z!cgThcGJb>j!t4+~1S6ETWk>f{1D`ncb5aL8gGrMy#x_X7mM!}E|5B2r*=DZ<4 zyv^p$^{eIOVv7eGIEOtMiC5LrG%9+6UZ}sC^VZWl+3W7cV1_HFMzfRS4>f#Z$0xB8 zyk-1oVd>1s2@efdte(M$fs4@T>8Vfk-X|!8`27ZvsPEkKIy2d zovy_WrLtIM2QS*$UTj|bDk<11>Gk{aswyk0s;ce}vw{!BLwUx$ZQi)K4L67h2=tg-5~f0}*v;nDr?8GxQ^^T& zFhiwysp9*rQXa-aI@jtl^F+LzVXzle zgw}R6mwM#HR1ZJ^`Z3Ms;`$&Yn`gf2Q_+LG@s5J{`A~BF(UB23NQy=U86K9ck^Ku{ zk}icqFA)I&0WC{3JXSgAF#m?vl;DX#X0mb3tZP&5=$7bF+ga9i$8Fb7B8i5Zl;D%l zV3MSPv>{vm9B8o=BQbu2O4*P7eaUD!mf|sw{s5r)ORY3pqn^1zOSmvP{H1& zitD4Gpn#+(F%n1jS%c?YsR+{+>~B&KQ(;-T;>E{W*MD^Sd9ctF^KsHqij=9?OC*1I zQzZY9T)YxA4DE4&Y*8JVHk33cH+N%(%53gokLCJSO(jOv+m{4(QwaBwg>KK_bFtQ_Uit^VEQFcnH2*+oGlnob|>=;1*! z^;y4qVPPRbfw^!Te5;YMF_=icagBnU*6w3@@IW#-9(OIU?^nq{R_av#@@(;ITXqz* z{K85alaoUY(b4mEaKJsq7EX*Vo@BRyW%tZDKmIG4eyC9q?xVPyvv5T{La;zxF7D!M zl&PneVb?NS6|vou;tXZP-A(x`Id-?PtuIL#BlqDV>q$M!+KbwyXlD3nB6WtXE4U?m9w6qZSzr;{L&nJvNN$ObJSwpuiN)h!vp?g-obVab^w62?_~O#`R^VBr?>!gDFp6!m)##*hgQnFM*9_?&)mFk_D zj(-lpW*ix|Mqtlpvz3zeP9qH8mz9<2a`Y8qI9~6~`ZHAG>S1hHDoX zkE%2zqV$8DSuwYE?u*yC9hW^THyI2!8P@f9H*|Z_ZRX>){I6fXZvA|8cvw}{+Va1v zwe>rDK~uADu>(b0M6Ji+e6|uaGb`)tTI?MomLI=3{z^9_6rS$fSmi`RfslhR{h|r= zkTyKTfEP_VI=I;EMpE*^D2lUl_IH+11FQu|IYfcP}OG#A!7$ zHzOjjrv$-L*s~LoN9}W%I>2hH?{9~6G^5t?aie!!8jVkU@)*dnK2JJ^G~Be|uJ#tq zQ$>dEb8V%ADuoST7b65jH$kC9zdE#1dLkr$R@yy&3NsXq(An_!hi(}14ORc)A$Pi6 z0Hwvyre}hJ4GczcU7VQrh&MccXfN}>$bQW}h-|G}jEU~-F)M!brYg7N;=tnOlHu+$ zMy?ptmNVaG5w0R05L!81jttKh+Fw3+&naRnmVZPPKFh#QFH z_)lZrCh$||*9-7W|F_+`4AiFL6-o|ZPj>RFsz4ge%;;qqJrtY|-M+8gF1-JFw&b3* zPCkg`n8~hEN<1WI1&Nqle;s_b)4^06AdwWJ`kN{K4K?qzPUROQlHi#{jv6-?7wTz2 zblo>5f;nDL!q**BHx@zkm!EKkrw`i=tD?0V*3LawPlGOL4?30L4y6|%tM5ly8~lCX&*Inz zY8#DfP%gWziLqvx6F$2UZEla7{;i%eeESElh4+{_X{7!j?Tap!V}j(cE0HrjWel`F zA+z|`nWa7@hrxy6U1x3NeA|HO^gePfPKvRvwKdjIbKRC*Eu%&D~_v2DT~2ixeFKS&))+Q2EW)Nyao|NZo8iZ`~fuyFge03KemZ1N2sMpfSS znXlPg3d6X7w?yIirsrUj(X`*O%Rwi5Dx>cI zZ2wF5YhSdkZ*Fd$PjbjnL|2(!UN#4@Xf&R5^z!xf1$#w%H*v+TL9}YBZr(Q__SM^M zINC6#pGN(p7Tn6oNtwKq{21e;7|z)_pDGHm zKC4Ky)yCVCd$%f?j+2|48z%<|sG*7v@BYo{mZk@L*OhqeK0l2|3}u_jj&A>(P}}Z( zz!gh6Bu*C@4NX;o*njql7Lk+w{YL; zc{w@%{bx6N42(qL%$xKcnZ3;4ms-7ef7$Xfm_>|zR=>qceAIg$_P?< zAKNiRX2Sc{ zaaL4amc%m@e6s!VUQM+bGp@HAbN5%H_kUUCE2j>V<#ZHxzamqayfMved6%qUJBgbb z=->4bBjx7C#xmI05~F(UNb54@L*$W!o*n|VzqMswscURv@;1J#2pP^w?cQ>6apAhu z8gW~x#opV!i<-e)I`j?8wOdJa^Q%=4(R(LQ(<1p}2@66& zYpNY}dJDT8OP{A%B7VKhv5)SCcQYunrp7?WJhNh;eSa`cunk2qoCq;?)V-#1V9_xC~WeE%M+<%Gov z(&kH%el?#l5l%S8nEYA|IFvXtTvyujRmvzRDJNwyBcV!*lgD1A-ujo59-262bKs2X zV6%mj8#eudU0_mF4I6N~Wi^#iKk!p!q)F~3jfhTPmb^|RmnGHySTU&jWoCJuK(qJ- z4+mK@!*sjHn>Sn;Ds~9e(*Lfv+he{)IaI6OjK$zg<$b=uJW+{pQW6PP5@^wyfdd6z03i={@h#Y-3?c>Wea+A>Me9Y@8IJ1=PqB zM$es08KtJ`s?UZ`vtP>W$=SZQFVb2ah4t^V({63<^LoPAZgTE71#%EO1sFDFk-Ini3kv^g?l%BvNRyN*Vv z1{9Uv^o=)bu~+tgc@LoTad0{}zK@QX6BoM&u1MY7A7 zwdO8beU0)HQTyQVEz19whTNNkM#b+mfP_bDdFz^3+ywoWq8>d*^>IE?%ssYvhY$sIkg7mxeH2G82{e3^v5Wl%_YV zOZ^~Iq|a$ELp#UDH28DmV%}b+)MI*CO|-VLGj}*hhA2yaDB<=0U;M5Ga2(w3Nes0_?>%luh#6{1Oi{Em+Pu_JiPE)y*|M*RMU1mK5|bBFi#!nBD48zb%Z zb{#TB6SucwG_nHuOKqPC(5$UbTP2OzAZBxGyikaq-rjjDR9PZBKid3mt2%cfm)uvf z>h3OCk=|^f764FXvzX}R@9qB)9i0jQEL$3`e0fwlwc{?FH){X3C9kT=VJz?Dv?d}V zB0rx_f07WiIX-2etJdXUdOTbFg_k-(4;tBD#{M0o#rKWry>l;-gvAJL4fWM%8N)-x z$Gr0-ZI3P)RWtbrF_$%Lhr$)Kn@zRT0SJQcbQbsgNt+wW%^w!bu}2K)(j|>(B-;DG zgUau!fW4kkoJyW?n0@e#mHF*tGI}wwvh>ba?f=chfLqPeYcVx@_urjA(ac$u<@WaW zgw>NsA7(lX%p|h#k&yhu^5NRM9BHuEybfsBsRU46jWtcBY-@Wv;z2MWsIVot=2k~u zB1^86JR2bMzVit8br+TCD5pXPRKAL3cO61%r+81XQ_U1@VPUBD#u#Hu@( zf_8A{_v49J{GQ!Y1WgMjJeCQ0upj6;y$#8G`L^7=*3HGm6bTzu{Q!M-@mf-_7cvc( zP4*X~-@d1%$aIiaJn;#D!<+Q0m)Zl7I+gne2Q{BQLDQ72x*47UU>S{$0}A0oobYld zoN#Hw+101hu3rQ^xwD41&)75zzWhl;gv>ZA`tJqx&9(`Ml2O#{UfExW+WWhvbweY{ zT++YCzA zs-9^;abj#FtK%O$W(_Er?94@O4&fqrxVc+-kxP~gDYNK3yv8*Y?VGu%7@-Bboxt#X zO7p)Xdm5G0{|W(ixx8qPqBYx(T-1FBZJj8*oC5vb_IP2G+}Yhx@QPV+=tg>ya}y@^ zJFD)EV5~k9)_#Q8YzXLvoE!0v{?1ezfMby%;8?oo2Q4m$o8yN`M_UBS9#QFJ%t?WX z-nZegb^OlT-a%9M#7q?5H4Wb;tmX;6=6($ValEJEIn^5)n5!S@gCtB?j}mj|v9aGuyO=cq2RIC{C4_#FndKXKVTrWXLa}MCadh5YQ7c!0_N5S7}+|F)nlx@Vt_bWDtPJC|L zZw{FuU)D1J+5`pB`*tT5p4ya8IsLi$tjp2-Y@f`2#hM_p+zIS*BQvvy5i$VMxpmyZ zT1*x6tU@c_F<#07P*V?LUSAJtbW?M)&)+{z3VVQrx!VQg$KOf5=9vf-AnwX^Lc0%T zx+JpAroMQ#Ebe$s$UCkY{H22+(*n=$12Y^hM9Z6|rzUQ?_!v`HxTnVNBqaxqy8pm= z$jew$>6TvnB96CBF@Bp9#4xf&fR$K`J!z!Qn5nmSSBWO-xR;f2Xtk zw#Br?)ZEn6OV^)8Sx0XsuIn&=X?~2@S5Q4SKi^J#*R*}}bfA)n>TMsw`2210i(GXj5gVcTN`v{YC>MxK&)u`b zs1Q^T+boMaeA*`DrlXLt=gCn3E)h^^Uj*V(}tV)-8;zG?Czc0UQ}Rdgr?{ z)iACI9O|P)slo+Y{-?^MDmdqRG*pB*2KxFF$3*|;)7Uh*d3Z_!y1{{0J#!jl8rnky zR`|$q=>gB;P#PO0B_)fpOgz?ass}uGyZ6eerF$YYTQBkX<5ALk8+I^=Op>_L0e9o; zZ}GtcE?Gy32|E`fu>PaZ(uYUYeEG->>KLfm=ibz1c000T12#`I(l$*#KED@*y zQ3Q>~2Y=7^06F{!3nMNrE=Epf_oY}$?~!%gt0cbR%?2x~#F0l&ab#O+iI=xWjz-M! z3^DNs?%LuDmxkK^8q^w3sh63v;(V%EQ9{mOwwbGB%l|InxMYenljQ8?)jG(g|7CvO;l~lYoC5Q@Yv-g zvIXgzH9GbW8^`z6!Eetnwc0U4k`%ezM)e@qPnNBe8%u?LJu;3kI2?icwE2qaXxV_0 z-u;VA#LCi=yixG5#Br@ksxCYEeA96v;DG=B_2bSu^KcC9!2#jIwuGfy85tQ#w?37z z&JN;FM#X$IKj1*K>{3r&KH2$@S^^R^Il0 z=mLcjcRLpMZb5gtyX5y}q5U_}Cn+Jt?1QFydXKp&*1Ih&y>8590Q+aXws$V_?Nerv zd*bhs?yqd5F}Mr?$g~~l7X=WJ?qo_TdNVB(kAYcvw_3(UuR)>(A>a zL?mhU&bthXPITTx{>_T|)%C;3=#uE4Nk9+rS0Ks2R|2!50c8xd4tdBPNd)61rUC*r zVvhhP9;01cS7P8dUQrTIf2ykdF7}OXV;itIJNw&^yi)zq7ZSt ze^x8r(cTt>boWQLaLGgQQ_0g0(`oUR^?X*jJu5D+?^0$I=_Rgtv42z2Uqv)t8JFqs z5tdl=ceK5qH_Tp=h-U6Uz##Nd-oD8mkm+XvPN_EJL-uQd&e@uG)lbU&c0^cFEs>3}=9i#_sV|!K>S`=sc@7+j^3ShcZQ4jKor}XN$_2+*iRO zEv3qJxU&S*$?Rseo8lal0aTR(XJw?-e{6-EZtYy|C@UFise%t5@%lfq6*DxXPZ^0s zK1omrG{`y-V!;YA`7N9~{4b5?mJFZxshohENAYhuPm9q{K}T5~+n$VYPc}#?C41~T z`%=7*2+Z(m$^R*z;yl9})=5LZFgS7v0Pt3ma6pnJM;h1YgVn7_1PhP|6XQkgcA<@! z*XYOKas4)=Mq?N;=g9cJ$$$Hiw`@|xw|FEm-e~uwHVUTbRt0a?baF>)b~pp%PUu~i z2L%us1BJ3=3GI$dRLG&E4}#HCZYQrFlukK)YR)vqPKDO53*0I|_|?fCkK#v!;e|>X zgJ__kA!J7xArk2k2OIgQ81}5;KI?j24s3he1&wsnsMR9qsfATN>O__!qk$I*N6<+}t+@+R#8p=9>ahLTxH7-!-Dr4HeRf#CJZuPY9?N@)8N`#JQo=3(Th)>Mu1LhA9~A<6 zxi8bhrgl$I7PzW1WjZ8JKPh5>wLno0 z0?@P%Y}0~$rPkpkSKoXQJN^|Ac9)wydf;E3 zB`vFxSB((f0az#b8RhjpA$3r|4|E+xF@Og?0HDs4ySgfN&RqX1WsNAxwdZH9sHiCT z+&U@d_WZ4k8|;k&_fH-&Hxmu-f0A{PWX(`&?b+ARwoJ63{^(!ljK$r5Op3{xba~RDFl&!N4;N?CrUL@_9Sv3B?@Dgtk;^4JUmS} zevdd68qs65WgT%w@h-6eE3t`v-4wfn!4uE3P`Goysw#8XZc30#AI6cf>m>q+5ZtLv z&zDA-nt8(YZZAX;TYo2;;KNG{vMtl|9;oG`k|6a@e-B=2by5@fkP=1pMEeE!xZXM zFc=J|+SyCP2yqCaq=S?MW#WE(v)6jaqifIT9z%NuUCFXOT#kj&+PrKn!(6$+rLHD9 zP?~!C_AM}lBw0}b#*iETN3>iLH_Im*hVF$*IO^sEG>(+FL5?6Z!z`T%e`smTFS*+F z_*|NY5VwhHUEBd13)v4WPRshF!%%U0SQz6{k+=OyR zQ7BZ9#5iCCRLTIQRB!l27v0Mi=E&UoxlqLW`v#^Mg)|O$wrGzhLm>aER)KQvXnDC4 zEV4~@^n$6Zom(p?(udh9J_g??u>x>du}_j%IEHQ*uycs(4S`BpQ(Ido85}e(>r=}Q zBvgV3jAAIDmq2@u4=4N?$nO&X+tN)SoAsKo=g&f!)PXG|ucM-{l z*-4$J1Z)cQ-ptHQ)dsv%`9?T&zf_~*RgM1J7TQ*0e_NQP zl~s{0N9c+X8?(L%-Eud1T0@ene% zM&nPd6-#XIo_{jrl!@532&cUc1oj2`a{X$S-3|s}td=CQ8hxhn_lIn>{@Fy(Z>T6V}aNr3&In^@TfFr}W2B+s`j@mlvm7ZQIG(F_zO3=|GC_RMLZh-8R016%z8XHGI z5+0{?!%E%AnCz6G*&HAFAYBb9=(RKp#CGhkSg%f9=aN*?q4<0j!hUU@`#T2DLa zYMLaUTvYJlz;3)K+pHbwo=X`*ym#(brb9ii;@-QN%x3Y8N;)5RF1H-zC7kCI|av|a4Dy1HIqD%Eh4sdq4h>n$#gn3$YfQEl zlU6kIML-0(dV74c8 zv5E-*^B5K3dXe>33siYz9cE#pKnXBNKzst|i!1ZKSyhn~TB_CBLag#*Yk9c4H z@Z3`XvZ}q^1e!;pfK^OzBvw@wh<3)?8 zTM(&}BU z%K`L6L(a@0TTuRzAMPC;borN|D-zo65IvW8g0dQ?AQ|7Mh-An?(J`|!4emQcz#(BQ zt|+{1V7D`X`s57;s$qOYD3cC|1x1eGtgm~mlj)8xw6!(;Nth^FNVum#?0 z={;uhz%vviWG2UN={SCL8(miPbD(m@*|C`hKf=~1a{0%O7x7A%L6jvgyKnX9=YD1A z3>!BKIl*jH`VI^P2vJvfuwqW~HZ^WzRMUoQr7nl$RTojVWmuM&;gjgE@8riLG1M=S zCbTR*7vIB7#+HHxYS{Ctaj~TjCD1F)^z{CevO;<@C-B1J^HDkRpR1gKM%=WJqlS~j z;?KIqB0$(9s6O~S*)CEYw`^H8{DyQhQQ47P&HZS2KUOY3<t1>UDh%tX{E=*%C;E| zlDkoUxw}O_J_zSBJ~5Q4vGW0*W4mN4yNg!^~iKu2KlF<(CTwT^3^SzYk!0+uuMmK67PZ}wUjezOR=-!I)MJ%)>5xInT;Y5-gJ?@Pb%})NP(ZVM^qPI0_5@U0?^8uEnJvl=XcENv8C?kjploY5H zfMvaqB@(x#?wVBmF7q;nHmQq?(`^k-b=1IFLZefpHE7FE0gu~s2RzpQG8Es#euZ%` z(DB1>W_p47J|)Qcfuj8G6o5rw>-Bm`7(TRtQ1#m+SX^|<)?0Bu&pu0q7JY#h&Ly6A zHZf#0TgS>~h*LEaQD`WqE@Ck(?aHxu{^T!z%2yWxH_xHofb>=r)MLD<(T{V<8zW8{ z0Cc7m6@bFacb6;U!GgT;bOuyq_WPXUXU`9*^PWey8*;W{De|AtUA7QAT;PT+Mq&O? zu>1vqlz|iU?)POzkf!J#GWDs|r}>42in2rl`9<@D{&?l{T0GD2gA6E^WyRy@C-Xk( zqKd+-s4!h@@7J>a_4uPa1GPxM z=MvPz;p-k`{K1$n!d8~ja*yqw0yl2JiV&!hj|Ryj$R0meAncL)+YW#8erG2Yx~2@g zYhP5K5kWYzhM8y6LY-KZNed?*y>0*RP;ZoC?5sNPF1}<+N}O|XKwd-Pa}Ga?Jn&A8{(11V+zIq*pbzj+lh+1K9-_Vd-?IPF$vR32(R&dkH+Guc( z4vlihWmfs;{y*baF{d;;`Feuiu2Ed(&v%JbDsL|(95frzoi8wbXoH%E=B>2tar(X9 zn@SS<3y`%K{G;qJhEk%D*Y5dAda@M*Z*PG*%p)>Nm7jyCe7-Ze7FeECuy=IhM?KOA zU0C|R$Hoi}1_GOUEl~6Ht4|(heak)Yd8_!F;0N}`iSYZd)?T^>zIyt2$CgV>wR1yG z9CX}xOtR%~$PV^SO5Xa97dGQEwg#Qz9|PWayg}>>7~6Y=FKOr09}A!FunsP4_2z72 z@=aW+S4;r_4Qyeh2$e$96QniZ0e-b)qyZz@ zV-4LczcAEt5@Yi1sG9e;k#**DH-Li!di^Bph?Q>?nZGk$@bm8*_5BMV`5EMuR6FPV zTsSIb@A{<*w&e@-(`UmzPP1!vtaqIjWVcVR*V=YqX{O^_)qNF(nKbU&V6jV>Xkz*QMB1;YnC1f_7_@orF z@vm-5dT&IxX@AwwCL}a?j0r2@z}97|E=uy06|p%Hf4JiclhwAqVIu!5$$=3&Z!`X) zbz~xqbE<Q!0jfaz?T}G&&eQi5FCV-Dn)$!<YSOwMs9Cl%3)1WhNeK3r(wxFyh6g6_1K{-YNp)^JQ|WW#SxMpCFw8!&FPZ&m6a#h zY3^RfnwGl12(`MV(-{F?c$wF$ek>54b+x92nz=o|yO%y!$6IfBq)Uo3 zg+LLJU*XNpbyuK=n3G!Z;6 z#yJce(ct1N_;G>+_FFE;cftY>@I2v|7?`W$zn-k~B9>(KFrPjNjpWu5MNtt);3~X~ zD;l{N>as&$ee4`0^id>$BXc8L;3Ox6^aC+)g2|AxJY`Rj>++cxJ?Dt7tZGFd5Z}Iux!)IE1p+PbVAYqClk)>{_r=B6&wK7osiQtg3MIsE z#pqQn*`xgqT>)Hq;2n?wTZ{vpZej6-6l$?3&u4VoQZdGxp!`d*6Aa^4+ zg?JVQ=aHf3HrC18dELa-arHl^);-bnvHN%9N*%0{=Kal%dfl@*I zHhc?gDsQ>FQ_eh)5;;rA$NM@v=s!~w4Yq$DOE^Gu$n8K!{MEu?pU5Vo8{)A8T#%b3 zRylqjqrDlBzWAP(kshdh?m~Wt8>#^>$yu!s8SJUv4Fj<;t`wy|3j+BX%!znhd0u8K zDAd}k4{Jux)djMKee*Ix6v0Qu;s0HVj3b%dI|nG)<%K{Fp)tyrw#K{`A!8$}j#n3g3+>BEJI6E;$MeoQ-YTl7lP_GgwgyRz~~*#pQF~XSQbQs=XDlp>wGWr zzU1%AC<)B>YC(%`M48A8?m+TK{#$90VrziLZ<@focN`7Eus_ z*^EEDg9J+X-hvBkr9<&}{<`b`#;0!ZqIXfao|l0+wM*O+@L{6#~C6Ru5Bclf0|AiDef zWB6pk@-h381UZ)?m)3^czg9Qg)xO(sRSX10Pp*w4 zb|1C#CnPDH`(2%|+#%D8qE=_I&KUWQ1?HE?6 z^ygh(DEcM{6VvMA#k&2p+x##flF!Es2tDiilUMfMM|M62M$;gf_&u*^T8 z@LhuQ2QUdj+Piu&U5Y)KR%_VQD}rlOxI0hotQP`1hp7`>zizxY*_Fdb`zXa5yL|z$ zwBW3KG|{RDi38xX<@1;za+APfQss-t+aYe)3A8AYUlYrdv@*x21Nd$hGzapl!W!i zGCCjepHU%$B;rk+Kj55cR*xYk)x&nSPU;sVtJX^k3&ZvZo17VsNXAQ1DbYu|S&hWs zwcZnUKg{_e&aF>lNBi;Mw;+G!kQz1T!!-p4_u$WGzZI$d$EpDL#F+~0mlMhto<05P zb}!S0h)LERuoOZW;gDkIMn5nZVlQkcA^(Ii>A(B6%_V#I>h{~|@FDI7Q*I=(57K%{ zF(CsG!)t=LecYoXDQ<6{?zLk~A~DPNTMKR-;(yZ<1+ZGkNDlV*!I`GU+C!Ds!K1CS zXp_gYBIzcl&rfLc$Mua*8kmmip%^=FJ2Mod?vFRG?G(NWHm$}Zh!*?RAeyk?!;t;G zcVZlYFIO^Y$O%Tnlnm*>?3UB(*L%Ps2_$zPhA(Ju{KRxd>PVhqqOhpdm|J!D?wNl9 z05wZ@>TG(qS}XG_6m5?FPidW2^;Vr*`Nz+n@15QQ9h+)6J&`R@w57?o z#*~}#QPhn*ZL2~4=8@8@p*N?X^SI?Hde&|`S$7wd+q>od&iY=U^ik5HpvyBN&olTw_%eBaMjLuQK+&^n#Y#WCXXsFl^T>3Os%D3 zTqM}2AJ*u<2HdB;y}hgJ=M@PM3t{q+iy{@tLs_oHz!wYR-NV#tGX>8P1%j+yUz0&= zEBVgF71#59cf zPN#-H8=q7DDi#CARJ7Cm+PLH448rZd%xmYs)|P+MgcgPr?W;Q;DDpZ`CdQZC>cmr8 zVNPmDx?GyLUSsE0CG4_1cFgFau^~GsN~g}MQ_}bKU#Vi(BujkS(zI}3%01`W#AT1f z<_;6B9a7`Ip1P#@u;*7Xb+V2xkte)MB;b}t9}}Wv$=Q!gBDtzliZjWG2DM*H%ca+d zPKfrEr{X;&QpJ@o$Vn^LcS}*+h1O5ltWtLEw==)XNTBP8ph)(K#4>9&x8+={-d*E< zNWsslD*Au%bv=>COUZS=ongxft1CC16auqp;OP&wGxLd^(NyA8;aN@7ePBeag zEhdYHfPs9ylWci!R+-kRrPKeb)rN@RW^>mencn`-DWze}DDRsTpQf+Pn>E@aP|l6tl07?wUn{2?Ce+D7oXCe{mKvvj-+;Z#*p6$*mFUWje9NRFNcvIFkmicDgoyS9YjR>OmSPGPTLES?X!K zEWW6er7A()@m+r2s$x<+$fCsdTHN$ykd%eZ3D!(Hf>FZeyuPBaV)jbiDqRj=zGYts z>l}ywt?QkHr{FU9i1mh`f_}^pTpXe`pyly&Yg$;PL!w-H{7VR}|DPA2MLuwy(Q2AO zrZK-6Z!ola%+T6$_q=to2Vml*3Hx5)qo9^P%O9ZYip`hip!P_m@A^G&^DxG*Em4T* z+goDodP6VU;E}o+XFL9!P=cAYuG3S`O5Ib3Z%G)6OaxB=wE#&0$c$tlZc%*-OhMH> zTrz`2T47;~E!$Hf?kp`v>USq>1*wmP>05LvZLRodJpg>|=tkRyQ>nN!v|e;dIOb$06P$=I1r{9+?~#oO#MP-ny^>NO?DN2v1E z%1ZVCF?ete>&9X9Tj`1^>OPmk-!(A`%Qn7cQg3j;Y~>881udABts;epQ;LLZdkV zvGvO~{-$7Cig1)bL-TJ?zhbKe?iG=jS=7EOn~MF`UTeXq{dE!;$SDm#8crWk(1`|^ z$h3#*q=3<4^I9+rxHu0cyUc6B)bDJcawpwD^rE+!!)M)0#VCsdB8QGyzbJvP`H_Tf z26~Ta;`(@jv>yN6sPqG6GlQ3>{i(LEGHwe{XMexi91W0~I|l{}GobE?Fc9>r-PBWN z!LUR=eXgzTxLo$XTnWDajp_HNlGGp*x17rP-ukhaOwdkvA0Gtn$nV1eW>c&5{=Cge zP4j$+5GRfK6IYzm`3V*JbVDna5KO<}kCK&^}?`Pb_l^FMmr+2Z3o* zwXjPJdF=`D@K6tNfdkN}1D83*_AgE&29LiszJ*e0C&o49PBihnutbP`KXW2Mi-3A$ z(_$2MdS1`9ieoK#zmHhfaDBqa3%J7>=9lQZ<_N{tHE<}tzfo@-3F=BaQeP-m`UJ*|vdUWvHY=2bT!IZTDK2U7N0v53RltpVM|&Gf~=qy=k{& z8^|8X`04Uu4bSc!kULU`N{iXAg5w2p7Iz41SQkdWe>M>)FM(_|uL5KUlY{HYetmX|4)@>2L6;oWvcWG;OLV`^T0l3p~T zOsCSbWxoMaH_NjHm=~WD&oIk9%hflsD4}g zPs;og?&KMNARy8zrX)tLZ;m=v;A*9Ayv$3d6Hf2J)Hxss8R2kR0#8NKcllX%M)pqy zGVi!d1l8X@eU5OO@TQ9&*Ud8Id?6$Re4fO>Ml!pKRv`pYJ&lXaBD1=w%ZGsJsy-rvIq zKOFVAXV2d2UU6Oj<=-uc-Eg(smV2bIges~}w9U#fk>9ke&Owj5g+owA7S1K|D{EuO z3DXTN&nDBu7T5<^Q;$J+yv3oWuI^3zMh^x4Tby%ma;NxF>(^-mD^@g-J!_uN4_~Ui z`u&l$7siPZ#h22+o(7tA;!Af2jty!+7f)&b!$yxiZSAda%DY+3w7k=|jGsEsgM68C zz2%w55r#i!oXQ?=>MnMnHs{tx@hVEDb29|*hQ{GY6xis|;o)-e9Hjd@hb9Gs!hea}Y1y(AVMU?~TBh(F$`o<@G0T7LoAfAV6RCa5u|qoQ4F8 zEt7XW9D9<@MV0ST-uw*%(nsa2La#?;MjAWr=Ld$y_T+H z0^p8I*L)BE+LWwvWEs$Lhg=1e?}BecZ@-?m2V~5A$z6DjO;o@~(CWSqCehR2ekwqe zg^vG(iQbwnx-JuvdKw#kQCmYy&z9S_7hKFbraRfqh=;+rHr5@mgZ5+;ehP|<s$&wU=Yr^-30~i1)__%(4P0D+9@JGP$Ro{r#;f~%zPTF0Hj+2Ksz zkIavz;lI-1Y`g4d34qT-@5GAm88fLfX(P`v5mD#%r|(@h7$17tH~rzpa~lJXK-vI^XcOb+f2}_d=)?{*%Yn~i>#$jv^$8iC-d0`YlbuNGr)ND55vm}YRV8xNu>c8<0GHpQ+9;8ItH2H(y$gbnQcYgv81xa37z!P#tBJ4 zjIaB(+wRXRjN5#H`Di6A;5yB#>|gxv9w{^I{ojmQ3^(m86MTEaM+3c*v{c+gS>KeP6A^ZQ`-(Yo#G?V84Li}QCD zPOOFo>DK3GA$l8exoy*toFr%*)L*bo64zs8X{j*20E5xzt48*50==|(l3IW|43|B@ z%J{0|CBR&;RJ0@xSwc@zDscXL0{qTKaf`f{kG53y7n=wj>q#M)G{L=eJ2MvvSjIU6 zl^fLX_BDCDL)LSd-4{S*AbK%lHeGHAdW;}gfS)QI46fui4|#6-T z&k8dZbmfRCe}#}Tfu?1FD)KWB&OtlD<4!njAaE)SZwlW2os+J^^JZhnsKqlO96h81 z+`-RJu4}ADz<+RhsLhN3y-L3WI27KzqQ@pdKv#lS3=yYE`tk0p8@1!%GOiE0Z%PJw ztvstiQnRrZzecmauSoz?3b0tT8bQ5I{{ie0Xv9*0Z~M82B!d)U$)WQp(+VS#KNYw< z)yvs1K>}=wfopw-l6b-JXCiTPukc(3}5$$=>T_bB8n#?NP7Es$Z zUv^G3WzFxuiPZH`rF%kjRcPbz;RDDa&AB7AGhlBZ*%k zMo-UYLOP|sZhyG#u(Ay}f5`q(ubE%}ayuKfAi%|_WEl6AD?F+mH3yhcVlP?Ve&j|P zb*MkR1~ZiV1y0UMW3P=A6V9ts(TCGDw^VPyT?(Q+zr8+9jr+(5VuIukE)2;;I-yXQ z!?a=Kwv3Fupjt+}UN|!vu>HY4^|-uO|7=sY(C3JTx=?vBZ((XGKK{!V8^9VUru~13 zyx3}Tp!P^jx7}tM!~UAnLm6;;+(WrpOAq&8Vx#SKH%7&qF#MVPw7{(8|h> z{{}N~k)p_RxfA-=!;9%JOlH4e>2~9zS30ilV3`d5KQC=2HdD>&K;$R~yLFun0RZp3DB8 zd-aD=g>LQ1O8(&2$ddA1fN$A7S7AC!61`3WA-zoNKQ0)^x0x(#uY4q5tGo3~BnmpK z-#r5@wC5YBWWWP9ZT%XfQ^&wSp=cZ^wV4S6Nb+CKNWyy5STUS<9b1mOr=j=f;Kkp5 zeEZiEWGKK*!(BR-Im5@&$2-A_EgXGwq{n4dOka+x*@@M!Wnt;VoU&?WNu*Mfwt?N@ zgdXAooKfJ|b#XaeXmn}Yq>SqUbdDC|g6#feGh1*#E6ODbEsC{>@2H_9US?La%Vrh= z&$(p{aRj7W>G(pZ&|f_M{%ETpPUL2*AVgvX0+~s zC-S(hk?(OA-;(dCN5G|r4N+O>%2&jRn+p};pD+BN*0Po*Np_a6T;u%Gi=Mu)+)T*; zf_mIgfGh*8OwbCCO^WVW6FtTkt!itF4J~I)wbVKC$fT2_N$HE-rt|nShGj6aKRCSF zbB#LUP^m8soVvhTMbEYKX=BO)Ef)pO{^#u~8Q)RI zNasc^K|BHx6`0}8bGz4^{^ZR&s``KCu^OgIwKOxMeG!%Y)}*K63tbG%{dtbE9zU3{TqGel(vVvL+tMzG6kamxR%z;AOG|) z{_$&%VlYFMz);mG)L;g_*{pCN*J=r)YO?`qE^)L>6%VW)x7cah zMXybAYu>L{{vhmZShP&T$!V{e@(MZ&jwvuupm+O@c9|F20q5GH7iOYw?9meoZIBoT zd8!XDpULQjRmRe^LR?q>L^wA_JD0rnZN8|j1^wQqg#K=BlXyf^*!*{vHQOSSX8+xM zajf}sf=rCT__8v#@z;QAcC&~nh;Vfb?ANA5p{5$8nmdG9WRWtgY=St*Z;jghsB-*o zY-l7_`d%9QCWM2e9DcPHmn-NoTwQc{!X1t8Cp|EO`mOx60_vonkD*B+_}@_8f@pr- zr|b`c$vIan+|b-qfD+=k`HtvCp^bzNBaJo3w(AYZ(XV|M4Vzu4jei^g(F}Mmy55gA zLuh}xc_=VG*7)EL2|rx zA6xu;w#Ev2)y07~`kbTX_m^^I(YrIxDB|c$nS)vHEQ2{!Xad{tSnf@6?#U%eD8FnP zN7H=ep88?eU$#PlQtQw2^Q#g)A|laor<)^(0Jd;&VASe8nR9O2UNv^$YbMYK+kpMu zNSJa1_%CpubW?dU1{)&JI-00ytby_yH55Eg67Q1X+5Iw-vD%VHVOBX_|1!WYDa zVMR{sB~(r$FE3xEV{c~G8HJ3!pX^r^ra^*m>#zH78^2tEs|tP0D55ypt{zW5kS>CpOj-DR z0$2lkN$K((>fs=bROrwZ*fvoYEaG9SG9G--`a-gz%2hx(Un8i=45vaT`PVhZn~qc=Es?3 zkrf73B^Av9E>(}uYVcn zb<)QO-ydWiB>SoTw@b+*T9XnDx$#tA$d)@5Hk>O8ZEGtp*}1v7@wp9W@PZtU+_F@>-TE*9c`(^dqn+X|_lLTLp zhdH~7V}uisqF4RS&JM#&Cw_XzEf2E_&r+N|&qH?gm>hV%%O$aymKr!0^<1Zfxdm;l zTJ%5L4)AAJo;J5S*nxSbT!|34g1+J@2-FH=u&tp1I6{cC;_9_yWjIqAX*7OXLX5$w zd42OYeW$h}CY7M(W?1^&Zo6zsQH4kYg+TsesUb?h}{ zvq}rZbZ-l(Kom45W$8MU9U=Inw_t(;e)ew^mZAzPbTW~7r_{NWArMheHj=EfGxre_@Fv0O;t}P>~5G{W*&+!?m(_7D~f^kByCiTow#FbY)trFru8mNfX(X z4N55 zfvA-sj?`3SB`*}H8`U2~U5Yg%Ix&505C-H+=>R@s-=*3DI_>T%x~xCK9~4(F{tc5^ zn5&0G*_<_w%%*BBAD)L&WzEY0y{)#%eC#BR+zyUh)yI0}ABp7@+UWIKFj;LgZMipB z3SZaaXefR09=^9lO9kY`jagsHvloXTvk4S4aTebKD(e6@Kfi3=jz5HgbR30Y=;}%b zLfSh`IRhIY#D~o8t5wj+q2giAVmFlo&nW;E9vUOq)ucouH1{Dm{@uenwvZ&6hWl8e z=pDQeE4_w8AK;*r5PgV(UPU?jpFUR9BSNQxpApx|SLn-~T1@DM-(AsEj#QIA)C1cp zI7&ZWU`@LDxw~tPzxElhe~HnI#}w(j<+rv^mxytR7qk`p1*&=I%3w}mn{Rx z3fgv2-_5+Ztju4cA>+96H+p#^?`HI&H)EH+>@(na`pLFU}NO^et!eaOqudlXH3WP9rfhz_f`+0 zD;qVtV>Ja4j*4S%4QPO12l46&Z;msm23aqM2l#QF@<6i{F2TBA~G`%sy*ivl7GWFG2=^Y|4UH zs%mOBcbHLT@E4jPhIBpU=Ht5z;qVO%H}YHwIrP75hMwy~r67+D?U8K3Fy?E>ghPEZ zjNNT}7&;6WO+PVAx;^lK4t)8qUJKtcql6Qw%WgUN*W0}^Vt?uX?~vFdm(d8Pm4I7- zjwX*B)Ut;Sqt?k4`y!*kfa?pGRje-nT8+{BYeCdDqlc!ZAu{neuR|ghLL*PLpiqjx zAv+rD4JeoZoK&QHzGZ?|u^Gj%Si@YOmnoDn*B2bpxwqYXZO5&LeutL<50~2yR)?_Q zb^ZaDV-E-6%Z%*#gLi_ZlXDW)_+j$6(ez2V7U9qMvc9oC--|bsbsk1Q46$%RyiJNP zQJ=Y`)+byRb5=88u42)fcY3NMt5%GIZ~uW+5Sc8*laE{PP--q?w(-Eghy4z|cI>r~aawc+*< z#eNHyoI=L5fo#zE1!ZHS!NKo9q(k3L!KKrjOOqK;D;w*Sd9>KKQ{#U%cWg)aJASd} zRbce$_l;$aLcGsNFFUtM-Q~iQ1fH(igiQcb*yzr>wImC6o*^!o}8h19r9wr)oVLujdf4B7DV}1D8v%wu@t6u=4Z=$lA zXTbW|W=*%FP}Yg#uFT`2wzwAR0q9+xK>$_n|G6IPB75T$OX|a(Fx*;nv>X0x9o6;}Bq~R9%&u3#&RgWWKAZqSWzFp~wF^91x@W z*fqYmtoRhJpr}Z!dt`(;lOtBJbPZzJKR)R7JoI$_Hq7H8z3pn+^;W6`?*mTl?zyr5 zzl*g5(}`FKw8k*28(Tqrzg;07Rc@hl}& z@ao`5&(1it+$UuWqMI)?%Agt}OSgkl8~!GOJ5j^P&=6pWQ;LiG7Z(2YM^4#SF#9k0 zJC=O=G`wK0DS*@X^*be2fT@>m=v~6Q@1bPwo2EUs2LPk2!W117Q|$lTlJ8{DV7B9+ z8cx)3V{vKPf8NLa$y`ZWq#I?F1H9D%$`D7N`T2{x$xEy32^zczTS$l9iJ({b0t7K3 zsaITswgk5^oS9L{Gs6DR$N0P*wQJi(?em|ns?WuE+cZR!yiRYU)Zxpho{?6Q6d!Vf4m;#El zLw#)>D(|W3$=5oGyk+OcWTmGT3oD|xouVyQt5J;x>=bd}IE8_}iOXvenCEy_&Yk1K zkoY={Ek18VB*Y?iRTx~Zd?MK!Quq`gN7c7I_?eDS^t^;x!#9jm$uo0BjU(DeL-L+a z@9LeCSxqcXn5L6^)u0#)73g5UC5!#Y52$)-#S?9Qa0SYTxvp6HOX-3Y_hwDA`D=5s z-~vot3g^mC#FaTPhfPZRmU5khj8fu#_hvv7KTmj~Lrw@jg#DYR;s80M$CV7yniC+1 z=avW4tSL{S`*v*qct29l>E_jk6Aojq5ud{6Pf-(+WT|X%HN%-94m8GT8&Bo%?B5uS zqpPO$`dwEa2HY$2sqT%+z{0Qvi;9YU?sWIb)d-X~M}m0ENz>2V6k>bcBVqCKJ#KEO@2{lxnXq< z+bk5?@7$+PE@R{3fXu<+)cm)uwFwO^E0F{D>Cn}N=2J97`9Q94Ql*?0kk*{^_s^nI z$PSG>HpMlN4Z5|u;fq=2t++=2-7TfE?7<%JDedO++GtuffgT|5zpe`L8nNM zZF^6fyusZxBYGVojfywFG6MwNc4)?J>54zp@J768Y#?#ej(yprr%0?f zv}3z5Q-!5^0^T8+SfbbKnk&mBGc+F_9EEN@|POY*3gf*mUr2F|JSAv-D=wd3OA$yugKquySa+Hor?RC zpP|!RPs+f*(sl%g25djviTLJP^0O;n-SpT2KKpssrGv|@w$YTo_h+;9{gnQ zloG1cj4u%m7~{7mO{g%)aGt%f3VPN{LxhnjBKZO#-I zi|7qN{b>vEA@Iu6%ft|T;qUQ+(+s^laV&ir-JPLv-oDPV#^!Td%x?Ppf|{llw?YJf z>H%J6W_Jncr*YkUOnyH>_%E+kBQm{74Q&U}$j!P6K8T6;4NO@?4l*Kq^!qzwj+kDO z78eqqRd0QI2EeTQ?#*%o}>Zq)0NpcT(C_iUZXXGx>t|&1>rN&~aNm zSZ_uojTpt@6G82ipWdsV)0nJhwo7aqFEyx5hSx&Db`FJ1Y-B{E3J=#ImgLkP&LKVw z2QR{l{HFdYo<5jE{lHEVMklWQ(g%4>UeBJmxVY15hBH@Ij{kyQv)Wyvnnb!OgkxgBz`2YUbMoM62E?h#SpInPXP5#o>(WEEku>#<#}Y#3a4j zbJ}vmmM#QQUCxayjg4}CJ15=kcHEt;SU0u>B`Ixx>Z)e#wXKkjSwg_mGf}3;d&@8M z3M03~_Af!#ppTxBiX~MGr_v?-?rw3Fp=7=V(PeR%+WtNp_pd|#R(l`pw3@K3BbQ7Phi$Iqul!x>- zIz!*GcSLH$>6dBsC0f+I?@1Ko_Vb?VoeGP_s#DKcY2JN>P$JPZ1?mV-P#D4Q{3nA9 z=8@Q}wr_3(ey-&#G~L}X-8`+B)To~-Ew-PuT`=H6THMxqj6V}#EQRu={LTo6c;hV@ z*47DNj8aETq}YYF^u|boDQWO)pz^DwIfHZ0r?82e|Crv@y5=MX=L>BhV(QNq!2?ZV zKO|D3e^qDH;pZ~}bN!d)KYt*#dgL}mUi6-Y2ivT?>9eJ$E(T6(*7QYUfPm< z9>tM>6(J=H3?8KmOl^Q0Rlyn9=P2T=mhrv{T#)C~1Kg}E9SZ3}_}z?ct@Uo=m#4ou zHOYfbvH;lGVsJ3Lva<5GRqCSmZkYfAr%>Z=Zi7f##ui~bChs@t_MJ)ASF5%qwobeX zkDa>y#|OAkk9+p&tfwP0ufEp{(LaH!~0#_tE z6YQ6#z6c={ru1$Dw0aW!a*f6TjoaCRQUTd)9;PzeH-<~-aVKD1R;tsHya z&|K#HRpms>Kbzia+Bg%2;G{ABKlB?we@PVsq1W(3?PU& z-<%B;Dcu$28)4evt-?ws8^V#Z%IM^D<#uN5b6hsJ78(&B6Os@I<>^rbXWuHFpO4OS zP0KExv&rZBp{C_;kUiph3T{3yBvI$9PRKM(RhjjX4}vx{Jd%#9mmENY(?5BTCkCOZ z7i%myc(A_Uxw=o!&TjQQUj2QV5Xd)aRy6~{GVli`532=(?~$PmO>x~LJF>s<(|O=c zJwBsRc5ZzQ1GQmUVN1$t!reNRZA~2vQp%{*5J7giVAeu7@muVZP|d1E#E@U3ngvmz zvCPOC4cZt82%x)g@W`2o_n%q)?aYax<}#2$6OF(=Zo)Y;4q3Iq!flhhuuP=e-kYr% z)CjB5=+WTotwBTF_Ne&K`C}75J?B0JG?fS+lS?XLQ4W`;lMu$I`6#Mcz0I{B>Z5v3 z|KJZX<{Z%_jml8+eh+HFi`L8_jBDt#ejTYf{iIw&{UrSiUmeh2MRG5%eN#8e|HRUJ zgmrT9@}>+~0vR4rjC4%HryK8HMGQ|Xgjk9=)I~(r=h1K<-GTnVKJw4^pqCXFF1B^Y z%&E6kPp__=F4cq5={eX@R-}wS`uNpF4+I8L$GxFq5<^MdL|V^+C~lpXmZta|c(IAy zADNatxkmWVd+CgU>Ey1PN;dJed6w{>K5KfWl_~DANcWeI z(fg6AlQy8qL?E3PN4xB2QqN3Fu*K=#oSlM6WH0P=v@AaP$;0no5F%3W-Ew>IpzhqE z?per6-voiczlzhkGRAbR)3yJ;I1eYKX%y9jAJXO^7cm53eX5QW;RIART;B9O|)8=S;X_ku-u;5wRcl)B*QOj6Jp-dEre0t<2Z5xS-|q zhYaJ<(!}ixQsZ0P`Wj7rkfJ>7{uBGYxj9f;8tZW6Bctk>=(yF7xXuFE0AH%e@#mMC zeX*33_6+(=ZVs`-IUtdV|FA|Ec{jAgKQ$G(bX(ZIpy59Q1e%%OO5S2(>5#D)7S8~D zD(GX$$B%X>F?fjMeANH?BVGi>v!q~jtGm1V2`2K_PB1Z^I%->zEbWkmV=Za`g}=w5 z7vmu-c{YNexsMaQ^y|qF?$3_&hW_=VM=L}1l5j4k7^vCU#oByVKf?o2JMQX(R$tY+ z?IhnN$Y|USz&>;XW#1}DOB)~H0Z+xz!aDW%${M+3_uc0=oQo+~Qr-b64iH|}&$HDS z5(;K41lG4~uszP@!T2&@T#+wBklUs7BBiV!!@Z1qH~x7vvU6`b8p5HTgXW@!sOQl# zxQz;`lQETfwzxqo?8DZCE=%_gVA)@!y!^viv9jUKbU>|q;=JYBb7dMaquIZc>~e$K zkyF^dKrq8YAL;t9iEUTlr@LgryW7>OY7D8a4oorEsRQ;%P#HyotkVgrpyD_ zlds6(B^->dx7|Bq+lLuFpi76R*M9@X&UWziCV4d4S4|a0O_NpJN*(O&2f-GvY28P; zUSfHTD?yt0A!zEr%nA~DNP^`X68;64{BBhq1SUNe{da0Bn7}hcc=u=fvJ$lo&_=UX0 z6t#MaiFA`ofF21>(TIp=L^?{F!<$tIl}Q}v{+?l-RbUIqe-c~I(KAHWWp!Gv1kRb- zcW6gjvnI-@E&7D$82opC^CZDHZe={IwnmhWIvd-W%tS=~+!6XLj^tIn&{_QSZitq; zu5JQh&QB*riG|4bD3j&s>Vi%kk_1Bj3;wrPL{_~l8Lij7v~Or4en#ZJ9y>UvYN!n|2DS|nhljj-oAaC0ou`xWV_pQFV7~uR0;IUQpb+K?inD_wYRr_ z{~pmP6)UV@^Tx#EeOMB}Uc8XPB}Ai|3|vz5o!lioyitp8{E47`_b73no0|h7p%FoO z`1>EYqZ$4+A=IOXU*NVORl*EHg>i)_S?s6tx+8NYJSlGGr=Dv?^uXa~*C z6nSlYQkA3OF(fb*;QzRMb-dPTUW)YwGaJ(ty*Z1}nCnQQ=j!V9yU6K^g%fXsp$wjs zNlo3Az8i48h((k94Rx>-P+>vJR>HUyFNC?q3Q!v+`X47N+ z${b2bsj<~ll+!O=Vd?eJ(gsUIgO@+*Pn#5#_p-Y^SbNZWs!dvHX>ZSPd7sG9;2|U6 zDKaE&ziKDFjsXU~Y7AST!Theawzl@;MTirfTy;ya-ptB$7hsuY5C6Ad5h#B^ni%uA z*`cpbPF=t{Df3u_v!>u77v>Q5^qWOY(3Gx0!JuwQW|@Gk#XnmX5*{_qQ0}To-DX(o zsQSJ4u=v*1!%An^+VNGk$Koqe{K9efIx9riE5n|m{tI3yAzp`;EYF6OS2;G^ zeuS(+TBo^1N`^u%49!hDRL;&DAsZRv6pDRJ9mi3v+iUI!=`QzUfS67 zT19s9*JEf0?pJ>)_j|R!Q^t%b@AC3%e{Q3G78!7!?ihx$U+rgbOG5mi0f6)gO#fb9 zvp)wJhz@}i9_S5L3i{tm>pU4Uw3t9rH%!&_h6rj~T+#e4sQhz&s;EGq>8-T*^Im1| z}WZdPw0wj7s=xA!tR5MqJ*cUh{D9mLQx&No#XLOXIEvzXGSU~ z&2*#lOIXL`CV00dyy^hI?o8aQ48-G}>JA)eK^Bd6rkhEENXf)MuWtPVXc9a(QnZ0; z!J*|?x&{l-`{f{D%P0g}1TA}nHKeW_`$v8C-;ob0O0HO@-*hkP&c{eq=N<0v%x@uI z`-mKki@f>UtoTaV27hIG^WFPJTSxz3lY^|!yjzWK_^=?F0U2kobtSxId!@v}0%GLZ zg!L^^0`lyt-sn;|*TsstFzz9T&gJbF>HYXYt;31&f_e6cs{a(!ZfEMw#WbCaa_ z4rI^FgcC2Oydm&>wndHlS$is1Ha%_n>f+*JfZSe_A>(!REh#9_E3?CdmZDPvWwN4q{zylBH@ad;y57A@2Xi|3 zlUraT1peTJxA;ZBIBT-4+!y7d*qq9KmY536T(2vA*<*L{Ihe1n&|%%7<0FQYX@Nuj z2Vz}SRbCSn@9-W+%MqYKUu_25TkT}-1Y4wb0LP1=rr`)lJ7Mvb-N8wI5vhsK*O37r zjR6H!-B3KurIs zZ_K_FsUS-yegos$G0PfqVQ+%ru{M+yYP624tA?LviqHug8fdUWjK*)i>?IqYQna(D zZF8Uhv?sSCjgLd8CT-PDPeGLNtj{nmlHdKiL~i&z+iw`dD|P8lxk*xZLnbPuB8C1F ze0`I#6$_ssc++G4aqVez&Y(IyS7u3Ia*APvh{V zbgz;d>778rJhnKF2Fo(b2HH3bsT$$DH1q>w=(fNIvcI>es{qpA3Myr@0U8h8!Xsn}x58shtZV!oyA zV4C}EL(MOoot^9P@qJYE#=b3qV;WsVl3n^zt)d3)$;nN$gfEDf10nVwH+YxS+;s@R zw78?ABQD@}ZRvJsNyEbZ9@st#>{m?#dw?AMvu!1!vuFMlI&$<95JiD8C+;(f?ZMh-f5o>B0E44#)b z*0EH~kB#Bso=2_%2XGexFQS{`Ht$azsc|Gky550kixn->4SO?0*EIKnl_~jYsqxo=gY2E(_=Ei+{JcX>U^KOG5vFMi`0v@*5zhLb^Q>HE)>42 zffoWmkRI;u#>dA8tA4t?U}AZJfpSfC3HFl$0?1w6yaRmoCDik5OI4kHx;7Rj0h-wE z&7%s0af&swe<(q(uA3KJAYQ~*XxP$WZf52xwDsdkhez8fIZYD~ym&oZw5$QR&dBgs zN>LGEVREc6zBg}xvvPX3YZLTwL5lPPY5Z^dfH_+5i!106-JGaT<{2@A`<1G9S44{;6cJLSZZ#EC72+BaaXiJ+gX0U=*uJc+9wc9wZ(D3hEc%dH8cc-Jds=uk{E87^HQ51 zjzfU zXJu#KZ2_X~AkRqj>U$&n8L9Q*3YX)0e@$8FQoz>Vys9cL8i|r3rpS-^j~2<#zfS_O zeV>qy3=Opl1)OFr?VVn?9RZ!vq0iEz&vG8b%C)W0f`FT%0B<0v*~|^NKU(ShPZM7c z)bn7y4$0H5(i4K{OR$qLM?V&=+AJn{$wxx-Y{oKD=B(Gf3E8WN>A8!&$1eWYK;wdW zr!3XC_hP8i%&Onz4<;Stp?dYl>gtK{P(3NCTd>~Gk59{nJuS-6C~F8DzQ@K7K>7g{ zeesCDRAKr?KkoMcUt-J$d+4JdDz!>il(b0x$;46k0Wmn8T+_on9C!>LKiGmld>=eh z4;S^&4>#v6H&s!+*>!chAraH#=^#nwT#48Mjt~GnRf7vI9%Iqy>FQqpMir^7t`1_D zK*gq@%Ll3_pC9|@-+@I9tWWZpZe9$zf^Khb7v*^GfWO)kM+uhufY0Bwk*W^@MxoFT z+l5O`MkT{Fzh210N>RI7M)?G?cPvzx04lt)|3s-jOF;mvJuT2TuzwDdS1TGg$BbSJDdiD`M}{07T3?7C(^Y$Tm2TD zy&f7ArKh(G0?37Z%XL{W8AT`as+1i%r z`%~kQXYjk;K!?{P%N{E0$^kd(G~Ne|n-o5KkK3I909Szln^MFV)c=qEg4WYvL0tOn zV^8ync(AF%e@l{bN8amDn^M~!jaF5Q+4%_#snHi=1^3OK(TreR-EP^tYSo%s*(@yX z-rw>OxncG-p2d2%u2ikGEfxrUzG6O}YQzsJIVm}29K!HO*l_uzgUb&A zT$m3B){rX^e-#4*18``LDeCMZ+Bdee49@skTK;=(e1(YXce_RjRKmc+!azf7G!7yJ zg|PS8J^YYw+u6u$MhZ()4Uz}QXx^wSBbpJh9Kz*6xccELX&tdQ8=4NRry@VI=pR?g# zh9me!;|~v^5BG-Fjfd=@xd2^?ODBFT^w`yd*K=g)7O%Sb{+$9Yi{DB|2=sOUY92cU zN?2eIQM2ojr8@y1)~j`K{IR(SswUC1JZy3i-XFre|@T%|1&x5AhUbE9RRPt zWfa}^JdqQ}(ZQjk{88QssNXz1l90!2PxxX6c*9p79nCk7jS|tD0@02(VB!yug|Pmx z)HatA(5fddP8wd)0#Y;{n7#4AT=AdJ>lqof?UpyU!C{B}?i#$4 z%FOwH>M%>X;crd_fu5_F%F>)rDlgNbQevh8+N50 zqAYqQ^;y!5Q8_=1amYb%p-W)$QMFtC+Q_co`TOf!#p0N7c<|vM%*8QBQ;3TF^AzUCb{X$9ni~uoz!I zeuBlzwo7of;O2hvxH?+d-nJpy%Vz@cuC+n#{Pj$*Ap$r-)iYmX4jwXLzx^F6z>~+4 z96RxHwAAuDmiteH<&2!R<5YeBH=-3D;bj3(r6rh|N0ZP&R?dZjGQ6;eo<=pX!()M3 zK>=VPX->dg6Do=q@$J#w&>`bXlS7n}ZNRP#8XS$G^B^S$#l&?OrT-OA?)6}!%cjQD zvpW#*EuTsgHYY4t@PcQl7i_u$!b5tG^!;i>@RH4fKb{``D0ug~Kg|WD=FupCP5S}U z!u^`)y%~ROIEZjJIP>y2$jvY?1sbhkACuJ|${QA>=*S@j82EvOv!*uT`q4vCk;rF9 zn?EvB<_!??biivWjOw<1~|#AcCm%Qfbf5~5=JTFaX@DCr%z|92@iY#)wSHXuqbKZTokWe zf4w9hxH4;%`-rdyJ|x|cOVH?QIUAFA^j#x01}4jtloZRF#=5$~pA=*<(jd{(7+~z< zRLhadzNCgLBLw6AI_@S9RxI9pGoK;+XQW84%v*>eoA@1>G#4@a>}&!^PTN4KF?oHB zSomj~IifR65YlEoCMXP}K2cIO_PA{r)-WzQFRvXYWzT2oKT|;C&Ql5Z0}FIwew~=y zrEBmKKQEby0W&n;D&y6XA*OnWlPc3U-_Wj2k;}Ih9bJ$9bl`1+0^#dpNMK3npPM6L zT0FcARwTm;vlYJ{H6@L{Y`-}1ZXGn|;rnd4{3?cIJ~yQV0<1^%^P`Q8e1=UE{BHds z!iGt7*s8}zD^8seFPXjoG0IUdlT9|*23%Hn^=b!_IDuL)Zupgb5 z8rMb!5%;judn}ek3ekT4+ZTNajO7*6DIwG;Y#C3&gRj5}PVm&J2;qVy1;80yDYVK_ zUnUjIJRZ0J_K<*jVZvpt9V-u8_m_{dzZHj>g(2I8Tr&&^h5!m0NbZgE(s`axh*zSo z2FUB{@D0A~3yNM``z0AM($`*5rnR-u=PV6 zl_ruVhc;><#w;yS@Pk`vzDog}9QPVI!EXh&jN)?aWQxxGT7WV`U0q90TIY6YBR+CG?K}K*k1$&w|Pyen)SUWr8g+{AwP^x~= z->B3{Flg~qwQR}qAt3)v_B^Z;EPZfssWGYmVXp2=klp9I*SAXopV*2?93vCJay}}w zTRTkxsSAO2bTQqVP^;T^SHkpt?f3=iUS!0;Hfux6lFPF+<`w5gF#fO7Is3ozuKF#i zE?QH<&?(X&0@6|fBHbV*(jr|VBGM%yozmUi4boE54I&^VA{_%r38;7ZJ@@_2!_UXOKhxs5=;Z{6>;Y+4IYX%O%YDVR2^O2lpY+4IlS3< z;?HcZQnEm(owxhG_(t5Afe38n{1_q>xvhWg){eUvQS&tNI$d`m*H1n19mT%x8%cqJ zV(t72|HEcrsakr;auOOkgvA^F1Py8MBNu+VyaeVkk`5^AJ{)v~_x6k&iz=!7J0`uV zW^9Rx?}e;Q&XCMw2{WQ;!BqeV)odsZo&ucY{)X^Fp^ejDs>b$j$6h4 zl^D_5>t%9s4Ai7s6&{dc*S(yw;E^x28_NVPr~cMOwl8`x$mL>YUE7TA)g2_Polv7b z=Z4#tqhaC~K@fjDj8Z7#^0o`y`^`E;D4QYwY2%6P2}j2<$N7>Q{nMw=hIsEh$pU~D zoH)(Sd}_)DX-T*am`;{S<%svy!OV6K;i|rSc!eU0-~)xu@A+{b%rpW=eCfN8PRdGE zQ8MNPctFZ@e%%{&--%zx-4UafKm0cJAL-FQ{)Ag5#&gIa>q-uavZ4_+jLqTQQ=-=3 zj7!OACTz59wJ!9&?Y3*b={=!1{lZ@mU-g*Gl9+EO^4C^F)!W~@cK$kLrzzv!eY>Ya zEjju;e$P}7WXbR_LA-r+*Asc$pvI_bVgD?q?=iyf@KMLfBVeE{lx?mbSf$2iLdVE3taRb3<5+ zWyuJFwVyD5Kvf}`bo{~IaEs9|hTvDhJr|?q*G1Fs=5jrqeY~n>!|&JI5n(X?e0@Y7 zVcr<`&}%FO
pW{;iVf-Y_HySpeaXbES⪙kXmf|snxH&lDNeBw|F@@jGS<}}J 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 Kwo=-}?|%RU1LWKQ diff --git a/doc/guide/figures/bloch3d+points.png b/doc/guide/figures/bloch3d+points.png deleted file mode 100644 index 14a8030365a17e72e44923f507c3c4c655da8c31..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 55538 zcmdqJ^;gx;_dR||l~$0D6p@mU?oztDyIZ`0U72DD0HkGvRt8!;5;5K5_@8e?I2J0AfsLa|!*gj(g zbP;2+H&1pxZWZ#_a|DQQvGi<=+~PqwkX$kaSCD+}=Q`$IYM0}4lVF9`G&$%Vyl?>j zw77I|+;iYgmwQg&juqOo5cL16FUQPx7c4U2*R#Rs1hZerNXD9bdSNwIO%5B~@P;+N zKkFh>N6)ay(qXWbCg(zm=QTAox;s2+ub58K%X_=NJtOzpe+#+4IbG}U|M&3lFb3mm zYIo)&*Ink(`JO8gC3rdU8T{1WEx{tg6SdQ>Va0s;hmC**ZYLNen1bNpE7CJ_3yXt< zCHofmROZv=!$a$&xAUL%E~IBzntzXY(j%}5+{e^6x`WZ91n>4EN%^oyxD9<^ega;X zwBBZJZr76%Cw?lvo|DN?%D4#{?#{i}{oq`$b z74QZ=w_}gXNeTCpWvlRZCvIZg|CXTpqxj8au0*EqwN~|J)7e-P{rcwE0zHPUqtN|v z4OqD4CTEL&4~Ebh7DxU6@`;!rJhwWh5IpW^viRVJttpsGJsQ=JTRJ@flP;LyKKOHc zdwU4WU`Vjoa61g{^azaq)*v)I;fH+Mf6sKcp9>LuJW!ab)G#$B8T$w_()Kl2Mi7R7 zuv4GC^{#K?ZT18=U=%wyw?uD4ZVitAi^$1x(?xbDSu=Qg$AIq%z`jj?Uzw&<9_61h8ToBa=2W+)^{pP#!Hso zm2-TYoT$P$$e}&iN4&o0C?Z*1ZTgk{TQ4yib(t#e(@P0Jd_PF#E612ME!fhBQx3Cg z?L>%wPTY~srwpeY1VbcJg}J@8GJp3y=WlsZ`&EmC9|21zx(5RJN7TPn0=17j2L43C z^a;OR1zv?kZ!izDvK=C@r^(%=oRGUH@Oic3b?HjxwnO}yo5rQBciRLRS zsezZ=l)Ae50R6KpEA7%5CvFl`hB@mx?ebk0UZk%9JsW>+efmTm{5sZ736Q&XoWgR7 zN)8`(ZWYLGw{I@i`U!C$eGp~}1i#L2zE!?aD-X#zw8O*0Ike)rN|0Epn6esj!qft* z)Pketl9J{Mo77ob&Xl)8oSgX3ULQen4bQQ@P_WMBMXK}((!qYT_|Whi_BWVnYNazJ zHm{{(b(xaDGB@cqSHx|`rN~x;@KHR!6r!sPHuPTIsme6a7kogW3lnUq{vr zAXxHQ%DC$-6jhpbZPR6viN!+;PQF;dV&Cv;lW(95?b&eP?%r^qjCmf~W8bhu2w8XF z#9GcDAY#coSg=)B!mEbL#^Pkr}b; z)PyWzSh?R^K^tCRHDj$dRqNIEw2Ynk4P6n!oQ1?Y0YT0}fym6{_v?Eeb5-8Ur6w=I zuWx-)3u3lVAX+<@%bi%#Tu7V7pU zUOg||N-37@xtg|i5;2}lARh{oM*mr?L{to!z^VPI&VaASq(Om`6~IWx=e#-f^+!MD z6@gfcldYycW|*LWQT*_ZE|W&FDo6$joM^KYR2Oa%vM3sy=iD&)lY%{65wMLz0(F`4 z4(hb!VyRdoOsWW>I?XHgN_bmm-1^MLu~bsAs>P}^7B72Ru)e3nCchXjb1~%-^ z5hpogsZbEV3R8UK-~jmn(sn;1n-+~UU%NaOTOyVUg!am-O`mz;>RsD4N#yH?sXn6U z{Cx1^`kz0uO6WZYal?>n^reV=1r5N1>k=dmNR~*?dI@HZo7X@F3^nkA&A5Im$_AQg znKSooIeLjH#al8Qx;3FBL9j0im}n8T1CX$0zVfcNK8~LH-`}gLS63)zmQ>G@lQ4zwEe%yn-Xw-rB8MEbf6K)iq^ahNFT~~fVr)(&1o3M?4lPycr-Td>jFRnwzUPlQOhIlu?AMg0u$4-zjsLy0i)YKU9p(9bJXCBnl)bl8luCNSw zS_WB9!R_V^`f)8jkR!Hlhn>pRTRhG-cZQQvq~z&eP~bc#poHFT z>*hXXnONd}tMciLjhTOo{9^6Gf5rKxy(Ml-N^Z)ji3H5pSsIigJ*9_vP;eQtf z8FvRN&yk+yY~3VFQ_h+A8!x*2JC4?fO`51K`}|NL;l0LSnwdXJO3=~K@e(jmztw)F z4gV?}kkAp!Gp@S2;}rbq?Mc$!4;*l!V5yd>mAriK?|EayOAuF!do% z7wlquXvz<-dJ$fjxub?xjHmhH#l%5V_JXY!KiO~fDb#q0N~^`s*aCRa-lya7rx9*} z+;)|M-R*6vHgEHd5$30B=5FjD^?$br*c6Db*8AMXuQQh)aUrITQz^gK^X6Qf>G7dp z7(BEpx+-@6T>nhNPYP93;;2-7wm1p z|6XJDdEJywtjWupffUGy2xy71c&Wdd)k~Ggvd8!0%oaXP8U5**Fki8My>0b&vM|W& zx`Ik-(vm%Cc*m(ploHedI{z3>Hz_6$2x$=#W={uxR!5@8=iFN#L3Jh_2KU73 z{fHj_1$VPaH9yCDDZ9p&g5gS`sFv^xCSPBoHVMJ&Y`cRwATnlQNtAxpv8|r4se5C^ z^?o3nfoq+1K3tAoA&e_uIn3*$DCJ8PBvh<^YTe}q zc5o2QT_q&Ec6tJ#JX}^TS4R&e_WSk=;*HxZ~j!9N$LlSK9g5Rn+z`VU;1N$qE{>o4(3AOWcB z5SOeUX{%mwR`v{kWXeYi3JT`Spb5PLGLP=sh6g-G+HVX@TtNde>P zRH@I8enMhq#W9duXMjcQI~&pK@Uu8OK;_;#Q+)Dnz|AgTX|C|*&v0rgvw9;CSo?)e zN|`h5eMji{-a8m3H`2co*uRqwfycGw*p|1PE+SuMQxy@OA~HMqv{lWd zgP*3?dkfhj#|fs;jHdixqss5lcvEijVH{Ws>m3UWz$- zd8PjHdRz17ga6+Te;}`fO^yQ4RL_<=&X-PGpI|QI4o$kaxVU9IMC!vy$+&&fMFHJ; z$Fr}|f+g#gu2zs&h>H#rDtNrrqp7TnjRWsDdEC|$@72GO90h;+YP%#%VW>(Zg7f>p zg%<`;N{nRCdr~)cc_LHyI2QR1RW%&sS7wQnVKes7?Taw++&+?P z`w4hZaRv-Z5x)8B*RP==G#=PKjyIyWKL5j}p1_f?-EX>VAV0`K--+0L)t9b?+GKP0VmGYJjui;CK)PwTr_Shr6q8XXs97NOj zlP49)@o-Ufn;rUS%PI2|;ifgdcLBC*+~BV*$Bwni68h2>7HZP!dIZg@%=N1mF=hV3 z4EVTWj<&WxP>q%pkt#(gyWEUiGs=eFapb)H=hvbnJCR(*GZGUZC4WBB3~^ld_MNe9(O9|lf;9V*8EYsMnpyx8^7k`0r+yVy zOCG!<&AL)pqS~;eN1s4jN&}NGR4%ruY)d41TG<`xu6OY^q*12D8`^PVz(3-Ch84PQ zpCShe;Mm>;fc#VJyP6iKv0Z)#zw<#NMfDeBLKx$_(2k6volu8S5rj`jt1p*u@goj1 z$M=fgezl4PkU211zHn;qg2nN8US#BZ#S!>pa5oi=YF(xq3I9t85XW-pDElNHw#U_s z2ExxqL|mNw6!!sPuaF->i4Ctg^#;`bB(5ja!0al^@< zOCMnYJKBQ*xSLEYI|ql^arDT>$Htv@t8A=;8S4brS*c#aKng|bwG=iBAHg*cwA{%e zDyh7}V%4EW79CBbfVm?xEQIQov{Ca0ewVm+8E#yoaq5e>$ZubTCA)F08ZPc+tiDeY zXnCP@p?{Yk#mRQ@LJ1GF*;5DXy`ja+)4OV*U`<-O7#Y3I9M9x+wFdd(^Z~)=md_xA zd0fbSqY1^#sQ*`_!s^qYj)i@G!;gk=QA!p*KJV*eEwoRHnIvAaMpL=oKb4N?+J~ww zi@v;Bd5QG3v)*pWOR&YE{*CT3*V{|j_>R+dkQIUMs~wpnCcRb<7CbaLIXO>$GBG7h zoG7WL!2k}me7BleIbxH#vG4Wl_MeC3&lfB1`y9;flQrdZvvd3*AQVSecaMJR0gEhf zScI%QiiwsLm+Tf*+@K^?_0OL_8yl0s`8Bw0(f*Vo*TNay=5#Fe6bF?*?G(oyb>-e@1s5@-N179E%xX~tDPzu9~;|ezl@B%?gdIvLRA%4 zy{O}q)MoZB>S)Xrxe|N?GlzG;Hl})&lA4;+?f$#p{^U6!awHznwJD6EW`&zo?J>GK z&f|)|<=Uv6^kgNTw6y$#6uEN~NliN(`_*&g>*Iyb)DQjtj`})u=^D=$B5&0$Ske8gKvo)uOVfc(J)5eF@J{uM3N8Gui!^e|pm z`WQ*@zTd3WEApW=uIroaQdy>i;i+hW!g|CDE%5?!uKysQ^UGG8uQKTe5wQ%TJnD+5 zmUopkU0jNB%~3C{x`t?8?O{|N_hprirr@}UTA$BUNd#V<{4izp+&xcWeyC(NM6^Wc zK0Gg{oPeZyFkR?2_<}P=#;-6%@fDTUrRvZxPII|$DHG5fNdye(>Q)cp`% zR58X7N)2x4{fsq-+?1tjbH>eUXqVPZ=_s7f*p`p2^91rVp7IYVDf=BJoE{=4jH3H@ zfJ^9{>+~+|ItC_h!GI5d%B@cZB3UijTQlL zqq9NKdo|f6-GUq{5u8uiiZu(Ss}A%8l>Y|*{^f3(rhCEsDol|mk|U1;sc~*8#w=y| zPIiHB@#)dnn#07IRa{bSP~g)^8A8E^124Z|R1UqO2EhR}saxx;*9cMlU7}d7(yZ2x zHd-lV${30;4LmU=JhV4yuafAJWrgB+n4foVZ=2F5XlrR@Ct!W+GUO&XNi-T<_~qc( zL2rc0$1##ql*%F(z3B=LSh{M4;RBb{#H1Rs^jjm<9%VPT)IpaZ5 zl+ps$4?}JZFPfSVzUN4uygVAP=GyhMKRF{H`|>;@WFGX-U-7dLD6^0lkG(Ghk6sRN zfP|4v3Z}{+vF|mGNwhNCvw7uJ|0Q49v1LUy*!yt0*lwx5ysS)CK_Mz4!V%X`rmOey z@Eg%_HG@p7DC(P(KKa5L_0kuC!di~vRRen$@iO-C{*Oe*D?#(^F*4&gIkd`>hEzlH ziuQvL6>A~1i{FY*QU+6LEsj{ioy3-nZ|J8%dN6QpAd?7Vwjj1#E1Wctg+|UZogkZS zs_>_SnOz?H@RjfjL`J_jb!!1x$4w$n9S6THNFPP(h%+z)2n^IaMrLLLG!&;+#Z3JY zp`3I4jx~kRt_6J=8JS*VM<1W%WbIb6lfx&bet>hPllJl%fpRj`UFkkoY0ibYBpRx_hj{_;9huc-@C!0SNEFmqGF`IqiOq&+9 ztsRO~bL4Uxs1o|nJ?#Ny$`vE{=pyvkB;c&@(1ok`c0Vu+-B`wjLyu|Z>i*{8sSBGN z+2L@SPGz$4h1bZTC-(E;Pax}!f2PQWlsuE2Vk`et2SV$?-)^_W3Aiz*Cf1aZn~r~v zJ3Bk#xkkPWV>4Lp!@8uPfr1h+Oh|9EL_twv!1OQ?)LLr2zh`yY zeK46QYswE80^nnr%;Qa(TU(bMHQmsrp@D+@OATKDBm(Vgf;5cBnL2IMiFtJQ8}z>K zeca%~HjPjdI6poTE?1dI@RJcBf34os+-MIbv0zOB85l$MZkToD?Lf~eOH>ZCE1SJ6 zHokpBi!Vq1b(?ME_|NYJea2Pok!G(%Syzh-tO&JE3AdN)Z03Ib3M#My%XV0Bs+zZ$ z(Y*$z1#B&7E@i){WYzf!%C0##fd(lR27{%iPqV{EQrME14GQ+3TTFHK`o05w-gu^v z|H(3s-W+ITz$;lN9YvlI!_CR9aO%Lm!cCf|K|ZWY7?n@tBCn`Y>|~|WjRjhO#s4`l zl{HmA??u|(+>%hxm6V*UtgiDY2SgsmJ7brvp;<8M5~S@7Xg#^9fydu=URzsRWqPSp zQsDE{)Ks>jrInSHpPyjb=(Vfvm^etLkX&6RFqNnConfp{6F@u=9fLM?dwy|JWXM>l zQnWM;G(FYr;zK7+O+^dMFg?^PosOS=&kp`lru_4zD|;kXqrjo?6#@cCclW1zR9~Xg z|22;*i;UC?GM6#!CLDuKPEG*eGBYrMJZ@Usa^eKNFBkqL%CLEjUx&Z7H9gMr=<1pP z3tGHtP5wB#mIAAS8J_34ANq1gx1xtm9Jq_@>(dqle^keD8Q=u9DlNr@?mS+`M_C><1j9pn|9P939qMAuVsWL+_&i;RqMJ4x7oxLN zWCE?)@LeF4sOOP-SKuARGAlhuBk9oD_*!y^qQeD3x?bDcS{_+LBa z3J)<2JMohhW_--+{b)*it?C8=*dsNVXpO_|_%Io3O;BDzR;PMeHq|a)nT6Bg)ZGEv zqvdi}$ENUZ8oPSSoV`8xOfXL;Y#zXo!8jT-Ex$is$R8dqGW!YF5tcPen0+@5gw8bf z{vBRElgH*{6&rx|QgYzs>UyZ0@Ym61jd(d_dA8|m|DuPrH)k4*o7(y+{h zJY((Z=hv>w)W3BmN%hIsWWaePsk+8kb*rVC0nj-a6@*8hHNQyNwf-S?e&uCFZS zbfu~tA6@hy539xn+uHLlF}DAcPG%^v1H!9y6w%JF(8XTjrzg~G%(QnZYb+)_rB?3M zX9%XOMAyw!Qeb-*%3CvcJg_Rnw0do9`t1NnL%^&*vUgFX$?z&{pjtM=CaS+j+4z{Y zrKCi)+SFxW-lbQQJZCRawNez;Uv-F2uBK6WO&r#HUC#e0x=(PAds>#K2kEBgfl>4MVWZ>jM zqKGi_Gdd1yUzoHAq29^Oiv$HCrnPW)&dHhg!>llih>sOL;C~nGq?5*_vJsK&lqw>c3;kQOf&Vlgk(CXBN{Y9SF zSoG_%-3w1XB)7C5Gd?7LqHZ^S*`V_j|6K@|=g7hs_(neMvaCoe3eG~}jJFor|K|nx z)yT4vW@V**P5CHim4IDwszIu|T!{g?+TYW(??IWR{;P>r0dhHqIeF^*Xg!K;?^#%Qlu>_E5l$T8uRMzeMIYwU)eeXOXAeT z@H|iodhdkzu?DuhHvP__JOL)=yM&!bJ7oCb=`uZ5tYpLx>nsBs0aT(N?tDic$-5>N zHBARRKz2jv55!7B;#EWl7k)X=o#HwcF|Jjc+#p26Nj{Xr(zITsojRO;T2LeTAEDTy z$#Aa!M+04Vd5$=@8i_V(xYq4=C%P6O2tH-wV4$W{-(Y0cWB+E}zRm8(iW5{;+lCZ} za~874cGKqmzN#HMFkWQPgu4q4ik^r4yMMvH@k;Nd|F6@t7O@5uS6&%;_r=E;F5m46 zu2r)~TO;z^%D(+T3k~7eJWU3m3`(t8C0Vjd%k8NYvaU-VH8<0mb=AWY6H_XJ1q|=7+Rs*2Gcz-X_82K&$5gsaJIbiE#aeA4 zsB_f`kvF>_9dmaw^)XrzMc6~xG1H2fE3gPTgz)B;YO|-G>>XeCZF0s7 z@tk8$okEtamCfUjOBaN{%b}y_Jd>~FZ+)J&2jjkY4(o)FbdZYREZg6JvaloZ`;}_^ zEui0&i{l63eOHh4fh}gU-~LmB)d3HFg=2}L`r9~G(u?*)V?icgnL4iOWYFG%$(#h6Nh zrDE&r%&;lir zqT>YV_nG6~ZL2_EN;BBnAY~k}<>DdF z-?u88CLoq;Viu)}Q~Lsr7Bo(?9@RW7W2xQ$Fl-Q^Fd(2?*YR?4_9Yf^b+xYog3rt> zSb4ogOk&W-tw}evK#8Ew)uAreJMHT=T#J3CXi+xHRS&dtZWs!0ekBvWlL8`rcK$uX zvLI8^Fx>41gPHi0wog^njrhn7T}iD@k=C?k!GoVEoKj~f!OuR^lMA(lWxsjs(iea$ zU-)pOW%l`{V$qQo`-f(CvWD~*b1uEiQMTs^08xG%p2T+M&VzpAWJKpjQE5gWO64t&s6cIzT zvCGumH6?hFB?)+k}63hDL&|T>Q3Uqwf!Lh&DVb69W~>Eh?9bPlOqmi z;LlWYqA*dkV8rrs?#^G_xxvu;@008iw%5R~B3}slLdgX_zf3vi*!Ddq9=WOW+t#Yt zctHK}e1{J+x|&d75wU(LUc!S=J5bZkbtflD4Vz+rOu5!OX6R&hfON_)Ct%6VW!f8V z7k|typ+72@yg^d=CwBL>0)u!qGAbCK897>(dXSInXZzOGp{z3}9UsPf`BC$lHk3~i zzSyBX0C3u-c2V~cz1Cfpc){Tgrd7?bci|b@^AZEqj-F5C!xlIPEmOtc2VN@L>Sn|* zjxWZ11AD+U{)rh_I6ig3-2i{r?+W;f!Z@up>RWZS8OmBiXI4`8s<9rT`A30(Z}Uf2 zZr;2iODHTkR&NgcxZ*PL-@e35j+8;qk&dke1xTe(9<%ZkGPJ`w8kESvC1A|zTNp^K zZ8XFEsimEd1WP(=`_9j7V^dK2@INa-;V%ifslNXHcQFHy<~T{J0rn3G(($#13^(NE z3L&ED434vxArr1`K$yHOWoc+zBGTGFjUJN!p}r8>O8)a_$zzY}l&|^m6bctzJKcz4 z>5OGMv3=v$OFgxj(~i5xy>Y=xsl~`MQa)q8>+zEYw)edF=oc>N=F#m(%^C8AODo3k zy9`OH-v{g{ouGmM&qWl5bX4(-HK=Ra<$cC1Q(hV34Dn)`@Chm@P)e2Y-X{$!VGBV@ zN_H=9!@|NcE)I6hjaYiXrX(e?lKG!LD(SoLtzWDvszg#iaMqa|*Wl@*(O{n^7jOVyUnjgJi{ z_Y(ZOxDdu;Iq(aKWhFEtThuJ{_V!+L%r0FC;6wALtRNBM-)P2l>HfNsV@{J7fYm}G zl8zEd`cmgBfQI@lJV&uCs+E2Ep{=k7M%rDpE8dgN9;ho$Dp#K&VA?!cq4Ie6r611c z#z8oUgm7oICsU?$LGlhBK8|6Ri~Db%O@iF-#7Oi%8xmG$n5XO0oylBg``+d8e3_MI zZrRjZ*Qg&t?>7A=&vNKxWy84jo zwlv>Juo(0O&~+F*P7Qjf`$3-znDUc;)NAFU=vOB3`owJMsLe?HJD8Msx_S2;39A#K z5rVxA0j`qMhgm)Y-_ufC_hzo2#Gko@e$d6Oup^iVdMo%AmSuqMD9)ndx$bhPn3~#o zUvxFn_g-UsgTEPAM1=9MCQ(bXd|S>^EA5&EUBLa&aNh^Uh@lE6n!PDZOI=El?f-S1 zVZ5j@{Ih*`%5RAE>=Eoqv8e}E zUWB0-EA+Bs_Fj0vVFf6iOOK54y|hv4_jJEGRTqNf^@!n9j8$zkoO}+hJ;qygme)&y zPmp(-r`%3vg&&07M2L5c!UYXhh!fa?Zp`YV3>-H^tY1pEAf@U>HH)NlWp zRWj5hd@Zc*11V~cyuBYwsHFbaGLz}*`Fngx$u3eaz4C$`>jG_AMBvAri%yy<4l-+( zl&8z7=QM zt&cuVkm1Jx>rKY}hN%G35XWJRwRc9UOG|4oezuh#HyqL*VyO-+R{p^pT9(z?!cnuz zP*rtG|Dpfg;d-ntKF-o)`2CpXl47rfPoC_CoALAuaMQ{mvI(UHdixe=6;6Biygj|X z!UHqFWJA|N8=hKl`{;*Zv|T;R!k*QP=IULkUJ85$%dCCA#aJ07>Fjd3cQ2bq=aoF) zZ68VK^g&12SVKG|cA~#_(R?0unJ2Gbv$|}lY+MuzWz4Iajn4h%CmBgY?z8qS^Ra&% zwLdUl`~@!s5~Hy9DEy>|r{YY{q*bQn>T&MI4ZsEa1zWfZ7u9*qKm=3_t+Wg$$w~zGY5wcXe03)eUCd#rt~*dg z?`(y=Wip1$5JO%elM2VgA41|uLi8knchz6t>Tz`(o%TOHO~i%Drwg!9M4 zZSLJr-^|Lm);2YItJB8_$2kf!5fJio+DbHAy39^m#$T?e`c0msXR~{r_!y8kHr_mY zf2%CrGIx}JPHEkhFhT%6}n?X=e~JgVV@kmK+ zaw~@o-WxqG;wA&hWq1SS&(8Vkh}TRw+m9xrZ&lSuiHUO?fVg9SvbT$Wj*dQz4%xjP zLGU=pPXYwGzCLH&9VG+n!omiSwP^7ubuv6{K{MR_ zWI`C=HA&+&i(?+WR81PFrD6yANuj-EOYt?rHvR5)WQ&$#Y#o@mcV2BL7q>vM%U3qS zw9-Nzr?xS=ic=UY*b)3W!~Z7|X$?zm<&T_o+s1myuDi>$h z_O+Tp!xz^;SkcsT>upMaj!Li$p;P5-nW0p1SK6<`N5 zc${|^Z7SjQDRN<BG&QZ7A zV)3BPW+$!HVRw}jcs~h`@15?QUC1KK$lwr=N##6QVMJ0;B?aThc-{6N=xpm8OQy&EdCio+U$B2j{ zP1IUtGZ3^A2NUDPuW%-{~I^XSTpKSPlgXj@w9gJPu7&5T#})9#gXGHrj3 z$z!INb(ihl4zu!@YR>Wme{M;EZ|_@jrhywF)082Y^Hu%A66d!*T%q@W%)R?d-s8gI z77@GrzExI^UG~)U7}v!tSG}9hIE|#88rlUg3MS#+RV_(puj4e``6a?@r&8(Ry!KW6 zaaTSJKQD@YrL$GGo)1NEtAk_PdBy^D0WcY1e z8eDc(fGle2;Fx;3Z%o%sHDGy-lwgK9Y(Y@?Mgsa-Gr36Ik?YX!9y`9X$1k5an`N@) zu@S=wRu$`Yv{3z7u?|7Ma#8gD^XGnN(3wnYTNs+1a&fKsMSn|YE8xImESOAcXCses ze;xZ5*oj+P%d8eJFJ1b(P(*+-2zp!mhAMy2NI6iggq7lDse4*~Ut*db$}SO_0UD`Z zs{x^0559jX0G#YSnb5F}G|WpB9#e)_{Dh3oXfIxHtQ* z+w$LGv%Cm2Qlreb1Q`9{ZaR3F0H34@IvzQO6q{>9FWfulC{^xkr%U<>FTbB4_0I!f|Gz0HF z*0kQXCTK1bUtuO5wIv_qd?fR&RUvIx4d1zMNQ%PZFMauGjEHsB?!>s`oJhEz`?UIB#TF12ec99m#1 zGfq=cQRyda$#>r9g? z&dS0t6hmZzqfbNJxISumsa?F@x?eTrEjjZdk(~*W`mVY2{Z&}Oo7+Vpe|TYCrr<+& zkDNd5D+&nW?$O2R-^|m$f2PP!>NUnas})og_AtEP$4CM{vzl5`QqqA5@Z$qwW3BMl z0WyI*+pP~!!Bk2(62?IqR(D%|#kx1=<1w{O9V87UrFJY&TZm+e8Lv}JiAjbRJW=(G zS=DsTx*tP>Wzfeny`J`k$b->w5NOs>2mEjxU7m`1+h+;DrKY3XXlL;`*I5*@&iQ%5 zFhRyQvSh>+r|-T(?~k%LSudRd%NKk(uurXZJX|h|m12ZnQKQ9b?=P53FQrEwLrRfq zZM;Ilic#X8Tc;SpbNR+`v!CXv|KAfiP5}Od_KZ8t1fq*@r>jH-vAGYg4RnOaI^D39 z8{DQsPX!l9N@tKo){SOjI(r{oO>4BY5`p5rO5N8jsW->y!wSzdcN&;y5QkrzO|Xw9 z)3g0ht&-Q*h-p~aPSmof@m=qk3^gssrF_=1N9w=BO_haU30YV;W>6DG;a6|b(Zx+w z7_TXiR^2k6)&rujGUy8s%S!0yZZ=6Eo8HYCh!5!>5aMTF|mvO>LwLCh8-smN&4 zq`Qk!`WdJ}9I9%1+S(ag4)j`sv zcG*Z2`+R!ku|Pukz7xvcL`)OcXc!X<9)<{_OBUL60KFoLY1Vfg8hbd;_p3Uy26pJ^ zBAW$am}vLWRpRhYaBy%2+lD5H6GviF61V~dfFcw8qYwKa4cH63?=T7Nwm06JCkxgV z(7D>!u+ftqYb<+{C53INnbzYzQj1m)dsi+%@u`xw&l}efQ-x7z5yU94C1$Jqz5b}W zr`fv$dt_WXTa#|2K8yY}s6)VQKeL|xbz>6?Qm1X_=%|Df1ms40n%^4n&7Si_L-N=} ze1s}yoRJHHpE5@l9eZhhV$DAYr%F=CM)@B)*5X`;`jU~03PbR+AbDq*tGAhgF2kY6 zKc?VlY5Z*X{?Dtmc~}35Dz!e{9U(mIO9)l75y?w5HX;GLi&L9^RIf29Dcv-Gr+fwm zG0#Aee7J+PjlbL_0 z`*ckNCvWNp8wCCgV?S)%|8p3)y}Y_HCOWw+J=<*2{ZH*ESLbs>+noQ}KrURR*$VE! z%+JsN`i1O0xgA$UH#=g>#p5;`ckQ=7z2vl_>ZF>jY7VJbG9!V1E$S(E49lrA{JFT^ zM7*c}a18BNAQL(}eDPcum+MOl4+RPc5)BPaXbSdg*QJHnMQ=sZ1TXQ_RbUUvqb~Cy zjp~{|++4L}2M3m@(vSWv{4`f*AL1QhwVC$KB0?CJcBi!R+$as3_AKKE|8322pLg7m zpaU9(r{rd;sR?+Vn=&QUFrVPB{3<%oA0LGPszm!&t}mUPR4iqTB)Gp+I0Y;qoEE)C zwyq7}64baUE4X90VjawIcqRAa<6LyKndiXdF&Wq$@d?)Yzk*lN)$L|Nh_uByrG@bB4hDAr6Qsf~S} zI+g7?aXq#Omv{mvn{Zxs+L=7`j~ab~tL3lyl=&Rb6JQP2WB}LeRG2@iG|_o!wVT#| z5uWS9f05trv#+@OXTz}*$phhd1&ZzWv-0Ka9g{Z)INw!@RSiC%XftQpIkV-Stv)s9 z&;qzgz`kGs)jk30Kml`aH{elZ;O6(Q|Jhgi(0>e>#mOnxY4nU>9!L4!Ff#%^A9O$w>ydj^{f9-qH! z5XeJ-_f_xT(n{l-6X3iJ&hZJ=-)NFqQb|-|$cpu7pzC%os!^Zg?=WxL_WK`mz?eylq3gGkFjc6q^paYe08zM3+l(|wvxd`a_^&^ga&fRpzn~Yq?$0gG z^Uj^XwWx%I201ysZtIo=?Q%{|&e`#+#@Ohy4C#C({nkd``)faU0Fsf`2m!}R9S2NF z@CJ+X5f>fxuO`OBVIZ*ppN{|G%g5Bwg1W}sDR;kGsc0nbQ8hI?&NLeWX@SeX***V! z2t|Yumo;4&Rv)HE(a8^Aj^sg@F?@dj^~<{o21My{b)>Jt{YNBWqIq5d&!@jqDk8Q9 zJhMbp3hf~ela-YvTbtje#({h6rJUI>C#&)6I%e*v{Yu1jS5pJmHOCXUXcKC|6jy#^WJR_5oU3u3~lN`xlgQvC^wA z3X&m!_$gP0%nKlu4Km`q+E^U+;< z81c722@HO-Pg`4ZPFiQrd}@nyA_{kMv@PqNF43=SSvk4>Y%805W?!h5Csp_C&$BlY z`z?%FuZm5gQ!Y6toUbV~$G zVF*b8pxnR9Ean4>30z`mwu$v2uH(ab{-afE_e=Yq?tjg<=FAhiGdHgxU;O#RwF~8G zj0J8r3?i7AbCpHv(6mCPUKHY1|By*K9NpC_!&m$3(+)1DMTjr7*?oiJ5uN(hg;N!+ zoOSlw-L%$($0-*B^u+nsb&0~3(eVrW8_>Zvt0f0=!tExC*Ie!?h!<=Ppr{?79v!Qz zsg1(~=03?3pUeSu?jvSN=opdy|=fX;jbE) zF%NI&p3>u zBIiD*_Fj9fwKwCb)0t6y0nPZ;v8IYi{zU{Th(Hkge;5t2?AIiu-DEEyTD|_ELwoSiKGHK4>b31olV@vgin}IKrili=u68NgK!OA3ON5Qir zi9ab+>%^5(9w+A)7D>OS1h}Z-C+Dyt_7u4Rn-zaEUKzW(iw5(+zRAT|wV%1EKWN^3 z{|!^#!Hy1j@fnuVKfT$iekb2c+?Vlp0R|yO=XI~2_ulR}bo+u@6yu6#`F0u!IBtwp zNAw>p9|N8o9hNyqf6L%jsCZ6*P1+X%>S#&-ge(P={e9u+1Vo1lE> zy)lujm7KP!(QdPWJFI?Fm8nnpmu>hlWLCa_8B$4KSKI)$H?6tSu&s*j?fTyZ-zS@} zsqPsZ^|JL2H^G0aUL>FN^hl)WIAxObccxTAcG?5VzO($N*#SZpAZ0cxBHl_bx%*Fy zKYIX5S3jgp?%nxL+v`aEnqg#NcTSS`+{_AC)| zR8o03)^>P*Ts`zUyC<#mS90Y`d<7Em0JA|2p_C$UtnGI{@8Bi*G{J5#GwIkSjT`in zqF++M6UD!&6t5l0Zk~73^+(I9#30iJCuW}fpY(Dr2#SXe0NLE3)%d$Uj((Y zU*5s@Xpp$C{@aME^M_wWq7bUTF>C&3#Qg>X;EMpZtmjyyV>h-)w~GChL{YtaUJ~oJF)up|!O#kfBy(68L$kq}_4_(}RgNuYi1P<~ZMK3` z;PD@y{cChcXYq8>>9JeW<88Sg1i;2G1IJXxyh6tzmXuO|Pi^rx9Llpl7ZE2e_*5fY zI{%E7vP|EG>xZHc-26J$bch4ddJUeL3* zBb4|kzmuz1E*y)MwSI_GFBX$4o$*H0V9*g;?58$vTma$DKCAIiUiX0pn!y-dVHeHa zskHO`Kgvv0HX$vGVv`T0pV{^u8(dG8`8VGlR!xTq=)1s7rT<1=ll-$(oxi&g)pl3s zdtgBZ`PKf0viPO&qlTZ07e%0SUguvsV8OAqtQjSUnH+$bO(6_3IpsGpbz1|xAi5aW zmf>E0ynFDS;4f4!26zbX(pK#U9pOcA|K=%M;G$?JaRM2LBeyZJcau)=(Dis(EDxj< zN4vC=tWOC07hL*`o~_!(S^Y4hG2zPmQpvhJ$tL?t-qB&X;h}mQT-xI~SVp*pC?D$? zB!N>_QlC6!Q1t;a%o}eK{~GQLB85(vs2LR-^>NrAYNj*#cYYF|drMjCBCqMgU$9QX zieh!|?d<`_2JVk}#!T?^7qushY1!#e^LVx*&fn=| zS&Hm*HOP+L7Esd5TCTNkZ`QlR#GM3^WRq0|k|d8GIK86Uycc(%YC39=U$D29owCHF z^U$oh-NX4b)SGFz00?DNWy(8`I>@QAnpJIR{mj8t={ZZHcg}Ef>_d3JUCB zt-m|oU2RW%z+pouB_P2r?N! zm$?>nC-ASM?y_!IO8RpbN>A!2I@>YXp^<_PLf{Z7l! zL0jw?-#BQ}Z?=e+Ed9;#VUKUvH^`Ii>Q$B4YQCMHX}!L`7%d0yVxjIN-RrpFpsWd? z1b|~Ycg~n_uolHfe{f!LLL3BFRAh@;*K%{qnKC~ZJ|-eO0JOe$NbT+eMMdYmidGzL z^>x%AUGHTn-T+KDk0x|aN22@VwT9?EZV^ZJkaNNyJFsYw76-h^(Ve@0y@IH#eKF+J z@~K=7>!S2Hy!NZ@i;GuIP8*t6eG5aabr+_1|4_(7*MDvH_!=i+R0@RTv>l2g)1~sb z0EwK6{@wfxWYJ|t_wM@0n3x}+8ih!?<4jLFcDJPfCIajf&pk?&oxHCw4zg}%T9=y`x{OD-C93=zH_@x~(Wyueij*3Wf?oj~m`dXn4ix9|ZFGb*$6`n&HY z_Cnt|>AbwYggZv`=^B6PiE3JEs_5@-8UGB7IjC@^9mztC_(*{73*cpBHqX~N-M*46s!SW)!Mcu7pRnIjMb_H;pxgY71`!B=#RSQU%;KDJZdiymlPP@)H(Lc6zPD8w5A^=P$L*8f z!xb?@<&{#k5zEl3 z`^~umJmcv&&FH@Of;*^vBP(2Gy;iz4ds7We0%VksJo(u)iro z!&3!(?weNln{5c2^}Yz(^Q_qm#je`l%kZR;kdkszw!ea4_tv(JOm}j$$a}D_+$}@X zz0ajWo0h@Os9SU#+_kBb4<9&QzFbk8i1m69?&dwLvUwTbvvCG?LWE@zF%UzM-v31N z-D_atUPOh^l-E`j8$P8!fP(i+n+com9%^byaV_OjW1odu>BEURs_IqPTz-G0O&+Go zt5fNU5Y8eStSl<(nHGci{_8RxrgA1`#7&j|_6`eSQ||1B1fTS25m}-4OHJ<{cwG%! z@j87P7vMY*xY>+5bN6P}m(#~{*J-T@47349~A9?yJ4Y9!Mm6~APGEE0qX&fRq+q8$cYbkd#okFYV0$^ zants3U?z-CeM`X-O96BOo6nS9S*m6-q4=z#F*Y`WF}7WvWI1Hm{o?fAfUKm!N`R5b z+EoO5Op`O~4+HBziLK%aMV4BwBWCL_uo*RG|9GYVK4s^WMufo8OaEu@pe@ z$mF*^IGXNx-Gs8D+s8mb-rv%6EdUf(u#!}99R|Oj%ZB&B^8=vTtg||$I8!vQ?NO)& zDJW?O^>rqkaN^Df^rJ)OHZggfFh z;u5?@hIJ+`#R1h`-qsppn_OYIysoY3e)9M|deh?&5)uM_ zXBXa~rVWug-A~D3+L&O%lOt4N%!;;LxSVVYg}43n3%R zkjR0l>?%@IrE|YXIU#wZ4;L#EKZGxtXSubEBn7v~zq5b~OAh=luYl>daJ_i+KG3EW zpM%mmc{#b#Jq!kBb)P!pW;;0r1rTNesthANJpf2JThAhniZRjr%MQd<(t1@F=Bn4_ zUtQrASh2@9Z$?w|*+05y?99yR&;bURY^edB!f3UtIB=dn|e$^md9s=XMbh zycoJV%HYvr{GU1Ai4KU&#^tLMKq$UDH$9F;pZ5fgd$GxKL&*neB= z$I7|YpDELYL&w69@T}V%He*FcM((_{yYEpM6@1DdjQBC}eQu6B;r)$O+x05M$(Hk> zaf$_?=k{FI>AIX@=dE|gaQ3&Cdw#bCemsug+##3P-rhcLz8DE0z3BhRH{1GW+xA7j z0ShoFWcZT?-(vjRIjFv^uPJtLOLkq49;bpw3Ww|US7GqeC-b)@1aHTOhQb|gs(hnT zKvD0CN5*BRx+QQ!$;Mx{=T@@ab5Yc9g-g!>|^Gb3^V8`lbK}%v|QThxM z3XZ|68GSq1Xlni82k45Rf4Lcj0JNAzCwO;|)wm+PXk7IKhrT5cr?|!~rQ}LDtNp{U z?D3TeAk|R;ZcoGi8GArD9UBeD7{06?6xLQ*bnB|ArTCq=no6W>0pkx-Ek>-DKw^zE zY+whx4F+Jw*gy>ca3dwTGLy7{?*_cZFg%?jxw zZW}pkWH`@Ua^P3H`1V0x- zv+KFphXD~6d9<{{@A>yM=LJ2j?`O5SpZ+Y)x`FTI?TRWu`GqS>Xz=fwLy9jV7B@AN z0cgzV22wAMYL||#bdv3|LipRSktgYOYL|4AbA?c3gr%kYg=J&{dN=}Vtu1IBkiQ%A zeve`$@rwK_dHd+Tjw|-}GaGg9Y-{{h#8ED-uvAy4WYy9Yd!+`ZV9Ifl0IS zU%$A#|DCqCw}V{BLfzRsnJUdvBJ+dE(3er^uLTl1<}JfWL%YmY{kqQu9E+9++A5Xf zH|%{>LHtChgs)x6!?DmZOG<{0Tz6#uIR802b1-X&W9l$-i%jiEEd%P%ll^_8;O4Yu z30>Z=Nl_|^$L_!c#RL|yR44t9PF zenHZ+X>z9&kBf)xac$UgmFzR*1B=VbT7l6o3}6*USg?awBaBffJ^d+_@DST5zW^^^ zKXhDc-?gc~Aeo`TyoArtrfb_^t@4}8?N2Pc^c_Dy`0zfNJL#4-I)&*}-9Uv&TN{zd zXu15V&>B6ZL_k15M@Ludw}#TH|6ut-5z#$A_)n13%?CTTwH2uQzu@9#0R1<(pVrV?9k^?>=QB0R5Jwd z+#n=px!`i!>ZvHHPdOUb?J{BC0ICD8iaf-K$T6j>eJB-B^3xK8ZhqGT7{o04M+8MR-z(Vep@^4}!ZA*yg8%$`? zxj!7lpz#A>Ow*xCwv9-L>@)NB^QSQnhZpAP>Pox0nfkPhjjdW*!7=mk@o{l!Dk(Y4 zTNGd~e0T{D%O>?iGf!TAEKgc{yO=iX8Nu(HG6%S&MM)p$KcZF0dixIyBNgBzRdbYV z6?wE+2$3=%qCo(Ly3_$hN}?KqP6&B{}2 zLCw#Y!ojU%j-y0=nb7QAYY$lueTxqh=xgcJ+B!N33_8G6xTgCRhxM#M72O=0&T($| z&Zok zQ6wbbYKQv=d4UJNUc^&KqqG`ybd*PJH+0cFQXVgzw{40atR-{IGX}@8yz&$h8Htk# z|H~v8p%>=YZ4YPmsK6jet%`DT>fAYfL`rixaH(*2udl8Kn%bydW5JJ4s!7*ChF(w* zl3Mm-J9#8}l;N;|tV5U^mk*Da7#2p26SiOP|1>AGKP! zS(mTtO~Z;)448c(AA+d}gTd*&j(9+euQWY&T0x!u?}~kf`4egx7<%9zB+E?KS&h%*QYxdz0 z5fzlZTaG_}{5Vxf-1aboH!cQS9?rml9ipwRJzcJEM?mIR6x~IaURuRQ+_nS8>U&Pj z_60S<4U0f{w3PTw)=_wE@Idgg0n;ayC>0`f7!t{~{0qCrJrT|=vUGY&t0qwE!7Xo1 zJ~&ahgx=}s;8xeL`(0Ospb~om^u$H&CCD`yG!P0g1P7U^`_lL|Dgpv%(2*$4cUaAk zj3B;`PK(~GKJSn_Wq-qd+X8-TtE-u0$Us+OYa|uhxyGD^du5#bR@9CM#^%%OVrjEi z(RFxP?tSq->7M(vA7=Yi>sFAzqpD82(eiwmW}bP&vqaK>zqj}Mptz}Qzg`+&>7Avh z0}X@>Mrm=R+g3!P`?yYlB|0>#z9|n zKKZ7laKxZ!>T?)AGu-tLCMEG%+Ab@kn*+(7)o4{ZvFZ2nX0EBSyEk}ydt-_Vr}InY zF89?r2lg`yB6cDlleAs;8WMc3|BiA@a$pVYNNs>iE;KY02n=2!YGL=|yB7};4GBB; z_Ww<})_SKOfqV%|hmMdc1d9hpHOlXHGITscy?2T5=_l2zb^!GERsNq*nbVEkw%LeA=CIUi%+j z6Mv2St4gG`(~-R474iQDsPF_M2g1?EFirMLR1yYQ$wF8IFcVTB%7 zU5Tmq--Dd<8u;4BW!!H*Tm5gw%2d9N9>tt**D5X4n;lXn$1Akx*L`2LYLZ6@I59a& z8KV*_=g!GHuI(4zz{Q9{3jFsw7ub6=@6}7leun?_1*QIMnvuvq+ptEivAjW%9>5uK za7+9Ay8<$GT@Dd3@st2bH7+Hm!h$M;Q1hWZS7kC0sg%k^_mM|$>Kxw(~mn;OHe;Mas4;gONCZ$XF* zet+nER|TQ1^@1J)QIbxp_l;?SZmq=x)q(D;Wv}m4tV@p-Yy^K9cGIrq1aV?a%4U+T zJ9U!mIY%STL9~-%1Q{~$sRYrLh=>R)#lB&_Ax9kV-2j+FN~y|P?+7z0!Kq#=9vjNA zpZ=-7myawq7vuOwLp|o7@=f_`5f#~u51=IZ+@B*JfSlcecI>yNmew3-pn+(xP-|1( zdb^M2_phr|r!JOgbUkfOEQk4RPM&NV`19f+El&WeW`JFs0HD~uK4jt;b*3Xd?HAo6 zTYu9d%c{+J;^Gt2MwV{nb5s(O;(?jYVIX17co=-fC@yd}?RHQ?yA(_c5j&@+ zqci9!YxVIh2Af{P)sQ`H5C{< z?1fLff7YN(Jy&U(c-sTa0Rfm3(nU^AKJSwO{>_Y@ z7X#iaMxENj^)My@kkfqg1ZV(Qm)D>EGL-dbpday-mVZb1%*4C=!zjUF6#vhmEZ$qk zDDfQpe*T;}+X|RDn1VD{U}bm*j5{cy!y;keBzgrHk-&?<$YzI&af#JKnr*>Vcy7q?bbkRE5q!EI7#L9SJ&$W-;U_`6z)NY=t3DoHe{1}0{8c#I z_*X|urXP$oMZXO~FyXVx%F4>inL;3?T2Wf^#{hvHx^`BeqC76}GBXqYieK7R*WB#w z?*4Cbq!;xAW9U4y%~41tebbUHZ}|l~J3FX&;QE8v2XYT+KXr!pc|Y~@+s1yc?%uL2 zL^d`t!OFuU&B6>`s13+23Y8TuCW#%AXT%1K2R@4r(PU(e?dwPOFB6yOLED*QQSM{nYbrEPd2e+gPjO~fcdzl4m#E_k1k|E$s zLcumyr$Cq6*_Ila7HYkk*0F23nr;D`2JC6zWG8sPhUVqzd3$$9NI(EWAP{3li+tK( z+sWSxypd&bYA(#rE%7NvhY$FXWQ|nO_PR;^4(j&suh(z%!pFC5|H9$>A0hpfl}o6f z>u~aoI!Rw+^5Y#cD3rSK91PRX>yJHXPvQ=RGq;}3YnUzTkG1BQgt1#qE6PN7baa3l z95v+b?yj&?!!|C-BEr`!kW)=<%6UlsCD+{-U$Z1F1f(i=Jx_}QdO)N|U3gtbC{Im< zO0VhP(XsuG_}1r(mQ{m_EwaD!Pez&|rkpK#lAF#^CjM%(9KI>-D1L6O{^J51t*gvF z9PO1u)=|C*fUfcIG=k*~!oD`(o@buw0HPMp(+(t3@6!%2uoC??z4y(Zx2}>lR4T;P zO&3vM?*>!OO0%=GjXr;-!FriQXMMwznhSe%d3bQVtflX_E|P#kNx8};K}T74!$b_l%i5Usn);^Oz`b5o>{DUki+I>!-#D_f9G(-<*Kd-wW(nJ@2z_Tz&87 zV%J7b1eQilH^7p+&#RKEGI7&)xFZJ(-iR0~9lqEcYS@E^dgF{Z$Bp|wPV4YK_)^57 zi`f$qp>h$?^*VfqhldIMpm1D;id8`p)=eQL-FrBvd{h{_G(lTBL3ET(7#`omC@HRS zWB~lH>C!5wR4yAv!ul@m!3%|3ZP-bNvtM$Vftmen2>0-FpC0s=VAT9=@bDj;QP6%p zGBN@zA7L^fHRV4CDU^pR{9IvQf9TvlN(iR%b~@ z7;oCGjiLDGt%xO>W6y;iQ55?pPg#f8Wxv(RQiP<>~Q3^sHE;T(8RP8HM>aUFs1>bTTiio4Tl- z959l-niVH@mA~Czduf^1XBmahE+YUzz z8t>`#n*RZZ-vSeX{IO8#@bm2P&!862ZB$~2`~kbb6nCy(?@gAja0_${%7I?=5`BTqI1J8eJA%f6`Q7zo`YN5t|w$of+eaCF= ztiOIH-q*{$%oC=i$nV4w6RAXuMo7Yb_|6ajhMdB2ejw2g*pOYrPnEe>c8}ZNmzwMdNd!7C5)%~x(<5bIFom(@awL%Gz$ZdW__(Fl z>)Ed(daYYzOU>RNK60lWj?VSb!u(F_DF4o^`um509Bzd1aU(EV&rdlcI*0YLNi(_epX;mo+QaM|-AkbGAHn#7u{~vr}jC>uL4q zA-E@BmT!itG6Ha)Aupe+z;YaKbrhWHB~PoFzP`Q+<8q!f8DPJu2jI9PvZ^;#Ulu!H z7|CFA{jtd4h%zB8{z#(jilXiE(Yzucn7SEI^0WG&7wR6FvJBVO$!!1)^6RwK@4*jvTR7Og<&JDXpPS<}A?B)#8xmWpMtzTBN<6TnzHaYhSAlN5OIpA!nF zkVaTSn-v(u`Oepa&B?+lkb!Y(W*V!N-VeO~r!&4b&Lg9C7gU2e7#Kyjh6 zu`zkdb~r=CiDWJGJy`pp@4;5Cvt0t@zr=|eynFen>2HiR;{5)i?_m73E`dc{-FYVs zO%>tE=wsGnTH7pbld001HAGc^q*RFCx=*8Bv%j8JWC?S|07ziWT_uJUp+Q!=ZUg(YYW%$;;>P`=GKD45!NG6^hCzwsF&;cbYp9%YG~k2 z10U)aVyq>_#pK1pSlyBo*%LO_bQ}of@y4-<1281Fgk*AJY^hv)(R?>?*7?1U(nx7Z zx+dlXW@rs?yG!#;dxD%Tmk9}xL_Dzg_8ZA+Gu91`G9tQgfB8bLPu`Tz5QcTH0u9Kq*Ob0s>s;SIDO}P$}A(+gK%}L?}O*&CVfp%MFq=U)$qT{ z>?uo4e?ANL?lRKCVq7X@SQJ7aPsyeUes0B;s#HxYPRyoT@f#Bzg8dEl9hueR+84nR z!CC#&q*Uj7kL8Tcl6P(pG@r+}N?7{4-fQhya2c8xFTx(_=Mdj2ss-6|mH6m0(oslN z{?bfRN_xwZ=HS_w*lp1cf^#$?E}@sQGBShU61NF1#{I&B(%m+#Etf^3xAVE;7jDn7 z$jjRCP>y-8e90{a+Sq|FQWllD->3pMv3gIbVTlNuU-yh#Cm+rC4JN7AIw2a>#_$>{ z-Mq`Hi1y*w9Z!3q3#|sau8Im%j=4Q78VigNf!C`ZkD!zYQUySpf$epW4d}Ow#_vAi z(kdRuVbyGMEw%7(_DS4knd>hzpdS?_G#m8dOZUz&SBMDC8*G>6p_`N;^+JD{d9iU%RWaKvM51;r7CsLIn zI$Bnf$=?*@w<^GzSD$&n+a(TPlKexc1haNySDpPIBNyX zg3)ULxD@m5olmlfP|s|_B`1O^wz89mTvsu)#q+AJ{846?iB{qzQ6{WIiEiU{;1=%v z#?wtNUzChSzq1tZBEory-XQ}BYeIay^0-saYCf84k3%?RPk!mQL@^?YfSNXL-}`!+ zP=Sn-$$9$x5#{rLa!)3*`oBYW?k6qS!FUjU8+C_*GMx3hV&z77;)57$;ZVLft8>mx zV`BNfjjjX_t^scYhR@+HMrL^^4NZpM0Jz|or@j1*;9Bn`+-ghw1T@ZYr+9lAsFO9K!19P$K0i;uFk>2qI7Cl z!;IJ<@eU)cXasM7%`D%2cW=)ypT{Xae}mq)NWns!v9GTR0ZT2EI8}a({e{PVA-Hn( z3qgb%n*JpTCQD9PPNR$e30Qni&+@Vgr57gh6G{ggQkm*+OOcBd(r_7Ank}WIy{}VkRLmL zwVa$dQtACq9agGqlAMR;;sZrDH|G<*v!5FEtRuhfb|o5|Mw03lSDse^#Kpx0gezhO?3&5gyO&*K%I{fyB_q*GH<9*E64d9*QWn8O>ed;2BG0R~a@1z@VOyHLk@`NRc$0mjn z*24V!<9_+$eo56x@^(M7`jo}l;TeV)%Yu;OM|4mZKDTzxnJ~G%69L?2q-K9EHB040 zOB7g03db+hh3kUFuRe0 zK8F4R9{Hk|+~KKFbCI+m&D#p7nMC%GxI$HfqQgyZVitZg)`q)>$J^W6Uec8&<6eCn zCV`774NTOK`DHyI5ZtDZo7zZg)4@)Dr$1kRwJ3D0s-U1CBNO(}0z#~&#zqKw=`(MW zsCTQRJvV5)fCn?&O_%IO_St)`+fV9n8KM+>)h2iIlh*u@AuLJf6g;Y>b z5cej3hxiBkMvLh8YVVae*nYqeff{;fEUd6U+G+qrJ4_4;6XxE-l$#WDThuZ?H+Gyl!o4)C)`A4IMvckp?wq2!RaUG&ioIko5edV+xX}h z_olS2rY5E~{o>~S}0dgq>DC_|M z{b6lryZj4LL~{36&U|U#U%Bs01$p)A-_yg2NXFB>7vat|U%_Bmt^c$ECqsWq1(FJn zCQ@@;;quf}w`Y7Zk4%_^&1U5L!^EnnmHy7D*8aWZFVX5%00u7!#ZT-MK#ALzqvBI5 zz{kgD!Et0E9vewWx24)!ciKOAh~lr!Z+S2$(m?qEkvD+la=jQvIca7^S^Mm2JRR$0 zXLq_5;)j;{S~2$rq5WsOEPk&{O6a0cj|`B>PK$jA zBfx<^g7Z)GT{g|%G&>doQ^g$)OeL%d%D0TV`?pi&0@tfS#(p@0~Q# zYA10gcyOe`Ag#h1sk|oADy8Yzqqr%Dh~*siif(8A-XWX$yi~MF4`6>VTkBlM=YveW zs=j-dK|3BQJ5G=H=@-bZUotK9d`+})#A4Q!|L)CmgyCzF>CAx`4LL9~1&#Iui0*r% zNCD}8=q7E*=tFLQV%ckOZvZ43B%Ek`_UgbdmEsL(`H{NqU7ZxAC3{S?iqI3lg`N?+ z$tAW1gRSWQR*Y5%J}@m&(#SQcSLHO9n*0vnHT6|hDm(Nwb5(3hi8m*mbcWb)VtK0; z9BZ_`oEv71RBuZXJCfP0=wICsP1>KMC6x^@CRlX@A^?oN!$5JNK?iBwM=G{5O@WU@ zLv5yZEX!S8fk3g39Z`n@J*kK0LSX`A`wIyf{h0^0HIcV$)&~)PvQUX_KE~6 z8;!K#e4<+vfaD;Xp&7Vjn*i+xqOxf~f9_ws8x8-~!!DfTanMy^hA$Pmt*T8TN^ny4 zvfM*oRXp|NPgwrNW+mI)-l1R_&faj9lY3n(V~nAN{#~?B#@b0hb-!Ix`TJMYssh@O zc&h(R)BQH5#wX`-CI}?RANdFD%Q2K!(=$lGGgO5B?Coesyk{{%gqzuK!WBy!k{K%V zz0|ye>C)oD!Uk}IR`G;c2sL&dBYZDUM0}Aqi)BZEZ-fLAjgf?eeKae5PPvfmWv&hb zdwcsawoo&B?=hxZg)WQfja^i*JC%-ru`|{OA@&>-Ep;r@*6=1;u{#`WyTI=sGhuSRHsTIKTKQ-6m6pCz0S}S~(oyqsi3z(xevX3p-w#j19&9lte2z zl90x2QsZNbqaQ<)&fd|-O--h|)<0FOv9s+w#gSXvTDxgV4E7v8zTq41nH1e6q{gP@ zBhS5~1e3PK=DHhF#>WuCk=Blp8DM7>(egVa`1!H4vf88c?*Z+`_ELxW;#tjZ#2PL} zf>9XlA1?&kCg$+ra&K=cBpabX&9_=Nz%^bhtlOk`!jHX?ew$Af`?~*Y3a4l z4*f8LPR%S(um>(maSuPyXq|ZpNYHb2n-}-z!eO({HhWxvl47;?YcYj+(vTrnoM}_@ zgydv`*j3;-ccDA#*0#`c;l_)(ESgOZgNrJP>b*Ua;jy_&KB|Aye8tso^4Gn1H!)4_ z@0$IOpFigW%Nv3~1q1@-Y^kbu*kN&OEV^s$S7G7XuIu&yI6yT0n(dyxqsxf9P(o+* zf4%^;{{R+7c%rF^UJ8P*($Z4#Qm6h{N)UXE>uC$0ZNa5H=l z_P#L1tYX^3z^+2z?_KZf68PQbE}k;5+v38@e7wQC|CK;t?3%3B!J1)?X(rf!3CyCu zkbfvkpx1H)SbJ-0D=g0(pjm)EYKVx409{3>GB<(IT}L#Qedm1J22KNjdc1N~QG_Io zxMz^4@hrWSy-+oOea^O-L=|^IDJ{_qTf}XWHr(xjizuGs;UvYbZw>&Q^?r4?f-3IVc_HfQ>@W?sGnV&G?XAx9OK z3eT$`+Pv)*of^EbfEqdN_Wk_=3K40UuUf=u79Zwfo8->(mP=)efd&u2_Fj_3EbN=$ z3hr9#OZ5AJ+S}RNF7iW^3#Mp&3qI8!HWxyY@o;2L`sprnZ*Mob*D^^LnyRerZ;=e; zW5(?x1`hx49049vvfD7aV`Xhkgoj6V!J*&__?@7v3ix%Zd}J9}v&TbP0}e#oG7PdV zZlLeJh0V6Y~pdyeRTFNJy)FhJyqEs4$}U64MxAb@M!`_=&gS z8(7VGD2Xair9ti-5fKp=7Y8szP0+k)IM zf;r!7gUWQYTAHNFrg+WXwKVR_?2XFKGNEZ{rL0IJbAtQ~C!$_9e7xzDcQ=4%q*-Cq z4MOJX?pHLPvG3Fzp?D7duj(`ZU{eGDP?MI7{fZi&`A^|^QtX_3h}n>yhK70aEM@4s zUWbdU4|~kiWhh<-iD!MjjI^|cWo5@lNBlqq7E3AP=#GXh#`5MndesFVDKb0=0&Aws z4ll5h_JL9&mwu4+Upo@W^(E#T(|qJ;>2=fkR1oeQe7P^0_O^Jo#H{Hvi!47Rf!^b& zAtL==<+pZPi${>KeCM|C2B<_+tjstRYj<2fKM z=;4@vka!)kKzJMh3>TUW4o3?uDRlVA4KO*Z(;fx&;%QZvfFTOB+T5u*v>AUr(%x(= zaglCMaFprbqqO3>O78)yW>W39>sZ2SQvm2?@;wV%vv&tzJQ5m7uB{(9OtJWr9Ndn| zU$ehf@ZS3VzRf4E?>pH?N<<16h{Iz2T90nqBJlR_^=FGdrBn zp8w$|_ToVo>P(Xykov`y$n~#qN5JK#bFi)YAE3g>O{y<=j$n;C4KOXq_I!E^s&=4x zLQ=>iNtEX!EV@_>_o9xrHaMk$wLGiz>7JwF_wT#uIm*%x4-cjS4;eSJG3I+7?IX5N7K=(-i(yI2`U3mcoIY75MKRhP6vJ)y*NvQoAmtL81Hwd6a$ zjeGc}I!XA6O|+;AwhGnOYPxmuNqNe0Iq8;600Hfp&kX_0lbm&D(CZ8U5CHI)Y3a-F zaSdkP*fanumh-W`ISQix9FAvtj?1 zGEbhOMA;V;P0~MX%Ay-cVF(BbqlmcAcXOhEwazjSQ_sHueS~u)q67dd(LCLQDoGBK zoRX4~oJ`^i3XP<`^@W9dprGD-+KIAL`Bwk;m-ZyIv{B6Xue89RK}H6Jl0QZy_QAqn zRY+Lmmc7QNIXAC5A&d>+tN}CP>+C~bkBu(5b>P3--G)6 zeGg|V_~4OXt7WH}5WM~|l7K^Uz)(fZt;3vAbnKMk6LX#c;Z0?sHA{{u`}n!SFuW?) zemT5lUQJCG;Ilxv>I-mws0_g!;Ok}IQCf7yR@ z?W1KuaG|ATkF-O4jI{!xO4cKNhg!=ilJ{a4%mm2}z6>{&r*FN}Gmqr+a>?tMur59cbz!if2($%3Kz}?{b^35ha z*7jFg8!Ow|M6d$qBOLr+8Qu|r>EmygaW7Dp-Aj`N%^$+{_j8uN%w7T=L+t$OR@Eos|vlec!%Q@PN+#-PWlVB*TF|LVkLCu?# zctrbNy}iggkg5_!oVH$CT7m$J|J`VT<0haf0r~;Zb#PLy`W&_^!SPT)P_f`>f>0>X zBY%au*iXDzo0*a5yQ398MY;XVnF<)V0tV9`zH{4wRcq(F1JrQ$n*>KAlSewqvO@S6 z==`}EtsF@_$pF_uR2D^$?0!DLxceE2P>C~%eS7`m8}dbyNShg4rZ4aa8gZsN#33$~EQZNtY9Ki=oy zG)V(NB#|u*M6KV+({rIE95GIyUkvAE#B03Z{$Rr=B9Z&okTUJycAvN0&^7nvF;Ked zz@ouE*|_RX95^>O=K`o*fIw}gadG)oOc&aB#~Hy<*R#`5x&Jw^v$)hTezpUt z5+`XnF;apO&wpm6FTb76>w9h(VB7!8XNix;uIZ=;bmoqlnlmsobgz&y-9nMJVNdma zxR?j$8woG(;83X%v{+{~osMlBnG*54g7J;Cn^O$ANQdjb$~3IF7aJ0WK!&;L^jVyM zxcjgY;m1;a(>7jR-Jj)#LDr7zO|b8{sVAktzZpK;+oPMMqMK{Zh;;319p7b?p|4KA z$!Kx|38R5_u1AW{s<8vxaN#F8_gJWq8*g!~qI4+Djq1hb7gPx$f+*dQAq4<`} z^G9&S{tKmnCmd`0=V^uR7Pq~_Eta-F(8J13_#eP_;tFhme83OuBhI4?CAvC-22QWV zqu=Swv!Ui$w~4WStzqC^&O;W1&rR8D@kRc$g`xBrLF3?SMy{=t#ZTy#d5f9L*Aj;S z76jV@85tR{rvz{Shu){GR7g$tq0~) zI>B+?nv@^-q)JT05+xCW`l5)lF7JrfJP}yiAdrg-8LBGBjlPnSxk}${aCcybhs$8y z1S<1ljpS%)L@~!^JtpTq_z*j;LIf@=b_G-Zn^hnzM$-TD<(&Y6f^NGZDQmw8(E8H5 zjR~U)P_w5;p}n`SBSU`%P$vvhelxklT)G2$4(!`{bGC&HBPX2qA#V`aX3chtd-T4U z8-46KI==z+JOp6=ihd958L6?yB3N#pFI|uFthe1A zw*sW6S6C(B0Ibo#57y$RnuZ3(ix(dXc}-EV^IS2OnSaodXeE??~_WBoT;4%!Vrqb73!(;=Cw(YO{?;s~200W75s+S4C z>M?~Z6vV6@NN@61c#?jp%RZmNrIKlMvj6Y&^wZ&?o)fOD%t&aTvdt|&P4D9%iVahc z^jm2n-3Ik3(jp^&5)I}sbF+q4ss1b|`qIDE0&lGSR^a9B#01uZ+({l7jP5s_c(=-{ z^*jbWCPpzP7s-QBb%Q}0o`0tZb~piTw{V>K4KQ2?PjkMwSD+Gp`}Q@m_}=VvJ{`K3W-{JU{i+lYaO_F;V86?Srbs7cx?TYj)ex zlDoQUUUC3O^5VtgoQCBsrxp7w99Fq10XhUtXZL`eWq8GQIp>+^}%&1dkMVEHu{ zDw?UzC?Kjc4FlLeMuM^>$zM|C*;F_O+iy;7+pJ|}(HtolC@NW^;Zzl@dG#Mgdy$bTS0tEPz;3Fif6@VsvY(t6MGqClT z9|jEIRNkstlq{gl0tfi%; zp0+<=MY@vk!9M z+=Wm`Z8_Yn`@|HC4KdyqFah^;e1875gFW64IHkC~88VK6_;hepUM_8=^5ErQqwd^O zbJI#YK!51J6(Bnykih`<)XuWltX)XRfoSc{C)zQY=D$7MN>;F3WNcTO;!k^z*;xpdUD{Fdo~K+pRIl;y9CA+d3p>NU$_hA{TD!C!shP8+N--7AU7?9syUtFIfSnh8fsD1#X|zB1VD zVHv>A-3HfIFQNF!xldl1O36`(ncGia-#<{X0L-8uJW9`+pbf4?CBCCvlTX)}442{~ znRZMcp0q^tlyIo&KX7$IrMyvN;XQrN13GUEZJr`VKeSeYL;=kGXntzg>-CeA!{1}) zzTmnN(_WL(V26K8O1ky!0B$ySz8zMci511g%0RT^$fRBK8=zp}5+>j9jW&6(Eo-+;@2aw;Ya=bm=J@IN!#;+kTrv)x6@$0Mx5?D4 zIHtcOS|crAv>NU~>t=qcIgO7bMR&tWwKSqgc?-gp0N)U(cB?6;-;7mpm~feiKRmo! zUG-&<#C@23Wilx16)7(*b=|^G(`nXwROb{>1$4qFC>Ww1xv~&mY+)1rnth=JxdOWN z0~`jO6GQN%!k{zgKEW)^%mqAqz!vL!@NX-lsaA11oRZ4QWb@{_I@gaMzxcKxLcTrr zYjp%vJ<(PIDVvDkSCt<~sR^$s_T`hB1!6+{V_{t)|P5fIoWTRcjB*Z zmUI$}V0<^fq97D1p8Cf<{xu2`l{Y(VFdd+Tf4$zyJra!?Xv}CLnmjyYs##+rL+C}} zB7bKf)Y>$b8EVA@E5)N?i-tqv$XR}$0@bIk3zbSn^j|_~A-&0a3%tNgM zW#Y|h#cea8)O_nSG&FQyQi7UmfmYkMXub31&hYU#nU;*$|Lc0;iiVRg$bWi#oDT?i zqPFTKoJPqX)}|IO#7F*$GC0Tc(a8x>Y@pYV*h#T@N;}7Ez$QIr=E3Z z^1`7mEq-9=czx}Ok(ZNF9e=s|Bl~+~hBv}YEj-b?%nq*UQ#Xp;=Wkq-^g!enZ?uiQ zJ(z<`Zj<;O4bp&#o_2V2JdJ63_+KPD zJUO$mBV*Vkm20=!!ulGbKGf4}m8xba*LO-$zLbpz!&iAZxuf@1WKtsucPLfXi1~gt z2ub^|1U7vh6+|+30gIrBP1W4IATdgIbW4%~Nk-KP6GIdcJC=4E7nf1SG{=*;gxi*u zKnm+KKoeC4Vy*V%Gw(4FP=MVtYB1wW;J#5Om%DI+asSs|6E*bVu?{fJ0LO3mf|hp? zc+BhyiJMH}{o2QLK}@cwUOG*VIabL9A$E3$pdAi)+Ht_HY!kP8K}n+Nu@%J}6-9nO zq*)^*{bqpY`0DNnwi9r;zn_XjyYbFO-bN!&`F(l+R;uRmd6!^w=kpRNQ9YWkl&vt( zFYuCF_Ff5!4G;eM^=o2cB3ClveWO*0osP$GOY*uWAw7lgc5OV|a|#6Z^xX5G+xWM> z!Zk-)I;e?nps~cH^Bf#(Ao4f}F(R@@F!}BDsy1*8d)9-~J3XA#+ZXbH?c?1dCDr05 z{KcemeH{y#b7sELlwnmwclY!BVR=QxrKeyGQ^IuO6zLxEj0FK)wnHpNS6wn-jm7jQ zR0NO()`W(l0ew3p&*!QyhNM`lbuPK1DiVV*HKVOxa6Ex^qwaQ^$vJyU9o5wTi&wo6 z`G{fbm1v)U8}H}#Xm5#S?=Qi#(3+Bd)_YuXN~br2wC5rwA=6qG!r zrwFyt)b|-KXq-s;9iB)P&Lm7}&mFyf@{c&K37P@}18`3t{6*YmtNq!}e|tbyS92cy zttKfusrd9NMugrUMe=2M-tI?>Jb4uk&+2NYmK2Fv(bLe7BqYS}aINCU_&CeR7F=)< zpaUus!`G?hWM3;Ddrui>1oq^6S3Kjws*i_S8PGJS zWyj@6VaHwZD=`a+NYvP#7rX0@VlX6I`JHeiim#j3*Syt&#%5;um6e!*^M}mz^l?=5 zcoCC1pscF5lZqS&TNmJOArH9MGBo6>8rV<@`oXvRncP>&?jv1psKJ6qrG6_vurbZN zy;alJ{=^bbkzIx^yv`~mC9JM;_C50VSjlm$5X;xiy*(g50&RdRO6R(vsw!Zw?fc8n zHeeC7>l6_-mj@7DQF6)p6vpjRTy9eq{GSV;Z&BaY@E5G_^BcM`S|%# zk?!55kJvYl+b-bD5FM?dsrg-HYAMks>!x$ucXxMvUvQ?Ffq~Y=MnSjMd+~sHer-X? z<}tUN85iUl0G%$9SW!_?_LZUjm>QT=fcG6pTlUXy1`s*E@-=51wO_ff`dta+F65Ws z0%l^bQzl@SSLzfTFZVCvp_CaBb!z=ME`Im!L0>=SIec#ao0io-p1<6KcQ>57V4)Nw z(ftlc?lzd^+fD?Wc+UXr;M|PIYVe*!nh`OikE8u(6TYc?gl)~r*bGK35LfAZbq!#@ zI87m=B#Qxw$U0PDliMA=ynm~B?5#B7qpeNYP63+roysik(~M`y=BeJ@y61P`a^YMr zT*V}pzj6y~CD^6NP|3;5gP`3a(o{|oNIX@B|Ls0Nkrfmb0Uc&gl$*%ZclBwm>1r+c zxZ`fr%o#}u?ivoxv804H?vG5o@2qSp@(56CVjP`T{wE41(E3QufTrG>mCMPbMP|r% zhT(3cLf?50sk*IgW#{AH_CJD(OTXF{4X{QMTFK1XUitm&;P(PUa&?nMn2nklzf0TN zR7)PFP!fq@rKlH$T#gcn0uCS;gMg)2`}4DhY_}F*q2;#dE6VLj=i0NcW=2?y1L4hZZ2koh>epW# zn<-=;!Z3t3uL9CxGWcQYgf7i`eVU>rL|GLkq#=DVk&$4l6vO)HF(d)9sih)1is}@? z03}W8iMbTl)>1SnMb3d6NOsRRVvxX!`aLkx1L&;LpMmUe*HnRNk{gWd3Xs?N?-)=J zvqOvZE2R6{JKTzKad81!V-V@0>k@h@AQYuDMVoRa3;`~$+%&L-Q<^GUhNqzy-#(ta zliYAW!U70lE+SKnR0dmujcabyljYx2#lHW94pA36btpkk^Io!27bVT$ zRfn()XalGISUC=0vZJjn%k}Kc!LvLh<0ak0SA`E(wECYydOtE5hA-uO1F=|%iQ8{a zz1qUOoO$4c7|cGzK33HCq`J(et-G+TF}n|Dtq&Rjqu@^zX6A&1He<0ctJ~xEvoBLR zMm)3plc@GrJ*24yh1A_8Qc=eOPRi8P!zIM;t}vZQ3aOFrDi73_RPhlMvZrq9n83eY zb=iF~kj7)S=Jm#@iTsDjDu1RDi~r$-CHaNYELFEntEhO;Mo8hgij|azkgdM_h%rdPm*6v2+} zs}hb3wr9u2-|6|qbQk+?pl`e>fEhtJH&Y+!N~#s0DdNfwYzN8xktcmHQj3` zmD}U3kOweEKQ9i%|L3}a_(frm9cQu_GWCHc!)Z>NB=w0?Mx&1_#E#(2Uvq7D{){F0 z{v^r>`=xy)R0zA7(&8P(p|=)gbiPB`cD}-JF^1^Vogn- z_B9~71Vt|x{=g@VRxf~mD@_FLZ@fqHh%gQ5-uTF=1G+HRA7gLGM-qH=z=wTi*fKJh z`)pik7mKucS?x1H3Qj_RzBaby`&O=kU@PN5D0He4q`f%!pMMaA{QN=p3qmCdF{7B8 zz_PJEgR9pmO-=c^QzJ3S-giaJfMNgCPHSp+rE9-sY_Sx&%! zB#kGAIM!!CLy=T2!c9MY3%F=GN`F5!3eH}x&G;GU>yH9a5KXTVzh~z8YbU9As_C>J zcx(oBhm|5xTO^_=zn;+qnu>}{JSTx)Yuo%fAT468unB_S=^1xT7l@RVC;t|(wzq$r zG6}JRZx)yzM+`w4XiEl!2=1)`d6F6yq{i1$-&vrePEyt(_L6 zpP!j=<{<-ig~~Y*Rs%26ZBh&w%72{Se?me7lQ8WKxV$AJ(yy82?537J7M;3#CpTY? z_C+6A;jLkFXSRx{MoqvyUOSGM*;zw14ZuSQJ{C${G$qyM z)r87+a2J5bf-6}K{Y_#^)e~69=c%1%C<&SyLz+gqN1-K)pnEdLSbB`F9*oi28wlUzh`vq zp3lzB)u7L=#Kzh0sqC_q?e8L8dA_21>R7-RWVE2g>ZDDOz3fZHohfcPIBIH1CR@tB z8(&4pf#aokho`Q*t=qiG)JlS)@^ZIkJwVbMa7lMf28nip4_VL% z=`NtIi~3sw&zMsnB*7nA9|-bNT0^I9VTytJJVUYB`OK;o&nGT4@K=<~)#YSMa(}*^ z9o9sm5)F-Bk^P;MNk-B?`W+_OE7-gYCg%|;rC)^yzph814z(}=JZQw{FbZvZJ1_8R zrN@K$y><$}^e!Je=T}71+^RGI+>cBpGnu%x#9K7j7h{yVl~W+xvmH~zMW{URcN>g_7?jM6)q%>UHRVYqh2$3 zG)F7>mV@v{U*Hq>JIYn=&zBKP8=p3nViEI;ow5JE!%pc*OXFH19i&Cf&d)dNh%l%I zo04`m6bc0%g0{9JOP;iB^n&EZOTakgZ?Vd#dTxp+g_5$e*u#ypaFJiE3l~w;&iIL0 zkJWzn;RRk*W2k-6y>@2Dx1a0+rC0JL-?(a*MUb483R|gNgYzTuxP+A6*#&PZnH8b~ zG+)*%o{Tc#ixTp#MGd6avsHKar)}|oeH_1#4%QMsRu-&N+?CLb~+S;iVoNc{Mz*$Ov z@^)lC6Aq}xU&RK=8Uc>i)wY1J|8wGW)B!B`ATSdeyV2V*es!LvIhQ^04N!ut1on&e zT}F$$oG9b6^TqB66A8&JfH)$gKlWJBs}=y^&~hfUSw>V*p!Nikk=+$`&3Y_JWt(XL z+ItFC?C3a%Mx+O?M6JUk`QSA0mh;cj0Y+KBNp&wsy=l9)GdHKMcLYyk;(=a2i0@D! z_Ti#!dTiOFze6CagJUzY=xd9`4G~xITJ?s|zttmK5|F>ZSkFt$!kZ1eK*2q!z!^*O z`e-3INK_Q-Gawimv1x&V;Yn*Lbd^8W;VY%fRL}wCXYtpjZ|Xk139E^d+FRoQ26v2? z!$nljlUU2z-rW>|MB2auyw}*_?-dl{Ypbi?AON5OZ18GN+8#=II0>gSb;L?xNdZrX z0fp?tOCoXp1tlfj8^3t%4Xm>iMuyp&f56EPp6d=T8QHKz0MDhAx|>G7fHv z&x?m@Y#9a!l);{SezVu!#oIJ$wGc#sj3bz!>zKbEJ`u*hD>eo%DfVe)&`$+j4B*_K zyNw%{?dr9F7Lc|{@PXVwDKS$y#uiefc z@Da)XctoPIRJJ7wJ!X`&2{sj$d>wo|c2hPSAEv9Bt{(TaWS6ND4)Yrl$5#IGzZ$K3 zVErbTA!~AFuX8a)6II6BE}Nd8F!I;gEx$$iB&{cW{0xm=IbQ;s3K^T?2a@~M<27n$ z#c7RRV7|exT@JFCFUhOD@z=+-<0oDnisFK1_B52oe%KNJCZ7!r4HX9MC1igeHP~=5 zvJ0_^p!7N=^Tij!ng!V5h?R^m`fL69D^mVfbpVSrTXw(O5-P}976`r#4gvAz(Q^3f zAUD(wUW&Xfi&6p0l$n)r5w2-jqEV;-HV!Pl>+89?*>uWc)oQW(HBZ-+t;rq0k3Vp{ zeH*7x$k+TqXzyXwh#{StQ#hh&nP*SLR&%eVLXJb%9^PGZ{YCde`KHWIqv<@3;h3S5 zAmBO=P#YsU*;%%w6)qc2A7TPO+4?knJ z3ZPy3vT1(hQgnTMBFR)B|i?olJ+%c~6VP|t4Kws?Ven6={Vn6MB8c<|u4>RZN< zK!$5T2)Tl{C_nV0yZ(A16x#Hw?Yt<}due_3S#Lz@dIrh6W<6-goC5b|Xhdon^aUoS zOKp;QHNpD$PltZAl;;O2?)YN=vn}4_XVVVgKkdL)SDGO^#ac8XS`Ptd0 zRJ~HFBEYSOZ`L~oNhnsFMBr>nbcgTv?m#aE;KeM4eJlFe;7goQvi-XXP9DP0Ne{lf zAva!!o+msyt=0v#!JjEwOx*13n`>nrI?&GG-Q%ylxmIcK0ld3a7(Hz&?dxl2S8h;Q z`gvq$$0DM}lASO$%L5JEeA1f5BM=p$UOBJ((@|u-8vutVI8jh5H6FYq7#p%W2pX7K zU)ER$nsK}32fnt1=qFB5oY6Z0_sjS#qL>sb9WiWnc)gSt%;ashB z*1LVpslqqFu@gu>?{oj@cv~ki4y3p$$@pE1PUKwb%a)`~KF`aa=zC1FFE@BJiubkq zE}g06O+tRXC2EcFa~kbtL#IBp=fB1EIeNH{*c~5F4~q^z>RAq3BP)Aed}J;CF%t$Z zQu$Ih`paF#OVp62zdRx$S#kwT!)K-nzx=tskDmGboSVKhN=Tw)YLTa z;7#sWNh0jK5Yxf2cPPc5v2&(hS>#xQ31|=yS2d~4C#GUwSDs?(*0W)*57V3^EWB?66y6|N4u~PLogRmK1Z*X&E6pK&9K!`fg0-mWpQB<^?5%(nAzPrr zgqY82TA6CXO;V4a-|B~aExDw>Qd)N0^}V&;&wI@Cw`GIAjO~8KU&VF8*Ico4G081F zASeJNl9OOqvZvbfzzL3JM6?QYuYFi-#?Lx@ir(_QtP~b_sJBAz3g>lHnm@3Z5_HIV zN_2QcCGFz;kXM8G;>88$Te$@Ri4~7jGj_so2@s`buH9f)YN6(k<0?{m*rEe-wcOBt z-p<$?vI>$`0YAs*TgbY`dSfqJq#bR6Yd&VV@$XhfQ?%7uR>5eqUAwxHhWL=(wS56x zN>9UH(*D%PslUaY!Xb3&Rl!&X-t8}~wYu>_Ph++N?t{ti;ULG~qT_vD`Xc*LYab6M@ya@1)B$>lZkUPsemK$KNA4oF@ z8VWQ@FX9ea6XiGyVHZ*MMGgMYB-O<`1@h`Cz}yD=02g&)<%^T8LP zYmf(TQi?$QBYd?A0N{ovpTL5@++_l1HzHp%!StEJyr@h%z6c?5ygfN=`g z$#xDHD-J#GLt_CWi92H*j(B1}KACI3;IxnYfw4wVSct?~aAd;fsQY@eLlfKq5GKqU z4YdO61&Vnk!#+lVO?_G`rK93ShPJ1N`^QG{YasTx(kE}WYqaivb42f~sr@ZuM)>x9 z@82-F?c`ze#4sS4J9R-(?v&_v5+qriiMiPjw55pjE?;9Ku7YdjgI|WX7JUIePg=@j z9~qWpz)>^_C-xCq;Ovp+pZXxAPN&b1sP%K>;da%;(~~!0u#UCv*s;@ZzOyj}Qp=Qo zl&i*IXKxP@38PyI3Yg0ku1!v0E*29CDzI5=Hz%h_MTWl71`mScZS5v(;l~s7r2re4 zc6M~f3n#V#VUd~rhu?{X>?1U+!fK?!NR}Xtzn~v zeCu!-A!8f@3w`_ZWgm1Hf>N;>P#%DTPu!vMf>abN{Jkb1_3us{I|j<~*9ihzQ4S7{ z^Ye2X8ylcRIl5%8MZr&C9xjbHQ6(MM_o(E8YbJqi|Tsk^wsi}nIX*m5V0@hw}~r$7bl4J(ZfhP)*9ldX0l(NBS^ zZ$vNmKB#|?6+~&JJX{+-^G&Bdzwq{R{&=SP^r&j+1G8}FDD_!J`|n&o^f#`? zIhJBMb;`1hsNIfX`TO~`Yk)LFE)R;bkMkZarnQ>2Y79}4ky=TT;h>uby%3M()CNm!6sf*Mf7Y1j_SD+IG;C_Zi{Oso zTF=CYg{+lyVe_;zFMM&Bbvrnv*zt65wec%Y@ClS)A;rZB(wyY%a+I=lU_(_(4qDJb9Mv49J-Sn7 z@*Yzgkm2L(digjz#xRT{%4IadN(cP_|3ks;CD~U)+^>uVi3=Q}*{2dP3PlWy&Xp6PK z0r$EXjEumy2&zauJvzF!8(2}iyqEd=~puK z1RRX*v_EWDp@H?(<EBHougFn3rDC`T&kJ1Ckj8eH9oW~v!jN7MJMDWuSj$must$m~&fwDuF6$-K&<)HeC(}FS@_z7J;3Zf6d|}9O93ty#59F!f<~o zZLF-q$&(y#%sXpiX`s6AsIa{bb=Qev`VTMoGf*JB(RobIKLIyb@HrG)!lA!-QSJTN zJAcVbXM&SCLt)pMZ`Nc;?aUyc;$Qsv?$GbEvzmT|R_)67A3kh5H_H{Ub8u``SK6&2 zx9OYDNSf=WTB!c~7p0-syxECAgV}~<(CNR*IqJ$U#LeB;3bz~ex7s}Bs)T6*Ft4Yk zrhsCn!mty=2uuhfmepLKsHz|fHt`eP9qAW|4%(6$z6vK%RIiCo%ihqI$BEiDNfT&4?8?s+02RN*bw9o}qCVu2 zhV$kx!N(IM^~N_3QG_aqcOy0&QWn(PW74#@ZEWA<3Ywe2-Npu!(9`R^y}blO@3Z|A z;V8~n2zHa>!_Uc1es5l<>v8hib6alW|J-!!Ke*%kYgik}y4lvwk{U-wMrN$JhHFwv zG#HEQ!_^`xNS?R_1>eiFi})CSM?b$~4r;47^x@GDvVp8v6{r{wa?fM(GYIGh5bhkd z{F9Ud$K=JlhWYvpQEt8YDL20UztrZ}ToO=~~(7rM(c``8rkLvkmJ?pnn2ayrI4 zpNz6z30hcMf)D=pZ!-`YM1{Tkc)T(_E)>Ot_55dGuvFE?-k8YY)BPa1|JicqBkU3h zvhnXDZlEB+SO=18uq^`3m~~`Ht9T!C93-WL`vXUujN|Wwajp19q!}(;)$`(Z4KMdb zP+Ng4Fi;klu@CBo*z848Ut!r2nmX$>Hjq4M934>I3N_k8_v6LBM3j8@(! z0`E}ZW!#><^2TPQ*nYLGn^*Irg~h+foo2mgSjSiUE}voP?2%^qCEZMQFUJ!-!AcfV z)}j&=gP+ z;zd@w#C@sBV1 z>e`i}NR469HtCs`a6ebkXp4I08=7n0(oPQ^)^(Fs84d%B-`X}`Sg0dgDyBc$T3HpV z6obByG}vQ~w}f2V6g682|F_{BZdM|4X6Zc>UFBM)UpQH zBEhUtVf+$XvF;`jWsw3y~APcw*6y6m5?-#Yw8u%*Fr-u0 z+Y9{5H!hdmY5uKO`h#Ho01el-{-mr(>DKRW1T4bB!irV(1|&I}*1UMfnLg{fH0K8H zutVB8n%5ST&oIaY2e1c$k^ zH%iQ18C#dXm(gl_Sn6~0@BpLm#<4uR(*(zLjkz1K6rYV#M6UetsD!P8PYk?@hH*`)6LXR{+$*$GTaM z`-7@_*Bv?#4B91%Rg{e9Ox!81vI$;k-Kj$(K^1iz^J{FFQ7!ou8uiWEE{m|H7L)J8 zvfDYv zZrZeZ0jy~a!vn{okAD)$J5FNO!zi$xx&DBd1U^gg{3{fbc{7mOA(G)Z{L-m&brHa% zD=UcERBLt*GC@X1gKrfVZ8hjUc@pQ-TvzhQdEC`2VJtl>8i$LDhteynl^fJuPHiKT&?YlK>i^T1_8zPb26dxL5>GMmJM#Owqax&R65w(tE|$U0%Z51jD?kTxdCjC zQq4Iij>C9M>SW7SPKtRVDSzzdy-{;boSdp9o0d^UEU}+Fz}LiN6PVpgfB%-pR~=*7mr@123A9 zvzfEA+1c+mw0GA^KkY)k+>s)nE#Ek_{5W5)dn)+Ya`IQ8q*kunF*d|IecK7<^<5Y$ zP)sFtX8_9CH*v_Cfs=YNw#{J++-`XWJ9xK;jPn6ER{@0217A~M^uQs=!1JeT?@E*3 zz4U>4!e;@qym9}_+J6DJf65*F(0+iL;`~NXvSCZdUiz*)!}wmJn9zE)!SCYLy*S%GKIYydJ+f@TF?c3c`gVzYaN{$I^Md?gHUooJHgJLEtB zH`Uc4Qe=%QzJI@hI32K|@jC7<12AKr`OKkg*MJIBuHYjlzp$`E0Wb&NLc5o7>2E{T zD<-b#ep`-CH5=4buO};JIVfGAB1@AnG;XHr!U?IJw-C9PCdVLv%MW@<0S6k{31hGx04&l1NTWP@fk0FLx0)CsJ{%WS9v4UCJ+bWItcYGe-Ln?){@MJ*4WiMGBY zoSr_Wp0?v@f~A$TcGNDT6%ANFB+RmcS-DH$)1~VKGJohRnP=&-2*e6qT}ZxH5$oz5 z@xHC*+5k3S*j+ngZt^_cR8=a zs`5YnI(2#SJhr|lQFVgXfWYzEj7@rw`dvC?aiIrK-_f1oOdGKgpoXZ`#+Yl@4%eE7 zzlC6eckkcq4Da7DmLYJJgXuTmCzD<&zD%Ap(v6#=+N7=!zN$`gQ$B+0IpvKywdzUd z)2!FQ8Gtfc&M(&gRV&I9fhZR8^Ln9{T;QD8@^+!vu^(`9EW434L;gf>fw>d^D~UYN zqm2Zme7vb|^{x}pApgt{e1!w=Ad9KZd2g=s&DX=FS4=foyL(C6zDFQfjx}3? z^7MxQ8Luv{&1{)e8C<_PzAD0j=O2B{%ND~849Olp(QMKvYs(j5-NkCwc6L!qI&?Ti zC(LfKGpZh>ROJfaCay}ZVQBe|^umQ=sZhTEuqSU2a{!DxwUbH~cj5jpfZ2tcexu#@ zAU>dA2;S(4x30G)2!&c%#~%RqLn>?l?|l=m@M2+MAqws4&gNK3WYjDg%etG|O>YW@ z{DRP6!3BD`Jlb>{-WhENAH+5plF70pgS0#A>aN{XP*mw_>Q~1RF5I(UTjK*EZFm%$8sn3D8sKqFCE%$!`YD+K&3)v1idHC%!z$L%+3RwbSB8U#Jz8CKw zXBI^8st>J+Fq0PUy|vw4E*w|Lx}BqsggnFgS&W(G{W~Kjo3IK26#YS>Mj-3Ed=cbR zf6!ymlR{pwB?__SlL~ZBt56sB7**xiM0VY@&SV{#1#RPK2+s zpUpssV}$?g4H^pWyLA49P;3gPd`yF4pYFS+EuuhUT$)!J$ZFH4XEKdWjh5k4++Fo| zmqg{sJLmkI$sEL5LdKKK>(O<VJH&T&dPNKEgN33)uNUgh~XrzcD8#6d=Icg|8Vk`Z&coi7C} zvu++a2#_cp#WCQ2wfBZ3qKRDZV{R${X)ch=fX!skZ05w~vU4=^=8nTMQzV#zMl~T} zaWJ7@!F@TIEmkJu?pJMnJ+Q3jy)t!kOWaS3exzbPsv@5i`74|^&8o`DycS!+l{JS< z%BtY2IqW+FfCL~UcJTMl#OH1_W+!|aby^U`AwY_Eq{M(1eQ_0f@{nM7|7y8`IO^YE zmm$%bu$BLTXL@=)y${O>fjo$**OO)8Q%>xz1J{B(0HZfATeFiimOn*Mk( z9n*IB@``cQaIpE%<;w3!m}(=Rq$h~L{$AvnEv4mdhtm-;KF=a4VeXpu10FOnRH%?%%L%X}0hfV%g!i*_oB{xWnm6fev1;C;$5#OY%({{&W~c zdf?UqhNqmkC;{?Mr}RFhH=Y!pC&2r}?`k#oNL+yO*a4636zmxQ)%hxw2h3kZ9#3Od zX&T8^5W_7|g^8l3D%(_wCpav20!A_wnYkb@WP&dj2IHxKe+h&KLXf5#2Y5f(`NH25JO1^ zK@>UW;lUKvx+8beE03Rbd#g$SZ!+5WIgpOUR?qY3drW=$21s`+R{LrrDG~TgsWXn( znWXULN6sUuHw%v9&dbcMlFr8tEPJmLWT^1)@ppH2{yAWP-|x7y7uk0%5cWx~FwQV3 zR@2efXU0K2ax?kS0rUAVt>%|w{|_(0^cDrPjqPpo#;2|aht_~gMa#~s&8x9aS2Awl z(;&sVm{C;=1BgH8UEaz4wZIp6yk8Ih1KRiw;@C0>N8aiKjlUvM4G?wX2Xe5 zAW8)OhwB-~x$oz>@9t~xJxaAV9d6VYJlAig%g5l20f!HOUvR=+0+KG*=mHj5 z6bI@65p$aFIR}`eKICW@^|PrL00uK_eTL1Gif7k0l>-v2g-`OM0b!HZg39|w`p6TE z_K?>#n!DvvcpSdAn0M+ZQsp{CZJ_d&(YqmJ`pyw-pN15nMu{rc|KT?)*dU1ItC97m zOPI3?EF=)`UkL+7t0~*A__4iX`Qg7@+@l&*fjt!?5bwrWv~?8k-p4mF>d{KJMP%HLC_`A#*Uwltl^1hLp6?|8k}~;KteZH4C&d&Z5HOjxp20Xs{Q2QT5Cwo z$JuOwv?d|_q@!X+hB<);H4Rt4=SX-tBPL<+=p336W}( zLn`Xtz?NfEQ?%CtE2d1n6My=c!1&h2XR{wTpI{X z+(8S9n1NWyA|^maZOM;;%w6*V1YGFh&uN32+T0E`EK(I<_P|(hxe?&Bp#D6b)hfQJ zv;eO{k|gtUDg!;wT6&XLzo+0^vGSZ zziK>rMd0{JQ?%SUI4wa^3+J1rtPfH}U#|*cwB&)c6@Xw~z_`2w3G1I{4B|=wve*}j zCYh+p_SOH@6Jkn2$66+(6-M!w9@V=bWy<}qlP8Q>`z#fB3?b<`y(jpTJ^pyZnM=>V zXR?Mo&QYIX>YP4Xt+wM{Pp;uKMdp|fi823FXFky)(xd~BsQi zvxyh@c*=;L`dHO+FWTeP;&2=Utd7ejG6xYrx$3CXIGbGytg*g;_5#oieICzh4CiAc z$2cpDUG2feoK~ZVDRP8s^+<%wTOjDMLBukmS@JQ;*QLBJ6$Vg8B9Ka-Dx@#oW;eaZ z9iry_JySE=dqD5^bjO*NHeG-=yI0jN-6aFriY11NoTD?#^gULp4qGM|oo z(|g!-1tkxo5#00o6W*9JvwibT=@JY-oh(l4E~kPuy}24eS&{J^Xg2bwQsjAt-1MZ~ z0N)+8JCc7@atTVC)97Dk=bv%dYHpI?Ipy$kukbe?Pej>v&e)xtLMcksO^}s~L5aR; z2C=3Ug3WJA1rm>Xx)2spE&FWwP;D{jB{C?lFl2*j;xxRHs?vf5O7fZwU${oeG(~tn zMF1dyA!^o3n=-dU`(re-4h<4eV*BE!8&jv7-2--Soaatcg3GqLKgQ_c`eeA*$1$GR zdHjZf$VQ0TpE2loj$^*SXpN0mx2Ea*SpOv0FSJ6=g-5IPz9rxPj7qnU_pDL0RFtBR zwCSvMYT9)Xpf+6?d5Iv%T@^~27EGK-WNN`WARyYXwBxDZ9cfxJTH4(IZ~a%%Slxl1 X`1r7woQ#T9 diff --git a/doc/guide/figures/bloch3d-blank.png b/doc/guide/figures/bloch3d-blank.png deleted file mode 100644 index 7888a7a89671c1f557efeb328d7c09b8a2389f19..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 46765 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 diff --git a/doc/guide/guide-basics.rst b/doc/guide/guide-basics.rst index fa86e506ab..19b035903b 100644 --- a/doc/guide/guide-basics.rst +++ b/doc/guide/guide-basics.rst @@ -1,8 +1,8 @@ .. _basics: -************************************ +*********************************** Basic Operations on Quantum Objects -************************************ +*********************************** .. _basics-first: @@ -323,13 +323,43 @@ For the destruction operator above: False >>> q.data + Dia(shape=(4, 4), num_diag=1) + + +The ``data`` attribute returns a Qutip diagonal matrix. +``Qobj`` instances store their data in Qutip matrix format. +In the core qutip module, the ``Dense``, ``CSR`` and ``Dia`` formats are available, but other module can add other formats. +For example, the qutip-jax module add ``Jax`` and ``JaxDia`` formats. +One can always access the underlying matrix as a numpy array using :meth:`.Qobj.full`. +It is also possible to access the underlying data as is in a common format using :meth:`.Qobj.data_as`. + +.. doctest:: [basics] + :options: +NORMALIZE_WHITESPACE + + >>> q.data_as("dia_matrix") + <4x4 sparse matrix of type '' + with 3 stored elements (1 diagonals) in DIAgonal format> + + +Conversion between storage type is done using the :meth:`.Qobj.to` method. + +.. doctest:: [basics] + :options: +NORMALIZE_WHITESPACE + + >>> q.to("CSR").data + CSR(shape=(4, 4), nnz=3) + + >>> q.to("CSR").data_as("CSR_matrix") <4x4 sparse matrix of type '' - with 3 stored elements in Compressed Sparse Row format> + with 3 stored elements in Compressed Sparse Row format> +Note that :meth:`.Qobj.data_as` does not do the conversion. -The data attribute returns a message stating that the data is a sparse matrix. All ``Qobj`` instances store their data as a sparse matrix to save memory. -To access the underlying dense matrix one needs to use the :meth:`.Qobj.full` function as described below. +QuTiP will do conversion when needed to keep everything working in any format. +However these conversions could slow down the computations and it is recomented to keep to one family of format. +For example, core qutip ``Dense`` and ``CSR`` work well together and binary operation between these format is efficient. +However binary operations between ``Dense`` and ``Jax`` should be avoided since it is not clear whether the operation will be executed by Jax, (possibly on GPU) or numpy. .. _basics-qobj-math: @@ -400,7 +430,7 @@ In addition, the logic operators "is equal" `==` and "is not equal" `!=` are als .. _basics-functions: Functions operating on Qobj class -================================== +================================= Like attributes, the quantum object class has defined functions (methods) that operate on ``Qobj`` class instances. For a general quantum object ``Q``: diff --git a/doc/guide/guide-bloch.rst b/doc/guide/guide-bloch.rst index 976264320e..696b18c4e9 100644 --- a/doc/guide/guide-bloch.rst +++ b/doc/guide/guide-bloch.rst @@ -9,39 +9,28 @@ Plotting on the Bloch Sphere Introduction ============ -When studying the dynamics of a two-level system, it is often convenient to visualize the state of the system by plotting the state-vector or density matrix on the Bloch sphere. In QuTiP, we have created two different classes to allow for easy creation and manipulation of data sets, both vectors and data points, on the Bloch sphere. The :class:`qutip.bloch.Bloch` class, uses Matplotlib to render the Bloch sphere, where as :class:`qutip.bloch3d.Bloch3d` uses the Mayavi rendering engine to generate a more faithful 3D reconstruction of the Bloch sphere. +When studying the dynamics of a two-level system, it is often convenient to visualize the state of the system by plotting the state-vector or density matrix on the Bloch sphere. In QuTiP, we have created two different classes to allow for easy creation and manipulation of data sets, both vectors and data points, on the Bloch sphere. .. _bloch-class: -The Bloch and Bloch3d Classes -============================= +The Bloch Class +=============== In QuTiP, creating a Bloch sphere is accomplished by calling either: .. plot:: - :context: + :context: reset b = qutip.Bloch() -which will load an instance of the :class:`qutip.bloch.Bloch` class, or using :: - - >>> b3d = qutip.Bloch3d() - -that loads the :class:`qutip.bloch3d.Bloch3d` version. Before getting into the details of these objects, we can simply plot the blank Bloch sphere associated with these instances via: +which will load an instance of the :class:`~qutip.bloch.Bloch` class. +Before getting into the details of these objects, we can simply plot the blank Bloch sphere associated with these instances via: .. plot:: :context: b.make_sphere() -or - -.. _image-blank3d: - -.. figure:: figures/bloch3d-blank.png - :width: 3.5in - :figclass: align-center - In addition to the ``show`` command, see the API documentation for :class:`~qutip.bloch.Bloch` for a full list of other available functions. As an example, we can add a single data point: @@ -87,14 +76,7 @@ In total, the code for constructing our Bloch sphere with one vector, one state, b.add_states(up) b.render() -where we have removed the extra ``show()`` commands. Replacing ``b=Bloch()`` with ``b=Bloch3d()`` in the above code generates the following 3D Bloch sphere. - -.. _image-bloch3ddata: - -.. figure:: figures/bloch3d+data.png - :width: 3.5in - :figclass: align-center - +where we have removed the extra ``show()`` commands. We can also plot multiple points, vectors, and states at the same time by passing list or arrays instead of individual elements. Before giving an example, we can use the `clear()` command to remove the current data from our Bloch sphere instead of creating a new instance: @@ -183,26 +165,9 @@ Now, the data points cycle through a variety of predefined colors. Now lets add b.add_points([xz, yz, zz]) # no 'm' b.render() -Again, the same plot can be generated using the :class:`qutip.bloch3d.Bloch3d` class by replacing ``Bloch`` with ``Bloch3d``: - -.. figure:: figures/bloch3d+points.png - :width: 3.5in - :figclass: align-center A more slick way of using this 'multi' color feature is also given in the example, where we set the color of the markers as a function of time. -Differences Between Bloch and Bloch3d -------------------------------------- -While in general the ``Bloch`` and ``Bloch3d`` classes are interchangeable, there are some important differences to consider when choosing between them. - -- The ``Bloch`` class uses Matplotlib to generate figures. As such, the data plotted on the sphere is in reality just a 2D object. In contrast the ``Bloch3d`` class uses the 3D rendering engine from VTK via mayavi to generate the sphere and the included data. In this sense the ``Bloch3d`` class is much more advanced, as objects are rendered in 3D leading to a higher quality figure. - -- Only the ``Bloch`` class can be embedded in a Matplotlib figure window. Thus if you want to combine a Bloch sphere with another figure generated in QuTiP, you can not use ``Bloch3d``. Of course you can always post-process your figures using other software to get the desired result. - -- Due to limitations in the rendering engine, the ``Bloch3d`` class does not support LaTeX for text. Again, you can get around this by post-processing. - -- The user customizable attributes for the ``Bloch`` and ``Bloch3d`` classes are not identical. Therefore, if you change the properties of one of the classes, these changes will cause an exception if the class is switched. - .. _bloch-config: @@ -270,67 +235,6 @@ At the end of the last section we saw that the colors and marker shapes of the d | b.zlpos | Position of z-axis labels | ``[1.2, -1.2]`` | +---------------+---------------------------------------------------------+-------------------------------------------------+ -Bloch3d Class Options ---------------------- - -The Bloch3d sphere is also customizable. Note however that the attributes for the ``Bloch3d`` class are not in one-to-one -correspondence to those of the ``Bloch`` class due to the different underlying rendering engines. Assuming ``b=Bloch3d()``: - -.. tabularcolumns:: | p{3cm} | p{7cm} | p{7cm} | - -.. cssclass:: table-striped - -+---------------+---------------------------------------------------------+---------------------------------------------+ -| Attribute | Function | Default Setting | -+===============+=========================================================+=============================================+ -| b.fig | User supplied Mayavi Figure instance. Set by ``fig`` | ``None`` | -| | keyword arg. | | -+---------------+---------------------------------------------------------+---------------------------------------------+ -| b.font_color | Color of fonts | ``'black'`` | -+---------------+---------------------------------------------------------+---------------------------------------------+ -| b.font_scale | Scale of fonts | 0.08 | -+---------------+---------------------------------------------------------+---------------------------------------------+ -| b.frame | Draw wireframe for sphere? | ``True`` | -+---------------+---------------------------------------------------------+---------------------------------------------+ -| b.frame_alpha | Transparency of wireframe | 0.05 | -+---------------+---------------------------------------------------------+---------------------------------------------+ -| b.frame_color | Color of wireframe | ``'gray'`` | -+---------------+---------------------------------------------------------+---------------------------------------------+ -| b.frame_num | Number of wireframe elements to draw | 8 | -+---------------+---------------------------------------------------------+---------------------------------------------+ -| b.frame_radius| Radius of wireframe lines | 0.005 | -+---------------+---------------------------------------------------------+---------------------------------------------+ -| b.point_color | List of colors for Bloch point markers to cycle through | ``['r', 'g', 'b', 'y']`` | -+---------------+---------------------------------------------------------+---------------------------------------------+ -| b.point_mode | Type of point markers to draw | ``'sphere'`` | -+---------------+---------------------------------------------------------+---------------------------------------------+ -| b.point_size | Size of points | 0.075 | -+---------------+---------------------------------------------------------+---------------------------------------------+ -| b.sphere_alpha| Transparency of Bloch sphere | 0.1 | -+---------------+---------------------------------------------------------+---------------------------------------------+ -| b.sphere_color| Color of Bloch sphere | ``'#808080'`` | -+---------------+---------------------------------------------------------+---------------------------------------------+ -| b.size | Sets size of figure window | ``[500, 500]`` (500x500 pixels) | -+---------------+---------------------------------------------------------+---------------------------------------------+ -| b.vector_color| List of colors for Bloch vectors to cycle through | ``['r', 'g', 'b', 'y']`` | -+---------------+---------------------------------------------------------+---------------------------------------------+ -| b.vector_width| Width of Bloch vectors | 3 | -+---------------+---------------------------------------------------------+---------------------------------------------+ -| b.view | Azimuthal and Elevation viewing angles | ``[45, 65]`` | -+---------------+---------------------------------------------------------+---------------------------------------------+ -| b.xlabel | Labels for x-axis | ``['|x>', '']`` +x and -x | -+---------------+---------------------------------------------------------+---------------------------------------------+ -| b.xlpos | Position of x-axis labels | ``[1.07, -1.07]`` | -+---------------+---------------------------------------------------------+---------------------------------------------+ -| b.ylabel | Labels for y-axis | ``['$y$', '']`` +y and -y | -+---------------+---------------------------------------------------------+---------------------------------------------+ -| b.ylpos | Position of y-axis labels | ``[1.07, -1.07]`` | -+---------------+---------------------------------------------------------+---------------------------------------------+ -| b.zlabel | Labels for z-axis | ``['|0>', '|1>']`` +z and -z | -+---------------+---------------------------------------------------------+---------------------------------------------+ -| b.zlpos | Position of z-axis labels | ``[1.07, -1.07]`` | -+---------------+---------------------------------------------------------+---------------------------------------------+ - These properties can also be accessed via the print command: .. doctest:: diff --git a/doc/guide/guide-control.rst b/doc/guide/guide-control.rst index f3c31ad7e9..b5baf9b0d5 100644 --- a/doc/guide/guide-control.rst +++ b/doc/guide/guide-control.rst @@ -191,65 +191,7 @@ algorithm. Optimal Quantum Control in QuTiP ================================ -There are two separate implementations of optimal control inside QuTiP. The -first is an implementation of first order GRAPE, and is not further described -here, but there are the example notebooks. The second is referred to as Qtrl -(when a distinction needs to be made) as this was its name before it was -integrated into QuTiP. Qtrl uses the Scipy optimize functions to perform the -multi-variable optimisation, typically the L-BFGS-B method for GRAPE and -Nelder-Mead for CRAB. The GRAPE implementation in Qtrl was initially based on -the open-source package DYNAMO, which is a MATLAB implementation, and is -described in [DYNAMO]_. It has since been restructured and extended for -flexibility and compatibility within QuTiP. - -The rest of this section describes the Qtrl implementation and how to use it. - -Object Model - The Qtrl code is organised in a hierarchical object model in order to try and maximise configurability whilst maintaining some clarity. It is not necessary to understand the model in order to use the pulse optimisation functions, but it is the most flexible method of using Qtrl. If you just want to use a simple single function call interface, then jump to :ref:`pulseoptim-functions` - -.. figure:: figures/qtrl-code_object_model.png - :align: center - :width: 3.5in - - Qtrl code object model. - -The object's properties and methods are described in detail in the documentation, so that will not be repeated here. - -OptimConfig - The OptimConfig object is used simply to hold configuration parameters used by all the objects. Typically this is the subclass types for the other objects and parameters for the users specific requirements. The ``loadparams`` module can be used read parameter values from a configuration file. - -Optimizer - This acts as a wrapper to the ``Scipy.optimize`` functions that perform the work of the pulse optimisation algorithms. Using the main classes the user can specify which of the optimisation methods are to be used. There are subclasses specifically for the BFGS and L-BFGS-B methods. There is another subclass for using the CRAB algorithm. - -Dynamics - This is mainly a container for the lists that hold the dynamics generators, propagators, and time evolution operators in each timeslot. The combining of dynamics generators is also complete by this object. Different subclasses support a range of types of quantum systems, including closed systems with unitary dynamics, systems with quadratic Hamiltonians that have Gaussian states and symplectic transforms, and a general subclass that can be used for open system dynamics with Lindbladian operators. - -PulseGen - There are many subclasses of pulse generators that generate different types of pulses as the initial amplitudes for the optimisation. Often the goal cannot be achieved from all starting conditions, and then typically some kind of random pulse is used and repeated optimisations are performed until the desired infidelity is reached or the minimum infidelity found is reported. - There is a specific subclass that is used by the CRAB algorithm to generate the pulses based on the basis coefficients that are being optimised. - -TerminationConditions - This is simply a convenient place to hold all the properties that will determine when the single optimisation run terminates. Limits can be set for number of iterations, time, and of course the target infidelity. - -Stats - Performance data are optionally collected during the optimisation. This object is shared to a single location to store, calculate and report run statistics. - -FidelityComputer - The subclass of the fidelity computer determines the type of fidelity measure. These are closely linked to the type of dynamics in use. These are also the most commonly user customised subclasses. - -PropagatorComputer - This object computes propagators from one timeslot to the next and also the propagator gradient. The options are using the spectral decomposition or Frechet derivative, as discussed above. - -TimeslotComputer - Here the time evolution is computed by calling the methods of the other computer objects. - -OptimResult - The result of a pulse optimisation run is returned as an object with properties for the outcome in terms of the infidelity, reason for termination, performance statistics, final evolution, and more. - -.. _pulseoptim-functions: - -Using the pulseoptim functions -============================== -The simplest method for optimising a control pulse is to call one of the functions in the ``pulseoptim`` module. This automates the creation and configuration of the necessary objects, generation of initial pulses, running the optimisation and returning the result. There are functions specifically for unitary dynamics, and also specifically for the CRAB algorithm (GRAPE is the default). The ``optimise_pulse`` function can in fact be used for unitary dynamics and / or the CRAB algorithm, the more specific functions simply have parameter names that are more familiar in that application. - -A semi-automated method is to use the ``create_optimizer_objects`` function to generate and configure all the objects, then manually set the initial pulse and call the optimisation. This would be more efficient when repeating runs with different starting conditions. +The Quantum Control part of qutip has been moved to it's own project. +Previously available implementation is now located in the `qutip-qtrl `_ module. +A newer interface with upgraded capacities is also being developped in `qutip-qoc `_. +Please give these module a look. diff --git a/doc/guide/guide-correlation.rst b/doc/guide/guide-correlation.rst index 2ca650263f..a7362e42cd 100644 --- a/doc/guide/guide-correlation.rst +++ b/doc/guide/guide-correlation.rst @@ -4,7 +4,7 @@ Two-time correlation functions ****************************** -With the QuTiP time-evolution functions (for example :func:`qutip.mesolve` and :func:`qutip.mcsolve`), a state vector or density matrix can be evolved from an initial state at :math:`t_0` to an arbitrary time :math:`t`, :math:`\rho(t)=V(t, t_0)\left\{\rho(t_0)\right\}`, where :math:`V(t, t_0)` is the propagator defined by the equation of motion. The resulting density matrix can then be used to evaluate the expectation values of arbitrary combinations of *same-time* operators. +With the QuTiP time-evolution functions (for example :func:`.mesolve` and :func:`.mcsolve`), a state vector or density matrix can be evolved from an initial state at :math:`t_0` to an arbitrary time :math:`t`, :math:`\rho(t)=V(t, t_0)\left\{\rho(t_0)\right\}`, where :math:`V(t, t_0)` is the propagator defined by the equation of motion. The resulting density matrix can then be used to evaluate the expectation values of arbitrary combinations of *same-time* operators. To calculate *two-time* correlation functions on the form :math:`\left`, we can use the quantum regression theorem (see, e.g., [Gar03]_) to write @@ -45,7 +45,7 @@ QuTiP provides a family of functions that assists in the process of calculating +----------------------------------+--------------------------------------------------+ -The most common use-case is to calculate the two time correlation function :math:`\left`. :func:`qutip.correlation_2op_1t` performs this task with sensible default values, but only allows using the :func:`mesolve` solver. From QuTiP 5.0 we added :func:`qutip.correlation_3op`. This function can also calculate correlation functions with two or three operators and with one or two times. Most importantly, this function accepts alternative solvers such as :func:`brmesolve`. +The most common use-case is to calculate the two time correlation function :math:`\left`. :func:`.correlation_2op_1t` performs this task with sensible default values, but only allows using the :func:`.mesolve` solver. From QuTiP 5.0 we added :func:`.correlation_3op`. This function can also calculate correlation functions with two or three operators and with one or two times. Most importantly, this function accepts alternative solvers such as :func:`.brmesolve`. .. _correlation-steady: @@ -55,7 +55,7 @@ Steadystate correlation function The following code demonstrates how to calculate the :math:`\left` correlation for a leaky cavity with three different relaxation rates. .. plot:: - :context: + :context: close-figs times = np.linspace(0,10.0,200) a = destroy(10) @@ -85,7 +85,7 @@ Given a correlation function :math:`\left` we can define the S(\omega) = \int_{-\infty}^{\infty} \left e^{-i\omega\tau} d\tau. -In QuTiP, we can calculate :math:`S(\omega)` using either :func:`qutip.correlation.spectrum_ss`, which first calculates the correlation function using one of the time-dependent solvers and then performs the Fourier transform semi-analytically, or we can use the function :func:`qutip.correlation.spectrum_correlation_fft` to numerically calculate the Fourier transform of a given correlation data using FFT. +In QuTiP, we can calculate :math:`S(\omega)` using either :func:`.spectrum`, which first calculates the correlation function using one of the time-dependent solvers and then performs the Fourier transform semi-analytically, or we can use the function :func:`.spectrum_correlation_fft` to numerically calculate the Fourier transform of a given correlation data using FFT. The following example demonstrates how these two functions can be used to obtain the emission power spectrum. @@ -99,13 +99,13 @@ The following example demonstrates how these two functions can be used to obtain Non-steadystate correlation function ==================================== -More generally, we can also calculate correlation functions of the kind :math:`\left`, i.e., the correlation function of a system that is not in its steady state. In QuTiP, we can evaluate such correlation functions using the function :func:`qutip.correlation.correlation_2op_2t`. The default behavior of this function is to return a matrix with the correlations as a function of the two time coordinates (:math:`t_1` and :math:`t_2`). +More generally, we can also calculate correlation functions of the kind :math:`\left`, i.e., the correlation function of a system that is not in its steady state. In QuTiP, we can evaluate such correlation functions using the function :func:`.correlation_2op_2t`. The default behavior of this function is to return a matrix with the correlations as a function of the two time coordinates (:math:`t_1` and :math:`t_2`). .. plot:: guide/scripts/correlation_ex2.py :width: 5.0in :include-source: -However, in some cases we might be interested in the correlation functions on the form :math:`\left`, but only as a function of time coordinate :math:`t_2`. In this case we can also use the :func:`qutip.correlation.correlation_2op_2t` function, if we pass the density matrix at time :math:`t_1` as second argument, and `None` as third argument. The :func:`qutip.correlation.correlation_2op_2t` function then returns a vector with the correlation values corresponding to the times in `taulist` (the fourth argument). +However, in some cases we might be interested in the correlation functions on the form :math:`\left`, but only as a function of time coordinate :math:`t_2`. In this case we can also use the :func:`.correlation_2op_2t` function, if we pass the density matrix at time :math:`t_1` as second argument, and `None` as third argument. The :func:`.correlation_2op_2t` function then returns a vector with the correlation values corresponding to the times in `taulist` (the fourth argument). Example: first-order optical coherence function ----------------------------------------------- @@ -116,7 +116,7 @@ This example demonstrates how to calculate a correlation function on the form :m :width: 5.0in :include-source: -For convenience, the steps for calculating the first-order coherence function have been collected in the function :func:`qutip.correlation.coherence_function_g1`. +For convenience, the steps for calculating the first-order coherence function have been collected in the function :func:`.coherence_function_g1`. Example: second-order optical coherence function ------------------------------------------------ @@ -129,7 +129,7 @@ The second-order optical coherence function, with time-delay :math:`\tau`, is de For a coherent state :math:`g^{(2)}(\tau) = 1`, for a thermal state :math:`g^{(2)}(\tau=0) = 2` and it decreases as a function of time (bunched photons, they tend to appear together), and for a Fock state with :math:`n` photons :math:`g^{(2)}(\tau = 0) = n(n - 1)/n^2 < 1` and it increases with time (anti-bunched photons, more likely to arrive separated in time). -To calculate this type of correlation function with QuTiP, we can use :func:`qutip.correlation.correlation_3op_1t`, which computes a correlation function on the form :math:`\left` (three operators, one delay-time vector). +To calculate this type of correlation function with QuTiP, we can use :func:`.correlation_3op_1t`, which computes a correlation function on the form :math:`\left` (three operators, one delay-time vector). We first have to combine the central two operators into one single one as they are evaluated at the same time, e.g. here we do :math:`a^\dagger(\tau)a(\tau) = (a^\dagger a)(\tau)`. The following code calculates and plots :math:`g^{(2)}(\tau)` as a function of :math:`\tau` for a coherent, thermal and Fock state. @@ -138,4 +138,4 @@ The following code calculates and plots :math:`g^{(2)}(\tau)` as a function of : :width: 5.0in :include-source: -For convenience, the steps for calculating the second-order coherence function have been collected in the function :func:`qutip.correlation.coherence_function_g2`. +For convenience, the steps for calculating the second-order coherence function have been collected in the function :func:`.coherence_function_g2`. diff --git a/doc/guide/guide-measurement.rst b/doc/guide/guide-measurement.rst index 2d74d1fab7..89149da74d 100644 --- a/doc/guide/guide-measurement.rst +++ b/doc/guide/guide-measurement.rst @@ -42,8 +42,8 @@ along the z-axis. We choose what to measure (in this case) by selecting a **measurement operator**. For example, -we could select :func:`~qutip.operators.sigmaz` which measures the z-component of the -spin of a spin-1/2 particle, or :func:`~qutip.operators.sigmax` which measures the +we could select :func:`.sigmaz` which measures the z-component of the +spin of a spin-1/2 particle, or :func:`.sigmax` which measures the x-component: .. testcode:: @@ -276,7 +276,7 @@ when called with a single observable: - `eigenstates` is an array of the eigenstates of the measurement operator, i.e. a list of the possible final states after the measurement is complete. - Each element of the array is a :obj:`~qutip.Qobj`. + Each element of the array is a :obj:`.Qobj`. - `probabilities` is a list of the probabilities of each measurement result. In our example the value is `[0.5, 0.5]` since the `up` state has equal @@ -343,7 +343,7 @@ the following result. The function :func:`~qutip.measurement.measurement_statistics` then returns two values: * `collapsed_states` is an array of the possible final states after the - measurement is complete. Each element of the array is a :obj:`~qutip.Qobj`. + measurement is complete. Each element of the array is a :obj:`.Qobj`. * `probabilities` is a list of the probabilities of each measurement outcome. diff --git a/doc/guide/guide-parfor.rst b/doc/guide/guide-parfor.rst deleted file mode 100644 index 4491cf30af..0000000000 --- a/doc/guide/guide-parfor.rst +++ /dev/null @@ -1,119 +0,0 @@ -.. _parfor: - -****************************************** -Parallel computation -****************************************** - -Parallel map and parallel for-loop ----------------------------------- - -Often one is interested in the output of a given function as a single-parameter is varied. -For instance, we can calculate the steady-state response of our system as the driving frequency is varied. -In cases such as this, where each iteration is independent of the others, we can speedup the calculation by performing the iterations in parallel. -In QuTiP, parallel computations may be performed using the :func:`qutip.solver.parallel.parallel_map` function. - -To use the this function we need to define a function of one or more variables, and the range over which one of these variables are to be evaluated. For example: - - -.. doctest:: - :skipif: not os_nt - :options: +NORMALIZE_WHITESPACE - - >>> result = parallel_map(func1, range(10)) - - >>> result_array = np.array(result) - - >>> print(result_array[:, 0]) # == a - [0 1 2 3 4 5 6 7 8 9] - - >>> print(result_array[:, 1]) # == b - [ 0 1 4 9 16 25 36 49 64 81] - - >>> print(result_array[:, 2]) # == c - [ 0 1 8 27 64 125 216 343 512 729] - - >>> print(result) - [(0, 0, 0), (1, 1, 1), (2, 4, 8), (3, 9, 27), (4, 16, 64)] - - -The :func:`qutip.solver.parallel.parallel_map` function is not limited to just numbers, but also works for a variety of outputs: - -.. doctest:: - :skipif: not os_nt - :options: +NORMALIZE_WHITESPACE - - >>> def func2(x): return x, Qobj(x), 'a' * x - - >>> results = parallel_map(func2, range(5)) - - >>> print([result[0] for result in results]) - [0 1 2 3 4] - - >>> print([result[1] for result in results]) - [Quantum object: dims = [[1], [1]], shape = (1, 1), type = bra - Qobj data = - [[0.]] - Quantum object: dims = [[1], [1]], shape = (1, 1), type = bra - Qobj data = - [[1.]] - Quantum object: dims = [[1], [1]], shape = (1, 1), type = bra - Qobj data = - [[2.]] - Quantum object: dims = [[1], [1]], shape = (1, 1), type = bra - Qobj data = - [[3.]] - Quantum object: dims = [[1], [1]], shape = (1, 1), type = bra - Qobj data = - [[4.]]] - - >>>print([result[2] for result in results]) - ['' 'a' 'aa' 'aaa' 'aaaa'] - - -One can also define functions with **multiple** input arguments and keyword arguments. - -.. doctest:: - :skipif: not os_nt - :options: +NORMALIZE_WHITESPACE - - >>> def sum_diff(x, y, z=0): return x + y, x - y, z - - >>> parallel_map(sum_diff, [1, 2, 3], task_args=(np.array([4, 5, 6]),), task_kwargs=dict(z=5.0)) - [(array([5, 6, 7]), array([-3, -4, -5]), 5.0), - (array([6, 7, 8]), array([-2, -3, -4]), 5.0), - (array([7, 8, 9]), array([-1, -2, -3]), 5.0)] - - -The :func:`qutip.solver.parallel.parallel_map` function supports progressbar by setting the keyword argument `progress_bar` to `True`. -The number of cpu used can also be controlled using the `map_kw` keyword, per default, all available cpus are used. - -.. doctest:: - :options: +SKIP - - >>> import time - - >>> def func(x): time.sleep(1) - - >>> result = parallel_map(func, range(50), progress_bar=True, map_kw={"num_cpus": 2}) - - 10.0%. Run time: 3.10s. Est. time left: 00:00:00:27 - 20.0%. Run time: 5.11s. Est. time left: 00:00:00:20 - 30.0%. Run time: 8.11s. Est. time left: 00:00:00:18 - 40.0%. Run time: 10.15s. Est. time left: 00:00:00:15 - 50.0%. Run time: 13.15s. Est. time left: 00:00:00:13 - 60.0%. Run time: 15.15s. Est. time left: 00:00:00:10 - 70.0%. Run time: 18.15s. Est. time left: 00:00:00:07 - 80.0%. Run time: 20.15s. Est. time left: 00:00:00:05 - 90.0%. Run time: 23.15s. Est. time left: 00:00:00:02 - 100.0%. Run time: 25.15s. Est. time left: 00:00:00:00 - Total run time: 28.91s - -There is a function called :func:`qutip.solver.parallel.serial_map` that works as a non-parallel drop-in replacement for :func:`qutip.solver.parallel.parallel_map`, which allows easy switching between serial and parallel computation. -Qutip also has the function :func:`qutip.solver.parallel.loky_map` as another drop-in replacement. It use the `loky` module instead of `multiprocessing` to run in parallel. -Parallel processing is useful for repeated tasks such as generating plots corresponding to the dynamical evolution of your system, or simultaneously simulating different parameter configurations. - - -IPython-based parallel_map --------------------------- - -When QuTiP is used with IPython interpreter, there is an alternative parallel for-loop implementation in the QuTiP module :func:`qutip.ipynbtools`, see :func:`qutip.ipynbtools.parallel_map`. The advantage of this parallel_map implementation is based on IPython's powerful framework for parallelization, so the compute processes are not confined to run on the same host as the main process. diff --git a/doc/guide/guide-piqs.rst b/doc/guide/guide-piqs.rst index f13cf8b3a4..3167918a85 100644 --- a/doc/guide/guide-piqs.rst +++ b/doc/guide/guide-piqs.rst @@ -32,7 +32,7 @@ where :math:`J_{\alpha,n}=\frac{1}{2}\sigma_{\alpha,n}` are SU(2) Pauli spin ope The inclusion of local processes in the dynamics lead to using a Liouvillian space of dimension :math:`4^N`. By exploiting the permutational invariance of identical particles [2-8], the Liouvillian :math:`\mathcal{D}_\text{TLS}(\rho)` can be built as a block-diagonal matrix in the basis of Dicke states :math:`|j, m \rangle`. The system under study is defined by creating an object of the -:code:`Dicke` class, e.g. simply named +:class:`~qutip.piqs.piqs.Dicke` class, e.g. simply named :code:`system`, whose first attribute is - :code:`system.N`, the number of TLSs of the system :math:`N`. @@ -48,8 +48,10 @@ The rates for collective and local processes are simply defined as Then the :code:`system.lindbladian()` creates the total TLS Lindbladian superoperator matrix. Similarly, :code:`system.hamiltonian` defines the TLS hamiltonian of the system :math:`H_\text{TLS}`. -The system's Liouvillian can be built using :code:`system.liouvillian()`. The properties of a Piqs object can be visualized by simply calling -:code:`system`. We give two basic examples on the use of *PIQS*. In the first example the incoherent emission of N driven TLSs is considered. +The system's Liouvillian can be built using :code:`system.liouvillian()`. +The properties of a Piqs object can be visualized by simply calling :code:`system`. +We give two basic examples on the use of *PIQS*. +In the first example the incoherent emission of N driven TLSs is considered. .. code-block:: python diff --git a/doc/guide/guide-random.rst b/doc/guide/guide-random.rst index 82b5920ea8..86d0abc1cd 100644 --- a/doc/guide/guide-random.rst +++ b/doc/guide/guide-random.rst @@ -11,7 +11,7 @@ Generating Random Quantum States & Operators QuTiP includes a collection of random state, unitary and channel generators for simulations, Monte Carlo evaluation, theorem evaluation, and code testing. Each of these objects can be sampled from one of several different distributions. -For example, a random Hermitian operator can be sampled by calling `rand_herm` function: +For example, a random Hermitian operator can be sampled by calling :func:`.rand_herm` function: .. doctest:: [random] :hide: @@ -40,23 +40,23 @@ For example, a random Hermitian operator can be sampled by calling `rand_herm` f .. cssclass:: table-striped -+-------------------------------+--------------------------------------------+------------------------------------------+ -| Random Variable Type | Sampling Functions | Dimensions | -+===============================+============================================+==========================================+ -| State vector (``ket``) | `rand_ket`, | :math:`N \times 1` | -+-------------------------------+--------------------------------------------+------------------------------------------+ -| Hermitian operator (``oper``) | `rand_herm` | :math:`N \times N` | -+-------------------------------+--------------------------------------------+------------------------------------------+ -| Density operator (``oper``) | `rand_dm`, | :math:`N \times N` | -+-------------------------------+--------------------------------------------+------------------------------------------+ -| Unitary operator (``oper``) | `rand_unitary`, | :math:`N \times N` | -+-------------------------------+--------------------------------------------+------------------------------------------+ -| stochastic matrix (``oper``) | `rand_stochastic`, | :math:`N \times N` | -+-------------------------------+--------------------------------------------+------------------------------------------+ -| CPTP channel (``super``) | `rand_super`, `rand_super_bcsz` | :math:`(N \times N) \times (N \times N)` | -+-------------------------------+--------------------------------------------+------------------------------------------+ -| CPTP map (list of ``oper``) | `rand_kraus_map` | :math:`N \times N` (N**2 operators) | -+-------------------------------+--------------------------------------------+------------------------------------------+ ++-------------------------------+-----------------------------------------------+------------------------------------------+ +| Random Variable Type | Sampling Functions | Dimensions | ++===============================+===============================================+==========================================+ +| State vector (``ket``) | :func:`.rand_ket` | :math:`N \times 1` | ++-------------------------------+-----------------------------------------------+------------------------------------------+ +| Hermitian operator (``oper``) | :func:`.rand_herm` | :math:`N \times N` | ++-------------------------------+-----------------------------------------------+------------------------------------------+ +| Density operator (``oper``) | :func:`.rand_dm` | :math:`N \times N` | ++-------------------------------+-----------------------------------------------+------------------------------------------+ +| Unitary operator (``oper``) | :func:`.rand_unitary` | :math:`N \times N` | ++-------------------------------+-----------------------------------------------+------------------------------------------+ +| stochastic matrix (``oper``) | :func:`.rand_stochastic` | :math:`N \times N` | ++-------------------------------+-----------------------------------------------+------------------------------------------+ +| CPTP channel (``super``) | :func:`.rand_super`, :func:`.rand_super_bcsz` | :math:`(N \times N) \times (N \times N)` | ++-------------------------------+-----------------------------------------------+------------------------------------------+ +| CPTP map (list of ``oper``) | :func:`.rand_kraus_map` | :math:`N \times N` (N**2 operators) | ++-------------------------------+-----------------------------------------------+------------------------------------------+ In all cases, these functions can be called with a single parameter :math:`dimensions` that can be the size of the relevant Hilbert space or the dimensions of a random state, unitary or channel. @@ -69,9 +69,9 @@ In all cases, these functions can be called with a single parameter :math:`dimen >>> rand_super_bcsz([[2, 3], [2, 3]]).dims [[[2, 3], [2, 3]], [[2, 3], [2, 3]]] -Several of the random `Qobj` function in QuTiP support additional parameters as well, namely *density* and *distribution*. -`rand_dm`, `rand_herm`, `rand_unitary` and `rand_ket` can be created using multiple method controlled by *distribution*. -The `rand_ket`, `rand_herm` and `rand_unitary` functions can return quantum objects such that a fraction of the elements are identically equal to zero. +Several of the random :class:`.Qobj` function in QuTiP support additional parameters as well, namely *density* and *distribution*. +:func:`.rand_dm`, :func:`.rand_herm`, :func:`.rand_unitary` and :func:`.rand_ket` can be created using multiple method controlled by *distribution*. +The :func:`.rand_ket`, :func:`.rand_herm` and :func:`.rand_unitary` functions can return quantum objects such that a fraction of the elements are identically equal to zero. The ratio of nonzero elements is passed as the ``density`` keyword argument. By contrast, `rand_super_bcsz` take as an argument the rank of the generated object, such that passing ``rank=1`` returns a random pure state or unitary channel, respectively. Passing ``rank=None`` specifies that the generated object should be full-rank for the given dimension. @@ -115,7 +115,7 @@ See the API documentation: :ref:`functions-rand` for details. Random objects with a given eigen spectrum ========================================== -It is also possible to generate random Hamiltonian (``rand_herm``) and densitiy matrices (``rand_dm``) with a given eigen spectrum. +It is also possible to generate random Hamiltonian (:func:`.rand_herm`) and densitiy matrices (:func:`.rand_dm`) with a given eigen spectrum. This is done by passing an array to eigenvalues argument to either function and choosing the "eigen" distribution. For example, @@ -153,9 +153,9 @@ This technique requires many steps to build the desired quantum object, and is t Composite random objects ======================== -In many cases, one is interested in generating random quantum objects that correspond to composite systems generated using the :func:`qutip.tensor.tensor` function. +In many cases, one is interested in generating random quantum objects that correspond to composite systems generated using the :func:`.tensor` function. Specifying the tensor structure of a quantum object is done passing a list for the first argument. -The resulting quantum objects size will be the product of the elements in the list and the resulting :class:`qutip.Qobj` dimensions will be ``[dims, dims]``: +The resulting quantum objects size will be the product of the elements in the list and the resulting :class:`.Qobj` dimensions will be ``[dims, dims]``: .. doctest:: [random] :hide: diff --git a/doc/guide/guide-saving.rst b/doc/guide/guide-saving.rst index ca78c98558..d419dfd24d 100644 --- a/doc/guide/guide-saving.rst +++ b/doc/guide/guide-saving.rst @@ -10,7 +10,7 @@ With time-consuming calculations it is often necessary to store the results to f Storing and loading QuTiP objects ================================= -To store and load arbitrary QuTiP related objects (:class:`qutip.Qobj`, :class:`qutip.solve.solver.Result`, etc.) there are two functions: :func:`qutip.fileio.qsave` and :func:`qutip.fileio.qload`. The function :func:`qutip.fileio.qsave` takes an arbitrary object as first parameter and an optional filename as second parameter (default filename is `qutip_data.qu`). The filename extension is always `.qu`. The function :func:`qutip.fileio.qload` takes a mandatory filename as first argument and loads and returns the objects in the file. +To store and load arbitrary QuTiP related objects (:class:`.Qobj`, :class:`.Result`, etc.) there are two functions: :func:`qutip.fileio.qsave` and :func:`qutip.fileio.qload`. The function :func:`qutip.fileio.qsave` takes an arbitrary object as first parameter and an optional filename as second parameter (default filename is `qutip_data.qu`). The filename extension is always `.qu`. The function :func:`qutip.fileio.qload` takes a mandatory filename as first argument and loads and returns the objects in the file. To illustrate how these functions can be used, consider a simple calculation of the steadystate of the harmonic oscillator :: @@ -18,7 +18,7 @@ To illustrate how these functions can be used, consider a simple calculation of >>> c_ops = [np.sqrt(0.5) * a, np.sqrt(0.25) * a.dag()] >>> rho_ss = steadystate(H, c_ops) -The steadystate density matrix `rho_ss` is an instance of :class:`qutip.Qobj`. It can be stored to a file `steadystate.qu` using :: +The steadystate density matrix `rho_ss` is an instance of :class:`.Qobj`. It can be stored to a file `steadystate.qu` using :: >>> qsave(rho_ss, 'steadystate') >>> !ls *.qu @@ -32,7 +32,8 @@ and it can later be loaded again, and used in further calculations :: >>> a = destroy(10) >>> np.testing.assert_almost_equal(expect(a.dag() * a, rho_ss_loaded), 0.9902248289345061) -The nice thing about the :func:`qutip.fileio.qsave` and :func:`qutip.fileio.qload` functions is that almost any object can be stored and load again later on. We can for example store a list of density matrices as returned by :func:`qutip.mesolve` :: +The nice thing about the :func:`qutip.fileio.qsave` and :func:`qutip.fileio.qload` functions is that almost any object can be stored and load again later on. +We can for example store a list of density matrices as returned by :func:`.mesolve` :: >>> a = destroy(10); H = a.dag() * a ; c_ops = [np.sqrt(0.5) * a, np.sqrt(0.25) * a.dag()] >>> psi0 = rand_ket(10) @@ -65,7 +66,7 @@ The :func:`qutip.fileio.file_data_store` takes two mandatory and three optional where `filename` is the name of the file, `data` is the data to be written to the file (must be a *numpy* array), `numtype` (optional) is a flag indicating numerical type that can take values `complex` or `real`, `numformat` (optional) specifies the numerical format that can take the values `exp` for the format `1.0e1` and `decimal` for the format `10.0`, and `sep` (optional) is an arbitrary single-character field separator (usually a tab, space, comma, semicolon, etc.). -A common use for the :func:`qutip.fileio.file_data_store` function is to store the expectation values of a set of operators for a sequence of times, e.g., as returned by the :func:`qutip.mesolve` function, which is what the following example does +A common use for the :func:`qutip.fileio.file_data_store` function is to store the expectation values of a set of operators for a sequence of times, e.g., as returned by the :func:`.mesolve` function, which is what the following example does .. plot:: :context: diff --git a/doc/guide/guide-steady.rst b/doc/guide/guide-steady.rst index 08e7846e84..41fb1af809 100644 --- a/doc/guide/guide-steady.rst +++ b/doc/guide/guide-steady.rst @@ -19,7 +19,7 @@ Although the requirement for time-independence seems quite resitrictive, one can Steady State solvers in QuTiP ============================= -In QuTiP, the steady-state solution for a system Hamiltonian or Liouvillian is given by :func:`qutip.steadystate.steadystate`. This function implements a number of different methods for finding the steady state, each with their own pros and cons, where the method used can be chosen using the ``method`` keyword argument. +In QuTiP, the steady-state solution for a system Hamiltonian or Liouvillian is given by :func:`.steadystate`. This function implements a number of different methods for finding the steady state, each with their own pros and cons, where the method used can be chosen using the ``method`` keyword argument. .. cssclass:: table-striped @@ -44,7 +44,7 @@ In QuTiP, the steady-state solution for a system Hamiltonian or Liouvillian is g - Steady-state solution via the **dense** SVD of the Liouvillian. -The function :func:`qutip.steadystate` can take either a Hamiltonian and a list +The function :func:`.steadystate` can take either a Hamiltonian and a list of collapse operators as input, generating internally the corresponding Liouvillian super operator in Lindblad form, or alternatively, a Liouvillian passed by the user. @@ -89,7 +89,7 @@ Kernel library that comes with the Anacoda (2.5+) and Intel Python distributions. This gives a substantial increase in performance compared with the standard SuperLU method used by SciPy. To verify that QuTiP can find the necessary libraries, one can check for ``INTEL MKL Ext: True`` in the QuTiP -about box (:func:`qutip.about`). +about box (:func:`.about`). .. _steady-usage: @@ -98,7 +98,7 @@ Using the Steadystate Solver ============================= Solving for the steady state solution to the Lindblad master equation for a -general system with :func:`qutip.steadystate` can be accomplished +general system with :func:`.steadystate` can be accomplished using:: >>> rho_ss = steadystate(H, c_ops) @@ -122,7 +122,7 @@ method, and ``solver="spsolve"`` indicate to use the sparse solver. Sparse solvers may still use quite a large amount of memory when they factorize the matrix since the Liouvillian usually has a large bandwidth. -To address this, :func:`qutip.steadystate` allows one to use the bandwidth minimization algorithms +To address this, :func:`.steadystate` allows one to use the bandwidth minimization algorithms listed in :ref:`steady-args`. For example: .. code-block:: python @@ -211,7 +211,7 @@ The following additional solver arguments are available for the steady-state sol See the corresponding documentation from scipy for a full list. -Further information can be found in the :func:`qutip.steadystate` docstrings. +Further information can be found in the :func:`.steadystate` docstrings. .. _steady-example: diff --git a/doc/guide/guide-super.rst b/doc/guide/guide-super.rst index 76ec70cb19..887e60620a 100644 --- a/doc/guide/guide-super.rst +++ b/doc/guide/guide-super.rst @@ -6,7 +6,7 @@ Superoperators, Pauli Basis and Channel Contraction written by `Christopher Granade `, Institute for Quantum Computing -In this guide, we will demonstrate the :func:`tensor_contract` function, which contracts one or more pairs of indices of a Qobj. This functionality can be used to find rectangular superoperators that implement the partial trace channel :math:S(\rho) = \Tr_2(\rho)`, for instance. Using this functionality, we can quickly turn a system-environment representation of an open quantum process into a superoperator representation. +In this guide, we will demonstrate the :func:`.tensor_contract` function, which contracts one or more pairs of indices of a Qobj. This functionality can be used to find rectangular superoperators that implement the partial trace channel :math:S(\rho) = \Tr_2(\rho)`, for instance. Using this functionality, we can quickly turn a system-environment representation of an open quantum process into a superoperator representation. .. _super-representation-plotting: @@ -17,7 +17,7 @@ Superoperator Representations and Plotting We start off by first demonstrating plotting of superoperators, as this will be useful to us in visualizing the results of a contracted channel. -In particular, we will use Hinton diagrams as implemented by :func:`qutip.visualization.hinton`, which +In particular, we will use Hinton diagrams as implemented by :func:`~qutip.visualization.hinton`, which show the real parts of matrix elements as squares whose size and color both correspond to the magnitude of each element. To illustrate, we first plot a few density operators. .. plot:: @@ -35,7 +35,7 @@ We show superoperators as matrices in the *Pauli basis*, such that any Hermicity As an example, conjugation by :math:`\sigma_z` leaves :math:`\mathbb{1}` and :math:`\sigma_z` invariant, but flips the sign of :math:`\sigma_x` and :math:`\sigma_y`. This is indicated in Hinton diagrams by a negative-valued square for the sign change and a positive-valued square for a +1 sign. .. plot:: - :context: + :context: close-figs hinton(to_super(sigmaz())) @@ -43,7 +43,7 @@ As an example, conjugation by :math:`\sigma_z` leaves :math:`\mathbb{1}` and :ma As a couple more examples, we also consider the supermatrix for a Hadamard transform and for :math:`\sigma_z \otimes H`. .. plot:: - :context: + :context: close-figs hinton(to_super(hadamard_transform())) hinton(to_super(tensor(sigmaz(), hadamard_transform()))) @@ -66,7 +66,8 @@ We can think of the :math:`\scriptstyle \rm CNOT` here as a system-environment r :width: 2.5in -The two tensor wires on the left indicate where we must take a tensor contraction to obtain the measurement map. Numbering the tensor wires from 0 to 3, this corresponds to a :func:`tensor_contract` argument of ``(1, 3)``. +The two tensor wires on the left indicate where we must take a tensor contraction to obtain the measurement map. +Numbering the tensor wires from 0 to 3, this corresponds to a :func:`.tensor_contract` argument of ``(1, 3)``. .. plot:: :context: @@ -74,7 +75,7 @@ The two tensor wires on the left indicate where we must take a tensor contractio tensor_contract(to_super(identity([2, 2])), (1, 3)) -Meanwhile, the :func:`super_tensor` function implements the swap on the right, such that we can quickly find the preparation map. +Meanwhile, the :func:`.super_tensor` function implements the swap on the right, such that we can quickly find the preparation map. .. plot:: :context: @@ -86,14 +87,14 @@ Meanwhile, the :func:`super_tensor` function implements the swap on the right, s For a :math:`\scriptstyle \rm CNOT` system-environment model, the composition of these maps should give us a completely dephasing channel. The channel on both qubits is just the superunitary :math:`\scriptstyle \rm CNOT` channel: .. plot:: - :context: + :context: close-figs hinton(to_super(cnot())) We now complete by multiplying the superunitary :math:`\scriptstyle \rm CNOT` by the preparation channel above, then applying the partial trace channel by contracting the second and fourth index indices. As expected, this gives us a dephasing map. .. plot:: - :context: + :context: close-figs hinton(tensor_contract(to_super(cnot()), (1, 3)) * s_prep) diff --git a/doc/guide/guide-tensor.rst b/doc/guide/guide-tensor.rst index b870ba2672..dab91bcfa9 100644 --- a/doc/guide/guide-tensor.rst +++ b/doc/guide/guide-tensor.rst @@ -12,7 +12,7 @@ Tensor products To describe the states of multipartite quantum systems - such as two coupled qubits, a qubit coupled to an oscillator, etc. - we need to expand the Hilbert space by taking the tensor product of the state vectors for each of the system components. Similarly, the operators acting on the state vectors in the combined Hilbert space (describing the coupled system) are formed by taking the tensor product of the individual operators. -In QuTiP the function :func:`qutip.core.tensor.tensor` is used to accomplish this task. This function takes as argument a collection:: +In QuTiP the function :func:`~qutip.core.tensor.tensor` is used to accomplish this task. This function takes as argument a collection:: >>> tensor(op1, op2, op3) # doctest: +SKIP @@ -58,7 +58,7 @@ or equivalently using the ``list`` format: [0.] [0.]] -This is straightforward to generalize to more qubits by adding more component state vectors in the argument list to the :func:`qutip.core.tensor.tensor` function, as illustrated in the following example: +This is straightforward to generalize to more qubits by adding more component state vectors in the argument list to the :func:`~qutip.core.tensor.tensor` function, as illustrated in the following example: .. testcode:: [tensor] @@ -83,7 +83,7 @@ This is straightforward to generalize to more qubits by adding more component st This state is slightly more complicated, describing two qubits in a superposition between the up and down states, while the third qubit is in its ground state. -To construct operators that act on an extended Hilbert space of a combined system, we similarly pass a list of operators for each component system to the :func:`qutip.core.tensor.tensor` function. For example, to form the operator that represents the simultaneous action of the :math:`\sigma_x` operator on two qubits: +To construct operators that act on an extended Hilbert space of a combined system, we similarly pass a list of operators for each component system to the :func:`~qutip.core.tensor.tensor` function. For example, to form the operator that represents the simultaneous action of the :math:`\sigma_x` operator on two qubits: .. testcode:: [tensor] @@ -125,7 +125,7 @@ To create operators in a combined Hilbert space that only act on a single compon Example: Constructing composite Hamiltonians ============================================ -The :func:`qutip.core.tensor.tensor` function is extensively used when constructing Hamiltonians for composite systems. Here we'll look at some simple examples. +The :func:`~qutip.core.tensor.tensor` function is extensively used when constructing Hamiltonians for composite systems. Here we'll look at some simple examples. .. _tensor-product-example-2qubits: @@ -189,15 +189,16 @@ A two-level system coupled to a cavity: The Jaynes-Cummings model The simplest possible quantum mechanical description for light-matter interaction is encapsulated in the Jaynes-Cummings model, which describes the coupling between a two-level atom and a single-mode electromagnetic field (a cavity mode). Denoting the energy splitting of the atom and cavity ``omega_a`` and ``omega_c``, respectively, and the atom-cavity interaction strength ``g``, the Jaynes-Cummings Hamiltonian can be constructed as: -.. testcode:: [tensor] +.. plot:: + :context: reset - N = 10 + N = 6 omega_a = 1.0 omega_c = 1.25 - g = 0.05 + g = 0.75 a = tensor(identity(2), destroy(N)) @@ -207,95 +208,7 @@ The simplest possible quantum mechanical description for light-matter interactio H = 0.5 * omega_a * sz + omega_c * a.dag() * a + g * (a.dag() * sm + a * sm.dag()) - print(H) - -**Output**: - -.. testoutput:: [tensor] - :options: +NORMALIZE_WHITESPACE - - Quantum object: dims = [[2, 10], [2, 10]], shape = (20, 20), type = oper, isherm = True - Qobj data = - [[ 0.5 0. 0. 0. 0. 0. - 0. 0. 0. 0. 0. 0. - 0. 0. 0. 0. 0. 0. - 0. 0. ] - [ 0. 1.75 0. 0. 0. 0. - 0. 0. 0. 0. 0.05 0. - 0. 0. 0. 0. 0. 0. - 0. 0. ] - [ 0. 0. 3. 0. 0. 0. - 0. 0. 0. 0. 0. 0.07071068 - 0. 0. 0. 0. 0. 0. - 0. 0. ] - [ 0. 0. 0. 4.25 0. 0. - 0. 0. 0. 0. 0. 0. - 0.08660254 0. 0. 0. 0. 0. - 0. 0. ] - [ 0. 0. 0. 0. 5.5 0. - 0. 0. 0. 0. 0. 0. - 0. 0.1 0. 0. 0. 0. - 0. 0. ] - [ 0. 0. 0. 0. 0. 6.75 - 0. 0. 0. 0. 0. 0. - 0. 0. 0.1118034 0. 0. 0. - 0. 0. ] - [ 0. 0. 0. 0. 0. 0. - 8. 0. 0. 0. 0. 0. - 0. 0. 0. 0.12247449 0. 0. - 0. 0. ] - [ 0. 0. 0. 0. 0. 0. - 0. 9.25 0. 0. 0. 0. - 0. 0. 0. 0. 0.13228757 0. - 0. 0. ] - [ 0. 0. 0. 0. 0. 0. - 0. 0. 10.5 0. 0. 0. - 0. 0. 0. 0. 0. 0.14142136 - 0. 0. ] - [ 0. 0. 0. 0. 0. 0. - 0. 0. 0. 11.75 0. 0. - 0. 0. 0. 0. 0. 0. - 0.15 0. ] - [ 0. 0.05 0. 0. 0. 0. - 0. 0. 0. 0. -0.5 0. - 0. 0. 0. 0. 0. 0. - 0. 0. ] - [ 0. 0. 0.07071068 0. 0. 0. - 0. 0. 0. 0. 0. 0.75 - 0. 0. 0. 0. 0. 0. - 0. 0. ] - [ 0. 0. 0. 0.08660254 0. 0. - 0. 0. 0. 0. 0. 0. - 2. 0. 0. 0. 0. 0. - 0. 0. ] - [ 0. 0. 0. 0. 0.1 0. - 0. 0. 0. 0. 0. 0. - 0. 3.25 0. 0. 0. 0. - 0. 0. ] - [ 0. 0. 0. 0. 0. 0.1118034 - 0. 0. 0. 0. 0. 0. - 0. 0. 4.5 0. 0. 0. - 0. 0. ] - [ 0. 0. 0. 0. 0. 0. - 0.12247449 0. 0. 0. 0. 0. - 0. 0. 0. 5.75 0. 0. - 0. 0. ] - [ 0. 0. 0. 0. 0. 0. - 0. 0.13228757 0. 0. 0. 0. - 0. 0. 0. 0. 7. 0. - 0. 0. ] - [ 0. 0. 0. 0. 0. 0. - 0. 0. 0.14142136 0. 0. 0. - 0. 0. 0. 0. 0. 8.25 - 0. 0. ] - [ 0. 0. 0. 0. 0. 0. - 0. 0. 0. 0.15 0. 0. - 0. 0. 0. 0. 0. 0. - 9.5 0. ] - [ 0. 0. 0. 0. 0. 0. - 0. 0. 0. 0. 0. 0. - 0. 0. 0. 0. 0. 0. - 0. 10.75 ]] + hinton(H, fig=plt.figure(figsize=(12, 12))) Here ``N`` is the number of Fock states included in the cavity mode. @@ -305,7 +218,12 @@ Here ``N`` is the number of Fock states included in the cavity mode. Partial trace ============= -The partial trace is an operation that reduces the dimension of a Hilbert space by eliminating some degrees of freedom by averaging (tracing). In this sense it is therefore the converse of the tensor product. It is useful when one is interested in only a part of a coupled quantum system. For open quantum systems, this typically involves tracing over the environment leaving only the system of interest. In QuTiP the class method :func:`qutip.Qobj.ptrace` is used to take partial traces. :func:`qutip.Qobj.ptrace` acts on the :class:`qutip.Qobj` instance for which it is called, and it takes one argument ``sel``, which is a ``list`` of integers that mark the component systems that should be **kept**. All other components are traced out. +The partial trace is an operation that reduces the dimension of a Hilbert space by eliminating some degrees of freedom by averaging (tracing). +In this sense it is therefore the converse of the tensor product. +It is useful when one is interested in only a part of a coupled quantum system. +For open quantum systems, this typically involves tracing over the environment leaving only the system of interest. +In QuTiP the class method :meth:`~qutip.core.qobj.Qobj.ptrace` is used to take partial traces. :meth:`~qutip.core.qobj.Qobj.ptrace` acts on the :class:`~qutip.core.qobj.Qobj` instance for which it is called, and it takes one argument ``sel``, which is a ``list`` of integers that mark the component systems that should be **kept**. +All other components are traced out. For example, the density matrix describing a single qubit obtained from a coupled two-qubit system is obtained via: @@ -374,8 +292,8 @@ using the isomorphism To represent superoperators acting on :math:`\mathcal{L}(\mathcal{H}_1 \otimes \mathcal{H}_2)` thus takes some tensor rearrangement to get the desired ordering :math:`\mathcal{H}_1 \otimes \mathcal{H}_2 \otimes \mathcal{H}_1 \otimes \mathcal{H}_2`. -In particular, this means that :func:`qutip.tensor` does not act as -one might expect on the results of :func:`qutip.superop_reps.to_super`: +In particular, this means that :func:`.tensor` does not act as +one might expect on the results of :func:`.to_super`: .. doctest:: [tensor] @@ -394,8 +312,8 @@ of the compound index with dims ``[2, 3]``. In the latter case, however, each of the Hilbert space indices is listed independently and in the wrong order. -The :func:`qutip.tensor.super_tensor` function performs the needed -rearrangement, providing the most direct analog to :func:`qutip.tensor` on +The :func:`.super_tensor` function performs the needed +rearrangement, providing the most direct analog to :func:`.tensor` on the underlying Hilbert space. In particular, for any two ``type="oper"`` Qobjs ``A`` and ``B``, ``to_super(tensor(A, B)) == super_tensor(to_super(A), to_super(B))`` and ``operator_to_vector(tensor(A, B)) == super_tensor(operator_to_vector(A), operator_to_vector(B))``. Returning to the previous example: @@ -405,8 +323,8 @@ Qobjs ``A`` and ``B``, ``to_super(tensor(A, B)) == super_tensor(to_super(A), to_ >>> super_tensor(to_super(A), to_super(B)).dims [[[2, 3], [2, 3]], [[2, 3], [2, 3]]] -The :func:`qutip.tensor.composite` function automatically switches between -:func:`qutip.tensor` and :func:`qutip.tensor.super_tensor` based on the ``type`` +The :func:`.composite` function automatically switches between +:func:`.tensor` and :func:`.super_tensor` based on the ``type`` of its arguments, such that ``composite(A, B)`` returns an appropriate Qobj to represent the composition of two systems. diff --git a/doc/guide/guide-visualization.rst b/doc/guide/guide-visualization.rst index 2b91b30ebb..c80ffc6973 100644 --- a/doc/guide/guide-visualization.rst +++ b/doc/guide/guide-visualization.rst @@ -282,7 +282,7 @@ structure and relative importance of various elements. QuTiP offers a few functions for quickly visualizing matrix data in the form of histograms, :func:`qutip.visualization.matrix_histogram` and as Hinton diagram of weighted squares, :func:`qutip.visualization.hinton`. -These functions takes a :class:`qutip.Qobj` as first argument, and optional arguments to, +These functions takes a :class:`.Qobj` as first argument, and optional arguments to, for example, set the axis labels and figure title (see the function's documentation for details). @@ -390,7 +390,8 @@ Note that to obtain :math:`\chi` with this method we have to construct a matrix Implementation in QuTiP ----------------------- -In QuTiP, the procedure described above is implemented in the function :func:`qutip.tomography.qpt`, which returns the :math:`\chi` matrix given a density matrix propagator. To illustrate how to use this function, let's consider the SWAP gate for two qubits. In QuTiP the function :func:`qutip.core.operators.swap` generates the unitary transformation for the state kets: +In QuTiP, the procedure described above is implemented in the function :func:`qutip.tomography.qpt`, which returns the :math:`\chi` matrix given a density matrix propagator. +To illustrate how to use this function, let's consider the SWAP gate for two qubits. In QuTiP the function :func:`.swap` generates the unitary transformation for the state kets: .. plot:: @@ -430,4 +431,4 @@ We are now ready to compute :math:`\chi` using :func:`qutip.tomography.qpt`, and -For a slightly more advanced example, where the density matrix propagator is calculated from the dynamics of a system defined by its Hamiltonian and collapse operators using the function :func:`qutip.propagator.propagator`, see notebook "Time-dependent master equation: Landau-Zener transitions" on the tutorials section on the QuTiP web site. +For a slightly more advanced example, where the density matrix propagator is calculated from the dynamics of a system defined by its Hamiltonian and collapse operators using the function :func:`.propagator`, see notebook "Time-dependent master equation: Landau-Zener transitions" on the tutorials section on the QuTiP web site. diff --git a/doc/guide/guide.rst b/doc/guide/guide.rst index 820a82fa07..e450ca737e 100644 --- a/doc/guide/guide.rst +++ b/doc/guide/guide.rst @@ -17,11 +17,10 @@ Users Guide guide-steady.rst guide-piqs.rst guide-correlation.rst - guide-control.rst guide-bloch.rst guide-visualization.rst - guide-parfor.rst guide-saving.rst guide-random.rst guide-settings.rst guide-measurement.rst + guide-control.rst diff --git a/doc/guide/heom/intro.rst b/doc/guide/heom/intro.rst index 5ae195250c..fd3c2ea624 100644 --- a/doc/guide/heom/intro.rst +++ b/doc/guide/heom/intro.rst @@ -40,4 +40,4 @@ In addition to support for bosonic environments, QuTiP also provides support for feriomic environments which is described in :doc:`fermionic`. Both bosonic and fermionic environments are supported via a single solver, -:class:`~qutip.nonmarkov.heom.HEOMSolver`, that supports solving for both dynamics and steady-states. +:class:`.HEOMSolver`, that supports solving for both dynamics and steady-states. From 09967bbde50061e250b7d6905b71d64af2f1187b Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Tue, 26 Dec 2023 16:08:46 +0900 Subject: [PATCH 03/66] Unified parallel_map and loky_pmap --- qutip/solver/parallel.py | 265 +++++++++++++++++++++------------------ 1 file changed, 143 insertions(+), 122 deletions(-) diff --git a/qutip/solver/parallel.py b/qutip/solver/parallel.py index 4e51c2c8a3..4038650ad8 100644 --- a/qutip/solver/parallel.py +++ b/qutip/solver/parallel.py @@ -131,57 +131,34 @@ def serial_map(task, values, task_args=None, task_kwargs=None, return results -def parallel_map(task, values, task_args=None, task_kwargs=None, - reduce_func=None, map_kw=None, - progress_bar=None, progress_bar_kwargs={}): +def _generic_pmap(task, values, task_args, task_kwargs, + reduce_func, timeout, fail_fast, + progress_bar, progress_bar_kwargs, + setup_executor, extract_result, shutdown_executor): """ - Parallel execution of a mapping of ``values`` to the function ``task``. - This is functionally equivalent to:: - - result = [task(value, *task_args, **task_kwargs) for value in values] - - Parameters - ---------- - task : a Python function - The function that is to be called for each value in ``task_vec``. - values : array / list - The list or array of values for which the ``task`` function is to be - evaluated. - task_args : list, optional - The optional additional arguments to the ``task`` function. - task_kwargs : dictionary, optional - The optional additional keyword arguments to the ``task`` function. - reduce_func : func, optional - If provided, it will be called with the output of each task instead of - storing them in a list. Note that the order in which results are - passed to ``reduce_func`` is not defined. It should return None or a - number. When returning a number, it represents the estimation of the - number of tasks left. On a return <= 0, the map will end early. - progress_bar : str, optional - Progress bar options's string for showing progress. - progress_bar_kwargs : dict, optional - Options for the progress bar. - map_kw: dict, optional - Dictionary containing entry for: - - timeout: float, Maximum time (sec) for the whole map. - - num_cpus: int, Number of jobs to run at once. - - fail_fast: bool, Abort at the first error. - - Returns - ------- - result : list - The result list contains the value of - ``task(value, *task_args, **task_kwargs)`` for - each value in ``values``. If a ``reduce_func`` is provided, and empty - list will be returned. - + Common functionality for parallel_map, loky_pmap and mpi_pmap. + The parameters `setup_executor`, `extract_value` and `destroy_executor` + are callback functions with the following signatures: + + setup_executor: () -> ProcessPoolExecutor, int + The second return value specifies the number of workers + + extract_result: (index: int, future: Future) -> Any + index: Corresponds to the indices of the `values` list + future: The Future that has finished running + + shutdown_executor: (executor: ProcessPoolExecutor, + active_tasks: set[Future]) -> None + executor: The ProcessPoolExecutor that was created in setup_executor + active_tasks: A set of Futures that are currently still being executed + (non-empty if: timeout, error, or reduce_func requesting exit) """ + if task_args is None: task_args = () if task_kwargs is None: task_kwargs = {} - map_kw = _read_map_kw(map_kw) - end_time = map_kw['timeout'] + time.time() + end_time = timeout + time.time() progress_bar = progress_bars[progress_bar]( len(values), **progress_bar_kwargs @@ -191,6 +168,7 @@ def parallel_map(task, values, task_args=None, task_kwargs=None, finished = [] if reduce_func is not None: results = None + def result_func(_, value): return reduce_func(value) else: @@ -200,7 +178,7 @@ def result_func(_, value): def _done_callback(future): if not future.cancelled(): try: - result = future.result() + result = extract_result(future._i, future) remaining_ntraj = result_func(future._i, result) if remaining_ntraj is not None and remaining_ntraj <= 0: finished.append(True) @@ -220,25 +198,20 @@ def _done_callback(future): pass progress_bar.update() - if sys.version_info >= (3, 7): - # ProcessPoolExecutor only supports mp_context from 3.7 onwards - ctx_kw = {"mp_context": mp_context} - else: - ctx_kw = {} - os.environ['QUTIP_IN_PARALLEL'] = 'TRUE' try: - with concurrent.futures.ProcessPoolExecutor( - max_workers=map_kw['num_cpus'], **ctx_kw, - ) as executor: + executor, num_workers = setup_executor() + with executor: waiting = set() i = 0 + aborted = False + while i < len(values): # feed values to the executor, ensuring that there is at # most one task per worker at any moment in time so that # we can shutdown without waiting for greater than the time # taken by the longest task - if len(waiting) >= map_kw['num_cpus']: + if len(waiting) >= num_workers: # no space left, wait for a task to complete or # the time to run out timeout = max(0, end_time - time.time()) @@ -249,12 +222,13 @@ def _done_callback(future): ) if ( time.time() >= end_time - or (errors and map_kw['fail_fast']) + or (errors and fail_fast) or finished ): # no time left, exit the loop + aborted = True break - while len(waiting) < map_kw['num_cpus'] and i < len(values): + while len(waiting) < num_workers and i < len(values): # space and time available, add tasks value = values[i] future = executor.submit( @@ -268,13 +242,21 @@ def _done_callback(future): waiting.add(future) i += 1 - timeout = max(0, end_time - time.time()) - concurrent.futures.wait(waiting, timeout=timeout) + if not aborted: + # all tasks have been submitted, timeout has not been reaches + # -> wait for all workers to finish before shutting down + timeout = max(0, end_time - time.time()) + _done, waiting = concurrent.futures.wait( + waiting, + timeout=timeout, + return_when=concurrent.futures.ALL_COMPLETED + ) + shutdown_executor(executor, waiting) finally: os.environ['QUTIP_IN_PARALLEL'] = 'FALSE' progress_bar.finished() - if errors and map_kw["fail_fast"]: + if errors and fail_fast: raise list(errors.values())[0] elif errors: raise MapExceptions( @@ -285,17 +267,15 @@ def _done_callback(future): return results -def loky_pmap(task, values, task_args=None, task_kwargs=None, - reduce_func=None, map_kw=None, - progress_bar=None, progress_bar_kwargs={}): +def parallel_map(task, values, task_args=None, task_kwargs=None, + reduce_func=None, map_kw=None, + progress_bar=None, progress_bar_kwargs={}): """ Parallel execution of a mapping of ``values`` to the function ``task``. This is functionally equivalent to:: result = [task(value, *task_args, **task_kwargs) for value in values] - Use the loky module instead of multiprocessing. - Parameters ---------- task : a Python function @@ -309,8 +289,8 @@ def loky_pmap(task, values, task_args=None, task_kwargs=None, The optional additional keyword arguments to the ``task`` function. reduce_func : func, optional If provided, it will be called with the output of each task instead of - storing them in a list. Note that the results are passed to - ``reduce_func`` in the original order. It should return None or a + storing them in a list. Note that the order in which results are + passed to ``reduce_func`` is not defined. It should return None or a number. When returning a number, it represents the estimation of the number of tasks left. On a return <= 0, the map will end early. progress_bar : str, optional @@ -332,68 +312,109 @@ def loky_pmap(task, values, task_args=None, task_kwargs=None, list will be returned. """ - if task_args is None: - task_args = () - if task_kwargs is None: - task_kwargs = {} + map_kw = _read_map_kw(map_kw) - end_time = map_kw['timeout'] + time.time() + if sys.version_info >= (3, 7): + # ProcessPoolExecutor only supports mp_context from 3.7 onwards + ctx_kw = {"mp_context": mp_context} + else: + ctx_kw = {} - progress_bar = progress_bars[progress_bar]( - len(values), **progress_bar_kwargs - ) + def setup_executor(): + num_workers = map_kw['num_cpus'] + executor = concurrent.futures.ProcessPoolExecutor( + max_workers=num_workers, **ctx_kw, + ) + return executor, num_workers + + def extract_result (_, future): + return future.result() - errors = {} - remaining_ntraj = None - if reduce_func is None: - results = [None] * len(values) - else: - results = None + def shutdown_executor(executor, _): + # Since `ProcessPoolExecutor` leaves no other option, + # we wait for all worker processes to finish their current task + executor.shutdown() - os.environ['QUTIP_IN_PARALLEL'] = 'TRUE' - from loky import get_reusable_executor, TimeoutError - try: - executor = get_reusable_executor(max_workers=map_kw['num_cpus']) + return _generic_pmap(task, values, task_args, task_kwargs, + reduce_func, map_kw['timeout'], map_kw['fail_fast'], + progress_bar, progress_bar_kwargs, + setup_executor, extract_result, shutdown_executor) - jobs = [executor.submit(task, value, *task_args, **task_kwargs) - for value in values] - for n, job in enumerate(jobs): - remaining_time = max(end_time - time.time(), 0) - try: - result = job.result(remaining_time) - except TimeoutError: - [job.cancel() for job in jobs] - break - except Exception as err: - if map_kw["fail_fast"]: - raise err - else: - errors[n] = err - else: - if reduce_func is not None: - remaining_ntraj = reduce_func(result) - else: - results[n] = result - progress_bar.update() - if remaining_ntraj is not None and remaining_ntraj <= 0: - break - - except KeyboardInterrupt as e: - [job.cancel() for job in jobs] - raise e +def loky_pmap(task, values, task_args=None, task_kwargs=None, + reduce_func=None, map_kw=None, + progress_bar=None, progress_bar_kwargs={}): + """ + Parallel execution of a mapping of ``values`` to the function ``task``. + This is functionally equivalent to:: - finally: - os.environ['QUTIP_IN_PARALLEL'] = 'FALSE' - executor.shutdown(kill_workers=True) + result = [task(value, *task_args, **task_kwargs) for value in values] + + Use the loky module instead of multiprocessing. + + Parameters + ---------- + task : a Python function + The function that is to be called for each value in ``task_vec``. + values : array / list + The list or array of values for which the ``task`` function is to be + evaluated. + task_args : list, optional + The optional additional arguments to the ``task`` function. + task_kwargs : dictionary, optional + The optional additional keyword arguments to the ``task`` function. + reduce_func : func, optional + If provided, it will be called with the output of each task instead of + storing them in a list. Note that the order in which results are + passed to ``reduce_func`` is not defined. It should return None or a + number. When returning a number, it represents the estimation of the + number of tasks left. On a return <= 0, the map will end early. + progress_bar : str, optional + Progress bar options's string for showing progress. + progress_bar_kwargs : dict, optional + Options for the progress bar. + map_kw: dict, optional + Dictionary containing entry for: + - timeout: float, Maximum time (sec) for the whole map. + - num_cpus: int, Number of jobs to run at once. + - fail_fast: bool, Abort at the first error. + + Returns + ------- + result : list + The result list contains the value of + ``task(value, *task_args, **task_kwargs)`` for + each value in ``values``. If a ``reduce_func`` is provided, and empty + list will be returned. + + """ + + from loky import get_reusable_executor + from loky.process_executor import ShutdownExecutorError + map_kw = _read_map_kw(map_kw) + + def setup_executor(): + num_workers = map_kw['num_cpus'] + executor = get_reusable_executor(max_workers=num_workers) + return executor, num_workers + + def extract_result (_, future: concurrent.futures.Future): + if isinstance(future.exception(), ShutdownExecutorError): + # Task was aborted due to timeout etc + return None + return future.result() + + def shutdown_executor(executor, active_tasks): + # If there are still tasks running, we kill all workers in order to + # return immediately. Otherwise, `kill_workers` is set to False so + # that the worker threads can be reused in subsequent loky_pmap calls. + executor.shutdown(kill_workers=(len(active_tasks) > 0)) + + return _generic_pmap(task, values, task_args, task_kwargs, + reduce_func, map_kw['timeout'], map_kw['fail_fast'], + progress_bar, progress_bar_kwargs, + setup_executor, extract_result, shutdown_executor) - progress_bar.finished() - if errors: - raise MapExceptions( - f"{len(errors)} iterations failed in loky_pmap", - errors, results - ) - return results _get_map = { From d08c235957c4687b86211e18898932f527c5d3c3 Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Tue, 26 Dec 2023 17:10:44 +0900 Subject: [PATCH 04/66] Added mpi_pmap --- qutip/solver/parallel.py | 79 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 77 insertions(+), 2 deletions(-) diff --git a/qutip/solver/parallel.py b/qutip/solver/parallel.py index 4038650ad8..ca85aae101 100644 --- a/qutip/solver/parallel.py +++ b/qutip/solver/parallel.py @@ -272,7 +272,7 @@ def parallel_map(task, values, task_args=None, task_kwargs=None, progress_bar=None, progress_bar_kwargs={}): """ Parallel execution of a mapping of ``values`` to the function ``task``. - This is functionally equivalent to:: + This is functionally equivalent to: result = [task(value, *task_args, **task_kwargs) for value in values] @@ -346,7 +346,7 @@ def loky_pmap(task, values, task_args=None, task_kwargs=None, progress_bar=None, progress_bar_kwargs={}): """ Parallel execution of a mapping of ``values`` to the function ``task``. - This is functionally equivalent to:: + This is functionally equivalent to: result = [task(value, *task_args, **task_kwargs) for value in values] @@ -416,6 +416,80 @@ def shutdown_executor(executor, active_tasks): setup_executor, extract_result, shutdown_executor) +def mpi_pmap(task, values, task_args=None, task_kwargs=None, + reduce_func=None, map_kw=None, + progress_bar=None, progress_bar_kwargs={}): + """ + Parallel execution of a mapping of ``values`` to the function ``task``. + This is functionally equivalent to: + + result = [task(value, *task_args, **task_kwargs) for value in values] + + Uses the mpi4py module to execute the tasks asynchronously with MPI + processes. For more information, consult the documentation of mpi4py and + the mpi4py.MPIPoolExecutor class. + + Parameters + ---------- + task : a Python function + The function that is to be called for each value in ``task_vec``. + values : array / list + The list or array of values for which the ``task`` function is to be + evaluated. + task_args : list, optional + The optional additional arguments to the ``task`` function. + task_kwargs : dictionary, optional + The optional additional keyword arguments to the ``task`` function. + reduce_func : func, optional + If provided, it will be called with the output of each task instead of + storing them in a list. Note that the order in which results are + passed to ``reduce_func`` is not defined. It should return None or a + number. When returning a number, it represents the estimation of the + number of tasks left. On a return <= 0, the map will end early. + progress_bar : str, optional + Progress bar options's string for showing progress. + progress_bar_kwargs : dict, optional + Options for the progress bar. + map_kw: dict, optional + Dictionary containing entry for: + - timeout: float, Maximum time (sec) for the whole map. + - num_cpus: int, Number of jobs to run at once. + - fail_fast: bool, Abort at the first error. + All remaining entries of map_kw will be passed to the + mpi4py.MPIPoolExecutor constructor. + + Returns + ------- + result : list + The result list contains the value of + ``task(value, *task_args, **task_kwargs)`` for + each value in ``values``. If a ``reduce_func`` is provided, and empty + list will be returned. + + """ + + from mpi4py.futures import MPIPoolExecutor + map_kw = _read_map_kw(map_kw) + timeout = map_kw.pop('timeout') + num_workers = map_kw.pop('num_cpus') + fail_fast = map_kw.pop('fail_fast') + + def setup_executor(): + executor = MPIPoolExecutor(max_workers=num_workers, **map_kw) + return executor, num_workers + + def extract_result (_, future): + return future.result() + + def shutdown_executor(executor, _): + executor.shutdown() + + return _generic_pmap(task, values, task_args, task_kwargs, + reduce_func, timeout, fail_fast, + progress_bar, progress_bar_kwargs, + setup_executor, extract_result, shutdown_executor) + + _get_map = { "parallel_map": parallel_map, @@ -423,4 +497,5 @@ def shutdown_executor(executor, active_tasks): "serial_map": serial_map, "serial": serial_map, "loky": loky_pmap, + "mpi": mpi_pmap } From 923bfda53bd831ead3271e337e61d9342268ede2 Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Wed, 27 Dec 2023 14:19:11 +0900 Subject: [PATCH 05/66] Clean up stochastic.py imports --- qutip/solver/stochastic.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/qutip/solver/stochastic.py b/qutip/solver/stochastic.py index f548b2a907..632a23a041 100644 --- a/qutip/solver/stochastic.py +++ b/qutip/solver/stochastic.py @@ -1,11 +1,10 @@ __all__ = ["smesolve", "SMESolver", "ssesolve", "SSESolver"] -from .sode.ssystem import * +from .sode.ssystem import StochasticOpenSystem, StochasticClosedSystem from .result import MultiTrajResult, Result, ExpectOp from .multitraj import MultiTrajSolver -from .. import Qobj, QobjEvo, liouvillian, lindblad_dissipator +from .. import Qobj, QobjEvo import numpy as np -from collections.abc import Iterable from functools import partial from .solver_base import _solver_deprecation from ._feedback import _QobjFeedback, _DataFeedback, _WeinerFeedback From 8a727118fe76a9c75c6c0c5ee8ef333403b6102a Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Wed, 27 Dec 2023 15:24:44 +0900 Subject: [PATCH 06/66] Added mpi_options option to MultiTrajSolver and all subclasses. Cleaned up options docstrings of these classes and related solve functions. --- doc/apidoc/functions.rst | 2 +- qutip/solver/mcsolve.py | 56 ++++++++++++++---------- qutip/solver/multitraj.py | 8 ++-- qutip/solver/nm_mcsolve.py | 52 ++++++++++++++--------- qutip/solver/parallel.py | 6 +-- qutip/solver/stochastic.py | 87 ++++++++++++++++++-------------------- 6 files changed, 116 insertions(+), 95 deletions(-) diff --git a/doc/apidoc/functions.rst b/doc/apidoc/functions.rst index c54628f1c4..6f8f4b4f93 100644 --- a/doc/apidoc/functions.rst +++ b/doc/apidoc/functions.rst @@ -297,7 +297,7 @@ Parallelization --------------- .. automodule:: qutip.solver.parallel - :members: parallel_map, serial_map, loky_pmap + :members: parallel_map, serial_map, loky_pmap, mpi_pmap .. _functions-ipython: diff --git a/qutip/solver/mcsolve.py b/qutip/solver/mcsolve.py index a55b1a094c..adb2677e3e 100644 --- a/qutip/solver/mcsolve.py +++ b/qutip/solver/mcsolve.py @@ -62,8 +62,6 @@ def mcsolve(H, state, tlist, c_ops=(), e_ops=None, ntraj=500, *, | Whether or not to store the state vectors or density matrices. On `None` the states will be saved if no expectation operators are given. - - | normalize_output : bool - | Normalize output state to hide ODE numerical errors. - | progress_bar : str {'text', 'enhanced', 'tqdm', ''} | How to present the solver progress. 'tqdm' uses the python module of the same name and raise an error @@ -78,18 +76,26 @@ def mcsolve(H, state, tlist, c_ops=(), e_ops=None, ntraj=500, *, | Maximum number of (internally defined) steps allowed in one ``tlist`` step. - | max_step : float - | Maximum lenght of one internal step. When using pulses, it should be + | Maximum length of one internal step. When using pulses, it should be less than half the width of the thinnest pulse. - | keep_runs_results : bool, [False] | Whether to store results from all trajectories or just store the averages. - - | map : str {"serial", "parallel", "loky"} - | How to run the trajectories. "parallel" uses concurent module to - run in parallel while "loky" use the module of the same name to do - so. + - | map : str {"serial", "parallel", "loky", "mpi"} + | How to run the trajectories. "parallel" uses the multiprocessing + module to run in parallel while "loky" and "mpi" use the "loky" and + "mpi4py" modules to do so. + - | mpi_options : dict + | Only applies if map is "mpi". This dictionary will be passed as + keyword arguments to the `mpi4py.futures.MPIPoolExecutor` + constructor. Note that the `max_workers` argument is provided + separately through the `num_cpus` option. - | num_cpus : int | Number of cpus to use when running in parallel. ``None`` detect the number of available cpus. + - | bitgenerator : {None, "MT19937", "PCG64", "PCG64DXSM", ...} + Which of numpy.random's bitgenerator to use. With `None`, your + numpy version's default is used. - | norm_t_tol, norm_tol, norm_steps : float, float, int | Parameters used to find the collapse location. ``norm_t_tol`` and ``norm_tol`` are the tolerance in time and norm respectively. @@ -102,6 +108,10 @@ def mcsolve(H, state, tlist, c_ops=(), e_ops=None, ntraj=500, *, | Whether to use the improved sampling algorithm from Abdelhafez et al. PRA (2019) + Additional options may be available depending on the selected + differential equation integration method, see + `Integrator <./classes.html#classes-ode>`_. + seeds : int, SeedSequence, list, optional Seed for the random number generator. It can be a single seed used to spawn seeds for each trajectory or a list of seeds, one for each @@ -407,21 +417,15 @@ class MCSolver(MultiTrajSolver): _trajectory_resultclass = McTrajectoryResult _mc_integrator_class = MCIntegrator solver_options = { - "progress_bar": "text", - "progress_kwargs": {"chunk_size": 10}, - "store_final_state": False, - "store_states": None, - "keep_runs_results": False, + **MultiTrajSolver.solver_options, "method": "adams", - "map": "serial", - "num_cpus": None, - "bitgenerator": None, "mc_corr_eps": 1e-10, "norm_steps": 5, "norm_t_tol": 1e-6, "norm_tol": 1e-4, "improved_sampling": False, } + del solver_options["normalize_output"] def __init__(self, H, c_ops, *, options=None): _time_start = time() @@ -589,16 +593,22 @@ def options(self): ``chunk_size``. keep_runs_results: bool, default: False - Whether to store results from all trajectories or just store the - averages. + Whether to store results from all trajectories or just store the + averages. method: str, default: "adams" - Which ODE integrator methods are supported. - - map: str {"serial", "parallel", "loky"}, default: "serial" - How to run the trajectories. "parallel" uses concurent module to - run in parallel while "loky" use the module of the same name to do - so. + Which differential equation integration method to use. + + map: str {"serial", "parallel", "loky", "mpi"}, default: "serial" + How to run the trajectories. "parallel" uses the multiprocessing + module to run in parallel while "loky" and "mpi" use the "loky" and + "mpi4py" modules to do so. + + mpi_options: dict, default: {} + Only applies if map is "mpi". This dictionary will be passed as + keyword arguments to the `mpi4py.futures.MPIPoolExecutor` + constructor. Note that the `max_workers` argument is provided + separately through the `num_cpus` option. num_cpus: None, int Number of cpus to use when running in parallel. ``None`` detect the diff --git a/qutip/solver/multitraj.py b/qutip/solver/multitraj.py index 60f52a7202..679bffab3d 100644 --- a/qutip/solver/multitraj.py +++ b/qutip/solver/multitraj.py @@ -1,5 +1,5 @@ from .result import Result, MultiTrajResult -from .parallel import _get_map +from .parallel import _get_map, mpi_pmap from time import time from .solver_base import Solver from ..core import QobjEvo @@ -64,6 +64,7 @@ class MultiTrajSolver(Solver): "normalize_output": False, "method": "", "map": "serial", + "mpi_options": {}, "num_cpus": None, "bitgenerator": None, } @@ -143,10 +144,11 @@ def _initialize_run(self, state, ntraj=1, args=None, e_ops=(), result.add_end_condition(ntraj, target_tol) map_func = _get_map[self.options['map']] - map_kw = { + map_kw = self.options['mpi_options'] if map_func == mpi_pmap else {} + map_kw.update({ 'timeout': timeout, 'num_cpus': self.options['num_cpus'], - } + }) state0 = self._prepare_state(state) stats['preparation time'] += time() - start_time return stats, seeds, result, map_func, map_kw, state0 diff --git a/qutip/solver/nm_mcsolve.py b/qutip/solver/nm_mcsolve.py index 690a86c1be..a8df2b93ce 100644 --- a/qutip/solver/nm_mcsolve.py +++ b/qutip/solver/nm_mcsolve.py @@ -83,8 +83,6 @@ def nm_mcsolve(H, state, tlist, ops_and_rates=(), e_ops=None, ntraj=500, *, | Whether or not to store the state vectors or density matrices. On `None` the states will be saved if no expectation operators are given. - - | normalize_output : bool - | Normalize output state to hide ODE numerical errors. - | progress_bar : str {'text', 'enhanced', 'tqdm', ''} | How to present the solver progress. 'tqdm' uses the python module of the same name and raise an error @@ -99,18 +97,26 @@ def nm_mcsolve(H, state, tlist, ops_and_rates=(), e_ops=None, ntraj=500, *, | Maximum number of (internally defined) steps allowed in one ``tlist`` step. - | max_step : float - | Maximum lenght of one internal step. When using pulses, it should be + | Maximum length of one internal step. When using pulses, it should be less than half the width of the thinnest pulse. - | keep_runs_results : bool, [False] | Whether to store results from all trajectories or just store the averages. - - | map : str {"serial", "parallel", "loky"} - | How to run the trajectories. "parallel" uses concurent module to - run in parallel while "loky" use the module of the same name to do - so. + - | map : str {"serial", "parallel", "loky", "mpi"} + | How to run the trajectories. "parallel" uses the multiprocessing + module to run in parallel while "loky" and "mpi" use the "loky" and + "mpi4py" modules to do so. + - | mpi_options : dict + | Only applies if map is "mpi". This dictionary will be passed as + keyword arguments to the `mpi4py.futures.MPIPoolExecutor` + constructor. Note that the `max_workers` argument is provided + separately through the `num_cpus` option. - | num_cpus : int | Number of cpus to use when running in parallel. ``None`` detect the number of available cpus. + - | bitgenerator : {None, "MT19937", "PCG64", "PCG64DXSM", ...} + Which of numpy.random's bitgenerator to use. With `None`, your + numpy version's default is used. - | norm_t_tol, norm_tol, norm_steps : float, float, int | Parameters used to find the collapse location. ``norm_t_tol`` and ``norm_tol`` are the tolerance in time and norm respectively. @@ -119,9 +125,6 @@ def nm_mcsolve(H, state, tlist, ops_and_rates=(), e_ops=None, ntraj=500, *, - | mc_corr_eps : float | Small number used to detect non-physical collapse caused by numerical imprecision. - - | improved_sampling : Bool - | Whether to use the improved sampling algorithm from Abdelhafez et - al. PRA (2019) - | completeness_rtol, completeness_atol : float, float | Parameters used in determining whether the given Lindblad operators satisfy a certain completeness relation. If they do not, an @@ -132,6 +135,9 @@ def nm_mcsolve(H, state, tlist, ops_and_rates=(), e_ops=None, ntraj=500, *, integration of the martingale. Note that the 'improved_sampling' option is not currently supported. + Additional options may be available depending on the selected + differential equation integration method, see + `Integrator <./classes.html#classes-ode>`_. seeds : int, SeedSequence, list, optional Seed for the random number generator. It can be a single seed used to @@ -538,24 +544,30 @@ def options(self): progress_bar: str {'text', 'enhanced', 'tqdm', ''}, default: "text" How to present the solver progress. - 'tqdm' uses the python module of the same name and raise an error - if not installed. Empty string or False will disable the bar. + 'tqdm' uses the python module of the same name and raise an error if + not installed. Empty string or False will disable the bar. progress_kwargs: dict, default: {"chunk_size":10} Arguments to pass to the progress_bar. Qutip's bars use ``chunk_size``. keep_runs_results: bool, default: False - Whether to store results from all trajectories or just store the - averages. + Whether to store results from all trajectories or just store the + averages. method: str, default: "adams" - Which ODE integrator methods are supported. - - map: str {"serial", "parallel", "loky"}, default: "serial" - How to run the trajectories. "parallel" uses concurent module to - run in parallel while "loky" use the module of the same name to do - so. + Which differential equation integration method to use. + + map: str {"serial", "parallel", "loky", "mpi"}, default: "serial" + How to run the trajectories. "parallel" uses the multiprocessing + module to run in parallel while "loky" and "mpi" use the "loky" and + "mpi4py" modules to do so. + + mpi_options: dict, default: {} + Only applies if map is "mpi". This dictionary will be passed as + keyword arguments to the `mpi4py.futures.MPIPoolExecutor` + constructor. Note that the `max_workers` argument is provided + separately through the `num_cpus` option. num_cpus: None, int Number of cpus to use when running in parallel. ``None`` detect the diff --git a/qutip/solver/parallel.py b/qutip/solver/parallel.py index ca85aae101..20dfc29dcd 100644 --- a/qutip/solver/parallel.py +++ b/qutip/solver/parallel.py @@ -272,7 +272,7 @@ def parallel_map(task, values, task_args=None, task_kwargs=None, progress_bar=None, progress_bar_kwargs={}): """ Parallel execution of a mapping of ``values`` to the function ``task``. - This is functionally equivalent to: + This is functionally equivalent to:: result = [task(value, *task_args, **task_kwargs) for value in values] @@ -346,7 +346,7 @@ def loky_pmap(task, values, task_args=None, task_kwargs=None, progress_bar=None, progress_bar_kwargs={}): """ Parallel execution of a mapping of ``values`` to the function ``task``. - This is functionally equivalent to: + This is functionally equivalent to:: result = [task(value, *task_args, **task_kwargs) for value in values] @@ -421,7 +421,7 @@ def mpi_pmap(task, values, task_args=None, task_kwargs=None, progress_bar=None, progress_bar_kwargs={}): """ Parallel execution of a mapping of ``values`` to the function ``task``. - This is functionally equivalent to: + This is functionally equivalent to:: result = [task(value, *task_args, **task_kwargs) for value in values] diff --git a/qutip/solver/stochastic.py b/qutip/solver/stochastic.py index 632a23a041..8e1cc9a033 100644 --- a/qutip/solver/stochastic.py +++ b/qutip/solver/stochastic.py @@ -332,13 +332,21 @@ def smesolve( - | method : str | Which stochastic differential equation integration method to use. Main ones are {"euler", "rouchon", "platen", "taylor1.5_imp"} - - | map : str {"serial", "parallel", "loky"} - | How to run the trajectories. "parallel" uses concurent module to - run in parallel while "loky" use the module of the same name to do - so. + - | map : str {"serial", "parallel", "loky", "mpi"} + | How to run the trajectories. "parallel" uses the multiprocessing + module to run in parallel while "loky" and "mpi" use the "loky" and + "mpi4py" modules to do so. + - | mpi_options : dict + | Only applies if map is "mpi". This dictionary will be passed as + keyword arguments to the `mpi4py.futures.MPIPoolExecutor` + constructor. Note that the `max_workers` argument is provided + separately through the `num_cpus` option. - | num_cpus : NoneType, int | Number of cpus to use when running in parallel. ``None`` detect the number of available cpus. + - | bitgenerator : {None, "MT19937", "PCG64", "PCG64DXSM", ...} + Which of numpy.random's bitgenerator to use. With `None`, your + numpy version's default is used. - | dt : float | The finite steps lenght for the Stochastic integration method. Default change depending on the integrator. @@ -452,13 +460,21 @@ def ssesolve( - | method : str | Which stochastic differential equation integration method to use. Main ones are {"euler", "rouchon", "platen", "taylor1.5_imp"} - - | map : str {"serial", "parallel", "loky"} - How to run the trajectories. "parallel" uses concurent module to - run in parallel while "loky" use the module of the same name to do - so. + - | map : str {"serial", "parallel", "loky", "mpi"} + | How to run the trajectories. "parallel" uses the multiprocessing + module to run in parallel while "loky" and "mpi" use the "loky" and + "mpi4py" modules to do so. + - | mpi_options : dict + | Only applies if map is "mpi". This dictionary will be passed as + keyword arguments to the `mpi4py.futures.MPIPoolExecutor` + constructor. Note that the `max_workers` argument is provided + separately through the `num_cpus` option. - | num_cpus : NoneType, int | Number of cpus to use when running in parallel. ``None`` detect the number of available cpus. + - | bitgenerator : {None, "MT19937", "PCG64", "PCG64DXSM", ...} + Which of numpy.random's bitgenerator to use. With `None`, your + numpy version's default is used. - | dt : float | The finite steps lenght for the Stochastic integration method. Default change depending on the integrator. @@ -493,17 +509,9 @@ class StochasticSolver(MultiTrajSolver): system = None _open = None solver_options = { - "progress_bar": "text", - "progress_kwargs": {"chunk_size": 10}, - "store_final_state": False, - "store_states": None, + **MultiTrajSolver.solver_options, + "method": "platen", "store_measurement": False, - "keep_runs_results": False, - "normalize_output": False, - "method": "taylor1.5", - "map": "serial", - "num_cpus": None, - "bitgenerator": None, } def __init__(self, H, sc_ops, heterodyne, *, c_ops=(), options=None): @@ -670,13 +678,22 @@ def options(self): Whether to store results from all trajectories or just store the averages. + normalize_output: bool + Normalize output state to hide ODE numerical errors. + method: str, default: "platen" - Which ODE integrator methods are supported. + Which differential equation integration method to use. + + map: str {"serial", "parallel", "loky", "mpi"}, default: "serial" + How to run the trajectories. "parallel" uses the multiprocessing + module to run in parallel while "loky" and "mpi" use the "loky" and + "mpi4py" modules to do so. - map: str {"serial", "parallel", "loky"}, default: "serial" - How to run the trajectories. "parallel" uses concurent module to - run in parallel while "loky" use the module of the same name to do - so. + mpi_options: dict, default: {} + Only applies if map is "mpi". This dictionary will be passed as + keyword arguments to the `mpi4py.futures.MPIPoolExecutor` + constructor. Note that the `max_workers` argument is provided + separately through the `num_cpus` option. num_cpus: None, int, default: None Number of cpus to use when running in parallel. ``None`` detect the @@ -778,17 +795,7 @@ class SMESolver(StochasticSolver): _avail_integrators = {} _open = True solver_options = { - "progress_bar": "text", - "progress_kwargs": {"chunk_size": 10}, - "store_final_state": False, - "store_states": None, - "store_measurement": False, - "keep_runs_results": False, - "normalize_output": False, - "method": "platen", - "map": "serial", - "num_cpus": None, - "bitgenerator": None, + **StochasticSolver.solver_options } @@ -821,15 +828,5 @@ class SSESolver(StochasticSolver): _avail_integrators = {} _open = False solver_options = { - "progress_bar": "text", - "progress_kwargs": {"chunk_size": 10}, - "store_final_state": False, - "store_states": None, - "store_measurement": False, - "keep_runs_results": False, - "normalize_output": False, - "method": "platen", - "map": "serial", - "num_cpus": None, - "bitgenerator": None, + **StochasticSolver.solver_options } From 4f0f079d24a2349fd196241e8861eefb055c0087 Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Wed, 27 Dec 2023 15:46:11 +0900 Subject: [PATCH 07/66] Added mpi_pmap to tests --- qutip/tests/solver/test_parallel.py | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/qutip/tests/solver/test_parallel.py b/qutip/tests/solver/test_parallel.py index bc3af0d62f..b7836983ee 100644 --- a/qutip/tests/solver/test_parallel.py +++ b/qutip/tests/solver/test_parallel.py @@ -4,7 +4,7 @@ import threading from qutip.solver.parallel import ( - parallel_map, serial_map, loky_pmap, MapExceptions + parallel_map, serial_map, loky_pmap, mpi_pmap, MapExceptions ) @@ -22,6 +22,7 @@ def _func2(x, a, b, c, d=0, e=0, f=0): @pytest.mark.parametrize('map', [ pytest.param(parallel_map, id='parallel_map'), pytest.param(loky_pmap, id='loky_pmap'), + pytest.param(mpi_pmap, id='mpi_pmap'), pytest.param(serial_map, id='serial_map'), ]) @pytest.mark.parametrize('num_cpus', @@ -29,7 +30,9 @@ def _func2(x, a, b, c, d=0, e=0, f=0): ids=['1', '2']) def test_map(map, num_cpus): if map is loky_pmap: - loky = pytest.importorskip("loky") + pytest.importorskip("loky") + if map is mpi_pmap: + pytest.importorskip("mpi4py") args = (1, 2, 3) kwargs = {'d': 4, 'e': 5, 'f': 6} @@ -48,6 +51,7 @@ def test_map(map, num_cpus): @pytest.mark.parametrize('map', [ pytest.param(parallel_map, id='parallel_map'), pytest.param(loky_pmap, id='loky_pmap'), + pytest.param(mpi_pmap, id='mpi_pmap'), pytest.param(serial_map, id='serial_map'), ]) @pytest.mark.parametrize('num_cpus', @@ -55,7 +59,10 @@ def test_map(map, num_cpus): ids=['1', '2']) def test_map_accumulator(map, num_cpus): if map is loky_pmap: - loky = pytest.importorskip("loky") + pytest.importorskip("loky") + if map is mpi_pmap: + pytest.importorskip("mpi4py") + args = (1, 2, 3) kwargs = {'d': 4, 'e': 5, 'f': 6} map_kw = { @@ -84,11 +91,14 @@ def func(i): @pytest.mark.parametrize('map', [ pytest.param(parallel_map, id='parallel_map'), pytest.param(loky_pmap, id='loky_pmap'), + pytest.param(mpi_pmap, id='mpi_pmap'), pytest.param(serial_map, id='serial_map'), ]) def test_map_pass_error(map): if map is loky_pmap: - loky = pytest.importorskip("loky") + pytest.importorskip("loky") + if map is mpi_pmap: + pytest.importorskip("mpi4py") with pytest.raises(CustomException) as err: map(func, range(10)) @@ -98,11 +108,14 @@ def test_map_pass_error(map): @pytest.mark.parametrize('map', [ pytest.param(parallel_map, id='parallel_map'), pytest.param(loky_pmap, id='loky_pmap'), + pytest.param(mpi_pmap, id='mpi_pmap'), pytest.param(serial_map, id='serial_map'), ]) def test_map_store_error(map): if map is loky_pmap: - loky = pytest.importorskip("loky") + pytest.importorskip("loky") + if map is mpi_pmap: + pytest.importorskip("mpi4py") with pytest.raises(MapExceptions) as err: map(func, range(10), map_kw={"fail_fast": False}) @@ -122,11 +135,14 @@ def test_map_store_error(map): @pytest.mark.parametrize('map', [ pytest.param(parallel_map, id='parallel_map'), pytest.param(loky_pmap, id='loky_pmap'), + pytest.param(mpi_pmap, id='mpi_pmap'), pytest.param(serial_map, id='serial_map'), ]) def test_map_early_end(map): if map is loky_pmap: - loky = pytest.importorskip("loky") + pytest.importorskip("loky") + if map is mpi_pmap: + pytest.importorskip("mpi4py") results = [] From f8ac0d19d1eedb1f519f70610c7fa44d2a893381 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Thu, 11 Jan 2024 14:26:25 -0500 Subject: [PATCH 08/66] Remove bloch3d --- doc/QuTiP_tree_plot/qutip-structure.py | 2 +- qutip/__init__.py | 1 - qutip/bloch3d.py | 516 ------------------------- 3 files changed, 1 insertion(+), 518 deletions(-) delete mode 100644 qutip/bloch3d.py diff --git a/doc/QuTiP_tree_plot/qutip-structure.py b/doc/QuTiP_tree_plot/qutip-structure.py index 7234598fba..b112303b0f 100755 --- a/doc/QuTiP_tree_plot/qutip-structure.py +++ b/doc/QuTiP_tree_plot/qutip-structure.py @@ -37,7 +37,7 @@ ("#043c6b", {"settings", "configrc", "solver"}), # Visualisation ("#3f8fd2", { - "bloch", "bloch3d", "sphereplot", "orbital", "visualization", "wigner", + "bloch", "sphereplot", "orbital", "visualization", "wigner", "distributions", "tomography", "topology", }), # Operators diff --git a/qutip/__init__.py b/qutip/__init__.py index ff8d76a7e8..19f373c8b1 100644 --- a/qutip/__init__.py +++ b/qutip/__init__.py @@ -39,7 +39,6 @@ from .bloch import * from .visualization import * from .animation import * -from .bloch3d import * from .matplotlib_utilities import * # library functions diff --git a/qutip/bloch3d.py b/qutip/bloch3d.py deleted file mode 100644 index 160b9662e1..0000000000 --- a/qutip/bloch3d.py +++ /dev/null @@ -1,516 +0,0 @@ -__all__ = ['Bloch3d'] - -import numpy as np -from . import Qobj, expect, sigmax, sigmay, sigmaz - - -class Bloch3d: - """Class for plotting data on a 3D Bloch sphere using mayavi. - Valid data can be either points, vectors, or qobj objects - corresponding to state vectors or density matrices. for - a two-state system (or subsystem). - - Attributes - ---------- - fig : instance {None} - User supplied Matplotlib Figure instance for plotting Bloch sphere. - font_color : str {'black'} - Color of font used for Bloch sphere labels. - font_scale : float {0.08} - Scale for font used for Bloch sphere labels. - frame : bool {True} - Draw frame for Bloch sphere - frame_alpha : float {0.05} - Sets transparency of Bloch sphere frame. - frame_color : str {'gray'} - Color of sphere wireframe. - frame_num : int {8} - Number of frame elements to draw. - frame_radius : floats {0.005} - Width of wireframe. - point_color : list {['r', 'g', 'b', 'y']} - List of colors for Bloch sphere point markers to cycle through. - i.e. By default, points 0 and 4 will both be blue ('r'). - point_mode : string {'sphere','cone','cube','cylinder','point'} - Point marker shapes. - point_size : float {0.075} - Size of points on Bloch sphere. - sphere_alpha : float {0.1} - Transparency of Bloch sphere itself. - sphere_color : str {'#808080'} - Color of Bloch sphere. - size : list {[500, 500]} - Size of Bloch sphere plot in pixels. Best to have both numbers the same - otherwise you will have a Bloch sphere that looks like a football. - vector_color : list {['r', 'g', 'b', 'y']} - List of vector colors to cycle through. - vector_width : int {3} - Width of displayed vectors. - view : list {[45,65]} - Azimuthal and Elevation viewing angles. - xlabel : list {['\|x>', '']} - List of strings corresponding to +x and -x axes labels, respectively. - xlpos : list {[1.07,-1.07]} - Positions of +x and -x labels respectively. - ylabel : list {['\|y>', '']} - List of strings corresponding to +y and -y axes labels, respectively. - ylpos : list {[1.07,-1.07]} - Positions of +y and -y labels respectively. - zlabel : list {["\|0>", '\|1>']} - List of strings corresponding to +z and -z axes labels, respectively. - zlpos : list {[1.07,-1.07]} - Positions of +z and -z labels respectively. - - Notes - ----- - The use of mayavi for 3D rendering of the Bloch sphere comes with - a few limitations: I) You can not embed a Bloch3d figure into a - matplotlib window. II) The use of LaTex is not supported by the - mayavi rendering engine. Therefore all labels must be defined using - standard text. Of course you can post-process the generated figures - later to add LaTeX using other software if needed. - - """ - def __init__(self, fig=None): - # ----check for mayavi----- - try: - from mayavi import mlab - except: - raise Exception("This function requires the mayavi module.") - - # ---Image options--- - self.fig = None - self.user_fig = None - # check if user specified figure or axes. - if fig: - self.user_fig = fig - # The size of the figure in inches, default = [500,500]. - self.size = [500, 500] - # Azimuthal and Elvation viewing angles, default = [45,65]. - self.view = [45, 65] - # Image background color - self.bgcolor = 'white' - # Image foreground color. Other options can override. - self.fgcolor = 'black' - - # ---Sphere options--- - # Color of Bloch sphere, default = #808080 - self.sphere_color = '#808080' - # Transparency of Bloch sphere, default = 0.1 - self.sphere_alpha = 0.1 - - # ---Frame options--- - # Draw frame? - self.frame = True - # number of lines to draw for frame - self.frame_num = 8 - # Color of wireframe, default = 'gray' - self.frame_color = 'black' - # Transparency of wireframe, default = 0.2 - self.frame_alpha = 0.05 - # Radius of frame lines - self.frame_radius = 0.005 - - # --Axes--- - # Axes color - self.axes_color = 'black' - # Transparency of axes - self.axes_alpha = 0.4 - # Radius of axes lines - self.axes_radius = 0.005 - - # ---Labels--- - # Labels for x-axis (in LaTex), default = ['$x$',''] - self.xlabel = ['|x>', ''] - # Position of x-axis labels, default = [1.2,-1.2] - self.xlpos = [1.07, -1.07] - # Labels for y-axis (in LaTex), default = ['$y$',''] - self.ylabel = ['|y>', ''] - # Position of y-axis labels, default = [1.1,-1.1] - self.ylpos = [1.07, -1.07] - # Labels for z-axis - self.zlabel = ['|0>', '|1>'] - # Position of z-axis labels, default = [1.05,-1.05] - self.zlpos = [1.07, -1.07] - - # ---Font options--- - # Color of fonts, default = 'black' - self.font_color = 'black' - # Size of fonts, default = 20 - self.font_scale = 0.08 - - # ---Vector options--- - # Object used for representing vectors on Bloch sphere. - # List of colors for Bloch vectors, default = ['b','g','r','y'] - self.vector_color = ['r', 'g', 'b', 'y'] - # Width of Bloch vectors, default = 2 - self.vector_width = 2.0 - # Height of vector head - self.vector_head_height = 0.15 - # Radius of vector head - self.vector_head_radius = 0.075 - - # ---Point options--- - # List of colors for Bloch point markers, default = ['b','g','r','y'] - self.point_color = ['r', 'g', 'b', 'y'] - # Size of point markers - self.point_size = 0.06 - # Shape of point markers - # Options: 'cone' or 'cube' or 'cylinder' or 'point' or 'sphere'. - # Default = 'sphere' - self.point_mode = 'sphere' - - # ---Data lists--- - # Data for point markers - self.points = [] - # Data for Bloch vectors - self.vectors = [] - # Number of times sphere has been saved - self.savenum = 0 - # Style of points, 'm' for multiple colors, 's' for single color - self.point_style = [] - # Transparency of points - self.point_alpha = [] - # Transparency of vectors - self.vector_alpha = [] - - def __str__(self): - s = "" - s += "Bloch3D data:\n" - s += "-----------\n" - s += "Number of points: " + str(len(self.points)) + "\n" - s += "Number of vectors: " + str(len(self.vectors)) + "\n" - s += "\n" - s += "Bloch3D sphere properties:\n" - s += "--------------------------\n" - s += "axes_alpha: " + str(self.axes_alpha) + "\n" - s += "axes_color: " + str(self.axes_color) + "\n" - s += "axes_radius: " + str(self.axes_radius) + "\n" - s += "bgcolor: " + str(self.bgcolor) + "\n" - s += "fgcolor: " + str(self.fgcolor) + "\n" - s += "font_color: " + str(self.font_color) + "\n" - s += "font_scale: " + str(self.font_scale) + "\n" - s += "frame: " + str(self.frame) + "\n" - s += "frame_alpha: " + str(self.frame_alpha) + "\n" - s += "frame_color: " + str(self.frame_color) + "\n" - s += "frame_num: " + str(self.frame_num) + "\n" - s += "frame_radius: " + str(self.frame_radius) + "\n" - s += "point_color: " + str(self.point_color) + "\n" - s += "point_mode: " + str(self.point_mode) + "\n" - s += "point_size: " + str(self.point_size) + "\n" - s += "sphere_alpha: " + str(self.sphere_alpha) + "\n" - s += "sphere_color: " + str(self.sphere_color) + "\n" - s += "size: " + str(self.size) + "\n" - s += "vector_color: " + str(self.vector_color) + "\n" - s += "vector_width: " + str(self.vector_width) + "\n" - s += "vector_head_height: " + str(self.vector_head_height) + "\n" - s += "vector_head_radius: " + str(self.vector_head_radius) + "\n" - s += "view: " + str(self.view) + "\n" - s += "xlabel: " + str(self.xlabel) + "\n" - s += "xlpos: " + str(self.xlpos) + "\n" - s += "ylabel: " + str(self.ylabel) + "\n" - s += "ylpos: " + str(self.ylpos) + "\n" - s += "zlabel: " + str(self.zlabel) + "\n" - s += "zlpos: " + str(self.zlpos) + "\n" - return s - - def clear(self): - """Resets the Bloch sphere data sets to empty. - """ - self.points = [] - self.vectors = [] - self.point_style = [] - - def add_points(self, points, meth='s', alpha=1.0): - """Add a list of data points to bloch sphere. - - Parameters - ---------- - points : array/list - Collection of data points. - - meth : str {'s','m'} - Type of points to plot, use 'm' for multicolored. - - alpha : float, default=1. - Transparency value for the vectors. Values between 0 and 1. - - """ - if not isinstance(points[0], (list, np.ndarray)): - points = [[points[0]], [points[1]], [points[2]]] - points = np.array(points) - if meth == 's': - if len(points[0]) == 1: - pnts = np.array( - [[points[0][0]], [points[1][0]], [points[2][0]]]) - pnts = np.append(pnts, points, axis=1) - else: - pnts = points - self.points.append(pnts) - self.point_style.append('s') - else: - self.points.append(points) - self.point_style.append('m') - self.point_alpha.append(alpha) - - def add_states(self, state, kind='vector', alpha=1.0): - """Add a state vector Qobj to Bloch sphere. - - Parameters - ---------- - state : qobj - Input state vector. - - kind : str {'vector','point'} - Type of object to plot. - - alpha : float, default=1. - Transparency value for the vectors. Values between 0 and 1. - """ - if isinstance(state, Qobj): - state = [state] - for st in state: - if kind == 'vector': - vec = [expect(sigmax(), st), expect(sigmay(), st), - expect(sigmaz(), st)] - self.add_vectors(vec, alpha=alpha) - elif kind == 'point': - pnt = [expect(sigmax(), st), expect(sigmay(), st), - expect(sigmaz(), st)] - self.add_points(pnt, alpha=alpha) - - def add_vectors(self, vectors, alpha=1.0): - """Add a list of vectors to Bloch sphere. - - Parameters - ---------- - vectors : array/list - Array with vectors of unit length or smaller. - - alpha : float, default=1. - Transparency value for the vectors. Values between 0 and 1. - - """ - if isinstance(vectors[0], (list, np.ndarray)): - for vec in vectors: - self.vectors.append(vec) - self.vector_alpha.append(alpha) - else: - self.vectors.append(vectors) - self.vector_alpha.append(alpha) - - def plot_vectors(self): - """ - Plots vectors on the Bloch sphere. - """ - from mayavi import mlab - from tvtk.api import tvtk - import matplotlib.colors as colors - ii = 0 - for k in range(len(self.vectors)): - vec = np.array(self.vectors[k]) - norm = np.linalg.norm(vec) - theta = np.arccos(vec[2] / norm) - phi = np.arctan2(vec[1], vec[0]) - vec -= 0.5 * self.vector_head_height * \ - np.array([np.sin(theta) * np.cos(phi), - np.sin(theta) * np.sin(phi), np.cos(theta)]) - - color = colors.colorConverter.to_rgb( - self.vector_color[np.mod(k, len(self.vector_color))]) - - mlab.plot3d([0, vec[0]], [0, vec[1]], [0, vec[2]], - name='vector' + str(ii), tube_sides=100, - line_width=self.vector_width, - opacity=self.vector_alpha[k], - color=color) - - cone = tvtk.ConeSource(height=self.vector_head_height, - radius=self.vector_head_radius, - resolution=100) - cone_mapper = tvtk.PolyDataMapper( - input_connection=cone.output_port) - prop = tvtk.Property(opacity=self.vector_alpha[k], color=color) - cc = tvtk.Actor(mapper=cone_mapper, property=prop) - cc.rotate_z(np.degrees(phi)) - cc.rotate_y(-90 + np.degrees(theta)) - cc.position = vec - self.fig.scene.add_actor(cc) - - def plot_points(self): - """ - Plots points on the Bloch sphere. - """ - from mayavi import mlab - import matplotlib.colors as colors - for k in range(len(self.points)): - num = len(self.points[k][0]) - dist = [np.sqrt(self.points[k][0][j] ** 2 + - self.points[k][1][j] ** 2 + - self.points[k][2][j] ** 2) for j in range(num)] - if any(abs(dist - dist[0]) / dist[0] > 1e-12): - # combine arrays so that they can be sorted together - # and sort rates from lowest to highest - zipped = sorted(zip(dist, range(num))) - dist, indperm = zip(*zipped) - indperm = np.array(indperm) - else: - indperm = range(num) - if self.point_style[k] == 's': - color = colors.colorConverter.to_rgb( - self.point_color[np.mod(k, len(self.point_color))]) - mlab.points3d( - self.points[k][0][indperm], self.points[k][1][indperm], - self.points[k][2][indperm], figure=self.fig, - resolution=100, scale_factor=self.point_size, - mode=self.point_mode, color=color, - opacity=self.point_alpha[k]) - - elif self.point_style[k] == 'm': - pnt_colors = np.array(self.point_color * np.ceil( - num / float(len(self.point_color)))) - pnt_colors = pnt_colors[0:num] - pnt_colors = list(pnt_colors[indperm]) - for kk in range(num): - mlab.points3d( - self.points[k][0][ - indperm[kk]], self.points[k][1][indperm[kk]], - self.points[k][2][ - indperm[kk]], figure=self.fig, resolution=100, - scale_factor=self.point_size, mode=self.point_mode, - color=colors.colorConverter.to_rgb(pnt_colors[kk]), - opacity=self.point_alpha[k]) - - def make_sphere(self): - """ - Plots Bloch sphere and data sets. - """ - # setup plot - # Figure instance for Bloch sphere plot - from mayavi import mlab - import matplotlib.colors as colors - if self.user_fig: - self.fig = self.user_fig - else: - self.fig = mlab.figure( - 1, size=self.size, - bgcolor=colors.colorConverter.to_rgb(self.bgcolor), - fgcolor=colors.colorConverter.to_rgb(self.fgcolor)) - - sphere = mlab.points3d( - 0, 0, 0, figure=self.fig, scale_mode='none', scale_factor=2, - color=colors.colorConverter.to_rgb(self.sphere_color), - resolution=100, opacity=self.sphere_alpha, name='bloch_sphere') - - # Thse commands make the sphere look better - sphere.actor.property.specular = 0.45 - sphere.actor.property.specular_power = 5 - sphere.actor.property.backface_culling = True - - # make frame for sphere surface - if self.frame: - theta = np.linspace(0, 2 * np.pi, 100) - for angle in np.linspace(-np.pi, np.pi, self.frame_num): - xlat = np.cos(theta) * np.cos(angle) - ylat = np.sin(theta) * np.cos(angle) - zlat = np.ones_like(theta) * np.sin(angle) - xlon = np.sin(angle) * np.sin(theta) - ylon = np.cos(angle) * np.sin(theta) - zlon = np.cos(theta) - mlab.plot3d( - xlat, ylat, zlat, - color=colors.colorConverter.to_rgb(self.frame_color), - opacity=self.frame_alpha, tube_radius=self.frame_radius) - mlab.plot3d( - xlon, ylon, zlon, - color=colors.colorConverter.to_rgb(self.frame_color), - opacity=self.frame_alpha, tube_radius=self.frame_radius) - - # add axes - axis = np.linspace(-1.0, 1.0, 10) - other = np.zeros_like(axis) - mlab.plot3d( - axis, other, other, - color=colors.colorConverter.to_rgb(self.axes_color), - tube_radius=self.axes_radius, opacity=self.axes_alpha) - mlab.plot3d( - other, axis, other, - color=colors.colorConverter.to_rgb(self.axes_color), - tube_radius=self.axes_radius, opacity=self.axes_alpha) - mlab.plot3d( - other, other, axis, - color=colors.colorConverter.to_rgb(self.axes_color), - tube_radius=self.axes_radius, opacity=self.axes_alpha) - - # add data to sphere - self.plot_points() - self.plot_vectors() - - # #add labels - mlab.text3d(0, 0, self.zlpos[0], self.zlabel[0], - color=colors.colorConverter.to_rgb(self.font_color), - scale=self.font_scale) - mlab.text3d(0, 0, self.zlpos[1], self.zlabel[1], - color=colors.colorConverter.to_rgb(self.font_color), - scale=self.font_scale) - mlab.text3d(self.xlpos[0], 0, 0, self.xlabel[0], - color=colors.colorConverter.to_rgb(self.font_color), - scale=self.font_scale) - mlab.text3d(self.xlpos[1], 0, 0, self.xlabel[1], - color=colors.colorConverter.to_rgb(self.font_color), - scale=self.font_scale) - mlab.text3d(0, self.ylpos[0], 0, self.ylabel[0], - color=colors.colorConverter.to_rgb(self.font_color), - scale=self.font_scale) - mlab.text3d(0, self.ylpos[1], 0, self.ylabel[1], - color=colors.colorConverter.to_rgb(self.font_color), - scale=self.font_scale) - - def show(self): - """ - Display the Bloch sphere and corresponding data sets. - """ - from mayavi import mlab - self.make_sphere() - mlab.view(azimuth=self.view[0], elevation=self.view[1], distance=5) - if self.fig: - mlab.show() - - def save(self, name=None, format='png', dirc=None): - """Saves Bloch sphere to file of type ``format`` in directory ``dirc``. - - Parameters - ---------- - name : str - Name of saved image. Must include path and format as well. - i.e. '/Users/Me/Desktop/bloch.png' - This overrides the 'format' and 'dirc' arguments. - format : str - Format of output image. Default is 'png'. - dirc : str - Directory for output images. Defaults to current working directory. - - Returns - ------- - File containing plot of Bloch sphere. - - """ - from mayavi import mlab - import os - self.make_sphere() - mlab.view(azimuth=self.view[0], elevation=self.view[1], distance=5) - if dirc: - if not os.path.isdir(os.getcwd() + "/" + str(dirc)): - os.makedirs(os.getcwd() + "/" + str(dirc)) - if name is None: - if dirc: - mlab.savefig(os.getcwd() + "/" + str(dirc) + '/bloch_' + - str(self.savenum) + '.' + format) - else: - mlab.savefig(os.getcwd() + '/bloch_' + str(self.savenum) + - '.' + format) - else: - mlab.savefig(name) - self.savenum += 1 - if self.fig: - mlab.close(self.fig) From 2d0556345b7b36c131864bce0080c460455d7eb4 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Fri, 12 Jan 2024 17:45:11 -0500 Subject: [PATCH 09/66] Faster Qobj.matmul --- qutip/core/dimensions.py | 19 +++++++++++++++++++ qutip/core/qobj.py | 41 ++++++++++++++++------------------------ 2 files changed, 35 insertions(+), 25 deletions(-) diff --git a/qutip/core/dimensions.py b/qutip/core/dimensions.py index 7e0f17cd57..7e7c4538d1 100644 --- a/qutip/core/dimensions.py +++ b/qutip/core/dimensions.py @@ -783,6 +783,25 @@ def __eq__(self, other): ) return NotImplemented + def __ne__(self, other): + if isinstance(other, Dimensions): + return not ( + self is other + or ( + self.to_ == other.to_ + and self.from_ == other.from_ + ) + ) + return NotImplemented + + def __matmul__(self, other): + if self.from_ != other.to_: + raise TypeError(f"incompatible dimensions {self} and {other}") + args = other.from_, self.to_ + if args in Dimensions._stored_dims: + return Dimensions._stored_dims[args] + return Dimensions(*args) + def __hash__(self): return hash((self.to_, self.from_)) diff --git a/qutip/core/qobj.py b/qutip/core/qobj.py index d42e08b9bc..9c1692df4d 100644 --- a/qutip/core/qobj.py +++ b/qutip/core/qobj.py @@ -377,12 +377,11 @@ def to(self, data_type): converter = _data.to[data_type] except (KeyError, TypeError): raise ValueError("Unknown conversion type: " + str(data_type)) - if type(self.data) is data_type: + if type(self._data) is data_type: return self return Qobj(converter(self._data), dims=self._dims, isherm=self._isherm, - superrep=self.superrep, isunitary=self._isunitary, copy=False) @@ -441,7 +440,6 @@ def __mul__(self, other): return Qobj(out, dims=self._dims, isherm=isherm, - superrep=self.superrep, isunitary=isunitary, copy=False) @@ -456,23 +454,16 @@ def __matmul__(self, other): other = Qobj(other) except TypeError: return NotImplemented - if self._dims[1] != other._dims[0]: - raise TypeError("".join([ - "incompatible dimensions ", - repr(self.dims), - " and ", - repr(other.dims), - ])) - if ( - (self.isbra and other.isket) - or (self.isoperbra and other.isoperket) - ): - return _data.inner(self.data, other.data) + new_dims = self._dims @ other._dims + if new_dims.type == 'scalar': + return _data.inner(self._data, other._data) - return Qobj(_data.matmul(self.data, other.data), - dims=Dimensions(other._dims[1], self._dims[0]), - isunitary=self._isunitary and other._isunitary, - copy=False) + return Qobj( + _data.matmul(self._data, other._data), + dims=new_dims, + isunitary=self._isunitary and other._isunitary, + copy=False + ) def __truediv__(self, other): return self.__mul__(1 / other) @@ -526,7 +517,7 @@ def __pow__(self, n, m=None): # calculates powers of Qobj def _str_header(self): out = ", ".join([ "Quantum object: dims=" + str(self.dims), - "shape=" + str(self.data.shape), + "shape=" + str(self._data.shape), "type=" + repr(self.type), ]) if self.type in ('oper', 'super'): @@ -701,7 +692,7 @@ def norm(self, norm=None, kwargs=None): "vector norm must be in " + repr(_NORM_ALLOWED_VECTOR) ) kwargs = kwargs or {} - return _NORM_FUNCTION_LOOKUP[norm](self.data, **kwargs) + return _NORM_FUNCTION_LOOKUP[norm](self._data, **kwargs) def proj(self): """Form the projector from a given ket or bra vector. @@ -755,8 +746,8 @@ def purity(self): if self.type in ("super", "operator-ket", "operator-bra"): raise TypeError('purity is only defined for states.') if self.isket or self.isbra: - return _data.norm.l2(self.data)**2 - return _data.trace(_data.matmul(self.data, self.data)).real + return _data.norm.l2(self._data)**2 + return _data.trace(_data.matmul(self._data, self._data)).real def full(self, order='C', squeeze=False): """Dense array from quantum object. @@ -794,7 +785,7 @@ def data_as(self, format=None, copy=True): data : numpy.ndarray, scipy.sparse.matrix_csr, etc. Matrix in the type of the underlying libraries. """ - return _data.extract(self.data, format, copy) + return _data.extract(self._data, format, copy) def diag(self): """Diagonal elements of quantum object. @@ -1733,7 +1724,7 @@ def isunitary(self): return self._isunitary @property - def shape(self): return self.data.shape + def shape(self): return self._data.shape isbra = property(isbra) isket = property(isket) From e377795a7e83e888e939e210808dfdfb8f21d095 Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Tue, 16 Jan 2024 13:40:18 +0900 Subject: [PATCH 10/66] Fix mistake made in merge --- qutip/solver/parallel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutip/solver/parallel.py b/qutip/solver/parallel.py index 5a64468963..50e0df3977 100644 --- a/qutip/solver/parallel.py +++ b/qutip/solver/parallel.py @@ -188,7 +188,7 @@ def _done_callback(future): if isinstance(ex, Exception): errors[future._i] = ex else: - result = future.result() + result = extract_result(future._i, future) remaining_ntraj = result_func(future._i, result) if remaining_ntraj is not None and remaining_ntraj <= 0: finished.append(True) From 86605cc187c6f7e78f2902582049d33eb53d39ef Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Tue, 16 Jan 2024 14:53:01 +0900 Subject: [PATCH 11/66] Fixed bug in handling ShutdownExecutorError --- qutip/solver/parallel.py | 102 +++++++++++++++++++++------------------ 1 file changed, 55 insertions(+), 47 deletions(-) diff --git a/qutip/solver/parallel.py b/qutip/solver/parallel.py index 50e0df3977..2d9a1aead5 100644 --- a/qutip/solver/parallel.py +++ b/qutip/solver/parallel.py @@ -3,7 +3,7 @@ mappings, using the builtin Python module multiprocessing or the loky parallel execution library. """ -__all__ = ['parallel_map', 'serial_map', 'loky_pmap'] +__all__ = ['parallel_map', 'serial_map', 'loky_pmap', 'mpi_pmap'] import multiprocessing import os @@ -131,21 +131,20 @@ def serial_map(task, values, task_args=None, task_kwargs=None, return results -def _generic_pmap(task, values, task_args, task_kwargs, - reduce_func, timeout, fail_fast, +def _generic_pmap(task, values, task_args, task_kwargs, reduce_func, + timeout, fail_fast, num_workers, progress_bar, progress_bar_kwargs, setup_executor, extract_result, shutdown_executor): """ Common functionality for parallel_map, loky_pmap and mpi_pmap. - The parameters `setup_executor`, `extract_value` and `destroy_executor` + The parameters `setup_executor`, `extract_result` and `shutdown_executor` are callback functions with the following signatures: - setup_executor: () -> ProcessPoolExecutor, int - The second return value specifies the number of workers + setup_executor: () -> ProcessPoolExecutor - extract_result: (index: int, future: Future) -> Any - index: Corresponds to the indices of the `values` list - future: The Future that has finished running + extract_result: Future -> (Any, BaseException) + If there was an exception e, returns (None, e). + Otherwise returns (result, None). shutdown_executor: (executor: ProcessPoolExecutor, active_tasks: set[Future]) -> None @@ -177,18 +176,17 @@ def result_func(_, value): def _done_callback(future): if not future.cancelled(): - ex = future.exception() - if isinstance(ex, KeyboardInterrupt): + result, exception = extract_result(future) + if isinstance(exception, KeyboardInterrupt): # When a keyboard interrupt happens, it is raised in the main # thread and in all worker threads. At this point in the code, # the worker threads have already returned and the main thread # is only waiting for the ProcessPoolExecutor to shutdown # before exiting. We therefore return immediately. return - if isinstance(ex, Exception): - errors[future._i] = ex + if isinstance(exception, Exception): + errors[future._i] = exception else: - result = extract_result(future._i, future) remaining_ntraj = result_func(future._i, result) if remaining_ntraj is not None and remaining_ntraj <= 0: finished.append(True) @@ -196,8 +194,7 @@ def _done_callback(future): os.environ['QUTIP_IN_PARALLEL'] = 'TRUE' try: - executor, num_workers = setup_executor() - with executor: + with setup_executor() as executor: waiting = set() i = 0 aborted = False @@ -317,24 +314,27 @@ def parallel_map(task, values, task_args=None, task_kwargs=None, ctx_kw = {} def setup_executor(): - num_workers = map_kw['num_cpus'] - executor = concurrent.futures.ProcessPoolExecutor( - max_workers=num_workers, **ctx_kw, + return concurrent.futures.ProcessPoolExecutor( + max_workers=map_kw['num_cpus'], **ctx_kw, ) - return executor, num_workers - def extract_result (_, future): - return future.result() + def extract_result (future: concurrent.futures.Future): + exception = future.exception() + if exception is not None: + return None, exception + return future.result(), None def shutdown_executor(executor, _): # Since `ProcessPoolExecutor` leaves no other option, # we wait for all worker processes to finish their current task executor.shutdown() - return _generic_pmap(task, values, task_args, task_kwargs, - reduce_func, map_kw['timeout'], map_kw['fail_fast'], - progress_bar, progress_bar_kwargs, - setup_executor, extract_result, shutdown_executor) + return _generic_pmap( + task, values, task_args, task_kwargs, reduce_func, + map_kw['timeout'], map_kw['fail_fast'], map_kw['num_cpus'], + progress_bar, progress_bar_kwargs, + setup_executor, extract_result, shutdown_executor + ) def loky_pmap(task, values, task_args=None, task_kwargs=None, @@ -390,26 +390,30 @@ def loky_pmap(task, values, task_args=None, task_kwargs=None, map_kw = _read_map_kw(map_kw) def setup_executor(): - num_workers = map_kw['num_cpus'] - executor = get_reusable_executor(max_workers=num_workers) - return executor, num_workers + return get_reusable_executor(max_workers=map_kw['num_cpus']) - def extract_result (_, future: concurrent.futures.Future): - if isinstance(future.exception(), ShutdownExecutorError): + def extract_result (future: concurrent.futures.Future): + exception = future.exception() + if isinstance(exception, ShutdownExecutorError): # Task was aborted due to timeout etc - return None - return future.result() + return None, None + if exception is not None: + return None, exception + return future.result(), None def shutdown_executor(executor, active_tasks): # If there are still tasks running, we kill all workers in order to # return immediately. Otherwise, `kill_workers` is set to False so # that the worker threads can be reused in subsequent loky_pmap calls. - executor.shutdown(kill_workers=(len(active_tasks) > 0)) - - return _generic_pmap(task, values, task_args, task_kwargs, - reduce_func, map_kw['timeout'], map_kw['fail_fast'], - progress_bar, progress_bar_kwargs, - setup_executor, extract_result, shutdown_executor) + kill_workers = len(active_tasks) > 0 + executor.shutdown(kill_workers=kill_workers) + + return _generic_pmap( + task, values, task_args, task_kwargs, reduce_func, + map_kw['timeout'], map_kw['fail_fast'], map_kw['num_cpus'], + progress_bar, progress_bar_kwargs, + setup_executor, extract_result, shutdown_executor + ) def mpi_pmap(task, values, task_args=None, task_kwargs=None, @@ -471,19 +475,23 @@ def mpi_pmap(task, values, task_args=None, task_kwargs=None, fail_fast = map_kw.pop('fail_fast') def setup_executor(): - executor = MPIPoolExecutor(max_workers=num_workers, **map_kw) - return executor, num_workers + return MPIPoolExecutor(max_workers=num_workers, **map_kw) - def extract_result (_, future): - return future.result() + def extract_result (future): + exception = future.exception() + if exception is not None: + return None, exception + return future.result(), None def shutdown_executor(executor, _): executor.shutdown() - return _generic_pmap(task, values, task_args, task_kwargs, - reduce_func, timeout, fail_fast, - progress_bar, progress_bar_kwargs, - setup_executor, extract_result, shutdown_executor) + return _generic_pmap( + task, values, task_args, task_kwargs, reduce_func, + timeout, fail_fast, num_workers, + progress_bar, progress_bar_kwargs, + setup_executor, extract_result, shutdown_executor + ) From cea831871aa6fbfd4a48a67fc9195a623ea4fd16 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Tue, 16 Jan 2024 12:12:18 -0500 Subject: [PATCH 12/66] Faster is_type_ proterties --- qutip/core/cy/qobjevo.pyx | 24 ++++++++++++++++++++++-- qutip/core/qobj.py | 35 +++++++++++++++++++++++++++++------ 2 files changed, 51 insertions(+), 8 deletions(-) diff --git a/qutip/core/cy/qobjevo.pyx b/qutip/core/cy/qobjevo.pyx index 67c5195c44..c2b4d5d3ae 100644 --- a/qutip/core/cy/qobjevo.pyx +++ b/qutip/core/cy/qobjevo.pyx @@ -908,12 +908,12 @@ cdef class QobjEvo: @property def isoper(self): """Indicates if the system represents an operator.""" - return self._dims.type == "oper" + return self._dims.type in ['oper', 'scalar'] @property def issuper(self): """Indicates if the system represents a superoperator.""" - return self._dims.issuper + return self._dims.type == 'super' @property def dims(self): @@ -927,6 +927,26 @@ cdef class QobjEvo: def superrep(self): return self._dims.superrep + @property + def isbra(self): + """Indicates if the system represents a bra state.""" + return self._dims.type in ['bra', 'scalar'] + + @property + def isket(self): + """Indicates if the system represents a ket state.""" + return self._dims.type in ['ket', 'scalar'] + + @property + def isoperket(self): + """Indicates if the system represents a operator-ket state.""" + return self._dims.type == 'operator-ket' + + @property + def isoperbra(self): + """Indicates if the system represents a operator-bra state.""" + return self._dims.type == 'operator-bra' + ########################################################################### # operation methods # ########################################################################### diff --git a/qutip/core/qobj.py b/qutip/core/qobj.py index 9c1692df4d..d545f25788 100644 --- a/qutip/core/qobj.py +++ b/qutip/core/qobj.py @@ -1726,12 +1726,35 @@ def isunitary(self): @property def shape(self): return self._data.shape - isbra = property(isbra) - isket = property(isket) - isoper = property(isoper) - issuper = property(issuper) - isoperbra = property(isoperbra) - isoperket = property(isoperket) + @property + def isoper(self): + """Indicates if the Qobj represents an operator.""" + return self._dims.type in ['oper', 'scalar'] + + @property + def isbra(self): + """Indicates if the Qobj represents a bra state.""" + return self._dims.type in ['bra', 'scalar'] + + @property + def isket(self): + """Indicates if the Qobj represents a ket state.""" + return self._dims.type in ['ket', 'scalar'] + + @property + def issuper(self): + """Indicates if the Qobj represents a superoperator.""" + return self._dims.type == 'super' + + @property + def isoperket(self): + """Indicates if the Qobj represents a operator-ket state.""" + return self._dims.type == 'operator-ket' + + @property + def isoperbra(self): + """Indicates if the Qobj represents a operator-bra state.""" + return self._dims.type == 'operator-bra' def ptrace(Q, sel): From 13ad4e63308ba4cb1878d73fbbb90664c2bc7d17 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Tue, 16 Jan 2024 12:50:06 -0500 Subject: [PATCH 13/66] Less Qobj.add overhead --- qutip/core/qobj.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/qutip/core/qobj.py b/qutip/core/qobj.py index d545f25788..45efd80d89 100644 --- a/qutip/core/qobj.py +++ b/qutip/core/qobj.py @@ -81,11 +81,18 @@ def _require_equal_type(method): """ @functools.wraps(method) def out(self, other): + if isinstance(other, Qobj): + if self._dims != other._dims: + msg = ( + "incompatible dimensions " + + repr(self.dims) + " and " + repr(other.dims) + ) + raise ValueError(msg) + return method(self, other) if other == 0: return method(self, other) if ( - self.type in ('oper', 'super') - and self._dims[0] == self._dims[1] + self._dims.issquare and isinstance(other, numbers.Number) ): scale = complex(other) @@ -95,17 +102,13 @@ def out(self, other): isherm=(scale.imag == 0), isunitary=(abs(abs(scale)-1) < settings.core['atol']), copy=False) - if not isinstance(other, Qobj): + else: try: + # This allow `Qobj + array` if the shape is good. + # Do we really want that? other = Qobj(other, dims=self._dims) except TypeError: return NotImplemented - if self._dims != other._dims: - msg = ( - "incompatible dimensions " - + repr(self.dims) + " and " + repr(other.dims) - ) - raise ValueError(msg) return method(self, other) return out @@ -389,7 +392,8 @@ def to(self, data_type): def __add__(self, other): if other == 0: return self.copy() - return Qobj(_data.add(self._data, other._data), + new_data = _data.add(self._data, other._data) + return Qobj(new_data, dims=self._dims, isherm=(self._isherm and other._isherm) or None, copy=False) From 29558e016bce3a9fd568bd1cee3df342d067270a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eric=20Gigu=C3=A8re?= Date: Wed, 17 Jan 2024 19:06:05 -0500 Subject: [PATCH 14/66] Update qutip/core/qobj.py Co-authored-by: Simon Cross --- qutip/core/qobj.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/qutip/core/qobj.py b/qutip/core/qobj.py index 45efd80d89..def8530ec8 100644 --- a/qutip/core/qobj.py +++ b/qutip/core/qobj.py @@ -1728,7 +1728,9 @@ def isunitary(self): return self._isunitary @property - def shape(self): return self._data.shape + def shape(self): + """Return the shape of the Qobj data.""" + return self._data.shape @property def isoper(self): From 4623f3ed5239e961236907d6ce5eb90ab0f9e67b Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Thu, 18 Jan 2024 17:19:44 +0900 Subject: [PATCH 15/66] Fixed bugs when adding MultiTrajResults --- qutip/solver/result.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/qutip/solver/result.py b/qutip/solver/result.py index b1d1bf2bbd..8c0d025513 100644 --- a/qutip/solver/result.py +++ b/qutip/solver/result.py @@ -829,6 +829,7 @@ def __add__(self, other): raise ValueError("Shared `times` are is required to merge results") new = self.__class__(self._raw_ops, self.options, solver=self.solver, stats=self.stats) + new.e_ops = self.e_ops if self.trajectories and other.trajectories: new.trajectories = self.trajectories + other.trajectories new.num_trajectories = self.num_trajectories + other.num_trajectories @@ -836,7 +837,8 @@ def __add__(self, other): new.seeds = self.seeds + other.seeds if self._sum_states is not None and other._sum_states is not None: - new._sum_states = self._sum_states + other._sum_states + new._sum_states = [state1 + state2 for state1, state2 + in zip(self._sum_states, other._sum_states)] if ( self._sum_final_states is not None From af32f38d60eecb9d4fe348c416b62d30e300cb72 Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Thu, 18 Jan 2024 17:37:39 +0900 Subject: [PATCH 16/66] Small fix --- qutip/solver/multitraj.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/qutip/solver/multitraj.py b/qutip/solver/multitraj.py index 679bffab3d..4b600434df 100644 --- a/qutip/solver/multitraj.py +++ b/qutip/solver/multitraj.py @@ -144,7 +144,9 @@ def _initialize_run(self, state, ntraj=1, args=None, e_ops=(), result.add_end_condition(ntraj, target_tol) map_func = _get_map[self.options['map']] - map_kw = self.options['mpi_options'] if map_func == mpi_pmap else {} + map_kw = {} + if map_func == mpi_pmap: + map_kw.update(self.options['mpi_options']) map_kw.update({ 'timeout': timeout, 'num_cpus': self.options['num_cpus'], From e911319377d000b143a7af2a5bcc25c6d186706f Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Thu, 18 Jan 2024 18:41:25 +0900 Subject: [PATCH 17/66] Formatting, small bug in parallel.py --- qutip/solver/nm_mcsolve.py | 4 ++-- qutip/solver/parallel.py | 22 ++++++++++++---------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/qutip/solver/nm_mcsolve.py b/qutip/solver/nm_mcsolve.py index a8df2b93ce..a328713c4a 100644 --- a/qutip/solver/nm_mcsolve.py +++ b/qutip/solver/nm_mcsolve.py @@ -544,8 +544,8 @@ def options(self): progress_bar: str {'text', 'enhanced', 'tqdm', ''}, default: "text" How to present the solver progress. - 'tqdm' uses the python module of the same name and raise an error if - not installed. Empty string or False will disable the bar. + 'tqdm' uses the python module of the same name and raise an error + if not installed. Empty string or False will disable the bar. progress_kwargs: dict, default: {"chunk_size":10} Arguments to pass to the progress_bar. Qutip's bars use diff --git a/qutip/solver/parallel.py b/qutip/solver/parallel.py index 2d9a1aead5..fd0956c125 100644 --- a/qutip/solver/parallel.py +++ b/qutip/solver/parallel.py @@ -139,7 +139,7 @@ def _generic_pmap(task, values, task_args, task_kwargs, reduce_func, Common functionality for parallel_map, loky_pmap and mpi_pmap. The parameters `setup_executor`, `extract_result` and `shutdown_executor` are callback functions with the following signatures: - + setup_executor: () -> ProcessPoolExecutor extract_result: Future -> (Any, BaseException) @@ -184,8 +184,11 @@ def _done_callback(future): # is only waiting for the ProcessPoolExecutor to shutdown # before exiting. We therefore return immediately. return - if isinstance(exception, Exception): - errors[future._i] = exception + if exception is not None: + if isinstance(exception, Exception): + errors[future._i] = exception + else: + raise exception else: remaining_ntraj = result_func(future._i, result) if remaining_ntraj is not None and remaining_ntraj <= 0: @@ -317,8 +320,8 @@ def setup_executor(): return concurrent.futures.ProcessPoolExecutor( max_workers=map_kw['num_cpus'], **ctx_kw, ) - - def extract_result (future: concurrent.futures.Future): + + def extract_result(future: concurrent.futures.Future): exception = future.exception() if exception is not None: return None, exception @@ -391,8 +394,8 @@ def loky_pmap(task, values, task_args=None, task_kwargs=None, def setup_executor(): return get_reusable_executor(max_workers=map_kw['num_cpus']) - - def extract_result (future: concurrent.futures.Future): + + def extract_result(future: concurrent.futures.Future): exception = future.exception() if isinstance(exception, ShutdownExecutorError): # Task was aborted due to timeout etc @@ -476,8 +479,8 @@ def mpi_pmap(task, values, task_args=None, task_kwargs=None, def setup_executor(): return MPIPoolExecutor(max_workers=num_workers, **map_kw) - - def extract_result (future): + + def extract_result(future): exception = future.exception() if exception is not None: return None, exception @@ -494,7 +497,6 @@ def shutdown_executor(executor, _): ) - _get_map = { "parallel_map": parallel_map, "parallel": parallel_map, From 35bfdb0bd4e3e11cc1f87b32cb37bdafb9cb6b01 Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Thu, 18 Jan 2024 18:46:11 +0900 Subject: [PATCH 18/66] Towncrier entry --- doc/changes/2296.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/changes/2296.feature diff --git a/doc/changes/2296.feature b/doc/changes/2296.feature new file mode 100644 index 0000000000..8fed655652 --- /dev/null +++ b/doc/changes/2296.feature @@ -0,0 +1 @@ +Added mpi_pmap, which uses the mpi4py module to run computations in parallel through the MPI interface. \ No newline at end of file From 3c1393f2661e3a98d776c0c363b9e71af7dcbf8f Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Thu, 18 Jan 2024 12:27:12 -0500 Subject: [PATCH 19/66] Add test for Dimensions _eq_, _ne_, _matmul_ --- qutip/core/qobj.py | 19 +++++-------------- qutip/tests/core/test_dimensions.py | 27 +++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/qutip/core/qobj.py b/qutip/core/qobj.py index def8530ec8..bdc72ca67e 100644 --- a/qutip/core/qobj.py +++ b/qutip/core/qobj.py @@ -91,10 +91,7 @@ def out(self, other): return method(self, other) if other == 0: return method(self, other) - if ( - self._dims.issquare - and isinstance(other, numbers.Number) - ): + if self._dims.issquare and isinstance(other, numbers.Number): scale = complex(other) other = Qobj(_data.identity(self.shape[0], scale, dtype=type(self.data)), @@ -102,14 +99,9 @@ def out(self, other): isherm=(scale.imag == 0), isunitary=(abs(abs(scale)-1) < settings.core['atol']), copy=False) - else: - try: - # This allow `Qobj + array` if the shape is good. - # Do we really want that? - other = Qobj(other, dims=self._dims) - except TypeError: - return NotImplemented - return method(self, other) + return method(self, other) + return NotImplemented + return out @@ -392,8 +384,7 @@ def to(self, data_type): def __add__(self, other): if other == 0: return self.copy() - new_data = _data.add(self._data, other._data) - return Qobj(new_data, + return Qobj(_data.add(self._data, other._data), dims=self._dims, isherm=(self._isherm and other._isherm) or None, copy=False) diff --git a/qutip/tests/core/test_dimensions.py b/qutip/tests/core/test_dimensions.py index 0686e28b71..7be9d27600 100644 --- a/qutip/tests/core/test_dimensions.py +++ b/qutip/tests/core/test_dimensions.py @@ -162,3 +162,30 @@ def test_super(self, base, expected): def test_bad_dims(dims_list): with pytest.raises(ValueError): Dimensions([dims_list, [1]]) + + +@pytest.mark.parametrize("space_l", [[1], [2], [2, 3]]) +@pytest.mark.parametrize("space_m", [[1], [2], [2, 3]]) +@pytest.mark.parametrize("space_r", [[1], [2], [2, 3]]) +def test_dims_matmul(space_l, space_m, space_r): + dims_l = Dimensions([space_l, space_m]) + dims_r = Dimensions([space_m, space_r]) + assert dims_l @ dims_r == Dimensions([space_l, space_r]) + + +def test_dims_matmul_bad(): + dims_l = Dimensions([[1], [3]]) + dims_r = Dimensions([[2], [2]]) + with pytest.raises(TypeError): + dims_l @ dims_r + + +def test_dims_comparison(): + assert Dimensions([[1], [2]]) == Dimensions([[1], [2]]) + assert not Dimensions([[1], [2]]) != Dimensions([[1], [2]]) + assert Dimensions([[1], [2]]) != Dimensions([[2], [1]]) + assert not Dimensions([[1], [2]]) == Dimensions([[2], [1]]) + assert Dimensions([[1], [2]])[1] == Dimensions([[1], [2]])[1] + assert Dimensions([[1], [2]])[0] != Dimensions([[1], [2]])[1] + assert not Dimensions([[1], [2]])[1] != Dimensions([[1], [2]])[1] + assert not Dimensions([[1], [2]])[0] != Dimensions([[1], [2]])[0] From 07798bd6677fddea0dd9b345b0b254f773e86754 Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Mon, 22 Jan 2024 14:39:46 +0900 Subject: [PATCH 20/66] Added mpi4py to setup.cfg --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index d5373c9c85..2bde0b8619 100644 --- a/setup.cfg +++ b/setup.cfg @@ -60,6 +60,7 @@ ipython = ipython extras = loky + mpi4py tqdm ; This uses ConfigParser's string interpolation to include all the above ; dependencies into one single target, convenient for testing full builds. From 57cca54994d2b70c9c99761b4726d1676c430b9e Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Mon, 22 Jan 2024 14:44:57 +0900 Subject: [PATCH 21/66] Explicit list of solver options --- qutip/solver/mcsolve.py | 11 +++++++++-- qutip/solver/nm_mcsolve.py | 16 ++++++++++++++-- qutip/solver/stochastic.py | 37 ++++++++++++++++++++++++++++++++++--- 3 files changed, 57 insertions(+), 7 deletions(-) diff --git a/qutip/solver/mcsolve.py b/qutip/solver/mcsolve.py index adb2677e3e..72b049553c 100644 --- a/qutip/solver/mcsolve.py +++ b/qutip/solver/mcsolve.py @@ -417,7 +417,15 @@ class MCSolver(MultiTrajSolver): _trajectory_resultclass = McTrajectoryResult _mc_integrator_class = MCIntegrator solver_options = { - **MultiTrajSolver.solver_options, + "progress_bar": "text", + "progress_kwargs": {"chunk_size": 10}, + "store_final_state": False, + "store_states": None, + "keep_runs_results": False, + "map": "serial", + "mpi_options": {}, + "num_cpus": None, + "bitgenerator": None, "method": "adams", "mc_corr_eps": 1e-10, "norm_steps": 5, @@ -425,7 +433,6 @@ class MCSolver(MultiTrajSolver): "norm_tol": 1e-4, "improved_sampling": False, } - del solver_options["normalize_output"] def __init__(self, H, c_ops, *, options=None): _time_start = time() diff --git a/qutip/solver/nm_mcsolve.py b/qutip/solver/nm_mcsolve.py index a328713c4a..31e7cbd4a4 100644 --- a/qutip/solver/nm_mcsolve.py +++ b/qutip/solver/nm_mcsolve.py @@ -331,12 +331,24 @@ class NonMarkovianMCSolver(MCSolver): name = "nm_mcsolve" _resultclass = NmmcResult solver_options = { - **MCSolver.solver_options, + "progress_bar": "text", + "progress_kwargs": {"chunk_size": 10}, + "store_final_state": False, + "store_states": None, + "keep_runs_results": False, + "map": "serial", + "mpi_options": {}, + "num_cpus": None, + "bitgenerator": None, + "method": "adams", + "mc_corr_eps": 1e-10, + "norm_steps": 5, + "norm_t_tol": 1e-6, + "norm_tol": 1e-4, "completeness_rtol": 1e-5, "completeness_atol": 1e-8, "martingale_quad_limit": 100, } - del solver_options["improved_sampling"] # both classes will be partially initialized in constructor _trajectory_resultclass = NmmcTrajectoryResult diff --git a/qutip/solver/stochastic.py b/qutip/solver/stochastic.py index 8e1cc9a033..0fcf362afe 100644 --- a/qutip/solver/stochastic.py +++ b/qutip/solver/stochastic.py @@ -509,7 +509,16 @@ class StochasticSolver(MultiTrajSolver): system = None _open = None solver_options = { - **MultiTrajSolver.solver_options, + "progress_bar": "text", + "progress_kwargs": {"chunk_size": 10}, + "store_final_state": False, + "store_states": None, + "keep_runs_results": False, + "normalize_output": False, + "map": "serial", + "mpi_options": {}, + "num_cpus": None, + "bitgenerator": None, "method": "platen", "store_measurement": False, } @@ -795,7 +804,18 @@ class SMESolver(StochasticSolver): _avail_integrators = {} _open = True solver_options = { - **StochasticSolver.solver_options + "progress_bar": "text", + "progress_kwargs": {"chunk_size": 10}, + "store_final_state": False, + "store_states": None, + "keep_runs_results": False, + "normalize_output": False, + "map": "serial", + "mpi_options": {}, + "num_cpus": None, + "bitgenerator": None, + "method": "platen", + "store_measurement": False, } @@ -828,5 +848,16 @@ class SSESolver(StochasticSolver): _avail_integrators = {} _open = False solver_options = { - **StochasticSolver.solver_options + "progress_bar": "text", + "progress_kwargs": {"chunk_size": 10}, + "store_final_state": False, + "store_states": None, + "keep_runs_results": False, + "normalize_output": False, + "map": "serial", + "mpi_options": {}, + "num_cpus": None, + "bitgenerator": None, + "method": "platen", + "store_measurement": False, } From a9ed0fb48884eed45c15f9f3276cbf766ece745e Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Mon, 22 Jan 2024 16:49:37 -0500 Subject: [PATCH 22/66] Fix for scipy 1.12 in tests --- qutip/tests/solver/test_steadystate.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/qutip/tests/solver/test_steadystate.py b/qutip/tests/solver/test_steadystate.py index 74287a81b3..161e904e95 100644 --- a/qutip/tests/solver/test_steadystate.py +++ b/qutip/tests/solver/test_steadystate.py @@ -1,7 +1,9 @@ import numpy as np +import scipy import pytest import qutip import warnings +from packaging import version as pac_version @pytest.mark.parametrize(['method', 'kwargs'], [ @@ -36,6 +38,13 @@ def test_qubit(method, kwargs, dtype): sz = qutip.sigmaz().to(dtype) sm = qutip.destroy(2, dtype=dtype) + if ( + pac_version.parse(scipy.__version__) >= pac_version.parse("1.12") + and "tol" in kwargs + ): + # From scipy 1.12, the tol keyword is renamed to rtol + kwargs["rtol"] = kwargs.pop("tol") + H = 0.5 * 2 * np.pi * sz gamma1 = 0.05 @@ -94,6 +103,13 @@ def test_exact_solution_for_simple_methods(method, kwargs): def test_ho(method, kwargs): # thermal steadystate of an oscillator: compare numerics with analytical # formula + if ( + pac_version.parse(scipy.__version__) >= pac_version.parse("1.12") + and "tol" in kwargs + ): + # From scipy 1.12, the tol keyword is renamed to rtol + kwargs["rtol"] = kwargs.pop("tol") + a = qutip.destroy(30) H = 0.5 * 2 * np.pi * a.dag() * a gamma1 = 0.05 @@ -130,6 +146,13 @@ def test_ho(method, kwargs): pytest.param('iterative-bicgstab', {"atol": 1e-10, "tol": 1e-10}, id="iterative-bicgstab"), ]) def test_driven_cavity(method, kwargs): + if ( + pac_version.parse(scipy.__version__) >= pac_version.parse("1.12") + and "tol" in kwargs + ): + # From scipy 1.12, the tol keyword is renamed to rtol + kwargs["rtol"] = kwargs.pop("tol") + N = 30 Omega = 0.01 * 2 * np.pi Gamma = 0.05 From e600344148457a136fedb3a32eb5d0c3c9460660 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Jan 2024 22:02:33 +0000 Subject: [PATCH 23/66] Bump pillow from 10.0.1 to 10.2.0 in /doc Bumps [pillow](https://github.com/python-pillow/Pillow) from 10.0.1 to 10.2.0. - [Release notes](https://github.com/python-pillow/Pillow/releases) - [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst) - [Commits](https://github.com/python-pillow/Pillow/compare/10.0.1...10.2.0) --- updated-dependencies: - dependency-name: pillow dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- doc/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index aa58f0e06e..6cb3ffacf5 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -21,7 +21,7 @@ packaging==23.0 parso==0.8.3 pexpect==4.8.0 pickleshare==0.7.5 -Pillow==10.0.1 +Pillow==10.2.0 prompt-toolkit==3.0.38 ptyprocess==0.7.0 Pygments==2.15.0 From c1854ec4690fd7dc8c4d8b5ca73dc37f707bb49e Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Tue, 23 Jan 2024 14:16:10 +0900 Subject: [PATCH 24/66] Update github workflows to include mpi tests --- .github/workflows/build_documentation.yml | 1 + .github/workflows/tests.yml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build_documentation.yml b/.github/workflows/build_documentation.yml index b5c6eb9cf6..17d818be48 100644 --- a/.github/workflows/build_documentation.yml +++ b/.github/workflows/build_documentation.yml @@ -10,6 +10,7 @@ jobs: steps: - uses: actions/checkout@v3 + - uses: mpi4py/setup-mpi@v1 - uses: actions/setup-python@v4 name: Install Python diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f44aa7e8f2..0c07620686 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -115,7 +115,7 @@ jobs: # rather than in the GitHub Actions file directly, because bash gives us # a proper programming language to use. run: | - QUTIP_TARGET="tests,graphics,semidefinite,ipython" + QUTIP_TARGET="tests,graphics,semidefinite,ipython,extra" if [[ -z "${{ matrix.nocython }}" ]]; then QUTIP_TARGET="$QUTIP_TARGET,runtime_compilation" fi From 1b203cc0435f1ca70a8c4e8fa4186b34630f6a0c Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Tue, 23 Jan 2024 15:14:39 +0900 Subject: [PATCH 25/66] Fixed typo --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 0c07620686..61de501929 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -115,7 +115,7 @@ jobs: # rather than in the GitHub Actions file directly, because bash gives us # a proper programming language to use. run: | - QUTIP_TARGET="tests,graphics,semidefinite,ipython,extra" + QUTIP_TARGET="tests,graphics,semidefinite,ipython,extras" if [[ -z "${{ matrix.nocython }}" ]]; then QUTIP_TARGET="$QUTIP_TARGET,runtime_compilation" fi From 030298b3ff259bd5432fdb281b17f54b664d9ca7 Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Tue, 23 Jan 2024 15:18:02 +0900 Subject: [PATCH 26/66] Missing MPI setup in tests.yml --- .github/workflows/tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 61de501929..baca23a293 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -103,6 +103,7 @@ jobs: steps: - uses: actions/checkout@v3 + - uses: mpi4py/setup-mpi@v1 - uses: conda-incubator/setup-miniconda@v2 with: From 9301d66cdf8fd6ada8a6d1cfc6defa2114a33092 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Tue, 23 Jan 2024 13:45:23 -0500 Subject: [PATCH 27/66] Add error message to MemoryError --- qutip/core/data/csr.pyx | 23 +++++++++++++++++++---- qutip/core/data/dense.pyx | 30 +++++++++++++++++++++++++----- qutip/core/data/dia.pyx | 12 ++++++++++-- 3 files changed, 54 insertions(+), 11 deletions(-) diff --git a/qutip/core/data/csr.pyx b/qutip/core/data/csr.pyx index c7eed323c2..5bbce551cc 100644 --- a/qutip/core/data/csr.pyx +++ b/qutip/core/data/csr.pyx @@ -530,9 +530,21 @@ cpdef CSR empty(base.idxint rows, base.idxint cols, base.idxint size): PyDataMem_NEW(size * sizeof(base.idxint)) out.row_index =\ PyDataMem_NEW(row_size * sizeof(base.idxint)) - if not out.data: raise MemoryError() - if not out.col_index: raise MemoryError() - if not out.row_index: raise MemoryError() + if not out.data: + raise MemoryError( + f"Failed to allocate the `data` of a ({rows}, {cols}) " + f"CSR array of {size} max elements." + ) + if not out.col_index: + raise MemoryError( + f"Failed to allocate the `col_index` of a ({rows}, {cols}) " + f"CSR array of {size} max elements." + ) + if not out.row_index: + raise MemoryError( + f"Failed to allocate the `row_index` of a ({rows}, {cols}) " + f"CSR array of {size} max elements." + ) # Set the number of non-zero elements to 0. out.row_index[rows] = 0 return out @@ -604,7 +616,10 @@ cdef CSR from_coo_pointers( data_tmp = mem.PyMem_Malloc(nnz * sizeof(double complex)) cols_tmp = mem.PyMem_Malloc(nnz * sizeof(base.idxint)) if data_tmp == NULL or cols_tmp == NULL: - raise MemoryError + raise MemoryError( + f"Failed to allocate the memory needed for a ({n_rows}, {n_cols}) " + f"CSR array with {nnz} elements." + ) with nogil: memset(out.row_index, 0, (n_rows + 1) * sizeof(base.idxint)) for ptr_in in range(nnz): diff --git a/qutip/core/data/dense.pyx b/qutip/core/data/dense.pyx index 1fc7f35670..0db9034fa0 100644 --- a/qutip/core/data/dense.pyx +++ b/qutip/core/data/dense.pyx @@ -120,7 +120,11 @@ cdef class Dense(base.Data): cdef Dense out = Dense.__new__(Dense) cdef size_t size = self.shape[0]*self.shape[1]*sizeof(double complex) cdef double complex *ptr = PyDataMem_NEW(size) - if not ptr: raise MemoryError() + if not ptr: + raise MemoryError( + "Could not allocate memory to copy a " + f"({self.shape[0]}, {self.shape[1]}) Dense matrix." + ) memcpy(ptr, self.data, size) out.shape = self.shape out.data = ptr @@ -163,7 +167,11 @@ cdef class Dense(base.Data): """ cdef size_t size = self.shape[0]*self.shape[1]*sizeof(double complex) cdef double complex *ptr = PyDataMem_NEW(size) - if not ptr: raise MemoryError() + if not ptr: + raise MemoryError( + "Could not allocate memory to convert to a numpy array a " + f"({self.shape[0]}, {self.shape[1]}) Dense matrix." + ) memcpy(ptr, self.data, size) cdef object out =\ cnp.PyArray_SimpleNewFromData(2, [self.shape[0], self.shape[1]], @@ -246,7 +254,11 @@ cpdef Dense empty(base.idxint rows, base.idxint cols, bint fortran=True): cdef Dense out = Dense.__new__(Dense) out.shape = (rows, cols) out.data = PyDataMem_NEW(rows * cols * sizeof(double complex)) - if not out.data: raise MemoryError() + if not out.data: + raise MemoryError( + "Could not allocate memory to create an empty " + f"({rows}, {cols}) Dense matrix." + ) out._deallocate = True out.fortran = fortran return out @@ -267,7 +279,11 @@ cpdef Dense zeros(base.idxint rows, base.idxint cols, bint fortran=True): out.shape = (rows, cols) out.data =\ PyDataMem_NEW_ZEROED(rows * cols, sizeof(double complex)) - if not out.data: raise MemoryError() + if not out.data: + raise MemoryError( + "Could not allocate memory to create a zero " + f"({rows}, {cols}) Dense matrix." + ) out.fortran = fortran out._deallocate = True return out @@ -294,7 +310,11 @@ cpdef Dense from_csr(CSR matrix, bint fortran=False): PyDataMem_NEW_ZEROED(out.shape[0]*out.shape[1], sizeof(double complex)) ) - if not out.data: raise MemoryError() + if not out.data: + raise MemoryError( + "Could not allocate memory to create a " + f"({out.shape[0]}, {out.shape[1]}) Dense matrix from a CSR." + ) out.fortran = fortran out._deallocate = True cdef size_t row, ptr_in, ptr_out, row_stride, col_stride diff --git a/qutip/core/data/dia.pyx b/qutip/core/data/dia.pyx index 92d68b8f89..9414298402 100644 --- a/qutip/core/data/dia.pyx +++ b/qutip/core/data/dia.pyx @@ -271,8 +271,16 @@ cpdef Dia empty(base.idxint rows, base.idxint cols, base.idxint num_diag): PyDataMem_NEW(cols * num_diag * sizeof(double complex)) out.offsets =\ PyDataMem_NEW(num_diag * sizeof(base.idxint)) - if not out.data: raise MemoryError() - if not out.offsets: raise MemoryError() + if not out.data: + raise MemoryError( + f"Failed to allocate the `data` of a ({rows}, {cols}) " + f"Dia array of {num_diag} diagonals." + ) + if not out.offsets: + raise MemoryError( + f"Failed to allocate the `offsets` of a ({rows}, {cols}) " + f"Dia array of {num_diag} diagonals." + ) return out From b1d582b2d0cee852dfb3a92471d0205257c1cf03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eric=20Gigu=C3=A8re?= Date: Wed, 24 Jan 2024 09:51:14 -0500 Subject: [PATCH 28/66] Apply suggestions from code review Co-authored-by: Simon Cross --- doc/guide/guide-basics.rst | 12 ++++++------ doc/guide/guide-bloch.rst | 2 +- doc/guide/guide-control.rst | 11 +++++++---- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/doc/guide/guide-basics.rst b/doc/guide/guide-basics.rst index 19b035903b..1d17cf0976 100644 --- a/doc/guide/guide-basics.rst +++ b/doc/guide/guide-basics.rst @@ -328,10 +328,10 @@ For the destruction operator above: The ``data`` attribute returns a Qutip diagonal matrix. ``Qobj`` instances store their data in Qutip matrix format. -In the core qutip module, the ``Dense``, ``CSR`` and ``Dia`` formats are available, but other module can add other formats. -For example, the qutip-jax module add ``Jax`` and ``JaxDia`` formats. +In the core qutip module, the ``Dense``, ``CSR`` and ``Dia`` formats are available, but other packages can add other formats. +For example, the ``qutip-jax`` module adds the ``Jax`` and ``JaxDia`` formats. One can always access the underlying matrix as a numpy array using :meth:`.Qobj.full`. -It is also possible to access the underlying data as is in a common format using :meth:`.Qobj.data_as`. +It is also possible to access the underlying data in a common format using :meth:`.Qobj.data_as`. .. doctest:: [basics] :options: +NORMALIZE_WHITESPACE @@ -357,9 +357,9 @@ Conversion between storage type is done using the :meth:`.Qobj.to` method. Note that :meth:`.Qobj.data_as` does not do the conversion. QuTiP will do conversion when needed to keep everything working in any format. -However these conversions could slow down the computations and it is recomented to keep to one family of format. -For example, core qutip ``Dense`` and ``CSR`` work well together and binary operation between these format is efficient. -However binary operations between ``Dense`` and ``Jax`` should be avoided since it is not clear whether the operation will be executed by Jax, (possibly on GPU) or numpy. +However these conversions could slow down computation and it is recommended to keep to one format family where possible. +For example, core QuTiP ``Dense`` and ``CSR`` work well together and binary operations between these formats is efficient. +However binary operations between ``Dense`` and ``Jax`` should be avoided since it is not always clear whether the operation will be executed by Jax (possibly on a GPU if present) or numpy. .. _basics-qobj-math: diff --git a/doc/guide/guide-bloch.rst b/doc/guide/guide-bloch.rst index 696b18c4e9..ad351a5f51 100644 --- a/doc/guide/guide-bloch.rst +++ b/doc/guide/guide-bloch.rst @@ -9,7 +9,7 @@ Plotting on the Bloch Sphere Introduction ============ -When studying the dynamics of a two-level system, it is often convenient to visualize the state of the system by plotting the state-vector or density matrix on the Bloch sphere. In QuTiP, we have created two different classes to allow for easy creation and manipulation of data sets, both vectors and data points, on the Bloch sphere. +When studying the dynamics of a two-level system, it is often convenient to visualize the state of the system by plotting the state-vector or density matrix on the Bloch sphere. In QuTiP, there is a class to allow for easy creation and manipulation of data sets, both vectors and data points, on the Bloch sphere. .. _bloch-class: diff --git a/doc/guide/guide-control.rst b/doc/guide/guide-control.rst index b5baf9b0d5..e0769cd592 100644 --- a/doc/guide/guide-control.rst +++ b/doc/guide/guide-control.rst @@ -191,7 +191,10 @@ algorithm. Optimal Quantum Control in QuTiP ================================ -The Quantum Control part of qutip has been moved to it's own project. -Previously available implementation is now located in the `qutip-qtrl `_ module. -A newer interface with upgraded capacities is also being developped in `qutip-qoc `_. -Please give these module a look. +The Quantum Control part of qutip has been moved to its own project. + +The previously available implementation is now located in the `qutip-qtrl `_ module. If the ``qutip-qtrl`` package is installed, it can also be imported under the name ``qutip.control`` to ease porting code developed for QuTiP 4 to QuTiP 5. + +A newer interface with upgraded capacities is being developped in `qutip-qoc `_. + +Please give these modules a try. From aeeb74fea93d90c8dd6196c2e3f8b80e47b5b256 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Wed, 24 Jan 2024 15:02:46 -0500 Subject: [PATCH 29/66] Add towncrier --- doc/changes/2306.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/changes/2306.misc diff --git a/doc/changes/2306.misc b/doc/changes/2306.misc new file mode 100644 index 0000000000..eb9042599c --- /dev/null +++ b/doc/changes/2306.misc @@ -0,0 +1 @@ +Remove Bloch3D: redundant to Bloch \ No newline at end of file From 5355a893bcfe95ced0b9fe665d03fd063851f729 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Thu, 25 Jan 2024 13:59:22 -0500 Subject: [PATCH 30/66] Renew tests matrix --- .github/workflows/tests.yml | 74 +++++++++++++++++-------------------- 1 file changed, 34 insertions(+), 40 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f44aa7e8f2..dafc1295d2 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -36,47 +36,43 @@ jobs: # the lack of a variable is _always_ false-y, and the defaults lack all # the special cases. include: - # Mac - # Mac has issues with MKL since september 2022. - - case-name: macos - os: macos-latest - python-version: "3.10" - condaforge: 1 - nomkl: 1 - - # Scipy 1.5 - - case-name: old SciPy + # Python 3.10, Scipy 1.8, numpy 1.23 + # Oldest version we have to support according to SPEC 0 + # https://scientific-python.org/specs/spec-0000/ + - case-name: Old setup os: ubuntu-latest - python-version: "3.8" - numpy-requirement: ">=1.20,<1.21" - scipy-requirement: ">=1.5,<1.6" + python-version: "3.10" + numpy-requirement: ">=1.23,<1.24" + scipy-requirement: ">=1.8,<1.9" condaforge: 1 oldcython: 1 pytest-extra-options: "-W ignore:dep_util:DeprecationWarning" - # No MKL runs. MKL is now the default for conda installations, but - # not necessarily for pip. - - case-name: no MKL + # Python 3.10, no mkl, no cython + - case-name: Python 3.10, no mkl os: ubuntu-latest - python-version: "3.9" - numpy-requirement: ">=1.20,<1.21" + python-version: "3.10" + condaforge: 1 nomkl: 1 - - # Builds without Cython at runtime. This is a core feature; - # everything should be able to run this. - - case-name: no Cython - os: ubuntu-latest - python-version: "3.8" nocython: 1 - # Python 3.10 and numpy 1.22 - # Use conda-forge to provide numpy 1.22 - - case-name: Python 3.10 - os: ubuntu-latest - python-version: "3.10" + # Mac + # Mac has issues with MKL since september 2022. + - case-name: macos + os: macos-latest + python-version: "3.11" condaforge: 1 - oldcython: 1 - pytest-extra-options: "-W ignore:dep_util:DeprecationWarning" + nomkl: 1 + + # Windows. Once all tests pass without special options needed, this + # can be moved to the main os list in the test matrix. All the tests + # that fail currently seem to do so because mcsolve uses + # multiprocessing under the hood. Windows does not support fork() + # well, which makes transfering objects to the child processes + # error prone. See, e.g., https://github.com/qutip/qutip/issues/1202 + - case-name: Windows + os: windows-latest + python-version: "3.11" # Python 3.11 and latest numpy # Use conda-forge to provide Python 3.11 and latest numpy @@ -91,15 +87,13 @@ jobs: conda-extra-pkgs: "suitesparse" # for compiling cvxopt pytest-extra-options: "-W ignore::DeprecationWarning:Cython.Tempita" - # Windows. Once all tests pass without special options needed, this - # can be moved to the main os list in the test matrix. All the tests - # that fail currently seem to do so because mcsolve uses - # multiprocessing under the hood. Windows does not support fork() - # well, which makes transfering objects to the child processes - # error prone. See, e.g., https://github.com/qutip/qutip/issues/1202 - - case-name: Windows Latest - os: windows-latest - python-version: "3.10" + # Python 3.12 and latest numpy + # Use conda-forge to provide Python 3.11 and latest numpy + - case-name: Python 3.12 + os: ubuntu-latest + python-version: "3.12" + condaforge: 1 + steps: - uses: actions/checkout@v3 From c46e17f17446fb929ff35afe2f4a6e1c07d85634 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Thu, 25 Jan 2024 14:05:47 -0500 Subject: [PATCH 31/66] Update build for python 3.10 to 3.12 --- .github/workflows/build.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 45b648fa35..4c2396502b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -57,7 +57,7 @@ jobs: # For the sdist we should be as conservative as possible with our # Python version. This should be the lowest supported version. This # means that no unsupported syntax can sneak through. - python-version: '3.8' + python-version: '3.10' - name: Install pip build run: | @@ -107,11 +107,11 @@ jobs: matrix: os: [ubuntu-latest, windows-latest, macos-latest] env: - # Set up wheels matrix. This is CPython 3.8--3.10 for all OS targets. - CIBW_BUILD: "cp3{8,9,10,11}-*" + # Set up wheels matrix. This is CPython 3.10--3.12 for all OS targets. + CIBW_BUILD: "cp3{10,11,12}-*" # Numpy and SciPy do not supply wheels for i686 or win32 for # Python 3.10+, so we skip those: - CIBW_SKIP: "*-musllinux* cp3{8,9,10,11}-manylinux_i686 cp3{8,9,10,11}-win32" + CIBW_SKIP: "*-musllinux* cp3{10,11,12}-manylinux_i686 cp3{10,11,12}-win32" OVERRIDE_VERSION: ${{ github.event.inputs.override_version }} steps: @@ -165,12 +165,12 @@ jobs: - uses: actions/setup-python@v4 name: Install Python with: - python-version: '3.8' + python-version: '3.10' - name: Verify this is not a dev version shell: bash run: | - python -m pip install wheels/*-cp38-cp38-manylinux*.whl + python -m pip install wheels/*-cp310-cp310-manylinux*.whl python -c 'import qutip; print(qutip.__version__); assert "dev" not in qutip.__version__; assert "+" not in qutip.__version__' # We built the zipfile for convenience distributing to Windows users on @@ -198,12 +198,12 @@ jobs: - uses: actions/setup-python@v4 name: Install Python with: - python-version: '3.8' + python-version: '3.10' - name: Verify this is not a dev version shell: bash run: | - python -m pip install wheels/*-cp38-cp38-manylinux*.whl + python -m pip install wheels/*-cp310-cp310-manylinux*.whl python -c 'import qutip; print(qutip.__version__); assert "dev" not in qutip.__version__; assert "+" not in qutip.__version__' # We built the zipfile for convenience distributing to Windows users on From c87f563f96540bf4e1c152c7919dc36d66eee772 Mon Sep 17 00:00:00 2001 From: DnMGalan Date: Thu, 25 Jan 2024 20:03:33 +0100 Subject: [PATCH 32/66] Add the possibility to customize point colors as in V4 --- qutip/bloch.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/qutip/bloch.py b/qutip/bloch.py index 0978ad48bd..84ceb83bd4 100644 --- a/qutip/bloch.py +++ b/qutip/bloch.py @@ -169,8 +169,10 @@ def __init__(self, fig=None, axes=None, view=None, figsize=None, # ---point options--- # List of colors for Bloch point markers, default = ['b','g','r','y'] self.point_default_color = ['b', 'r', 'g', '#CC6600'] + # Old variable used in V4 to customise the color of the points + self.point_color = None # List that stores the display colors for each set of points - self.point_color = [] + self.inner_point_color = [] # Size of point markers, default = 25 self.point_size = [25, 32, 35, 45] # Shape of point markers, default = ['o','^','d','s'] @@ -360,7 +362,7 @@ def add_points(self, points, meth='s', colors=None, alpha=1.0): self.point_style.append(meth) self.points.append(points) self.point_alpha.append(alpha) - self.point_color.append(colors) + self.inner_point_color.append(colors) def add_states(self, state, kind='vector', colors=None, alpha=1.0): """Add a state vector Qobj to Bloch sphere. @@ -799,12 +801,15 @@ def plot_points(self): s = self.point_size[np.mod(k, len(self.point_size))] marker = self.point_marker[np.mod(k, len(self.point_marker))] style = self.point_style[k] - if self.point_color[k] is not None: - color = self.point_color[k] + + if self.inner_point_color[k] is not None: + color = self.inner_point_color[k] + elif self.point_color is not None: + color = self.point_color elif self.point_style[k] in ['s', 'l']: - color = self.point_default_color[ + color = [self.point_default_color[ k % len(self.point_default_color) - ] + ]] elif self.point_style[k] == 'm': length = np.ceil(num_points/len(self.point_default_color)) color = np.tile(self.point_default_color, length.astype(int)) @@ -824,6 +829,7 @@ def plot_points(self): ) elif self.point_style[k] == 'l': + color = color[k % len(color)] self.axes.plot(np.real(points[1]), -np.real(points[0]), np.real(points[2]), From 699f7279163d29036cc874c7695cd8742171bf1a Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Thu, 25 Jan 2024 14:22:59 -0500 Subject: [PATCH 33/66] Update requirements --- .github/workflows/build.yml | 16 ++++++++-------- .github/workflows/tests.yml | 33 +++++++++++++++++++++++---------- pyproject.toml | 8 +------- requirements.txt | 4 ++-- setup.cfg | 8 ++++---- 5 files changed, 38 insertions(+), 31 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4c2396502b..c08e003ea1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -57,7 +57,7 @@ jobs: # For the sdist we should be as conservative as possible with our # Python version. This should be the lowest supported version. This # means that no unsupported syntax can sneak through. - python-version: '3.10' + python-version: '3.9' - name: Install pip build run: | @@ -107,8 +107,8 @@ jobs: matrix: os: [ubuntu-latest, windows-latest, macos-latest] env: - # Set up wheels matrix. This is CPython 3.10--3.12 for all OS targets. - CIBW_BUILD: "cp3{10,11,12}-*" + # Set up wheels matrix. This is CPython 3.9--3.12 for all OS targets. + CIBW_BUILD: "cp3{9,10,11,12}-*" # Numpy and SciPy do not supply wheels for i686 or win32 for # Python 3.10+, so we skip those: CIBW_SKIP: "*-musllinux* cp3{10,11,12}-manylinux_i686 cp3{10,11,12}-win32" @@ -121,7 +121,7 @@ jobs: name: Install Python with: # This is about the build environment, not the released wheel version. - python-version: '3.8' + python-version: '3.9' - name: Install cibuildwheel run: | @@ -165,12 +165,12 @@ jobs: - uses: actions/setup-python@v4 name: Install Python with: - python-version: '3.10' + python-version: '3.9' - name: Verify this is not a dev version shell: bash run: | - python -m pip install wheels/*-cp310-cp310-manylinux*.whl + python -m pip install wheels/*-cp39-cp39-manylinux*.whl python -c 'import qutip; print(qutip.__version__); assert "dev" not in qutip.__version__; assert "+" not in qutip.__version__' # We built the zipfile for convenience distributing to Windows users on @@ -198,12 +198,12 @@ jobs: - uses: actions/setup-python@v4 name: Install Python with: - python-version: '3.10' + python-version: '3.9' - name: Verify this is not a dev version shell: bash run: | - python -m pip install wheels/*-cp310-cp310-manylinux*.whl + python -m pip install wheels/*-cp39-cp39-manylinux*.whl python -c 'import qutip; print(qutip.__version__); assert "dev" not in qutip.__version__; assert "+" not in qutip.__version__' # We built the zipfile for convenience distributing to Windows users on diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index dafc1295d2..5fc826a276 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -27,34 +27,45 @@ jobs: os: [ubuntu-latest] # Test other versions of Python in special cases to avoid exploding the # matrix size; make sure to test all supported versions in some form. - python-version: ["3.9"] + python-version: ["3.11"] case-name: [defaults] - numpy-requirement: [">=1.20,<1.21"] + numpy-requirement: [">=1.22,<1.27"] + scipy-requirement: [">=1.8,<1.12"] coverage-requirement: ["==6.5"] # Extra special cases. In these, the new variable defined should always # be a truth-y value (hence 'nomkl: 1' rather than 'mkl: 0'), because # the lack of a variable is _always_ false-y, and the defaults lack all # the special cases. include: - # Python 3.10, Scipy 1.8, numpy 1.23 - # Oldest version we have to support according to SPEC 0 + # Python 3.9, Scipy 1.7, numpy 1.22 + # On more version than suggested by SPEC 0 # https://scientific-python.org/specs/spec-0000/ - case-name: Old setup os: ubuntu-latest - python-version: "3.10" - numpy-requirement: ">=1.23,<1.24" + python-version: "3.9" + numpy-requirement: ">=1.22,<1.23" scipy-requirement: ">=1.8,<1.9" condaforge: 1 oldcython: 1 pytest-extra-options: "-W ignore:dep_util:DeprecationWarning" - # Python 3.10, no mkl, no cython + # Python 3.10, no cython, oldist dependencies + - case-name: Python 3.10, no cython + os: ubuntu-latest + python-version: "3.10" + scipy-requirement: ">=1.9,<1.10" + numpy-requirement: ">=1.23,<1.24" + condaforge: 1 + nocython: 1 + + # Python 3.10, no mkl - case-name: Python 3.10, no mkl os: ubuntu-latest python-version: "3.10" + scipy-requirement: ">=1.10,<1.11" + numpy-requirement: ">=1.24,<1.25" condaforge: 1 nomkl: 1 - nocython: 1 # Mac # Mac has issues with MKL since september 2022. @@ -74,7 +85,7 @@ jobs: os: windows-latest python-version: "3.11" - # Python 3.11 and latest numpy + # Python 3.11 and recent numpy # Use conda-forge to provide Python 3.11 and latest numpy # Ignore deprecation of the cgi module in Python 3.11 that is # still imported by Cython.Tempita. This was addressed in @@ -84,8 +95,10 @@ jobs: os: ubuntu-latest python-version: "3.11" condaforge: 1 + numpy-requirement: ">=1.25,<1.26" + scipy-requirement: ">=1.11,<1.12" conda-extra-pkgs: "suitesparse" # for compiling cvxopt - pytest-extra-options: "-W ignore::DeprecationWarning:Cython.Tempita" + # pytest-extra-options: "-W ignore::DeprecationWarning:Cython.Tempita" # Python 3.12 and latest numpy # Use conda-forge to provide Python 3.11 and latest numpy diff --git a/pyproject.toml b/pyproject.toml index 230bbad6b3..896cd3f115 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ requires = [ # See https://numpy.org/doc/stable/user/depending_on_numpy.html for # the recommended way to build against numpy's C API: "oldest-supported-numpy", - "scipy>=1.0", + "scipy>=1.8", ] build-backend = "setuptools.build_meta" @@ -18,12 +18,6 @@ manylinux-i686-image = "manylinux2014" # Change in future version to "build" build-frontend = "pip" -[[tool.cibuildwheel.overrides]] -# NumPy and SciPy support manylinux2010 on CPython 3.6 and 3.7 -select = "cp3{6,7}-*" -manylinux-x86_64-image = "manylinux2010" -manylinux-i686-image = "manylinux2010" - [tool.towncrier] directory = "doc/changes" filename = "doc/changelog.rst" diff --git a/requirements.txt b/requirements.txt index 41d2c09a1b..9fc7beade5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ cython>=0.29.20 -numpy>=1.16.6 -scipy>=1.5 +numpy>=1.22 +scipy>=1.8 packaging diff --git a/setup.cfg b/setup.cfg index d5373c9c85..4863e1ec63 100644 --- a/setup.cfg +++ b/setup.cfg @@ -31,12 +31,12 @@ packages = find: include_package_data = True zip_safe = False install_requires = - numpy>=1.16.6 - scipy>=1.0 + numpy>=1.22,<1.26 + scipy>=1.8,<1.12 packaging setup_requires = - numpy>=1.13.3 - scipy>=1.0 + numpy>=1.22 + scipy>=1.8 cython>=0.29.20; python_version>='3.10' cython>=0.29.20,<3.0.3; python_version<='3.9' packaging From f36cc220f68adfb4e579229e8640b2c1db17acfe Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Thu, 25 Jan 2024 15:17:03 -0500 Subject: [PATCH 34/66] installable with oldest supported numpy --- setup.cfg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index 4863e1ec63..1491a2cbb2 100644 --- a/setup.cfg +++ b/setup.cfg @@ -31,11 +31,11 @@ packages = find: include_package_data = True zip_safe = False install_requires = - numpy>=1.22,<1.26 + numpy>=1.22 scipy>=1.8,<1.12 packaging setup_requires = - numpy>=1.22 + numpy>=1.19 scipy>=1.8 cython>=0.29.20; python_version>='3.10' cython>=0.29.20,<3.0.3; python_version<='3.9' From 1afbd10a1e3c8c78fc282946cb97dfa63c6bbedc Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Thu, 25 Jan 2024 16:42:50 -0500 Subject: [PATCH 35/66] filter dateutil deprecation --- .github/workflows/tests.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5fc826a276..b835a3f5b0 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -50,7 +50,7 @@ jobs: pytest-extra-options: "-W ignore:dep_util:DeprecationWarning" # Python 3.10, no cython, oldist dependencies - - case-name: Python 3.10, no cython + - case-name: no cython os: ubuntu-latest python-version: "3.10" scipy-requirement: ">=1.9,<1.10" @@ -59,7 +59,7 @@ jobs: nocython: 1 # Python 3.10, no mkl - - case-name: Python 3.10, no mkl + - case-name: no mkl os: ubuntu-latest python-version: "3.10" scipy-requirement: ">=1.10,<1.11" @@ -98,7 +98,6 @@ jobs: numpy-requirement: ">=1.25,<1.26" scipy-requirement: ">=1.11,<1.12" conda-extra-pkgs: "suitesparse" # for compiling cvxopt - # pytest-extra-options: "-W ignore::DeprecationWarning:Cython.Tempita" # Python 3.12 and latest numpy # Use conda-forge to provide Python 3.11 and latest numpy @@ -106,6 +105,7 @@ jobs: os: ubuntu-latest python-version: "3.12" condaforge: 1 + pytest-extra-options: "-W ignore:datetime:DeprecationWarning" steps: From ac6f7fb3d368a0d4abcced776704adca65a4a02e Mon Sep 17 00:00:00 2001 From: DnMGalan Date: Fri, 26 Jan 2024 12:24:17 +0100 Subject: [PATCH 36/66] Fix point plot behavior for 'l' style and add Towncrier entry. --- doc/changes/1974.bugfix | 1 + qutip/bloch.py | 7 +++---- 2 files changed, 4 insertions(+), 4 deletions(-) create mode 100644 doc/changes/1974.bugfix diff --git a/doc/changes/1974.bugfix b/doc/changes/1974.bugfix new file mode 100644 index 0000000000..f7b521a0bf --- /dev/null +++ b/doc/changes/1974.bugfix @@ -0,0 +1 @@ +Add the possibility to customize point colors as in V4 and fix point plot behavior for 'l' style \ No newline at end of file diff --git a/qutip/bloch.py b/qutip/bloch.py index 84ceb83bd4..236493ba04 100644 --- a/qutip/bloch.py +++ b/qutip/bloch.py @@ -794,7 +794,6 @@ def plot_points(self): dist = np.linalg.norm(points, axis=0) if not np.allclose(dist, dist[0], rtol=1e-12): indperm = np.argsort(dist) - points = points[:, indperm] else: indperm = np.arange(num_points) @@ -817,9 +816,9 @@ def plot_points(self): color = list(color) if self.point_style[k] in ['s', 'm']: - self.axes.scatter(np.real(points[1]), - -np.real(points[0]), - np.real(points[2]), + self.axes.scatter(np.real(points[1][indperm]), + -np.real(points[0][indperm]), + np.real(points[2][indperm]), s=s, marker=marker, color=color, From 9123786721a44c284f363c29f8bca77901d9dd1a Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Fri, 26 Jan 2024 10:31:28 -0500 Subject: [PATCH 37/66] Use old cython for old scipy --- .github/workflows/tests.yml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b835a3f5b0..71382bad8c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -43,29 +43,30 @@ jobs: - case-name: Old setup os: ubuntu-latest python-version: "3.9" - numpy-requirement: ">=1.22,<1.23" scipy-requirement: ">=1.8,<1.9" + numpy-requirement: ">=1.22,<1.23" condaforge: 1 oldcython: 1 pytest-extra-options: "-W ignore:dep_util:DeprecationWarning" - # Python 3.10, no cython, oldist dependencies - - case-name: no cython + # Python 3.10, no mkl, scipy 1.9, numpy 1.23 + # Scipy 1.10 did not support cython 3.0 yet. + - case-name: no mkl os: ubuntu-latest python-version: "3.10" scipy-requirement: ">=1.9,<1.10" numpy-requirement: ">=1.23,<1.24" condaforge: 1 - nocython: 1 + oldcython: 1 + nomkl: 1 - # Python 3.10, no mkl + # Python 3.10, no cython, scipy 1.10, numpy 1.24 - case-name: no mkl os: ubuntu-latest python-version: "3.10" scipy-requirement: ">=1.10,<1.11" numpy-requirement: ">=1.24,<1.25" - condaforge: 1 - nomkl: 1 + nocython: 1 # Mac # Mac has issues with MKL since september 2022. From a8c6dfdeadb67e5526f9bc4764fc6609a3a18d64 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Fri, 26 Jan 2024 11:13:11 -0500 Subject: [PATCH 38/66] Fix filter warnings --- .github/workflows/tests.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 71382bad8c..19b1e79fb8 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -50,7 +50,8 @@ jobs: pytest-extra-options: "-W ignore:dep_util:DeprecationWarning" # Python 3.10, no mkl, scipy 1.9, numpy 1.23 - # Scipy 1.10 did not support cython 3.0 yet. + # Scipy 1.9 did not support cython 3.0 yet. + # cython#17234 - case-name: no mkl os: ubuntu-latest python-version: "3.10" @@ -59,9 +60,10 @@ jobs: condaforge: 1 oldcython: 1 nomkl: 1 + pytest-extra-options: "-W ignore:dep_util:DeprecationWarning" # Python 3.10, no cython, scipy 1.10, numpy 1.24 - - case-name: no mkl + - case-name: no cython os: ubuntu-latest python-version: "3.10" scipy-requirement: ">=1.10,<1.11" From d4c441f97703d10c070b2cfac49a6998c05bc651 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Fri, 26 Jan 2024 12:03:21 -0500 Subject: [PATCH 39/66] Reorder matrix --- .github/workflows/tests.yml | 44 +++++++++++++++++++------------------ 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 19b1e79fb8..4ee25f99e2 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -29,8 +29,8 @@ jobs: # matrix size; make sure to test all supported versions in some form. python-version: ["3.11"] case-name: [defaults] - numpy-requirement: [">=1.22,<1.27"] - scipy-requirement: [">=1.8,<1.12"] + numpy-requirement: [">=1.22"] + scipy-requirement: [">=1.8"] coverage-requirement: ["==6.5"] # Extra special cases. In these, the new variable defined should always # be a truth-y value (hence 'nomkl: 1' rather than 'mkl: 0'), because @@ -40,6 +40,7 @@ jobs: # Python 3.9, Scipy 1.7, numpy 1.22 # On more version than suggested by SPEC 0 # https://scientific-python.org/specs/spec-0000/ + # There are deprecation warnings when using cython 0.29.X - case-name: Old setup os: ubuntu-latest python-version: "3.9" @@ -70,24 +71,6 @@ jobs: numpy-requirement: ">=1.24,<1.25" nocython: 1 - # Mac - # Mac has issues with MKL since september 2022. - - case-name: macos - os: macos-latest - python-version: "3.11" - condaforge: 1 - nomkl: 1 - - # Windows. Once all tests pass without special options needed, this - # can be moved to the main os list in the test matrix. All the tests - # that fail currently seem to do so because mcsolve uses - # multiprocessing under the hood. Windows does not support fork() - # well, which makes transfering objects to the child processes - # error prone. See, e.g., https://github.com/qutip/qutip/issues/1202 - - case-name: Windows - os: windows-latest - python-version: "3.11" - # Python 3.11 and recent numpy # Use conda-forge to provide Python 3.11 and latest numpy # Ignore deprecation of the cgi module in Python 3.11 that is @@ -98,8 +81,8 @@ jobs: os: ubuntu-latest python-version: "3.11" condaforge: 1 - numpy-requirement: ">=1.25,<1.26" scipy-requirement: ">=1.11,<1.12" + numpy-requirement: ">=1.25,<1.26" conda-extra-pkgs: "suitesparse" # for compiling cvxopt # Python 3.12 and latest numpy @@ -107,9 +90,28 @@ jobs: - case-name: Python 3.12 os: ubuntu-latest python-version: "3.12" + scipy-requirement: ">=1.12,<1.13" + numpy-requirement: ">=1.26,<1.27" condaforge: 1 pytest-extra-options: "-W ignore:datetime:DeprecationWarning" + # Mac + # Mac has issues with MKL since september 2022. + - case-name: macos + os: macos-latest + python-version: "3.11" + condaforge: 1 + nomkl: 1 + + # Windows. Once all tests pass without special options needed, this + # can be moved to the main os list in the test matrix. All the tests + # that fail currently seem to do so because mcsolve uses + # multiprocessing under the hood. Windows does not support fork() + # well, which makes transfering objects to the child processes + # error prone. See, e.g., https://github.com/qutip/qutip/issues/1202 + - case-name: Windows + os: windows-latest + python-version: "3.11" steps: - uses: actions/checkout@v3 From ca8f8c2a23a37b9120a60a9d8bc176075953b5b1 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Mon, 29 Jan 2024 14:25:31 -0500 Subject: [PATCH 40/66] Ensure tests can pass without ipython and matplotlib --- qutip/tests/test_animation.py | 5 +++-- qutip/tests/test_ipynbtools.py | 3 ++- qutip/tests/test_visualization.py | 4 ++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/qutip/tests/test_animation.py b/qutip/tests/test_animation.py index 676e7473a4..0433a38608 100644 --- a/qutip/tests/test_animation.py +++ b/qutip/tests/test_animation.py @@ -1,10 +1,11 @@ import pytest import qutip import numpy as np -import matplotlib as mpl -import matplotlib.pyplot as plt from scipy.special import sph_harm +mpl = pytest.importorskip("matplotlib") +plt = pytest.importorskip("matplotlib.pyplot") + def test_result_state(): H = qutip.rand_dm(2) tlist = np.linspace(0, 3*np.pi, 2) diff --git a/qutip/tests/test_ipynbtools.py b/qutip/tests/test_ipynbtools.py index 2b1c76559a..2227e0c784 100644 --- a/qutip/tests/test_ipynbtools.py +++ b/qutip/tests/test_ipynbtools.py @@ -1,6 +1,7 @@ -from qutip.ipynbtools import version_table import pytest +pytest.importorskip("IPython") +from qutip.ipynbtools import version_table @pytest.mark.parametrize('verbose', [False, True]) def test_version_table(verbose): diff --git a/qutip/tests/test_visualization.py b/qutip/tests/test_visualization.py index 2d2901e1be..ebf92b4d11 100644 --- a/qutip/tests/test_visualization.py +++ b/qutip/tests/test_visualization.py @@ -1,10 +1,10 @@ import pytest import qutip import numpy as np -import matplotlib as mpl -import matplotlib.pyplot as plt from scipy.special import sph_harm +mpl = pytest.importorskip("matplotlib") +plt = pytest.importorskip("matplotlib.pyplot") def test_cyclic(): qutip.settings.colorblind_safe = True From 99124bcffad9913c85719cdec2a552e11548bd05 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Mon, 29 Jan 2024 14:34:03 -0500 Subject: [PATCH 41/66] Add towncrier --- doc/changes/2311.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/changes/2311.misc diff --git a/doc/changes/2311.misc b/doc/changes/2311.misc new file mode 100644 index 0000000000..d5712b0c02 --- /dev/null +++ b/doc/changes/2311.misc @@ -0,0 +1 @@ +Allow tests to run without matplotlib and ipython. \ No newline at end of file From 6bb7a93b07048a9e4b462fde84117627e4ea0eb7 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Tue, 30 Jan 2024 09:38:57 -0500 Subject: [PATCH 42/66] Increase small step over default dt. --- qutip/tests/solver/test_stochastic.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/qutip/tests/solver/test_stochastic.py b/qutip/tests/solver/test_stochastic.py index ebbf194daf..f5dac26af2 100644 --- a/qutip/tests/solver/test_stochastic.py +++ b/qutip/tests/solver/test_stochastic.py @@ -348,23 +348,23 @@ def func(t, A, W): def test_deprecation_warnings(): with pytest.warns(FutureWarning, match=r'map_func'): - ssesolve(qeye(2), basis(2), [0, 1e-5], [qeye(2)], map_func=None) + ssesolve(qeye(2), basis(2), [0, 0.01], [qeye(2)], map_func=None) with pytest.warns(FutureWarning, match=r'progress_bar'): - ssesolve(qeye(2), basis(2), [0, 1e-5], [qeye(2)], progress_bar=None) + ssesolve(qeye(2), basis(2), [0, 0.01], [qeye(2)], progress_bar=None) with pytest.warns(FutureWarning, match=r'nsubsteps'): - ssesolve(qeye(2), basis(2), [0, 1e-5], [qeye(2)], nsubsteps=None) + ssesolve(qeye(2), basis(2), [0, 0.01], [qeye(2)], nsubsteps=None) with pytest.warns(FutureWarning, match=r'map_func'): - ssesolve(qeye(2), basis(2), [0, 1e-5], [qeye(2)], map_func=None) + ssesolve(qeye(2), basis(2), [0, 0.01], [qeye(2)], map_func=None) with pytest.warns(FutureWarning, match=r'store_all_expect'): - ssesolve(qeye(2), basis(2), [0, 1e-5], [qeye(2)], store_all_expect=1) + ssesolve(qeye(2), basis(2), [0, 0.01], [qeye(2)], store_all_expect=1) with pytest.warns(FutureWarning, match=r'store_measurement'): - ssesolve(qeye(2), basis(2), [0, 1e-5], [qeye(2)], store_measurement=1) + ssesolve(qeye(2), basis(2), [0, 0.01], [qeye(2)], store_measurement=1) with pytest.raises(TypeError) as err: - ssesolve(qeye(2), basis(2), [0, 1e-5], [qeye(2)], m_ops=1) + ssesolve(qeye(2), basis(2), [0, 0.01], [qeye(2)], m_ops=1) assert '"m_ops" and "dW_factors"' in str(err.value) From ebb730506af0079093ca4dcb318881af7ed69556 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Tue, 30 Jan 2024 10:05:49 -0500 Subject: [PATCH 43/66] Add small step warnings --- qutip/solver/sode/sode.py | 12 +++++++++--- qutip/tests/solver/test_stochastic.py | 4 ++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/qutip/solver/sode/sode.py b/qutip/solver/sode/sode.py index 33ffaae96a..63c283a774 100644 --- a/qutip/solver/sode/sode.py +++ b/qutip/solver/sode/sode.py @@ -1,4 +1,5 @@ import numpy as np +import warnings from . import _sode from ..integrator.integrator import Integrator from ..stochastic import StochasticSolver, SMESolver @@ -135,12 +136,17 @@ def __init__(self, rhs, options): def integrate(self, t, copy=True): delta_t = t - self.t + dt = self.options["dt"] if delta_t < 0: - raise ValueError("Stochastic integration time") - elif delta_t == 0: + raise ValueError("Integration time, can't be negative.") + elif delta_t < 0.5 * dt: + warnings.warn( + f"Step under minimum step ({dt}), skipped.", + RuntimeWarning + ) return self.t, self.state, np.zeros(self.N_dw) - dt = self.options["dt"] + N, extra = np.divmod(delta_t, dt) N = int(N) if extra > 0.5 * dt: diff --git a/qutip/tests/solver/test_stochastic.py b/qutip/tests/solver/test_stochastic.py index f5dac26af2..da11402b96 100644 --- a/qutip/tests/solver/test_stochastic.py +++ b/qutip/tests/solver/test_stochastic.py @@ -368,3 +368,7 @@ def test_deprecation_warnings(): with pytest.raises(TypeError) as err: ssesolve(qeye(2), basis(2), [0, 0.01], [qeye(2)], m_ops=1) assert '"m_ops" and "dW_factors"' in str(err.value) + +def test_small_step_warnings(): + with pytest.warns(RuntimeWarning, match=r'under minimum'): + ssesolve(qeye(2), basis(2), [0, 0.0000001], [qeye(2)]) From 9e00d1fd59e5e143b963da62f1592a3487e73339 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Tue, 30 Jan 2024 11:11:21 -0500 Subject: [PATCH 44/66] Add towncrier --- doc/changes/2313.misc | 1 + qutip/solver/sode/rouchon.py | 11 ++++++++--- qutip/solver/sode/sode.py | 1 - qutip/tests/solver/test_stochastic.py | 9 +++++++-- 4 files changed, 16 insertions(+), 6 deletions(-) create mode 100644 doc/changes/2313.misc diff --git a/doc/changes/2313.misc b/doc/changes/2313.misc new file mode 100644 index 0000000000..8020b81549 --- /dev/null +++ b/doc/changes/2313.misc @@ -0,0 +1 @@ +Add too small step warnings in fixed dt SODE solver \ No newline at end of file diff --git a/qutip/solver/sode/rouchon.py b/qutip/solver/sode/rouchon.py index a00d8e41de..61c30c0996 100644 --- a/qutip/solver/sode/rouchon.py +++ b/qutip/solver/sode/rouchon.py @@ -1,4 +1,5 @@ import numpy as np +import warnings from qutip import unstack_columns, stack_columns from qutip.core import data as _data from ..stochastic import StochasticSolver @@ -97,12 +98,16 @@ def set_state(self, t, state0, generator): def integrate(self, t, copy=True): delta_t = (t - self.t) + dt = self.options["dt"] if delta_t < 0: raise ValueError("Stochastic integration need increasing times") - elif delta_t == 0: - return self.t, self.state, np.zeros() + elif delta_t < 0.5 * dt: + warnings.warn( + f"Step under minimum step ({dt}), skipped.", + RuntimeWarning + ) + return self.t, self.state, np.zeros(len(self.sc_ops)) - dt = self.options["dt"] N, extra = np.divmod(delta_t, dt) N = int(N) if extra > 0.5 * dt: diff --git a/qutip/solver/sode/sode.py b/qutip/solver/sode/sode.py index 63c283a774..e43e8f5ac1 100644 --- a/qutip/solver/sode/sode.py +++ b/qutip/solver/sode/sode.py @@ -146,7 +146,6 @@ def integrate(self, t, copy=True): ) return self.t, self.state, np.zeros(self.N_dw) - N, extra = np.divmod(delta_t, dt) N = int(N) if extra > 0.5 * dt: diff --git a/qutip/tests/solver/test_stochastic.py b/qutip/tests/solver/test_stochastic.py index da11402b96..dc04b966f1 100644 --- a/qutip/tests/solver/test_stochastic.py +++ b/qutip/tests/solver/test_stochastic.py @@ -369,6 +369,11 @@ def test_deprecation_warnings(): ssesolve(qeye(2), basis(2), [0, 0.01], [qeye(2)], m_ops=1) assert '"m_ops" and "dW_factors"' in str(err.value) -def test_small_step_warnings(): + +@pytest.mark.parametrize("method", ["euler", "rouchon"]) +def test_small_step_warnings(method): with pytest.warns(RuntimeWarning, match=r'under minimum'): - ssesolve(qeye(2), basis(2), [0, 0.0000001], [qeye(2)]) + smesolve( + qeye(2), basis(2), [0, 0.0000001], [qeye(2)], + options={"method": method} + ) From 74f148a426620cca67fd0fa8a3ce6248d197e867 Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Thu, 1 Feb 2024 18:41:28 +0900 Subject: [PATCH 45/66] Added documentation regarding default value of 'num_cpus' for 'mpi_pmap', changed tests to not use default value --- qutip/solver/parallel.py | 21 +++++++++++++++++++++ qutip/tests/solver/test_parallel.py | 15 ++++++++++++--- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/qutip/solver/parallel.py b/qutip/solver/parallel.py index fd0956c125..16d4413370 100644 --- a/qutip/solver/parallel.py +++ b/qutip/solver/parallel.py @@ -11,6 +11,7 @@ import time import threading import concurrent.futures +import warnings from qutip.ui.progressbar import progress_bars from qutip.settings import available_cpu_count @@ -432,6 +433,12 @@ def mpi_pmap(task, values, task_args=None, task_kwargs=None, processes. For more information, consult the documentation of mpi4py and the mpi4py.MPIPoolExecutor class. + Note: in keeping consistent with the API of `parallel_map`, the parameter + determining the number of requested worker processes is called `num_cpus`. + The value of `map_kw['num_cpus']` is passed to the MPIPoolExecutor as its + `max_workers` argument. Its default value is the number of logical CPUs + (i.e., threads), which might be unsuitable for MPI applications. + Parameters ---------- task : a Python function @@ -472,11 +479,25 @@ def mpi_pmap(task, values, task_args=None, task_kwargs=None, """ from mpi4py.futures import MPIPoolExecutor + + # If the provided num_cpus is None, we use the default value instead (and + # emit a warning). We thus intentionally make it impossible to call + # MPIPoolExecutor(max_workers=None, ...) + # in which case mpi4py would determine a default value. + # The default value provided by mpi4py would be better suited, but mpi4py + # provides no public API to access the actual number of workers that is + # used in this case, which we need. + worker_number_provided = (map_kw is not None) and ('num_cpus' in map_kw) + map_kw = _read_map_kw(map_kw) timeout = map_kw.pop('timeout') num_workers = map_kw.pop('num_cpus') fail_fast = map_kw.pop('fail_fast') + if not worker_number_provided: + warnings.warn(f'mpi_pmap was called without specifying the number of ' + f'worker processes, using the default {num_workers}') + def setup_executor(): return MPIPoolExecutor(max_workers=num_workers, **map_kw) diff --git a/qutip/tests/solver/test_parallel.py b/qutip/tests/solver/test_parallel.py index b7836983ee..a666ab8456 100644 --- a/qutip/tests/solver/test_parallel.py +++ b/qutip/tests/solver/test_parallel.py @@ -95,13 +95,16 @@ def func(i): pytest.param(serial_map, id='serial_map'), ]) def test_map_pass_error(map): + kwargs = {} if map is loky_pmap: pytest.importorskip("loky") if map is mpi_pmap: pytest.importorskip("mpi4py") + # do not use default value for num_cpus for mpi_pmap + kwargs = {'map_kw': {'num_cpus': 1}} with pytest.raises(CustomException) as err: - map(func, range(10)) + map(func, range(10), **kwargs) assert "Error in subprocess" in str(err.value) @@ -112,13 +115,16 @@ def test_map_pass_error(map): pytest.param(serial_map, id='serial_map'), ]) def test_map_store_error(map): + map_kw = {"fail_fast": False} if map is loky_pmap: pytest.importorskip("loky") if map is mpi_pmap: pytest.importorskip("mpi4py") + # do not use default value for num_cpus for mpi_pmap + map_kw.update({'num_cpus': 1}) with pytest.raises(MapExceptions) as err: - map(func, range(10), map_kw={"fail_fast": False}) + map(func, range(10), map_kw=map_kw) map_error = err.value assert "iterations failed" in str(map_error) for iter, error in map_error.errors.items(): @@ -139,10 +145,13 @@ def test_map_store_error(map): pytest.param(serial_map, id='serial_map'), ]) def test_map_early_end(map): + kwargs = {} if map is loky_pmap: pytest.importorskip("loky") if map is mpi_pmap: pytest.importorskip("mpi4py") + # do not use default value for num_cpus for mpi_pmap + kwargs = {'map_kw': {'num_cpus': 1}} results = [] @@ -150,6 +159,6 @@ def reduce_func(result): results.append(result) return 5 - len(results) - map(_func1, range(100), reduce_func=reduce_func) + map(_func1, range(100), reduce_func=reduce_func, **kwargs) assert len(results) < 100 From 3b31f632c473ec8c41c562d872d05e130fbb842c Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Thu, 1 Feb 2024 19:05:02 +0900 Subject: [PATCH 46/66] Update workflows: remove mpi4py from extras, install it with conda on one of the test runs --- .github/workflows/build_documentation.yml | 1 - .github/workflows/tests.yml | 6 ++++-- setup.cfg | 1 - 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build_documentation.yml b/.github/workflows/build_documentation.yml index 17d818be48..b5c6eb9cf6 100644 --- a/.github/workflows/build_documentation.yml +++ b/.github/workflows/build_documentation.yml @@ -10,7 +10,6 @@ jobs: steps: - uses: actions/checkout@v3 - - uses: mpi4py/setup-mpi@v1 - uses: actions/setup-python@v4 name: Install Python diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5087eb1d23..a2395568c7 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -94,6 +94,10 @@ jobs: numpy-requirement: ">=1.26,<1.27" condaforge: 1 pytest-extra-options: "-W ignore:datetime:DeprecationWarning" + conda-extra-pkgs: "mpi4py" + # Enough to include mpi4py in one of the test runs + # Install it with conda (uses openmpi) instead of pip (uses mpich) + # because of issues with mpich # Mac # Mac has issues with MKL since september 2022. @@ -115,8 +119,6 @@ jobs: steps: - uses: actions/checkout@v3 - - uses: mpi4py/setup-mpi@v1 - - uses: conda-incubator/setup-miniconda@v2 with: auto-update-conda: true diff --git a/setup.cfg b/setup.cfg index f2184414a0..1491a2cbb2 100644 --- a/setup.cfg +++ b/setup.cfg @@ -60,7 +60,6 @@ ipython = ipython extras = loky - mpi4py tqdm ; This uses ConfigParser's string interpolation to include all the above ; dependencies into one single target, convenient for testing full builds. From a2d37418e1211c89384a8a00f6fd63d2d3294115 Mon Sep 17 00:00:00 2001 From: DnMGalan Date: Thu, 1 Feb 2024 20:31:40 +0100 Subject: [PATCH 47/66] Rename the variable inner_point_color to _inner_point_color --- qutip/bloch.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/qutip/bloch.py b/qutip/bloch.py index 236493ba04..5a918ca311 100644 --- a/qutip/bloch.py +++ b/qutip/bloch.py @@ -172,7 +172,7 @@ def __init__(self, fig=None, axes=None, view=None, figsize=None, # Old variable used in V4 to customise the color of the points self.point_color = None # List that stores the display colors for each set of points - self.inner_point_color = [] + self._inner_point_color = [] # Size of point markers, default = 25 self.point_size = [25, 32, 35, 45] # Shape of point markers, default = ['o','^','d','s'] @@ -362,7 +362,7 @@ def add_points(self, points, meth='s', colors=None, alpha=1.0): self.point_style.append(meth) self.points.append(points) self.point_alpha.append(alpha) - self.inner_point_color.append(colors) + self._inner_point_color.append(colors) def add_states(self, state, kind='vector', colors=None, alpha=1.0): """Add a state vector Qobj to Bloch sphere. @@ -801,8 +801,8 @@ def plot_points(self): marker = self.point_marker[np.mod(k, len(self.point_marker))] style = self.point_style[k] - if self.inner_point_color[k] is not None: - color = self.inner_point_color[k] + if self._inner_point_color[k] is not None: + color = self._inner_point_color[k] elif self.point_color is not None: color = self.point_color elif self.point_style[k] in ['s', 'l']: From 3e58a4b249179cbffed366c488077e87b251e5b7 Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Fri, 2 Feb 2024 13:22:45 +0900 Subject: [PATCH 48/66] mpi_pmap: document interaction with QUTIP_NUM_PROCESSES --- qutip/solver/parallel.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/qutip/solver/parallel.py b/qutip/solver/parallel.py index 16d4413370..0eca5cac99 100644 --- a/qutip/solver/parallel.py +++ b/qutip/solver/parallel.py @@ -436,8 +436,11 @@ def mpi_pmap(task, values, task_args=None, task_kwargs=None, Note: in keeping consistent with the API of `parallel_map`, the parameter determining the number of requested worker processes is called `num_cpus`. The value of `map_kw['num_cpus']` is passed to the MPIPoolExecutor as its - `max_workers` argument. Its default value is the number of logical CPUs - (i.e., threads), which might be unsuitable for MPI applications. + `max_workers` argument. + If this parameter is not provided, the environment variable + `QUTIP_NUM_PROCESSES` is used instead. If this environment variable is not + set either, QuTiP will use default values that might be unsuitable for MPI + applications. Parameters ---------- @@ -480,14 +483,15 @@ def mpi_pmap(task, values, task_args=None, task_kwargs=None, from mpi4py.futures import MPIPoolExecutor - # If the provided num_cpus is None, we use the default value instead (and - # emit a warning). We thus intentionally make it impossible to call + # If the provided num_cpus is None, we use the default value instead. + # We thus intentionally make it impossible to call # MPIPoolExecutor(max_workers=None, ...) - # in which case mpi4py would determine a default value. - # The default value provided by mpi4py would be better suited, but mpi4py - # provides no public API to access the actual number of workers that is - # used in this case, which we need. - worker_number_provided = (map_kw is not None) and ('num_cpus' in map_kw) + # in which case mpi4py would determine a default value. That would be + # useful, but unfortunately mpi4py provides no public API to access the + # actual number of workers that is used in that case, which we would need. + worker_number_provided = ( + ((map_kw is not None) and ('num_cpus' in map_kw)) + or 'QUTIP_NUM_PROCESSES' in os.environ) map_kw = _read_map_kw(map_kw) timeout = map_kw.pop('timeout') From e0bf5d3a255c0a6545b6b2f8bc8fc59b7625ae72 Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Fri, 2 Feb 2024 13:26:12 +0900 Subject: [PATCH 49/66] Fix test workflow with MPI --- .github/workflows/tests.yml | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a2395568c7..c046f02a4e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -94,10 +94,9 @@ jobs: numpy-requirement: ">=1.26,<1.27" condaforge: 1 pytest-extra-options: "-W ignore:datetime:DeprecationWarning" - conda-extra-pkgs: "mpi4py" - # Enough to include mpi4py in one of the test runs - # Install it with conda (uses openmpi) instead of pip (uses mpich) - # because of issues with mpich + # Install mpi4py to test mpi_pmap + # Should be enough to include this in one of the runs + includempi: 1 # Mac # Mac has issues with MKL since september 2022. @@ -150,6 +149,10 @@ jobs: if [[ -n "${{ matrix.conda-extra-pkgs }}" ]]; then conda install "${{ matrix.conda-extra-pkgs }}" fi + if [[ "${{ matrix.includempi }}" ]]; then + # Use openmpi because mpich causes problems. Note, environment variable names change in v5 + conda install "openmpi<5" mpi4py + fi python -m pip install -e .[$QUTIP_TARGET] python -m pip install "coverage${{ matrix.coverage-requirement }}" python -m pip install pytest-cov coveralls pytest-fail-slow @@ -181,6 +184,12 @@ jobs: # truly being executed. export QUTIP_NUM_PROCESSES=2 fi + if [[ "${{ matrix.includempi }}" ]]; then + # By default, the max. number of allowed worker processes in openmpi is + # (number of physical cpu cores) - 1. + # We only have 2 physical cores, but we want to test mpi_pmap with 2 workers. + export OMPI_MCA_rmaps_base_oversubscribe=true + fi pytest -Werror --strict-config --strict-markers --fail-slow=300 --durations=0 --durations-min=1.0 --verbosity=1 --cov=qutip --cov-report= --color=yes ${{ matrix.pytest-extra-options }} qutip/tests # Above flags are: # -Werror From e4fc3ac0e19492af6a83eff62a8d9f035f10a194 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Fri, 2 Feb 2024 13:22:57 -0500 Subject: [PATCH 50/66] Focus on RIKEN and UdeS in support section --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 376e25b3a8..dec8085f8e 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,11 @@ Support [![Powered by NumFOCUS](https://img.shields.io/badge/powered%20by-NumFOCUS-orange.svg?style=flat&colorA=E1523D&colorB=007D8A)](https://numfocus.org) We are proud to be affiliated with [Unitary Fund](https://unitary.fund) and [numFOCUS](https://numfocus.org). -QuTiP development is supported by [Nori's lab](https://dml.riken.jp/) at RIKEN, by the University of Sherbrooke, and by Aberystwyth University, [among other supporting organizations](https://qutip.org/#supporting-organizations). + +[Nori's lab](https://dml.riken.jp/) at RIKEN and [Blais' lab](https://www.physique.usherbrooke.ca/blais/) at the University of Sherbrooke +have been providing developers to work on QuTiP. + +We also thank Google for supporting us by financing GSoC student to work on the QuTiP as well as [other supporting organizations](https://qutip.org/#supporting-organizations) that have been supporting QuTiP over the years. Installation From 5c55923d2afcdf7b20ba35ba9e65be785b198224 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Fri, 2 Feb 2024 14:31:47 -0500 Subject: [PATCH 51/66] IQ instead of UdeS --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index dec8085f8e..98fab16f38 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ Support We are proud to be affiliated with [Unitary Fund](https://unitary.fund) and [numFOCUS](https://numfocus.org). -[Nori's lab](https://dml.riken.jp/) at RIKEN and [Blais' lab](https://www.physique.usherbrooke.ca/blais/) at the University of Sherbrooke +[Nori's lab](https://dml.riken.jp/) at RIKEN and [Blais' lab](https://www.physique.usherbrooke.ca/blais/) at the Institut Quantique have been providing developers to work on QuTiP. We also thank Google for supporting us by financing GSoC student to work on the QuTiP as well as [other supporting organizations](https://qutip.org/#supporting-organizations) that have been supporting QuTiP over the years. From 54f2d08ea7506251559c404354493eebfc9a8ac7 Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Tue, 6 Feb 2024 12:08:06 +0900 Subject: [PATCH 52/66] Remove bitgenerator and mpi_options from solver function docstrings, formatting --- qutip/solver/mcsolve.py | 24 +++++++++--------------- qutip/solver/nm_mcsolve.py | 20 +++++++------------- qutip/solver/stochastic.py | 31 +++++++++++-------------------- 3 files changed, 27 insertions(+), 48 deletions(-) diff --git a/qutip/solver/mcsolve.py b/qutip/solver/mcsolve.py index 72b049553c..e18f3bb97c 100644 --- a/qutip/solver/mcsolve.py +++ b/qutip/solver/mcsolve.py @@ -73,11 +73,11 @@ def mcsolve(H, state, tlist, c_ops=(), e_ops=None, ntraj=500, *, - | atol, rtol : float | Absolute and relative tolerance of the ODE integrator. - | nsteps : int - | Maximum number of (internally defined) steps allowed in one ``tlist`` - step. + | Maximum number of (internally defined) steps allowed in one + ``tlist`` step. - | max_step : float - | Maximum length of one internal step. When using pulses, it should be - less than half the width of the thinnest pulse. + | Maximum length of one internal step. When using pulses, it should + be less than half the width of the thinnest pulse. - | keep_runs_results : bool, [False] | Whether to store results from all trajectories or just store the averages. @@ -85,17 +85,9 @@ def mcsolve(H, state, tlist, c_ops=(), e_ops=None, ntraj=500, *, | How to run the trajectories. "parallel" uses the multiprocessing module to run in parallel while "loky" and "mpi" use the "loky" and "mpi4py" modules to do so. - - | mpi_options : dict - | Only applies if map is "mpi". This dictionary will be passed as - keyword arguments to the `mpi4py.futures.MPIPoolExecutor` - constructor. Note that the `max_workers` argument is provided - separately through the `num_cpus` option. - | num_cpus : int | Number of cpus to use when running in parallel. ``None`` detect the number of available cpus. - - | bitgenerator : {None, "MT19937", "PCG64", "PCG64DXSM", ...} - Which of numpy.random's bitgenerator to use. With `None`, your - numpy version's default is used. - | norm_t_tol, norm_tol, norm_steps : float, float, int | Parameters used to find the collapse location. ``norm_t_tol`` and ``norm_tol`` are the tolerance in time and norm respectively. @@ -108,7 +100,9 @@ def mcsolve(H, state, tlist, c_ops=(), e_ops=None, ntraj=500, *, | Whether to use the improved sampling algorithm from Abdelhafez et al. PRA (2019) - Additional options may be available depending on the selected + Additional options are listed under + `options <./classes.html#qutip.solver.mcsolve.MCSolver.options>`__. + More options may be available depending on the selected differential equation integration method, see `Integrator <./classes.html#classes-ode>`_. @@ -592,8 +586,8 @@ def options(self): progress_bar: str {'text', 'enhanced', 'tqdm', ''}, default: "text" How to present the solver progress. - 'tqdm' uses the python module of the same name and raise an error if - not installed. Empty string or False will disable the bar. + 'tqdm' uses the python module of the same name and raise an error + if not installed. Empty string or False will disable the bar. progress_kwargs: dict, default: {"chunk_size":10} Arguments to pass to the progress_bar. Qutip's bars use diff --git a/qutip/solver/nm_mcsolve.py b/qutip/solver/nm_mcsolve.py index 31e7cbd4a4..69c7e397a5 100644 --- a/qutip/solver/nm_mcsolve.py +++ b/qutip/solver/nm_mcsolve.py @@ -94,11 +94,11 @@ def nm_mcsolve(H, state, tlist, ops_and_rates=(), e_ops=None, ntraj=500, *, - | atol, rtol : float | Absolute and relative tolerance of the ODE integrator. - | nsteps : int - | Maximum number of (internally defined) steps allowed in one ``tlist`` - step. + | Maximum number of (internally defined) steps allowed in one + ``tlist`` step. - | max_step : float - | Maximum length of one internal step. When using pulses, it should be - less than half the width of the thinnest pulse. + | Maximum length of one internal step. When using pulses, it should + be less than half the width of the thinnest pulse. - | keep_runs_results : bool, [False] | Whether to store results from all trajectories or just store the averages. @@ -106,17 +106,9 @@ def nm_mcsolve(H, state, tlist, ops_and_rates=(), e_ops=None, ntraj=500, *, | How to run the trajectories. "parallel" uses the multiprocessing module to run in parallel while "loky" and "mpi" use the "loky" and "mpi4py" modules to do so. - - | mpi_options : dict - | Only applies if map is "mpi". This dictionary will be passed as - keyword arguments to the `mpi4py.futures.MPIPoolExecutor` - constructor. Note that the `max_workers` argument is provided - separately through the `num_cpus` option. - | num_cpus : int | Number of cpus to use when running in parallel. ``None`` detect the number of available cpus. - - | bitgenerator : {None, "MT19937", "PCG64", "PCG64DXSM", ...} - Which of numpy.random's bitgenerator to use. With `None`, your - numpy version's default is used. - | norm_t_tol, norm_tol, norm_steps : float, float, int | Parameters used to find the collapse location. ``norm_t_tol`` and ``norm_tol`` are the tolerance in time and norm respectively. @@ -135,7 +127,9 @@ def nm_mcsolve(H, state, tlist, ops_and_rates=(), e_ops=None, ntraj=500, *, integration of the martingale. Note that the 'improved_sampling' option is not currently supported. - Additional options may be available depending on the selected + Additional options are listed under `options + <./classes.html#qutip.solver.nm_mcsolve.NonMarkovianMCSolver.options>`__. + More options may be available depending on the selected differential equation integration method, see `Integrator <./classes.html#classes-ode>`_. diff --git a/qutip/solver/stochastic.py b/qutip/solver/stochastic.py index 0fcf362afe..b2f81e57df 100644 --- a/qutip/solver/stochastic.py +++ b/qutip/solver/stochastic.py @@ -9,6 +9,7 @@ from .solver_base import _solver_deprecation from ._feedback import _QobjFeedback, _DataFeedback, _WeinerFeedback + class StochasticTrajResult(Result): def _post_init(self, m_ops=(), dw_factor=(), heterodyne=False): super()._post_init() @@ -336,23 +337,18 @@ def smesolve( | How to run the trajectories. "parallel" uses the multiprocessing module to run in parallel while "loky" and "mpi" use the "loky" and "mpi4py" modules to do so. - - | mpi_options : dict - | Only applies if map is "mpi". This dictionary will be passed as - keyword arguments to the `mpi4py.futures.MPIPoolExecutor` - constructor. Note that the `max_workers` argument is provided - separately through the `num_cpus` option. - | num_cpus : NoneType, int | Number of cpus to use when running in parallel. ``None`` detect the number of available cpus. - - | bitgenerator : {None, "MT19937", "PCG64", "PCG64DXSM", ...} - Which of numpy.random's bitgenerator to use. With `None`, your - numpy version's default is used. - | dt : float | The finite steps lenght for the Stochastic integration method. Default change depending on the integrator. - Other options could be supported depending on the integration method, - see `SIntegrator <./classes.html#classes-sode>`_. + Additional options are listed under + `options <./classes.html#qutip.solver.stochastic.SMESolver.options>`__. + More options may be available depending on the selected + differential equation integration method, see + `SIntegrator <./classes.html#classes-sode>`_. Returns ------- @@ -464,23 +460,18 @@ def ssesolve( | How to run the trajectories. "parallel" uses the multiprocessing module to run in parallel while "loky" and "mpi" use the "loky" and "mpi4py" modules to do so. - - | mpi_options : dict - | Only applies if map is "mpi". This dictionary will be passed as - keyword arguments to the `mpi4py.futures.MPIPoolExecutor` - constructor. Note that the `max_workers` argument is provided - separately through the `num_cpus` option. - | num_cpus : NoneType, int | Number of cpus to use when running in parallel. ``None`` detect the number of available cpus. - - | bitgenerator : {None, "MT19937", "PCG64", "PCG64DXSM", ...} - Which of numpy.random's bitgenerator to use. With `None`, your - numpy version's default is used. - | dt : float | The finite steps lenght for the Stochastic integration method. Default change depending on the integrator. - Other options could be supported depending on the integration method, - see `SIntegrator <./classes.html#classes-sode>`_. + Additional options are listed under + `options <./classes.html#qutip.solver.stochastic.SSESolver.options>`__. + More options may be available depending on the selected + differential equation integration method, see + `SIntegrator <./classes.html#classes-sode>`_. Returns ------- From 7e5ba81941e246d749bd065a0f57d9ee2012b96a Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Tue, 6 Feb 2024 12:15:56 +0900 Subject: [PATCH 53/66] Moved initialization of map_kw to parallel module --- qutip/solver/multitraj.py | 7 ++----- qutip/solver/parallel.py | 13 ++++++++++++- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/qutip/solver/multitraj.py b/qutip/solver/multitraj.py index 4b600434df..3ca7930a96 100644 --- a/qutip/solver/multitraj.py +++ b/qutip/solver/multitraj.py @@ -1,5 +1,5 @@ from .result import Result, MultiTrajResult -from .parallel import _get_map, mpi_pmap +from .parallel import _get_map from time import time from .solver_base import Solver from ..core import QobjEvo @@ -143,10 +143,7 @@ def _initialize_run(self, state, ntraj=1, args=None, e_ops=(), ) result.add_end_condition(ntraj, target_tol) - map_func = _get_map[self.options['map']] - map_kw = {} - if map_func == mpi_pmap: - map_kw.update(self.options['mpi_options']) + map_func, map_kw = _get_map(self.options) map_kw.update({ 'timeout': timeout, 'num_cpus': self.options['num_cpus'], diff --git a/qutip/solver/parallel.py b/qutip/solver/parallel.py index 0eca5cac99..d1043a51a2 100644 --- a/qutip/solver/parallel.py +++ b/qutip/solver/parallel.py @@ -522,7 +522,7 @@ def shutdown_executor(executor, _): ) -_get_map = { +_maps = { "parallel_map": parallel_map, "parallel": parallel_map, "serial_map": serial_map, @@ -530,3 +530,14 @@ def shutdown_executor(executor, _): "loky": loky_pmap, "mpi": mpi_pmap } + + +def _get_map(options): + map_func = _get_map[options['map']] + + if map_func == mpi_pmap: + map_kw = options['mpi_options'] + else: + map_kw = {} + + return map_func, map_kw From 53f4582a8e382ff2e2939dec2a64eea665cb8632 Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Tue, 6 Feb 2024 12:23:40 +0900 Subject: [PATCH 54/66] Added mpi4py to setup.cfg again --- .github/workflows/build_documentation.yml | 3 +++ setup.cfg | 3 +++ 2 files changed, 6 insertions(+) diff --git a/.github/workflows/build_documentation.yml b/.github/workflows/build_documentation.yml index b5c6eb9cf6..9e952b0a30 100644 --- a/.github/workflows/build_documentation.yml +++ b/.github/workflows/build_documentation.yml @@ -10,6 +10,9 @@ jobs: steps: - uses: actions/checkout@v3 + - uses: mpi4py/setup-mpi@v1 + with: + mpi: 'openmpi' - uses: actions/setup-python@v4 name: Install Python diff --git a/setup.cfg b/setup.cfg index 1491a2cbb2..8a3bf41c79 100644 --- a/setup.cfg +++ b/setup.cfg @@ -61,6 +61,8 @@ ipython = extras = loky tqdm +mpi = + mpi4py ; This uses ConfigParser's string interpolation to include all the above ; dependencies into one single target, convenient for testing full builds. full = @@ -70,3 +72,4 @@ full = %(tests)s %(ipython)s %(extras)s + %(mpi)s From 8b58b587ff9e93c47b536a45aa75791490ff28c2 Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Tue, 6 Feb 2024 13:50:38 +0900 Subject: [PATCH 55/66] Typo --- qutip/solver/parallel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutip/solver/parallel.py b/qutip/solver/parallel.py index d1043a51a2..425a317f21 100644 --- a/qutip/solver/parallel.py +++ b/qutip/solver/parallel.py @@ -533,7 +533,7 @@ def shutdown_executor(executor, _): def _get_map(options): - map_func = _get_map[options['map']] + map_func = _maps[options['map']] if map_func == mpi_pmap: map_kw = options['mpi_options'] From 02f6f9b382c60b77d85d4f1184aff6408645e4be Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Tue, 6 Feb 2024 14:28:19 +0900 Subject: [PATCH 56/66] Docstring formatting mistakes --- qutip/solver/nm_mcsolve.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qutip/solver/nm_mcsolve.py b/qutip/solver/nm_mcsolve.py index 69c7e397a5..6c064bb975 100644 --- a/qutip/solver/nm_mcsolve.py +++ b/qutip/solver/nm_mcsolve.py @@ -95,7 +95,7 @@ def nm_mcsolve(H, state, tlist, ops_and_rates=(), e_ops=None, ntraj=500, *, | Absolute and relative tolerance of the ODE integrator. - | nsteps : int | Maximum number of (internally defined) steps allowed in one - ``tlist`` step. + ``tlist`` step. - | max_step : float | Maximum length of one internal step. When using pulses, it should be less than half the width of the thinnest pulse. @@ -123,7 +123,7 @@ def nm_mcsolve(H, state, tlist, ops_and_rates=(), e_ops=None, ntraj=500, *, additional Lindblad operator is added automatically (with zero rate). - | martingale_quad_limit : float or int - An upper bound on the number of subintervals used in the adaptive + | An upper bound on the number of subintervals used in the adaptive integration of the martingale. Note that the 'improved_sampling' option is not currently supported. From d550e7c82e8e4e399765649dfe03703151b620e2 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 6 Feb 2024 09:47:52 +0000 Subject: [PATCH 57/66] Only pre-compute density matrix if not stoing individual trajectories --- doc/changes/2303.feature | 1 + qutip/solver/heom/bofin_solvers.py | 14 +- qutip/solver/mcsolve.py | 5 +- qutip/solver/result.py | 346 +++++++++++++++++++---------- 4 files changed, 242 insertions(+), 124 deletions(-) create mode 100644 doc/changes/2303.feature diff --git a/doc/changes/2303.feature b/doc/changes/2303.feature new file mode 100644 index 0000000000..db293b995f --- /dev/null +++ b/doc/changes/2303.feature @@ -0,0 +1 @@ +Only pre-compute density matricies if keep_runs_results is False \ No newline at end of file diff --git a/qutip/solver/heom/bofin_solvers.py b/qutip/solver/heom/bofin_solvers.py index bb49890be2..23b82f629f 100644 --- a/qutip/solver/heom/bofin_solvers.py +++ b/qutip/solver/heom/bofin_solvers.py @@ -385,7 +385,7 @@ def _post_init(self): self.store_ados = self.options["store_ados"] if self.store_ados: - self.final_ado_state = None + self._final_ado_state = None self.ado_states = [] def _e_op_func(self, e_op): @@ -407,9 +407,17 @@ def _store_state(self, t, ado_state): self.ado_states.append(ado_state) def _store_final_state(self, t, ado_state): - self.final_state = ado_state.rho + self._final_state = ado_state.rho if self.store_ados: - self.final_ado_state = ado_state + self._final_ado_state = ado_state + + @property + def final_ado_state(self): + if self._final_ado_state is not None: + return self._final_state + if self.ado_states: + return self.ado_states[-1] + return None def heomsolve( diff --git a/qutip/solver/mcsolve.py b/qutip/solver/mcsolve.py index a55b1a094c..fb60f5fdd5 100644 --- a/qutip/solver/mcsolve.py +++ b/qutip/solver/mcsolve.py @@ -167,6 +167,7 @@ class _MCSystem(_MTSystem): """ Container for the operators of the solver. """ + def __init__(self, rhs, c_ops, n_ops): self.rhs = rhs self.c_ops = c_ops @@ -247,8 +248,8 @@ def set_state(self, t, state0, generator, self.target_norm = 0.0 else: self.target_norm = ( - self._generator.random() * (1 - jump_prob_floor) - + jump_prob_floor + self._generator.random() * (1 - jump_prob_floor) + + jump_prob_floor ) self._integrator.set_state(t, state0) self._is_set = True diff --git a/qutip/solver/result.py b/qutip/solver/result.py index b1d1bf2bbd..0778a5cfd9 100644 --- a/qutip/solver/result.py +++ b/qutip/solver/result.py @@ -1,9 +1,17 @@ """ Class for solve function results""" + +from typing import TypedDict import numpy as np from ..core import Qobj, QobjEvo, expect, isket, ket2dm, qzero_like -__all__ = ["Result", "MultiTrajResult", "McResult", "NmmcResult", - "McTrajectoryResult", "McResultImprovedSampling"] +__all__ = [ + "Result", + "MultiTrajResult", + "McResult", + "NmmcResult", + "McTrajectoryResult", + "McResultImprovedSampling", +] class _QobjExpectEop: @@ -16,6 +24,7 @@ class _QobjExpectEop: op : :obj:`.Qobj` The expectation value operator. """ + def __init__(self, op): self.op = op @@ -46,6 +55,7 @@ class ExpectOp: op : object The original object used to define the e_op operation. """ + def __init__(self, op, f, append): self.op = op self._f = f @@ -70,6 +80,7 @@ class _BaseResult: """ Common method for all ``Result``. """ + def __init__(self, options, *, solver=None, stats=None): self.solver = solver if stats is None: @@ -81,12 +92,12 @@ def __init__(self, options, *, solver=None, stats=None): # make sure not to store a reference to the solver options_copy = options.copy() - if hasattr(options_copy, '_feedback'): + if hasattr(options_copy, "_feedback"): options_copy._feedback = None self.options = options_copy def _e_ops_to_dict(self, e_ops): - """ Convert the supplied e_ops to a dictionary of Eop instances. """ + """Convert the supplied e_ops to a dictionary of Eop instances.""" if e_ops is None: e_ops = {} elif isinstance(e_ops, (list, tuple)): @@ -118,6 +129,11 @@ def add_processor(self, f, requires_copy=False): self._state_processors_require_copy |= requires_copy +class ResultOptions(TypedDict): + store_states: bool + store_final_state: bool + + class Result(_BaseResult): """ Base class for storing solver results. @@ -199,7 +215,18 @@ class Result(_BaseResult): options : dict The options for this result class. """ - def __init__(self, e_ops, options, *, solver=None, stats=None, **kw): + + options: ResultOptions + + def __init__( + self, + e_ops, + options: ResultOptions, + *, + solver=None, + stats=None, + **kw, + ): super().__init__(options, solver=solver, stats=stats) raw_ops = self._e_ops_to_dict(e_ops) self.e_data = {k: [] for k in raw_ops} @@ -211,7 +238,7 @@ def __init__(self, e_ops, options, *, solver=None, stats=None, **kw): self.times = [] self.states = [] - self.final_state = None + self._final_state = None self._post_init(**kw) @@ -243,29 +270,29 @@ def _post_init(self): Sub-class ``.post_init()`` implementation may take additional keyword arguments if required. """ - store_states = self.options['store_states'] - store_final_state = self.options['store_final_state'] - - if store_states is None: - store_states = len(self.e_ops) == 0 + store_states = self.options["store_states"] + store_states = store_states or ( + len(self.e_ops) == 0 and store_states is None + ) if store_states: self.add_processor(self._store_state, requires_copy=True) - if store_states or store_final_state: + store_final_state = self.options["store_final_state"] + if store_final_state and not store_states: self.add_processor(self._store_final_state, requires_copy=True) def _store_state(self, t, state): - """ Processor that stores a state in ``.states``. """ + """Processor that stores a state in ``.states``.""" self.states.append(state) def _store_final_state(self, t, state): - """ Processor that writes the state to ``.final_state``. """ - self.final_state = state + """Processor that writes the state to ``._final_state``.""" + self._final_state = state def _pre_copy(self, state): - """ Return a copy of the state. Sub-classes may override this to - copy a state in different manner or to skip making a copy - altogether if a copy is not necessary. + """Return a copy of the state. Sub-classes may override this to + copy a state in different manner or to skip making a copy + altogether if a copy is not necessary. """ return state.copy() @@ -311,10 +338,7 @@ def __repr__(self): ] if self.stats: lines.append(" Solver stats:") - lines.extend( - f" {k}: {v!r}" - for k, v in self.stats.items() - ) + lines.extend(f" {k}: {v!r}" for k, v in self.stats.items()) if self.times: lines.append( f" Time interval: [{self.times[0]}, {self.times[-1]}]" @@ -334,6 +358,20 @@ def __repr__(self): def expect(self): return [np.array(e_op) for e_op in self.e_data.values()] + @property + def final_state(self): + if self._final_state is not None: + return self._final_state + if self.states: + return self.states[-1] + return None + + +class MultiTrajResultOptions(TypedDict): + store_states: bool + store_final_state: bool + keep_runs_results: bool + class MultiTrajResult(_BaseResult): """ @@ -455,7 +493,18 @@ class MultiTrajResult(_BaseResult): options : :obj:`~SolverResultsOptions` The options for this result class. """ - def __init__(self, e_ops, options, *, solver=None, stats=None, **kw): + + options: MultiTrajResultOptions + + def __init__( + self, + e_ops, + options: MultiTrajResultOptions, + *, + solver=None, + stats=None, + **kw, + ): super().__init__(options, solver=solver, stats=stats) self._raw_ops = self._e_ops_to_dict(e_ops) @@ -471,15 +520,29 @@ def __init__(self, e_ops, options, *, solver=None, stats=None, **kw): self._target_tols = None self.average_e_data = {} - self.e_data = {} self.std_e_data = {} self.runs_e_data = {} self._post_init(**kw) + @property + def _store_average_density_matricies(self) -> bool: + return ( + self.options["store_states"] + or (self.options["store_states"] is None and self._raw_ops == {}) + ) and not self.options["keep_runs_results"] + + @property + def _store_final_density_matrix(self) -> bool: + return ( + self.options["store_final_state"] + and not self._store_average_density_matricies + and not self.options["keep_runs_results"] + ) + @staticmethod def _to_dm(state): - if state.type == 'ket': + if state.type == "ket": state = state.proj() return state @@ -489,10 +552,12 @@ def _add_first_traj(self, trajectory): """ self.times = trajectory.times - if trajectory.states: - self._sum_states = [qzero_like(self._to_dm(state)) - for state in trajectory.states] - if trajectory.final_state: + if trajectory.states and self._store_average_density_matricies: + self._sum_states = [ + qzero_like(self._to_dm(state)) for state in trajectory.states + ] + + if trajectory.final_state and self._store_final_density_matrix: state = trajectory.final_state self._sum_final_states = qzero_like(self._to_dm(state)) @@ -507,13 +572,10 @@ def _add_first_traj(self, trajectory): self.average_e_data = { k: list(avg_expect) - for k, avg_expect - in zip(self._raw_ops, self._sum_expect) + for k, avg_expect in zip(self._raw_ops, self._sum_expect) } - self.e_data = self.average_e_data - if self.options['keep_runs_results']: + if self.options["keep_runs_results"]: self.runs_e_data = {k: [] for k in self._raw_ops} - self.e_data = self.runs_e_data def _store_trajectory(self, trajectory): self.trajectories.append(trajectory) @@ -521,8 +583,7 @@ def _store_trajectory(self, trajectory): def _reduce_states(self, trajectory): self._sum_states = [ accu + self._to_dm(state) - for accu, state - in zip(self._sum_states, trajectory.states) + for accu, state in zip(self._sum_states, trajectory.states) ] def _reduce_final_state(self, trajectory): @@ -569,7 +630,7 @@ def _fixed_end(self): """ ntraj_left = self._target_ntraj - self.num_trajectories if ntraj_left == 0: - self.stats['end_condition'] = 'ntraj reached' + self.stats["end_condition"] = "ntraj reached" return ntraj_left def _average_computer(self): @@ -586,40 +647,36 @@ def _target_tolerance_end(self): if self.num_trajectories <= 1: return np.inf avg, avg2 = self._average_computer() - target = np.array([ - atol + rtol * mean - for mean, (atol, rtol) - in zip(avg, self._target_tols) - ]) + target = np.array( + [ + atol + rtol * mean + for mean, (atol, rtol) in zip(avg, self._target_tols) + ] + ) target_ntraj = np.max((avg2 - abs(avg) ** 2) / target**2 + 1) self._estimated_ntraj = min(target_ntraj, self._target_ntraj) if (self._estimated_ntraj - self.num_trajectories) <= 0: - self.stats['end_condition'] = 'target tolerance reached' + self.stats["end_condition"] = "target tolerance reached" return self._estimated_ntraj - self.num_trajectories def _post_init(self): self.num_trajectories = 0 self._target_ntraj = None - store_states = self.options['store_states'] - store_final_state = self.options['store_final_state'] - store_traj = self.options['keep_runs_results'] - self.add_processor(self._increment_traj) - if store_traj: + store_trajectory = self.options["keep_runs_results"] + if store_trajectory: self.add_processor(self._store_trajectory) - if store_states is None: - store_states = len(self._raw_ops) == 0 - if store_states: + if self._store_average_density_matricies: self.add_processor(self._reduce_states) - if store_states or store_final_state: + if self._store_final_density_matrix: self.add_processor(self._reduce_final_state) if self._raw_ops: self.add_processor(self._reduce_expect) self._early_finish_check = self._no_end - self.stats['end_condition'] = 'unknown' + self.stats["end_condition"] = "unknown" def add(self, trajectory_info): """ @@ -676,7 +733,7 @@ def add_end_condition(self, ntraj, target_tol=None): Error estimation is done with jackknife resampling. """ self._target_ntraj = ntraj - self.stats['end_condition'] = 'timeout' + self.stats["end_condition"] = "timeout" if target_tol is None: self._early_finish_check = self._fixed_end @@ -691,14 +748,16 @@ def add_end_condition(self, ntraj, target_tol=None): targets = np.array(target_tol) if targets.ndim == 0: - self._target_tols = np.array([(target_tol, 0.)] * num_e_ops) + self._target_tols = np.array([(target_tol, 0.0)] * num_e_ops) elif targets.shape == (2,): self._target_tols = np.ones((num_e_ops, 2)) * targets elif targets.shape == (num_e_ops, 2): self._target_tols = targets else: - raise ValueError("target_tol must be a number, a pair of (atol, " - "rtol) or a list of (atol, rtol) for each e_ops") + raise ValueError( + "target_tol must be a number, a pair of (atol, " + "rtol) or a list of (atol, rtol) for each e_ops" + ) self._early_finish_check = self._target_tolerance_end @@ -718,8 +777,18 @@ def average_states(self): States averages as density matrices. """ if self._sum_states is None: - return None - return [final / self.num_trajectories for final in self._sum_states] + if not (self.trajectories and self.trajectories[0].states): + return None + self._sum_states = [ + qzero_like(self._to_dm(state)) + for state in self.trajectories[0].states + ] + for trajectory in self.trajectories: + self._reduce_states(trajectory) + + return [ + final / self.num_trajectories for final in self._sum_states + ] @property def states(self): @@ -744,6 +813,8 @@ def average_final_state(self): Last states of each trajectories averaged into a density matrix. """ if self._sum_final_states is None: + if self.average_states is not None: + return self.average_states[-1] return None return self._sum_final_states / self.num_trajectories @@ -770,6 +841,10 @@ def runs_expect(self): def expect(self): return [np.array(val) for val in self.e_data.values()] + @property + def e_data(self): + return self.runs_e_data or self.average_e_data + def steady_state(self, N=0): """ Average the states of the last ``N`` times of every runs as a density @@ -796,16 +871,13 @@ def __repr__(self): ] if self.stats: lines.append(" Solver stats:") - lines.extend( - f" {k}: {v!r}" - for k, v in self.stats.items() - ) + lines.extend(f" {k}: {v!r}" for k, v in self.stats.items()) if self.times: lines.append( f" Time interval: [{self.times[0]}, {self.times[-1]}]" f" ({len(self.times)} steps)" ) - lines.append(f" Number of e_ops: {len(self.e_ops)}") + lines.append(f" Number of e_ops: {len(self.e_data)}") if self.states: lines.append(" States saved.") elif self.final_state is not None: @@ -827,23 +899,30 @@ def __add__(self, other): raise ValueError("Shared `e_ops` is required to merge results") if self.times != other.times: raise ValueError("Shared `times` are is required to merge results") - new = self.__class__(self._raw_ops, self.options, - solver=self.solver, stats=self.stats) + new = self.__class__( + self._raw_ops, self.options, solver=self.solver, stats=self.stats + ) if self.trajectories and other.trajectories: new.trajectories = self.trajectories + other.trajectories new.num_trajectories = self.num_trajectories + other.num_trajectories new.times = self.times new.seeds = self.seeds + other.seeds - if self._sum_states is not None and other._sum_states is not None: - new._sum_states = self._sum_states + other._sum_states + if ( + self._sum_states is not None + and other._sum_states is not None + ): + new._sum_states = ( + self._sum_states + other._sum_states + ) if ( self._sum_final_states is not None and other._sum_final_states is not None ): new._sum_final_states = ( - self._sum_final_states + other._sum_final_states + self._sum_final_states + + other._sum_final_states ) new._target_tols = None @@ -854,23 +933,21 @@ def __add__(self, other): for i, k in enumerate(self._raw_ops): new._sum_expect.append(self._sum_expect[i] + other._sum_expect[i]) - new._sum2_expect.append(self._sum2_expect[i] - + other._sum2_expect[i]) + new._sum2_expect.append( + self._sum2_expect[i] + other._sum2_expect[i] + ) avg = new._sum_expect[i] / new.num_trajectories avg2 = new._sum2_expect[i] / new.num_trajectories new.average_e_data[k] = list(avg) - new.e_data = new.average_e_data - new.std_e_data[k] = np.sqrt(np.abs(avg2 - np.abs(avg**2))) - if new.trajectories: + if self.runs_e_data and other.runs_e_data: new.runs_e_data[k] = self.runs_e_data[k] + other.runs_e_data[k] - new.e_data = new.runs_e_data new.stats["run time"] += other.stats["run time"] - new.stats['end_condition'] = "Merged results" + new.stats["end_condition"] = "Merged results" return new @@ -881,8 +958,9 @@ class McTrajectoryResult(Result): """ def __init__(self, e_ops, options, *args, **kwargs): - super().__init__(e_ops, {**options, "normalize_output": False}, - *args, **kwargs) + super().__init__( + e_ops, {**options, "normalize_output": False}, *args, **kwargs + ) class McResult(MultiTrajResult): @@ -922,6 +1000,7 @@ class McResult(MultiTrajResult): For each runs, a list of every collapse as a tuple of the time it happened and the corresponding ``c_ops`` index. """ + # Collapse are only produced by mcsolve. def _add_collapse(self, trajectory): @@ -941,7 +1020,7 @@ def col_times(self): out = [] for col_ in self.collapse: col = list(zip(*col_)) - col = ([] if len(col) == 0 else col[0]) + col = [] if len(col) == 0 else col[0] out.append(col) return out @@ -953,7 +1032,7 @@ def col_which(self): out = [] for col_ in self.collapse: col = list(zip(*col_)) - col = ([] if len(col) == 0 else col[1]) + col = [] if len(col) == 0 else col[1] out.append(col) return out @@ -968,7 +1047,9 @@ def photocurrent(self): for t, which in collapses: cols[which].append(t) mesurement = [ - np.histogram(cols[i], tlist)[0] / np.diff(tlist) / self.num_trajectories + np.histogram(cols[i], tlist)[0] + / np.diff(tlist) + / self.num_trajectories for i in range(self.num_c_ops) ] return mesurement @@ -984,10 +1065,12 @@ def runs_photocurrent(self): cols = [[] for _ in range(self.num_c_ops)] for t, which in collapses: cols[which].append(t) - measurements.append([ - np.histogram(cols[i], tlist)[0] / np.diff(tlist) - for i in range(self.num_c_ops) - ]) + measurements.append( + [ + np.histogram(cols[i], tlist)[0] / np.diff(tlist) + for i in range(self.num_c_ops) + ] + ) return measurements @@ -998,6 +1081,7 @@ class McResultImprovedSampling(McResult, MultiTrajResult): using the improved sampling algorithm, which samples the no-jump trajectory first and then only samples jump trajectories afterwards. """ + def __init__(self, e_ops, options, **kw): MultiTrajResult.__init__(self, e_ops=e_ops, options=options, **kw) self._sum_expect_no_jump = None @@ -1016,14 +1100,16 @@ def _reduce_states(self, trajectory): if self.num_trajectories == 1: self._sum_states_no_jump = [ accu + self._to_dm(state) - for accu, state - in zip(self._sum_states_no_jump, trajectory.states) + for accu, state in zip( + self._sum_states_no_jump, trajectory.states + ) ] else: self._sum_states_jump = [ accu + self._to_dm(state) - for accu, state - in zip(self._sum_states_jump, trajectory.states) + for accu, state in zip( + self._sum_states_jump, trajectory.states + ) ] def _reduce_final_state(self, trajectory): @@ -1040,13 +1126,15 @@ def _average_computer(self): def _add_first_traj(self, trajectory): super()._add_first_traj(trajectory) - if trajectory.states: + if trajectory.states and self._store_average_density_matricies: del self._sum_states - self._sum_states_no_jump = [qzero_like(self._to_dm(state)) - for state in trajectory.states] - self._sum_states_jump = [qzero_like(self._to_dm(state)) - for state in trajectory.states] - if trajectory.final_state: + self._sum_states_no_jump = [ + qzero_like(self._to_dm(state)) for state in trajectory.states + ] + self._sum_states_jump = [ + qzero_like(self._to_dm(state)) for state in trajectory.states + ] + if trajectory.final_state and self._store_final_density_matrix: state = trajectory.final_state del self._sum_final_states self._sum_final_states_no_jump = qzero_like(self._to_dm(state)) @@ -1063,10 +1151,12 @@ def _add_first_traj(self, trajectory): self._sum2_expect_no_jump = [ np.zeros_like(expect) for expect in trajectory.expect ] - self._sum_expect_jump = [np.zeros_like(expect) - for expect in trajectory.expect] - self._sum2_expect_jump = [np.zeros_like(expect) - for expect in trajectory.expect] + self._sum_expect_jump = [ + np.zeros_like(expect) for expect in trajectory.expect + ] + self._sum2_expect_jump = [ + np.zeros_like(expect) for expect in trajectory.expect + ] del self._sum_expect del self._sum2_expect @@ -1088,12 +1178,12 @@ def _reduce_expect(self, trajectory): else: self._sum_expect_jump[i] += expect_traj * (1 - p) self._sum2_expect_jump[i] += expect_traj**2 * (1 - p) - avg = (self._sum_expect_no_jump[i] - + self._sum_expect_jump[i] - / (self.num_trajectories - 1)) - avg2 = (self._sum2_expect_no_jump[i] - + self._sum2_expect_jump[i] - / (self.num_trajectories - 1)) + avg = self._sum_expect_no_jump[i] + ( + self._sum_expect_jump[i] / (self.num_trajectories - 1) + ) + avg2 = self._sum2_expect_no_jump[i] + ( + self._sum2_expect_jump[i] / (self.num_trajectories - 1) + ) self.average_e_data[k] = list(avg) @@ -1110,12 +1200,28 @@ def average_states(self): States averages as density matrices. """ if self._sum_states_no_jump is None: - return None + if not (self.trajectories and self.trajectories[0].states): + return None + self._sum_states_no_jump = [ + qzero_like(self._to_dm(state)) + for state in self.trajectories[0].states + ] + self._sum_states_jump = [ + qzero_like(self._to_dm(state)) + for state in self.trajectories[0].states + ] + self.num_trajectories = 0 + for trajectory in self.trajectories: + self.num_trajectories += 1 + self._reduce_states(trajectory) p = self.no_jump_prob - return [p * final_no_jump - + (1 - p) * final_jump / (self.num_trajectories - 1) - for final_no_jump, final_jump in - zip(self._sum_states_no_jump, self._sum_states_jump)] + return [ + p * final_no_jump + + (1 - p) * final_jump / (self.num_trajectories - 1) + for final_no_jump, final_jump in zip( + self._sum_states_no_jump, self._sum_states_jump + ) + ] @property def average_final_state(self): @@ -1123,11 +1229,11 @@ def average_final_state(self): Last states of each trajectory averaged into a density matrix. """ if self._sum_final_states_no_jump is None: - return None + if self.average_states is not None: + return self.average_states[-1] p = self.no_jump_prob - return ( - p * self._sum_final_states_no_jump - + (1 - p) * self._sum_final_states_jump + return p * self._sum_final_states_no_jump + ( + ((1 - p) * self._sum_final_states_jump) / (self.num_trajectories - 1) ) @@ -1145,8 +1251,10 @@ def photocurrent(self): for t, which in collapses: cols[which].append(t) mesurement = [ - (1 - self.no_jump_prob) / (self.num_trajectories - 1) * - np.histogram(cols[i], tlist)[0] / np.diff(tlist) + (1 - self.no_jump_prob) + / (self.num_trajectories - 1) + * np.histogram(cols[i], tlist)[0] + / np.diff(tlist) for i in range(self.num_c_ops) ] return mesurement @@ -1241,15 +1349,15 @@ def _add_first_traj(self, trajectory): def _add_trace(self, trajectory): new_trace = np.array(trajectory.trace) self._sum_trace += new_trace - self._sum2_trace += np.abs(new_trace)**2 + self._sum2_trace += np.abs(new_trace) ** 2 avg = self._sum_trace / self.num_trajectories avg2 = self._sum2_trace / self.num_trajectories self.average_trace = avg - self.std_trace = np.sqrt(np.abs(avg2 - np.abs(avg)**2)) + self.std_trace = np.sqrt(np.abs(avg2 - np.abs(avg) ** 2)) - if self.options['keep_runs_results']: + if self.options["keep_runs_results"]: self.runs_trace.append(trajectory.trace) @property From 5143be062679bc8570749361fc851dfc6cb002fa Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Tue, 6 Feb 2024 16:38:58 -0500 Subject: [PATCH 58/66] Remove mpi from full and update versions --- doc/rtd-environment.yml | 6 +++--- setup.cfg | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/doc/rtd-environment.yml b/doc/rtd-environment.yml index dd4e9f26ca..7cafb0adc0 100644 --- a/doc/rtd-environment.yml +++ b/doc/rtd-environment.yml @@ -8,7 +8,7 @@ dependencies: - certifi==2022.12.7 - chardet==4.0.0 - cycler==0.10.0 -- Cython==0.29.33 +- Cython==3.0.8 - decorator==5.1.1 - docutils==0.18.1 - idna==3.4 @@ -19,7 +19,7 @@ dependencies: - kiwisolver==1.4.4 - MarkupSafe==2.1.2 - matplotlib==3.7.1 -- numpy==1.24.2 +- numpy==1.25.2 - numpydoc==1.5.0 - packaging==23.0 - parso==0.8.3 @@ -33,7 +33,7 @@ dependencies: - python-dateutil==2.8.2 - pytz==2023.3 - requests==2.28.2 -- scipy==1.10.1 +- scipy==1.11.4 - six==1.16.0 - snowballstemmer==2.2.0 - Sphinx==6.1.3 diff --git a/setup.cfg b/setup.cfg index 8a3bf41c79..ecb3cc0ee4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -72,4 +72,3 @@ full = %(tests)s %(ipython)s %(extras)s - %(mpi)s From 7b0cca5e2a7ddf1d69b3df0426d402cf84c59eb7 Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Wed, 7 Feb 2024 11:27:47 +0900 Subject: [PATCH 59/66] Remove setup-mpi step from build documentation workflow --- .github/workflows/build_documentation.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/build_documentation.yml b/.github/workflows/build_documentation.yml index 9e952b0a30..b5c6eb9cf6 100644 --- a/.github/workflows/build_documentation.yml +++ b/.github/workflows/build_documentation.yml @@ -10,9 +10,6 @@ jobs: steps: - uses: actions/checkout@v3 - - uses: mpi4py/setup-mpi@v1 - with: - mpi: 'openmpi' - uses: actions/setup-python@v4 name: Install Python From 1316bd6f2e8243af7e1c88bb8fa43d0ee8245d57 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Thu, 8 Feb 2024 15:26:31 -0500 Subject: [PATCH 60/66] Ensure final dtype in Qobj creation functions. --- qutip/core/operators.py | 9 ++++++--- qutip/core/states.py | 33 ++++++++++++++++++++++++--------- 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/qutip/core/operators.py b/qutip/core/operators.py index 2ef1f3ff03..0b3a99f266 100644 --- a/qutip/core/operators.py +++ b/qutip/core/operators.py @@ -588,6 +588,7 @@ def _f_op(n_sites, site, action, dtype=None): oper : qobj Qobj for destruction operator. """ + dtype = dtype or settings.core["default_dtype"] or _data.CSR # get `tensor` and sigma z objects from .tensor import tensor s_z = 2 * jmat(0.5, 'z', dtype=dtype) @@ -614,7 +615,7 @@ def _f_op(n_sites, site, action, dtype=None): eye = identity(2, dtype=dtype) opers = [s_z] * site + [operator] + [eye] * (n_sites - site - 1) - return tensor(opers) + return tensor(opers).to(dtype) def _implicit_tensor_dimensions(dimensions): @@ -798,10 +799,11 @@ def position(N, offset=0, *, dtype=None): oper : qobj Position operator as Qobj. """ + dtype = dtype or settings.core["default_dtype"] or _data.Dia a = destroy(N, offset=offset, dtype=dtype) position = np.sqrt(0.5) * (a + a.dag()) position.isherm = True - return position + return position.to(dtype) def momentum(N, offset=0, *, dtype=None): @@ -826,10 +828,11 @@ def momentum(N, offset=0, *, dtype=None): oper : qobj Momentum operator as Qobj. """ + dtype = dtype or settings.core["default_dtype"] or _data.Dia a = destroy(N, offset=offset, dtype=dtype) momentum = -1j * np.sqrt(0.5) * (a - a.dag()) momentum.isherm = True - return momentum + return momentum.to(dtype) def num(N, offset=0, *, dtype=None): diff --git a/qutip/core/states.py b/qutip/core/states.py index 79bc89529a..a5139b46f9 100644 --- a/qutip/core/states.py +++ b/qutip/core/states.py @@ -295,7 +295,9 @@ def coherent_dm(N, alpha, offset=0, method='operator', *, dtype=None): """ dtype = dtype or settings.core["default_dtype"] or _data.Dense - return coherent(N, alpha, offset=offset, method=method, dtype=dtype).proj() + return coherent( + N, alpha, offset=offset, method=method, dtype=dtype + ).proj().to(dtype) def fock_dm(dimensions, n=None, offset=None, *, dtype=None): @@ -340,7 +342,7 @@ def fock_dm(dimensions, n=None, offset=None, *, dtype=None): """ dtype = dtype or settings.core["default_dtype"] or _data.Dia - return basis(dimensions, n, offset=offset, dtype=dtype).proj() + return basis(dimensions, n, offset=offset, dtype=dtype).proj().to(dtype) def fock(dimensions, n=None, offset=None, *, dtype=None): @@ -550,8 +552,10 @@ def projection(N, n, m, offset=None, *, dtype=None): Requested projection operator. """ dtype = dtype or settings.core["default_dtype"] or _data.CSR - return basis(N, n, offset=offset, dtype=dtype) @ \ - basis(N, m, offset=offset, dtype=dtype).dag() + return ( + basis(N, n, offset=offset, dtype=dtype) @ \ + basis(N, m, offset=offset, dtype=dtype).dag() + ).to(dtype) def qstate(string, *, dtype=None): @@ -1154,10 +1158,15 @@ def triplet_states(*, dtype=None): trip_states : list 2 particle triplet states """ + dtype = dtype or settings.core["default_dtype"] or _data.Dense return [ basis([2, 2], [1, 1], dtype=dtype), - np.sqrt(0.5) * (basis([2, 2], [0, 1], dtype=dtype) + - basis([2, 2], [1, 0], dtype=dtype)), + ( + np.sqrt(0.5) * ( + basis([2, 2], [0, 1], dtype=dtype) + + basis([2, 2], [1, 0], dtype=dtype) + ) + ).to(dtype), basis([2, 2], [0, 0], dtype=dtype), ] @@ -1181,12 +1190,13 @@ def w_state(N=3, *, dtype=None): W : :obj:`.Qobj` N-qubit W-state """ + dtype = dtype or settings.core["default_dtype"] or _data.Dense inds = np.zeros(N, dtype=int) inds[0] = 1 state = basis([2]*N, list(inds), dtype=dtype) for kk in range(1, N): state += basis([2]*N, list(np.roll(inds, kk)), dtype=dtype) - return np.sqrt(1 / N) * state + return (np.sqrt(1 / N) * state).to(dtype) def ghz_state(N=3, *, dtype=None): @@ -1208,5 +1218,10 @@ def ghz_state(N=3, *, dtype=None): G : qobj N-qubit GHZ-state """ - return np.sqrt(0.5) * (basis([2]*N, [0]*N, dtype=dtype) + - basis([2]*N, [1]*N, dtype=dtype)) + dtype = dtype or settings.core["default_dtype"] or _data.Dense + return ( + np.sqrt(0.5) * ( + basis([2]*N, [0]*N, dtype=dtype) + + basis([2]*N, [1]*N, dtype=dtype) + ) + ).to(dtype) From 42fe13305c1ae12353ba8a720321ba2ef82f63e2 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Tue, 13 Feb 2024 11:11:06 -0500 Subject: [PATCH 61/66] Reword --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 98fab16f38..af98f064bf 100644 --- a/README.md +++ b/README.md @@ -49,8 +49,8 @@ Support We are proud to be affiliated with [Unitary Fund](https://unitary.fund) and [numFOCUS](https://numfocus.org). -[Nori's lab](https://dml.riken.jp/) at RIKEN and [Blais' lab](https://www.physique.usherbrooke.ca/blais/) at the Institut Quantique -have been providing developers to work on QuTiP. +We are grateful for [Nori's lab](https://dml.riken.jp/) at RIKEN and [Blais' lab](https://www.physique.usherbrooke.ca/blais/) at the Institut Quantique +for providing developer positions to work on QuTiP. We also thank Google for supporting us by financing GSoC student to work on the QuTiP as well as [other supporting organizations](https://qutip.org/#supporting-organizations) that have been supporting QuTiP over the years. From 05cdbb0aae8f6d2d6894c5ce60e93ef785c8f491 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eric=20Gigu=C3=A8re?= Date: Tue, 13 Feb 2024 14:37:58 -0500 Subject: [PATCH 62/66] Update README.md Co-authored-by: Simon Cross --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index af98f064bf..ba6dbf7c6c 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ We are proud to be affiliated with [Unitary Fund](https://unitary.fund) and [num We are grateful for [Nori's lab](https://dml.riken.jp/) at RIKEN and [Blais' lab](https://www.physique.usherbrooke.ca/blais/) at the Institut Quantique for providing developer positions to work on QuTiP. -We also thank Google for supporting us by financing GSoC student to work on the QuTiP as well as [other supporting organizations](https://qutip.org/#supporting-organizations) that have been supporting QuTiP over the years. +We also thank Google for supporting us by financing GSoC students to work on the QuTiP as well as [other supporting organizations](https://qutip.org/#supporting-organizations) that have been supporting QuTiP over the years. Installation From 77def926e817c4fe58ee23f9855e87e949ca2f6f Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Wed, 14 Feb 2024 10:43:54 -0500 Subject: [PATCH 63/66] Add dtype to Qobj(Evo) and use it in ..._like functions --- qutip/core/cy/_element.pyx | 12 ++++++++++++ qutip/core/cy/qobjevo.pyx | 15 +++++++++++++++ qutip/core/operators.py | 15 ++++++--------- qutip/core/qobj.py | 4 ++++ qutip/tests/core/test_operators.py | 4 ++++ qutip/tests/core/test_qobj.py | 6 ++++++ qutip/tests/core/test_qobjevo.py | 16 ++++++++++++++++ 7 files changed, 63 insertions(+), 9 deletions(-) diff --git a/qutip/core/cy/_element.pyx b/qutip/core/cy/_element.pyx index cfe3154742..d77f3e6dfd 100644 --- a/qutip/core/cy/_element.pyx +++ b/qutip/core/cy/_element.pyx @@ -267,6 +267,10 @@ cdef class _BaseElement: return new.qobj(t) * new.coeff(t) return self.qobj(t) * self.coeff(t) + @property + def dtype(self): + return None + cdef class _ConstantElement(_BaseElement): """ @@ -313,6 +317,10 @@ cdef class _ConstantElement(_BaseElement): def __call__(self, t, args=None): return self._qobj + @property + def dtype(self): + return type(self._data) + cdef class _EvoElement(_BaseElement): """ @@ -370,6 +378,10 @@ cdef class _EvoElement(_BaseElement): self._coefficient.replace_arguments(args) ) + @property + def dtype(self): + return type(self._data) + cdef class _FuncElement(_BaseElement): """ diff --git a/qutip/core/cy/qobjevo.pyx b/qutip/core/cy/qobjevo.pyx index c2b4d5d3ae..f508189c31 100644 --- a/qutip/core/cy/qobjevo.pyx +++ b/qutip/core/cy/qobjevo.pyx @@ -947,6 +947,21 @@ cdef class QobjEvo: """Indicates if the system represents a operator-bra state.""" return self._dims.type == 'operator-bra' + @property + def dtype(self): + """ + Type of the data layers of the QobjEvo. + When different data layers are used, we return the type of the sum of + the parts. + """ + part_types = [part.dtype for part in self.elements] + if ( + part_types[0] is not None + and all(part == part_types[0] for part in part_types) + ): + return part_types[0] + return self(0).dtype + ########################################################################### # operation methods # ########################################################################### diff --git a/qutip/core/operators.py b/qutip/core/operators.py index 2ef1f3ff03..c89e170959 100644 --- a/qutip/core/operators.py +++ b/qutip/core/operators.py @@ -691,11 +691,9 @@ def qzero_like(qobj): Zero operator Qobj. """ - from .cy.qobjevo import QobjEvo - if isinstance(qobj, QobjEvo): - qobj = qobj(0) + return Qobj( - _data.zeros_like(qobj.data), dims=qobj._dims, + _data.zeros[qobj.dtype](*qobj.shape), dims=qobj._dims, isherm=True, isunitary=False, copy=False ) @@ -767,12 +765,11 @@ def qeye_like(qobj): Identity operator Qobj. """ - from .cy.qobjevo import QobjEvo - if isinstance(qobj, QobjEvo): - qobj = qobj(0) + if qobj.shape[0] != qobj.shape[1]: + raise ValueError(f"Cannot build a {qobj.shape} identity matrix.") return Qobj( - _data.identity_like(qobj.data), dims=qobj._dims, - isherm=True, isunitary=True, copy=False + _data.identity[qobj.dtype](qobj.shape[0]), dims=qobj._dims, + isherm=True, isunitary=False, copy=False ) diff --git a/qutip/core/qobj.py b/qutip/core/qobj.py index bdc72ca67e..2b4dcda24f 100644 --- a/qutip/core/qobj.py +++ b/qutip/core/qobj.py @@ -330,6 +330,10 @@ def superrep(self, super_rep): def data(self): return self._data + @property + def dtype(self): + return type(self._data) + @data.setter def data(self, data): if not isinstance(data, _data.Data): diff --git a/qutip/tests/core/test_operators.py b/qutip/tests/core/test_operators.py index 46fa020481..77eeb70ff3 100644 --- a/qutip/tests/core/test_operators.py +++ b/qutip/tests/core/test_operators.py @@ -320,10 +320,12 @@ def test_qeye_like(dims, superrep, dtype): expected = qutip.qeye(dims, dtype=dtype) expected.superrep = superrep assert new == expected + assert new.dtype is qutip.data.to.parse(dtype) opevo = qutip.QobjEvo(op) new = qutip.qeye_like(op) assert new == expected + assert new.dtype is qutip.data.to.parse(dtype) @pytest.mark.parametrize(["dims", "superrep"], [ @@ -340,10 +342,12 @@ def test_qzero_like(dims, superrep, dtype): expected = qutip.qzero(dims, dtype=dtype) expected.superrep = superrep assert new == expected + assert new.dtype is qutip.data.to.parse(dtype) opevo = qutip.QobjEvo(op) new = qutip.qzero_like(op) assert new == expected + assert new.dtype is qutip.data.to.parse(dtype) @pytest.mark.parametrize('n_sites', [2, 3, 4, 5]) diff --git a/qutip/tests/core/test_qobj.py b/qutip/tests/core/test_qobj.py index 9ad09fe6fa..7aa7dc0b3b 100644 --- a/qutip/tests/core/test_qobj.py +++ b/qutip/tests/core/test_qobj.py @@ -1262,3 +1262,9 @@ def test_data_as(): with pytest.raises(ValueError) as err: qobj.data_as("ndarray") assert "dia_matrix" in str(err.value) + + +@pytest.mark.parametrize('dtype', ["CSR", "Dense"]) +def test_qobj_dtype(dtype): + obj = qutip.qeye(2, dtype=dtype) + assert obj.dtype == qutip.data.to.parse(dtype) \ No newline at end of file diff --git a/qutip/tests/core/test_qobjevo.py b/qutip/tests/core/test_qobjevo.py index 9fc8454d15..6fb6211d64 100644 --- a/qutip/tests/core/test_qobjevo.py +++ b/qutip/tests/core/test_qobjevo.py @@ -6,6 +6,7 @@ rand_herm, rand_ket, liouvillian, basis, spre, spost, to_choi, expect, rand_ket, rand_dm, operator_to_vector, SESolver, MESolver ) +import qutip.core.data as _data import numpy as np from numpy.testing import assert_allclose @@ -623,3 +624,18 @@ def test_feedback_super(): checker.state = rand_dm(4) checker.state.dims = [[[2],[2]], [[2],[2]]] qevo.matmul_data(0, checker.state.data) + + +@pytest.mark.parametrize('dtype', ["CSR", "Dense"]) +def test_qobjevo_dtype(dtype): + obj = QobjEvo([qeye(2, dtype=dtype), [num(2, dtype=dtype), lambda t: t]]) + assert obj.dtype == _data.to.parse(dtype) + + obj = QobjEvo(lambda t: qeye(2, dtype=dtype)) + assert obj.dtype == _data.to.parse(dtype) + + +def test_qobjevo_mixed(): + obj = QobjEvo([qeye(2, dtype="CSR"), [num(2, dtype="Dense"), lambda t: t]]) + # We test that the output dtype is a know type: accepted by `to.parse`. + _data.to.parse(obj.dtype) \ No newline at end of file From 88e95ceaef3d0ee5e4531c73650f52aa51bd658a Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Wed, 14 Feb 2024 11:11:33 -0500 Subject: [PATCH 64/66] Improve error message --- qutip/core/data/constant.py | 4 ++-- qutip/core/operators.py | 4 +++- qutip/tests/core/test_operators.py | 7 +++++++ 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/qutip/core/data/constant.py b/qutip/core/data/constant.py index 8061bb8832..52f9f50bfd 100644 --- a/qutip/core/data/constant.py +++ b/qutip/core/data/constant.py @@ -96,7 +96,7 @@ def identity_like_data(data, /): Create an identity matrix of the same type and shape. """ if not data.shape[0] == data.shape[1]: - raise ValueError("Can't create and identity like a non square matrix.") + raise ValueError("Can't create an identity matrix like a non square matrix.") return identity[type(data)](data.shape[0]) @@ -105,7 +105,7 @@ def identity_like_dense(data, /): Create an identity matrix of the same type and shape. """ if not data.shape[0] == data.shape[1]: - raise ValueError("Can't create and identity like a non square matrix.") + raise ValueError("Can't create an identity matrix like a non square matrix.") return dense.identity(data.shape[0], fortran=data.fortran) diff --git a/qutip/core/operators.py b/qutip/core/operators.py index c89e170959..5ab8521e01 100644 --- a/qutip/core/operators.py +++ b/qutip/core/operators.py @@ -766,7 +766,9 @@ def qeye_like(qobj): """ if qobj.shape[0] != qobj.shape[1]: - raise ValueError(f"Cannot build a {qobj.shape} identity matrix.") + raise ValueError( + "Can't create an identity matrix like a non square matrix." + ) return Qobj( _data.identity[qobj.dtype](qobj.shape[0]), dims=qobj._dims, isherm=True, isunitary=False, copy=False diff --git a/qutip/tests/core/test_operators.py b/qutip/tests/core/test_operators.py index 77eeb70ff3..625385def3 100644 --- a/qutip/tests/core/test_operators.py +++ b/qutip/tests/core/test_operators.py @@ -328,6 +328,13 @@ def test_qeye_like(dims, superrep, dtype): assert new.dtype is qutip.data.to.parse(dtype) +def test_qeye_like_error(): + with pytest.raises(ValueError) as err: + qutip.qeye_like(qutip.basis(3)) + + assert "non square matrix" in str(err.value) + + @pytest.mark.parametrize(["dims", "superrep"], [ pytest.param([2], None, id="simple"), pytest.param([2, 3], None, id="tensor"), From e8f36e2d274296f26298814d6dd7539939b41a56 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Wed, 14 Feb 2024 11:33:51 -0500 Subject: [PATCH 65/66] Add towncrier --- doc/changes/2325.misc | 1 + qutip/core/data/constant.py | 8 ++++++-- 2 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 doc/changes/2325.misc diff --git a/doc/changes/2325.misc b/doc/changes/2325.misc new file mode 100644 index 0000000000..9b64dc7253 --- /dev/null +++ b/doc/changes/2325.misc @@ -0,0 +1 @@ +Add `dtype` to `Qobj` and `QobjEvo` \ No newline at end of file diff --git a/qutip/core/data/constant.py b/qutip/core/data/constant.py index 52f9f50bfd..d6264c5f89 100644 --- a/qutip/core/data/constant.py +++ b/qutip/core/data/constant.py @@ -96,7 +96,9 @@ def identity_like_data(data, /): Create an identity matrix of the same type and shape. """ if not data.shape[0] == data.shape[1]: - raise ValueError("Can't create an identity matrix like a non square matrix.") + raise ValueError( + "Can't create an identity matrix like a non square matrix." + ) return identity[type(data)](data.shape[0]) @@ -105,7 +107,9 @@ def identity_like_dense(data, /): Create an identity matrix of the same type and shape. """ if not data.shape[0] == data.shape[1]: - raise ValueError("Can't create an identity matrix like a non square matrix.") + raise ValueError( + "Can't create an identity matrix like a non square matrix." + ) return dense.identity(data.shape[0], fortran=data.fortran) From fa3a0732a6b818ddccc5de6d6c88d30f28e84a2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eric=20Gigu=C3=A8re?= Date: Thu, 15 Feb 2024 08:38:05 -0500 Subject: [PATCH 66/66] Update qutip/core/operators.py --- qutip/core/operators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutip/core/operators.py b/qutip/core/operators.py index 5ab8521e01..a220286758 100644 --- a/qutip/core/operators.py +++ b/qutip/core/operators.py @@ -771,7 +771,7 @@ def qeye_like(qobj): ) return Qobj( _data.identity[qobj.dtype](qobj.shape[0]), dims=qobj._dims, - isherm=True, isunitary=False, copy=False + isherm=True, isunitary=True, copy=False )