diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 45b648fa35..ea101cb160 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.9' - 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.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{8,9,10,11}-manylinux_i686 cp3{8,9,10,11}-win32" + CIBW_SKIP: "*-musllinux* *-manylinux_i686 *-win32" OVERRIDE_VERSION: ${{ github.event.inputs.override_version }} steps: @@ -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.8' + python-version: '3.9' - 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/*-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.8' + python-version: '3.9' - 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/*-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 096e5cb593..c046f02a4e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -27,56 +27,51 @@ 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"] + 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 # 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.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.8" - numpy-requirement: ">=1.20,<1.21" - scipy-requirement: ">=1.5,<1.6" + python-version: "3.9" + scipy-requirement: ">=1.8,<1.9" + numpy-requirement: ">=1.22,<1.23" 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, scipy 1.9, numpy 1.23 + # Scipy 1.9 did not support cython 3.0 yet. + # cython#17234 + - case-name: no mkl os: ubuntu-latest - python-version: "3.9" - numpy-requirement: ">=1.20,<1.21" + python-version: "3.10" + scipy-requirement: ">=1.9,<1.10" + numpy-requirement: ">=1.23,<1.24" + condaforge: 1 + oldcython: 1 nomkl: 1 + pytest-extra-options: "-W ignore:dep_util:DeprecationWarning" - # 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 + # Python 3.10, no cython, scipy 1.10, numpy 1.24 + - case-name: no cython os: ubuntu-latest python-version: "3.10" - condaforge: 1 - oldcython: 1 + scipy-requirement: ">=1.10,<1.11" + numpy-requirement: ">=1.24,<1.25" + nocython: 1 - # 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 @@ -86,8 +81,30 @@ jobs: os: ubuntu-latest python-version: "3.11" condaforge: 1 + scipy-requirement: ">=1.11,<1.12" + numpy-requirement: ">=1.25,<1.26" 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 + - 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" + # 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. + - 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 @@ -95,13 +112,12 @@ jobs: # 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 + - case-name: Windows os: windows-latest - python-version: "3.10" + python-version: "3.11" steps: - uses: actions/checkout@v3 - - uses: conda-incubator/setup-miniconda@v2 with: auto-update-conda: true @@ -113,7 +129,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,extras" if [[ -z "${{ matrix.nocython }}" ]]; then QUTIP_TARGET="$QUTIP_TARGET,runtime_compilation" fi @@ -133,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 @@ -164,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 diff --git a/README.md b/README.md index 376e25b3a8..ba6dbf7c6c 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). + +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 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 diff --git a/VERSION b/VERSION index d51bd13201..a799a6e466 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -5.0.0a2 +5.0.0b1 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/doc/apidoc/classes.rst b/doc/apidoc/classes.rst index 270e6c4acc..ecd0cd7a97 100644 --- a/doc/apidoc/classes.rst +++ b/doc/apidoc/classes.rst @@ -11,6 +11,7 @@ Qobj .. autoclass:: qutip.core.qobj.Qobj :members: + :special-members: __call__ .. _classes-qobjevo: @@ -19,6 +20,7 @@ QobjEvo .. autoclass:: qutip.core.cy.qobjevo.QobjEvo :members: + :special-members: __call__ .. _classes-bloch: @@ -29,9 +31,6 @@ Bloch sphere .. autoclass:: qutip.bloch.Bloch :members: -.. autoclass:: qutip.bloch3d.Bloch3d - :members: - Distributions ------------- @@ -48,28 +47,33 @@ Solvers :members: :inherited-members: :show-inheritance: + :exclude-members: add_integrator .. autoclass:: qutip.solver.mesolve.MESolver :members: :inherited-members: :show-inheritance: + :exclude-members: add_integrator .. autoclass:: qutip.solver.brmesolve.BRSolver :members: :inherited-members: :show-inheritance: + :exclude-members: add_integrator - -.. autoclass:: qutip.solver.stochastic.SMESolver +.. autoclass:: qutip.solver.floquet.FMESolver :members: :inherited-members: :show-inheritance: + :exclude-members: add_integrator -.. autoclass:: qutip.solver.stochastic.SSESolver +.. autoclass:: qutip.solver.floquet.FloquetBasis :members: - :inherited-members: - :show-inheritance: +.. autoclass:: qutip.solver.propagator.Propagator + :members: + :inherited-members: + :special-members: __call__ .. _classes-monte-carlo-solver: @@ -81,11 +85,13 @@ Monte Carlo Solvers :members: :inherited-members: :show-inheritance: + :exclude-members: add_integrator .. autoclass:: qutip.solver.nm_mcsolve.NonMarkovianMCSolver :members: :inherited-members: :show-inheritance: + :exclude-members: add_integrator .. _classes-non_markov_heom: @@ -136,6 +142,22 @@ Non-Markovian HEOM Solver :members: +.. _classes-stochastic: + +Stochastic Solver +----------------- + +.. autoclass:: qutip.solver.stochastic.SMESolver + :members: + :inherited-members: + :exclude-members: add_integrator + +.. autoclass:: qutip.solver.stochastic.SSESolver + :members: + :inherited-members: + :exclude-members: add_integrator + + .. _classes-ode: Integrator @@ -206,6 +228,23 @@ Solver Options and Results .. autoclass:: qutip.solver.result.Result :members: + :inherited-members: + :exclude-members: add_processor, add + +.. autoclass:: qutip.solver.result.MultiTrajResult + :members: + :inherited-members: + :exclude-members: add_processor, add, add_end_condition + +.. autoclass:: qutip.solver.result.McResult + :members: + :inherited-members: + :exclude-members: add_processor, add, add_end_condition + +.. autoclass:: qutip.solver.result.NmmcResult + :members: + :inherited-members: + :exclude-members: add_processor, add, add_end_condition .. _classes-piqs: @@ -221,22 +260,25 @@ Permutational Invariance .. _classes-distributions: Distribution functions ----------------------------- +---------------------- .. autoclass:: qutip.distributions.Distribution :members: -.. autoclass:: qutip.distributions.WignerDistribution - :members: +.. + Docstrings are empty... -.. autoclass:: qutip.distributions.QDistribution - :members: + .. autoclass:: qutip.distributions.WignerDistribution + :members: -.. autoclass:: qutip.distributions.TwoModeQuadratureCorrelation - :members: + .. autoclass:: qutip.distributions.QDistribution + :members: -.. autoclass:: qutip.distributions.HarmonicOscillatorWaveFunction - :members: + .. autoclass:: qutip.distributions.TwoModeQuadratureCorrelation + :members: -.. autoclass:: qutip.distributions.HarmonicOscillatorProbabilityFunction - :members: + .. autoclass:: qutip.distributions.HarmonicOscillatorWaveFunction + :members: + + .. autoclass:: qutip.distributions.HarmonicOscillatorProbabilityFunction + :members: diff --git a/doc/apidoc/functions.rst b/doc/apidoc/functions.rst index 3c781b4200..f64707a415 100644 --- a/doc/apidoc/functions.rst +++ b/doc/apidoc/functions.rst @@ -11,14 +11,21 @@ Quantum States -------------- .. automodule:: qutip.core.states - :members: basis, bell_state, bra, coherent, coherent_dm, enr_state_dictionaries, enr_thermal_dm, enr_fock, fock, fock_dm, ghz_state, maximally_mixed_dm, ket, ket2dm, phase_basis, projection, qutrit_basis, singlet_state, spin_state, spin_coherent, state_number_enumerate, state_number_index, state_index_number, state_number_qobj, thermal_dm, triplet_states, w_state, zero_ket + :members: basis, bell_state, bra, coherent, coherent_dm, fock, fock_dm, ghz_state, maximally_mixed_dm, ket, ket2dm, phase_basis, projection, qutrit_basis, singlet_state, spin_state, spin_coherent, state_number_enumerate, state_number_index, state_index_number, state_number_qobj, thermal_dm, triplet_states, w_state, zero_ket Quantum Operators ----------------- .. automodule:: qutip.core.operators - :members: charge, commutator, create, destroy, displace, enr_destroy, enr_identity, fcreate, fdestroy, jmat, num, qeye, identity, momentum, phase, position, qdiags, qutrit_ops, qzero, sigmam, sigmap, sigmax, sigmay, sigmaz, spin_Jx, spin_Jy, spin_Jz, spin_Jm, spin_Jp, squeeze, squeezing, tunneling + :members: charge, commutator, create, destroy, displace, fcreate, fdestroy, jmat, num, qeye, identity, momentum, phase, position, qdiags, qutrit_ops, qzero, sigmam, sigmap, sigmax, sigmay, sigmaz, spin_Jx, spin_Jy, spin_Jz, spin_Jm, spin_Jp, squeeze, squeezing, tunneling, qeye_like, qzero_like + + +Energy Restricted Operators +--------------------------- + +.. automodule:: qutip.core.energy_restricted + :members: enr_state_dictionaries, enr_thermal_dm, enr_fock, enr_destroy, enr_identity .. _functions-rand: @@ -34,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 @@ -54,7 +61,7 @@ Operators and Superoperator Dimensions -------------------------------------- .. automodule:: qutip.core.dimensions - :members: is_scalar, is_vector, is_vectorized_oper, type_from_dims, flatten, deep_remove, unflatten, collapse_dims_oper, collapse_dims_super, enumerate_flat, deep_map, dims_to_tensor_perm, dims_to_tensor_shape, dims_idxs_to_tensor_idxs + :members: to_tensor_rep, from_tensor_rep Functions acting on states and operators @@ -158,7 +165,7 @@ Floquet States and Floquet-Markov Master Equation ------------------------------------------------- .. automodule:: qutip.solver.floquet - :members: fmmesolve, fsesolve, FloquetBasis, FMESolver, floquet_tensor + :members: fmmesolve, fsesolve, floquet_tensor Stochastic Schrödinger Equation and Master Equation @@ -168,6 +175,13 @@ Stochastic Schrödinger Equation and Master Equation :members: ssesolve, smesolve +Constructing time dependent systems +----------------------------------- + +.. automodule:: qutip.core.coefficient + :members: coefficient + + Hierarchical Equations of Motion -------------------------------- @@ -283,26 +297,16 @@ Parallelization --------------- .. automodule:: qutip.solver.parallel - :members: parallel_map, serial_map + :members: parallel_map, serial_map, loky_pmap, mpi_pmap .. _functions-ipython: -Semidefinite Programming ------------------------- - -.. Was this removed - .. automodule:: qutip.semidefinite - :members: complex_var, herm, pos_noherm, pos, dens, kron, conj, bmat, bmat, memoize, qudit_swap, dnorm_problem - - -.. _functions-semidefinite: - IPython Notebook Tools ---------------------- .. automodule:: qutip.ipynbtools - :members: parallel_map, version_table + :members: version_table .. _functions-misc: 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/changelog.rst b/doc/changelog.rst index ad63be5726..a916a657c5 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -6,6 +6,47 @@ Change Log .. towncrier release notes start +QuTiP 5.0.0b1 (2024-03-04) +========================== + +Features +-------- + +- Create a Dimension class (#1996) +- Add arguments of plot_wigner() and plot_wigner_fock_distribution() to specify parameters for wigner(). (#2057, by Kosuke Mizuno) +- Restore feedback to solvers (#2210) +- Added mpi_pmap, which uses the mpi4py module to run computations in parallel through the MPI interface. (#2296, by Paul) +- Only pre-compute density matrices if keep_runs_results is False (#2303, by Matt Ord) + + +Bug Fixes +--------- + +- Add the possibility to customize point colors as in V4 and fix point plot behavior for 'l' style (#1974, by Daniel Moreno Galán) +- Disabled broken "improved sampling" for `nm_mcsolve`. (#2234, by Paul) +- Fixed result objects storing a reference to the solver through options._feedback. (#2262, by Paul) +- Fixed simdiag not returning orthonormal eigenvectors. (#2269, by Sola85) +- Fix LaTeX display of Qobj state in Jupyter cell outputs (#2272, by Edward Thomas) +- Improved behavior of `parallel_map` and `loky_pmap` in the case of timeouts, errors or keyboard interrupts (#2280, by Paul) +- Ignore deprecation warnings from cython 0.29.X in tests. (#2288) +- Fixed two problems with the steady_state() solver in the HEOM method. (#2333) + + +Miscellaneous +------------- + +- Improve fidelity doc-string (#2257) +- Improve documentation in guide/dynamics (#2271) +- Improve states and operator parameters documentation. (#2289) +- Rework `kraus_to_choi` making it faster (#2284, by Bogdan Reznychenko) +- Remove Bloch3D: redundant to Bloch (#2306) +- Allow tests to run without matplotlib and ipython. (#2311) +- Add too small step warnings in fixed dt SODE solver (#2313) +- Add `dtype` to `Qobj` and `QobjEvo` (#2325) +- Fix typos in `expect` documentation (#2331, by gabbence95) +- Allow measurement functions to support degenerate operators. (#2342) + + QuTiP 5.0.0a2 (2023-09-06) ========================== @@ -435,6 +476,75 @@ Feature removals - The ``~/.qutip/qutiprc`` config file is no longer supported. It contained settings for the OpenMP support. +QuTiP 4.7.5 (2024-01-29) +======================== + +Patch release for QuTiP 4.7. It adds support for SciPy 1.12. + +Bug Fixes +--------- + +- Remove use of scipy. in parallel.py, incompatible with scipy==1.12 (#2305 by Evan McKinney) + + +QuTiP 4.7.4 (2024-01-15) +======================== + +Bug Fixes +--------- + +- Adapt to deprecation from matplotlib 3.8 (#2243, reported by Bogdan Reznychenko) +- Fix name of temp files for removal after use. (#2251, reported by Qile Su) +- Avoid integer overflow in Qobj creation. (#2252, reported by KianHwee-Lim) +- Ignore DeprecationWarning from pyximport (#2287) +- Add partial support and tests for python 3.12. (#2294) + + +Miscellaneous +------------- + +- Rework `choi_to_kraus`, making it rely on an eigenstates solver that can choose `eigh` if the Choi matrix is Hermitian, as it is more numerically stable. (#2276, by Bogdan Reznychenko) +- Rework `kraus_to_choi`, making it faster (#2283, by Bogdan Reznychenko and Rafael Haenel) + + +QuTiP 4.7.3 (2023-08-22) +======================== + +Bug Fixes +--------- + +- Non-oper qobj + scalar raise an error. (#2208 reported by vikramkashyap) +- Fixed issue where `extract_states` did not preserve hermiticity. + Fixed issue where `rand_herm` did not set the private attribute _isherm to True. (#2214 by AGaliciaMartinez) +- ssesolve average states to density matrices (#2216 reported by BenjaminDAnjou) + + +Miscellaneous +------------- + +- Exclude cython 3.0.0 from requirement (#2204) +- Run in no cython mode with cython >=3.0.0 (#2207) + + +QuTiP 4.7.2 (2023-06-28) +======================== + +This is a bugfix release for QuTiP 4.7.X. It adds support for +numpy 1.25 and scipy 1.11. + +Bug Fixes +--------- +- Fix setting of sso.m_ops in heterodyne smesolver and passing through of sc_ops to photocurrent solver. (#2081 by Bogdan Reznychenko and Simon Cross) +- Update calls to SciPy eigvalsh and eigsh to pass the range of eigenvalues to return using ``subset_by_index=``. (#2081 by Simon Cross) +- Fixed bug where some matrices were wrongly found to be hermitian. (#2082 by AGaliciaMartinez) + +Miscellaneous +------------- +- Fixed typo in stochastic.py (#2049, by eltociear) +- `ptrace` always return density matrix (#2185, issue by udevd) +- `mesolve` can support mixed callable and Qobj for `e_ops` (#2184 issue by balopat) + + Version 4.7.1 (December 11, 2022) +++++++++++++++++++++++++++++++++ @@ -501,7 +611,7 @@ Improvements - Added transparency parameter to the add_point, add_vector and add_states methods in the Bloch and Bloch3d classes. (`#1837 `_ by Xavier Spronken) - Support ``Path`` objects in ``qutip.fileio``. (`#1813 `_ by Adrià Labay) - Improved the weighting in steadystate solver, so that the default weight matches the documented behaviour and the dense solver applies the weights in the same manner as the sparse solver. (`#1275 `_ and `#1802 `_ by NS2 Group at LPS and Simon Cross) -- Added a ``color_style`` option to the ``hinton`` plotting function. (`#1595 `_ by Cassandra Granade) +- Added a ``color_style`` option to the ``hinton`` plotting function. (`#1595 `_ by Cassandra Granade) - Improved the scaling of ``floquet_master_equation_rates`` and ``floquet_master_equation_tensor`` and fixed transposition and basis change errors in ``floquet_master_equation_tensor`` and ``floquet_markov_mesolve``. (`#1248 `_ by Camille Le Calonnec, Jake Lishman and Eric Giguère) - Removed ``linspace_with`` and ``view_methods`` from ``qutip.utilities``. For the former it is far better to use ``numpy.linspace`` and for the later Python's in-built ``help`` function or other tools. (`#1680 `_ by Eric Giguère) - Added support for passing callable functions as ``e_ops`` to ``mesolve`` and ``sesolve``. (`#1655 `_ by Marek Narożniak) diff --git a/doc/contributors.rst b/doc/contributors.rst index e112abeeee..da55af92f3 100644 --- a/doc/contributors.rst +++ b/doc/contributors.rst @@ -99,7 +99,6 @@ Lead Developers - `Neill Lambert `_ - `Eric Giguère `_ - `Boxi Li `_ -- `Jake Lishman `_ - `Simon Cross `_ - `Asier Galicia `_ @@ -107,9 +106,10 @@ Past Lead Developers ==================== - `Robert Johansson `_ (RIKEN) -- `Paul Nation `_ (Korea University) +- `Paul Nation `_ (Korea University) - `Chris Granade `_ - `Arne Grimsmo `_ +- `Jake Lishman `_ .. _developers-contributors: diff --git a/doc/development/contributing.rst b/doc/development/contributing.rst index de2af1b5c0..083754475e 100644 --- a/doc/development/contributing.rst +++ b/doc/development/contributing.rst @@ -8,7 +8,7 @@ Quick Start =========== QuTiP is developed through wide collaboration using the ``git`` version-control system, with the main repositories hosted in the `qutip organisation on GitHub `_. -You will need to be familiar with ``git`` as a tool, and the `GitHub Flow `_ workflow for branching and making pull requests. +You will need to be familiar with ``git`` as a tool, and the `GitHub Flow `_ workflow for branching and making pull requests. The exact details of environment set-up, build process and testing vary by repository and are discussed below, however in overview, the steps to contribute are: #. Consider creating an issue on the GitHub page of the relevant repository, describing the change you think should be made and why, so we can discuss details with you and make sure it is appropriate. @@ -102,7 +102,7 @@ Code Style The biggest concern you should always have is to make it easy for your code to be read and understood by the person who comes next. -All new contributions must follow `PEP 8 style `_; all pull requests will be passed through a linter that will complain if you violate it. +All new contributions must follow `PEP 8 style `_; all pull requests will be passed through a linter that will complain if you violate it. You should use the ``pycodestyle`` package locally (available on ``pip``) to test you satisfy the requirements before you push your commits, since this is rather faster than pushing 10 different commits trying to fix minor niggles. Keep in mind that there is quite a lot of freedom in this style, especially when it comes to line breaks. If a line is too long, consider the *best* way to split it up with the aim of making the code readable, not just the first thing that doesn't generate a warning. @@ -152,7 +152,7 @@ When making a pull request, we require that you add a towncrier entry along with You should create a file named ``.`` in the ``doc/changes`` directory, where the PR number should be substituted for ````, and ```` is either ``feature``, ``bugfix``, ``doc``, ``removal``, ``misc``, or ``deprecation``, depending on the type of change included in the PR. -You can also create this file by installing ``towncrier`` and running +You can also create this file by installing ``towncrier`` and running towncrier create . diff --git a/doc/development/ideas/quantum-error-mitigation.rst b/doc/development/ideas/quantum-error-mitigation.rst index 17e279ba12..2c8a68b9a1 100644 --- a/doc/development/ideas/quantum-error-mitigation.rst +++ b/doc/development/ideas/quantum-error-mitigation.rst @@ -20,7 +20,8 @@ device models, new noise models and integration with the existing general framework for quantum circuits (`qutip.qip.circuit`). There are also possible applications such as error mitigation techniques ([1]_, [2]_, [3]_). -The tutorial notebooks can be found at https://qutip.org/tutorials.html#nisq. A +The tutorial notebooks can be found in the Quantum information processing +section of https://qutip.org/qutip-tutorials/index-v5.html. A recent presentation on the FOSDEM conference may help you get an overview (https://fosdem.org/2020/schedule/event/quantum_qutip/). See also the Github Project page for a collection of related issues and ongoing Pull Requests. diff --git a/doc/development/release_distribution.rst b/doc/development/release_distribution.rst index 5d6956014d..75c581cbf9 100644 --- a/doc/development/release_distribution.rst +++ b/doc/development/release_distribution.rst @@ -324,7 +324,7 @@ HTML File Updates Conda Forge +++++++++++ -If not done previously then fork the `qutip-feedstock `_. +If not done previously then fork the `qutip-feedstock `_. Checkout a new branch on your fork, e.g. :: diff --git a/doc/development/roadmap.rst b/doc/development/roadmap.rst index 36f61a0600..c422d98d7c 100644 --- a/doc/development/roadmap.rst +++ b/doc/development/roadmap.rst @@ -435,7 +435,7 @@ HEOM revamp :tag: heom-revamp :status: completed :admin lead: `Neill `_ -:main dev: `Simon Cross `_, `Tarun Raheja `_ +:main dev: `Simon Cross `_, `Tarun Raheja `_ An overhaul of the HEOM solver, to incorporate the improvements pioneered in BoFiN. diff --git a/doc/frontmatter.rst b/doc/frontmatter.rst index af1db23c7b..ba45c750a1 100644 --- a/doc/frontmatter.rst +++ b/doc/frontmatter.rst @@ -9,11 +9,11 @@ Frontmatter About This Documentation ========================== -This document contains a user guide and automatically generated API documentation for QuTiP. A PDF version of this text is available at the `documentation page `_. +This document contains a user guide and automatically generated API documentation for QuTiP. A PDF version of this text is available at the `documentation page `_. **For more information see the** `QuTiP project web page`_. -.. _QuTiP project web page: https://www.qutip.org +.. _QuTiP project web page: https://qutip.org/ :Author: J.R. Johansson @@ -165,7 +165,7 @@ Several libraries rely on QuTiP for quantum physics or quantum information proce :QPtomographer: `QPtomographer `_ derive quantum error bars for quantum processes in terms of the diamond norm to a reference quantum channel -:QuNetSim: `QuNetSim `_ is a quantum networking simulation framework to develop and test protocols for quantum networks +:QuNetSim: `QuNetSim `_ is a quantum networking simulation framework to develop and test protocols for quantum networks :qupulse: `qupulse `_ is a toolkit to facilitate experiments involving pulse driven state manipulation of physical qubits diff --git a/doc/guide/dynamics/dynamics-bloch-redfield.rst b/doc/guide/dynamics/dynamics-bloch-redfield.rst index ceaf9a997c..9f29de7388 100644 --- a/doc/guide/dynamics/dynamics-bloch-redfield.rst +++ b/doc/guide/dynamics/dynamics-bloch-redfield.rst @@ -87,16 +87,19 @@ This allows us to write master equation in terms of system operators and bath co g_{\alpha\beta}(-\tau) \left[\rho_S(t)A_\alpha(t-\tau)A_\beta(t) - A_\alpha(t)\rho_S(t)A_\beta(t-\tau)\right] \right\}, -where :math:`g_{\alpha\beta}(\tau) = {\rm Tr}_B\left[B_\alpha(t)B_\beta(t-\tau)\rho_B\right] = \left`, since the bath state :math:`\rho_B` is a steady state. +where :math:`g_{\alpha\beta}(\tau) = {\rm Tr}_B\left[B_\alpha(t)B_\beta(t-\tau)\rho_B\right] = \left`, +since the bath state :math:`\rho_B` is a steady state. -In the eigenbasis of the system Hamiltonian, where :math:`A_{mn}(t) = A_{mn} e^{i\omega_{mn}t}`, :math:`\omega_{mn} = \omega_m - \omega_n` and :math:`\omega_m` are the eigenfrequencies corresponding the eigenstate :math:`\left|m\right>`, we obtain in matrix form in the Schrödinger picture +In the eigenbasis of the system Hamiltonian, where :math:`A_{mn}(t) = A_{mn} e^{i\omega_{mn}t}`, +:math:`\omega_{mn} = \omega_m - \omega_n` and :math:`\omega_m` are the eigenfrequencies +corresponding the eigenstate :math:`\left|m\right>`, we obtain in matrix form in the Schrödinger picture .. math:: \frac{d}{dt}\rho_{ab}(t) - = - -i\omega_{ab}\rho_{ab}(t) - -\hbar^{-2} + =& + -i\omega_{ab}\rho_{ab}(t) \nonumber\\ + &-\hbar^{-2} \sum_{\alpha,\beta} \sum_{c,d}^{\rm sec} \int_0^\infty d\tau\; @@ -107,7 +110,7 @@ In the eigenbasis of the system Hamiltonian, where :math:`A_{mn}(t) = A_{mn} e^{ A^\alpha_{ac} A^\beta_{db} e^{i\omega_{ca}\tau} \right] \right. \nonumber\\ - + + &+ \left. g_{\alpha\beta}(-\tau) \left[\delta_{ac}\sum_n A^\alpha_{dn}A^\beta_{nb} e^{i\omega_{nd}\tau} @@ -185,8 +188,9 @@ Bloch-Redfield master equation in QuTiP -In QuTiP, the Bloch-Redfield tensor Eq. :eq:`br-tensor` can be calculated using the function :func:`qutip.bloch_redfield.bloch_redfield_tensor`. -It takes two mandatory arguments: The system Hamiltonian :math:`H`, a nested list of operator :math:`A_\alpha`, spectral density functions :math:`S_\alpha(\omega)` pairs that characterize the coupling between system and bath. +In QuTiP, the Bloch-Redfield tensor Eq. :eq:`br-tensor` can be calculated using the function :func:`.bloch_redfield_tensor`. +It takes two mandatory arguments: The system Hamiltonian :math:`H`, a nested list of operator +:math:`A_\alpha`, spectral density functions :math:`S_\alpha(\omega)` pairs that characterize the coupling between system and bath. The spectral density functions are Python callback functions that takes the (angular) frequency as a single argument. To illustrate how to calculate the Bloch-Redfield tensor, let's consider a two-level atom @@ -231,13 +235,22 @@ To illustrate how to calculate the Bloch-Redfield tensor, let's consider a two-l [ 0. +0.j 0. +0.j 0. +0.j -0.24514517+0.j ]] -Note that it is also possible to add Lindblad dissipation superoperators in the Bloch-Refield tensor by passing the operators via the ``c_ops`` keyword argument like you would in the :func:`qutip.mesolve` or :func:`qutip.mcsolve` functions. -For convenience, the function :func:`qutip.bloch_redfield_tensor` also returns the basis transformation operator, the eigen vector matrix, since they are calculated in the process of calculating the Bloch-Redfield tensor `R`, and the `ekets` are usually needed again later when transforming operators between the laboratory basis and the eigen basis. -The tensor can be obtained in the laboratory basis by setting ``fock_basis=True``, in that case, the transformation operator is not returned. +Note that it is also possible to add Lindblad dissipation superoperators in the +Bloch-Refield tensor by passing the operators via the ``c_ops`` keyword argument +like you would in the :func:`.mesolve` or :func:`.mcsolve` functions. +For convenience, the function :func:`.bloch_redfield_tensor` also returns the basis +transformation operator, the eigen vector matrix, since they are calculated in the +process of calculating the Bloch-Redfield tensor `R`, and the `ekets` are usually +needed again later when transforming operators between the laboratory basis and the eigen basis. +The tensor can be obtained in the laboratory basis by setting ``fock_basis=True``, +in that case, the transformation operator is not returned. -The evolution of a wavefunction or density matrix, according to the Bloch-Redfield master equation :eq:`br-final`, can be calculated using the QuTiP function :func:`qutip.mesolve` using Bloch-Refield tensor in the laboratory basis instead of a liouvillian. -For example, to evaluate the expectation values of the :math:`\sigma_x`, :math:`\sigma_y`, and :math:`\sigma_z` operators for the example above, we can use the following code: +The evolution of a wavefunction or density matrix, according to the Bloch-Redfield +master equation :eq:`br-final`, can be calculated using the QuTiP function :func:`.mesolve` +using Bloch-Refield tensor in the laboratory basis instead of a liouvillian. +For example, to evaluate the expectation values of the :math:`\sigma_x`, +:math:`\sigma_y`, and :math:`\sigma_z` operators for the example above, we can use the following code: .. plot:: :context: @@ -274,24 +287,35 @@ For example, to evaluate the expectation values of the :math:`\sigma_x`, :math:` sphere.make_sphere() -The two steps of calculating the Bloch-Redfield tensor and evolving according to the corresponding master equation can be combined into one by using the function :func:`qutip.brmesolve`, which takes same arguments as :func:`qutip.mesolve` and :func:`qutip.mcsolve`, save for the additional nested list of operator-spectrum pairs that is called ``a_ops``. +The two steps of calculating the Bloch-Redfield tensor and evolving according to +the corresponding master equation can be combined into one by using the function +:func:`.brmesolve`, which takes same arguments as :func:`.mesolve` and +:func:`.mcsolve`, save for the additional nested list of operator-spectrum +pairs that is called ``a_ops``. .. plot:: :context: close-figs output = brmesolve(H, psi0, tlist, a_ops=[[sigmax(),ohmic_spectrum]], e_ops=e_ops) -where the resulting `output` is an instance of the class :class:`qutip.Result`. +where the resulting `output` is an instance of the class :class:`.Result`. .. note:: - While the code example simulates the Bloch-Redfield equation in the secular approximation, QuTiP's implementation allows the user to simulate the non-secular version of the Bloch-Redfield equation by setting ``sec_cutoff=-1``, as well as do a partial secular approximation by setting it to a ``float`` , this float will become the cutoff for the sum in :eq:`br-final` meaning terms with :math:`|\omega_{ab}-\omega_{cd}|` greater than the cutoff will be neglected. + While the code example simulates the Bloch-Redfield equation in the secular + approximation, QuTiP's implementation allows the user to simulate the non-secular + version of the Bloch-Redfield equation by setting ``sec_cutoff=-1``, as well as + do a partial secular approximation by setting it to a ``float`` , this float + will become the cutoff for the sum in :eq:`br-final` meaning terms with + :math:`|\omega_{ab}-\omega_{cd}|` greater than the cutoff will be neglected. Its default value is 0.1 which corresponds to the secular approximation. For example the command :: - output = brmesolve(H, psi0, tlist, a_ops=[[sigmax(), ohmic_spectrum]], e_ops=e_ops, sec_cutoff=-1) - - will simulate the same example as above without the secular approximation. Note that using the non-secular version may lead to negativity issues. + output = brmesolve(H, psi0, tlist, a_ops=[[sigmax(), ohmic_spectrum]], + e_ops=e_ops, sec_cutoff=-1) + + will simulate the same example as above without the secular approximation. + Note that using the non-secular version may lead to negativity issues. .. _td-bloch-redfield: @@ -300,17 +324,23 @@ Time-dependent Bloch-Redfield Dynamics If you have not done so already, please read the section: :ref:`time`. -As we have already discussed, the Bloch-Redfield master equation requires transforming into the eigenbasis of the system Hamiltonian. +As we have already discussed, the Bloch-Redfield master equation requires transforming +into the eigenbasis of the system Hamiltonian. For time-independent systems, this transformation need only be done once. -However, for time-dependent systems, one must move to the instantaneous eigenbasis at each time-step in the evolution, thus greatly increasing the computational complexity of the dynamics. +However, for time-dependent systems, one must move to the instantaneous eigenbasis +at each time-step in the evolution, thus greatly increasing the computational complexity of the dynamics. In addition, the requirement for computing all the eigenvalues severely limits the scalability of the method. -Fortunately, this eigen decomposition occurs at the Hamiltonian level, as opposed to the super-operator level, and thus, with efficient programming, one can tackle many systems that are commonly encountered. +Fortunately, this eigen decomposition occurs at the Hamiltonian level, as opposed to the +super-operator level, and thus, with efficient programming, one can tackle many systems that are commonly encountered. -For time-dependent Hamiltonians, the Hamiltonian itself can be passed into the solver like any other time dependent Hamiltonian, as thus we will not discuss this topic further. +For time-dependent Hamiltonians, the Hamiltonian itself can be passed into the solver +like any other time dependent Hamiltonian, as thus we will not discuss this topic further. Instead, here the focus is on time-dependent bath coupling terms. -To this end, suppose that we have a dissipative harmonic oscillator, where the white-noise dissipation rate decreases exponentially with time :math:`\kappa(t) = \kappa(0)\exp(-t)`. -In the Lindblad or Monte Carlo solvers, this could be implemented as a time-dependent collapse operator list ``c_ops = [[a, 'sqrt(kappa*exp(-t))']]``. +To this end, suppose that we have a dissipative harmonic oscillator, where the white-noise +dissipation rate decreases exponentially with time :math:`\kappa(t) = \kappa(0)\exp(-t)`. +In the Lindblad or Monte Carlo solvers, this could be implemented as a time-dependent +collapse operator list ``c_ops = [[a, 'sqrt(kappa*exp(-t))']]``. In the Bloch-Redfield solver, the bath coupling terms must be Hermitian. As such, in this example, our coupling operator is the position operator ``a+a.dag()``. The complete example, and comparison to the analytic expression is: @@ -320,31 +350,21 @@ The complete example, and comparison to the analytic expression is: :context: close-figs N = 10 # number of basis states to consider - a = destroy(N) - H = a.dag() * a - psi0 = basis(N, 9) # initial state - kappa = 0.2 # coupling to oscillator - a_ops = [ ([a+a.dag(), f'sqrt({kappa}*exp(-t))'], '(w>=0)') ] - tlist = np.linspace(0, 10, 100) out = brmesolve(H, psi0, tlist, a_ops, e_ops=[a.dag() * a]) - actual_answer = 9.0 * np.exp(-kappa * (1.0 - np.exp(-tlist))) plt.figure() - plt.plot(tlist, out.expect[0]) - plt.plot(tlist, actual_answer) - plt.show() @@ -361,7 +381,8 @@ In this example, the ``a_ops`` list would be: ] -where the first tuple element ``[[a, 'exp(1j*t)'], [a.dag(), 'exp(-1j*t)']]`` tells the solver what is the time-dependent Hermitian coupling operator. +where the first tuple element ``[[a, 'exp(1j*t)'], [a.dag(), 'exp(-1j*t)']]`` tells +the solver what is the time-dependent Hermitian coupling operator. The second tuple ``f'{kappa} * (w >= 0)'``, gives the noise power spectrum. A full example is: @@ -369,37 +390,25 @@ A full example is: :context: close-figs N = 10 - w0 = 1.0 * 2 * np.pi - g = 0.05 * w0 - kappa = 0.15 - times = np.linspace(0, 25, 1000) a = destroy(N) - H = w0 * a.dag() * a + g * (a + a.dag()) - psi0 = ket2dm((basis(N, 4) + basis(N, 2) + basis(N, 0)).unit()) - a_ops = [[ QobjEvo([[a, 'exp(1j*t)'], [a.dag(), 'exp(-1j*t)']]), (f'{kappa} * (w >= 0)') ]] - e_ops = [a.dag() * a, a + a.dag()] res_brme = brmesolve(H, psi0, times, a_ops, e_ops) plt.figure() - - plt.plot(times,res_brme.expect[0], label=r'$a^{+}a$') - - plt.plot(times,res_brme.expect[1], label=r'$a+a^{+}$') - + plt.plot(times, res_brme.expect[0], label=r'$a^{+}a$') + plt.plot(times, res_brme.expect[1], label=r'$a+a^{+}$') plt.legend() - plt.show() Further examples on time-dependent Bloch-Redfield simulations can be found in the online tutorials. diff --git a/doc/guide/dynamics/dynamics-class.rst b/doc/guide/dynamics/dynamics-class.rst new file mode 100644 index 0000000000..3469768c6f --- /dev/null +++ b/doc/guide/dynamics/dynamics-class.rst @@ -0,0 +1,166 @@ +.. _solver_class: + + + +******************************************* +Solver Class Interface +******************************************* + +In QuTiP version 5 and later, solvers such as :func:`.mesolve`, :func:`.mcsolve` also have +a class interface. The class interface allows reusing the Hamiltonian and fine tuning +many details of how the solver is run. + +Examples of some of the solver class features are given below. + +Reusing Hamiltonian Data +------------------------ + +There are many cases where one would like to study multiple evolutions of +the same quantum system, whether by changing the initial state or other parameters. +In order to evolve a given system as fast as possible, the solvers in QuTiP +take the given input operators (Hamiltonian, collapse operators, etc) and prepare +them for use with the selected ODE solver. + +These operations are usually reasonably fast, but for some solvers, such as +:func:`.brmesolve` or :func:`.fmmesolve`, the overhead can be significant. +Even for simpler solvers, the time spent organizing data can become appreciable +when repeatedly solving a system. + +The class interface allows us to setup the system once and reuse it with various +parameters. Most ``...solve`` function have a paired ``...Solver`` class, with a +``..Solver.run`` method to run the evolution. At class +instance creation, the physics (``H``, ``c_ops``, ``a_ops``, etc.) and options +are passed. The initial state, times and expectation operators are only passed +when calling ``run``: + +.. plot:: + :context: close-figs + + times = np.linspace(0.0, 6.0, 601) + a = tensor(qeye(2), destroy(10)) + sm = tensor(destroy(2), qeye(10)) + e_ops = [a.dag() * a, sm.dag() * sm] + H = QobjEvo( + [a.dag()*a + sm.dag()*sm, [(sm*a.dag() + sm.dag()*a), lambda t, A: A]], + args={"A": 0.5*np.pi} + ) + + solver = MESolver(H, c_ops=[np.sqrt(0.1) * a], options={"atol": 1e-8}) + solver.options["normalize_output"] = True + psi0 = tensor(fock(2, 0), fock(10, 5)) + data1 = solver.run(psi0, times, e_ops=e_ops) + psi1 = tensor(fock(2, 0), coherent(10, 2 - 1j)) + data2 = solver.run(psi1, times, e_ops=e_ops) + + plt.figure() + plt.plot(times, data1.expect[0], "b", times, data1.expect[1], "r", lw=2) + plt.plot(times, data2.expect[0], 'b--', times, data2.expect[1], 'r--', lw=2) + plt.title('Master Equation time evolution') + plt.xlabel('Time', fontsize=14) + plt.ylabel('Expectation values', fontsize=14) + plt.legend(("cavity photon number", "atom excitation probability")) + plt.show() + + +Note that as shown, options can be set at initialization or with the +``options`` property. + +The simulation parameters, the ``args`` of the :class:`.QobjEvo` passed as system +operators, can be updated at the start of a run: + +.. plot:: + :context: close-figs + + data1 = solver.run(psi0, times, e_ops=e_ops) + data2 = solver.run(psi0, times, e_ops=e_ops, args={"A": 0.25*np.pi}) + data3 = solver.run(psi0, times, e_ops=e_ops, args={"A": 0.125*np.pi}) + + plt.figure() + plt.plot(times, data1.expect[0], label="A=pi/2") + plt.plot(times, data2.expect[0], label="A=pi/4") + plt.plot(times, data3.expect[0], label="A=pi/8") + plt.title('Master Equation time evolution') + plt.xlabel('Time', fontsize=14) + plt.ylabel('Expectation values', fontsize=14) + plt.legend() + plt.show() + + +Stepping through the run +------------------------ + +The solver class also allows to run through a simulation one step at a time, updating +args at each step: + + +.. plot:: + :context: close-figs + + data = [5.] + solver.start(state0=psi0, t0=times[0]) + for t in times[1:]: + psi_t = solver.step(t, args={"A": np.pi*np.exp(-(t-3)**2)}) + data.append(expect(e_ops[0], psi_t)) + + plt.figure() + plt.plot(times, data) + plt.title('Master Equation time evolution') + plt.xlabel('Time', fontsize=14) + plt.ylabel('Expectation values', fontsize=14) + plt.legend(("cavity photon number")) + plt.show() + + +.. note:: + + This is an example only, updating a constant ``args`` parameter between step + should not replace using a function as QobjEvo's coefficient. + +.. note:: + + It is possible to create multiple solvers and to advance them using ``step`` in + parallel. However, many ODE solver, including the default ``adams`` method, only + allow one instance at a time per process. QuTiP supports using multiple solver instances + of these ODE solvers but with a performance cost. In these situations, using + ``dop853`` or ``vern9`` integration method is recommended instead. + + + + +Feedback: Accessing the solver state from evolution operators +============================================================= + +The state of the system during the evolution is accessible via properties of the solver classes. + +Each solver has a ``StateFeedback`` and ``ExpectFeedback`` class method that can +be passed as arguments to time dependent systems. For example, ``ExpectFeedback`` +can be used to create a system which uncouples when there are 5 or fewer photons in the +cavity. + +.. plot:: + :context: close-figs + + def f(t, e1): + ex = (e1.real - 5) + return (ex > 0) * ex * 10 + + times = np.linspace(0.0, 1.0, 301) + a = tensor(qeye(2), destroy(10)) + sm = tensor(destroy(2), qeye(10)) + e_ops = [a.dag() * a, sm.dag() * sm] + psi0 = tensor(fock(2, 0), fock(10, 8)) + e_ops = [a.dag() * a, sm.dag() * sm] + + H = [a*a.dag(), [sm*a.dag() + sm.dag()*a, f]] + data = mesolve(H, psi0, times, c_ops=[a], e_ops=e_ops, + args={"e1": MESolver.ExpectFeedback(a.dag() * a)} + ).expect + + plt.figure() + plt.plot(times, data[0]) + plt.plot(times, data[1]) + plt.title('Master Equation time evolution') + plt.xlabel('Time', fontsize=14) + plt.ylabel('Expectation values', fontsize=14) + plt.legend(("cavity photon number", "atom excitation probability")) + plt.show() diff --git a/doc/guide/dynamics/dynamics-data.rst b/doc/guide/dynamics/dynamics-data.rst index d6c616dbbe..e1efafe9b1 100644 --- a/doc/guide/dynamics/dynamics-data.rst +++ b/doc/guide/dynamics/dynamics-data.rst @@ -9,9 +9,12 @@ Dynamics Simulation Results The solver.Result Class ======================= -Before embarking on simulating the dynamics of quantum systems, we will first look at the data structure used for returning the simulation results. -This object is a :func:`qutip.Result` class that stores all the crucial data needed for analyzing and plotting the results of a simulation. -A generic ``Result`` object ``result`` contains the following properties for storing simulation data: +Before embarking on simulating the dynamics of quantum systems, we will first +look at the data structure used for returning the simulation results. This +object is a :func:`~qutip.solver.result.Result` class that stores all the +crucial data needed for analyzing and plotting the results of a simulation. +A generic ``Result`` object ``result`` contains the following properties for +storing simulation data: .. cssclass:: table-striped @@ -31,8 +34,7 @@ A generic ``Result`` object ``result`` contains the following properties for sto +------------------------+-----------------------------------------------------------------------+ | ``result.final_state`` | State vector or density matrix at the last time of the evolution. | +------------------------+-----------------------------------------------------------------------+ -| ``result.stats`` | Various statistics about the evolution including the integration | -| | method used, number of collapse operators etc. | +| ``result.stats`` | Various statistics about the evolution. | +------------------------+-----------------------------------------------------------------------+ @@ -41,9 +43,11 @@ A generic ``Result`` object ``result`` contains the following properties for sto Accessing Result Data ====================== -To understand how to access the data in a Result object we will use an example as a guide, although we do not worry about the simulation details at this stage. -Like all solvers, the Master Equation solver used in this example returns an Result object, here called simply ``result``. -To see what is contained inside ``result`` we can use the print function: +To understand how to access the data in a Result object we will use an example +as a guide, although we do not worry about the simulation details at this stage. +Like all solvers, the Master Equation solver used in this example returns an +Result object, here called simply ``result``. To see what is contained inside +``result`` we can use the print function: .. doctest:: :options: +SKIP @@ -63,10 +67,11 @@ To see what is contained inside ``result`` we can use the print function: State not saved. > -The first line tells us that this data object was generated from the Master Equation solver :func:`mesolve`. -Next we have the statistics including the ODE solver used, setup time, number of collpases. -Then the integration interval is described, followed with the number of expectation value computed. -Finally, it says whether the states are stored. +The first line tells us that this data object was generated from the Master +Equation solver :func:`.mesolve`. Next we have the statistics including the ODE +solver used, setup time, number of collpases. Then the integration interval is +described, followed with the number of expectation value computed. Finally, it +says whether the states are stored. Now we have all the information needed to analyze the simulation results. To access the data for the two expectation values one can do: @@ -78,7 +83,8 @@ To access the data for the two expectation values one can do: expt0 = result.expect[0] expt1 = result.expect[1] -Recall that Python uses C-style indexing that begins with zero (i.e., [0] => 1st collapse operator data). +Recall that Python uses C-style indexing that begins with zero (i.e., +[0] => 1st collapse operator data). Alternatively, expectation values can be obtained as a dictionary: .. testcode:: @@ -88,8 +94,8 @@ Alternatively, expectation values can be obtained as a dictionary: ... expt_sx = result.e_data["sx"] -When ``e_ops`` is a list, ``e_data`` ca be used with the list index. -Together with the array of times at which these expectation values are calculated: +When ``e_ops`` is a list, ``e_data`` ca be used with the list index. Together +with the array of times at which these expectation values are calculated: .. testcode:: :skipif: True @@ -106,15 +112,21 @@ we can plot the resulting expectation values: show() -State vectors, or density matrices, are accessed in a similar manner, although typically one does not need an index (i.e [0]) since there is only one list for each of these components. -Some other solver can have other output, :func:`heomsolve`'s results can have ``ado_states`` output if the options ``store_ados`` is set, similarly, :func:`fmesolve` can return `floquet_states`. +State vectors, or density matrices, are accessed in a similar manner, although +typically one does not need an index (i.e [0]) since there is only one list for +each of these components. Some other solver can have other output, +:func:`.heomsolve`'s results can have ``ado_states`` output if the options +``store_ados`` is set, similarly, :func:`.fmmesolve` can return +``floquet_states``. Multiple Trajectories Solver Results ==================================== -Solver which compute multiple trajectories such as the Monte Carlo Equations Solvers or the Stochastics Solvers result will differ depending on whether the trajectories are flags to be saved. +Solver which compute multiple trajectories such as the Monte Carlo Equations +Solvers or the Stochastics Solvers result will differ depending on whether the +trajectories are flags to be saved. For example: .. doctest:: @@ -129,10 +141,12 @@ For example: (1, 25, 11) -When the runs are not saved, the expectation values and states are averaged over all trajectories, while a list over the runs are given when they are stored. -For a fix output format, ``average_expect`` return the average, while ``runs_states`` return the list over trajectories. -The ``runs_`` output will return ``None`` when the trajectories are not saved. -Standard derivation of the expectation values is also available: +When the runs are not saved, the expectation values and states are averaged +over all trajectories, while a list over the runs are given when they are stored. +For a fix output format, ``average_expect`` return the average, while +``runs_states`` return the list over trajectories. The ``runs_`` output will +return ``None`` when the trajectories are not saved. Standard derivation of the +expectation values is also available: +-------------------------+----------------------+------------------------------------------------------------------------+ | Reduced result | Trajectories results | Description | @@ -150,9 +164,28 @@ Standard derivation of the expectation values is also available: | ``std_e_data`` | | Dictionary of standard derivation of the expectation values. | +-------------------------+----------------------+------------------------------------------------------------------------+ -Multiple trajectories results also keep the trajectories seeds to allows recomputing the results. +Multiple trajectories results also keep the trajectories ``seeds`` to allows +recomputing the results. .. testcode:: :skipif: True seeds = result.seeds + +One last feature specific to multi-trajectories results is the addition operation +that can be used to merge sets of trajectories. + + +.. code-block:: + + >>> run1 = smesolve(H, psi, np.linspace(0, 1, 11), c_ops, e_ops=[num(N)], ntraj=25) + >>> print(run1.num_trajectories) + 25 + >>> run2 = smesolve(H, psi, np.linspace(0, 1, 11), c_ops, e_ops=[num(N)], ntraj=25) + >>> print(run2.num_trajectories) + 25 + >>> merged = run1 + run2 + >>> print(merged.num_trajectories) + 50 + +This allows one to improve statistics while keeping previous computations. diff --git a/doc/guide/dynamics/dynamics-floquet.rst b/doc/guide/dynamics/dynamics-floquet.rst index 7d3da8c38e..b5f67b6d95 100644 --- a/doc/guide/dynamics/dynamics-floquet.rst +++ b/doc/guide/dynamics/dynamics-floquet.rst @@ -81,7 +81,8 @@ so that :math:`\Phi_\alpha(t) = \exp(i\epsilon_\alpha t/\hbar) U(t,0)\Phi_\alpha Floquet formalism in QuTiP -------------------------- -QuTiP provides a family of functions to calculate the Floquet modes and quasi energies, Floquet state decomposition, etc., given a time-dependent Hamiltonian on the *callback format*, *list-string format* and *list-callback format* (see, e.g., :func:`qutip.mesolve` for details). +QuTiP provides a family of functions to calculate the Floquet modes and quasi energies, +Floquet state decomposition, etc., given a time-dependent Hamiltonian. Consider for example the case of a strongly driven two-level atom, described by the Hamiltonian @@ -92,8 +93,7 @@ Consider for example the case of a strongly driven two-level atom, described by In QuTiP we can define this Hamiltonian as follows: -.. plot:: - :context: reset +.. code-block:: python >>> delta = 0.2 * 2*np.pi >>> eps0 = 1.0 * 2*np.pi @@ -104,10 +104,11 @@ In QuTiP we can define this Hamiltonian as follows: >>> args = {'w': omega} >>> H = [H0, [H1, 'sin(w * t)']] -The :math:`t=0` Floquet modes corresponding to the Hamiltonian :eq:`eq_driven_qubit` can then be calculated using the :class:`qutip.FloquetBasis` class, which encapsulates the Floquet modes and the quasienergies: +The :math:`t=0` Floquet modes corresponding to the Hamiltonian :eq:`eq_driven_qubit` +can then be calculated using the :class:`.FloquetBasis` class, which encapsulates +the Floquet modes and the quasienergies: -.. plot:: - :context: close-figs +.. code-block:: python >>> T = 2*np.pi / omega >>> floquet_basis = FloquetBasis(H, T, args) @@ -126,9 +127,12 @@ The :math:`t=0` Floquet modes corresponding to the Hamiltonian :eq:`eq_driven_qu [0.72964231+0.j ]]] For some problems interesting observations can be draw from the quasienergy levels alone. -Consider for example the quasienergies for the driven two-level system introduced above as a function of the driving amplitude, calculated and plotted in the following example. +Consider for example the quasienergies for the driven two-level system introduced +above as a function of the driving amplitude, calculated and plotted in the following example. For certain driving amplitudes the quasienergy levels cross. -Since the quasienergies can be associated with the time-scale of the long-term dynamics due that the driving, degenerate quasienergies indicates a "freezing" of the dynamics (sometimes known as coherent destruction of tunneling). +Since the quasienergies can be associated with the time-scale of the long-term dynamics +due that the driving, degenerate quasienergies indicates a "freezing" of the dynamics +(sometimes known as coherent destruction of tunneling). .. plot:: :context: close-figs @@ -155,7 +159,8 @@ Since the quasienergies can be associated with the time-scale of the long-term d >>> plt.title(r'Floquet quasienergies') # doctest: +SKIP >>> plt.show() # doctest: +SKIP -Given the Floquet modes at :math:`t=0`, we obtain the Floquet mode at some later time :math:`t` using :meth:`FloquetBasis.mode`: +Given the Floquet modes at :math:`t=0`, we obtain the Floquet mode at some later +time :math:`t` using :meth:`.FloquetBasis.mode`: .. plot:: :context: close-figs @@ -171,8 +176,10 @@ Given the Floquet modes at :math:`t=0`, we obtain the Floquet mode at some later [[-0.37793106-0.00431336j] [-0.89630512+0.23191946j]]] -The purpose of calculating the Floquet modes is to find the wavefunction solution to the original problem :eq:`eq_driven_qubit` given some initial state :math:`\left|\psi_0\right>`. -To do that, we first need to decompose the initial state in the Floquet states, using the function :meth:`FloquetBasis.to_floquet_basis` +The purpose of calculating the Floquet modes is to find the wavefunction solution +to the original problem :eq:`eq_driven_qubit` given some initial state :math:`\left|\psi_0\right>`. +To do that, we first need to decompose the initial state in the Floquet states, +using the function :meth:`.FloquetBasis.to_floquet_basis` .. plot:: :context: close-figs @@ -183,7 +190,9 @@ To do that, we first need to decompose the initial state in the Floquet states, [(-0.645265993068382+0.7304552549315746j), (0.15517002114250228-0.1612116102238258j)] -and given this decomposition of the initial state in the Floquet states we can easily evaluate the wavefunction that is the solution to :eq:`eq_driven_qubit` at an arbitrary time :math:`t` using the function :meth:`FloquetBasis.from_floquet_basis`: +and given this decomposition of the initial state in the Floquet states we can easily +evaluate the wavefunction that is the solution to :eq:`eq_driven_qubit` at an arbitrary +time :math:`t` using the function :meth:`.FloquetBasis.from_floquet_basis`: .. plot:: :context: close-figs @@ -191,7 +200,8 @@ and given this decomposition of the initial state in the Floquet states we can e >>> t = 10 * np.random.rand() >>> psi_t = floquet_basis.from_floquet_basis(f_coeff, t) -The following example illustrates how to use the functions introduced above to calculate and plot the time-evolution of :eq:`eq_driven_qubit`. +The following example illustrates how to use the functions introduced above to calculate +and plot the time-evolution of :eq:`eq_driven_qubit`. .. plot:: guide/scripts/floquet_ex1.py :width: 4.0in @@ -200,17 +210,25 @@ The following example illustrates how to use the functions introduced above to c Pre-computing the Floquet modes for one period ---------------------------------------------- -When evaluating the Floquet states or the wavefunction at many points in time it is useful to pre-compute the Floquet modes for the first period of the driving with the required times. -The list of times to pre-compute modes for may be passed to :class:`FloquetBasis` using `precompute=tlist`, and then `:meth:`FloquetBasis.from_floquet_basis` and :meth:`FloquetBasis.to_floquet_basis` can be used to efficiently retrieve the wave function at the pre-computed times. -The following example illustrates how the example from the previous section can be solved more efficiently using these functions for pre-computing the Floquet modes: +When evaluating the Floquet states or the wavefunction at many points in time it +is useful to pre-compute the Floquet modes for the first period of the driving with +the required times. The list of times to pre-compute modes for may be passed to +:class:`.FloquetBasis` using ``precompute=tlist``, and then +:meth:`.FloquetBasis.from_floquet_basis` and :meth:`.FloquetBasis.to_floquet_basis` +can be used to efficiently retrieve the wave function at the pre-computed times. +The following example illustrates how the example from the previous section can be +solved more efficiently using these functions for pre-computing the Floquet modes: .. plot:: guide/scripts/floquet_ex2.py :width: 4.0in :include-source: -Note that the parameters and the Hamiltonian used in this example is not the same as in the previous section, and hence the different appearance of the resulting figure. +Note that the parameters and the Hamiltonian used in this example is not the same as +in the previous section, and hence the different appearance of the resulting figure. -For convenience, all the steps described above for calculating the evolution of a quantum system using the Floquet formalisms are encapsulated in the function :func:`qutip.floquet.fsesolve`. Using this function, we could have achieved the same results as in the examples above using +For convenience, all the steps described above for calculating the evolution of a +quantum system using the Floquet formalisms are encapsulated in the function :func:`.fsesolve`. +Using this function, we could have achieved the same results as in the examples above using .. code-block:: python @@ -222,21 +240,32 @@ For convenience, all the steps described above for calculating the evolution of Floquet theory for dissipative evolution ======================================== -A driven system that is interacting with its environment is not necessarily well described by the standard Lindblad master equation, since its dissipation process could be time-dependent due to the driving. In such cases a rigorious approach would be to take the driving into account when deriving the master equation. This can be done in many different ways, but one way common approach is to derive the master equation in the Floquet basis. That approach results in the so-called Floquet-Markov master equation, see Grifoni et al., Physics Reports 304, 299 (1998) for details. +A driven system that is interacting with its environment is not necessarily well +described by the standard Lindblad master equation, since its dissipation process +could be time-dependent due to the driving. In such cases a rigorious approach would +be to take the driving into account when deriving the master equation. This can be +done in many different ways, but one way common approach is to derive the master +equation in the Floquet basis. That approach results in the so-called Floquet-Markov +master equation, see Grifoni et al., Physics Reports 304, 299 (1998) for details. -For a brief summary of the derivation, the important contents for the implementation in QuTiP are listed below. +For a brief summary of the derivation, the important contents for the implementation +in QuTiP are listed below. -The floquet mode :math:`\ket{\phi_\alpha(t)}` refers to a full class of quasienergies defined by :math:`\epsilon_\alpha + k \Omega` for arbitrary :math:`k`. Hence, the quasienenergy difference between two floquet modes is given by +The floquet mode :math:`\ket{\phi_\alpha(t)}` refers to a full class of quasienergies +defined by :math:`\epsilon_\alpha + k \Omega` for arbitrary :math:`k`. Hence, the +quasienenergy difference between two floquet modes is given by .. math:: \Delta_{\alpha \beta k} = \frac{\epsilon_\alpha - \epsilon_\beta}{\hbar} + k \Omega -For any coupling operator :math:`q` (given by the user) the matrix elements in the floquet basis are calculated as: +For any coupling operator :math:`q` (given by the user) the matrix elements in +the floquet basis are calculated as: .. math:: X_{\alpha \beta k} = \frac{1}{T} \int_0^T dt \; e^{-ik \Omega t} \bra{\phi_\alpha(t)}q\ket{\phi_\beta(t)} -From the matrix elements and the spectral density :math:`J(\omega)`, the decay rate :math:`\gamma_{\alpha \beta k}` is defined: +From the matrix elements and the spectral density :math:`J(\omega)`, the decay +rate :math:`\gamma_{\alpha \beta k}` is defined: .. math:: \gamma_{\alpha \beta k} = 2 \pi J(\Delta_{\alpha \beta k}) | X_{\alpha \beta k}|^2 @@ -258,13 +287,23 @@ The density matrix of the system then evolves according to: The Floquet-Markov master equation in QuTiP ------------------------------------------- -The QuTiP function :func:`qutip.floquet.fmmesolve` implements the Floquet-Markov master equation. It calculates the dynamics of a system given its initial state, a time-dependent Hamiltonian, a list of operators through which the system couples to its environment and a list of corresponding spectral-density functions that describes the environment. In contrast to the :func:`qutip.mesolve` and :func:`qutip.mcsolve`, and the :func:`qutip.floquet.fmmesolve` does characterize the environment with dissipation rates, but extract the strength of the coupling to the environment from the noise spectral-density functions and the instantaneous Hamiltonian parameters (similar to the Bloch-Redfield master equation solver :func:`qutip.bloch_redfield.brmesolve`). +The QuTiP function :func:`.fmmesolve` implements the Floquet-Markov master equation. +It calculates the dynamics of a system given its initial state, a time-dependent +Hamiltonian, a list of operators through which the system couples to its environment +and a list of corresponding spectral-density functions that describes the environment. +In contrast to the :func:`.mesolve` and :func:`.mcsolve`, and the :func:`.fmmesolve` +does characterize the environment with dissipation rates, but extract the strength +of the coupling to the environment from the noise spectral-density functions and +the instantaneous Hamiltonian parameters (similar to the Bloch-Redfield master +equation solver :func:`.brmesolve`). .. note:: - Currently the :func:`qutip.floquet.fmmesolve` can only accept a single environment coupling operator and spectral-density function. + Currently the :func:`.fmmesolve` can only accept a single environment coupling + operator and spectral-density function. -The noise spectral-density function of the environment is implemented as a Python callback function that is passed to the solver. For example: +The noise spectral-density function of the environment is implemented as a Python +callback function that is passed to the solver. For example: .. code-block:: python @@ -273,18 +312,26 @@ The noise spectral-density function of the environment is implemented as a Pytho def noise_spectrum(omega): return (omega>0) * 0.5 * gamma1 * omega/(2*pi) -The other parameters are similar to the :func:`qutip.mesolve` and :func:`qutip.mcsolve`, and the same format for the return value is used :class:`qutip.solve.solver.Result`. The following example extends the example studied above, and uses :func:`qutip.floquet.fmmesolve` to introduce dissipation into the calculation +The other parameters are similar to the :func:`.mesolve` and :func:`.mcsolve`, +and the same format for the return value is used :class:`.Result`. +The following example extends the example studied above, and uses :func:`.fmmesolve` +to introduce dissipation into the calculation .. plot:: guide/scripts/floquet_ex3.py :width: 4.0in :include-source: -Finally, :func:`qutip.solver.floquet.fmmesolve` always expects the ``e_ops`` to be specified in the laboratory basis (as for other solvers) and we can calculate expectation values using: +Finally, :func:`.fmmesolve` always expects the ``e_ops`` to +be specified in the laboratory basis (as for other solvers) and we can calculate +expectation values using: + +.. code-block:: python - output = fmmesolve(H, psi0, tlist, [sigmax()], e_ops=[num(2)], spectra_cb=[noise_spectrum], T=T, args=args) + output = fmmesolve(H, psi0, tlist, [sigmax()], e_ops=[num(2)], + spectra_cb=[noise_spectrum], T=T, args=args) p_ex = output.expect[0] .. plot:: :context: reset :include-source: false - :nofigs: \ No newline at end of file + :nofigs: diff --git a/doc/guide/dynamics/dynamics-intro.rst b/doc/guide/dynamics/dynamics-intro.rst index b74663ab0a..cfdf0c1ce0 100644 --- a/doc/guide/dynamics/dynamics-intro.rst +++ b/doc/guide/dynamics/dynamics-intro.rst @@ -4,60 +4,73 @@ Introduction ************ -Although in some cases, we want to find the stationary states of -a quantum system, often we are interested in the dynamics: -how the state of a system or an ensemble of systems evolves with time. QuTiP provides -many ways to model dynamics. +Although in some cases, we want to find the stationary states of +a quantum system, often we are interested in the dynamics: +how the state of a system or an ensemble of systems evolves with time. +QuTiP provides many ways to model dynamics. -Broadly speaking, there are two categories -of dynamical models: unitary and non-unitary. In unitary evolution, -the state of the system remains normalized. In non-unitary, or -dissipative, systems, it does not. +There are two kinds of quantum systems: open systems that interact +with a larger environment and closed systems that do not. +In a closed system, the state can be described by a state vector. +When we are modeling an open system, or an ensemble of systems, +the use of the density matrix is mandatory. -There are two kinds of quantum systems: open systems that interact -with a larger environment and closed systems that do not. -In a closed system, the state can be described by a state vector, -although when there is entanglement a density matrix may be -needed instead. When we are modeling an open system, or an ensemble -of systems, the use of the density matrix is mandatory. - -Collapse operators are used to model the collapse of the state vector -that can occur when a measurement is performed. - -The following tables lists some of the solvers QuTiP provides for dynamic quantum systems and indicates the type of object -returned by the solver: +The following table lists of the solvers QuTiP provides for dynamic +quantum systems and indicates the type of object returned by the solver: .. list-table:: QuTiP Solvers - :widths: 25 25 50 + :widths: 50 25 25 25 :header-rows: 1 - * - Solver + * - Equation + - Function + - Class - Returns - - Remarks - * - sesolve() - - :func:`qutip.solver.Result` - - Unitary evolution, single system - * - mesolve() - - :func:`qutip.solver.Result` - - Lindblad master eqn. or Von Neuman eqn. Density matrix. - * - mcsolve() - - :func:`qutip.solver.Result` - - Monte Carlo with collapse operators - * - essolve() - - Array of expectation values - - Exponential series with collapse operators - * - bloch_redfield_solve() - - :func:`qutip.solver` - - - * - floquet_markov_solve() - - :func:`qutip.solver.Result` - - Floquet-Markov master equation - * - fmmesolve() - - :func:`qutip.solver` - - Floquet-Markov master equation - * - smesolve() - - :func:`qutip.solver.Result` - - Stochastic master equation - * - ssesolve() - - :func:`qutip.solver.Result` - - Stochastic Schrödinger equation \ No newline at end of file + * - Unitary evolution, Schrödinger equation. + - :func:`~qutip.solver.sesolve.sesolve` + - :obj:`~qutip.solver.sesolve.SESolver` + - :obj:`~qutip.solver.result.Result` + * - Periodic Schrödinger equation. + - :func:`~qutip.solver.floquet.fsesolve` + - None + - :obj:`~qutip.solver.result.Result` + * - Schrödinger equation using Krylov method + - :func:`~qutip.solver.krylovsolve.krylovsolve` + - None + - :obj:`~qutip.solver.result.Result` + * - Lindblad master eqn. or Von Neuman eqn. + - :func:`~qutip.solver.mesolve.mesolve` + - :obj:`~qutip.solver.mesolve.MESolver` + - :obj:`~qutip.solver.result.Result` + * - Monte Carlo evolution + - :func:`~qutip.solver.mcsolve.mcsolve` + - :obj:`~qutip.solver.mcsolve.MCSolver` + - :obj:`~qutip.solver.result.McResult` + * - Non-Markovian Monte Carlo + - :func:`~qutip.solver.nm_mcsolve.nm_mcsolve` + - :obj:`~qutip.solver.nm_mcsolve.NonMarkovianMCSolver` + - :obj:`~qutip.solver.result.NmmcResult` + * - Bloch-Redfield master equation + - :func:`~qutip.solver.mesolve.brmesolve` + - :obj:`~qutip.solver.mesolve.BRSolver` + - :obj:`~qutip.solver.result.Result` + * - Floquet-Markov master equation + - :func:`~qutip.solver.floquet.fmmesolve` + - :obj:`~qutip.solver.floquet.FMESolver` + - :obj:`~qutip.solver.floquet.FloquetResult` + * - Stochastic Schrödinger equation + - :func:`~qutip.solver.stochastic.ssesolve` + - :obj:`~qutip.solver.stochastic.SSESolver` + - :obj:`~qutip.solver.result.MultiTrajResult` + * - Stochastic master equation + - :func:`~qutip.solver.stochastic.smesolve` + - :obj:`~qutip.solver.stochastic.SMESolver` + - :obj:`~qutip.solver.result.MultiTrajResult` + * - Transfer Tensor Method time-evolution + - :func:`~qutip.solver.nonmarkov.transfertensor.ttmsolve` + - None + - :obj:`~qutip.solver.result.Result` + * - Hierarchical Equations of Motion evolution + - :func:`~qutip.solver.heom.bofin_solvers.heomsolve` + - :obj:`~qutip.solver.heom.bofin_solvers.HEOMSolver` + - :obj:`~qutip.solver.heom.bofin_solvers.HEOMResult` diff --git a/doc/guide/dynamics/dynamics-krylov.rst b/doc/guide/dynamics/dynamics-krylov.rst index 732157254b..b596072fb8 100644 --- a/doc/guide/dynamics/dynamics-krylov.rst +++ b/doc/guide/dynamics/dynamics-krylov.rst @@ -9,48 +9,77 @@ Krylov Solver Introduction ============= -The Krylov-subspace method is a standard method to approximate quantum dynamics. Let :math:`\left|\psi\right\rangle` be a state in a :math:`D`-dimensional complex Hilbert space that evolves under a time-independent Hamiltonian :math:`H`. Then, the :math:`N`-dimensional Krylov subspace associated with that state and Hamiltonian is given by +The Krylov-subspace method is a standard method to approximate quantum dynamics. +Let :math:`\left|\psi\right\rangle` be a state in a :math:`D`-dimensional +complex Hilbert space that evolves under a time-independent Hamiltonian :math:`H`. +Then, the :math:`N`-dimensional Krylov subspace associated with that state and +Hamiltonian is given by .. math:: :label: krylovsubspace \mathcal{K}_{N}=\operatorname{span}\left\{|\psi\rangle, H|\psi\rangle, \ldots, H^{N-1}|\psi\rangle\right\}, -where the dimension :math:`N>> from qutip import jmat, rand_ket - >>> from qutip.solver.krylovsolve import krylovsolve - >>> import numpy as np - >>> import matplotlib.pyplot as plt >>> dim = 100 - >>> e_ops = [jmat((dim - 1) / 2.0, "x"), jmat((dim - 1) / 2.0, "y"), jmat((dim - 1) / 2.0, "z")] - >>> H = .5*jmat((dim - 1) / 2.0, "z") + .5*jmat((dim - 1) / 2.0, "x") + >>> jx = jmat((dim - 1) / 2.0, "x") + >>> jy = jmat((dim - 1) / 2.0, "y") + >>> jz = jmat((dim - 1) / 2.0, "z") + >>> e_ops = [jx, jy, jz] + >>> H = (jz + jx) / 2 >>> psi0 = rand_ket(dim, seed=1) >>> tlist = np.linspace(0.0, 10.0, 200) >>> results = krylovsolve(H, psi0, tlist, krylov_dim=20, e_ops=e_ops) diff --git a/doc/guide/dynamics/dynamics-master.rst b/doc/guide/dynamics/dynamics-master.rst index 832b971e3f..d0dd25ae0b 100644 --- a/doc/guide/dynamics/dynamics-master.rst +++ b/doc/guide/dynamics/dynamics-master.rst @@ -15,19 +15,43 @@ The dynamics of a closed (pure) quantum system is governed by the Schrödinger e i\hbar\frac{\partial}{\partial t}\Psi = \hat H \Psi, -where :math:`\Psi` is the wave function, :math:`\hat H` the Hamiltonian, and :math:`\hbar` is Planck's constant. In general, the Schrödinger equation is a partial differential equation (PDE) where both :math:`\Psi` and :math:`\hat H` are functions of space and time. For computational purposes it is useful to expand the PDE in a set of basis functions that span the Hilbert space of the Hamiltonian, and to write the equation in matrix and vector form +where :math:`\Psi` is the wave function, :math:`\hat H` the Hamiltonian, and +:math:`\hbar` is Planck's constant. In general, the Schrödinger equation is a +partial differential equation (PDE) where both :math:`\Psi` and :math:`\hat H` +are functions of space and time. For computational purposes it is useful to +expand the PDE in a set of basis functions that span the Hilbert space of the +Hamiltonian, and to write the equation in matrix and vector form .. math:: i\hbar\frac{d}{dt}\left|\psi\right> = H \left|\psi\right> -where :math:`\left|\psi\right>` is the state vector and :math:`H` is the matrix representation of the Hamiltonian. This matrix equation can, in principle, be solved by diagonalizing the Hamiltonian matrix :math:`H`. In practice, however, it is difficult to perform this diagonalization unless the size of the Hilbert space (dimension of the matrix :math:`H`) is small. Analytically, it is a formidable task to calculate the dynamics for systems with more than two states. If, in addition, we consider dissipation due to the inevitable interaction with a surrounding environment, the computational complexity grows even larger, and we have to resort to numerical calculations in all realistic situations. This illustrates the importance of numerical calculations in describing the dynamics of open quantum systems, and the need for efficient and accessible tools for this task. - -The Schrödinger equation, which governs the time-evolution of closed quantum systems, is defined by its Hamiltonian and state vector. In the previous section, :ref:`tensor`, we showed how Hamiltonians and state vectors are constructed in QuTiP. -Given a Hamiltonian, we can calculate the unitary (non-dissipative) time-evolution of an arbitrary state vector :math:`\left|\psi_0\right>` (``psi0``) using the QuTiP function :func:`qutip.sesolve`. -It evolves the state vector and evaluates the expectation values for a set of operators ``e_ops`` at the points in time in the list ``times``, using an ordinary differential equation solver. - -For example, the time evolution of a quantum spin-1/2 system with tunneling rate 0.1 that initially is in the up state is calculated, and the expectation values of the :math:`\sigma_z` operator evaluated, with the following code +where :math:`\left|\psi\right>` is the state vector and :math:`H` is the matrix +representation of the Hamiltonian. This matrix equation can, in principle, be +solved by diagonalizing the Hamiltonian matrix :math:`H`. In practice, however, +it is difficult to perform this diagonalization unless the size of the Hilbert +space (dimension of the matrix :math:`H`) is small. Analytically, it is a +formidable task to calculate the dynamics for systems with more than two states. +If, in addition, we consider dissipation due to the inevitable interaction with +a surrounding environment, the computational complexity grows even larger, and +we have to resort to numerical calculations in all realistic situations. This +illustrates the importance of numerical calculations in describing the dynamics +of open quantum systems, and the need for efficient and accessible tools for +this task. + +The Schrödinger equation, which governs the time-evolution of closed quantum +systems, is defined by its Hamiltonian and state vector. In the previous +section, :ref:`tensor`, we showed how Hamiltonians and state vectors are +constructed in QuTiP. Given a Hamiltonian, we can calculate the unitary +(non-dissipative) time-evolution of an arbitrary state vector +:math:`\left|\psi_0\right>` (``psi0``) using the QuTiP solver :obj:`.SESolver` +or the function :func:`.sesolve`. It evolves the state vector and evaluates the +expectation values for a set of operators ``e_ops`` at the points in time in +the list ``times``, using an ordinary differential equation solver. + +For example, the time evolution of a quantum spin-1/2 system with tunneling rate +0.1 that initially is in the up state is calculated, and the expectation values +of the :math:`\sigma_z` operator evaluated, with the following code .. plot:: :context: reset @@ -35,33 +59,39 @@ For example, the time evolution of a quantum spin-1/2 system with tunneling rate >>> H = 2*np.pi * 0.1 * sigmax() >>> psi0 = basis(2, 0) >>> times = np.linspace(0.0, 10.0, 20) - >>> result = sesolve(H, psi0, times, e_ops=[sigmaz()]) - + >>> solver = SESolver(H) + >>> result = solver.run(psi0, times, e_ops=[sigmaz()]) + >>> result.expect + [array([ 1. , 0.78914057, 0.24548543, -0.40169579, -0.87947417, + -0.98636112, -0.67728018, -0.08257665, 0.54695111, 0.94581862, + 0.94581574, 0.54694361, -0.08258559, -0.67728679, -0.9863626 , + -0.87946979, -0.40168705, 0.24549517, 0.78914703, 1. ])] -See the next section for examples on how dissipation is included by defining a list of collapse operators and using :func:`qutip.mesolve` instead. +See the next section for examples on evolution with dissipation using +:func:`.mesolve`. -The function returns an instance of :class:`qutip.Result`, as described in the previous section :ref:`solver_result`. -The attribute ``expect`` in ``result`` is a list of expectation values for the operators that are included in the list in the fifth argument. -Adding operators to this list results in a larger output list returned by the function (one array of numbers, corresponding to the times in times, for each operator) +The function returns an instance of :class:`.Result`, as described in the +previous section :ref:`solver_result`. The attribute ``expect`` in ``result`` +is a list of expectation values for the operator(s) that are passed to the +``e_ops`` parameter. Passing multiple operators to ``e_ops`` as a list or dict +results in a vector of expectation value for each operators. ``result.e_data`` +present the expectation values as a dict of list of expect outputs, while +``result.expect`` coerce the values to numpy arrays. .. plot:: :context: close-figs - >>> result = sesolve(H, psi0, times, e_ops=[sigmaz(), sigmay()]) - >>> result.expect # doctest: +NORMALIZE_WHITESPACE - [array([ 1. , 0.78914057, 0.24548559, -0.40169513, -0.8794735 , - -0.98636142, -0.67728219, -0.08258023, 0.54694721, 0.94581685, - 0.94581769, 0.54694945, -0.08257765, -0.67728015, -0.98636097, - -0.87947476, -0.40169736, 0.24548326, 0.78913896, 1. ]), - array([ 0.00000000e+00, -6.14212640e-01, -9.69400240e-01, -9.15773457e-01, - -4.75947849e-01, 1.64593874e-01, 7.35723339e-01, 9.96584419e-01, - 8.37167094e-01, 3.24700624e-01, -3.24698160e-01, -8.37165632e-01, - -9.96584633e-01, -7.35725221e-01, -1.64596567e-01, 4.75945525e-01, - 9.15772479e-01, 9.69400830e-01, 6.14214701e-01, 2.77159958e-06])] + >>> solver.run(psi0, times, e_ops={"s_z": sigmaz(), "s_y": sigmay()}).e_data + {'s_z': [1.0, 0.7891405656865187, 0.24548542861367784, -0.40169578982499127, + ..., 0.24549516882108563, 0.7891470300925004, 0.9999999999361128], + 's_y': [0.0, -0.6142126403681064, -0.9694002807604085, -0.9157731664756708, + ..., 0.9693978141534602, 0.6142043348073879, -1.1303742482923297e-05]} -The resulting list of expectation values can easily be visualized using matplotlib's plotting functions: + +The resulting expectation values can easily be visualized using matplotlib's +plotting functions: .. plot:: :context: close-figs @@ -71,21 +101,24 @@ The resulting list of expectation values can easily be visualized using matplotl >>> times = np.linspace(0.0, 10.0, 100) >>> result = sesolve(H, psi0, times, [sigmaz(), sigmay()]) >>> fig, ax = plt.subplots() - >>> ax.plot(result.times, result.expect[0]) # doctest: +SKIP - >>> ax.plot(result.times, result.expect[1]) # doctest: +SKIP - >>> ax.set_xlabel('Time') # doctest: +SKIP - >>> ax.set_ylabel('Expectation values') # doctest: +SKIP - >>> ax.legend(("Sigma-Z", "Sigma-Y")) # doctest: +SKIP - >>> plt.show() # doctest: +SKIP - -If an empty list of operators is passed to the ``e_ops`` parameter, the :func:`qutip.sesolve` and :func:`qutip.mesolve` functions return a :class:`qutip.Result` instance that contains a list of state vectors for the times specified in ``times`` + >>> ax.plot(result.times, result.expect[0]) + >>> ax.plot(result.times, result.expect[1]) + >>> ax.set_xlabel('Time') + >>> ax.set_ylabel('Expectation values') + >>> ax.legend(("Sigma-Z", "Sigma-Y")) + >>> plt.show() + +If an empty list of operators is passed to the ``e_ops`` parameter, the +:func:`.sesolve` and :func:`.mesolve` functions return a :class:`.Result` +instance that contains a list of state vectors for the times specified in +``times`` .. plot:: :context: close-figs >>> times = [0.0, 1.0] >>> result = sesolve(H, psi0, times, []) - >>> result.states # doctest: +NORMALIZE_WHITESPACE + >>> result.states [Quantum object: dims = [[2], [1]], shape = (2, 1), type = ket Qobj data = [[1.] @@ -99,52 +132,106 @@ If an empty list of operators is passed to the ``e_ops`` parameter, the :func:`q Non-unitary evolution ======================= -While the evolution of the state vector in a closed quantum system is deterministic, open quantum systems are stochastic in nature. The effect of an environment on the system of interest is to induce stochastic transitions between energy levels, and to introduce uncertainty in the phase difference between states of the system. The state of an open quantum system is therefore described in terms of ensemble averaged states using the density matrix formalism. A density matrix :math:`\rho` describes a probability distribution of quantum states :math:`\left|\psi_n\right>`, in a matrix representation :math:`\rho = \sum_n p_n \left|\psi_n\right>\left<\psi_n\right|`, where :math:`p_n` is the classical probability that the system is in the quantum state :math:`\left|\psi_n\right>`. The time evolution of a density matrix :math:`\rho` is the topic of the remaining portions of this section. +While the evolution of the state vector in a closed quantum system is +deterministic, open quantum systems are stochastic in nature. The effect of an +environment on the system of interest is to induce stochastic transitions +between energy levels, and to introduce uncertainty in the phase difference +between states of the system. The state of an open quantum system is therefore +described in terms of ensemble averaged states using the density matrix +formalism. A density matrix :math:`\rho` describes a probability distribution +of quantum states :math:`\left|\psi_n\right>`, in a matrix representation +:math:`\rho = \sum_n p_n \left|\psi_n\right>\left<\psi_n\right|`, where +:math:`p_n` is the classical probability that the system is in the quantum state +:math:`\left|\psi_n\right>`. The time evolution of a density matrix :math:`\rho` +is the topic of the remaining portions of this section. .. _master-master: The Lindblad Master equation ============================= -The standard approach for deriving the equations of motion for a system interacting with its environment is to expand the scope of the system to include the environment. The combined quantum system is then closed, and its evolution is governed by the von Neumann equation +The standard approach for deriving the equations of motion for a system +interacting with its environment is to expand the scope of the system to +include the environment. The combined quantum system is then closed, and its +evolution is governed by the von Neumann equation .. math:: :label: neumann_total \dot \rho_{\rm tot}(t) = -\frac{i}{\hbar}[H_{\rm tot}, \rho_{\rm tot}(t)], -the equivalent of the Schrödinger equation :eq:`schrodinger` in the density matrix formalism. Here, the total Hamiltonian +the equivalent of the Schrödinger equation :eq:`schrodinger` in the density +matrix formalism. Here, the total Hamiltonian .. math:: H_{\rm tot} = H_{\rm sys} + H_{\rm env} + H_{\rm int}, -includes the original system Hamiltonian :math:`H_{\rm sys}`, the Hamiltonian for the environment :math:`H_{\rm env}`, and a term representing the interaction between the system and its environment :math:`H_{\rm int}`. Since we are only interested in the dynamics of the system, we can at this point perform a partial trace over the environmental degrees of freedom in Eq. :eq:`neumann_total`, and thereby obtain a master equation for the motion of the original system density matrix. The most general trace-preserving and completely positive form of this evolution is the Lindblad master equation for the reduced density matrix :math:`\rho = {\rm Tr}_{\rm env}[\rho_{\rm tot}]` +includes the original system Hamiltonian :math:`H_{\rm sys}`, the Hamiltonian +for the environment :math:`H_{\rm env}`, and a term representing the interaction +between the system and its environment :math:`H_{\rm int}`. Since we are only +interested in the dynamics of the system, we can at this point perform a partial +trace over the environmental degrees of freedom in Eq. :eq:`neumann_total`, and +thereby obtain a master equation for the motion of the original system density +matrix. The most general trace-preserving and completely positive form of this +evolution is the Lindblad master equation for the reduced density matrix +:math:`\rho = {\rm Tr}_{\rm env}[\rho_{\rm tot}]` .. math:: :label: lindblad_master_equation \dot\rho(t)=-\frac{i}{\hbar}[H(t),\rho(t)]+\sum_n \frac{1}{2} \left[2 C_n \rho(t) C_n^\dagger - \rho(t) C_n^\dagger C_n - C_n^\dagger C_n \rho(t)\right] -where the :math:`C_n = \sqrt{\gamma_n} A_n` are collapse operators, and :math:`A_n` are the operators through which the environment couples to the system in :math:`H_{\rm int}`, and :math:`\gamma_n` are the corresponding rates. The derivation of Eq. :eq:`lindblad_master_equation` may be found in several sources, and will not be reproduced here. Instead, we emphasize the approximations that are required to arrive at the master equation in the form of Eq. :eq:`lindblad_master_equation` from physical arguments, and hence perform a calculation in QuTiP: - -- **Separability:** At :math:`t=0` there are no correlations between the system and its environment such that the total density matrix can be written as a tensor product :math:`\rho^I_{\rm tot}(0) = \rho^I(0) \otimes \rho^I_{\rm env}(0)`. - -- **Born approximation:** Requires: (1) that the state of the environment does not significantly change as a result of the interaction with the system; (2) The system and the environment remain separable throughout the evolution. These assumptions are justified if the interaction is weak, and if the environment is much larger than the system. In summary, :math:`\rho_{\rm tot}(t) \approx \rho(t)\otimes\rho_{\rm env}`. - -- **Markov approximation** The time-scale of decay for the environment :math:`\tau_{\rm env}` is much shorter than the smallest time-scale of the system dynamics :math:`\tau_{\rm sys} \gg \tau_{\rm env}`. This approximation is often deemed a "short-memory environment" as it requires that environmental correlation functions decay on a time-scale fast compared to those of the system. - -- **Secular approximation** Stipulates that elements in the master equation corresponding to transition frequencies satisfy :math:`|\omega_{ab}-\omega_{cd}| \ll 1/\tau_{\rm sys}`, i.e., all fast rotating terms in the interaction picture can be neglected. It also ignores terms that lead to a small renormalization of the system energy levels. This approximation is not strictly necessary for all master-equation formalisms (e.g., the Block-Redfield master equation), but it is required for arriving at the Lindblad form :eq:`lindblad_master_equation` which is used in :func:`qutip.mesolve`. - - -For systems with environments satisfying the conditions outlined above, the Lindblad master equation :eq:`lindblad_master_equation` governs the time-evolution of the system density matrix, giving an ensemble average of the system dynamics. In order to ensure that these approximations are not violated, it is important that the decay rates :math:`\gamma_n` be smaller than the minimum energy splitting in the system Hamiltonian. Situations that demand special attention therefore include, for example, systems strongly coupled to their environment, and systems with degenerate or nearly degenerate energy levels. +where the :math:`C_n = \sqrt{\gamma_n} A_n` are collapse operators, and +:math:`A_n` are the operators through which the environment couples to the +system in :math:`H_{\rm int}`, and :math:`\gamma_n` are the corresponding rates. +The derivation of Eq. :eq:`lindblad_master_equation` may be found in several +sources, and will not be reproduced here. Instead, we emphasize the +approximations that are required to arrive at the master equation in the form +of Eq. :eq:`lindblad_master_equation` from physical arguments, and hence +perform a calculation in QuTiP: + +- **Separability:** At :math:`t=0` there are no correlations between the system + and its environment such that the total density matrix can be written as a + tensor product :math:`\rho^I_{\rm tot}(0) = \rho^I(0) \otimes \rho^I_{\rm env}(0)`. + +- **Born approximation:** Requires: (1) that the state of the environment does + not significantly change as a result of the interaction with the system; + (2) The system and the environment remain separable throughout the evolution. + These assumptions are justified if the interaction is weak, and if the + environment is much larger than the system. In summary, + :math:`\rho_{\rm tot}(t) \approx \rho(t)\otimes\rho_{\rm env}`. + +- **Markov approximation** The time-scale of decay for the environment + :math:`\tau_{\rm env}` is much shorter than the smallest time-scale of the + system dynamics :math:`\tau_{\rm sys} \gg \tau_{\rm env}`. This approximation + is often deemed a "short-memory environment" as it requires that environmental + correlation functions decay on a time-scale fast compared to those of the system. + +- **Secular approximation** Stipulates that elements in the master equation corresponding + to transition frequencies satisfy :math:`|\omega_{ab}-\omega_{cd}| \ll 1/\tau_{\rm sys}`, + i.e., all fast rotating terms in the interaction picture can be neglected. + It also ignores terms that lead to a small renormalization of the system energy levels. + This approximation is not strictly necessary for all master-equation formalisms + (e.g., the Block-Redfield master equation), but it is required for arriving + at the Lindblad form :eq:`lindblad_master_equation` which is used in :func:`.mesolve`. + + +For systems with environments satisfying the conditions outlined above, the +Lindblad master equation :eq:`lindblad_master_equation` governs the +time-evolution of the system density matrix, giving an ensemble average of the +system dynamics. In order to ensure that these approximations are not violated, +it is important that the decay rates :math:`\gamma_n` be smaller than the +minimum energy splitting in the system Hamiltonian. Situations that demand +special attention therefore include, for example, systems strongly coupled to +their environment, and systems with degenerate or nearly degenerate energy levels. For non-unitary evolution of a quantum systems, i.e., evolution that includes incoherent processes such as relaxation and dephasing, it is common to use -master equations. In QuTiP, the function :func:`qutip.mesolve` is used for both: +master equations. In QuTiP, the function :func:`.mesolve` is used for both: the evolution according to the Schrödinger equation and to the master equation, -even though these two equations of motion are very different. The :func:`qutip.mesolve` +even though these two equations of motion are very different. The :func:`.mesolve` function automatically determines if it is sufficient to use the Schrödinger equation (if no collapse operators were given) or if it has to use the master equation (if collapse operators were given). Note that to calculate @@ -161,15 +248,14 @@ describe the strength of the processes. In QuTiP, the product of the square root of the rate and the operator that describe the dissipation process is called a collapse operator. A list of collapse operators (``c_ops``) is passed as the fourth argument to the -:func:`qutip.mesolve` function in order to define the dissipation processes in the master -equation. When the ``c_ops`` isn't empty, the :func:`qutip.mesolve` function will use +:func:`.mesolve` function in order to define the dissipation processes in the master +equation. When the ``c_ops`` isn't empty, the :func:`.mesolve` function will use the master equation instead of the unitary Schrödinger equation. Using the example with the spin dynamics from the previous section, we can easily add a relaxation process (describing the dissipation of energy from the spin to its environment), by adding ``np.sqrt(0.05) * sigmax()`` in the fourth -parameter to the :func:`qutip.mesolve` function and moving the expectation -operators ``[sigmaz(), sigmay()]`` to the fifth argument. +parameter to the :func:`.mesolve` function. .. plot:: @@ -178,18 +264,22 @@ operators ``[sigmaz(), sigmay()]`` to the fifth argument. >>> times = np.linspace(0.0, 10.0, 100) >>> result = mesolve(H, psi0, times, [np.sqrt(0.05) * sigmax()], e_ops=[sigmaz(), sigmay()]) >>> fig, ax = plt.subplots() - >>> ax.plot(times, result.expect[0]) # doctest: +SKIP - >>> ax.plot(times, result.expect[1]) # doctest: +SKIP - >>> ax.set_xlabel('Time') # doctest: +SKIP - >>> ax.set_ylabel('Expectation values') # doctest: +SKIP - >>> ax.legend(("Sigma-Z", "Sigma-Y")) # doctest: +SKIP - >>> plt.show() # doctest: +SKIP + >>> ax.plot(times, result.expect[0]) + >>> ax.plot(times, result.expect[1]) + >>> ax.set_xlabel('Time') + >>> ax.set_ylabel('Expectation values') + >>> ax.legend(("Sigma-Z", "Sigma-Y")) + >>> plt.show() -Here, 0.05 is the rate and the operator :math:`\sigma_x` (:func:`qutip.sigmax`) describes the dissipation -process. +Here, 0.05 is the rate and the operator :math:`\sigma_x` (:func:`.sigmax`) +describes the dissipation process. -Now a slightly more complex example: Consider a two-level atom coupled to a leaky single-mode cavity through a dipole-type interaction, which supports a coherent exchange of quanta between the two systems. If the atom initially is in its groundstate and the cavity in a 5-photon Fock state, the dynamics is calculated with the lines following code +Now a slightly more complex example: Consider a two-level atom coupled to a +leaky single-mode cavity through a dipole-type interaction, which supports a +coherent exchange of quanta between the two systems. If the atom initially is +in its groundstate and the cavity in a 5-photon Fock state, the dynamics is +calculated with the lines following code .. plot:: :context: close-figs @@ -200,15 +290,15 @@ Now a slightly more complex example: Consider a two-level atom coupled to a leak >>> sm = tensor(destroy(2), qeye(10)) >>> H = 2 * np.pi * a.dag() * a + 2 * np.pi * sm.dag() * sm + 2 * np.pi * 0.25 * (sm * a.dag() + sm.dag() * a) >>> result = mesolve(H, psi0, times, [np.sqrt(0.1)*a], e_ops=[a.dag()*a, sm.dag()*sm]) - >>> plt.figure() # doctest: +SKIP - >>> plt.plot(times, result.expect[0]) # doctest: +SKIP - >>> plt.plot(times, result.expect[1]) # doctest: +SKIP - >>> plt.xlabel('Time') # doctest: +SKIP - >>> plt.ylabel('Expectation values') # doctest: +SKIP - >>> plt.legend(("cavity photon number", "atom excitation probability")) # doctest: +SKIP - >>> plt.show() # doctest: +SKIP + >>> plt.figure() + >>> plt.plot(times, result.expect[0]) + >>> plt.plot(times, result.expect[1]) + >>> plt.xlabel('Time') + >>> plt.ylabel('Expectation values') + >>> plt.legend(("cavity photon number", "atom excitation probability")) + >>> plt.show() .. plot:: :context: reset :include-source: false - :nofigs: \ No newline at end of file + :nofigs: diff --git a/doc/guide/dynamics/dynamics-monte.rst b/doc/guide/dynamics/dynamics-monte.rst index 5dda6b35eb..4ca406bf90 100644 --- a/doc/guide/dynamics/dynamics-monte.rst +++ b/doc/guide/dynamics/dynamics-monte.rst @@ -10,52 +10,87 @@ Monte Carlo Solver Introduction ============= -Where as the density matrix formalism describes the ensemble average over many identical realizations of a quantum system, the Monte Carlo (MC), or quantum-jump approach to wave function evolution, allows for simulating an individual realization of the system dynamics. Here, the environment is continuously monitored, resulting in a series of quantum jumps in the system wave function, conditioned on the increase in information gained about the state of the system via the environmental measurements. In general, this evolution is governed by the Schrödinger equation with a **non-Hermitian** effective Hamiltonian +Where as the density matrix formalism describes the ensemble average over many +identical realizations of a quantum system, the Monte Carlo (MC), or +quantum-jump approach to wave function evolution, allows for simulating an +individual realization of the system dynamics. Here, the environment is +continuously monitored, resulting in a series of quantum jumps in the system +wave function, conditioned on the increase in information gained about the +state of the system via the environmental measurements. In general, this +evolution is governed by the Schrödinger equation with a **non-Hermitian** +effective Hamiltonian .. math:: :label: heff H_{\rm eff}=H_{\rm sys}-\frac{i\hbar}{2}\sum_{i}C^{+}_{n}C_{n}, -where again, the :math:`C_{n}` are collapse operators, each corresponding to a separate irreversible process with rate :math:`\gamma_{n}`. Here, the strictly negative non-Hermitian portion of Eq. :eq:`heff` gives rise to a reduction in the norm of the wave function, that to first-order in a small time :math:`\delta t`, is given by :math:`\left<\psi(t+\delta t)|\psi(t+\delta t)\right>=1-\delta p` where +where again, the :math:`C_{n}` are collapse operators, each corresponding to a +separate irreversible process with rate :math:`\gamma_{n}`. Here, the strictly +negative non-Hermitian portion of Eq. :eq:`heff` gives rise to a reduction in +the norm of the wave function, that to first-order in a small time +:math:`\delta t`, is given by +:math:`\left<\psi(t+\delta t)|\psi(t+\delta t)\right>=1-\delta p` where .. math:: :label: jump \delta p =\delta t \sum_{n}\left<\psi(t)|C^{+}_{n}C_{n}|\psi(t)\right>, -and :math:`\delta t` is such that :math:`\delta p \ll 1`. With a probability of remaining in the state :math:`\left|\psi(t+\delta t)\right>` given by :math:`1-\delta p`, the corresponding quantum jump probability is thus Eq. :eq:`jump`. If the environmental measurements register a quantum jump, say via the emission of a photon into the environment, or a change in the spin of a quantum dot, the wave function undergoes a jump into a state defined by projecting :math:`\left|\psi(t)\right>` using the collapse operator :math:`C_{n}` corresponding to the measurement +and :math:`\delta t` is such that :math:`\delta p \ll 1`. With a probability +of remaining in the state :math:`\left|\psi(t+\delta t)\right>` given by +:math:`1-\delta p`, the corresponding quantum jump probability is thus Eq. +:eq:`jump`. If the environmental measurements register a quantum jump, say via +the emission of a photon into the environment, or a change in the spin of a +quantum dot, the wave function undergoes a jump into a state defined by +projecting :math:`\left|\psi(t)\right>` using the collapse operator +:math:`C_{n}` corresponding to the measurement .. math:: :label: project \left|\psi(t+\delta t)\right>=C_{n}\left|\psi(t)\right>/\left<\psi(t)|C_{n}^{+}C_{n}|\psi(t)\right>^{1/2}. -If more than a single collapse operator is present in Eq. :eq:`heff`, the probability of collapse due to the :math:`i\mathrm{th}`-operator :math:`C_{i}` is given by +If more than a single collapse operator is present in Eq. :eq:`heff`, the +probability of collapse due to the :math:`i\mathrm{th}`-operator :math:`C_{i}` +is given by .. math:: :label: pcn P_{i}(t)=\left<\psi(t)|C_{i}^{+}C_{i}|\psi(t)\right>/\delta p. -Evaluating the MC evolution to first-order in time is quite tedious. Instead, QuTiP uses the following algorithm to simulate a single realization of a quantum system. Starting from a pure state :math:`\left|\psi(0)\right>`: +Evaluating the MC evolution to first-order in time is quite tedious. Instead, +QuTiP uses the following algorithm to simulate a single realization of a quantum system. +Starting from a pure state :math:`\left|\psi(0)\right>`: -- **Ia:** Choose a random number :math:`r_1` between zero and one, representing the probability that a quantum jump occurs. +- **Ia:** Choose a random number :math:`r_1` between zero and one, representing + the probability that a quantum jump occurs. -- **Ib:** Choose a random number :math:`r_2` between zero and one, used to select which collapse operator was responsible for the jump. +- **Ib:** Choose a random number :math:`r_2` between zero and one, used to + select which collapse operator was responsible for the jump. -- **II:** Integrate the Schrödinger equation, using the effective Hamiltonian :eq:`heff` until a time :math:`\tau` such that the norm of the wave function satisfies :math:`\left<\psi(\tau)\right.\left|\psi(\tau)\right> = r_1`, at which point a jump occurs. +- **II:** Integrate the Schrödinger equation, using the effective Hamiltonian + :eq:`heff` until a time :math:`\tau` such that the norm of the wave function + satisfies :math:`\left<\psi(\tau)\right.\left|\psi(\tau)\right> = r_1`, at + which point a jump occurs. -- **III:** The resultant jump projects the system at time :math:`\tau` into one of the renormalized states given by Eq. :eq:`project`. The corresponding collapse operator :math:`C_{n}` is chosen such that :math:`n` is the smallest integer satisfying: +- **III:** The resultant jump projects the system at time :math:`\tau` into one + of the renormalized states given by Eq. :eq:`project`. The corresponding + collapse operator :math:`C_{n}` is chosen such that :math:`n` is the smallest + integer satisfying: .. math:: :label: mc3 \sum_{i=1}^{n} P_{n}(\tau) \ge r_2 -where the individual :math:`P_{n}` are given by Eq. :eq:`pcn`. Note that the left hand side of Eq. :eq:`mc3` is, by definition, normalized to unity. +where the individual :math:`P_{n}` are given by Eq. :eq:`pcn`. Note that the +left hand side of Eq. :eq:`mc3` is, by definition, normalized to unity. -- **IV:** Using the renormalized state from step III as the new initial condition at time :math:`\tau`, draw a new random number, and repeat the above procedure until the final simulation time is reached. +- **IV:** Using the renormalized state from step III as the new initial + condition at time :math:`\tau`, draw a new random number, and repeat the + above procedure until the final simulation time is reached. .. _monte-qutip: @@ -63,16 +98,22 @@ where the individual :math:`P_{n}` are given by Eq. :eq:`pcn`. Note that the le Monte Carlo in QuTiP ==================== -In QuTiP, Monte Carlo evolution is implemented with the :func:`qutip.mcsolve` function. It takes nearly the same arguments as the :func:`qutip.mesolve` -function for master-equation evolution, except that the initial state must be a ket vector, as oppose to a density matrix, and there is an optional keyword parameter ``ntraj`` that defines the number of stochastic trajectories to be simulated. By default, ``ntraj=500`` indicating that 500 Monte Carlo trajectories will be performed. +In QuTiP, Monte Carlo evolution is implemented with the :func:`.mcsolve` +function. It takes nearly the same arguments as the :func:`.mesolve` +function for master-equation evolution, except that the initial state must be a +ket vector, as oppose to a density matrix, and there is an optional keyword +parameter ``ntraj`` that defines the number of stochastic trajectories to be +simulated. By default, ``ntraj=500`` indicating that 500 Monte Carlo +trajectories will be performed. -To illustrate the use of the Monte Carlo evolution of quantum systems in QuTiP, let's again consider the case of a two-level atom coupled to a leaky cavity. The only differences to the master-equation treatment is that in this case we invoke the :func:`qutip.mcsolve` function instead of :func:`qutip.mesolve` +To illustrate the use of the Monte Carlo evolution of quantum systems in QuTiP, +let's again consider the case of a two-level atom coupled to a leaky cavity. +The only differences to the master-equation treatment is that in this case we +invoke the :func:`.mcsolve` function instead of :func:`.mesolve` .. plot:: :context: reset - from qutip.solver.mcsolve import MCSolver, mcsolve - times = np.linspace(0.0, 10.0, 200) psi0 = tensor(fock(2, 0), fock(10, 8)) a = tensor(qeye(2), destroy(10)) @@ -90,89 +131,184 @@ To illustrate the use of the Monte Carlo evolution of quantum systems in QuTiP, .. guide-dynamics-mc1: -The advantage of the Monte Carlo method over the master equation approach is that only the state vector is required to be kept in the computers memory, as opposed to the entire density matrix. For large quantum system this becomes a significant advantage, and the Monte Carlo solver is therefore generally recommended for such systems. For example, simulating a Heisenberg spin-chain consisting of 10 spins with random parameters and initial states takes almost 7 times longer using the master equation rather than Monte Carlo approach with the default number of trajectories running on a quad-CPU machine. Furthermore, it takes about 7 times the memory as well. However, for small systems, the added overhead of averaging a large number of stochastic trajectories to obtain the open system dynamics, as well as starting the multiprocessing functionality, outweighs the benefit of the minor (in this case) memory saving. Master equation methods are therefore generally more efficient when Hilbert space sizes are on the order of a couple of hundred states or smaller. +The advantage of the Monte Carlo method over the master equation approach is that +only the state vector is required to be kept in the computers memory, as opposed +to the entire density matrix. For large quantum system this becomes a significant +advantage, and the Monte Carlo solver is therefore generally recommended for such +systems. For example, simulating a Heisenberg spin-chain consisting of 10 spins +with random parameters and initial states takes almost 7 times longer using the +master equation rather than Monte Carlo approach with the default number of +trajectories running on a quad-CPU machine. Furthermore, it takes about 7 times +the memory as well. However, for small systems, the added overhead of averaging +a large number of stochastic trajectories to obtain the open system dynamics, as +well as starting the multiprocessing functionality, outweighs the benefit of the +minor (in this case) memory saving. Master equation methods are therefore +generally more efficient when Hilbert space sizes are on the order of a couple +of hundred states or smaller. Monte Carlo Solver Result ------------------------- -The Monte Carlo solver returns a :class:`qutip.MultitrajResult` object consisting of expectation values and/or states. -The main difference with :func:`qutip.mesolve`'s :class:`qutip.Result` is that it optionally stores the result of each trajectory together with their averages. -When trajectories are stored, ``result.runs_expect`` is a list over the expectation operators, trajectories and times in that order. -The averages are stored in ``result.average_expect`` and the standard derivation of the expectation values in ``result.std_expect``. -When the states are returned, ``result.runs_states`` will be an array of length ``ntraj``. Each element contains an array of "Qobj" type ket with the same number of elements as ``times``. ``result.average_states`` is a list of density matrices computed as the average of the states at each time step. -Furthermore, the output will also contain a list of times at which the collapse occurred, and which collapse operators did the collapse. These can be obtained in ``result.col_times`` and ``result.col_which`` respectively. +The Monte Carlo solver returns a :class:`.McResult` object consisting of +expectation values and/or states. The main difference with :func:`.mesolve`'s +:class:`.Result` is that it optionally stores the result of each trajectory +together with their averages. When trajectories are stored, ``result.runs_expect`` +is a list over the expectation operators, trajectories and times in that order. +The averages are stored in ``result.average_expect`` and the standard derivation +of the expectation values in ``result.std_expect``. When the states are returned, +``result.runs_states`` will be an array of length ``ntraj``. Each element +contains an array of "Qobj" type ket with the same number of elements as ``times``. +``result.average_states`` is a list of density matrices computed as the average +of the states at each time step. Furthermore, the output will also contain a +list of times at which the collapse occurred, and which collapse operators did +the collapse. These can be obtained in ``result.col_times`` and +``result.col_which`` respectively. -Photocurrent ------------- +.. _monte-ntraj: -The photocurrent, previously computed using the ``photocurrent_sesolve`` and ``photocurrent_sesolve`` functions, are now included in the output of :func:`qutip.solver.mcsolve` as ``result.photocurrent``. +Changing the Number of Trajectories +----------------------------------- +By default, the ``mcsolve`` function runs 500 trajectories. +This value was chosen because it gives good accuracy, Monte Carlo errors scale +as :math:`1/n` where :math:`n` is the number of trajectories, and simultaneously +does not take an excessive amount of time to run. However, you can change the +number of trajectories to fit your needs. In order to run 1000 trajectories in +the above example, we can simply modify the call to ``mcsolve`` like: -.. plot:: - :context: close-figs +.. code-block:: - times = np.linspace(0.0, 10.0, 200) - psi0 = tensor(fock(2, 0), fock(10, 8)) - a = tensor(qeye(2), destroy(10)) - sm = tensor(destroy(2), qeye(10)) - H = 2*np.pi*a.dag()*a + 2*np.pi*sm.dag()*sm + 2*np.pi*0.25*(sm*a.dag() + sm.dag()*a) - data = mcsolve(H, psi0, times, [np.sqrt(0.1) * a], e_ops=[a.dag() * a, sm.dag() * sm]) + data = mcsolve(H, psi0, times, c_ops e_ops=e_ops, ntraj=1000) - plt.figure() - plt.plot((times[:-1] + times[1:])/2, data.photocurrent[0]) - plt.title('Monte Carlo Photocurrent') - plt.xlabel('Time') - plt.ylabel('Photon detections') - plt.show() +where we have added the keyword argument ``ntraj=1000`` at the end of the inputs. +Now, the Monte Carlo solver will calculate expectation values for both operators, +``a.dag() * a, sm.dag() * sm`` averaging over 1000 trajectories. -.. _monte-ntraj: +Other than a target number of trajectories, it is possible to use a computation +time or errors bars as condition to stop computing trajectories. -Changing the Number of Trajectories ------------------------------------ +``timeout`` is quite simple as ``mcsolve`` will stop starting the computation of +new trajectories when it is reached. Thus: -By default, the ``mcsolve`` function runs 500 trajectories. -This value was chosen because it gives good accuracy, Monte Carlo errors scale as :math:`1/n` where :math:`n` is the number of trajectories, and simultaneously does not take an excessive amount of time to run. -However, you can change the number of trajectories to fit your needs. -In order to run 1000 trajectories in the above example, we can simply modify the call to ``mcsolve`` like: + +.. code-block:: + + data = mcsolve(H, psi0, times, [np.sqrt(0.1) * a], e_ops=e_ops, ntraj=1000, timeout=60) + +Will compute 60 seconds of trajectories or 1000, which ever is reached first. +The solver will finish any trajectory started when the timeout is reached. Therefore +if the computation time of a single trajectory is quite long, the overall computation +time can be much longer that the provided timeout. + +Lastly, ``mcsolve`` can be instructed to stop when the statistical error of the +expectation values get under a certain value. When computing the average over +trajectories, the error on these are computed using +`jackknife resampling `_ +for each expect and each time and the computation will be stopped when all these values +are under the tolerance passed to ``target_tol``. Therefore: + +.. code-block:: + + data = mcsolve(H, psi0, times, [np.sqrt(0.1) * a], e_ops=e_ops, + ntraj=1000, target_tol=0.01, timeout=600) + +will stop either after all errors bars on expectation values are under ``0.01``, 1000 +trajectories are computed or 10 minutes have passed, whichever comes first. When a +single values is passed, it is used as the absolute value of the tolerance. +When a pair of values is passed, it is understood as an absolute and relative +tolerance pair. For even finer control, one such pair can be passed for each ``e_ops``. +For example: + +.. code-block:: + + data = mcsolve(H, psi0, times, c_ops, e_ops=e_ops, target_tol=[ + (1e-5, 0.1), + (0, 0), + ]) + +will stop when the error bars on the expectation values of the first ``e_ops`` are +under 10% of their average values. + +If after computation of some trajectories, it is determined that more are needed, it +is possible to add trajectories to existing result by adding result together: + +.. code-block:: + + >>> run1 = mcsolve(H, psi, times, c_ops, e_ops=e_ops, ntraj=25) + >>> print(run1.num_trajectories) + 25 + >>> run2 = mcsolve(H, psi, times, c_ops, e_ops=e_ops, ntraj=25) + >>> print(run2.num_trajectories) + 25 + >>> merged = run1 + run2 + >>> print(merged.num_trajectories) + 50 + +Note that this merging operation only checks that the result are compatible -- +i.e. that the ``e_ops`` and ``tlist`` are the same. It does not check that the same initial state or +Hamiltonian where used. + + +This can be used to explore the convergence of the Monte Carlo solver. +For example, the following code block plots expectation values for 1, 10 and 100 +trajectories: .. plot:: :context: close-figs - data = mcsolve(H, psi0, times, [np.sqrt(0.1) * a], e_ops=[a.dag() * a, sm.dag() * sm], ntraj=1000) + solver = MCSolver(H, c_ops=[np.sqrt(0.1) * a]) + c_ops=[np.sqrt(0.1) * a] + e_ops = [a.dag() * a, sm.dag() * sm] -where we have added the keyword argument ``ntraj=1000`` at the end of the inputs. -Now, the Monte Carlo solver will calculate expectation values for both operators, ``a.dag() * a, sm.dag() * sm`` averaging over 1000 trajectories. + data1 = mcsolve(H, psi0, times, c_ops, e_ops=e_ops, ntraj=1) + data10 = data1 + mcsolve(H, psi0, times, c_ops, e_ops=e_ops, ntraj=9) + data100 = data10 + mcsolve(H, psi0, times, c_ops, e_ops=e_ops, ntraj=90) + expt1 = data1.expect + expt10 = data10.expect + expt100 = data100.expect + + plt.figure() + plt.plot(times, expt1[0], label="ntraj=1") + plt.plot(times, expt10[0], label="ntraj=10") + plt.plot(times, expt100[0], label="ntraj=100") + plt.title('Monte Carlo time evolution') + plt.xlabel('Time') + plt.ylabel('Expectation values') + plt.legend() + plt.show() Using the Improved Sampling Algorithm ------------------------------------- -Oftentimes, quantum jumps are rare. This is especially true in the context of simulating gates -for quantum information purposes, where typical gate times are orders of magnitude smaller than -typical timescales for decoherence. In this case, using the standard monte-carlo sampling algorithm, -we often repeatedly sample the no-jump trajectory. We can thus reduce the number of required runs -by only sampling the no-jump trajectory once. We then extract the no-jump probability :math:`p`, -and for all future runs we only sample random numbers :math:`r_1` where :math:`r_1>p`, thus ensuring -that a jump will occur. When it comes time to compute expectation values, we weight the no-jump -trajectory by :math:`p` and the jump trajectories by :math:`1-p`. This algorithm is described -in [Abd19]_ and can be utilized by setting the option ``"improved_sampling"`` in the call to -``mcsolve``: +Oftentimes, quantum jumps are rare. This is especially true in the context of +simulating gates for quantum information purposes, where typical gate times are +orders of magnitude smaller than typical timescales for decoherence. In this case, +using the standard monte-carlo sampling algorithm, we often repeatedly sample the +no-jump trajectory. We can thus reduce the number of required runs by only +sampling the no-jump trajectory once. We then extract the no-jump probability +:math:`p`, and for all future runs we only sample random numbers :math:`r_1` +where :math:`r_1>p`, thus ensuring that a jump will occur. When it comes time to +compute expectation values, we weight the no-jump trajectory by :math:`p` and +the jump trajectories by :math:`1-p`. This algorithm is described in [Abd19]_ +and can be utilized by setting the option ``"improved_sampling"`` in the call +to ``mcsolve``: .. plot:: :context: close-figs - data = mcsolve(H, psi0, times, [np.sqrt(0.1) * a], e_ops=[a.dag() * a, sm.dag() * sm], options={"improved_sampling": True}) + data = mcsolve(H, psi0, times, [np.sqrt(0.1) * a], options={"improved_sampling": True}) -where in this case the first run samples the no-jump trajectory, and the remaining 499 trajectories are all -guaranteed to include (at least) one jump. +where in this case the first run samples the no-jump trajectory, and the +remaining 499 trajectories are all guaranteed to include (at least) one jump. -The power of this algorithm is most obvious when considering systems that rarely undergo jumps. -For instance, consider the following T1 simulation of a qubit with a lifetime of 10 microseconds -(assuming time is in units of nanoseconds) +The power of this algorithm is most obvious when considering systems that rarely +undergo jumps. For instance, consider the following T1 simulation of a qubit with +a lifetime of 10 microseconds (assuming time is in units of nanoseconds) .. plot:: @@ -184,8 +320,13 @@ For instance, consider the following T1 simulation of a qubit with a lifetime of omega = 2.0 * np.pi * 1.0 H0 = -0.5 * omega * sigmaz() gamma = 1/10000 - data = mcsolve([H0], psi0, times, [np.sqrt(gamma) * sm], [sm.dag() * sm], ntraj=100) - data_imp = mcsolve([H0], psi0, times, [np.sqrt(gamma) * sm], [sm.dag() * sm],ntraj=100, options={"improved_sampling": True}) + data = mcsolve( + [H0], psi0, times, [np.sqrt(gamma) * sm], [sm.dag() * sm], ntraj=100 + ) + data_imp = mcsolve( + [H0], psi0, times, [np.sqrt(gamma) * sm], [sm.dag() * sm], ntraj=100, + options={"improved_sampling": True} + ) plt.figure() plt.plot(times, data.expect[0], label="original") @@ -198,110 +339,112 @@ For instance, consider the following T1 simulation of a qubit with a lifetime of plt.show() -The original sampling algorithm samples the no-jump trajectory on average 96.7% of the time, while the improved -sampling algorithm only does so once. +The original sampling algorithm samples the no-jump trajectory on average 96.7% +of the time, while the improved sampling algorithm only does so once. -.. _monte-reuse: +.. _monte-seeds: -Reusing Hamiltonian Data ------------------------- +Reproducibility +--------------- -.. note:: This section covers a specialized topic and may be skipped if you are new to QuTiP. +For reproducibility of Monte-Carlo computations it is possible to set the seed of the random +number generator: +.. code-block:: -In order to solve a given simulation as fast as possible, the solvers in QuTiP take the given input operators and break them down into simpler components before passing them on to the ODE solvers. -Although these operations are reasonably fast, the time spent organizing data can become appreciable when repeatedly solving a system over, for example, many different initial conditions. -In cases such as this, the Monte Carlo Solver may be reused after the initial configuration, thus speeding up calculations. + >>> res1 = mcsolve(H, psi0, tlist, c_ops, e_ops=e_ops, seeds=1, ntraj=1) + >>> res2 = mcsolve(H, psi0, tlist, c_ops, e_ops=e_ops, seeds=1, ntraj=1) + >>> res3 = mcsolve(H, psi0, tlist, c_ops, e_ops=e_ops, seeds=2, ntraj=1) + >>> np.allclose(res1, res2) + True + >>> np.allclose(res1, res3) + False +The ``seeds`` parameter can either be an integer or a numpy ``SeedSequence``, which +will then be used to create seeds for each trajectory. Alternatively it may be a list of +intergers or ``SeedSequence`` s with one seed for each trajectories. Seeds available in +the result object can be used to redo the same evolution: -Using the previous example, we will calculate the dynamics for two different initial states, with the Hamiltonian data being reused on the second call -.. plot:: - :context: close-figs +.. code-block:: - times = np.linspace(0.0, 10.0, 200) - psi0 = tensor(fock(2, 0), fock(10, 5)) - a = tensor(qeye(2), destroy(10)) - sm = tensor(destroy(2), qeye(10)) + >>> res1 = mcsolve(H, psi0, tlist, c_ops, e_ops=e_ops, ntraj=10) + >>> res2 = mcsolve(H, psi0, tlist, c_ops, e_ops=e_ops, seeds=res1.seeds, ntraj=10) + >>> np.allclose(res1, res2) + True - H = 2*np.pi*a.dag()*a + 2*np.pi*sm.dag()*sm + 2*np.pi*0.25*(sm*a.dag() + sm.dag()*a) - solver = MCSolver(H, c_ops=[np.sqrt(0.1) * a]) - data1 = solver.run(psi0, times, e_ops=[a.dag() * a, sm.dag() * sm], ntraj=100) - psi1 = tensor(fock(2, 0), coherent(10, 2 - 1j)) - data2 = solver.run(psi1, times, e_ops=[a.dag() * a, sm.dag() * sm], ntraj=100) - plt.figure() - plt.plot(times, data1.expect[0], "b", times, data1.expect[1], "r", lw=2) - plt.plot(times, data2.expect[0], 'b--', times, data2.expect[1], 'r--', lw=2) - plt.title('Monte Carlo time evolution') - plt.xlabel('Time', fontsize=14) - plt.ylabel('Expectation values', fontsize=14) - plt.legend(("cavity photon number", "atom excitation probability")) - plt.show() +.. _monte-parallel: -.. guide-dynamics-mc2: +Running trajectories in parallel +-------------------------------- -The ``MCSolver`` also allows adding new trajectories after the first computation. This is shown in the next example where the results of two separated runs with identical conditions are merged into a single ``result`` object. +Monte-Carlo evolutions often need hundreds of trajectories to obtain sufficient +statistics. Since all trajectories are independent of each other, they can be computed +in parallel. The option ``map`` can take ``"serial"``, ``"parallel"`` or ``"loky"``. +Both ``"parallel"`` and ``"loky"`` compute trajectories on multiple CPUs using +respectively the `multiprocessing `_ +and `loky `_ python modules. -.. plot:: - :context: close-figs +.. code-block:: - times = np.linspace(0.0, 10.0, 200) - psi0 = tensor(fock(2, 0), fock(10, 5)) - a = tensor(qeye(2), destroy(10)) - sm = tensor(destroy(2), qeye(10)) + >>> res_par = mcsolve(H, psi0, tlist, c_ops, e_ops=e_ops, options={"map": "parallel"}, seeds=1) + >>> res_ser = mcsolve(H, psi0, tlist, c_ops, e_ops=e_ops, options={"map": "serial"}, seeds=1) + >>> np.allclose(res_par.average_expect, res_ser.average_expect) + True - H = 2*np.pi*a.dag()*a + 2*np.pi*sm.dag()*sm + 2*np.pi*0.25*(sm*a.dag() + sm.dag()*a) - solver = MCSolver(H, c_ops=[np.sqrt(0.1) * a]) - data1 = solver.run(psi0, times, e_ops=[a.dag() * a, sm.dag() * sm], ntraj=1, seed=1) - data2 = solver.run(psi0, times, e_ops=[a.dag() * a, sm.dag() * sm], ntraj=1, seed=3) - data_merged = data1 + data2 +Note that when running in parallel, the order in which the trajectories are added +to the result can differ. Therefore - plt.figure() - plt.plot(times, data1.expect[0], times, data1.expect[1], lw=2) - plt.plot(times, data2.expect[0], '--', times, data2.expect[1], '--', lw=2) - plt.plot(times, data_merged.expect[0], ':', times, data_merged.expect[1], ':', lw=2) - plt.title('Monte Carlo time evolution') - plt.xlabel('Time', fontsize=14) - plt.ylabel('Expectation values', fontsize=14) - plt.legend(("cavity photon number", "atom excitation probability")) - plt.show() +.. code-block:: + >>> print(res_par.seeds[:3]) + [SeedSequence(entropy=1,spawn_key=(1,),), + SeedSequence(entropy=1,spawn_key=(0,),), + SeedSequence(entropy=1,spawn_key=(2,),)] -This can be used to explore the convergence of the Monte Carlo solver. -For example, the following code block plots expectation values for 1, 10 and 100 trajectories: + >>> print(res_ser.seeds[:3]) + [SeedSequence(entropy=1,spawn_key=(0,),), + SeedSequence(entropy=1,spawn_key=(1,),), + SeedSequence(entropy=1,spawn_key=(2,),)] -.. plot:: - :context: close-figs - solver = MCSolver(H, c_ops=[np.sqrt(0.1) * a]) +Photocurrent +------------ - data1 = solver.run(psi0, times, e_ops=[a.dag() * a, sm.dag() * sm], ntraj=1) - data10 = data1 + solver.run(psi0, times, e_ops=[a.dag() * a, sm.dag() * sm], ntraj=9) - data100 = data10 + solver.run(psi0, times, e_ops=[a.dag() * a, sm.dag() * sm], ntraj=90) +The photocurrent, previously computed using the ``photocurrent_sesolve`` and +``photocurrent_sesolve`` functions, are now included in the output of +:func:`.mcsolve` as ``result.photocurrent``. - expt1 = data1.expect - expt10 = data10.expect - expt100 = data100.expect + +.. plot:: + :context: close-figs + + times = np.linspace(0.0, 10.0, 200) + psi0 = tensor(fock(2, 0), fock(10, 8)) + a = tensor(qeye(2), destroy(10)) + sm = tensor(destroy(2), qeye(10)) + e_ops = [a.dag() * a, sm.dag() * sm] + H = 2*np.pi*a.dag()*a + 2*np.pi*sm.dag()*sm + 2*np.pi*0.25*(sm*a.dag() + sm.dag()*a) + data = mcsolve(H, psi0, times, [np.sqrt(0.1) * a], e_ops=e_ops) plt.figure() - plt.plot(times, expt1[0], label="ntraj=1") - plt.plot(times, expt10[0], label="ntraj=10") - plt.plot(times, expt100[0], label="ntraj=100") - plt.title('Monte Carlo time evolution') + plt.plot((times[:-1] + times[1:])/2, data.photocurrent[0]) + plt.title('Monte Carlo Photocurrent') plt.xlabel('Time') - plt.ylabel('Expectation values') - plt.legend() + plt.ylabel('Photon detections') plt.show() + .. openmcsolve: Open Systems ------------ -``mcsolve`` can be used to study system with have measured and dissipative interaction with the bath. -This is done by using a liouvillian including the dissipative interaction instead of an Hamiltonian. +``mcsolve`` can be used to study systems which have measurement and dissipative +interactions with their environment. This is done by passing a Liouvillian including the +dissipative interaction to the solver instead of a Hamiltonian. .. plot:: :context: close-figs @@ -322,120 +465,6 @@ This is done by using a liouvillian including the dissipative interaction instea plt.show() - -.. _monte-nonmarkov: - -Monte Carlo for Non-Markovian Dynamics --------------------------------------- - -The Monte Carlo solver of QuTiP can also be used to solve the dynamics of time-local non-Markovian master equations, i.e., master equations of the Lindblad form - -.. math:: - :label: lindblad_master_equation_with_rates - - \dot\rho(t) = -\frac{i}{\hbar} [H, \rho(t)] + \sum_n \frac{\gamma_n(t)}{2} \left[2 A_n \rho(t) A_n^\dagger - \rho(t) A_n^\dagger A_n - A_n^\dagger A_n \rho(t)\right] - -with "rates" :math:`\gamma_n(t)` that can take negative values. -This can be done with the :func:`qutip.nm_mcsolve` function. -The function is based on the influence martingale formalism [Donvil22]_ and formally requires that the collapse operators :math:`A_n` satisfy a completeness relation of the form - -.. math:: - :label: nmmcsolve_completeness - - \sum_n A_n^\dagger A_n = \alpha \mathbb{I} , - -where :math:`\mathbb{I}` is the identity operator on the system Hilbert space and :math:`\alpha>0`. -Note that when the collapse operators of a model don't satisfy such a relation, ``qutip.nm_mcsolve`` automatically adds an extra collapse operator such that :eq:`nmmcsolve_completeness` is satisfied. -The rate corresponding to this extra collapse operator is set to zero. - -Technically, the influence martingale formalism works as follows. -We introduce an influence martingale :math:`\mu(t)`, which follows the evolution of the system state. -When no jump happens, it evolves as - -.. math:: - :label: influence_cont - - \mu(t) = \exp\left( \alpha\int_0^t K(\tau) d\tau \right) - -where :math:`K(t)` is for now an arbitrary function. -When a jump corresponding to the collapse operator :math:`A_n` happens, the influence martingale becomes - -.. math:: - :label: influence_disc - - \mu(t+\delta t) = \mu(t)\left(\frac{K(t)-\gamma_n(t)}{\gamma_n(t)}\right) - -Assuming that the state :math:`\bar\rho(t)` computed by the Monte Carlo average - -.. math:: - :label: mc_paired_state - - \bar\rho(t) = \frac{1}{N}\sum_{l=1}^N |\psi_l(t)\rangle\langle \psi_l(t)| - -solves a Lindblad master equation with collapse operators :math:`A_n` and rates :math:`\Gamma_n(t)`, the state :math:`\rho(t)` defined by - -.. math:: - :label: mc_martingale_state - - \rho(t) = \frac{1}{N}\sum_{l=1}^N \mu_l(t) |\psi_l(t)\rangle\langle \psi_l(t)| - -solves a Lindblad master equation with collapse operators :math:`A_n` and shifted rates :math:`\gamma_n(t)-K(t)`. -Thus, while :math:`\Gamma_n(t) \geq 0`, the new "rates" :math:`\gamma_n(t) = \Gamma_n(t) - K(t)` satisfy no positivity requirement. - -The input of :func:`qutip.nm_mcsolve` is almost the same as for :func:`qutip.mcsolve`. -The only difference is how the collapse operators and rate functions should be defined. -``nm_mcsolve`` requires collapse operators :math:`A_n` and target "rates" :math:`\gamma_n` (which are allowed to take negative values) to be given in list form ``[[C_1, gamma_1], [C_2, gamma_2], ...]``. -Note that we give the actual rate and not its square root, and that ``nm_mcsolve`` automatically computes associated jump rates :math:`\Gamma_n(t)\geq0` appropriate for simulation. - -We conclude with a simple example demonstrating the usage of the ``nm_mcsolve`` function. -For more elaborate, physically motivated examples, we refer to the `accompanying tutorial notebook `_. - - -.. plot:: - :context: reset - - import qutip as qt - - times = np.linspace(0, 1, 201) - psi0 = qt.basis(2, 1) - a0 = qt.destroy(2) - H = a0.dag() * a0 - - # Rate functions - gamma1 = "kappa * nth" - gamma2 = "kappa * (nth+1) + 12 * np.exp(-2*t**3) * (-np.sin(15*t)**2)" - # gamma2 becomes negative during some time intervals - - # nm_mcsolve integration - ops_and_rates = [] - ops_and_rates.append([a0.dag(), gamma1]) - ops_and_rates.append([a0, gamma2]) - MCSol = qt.nm_mcsolve(H, psi0, times, ops_and_rates, - args={'kappa': 1.0 / 0.129, 'nth': 0.063}, - e_ops=[a0.dag() * a0, a0 * a0.dag()], - options={'map': 'parallel'}, ntraj=2500) - - # mesolve integration for comparison - d_ops = [[qt.lindblad_dissipator(a0.dag(), a0.dag()), gamma1], - [qt.lindblad_dissipator(a0, a0), gamma2]] - MESol = qt.mesolve(H, psi0, times, d_ops, e_ops=[a0.dag() * a0, a0 * a0.dag()], - args={'kappa': 1.0 / 0.129, 'nth': 0.063}) - - plt.figure() - plt.plot(times, MCSol.expect[0], 'g', - times, MCSol.expect[1], 'b', - times, MCSol.trace, 'r') - plt.plot(times, MESol.expect[0], 'g--', - times, MESol.expect[1], 'b--') - plt.title('Monte Carlo time evolution') - plt.xlabel('Time') - plt.ylabel('Expectation values') - plt.legend((r'$\langle 1 | \rho | 1 \rangle$', - r'$\langle 0 | \rho | 0 \rangle$', - r'$\operatorname{tr} \rho$')) - plt.show() - - .. plot:: :context: reset :include-source: false diff --git a/doc/guide/dynamics/dynamics-nmmonte.rst b/doc/guide/dynamics/dynamics-nmmonte.rst new file mode 100644 index 0000000000..fb8c0bcbde --- /dev/null +++ b/doc/guide/dynamics/dynamics-nmmonte.rst @@ -0,0 +1,130 @@ +.. _monte-nonmarkov: + +******************************************* +Monte Carlo for Non-Markovian Dynamics +******************************************* + +The Monte Carlo solver of QuTiP can also be used to solve the dynamics of +time-local non-Markovian master equations, i.e., master equations of the Lindblad +form + +.. math:: + :label: lindblad_master_equation_with_rates + + \dot\rho(t) = -\frac{i}{\hbar} [H, \rho(t)] + \sum_n \frac{\gamma_n(t)}{2} \left[2 A_n \rho(t) A_n^\dagger - \rho(t) A_n^\dagger A_n - A_n^\dagger A_n \rho(t)\right] + +with "rates" :math:`\gamma_n(t)` that can take negative values. +This can be done with the :func:`.nm_mcsolve` function. +The function is based on the influence martingale formalism [Donvil22]_ and +formally requires that the collapse operators :math:`A_n` satisfy a completeness +relation of the form + +.. math:: + :label: nmmcsolve_completeness + + \sum_n A_n^\dagger A_n = \alpha \mathbb{I} , + +where :math:`\mathbb{I}` is the identity operator on the system Hilbert space +and :math:`\alpha>0`. +Note that when the collapse operators of a model don't satisfy such a relation, +``nm_mcsolve`` automatically adds an extra collapse operator such that +:eq:`nmmcsolve_completeness` is satisfied. +The rate corresponding to this extra collapse operator is set to zero. + +Technically, the influence martingale formalism works as follows. +We introduce an influence martingale :math:`\mu(t)`, which follows the evolution +of the system state. When no jump happens, it evolves as + +.. math:: + :label: influence_cont + + \mu(t) = \exp\left( \alpha\int_0^t K(\tau) d\tau \right) + +where :math:`K(t)` is for now an arbitrary function. +When a jump corresponding to the collapse operator :math:`A_n` happens, the +influence martingale becomes + +.. math:: + :label: influence_disc + + \mu(t+\delta t) = \mu(t)\left(\frac{K(t)-\gamma_n(t)}{\gamma_n(t)}\right) + +Assuming that the state :math:`\bar\rho(t)` computed by the Monte Carlo average + +.. math:: + :label: mc_paired_state + + \bar\rho(t) = \frac{1}{N}\sum_{l=1}^N |\psi_l(t)\rangle\langle \psi_l(t)| + +solves a Lindblad master equation with collapse operators :math:`A_n` and rates +:math:`\Gamma_n(t)`, the state :math:`\rho(t)` defined by + +.. math:: + :label: mc_martingale_state + + \rho(t) = \frac{1}{N}\sum_{l=1}^N \mu_l(t) |\psi_l(t)\rangle\langle \psi_l(t)| + +solves a Lindblad master equation with collapse operators :math:`A_n` and shifted +rates :math:`\gamma_n(t)-K(t)`. Thus, while :math:`\Gamma_n(t) \geq 0`, the new +"rates" :math:`\gamma_n(t) = \Gamma_n(t) - K(t)` satisfy no positivity requirement. + +The input of :func:`.nm_mcsolve` is almost the same as for :func:`.mcsolve`. +The only difference is how the collapse operators and rate functions should be +defined. ``nm_mcsolve`` requires collapse operators :math:`A_n` and target "rates" +:math:`\gamma_n` (which are allowed to take negative values) to be given in list +form ``[[C_1, gamma_1], [C_2, gamma_2], ...]``. Note that we give the actual +rate and not its square root, and that ``nm_mcsolve`` automatically computes +associated jump rates :math:`\Gamma_n(t)\geq0` appropriate for simulation. + +We conclude with a simple example demonstrating the usage of the ``nm_mcsolve`` +function. For more elaborate, physically motivated examples, we refer to the +`accompanying tutorial notebook `_. + + +.. plot:: + :context: reset + + times = np.linspace(0, 1, 201) + psi0 = basis(2, 1) + a0 = destroy(2) + H = a0.dag() * a0 + + # Rate functions + gamma1 = "kappa * nth" + gamma2 = "kappa * (nth+1) + 12 * np.exp(-2*t**3) * (-np.sin(15*t)**2)" + # gamma2 becomes negative during some time intervals + + # nm_mcsolve integration + ops_and_rates = [] + ops_and_rates.append([a0.dag(), gamma1]) + ops_and_rates.append([a0, gamma2]) + MCSol = nm_mcsolve(H, psi0, times, ops_and_rates, + args={'kappa': 1.0 / 0.129, 'nth': 0.063}, + e_ops=[a0.dag() * a0, a0 * a0.dag()], + options={'map': 'parallel'}, ntraj=2500) + + # mesolve integration for comparison + d_ops = [[lindblad_dissipator(a0.dag(), a0.dag()), gamma1], + [lindblad_dissipator(a0, a0), gamma2]] + MESol = mesolve(H, psi0, times, d_ops, e_ops=[a0.dag() * a0, a0 * a0.dag()], + args={'kappa': 1.0 / 0.129, 'nth': 0.063}) + + plt.figure() + plt.plot(times, MCSol.expect[0], 'g', + times, MCSol.expect[1], 'b', + times, MCSol.trace, 'r') + plt.plot(times, MESol.expect[0], 'g--', + times, MESol.expect[1], 'b--') + plt.title('Monte Carlo time evolution') + plt.xlabel('Time') + plt.ylabel('Expectation values') + plt.legend((r'$\langle 1 | \rho | 1 \rangle$', + r'$\langle 0 | \rho | 0 \rangle$', + r'$\operatorname{tr} \rho$')) + plt.show() + + +.. plot:: + :context: reset + :include-source: false + :nofigs: diff --git a/doc/guide/dynamics/dynamics-options.rst b/doc/guide/dynamics/dynamics-options.rst index 587000ca84..5531450d09 100644 --- a/doc/guide/dynamics/dynamics-options.rst +++ b/doc/guide/dynamics/dynamics-options.rst @@ -9,7 +9,8 @@ Setting Options for the Dynamics Solvers from qutip.solver.mesolve import MESolver, mesolve import numpy as np -Occasionally it is necessary to change the built in parameters of the dynamics solvers used by for example the :func:`qutip.mesolve` and :func:`qutip.mcsolve` functions. +Occasionally it is necessary to change the built in parameters of the dynamics +solvers used by for example the :func:`.mesolve` and :func:`.mcsolve` functions. The options for all dynamics solvers may be changed by using the dictionaries. .. testcode:: [dynamics_options] @@ -23,22 +24,25 @@ Supported solver options and their default can be seen using the class interface help(MESolver.options) -Options supported by the ODE integration depend on the "method" options of the solver, they can be listed through the integrator method of the solvers: +Options supported by the ODE integration depend on the "method" options of the solver, +they can be listed through the integrator method of the solvers: .. testcode:: [dynamics_options] help(MESolver.integrator("adams").options) -See `Integrator <../../apidoc/classes.html#classes-ode>`_ for a list of supported methods. +See :ref:`classes-ode` for a list of supported methods. -As an example, let us consider changing the integrator, turn the GUI off, and strengthen the absolute tolerance. +As an example, let us consider changing the integrator, turn the GUI off, and +strengthen the absolute tolerance. .. testcode:: [dynamics_options] options = {method="bdf", "atol": 1e-10, "progress_bar": False} -To use these new settings we can use the keyword argument ``options`` in either the :func:`qutip.mesolve` and :func:`qutip.mcsolve` function:: +To use these new settings we can use the keyword argument ``options`` in either +the :func:`.mesolve` and :func:`.mcsolve` function:: >>> mesolve(H0, psi0, tlist, c_op_list, [sigmaz()], options=options) diff --git a/doc/guide/dynamics/dynamics-propagator.rst b/doc/guide/dynamics/dynamics-propagator.rst new file mode 100644 index 0000000000..c464aadefa --- /dev/null +++ b/doc/guide/dynamics/dynamics-propagator.rst @@ -0,0 +1,81 @@ +.. _propagator: + +********************* +Computing propagators +********************* + +Sometime the evolution of a single state is not sufficient and the full propagator +is desired. QuTiP has the :func:`.propagator` function to compute them: + +.. code-block:: + + >>> H = sigmaz() + np.pi *sigmax() + >>> psi_t = sesolve(H, basis(2, 1), [0, 0.5, 1]).states + >>> prop = propagator(H, [0, 0.5, 1]) + + >>> print((psi_t[1] - prop[1] @ basis(2, 1)).norm()) + 2.455965272327082e-06 + + >>> print((psi_t[2] - prop[2] @ basis(2, 1)).norm()) + 2.0071900004562142e-06 + + +The first argument is the Hamiltonian, any time dependent system format is +accepted. The function also accepts an optional `c_ops` argument for collapse operators. +When used, a propagator for density matrices is computed: +:math:`\rho(t) = U(t)(\rho(0))`: + +.. code-block:: + + >>> rho_t = mesolve(H, fock_dm(2, 1), [0, 0.5, 1], c_ops=[sigmam()]).states + >>> prop = propagator(H, [0, 0.5, 1], c_ops=[sigmam()]) + + >>> print((rho_t[1] - prop[1](fock_dm(2, 1))).norm()) + 7.23009476734681e-07 + + >>> print((rho_t[2] - prop[2](fock_dm(2, 1))).norm()) + 1.2666967766644768e-06 + + +The propagator function is also available as a class: + +.. code-block:: + + >>> U = Propagator(H, c_ops=[sigmam()]) + + >>> state_0_5 = U(0.5)(fock_dm(2, 1)) + >>> state_1 = U(1., t_start=0.5)(state_0_5) + + >>> print((rho_t[1] - state_0_5).norm()) + 7.23009476734681e-07 + + >>> print((rho_t[2] - state_1).norm()) + 8.355518501351504e-07 + +The :obj:`.Propagator` can take ``options`` and ``args`` as a solver instance. + +.. _propagator_solver: + + +Using a solver to compute a propagator +====================================== + +Many solvers accept an operator as the initial state. When an identity matrix is +passed as the initial state, the propagator is computed. This can be used to compute +a propagator for Bloch-Redfield or Floquet equations: + +.. code-block:: + + >>> delta = 0.2 * 2*np.pi + >>> eps0 = 1.0 * 2*np.pi + >>> gamma1 = 0.5 + + >>> H = - delta/2.0 * sigmax() - eps0/2.0 * sigmaz() + + >>> def ohmic_spectrum(w): + >>> if w == 0.0: # dephasing inducing noise + >>> return gamma1 + >>> else: # relaxation inducing noise + >>> return gamma1 / 2 * (w / (2 * np.pi)) * (w > 0.0) + + >>> prop = brmesolve(H, qeye(2), [0, 1], a_ops=[[sigmax(), ohmic_spectrum]]).final_state diff --git a/doc/guide/dynamics/dynamics-stochastic.rst b/doc/guide/dynamics/dynamics-stochastic.rst index faa2baf9a4..8e60bc2c44 100644 --- a/doc/guide/dynamics/dynamics-stochastic.rst +++ b/doc/guide/dynamics/dynamics-stochastic.rst @@ -15,7 +15,7 @@ QuTiP by solving the general equation .. math:: :label: general_form - d \rho (t) = d_1 \rho dt + \sum_n d_{2,n} \rho dW_n, + d \rho (t) = d_1 \rho \, dt + \sum_n d_{2,n} \rho \, dW_n, where :math:`dW_n` is a Wiener increment, which has the expectation values :math:`E[dW] = 0` and :math:`E[dW^2] = dt`. @@ -41,7 +41,8 @@ where :math:`H` is the Hamiltonian, :math:`S_n` are the stochastic collapse oper e_n = \left<\psi(t)|S_n + S_n^\dagger|\psi(t)\right> -In QuTiP, this equation can be solved using the function :func:`qutip.solver.stochastic.ssesolve`, which is implemented by defining :math:`d_1` and :math:`d_{2,n}` from Equation :eq:`general_form` as +In QuTiP, this equation can be solved using the function :func:`~qutip.solver.stochastic.ssesolve`, +which is implemented by defining :math:`d_1` and :math:`d_{2,n}` from Equation :eq:`general_form` as .. math:: :label: d1_def @@ -55,13 +56,19 @@ and d_{2, n} = S_n - \frac{e_n}{2}. -The solver :func:`qutip.solver.stochastic.ssesolve` will construct the operators :math:`d_1` and :math:`d_{2,n}` once the user passes the Hamiltonian (``H``) and the stochastic operator list (``sc_ops``). -As with the :func:`qutip.solver.mcsolve.mcsolve`, the number of trajectories and the seed for the noise realisation can be fixed using the arguments: ``ntraj`` and ``seeds``, respectively. -If the user also requires the measurement output, the options entry ``{"store_measurement": True}`` should be included. +The solver :func:`~qutip.solver.stochastic.ssesolve` will construct the operators +:math:`d_1` and :math:`d_{2,n}` once the user passes the Hamiltonian (``H``) and +the stochastic operator list (``sc_ops``). As with the :func:`~qutip.solver.mcsolve.mcsolve`, +the number of trajectories and the seed for the noise realisation can be fixed using +the arguments: ``ntraj`` and ``seeds``, respectively. If the user also requires the +measurement output, the options entry ``{"store_measurement": True}`` should be included. -Per default, homodyne is used. Heterodyne detections can be easily simulated by passing the arguments ``'heterodyne=True'`` to :func:`qutip.solver.stochastic.ssesolve`. +Per default, homodyne is used. Heterodyne detections can be easily simulated by passing +the arguments ``'heterodyne=True'`` to :func:`~qutip.solver.stochastic.ssesolve`. -Examples of how to solve the stochastic Schrodinger equation using QuTiP can be found in this `development notebook `_. +.. + Examples of how to solve the stochastic Schrodinger equation using QuTiP + can be found in this `development notebook <...TODO-Merge 61...>`_. Stochastic Master Equation ========================== @@ -89,12 +96,15 @@ and .. math:: :label: h_cal - \mathcal{H}[A]\rho = A\rho(t) + \rho(t) A^\dagger - \tr[A\rho(t) + \rho(t) A^\dagger]. + \mathcal{H}[A]\rho = A\rho(t) + \rho(t) A^\dagger - \mathrm{tr}[A\rho(t) + \rho(t) A^\dagger]. -In QuTiP, solutions for the stochastic master equation are obtained using the solver :func:`qutip.solver.stochastic.smesolve`. -The implementation takes into account 2 types of collapse operators. :math:`C_i` (``c_ops``) represent the dissipation in the environment, while :math:`S_n` (``sc_ops``) are monitored operators. -The deterministic part of the evolution, described by the :math:`d_1` in Equation :eq:`general_form`, takes into account all operators :math:`C_i` and :math:`S_n`: +In QuTiP, solutions for the stochastic master equation are obtained using the solver +:func:`~qutip.solver.stochastic.smesolve`. The implementation takes into account 2 +types of collapse operators. :math:`C_i` (``c_ops``) represent the dissipation in +the environment, while :math:`S_n` (``sc_ops``) are monitored operators. +The deterministic part of the evolution, described by the :math:`d_1` in Equation +:eq:`general_form`, takes into account all operators :math:`C_i` and :math:`S_n`: .. math:: :label: liouvillian @@ -109,31 +119,31 @@ The stochastic part, :math:`d_{2,n}`, is given solely by the operators :math:`S_ .. math:: :label: stochastic_smesolve - d_{2,n} = S_n \rho(t) + \rho(t) S_n^\dagger - \tr \left(S_n \rho (t) - + \rho(t) S_n^\dagger \right)\rho(t). + d_{2,n} = S_n \rho(t) + \rho(t) S_n^\dagger - \mathrm{tr}\left(S_n \rho (t) + + \rho(t) S_n^\dagger \right)\,\rho(t). As in the stochastic Schrodinger equation, heterodyne detection can be chosen by passing ``heterodyne=True``. Example ------- -Below, we solve the dynamics for an optical cavity at 0K whose output is monitored using homodyne detection. -The cavity decay rate is given by :math:`\kappa` and the :math:`\Delta` is the cavity detuning with respect to the driving field. -The measurement operators can be passed using the option ``m_ops``. The homodyne current :math:`J_x` is calculated using +Below, we solve the dynamics for an optical cavity at 0K whose output is monitored +using homodyne detection. The cavity decay rate is given by :math:`\kappa` and the +:math:`\Delta` is the cavity detuning with respect to the driving field. +The measurement operators can be passed using the option ``m_ops``. The homodyne +current :math:`J_x` is calculated using .. math:: :label: measurement_result J_x = \langle x \rangle + dW / dt, -where :math:`x` is the operator passed using ``m_ops``. The results are available in ``result.measurements``. +where :math:`x` is the operator passed using ``m_ops``. The results are available +in ``result.measurements``. .. plot:: :context: reset - import numpy as np - import matplotlib.pyplot as plt - import qutip # parameters DIM = 20 # Hilbert space dimension @@ -143,20 +153,20 @@ where :math:`x` is the operator passed using ``m_ops``. The results are availabl NUMBER_OF_TRAJECTORIES = 500 # operators - a = qutip.destroy(DIM) + a = destroy(DIM) x = a + a.dag() H = DELTA * a.dag() * a - rho_0 = qutip.coherent(DIM, np.sqrt(INTENSITY)) + rho_0 = coherent(DIM, np.sqrt(INTENSITY)) times = np.arange(0, 1, 0.0025) - stoc_solution = qutip.smesolve( + stoc_solution = smesolve( H, rho_0, times, c_ops=[], sc_ops=[np.sqrt(KAPPA) * a], e_ops=[x], ntraj=NUMBER_OF_TRAJECTORIES, - options={"dt": 0.00125, "store_measurement":True,} + options={"dt": 0.00125, "store_measurement": True,} ) fig, ax = plt.subplots() @@ -168,8 +178,20 @@ where :math:`x` is the operator passed using ``m_ops``. The results are availabl ax.set_xlabel('Time') ax.legend() +.. + TODO merge qutip-tutorials#61 + For other examples on :func:`qutip.solver.stochastic.smesolve`, see the + `following notebook <...>`_, as well as these notebooks available at + `QuTiP Tutorials page `_: + `heterodyne detection <...>`_, + `inefficient detection <...>`_, and + `feedback control `_. + + +The stochastic solvers share many features with :func:`.mcsolve`, such as +end conditions, seed control and running in parallel. See the sections +:ref:`monte-ntraj`, :ref:`monte-seeds` and :ref:`monte-parallel` for details. -For other examples on :func:`qutip.solver.stochastic.smesolve`, see the `following notebook `_, as well as these notebooks available at `QuTiP Tutorials page `_: `heterodyne detection `_, `inefficient detection `_, and `feedback control `_. .. plot:: :context: reset diff --git a/doc/guide/dynamics/dynamics-time.rst b/doc/guide/dynamics/dynamics-time.rst index 4885803a65..0c60c5e482 100644 --- a/doc/guide/dynamics/dynamics-time.rst +++ b/doc/guide/dynamics/dynamics-time.rst @@ -11,42 +11,48 @@ Time-Dependent Operators In the previous examples of quantum evolution, we assumed that the systems under consideration were described by time-independent Hamiltonians. However, many systems have explicit time dependence in either the Hamiltonian, -or the collapse operators describing coupling to the environment, and sometimes both components might depend on time. -The time-evolutions solvers such as :func:`sesolve`, :func:`brmesolve`, etc. are all capable of handling time-dependent Hamiltonians and collapse terms. -QuTiP use :class:`QobjEvo` to represent time-dependent quantum operators. -There are three different ways to build a :class:`QobjEvo`: : +or the collapse operators describing coupling to the environment, and sometimes +both components might depend on time. The time-evolutions solvers such as :func:`.sesolve`, +:func:`.brmesolve`, etc. are all capable of handling time-dependent Hamiltonians and collapse terms. +QuTiP use :obj:`.QobjEvo` to represent time-dependent quantum operators. +There are three different ways to build a :obj:`.QobjEvo`: -1. **Function based**: Build the time dependent operator from a function returning a :class:`Qobj`: +1. **Function based**: Build the time dependent operator from a function returning a :obj:`.Qobj`: .. code-block:: python def oper(t): return num(N) + (destroy(N) + create(N)) * np.sin(t) + H_t = QobjEvo(oper) -1. **List based**: The time dependent quantum operator is represented as a list of ``qobj`` and ``[qobj, coefficient]`` pairs. +1. **List based**: The time dependent quantum operator is represented as a list of ``qobj`` and ``[qobj, coefficient]`` pairs: .. code-block:: python H_t = QobjEvo([num(N), [create(N), lambda t: np.sin(t)], [destroy(N), lambda t: np.sin(t)]]) -3. **coefficent based**: The product of a :class:`Qobj` with a :class:`Coefficient` result in a :class:`QobjEvo`: +3. **coefficent based**: The product of a :obj:`.Qobj` with a :obj:`.Coefficient`, +created by the :func:`.coefficient` function, result in a :obj:`.QobjEvo`: .. code-block:: python coeff = coefficent(lambda t: np.sin(t)) H_t = num(N) + (destroy(N) + create(N)) * coeff -These 3 examples will create the same time dependent operator, however the function based method will usually be slower when used in solver. +These 3 examples will create the same time dependent operator, however the function +based method will usually be slower when used in solver. -Solvers will accept a :class:`QobjEvo`: when an operator is expected: this include the Hamiltonian ``H``, collapse operators, expectation values operators, the operator of :func:`brmesolve`'s ``a_ops``, etc. -Exception are :func:`krylovsolve`'s Hamiltonian and HEOM's Bath operators. +Most solvers accept a :obj:`.QobjEvo` when an operator is expected: this include +the Hamiltonian ``H``, collapse operators, expectation values operators, the operator +of :func:`.brmesolve`'s ``a_ops``, etc. Exception are :func:`.krylovsolve`'s +Hamiltonian and HEOM's Bath operators. -Most solvers will accept any format that could be made into a :class:`QobjEvo`: for the Hamiltonian. +Most solvers will accept any format that could be made into a :obj:`.QobjEvo` for the Hamiltonian. All of the following are equivalent: @@ -57,19 +63,23 @@ All of the following are equivalent: result = mesolve(oper, ...) -Collapse operator also accept a list of object that could be made into :class:`QobjEvo`:. -However one needs to be careful about not confusing the list nature of the `c_ops` parameter with list format quantum system. -In the following call: +Collapse operator also accept a list of object that could be made into :obj:`.QobjEvo`. +However one needs to be careful about not confusing the list nature of the `c_ops` +parameter with list format quantum system. In the following call: .. code-block:: python result = mesolve(H_t, ..., c_ops=[num(N), [destroy(N) + create(N), lambda t: np.sin(t)]]) -:func:`mesolve` will see 2 collapses operators: ``num(N)`` and ``[destroy(N) + create(N), lambda t: np.sin(t)]``. -It is therefore preferred to pass each collapse operator as either a :class:`Qobj`: or a :class:`QobjEvo`:. +:func:`.mesolve` will see 2 collapses operators: +``num(N)`` and ``[destroy(N) + create(N), lambda t: np.sin(t)]``. +It is therefore preferred to pass each collapse operator as either a :obj:`.Qobj` +or a :obj:`.QobjEvo`. -As an example, we will look at a case with a time-dependent Hamiltonian of the form :math:`H=H_{0}+f(t)H_{1}` where :math:`f(t)` is the time-dependent driving strength given as :math:`f(t)=A\exp\left[-\left( t/\sigma \right)^{2}\right]`. +As an example, we will look at a case with a time-dependent Hamiltonian of the form +:math:`H=H_{0}+f(t)H_{1}` where :math:`f(t)` is the time-dependent driving strength +given as :math:`f(t)=A\exp\left[-\left( t/\sigma \right)^{2}\right]`. The following code sets up the problem .. plot:: @@ -104,7 +114,9 @@ The following code sets up the problem H0 = -g * (sigma_ge.dag() * a + a.dag() * sigma_ge) # time-independent term H1 = (sigma_ue.dag() + sigma_ue) # time-dependent term -Given that we have a single time-dependent Hamiltonian term, and constant collapse terms, we need to specify a single Python function for the coefficient :math:`f(t)`. In this case, one can simply do +Given that we have a single time-dependent Hamiltonian term, and constant collapse terms, +we need to specify a single Python function for the coefficient :math:`f(t)`. +In this case, one can simply do .. plot:: :context: close-figs @@ -113,8 +125,10 @@ Given that we have a single time-dependent Hamiltonian term, and constant collap def H1_coeff(t): return 9 * np.exp(-(t / 5.) ** 2) -In this case, the return value depends only on time. However it is possible to add optional arguments to the call, see `Using arguments`_. -Having specified our coefficient function, we can now specify the Hamiltonian in list format and call the solver (in this case :func:`qutip.mesolve`) +In this case, the return value depends only on time. However it is possible to +add optional arguments to the call, see `Using arguments`_. +Having specified our coefficient function, we can now specify the Hamiltonian in +list format and call the solver (in this case :func:`.mesolve`) .. plot:: :context: close-figs @@ -133,8 +147,10 @@ We can call the Monte Carlo solver in the exact same way (if using the default ` output = mcsolve(H, psi0, t, c_ops, [ada, sigma_UU, sigma_GG]) -The output from the master equation solver is identical to that shown in the examples, the Monte Carlo however will be noticeably off, suggesting we should increase the number of trajectories for this example. -In addition, we can also consider the decay of a simple Harmonic oscillator with time-varying decay rate +The output from the master equation solver is identical to that shown in the examples, +the Monte Carlo however will be noticeably off, suggesting we should increase the number +of trajectories for this example. In addition, we can also consider the decay of a +simple Harmonic oscillator with time-varying decay rate .. plot:: :context: close-figs @@ -157,7 +173,8 @@ In addition, we can also consider the decay of a simple Harmonic oscillator with Qobjevo ======= -:class:`QobjEvo` as a time dependent quantum system, as it's main functionality create a :class:`Qobj` at a time: +:obj:`.QobjEvo` as a time dependent quantum system, as it's main functionality +create a :obj:`.Qobj` at a time: .. doctest:: [basics] :options: +NORMALIZE_WHITESPACE @@ -169,28 +186,29 @@ Qobjevo [1. 1.]] -:class:`QobjEvo` shares a lot of properties with the :class:`Qobj`. +:obj:`.QobjEvo` shares a lot of properties with the :obj:`.Qobj`. -+---------------+------------------+----------------------------------------+ -| Property | Attribute | Description | -+===============+==================+========================================+ -| Dimensions | ``Q.dims`` | List keeping track of shapes for | -| | | individual components of a | -| | | multipartite system (for tensor | -| | | products and partial traces). | -+---------------+------------------+----------------------------------------+ -| Shape | ``Q.shape`` | Dimensions of underlying data matrix. | -+---------------+------------------+----------------------------------------+ -| Type | ``Q.type`` | Is object of type 'ket, 'bra', | -| | | 'oper', or 'super'? | -+---------------+------------------+----------------------------------------+ -| is constant? | ``Q.isconstant`` | Is the operator Hermitian or not? | -+---------------+------------------+----------------------------------------+ ++----------------+------------------+----------------------------------------+ +| Property | Attribute | Description | ++================+==================+========================================+ +| Dimensions | ``Q.dims`` | Shapes the tensor structure. | ++----------------+------------------+----------------------------------------+ +| Shape | ``Q.shape`` | Dimensions of underlying data matrix. | ++----------------+------------------+----------------------------------------+ +| Type | ``Q.type`` | Is object of type 'ket, 'bra', | +| | | 'oper', or 'super'? | ++----------------+------------------+----------------------------------------+ +| Representation | ``Q.superrep`` | Representation used if `type` is | +| | | 'super'? | ++----------------+------------------+----------------------------------------+ +| Is constant | ``Q.isconstant`` | Does the QobjEvo depend on time. | ++----------------+------------------+----------------------------------------+ -:class:`QobjEvo`'s follow the same mathematical operations rules than :class:`Qobj`. +:obj:`.QobjEvo`'s follow the same mathematical operations rules than :obj:`.Qobj`. They can be added, subtracted and multiplied with scalar, ``Qobj`` and ``QobjEvo``. -They also support the `dag` and `trans` and `conj` method and can be used for tensor operations and super operator transformation: +They also support the ``dag`` and ``trans`` and ``conj`` method and can be used +for tensor operations and super operator transformation: .. code-block:: python @@ -211,68 +229,79 @@ Or equivalently: Using arguments --------------- -Until now, the coefficient were only functions of time. -In the definition of ``H1_coeff``, the driving amplitude ``A`` and width ``sigma`` were hardcoded with their numerical values. +Until now, the coefficients were only functions of time. In the definition of ``H1_coeff``, +the driving amplitude ``A`` and width ``sigma`` were hardcoded with their numerical values. This is fine for problems that are specialized, or that we only want to run once. -However, in many cases, we would like study the same problem with a range of parameters and not have to worry about manually changing the values on each run. -QuTiP allows you to accomplish this using by adding extra arguments to coefficients function that make the :class:`QobjEvo`. -For instance, instead of explicitly writing 9 for the amplitude and 5 for the width of the gaussian driving term, we can add an `args` positional variable: +However, in many cases, we would like study the same problem with a range of parameters and +not have to worry about manually changing the values on each run. +QuTiP allows you to accomplish this using by adding extra arguments to coefficients +function that make the :obj:`.QobjEvo`. For instance, instead of explicitly writing +9 for the amplitude and 5 for the width of the gaussian driving term, we can add an +`args` positional variable: -.. plot:: - :context: close-figs +.. code-block:: python - def H1_coeff(t, args): - return args['A'] * np.exp(-(t/args['sigma'])**2) + >>> def H1_coeff(t, args): + >>> return args['A'] * np.exp(-(t/args['sigma'])**2) or, new from v5, add the extra parameter directly: -.. plot:: - :context: close-figs +.. code-block:: python - def H1_coeff(t, A, sigma): - return A * np.exp(-(t / sigma)**2) + >>> def H1_coeff(t, A, sigma): + >>> return A * np.exp(-(t / sigma)**2) -When the second positional input of the coefficient function is named ``args``, the arguments are passed as a Python dictionary of ``key: value`` pairs. +When the second positional input of the coefficient function is named ``args``, +the arguments are passed as a Python dictionary of ``key: value`` pairs. Otherwise the coefficient function is called as ``coeff(t, **args)``. -In the last example, ``args = {'A': a, 'sigma': b}`` where ``a`` and ``b`` are the two parameters for the amplitude and width, respectively. -This ``args`` dictionary need to be given at creation of the :class:`QobjEvo` when function using then are included: - -.. plot:: - :context: close-figs +In the last example, ``args = {'A': a, 'sigma': b}`` where ``a`` and ``b`` are the +two parameters for the amplitude and width, respectively. +This ``args`` dictionary need to be given at creation of the :obj:`.QobjEvo` when +function using then are included: - system = [H0, [H1, H1_coeff]] - args={'A': 9, 'sigma': 5} - qevo = QobjEvo(system, args=args) +.. code-block:: python -But without ``args``, the :class:`QobjEvo` creation will fail: + >>> system = [sigmaz(), [sigmax(), H1_coeff]] + >>> args={'A': 9, 'sigma': 5} + >>> qevo = QobjEvo(system, args=args) -.. plot:: - :context: close-figs +But without ``args``, the :obj:`.QobjEvo` creation will fail: - try: - QobjEvo(system) - except TypeError as err: - print(err) +.. code-block:: python -When evaluation the :class:`QobjEvo` at a time, new arguments can be passed either with the ``args`` dictionary positional arguments, or with specific keywords arguments: + >>> QobjEvo(system) + TypeError: H1_coeff() missing 2 required positional arguments: 'A' and 'sigma' -.. plot:: - :context: close-figs +When evaluation the :obj:`.QobjEvo` at a time, new arguments can be passed either +with the ``args`` dictionary positional arguments, or with specific keywords arguments: - print(qevo(1)) - print(qevo(1, {"A": 5, "sigma": 0.2})) - print(qevo(1, A=5)) +.. code-block:: python + >>> print(qevo(1)) + Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', isherm=True + Qobj data = + [[ 1. 8.64710495] + [ 8.64710495 -1. ]] + >>> print(qevo(1, {"A": 5, "sigma": 0.2})) + Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', isherm=True + Qobj data = + [[ 1.00000000e+00 6.94397193e-11] + [ 6.94397193e-11 -1.00000000e+00]] + >>> print(qevo(1, A=5)) + Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', isherm=True + Qobj data = + [[ 1. 4.8039472] + [ 4.8039472 -1. ]] Whether the original coefficient used the ``args`` or specific input does not matter. It is fine to mix the different signatures. Solver calls take an ``args`` input that is used to build the time dependent system. -If the Hamiltonian or collapse operators are already :class:`QobjEvo`, their arguments will be overwritten. +If the Hamiltonian or collapse operators are already :obj:`.QobjEvo`, their arguments will be overwritten. .. code-block:: python @@ -282,19 +311,18 @@ If the Hamiltonian or collapse operators are already :class:`QobjEvo`, their arg mesolve(system, ..., args=args) -To update arguments of an existing time dependent quantum system, you can pass the previous object as the input of a :class:`QobjEvo` with new ``args``: +To update arguments of an existing time dependent quantum system, you can pass the +previous object as the input of a :obj:`.QobjEvo` with new ``args``: -.. plot:: - :context: close-figs +.. code-block:: python - print(qevo(1)) - print(qevo(1, {"A": 5, "sigma": 0.2})) - new_qevo = QobjEvo(qevo, args={"A": 5, "sigma": 0.2}) - print(new_qevo(1)) + >>> new_qevo = QobjEvo(qevo, args={"A": 5, "sigma": 0.2}) + >>> new_qevo(1) == qevo(1, {"A": 5, "sigma": 0.2}) + True -:class:`QobjEvo` created from a monolithic function can also use arguments: +:obj:`.QobjEvo` created from a monolithic function can also use arguments: .. code-block:: python @@ -305,25 +333,34 @@ To update arguments of an existing time dependent quantum system, you can pass t H_t = QobjEvo(oper, args={"w": np.pi}) -When merging two or more :class:`QobjEvo`, each will keep it arguments, but calling it with updated are will affect all parts: +When merging two or more :obj:`.QobjEvo`, each will keep it arguments, but +calling it with updated are will affect all parts: -.. plot:: - :context: close-figs +.. code-block:: python - qevo1 = QobjEvo([[sigmap(), lambda t, a: a], [sigmam(), lambda t, a, b: a+1j*b]], args={"a": 1, "b":2}) - qevo2 = QobjEvo([[num(2), lambda t, a, c: a+1j*c]], args={"a": 2, "c":2}) - summed_evo = qevo1 + qevo2 - print(summed_evo(0)) - print(summed_evo(0, a=3, b=1)) + >>> qevo1 = QobjEvo([[sigmap(), lambda t, a: a]], args={"a": 1}) + >>> qevo2 = QobjEvo([[sigmam(), lambda t, a: a]], args={"a": 2}) + >>> summed_evo = qevo1 + qevo2 + >>> print(summed_evo(0)) + Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', isherm=False + Qobj data = + [[0. 1.] + [2. 0.]] + >>> print(summed_evo(0, a=3, b=1)) + Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', isherm=True + Qobj data = + [[0. 3.] + [3. 0.]] Coefficients ============ -To build time dependent quantum system we often use a list of :class:`Qobj` and *coefficient*. -These *coefficients* represent the strength of the corresponding quantum object a function that of time. -Up to now, we used functions for these, but QuTiP support multiple formats: ``callable``, ``strings``, ``array``. +To build time dependent quantum system we often use a list of :obj:`.Qobj` and +:obj:`.Coefficient`. These :obj:`.Coefficient` represent the strength of the corresponding +quantum object a function that of time. Up to now, we used functions for these, +but QuTiP support multiple formats: ``callable``, ``strings``, ``array``. **Function coefficients** : @@ -341,12 +378,15 @@ Any function or method that can be called by ``f(t, args)``, ``f(t, **args)`` is **String coefficients** : Use a string containing a simple Python expression. -The variable ``t``, common mathematical functions such as ``sin`` or ``exp`` an variable in args will be available. -If available, the string will be compiled using cython, fixing variable type when possible, allowing slightly faster execution than function. -While the speed up is usually very small, in long evolution, numerous calls to the functions are made and it's can accumulate. -From version 5, compilation of the coefficient is done only once and saved between sessions. -When either the cython or filelock modules are not available, the code will be executed in python using ``exec`` with the same environment . -This, however, as no advantage over using python function. +The variable ``t``, common mathematical functions such as ``sin`` or ``exp`` an +variable in args will be available. If available, the string will be compiled using +cython, fixing variable type when possible, allowing slightly faster execution than function. +While the speed up is usually very small, in long evolution, numerous calls to the +functions are made and it's can accumulate. From version 5, compilation of the +coefficient is done only once and saved between sessions. When either the cython or +filelock modules are not available, the code will be executed in python using +``exec`` with the same environment . This, however, as no advantage over using +python function. .. code-block:: python @@ -361,7 +401,8 @@ Here is a list of defined variables: ``sinh``, ``cosh``, ``tanh``, ``asinh``, ``acosh``, ``atanh``, ``exp``, ``log``, ``log10``, ``erf``, ``zerf``, ``sqrt``, ``real``, ``imag``, ``conj``, ``abs``, ``norm``, ``arg``, ``proj``, - ``np`` (numpy) and ``spe`` (scipy.special). + ``np`` (numpy), ``spe`` (scipy.special) and ``cython_special`` + (scipy cython interface). **Array coefficients** : @@ -399,7 +440,8 @@ Outside the interpolation range, the first or last value are used. plt.legend() -When using array coefficients in solver, if the time dependent quantum system is in list format, the solver tlist is used as times of the array. +When using array coefficients in solver, if the time dependent quantum system is +in list format, the solver tlist is used as times of the array. This is often not ideal as the interpolation is usually less precise close the extremities of the range. It is therefore better to create the QobjEvo using an extended range prior to the solver: @@ -412,12 +454,12 @@ It is therefore better to create the QobjEvo using an extended range prior to th coeff = np.exp(-times) c_ops = [QobjEvo([destroy(N), coeff], tlist=times)] - plt.plot( - mesolve(qeye(N), basis(N, N-1), np.linspace(0, 1, 11), c_ops=c_ops, e_ops=[num(N)]).expect - ) + tlist = np.linspace(0, 1, 11) + data = mesolve(qeye(N), basis(N, N-1), tlist, c_ops=c_ops, e_ops=[num(N)]).expect[0] + plt.plot(tlist, data) -Different coefficient types can be mixed in a :class:`QobjEvo`. +Different coefficient types can be mixed in a :obj:`.QobjEvo`. Given the multiple choices of input style, the first question that arises is which option to choose? @@ -429,12 +471,38 @@ Of course, for small system sizes and evolution times, the difference will be mi Lastly the spline method is usually as fast the string method, but it cannot be modified once created. -.. _time-dynargs: +.. _time_max_step: + +Working with pulses +=================== + +Special care is needed when working with pulses. ODE solvers select the step +length automatically and can miss thin pulses when not properly warned. +Integrations methods with variable step sizes have the ``max_step`` option that +control the maximum length of a single internal integration step. This value +should be set to under half the pulse width to be certain they are not missed. + +For example, the following pulse is missed without fixing the maximum step length. + +.. plot:: + :context: close-figs + + def pulse(t): + return 10 * np.pi * (0.7 < t < 0.75) + + tlist = np.linspace(0, 1, 201) + H = [sigmaz(), [sigmax(), pulse]] + psi0 = basis(2,1) + + data1 = sesolve(H, psi0, tlist, e_ops=num(2)).expect[0] + data2 = sesolve(H, psi0, tlist, e_ops=num(2), options={"max_step": 0.01}).expect[0] -Accessing the state from solver -=============================== + plt.plot(tlist, data1, label="no max_step") + plt.plot(tlist, data2, label="fixed max_step") + plt.fill_between(tlist, [pulse(t) for t in tlist], color="g", alpha=0.2, label="pulse") + plt.ylim([-0.1, 1.1]) + plt.legend(loc="center left") -In QuTiP 4.4 to 4.7, it was possible to request that the solver pass the state, expectation values or collapse operators via arguments to :class:`QobjEvo`. Support for this is not yet available in QuTiP 5. .. plot:: :context: reset diff --git a/doc/guide/figures/bloch3d+data.png b/doc/guide/figures/bloch3d+data.png deleted file mode 100644 index 4214368d3b..0000000000 Binary files a/doc/guide/figures/bloch3d+data.png and /dev/null differ diff --git a/doc/guide/figures/bloch3d+points.png b/doc/guide/figures/bloch3d+points.png deleted file mode 100644 index 14a8030365..0000000000 Binary files a/doc/guide/figures/bloch3d+points.png and /dev/null differ diff --git a/doc/guide/figures/bloch3d-blank.png b/doc/guide/figures/bloch3d-blank.png deleted file mode 100644 index 7888a7a896..0000000000 Binary files a/doc/guide/figures/bloch3d-blank.png and /dev/null differ diff --git a/doc/guide/guide-basics.rst b/doc/guide/guide-basics.rst index 072b9a939d..1d17cf0976 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: @@ -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 | | | +--------------------------+----------------------------+----------------------------------------+ @@ -321,12 +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 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 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 in Compressed Sparse Row format> + with 3 stored elements (1 diagonals) in DIAgonal format> +Conversion between storage type is done using the :meth:`.Qobj.to` method. -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. +.. 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> + + +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 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: @@ -397,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``: @@ -429,6 +462,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 +489,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-bloch.rst b/doc/guide/guide-bloch.rst index 976264320e..ad351a5f51 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, 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: -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..e0769cd592 100644 --- a/doc/guide/guide-control.rst +++ b/doc/guide/guide-control.rst @@ -191,65 +191,10 @@ 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. +The Quantum Control part of qutip has been moved to its own project. -.. _pulseoptim-functions: +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. -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 newer interface with upgraded capacities is being developped in `qutip-qoc `_. -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. +Please give these modules a try. diff --git a/doc/guide/guide-correlation.rst b/doc/guide/guide-correlation.rst index 6353077608..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) @@ -67,7 +67,9 @@ The following code demonstrates how to calculate the :math:`\left$') @@ -83,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. @@ -97,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 ----------------------------------------------- @@ -114,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 ------------------------------------------------ @@ -127,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. @@ -136,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-dynamics.rst b/doc/guide/guide-dynamics.rst index 8f6d85538c..63d8eae06b 100644 --- a/doc/guide/guide-dynamics.rst +++ b/doc/guide/guide-dynamics.rst @@ -14,7 +14,9 @@ Time Evolution and Quantum System Dynamics dynamics/dynamics-krylov.rst dynamics/dynamics-stochastic.rst dynamics/dynamics-time.rst + dynamics/dynamics-class.rst dynamics/dynamics-bloch-redfield.rst dynamics/dynamics-floquet.rst - dynamics/dynamics-piqs.rst + dynamics/dynamics-nmmonte.rst dynamics/dynamics-options.rst + dynamics/dynamics-propagator.rst 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/dynamics/dynamics-piqs.rst b/doc/guide/guide-piqs.rst similarity index 74% rename from doc/guide/dynamics/dynamics-piqs.rst rename to doc/guide/guide-piqs.rst index e004ab64f8..3167918a85 100644 --- a/doc/guide/dynamics/dynamics-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,15 +48,16 @@ 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 - from piqs import Dicke - from qutip import steadystate + from qutip import piqs N = 10 - system = Dicke(N, emission = 1, pumping = 2) + system = piqs.Dicke(N, emission = 1, pumping = 2) L = system.liouvillian() steady = steadystate(L) @@ -109,6 +110,22 @@ For more example of use, see the "Permutational Invariant Lindblad Dynamics" sec - ``Dicke.c_ops()`` - The collapse operators for the ensemble can be called by the `c_ops` method of the Dicke class. -Note that the mathematical object representing the density matrix of the full system that is manipulated (or obtained from `steadystate`) in the Dicke-basis formalism used here is a *representative of the density matrix*. This *representative object* is of linear size N^2, whereas the full density matrix is defined over a 2^N Hilbert space. In order to calculate nonlinear functions of such density matrix, such as the Von Neumann entropy or the purity, it is necessary to take into account the degeneracy of each block of such block-diagonal density matrix. Note that as long as one calculates expected values of operators, being Tr[A*rho] a *linear* function of `rho`, the *representative density matrix* give straightforwardly the correct result. When a *nonlinear* function of the density matrix needs to be calculated, one needs to weigh each degenerate block correctly; this is taken care by the `dicke_function_trace` in `qutip.piqs`, and the user can use it to define general nonlinear functions that can be described as the trace of a Taylor expandable function. Two nonlinear functions that use `dicke_function_trace` and are already implemented are `purity_dicke`, to calculate the purity of a density matrix in the Dicke basis, and `entropy_vn_dicke`, which can be used to calculate the Von Neumann entropy. - -More functions relative to the `qutip.piqs` module can be found at :ref:`apidoc`. Attributes to the :class:`qutip.piqs.Dicke` and :class:`qutip.piqs.Pim` class can also be found there. +Note that the mathematical object representing the density matrix of the full system +that is manipulated (or obtained from `steadystate`) in the Dicke-basis formalism +used here is a *representative of the density matrix*. This *representative object* +is of linear size N^2, whereas the full density matrix is defined over a 2^N Hilbert +space. In order to calculate nonlinear functions of such density matrix, such as the +Von Neumann entropy or the purity, it is necessary to take into account the degeneracy +of each block of such block-diagonal density matrix. Note that as long as one calculates +expected values of operators, being Tr[A*rho] a *linear* function of `rho`, the +*representative density matrix* give straightforwardly the correct result. When a +*nonlinear* function of the density matrix needs to be calculated, one needs to +weigh each degenerate block correctly; this is taken care by the `dicke_function_trace` +in :obj:`.piqs`, and the user can use it to define general nonlinear functions that +can be described as the trace of a Taylor expandable function. Two nonlinear functions +that use `dicke_function_trace` and are already implemented are `purity_dicke`, to +calculate the purity of a density matrix in the Dicke basis, and `entropy_vn_dicke`, +which can be used to calculate the Von Neumann entropy. + +More functions relative to the :obj:`qutip.piqs` module can be found at :ref:`apidoc`. +Attributes to the :class:`.piqs.Dicke` and :class:`.piqs.Pim` class can also be found there. 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-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] 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 beb6c26b7a..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. @@ -421,9 +339,8 @@ represent the composition of two systems. QuTiP also allows more general tensor manipulations that are useful for converting between superoperator representations [WBC11]_. In particular, the :func:`~qutip.core.tensor.tensor_contract` function allows for -contracting one or more pairs of indices. As detailed in -the `channel contraction tutorial`_, this can be used to find -superoperators that represent partial trace maps. +contracting one or more pairs of indices. +This can be used to find superoperators that represent partial trace maps. Using this functionality, we can construct some quite exotic maps, such as a map from :math:`3 \times 3` operators to :math:`2 \times 2` operators: @@ -434,5 +351,6 @@ operators: [[[2], [2]], [[3], [3]]] - -.. _channel contraction tutorial: https://nbviewer.ipython.org/github/qutip/qutip-notebooks/blob/master/examples/superop-contract.ipynb +.. + TODO: remake from notebook to tutorials + .. _channel contraction tutorial: github/qutip/qutip-notebooks/blob/master/examples/superop-contract.ipynb 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 39fdc1ccd3..e450ca737e 100644 --- a/doc/guide/guide.rst +++ b/doc/guide/guide.rst @@ -15,12 +15,12 @@ Users Guide guide-dynamics.rst guide-heom.rst 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. diff --git a/doc/installation.rst b/doc/installation.rst index efce6af430..fbed3cea04 100644 --- a/doc/installation.rst +++ b/doc/installation.rst @@ -79,9 +79,6 @@ QuTiP will detect if it is being used within one of these richer environments, a Installing with conda ===================== -QuTiP is designed to work best when using the `Anaconda `_ or `Intel `_ Python distributions that support the conda package management system. -It is still possible to use ``pip`` to install QuTiP while using conda, but uniformly using conda will make complete dependency management easier. - If you already have your conda environment set up, and have the ``conda-forge`` channel available, then you can install QuTiP using: .. code-block:: bash @@ -267,7 +264,7 @@ At the end, the testing report should report a success; it is normal for some te Skips may be tests that do not run on your operating system, or tests of optional components that you have not installed the dependencies for. If any failures or errors occur, please check that you have installed all of the required modules. See the next section on how to check the installed versions of the QuTiP dependencies. -If these tests still fail, then head on over to the `QuTiP Discussion Board `_ or `the GitHub issues page `_ and post a message detailing your particular issue. +If these tests still fail, then head on over to the `QuTiP Discussion Board `_ or `the GitHub issues page `_ and post a message detailing your particular issue. .. _install-about: diff --git a/doc/requirements.txt b/doc/requirements.txt index 68e490ddb4..6cb3ffacf5 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -11,7 +11,7 @@ idna==3.4 imagesize==1.4.1 ipython==8.11.0 jedi==0.18.2 -Jinja2==3.1.2 +Jinja2==3.1.3 kiwisolver==1.4.4 MarkupSafe==2.1.2 matplotlib==3.7.1 @@ -21,7 +21,7 @@ packaging==23.0 parso==0.8.3 pexpect==4.8.0 pickleshare==0.7.5 -Pillow==9.4.0 +Pillow==10.2.0 prompt-toolkit==3.0.38 ptyprocess==0.7.0 Pygments==2.15.0 @@ -43,6 +43,6 @@ sphinxcontrib-jsmath==1.0.1 sphinxcontrib-qthelp==1.0.3 sphinxcontrib-serializinghtml==1.1.5 traitlets==5.9.0 -urllib3==1.26.14 +urllib3==1.26.18 wcwidth==0.2.6 wheel==0.38.4 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/pyproject.toml b/pyproject.toml index f854fa46f5..896cd3f115 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,11 +3,12 @@ requires = [ "setuptools", "packaging", "wheel", - "cython>=0.29.20", + "cython>=0.29.20; python_version>='3.10'", + "cython>=0.29.20,<3.0.3; python_version<='3.9'", # 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" @@ -17,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/qutip/__init__.py b/qutip/__init__.py index ff8d76a7e8..3fe38033a1 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 @@ -51,7 +50,7 @@ from .partial_transpose import * from .continuous_variables import * from .distributions import * - +from . import measurement # utilities from .utilities import * diff --git a/qutip/animation.py b/qutip/animation.py index 326d73ea59..0f19485585 100644 --- a/qutip/animation.py +++ b/qutip/animation.py @@ -9,6 +9,7 @@ plot_fock_distribution, plot_wigner, plot_spin_distribution, plot_qubism, plot_schmidt) from .solver import Result +from numpy import sqrt def _result_state(obj): @@ -30,13 +31,13 @@ def anim_wigner_sphere(wigners, reflections=False, *, cmap=None, wigners : list of transformations The wigner transformation at `steps` different theta and phi. - reflections : bool, default=False + reflections : bool, default: False If the reflections of the sphere should be plotted as well. cmap : a matplotlib colormap instance, optional Color map to use when plotting. - colorbar : bool, default=True + colorbar : bool, default: True Whether (True) or not (False) a colorbar should be attached. fig : a matplotlib Figure instance, optional @@ -69,7 +70,7 @@ def anim_hinton(rhos, x_basis=None, y_basis=None, color_style="scaled", Parameters ---------- - rhos : :class:`qutip.solver.Result` or list of :class:`qutip.Qobj` + rhos : :class:`.Result` or list of :class:`.Qobj` Input density matrix or superoperator. .. note:: @@ -83,7 +84,7 @@ def anim_hinton(rhos, x_basis=None, y_basis=None, color_style="scaled", y_basis : list of strings, optional list of y ticklabels to represent y basis of the input. - color_style : string, default="scaled" + color_style : str, {"scaled", "threshold", "phase"}, default: "scaled" Determines how colors are assigned to each square: @@ -97,14 +98,14 @@ def anim_hinton(rhos, x_basis=None, y_basis=None, color_style="scaled", - If set to ``"phase"``, each color is chosen according to the angle of the corresponding matrix element. - label_top : bool, default=True + label_top : bool, default: True If True, x ticklabels will be placed on top, otherwise they will appear below the plot. cmap : a matplotlib colormap instance, optional Color map to use when plotting. - colorbar : bool, default=True + colorbar : bool, default: True Whether (True) or not (False) a colorbar should be attached. fig : a matplotlib Figure instance, optional @@ -152,7 +153,7 @@ def anim_sphereplot(V, theta, phi, *, cmap=None, cmap : a matplotlib colormap instance, optional Color map to use when plotting. - colorbar : bool, default=True + colorbar : bool, default: True Whether (True) or not (False) a colorbar should be attached. fig : a matplotlib Figure instance, optional @@ -184,7 +185,7 @@ def anim_matrix_histogram(Ms, x_basis=None, y_basis=None, limits=None, Parameters ---------- - Ms : list of matrices or :class:`qutip.solver.Result` + Ms : list of matrices or :class:`.Result` The matrix to visualize x_basis : list of strings, optional @@ -196,7 +197,7 @@ def anim_matrix_histogram(Ms, x_basis=None, y_basis=None, limits=None, limits : list/array with two float numbers, optional The z-axis limits [min, max] - bar_style : string, default="real" + bar_style : str, {"real", "img", "abs", "phase"}, default: "real" - If set to ``"real"`` (default), each bar is plotted as the real part of the corresponding matrix element @@ -210,7 +211,7 @@ def anim_matrix_histogram(Ms, x_basis=None, y_basis=None, limits=None, color_limits : list/array with two float numbers, optional The limits of colorbar [min, max] - color_style : string, default="real" + color_style : str, {"real", "img", "abs", "phase"}, default: "real" Determines how colors are assigned to each square: - If set to ``"real"`` (default), each color is chosen @@ -225,7 +226,7 @@ def anim_matrix_histogram(Ms, x_basis=None, y_basis=None, limits=None, cmap : a matplotlib colormap instance, optional Color map to use when plotting. - colorbar : bool, default=True + colorbar : bool, default: True show colorbar fig : a matplotlib Figure instance, optional @@ -242,40 +243,40 @@ def anim_matrix_histogram(Ms, x_basis=None, y_basis=None, limits=None, 'zticks' : list of numbers, optional A list of z-axis tick locations. - 'bars_spacing' : float, default=0.1 + 'bars_spacing' : float, default: 0.1 spacing between bars. - 'bars_alpha' : float, default=1. + 'bars_alpha' : float, default: 1. transparency of bars, should be in range 0 - 1 - 'bars_lw' : float, default=0.5 + 'bars_lw' : float, default: 0.5 linewidth of bars' edges. - 'bars_edgecolor' : color, default='k' + 'bars_edgecolor' : color, default: 'k' The colors of the bars' edges. Examples: 'k', (0.1, 0.2, 0.5) or '#0f0f0f80'. - 'shade' : bool, default=True + 'shade' : bool, default: True Whether to shade the dark sides of the bars (True) or not (False). The shading is relative to plot's source of light. - 'azim' : float, default=-35 + 'azim' : float, default: -35 The azimuthal viewing angle. - 'elev' : float, default=35 + 'elev' : float, default: 35 The elevation viewing angle. - 'stick' : bool, default=False + 'stick' : bool, default: False Changes xlim and ylim in such a way that bars next to XZ and YZ planes will stick to those planes. This option has no effect if ``ax`` is passed as a parameter. - 'cbar_pad' : float, default=0.04 + 'cbar_pad' : float, default: 0.04 The fraction of the original axes between the colorbar and the new image axes. (i.e. the padding between the 3D figure and the colorbar). - 'cbar_to_z' : bool, default=False + 'cbar_to_z' : bool, default: False Whether to set the color of maximum and minimum z-values to the maximum and minimum colors in the colorbar (True) or not (False). @@ -313,16 +314,16 @@ def anim_fock_distribution(rhos, fock_numbers=None, color="green", Parameters ---------- - rhos : :class:`qutip.solver.Result` or list of :class:`qutip.Qobj` + rhos : :class:`.Result` or list of :class:`.Qobj` The density matrix (or ket) of the state to visualize. fock_numbers : list of strings, optional list of x ticklabels to represent fock numbers - color : color or list of colors, default="green" + color : color or list of colors, default: "green" The colors of the bar faces. - unit_y_range : bool, default=True + unit_y_range : bool, default: True Set y-axis limits [0, 1] or not fig : a matplotlib Figure instance, optional @@ -346,16 +347,16 @@ def anim_fock_distribution(rhos, fock_numbers=None, color="green", return fig, ani -def anim_wigner(rhos, xvec=None, yvec=None, method='clenshaw', - projection='2d', *, cmap=None, colorbar=False, - fig=None, ax=None): +def anim_wigner(rhos, xvec=None, yvec=None, method='clenshaw', projection='2d', + g=sqrt(2), sparse=False, parfor=False, *, + cmap=None, colorbar=False, fig=None, ax=None): """ Animation of the Wigner function for a density matrix (or ket) that describes an oscillator mode. Parameters ---------- - rhos : :class:`qutip.solver.Result` or list of :class:`qutip.Qobj` + rhos : :class:`.Result` or list of :class:`.Qobj` The density matrix (or ket) of the state to visualize. xvec : array_like, optional @@ -365,19 +366,30 @@ def anim_wigner(rhos, xvec=None, yvec=None, method='clenshaw', y-coordinates at which to calculate the Wigner function. Does not apply to the 'fft' method. - method : string {'clenshaw', 'iterative', 'laguerre', 'fft'}, - default='clenshaw' + method : str {'clenshaw', 'iterative', 'laguerre', 'fft'}, default: 'clenshaw' The method used for calculating the wigner function. See the documentation for qutip.wigner for details. - projection: string {'2d', '3d'}, default='2d' + projection: str {'2d', '3d'}, default: '2d' Specify whether the Wigner function is to be plotted as a contour graph ('2d') or surface plot ('3d'). + g : float + Scaling factor for `a = 0.5 * g * (x + iy)`, default `g = sqrt(2)`. + See the documentation for qutip.wigner for details. + + sparse : bool {False, True} + Flag for sparse format. + See the documentation for qutip.wigner for details. + + parfor : bool {False, True} + Flag for parallel calculation. + See the documentation for qutip.wigner for details. + cmap : a matplotlib cmap instance, optional The colormap. - colorbar : bool, default=False + colorbar : bool, default: False Whether (True) or not (False) a colorbar should be attached to the Wigner function graph. @@ -396,7 +408,8 @@ def anim_wigner(rhos, xvec=None, yvec=None, method='clenshaw', rhos = _result_state(rhos) - fig, ani = plot_wigner(rhos, xvec, yvec, method, projection, + fig, ani = plot_wigner(rhos, xvec, yvec, method=method, g=g, sparse=sparse, + parfor=parfor, projection=projection, cmap=cmap, colorbar=colorbar, fig=fig, ax=ax) return fig, ani @@ -418,7 +431,7 @@ def anim_spin_distribution(Ps, THETA, PHI, projection='2d', *, PHI : matrix Meshgrid matrix for the phi coordinate. Its range is between 0 and 2*pi - projection: string {'2d', '3d'}, default='2d' + projection: str {'2d', '3d'}, default: '2d' Specify whether the spin distribution function is to be plotted as a 2D projection where the surface of the unit sphere is mapped on the unit disk ('2d') or surface plot ('3d'). @@ -426,7 +439,7 @@ def anim_spin_distribution(Ps, THETA, PHI, projection='2d', *, cmap : a matplotlib cmap instance, optional The colormap. - colorbar : bool, default=False + colorbar : bool, default: False Whether (True) or not (False) a colorbar should be attached to the Wigner function graph. @@ -463,25 +476,25 @@ def anim_qubism(kets, theme='light', how='pairs', grid_iteration=1, Parameters ---------- - kets : :class:`qutip.solver.Result` or list of :class:`qutip.Qobj` + kets : :class:`.Result` or list of :class:`.Qobj` Pure states for animation. - theme : 'light' or 'dark', default='light' + theme : str {'light', 'dark'}, default: 'light' Set coloring theme for mapping complex values into colors. See: complex_array_to_rgb. - how : 'pairs', 'pairs_skewed' or 'before_after', default='pairs' + how : str {'pairs', 'pairs_skewed', 'before_after'}, default: 'pairs' Type of Qubism plotting. Options: - 'pairs' - typical coordinates, - 'pairs_skewed' - for ferromagnetic/antriferromagnetic plots, - 'before_after' - related to Schmidt plot (see also: plot_schmidt). - grid_iteration : int, default=1 + grid_iteration : int, default: 1 Helper lines to be drawn on plot. Show tiles for 2*grid_iteration particles vs all others. - legend_iteration : int or 'grid_iteration' or 'all', default=0 + legend_iteration : int or 'grid_iteration' or 'all', default: 0 Show labels for first ``2*legend_iteration`` particles. Option 'grid_iteration' sets the same number of particles as for grid_iteration. Option 'all' makes label for all particles. Typically @@ -535,10 +548,10 @@ def anim_schmidt(kets, theme='light', splitting=None, Parameters ---------- - ket : :class:`qutip.solver.Result` or list of :class:`qutip.Qobj` + ket : :class:`.Result` or list of :class:`.Qobj` Pure states for animation. - theme : 'light' or 'dark', default='light' + theme : str {'light', 'dark'}, default: 'light' Set coloring theme for mapping complex values into colors. See: complex_array_to_rgb. @@ -546,7 +559,7 @@ def anim_schmidt(kets, theme='light', splitting=None, Plot for a number of first particles versus the rest. If not given, it is (number of particles + 1) // 2. - labels_iteration : int or pair of ints, default=(3,2) + labels_iteration : int or pair of ints, default: (3, 2) Number of particles to be shown as tick labels, for first (vertical) and last (horizontal) particles, respectively. diff --git a/qutip/bloch.py b/qutip/bloch.py index 980502dd9d..6a7e38a687 100644 --- a/qutip/bloch.py +++ b/qutip/bloch.py @@ -159,18 +159,20 @@ def __init__(self, fig=None, axes=None, view=None, figsize=None, self.vector_default_color = ['g', '#CC6600', 'b', 'r'] # List that stores the display colors for each vector self.vector_color = [] - #: Width of Bloch vectors, default = 5 + # Width of Bloch vectors, default = 5 self.vector_width = 3 - #: Style of Bloch vectors, default = '-\|>' (or 'simple') + # Style of Bloch vectors, default = '-\|>' (or 'simple') self.vector_style = '-|>' - #: Sets the width of the vectors arrowhead + # Sets the width of the vectors arrowhead self.vector_mutation = 20 # ---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'] @@ -308,7 +310,7 @@ def clear(self): self.vector_alpha = [] self.annotations = [] self.vector_color = [] - self.point_color = [] + self.point_color = None self._lines = [] self._arcs = [] @@ -331,12 +333,12 @@ def add_points(self, points, meth='s', colors=None, alpha=1.0): alpha : float, default=1. Transparency value for the vectors. Values between 0 and 1. - .. note:: - - When using ``meth=l`` in QuTiP 4.6, the line transparency defaulted - to ``0.75`` and there was no way to alter it. - When the ``alpha`` parameter was added in QuTiP 4.7, the default - became ``alpha=1.0`` for values of ``meth``. + Notes + ----- + When using ``meth=l`` in QuTiP 4.6, the line transparency defaulted + to ``0.75`` and there was no way to alter it. + When the ``alpha`` parameter was added in QuTiP 4.7, the default + became ``alpha=1.0`` for values of ``meth``. """ points = np.asarray(points) @@ -360,14 +362,14 @@ 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. Parameters ---------- - state : Qobj + state : :obj:`.Qobj` Input state vector. kind : {'vector', 'point'} @@ -450,7 +452,7 @@ def add_annotation(self, state_or_vector, text, **kwargs): Parameters ---------- - state_or_vector : Qobj/array/list/tuple + state_or_vector : :obj:`.Qobj`/array/list/tuple Position for the annotaion. Qobj of a qubit or a vector of 3 elements. @@ -489,11 +491,11 @@ def add_arc(self, start, end, fmt="b", steps=None, **kwargs): Parameters ---------- - start : Qobj or array-like + start : :obj:`.Qobj` or array-like Array with cartesian coordinates of the first point, or a state vector or density matrix that can be mapped to a point on or within the Bloch sphere. - end : Qobj or array-like + end : :obj:`.Qobj` or array-like Array with cartesian coordinates of the second point, or a state vector or density matrix that can be mapped to a point on or within the Bloch sphere. @@ -563,11 +565,11 @@ def add_line(self, start, end, fmt="k", **kwargs): Parameters ---------- - start : Qobj or array-like + start : :obj:`.Qobj` or array-like Array with cartesian coordinates of the first point, or a state vector or density matrix that can be mapped to a point on or within the Bloch sphere. - end : Qobj or array-like + end : :obj:`.Qobj` or array-like Array with cartesian coordinates of the second point, or a state vector or density matrix that can be mapped to a point on or within the Bloch sphere. @@ -792,28 +794,31 @@ 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) 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)) color = color[indperm] + 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, @@ -823,6 +828,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]), @@ -880,7 +886,7 @@ def save(self, name=None, format='png', dirc=None, dpin=None): name : str Name of saved image. Must include path and format as well. - i.e. '/Users/Paul/Desktop/bloch.png' + i.e. '/Users/Me/Desktop/bloch.png' This overrides the 'format' and 'dirc' arguments. format : str Format of output image. diff --git a/qutip/bloch3d.py b/qutip/bloch3d.py deleted file mode 100644 index d387b6c4d3..0000000000 --- a/qutip/bloch3d.py +++ /dev/null @@ -1,517 +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/Paul/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) diff --git a/qutip/continuous_variables.py b/qutip/continuous_variables.py index 519f070ffa..bcd4391ce1 100644 --- a/qutip/continuous_variables.py +++ b/qutip/continuous_variables.py @@ -24,7 +24,7 @@ def correlation_matrix(basis, rho=None): ---------- basis : list List of operators that defines the basis for the correlation matrix. - rho : Qobj + rho : Qobj, optional Density matrix for which to calculate the correlation matrix. If `rho` is `None`, then a matrix of correlation matrix operators is returned instead of expectation values of those operators. @@ -71,7 +71,7 @@ def covariance_matrix(basis, rho, symmetrized=True): List of operators that defines the basis for the covariance matrix. rho : Qobj Density matrix for which to calculate the covariance matrix. - symmetrized : bool {True, False} + symmetrized : bool, default: True Flag indicating whether the symmetrized (default) or non-symmetrized correlation matrix is to be calculated. @@ -103,12 +103,12 @@ def correlation_matrix_field(a1, a2, rho=None): Field operator for mode 1. a2 : Qobj Field operator for mode 2. - rho : Qobj + rho : Qobj, optional Density matrix for which to calculate the covariance matrix. Returns ------- - cov_mat : ndarray + cov_mat : ndarray Array of complex numbers or Qobj's A 2-dimensional *array* of covariance values, or, if rho=0, a matrix of operators. @@ -129,17 +129,17 @@ def correlation_matrix_quadrature(a1, a2, rho=None, g=np.sqrt(2)): Field operator for mode 1. a2 : Qobj Field operator for mode 2. - rho : Qobj + rho : Qobj, optional Density matrix for which to calculate the covariance matrix. - g : float - Scaling factor for `a = 0.5 * g * (x + iy)`, default `g = sqrt(2)`. - The value of `g` is related to the value of `hbar` in the commutation - relation `[x, y] = i * hbar` via `hbar=2/g ** 2` giving the default - value `hbar=1`. + g : float, default: sqrt(2) + Scaling factor for ``a = 0.5 * g * (x + iy)``, default ``g = sqrt(2)``. + The value of ``g`` is related to the value of ``hbar`` in the + commutation relation ``[x, y] = i * hbar`` via ``hbar=2/g ** 2`` giving + the default value ``hbar=1``. Returns ------- - corr_mat : ndarray + corr_mat : ndarray Array of complex numbers or Qobj's A 2-dimensional *array* of covariance values for the field quadratures, or, if rho=0, a matrix of operators. @@ -163,31 +163,31 @@ def wigner_covariance_matrix(a1=None, a2=None, R=None, rho=None, g=np.sqrt(2)): :math:`R = (q_1, p_1, q_2, p_2)^T` is the vector with quadrature operators for the two modes. - Alternatively, if `R = None`, and if annihilation operators `a1` and `a2` - for the two modes are supplied instead, the quadrature correlation matrix - is constructed from the annihilation operators before then the covariance - matrix is calculated. + Alternatively, if ``R = None``, and if annihilation operators ``a1`` and + ``a2`` for the two modes are supplied instead, the quadrature correlation + matrix is constructed from the annihilation operators before then the + covariance matrix is calculated. Parameters ---------- - a1 : Qobj + a1 : Qobj, optional Field operator for mode 1. - a2 : Qobj + a2 : Qobj, optional Field operator for mode 2. - R : ndarray + R : ndarray, optional The quadrature correlation matrix. - rho : Qobj + rho : Qobj, optional Density matrix for which to calculate the covariance matrix. - g : float - Scaling factor for `a = 0.5 * g * (x + iy)`, default `g = sqrt(2)`. - The value of `g` is related to the value of `hbar` in the commutation - relation `[x, y] = i * hbar` via `hbar=2/g ** 2` giving the default - value `hbar=1`. + g : float, default: sqrt(2) + Scaling factor for ``a = 0.5 * g * (x + iy)``, default ``g = sqrt(2)``. + The value of ``g`` is related to the value of ``hbar`` in the + commutation relation ``[x, y] = i * hbar`` via ``hbar=2/g ** 2`` giving + the default value ``hbar=1``. Returns ------- @@ -229,19 +229,19 @@ def logarithmic_negativity(V, g=np.sqrt(2)): Parameters ---------- - V : *2d array* + V : ndarray The covariance matrix. - g : float - Scaling factor for `a = 0.5 * g * (x + iy)`, default `g = sqrt(2)`. - The value of `g` is related to the value of `hbar` in the commutation - relation `[x, y] = i * hbar` via `hbar=2/g ** 2` giving the default - value `hbar=1`. + g : float, default: sqrt(2) + Scaling factor for ``a = 0.5 * g * (x + iy)``, default ``g = sqrt(2)``. + The value of ``g`` is related to the value of ``hbar`` in the + commutation relation ``[x, y] = i * hbar`` via ``hbar=2/g ** 2`` giving + the default value ``hbar=1``. Returns ------- - N : float + N : float The logarithmic negativity for the two-mode Gaussian state that is described by the the Wigner covariance matrix V. diff --git a/qutip/core/__init__.py b/qutip/core/__init__.py index b12830ba87..4574f20454 100644 --- a/qutip/core/__init__.py +++ b/qutip/core/__init__.py @@ -11,6 +11,7 @@ from .superop_reps import * from .subsystem_apply import * from .blochredfield import * +from .energy_restricted import * from . import gates del cy # File in cy are not public facing diff --git a/qutip/core/_brtensor.pyx b/qutip/core/_brtensor.pyx index 530cf63d1b..984e016ede 100644 --- a/qutip/core/_brtensor.pyx +++ b/qutip/core/_brtensor.pyx @@ -266,8 +266,7 @@ cdef class _BlochRedfieldElement(_BaseElement): raise ValueError('Invalid tensortype') cpdef object qobj(self, t): - return Qobj(self.data(t), dims=self.dims, type="super", - copy=False, superrep="super") + return Qobj(self.data(t), dims=self.dims, copy=False, superrep="super") cpdef object coeff(self, t): return 1. diff --git a/qutip/core/_brtools.pyx b/qutip/core/_brtools.pyx index 45144053c4..eb43e758b0 100644 --- a/qutip/core/_brtools.pyx +++ b/qutip/core/_brtools.pyx @@ -37,7 +37,7 @@ cdef class SpectraCoefficient(Coefficient): return self.coeff_t(t) * self.coeff_w(self.w) cpdef Coefficient copy(self): - """Return a copy of the :obj:`Coefficient`.""" + """Return a copy of the :obj:`.Coefficient`.""" return SpectraCoefficient(self.coeff_t, self.coeff_w, self.w) def replace_arguments(self, _args=None, *, w=None, **kwargs): diff --git a/qutip/core/blochredfield.py b/qutip/core/blochredfield.py index e23f6af90a..b3c0509572 100644 --- a/qutip/core/blochredfield.py +++ b/qutip/core/blochredfield.py @@ -33,13 +33,13 @@ def bloch_redfield_tensor(H, a_ops, c_ops=[], sec_cutoff=0.1, a_op : :class:`qutip.Qobj`, :class:`qutip.QobjEvo` The operator coupling to the environment. Must be hermitian. - spectra : :class:`Coefficient`, func, str + spectra : :obj:`.Coefficient`, func, str The corresponding bath spectra. - Can be a :class:`~Coefficient` using an 'w' args, a function of the - frequency or a string. The :class:`SpectraCoefficient` can be used for - array based coefficient. + Can be a :obj:`.Coefficient` using an 'w' args, a function of the + frequency or a string. The :class:`SpectraCoefficient` can be used + for array based coefficient. The spectra can depend on ``t`` if the corresponding - ``a_op`` is a :class:`QobjEvo`. + ``a_op`` is a :obj:`.QobjEvo`. Example: @@ -117,13 +117,13 @@ def brterm(H, a_op, spectra, sec_cutoff=0.1, a_op : :class:`qutip.Qobj`, :class:`qutip.QobjEvo` The operator coupling to the environment. Must be hermitian. - spectra : :class:`Coefficient`, func, str + spectra : :obj:`.Coefficient`, func, str The corresponding bath spectra. - Can be a :class:`~Coefficient` using an 'w' args, a function of the + Can be a :obj:`.Coefficient` using an 'w' args, a function of the frequency or a string. The :class:`SpectraCoefficient` can be used for array based coefficient. The spectra can depend on ``t`` if the corresponding - ``a_op`` is a :class:`QobjEvo`. + ``a_op`` is a :obj:`.QobjEvo`. Example: @@ -147,12 +147,12 @@ def brterm(H, a_op, spectra, sec_cutoff=0.1, Returns ------- - R, [evecs]: :class:`~Qobj`, :class:`~QobjEvo` or tuple + R, [evecs]: :obj:`.Qobj`, :obj:`.QobjEvo` or tuple If ``fock_basis``, return the Bloch Redfield tensor in the outside basis. Otherwise return the Bloch Redfield tensor in the diagonalized Hamiltonian basis and the eigenvectors of the Hamiltonian as hstacked - column. The tensors and, if given, evecs, will be :obj:`~QobjEvo` if - the ``H`` and ``a_op`` is time dependent, :obj:`Qobj` otherwise. + column. The tensors and, if given, evecs, will be :obj:`.QobjEvo` if + the ``H`` and ``a_op`` is time dependent, :obj:`.Qobj` otherwise. """ if isinstance(H, _EigenBasisTransform): Hdiag = H diff --git a/qutip/core/coefficient.py b/qutip/core/coefficient.py index 0c6ed689be..cf30326685 100644 --- a/qutip/core/coefficient.py +++ b/qutip/core/coefficient.py @@ -66,7 +66,7 @@ def coefficient(base, *, tlist=None, args={}, args_ctypes={}, For function based coefficients, the function signature must be either: * ``f(t, ...)`` where the other arguments are supplied as ordinary - "pythonic" arguments (e.g. ``f(t, w, a=5)) + "pythonic" arguments (e.g. ``f(t, w, a=5)``) * ``f(t, args)`` where the arguments are supplied in a "dict" named ``args`` @@ -75,33 +75,37 @@ def coefficient(base, *, tlist=None, args={}, args_ctypes={}, may be overriden here by specifying either ``function_style="pythonic"`` or ``function_style="dict"``. - *Examples* - # pythonic style function signature + *Examples*: - def f1_t(t, w): - return np.exp(-1j * t * w) + - pythonic style function signature:: - coeff1 = coefficient(f1_t, args={"w": 1.}) + def f1_t(t, w): + return np.exp(-1j * t * w) - # dict style function signature + coeff1 = coefficient(f1_t, args={"w": 1.}) - def f2_t(t, args): - return np.exp(-1j * t * args["w"]) + - dict style function signature:: - coeff2 = coefficient(f2_t, args={"w": 1.}) + def f2_t(t, args): + return np.exp(-1j * t * args["w"]) + + coeff2 = coefficient(f2_t, args={"w": 1.}) For string based coeffients, the string must be a compilable python code resulting in a complex. The following symbols are defined: - sin cos tan asin acos atan pi - sinh cosh tanh asinh acosh atanh - exp log log10 erf zerf sqrt - real imag conj abs norm arg proj + + sin, cos, tan, asin, acos, atan, pi, + sinh, cosh, tanh, asinh, acosh, atanh, + exp, log, log10, erf, zerf, sqrt, + real, imag, conj, abs, norm, arg, proj, numpy as np, scipy.special as spe (python interface) and cython_special (scipy cython interface) - *Examples* + *Examples*:: + coeff = coefficient('exp(-1j*w1*t)', args={"w1":1.}) + 'args' is needed for string coefficient at compilation. It is a dict of (name:object). The keys must be a valid variables string. @@ -116,7 +120,8 @@ def f2_t(t, args): interpolation. When ``order = 0``, the interpolation is step function that evaluates to the most recent value. - *Examples* + *Examples*:: + tlist = np.logspace(-5,0,100) H = QobjEvo(np.exp(-1j*tlist), tlist=tlist) @@ -140,7 +145,7 @@ def f2_t(t, args): tlist : iterable, optional Times for each element of an array based coefficient. - function_style : str, ["dict", "pythonic", None] + function_style : str {"dict", "pythonic", None}, optional Function signature of function based coefficients. args_ctypes : dict, optional @@ -245,6 +250,9 @@ class CompilationOptions(QutipOptions): clean_on_error : bool [True] When writing a cython file that cannot be imported, erase it. + + build_dir: str [None] + cythonize's build_dir. """ _link_flags = "" _compiler_flags = "" @@ -275,6 +283,7 @@ class CompilationOptions(QutipOptions): "link_flags": _link_flags, "extra_import": "", "clean_on_error": True, + "build_dir": None, } _settings_name = "compile" @@ -479,7 +488,7 @@ def make_cy_code(code, variables, constants, raw, compile_opt): @cython.auto_pickle(True) cdef class StrCoefficient(Coefficient): \"\"\" - String compiled as a :obj:`Coefficient` using cython. + String compiled as a :obj:`.Coefficient` using cython. \"\"\" cdef: str codeString @@ -490,7 +499,7 @@ def __init__(self, base, var, cte, args): {init_cte}{init_var}{init_arg} cpdef Coefficient copy(self): - \"\"\"Return a copy of the :obj:`Coefficient`.\"\"\" + \"\"\"Return a copy of the :obj:`.Coefficient`.\"\"\" cdef StrCoefficient out = StrCoefficient.__new__(StrCoefficient) out.codeString = self.codeString {copy_cte}{copy_var} @@ -498,9 +507,9 @@ def __init__(self, base, var, cte, args): def replace_arguments(self, _args=None, **kwargs): \"\"\" - Return a :obj:`Coefficient` with args changed for :obj:`Coefficient` - built from 'str' or a python function. Or a the :obj:`Coefficient` - itself if the :obj:`Coefficient` does not use arguments. New arguments + Return a :obj:`.Coefficient` with args changed for :obj:`.Coefficient` + built from 'str' or a python function. Or a the :obj:`.Coefficient` + itself if the :obj:`.Coefficient` does not use arguments. New arguments can be passed as a dict or as keywords. Parameters @@ -554,7 +563,9 @@ def compile_code(code, file_name, parsed, c_opt): include_dirs=[np.get_include()], language='c++' ) - ext_modules = cythonize(coeff_file, force=True) + ext_modules = cythonize( + coeff_file, force=True, build_dir=c_opt['build_dir'] + ) setup(ext_modules=ext_modules) except Exception as e: if c_opt['clean_on_error']: diff --git a/qutip/core/cy/_element.pyx b/qutip/core/cy/_element.pyx index de2f574690..d77f3e6dfd 100644 --- a/qutip/core/cy/_element.pyx +++ b/qutip/core/cy/_element.pyx @@ -23,11 +23,11 @@ cdef class _BaseElement: terms used by QobjEvo and solvers to describe operators. Conceptually each term is given by ``coeff(t) * qobj(t)`` where - ``coeff`` is a complex coefficient and ``qobj`` is a :obj:`~Qobj`. Both + ``coeff`` is a complex coefficient and ``qobj`` is a :obj:`.Qobj`. Both are functions of time. :meth:`~_BaseElement.coeff` returns the - coefficient at ``t``. :meth:`~_BaseElement.qobj` returns the :obj:`~Qobj`. + coefficient at ``t``. :meth:`~_BaseElement.qobj` returns the :obj:`.Qobj`. - For example, a :obj:`QobjEvo` instance created by:: + For example, a :obj:`.QobjEvo` instance created by:: QobjEvo([sigmax(), [sigmay(), 'cos(pi * t)']]) @@ -37,9 +37,9 @@ cdef class _BaseElement: :obj:`~_BaseElement` defines the interface to time-dependent terms. Sub-classes implement terms defined in different ways. For example, :obj:`~_ConstantElement` implements a term that - consists only of a constant :obj:`~Qobj` (i.e. where there is no dependence + consists only of a constant :obj:`.Qobj` (i.e. where there is no dependence on ``t``), :obj:`~_EvoElement` implements a term that consists of a - time-dependet :obj:`~Coefficient` times a constant :obj:`~Qobj`, and + time-dependet :obj:`.Coefficient` times a constant :obj:`.Qobj`, and so on. .. note:: @@ -61,7 +61,7 @@ cdef class _BaseElement: """ cpdef Data data(self, t): """ - Returns the underlying :obj:`~Data` of the :obj:`~Qobj` component + Returns the underlying :obj:`~Data` of the :obj:`.Qobj` component of the term at time ``t``. Parameters @@ -72,7 +72,7 @@ cdef class _BaseElement: Returns ------- :obj:`~Data` - The underlying data of the :obj:`~Qobj` component of the term + The underlying data of the :obj:`.Qobj` component of the term at time ``t``. """ raise NotImplementedError( @@ -81,7 +81,7 @@ cdef class _BaseElement: cpdef object qobj(self, t): """ - Returns the :obj:`~Qobj` component of the term at time ``t``. + Returns the :obj:`.Qobj` component of the term at time ``t``. Parameters ---------- @@ -90,8 +90,8 @@ cdef class _BaseElement: Returns ------- - :obj:`~Qobj` - The :obj:`~Qobj` component of the term at time ``t``. + :obj:`.Qobj` + The :obj:`.Qobj` component of the term at time ``t``. """ raise NotImplementedError( "Sub-classes of _BaseElement should implement .qobj(t)." @@ -175,7 +175,7 @@ cdef class _BaseElement: def linear_map(self, f, anti=False): """ Return a new element representing a linear transformation ``f`` - of the :obj:`~Qobj` portion of this element and possibly a + of the :obj:`.Qobj` portion of this element and possibly a complex conjucation of the coefficient portion (when ``f`` is an antilinear map). @@ -186,7 +186,7 @@ cdef class _BaseElement: Parameters ---------- f : function - The linear transformation to apply to the :obj:`~Qobj` of this + The linear transformation to apply to the :obj:`.Qobj` of this element. anti : bool Whether to take the complex conjugate of the coefficient. Default @@ -206,7 +206,7 @@ cdef class _BaseElement: """ Return a copy of the element with the (possible) additional arguments to any time-dependent functions updated to the given argument values. - The arguments of any contained :obj:`~Coefficient` instances are also + The arguments of any contained :obj:`.Coefficient` instances are also replaced. If the operation does not modify this element, the original element @@ -263,14 +263,19 @@ cdef class _BaseElement: def __call__(self, t, args=None): if args: cache = [] - self = self.replace_arguments(args, cache) + new = self.replace_arguments(args, cache) + return new.qobj(t) * new.coeff(t) return self.qobj(t) * self.coeff(t) + @property + def dtype(self): + return None + cdef class _ConstantElement(_BaseElement): """ - Constant part of a list format :obj:`QobjEvo`. - A constant :obj:`QobjEvo` will contain one `_ConstantElement`:: + Constant part of a list format :obj:`.QobjEvo`. + A constant :obj:`.QobjEvo` will contain one `_ConstantElement`:: qevo = QobjEvo(H0) qevo.elements = [_ConstantElement(H0)] @@ -312,10 +317,14 @@ 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): """ - A pair of a :obj:`Qobj` and a :obj:`Coefficient` from the list format + A pair of a :obj:`.Qobj` and a :obj:`.Coefficient` from the list format time-dependent operator:: qevo = QobjEvo([[H0, coeff0], [H1, coeff1]]) @@ -365,14 +374,18 @@ cdef class _EvoElement(_BaseElement): def replace_arguments(self, args, cache=None): return _EvoElement( - self._qobj.copy(), + self._qobj, self._coefficient.replace_arguments(args) ) + @property + def dtype(self): + return type(self._data) + cdef class _FuncElement(_BaseElement): """ - Used with :obj:`QobjEvo` to build an evolution term from a function with + Used with :obj:`.QobjEvo` to build an evolution term from a function with either the signature: :: func(t: float, ...) -> Qobj @@ -384,7 +397,7 @@ cdef class _FuncElement(_BaseElement): In the new style, ``func`` may accept arbitrary named arguments and is called as ``func(t, **args)``. - A :obj:`QobjEvo` created from such a function contains one + A :obj:`.QobjEvo` created from such a function contains one :obj:`_FuncElement`: :: qevo = QobjEvo(func, args=args) @@ -496,7 +509,7 @@ cdef class _MapElement(_BaseElement): """ :obj:`_FuncElement` decorated with linear tranformations. - Linear tranformations available in :obj:`QobjEvo` include transpose, + Linear tranformations available in :obj:`.QobjEvo` include transpose, adjoint, conjugate, convertion and product with number:: ``` op = QobjEvo(f, args=args) diff --git a/qutip/core/cy/coefficient.pyx b/qutip/core/cy/coefficient.pyx index 5ecc6bf9cb..42a84cfca6 100644 --- a/qutip/core/cy/coefficient.pyx +++ b/qutip/core/cy/coefficient.pyx @@ -79,9 +79,9 @@ def coefficient_function_parameters(func, style=None): cdef class Coefficient: """ `Coefficient` are the time-dependant scalar of a `[Qobj, coeff]` pair - composing time-dependant operator in list format for :obj:`QobjEvo`. + composing time-dependant operator in list format for :obj:`.QobjEvo`. - :obj:`Coefficient` are immutable. + :obj:`.Coefficient` are immutable. """ def __init__(self, args, **_): self.args = args @@ -90,7 +90,7 @@ cdef class Coefficient: """ Replace the arguments (``args``) of a coefficient. - Returns a new :obj:`Coefficient` if the coefficient has arguments, or + Returns a new :obj:`.Coefficient` if the coefficient has arguments, or the original coefficient if it does not. Arguments to replace may be supplied either in a dictionary as the first position argument, or passed as keywords, or as a combination of the two. Arguments not @@ -114,7 +114,7 @@ cdef class Coefficient: Parameters ---------- t : float - Time at which to evaluate the :obj:`Coefficient`. + Time at which to evaluate the :obj:`.Coefficient`. _args : dict Dictionary of arguments to use instead of the stored ones. @@ -128,7 +128,7 @@ cdef class Coefficient: return self._call(t) cdef double complex _call(self, double t) except *: - """Core computation of the :obj:`Coefficient`.""" + """Core computation of the :obj:`.Coefficient`.""" # All Coefficient sub-classes should overwrite this or __call__ return complex(self(t)) @@ -152,22 +152,22 @@ cdef class Coefficient: return NotImplemented cpdef Coefficient copy(self): - """Return a copy of the :obj:`Coefficient`.""" + """Return a copy of the :obj:`.Coefficient`.""" return pickle.loads(pickle.dumps(self)) def conj(self): - """ Return a conjugate :obj:`Coefficient` of this""" + """ Return a conjugate :obj:`.Coefficient` of this""" return ConjCoefficient(self) def _cdc(self): - """ Return a :obj:`Coefficient` being the norm of this""" + """ Return a :obj:`.Coefficient` being the norm of this""" return NormCoefficient(self) @cython.auto_pickle(True) cdef class FunctionCoefficient(Coefficient): """ - :obj:`Coefficient` wrapping a Python function. + :obj:`.Coefficient` wrapping a Python function. Parameters ---------- @@ -220,7 +220,7 @@ cdef class FunctionCoefficient(Coefficient): return self.func(t, self.args) cpdef Coefficient copy(self): - """Return a copy of the :obj:`Coefficient`.""" + """Return a copy of the :obj:`.Coefficient`.""" return FunctionCoefficient( self.func, self.args.copy(), @@ -232,7 +232,7 @@ cdef class FunctionCoefficient(Coefficient): """ Replace the arguments (``args``) of a coefficient. - Returns a new :obj:`Coefficient` if the coefficient has arguments, or + Returns a new :obj:`.Coefficient` if the coefficient has arguments, or the original coefficient if it does not. Arguments to replace may be supplied either in a dictionary as the first position argument, or passed as keywords, or as a combination of the two. Arguments not @@ -269,7 +269,7 @@ def proj(x): cdef class StrFunctionCoefficient(Coefficient): """ - A :obj:`Coefficient` defined by a string containing a simple Python + A :obj:`.Coefficient` defined by a string containing a simple Python expression. The string should contain a compilable Python expression that results in a @@ -348,7 +348,7 @@ def coeff(t, args): return self.func(t, self.args) cpdef Coefficient copy(self): - """Return a copy of the :obj:`Coefficient`.""" + """Return a copy of the :obj:`.Coefficient`.""" return StrFunctionCoefficient(self.base, self.args.copy()) def __reduce__(self): @@ -358,7 +358,7 @@ def coeff(t, args): """ Replace the arguments (``args``) of a coefficient. - Returns a new :obj:`Coefficient` if the coefficient has arguments, or + Returns a new :obj:`.Coefficient` if the coefficient has arguments, or the original coefficient if it does not. Arguments to replace may be supplied either in a dictionary as the first position argument, or passed as keywords, or as a combination of the two. Arguments not @@ -381,7 +381,7 @@ def coeff(t, args): cdef class InterCoefficient(Coefficient): """ - A :obj:`Coefficient` built from an interpolation of a numpy array. + A :obj:`.Coefficient` built from an interpolation of a numpy array. Parameters ---------- @@ -532,7 +532,7 @@ cdef class InterCoefficient(Coefficient): return cls.restore(tlist, poly) cpdef Coefficient copy(self): - """Return a copy of the :obj:`Coefficient`.""" + """Return a copy of the :obj:`.Coefficient`.""" return InterCoefficient.restore(*self.np_arrays, self.dt) @@ -556,7 +556,7 @@ cdef Coefficient add_inter(InterCoefficient left, InterCoefficient right): @cython.auto_pickle(True) cdef class SumCoefficient(Coefficient): """ - A :obj:`Coefficient` built from the sum of two other coefficients. + A :obj:`.Coefficient` built from the sum of two other coefficients. A :obj:`SumCoefficient` is returned as the result of the addition of two coefficients, e.g. :: @@ -574,14 +574,14 @@ cdef class SumCoefficient(Coefficient): return self.first._call(t) + self.second._call(t) cpdef Coefficient copy(self): - """Return a copy of the :obj:`Coefficient`.""" + """Return a copy of the :obj:`.Coefficient`.""" return SumCoefficient(self.first.copy(), self.second.copy()) def replace_arguments(self, _args=None, **kwargs): """ Replace the arguments (``args``) of a coefficient. - Returns a new :obj:`Coefficient` if the coefficient has arguments, or + Returns a new :obj:`.Coefficient` if the coefficient has arguments, or the original coefficient if it does not. Arguments to replace may be supplied either in a dictionary as the first position argument, or passed as keywords, or as a combination of the two. Arguments not @@ -604,7 +604,7 @@ cdef class SumCoefficient(Coefficient): @cython.auto_pickle(True) cdef class MulCoefficient(Coefficient): """ - A :obj:`Coefficient` built from the product of two other coefficients. + A :obj:`.Coefficient` built from the product of two other coefficients. A :obj:`MulCoefficient` is returned as the result of the multiplication of two coefficients, e.g. :: @@ -622,14 +622,14 @@ cdef class MulCoefficient(Coefficient): return self.first._call(t) * self.second._call(t) cpdef Coefficient copy(self): - """Return a copy of the :obj:`Coefficient`.""" + """Return a copy of the :obj:`.Coefficient`.""" return MulCoefficient(self.first.copy(), self.second.copy()) def replace_arguments(self, _args=None, **kwargs): """ Replace the arguments (``args``) of a coefficient. - Returns a new :obj:`Coefficient` if the coefficient has arguments, or + Returns a new :obj:`.Coefficient` if the coefficient has arguments, or the original coefficient if it does not. Arguments to replace may be supplied either in a dictionary as the first position argument, or passed as keywords, or as a combination of the two. Arguments not @@ -652,7 +652,7 @@ cdef class MulCoefficient(Coefficient): @cython.auto_pickle(True) cdef class ConjCoefficient(Coefficient): """ - The conjugate of a :obj:`Coefficient`. + The conjugate of a :obj:`.Coefficient`. A :obj:`ConjCoefficient` is returned by ``Coefficient.conj()`` and ``qutip.coefficent.conj(Coefficient)``. @@ -666,14 +666,14 @@ cdef class ConjCoefficient(Coefficient): return conj(self.base._call(t)) cpdef Coefficient copy(self): - """Return a copy of the :obj:`Coefficient`.""" + """Return a copy of the :obj:`.Coefficient`.""" return ConjCoefficient(self.base.copy()) def replace_arguments(self, _args=None, **kwargs): """ Replace the arguments (``args``) of a coefficient. - Returns a new :obj:`Coefficient` if the coefficient has arguments, or + Returns a new :obj:`.Coefficient` if the coefficient has arguments, or the original coefficient if it does not. Arguments to replace may be supplied either in a dictionary as the first position argument, or passed as keywords, or as a combination of the two. Arguments not @@ -695,7 +695,7 @@ cdef class ConjCoefficient(Coefficient): @cython.auto_pickle(True) cdef class NormCoefficient(Coefficient): """ - The L2 :func:`norm` of a :obj:`Coefficient`. A shortcut for + The L2 :func:`norm` of a :obj:`.Coefficient`. A shortcut for ``conj(coeff) * coeff``. :obj:`NormCoefficient` is returned by @@ -710,7 +710,7 @@ cdef class NormCoefficient(Coefficient): """ Replace the arguments (``args``) of a coefficient. - Returns a new :obj:`Coefficient` if the coefficient has arguments, or + Returns a new :obj:`.Coefficient` if the coefficient has arguments, or the original coefficient if it does not. Arguments to replace may be supplied either in a dictionary as the first position argument, or passed as keywords, or as a combination of the two. Arguments not @@ -732,7 +732,7 @@ cdef class NormCoefficient(Coefficient): return norm(self.base._call(t)) cpdef Coefficient copy(self): - """Return a copy of the :obj:`Coefficient`.""" + """Return a copy of the :obj:`.Coefficient`.""" return NormCoefficient(self.base.copy()) @@ -752,7 +752,7 @@ cdef class ConstantCoefficient(Coefficient): """ Replace the arguments (``args``) of a coefficient. - Returns a new :obj:`Coefficient` if the coefficient has arguments, or + Returns a new :obj:`.Coefficient` if the coefficient has arguments, or the original coefficient if it does not. Arguments to replace may be supplied either in a dictionary as the first position argument, or passed as keywords, or as a combination of the two. Arguments not @@ -772,5 +772,5 @@ cdef class ConstantCoefficient(Coefficient): return self.value cpdef Coefficient copy(self): - """Return a copy of the :obj:`Coefficient`.""" + """Return a copy of the :obj:`.Coefficient`.""" return self diff --git a/qutip/core/cy/qobjevo.pxd b/qutip/core/cy/qobjevo.pxd index 07e0d3df7c..04793dc825 100644 --- a/qutip/core/cy/qobjevo.pxd +++ b/qutip/core/cy/qobjevo.pxd @@ -6,12 +6,12 @@ from qutip.core.data.base cimport idxint cdef class QobjEvo: cdef: list elements - readonly list dims + readonly object _dims readonly (idxint, idxint) shape - readonly str type - readonly str superrep int _issuper int _isoper + readonly dict _feedback_functions + readonly dict _solver_only_feedback cpdef Data _call(QobjEvo self, double t) diff --git a/qutip/core/cy/qobjevo.pyx b/qutip/core/cy/qobjevo.pyx index 388551dd39..f508189c31 100644 --- a/qutip/core/cy/qobjevo.pyx +++ b/qutip/core/cy/qobjevo.pyx @@ -9,9 +9,9 @@ from functools import partial import qutip from .. import Qobj from .. import data as _data +from ..dimensions import Dimensions from ..coefficient import coefficient, CompilationOptions from ._element import * -from ..dimensions import type_from_dims from qutip.settings import settings from qutip.core.cy._element cimport _BaseElement @@ -19,7 +19,6 @@ from qutip.core.data cimport Dense, Data, dense from qutip.core.data.expect cimport * from qutip.core.data.reshape cimport (column_stack_dense, column_unstack_dense) from qutip.core.cy.coefficient cimport Coefficient -from qutip.core.qobj import _MATMUL_TYPE_LOOKUP from libc.math cimport fabs __all__ = ['QobjEvo'] @@ -29,29 +28,29 @@ cdef class QobjEvo: A class for representing time-dependent quantum objects, such as quantum operators and states. - Importantly, :obj:`~QobjEvo` instances are used to represent such + Importantly, :obj:`.QobjEvo` instances are used to represent such time-dependent quantum objects when working with QuTiP solvers. - A :obj:`~QobjEvo` instance may be constructed from one of the following: + A :obj:`.QobjEvo` instance may be constructed from one of the following: * a callable ``f(t: double, args: dict) -> Qobj`` that returns the value of the quantum object at time ``t``. - * a ``[Qobj, Coefficient]`` pair, where :obj:`~Coefficient` may also be - any item that can be used to construct a coefficient (e.g. a function, - a numpy array of coefficient values, a string expression). + * a ``[Qobj, Coefficient]`` pair, where the :obj:`Coefficient` may be any + item that :func:`.coefficient` can accept (e.g. a function, a numpy + array of coefficient values, a string expression). - * a :obj:`~Qobj` (which creates a constant :obj:`~QobjEvo` term). + * a :obj:`.Qobj` (which creates a constant :obj:`.QobjEvo` term). - * a list of such callables, pairs or :obj:`~Qobj`\s. + * a list of such callables, pairs or :obj:`.Qobj`\s. - * a :obj:`~QobjEvo` (in which case a copy is created, all other arguments + * a :obj:`.QobjEvo` (in which case a copy is created, all other arguments are ignored except ``args`` which, if passed, replaces the existing arguments). Parameters ---------- - Q_object : callable, list or Qobj + Q_object : callable, list or :obj:`.Qobj` A specification of the time-depedent quantum object. See the paragraph above for a full description and the examples section below for examples. @@ -80,15 +79,15 @@ cdef class QobjEvo: ``0`` use previous or left value. copy : bool, default=True - Whether to make a copy of the :obj:`Qobj` instances supplied in + Whether to make a copy of the :obj:`.Qobj` instances supplied in the ``Q_object`` parameter. compress : bool, default=True - Whether to compress the :obj:`QobjEvo` instance terms after the + Whether to compress the :obj:`.QobjEvo` instance terms after the instance has been created. This sums the constant terms in a single term and combines - ``[Qobj, coefficient]`` pairs with the same :obj:`~Qobj` into a single + ``[Qobj, coefficient]`` pairs with the same :obj:`.Qobj` into a single pair containing the sum of the coefficients. See :meth:`compress`. @@ -106,7 +105,6 @@ cdef class QobjEvo: Refer to Scipy's documentation for further details: https://docs.scipy.org/doc/scipy/reference/generated/scipy.interpolate.make_interp_spline.html - Attributes ---------- dims : list @@ -126,7 +124,7 @@ cdef class QobjEvo: Examples -------- - A :obj:`~QobjEvo` constructed from a function: + A :obj:`.QobjEvo` constructed from a function: .. code-block:: @@ -136,17 +134,17 @@ cdef class QobjEvo: QobjEvo(f, args={'w': 1j}) - For list based :obj:`~QobjEvo`, the list must consist of :obj`~Qobj` or + For list based :obj:`.QobjEvo`, the list must consist of :obj:`.Qobj` or ``[Qobj, Coefficient]`` pairs: .. code-block:: QobjEvo([H0, [H1, coeff1], [H2, coeff2]], args=args) - The coefficients may be specified either using a :obj:`~Coefficient` - object or by a function, string, numpy array or any object that - can be passed to the :func:`~coefficient` function. See the documentation - of :func:`coefficient` for a full description. + The coefficients may be specified either using a :obj:`Coefficient` object + or by a function, string, numpy array or any object that can be passed to + the :func:`.coefficient` function. See the documentation of + :func:`.coefficient` for a full description. An example of a coefficient specified by a function: @@ -179,8 +177,8 @@ cdef class QobjEvo: The coeffients array must have the same len as the tlist. - A :obj:`~QobjEvo` may also be built using simple arithmetic operations - combining :obj:`~Qobj` with :obj:`~Coefficient`, for example: + A :obj:`.QobjEvo` may also be built using simple arithmetic operations + combining :obj:`.Qobj` with :obj:`Coefficient`, for example: .. code-block:: python @@ -188,16 +186,15 @@ cdef class QobjEvo: qevo = H0 + H1 * coeff """ - def __init__(QobjEvo self, Q_object, args=None, tlist=None, - order=3, copy=True, compress=True, - function_style=None, boundary_conditions=None): + def __init__(QobjEvo self, Q_object, args=None, *, copy=True, compress=True, + function_style=None, + tlist=None, order=3, boundary_conditions=None): if isinstance(Q_object, QobjEvo): - self.dims = Q_object.dims.copy() + self._dims = Q_object._dims self.shape = Q_object.shape - self.type = Q_object.type - self._issuper = ( Q_object)._issuper - self._isoper = ( Q_object)._isoper self.elements = ( Q_object).elements.copy() + self._feedback_functions = Q_object._feedback_functions.copy() + self._solver_only_feedback = Q_object._solver_only_feedback.copy() if args: self.arguments(args) if compress: @@ -205,11 +202,11 @@ cdef class QobjEvo: return self.elements = [] - self.dims = None + self._dims = None self.shape = (0, 0) - self._issuper = -1 - self._isoper = -1 - args = args or {} + self._feedback_functions = {} + self._solver_only_feedback = {} + args = self._read_args(args or {}) if ( isinstance(Q_object, list) @@ -238,6 +235,12 @@ cdef class QobjEvo: ) ) + for key in self._feedback_functions: + # During _read_args, the dims could not have been set yet. + # To set the dims, for function QobjEvo, they need to be called. + # But to be called, the feedback args need to be read... + self._feedback_functions[key].check_consistency(self._dims) + if compress: self.compress() @@ -245,7 +248,16 @@ cdef class QobjEvo: cls = self.__class__.__name__ repr_str = f'{cls}: dims = {self.dims}, shape = {self.shape}, ' repr_str += f'type = {self.type}, superrep = {self.superrep}, ' - repr_str += f'isconstant = {self.isconstant}, num_elements = {self.num_elements}' + repr_str += f'isconstant = {self.isconstant}, ' + repr_str += f'num_elements = {self.num_elements}' + feedback_pairs = [] + for key, val in self._feedback_functions.items(): + feedback_pairs.append(key + ":" + repr(val)) + for key, val in self._solver_only_feedback.items(): + feedback_pairs.append(key + ":" + val) + if feedback_pairs: + repr_str += f', feedback = {feedback_pairs}' + return repr_str def _read_element(self, op, copy, tlist, args, order, function_style, @@ -280,40 +292,25 @@ cdef class QobjEvo: " received: {!r}".format(op) ) - if self.dims is None: - self.dims = qobj.dims + if self._dims is None: + self._dims = qobj._dims self.shape = qobj.shape - self.type = qobj.type - self.superrep = qobj.superrep - elif self.dims != qobj.dims or self.shape != qobj.shape: + elif self._dims != qobj._dims: raise ValueError( f"QobjEvo term {op!r} has dims {qobj.dims!r} and shape" f" {qobj.shape!r} but previous terms had dims {self.dims!r}" f" and shape {self.shape!r}." ) - elif self.type != qobj.type: - raise ValueError( - f"QobjEvo term {op!r} has type {qobj.type!r} but " - f"previous terms had type {self.type!r}." - ) - elif self.superrep != qobj.superrep: - raise ValueError( - f"QobjEvo term {op!r} has superrep {qobj.superrep!r} but " - f"previous terms had superrep {self.superrep!r}." - ) return out @classmethod - def _restore(cls, elements, dims, shape, type, superrep, flags): + def _restore(cls, elements, dims, shape): """Recreate a QobjEvo without using __init__. """ cdef QobjEvo out = cls.__new__(cls) out.elements = elements - out.dims = dims + out._dims = dims out.shape = shape - out.type = type - out.superrep = superrep - out._issuper, out._isoper = flags return out def _getstate(self): @@ -324,21 +321,18 @@ cdef class QobjEvo: # etc., so we create our own. return { "elements": self.elements, - "dims": self.dims, + "dims": self._dims, "shape": self.shape, - "type": self.type, - "superrep": self.superrep, - "flags": (self._issuper, self._isoper,) } def __call__(self, double t, dict _args=None, **kwargs): """ - Get the :class:`~Qobj` at ``t``. + Get the :obj:`.Qobj` at ``t``. Parameters ---------- t : float - Time at which the ``QobjEvo`` is to be evalued. + Time at which the :obj:`.QobjEvo` is to be evalued. _args : dict [optional] New arguments as a dict. Update args with ``arguments(new_args)``. @@ -347,10 +341,11 @@ cdef class QobjEvo: New arguments as a keywors. Update args with ``arguments(**new_args)``. - .. note:: - If both the positional ``_args`` and keywords are passed new values - from both will be used. If a key is present with both, the - ``_args`` dict value will take priority. + Notes + ----- + If both the positional ``_args`` and keywords are passed new values + from both will be used. If a key is present with both, the + ``_args`` dict value will take priority. """ if _args is not None or kwargs: if _args is not None: @@ -378,10 +373,7 @@ cdef class QobjEvo: isherm &= obj._isherm and coeff.imag == 0 out = _data.add(out, obj.data, coeff) - return Qobj( - out, dims=self.dims, copy=False, isherm=isherm or None, - type=self.type, superrep=self.superrep - ) + return Qobj(out, dims=self._dims, copy=False, isherm=isherm or None) cpdef Data _call(QobjEvo self, double t): t = self._prepare(t, None) @@ -402,10 +394,21 @@ cdef class QobjEvo: cdef object _prepare(QobjEvo self, object t, Data state=None): """ Precomputation before computing getting the element at `t`""" # We keep the function for feedback eventually + if self._feedback_functions and state is not None: + new_args = { + key: func(t, state) + for key, func in self._feedback_functions.items() + } + cache = [] + self.elements = [ + element.replace_arguments(new_args, cache=cache) + for element in self.elements + ] + return t def copy(QobjEvo self): - """Return a copy of this `QobjEvo`""" + """Return a copy of this :obj:`.QobjEvo`""" return QobjEvo(self, compress=False) def arguments(QobjEvo self, dict _args=None, **kwargs): @@ -421,19 +424,84 @@ cdef class QobjEvo: New arguments as a keywors. Update args with ``arguments(**new_args)``. - .. note:: - If both the positional ``_args`` and keywords are passed new values - from both will be used. If a key is present with both, the ``_args`` - dict value will take priority. + Notes + ----- + If both the positional ``_args`` and keywords are passed new values + from both will be used. If a key is present with both, the ``_args`` + dict value will take priority. """ if _args is not None: kwargs.update(_args) cache = [] + kwargs = self._read_args(kwargs) self.elements = [ element.replace_arguments(kwargs, cache=cache) for element in self.elements ] + def _read_args(self, args): + """ + Filter feedback args from normal args. + """ + new_args = {} + for key, val in args.items(): + if isinstance(val, _Feedback): + new_args[key] = val.default + if self._dims is not None: + val.check_consistency(self._dims) + if callable(val): + self._feedback_functions[key] = val + else: + self._solver_only_feedback[key] = val.code + else: + new_args[key] = val + if key in self._feedback_functions: + del self._feedback_functions[key] + if key in self._solver_only_feedback: + del self._solver_only_feedback[key] + + return new_args + + def _register_feedback(self, solvers_feeds, solver): + """ + Receive feedback source from solver. + + Parameters + ---------- + solvers_feeds : dict[str] + When ``feedback={key: solver_specific}`` is used, update arguments + with ``args[key] = solvers_feeds[solver_specific]``. + + solver: str + Name of the solver for the error message. + """ + new_args = {} + for key, feed in self._solver_only_feedback.items(): + if feed not in solvers_feeds: + raise ValueError( + f"Desired feedback {key} is not available for the {solver}." + ) + new_args[key] = solvers_feeds[feed] + self.arguments(**new_args) + + def _update_feedback(QobjEvo self, QobjEvo other=None): + """ + Merge feedback from ``op`` into self. + """ + if other is not None: + if not self._feedback_functions and other._feedback_functions: + self._feedback_functions = other._feedback_functions.copy() + elif other._feedback_functions: + self._feedback_functions.update(other._feedback_functions) + + if not self._solver_only_feedback and other._solver_only_feedback: + self._solver_only_feedback = other._solver_only_feedback.copy() + elif other._solver_only_feedback: + self._solver_only_feedback.update(other._solver_only_feedback) + + if self._feedback_functions: + for key in self._feedback_functions: + self._feedback_functions[key].check_consistency(self._dims) ########################################################################### # Math function # @@ -461,19 +529,21 @@ cdef class QobjEvo: def __iadd__(QobjEvo self, other): cdef _BaseElement element if isinstance(other, QobjEvo): - if other.dims != self.dims: + if other._dims != self._dims: raise TypeError("incompatible dimensions" + str(self.dims) + ", " + str(other.dims)) for element in ( other).elements: self.elements.append(element) + self._update_feedback(other) + elif isinstance(other, Qobj): - if other.dims != self.dims: + if other._dims != self._dims: raise TypeError("incompatible dimensions" + str(self.dims) + ", " + str(other.dims)) self.elements.append(_ConstantElement(other)) elif ( isinstance(other, numbers.Number) and - self.dims[0] == self.dims[1] + self._dims[0] == self._dims[1] ): self.elements.append(_ConstantElement(other * qutip.qeye_like(self))) else: @@ -508,53 +578,35 @@ cdef class QobjEvo: if isinstance(left, QobjEvo): return left.copy().__imatmul__(right) elif isinstance(left, Qobj): - if left.dims[1] != ( right).dims[0]: + if left._dims[1] != ( right)._dims[0]: raise TypeError("incompatible dimensions" + str(left.dims[1]) + ", " + str(( right).dims[0])) - - type_ =_MATMUL_TYPE_LOOKUP.get((left.type, right.type)) - if type_ is None: - raise TypeError( - "incompatible matmul types " - + repr(left.type) + " and " + repr(right.type) - ) - res = right.copy() - res.dims = [left.dims[0], right.dims[1]] + res._dims = Dimensions(left._dims[0], right._dims[1]) res.shape = (left.shape[0], right.shape[1]) - res.type = type_ left = _ConstantElement(left) res.elements = [left @ element for element in res.elements] - res._issuper = -1 - res._isoper = -1 + res._update_feedback() + return res + else: return NotImplemented def __rmatmul__(QobjEvo self, other): cdef QobjEvo res if isinstance(other, Qobj): - if other.dims[1] != self.dims[0]: + if other._dims[1] != self._dims[0]: raise TypeError("incompatible dimensions" + str(other.dims[1]) + ", " + str(self.dims[0])) - - type_ =_MATMUL_TYPE_LOOKUP.get((other.type, self.type)) - if type_ is None: - raise TypeError( - "incompatible matmul types " - + repr(other.type) + " and " + repr(self.type) - ) - res = self.copy() - res.dims = [other.dims[0], res.dims[1]] + res._dims = Dimensions([other._dims[0], res._dims[1]]) res.shape = (other.shape[0], res.shape[1]) - res.type = type_ other = _ConstantElement(other) res.elements = [other @ element for element in res.elements] - res._issuper = -1 - res._isoper = -1 + res._update_feedback() return res else: return NotImplemented @@ -565,28 +617,20 @@ cdef class QobjEvo: raise TypeError("incompatible dimensions" + str(self.dims[1]) + ", " + str(other.dims[0])) - - type_ =_MATMUL_TYPE_LOOKUP.get((self.type, other.type)) - if type_ is None: - raise TypeError( - "incompatible matmul types " - + repr(self.type) + " and " + repr(other.type) - ) - - self.dims = [self.dims[0], other.dims[1]] + self._dims = Dimensions([self.dims[0], other.dims[1]]) self.shape = (self.shape[0], other.shape[1]) - self.type = type_ - self._issuper = -1 - self._isoper = -1 if isinstance(other, Qobj): other = _ConstantElement(other) self.elements = [element @ other for element in self.elements] + self._update_feedback() elif isinstance(other, QobjEvo): self.elements = [left @ right for left, right in itertools.product( self.elements, ( other).elements )] + self._update_feedback(other) + else: return NotImplemented return self @@ -657,6 +701,7 @@ cdef class QobjEvo: cdef QobjEvo res = self.copy() res.elements = [element.linear_map(Qobj.trans) for element in res.elements] + res._dims = Dimensions(res._dims[0], res._dims[1]) return res def conj(self): @@ -671,6 +716,7 @@ cdef class QobjEvo: cdef QobjEvo res = self.copy() res.elements = [element.linear_map(Qobj.dag, True) for element in res.elements] + res._dims = Dimensions(res._dims[0], res._dims[1]) return res def to(self, data_type): @@ -679,16 +725,16 @@ cdef class QobjEvo: storage representation. The different storage representations available are the "data-layer - types". By default, these are `qutip.data.Dense` and `qutip.data.CSR`, - which respectively construct a dense matrix store and a compressed - sparse row one. + types". By default, these are :obj:`.Dense`, :obj:`.Dia` and + :obj:`.CSR`, which respectively construct a dense matrix, diagonal + sparse matrixand a compressed sparse row one. - The `QobjEvo` is transformed inplace. + The :obj:`.QobjEvo` is transformed inplace. Parameters ---------- data_type : type - The data-layer type that the data of this `Qobj` should be + The data-layer type that the data of this :obj:`.Qobj` should be converted to. Returns @@ -713,9 +759,17 @@ cdef class QobjEvo: Apply mapping to each Qobj contribution. Example: - ``QobjEvo([sigmax(), coeff]).linear_map(spre)`` + + ``QobjEvo([sigmax(), coeff]).linear_map(spre)`` + gives the same result has - ``QobjEvo([spre(sigmax()), coeff])`` + + ``QobjEvo([spre(sigmax()), coeff])`` + + Parameters + ---------- + op_mapping: callable + Funtion to apply to each elements. Returns ------- @@ -734,11 +788,10 @@ cdef class QobjEvo: raise TypeError("The op_mapping function must return a Qobj") cdef QobjEvo res = self.copy() res.elements = [element.linear_map(op_mapping) for element in res.elements] - res.dims = res.elements[0].qobj(0).dims + res._dims = res.elements[0].qobj(0)._dims res.shape = res.elements[0].qobj(0).shape - res.type = res.elements[0].qobj(0).type - res._issuper = res.elements[0].qobj(0).issuper - res._isoper = res.elements[0].qobj(0).isoper + res._update_feedback() + if not _skip_check: if res(0) != out: raise ValueError("The mapping is not linear") @@ -770,16 +823,16 @@ cdef class QobjEvo: def compress(self): """ - Look for redundance in the :obj:`~QobjEvo` components: + Look for redundance in the :obj:`.QobjEvo` components: - Constant parts, (:class:`~Qobj` without :class:`~Coefficient`) will be + Constant parts, (:obj:`.Qobj` without :obj:`Coefficient`) will be summed. - Pairs ``[Qobj, Coefficient]`` with the same :class:`~Qobj` are merged. + Pairs ``[Qobj, Coefficient]`` with the same :obj:`.Qobj` are merged. Example: ``[[sigmax(), f1], [sigmax(), f2]] -> [[sigmax(), f1+f2]]`` - The :class:`~QobjEvo` is transformed inplace. + The :obj:`.QobjEvo` is transformed inplace. Returns ------- @@ -816,12 +869,12 @@ cdef class QobjEvo: Returns ------- list_qevo: list - The QobjEvo as a list, element are either :class:`Qobj` for + The QobjEvo as a list, element are either :obj:`.Qobj` for constant parts, ``[Qobj, Coefficient]`` for coefficient based term. - The original format of the :class:`Coefficient` is not restored. - Lastly if the original `QobjEvo` is constructed with a function - returning a Qobj, the term is returned as a pair of the original - function and args (``dict``). + The original format of the :obj:`Coefficient` is not restored. + Lastly if the original :obj:`.QobjEvo` is constructed with a + function returning a Qobj, the term is returned as a pair of the + original function and args (``dict``). """ out = [] for element in self.elements: @@ -855,19 +908,59 @@ cdef class QobjEvo: @property def isoper(self): """Indicates if the system represents an operator.""" - # TODO: isoper should be part of dims - if self._isoper == -1: - self._isoper = type_from_dims(self.dims) == "oper" - return self._isoper + return self._dims.type in ['oper', 'scalar'] @property def issuper(self): """Indicates if the system represents a superoperator.""" - # TODO: issuper should/will be part of dims - # remove self._issuper then - if self._issuper == -1: - self._issuper = type_from_dims(self.dims) == "super" - return self._issuper + return self._dims.type == 'super' + + @property + def dims(self): + return self._dims.as_list() + + @property + def type(self): + return self._dims.type + + @property + 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' + + @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 # @@ -905,8 +998,8 @@ cdef class QobjEvo: raise ValueError("Must be an operator or super operator to compute" " an expectation value") if not ( - (self.dims[1] == state.dims[0]) or - (self.issuper and self.dims[1] == state.dims) + (self._dims[1] == state._dims[0]) or + (self.issuper and self._dims[1] == state._dims) ): raise ValueError("incompatible dimensions " + str(self.dims) + ", " + str(state.dims)) @@ -994,12 +1087,12 @@ cdef class QobjEvo: if not isinstance(state, Qobj): raise TypeError("A Qobj state is expected") - if self.dims[1] != state.dims[0]: + if self._dims[1] != state._dims[0]: raise ValueError("incompatible dimensions " + str(self.dims[1]) + ", " + str(state.dims[0])) return Qobj(self.matmul_data(t, state.data), - dims=[self.dims[0], state.dims[1]], + dims=[self._dims[0], state._dims[1]], copy=False ) @@ -1017,3 +1110,16 @@ cdef class QobjEvo: part = (<_BaseElement> element) out = part.matmul_data_t(t, state, out) return out + + +class _Feedback: + default = None + + def __init__(self): + raise NotImplementedError("Use subclass") + + def check_consistency(self, dims): + """ + Raise an error when the dims of the e_ops / state don't match. + Tell the dims to the feedback for reconstructing the Qobj. + """ diff --git a/qutip/core/data/add.pyx b/qutip/core/data/add.pyx index 45515af87a..abebef990e 100644 --- a/qutip/core/data/add.pyx +++ b/qutip/core/data/add.pyx @@ -27,7 +27,7 @@ __all__ = [ cdef int _ONE=1 -cdef void _check_shape(Data left, Data right) except * nogil: +cdef int _check_shape(Data left, Data right) except -1 nogil: if left.shape[0] != right.shape[0] or left.shape[1] != right.shape[1]: raise ValueError( "incompatible matrix shapes " @@ -35,6 +35,7 @@ cdef void _check_shape(Data left, Data right) except * nogil: + " and " + str(right.shape) ) + return 0 cdef idxint _add_csr(Accumulator *acc, CSR a, CSR b, CSR c, double tol) nogil: diff --git a/qutip/core/data/constant.py b/qutip/core/data/constant.py index 8061bb8832..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 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 +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 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/data/convert.pyx b/qutip/core/data/convert.pyx index d2681551b1..9b430d99af 100644 --- a/qutip/core/data/convert.pyx +++ b/qutip/core/data/convert.pyx @@ -314,7 +314,7 @@ cdef class _to: def register_aliases(self, aliases, layer_type): """ Register a user frendly name for a data-layer type to be recognized by - the :method:`parse` method. + the :meth:`parse` method. Parameters ---------- @@ -323,7 +323,7 @@ cdef class _to: layer_type : type Data-layer type, must have been registered with - :method:`add_conversions` first. + :meth:`add_conversions` first. """ if layer_type not in self.dtypes: raise ValueError( @@ -461,7 +461,7 @@ cdef class _create: def __call__(self, arg, shape=None, copy=True): """ - Build a :class:`qutip.data.Data` object from arg. + Build a :class:`.Data` object from arg. Parameters ---------- diff --git a/qutip/core/data/csr.pxd b/qutip/core/data/csr.pxd index 4c813564ef..4bc2b090b7 100644 --- a/qutip/core/data/csr.pxd +++ b/qutip/core/data/csr.pxd @@ -67,7 +67,7 @@ cdef inline Accumulator acc_alloc(size_t size): acc._sorted = True return acc -cdef inline void acc_scatter(Accumulator *acc, double complex value, base.idxint position) nogil: +cdef inline void acc_scatter(Accumulator *acc, double complex value, base.idxint position) noexcept nogil: """ Add a value to the accumulator for this row, in column `position`. The value is added on to any value already scattered into this position. @@ -85,7 +85,7 @@ cdef inline void acc_scatter(Accumulator *acc, double complex value, base.idxint acc._sorted &= acc.nnz == 0 or acc.nonzero[acc.nnz - 1] < position acc.nnz += 1 -cdef inline base.idxint acc_gather(Accumulator *acc, double complex *values, base.idxint *indices, double tol=0) nogil: +cdef inline base.idxint acc_gather(Accumulator *acc, double complex *values, base.idxint *indices, double tol=0) noexcept nogil: """ Copy all the accumulated values into this row into the output pointers. This will always output its values in sorted order. The pointers should @@ -115,7 +115,7 @@ cdef inline base.idxint acc_gather(Accumulator *acc, double complex *values, bas nnz += 1 return nnz -cdef inline void acc_reset(Accumulator *acc) nogil: +cdef inline void acc_reset(Accumulator *acc) noexcept nogil: """Prepare the accumulator to accept the next row of input.""" # We actually don't need to do anything to reset other than to change # our sentinel values; the sentinel `_cur_row` makes it easy to detect diff --git a/qutip/core/data/csr.pyx b/qutip/core/data/csr.pyx index 182c79714b..5bbce551cc 100644 --- a/qutip/core/data/csr.pyx +++ b/qutip/core/data/csr.pyx @@ -296,7 +296,7 @@ cpdef CSR copy_structure(CSR matrix): return out -cpdef inline base.idxint nnz(CSR matrix) nogil: +cpdef inline base.idxint nnz(CSR matrix) noexcept nogil: """Get the number of non-zero elements of a CSR matrix.""" return matrix.row_index[matrix.shape[0]] @@ -311,7 +311,7 @@ ctypedef fused _swap_data: double complex base.idxint -cdef inline void _sorter_swap(_swap_data *a, _swap_data *b) nogil: +cdef inline void _sorter_swap(_swap_data *a, _swap_data *b) noexcept nogil: a[0], b[0] = b[0], a[0] cdef class Sorter: @@ -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): @@ -643,29 +658,26 @@ cdef CSR from_coo_pointers( cpdef CSR from_dia(Dia matrix): - if matrix.num_diag == 0: - return zeros(*matrix.shape) - mat = matrix.as_scipy() - ordered = np.argsort(mat.offsets) - nnz = len(mat.offsets) * max(mat.shape) - ptrs = np.zeros(mat.shape[0]+1, dtype=idxint_dtype) - indices = np.zeros(nnz, dtype=idxint_dtype) - data = np.zeros(nnz, dtype=complex) - - ptr = 0 - for row in range(mat.shape[0]): - for idx in ordered: - off = mat.offsets[idx] - if off + row < 0: + cdef base.idxint col, diag, i, ptr=0 + cdef base.idxint nrows=matrix.shape[0], ncols=matrix.shape[1] + cdef base.idxint nnz = matrix.num_diag * min(matrix.shape) + cdef double complex[:] data = np.zeros(nnz, dtype=complex) + cdef base.idxint[:] cols = np.zeros(nnz, dtype=idxint_dtype) + cdef base.idxint[:] rows = np.zeros(nnz, dtype=idxint_dtype) + + for i in range(matrix.num_diag): + diag = matrix.offsets[i] + + for col in range(ncols): + if col - diag < 0 or col - diag >= nrows: continue - elif off + row >= mat.shape[1]: - break - indices[ptr] = off + row - data[ptr] = mat.data[idx, off + row] + data[ptr] = matrix.data[i * ncols + col] + rows[ptr] = col - diag + cols[ptr] = col ptr += 1 - ptrs[row + 1] = ptr - return CSR((data, indices, ptrs), matrix.shape, copy=False) + return from_coo_pointers(&rows[0], &cols[0], &data[0], matrix.shape[0], + matrix.shape[1], nnz) cdef inline base.idxint _diagonal_length( 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 2b37d09159..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 @@ -438,6 +446,7 @@ cdef Dia diags_( The shape of the output. The result does not need to be square, but the diagonals must be of the correct length to fit in. """ + cdef base.idxint i out = empty(n_rows, n_cols, len(offsets)) out.num_diag = len(offsets) for i in range(len(offsets)): diff --git a/qutip/core/data/expect.pxd b/qutip/core/data/expect.pxd index fd2c108fe2..26e1c4a565 100644 --- a/qutip/core/data/expect.pxd +++ b/qutip/core/data/expect.pxd @@ -3,13 +3,13 @@ from qutip.core.data cimport CSR, Dense, Data, Dia -cpdef double complex expect_csr(CSR op, CSR state) except * nogil -cpdef double complex expect_super_csr(CSR op, CSR state) except * nogil +cpdef double complex expect_csr(CSR op, CSR state) except * +cpdef double complex expect_super_csr(CSR op, CSR state) except * -cpdef double complex expect_csr_dense(CSR op, Dense state) except * nogil +cpdef double complex expect_csr_dense(CSR op, Dense state) except * cpdef double complex expect_super_csr_dense(CSR op, Dense state) except * nogil -cpdef double complex expect_dense(Dense op, Dense state) except * nogil +cpdef double complex expect_dense(Dense op, Dense state) except * cpdef double complex expect_super_dense(Dense op, Dense state) except * nogil cpdef double complex expect_dia(Dia op, Dia state) except * diff --git a/qutip/core/data/expect.pyx b/qutip/core/data/expect.pyx index a05969ddbb..1314e43658 100644 --- a/qutip/core/data/expect.pyx +++ b/qutip/core/data/expect.pyx @@ -24,7 +24,7 @@ __all__ = [ 'expect_super_csr_dense', 'expect_super_dia_dense', 'expect_super_data', ] -cdef void _check_shape_ket(Data op, Data state) except * nogil: +cdef int _check_shape_ket(Data op, Data state) except -1 nogil: if ( op.shape[1] != state.shape[0] # Matrix multiplication or state.shape[1] != 1 # State is ket @@ -32,8 +32,9 @@ cdef void _check_shape_ket(Data op, Data state) except * nogil: ): raise ValueError("incorrect input shapes " + str(op.shape) + " and " + str(state.shape)) + return 0 -cdef void _check_shape_dm(Data op, Data state) except * nogil: +cdef int _check_shape_dm(Data op, Data state) except -1 nogil: if ( op.shape[1] != state.shape[0] # Matrix multiplication or state.shape[0] != state.shape[1] # State is square @@ -41,15 +42,18 @@ cdef void _check_shape_dm(Data op, Data state) except * nogil: ): raise ValueError("incorrect input shapes " + str(op.shape) + " and " + str(state.shape)) + return 0 -cdef void _check_shape_super(Data op, Data state) except * nogil: +cdef int _check_shape_super(Data op, Data state) except -1 nogil: if state.shape[1] != 1: raise ValueError("expected a column-stacked matrix") if ( op.shape[1] != state.shape[0] # Matrix multiplication or op.shape[0] != op.shape[1] # Square matrix ): - raise ValueError("incompatible shapes " + str(op.shape) + ", " + str(state.shape)) + raise ValueError("incompatible shapes " + + str(op.shape) + ", " + str(state.shape)) + return 0 cdef double complex _expect_csr_ket(CSR op, CSR state) except * nogil: @@ -94,7 +98,7 @@ cdef double complex _expect_csr_dm(CSR op, CSR state) except * nogil: return out -cpdef double complex expect_super_csr(CSR op, CSR state) except * nogil: +cpdef double complex expect_super_csr(CSR op, CSR state) except *: """ Perform the operation `tr(op @ state)` where `op` is supplied as a superoperator, and `state` is a column-stacked operator. @@ -112,7 +116,7 @@ cpdef double complex expect_super_csr(CSR op, CSR state) except * nogil: return out -cpdef double complex expect_csr(CSR op, CSR state) except * nogil: +cpdef double complex expect_csr(CSR op, CSR state) except *: """ Get the expectation value of the operator `op` over the state `state`. The state can be either a ket or a density matrix. @@ -186,7 +190,7 @@ cdef double complex _expect_dense_dense_dm(Dense op, Dense state) except * nogil return out -cpdef double complex expect_csr_dense(CSR op, Dense state) except * nogil: +cpdef double complex expect_csr_dense(CSR op, Dense state) except *: """ Get the expectation value of the operator `op` over the state `state`. The state can be either a ket or a density matrix. @@ -201,7 +205,7 @@ cpdef double complex expect_csr_dense(CSR op, Dense state) except * nogil: return _expect_csr_dense_dm(op, state) -cpdef double complex expect_dense(Dense op, Dense state) except * nogil: +cpdef double complex expect_dense(Dense op, Dense state) except *: """ Get the expectation value of the operator `op` over the state `state`. The state can be either a ket or a density matrix. diff --git a/qutip/core/data/inner.pxd b/qutip/core/data/inner.pxd index 5c202060a6..ab7f6d6b24 100644 --- a/qutip/core/data/inner.pxd +++ b/qutip/core/data/inner.pxd @@ -2,5 +2,5 @@ from qutip.core.data.csr cimport CSR -cpdef double complex inner_csr(CSR left, CSR right, bint scalar_is_ket=*) except * nogil -cpdef double complex inner_op_csr(CSR left, CSR op, CSR right, bint scalar_is_ket=*) except * nogil +cpdef double complex inner_csr(CSR left, CSR right, bint scalar_is_ket=*) except * +cpdef double complex inner_op_csr(CSR left, CSR op, CSR right, bint scalar_is_ket=*) except * diff --git a/qutip/core/data/inner.pyx b/qutip/core/data/inner.pyx index d488ae5210..01b3cd0da8 100644 --- a/qutip/core/data/inner.pyx +++ b/qutip/core/data/inner.pyx @@ -21,7 +21,7 @@ __all__ = [ ] -cdef void _check_shape_inner(Data left, Data right) except * nogil: +cdef int _check_shape_inner(Data left, Data right) except -1 nogil: if ( (left.shape[0] != 1 and left.shape[1] != 1) or right.shape[1] != 1 @@ -32,8 +32,9 @@ cdef void _check_shape_inner(Data left, Data right) except * nogil: + " and " + str(right.shape) ) + return 0 -cdef void _check_shape_inner_op(Data left, Data op, Data right) except * nogil: +cdef int _check_shape_inner_op(Data left, Data op, Data right) except -1 nogil: cdef bint left_shape = left.shape[0] == 1 or left.shape[1] == 1 cdef bint left_op = ( (left.shape[0] == 1 and left.shape[1] == op.shape[0]) @@ -50,6 +51,7 @@ cdef void _check_shape_inner_op(Data left, Data op, Data right) except * nogil: " and ", str(right.shape), ])) + return 0 cdef double complex _inner_csr_bra_ket(CSR left, CSR right) nogil: cdef size_t col, ptr_bra, ptr_ket @@ -72,7 +74,7 @@ cdef double complex _inner_csr_ket_ket(CSR left, CSR right) nogil: out += conj(left.data[ptr_l]) * right.data[ptr_r] return out -cpdef double complex inner_csr(CSR left, CSR right, bint scalar_is_ket=False) except * nogil: +cpdef double complex inner_csr(CSR left, CSR right, bint scalar_is_ket=False) except *: """ Compute the complex inner product . The shape of `left` is used to determine if it has been supplied as a ket or a bra. The result of @@ -238,7 +240,7 @@ cpdef double complex inner_op_dia(Dia left, Dia op, Dia right, return inner cpdef double complex inner_op_csr(CSR left, CSR op, CSR right, - bint scalar_is_ket=False) except * nogil: + bint scalar_is_ket=False) except *: """ Compute the complex inner product . The shape of `left` is used to determine if it has been supplied as a ket or a bra. The result of @@ -251,7 +253,7 @@ cpdef double complex inner_op_csr(CSR left, CSR op, CSR right, """ _check_shape_inner_op(left, op, right) cdef double complex l - if left.shape[0] == left.shape[1] == op.shape[0] == op.shape[1] == right.shape[1] == 1: + if 1 == left.shape[1] == left.shape[0] == op.shape[0] == op.shape[1] == right.shape[1]: if not (csr.nnz(left) and csr.nnz(op) and csr.nnz(right)): return 0 l = conj(left.data[0]) if scalar_is_ket else left.data[0] diff --git a/qutip/core/data/matmul.pyx b/qutip/core/data/matmul.pyx index 06942c1314..238520c13c 100644 --- a/qutip/core/data/matmul.pyx +++ b/qutip/core/data/matmul.pyx @@ -59,7 +59,7 @@ __all__ = [ ] -cdef void _check_shape(Data left, Data right, Data out=None) except * nogil: +cdef int _check_shape(Data left, Data right, Data out=None) except -1 nogil: if left.shape[1] != right.shape[0]: raise ValueError( "incompatible matrix shapes " @@ -78,7 +78,7 @@ cdef void _check_shape(Data left, Data right, Data out=None) except * nogil: + " but needed " + str((left.shape[0], right.shape[1])) ) - + return 0 cdef idxint _matmul_csr_estimate_nnz(CSR left, CSR right): """ diff --git a/qutip/core/data/project.pyx b/qutip/core/data/project.pyx index 45fa7ac956..8b3b5e4751 100644 --- a/qutip/core/data/project.pyx +++ b/qutip/core/data/project.pyx @@ -15,7 +15,7 @@ __all__ = [ ] -cdef void _project_ket_csr(CSR ket, CSR out) nogil: +cdef int _project_ket_csr(CSR ket, CSR out) except -1 nogil: """ Calculate the projection of the given ket, and place the output in out. """ @@ -33,9 +33,10 @@ cdef void _project_ket_csr(CSR ket, CSR out) nogil: for ptr in range(nnz_in): out.data[offset + ptr] = ket.data[row] * conj(ket.data[ptr]) offset += nnz_in + return 0 -cdef void _project_bra_csr(CSR bra, CSR out) nogil: +cdef int _project_bra_csr(CSR bra, CSR out) except -1 nogil: """ Calculate the projection of the given bra, and place the output in out. """ @@ -66,6 +67,7 @@ cdef void _project_bra_csr(CSR bra, CSR out) nogil: # Handle all zero rows after the last non-zero entry. for row_out in range(row_out, out.shape[0]): out.row_index[row_out + 1] = cur + return 0 cpdef CSR project_csr(CSR state): diff --git a/qutip/core/data/solve.py b/qutip/core/data/solve.py index 31fc6c9659..db7abcafec 100644 --- a/qutip/core/data/solve.py +++ b/qutip/core/data/solve.py @@ -75,6 +75,9 @@ def solve_csr_dense(matrix: Union[CSR, Dia], target: Dense, method=None, raise ValueError("mkl is not available") elif method == "mkl_spsolve": solver = mkl_spsolve + # mkl does not support dia. + if isinstance(matrix, Dia): + matrix = _data.to("CSR", matrix) else: raise ValueError(f"Unknown sparse solver {method}.") diff --git a/qutip/core/data/trace.pyx b/qutip/core/data/trace.pyx index 210444dc31..3460424de1 100644 --- a/qutip/core/data/trace.pyx +++ b/qutip/core/data/trace.pyx @@ -15,18 +15,20 @@ __all__ = [ ] -cdef void _check_shape(Data matrix) except * nogil: +cdef int _check_shape(Data matrix) except -1 nogil: if matrix.shape[0] != matrix.shape[1]: raise ValueError("".join([ "matrix shape ", str(matrix.shape), " is not square.", ])) + return 0 -cdef void _check_shape_oper_ket(int N, Data matrix) except * nogil: +cdef int _check_shape_oper_ket(int N, Data matrix) except -1 nogil: if matrix.shape[0] != N * N or matrix.shape[1] != 1: raise ValueError("".join([ "matrix ", str(matrix.shape), " is not a stacked square matrix." ])) + return 0 cpdef double complex trace_csr(CSR matrix) except * nogil: diff --git a/qutip/core/dimensions.py b/qutip/core/dimensions.py index a2a4003a52..f19e3b2dee 100644 --- a/qutip/core/dimensions.py +++ b/qutip/core/dimensions.py @@ -5,67 +5,25 @@ # Everything should be explicitly imported, not made available by default. import numpy as np +import numbers from operator import getitem from functools import partial +from qutip.settings import settings -def is_scalar(dims): - """ - Returns True if a dims specification is effectively - a scalar (has dimension 1). - """ - return np.prod(flatten(dims)) == 1 - - -def is_vector(dims): - return ( - isinstance(dims, list) and - isinstance(dims[0], (int, np.integer)) - ) - - -def is_vectorized_oper(dims): - return ( - isinstance(dims, list) and - isinstance(dims[0], list) - ) - - -def type_from_dims(dims, enforce_square=False): - bra_like, ket_like = map(is_scalar, dims) - - if bra_like: - if is_vector(dims[1]): - return 'bra' - elif is_vectorized_oper(dims[1]): - return 'operator-bra' - - if ket_like: - if is_vector(dims[0]): - return 'ket' - elif is_vectorized_oper(dims[0]): - return 'operator-ket' - - elif is_vector(dims[0]) and (dims[0] == dims[1] or not enforce_square): - return 'oper' - - elif ( - is_vectorized_oper(dims[0]) - and ( - not enforce_square - or (dims[0] == dims[1] and dims[0][0] == dims[1][0]) - ) - ): - return 'super' - return 'other' +__all__ = ["to_tensor_rep", "from_tensor_rep", "Space", "Dimensions"] def flatten(l): """Flattens a list of lists to the first level. - Given a list containing a mix of scalars and lists, - flattens down to a list of the scalars within the original - list. + Given a list containing a mix of scalars and lists or a dimension object, + flattens it down to a list of the scalars within the original list. + + Parameters + ---------- + l : scalar, list, Space, Dimension + Object to flatten. Examples -------- @@ -73,7 +31,14 @@ def flatten(l): >>> flatten([[[0], 1], 2]) # doctest: +SKIP [0, 1, 2] + Notes + ----- + Any scalar will be returned wrapped in a list: ``flaten(1) == [1]``. + A non-list iterable will not be treated as a list by flatten. For example, flatten would treat a tuple + as a scalar. """ + if isinstance(l, (Space, Dimensions)): + l = l.as_list() if not isinstance(l, list): return [l] else: @@ -194,10 +159,10 @@ def collapse_dims_oper(dims): def collapse_dims_super(dims): """ Given the dimensions specifications for an operator-ket-, operator-bra- or - super-type Qobj, returns a dimensions specification describing the same shape - by collapsing all composite systems. For instance, the super-type - dimensions specification ``[[[2, 3], [2, 3]], [[2, 3], [2, 3]]]`` collapses to - ``[[[6], [6]], [[6], [6]]]``. + super-type Qobj, returns a dimensions specification describing the same + shape by collapsing all composite systems. For instance, the super-type + dimensions specification ``[[[2, 3], [2, 3]], [[2, 3], [2, 3]]]`` collapses + to ``[[[6], [6]], [[6], [6]]]``. Parameters ---------- @@ -249,7 +214,7 @@ def dims_to_tensor_perm(dims): Parameters ---------- - dims : list + dims : list, Dimensions Dimensions specification for a Qobj. Returns @@ -260,35 +225,9 @@ def dims_to_tensor_perm(dims): index of the tensor ``data`` corresponding to the ``idx``th dimension of ``dims``. """ - # We figure out the type of the dims specification, - # relaxing the requirement that operators be square. - # This means that dims_type need not coincide with - # Qobj.type, but that works fine for our purposes here. - dims_type = type_from_dims(dims, enforce_square=False) - perm = enumerate_flat(dims) - if dims_type in ('oper', 'ket', 'bra'): - return flatten(perm) - - # If the type is other, we need to figure out if the - # dims is superlike on its outputs and inputs - # This is the case if the dims type for left or right - # are, respectively, oper-like. - if dims_type == 'other': - raise NotImplementedError("Not yet implemented for type='other'.") - - # If we're still here, the story is more complicated. We'll - # follow the strategy of creating a permutation by using - # enumerate_flat then transforming the result to swap - # input and output indices of vectorized matrices, then flattening - # the result. We'll then rebuild indices using this permutation. - if dims_type in ('operator-ket', 'super'): - # Swap the input and output spaces of the right part of - # perm. - perm[1] = list(reversed(perm[1])) - if dims_type in ('operator-bra', 'super'): - # Ditto, but for the left indices. - perm[0] = list(reversed(perm[0])) - return flatten(perm) + if isinstance(dims, list): + dims = Dimensions(dims) + return dims._get_tensor_perm() def dims_to_tensor_shape(dims): @@ -300,7 +239,7 @@ def dims_to_tensor_shape(dims): Parameters ---------- - dims : list + dims : list, Dimensions Dimensions specification for a Qobj. Returns @@ -325,7 +264,7 @@ def dims_idxs_to_tensor_idxs(dims, indices): Parameters ---------- - dims : list + dims : list, Dimensions Dimensions specification for a Qobj. indices : int, list or tuple @@ -343,3 +282,673 @@ def dims_idxs_to_tensor_idxs(dims, indices): """ perm = dims_to_tensor_perm(dims) return deep_map(partial(getitem, perm), indices) + + +def to_tensor_rep(q_oper): + """ + Transform a ``Qobj`` to a numpy array whose shape is the flattened + dimensions. + + Parameters + ---------- + q_oper: Qobj + Object to reshape + + Returns + ------- + ndarray: + Numpy array with one dimension for each index in dims. + + Examples + -------- + >>> ket.dims + [[2, 3], [1]] + >>> to_tensor_rep(ket).shape + (2, 3, 1) + + >>> oper.dims + [[2, 3], [2, 3]] + >>> to_tensor_rep(oper).shape + (2, 3, 2, 3) + + >>> super_oper.dims + [[[2, 3], [2, 3]], [[2, 3], [2, 3]]] + >>> to_tensor_rep(super_oper).shape + (2, 3, 2, 3, 2, 3, 2, 3) + """ + dims = q_oper._dims + data = q_oper.full().reshape(dims._get_tensor_shape()) + return data.transpose(dims._get_tensor_perm()) + + +def from_tensor_rep(tensorrep, dims): + """ + Reverse operator of :func:`to_tensor_rep`. + Create a Qobj From a N-dimensions numpy array and dimensions with N + indices. + + Parameters + ---------- + tensorrep: ndarray + Numpy array with one dimension for each index in dims. + + dims: list of list, Dimensions + Dimensions of the Qobj. + + Returns + ------- + Qobj + Re constructed Qobj + """ + from . import Qobj + dims = Dimensions(dims) + data = tensorrep.transpose(np.argsort(dims._get_tensor_perm())) + return Qobj(data.reshape(dims.shape), dims=dims) + + +def _frozen(*args, **kwargs): + raise RuntimeError("Dimension cannot be modified.") + + +class MetaSpace(type): + def __call__(cls, *args, rep=None): + """ + Select which subclass is instantiated. + """ + if cls is Space and len(args) == 1 and isinstance(args[0], list): + # From a list of int. + return cls.from_list(*args, rep=rep) + elif len(args) == 1 and isinstance(args[0], Space): + # Already a Space + return args[0] + + if cls is Space: + if len(args) == 0: + # Empty space: a Field. + cls = Field + elif len(args) == 1 and args[0] == 1: + # Space(1): a Field. + cls = Field + elif len(args) == 1 and isinstance(args[0], Dimensions): + # Making a Space out of a Dimensions object: Super Operator. + cls = SuperSpace + elif len(args) > 1 and all(isinstance(arg, Space) for arg in args): + # list of space: tensor product space. + cls = Compound + + if settings.core['auto_tidyup_dims']: + if cls is Compound and all(isinstance(arg, Field) for arg in args): + cls = Field + if cls is SuperSpace and args[0].type == "scalar": + cls = Field + + args = tuple([ + tuple(arg) if isinstance(arg, list) else arg + for arg in args + ]) + + if cls is Field: + return cls.field_instance + + if cls is SuperSpace: + args = (*args, rep or 'super') + + if args not in cls._stored_dims: + instance = cls.__new__(cls) + instance.__init__(*args) + cls._stored_dims[args] = instance + return cls._stored_dims[args] + + def from_list(cls, list_dims, rep=None): + if len(list_dims) == 0: + raise ValueError("Empty list can't be used as dims.") + elif ( + sum(isinstance(entry, list) for entry in list_dims) + not in [0, len(list_dims)] + ): + raise ValueError(f"Format dims not understood {list_dims}.") + elif not isinstance(list_dims[0], list): + # Tensor + spaces = [Space(size) for size in list_dims] + elif len(list_dims) == 1: + # [[2, 3]]: tensor with an extra layer of list. + spaces = [Space(size) for size in list_dims[0]] + elif len(list_dims) % 2 == 0: + # Superoperators or tensor of Superoperators + spaces = [ + Space(Dimensions( + Space(list_dims[i+1]), + Space(list_dims[i]) + ), rep=rep) + for i in range(0, len(list_dims), 2) + ] + else: + raise ValueError(f'Format not understood {list_dims}') + + if len(spaces) == 1: + return spaces[0] + elif len(spaces) >= 2: + return Space(*spaces) + else: + raise ValueError(f'Format not understood {list_dims}') + + +class Space(metaclass=MetaSpace): + _stored_dims = {} + + def __init__(self, dims): + idims = int(dims) + if idims <= 0 or idims != dims: + raise ValueError("Dimensions must be integers > 0") + # Size of the hilbert space + self.size = dims + self.issuper = False + # Super representation, should be an empty string except for SuperSpace + self.superrep = None + # Does the size and dims match directly: size == prod(dims) + self._pure_dims = True + self.__setitem__ = _frozen + + def __eq__(self, other): + return self is other or ( + type(other) is type(self) + and other.size == self.size + ) + + def __hash__(self): + return hash(self.size) + + def __repr__(self): + return f"Space({self.size})" + + def as_list(self): + return [self.size] + + def __str__(self): + return str(self.as_list()) + + def dims2idx(self, dims): + """ + Transform dimensions indices to full array indices. + """ + if not isinstance(dims, list) or len(dims) != 1: + raise ValueError("Dimensions must be a list of one element") + if not (0 <= dims[0] < self.size): + raise IndexError("Dimensions out of range") + if not isinstance(dims[0], numbers.Integral): + raise TypeError("Dimensions must be integers") + return dims[0] + + def idx2dims(self, idx): + """ + Transform full array indices to dimensions indices. + """ + if not (0 <= idx < self.size): + raise IndexError("Index out of range") + return [idx] + + def step(self): + """ + Get the step in the array between for each dimensions index. + + If element ``[i, j, k]`` is ``ket.full()[m, 0]`` then element + ``[i, j+1, k]`` is ``ket.full()[m + ket._dims.step()[1], 0]``. + """ + return [1] + + def flat(self): + """ Dimensions as a flat list. """ + return [self.size] + + def remove(self, idx): + """ + Remove a Space from a Dimensons or complex Space. + + ``Space([2, 3, 4]).remove(1) == Space([2, 4])`` + """ + raise RuntimeError("Cannot delete a flat space.") + + def replace(self, idx, new): + """ + Reshape a Space from a Dimensons or complex Space. + + ``Space([2, 3, 4]).replace(1, 5) == Space([2, 5, 4])`` + """ + if idx != 0: + raise ValueError( + "Cannot replace a non-zero index in a flat space." + ) + return Space(new) + + def replace_superrep(self, super_rep): + return self + + def scalar_like(self): + return Field() + + +class Field(Space): + field_instance = None + + def __init__(self): + self.size = 1 + self.issuper = False + self.superrep = None + self._pure_dims = True + self.__setitem__ = _frozen + + def __eq__(self, other): + return type(other) is Field + + def __hash__(self): + return hash(0) + + def __repr__(self): + return "Field()" + + def as_list(self): + return [1] + + def step(self): + return [1] + + def flat(self): + return [1] + + def remove(self, idx): + return self + + def replace(self, idx, new): + return Space(new) + + +Field.field_instance = Field.__new__(Field) +Field.field_instance.__init__() + + +class Compound(Space): + _stored_dims = {} + + def __init__(self, *spaces): + self.spaces = [] + if len(spaces) <= 1: + raise ValueError("Compound need multiple space to join.") + for space in spaces: + if isinstance(space, Compound): + self.spaces += space.spaces + else: + self.spaces += [space] + self.spaces = tuple(self.spaces) + self.size = np.prod([space.size for space in self.spaces]) + self.issuper = all(space.issuper for space in self.spaces) + if not self.issuper and any(space.issuper for space in self.spaces): + raise TypeError( + "Cannot create compound space of super and non super." + ) + self._pure_dims = all(space._pure_dims for space in self.spaces) + superrep = [space.superrep for space in self.spaces] + if all(superrep[0] == rep for rep in superrep): + self.superrep = superrep[0] + else: + raise TypeError( + "Cannot create compound space of of super operators " + "with different representation." + ) + self.__setitem__ = _frozen + + def __eq__(self, other): + return self is other or ( + type(other) is type(self) and + self.spaces == other.spaces + ) + + def __hash__(self): + return hash(self.spaces) + + def __repr__(self): + parts_rep = ", ".join(repr(space) for space in self.spaces) + return f"Compound({parts_rep})" + + def as_list(self): + return sum([space.as_list() for space in self.spaces], []) + + def dims2idx(self, dims): + if len(dims) != len(self.spaces): + raise ValueError("Length of supplied dims does not match the number of subspaces.") + pos = 0 + step = 1 + for space, dim in zip(self.spaces[::-1], dims[::-1]): + pos += space.dims2idx([dim]) * step + step *= space.size + return pos + + def idx2dims(self, idx): + dims = [] + for space in self.spaces[::-1]: + idx, dim = divmod(idx, space.size) + dims = space.idx2dims(dim) + dims + return dims + + def step(self): + steps = [] + step = 1 + for space in self.spaces[::-1]: + steps = [step * N for N in space.step()] + steps + step *= space.size + return steps + + def flat(self): + return sum([space.flat() for space in self.spaces], []) + + def remove(self, idx): + new_spaces = [] + for space in self.spaces: + n_indices = len(space.flat()) + idx_space = [i for i in idx if i < n_indices] + idx = [i-n_indices for i in idx if i >= n_indices] + new_space = space.remove(idx_space) + if new_spaces: + return Compound(*new_spaces) + return Field() + + def replace(self, idx, new): + new_spaces = [] + for space in self.spaces: + n_indices = len(space.flat()) + if 0 <= idx < n_indices: + new_spaces.append(space.replace(idx, new)) + else: + new_spaces.append(space) + idx -= n_indices + return Compound(*new_spaces) + + def replace_superrep(self, super_rep): + return Compound( + *[space.replace_superrep(super_rep) for space in self.spaces] + ) + + def scalar_like(self): + return [space.scalar_like() for space in self.spaces] + + +class SuperSpace(Space): + _stored_dims = {} + + def __init__(self, oper, rep='super'): + self.oper = oper + self.superrep = rep + self.size = oper.shape[0] * oper.shape[1] + self.issuper = True + self._pure_dims = oper._pure_dims + self.__setitem__ = _frozen + + def __eq__(self, other): + return ( + self is other + or self.oper == other + or ( + type(other) is type(self) + and self.oper == other.oper + and self.superrep == other.superrep + ) + ) + + def __hash__(self): + return hash((self.oper, self.superrep)) + + def __repr__(self): + return f"Super({repr(self.oper)}, rep={self.superrep})" + + def as_list(self): + return self.oper.as_list() + + def dims2idx(self, dims): + posl, posr = self.oper.dims2idx(dims) + return posl + posr * self.oper.shape[0] + + def idx2dims(self, idx): + posl = idx % self.oper.shape[0] + posr = idx // self.oper.shape[0] + return self.oper.idx2dims(posl, posr) + + def step(self): + stepl, stepr = self.oper.step() + step = self.oper.shape[0] + return stepl + [step * N for N in stepr] + + def flat(self): + return sum(self.oper.flat(), []) + + def remove(self, idx): + new_dims = self.oper.remove(idx) + if new_dims.type == 'scalar': + return Field() + return SuperSpace(new_dims, rep=self.superrep) + + def replace(self, idx, new): + return SuperSpace(self.oper.replace(idx, new), rep=self.superrep) + + def replace_superrep(self, super_rep): + return SuperSpace(self.oper, rep=super_rep) + + def scalar_like(self): + return self.oper.scalar_like() + + +class MetaDims(type): + def __call__(cls, *args, rep=None): + if len(args) == 1 and isinstance(args[0], Dimensions): + return args[0] + elif len(args) == 1 and len(args[0]) == 2: + args = ( + Space(args[0][1], rep=rep), + Space(args[0][0], rep=rep) + ) + elif len(args) != 2: + raise NotImplementedError('No Dual, Ket, Bra...', args) + elif ( + settings.core["auto_tidyup_dims"] + and args[0] == args[1] == Field() + ): + return Field() + + if args not in cls._stored_dims: + instance = cls.__new__(cls) + instance.__init__(*args) + cls._stored_dims[args] = instance + return cls._stored_dims[args] + + +class Dimensions(metaclass=MetaDims): + _stored_dims = {} + _type = None + + def __init__(self, from_, to_): + self.from_ = from_ + self.to_ = to_ + self.shape = to_.size, from_.size + self.issuper = from_.issuper + self._pure_dims = from_._pure_dims and to_._pure_dims + self.issquare = False + if self.from_.size == 1 and self.to_.size == 1: + self.type = 'scalar' + self.issquare = True + self.superrep = None + elif self.from_.size == 1: + self.issuper = self.to_.issuper + self.type = 'operator-ket' if self.issuper else 'ket' + self.superrep = self.to_.superrep + elif self.to_.size == 1: + self.issuper = self.from_.issuper + self.type = 'operator-bra' if self.issuper else 'bra' + self.superrep = self.from_.superrep + elif self.from_ == self.to_: + self.issuper = self.from_.issuper + self.type = 'super' if self.issuper else 'oper' + self.superrep = self.from_.superrep + self.issquare = True + else: + if from_.issuper != to_.issuper: + raise NotImplementedError( + "Operator with both space and superspace dimensions are " + "not supported. Please open an issue if you have an use " + f"case for these: {from_}, {to_}]" + ) + self.type = 'super' if self.from_.issuper else 'oper' + if self.from_.superrep == self.to_.superrep: + self.superrep = self.from_.superrep + else: + self.superrep = 'mixed' + self.__setitem__ = _frozen + + def __eq__(self, other): + if isinstance(other, Dimensions): + return ( + self is other + or ( + self.to_ == other.to_ + and self.from_ == other.from_ + ) + ) + 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_)) + + def __repr__(self): + return f"Dimensions({repr(self.from_)}, {repr(self.to_)})" + + def __str__(self): + return str(self.as_list()) + + def as_list(self): + """ + Return the list representation of the Dimensions object. + """ + return [self.to_.as_list(), self.from_.as_list()] + + def __getitem__(self, key): + if key == 0: + return self.to_ + elif key == 1: + return self.from_ + raise IndexError("Dimensions index out of range") + + def dims2idx(self, dims): + """ + Transform dimensions indices to full array indices. + """ + return self.to_.dims2idx(dims[0]), self.from_.dims2idx(dims[1]) + + def idx2dims(self, idxl, idxr): + """ + Transform full array indices to dimensions indices. + """ + return [self.to_.idx2dims(idxl), self.from_.idx2dims(idxr)] + + def step(self): + """ + Get the step in the array between for each dimensions index. + + If element ``[i, j, k]`` is ``ket.full()[m, 0]`` then element + ``[i, j+1, k]`` is ``ket.full()[m + ket._dims.step()[1], 0]``. + """ + return [self.to_.step(), self.from_.step()] + + def flat(self): + """ Dimensions as a flat list. """ + return [self.to_.flat(), self.from_.flat()] + + def _get_tensor_shape(self): + """ + Get the shape to of the Nd tensor with one dimensions for each + Dimension index. The order of the space values are not in the order of + the Dimension index. + """ + # dims_to_tensor_shape + stepl = self.to_.step() + flatl = self.to_.flat() + stepr = self.from_.step() + flatr = self.from_.flat() + return tuple(np.concatenate([ + np.array(flatl)[np.argsort(stepl)[::-1]], + np.array(flatr)[np.argsort(stepr)[::-1]], + ])) + + def _get_tensor_perm(self): + """ + Get the permutation of a tensor created using ``_get_tensor_shape`` to + reorder the tensor dimensions with those of the Dimensions object. + """ + # dims_to_tensor_perm + stepl = self.to_.step() + stepr = self.from_.step() + return list(np.concatenate([ + np.argsort(stepl)[::-1], + np.argsort(stepr)[::-1] + len(stepl) + ])) + + def remove(self, idx): + """ + Remove a Space from a Dimensons or complex Space. + + ``Space([2, 3, 4]).remove(1) == Space([2, 4])`` + """ + if not isinstance(idx, list): + idx = [idx] + if not idx: + return self + idx = sorted(idx) + n_indices = len(self.to_.flat()) + idx_to = [i for i in idx if i < n_indices] + idx_from = [i-n_indices for i in idx if i >= n_indices] + return Dimensions( + self.from_.remove(idx_from), + self.to_.remove(idx_to), + ) + + def replace(self, idx, new): + """ + Reshape a Space from a Dimensons or complex Space. + + ``Space([2, 3, 4]).replace(1, 5) == Space([2, 5, 4])`` + """ + n_indices = len(self.to_.flat()) + if idx < n_indices: + new_to = self.to_.replace(idx, new) + new_from = self.from_ + else: + new_to = self.to_ + new_from = self.from_.replace(idx-n_indices, new) + + return Dimensions(new_from, new_to) + + def replace_superrep(self, super_rep): + if not self.issuper and super_rep is not None: + raise TypeError("Can't set a superrep of a non super object.") + return Dimensions( + self.from_.replace_superrep(super_rep), + self.to_.replace_superrep(super_rep) + ) + + def scalar_like(self): + return [self.to_.scalar_like(), self.from_.scalar_like()] diff --git a/qutip/core/energy_restricted.py b/qutip/core/energy_restricted.py new file mode 100644 index 0000000000..f790b09140 --- /dev/null +++ b/qutip/core/energy_restricted.py @@ -0,0 +1,286 @@ +from .dimensions import Space +from .states import state_number_enumerate +from . import data as _data +from . import Qobj, qdiags +import numpy as np +import scipy.sparse +from .. import settings + + +__all__ = ['enr_state_dictionaries', 'enr_fock', + 'enr_thermal_dm', 'enr_destroy', 'enr_identity'] + + +def enr_state_dictionaries(dims, excitations): + """ + Return the number of states, and lookup-dictionaries for translating + a state tuple to a state index, and vice versa, for a system with a given + number of components and maximum number of excitations. + + Parameters + ---------- + dims: list + A list with the number of states in each sub-system. + + excitations : integer + The maximum numbers of dimension + + Returns + ------- + nstates, state2idx, idx2state: integer, dict, dict + The number of states `nstates`, a dictionary for looking up state + indices from a state tuple, and a dictionary for looking up state + state tuples from state indices. state2idx and idx2state are reverses + of each other, i.e., ``state2idx[idx2state[idx]] = idx`` and + ``idx2state[state2idx[state]] = state``. + """ + nstates = 0 + state2idx = {} + idx2state = {} + + for state in state_number_enumerate(dims, excitations): + state2idx[state] = nstates + idx2state[nstates] = state + nstates += 1 + + return nstates, state2idx, idx2state + + +class EnrSpace(Space): + _stored_dims = {} + + def __init__(self, dims, excitations): + self.dims = tuple(dims) + self.n_excitations = excitations + enr_dicts = enr_state_dictionaries(dims, excitations) + self.size, self.state2idx, self.idx2state = enr_dicts + self.issuper = False + self.superrep = None + self._pure_dims = False + + def __eq__(self, other): + return ( + self is other + or ( + type(other) is type(self) + and self.dims == other.dims + and self.n_excitations == other.n_excitations + ) + ) + + def __hash__(self): + return hash((self.dims, self.n_excitations)) + + def __repr__(self): + return f"EnrSpace({self.dims}, {self.n_excitations})" + + def as_list(self): + return list(self.dims) + + def dims2idx(self, dims): + return self.state2idx[tuple(dims)] + + def idx2dims(self, idx): + return self.idx2state[idx] + + +def enr_fock(dims, excitations, state, *, dtype=None): + """ + Generate the Fock state representation in a excitation-number restricted + state space. The `dims` argument is a list of integers that define the + number of quantums states of each component of a composite quantum system, + and the `excitations` specifies the maximum number of excitations for + the basis states that are to be included in the state space. The `state` + argument is a tuple of integers that specifies the state (in the number + basis representation) for which to generate the Fock state representation. + + Parameters + ---------- + dims : list + A list of the dimensions of each subsystem of a composite quantum + system. + + excitations : integer + The maximum number of excitations that are to be included in the + state space. + + state : list of integers + The state in the number basis representation. + + dtype : type or str, optional + Storage representation. Any data-layer known to `qutip.data.to` is + accepted. + + Returns + ------- + ket : Qobj + A Qobj instance that represent a Fock state in the exication-number- + restricted state space defined by `dims` and `exciations`. + + """ + dtype = dtype or settings.core["default_dtype"] or _data.Dense + nstates, state2idx, _ = enr_state_dictionaries(dims, excitations) + try: + data = _data.one_element[dtype]( + (nstates, 1), + (state2idx[tuple(state)], 0), + 1 + ) + except KeyError: + msg = ( + "state tuple " + str(tuple(state)) + + " is not in the restricted state space." + ) + raise ValueError(msg) from None + return Qobj(data, dims=[EnrSpace(dims, excitations), [1]*len(dims)], + copy=False) + + +def enr_thermal_dm(dims, excitations, n, *, dtype=None): + """ + Generate the density operator for a thermal state in the excitation-number- + restricted state space defined by the `dims` and `exciations` arguments. + See the documentation for enr_fock for a more detailed description of + these arguments. The temperature of each mode in dims is specified by + the average number of excitatons `n`. + + Parameters + ---------- + dims : list + A list of the dimensions of each subsystem of a composite quantum + system. + + excitations : integer + The maximum number of excitations that are to be included in the + state space. + + n : integer + The average number of exciations in the thermal state. `n` can be + a float (which then applies to each mode), or a list/array of the same + length as dims, in which each element corresponds specifies the + temperature of the corresponding mode. + + dtype : type or str, optional + Storage representation. Any data-layer known to `qutip.data.to` is + accepted. + + Returns + ------- + dm : Qobj + Thermal state density matrix. + """ + dtype = dtype or settings.core["default_dtype"] or _data.CSR + nstates, _, idx2state = enr_state_dictionaries(dims, excitations) + enr_dims = [EnrSpace(dims, excitations)] * 2 + if not isinstance(n, (list, np.ndarray)): + n = np.ones(len(dims)) * n + else: + n = np.asarray(n) + + diags = [np.prod((n / (n + 1)) ** np.array(state)) + for idx, state in idx2state.items()] + diags /= np.sum(diags) + out = qdiags(diags, 0, dims=enr_dims, + shape=(nstates, nstates), dtype=dtype) + out._isherm = True + return out + + +def enr_destroy(dims, excitations, *, dtype=None): + """ + Generate annilation operators for modes in a excitation-number-restricted + state space. For example, consider a system consisting of 4 modes, each + with 5 states. The total hilbert space size is 5**4 = 625. If we are + only interested in states that contain up to 2 excitations, we only need + to include states such as + + (0, 0, 0, 0) + (0, 0, 0, 1) + (0, 0, 0, 2) + (0, 0, 1, 0) + (0, 0, 1, 1) + (0, 0, 2, 0) + ... + + This function creates annihilation operators for the 4 modes that act + within this state space: + + a1, a2, a3, a4 = enr_destroy([5, 5, 5, 5], excitations=2) + + From this point onwards, the annihiltion operators a1, ..., a4 can be + used to setup a Hamiltonian, collapse operators and expectation-value + operators, etc., following the usual pattern. + + Parameters + ---------- + dims : list + A list of the dimensions of each subsystem of a composite quantum + system. + + excitations : integer + The maximum number of excitations that are to be included in the + state space. + + dtype : type or str, optional + Storage representation. Any data-layer known to `qutip.data.to` is + accepted. + + Returns + ------- + a_ops : list of qobj + A list of annihilation operators for each mode in the composite + quantum system described by dims. + """ + dtype = dtype or settings.core["default_dtype"] or _data.CSR + nstates, state2idx, idx2state = enr_state_dictionaries(dims, excitations) + enr_dims = [EnrSpace(dims, excitations)] * 2 + + a_ops = [scipy.sparse.lil_matrix((nstates, nstates), dtype=np.complex128) + for _ in dims] + + for n1, state1 in idx2state.items(): + for idx, s in enumerate(state1): + # if s > 0, the annihilation operator of mode idx has a non-zero + # entry with one less excitation in mode idx in the final state + if s > 0: + state2 = state1[:idx] + (s-1,) + state1[idx+1:] + n2 = state2idx[state2] + a_ops[idx][n2, n1] = np.sqrt(s) + + return [Qobj(a, dims=enr_dims).to(dtype) for a in a_ops] + + +def enr_identity(dims, excitations, *, dtype=None): + """ + Generate the identity operator for the excitation-number restricted + state space defined by the `dims` and `exciations` arguments. See the + docstring for enr_fock for a more detailed description of these arguments. + + Parameters + ---------- + dims : list + A list of the dimensions of each subsystem of a composite quantum + system. + + excitations : integer + The maximum number of excitations that are to be included in the + state space. + + dtype : type or str, optional + Storage representation. Any data-layer known to `qutip.data.to` is + accepted. + + Returns + ------- + op : Qobj + A Qobj instance that represent the identity operator in the + exication-number-restricted state space defined by `dims` and + `exciations`. + """ + dtype = dtype or settings.core["default_dtype"] or _data.Dia + dims = EnrSpace(dims, excitations) + return Qobj(_data.identity[dtype](dims.size), + dims=[dims, dims], + isherm=True, + isunitary=True, + copy=False) diff --git a/qutip/core/expect.py b/qutip/core/expect.py index 65888f8c7e..bb72fccd95 100644 --- a/qutip/core/expect.py +++ b/qutip/core/expect.py @@ -9,13 +9,14 @@ def expect(oper, state): """ Calculate the expectation value for operator(s) and state(s). The - expectation of state `k` on operator `A` is defined as `k.dag() @ A @ k`, - and for density matrix `R` on operator `A` it is `trace(A @ R)`. + expectation of state ``k`` on operator ``A`` is defined as + ``k.dag() @ A @ k``, and for density matrix ``R`` on operator ``A`` it is + ``trace(A @ R)``. Parameters ---------- oper : qobj/array-like - A single or a `list` or operators for expectation value. + A single or a `list` of operators for expectation value. state : qobj/array-like A single or a `list` of quantum states or density matrices. @@ -23,9 +24,9 @@ def expect(oper, state): Returns ------- expt : float/complex/array-like - Expectation value. ``real`` if `oper` is Hermitian, ``complex`` - otherwise. A (nested) array of expectaction values of state or operator - are arrays. + Expectation value. ``real`` if ``oper`` is Hermitian, ``complex`` + otherwise. A (nested) array of expectaction values if ``state`` or + ``oper`` are arrays. Examples -------- @@ -86,7 +87,7 @@ def variance(oper, state): Operator for expectation value. state : qobj/list - A single or `list` of quantum states or density matrices.. + A single or ``list`` of quantum states or density matrices.. Returns ------- diff --git a/qutip/core/gates.py b/qutip/core/gates.py index 5b3d988d10..ee97c819f6 100644 --- a/qutip/core/gates.py +++ b/qutip/core/gates.py @@ -49,7 +49,7 @@ def cy_gate(*, dtype="csr"): Returns ------- - result : :class:`qutip.Qobj` + result : :class:`.Qobj` Quantum object for operator describing the rotation. """ return Qobj( @@ -69,7 +69,7 @@ def cz_gate(*, dtype="csr"): Returns ------- - result : :class:`qutip.Qobj` + result : :class:`.Qobj` Quantum object for operator describing the rotation. """ return qdiags([1, 1, 1, -1], dims=[[2, 2], [2, 2]], dtype=dtype) @@ -86,7 +86,7 @@ def s_gate(*, dtype="csr"): Returns ------- - result : :class:`qutip.Qobj` + result : :class:`.Qobj` Quantum object for operator describing a 90 degree rotation around the z-axis. @@ -105,7 +105,7 @@ def cs_gate(*, dtype="csr"): Returns ------- - result : :class:`qutip.Qobj` + result : :class:`.Qobj` Quantum object for operator describing the rotation. """ @@ -123,7 +123,7 @@ def t_gate(*, dtype="csr"): Returns ------- - result : :class:`qutip.Qobj` + result : :class:`.Qobj` Quantum object for operator describing a phase shift of pi/4. """ @@ -141,7 +141,7 @@ def ct_gate(*, dtype="csr"): Returns ------- - result : :class:`qutip.Qobj` + result : :class:`.Qobj` Quantum object for operator describing the rotation. """ @@ -289,7 +289,7 @@ def qrot(theta, phi, *, dtype="dense"): Returns ------- - qrot_gate : :class:`qutip.Qobj` + qrot_gate : :class:`.Qobj` Quantum object representation of physical qubit rotation under a rabi pulse. """ @@ -537,7 +537,7 @@ def molmer_sorensen(theta, *, dtype="dense"): Returns ------- - molmer_sorensen_gate: :class:`qutip.Qobj` + molmer_sorensen_gate: :class:`.Qobj` Quantum object representation of the Mølmer–Sørensen gate. """ return Qobj( diff --git a/qutip/core/metrics.py b/qutip/core/metrics.py index acd43eba49..0df7b19f36 100644 --- a/qutip/core/metrics.py +++ b/qutip/core/metrics.py @@ -9,13 +9,10 @@ 'hellinger_dist', 'hilbert_dist', 'average_gate_fidelity', 'process_fidelity', 'unitarity', 'dnorm'] -import warnings - import numpy as np from scipy import linalg as la import scipy.sparse as sp -from .superop_reps import (to_kraus, to_choi, _to_superpauli, to_super, - kraus_to_choi) +from .superop_reps import to_choi, _to_superpauli, to_super, kraus_to_choi from .superoperator import operator_to_vector, vector_to_operator from .operators import qeye, qeye_like from .states import ket2dm @@ -31,7 +28,13 @@ def fidelity(A, B): """ Calculates the fidelity (pseudo-metric) between two density matrices. - See: Nielsen & Chuang, "Quantum Computation and Quantum Information" + + Notes + ----- + Uses the definition from Nielsen & Chuang, "Quantum Computation and Quantum + Information". It is the square root of the fidelity defined in + R. Jozsa, Journal of Modern Optics, 41:12, 2315 (1994), used in + :func:`qutip.core.metrics.process_fidelity`. Parameters ---------- @@ -115,7 +118,7 @@ def _process_fidelity_to_id(oper): to the identity quantum channel. Parameters ---------- - oper : :class:`qutip.Qobj`/list + oper : :class:`.Qobj`/list A unitary operator, or a superoperator in supermatrix, Choi or chi-matrix form, or a list of Kraus operators Returns @@ -154,10 +157,10 @@ def process_fidelity(oper, target=None): Parameters ---------- - oper : :class:`qutip.Qobj`/list + oper : :class:`.Qobj`/list A unitary operator, or a superoperator in supermatrix, Choi or chi-matrix form, or a list of Kraus operators - target : :class:`qutip.Qobj`/list + target : :class:`.Qobj`/list, optional A unitary operator, or a superoperator in supermatrix, Choi or chi-matrix form, or a list of Kraus operators @@ -171,8 +174,8 @@ def process_fidelity(oper, target=None): Since Qutip 5.0, this function computes the process fidelity as defined for example in: A. Gilchrist, N.K. Langford, M.A. Nielsen, Phys. Rev. A 71, 062310 (2005). Previously, it computed a function - that is now implemented in - :func:`control.fidcomp.FidCompUnitary.get_fidelity`. + that is now implemented as ``get_fidelity`` in qutip-qtrl. + The definition of state fidelity that the process fidelity is based on is the one from R. Jozsa, Journal of Modern Optics, 41:12, 2315 (1994). It is the square of the one implemented in @@ -196,7 +199,7 @@ def process_fidelity(oper, target=None): if isinstance(oper, list): # oper is a list of Kraus operators return _process_fidelity_to_id([k * target.dag() for k in oper]) elif oper.type == 'oper': - return _process_fidelity_to_id(oper*target.dag()) + return _process_fidelity_to_id(oper * target.dag()) elif oper.type == 'super': oper_super = to_super(oper) target_dag_super = to_super(target.dag()) @@ -217,10 +220,10 @@ def average_gate_fidelity(oper, target=None): Parameters ---------- - oper : :class:`qutip.Qobj`/list + oper : :class:`.Qobj`/list A unitary operator, or a superoperator in supermatrix, Choi or chi-matrix form, or a list of Kraus operators - target : :class:`qutip.Qobj` + target : :class:`.Qobj` A unitary operator Returns @@ -260,9 +263,9 @@ def tracedist(A, B, sparse=False, tol=0): Density matrix or state vector. B : qobj Density matrix or state vector with same dimensions as A. - tol : float - Tolerance used by sparse eigensolver, if used. (0=Machine precision) - sparse : {False, True} + tol : float, default: 0 + Tolerance used by sparse eigensolver, if used. (0 = Machine precision) + sparse : bool, default: False Use sparse eigensolver. Returns @@ -379,7 +382,8 @@ def hellinger_dist(A, B, sparse=False, tol=0): Calculates the quantum Hellinger distance between two density matrices. Formula: - hellinger_dist(A, B) = sqrt(2-2*Tr(sqrt(A)*sqrt(B))) + + ``hellinger_dist(A, B) = sqrt(2 - 2 * tr(sqrt(A) * sqrt(B)))`` See: D. Spehner, F. Illuminati, M. Orszag, and W. Roga, "Geometric measures of quantum correlations with Bures and Hellinger distances" @@ -387,13 +391,13 @@ def hellinger_dist(A, B, sparse=False, tol=0): Parameters ---------- - A : :class:`qutip.Qobj` + A : :class:`.Qobj` Density matrix or state vector. - B : :class:`qutip.Qobj` + B : :class:`.Qobj` Density matrix or state vector with same dimensions as A. - tol : float - Tolerance used by sparse eigensolver, if used. (0=Machine precision) - sparse : {False, True} + tol : float, default: 0 + Tolerance used by sparse eigensolver, if used. (0 = Machine precision) + sparse : bool, default: False Use sparse eigensolver. Returns @@ -403,9 +407,10 @@ def hellinger_dist(A, B, sparse=False, tol=0): Examples -------- - >>> x=fock_dm(5,3) - >>> y=coherent_dm(5,1) - >>> np.testing.assert_almost_equal(hellinger_dist(x,y), 1.3725145002591095) + >>> x = fock_dm(5,3) + >>> y = coherent_dm(5,1) + >>> np.allclose(hellinger_dist(x, y), 1.3725145002591095) + True """ if A.isket or A.isbra: sqrtmA = ket2dm(A) @@ -433,7 +438,7 @@ def dnorm(A, B=None, solver="CVXOPT", verbose=False, force_solve=False, Calculates the diamond norm of the quantum map q_oper, using the simplified semidefinite program of [Wat13]_. - The diamond norm SDP is solved by using CVXPY_. + The diamond norm SDP is solved by using `CVXPY `_. Parameters ---------- @@ -441,15 +446,15 @@ def dnorm(A, B=None, solver="CVXOPT", verbose=False, force_solve=False, Quantum map to take the diamond norm of. B : Qobj or None If provided, the diamond norm of :math:`A - B` is taken instead. - solver : str - Solver to use with CVXPY. One of "CVXOPT" (default) or "SCS". The - latter tends to be significantly faster, but somewhat less accurate. - verbose : bool + solver : str {"CVXOPT", "SCS"}, default: "CVXOPT" + Solver to use with CVXPY. "SCS" tends to be significantly faster, but + somewhat less accurate. + verbose : bool, default: False If True, prints additional information about the solution. - force_solve : bool + force_solve : bool, default: False If True, forces dnorm to solve the associated SDP, even if a special case is known for the argument. - sparse : bool + sparse : bool, default: True Whether to use sparse matrices in the convex optimisation problem. Default True. @@ -463,7 +468,6 @@ def dnorm(A, B=None, solver="CVXOPT", verbose=False, force_solve=False, ImportError If CVXPY cannot be imported. - .. _cvxpy: https://www.cvxpy.org/en/latest/ """ if cvxpy is None: # pragma: no cover raise ImportError("dnorm() requires CVXPY to be installed.") diff --git a/qutip/core/operators.py b/qutip/core/operators.py index f51f1de878..c89cde6104 100644 --- a/qutip/core/operators.py +++ b/qutip/core/operators.py @@ -3,13 +3,14 @@ of commonly occuring quantum operators. """ -__all__ = ['jmat', 'spin_Jx', 'spin_Jy', 'spin_Jz', 'spin_Jm', 'spin_Jp', - 'spin_J_set', 'sigmap', 'sigmam', 'sigmax', 'sigmay', 'sigmaz', - 'destroy', 'create', 'fdestroy', 'fcreate', 'qeye', 'qeye_like', - 'identity', 'position', 'momentum', 'num', 'squeeze', 'squeezing', - 'swap', 'displace', 'commutator', 'qutrit_ops', 'qdiags', 'phase', - 'qzero', 'qzero_like', 'enr_destroy', 'enr_identity', 'charge', - 'tunneling', 'qft'] +__all__ = [ + 'jmat', 'spin_Jx', 'spin_Jy', 'spin_Jz', 'spin_Jm', 'spin_Jp', + 'spin_J_set', 'sigmap', 'sigmam', 'sigmax', 'sigmay', 'sigmaz', + 'destroy', 'create', 'fdestroy', 'fcreate', 'qeye', 'identity', + 'position', 'momentum', 'num', 'squeeze', 'squeezing', 'displace', + 'commutator', 'qutrit_ops', 'qdiags', 'phase', 'qzero', 'charge', + 'tunneling', 'qft', 'qzero_like', 'qeye_like', 'swap', +] import numbers @@ -18,7 +19,7 @@ from . import data as _data from .qobj import Qobj -from .dimensions import flatten +from .dimensions import flatten, Space from .. import settings @@ -45,8 +46,8 @@ def qdiags(diagonals, offsets=None, dims=None, shape=None, *, Shape of operator. If omitted, a square operator large enough to contain the diagonals is generated. - dtype : type or str - Storage representation. Any data-layer known to `qutip.data.to` is + dtype : type or str, optional + Storage representation. Any data-layer known to ``qutip.data.to`` is accepted. Examples @@ -64,7 +65,7 @@ def qdiags(diagonals, offsets=None, dims=None, shape=None, *, dtype = dtype or settings.core["default_dtype"] or _data.Dia offsets = [0] if offsets is None else offsets data = _data.diag[dtype](diagonals, offsets, shape) - return Qobj(data, dims=dims, type='oper', copy=False) + return Qobj(data, dims=dims, copy=False) def jmat(j, which=None, *, dtype=None): @@ -75,12 +76,12 @@ def jmat(j, which=None, *, dtype=None): j : float Spin of operator - which : str + which : str, optional Which operator to return 'x','y','z','+','-'. - If no args given, then output is ['x','y','z'] + If not given, then output is ['x','y','z'] - dtype : type or str - Storage representation. Any data-layer known to `qutip.data.to` is + dtype : type or str, optional + Storage representation. Any data-layer known to ``qutip.data.to`` is accepted. Returns @@ -110,11 +111,6 @@ def jmat(j, which=None, *, dtype=None): [[ 1. 0. 0.] [ 0. 0. 0.] [ 0. 0. -1.]]] - - Notes - ----- - If no 'args' input, then returns array of ['x','y','z'] operators. - """ dtype = dtype or settings.core["default_dtype"] or _data.CSR if int(2 * j) != 2 * j or j < 0: @@ -129,21 +125,21 @@ def jmat(j, which=None, *, dtype=None): dims = [[int(2*j + 1)]]*2 if which == '+': - return Qobj(_jplus(j, dtype=dtype), dims=dims, type='oper', + return Qobj(_jplus(j, dtype=dtype), dims=dims, isherm=False, isunitary=False, copy=False) if which == '-': - return Qobj(_jplus(j, dtype=dtype).adjoint(), dims=dims, type='oper', + return Qobj(_jplus(j, dtype=dtype).adjoint(), dims=dims, isherm=False, isunitary=False, copy=False) if which == 'x': A = _jplus(j, dtype=dtype) - return Qobj(_data.add(A, A.adjoint()), dims=dims, type='oper', + return Qobj(_data.add(A, A.adjoint()), dims=dims, isherm=True, isunitary=False, copy=False) * 0.5 if which == 'y': A = _data.mul(_jplus(j, dtype=dtype), -0.5j) - return Qobj(_data.add(A, A.adjoint()), dims=dims, type='oper', + return Qobj(_data.add(A, A.adjoint()), dims=dims, isherm=True, isunitary=False, copy=False) if which == 'z': - return Qobj(_jz(j, dtype=dtype), dims=dims, type='oper', + return Qobj(_jz(j, dtype=dtype), dims=dims, isherm=True, isunitary=False, copy=False) raise ValueError('Invalid spin operator: ' + which) @@ -179,8 +175,8 @@ def spin_Jx(j, *, dtype=None): j : float Spin of operator - dtype : type or str - Storage representation. Any data-layer known to `qutip.data.to` is + dtype : type or str, optional + Storage representation. Any data-layer known to ``qutip.data.to`` is accepted. Returns @@ -200,8 +196,8 @@ def spin_Jy(j, *, dtype=None): j : float Spin of operator - dtype : type or str - Storage representation. Any data-layer known to `qutip.data.to` is + dtype : type or str, optional + Storage representation. Any data-layer known to ``qutip.data.to`` is accepted. Returns @@ -221,8 +217,8 @@ def spin_Jz(j, *, dtype=None): j : float Spin of operator - dtype : type or str - Storage representation. Any data-layer known to `qutip.data.to` is + dtype : type or str, optional + Storage representation. Any data-layer known to ``qutip.data.to`` is accepted. Returns @@ -242,8 +238,8 @@ def spin_Jm(j, *, dtype=None): j : float Spin of operator - dtype : type or str - Storage representation. Any data-layer known to `qutip.data.to` is + dtype : type or str, optional + Storage representation. Any data-layer known to ``qutip.data.to`` is accepted. Returns @@ -263,8 +259,8 @@ def spin_Jp(j, *, dtype=None): j : float Spin of operator - dtype : type or str - Storage representation. Any data-layer known to `qutip.data.to` is + dtype : type or str, optional + Storage representation. Any data-layer known to ``qutip.data.to`` is accepted. Returns @@ -284,8 +280,8 @@ def spin_J_set(j, *, dtype=None): j : float Spin of operators - dtype : type or str - Storage representation. Any data-layer known to `qutip.data.to` is + dtype : type or str, optional + Storage representation. Any data-layer known to ``qutip.data.to`` is accepted. Returns @@ -396,14 +392,14 @@ def destroy(N, offset=0, *, dtype=None): Parameters ---------- N : int - Dimension of Hilbert space. + Number of basis states in the Hilbert space. - offset : int (default 0) + offset : int, default: 0 The lowest number state that is included in the finite number state representation of the operator. - dtype : type or str - Storage representation. Any data-layer known to `qutip.data.to` is + dtype : type or str, optional + Storage representation. Any data-layer known to ``qutip.data.to`` is accepted. Returns @@ -435,14 +431,14 @@ def create(N, offset=0, *, dtype=None): Parameters ---------- N : int - Dimension of Hilbert space. + Number of basis states in the Hilbert space. - offset : int (default 0) + offset : int, default: 0 The lowest number state that is included in the finite number state representation of the operator. - dtype : type or str - Storage representation. Any data-layer known to `qutip.data.to` is + dtype : type or str, optional + Storage representation. Any data-layer known to ``qutip.data.to`` is accepted. Returns @@ -450,10 +446,6 @@ def create(N, offset=0, *, dtype=None): oper : qobj Qobj for raising operator. - offset : int (default 0) - The lowest number state that is included in the finite number state - representation of the operator. - Examples -------- >>> create(4) # doctest: +SKIP @@ -489,10 +481,14 @@ def fdestroy(n_sites, site, dtype=None): n_sites : int Number of sites in Fock space. - site : int (default 0) + site : int, default: 0 The site in Fock space to add a fermion to. Corresponds to j in the above JW transform. + dtype : type or str, optional + Storage representation. Any data-layer known to ``qutip.data.to`` is + accepted. + Returns ------- oper : qobj @@ -522,7 +518,7 @@ def fcreate(n_sites, site, dtype=None): .. math:: a_j = \\sigma_z^{\\otimes j} - \\otimes (frac{sigma_x - i sigma_y}{2}) + \\otimes (\\frac{\\sigma_x - i \\sigma_y}{2}) \\otimes I^{\\otimes N-j-1} @@ -535,6 +531,10 @@ def fcreate(n_sites, site, dtype=None): The site in Fock space to add a fermion to. Corresponds to j in the above JW transform. + dtype : type or str, optional + Storage representation. Any data-layer known to ``qutip.data.to`` is + accepted. + Returns ------- oper : qobj @@ -579,11 +579,16 @@ def _f_op(n_sites, site, action, dtype=None): The site in Fock space to create/destroy a fermion on. Corresponds to j in the above JW transform. + dtype : type or str, optional + Storage representation. Any data-layer known to ``qutip.data.to`` is + accepted. + Returns ------- 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) @@ -610,52 +615,27 @@ 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): - """ - Total flattened size and operator dimensions for operator creation routines - that automatically perform tensor products. - - Parameters - ---------- - dimensions : (int) or (list of int) or (list of list of int) - First dimension of an operator which can create an implicit tensor - product. If the type is `int`, it is promoted first to `[dimensions]`. - From there, it should be one of the two-elements `dims` parameter of a - `qutip.Qobj` representing an `oper` or `super`, with possible tensor - products. - - Returns - ------- - size : int - Dimension of backing matrix required to represent operator. - dimensions : list - Dimension list in the form required by ``Qobj`` creation. - """ - if not isinstance(dimensions, list): - dimensions = [dimensions] - flat = flatten(dimensions) - if not all(isinstance(x, numbers.Integral) and x >= 0 for x in flat): - raise ValueError("All dimensions must be integers >= 0") - return np.prod(flat), [dimensions, dimensions] - - -def qzero(dimensions, *, dtype=None): +def qzero(dimensions, dims_right=None, *, dtype=None): """ Zero operator. Parameters ---------- - dimensions : (int) or (list of int) or (list of list of int) - Dimension of Hilbert space. If provided as a list of ints, then the - dimension is the product over this list, but the ``dims`` property of - the new Qobj are set to this list. This can produce either `oper` or - `super` depending on the passed `dimensions`. - - dtype : type or str - Storage representation. Any data-layer known to `qutip.data.to` is + dimensions : int, list of int, list of list of int, Space + Number of basis states in the Hilbert space. If provided as a list of + ints, then the dimension is the product over this list, but the + ``dims`` property of the new Qobj are set to this list. This can + produce either `oper` or `super` depending on the passed `dimensions`. + + dims_right : int, list of int, list of list of int, Space, optional + Number of basis states in the right Hilbert space when the operator is + rectangular. + + dtype : type or str, optional + Storage representation. Any data-layer known to ``qutip.data.to`` is accepted. Returns @@ -665,10 +645,17 @@ def qzero(dimensions, *, dtype=None): """ dtype = dtype or settings.core["default_dtype"] or _data.CSR - size, dimensions = _implicit_tensor_dimensions(dimensions) + dims_left = Space(dimensions) + size_left = dims_left.size + if dims_right is None: + dims_right = dims_left + size_right = size_left + else: + dims_right = Space(dims_right) + size_right = dims_right.size + dims = [dims_left, dims_right] # A sparse matrix with no data is equal to a zero matrix. - type_ = 'super' if isinstance(dimensions[0][0], list) else 'oper' - return Qobj(_data.zeros[dtype](size, size), dims=dimensions, type=type_, + return Qobj(_data.zeros[dtype](size_left, size_right), dims=dims, isherm=True, isunitary=False, copy=False) @@ -687,12 +674,10 @@ 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, type=qobj.type, - superrep=qobj.superrep, isherm=True, isunitary=False, copy=False + _data.zeros[qobj.dtype](*qobj.shape), dims=qobj._dims, + isherm=True, isunitary=False, copy=False ) @@ -702,14 +687,14 @@ def qeye(dimensions, *, dtype=None): Parameters ---------- - dimensions : (int) or (list of int) or (list of list of int) - Dimension of Hilbert space. If provided as a list of ints, then the - dimension is the product over this list, but the ``dims`` property of - the new Qobj are set to this list. This can produce either `oper` or - `super` depending on the passed `dimensions`. - - dtype : type or str - Storage representation. Any data-layer known to `qutip.data.to` is + dimensions : (int) or (list of int) or (list of list of int), Space + Number of basis states in the Hilbert space. If provided as a list of + ints, then the dimension is the product over this list, but the + ``dims`` property of the new Qobj are set to this list. This can + produce either `oper` or `super` depending on the passed `dimensions`. + + dtype : type or str, optional + Storage representation. Any data-layer known to ``qutip.data.to`` is accepted. Returns @@ -737,9 +722,8 @@ def qeye(dimensions, *, dtype=None): """ dtype = dtype or settings.core["default_dtype"] or _data.Dia - size, dimensions = _implicit_tensor_dimensions(dimensions) - type_ = 'super' if isinstance(dimensions[0][0], list) else 'oper' - return Qobj(_data.identity[dtype](size), dims=dimensions, type=type_, + dimensions = Space(dimensions) + return Qobj(_data.identity[dtype](dimensions.size), dims=[dimensions]*2, isherm=True, isunitary=True, copy=False) @@ -763,30 +747,31 @@ 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( + "Can't create an identity matrix like a non square matrix." + ) return Qobj( - _data.identity_like(qobj.data), dims=qobj.dims, type=qobj.type, - superrep=qobj.superrep, isherm=True, isunitary=True, copy=False + _data.identity[qobj.dtype](qobj.shape[0]), dims=qobj._dims, + isherm=True, isunitary=True, copy=False ) def position(N, offset=0, *, dtype=None): """ - Position operator x=1/sqrt(2)*(a+a.dag()) + Position operator :math:`x = 1 / sqrt(2) * (a + a.dag())` Parameters ---------- N : int - Number of Fock states in Hilbert space. + Number of basis states in the Hilbert space. - offset : int (default 0) + offset : int, default: 0 The lowest number state that is included in the finite number state representation of the operator. - dtype : type or str - Storage representation. Any data-layer known to `qutip.data.to` is + dtype : type or str, optional + Storage representation. Any data-layer known to ``qutip.data.to`` is accepted. Returns @@ -794,10 +779,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): @@ -807,14 +793,14 @@ def momentum(N, offset=0, *, dtype=None): Parameters ---------- N : int - Number of Fock states in Hilbert space. + Number of basis states in the Hilbert space. - offset : int (default 0) + offset : int, default: 0 The lowest number state that is included in the finite number state representation of the operator. - dtype : type or str - Storage representation. Any data-layer known to `qutip.data.to` is + dtype : type or str, optional + Storage representation. Any data-layer known to ``qutip.data.to`` is accepted. Returns @@ -822,10 +808,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): @@ -835,14 +822,14 @@ def num(N, offset=0, *, dtype=None): Parameters ---------- N : int - The dimension of the Hilbert space. + Number of basis states in the Hilbert space. - offset : int (default 0) + offset : int, default: 0 The lowest number state that is included in the finite number state representation of the operator. - dtype : type or str - Storage representation. Any data-layer known to `qutip.data.to` is + dtype : type or str, optional + Storage representation. Any data-layer known to ``qutip.data.to`` is accepted. Returns @@ -876,17 +863,17 @@ def squeeze(N, z, offset=0, *, dtype=None): z : float/complex Squeezing parameter. - offset : int (default 0) + offset : int, default: 0 The lowest number state that is included in the finite number state representation of the operator. - dtype : type or str - Storage representation. Any data-layer known to `qutip.data.to` is + dtype : type or str, optional + Storage representation. Any data-layer known to ``qutip.data.to`` is accepted. Returns ------- - oper : :class:`qutip.Qobj` + oper : :class:`.Qobj` Squeezing operator. @@ -917,10 +904,10 @@ def squeezing(a1, a2, z): Parameters ---------- - a1 : :class:`qutip.Qobj` + a1 : :class:`.Qobj` Operator 1. - a2 : :class:`qutip.Qobj` + a2 : :class:`.Qobj` Operator 2. z : float/complex @@ -928,7 +915,7 @@ def squeezing(a1, a2, z): Returns ------- - oper : :class:`qutip.Qobj` + oper : :class:`.Qobj` Squeezing operator. """ @@ -942,17 +929,17 @@ def displace(N, alpha, offset=0, *, dtype=None): Parameters ---------- N : int - Dimension of Hilbert space. + Number of basis states in the Hilbert space. alpha : float/complex Displacement amplitude. - offset : int (default 0) + offset : int, default: 0 The lowest number state that is included in the finite number state representation of the operator. - dtype : type or str - Storage representation. Any data-layer known to `qutip.data.to` is + dtype : type or str, optional + Storage representation. Any data-layer known to ``qutip.data.to`` is accepted. Returns @@ -981,6 +968,14 @@ def commutator(A, B, kind="normal"): """ Return the commutator of kind `kind` (normal, anti) of the two operators A and B. + + Parameters + ---------- + A, B : :obj:`Qobj`, :obj:`QobjEvo` + The operators to compute the commutator of. + + kind: str {"normal", "anti"}, default: "anti" + Which kind of commutator to compute. """ if kind == 'normal': return A @ B - B @ A @@ -998,8 +993,8 @@ def qutrit_ops(*, dtype=None): Parameters ---------- - dtype : type or str - Storage representation. Any data-layer known to `qutip.data.to` is + dtype : type or str, optional + Storage representation. Any data-layer known to ``qutip.data.to`` is accepted. Returns @@ -1029,13 +1024,13 @@ def phase(N, phi0=0, *, dtype=None): Parameters ---------- N : int - Number of basis states in Hilbert space. + Number of basis states in the Hilbert space. - phi0 : float + phi0 : float, default: 0 Reference phase. - dtype : type or str - Storage representation. Any data-layer known to `qutip.data.to` is + dtype : type or str, optional + Storage representation. Any data-layer known to ``qutip.data.to`` is accepted. Returns @@ -1054,113 +1049,7 @@ def phase(N, phi0=0, *, dtype=None): states = np.array([np.sqrt(kk) / np.sqrt(N) * np.exp(1j * n * kk) for kk in phim]) ops = np.sum([np.outer(st, st.conj()) for st in states], axis=0) - return Qobj(ops, dims=[[N], [N]], type='oper', copy=False).to(dtype) - - -def enr_destroy(dims, excitations, *, dtype=None): - """ - Generate annilation operators for modes in a excitation-number-restricted - state space. For example, consider a system consisting of 4 modes, each - with 5 states. The total hilbert space size is 5**4 = 625. If we are - only interested in states that contain up to 2 excitations, we only need - to include states such as - - (0, 0, 0, 0) - (0, 0, 0, 1) - (0, 0, 0, 2) - (0, 0, 1, 0) - (0, 0, 1, 1) - (0, 0, 2, 0) - ... - - This function creates annihilation operators for the 4 modes that act - within this state space: - - a1, a2, a3, a4 = enr_destroy([5, 5, 5, 5], excitations=2) - - From this point onwards, the annihiltion operators a1, ..., a4 can be - used to setup a Hamiltonian, collapse operators and expectation-value - operators, etc., following the usual pattern. - - Parameters - ---------- - dims : list - A list of the dimensions of each subsystem of a composite quantum - system. - - excitations : integer - The maximum number of excitations that are to be included in the - state space. - - dtype : type or str - Storage representation. Any data-layer known to `qutip.data.to` is - accepted. - - Returns - ------- - a_ops : list of qobj - A list of annihilation operators for each mode in the composite - quantum system described by dims. - """ - from .states import enr_state_dictionaries - dtype = dtype or settings.core["default_dtype"] or _data.CSR - - nstates, state2idx, idx2state = enr_state_dictionaries(dims, excitations) - - a_ops = [scipy.sparse.lil_matrix((nstates, nstates), dtype=np.complex128) - for _ in dims] - - for n1, state1 in enumerate(idx2state): - for idx, s in enumerate(state1): - # if s > 0, the annihilation operator of mode idx has a non-zero - # entry with one less excitation in mode idx in the final state - if s > 0: - state2 = state1[:idx] + (s-1,) + state1[idx+1:] - n2 = state2idx[state2] - a_ops[idx][n2, n1] = np.sqrt(s) - - return [Qobj(a, dims=[dims, dims]).to(dtype) for a in a_ops] - - -def enr_identity(dims, excitations, *, dtype=None): - """ - Generate the identity operator for the excitation-number restricted - state space defined by the `dims` and `exciations` arguments. See the - docstring for enr_fock for a more detailed description of these arguments. - - Parameters - ---------- - dims : list - A list of the dimensions of each subsystem of a composite quantum - system. - - excitations : integer - The maximum number of excitations that are to be included in the - state space. - - state : list of integers - The state in the number basis representation. - - dtype : type or str - Storage representation. Any data-layer known to `qutip.data.to` is - accepted. - - Returns - ------- - op : Qobj - A Qobj instance that represent the identity operator in the - exication-number-restricted state space defined by `dims` and - `exciations`. - """ - dtype = dtype or settings.core["default_dtype"] or _data.Dia - from .states import enr_state_dictionaries - nstates, _, _ = enr_state_dictionaries(dims, excitations) - return Qobj(_data.identity[dtype](nstates), - dims=[dims, dims], - type='oper', - isherm=True, - isunitary=True, - copy=False) + return Qobj(ops, dims=[[N], [N]], copy=False).to(dtype) def charge(Nmax, Nmin=None, frac=1, *, dtype=None): @@ -1173,14 +1062,14 @@ def charge(Nmax, Nmin=None, frac=1, *, dtype=None): Nmax : int Maximum charge state to consider. - Nmin : int (default = -Nmax) + Nmin : int, default: -Nmax Lowest charge state to consider. - frac : float (default = 1) + frac : float, default: 1 Specify fractional charge if needed. - dtype : type or str - Storage representation. Any data-layer known to `qutip.data.to` is + dtype : type or str, optional + Storage representation. Any data-layer known to ``qutip.data.to`` is accepted. Returns @@ -1210,24 +1099,19 @@ def tunneling(N, m=1, *, dtype=None): Parameters ---------- N : int - Number of basis states in Hilbert space. + Number of basis states in the Hilbert space. - m : int (default = 1) + m : int, default: 1 Number of excitations in tunneling event. - dtype : type or str - Storage representation. Any data-layer known to `qutip.data.to` is + dtype : type or str, optional + Storage representation. Any data-layer known to ``qutip.data.to`` is accepted. Returns ------- T : Qobj Tunneling operator. - - Notes - ----- - .. versionadded:: 3.2 - """ diags = [np.ones(N-m, dtype=int), np.ones(N-m, dtype=int)] T = qdiags(diags, [m, -m], dtype=dtype) @@ -1242,12 +1126,12 @@ def qft(dimensions, *, dtype="dense"): Parameters ---------- dimensions : (int) or (list of int) or (list of list of int) - Dimension of Hilbert space. If provided as a list of ints, then the - dimension is the product over this list, but the ``dims`` property of - the new Qobj are set to this list. + Number of basis states in the Hilbert space. If provided as a list of + ints, then the dimension is the product over this list, but the + ``dims`` property of the new Qobj are set to this list. dtype : str or type, [keyword only] [optional] - Storage representation. Any data-layer known to `qutip.data.to` is + Storage representation. Any data-layer known to ``qutip.data.to`` is accepted. Returns @@ -1256,13 +1140,14 @@ def qft(dimensions, *, dtype="dense"): Quantum Fourier transform operator. """ - N2, dimensions = _implicit_tensor_dimensions(dimensions) + dimensions = Space(dimensions) + N2 = dimensions.size phase = 2.0j * np.pi / N2 arr = np.arange(N2) L, M = np.meshgrid(arr, arr) data = np.exp(phase * (L * M)) / np.sqrt(N2) - return Qobj(data, dims=dimensions).to(dtype) + return Qobj(data, dims=[dimensions]*2).to(dtype) def swap(N, M, *, dtype=None): diff --git a/qutip/core/options.py b/qutip/core/options.py index 1724c81890..4748dd3612 100644 --- a/qutip/core/options.py +++ b/qutip/core/options.py @@ -52,7 +52,7 @@ def __exit__(self, exc_type, exc_value, exc_traceback): class CoreOptions(QutipOptions): """ - Options used by the core of qutip such as the tolerance of :class:`Qobj` + Options used by the core of qutip such as the tolerance of :obj:`.Qobj` comparison or coefficient's format. Values can be changed in ``qutip.settings.core`` or by using context: @@ -63,8 +63,15 @@ class CoreOptions(QutipOptions): auto_tidyup : bool Whether to tidyup during sparse operations. - auto_tidyup_dims : bool [True] - Use auto tidyup dims on multiplication. (Not used yet) + auto_tidyup_dims : bool [False] + Use auto tidyup dims on multiplication, tensor, etc. + Without auto_tidyup_dims: + ``basis([2, 2]).dims == [[2, 2], [1, 1]]`` + With auto_tidyup_dims: + ``basis([2, 2]).dims == [[2, 2], [1]]`` + + auto_herm : boolTrue + detect hermiticity atol : float {1e-12} General absolute tolerance @@ -74,9 +81,9 @@ class CoreOptions(QutipOptions): Used to choose QobjEvo.expect output type auto_tidyup_atol : float {1e-14} - The absolute tolerance used in automatic tidyup (see the ``auto_tidyup`` - parameter above) and the default value of ``atol`` used in - :method:`Qobj.tidyup`. + The absolute tolerance used in automatic tidyup (see the + ``auto_tidyup`` parameter above) and the default value of ``atol`` used + in :meth:`Qobj.tidyup`. function_coefficient_style : str {"auto"} The signature expected by function coefficients. The options are: @@ -95,7 +102,7 @@ class CoreOptions(QutipOptions): ``pythonic`` is used. default_dtype : Nonetype, str, type {None} - When set, functions creating :class:`Qobj`, such as :func:"qeye" or + When set, functions creating :obj:`.Qobj`, such as :func:"qeye" or :func:"rand_herm", will use the specified data type. Any data-layer known to ``qutip.data.to`` is accepted. When ``None``, these functions will default to a sensible data type. @@ -104,7 +111,9 @@ class CoreOptions(QutipOptions): # use auto tidyup "auto_tidyup": True, # use auto tidyup dims on multiplication - "auto_tidyup_dims": True, + "auto_tidyup_dims": False, + # detect hermiticity + "auto_herm": True, # general absolute tolerance "atol": 1e-12, # general relative tolerance diff --git a/qutip/core/qobj.py b/qutip/core/qobj.py index 2560a90b54..c050143ecd 100644 --- a/qutip/core/qobj.py +++ b/qutip/core/qobj.py @@ -18,30 +18,10 @@ from ..settings import settings from . import data as _data from .dimensions import ( - type_from_dims, enumerate_flat, collapse_dims_super, flatten, unflatten, + enumerate_flat, collapse_dims_super, flatten, unflatten, Dimensions ) -_ADJOINT_TYPE_LOOKUP = { - 'oper': 'oper', - 'super': 'super', - 'ket': 'bra', - 'bra': 'ket', - 'operator-ket': 'operator-bra', - 'operator-bra': 'operator-ket', -} - -_MATMUL_TYPE_LOOKUP = { - ('oper', 'ket'): 'ket', - ('oper', 'oper'): 'oper', - ('ket', 'bra'): 'oper', - ('bra', 'oper'): 'bra', - ('super', 'super'): 'super', - ('super', 'operator-ket'): 'operator-ket', - ('operator-bra', 'super'): 'operator-bra', - ('operator-ket', 'operator-bra'): 'super', -} - _NORM_FUNCTION_LOOKUP = { 'tr': _data.norm.trace, 'one': _data.norm.one, @@ -60,27 +40,33 @@ def isbra(x): - return isinstance(x, Qobj) and x.type == 'bra' + from .cy.qobjevo import QobjEvo + return isinstance(x, (Qobj, QobjEvo)) and x.type in ['bra', 'scalar'] def isket(x): - return isinstance(x, Qobj) and x.type == 'ket' + from .cy.qobjevo import QobjEvo + return isinstance(x, (Qobj, QobjEvo)) and x.type in ['ket', 'scalar'] def isoper(x): - return isinstance(x, Qobj) and x.type == 'oper' + from .cy.qobjevo import QobjEvo + return isinstance(x, (Qobj, QobjEvo)) and x.type in ['oper', 'scalar'] def isoperbra(x): - return isinstance(x, Qobj) and x.type == 'operator-bra' + from .cy.qobjevo import QobjEvo + return isinstance(x, (Qobj, QobjEvo)) and x.type in ['operator-bra'] def isoperket(x): - return isinstance(x, Qobj) and x.type == 'operator-ket' + from .cy.qobjevo import QobjEvo + return isinstance(x, (Qobj, QobjEvo)) and x.type in ['operator-ket'] def issuper(x): - return isinstance(x, Qobj) and x.type == 'super' + from .cy.qobjevo import QobjEvo + return isinstance(x, (Qobj, QobjEvo)) and x.type in ['super'] def isherm(x): @@ -95,43 +81,27 @@ 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] - 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)), - dims=self.dims, - type=self.type, - superrep=self.superrep, + dims=self._dims, isherm=(scale.imag == 0), isunitary=(abs(abs(scale)-1) < settings.core['atol']), copy=False) - if not isinstance(other, Qobj): - try: - other = Qobj(other, type=self.type) - except TypeError: - return NotImplemented - if self.dims != other.dims: - msg = ( - "incompatible dimensions " - + repr(self.dims) + " and " + repr(other.dims) - ) - raise ValueError(msg) - if self.type != other.type: - msg = "incompatible types " + self.type + " and " + other.type - raise ValueError(msg) - if self.superrep != other.superrep: - msg = ( - "incompatible superoperator representations" - + self.superrep + " and " + other.superrep - ) - raise ValueError(msg) - return method(self, other) + return method(self, other) + return NotImplemented + return out @@ -177,12 +147,10 @@ class Qobj: Parameters ---------- - inpt: array_like + inpt: array_like, data object or :obj:`.Qobj` Data for vector/matrix representation of the quantum object. dims: list Dimensions of object used for tensor products. - type: {'bra', 'ket', 'oper', 'operator-ket', 'operator-bra', 'super'} - The type of quantum object to be represented. shape: list Shape of underlying data structure (matrix shape). copy: bool @@ -192,8 +160,12 @@ class Qobj: Attributes ---------- - data : array_like - Sparse matrix characterizing the quantum object. + data : object + The data object storing the vector / matrix representation of the + `Qobj`. + dtype : type + The data-layer type used for storing the data. The possible types are + described in `Qobj.to <./classes.html#qutip.core.qobj.Qobj.to>`__. dims : list List of dimensions keeping track of the tensor structure. shape : list @@ -203,7 +175,8 @@ class Qobj: 'operator-bra', or 'super'. superrep : str Representation used if `type` is 'super'. One of 'super' - (Liouville form) or 'choi' (Choi matrix with tr = dimension). + (Liouville form), 'choi' (Choi matrix with tr = dimension), + or 'chi' (chi-matrix representation). isherm : bool Indicates if quantum object represents Hermitian operator. isunitary : bool @@ -241,10 +214,16 @@ class Qobj: Create copy of Qobj conj() Conjugate of quantum object. + contract() + Contract subspaces of the tensor structure which are 1D. cosm() Cosine of quantum object. dag() Adjoint (dagger) of quantum object. + data_as(format, copy) + Vector / matrix representation of quantum object. + diag() + Diagonal elements of quantum object. dnorm() Diamond norm of quantum operator. dual_chan() @@ -262,10 +241,14 @@ class Qobj: object. inv() Return a Qobj corresponding to the matrix inverse of the operator. + logm() + Matrix logarithm of quantum operator. matrix_element(bra, ket) Returns the matrix element of operator between `bra` and `ket` vectors. norm(norm='tr', sparse=False, tol=0, maxiter=100000) Returns norm of a ket or an operator. + overlap(other) + Overlap between two state vectors or two operators. permute(order) Returns composite qobj with indices reordered. proj() @@ -273,6 +256,8 @@ class Qobj: ptrace(sel) Returns quantum object for selected dimensions after performing partial trace. + purity() + Calculates the purity of a quantum object. sinm() Sine of quantum object. sqrtm() @@ -297,65 +282,80 @@ class Qobj: def _initialize_data(self, arg, dims, copy): if isinstance(arg, _data.Data): - self.dims = dims or [[arg.shape[0]], [arg.shape[1]]] self._data = arg.copy() if copy else arg + self._dims = Dimensions(dims or [[arg.shape[0]], [arg.shape[1]]]) elif isinstance(arg, Qobj): - self.dims = dims or arg.dims.copy() self._data = arg.data.copy() if copy else arg.data - elif arg is None or isinstance(arg, numbers.Number): - self.dims = dims or [[1], [1]] - size = np.prod(self.dims[0]) - if arg is None: - self._data = _data.zeros(size, size) - else: - self._data = _data.identity(size, scale=complex(arg)) + self._dims = Dimensions(dims or arg._dims) + if self._isherm is None and arg._isherm is not None: + self._isherm = arg._isherm + if self._isunitary is None and arg._isunitary is not None: + self._isunitary = arg._isunitary else: self._data = _data.create(arg, copy=copy) - self.dims = dims or [[self._data.shape[0]], [self._data.shape[1]]] + self._dims = Dimensions( + dims or [[self._data.shape[0]], [self._data.shape[1]]] + ) + if self._dims.shape != self._data.shape: + raise ValueError('Provided dimensions do not match the data: ' + + f"{self._dims.shape} vs {self._data.shape}") - def __init__(self, arg=None, dims=None, type=None, + def __init__(self, arg=None, dims=None, copy=True, superrep=None, isherm=None, isunitary=None): - self._initialize_data(arg, dims, copy) - self.type = type or type_from_dims(self.dims) self._isherm = isherm self._isunitary = isunitary + self._initialize_data(arg, dims, copy) - if self.type == 'super' and type_from_dims(self.dims) == 'oper': - if self._data.shape[0] != self._data.shape[1]: - raise ValueError("".join([ - "cannot build superoperator from nonsquare data of shape ", - repr(self._data.shape), - ])) - root = int(np.sqrt(self._data.shape[0])) - if root * root != self._data.shape[0]: - raise ValueError("".join([ - "cannot build superoperator from nonsquare subspaces ", - "of size ", - repr(self._data.shape[0]), - ])) - self.dims = [[[root]]*2]*2 - if self.type in ['super', 'operator-ket', 'operator-bra']: - superrep = superrep or 'super' - self.superrep = superrep + if superrep is not None: + self.superrep = superrep def copy(self): """Create identical copy""" return Qobj(arg=self._data, dims=self.dims, - type=self.type, - superrep=self.superrep, isherm=self._isherm, isunitary=self._isunitary, copy=True) + @property + def dims(self): + return self._dims.as_list() + + @dims.setter + def dims(self, dims): + dims = Dimensions(dims, rep=self.superrep) + if dims.shape != self._data.shape: + raise ValueError('Provided dimensions do not match the data: ' + + f"{dims.shape} vs {self._data.shape}") + self._dims = dims + + @property + def type(self): + return self._dims.type + + @property + def superrep(self): + return self._dims.superrep + + @superrep.setter + def superrep(self, super_rep): + self._dims = self._dims.replace_superrep(super_rep) + @property 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): raise TypeError('Qobj data must be a data-layer format.') + if self._dims.shape != data.shape: + raise ValueError('Provided data do not match the dimensions: ' + + f"{self._dims.shape} vs {data.shape}") self._data = data def to(self, data_type): @@ -364,11 +364,12 @@ def to(self, data_type): storage representation. The different storage representations available are the "data-layer - types" which are known to `qutip.data.to`. By default, these are - `qutip.data.Dense` and `qutip.data.CSR`, which respectively construct a - dense matrix store and a compressed sparse row one. Certain algorithms - and operations may be faster or more accurate when using a more - appropriate data store. + types" which are known to :obj:`qutip.core.data.to`. By default, these + are :class:`~qutip.core.data.CSR`, :class:`~qutip.core.data.Dense` and + :class:`~qutip.core.data.Dia`, which respectively construct a + compressed sparse row matrix, diagonal matrix and a dense one. Certain + algorithms and operations may be faster or more accurate when using a + more appropriate data store. If the data store is already in the format requested, the function returns `self`. Otherwise, it returns a copy of itself with the data @@ -377,25 +378,23 @@ def to(self, data_type): Parameters ---------- data_type : type - The data-layer type that the data of this `Qobj` should be + The data-layer type that the data of this :class:`Qobj` should be converted to. Returns ------- Qobj - A new `Qobj` if a type conversion took place with the data stored - in the requested format, or `self` if not. + A new :class:`Qobj` if a type conversion took place with the data + stored in the requested format, or `self` if not. """ try: 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, - type=self.type, - superrep=self.superrep, + dims=self._dims, isherm=self._isherm, isunitary=self._isunitary, copy=False) @@ -404,12 +403,9 @@ def to(self, data_type): def __add__(self, other): if other == 0: return self.copy() - isherm = (self._isherm and other._isherm) or None return Qobj(_data.add(self._data, other._data), - dims=self.dims, - type=self.type, - superrep=self.superrep, - isherm=isherm, + dims=self._dims, + isherm=(self._isherm and other._isherm) or None, copy=False) def __radd__(self, other): @@ -419,12 +415,9 @@ def __radd__(self, other): def __sub__(self, other): if other == 0: return self.copy() - isherm = (self._isherm and other._isherm) or None return Qobj(_data.sub(self._data, other._data), - dims=self.dims, - type=self.type, - superrep=self.superrep, - isherm=isherm, + dims=self._dims, + isherm=(self._isherm and other._isherm) or None, copy=False) def __rsub__(self, other): @@ -459,9 +452,7 @@ def __mul__(self, other): isunitary = None return Qobj(out, - dims=self.dims, - type=self.type, - superrep=self.superrep, + dims=self._dims, isherm=isherm, isunitary=isunitary, copy=False) @@ -477,48 +468,23 @@ 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.superrep != other.superrep: - raise TypeError("".join([ - "incompatible superoperator representations ", - repr(self.superrep), - " and ", - repr(other.superrep), - ])) - 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) - try: - type_ = _MATMUL_TYPE_LOOKUP[(self.type, other.type)] - except KeyError: - raise TypeError( - "incompatible matmul types " - + repr(self.type) + " and " + repr(other.type) - ) from None - return Qobj(_data.matmul(self.data, other.data), - dims=[self.dims[0], other.dims[1]], - type=type_, - isunitary=self._isunitary and other._isunitary, - superrep=self.superrep, - 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) def __neg__(self): return Qobj(_data.neg(self._data), - dims=self.dims.copy(), - type=self.type, - superrep=self.superrep, + dims=self._dims, isherm=self._isherm, isunitary=self._isunitary, copy=False) @@ -542,7 +508,7 @@ def __getitem__(self, ind): def __eq__(self, other): if self is other: return True - if not isinstance(other, Qobj) or self.dims != other.dims: + if not isinstance(other, Qobj) or self._dims != other._dims: return False return _data.iszero(_data.sub(self._data, other._data), tol=settings.core['atol']) @@ -550,16 +516,14 @@ def __eq__(self, other): def __pow__(self, n, m=None): # calculates powers of Qobj if ( self.type not in ('oper', 'super') - or self.dims[0] != self.dims[1] + or self._dims[0] != self._dims[1] or m is not None or not isinstance(n, numbers.Integral) or n < 0 ): return NotImplemented return Qobj(_data.pow(self._data, n), - dims=self.dims, - type=self.type, - superrep=self.superrep, + dims=self._dims, isherm=self._isherm, isunitary=self._isunitary, copy=False) @@ -567,7 +531,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'): @@ -644,10 +608,10 @@ def _repr_latex_(self): cols.append(None) cols += list(range(n_cols - half_length, n_cols)) # Make the data array. - data = r'\begin{equation*}\left(\begin{array}{*{11}c}' + data = r'$$\left(\begin{array}{cc}' data += r"\\".join(_latex_row(row, cols, self.data.to_array()) for row in rows) - data += r'\end{array}\right)\end{equation*}' + data += r'\end{array}\right)$$' return self._str_header() + data def __and__(self, other): @@ -662,9 +626,7 @@ def dag(self): if self._isherm: return self.copy() return Qobj(_data.adjoint(self._data), - dims=[self.dims[1], self.dims[0]], - type=_ADJOINT_TYPE_LOOKUP[self.type], - superrep=self.superrep, + dims=Dimensions(self._dims[0], self._dims[1]), isherm=self._isherm, isunitary=self._isunitary, copy=False) @@ -672,9 +634,7 @@ def dag(self): def conj(self): """Get the element-wise conjugation of the quantum object.""" return Qobj(_data.conj(self._data), - dims=self.dims.copy(), - type=self.type, - superrep=self.superrep, + dims=self._dims, isherm=self._isherm, isunitary=self._isunitary, copy=False) @@ -688,9 +648,7 @@ def trans(self): Transpose of input operator. """ return Qobj(_data.transpose(self._data), - dims=[self.dims[1], self.dims[0]], - type=_ADJOINT_TYPE_LOOKUP[self.type], - superrep=self.superrep, + dims=Dimensions(self._dims[0], self._dims[1]), isherm=self._isherm, isunitary=self._isunitary, copy=False) @@ -748,28 +706,27 @@ 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. Parameters ---------- - Q : :class:`qutip.Qobj` + Q : :class:`.Qobj` Input bra or ket vector Returns ------- - P : :class:`qutip.Qobj` + P : :class:`.Qobj` Projection operator. """ if not (self.isket or self.isbra): raise TypeError("projection is only defined for bras and kets") - dims = ([self.dims[0], self.dims[0]] if self.isket - else [self.dims[1], self.dims[1]]) + dims = ([self._dims[0], self._dims[0]] if self.isket + else [self._dims[1], self._dims[1]]) return Qobj(_data.project(self._data), dims=dims, - type='oper', isherm=True, copy=False) @@ -803,8 +760,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. @@ -842,7 +799,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. @@ -874,7 +831,7 @@ def expm(self, dtype=_data.Dense): Returns ------- - oper : :class:`qutip.Qobj` + oper : :class:`.Qobj` Exponentiated quantum operator. Raises @@ -882,12 +839,10 @@ def expm(self, dtype=_data.Dense): TypeError Quantum operator is not square. """ - if self.dims[0] != self.dims[1]: + if not self._dims.issquare: raise TypeError("expm is only valid for square operators") return Qobj(_data.expm(self._data, dtype=dtype), - dims=self.dims, - type=self.type, - superrep=self.superrep, + dims=self._dims, isherm=self._isherm, copy=False) @@ -898,7 +853,7 @@ def logm(self): Returns ------- - oper : :class:`qutip.Qobj` + oper : :class:`.Qobj` Logarithm of the quantum operator. Raises @@ -906,12 +861,10 @@ def logm(self): TypeError Quantum operator is not square. """ - if self.dims[0] != self.dims[1]: + if not self._dims.issquare: raise TypeError("expm is only valid for square operators") return Qobj(_data.logm(self._data), - dims=self.dims, - type=self.type, - superrep=self.superrep, + dims=self._dims, isherm=self._isherm, copy=False) @@ -941,7 +894,7 @@ def sqrtm(self, sparse=False, tol=0, maxiter=100000): Returns ------- - oper : :class:`qutip.Qobj` + oper : :class:`.Qobj` Matrix square root of operator. Raises @@ -954,7 +907,7 @@ def sqrtm(self, sparse=False, tol=0, maxiter=100000): The sparse eigensolver is much slower than the dense version. Use sparse only if memory requirements demand it. """ - if self.dims[0] != self.dims[1]: + if self._dims[0] != self._dims[1]: raise TypeError('sqrt only valid on square matrices') if isinstance(self.data, _data.CSR) and sparse: evals, evecs = _data.eigs_csr(self.data, @@ -966,16 +919,13 @@ def sqrtm(self, sparse=False, tol=0, maxiter=100000): else: evals, evecs = _data.eigs(self.data, isherm=self._isherm) - numevals = len(evals) dV = _data.diag([np.sqrt(evals, dtype=complex)], 0) if self.isherm: spDv = _data.matmul(dV, evecs.conj().transpose()) else: spDv = _data.matmul(dV, _data.inv(evecs)) return Qobj(_data.matmul(evecs, spDv), - dims=self.dims, - type=self.type, - superrep=self.superrep, + dims=self._dims, copy=False) def cosm(self): @@ -985,7 +935,7 @@ def cosm(self): Returns ------- - oper : :class:`qutip.Qobj` + oper : :class:`.Qobj` Matrix cosine of operator. Raises @@ -998,7 +948,7 @@ def cosm(self): Uses the Q.expm() method. """ - if self.dims[0] != self.dims[1]: + if self._dims[0] != self._dims[1]: raise TypeError('invalid operand for matrix cosine') return 0.5 * ((1j * self).expm() + (-1j * self).expm()) @@ -1009,7 +959,7 @@ def sinm(self): Returns ------- - oper : :class:`qutip.Qobj` + oper : :class:`.Qobj` Matrix sine of operator. Raises @@ -1021,7 +971,7 @@ def sinm(self): ----- Uses the Q.expm() method. """ - if self.dims[0] != self.dims[1]: + if self._dims[0] != self._dims[1]: raise TypeError('invalid operand for matrix sine') return -0.5j * ((1j * self).expm() - (-1j * self).expm()) @@ -1032,7 +982,7 @@ def inv(self, sparse=False): Returns ------- - oper : :class:`qutip.Qobj` + oper : :class:`.Qobj` Matrix inverse of operator. Raises @@ -1048,9 +998,7 @@ def inv(self, sparse=False): data = self.data return Qobj(_data.inv(data), - dims=[self.dims[1], self.dims[0]], - type=self.type, - superrep=self.superrep, + dims=[self._dims[1], self._dims[0]], copy=False) def unit(self, inplace=False, norm=None, kwargs=None): @@ -1069,7 +1017,7 @@ def unit(self, inplace=False, norm=None, kwargs=None): Returns ------- - obj : :class:`qutip.Qobj` + obj : :class:`.Qobj` Normalized quantum object. Will be the `self` object if in place. """ norm = self.norm(norm=norm, kwargs=kwargs) @@ -1119,7 +1067,7 @@ def ptrace(self, sel, dtype=None): Returns ------- - oper : :class:`qutip.Qobj` + oper : :class:`.Qobj` Quantum object representing partial trace with selected components remaining. """ @@ -1147,8 +1095,8 @@ def ptrace(self, sel, dtype=None): raise ValueError("partial trace is not defined on non-square maps") dims = flatten(dims[0]) new_data = _data.ptrace(data, dims, sel, dtype=dtype) - new_dims = [[dims[x] for x in sel]] * 2 - out = Qobj(new_data, dims=new_dims, type='oper', copy=False) + new_dims = [[dims[x] for x in sel]] * 2 if sel else None + out = Qobj(new_data, dims=new_dims, copy=False) if self.isoperket: return operator_to_vector(out) if self.isoperbra: @@ -1204,33 +1152,47 @@ def contract(self, inplace=False): if inplace: self.dims = dims return self - return Qobj(self.data.copy(), dims=dims, type=self.type, copy=False) + return Qobj(self.data.copy(), dims=dims, copy=False) def permute(self, order): """ Permute the tensor structure of a quantum object. For example, - ``qutip.tensor(x, y).permute([1, 0])`` + + ``qutip.tensor(x, y).permute([1, 0])`` + will give the same result as - ``qutip.tensor(y, x)`` + + ``qutip.tensor(y, x)`` + and - ``qutip.tensor(a, b, c).permute([1, 2, 0])`` + + ``qutip.tensor(a, b, c).permute([1, 2, 0])`` + will be the same as - ``qutip.tensor(b, c, a)`` + + ``qutip.tensor(b, c, a)`` For regular objects (bras, kets and operators) we expect ``order`` to be a flat list of integers, which specifies the new order of the tensor product. For superoperators, we expect ``order`` to be something like - ``[[0, 2], [1, 3]]`` + + ``[[0, 2], [1, 3]]`` + which tells us to permute according to [0, 2, 1, 3], and then group indices according to the length of each sublist. As another example, permuting a superoperator with dimensions of - ``[[[1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3]]]`` + + ``[[[1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3]]]`` + by an ``order`` - ``[[0, 3], [1, 4], [2, 5]]`` + + ``[[0, 3], [1, 4], [2, 5]]`` + should give a new object with dimensions - ``[[[1, 1], [2, 2], [3, 3]], [[1, 1], [2, 2], [3, 3]]]``. + + ``[[[1, 1], [2, 2], [3, 3]], [[1, 1], [2, 2], [3, 3]]]``. Parameters ---------- @@ -1239,7 +1201,7 @@ def permute(self, order): Returns ------- - P : :class:`qutip.Qobj` + P : :class:`.Qobj` Permuted quantum object. """ if self.type in ('bra', 'ket', 'oper'): @@ -1250,13 +1212,12 @@ def permute(self, order): elif self.isket: dims = [new_structure, self.dims[1]] else: - if self.dims[0] != self.dims[1]: + if self._dims[0] != self._dims[1]: raise TypeError("undefined for non-square operators") dims = [new_structure, new_structure] data = _data.permute.dimensions(self.data, structure, order) return Qobj(data, dims=dims, - type=self.type, isherm=self._isherm, isunitary=self._isunitary, copy=False) @@ -1272,13 +1233,12 @@ def permute(self, order): elif self.isoperket: dims = [new_structure, self.dims[1]] else: - if self.dims[0] != self.dims[1]: + if self._dims[0] != self._dims[1]: raise TypeError("undefined for non-square operators") dims = [new_structure, new_structure] data = _data.permute.dimensions(self.data, flat_structure, flat_order) return Qobj(data, dims=dims, - type=self.type, superrep=self.superrep, copy=False) @@ -1294,7 +1254,7 @@ def tidyup(self, atol=None): Returns ------- - oper : :class:`qutip.Qobj` + oper : :class:`.Qobj` Quantum object with small elements removed. """ atol = atol or settings.core['auto_tidyup_atol'] @@ -1316,7 +1276,7 @@ def transform(self, inpt, inverse=False): Returns ------- - oper : :class:`qutip.Qobj` + oper : :class:`.Qobj` Operator in new basis. Notes @@ -1354,7 +1314,6 @@ def transform(self, inpt, inverse=False): data = _data.matmul(_data.matmul(S, self.data), S.adjoint()) return Qobj(data, dims=self.dims, - type=self.type, isherm=self._isherm, superrep=self.superrep, copy=False) @@ -1366,7 +1325,6 @@ def trunc_neg(self, method="clip"): of this instance, then renormalizing to obtain a valid density operator. - Parameters ---------- method : str @@ -1377,7 +1335,7 @@ def trunc_neg(self, method="clip"): Returns ------- - oper : :class:`qutip.Qobj` + oper : :class:`.Qobj` A valid density operator. """ if not self.isherm: @@ -1415,11 +1373,7 @@ def trunc_neg(self, method="clip"): _data.project(state.data), value) out_data = _data.mul(out_data, 1/_data.norm.trace(out_data)) - return Qobj(out_data, - dims=self.dims.copy(), - type=self.type, - isherm=True, - copy=False) + return Qobj(out_data, dims=self._dims, isherm=True, copy=False) def matrix_element(self, bra, ket): """Calculates a matrix element. @@ -1429,10 +1383,10 @@ def matrix_element(self, bra, ket): Parameters ---------- - bra : :class:`qutip.Qobj` + bra : :class:`.Qobj` Quantum object of type 'bra' or 'ket' - ket : :class:`qutip.Qobj` + ket : :class:`.Qobj` Quantum object of type 'ket'. Returns @@ -1466,7 +1420,7 @@ def overlap(self, other): Parameters ---------- - other : :class:`qutip.Qobj` + other : :class:`.Qobj` Quantum object for a state vector of type 'ket', 'bra' or density matrix. @@ -1568,12 +1522,10 @@ def eigenstates(self, sparse=False, sort='low', eigvals=0, if self.type == 'super': new_dims = [self.dims[0], [1]] - new_type = 'operator-ket' else: new_dims = [self.dims[0], [1]*len(self.dims[0])] - new_type = 'ket' ekets = np.empty((evecs.shape[1],), dtype=object) - ekets[:] = [Qobj(vec, dims=new_dims, type=new_type, copy=False) + ekets[:] = [Qobj(vec, dims=new_dims, copy=False) for vec in _data.split_columns(evecs, False)] norms = np.array([ket.norm() for ket in ekets]) if phase_fix is None: @@ -1653,7 +1605,7 @@ def groundstate(self, sparse=False, tol=0, maxiter=100000, safe=True): ------- eigval : float Eigenvalue for the ground state of quantum operator. - eigvec : :class:`qutip.Qobj` + eigvec : :class:`.Qobj` Eigenket for the ground state of quantum operator. Notes @@ -1679,7 +1631,7 @@ def dnorm(self, B=None): Parameters ---------- - B : :class:`qutip.Qobj` or None + B : :class:`.Qobj` or None If B is not None, the diamond distance d(A, B) = dnorm(A - B) between this operator and B is returned instead of the diamond norm. @@ -1737,7 +1689,6 @@ def istp(self): for index in super_index]): qobj = Qobj(qobj.data, dims=collapse_dims_super(qobj.dims), - type=qobj.type, superrep=qobj.superrep, copy=False) # We use the condition from John Watrous' lecture notes, @@ -1786,14 +1737,39 @@ 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): + """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' - isbra = property(isbra) - isket = property(isket) - isoper = property(isoper) - issuper = property(issuper) - isoperbra = property(isoperbra) - isoperket = property(isoperket) + @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): @@ -1802,14 +1778,14 @@ def ptrace(Q, sel): Parameters ---------- - Q : :class:`qutip.Qobj` + Q : :class:`.Qobj` Composite quantum object. sel : int/list An ``int`` or ``list`` of components to keep after partial trace. Returns ------- - oper : :class:`qutip.Qobj` + oper : :class:`.Qobj` Quantum object representing partial trace with selected components remaining. diff --git a/qutip/core/states.py b/qutip/core/states.py index a3c310998a..2075dfe7b6 100644 --- a/qutip/core/states.py +++ b/qutip/core/states.py @@ -4,8 +4,7 @@ 'state_number_index', 'state_index_number', 'state_number_qobj', 'phase_basis', 'zero_ket', 'spin_state', 'spin_coherent', 'bell_state', 'singlet_state', 'triplet_states', 'w_state', - 'ghz_state', 'enr_state_dictionaries', 'enr_fock', - 'enr_thermal_dm'] + 'ghz_state'] import itertools import numbers @@ -19,8 +18,10 @@ from .qobj import Qobj from .operators import jmat, displace, qdiags from .tensor import tensor +from .dimensions import Space from .. import settings + def _promote_to_zero_list(arg, length): """ Ensure `arg` is a list of length `length`. If `arg` is None it is promoted @@ -43,13 +44,29 @@ def _promote_to_zero_list(arg, length): raise TypeError("Dimensions must be an integer or list of integers.") +def _to_space(dimensions): + """ + Convert `dimensions` to a :class:`.Space`. + + Returns + ------- + space : :class:`.Space` + """ + if isinstance(dimensions, Space): + return dimensions + elif isinstance(dimensions, list): + return Space(dimensions) + else: + return Space([dimensions]) + + def basis(dimensions, n=None, offset=None, *, dtype=None): """Generates the vector representation of a Fock state. Parameters ---------- - dimensions : int or list of ints - Number of Fock states in Hilbert space. If a list, then the resultant + dimensions : int or list of ints, Space + Number of basis states in Hilbert space. If a list, then the resultant object will be a tensor product over spaces with those dimensions. n : int or list of ints, optional (default 0 for all dimensions) @@ -62,13 +79,13 @@ def basis(dimensions, n=None, offset=None, *, dtype=None): The lowest number state that is included in the finite number state representation of the state in the relevant dimension. - dtype : type or str - storage representation. Any data-layer known to `qutip.data.to` is + dtype : type or str, optional + storage representation. Any data-layer known to ``qutip.data.to`` is accepted. Returns ------- - state : :class:`qutip.Qobj` + state : :class:`.Qobj` Qobj representing the requested number state ``|n>``. Examples @@ -106,26 +123,40 @@ def basis(dimensions, n=None, offset=None, *, dtype=None): """ dtype = dtype or settings.core["default_dtype"] or _data.Dense - # Promote all parameters to lists to simplify later logic. - if not isinstance(dimensions, list): - dimensions = [dimensions] - n_dimensions = len(dimensions) - ns = [m-off for m, off in zip(_promote_to_zero_list(n, n_dimensions), - _promote_to_zero_list(offset, n_dimensions))] - if any((not isinstance(x, numbers.Integral)) or x < 0 for x in dimensions): - raise ValueError("All dimensions must be >= 0.") - if not all(0 <= n < dimension for n, dimension in zip(ns, dimensions)): - raise ValueError("All basis indices must be " - "`offset <= n < dimension+offset`.") - location, size = 0, 1 - for m, dimension in zip(reversed(ns), reversed(dimensions)): - location += m*size - size *= dimension + # Promote all parameters to Space to simplify later logic. + dimensions = _to_space(dimensions) + + size = dimensions.size + if n is None: + location = 0 + elif offset: + if not isinstance(offset, list): + offset = [offset] + if not isinstance(n, list): + n = [n] + if len(n) != len(dimensions.as_list()) or len(offset) != len(n): + raise ValueError("All list inputs must be the same length.") + + n_off = [m-off for m, off in zip(n, offset)] + try: + location = dimensions.dims2idx(n_off) + except IndexError: + raise ValueError("All basis indices must be integers in the range " + "`offset <= n < dimension+offset`.") + else: + if not isinstance(n, list): + n = [n] + if len(n) != len(dimensions.as_list()): + raise ValueError("All list inputs must be the same length.") + try: + location = dimensions.dims2idx(n) + except IndexError: + raise ValueError("All basis indices must be integers in the range " + "`0 <= n < dimension`.") data = _data.one_element[dtype]((size, 1), (location, 0), 1) return Qobj(data, - dims=[dimensions, [1]*n_dimensions], - type='ket', + dims=[dimensions, dimensions.scalar_like()], isherm=False, isunitary=False, copy=False) @@ -134,8 +165,8 @@ def basis(dimensions, n=None, offset=None, *, dtype=None): def qutrit_basis(*, dtype=None): """Basis states for a three level system (qutrit) - dtype : type or str - storage representation. Any data-layer known to `qutip.data.to` is + dtype : type or str, optional + storage representation. Any data-layer known to ``qutip.data.to`` is accepted. Returns @@ -153,6 +184,7 @@ def qutrit_basis(*, dtype=None): ] return out + _COHERENT_METHODS = ('operator', 'analytic') @@ -169,16 +201,16 @@ def coherent(N, alpha, offset=0, method=None, *, dtype=None): alpha : float/complex Eigenvalue of coherent state. - offset : int (default 0) + offset : int, default: 0 The lowest number state that is included in the finite number state representation of the state. Using a non-zero offset will make the default method 'analytic'. - method : string {'operator', 'analytic'} + method : string {'operator', 'analytic'}, optional Method for generating coherent state. - dtype : type or str - Storage representation. Any data-layer known to `qutip.data.to` is + dtype : type or str, optional + Storage representation. Any data-layer known to ``qutip.data.to`` is accepted. Returns @@ -235,10 +267,7 @@ def coherent(N, alpha, offset=0, method=None, *, dtype=None): s = np.prod(np.sqrt(np.arange(1, offset + 1))) # sqrt factorial data[0] = np.exp(-abs(alpha)**2 * 0.5) * alpha**offset / s np.cumprod(data, out=sqrtn) # Reuse sqrtn array - return Qobj(sqrtn, - dims=[[N], [1]], - type='ket', - copy=False).to(dtype) + return Qobj(sqrtn, dims=[[N], [1]], copy=False).to(dtype) raise TypeError( "The method option can only take values in " + repr(_COHERENT_METHODS) ) @@ -247,25 +276,25 @@ def coherent(N, alpha, offset=0, method=None, *, dtype=None): def coherent_dm(N, alpha, offset=0, method='operator', *, dtype=None): """Density matrix representation of a coherent state. - Constructed via outer product of :func:`qutip.states.coherent` + Constructed via outer product of :func:`coherent` Parameters ---------- N : int - Number of Fock states in Hilbert space. + Number of basis states in Hilbert space. alpha : float/complex Eigenvalue for coherent state. - offset : int (default 0) + offset : int, default: 0 The lowest number state that is included in the finite number state representation of the state. - method : string {'operator', 'analytic'} + method : string {'operator', 'analytic'}, optional Method for generating coherent density matrix. - dtype : type or str - Storage representation. Any data-layer known to `qutip.data.to` is + dtype : type or str, optional + Storage representation. Any data-layer known to ``qutip.data.to`` is accepted. Returns @@ -298,32 +327,34 @@ 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): """Density matrix representation of a Fock state - Constructed via outer product of :func:`qutip.states.fock`. + Constructed via outer product of :func:`basis`. Parameters ---------- - dimensions : int or list of ints - Number of Fock states in Hilbert space. If a list, then the resultant + dimensions : int or list of ints, Space + Number of basis states in Hilbert space. If a list, then the resultant object will be a tensor product over spaces with those dimensions. - n : int or list of ints, optional (default 0 for all dimensions) + n : int or list of ints, default: 0 for all dimensions Integer corresponding to desired number state, defaults to 0 for all dimensions if omitted. The shape must match ``dimensions``, e.g. if ``dimensions`` is a list, then ``n`` must either be omitted or a list of equal length. - offset : int or list of ints, optional (default 0 for all dimensions) + offset : int or list of ints, default: 0 for all dimensions The lowest number state that is included in the finite number state representation of the state in the relevant dimension. - dtype : type or str - Storage representation. Any data-layer known to `qutip.data.to` is + dtype : type or str, optional + Storage representation. Any data-layer known to ``qutip.data.to`` is accepted. Returns @@ -343,32 +374,32 @@ 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): """Bosonic Fock (number) state. - Same as :func:`qutip.states.basis`. + Same as :func:`basis`. Parameters ---------- - dimensions : int or list of ints - Number of Fock states in Hilbert space. If a list, then the resultant + dimensions : int or list of ints, Space + Number of basis states in Hilbert space. If a list, then the resultant object will be a tensor product over spaces with those dimensions. - n : int or list of ints, optional (default 0 for all dimensions) + n : int or list of ints, default: 0 for all dimensions Integer corresponding to desired number state, defaults to 0 for all dimensions if omitted. The shape must match ``dimensions``, e.g. if ``dimensions`` is a list, then ``n`` must either be omitted or a list of equal length. - offset : int or list of ints, optional (default 0 for all dimensions) + offset : int or list of ints, default: 0 for all dimensions The lowest number state that is included in the finite number state representation of the state in the relevant dimension. - dtype : type or str - Storage representation. Any data-layer known to `qutip.data.to` is + dtype : type or str, optional + Storage representation. Any data-layer known to ``qutip.data.to`` is accepted. Returns @@ -400,12 +431,12 @@ def thermal_dm(N, n, method='operator', *, dtype=None): n : float Expectation value for number of particles in thermal state. - method : string {'operator', 'analytic'} + method : string {'operator', 'analytic'}, default: 'operator' ``string`` that sets the method used to generate the thermal state probabilities - dtype : type or str - Storage representation. Any data-layer known to `qutip.data.to` is + dtype : type or str, optional + Storage representation. Any data-layer known to ``qutip.data.to`` is accepted. Returns @@ -468,45 +499,48 @@ def thermal_dm(N, n, method='operator', *, dtype=None): return out -def maximally_mixed_dm(N, *, dtype=None): +def maximally_mixed_dm(dimensions, *, dtype=None): """ Returns the maximally mixed density matrix for a Hilbert space of dimension N. Parameters ---------- - N : int - Number of basis states in Hilbert space. + dimensions : int or list of ints, Space + Number of basis states in Hilbert space. If a list, then the resultant + object will be a tensor product over spaces with those dimensions. - dtype : type or str - Storage representation. Any data-layer known to `qutip.data.to` is + dtype : type or str, optional + Storage representation. Any data-layer known to ``qutip.data.to`` is accepted. Returns ------- - dm : qobj + dm : :obj:`.Qobj` Thermal state density matrix. """ dtype = dtype or settings.core["default_dtype"] or _data.Dia - if not isinstance(N, numbers.Integral) or N <= 0: - raise ValueError("N must be integer N > 0") - return Qobj(_data.identity[dtype](N, scale=1/N), dims=[[N], [N]], - type='oper', isherm=True, isunitary=(N == 1), copy=False) + dimensions = _to_space(dimensions) + N = dimensions.size + + return Qobj(_data.identity[dtype](N, scale=1/N), + dims=[dimensions, dimensions], + isherm=True, isunitary=(N == 1), copy=False) def ket2dm(Q): """ Takes input ket or bra vector and returns density matrix formed by outer - product. This is completely identical to calling `Q.proj()`. + product. This is completely identical to calling ``Q.proj()``. Parameters ---------- - Q : qobj + Q : :obj:`.Qobj` Ket or bra type quantum object. Returns ------- - dm : qobj + dm : :obj:`.Qobj` Density matrix formed by outer product of `Q`. Examples @@ -526,25 +560,26 @@ def ket2dm(Q): raise TypeError("Input is not a ket or bra vector.") -def projection(N, n, m, offset=None, *, dtype=None): +def projection(dimensions, n, m, offset=None, *, dtype=None): r""" The projection operator that projects state :math:`\lvert m\rangle` on state :math:`\lvert n\rangle`. Parameters ---------- - N : int - Number of basis states in Hilbert space. + dimensions : int or list of ints, Space + Number of basis states in Hilbert space. If a list, then the resultant + object will be a tensor product over spaces with those dimensions. n, m : float The number states in the projection. - offset : int (default 0) + offset : int, default: 0 The lowest number state that is included in the finite number state representation of the projector. - dtype : type or str - Storage representation. Any data-layer known to `qutip.data.to` is + dtype : type or str, optional + Storage representation. Any data-layer known to ``qutip.data.to`` is accepted. Returns @@ -553,8 +588,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(dimensions, n, offset=offset, dtype=dtype) @ + basis(dimensions, m, offset=offset, dtype=dtype).dag() + ).to(dtype) def qstate(string, *, dtype=None): @@ -571,8 +608,8 @@ def qstate(string, *, dtype=None): qstate : qobj Qobj for tensor product corresponding to input string. - dtype : type or str - Storage representation. Any data-layer known to `qutip.data.to` is + dtype : type or str, optional + Storage representation. Any data-layer known to ``qutip.data.to`` is accepted. Notes @@ -639,12 +676,12 @@ def ket(seq, dim=2, *, dtype=None): Note: for dimension > 9 you need to use a list. - dim : int (default: 2) / list of ints + dim : int or list of ints, default: 2 Space dimension for each particle: int if there are the same, list if they are different. - dtype : type or str - Storage representation. Any data-layer known to `qutip.data.to` is + dtype : type or str, optional + Storage representation. Any data-layer known to ``qutip.data.to`` is accepted. Returns @@ -729,8 +766,8 @@ def bra(seq, dim=2, *, dtype=None): Space dimension for each particle: int if there are the same, list if they are different. - dtype : type or str - Storage representation. Any data-layer known to `qutip.data.to` is + dtype : type or str, optional + Storage representation. Any data-layer known to ``qutip.data.to`` is accepted. Returns @@ -783,7 +820,7 @@ def state_number_enumerate(dims, excitations=None): dims : list or array The quantum state dimensions array, as it would appear in a Qobj. - excitations : integer (None) + excitations : integer, optional Restrict state space to states with excitation numbers below or equal to this value. @@ -883,6 +920,9 @@ def state_number_qobj(dims, state, *, dtype=None): Return a Qobj representation of a quantum state specified by the state array `state`. + .. note:: + Deprecated in QuTiP 5.0, use :func:`basis` instead. + Example: >>> state_number_qobj([2, 2, 2], [1, 0, 1]) # doctest: +SKIP @@ -906,17 +946,14 @@ def state_number_qobj(dims, state, *, dtype=None): state : list State number array. - dtype : type or str - Storage representation. Any data-layer known to `qutip.data.to` is + dtype : type or str, optional + Storage representation. Any data-layer known to ``qutip.data.to`` is accepted. Returns ------- - state : :class:`qutip.Qobj` - The state as a :class:`qutip.Qobj` instance. - - .. note:: - Deprecated in QuTiP 5.0, use :func:`basis` instead. + state : :class:`.Qobj` + The state as a :class:`.Qobj` instance. """ dtype = dtype or settings.core["default_dtype"] or _data.Dense warnings.warn("basis() is a drop-in replacement for this", @@ -924,134 +961,6 @@ def state_number_qobj(dims, state, *, dtype=None): return basis(dims, state, dtype=dtype) -# Excitation-number restricted (enr) states - -def enr_state_dictionaries(dims, excitations): - """ - Return the number of states, and lookup-dictionaries for translating - a state tuple to a state index, and vice versa, for a system with a given - number of components and maximum number of excitations. - - Parameters - ---------- - dims: list - A list with the number of states in each sub-system. - - excitations : integer - The maximum numbers of dimension - - Returns - ------- - nstates, state2idx, idx2state: integer, dict, list - The number of states `nstates`, a dictionary for looking up state - indices from a state tuple, and a list containing the state tuples - ordered by state indices. state2idx and idx2state are reverses of - each other, i.e., state2idx[idx2state[idx]] = idx and - idx2state[state2idx[state]] = state. - """ - idx2state = list(state_number_enumerate(dims, excitations)) - state2idx = {state: idx for idx, state in enumerate(idx2state)} - nstates = len(idx2state) - - return nstates, state2idx, idx2state - - -def enr_fock(dims, excitations, state, *, dtype=None): - """ - Generate the Fock state representation in a excitation-number restricted - state space. The `dims` argument is a list of integers that define the - number of quantums states of each component of a composite quantum system, - and the `excitations` specifies the maximum number of excitations for - the basis states that are to be included in the state space. The `state` - argument is a tuple of integers that specifies the state (in the number - basis representation) for which to generate the Fock state representation. - - Parameters - ---------- - dims : list - A list of the dimensions of each subsystem of a composite quantum - system. - - excitations : integer - The maximum number of excitations that are to be included in the - state space. - - state : list of integers - The state in the number basis representation. - - dtype : type or str - Storage representation. Any data-layer known to `qutip.data.to` is - accepted. - - Returns - ------- - ket : Qobj - A Qobj instance that represent a Fock state in the exication-number- - restricted state space defined by `dims` and `exciations`. - - """ - dtype = dtype or settings.core["default_dtype"] or _data.Dense - nstates, state2idx, _ = enr_state_dictionaries(dims, excitations) - try: - data =_data.one_element[dtype]((nstates, 1), - (state2idx[tuple(state)], 0), 1) - except KeyError: - msg = ( - "The state tuple " + str(tuple(state)) - + " is not in the restricted state space." - ) - raise ValueError(msg) from None - return Qobj(data, dims=[dims, [1]*len(dims)], type='ket', copy=False) - - -def enr_thermal_dm(dims, excitations, n, *, dtype=None): - """ - Generate the density operator for a thermal state in the excitation-number- - restricted state space defined by the `dims` and `exciations` arguments. - See the documentation for enr_fock for a more detailed description of - these arguments. The temperature of each mode in dims is specified by - the average number of excitatons `n`. - - Parameters - ---------- - dims : list - A list of the dimensions of each subsystem of a composite quantum - system. - - excitations : integer - The maximum number of excitations that are to be included in the - state space. - - n : integer - The average number of exciations in the thermal state. `n` can be - a float (which then applies to each mode), or a list/array of the same - length as dims, in which each element corresponds specifies the - temperature of the corresponding mode. - - dtype : type or str - Storage representation. Any data-layer known to `qutip.data.to` is - accepted. - - Returns - ------- - dm : Qobj - Thermal state density matrix. - """ - dtype = dtype or settings.core["default_dtype"] or _data.CSR - nstates, _, idx2state = enr_state_dictionaries(dims, excitations) - if not isinstance(n, (list, np.ndarray)): - n = np.ones(len(dims)) * n - else: - n = np.asarray(n) - - diags = [np.prod((n / (n + 1)) ** np.array(state)) for state in idx2state] - diags /= np.sum(diags) - out = qdiags(diags, 0, dims=[dims, dims], - shape=(nstates, nstates), dtype=dtype) - out._isherm = True - return out - - def phase_basis(N, m, phi0=0, *, dtype=None): """ Basis vector for the mth phase of the Pegg-Barnett phase operator. @@ -1059,17 +968,17 @@ def phase_basis(N, m, phi0=0, *, dtype=None): Parameters ---------- N : int - Number of basis vectors in Hilbert space. + Number of basis states in Hilbert space. m : int Integer corresponding to the mth discrete phase - phi_m = phi0 + 2 * pi * m / N + ``phi_m = phi0 + 2 * pi * m / N`` - phi0 : float (default=0) + phi0 : float, default: 0 Reference phase angle. - dtype : type or str - Storage representation. Any data-layer known to `qutip.data.to` is + dtype : type or str, optional + Storage representation. Any data-layer known to ``qutip.data.to`` is accepted. Returns @@ -1087,23 +996,21 @@ def phase_basis(N, m, phi0=0, *, dtype=None): phim = phi0 + (2.0 * np.pi * m) / N n = np.arange(N)[:, np.newaxis] data = np.exp(1.0j * n * phim) / np.sqrt(N) - return Qobj(data, dims=[[N], [1]], type='ket', copy=False).to(dtype) + return Qobj(data, dims=[[N], [1]], copy=False).to(dtype) -def zero_ket(N, dims=None, *, dtype=None): +def zero_ket(dimensions, *, dtype=None): """ Creates the zero ket vector with shape Nx1 and dimensions `dims`. Parameters ---------- - N : int - Hilbert space dimensionality - dims : list - Optional dimensions if ket corresponds to - a composite Hilbert space. + dimensions : int or list of ints, Space + Number of basis states in Hilbert space. If a list, then the resultant + object will be a tensor product over spaces with those dimensions. - dtype : type or str - Storage representation. Any data-layer known to `qutip.data.to` is + dtype : type or str, optional + Storage representation. Any data-layer known to ``qutip.data.to`` is accepted. Returns @@ -1113,7 +1020,10 @@ def zero_ket(N, dims=None, *, dtype=None): """ dtype = dtype or settings.core["default_dtype"] or _data.Dense - return Qobj(_data.zeros[dtype](N, 1), dims=dims, type='ket', copy=False) + dimensions = _to_space(dimensions) + N = dimensions.size + return Qobj(_data.zeros[dtype](N, 1), + dims=[dimensions, dimensions.scalar_like()], copy=False) def spin_state(j, m, type='ket', *, dtype=None): @@ -1128,11 +1038,11 @@ def spin_state(j, m, type='ket', *, dtype=None): m : int Eigenvalue of the spin-j Sz operator. - type : string {'ket', 'bra', 'dm'} + type : string {'ket', 'bra', 'dm'}, default: 'ket' Type of state to generate. - dtype : type or str - Storage representation. Any data-layer known to `qutip.data.to` is + dtype : type or str, optional + Storage representation. Any data-layer known to ``qutip.data.to`` is accepted. Returns @@ -1167,11 +1077,11 @@ def spin_coherent(j, theta, phi, type='ket', *, dtype=None): phi : float Angle from x axis. - type : string {'ket', 'bra', 'dm'} + type : string {'ket', 'bra', 'dm'}, default: 'ket' Type of state to generate. - dtype : type or str - Storage representation. Any data-layer known to `qutip.data.to` is + dtype : type or str, optional + Storage representation. Any data-layer known to ``qutip.data.to`` is accepted. Returns @@ -1222,11 +1132,11 @@ def bell_state(state='00', *, dtype=None): Parameters ---------- - state : str ['00', '01', `10`, `11`] + state : str ['00', '01', '10', '11'] Which bell state to return - dtype : type or str - Storage representation. Any data-layer known to `qutip.data.to` is + dtype : type or str, optional + Storage representation. Any data-layer known to ``qutip.data.to`` is accepted. Returns @@ -1250,8 +1160,8 @@ def singlet_state(*, dtype=None): Parameters ---------- - dtype : type or str - Storage representation. Any data-layer known to `qutip.data.to` is + dtype : type or str, optional + Storage representation. Any data-layer known to ``qutip.data.to`` is accepted. Returns @@ -1275,8 +1185,8 @@ def triplet_states(*, dtype=None): Parameters ---------- - dtype : type or str - Storage representation. Any data-layer known to `qutip.data.to` is + dtype : type or str, optional + Storage representation. Any data-layer known to ``qutip.data.to`` is accepted. Returns @@ -1284,53 +1194,60 @@ 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), ] -def w_state(N=3, *, dtype=None): +def w_state(N_qubit, *, dtype=None): """ Returns the N-qubit W-state: ``[ |100..0> + |010..0> + |001..0> + ... |000..1> ] / sqrt(n)`` Parameters ---------- - N : int (default=3) + N_qubit : int Number of qubits in state - dtype : type or str - Storage representation. Any data-layer known to `qutip.data.to` is + dtype : type or str, optional + Storage representation. Any data-layer known to ``qutip.data.to`` is accepted. Returns ------- - W : :obj:`~Qobj` + W : :obj:`.Qobj` N-qubit W-state """ - inds = np.zeros(N, dtype=int) + + dtype = dtype or settings.core["default_dtype"] or _data.Dense + inds = np.zeros(N_qubit, 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 + state = basis([2]*N_qubit, list(inds), dtype=dtype) + for kk in range(1, N_qubit): + state += basis([2] * N_qubit, list(np.roll(inds, kk)), dtype=dtype) + return (np.sqrt(1 / N_qubit) * state).to(dtype) -def ghz_state(N=3, *, dtype=None): +def ghz_state(N_qubit, *, dtype=None): """ Returns the N-qubit GHZ-state: ``[ |00...00> + |11...11> ] / sqrt(2)`` Parameters ---------- - N : int (default=3) + N_qubit : int Number of qubits in state - dtype : type or str - Storage representation. Any data-layer known to `qutip.data.to` is + dtype : type or str, optional + Storage representation. Any data-layer known to ``qutip.data.to`` is accepted. Returns @@ -1338,5 +1255,11 @@ 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_qubit, [0] * N_qubit, dtype=dtype) + + basis([2] * N_qubit, [1] * N_qubit, dtype=dtype) + ) + ).to(dtype) diff --git a/qutip/core/subsystem_apply.py b/qutip/core/subsystem_apply.py index 013bcbc7d2..e7bea30a2f 100644 --- a/qutip/core/subsystem_apply.py +++ b/qutip/core/subsystem_apply.py @@ -21,10 +21,10 @@ def subsystem_apply(state, channel, mask, reference=False): Parameters ---------- - state : :class:`qutip.qobj` + state : :class:`.Qobj` A density matrix or ket. - channel : :class:`qutip.qobj` + channel : :class:`.Qobj` A propagator, either an `oper` or `super`. mask : *list* / *array* @@ -37,7 +37,7 @@ def subsystem_apply(state, channel, mask, reference=False): Returns ------- - rho_out: :class:`qutip.qobj` + rho_out: :class:`.Qobj` A density matrix with the selected subsystems transformed according to the specified channel. """ diff --git a/qutip/core/superop_reps.py b/qutip/core/superop_reps.py index c48f05fd1a..9a5d7222c3 100644 --- a/qutip/core/superop_reps.py +++ b/qutip/core/superop_reps.py @@ -59,7 +59,6 @@ def _superpauli_basis(nq=1): sci.indptr[-1] = nnz return Qobj(data.adjoint(), dims=dims, - type='super', superrep='super', isherm=False, isunitary=False, @@ -132,36 +131,59 @@ def _choi_to_kraus(q_oper, tol=1e-9): dims = [q_oper.dims[0][1], q_oper.dims[0][0]] shape = (np.prod(q_oper.dims[0][1]), np.prod(q_oper.dims[0][0])) return [Qobj(_data.mul(unstack_columns(vec.data, shape=shape), np.sqrt(val)), - dims=dims, type='oper', copy='False') + dims=dims, copy='False') for val, vec in zip(vals, vecs) if abs(val) >= tol] # Individual conversions from Kraus operators are public because the output # list of Kraus operators is not itself a quantum object. -def kraus_to_choi(kraus_list): - """ - Take a list of Kraus operators and returns the Choi matrix for the channel - represented by the Kraus operators in `kraus_list` +def kraus_to_choi(kraus_ops): + r""" + Convert a list of Kraus operators into Choi representation of the channel. + + Essentially, kraus operators are a decomposition of a Choi matrix, + and its reconstruction from them should go as + :math:`E = \sum_{i} |K_i\rangle\rangle \langle\langle K_i|`, + where we use vector representation of Kraus operators. + + Parameters + ---------- + kraus_ops : list[Qobj] + The list of Kraus operators to be converted to Choi representation. + + Returns + ------- + choi : Qobj + A quantum object representing the same map as ``kraus_ops``, such that + ``choi.superrep == "choi"``. """ - kraus_mat_list = [k.full() for k in kraus_list] - op_rng = list(range(kraus_mat_list[0].shape[1])) - choi_blocks = np.array( - [[sum(op[:, c_ix, None] @ np.conj(op[None, :, r_ix]) - for op in kraus_mat_list) - for r_ix in op_rng] - for c_ix in op_rng] + len_op = np.prod(kraus_ops[0].shape) + # If Kraus ops have dims [M, N] in qutip notation (act on [N, N] density + # matrix and produce [M, M] d.m.), Choi matrix Hilbert space will + # be [[M, N], [M, N]] because Choi Hilbert space + # is (output space) x (input space). + choi_dims = [kraus_ops[0].dims] * 2 + # transform a list of Qobj matrices list[sum_ij k_ij |i>> = sum_I k_I |I>> + kraus_vectors = np.asarray( + [np.reshape(kraus_op.full(), len_op, "F") for kraus_op in kraus_ops] ) - return Qobj(np.hstack(np.hstack(choi_blocks)), - dims=[kraus_list[0].dims[::-1]]*2, - type='super', - superrep='choi', - copy=False) + # sum_{I} |k_I|^2 |I>><`_ - and `Photon-mediated electron transport in hybrid circuit-QED `_. - This parameter is deprecated and may be removed in QuTiP 5. - - data_only : bool [False] + chi : float, optional + In some systems it is possible to determine the statistical moments + (mean, variance, etc) of the probability distribution of the occupation + numbers of states by numerically evaluating the derivatives of the + steady state occupation probability as a function of an artificial + phase parameter ``chi`` which multiplies the ``a \\rho a^dagger`` term + of the dissipator by ``e ^ (i * chi)``. The factor ``e ^ (i * chi)`` is + introduced via the generating function of the statistical moments. For + examples of the technique, see `Full counting statistics of + nano-electromechanical systems + `_ and `Photon-mediated + electron transport in hybrid circuit-QED + `_. This parameter is deprecated and + may be removed in QuTiP 5. + + data_only : bool, default: False Return the data object instead of a Qobj Returns @@ -200,7 +207,6 @@ def operator_to_vector(op): "in super representation") return Qobj(stack_columns(op.data), dims=[op.dims, [1]], - type='operator-ket', superrep="super", copy=False) @@ -314,7 +320,6 @@ def spost(A): data = _data.kron_transpose(A.data, _data.identity_like(A.data)) return Qobj(data, dims=[A.dims, A.dims], - type='super', superrep='super', isherm=A._isherm, copy=False) @@ -338,8 +343,7 @@ def spre(A): raise TypeError('Input is not a quantum operator') data = _data.kron(_data.identity_like(A.data), A.data) return Qobj(data, - dims=[A.dims, A.dims], - type='super', + dims=[A._dims, A._dims], superrep='super', isherm=A._isherm, copy=False) @@ -381,32 +385,103 @@ def sprepost(A, B): _drop_projected_dims(B.dims[0])]] return Qobj(_data.kron_transpose(B.data, A.data), dims=dims, - type='super', superrep='super', isherm=A._isherm and B._isherm, copy=False) +def _to_super_of_tensor(q_oper): + """ + Transform a superoperator composed of multiple space into a superoperator + over a composite spaces. + """ + msg = "Reshuffling is only supported for square operators." + if not q_oper._dims.issuper: + raise TypeError("Reshuffling is only supported on type='super' " + "or type='operator-ket'.") + if q_oper.isoper and not q_oper._dims.issquare: + raise NotImplementedError(msg) + + dims = q_oper._dims[0] + if isinstance(dims, SuperSpace): + return q_oper.copy() + + perm_idxs = [[], []] + + if isinstance(dims, Compound): + shift = 0 + for space in dims.spaces: + if not isinstance(space, SuperSpace) or not space.oper.issquare: + raise NotImplementedError(msg) + space_dims = space.oper.to_ + if type(space_dims) is Space: + perm_idxs[0] += [shift] + perm_idxs[1] += [shift + 1] + shift += 2 + elif isinstance(space_dims, Compound): + N = len(space_dims.spaces) + perm_idxs[0] += [shift + i for i in range(N)] + perm_idxs[1] += [shift + N + i for i in range(N)] + shift += 2 * N + else: + # ENR space or other complex spaces + raise NotImplementedError("Reshuffling with non standard space" + "is not supported.") + + return q_oper.permute(perm_idxs) + + +def _to_tensor_of_super(q_oper): + """ + Transform a superoperator composed of multiple space into a tensor of + superoperator on each spaces. + """ + msg = "Reshuffling is only supported for square operators." + if not q_oper._dims[0].issuper: + raise TypeError("Reshuffling is only supported on type='super' " + "or type='operator-ket'.") + + dims = q_oper._dims[0] + perm_idxs = [] + + if isinstance(dims, Compound): + shift = 0 + for space in dims.spaces: + if not isinstance(space, SuperSpace) or not space.oper.issquare: + raise TypeError(msg) + space_dims = space.oper.to_ + if type(space_dims) is Space: + perm_idxs += [[shift], [shift + 1]] + shift += 2 + elif isinstance(space_dims, Compound): + N = len(space_dims.spaces) + idxs = range(0, N * 2, 2) + perm_idxs += [[i + shift] for i in idxs] + perm_idxs += [[i + shift + 1] for i in idxs] + shift += N * 2 + else: + # ENR space or other complex spaces + raise NotImplementedError("Reshuffling with non standard space" + "is not supported.") + elif isinstance(dims, SuperSpace): + if isinstance(dims.oper.to_, Compound): + step = len(dims.oper.to_.spaces) + perm_idxs = sum([[[i], [i+step]] for i in range(step)], []) + else: + return q_oper + + return q_oper.permute(perm_idxs) + + def reshuffle(q_oper): """ - Column-reshuffles a ``type="super"`` Qobj. + Column-reshuffles a super operator or a operator-ket Qobj. """ - if q_oper.type not in ('super', 'operator-ket'): + if q_oper.type not in ["super", "operator-ket"]: raise TypeError("Reshuffling is only supported on type='super' " "or type='operator-ket'.") - # How many indices are there, and how many subsystems can we decompose - # each index into? - n_indices = len(q_oper.dims[0]) - n_subsystems = len(q_oper.dims[0][0]) - # Generate a list of lists (lol) that represents the permutation order we - # need. It's easiest to do so if we make an array, then turn it into a lol - # by using map(list, ...). That array is generated by using reshape and - # transpose to turn an array like [a, b, a, b, ..., a, b] into one like - # [a, a, ..., a, b, b, ..., b]. - perm_idxs = map(list, - np.arange(n_subsystems * n_indices)[ - np.arange(n_subsystems * n_indices).reshape( - (n_indices, n_subsystems)).T.flatten() - ].reshape((n_subsystems, n_indices)) - ) - return q_oper.permute(list(perm_idxs)) + + if isinstance(q_oper._dims[0], Compound): + return _to_super_of_tensor(q_oper) + else: + return _to_tensor_of_super(q_oper) diff --git a/qutip/core/tensor.py b/qutip/core/tensor.py index 3a3661752c..b9fde85fb9 100644 --- a/qutip/core/tensor.py +++ b/qutip/core/tensor.py @@ -83,26 +83,25 @@ def tensor(*args): "In tensor products of superroperators,", " all must have the same representation" ])) - type = args[0].type + isherm = args[0]._isherm isunitary = args[0]._isunitary out_data = args[0].data - dims_l = [d for arg in args for d in arg.dims[0]] - dims_r = [d for arg in args for d in arg.dims[1]] + dims_l = [args[0]._dims[0]] + dims_r = [args[0]._dims[1]] for arg in args[1:]: out_data = _data.kron(out_data, arg.data) # If both _are_ Hermitian and/or unitary, then so is the output, but if # both _aren't_, then output still can be. isherm = (isherm and arg._isherm) or None isunitary = (isunitary and arg._isunitary) or None - if arg.type != type: - type = None + dims_l.append(arg._dims[0]) + dims_r.append(arg._dims[1]) + return Qobj(out_data, dims=[dims_l, dims_r], - type=type, isherm=isherm, isunitary=isunitary, - superrep=args[0].superrep, copy=False) @@ -291,6 +290,8 @@ def tensor_contract(qobj, *pairs): Parameters ---------- + qobj: Qobj + Operator to contract subspaces on. pairs : tuple One or more tuples ``(i, j)`` indicating that the @@ -348,7 +349,7 @@ def _check_oper_dims(oper, dims=None, targets=None): Parameters ---------- - oper : :class:`qutip.Qobj` + oper : :class:`.Qobj` The quantum object to be checked. dims : list, optional A list of integer for the dimension of each composite system. @@ -379,8 +380,8 @@ def _targets_to_list(targets, oper=None, N=None): ---------- targets : int or list of int The indices of subspace that are acted on. - oper : :class:`qutip.Qobj`, optional - An operator, the type of the :class:`qutip.Qobj` + oper : :class:`.Qobj`, optional + An operator, the type of the :class:`.Qobj` has to be an operator and the dimension matches the tensored qubit Hilbert space e.g. dims = ``[[2, 2, 2], [2, 2, 2]]`` @@ -425,7 +426,7 @@ def expand_operator(oper, dims, targets): Parameters ---------- - oper : :class:`qutip.Qobj` + oper : :class:`.Qobj` An operator that act on the subsystem, has to be an operator and the dimension matches the tensored dims Hilbert space e.g. oper.dims = ``[[2, 3], [2, 3]]`` @@ -437,7 +438,7 @@ def expand_operator(oper, dims, targets): Returns ------- - expanded_oper : :class:`qutip.Qobj` + expanded_oper : :class:`.Qobj` The expanded operator acting on a system with desired dimension. """ from .operators import identity diff --git a/qutip/entropy.py b/qutip/entropy.py index cd62a69b76..6f3f5cc7a6 100644 --- a/qutip/entropy.py +++ b/qutip/entropy.py @@ -17,9 +17,9 @@ def entropy_vn(rho, base=e, sparse=False): ---------- rho : qobj Density matrix. - base : {e,2} + base : {e, 2}, default: e Base of logarithm. - sparse : {False,True} + sparse : bool, default: False Use sparse eigensolver. Returns @@ -90,7 +90,7 @@ def concurrence(rho): References ---------- - .. [1] https://en.wikipedia.org/wiki/Concurrence_(quantum_computing) + .. [1] `https://en.wikipedia.org/wiki/Concurrence_(quantum_computing)` """ if rho.isket and rho.dims != [[2, 2], [1, 1]]: @@ -159,9 +159,9 @@ def entropy_mutual(rho, selA, selB, base=e, sparse=False): `int` or `list` of first selected density matrix components. selB : int/list `int` or `list` of second selected density matrix components. - base : {e,2} + base : {e, 2}, default: e Base of logarithm. - sparse : {False,True} + sparse : bool, default: False Use sparse eigensolver. Returns @@ -195,18 +195,18 @@ def entropy_relative(rho, sigma, base=e, sparse=False, tol=1e-12): Parameters ---------- - rho : :class:`qutip.Qobj` + rho : :class:`.Qobj` First density matrix (or ket which will be converted to a density matrix). - sigma : :class:`qutip.Qobj` + sigma : :class:`.Qobj` Second density matrix (or ket which will be converted to a density matrix). - base : {e,2} + base : {e, 2}, default: e Base of logarithm. Defaults to e. - sparse : bool + sparse : bool, default: False Flag to use sparse solver when determining the eigenvectors of the density matrices. Defaults to False. - tol : float + tol : float, default: 1e-12 Tolerance to use to detect 0 eigenvalues or dot producted between eigenvectors. Defaults to 1e-12. @@ -295,9 +295,9 @@ def entropy_conditional(rho, selB, base=e, sparse=False): Density matrix of composite object selB : int/list Selected components for density matrix B - base : {e,2} + base : {e, 2}, default: e Base of logarithm. - sparse : {False,True} + sparse : bool, default: False Use sparse eigensolver. Returns diff --git a/qutip/fileio.py b/qutip/fileio.py index ac94dd41f5..8f20eea862 100644 --- a/qutip/fileio.py +++ b/qutip/fileio.py @@ -20,11 +20,11 @@ def file_data_store(filename, data, numtype="complex", numformat="decimal", Name of data file to be stored, including extension. data: array_like Data to be written to file. - numtype : str {'complex, 'real'} + numtype : str {'complex, 'real'}, default: 'complex' Type of numerical data. - numformat : str {'decimal','exp'} + numformat : str {'decimal','exp'}, default: 'decimal' Format for written data. - sep : str + sep : str, default: ',' Single-character field seperator. Usually a tab, space, comma, or semicolon. @@ -114,7 +114,7 @@ def file_data_read(filename, sep=None): ---------- filename : str or pathlib.Path Name of file containing reqested data. - sep : str + sep : str, optional Seperator used to store data. Returns @@ -217,7 +217,7 @@ def qsave(data, name='qutip_data'): ---------- data : instance/array_like Input Python object to be stored. - filename : str or pathlib.Path + filename : str or pathlib.Path, default: "qutip_data" Name of output data file. """ @@ -230,13 +230,13 @@ def qsave(data, name='qutip_data'): pickle.dump(data, fileObject) -def qload(name): +def qload(filename): """ - Loads data file from file named 'filename.qu' in current directory. + Loads data file from file ``filename`` in current directory. Parameters ---------- - name : str or pathlib.Path + filename : str or pathlib.Path Name of data file to be loaded. Returns @@ -245,7 +245,7 @@ def qload(name): Object retrieved from requested file. """ - path = Path(name) + path = Path(filename) path = path.with_suffix(path.suffix + ".qu") with open(path, "rb") as fileObject: diff --git a/qutip/ipynbtools.py b/qutip/ipynbtools.py index 48e2eeb14b..2424891922 100644 --- a/qutip/ipynbtools.py +++ b/qutip/ipynbtools.py @@ -56,10 +56,14 @@ def version_table(verbose=False): different packages that were used to run the notebook. This should make it possible to reproduce the environment and the calculation later on. + Parameters + ---------- + verbose : bool, default: False + Add extra information about install location. Returns ------- - version_table: string + version_table: str Return an HTML-formatted string containing version information for QuTiP dependencies. diff --git a/qutip/legacy/nonmarkov/memorycascade.py b/qutip/legacy/nonmarkov/memorycascade.py index 0cdf18cd3b..a069bca7b4 100644 --- a/qutip/legacy/nonmarkov/memorycascade.py +++ b/qutip/legacy/nonmarkov/memorycascade.py @@ -41,14 +41,14 @@ class MemoryCascade: Attributes ---------- - H_S : :class:`qutip.Qobj` + H_S : :class:`.Qobj` System Hamiltonian (can also be a Liouvillian) - L1 : :class:`qutip.Qobj` / list of :class:`qutip.Qobj` + L1 : :class:`.Qobj` / list of :class:`.Qobj` System operators coupling into the feedback loop. Can be a single operator or a list of operators. - L2 : :class:`qutip.Qobj` / list of :class:`qutip.Qobj` + L2 : :class:`.Qobj` / list of :class:`.Qobj` System operators coupling out of the feedback loop. Can be a single operator or a list of operators. L2 must have the same length as L1. @@ -57,7 +57,7 @@ class MemoryCascade: operators in L2 by the feedback channel. Defaults to an n by n identity matrix where n is the number of elements in L1/L2. - c_ops_markov : :class:`qutip.Qobj` / list of :class:`qutip.Qobj` + c_ops_markov : :class:`.Qobj` / list of :class:`.Qobj` Decay operators describing conventional Markovian decay channels. Can be a single operator or a list of operators. @@ -140,7 +140,7 @@ def propagator(self, t, tau, notrace=False): and a propagator for a single system is returned. Returns ------- - : :class:`qutip.Qobj` + : :class:`.Qobj` time-propagator for reduced system dynamics """ k = int(t / tau) + 1 @@ -186,12 +186,12 @@ def outfieldpropagator( tau : float time-delay - c1 : :class:`qutip.Qobj` + c1 : :class:`.Qobj` system collapse operator that couples to the in-loop field in question (only needs to be specified if self.L1 has more than one element) - c2 : :class:`qutip.Qobj` + c2 : :class:`.Qobj` system collapse operator that couples to the output field in question (only needs to be specified if self.L2 has more than one element) @@ -204,7 +204,7 @@ def outfieldpropagator( Returns ------- - : :class:`qutip.Qobj` + : :class:`.Qobj` time-propagator for computing field correlation function """ if c1 is None and len(self.L1) == 1: @@ -278,7 +278,7 @@ def rhot(self, rho0, t, tau): Parameters ---------- - rho0 : :class:`qutip.Qobj` + rho0 : :class:`.Qobj` initial density matrix or state vector (ket) t : float @@ -289,7 +289,7 @@ def rhot(self, rho0, t, tau): Returns ------- - : :class:`qutip.Qobj` + : :class:`.Qobj` density matrix at time :math:`t` """ if isket(rho0): @@ -307,7 +307,7 @@ def outfieldcorr(self, rho0, blist, tlist, tau, c1=None, c2=None): Parameters ---------- - rho0 : :class:`qutip.Qobj` + rho0 : :class:`.Qobj` initial density matrix or state vector (ket). blist : array_like @@ -325,12 +325,12 @@ def outfieldcorr(self, rho0, blist, tlist, tau, c1=None, c2=None): tau : float time-delay - c1 : :class:`qutip.Qobj` + c1 : :class:`.Qobj` system collapse operator that couples to the in-loop field in question (only needs to be specified if self.L1 has more than one element) - c2 : :class:`qutip.Qobj` + c2 : :class:`.Qobj` system collapse operator that couples to the output field in question (only needs to be specified if self.L2 has more than one element) diff --git a/qutip/legacy/rcsolve.py b/qutip/legacy/rcsolve.py index b171946ba2..ee7796ddfb 100644 --- a/qutip/legacy/rcsolve.py +++ b/qutip/legacy/rcsolve.py @@ -33,7 +33,7 @@ def rcsolve(Hsys, psi0, tlist, e_ops, Q, wc, alpha, N, w_th, sparse=False, Initial state of the system. tlist: List. Time over which system evolves. - e_ops: list of :class:`qutip.Qobj` / callback function single + e_ops: list of :class:`.Qobj` / callback function single Single operator or list of operators for which to evaluate expectation values. Q: Qobj diff --git a/qutip/matplotlib_utilities.py b/qutip/matplotlib_utilities.py index 3181b29192..869a5deaaa 100644 --- a/qutip/matplotlib_utilities.py +++ b/qutip/matplotlib_utilities.py @@ -27,24 +27,24 @@ def wigner_cmap(W, levels=1024, shift=0, max_color='#09224F', ---------- W : array Wigner function array, or any array. - levels : int + levels : int, default: 1024 Number of color levels to create. - shift : float + shift : float, default: 0 Shifts the value at which Wigner elements are emphasized. This parameter should typically be negative and small (i.e -1e-5). - max_color : str + max_color : str, default: '#09224F' String for color corresponding to maximum value of data. Accepts any string format compatible with the Matplotlib.colors.ColorConverter. - mid_color : str + mid_color : str, default: '#FFFFFF' Color corresponding to zero values. Accepts any string format compatible with the Matplotlib.colors.ColorConverter. - min_color : str + min_color : str, default: '#530017' Color corresponding to minimum data values. Accepts any string format compatible with the Matplotlib.colors.ColorConverter. - neg_color : str + neg_color : str, default: '#FF97D4' Color that starts highlighting negative values. Accepts any string format compatible with the Matplotlib.colors.ColorConverter. - invert : bool + invert : bool, default: False Invert the color scheme for negative values so that smaller negative values have darker color. diff --git a/qutip/measurement.py b/qutip/measurement.py index da90387dd0..5b29c40836 100644 --- a/qutip/measurement.py +++ b/qutip/measurement.py @@ -13,7 +13,7 @@ import numpy as np -from . import Qobj, expect, identity, tensor +from . import Qobj, expect, identity, tensor, settings def _verify_input(op, state): @@ -36,7 +36,7 @@ def _verify_input(op, state): raise ValueError("state must be a ket or a density matrix") -def _measurement_statistics_povm_ket(state, ops): +def _measurement_statistics_povm_ket(state, ops, tol=None): r""" Returns measurement statistics (resultant states and probabilities) for a measurements specified by a set of positive operator valued @@ -51,6 +51,9 @@ def _measurement_statistics_povm_ket(state, ops): List of measurement operators :math:`M_i` (specifying a POVM such that :math:`E_i = M_i^\dagger M_i`). + tol : float, optional + Smallest value for the probabilities. Smaller probabilities will be + rounded to ``0``. Returns ------- @@ -65,19 +68,23 @@ def _measurement_statistics_povm_ket(state, ops): """ probabilities = [] collapsed_states = [] + if tol is None: + tol = settings.core["atol"] for i, op in enumerate(ops): - p = np.absolute((state.dag() * op.dag() * op * state)) - probabilities.append(p) - if p != 0: - collapsed_states.append((op * state) / np.sqrt(p)) + psi = op * state + p = np.absolute(psi.overlap(psi)) + if p >= tol: + collapsed_states.append(psi / np.sqrt(p)) + probabilities.append(p) else: collapsed_states.append(None) + probabilities.append(0.) return collapsed_states, probabilities -def _measurement_statistics_povm_dm(density_mat, ops): +def _measurement_statistics_povm_dm(density_mat, ops, tol=None): r""" Returns measurement statistics (resultant states and probabilities) for a measurements specified by a set of positive operator valued @@ -92,6 +99,10 @@ def _measurement_statistics_povm_dm(density_mat, ops): List of measurement operators :math:`M_i` (specifying a POVM s.t. :mathm:`E_i = M_i^\dagger M_i`) + tol : float, optional + Smallest value for the probabilities. Smaller probabilities will be + rounded to ``0``. + Returns ------- collapsed_states : list of :class:`.Qobj` @@ -105,20 +116,24 @@ def _measurement_statistics_povm_dm(density_mat, ops): """ probabilities = [] collapsed_states = [] + if tol is None: + tol = settings.core["atol"] for i, op in enumerate(ops): st = op * density_mat * op.dag() p = st.tr() - probabilities.append(p) - if p != 0: + if p >= tol: collapsed_states.append(st/p) + probabilities.append(p) else: collapsed_states.append(None) + probabilities.append(0.) + return collapsed_states, probabilities -def measurement_statistics_povm(state, ops): +def measurement_statistics_povm(state, ops, tol=None): r""" Returns measurement statistics (resultant states and probabilities) for a measurement specified by a set of positive operator valued measurements on @@ -137,6 +152,10 @@ def measurement_statistics_povm(state, ops): projectors (s.t. :math:`E_i = M_i^\dagger = M_i`) 3. kets (transformed to projectors) + tol : float, optional + Smallest value for the probabilities. Smaller probabilities will be + rounded to ``0``. Default is qutip's core settings' ``atol``. + Returns ------- collapsed_states : list of :class:`.Qobj` @@ -160,12 +179,12 @@ def measurement_statistics_povm(state, ops): raise ValueError("measurement operators must sum to identity") if state.isket: - return _measurement_statistics_povm_ket(state, ops) + return _measurement_statistics_povm_ket(state, ops, tol) else: - return _measurement_statistics_povm_dm(state, ops) + return _measurement_statistics_povm_dm(state, ops, tol) -def measurement_statistics_observable(state, op): +def measurement_statistics_observable(state, op, tol=None): """ Return the measurement eigenvalues, eigenstates (or projectors) and measurement probabilities for the given state and measurement operator. @@ -178,33 +197,57 @@ def measurement_statistics_observable(state, op): op : :class:`.Qobj` The measurement operator. + tol : float, optional + Smallest value for the probabilities. + Default is qutip's core settings' ``atol``. + Returns ------- eigenvalues: list of float The list of eigenvalues of the measurement operator. - eigenstates_or_projectors: list of :class:`.Qobj` - If the state was a ket, return the eigenstates of the measurement - operator. Otherwise return the projectors onto the eigenstates. + projectors: list of :class:`.Qobj` + Return the projectors onto the eigenstates. probabilities: list of float The probability of measuring the state as being in the corresponding eigenstate (and the measurement result being the corresponding eigenvalue). """ + probabilities = [] + values = [] + projectors = [] _verify_input(op, state) + if tol is None: + tol = settings.core["atol"] eigenvalues, eigenstates = op.eigenstates() - if state.isket: - probabilities = [abs(e.overlap(state))**2 for e in eigenstates] - return eigenvalues, eigenstates, probabilities - else: - projectors = [e.proj() for e in eigenstates] - probabilities = [expect(v, state) for v in projectors] - return eigenvalues, projectors, probabilities + # Detect groups of eigenvalues within atol of each other. + # A group will be [False] * N + [True] + groups = np.append(np.diff(eigenvalues) >= tol, True) + + present_group = [] + for i in range(len(eigenvalues)): + present_group.append(i) + if not groups[i]: + continue + projector = 0 + for j in present_group: + projector += eigenstates[j].proj() + probability = expect(projector, state) -def measure_observable(state, op): + if probability >= tol: + probabilities.append(probability) + values.append(np.mean(eigenvalues[present_group])) + projectors.append(projector) + + present_group = [] + + return values, projectors, probabilities + + +def measure_observable(state, op, tol=None): """ Perform a measurement specified by an operator on the given state. @@ -221,6 +264,10 @@ def measure_observable(state, op): op : :class:`.Qobj` The measurement operator. + tol : float, optional + Smallest value for the probabilities. + Default is qutip's core settings' ``atol``. + Returns ------- measured_value : float @@ -269,19 +316,17 @@ def measure_observable(state, op): The measurement result is the same, but the new state is returned as a density matrix. """ - eigenvalues, eigenstates_or_projectors, probabilities = ( - measurement_statistics_observable(state, op)) - i = np.random.choice(range(len(eigenvalues)), p=probabilities) + eigenvalues, projectors, probabilities = ( + measurement_statistics_observable(state, op, tol)) + i = np.random.choice(len(eigenvalues), p=probabilities) if state.isket: - eigenstates = eigenstates_or_projectors - state = eigenstates[i] + state = (projectors[i] * state) / probabilities[i]**0.5 else: - projectors = eigenstates_or_projectors state = (projectors[i] * state * projectors[i]) / probabilities[i] return eigenvalues[i], state -def measure_povm(state, ops): +def measure_povm(state, ops, tol=None): r""" Perform a measurement specified by list of POVMs. @@ -303,6 +348,10 @@ def measure_povm(state, ops): :math:`E_i = M_i^\dagger = M_i`) 3. kets (transformed to projectors) + tol : float, optional + Smallest value for the probabilities. + Default is qutip's core settings' ``atol``. + Returns ------- index : float @@ -311,13 +360,14 @@ def measure_povm(state, ops): state : :class:`.Qobj` The new state (a ket if a ket was given, otherwise a density matrix). """ - collapsed_states, probabilities = measurement_statistics_povm(state, ops) - index = np.random.choice(range(len(collapsed_states)), p=probabilities) + collapsed_states, probabilities = ( + measurement_statistics_povm(state, ops, tol)) + index = np.random.choice(len(collapsed_states), p=probabilities) state = collapsed_states[index] return index, state -def measurement_statistics(state, ops): +def measurement_statistics(state, ops, tol=None): r""" A dispatch method that provides measurement statistics handling both observable style measurements and projector style measurements(POVMs and @@ -342,14 +392,18 @@ def measurement_statistics(state, ops): 2. projection operators if ops correspond to projectors (s.t. :math:`E_i = M_i^\dagger = M_i`) 3. kets (transformed to projectors) + + tol : float, optional + Smallest value for the probabilities. + Default is qutip's core settings' ``atol``. """ if isinstance(ops, list): - return measurement_statistics_povm(state, ops) + return measurement_statistics_povm(state, ops, tol) else: - return measurement_statistics_observable(state, ops) + return measurement_statistics_observable(state, ops, tol) -def measure(state, ops): +def measure(state, ops, tol=None): r""" A dispatch method that provides measurement results handling both observable style measurements and projector style measurements (POVMs and @@ -374,8 +428,12 @@ def measure(state, ops): 2. projection operators if ops correspond to projectors (s.t. :math:`E_i = M_i^\dagger = M_i`) 3. kets (transformed to projectors) + + tol : float, optional + Smallest value for the probabilities. + Default is qutip's core settings' ``atol``. """ if isinstance(ops, list): - return measure_povm(state, ops) + return measure_povm(state, ops, tol) else: - return measure_observable(state, ops) + return measure_observable(state, ops, tol) diff --git a/qutip/partial_transpose.py b/qutip/partial_transpose.py index 61871369d8..919bfa9e63 100644 --- a/qutip/partial_transpose.py +++ b/qutip/partial_transpose.py @@ -23,22 +23,20 @@ def partial_transpose(rho, mask, method='dense'): Parameters ---------- - rho : :class:`qutip.qobj` + rho : :class:`.Qobj` A density matrix. mask : *list* / *array* A mask that selects which subsystems should be transposed. - method : str - choice of method, `dense` or `sparse`. The default method - is `dense`. The `sparse` implementation can be faster for + method : str {"dense", "sparse"}, default: "dense" + Choice of method. The "sparse" implementation can be faster for large and sparse systems (hundreds of quantum states). Returns ------- - rho_pr: :class:`qutip.qobj` - + rho_pr: :class:`.Qobj` A density matrix with the selected subsystems transposed. """ diff --git a/qutip/piqs/_piqs.pyx b/qutip/piqs/_piqs.pyx index fcb614dccd..07096e192b 100644 --- a/qutip/piqs/_piqs.pyx +++ b/qutip/piqs/_piqs.pyx @@ -286,7 +286,7 @@ cdef class Dicke(object): Returns ---------- - lindblad_qobj: :class: qutip.Qobj + lindblad_qobj: :class:`.Qobj` The matrix size is (nds**2, nds**2) where nds is the number of Dicke states. """ diff --git a/qutip/piqs/piqs.py b/qutip/piqs/piqs.py index a8f14e7668..73b3995581 100644 --- a/qutip/piqs/piqs.py +++ b/qutip/piqs/piqs.py @@ -156,18 +156,19 @@ def isdiagonal(mat): # nonlinear functions of the density matrix def dicke_blocks(rho): - """Create the list of blocks for block-diagonal density matrix in the Dicke basis. + """Create the list of blocks for block-diagonal density matrix in the Dicke + basis. Parameters ---------- - rho : :class:`qutip.Qobj` + rho : :class:`.Qobj` A 2D block-diagonal matrix of ones with dimension (nds,nds), where nds is the number of Dicke states for N two-level systems. Returns ------- - square_blocks: list of np.array + square_blocks: list of np.ndarray Give back the blocks list. """ @@ -199,7 +200,7 @@ def dicke_blocks_full(rho): Parameters ---------- - rho : :class:`qutip.Qobj` + rho : :class:`.Qobj` A 2D block-diagonal matrix of ones with dimension (nds,nds), where nds is the number of Dicke states for N two-level systems. @@ -244,7 +245,7 @@ def dicke_function_trace(f, rho): f : function A Taylor-expandable function of `rho`. - rho : :class:`qutip.Qobj` + rho : :class:`.Qobj` A density matrix in the Dicke basis. Returns @@ -280,8 +281,8 @@ def entropy_vn_dicke(rho): Parameters ---------- - rho : :class:`qutip.Qobj` - A 2D block-diagonal matrix of ones with dimension (nds,nds), + rho : :class:`.Qobj` + A 2D block-diagonal matrix of ones with dimension (nds, nds), where nds is the number of Dicke states for N two-level systems. @@ -300,7 +301,7 @@ def purity_dicke(rho): Parameters ---------- - rho : :class:`qutip.Qobj` + rho : :class:`.Qobj` Density matrix in the Dicke basis of qutip.piqs.jspin(N), for N spins. Returns @@ -335,7 +336,7 @@ class Dicke(object): N: int The number of two-level systems. - hamiltonian : :class:`qutip.Qobj` + hamiltonian : :class:`.Qobj` A Hamiltonian in the Dicke basis. The matrix dimensions are (nds, nds), @@ -372,7 +373,7 @@ class Dicke(object): N: int The number of two-level systems. - hamiltonian : :class:`qutip.Qobj` + hamiltonian : :class:`.Qobj` A Hamiltonian in the Dicke basis. The matrix dimensions are (nds, nds), @@ -468,7 +469,7 @@ def lindbladian(self): Returns ------- - lindbladian : :class:`qutip.Qobj` + lindbladian : :class:`.Qobj` The Lindbladian matrix as a `qutip.Qobj`. """ cythonized_dicke = _Dicke( @@ -487,7 +488,7 @@ def liouvillian(self): Returns ------- - liouv : :class:`qutip.Qobj` + liouv : :class:`.Qobj` The Liouvillian matrix for the system. """ lindblad = self.lindbladian() @@ -508,7 +509,7 @@ def pisolve(self, initial_state, tlist): Parameters ========== - initial_state : :class:`qutip.Qobj` + initial_state : :class:`.Qobj` An initial state specified as a density matrix of `qutip.Qbj` type. @@ -598,8 +599,8 @@ def coefficient_matrix(self): def energy_degeneracy(N, m): """Calculate the number of Dicke states with same energy. - The use of the `Decimals` class allows to explore N > 1000, - unlike the built-in function `scipy.special.binom` + The use of the ``Decimals`` class allows to explore N > 1000, + unlike the built-in function ``scipy.special.binom``. Parameters ---------- @@ -745,7 +746,7 @@ def spin_algebra(N, op=None): Returns ------- - spin_operators: list or :class: qutip.Qobj + spin_operators: list or :class:`.Qobj` A list of `qutip.Qobj` operators - [sx, sy, sz] or the requested operator. """ @@ -820,7 +821,7 @@ def _jspin_uncoupled(N, op=None): Returns ------- - collective_operators: list or :class: qutip.Qobj + collective_operators: list or :class:`.Qobj` A list of `qutip.Qobj` representing all the operators in uncoupled" basis or a single operator requested. """ @@ -867,18 +868,17 @@ def jspin(N, op=None, basis="dicke"): N: int Number of two-level systems. - op: str + op: str {'x', 'y', 'z', '+', '-'}, optional The operator to return 'x','y','z','+','-'. If no operator given, then output is the list of operators for ['x','y','z']. - basis: str - The basis of the operators - "dicke" or "uncoupled" - default: "dicke". + basis: str {"dicke", "uncoupled"}, default: "dicke" + The basis of the operators. Returns ------- - j_alg: list or :class: qutip.Qobj + j_alg: list or :class:`.Qobj` A list of `qutip.Qobj` representing all the operators in the "dicke" or "uncoupled" basis or a single operator requested. """ @@ -956,34 +956,29 @@ def collapse_uncoupled( N: int The number of two-level systems. - emission: float + emission: float, default: 0.0 Incoherent emission coefficient (also nonradiative emission). - default: 0.0 - dephasing: float + + dephasing: float, default: 0.0 Local dephasing coefficient. - default: 0.0 - pumping: float + pumping: float, default: 0.0 Incoherent pumping coefficient. - default: 0.0 - collective_emission: float + collective_emission: float, default: 0.0 Collective (superradiant) emmission coefficient. - default: 0.0 - collective_pumping: float + collective_pumping: float, default: 0.0 Collective pumping coefficient. - default: 0.0 - collective_dephasing: float + collective_dephasing: float, default: 0.0 Collective dephasing coefficient. - default: 0.0 Returns ------- c_ops: list - The list of collapse operators as `qutip.Qobj` for the system. + The list of collapse operators as :obj:`.Qobj` for the system. """ N = int(N) @@ -1028,7 +1023,7 @@ def collapse_uncoupled( # State definitions in the Dicke basis with an option for basis transformation -def dicke_basis(N, jmm1=None): +def dicke_basis(N, jmm1): r""" Initialize the density matrix of a Dicke state for several (j, m, m1). @@ -1050,10 +1045,10 @@ def dicke_basis(N, jmm1=None): Returns ------- - rho: :class: qutip.Qobj + rho: :class:`.Qobj` The density matrix in the Dicke basis. """ - if jmm1 is None: + if not isinstance(jmm1, dict): msg = "Please specify the jmm1 values as a dictionary" msg += "or use the `excited(N)` function to create an" msg += "excited state where jmm1 = {(N/2, N/2, N/2): 1}" @@ -1091,7 +1086,7 @@ def dicke(N, j, m): Returns ------- - rho: :class: qutip.Qobj + rho: :class:`.Qobj` The density matrix. """ nds = num_dicke_states(N) @@ -1118,7 +1113,7 @@ def _uncoupled_excited(N): Returns ------- - psi0: :class: qutip.Qobj + psi0: :class:`.Qobj` The density matrix for the excited state in the uncoupled basis. """ N = int(N) @@ -1140,7 +1135,7 @@ def _uncoupled_superradiant(N): Returns ------- - psi0: :class: qutip.Qobj + psi0: :class:`.Qobj` The density matrix for the superradiant state in the full Hilbert space. """ @@ -1163,7 +1158,7 @@ def _uncoupled_ground(N): Returns ------- - psi0: :class: qutip.Qobj + psi0: :class:`.Qobj` The density matrix for the ground state in the full Hilbert space. """ N = int(N) @@ -1185,7 +1180,7 @@ def _uncoupled_ghz(N): Returns ------- - ghz: :class: qutip.Qobj + ghz: :class:`.Qobj` The density matrix for the GHZ state in the full Hilbert space. """ N = int(N) @@ -1221,7 +1216,7 @@ def _uncoupled_css(N, a, b): Returns ------- - css: :class: qutip.Qobj + css: :class:`.Qobj` The density matrix for the CSS state in the full Hilbert space. """ N = int(N) @@ -1257,7 +1252,7 @@ def excited(N, basis="dicke"): Generate the density matrix for the excited state. This state is given by (N/2, N/2) in the default Dicke basis. If the - argument `basis` is "uncoupled" then it generates the state in a + argument ``basis`` is "uncoupled" then it generates the state in a 2**N dim Hilbert space. Parameters @@ -1265,12 +1260,12 @@ def excited(N, basis="dicke"): N: int The number of two-level systems. - basis: str - The basis to use. Either "dicke" or "uncoupled". + basis: str, {"dicke", "uncoupled"}, default: "dicke" + The basis to use. Returns ------- - state: :class: qutip.Qobj + state: :class:`.Qobj` The excited state density matrix in the requested basis. """ if basis == "uncoupled": @@ -1294,12 +1289,12 @@ def superradiant(N, basis="dicke"): N: int The number of two-level systems. - basis: str - The basis to use. Either "dicke" or "uncoupled". + basis: str, {"dicke", "uncoupled"}, default: "dicke" + The basis to use. Returns ------- - state: :class: qutip.Qobj + state: :class:`.Qobj` The superradiant state density matrix in the requested basis. """ if basis == "uncoupled": @@ -1338,19 +1333,19 @@ def css( N: int The number of two-level systems. - x, y: float + x, y: float, default: sqrt(1/2) The coefficients of the CSS state. - basis: str - The basis to use. Either "dicke" or "uncoupled". + basis: str {"dicke", "uncoupled"}, default: "dicke" + The basis to use. - coordinates: str - Either "cartesian" or "polar". If polar then the coefficients - are constructed as sin(x/2), cos(x/2)e^(iy). + coordinates: str {"cartesian", "polar"}, default: "cartesian" + If polar then the coefficients are constructed as + :math:`sin(x/2), cos(x/2)e^(iy)``. Returns ------- - rho: :class: qutip.Qobj + rho: :class:`.Qobj` The CSS state density matrix. """ if coordinates == "polar": @@ -1393,7 +1388,7 @@ def ghz(N, basis="dicke"): """ Generate the density matrix of the GHZ state. - If the argument `basis` is "uncoupled" then it generates the state + If the argument ``basis`` is "uncoupled" then it generates the state in a :math:`2^N`-dimensional Hilbert space. Parameters @@ -1401,12 +1396,12 @@ def ghz(N, basis="dicke"): N: int The number of two-level systems. - basis: str - The basis to use. Either "dicke" or "uncoupled". + basis: str, {"dicke", "uncoupled"}, default: "dicke" + The basis to use. Returns ------- - state: :class: qutip.Qobj + state: :class:`.Qobj` The GHZ state density matrix in the requested basis. """ if basis == "uncoupled": @@ -1425,7 +1420,7 @@ def ground(N, basis="dicke"): Generate the density matrix of the ground state. This state is given by (N/2, -N/2) in the Dicke basis. If the argument - `basis` is "uncoupled" then it generates the state in a + ``basis`` is "uncoupled" then it generates the state in a :math:`2^N`-dimensional Hilbert space. Parameters @@ -1433,12 +1428,12 @@ def ground(N, basis="dicke"): N: int The number of two-level systems. - basis: str - The basis to use. Either "dicke" or "uncoupled" + basis: str, {"dicke", "uncoupled"}, default: "dicke" + The basis to use. Returns ------- - state: :class: qutip.Qobj + state: :class:`.Qobj` The ground state density matrix in the requested basis. """ if basis == "uncoupled": @@ -1463,7 +1458,7 @@ def identity_uncoupled(N): Returns ------- - identity: :class: qutip.Qobj + identity: :class:`.Qobj` The identity matrix. """ N = int(N) @@ -1483,7 +1478,7 @@ def block_matrix(N, elements="ones"): ---------- N : int Number of two-level systems. - elements : str {'ones' (default),'degeneracy'} + elements : str {'ones', 'degeneracy'}, default: 'ones' Returns ------- diff --git a/qutip/random_objects.py b/qutip/random_objects.py index be759ae9db..545c9639b8 100644 --- a/qutip/random_objects.py +++ b/qutip/random_objects.py @@ -23,7 +23,7 @@ from . import (Qobj, create, destroy, jmat, basis, to_super, to_choi, to_chi, to_kraus, to_stinespring) from .core import data as _data -from .core.dimensions import flatten +from .core.dimensions import flatten, Dimensions from . import settings @@ -222,10 +222,10 @@ def rand_herm(dimensions, density=0.30, distribution="fill", *, the new Qobj are set to this list. This can produce either `oper` or `super` depending on the passed `dimensions`. - density : float, [0.30] + density : float, default: 0.30 Density between [0,1] of output Hermitian operator. - distribution : str {"fill", "pos_def", "eigen"} + distribution : str {"fill", "pos_def", "eigen"}, default: "fill" Method used to obtain the density matrices. - "fill" : Uses :math:`H=0.5*(X+X^{+})` where :math:`X` is a randomly @@ -244,13 +244,13 @@ def rand_herm(dimensions, density=0.30, distribution="fill", *, Seed to create the random number generator or a pre prepared generator. When none is suplied, a default generator is used. - dtype : type or str - Storage representation. Any data-layer known to `qutip.data.to` is + dtype : type or str, optional + Storage representation. Any data-layer known to ``qutip.data.to`` is accepted. Returns ------- - oper : :class:`qobj` + oper : :obj:`.Qobj` Hermitian quantum operator. Notes @@ -274,7 +274,7 @@ def rand_herm(dimensions, density=0.30, distribution="fill", *, out = _rand_jacobi_rotation(out, generator) while _data.csr.nnz(out) < 0.95 * nvals: out = _rand_jacobi_rotation(out, generator) - out = Qobj(out, type='oper', dims=dims, isherm=True, copy=False) + out = Qobj(out, dims=dims, isherm=True, copy=False) dtype = dtype or settings.core["default_dtype"] or _data.CSR else: @@ -286,7 +286,7 @@ def rand_herm(dimensions, density=0.30, distribution="fill", *, M = _rand_herm_dense(N, density, pos_def, generator) dtype = dtype or settings.core["default_dtype"] or _data.Dense - out = Qobj(M, type='oper', dims=dims, isherm=True, copy=False) + out = Qobj(M, dims=dims, isherm=True, copy=False) return out.to(dtype) @@ -347,10 +347,10 @@ def rand_unitary(dimensions, density=1, distribution="haar", *, the new Qobj are set to this list. This can produce either `oper` or `super` depending on the passed `dimensions`. - density : float, [1] + density : float, default: 1 Density between [0,1] of output unitary operator. - distribution : ["haar", "exp"] + distribution : str {"haar", "exp"}, default: "haar" Method used to obtain the unitary matrices. - haar : Haar random unitary matrix using the algorithm of [Mez07]_. @@ -361,8 +361,8 @@ def rand_unitary(dimensions, density=1, distribution="haar", *, Seed to create the random number generator or a pre prepared generator. When none is suplied, a default generator is used. - dtype : type or str - Storage representation. Any data-layer known to `qutip.data.to` is + dtype : type or str, optional + Storage representation. Any data-layer known to ``qutip.data.to`` is accepted. Returns @@ -390,7 +390,7 @@ def rand_unitary(dimensions, density=1, distribution="haar", *, merged = _merge_shuffle_blocks(blocks, generator) - mat = Qobj(merged, type='oper', dims=dims, isunitary=True, copy=False) + mat = Qobj(merged, dims=dims, isunitary=True, copy=False) return mat.to(dtype) @@ -450,11 +450,11 @@ def rand_ket(dimensions, density=1, distribution="haar", *, the new Qobj are set to this list. This can produce either `oper` or `super` depending on the passed `dimensions`. - density : float, [1] + density : float, default: 1 Density between [0,1] of output ket state when using the ``fill`` method. - distribution : str {"haar", "fill"} + distribution : str {"haar", "fill"}, default: "haar" Method used to obtain the kets. - haar : Haar random pure state obtained by applying a Haar random @@ -465,8 +465,8 @@ def rand_ket(dimensions, density=1, distribution="haar", *, Seed to create the random number generator or a pre prepared generator. When none is suplied, a default generator is used. - dtype : type or str - Storage representation. Any data-layer known to `qutip.data.to` is + dtype : type or str, optional + Storage representation. Any data-layer known to ``qutip.data.to`` is accepted. Returns @@ -480,7 +480,9 @@ def rand_ket(dimensions, density=1, distribution="haar", *, if distribution not in ["haar", "fill"]: raise ValueError("distribution must be one of {'haar', 'fill'}") - if distribution == "haar": + if N == 1: + ket = rand_unitary(1, seed=generator) + elif distribution == "haar": ket = rand_unitary(N, density, "haar", seed=generator) @ basis(N, 0) else: X = scipy.sparse.rand(N, 1, density, format='csr', @@ -494,7 +496,7 @@ def rand_ket(dimensions, density=1, distribution="haar", *, Y.data = 1.0j * (generator.random(len(X.data)) - 0.5) X = _data.csr.CSR(X + Y) ket = Qobj(_data.mul(X, 1 / _data.norm.l2(X)), - copy=False, type="ket", isherm=False, isunitary=False) + copy=False, isherm=False, isunitary=False) ket.dims = [dims[0], [1]] return ket.to(dtype) @@ -512,11 +514,12 @@ def rand_dm(dimensions, density=0.75, distribution="ginibre", *, the new Qobj are set to this list. This can produce either ``oper`` or ``super`` depending on the passed ``dimensions``. - density : float + density : float, default: 0.75 Density between [0,1] of output density matrix. Used by the "pure", "eigen" and "herm". - distribution : str {"ginibre", "hs", "pure", "eigen", "uniform"} + distribution : str {"ginibre", "hs", "pure", "eigen", "uniform"}, \ +default: "ginibre" Method used to obtain the density matrices. - "ginibre" : Ginibre random density operator of rank ``rank`` by using @@ -539,8 +542,8 @@ def rand_dm(dimensions, density=0.75, distribution="ginibre", *, Seed to create the random number generator or a pre prepared generator. When none is suplied, a default generator is used. - dtype : type or str - Storage representation. Any data-layer known to `qutip.data.to` is + dtype : type or str, optional + Storage representation. Any data-layer known to ``qutip.data.to`` is accepted. Returns @@ -591,7 +594,7 @@ def rand_dm(dimensions, density=0.75, distribution="ginibre", *, H = _merge_shuffle_blocks(blocks, generator) H /= H.trace() - return Qobj(H, dims=dims, type='oper', isherm=True, copy=False).to(dtype) + return Qobj(H, dims=dims, isherm=True, copy=False).to(dtype) def _rand_dm_ginibre(N, rank, generator): @@ -628,8 +631,7 @@ def _rand_dm_ginibre(N, rank, generator): return rho -def rand_kraus_map(dimensions, *, seed=None, - dtype=None): +def rand_kraus_map(dimensions, *, seed=None, dtype=None): """ Creates a random CPTP map on an N-dimensional Hilbert space in Kraus form. @@ -646,8 +648,8 @@ def rand_kraus_map(dimensions, *, seed=None, Seed to create the random number generator or a pre prepared generator. When none is suplied, a default generator is used. - dtype : type or str - Storage representation. Any data-layer known to `qutip.data.to` is + dtype : type or str, optional + Storage representation. Any data-layer known to ``qutip.data.to`` is accepted. Returns @@ -658,17 +660,18 @@ def rand_kraus_map(dimensions, *, seed=None, """ dtype = dtype or settings.core["default_dtype"] or _data.Dense N, dims = _implicit_tensor_dimensions(dimensions) + dims = Dimensions(dims) + if dims.issuper: + raise TypeError("Each kraus operator cannot itself a super operator.") # Random unitary (Stinespring Dilation) big_unitary = rand_unitary(N ** 3, seed=seed, dtype=dtype).full() orthog_cols = np.array(big_unitary[:, :N]) oper_list = np.reshape(orthog_cols, (N ** 2, N, N)) - return [Qobj(x, dims=dims, type='oper', copy=False).to(dtype) - for x in oper_list] + return [Qobj(x, dims=dims, copy=False).to(dtype) for x in oper_list] -def rand_super(dimensions, *, superrep="super", seed=None, - dtype=None): +def rand_super(dimensions, *, superrep="super", seed=None, dtype=None): """ Returns a randomly drawn superoperator acting on operators acting on N dimensions. @@ -681,15 +684,15 @@ def rand_super(dimensions, *, superrep="super", seed=None, the new Qobj are set to this list. This can produce either `oper` or `super` depending on the passed `dimensions`. - superrop : str, optional, {"super"} + superrop : str, default: "super" Representation of the super operator seed : int, SeedSequence, Generator, optional Seed to create the random number generator or a pre prepared generator. When none is suplied, a default generator is used. - dtype : type or str - Storage representation. Any data-layer known to `qutip.data.to` is + dtype : type or str, optional + Storage representation. Any data-layer known to ``qutip.data.to`` is accepted. """ dtype = dtype or settings.core["default_dtype"] or _data.Dense @@ -729,11 +732,11 @@ def rand_super_bcsz(dimensions, enforce_tp=True, rank=None, *, density matrices this superoperator is applied to: ``dimensions=[2,2]`` ``dims=[[[2,2],[2,2]], [[2,2],[2,2]]]``. - enforce_tp : bool + enforce_tp : bool, default: True If True, the trace-preserving condition of [BCSZ08]_ is enforced; otherwise only complete positivity is enforced. - rank : int or None + rank : int, optional Rank of the sampled superoperator. If None, a full-rank superoperator is generated. @@ -741,11 +744,11 @@ def rand_super_bcsz(dimensions, enforce_tp=True, rank=None, *, Seed to create the random number generator or a pre prepared generator. When none is suplied, a default generator is used. - superrop : str, optional, {"super"} + superrop : str, default: "super" representation of the - dtype : type or str - Storage representation. Any data-layer known to `qutip.data.to` is + dtype : type or str, optional + Storage representation. Any data-layer known to ``qutip.data.to`` is accepted. Returns @@ -794,9 +797,9 @@ def rand_super_bcsz(dimensions, enforce_tp=True, rank=None, *, # marking the dimensions as that of a type=super (that is, # with left and right compound indices, each representing # left and right indices on the underlying Hilbert space). - D = Qobj(np.dot(Z, np.dot(XXdag, Z)), dims=tmp_dims, type='super') + D = Qobj(np.dot(Z, np.dot(XXdag, Z)), dims=tmp_dims) else: - D = N * Qobj(XXdag / np.trace(XXdag), dims=tmp_dims, type='super') + D = N * Qobj(XXdag / np.trace(XXdag), dims=tmp_dims) # Since [BCSZ08] gives a row-stacking Choi matrix, but QuTiP # expects a column-stacking Choi matrix, we must permute the indices. @@ -825,18 +828,18 @@ def rand_stochastic(dimensions, density=0.75, kind='left', the new Qobj are set to this list. This can produce either `oper` or `super` depending on the passed `dimensions`. - density : float, [0.75] + density : float, default: 0.75 Density between [0,1] of output density matrix. - kind : str (Default = 'left') + kind : str {"left", "right"}, default: "left" Generate 'left' or 'right' stochastic matrix. seed : int, SeedSequence, Generator, optional Seed to create the random number generator or a pre prepared generator. When none is suplied, a default generator is used. - dtype : type or str - Storage representation. Any data-layer known to `qutip.data.to` is + dtype : type or str, optional + Storage representation. Any data-layer known to ``qutip.data.to`` is accepted. Returns diff --git a/qutip/simdiag.py b/qutip/simdiag.py index 2f875ff82b..63d2a6e107 100644 --- a/qutip/simdiag.py +++ b/qutip/simdiag.py @@ -22,7 +22,7 @@ def _degen(tol, vecs, ops, i=0): / (1 - np.abs(dot)**2)**0.5) subspace = vecs.conj().T @ ops[i].full() @ vecs - eigvals, eigvecs = la.eig(subspace) + eigvals, eigvecs = la.eigh(subspace) perm = np.argsort(eigvals) eigvals = eigvals[perm] @@ -47,18 +47,18 @@ def simdiag(ops, evals: bool = True, *, Parameters ---------- - ops : list/array + ops : list, array ``list`` or ``array`` of qobjs representing commuting Hermitian operators. - evals : bool [True] + evals : bool, default: True Whether to return the eigenvalues for each ops and eigenvectors or just the eigenvectors. - tol : float [1e-14] + tol : float, default: 1e-14 Tolerance for detecting degenerate eigenstates. - safe_mode : bool [True] + safe_mode : bool, default: True Whether to check that all ops are Hermitian and commuting. If set to ``False`` and operators are not commuting, the eigenvectors returned will often be eigenvectors of only the first operator. diff --git a/qutip/solver/_feedback.py b/qutip/solver/_feedback.py new file mode 100644 index 0000000000..79d82259cc --- /dev/null +++ b/qutip/solver/_feedback.py @@ -0,0 +1,150 @@ +from qutip.core.cy.qobjevo import QobjEvo, _Feedback +from qutip.core.dimensions import Dimensions, Field, SuperSpace +import qutip.core.data as _data +from qutip.core.qobj import Qobj +import numpy as np + + +class _ExpectFeedback(_Feedback): + def __init__(self, oper, default=0.): + self.oper = QobjEvo(oper) + self.N = oper.shape[1] + self.N2 = oper.shape[1]**2 + self.default = default + + def check_consistency(self, dims): + if not ( + self.oper._dims == dims + or self.oper._dims[1] == dims # super e_op, oper QobjEvo + or self.oper._dims == dims[0] # oper e_op, super QobjEvo + ): + raise ValueError( + f"Dimensions of the expect operator ({self.oper.dims}) " + f"does not match the operator ({dims})." + ) + + def __call__(self, t, state): + if state.shape[0] == self.N: + return self.oper.expect_data(t, state) + if state.shape[0] == self.N2 and state.shape[1] == 1: + return self.oper.expect_data( + t, _data.column_unstack(state, self.N) + ) + raise ValueError( + f"Shape of the expect operator ({self.oper.shape}) " + f"does not match the state ({state.shape})." + ) + + def __repr__(self): + return "ExpectFeedback" + + +class _QobjFeedback(_Feedback): + def __init__(self, default=None, prop=False, open=True): + self.open = open + self.prop = prop + self.default = default + + def check_consistency(self, dims): + if not self.open: + # Ket + self.dims = Dimensions(Field(), dims[1]) + elif dims.issuper: + # Density matrix, operator already super + self.dims = dims[1].oper + else: + # operator not super, dm dims match oper + self.dims = dims + if self.prop and self.dims[1] == Field(): + self.dims = Dimensions(self.dims[0], self.dims[0]) + elif self.prop: + self.dims = Dimensions( + SuperSpace(self.dims), SuperSpace(self.dims) + ) + + if self.default is None: + pass + elif self.default._dims == self.dims: + pass + elif self.default._dims[0] == self.dims[0]: + # Catch rectangular state and propagator when the flag is not set. + self.dims = self.default._dims + else: + # The state could not be used for qevo @ default... + raise TypeError( + f"The dimensions of the default state ({self.default.dims}) " + f"does not match the operators ({self.dims})." + ) + + def __call__(self, t, state): + if state.shape == self.dims.shape: + out = Qobj(state, dims=self.dims) + else: + out = Qobj( + _data.column_unstack(state, self.dims.shape[0]), + dims=self.dims + ) + return out + + def __repr__(self): + return "QobjFeedback" + + +class _DataFeedback(_Feedback): + def __init__(self, default=None, open=True, prop=False): + self.open = open + self.default = default + self.prop = prop + + def check_consistency(self, dims): + if self.default is None: + return + if not ( + dims.shape[1] == self.default.shape[0] + or (dims.shape[1]**2 == self.default.shape[0] and self.open) + ): + raise ValueError( + f"The shape of the default state {self.default.shape} " + f"does not match the operators {dims.shape}." + ) + if self.prop and self.default.shape[0] != self.default.shape[1]: + raise ValueError( + f"The default state is expected to be square when computing " + f"propagators, but is {self.default.shape}." + ) + + def __call__(self, t, state): + return state + + def __repr__(self): + return "DataFeedback" + + +class _CollapseFeedback(_Feedback): + code = "CollapseFeedback" + + def __init__(self, default=None): + self.default = default or [] + + def check_consistency(self, dims): + pass + + def __repr__(self): + return "CollapseFeedback" + + +def _default_weiner(t): + return np.zeros(1) + + +class _WeinerFeedback(_Feedback): + code = "WeinerFeedback" + + def __init__(self, default=None): + self.default = default or _default_weiner + + def check_consistency(self, dims): + pass + + def __repr__(self): + return "WeinerFeedback" diff --git a/qutip/solver/brmesolve.py b/qutip/solver/brmesolve.py index 7bfc8b27ad..4b54a70e8c 100644 --- a/qutip/solver/brmesolve.py +++ b/qutip/solver/brmesolve.py @@ -12,12 +12,13 @@ from ..core.blochredfield import bloch_redfield_tensor, SpectraCoefficient from ..core.cy.coefficient import InterCoefficient from ..core import data as _data -from .solver_base import Solver +from .solver_base import Solver, _solver_deprecation from .options import _SolverOptions +from ._feedback import _QobjFeedback, _DataFeedback -def brmesolve(H, psi0, tlist, a_ops=[], e_ops=[], c_ops=[], - args={}, sec_cutoff=0.1, options=None): +def brmesolve(H, psi0, tlist, a_ops=(), e_ops=(), c_ops=(), + args=None, sec_cutoff=0.1, options=None, **kwargs): """ Solves for the dynamics of a system using the Bloch-Redfield master equation, given an input Hamiltonian, Hermitian bath-coupling terms and @@ -26,10 +27,10 @@ def brmesolve(H, psi0, tlist, a_ops=[], e_ops=[], c_ops=[], Parameters ---------- - H : :class:`Qobj`, :class:`QobjEvo` + H : :obj:`.Qobj`, :obj:`.QobjEvo` Possibly time-dependent system Liouvillian or Hamiltonian as a Qobj or - QobjEvo. list of [:class:`Qobj`, :class:`Coefficient`] or callable that - can be made into :class:`QobjEvo` are also accepted. + QobjEvo. list of [:obj:`.Qobj`, :obj:`.Coefficient`] or callable that + can be made into :obj:`.QobjEvo` are also accepted. psi0: Qobj Initial density matrix or state vector (ket). @@ -41,10 +42,10 @@ def brmesolve(H, psi0, tlist, a_ops=[], e_ops=[], c_ops=[], Nested list of system operators that couple to the environment, and the corresponding bath spectra. - a_op : :class:`qutip.Qobj`, :class:`qutip.QobjEvo` + a_op : :obj:`.Qobj`, :obj:`.QobjEvo` The operator coupling to the environment. Must be hermitian. - spectra : :class:`Coefficient`, str, func + spectra : :obj:`.Coefficient`, str, func The corresponding bath spectral responce. Can be a `Coefficient` using an 'w' args, a function of the frequence or a string. Coefficient build from a numpy array are @@ -52,7 +53,7 @@ def brmesolve(H, psi0, tlist, a_ops=[], e_ops=[], c_ops=[], expected to be of the signature ``f(w)`` or ``f(t, w, **args)``. The spectra function can depend on ``t`` if the corresponding - ``a_op`` is a :class:`QobjEvo`. + ``a_op`` is a :obj:`.QobjEvo`. Example: @@ -66,82 +67,77 @@ def brmesolve(H, psi0, tlist, a_ops=[], e_ops=[], c_ops=[], ] .. note: - ``Cubic_Spline`` has been replaced by :class:`Coefficient`: + ``Cubic_Spline`` has been replaced by :obj:`.Coefficient`: ``spline = qutip.coefficient(array, tlist=times)`` Whether the ``a_ops`` is time dependent is decided by the type of - the operator: :class:`Qobj` vs :class:`QobjEvo` instead of the type + the operator: :obj:`.Qobj` vs :obj:`.QobjEvo` instead of the type of the spectra. - e_ops : list of :class:`Qobj` / callback function + e_ops : list of :obj:`.Qobj` / callback function, optional Single operator or list of operators for which to evaluate expectation values or callable or list of callable. Callable signature must be, `f(t: float, state: Qobj)`. See :func:`expect` for more detail of operator expectation - c_ops : list of (:class:`QobjEvo`, :class:`QobjEvo` compatible format) + c_ops : list of (:obj:`.QobjEvo`, :obj:`.QobjEvo` compatible format), optional List of collapse operators. - args : dict + args : dict, optional Dictionary of parameters for time-dependent Hamiltonians and collapse operators. The key ``w`` is reserved for the spectra function. - sec_cutoff : float {0.1} + sec_cutoff : float, default: 0.1 Cutoff for secular approximation. Use ``-1`` if secular approximation is not used when evaluating bath-coupling terms. - options : None / dict + options : dict, optional Dictionary of options for the solver. - - store_final_state : bool - Whether or not to store the final state of the evolution in the - result class. - - store_states : bool, None - 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 - if not installed. Empty string or False will disable the bar. - - progress_kwargs : dict - kwargs to pass to the progress_bar. Qutip's bars use `chunk_size`. - - tensor_type : str ['sparse', 'dense', 'data'] - Which data type to use when computing the brtensor. - With a cutoff 'sparse' is usually the most efficient. - - sparse_eigensolver : bool {False} - Whether to use the sparse eigensolver - - method : str ["adams", "bdf", "lsoda", "dop853", "vern9", etc.] - Which differential equation integration method to use. - - atol, rtol : float - Absolute and relative tolerance of the ODE integrator. - - nsteps : - Maximum number of (internally defined) steps allowed in one ``tlist`` - step. - - max_step : float, 0 - Maximum lenght of one internal step. When using pulses, it should be - less than half the width of the thinnest pulse. + - | store_final_state : bool + | Whether or not to store the final state of the evolution in the + result class. + - | store_states : bool, None + | 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 + if not installed. Empty string or False will disable the bar. + - | progress_kwargs : dict + | kwargs to pass to the progress_bar. Qutip's bars use `chunk_size`. + - | tensor_type : str ['sparse', 'dense', 'data'] + | Which data type to use when computing the brtensor. + With a cutoff 'sparse' is usually the most efficient. + - | sparse_eigensolver : bool {False} + Whether to use the sparse eigensolver + - | method : str ["adams", "bdf", "lsoda", "dop853", "vern9", etc.] + Which differential equation integration method to use. + - | atol, rtol : float + | Absolute and relative tolerance of the ODE integrator. + - | nsteps : int + | Maximum number of (internally defined) steps allowed in one + ``tlist`` step. + - | max_step : float, 0 + | Maximum lenght of one internal step. When using pulses, it should + be less than half the width of the thinnest pulse. Other options could be supported depending on the integration method, see `Integrator <./classes.html#classes-ode>`_. Returns ------- - result: :class:`qutip.solver.Result` + result: :obj:`.Result` - An instance of the class :class:`qutip.solver.Result`, which contains + An instance of the class :obj:`qutip.solver.Result`, which contains either an array of expectation values, for operators given in e_ops, - or a list of states for the times specified by `tlist`. - - .. note: - The option ``operator_data_type`` is used to determine in which format - the bloch redfield tensor is computed. Use 'csr' for sparse and 'dense' - for dense array. With 'data', it will try to use the same data type as - the ``a_ops``, but it is usually less efficient than manually choosing - it. + or a list of states for the times specified by ``tlist``. """ + options = _solver_deprecation(kwargs, options, "br") + args = args or {} H = QobjEvo(H, args=args, tlist=tlist) c_ops = c_ops if c_ops is not None else [] @@ -181,22 +177,22 @@ class BRSolver(Solver): Parameters ---------- - H : :class:`Qobj`, :class:`QobjEvo` + H : :obj:`.Qobj`, :obj:`.QobjEvo` Possibly time-dependent system Liouvillian or Hamiltonian as a Qobj or - QobjEvo. list of [:class:`Qobj`, :class:`Coefficient`] or callable that - can be made into :class:`QobjEvo` are also accepted. + QobjEvo. list of [:obj:`.Qobj`, :obj:`.Coefficient`] or callable that + can be made into :obj:`.QobjEvo` are also accepted. a_ops : list of (a_op, spectra) Nested list of system operators that couple to the environment, and the corresponding bath spectra. - a_op : :class:`qutip.Qobj`, :class:`qutip.QobjEvo` + a_op : :obj:`.Qobj`, :obj:`.QobjEvo` The operator coupling to the environment. Must be hermitian. - spectra : :class:`Coefficient` + spectra : :obj:`.Coefficient` The corresponding bath spectra. As a `Coefficient` using an 'w' - args. Can depend on ``t`` only if a_op is a :class:`qutip.QobjEvo`. - :class:`SpectraCoefficient` can be used to conver a coefficient + args. Can depend on ``t`` only if a_op is a :obj:`.QobjEvo`. + :obj:`SpectraCoefficient` can be used to conver a coefficient depending on ``t`` to one depending on ``w``. Example: @@ -210,7 +206,7 @@ class BRSolver(Solver): (c+c.dag(), SpectraCoefficient(coefficient(array, tlist=ws))), ] - c_ops : list of :class:`Qobj`, :class:`QobjEvo` + c_ops : list of :obj:`.Qobj`, :obj:`.QobjEvo` Single collapse operator, or list of collapse operators, or a list of Lindblad dissipator. None is equivalent to an empty list. @@ -222,14 +218,14 @@ class BRSolver(Solver): Cutoff for secular approximation. Use ``-1`` if secular approximation is not used when evaluating bath-coupling terms. - attributes + Attributes ---------- stats: dict Diverse diagnostic statistics of the evolution. """ name = "brmesolve" solver_options = { - "progress_bar": "text", + "progress_bar": "", "progress_kwargs": {"chunk_size":10}, "store_final_state": False, "store_states": None, @@ -277,6 +273,7 @@ def __init__(self, H, a_ops, c_ops=None, sec_cutoff=0.1, *, options=None): self._integrator = self._get_integrator() self._state_metadata = {} self.stats = self._initialize_stats() + self.rhs._register_feedback({}, solver=self.name) def _initialize_stats(self): @@ -306,35 +303,35 @@ def options(self): """ Options for bloch redfield solver: - store_final_state: bool, default=False + store_final_state: bool, default: False Whether or not to store the final state of the evolution in the result class. - store_states: bool, default=None + store_states: bool, default: None 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, default=False + normalize_output: bool, default: False Normalize output state to hide ODE numerical errors. - progress_bar: str {'text', 'enhanced', 'tqdm', ''}, default="text" + progress_bar: str {'text', 'enhanced', 'tqdm', ''}, default: "" 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. - progress_kwargs: dict, default={"chunk_size":10} + progress_kwargs: dict, default: {"chunk_size":10} Arguments to pass to the progress_bar. Qutip's bars use ``chunk_size``. - tensor_type: str ['sparse', 'dense', 'data'], default="sparse" + tensor_type: str ['sparse', 'dense', 'data'], default: "sparse" Which data type to use when computing the brtensor. With a cutoff 'sparse' is usually the most efficient. - sparse_eigensolver: bool, default=False + sparse_eigensolver: bool, default: False Whether to use the sparse eigensolver - method: str, default="adams" + method: str, default: "adams" Which ODE integrator methods are supported. """ return self._options @@ -360,3 +357,33 @@ def _apply_options(self, keys): else: self._integrator.options = self._options self._integrator.reset(hard=True) + + @classmethod + def StateFeedback(cls, default=None, raw_data=False): + """ + State of the evolution to be used in a time-dependent operator. + + When used as an args: + + ``QobjEvo([op, func], args={"state": BRMESolver.StateFeedback()})`` + + The ``func`` will receive the density matrix as ``state`` during the + evolution. + + .. note:: + + The state will not be in the lab basis, but in the evolution basis. + + Parameters + ---------- + default : Qobj or qutip.core.data.Data, default : None + Initial value to be used at setup of the system. + + raw_data : bool, default : False + If True, the raw matrix will be passed instead of a Qobj. + For density matrices, the matrices can be column stacked or square + depending on the integration method. + """ + if raw_data: + return _DataFeedback(default, open=True) + return _QobjFeedback(default, open=True) diff --git a/qutip/solver/correlation.py b/qutip/solver/correlation.py index fcbeff8e13..312211aba8 100644 --- a/qutip/solver/correlation.py +++ b/qutip/solver/correlation.py @@ -27,8 +27,8 @@ def correlation_2op_1t(H, state0, taulist, c_ops, a_op, b_op, - solver="me", reverse=False, args={}, - options={}): + solver="me", reverse=False, args=None, + options=None): r""" Calculate the two-operator one-time correlation function: :math:`\left` @@ -38,9 +38,9 @@ def correlation_2op_1t(H, state0, taulist, c_ops, a_op, b_op, Parameters ---------- - H : :class:`Qobj`, :class:`QobjEvo` + H : :obj:`.Qobj`, :obj:`.QobjEvo` System Hamiltonian, may be time-dependent for solver choice of `me`. - state0 : :class:`Qobj` + state0 : :obj:`.Qobj` Initial state density matrix :math:`\rho(t_0)` or state vector :math:`\psi(t_0)`. If 'state0' is 'None', then the steady state will be used as the initial state. The 'steady-state' is only implemented @@ -48,25 +48,26 @@ def correlation_2op_1t(H, state0, taulist, c_ops, a_op, b_op, taulist : array_like List of times for :math:`\tau`. taulist must be positive and contain the element `0`. - c_ops : list of {:class:`Qobj`, :class:`QobjEvo`} + c_ops : list of {:obj:`.Qobj`, :obj:`.QobjEvo`} List of collapse operators - a_op : :class:`Qobj`, :class:`QobjEvo` + a_op : :obj:`.Qobj`, :obj:`.QobjEvo` Operator A. - b_op : :class:`Qobj`, :class:`QobjEvo` + b_op : :obj:`.Qobj`, :obj:`.QobjEvo` Operator B. - reverse : bool {False} - If `True`, calculate :math:`\left` instead of + reverse : bool, default: False + If ``True``, calculate :math:`\left` instead of :math:`\left`. - solver : str {'me', 'es'} - Choice of solver, `me` for master-equation, and `es` for exponential - series. `es` is equivalent to `me` with ``options={"method": "diag"}``. + solver : str {'me', 'es'}, default: 'me' + Choice of solver, ``me`` for master-equation, and ``es`` for + exponential series. ``es`` is equivalent to `me` with + ``options={"method": "diag"}``. options : dict, optional - Options for the solver. Only used with `me` solver. + Options for the solver. Returns ------- corr_vec : ndarray - An array of correlation values for the times specified by `taulist`. + An array of correlation values for the times specified by ``taulist``. See Also -------- @@ -91,8 +92,8 @@ def correlation_2op_1t(H, state0, taulist, c_ops, a_op, b_op, def correlation_2op_2t(H, state0, tlist, taulist, c_ops, a_op, b_op, - solver="me", reverse=False, args={}, - options={}): + solver="me", reverse=False, args=None, + options=None): r""" Calculate the two-operator two-time correlation function: :math:`\left` @@ -101,9 +102,9 @@ def correlation_2op_2t(H, state0, tlist, taulist, c_ops, a_op, b_op, Parameters ---------- - H : :class:`Qobj`, :class:`QobjEvo` + H : :obj:`.Qobj`, :obj:`.QobjEvo` System Hamiltonian, may be time-dependent for solver choice of `me`. - state0 : :class:`Qobj` + state0 : :obj:`.Qobj` Initial state density matrix :math:`\rho(t_0)` or state vector :math:`\psi(t_0)`. If 'state0' is 'None', then the steady state will be used as the initial state. The 'steady-state' is only implemented @@ -116,26 +117,27 @@ def correlation_2op_2t(H, state0, tlist, taulist, c_ops, a_op, b_op, taulist : array_like List of times for :math:`\tau`. taulist must be positive and contain the element `0`. - c_ops : list of {:class:`Qobj`, :class:`QobjEvo`} + c_ops : list of {:obj:`.Qobj`, :obj:`.QobjEvo`} List of collapse operators - a_op : :class:`Qobj`, :class:`QobjEvo` + a_op : :obj:`.Qobj`, :obj:`.QobjEvo` Operator A. - b_op : :class:`Qobj`, :class:`QobjEvo` + b_op : :obj:`.Qobj`, :obj:`.QobjEvo` Operator B. - reverse : bool {False} - If `True`, calculate :math:`\left` instead of + reverse : bool, default: False + If ``True``, calculate :math:`\left` instead of :math:`\left`. - solver : str {'me', 'es'} - Choice of solver, `me` for master-equation, and `es` for exponential - series. `es` is equivalent to `me` with ``options={"method": "diag"}``. + solver : str {'me', 'es'}, default: 'me' + Choice of solver, ``me`` for master-equation, and ``es`` for + exponential series. ``es`` is equivalent to `me` with + ``options={"method": "diag"}``. options : dict, optional - Options for the solver. Only used with `me` solver. + Options for the solver. Returns ------- corr_mat : ndarray An 2-dimensional array (matrix) of correlation values for the times - specified by `tlist` (first index) and `taulist` (second index). + specified by ``tlist`` (first index) and ``taulist`` (second index). See Also -------- @@ -162,8 +164,7 @@ def correlation_2op_2t(H, state0, tlist, taulist, c_ops, a_op, b_op, def correlation_3op_1t(H, state0, taulist, c_ops, a_op, b_op, c_op, - solver="me", args={}, - options={}): + solver="me", args=None, options=None): r""" Calculate the three-operator two-time correlation function: :math:`\left` along one time axis using the @@ -175,9 +176,9 @@ def correlation_3op_1t(H, state0, taulist, c_ops, a_op, b_op, c_op, Parameters ---------- - H : :class:`Qobj`, :class:`QobjEvo` - System Hamiltonian, may be time-dependent for solver choice of `me`. - state0 : :class:`Qobj` + H : :obj:`.Qobj`, :obj:`.QobjEvo` + System Hamiltonian, may be time-dependent for solver choice of ``me``. + state0 : :obj:`.Qobj` Initial state density matrix :math:`\rho(t_0)` or state vector :math:`\psi(t_0)`. If 'state0' is 'None', then the steady state will be used as the initial state. The 'steady-state' is only implemented @@ -185,24 +186,25 @@ def correlation_3op_1t(H, state0, taulist, c_ops, a_op, b_op, c_op, taulist : array_like List of times for :math:`\tau`. taulist must be positive and contain the element `0`. - c_ops : list of {:class:`Qobj`, :class:`QobjEvo`} + c_ops : list of {:obj:`.Qobj`, :obj:`.QobjEvo`} List of collapse operators - a_op : :class:`Qobj`, :class:`QobjEvo` + a_op : :obj:`.Qobj`, :obj:`.QobjEvo` Operator A. - b_op : :class:`Qobj`, :class:`QobjEvo` + b_op : :obj:`.Qobj`, :obj:`.QobjEvo` Operator B. - c_op : :class:`Qobj`, :class:`QobjEvo` + c_op : :obj:`.Qobj`, :obj:`.QobjEvo` Operator C. - solver : str {'me', 'es'} - Choice of solver, `me` for master-equation, and `es` for exponential - series. `es` is equivalent to `me` with ``options={"method": "diag"}``. + solver : str {'me', 'es'}, default: 'me' + Choice of solver, ``me`` for master-equation, and ``es`` for + exponential series. ``es`` is equivalent to `me` with + ``options={"method": "diag"}``. options : dict, optional - Options for the solver. Only used with `me` solver. + Options for the solver. Returns ------- corr_vec : array - An array of correlation values for the times specified by `taulist`. + An array of correlation values for the times specified by ``taulist``. See Also -------- @@ -221,8 +223,7 @@ def correlation_3op_1t(H, state0, taulist, c_ops, a_op, b_op, c_op, def correlation_3op_2t(H, state0, tlist, taulist, c_ops, a_op, b_op, c_op, - solver="me", args={}, - options={}): + solver="me", args=None, options=None): r""" Calculate the three-operator two-time correlation function: :math:`\left` along two time axes using the @@ -234,9 +235,9 @@ def correlation_3op_2t(H, state0, tlist, taulist, c_ops, a_op, b_op, c_op, Parameters ---------- - H : :class:`Qobj`, :class:`QobjEvo` - System Hamiltonian, may be time-dependent for solver choice of `me`. - state0 : :class:`Qobj` + H : :obj:`.Qobj`, :obj:`.QobjEvo` + System Hamiltonian, may be time-dependent for solver choice of ``me``. + state0 : :obj:`.Qobj` Initial state density matrix :math:`\rho(t_0)` or state vector :math:`\psi(t_0)`. If 'state0' is 'None', then the steady state will be used as the initial state. The 'steady-state' is only implemented @@ -249,25 +250,26 @@ def correlation_3op_2t(H, state0, tlist, taulist, c_ops, a_op, b_op, c_op, taulist : array_like List of times for :math:`\tau`. taulist must be positive and contain the element `0`. - c_ops : list of {:class:`Qobj`, :class:`QobjEvo`} + c_ops : list of {:obj:`.Qobj`, :obj:`.QobjEvo`} List of collapse operators - a_op : :class:`Qobj`, :class:`QobjEvo` + a_op : :obj:`.Qobj`, :obj:`.QobjEvo` Operator A. - b_op : :class:`Qobj`, :class:`QobjEvo` + b_op : :obj:`.Qobj`, :obj:`.QobjEvo` Operator B. - c_op : :class:`Qobj`, :class:`QobjEvo` + c_op : :obj:`.Qobj`, :obj:`.QobjEvo` Operator C. - solver : str {'me', 'es'} - Choice of solver, `me` for master-equation, and `es` for exponential - series. `es` is equivalent to `me` with ``options={"method": "diag"}``. + solver : str {'me', 'es'}, default: 'me' + Choice of solver, ``me`` for master-equation, and ``es`` for + exponential series. ``es`` is equivalent to `me` with + ``options={"method": "diag"}``. options : dict, optional - Options for the solver. Only used with `me` solver. + Options for the solver. Only used with ``me`` solver. Returns ------- corr_mat : array An 2-dimensional array (matrix) of correlation values for the times - specified by `tlist` (first index) and `taulist` (second index). + specified by ``tlist`` (first index) and ``taulist`` (second index). See Also -------- @@ -293,7 +295,7 @@ def correlation_3op_2t(H, state0, tlist, taulist, c_ops, a_op, b_op, c_op, # high level correlation def coherence_function_g1( - H, state0, taulist, c_ops, a_op, solver="me", args={}, options={} + H, state0, taulist, c_ops, a_op, solver="me", args=None, options=None ): r""" Calculate the normalized first-order quantum coherence function: @@ -310,9 +312,9 @@ def coherence_function_g1( Parameters ---------- - H : :class:`Qobj`, :class:`QobjEvo` - System Hamiltonian, may be time-dependent for solver choice of `me`. - state0 : :class:`Qobj` + H : :obj:`.Qobj`, :obj:`.QobjEvo` + System Hamiltonian, may be time-dependent for solver choice of ``me``. + state0 : :obj:`.Qobj` Initial state density matrix :math:`\rho(t_0)` or state vector :math:`\psi(t_0)`. If 'state0' is 'None', then the steady state will be used as the initial state. The 'steady-state' is only implemented @@ -320,15 +322,18 @@ def coherence_function_g1( taulist : array_like List of times for :math:`\tau`. taulist must be positive and contain the element `0`. - c_ops : list of {:class:`Qobj`, :class:`QobjEvo`} + c_ops : list of {:obj:`.Qobj`, :obj:`.QobjEvo`} List of collapse operators - a_op : :class:`Qobj`, :class:`QobjEvo` + a_op : :obj:`.Qobj`, :obj:`.QobjEvo` Operator A. - solver : str {'me', 'es'} - Choice of solver, `me` for master-equation, and `es` for exponential - series. `es` is equivalent to `me` with ``options={"method": "diag"}``. + solver : str {'me', 'es'}, default: 'me' + Choice of solver, ``me`` for master-equation, and ``es`` for + exponential series. ``es`` is equivalent to `me` with + ``options={"method": "diag"}``. + args : dict, optional + dictionary of parameters for time-dependent Hamiltonians options : dict, optional - Options for the solver. Only used with `me` solver. + Options for the solver. Returns ------- @@ -353,7 +358,7 @@ def coherence_function_g1( def coherence_function_g2(H, state0, taulist, c_ops, a_op, solver="me", - args={}, options={}): + args=None, options=None): r""" Calculate the normalized second-order quantum coherence function: @@ -369,9 +374,9 @@ def coherence_function_g2(H, state0, taulist, c_ops, a_op, solver="me", Parameters ---------- - H : :class:`Qobj`, :class:`QobjEvo` - System Hamiltonian, may be time-dependent for solver choice of `me`. - state0 : :class:`Qobj` + H : :obj:`.Qobj`, :obj:`.QobjEvo` + System Hamiltonian, may be time-dependent for solver choice of ``me``. + state0 : :obj:`.Qobj` Initial state density matrix :math:`\rho(t_0)` or state vector :math:`\psi(t_0)`. If 'state0' is 'None', then the steady state will be used as the initial state. The 'steady-state' is only implemented @@ -381,16 +386,17 @@ def coherence_function_g2(H, state0, taulist, c_ops, a_op, solver="me", the element `0`. c_ops : list List of collapse operators, may be time-dependent for solver choice of - `me`. - a_op : :class:`Qobj` + ``me``. + a_op : :obj:`.Qobj` Operator A. - args : dict + args : dict, optional Dictionary of arguments to be passed to solver. - solver : str {'me', 'es'} - Choice of solver, `me` for master-equation, and `es` for exponential - series. `es` is equivalent to `me` with ``options={"method": "diag"}``. + solver : str {'me', 'es'}, default: 'me' + Choice of solver, ``me`` for master-equation, and ``es`` for + exponential series. ``es`` is equivalent to ``me`` with + ``options={"method": "diag"}``. options : dict, optional - Options for the solver. Only used with `me` solver. + Options for the solver. Returns ------- @@ -434,16 +440,16 @@ def correlation_3op(solver, state0, tlist, taulist, A=None, B=None, C=None): :math:`\left`. - from a open system :class:`Solver`. + from a open system :class:`.Solver`. Note: it is not possible to calculate a physically meaningful correlation where :math:`\tau<0`. Parameters ---------- - solver : :class:`MESolver`, :class:`BRSolver` + solver : :class:`.MESolver`, :class:`.BRSolver` Qutip solver for an open system. - state0 : :class:`Qobj` + state0 : :obj:`.Qobj` Initial state density matrix :math:`\rho(t_0)` or state vector :math:`\psi(t_0)`. tlist : array_like @@ -452,7 +458,7 @@ def correlation_3op(solver, state0, tlist, taulist, A=None, B=None, C=None): taulist : array_like List of times for :math:`\tau`. taulist must be positive and contain the element `0`. - A, B, C: :class:`Qobj`, :class:`QobjEvo`, optional, default=None + A, B, C : :class:`.Qobj`, :class:`.QobjEvo`, optional, default=None Operators ``A``, ``B``, ``C`` from the equation ```` in the Schrodinger picture. They do not need to be all provided. For exemple, if ``A`` is not provided, ```` is computed. @@ -461,8 +467,8 @@ def correlation_3op(solver, state0, tlist, taulist, A=None, B=None, C=None): ------- corr_mat : array An 2-dimensional array (matrix) of correlation values for the times - specified by `tlist` (first index) and `taulist` (second index). If - `tlist` is `None`, then a 1-dimensional array of correlation values + specified by ``tlist`` (first index) and `taulist` (second index). If + ``tlist`` is ``None``, then a 1-dimensional array of correlation values is returned instead. """ taulist = np.asarray(taulist) diff --git a/qutip/solver/countstat.py b/qutip/solver/countstat.py index f6a3a4341b..a37a66e0ea 100644 --- a/qutip/solver/countstat.py +++ b/qutip/solver/countstat.py @@ -30,14 +30,14 @@ def countstat_current(L, c_ops=None, rhoss=None, J_ops=None): Parameters ---------- - L : :class:`qutip.Qobj` + L : :class:`.Qobj` Qobj representing the system Liouvillian. c_ops : array / list (optional) List of current collapse operators. Required if either ``rhoss`` or ``J_ops`` is not given. - rhoss : :class:`qutip.Qobj` (optional) + rhoss : :class:`.Qobj` (optional) The steadystate density matrix for the given system Liouvillian ``L`` and collapse operators. If not given, it defaults to ``steadystate(L, c_ops)``. @@ -155,13 +155,13 @@ def countstat_current_noise(L, c_ops, wlist=None, rhoss=None, J_ops=None, Parameters ---------- - L : :class:`qutip.Qobj` + L : :class:`.Qobj` Qobj representing the system Liouvillian. c_ops : array / list List of current collapse operators. - rhoss : :class:`qutip.Qobj` (optional) + rhoss : :class:`.Qobj` (optional) The steadystate density matrix corresponding the system Liouvillian `L`. @@ -189,7 +189,7 @@ def countstat_current_noise(L, c_ops, wlist=None, rhoss=None, J_ops=None, .. note:: The algoryth is described in page 67 of "Electrons in nanostructures" C. Flindt, PhD Thesis, available online: - https://orbit.dtu.dk/fedora/objects/orbit:82314/datastreams/file_4732600/content + https://orbit.dtu.dk/en/publications/electrons-in-nanostructures-coherent-manipulation-and-counting-st Returns -------- diff --git a/qutip/solver/cy/nm_mcsolve.pyx b/qutip/solver/cy/nm_mcsolve.pyx index 41dff11a97..bf58763163 100644 --- a/qutip/solver/cy/nm_mcsolve.pyx +++ b/qutip/solver/cy/nm_mcsolve.pyx @@ -21,7 +21,7 @@ cdef class RateShiftCoefficient(Coefficient): Parameters ---------- - coeffs : list of :class:`Coefficient` + coeffs : list of :obj:`.Coefficient` The list of coefficients to determine the rate shift of. """ def __init__(self, list coeffs): @@ -34,7 +34,7 @@ cdef class RateShiftCoefficient(Coefficient): """ Replace the arguments (``args``) of a coefficient. - Returns a new :obj:`Coefficient` if the coefficient has arguments, or + Returns a new :obj:`.Coefficient` if the coefficient has arguments, or the original coefficient if it does not. Arguments to replace may be supplied either in a dictionary as the first position argument, or passed as keywords, or as a combination of the two. Arguments not @@ -70,7 +70,7 @@ cdef class RateShiftCoefficient(Coefficient): return real(self._call(t)) cpdef Coefficient copy(self): - """Return a copy of the :obj:`Coefficient`.""" + """Return a copy of the :obj:`.Coefficient`.""" return RateShiftCoefficient( [coeff.copy() for coeff in self.coeffs], ) @@ -89,7 +89,7 @@ cdef class SqrtRealCoefficient(Coefficient): """ Replace the arguments (``args``) of a coefficient. - Returns a new :obj:`Coefficient` if the coefficient has arguments, or + Returns a new :obj:`.Coefficient` if the coefficient has arguments, or the original coefficient if it does not. Arguments to replace may be supplied either in a dictionary as the first position argument, or passed as keywords, or as a combination of the two. Arguments not @@ -112,5 +112,5 @@ cdef class SqrtRealCoefficient(Coefficient): return sqrt(real(self.base._call(t))) cpdef Coefficient copy(self): - """Return a copy of the :obj:`Coefficient`.""" + """Return a copy of the :obj:`.Coefficient`.""" return SqrtRealCoefficient(self.base.copy()) diff --git a/qutip/solver/floquet.py b/qutip/solver/floquet.py index 61c139d349..d5d1d20b60 100644 --- a/qutip/solver/floquet.py +++ b/qutip/solver/floquet.py @@ -8,6 +8,7 @@ import numpy as np from qutip.core import data as _data +from qutip.core.data import Data from qutip import Qobj, QobjEvo from .propagator import Propagator from .mesolve import MESolver @@ -24,10 +25,10 @@ class FloquetBasis: Attributes ---------- - U : :class:`Propagator` + U : :class:`.Propagator` The propagator of the Hamiltonian over one period. - evecs : :class:`qutip.data.Data` + evecs : :class:`.Data` Matrix where each column is an initial Floquet mode. e_quasi : np.ndarray[float] @@ -47,7 +48,7 @@ def __init__( """ Parameters ---------- - H : :class:`Qobj`, :class:`QobjEvo`, QobjEvo compatible format. + H : :obj:`.Qobj`, :obj:`.QobjEvo`, QobjEvo compatible format. System Hamiltonian, with period `T`. T : float @@ -87,6 +88,11 @@ def __init__( # Default computation tlist = np.linspace(0, T, 101) memoize = 101 + if ( + isinstance(H, QobjEvo) + and (H._feedback_functions or H._solver_only_feedback) + ): + raise NotImplementedError("FloquetBasis does not support feedback") self.U = Propagator(H, args=args, options=options, memoize=memoize) for t in tlist: # Do the evolution by steps to save the intermediate results. @@ -110,7 +116,7 @@ def _as_ketlist(self, kets_mat): """ dims = [self.U(0).dims[0], [1]] return [ - Qobj(ket, dims=dims, type="ket") + Qobj(ket, dims=dims) for ket in _data.split_columns(kets_mat) ] @@ -129,7 +135,7 @@ def mode(self, t, data=False): Returns ------- - output : list[:class:`Qobj`], :class:`qutip.data.Data` + output : list[:obj:`.Qobj`], :class:`.Data` A list of Floquet states for the time ``t`` or the states as column in a single matrix. """ @@ -160,7 +166,7 @@ def state(self, t, data=False): Returns ------- - output : list[:class:`Qobj`], :class:`qutip.data.Data` + output : list[:obj:`.Qobj`], :class:`.Data` A list of Floquet states for the time ``t`` or the states as column in a single matrix. """ @@ -181,7 +187,7 @@ def from_floquet_basis(self, floquet_basis, t=0): Parameters ---------- - floquet_basis : :class:`Qobj`, :class:`qutip.data.Data` + floquet_basis : :obj:`.Qobj`, :class:`.Data` Initial state in the Floquet basis at time ``t``. May be either a ket or density matrix. @@ -190,7 +196,7 @@ def from_floquet_basis(self, floquet_basis, t=0): Returns ------- - output : :class:`Qobj`, :class:`qutip.data.Data` + output : :obj:`.Qobj`, :class:`.Data` The state in the lab basis. The return type is the same as the type of the input state. """ @@ -219,7 +225,7 @@ def to_floquet_basis(self, lab_basis, t=0): Parameters ---------- - lab_basis : :class:`Qobj`, :class:`qutip.data.Data` + lab_basis : :obj:`.Qobj`, :class:`.Data` Initial state in the lab basis. t : float [0] @@ -227,7 +233,7 @@ def to_floquet_basis(self, lab_basis, t=0): Returns ------- - output : :class:`Qobj`, :class:`qutip.data.Data` + output : :obj:`.Qobj`, :class:`.Data` The state in the Floquet basis. The return type is the same as the type of the input state. """ @@ -283,7 +289,7 @@ def _floquet_X_matrices(floquet_basis, c_ops, kmax, ntimes=100): floquet_basis : :class:`FloquetBasis` The system Hamiltonian wrapped in a FloquetBasis object. - c_ops : list of :class:`Qobj` + c_ops : list of :obj:`.Qobj` The collapse operators describing the dissipation. kmax : int @@ -294,7 +300,7 @@ def _floquet_X_matrices(floquet_basis, c_ops, kmax, ntimes=100): Returns ------- - X : list of dict of :class:`qutip.data.Data` + X : list of dict of :class:`.Data` A dict of the sidebands ``k`` for the X matrices of each c_ops """ T = floquet_basis.T @@ -321,7 +327,7 @@ def _floquet_gamma_matrices(X, delta, J_cb): Parameters ---------- - X : list of dict of :class:`qutip.data.Data` + X : list of dict of :class:`.Data` Floquet X matrices created by :func:`_floquet_X_matrices`. delta : np.ndarray @@ -336,7 +342,7 @@ def _floquet_gamma_matrices(X, delta, J_cb): Returns ------- - gammas : dict of :class:`qutip.data.Data` + gammas : dict of :class:`.Data` A dict mapping the sidebands ``k`` to their gamma matrices. """ N = delta.shape[0] @@ -369,7 +375,7 @@ def _floquet_A_matrix(delta, gamma, w_th): delta : np.ndarray Floquet delta tensor created by :func:`_floquet_delta_tensor`. - gamma : dict of :class:`qutip.data.Data` + gamma : dict of :class:`.Data` Floquet gamma matrices created by :func:`_floquet_gamma_matrices`. w_th : float @@ -411,7 +417,7 @@ def _floquet_master_equation_tensor(A): Parameters ---------- - A : :class:`qutip.data.Data` + A : :class:`.Data` Floquet-Markov master equation rate matrix. Returns @@ -447,25 +453,29 @@ def floquet_tensor(H, c_ops, spectra_cb, T=0, w_th=0.0, kmax=5, nT=100): Parameters ---------- - H : :class:`QobjEvo` - Periodic Hamiltonian + H : :obj:`.QobjEvo`, :obj:`.FloquetBasis` + Periodic Hamiltonian a floquet basis system. - T : float - The period of the time-dependence of the hamiltonian. + T : float, optional + The period of the time-dependence of the hamiltonian. Optional if ``H`` + is a ``FloquetBasis`` object. - c_ops : list of :class:`qutip.qobj` + c_ops : list of :class:`.Qobj` list of collapse operators. spectra_cb : list callback functions List of callback functions that compute the noise power spectrum as a function of frequency for the collapse operators in `c_ops`. - w_th : float + w_th : float, default: 0.0 The temperature in units of frequency. - kmax : int + kmax : int, default: 5 The truncation of the number of sidebands (default 5). + nT : int, default: 100 + The number of integration steps (for calculating X) within one period. + Returns ------- output : array @@ -483,9 +493,7 @@ def floquet_tensor(H, c_ops, spectra_cb, T=0, w_th=0.0, kmax=5, nT=100): a = _floquet_A_matrix(delta, gamma, w_th) r = _floquet_master_equation_tensor(a) dims = floquet_basis.U(0).dims - return Qobj( - r, dims=[dims, dims], type="super", superrep="super", copy=False - ) + return Qobj(r, dims=[dims, dims], superrep="super", copy=False) def fsesolve(H, psi0, tlist, e_ops=None, T=0.0, args=None, options=None): @@ -494,18 +502,18 @@ def fsesolve(H, psi0, tlist, e_ops=None, T=0.0, args=None, options=None): Parameters ---------- - H : :class:`Qobj`, :class:`QobjEvo`, :class:`QobjEvo` compatible format. - Periodic system Hamiltonian as :class:`QobjEvo`. List of - [:class:`Qobj`, :class:`Coefficient`] or callable that - can be made into :class:`QobjEvo` are also accepted. + H : :obj:`.Qobj`, :obj:`.QobjEvo`, :obj:`.QobjEvo` compatible format. + Periodic system Hamiltonian as :obj:`.QobjEvo`. List of + [:obj:`.Qobj`, :obj:`.Coefficient`] or callable that + can be made into :obj:`.QobjEvo` are also accepted. - psi0 : :class:`qutip.qobj` + psi0 : :class:`.Qobj` Initial state vector (ket). If an operator is provided, tlist : *list* / *array* List of times for :math:`t`. - e_ops : list of :class:`qutip.qobj` / callback function, optional + e_ops : list of :class:`.Qobj` / callback function, optional List of operators for which to evaluate expectation values. If this list is empty, the state vectors for each time in `tlist` will be returned instead of expectation values. @@ -519,20 +527,20 @@ def fsesolve(H, psi0, tlist, e_ops=None, T=0.0, args=None, options=None): options : dict, optional Options for the results. - - store_final_state : bool - Whether or not to store the final state of the evolution in the - result class. - - store_states : bool, None - 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. + - | store_final_state : bool + | Whether or not to store the final state of the evolution in the + result class. + - | store_states : bool, None + | 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. Returns ------- - output : :class:`qutip.solver.Result` - An instance of the class :class:`qutip.solver.Result`, which + output : :class:`.Result` + An instance of the class :class:`.Result`, which contains either an *array* of expectation values or an array of state vectors, for the times specified by `tlist`. """ @@ -576,34 +584,34 @@ def fmmesolve( Parameters ---------- - H : :class:`Qobj`, :class:`QobjEvo`, :class:`QobjEvo` compatible format. - Periodic system Hamiltonian as :class:`QobjEvo`. List of - [:class:`Qobj`, :class:`Coefficient`] or callable that - can be made into :class:`QobjEvo` are also accepted. + H : :obj:`.Qobj`, :obj:`.QobjEvo`, :obj:`.QobjEvo` compatible format. + Periodic system Hamiltonian as :obj:`.QobjEvo`. List of + [:obj:`.Qobj`, :obj:`.Coefficient`] or callable that + can be made into :obj:`.QobjEvo` are also accepted. - rho0 / psi0 : :class:`qutip.Qobj` + rho0 / psi0 : :class:`.Qobj` Initial density matrix or state vector (ket). tlist : *list* / *array* List of times for :math:`t`. - c_ops : list of :class:`qutip.Qobj` + c_ops : list of :class:`.Qobj`, optional List of collapse operators. Time dependent collapse operators are not - supported. + supported. Fall back on :func:`fsesolve` if not provided. - e_ops : list of :class:`qutip.Qobj` / callback function + e_ops : list of :class:`.Qobj` / callback function, optional List of operators for which to evaluate expectation values. The states are reverted to the lab basis before applying the - spectra_cb : list callback functions + spectra_cb : list callback functions, default: ``lambda w: (w > 0)`` List of callback functions that compute the noise power spectrum as a function of frequency for the collapse operators in `c_ops`. - T : float + T : float, default=tlist[-1] The period of the time-dependence of the hamiltonian. The default value - 'None' indicates that the 'tlist' spans a single period of the driving. + ``0`` indicates that the 'tlist' spans a single period of the driving. - w_th : float + w_th : float, default: 0.0 The temperature of the environment in units of frequency. For example, if the Hamiltonian written in units of 2pi GHz, and the temperature is given in K, use the following conversion: @@ -613,50 +621,50 @@ def fmmesolve( kB = 1.38e-23 args['w_th'] = temperature * (kB / h) * 2 * pi * 1e-9 - args : *dictionary* + args : dict, optional Dictionary of parameters for time-dependent Hamiltonian - options : None / dict + options : dict, optional Dictionary of options for the solver. - - store_final_state : bool - Whether or not to store the final state of the evolution in the - result class. - - store_states : bool, None - Whether or not to store the state vectors or density matrices. - On `None` the states will be saved if no expectation operators are - given. - - store_floquet_states : bool - Whether or not to store the density matrices in the floquet basis in - ``result.floquet_states``. - - 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 - if not installed. Empty string or False will disable the bar. - - progress_kwargs : dict - kwargs to pass to the progress_bar. Qutip's bars use `chunk_size`. - - method : str ["adams", "bdf", "lsoda", "dop853", "vern9", etc.] - Which differential equation integration method to use. - - atol, rtol : float - Absolute and relative tolerance of the ODE integrator. - - nsteps : - Maximum number of (internally defined) steps allowed in one ``tlist`` - step. - - max_step : float, 0 - Maximum lenght of one internal step. When using pulses, it should be - less than half the width of the thinnest pulse. + - | store_final_state : bool + | Whether or not to store the final state of the evolution in the + result class. + - | store_states : bool, None + | Whether or not to store the state vectors or density matrices. + On `None` the states will be saved if no expectation operators are + given. + - | store_floquet_states : bool + | Whether or not to store the density matrices in the floquet basis + in ``result.floquet_states``. + - | 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 + if not installed. Empty string or False will disable the bar. + - | progress_kwargs : dict + | kwargs to pass to the progress_bar. Qutip's bars use `chunk_size`. + - | method : str ["adams", "bdf", "lsoda", "dop853", "vern9", etc.] + | Which differential equation integration method to use. + - | atol, rtol : float + | Absolute and relative tolerance of the ODE integrator. + - | nsteps : int + | 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 less than half the width of the thinnest pulse. Other options could be supported depending on the integration method, see `Integrator <./classes.html#classes-ode>`_. Returns ------- - result: :class:`qutip.Result` + result: :class:`.Result` - An instance of the class :class:`qutip.Result`, which contains - the expectation values for the times specified by `tlist`, and/or the + An instance of the class :class:`.Result`, which contains + the expectation values for the times specified by ``tlist``, and/or the state density matrices corresponding to the times. """ if c_ops is None and rho0.isket: @@ -724,14 +732,14 @@ class FMESolver(MESolver): Parameters ---------- - floquet_basis : :class:`qutip.FloquetBasis` + floquet_basis : :class:`.FloquetBasis` The system Hamiltonian wrapped in a FloquetBasis object. Choosing a different integrator for the ``floquet_basis`` than for the evolution of the floquet state can improve the performance. - a_ops : list of tuple(:class:`qutip.Qobj`, callable) + a_ops : list of tuple(:class:`.Qobj`, callable) List of collapse operators and the corresponding function for the noise - power spectrum. The collapse operator must be a :class:`Qobj` and + power spectrum. The collapse operator must be a :obj:`.Qobj` and cannot be time dependent. The spectrum function must take and return an numpy array. @@ -751,7 +759,7 @@ class FMESolver(MESolver): name = "fmmesolve" _avail_integrators = {} - resultclass = FloquetResult + _resultclass = FloquetResult solver_options = { "progress_bar": "text", "progress_kwargs": {"chunk_size": 10}, @@ -815,7 +823,7 @@ def start(self, state0, t0, *, floquet=False): Parameters ---------- - state0 : :class:`Qobj` + state0 : :obj:`.Qobj` Initial state of the evolution. t0 : double @@ -831,7 +839,7 @@ def start(self, state0, t0, *, floquet=False): def step(self, t, *, args=None, copy=True, floquet=False): """ - Evolve the state to ``t`` and return the state as a :class:`Qobj`. + Evolve the state to ``t`` and return the state as a :obj:`.Qobj`. Parameters ---------- @@ -848,10 +856,11 @@ def step(self, t, *, args=None, copy=True, floquet=False): args : dict, optional {None} Not supported - .. note:: - The state must be initialized first by calling ``start`` or - ``run``. If ``run`` is called, ``step`` will continue from the last - time and state obtained. + Notes + ----- + The state must be initialized first by calling ``start`` or + ``run``. If ``run`` is called, ``step`` will continue from the last + time and state obtained. """ if args: raise ValueError("FMESolver cannot update arguments") @@ -868,12 +877,12 @@ def run(self, state0, tlist, *, floquet=False, args=None, e_ops=None): For a ``state0`` at time ``tlist[0]`` do the evolution as directed by ``rhs`` and for each time in ``tlist`` store the state and/or - expectation values in a :class:`Result`. The evolution method and + expectation values in a :class:`.Result`. The evolution method and stored results are determined by ``options``. Parameters ---------- - state0 : :class:`Qobj` + state0 : :obj:`.Qobj` Initial state of the evolution. tlist : list of double @@ -895,7 +904,7 @@ def run(self, state0, tlist, *, floquet=False, args=None, e_ops=None): Returns ------- - results : :class:`qutip.solver.FloquetResult` + results : :class:`FloquetResult` Results of the evolution. States and/or expect will be saved. You can control the saved data in the options. """ @@ -907,7 +916,7 @@ def run(self, state0, tlist, *, floquet=False, args=None, e_ops=None): _data0 = self._prepare_state(state0) self._integrator.set_state(tlist[0], _data0) stats = self._initialize_stats() - results = self.resultclass( + results = self._resultclass( e_ops, self.options, solver=self.name, @@ -929,3 +938,26 @@ def run(self, state0, tlist, *, floquet=False, args=None, e_ops=None): # TODO: It would be nice if integrator could give evolution statistics # stats.update(_integrator.stats) return results + + @classmethod + def ExpectFeedback(cls): + """ + Expect of the state of the evolution to be used in a time-dependent + operator. + + Not not implemented for FMESolver + """ + raise NotImplementedError( + "Feedback not implemented for floquet solver." + ) + + @classmethod + def StateFeedback(cls): + """ + State of the evolution to be used in a time-dependent operator. + + Not not implemented for FMESolver + """ + raise NotImplementedError( + "Feedback not implemented for floquet solver." + ) diff --git a/qutip/solver/floquet_bwcomp.py b/qutip/solver/floquet_bwcomp.py index 8ee683ffd8..6f3a059466 100644 --- a/qutip/solver/floquet_bwcomp.py +++ b/qutip/solver/floquet_bwcomp.py @@ -28,7 +28,7 @@ def floquet_modes(H, T, args=None, sort=False, U=None, options=None): Calculate the initial Floquet modes Phi_alpha(0) for a driven system with period T. - Deprecated from qutip v5. Use :class:`FloquetBasis` instead: + Deprecated from qutip v5. Use :class:`.FloquetBasis` instead: fbasis = FloquetBasis(H, T, args=args, options=options, sort=sort) f_mode_0 = fbasis.mode(0) @@ -48,7 +48,7 @@ def floquet_modes_t(f_modes_0, f_energies, t, H, T, args=None, options=None): Calculate the Floquet modes at times tlist Phi_alpha(tlist) propagting the initial Floquet modes Phi_alpha(0). - Deprecated from qutip v5. Use :class:`FloquetBasis` instead: + Deprecated from qutip v5. Use :class:`.FloquetBasis` instead: fbasis = FloquetBasis(H, T, args=args, options=options) f_mode_t = fbasis.mode(t) @@ -68,7 +68,7 @@ def floquet_modes_table( period. Can later be used as a table to look up the floquet modes for any time. - Deprecated from qutip v5. Use :class:`FloquetBasis` instead: + Deprecated from qutip v5. Use :class:`.FloquetBasis` instead: fbasis = FloquetBasis(H, T, args=args, options=options, precompute=tlist) """ @@ -80,7 +80,7 @@ def floquet_modes_t_lookup(f_modes_table_t, t, T): Lookup the floquet mode at time t in the pre-calculated table of floquet modes in the first period of the time-dependence. - Deprecated from qutip v5. Use :class:`FloquetBasis` instead: + Deprecated from qutip v5. Use :class:`.FloquetBasis` instead: f_modes_table_t = fbasis = FloquetBasis(...) f_mode_t = f_modes_table_t.mode(t) @@ -95,7 +95,7 @@ def floquet_states(f_modes_t, f_energies, t): """ Evaluate the floquet states at time t given the Floquet modes at that time. - Deprecated from qutip v5. Use :class:`FloquetBasis` instead: + Deprecated from qutip v5. Use :class:`.FloquetBasis` instead: fbasis = FloquetBasis(H, T, args=args, options=options) f_state_t = fbasis.state(t) @@ -114,7 +114,7 @@ def floquet_states_t(f_modes_0, f_energies, t, H, T, args=None, options=None): """ Evaluate the floquet states at time t given the initial Floquet modes. - Deprecated from qutip v5. Use :class:`FloquetBasis` instead: + Deprecated from qutip v5. Use :class:`.FloquetBasis` instead: fbasis = FloquetBasis(H, T, args=args, options=options) f_state_t = fbasis.state(t) @@ -132,7 +132,7 @@ def floquet_wavefunction(f_modes_t, f_energies, f_coeff, t): Evaluate the wavefunction for a time t using the Floquet state decompositon, given the Floquet modes at time `t`. - Deprecated from qutip v5. Use :class:`FloquetBasis` instead: + Deprecated from qutip v5. Use :class:`.FloquetBasis` instead: fbasis = FloquetBasis(H, T, args=args, options=options) psi_t = fbasis.from_floquet_basis(f_coeff, t) @@ -150,7 +150,7 @@ def floquet_wavefunction_t( Evaluate the wavefunction for a time t using the Floquet state decompositon, given the initial Floquet modes. - Deprecated from qutip v5. Use :class:`FloquetBasis` instead: + Deprecated from qutip v5. Use :class:`.FloquetBasis` instead: fbasis = FloquetBasis(H, T, args=args, options=options) psi_t = fbasis.from_floquet_basis(f_coeff, t) @@ -168,7 +168,7 @@ def floquet_state_decomposition(f_states, f_energies, psi): Decompose the wavefunction `psi` (typically an initial state) in terms of the Floquet states, :math:`\psi = \sum_\alpha c_\alpha \psi_\alpha(0)`. - Deprecated from qutip v5. Use :class:`FloquetBasis` instead: + Deprecated from qutip v5. Use :class:`.FloquetBasis` instead: fbasis = FloquetBasis(H, T, args=args, options=options) f_coeff = fbasis.to_floquet_basis(psi) @@ -208,9 +208,9 @@ def floquet_master_equation_rates( No longer used. f_energies : Any No longer used. - c_op : :class:`qutip.qobj` + c_op : :class:`.Qobj` The collapse operators describing the dissipation. - H : :class:`qutip.qobj` + H : :class:`.Qobj` System Hamiltonian, time-dependent with period `T`. T : float The period of the time-dependence of the hamiltonian. diff --git a/qutip/solver/heom/bofin_solvers.py b/qutip/solver/heom/bofin_solvers.py index d38553c902..1061c1c492 100644 --- a/qutip/solver/heom/bofin_solvers.py +++ b/qutip/solver/heom/bofin_solvers.py @@ -136,8 +136,8 @@ def idx(self, label): int The index of the label within the list of ADO labels. - Note - ---- + Notes + ----- This implementation of the ``.idx(...)`` method is just for reference and documentation. To avoid the cost of a Python function call, it is replaced with @@ -326,7 +326,7 @@ class HierarchyADOsState: Parameters ---------- - rho : :class:`~qutip.Qobj` + rho : :class:`.Qobj` The current state of the system (i.e. the 0th component of the hierarchy). ados : :class:`HierarchyADOs` @@ -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( @@ -427,10 +435,10 @@ def heomsolve( Parameters ---------- - H : :class:`Qobj`, :class:`QobjEvo` + H : :obj:`.Qobj`, :obj:`.QobjEvo` Possibly time-dependent system Liouvillian or Hamiltonian as a Qobj or - QobjEvo. list of [:class:`Qobj`, :class:`Coefficient`] or callable that - can be made into :class:`QobjEvo` are also accepted. + QobjEvo. list of [:obj:`.Qobj`, :obj:`.Coefficient`] or callable that + can be made into :obj:`.QobjEvo` are also accepted. bath : Bath or list of Bath A :obj:`Bath` containing the exponents of the expansion of the @@ -444,9 +452,9 @@ def heomsolve( The maximum depth of the heirarchy (i.e. the maximum number of bath exponent "excitations" to retain). - state0 : :class:`~Qobj` or :class:`~HierarchyADOsState` or array-like - If ``rho0`` is a :class:`~Qobj` the it is the initial state - of the system (i.e. a :obj:`~Qobj` density matrix). + state0 : :obj:`.Qobj` or :class:`~HierarchyADOsState` or array-like + If ``rho0`` is a :obj:`.Qobj` the it is the initial state + of the system (i.e. a :obj:`.Qobj` density matrix). If it is a :class:`~HierarchyADOsState` or array-like, then ``rho0`` gives the initial state of all ADOs. @@ -465,8 +473,8 @@ def heomsolve( An ordered list of times at which to return the value of the state. e_ops : Qobj / QobjEvo / callable / list / dict / None, optional - A list or dictionary of operators as :class:`~Qobj`, - :class:`~QobjEvo` and/or callable functions (they can be mixed) or + A list or dictionary of operators as :obj:`.Qobj`, + :obj:`.QobjEvo` and/or callable functions (they can be mixed) or a single operator or callable function. For an operator ``op``, the result will be computed using ``(state * op).tr()`` and the state at each time ``t``. For callable functions, ``f``, the result is @@ -474,43 +482,43 @@ def heomsolve( ``expect`` and ``e_data`` attributes of the result (see the return section below). - args : dict, optional {None} + args : dict, optional Change the ``args`` of the RHS for the evolution. - options : dict, optional {None} + options : dict, optional Generic solver options. - - store_final_state : bool - Whether or not to store the final state of the evolution in the - result class. - - store_states : bool, None - Whether or not to store the state vectors or density matrices. - On `None` the states will be saved if no expectation operators are - given. - - store_ados : bool {False, True} - Whether or not to store the HEOM ADOs. - - 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 - if not installed. Empty string or False will disable the bar. - - progress_kwargs : dict - kwargs to pass to the progress_bar. Qutip's bars use `chunk_size`. - - state_data_type: str {'dense'} - Name of the data type of the state used during the ODE evolution. - Use an empty string to keep the input state type. Many integrator can - only work with `Dense`. - - method : str ["adams", "bdf", "lsoda", "dop853", "vern9", etc.] - Which differential equation integration method to use. - - atol, rtol : float - Absolute and relative tolerance of the ODE integrator. - - nsteps : - Maximum number of (internally defined) steps allowed in one ``tlist`` - step. - - max_step : float, 0 - Maximum lenght of one internal step. When using pulses, it should be - less than half the width of the thinnest pulse. + - | store_final_state : bool + | Whether or not to store the final state of the evolution in the + result class. + - | store_states : bool, None + | Whether or not to store the state vectors or density matrices. + On `None` the states will be saved if no expectation operators are + given. + - | store_ados : bool + | Whether or not to store the HEOM ADOs. + - | 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 + if not installed. Empty string or False will disable the bar. + - | progress_kwargs : dict + | kwargs to pass to the progress_bar. Qutip's bars use `chunk_size`. + - | state_data_type: str {'dense', 'CSR', 'Dia', } + | Name of the data type of the state used during the ODE evolution. + Use an empty string to keep the input state type. Many integrator + can only work with `Dense`. + - | method : str ["adams", "bdf", "lsoda", "dop853", "vern9", etc.] + | Which differential equation integration method to use. + - | atol, rtol : float + | Absolute and relative tolerance of the ODE integrator. + - | nsteps : int + | 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 less than half the width of the thinnest pulse. Returns ------- @@ -537,7 +545,7 @@ def heomsolve( at tme ``t``. The keys are those given by ``e_ops`` if it was a dict, otherwise they are the indexes of the supplied ``e_ops``. - See :class:`~HEOMResult` and :class:`~Result` for the complete + See :class:`~HEOMResult` and :class:`.Result` for the complete list of attributes. """ H = QobjEvo(H, args=args, tlist=tlist) @@ -553,10 +561,10 @@ class HEOMSolver(Solver): Parameters ---------- - H : :class:`Qobj`, :class:`QobjEvo` + H : :obj:`.Qobj`, :obj:`.QobjEvo` Possibly time-dependent system Liouvillian or Hamiltonian as a Qobj or - QobjEvo. list of [:class:`Qobj`, :class:`Coefficient`] or callable that - can be made into :class:`QobjEvo` are also accepted. + QobjEvo. list of [:obj:`.Qobj`, :obj:`.Coefficient`] or callable that + can be made into :obj:`.QobjEvo` are also accepted. bath : Bath or list of Bath A :obj:`Bath` containing the exponents of the expansion of the @@ -581,7 +589,7 @@ class HEOMSolver(Solver): The description of the hierarchy constructed from the given bath and maximum depth. - rhs : :obj:`QobjEvo` + rhs : :obj:`.QobjEvo` The right-hand side (RHS) of the hierarchy evolution ODE. Internally the system and bath coupling operators are converted to :class:`qutip.data.CSR` instances during construction of the RHS, @@ -589,7 +597,7 @@ class HEOMSolver(Solver): """ name = "heomsolver" - resultclass = HEOMResult + _resultclass = HEOMResult _avail_integrators = {} solver_options = { "progress_bar": "text", @@ -847,7 +855,7 @@ def _calculate_rhs(self): """ Make the full RHS required by the solver. """ rhs_mat = self._rhs() rhs_dims = [ - self._sup_shape * self._n_ados, self._sup_shape * self._n_ados + [self._sup_shape * self._n_ados], [self._sup_shape * self._n_ados] ] h_identity = _data.identity(self._n_ados, dtype="csr") @@ -903,16 +911,16 @@ def steady_state( Specifies the the maximum number of iterative refinement steps that the MKL PARDISO solver performs. - For a complete description, see iparm(8) in - http://cali2.unilim.fr/intel-xe/mkl/mklman/GUID-264E311E-ACED-4D56-AC31-E9D3B11D1CBF.htm. + For a complete description, see iparm(7) in + https://www.intel.com/content/www/us/en/docs/onemkl/developer-reference-c/2023-0/pardiso-iparm-parameter.html mkl_weighted_matching : bool MKL PARDISO can use a maximum weighted matching algorithm to permute large elements close the diagonal. This strategy adds an additional level of reliability to the factorization methods. - For a complete description, see iparm(13) in - http://cali2.unilim.fr/intel-xe/mkl/mklman/GUID-264E311E-ACED-4D56-AC31-E9D3B11D1CBF.htm. + For a complete description, see iparm(12) in + https://www.intel.com/content/www/us/en/docs/onemkl/developer-reference-c/2023-0/pardiso-iparm-parameter.html Returns ------- @@ -922,7 +930,7 @@ def steady_state( steady_ados : :class:`HierarchyADOsState` The steady state of the full ADO hierarchy. A particular ADO may be extracted from the full state by calling - :meth:`HEOMSolver.extract`. + :meth:`extract`. """ if not self.L_sys.isconstant: raise ValueError( @@ -958,8 +966,8 @@ def steady_state( L = L.tocsc() solution = spsolve(L, b_mat) - data = _data.Dense(solution[:n ** 2].reshape((n, n))) - data = _data.mul(_data.add(data, data.conj()), 0.5) + data = _data.Dense(solution[:n ** 2].reshape((n, n), order='F')) + data = _data.mul(_data.add(data, data.adjoint()), 0.5) steady_state = Qobj(data, dims=self._sys_dims) solution = solution.reshape((self._n_ados, n, n)) @@ -973,9 +981,9 @@ def run(self, state0, tlist, *, args=None, e_ops=None): Parameters ---------- - state0 : :class:`~Qobj` or :class:`~HierarchyADOsState` or array-like - If ``rho0`` is a :class:`~Qobj` the it is the initial state - of the system (i.e. a :obj:`~Qobj` density matrix). + state0 : :obj:`.Qobj` or :class:`~HierarchyADOsState` or array-like + If ``rho0`` is a :obj:`.Qobj` the it is the initial state + of the system (i.e. a :obj:`.Qobj` density matrix). If it is a :class:`~HierarchyADOsState` or array-like, then ``rho0`` gives the initial state of all ADOs. @@ -997,8 +1005,8 @@ def run(self, state0, tlist, *, args=None, e_ops=None): Change the ``args`` of the RHS for the evolution. e_ops : Qobj / QobjEvo / callable / list / dict / None, optional - A list or dictionary of operators as :class:`~Qobj`, - :class:`~QobjEvo` and/or callable functions (they can be mixed) or + A list or dictionary of operators as :obj:`.Qobj`, + :obj:`.QobjEvo` and/or callable functions (they can be mixed) or a single operator or callable function. For an operator ``op``, the result will be computed using ``(state * op).tr()`` and the state at each time ``t``. For callable functions, ``f``, the result is @@ -1031,7 +1039,7 @@ def run(self, state0, tlist, *, args=None, e_ops=None): at tme ``t``. The keys are those given by ``e_ops`` if it was a dict, otherwise they are the indexes of the supplied ``e_ops``. - See :class:`~HEOMResult` and :class:`~Result` for the complete + See :class:`~HEOMResult` and :class:`.Result` for the complete list of attributes. """ return super().run(state0, tlist, args=args, e_ops=e_ops) @@ -1098,7 +1106,7 @@ def start(self, state0, t0): Parameters ---------- - state0 : :class:`Qobj` + state0 : :obj:`.Qobj` Initial state of the evolution. This may provide either just the initial density matrix of the system, or the full set of ADOs for the hierarchy. See the documentation for ``rho0`` in the @@ -1114,36 +1122,36 @@ def options(self): """ Options for HEOMSolver: - store_final_state: bool, default=False + store_final_state: bool, default: False Whether or not to store the final state of the evolution in the result class. - store_states: bool, default=None + store_states: bool, default: None 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, default=False + normalize_output: bool, default: False Normalize output state to hide ODE numerical errors. - progress_bar: str {'text', 'enhanced', 'tqdm', ''}, default="text" + 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. - progress_kwargs: dict, default={"chunk_size": 10} + progress_kwargs: dict, default: {"chunk_size": 10} Arguments to pass to the progress_bar. Qutip's bars use ``chunk_size``. - method: str, default="adams" + method: str, default: "adams" Which ordinary differential equation integration method to use. - state_data_type: str, default="dense" + state_data_type: str, default: "dense" Name of the data type of the state used during the ODE evolution. Use an empty string to keep the input state type. Many integrators support only work with `Dense`. - store_ados : bool, default=False + store_ados : bool, default: False Whether or not to store the HEOM ADOs. Only relevant when using the HEOM solver. """ @@ -1228,7 +1236,7 @@ class HSolverDL(HEOMSolver): If set to None the default options will be used. Keyword only. Default: None. - combine : bool, default True + combine : bool, default: True Whether to combine exponents with the same frequency (and coupling operator). See :meth:`BosonicBath.combine` for details. Keyword only. Default: True. diff --git a/qutip/solver/integrator/explicit_rk.pyx b/qutip/solver/integrator/explicit_rk.pyx index 33749a90b7..a643de4a44 100644 --- a/qutip/solver/integrator/explicit_rk.pyx +++ b/qutip/solver/integrator/explicit_rk.pyx @@ -65,7 +65,7 @@ cdef Data iadd_data(Data left, Data right, double complex factor): cdef class Explicit_RungeKutta: """ Qutip implementation of Runge Kutta ODE. - Works in :class:`Data` allowing solving using sparse and gpu data. + Works in :class:`.Data` allowing solving using sparse and gpu data. Parameters ---------- diff --git a/qutip/solver/integrator/integrator.py b/qutip/solver/integrator/integrator.py index 44df5546f1..f9c2598e17 100644 --- a/qutip/solver/integrator/integrator.py +++ b/qutip/solver/integrator/integrator.py @@ -20,7 +20,7 @@ class Integrator: """ A wrapper around ODE solvers. It ensures a common interface for Solver usage. - It takes and return states as :class:`qutip.core.data.Data`, it may return + It takes and return states as :class:`.Data`, it may return a different data-type than the input type. Parameters @@ -111,7 +111,7 @@ def integrate(self, t, copy=True): t : float Time to integrate to, should be larger than the previous time. - copy : bool [True] + copy : bool, default: True Whether to return a copy of the state or the state itself. Returns @@ -142,7 +142,7 @@ def mcstep(self, t, copy=True): the last integrate call was use with ``step=True``, the time can be between the time at the start of the last call and now. - copy : bool [True] + copy : bool, default: True Whether to return a copy of the state or the state itself. Returns @@ -173,7 +173,7 @@ def get_state(self, copy=True): Parameters ---------- - copy : bool (True) + copy : bool, default: True Whether to return the data stored in the Integrator or a copy. Returns diff --git a/qutip/solver/integrator/krylov.py b/qutip/solver/integrator/krylov.py index d9cf5757ed..5559020982 100644 --- a/qutip/solver/integrator/krylov.py +++ b/qutip/solver/integrator/krylov.py @@ -204,26 +204,26 @@ def options(self): """ Supported options by krylov method: - atol : float, default=1e-7 + atol : float, default: 1e-7 Absolute tolerance. - nsteps : int, default=100 + nsteps : int, default: 100 Max. number of internal steps/call. - min_step, max_step : float, default=(1e-5, 1e5) + min_step, max_step : float, default: (1e-5, 1e5) Minimum and maximum step size. - krylov_dim: int, default=0 + krylov_dim: int, default: 0 Dimension of Krylov approximation subspaces used for the time evolution approximation. If the defaut 0 is given, the dimension is calculated from the system size N, using `min(int((N + 100)**0.5), N-1)`. - sub_system_tol: float, default=1e-7 + sub_system_tol: float, default: 1e-7 Tolerance to detect a happy breakdown. A happy breakdown occurs when the initial ket is in a subspace of the Hamiltonian smaller than ``krylov_dim``. - always_compute_step: bool, default=False + always_compute_step: bool, default: False If True, the step length is computed each time a new Krylov subspace is computed. Otherwise it is computed only once when creating the integrator. diff --git a/qutip/solver/integrator/qutip_integrator.py b/qutip/solver/integrator/qutip_integrator.py index 03749a57bd..943c6740ea 100644 --- a/qutip/solver/integrator/qutip_integrator.py +++ b/qutip/solver/integrator/qutip_integrator.py @@ -18,7 +18,7 @@ class IntegratorVern7(Integrator): sparse, GPU or other data layer objects to be used efficiently by the solver in their native formats. - See http://people.math.sfu.ca/~jverner/ for a detailed description of the + See https://www.sfu.ca/~jverner/ for a detailed description of the methods. Usable with ``method="vern7"`` @@ -71,27 +71,27 @@ def options(self): """ Supported options by verner method: - atol : float, default=1e-8 + atol : float, default: 1e-8 Absolute tolerance. - rtol : float, default=1e-6 + rtol : float, default: 1e-6 Relative tolerance. - nsteps : int, default=1000 + nsteps : int, default: 1000 Max. number of internal steps/call. - first_step : float, default=0 + first_step : float, default: 0 Size of initial step (0 = automatic). - min_step : float, default=0 + min_step : float, default: 0 Minimum step size (0 = automatic). - max_step : float, default=0 + max_step : float, default: 0 Maximum step size (0 = automatic) When using pulses, change to half the thinest pulse otherwise it may be skipped. - interpolate : bool, default=True + interpolate : bool, default: True Whether to use interpolation step, faster most of the time. """ return self._options @@ -111,7 +111,7 @@ class IntegratorVern9(IntegratorVern7): sparse, GPU or other data layer objects to be used efficiently by the solver in their native formats. - See http://people.math.sfu.ca/~jverner/ for a detailed description of the + See https://www.sfu.ca/~jverner/ for a detailed description of the methods. Usable with ``method="vern9"`` @@ -183,7 +183,7 @@ def options(self): """ Supported options by "diag" method: - eigensolver_dtype : str, default="dense" + eigensolver_dtype : str, default: "dense" Qutip data type {"dense", "csr", etc.} to use when computing the eigenstates. The dense eigen solver is usually faster and more stable. diff --git a/qutip/solver/integrator/scipy_integrator.py b/qutip/solver/integrator/scipy_integrator.py index dac1028a61..36b8f78e2d 100644 --- a/qutip/solver/integrator/scipy_integrator.py +++ b/qutip/solver/integrator/scipy_integrator.py @@ -21,7 +21,7 @@ class IntegratorScipyAdams(Integrator): """ Integrator using Scipy `ode` with zvode integrator using adams method. Ordinary Differential Equation solver by netlib - (http://www.netlib.org/odepack). + (https://www.netlib.org/odepack). Usable with ``method="adams"`` """ @@ -163,25 +163,25 @@ def options(self): """ Supported options by zvode integrator: - atol : float, default=1e-8 + atol : float, default: 1e-8 Absolute tolerance. - rtol : float, default=1e-6 + rtol : float, default: 1e-6 Relative tolerance. - order : int, default=12, 'adams' or 5, 'bdf' + order : int, default: 12, 'adams' or 5, 'bdf' Order of integrator <=12 'adams', <=5 'bdf' - nsteps : int, default=2500 + nsteps : int, default: 2500 Max. number of internal steps/call. - first_step : float, default=0 + first_step : float, default: 0 Size of initial step (0 = automatic). - min_step : float, default=0 + min_step : float, default: 0 Minimum step size (0 = automatic). - max_step : float, default=0 + max_step : float, default: 0 Maximum step size (0 = automatic) When using pulses, change to half the thinest pulse otherwise it may be skipped. @@ -197,7 +197,7 @@ class IntegratorScipyBDF(IntegratorScipyAdams): """ Integrator using Scipy `ode` with zvode integrator using bdf method. Ordinary Differential Equation solver by netlib - (http://www.netlib.org/odepack). + (https://www.netlib.org/odepack). Usable with ``method="bdf"`` """ @@ -323,25 +323,25 @@ def options(self): """ Supported options by dop853 integrator: - atol : float, default=1e-8 + atol : float, default: 1e-8 Absolute tolerance. - rtol : float, default=1e-6 + rtol : float, default: 1e-6 Relative tolerance. - nsteps : int, default=2500 + nsteps : int, default: 2500 Max. number of internal steps/call. - first_step : float, default=0 + first_step : float, default: 0 Size of initial step (0 = automatic). - max_step : float, default=0 + max_step : float, default: 0 Maximum step size (0 = automatic) - ifactor, dfactor : float, default=6., 0.3 + ifactor, dfactor : float, default: 6., 0.3 Maximum factor to increase/decrease step size by in one step - beta : float, default=0 + beta : float, default: 0 Beta parameter for stabilised step size control. See scipy.integrate.ode ode for more detail @@ -356,7 +356,7 @@ def options(self, new_options): class IntegratorScipylsoda(IntegratorScipyDop853): """ Integrator using Scipy `ode` with lsoda integrator. ODE solver by netlib - (http://www.netlib.org/odepack) Automatically choose between 'Adams' and + (https://www.netlib.org/odepack) Automatically choose between 'Adams' and 'BDF' methods to solve both stiff and non-stiff systems. Usable with ``method="lsoda"`` @@ -488,30 +488,30 @@ def options(self): """ Supported options by lsoda integrator: - atol : float, default=1e-8 + atol : float, default: 1e-8 Absolute tolerance. - rtol : float, default=1e-6 + rtol : float, default: 1e-6 Relative tolerance. - nsteps : int, default=2500 + nsteps : int, default: 2500 Max. number of internal steps/call. - max_order_ns : int, default=12 + max_order_ns : int, default: 12 Maximum order used in the nonstiff case (<= 12). - max_order_s : int, default=5 + max_order_s : int, default: 5 Maximum order used in the stiff case (<= 5). - first_step : float, default=0 + first_step : float, default: 0 Size of initial step (0 = automatic). - max_step : float, default=0 + max_step : float, default: 0 Maximum step size (0 = automatic) When using pulses, change to half the thinest pulse otherwise it may be skipped. - min_step : float, default=0 + min_step : float, default: 0 Minimum step size (0 = automatic) """ return self._options diff --git a/qutip/solver/integrator/verner7efficient.py b/qutip/solver/integrator/verner7efficient.py index c49882a221..071fd99237 100644 --- a/qutip/solver/integrator/verner7efficient.py +++ b/qutip/solver/integrator/verner7efficient.py @@ -1,10 +1,10 @@ """ Provide a cython implimentation verner 'most-efficient' order 7 runge-Kutta method. -See http://people.math.sfu.ca/~jverner/ +See https://www.sfu.ca/~jverner/ """ # Verner 7 Efficient -# http://people.math.sfu.ca/~jverner/RKV76.IIa.Efficient.00001675585.081206.CoeffsOnlyFLOAT +# https://www.sfu.ca/~jverner/RKV76.IIa.Efficient.00001675585.081206.CoeffsOnlyFLOAT __all__ = ["vern7_coeff"] import numpy as np order = 7 diff --git a/qutip/solver/integrator/verner9efficient.py b/qutip/solver/integrator/verner9efficient.py index 02b1d92681..0112498cf3 100644 --- a/qutip/solver/integrator/verner9efficient.py +++ b/qutip/solver/integrator/verner9efficient.py @@ -1,10 +1,10 @@ """ Provide a cython implimentation verner 'most-efficient' order 9 runge-Kutta method. -See http://people.math.sfu.ca/~jverner/ +See https://www.sfu.ca/~jverner/ """ # Verner 9 Efficient -# http://people.math.sfu.ca/~jverner/ +# https://www.sfu.ca/~jverner/ __all__ = ["vern9_coeff"] import numpy as np diff --git a/qutip/solver/krylovsolve.py b/qutip/solver/krylovsolve.py index 99fa78b229..6143aae06c 100644 --- a/qutip/solver/krylovsolve.py +++ b/qutip/solver/krylovsolve.py @@ -25,14 +25,13 @@ def krylovsolve( Parameters ---------- - H : :class:`Qobj`, :class:`QobjEvo`, :class:`QobjEvo` compatible format. + H : :obj:`.Qobj`, :obj:`.QobjEvo`, :obj:`.QobjEvo` compatible format. System Hamiltonian as a Qobj or QobjEvo for time-dependent - Hamiltonians. List of [:class:`Qobj`, :class:`Coefficient`] or callable - that can be made into :class:`QobjEvo` are also accepted. + Hamiltonians. List of [:obj:`.Qobj`, :obj:`.Coefficient`] or callable + that can be made into :obj:`.QobjEvo` are also accepted. - psi0 : :class:`qutip.qobj` - initial state vector (ket) - or initial unitary operator `psi0 = U` + psi0 : :class:`.Qobj` + Initial state vector (ket) tlist : *list* / *array* list of times for :math:`t`. @@ -41,58 +40,59 @@ def krylovsolve( Dimension of Krylov approximation subspaces used for the time evolution approximation. - e_ops : :class:`qutip.qobj`, callable, or list. + e_ops : :class:`.Qobj`, callable, or list, optional Single operator or list of operators for which to evaluate expectation values or callable or list of callable. Callable signature must be, `f(t: float, state: Qobj)`. - See :func:`expect` for more detail of operator expectation. + See :func:`~qutip.core.expect.expect` for more detail of operator + expectation. - args : None / *dictionary* + args : dict, optional dictionary of parameters for time-dependent Hamiltonians - options : None / dict + options : dict, optional Dictionary of options for the solver. - - store_final_state : bool, [False] - Whether or not to store the final state of the evolution in the - result class. - - store_states : bool, [None] - 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, [True] - Normalize output state to hide ODE numerical errors. - - progress_bar : str {'text', 'enhanced', 'tqdm', ''}, ["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. - - progress_kwargs : dict, [{"chunk_size": 10}] - kwargs to pass to the progress_bar. Qutip's bars use `chunk_size`. - - atol: float [1e-7] - Absolute and relative tolerance of the ODE integrator. - - nsteps : int [100] - Maximum number of (internally defined) steps allowed in one ``tlist`` - step. - - min_step, max_step : float, [1e-5, 1e5] - Miniumum and maximum lenght of one internal step. - - always_compute_step: bool [False] - If True, the step lenght is computed each time a new Krylov - subspace is computed. Otherwise it is computed only once when - creating the integrator. - - sub_system_tol: float, [1e-7] - Tolerance to detect an happy breakdown. An happy breakdown happens - when the initial ket is in a subspace of the Hamiltonian smaller - than ``krylov_dim``. + - | store_final_state : bool + | Whether or not to store the final state of the evolution in the + result class. + - | store_states : bool, None + | 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 + if not installed. Empty string or False will disable the bar. + - | progress_kwargs : dict + | kwargs to pass to the progress_bar. Qutip's bars use `chunk_size`. + - | atol: float + | Absolute tolerance of the ODE integrator. + - | nsteps : int + | Maximum number of (internally defined) steps allowed in one + ``tlist`` step. + - | min_step, max_step : float + | Miniumum and maximum lenght of one internal step. + - | always_compute_step: bool + | If True, the step lenght is computed each time a new Krylov + subspace is computed. Otherwise it is computed only once when + creating the integrator. + - | sub_system_tol: float + | Tolerance to detect an happy breakdown. An happy breakdown happens + when the initial ket is in a subspace of the Hamiltonian smaller + than ``krylov_dim``. Returns ------- - result: :class:`qutip.Result` + result: :class:`.Result` - An instance of the class :class:`qutip.Result`, which contains - a *list of array* `result.expect` of expectation values for the times - specified by `tlist`, and/or a *list* `result.states` of state vectors - or density matrices corresponding to the times in `tlist` [if `e_ops` - is an empty list of `store_states=True` in options]. + An instance of the class :class:`.Result`, which contains + a *list of array* ``result.expect`` of expectation values for the times + specified by ``tlist``, and/or a *list* ``result.states`` of state + vectors or density matrices corresponding to the times in ``tlist`` [if + ``e_ops`` is an empty list of ``store_states=True`` in options]. """ H = QobjEvo(H, args=args, tlist=tlist) options = options or {} diff --git a/qutip/solver/mcsolve.py b/qutip/solver/mcsolve.py index 9682d73437..66005e1080 100644 --- a/qutip/solver/mcsolve.py +++ b/qutip/solver/mcsolve.py @@ -2,16 +2,18 @@ import numpy as np from ..core import QobjEvo, spre, spost, Qobj, unstack_columns -from .multitraj import MultiTrajSolver -from .solver_base import Solver, Integrator +from .multitraj import MultiTrajSolver, _MTSystem +from .solver_base import Solver, Integrator, _solver_deprecation from .result import McResult, McTrajectoryResult, McResultImprovedSampling from .mesolve import mesolve, MESolver +from ._feedback import _QobjFeedback, _DataFeedback, _CollapseFeedback import qutip.core.data as _data from time import time def mcsolve(H, state, tlist, c_ops=(), e_ops=None, ntraj=500, *, - args=None, options=None, seeds=None, target_tol=None, timeout=None): + args=None, options=None, seeds=None, target_tol=None, timeout=None, + **kwargs): r""" Monte Carlo evolution of a state vector :math:`|\psi \rangle` for a given Hamiltonian and sets of collapse operators. Options for the @@ -19,13 +21,13 @@ def mcsolve(H, state, tlist, c_ops=(), e_ops=None, ntraj=500, *, Parameters ---------- - H : :class:`qutip.Qobj`, :class:`qutip.QobjEvo`, ``list``, callable. + H : :class:`.Qobj`, :class:`.QobjEvo`, ``list``, callable. System Hamiltonian as a Qobj, QobjEvo. It can also be any input type - that QobjEvo accepts (see :class:`qutip.QobjEvo`'s documentation). + that QobjEvo accepts (see :class:`.QobjEvo`'s documentation). ``H`` can also be a superoperator (liouvillian) if some collapse operators are to be treated deterministically. - state : :class:`qutip.Qobj` + state : :class:`.Qobj` Initial state vector. tlist : array_like @@ -33,80 +35,85 @@ def mcsolve(H, state, tlist, c_ops=(), e_ops=None, ntraj=500, *, c_ops : list A ``list`` of collapse operators in any input type that QobjEvo accepts - (see :class:`qutip.QobjEvo`'s documentation). They must be operators + (see :class:`.QobjEvo`'s documentation). They must be operators even if ``H`` is a superoperator. If none are given, the solver will defer to ``sesolve`` or ``mesolve``. - e_ops : list, [optional] + e_ops : list, optional A ``list`` of operator as Qobj, QobjEvo or callable with signature of (t, state: Qobj) for calculating expectation values. When no ``e_ops`` are given, the solver will default to save the states. - ntraj : int + ntraj : int, default: 500 Maximum number of trajectories to run. Can be cut short if a time limit is passed with the ``timeout`` keyword or if the target tolerance is reached, see ``target_tol``. - args : None / dict + args : dict, optional Arguments for time-dependent Hamiltonian and collapse operator terms. - options : None / dict + options : dict, optional Dictionary of options for the solver. - - store_final_state : bool, [False] - Whether or not to store the final state of the evolution in the - result class. - - store_states : bool, NoneType, [None] - Whether or not to store the state vectors or density matrices. - On `None` the states will be saved if no expectation operators are - given. - - progress_bar : str {'text', 'enhanced', 'tqdm', ''}, ['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. - - progress_kwargs : dict, [{"chunk_size": 10}] - kwargs to pass to the progress_bar. Qutip's bars use `chunk_size`. - - method : str {"adams", "bdf", "dop853", "vern9", etc.}, ["adams"] - Which differential equation integration method to use. - - keep_runs_results : bool, [False] - Whether to store results from all trajectories or just store the - averages. - - map : str {"serial", "parallel", "loky"}, ["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. - - job_timeout : NoneType, int, [None] - Maximum time to compute one trajectory. - - num_cpus : NoneType, int, [None] - Number of cpus to use when running in parallel. ``None`` detect the - number of available cpus. - - norm_t_tol, norm_tol, norm_steps : float, float, int, [1e-6, 1e-4, 5] - Parameters used to find the collapse location. ``norm_t_tol`` and - ``norm_tol`` are the tolerance in time and norm respectively. - An error will be raised if the collapse could not be found within - ``norm_steps`` tries. - - mc_corr_eps : float, [1e-10] - Small number used to detect non-physical collapse caused by numerical - imprecision. - - atol, rtol : float, [1e-8, 1e-6] - Absolute and relative tolerance of the ODE integrator. - - nsteps : int [2500] - Maximum number of (internally defined) steps allowed in one ``tlist`` - step. - - max_step : float, [0] - Maximum lenght of one internal step. When using pulses, it should be - less than half the width of the thinnest pulse. - - improved_sampling : Bool - Whether to use the improved sampling algorithm from Abdelhafez et al. - PRA (2019) - - seeds : int, SeedSequence, list, [optional] + - | store_final_state : bool + | Whether or not to store the final state of the evolution in the + result class. + - | store_states : bool, None + | Whether or not to store the state vectors or density matrices. + On `None` the states will be saved if no expectation operators are + given. + - | 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 + if not installed. Empty string or False will disable the bar. + - | progress_kwargs : dict + | kwargs to pass to the progress_bar. Qutip's bars use `chunk_size`. + - | method : str ["adams", "bdf", "lsoda", "dop853", "vern9", etc.] + | Which differential equation integration method to use. + - | atol, rtol : float + | Absolute and relative tolerance of the ODE integrator. + - | nsteps : int + | 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. + - | keep_runs_results : bool, [False] + | Whether to store results from all trajectories or just store the + averages. + - | 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. + - | num_cpus : int + | Number of cpus to use when running in parallel. ``None`` detect the + number of available cpus. + - | 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. + An error will be raised if the collapse could not be found within + ``norm_steps`` tries. + - | 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) + + 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>`_. + + 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 trajectory. Seeds are saved in the result and they can be reused with:: seeds=prev_result.seeds - target_tol : float, tuple, list, [optional] + target_tol : float, tuple, list, optional Target tolerance of the evolution. The evolution will compute trajectories until the error on the expectation values is lower than this tolerance. The maximum number of trajectories employed is @@ -115,21 +122,23 @@ def mcsolve(H, state, tlist, c_ops=(), e_ops=None, ntraj=500, *, relative tolerance, in that order. Lastly, it can be a list of pairs of (atol, rtol) for each e_ops. - timeout : float, [optional] + timeout : float, optional Maximum time for the evolution in second. When reached, no more trajectories will be computed. Returns ------- - results : :class:`qutip.solver.McResult` + results : :class:`.McResult` Object storing all results from the simulation. Which results is saved depends on the presence of ``e_ops`` and the options used. ``collapse`` and ``photocurrent`` is available to Monte Carlo simulation results. - .. note: - The simulation will end when the first end condition is reached between - ``ntraj``, ``timeout`` and ``target_tol``. + Notes + ----- + The simulation will end when the first end condition is reached between + ``ntraj``, ``timeout`` and ``target_tol``. """ + options = _solver_deprecation(kwargs, options, "mc") H = QobjEvo(H, args=args, tlist=tlist) if not isinstance(c_ops, (list, tuple)): c_ops = [c_ops] @@ -154,25 +163,62 @@ def mcsolve(H, state, tlist, c_ops=(), e_ops=None, ntraj=500, *, mc = MCSolver(H, c_ops, options=options) result = mc.run(state, tlist=tlist, ntraj=ntraj, e_ops=e_ops, - seed=seeds, target_tol=target_tol, timeout=timeout) + seeds=seeds, target_tol=target_tol, timeout=timeout) return result +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 + self.n_ops = n_ops + self._collapse_key = "" + + def __call__(self): + return self.rhs + + def __getattr__(self, attr): + if attr == "rhs": + raise AttributeError + if hasattr(self.rhs, attr): + return getattr(self.rhs, attr) + raise AttributeError + + def arguments(self, args): + self.rhs.arguments(args) + for c_op in self.c_ops: + c_op.arguments(args) + for n_op in self.n_ops: + n_op.arguments(args) + + def _register_feedback(self, key, val): + self.rhs._register_feedback({key: val}, solver="McSolver") + for c_op in self.c_ops: + c_op._register_feedback({key: val}, solver="McSolver") + for n_op in self.n_ops: + n_op._register_feedback({key: val}, solver="McSolver") + + class MCIntegrator: """ Integrator like object for mcsolve trajectory. """ name = "mcsolve" - def __init__(self, integrator, c_ops, n_ops, options=None): + def __init__(self, integrator, system, options=None): self._integrator = integrator - self._c_ops = c_ops - self._n_ops = n_ops + self.system = system + self._c_ops = system.c_ops + self._n_ops = system.n_ops self.options = options self._generator = None self.method = f"{self.name} {self._integrator.method}" self._is_set = False - self.issuper = c_ops[0].issuper + self.issuper = self._c_ops[0].issuper def set_state(self, t, state0, generator, no_jump=False, jump_prob_floor=0.0): @@ -200,13 +246,14 @@ def set_state(self, t, state0, generator, a trajectory with jumps """ self.collapses = [] + self.system._register_feedback("CollapseFeedback", self.collapses) self._generator = generator if no_jump: 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 @@ -347,34 +394,34 @@ class MCSolver(MultiTrajSolver): Parameters ---------- - H : :class:`qutip.Qobj`, :class:`qutip.QobjEvo`, ``list``, callable. + H : :class:`.Qobj`, :class:`.QobjEvo`, list, callable. System Hamiltonian as a Qobj, QobjEvo. It can also be any input type - that QobjEvo accepts (see :class:`qutip.QobjEvo`'s documentation). + that QobjEvo accepts (see :class:`.QobjEvo`'s documentation). ``H`` can also be a superoperator (liouvillian) if some collapse operators are to be treated deterministically. c_ops : list A ``list`` of collapse operators in any input type that QobjEvo accepts - (see :class:`qutip.QobjEvo`'s documentation). They must be operators + (see :class:`.QobjEvo`'s documentation). They must be operators even if ``H`` is a superoperator. options : dict, [optional] Options for the evolution. """ name = "mcsolve" - trajectory_resultclass = McTrajectoryResult - mc_integrator_class = MCIntegrator + _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, - "method": "adams", "map": "serial", - "job_timeout": None, + "mpi_options": {}, "num_cpus": None, "bitgenerator": None, + "method": "adams", "mc_corr_eps": 1e-10, "norm_steps": 5, "norm_t_tol": 1e-6, @@ -409,7 +456,8 @@ def __init__(self, H, c_ops, *, options=None): self._num_collapse = len(self._c_ops) self.options = options - super().__init__(rhs, options=options) + system = _MCSystem(rhs, self._c_ops, self._n_ops) + super().__init__(system, options=options) def _restore_state(self, data, *, copy=True): """ @@ -432,17 +480,9 @@ def _initialize_stats(self): }) return stats - def _argument(self, args): - self._integrator.arguments(args) - self.rhs.arguments(args) - for c_op in self._c_ops: - c_op.arguments(args) - for n_op in self._n_ops: - n_op.arguments(args) - def _initialize_run_one_traj(self, seed, state, tlist, e_ops, no_jump=False, jump_prob_floor=0.0): - result = self.trajectory_resultclass(e_ops, self.options) + result = self._trajectory_resultclass(e_ops, self.options) generator = self._get_generator(seed) self._integrator.set_state(tlist[0], state, generator, no_jump=no_jump, @@ -463,7 +503,7 @@ def _run_one_traj(self, seed, state, tlist, e_ops, no_jump=False, return seed, result def run(self, state, tlist, ntraj=1, *, - args=None, e_ops=(), timeout=None, target_tol=None, seed=None): + args=None, e_ops=(), timeout=None, target_tol=None, seeds=None): """ Do the evolution of the Quantum system. See the overridden method for further details. The modification @@ -471,10 +511,10 @@ def run(self, state, tlist, ntraj=1, *, probability is used as a lower-bound for random numbers in future monte carlo runs """ - if not self.options["improved_sampling"]: + if not self.options.get("improved_sampling", False): return super().run(state, tlist, ntraj=ntraj, args=args, e_ops=e_ops, timeout=timeout, - target_tol=target_tol, seed=seed) + target_tol=target_tol, seeds=seeds) stats, seeds, result, map_func, map_kw, state0 = self._initialize_run( state, ntraj, @@ -482,7 +522,7 @@ def run(self, state, tlist, ntraj=1, *, e_ops=e_ops, timeout=timeout, target_tol=target_tol, - seed=seed, + seeds=seeds, ) # first run the no-jump trajectory start_time = time() @@ -517,16 +557,16 @@ def _get_integrator(self): integrator = method else: raise ValueError("Integrator method not supported.") - integrator_instance = integrator(self.rhs, self.options) - mc_integrator = self.mc_integrator_class( - integrator_instance, self._c_ops, self._n_ops, self.options + integrator_instance = integrator(self.system(), self.options) + mc_integrator = self._mc_integrator_class( + integrator_instance, self.system, self.options ) self._init_integrator_time = time() - _time_start return mc_integrator @property - def resultclass(self): - if self.options["improved_sampling"]: + def _resultclass(self): + if self.options.get("improved_sampling", False): return McResultImprovedSampling else: return McResult @@ -536,38 +576,41 @@ def options(self): """ Options for monte carlo solver: - store_final_state: bool, default=False + store_final_state: bool, default: False Whether or not to store the final state of the evolution in the result class. - store_states: bool, default=None + store_states: bool, default: None Whether or not to store the state vectors or density matrices. On `None` the states will be saved if no expectation operators are given. - progress_bar: str {'text', 'enhanced', 'tqdm', ''}, default="text" + 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} + progress_kwargs: dict, default: {"chunk_size":10} Arguments to pass to the progress_bar. Qutip's bars use ``chunk_size``. - keep_runs_results: bool - Whether to store results from all trajectories or just store the - averages. + keep_runs_results: bool, default: False + Whether to store results from all trajectories or just store the + averages. - method: str, default="adams" - Which ODE integrator methods are supported. + method: str, default: "adams" + Which differential equation integration method to use. - 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"}, 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. - job_timeout: None, int - Maximum time to compute one trajectory. + 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 @@ -577,20 +620,20 @@ def options(self): Which of numpy.random's bitgenerator to use. With ``None``, your numpy version's default is used. - mc_corr_eps: float + mc_corr_eps: float, default: 1e-10 Small number used to detect non-physical collapse caused by numerical imprecision. - norm_t_tol: float + norm_t_tol: float, default: 1e-6 Tolerance in time used when finding the collapse. - norm_tol: float + norm_tol: float, default: 1e-4 Tolerance in norm used when finding the collapse. - norm_steps: int + norm_steps: int, default: 5 Maximum number of tries to find the collapse. - improved_sampling: Bool + improved_sampling: Bool, default: False Whether to use the improved sampling algorithm of Abdelhafez et al. PRA (2019) """ @@ -608,3 +651,57 @@ def avail_integrators(cls): **Solver.avail_integrators(), **cls._avail_integrators, } + + @classmethod + def CollapseFeedback(cls, default=None): + """ + Collapse of the trajectory argument for time dependent systems. + + When used as an args: + + ``QobjEvo([op, func], args={"cols": MCSolver.CollapseFeedback()})`` + + The ``func`` will receive a list of ``(time, operator number)`` for + each collapses of the trajectory as ``cols``. + + .. note:: + + CollapseFeedback can't be added to a running solver when updating + arguments between steps: ``solver.step(..., args={})``. + + Parameters + ---------- + default : callable, default : [] + Default function used outside the solver. + + """ + return _CollapseFeedback(default) + + @classmethod + def StateFeedback(cls, default=None, raw_data=False, open=False): + """ + State of the evolution to be used in a time-dependent operator. + + When used as an args: + + ``QobjEvo([op, func], args={"state": MCSolver.StateFeedback()})`` + + The ``func`` will receive the density matrix as ``state`` during the + evolution. + + Parameters + ---------- + default : Qobj or qutip.core.data.Data, default : None + Initial value to be used at setup of the system. + + open : bool, default False + Set to ``True`` when using the monte carlo solver for open systems. + + raw_data : bool, default : False + If True, the raw matrix will be passed instead of a Qobj. + For density matrices, the matrices can be column stacked or square + depending on the integration method. + """ + if raw_data: + return _DataFeedback(default, open=open) + return _QobjFeedback(default, open=open) diff --git a/qutip/solver/mesolve.py b/qutip/solver/mesolve.py index 5792038af1..caafc5ef1c 100644 --- a/qutip/solver/mesolve.py +++ b/qutip/solver/mesolve.py @@ -10,121 +10,124 @@ from .. import (Qobj, QobjEvo, isket, liouvillian, ket2dm, lindblad_dissipator) from ..core import stack_columns, unstack_columns from ..core.data import to -from .solver_base import Solver +from .solver_base import Solver, _solver_deprecation from .sesolve import sesolve, SESolver +from ._feedback import _QobjFeedback, _DataFeedback -def mesolve(H, rho0, tlist, c_ops=None, e_ops=None, args=None, options=None): +def mesolve(H, rho0, tlist, c_ops=None, e_ops=None, args=None, options=None, + **kwargs): """ Master equation evolution of a density matrix for a given Hamiltonian and set of collapse operators, or a Liouvillian. - Evolve the state vector or density matrix (`rho0`) using a given - Hamiltonian or Liouvillian (`H`) and an optional set of collapse operators - (`c_ops`), by integrating the set of ordinary differential equations + Evolve the state vector or density matrix (``rho0``) using a given + Hamiltonian or Liouvillian (``H``) and an optional set of collapse operators + (``c_ops``), by integrating the set of ordinary differential equations that define the system. In the absence of collapse operators the system is evolved according to the unitary evolution of the Hamiltonian. The output is either the state vector at arbitrary points in time - (`tlist`), or the expectation values of the supplied operators - (`e_ops`). If e_ops is a callback function, it is invoked for each - time in `tlist` with time and the state as arguments, and the function + (``tlist``), or the expectation values of the supplied operators + (``e_ops``). If e_ops is a callback function, it is invoked for each + time in ``tlist`` with time and the state as arguments, and the function does not use any return values. - If either `H` or the Qobj elements in `c_ops` are superoperators, they + If either ``H`` or the Qobj elements in ``c_ops`` are superoperators, they will be treated as direct contributions to the total system Liouvillian. This allows the solution of master equations that are not in standard Lindblad form. **Time-dependent operators** - For time-dependent problems, `H` and `c_ops` can be a :class:`QobjEvo` or - object that can be interpreted as :class:`QobjEvo` such as a list of + For time-dependent problems, ``H`` and ``c_ops`` can be a :obj:`.QobjEvo` + or object that can be interpreted as :obj:`.QobjEvo` such as a list of (Qobj, Coefficient) pairs or a function. **Additional options** - Additional options to mesolve can be set via the `options` argument. Many - ODE integration options can be set this way, and the `store_states` and - `store_final_state` options can be used to store states even though - expectation values are requested via the `e_ops` argument. + Additional options to mesolve can be set via the ``options`` argument. Many + ODE integration options can be set this way, and the ``store_states`` and + ``store_final_state`` options can be used to store states even though + expectation values are requested via the ``e_ops`` argument. - .. note:: - - When no collapse operator are given and the `H` is not a superoperator, - it will defer to :func:`sesolve`. + Notes + ----- + When no collapse operator are given and the `H` is not a superoperator, + it will defer to :func:`sesolve`. Parameters ---------- - H : :class:`Qobj`, :class:`QobjEvo`, :class:`QobjEvo` compatible format. + H : :obj:`.Qobj`, :obj:`.QobjEvo`, :obj:`.QobjEvo` compatible format. Possibly time-dependent system Liouvillian or Hamiltonian as a Qobj or - QobjEvo. List of [:class:`Qobj`, :class:`Coefficient`] or callable that - can be made into :class:`QobjEvo` are also accepted. + QobjEvo. List of [:obj:`.Qobj`, :obj:`.Coefficient`] or callable that + can be made into :obj:`.QobjEvo` are also accepted. - rho0 : :class:`Qobj` + rho0 : :obj:`.Qobj` initial density matrix or state vector (ket). tlist : *list* / *array* list of times for :math:`t`. - c_ops : list of (:class:`QobjEvo`, :class:`QobjEvo` compatible format) + c_ops : list of (:obj:`.QobjEvo`, :obj:`.QobjEvo` compatible format) Single collapse operator, or list of collapse operators, or a list of Liouvillian superoperators. None is equivalent to an empty list. - e_ops : list of :class:`Qobj` / callback function + e_ops : list of :obj:`.Qobj` / callback function, optional Single operator or list of operators for which to evaluate expectation values or callable or list of callable. Callable signature must be, `f(t: float, state: Qobj)`. See :func:`expect` for more detail of operator expectation. - args : None / *dictionary* + args : dict, optional dictionary of parameters for time-dependent Hamiltonians and collapse operators. - options : None / dict + options : dict, optional Dictionary of options for the solver. - - store_final_state : bool - Whether or not to store the final state of the evolution in the - result class. - - store_states : bool, None - 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 - if not installed. Empty string or False will disable the bar. - - progress_kwargs : dict - kwargs to pass to the progress_bar. Qutip's bars use `chunk_size`. - - method : str ["adams", "bdf", "lsoda", "dop853", "vern9", etc.] - Which differential equation integration method to use. - - atol, rtol : float - Absolute and relative tolerance of the ODE integrator. - - nsteps : - Maximum number of (internally defined) steps allowed in one ``tlist`` - step. - - max_step : float, 0 - Maximum lenght of one internal step. When using pulses, it should be - less than half the width of the thinnest pulse. + - | store_final_state : bool + | Whether or not to store the final state of the evolution in the + result class. + - | store_states : bool, None + | 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 + if not installed. Empty string or False will disable the bar. + - | progress_kwargs : dict + | kwargs to pass to the progress_bar. Qutip's bars use `chunk_size`. + - | method : str ["adams", "bdf", "lsoda", "dop853", "vern9", etc.] + | Which differential equation integration method to use. + - | atol, rtol : float + | Absolute and relative tolerance of the ODE integrator. + - | nsteps : int + | 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 + less than half the width of the thinnest pulse. Other options could be supported depending on the integration method, see `Integrator <./classes.html#classes-ode>`_. Returns ------- - result: :class:`qutip.Result` + result: :obj:`.Result` - An instance of the class :class:`qutip.Result`, which contains - a *list of array* `result.expect` of expectation values for the times - specified by `tlist`, and/or a *list* `result.states` of state vectors - or density matrices corresponding to the times in `tlist` [if `e_ops` - is an empty list of `store_states=True` in options]. + An instance of the class :obj:`.Result`, which contains a *list of + array* ``result.expect`` of expectation values for the times specified + by ``tlist``, and/or a *list* ``result.states`` of state vectors or + density matrices corresponding to the times in ``tlist`` [if ``e_ops`` + is an empty list of ``store_states=True`` in options]. """ + options = _solver_deprecation(kwargs, options) H = QobjEvo(H, args=args, tlist=tlist) c_ops = c_ops if c_ops is not None else [] @@ -160,12 +163,12 @@ class MESolver(SESolver): Parameters ---------- - H : :class:`Qobj`, :class:`QobjEvo` + H : :obj:`.Qobj`, :obj:`.QobjEvo` Possibly time-dependent system Liouvillian or Hamiltonian as a Qobj or - QobjEvo. List of [:class:`Qobj`, :class:`Coefficient`] or callable that - can be made into :class:`QobjEvo` are also accepted. + QobjEvo. List of [:obj:`.Qobj`, :obj:`.Coefficient`] or callable that + can be made into :obj:`.QobjEvo` are also accepted. - c_ops : list of :class:`Qobj`, :class:`QobjEvo` + c_ops : list of :obj:`.Qobj`, :obj:`.QobjEvo` Single collapse operator, or list of collapse operators, or a list of Liouvillian superoperators. None is equivalent to an empty list. @@ -173,7 +176,7 @@ class MESolver(SESolver): Options for the solver, see :obj:`MESolver.options` and `Integrator <./classes.html#classes-ode>`_ for a list of all options. - attributes + Attributes ---------- stats: dict Diverse diagnostic statistics of the evolution. @@ -181,7 +184,7 @@ class MESolver(SESolver): name = "mesolve" _avail_integrators = {} solver_options = { - "progress_bar": "text", + "progress_bar": "", "progress_kwargs": {"chunk_size":10}, "store_final_state": False, "store_states": None, @@ -215,3 +218,34 @@ def _initialize_stats(self): "num_collapse": self._num_collapse, }) return stats + + @classmethod + def StateFeedback(cls, default=None, raw_data=False, prop=False): + """ + State of the evolution to be used in a time-dependent operator. + + When used as an args: + + ``QobjEvo([op, func], args={"state": MESolver.StateFeedback()})`` + + The ``func`` will receive the density matrix as ``state`` during the + evolution. + + Parameters + ---------- + default : Qobj or qutip.core.data.Data, default : None + Initial value to be used at setup of the system. + + prop : bool, default : False + Set to True when computing propagators. + The default with take the shape of the propagator instead of a + state. + + raw_data : bool, default : False + If True, the raw matrix will be passed instead of a Qobj. + For density matrices, the matrices can be column stacked or square + depending on the integration method. + """ + if raw_data: + return _DataFeedback(default, open=True, prop=prop) + return _QobjFeedback(default, open=True, prop=prop) diff --git a/qutip/solver/multitraj.py b/qutip/solver/multitraj.py index e422413afc..3ca7930a96 100644 --- a/qutip/solver/multitraj.py +++ b/qutip/solver/multitraj.py @@ -2,16 +2,39 @@ from .parallel import _get_map from time import time from .solver_base import Solver +from ..core import QobjEvo import numpy as np __all__ = ["MultiTrajSolver"] +class _MTSystem: + """ + Container for the operators of the solver. + """ + def __init__(self, rhs): + self.rhs = rhs + + def __call__(self): + return self.rhs + + def arguments(self, args): + self.rhs.arguments(args) + + def _register_feedback(self, type, val): + pass + + def __getattr__(self, attr): + if hasattr(self.rhs, attr): + return getattr(self.rhs, attr) + raise AttributeError + + class MultiTrajSolver(Solver): """ Basic class for multi-trajectory evolutions. - As :class:`Solver` it can ``run`` or ``step`` evolution. + As :class:`.Solver` it can ``run`` or ``step`` evolution. It manages the random seed for each trajectory. The actual evolution is done by a single trajectory solver:: @@ -27,8 +50,8 @@ class MultiTrajSolver(Solver): Options for the solver. """ name = "generic multi trajectory" - resultclass = MultiTrajResult - trajectory_resultclass = Result + _resultclass = MultiTrajResult + _trajectory_resultclass = Result _avail_integrators = {} # Class of option used by the solver @@ -41,13 +64,19 @@ class MultiTrajSolver(Solver): "normalize_output": False, "method": "", "map": "serial", - "job_timeout": None, + "mpi_options": {}, "num_cpus": None, "bitgenerator": None, } def __init__(self, rhs, *, options=None): - self.rhs = rhs + if isinstance(rhs, QobjEvo): + self.system = _MTSystem(rhs) + elif isinstance(rhs, _MTSystem): + self.system = rhs + else: + raise TypeError("The system should be a QobjEvo") + self.rhs = self.system() self.options = options self.seed_sequence = np.random.SeedSequence() self._integrator = self._get_integrator() @@ -60,20 +89,20 @@ def start(self, state, t0, seed=None): Parameters ---------- - state : :class:`Qobj` + state : :obj:`.Qobj` Initial state of the evolution. t0 : double Initial time of the evolution. - seed : int, SeedSequence, list, {None} + seed : 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 seed, one for each trajectory. - ..note :: - When using step evolution, only one trajectory can be computed at - once. + Notes + ----- + When using step evolution, only one trajectory can be computed at once. """ seeds = self._read_seed(seed, 1) generator = self._get_generator(seeds[0]) @@ -81,19 +110,19 @@ def start(self, state, t0, seed=None): def step(self, t, *, args=None, copy=True): """ - Evolve the state to ``t`` and return the state as a :class:`Qobj`. + Evolve the state to ``t`` and return the state as a :obj:`.Qobj`. Parameters ---------- t : double Time to evolve to, must be higher than the last call. - args : dict, optional {None} + args : dict, optional Update the ``args`` of the system. The change is effective from the beginning of the interval. Changing ``args`` can slow the evolution. - copy : bool, optional {True} + copy : bool, default: True Whether to return a copy of the data or the data in the ODE solver. """ if not self._integrator._is_set: @@ -103,40 +132,39 @@ def step(self, t, *, args=None, copy=True): return self._restore_state(state, copy=copy) def _initialize_run(self, state, ntraj=1, args=None, e_ops=(), - timeout=None, target_tol=None, seed=None): + timeout=None, target_tol=None, seeds=None): start_time = time() self._argument(args) stats = self._initialize_stats() - seeds = self._read_seed(seed, ntraj) + seeds = self._read_seed(seeds, ntraj) - result = self.resultclass( + result = self._resultclass( e_ops, self.options, solver=self.name, stats=stats ) result.add_end_condition(ntraj, target_tol) - map_func = _get_map[self.options['map']] - map_kw = { + map_func, map_kw = _get_map(self.options) + map_kw.update({ 'timeout': timeout, - 'job_timeout': self.options['job_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 def run(self, state, tlist, ntraj=1, *, - args=None, e_ops=(), timeout=None, target_tol=None, seed=None): + args=None, e_ops=(), timeout=None, target_tol=None, seeds=None): """ Do the evolution of the Quantum system. For a ``state`` at time ``tlist[0]`` do the evolution as directed by ``rhs`` and for each time in ``tlist`` store the state and/or - expectation values in a :class:`Result`. The evolution method and + expectation values in a :class:`.Result`. The evolution method and stored results are determined by ``options``. Parameters ---------- - state : :class:`Qobj` + state : :obj:`.Qobj` Initial state of the evolution. tlist : list of double @@ -148,7 +176,7 @@ def run(self, state, tlist, ntraj=1, *, ntraj : int Number of trajectories to add. - args : dict, optional {None} + args : dict, optional Change the ``args`` of the rhs for the evolution. e_ops : list @@ -156,14 +184,14 @@ def run(self, state, tlist, ntraj=1, *, Alternatively, function[s] with the signature f(t, state) -> expect can be used. - timeout : float, optional [1e8] + timeout : float, optional Maximum time in seconds for the trajectories to run. Once this time is reached, the simulation will end even if the number of trajectories is less than ``ntraj``. The map function, set in options, can interupt the running trajectory or wait for it to finish. Set to an arbitrary high number to disable. - target_tol : {float, tuple, list}, optional [None] + target_tol : {float, tuple, list}, optional Target tolerance of the evolution. The evolution will compute trajectories until the error on the expectation values is lower than this tolerance. The maximum number of trajectories employed is @@ -172,12 +200,12 @@ def run(self, state, tlist, ntraj=1, *, of absolute and relative tolerance, in that order. Lastly, it can be a list of pairs of (atol, rtol) for each e_ops. - seed : {int, SeedSequence, list} optional + seeds : {int, SeedSequence, list}, optional Seed or list of seeds for each trajectories. Returns ------- - results : :class:`qutip.solver.MultiTrajResult` + results : :class:`.MultiTrajResult` Results of the evolution. States and/or expect will be saved. You can control the saved data in the options. @@ -192,7 +220,7 @@ def run(self, state, tlist, ntraj=1, *, e_ops=e_ops, timeout=timeout, target_tol=target_tol, - seed=seed, + seeds=seeds, ) start_time = time() map_func( @@ -206,7 +234,7 @@ def run(self, state, tlist, ntraj=1, *, return result def _initialize_run_one_traj(self, seed, state, tlist, e_ops): - result = self.trajectory_resultclass(e_ops, self.options) + result = self._trajectory_resultclass(e_ops, self.options) generator = self._get_generator(seed) self._integrator.set_state(tlist[0], state, generator) result.add(tlist[0], self._restore_state(state, copy=False)) @@ -250,7 +278,7 @@ def _read_seed(self, seed, ntraj): def _argument(self, args): """Update the args, for the `rhs` and `c_ops` and other operators.""" if args: - self.rhs.arguments(args) + self.system.arguments(args) def _get_generator(self, seed): """ @@ -269,7 +297,3 @@ def _get_generator(self, seed): else: generator = np.random.default_rng(seed) return generator - - @classmethod - def avail_integrators(cls): - return cls._avail_integrators diff --git a/qutip/solver/nm_mcsolve.py b/qutip/solver/nm_mcsolve.py index f97298b390..6c064bb975 100644 --- a/qutip/solver/nm_mcsolve.py +++ b/qutip/solver/nm_mcsolve.py @@ -38,13 +38,13 @@ def nm_mcsolve(H, state, tlist, ops_and_rates=(), e_ops=None, ntraj=500, *, Parameters ---------- - H : :class:`qutip.Qobj`, :class:`qutip.QobjEvo`, ``list``, callable. + H : :class:`.Qobj`, :class:`.QobjEvo`, ``list``, callable. System Hamiltonian as a Qobj, QobjEvo. It can also be any input type - that QobjEvo accepts (see :class:`qutip.QobjEvo`'s documentation). + that QobjEvo accepts (see :class:`.QobjEvo`'s documentation). ``H`` can also be a superoperator (liouvillian) if some collapse operators are to be treated deterministically. - state : :class:`qutip.Qobj` + state : :class:`.Qobj` Initial state vector. tlist : array_like @@ -52,87 +52,95 @@ def nm_mcsolve(H, state, tlist, ops_and_rates=(), e_ops=None, ntraj=500, *, ops_and_rates : list A ``list`` of tuples ``(L, Gamma)``, where the Lindblad operator ``L`` - is a :class:`qutip.Qobj` and ``Gamma`` represents the corresponding + is a :class:`.Qobj` and ``Gamma`` represents the corresponding rate, which is allowed to be negative. The Lindblad operators must be operators even if ``H`` is a superoperator. If none are given, the solver will defer to ``sesolve`` or ``mesolve``. Each rate ``Gamma`` may be just a number (in the case of a constant rate) or, otherwise, - specified using any format accepted by :func:`qutip.coefficient`. + specified using any format accepted by + :func:`~qutip.core.coefficient.coefficient`. - e_ops : list, [optional] + e_ops : list, optional A ``list`` of operator as Qobj, QobjEvo or callable with signature of (t, state: Qobj) for calculating expectation values. When no ``e_ops`` are given, the solver will default to save the states. - ntraj : int + ntraj : int, default: 500 Maximum number of trajectories to run. Can be cut short if a time limit is passed with the ``timeout`` keyword or if the target tolerance is reached, see ``target_tol``. - args : None / dict + args : dict, optional Arguments for time-dependent Hamiltonian and collapse operator terms. - options : None / dict + options : dict, optional Dictionary of options for the solver. - - store_final_state : bool, [False] - Whether or not to store the final state of the evolution in the - result class. - - store_states : bool, NoneType, [None] - Whether or not to store the state density matrices. - On ``None`` the states will be saved if no expectation operators are - given. - - progress_bar : str {'text', 'enhanced', 'tqdm', ''}, ['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. - - progress_kwargs : dict, [{"chunk_size": 10}] - kwargs to pass to the progress_bar. Qutip's bars use ``chunk_size``. - - method : str {"adams", "bdf", "dop853", "vern9", etc.}, ["adams"] - Which differential equation integration method to use. - - keep_runs_results : bool, [False] - Whether to store results from all trajectories or just store the - averages. - - map : str {"serial", "parallel", "loky"}, ["serial"] - How to run the trajectories. "parallel" uses concurrent module to run - in parallel while "loky" use the module of the same name to do so. - - job_timeout : NoneType, int, [None] - Maximum time to compute one trajectory. - - num_cpus : NoneType, int, [None] - Number of cpus to use when running in parallel. ``None`` detect the - number of available cpus. - - norm_t_tol, norm_tol, norm_steps : float, float, int, [1e-6, 1e-4, 5] - Parameters used to find the collapse location. ``norm_t_tol`` and - ``norm_tol`` are the tolerance in time and norm respectively. - An error will be raised if the collapse could not be found within - ``norm_steps`` tries. - - mc_corr_eps : float, [1e-10] - Small number used to detect non-physical collapse caused by numerical - imprecision. - - atol, rtol : float, [1e-8, 1e-6] - Absolute and relative tolerance of the ODE integrator. - - nsteps : int [2500] - Maximum number of (internally defined) steps allowed in one ``tlist`` - step. - - max_step : float, [0] - Maximum length of one internal step. When using pulses, it should be - less than half the width of the thinnest pulse. - - completeness_rtol, completeness_atol : float, float, [1e-5, 1e-8] - Parameters used in determining whether the given Lindblad operators - satisfy a certain completeness relation. If they do not, an - additional Lindblad operator is added automatically (with zero rate). - - martingale_quad_limit : float or int, [100] - An upper bound on the number of subintervals used in the adaptive - integration of the martingale. - - seeds : int, SeedSequence, list, [optional] + - | store_final_state : bool + | Whether or not to store the final state of the evolution in the + result class. + - | store_states : bool, None + | Whether or not to store the state vectors or density matrices. + On `None` the states will be saved if no expectation operators are + given. + - | 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 + if not installed. Empty string or False will disable the bar. + - | progress_kwargs : dict + | kwargs to pass to the progress_bar. Qutip's bars use `chunk_size`. + - | method : str ["adams", "bdf", "lsoda", "dop853", "vern9", etc.] + | Which differential equation integration method to use. + - | atol, rtol : float + | Absolute and relative tolerance of the ODE integrator. + - | nsteps : int + | 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. + - | keep_runs_results : bool, [False] + | Whether to store results from all trajectories or just store the + averages. + - | 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. + - | num_cpus : int + | Number of cpus to use when running in parallel. ``None`` detect the + number of available cpus. + - | 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. + An error will be raised if the collapse could not be found within + ``norm_steps`` tries. + - | mc_corr_eps : float + | Small number used to detect non-physical collapse caused by + numerical imprecision. + - | 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 + 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 + integration of the martingale. + + Note that the 'improved_sampling' option is not currently supported. + 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>`_. + + 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 trajectory. Seeds are saved in the result and they can be reused with:: seeds=prev_result.seeds - target_tol : float, tuple, list, [optional] + target_tol : float, tuple, list, optional Target tolerance of the evolution. The evolution will compute trajectories until the error on the expectation values is lower than this tolerance. The maximum number of trajectories employed is @@ -141,13 +149,13 @@ def nm_mcsolve(H, state, tlist, ops_and_rates=(), e_ops=None, ntraj=500, *, relative tolerance, in that order. Lastly, it can be a list of pairs of (atol, rtol) for each e_ops. - timeout : float, [optional] + timeout : float, optional Maximum time for the evolution in seconds. When reached, no more trajectories will be computed. Returns ------- - results : :class:`qutip.solver.NmmcResult` + results : :class:`.NmmcResult` Object storing all results from the simulation. Compared to a result returned by ``mcsolve``, this result contains the additional field ``trace`` (and ``runs_trace`` if ``store_final_state`` is set). Note @@ -176,7 +184,7 @@ def nm_mcsolve(H, state, tlist, ops_and_rates=(), e_ops=None, ntraj=500, *, nmmc = NonMarkovianMCSolver(H, ops_and_rates, options=options) result = nmmc.run(state, tlist=tlist, ntraj=ntraj, e_ops=e_ops, - seed=seeds, target_tol=target_tol, timeout=timeout) + seeds=seeds, target_tol=target_tol, timeout=timeout) return result @@ -288,21 +296,21 @@ def set_state(self, t, *args, **kwargs): class NonMarkovianMCSolver(MCSolver): """ Monte Carlo Solver for Lindblad equations with "rates" that may be - negative. The ``c_ops`` parameter of :class:`qutip.MCSolver` is replaced by + negative. The ``c_ops`` parameter of :class:`.MCSolver` is replaced by an ``ops_and_rates`` parameter to allow for negative rates. Options for the underlying ODE solver are given by the Options class. Parameters ---------- - H : :class:`qutip.Qobj`, :class:`qutip.QobjEvo`, ``list``, callable. + H : :class:`.Qobj`, :class:`.QobjEvo`, ``list``, callable. System Hamiltonian as a Qobj, QobjEvo. It can also be any input type - that QobjEvo accepts (see :class:`qutip.QobjEvo` documentation). + that QobjEvo accepts (see :class:`.QobjEvo` documentation). ``H`` can also be a superoperator (liouvillian) if some collapse operators are to be treated deterministically. ops_and_rates : list A ``list`` of tuples ``(L, Gamma)``, where the Lindblad operator ``L`` - is a :class:`qutip.Qobj` and ``Gamma`` represents the corresponding + is a :class:`.Qobj` and ``Gamma`` represents the corresponding rate, which is allowed to be negative. The Lindblad operators must be operators even if ``H`` is a superoperator. Each rate ``Gamma`` may be just a number (in the case of a constant rate) or, otherwise, specified @@ -313,29 +321,35 @@ class NonMarkovianMCSolver(MCSolver): options : SolverOptions, [optional] Options for the evolution. - - seed : 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 seed, one for each - trajectory. Seeds are saved in the result and can be reused with:: - - seeds=prev_result.seeds """ name = "nm_mcsolve" - resultclass = NmmcResult + _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, } # both classes will be partially initialized in constructor - trajectory_resultclass = NmmcTrajectoryResult - mc_integrator_class = NmMCIntegrator + _trajectory_resultclass = NmmcTrajectoryResult + _mc_integrator_class = NmMCIntegrator def __init__( - self, H, ops_and_rates, *_args, args=None, options=None, **kwargs, + self, H, ops_and_rates, args=None, options=None, ): self.options = options @@ -366,13 +380,13 @@ def __init__( for op, sqrt_shifted_rate in zip(self.ops, self._sqrt_shifted_rates) ] - self.trajectory_resultclass = functools.partial( + self._trajectory_resultclass = functools.partial( NmmcTrajectoryResult, __nm_solver=self, ) - self.mc_integrator_class = functools.partial( + self._mc_integrator_class = functools.partial( NmMCIntegrator, __martingale=self._martingale, ) - super().__init__(H, c_ops, *_args, options=options, **kwargs) + super().__init__(H, c_ops, options=options) def _check_completeness(self, ops_and_rates): """ @@ -412,6 +426,11 @@ def _argument(self, args): ] super()._argument(args) + def add_feedback(self, key, type): + raise NotImplementedError( + "NM mcsolve does not support feedback currently." + ) + def rate_shift(self, t): """ Return the rate shift at time ``t``. @@ -505,17 +524,100 @@ def step(self, t, *, args=None, copy=True): state = ket2dm(state) return state * self.current_martingale() - def run(self, state, tlist, *args, **kwargs): + def run(self, state, tlist, ntraj=1, *, args=None, **kwargs): # update `args` dictionary before precomputing martingale - if 'args' in kwargs: - self._argument(kwargs.pop('args')) + self._argument(args) self._martingale.initialize(tlist[0], cache=tlist) - result = super().run(state, tlist, *args, **kwargs) + result = super().run(state, tlist, ntraj, **kwargs) self._martingale.reset() return result + @property + def options(self): + """ + Options for non-Markovian Monte Carlo solver: + + store_final_state: bool, default: False + Whether or not to store the final state of the evolution in the + result class. + + store_states: bool, default: None + Whether or not to store the state vectors or density matrices. + On `None` the states will be saved if no expectation operators are + given. + + 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. + + 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. + + method: str, default: "adams" + 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 + 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. + + mc_corr_eps: float, default: 1e-10 + Small number used to detect non-physical collapse caused by + numerical imprecision. + + norm_t_tol: float, default: 1e-6 + Tolerance in time used when finding the collapse. + + norm_tol: float, default: 1e-4 + Tolerance in norm used when finding the collapse. + + norm_steps: int, default: 5 + Maximum number of tries to find the collapse. + + completeness_rtol: float, default: 1e-5 + Used in determining whether the given Lindblad operators satisfy + a certain completeness relation. If they do not, an additional + Lindblad operator is added automatically (with zero rate). + + completeness_atol: float, default: 1e-8 + Used in determining whether the given Lindblad operators satisfy + a certain completeness relation. If they do not, an additional + Lindblad operator is added automatically (with zero rate). + + martingale_quad_limit: float or int, default: 100 + 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. + """ + return self._options + + @options.setter + def options(self, new_options): + MCSolver.options.fset(self, new_options) + start.__doc__ = MultiTrajSolver.start.__doc__ step.__doc__ = MultiTrajSolver.step.__doc__ run.__doc__ = MultiTrajSolver.run.__doc__ diff --git a/qutip/solver/nonmarkov/transfertensor.py b/qutip/solver/nonmarkov/transfertensor.py index fd4f1721c7..ce8bea512f 100644 --- a/qutip/solver/nonmarkov/transfertensor.py +++ b/qutip/solver/nonmarkov/transfertensor.py @@ -15,36 +15,36 @@ from qutip import spre, vector_to_operator, operator_to_vector, Result -def ttmsolve(dynmaps, state0, times, e_ops=[], num_learning=0, options=None): +def ttmsolve(dynmaps, state0, times, e_ops=(), num_learning=0, options=None): """ Expand time-evolution using the Transfer Tensor Method [1]_, based on a set of precomputed dynamical maps. Parameters ---------- - dynmaps : list of :class:`qutip.Qobj`, callable + dynmaps : list of :class:`.Qobj`, callable List of precomputed dynamical maps (superoperators) for the first times of ``times`` or a callback function that returns the superoperator at a given time. - state0 : :class:`qutip.Qobj` + state0 : :class:`.Qobj` Initial density matrix or state vector (ket). times : array_like List of times :math:`t_n` at which to compute results. Must be uniformily spaced. - e_ops : :class:`qutip.qobj`, callable, or list. + e_ops : :class:`.Qobj`, callable, or list, optional Single operator or list of operators for which to evaluate expectation values or callable or list of callable. Callable signature must be, `f(t: float, state: Qobj)`. See :func:`expect` for more detail of operator expectation. - num_learning : int + num_learning : int, default: 0 Number of times used to construct the dynmaps operators when ``dynmaps`` is a callable. - options : dictionary + options : dictionary, optional Dictionary of options for the solver. - store_final_state : bool @@ -62,8 +62,8 @@ def ttmsolve(dynmaps, state0, times, e_ops=[], num_learning=0, options=None): Returns ------- - output: :class:`qutip.solver.Result` - An instance of the class :class:`qutip.solver.Result`. + output: :class:`.Result` + An instance of the class :class:`.Result`. .. [1] Javier Cerrillo and Jianshu Cao, Phys. Rev. Lett 112, 110401 (2014) """ @@ -148,7 +148,7 @@ def _generatetensors(dynmaps, threshold): Parameters ---------- - dynmaps : list of :class:`qutip.Qobj` + dynmaps : list of :class:`.Qobj` List of precomputed dynamical maps (superoperators) at the times specified in `learningtimes`. @@ -158,7 +158,7 @@ def _generatetensors(dynmaps, threshold): Returns ------- - Tensors, diffs: list of :class:`qutip.Qobj.` + Tensors, diffs: list of :class:`.Qobj.` A list of transfer tensors :math:`T_1,\dots,T_K` """ Tensors = [] diff --git a/qutip/solver/parallel.py b/qutip/solver/parallel.py index 83bf727a0a..425a317f21 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 @@ -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 @@ -25,7 +26,6 @@ default_map_kw = { - 'job_timeout': threading.TIMEOUT_MAX, 'timeout': threading.TIMEOUT_MAX, 'num_cpus': available_cpu_count(), 'fail_fast': True, @@ -56,7 +56,7 @@ def serial_map(task, values, task_args=None, task_kwargs=None, result = [task(value, *task_args, **task_kwargs) for value in values] - This function work as a drop-in replacement of :func:`qutip.parallel_map`. + This function work as a drop-in replacement of :func:`parallel_map`. Parameters ---------- @@ -65,20 +65,20 @@ def serial_map(task, values, task_args=None, task_kwargs=None, values : array / list The list or array of values for which the ``task`` function is to be evaluated. - task_args : list / dictionary + task_args : list, optional The optional additional argument to the ``task`` function. - task_kwargs : list / dictionary + task_kwargs : dictionary, optional The optional additional keyword argument to the ``task`` function. - reduce_func : func (optional) + reduce_func : func, optional If provided, it will be called with the output of each tasks instead of storing a them in a list. It should return None or a number. When returning a number, it represent the estimation of the number of task left. On a return <= 0, the map will end early. - progress_bar : string + progress_bar : str, optional Progress bar options's string for showing progress. - progress_bar_kwargs : dict + progress_bar_kwargs : dict, optional Options for the progress bar. - map_kw: dict (optional) + map_kw: dict, optional Dictionary containing: - timeout: float, Maximum time (sec) for the whole map. - fail_fast: bool, Raise an error at the first. @@ -132,59 +132,33 @@ 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, num_workers, + 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:: + 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: - result = [task(value, *task_args, **task_kwargs) for value in values] + setup_executor: () -> ProcessPoolExecutor - 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 / dictionary - The optional additional argument to the ``task`` function. - task_kwargs : list / dictionary - The optional additional keyword argument to the ``task`` function. - reduce_func : func (optional) - If provided, it will be called with the output of each tasks instead of - storing a 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 represent the estimation of the - number of task left. On a return <= 0, the map will end early. - progress_bar : string - Progress bar options's string for showing progress. - progress_bar_kwargs : dict - Options for the progress bar. - map_kw: dict (optional) - Dictionary containing entry for: - - timeout: float, Maximum time (sec) for the whole map. - - job_timeout: float, Maximum time (sec) for each job in the map. - - num_cpus: int, Number of job to run at once. - - fail_fast: bool, Raise an error at the first. - - 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. + 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 + 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() - job_time = map_kw['job_timeout'] + end_time = timeout + time.time() progress_bar = progress_bars[progress_bar]( len(values), **progress_bar_kwargs @@ -194,41 +168,47 @@ def parallel_map(task, values, task_args=None, task_kwargs=None, finished = [] if reduce_func is not None: results = None - result_func = lambda i, value: reduce_func(value) + + def result_func(_, value): + return reduce_func(value) else: results = [None] * len(values) - result_func = lambda i, value: results.__setitem__(i, value) + result_func = results.__setitem__ def _done_callback(future): if not future.cancelled(): - try: - result = future.result() - except Exception as e: - errors[future._i] = e - remaining_ntraj = result_func(future._i, result) - if remaining_ntraj is not None and remaining_ntraj <= 0: - finished.append(True) + 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 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: + finished.append(True) 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: + with setup_executor() as 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()) @@ -239,12 +219,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( @@ -258,13 +239,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( @@ -275,12 +264,89 @@ def _done_callback(future): 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={}): + """ + 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. + + """ + + map_kw = _read_map_kw(map_kw) + if sys.version_info >= (3, 7): + # ProcessPoolExecutor only supports mp_context from 3.7 onwards + ctx_kw = {"mp_context": mp_context} + else: + ctx_kw = {} + + def setup_executor(): + return concurrent.futures.ProcessPoolExecutor( + max_workers=map_kw['num_cpus'], **ctx_kw, + ) + + 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'], 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, 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:: + 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] @@ -293,28 +359,28 @@ def loky_pmap(task, values, task_args=None, task_kwargs=None, values : array / list The list or array of values for which the ``task`` function is to be evaluated. - task_args : list / dictionary - The optional additional argument to the ``task`` function. - task_kwargs : list / dictionary - The optional additional keyword argument to the ``task`` function. - reduce_func : func (optional) - If provided, it will be called with the output of each tasks instead of - storing a them in a list. It should return None or a number. When - returning a number, it represent the estimation of the number of task - left. On a return <= 0, the map will end early. - progress_bar : string + 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 + progress_bar_kwargs : dict, optional Options for the progress bar. - map_kw: dict (optional) + map_kw: dict, optional Dictionary containing entry for: - timeout: float, Maximum time (sec) for the whole map. - - job_timeout: float, Maximum time (sec) for each job in the map. - - num_cpus: int, Number of job to run at once. - - fail_fast: bool, Raise an error at the first. + - 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 @@ -322,72 +388,156 @@ 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 = {} + + from loky import get_reusable_executor + from loky.process_executor import ShutdownExecutorError map_kw = _read_map_kw(map_kw) - os.environ['QUTIP_IN_PARALLEL'] = 'TRUE' - from loky import get_reusable_executor, TimeoutError - progress_bar = progress_bars[progress_bar]( - len(values), **progress_bar_kwargs + def setup_executor(): + return get_reusable_executor(max_workers=map_kw['num_cpus']) + + def extract_result(future: concurrent.futures.Future): + exception = future.exception() + if isinstance(exception, ShutdownExecutorError): + # Task was aborted due to timeout etc + 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. + 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 ) - executor = get_reusable_executor(max_workers=map_kw['num_cpus']) - end_time = map_kw['timeout'] + time.time() - job_time = map_kw['job_timeout'] - results = None - remaining_ntraj = None - errors = {} - if reduce_func is None: - results = [None] * len(values) - try: - jobs = [executor.submit(task, value, *task_args, **task_kwargs) - for value in values] - - for n, job in enumerate(jobs): - remaining_time = min(end_time - time.time(), job_time) - try: - result = job.result(remaining_time) - 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 +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] - except KeyboardInterrupt as e: - [job.cancel() for job in jobs] - raise e + 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. - except TimeoutError: - [job.cancel() for job in jobs] + 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. + 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. - finally: + 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 + + # 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. 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') + 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) + + 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() - progress_bar.finished() - os.environ['QUTIP_IN_PARALLEL'] = 'FALSE' - if errors: - raise MapExceptions( - f"{len(errors)} iterations failed in loky_pmap", - errors, results - ) - return results + + 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 + ) -_get_map = { +_maps = { "parallel_map": parallel_map, "parallel": parallel_map, "serial_map": serial_map, "serial": serial_map, - "loky": loky_pmap + "loky": loky_pmap, + "mpi": mpi_pmap } + + +def _get_map(options): + map_func = _maps[options['map']] + + if map_func == mpi_pmap: + map_kw = options['mpi_options'] + else: + map_kw = {} + + return map_func, map_kw diff --git a/qutip/solver/propagator.py b/qutip/solver/propagator.py index e85ad5c5a0..f64bf377d0 100644 --- a/qutip/solver/propagator.py +++ b/qutip/solver/propagator.py @@ -22,10 +22,10 @@ def propagator(H, t, c_ops=(), args=None, options=None, **kwargs): Parameters ---------- - H : :class:`Qobj`, :class:`QobjEvo`, :class:`QobjEvo` compatible format. + H : :obj:`.Qobj`, :obj:`.QobjEvo`, :obj:`.QobjEvo` compatible format Possibly time-dependent system Liouvillian or Hamiltonian as a Qobj or - QobjEvo. ``list`` of [:class:`Qobj`, :class:`Coefficient`] or callable - that can be made into :class:`QobjEvo` are also accepted. + QobjEvo. ``list`` of [:obj:`.Qobj`, :obj:`.Coefficient`] or callable + that can be made into :obj:`.QobjEvo` are also accepted. t : float or array-like Time or list of times for which to evaluate the propagator. @@ -41,12 +41,12 @@ def propagator(H, t, c_ops=(), args=None, options=None, **kwargs): Options for the solver. **kwargs : - Extra parameters to use when creating the :class:`QobjEvo` from a list - format ``H``. + Extra parameters to use when creating the + :obj:`.QobjEvo` from a list format ``H``. Returns ------- - U : qobj, list + U : :obj:`.Qobj`, list Instance representing the propagator(s) :math:`U(t)`. Return a single Qobj when ``t`` is a number or a list when ``t`` is a list. @@ -83,12 +83,12 @@ def propagator_steadystate(U): Parameters ---------- - U : qobj + U : :obj:`.Qobj` Operator representing the propagator. Returns ------- - a : qobj + a : :obj:`.Qobj` Instance representing the steady-state density matrix. """ evals, estates = U.eigenstates() @@ -98,7 +98,6 @@ def propagator_steadystate(U): rho_data = _data.mul(rho_data, 0.5 / _data.trace(rho_data)) return Qobj(_data.add(rho_data, _data.adjoint(rho_data)), dims=U.dims[0], - type='oper', isherm=True, copy=False) @@ -108,7 +107,9 @@ class Propagator: A generator of propagator for a system. Usage: + U = Propagator(H, c_ops) + psi_t = U(t) @ psi_0 Save some previously computed propagator are stored to speed up subsequent @@ -116,38 +117,42 @@ class Propagator: Parameters ---------- - system : :class:`Qobj`, :class:`QobjEvo`, :class:`Solver` + system : :obj:`.Qobj`, :obj:`.QobjEvo`, :class:`.Solver` Possibly time-dependent system driving the evolution, either already - packaged in a solver, such as :class:`SESolver` or :class:`BRSolver`, - or the Liouvillian or Hamiltonian as a :class:`Qobj`, :class:`QobjEvo`. - ``list`` of [:class:`Qobj`, :class:`Coefficient`] or callable that can - be made into :class:`QobjEvo` are also accepted. + packaged in a solver, such as :class:`.SESolver` or :class:`.BRSolver`, + or the Liouvillian or Hamiltonian as a :obj:`.Qobj`, + :obj:`.QobjEvo`. ``list`` of [:obj:`.Qobj`, :obj:`.Coefficient`] + or callable that can be made into :obj:`.QobjEvo` are also accepted. - Solvers that run non-deterministacilly, such as :class:`MCSolver`, are + Solvers that run non-deterministacilly, such as :class:`.MCSolver`, are not supported. c_ops : list, optional - List of Qobj or QobjEvo collapse operators. + List of :obj:`.Qobj` or :obj:`.QobjEvo` collapse operators. - args : dictionary + args : dictionary, optional Parameters to callback functions for time-dependent Hamiltonians and collapse operators. - options : dict + options : dict, optional Options for the solver. - memoize : int [10] + memoize : int, default: 10 Max number of propagator to save. - tol : float [1e-14] + tol : float, default: 1e-14 Absolute tolerance for the time. If a previous propagator was computed at a time within tolerance, that propagator will be returned. - .. note:: - The :class:`Propagator` is not a :class:`QobjEvo` so it cannot be used - for operations with :class:`Qobj` or :class:`QobjEvo`. It can be made - into a :class:`QobjEvo` with :: - U = QobjEvo(Propagator(H)) + Notes + ----- + The :class:`Propagator` is not a :obj:`.QobjEvo` so + it cannot be used for operations with :obj:`.Qobj` or + :obj:`.QobjEvo`. It can be made into a + :obj:`.QobjEvo` with :: + + U = QobjEvo(Propagator(H)) + """ def __init__(self, system, *, c_ops=(), args=None, options=None, memoize=10, tol=1e-14): diff --git a/qutip/solver/result.py b/qutip/solver/result.py index 7544d09761..2f32dddbc9 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, qzero_like +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: @@ -13,9 +21,10 @@ class _QobjExpectEop: Parameters ---------- - op : :obj:`~Qobj` + 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: @@ -79,10 +90,14 @@ def __init__(self, options, *, solver=None, stats=None): self._state_processors = [] self._state_processors_require_copy = False - self.options = options + # make sure not to store a reference to the solver + options_copy = options.copy() + 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)): @@ -114,16 +129,21 @@ 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. Parameters ---------- - e_ops : :obj:`~Qobj`, :obj:`~QobjEvo`, function or list or dict of these + e_ops : :obj:`.Qobj`, :obj:`.QobjEvo`, function or list or dict of these The ``e_ops`` parameter defines the set of values to record at - each time step ``t``. If an element is a :obj:`~Qobj` or - :obj:`~QobjEvo` the value recorded is the expectation value of that + each time step ``t``. If an element is a :obj:`.Qobj` or + :obj:`.QobjEvo` the value recorded is the expectation value of that operator given the state at ``t``. If the element is a function, ``f``, the value recorded is ``f(t, state)``. @@ -151,11 +171,11 @@ class Result(_BaseResult): A list of the times at which the expectation values and states were recorded. - states : list of :obj:`~Qobj` + states : list of :obj:`.Qobj` The state at each time ``t`` (if the recording of the state was requested). - final_state : :obj:`~Qobj`: + final_state : :obj:`.Qobj`: The final state (if the recording of the final state was requested). expect : list of arrays of expectation values @@ -195,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} @@ -207,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) @@ -239,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() @@ -280,17 +311,17 @@ def add(self, t, state): t : float The time of the added state. - state : typically a :obj:`~Qobj` - The state a time ``t``. Usually this is a :obj:`~Qobj` with + state : typically a :obj:`.Qobj` + The state a time ``t``. Usually this is a :obj:`.Qobj` with suitable dimensions, but it sub-classes of result might support other forms of the state. - .. note:: - - The expectation values, i.e. ``e_ops``, and states are recorded by - the state processors (see ``.add_processor``). + Notes + ----- + The expectation values, i.e. ``e_ops``, and states are recorded by + the state processors (see ``.add_processor``). - Additional processors may be added by sub-classes. + Additional processors may be added by sub-classes. """ self.times.append(t) @@ -307,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]}]" @@ -330,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): """ @@ -337,10 +379,10 @@ class MultiTrajResult(_BaseResult): Parameters ---------- - e_ops : :obj:`~Qobj`, :obj:`~QobjEvo`, function or list or dict of these + e_ops : :obj:`.Qobj`, :obj:`.QobjEvo`, function or list or dict of these The ``e_ops`` parameter defines the set of values to record at - each time step ``t``. If an element is a :obj:`~Qobj` or - :obj:`~QobjEvo` the value recorded is the expectation value of that + each time step ``t``. If an element is a :obj:`.Qobj` or + :obj:`.QobjEvo` the value recorded is the expectation value of that operator given the state at ``t``. If the element is a function, ``f``, the value recorded is ``f(t, state)``. @@ -363,25 +405,25 @@ class MultiTrajResult(_BaseResult): kw : dict Additional parameters specific to a result sub-class. - Properties + Attributes ---------- times : list A list of the times at which the expectation values and states were recorded. - average_states : list of :obj:`~Qobj` + average_states : list of :obj:`.Qobj` The state at each time ``t`` (if the recording of the state was requested) averaged over all trajectories as a density matrix. - runs_states : list of list of :obj:`~Qobj` + runs_states : list of list of :obj:`.Qobj` The state for each trajectory and each time ``t`` (if the recording of the states and trajectories was requested) - final_state : :obj:`~Qobj: + final_state : :obj:`.Qobj`: The final state (if the recording of the final state was requested) averaged over all trajectories as a density matrix. - runs_final_state : list of :obj:`~Qobj` + runs_final_state : list of :obj:`.Qobj` The final state for each trajectory (if the recording of the final state and trajectories was requested). @@ -451,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) @@ -467,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 @@ -485,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)) @@ -503,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) @@ -517,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): @@ -565,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): @@ -582,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): """ @@ -632,8 +693,8 @@ def add(self, trajectory_info): - trajectory : :class:`Result` Run result for one evolution over the times. - Return - ------ + Returns + ------- remaing_traj : number Return the number of trajectories still needed to reach the target tolerance. If no tolerance is provided, return infinity. @@ -651,6 +712,7 @@ def add_end_condition(self, ntraj, target_tol=None): Set the condition to stop the computing trajectories when the certain condition are fullfilled. Supported end condition for multi trajectories computation are: + - Reaching a number of trajectories. - Error bar on the expectation values reach smaller than a given tolerance. @@ -671,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 @@ -686,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 @@ -713,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): @@ -739,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 @@ -765,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 @@ -791,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: @@ -822,23 +899,35 @@ 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 + ) + 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 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 = [ + state1 + state2 for state1, state2 in zip( + 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 @@ -849,35 +938,34 @@ 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 class McTrajectoryResult(Result): """ - Result class used by the :class:`qutip.MCSolver` for single trajectories. + Result class used by the :class:`.MCSolver` for single trajectories. """ 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): @@ -886,10 +974,10 @@ class McResult(MultiTrajResult): Parameters ---------- - e_ops : :obj:`~Qobj`, :obj:`~QobjEvo`, function or list or dict of these + e_ops : :obj:`.Qobj`, :obj:`.QobjEvo`, function or list or dict of these The ``e_ops`` parameter defines the set of values to record at - each time step ``t``. If an element is a :obj:`~Qobj` or - :obj:`~QobjEvo` the value recorded is the expectation value of that + each time step ``t``. If an element is a :obj:`.Qobj` or + :obj:`.QobjEvo` the value recorded is the expectation value of that operator given the state at ``t``. If the element is a function, ``f``, the value recorded is ``f(t, state)``. @@ -911,12 +999,13 @@ class McResult(MultiTrajResult): kw : dict Additional parameters specific to a result sub-class. - Properties + Attributes ---------- collapse : list 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): @@ -936,7 +1025,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 @@ -948,7 +1037,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 @@ -963,7 +1052,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 @@ -979,10 +1070,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 @@ -993,6 +1086,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 @@ -1011,14 +1105,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): @@ -1035,13 +1131,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)) @@ -1058,10 +1156,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 @@ -1083,12 +1183,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) @@ -1105,12 +1205,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): @@ -1118,11 +1234,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) ) @@ -1140,8 +1256,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 @@ -1149,7 +1267,7 @@ def photocurrent(self): class NmmcTrajectoryResult(McTrajectoryResult): """ - Result class used by the :class:`qutip.NonMarkovianMCSolver` for single + Result class used by the :class:`.NonMarkovianMCSolver` for single trajectories. Additionally stores the trace of the state along the trajectory. """ @@ -1179,10 +1297,10 @@ class NmmcResult(McResult): Parameters ---------- - e_ops : :obj:`~Qobj`, :obj:`~QobjEvo`, function or list or dict of these + e_ops : :obj:`.Qobj`, :obj:`.QobjEvo`, function or list or dict of these The ``e_ops`` parameter defines the set of values to record at - each time step ``t``. If an element is a :obj:`~Qobj` or - :obj:`~QobjEvo` the value recorded is the expectation value of that + each time step ``t``. If an element is a :obj:`.Qobj` or + :obj:`.QobjEvo` the value recorded is the expectation value of that operator given the state at ``t``. If the element is a function, ``f``, the value recorded is ``f(t, state)``. @@ -1204,7 +1322,7 @@ class NmmcResult(McResult): kw : dict Additional parameters specific to a result sub-class. - Properties + Attributes ---------- average_trace : list The average trace (i.e., averaged over all trajectories) at each time. @@ -1236,15 +1354,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 diff --git a/qutip/solver/scattering.py b/qutip/solver/scattering.py index 5855832dbb..23b5450fe9 100644 --- a/qutip/solver/scattering.py +++ b/qutip/solver/scattering.py @@ -53,7 +53,7 @@ def photon_scattering_amplitude(propagator, c_ops, tlist, taus, psi, psit): Parameters ---------- - propagator : :class:Propagator + propagator : :class:`.Propagator` Propagator c_ops : list list of collapse operators for each waveguide; these are assumed to @@ -104,6 +104,8 @@ def _temporal_basis_dims(waveguide_emission_indices, n_time_bins, """ Return the dims of the ``temporal_basis_vector``. """ + # TODO: Review n_emissions: change the number of dims but the equivalent + # does not exist in _temporal_basis_idx num_col = len(waveguide_emission_indices) if n_emissions is None: n_emissions = sum( @@ -113,8 +115,7 @@ def _temporal_basis_dims(waveguide_emission_indices, n_time_bins, return [num_col * n_time_bins] * n_emissions -def temporal_basis_vector(waveguide_emission_indices, n_time_bins, - n_emissions=None): +def temporal_basis_vector(waveguide_emission_indices, n_time_bins): """ Generate a temporal basis vector for emissions at specified time bins into specified waveguides. @@ -129,14 +130,13 @@ def temporal_basis_vector(waveguide_emission_indices, n_time_bins, Returns ------- - temporal_basis_vector : :class: qutip.Qobj + temporal_basis_vector : :class:`.Qobj` A basis vector representing photon scattering at the specified indices. If there are W waveguides, T times, and N photon emissions, then the basis vector has dimensionality (W*T)^N. """ idx = _temporal_basis_idx(waveguide_emission_indices, n_time_bins) - dims = _temporal_basis_dims(waveguide_emission_indices, - n_time_bins, n_emissions) + dims = _temporal_basis_dims(waveguide_emission_indices, n_time_bins, None) return basis(dims, list(idx)) @@ -189,12 +189,12 @@ def temporal_scattered_state(H, psi0, n_emissions, c_ops, tlist, Parameters ---------- - H : :class: qutip.Qobj or list + H : :class:`.Qobj` or list System-waveguide(s) Hamiltonian or effective Hamiltonian in Qobj or list-callback format. If construct_effective_hamiltonian is not specified, an effective Hamiltonian is constructed from `H` and `c_ops`. - psi0 : :class: qutip.Qobj + psi0 : :class:`.Qobj` Initial state density matrix :math:`\\rho(t_0)` or state vector :math:`\\psi(t_0)`. n_emissions : int @@ -206,10 +206,10 @@ def temporal_scattered_state(H, psi0, n_emissions, c_ops, tlist, tlist : array_like List of times for :math:`\\tau_i`. tlist should contain 0 and exceed the pulse duration / temporal region of interest. - system_zero_state : :class: qutip.Qobj + system_zero_state : :class:`.Qobj`, optional State representing zero excitations in the system. Defaults to :math:`\\psi(t_0)` - construct_effective_hamiltonian : bool + construct_effective_hamiltonian : bool, default: True Whether an effective Hamiltonian should be constructed from H and c_ops: :math:`H_{eff} = H - \\frac{i}{2} \\sum_n \\sigma_n^\\dagger \\sigma_n` @@ -217,7 +217,7 @@ def temporal_scattered_state(H, psi0, n_emissions, c_ops, tlist, Returns ------- - phi_n : :class: qutip.Qobj + phi_n : :class:`.Qobj` The scattered bath state projected onto the temporal basis given by tlist. If there are W waveguides, T times, and N photon emissions, then the state is a tensor product state with dimensionality T^(W*N). @@ -241,12 +241,12 @@ def scattering_probability(H, psi0, n_emissions, c_ops, tlist, Parameters ---------- - H : :class: qutip.Qobj or list + H : :class:`.Qobj` or list System-waveguide(s) Hamiltonian or effective Hamiltonian in Qobj or list-callback format. If construct_effective_hamiltonian is not specified, an effective Hamiltonian is constructed from H and `c_ops`. - psi0 : :class: qutip.Qobj + psi0 : :class:`.Qobj` Initial state density matrix :math:`\\rho(t_0)` or state vector :math:`\\psi(t_0)`. n_emissions : int @@ -260,10 +260,10 @@ def scattering_probability(H, psi0, n_emissions, c_ops, tlist, List of times for :math:`\\tau_i`. tlist should contain 0 and exceed the pulse duration / temporal region of interest; tlist need not be linearly spaced. - system_zero_state : :class: qutip.Qobj + system_zero_state : :class:`.Qobj`, optional State representing zero excitations in the system. Defaults to `basis(systemDims, 0)`. - construct_effective_hamiltonian : bool + construct_effective_hamiltonian : bool, default: True Whether an effective Hamiltonian should be constructed from H and c_ops: :math:`H_{eff} = H - \\frac{i}{2} \\sum_n \\sigma_n^\\dagger \\sigma_n` diff --git a/qutip/solver/sesolve.py b/qutip/solver/sesolve.py index 0914624232..8d43988299 100644 --- a/qutip/solver/sesolve.py +++ b/qutip/solver/sesolve.py @@ -7,97 +7,100 @@ import numpy as np from time import time from .. import Qobj, QobjEvo -from .solver_base import Solver +from .solver_base import Solver, _solver_deprecation +from ._feedback import _QobjFeedback, _DataFeedback -def sesolve(H, psi0, tlist, e_ops=None, args=None, options=None): +def sesolve(H, psi0, tlist, e_ops=None, args=None, options=None, **kwargs): """ Schrodinger equation evolution of a state vector or unitary matrix for a given Hamiltonian. - Evolve the state vector (`psi0`) using a given - Hamiltonian (`H`), by integrating the set of ordinary differential + Evolve the state vector (``psi0``) using a given + Hamiltonian (``H``), by integrating the set of ordinary differential equations that define the system. Alternatively evolve a unitary matrix in solving the Schrodinger operator equation. The output is either the state vector or unitary matrix at arbitrary points - in time (`tlist`), or the expectation values of the supplied operators - (`e_ops`). If e_ops is a callback function, it is invoked for each + in time (``tlist``), or the expectation values of the supplied operators + (``e_ops``). If e_ops is a callback function, it is invoked for each time in `tlist` with time and the state as arguments, and the function does not use any return values. e_ops cannot be used in conjunction with solving the Schrodinger operator equation **Time-dependent operators** - For time-dependent problems, `H` and `c_ops` can be a :class:`QobjEvo` or - object that can be interpreted as :class:`QobjEvo` such as a list of + For time-dependent problems, ``H`` and ``c_ops`` can be a :obj:`.QobjEvo` + or object that can be interpreted as :obj:`.QobjEvo` such as a list of (Qobj, Coefficient) pairs or a function. Parameters ---------- - H : :class:`Qobj`, :class:`QobjEvo`, :class:`QobjEvo` compatible format. + H : :obj:`.Qobj`, :obj:`.QobjEvo`, :obj:`.QobjEvo` compatible format. System Hamiltonian as a Qobj or QobjEvo for time-dependent - Hamiltonians. List of [:class:`Qobj`, :class:`Coefficient`] or callable - that can be made into :class:`QobjEvo` are also accepted. + Hamiltonians. List of [:obj:`.Qobj`, :obj:`.Coefficient`] or callable + that can be made into :obj:`.QobjEvo` are also accepted. - psi0 : :class:`qutip.qobj` + psi0 : :obj:`.Qobj` initial state vector (ket) or initial unitary operator `psi0 = U` tlist : *list* / *array* list of times for :math:`t`. - e_ops : :class:`qutip.qobj`, callable, or list. + e_ops : :obj:`.Qobj`, callable, or list, optional Single operator or list of operators for which to evaluate expectation values or callable or list of callable. Callable signature must be, `f(t: float, state: Qobj)`. - See :func:`expect` for more detail of operator expectation. + See :func:`~qutip.core.expect.expect` for more detail of operator + expectation. - args : None / *dictionary* + args : dict, optional dictionary of parameters for time-dependent Hamiltonians - options : None / dict + options : dict, optional Dictionary of options for the solver. - - store_final_state : bool - Whether or not to store the final state of the evolution in the - result class. - - store_states : bool, None - 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 - if not installed. Empty string or False will disable the bar. - - progress_kwargs : dict - kwargs to pass to the progress_bar. Qutip's bars use `chunk_size`. - - method : str ["adams", "bdf", "lsoda", "dop853", "vern9", etc.] - Which differential equation integration method to use. - - atol, rtol : float - Absolute and relative tolerance of the ODE integrator. - - nsteps : - Maximum number of (internally defined) steps allowed in one ``tlist`` - step. - - max_step : float, 0 - Maximum lenght of one internal step. When using pulses, it should be - less than half the width of the thinnest pulse. + - | store_final_state : bool + | Whether or not to store the final state of the evolution in the + result class. + - | store_states : bool, None + | 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 + if not installed. Empty string or False will disable the bar. + - | progress_kwargs : dict + | kwargs to pass to the progress_bar. Qutip's bars use `chunk_size`. + - | method : str ["adams", "bdf", "lsoda", "dop853", "vern9", etc.] + | Which differential equation integration method to use. + - | atol, rtol : float + | Absolute and relative tolerance of the ODE integrator. + - | nsteps : int + | 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 + less than half the width of the thinnest pulse. Other options could be supported depending on the integration method, see `Integrator <./classes.html#classes-ode>`_. Returns ------- - result: :class:`qutip.Result` + result: :obj:`.Result` - An instance of the class :class:`qutip.Result`, which contains - a *list of array* `result.expect` of expectation values for the times - specified by `tlist`, and/or a *list* `result.states` of state vectors - or density matrices corresponding to the times in `tlist` [if `e_ops` - is an empty list of `store_states=True` in options]. + An instance of the class :obj:`.Result`, which contains a *list of + array* ``result.expect`` of expectation values for the times specified + by ``tlist``, and/or a *list* ``result.states`` of state vectors or + density matrices corresponding to the times in ``tlist`` [if ``e_ops`` + is an empty list of ``store_states=True`` in options]. """ + options = _solver_deprecation(kwargs, options) H = QobjEvo(H, args=args, tlist=tlist) solver = SESolver(H, options=options) return solver.run(psi0, tlist, e_ops=e_ops) @@ -110,10 +113,10 @@ class SESolver(Solver): Parameters ---------- - H : :class:`Qobj`, :class:`QobjEvo` + H : :obj:`.Qobj`, :obj:`.QobjEvo` System Hamiltonian as a Qobj or QobjEvo for time-dependent - Hamiltonians. List of [:class:`Qobj`, :class:`Coefficient`] or callable - that can be made into :class:`QobjEvo` are also accepted. + Hamiltonians. List of [:obj:`.Qobj`, :obj:`.Coefficient`] or callable + that can be made into :obj:`.QobjEvo` are also accepted. options : dict, optional Options for the solver, see :obj:`SESolver.options` and @@ -127,7 +130,7 @@ class SESolver(Solver): name = "sesolve" _avail_integrators = {} solver_options = { - "progress_bar": "text", + "progress_bar": "", "progress_kwargs": {"chunk_size":10}, "store_final_state": False, "store_states": None, @@ -158,28 +161,28 @@ def options(self): """ Solver's options: - store_final_state: bool, default=False + store_final_state: bool, default: False Whether or not to store the final state of the evolution in the result class. - store_states: bool, default=None + store_states: bool, default: None 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, default=True + normalize_output: bool, default: True Normalize output state to hide ODE numerical errors. - progress_bar: str {'text', 'enhanced', 'tqdm', ''}, {} + progress_bar: str {"text", "enhanced", "tqdm", ""}, default: "" 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. - progress_kwargs: dict, default={"chunk_size": 10} + progress_kwargs: dict, default: {"chunk_size": 10} Arguments to pass to the progress_bar. Qutip's bars use ``chunk_size``. - method: str, default="adams" + method: str, default: "adams" Which ordinary differential equation integration method to use. """ return self._options @@ -187,3 +190,31 @@ def options(self): @options.setter def options(self, new_options): Solver.options.fset(self, new_options) + + @classmethod + def StateFeedback(cls, default=None, raw_data=False, prop=False): + """ + State of the evolution to be used in a time-dependent operator. + + When used as an args: + + ``QobjEvo([op, func], args={"state": SESolver.StateFeedback()})`` + + The ``func`` will receive the ket as ``state`` during the evolution. + + Parameters + ---------- + default : Qobj or qutip.core.data.Data, default : None + Initial value to be used at setup of the system. + + prop : bool, default : False + Set to True when using sesolve for computing propagators. + + raw_data : bool, default : False + If True, the raw matrix will be passed instead of a Qobj. + For density matrices, the matrices can be column stacked or square + depending on the integration method. + """ + if raw_data: + return _DataFeedback(default, open=False, prop=prop) + return _QobjFeedback(default, open=False, prop=prop) diff --git a/qutip/solver/sode/_noise.py b/qutip/solver/sode/_noise.py index ab5993c8b9..e1b4592a49 100644 --- a/qutip/solver/sode/_noise.py +++ b/qutip/solver/sode/_noise.py @@ -1,11 +1,45 @@ import numpy as np -__all__ = [] +__all__ = ["Wiener"] + + +class Wiener: + """ + Wiener process. + """ + def __init__(self, t0, dt, generator, shape): + self.t0 = t0 + self.dt = dt + self.generator = generator + self.t_end = t0 + self.shape = shape + self.process = np.zeros((1,) + shape, dtype=float) + + def _extend(self, t): + N_new_vals = int((t - self.t_end + self.dt*0.01) // self.dt) + dW = self.generator.normal( + 0, np.sqrt(self.dt), size=(N_new_vals,) + self.shape + ) + W = self.process[-1, :, :] + np.cumsum(dW, axis=0) + self.process = np.concatenate((self.process, W), axis=0) + self.t_end = self.t0 + (self.process.shape[0] - 1) * self.dt + + def dW(self, t, N): + if t + N * self.dt > self.t_end: + self._extend(t + N * self.dt) + idx0 = int((t - self.t0 + self.dt * 0.01) // self.dt) + return np.diff(self.process[idx0:idx0 + N + 1, :, :], axis=0) + + def __call__(self, t): + if t > self.t_end: + self._extend(t) + idx = int((t - self.t0 + self.dt * 0.01) // self.dt) + return self.process[idx, 0, :] class _Noise: """ - Weiner process generator used for tests. + Wiener process generator used for tests. """ def __init__(self, T, dt, num=1): diff --git a/qutip/solver/sode/itotaylor.py b/qutip/solver/sode/itotaylor.py index 040f9e1ec8..6ad4df1545 100644 --- a/qutip/solver/sode/itotaylor.py +++ b/qutip/solver/sode/itotaylor.py @@ -55,13 +55,13 @@ def options(self): """ Supported options by Order 1.5 strong Taylor Stochastic Integrators: - dt : float, default=0.001 + dt : float, default: 0.001 Internal time step. - tol : float, default=1e-10 + tol : float, default: 1e-10 Relative tolerance. - derr_dt : float, default=1e-6 + derr_dt : float, default: 1e-6 Finite time difference used to compute the derrivative of the hamiltonian and ``sc_ops``. """ @@ -126,22 +126,22 @@ def options(self): Supported options by Implicit Order 1.5 strong Taylor Stochastic Integrators: - dt : float, default=0.001 + dt : float, default: 0.001 Internal time step. - tol : float, default=1e-10 + tol : float, default: 1e-10 Tolerance for the time steps. - solve_method : str, default=None + solve_method : str, default: None Method used for solver the ``Ax=b`` of the implicit step. Accept methods supported by :func:`qutip.core.data.solve`. When the system is constant, the inverse of the matrix ``A`` can be used by entering ``inv``. - solve_options : dict, default={} + solve_options : dict, default: {} Options to pass to the call to :func:`qutip.core.data.solve`. - derr_dt : float, default=1e-6 + derr_dt : float, default: 1e-6 Finite time difference used to compute the derrivative of the hamiltonian and ``sc_ops``. """ diff --git a/qutip/solver/sode/rouchon.py b/qutip/solver/sode/rouchon.py index 30c48fd0cb..61c30c0996 100644 --- a/qutip/solver/sode/rouchon.py +++ b/qutip/solver/sode/rouchon.py @@ -1,9 +1,11 @@ import numpy as np +import warnings from qutip import unstack_columns, stack_columns from qutip.core import data as _data from ..stochastic import StochasticSolver from .sode import SIntegrator from ..integrator.integrator import Integrator +from ._noise import Wiener __all__ = ["RouchonSODE"] @@ -19,12 +21,12 @@ class RouchonSODE(SIntegrator): - Order: strong 1 - .. note:: - - This method should be used with very small ``dt``. Unlike other - methods that will return unphysical state (negative eigenvalues, Nans) - when the time step is too large, this method will return state that - seems normal. + Notes + ----- + This method should be used with very small ``dt``. Unlike other + methods that will return unphysical state (negative eigenvalues, Nans) + when the time step is too large, this method will return state that + seems normal. """ integrator_options = { "dt": 0.0001, @@ -34,7 +36,11 @@ class RouchonSODE(SIntegrator): def __init__(self, rhs, options): self._options = self.integrator_options.copy() self.options = options + self.rhs = rhs + self._make_operators() + def _make_operators(self): + rhs = self.rhs self.H = rhs.H if self.H.issuper: raise TypeError("The rouchon stochastic integration method can't" @@ -62,25 +68,52 @@ def __init__(self, rhs, options): self.id = _data.identity[dtype](self.H.shape[0]) + def set_state(self, t, state0, generator): + """ + Set the state of the SODE solver. + + Parameters + ---------- + t : float + Initial time + + state0 : qutip.Data + Initial state. + + generator : numpy.random.generator + Random number generator. + """ + self.t = t + self.state = state0 + if isinstance(generator, Wiener): + self.wiener = generator + else: + self.wiener = Wiener( + t, self.options["dt"], generator, + (1, self.num_collapses,) + ) + self.rhs._register_feedback(self.wiener) + self._make_operators() + self._is_set = True + 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(self.N_dw) + 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 > self.options["tol"]: - # Not a whole number of steps. + if extra > 0.5 * dt: + # Not a whole number of steps, round to higher N += 1 - dt = delta_t / N - dW = self.generator.normal( - 0, - np.sqrt(dt), - size=(N, self.num_collapses) - ) + dW = self.wiener.dW(self.t, N)[:, 0, :] if self._issuper: self.state = unstack_columns(self.state) @@ -120,10 +153,10 @@ def options(self): """ Supported options by Rouchon Stochastic Integrators: - dt : float, default=0.001 + dt : float, default: 0.001 Internal time step. - tol : float, default=1e-7 + tol : float, default: 1e-7 Relative tolerance. """ return self._options @@ -132,5 +165,16 @@ def options(self): def options(self, new_options): Integrator.options.fset(self, new_options) + def reset(self, hard=False): + if self._is_set: + state = self.get_state() + if hard: + raise NotImplementedError( + "Changing stochastic integrator " + "options is not supported." + ) + if self._is_set: + self.set_state(*state) + StochasticSolver.add_integrator(RouchonSODE, "rouchon") diff --git a/qutip/solver/sode/sode.py b/qutip/solver/sode/sode.py index 94b04409a5..e43e8f5ac1 100644 --- a/qutip/solver/sode/sode.py +++ b/qutip/solver/sode/sode.py @@ -1,8 +1,9 @@ import numpy as np +import warnings from . import _sode from ..integrator.integrator import Integrator from ..stochastic import StochasticSolver, SMESolver - +from ._noise import Wiener __all__ = ["SIntegrator", "PlatenSODE", "PredCorr_SODE"] @@ -44,6 +45,8 @@ class SIntegrator(Integrator): keys, not the full options object passed to the solver. Options' keys included here will be supported by the :cls:SolverOdeOptions. """ + _is_set = False + _stepper_options = [] def set_state(self, t, state0, generator): """ @@ -62,10 +65,21 @@ def set_state(self, t, state0, generator): """ self.t = t self.state = state0 - self.generator = generator + if isinstance(generator, Wiener): + self.wiener = generator + else: + num_collapse = len(self.rhs.sc_ops) + self.wiener = Wiener( + t, self.options["dt"], generator, + (self.N_dw, num_collapse) + ) + self.rhs._register_feedback(self.wiener) + opt = [self.options[key] for key in self._stepper_options] + self.step_func = self.stepper(self.rhs(self.options), *opt).run + self._is_set = True def get_state(self, copy=True): - return self.t, self.state, self.generator + return self.t, self.state, self.wiener def integrate(self, t, copy=True): """ @@ -92,6 +106,16 @@ def integrate(self, t, copy=True): def mcstep(self, t, copy=True): raise NotImplementedError + def reset(self, hard=False): + if self._is_set: + state = self.get_state() + self.set_state(*state) + if hard: + raise NotImplementedError( + "Changing stochastic integrator " + "options is not supported." + ) + class _Explicit_Simple_Integrator(SIntegrator): """ @@ -108,26 +132,26 @@ class _Explicit_Simple_Integrator(SIntegrator): def __init__(self, rhs, options): self._options = self.integrator_options.copy() self.options = options - self.system = rhs(self.options) - self.step_func = self.stepper(self.system).run + self.rhs = rhs 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 > self.options["tol"]: - # Not a whole number of steps. + if extra > 0.5 * dt: + # Not a whole number of steps, round to higher N += 1 - dt = delta_t / N - dW = self.generator.normal( - 0, np.sqrt(dt), size=(N, self.N_dw, self.system.num_collapse) - ) + dW = self.wiener.dW(self.t, N) self.state = self.step_func(self.t, self.state, dt, dW, N) self.t += dt * N @@ -139,10 +163,10 @@ def options(self): """ Supported options by Explicit Stochastic Integrators: - dt : float, default=0.001 + dt : float, default: 0.001 Internal time step. - tol : float, default=1e-10 + tol : float, default: 1e-10 Tolerance for the time steps. """ return self._options @@ -163,37 +187,28 @@ class _Implicit_Simple_Integrator(_Explicit_Simple_Integrator): "solve_method": None, "solve_options": {}, } + _stepper_options = ["solve_method", "solve_options"] stepper = None N_dw = 0 - def __init__(self, rhs, options): - self._options = self.integrator_options.copy() - self.options = options - self.system = rhs(self.options) - self.step_func = self.stepper( - self.system, - self.options["solve_method"], - self.options["solve_options"], - ).run - @property def options(self): """ Supported options by Implicit Stochastic Integrators: - dt : float, default=0.001 + dt : float, default: 0.001 Internal time step. - tol : float, default=1e-10 + tol : float, default: 1e-10 Tolerance for the time steps. - solve_method : str, default=None + solve_method : str, default: None Method used for solver the ``Ax=b`` of the implicit step. Accept methods supported by :func:`qutip.core.data.solve`. When the system is constant, the inverse of the matrix ``A`` can be used by entering ``inv``. - solve_options : dict, default={} + solve_options : dict, default: {} Options to pass to the call to :func:`qutip.core.data.solve`. """ return self._options @@ -242,31 +257,24 @@ class PredCorr_SODE(_Explicit_Simple_Integrator): } stepper = _sode.PredCorr N_dw = 1 - - def __init__(self, rhs, options): - self._options = self.integrator_options.copy() - self.options = options - self.system = rhs(self.options) - self.step_func = self.stepper( - self.system, self.options["alpha"], self.options["eta"] - ).run + _stepper_options = ["alpha", "eta"] @property def options(self): """ Supported options by Explicit Stochastic Integrators: - dt : float, default=0.001 + dt : float, default: 0.001 Internal time step. - tol : float, default=1e-10 + tol : float, default: 1e-10 Tolerance for the time steps. - alpha : float, default=0. + alpha : float, default: 0. Implicit factor to the drift. eff_drift ~= drift(t) * (1-alpha) + drift(t+dt) * alpha - eta : float, default=0.5 + eta : float, default: 0.5 Implicit factor to the diffusion. eff_diffusion ~= diffusion(t) * (1-eta) + diffusion(t+dt) * eta """ diff --git a/qutip/solver/solver_base.py b/qutip/solver/solver_base.py index 028eb448f3..28a4c8ba60 100644 --- a/qutip/solver/solver_base.py +++ b/qutip/solver/solver_base.py @@ -6,18 +6,20 @@ from .result import Result from .integrator import Integrator from ..ui.progressbar import progress_bars +from ._feedback import _ExpectFeedback from time import time +import warnings class Solver: """ Runner for an evolution. - Can run the evolution at once using :method:`run` or step by step using - :method:`start` and :method:`step`. + Can run the evolution at once using :meth:`run` or step by step using + :meth:`start` and :meth:`step`. Parameters ---------- - rhs : Qobj, QobjEvo + rhs : :obj:`.Qobj`, :obj:`.QobjEvo` Right hand side of the evolution:: d state / dt = rhs @ state @@ -39,7 +41,7 @@ class Solver: "normalize_output": "ket", "method": "adams", } - resultclass = Result + _resultclass = Result def __init__(self, rhs, *, options=None): if isinstance(rhs, (QobjEvo, Qobj)): @@ -50,6 +52,7 @@ def __init__(self, rhs, *, options=None): self._integrator = self._get_integrator() self._state_metadata = {} self.stats = self._initialize_stats() + self.rhs._register_feedback({}, solver=self.name) def _initialize_stats(self): """ Return the initial values for the solver stats. @@ -66,7 +69,7 @@ def _prepare_state(self, state): Extract the data of the Qobj state. Is responsible for dims checks, preparing the data (stack columns, ...) - determining the dims of the output for :method:`_restore_state`. + determining the dims of the output for :meth:`_restore_state`. Should return the state's data such that it can be used by Integrators. """ @@ -82,7 +85,6 @@ def _prepare_state(self, state): self._state_metadata = { 'dims': state.dims, - 'type': state.type, 'isherm': state.isherm and not (self.rhs.dims == state.dims) } if self.rhs.dims[1] == state.dims: @@ -110,12 +112,12 @@ def run(self, state0, tlist, *, args=None, e_ops=None): For a ``state0`` at time ``tlist[0]`` do the evolution as directed by ``rhs`` and for each time in ``tlist`` store the state and/or - expectation values in a :class:`Result`. The evolution method and + expectation values in a :class:`.Result`. The evolution method and stored results are determined by ``options``. Parameters ---------- - state0 : :class:`Qobj` + state0 : :obj:`.Qobj` Initial state of the evolution. tlist : list of double @@ -132,9 +134,9 @@ def run(self, state0, tlist, *, args=None, e_ops=None): values. Function[s] must have the signature f(t : float, state : Qobj) -> expect. - Return - ------ - results : :class:`qutip.solver.Result` + Returns + ------- + results : :obj:`.Result` Results of the evolution. States and/or expect will be saved. You can control the saved data in the options. """ @@ -143,7 +145,7 @@ def run(self, state0, tlist, *, args=None, e_ops=None): self._integrator.set_state(tlist[0], _data0) self._argument(args) stats = self._initialize_stats() - results = self.resultclass( + results = self._resultclass( e_ops, self.options, solver=self.name, stats=stats, ) @@ -166,11 +168,10 @@ def run(self, state0, tlist, *, args=None, e_ops=None): def start(self, state0, t0): """ Set the initial state and time for a step evolution. - ``options`` for the evolutions are read at this step. Parameters ---------- - state0 : :class:`Qobj` + state0 : :obj:`.Qobj` Initial state of the evolution. t0 : double @@ -182,7 +183,7 @@ def start(self, state0, t0): def step(self, t, *, args=None, copy=True): """ - Evolve the state to ``t`` and return the state as a :class:`Qobj`. + Evolve the state to ``t`` and return the state as a :obj:`.Qobj`. Parameters ---------- @@ -197,10 +198,12 @@ def step(self, t, *, args=None, copy=True): copy : bool, optional {True} Whether to return a copy of the data or the data in the ODE solver. - .. note:: - The state must be initialized first by calling ``start`` or - ``run``. If ``run`` is called, ``step`` will continue from the last - time and state obtained. + Notes + ----- + The state must be initialized first by calling :meth:`start` or + :meth:`run`. If :meth:`run` is called, :meth:`step` will continue from + the last time and state obtained. + """ if not self._integrator._is_set: raise RuntimeError("The `start` method must called first") @@ -394,3 +397,130 @@ def add_integrator(cls, integrator, key): " of `qutip.solver.Integrator`") cls._avail_integrators[key] = integrator + + @classmethod + def ExpectFeedback(cls, operator, default=0.): + """ + Expectation value of the instantaneous state of the evolution to be + used by a time-dependent operator. + + When used as an args: + + ``QobjEvo([op, func], args={"E0": Solver.ExpectFeedback(oper)})`` + + The ``func`` will receive ``expect(oper, state)`` as ``E0`` during the + evolution. + + Parameters + ---------- + operator : Qobj, QobjEvo + Operator to compute the expectation values of. + + default : float, default : 0. + Initial value to be used at setup. + """ + return _ExpectFeedback(operator, default) + + +def _solver_deprecation(kwargs, options, solver="me"): + """ + Function to help the transition from v4 to v5. + Raise warnings for solver input that where moved from parameter to options. + """ + if options is None: + options = {} + # TODO remove by 5.1 + if "progress_bar" in kwargs: + warnings.warn( + '"progress_bar" is now included in options:\n Use ' + '`options={"progress_bar": False / True / "tqdm" / "enhanced"}`', + FutureWarning + ) + options["progress_bar"] = kwargs.pop("progress_bar") + + if "_safe_mode" in kwargs: + warnings.warn( + '"_safe_mode" is no longer supported.', + FutureWarning + ) + del kwargs["_safe_mode"] + + if "verbose" in kwargs and solver == "br": + warnings.warn( + '"verbose" is no longer supported.', + FutureWarning + ) + del kwargs["verbose"] + + if "tol" in kwargs and solver == "br": + warnings.warn( + 'The "tol" parameter is no longer used. ' + '`qutip.settings.core["auto_tidyup_atol"]` ' + 'is now used for rounding small values in sparse arrays.', + FutureWarning + ) + del kwargs["tol"] + + if "map_func" in kwargs and solver in ["mc", "stoc"]: + warnings.warn( + '"map_func" is now included in options:\n' + 'Use `options={"map": "serial" / "parallel" / "loky"}`', + FutureWarning + ) + del kwargs["map_func"] + + if "map_kwargs" in kwargs and solver in ["mc", "stoc"]: + warnings.warn( + '"map_kwargs" are now included in options:\n' + 'Use `options={"num_cpus": N}`', + FutureWarning + ) + del kwargs["map_kwargs"] + + if "nsubsteps" in kwargs and solver == "stoc": + warnings.warn( + '"nsubsteps" is now replaced by "dt" in options:\n' + 'Use `options={"dt": 0.001}`\n' + 'The given value of "nsubsteps" is ignored in this call.', + FutureWarning + ) + # Could be (tlist[1] - tlist[0]) / kwargs["nsubsteps"] + del kwargs["nsubsteps"] + + if "tol" in kwargs and solver == "stoc": + warnings.warn( + 'The "tol" parameter is now the "atol" options:\n' + 'Use `options={"atol": tol}`', + FutureWarning + ) + options["atol"] = kwargs.pop("tol") + + if "store_all_expect" in kwargs and solver == "stoc": + warnings.warn( + 'The "store_all_expect" parameter is now the ' + '"keep_runs_results" options:\n' + 'Use `options={"keep_runs_results": False / True}`', + FutureWarning + ) + options["keep_runs_results"] = kwargs.pop("store_all_expect") + + if "store_measurement" in kwargs and solver == "stoc": + warnings.warn( + 'The "store_measurement" parameter is now an options:\n' + 'Use `options={"store_measurement": False / True}`', + FutureWarning + ) + options["store_measurement"] = kwargs.pop("store_measurement") + + if ("dW_factors" in kwargs or "m_ops" in kwargs) and solver == "stoc": + raise TypeError( + '"m_ops" and "dW_factors" are now properties of ' + 'the stochastic solver class, use:\n' + '>>> solver = SMESolver(H, c_ops)\n' + '>>> solver.m_ops = m_ops\n' + '>>> solver.dW_factors = dW_factors\n' + ) + + if kwargs: + raise TypeError(f"unexpected keyword argument {kwargs.keys()}") + return options diff --git a/qutip/solver/spectrum.py b/qutip/solver/spectrum.py index cb92e1f0e6..2ed206d52c 100644 --- a/qutip/solver/spectrum.py +++ b/qutip/solver/spectrum.py @@ -20,24 +20,24 @@ def spectrum(H, wlist, c_ops, a_op, b_op, solver="es"): \lim_{t \to \infty} \left e^{-i\omega\tau} d\tau. - using the solver indicated by the `solver` parameter. Note: this spectrum + using the solver indicated by the ``solver`` parameter. Note: this spectrum is only defined for stationary statistics (uses steady state rho0) Parameters ---------- - H : :class:`qutip.qobj` + H : :class:`.qobj` system Hamiltonian. wlist : array_like List of frequencies for :math:`\omega`. c_ops : list List of collapse operators. - a_op : Qobj + a_op : :class:`.Qobj` Operator A. - b_op : Qobj + b_op : :class:`.Qobj` Operator B. - solver : str - Choice of solver (`es` for exponential series and - `pi` for psuedo-inverse, `solve` for generic solver). + solver : str, {'es', 'pi', 'solve'}, default: 'es' + Choice of solver, ``es`` for exponential series and + ``pi`` for psuedo-inverse, ``solve`` for generic solver. Returns ------- @@ -70,7 +70,7 @@ def spectrum_correlation_fft(tlist, y, inverse=False): list/array of times :math:`t` which the correlation function is given. y : array_like list/array of correlations corresponding to time delays :math:`t`. - inverse: boolean + inverse: bool, default: False boolean parameter for using a positive exponent in the Fourier Transform instead. Default is False. diff --git a/qutip/solver/steadystate.py b/qutip/solver/steadystate.py index 41fe9e4b63..5993822bd7 100644 --- a/qutip/solver/steadystate.py +++ b/qutip/solver/steadystate.py @@ -42,20 +42,20 @@ def steadystate(A, c_ops=[], *, method='direct', solver=None, **kwargs): Parameters ---------- - A : :obj:`~Qobj` + A : :obj:`.Qobj` A Hamiltonian or Liouvillian operator. c_op_list : list A list of collapse operators. - method : str, default='direct' + method : str, {"direct", "eigen", "svd", "power"}, default: "direct" The allowed methods are composed of 2 parts, the steadystate method: - "direct": Solving ``L(rho_ss) = 0`` - "eigen" : Eigenvalue problem - "svd" : Singular value decomposition - "power" : Inverse-power method - solver : str, default=None + solver : str, optional 'direct' and 'power' methods only. Solver to use when solving the ``L(rho_ss) = 0`` equation. Default supported solver are: @@ -73,12 +73,12 @@ def steadystate(A, c_ops=[], *, method='direct', solver=None, **kwargs): Extra options for these solver can be passed in ``**kw``. - use_rcm : bool, default False + use_rcm : bool, default: False Use reverse Cuthill-Mckee reordering to minimize fill-in in the LU factorization of the Liouvillian. Used with 'direct' or 'power' method. - use_wbm : bool, default False + use_wbm : bool, default: False Use Weighted Bipartite Matching reordering to make the Liouvillian diagonally dominant. This is useful for iterative preconditioners only. Used with 'direct' or 'power' method. @@ -89,17 +89,17 @@ def steadystate(A, c_ops=[], *, method='direct', solver=None, **kwargs): Liouvillian elements if not specified by the user. Used with 'direct' method. - power_tol : float, default 1e-12 + power_tol : float, default: 1e-12 Tolerance for the solution when using the 'power' method. - power_maxiter : int, default 10 + power_maxiter : int, default: 10 Maximum number of iteration to use when looking for a solution when using the 'power' method. - power_eps: double, default 1e-15 + power_eps: double, default: 1e-15 Small weight used in the "power" method. - sparse: bool + sparse: bool, default: True Whether to use the sparse eigen solver with the "eigen" method (default sparse). With "direct" and "power" method, when the solver is not specified, it is used to set whether "solve" or "spsolve" is @@ -117,18 +117,18 @@ def steadystate(A, c_ops=[], *, method='direct', solver=None, **kwargs): info : dict, optional Dictionary containing solver-specific information about the solution. - .. note:: - - The SVD method works only for dense operators (i.e. small systems). - + Notes + ----- + The SVD method works only for dense operators (i.e. small systems). """ if not A.issuper and not c_ops: raise TypeError('Cannot calculate the steady state for a ' + 'non-dissipative system.') if not A.issuper: - A = liouvillian(A) - for op in c_ops: - A += lindblad_dissipator(op) + A = liouvillian(A, c_ops) + else: + for op in c_ops: + A += lindblad_dissipator(op) if "-" in method: # to support v4's "power-gmres" method @@ -202,10 +202,14 @@ def _steadystate_direct(A, weight, **kw): N = A.shape[0] n = int(N**0.5) dtype = type(A.data) + if dtype == _data.Dia: + # Dia is bad at vector, the following matmul is 10x slower with Dia + # than CSR and Dia is missing optimization such as `use_wbm`. + dtype = _data.CSR weight_vec = _data.column_stack(_data.diag([weight] * n, 0, dtype=dtype)) - weight_mat = _data.kron( - weight_vec.transpose(), - _data.one_element[dtype]((N, 1), (0, 0), 1) + weight_mat = _data.matmul( + _data.one_element[dtype]((N, 1), (0, 0), 1), + weight_vec.transpose() ) L = _data.add(weight_mat, A.data) b = _data.one_element[dtype]((N, 1), (0, 0), weight) @@ -215,14 +219,14 @@ def _steadystate_direct(A, weight, **kw): if isinstance(L, _data.CSR): L, b = _permute_wbm(L, b) else: - warn("Only CSR matrice can be permuted.", RuntimeWarning) + warn("Only CSR matrices can be permuted.", RuntimeWarning) use_rcm = False if kw.pop("use_rcm", False): if isinstance(L, _data.CSR): L, b, perm = _permute_rcm(L, b) use_rcm = True else: - warn("Only CSR matrice can be permuted.", RuntimeWarning) + warn("Only CSR matrices can be permuted.", RuntimeWarning) if kw.pop("use_precond", False): if isinstance(L, (_data.CSR, _data.Dia)): kw["M"] = _compute_precond(L, kw) @@ -271,14 +275,14 @@ def _steadystate_power(A, **kw): if isinstance(L, _data.CSR): L, y = _permute_wbm(L, y) else: - warn("Only CSR matrice can be permuted.", RuntimeWarning) + warn("Only CSR matrices can be permuted.", RuntimeWarning) use_rcm = False if kw.pop("use_rcm", False): if isinstance(L, _data.CSR): L, y, perm = _permute_rcm(L, y) use_rcm = True else: - warn("Only CSR matrice can be permuted.", RuntimeWarning) + warn("Only CSR matrices can be permuted.", RuntimeWarning) if kw.pop("use_precond", False): if isinstance(L, (_data.CSR, _data.Dia)): kw["M"] = _compute_precond(L, kw) @@ -321,25 +325,25 @@ def steadystate_floquet(H_0, c_ops, Op_t, w_d=1.0, n_it=3, sparse=False, Parameters ---------- - H_0 : :obj:`~Qobj` + H_0 : :obj:`.Qobj` A Hamiltonian or Liouvillian operator. c_ops : list A list of collapse operators. - Op_t : :obj:`~Qobj` + Op_t : :obj:`.Qobj` The the interaction operator which is multiplied by the cosine - w_d : float, default 1.0 + w_d : float, default: 1.0 The frequency of the drive - n_it : int, default 3 + n_it : int, default: 3 The number of iterations for the solver - sparse : bool, default False + sparse : bool, default: False Solve for the steady state using sparse algorithms. - solver : str, default=None + solver : str, optional Solver to use when solving the linear system. Default supported solver are: @@ -364,11 +368,11 @@ def steadystate_floquet(H_0, c_ops, Op_t, w_d=1.0, n_it=3, sparse=False, dm : qobj Steady state density matrix. - .. note:: - - See: Sze Meng Tan, - https://copilot.caltech.edu/documents/16743/qousersguide.pdf, - Section (10.16) + Notes + ----- + See: Sze Meng Tan, + https://copilot.caltech.edu/documents/16743/qousersguide.pdf, + Section (10.16) """ @@ -408,20 +412,20 @@ def pseudo_inverse(L, rhoss=None, w=None, method='splu', *, use_rcm=False, L : Qobj A Liouvillian superoperator for which to compute the pseudo inverse. - rhoss : Qobj + rhoss : Qobj, optional A steadystate density matrix as Qobj instance, for the Liouvillian superoperator L. - w : double + w : double, optional frequency at which to evaluate pseudo-inverse. Can be zero for dense systems and large sparse systems. Small sparse systems can fail for zero frequencies. - sparse : bool + sparse : bool, optional Flag that indicate whether to use sparse or dense matrix methods when computing the pseudo inverse. - method : string + method : str, optional Method used to compte matrix inverse. Choice are 'pinv' to use scipy's function of the same name, or a linear system solver. @@ -438,6 +442,10 @@ def pseudo_inverse(L, rhoss=None, w=None, method='splu', *, use_rcm=False, own solver. When ``L`` use these data backends, see the corresponding libraries ``linalg`` for available solver. + use_rcm : bool, default: False + Use reverse Cuthill-Mckee reordering to minimize fill-in in the LU + factorization of the Liouvillian. + kwargs : dictionary Additional keyword arguments for setting parameters for solver methods. @@ -446,19 +454,18 @@ def pseudo_inverse(L, rhoss=None, w=None, method='splu', *, use_rcm=False, R : Qobj Returns a Qobj instance representing the pseudo inverse of L. - .. note:: - - In general the inverse of a sparse matrix will be dense. If you - are applying the inverse to a density matrix then it is better to - cast the problem as an Ax=b type problem where the explicit calculation - of the inverse is not required. See page 67 of "Electrons in - nanostructures" C. Flindt, PhD Thesis available online: - https://orbit.dtu.dk/fedora/objects/orbit:82314/datastreams/ - file_4732600/content - - Note also that the definition of the pseudo-inverse herein is different - from numpys pinv() alone, as it includes pre and post projection onto - the subspace defined by the projector Q. + Notes + ----- + In general the inverse of a sparse matrix will be dense. If you + are applying the inverse to a density matrix then it is better to + cast the problem as an Ax=b type problem where the explicit calculation + of the inverse is not required. See page 67 of "Electrons in + nanostructures" C. Flindt, PhD Thesis available online: + https://orbit.dtu.dk/en/publications/electrons-in-nanostructures-coherent-manipulation-and-counting-st + + Note also that the definition of the pseudo-inverse herein is different + from numpys pinv() alone, as it includes pre and post projection onto + the subspace defined by the projector Q. """ if rhoss is None: diff --git a/qutip/solver/stochastic.py b/qutip/solver/stochastic.py index d5d0504a85..b2f81e57df 100644 --- a/qutip/solver/stochastic.py +++ b/qutip/solver/stochastic.py @@ -1,12 +1,13 @@ __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 class StochasticTrajResult(Result): @@ -198,6 +199,7 @@ def __init__(self, issuper, H, sc_ops, c_ops, heterodyne): self.issuper = issuper self.heterodyne = heterodyne + self._noise_key = None if heterodyne: sc_ops = [] @@ -219,52 +221,70 @@ def __call__(self, options): else: return StochasticClosedSystem(self.H, self.sc_ops) + def arguments(self, args): + self.H.arguments(args) + for c_op in self.c_ops: + c_op.arguments(args) + for sc_op in self.sc_ops: + sc_op.arguments(args) + + def _register_feedback(self, val): + self.H._register_feedback({"wiener_process": val}, "stochatic solver") + for c_op in self.c_ops: + c_op._register_feedback( + {"WeinerFeedback": val}, "stochatic solver" + ) + for sc_op in self.sc_ops: + sc_op._register_feedback( + {"WeinerFeedback": val}, "stochatic solver" + ) + def smesolve( H, rho0, tlist, c_ops=(), sc_ops=(), heterodyne=False, *, e_ops=(), args={}, ntraj=500, options=None, - seeds=None, target_tol=None, timeout=None, + seeds=None, target_tol=None, timeout=None, **kwargs ): """ Solve stochastic master equation. Parameters ---------- - H : :class:`Qobj`, :class:`QobjEvo`, :class:`QobjEvo` compatible format. + H : :obj:`.Qobj`, :obj:`.QobjEvo`, :obj:`.QobjEvo` compatible format. System Hamiltonian as a Qobj or QobjEvo for time-dependent - Hamiltonians. List of [:class:`Qobj`, :class:`Coefficient`] or callable - that can be made into :class:`QobjEvo` are also accepted. + Hamiltonians. List of [:obj:`.Qobj`, :obj:`.Coefficient`] or callable + that can be made into :obj:`.QobjEvo` are also accepted. - rho0 : :class:`qutip.Qobj` + rho0 : :class:`.Qobj` Initial density matrix or state vector (ket). tlist : *list* / *array* List of times for :math:`t`. - c_ops : list of (:class:`QobjEvo`, :class:`QobjEvo` compatible format) + c_ops : list of (:obj:`.QobjEvo`, :obj:`.QobjEvo` compatible format), optional Deterministic collapse operator which will contribute with a standard Lindblad type of dissipation. - sc_ops : list of (:class:`QobjEvo`, :class:`QobjEvo` compatible format) + sc_ops : list of (:obj:`.QobjEvo`, :obj:`.QobjEvo` compatible format) List of stochastic collapse operators. - e_ops : : :class:`qutip.qobj`, callable, or list. + e_ops : : :class:`.qobj`, callable, or list, optional Single operator or list of operators for which to evaluate expectation values or callable or list of callable. Callable signature must be, `f(t: float, state: Qobj)`. - See :func:`expect` for more detail of operator expectation. + See :func:`.expect` for more detail of operator expectation. - args : None / *dictionary* + args : dict, optional Dictionary of parameters for time-dependent Hamiltonians and collapse operators. - ntraj : int [500] + ntraj : int, default: 500 Number of trajectories to compute. - heterodyne : bool [False] + heterodyne : bool, default: False Whether to use heterodyne or homodyne detection. - seeds : int, SeedSequence, list, [optional] + 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 trajectory. Seeds are saved in the result and they can be reused with:: @@ -282,59 +302,61 @@ def smesolve( relative tolerance, in that order. Lastly, it can be a list of pairs of ``(atol, rtol)`` for each e_ops. - timeout : float [optional] + timeout : float, optional Maximum time for the evolution in second. When reached, no more trajectories will be computed. Overwrite the option of the same name. - options : None / dict + options : dict, optional Dictionary of options for the solver. - - store_final_state : bool, [False] - Whether or not to store the final state of the evolution in the - result class. - - store_states : bool, None, [None] - Whether or not to store the state vectors or density matrices. - On `None` the states will be saved if no expectation operators are - given. - - store_measurement: bool, [False] - Whether to store the measurement and wiener process for each - trajectories. - - keep_runs_results : bool, [False] - Whether to store results from all trajectories or just store the - averages. - - normalize_output : bool, [False] - Normalize output state to hide ODE numerical errors. - - progress_bar : str {'text', 'enhanced', 'tqdm', ''}, ["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. - - progress_kwargs : dict, [{"chunk_size": 10}] - kwargs to pass to the progress_bar. Qutip's bars use `chunk_size`. - - method : str, ["rouchon"] - Which stochastic differential equation integration method to use. - Main ones are {"euler", "rouchon", "platen", "taylor1.5_imp"} - - map : str {"serial", "parallel", "loky"}, ["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. - - job_timeout : NoneType, int, [None] - Maximum time to compute one trajectory. - - num_cpus : NoneType, int, [None] - Number of cpus to use when running in parallel. ``None`` detect the - number of available cpus. - - dt : float [0.001 ~ 0.0001] - 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>`_. + - | store_final_state : bool + | Whether or not to store the final state of the evolution in the + result class. + - | store_states : bool, None + | Whether or not to store the state vectors or density matrices. + On `None` the states will be saved if no expectation operators are + given. + - | store_measurement: bool + | Whether to store the measurement and wiener process for each + trajectories. + - | keep_runs_results : bool + | Whether to store results from all trajectories or just store the + averages. + - | 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 + if not installed. Empty string or False will disable the bar. + - | progress_kwargs : dict + | kwargs to pass to the progress_bar. Qutip's bars use `chunk_size`. + - | method : str + | Which stochastic differential equation integration method to use. + Main ones are {"euler", "rouchon", "platen", "taylor1.5_imp"} + - | 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. + - | num_cpus : NoneType, int + | Number of cpus to use when running in parallel. ``None`` detect the + number of available cpus. + - | dt : float + | The finite steps lenght for the Stochastic integration method. + Default change depending on the integrator. + + 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 ------- - output: :class:`qutip.solver.Result` - - An instance of the class :class:`qutip.solver.Result`. + output: :class:`.Result` + An instance of the class :class:`.Result`. """ + options = _solver_deprecation(kwargs, options, "stoc") H = QobjEvo(H, args=args, tlist=tlist) c_ops = [QobjEvo(c_op, args=args, tlist=tlist) for c_op in c_ops] sc_ops = [QobjEvo(c_op, args=args, tlist=tlist) for c_op in sc_ops] @@ -343,51 +365,51 @@ def smesolve( ) return sol.run( rho0, tlist, ntraj, e_ops=e_ops, - seed=seeds, target_tol=target_tol, timeout=timeout, + seeds=seeds, target_tol=target_tol, timeout=timeout, ) def ssesolve( H, psi0, tlist, sc_ops=(), heterodyne=False, *, e_ops=(), args={}, ntraj=500, options=None, - seeds=None, target_tol=None, timeout=None, + seeds=None, target_tol=None, timeout=None, **kwargs ): """ Solve stochastic Schrodinger equation. Parameters ---------- - H : :class:`Qobj`, :class:`QobjEvo`, :class:`QobjEvo` compatible format. + H : :obj:`.Qobj`, :obj:`.QobjEvo`, :obj:`.QobjEvo` compatible format. System Hamiltonian as a Qobj or QobjEvo for time-dependent - Hamiltonians. List of [:class:`Qobj`, :class:`Coefficient`] or callable - that can be made into :class:`QobjEvo` are also accepted. + Hamiltonians. List of [:obj:`.Qobj`, :obj:`.Coefficient`] or callable + that can be made into :obj:`.QobjEvo` are also accepted. - psi0 : :class:`qutip.Qobj` + psi0 : :class:`.Qobj` Initial state vector (ket). tlist : *list* / *array* List of times for :math:`t`. - sc_ops : list of (:class:`QobjEvo`, :class:`QobjEvo` compatible format) + sc_ops : list of (:obj:`.QobjEvo`, :obj:`.QobjEvo` compatible format) List of stochastic collapse operators. - e_ops : :class:`qutip.qobj`, callable, or list. + e_ops : :class:`.qobj`, callable, or list, optional Single operator or list of operators for which to evaluate expectation values or callable or list of callable. Callable signature must be, `f(t: float, state: Qobj)`. See :func:`expect` for more detail of operator expectation. - args : None / *dictionary* + args : dict, optional Dictionary of parameters for time-dependent Hamiltonians and collapse operators. - ntraj : int [500] + ntraj : int, default: 500 Number of trajectories to compute. - heterodyne : bool [False] + heterodyne : bool, default: False Whether to use heterodyne or homodyne detection. - seeds : int, SeedSequence, list, [optional] + 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 trajectory. Seeds are saved in the result and they can be reused with:: @@ -403,64 +425,67 @@ def ssesolve( relative tolerance, in that order. Lastly, it can be a list of pairs of (atol, rtol) for each e_ops. - timeout : float [optional] + timeout : float, optional Maximum time for the evolution in second. When reached, no more trajectories will be computed. Overwrite the option of the same name. - options : None / dict + options : dict, optional Dictionary of options for the solver. - - store_final_state : bool, [False] - Whether or not to store the final state of the evolution in the - result class. - - store_states : bool, None, [None] - Whether or not to store the state vectors or density matrices. - On `None` the states will be saved if no expectation operators are - given. - - store_measurement: bool, [False] - Whether to store the measurement and wiener process, or brownian - noise for each trajectories. - - keep_runs_results : bool, [False] - Whether to store results from all trajectories or just store the - averages. - - normalize_output : bool, [False] - Normalize output state to hide ODE numerical errors. - - progress_bar : str {'text', 'enhanced', 'tqdm', ''}, ["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. - - progress_kwargs : dict, [{"chunk_size": 10}] - kwargs to pass to the progress_bar. Qutip's bars use `chunk_size`. - - method : str, ["rouchon"] - Which stochastic differential equation integration method to use. - Main ones are {"euler", "rouchon", "platen", "taylor1.5_imp"} - - map : str {"serial", "parallel", "loky"}, ["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. - - job_timeout : NoneType, int, [None] - Maximum time to compute one trajectory. - - num_cpus : NoneType, int, [None] - Number of cpus to use when running in parallel. ``None`` detect the - number of available cpus. - - dt : float [0.001 ~ 0.0001] - 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>`_. + - | store_final_state : bool + | Whether or not to store the final state of the evolution in the + result class. + - | store_states : bool, None + | Whether or not to store the state vectors or density matrices. + On `None` the states will be saved if no expectation operators are + given. + - | store_measurement: bool + Whether to store the measurement and wiener process, or brownian + noise for each trajectories. + - | keep_runs_results : bool + | Whether to store results from all trajectories or just store the + averages. + - | 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 + if not installed. Empty string or False will disable the bar. + - | progress_kwargs : dict + | kwargs to pass to the progress_bar. Qutip's bars use `chunk_size`. + - | method : str + | Which stochastic differential equation integration method to use. + Main ones are {"euler", "rouchon", "platen", "taylor1.5_imp"} + - | 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. + - | num_cpus : NoneType, int + | Number of cpus to use when running in parallel. ``None`` detect the + number of available cpus. + - | dt : float + | The finite steps lenght for the Stochastic integration method. + Default change depending on the integrator. + + 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 ------- - output: :class:`qutip.solver.Result` - An instance of the class :class:`qutip.solver.Result`. + output: :class:`.Result` + An instance of the class :class:`.Result`. """ + options = _solver_deprecation(kwargs, options, "stoc") H = QobjEvo(H, args=args, tlist=tlist) sc_ops = [QobjEvo(c_op, args=args, tlist=tlist) for c_op in sc_ops] sol = SSESolver(H, sc_ops, options=options, heterodyne=heterodyne) return sol.run( psi0, tlist, ntraj, e_ops=e_ops, - seed=seeds, target_tol=target_tol, timeout=timeout, + seeds=seeds, target_tol=target_tol, timeout=timeout, ) @@ -470,22 +495,23 @@ class StochasticSolver(MultiTrajSolver): """ name = "StochasticSolver" - resultclass = StochasticResult + _resultclass = StochasticResult _avail_integrators = {} system = None + _open = None 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": "taylor1.5", "map": "serial", - "job_timeout": None, + "mpi_options": {}, "num_cpus": None, "bitgenerator": None, + "method": "platen", + "store_measurement": False, } def __init__(self, H, sc_ops, heterodyne, *, c_ops=(), options=None): @@ -495,7 +521,13 @@ def __init__(self, H, sc_ops, heterodyne, *, c_ops=(), options=None): raise ValueError("c_ops are not supported by ssesolve.") rhs = _StochasticRHS(self._open, H, sc_ops, c_ops, heterodyne) - super().__init__(rhs, options=options) + self.rhs = rhs + self.system = rhs + self.options = options + self.seed_sequence = np.random.SeedSequence() + self._integrator = self._get_integrator() + self._state_metadata = {} + self.stats = self._initialize_stats() if heterodyne: self._m_ops = [] @@ -619,49 +651,55 @@ def options(self): """ Options for stochastic solver: - store_final_state: bool, default=False + store_final_state: bool, default: False Whether or not to store the final state of the evolution in the result class. - store_states: bool, default=None + store_states: None, bool, default: None Whether or not to store the state vectors or density matrices. On `None` the states will be saved if no expectation operators are given. - store_measurement: bool, [False] + store_measurement: bool, default: False Whether to store the measurement for each trajectories. Storing measurements will also store the wiener process, or brownian noise for each trajectories. - progress_bar: str {'text', 'enhanced', 'tqdm', ''}, default="text" + 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. - progress_kwargs: dict, default={"chunk_size":10} + progress_kwargs: dict, default: {"chunk_size":10} Arguments to pass to the progress_bar. Qutip's bars use ``chunk_size``. - keep_runs_results: bool + keep_runs_results: bool, default: False Whether to store results from all trajectories or just store the averages. - method: str, default="rouchon" - Which ODE integrator methods are supported. + normalize_output: bool + Normalize output state to hide ODE numerical errors. + + method: str, default: "platen" + Which differential equation integration method to use. - 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. + 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. - job_timeout: None, int, default=None - Maximum time to compute one trajectory. + 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 + num_cpus: None, int, default: None Number of cpus to use when running in parallel. ``None`` detect the number of available cpus. - bitgenerator: {None, "MT19937", "PCG64DXSM", ...}, default=None + bitgenerator: {None, "MT19937", "PCG64DXSM", ...}, default: None Which of numpy.random's bitgenerator to use. With ``None``, your numpy version's default is used. """ @@ -671,6 +709,66 @@ def options(self): def options(self, new_options): MultiTrajSolver.options.fset(self, new_options) + @classmethod + def WeinerFeedback(cls, default=None): + """ + Weiner function of the trajectory argument for time dependent systems. + + When used as an args: + + ``QobjEvo([op, func], args={"W": SMESolver.WeinerFeedback()})`` + + The ``func`` will receive a function as ``W`` that return an array of + wiener processes values at ``t``. The wiener process for the i-th + sc_ops is the i-th element for homodyne detection and the (2i, 2i+1) + pairs of process in heterodyne detection. The process is a step + function with step of length ``options["dt"]``. + + .. note:: + + WeinerFeedback can't be added to a running solver when updating + arguments between steps: ``solver.step(..., args={})``. + + Parameters + ---------- + default : callable, optional + Default function used outside the solver. + When not passed, a function returning ``np.array([0])`` is used. + + """ + return _WeinerFeedback(default) + + @classmethod + def StateFeedback(cls, default=None, raw_data=False): + """ + State of the evolution to be used in a time-dependent operator. + + When used as an args: + + ``QobjEvo([op, func], args={"state": SMESolver.StateFeedback()})`` + + The ``func`` will receive the density matrix as ``state`` during the + evolution. + + .. note:: + + Not supported by the ``rouchon`` mehtod. + + Parameters + ---------- + default : Qobj or qutip.core.data.Data, default : None + Initial value to be used at setup of the system. + + raw_data : bool, default : False + If True, the raw matrix will be passed instead of a Qobj. + For density matrices, the matrices can be column stacked or square + depending on the integration method. + + """ + if raw_data: + return _DataFeedback(default, open=cls._open) + return _QobjFeedback(default, open=cls._open) + class SMESolver(StochasticSolver): r""" @@ -678,18 +776,18 @@ class SMESolver(StochasticSolver): Parameters ---------- - H : :class:`Qobj`, :class:`QobjEvo`, :class:`QobjEvo` compatible format. + H : :obj:`.Qobj`, :obj:`.QobjEvo`, :obj:`.QobjEvo` compatible format. System Hamiltonian as a Qobj or QobjEvo for time-dependent - Hamiltonians. List of [:class:`Qobj`, :class:`Coefficient`] or callable - that can be made into :class:`QobjEvo` are also accepted. + Hamiltonians. List of [:obj:`.Qobj`, :obj:`.Coefficient`] or callable + that can be made into :obj:`.QobjEvo` are also accepted. - sc_ops : list of (:class:`QobjEvo`, :class:`QobjEvo` compatible format) + sc_ops : list of (:obj:`.QobjEvo`, :obj:`.QobjEvo` compatible format) List of stochastic collapse operators. - heterodyne : bool, [False] + heterodyne : bool, default: False Whether to use heterodyne or homodyne detection. - options : dict, [optional] + options : dict, optional Options for the solver, see :obj:`SMESolver.options` and `SIntegrator <./classes.html#classes-sode>`_ for a list of all options. """ @@ -701,14 +799,14 @@ class SMESolver(StochasticSolver): "progress_kwargs": {"chunk_size": 10}, "store_final_state": False, "store_states": None, - "store_measurement": False, "keep_runs_results": False, "normalize_output": False, - "method": "taylor1.5", "map": "serial", - "job_timeout": None, + "mpi_options": {}, "num_cpus": None, "bitgenerator": None, + "method": "platen", + "store_measurement": False, } @@ -718,22 +816,22 @@ class SSESolver(StochasticSolver): Parameters ---------- - H : :class:`Qobj`, :class:`QobjEvo`, :class:`QobjEvo` compatible format. + H : :obj:`.Qobj`, :obj:`.QobjEvo`, :obj:`.QobjEvo` compatible format. System Hamiltonian as a Qobj or QobjEvo for time-dependent - Hamiltonians. List of [:class:`Qobj`, :class:`Coefficient`] or callable - that can be made into :class:`QobjEvo` are also accepted. + Hamiltonians. List of [:obj:`.Qobj`, :obj:`.Coefficient`] or callable + that can be made into :obj:`.QobjEvo` are also accepted. - c_ops : list of (:class:`QobjEvo`, :class:`QobjEvo` compatible format) + c_ops : list of (:obj:`.QobjEvo`, :obj:`.QobjEvo` compatible format) Deterministic collapse operator which will contribute with a standard Lindblad type of dissipation. - sc_ops : list of (:class:`QobjEvo`, :class:`QobjEvo` compatible format) + sc_ops : list of (:obj:`.QobjEvo`, :obj:`.QobjEvo` compatible format) List of stochastic collapse operators. - heterodyne : bool, [False] + heterodyne : bool, default: False Whether to use heterodyne or homodyne detection. - options : dict, [optional] + options : dict, optional Options for the solver, see :obj:`SSESolver.options` and `SIntegrator <./classes.html#classes-sode>`_ for a list of all options. """ @@ -745,12 +843,12 @@ class SSESolver(StochasticSolver): "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", - "job_timeout": None, + "mpi_options": {}, "num_cpus": None, "bitgenerator": None, + "method": "platen", + "store_measurement": False, } diff --git a/qutip/tests/conftest.py b/qutip/tests/conftest.py index 2ac625ee47..17fbccf055 100644 --- a/qutip/tests/conftest.py +++ b/qutip/tests/conftest.py @@ -100,7 +100,7 @@ def _patched_build_err_msg(arrays, err_msg, header='Items are not equal:', except Exception as exc: r = '[repr failed for <{}>: {}]'.format(type(a).__name__, exc) # [diff] The original truncates the output to 3 lines here. - msg.append(' %s: %s' % (names[i], r)) + msg.append(f' {names[i]}: {r}') return '\n'.join(msg) diff --git a/qutip/tests/core/data/test_linalg.py b/qutip/tests/core/data/test_linalg.py index 56a1bb2b6b..b257ea0e00 100644 --- a/qutip/tests/core/data/test_linalg.py +++ b/qutip/tests/core/data/test_linalg.py @@ -99,7 +99,7 @@ def test_incorrect_shape_mismatch(self): class TestSVD(): def op_numpy(self, A): - return np.linalg.svd(A) + return scipy.linalg.svd(A) def _gen_dm(self, N, rank, dtype): return qutip.rand_dm(N, rank=rank, dtype=dtype).data diff --git a/qutip/tests/core/test_dimensions.py b/qutip/tests/core/test_dimensions.py index a32c302941..7be9d27600 100644 --- a/qutip/tests/core/test_dimensions.py +++ b/qutip/tests/core/test_dimensions.py @@ -4,47 +4,11 @@ import collections import qutip from qutip.core.dimensions import ( - type_from_dims, flatten, unflatten, enumerate_flat, deep_remove, deep_map, + flatten, unflatten, enumerate_flat, deep_remove, deep_map, dims_idxs_to_tensor_idxs, dims_to_tensor_shape, dims_to_tensor_perm, - collapse_dims_super, collapse_dims_oper, + collapse_dims_super, collapse_dims_oper, Dimensions ) -_v = "vector" -_vo = "vectorized_oper" - - -@pytest.mark.parametrize(["rank", "actual_type", "scalar"], [ - pytest.param([1], _v, True, id="scalar"), - pytest.param([1, 1], _v, True, id="tensor scalar"), - pytest.param([[1]], _vo, True, id="nested scalar"), - pytest.param([[1], [1]], _vo, True, id="nested tensor scalar 1"), - pytest.param([[1, 1]], _vo, True, id="nested tensor scalar 2"), - pytest.param([2], _v, False, id="vector"), - pytest.param([2, 3], _v, False, id="tensor vector"), - pytest.param([1, 2, 3], _v, False, id="tensor vector with 1d subspace 1"), - pytest.param([2, 1, 1], _v, False, id="tensor vector with 1d subspace 2"), - pytest.param([[2]], _vo, False, id="vectorised operator"), - pytest.param([[2, 3]], _vo, False, id="vector tensor operator"), - pytest.param([[1, 3]], _vo, False, id="vector tensor operator with 1d"), -]) -@pytest.mark.parametrize("test_type", ["scalar", _v, _vo]) -def test_rank_type_detection(rank, actual_type, scalar, test_type): - """ - Test the rank detection tests `is_scalar`, `is_vector` and - `is_vectorized_oper` for a range of different test cases. These tests are - designed to be called on individual elements of the two-element `dims` - parameter of `Qobj`s, so they're testing the row-rank and column-rank. - - It's possible to be both a scalar and something else, but "vector" and - "vectorized_oper" are mutually exclusive. - - These functions aren't properly specified for improper dimension setups, so - there are no tests for those. - """ - expected = scalar if test_type == "scalar" else (actual_type == test_type) - function = getattr(qutip.dimensions, "is_" + test_type) - assert function(rank) == expected - @pytest.mark.parametrize(["base", "flat"], [ pytest.param([[[0], 1], 2], [0, 1, 2], id="standard"), @@ -116,23 +80,23 @@ def test_deep_map(base, mapping): @pytest.mark.parametrize("indices", [ pytest.param(_Indices([[2], [1]], [0, 1], (2, 1)), id="ket preserved"), - pytest.param(_Indices([[2, 3], [1, 1]], [0, 1, 2, 3], (2, 3, 1, 1)), + pytest.param(_Indices([[2, 3], [1]], [0, 1, 2], (2, 3, 1)), id="tensor-ket preserved"), pytest.param(_Indices([[1], [2]], [0, 1], (1, 2)), id="bra preserved"), - pytest.param(_Indices([[1, 1], [2, 2]], [0, 1, 2, 3], (1, 1, 2, 2)), + pytest.param(_Indices([[1], [2, 2]], [0, 1, 2], (1, 2, 2)), id="tensor-bra preserved"), pytest.param(_Indices([[2], [3]], [0, 1], (2, 3)), id="oper preserved"), - pytest.param(_Indices([[2, 3], [1, 0]], [0, 1, 2, 3], (2, 3, 1, 0)), + pytest.param(_Indices([[2, 3], [1, 2]], [0, 1, 2, 3], (2, 3, 1, 2)), id="tensor-oper preserved"), pytest.param(_Indices([[[2, 4], [6, 8]], [[1, 3], [5, 7]]], [2, 3, 0, 1, 6, 7, 4, 5], (6, 8, 2, 4, 5, 7, 1, 3)), id="super-oper"), pytest.param(_Indices([[[2, 4], [6, 8]], [1]], - [0, 1, 2, 3, 4], (2, 4, 6, 8, 1)), + [2, 3, 0, 1, 4], (6, 8, 2, 4, 1)), id="operator-ket"), pytest.param(_Indices([[1], [[2, 4], [6, 8]]], - [0, 1, 2, 3, 4], (1, 2, 4, 6, 8)), + [0, 3, 4, 1, 2], (1, 6, 8, 2, 4)), id="operator-bra"), ]) class TestSuperOperatorDimsModification: @@ -151,30 +115,18 @@ def test_dims_to_tensor_shape(self, indices): class TestTypeFromDims: - @pytest.mark.parametrize(["base", "expected", "enforce_square"], [ - pytest.param([[2], [2]], 'oper', True), - pytest.param([[2, 3], [2, 3]], 'oper', True), - pytest.param([[2], [3]], 'other', True), - pytest.param([[2], [3]], 'oper', False), - pytest.param([[2], [1]], 'ket', True), - pytest.param([[1], [2]], 'bra', True), - pytest.param([[[2, 3], [2, 3]], [1]], 'operator-ket', True), - pytest.param([[1], [[2, 3], [2, 3]]], 'operator-bra', True), - pytest.param([[[3], [3]], [[2, 3], [2, 3]]], 'other', True), - pytest.param([[[3], [3]], [[2, 3], [2, 3]]], 'super', False), - pytest.param([[[2], [3, 3]], [[3], [2, 3]]], 'other', True), - ]) - def test_type_from_dims(self, base, expected, enforce_square): - assert type_from_dims(base, enforce_square=enforce_square) == expected - - @pytest.mark.parametrize("qobj", [ - pytest.param(qutip.rand_ket(10), id='ket'), - pytest.param(qutip.rand_ket(10).dag(), id='bra'), - pytest.param(qutip.rand_dm(10), id='oper'), - pytest.param(qutip.to_super(qutip.rand_dm(10)), id='super'), + @pytest.mark.parametrize(["base", "expected"], [ + pytest.param([[2], [2]], 'oper'), + pytest.param([[2, 3], [2, 3]], 'oper'), + pytest.param([[2], [3]], 'oper'), + pytest.param([[2], [1]], 'ket'), + pytest.param([[1], [2]], 'bra'), + pytest.param([[[2, 3], [2, 3]], [1]], 'operator-ket'), + pytest.param([[1], [[2, 3], [2, 3]]], 'operator-bra'), + pytest.param([[[3], [3]], [[2, 3], [2, 3]]], 'super'), ]) - def test_qobj_dims_match_qobj(self, qobj): - assert type_from_dims(qobj.dims) == qobj.type + def test_Dimensions_type(self, base, expected): + assert Dimensions(base).type == expected class TestCollapseDims: @@ -199,3 +151,41 @@ def test_oper(self, base, expected): ]) def test_super(self, base, expected): assert collapse_dims_super(base) == expected + + +@pytest.mark.parametrize("dims_list", [ + pytest.param([0], id="zero"), + pytest.param([], id="empty"), + pytest.param([1, [2]], id="mixed depth"), + pytest.param([[2], [3], [4]], id="bay type"), +]) +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] diff --git a/qutip/tests/core/test_metrics.py b/qutip/tests/core/test_metrics.py index e52a074b32..575cafdc97 100644 --- a/qutip/tests/core/test_metrics.py +++ b/qutip/tests/core/test_metrics.py @@ -286,7 +286,7 @@ def had_mixture(x): def swap_map(x): base = (1j * x * swap()).expm() dims = [[[2], [2]], [[2], [2]]] - return Qobj(base, dims=dims, type='super', superrep='super') + return Qobj(base, dims=dims, superrep='super') def adc_choi(x): diff --git a/qutip/tests/core/test_operators.py b/qutip/tests/core/test_operators.py index 5a887280cf..712194934f 100644 --- a/qutip/tests/core/test_operators.py +++ b/qutip/tests/core/test_operators.py @@ -107,7 +107,7 @@ def test_diagonal_operators(oper_func, diag, offset, args): @pytest.mark.parametrize(['function', 'message'], [ - (qutip.qeye, "All dimensions must be integers >= 0"), + (qutip.qeye, "Dimensions must be integers > 0"), (qutip.destroy, "Hilbert space dimension must be integer value"), (qutip.create, "Hilbert space dimension must be integer value"), ], ids=["qeye", "destroy", "create"]) @@ -125,14 +125,24 @@ def test_diagonal_raise(function, message): 1, [1], [1, 1], + qutip.dimensions.Space([2, 3, 4]), ]) def test_implicit_tensor_creation(to_test, dimensions): implicit = to_test(dimensions) if isinstance(dimensions, numbers.Integral): dimensions = [dimensions] + if isinstance(dimensions, qutip.dimensions.Space): + dimensions = dimensions.as_list() assert implicit.dims == [dimensions, dimensions] +def test_qzero_rectangular(): + assert qutip.qzero([2, 3], [3, 4]).dims == [[2, 3], [3, 4]] + assert qutip.qzero([2], [3]).dims == [[2], [3]] + assert qutip.qzero([2, 3], [3]).dims == [[2, 3], [3]] + assert qutip.qzero(qutip.dimensions.Space([2, 3]), qutip.dimensions.Space([3])).dims == [[2, 3], [3]] + + @pytest.mark.parametrize("to_test", [qutip.qzero, qutip.qeye, qutip.identity]) def test_super_operator_creation(to_test): size = 2 @@ -298,7 +308,7 @@ def test_qft(dims): @pytest.mark.parametrize('N', [1, 3, 5, 8]) -@pytest.mark.parametrize('M', [1, 3, 5, 8]) +@pytest.mark.parametrize('M', [2, 3, 5, 8]) def test_swap(N, M): ket1 = qutip.rand_ket(N) ket2 = qutip.rand_ket(M) @@ -320,10 +330,19 @@ 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) + + +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"], [ @@ -340,10 +359,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]) @@ -365,3 +386,15 @@ def test_fcreate_fdestroy(n_sites): assert qutip.commutator(c_0, d_1, 'anti') == zero_tensor assert qutip.commutator(c_1, d_0, 'anti') == zero_tensor assert qutip.commutator(identity, c_0) == zero_tensor + +@pytest.mark.parametrize(['func', 'args'], [ + (qutip.qzero, (None,)), + (qutip.fock, (None,)), + (qutip.fock_dm, (None,)), + (qutip.maximally_mixed_dm, ()), + (qutip.projection, ([0, 1, 1], [1, 1, 0])), + (qutip.zero_ket, ()), +], ids=_id_func) +def test_state_space_input(func, args): + dims = qutip.dimensions.Space([2, 2, 2]) + assert func(dims, *args) == func([2, 2, 2], *args) diff --git a/qutip/tests/core/test_ptrace.py b/qutip/tests/core/test_ptrace.py index 40efd15109..86d610e507 100644 --- a/qutip/tests/core/test_ptrace.py +++ b/qutip/tests/core/test_ptrace.py @@ -13,6 +13,8 @@ def expected(qobj, sel): sel = sorted(sel) dims = [[x for i, x in enumerate(qobj.dims[0]) if i in sel]]*2 new_shape = (np.prod(dims[0], dtype=int),) * 2 + if not dims[0]: + dims = None out = qobj.full() before, after = 1, qobj.shape[0] for i, dim in enumerate(qobj.dims[0]): @@ -22,7 +24,7 @@ def expected(qobj, sel): continue tmp_dims = (before, dim, after) * 2 out = np.einsum('aibcid->abcd', out.reshape(tmp_dims)) - return qutip.Qobj(out.reshape(new_shape), dims=dims, type='oper') + return qutip.Qobj(out.reshape(new_shape), dims=dims) @pytest.fixture(params=[_data.CSR, _data.Dense], ids=['CSR', 'Dense']) diff --git a/qutip/tests/core/test_qobj.py b/qutip/tests/core/test_qobj.py index 5badd1e88e..7aa7dc0b3b 100644 --- a/qutip/tests/core/test_qobj.py +++ b/qutip/tests/core/test_qobj.py @@ -107,7 +107,7 @@ def test_QobjType(): N = 9 super_data = np.random.random((N, N)) - super_qobj = qutip.Qobj(super_data, dims=[[[3]], [[3]]]) + super_qobj = qutip.Qobj(super_data, dims=[[[3], [3]], [[3], [3]]]) assert super_qobj.type == 'super' assert super_qobj.issuper assert super_qobj.superrep == 'super' @@ -118,7 +118,7 @@ def test_QobjType(): assert super_qobj.isoperket assert super_qobj.superrep == 'super' - super_data = np.random.random(N) + super_data = np.random.random((1, N)) super_qobj = qutip.Qobj(super_data, dims=[[[1]], [[3], [3]]]) assert super_qobj.type == 'operator-bra' assert super_qobj.isoperbra @@ -260,11 +260,8 @@ def test_QobjAddition(): q3 = qutip.Qobj(data3) q4 = q1 + q2 - q4_type = q4.type q4_isherm = q4.isherm - q4._type = None q4._isherm = None # clear cached values - assert q4_type == q4.type assert q4_isherm == q4.isherm # check elementwise addition/subtraction @@ -1265,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 77fe8fba93..6fb6211d64 100644 --- a/qutip/tests/core/test_qobjevo.py +++ b/qutip/tests/core/test_qobjevo.py @@ -1,9 +1,12 @@ import operator import pytest -from qutip import (Qobj, QobjEvo, coefficient, qeye, sigmax, sigmaz, - rand_stochastic, rand_herm, rand_ket, liouvillian, - basis, spre, spost, to_choi) +from qutip import ( + Qobj, QobjEvo, coefficient, qeye, sigmax, sigmaz, num, rand_stochastic, + 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 @@ -47,6 +50,10 @@ def __call__(self, t, args={}): def __getitem__(self, which): return getattr(self, which)() + @property + def _dims(self): + return self.qobj._dims + N = 3 args = {'w1': 1, "w2": 2} @@ -95,7 +102,9 @@ def pseudo_qevo(request): @pytest.fixture def all_qevo(pseudo_qevo, coeff_type): - return QobjEvo(*pseudo_qevo[coeff_type]) + base, args, *tlist = pseudo_qevo[coeff_type] + if tlist: tlist = tlist[0] + return QobjEvo(base, args, tlist=tlist) @pytest.fixture @@ -104,6 +113,8 @@ def other_qevo(all_qevo): def _assert_qobjevo_equivalent(obj1, obj2, tol=1e-8): + assert obj1._dims == obj1(0)._dims + assert obj2._dims == obj2(0)._dims for t in TESTTIMES: _assert_qobj_almost_eq(obj1(t), obj2(t), tol) @@ -125,7 +136,9 @@ def _div(a, b): def test_call(pseudo_qevo, coeff_type): # test creation of QobjEvo and call - qevo = QobjEvo(*pseudo_qevo[coeff_type]) + base, args, *tlist = pseudo_qevo[coeff_type] + if tlist: tlist = tlist[0] + qevo = QobjEvo(base, args, tlist=tlist) assert isinstance(qevo(0), Qobj) assert qevo.isoper assert not qevo.isconstant @@ -285,8 +298,26 @@ def test_unary(all_qevo, unary_op): "QobjEvo arithmetic" obj = all_qevo for t in TESTTIMES: - as_qevo = unary_op(obj)(t) + transformed = unary_op(obj) + as_qevo = transformed(t) as_qobj = unary_op(obj(t)) + assert transformed._dims == as_qevo._dims + _assert_qobj_almost_eq(as_qevo, as_qobj) + + +@pytest.mark.parametrize('unary_op', [ + pytest.param(lambda a: a.conj(), id="conj"), + pytest.param(lambda a: a.dag(), id="dag"), + pytest.param(lambda a: a.trans(), id="trans"), + pytest.param(lambda a: -a, id="neg"), +]) +def test_unary_ket(unary_op): + obj = QobjEvo(rand_ket(5)) + for t in TESTTIMES: + transformed = unary_op(obj) + as_qevo = transformed(t) + as_qobj = unary_op(obj(t)) + assert transformed._dims == as_qevo._dims _assert_qobj_almost_eq(as_qevo, as_qobj) @@ -508,8 +539,103 @@ def test_QobjEvo_isherm_flag_knowcase(): ['func_coeff', 'string', 'array', 'logarray'] ) def test_QobjEvo_to_list(coeff_type, pseudo_qevo): - qevo = QobjEvo(*pseudo_qevo[coeff_type]) + base, args, *tlist = pseudo_qevo[coeff_type] + if tlist: tlist = tlist[0] + qevo = QobjEvo(base, args, tlist=tlist) as_list = qevo.to_list() assert len(as_list) == 2 restored = QobjEvo(as_list) _assert_qobjevo_equivalent(qevo, restored) + + +class Feedback_Checker_Coefficient: + def __init__(self, stacked=True): + self.state = None + self.stacked = stacked + + def __call__(self, t, data=None, qobj=None, e_val=None): + if self.state is not None: + if data is not None and self.stacked: + assert data == operator_to_vector(self.state).data + elif data is not None: + assert data == self.state.data + if qobj is not None: + assert qobj == self.state + if e_val is not None: + expected = expect(qeye(self.state.dims[0]), self.state) + assert e_val == pytest.approx(expected, abs=1e-7) + return 1. + + +def test_feedback_oper(): + checker = Feedback_Checker_Coefficient(stacked=False) + checker.state = basis(2, 1) + qevo = QobjEvo( + [qeye(2), checker], + args={ + "e_val": SESolver.ExpectFeedback(qeye(2), default=1.), + "data": SESolver.StateFeedback(default=checker.state.data, + raw_data=True), + "qobj": SESolver.StateFeedback(default=checker.state), + }, + ) + + checker.state = rand_ket(2) + qevo.expect(0, checker.state) + checker.state = rand_ket(2) + qevo.expect(0, checker.state) + + checker.state = rand_ket(2) + qevo.matmul_data(0, checker.state.data) + checker.state = rand_ket(2) + qevo.matmul_data(0, checker.state.data) + + +def test_feedback_super(): + checker = Feedback_Checker_Coefficient() + qevo = QobjEvo( + [spre(qeye(2)), checker], + args={ + "e_val": MESolver.ExpectFeedback(qeye(2)), + "data": MESolver.StateFeedback(raw_data=True), + "qobj": MESolver.StateFeedback(), + }, + ) + + checker.state = rand_dm(2) + qevo.expect(0, operator_to_vector(checker.state)) + qevo.matmul_data(0, operator_to_vector(checker.state).data) + + qevo.arguments(e_val=MESolver.ExpectFeedback(spre(qeye(2)))) + + checker.state = rand_dm(2) + qevo.expect(0, operator_to_vector(checker.state)) + qevo.matmul_data(0, operator_to_vector(checker.state).data) + + checker = Feedback_Checker_Coefficient(stacked=False) + qevo = QobjEvo( + [spre(qeye(2)), checker], + args={ + "data": MESolver.StateFeedback(raw_data=True, prop=True), + "qobj": MESolver.StateFeedback(prop=True), + }, + ) + + 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 diff --git a/qutip/tests/core/test_states.py b/qutip/tests/core/test_states.py index be17941dec..6168ddb76c 100644 --- a/qutip/tests/core/test_states.py +++ b/qutip/tests/core/test_states.py @@ -25,12 +25,12 @@ def test_implicit_tensor_basis_like(to_test, size, n): @pytest.mark.parametrize("size, n, offset, msg", [ ([2, 2], [0, 1, 1], [0, 0], "All list inputs must be the same length."), - ([2, 2], [0, 1], 0, "All list inputs must be the same length."), - (-1, 0, 0, "All dimensions must be >= 0."), - (1.5, 0, 0, "All dimensions must be >= 0."), - (5, 5, 0, "All basis indices must be `offset <= n < dimension+offset`."), - (5, 0, 2, "All basis indices must be `offset <= n < dimension+offset`."), -], ids=["n too long", "offset too long", "neg dims", + ([2, 2], [1, 1], 1, "All list inputs must be the same length."), + (-1, 0, 0, "Dimensions must be integers > 0"), + (1.5, 0, 0, "Dimensions must be integers > 0"), + (5, 5, 0, "All basis indices must be integers in the range `0 <= n < dimension`."), + (5, 0, 2, "All basis indices must be integers in the range `offset <= n < dimension+offset`."), +], ids=["n too long", "offset too short", "neg dims", "fraction dims", "n too large", "n too small"] ) def test_basis_error(size, n, offset, msg): @@ -42,8 +42,7 @@ def test_basis_error(size, n, offset, msg): def test_basis_error_type(): with pytest.raises(TypeError) as e: qutip.basis(5, 3.5) - assert str(e.value) == ("Dimensions must be an integer " - "or list of integers.") + assert str(e.value) == ("Dimensions must be integers") @pytest.mark.parametrize("size, n, m", [ @@ -183,7 +182,7 @@ def test_spin_output(func): def test_maximally_mixed_dm_error(N): with pytest.raises(ValueError) as e: qutip.maximally_mixed_dm(N) - assert str(e.value) == "N must be integer N > 0" + assert str(e.value) == "Dimensions must be integers > 0" def test_TripletStateNorm(): @@ -316,3 +315,16 @@ def test_state_type(func, args, alias, dtype): else: for obj in object: assert isinstance(obj.data, dtype) + + +@pytest.mark.parametrize(['func', 'args'], [ + (qutip.basis, (None,)), + (qutip.fock, (None,)), + (qutip.fock_dm, (None,)), + (qutip.maximally_mixed_dm, ()), + (qutip.projection, ([0, 1, 1], [1, 1, 0])), + (qutip.zero_ket, ()), +], ids=_id_func) +def test_state_space_input(func, args): + dims = qutip.dimensions.Space([2, 2, 2]) + assert func(dims, *args) == func([2, 2, 2], *args) \ No newline at end of file diff --git a/qutip/tests/core/test_superop_reps.py b/qutip/tests/core/test_superop_reps.py index b1e1f1b89e..80da2bd821 100644 --- a/qutip/tests/core/test_superop_reps.py +++ b/qutip/tests/core/test_superop_reps.py @@ -185,10 +185,10 @@ def test_random_iscptp(self, superoperator): + to_super(tensor(qeye(2), sigmay()))), True, True, True, id="linear combination of bipartite unitaries"), - pytest.param(Qobj(swap(), type='super', superrep='choi'), + pytest.param(Qobj(swap(), dims=[[[2],[2]]]*2, superrep='choi'), True, False, True, id="partial transpose map"), - pytest.param(Qobj(qeye(4)*0.9, type='super'), True, True, False, + pytest.param(Qobj(qeye(4)*0.9, dims=[[[2],[2]]]*2), True, True, False, id="subnormalized map"), pytest.param(basis(2, 0), False, False, False, id="ket"), ]) @@ -223,10 +223,11 @@ def test_choi_tr(self): ) / 2 # The partial transpose map, whose Choi matrix is SWAP - ptr_swap = Qobj(swap(), type='super', superrep='choi') + ptr_swap = Qobj(swap(), dims=[[[2], [2]]]*2, superrep='choi') # Subnormalized maps (representing erasure channels, for instance) - subnorm_map = Qobj(identity(4) * 0.9, type='super', superrep='super') + subnorm_map = Qobj(identity(4) * 0.9, dims=[[[2], [2]]]*2, + superrep='super') @pytest.mark.parametrize(['qobj', 'shouldhp', 'shouldcp', 'shouldtp'], [ pytest.param(S, True, True, False, id="conjugatio by create op"), diff --git a/qutip/tests/core/test_superoper.py b/qutip/tests/core/test_superoper.py index b5ba6fd105..80e1b3de57 100644 --- a/qutip/tests/core/test_superoper.py +++ b/qutip/tests/core/test_superoper.py @@ -43,7 +43,7 @@ def testsuperrep(self): with pytest.raises(TypeError) as err: bad_vec = as_vec.copy() - bad_vec.superrep = "" + bad_vec.superrep = "bad" qutip.vector_to_operator(bad_vec) assert err.value.args[0] == ("only defined for operator-kets " "in super format") @@ -156,7 +156,7 @@ def test_reshuffle(self): U = qutip.tensor(U1, U2, U3) S = qutip.to_super(U) S_col = qutip.reshuffle(S) - assert S_col.dims[0] == [[2, 2], [3, 3], [4, 4]] + assert S_col.dims[0] == [[2], [2], [3], [3], [4], [4]] assert qutip.reshuffle(S_col) == S def test_sprepost(self): diff --git a/qutip/tests/piqs/test_piqs.py b/qutip/tests/piqs/test_piqs.py index 54e17b91bd..2397d318b9 100644 --- a/qutip/tests/piqs/test_piqs.py +++ b/qutip/tests/piqs/test_piqs.py @@ -854,7 +854,7 @@ def test_dicke_basis(self): assert_equal(test_dicke_basis, true_dicke_basis) # error - assert_raises(AttributeError, dicke_basis, N) + assert_raises(AttributeError, dicke_basis, N, None) def test_dicke(self): """ @@ -1111,7 +1111,7 @@ def test_liouvillian(self): true_L.dims = [[[2], [2]], [[2], [2]]] true_H = [[1.0 + 0.0j, 1.0 + 0.0j], [1.0 + 0.0j, -1.0 + 0.0j]] true_H = Qobj(true_H) - true_H.dims = [[[2], [2]]] + true_H.dims = [[2], [2]] true_liouvillian = [ [-4, -1.0j, 1.0j, 3], [-1.0j, -3.54999995 + 2.0j, 0, 1.0j], diff --git a/qutip/tests/solver/heom/test_bofin_solvers.py b/qutip/tests/solver/heom/test_bofin_solvers.py index ad6e5f2297..b704b015db 100644 --- a/qutip/tests/solver/heom/test_bofin_solvers.py +++ b/qutip/tests/solver/heom/test_bofin_solvers.py @@ -8,8 +8,8 @@ from scipy.integrate import quad from qutip import ( - basis, destroy, expect, liouvillian, qeye, sigmax, sigmaz, - tensor, Qobj, QobjEvo + basis, destroy, expect, liouvillian, qeye, sigmax, sigmaz, sigmay, + tensor, Qobj, QobjEvo, fidelity ) from qutip.core import data as _data from qutip.solver.heom.bofin_baths import ( @@ -741,6 +741,24 @@ def test_pure_dephasing_model_bosonic_bath( else: assert_raises_steady_state_time_dependent(hsolver) + def test_steady_state_bosonic_bath( + self, atol=1e-3 + ): + H_sys = 0.25 * sigmaz() + 0.5 * sigmay() + + bath = DrudeLorentzBath(sigmaz(), lam=0.025, + gamma=0.05, T=1/0.95, Nk=2) + options = {"nsteps": 15000, "store_states": True} + hsolver = HEOMSolver(H_sys, bath, 5, options=options) + + tlist = np.linspace(0, 500, 21) + rho0 = basis(2, 0) * basis(2, 0).dag() + + result = hsolver.run(rho0, tlist) + rho_final, ado_state = hsolver.steady_state() + fid = fidelity(rho_final, result.states[-1]) + np.testing.assert_allclose(fid, 1.0, atol=atol) + @pytest.mark.parametrize(['terminator'], [ pytest.param(True, id="terminator"), pytest.param(False, id="noterminator"), diff --git a/qutip/tests/solver/test_brmesolve.py b/qutip/tests/solver/test_brmesolve.py index 7b1aef0663..a8420fa27e 100644 --- a/qutip/tests/solver/test_brmesolve.py +++ b/qutip/tests/solver/test_brmesolve.py @@ -294,3 +294,21 @@ def test_hamiltonian_taking_arguments(): args = brmesolve([[H, 'ii']], psi0, times, a_ops, e_ops, args=args) for arg, no_arg in zip(args.expect, no_args.expect): np.testing.assert_allclose(arg, no_arg, atol=1e-10) + + +def test_feedback(): + N = 10 + tol = 1e-4 + psi0 = qutip.basis(N, 7) + a = qutip.destroy(N) + H = qutip.QobjEvo(qutip.num(N)) + a_op = ( + qutip.QobjEvo(a + a.dag()), + qutip.coefficient("(A.real - 4)*(w > 0)", args={"A": 7.+0j, "w": 0.}) + ) + solver = qutip.BRSolver(H, [a_op]) + result = solver.run( + psi0, np.linspace(0, 3, 31), e_ops=[qutip.num(N)], + args={"A": qutip.BRSolver.ExpectFeedback(qutip.num(N))} + ) + assert np.all(result.expect[0] > 4. - tol) diff --git a/qutip/tests/solver/test_floquet.py b/qutip/tests/solver/test_floquet.py index 679a3cba80..3c990d0c0f 100644 --- a/qutip/tests/solver/test_floquet.py +++ b/qutip/tests/solver/test_floquet.py @@ -51,7 +51,7 @@ def testFloquetBasis(self): states = sesolve(H, psi0, tlist).states for t, state in zip(tlist, states): from_floquet = floquet_basis.from_floquet_basis(floquet_psi0, t) - assert state.overlap(from_floquet) == pytest.approx(1., abs=5e-5) + assert state.overlap(from_floquet) == pytest.approx(1., abs=8e-5) def testFloquetUnitary(self): N = 10 diff --git a/qutip/tests/solver/test_mcsolve.py b/qutip/tests/solver/test_mcsolve.py index 7a14089a29..717fc74fc1 100644 --- a/qutip/tests/solver/test_mcsolve.py +++ b/qutip/tests/solver/test_mcsolve.py @@ -411,7 +411,7 @@ def test_super_H(improved_sampling): target_tol=0.1, options={'map': 'serial', "improved_sampling": improved_sampling}) - np.testing.assert_allclose(mc_expected.expect[0], mc.expect[0], atol=0.5) + np.testing.assert_allclose(mc_expected.expect[0], mc.expect[0], atol=0.65) def test_MCSolver_run(): @@ -451,20 +451,30 @@ def test_MCSolver_stepping(): assert state.isket -# Defined in module-scope so it's pickleable. -def _dynamic(t, args): - return 0 if args["collapse"] else 1 - - -@pytest.mark.xfail(reason="current limitation of McSolve") -def test_dynamic_arguments(): - """Test dynamically updated arguments are usable.""" - size = 5 - a = qutip.destroy(size) - H = qutip.num(size) - times = np.linspace(0, 1, 11) - state = qutip.basis(size, 2) - - c_ops = [[a, _dynamic], [a.dag(), _dynamic]] - mc = mcsolve(H, state, times, c_ops, ntraj=25, args={"collapse": []}) - assert all(len(collapses) <= 1 for collapses in mc.col_which) +@pytest.mark.parametrize(["func", "kind"], [ + pytest.param( + lambda t, A: A-4, + lambda: qutip.MCSolver.ExpectFeedback(qutip.num(10)), + # 7.+0j, + id="expect" + ), + pytest.param( + lambda t, A: (len(A) < 3) * 1.0, + lambda: qutip.MCSolver.CollapseFeedback(), + id="collapse" + ), +]) +def test_feedback(func, kind): + tol = 1e-6 + psi0 = qutip.basis(10, 7) + a = qutip.destroy(10) + H = qutip.QobjEvo(qutip.num(10)) + solver = qutip.MCSolver( + H, + c_ops=[qutip.QobjEvo([a, func], args={"A": kind()})], + options={"map": "serial"} + ) + result = solver.run( + psi0,np.linspace(0, 3, 31), e_ops=[qutip.num(10)], ntraj=10 + ) + assert np.all(result.expect[0] > 4. - tol) diff --git a/qutip/tests/solver/test_mesolve.py b/qutip/tests/solver/test_mesolve.py index 7a72a02f64..5a79d59dda 100644 --- a/qutip/tests/solver/test_mesolve.py +++ b/qutip/tests/solver/test_mesolve.py @@ -680,3 +680,21 @@ def test_mesolve_bad_state(): def test_mesolve_bad_options(): with pytest.raises(TypeError): MESolver(qutip.qeye(4), [], options=False) + + +def test_feedback(): + + def f(t, A): + return (A-4.) + + N = 10 + tol = 1e-14 + psi0 = qutip.basis(N, 7) + a = qutip.QobjEvo( + [qutip.destroy(N), f], + args={"A": MESolver.ExpectFeedback(qutip.spre(qutip.num(N)))} + ) + H = qutip.QobjEvo(qutip.num(N)) + solver = qutip.MESolver(H, c_ops=[a]) + result = solver.run(psi0, np.linspace(0, 30, 301), e_ops=[qutip.num(N)]) + assert np.all(result.expect[0] > 4. - tol) diff --git a/qutip/tests/solver/test_parallel.py b/qutip/tests/solver/test_parallel.py index bb53496edb..a666ab8456 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,12 +30,13 @@ 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} map_kw = { - 'job_timeout': threading.TIMEOUT_MAX, 'timeout': threading.TIMEOUT_MAX, 'num_cpus': num_cpus, } @@ -49,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', @@ -56,11 +59,13 @@ 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 = { - 'job_timeout': threading.TIMEOUT_MAX, 'timeout': threading.TIMEOUT_MAX, 'num_cpus': num_cpus, } @@ -86,28 +91,40 @@ 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): + kwargs = {} if map is loky_pmap: - loky = pytest.importorskip("loky") + 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) @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): + map_kw = {"fail_fast": False} if map is loky_pmap: - loky = pytest.importorskip("loky") + 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(): @@ -124,11 +141,17 @@ 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): + kwargs = {} if map is loky_pmap: - loky = pytest.importorskip("loky") + 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 = [] @@ -136,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 diff --git a/qutip/tests/solver/test_sesolve.py b/qutip/tests/solver/test_sesolve.py index 12e0e23754..cfa5ee86ea 100644 --- a/qutip/tests/solver/test_sesolve.py +++ b/qutip/tests/solver/test_sesolve.py @@ -304,3 +304,21 @@ def test_krylovsolve(always_compute_step): options = {"always_compute_step", always_compute_step} krylov_sol = krylovsolve(H, psi0, tlist, 20, e_ops=[e_op]).expect[0] np.testing.assert_allclose(ref, krylov_sol) + + +def test_feedback(): + + def f(t, A, qobj=None): + return (A-2.) + + N = 4 + tol = 1e-14 + psi0 = qutip.basis(N, N-1) + a = qutip.destroy(N) + H = qutip.QobjEvo( + [qutip.num(N), [a+a.dag(), f]], + args={"A": SESolver.ExpectFeedback(qutip.num(N), default=3.)} + ) + solver = qutip.SESolver(H) + result = solver.run(psi0, np.linspace(0, 30, 301), e_ops=[qutip.num(N)]) + assert np.all(result.expect[0] > 2 - tol) diff --git a/qutip/tests/solver/test_sode_method.py b/qutip/tests/solver/test_sode_method.py index 075dbe9a13..356a2ce70a 100644 --- a/qutip/tests/solver/test_sode_method.py +++ b/qutip/tests/solver/test_sode_method.py @@ -100,8 +100,8 @@ def get_error_order_integrator(integrator, ref_integrator, state, plot=False): # state = rand_ket(system.dims[0]).data err = np.zeros(len(ts), dtype=float) for i, t in enumerate(ts): - integrator.options["dt"] = 0.1 - ref_integrator.options["dt"] = 0.1 + integrator.options["dt"] = t + ref_integrator.options["dt"] = t integrator.set_state(0., state, np.random.default_rng(0)) ref_integrator.set_state(0., state, np.random.default_rng(0)) out = integrator.integrate(t)[1] diff --git a/qutip/tests/solver/test_steadystate.py b/qutip/tests/solver/test_steadystate.py index 6ae02a6489..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'], [ @@ -29,10 +31,19 @@ pytest.param('iterative-bicgstab', {'atol': 1e-12, "tol": 1e-10}, id="iterative-bicgstab"), ]) -def test_qubit(method, kwargs): +@pytest.mark.parametrize("dtype", ["dense", "dia", "csr"]) +@pytest.mark.filterwarnings("ignore:Only CSR matrices:RuntimeWarning") +def test_qubit(method, kwargs, dtype): # thermal steadystate of a qubit: compare numerics with analytical formula - sz = qutip.sigmaz() - sm = qutip.destroy(2) + 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 @@ -92,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 @@ -128,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 diff --git a/qutip/tests/solver/test_stochastic.py b/qutip/tests/solver/test_stochastic.py index f6843c997e..dc04b966f1 100644 --- a/qutip/tests/solver/test_stochastic.py +++ b/qutip/tests/solver/test_stochastic.py @@ -316,3 +316,64 @@ def test_m_ops(heterodyne): noise = res.expect[0][1:] - res.measurement[0][0] assert np.mean(noise) == pytest.approx(0., abs=std/50**0.5 * 4) assert np.std(noise) == pytest.approx(std, abs=std/50**0.5 * 4) + + +def test_feedback(): + tol = 0.05 + N = 10 + ntraj = 2 + + def func(t, A, W): + return (A - 6) * (A.real > 6.) * W(t)[0] + + H = num(10) + sc_ops = [QobjEvo( + [destroy(N), func], + args={ + "A": SMESolver.ExpectFeedback(num(10)), + "W": SMESolver.WeinerFeedback() + } + )] + psi0 = basis(N, N-3) + + times = np.linspace(0, 10, 101) + options = {"map": "serial", "dt": 0.001} + + solver = SMESolver(H, sc_ops=sc_ops, heterodyne=False, options=options) + results = solver.run(psi0, times, e_ops=[num(N)], ntraj=ntraj) + + assert np.all(results.expect[0] > 6.-1e-6) + assert np.all(results.expect[0][-20:] < 6.7) + + +def test_deprecation_warnings(): + with pytest.warns(FutureWarning, match=r'map_func'): + 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, 0.01], [qeye(2)], progress_bar=None) + + with pytest.warns(FutureWarning, match=r'nsubsteps'): + 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, 0.01], [qeye(2)], map_func=None) + + with pytest.warns(FutureWarning, match=r'store_all_expect'): + 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, 0.01], [qeye(2)], store_measurement=1) + + 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) + + +@pytest.mark.parametrize("method", ["euler", "rouchon"]) +def test_small_step_warnings(method): + with pytest.warns(RuntimeWarning, match=r'under minimum'): + smesolve( + qeye(2), basis(2), [0, 0.0000001], [qeye(2)], + options={"method": method} + ) 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_bloch.py b/qutip/tests/test_bloch.py index 33e8fb2fae..c1faafdf3d 100644 --- a/qutip/tests/test_bloch.py +++ b/qutip/tests/test_bloch.py @@ -213,6 +213,7 @@ def plot_point_ref(self, fig, point_kws): np.ceil(points.shape[1]/len(point_colors) ).astype(int)) colors = colors[:points.shape[1]] + colors = list(colors) point_size = point_sizes[idx % len(point_sizes)] point_marker = point_markers[idx % len(point_markers)] point_alpha = kw.get("alpha", 1.0) @@ -469,6 +470,23 @@ def test_vector_errors_color_length(self, vectors, colors): "size as the number of vectors. ") assert str(err.value) == err_msg + @check_pngs_equal + def test_clear(self, fig_test=None, fig_ref=None): + b = Bloch(fig=fig_test) + b.add_vectors([0, 1, 0]) + b.add_points([1, 0, 0]) + b.vector_color = ["g"] + b.point_color = ["g"] + b.clear() + b.add_vectors([0, 0, 1]) + b.add_points([0, 0, -1]) + b.make_sphere() + + b2 = Bloch(fig=fig_ref) + b2.add_vectors([0, 0, 1]) + b2.add_points([0, 0, -1]) + b2.make_sphere() + def test_repr_svg(): pytest.importorskip("matplotlib") diff --git a/qutip/tests/test_enr_state_operator.py b/qutip/tests/test_enr_state_operator.py index 9002c3759b..45a4a2226f 100644 --- a/qutip/tests/test_enr_state_operator.py +++ b/qutip/tests/test_enr_state_operator.py @@ -44,7 +44,7 @@ def test_no_restrictions(self, dimensions): iden = [qutip.qeye(n) for n in dimensions] for i, test in enumerate(test_operators): expected = qutip.tensor(iden[:i] + [a[i]] + iden[i+1:]) - assert test == expected + assert test.data == expected.data assert test.dims == [dimensions, dimensions] def test_space_size_reduction(self, dimensions, n_excitations): @@ -82,7 +82,7 @@ def test_fock_state(dimensions, n_excitations): def test_fock_state_error(): with pytest.raises(ValueError) as e: state = qutip.enr_fock([2, 2, 2], 1, [1, 1, 1]) - assert str(e.value).startswith("The state tuple ") + assert str(e.value).startswith("state tuple ") def _reference_dm(dimensions, n_excitations, nbars): 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_measurement.py b/qutip/tests/test_measurement.py index 6bccd1a35c..44b57ce28f 100644 --- a/qutip/tests/test_measurement.py +++ b/qutip/tests/test_measurement.py @@ -3,8 +3,8 @@ import pytest from math import sqrt from qutip import ( - Qobj, basis, ket2dm, sigmax, sigmay, sigmaz, identity, tensor, - rand_ket, + Qobj, basis, ket2dm, sigmax, sigmay, sigmaz, identity, num, tensor, + rand_ket ) from qutip.measurement import ( measure_povm, measurement_statistics_povm, measure_observable, @@ -70,8 +70,6 @@ def _equivalent(left, right, tol=1e-8): @pytest.mark.parametrize(["op", "state", "pairs", "probabilities"], [ - pytest.param(sigmaz(), basis(2, 0), SIGMAZ, [0, 1], id="sigmaz_ket"), - pytest.param(sigmaz(), basis(2, 0).proj(), SIGMAZ, [0, 1], id="sigmaz_dm"), pytest.param(sigmax(), basis(2, 0), SIGMAX, [0.5, 0.5], id="sigmax_ket"), pytest.param(sigmax(), basis(2, 0).proj(), SIGMAX, [0.5, 0.5], id="sigmax_dm"), @@ -82,19 +80,24 @@ def _equivalent(left, right, tol=1e-8): def test_measurement_statistics_observable(op, state, pairs, probabilities): """ measurement_statistics_observable: observables on basis states. """ - evs, ess_or_projs, probs = measurement_statistics_observable(state, op) - np.testing.assert_almost_equal(evs, pairs.eigenvalues) - if state.isket: - ess = ess_or_projs - assert len(ess) == len(pairs.eigenstates) - for a, b in zip(ess, pairs.eigenstates): - assert _equivalent(a, b) - else: - projs = ess_or_projs - assert len(projs) == len(pairs.projectors) - for a, b in zip(projs, pairs.projectors): - assert _equivalent(a, b) + evs, projs, probs = measurement_statistics_observable(state, op) + assert len(projs) == len(probabilities) np.testing.assert_almost_equal(probs, probabilities) + for a, b in zip(projs, pairs.projectors): + assert _equivalent(a, b) + + +def test_measurement_statistics_observable_degenerate(): + """ measurement_statistics_observable: observables on basis states. """ + + state = basis(2, 1) & (basis(2, 0) + basis(2, 1)).unit() + op = sigmaz() & identity(2) + expected_projector = num(2) & identity(2) + evs, projs, probs = measurement_statistics_observable(state, op) + assert len(probs) == 1 + np.testing.assert_almost_equal(probs, [1.]) + np.testing.assert_almost_equal(evs, [-1.]) + assert _equivalent(projs[0], expected_projector) @pytest.mark.parametrize(["ops", "state", "final_states", "probabilities"], [ diff --git a/qutip/tests/test_random.py b/qutip/tests/test_random.py index 13f9554bbf..085e940448 100644 --- a/qutip/tests/test_random.py +++ b/qutip/tests/test_random.py @@ -202,7 +202,10 @@ def test_rand_ket(dimensions, distribution, dtype): """ random_qobj = rand_ket(dimensions, distribution=distribution, dtype=dtype) - assert random_qobj.type == 'ket' + target_type = "ket" + if isinstance(dimensions, list) and isinstance(dimensions[0], list): + target_type = "operator-ket" + assert random_qobj.type == target_type assert abs(random_qobj.norm() - 1) < 1e-14 if isinstance(dimensions, int): @@ -219,7 +222,7 @@ def test_rand_super(dimensions, dtype, superrep): """ random_qobj = rand_super(dimensions, dtype=dtype, superrep=superrep) assert random_qobj.issuper - with CoreOptions(atol=1e-9): + with CoreOptions(atol=2e-9): assert random_qobj.iscptp assert random_qobj.superrep == superrep _assert_metadata(random_qobj, dimensions, dtype, super=True) @@ -281,10 +284,16 @@ def test_random_seeds(function, seed): def test_kraus_map(dimensions, dtype): - kmap = rand_kraus_map(dimensions, dtype=dtype) - _assert_metadata(kmap[0], dimensions, dtype) - with CoreOptions(atol=1e-9): - assert kraus_to_choi(kmap).iscptp + if isinstance(dimensions, list) and isinstance(dimensions[0], list): + # Each element of a kraus map cannot be a super operators + with pytest.raises(TypeError) as err: + kmap = rand_kraus_map(dimensions, dtype=dtype) + assert "super operator" in str(err.value) + else: + kmap = rand_kraus_map(dimensions, dtype=dtype) + _assert_metadata(kmap[0], dimensions, dtype) + with CoreOptions(atol=1e-9): + assert kraus_to_choi(kmap).iscptp dtype_names = list(_data.to._str2type.keys()) + list(_data.to.dtypes) diff --git a/qutip/tests/test_simdiag.py b/qutip/tests/test_simdiag.py index a45076fd60..16fc6682e2 100644 --- a/qutip/tests/test_simdiag.py +++ b/qutip/tests/test_simdiag.py @@ -73,6 +73,26 @@ def test_simdiag_degen_large(): atol=1e-13 ) +def test_simdiag_orthonormal_eigenvectors(): + + # Special matrix that used to be problematic (see Issue #2268) + a = np.array([[1, 0, 1, -1, 0], + [0, 4, 0, 0, 1], + [1, 0, 4, 1, 0], + [-1, 0, 1, 4, 0], + [0, 1, 0, 0, 4]]) + + _, evecs = qutip.simdiag([qutip.Qobj(a), qutip.qeye(5)]) + evecs = np.array([evec.full() for evec in evecs]).squeeze() + + # Check that eigenvectors form an othonormal basis + # (<=> matrix of eigenvectors is unitary) + np.testing.assert_allclose( + evecs@evecs.conj().T, + np.eye(len(evecs)), + atol=1e-13 + ) + def test_simdiag_no_input(): with pytest.raises(ValueError): diff --git a/qutip/tests/test_visualization.py b/qutip/tests/test_visualization.py index b4c2a7e365..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 @@ -85,6 +85,7 @@ def test_set_ticklabels(): with pytest.raises(Exception) as exc_info: fig, ax = qutip.hinton(rho, x_basis=[1]) assert str(exc_info.value) == text + plt.close() def test_equal_shape(): @@ -94,6 +95,7 @@ def test_equal_shape(): with pytest.raises(Exception) as exc_info: fig, ax = qutip.hinton(rhos) assert str(exc_info.value) == text + plt.close() @pytest.mark.parametrize('args', [ @@ -178,6 +180,7 @@ def test_hinton_ValueError0(): with pytest.raises(ValueError) as exc_info: fig, ax = qutip.hinton(rho) assert str(exc_info.value) == text + plt.close() @pytest.mark.parametrize('transform, args, error_message', [ @@ -192,6 +195,7 @@ def test_hinton_ValueError1(transform, args, error_message): with pytest.raises(ValueError) as exc_info: fig, ax = qutip.hinton(rho, **args) assert str(exc_info.value) == error_message + plt.close() @pytest.mark.parametrize('args', [ @@ -241,6 +245,7 @@ def test_update_yaxis(response): y_basis=[1]) assert str(exc_info.value) == text + plt.close() @pytest.mark.parametrize('response', [ @@ -261,6 +266,7 @@ def test_update_xaxis(response): fig, ax = qutip.matrix_histogram(qutip.rand_dm(5), x_basis=[1]) assert str(exc_info.value) == text + plt.close() def test_get_matrix_components(): @@ -346,6 +352,7 @@ def test_matrix_histogram_ValueError(args, expected): fig, ax = qutip.matrix_histogram(qutip.rand_dm(5), **args) assert str(exc_info.value) in expected + plt.close() @pytest.mark.parametrize('args', [ @@ -368,6 +375,7 @@ def test_plot_energy_levels_ValueError(): with pytest.raises(ValueError) as exc_info: fig, ax = qutip.plot_energy_levels(1) assert str(exc_info.value) == "H_list must be a list of Qobj instances" + plt.close() @pytest.mark.parametrize('rho_type, args', [ @@ -440,6 +448,7 @@ def test_plot_wigner_ValueError(): fig, ax = qutip.plot_wigner(rho, projection=1) assert str(exc_info.value) == text + plt.close() @pytest.mark.parametrize('n_of_results, n_of_e_ops, one_axes, args', [ @@ -532,6 +541,7 @@ def test_plot_spin_distribution_ValueError(): with pytest.raises(ValueError) as exc_info: fig, ax = qutip.plot_spin_distribution(Q, THETA, PHI, projection=1) assert str(exc_info.value) == text + plt.close() @pytest.mark.parametrize('args', [ @@ -595,6 +605,7 @@ def test_plot_qubism_Error(ket, args, expected): with pytest.raises(Exception) as exc_info: fig, ax = qutip.plot_qubism(state, **args) assert str(exc_info.value) == expected + plt.close() def test_plot_qubism_dimension(): @@ -605,6 +616,7 @@ def test_plot_qubism_dimension(): with pytest.raises(Exception) as exc_info: qutip.plot_qubism(ket, how='pairs_skewed') assert str(exc_info.value) == text + plt.close() @pytest.mark.parametrize('args', [ @@ -638,3 +650,4 @@ def test_plot_schmidt_Error(): with pytest.raises(Exception) as exc_info: fig, ax = qutip.plot_schmidt(state) assert str(exc_info.value) == text + plt.close() diff --git a/qutip/tomography.py b/qutip/tomography.py index ebff6a0c68..185c4145a9 100644 --- a/qutip/tomography.py +++ b/qutip/tomography.py @@ -41,11 +41,11 @@ def qpt_plot(chi, lbls_list, title=None, fig=None, axes=None): Input QPT chi matrix. lbls_list : list List of labels for QPT plot axes. - title : string + title : str, optional Plot title. - fig : figure instance + fig : figure instance, optional User defined figure instance used for generating QPT plot. - axes : list of figure axis instance + axes : list of figure axis instance, optional User defined figure axis instance (list of two axes) used for generating QPT plot. @@ -99,17 +99,20 @@ def qpt_plot_combined(chi, lbls_list, title=None, lbls_list : list List of labels for QPT plot axes. - title : string + title : str, optional Plot title. - fig : figure instance + fig : figure instance, optional User defined figure instance used for generating QPT plot. - ax : figure axis instance + figsize : (int, int), default: (8, 6) + Size of the figure when the ``fig`` is not provided. + + ax : figure axis instance, optional User defined figure axis instance used for generating QPT plot (alternative to the fig argument). - threshold: float (None) + threshold: float, optional Threshold for when bars of smaller height should be transparent. If not set, all bars are colored according to the color map. diff --git a/qutip/utilities.py b/qutip/utilities.py index 8d6383071a..2032b4be30 100644 --- a/qutip/utilities.py +++ b/qutip/utilities.py @@ -16,17 +16,17 @@ def n_thermal(w, w_th): Parameters ---------- - w : *float* or *array* + w : float or ndarray Frequency of the oscillator. - w_th : *float* + w_th : float The temperature in units of frequency (or the same units as `w`). Returns ------- - n_avg : *float* or *array* + n_avg : float or array Return the number of average photons in thermal equilibrium for a an oscillator with the given frequency and temperature. @@ -150,11 +150,11 @@ def convert_unit(value, orig="meV", to="GHz"): value : float / array The energy in the old unit. - orig : string - The name of the original unit ("J", "eV", "meV", "GHz", "mK") + orig : str, {"J", "eV", "meV", "GHz", "mK"}, default: "meV" + The name of the original unit. - to : string - The name of the new unit ("J", "eV", "meV", "GHz", "mK") + to : str, {"J", "eV", "meV", "GHz", "mK"}, default: "GHz" + The name of the new unit. Returns ------- diff --git a/qutip/visualization.py b/qutip/visualization.py index 32cb6e401c..b5f10f7dcf 100644 --- a/qutip/visualization.py +++ b/qutip/visualization.py @@ -12,7 +12,7 @@ import warnings import itertools as it import numpy as np -from numpy import pi, array, sin, cos, angle, log2 +from numpy import pi, array, sin, cos, angle, log2, sqrt from packaging.version import parse as parse_version @@ -119,13 +119,13 @@ def plot_wigner_sphere(wigner, reflections=False, *, cmap=None, wigner : a wigner transformation The wigner transformation at `steps` different theta and phi. - reflections : bool, default=False + reflections : bool, default: False If the reflections of the sphere should be plotted as well. cmap : a matplotlib colormap instance, optional Color map to use when plotting. - colorbar : bool, default=True + colorbar : bool, default: True Whether (True) or not (False) a colorbar should be attached. fig : a matplotlib Figure instance, optional @@ -296,7 +296,7 @@ def hinton(rho, x_basis=None, y_basis=None, color_style="scaled", y_basis : list of strings, optional list of y ticklabels to represent y basis of the input. - color_style : string, default="scaled" + color_style : str, {"scaled", "threshold", "phase"}, default: "scaled" Determines how colors are assigned to each square: @@ -310,14 +310,14 @@ def hinton(rho, x_basis=None, y_basis=None, color_style="scaled", - If set to ``"phase"``, each color is chosen according to the angle of the corresponding matrix element. - label_top : bool, default=True + label_top : bool, default: True If True, x ticklabels will be placed on top, otherwise they will appear below the plot. cmap : a matplotlib colormap instance, optional Color map to use when plotting. - colorbar : bool, default=True + colorbar : bool, default: True Whether (True) or not (False) a colorbar should be attached. fig : a matplotlib Figure instance, optional @@ -340,16 +340,16 @@ def hinton(rho, x_basis=None, y_basis=None, color_style="scaled", Examples -------- >>> import qutip - >>> + >>> dm = qutip.rand_dm(4) >>> fig, ax = qutip.hinton(dm) >>> fig.show() - >>> + >>> qutip.settings.colorblind_safe = True >>> fig, ax = qutip.hinton(dm, color_style="threshold") >>> fig.show() >>> qutip.settings.colorblind_safe = False - >>> + >>> fig, ax = qutip.hinton(dm, color_style="phase") >>> fig.show() """ @@ -502,7 +502,7 @@ def sphereplot(values, theta, phi, *, cmap : a matplotlib colormap instance, optional Color map to use when plotting. - colorbar : bool, default=True + colorbar : bool, default: True Whether (True) or not (False) a colorbar should be attached. fig : a matplotlib Figure instance, optional @@ -691,7 +691,7 @@ def matrix_histogram(M, x_basis=None, y_basis=None, limits=None, limits : list/array with two float numbers, optional The z-axis limits [min, max] - bar_style : string, default="real" + bar_style : str, {"real", "img", "abs", "phase"}, default: "real" - If set to ``"real"`` (default), each bar is plotted as the real part of the corresponding matrix element @@ -705,7 +705,7 @@ def matrix_histogram(M, x_basis=None, y_basis=None, limits=None, color_limits : list/array with two float numbers, optional The limits of colorbar [min, max] - color_style : string, default="real" + color_style : str, {"real", "img", "abs", "phase"}, default: "real" Determines how colors are assigned to each square: - If set to ``"real"`` (default), each color is chosen @@ -720,7 +720,7 @@ def matrix_histogram(M, x_basis=None, y_basis=None, limits=None, cmap : a matplotlib colormap instance, optional Color map to use when plotting. - colorbar : bool, default=True + colorbar : bool, default: True show colorbar fig : a matplotlib Figure instance, optional @@ -737,40 +737,40 @@ def matrix_histogram(M, x_basis=None, y_basis=None, limits=None, 'zticks' : list of numbers, optional A list of z-axis tick locations. - 'bars_spacing' : float, default=0.1 + 'bars_spacing' : float, default: 0.1 spacing between bars. - 'bars_alpha' : float, default=1. + 'bars_alpha' : float, default: 1. transparency of bars, should be in range 0 - 1 - 'bars_lw' : float, default=0.5 + 'bars_lw' : float, default: 0.5 linewidth of bars' edges. - 'bars_edgecolor' : color, default='k' + 'bars_edgecolor' : color, default: 'k' The colors of the bars' edges. Examples: 'k', (0.1, 0.2, 0.5) or '#0f0f0f80'. - 'shade' : bool, default=True + 'shade' : bool, default: True Whether to shade the dark sides of the bars (True) or not (False). The shading is relative to plot's source of light. - 'azim' : float, default=-35 + 'azim' : float, default: -35 The azimuthal viewing angle. - 'elev' : float, default=35 + 'elev' : float, default: 35 The elevation viewing angle. - 'stick' : bool, default=False + 'stick' : bool, default: False Changes xlim and ylim in such a way that bars next to XZ and YZ planes will stick to those planes. This option has no effect if ``ax`` is passed as a parameter. - 'cbar_pad' : float, default=0.04 + 'cbar_pad' : float, default: 0.04 The fraction of the original axes between the colorbar and the new image axes. (i.e. the padding between the 3D figure and the colorbar). - 'cbar_to_z' : bool, default=False + 'cbar_to_z' : bool, default: False Whether to set the color of maximum and minimum z-values to the maximum and minimum colors in the colorbar (True) or not (False). @@ -977,7 +977,7 @@ def plot_energy_levels(H_list, h_labels=None, energy_levels=None, N=0, *, A list of yticklabels to the left of energy levels of the initial Hamiltonian. - N : int, default=0 + N : int, default: 0 The number of energy levels to plot fig : a matplotlib Figure instance, optional @@ -1062,16 +1062,16 @@ def plot_fock_distribution(rho, fock_numbers=None, color="green", Parameters ---------- - rho : `qutip.Qobj` + rho : :obj:`.Qobj` The density matrix (or ket) of the state to visualize. fock_numbers : list of strings, optional list of x ticklabels to represent fock numbers - color : color or list of colors, default="green" + color : color or list of colors, default: "green" The colors of the bar faces. - unit_y_range : bool, default=True + unit_y_range : bool, default: True Set y-axis limits [0, 1] or not fig : a matplotlib Figure instance, optional @@ -1126,16 +1126,16 @@ def plot_fock_distribution(rho, fock_numbers=None, color="green", return fig, output -def plot_wigner(rho, xvec=None, yvec=None, method='clenshaw', - projection='2d', *, cmap=None, colorbar=False, - fig=None, ax=None): +def plot_wigner(rho, xvec=None, yvec=None, method='clenshaw', projection='2d', + g=sqrt(2), sparse=False, parfor=False, *, + cmap=None, colorbar=False, fig=None, ax=None): """ Plot the the Wigner function for a density matrix (or ket) that describes an oscillator mode. Parameters ---------- - rho : `qutip.Qobj` + rho : :obj:`.Qobj` The density matrix (or ket) of the state to visualize. xvec : array_like, optional @@ -1145,19 +1145,30 @@ def plot_wigner(rho, xvec=None, yvec=None, method='clenshaw', y-coordinates at which to calculate the Wigner function. Does not apply to the 'fft' method. - method : string {'clenshaw', 'iterative', 'laguerre', 'fft'}, - default='clenshaw' + method : str {'clenshaw', 'iterative', 'laguerre', 'fft'}, default: 'clenshaw' The method used for calculating the wigner function. See the documentation for qutip.wigner for details. - projection: string {'2d', '3d'}, default='2d' + projection: str {'2d', '3d'}, default: '2d' Specify whether the Wigner function is to be plotted as a contour graph ('2d') or surface plot ('3d'). + g : float + Scaling factor for `a = 0.5 * g * (x + iy)`, default `g = sqrt(2)`. + See the documentation for qutip.wigner for details. + + sparse : bool {False, True} + Flag for sparse format. + See the documentation for qutip.wigner for details. + + parfor : bool {False, True} + Flag for parallel calculation. + See the documentation for qutip.wigner for details. + cmap : a matplotlib cmap instance, optional The colormap. - colorbar : bool, default=False + colorbar : bool, default: False Whether (True) or not (False) a colorbar should be attached to the Wigner function graph. @@ -1194,7 +1205,10 @@ def plot_wigner(rho, xvec=None, yvec=None, method='clenshaw', if isket(rho): rho = ket2dm(rho) - W0 = wigner(rho, xvec, yvec, method=method) + W0 = wigner( + rho, xvec, yvec, method=method, + g=g, sparse=sparse, parfor=parfor + ) W, yvec = W0 if isinstance(W0, tuple) else (W0, yvec) Ws.append(W) @@ -1209,8 +1223,11 @@ def plot_wigner(rho, xvec=None, yvec=None, method='clenshaw', artist_list = list() for W in Ws: if projection == '2d': - cf = ax.contourf(xvec, yvec, W, 100, norm=norm, - cmap=cmap).collections + if parse_version(mpl.__version__) >= parse_version('3.8'): + cf = [ax.contourf(xvec, yvec, W, 100, norm=norm, cmap=cmap)] + else: + cf = ax.contourf(xvec, yvec, W, 100, norm=norm, + cmap=cmap).collections else: X, Y = np.meshgrid(xvec, yvec) cf = [ax.plot_surface(X, Y, W, rstride=5, cstride=5, linewidth=0.5, @@ -1243,9 +1260,10 @@ def plot_expectation_values(results, ylabels=None, *, Visualize the results (expectation values) for an evolution solver. `results` is assumed to be an instance of Result, or a list of Result instances. + Parameters ---------- - results : (list of) :class:`qutip.solver.Result` + results : (list of) :class:`.Result` List of results objects returned by any of the QuTiP evolution solvers. ylabels : list of strings, optional @@ -1309,7 +1327,7 @@ def plot_spin_distribution(P, THETA, PHI, projection='2d', *, PHI : matrix Meshgrid matrix for the phi coordinate. Its range is between 0 and 2*pi - projection: string {'2d', '3d'}, default='2d' + projection: str {'2d', '3d'}, default: '2d' Specify whether the spin distribution function is to be plotted as a 2D projection where the surface of the unit sphere is mapped on the unit disk ('2d') or surface plot ('3d'). @@ -1317,7 +1335,7 @@ def plot_spin_distribution(P, THETA, PHI, projection='2d', *, cmap : a matplotlib cmap instance, optional The colormap. - colorbar : bool, default=False + colorbar : bool, default: False Whether (True) or not (False) a colorbar should be attached to the Wigner function graph. @@ -1416,7 +1434,7 @@ def complex_array_to_rgb(X, theme='light', rmax=None): X : array Array (of any dimension) of complex numbers. - theme : 'light' or 'dark', default='light' + theme : str {'light', 'dark'}, default: 'light' Set coloring theme for mapping complex values into colors. rmax : float, optional @@ -1587,22 +1605,22 @@ def plot_qubism(ket, theme='light', how='pairs', grid_iteration=1, ket : Qobj Pure state for plotting. - theme : 'light' or 'dark', default='light' + theme : str {'light', 'dark'}, default: 'light' Set coloring theme for mapping complex values into colors. See: complex_array_to_rgb. - how : 'pairs', 'pairs_skewed' or 'before_after', default='pairs' + how : str {'pairs', 'pairs_skewed' or 'before_after'}, default: 'pairs' Type of Qubism plotting. Options: - 'pairs' - typical coordinates, - 'pairs_skewed' - for ferromagnetic/antriferromagnetic plots, - 'before_after' - related to Schmidt plot (see also: plot_schmidt). - grid_iteration : int, default=1 + grid_iteration : int, default: 1 Helper lines to be drawn on plot. Show tiles for 2*grid_iteration particles vs all others. - legend_iteration : int or 'grid_iteration' or 'all', default=0 + legend_iteration : int or 'grid_iteration' or 'all', default: 0 Show labels for first ``2*legend_iteration`` particles. Option 'grid_iteration' sets the same number of particles as for grid_iteration. Option 'all' makes label for all particles. Typically @@ -1771,7 +1789,7 @@ def plot_schmidt(ket, theme='light', splitting=None, ket : Qobj Pure state for plotting. - theme : 'light' or 'dark', default='light' + theme : str {'light', 'dark'}, default: 'light' Set coloring theme for mapping complex values into colors. See: complex_array_to_rgb. @@ -1779,7 +1797,7 @@ def plot_schmidt(ket, theme='light', splitting=None, Plot for a number of first particles versus the rest. If not given, it is (number of particles + 1) // 2. - labels_iteration : int or pair of ints, default=(3,2) + labels_iteration : int or pair of ints, default: (3, 2) Number of particles to be shown as tick labels, for first (vertical) and last (horizontal) particles, respectively. diff --git a/qutip/wigner.py b/qutip/wigner.py index 48abd66385..dceeffbb87 100644 --- a/qutip/wigner.py +++ b/qutip/wigner.py @@ -161,7 +161,7 @@ def _angle_slice(slicearray, theta, phi): return theta, phi -def wigner(psi, xvec, yvec, method='clenshaw', g=sqrt(2), +def wigner(psi, xvec, yvec=None, method='clenshaw', g=sqrt(2), sparse=False, parfor=False): """Wigner function for a state vector or density matrix at points `xvec + i * yvec`. @@ -179,13 +179,13 @@ def wigner(psi, xvec, yvec, method='clenshaw', g=sqrt(2), y-coordinates at which to calculate the Wigner function. Does not apply to the 'fft' method. - g : float + g : float, default: sqrt(2) Scaling factor for `a = 0.5 * g * (x + iy)`, default `g = sqrt(2)`. The value of `g` is related to the value of `hbar` in the commutation relation `[x, y] = i * hbar` via `hbar=2/g^2` giving the default value `hbar=1`. - method : string {'clenshaw', 'iterative', 'laguerre', 'fft'} + method : string {'clenshaw', 'iterative', 'laguerre', 'fft'}, default: 'clenshaw' Select method 'clenshaw' 'iterative', 'laguerre', or 'fft', where 'clenshaw' and 'iterative' use an iterative method to evaluate the Wigner functions for density matrices :math:`|m>~50). 'clenshaw' is a fast and numerically stable method. - sparse : bool {False, True} + sparse : bool, optional Tells the default solver whether or not to keep the input density matrix in sparse format. As the dimensions of the density matrix grow, setthing this flag can result in increased performance. - parfor : bool {False, True} + parfor : bool, optional Flag for calculating the Laguerre polynomial based Wigner function method='laguerre' in parallel using the parfor function. @@ -657,13 +657,13 @@ class QFunc: xvec, yvec : array_like x- and y-coordinates at which to calculate the Husimi-Q function. - g : float, default sqrt(2) + g : float, default: sqrt(2) Scaling factor for ``a = 0.5 * g * (x + iy)``. The value of `g` is related to the value of `hbar` in the commutation relation :math:`[x,\,y] = i\hbar` via :math:`\hbar=2/g^2`, so the default corresponds to :math:`\hbar=1`. - memory : real, default 1024 + memory : real, default: 1024 Size in MB that may be used internally as workspace. This class will raise ``MemoryError`` if subsequently passed a state of sufficiently large dimension that this bound would be exceeded. In those cases, use @@ -687,7 +687,7 @@ class QFunc: See Also -------- :obj:`.qfunc` : - a single function version, which will involve computing several + A single function version, which will involve computing several quantities multiple times in order to use less memory. """ @@ -791,13 +791,13 @@ def qfunc( xvec, yvec : array_like x- and y-coordinates at which to calculate the Husimi-Q function. - g : float, default sqrt(2) + g : float, default: sqrt(2) Scaling factor for ``a = 0.5 * g * (x + iy)``. The value of `g` is related to the value of :math:`\hbar` in the commutation relation :math:`[x,\,y] = i\hbar` via :math:`\hbar=2/g^2`, so the default corresponds to :math:`\hbar=1`. - precompute_memory : real, default 1024 + precompute_memory : real, default: 1024 Size in MB that may be used during calculations as working space when dealing with density-matrix inputs. This is ignored for state-vector inputs. The bound is not quite exact due to other, order-of-magnitude 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 1eb1f11cd2..90c93f7b07 100644 --- a/setup.cfg +++ b/setup.cfg @@ -12,7 +12,7 @@ project_urls = Documentation = https://qutip.org/docs/latest/ Source Code = https://github.com/qutip/qutip classifiers = - Development Status :: 2 - Pre-Alpha + Development Status :: 4 - Beta Intended Audience :: Science/Research License :: OSI Approved :: BSD License Programming Language :: Python @@ -31,13 +31,14 @@ packages = find: include_package_data = True zip_safe = False install_requires = - numpy>=1.16.6 - scipy>=1.0 + numpy>=1.22 + scipy>=1.8,<1.12 packaging setup_requires = - numpy>=1.13.3 - scipy>=1.0 - cython>=0.29.20 + 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' packaging [options.packages.find] @@ -46,7 +47,8 @@ include = qutip* [options.extras_require] graphics = matplotlib>=1.2.1 runtime_compilation = - cython>=0.29.20 + cython>=0.29.20; python_version>='3.10' + cython>=0.29.20,<3.0.3; python_version<='3.9' filelock semidefinite = cvxpy>=1.0 @@ -59,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 =