diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 997e3890c2..45554412b8 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -6,6 +6,7 @@ Thank you for contributing to QuTiP! Please make sure you have finished the foll You can use [pycodestyle](http://pycodestyle.pycqa.org/en/latest/index.html) to check your code automatically - [ ] Please add tests to cover your changes if applicable. - [ ] If the behavior of the code has changed or new feature has been added, please also update the documentation in the `doc` folder, and the [notebook](https://github.com/qutip/qutip-notebooks). Feel free to ask if you are not sure. +- [ ] Include the changelog in a file named: `doc/changes/.` 'type' can be one of the following: feature, bugfix, doc, removal, misc, or deprecation (see [here](http://qutip.org/docs/latest/development/contributing.html#Changelog%20Generation) for more information). Delete this checklist after you have completed all the tasks. If you have not finished them all, you can also open a [Draft Pull Request](https://github.blog/2019-02-14-introducing-draft-pull-requests/) to let the others know this on-going work and keep this checklist in the PR description. @@ -13,10 +14,4 @@ Delete this checklist after you have completed all the tasks. If you have not fi Describe here the proposed change. **Related issues or PRs** -Please mention the related issues or PRs here. If the PR fixes an issue, use the keyword fix/fixes/fixed followed by the issue id, e.g. fix #1184 - -**Changelog** -Give a short description of the PR in a few words. This will be shown in the QuTiP change log after the PR gets merged. -For example: -Fixed error checking for null matrix in essolve. -Added option for specifying resolution in Bloch.save function. +Please mention the related issues or PRs here. If the PR fixes an issue, use the keyword fix/fixes/fixed followed by the issue id, e.g. fix #1184 \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9e7cc60530..45b648fa35 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,14 +12,15 @@ on: jobs: - # The deploy_test job is part of the test of whether we should deploy to PyPI. - # The job will succeed if either the confirmation reference is empty or if the - # confirmation is the selected branch or tag name. It will fail if it is - # nonempty and does not match. All later jobs depend on this job, so that - # they will be immediately cancelled if the confirmation is bad. The - # dependency is currently necessary (2021-03) because GitHub Actions does not - # have a simpler method of cancelling an entire workflow---the normal use-case - # expects to try and run as much as possible despite one or two failures. + # The deploy_test job is part of the test of whether we should deploy to PyPI + # or test.PyPI. The job will succeed if either the confirmation reference is + # empty, 'test' or if the confirmation is the selected branch or tag name. + # It will fail if it is nonempty and does not match. All later jobs depend + # on this job, so that they will be immediately cancelled if the confirmation + # is bad. The dependency is currently necessary (2021-03) because GitHub + # Actions does not have a simpler method of cancelling an entire workflow--- + # the normal use-case expects to try and run as much as possible despite one + # or two failures. deploy_test: name: Verify PyPI deployment confirmation runs-on: ubuntu-latest @@ -30,8 +31,10 @@ jobs: - name: Compare confirmation to current reference shell: bash run: | - [[ -z $CONFIRM_REF || $GITHUB_REF =~ ^refs/(heads|tags)/$CONFIRM_REF$ ]] - if [[ -z $CONFIRM_REF ]]; then + [[ -z $CONFIRM_REF || $GITHUB_REF =~ ^refs/(heads|tags)/$CONFIRM_REF$ || $CONFIRM_REF == "test" ]] + if [[ $CONFIRM_REF == "test" ]]; then + echo "Build and deploy to test.pypi.org." + elif [[ -z $CONFIRM_REF ]]; then echo "Build only. Nothing will be uploaded to PyPI." else echo "Full build and deploy. Wheels and source will be uploaded to PyPI." @@ -46,15 +49,15 @@ jobs: OVERRIDE_VERSION: ${{ github.event.inputs.override_version }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - - uses: actions/setup-python@v2 + - uses: actions/setup-python@v4 name: Install Python with: # 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.6' + python-version: '3.8' - name: Install pip build run: | @@ -87,7 +90,7 @@ jobs: zip "$zipfile" -r "$stem" rm -r "$stem" - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v3 with: name: sdist path: | @@ -104,27 +107,27 @@ jobs: matrix: os: [ubuntu-latest, windows-latest, macos-latest] env: - # Set up wheels matrix. This is CPython 3.6--3.10 for all OS targets. - CIBW_BUILD: "cp3{6,7,8,9,10}-*" - # Numpy and SciPy do not supply wheels for i686 for Python 3.10, so we - # skip those too: - CIBW_SKIP: "*-musllinux* cp310-manylinux_i686 cp310-win32" + # Set up wheels matrix. This is CPython 3.8--3.10 for all OS targets. + CIBW_BUILD: "cp3{8,9,10,11}-*" + # 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" OVERRIDE_VERSION: ${{ github.event.inputs.override_version }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - - uses: actions/setup-python@v2 + - uses: actions/setup-python@v4 name: Install Python with: # This is about the build environment, not the released wheel version. - python-version: '3.7' + python-version: '3.8' - name: Install cibuildwheel run: | # cibuildwheel does the heavy lifting for us. Originally tested on - # 2.3.0, but should be fine at least up to any minor new release. - python -m pip install 'cibuildwheel==2.3.*' + # 2.11.3, but should be fine at least up to any minor new release. + python -m pip install 'cibuildwheel==2.11.*' - name: Build wheels shell: bash @@ -134,7 +137,7 @@ jobs: if [[ ! -z "$OVERRIDE_VERSION" ]]; then echo "$OVERRIDE_VERSION" > VERSION; fi python -m cibuildwheel --output-dir wheelhouse - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v3 with: name: wheels path: ./wheelhouse/*.whl @@ -143,10 +146,10 @@ jobs: deploy: name: "Deploy to PyPI if desired" # The confirmation is tested explicitly in `deploy_test`, so we know it is - # either a missing confirmation (so we shouldn't run this job) or a valid - # confirmation. We don't need to retest the value of the confirmation, - # beyond checking that one existed. - if: ${{ github.event.inputs.confirm_ref != '' }} + # either a missing confirmation (so we shouldn't run this job), 'test' or a + # valid confirmation. We don't need to retest the value of the + # confirmation, beyond checking that one existed. + if: ${{ github.event.inputs.confirm_ref != '' && github.event.inputs.confirm_ref != 'test' }} needs: [deploy_test, build_sdist, build_wheels] runs-on: ubuntu-latest env: @@ -157,17 +160,17 @@ jobs: steps: - name: Download build artifacts to local runner - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 - - uses: actions/setup-python@v2 + - uses: actions/setup-python@v4 name: Install Python with: - python-version: '3.7' + python-version: '3.8' - name: Verify this is not a dev version shell: bash run: | - python -m pip install wheels/*-cp37-cp37m-*.manylinux2010_x86_64.whl + python -m pip install wheels/*-cp38-cp38-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 @@ -176,3 +179,36 @@ jobs: run: | python -m pip install "twine" python -m twine upload --verbose wheels/*.whl sdist/*.tar.gz + + + deploy_testpypi: + name: "Deploy to TestPyPI if desired" + if: ${{ github.event.inputs.confirm_ref == 'test' }} + needs: [deploy_test, build_sdist, build_wheels] + runs-on: ubuntu-latest + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.TESTPYPI_TOKEN }} + TWINE_NON_INTERACTIVE: 1 + + steps: + - name: Download build artifacts to local runner + uses: actions/download-artifact@v3 + + - uses: actions/setup-python@v4 + name: Install Python + with: + python-version: '3.8' + + - name: Verify this is not a dev version + shell: bash + run: | + python -m pip install wheels/*-cp38-cp38-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 + # our end, but PyPI only needs the tarball. + - name: Upload sdist and wheels to TestPyPI + run: | + python -m pip install "twine" + python -m twine upload --repository testpypi --verbose wheels/*.whl sdist/*.tar.gz diff --git a/.github/workflows/build_documentation.yml b/.github/workflows/build_documentation.yml index af315e8be1..b5c6eb9cf6 100644 --- a/.github/workflows/build_documentation.yml +++ b/.github/workflows/build_documentation.yml @@ -1,4 +1,4 @@ -name: Build HTML documentation +name: Build documentation on: [push, pull_request] @@ -9,9 +9,9 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - - uses: actions/setup-python@v2 + - uses: actions/setup-python@v4 name: Install Python with: python-version: '3.8' @@ -20,6 +20,8 @@ jobs: run: | pip install pip --upgrade python -mpip install -r doc/requirements.txt + sudo apt-get update + sudo apt-get install texlive-full - name: Install QuTiP from GitHub run: | @@ -31,7 +33,23 @@ jobs: # because we're importing from the wrong location. python -c 'import qutip; qutip.about()' - - name: Build documentation + - name: Build PDF documentation + working-directory: doc + run: | + make latexpdf SPHINXOPTS="-W --keep-going -T" + # Above flags are: + # -W : turn warnings into errors + # --keep-going : do not stop after the first error + # -T : display a full traceback if a Python exception occurs + + - name: Upload built PDF files + uses: actions/upload-artifact@v3 + with: + name: qutip_pdf_docs + path: doc/_build/latex/* + if-no-files-found: error + + - name: Build HTML documentation working-directory: doc run: | make html SPHINXOPTS="-W --keep-going -T" @@ -40,8 +58,8 @@ jobs: # --keep-going : do not stop after the first error # -T : display a full traceback if a Python exception occurs - - name: Upload built files - uses: actions/upload-artifact@v2 + - name: Upload built HTML files + uses: actions/upload-artifact@v3 with: name: qutip_html_docs path: doc/_build/html/* diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4e7b1932e4..ca207a7b95 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -13,26 +13,37 @@ defaults: # necessary for having conda behave sensibly. We use bash as the shell even # on Windows, since we don't run anything much complicated, and it makes # things much simpler. - shell: bash -l {0} + shell: bash -l -e {0} jobs: cases: name: ${{ matrix.os }}, python${{ matrix.python-version }}, ${{ matrix.case-name }} runs-on: ${{ matrix.os }} + env: + MPLBACKEND: Agg # Explicitly define matplotlib backend for Windows tests strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-latest] + 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"] case-name: [defaults] numpy-requirement: [">=1.20,<1.21"] + 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 os: ubuntu-latest @@ -58,16 +69,23 @@ jobs: # Python 3.10 and numpy 1.22 # Use conda-forge to provide numpy 1.22 - # Ignore ImportWarning because pyximport registered an importer - # PyxImporter that does not have a find_spec method and this raises - # a warning on Python 3.10 - # Ignore DeprecationWarnings raised by cvxpy importing scipy.sparse.X - # under SciPy 1.8.0+. - case-name: Python 3.10 os: ubuntu-latest python-version: "3.10" condaforge: 1 - pytest-extra-options: "-W ignore::ImportWarning -W ignore::DeprecationWarning:cvxpy.interface.scipy_wrapper" + + # Python 3.11 and latest 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 + # https://github.com/cython/cython/pull/5128 but not backported + # to any currently released version. + - case-name: Python 3.11 + os: ubuntu-latest + python-version: "3.11" + condaforge: 1 + conda-extra-pkgs: "suitesparse" # for compiling cvxopt + pytest-extra-options: "-W ignore::DeprecationWarning:Cython.Tempita" # Windows. Once all tests pass without special options needed, this # can be moved to the main os list in the test matrix. All the tests @@ -77,15 +95,17 @@ jobs: # error prone. See, e.g., https://github.com/qutip/qutip/issues/1202 - case-name: Windows Latest os: windows-latest - pytest-extra-options: "-k 'not (test_correlation)'" + python-version: "3.10" + pytest-extra-options: "-k 'not (test_correlation or test_interpolate or test_mcsolve)'" steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: conda-incubator/setup-miniconda@v2 with: auto-update-conda: true python-version: ${{ matrix.python-version }} + channels: ${{ matrix.condaforge == 1 && 'conda-forge' || 'defaults' }} - name: Install QuTiP and dependencies # In the run, first we handle any special cases. We do this in bash @@ -97,10 +117,6 @@ jobs: QUTIP_TARGET="$QUTIP_TARGET,runtime_compilation" fi export CI_QUTIP_WITH_OPENMP=${{ matrix.openmp }} - if [[ -n "${{ matrix.condaforge }}" ]]; then - conda config --prepend channels conda-forge - conda config --set channel_priority strict - fi if [[ -z "${{ matrix.nomkl }}" ]]; then conda install blas=*=mkl "numpy${{ matrix.numpy-requirement }}" "scipy${{ matrix.scipy-requirement }}" elif [[ "${{ matrix.os }}" =~ ^windows.*$ ]]; then @@ -110,8 +126,11 @@ jobs: else conda install nomkl "numpy${{ matrix.numpy-requirement }}" "scipy${{ matrix.scipy-requirement }}" fi + if [[ -n "${{ matrix.conda-extra-pkgs }}" ]]; then + conda install "${{ matrix.conda-extra-pkgs }}" + fi python -m pip install -e .[$QUTIP_TARGET] - python -m pip install coverage==6.2 + python -m pip install "coverage${{ matrix.coverage-requirement }}" python -m pip install pytest-cov coveralls pytest-fail-slow - name: Package information @@ -173,6 +192,30 @@ jobs: COVERALLS_PARALLEL: true run: coveralls --service=github + towncrier-check: + name: Verify Towncrier entry added + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Install Towncrier + run: | + python -m pip install towncrier + + - name: Verify Towncrier entry added + if: False # github.event_name == 'pull_request' + env: + BASE_BRANCH: ${{ github.base_ref }} + run: | + # Fetch the pull request' base branch so towncrier will be able to + # compare the current branch with the base branch. + # Source: https://github.com/actions/checkout/#fetch-all-branches. + git fetch --no-tags origin +refs/heads/${BASE_BRANCH}:refs/remotes/origin/${BASE_BRANCH} + towncrier check --compare-with origin/${BASE_BRANCH} + towncrier build --version "$(cat VERSION)" --draft + finalise: name: Finalise coverage reporting needs: cases diff --git a/README.md b/README.md index 900f320f3a..19a901e5da 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ QuTiP: Quantum Toolbox in Python [B. Li](https://github.com/boxili), [J. Lishman](https://github.com/jakelishman), [S. Cross](https://github.com/hodgestar), +[A. Galicia](https://github.com/AGaliciaMartinez), [P. D. Nation](https://github.com/nonhermitian), and [J. R. Johansson](https://github.com/jrjohansson) @@ -21,6 +22,19 @@ and [J. R. Johansson](https://github.com/jrjohansson) [![PyPi Downloads](https://img.shields.io/pypi/dm/qutip?label=downloads%20%7C%20pip&logo=PyPI)](https://pypi.org/project/qutip) [![Conda-Forge Downloads](https://img.shields.io/conda/dn/conda-forge/qutip?label=downloads%20%7C%20conda&logo=Conda-Forge)](https://anaconda.org/conda-forge/qutip) +> **Note** +> +> The master branch now contains the alpha version of QuTiP 5. This is major +> revision that breaks compatibility in many small ways withh QuTiP 4.7. +> +> If you need to track QuTiP 4.7 changes or submit pull requests for 4.7, +> please use the `qutip-4.7.X` branch. +> +> If you need to track QuTiP 5 changes or submit pull request for 5, +> please use the `master` branch (and not the `dev.major` branch). +> +> The change to master happened on 16 January 2023 in commit @fccec5d. + QuTiP is open-source software for simulating the dynamics of closed and open quantum systems. It uses the excellent Numpy, Scipy, and Cython packages as numerical backends, and graphical output is provided by Matplotlib. QuTiP aims to provide user-friendly and efficient numerical simulations of a wide variety of quantum mechanical problems, including those with Hamiltonians and/or collapse operators with arbitrary time-dependence, commonly found in a wide range of physics applications. @@ -58,6 +72,17 @@ All back releases are also available for download in the [releases section of th For the most complete set of release notes and changelogs for historic versions, see the [changelog](https://qutip.org/docs/latest/changelog.html) section in the documentation. +The pre-release of QuTiP 5.0 is available on PyPI and can be installed using pip: + +```bash +pip install --pre qutip +``` + +This version breaks compatibility with QuTiP 4.7 in many small ways. +Please see the [changelog](https://github.com/qutip/qutip/blob/master/doc/changelog.rst) for a list of changes, new features and deprecations. +This version should be fully working. If you find any bugs, confusing documentation or missing features, please create a GitHub issue. + + Documentation ------------- diff --git a/doc/QuTiP_tree_plot/d3_data/qutip.json b/doc/QuTiP_tree_plot/d3_data/qutip.json index cbed9aa31c..0f46d672ee 100644 --- a/doc/QuTiP_tree_plot/d3_data/qutip.json +++ b/doc/QuTiP_tree_plot/d3_data/qutip.json @@ -1 +1 @@ -{"name": "QuTiP", "color": "black", "index": -1, "children": [{"name": "core", "color": "black", "index": -1, "children": [{"name": "cy", "color": "black", "index": -1, "children": [{"name": "coefficient", "color": "black", "index": -1, "children": [{"name": "Coefficient", "color": "black", "index": -1}, {"name": "ConjCoefficient", "color": "black", "index": -1}, {"name": "FunctionCoefficient", "color": "black", "index": -1}, {"name": "InterCoefficient", "color": "black", "index": -1}, {"name": "MulCoefficient", "color": "black", "index": -1}, {"name": "NormCoefficient", "color": "black", "index": -1}, {"name": "ShiftCoefficient", "color": "black", "index": -1}, {"name": "StrFunctionCoefficient", "color": "black", "index": -1}, {"name": "SumCoefficient", "color": "black", "index": -1}, {"name": "coefficient_function_parameters", "color": "black", "index": -1}, {"name": "proj", "color": "black", "index": -1}]}, {"name": "_element", "color": "black", "index": -1}, {"name": "qobjevo", "color": "black", "index": -1, "children": [{"name": "QobjEvo", "color": "black", "index": -1}]}]}, {"name": "data", "color": "black", "index": -1, "children": [{"name": "csr", "color": "black", "index": -1, "children": [{"name": "CSR", "color": "black", "index": -1}, {"name": "Sorter", "color": "black", "index": -1}, {"name": "copy_structure", "color": "black", "index": -1}, {"name": "diags", "color": "black", "index": -1}, {"name": "empty", "color": "black", "index": -1}, {"name": "empty_like", "color": "black", "index": -1}, {"name": "fast_from_scipy", "color": "black", "index": -1}, {"name": "from_dense", "color": "black", "index": -1}, {"name": "identity", "color": "black", "index": -1}, {"name": "nnz", "color": "black", "index": -1}, {"name": "sorted", "color": "black", "index": -1}, {"name": "zeros", "color": "black", "index": -1}]}, {"name": "base", "color": "black", "index": -1, "children": [{"name": "Data", "color": "black", "index": -1}, {"name": "EfficiencyWarning", "color": "black", "index": -1}]}, {"name": "dense", "color": "black", "index": -1, "children": [{"name": "Dense", "color": "black", "index": -1}, {"name": "OrderEfficiencyWarning", "color": "black", "index": -1}, {"name": "diags", "color": "black", "index": -1}, {"name": "empty", "color": "black", "index": -1}, {"name": "empty_like", "color": "black", "index": -1}, {"name": "fast_from_numpy", "color": "black", "index": -1}, {"name": "from_csr", "color": "black", "index": -1}, {"name": "identity", "color": "black", "index": -1}, {"name": "zeros", "color": "black", "index": -1}]}, {"name": "dispatch", "color": "black", "index": -1, "children": [{"name": "Dispatcher", "color": "black", "index": -1}]}, {"name": "add", "color": "black", "index": -1, "children": [{"name": "add_csr", "color": "black", "index": -1}, {"name": "add_dense", "color": "black", "index": -1}, {"name": "iadd_dense", "color": "black", "index": -1}, {"name": "sub_csr", "color": "black", "index": -1}, {"name": "sub_dense", "color": "black", "index": -1}]}, {"name": "adjoint", "color": "black", "index": -1, "children": [{"name": "adjoint_csr", "color": "black", "index": -1}, {"name": "adjoint_dense", "color": "black", "index": -1}, {"name": "conj_csr", "color": "black", "index": -1}, {"name": "conj_dense", "color": "black", "index": -1}, {"name": "transpose_csr", "color": "black", "index": -1}, {"name": "transpose_dense", "color": "black", "index": -1}]}, {"name": "reshape", "color": "black", "index": -1, "children": [{"name": "column_stack_csr", "color": "black", "index": -1}, {"name": "column_stack_dense", "color": "black", "index": -1}, {"name": "column_unstack_csr", "color": "black", "index": -1}, {"name": "column_unstack_dense", "color": "black", "index": -1}, {"name": "reshape_csr", "color": "black", "index": -1}, {"name": "reshape_dense", "color": "black", "index": -1}, {"name": "split_columns_csr", "color": "black", "index": -1}, {"name": "split_columns_dense", "color": "black", "index": -1}]}, {"name": "constant", "color": "black", "index": -1}, {"name": "convert", "color": "black", "index": -1}, {"name": "eigen", "color": "black", "index": -1, "children": [{"name": "eigs_csr", "color": "black", "index": -1}, {"name": "eigs_dense", "color": "black", "index": -1}]}, {"name": "expect", "color": "black", "index": -1, "children": [{"name": "expect_csr", "color": "black", "index": -1}, {"name": "expect_csr_dense", "color": "black", "index": -1}, {"name": "expect_dense", "color": "black", "index": -1}, {"name": "expect_super_csr", "color": "black", "index": -1}, {"name": "expect_super_csr_dense", "color": "black", "index": -1}, {"name": "expect_super_dense", "color": "black", "index": -1}]}, {"name": "expm", "color": "black", "index": -1, "children": [{"name": "expm_csr", "color": "black", "index": -1}, {"name": "expm_csr_dense", "color": "black", "index": -1}]}, {"name": "properties", "color": "black", "index": -1, "children": [{"name": "isdiag_csr", "color": "black", "index": -1}, {"name": "isherm_csr", "color": "black", "index": -1}, {"name": "iszero_csr", "color": "black", "index": -1}, {"name": "iszero_dense", "color": "black", "index": -1}]}, {"name": "mul", "color": "black", "index": -1, "children": [{"name": "imul_csr", "color": "black", "index": -1}, {"name": "imul_data", "color": "black", "index": -1}, {"name": "imul_dense", "color": "black", "index": -1}, {"name": "mul_csr", "color": "black", "index": -1}, {"name": "mul_dense", "color": "black", "index": -1}, {"name": "neg_csr", "color": "black", "index": -1}, {"name": "neg_dense", "color": "black", "index": -1}]}, {"name": "inner", "color": "black", "index": -1, "children": [{"name": "inner_csr", "color": "black", "index": -1}, {"name": "inner_op_csr", "color": "black", "index": -1}]}, {"name": "linalg", "color": "black", "index": -1, "children": [{"name": "inv_csr", "color": "black", "index": -1}, {"name": "inv_dense", "color": "black", "index": -1}]}, {"name": "kron", "color": "black", "index": -1, "children": [{"name": "kron_csr", "color": "black", "index": -1}, {"name": "kron_dense", "color": "black", "index": -1}]}, {"name": "make", "color": "black", "index": -1, "children": [{"name": "one_element_csr", "color": "black", "index": -1}, {"name": "one_element_dense", "color": "black", "index": -1}]}, {"name": "matmul", "color": "black", "index": -1, "children": [{"name": "matmul_csr", "color": "black", "index": -1}, {"name": "matmul_csr_dense_dense", "color": "black", "index": -1}, {"name": "matmul_dense", "color": "black", "index": -1}, {"name": "multiply_csr", "color": "black", "index": -1}, {"name": "multiply_dense", "color": "black", "index": -1}]}, {"name": "norm", "color": "black", "index": -1, "children": [{"name": "frobenius_csr", "color": "black", "index": -1}, {"name": "frobenius_data", "color": "black", "index": -1}, {"name": "frobenius_dense", "color": "black", "index": -1}, {"name": "l2_csr", "color": "black", "index": -1}, {"name": "l2_dense", "color": "black", "index": -1}, {"name": "max_csr", "color": "black", "index": -1}, {"name": "max_dense", "color": "black", "index": -1}, {"name": "one_csr", "color": "black", "index": -1}, {"name": "one_dense", "color": "black", "index": -1}, {"name": "trace_csr", "color": "black", "index": -1}, {"name": "trace_dense", "color": "black", "index": -1}]}, {"name": "permute", "color": "black", "index": -1, "children": [{"name": "dimensions_csr", "color": "black", "index": -1}, {"name": "indices_csr", "color": "black", "index": -1}]}, {"name": "pow", "color": "black", "index": -1, "children": [{"name": "pow_csr", "color": "black", "index": -1}]}, {"name": "project", "color": "black", "index": -1, "children": [{"name": "project_csr", "color": "black", "index": -1}, {"name": "project_dense", "color": "black", "index": -1}]}, {"name": "ptrace", "color": "black", "index": -1, "children": [{"name": "ptrace_csr", "color": "black", "index": -1}, {"name": "ptrace_csr_dense", "color": "black", "index": -1}, {"name": "ptrace_dense", "color": "black", "index": -1}]}, {"name": "tidyup", "color": "black", "index": -1, "children": [{"name": "tidyup_csr", "color": "black", "index": -1}, {"name": "tidyup_dense", "color": "black", "index": -1}]}, {"name": "trace", "color": "black", "index": -1, "children": [{"name": "trace_csr", "color": "black", "index": -1}, {"name": "trace_dense", "color": "black", "index": -1}]}]}, {"name": "coefficient", "color": "black", "index": -1, "children": [{"name": "CompilationOptions", "color": "black", "index": -1}, {"name": "StringParsingWarning", "color": "black", "index": -1}, {"name": "clean_compiled_coefficient", "color": "black", "index": -1}, {"name": "coeff_from_str", "color": "black", "index": -1}, {"name": "coefficient", "color": "black", "index": -1}, {"name": "compileType", "color": "black", "index": -1}, {"name": "compile_code", "color": "black", "index": -1}, {"name": "conj", "color": "black", "index": -1}, {"name": "extract_constant", "color": "black", "index": -1}, {"name": "extract_cte_pattern", "color": "black", "index": -1}, {"name": "find_type_from_str", "color": "black", "index": -1}, {"name": "fix_type", "color": "black", "index": -1}, {"name": "fromstr", "color": "black", "index": -1}, {"name": "make_cy_code", "color": "black", "index": -1}, {"name": "norm", "color": "black", "index": -1}, {"name": "parse", "color": "black", "index": -1}, {"name": "proj", "color": "black", "index": -1}, {"name": "shift", "color": "black", "index": -1}, {"name": "space_parts", "color": "black", "index": -1}, {"name": "test_parsed", "color": "black", "index": -1}, {"name": "try_import", "color": "black", "index": -1}, {"name": "try_parse", "color": "black", "index": -1}, {"name": "use_hinted_type", "color": "black", "index": -1}]}, {"name": "options", "color": "black", "index": -1, "children": [{"name": "CoreOptions", "color": "black", "index": -1}, {"name": "QutipOptions", "color": "black", "index": -1}]}, {"name": "qobj", "color": "black", "index": -1, "children": [{"name": "Qobj", "color": "black", "index": -1}, {"name": "isbra", "color": "black", "index": -1}, {"name": "isherm", "color": "black", "index": -1}, {"name": "isket", "color": "black", "index": -1}, {"name": "isoper", "color": "black", "index": -1}, {"name": "isoperbra", "color": "black", "index": -1}, {"name": "isoperket", "color": "black", "index": -1}, {"name": "issuper", "color": "black", "index": -1}, {"name": "ptrace", "color": "black", "index": -1}]}, {"name": "dimensions", "color": "black", "index": -1, "children": [{"name": "collapse_dims_oper", "color": "black", "index": -1}, {"name": "collapse_dims_super", "color": "black", "index": -1}, {"name": "deep_map", "color": "black", "index": -1}, {"name": "deep_remove", "color": "black", "index": -1}, {"name": "dims_idxs_to_tensor_idxs", "color": "black", "index": -1}, {"name": "dims_to_tensor_perm", "color": "black", "index": -1}, {"name": "dims_to_tensor_shape", "color": "black", "index": -1}, {"name": "enumerate_flat", "color": "black", "index": -1}, {"name": "flatten", "color": "black", "index": -1}, {"name": "is_scalar", "color": "black", "index": -1}, {"name": "is_vector", "color": "black", "index": -1}, {"name": "is_vectorized_oper", "color": "black", "index": -1}, {"name": "type_from_dims", "color": "black", "index": -1}, {"name": "unflatten", "color": "black", "index": -1}]}, {"name": "metrics", "color": "black", "index": -1, "children": [{"name": "average_gate_fidelity", "color": "black", "index": -1}, {"name": "bures_angle", "color": "black", "index": -1}, {"name": "bures_dist", "color": "black", "index": -1}, {"name": "dnorm", "color": "black", "index": -1}, {"name": "fidelity", "color": "black", "index": -1}, {"name": "hellinger_dist", "color": "black", "index": -1}, {"name": "hilbert_dist", "color": "black", "index": -1}, {"name": "process_fidelity", "color": "black", "index": -1}, {"name": "tracedist", "color": "black", "index": -1}, {"name": "unitarity", "color": "black", "index": -1}]}, {"name": "superop_reps", "color": "black", "index": -1, "children": [{"name": "isqubitdims", "color": "black", "index": -1}, {"name": "kraus_to_choi", "color": "black", "index": -1}, {"name": "kraus_to_super", "color": "black", "index": -1}, {"name": "to_chi", "color": "black", "index": -1}, {"name": "to_choi", "color": "black", "index": -1}, {"name": "to_kraus", "color": "black", "index": -1}, {"name": "to_stinespring", "color": "black", "index": -1}, {"name": "to_super", "color": "black", "index": -1}]}, {"name": "states", "color": "black", "index": -1, "children": [{"name": "basis", "color": "black", "index": -1}, {"name": "bell_state", "color": "black", "index": -1}, {"name": "bra", "color": "black", "index": -1}, {"name": "coherent", "color": "black", "index": -1}, {"name": "coherent_dm", "color": "black", "index": -1}, {"name": "enr_fock", "color": "black", "index": -1}, {"name": "enr_state_dictionaries", "color": "black", "index": -1}, {"name": "enr_thermal_dm", "color": "black", "index": -1}, {"name": "fock", "color": "black", "index": -1}, {"name": "fock_dm", "color": "black", "index": -1}, {"name": "ghz_state", "color": "black", "index": -1}, {"name": "ket", "color": "black", "index": -1}, {"name": "ket2dm", "color": "black", "index": -1}, {"name": "maximally_mixed_dm", "color": "black", "index": -1}, {"name": "phase_basis", "color": "black", "index": -1}, {"name": "projection", "color": "black", "index": -1}, {"name": "qstate", "color": "black", "index": -1}, {"name": "qutrit_basis", "color": "black", "index": -1}, {"name": "singlet_state", "color": "black", "index": -1}, {"name": "spin_coherent", "color": "black", "index": -1}, {"name": "spin_state", "color": "black", "index": -1}, {"name": "state_index_number", "color": "black", "index": -1}, {"name": "state_number_enumerate", "color": "black", "index": -1}, {"name": "state_number_index", "color": "black", "index": -1}, {"name": "state_number_qobj", "color": "black", "index": -1}, {"name": "thermal_dm", "color": "black", "index": -1}, {"name": "triplet_states", "color": "black", "index": -1}, {"name": "w_state", "color": "black", "index": -1}, {"name": "zero_ket", "color": "black", "index": -1}]}, {"name": "operators", "color": "black", "index": -1, "children": [{"name": "charge", "color": "black", "index": -1}, {"name": "commutator", "color": "black", "index": -1}, {"name": "create", "color": "black", "index": -1}, {"name": "destroy", "color": "black", "index": -1}, {"name": "displace", "color": "black", "index": -1}, {"name": "enr_destroy", "color": "black", "index": -1}, {"name": "enr_identity", "color": "black", "index": -1}, {"name": "qeye", "color": "black", "index": -1}, {"name": "jmat", "color": "black", "index": -1}, {"name": "momentum", "color": "black", "index": -1}, {"name": "num", "color": "black", "index": -1}, {"name": "phase", "color": "black", "index": -1}, {"name": "position", "color": "black", "index": -1}, {"name": "qdiags", "color": "black", "index": -1}, {"name": "qft", "color": "black", "index": -1}, {"name": "qutrit_ops", "color": "black", "index": -1}, {"name": "qzero", "color": "black", "index": -1}, {"name": "sigmam", "color": "black", "index": -1}, {"name": "sigmap", "color": "black", "index": -1}, {"name": "sigmax", "color": "black", "index": -1}, {"name": "sigmay", "color": "black", "index": -1}, {"name": "sigmaz", "color": "black", "index": -1}, {"name": "spin_J_set", "color": "black", "index": -1}, {"name": "spin_Jm", "color": "black", "index": -1}, {"name": "spin_Jp", "color": "black", "index": -1}, {"name": "spin_Jx", "color": "black", "index": -1}, {"name": "spin_Jy", "color": "black", "index": -1}, {"name": "spin_Jz", "color": "black", "index": -1}, {"name": "squeeze", "color": "black", "index": -1}, {"name": "squeezing", "color": "black", "index": -1}, {"name": "tunneling", "color": "black", "index": -1}]}, {"name": "tensor", "color": "black", "index": -1, "children": [{"name": "composite", "color": "black", "index": -1}, {"name": "expand_operator", "color": "black", "index": -1}, {"name": "super_tensor", "color": "black", "index": -1}, {"name": "tensor", "color": "black", "index": -1}, {"name": "tensor_contract", "color": "black", "index": -1}, {"name": "tensor_swap", "color": "black", "index": -1}]}, {"name": "superoperator", "color": "black", "index": -1, "children": [{"name": "lindblad_dissipator", "color": "black", "index": -1}, {"name": "liouvillian", "color": "black", "index": -1}, {"name": "operator_to_vector", "color": "black", "index": -1}, {"name": "reshuffle", "color": "black", "index": -1}, {"name": "spost", "color": "black", "index": -1}, {"name": "spre", "color": "black", "index": -1}, {"name": "sprepost", "color": "black", "index": -1}, {"name": "stack_columns", "color": "black", "index": -1}, {"name": "stacked_index", "color": "black", "index": -1}, {"name": "unstack_columns", "color": "black", "index": -1}, {"name": "unstacked_index", "color": "black", "index": -1}, {"name": "vector_to_operator", "color": "black", "index": -1}]}, {"name": "semidefinite", "color": "black", "index": -1, "children": [{"name": "Complex", "color": "black", "index": -1}, {"name": "bmat", "color": "black", "index": -1}, {"name": "complex_var", "color": "black", "index": -1}, {"name": "conj", "color": "black", "index": -1}, {"name": "dag", "color": "black", "index": -1}, {"name": "dens", "color": "black", "index": -1}, {"name": "dnorm_problem", "color": "black", "index": -1}, {"name": "dnorm_sparse_problem", "color": "black", "index": -1}, {"name": "herm", "color": "black", "index": -1}, {"name": "initialize_constraints_on_dnorm_problem", "color": "black", "index": -1}, {"name": "kron", "color": "black", "index": -1}, {"name": "memoize", "color": "black", "index": -1}, {"name": "pos", "color": "black", "index": -1}, {"name": "pos_noherm", "color": "black", "index": -1}, {"name": "qudit_swap", "color": "black", "index": -1}]}, {"name": "_brtensor", "color": "black", "index": -1}, {"name": "_brtools", "color": "black", "index": -1, "children": [{"name": "SpectraCoefficient", "color": "black", "index": -1}, {"name": "matmul_var_data", "color": "black", "index": -1}]}, {"name": "blochredfield", "color": "black", "index": -1, "children": [{"name": "bloch_redfield_tensor", "color": "black", "index": -1}, {"name": "brterm", "color": "black", "index": -1}]}, {"name": "expect", "color": "black", "index": -1, "children": [{"name": "expect", "color": "black", "index": -1}, {"name": "variance", "color": "black", "index": -1}]}, {"name": "gates", "color": "black", "index": -1, "children": [{"name": "berkeley", "color": "black", "index": -1}, {"name": "cnot", "color": "black", "index": -1}, {"name": "cphase", "color": "black", "index": -1}, {"name": "cs_gate", "color": "black", "index": -1}, {"name": "csign", "color": "black", "index": -1}, {"name": "ct_gate", "color": "black", "index": -1}, {"name": "cy_gate", "color": "black", "index": -1}, {"name": "cz_gate", "color": "black", "index": -1}, {"name": "fredkin", "color": "black", "index": -1}, {"name": "globalphase", "color": "black", "index": -1}, {"name": "hadamard_transform", "color": "black", "index": -1}, {"name": "iswap", "color": "black", "index": -1}, {"name": "molmer_sorensen", "color": "black", "index": -1}, {"name": "phasegate", "color": "black", "index": -1}, {"name": "qrot", "color": "black", "index": -1}, {"name": "qubit_clifford_group", "color": "black", "index": -1}, {"name": "rx", "color": "black", "index": -1}, {"name": "ry", "color": "black", "index": -1}, {"name": "rz", "color": "black", "index": -1}, {"name": "s_gate", "color": "black", "index": -1}, {"name": "snot", "color": "black", "index": -1}, {"name": "sqrtiswap", "color": "black", "index": -1}, {"name": "sqrtnot", "color": "black", "index": -1}, {"name": "sqrtswap", "color": "black", "index": -1}, {"name": "swap", "color": "black", "index": -1}, {"name": "swapalpha", "color": "black", "index": -1}, {"name": "t_gate", "color": "black", "index": -1}, {"name": "toffoli", "color": "black", "index": -1}]}, {"name": "subsystem_apply", "color": "black", "index": -1, "children": [{"name": "subsystem_apply", "color": "black", "index": -1}]}]}, {"name": "optionsclass", "color": "black", "index": -1, "children": [{"name": "AllOptions", "color": "black", "index": -1}, {"name": "optionsclass", "color": "black", "index": -1}]}, {"name": "solve", "color": "black", "index": -1, "children": [{"name": "piqs", "color": "black", "index": -1, "children": [{"name": "Dicke", "color": "black", "index": -1}, {"name": "Pim", "color": "black", "index": -1}, {"name": "am", "color": "black", "index": -1}, {"name": "ap", "color": "black", "index": -1}, {"name": "block_matrix", "color": "black", "index": -1}, {"name": "collapse_uncoupled", "color": "black", "index": -1}, {"name": "css", "color": "black", "index": -1}, {"name": "dicke", "color": "black", "index": -1}, {"name": "dicke_basis", "color": "black", "index": -1}, {"name": "dicke_blocks", "color": "black", "index": -1}, {"name": "dicke_blocks_full", "color": "black", "index": -1}, {"name": "dicke_function_trace", "color": "black", "index": -1}, {"name": "energy_degeneracy", "color": "black", "index": -1}, {"name": "entropy_vn_dicke", "color": "black", "index": -1}, {"name": "excited", "color": "black", "index": -1}, {"name": "ghz", "color": "black", "index": -1}, {"name": "ground", "color": "black", "index": -1}, {"name": "identity_uncoupled", "color": "black", "index": -1}, {"name": "isdiagonal", "color": "black", "index": -1}, {"name": "jspin", "color": "black", "index": -1}, {"name": "m_degeneracy", "color": "black", "index": -1}, {"name": "num_dicke_ladders", "color": "black", "index": -1}, {"name": "num_dicke_states", "color": "black", "index": -1}, {"name": "num_tls", "color": "black", "index": -1}, {"name": "purity_dicke", "color": "black", "index": -1}, {"name": "spin_algebra", "color": "black", "index": -1}, {"name": "state_degeneracy", "color": "black", "index": -1}, {"name": "superradiant", "color": "black", "index": -1}, {"name": "tau_column", "color": "black", "index": -1}]}, {"name": "solver", "color": "black", "index": -1, "children": [{"name": "ExpectOps", "color": "black", "index": -1}, {"name": "McOptions", "color": "black", "index": -1}, {"name": "Result", "color": "black", "index": -1}, {"name": "SolverConfiguration", "color": "black", "index": -1}, {"name": "SolverOptions", "color": "black", "index": -1}, {"name": "SolverSystem", "color": "black", "index": -1}, {"name": "Stats", "color": "black", "index": -1}]}, {"name": "pdpsolve", "color": "black", "index": -1, "children": [{"name": "StochasticSolverOptions", "color": "black", "index": -1}, {"name": "main_smepdpsolve", "color": "black", "index": -1}, {"name": "main_ssepdpsolve", "color": "black", "index": -1}]}, {"name": "_mcsolve", "color": "black", "index": -1, "children": [{"name": "CyMcOde", "color": "black", "index": -1}, {"name": "CyMcOdeDiag", "color": "black", "index": -1}]}, {"name": "_piqs", "color": "black", "index": -1, "children": [{"name": "Dicke", "color": "black", "index": -1}, {"name": "get_blocks", "color": "black", "index": -1}, {"name": "get_index", "color": "black", "index": -1}, {"name": "j_min", "color": "black", "index": -1}, {"name": "j_vals", "color": "black", "index": -1}, {"name": "jmm1_dictionary", "color": "black", "index": -1}, {"name": "m_vals", "color": "black", "index": -1}]}, {"name": "_steadystate", "color": "black", "index": -1, "children": [{"name": "weighted_bipartite_matching", "color": "black", "index": -1}]}, {"name": "_stochastic", "color": "black", "index": -1, "children": [{"name": "GenericSSolver", "color": "black", "index": -1}, {"name": "PcSMESolver", "color": "black", "index": -1}, {"name": "PcSSESolver", "color": "black", "index": -1}, {"name": "PmSMESolver", "color": "black", "index": -1}, {"name": "SMESolver", "color": "black", "index": -1}, {"name": "SSESolver", "color": "black", "index": -1}, {"name": "StochasticSolver", "color": "black", "index": -1}, {"name": "TaylorNoise", "color": "black", "index": -1}, {"name": "normalize_inplace", "color": "black", "index": -1}]}, {"name": "steadystate", "color": "black", "index": -1, "children": [{"name": "build_preconditioner", "color": "black", "index": -1}, {"name": "pseudo_inverse", "color": "black", "index": -1}, {"name": "steadystate", "color": "black", "index": -1}, {"name": "steadystate_floquet", "color": "black", "index": -1}]}, {"name": "correlation", "color": "black", "index": -1, "children": [{"name": "coherence_function_g1", "color": "black", "index": -1}, {"name": "coherence_function_g2", "color": "black", "index": -1}, {"name": "correlation_2op_1t", "color": "black", "index": -1}, {"name": "correlation_2op_2t", "color": "black", "index": -1}, {"name": "correlation_3op_1t", "color": "black", "index": -1}, {"name": "correlation_3op_2t", "color": "black", "index": -1}, {"name": "spectrum", "color": "black", "index": -1}, {"name": "spectrum_correlation_fft", "color": "black", "index": -1}]}, {"name": "mcsolve", "color": "black", "index": -1, "children": [{"name": "mcsolve", "color": "black", "index": -1}, {"name": "qutip_zvode", "color": "black", "index": -1}]}, {"name": "sesolve", "color": "black", "index": -1, "children": [{"name": "sesolve", "color": "black", "index": -1}]}, {"name": "mesolve", "color": "black", "index": -1, "children": [{"name": "mesolve", "color": "black", "index": -1}]}, {"name": "countstat", "color": "black", "index": -1, "children": [{"name": "countstat_current", "color": "black", "index": -1}, {"name": "countstat_current_noise", "color": "black", "index": -1}]}, {"name": "floquet", "color": "black", "index": -1, "children": [{"name": "floquet_basis_transform", "color": "black", "index": -1}, {"name": "floquet_collapse_operators", "color": "black", "index": -1}, {"name": "floquet_markov_mesolve", "color": "black", "index": -1}, {"name": "floquet_master_equation_rates", "color": "black", "index": -1}, {"name": "floquet_master_equation_steadystate", "color": "black", "index": -1}, {"name": "floquet_master_equation_tensor", "color": "black", "index": -1}, {"name": "floquet_modes", "color": "black", "index": -1}, {"name": "floquet_modes_t", "color": "black", "index": -1}, {"name": "floquet_modes_t_lookup", "color": "black", "index": -1}, {"name": "floquet_modes_table", "color": "black", "index": -1}, {"name": "floquet_state_decomposition", "color": "black", "index": -1}, {"name": "floquet_states", "color": "black", "index": -1}, {"name": "floquet_states_t", "color": "black", "index": -1}, {"name": "floquet_wavefunction", "color": "black", "index": -1}, {"name": "floquet_wavefunction_t", "color": "black", "index": -1}, {"name": "fmmesolve", "color": "black", "index": -1}, {"name": "fsesolve", "color": "black", "index": -1}]}, {"name": "stochastic", "color": "black", "index": -1, "children": [{"name": "StochasticSolverOptions", "color": "black", "index": -1}, {"name": "StochasticSolverOptionsPhoto", "color": "black", "index": -1}, {"name": "general_stochastic", "color": "black", "index": -1}, {"name": "photocurrent_mesolve", "color": "black", "index": -1}, {"name": "photocurrent_sesolve", "color": "black", "index": -1}, {"name": "smepdpsolve", "color": "black", "index": -1}, {"name": "smesolve", "color": "black", "index": -1}, {"name": "ssepdpsolve", "color": "black", "index": -1}, {"name": "ssesolve", "color": "black", "index": -1}, {"name": "stochastic_solvers", "color": "black", "index": -1}]}, {"name": "krylovsolve", "color": "black", "index": -1, "children": [{"name": "krylovsolve", "color": "black", "index": -1}, {"name": "lanczos_algorithm", "color": "black", "index": -1}, {"name": "particular_tlist_or_happy_breakdown", "color": "black", "index": -1}]}, {"name": "nonmarkov", "color": "black", "index": -1}, {"name": "rcsolve", "color": "black", "index": -1, "children": [{"name": "rcsolve", "color": "black", "index": -1}]}]}, {"name": "solver", "color": "#043c6b", "index": 1, "children": [{"name": "brmesolve", "color": "#043c6b", "index": 1, "children": [{"name": "BRSolver", "color": "#043c6b", "index": 1}, {"name": "brmesolve", "color": "#043c6b", "index": 1}]}, {"name": "solver_base", "color": "#043c6b", "index": 1, "children": [{"name": "Solver", "color": "#043c6b", "index": 1}]}, {"name": "integrator", "color": "#043c6b", "index": 1, "children": [{"name": "Integrator", "color": "#043c6b", "index": 1}, {"name": "IntegratorException", "color": "#043c6b", "index": 1}]}, {"name": "ode", "color": "#043c6b", "index": 1, "children": [{"name": "qutip_integrator", "color": "#043c6b", "index": 1, "children": [{"name": "IntegratorDiag", "color": "#043c6b", "index": 1}, {"name": "IntegratorVern", "color": "#043c6b", "index": 1}]}, {"name": "explicit_rk", "color": "#043c6b", "index": 1, "children": [{"name": "Explicit_RungeKutta", "color": "#043c6b", "index": 1}]}, {"name": "scipy_integrator", "color": "#043c6b", "index": 1, "children": [{"name": "IntegratorScipyDop853", "color": "#043c6b", "index": 1}, {"name": "IntegratorScipyZvode", "color": "#043c6b", "index": 1}, {"name": "IntegratorScipylsoda", "color": "#043c6b", "index": 1}]}, {"name": "verner7efficient", "color": "#043c6b", "index": 1}, {"name": "verner9efficient", "color": "#043c6b", "index": 1}]}, {"name": "options", "color": "#043c6b", "index": 1, "children": [{"name": "McOptions", "color": "#043c6b", "index": 1}, {"name": "SolverOdeOptions", "color": "#043c6b", "index": 1}, {"name": "SolverOptions", "color": "#043c6b", "index": 1}, {"name": "SolverResultsOptions", "color": "#043c6b", "index": 1}]}, {"name": "result", "color": "#043c6b", "index": 1, "children": [{"name": "ExpectOp", "color": "#043c6b", "index": 1}, {"name": "MultiTrajResult", "color": "#043c6b", "index": 1}, {"name": "MultiTrajResultAveraged", "color": "#043c6b", "index": 1}, {"name": "Result", "color": "#043c6b", "index": 1}]}, {"name": "propagator", "color": "#043c6b", "index": 1, "children": [{"name": "Propagator", "color": "#043c6b", "index": 1}, {"name": "propagator", "color": "#043c6b", "index": 1}, {"name": "propagator_steadystate", "color": "#043c6b", "index": 1}]}, {"name": "mesolve", "color": "#043c6b", "index": 1, "children": [{"name": "MeSolver", "color": "#043c6b", "index": 1}, {"name": "mesolve", "color": "#043c6b", "index": 1}]}, {"name": "sesolve", "color": "#043c6b", "index": 1, "children": [{"name": "SeSolver", "color": "#043c6b", "index": 1}, {"name": "sesolve", "color": "#043c6b", "index": 1}]}, {"name": "scattering", "color": "#043c6b", "index": 1, "children": [{"name": "photon_scattering_amplitude", "color": "#043c6b", "index": 1}, {"name": "scattering_probability", "color": "#043c6b", "index": 1}, {"name": "set_partition", "color": "#043c6b", "index": 1}, {"name": "temporal_basis_vector", "color": "#043c6b", "index": 1}, {"name": "temporal_scattered_state", "color": "#043c6b", "index": 1}]}]}, {"name": "settings", "color": "#043c6b", "index": 1, "children": [{"name": "Settings", "color": "#043c6b", "index": 1}, {"name": "available_cpu_count", "color": "#043c6b", "index": 1}]}, {"name": "bloch", "color": "#3f8fd2", "index": 2, "children": [{"name": "Arrow3D", "color": "#3f8fd2", "index": 2}, {"name": "Bloch", "color": "#3f8fd2", "index": 2}]}, {"name": "bloch3d", "color": "#3f8fd2", "index": 2, "children": [{"name": "Bloch3d", "color": "#3f8fd2", "index": 2}]}, {"name": "distributions", "color": "#3f8fd2", "index": 2, "children": [{"name": "Distribution", "color": "#3f8fd2", "index": 2}, {"name": "HarmonicOscillatorProbabilityFunction", "color": "#3f8fd2", "index": 2}, {"name": "HarmonicOscillatorWaveFunction", "color": "#3f8fd2", "index": 2}, {"name": "QDistribution", "color": "#3f8fd2", "index": 2}, {"name": "TwoModeQuadratureCorrelation", "color": "#3f8fd2", "index": 2}, {"name": "WignerDistribution", "color": "#3f8fd2", "index": 2}]}, {"name": "wigner", "color": "#3f8fd2", "index": 2, "children": [{"name": "QFunc", "color": "#3f8fd2", "index": 2}, {"name": "qfunc", "color": "#3f8fd2", "index": 2}, {"name": "spin_q_function", "color": "#3f8fd2", "index": 2}, {"name": "spin_wigner", "color": "#3f8fd2", "index": 2}, {"name": "wigner", "color": "#3f8fd2", "index": 2}, {"name": "wigner_transform", "color": "#3f8fd2", "index": 2}]}, {"name": "visualization", "color": "#3f8fd2", "index": 2, "children": [{"name": "complex_array_to_rgb", "color": "#3f8fd2", "index": 2}, {"name": "energy_level_diagram", "color": "#3f8fd2", "index": 2}, {"name": "fock_distribution", "color": "#3f8fd2", "index": 2}, {"name": "hinton", "color": "#3f8fd2", "index": 2}, {"name": "matrix_histogram", "color": "#3f8fd2", "index": 2}, {"name": "matrix_histogram_complex", "color": "#3f8fd2", "index": 2}, {"name": "plot_energy_levels", "color": "#3f8fd2", "index": 2}, {"name": "plot_expectation_values", "color": "#3f8fd2", "index": 2}, {"name": "plot_fock_distribution", "color": "#3f8fd2", "index": 2}, {"name": "plot_qubism", "color": "#3f8fd2", "index": 2}, {"name": "plot_schmidt", "color": "#3f8fd2", "index": 2}, {"name": "plot_spin_distribution_2d", "color": "#3f8fd2", "index": 2}, {"name": "plot_spin_distribution_3d", "color": "#3f8fd2", "index": 2}, {"name": "plot_wigner", "color": "#3f8fd2", "index": 2}, {"name": "plot_wigner_fock_distribution", "color": "#3f8fd2", "index": 2}, {"name": "plot_wigner_sphere", "color": "#3f8fd2", "index": 2}, {"name": "sphereplot", "color": "#3f8fd2", "index": 2}, {"name": "wigner_fock_distribution", "color": "#3f8fd2", "index": 2}]}, {"name": "orbital", "color": "#3f8fd2", "index": 2, "children": [{"name": "orbital", "color": "#3f8fd2", "index": 2}]}, {"name": "tomography", "color": "#3f8fd2", "index": 2, "children": [{"name": "qpt", "color": "#3f8fd2", "index": 2}, {"name": "qpt_plot", "color": "#3f8fd2", "index": 2}, {"name": "qpt_plot_combined", "color": "#3f8fd2", "index": 2}]}, {"name": "continuous_variables", "color": "#007143", "index": 4, "children": [{"name": "correlation_matrix", "color": "#007143", "index": 4}, {"name": "correlation_matrix_field", "color": "#007143", "index": 4}, {"name": "correlation_matrix_quadrature", "color": "#007143", "index": 4}, {"name": "covariance_matrix", "color": "#007143", "index": 4}, {"name": "logarithmic_negativity", "color": "#007143", "index": 4}, {"name": "wigner_covariance_matrix", "color": "#007143", "index": 4}]}, {"name": "random_objects", "color": "#007143", "index": 4, "children": [{"name": "rand_dm", "color": "#007143", "index": 4}, {"name": "rand_dm_ginibre", "color": "#007143", "index": 4}, {"name": "rand_dm_hs", "color": "#007143", "index": 4}, {"name": "rand_herm", "color": "#007143", "index": 4}, {"name": "rand_jacobi_rotation", "color": "#007143", "index": 4}, {"name": "rand_ket", "color": "#007143", "index": 4}, {"name": "rand_ket_haar", "color": "#007143", "index": 4}, {"name": "rand_kraus_map", "color": "#007143", "index": 4}, {"name": "rand_stochastic", "color": "#007143", "index": 4}, {"name": "rand_super", "color": "#007143", "index": 4}, {"name": "rand_super_bcsz", "color": "#007143", "index": 4}, {"name": "rand_unitary", "color": "#007143", "index": 4}, {"name": "rand_unitary_haar", "color": "#007143", "index": 4}]}, {"name": "three_level_atom", "color": "#007143", "index": 4, "children": [{"name": "three_level_basis", "color": "#007143", "index": 4}, {"name": "three_level_ops", "color": "#007143", "index": 4}]}, {"name": "entropy", "color": "#ff4500", "index": 6, "children": [{"name": "concurrence", "color": "#ff4500", "index": 6}, {"name": "entangling_power", "color": "#ff4500", "index": 6}, {"name": "entropy_conditional", "color": "#ff4500", "index": 6}, {"name": "entropy_linear", "color": "#ff4500", "index": 6}, {"name": "entropy_mutual", "color": "#ff4500", "index": 6}, {"name": "entropy_relative", "color": "#ff4500", "index": 6}, {"name": "entropy_vn", "color": "#ff4500", "index": 6}, {"name": "negativity", "color": "#ff4500", "index": 6}, {"name": "participation_ratio", "color": "#ff4500", "index": 6}]}, {"name": "partial_transpose", "color": "#692102", "index": 7, "children": [{"name": "partial_transpose", "color": "#692102", "index": 7}]}, {"name": "ui", "color": "#bf5730", "index": 8, "children": [{"name": "progressbar", "color": "#bf5730", "index": 8, "children": [{"name": "BaseProgressBar", "color": "#bf5730", "index": 8}, {"name": "EnhancedTextProgressBar", "color": "#bf5730", "index": 8}, {"name": "TextProgressBar", "color": "#bf5730", "index": 8}, {"name": "TqdmProgressBar", "color": "#bf5730", "index": 8}]}]}, {"name": "parallel", "color": "#bf5730", "index": 8, "children": [{"name": "parallel_map", "color": "#bf5730", "index": 8}, {"name": "parfor", "color": "#bf5730", "index": 8}, {"name": "serial_map", "color": "#bf5730", "index": 8}]}, {"name": "utilities", "color": "#bf5730", "index": 8, "children": [{"name": "clebsch", "color": "#bf5730", "index": 8}, {"name": "convert_GHz_to_mK", "color": "#bf5730", "index": 8}, {"name": "convert_GHz_to_meV", "color": "#bf5730", "index": 8}, {"name": "convert_J_to_meV", "color": "#bf5730", "index": 8}, {"name": "convert_mK_to_GHz", "color": "#bf5730", "index": 8}, {"name": "convert_mK_to_meV", "color": "#bf5730", "index": 8}, {"name": "convert_meV_to_GHz", "color": "#bf5730", "index": 8}, {"name": "convert_meV_to_J", "color": "#bf5730", "index": 8}, {"name": "convert_meV_to_mK", "color": "#bf5730", "index": 8}, {"name": "convert_unit", "color": "#bf5730", "index": 8}, {"name": "n_thermal", "color": "#bf5730", "index": 8}]}, {"name": "about", "color": "#bf5730", "index": 8, "children": [{"name": "about", "color": "#bf5730", "index": 8}]}, {"name": "cite", "color": "#bf5730", "index": 8, "children": [{"name": "cite", "color": "#bf5730", "index": 8}]}, {"name": "fileio", "color": "#bf5730", "index": 8, "children": [{"name": "file_data_read", "color": "#bf5730", "index": 8}, {"name": "file_data_store", "color": "#bf5730", "index": 8}, {"name": "qload", "color": "#bf5730", "index": 8}, {"name": "qsave", "color": "#bf5730", "index": 8}]}, {"name": "simdiag", "color": "#bf5730", "index": 8, "children": [{"name": "simdiag", "color": "#bf5730", "index": 8}]}, {"name": "version", "color": "#bf5730", "index": 8}]} \ No newline at end of file +{"name": "QuTiP", "color": "black", "index": -1, "children": [{"name": "core", "color": "black", "index": -1, "children": [{"name": "cy", "color": "black", "index": -1, "children": [{"name": "coefficient", "color": "black", "index": -1, "children": [{"name": "Coefficient", "color": "black", "index": -1}, {"name": "ConjCoefficient", "color": "black", "index": -1}, {"name": "FunctionCoefficient", "color": "black", "index": -1}, {"name": "InterCoefficient", "color": "black", "index": -1}, {"name": "MulCoefficient", "color": "black", "index": -1}, {"name": "NormCoefficient", "color": "black", "index": -1}, {"name": "ShiftCoefficient", "color": "black", "index": -1}, {"name": "StrFunctionCoefficient", "color": "black", "index": -1}, {"name": "SumCoefficient", "color": "black", "index": -1}, {"name": "coefficient_function_parameters", "color": "black", "index": -1}, {"name": "proj", "color": "black", "index": -1}]}, {"name": "_element", "color": "black", "index": -1}, {"name": "qobjevo", "color": "black", "index": -1, "children": [{"name": "QobjEvo", "color": "black", "index": -1}]}]}, {"name": "data", "color": "black", "index": -1, "children": [{"name": "csr", "color": "black", "index": -1, "children": [{"name": "CSR", "color": "black", "index": -1}, {"name": "Sorter", "color": "black", "index": -1}, {"name": "copy_structure", "color": "black", "index": -1}, {"name": "diags", "color": "black", "index": -1}, {"name": "empty", "color": "black", "index": -1}, {"name": "empty_like", "color": "black", "index": -1}, {"name": "fast_from_scipy", "color": "black", "index": -1}, {"name": "from_dense", "color": "black", "index": -1}, {"name": "identity", "color": "black", "index": -1}, {"name": "nnz", "color": "black", "index": -1}, {"name": "sorted", "color": "black", "index": -1}, {"name": "zeros", "color": "black", "index": -1}]}, {"name": "base", "color": "black", "index": -1, "children": [{"name": "Data", "color": "black", "index": -1}, {"name": "EfficiencyWarning", "color": "black", "index": -1}]}, {"name": "dense", "color": "black", "index": -1, "children": [{"name": "Dense", "color": "black", "index": -1}, {"name": "OrderEfficiencyWarning", "color": "black", "index": -1}, {"name": "diags", "color": "black", "index": -1}, {"name": "empty", "color": "black", "index": -1}, {"name": "empty_like", "color": "black", "index": -1}, {"name": "fast_from_numpy", "color": "black", "index": -1}, {"name": "from_csr", "color": "black", "index": -1}, {"name": "identity", "color": "black", "index": -1}, {"name": "zeros", "color": "black", "index": -1}]}, {"name": "dispatch", "color": "black", "index": -1, "children": [{"name": "Dispatcher", "color": "black", "index": -1}]}, {"name": "add", "color": "black", "index": -1, "children": [{"name": "add_csr", "color": "black", "index": -1}, {"name": "add_dense", "color": "black", "index": -1}, {"name": "iadd_dense", "color": "black", "index": -1}, {"name": "sub_csr", "color": "black", "index": -1}, {"name": "sub_dense", "color": "black", "index": -1}]}, {"name": "adjoint", "color": "black", "index": -1, "children": [{"name": "adjoint_csr", "color": "black", "index": -1}, {"name": "adjoint_dense", "color": "black", "index": -1}, {"name": "conj_csr", "color": "black", "index": -1}, {"name": "conj_dense", "color": "black", "index": -1}, {"name": "transpose_csr", "color": "black", "index": -1}, {"name": "transpose_dense", "color": "black", "index": -1}]}, {"name": "reshape", "color": "black", "index": -1, "children": [{"name": "column_stack_csr", "color": "black", "index": -1}, {"name": "column_stack_dense", "color": "black", "index": -1}, {"name": "column_unstack_csr", "color": "black", "index": -1}, {"name": "column_unstack_dense", "color": "black", "index": -1}, {"name": "reshape_csr", "color": "black", "index": -1}, {"name": "reshape_dense", "color": "black", "index": -1}, {"name": "split_columns_csr", "color": "black", "index": -1}, {"name": "split_columns_dense", "color": "black", "index": -1}]}, {"name": "constant", "color": "black", "index": -1}, {"name": "convert", "color": "black", "index": -1}, {"name": "eigen", "color": "black", "index": -1, "children": [{"name": "eigs_csr", "color": "black", "index": -1}, {"name": "eigs_dense", "color": "black", "index": -1}]}, {"name": "expect", "color": "black", "index": -1, "children": [{"name": "expect_csr", "color": "black", "index": -1}, {"name": "expect_csr_dense", "color": "black", "index": -1}, {"name": "expect_dense", "color": "black", "index": -1}, {"name": "expect_super_csr", "color": "black", "index": -1}, {"name": "expect_super_csr_dense", "color": "black", "index": -1}, {"name": "expect_super_dense", "color": "black", "index": -1}]}, {"name": "expm", "color": "black", "index": -1, "children": [{"name": "expm_csr", "color": "black", "index": -1}, {"name": "expm_csr_dense", "color": "black", "index": -1}]}, {"name": "properties", "color": "black", "index": -1, "children": [{"name": "isdiag_csr", "color": "black", "index": -1}, {"name": "isherm_csr", "color": "black", "index": -1}, {"name": "iszero_csr", "color": "black", "index": -1}, {"name": "iszero_dense", "color": "black", "index": -1}]}, {"name": "mul", "color": "black", "index": -1, "children": [{"name": "imul_csr", "color": "black", "index": -1}, {"name": "imul_data", "color": "black", "index": -1}, {"name": "imul_dense", "color": "black", "index": -1}, {"name": "mul_csr", "color": "black", "index": -1}, {"name": "mul_dense", "color": "black", "index": -1}, {"name": "neg_csr", "color": "black", "index": -1}, {"name": "neg_dense", "color": "black", "index": -1}]}, {"name": "inner", "color": "black", "index": -1, "children": [{"name": "inner_csr", "color": "black", "index": -1}, {"name": "inner_op_csr", "color": "black", "index": -1}]}, {"name": "linalg", "color": "black", "index": -1, "children": [{"name": "inv_csr", "color": "black", "index": -1}, {"name": "inv_dense", "color": "black", "index": -1}]}, {"name": "kron", "color": "black", "index": -1, "children": [{"name": "kron_csr", "color": "black", "index": -1}, {"name": "kron_dense", "color": "black", "index": -1}]}, {"name": "make", "color": "black", "index": -1, "children": [{"name": "one_element_csr", "color": "black", "index": -1}, {"name": "one_element_dense", "color": "black", "index": -1}]}, {"name": "matmul", "color": "black", "index": -1, "children": [{"name": "matmul_csr", "color": "black", "index": -1}, {"name": "matmul_csr_dense_dense", "color": "black", "index": -1}, {"name": "matmul_dense", "color": "black", "index": -1}, {"name": "multiply_csr", "color": "black", "index": -1}, {"name": "multiply_dense", "color": "black", "index": -1}]}, {"name": "norm", "color": "black", "index": -1, "children": [{"name": "frobenius_csr", "color": "black", "index": -1}, {"name": "frobenius_data", "color": "black", "index": -1}, {"name": "frobenius_dense", "color": "black", "index": -1}, {"name": "l2_csr", "color": "black", "index": -1}, {"name": "l2_dense", "color": "black", "index": -1}, {"name": "max_csr", "color": "black", "index": -1}, {"name": "max_dense", "color": "black", "index": -1}, {"name": "one_csr", "color": "black", "index": -1}, {"name": "one_dense", "color": "black", "index": -1}, {"name": "trace_csr", "color": "black", "index": -1}, {"name": "trace_dense", "color": "black", "index": -1}]}, {"name": "permute", "color": "black", "index": -1, "children": [{"name": "dimensions_csr", "color": "black", "index": -1}, {"name": "indices_csr", "color": "black", "index": -1}]}, {"name": "pow", "color": "black", "index": -1, "children": [{"name": "pow_csr", "color": "black", "index": -1}]}, {"name": "project", "color": "black", "index": -1, "children": [{"name": "project_csr", "color": "black", "index": -1}, {"name": "project_dense", "color": "black", "index": -1}]}, {"name": "ptrace", "color": "black", "index": -1, "children": [{"name": "ptrace_csr", "color": "black", "index": -1}, {"name": "ptrace_csr_dense", "color": "black", "index": -1}, {"name": "ptrace_dense", "color": "black", "index": -1}]}, {"name": "tidyup", "color": "black", "index": -1, "children": [{"name": "tidyup_csr", "color": "black", "index": -1}, {"name": "tidyup_dense", "color": "black", "index": -1}]}, {"name": "trace", "color": "black", "index": -1, "children": [{"name": "trace_csr", "color": "black", "index": -1}, {"name": "trace_dense", "color": "black", "index": -1}]}]}, {"name": "coefficient", "color": "black", "index": -1, "children": [{"name": "CompilationOptions", "color": "black", "index": -1}, {"name": "StringParsingWarning", "color": "black", "index": -1}, {"name": "clean_compiled_coefficient", "color": "black", "index": -1}, {"name": "coeff_from_str", "color": "black", "index": -1}, {"name": "coefficient", "color": "black", "index": -1}, {"name": "compileType", "color": "black", "index": -1}, {"name": "compile_code", "color": "black", "index": -1}, {"name": "conj", "color": "black", "index": -1}, {"name": "extract_constant", "color": "black", "index": -1}, {"name": "extract_cte_pattern", "color": "black", "index": -1}, {"name": "find_type_from_str", "color": "black", "index": -1}, {"name": "fix_type", "color": "black", "index": -1}, {"name": "fromstr", "color": "black", "index": -1}, {"name": "make_cy_code", "color": "black", "index": -1}, {"name": "norm", "color": "black", "index": -1}, {"name": "parse", "color": "black", "index": -1}, {"name": "proj", "color": "black", "index": -1}, {"name": "shift", "color": "black", "index": -1}, {"name": "space_parts", "color": "black", "index": -1}, {"name": "test_parsed", "color": "black", "index": -1}, {"name": "try_import", "color": "black", "index": -1}, {"name": "try_parse", "color": "black", "index": -1}, {"name": "use_hinted_type", "color": "black", "index": -1}]}, {"name": "options", "color": "black", "index": -1, "children": [{"name": "CoreOptions", "color": "black", "index": -1}, {"name": "QutipOptions", "color": "black", "index": -1}]}, {"name": "qobj", "color": "black", "index": -1, "children": [{"name": "Qobj", "color": "black", "index": -1}, {"name": "isbra", "color": "black", "index": -1}, {"name": "isherm", "color": "black", "index": -1}, {"name": "isket", "color": "black", "index": -1}, {"name": "isoper", "color": "black", "index": -1}, {"name": "isoperbra", "color": "black", "index": -1}, {"name": "isoperket", "color": "black", "index": -1}, {"name": "issuper", "color": "black", "index": -1}, {"name": "ptrace", "color": "black", "index": -1}]}, {"name": "dimensions", "color": "black", "index": -1, "children": [{"name": "collapse_dims_oper", "color": "black", "index": -1}, {"name": "collapse_dims_super", "color": "black", "index": -1}, {"name": "deep_map", "color": "black", "index": -1}, {"name": "deep_remove", "color": "black", "index": -1}, {"name": "dims_idxs_to_tensor_idxs", "color": "black", "index": -1}, {"name": "dims_to_tensor_perm", "color": "black", "index": -1}, {"name": "dims_to_tensor_shape", "color": "black", "index": -1}, {"name": "enumerate_flat", "color": "black", "index": -1}, {"name": "flatten", "color": "black", "index": -1}, {"name": "is_scalar", "color": "black", "index": -1}, {"name": "is_vector", "color": "black", "index": -1}, {"name": "is_vectorized_oper", "color": "black", "index": -1}, {"name": "type_from_dims", "color": "black", "index": -1}, {"name": "unflatten", "color": "black", "index": -1}]}, {"name": "metrics", "color": "black", "index": -1, "children": [{"name": "average_gate_fidelity", "color": "black", "index": -1}, {"name": "bures_angle", "color": "black", "index": -1}, {"name": "bures_dist", "color": "black", "index": -1}, {"name": "dnorm", "color": "black", "index": -1}, {"name": "fidelity", "color": "black", "index": -1}, {"name": "hellinger_dist", "color": "black", "index": -1}, {"name": "hilbert_dist", "color": "black", "index": -1}, {"name": "process_fidelity", "color": "black", "index": -1}, {"name": "tracedist", "color": "black", "index": -1}, {"name": "unitarity", "color": "black", "index": -1}]}, {"name": "superop_reps", "color": "black", "index": -1, "children": [{"name": "isqubitdims", "color": "black", "index": -1}, {"name": "kraus_to_choi", "color": "black", "index": -1}, {"name": "kraus_to_super", "color": "black", "index": -1}, {"name": "to_chi", "color": "black", "index": -1}, {"name": "to_choi", "color": "black", "index": -1}, {"name": "to_kraus", "color": "black", "index": -1}, {"name": "to_stinespring", "color": "black", "index": -1}, {"name": "to_super", "color": "black", "index": -1}]}, {"name": "states", "color": "black", "index": -1, "children": [{"name": "basis", "color": "black", "index": -1}, {"name": "bell_state", "color": "black", "index": -1}, {"name": "bra", "color": "black", "index": -1}, {"name": "coherent", "color": "black", "index": -1}, {"name": "coherent_dm", "color": "black", "index": -1}, {"name": "enr_fock", "color": "black", "index": -1}, {"name": "enr_state_dictionaries", "color": "black", "index": -1}, {"name": "enr_thermal_dm", "color": "black", "index": -1}, {"name": "fock", "color": "black", "index": -1}, {"name": "fock_dm", "color": "black", "index": -1}, {"name": "ghz_state", "color": "black", "index": -1}, {"name": "ket", "color": "black", "index": -1}, {"name": "ket2dm", "color": "black", "index": -1}, {"name": "maximally_mixed_dm", "color": "black", "index": -1}, {"name": "phase_basis", "color": "black", "index": -1}, {"name": "projection", "color": "black", "index": -1}, {"name": "qstate", "color": "black", "index": -1}, {"name": "qutrit_basis", "color": "black", "index": -1}, {"name": "singlet_state", "color": "black", "index": -1}, {"name": "spin_coherent", "color": "black", "index": -1}, {"name": "spin_state", "color": "black", "index": -1}, {"name": "state_index_number", "color": "black", "index": -1}, {"name": "state_number_enumerate", "color": "black", "index": -1}, {"name": "state_number_index", "color": "black", "index": -1}, {"name": "state_number_qobj", "color": "black", "index": -1}, {"name": "thermal_dm", "color": "black", "index": -1}, {"name": "triplet_states", "color": "black", "index": -1}, {"name": "w_state", "color": "black", "index": -1}, {"name": "zero_ket", "color": "black", "index": -1}]}, {"name": "operators", "color": "black", "index": -1, "children": [{"name": "charge", "color": "black", "index": -1}, {"name": "commutator", "color": "black", "index": -1}, {"name": "create", "color": "black", "index": -1}, {"name": "destroy", "color": "black", "index": -1}, {"name": "displace", "color": "black", "index": -1}, {"name": "enr_destroy", "color": "black", "index": -1}, {"name": "enr_identity", "color": "black", "index": -1}, {"name": "qeye", "color": "black", "index": -1}, {"name": "jmat", "color": "black", "index": -1}, {"name": "momentum", "color": "black", "index": -1}, {"name": "num", "color": "black", "index": -1}, {"name": "phase", "color": "black", "index": -1}, {"name": "position", "color": "black", "index": -1}, {"name": "qdiags", "color": "black", "index": -1}, {"name": "qft", "color": "black", "index": -1}, {"name": "qutrit_ops", "color": "black", "index": -1}, {"name": "qzero", "color": "black", "index": -1}, {"name": "sigmam", "color": "black", "index": -1}, {"name": "sigmap", "color": "black", "index": -1}, {"name": "sigmax", "color": "black", "index": -1}, {"name": "sigmay", "color": "black", "index": -1}, {"name": "sigmaz", "color": "black", "index": -1}, {"name": "spin_J_set", "color": "black", "index": -1}, {"name": "spin_Jm", "color": "black", "index": -1}, {"name": "spin_Jp", "color": "black", "index": -1}, {"name": "spin_Jx", "color": "black", "index": -1}, {"name": "spin_Jy", "color": "black", "index": -1}, {"name": "spin_Jz", "color": "black", "index": -1}, {"name": "squeeze", "color": "black", "index": -1}, {"name": "squeezing", "color": "black", "index": -1}, {"name": "tunneling", "color": "black", "index": -1}]}, {"name": "tensor", "color": "black", "index": -1, "children": [{"name": "composite", "color": "black", "index": -1}, {"name": "expand_operator", "color": "black", "index": -1}, {"name": "super_tensor", "color": "black", "index": -1}, {"name": "tensor", "color": "black", "index": -1}, {"name": "tensor_contract", "color": "black", "index": -1}, {"name": "tensor_swap", "color": "black", "index": -1}]}, {"name": "superoperator", "color": "black", "index": -1, "children": [{"name": "lindblad_dissipator", "color": "black", "index": -1}, {"name": "liouvillian", "color": "black", "index": -1}, {"name": "operator_to_vector", "color": "black", "index": -1}, {"name": "reshuffle", "color": "black", "index": -1}, {"name": "spost", "color": "black", "index": -1}, {"name": "spre", "color": "black", "index": -1}, {"name": "sprepost", "color": "black", "index": -1}, {"name": "stack_columns", "color": "black", "index": -1}, {"name": "stacked_index", "color": "black", "index": -1}, {"name": "unstack_columns", "color": "black", "index": -1}, {"name": "unstacked_index", "color": "black", "index": -1}, {"name": "vector_to_operator", "color": "black", "index": -1}]}, {"name": "semidefinite", "color": "black", "index": -1, "children": [{"name": "Complex", "color": "black", "index": -1}, {"name": "bmat", "color": "black", "index": -1}, {"name": "complex_var", "color": "black", "index": -1}, {"name": "conj", "color": "black", "index": -1}, {"name": "dag", "color": "black", "index": -1}, {"name": "dens", "color": "black", "index": -1}, {"name": "dnorm_problem", "color": "black", "index": -1}, {"name": "dnorm_sparse_problem", "color": "black", "index": -1}, {"name": "herm", "color": "black", "index": -1}, {"name": "initialize_constraints_on_dnorm_problem", "color": "black", "index": -1}, {"name": "kron", "color": "black", "index": -1}, {"name": "memoize", "color": "black", "index": -1}, {"name": "pos", "color": "black", "index": -1}, {"name": "pos_noherm", "color": "black", "index": -1}, {"name": "qudit_swap", "color": "black", "index": -1}]}, {"name": "_brtensor", "color": "black", "index": -1}, {"name": "_brtools", "color": "black", "index": -1, "children": [{"name": "SpectraCoefficient", "color": "black", "index": -1}, {"name": "matmul_var_data", "color": "black", "index": -1}]}, {"name": "blochredfield", "color": "black", "index": -1, "children": [{"name": "bloch_redfield_tensor", "color": "black", "index": -1}, {"name": "brterm", "color": "black", "index": -1}]}, {"name": "expect", "color": "black", "index": -1, "children": [{"name": "expect", "color": "black", "index": -1}, {"name": "variance", "color": "black", "index": -1}]}, {"name": "gates", "color": "black", "index": -1, "children": [{"name": "berkeley", "color": "black", "index": -1}, {"name": "cnot", "color": "black", "index": -1}, {"name": "cphase", "color": "black", "index": -1}, {"name": "cs_gate", "color": "black", "index": -1}, {"name": "csign", "color": "black", "index": -1}, {"name": "ct_gate", "color": "black", "index": -1}, {"name": "cy_gate", "color": "black", "index": -1}, {"name": "cz_gate", "color": "black", "index": -1}, {"name": "fredkin", "color": "black", "index": -1}, {"name": "globalphase", "color": "black", "index": -1}, {"name": "hadamard_transform", "color": "black", "index": -1}, {"name": "iswap", "color": "black", "index": -1}, {"name": "molmer_sorensen", "color": "black", "index": -1}, {"name": "phasegate", "color": "black", "index": -1}, {"name": "qrot", "color": "black", "index": -1}, {"name": "qubit_clifford_group", "color": "black", "index": -1}, {"name": "rx", "color": "black", "index": -1}, {"name": "ry", "color": "black", "index": -1}, {"name": "rz", "color": "black", "index": -1}, {"name": "s_gate", "color": "black", "index": -1}, {"name": "snot", "color": "black", "index": -1}, {"name": "sqrtiswap", "color": "black", "index": -1}, {"name": "sqrtnot", "color": "black", "index": -1}, {"name": "sqrtswap", "color": "black", "index": -1}, {"name": "swap", "color": "black", "index": -1}, {"name": "swapalpha", "color": "black", "index": -1}, {"name": "t_gate", "color": "black", "index": -1}, {"name": "toffoli", "color": "black", "index": -1}]}, {"name": "subsystem_apply", "color": "black", "index": -1, "children": [{"name": "subsystem_apply", "color": "black", "index": -1}]}]}, {"name": "optionsclass", "color": "black", "index": -1, "children": [{"name": "AllOptions", "color": "black", "index": -1}, {"name": "optionsclass", "color": "black", "index": -1}]}, {"name": "solve", "color": "black", "index": -1, "children": [{"name": "piqs", "color": "black", "index": -1, "children": [{"name": "Dicke", "color": "black", "index": -1}, {"name": "Pim", "color": "black", "index": -1}, {"name": "am", "color": "black", "index": -1}, {"name": "ap", "color": "black", "index": -1}, {"name": "block_matrix", "color": "black", "index": -1}, {"name": "collapse_uncoupled", "color": "black", "index": -1}, {"name": "css", "color": "black", "index": -1}, {"name": "dicke", "color": "black", "index": -1}, {"name": "dicke_basis", "color": "black", "index": -1}, {"name": "dicke_blocks", "color": "black", "index": -1}, {"name": "dicke_blocks_full", "color": "black", "index": -1}, {"name": "dicke_function_trace", "color": "black", "index": -1}, {"name": "energy_degeneracy", "color": "black", "index": -1}, {"name": "entropy_vn_dicke", "color": "black", "index": -1}, {"name": "excited", "color": "black", "index": -1}, {"name": "ghz", "color": "black", "index": -1}, {"name": "ground", "color": "black", "index": -1}, {"name": "identity_uncoupled", "color": "black", "index": -1}, {"name": "isdiagonal", "color": "black", "index": -1}, {"name": "jspin", "color": "black", "index": -1}, {"name": "m_degeneracy", "color": "black", "index": -1}, {"name": "num_dicke_ladders", "color": "black", "index": -1}, {"name": "num_dicke_states", "color": "black", "index": -1}, {"name": "num_tls", "color": "black", "index": -1}, {"name": "purity_dicke", "color": "black", "index": -1}, {"name": "spin_algebra", "color": "black", "index": -1}, {"name": "state_degeneracy", "color": "black", "index": -1}, {"name": "superradiant", "color": "black", "index": -1}, {"name": "tau_column", "color": "black", "index": -1}]}, {"name": "solver", "color": "black", "index": -1, "children": [{"name": "ExpectOps", "color": "black", "index": -1}, {"name": "McOptions", "color": "black", "index": -1}, {"name": "Result", "color": "black", "index": -1}, {"name": "SolverConfiguration", "color": "black", "index": -1}, {"name": "SolverOptions", "color": "black", "index": -1}, {"name": "SolverSystem", "color": "black", "index": -1}, {"name": "Stats", "color": "black", "index": -1}]}, {"name": "pdpsolve", "color": "black", "index": -1, "children": [{"name": "StochasticSolverOptions", "color": "black", "index": -1}, {"name": "main_smepdpsolve", "color": "black", "index": -1}, {"name": "main_ssepdpsolve", "color": "black", "index": -1}]}, {"name": "_mcsolve", "color": "black", "index": -1, "children": [{"name": "CyMcOde", "color": "black", "index": -1}, {"name": "CyMcOdeDiag", "color": "black", "index": -1}]}, {"name": "_piqs", "color": "black", "index": -1, "children": [{"name": "Dicke", "color": "black", "index": -1}, {"name": "get_blocks", "color": "black", "index": -1}, {"name": "get_index", "color": "black", "index": -1}, {"name": "j_min", "color": "black", "index": -1}, {"name": "j_vals", "color": "black", "index": -1}, {"name": "jmm1_dictionary", "color": "black", "index": -1}, {"name": "m_vals", "color": "black", "index": -1}]}, {"name": "_steadystate", "color": "black", "index": -1, "children": [{"name": "weighted_bipartite_matching", "color": "black", "index": -1}]}, {"name": "_stochastic", "color": "black", "index": -1, "children": [{"name": "GenericSSolver", "color": "black", "index": -1}, {"name": "PcSMESolver", "color": "black", "index": -1}, {"name": "PcSSESolver", "color": "black", "index": -1}, {"name": "PmSMESolver", "color": "black", "index": -1}, {"name": "SMESolver", "color": "black", "index": -1}, {"name": "SSESolver", "color": "black", "index": -1}, {"name": "StochasticSolver", "color": "black", "index": -1}, {"name": "TaylorNoise", "color": "black", "index": -1}, {"name": "normalize_inplace", "color": "black", "index": -1}]}, {"name": "steadystate", "color": "black", "index": -1, "children": [{"name": "build_preconditioner", "color": "black", "index": -1}, {"name": "pseudo_inverse", "color": "black", "index": -1}, {"name": "steadystate", "color": "black", "index": -1}, {"name": "steadystate_floquet", "color": "black", "index": -1}]}, {"name": "correlation", "color": "black", "index": -1, "children": [{"name": "coherence_function_g1", "color": "black", "index": -1}, {"name": "coherence_function_g2", "color": "black", "index": -1}, {"name": "correlation_2op_1t", "color": "black", "index": -1}, {"name": "correlation_2op_2t", "color": "black", "index": -1}, {"name": "correlation_3op_1t", "color": "black", "index": -1}, {"name": "correlation_3op_2t", "color": "black", "index": -1}, {"name": "spectrum", "color": "black", "index": -1}, {"name": "spectrum_correlation_fft", "color": "black", "index": -1}]}, {"name": "mcsolve", "color": "black", "index": -1, "children": [{"name": "mcsolve", "color": "black", "index": -1}, {"name": "qutip_zvode", "color": "black", "index": -1}]}, {"name": "sesolve", "color": "black", "index": -1, "children": [{"name": "sesolve", "color": "black", "index": -1}]}, {"name": "mesolve", "color": "black", "index": -1, "children": [{"name": "mesolve", "color": "black", "index": -1}]}, {"name": "countstat", "color": "black", "index": -1, "children": [{"name": "countstat_current", "color": "black", "index": -1}, {"name": "countstat_current_noise", "color": "black", "index": -1}]}, {"name": "floquet", "color": "black", "index": -1, "children": [{"name": "floquet_basis_transform", "color": "black", "index": -1}, {"name": "floquet_collapse_operators", "color": "black", "index": -1}, {"name": "floquet_markov_mesolve", "color": "black", "index": -1}, {"name": "floquet_master_equation_rates", "color": "black", "index": -1}, {"name": "floquet_master_equation_steadystate", "color": "black", "index": -1}, {"name": "floquet_master_equation_tensor", "color": "black", "index": -1}, {"name": "floquet_modes", "color": "black", "index": -1}, {"name": "floquet_modes_t", "color": "black", "index": -1}, {"name": "floquet_modes_t_lookup", "color": "black", "index": -1}, {"name": "floquet_modes_table", "color": "black", "index": -1}, {"name": "floquet_state_decomposition", "color": "black", "index": -1}, {"name": "floquet_states", "color": "black", "index": -1}, {"name": "floquet_states_t", "color": "black", "index": -1}, {"name": "floquet_wavefunction", "color": "black", "index": -1}, {"name": "floquet_wavefunction_t", "color": "black", "index": -1}, {"name": "fmmesolve", "color": "black", "index": -1}, {"name": "fsesolve", "color": "black", "index": -1}]}, {"name": "stochastic", "color": "black", "index": -1, "children": [{"name": "StochasticSolverOptions", "color": "black", "index": -1}, {"name": "StochasticSolverOptionsPhoto", "color": "black", "index": -1}, {"name": "general_stochastic", "color": "black", "index": -1}, {"name": "photocurrent_mesolve", "color": "black", "index": -1}, {"name": "photocurrent_sesolve", "color": "black", "index": -1}, {"name": "smepdpsolve", "color": "black", "index": -1}, {"name": "smesolve", "color": "black", "index": -1}, {"name": "ssepdpsolve", "color": "black", "index": -1}, {"name": "ssesolve", "color": "black", "index": -1}, {"name": "stochastic_solvers", "color": "black", "index": -1}]}, {"name": "krylovsolve", "color": "black", "index": -1, "children": [{"name": "krylovsolve", "color": "black", "index": -1}, {"name": "lanczos_algorithm", "color": "black", "index": -1}, {"name": "particular_tlist_or_happy_breakdown", "color": "black", "index": -1}]}, {"name": "nonmarkov", "color": "black", "index": -1}, {"name": "rcsolve", "color": "black", "index": -1, "children": [{"name": "rcsolve", "color": "black", "index": -1}]}]}, {"name": "solver", "color": "#043c6b", "index": 1, "children": [{"name": "brmesolve", "color": "#043c6b", "index": 1, "children": [{"name": "BRSolver", "color": "#043c6b", "index": 1}, {"name": "brmesolve", "color": "#043c6b", "index": 1}]}, {"name": "solver_base", "color": "#043c6b", "index": 1, "children": [{"name": "Solver", "color": "#043c6b", "index": 1}]}, {"name": "integrator", "color": "#043c6b", "index": 1, "children": [{"name": "Integrator", "color": "#043c6b", "index": 1}, {"name": "IntegratorException", "color": "#043c6b", "index": 1}]}, {"name": "ode", "color": "#043c6b", "index": 1, "children": [{"name": "qutip_integrator", "color": "#043c6b", "index": 1, "children": [{"name": "IntegratorDiag", "color": "#043c6b", "index": 1}, {"name": "IntegratorVern", "color": "#043c6b", "index": 1}]}, {"name": "explicit_rk", "color": "#043c6b", "index": 1, "children": [{"name": "Explicit_RungeKutta", "color": "#043c6b", "index": 1}]}, {"name": "scipy_integrator", "color": "#043c6b", "index": 1, "children": [{"name": "IntegratorScipyDop853", "color": "#043c6b", "index": 1}, {"name": "IntegratorScipyZvode", "color": "#043c6b", "index": 1}, {"name": "IntegratorScipylsoda", "color": "#043c6b", "index": 1}]}, {"name": "verner7efficient", "color": "#043c6b", "index": 1}, {"name": "verner9efficient", "color": "#043c6b", "index": 1}]}, {"name": "options", "color": "#043c6b", "index": 1, "children": [{"name": "McOptions", "color": "#043c6b", "index": 1}, {"name": "SolverOdeOptions", "color": "#043c6b", "index": 1}, {"name": "SolverOptions", "color": "#043c6b", "index": 1}, {"name": "SolverResultsOptions", "color": "#043c6b", "index": 1}]}, {"name": "result", "color": "#043c6b", "index": 1, "children": [{"name": "ExpectOp", "color": "#043c6b", "index": 1}, {"name": "MultiTrajResult", "color": "#043c6b", "index": 1}, {"name": "MultiTrajResultAveraged", "color": "#043c6b", "index": 1}, {"name": "Result", "color": "#043c6b", "index": 1}]}, {"name": "propagator", "color": "#043c6b", "index": 1, "children": [{"name": "Propagator", "color": "#043c6b", "index": 1}, {"name": "propagator", "color": "#043c6b", "index": 1}, {"name": "propagator_steadystate", "color": "#043c6b", "index": 1}]}, {"name": "mesolve", "color": "#043c6b", "index": 1, "children": [{"name": "MESolver", "color": "#043c6b", "index": 1}, {"name": "mesolve", "color": "#043c6b", "index": 1}]}, {"name": "sesolve", "color": "#043c6b", "index": 1, "children": [{"name": "SESolver", "color": "#043c6b", "index": 1}, {"name": "sesolve", "color": "#043c6b", "index": 1}]}, {"name": "scattering", "color": "#043c6b", "index": 1, "children": [{"name": "photon_scattering_amplitude", "color": "#043c6b", "index": 1}, {"name": "scattering_probability", "color": "#043c6b", "index": 1}, {"name": "set_partition", "color": "#043c6b", "index": 1}, {"name": "temporal_basis_vector", "color": "#043c6b", "index": 1}, {"name": "temporal_scattered_state", "color": "#043c6b", "index": 1}]}]}, {"name": "settings", "color": "#043c6b", "index": 1, "children": [{"name": "Settings", "color": "#043c6b", "index": 1}, {"name": "available_cpu_count", "color": "#043c6b", "index": 1}]}, {"name": "bloch", "color": "#3f8fd2", "index": 2, "children": [{"name": "Arrow3D", "color": "#3f8fd2", "index": 2}, {"name": "Bloch", "color": "#3f8fd2", "index": 2}]}, {"name": "bloch3d", "color": "#3f8fd2", "index": 2, "children": [{"name": "Bloch3d", "color": "#3f8fd2", "index": 2}]}, {"name": "distributions", "color": "#3f8fd2", "index": 2, "children": [{"name": "Distribution", "color": "#3f8fd2", "index": 2}, {"name": "HarmonicOscillatorProbabilityFunction", "color": "#3f8fd2", "index": 2}, {"name": "HarmonicOscillatorWaveFunction", "color": "#3f8fd2", "index": 2}, {"name": "QDistribution", "color": "#3f8fd2", "index": 2}, {"name": "TwoModeQuadratureCorrelation", "color": "#3f8fd2", "index": 2}, {"name": "WignerDistribution", "color": "#3f8fd2", "index": 2}]}, {"name": "wigner", "color": "#3f8fd2", "index": 2, "children": [{"name": "QFunc", "color": "#3f8fd2", "index": 2}, {"name": "qfunc", "color": "#3f8fd2", "index": 2}, {"name": "spin_q_function", "color": "#3f8fd2", "index": 2}, {"name": "spin_wigner", "color": "#3f8fd2", "index": 2}, {"name": "wigner", "color": "#3f8fd2", "index": 2}, {"name": "wigner_transform", "color": "#3f8fd2", "index": 2}]}, {"name": "visualization", "color": "#3f8fd2", "index": 2, "children": [{"name": "complex_array_to_rgb", "color": "#3f8fd2", "index": 2}, {"name": "energy_level_diagram", "color": "#3f8fd2", "index": 2}, {"name": "fock_distribution", "color": "#3f8fd2", "index": 2}, {"name": "hinton", "color": "#3f8fd2", "index": 2}, {"name": "matrix_histogram", "color": "#3f8fd2", "index": 2}, {"name": "matrix_histogram_complex", "color": "#3f8fd2", "index": 2}, {"name": "plot_energy_levels", "color": "#3f8fd2", "index": 2}, {"name": "plot_expectation_values", "color": "#3f8fd2", "index": 2}, {"name": "plot_fock_distribution", "color": "#3f8fd2", "index": 2}, {"name": "plot_qubism", "color": "#3f8fd2", "index": 2}, {"name": "plot_schmidt", "color": "#3f8fd2", "index": 2}, {"name": "plot_spin_distribution_2d", "color": "#3f8fd2", "index": 2}, {"name": "plot_spin_distribution_3d", "color": "#3f8fd2", "index": 2}, {"name": "plot_wigner", "color": "#3f8fd2", "index": 2}, {"name": "plot_wigner_fock_distribution", "color": "#3f8fd2", "index": 2}, {"name": "plot_wigner_sphere", "color": "#3f8fd2", "index": 2}, {"name": "sphereplot", "color": "#3f8fd2", "index": 2}, {"name": "wigner_fock_distribution", "color": "#3f8fd2", "index": 2}]}, {"name": "orbital", "color": "#3f8fd2", "index": 2, "children": [{"name": "orbital", "color": "#3f8fd2", "index": 2}]}, {"name": "tomography", "color": "#3f8fd2", "index": 2, "children": [{"name": "qpt", "color": "#3f8fd2", "index": 2}, {"name": "qpt_plot", "color": "#3f8fd2", "index": 2}, {"name": "qpt_plot_combined", "color": "#3f8fd2", "index": 2}]}, {"name": "continuous_variables", "color": "#007143", "index": 4, "children": [{"name": "correlation_matrix", "color": "#007143", "index": 4}, {"name": "correlation_matrix_field", "color": "#007143", "index": 4}, {"name": "correlation_matrix_quadrature", "color": "#007143", "index": 4}, {"name": "covariance_matrix", "color": "#007143", "index": 4}, {"name": "logarithmic_negativity", "color": "#007143", "index": 4}, {"name": "wigner_covariance_matrix", "color": "#007143", "index": 4}]}, {"name": "random_objects", "color": "#007143", "index": 4, "children": [{"name": "rand_dm", "color": "#007143", "index": 4}, {"name": "rand_dm_ginibre", "color": "#007143", "index": 4}, {"name": "rand_dm_hs", "color": "#007143", "index": 4}, {"name": "rand_herm", "color": "#007143", "index": 4}, {"name": "rand_jacobi_rotation", "color": "#007143", "index": 4}, {"name": "rand_ket", "color": "#007143", "index": 4}, {"name": "rand_ket_haar", "color": "#007143", "index": 4}, {"name": "rand_kraus_map", "color": "#007143", "index": 4}, {"name": "rand_stochastic", "color": "#007143", "index": 4}, {"name": "rand_super", "color": "#007143", "index": 4}, {"name": "rand_super_bcsz", "color": "#007143", "index": 4}, {"name": "rand_unitary", "color": "#007143", "index": 4}, {"name": "rand_unitary_haar", "color": "#007143", "index": 4}]}, {"name": "three_level_atom", "color": "#007143", "index": 4, "children": [{"name": "three_level_basis", "color": "#007143", "index": 4}, {"name": "three_level_ops", "color": "#007143", "index": 4}]}, {"name": "entropy", "color": "#ff4500", "index": 6, "children": [{"name": "concurrence", "color": "#ff4500", "index": 6}, {"name": "entangling_power", "color": "#ff4500", "index": 6}, {"name": "entropy_conditional", "color": "#ff4500", "index": 6}, {"name": "entropy_linear", "color": "#ff4500", "index": 6}, {"name": "entropy_mutual", "color": "#ff4500", "index": 6}, {"name": "entropy_relative", "color": "#ff4500", "index": 6}, {"name": "entropy_vn", "color": "#ff4500", "index": 6}, {"name": "negativity", "color": "#ff4500", "index": 6}, {"name": "participation_ratio", "color": "#ff4500", "index": 6}]}, {"name": "partial_transpose", "color": "#692102", "index": 7, "children": [{"name": "partial_transpose", "color": "#692102", "index": 7}]}, {"name": "ui", "color": "#bf5730", "index": 8, "children": [{"name": "progressbar", "color": "#bf5730", "index": 8, "children": [{"name": "BaseProgressBar", "color": "#bf5730", "index": 8}, {"name": "EnhancedTextProgressBar", "color": "#bf5730", "index": 8}, {"name": "TextProgressBar", "color": "#bf5730", "index": 8}, {"name": "TqdmProgressBar", "color": "#bf5730", "index": 8}]}]}, {"name": "parallel", "color": "#bf5730", "index": 8, "children": [{"name": "parallel_map", "color": "#bf5730", "index": 8}, {"name": "parfor", "color": "#bf5730", "index": 8}, {"name": "serial_map", "color": "#bf5730", "index": 8}]}, {"name": "utilities", "color": "#bf5730", "index": 8, "children": [{"name": "clebsch", "color": "#bf5730", "index": 8}, {"name": "convert_GHz_to_mK", "color": "#bf5730", "index": 8}, {"name": "convert_GHz_to_meV", "color": "#bf5730", "index": 8}, {"name": "convert_J_to_meV", "color": "#bf5730", "index": 8}, {"name": "convert_mK_to_GHz", "color": "#bf5730", "index": 8}, {"name": "convert_mK_to_meV", "color": "#bf5730", "index": 8}, {"name": "convert_meV_to_GHz", "color": "#bf5730", "index": 8}, {"name": "convert_meV_to_J", "color": "#bf5730", "index": 8}, {"name": "convert_meV_to_mK", "color": "#bf5730", "index": 8}, {"name": "convert_unit", "color": "#bf5730", "index": 8}, {"name": "n_thermal", "color": "#bf5730", "index": 8}]}, {"name": "about", "color": "#bf5730", "index": 8, "children": [{"name": "about", "color": "#bf5730", "index": 8}]}, {"name": "cite", "color": "#bf5730", "index": 8, "children": [{"name": "cite", "color": "#bf5730", "index": 8}]}, {"name": "fileio", "color": "#bf5730", "index": 8, "children": [{"name": "file_data_read", "color": "#bf5730", "index": 8}, {"name": "file_data_store", "color": "#bf5730", "index": 8}, {"name": "qload", "color": "#bf5730", "index": 8}, {"name": "qsave", "color": "#bf5730", "index": 8}]}, {"name": "simdiag", "color": "#bf5730", "index": 8, "children": [{"name": "simdiag", "color": "#bf5730", "index": 8}]}, {"name": "version", "color": "#bf5730", "index": 8}]} \ No newline at end of file diff --git a/doc/QuTiP_tree_plot/qutip-structure.py b/doc/QuTiP_tree_plot/qutip-structure.py index 6ba99fcc79..cf18a9f171 100755 --- a/doc/QuTiP_tree_plot/qutip-structure.py +++ b/doc/QuTiP_tree_plot/qutip-structure.py @@ -62,7 +62,7 @@ ("#bf5730", { "fileio", "utilities", "ipynbtools", "sparse", "graph", "simdiag", "permute", "demos", "about", "parallel", "version", "testing", - "parfor", "hardware_info", "ui", "cite", "lattice", + "hardware_info", "ui", "cite", }), ] diff --git a/doc/apidoc/classes.rst b/doc/apidoc/classes.rst index 1e85f82f1e..aea4e75e18 100644 --- a/doc/apidoc/classes.rst +++ b/doc/apidoc/classes.rst @@ -9,7 +9,7 @@ Classes Qobj -------------- -.. autoclass:: qutip.Qobj +.. autoclass:: qutip.core.qobj.Qobj :members: .. _classes-qobjevo: @@ -17,7 +17,7 @@ Qobj QobjEvo -------------- -.. autoclass:: qutip.QobjEvo +.. autoclass:: qutip.core.cy.qobjevo.QobjEvo :members: @@ -29,6 +29,8 @@ Bloch sphere .. autoclass:: qutip.bloch.Bloch :members: +.. autoclass:: qutip.bloch3d.Bloch3d + :members: Distributions ------------- @@ -42,10 +44,10 @@ Distributions Solver ------ -.. autoclass:: qutip.solver.sesolve.SeSolver +.. autoclass:: qutip.solver.sesolve.SESolver :members: -.. autoclass:: qutip.solver.mesolve.MeSolver +.. autoclass:: qutip.solver.mesolve.MESolver :members: .. autoclass:: qutip.solver.brmesolve.BRSolver @@ -105,25 +107,28 @@ Non-Markovian HEOM Solver Integrator ---------- -.. autoclass:: qutip.solver.ode.scipy_integrator.IntegratorScipyAdams +.. autoclass:: qutip.solver.integrator.scipy_integrator.IntegratorScipyAdams + :members: options + +.. autoclass:: qutip.solver.integrator.scipy_integrator.IntegratorScipyBDF :members: options -.. autoclass:: qutip.solver.ode.scipy_integrator.IntegratorScipyBDF +.. autoclass:: qutip.solver.integrator.scipy_integrator.IntegratorScipylsoda :members: options -.. autoclass:: qutip.solver.ode.scipy_integrator.IntegratorScipylsoda +.. autoclass:: qutip.solver.integrator.scipy_integrator.IntegratorScipyDop853 :members: options -.. autoclass:: qutip.solver.ode.scipy_integrator.IntegratorScipyDop853 +.. autoclass:: qutip.solver.integrator.qutip_integrator.IntegratorVern7 :members: options -.. autoclass:: qutip.solver.ode.qutip_integrator.IntegratorVern7 +.. autoclass:: qutip.solver.integrator.qutip_integrator.IntegratorVern9 :members: options -.. autoclass:: qutip.solver.ode.qutip_integrator.IntegratorVern9 +.. autoclass:: qutip.solver.integrator.qutip_integrator.IntegratorDiag :members: options -.. autoclass:: qutip.solver.ode.qutip_integrator.IntegratorDiag +.. autoclass:: qutip.solver.integrator.krylov.IntegratorKrylov :members: options @@ -144,19 +149,7 @@ Non-Markovian Memory Cascade and Transfer Tensor Solvers Solver Options and Results --------------------------- -.. autoclass:: qutip.solve.solver.ExpectOps - :members: - -.. autoclass:: qutip.solve.solver.Result - :members: - -.. autoclass:: qutip.solve.solver.SolverConfiguration - :members: - -.. autoclass:: qutip.solve.solver.Stats - :members: - -.. autoclass:: qutip.solve.stochastic.StochasticSolverOptions +.. autoclass:: qutip.solver.result.Result :members: .. _classes-piqs: diff --git a/doc/apidoc/functions.rst b/doc/apidoc/functions.rst index dd90f9290f..78a06941b5 100644 --- a/doc/apidoc/functions.rst +++ b/doc/apidoc/functions.rst @@ -103,7 +103,7 @@ Density Matrix Metrics ---------------------- .. automodule:: qutip.core.metrics - :members: fidelity, tracedist, bures_dist, bures_angle, hilbert_dist, average_gate_fidelity, process_fidelity + :members: fidelity, tracedist, bures_dist, bures_angle, hellinger_dist, hilbert_dist, average_gate_fidelity, process_fidelity, unitarity, dnorm Continuous Variables @@ -120,7 +120,7 @@ Measurement of quantum states ----------------------------- .. automodule:: qutip.measurement - :members: measure, measure_observable, measurement_statistics, measurement_statistics_observable + :members: measure, measure_povm, measure_observable, measurement_statistics, measurement_statistics_observable, measurement_statistics_povm Dynamics and Time-Evolution @@ -141,18 +141,14 @@ Master Equation Monte Carlo Evolution --------------------- -.. automodule:: qutip.solve.mcsolve +.. automodule:: qutip.solver.mcsolve :members: mcsolve -.. ignore f90 stuff for now - .. automodule:: qutip.fortran.mcsolve_f90 - :members: mcsolve_f90 - Krylov Subspace Solver ---------------------- -.. automodule:: qutip.solve.krylovsolve +.. automodule:: qutip.solver.krylovsolve :members: krylovsolve @@ -166,8 +162,8 @@ Bloch-Redfield Master Equation Floquet States and Floquet-Markov Master Equation ------------------------------------------------- -.. automodule:: qutip.solve.floquet - :members: fmmesolve, floquet_modes, floquet_modes_t, floquet_modes_table, floquet_modes_t_lookup, floquet_states, floquet_states_t, floquet_wavefunction, floquet_wavefunction_t, floquet_state_decomposition, fsesolve, floquet_master_equation_rates, floquet_master_equation_steadystate, floquet_basis_transform, floquet_markov_mesolve +.. automodule:: qutip.solver.floquet + :members: fmmesolve, fsesolve, FloquetBasis, FMESolver, floquet_tensor Stochastic Schrödinger Equation and Master Equation @@ -187,15 +183,18 @@ Hierarchical Equations of Motion Correlation Functions --------------------- -.. automodule:: qutip.solve.correlation - :members: correlation_2op_1t, correlation_2op_2t, correlation_3op_1t, correlation_3op_2t, spectrum, spectrum_correlation_fft, coherence_function_g1, coherence_function_g2 +.. automodule:: qutip.solver.correlation + :members: correlation_2op_1t, correlation_2op_2t, correlation_3op_1t, correlation_3op_2t, correlation_3op, coherence_function_g1, coherence_function_g2 + +.. automodule:: qutip.solver.spectrum + :members: spectrum, spectrum_correlation_fft Steady-state Solvers -------------------- -.. automodule:: qutip.solve.steadystate - :members: steadystate, build_preconditioner +.. automodule:: qutip.solver.steadystate + :members: steadystate, pseudo_inverse, steadystate_floquet :undoc-members: Propagators @@ -317,7 +316,7 @@ IPython Notebook Tools ---------------------- .. automodule:: qutip.ipynbtools - :members: parfor, parallel_map, version_table + :members: parallel_map, version_table .. _functions-misc: diff --git a/doc/biblio.rst b/doc/biblio.rst index 2675832008..7d3de972c4 100644 --- a/doc/biblio.rst +++ b/doc/biblio.rst @@ -67,3 +67,7 @@ Bibliography .. [Wis09] Wiseman, H. M. & Milburn, G. J. *Quantum Measurement and Control*, (Cambridge University Press, 2009). + +.. [NKanej] + + N Khaneja et. al. *Optimal control of coupled spin dynamics: Design of NMR pulse sequences by gradient ascent algorithms.* J. Magn. Reson. **172**, 296–305 (2005). :doi:`10.1016/j.jmr.2004.11.004` diff --git a/doc/changelog.rst b/doc/changelog.rst index 47150de7f8..071f1e5de9 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -4,6 +4,400 @@ Change Log ********** +.. towncrier release notes start + +Version 5.0.0a1 (February 7, 2023) +++++++++++++++++++++++++++++++++++ + +QuTiP 5 is a redesign of many of the core components of QuTiP (``Qobj``, +``QobjEvo``, solvers) to make them more consistent and more flexible. + +``Qobj`` may now be stored in either sparse or dense representations, +and the two may be mixed sensibly as needed. ``QobjEvo`` is now used +consistently throughout QuTiP, and the implementation has been +substantially cleaned up. A new ``Coefficient`` class is used to +represent the time-dependent factors inside ``QobjEvo``. + +The solvers have been rewritten to work well with the new data layer +and the concept of ``Integrators`` which solve ODEs has been introduced. +In future, new data layers may provide their own ``Integrators`` +specialized to their representation of the underlying data. + +Much of the user-facing API of QuTiP remains familiar, but there have +had to be many small breaking changes. If we can make changes to +easy migrating code from QuTiP 4 to QuTiP 5, please let us know. + +Any extensive list of changes follows. + +Contributors +------------ + +QuTiP 5 has been a large effort by many people over the last three years. + +In particular: + +- Jake Lishman led the implementation of the new data layer and coefficients. +- Eric Giguère led the implementation of the new QobjEvo interface and solvers. +- Boxi Li led the updating of QuTiP's QIP support and the creation of ``qutip_qip``. + +Other members of the QuTiP Admin team have been heavily involved in reviewing, +testing and designing QuTiP 5: + +- Alexander Pitchford +- Asier Galicia +- Nathan Shammah +- Shahnawaz Ahmed +- Neill Lambert +- Simon Cross + +Two Google Summer of Code contributors updated the tutorials and benchmarks to +QuTiP 5: + +- Christian Staufenbiel updated many of the tutorials (``). +- Xavier Sproken update the benchmarks (``). + +Four experimental data layers backends were written either as part of Google Summer +of Code or as separate projects. While these are still alpha quality, the helped +significantly to test the data layer API: + +- ``qutip-tensorflow``: a TensorFlow backend by Asier Galicia (``) +- ``qutip-cupy``: a CuPy GPU backend by Felipe Bivort Haiek (``)` +- ``qutip-tensornetwork``: a TensorNetwork backend by Asier Galicia (``) +- ``qutip-jax```: a JAX backend by Eric Giguère (``) + +We have also had many other contributors, whose specific contributions are +detailed below: + +- Pieter Eendebak (updated the required SciPy to 1.4+, `#1982 `). +- Pieter Eendebak (reduced import times by setting logger names, `#1981 `) +- Xavier Sproken (included C header files in the source distribution, `#1971 `) +- Christian Staufenbiel (added support for multiple collapse operators to the Floquet solver, `#1962 `) +- Christian Staufenbiel (fixed the basis used in the Floquet Master Equation solver, `#1952 `) +- Christian Staufenbiel (allowed the ``bloch_redfield_tensor`` function to accept strings and callables for `a_ops`, `#1951 `) +- Henrique Silvéro (allowed ``qutip_qip`` to be imported as ``qutip.qip``, `#1920 `) +- Florian Hopfmueller (added a vastly improved implementations of ``process_fidelity`` and ``average_gate_fidelity``, `#1712 `, `#1748 `, `#1788 `) +- Felipe Bivort Haiek (fixed inaccuracy in docstring of the dense implementation of negation, `#1608 `) +- Rajath Shetty (added support for specifying colors for individual points, vectors and states display by `qutip.Bloch`, `#1335 `) + +Qobj changes +------------ + +Previously ``Qobj`` data was stored in a SciPy-like sparse matrix. Now the +representation is flexible. Implementations for dense and sparse formats are +included in QuTiP and custom implementations are possible. QuTiP's performance +on dense states and operators is significantly improved as a result. + +Some highlights: + +- The data is still acessible via the ``.data`` attribute, but is now an + instance of the underlying data type instead of a SciPy-like sparse matrix. + The operations available in ``qutip.core.data`` may be used on ``.data``, + regardless of the data type. +- ``Qobj`` with different data types may be mixed in arithmetic and other + operations. A sensible output type will be automatically determined. +- The new ``.to(...)`` method may be used to convert a ``Qobj`` from one data type + to another. E.g. ``.to("dense")`` will convert to the dense representation and + ``.to("csr")`` will convert to the sparse type. +- Many ``Qobj`` methods and methods that create ``Qobj`` now accepted a ``dtype`` + parameter that allows the data type of the returned ``Qobj`` to specified. +- The new ``&`` operator may be used to obtain the tensor product. +- The new ``@`` operator may be used to obtain the matrix / operator product. + ``bar @ ket`` returns a scalar. +- The new ``.contract()`` method will collapse 1D subspaces of the dimensions of + the ``Qobj``. +- The new ``.logm()`` method returns the matrix logarithm of an operator. +- The methods ``.set_data``, ``.get_data``, ``.extract_state``, ``.eliminate_states``, + ``.evaluate`` and ``.check_isunitary`` have been removed. + +QobjEvo changes +--------------- + +The ``QobjEvo`` type for storing time-dependent quantum objects has been +significantly expanded, standardized and extended. The time-dependent +coefficients are now represented using a new ``Coefficient`` type that +may be independently created and manipulated if required. + +Some highlights: + +- The ``.compile()`` method has been removed. Coefficients specified as + strings are automatically compiled if possible and the compilation is + cached across different Python runs and instances. +- Mixing coefficient types within a single ``Qobj`` is now supported. +- Many new attributes were added to ``QobjEvo`` for convenience. Examples + include ``.dims``, ``.shape``, ``.superrep`` and ``.isconstant``. +- Many old attributes such as ``.cte``, ``.use_cython``, ``.type``, ``.const``, + and ``.coeff_file`` were removed. +- A new ``Spline`` coefficient supports spline interpolations of different + orders. The old ``Cubic_Spline`` coefficient has been removed. +- The new ``.arguments(...)`` method allows additional arguments to the + underlying coefficient functions to be updated. +- The ``_step_func_coeff`` argument has been replaced by the ``order`` + parameter. ``_step_func_coeff=False`` is equivalent to ``order=3``. + ``_step_func_coeff=True`` is equivalent to ``order=0``. Higher values + of ``order`` gives spline interpolations of higher orders. + +Solver changes +-------------- + +The solvers in QuTiP have been heavily reworked and standardized. +Under the hood solvers now make use of swappable ODE ``Integrators``. +Many ``Integrators`` are included (see the list below) and +custom implementations are possible. Solvers now consistently +accept a ``QobjEvo`` instance at the Hamiltonian or Liouvillian, or +any object which can be passed to the ``QobjEvo`` constructor. + +A breakdown of highlights follows. + +All solvers: + +- Solver options are now supplied in an ordinary Python dict. + ``qutip.Options`` is deprecated and returns a dict for backwards + compatibility. +- A specific ODE integrator may be selected by supplying a + ``method`` option. +- Each solver provides a class interface. Creating an instance + of the class allows a solver to be run multiple times for the + same system without having to repeatedly reconstruct the + right-hand side of the ODE to be integrated. +- A ``QobjEvo`` instance is accepted for most operators, e.g., + ``H``, ``c_ops``, ``e_ops``, ``a_ops``. +- The progress bar is now selected using the ``progress_bar`` option. + A new progess bar using the ``tqdm`` Python library is provided. +- Dynamic arguments, where the value of an operator depends on + the current state of the evolution, have been removed. They + may be re-implemented later if there is demand for them. + +Integrators: + +- The SciPy zvode integrator is available with the BDF and + Adams methods as ``bdf`` and ``adams``. +- The SciPy dop853 integrator (an eighth order Runge-Kutta method by + Dormand & Prince) is available as ``dop853``. +- The SciPy lsoda integrator is available as ``lsoda``. +- QuTiP's own implementation of Verner's "most efficient" Runge-Kutta methods + of order 7 and 9 are available as ``vern7`` and ``vern9``. See + http://people.math.sfu.ca/~jverner/ for a description of the methods. +- QuTiP's own implementation of a solver that directly diagonalizes the + the system to be integrated is available as ``diag``. It only works on + time-independent systems and is slow to setup, but once the diagonalization + is complete, it generates solutions very quickly. +- QuTiP's own implementatoin of an approximate Krylov subspace integrator is + available as ``krylov``. This integrator is only usable with ``sesolve``. + +Result class: + +- A new ``.e_data`` attribute provides expectation values as a dictionary. + Unlike ``.expect``, the values are provided in a Python list rather than + a numpy array, which better supports non-numeric types. +- The contents of the ``.stats`` attribute changed significantly and is + now more consistent across solvers. + +Monte-Carlo Solver (mcsolve): + +- The system, H, may now be a super-operator. +- The ``seed`` parameter now supports supplying numpy ``SeedSequence`` or + ``Generator`` types. +- The new ``timeout`` and ``target_tol`` parameters allow the solver to exit + early if a timeout or target tolerance is reached. +- The ntraj option no longer supports a list of numbers of trajectories. + Instead, just run the solver multiple times and use the class ``MCSolver`` + if setting up the solver uses a significant amount of time. +- The ``map_func`` parameter has been replaced by the ``map`` option. In + addition to the existing ``serial`` and ``parallel`` values, the value + ``loky`` may be supplied to use the loky package to parallelize trajectories. +- The result returned by ``mcsolve`` now supports calculating photocurrents + and calculating the steady state over N trajectories. +- The old ``parfor`` parallel execution function has been removed from + ``qutip.parallel``. Use ``parallel_map`` or ``loky_map`` instead. + +Bloch-Redfield Master Equation Solver (brmesolve): + +- The ``a_ops`` and ``spectra`` support implementaitons been heavily reworked to + reuse the techniques from the new Coefficient and QobjEvo classes. +- The ``use_secular`` parameter has been removed. Use ``sec_cutoff=-1`` instead. +- The required tolerance is now read from ``qutip.settings``. + +Krylov Subspace Solver (krylovsolve): + +- The Krylov solver is now implemented using ``SESolver`` and the ``krylov`` + ODE integrator. The function ``krylovsolve`` is maintained for convenience + and now supports many more options. +- The ``sparse`` parameter has been removed. Supply a sparse ``Qobj`` for the + Hamiltonian instead. + +Floquet Solver (fsesolve and fmmesolve): + +- The Floquet solver has been rewritten to use a new ``FloquetBasis`` class + which manages the transformations from lab to Floquet basis and back. +- Many of the internal methods used by the old Floquet solvers have + been removed. The Floquet tensor may still be retried using + the function ``floquet_tensor``. +- The Floquet Markov Master Equation solver has had many changes and + new options added. The environment temperature may be specified using + ``w_th``, and the result states are stored in the lab basis and optionally + in the Floquet basis using ``store_floquet_state``. +- The spectra functions supplied to ``fmmesolve`` must now be vectorized + (i.e. accept and return numpy arrays for frequencies and densities) and + must accept negative frequence (i.e. usually include a ``w > 0`` factor + so that the returned densities are zero for negative frequencies). +- The number of sidebands to keep, ``kmax`` may only be supplied when using + the ``FMESolver`` +- The ``Tsteps`` parameter has been removed from both ``fsesolve`` and + ``fmmesolve``. The ``precompute`` option to ``FloquetBasis`` may be used + instead. + +Evolution of State Solver (essovle): + +- The function ``essolve`` has been removed. Use the ``diag`` integration + method with ``sesolve`` or ``mesolve`` instead. + +Steady-state solvers (steadystate module): + +- The ``method`` parameter and ``solver`` parameters have been separated. Previously + they were mixed together in the ``method`` parameter. +- The previous options are now passed as parameters to the steady state + solver and mostly passed through to the underlying SciPy functions. +- The logging and statistics have been removed. + +Correlation functions (correlation module): + +- A new ``correlation_3op`` function has been added. It supports ``MESolver`` + or ``BRMESolver``. +- The ``correlation``, ``correlation_4op``, and ``correlation_ss`` functions have been + removed. +- Support for calculating correlation with ``mcsolve`` has been removed. + +Propagators (propagator module): + +- A class interface, ``qutip.Propagator``, has been added for propagators. +- Propagation of time-dependent systems is now supported using ``QobjEvo``. +- The ``unitary_mode`` and ``parallel`` options have been removed. + +Correlation spectra (spectrum module): + +- The functions ``spectrum_ss`` and ``spectrum_pi`` have been removed and + are now internal functions. +- The ``use_pinv`` parameter for ``spectrum`` has been removed and the + functionality merged into the ``solver`` parameter. Use ``solver="pi"`` + instead. + +QuTiP core +---------- + +There have been numerous other small changes to core QuTiP features: + +- ``qft(...)`` the function that returns the quantum Fourier + transform operator was moved from ``qutip.qip.algorithm`` into ``qutip``. +- The Bloch-Redfield solver tensor, ``brtensor``, has been moved into + ``qutip.core``. See the section above on the Bloch-Redfield solver + for details. +- The functions ``mat2vec`` and ``vec2mat`` for transforming states to and + from super-operator states have been renamed to ``stack_columns`` and + ``unstack_columns``. +- The function ``liouvillian_ref`` has been removed. Used ``liouvillian`` + instead. +- The superoperator transforms ``super_to_choi``, ``choi_to_super``, + ``choi_to_kraus``, ``choi_to_chi`` and ``chi_to_choi`` have been removed. + Used ``to_choi``, ``to_super``, ``to_kraus`` and ``to_chi`` instead. +- All of the random object creation functions now accepted a + numpy ``Generator`` as a seed. +- The ``dims`` parameter of all random object creation functions has + been removed. Supply the dimensions as the first parameter if + explicit dimensions are required. +- The function ``rand_unitary_haar`` has been removed. Use + ``rand_unitary(distribution="haar")`` instead. +- The functions ``rand_dm_hs`` and ``rand_dm_ginibre`` have been removed. + Use ``rand_dm(distribution="hs")`` and ``rand_dm(distribution="ginibre")`` + instead. +- The function ``rand_ket_haar`` has been removed. Use + ``rand_ket(distribution="haar")`` instead. +- The measurement functions have had the ``target`` parameter for + expanding the measurement operator removed. Used ``expand_operator`` + to expand the operator instead. +- ``qutip.Bloch`` now supports applying colours per-point, state or vector in + ``add_point``, ``add_states``, and ``add_vectors``. + +QuTiP settings +-------------- + +Previously ``qutip.settings`` was an ordinary module. Now ``qutip.settings`` is +an instance of a settings class. All the runtime modifiable settings for +core operations are in ``qutip.settings.core``. The other settings are not +modifiable at runtime. + +- Removed ``load``. ``reset`` and ``save`` functions. +- Removed ``.debug``, ``.fortran``, ``.openmp_thresh``. +- New ``.compile`` stores the compilation options for compiled coefficients. +- New ``.core["rtol"]`` core option gives the default relative tolerance used by QuTiP. +- The absolute tolerance setting ``.atol`` has been moved to ``.core["atol"]``. + +Package reorganization +---------------------- + +- ``qutip.qip`` has been moved into its own package, qutip-qip. Once installed, qutip-qip is available as either ``qutip.qip`` or ``qutip_qip``. Some widely useful gates have been retained in ``qutip.gates``. +- ``qutip.lattice`` has been moved into its own package, qutip-lattice. It is available from ``. +- ``qutip.sparse`` has been removed. It contained the old sparse matrix representation and is replaced by the new implementation in ``qutip.data``. +- ``qutip.piqs`` functions are no longer available from the ``qutip`` namespace. They are accessible from ``qutip.piqs`` instead. + +Miscellaneous +------------- + +- Support has been added for 64-bit integer sparse matrix indices, allowing + sparse matrices with up to 2**63 rows and columns. This support needs to + be enabled at compilation time by calling ``setup.py`` and passing + ``--with-idxint-64``. + +Feature removals +---------------- + +- Support for OpenMP has been removed. If there is enough demand and a good plan for how to organize it, OpenMP support may return in a future QuTiP release. +- The ``qutip.parfor`` function has been removed. Use ``qutip.parallel_map`` instead. +- ``qutip.graph`` has been removed and replaced by SciPy's graph functions. +- ``qutip.topology`` has been removed. It contained only one function ``berry_curvature``. +- The ``~/.qutip/qutiprc`` config file is no longer supported. It contained settings for the OpenMP support. + + +Version 4.7.1 (December 11, 2022) ++++++++++++++++++++++++++++++++++ + +This is a bugfix release for QuTiP 4.7.X. In addition to the minor fixes +listed below, the release adds builds for Python 3.11 and support for +packaging 22.0. + +Features +-------- +- Improve qutip import times by setting logger names explicitly. (#1980) + +Bug Fixes +--------- +- Change floquet_master_equation_rates(...) to use an adaptive number of time steps scaled by the number of sidebands, kmax. (#1961) +- Change fidelity(A, B) to use the reduced fidelity formula for pure states which is more numerically efficient and accurate. (#1964) +- Change ``brmesolve`` to raise an exception when ode integration is not successful. (#1965) +- Backport fix for IPython helper Bloch._repr_svg_ from dev.major. Previously the print_figure function returned bytes, but since ipython/ipython#5452 (in 2014) it returns a Unicode string. This fix updates QuTiP's helper to match. (#1970) +- Fix correlation for case where only the collapse operators are time dependent. (#1979) +- Fix the hinton visualization method to plot the matrix instead of its transpose. (#2011) +- Fix the hinton visualization method to take into account all the matrix coefficients to set the squares scale, instead of only the diagonal coefficients. (#2012) +- Fix parsing of package versions in setup.py to support packaging 22.0. (#2037) +- Add back .qu suffix to objects saved with qsave and loaded with qload. The suffix was accidentally removed in QuTiP 4.7.0. (#2038) +- Add a default max_step to processors. (#2040) + +Documentation +------------- +- Add towncrier for managing the changelog. (#1927) +- Update the version of numpy used to build documentation to 1.22.0. (#1940) +- Clarify returned objects from bloch_redfield_tensor(). (#1950) +- Update Floquet Markov solver docs. (#1958) +- Update the roadmap and ideas to show completed work as of August 2022. (#1967) + +Miscellaneous +------------- +- Return TypeError instead of Exception for type error in sesolve argument. (#1924) +- Add towncrier draft build of changelog to CI tests. (#1946) +- Add Python 3.11 to builds. (#2041) +- Simplify version parsing by using packaging.version.Version. (#2043) +- Update builds to use cibuildwheel 2.11, and to build with manylinux2014 on Python 3.8 and 3.9, since numpy and SciPy no longer support manylinux2010 on those versions of Python. (#2047) + + Version 4.7.0 (April 13, 2022) ++++++++++++++++++++++++++++++ diff --git a/doc/changes/.gitignore b/doc/changes/.gitignore new file mode 100644 index 0000000000..14a993c3aa --- /dev/null +++ b/doc/changes/.gitignore @@ -0,0 +1,2 @@ +# This ensures that the folder persists after the removal of the files in it in a new version release. +!.gitignore \ No newline at end of file diff --git a/doc/changes/2032.bugfix b/doc/changes/2032.bugfix new file mode 100644 index 0000000000..8cf71d67e7 --- /dev/null +++ b/doc/changes/2032.bugfix @@ -0,0 +1 @@ +Added default _isherm value (True) for momentum and position operators. diff --git a/doc/development/contributing.rst b/doc/development/contributing.rst index ebc791b88d..de2af1b5c0 100644 --- a/doc/development/contributing.rst +++ b/doc/development/contributing.rst @@ -144,6 +144,20 @@ You can also use the ``-k`` selector to only run tests whose names include a par to run the tests of :meth:`Qobj.expm`. +Changelog Generation +-------------------- + +We use ``towncrier`` for tracking changes and generating a changelog. +When making a pull request, we require that you add a towncrier entry along with the code changes. +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 + + towncrier create . + +Running this will create a file in the ``doc/changes`` directory with a filename corresponding to the argument you passed to ``towncrier create``. +In this file, you should add a short description of the changes that the PR introduces. .. _contributing-docs: diff --git a/doc/development/ideas.rst b/doc/development/ideas.rst index 866d248dd3..da421facd2 100644 --- a/doc/development/ideas.rst +++ b/doc/development/ideas.rst @@ -4,14 +4,35 @@ Ideas for future QuTiP development ********************************** -This chapter covers the development of QuTiP and its subpackages, including -a roadmap for upcoming releases and ideas for future improvements. +Ideas for significant new features are listed here. For the general roadmap, +see :doc:`roadmap`. .. toctree:: :maxdepth: 1 ideas/qutip-interactive.rst ideas/pulse-level-quantum-circuits.rst - ideas/tensorflow-data-backend.rst ideas/quantum-error-mitigation.rst ideas/heom-gpu.rst + + +Google Summer of Code +===================== + +Many possible extensions and improvements to QuTiP have been documented as +part of `Google Summer of Code `_: + +* `GSoC 2021 `_ +* `GSoC 2022 `_ + + + +Completed Projects +================== + +These projects have been completed: + +.. toctree:: + :maxdepth: 1 + + ideas/tensorflow-data-backend.rst diff --git a/doc/development/ideas/tensorflow-data-backend.rst b/doc/development/ideas/tensorflow-data-backend.rst index 3652cce497..6bb94b0140 100644 --- a/doc/development/ideas/tensorflow-data-backend.rst +++ b/doc/development/ideas/tensorflow-data-backend.rst @@ -6,6 +6,9 @@ TensorFlow Data Backend :local: :depth: 3 +.. note:: + This project was completed as part of GSoC 2021 [3]_. + QuTiP's data layer provides the mathematical operations needed to work with quantum states and operators, i.e. ``Qobj``, inside QuTiP. As part of Google Summer of Code 2020, the data layer was rewritten to allow new backends to @@ -74,3 +77,4 @@ References .. [1] https://www.tensorflow.org/ .. [2] https://github.com/tehruhn/bofin +.. [3] https://github.com/qutip/qutip-tensorflow/ diff --git a/doc/development/release_distribution.rst b/doc/development/release_distribution.rst index 4d07bcf300..5d6956014d 100644 --- a/doc/development/release_distribution.rst +++ b/doc/development/release_distribution.rst @@ -70,11 +70,12 @@ Updating the Changelog This needs to be done no matter what type of release is being made. #. Create a new branch to use to make a pull request. -#. Write the changelog for this version in ``doc/changelog.rst``. - Look at recent entries in that file to get a feel for the style. - In general, the format is one or two paragraphs written in regular prose describing the major new features of the version, and anything that needs special attention. - After that, in suitable headings, list all the changes and who made them. - Headings you may want to have include "Features", "Improvements", "Bug Fixes", "Deprecations", "Removals" and "Developer Changes", but feel free to use anything sensible. +#. Update the changelog using ``towncrier``: + + towncrier build --version= + +Where ```` is the expected version number of the release + #. Make a pull request on the main ``qutip/qutip`` repository with this changelog, and get other members of the admin team to approve it. #. Merge this into ``master``. diff --git a/doc/development/roadmap.rst b/doc/development/roadmap.rst index 3d3cf84090..36f61a0600 100644 --- a/doc/development/roadmap.rst +++ b/doc/development/roadmap.rst @@ -94,11 +94,6 @@ QuTiP affilliated packages such [this needs clarification]. These packages will not be maintained by the QuTiP Team. -.. todo:: - - Do we really need optional subpackages? It seems that are a bit fiddly and as - we are side-stepping bw compat with a major version, we could just make QIP a - separate package. Family packages --------------- @@ -143,12 +138,7 @@ QIP ^^^ * **current package status**: integrated sub-package `qutip.qip` -* **planned package status**: optional sub-package `qutip.qip` - -.. todo:: - - Is it really necessary for this to be a sub-package? It could just be a - separate package. +* **planned package status**: family package `qutip-qip` The QIP subpackage has been deemed out of scope (feature-wise). It also depends on `qutip.control` and hence would be out of scope for dependency reasons. A @@ -196,61 +186,6 @@ candiate for affilliation. Development Projects ==================== -.. _dl-abs: - -data layer abstraction ----------------------- - -:tag: dl-abs -:status: majority of development completed. -:admin lead: `Eric `_ -:main dev: `Jake Lishman `_ - -Development completed as a GSoC project. Fully implemented in the dev.major -branch. Currently being used by some research groups. - -Abstraction of the linear algebra data from code qutip components, allowing -for alternatives, such as sparse, dense etc. Difficult to summarize. Almost -every file in qutip affected in some way. A major milestone for qutip. -Significant performance improvements throughout qutip. - -Some developments tasks remain, including providing full control over how the -data-layer dispatchers choose the most appropriate output type. - -.. _qmain-reorg: - -qutip main reorganization -------------------------- - -:tag: qmain-reorg -:status: development [pretty much] complete -:admin lead: `Eric `_ -:main dev: `Jake Lishman `_ - -Reorganise qutip main components to the structure :ref:`described above `. - -.. _qmain-docs: - -qutip user docs migration -------------------------- - -:tag: qmain-docs -:status: conceptualised -:admin lead: TBA; `Shahnawaz `_? -:main dev: TBA - -The qutip user documentation build files are to be moved to the qutip/qutip -repo. This is more typical for an OSS package. - -As part of the move, the plan is to reconstruct the Sphinx structure from -scratch. Historically, there have been many issues with building the docs. -Sphinx has come a long way since qutip docs first developed. The main source -(rst) files will remain [pretty much] as they are, although there is a lot of -scope to improve them. - -The qutip-doc repo will afterwards just be used for documents, such as this one, -pertaining to the QuTiP project. - .. _solve-dl: Solver data layer integration @@ -266,32 +201,6 @@ the qutip solvers. Eric has been revamping the solvers by deploying `QobjEvo` (the time-dependent quantum object) that he developed. `QobjEvo` will exploit the data layer, and the solvers in turn exploit `QobjEvo`. -.. _qip-mig: - -QIP migration -------------- - -:tag: qip-mig -:status: development [pretty much] complete -:admin lead: `Boxi `_ -:main dev: `Sidhant Saraogi `_ - -A separate package for qutip-qip was created during Sidhant's GSoC project. -There is some fine tuning required, especially after qutip.control is migrated. - -.. _heom-revamp: - -HEOM revamp ------------ - -:tag: heom-revamp -:status: development [pretty much] complete -:admin lead: `Neill `_ -:main dev: `Tarun Raheja `_ - -An overhaul of the HEOM solver. C++ components used to speed up construction of -the hierarchy. - .. _qtrl-mig: Qtrl migration @@ -447,6 +356,89 @@ QIP circuits could be animated. Status lights showing evolution of states during the processing. Animated Bloch spheres for qubits. +Completed Development Projects +============================== + +.. _dl-abs: + +data layer abstraction +---------------------- + +:tag: dl-abs +:status: completed +:admin lead: `Eric `_ +:main dev: `Jake Lishman `_ + +Development completed as a GSoC project. Fully implemented in the dev.major +branch. Currently being used by some research groups. + +Abstraction of the linear algebra data from code qutip components, allowing +for alternatives, such as sparse, dense etc. Difficult to summarize. Almost +every file in qutip affected in some way. A major milestone for qutip. +Significant performance improvements throughout qutip. + +Some developments tasks remain, including providing full control over how the +data-layer dispatchers choose the most appropriate output type. + +.. _qmain-reorg: + +qutip main reorganization +------------------------- + +:tag: qmain-reorg +:status: completed +:admin lead: `Eric `_ +:main dev: `Jake Lishman `_ + +Reorganise qutip main components to the structure :ref:`described above `. + +.. _qmain-docs: + +qutip user docs migration +------------------------- + +:tag: qmain-docs +:status: completed +:admin lead: `Jake Lishman `_ +:main dev: `Jake Lishman `_ + +The qutip user documentation build files are to be moved to the qutip/qutip +repo. This is more typical for an OSS package. + +As part of the move, the plan is to reconstruct the Sphinx structure from +scratch. Historically, there have been many issues with building the docs. +Sphinx has come a long way since qutip docs first developed. The main source +(rst) files will remain [pretty much] as they are, although there is a lot of +scope to improve them. + +The qutip-doc repo will afterwards just be used for documents, such as this one, +pertaining to the QuTiP project. + +.. _qip-mig: + +QIP migration +------------- + +:tag: qip-mig +:status: completed +:admin lead: `Boxi `_ +:main dev: `Sidhant Saraogi `_ + +A separate package for qutip-qip was created during Sidhant's GSoC project. +There is some fine tuning required, especially after qutip.control is migrated. + +.. _heom-revamp: + +HEOM revamp +----------- + +:tag: heom-revamp +:status: completed +:admin lead: `Neill `_ +:main dev: `Simon Cross `_, `Tarun Raheja `_ + +An overhaul of the HEOM solver, to incorporate the improvements pioneered in BoFiN. + .. _release roadmap: QuTiP major release roadmap @@ -457,21 +449,21 @@ QuTiP v.5 These Projects need to be completed for the qutip v.5 release. -- :ref:`dl-abs` -- :ref:`qmain-reorg` -- :ref:`qmain-docs` -- :ref:`solve-dl` -- :ref:`qip-mig` +- :ref:`dl-abs` (completed) +- :ref:`qmain-reorg` (completed) +- :ref:`qmain-docs` (completed) +- :ref:`solve-dl` (in-progress) +- :ref:`qip-mig` (completed) - :ref:`qtrl-mig` -- :ref:`heom-revamp` +- :ref:`heom-revamp` (completed) The planned timeline for the release is: -- **alpha version, September 2022**. Core features packaged and available for +- **alpha version, December 2022**. Core features packaged and available for experienced users to test. -- **beta version, November 2022**. All required features and documentation - complete, packaged and ready for community testing. -- **full release, December 2022**. Full tested version released. +- **beta version, January 2023**. All required features and documentation complete, + packaged and ready for community testing. +- **full release, April 2023**. Full tested version released. Planned supported environment: diff --git a/doc/guide/dynamics/dynamics-bloch-redfield.rst b/doc/guide/dynamics/dynamics-bloch-redfield.rst index f192e2ffc8..5ec31e0e09 100644 --- a/doc/guide/dynamics/dynamics-bloch-redfield.rst +++ b/doc/guide/dynamics/dynamics-bloch-redfield.rst @@ -18,11 +18,20 @@ Bloch-Redfield master equation Introduction ============ -The Lindblad master equation introduced earlier is constructed so that it describes a physical evolution of the density matrix (i.e., trace and positivity preserving), but it does not provide a connection to any underlaying microscopic physical model. The Lindblad operators (collapse operators) describe phenomenological processes, such as for example dephasing and spin flips, and the rates of these processes are arbitrary parameters in the model. In many situations the collapse operators and their corresponding rates have clear physical interpretation, such as dephasing and relaxation rates, and in those cases the Lindblad master equation is usually the method of choice. +The Lindblad master equation introduced earlier is constructed so that it describes a physical evolution of the density matrix (i.e., trace and positivity preserving), but it does not provide a connection to any underlaying microscopic physical model. +The Lindblad operators (collapse operators) describe phenomenological processes, such as for example dephasing and spin flips, and the rates of these processes are arbitrary parameters in the model. +In many situations the collapse operators and their corresponding rates have clear physical interpretation, such as dephasing and relaxation rates, and in those cases the Lindblad master equation is usually the method of choice. However, in some cases, for example systems with varying energy biases and eigenstates and that couple to an environment in some well-defined manner (through a physically motivated system-environment interaction operator), it is often desirable to derive the master equation from more fundamental physical principles, and relate it to for example the noise-power spectrum of the environment. -The Bloch-Redfield formalism is one such approach to derive a master equation from a microscopic system. It starts from a combined system-environment perspective, and derives a perturbative master equation for the system alone, under the assumption of weak system-environment coupling. One advantage of this approach is that the dissipation processes and rates are obtained directly from the properties of the environment. On the downside, it does not intrinsically guarantee that the resulting master equation unconditionally preserves the physical properties of the density matrix (because it is a perturbative method). The Bloch-Redfield master equation must therefore be used with care, and the assumptions made in the derivation must be honored. (The Lindblad master equation is in a sense more robust -- it always results in a physical density matrix -- although some collapse operators might not be physically justified). For a full derivation of the Bloch Redfield master equation, see e.g. [Coh92]_ or [Bre02]_. Here we present only a brief version of the derivation, with the intention of introducing the notation and how it relates to the implementation in QuTiP. +The Bloch-Redfield formalism is one such approach to derive a master equation from a microscopic system. +It starts from a combined system-environment perspective, and derives a perturbative master equation for the system alone, under the assumption of weak system-environment coupling. +One advantage of this approach is that the dissipation processes and rates are obtained directly from the properties of the environment. +On the downside, it does not intrinsically guarantee that the resulting master equation unconditionally preserves the physical properties of the density matrix (because it is a perturbative method). +The Bloch-Redfield master equation must therefore be used with care, and the assumptions made in the derivation must be honored. +(The Lindblad master equation is in a sense more robust -- it always results in a physical density matrix -- although some collapse operators might not be physically justified). +For a full derivation of the Bloch Redfield master equation, see e.g. [Coh92]_ or [Bre02]_. +Here we present only a brief version of the derivation, with the intention of introducing the notation and how it relates to the implementation in QuTiP. .. _bloch-redfield-derivation: @@ -39,9 +48,13 @@ The most general form of a master equation for the system dynamics is obtained b \frac{d}{dt}\rho_S(t) = - \hbar^{-2}\int_0^t d\tau\; {\rm Tr}_B [H_I(t), [H_I(\tau), \rho_S(\tau)\otimes\rho_B]], -where the additional assumption that the total system-bath density matrix can be factorized as :math:`\rho(t) \approx \rho_S(t) \otimes \rho_B`. This assumption is known as the Born approximation, and it implies that there never is any entanglement between the system and the bath, neither in the initial state nor at any time during the evolution. *It is justified for weak system-bath interaction.* +where the additional assumption that the total system-bath density matrix can be factorized as :math:`\rho(t) \approx \rho_S(t) \otimes \rho_B`. +This assumption is known as the Born approximation, and it implies that there never is any entanglement between the system and the bath, neither in the initial state nor at any time during the evolution. +*It is justified for weak system-bath interaction.* -The master equation :eq:`br-nonmarkovian-form-one` is non-Markovian, i.e., the change in the density matrix at a time :math:`t` depends on states at all times :math:`\tau < t`, making it intractable to solve both theoretically and numerically. To make progress towards a manageable master equation, we now introduce the Markovian approximation, in which :math:`\rho(s)` is replaced by :math:`\rho(t)` in Eq. :eq:`br-nonmarkovian-form-one`. The result is the Redfield equation +The master equation :eq:`br-nonmarkovian-form-one` is non-Markovian, i.e., the change in the density matrix at a time :math:`t` depends on states at all times :math:`\tau < t`, making it intractable to solve both theoretically and numerically. +To make progress towards a manageable master equation, we now introduce the Markovian approximation, in which :math:`\rho_S(\tau)` is replaced by :math:`\rho_S(t)` in Eq. :eq:`br-nonmarkovian-form-one`. +The result is the Redfield equation .. math:: :label: br-nonmarkovian-form-two @@ -57,7 +70,8 @@ which is local in time with respect the density matrix, but still not Markovian The two Markovian approximations introduced above are valid if the time-scale with which the system dynamics changes is large compared to the time-scale with which correlations in the bath decays (corresponding to a "short-memory" bath, which results in Markovian system dynamics). -The master equation :eq:`br-markovian-form` is still on a too general form to be suitable for numerical implementation. We therefore assume that the system-bath interaction takes the form :math:`H_I = \sum_\alpha A_\alpha \otimes B_\alpha` and where :math:`A_\alpha` are system operators and :math:`B_\alpha` are bath operators. This allows us to write master equation in terms of system operators and bath correlation functions: +The master equation :eq:`br-markovian-form` is still on a too general form to be suitable for numerical implementation. We therefore assume that the system-bath interaction takes the form :math:`H_I = \sum_\alpha A_\alpha \otimes B_\alpha` and where :math:`A_\alpha` are system operators and :math:`B_\alpha` are bath operators. +This allows us to write master equation in terms of system operators and bath correlation functions: .. math:: @@ -102,7 +116,8 @@ In the eigenbasis of the system Hamiltonian, where :math:`A_{mn}(t) = A_{mn} e^{ \right\} \rho_{cd}(t), \nonumber\\ -where the "sec" above the summation symbol indicate summation of the secular terms which satisfy :math:`|\omega_{ab}-\omega_{cd}| \ll \tau_ {\rm decay}`. This is an almost-useful form of the master equation. The final step before arriving at the form of the Bloch-Redfield master equation that is implemented in QuTiP, involves rewriting the bath correlation function :math:`g(\tau)` in terms of the noise-power spectrum of the environment :math:`S(\omega) = \int_{-\infty}^\infty d\tau e^{i\omega\tau} g(\tau)`: +where the "sec" above the summation symbol indicate summation of the secular terms which satisfy :math:`|\omega_{ab}-\omega_{cd}| \ll \tau_ {\rm decay}`. +This is an almost-useful form of the master equation. The final step before arriving at the form of the Bloch-Redfield master equation that is implemented in QuTiP, involves rewriting the bath correlation function :math:`g(\tau)` in terms of the noise-power spectrum of the environment :math:`S(\omega) = \int_{-\infty}^\infty d\tau e^{i\omega\tau} g(\tau)`: .. math:: :label: br-nonmarkovian-form-four @@ -169,7 +184,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. The spectral density functions are Python callback functions that takes the (angular) frequency as a single argument. +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. +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 @@ -194,7 +211,7 @@ To illustrate how to calculate the Bloch-Redfield tensor, let's consider a two-l return gamma1 / 2 * (w / (2 * np.pi)) * (w > 0.0) - R, ekets = bloch_redfield_tensor(H, [[sigmax(), ohmic_spectrum]]) + R, ekets = bloch_redfield_tensor(H, a_ops=[[sigmax(), ohmic_spectrum]]) print(R) @@ -213,12 +230,16 @@ 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.bloch_redfield_tensor` also returns a list of eigenkets `ekets`, 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 computational basis and the eigenbasis. +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. +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: + .. plot:: :context: - :include-source: False delta = 0.2 * 2*np.pi eps0 = 1.0 * 2*np.pi @@ -226,24 +247,17 @@ Note that it is also possible to add Lindblad dissipation superoperators in the 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) - - ohmic_spectrum = coefficient(ohmic_spectrum, args={'w': 0}) + 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) R = bloch_redfield_tensor(H, [[sigmax(), ohmic_spectrum]], fock_basis=True) -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 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: - tlist = np.linspace(0, 15.0, 1000) - psi0 = rand_ket(2) + psi0 = rand_ket(2, seed=1) e_ops = [sigmax(), sigmay(), sigmaz()] @@ -259,14 +273,14 @@ The evolution of a wavefunction or density matrix, according to the Bloch-Redfie 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.bloch_redfield.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:`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``. .. 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.solve.solver.Result`. +where the resulting `output` is an instance of the class :class:`qutip.Result`. .. _td-bloch-redfield: @@ -276,24 +290,14 @@ 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. 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. 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. - - -The time-dependent Bloch-Redfield solver in QuTiP relies on the efficient numerical computations afforded by the string-based time-dependent format, and Cython compilation. As such, all the time-dependent terms, and noise power spectra must be expressed in the string format. To begin, lets consider the previous example, but formatted to call the time-dependent solver: +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. +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. -.. plot:: - :context: - - ohmic = "{gamma1} / 2.0 * (w / (2 * pi)) * (w > 0.0)".format(gamma1=gamma1) - - output = brmesolve(H, psi0, tlist, a_ops=[[sigmax(),ohmic]], e_ops=e_ops) - - -Although the problem itself is time-independent, the use of a string as the noise power spectrum tells the solver to go into time-dependent mode. The string is nearly identical to the Python function format, except that we replaced ``np.pi`` with ``pi`` to avoid calling Python in our Cython code, and we have hard coded the ``gamma1`` argument into the string as limitations prevent passing arguments into the time-dependent Bloch-Redfield solver. - - -For actual 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))']]``. @@ -352,7 +356,7 @@ The second tuple ``f'{kappa} * (w >= 0)'``, gives the noise power spectrum. A full example is: .. plot:: - :context: + :context: close-figs N = 10 @@ -388,5 +392,7 @@ A full example is: plt.show() +.. plot:: + :context: close-figs Further examples on time-dependent Bloch-Redfield simulations can be found in the online tutorials. diff --git a/doc/guide/dynamics/dynamics-data.rst b/doc/guide/dynamics/dynamics-data.rst index 97b2c404e1..d6c616dbbe 100644 --- a/doc/guide/dynamics/dynamics-data.rst +++ b/doc/guide/dynamics/dynamics-data.rst @@ -9,7 +9,9 @@ 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 to the user. This object is a :func:`qutip.solve.solver.Result` class that stores all the crucial data needed for analyzing and plotting the results of a simulation. Like the :func:`qutip.Qobj` class, the ``Result`` class has a collection of properties for storing information. However, in contrast to the ``Qobj`` class, this structure contains no methods, and is therefore nothing but a container object. 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.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 @@ -22,21 +24,15 @@ Before embarking on simulating the dynamics of quantum systems, we will first lo +------------------------+-----------------------------------------------------------------------+ | ``result.expect`` | List/array of expectation values, if requested. | +------------------------+-----------------------------------------------------------------------+ +| ``result.e_data`` | Dictionary of expectation values, if requested. | ++------------------------+-----------------------------------------------------------------------+ | ``result.states`` | List/array of state vectors/density matrices calculated at ``times``, | | | if requested. | +------------------------+-----------------------------------------------------------------------+ -| ``result.num_expect`` | The number of expectation value operators in the simulation. | -+------------------------+-----------------------------------------------------------------------+ -| ``result.num_collapse``| The number of collapse operators in the simulation. | -+------------------------+-----------------------------------------------------------------------+ -| ``result.ntraj`` | Number of Monte Carlo trajectories run. | -+------------------------+-----------------------------------------------------------------------+ -| ``result.col_times`` | Times at which state collapse occurred. Only for Monte Carlo solver. | -+------------------------+-----------------------------------------------------------------------+ -| ``result.col_which`` | Which collapse operator was responsible for each collapse in | -| | in ``col_times``. Only used by Monte Carlo solver. | +| ``result.final_state`` | State vector or density matrix at the last time of the evolution. | +------------------------+-----------------------------------------------------------------------+ -| ``result.seeds`` | Seeds used in generating random numbers for Monte Carlo solver. | +| ``result.stats`` | Various statistics about the evolution including the integration | +| | method used, number of collapse operators etc. | +------------------------+-----------------------------------------------------------------------+ @@ -45,18 +41,32 @@ Before embarking on simulating the dynamics of quantum systems, we will first lo 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 Monte Carlo 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 >>> print(result) - Result object with mcsolve data. - --------------------------------- - expect = True - num_expect = 2, num_collapse = 2, ntraj = 500 - -The first line tells us that this data object was generated from the Monte Carlo solver ``mcsolve`` (discussed in :ref:`monte`). The next line (not the ``---`` line of course) indicates that this object contains expectation value data. Finally, the last line gives the number of expectation value and collapse operators used in the simulation, along with the number of Monte Carlo trajectories run. Note that the number of trajectories ``ntraj`` is only displayed when using the Monte Carlo solver. + + +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: @@ -68,7 +78,18 @@ 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). Together with the array of times at which these expectation values are calculated: +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:: + :skipif: True + + e_ops = {"sx": sigmax(), "sy": sigmay(), "sz": sigmaz()} + ... + 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: .. testcode:: :skipif: True @@ -80,45 +101,58 @@ we can plot the resulting expectation values: .. testcode:: :skipif: True - plot(times, expt0, times, expt1) + plot(times, expt0) + plot(times, expt1) show() -State vectors, or density matrices, as well as ``col_times`` and ``col_which``, 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. The one exception to this rule is if you choose to output state vectors from the Monte Carlo solver, in which case there are ``ntraj`` number of state vector arrays. - -.. _odedata-saving: +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`. -Saving and Loading Result Objects -================================== -The main advantage in using the Result class as a data storage object comes from the simplicity in which simulation data can be stored and later retrieved. The :func:`qutip.fileio.qsave` and :func:`qutip.fileio.qload` functions are designed for this task. To begin, let us save the ``data`` object from the previous section into a file called "cavity+qubit-data" in the current working directory by calling: +Multiple Trajectories Solver Results +==================================== -.. testcode:: - :skipif: True - qsave(result, 'cavity+qubit-data') - -All of the data results are then stored in a single file of the same name with a ".qu" extension. Therefore, everything needed to later this data is stored in a single file. Loading the file is just as easy as saving: +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:: :options: +SKIP - >>> stored_result = qload('cavity+qubit-data') - Loaded Result object: - Result object with mcsolve data. - --------------------------------- - expect = True - num_expect = 2, num_collapse = 2, ntraj = 500 - -where ``stored_result`` is the new name of the Result object. We can then extract the data and plot in the same manner as before: + >>> mcsolve(H, psi, np.linspace(0, 1, 11), c_ops, e_ops=[num(N)], ntraj=25, options={"keep_runs_results": False}) + >>> np.shape(result.expect) + (1, 11) + + >>> mcsolve(H, psi, np.linspace(0, 1, 11), c_ops, e_ops=[num(N)], ntraj=25, options={"keep_runs_results": True}) + >>> np.shape(result.expect) + (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: + ++-------------------------+----------------------+------------------------------------------------------------------------+ +| Reduced result | Trajectories results | Description | ++=========================+======================+========================================================================+ +| ``average_states`` | ``runs_states`` | State vectors or density matrices calculated at each times of tlist | ++-------------------------+----------------------+------------------------------------------------------------------------+ +| ``average_final_state`` | ``runs_final_state`` | State vectors or density matrices calculated at the last time of tlist | ++-------------------------+----------------------+------------------------------------------------------------------------+ +| ``average_expect`` | ``runs_expect`` | List/array of expectation values, if requested. | ++-------------------------+----------------------+------------------------------------------------------------------------+ +| ``std_expect`` | | List/array of standard derivation of the expectation values. | ++-------------------------+----------------------+------------------------------------------------------------------------+ +| ``average_e_data`` | ``runs_e_data`` | Dictionary of expectation values, if requested. | ++-------------------------+----------------------+------------------------------------------------------------------------+ +| ``std_e_data`` | | Dictionary of standard derivation of the expectation values. | ++-------------------------+----------------------+------------------------------------------------------------------------+ + +Multiple trajectories results also keep the trajectories seeds to allows recomputing the results. .. testcode:: - :skipif: True - - expt0 = stored_result.expect[0] - expt1 = stored_result.expect[1] - times = stored_result.times - plot(times, expt0, times, expt1) - show() + :skipif: True -Also see :ref:`saving` for more information on saving quantum objects, as well as arrays for use in other programs. + seeds = result.seeds diff --git a/doc/guide/dynamics/dynamics-floquet.rst b/doc/guide/dynamics/dynamics-floquet.rst index a0a4b39694..e3d0a3020c 100644 --- a/doc/guide/dynamics/dynamics-floquet.rst +++ b/doc/guide/dynamics/dynamics-floquet.rst @@ -27,7 +27,7 @@ The Schrödinger equation with a time-dependent Hamiltonian :math:`H(t)` is H(t)\Psi(t) = i\hbar\frac{\partial}{\partial t}\Psi(t), -where :math:`\Psi(t)` is the wave function solution. Here we are interested in problems with periodic time-dependence, i.e., the Hamiltonian satisfies :math:`H(t) = H(t+T)` where :math:`T` is the period. According to the Floquet theorem, there exist solutions to :eq:`eq_td_schrodinger` on the form +where :math:`\Psi(t)` is the wave function solution. Here we are interested in problems with periodic time-dependence, i.e., the Hamiltonian satisfies :math:`H(t) = H(t+T)` where :math:`T` is the period. According to the Floquet theorem, there exist solutions to :eq:`eq_td_schrodinger` of the form .. math:: :label: eq_floquet_states @@ -93,7 +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: + :context: close-figs >>> delta = 0.2 * 2*np.pi >>> eps0 = 1.0 * 2*np.pi @@ -104,15 +104,17 @@ 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 :func:`qutip.floquet.floquet_modes` function, which returns lists containing 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:`qutip.FloquetBasis` class, which encapsulates the Floquet modes and the quasienergies: .. plot:: :context: >>> T = 2*np.pi / omega - >>> f_modes_0, f_energies = floquet_modes(H, T, args) + >>> floquet_basis = FloquetBasis(H, T, args) + >>> f_energies = floquet_basis.e_quasi >>> f_energies # doctest: +NORMALIZE_WHITESPACE array([-2.83131212, 2.83131212]) + >>> f_modes_0 = floquet_basis.mode(0) >>> f_modes_0 # doctest: +NORMALIZE_WHITESPACE [Quantum object: dims = [[2], [1]], shape = (2, 1), type = ket Qobj data = @@ -123,26 +125,29 @@ The :math:`t=0` Floquet modes corresponding to the Hamiltonian :eq:`eq_driven_qu [[0.39993746+0.554682j] [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. 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). +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. +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). .. plot:: :context: - >>> delta = 0.2 * 2*np.pi - >>> eps0 = 0.0 * 2*np.pi - >>> omega = 1.0 * 2*np.pi + >>> delta = 0.2 * 2 * np.pi + >>> eps0 = 0.0 * 2 * np.pi + >>> omega = 1.0 * 2 * np.pi >>> A_vec = np.linspace(0, 10, 100) * omega - >>> T = (2*np.pi)/omega + >>> T = (2 * np.pi) / omega >>> tlist = np.linspace(0.0, 10 * T, 101) - >>> spsi0 = basis(2,0) + >>> spsi0 = basis(2, 0) >>> q_energies = np.zeros((len(A_vec), 2)) - >>> H0 = delta/2.0 * sigmaz() - eps0/2.0 * sigmax() + >>> H0 = delta / 2.0 * sigmaz() - eps0 / 2.0 * sigmax() >>> args = {'w': omega} >>> for idx, A in enumerate(A_vec): # doctest: +SKIP - >>> H1 = A/2.0 * sigmax() # doctest: +SKIP - >>> H = [H0, [H1, lambda t, args: np.sin(args['w']*t)]] # doctest: +SKIP - >>> f_modes, f_energies = floquet_modes(H, T, args, True) # doctest: +SKIP - >>> q_energies[idx,:] = f_energies # doctest: +SKIP + >>> H1 = A / 2.0 * sigmax() # doctest: +SKIP + >>> H = [H0, [H1, lambda t, args: np.sin(args['w'] * t)]] # doctest: +SKIP + >>> floquet_basis = FloquetBasis(H, T, args) + >>> q_energies[idx,:] = floquet_basis.e_quasi # doctest: +SKIP >>> plt.figure() # doctest: +SKIP >>> plt.plot(A_vec/omega, q_energies[:,0] / delta, 'b', A_vec/omega, q_energies[:,1] / delta, 'r') # doctest: +SKIP >>> plt.xlabel(r'$A/\omega$') # doctest: +SKIP @@ -150,12 +155,12 @@ For some problems interesting observations can be draw from the quasienergy leve >>> 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 the function :func:`qutip.floquet.floquet_mode_t`: +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 - >>> f_modes_t = floquet_modes_t(f_modes_0, f_energies, 2.5, H, T, args) + >>> f_modes_t = floquet_basis.mode(2.5) >>> f_modes_t # doctest: +SKIP [Quantum object: dims = [[2], [1]], shape = (2, 1), type = ket Qobj data = @@ -166,24 +171,25 @@ 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 :func:`qutip.floquet.floquet_state_decomposition` +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: >>> psi0 = rand_ket(2) - >>> f_coeff = floquet_state_decomposition(f_modes_0, f_energies, psi0) + >>> f_coeff = floquet_basis.to_floquet_basis(psi0) >>> f_coeff # doctest: +SKIP [(-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 :func:`qutip.floquet.floquet_wavefunction_t` +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: >>> t = 10 * np.random.rand() - >>> psi_t = floquet_wavefunction_t(f_modes_0, f_energies, f_coeff, t, H, T, args) + >>> 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`. @@ -194,7 +200,9 @@ 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 resolution. In QuTiP the function :func:`qutip.floquet.floquet_modes_table` calculates a table of Floquet modes which later can be used together with the function :func:`qutip.floquet.floquet_modes_t_lookup` to efficiently lookup the Floquet mode at an arbitrary time. 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 @@ -216,6 +224,36 @@ 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. +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 + +.. 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: + +.. 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: + +.. math:: + \gamma_{\alpha \beta k} = 2 \pi J(\Delta_{\alpha \beta k}) | X_{\alpha \beta k}|^2 + +The master equation is further simplified by the RWA, which makes the following matrix useful: + +.. math:: + A_{\alpha \beta} = \sum_{k = -\infty}^\infty [\gamma_{\alpha \beta k} + n_{th}(|\Delta_{\alpha \beta k}|)(\gamma_{\alpha \beta k} + \gamma_{\alpha \beta -k}) + +The density matrix of the system then evolves according to: + +.. math:: + \dot{\rho}_{\alpha \alpha}(t) = \sum_\nu (A_{\alpha \nu} \rho_{\nu \nu}(t) - A_{\nu \alpha} \rho_{\alpha \alpha} (t)) + +.. math:: + \dot{\rho}_{\alpha \beta}(t) = -\frac{1}{2} \sum_\nu (A_{\nu \alpha} + A_{\nu \beta}) \rho_{\alpha \beta}(t) \qquad \alpha \neq \beta + The Floquet-Markov master equation in QuTiP ------------------------------------------- @@ -233,7 +271,7 @@ The noise spectral-density function of the environment is implemented as a Pytho gamma1 = 0.1 def noise_spectrum(omega): - return 0.5 * gamma1 * omega/(2*pi) + 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 @@ -241,7 +279,7 @@ The other parameters are similar to the :func:`qutip.mesolve` and :func:`qutip.m :width: 4.0in :include-source: -Alternatively, we can let the :func:`qutip.floquet.fmmesolve` function transform the density matrix at each time step back to the computational basis, and calculating the expectation values for us, but using:: +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: - output = fmmesolve(H, psi0, tlist, [sigmax()], [num(2)], [noise_spectrum], T, args) + output = fmmesolve(H, psi0, tlist, [sigmax()], e_ops=[num(2)], spectra_cb=[noise_spectrum], T=T, args=args) p_ex = output.expect[0] diff --git a/doc/guide/dynamics/dynamics-intro.rst b/doc/guide/dynamics/dynamics-intro.rst new file mode 100644 index 0000000000..b74663ab0a --- /dev/null +++ b/doc/guide/dynamics/dynamics-intro.rst @@ -0,0 +1,63 @@ +.. _intro: + +************ +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. + +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, +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: + +.. list-table:: QuTiP Solvers + :widths: 25 25 50 + :header-rows: 1 + + * - Solver + - 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 diff --git a/doc/guide/dynamics/dynamics-krylov.rst b/doc/guide/dynamics/dynamics-krylov.rst new file mode 100644 index 0000000000..732157254b --- /dev/null +++ b/doc/guide/dynamics/dynamics-krylov.rst @@ -0,0 +1,68 @@ +.. _krylov: + +******************************************* +Krylov Solver +******************************************* + +.. _krylov-intro: + +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 + +.. 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") + >>> 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) + >>> plt.figure() + >>> for expect in results.expect: + >>> plt.plot(tlist, expect) + >>> plt.legend(('jmat x', 'jmat y', 'jmat z')) + >>> plt.xlabel('Time') + >>> plt.ylabel('Expectation values') + >>> plt.show() + +.. plot:: + :context: reset + :include-source: false + :nofigs: diff --git a/doc/guide/dynamics/dynamics-master.rst b/doc/guide/dynamics/dynamics-master.rst index 49720ea763..2fa7f87d9b 100644 --- a/doc/guide/dynamics/dynamics-master.rst +++ b/doc/guide/dynamics/dynamics-master.rst @@ -23,7 +23,9 @@ where :math:`\Psi` is the wave function, :math:`\hat H` the Hamiltonian, and :ma 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.mesolve`. It evolves the state vector and evaluates the expectation values for a set of operators ``expt_ops`` at the points in time in the list ``times``, using an ordinary differential equation solver. +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 @@ -33,17 +35,21 @@ 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, [sigmaz()]) + >>> result = sesolve(H, psi0, times, e_ops=[sigmaz()]) -The brackets in the fourth argument is an empty list of collapse operators, since we consider unitary evolution in this example. See the next section for examples on how dissipation is included by defining a list of collapse operators. +See the next section for examples on how dissipation is included by defining a list of collapse operators and using :func:`qutip.mesolve` instead. + + +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:`qutip.solve.solver.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) .. plot:: :context: - >>> result = sesolve(H, psi0, times, [sigmaz(), sigmay()]) + >>> 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, @@ -72,13 +78,13 @@ The resulting list of expectation values can easily be visualized using matplotl >>> ax.legend(("Sigma-Z", "Sigma-Y")) # doctest: +SKIP >>> plt.show() # doctest: +SKIP -If an empty list of operators is passed as fifth parameter, the :func:`qutip.mesolve` function returns a :class:`qutip.solve.solver.Result` instance that contains a list of state vectors for the times specified in ``times`` +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`` .. plot:: :context: close-figs >>> times = [0.0, 1.0] - >>> result = mesolve(H, psi0, times, [], []) + >>> result = sesolve(H, psi0, times, []) >>> result.states # doctest: +NORMALIZE_WHITESPACE [Quantum object: dims = [[2], [1]], shape = (2, 1), type = ket Qobj data = @@ -136,8 +142,8 @@ For systems with environments satisfying the conditions outlined above, the Lind 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 same function (:func:`qutip.mesolve`) is used for -evolution both according to the Schrödinger equation and to the master equation, +master equations. In QuTiP, the function :func:`qutip.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` 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 @@ -161,15 +167,16 @@ 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()`` to -the previously empty list in the fourth parameter to the :func:`qutip.mesolve` function: +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. .. plot:: :context: >>> times = np.linspace(0.0, 10.0, 100) - >>> result = mesolve(H, psi0, times, [np.sqrt(0.05) * sigmax()], [sigmaz(), sigmay()]) + >>> 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 @@ -192,7 +199,7 @@ Now a slightly more complex example: Consider a two-level atom coupled to a leak >>> 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) - >>> result = mesolve(H, psi0, times, [np.sqrt(0.1)*a], [a.dag()*a, sm.dag()*sm]) + >>> 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 diff --git a/doc/guide/dynamics/dynamics-monte.rst b/doc/guide/dynamics/dynamics-monte.rst index 797892b824..ce05c5e912 100644 --- a/doc/guide/dynamics/dynamics-monte.rst +++ b/doc/guide/dynamics/dynamics-monte.rst @@ -4,6 +4,7 @@ Monte Carlo Solver ******************************************* + .. _monte-intro: Introduction @@ -70,12 +71,14 @@ To illustrate the use of the Monte Carlo evolution of quantum systems in QuTiP, .. plot:: :context: + from qutip.solver.mcsolve import MCSolver, mcsolve + times = np.linspace(0.0, 10.0, 200) - psi0 = tensor(fock(2, 0), fock(10, 5)) + 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], [a.dag() * a, sm.dag() * sm]) + data = mcsolve(H, psi0, times, [np.sqrt(0.1) * a], e_ops=[a.dag() * a, sm.dag() * sm]) plt.figure() plt.plot(times, data.expect[0], times, data.expect[1]) @@ -89,44 +92,55 @@ To illustrate the use of the Monte Carlo evolution of quantum systems in QuTiP, 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. -Like the master equation solver :func:`qutip.mesolve`, the Monte Carlo solver returns a :class:`qutip.solve.solver.Result` object consisting of expectation values, if the user has defined expectation value operators in the 5th argument to ``mcsolve``, or state vectors if no expectation value operators are given. If state vectors are returned, then the :class:`qutip.solve.solver.Result` returned by :func:`qutip.mcsolve` will be an array of length ``ntraj``, with each element containing an array of ket-type qobjs with the same number of elements as ``times``. Furthermore, the output :class:`qutip.solve.solver.Result` object will also contain a list of times at which collapse occurred, and which collapse operators did the collapse, in the ``col_times`` and ``col_which`` properties, respectively. - -.. _monte-ntraj: +Monte Carlo Solver Result +------------------------- -Changing the Number of Trajectories ------------------------------------ +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. +Lastly ``result.photocurrent`` contain the measurement of the evolution. -As mentioned earlier, 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, like many other options in QuTiP you are free to change the number of trajectories to fit your needs. If we want to run 1000 trajectories in the above example, we can simply modify the call to ``mcsolve`` like: .. plot:: :context: close-figs - data = mcsolve(H, psi0, times, [np.sqrt(0.1) * a], [a.dag() * a, sm.dag() * sm], ntraj=1000) + 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]) -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. Sometimes one is also interested in seeing how the Monte Carlo trajectories converge to the master equation solution by calculating expectation values over a range of trajectory numbers. If, for example, we want to average over 1, 10, 100, and 1000 trajectories, then we can input this into the solver using: + 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() -.. plot:: - :context: - ntraj = [1, 10, 100, 1000] +.. _monte-ntraj: + +Changing the Number of Trajectories +----------------------------------- -Keep in mind that the input list must be in ascending order since the total number of trajectories run by ``mcsolve`` will be calculated using the last element of ``ntraj``. In this case, we need to use an extra index when getting the expectation values from the :class:`qutip.solve.solver.Result` object returned by ``mcsolve``. In the above example using: +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: + :context: close-figs - data = mcsolve(H, psi0, times, [np.sqrt(0.1) * a], [a.dag() * a, sm.dag() * sm], ntraj=[1, 10, 100, 1000]) + data = mcsolve(H, psi0, times, [np.sqrt(0.1) * a], e_ops=[a.dag() * a, sm.dag() * sm], ntraj=1000) -we can extract the relevant expectation values using: +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. -.. plot:: - :context: - - expt1 = data.expect[0] - expt10 = data.expect[1] - expt100 = data.expect[2] - expt1000 = data.expect[3] .. _monte-reuse: @@ -136,12 +150,16 @@ Reusing Hamiltonian Data .. note:: This section covers a specialized topic and may be skipped if you are new to QuTiP. -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 Hamiltonian and other operators may be reused after the initial configuration, thus speeding up calculations. Note that, unless you are planning to reuse the data many times, this functionality will not be very useful. + +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. + 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: + :context: close-figs times = np.linspace(0.0, 10.0, 200) psi0 = tensor(fock(2, 0), fock(10, 5)) @@ -149,20 +167,71 @@ Using the previous example, we will calculate the dynamics for two different ini 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) - data1 = mcsolve(H, psi0, times, [np.sqrt(0.1) * a], [a.dag() * a, sm.dag() * sm]) + 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)) - opts = SolverOptions() # Run a second time, reusing RHS - data2 = mcsolve(H, psi1, times, [np.sqrt(0.1) * a], [a.dag() * a, sm.dag() * sm], options=opts) + 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() + +.. guide-dynamics-mc2: + +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. + +.. plot:: + :context: close-figs + + 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)) + + 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 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() -.. guide-dynamics-mc2: -In addition to the initial state, one may reuse the Hamiltonian data when changing the number of trajectories ``ntraj`` or simulation times ``times``. The reusing of Hamiltonian data is also supported for time-dependent Hamiltonians. See :ref:`time` for further details. +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 + + 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) + 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) + + 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() diff --git a/doc/guide/dynamics/dynamics-options.rst b/doc/guide/dynamics/dynamics-options.rst index b7054d9a1a..587000ca84 100644 --- a/doc/guide/dynamics/dynamics-options.rst +++ b/doc/guide/dynamics/dynamics-options.rst @@ -6,7 +6,7 @@ Setting Options for the Dynamics Solvers .. testsetup:: [dynamics_options] - from qutip.solver.mesolve import MeSolver, mesolve + 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. @@ -17,33 +17,31 @@ The options for all dynamics solvers may be changed by using the dictionaries. options = {"store_states": True, "atol": 1e-12} Supported items come from 2 sources, the solver and the ODE integration method. -Supported solver options and their default can be seen with the class interface: +Supported solver options and their default can be seen using the class interface: .. testcode:: [dynamics_options] - help(MeSolver.options) + 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: .. testcode:: [dynamics_options] - help(MeSolver.integrator("adams")) + help(MESolver.integrator("adams").options) See `Integrator <../../apidoc/classes.html#classes-ode>`_ for a list of supported methods. -As an example, let us consider changing the number of processors used, turn the GUI off, and strengthen the absolute tolerance. There are two equivalent ways to do this using the SolverOptions class. First way, +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} + 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. We can modify the last example as:: +To use these new settings we can use the keyword argument ``options`` in either the :func:`qutip.mesolve` and :func:`qutip.mcsolve` function:: >>> mesolve(H0, psi0, tlist, c_op_list, [sigmaz()], options=options) - >>> MeSolver(hamiltonian_t, c_op_list, options=options) or:: - >>> mcsolve(H0, psi0, tlist, ntraj,c_op_list, [sigmaz()], options=options) - >>> mcsolve(hamiltonian_t, psi0, tlist, ntraj, c_op_list, [sigmaz()], H_args, options=options) + >>> MCSolver(H0, c_op_list, options=options) diff --git a/doc/guide/dynamics/dynamics-photocurrent.rst b/doc/guide/dynamics/dynamics-photocurrent.rst index 6827dbe4b5..5b69a338d4 100644 --- a/doc/guide/dynamics/dynamics-photocurrent.rst +++ b/doc/guide/dynamics/dynamics-photocurrent.rst @@ -63,7 +63,7 @@ Here :math:`\delta N = 1` with a probability of :math:`\delta \omega` and Trajectories obtained with this algorithm are equivalent to those obtained with monte-carlo evolution (up to :math:`O(\delta t^2)`). In most cases, :func:`qutip.mcsolve` is more efficient than -:func:`qutip.photocurrent_sesolve`. +:func:`qutip.stochastic.photocurrent_sesolve`. Open system ----------- diff --git a/doc/guide/dynamics/dynamics-time.rst b/doc/guide/dynamics/dynamics-time.rst index 802e79e91d..8902d2b94d 100644 --- a/doc/guide/dynamics/dynamics-time.rst +++ b/doc/guide/dynamics/dynamics-time.rst @@ -5,82 +5,75 @@ Solving Problems with Time-dependent Hamiltonians ************************************************* -Methods for Writing Time-Dependent Operators -============================================ +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 -:func:`qutip.solve.mesolve`, :func:`qutip.solve.mcsolve`, :func:`qutip.solve.sesolve`, :func:`qutip.solve.brmesolve` -:func:`qutip.solve.ssesolve`, :func:`qutip.solve.photocurrent_sesolve`, :func:`qutip.solve.smesolve`, and :func:`qutip.solve.photocurrent_mesolve` -are all capable of handling time-dependent Hamiltonians and collapse terms. -There are, in general, three different ways to implement time-dependent problems in QuTiP: +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`: : -1. **Function based**: Hamiltonian / collapse operators expressed using [qobj, func] pairs, where the time-dependent coefficients of the Hamiltonian (or collapse operators) are expressed using Python functions. +1. **Function based**: Build the time dependent operator from a function returning a :class:`Qobj`: -2. **String (Cython) based**: The Hamiltonian and/or collapse operators are expressed as a list of [qobj, string] pairs, where the time-dependent coefficients are represented as strings. The resulting Hamiltonian is then compiled into C code using Cython and executed. +.. code-block:: python -3. **Array Based**: The Hamiltonian and/or collapse operators are expressed as a list of [qobj, np.array] pairs. The arrays are 1 dimensional and dtype are complex or float. They must contain one value for each time in the tlist given to the solver. Cubic spline interpolation will be used between the given times. + def oper(t): + return num(N) + (destroy(N) + create(N)) * np.sin(t) + H_t = QobjEvo(oper) -4. **Hamiltonian function (outdated)**: The Hamiltonian is itself a Python function with time-dependence. Collapse operators must be time independent using this input format. +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)]]) -Give the multiple choices of input style, the first question that arrises is which option to choose? -In short, the function based method (option #1) is the most general, -allowing for essentially arbitrary coefficients expressed via user defined functions. -However, by automatically compiling your system into C++ code, -the second option (string based) tends to be more efficient and will run faster -[This is also the only format that is supported in the :func:`qutip.solve.brmesolve` solver]. -Of course, for small system sizes and evolution times, the difference will be minor. -Although this method does not support all time-dependent coefficients that one can think of, -it does support essentially all problems that one would typically encounter. -Time-dependent coefficients using any of the following functions, -or combinations thereof (including constants) can be compiled directly into C++-code:: - 'abs', 'acos', 'acosh', 'arg', 'asin', 'asinh', 'atan', 'atanh', 'conj', - 'cos', 'cosh','exp', 'erf', 'zerf', 'imag', 'log', 'log10', 'norm', 'pi', - 'proj', 'real', 'sin', 'sinh', 'sqrt', 'tan', 'tanh' +3. **coefficent based**: The product of a :class:`Qobj` with a :class:`Coefficient` result in a :class:`QobjEvo`: -In addition, QuTiP supports cubic spline based interpolation functions [:ref:`time-interp`]. +.. 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. -If you require mathematical functions other than those listed above, -it is possible to call any of the functions in the NumPy library using the prefix ``np.`` -before the function name in the string, i.e ``'np.sin(t)'`` and ``scipy.special`` imported as ``spe``. -This includes a wide range of functionality, but comes with a small overhead created by going from C++->Python->C++. -Finally option #4, expressing the Hamiltonian as a Python function, -is the original method for time dependence in QuTiP 1.x. -However, this method is somewhat less efficient then the previously mentioned methods. -However, in contrast to the other options -this method can be used in implementing time-dependent Hamiltonians that cannot be -expressed as a function of constant operators with time-dependent coefficients. +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. -A collection of examples demonstrating the simulation of time-dependent problems can be found on the `tutorials `_ web page. -.. _time-function: +Most solvers will accept any format that could be made into a :class:`QobjEvo`: for the Hamiltonian. +All of the following are equivalent: + + +.. code-block:: python -Function Based Time Dependence -============================== + result = mesolve(H_t, ...) + result = mesolve([num(N), [destroy(N) + create(N), lambda t: np.sin(t)]], ...) + result = mesolve(oper, ...) -A very general way to write a time-dependent Hamiltonian or collapse operator is by using Python functions as the time-dependent coefficients. To accomplish this, we need to write a Python function that returns the time-dependent coefficient. Additionally, we need to tell QuTiP that a given Hamiltonian or collapse operator should be associated with a given Python function. To do this, one needs to specify operator-function pairs in list format: ``[Op, py_coeff]``, where ``Op`` is a given Hamiltonian or collapse operator and ``py_coeff`` is the name of the Python function representing the coefficient. With this format, the form of the Hamiltonian for both ``mesolve`` and ``mcsolve`` is: ->>> H = [H0, [H1, py_coeff1], [H2, py_coeff2], ...] # doctest: +SKIP +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: -where ``H0`` is a time-independent Hamiltonian, while ``H1``and ``H2`` are time dependent. The same format can be used for collapse operators: +.. code-block:: python ->>> c_ops = [[C0, py_coeff0], C1, [C2, py_coeff2], ...] # doctest: +SKIP + result = mesolve(H_t, ..., c_ops=[num(N), [destroy(N) + create(N), lambda t: np.sin(t)]]) -Here we have demonstrated that the ordering of time-dependent and time-independent terms does not matter. In addition, any or all of the collapse operators may be time dependent. +: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`:. -.. note:: While, in general, you can arrange time-dependent and time-independent terms in any order you like, it is best to place all time-independent terms first. -As an example, we will look at an example that has 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 +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:: - :context: + :context: close-figs ustate = basis(3, 0) excited = basis(3, 1) @@ -115,16 +108,18 @@ Given that we have a single time-dependent Hamiltonian term, and constant collap .. plot:: :context: + :nofigs: - def H1_coeff(t, args): + def H1_coeff(t): return 9 * np.exp(-(t / 5.) ** 2) -In this case, the return value dependents only on time. However, when specifying Python functions for coefficients, **the function must have (t,args) as the input variables, in that order**. 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:`qutip.mesolve`) .. plot:: - :context: + :context: close-figs - H = [H0,[H1, H1_coeff]] + H = [H0, [H1, H1_coeff]] output = mesolve(H, psi0, t, c_ops, [ada, sigma_UU, sigma_GG]) We can call the Monte Carlo solver in the exact same way (if using the default ``ntraj=500``): @@ -138,10 +133,11 @@ 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: + :context: close-figs kappa = 0.5 @@ -152,229 +148,290 @@ The output from the master equation solver is identical to that shown in the exa a = destroy(N) H = a.dag() * a # simple HO psi0 = basis(N, 9) # initial state - c_ops = [[a, col_coeff]] # time-dependent collapse term + c_ops = [QobjEvo([a, col_coeff])] # time-dependent collapse term times = np.linspace(0, 10, 100) output = mesolve(H, psi0, times, c_ops, [a.dag() * a]) -Using the args variable ------------------------- -In the previous example we hardcoded all of the variables, driving amplitude :math:`A` and width :math:`\sigma`, 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 to change the parameters of the problem in only one location (usually at the top of the script), and not have to worry about manually changing the values on each run. QuTiP allows you to accomplish this using the keyword ``args`` as an input to the solvers. For instance, instead of explicitly writing 9 for the amplitude and 5 for the width of the gaussian driving term, we can make us of the args variable -.. plot:: - :context: +Qobjevo +======= - def H1_coeff(t, args): - return args['A'] * np.exp(-(t/args['sigma'])**2) +:class:`QobjEvo` as a time dependent quantum system, as it's main functionality create a :class:`Qobj` at a time: -or equivalently, +.. doctest:: [basics] + :options: +NORMALIZE_WHITESPACE -.. plot:: - :context: + >>> print(H_t(np.pi / 2)) + Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', isherm=True + Qobj data = + [[0. 1.] + [1. 1.]] - def H1_coeff(t, args): - A = args['A'] - sig = args['sigma'] - return A * np.exp(-(t / sig) ** 2) -where args is a Python dictionary of ``key: value`` pairs ``args = {'A': a, 'sigma': b}`` where ``a`` and ``b`` are the two parameters for the amplitude and width, respectively. Of course, we can always hardcode the values in the dictionary as well ``args = {'A': 9, 'sigma': 5}``, but there is much more flexibility by using variables in ``args``. To let the solvers know that we have a set of args to pass we append the ``args`` to the end of the solver input: +:class:`QobjEvo` shares a lot of properties with the :class:`Qobj`. -.. plot:: - :context: ++---------------+------------------+----------------------------------------+ +| 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? | ++---------------+------------------+----------------------------------------+ - output = mesolve(H, psi0, times, c_ops, [a.dag() * a], args={'A': 9, 'sigma': 5}) -or to keep things looking pretty +:class:`QobjEvo`'s follow the same mathematical operations rules than :class:`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: -.. plot:: - :context: +.. code-block:: python + + H = tensor(H_t, qeye(2)) + c_op = tensor(QobjEvo([destroy(N), lambda t: np.exp(-t)]), sigmax()) - args = {'A': 9, 'sigma': 5} - output = mesolve(H, psi0, times, c_ops, [a.dag() * a], args=args) + L = -1j * (spre(H) - spost(H.dag())) + L += spre(c_op) * spost(c_op.dag()) - 0.5 * spre(c_op.dag() * c_op) - 0.5 * spost(c_op.dag() * c_op) -Once again, the Monte Carlo solver :func:`qutip.mcsolve` works in an identical manner. -.. _time-string: +Or equivalently: + +.. code-block:: python + + L = liouvillian(H, [c_op]) -String Format Method -===================== -.. note:: You must have Cython installed on your computer to use this format. See :ref:`install` for instructions on installing Cython. +Using arguments +--------------- -The string-based time-dependent format works in a similar manner as the previously discussed Python function method. That being said, the underlying code does something completely different. When using this format, the strings used to represent the time-dependent coefficients, as well as Hamiltonian and collapse operators, are rewritten as Cython code using a code generator class and then compiled into C code. The details of this meta-programming will be published in due course. however, in short, this can lead to a substantial reduction in time for complex time-dependent problems, or when simulating over long intervals. +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. +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: -Like the previous method, the string-based format uses a list pair format ``[Op, str]`` where ``str`` is now a string representing the time-dependent coefficient. For our first example, this string would be ``'9 * exp(-(t / 5.) ** 2)'``. The Hamiltonian in this format would take the form: .. plot:: - :context: + :context: close-figs + + def H1_coeff(t, args): + return args['A'] * np.exp(-(t/args['sigma'])**2) - ustate = basis(3, 0) - excited = basis(3, 1) - ground = basis(3, 2) - N = 2 # Set where to truncate Fock state for cavity +or, new from v5, add the extra parameter directly: - sigma_ge = tensor(qeye(N), ground * excited.dag()) # |g>u - c_ops.append(np.sqrt(4*gamma/9) * sigma_ge) # 4/9 e->g +.. plot:: + :context: close-figs - t = np.linspace(-15, 15, 100) # Define time vector - psi0 = tensor(basis(N, 0), ustate) # Define initial state - state_GG = tensor(basis(N, 1), ground) # Define states onto which to project - sigma_GG = state_GG * state_GG.dag() - state_UU = tensor(basis(N, 0), ustate) - sigma_UU = state_UU * state_UU.dag() + def H1_coeff(t, A, sigma): + return A * np.exp(-(t / sigma)**2) - g = 5 # coupling strength - H0 = -g * (sigma_ge.dag() * a + a.dag() * sigma_ge) # time-independent term - H1 = (sigma_ue.dag() + sigma_ue) # time-dependent term +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: + :context: close-figs - H = [H0, [H1, '9 * exp(-(t / 5) ** 2)']] + system = [H0, [H1, H1_coeff]] + args={'A': 9, 'sigma': 5} + qevo = QobjEvo(system, args=args) -Notice that this is a valid Hamiltonian for the string-based format as ``exp`` is included in the above list of suitable functions. Calling the solvers is the same as before: +But without ``args``, the :class:`QobjEvo` creation will fail: .. plot:: - :context: + :context: close-figs - output = mesolve(H, psi0, t, c_ops, [a.dag() * a]) + try: + QobjEvo(system) + except TypeError as err: + print(err) -We can also use the ``args`` variable in the same manner as before, however we must rewrite our string term to read: ``'A * exp(-(t / sig) ** 2)'`` +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: .. plot:: - :context: + :context: close-figs - H = [H0, [H1, 'A * exp(-(t / sig) ** 2)']] - args = {'A': 9, 'sig': 5} - output = mesolve(H, psi0, times, c_ops, [a.dag()*a], args=args) + print(qevo(1)) + print(qevo(1, {"A": 5, "sigma": 0.2})) + print(qevo(1, A=5)) -.. important:: Naming your ``args`` variables ``exp``, ``sin``, ``pi`` etc. will cause errors when using the string-based format. +Whether the original coefficient used the ``args`` or specific input does not matter. +It is fine to mix the different signatures. -Collapse operators are handled in the exact same way. +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. +.. code-block:: python + + def system(t, A, sigma): + return H0 + H1 * (A * np.exp(-(t / sigma)**2)) + + mesolve(system, ..., args=args) -.. _time-interp: -Modeling Non-Analytic and/or Experimental Time-Dependent Parameters using Interpolating Functions -================================================================================================= +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``: -Sometimes it is necessary to model a system where the time-dependent parameters are non-analytic functions, or are derived from experimental data (i.e. a collection of data points). -In these situations, one can use interpolating functions as an approximate functional form for input into a time-dependent solver. -QuTiP support spline interpolation in it's :class:`qutip.Coefficient`. -To see how this works, lets first generate some noisy data: .. plot:: - :context: + :context: close-figs + + 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)) - times = np.linspace(-15, 15, 100) - func = lambda t: 9 * np.exp(-(t / 5)** 2) - noisy_data = func(times) * (1 + 0.05 * np.random.randn(len(times))) - plt.figure() - plt.plot(times, func(times)) - plt.plot(times, noisy_data, 'o') - plt.show() +:class:`QobjEvo` created from a monolithic function can also use arguments: -To turn these data points into a function we call the QuTiP :func:`qutip.coefficient` using the array of data points, times to which these are measured and spline interpolation order : +.. code-block:: python + + def oper(t, w): + return num(N) + (destroy(N) + create(N)) * np.sin(t*w) + + 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: .. plot:: :context: close-figs - S = coefficient(noisy_data, tlist=times, order=3) + 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)) + - plt.figure() - plt.plot(times, func(times)) - plt.plot(times, noisy_data, 'o') - plt.plot(times, [S(t).real for t in times], lw=2) - plt.show() +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``. + + +**Function coefficients** : +Use a callable with the signature ``f(t: double, ...) -> double`` as coefficient. +Any function or method that can be called by ``f(t, args)``, ``f(t, **args)`` is accepted. -This :class:`qutip.Coefficient` instance can now be used in any of the solver supporting time-dependent operators, such as the ``mesolve``, ``mcsolve``, or ``sesolve`` functions. -Taking the problem from the previous section as an example. -We would make the replacement: .. code-block:: python - H = [H0, [H1, '9 * exp(-(t / 5) ** 2)']] + def coeff(t, A, sigma): + return A * np.exp(-(t / sigma)**2) + + H = QobjEvo([H0, [H1, coeff]], args=args) + + +**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 excution 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 Cython is not available, the code will be executed in python with the same environment. +This, however, as no advantage over using python function. -to .. code-block:: python - H = [H0, [H1, S]] + coeff = "A * exp(-(t / sigma)**2)" + H = QobjEvo([H0, [H1, coeff]], args=args) -When combining interpolating functions with other Python functions or strings, the interpolating class will automatically pick the appropriate method for calling the class. That is to say that, if for example, you have other time-dependent terms that are given in the string-format, then the cubic spline representation will also be passed in a string-compatible format. In the string-format, the interpolation function is compiled into c-code, and thus is quite fast. This is the default method if no other time-dependent terms are present. +Here is a list of defined variables: + ``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``, + ``np`` (numpy) and ``spe`` (scipy.special). + + +**Array coefficients** : +Use the spline interpolation of an array. +Useful when the coefficient is hard to define as a function or obtained from experimental data. +The times at which the array are defined must be passed as ``tlist``: + +.. code-block:: python + + times = np.linspace(-sigma*5, sigma*5, 500) + coeff = A * exp(-(times / sigma)**2) + + H = QobjEvo([H0, [H1, coeff]], tlist=times) -.. _time-dynargs: -Accesing the state from solver -============================== - -New in QuTiP 4.4 - -The state of the system, the ket vector or the density matrix, -is available to time-dependent Hamiltonian and collapse operators in ``args``. -Some keys of the argument dictionary are understood by the solver to be values -to be updated with the evolution of the system. -The state can be obtained in 3 forms: ``Qobj``, vector (1d ``np.array``), matrix (2d ``np.array``), -expectation values and collapse can also be obtained. - -+-------------------+-------------------------+----------------------+------------------------------------------------------------------+ -| | Preparation | usage | Notes | -+-------------------+-------------------------+----------------------+------------------------------------------------------------------+ -| state as Qobj | ``name+"=Qobj":psi0`` | ``psi_t=args[name]`` | The ket or density matrix as a Qobj with ``psi0``'s dimensions | -+-------------------+-------------------------+----------------------+------------------------------------------------------------------+ -| state as matrix | ``name+"=mat":psi0`` | ``mat_t=args[name]`` | The state as a matrix, equivalent to ``state.full()`` | -+-------------------+-------------------------+----------------------+------------------------------------------------------------------+ -| state as vector | ``name+"=vec":psi0`` | ``vec_t=args[name]`` | The state as a vector, equivalent to ``state.full().ravel('F')`` | -+-------------------+-------------------------+----------------------+------------------------------------------------------------------+ -| expectation value | ``name+"=expect":O`` | ``e=args[name]`` | Expectation value of the operator ``O``, either | -| | | | :math:`\left<\psi(t)|O|\psi(t)\right>` | -| | | | or :math:`\rm{tr}\left(O \rho(t)\right)` | -+-------------------+-------------------------+----------------------+------------------------------------------------------------------+ -| collpases | ``name+"=collapse":[]`` | ``col=args[name]`` | List of collapse, | -| | | | each collapse is a tuple of the pair ``(time, which)`` | -| | | | ``which`` being the indice of the collapse operator. | -| | | | ``mcsolve`` only. | -+-------------------+-------------------------+----------------------+------------------------------------------------------------------+ - -Here ``psi0`` is the initial value used for tests before the evolution begins. -:func:`qutip.brmesolve` does not support these arguments. - -Reusing Time-Dependent Hamiltonian Data -======================================= - -.. note:: This section covers a specialized topic and may be skipped if you are new to QuTiP. - -When repeatedly simulating a system where only the time-dependent variables, or initial state change, it is possible to reuse the Hamiltonian data stored in QuTiP and there by avoid spending time needlessly preparing the Hamiltonian and collapse terms for simulation. -To turn on the reuse features, we must use the class interface of the solver: +Per default, a cubic spline interpolation is used, but the order of the interpolation can be controlled with the order input: +Outside the interpolation range, the first or last value are used. .. plot:: :context: close-figs - from qutip.solver.mesolve import MeSolver - H = QobjEvo([H0, [H1, 'A * exp(-(t / sig)**2)']], args={'A': 0, 'sig': 1}) - solver = MeSolver(H, c_ops) - args = {'A': 9, 'sig': 5} - output = solver.run(psi0, times, e_ops=[a.dag()*a], args=args) - args = {'A': 10, 'sig': 3} - output = solver.run(psi0, times, e_ops=[a.dag()*a], args=args) + times = np.array([0, 0.1, 0.3, 0.6, 1.0]) + coeff = times * (1.1 - times) + tlist = np.linspace(-0.1, 1.1, 25) + + H = QobjEvo([qeye(1), coeff], tlist=times) + plt.plot(tlist, [H(t).norm() for t in tlist], label="CubicSpline") + + H = QobjEvo([qeye(1), coeff], tlist=times, order=0) + plt.plot(tlist, [H(t).norm() for t in tlist], label="step") + + H = QobjEvo([qeye(1), coeff], tlist=times, order=1) + plt.plot(tlist, [H(t).norm() for t in tlist], label="linear") + + 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. +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: + + +.. plot:: + :context: close-figs + + N = 5 + times = np.linspace(-0.1, 1.1, 13) + 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 + ) + + +Different coefficient types can be mixed in a :class:`QobjEvo`. + + +Given the multiple choices of input style, the first question that arises is which option to choose? +In short, the function based method (first option) is the most general, +allowing for essentially arbitrary coefficients expressed via user defined functions. +However, by automatically compiling your system into C++ code, +the second option (string based) tends to be more efficient and run faster. +Of course, for small system sizes and evolution times, the difference will be minor. +Lastly the spline method is usually as fast the string method, but it cannot be modified once created. + + +.. _time-dynargs: + +Accessing the state from solver +=============================== -The preparation of the Liouvillian and in the case of the string format, compilation of the Cython code, is done once in the initialization of the solver instance. -For the small system here, the savings in computation time is quite small, however, if you need to call the solvers many times for different parameters, this savings will obviously start to add up. +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. diff --git a/doc/guide/guide-basics.rst b/doc/guide/guide-basics.rst index 1be70997ac..072b9a939d 100644 --- a/doc/guide/guide-basics.rst +++ b/doc/guide/guide-basics.rst @@ -114,8 +114,8 @@ We can create a ``Qobj`` with a user defined data set by passing a list or array [0.60111501 0.70807258 0.02058449 0.96990985] [0.83244264 0.21233911 0.18182497 0.18340451]] -Notice how both the dims and shape change according to the input data. Although dims and shape appear to be the same, -dims keep track of the shapes for individual components of a multipartite system, while shape does not. We refer the reader to the section +Notice how both the dims and shape change according to the input data. Although dims and shape appear to be the same, +dims keep track of the shapes for individual components of a multipartite system, while shape does not. We refer the reader to the section :ref:`tensor products and partial traces ` for more information. .. note:: If you are running QuTiP from a python script you must use the :func:`print` function to view the Qobj attributes. @@ -125,9 +125,9 @@ dims keep track of the shapes for individual components of a multipartite system States and operators --------------------- -Manually specifying the data for each quantum object is inefficient. Even more so when most objects correspond to commonly used -types such as the -ladder operators of a harmonic oscillator, the Pauli spin operators for a two-level system, or state vectors such as Fock states. +Manually specifying the data for each quantum object is inefficient. Even more so when most objects correspond to commonly used +types such as the +ladder operators of a harmonic oscillator, the Pauli spin operators for a two-level system, or state vectors such as Fock states. Therefore, QuTiP includes predefined objects for a variety of states and operators: .. cssclass:: table-striped @@ -139,6 +139,8 @@ Therefore, QuTiP includes predefined objects for a variety of states and operato | | | m = level containing excitation | | | | (0 if no m given) | +--------------------------+----------------------------------+----------------------------------------+ +| Empty ket vector | ``zero_ket(N)`` | N = number of levels in Hilbert space, | ++--------------------------+----------------------------------+----------------------------------------+ | Fock density matrix | ``fock_dm(N,#p)`` | same as basis(N,m) / fock(N,m) | | (outer product of basis) | | | +--------------------------+----------------------------------+----------------------------------------+ @@ -151,6 +153,9 @@ Therefore, QuTiP includes predefined objects for a variety of states and operato | Thermal density matrix | ``thermal_dm(N,n)`` | n = particle number expectation value | | (for n particles) | | | +--------------------------+----------------------------------+----------------------------------------+ +| Maximally mixed density | ``maximally_mixed_dm(N)`` | N = number of levels in Hilbert space | +| matrix | | | ++--------------------------+----------------------------------+----------------------------------------+ .. cssclass:: table-striped @@ -274,7 +279,7 @@ We have seen that a quantum object has several internal attributes, such as data >>> q.shape (4, 4) -In general, the attributes (properties) of a ``Qobj`` object (or any Python object) can be retrieved using the `Q.attribute` notation. +In general, the attributes (properties) of a ``Qobj`` object (or any Python object) can be retrieved using the `Q.attribute` notation. In addition to the those shown with the ``print`` function, an instance of the ``Qobj`` class also has the following attributes: .. cssclass:: table-striped @@ -417,13 +422,8 @@ Like attributes, the quantum object class has defined functions (methods) that o +-----------------+-------------------------------+----------------------------------------+ | Eigenstates | ``Q.eigenstates()`` | Returns eigenvalues and eigenvectors. | +-----------------+-------------------------------+----------------------------------------+ -| Eliminate States| ``Q.eliminate_states(inds)`` | Returns quantum object with states in | -| | | list inds removed. | -+-----------------+-------------------------------+----------------------------------------+ | Exponential | ``Q.expm()`` | Matrix exponential of operator. | +-----------------+-------------------------------+----------------------------------------+ -| Extract States | ``Q.extract_states(inds)`` | Qobj with states listed in inds only. | -+-----------------+-------------------------------+----------------------------------------+ | Full | ``Q.full()`` | Returns full (not sparse) array of | | | | Q's data. | +-----------------+-------------------------------+----------------------------------------+ diff --git a/doc/guide/guide-bloch.rst b/doc/guide/guide-bloch.rst index 74aad63ebd..976264320e 100644 --- a/doc/guide/guide-bloch.rst +++ b/doc/guide/guide-bloch.rst @@ -9,7 +9,7 @@ Plotting on the Bloch Sphere Introduction ============ -When studying the dynamics of a two-level system, it is often convenient to visualize the state of the system by plotting the state-vector or density matrix on the Bloch sphere. In QuTiP, we have created two different classes to allow for easy creation and manipulation of data sets, both vectors and data points, on the Bloch sphere. The :class:`qutip.Bloch` class, uses Matplotlib to render the Bloch sphere, where as :class:`qutip.Bloch3d` uses the Mayavi rendering engine to generate a more faithful 3D reconstruction of the Bloch sphere. +When studying the dynamics of a two-level system, it is often convenient to visualize the state of the system by plotting the state-vector or density matrix on the Bloch sphere. In QuTiP, we have created two different classes to allow for easy creation and manipulation of data sets, both vectors and data points, on the Bloch sphere. 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. .. _bloch-class: @@ -23,11 +23,11 @@ In QuTiP, creating a Bloch sphere is accomplished by calling either: b = qutip.Bloch() -which will load an instance of the :class:`qutip.Bloch` class, or using :: +which will load an instance of the :class:`qutip.bloch.Bloch` class, or using :: >>> b3d = qutip.Bloch3d() -that loads the :class:`qutip.Bloch3d` version. Before getting into the details of these objects, we can simply plot the blank Bloch sphere associated with these instances via: +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: .. plot:: :context: @@ -42,7 +42,7 @@ or :width: 3.5in :figclass: align-center -In addition to the ``show`` command, see the API documentation for :class:`~Bloch` for a full list of other available functions. +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: .. plot:: @@ -156,7 +156,7 @@ Notice that, in contrast to states or vectors, each point remains the same color The color and shape of the data points is varied automatically by the Bloch class. Notice how the color and point markers change for each set of data. Again, we have had to call ``add_points`` twice because adding more than one set of multiple data points is *not* supported by the ``add_points`` function. -What if we want to vary the color of our points. We can tell the :class:`qutip.Bloch` class to vary the color of each point according to the colors listed in the ``b.point_color`` list (see :ref:`bloch-config` below). Again after ``clear()``: +What if we want to vary the color of our points. We can tell the :class:`qutip.bloch.Bloch` class to vary the color of each point according to the colors listed in the ``b.point_color`` list (see :ref:`bloch-config` below). Again after ``clear()``: .. plot:: :context: close-figs @@ -183,7 +183,7 @@ 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` class by replacing ``Bloch`` with ``Bloch3d``: +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 diff --git a/doc/guide/guide-control.rst b/doc/guide/guide-control.rst index b8305b4a51..f3c31ad7e9 100644 --- a/doc/guide/guide-control.rst +++ b/doc/guide/guide-control.rst @@ -59,7 +59,7 @@ We can use optimal control algorithms to determine a set of :math:`u_j` that wil The GRAPE algorithm =================== -The **GR**\ adient **A**\ scent **P**\ ulse **E**\ ngineering was first proposed in [2]. Solutions to Schrödinger's equation for a time-dependent Hamiltonian are not generally possible to obtain analytically. Therefore, a piecewise constant approximation to the pulse amplitudes is made. Time allowed for the system to evolve :math:`T` is split into :math:`M` timeslots (typically these are of equal duration), during which the control amplitude is assumed to remain constant. The combined Hamiltonian can then be approximated as: +The **GR**\ adient **A**\ scent **P**\ ulse **E**\ ngineering was first proposed in [NKanej]_. Solutions to Schrödinger's equation for a time-dependent Hamiltonian are not generally possible to obtain analytically. Therefore, a piecewise constant approximation to the pulse amplitudes is made. Time allowed for the system to evolve :math:`T` is split into :math:`M` timeslots (typically these are of equal duration), during which the control amplitude is assumed to remain constant. The combined Hamiltonian can then be approximated as: .. math:: diff --git a/doc/guide/guide-correlation.rst b/doc/guide/guide-correlation.rst index 36e68c229e..6353077608 100644 --- a/doc/guide/guide-correlation.rst +++ b/doc/guide/guide-correlation.rst @@ -28,21 +28,24 @@ QuTiP provides a family of functions that assists in the process of calculating .. cssclass:: table-striped -+----------------------------------------------+--------------------------------------------------+ -| QuTiP function | Correlation function | -+==============================================+==================================================+ -| | :math:`\left` or | -| :func:`qutip.correlation.correlation_2op_2t` | :math:`\left`. | -+----------------------------------------------+--------------------------------------------------+ -| | :math:`\left` or | -| :func:`qutip.correlation.correlation_2op_1t` | :math:`\left`. | -+----------------------------------------------+--------------------------------------------------+ -| :func:`qutip.correlation.correlation_3op_1t` | :math:`\left`. | -+----------------------------------------------+--------------------------------------------------+ -| :func:`qutip.correlation.correlation_3op_2t` | :math:`\left`. | -+----------------------------------------------+--------------------------------------------------+ - -The most common use-case is to calculate correlation functions of the kind :math:`\left`, in which case we use the correlation function solvers that start from the steady state, e.g., the :func:`qutip.correlation.correlation_2op_1t` function. These correlation function solvers return a vector or matrix (in general complex) with the correlations as a function of the delays times. ++----------------------------------+--------------------------------------------------+ +| QuTiP function | Correlation function | ++==================================+==================================================+ +| | :math:`\left` or | +| :func:`qutip.correlation_2op_2t` | :math:`\left`. | ++----------------------------------+--------------------------------------------------+ +| | :math:`\left` or | +| :func:`qutip.correlation_2op_1t` | :math:`\left`. | ++----------------------------------+--------------------------------------------------+ +| :func:`qutip.correlation_3op_1t` | :math:`\left`. | ++----------------------------------+--------------------------------------------------+ +| :func:`qutip.correlation_3op_2t` | :math:`\left`. | ++----------------------------------+--------------------------------------------------+ +| :func:`qutip.correlation_3op` | :math:`\left`. | ++----------------------------------+--------------------------------------------------+ + + +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`. .. _correlation-steady: diff --git a/doc/guide/guide-dynamics.rst b/doc/guide/guide-dynamics.rst index 9bc85905d1..5f214bb8b3 100644 --- a/doc/guide/guide-dynamics.rst +++ b/doc/guide/guide-dynamics.rst @@ -7,9 +7,11 @@ Time Evolution and Quantum System Dynamics .. toctree:: :maxdepth: 2 + dynamics/dynamics-intro.rst dynamics/dynamics-data.rst dynamics/dynamics-master.rst dynamics/dynamics-monte.rst + dynamics/dynamics-krylov.rst dynamics/dynamics-photocurrent.rst dynamics/dynamics-stochastic.rst dynamics/dynamics-time.rst diff --git a/doc/guide/guide-measurement.rst b/doc/guide/guide-measurement.rst index 1517badfff..2d74d1fab7 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.sigmaz` which measures the z-component of the -spin of a spin-1/2 particle, or :func:`~qutip.sigmax` which measures the +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 x-component: .. testcode:: @@ -136,7 +136,7 @@ We can also choose what to measure by specifying a *list of projection operators example, we could select the projection operators :math:`\ket{0} \bra{0}` and :math:`\ket{1} \bra{1}` which measure the state in the :math:`\ket{0}, \ket{1}` basis. Note that these projection operators are simply the projectors determined by -the eigenstates of the :func:`~qutip.sigmaz` operator. +the eigenstates of the :func:`~qutip.operators.sigmaz` operator. .. testcode:: diff --git a/doc/guide/guide-parfor.rst b/doc/guide/guide-parfor.rst index abdeeba867..4491cf30af 100644 --- a/doc/guide/guide-parfor.rst +++ b/doc/guide/guide-parfor.rst @@ -7,30 +7,14 @@ 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.parallel.parallel_map` function or the :func:`qutip.parallel.parfor` (parallel-for-loop) function. +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 these functions 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: +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 - - >>> def func1(x): return x, x**2, x**3 - - >>> a, b, c = parfor(func1, range(10)) - - >>> print(a) - [0 1 2 3 4 5 6 7 8 9] - - >>> print(b) - [ 0 1 4 9 16 25 36 49 64 81] - - >>> print(c) - [ 0 1 8 27 64 125 216 343 512 729] - -or - .. doctest:: :skipif: not os_nt :options: +NORMALIZE_WHITESPACE @@ -48,25 +32,11 @@ or >>> print(result_array[:, 2]) # == c [ 0 1 8 27 64 125 216 343 512 729] - -Note that the return values are arranged differently for the :func:`qutip.parallel.parallel_map` and the :func:`qutip.parallel.parfor` functions, as illustrated below. In particular, the return value of :func:`qutip.parallel.parallel_map` is not enforced to be NumPy arrays, which can avoid unnecessary copying if all that is needed is to iterate over the resulting list: - - -.. doctest:: - :skipif: not os_nt - :options: +NORMALIZE_WHITESPACE - - >>> result = parfor(func1, range(5)) - - >>> print(result) - [array([0, 1, 2, 3, 4]), array([ 0, 1, 4, 9, 16]), array([ 0, 1, 8, 27, 64])] - - >>> result = parallel_map(func1, range(5)) - >>> print(result) [(0, 0, 0), (1, 1, 1), (2, 4, 8), (3, 9, 27), (4, 16, 64)] -The :func:`qutip.parallel.parallel_map` and :func:`qutip.parallel.parfor` functions are not limited to just numbers, but also works for a variety of outputs: + +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 @@ -74,12 +44,12 @@ The :func:`qutip.parallel.parallel_map` and :func:`qutip.parallel.parfor` functi >>> def func2(x): return x, Qobj(x), 'a' * x - >>> a, b, c = parfor(func2, range(5)) + >>> results = parallel_map(func2, range(5)) - >>> print(a) + >>> print([result[0] for result in results]) [0 1 2 3 4] - >>> print(b) + >>> print([result[1] for result in results]) [Quantum object: dims = [[1], [1]], shape = (1, 1), type = bra Qobj data = [[0.]] @@ -96,12 +66,11 @@ The :func:`qutip.parallel.parallel_map` and :func:`qutip.parallel.parfor` functi Qobj data = [[4.]]] - >>>print(c) + >>>print([result[2] for result in results]) ['' 'a' 'aa' 'aaa' 'aaaa'] -One can also define functions with **multiple** input arguments and even keyword arguments. Here the :func:`qutip.parallel.parallel_map` and :func:`qutip.parallel.parfor` functions behaves differently: -While :func:`qutip.parallel.parallel_map` only iterate over the values `arguments`, the :func:`qutip.parallel.parfor` function simultaneously iterates over all arguments: +One can also define functions with **multiple** input arguments and keyword arguments. .. doctest:: :skipif: not os_nt @@ -109,17 +78,14 @@ While :func:`qutip.parallel.parallel_map` only iterate over the values `argument >>> def sum_diff(x, y, z=0): return x + y, x - y, z - >>> parfor(sum_diff, [1, 2, 3], [4, 5, 6], z=5.0) - [array([5, 7, 9]), array([-3, -3, -3]), array([5., 5., 5.])] - >>> 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)] -Note that the keyword arguments can be anything you like, but the keyword values are **not** iterated over. The keyword argument *num_cpus* is reserved as it sets the number of CPU's used by parfor. By default, this value is set to the total number of physical processors on your system. You can change this number to a lower value, however setting it higher than the number of CPU's will cause a drop in performance. In :func:`qutip.parallel.parallel_map`, keyword arguments to the task function are specified using `task_kwargs` argument, so there is no special reserved keyword arguments. -The :func:`qutip.parallel.parallel_map` function also supports progressbar, using the keyword argument `progress_bar` which can be set to `True` or to an instance of :class:`qutip.ui.progressbar.BaseProgressBar`. There is a function called :func:`qutip.parallel.serial_map` that works as a non-parallel drop-in replacement for :func:`qutip.parallel.parallel_map`, which allows easy switching between serial and parallel computation. +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 @@ -128,7 +94,7 @@ The :func:`qutip.parallel.parallel_map` function also supports progressbar, usin >>> def func(x): time.sleep(1) - >>> result = parallel_map(func, range(50), progress_bar=True) + >>> 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 @@ -142,6 +108,8 @@ The :func:`qutip.parallel.parallel_map` function also supports progressbar, usin 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. diff --git a/doc/guide/guide-random.rst b/doc/guide/guide-random.rst index 2437647115..82b5920ea8 100644 --- a/doc/guide/guide-random.rst +++ b/doc/guide/guide-random.rst @@ -9,8 +9,7 @@ Generating Random Quantum States & Operators from qutip import rand_herm, rand_dm, rand_super_bcsz, rand_dm_ginibre 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 including the default distributions -used by QuTiP versions prior to 3.2.0. +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: @@ -44,33 +43,39 @@ For example, a random Hermitian operator can be sampled by calling `rand_herm` f +-------------------------------+--------------------------------------------+------------------------------------------+ | Random Variable Type | Sampling Functions | Dimensions | +===============================+============================================+==========================================+ -| State vector (``ket``) | `rand_ket`, `rand_ket_haar` | :math:`N \times 1` | +| State vector (``ket``) | `rand_ket`, | :math:`N \times 1` | +-------------------------------+--------------------------------------------+------------------------------------------+ -| Hermitian operator (``oper``) | `rand_herm` | :math:`N \times 1` | +| Hermitian operator (``oper``) | `rand_herm` | :math:`N \times N` | +-------------------------------+--------------------------------------------+------------------------------------------+ -| Density operator (``oper``) | `rand_dm`, `rand_dm_hs`, `rand_dm_ginibre` | :math:`N \times N` | +| Density operator (``oper``) | `rand_dm`, | :math:`N \times N` | +-------------------------------+--------------------------------------------+------------------------------------------+ -| Unitary operator (``oper``) | `rand_unitary`, `rand_unitary_haar` | :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) | ++-------------------------------+--------------------------------------------+------------------------------------------+ + +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. + -In all cases, these functions can be called with a single parameter :math:`N` that specifies the dimension of the relevant Hilbert space. The optional -``dims`` keyword argument allows for the dimensions of a random state, unitary or channel to be broken down into subsystems. .. doctest:: [random] >>> rand_super_bcsz(7).dims [[[7], [7]], [[7], [7]]] - >>> rand_super_bcsz(6, dims=[[[2, 3], [2, 3]], [[2, 3], [2, 3]]]).dims + >>> rand_super_bcsz([[2, 3], [2, 3]]).dims [[[2, 3], [2, 3]], [[2, 3], [2, 3]]] -Several of the distributions supported by QuTiP support additional parameters as well, namely *density* and *rank*. In particular, -the `rand_herm` and `rand_dm` functions 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, the `rand_dm_ginibre` and -`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. +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. +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. +`rand_dm` can support *density* or *rank* depending on the chosen distribution. For example, @@ -81,40 +86,23 @@ For example, .. doctest:: [random] - >>> rand_dm(5, density=0.5) + >>> rand_dm(5, density=0.5, distribution="herm") Quantum object: dims = [[5], [5]], shape = (5, 5), type = oper, isherm = True Qobj data = - [[ 0.05157906+0.j 0.04491736+0.01043329j 0.06966148+0.00344713j - 0. +0.j 0.04031493-0.01886791j] - [ 0.04491736-0.01043329j 0.33632352+0.j -0.08046093+0.02954712j - 0.0037455 +0.03940256j -0.05679126-0.01322392j] - [ 0.06966148-0.00344713j -0.08046093-0.02954712j 0.2938209 +0.j - 0.0029377 +0.04463531j 0.05318743-0.02817689j] - [ 0. +0.j 0.0037455 -0.03940256j 0.0029377 -0.04463531j - 0.22553181+0.j 0.01657495+0.06963845j] - [ 0.04031493+0.01886791j -0.05679126+0.01322392j 0.05318743+0.02817689j - 0.01657495-0.06963845j 0.09274471+0.j ]] + [[ 0.298+0.j , 0. +0.j , -0.095+0.1j , 0. +0.j ,-0.105+0.122j], + [ 0. +0.j , 0.088+0.j , 0. +0.j , -0.018-0.001j, 0. +0.j ], + [-0.095-0.1j , 0. +0.j , 0.328+0.j , 0. +0.j ,-0.077-0.033j], + [ 0. +0.j , -0.018+0.001j, 0. +0.j , 0.084+0.j , 0. +0.j ], + [-0.105-0.122j, 0. +0.j , -0.077+0.033j, 0. +0.j , 0.201+0.j ]] >>> rand_dm_ginibre(5, rank=2) Quantum object: dims = [[5], [5]], shape = (5, 5), type = oper, isherm = True Qobj data = - [[ 0.07318288+2.60675616e-19j 0.10426866-6.63115850e-03j - -0.05377455-2.66949369e-02j -0.01623153+7.66824687e-02j - -0.12255602+6.11342416e-02j] - [ 0.10426866+6.63115850e-03j 0.30603789+1.44335373e-18j - -0.03129486-4.16194216e-03j -0.09832531+1.74110000e-01j - -0.27176358-4.84608761e-02j] - [-0.05377455+2.66949369e-02j -0.03129486+4.16194216e-03j - 0.07055265-8.76912454e-19j -0.0183289 -2.72720794e-02j - 0.01196277-1.01037189e-01j] - [-0.01623153-7.66824687e-02j -0.09832531-1.74110000e-01j - -0.0183289 +2.72720794e-02j 0.14168414-1.51340961e-19j - 0.07847628+2.07735199e-01j] - [-0.12255602-6.11342416e-02j -0.27176358+4.84608761e-02j - 0.01196277+1.01037189e-01j 0.07847628-2.07735199e-01j - 0.40854244-6.75775934e-19j]] - - + [[ 0.307+0.j , -0.258+0.039j, -0.039+0.184j, 0.041-0.054j, 0.016+0.045j], + [-0.258-0.039j, 0.239+0.j , 0.075-0.15j , -0.053+0.008j,-0.057-0.078j], + [-0.039-0.184j, 0.075+0.15j , 0.136+0.j , -0.05 -0.052j,-0.028-0.058j], + [ 0.041+0.054j, -0.053-0.008j, -0.05 +0.052j, 0.083+0.j , 0.101-0.056j], + [ 0.016-0.045j, -0.057+0.078j, -0.028+0.058j, 0.101+0.056j, 0.236+0.j ]] See the API documentation: :ref:`functions-rand` for details. @@ -127,7 +115,9 @@ 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. This is done by passing an array of eigenvalues as the first argument to either function. For example, +It is also possible to generate random Hamiltonian (``rand_herm``) and densitiy matrices (``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, .. doctest:: [random] :hide: @@ -138,26 +128,16 @@ It is also possible to generate random Hamiltonian (``rand_herm``) and densitiy >>> eigs = np.arange(5) - >>> H = rand_herm(eigs, density=0.5) + >>> H = rand_herm(5, density=0.5, eigenvalues=eigs, distribution="eigen") >>> H # doctest: +NORMALIZE_WHITESPACE Quantum object: dims = [[5], [5]], shape = (5, 5), type = oper, isherm = True Qobj data = - [[ 2.51387054-5.55111512e-17j 0.81161447+2.02283642e-01j - 0. +0.00000000e+00j 0.875 +3.35634092e-01j - 0.81161447+2.02283642e-01j] - [ 0.81161447-2.02283642e-01j 1.375 +0.00000000e+00j - 0. +0.00000000e+00j -0.76700198+5.53011066e-01j - 0.375 +0.00000000e+00j] - [ 0. +0.00000000e+00j 0. +0.00000000e+00j - 2. +0.00000000e+00j 0. +0.00000000e+00j - 0. +0.00000000e+00j] - [ 0.875 -3.35634092e-01j -0.76700198-5.53011066e-01j - 0. +0.00000000e+00j 2.73612946+0.00000000e+00j - -0.76700198-5.53011066e-01j] - [ 0.81161447-2.02283642e-01j 0.375 +0.00000000e+00j - 0. +0.00000000e+00j -0.76700198+5.53011066e-01j - 1.375 +0.00000000e+00j]] + [[ 0.5 +0.j , 0.228+0.27j, 0. +0.j , 0. +0.j ,-0.228-0.27j], + [ 0.228-0.27j, 1.75 +0.j , 0.456+0.54j, 0. +0.j , 1.25 +0.j ], + [ 0. +0.j , 0.456-0.54j, 3. +0.j , 0. +0.j , 0.456-0.54j], + [ 0. +0.j , 0. +0.j , 0. +0.j , 3. +0.j , 0. +0.j ], + [-0.228+0.27j, 1.25 +0.j , 0.456+0.54j, 0. +0.j , 1.75 +0.j ]] >>> H.eigenenergies() # doctest: +NORMALIZE_WHITESPACE @@ -165,14 +145,17 @@ It is also possible to generate random Hamiltonian (``rand_herm``) and densitiy 4.00000000e+00]) -In order to generate a random object with a given spectrum QuTiP applies a series of random complex Jacobi rotations. This technique requires many steps to build the desired quantum object, and is thus suitable only for objects with Hilbert dimensionality :math:`\lesssim 1000`. +In order to generate a random object with a given spectrum QuTiP applies a series of random complex Jacobi rotations. +This technique requires many steps to build the desired quantum object, and is thus suitable only for objects with Hilbert dimensionality :math:`\lesssim 1000`. 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. Specifying the tensor structure of a quantum object is done using the `dims` keyword argument in the same fashion as one would do for a :class:`qutip.Qobj` object: +In many cases, one is interested in generating random quantum objects that correspond to composite systems generated using the :func:`qutip.tensor.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]``: .. doctest:: [random] :hide: @@ -181,14 +164,43 @@ In many cases, one is interested in generating random quantum objects that corre .. doctest:: [random] - >>> rand_dm(4, 0.5, dims=[[2,2], [2,2]]) # doctest: +NORMALIZE_WHITESPACE + >>> rand_unitary([2, 2], density=0.5) # doctest: +NORMALIZE_WHITESPACE Quantum object: dims = [[2, 2], [2, 2]], shape = (4, 4), type = oper, isherm = True Qobj data = - [[ 0.13622928+0.j 0. +0.j 0.01180807-0.01739166j - 0. +0.j ] - [ 0. +0.j 0.14600238+0.j 0.10335328+0.21790786j - -0.00426027-0.02193627j] - [ 0.01180807+0.01739166j 0.10335328-0.21790786j 0.57566072+0.j - -0.0670631 +0.04124094j] - [ 0. +0.j -0.00426027+0.02193627j -0.0670631 -0.04124094j - 0.14210761+0.j ]] + [[ 0.887+0.061j, 0. +0.j , 0. +0.j , -0.191-0.416j], + [ 0. +0.j , 0.604+0.116j, -0.32 -0.721j, 0. +0.j ], + [ 0. +0.j , 0.768+0.178j, 0.227+0.572j, 0. +0.j ], + [ 0.412-0.2j , 0. +0.j , 0. +0.j , 0.724+0.516j]] + + +Controlling the random number generator +======================================= + +Qutip uses numpy random number generator to create random quantum objects. +To control the random number, a seed as an `int` or `numpy.random.SeedSequence` or a `numpy.random.Generator` can be passed to the `seed` keyword argument: + +.. doctest:: [random] + + >>> rng = np.random.default_rng(12345) + >>> rand_ket(2, seed=rng) # doctest: +NORMALIZE_WHITESPACE + Quantum object: dims=[[2], [1]], shape=(2, 1), type='ket' + Qobj data = + [[-0.697+0.618j], + [-0.326-0.163j]] + + +Internal matrix format +====================== + +The internal storage type of the generated random quantum objects can be set with the *dtype* keyword. + +.. doctest:: [random] + + >>> rand_ket(2, dtype="dense").data + Dense(shape=(2, 1), fortran=True) + + >>> rand_ket(2, dtype="CSR").data + CSR(shape=(2, 1), nnz=2) + +.. + TODO: add a link to a page explaining data-types. diff --git a/doc/guide/guide-settings.rst b/doc/guide/guide-settings.rst index e621f3687e..7314ff3484 100644 --- a/doc/guide/guide-settings.rst +++ b/doc/guide/guide-settings.rst @@ -9,60 +9,38 @@ Modifying Internal QuTiP Settings User Accessible Parameters ========================== -In this section we show how to modify a few of the internal parameters used by QuTiP. The settings that can be modified are given in the following table: +In this section we show how to modify a few of the internal parameters used by QuTiP. +The settings that can be modified are given in the following table: .. tabularcolumns:: | p{3cm} | p{5cm} | p{5cm} | .. cssclass:: table-striped -+-------------------------------+-------------------------------------------+-----------------------------+ -| Setting | Description | Options | -+===============================+===========================================+=============================+ -| `auto_herm` | Automatically calculate the hermicity of | True / False | -| | quantum objects. | | -+-------------------------------+-------------------------------------------+-----------------------------+ -| `auto_tidyup` | Automatically tidyup quantum objects. | True / False | -+-------------------------------+-------------------------------------------+-----------------------------+ -| `auto_tidyup_atol` | Tolerance used by tidyup | any `float` value > 0 | -+-------------------------------+-------------------------------------------+-----------------------------+ -| `atol` | General tolerance | any `float` value > 0 | -+-------------------------------+-------------------------------------------+-----------------------------+ -| `num_cpus` | Number of CPU's used for multiprocessing. | `int` between 1 and # cpu's | -+-------------------------------+-------------------------------------------+-----------------------------+ -| `debug` | Show debug printouts. | True / False | -+-------------------------------+-------------------------------------------+-----------------------------+ -| `openmp_thresh` | NNZ matrix must have for OPENMP. | Int | -+-------------------------------+-------------------------------------------+-----------------------------+ ++------------------------------+----------------------------------------------+------------------------------+ +| Setting | Description | Options | ++==============================+==============================================+==============================+ +| `auto_tidyup` | Automatically tidyup sparse quantum objects. | True / False | ++------------------------------+----------------------------------------------+------------------------------+ +| `auto_tidyup_atol` | Tolerance used by tidyup. (sparse only) | float {1e-14} | ++------------------------------+----------------------------------------------+------------------------------+ +| `atol` | General absolute tolerance. | float {1e-12} | ++------------------------------+----------------------------------------------+------------------------------+ +| `rtol` | General relative tolerance. | float {1e-12} | ++------------------------------+----------------------------------------------+------------------------------+ +| `function_coefficient_style` | Signature expected by function coefficients. | {"auto", "pythonic", "dict"} | ++------------------------------+----------------------------------------------+------------------------------+ .. _settings-usage: Example: Changing Settings ========================== -The two most important settings are ``auto_tidyup`` and ``auto_tidyup_atol`` as they control whether the small elements of a quantum object should be removed, and what number should be considered as the cut-off tolerance. Modifying these, or any other parameters, is quite simple:: +The two most important settings are ``auto_tidyup`` and ``auto_tidyup_atol`` as they control whether the small elements of a quantum object should be removed, and what number should be considered as the cut-off tolerance. +Modifying these, or any other parameters, is quite simple:: ->>> qutip.settings.auto_tidyup = False - -These settings will be used for the current QuTiP session only and will need to be modified again when restarting QuTiP. If running QuTiP from a script file, then place the `qutip.settings.xxxx` commands immediately after `from qutip import *` at the top of the script file. If you want to reset the parameters back to their default values then call the reset command:: - ->>> qutip.settings.reset() - -Persistent Settings -=================== - -When QuTiP is imported, it looks for a file named ``qutiprc`` in a folder called ``.qutip`` user's home directory. If this file is found, it will be loaded and overwrite the QuTiP default settings, which allows for persistent changes in the QuTiP settings to be made. A sample ``qutiprc`` file is show below. The syntax is a simple key-value format, where the keys and possible values are described in the table above:: - - [qutip] - auto_tidyup=True - auto_herm=True - auto_tidyup_atol=1e-12 - num_cpus=4 - debug=False - -Note that the ``openmp_thresh`` value is automatically generatd by QuTiP. It is also possible to set a specific compiler for QuTiP to use when generating runtime Cython code for time-dependent problems. For example, the following section in the ``qutiprc`` file will set the compiler to be ``clang-3.9``:: - - [compiler] - cc = clang-3.9 - cxx = clang-3.9 +>>> qutip.settings.core["auto_tidyup"] = False +The settings can also be changed for a code block:: +>>> with qutip.CoreOptions(atol=1e-5): +>>> assert qutip.qeye(2) * 1e-9 == qutip.qzero(2) diff --git a/doc/guide/guide-states.rst b/doc/guide/guide-states.rst index 69774b22d5..3cabf6e020 100644 --- a/doc/guide/guide-states.rst +++ b/doc/guide/guide-states.rst @@ -503,7 +503,7 @@ 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.metrics.tracedist`, fidelity :func:`qutip.metrics.fidelity`, Hilbert-Schmidt distance :func:`qutip.metrics.hilbert_dist`, Bures distance :func:`qutip.metrics.bures_dist`, Bures angle :func:`qutip.metrics.bures_angle`, and quantum Hellinger distance :func:`qutip.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:`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`. .. testcode:: [states] @@ -1002,7 +1002,7 @@ 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:`~Qobj`. +function on a ``type="super"`` :obj:`~qutip.Qobj`. .. testcode:: [states] @@ -1042,7 +1042,7 @@ function on a ``type="super"`` :obj:`~Qobj`. [0. 0. 0. 0.] [1. 0. 0. 1.]] -If a :obj:`~Qobj` instance is already in the Choi :attr:`~Qobj.superrep`, then calling :func:`~qutip.superop_reps.to_choi` +If a :obj:`~qutip.Qobj` instance is already in the Choi :attr:`~qutip.Qobj.superrep`, then calling :func:`~qutip.superop_reps.to_choi` does nothing: .. testcode:: [states] @@ -1220,7 +1220,7 @@ using the :func:`~qutip.superop_reps.to_kraus` function. [0. 0.70710678]]] As with the other representation conversion functions, :func:`~qutip.superop_reps.to_kraus` -checks the :attr:`~Qobj.superrep` attribute of its input, and chooses an appropriate +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` on ``J``. @@ -1414,7 +1414,7 @@ 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:`~Qobj.superrep`, such that :func:`~qutip.superop_reps.to_super`, +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` all convert from the :math:`\chi` representation appropriately. @@ -1425,7 +1425,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:`~Qobj.superrep` to automatically perform any needed conversions. +uses :attr:`~qutip.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 +1451,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:`~Qobj.iscp` +is positive [Wat13]_. QuTiP implements this check with the :attr:`~qutip.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 +1477,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:`~Qobj.ishp` attribute: +We can confirm this by checking the :attr:`~qutip.Qobj.ishp` attribute: .. testcode:: [states] @@ -1492,7 +1492,7 @@ We can confirm this by checking the :attr:`~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:`~Qobj.istp` attribute: +This can be confirmed by the :attr:`~qutip.Qobj.istp` attribute: .. testcode:: [states] diff --git a/doc/guide/guide-steady.rst b/doc/guide/guide-steady.rst index 0efd4e3b46..08e7846e84 100644 --- a/doc/guide/guide-steady.rst +++ b/doc/guide/guide-steady.rst @@ -32,30 +32,64 @@ In QuTiP, the steady-state solution for a system Hamiltonian or Liouvillian is g - Description * - Direct (default) - 'direct' - - Direct solution solving :math:`Ax=b` via sparse LU decomposition. + - Direct solution solving :math:`Ax=b`. * - Eigenvalue - 'eigen' - Iteratively find the zero eigenvalue of :math:`\mathcal{L}`. * - Inverse-Power - 'power' - Solve using the inverse-power method. - * - GMRES - - 'iterative-gmres' - - Solve using the GMRES method and optional preconditioner. - * - LGMRES - - 'iterative-lgmres' - - Solve using the LGMRES method and optional preconditioner. - * - BICGSTAB - - 'iterative-bicgstab' - - Solve using the BICGSTAB method and optional preconditioner. * - SVD - 'svd' - Steady-state solution via the **dense** SVD of the Liouvillian. -The function :func:`qutip.steadystate.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. When possible, we recommend passing the Hamiltonian and collapse operators to :func:`qutip.steadystate.steadystate`, and letting the function automatically build the Liouvillian (in Lindblad form) for the system. +The function :func:`qutip.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. -As of QuTiP 3.2, the ``direct`` and ``power`` methods can take advantage of the Intel Pardiso LU solver in the Intel Math 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`). +Both the ``"direct"`` and ``"power"`` method need to solve a linear equation +system. To do so, there are multiple solvers available: `` + +.. cssclass:: table-striped + +.. list-table:: + :widths: 10 15 20 + :header-rows: 1 + + * - Solver + - Original function + - Description + * - "solve" + - ``numpy.linalg.solve`` + - Dense solver from numpy. + * - "lstsq" + - ``numpy.linalg.lstsq`` + - Dense least-squares solver. + * - "spsolve" + - ``scipy.sparse.linalg.spsolve`` + - Sparse solver from scipy. + * - "gmres" + - ``scipy.sparse.linalg.gmres`` + - Generalized Minimal RESidual iterative solver. + * - "lgmres" + - ``scipy.sparse.linalg.lgmres`` + - LGMRES iterative solver. + * - "bicgstab" + - ``scipy.sparse.linalg.bicgstab`` + - BIConjugate Gradient STABilized iterative solver. + * - "mkl_spsolve" + - ``pardiso`` + - Intel Pardiso LU solver from MKL + + +QuTiP can take advantage of the Intel Pardiso LU solver in the Intel Math +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`). .. _steady-usage: @@ -63,26 +97,75 @@ As of QuTiP 3.2, the ``direct`` and ``power`` methods can take advantage of the Using the Steadystate Solver ============================= -Solving for the steady state solution to the Lindblad master equation for a general system with :func:`qutip.steadystate.steadystate` can be accomplished using:: +Solving for the steady state solution to the Lindblad master equation for a +general system with :func:`qutip.steadystate` can be accomplished +using:: >>> rho_ss = steadystate(H, c_ops) -where ``H`` is a quantum object representing the system Hamiltonian, and ``c_ops`` is a list of quantum objects for the system collapse operators. The output, labeled as ``rho_ss``, is the steady-state solution for the systems. If no other keywords are passed to the solver, the default 'direct' method is used, generating a solution that is exact to machine precision at the expense of a large memory requirement. The large amount of memory need for the direct LU decomposition method stems from the large bandwidth of the system Liouvillian and the correspondingly large fill-in (extra nonzero elements) generated in the LU factors. This fill-in can be reduced by using bandwidth minimization algorithms such as those discussed in :ref:`steady-args`. However, in most cases, the default fill-in reducing algorithm is nearly optimal. Additional parameters may be used by calling the steady-state solver as: +where ``H`` is a quantum object representing the system Hamiltonian, and +``c_ops`` is a list of quantum objects for the system collapse operators. The +output, labelled as ``rho_ss``, is the steady-state solution for the systems. +If no other keywords are passed to the solver, the default 'direct' method is +used with ``numpy.linalg.solve``, generating a solution that is exact to +machine precision at the expense of a large memory requirement. However +Liouvillians are often quite sparse and using a sparse solver may be preferred: + .. code-block:: python - rho_ss = steadystate(H, c_ops, method='power', use_rcm=True) + rho_ss = steadystate(H, c_ops, method="power", solver="spsolve") + +where ``method='power'`` indicates that we are using the inverse-power solution +method, and ``solver="spsolve"`` indicate to use the sparse solver. -where ``method='power'`` indicates that we are using the inverse-power solution method, and ``use_rcm=True`` turns on a bandwidth minimization routine. +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 +listed in :ref:`steady-args`. For example: -Although it is not obvious, the ``'direct'``, ``eigen``, and ``'power'`` methods all use an LU decomposition internally and thus suffer from a large memory overhead. In contrast, iterative methods such as the ``'iterative-gmres'``, ``'iterative-lgmres'``, and ``'iterative-bicgstab'`` methods do not factor the matrix and thus take less memory than these previous methods and allowing, in principle, for extremely large system sizes. The downside is that these methods can take much longer than the direct method as the condition number of the Liouvillian matrix is large, indicating that these iterative methods require a large number of iterations for convergence. To overcome this, one can use a preconditioner :math:`M` that solves for an approximate inverse for the (modified) Liouvillian, thus better conditioning the problem, leading to faster convergence. The use of a preconditioner can actually make these iterative methods faster than the other solution methods. The problem with precondioning is that it is only well defined for Hermitian matrices. Since the Liouvillian is non-Hermitian, the ability to find a good preconditioner is not guaranteed. And moreover, if a preconditioner is found, it is not guaranteed to have a good condition number. QuTiP can make use of an incomplete LU preconditioner when using the iterative ``'gmres'``, ``'lgmres'``, and ``'bicgstab'`` solvers by setting ``use_precond=True``. The preconditioner optionally makes use of a combination of symmetric and anti-symmetric matrix permutations that attempt to improve the preconditioning process. These features are discussed in the :ref:`steady-args` section. Even with these state-of-the-art permutations, the generation of a successful preconditoner for non-symmetric matrices is currently a trial-and-error process due to the lack of mathematical work done in this area. It is always recommended to begin with the direct solver with no additional arguments before selecting a different method. +.. code-block:: python -Finding the steady-state solution is not limited to the Lindblad form of the master equation. Any time-independent Liouvillian constructed from a Hamiltonian and collapse operators can be used as an input:: + rho_ss = steadystate(H, c_ops, solver="spsolve", use_rcm=True) + +where ``use_rcm=True`` turns on a bandwidth minimization routine. + +Although it is not obvious, the ``'direct'``, ``'eigen'``, and ``'power'`` +methods all use an LU decomposition internally and thus can have a large +memory overhead. In contrast, iterative solvers such as the ``'gmres'``, +``'lgmres'``, and ``'bicgstab'`` do not factor the matrix and thus take less +memory than the LU methods and allow, in principle, for extremely +large system sizes. The downside is that these methods can take much longer +than the direct method as the condition number of the Liouvillian matrix is +large, indicating that these iterative methods require a large number of +iterations for convergence. To overcome this, one can use a preconditioner +:math:`M` that solves for an approximate inverse for the (modified) +Liouvillian, thus better conditioning the problem, leading to faster +convergence. The use of a preconditioner can actually make these iterative +methods faster than the other solution methods. The problem with precondioning +is that it is only well defined for Hermitian matrices. Since the Liouvillian +is non-Hermitian, the ability to find a good preconditioner is not guaranteed. +And moreover, if a preconditioner is found, it is not guaranteed to have a good +condition number. QuTiP can make use of an incomplete LU preconditioner when +using the iterative ``'gmres'``, ``'lgmres'``, and ``'bicgstab'`` solvers by +setting ``use_precond=True``. The preconditioner optionally makes use of a +combination of symmetric and anti-symmetric matrix permutations that attempt to +improve the preconditioning process. These features are discussed in the +:ref:`steady-args` section. Even with these state-of-the-art permutations, +the generation of a successful preconditoner for non-symmetric matrices is +currently a trial-and-error process due to the lack of mathematical work done +in this area. It is always recommended to begin with the direct solver with no +additional arguments before selecting a different method. + +Finding the steady-state solution is not limited to the Lindblad form of the +master equation. Any time-independent Liouvillian constructed from a +Hamiltonian and collapse operators can be used as an input:: >>> rho_ss = steadystate(L) -where ``L`` is the Louvillian. All of the additional arguments can also be used in this case. +where ``L`` is the Louvillian. All of the additional arguments can also be +used in this case. .. _steady-args: @@ -99,52 +182,36 @@ The following additional solver arguments are available for the steady-state sol :header-rows: 1 * - Keyword - - Options (default listed first) + - Default - Description - * - method - - 'direct', 'eigen', 'power', 'iterative-gmres','iterative-lgmres', 'svd' - - Method used for solving for the steady-state density matrix. - * - sparse - - True, False - - Use sparse version of direct solver. * - weight - None - - Allows the user to define the weighting factor used in the ``'direct'``, ``'GMRES'``, and ``'LGMRES'`` solvers. - * - permc_spec - - 'COLAMD', 'NATURAL' - - Column ordering used in the sparse LU decomposition. - * - use_rcm - - False, True - - Use a Reverse Cuthill-Mckee reordering to minimize the bandwidth of the modified Liouvillian used in the LU decomposition. If ``use_rcm=True`` then the column ordering is set to ``'Natural'`` automatically unless explicitly set. + - Set the weighting factor used in the ``'direct'`` method. * - use_precond - - False, True - - Attempt to generate a preconditioner when using the ``'iterative-gmres'`` and ``'iterative-lgmres'`` methods. - * - M - - None, sparse_matrix, LinearOperator - - A user defined preconditioner, if any. + - False + - Generate a preconditioner when using the ``'gmres'`` and ``'lgmres'`` methods. + * - use_rcm + - False + - Use a Reverse Cuthill-Mckee reordering to minimize the bandwidth of the modified Liouvillian used in the LU decomposition. * - use_wbm - - False, True - - Use a Weighted Bipartite Matching algorithm to attempt to make the modified Liouvillian more diagonally dominate, and thus for favorable for preconditioning. Set to ``True`` automatically when using a iterative method, unless explicitly set. - * - tol - - 1e-9 - - Tolerance used in finding the solution for all methods expect ``'direct'`` and ``'svd'``. - * - maxiter - - 10000 - - Maximum number of iterations to perform for all methods expect ``'direct'`` and ``'svd'``. - * - fill_factor + - False + - Use a Weighted Bipartite Matching algorithm to attempt to make the modified Liouvillian more diagonally dominant, and thus for favorable for preconditioning. + * - power_tol + - 1e-12 + - Tolerance for the solution when using the 'power' method. + * - power_maxiter - 10 - - Upper-bound on the allowed fill-in for the approximate inverse preconditioner. This value may need to be set much higher than this in some cases. - * - drop_tol - - 1e-3 - - Sets the threshold for the relative magnitude of preconditioner elements that should be dropped. A lower number yields a more accurate approximate inverse at the expense of fill-in and increased runtime. - * - diag_pivot_thresh - - None - - Sets the threshold between :math:`[0,1]` for which diagonal elements are considered acceptable pivot points when using a preconditioner. - * - ILU_MILU - - 'smilu_2' - - Selects the incomplete LU decomposition method algorithm used. + - Maximum number of iterations of the power method. + * - power_eps + - 1e-15 + - Small weight used in the "power" method. + * - \*\*kwargs + - {} + - Options to pass through the linalg solvers. + See the corresponding documentation from scipy for a full list. + -Further information can be found in the :func:`qutip.steadystate.steadystate` docstrings. +Further information can be found in the :func:`qutip.steadystate` docstrings. .. _steady-example: diff --git a/doc/guide/guide-tensor.rst b/doc/guide/guide-tensor.rst index 0bb7f7f119..beb6c26b7a 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.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.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.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.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: @@ -375,7 +375,7 @@ To represent superoperators acting on :math:`\mathcal{L}(\mathcal{H}_1 \otimes \ :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.to_super`: +one might expect on the results of :func:`qutip.superop_reps.to_super`: .. doctest:: [tensor] @@ -394,7 +394,7 @@ 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.super_tensor` function performs the needed +The :func:`qutip.tensor.super_tensor` function performs the needed rearrangement, providing the most direct analog to :func:`qutip.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 @@ -405,8 +405,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.composite` function automatically switches between -:func:`qutip.tensor` and :func:`qutip.super_tensor` based on the ``type`` +The :func:`qutip.tensor.composite` function automatically switches between +:func:`qutip.tensor` and :func:`qutip.tensor.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. @@ -420,7 +420,7 @@ 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:`tensor_contract` function allows for +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. diff --git a/doc/guide/guide-visualization.rst b/doc/guide/guide-visualization.rst index 896f980ebf..0b99bf4f57 100644 --- a/doc/guide/guide-visualization.rst +++ b/doc/guide/guide-visualization.rst @@ -280,7 +280,7 @@ QuTiP offers a few functions for quickly visualizing matrix data in the form of histograms, :func:`qutip.visualization.matrix_histogram` and :func:`qutip.visualization.matrix_histogram_complex`, and as Hinton diagram of weighted squares, :func:`qutip.visualization.hinton`. These functions takes a -:class:`qutip.Qobj.Qobj` as first argument, and optional arguments to, for +:class:`qutip.Qobj` as first argument, and optional arguments to, for example, set the axis labels and figure title (see the function's documentation for details). diff --git a/doc/guide/heom/intro.rst b/doc/guide/heom/intro.rst index d58039830d..5ae195250c 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:`HEOMSolver`, that supports solving for both dynamics and steady-states. +:class:`~qutip.nonmarkov.heom.HEOMSolver`, that supports solving for both dynamics and steady-states. diff --git a/doc/guide/scripts/correlation_ex3.py b/doc/guide/scripts/correlation_ex3.py index dbd65932ef..5b3553470a 100644 --- a/doc/guide/scripts/correlation_ex3.py +++ b/doc/guide/scripts/correlation_ex3.py @@ -19,8 +19,8 @@ n = qutip.mesolve(H, rho0, taus, c_ops, [a.dag() * a]).expect[0] # calculate the correlation function G1 and normalize with n to obtain g1 -G1 = qutip.correlation_2op_2t(H, rho0, None, taus, c_ops, a.dag(), a) -g1 = G1 / np.sqrt(n[0] * n) +G1 = qutip.correlation_2op_1t(H, rho0, taus, c_ops, a.dag(), a) +g1 = np.array(G1) / np.sqrt(n[0] * np.array(n)) plt.plot(taus, np.real(g1), 'b', lw=2) plt.plot(taus, n, 'r', lw=2) diff --git a/doc/guide/scripts/correlation_ex4.py b/doc/guide/scripts/correlation_ex4.py index d43843bcbe..0e88c49f71 100644 --- a/doc/guide/scripts/correlation_ex4.py +++ b/doc/guide/scripts/correlation_ex4.py @@ -28,7 +28,7 @@ # calculate the correlation function G2 and normalize with n(0)n(t) to # obtain g2 G2 = qutip.correlation_3op_1t(H, rho0, taus, c_ops, a.dag(), a.dag()*a, a) - g2 = G2 / (n[0] * n) + g2 = np.array(G2) / (n[0] * np.array(n)) ax.plot(taus, np.real(g2), label=state['label'], lw=2) diff --git a/doc/guide/scripts/floquet_ex0.py b/doc/guide/scripts/floquet_ex0.py deleted file mode 100644 index 0b3165b4f2..0000000000 --- a/doc/guide/scripts/floquet_ex0.py +++ /dev/null @@ -1,31 +0,0 @@ -import numpy as np -from matplotlib import pyplot -import qutip - -delta = 0.2 * 2*np.pi -eps0 = 0.0 * 2*np.pi -omega = 1.0 * 2*np.pi -A_vec = np.linspace(0, 10, 100) * omega -T = 2*np.pi/omega -tlist = np.linspace(0.0, 10 * T, 101) -psi0 = qutip.basis(2, 0) - -q_energies = np.zeros((len(A_vec), 2)) - -H0 = delta/2.0 * qutip.sigmaz() - eps0/2.0 * qutip.sigmax() -args = {'w': omega} -for idx, A in enumerate(A_vec): - H1 = A/2.0 * qutip.sigmax() - H = [H0, [H1, lambda t, w: np.sin(w*t)]] - f_modes,f_energies = qutip.floquet_modes(H, T, args, True) - q_energies[idx,:] = f_energies - -# plot the results -pyplot.plot( - A_vec/omega, np.real(q_energies[:, 0]) / delta, 'b', - A_vec/omega, np.real(q_energies[:, 1]) / delta, 'r', -) -pyplot.xlabel(r'$A/\omega$') -pyplot.ylabel(r'Quasienergy / $\Delta$') -pyplot.title(r'Floquet quasienergies') -pyplot.show() diff --git a/doc/guide/scripts/floquet_ex1.py b/doc/guide/scripts/floquet_ex1.py index 7afea3efa2..3ba5646c17 100644 --- a/doc/guide/scripts/floquet_ex1.py +++ b/doc/guide/scripts/floquet_ex1.py @@ -14,18 +14,18 @@ H0 = - delta/2.0 * qutip.sigmax() - eps0/2.0 * qutip.sigmaz() H1 = A/2.0 * qutip.sigmaz() args = {'w': omega} -H = [H0, [H1, lambda t,args: np.sin(args['w'] * t)]] +H = [H0, [H1, lambda t, w: np.sin(w * t)]] -# find the floquet modes for the time-dependent hamiltonian -f_modes_0,f_energies = qutip.floquet_modes(H, T, args) +# Create the floquet system for the time-dependent hamiltonian +floquetbasis = qutip.FloquetBasis(H, T, args) # decompose the inital state in the floquet modes -f_coeff = qutip.floquet_state_decomposition(f_modes_0, f_energies, psi0) +f_coeff = floquetbasis.to_floquet_basis(psi0) -# calculate the wavefunctions using the from the floquet modes +# calculate the wavefunctions using the from the floquet modes coefficients p_ex = np.zeros(len(tlist)) for n, t in enumerate(tlist): - psi_t = qutip.floquet_wavefunction_t(f_modes_0, f_energies, f_coeff, t, H, T, args) + psi_t = floquetbasis.from_floquet_basis(f_coeff, t) p_ex[n] = qutip.expect(qutip.num(2), psi_t) # For reference: calculate the same thing with mesolve diff --git a/doc/guide/scripts/floquet_ex2.py b/doc/guide/scripts/floquet_ex2.py index fd652bc76b..823dec0b7d 100644 --- a/doc/guide/scripts/floquet_ex2.py +++ b/doc/guide/scripts/floquet_ex2.py @@ -13,20 +13,18 @@ H0 = - delta/2.0 * qutip.sigmax() - eps0/2.0 * qutip.sigmaz() H1 = A/2.0 * qutip.sigmax() args = {'w': omega} -H = [H0, [H1, lambda t, args: np.sin(args['w'] * t)]] +H = [H0, [H1, lambda t, w: np.sin(w * t)]] -# find the floquet modes for the time-dependent hamiltonian -f_modes_0,f_energies = qutip.floquet_modes(H, T, args) +# find the floquet modes for the time-dependent hamiltonian +floquetbasis = qutip.FloquetBasis(H, T, args, precompute=tlist) # decompose the inital state in the floquet modes -f_coeff = qutip.floquet_state_decomposition(f_modes_0, f_energies, psi0) +f_coeff = floquetbasis.to_floquet_basis(psi0) -# calculate the wavefunctions using the from the floquet modes -f_modes_table_t = qutip.floquet_modes_table(f_modes_0, f_energies, tlist, H, T, args) +# calculate the wavefunctions using the from the floquet modes coefficients p_ex = np.zeros(len(tlist)) for n, t in enumerate(tlist): - f_modes_t = qutip.floquet_modes_t_lookup(f_modes_table_t, t, T) - psi_t = qutip.floquet_wavefunction(f_modes_t, f_energies, f_coeff, t) + psi_t = floquetbasis.from_floquet_basis(f_coeff, t) p_ex[n] = qutip.expect(qutip.num(2), psi_t) # For reference: calculate the same thing with mesolve diff --git a/doc/guide/scripts/floquet_ex3.py b/doc/guide/scripts/floquet_ex3.py index 756b0d4fec..d6f2b75b63 100644 --- a/doc/guide/scripts/floquet_ex3.py +++ b/doc/guide/scripts/floquet_ex3.py @@ -7,35 +7,34 @@ A = 0.25 * 2*np.pi omega = 1.0 * 2*np.pi T = 2*np.pi / omega -tlist = np.linspace(0.0, 20 * T, 101) +tlist = np.linspace(0.0, 20 * T, 301) psi0 = qutip.basis(2,0) H0 = - delta/2.0 * qutip.sigmax() - eps0/2.0 * qutip.sigmaz() H1 = A/2.0 * qutip.sigmax() args = {'w': omega} -H = [H0, [H1, lambda t,args: np.sin(args['w'] * t)]] +H = [H0, [H1, lambda t, w: np.sin(w * t)]] # noise power spectrum gamma1 = 0.1 def noise_spectrum(omega): - return 0.5 * gamma1 * omega/(2*np.pi) - -# find the floquet modes for the time-dependent hamiltonian -f_modes_0, f_energies = qutip.floquet_modes(H, T, args) - -# precalculate mode table -f_modes_table_t = qutip.floquet_modes_table( - f_modes_0, f_energies, np.linspace(0, T, 500 + 1), H, T, args, -) + return (omega>0) * 0.5 * gamma1 * omega/(2*np.pi) # solve the floquet-markov master equation -output = qutip.fmmesolve(H, psi0, tlist, [qutip.sigmax()], [], [noise_spectrum], T, args) +output = qutip.fmmesolve( + H, psi0, tlist, [qutip.sigmax()], + spectra_cb=[noise_spectrum], T=T, + args=args, options={"store_floquet_states": True} +) # calculate expectation values in the computational basis p_ex = np.zeros(tlist.shape, dtype=np.complex128) for idx, t in enumerate(tlist): - f_modes_t = qutip.floquet_modes_t_lookup(f_modes_table_t, t, T) - p_ex[idx] = qutip.expect(qutip.num(2), output.states[idx].transform(f_modes_t, True)) + f_coeff_t = output.floquet_states[idx] + psi_t = output.floquet_basis.from_floquet_basis(f_coeff_t, t) + # Alternatively + psi_t = output.states[idx] + p_ex[idx] = qutip.expect(qutip.num(2), psi_t) # For reference: calculate the same thing with mesolve output = qutip.mesolve(H, psi0, tlist, diff --git a/doc/installation.rst b/doc/installation.rst index df3308a1fd..efce6af430 100644 --- a/doc/installation.rst +++ b/doc/installation.rst @@ -64,7 +64,7 @@ In addition, there are several optional packages that provide additional functio | ``pytest``, | 5.3+ | For running the test suite. | | ``pytest-rerunfailures`` | | | +--------------------------+--------------+-----------------------------------------------------+ -| LaTeX | TeXLive 2009+| Needed if using LaTeX in matplotlib figures, or for | +| LaTeX | TeXLive 2009+| Needed if using LaTeX in matplotlib figures, or for | | | | nice circuit drawings in IPython. | +--------------------------+--------------+-----------------------------------------------------+ @@ -128,6 +128,24 @@ You activate the new environment by running You can also install any more optional packages you want with ``conda install``, for example ``matplotlib``, ``ipython`` or ``jupyter``. + +Installation of the pre-release of version 5 +============================================ + +QuTiP version 5 has been in development for some time and brings many new features, heavily reworks the core functionalities of QuTiP. +It is available as a pre-release on PyPI. Anyone wanting to try the new features can install it with: + +.. code-block:: bash + + pip install --pre qutip + +We expect the pre-release to fully work. +If you find any bugs, confusing documentation or missing features, please tell create an issue on `github `_. + +This version breaks compatibility with QuTiP 4.7 in many small ways. +Please see the :doc:`changelog` for a list of changes, new features and deprecations. + + .. _install-from-source: Installing from Source @@ -192,7 +210,7 @@ To install OpenMP support, if available, run: This will attempt to load up OpenMP libraries during the compilation process, which depends on you having suitable C++ compiler and library support. If you are on Linux this is probably already done, but the compiler macOS ships with does not have OpenMP support. You will likely need to refer to external operating-system-specific guides for more detail here, as it may be very non-trivial to correctly configure. - + If you wish to contribute to the QuTiP project, then you will want to create your own fork of `the QuTiP git repository `_, clone this to a local folder, and install it into your Python environment using: .. code-block:: bash diff --git a/doc/requirements.txt b/doc/requirements.txt index e7b95cdbc0..979eb016b4 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -2,7 +2,7 @@ alabaster==0.7.12 appnope==0.1.2 Babel==2.9.1 backcall==0.2.0 -certifi==2020.12.5 +certifi==2022.12.7 chardet==4.0.0 cycler==0.10.0 Cython==0.29.23 @@ -10,21 +10,21 @@ decorator==5.0.7 docutils==0.16 idna==2.10 imagesize==1.2.0 -ipython==7.31.1 +ipython==8.10.0 ipython-genutils==0.2.0 jedi==0.18.0 Jinja2==2.11.3 kiwisolver==1.3.1 MarkupSafe==1.1.1 matplotlib==3.3.4 -numpy==1.21.0 +numpy==1.22.0 numpydoc==1.1.0 packaging==20.9 parso==0.8.2 pexpect==4.8.0 pickleshare==0.7.5 -Pillow==9.0.1 -prompt-toolkit==3.0.18 +Pillow==9.3.0 +prompt-toolkit==3.0.36 ptyprocess==0.7.0 Pygments==2.8.1 pyparsing==2.4.7 @@ -47,4 +47,4 @@ sphinxcontrib-serializinghtml==1.1.4 traitlets==5.0.5 urllib3==1.26.5 wcwidth==0.2.5 -wheel==0.37.0 +wheel==0.38.1 diff --git a/pyproject.toml b/pyproject.toml index 6bf5f7c15f..f854fa46f5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,7 +18,37 @@ manylinux-i686-image = "manylinux2014" build-frontend = "pip" [[tool.cibuildwheel.overrides]] -# NumPy support manylinux2010 on CPython 3.6-3.9 -select = "cp3?-*" +# 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" +name = "QuTiP" +package = "qutip" +[[tool.towncrier.type]] +directory = "deprecation" +name = "Deprecations" +showcontent = true +[[tool.towncrier.type]] +directory = "feature" +name = "Features" +showcontent = true +[[tool.towncrier.type]] +directory = "bugfix" +name = "Bug Fixes" +showcontent = true +[[tool.towncrier.type]] +directory = "removal" +name = "Removals" +showcontent = true +[[tool.towncrier.type]] +directory = "doc" +name = "Documentation" +showcontent = true +[[tool.towncrier.type]] +directory = "misc" +name = "Miscellaneous" +showcontent = true diff --git a/qutip/__init__.py b/qutip/__init__.py index 84ca22a3f0..fc7f65b409 100644 --- a/qutip/__init__.py +++ b/qutip/__init__.py @@ -2,6 +2,7 @@ import warnings import qutip.settings +from qutip.settings import settings import qutip.version from qutip.version import version as __version__ @@ -32,10 +33,9 @@ from .core import * from .solver import * -from .solve import * -# Import here to avoid circular imports -from .solver.countstat import * -from .solver.brmesolve import * +from .solve import nonmarkov +import qutip.solve.piqs as piqs +from .solve.stochastic import * # graphics from .bloch import * @@ -57,7 +57,6 @@ # utilities -from .parallel import * from .utilities import * from .fileio import * from .about import * diff --git a/qutip/about.py b/qutip/about.py index 7f7cf03731..0e5eb1662a 100644 --- a/qutip/about.py +++ b/qutip/about.py @@ -25,7 +25,7 @@ def about(): print( "Current admin team: Alexander Pitchford, " "Nathan Shammah, Shahnawaz Ahmed, Neill Lambert, Eric Giguère, " - "Boxi Li, Jake Lishman and Simon Cross." + "Boxi Li, Jake Lishman, Simon Cross and Asier Galicia." ) print( "Board members: Daniel Burgarth, Robert Johansson, Anton F. Kockum, " diff --git a/qutip/continuous_variables.py b/qutip/continuous_variables.py index 3a6ea32765..519f070ffa 100644 --- a/qutip/continuous_variables.py +++ b/qutip/continuous_variables.py @@ -222,7 +222,7 @@ def wigner_covariance_matrix(a1=None, a2=None, R=None, rho=None, g=np.sqrt(2)): def logarithmic_negativity(V, g=np.sqrt(2)): """ Calculates the logarithmic negativity given a symmetrized covariance - matrix, see :func:`qutip.continous_variables.covariance_matrix`. Note that + matrix, see :func:`qutip.continuous_variables.covariance_matrix`. Note that the two-mode field state that is described by `V` must be Gaussian for this function to applicable. diff --git a/qutip/control/dump.py b/qutip/control/dump.py index 055f794d99..7e707a65cd 100644 --- a/qutip/control/dump.py +++ b/qutip/control/dump.py @@ -220,7 +220,7 @@ class OptimDump(Dump): dump_summary : bool When True summary items are appended to the iter_summary - iter_summary : list of :class:`optimizer.OptimIterSummary` + iter_summary : list of :class:`qutip.control.optimizer.OptimIterSummary` Summary at each iteration dump_fid_err : bool diff --git a/qutip/control/dynamics.py b/qutip/control/dynamics.py index 28c2165dd4..70600392ed 100644 --- a/qutip/control/dynamics.py +++ b/qutip/control/dynamics.py @@ -315,7 +315,7 @@ class Dynamics(object): Tolerance used in checking if operator is unitary Default is 1e-10 - dump : :class:`dump.DynamicsDump` + dump : :class:`qutip.control.dump.DynamicsDump` Store of historical calculation data. Set to None (Default) for no storing of historical data Use dumping property to set level of data dumping diff --git a/qutip/control/optimconfig.py b/qutip/control/optimconfig.py index ffd8a5b65e..dd4d5ecbfd 100644 --- a/qutip/control/optimconfig.py +++ b/qutip/control/optimconfig.py @@ -10,11 +10,10 @@ """ import numpy as np -import qutip.control.io as qtrlio # QuTiP logging import qutip.logging_utils -logger = qutip.logging_utils.get_logger('qutip.optimcontrol.grape') - +logger = qutip.logging_utils.get_logger('qutip.control.optimconfig') +import qutip.control.io as qtrlio class OptimConfig(object): """ diff --git a/qutip/control/optimizer.py b/qutip/control/optimizer.py index c6c8f6dc89..2cd0094046 100644 --- a/qutip/control/optimizer.py +++ b/qutip/control/optimizer.py @@ -206,7 +206,7 @@ class Optimizer(object): set to None to reduce overhead of calculating stats. Note it is (usually) shared with the Dynamics instance - dump : :class:`dump.OptimDump` + dump : :class:`qutip.control.dump.OptimDump` Container for data dumped during the optimisation. Can be set by specifying the dumping level or set directly. Note this is mainly intended for user and a development debugging diff --git a/qutip/control/pulseoptim.py b/qutip/control/pulseoptim.py index a8d608c9f1..b96eec4cfa 100644 --- a/qutip/control/pulseoptim.py +++ b/qutip/control/pulseoptim.py @@ -203,10 +203,10 @@ def optimize_pulse( method_params : dict Parameters for the ``optim_method``. Note that where there is an - attribute of the :obj:`~Optimizer` object or the termination_conditions - matching the key that attribute. Otherwise, and in some case also, they - are assumed to be method_options for the ``scipy.optimize.minimize`` - method. + attribute of the :obj:`~qutip.control.optimizer.Optimizer` object or + the termination_conditions matching the key that attribute. + Otherwise, and in some case also, they are assumed to be method_options + for the ``scipy.optimize.minimize`` method. optim_alg : string Deprecated. Use ``optim_method``. @@ -219,35 +219,37 @@ def optimize_pulse( dyn_type : string Dynamics type, i.e. the type of matrix used to describe the dynamics. - Options are ``UNIT``, ``GEN_MAT``, ``SYMPL`` (see :obj:`~Dynamics` - classes for details). + Options are ``UNIT``, ``GEN_MAT``, ``SYMPL`` + (see :obj:`~qutip.control.dynamics.Dynamics` classes for details). dyn_params : dict - Parameters for the :obj:`~Dynamics` object. The key value pairs are - assumed to be attribute name value pairs. They applied after the - object is created. + Parameters for the :obj:`~qutip.control.dynamics.Dynamics` object. + The key value pairs are assumed to be attribute name value pairs. + They applied after the object is created. prop_type : string Propagator type i.e. the method used to calculate the propagators and propagator gradient for each timeslot options are DEF, APPROX, DIAG, FRECHET, AUG_MAT. DEF will use the default for the specific - ``dyn_type`` (see :obj:`~PropagatorComputer` classes for details). + ``dyn_type`` (see :obj:`~qutip.control.propcomp.PropagatorComputer` + classes for details). prop_params : dict - Parameters for the :obj:`~PropagatorComputer` object. The key value - pairs are assumed to be attribute name value pairs. They applied after - the object is created. + Parameters for the :obj:`~qutip.control.propcomp.PropagatorComputer` + object. The key value pairs are assumed to be attribute name value + pairs. They applied after the object is created. fid_type : string Fidelity error (and fidelity error gradient) computation method. Options are DEF, UNIT, TRACEDIFF, TD_APPROX. DEF will use the default - for the specific ``dyn_type`` (See :obj:`~FidelityComputer` classes for + for the specific ``dyn_type`` + (See :obj:`~qutip.control.fidcomp.FidelityComputer` classes for details). fid_params : dict - Parameters for the :obj:`~FidelityComputer` object. The key value - pairs are assumed to be attribute name value pairs. They applied after - the object is created. + Parameters for the :obj:`~qutip.control.fidcomp.FidelityComputer` + object. The key value pairs are assumed to be attribute name value + pairs. They applied after the object is created. phase_option : string Deprecated. Pass in ``fid_params`` instead. @@ -258,13 +260,14 @@ def optimize_pulse( tslot_type : string Method for computing the dynamics generators, propagators and evolution in the timeslots. Options: DEF, UPDATE_ALL, DYNAMIC. UPDATE_ALL is - the only one that currently works. (See :obj:`~TimeslotComputer` - classes for details.) + the only one that currently works. + (See :obj:`~qutip.control.tslotcomp.TimeslotComputer` classes for + details.) tslot_params : dict - Parameters for the :obj:`~TimeslotComputer` object. The key value - pairs are assumed to be attribute name value pairs. They applied after - the object is created. + Parameters for the :obj:`~qutip.control.tslotcomp.TimeslotComputer` + object. The key value pairs are assumed to be attribute name value + pairs. They applied after the object is created. amp_update_mode : string Deprecated. Use ``tslot_type`` instead. @@ -272,8 +275,8 @@ def optimize_pulse( init_pulse_type : string Type / shape of pulse(s) used to initialise the control amplitudes. Options (GRAPE) include: RND, LIN, ZERO, SINE, SQUARE, TRIANGLE, SAW. - Default is RND. (see :obj:`~PulseGen` classes for details.) For the - CRAB the this the ``guess_pulse_type``. + Default is RND. (see :obj:`~qutip.control.pulsegen.PulseGen` classes + for details). For the CRAB the this the ``guess_pulse_type``. init_pulse_params : dict Parameters for the initial / guess pulse generator object. @@ -321,9 +324,9 @@ def optimize_pulse( Returns ------- opt : OptimResult - Returns instance of :obj:`~OptimResult`, which has attributes giving - the reason for termination, final fidelity error, final evolution final - amplitudes, statistics etc. + Returns instance of :obj:`~qutip.control.optimresult.OptimResult`, + which has attributes giving the reason for termination, final fidelity + error, final evolution final amplitudes, statistics etc. """ if log_level == logging.NOTSET: log_level = logger.getEffectiveLevel() @@ -581,10 +584,10 @@ def optimize_pulse_unitary( method_params : dict Parameters for the ``optim_method``. Note that where there is an - attribute of the :obj:`~Optimizer` object or the - ``termination_conditions`` matching the key that attribute. Otherwise, - and in some case also, they are assumed to be method_options for the - ``scipy.optimize.minimize`` method. + attribute of the :obj:`~qutip.control.optimizer.Optimizer` object or + the ``termination_conditions`` matching the key that attribute. + Otherwise, and in some case also, they are assumed to be + method_options for the ``scipy.optimize.minimize`` method. optim_alg : string Deprecated. Use ``optim_method``. @@ -603,30 +606,30 @@ def optimize_pulse_unitary( - SU - global phase included dyn_params : dict - Parameters for the :obj:`~Dynamics` object. The key value pairs are - assumed to be attribute name value pairs. They applied after the - object is created. + Parameters for the :obj:`~qutip.control.dynamics.Dynamics` object. + The key value pairs are assumed to be attribute name value pairs. + They applied after the object is created. prop_params : dict - Parameters for the :obj:`~PropagatorComputer` object. The key value - pairs are assumed to be attribute name value pairs. They applied after - the object is created. + Parameters for the :obj:`~qutip.control.propcomp.PropagatorComputer` + object. The key value pairs are assumed to be attribute name value + pairs. They applied after the object is created. fid_params : dict - Parameters for the :obj:`~FidelityComputer` object. The key value - pairs are assumed to be attribute name value pairs. They applied after - the object is created. + Parameters for the :obj:`~qutip.control.fidcomp.FidelityComputer` + object. The key value pairs are assumed to be attribute name value + pairs. They applied after the object is created. tslot_type : string Method for computing the dynamics generators, propagators and evolution in the timeslots. Options: ``DEF``, ``UPDATE_ALL``, ``DYNAMIC``. ``UPDATE_ALL`` is the only one that currently works. (See - :obj:`~TimeslotComputer` classes for details.) + :obj:`~qutip.control.tslotcomp.TimeslotComputer` classes for details.) tslot_params : dict - Parameters for the :obj:`~TimeslotComputer` object. The key value - pairs are assumed to be attribute name value pairs. They applied after - the object is created. + Parameters for the :obj:`~qutip.control.tslotcomp.TimeslotComputer` + object. The key value pairs are assumed to be attribute name value + pairs. They applied after the object is created. amp_update_mode : string Deprecated. Use ``tslot_type`` instead. @@ -634,8 +637,8 @@ def optimize_pulse_unitary( init_pulse_type : string Type / shape of pulse(s) used to initialise the control amplitudes. Options (GRAPE) include: RND, LIN, ZERO, SINE, SQUARE, TRIANGLE, SAW. - DEF is RND. (see :obj:`~PulseGen` classes for details.) For the CRAB - the this the guess_pulse_type. + DEF is RND. (see :obj:`~qutip.control.pulsegen.PulseGen` classes for + details.) For the CRAB the this the guess_pulse_type. init_pulse_params : dict Parameters for the initial / guess pulse generator object. The key @@ -683,9 +686,9 @@ def optimize_pulse_unitary( Returns ------- opt : OptimResult - Returns instance of :obj:`~OptimResult`, which has attributes giving - the reason for termination, final fidelity error, final evolution final - amplitudes, statistics etc. + Returns instance of :obj:`~qutip.control.optimresult.OptimResult`, + which has attributes giving the reason for termination, final fidelity + error, final evolution final amplitudes, statistics etc. """ # parameters are checked in create pulse optimiser @@ -865,10 +868,10 @@ def opt_pulse_crab( method_params : dict Parameters for the optim_method. Note that where there is an attribute - of the :obj:`~Optimizer` object or the termination_conditions matching - the key that attribute. Otherwise, and in some case also, they are - assumed to be method_options for the ``scipy.optimize.minimize`` - method. The commonly used parameter are: + of the :class:`~qutip.control.optimizer.Optimizer` object or the + termination_conditions matching the key that attribute. Otherwise, + and in some case also, they are assumed to be method_options for the + ``scipy.optimize.minimize`` method. The commonly used parameter are: - xtol - limit on variable change for convergence - ftol - limit on fidelity error change for convergence @@ -878,42 +881,45 @@ def opt_pulse_crab( Options are UNIT, GEN_MAT, SYMPL (see Dynamics classes for details). dyn_params : dict - Parameters for the Dynamics object. The key value pairs are assumed to - be attribute name value pairs. They applied after the object is - created. + Parameters for the :class:`qutip.control.dynamics.Dynamics` object. + The key value pairs are assumed to be attribute name value pairs. + They applied after the object is created. prop_type : string Propagator type i.e. the method used to calculate the propagtors and propagtor gradient for each timeslot options are DEF, APPROX, DIAG, FRECHET, AUG_MAT DEF will use the default for the specific dyn_type - (see :obj:`~PropagatorComputer` classes for details). + (see :obj:`~qutip.control.propcomp.PropagatorComputer` classes for + details). prop_params : dict - Parameters for the :obj:`~PropagatorComputer` object. The key value - pairs are assumed to be attribute name value pairs. They applied after - the object is created. + Parameters for the :obj:`~qutip.control.propcomp.PropagatorComputer` + object. The key value pairs are assumed to be attribute name value + pairs. They applied after the object is created. fid_type : string Fidelity error (and fidelity error gradient) computation method. Options are DEF, UNIT, TRACEDIFF, TD_APPROX. DEF will use the default - for the specific dyn_type. (See :obj:`~FidelityComputer` classes for + for the specific dyn_type. + (See :obj:`~qutip.control.fidcomp.FidelityComputer` classes for details). fid_params : dict - Parameters for the :obj:`~FidelityComputer` object. The key value - pairs are assumed to be attribute name value pairs. They applied after - the object is created. + Parameters for the :obj:`~qutip.control.fidcomp.FidelityComputer` + object. The key value pairs are assumed to be attribute name value + pairs. They applied after the object is created. tslot_type : string Method for computing the dynamics generators, propagators and evolution in the timeslots. Options: DEF, UPDATE_ALL, DYNAMIC UPDATE_ALL is the - only one that currently works. (See :obj:`~TimeslotComputer` classes + only one that currently works. + (See :obj:`~qutip.control.tslotcomp.TimeslotComputer` classes for details). tslot_params : dict - Parameters for the :obj:`~TimeslotComputer` object. The key value - pairs are assumed to be attribute name value pairs. They applied after - the object is created. + Parameters for the :obj:`~qutip.control.tslotcomp.TimeslotComputer` + object. The key value pairs are assumed to be attribute name value + pairs. They applied after the object is created. guess_pulse_type : string, default None Type / shape of pulse(s) used modulate the control amplitudes. @@ -1141,10 +1147,10 @@ def opt_pulse_crab_unitary( method_params : dict Parameters for the ``optim_method``. Note that where there is an - attribute of the :obj:`~Optimizer` object or the termination_conditions - matching the key that attribute. Otherwise, and in some case also, they - are assumed to be method_options for the ``scipy.optimize.minimize`` - method. The commonly used parameter are: + attribute of the :obj:`~qutip.control.optimizer.Optimizer` object or + the termination_conditions matching the key that attribute. Otherwise, + and in some case also, they are assumed to be method_options for the + ``scipy.optimize.minimize`` method. The commonly used parameter are: - xtol - limit on variable change for convergence - ftol - limit on fidelity error change for convergence @@ -1157,30 +1163,31 @@ def opt_pulse_crab_unitary( - SU - global phase included dyn_params : dict - Parameters for the :obj:`~Dynamics` object. The key value pairs are - assumed to be attribute name value pairs. They applied after the - object is created. + Parameters for the :obj:`~qutip.control.dynamics.Dynamics` object. + The key value pairs are assumed to be attribute name value pairs. + They applied after the object is created. prop_params : dict - Parameters for the :obj:`~PropagatorComputer` object. The key value - pairs are assumed to be attribute name value pairs. They applied after - the object is created. + Parameters for the :obj:`~qutip.control.propcomp.PropagatorComputer` + object. The key value pairs are assumed to be attribute name value + pairs. They applied after the object is created. fid_params : dict - Parameters for the :obj:`~FidelityComputer` object. The key value - pairs are assumed to be attribute name value pairs. They applied after - the object is created. + Parameters for the :obj:`~qutip.control.fidcomp.FidelityComputer` + object. The key value pairs are assumed to be attribute name value + pairs. They applied after the object is created. tslot_type : string Method for computing the dynamics generators, propagators and evolution in the timeslots. Options: DEF, UPDATE_ALL, DYNAMIC. UPDATE_ALL is - the only one that currently works. (See :obj:`~TimeslotComputer` - classes for details). + the only one that currently works. + (See :obj:`~qutip.control.tslotcomp.TimeslotComputer` classes for + details). tslot_params : dict - Parameters for the :obj:`~TimeslotComputer` object The key value pairs - are assumed to be attribute name value pairs. They applied after the - object is created. + Parameters for the :obj:`~qutip.control.tslotcomp.TimeslotComputer` + object. The key value pairs are assumed to be attribute name value + pairs. They applied after the object is created. guess_pulse_type : string, optional Type / shape of pulse(s) used modulate the control amplitudes. @@ -1236,9 +1243,9 @@ def opt_pulse_crab_unitary( Returns ------- opt : OptimResult - Returns instance of :obj:`~OptimResult`, which has attributes giving - the reason for termination, final fidelity error, final evolution final - amplitudes, statistics etc. + Returns instance of :obj:`~qutip.control.optimresult.OptimResult`, + which has attributes giving the reason for termination, final fidelity + error, final evolution final amplitudes, statistics etc. """ # The parameters are checked in create_pulse_optimizer @@ -1406,9 +1413,9 @@ def create_pulse_optimizer( method_params : dict Parameters for the optim_method. Note that where there is an attribute of the - Optimizer object or the termination_conditions matching the key - that attribute. Otherwise, and in some case also, - they are assumed to be method_options + :class:`~qutip.control.optimizer.Optimizer` object or the + termination_conditions matching the key that attribute. Otherwise, + and in some case also, they are assumed to be method_options for the scipy.optimize.minimize method. optim_alg : string @@ -1467,9 +1474,9 @@ def create_pulse_optimizer( (See TimeslotComputer classes for details) tslot_params : dict - Parameters for the TimeslotComputer object - The key value pairs are assumed to be attribute name value pairs - They applied after the object is created + Parameters for the TimeslotComputer object. + The key value pairs are assumed to be attribute name value pairs. + They applied after the object is created. amp_update_mode : string Deprecated. Use tslot_type instead. @@ -1486,9 +1493,9 @@ def create_pulse_optimizer( For the CRAB the this the guess_pulse_type. init_pulse_params : dict - Parameters for the initial / guess pulse generator object - The key value pairs are assumed to be attribute name value pairs - They applied after the object is created + Parameters for the initial / guess pulse generator object. + The key value pairs are assumed to be attribute name value pairs. + They applied after the object is created. pulse_scaling : float Linear scale factor for generated initial / guess pulses @@ -1507,8 +1514,8 @@ def create_pulse_optimizer( GAUSSIAN_EDGE was added for this purpose. ramping_pulse_params : dict - Parameters for the ramping pulse generator object - The key value pairs are assumed to be attribute name value pairs + Parameters for the ramping pulse generator object. + The key value pairs are assumed to be attribute name value pairs. They applied after the object is created log_level : integer @@ -1533,8 +1540,10 @@ def create_pulse_optimizer( Config, Dynamics, PulseGen, and TerminationConditions objects can be accessed as attributes. The PropagatorComputer, FidelityComputer and TimeslotComputer objects - can be accessed as attributes of the Dynamics object, e.g. optimizer.dynamics.fid_computer - The optimisation can be run through the optimizer.run_optimization + can be accessed as attributes of the Dynamics object, + e.g. optimizer.dynamics.fid_computer The optimisation can be run + through the optimizer.run_optimization + """ # check parameters diff --git a/qutip/core/__init__.py b/qutip/core/__init__.py index 6473367d24..b12830ba87 100644 --- a/qutip/core/__init__.py +++ b/qutip/core/__init__.py @@ -12,3 +12,5 @@ from .subsystem_apply import * from .blochredfield import * from . import gates + +del cy # File in cy are not public facing diff --git a/qutip/core/coefficient.py b/qutip/core/coefficient.py index db9c9fc75e..1c9b0e4e38 100644 --- a/qutip/core/coefficient.py +++ b/qutip/core/coefficient.py @@ -24,7 +24,7 @@ from .data import Data from .cy.coefficient import ( Coefficient, InterCoefficient, FunctionCoefficient, StrFunctionCoefficient, - ConjCoefficient, NormCoefficient, ShiftCoefficient + ConjCoefficient, NormCoefficient ) @@ -32,17 +32,6 @@ "clean_compiled_coefficient"] -@contextmanager -def _ignore_import_warning_for_pyximporter(): - """ - A helper for ignoring PyxImporter import warnings generated by Python 3.10+ - because PyxImporter has no .find_spec method. - """ - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=ImportWarning) - yield - - class StringParsingWarning(Warning): pass @@ -153,12 +142,6 @@ def conj(coeff): return ConjCoefficient(coeff) -def shift(coeff, _t0=0): - """ return a Coefficient in which t is shifted by _t0. - """ - return ShiftCoefficient(coeff, _t0) - - # %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% # %%%%%%%%% Everything under this is for string compilation %%%%%%%%% # %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% @@ -364,8 +347,7 @@ def try_import(file_name, parsed_in): name collision. """ try: - with _ignore_import_warning_for_pyximporter(): - mod = importlib.import_module(file_name) + mod = importlib.import_module(file_name) except ModuleNotFoundError: # Coefficient does not exist, to compile as file_name return None @@ -505,9 +487,8 @@ def compile_code(code, file_name, parsed, c_opt): include_dirs=[np.get_include()], language='c++' ) - with _ignore_import_warning_for_pyximporter(): - ext_modules = cythonize(coeff_file, force=True) - setup(ext_modules=ext_modules) + ext_modules = cythonize(coeff_file, force=True) + setup(ext_modules=ext_modules) except Exception as e: if c_opt['clean_on_error']: for file in glob.glob(file_name + "*"): diff --git a/qutip/core/cy/coefficient.pyx b/qutip/core/cy/coefficient.pyx index 72f031537e..819a22c1e5 100644 --- a/qutip/core/cy/coefficient.pyx +++ b/qutip/core/cy/coefficient.pyx @@ -155,10 +155,6 @@ cdef class Coefficient: """ Return a :obj:`Coefficient` being the norm of this""" return NormCoefficient(self) - def _shift(self): - """ Return a :obj:`Coefficient` with a time shift""" - return ShiftCoefficient(self, 0) - @cython.auto_pickle(True) cdef class FunctionCoefficient(Coefficient): @@ -721,55 +717,3 @@ cdef class NormCoefficient(Coefficient): cpdef Coefficient copy(self): """Return a copy of the :obj:`Coefficient`.""" return NormCoefficient(self.base.copy()) - - -@cython.auto_pickle(True) -cdef class ShiftCoefficient(Coefficient): - """ - Introduce a time shift into the :obj:`Coefficient`. - - Used internally within qutip when calculating correlations. - - :obj:ShiftCoefficient is returned by - ``qutip.coefficent.shift(Coefficient)``. - """ - cdef Coefficient base - cdef double _t0 - - def __init__(self, Coefficient base, double _t0): - self.base = base - self._t0 = _t0 - - 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 - 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 - replaced retain their previous values. - - Parameters - ---------- - _args : dict - Dictionary of arguments to replace. - - **kwargs - Arguments to replace. - """ - if _args: - kwargs.update(_args) - try: - _t0 = kwargs["_t0"] - del kwargs["_t0"] - except KeyError: - _t0 = self._t0 - return ShiftCoefficient(self.base.replace_arguments(**kwargs), _t0) - - cdef complex _call(self, double t) except *: - return self.base._call(t + self._t0) - - cpdef Coefficient copy(self): - """Return a copy of the :obj:`Coefficient`.""" - return ShiftCoefficient(self.base.copy(), self._t0) diff --git a/qutip/core/cy/qobjevo.pxd b/qutip/core/cy/qobjevo.pxd index d02f3bf18b..de310b6910 100644 --- a/qutip/core/cy/qobjevo.pxd +++ b/qutip/core/cy/qobjevo.pxd @@ -12,7 +12,6 @@ cdef class QobjEvo: readonly str superrep int _issuper int _isoper - double _shift_dt cpdef Data _call(QobjEvo self, double t) diff --git a/qutip/core/cy/qobjevo.pyx b/qutip/core/cy/qobjevo.pyx index c73cefe562..70fd6df1ae 100644 --- a/qutip/core/cy/qobjevo.pyx +++ b/qutip/core/cy/qobjevo.pyx @@ -187,7 +187,6 @@ cdef class QobjEvo: self.dims = Q_object.dims.copy() self.shape = Q_object.shape self.type = Q_object.type - self._shift_dt = ( Q_object)._shift_dt self._issuper = ( Q_object)._issuper self._isoper = ( Q_object)._isoper self.elements = ( Q_object).elements.copy() @@ -202,7 +201,6 @@ cdef class QobjEvo: self.shape = (0, 0) self._issuper = -1 self._isoper = -1 - self._shift_dt = 0 args = args or {} if ( @@ -312,8 +310,30 @@ cdef class QobjEvo: if _args is not None: kwargs.update(_args) return QobjEvo(self, args=kwargs)(t) + + t = self._prepare(t, None) + + if self.isconstant: + # For constant QobjEvo's, we sum the contained Qobjs directly in + # order to retain the cached values of attributes like .isherm when + # possible, rather than calling _call(t) which may lose this cached + # information. + return sum(element.qobj(t) for element in self.elements) + + cdef _BaseElement part = self.elements[0] + cdef double complex coeff = part.coeff(t) + obj = part.qobj(t) + cdef Data out = _data.mul(obj.data, coeff) + cdef bint isherm = obj._isherm and coeff.imag == 0 + for element in self.elements[1:]: + part = <_BaseElement> element + coeff = part.coeff(t) + obj = part.qobj(t) + isherm &= obj._isherm and coeff.imag == 0 + out = _data.add(out, obj.data, coeff) + return Qobj( - self._call(t), dims=self.dims, copy=False, + out, dims=self.dims, copy=False, isherm=isherm or None, type=self.type, superrep=self.superrep ) @@ -325,6 +345,7 @@ cdef class QobjEvo: part.coeff(t)) for element in self.elements[1:]: part = <_BaseElement> element + out = _data.add( out, part.data(t), @@ -334,7 +355,8 @@ cdef class QobjEvo: cdef double _prepare(QobjEvo self, double t, Data state=None): """ Precomputation before computing getting the element at `t`""" - return t + self._shift_dt + # We keep the function for feedback eventually + return t def copy(QobjEvo self): """Return a copy of this `QobjEvo`""" @@ -630,16 +652,6 @@ cdef class QobjEvo: return self.linear_map(partial(Qobj.to, data_type=data_type), _skip_check=True) - def _insert_time_shift(QobjEvo self, dt): - """ - Add a shift in the time ``t = t + _t0``. - To be used in correlation.py only. It does not propage safely with - binop between QobjEvo with different shift. - """ - cdef QobjEvo out = self.copy() - out._shift_dt = dt - return out - def tidyup(self, atol=1e-12): """Removes small elements from quantum object.""" for element in self.elements: diff --git a/qutip/core/data/__init__.py b/qutip/core/data/__init__.py index 12dfae950a..557aff5c04 100644 --- a/qutip/core/data/__init__.py +++ b/qutip/core/data/__init__.py @@ -24,6 +24,7 @@ from .reshape import * from .tidyup import * from .trace import * +from .solve import * # For operations with mulitple related versions, we just import the module. from . import norm, permute diff --git a/qutip/core/data/constant.py b/qutip/core/data/constant.py index 45803d77bb..a60341a364 100644 --- a/qutip/core/data/constant.py +++ b/qutip/core/data/constant.py @@ -30,8 +30,8 @@ (which is their representation of 0), and dense matrices will still be filled. - Arguments - --------- + Parameters + ---------- rows, cols : int The number of rows and columns in the output matrix. """ @@ -58,8 +58,8 @@ `scale` can be given, where all the diagonal elements will be that instead of 1. - Arguments - --------- + Parameters + ---------- dimension : int The dimension of the square output identity matrix. scale : complex, optional diff --git a/qutip/core/data/dispatch.pyx b/qutip/core/data/dispatch.pyx index e9984ba993..e8abebd7bc 100644 --- a/qutip/core/data/dispatch.pyx +++ b/qutip/core/data/dispatch.pyx @@ -347,8 +347,8 @@ cdef class Dispatcher: """ Create a new data layer dispatching operator. - Arguments - --------- + Parameters + ---------- signature_source : callable or inspect.Signature An object from which the call signature of operation can be determined. You can pass any callable defined in Python space, and @@ -430,8 +430,8 @@ cdef class Dispatcher: recent version; you can use this to override currently known specialisations if desired. - Arguments - --------- + Parameters + ---------- specialisations : iterable of tuples An iterable where each element specifies a new specialisation for this operation. Each element of the iterable should be a tuple, diff --git a/qutip/core/data/eigen.py b/qutip/core/data/eigen.py index fde1717151..c860fc2003 100644 --- a/qutip/core/data/eigen.py +++ b/qutip/core/data/eigen.py @@ -1,6 +1,7 @@ import numpy as np import scipy.linalg import scipy.sparse as sp +import scipy.sparse.linalg from itertools import combinations from .dense import Dense, from_csr @@ -10,6 +11,7 @@ __all__ = [ 'eigs', 'eigs_csr', 'eigs_dense', + 'svd', 'svd_csr', 'svd_dense', ] @@ -59,8 +61,9 @@ def _eigs_dense(data, isherm, vecs, eigvals, num_large, num_small): N = data.shape[0] kwargs = {} if eigvals != 0 and isherm: - kwargs['eigvals'] = ([0, num_small-1] if num_small - else [N-num_large, N-1]) + kwargs['subset_by_index'] = ( + [0, num_small-1] if num_small else [N-num_large, N-1] + ) if vecs: driver = eigh if isherm else scipy.linalg.eig evals, evecs = driver(data, **kwargs) @@ -276,6 +279,8 @@ def eigs_dense(data, isherm=None, vecs=True, sort='low', eigvals=0): from .dispatch import Dispatcher as _Dispatcher +import inspect as _inspect + # We use eigs_dense as the signature source, since in this case it has the # complete signature that we allow, so we don't need to manually set it. @@ -323,4 +328,127 @@ def eigs_dense(data, isherm=None, vecs=True, sort='low', eigvals=0): (Dense, eigs_dense), ], _defer=True) + +def svd_csr(data, vecs=True, k=6, **kw): + """ + Singular Value Decomposition: + + ``data = U @ S @ Vh`` + + Where ``S`` is diagonal. + + Parameters + ---------- + data : Data + Input matrix + vecs : bool, optional (True) + Whether the singular vectors (``U``, ``Vh``) should be returned. + k : int, optional (6) + Number of state to compute, default is ``6`` to match scipy's default. + **kw : dict + Options to pass to ``scipy.sparse.linalg.svds``. + + Returns + ------- + U : Dense + Left singular vectors as columns. Only returned if ``vecs == True``. + shape = (data.shape[0], k) + S : np.ndarray + The ``k``'s largest singular values. + Vh : Dense + Right singular vectors as rows. Only returned if ``vecs == True``. + shape = (k, data.shape[1]) + + .. note:: + svds cannot compute all states at once. While it could find the + largest and smallest in 2 calls, it has issues converging with when + solving for the smallest (finding the 5 smallest in a 50x50 matrix + can fail with default options). It should be used when not all states + are needed. + """ + out = scipy.sparse.linalg.svds( + data.as_scipy(), k, return_singular_vectors=vecs, **kw + ) + if vecs: + u, s, vh = out + return Dense(u, copy=False), s, Dense(vh, copy=False) + return out + + +def svd_dense(data, vecs=True, **kw): + """ + Singular Value Decomposition: + + ``data = U @ S @ Vh`` + + Where ``S`` is diagonal. + + Parameters + ---------- + data : Data + Input matrix + vecs : bool, optional (True) + Whether the singular vectors (``U``, ``Vh``) should be returned. + **kw : dict + Options to pass to ``scipy.linalg.svd``. + + Returns + ------- + U : Dense + Left singular vectors as columns. Only returned if ``vecs == True``. + S : np.ndarray + Singular values. + Vh : Dense + Right singular vectors as rows. Only returned if ``vecs == True``. + """ + out = scipy.linalg.svd( + data.to_array(), compute_uv=vecs, **kw + ) + if vecs: + u, s, vh = out + return Dense(u, copy=False), s, Dense(vh, copy=False) + return out + + +svd = _Dispatcher( + _inspect.Signature([ + _inspect.Parameter('data', _inspect.Parameter.POSITIONAL_OR_KEYWORD), + _inspect.Parameter('vecs', _inspect.Parameter.POSITIONAL_OR_KEYWORD), + ]), + name='svd', + module=__name__, + inputs=('data',), + out=False) +svd.__doc__ =\ + """ + Singular Value Decomposition: + + ``data = U @ S @ Vh`` + + Where ``S`` is diagonal. + + Parameters + ---------- + data : Data + Input matrix + vecs : bool, optional (True) + Whether the singular vectors (``U``, ``Vh``) should be returned. + + Returns + ------- + U : Dense + Left singular vectors as columns. Only returned if ``vecs == True``. + S : np.ndarray + Singular values. + Vh : Dense + Right singular vectors as rows. Only returned if ``vecs == True``. + """ +# Dense implementation return all states, but sparse implementation compute +# only a few states. So only the dense version is registered. +svd.add_specialisations([ + (Dense, svd_dense), +], _defer=True) + + del _Dispatcher +del _inspect diff --git a/qutip/core/data/inner.pyx b/qutip/core/data/inner.pyx index fb8a675084..9491970044 100644 --- a/qutip/core/data/inner.pyx +++ b/qutip/core/data/inner.pyx @@ -5,12 +5,14 @@ cdef extern from "" namespace "std" nogil: double complex conj(double complex x) from qutip.core.data.base cimport idxint, Data -from qutip.core.data cimport csr +from qutip.core.data cimport csr, dense from qutip.core.data.csr cimport CSR +from qutip.core.data.dense cimport Dense +from qutip.core.data.matmul cimport matmul_dense __all__ = [ - 'inner', 'inner_csr', - 'inner_op', 'inner_op_csr', + 'inner', 'inner_csr', 'inner_dense', + 'inner_op', 'inner_op_csr', 'inner_op_dense', ] @@ -88,6 +90,33 @@ cpdef double complex inner_csr(CSR left, CSR right, bint scalar_is_ket=False) no return _inner_csr_bra_ket(left, right) return _inner_csr_ket_ket(left, right) +cpdef double complex inner_dense(Dense left, Dense right, bint scalar_is_ket=False) nogil 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 + this function will be identical if passed `left` or `adjoint(left)`. + + The parameter `scalar_is_ket` is only intended for the case where `left` + and `right` are both of shape (1, 1). In this case, `left` will be assumed + to be a ket unless `scalar_is_ket` is False. This parameter is ignored at + all other times. + """ + _check_shape_inner(left, right) + if left.shape[0] == left.shape[1] == right.shape[1] == 1: + return ( + conj(left.data[0]) * right.data[0] if scalar_is_ket + else left.data[0] * right.data[0] + ) + cdef double complex out = 0 + cdef size_t i + if left.shape[0] == 1: + for i in range(right.shape[0]): + out += left.data[i] * right.data[i] + else: + for i in range(right.shape[0]): + out += conj(left.data[i]) * right.data[i] + return out + cdef double complex _inner_op_csr_bra_ket(CSR left, CSR op, CSR right) nogil: cdef size_t ptr_l, ptr_op, ptr_r, row, col @@ -143,6 +172,21 @@ cpdef double complex inner_op_csr(CSR left, CSR op, CSR right, return _inner_op_csr_bra_ket(left, op, right) return _inner_op_csr_ket_ket(left, op, right) +cpdef double complex inner_op_dense(Dense left, Dense op, Dense 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 + this function will be identical if passed `left` or `adjoint(left)`. + + The parameter `scalar_is_ket` is only intended for the case where `left` + and `right` are both of shape (1, 1). In this case, `left` will be assumed + to be a ket unless `scalar_is_ket` is False. This parameter is ignored at + all other times. + """ + _check_shape_inner_op(left, op, right) + return inner_dense(left, matmul_dense(op, right), scalar_is_ket) + from .dispatch import Dispatcher as _Dispatcher import inspect as _inspect @@ -172,8 +216,8 @@ inner.__doc__ =\ to be a ket unless `scalar_is_ket` is False. This parameter is ignored at all other times. - Arguments - --------- + Parameters + ---------- left : Data The left operand as either a bra or a ket matrix. @@ -187,6 +231,7 @@ inner.__doc__ =\ """ inner.add_specialisations([ (CSR, CSR, inner_csr), + (Dense, Dense, inner_dense), ], _defer=True) inner_op = _Dispatcher( @@ -216,8 +261,8 @@ inner_op.__doc__ =\ to be a ket unless `scalar_is_ket` is False. This parameter is ignored at all other times. - Arguments - --------- + Parameters + ---------- left : Data The left operand as either a bra or a ket matrix. @@ -235,6 +280,7 @@ inner_op.__doc__ =\ """ inner_op.add_specialisations([ (CSR, CSR, CSR, inner_op_csr), + (Dense, Dense, Dense, inner_op_dense), ], _defer=True) del _inspect, _Dispatcher diff --git a/qutip/core/data/linalg.py b/qutip/core/data/linalg.py index b9599c4021..f55077381c 100644 --- a/qutip/core/data/linalg.py +++ b/qutip/core/data/linalg.py @@ -32,10 +32,10 @@ def inv_csr(data): from .dispatch import Dispatcher as _Dispatcher -inv = _Dispatcher(inv_dense, name='inv', inputs=('data',), out=False) +inv = _Dispatcher(inv_dense, name='inv', inputs=('data',), out=True) inv.__doc__ =\ """ - Return matric inverse for a data-layer object. + Return matrix inverse for a data-layer object. Parameters ---------- @@ -48,8 +48,8 @@ def inv_csr(data): Inverse of data """ inv.add_specialisations([ - (CSR, inv_csr), - (Dense, inv_dense), + (CSR, CSR, inv_csr), + (Dense, Dense, inv_dense), ], _defer=True) del _Dispatcher diff --git a/qutip/core/data/matmul.pyx b/qutip/core/data/matmul.pyx index 39756d561d..0accc05286 100644 --- a/qutip/core/data/matmul.pyx +++ b/qutip/core/data/matmul.pyx @@ -255,6 +255,23 @@ cpdef Dense matmul_dense(Dense left, Dense right, double complex scale=1, Dense cdef double complex *b cdef char transa, transb cdef int m, n, k=left.shape[1], lda, ldb + if right.shape[1] == 1: + # Matrix Vector product + a, b = left.data, right.data + if left.fortran: + lda = left.shape[0] + transa = b'n' + m = left.shape[0] + n = left.shape[1] + else: + lda = left.shape[1] + transa = b't' + m = left.shape[1] + n = left.shape[0] + ldb = 1 + blas.zgemv(&transa, &m , &n, &scale, a, &lda, b, &ldb, + &out_scale, out.data, &ldb) + return out # We use the BLAS routine zgemm for every single call and pretend that # we're always supplying it with Fortran-ordered matrices, but to achieve # what we want, we use the property of matrix multiplication that @@ -397,8 +414,8 @@ matmul.__doc__ =\ where `scale` is (optionally) a scalar, and `left` and `right` are matrices. - Arguments - --------- + Parameters + ---------- left : Data The left operand as either a bra or a ket matrix. diff --git a/qutip/core/data/permute.pyx b/qutip/core/data/permute.pyx index bd47ff56f1..72f44af2af 100644 --- a/qutip/core/data/permute.pyx +++ b/qutip/core/data/permute.pyx @@ -14,8 +14,7 @@ import numpy as np cimport numpy as cnp from qutip.core.data.base cimport idxint, idxint_DTYPE -from qutip.core.data cimport csr -from qutip.core.data.csr cimport CSR +from qutip.core.data cimport csr, dense, CSR, Dense from qutip.core.data.base import idxint_dtype @@ -201,6 +200,16 @@ cpdef CSR indices_csr(CSR matrix, object row_perm=None, object col_perm=None): np.asarray(row_perm, dtype=idxint_dtype), np.asarray(col_perm, dtype=idxint_dtype)) +cpdef Dense indices_dense(Dense matrix, object row_perm=None, object col_perm=None): + if row_perm is None and col_perm is None: + return matrix.copy() + array = matrix.as_ndarray() + if row_perm is not None: + array = array[np.argsort(row_perm), :] + if col_perm is not None: + array = array[:, np.argsort(col_perm)] + return Dense(array) + cdef CSR _dimensions_csr_columns(CSR matrix, _Indexer index): if matrix.shape[0] != 1: @@ -293,6 +302,18 @@ cpdef CSR dimensions_csr(CSR matrix, object dimensions, object order): return _indices_csr_full(matrix, permutation, permutation) return _dimensions_csr_sparse(matrix, index) +@cython.cdivision(True) +cpdef Dense dimensions_dense(Dense matrix, object dimensions, object order): + cdef _Indexer index = _Indexer(np.asarray(dimensions, dtype=idxint_dtype), + np.asarray(order, dtype=idxint_dtype)) + cdef idxint[:] permutation = index.all() + row_perm, col_perm = None, None + if matrix.shape[0] != 1: + row_perm = permutation + if matrix.shape[1] != 1: + col_perm = permutation + return indices_dense(matrix, row_perm, col_perm) + from .dispatch import Dispatcher as _Dispatcher import inspect as _inspect @@ -323,8 +344,8 @@ dimensions.__doc__ =\ In other words, the inputs to `kron` are reordered so that input `n` moves to position `order[n]`. - Arguments - --------- + Parameters + ---------- matrix : Data Input matrix to reorder. This can either be a square matrix representing an operator, or a bra- or ket-like vector. @@ -341,6 +362,7 @@ dimensions.__doc__ =\ """ dimensions.add_specialisations([ (CSR, CSR, dimensions_csr), + (Dense, Dense, dimensions_dense), ], _defer=True) indices = _Dispatcher( @@ -363,8 +385,8 @@ indices.__doc__ =\ of quantum states; if you want to "reorder" the tensor-product structure of a system, you want `permute.dimensions` instead. - Arguments - --------- + Parameters + ---------- matrix : Data The input matrix. @@ -377,6 +399,7 @@ indices.__doc__ =\ """ indices.add_specialisations([ (CSR, CSR, indices_csr), + (Dense, Dense, indices_dense), ], _defer=True) del _inspect, _Dispatcher diff --git a/qutip/core/data/pow.pyx b/qutip/core/data/pow.pyx index fede460e85..b90a1a44b7 100644 --- a/qutip/core/data/pow.pyx +++ b/qutip/core/data/pow.pyx @@ -3,12 +3,14 @@ cimport cython -from qutip.core.data cimport csr +from qutip.core.data cimport csr, dense from qutip.core.data.csr cimport CSR +from qutip.core.data.dense cimport Dense from qutip.core.data.matmul cimport matmul_csr +import numpy as np __all__ = [ - 'pow', 'pow_csr', + 'pow', 'pow_csr', 'pow_dense', ] @@ -39,6 +41,16 @@ cpdef CSR pow_csr(CSR matrix, unsigned long long n): return out +cpdef Dense pow_dense(Dense matrix, unsigned long long n): + if matrix.shape[0] != matrix.shape[1]: + raise ValueError("matrix power only works with square matrices") + if n == 0: + return dense.identity(matrix.shape[0]) + if n == 1: + return matrix.copy() + return Dense(np.linalg.matrix_power(matrix.as_ndarray(), n)) + + from .dispatch import Dispatcher as _Dispatcher import inspect as _inspect @@ -58,8 +70,8 @@ pow.__doc__ =\ must be an integer >= 0. `A ** 0` is defined to be the identity matrix of the same shape. - Arguments - --------- + Parameters + ---------- matrix : Data Input matrix to take the power of. @@ -68,6 +80,7 @@ pow.__doc__ =\ """ pow.add_specialisations([ (CSR, CSR, pow_csr), + (Dense, Dense, pow_dense), ], _defer=True) del _inspect, _Dispatcher diff --git a/qutip/core/data/project.pyx b/qutip/core/data/project.pyx index fef163a7a3..b88771e365 100644 --- a/qutip/core/data/project.pyx +++ b/qutip/core/data/project.pyx @@ -131,8 +131,8 @@ project.__doc__ =\ Get the projector of a state with itself. Mathematically, if passed an object `|a>` or `= 0 else settings.core["atol"] + cdef size_t row, col, size=matrix.shape[0] + for row in range(size): + for col in range(row + 1): + if not _conj_feq( + matrix.data[col*size+row], + matrix.data[row*size+col], + tol + ): + return False + return True + + cpdef bint isdiag_csr(CSR matrix) nogil: cdef size_t row, ptr_start, ptr_end=matrix.row_index[0] for row in range(matrix.shape[0]): @@ -156,6 +189,16 @@ cpdef bint isdiag_csr(CSR matrix) nogil: return True +cpdef bint isdiag_dense(Dense matrix) nogil: + cdef size_t row, row_stride = 1 if matrix.fortran else matrix.shape[1] + cdef size_t col, col_stride = matrix.shape[0] if matrix.fortran else 1 + for row in range(matrix.shape[0]): + for col in range(matrix.shape[1]): + if (col != row) and matrix.data[col * col_stride + row * row_stride] != 0.: + return False + return True + + cpdef bint iszero_csr(CSR matrix, double tol=-1) nogil: cdef size_t ptr if tol < 0: @@ -201,8 +244,8 @@ isherm.__doc__ =\ Only square matrices can possibly be Hermitian. - Arguments - --------- + Parameters + ---------- matrix : Data The matrix to test for Hermicity. @@ -228,8 +271,8 @@ isdiag.__doc__ =\ """ Check if the matrix is diagonal. The matrix need not be square to test. - Arguments - --------- + Parameters + ---------- matrix : Data The matrix to test for diagonality. """ @@ -252,8 +295,8 @@ iszero.__doc__ =\ """ Test if this matrix is the zero matrix, up to a certain absolute tolerance. - Arguments - --------- + Parameters + ---------- matrix : Data The matrix to test. tol : real, optional diff --git a/qutip/core/data/ptrace.pyx b/qutip/core/data/ptrace.pyx index 1b95202f19..3e58e82d94 100644 --- a/qutip/core/data/ptrace.pyx +++ b/qutip/core/data/ptrace.pyx @@ -193,8 +193,8 @@ ptrace.__doc__ =\ ptrace(data, [2, 3, 4], [0, 2]) will be a matrix with effective dimensions `[[2, 4], [2, 4]]`. - Arguments - --------- + Parameters + ---------- matrix : Data The density matrix to be partially traced. diff --git a/qutip/core/data/reshape.pyx b/qutip/core/data/reshape.pyx index 06bac1b6d7..6871e12c9b 100644 --- a/qutip/core/data/reshape.pyx +++ b/qutip/core/data/reshape.pyx @@ -139,7 +139,7 @@ reshape = _Dispatcher( _inspect.Parameter('n_rows_out', _inspect.Parameter.POSITIONAL_OR_KEYWORD), _inspect.Parameter('n_cols_out', _inspect.Parameter.POSITIONAL_OR_KEYWORD), ]), - name='inspect', + name='reshape', module=__name__, inputs=('matrix',), out=True, @@ -149,8 +149,8 @@ reshape.__doc__ =\ Reshape the input matrix. The values of `n_rows_out` and `n_cols_out` must match the current total number of elements of the matrix. - Arguments - --------- + Parameters + ---------- matrix : Data The input matrix to reshape. @@ -196,8 +196,8 @@ column_stack.__doc__ =\ The inverse of this operation is `column_unstack`. - Arguments - --------- + Parameters + ---------- matrix : Data The matrix to stack the columns of. """ @@ -232,8 +232,8 @@ column_unstack.__doc__ =\ The inverse of this operation is `column_stack`. - Arguments - --------- + Parameters + ---------- matrix : Data The matrix to unstack the columns of. @@ -263,8 +263,8 @@ split_columns.__doc__ =\ Make a ket-shaped data out of each columns of the matrix. This is used for to split the eigenvectors from :obj:`eigs`. - Arguments - --------- + Parameters + ---------- matrix : Data The matrix to unstack the columns of. diff --git a/qutip/core/data/solve.py b/qutip/core/data/solve.py new file mode 100644 index 0000000000..a164d4f745 --- /dev/null +++ b/qutip/core/data/solve.py @@ -0,0 +1,208 @@ +from qutip.core.data import CSR, Data, csr, Dense +import qutip.core.data as _data +import scipy.sparse.linalg as splinalg +import numpy as np +from qutip.settings import settings +if settings.has_mkl: + from qutip._mkl.spsolve import mkl_spsolve +else: + mkl_spsolve = None + + +def _splu(A, B, **kwargs): + lu = splinalg.splu(A, **kwargs) + return lu.solve(B) + + +def solve_csr_dense(matrix: CSR, target: Dense, method=None, + options: dict={}) -> Dense: + """ + Solve ``Ax=b`` for ``x``. + + Parameters: + ----------- + + matrix : CSR + The matrix ``A``. + + target : Data + The matrix or vector ``b``. + + method : str {"spsolve", "splu", "mkl_spsolve", etc.}, default="spsolve" + The function to use to solve the system. Any function from + scipy.sparse.linalg which solve the equation Ax=b can be used. + `splu` from the same and `mkl_spsolve` are also valid choice. + + options : dict + Keywork options to pass to the solver. Refer to the documenentation in + scipy.sparse.linalg of the used method for a list of supported keyword. + The keyword "csc" can be set to ``True`` to convert the sparse matrix + before passing it to the solver. + + .. note:: + Options for ``mkl_spsolve`` are presently only found in the source + code. + + Returns: + -------- + x : Dense + Solution to the system Ax = b. + """ + if matrix.shape[0] != matrix.shape[1]: + raise ValueError("can only solve using square matrix") + if matrix.shape[1] != target.shape[0]: + raise ValueError("target does not match the system") + + b = target.as_ndarray() + + method = method or "spsolve" + + if method == "splu": + solver = _splu + elif hasattr(splinalg, method): + solver = getattr(splinalg, method) + elif method == "mkl_spsolve" and mkl_spsolve is None: + raise ValueError("mkl is not available") + elif method == "mkl_spsolve": + solver = mkl_spsolve + else: + raise ValueError(f"Unknown sparse solver {method}.") + + options = options.copy() + M = matrix.as_scipy() + if options.pop("csc", False): + M = M.tocsc() + + out = solver(M, b, **options) + + if isinstance(out, tuple) and len(out) == 2: + # iterative method return a success flag + out, check = out + if check == 0: + # Successful + pass + elif check > 0: + raise RuntimeError( + f"scipy.sparse.linalg.{method} error: Tolerance was not" + f" reached. Error code: {check}" + ) + + elif check < 0: + raise RuntimeError( + f"scipy.sparse.linalg.{method} error: Bad input. " + f"Error code: {check}" + ) + elif isinstance(out, tuple) and len(out) > 2: + # least sqare method return residual, flag, etc. + out, *_ = out + return Dense(out, copy=False) + + +def solve_dense(matrix: Dense, target: Data, method=None, + options: dict={}) -> Dense: + """ + Solve ``Ax=b`` for ``x``. + + Parameters: + ----------- + + matrix : Dense + The matrix ``A``. + + target : Data + The matrix or vector ``b``. + + method : str {"solve", "lstsq"}, default="solve" + The function from numpy.linalg to use to solve the system. + + options : dict + Options to pass to the solver. "lstsq" use "rcond" while, "solve" do + not use any. + + Returns: + -------- + x : Dense + Solution to the system Ax = b. + """ + if matrix.shape[0] != matrix.shape[1]: + raise ValueError("can only solve using square matrix") + if matrix.shape[1] != target.shape[0]: + raise ValueError("target does not match the system") + + if isinstance(target, Dense): + b = target.as_ndarray() + else: + b = target.to_array() + + if method in ["solve", None]: + out = np.linalg.solve(matrix.as_ndarray(), b) + elif method == "lstsq": + out, *_ = np.linalg.lstsq( + matrix.as_ndarray(), + b, + rcond=options.get("rcond", None) + ) + else: + raise ValueError(f"Unknown solver {method}," + " 'solve' and 'lstsq' are supported.") + return Dense(out, copy=False) + + +from .dispatch import Dispatcher as _Dispatcher +import inspect as _inspect + + +solve = _Dispatcher( + _inspect.Signature([ + _inspect.Parameter('matrix', _inspect.Parameter.POSITIONAL_OR_KEYWORD), + _inspect.Parameter('target', _inspect.Parameter.POSITIONAL_OR_KEYWORD), + _inspect.Parameter('method', _inspect.Parameter.POSITIONAL_OR_KEYWORD, + default=None), + _inspect.Parameter('options', _inspect.Parameter.POSITIONAL_OR_KEYWORD, + default={}), + ]), + name='solve', + module=__name__, + inputs=('matrix', 'target'), + out=True, +) +solve.__doc__ = """ + Solve ``Ax=b`` for ``x``. + + Parameters: + ----------- + matrix : Data + The matrix ``A``. + + target : Data + The matrix or vector ``b``. + + method : str + The function to use to solve the system. Function which solve the + equation Ax=b from scipy.sparse.linalg (CSR ``matrix``) or + numpy.linalg (Dense ``matrix``) can be used. + Sparse cases also accept `splu` and `mkl_spsolve`. + + options : dict + Keywork options to pass to the solver. Refer to the documenentation + for the chosen method in scipy.sparse.linalg or numpy.linalg. + The keyword "csc" can be set to ``True`` to convert the sparse matrix + in sparse cases. + + .. note:: + Options for ``mkl_spsolve`` are presently only found in the source + code. + + Returns: + -------- + x : Data + Solution to the system Ax = b. +""" +solve.add_specialisations([ + (CSR, Dense, Dense, solve_csr_dense), + (Dense, Dense, Dense, solve_dense), +], _defer=True) + + +del _Dispatcher +del _inspect diff --git a/qutip/core/data/tidyup.pyx b/qutip/core/data/tidyup.pyx index 68478364e9..a9b80798f1 100644 --- a/qutip/core/data/tidyup.pyx +++ b/qutip/core/data/tidyup.pyx @@ -84,8 +84,8 @@ tidyup.__doc__ =\ By default, this operation is in-place. The output type will always match the input type; no dispatching takes place on the output. - Arguments - --------- + Parameters + ---------- matrix : Data The matrix to tidy up. diff --git a/qutip/core/metrics.py b/qutip/core/metrics.py index aa3e4b1dce..08c635326c 100644 --- a/qutip/core/metrics.py +++ b/qutip/core/metrics.py @@ -52,12 +52,13 @@ def fidelity(A, B): >>> np.testing.assert_almost_equal(fidelity(x,y), 0.24104350624628332) """ if A.isket or A.isbra: + if B.isket or B.isbra: + # The fidelity for pure states reduces to the modulus of their + # inner product. + return np.abs(A.overlap(B)) # Take advantage of the fact that the density operator for A # is a projector to avoid a sqrtm call. - sqrtmA = A.proj() - # Check whether we have to turn B into a density operator, too. - if B.isket or B.isbra: - B = B.proj() + sqrtmA = ket2dm(A) else: if B.isket or B.isbra: # Swap the order so that we can take a more numerically @@ -175,7 +176,7 @@ def process_fidelity(oper, target=None): 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 - :func:`qutip.metrics.fidelity` which follows Nielsen & Chuang, + :func:`qutip.core.metrics.fidelity` which follows Nielsen & Chuang, "Quantum Computation and Quantum Information" """ @@ -235,7 +236,7 @@ def average_gate_fidelity(oper, target=None): The definition of state fidelity that the average gate fidelity is based on is the one from R. Jozsa, Journal of Modern Optics, 41:12, 2315 (1994). It is the square of the fidelity implemented in - :func:`qutip.metrics.fidelity` which follows Nielsen & Chuang, + :func:`qutip.core.metrics.fidelity` which follows Nielsen & Chuang, "Quantum Computation and Quantum Information" """ @@ -430,7 +431,7 @@ def dnorm(A, B=None, solver="CVXOPT", verbose=False, force_solve=False, sparse=True): """ Calculates the diamond norm of the quantum map q_oper, using - the simplified semidefinite program of [Wat12]_. + the simplified semidefinite program of [Wat13]_. The diamond norm SDP is solved by using CVXPY_. diff --git a/qutip/core/operators.py b/qutip/core/operators.py index 140ff82d59..6a3a990bc0 100644 --- a/qutip/core/operators.py +++ b/qutip/core/operators.py @@ -595,7 +595,9 @@ def position(N, offset=0, *, dtype=_data.CSR): Position operator as Qobj. """ a = destroy(N, offset=offset, dtype=dtype) - return np.sqrt(0.5) * (a + a.dag()) + position = np.sqrt(0.5) * (a + a.dag()) + position.isherm = True + return position def momentum(N, offset=0, *, dtype=_data.CSR): @@ -621,7 +623,9 @@ def momentum(N, offset=0, *, dtype=_data.CSR): Momentum operator as Qobj. """ a = destroy(N, offset=offset, dtype=dtype) - return -1j * np.sqrt(0.5) * (a - a.dag()) + momentum = -1j * np.sqrt(0.5) * (a - a.dag()) + momentum.isherm = True + return momentum def num(N, offset=0, *, dtype=_data.CSR): @@ -681,7 +685,7 @@ def squeeze(N, z, offset=0, *, dtype=_data.CSR): Returns ------- - oper : :class:`qutip.qobj.Qobj` + oper : :class:`qutip.Qobj` Squeezing operator. @@ -712,10 +716,10 @@ def squeezing(a1, a2, z): Parameters ---------- - a1 : :class:`qutip.qobj.Qobj` + a1 : :class:`qutip.Qobj` Operator 1. - a2 : :class:`qutip.qobj.Qobj` + a2 : :class:`qutip.Qobj` Operator 2. z : float/complex @@ -723,7 +727,7 @@ def squeezing(a1, a2, z): Returns ------- - oper : :class:`qutip.qobj.Qobj` + oper : :class:`qutip.Qobj` Squeezing operator. """ @@ -756,7 +760,7 @@ def displace(N, alpha, offset=0, *, dtype=_data.Dense): Displacement operator. Examples - --------- + -------- >>> displace(4,0.25) # doctest: +SKIP Quantum object: dims = [[4], [4]], \ shape = [4, 4], type = oper, isHerm = False diff --git a/qutip/core/options.py b/qutip/core/options.py index 19adef49a3..114dd95459 100644 --- a/qutip/core/options.py +++ b/qutip/core/options.py @@ -60,16 +60,13 @@ class CoreOptions(QutipOptions): Whether to tidyup during sparse operations. auto_tidyup_dims : bool [True] - use auto tidyup dims on multiplication - - auto_herm : boolTrue - detect hermiticity + Use auto tidyup dims on multiplication. (Not used yet) atol : float {1e-12} - general absolute tolerance + General absolute tolerance rtol : float {1e-12} - general relative tolerance + General relative tolerance Used to choose QobjEvo.expect output type auto_tidyup_atol : float {1e-14} @@ -98,8 +95,6 @@ class CoreOptions(QutipOptions): "auto_tidyup": True, # use auto tidyup dims on multiplication "auto_tidyup_dims": True, - # 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 21e2ea1cca..83a778b052 100644 --- a/qutip/core/qobj.py +++ b/qutip/core/qobj.py @@ -512,9 +512,7 @@ def __matmul__(self, other): copy=False) def __truediv__(self, other): - if not isinstance(other, numbers.Number): - return NotImplemented - return self.__mul__(1 / complex(other)) + return self.__mul__(1 / other) def __neg__(self): return Qobj(_data.neg(self._data), @@ -1410,7 +1408,7 @@ def matrix_element(self, bra, ket): `bra` and `ket` vector. Parameters - ----------- + ---------- bra : :class:`qutip.Qobj` Quantum object of type 'bra' or 'ket' @@ -1447,7 +1445,7 @@ def overlap(self, other): when one of the Qobj is an operator/density matrix. Parameters - ----------- + ---------- other : :class:`qutip.Qobj` Quantum object for a state vector of type 'ket', 'bra' or density matrix. diff --git a/qutip/core/states.py b/qutip/core/states.py index bd820e0d88..85b57631d6 100644 --- a/qutip/core/states.py +++ b/qutip/core/states.py @@ -900,8 +900,8 @@ def state_number_qobj(dims, state, *, dtype=_data.Dense): Returns ------- - state : :class:`qutip.Qobj.qobj` - The state as a :class:`qutip.Qobj.qobj` instance. + state : :class:`qutip.Qobj` + The state as a :class:`qutip.Qobj` instance. .. note:: Deprecated in QuTiP 5.0, use :func:`basis` instead. diff --git a/qutip/core/superoperator.py b/qutip/core/superoperator.py index 972d3db625..328862b8f3 100644 --- a/qutip/core/superoperator.py +++ b/qutip/core/superoperator.py @@ -330,7 +330,7 @@ def spre(A): Quantum operator for pre-multiplication. Returns - -------- + ------- super :Qobj or QobjEvo Superoperator formed from input quantum object. """ @@ -367,7 +367,7 @@ def sprepost(A, B): Quantum operator for post-multiplication. Returns - -------- + ------- super : Qobj or QobjEvo Superoperator formed from input quantum objects. """ diff --git a/qutip/fileio.py b/qutip/fileio.py index 9a87082249..ac94dd41f5 100644 --- a/qutip/fileio.py +++ b/qutip/fileio.py @@ -4,7 +4,6 @@ import numpy as np import sys from .core import Qobj -from .solve import Result from pathlib import Path @@ -223,10 +222,10 @@ def qsave(data, name='qutip_data'): """ # open the file for writing - file = Path(name) - file = file.with_suffix(file.suffix + ".qu") + path = Path(name) + path = path.with_suffix(path.suffix + ".qu") - with open(name, "wb") as fileObject: + with open(path, "wb") as fileObject: # this writes the object a to the file named 'filename.qu' pickle.dump(data, fileObject) @@ -246,10 +245,10 @@ def qload(name): Object retrieved from requested file. """ - file = Path(name) - file = file.with_suffix(file.suffix + ".qu") + path = Path(name) + path = path.with_suffix(path.suffix + ".qu") - with open(name, "rb") as fileObject: + with open(path, "rb") as fileObject: if sys.version_info >= (3, 0): out = pickle.load(fileObject, encoding='latin1') else: diff --git a/qutip/ipynbtools.py b/qutip/ipynbtools.py index 2840dac869..e2d906b637 100644 --- a/qutip/ipynbtools.py +++ b/qutip/ipynbtools.py @@ -10,14 +10,14 @@ if IPython.version_info[0] >= 4: try: from ipyparallel import Client - __all__ = ['version_table', 'parfor', 'plot_animation', + __all__ = ['version_table', 'plot_animation', 'parallel_map', 'HTMLProgressBar'] except: __all__ = ['version_table', 'plot_animation', 'HTMLProgressBar'] else: try: from IPython.parallel import Client - __all__ = ['version_table', 'parfor', 'plot_animation', + __all__ = ['version_table', 'plot_animation', 'parallel_map', 'HTMLProgressBar'] except: __all__ = ['version_table', 'plot_animation', 'HTMLProgressBar'] @@ -53,7 +53,7 @@ def version_table(verbose=False): Returns - -------- + ------- version_table: string Return an HTML-formatted string containing version information for QuTiP dependencies. @@ -219,7 +219,7 @@ def parfor(task, task_vec, args=None, client=None, view=None, loop. Returns - -------- + ------- result : list The result list contains the value of ``task(value, args)`` for each value in ``task_vec``, that is, it should be equivalent to @@ -284,7 +284,7 @@ def parallel_map(task, values, task_args=None, task_kwargs=None, loop. Returns - -------- + ------- result : list The result list contains the value of ``task(value, task_args, task_kwargs)`` for each diff --git a/qutip/orbital.py b/qutip/orbital.py index ada1ba70f4..209b65b406 100644 --- a/qutip/orbital.py +++ b/qutip/orbital.py @@ -1,7 +1,7 @@ __all__ = ['orbital'] import numpy as np -from scipy.special import factorial, sph_harm +from scipy.special import sph_harm def orbital(theta, phi, *args): diff --git a/qutip/settings.py b/qutip/settings.py index 7b8c12e747..7537942884 100644 --- a/qutip/settings.py +++ b/qutip/settings.py @@ -2,7 +2,8 @@ This module contains settings for the QuTiP graphics, multiprocessing, and tidyup functionality, etc. """ -import os, sys +import os +import sys from ctypes import cdll import platform import numpy as np @@ -18,15 +19,15 @@ def _blas_info(): blas_info = config.blas_opt_info else: blas_info = {} - _has_lib_key = 'libraries' in blas_info.keys() - blas = None - if hasattr(config,'mkl_info') or \ - (_has_lib_key and any('mkl' in lib for lib in blas_info['libraries'])): + + def _in_libaries(name): + return any(name in lib for lib in blas_info.get('libraries', [])) + + if getattr(config, 'mkl_info', False) or _in_libaries("mkl"): blas = 'INTEL MKL' - elif hasattr(config,'openblas_info') or \ - (_has_lib_key and any('openblas' in lib for lib in blas_info['libraries'])): + elif getattr(config, 'openblas_info', False) or _in_libaries('openblas'): blas = 'OPENBLAS' - elif 'extra_link_args' in blas_info.keys() and ('-Wl,Accelerate' in blas_info['extra_link_args']): + elif '-Wl,Accelerate' in blas_info.get('extra_link_args', []): blas = 'Accelerate' else: blas = 'Generic' @@ -81,7 +82,7 @@ def _find_mkl(): if _blas_info() == 'INTEL MKL': plat = sys.platform python_dir = os.path.dirname(sys.executable) - if plat in ['darwin','linux2', 'linux']: + if plat in ['darwin', 'linux2', 'linux']: python_dir = os.path.dirname(python_dir) if plat == 'darwin': @@ -93,27 +94,26 @@ def _find_mkl(): else: raise Exception('Unknown platfrom.') - if plat in ['darwin','linux2', 'linux']: + if plat in ['darwin', 'linux2', 'linux']: lib_dir = '/lib' else: lib_dir = r'\Library\bin' # Try in default Anaconda location first try: mkl_lib = cdll.LoadLibrary(python_dir+lib_dir+lib) - except: + except Exception: pass # Look in Intel Python distro location if mkl_lib is None: - if plat in ['darwin','linux2', 'linux']: + if plat in ['darwin', 'linux2', 'linux']: lib_dir = '/ext/lib' else: lib_dir = r'\ext\lib' try: mkl_lib = \ cdll.LoadLibrary(python_dir + lib_dir + lib) - - except: + except Exception: pass return mkl_lib @@ -122,21 +122,14 @@ class Settings: """ Qutip's settings and options. """ - def __new__(cls): - """Set Settings as a singleton.""" - if not hasattr(cls, '_instance'): - cls._instance = super(Settings, cls).__new__(cls) - return cls._instance - def __init__(self): self._mkl_lib = "" try: self.tmproot = os.path.join(os.path.expanduser("~"), '.qutip') except OSError: self._tmproot = "." - self.core = None - self._solvers = [] - self._integrators = [] + self.core = None # set in qutip.core.options + self.compile = None # set in qutip.core.coefficient self._debug = False self._log_handler = "default" self._colorblind_safe = False diff --git a/qutip/solve/__init__.py b/qutip/solve/__init__.py index cc42dc9cd3..e69de29bb2 100644 --- a/qutip/solve/__init__.py +++ b/qutip/solve/__init__.py @@ -1,24 +0,0 @@ -from .correlation import * -from .floquet import * -from .mcsolve import * -from .mesolve import * -from . import nonmarkov -from .pdpsolve import * -from .piqs import * -from .rcsolve import * -from .sesolve import * -from .solver import * -from .steadystate import * -from .stochastic import * -from .krylovsolve import * - -# TODO: most of these don't have a __all__ leaking names, ex: -del np -del Qobj -del debug - -# Temporary patch -# There is a collision between the file and the folder solver. -# We remove the file import here to allow qutip.solver to be the folder. -# The names from file are still available in qutip namespace. -del solver diff --git a/qutip/solve/_mcsolve.pyx b/qutip/solve/_mcsolve.pyx deleted file mode 100644 index b225524d9b..0000000000 --- a/qutip/solve/_mcsolve.pyx +++ /dev/null @@ -1,440 +0,0 @@ -#cython: language_level=3 - -import numpy as np -cimport numpy as np -from scipy.linalg.cython_blas cimport dznrm2 as raw_dznrm2 - -from libc cimport math - -cimport cython -from cpython.exc cimport PyErr_CheckSignals - -from qutip.core import Qobj -from qutip.core import data as _data - -from qutip.core.cy.qobjevo cimport QobjEvo -from qutip.core.data cimport Dense -from qutip.core.data.norm cimport l2_dense - -cdef extern from "" namespace "std" nogil: - double complex conj(double complex x) - double complex exp(double complex x) - - -cdef int _ONE = 1 - -cdef double dznrm2(complex[::1] psi): - cdef int l = psi.shape[0] - return raw_dznrm2(&l, &psi[0], &_ONE) - -@cython.cdivision(True) -@cython.boundscheck(False) -@cython.wraparound(False) -cdef np.ndarray[complex, ndim=1] normalize(complex[::1] psi): - cdef int i, l = psi.shape[0] - cdef double norm = dznrm2(psi) - cdef np.ndarray[ndim=1, dtype=complex] out = np.empty(l, dtype=complex) - for i in range(l): - out[i] = psi[i] / norm - return out - -cdef class CyMcOde: - cdef: - int steady_state, store_states, col_args, store_final_state - int norm_steps, l_vec, num_ops - double norm_t_tol, norm_tol - list collapses - list collapses_args - list c_ops - list n_ops - object states_out - object ss_out - double[::1] n_dp - - def __init__(self, ss, opt): - self.c_ops = ss.td_c_ops - self.n_ops = ss.td_n_ops - self.norm_steps = opt.mcsolve['norm_steps'] - self.norm_t_tol = opt.mcsolve['norm_t_tol'] - self.norm_tol = opt.mcsolve['norm_tol'] - self.steady_state = opt['steady_state_average'] - self.store_states = opt['store_states'] or opt['average_states'] - self.store_final_state = opt['store_final_state'] - self.collapses = [] - self.l_vec = self.c_ops[0].shape[0] - self.num_ops = len(ss.td_n_ops) - self.n_dp = np.zeros(self.num_ops) - - if ss.col_args: - self.col_args = 1 - self.collapses_args = ss.args[ss.col_args] - if ss.type == "QobjEvo": - ss.H_td.arguments(ss.args) - for c in ss.td_c_ops: - c.arguments(ss.args) - for c in ss.td_n_ops: - c.arguments(ss.args) - else: - self.col_args = 0 - - if self.steady_state: - self.ss_out = np.zeros((self.l_vec, self.l_vec), dtype=complex) - else: - self.ss_out = np.zeros((0, 0), dtype=complex) - - @cython.boundscheck(False) - @cython.wraparound(False) - cdef void sumsteadystate(self, complex[::1] state): - cdef int ii, jj, l_vec - cdef complex [:, ::1] _ss_out = self.ss_out - l_vec = state.shape[0] - for ii in range(l_vec): - for jj in range(l_vec): - _ss_out[ii,jj] += state[ii] * conj(state[jj]) - - - @cython.boundscheck(False) - @cython.wraparound(False) - def run_ode(self, ODE, tlist_, e_call, prng): - cdef np.ndarray[double, ndim=1] rand_vals - cdef np.ndarray[double, ndim=1] tlist = np.array(tlist_, dtype=np.double) - cdef np.ndarray[complex, ndim=1] y_prev - cdef np.ndarray[complex, ndim=1] out_psi = ODE._y - cdef int num_times = tlist.shape[0] - cdef int ii, which, k - cdef double norm2_prev, norm2_psi - cdef double t_prev - cdef complex [:, ::1] _states_out - - if self.steady_state: - self.sumsteadystate(out_psi) - - if self.store_states: - self.states_out = np.zeros((num_times, self.l_vec), dtype=complex) - for ii in range(self.l_vec): - self.states_out[0, ii] = out_psi[ii] - elif self.store_final_state: - self.states_out = np.zeros((1, self.l_vec), dtype=complex) - else: - self.states_out = None - _states_out = self.states_out - - e_call.step(0, out_psi) - rand_vals = prng.rand(2) - - # RUN ODE UNTIL EACH TIME IN TLIST - norm2_prev = dznrm2(ODE._y) ** 2 - for k in range(1, num_times): - PyErr_CheckSignals() - # ODE WHILE LOOP FOR INTEGRATE UP TO TIME TLIST[k] - t_prev = ODE.t - y_prev = ODE.y - while t_prev < tlist[k]: - # integrate up to tlist[k], one step at a time. - ODE.integrate(tlist[k], step=1) - if not ODE.successful(): - print(ODE.t, t_prev, tlist[k]) - print(ODE._integrator.call_args) - raise Exception("ZVODE failed!") - norm2_psi = dznrm2(ODE._y) ** 2 - if norm2_psi <= rand_vals[0]: - # collapse has occured: - self._find_collapse(ODE, norm2_psi, t_prev, y_prev, - norm2_prev, rand_vals[0]) - t_prev = ODE.t - y_prev = ODE.y - - which = self._which_collapse(t_prev, y_prev, rand_vals[1]) - y_prev = self._collapse(t_prev, which, y_prev) - ODE.set_initial_value(y_prev, t_prev) - - self.collapses.append((t_prev, which)) - if self.col_args: - self.collapses_args.append((t_prev, which)) - rand_vals = prng.rand(2) - norm2_prev = 1. # dznrm2(ODE._y)**2 - else: - norm2_prev = norm2_psi - t_prev = ODE.t - y_prev = ODE.y - - # after while loop - # ---------------- - out_psi = normalize(ODE._y) - e_call.step(k, out_psi) - if self.steady_state: - self.sumsteadystate(out_psi) - if self.store_states: - for ii in range(self.l_vec): - _states_out[k, ii] = out_psi[ii] - if not self.store_states and self.store_final_state: - for ii in range(self.l_vec): - _states_out[0, ii] = out_psi[ii] - return self.states_out, self.ss_out, self.collapses - - - @cython.cdivision(True) - @cython.boundscheck(False) - @cython.wraparound(False) - cdef void _find_collapse(self, ODE, double norm2_psi, - double t_prev, np.ndarray[complex, ndim=1] y_prev, - double norm2_prev, double target_norm): - # find collapse time to within specified tolerance - cdef int ii = 0 - cdef double t_final = ODE.t - cdef double t_guess, norm2_guess - - while ii < self.norm_steps: - ii += 1 - if (t_final - t_prev) < self.norm_t_tol: - t_prev = t_final - y_prev = ODE.y - break - - t_guess = ( - t_prev - + ((t_final - t_prev) - * math.log(norm2_prev/target_norm) - / math.log(norm2_prev/norm2_psi)) - ) - if (t_guess - t_prev) < self.norm_t_tol: - t_guess = t_prev + self.norm_t_tol - - ODE.t = t_prev - ODE._y = y_prev - ODE._integrator.call_args[3] = 1 - ODE.integrate(t_guess, step=0) - if not ODE.successful(): - raise Exception("ZVODE failed after adjusting step size!") - - norm2_guess = dznrm2(ODE._y)**2 - if (np.abs(target_norm - norm2_guess) < self.norm_tol * target_norm): - break - elif (norm2_guess < target_norm): - # t_guess is still > t_jump - t_final = t_guess - norm2_psi = norm2_guess - else: - # t_guess < t_jump - t_prev = t_guess - y_prev = ODE.y - norm2_prev = norm2_guess - - if ii > self.norm_steps: - raise Exception("Norm tolerance not reached. " + - "Increase accuracy of ODE solver or " + - "SolverOptions.mcsolve['norm_steps'].") - - @cython.cdivision(True) - @cython.boundscheck(False) - @cython.wraparound(False) - cdef int _which_collapse(self, double t, complex[::1] y, double rand): - # determine which operator does collapse - cdef int ii, j = self.num_ops - cdef double e, sum_ = 0 - cdef QobjEvo cobj - for ii in range(self.num_ops): - cobj = self.n_ops[ii] - e = cobj.expect_data(t, _data.dense.fast_from_numpy(y.base)).real - self.n_dp[ii] = e - sum_ += e - rand *= sum_ - for ii in range(self.num_ops): - if rand <= self.n_dp[ii]: - j = ii - break - else: - rand -= self.n_dp[ii] - return j - - @cython.cdivision(True) - @cython.boundscheck(False) - @cython.wraparound(False) - cdef np.ndarray[complex, ndim=1] _collapse(self, double t, int j, complex[::1] y): - cdef QobjEvo cobj - cdef Dense state - cobj = self.c_ops[j] - state = cobj.matmul_data(t, _data.dense.fast_from_numpy(y.base)) - state = _data.mul(state, 1/l2_dense(state)) - return state.as_ndarray()[:, 0] - - -cdef class CyMcOdeDiag(CyMcOde): - cdef: - complex[::1] diag - complex[::1] diag_dt - complex[::1] psi - complex[::1] psi_temp - double t - object prng - - def __init__(self, ss, opt): - self.c_ops = ss.td_c_ops - self.n_ops = ss.td_n_ops - self.diag = ss.H_diag - self.norm_steps = opt.mcsolve['norm_steps'] - self.norm_t_tol = opt.mcsolve['norm_t_tol'] - self.norm_tol = opt.mcsolve['norm_tol'] - self.steady_state = opt['steady_state_average'] - self.store_states = opt['store_states'] or opt['average_states'] - self.collapses = [] - self.l_vec = self.c_ops[0].shape[0] - self.num_ops = len(ss.td_n_ops) - self.n_dp = np.zeros(self.num_ops) - self.col_args = 0 - - if self.steady_state: - self.ss_out = np.zeros((self.l_vec, self.l_vec), dtype=complex) - else: - self.ss_out = np.zeros((0, 0), dtype=complex) - - @cython.boundscheck(False) - @cython.wraparound(False) - cdef void qode(self, complex[::1] psi_new): - cdef int i - for i in range(self.l_vec): - psi_new[i] = self.diag_dt[i] * self.psi[i] - - @cython.boundscheck(False) - @cython.wraparound(False) - cdef void ode(self, double t_new, complex[::1] psi_new): - cdef int i - cdef double dt = t_new - self.t - for i in range(self.l_vec): - psi_new[i] = exp(self.diag[i]*dt) * self.psi[i] - - @cython.cdivision(True) - @cython.boundscheck(False) - @cython.wraparound(False) - cdef double advance(self, double t_target, double norm2_prev, - double[::1] rand_vals, int use_quick): - target_norm = rand_vals[0] - if use_quick: - self.qode(self.psi_temp) - else: - self.ode(t_target, self.psi_temp) - norm2_psi = dznrm2(self.psi_temp) ** 2 - - if norm2_psi <= target_norm: - # collapse has occured: - self._find_collapse_diag(norm2_psi, t_target, self.psi_temp, norm2_prev, target_norm) - which = self._which_collapse(self.t, self.psi, rand_vals[1]) - self.psi = self._collapse(self.t, which, self.psi) - self.collapses.append((self.t, which)) - prn = self.prng.rand(2) - rand_vals[0] = prn[0] - rand_vals[1] = prn[1] - norm2_psi = 1. - else: - self.t = t_target - for ii in range(self.l_vec): - self.psi[ii] = self.psi_temp[ii] - - return norm2_psi - - @cython.boundscheck(False) - @cython.wraparound(False) - def run_ode(self, initial_vector, tlist_, e_call, prng): - cdef np.ndarray[double, ndim=1] rand_vals - cdef np.ndarray[double, ndim=1] tlist = np.array(tlist_) - cdef np.ndarray[complex, ndim=1] out_psi = initial_vector.copy() - cdef int ii, k, use_quick, num_times=tlist.shape[0] - cdef double norm2_prev - dt = tlist_[1]-tlist_[0] - if np.allclose(np.diff(tlist_), dt): - use_quick = 1 - self.diag_dt = np.zeros(self.l_vec, dtype=complex) - for ii in range(self.l_vec): - self.diag_dt[ii] = exp(self.diag[ii]*dt) - else: - use_quick = 0 - - if self.steady_state: - self.sumsteadystate(out_psi) - - if self.store_states: - self.states_out = np.zeros((num_times, self.l_vec), dtype=complex) - for ii in range(self.l_vec): - self.states_out[0, ii] = out_psi[ii] - elif self.store_final_state: - self.states_out = np.zeros((1, self.l_vec), dtype=complex) - else: - self.states_out = None - - e_call.step(0, out_psi) - rand_vals = prng.rand(2) - self.prng = prng - - # RUN ODE UNTIL EACH TIME IN TLIST - self.psi = initial_vector.copy() - self.psi_temp = initial_vector.copy() - self.t = tlist[0] - norm2_prev = dznrm2(self.psi) ** 2 - for k in range(1, num_times): - PyErr_CheckSignals() - norm2_prev = self.advance(tlist[k], norm2_prev, rand_vals, use_quick) - while self.t < tlist[k]: - norm2_prev = self.advance(tlist[k], norm2_prev, rand_vals, 0) - # after while loop - # ---------------- - out_psi = normalize(self.psi) - e_call.step(k, out_psi) - if self.steady_state: - self.sumsteadystate(out_psi) - if self.store_states: - for ii in range(self.l_vec): - self.states_out[k, ii] = out_psi[ii] - if not self.store_states and self.store_final_state: - for ii in range(self.l_vec): - self.states_out[0, ii] = out_psi[ii] - return self.states_out, self.ss_out, self.collapses - - @cython.cdivision(True) - @cython.boundscheck(False) - @cython.wraparound(False) - cpdef void _find_collapse_diag(self, double norm2_psi, - double t_final, complex[::1] y_new, - double norm2_prev, double target_norm): - # find collapse time to within specified tolerance - cdef int ii = 0, jj - cdef double t_guess, norm2_guess - while ii < self.norm_steps: - ii += 1 - if (t_final - self.t) < self.norm_t_tol: - self.t = t_final - for jj in range(self.l_vec): - self.psi[jj] = y_new[jj] - break - - t_guess = ( - self.t - + ((t_final - self.t) - * math.log(norm2_prev/target_norm) - / math.log(norm2_prev/norm2_psi)) - ) - if (t_guess - self.t) < self.norm_t_tol: - t_guess = self.t + self.norm_t_tol - - self.ode(t_guess, y_new) - norm2_guess = dznrm2(y_new)**2 - - if (np.abs(target_norm - norm2_guess) < self.norm_tol * target_norm): - self.t = t_guess - for jj in range(self.l_vec): - self.psi[jj] = y_new[jj] - break - elif (norm2_guess < target_norm): - # t_guess is still > t_jump - t_final = t_guess - norm2_psi = norm2_guess - else: - # t_guess < t_jump - self.t = t_guess - for jj in range(self.l_vec): - self.psi[jj] = y_new[jj] - norm2_prev = norm2_guess - - if ii > self.norm_steps: - raise Exception("Norm tolerance not reached. " + - "Increase accuracy of ODE solver or " + - "SolverOptions.mcsolve['norm_steps'].") diff --git a/qutip/solve/_steadystate.pyx b/qutip/solve/_steadystate.pyx deleted file mode 100644 index 46df8e9b05..0000000000 --- a/qutip/solve/_steadystate.pyx +++ /dev/null @@ -1,123 +0,0 @@ -#cython: language_level=3 -#cython: boundscheck=False, wraparound=False, initializedcheck=False - -import numpy as np -cimport numpy as cnp - -from qutip.core.data.base cimport idxint -from qutip.core.data.base import idxint_dtype - -cnp.import_array() - -cdef cnp.ndarray[double] _max_row_weights( - double[::1] data, - int[::1] inds, - int[::1] ptrs, - int ncols): - """ - Finds the largest abs value in each matrix column - and the max. total number of elements in the cols (given by weights[-1]). - - Here we assume that the user already took the ABS value of the data. - This keeps us from having to call abs over and over. - """ - cdef cnp.ndarray[double] weights = np.zeros(ncols + 1, dtype=np.float64) - cdef int ln, mx, ii, jj - cdef double weight, current - mx = 0 - for jj in range(ncols): - ln = (ptrs[jj + 1] - ptrs[jj]) - if ln > mx: - mx = ln - weight = data[ptrs[jj]] - for ii in range(ptrs[jj] + 1, ptrs[jj + 1]): - current = data[ii] - if current > weight: - weight = current - weights[jj] = weight - weights[ncols] = mx - return weights - - -def weighted_bipartite_matching( - double[::1] data, - int[::1] inds, - int[::1] ptrs, - int n): - """ - Here we assume that the user already took the ABS value of the data. - This keeps us from having to call abs over and over. - """ - cdef cnp.ndarray[idxint] visited = np.zeros(n, dtype=idxint_dtype) - cdef cnp.ndarray[idxint] queue = np.zeros(n, dtype=idxint_dtype) - cdef cnp.ndarray[idxint] previous = np.zeros(n, dtype=idxint_dtype) - cdef cnp.ndarray[idxint] match = -1 * np.ones(n, dtype=idxint_dtype) - cdef cnp.ndarray[idxint] row_match = -1 * np.ones(n, dtype=idxint_dtype) - cdef cnp.ndarray[double] weights = _max_row_weights(data, inds, ptrs, n) - cdef cnp.ndarray[idxint] order = np.argsort(-weights[:n]).astype(idxint_dtype) - cdef cnp.ndarray[idxint] row_order = np.zeros(int(weights[n]), - dtype=idxint_dtype) - cdef cnp.ndarray[double] temp_weights = np.zeros(int(weights[n]), - dtype=np.float64) - cdef int queue_ptr, queue_col, queue_size, next_num - cdef int i, j, zz, ll, kk, row, col, temp, eptr, temp2 - - next_num = 1 - for i in range(n): - zz = order[i] # cols with largest abs values first - if (match[zz] == -1 and (ptrs[zz] != ptrs[zz + 1])): - queue[0] = zz - queue_ptr = 0 - queue_size = 1 - - while (queue_size > queue_ptr): - queue_col = queue[queue_ptr] - queue_ptr += 1 - eptr = ptrs[queue_col + 1] - - # get row inds in current column - temp = ptrs[queue_col] - for kk in range(eptr - ptrs[queue_col]): - row_order[kk] = inds[temp] - temp_weights[kk] = data[temp] - temp += 1 - - # linear sort by row weight - for kk in range(1, (eptr - ptrs[queue_col])): - val = temp_weights[kk] - row_val = row_order[kk] - ll = kk - 1 - while (ll >= 0) and (temp_weights[ll] > val): - temp_weights[ll + 1] = temp_weights[ll] - row_order[ll + 1] = row_order[ll] - ll -= 1 - temp_weights[ll + 1] = val - row_order[ll + 1] = row_val - - # go through rows by decending weight - temp2 = (eptr - ptrs[queue_col]) - 1 - for kk in range(eptr - ptrs[queue_col]): - row = row_order[temp2 - kk] - temp = visited[row] - if temp != next_num and temp != -1: - previous[row] = queue_col - visited[row] = next_num - col = row_match[row] - if col == -1: - while row != -1: - col = previous[row] - temp = match[col] - match[col] = row - row_match[row] = col - row = temp - next_num += 1 - queue_size = 0 - break - queue[queue_size] = col - queue_size += 1 - - if match[zz] == -1: - for j in range(1, queue_size): - visited[match[queue[j]]] = -1 - - return match diff --git a/qutip/solve/correlation.py b/qutip/solve/correlation.py deleted file mode 100644 index 20118f11ec..0000000000 --- a/qutip/solve/correlation.py +++ /dev/null @@ -1,854 +0,0 @@ -__all__ = [ - 'correlation_2op_1t', 'correlation_2op_2t', 'correlation_3op_1t', - 'correlation_3op_2t', 'coherence_function_g1', 'coherence_function_g2', - 'spectrum', 'spectrum_correlation_fft', -] - -from re import sub -import types - -import numpy as np -import scipy.fftpack - -from ..core import ( - qeye, Qobj, QobjEvo, liouvillian, spre, unstack_columns, stack_columns, - tensor, qzero, expect -) -from .mesolve import mesolve -from .mcsolve import mcsolve -from .solver import SolverOptions, config -from .steadystate import steadystate -from ..settings import settings -debug = settings.debug - -if debug: - import inspect - - -# ----------------------------------------------------------------------------- -# PUBLIC API -# ----------------------------------------------------------------------------- - -# low level correlation - -def correlation_2op_1t(H, state0, taulist, c_ops, a_op, b_op, - solver="me", reverse=False, args={}, - options=SolverOptions(ntraj=[20, 100])): - r""" - Calculate the two-operator two-time correlation function: - :math:`\left` - along one time axis using the quantum regression theorem and the evolution - solver indicated by the `solver` parameter. - - Parameters - ---------- - - H : Qobj - system Hamiltonian, may be time-dependent for solver choice of `me` or - `mc`. - state0 : 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 - for the `me` and `es` solvers. - taulist : array_like - list of times for :math:`\tau`. taulist must be positive and contain - the element `0`. - c_ops : list - list of collapse operators, may be time-dependent for solver choice of - `me` or `mc`. - a_op : Qobj - operator A. - b_op : Qobj - operator B. - reverse : bool {False, True} - If `True`, calculate :math:`\left` instead of - :math:`\left`. - solver : str {'me', 'mc', 'es'} - choice of solver (`me` for master-equation, `mc` for Monte Carlo, and - `es` for exponential series). - options : SolverOptions - Solver options class. `ntraj` is taken as a two-element list because - the `mc` correlator calls `mcsolve()` recursively; by default, - `ntraj=[20, 100]`. `mc_corr_eps` prevents divide-by-zero errors in - the `mc` correlator; by default, `mc_corr_eps=1e-10`. - - Returns - ------- - corr_vec : ndarray - An array of correlation values for the times specified by `taulist`. - - References - ---------- - See, Gardiner, Quantum Noise, Section 5.2. - - """ - - if debug: - print(inspect.stack()[0][3]) - if reverse: - A_op = a_op - B_op = b_op - C_op = 1 - else: - A_op = 1 - B_op = a_op - C_op = b_op - return _correlation_2t(H, state0, [0], taulist, c_ops, A_op, B_op, C_op, - solver=solver, args=args, options=options)[0] - - -def correlation_2op_2t(H, state0, tlist, taulist, c_ops, a_op, b_op, - solver="me", reverse=False, args={}, - options=SolverOptions(ntraj=[20, 100])): - r""" - Calculate the two-operator two-time correlation function: - :math:`\left` - along two time axes using the quantum regression theorem and the - evolution solver indicated by the `solver` parameter. - - Parameters - ---------- - H : Qobj - system Hamiltonian, may be time-dependent for solver choice of `me` or - `mc`. - state0 : Qobj - Initial state density matrix :math:`\rho_0` or state vector - :math:`\psi_0`. If 'state0' is 'None', then the steady state will - be used as the initial state. The 'steady-state' is only implemented - for the `me` and `es` solvers. - tlist : array_like - list of times for :math:`t`. tlist must be positive and contain the - element `0`. When taking steady-steady correlations only one tlist - value is necessary, i.e. when :math:`t \rightarrow \infty`; here - tlist is automatically set, ignoring user input. - taulist : array_like - list of times for :math:`\tau`. taulist must be positive and contain - the element `0`. - c_ops : list - list of collapse operators, may be time-dependent for solver choice of - `me` or `mc`. - a_op : Qobj - operator A. - b_op : Qobj - operator B. - reverse : bool {False, True} - If `True`, calculate :math:`\left` instead of - :math:`\left`. - solver : str - choice of solver (`me` for master-equation, `mc` for Monte Carlo, and - `es` for exponential series). - options : SolverOptions - solver options class. `ntraj` is taken as a two-element list because - the `mc` correlator calls `mcsolve()` recursively; by default, - `ntraj=[20, 100]`. `mc_corr_eps` prevents divide-by-zero errors in - the `mc` correlator; by default, `mc_corr_eps=1e-10`. - - Returns - ------- - corr_mat : ndarray - 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 - is returned instead. - - References - ---------- - See, Gardiner, Quantum Noise, Section 5.2. - - """ - if debug: - print(inspect.stack()[0][3]) - if tlist is None: - return correlation_2op_1t(H, state0, taulist, c_ops, a_op, b_op, - solver=solver, reverse=reverse, args=args, - options=options) - if reverse: - A_op = a_op - B_op = b_op - C_op = 1 - else: - A_op = 1 - B_op = a_op - C_op = b_op - return _correlation_2t(H, state0, tlist, taulist, c_ops, A_op, B_op, C_op, - solver=solver, args=args, options=options) - - -def correlation_3op_1t(H, state0, taulist, c_ops, a_op, b_op, c_op, - solver="me", args={}, - options=SolverOptions(ntraj=[20, 100])): - r""" - Calculate the three-operator two-time correlation function: - :math:`\left` along one time axis using the - quantum regression theorem and the evolution solver indicated by the - `solver` parameter. - - Note: it is not possibly to calculate a physically meaningful correlation - of this form where :math:`\tau<0`. - - Parameters - ---------- - H : Qobj - system Hamiltonian, may be time-dependent for solver choice of `me` or - `mc`. - rho0 : 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 - for the `me` and `es` solvers. - taulist : array_like - list of times for :math:`\tau`. taulist must be positive and contain - the element `0`. - c_ops : list - list of collapse operators, may be time-dependent for solver choice of - `me` or `mc`. - a_op : Qobj - operator A. - b_op : Qobj - operator B. - c_op : Qobj - operator C. - solver : str - choice of solver (`me` for master-equation, `mc` for Monte Carlo, and - `es` for exponential series). - options : Options - solver options class. `ntraj` is taken as a two-element list because - the `mc` correlator calls `mcsolve()` recursively; by default, - `ntraj=[20, 100]`. `mc_corr_eps` prevents divide-by-zero errors in - the `mc` correlator; by default, `mc_corr_eps=1e-10`. - - Returns - ------- - corr_vec : array - An array of correlation values for the times specified by `taulist` - - References - ---------- - See, Gardiner, Quantum Noise, Section 5.2. - - """ - if debug: - print(inspect.stack()[0][3]) - return _correlation_2t(H, state0, [0], taulist, c_ops, a_op, b_op, c_op, - solver=solver, args=args, options=options)[0] - - -def correlation_3op_2t(H, state0, tlist, taulist, c_ops, a_op, b_op, c_op, - solver="me", args={}, - options=SolverOptions(ntraj=[20, 100])): - r""" - Calculate the three-operator two-time correlation function: - :math:`\left` along two time axes using the - quantum regression theorem and the evolution solver indicated by the - `solver` parameter. - - Note: it is not possibly to calculate a physically meaningful correlation - of this form where :math:`\tau<0`. - - Parameters - ---------- - H : Qobj - system Hamiltonian, may be time-dependent for solver choice of `me` or - `mc`. - rho0 : Qobj - Initial state density matrix :math:`\rho_0` or state vector - :math:`\psi_0`. If 'state0' is 'None', then the steady state will - be used as the initial state. The 'steady-state' is only implemented - for the `me` and `es` solvers. - tlist : array_like - list of times for :math:`t`. tlist must be positive and contain the - element `0`. When taking steady-steady correlations only one tlist - value is necessary, i.e. when :math:`t \rightarrow \infty`; here - tlist is automatically set, ignoring user input. - taulist : array_like - list of times for :math:`\tau`. taulist must be positive and contain - the element `0`. - c_ops : list - list of collapse operators, may be time-dependent for solver choice of - `me` or `mc`. - a_op : Qobj - operator A. - b_op : Qobj - operator B. - c_op : Qobj - operator C. - solver : str - choice of solver (`me` for master-equation, `mc` for Monte Carlo, and - `es` for exponential series). - options : SolverOptions - solver options class. `ntraj` is taken as a two-element list because - the `mc` correlator calls `mcsolve()` recursively; by default, - `ntraj=[20, 100]`. `mc_corr_eps` prevents divide-by-zero errors in - the `mc` correlator; by default, `mc_corr_eps=1e-10`. - - Returns - ------- - 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 - is returned instead. - - References - ---------- - - See, Gardiner, Quantum Noise, Section 5.2. - - """ - if debug: - print(inspect.stack()[0][3]) - if tlist is None: - return correlation_3op_1t(H, state0, taulist, c_ops, a_op, b_op, c_op, - solver=solver, args=args, options=options) - return _correlation_2t(H, state0, tlist, taulist, c_ops, a_op, b_op, c_op, - solver=solver, args=args, options=options) - - -# high level correlation - -def coherence_function_g1(H, state0, taulist, c_ops, a_op, solver="me", - args={}, options=SolverOptions(ntraj=[20, 100])): - r""" - Calculate the normalized first-order quantum coherence function: - - .. math:: - - g^{(1)}(\tau) = - \frac{\langle A^\dagger(\tau)A(0)\rangle} - {\sqrt{\langle A^\dagger(\tau)A(\tau)\rangle - \langle A^\dagger(0)A(0)\rangle}} - - using the quantum regression theorem and the evolution solver indicated by - the `solver` parameter. - - Parameters - ---------- - H : Qobj - system Hamiltonian, may be time-dependent for solver choice of `me` or - `mc`. - state0 : 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 - for the `me` and `es` solvers. - taulist : array_like - list of times for :math:`\tau`. taulist must be positive and contain - the element `0`. - c_ops : list - list of collapse operators, may be time-dependent for solver choice of - `me` or `mc`. - a_op : Qobj - operator A. - solver : str - choice of solver (`me` for master-equation and - `es` for exponential series). - options : SolverOptions - solver options class. `ntraj` is taken as a two-element list because - the `mc` correlator calls `mcsolve()` recursively; by default, - `ntraj=[20, 100]`. `mc_corr_eps` prevents divide-by-zero errors in - the `mc` correlator; by default, `mc_corr_eps=1e-10`. - - Returns - ------- - g1, G1 : tuple - The normalized and unnormalized second-order coherence function. - - """ - # first calculate the photon number - if state0 is None: - state0 = steadystate(H, c_ops) - n = np.array([expect(state0, a_op.dag() * a_op)]) - else: - n = mesolve(H, state0, taulist, c_ops, [a_op.dag() * a_op], - options=options).expect[0] - - # calculate the correlation function G1 and normalize with n to obtain g1 - G1 = correlation_2op_1t(H, state0, taulist, c_ops, a_op.dag(), a_op, - solver=solver, args=args, options=options) - g1 = G1 / np.sqrt(n[0] * n) - return g1, G1 - - -def coherence_function_g2(H, state0, taulist, c_ops, a_op, solver="me", - args={}, options=SolverOptions(ntraj=[20, 100])): - r""" - Calculate the normalized second-order quantum coherence function: - - .. math:: - - g^{(2)}(\tau) = - \frac{\langle A^\dagger(0)A^\dagger(\tau)A(\tau)A(0)\rangle} - {\langle A^\dagger(\tau)A(\tau)\rangle - \langle A^\dagger(0)A(0)\rangle} - - using the quantum regression theorem and the evolution solver indicated by - the `solver` parameter. - - Parameters - ---------- - H : Qobj - system Hamiltonian, may be time-dependent for solver choice of `me` or - `mc`. - state0 : 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 - for the `me` and `es` solvers. - taulist : array_like - list of times for :math:`\tau`. taulist must be positive and contain - the element `0`. - c_ops : list - list of collapse operators, may be time-dependent for solver choice of - `me` or `mc`. - a_op : Qobj - operator A. - args : dict - Dictionary of arguments to be passed to solver. - solver : str - choice of solver (`me` for master-equation and - `es` for exponential series). - options : SolverOptions - solver options class. `ntraj` is taken as a two-element list because - the `mc` correlator calls `mcsolve()` recursively; by default, - `ntraj=[20, 100]`. `mc_corr_eps` prevents divide-by-zero errors in - the `mc` correlator; by default, `mc_corr_eps=1e-10`. - - Returns - ------- - g2, G2 : tuple - The normalized and unnormalized second-order coherence function. - - """ - # first calculate the photon number - if state0 is None: - state0 = steadystate(H, c_ops) - n = np.array([expect(state0, a_op.dag() * a_op)]) - else: - n = mesolve(H, state0, taulist, c_ops, [a_op.dag() * a_op], args=args).expect[0] - - # calculate the correlation function G2 and normalize with n to obtain g2 - G2 = correlation_3op_1t(H, state0, taulist, c_ops, - a_op.dag(), a_op.dag()*a_op, a_op, - solver=solver, args=args, options=options) - g2 = G2 / (n[0] * n) - return g2, G2 - - -# spectrum - -def spectrum(H, wlist, c_ops, a_op, b_op, solver="es", use_pinv=False): - r""" - Calculate the spectrum of the correlation function - :math:`\lim_{t \to \infty} \left`, - i.e., the Fourier transform of the correlation function: - - .. math:: - - S(\omega) = \int_{-\infty}^{\infty} - \lim_{t \to \infty} \left - e^{-i\omega\tau} d\tau. - - 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` - system Hamiltonian. - wlist : array_like - list of frequencies for :math:`\omega`. - c_ops : list - list of collapse operators. - a_op : Qobj - operator A. - b_op : Qobj - operator B. - solver : str - choice of solver (`es` for exponential series and - `pi` for psuedo-inverse). - use_pinv : bool - For use with the `pi` solver: if `True` use numpy's pinv method, - otherwise use a generic solver. - - Returns - ------- - spectrum : array - An array with spectrum :math:`S(\omega)` for the frequencies - specified in `wlist`. - - """ - if debug: - print(inspect.stack()[0][3]) - if solver == "es": - return _spectrum_es(H, wlist, c_ops, a_op, b_op) - elif solver == "pi": - return _spectrum_pi(H, wlist, c_ops, a_op, b_op, use_pinv) - raise ValueError("Unrecognized choice of solver {} (use es or pi)." - .format(solver)) - - -def spectrum_correlation_fft(tlist, y, inverse=False): - """ - Calculate the power spectrum corresponding to a two-time correlation - function using FFT. - - Parameters - ---------- - tlist : array_like - 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 - boolean parameter for using a positive exponent in the Fourier Transform instead. Default is False. - - Returns - ------- - w, S : tuple - Returns an array of angular frequencies 'w' and the corresponding - two-sided power spectrum 'S(w)'. - - """ - if debug: - print(inspect.stack()[0][3]) - tlist = np.asarray(tlist) - N = tlist.shape[0] - dt = tlist[1] - tlist[0] - if not np.allclose(np.diff(tlist), dt*np.ones(N-1, dtype=float)): - raise ValueError('tlist must be equally spaced for FFT.') - F = (N*scipy.fftpack.ifft(y)) if inverse else scipy.fftpack.fft(y) - # calculate the frequencies for the components in F - f = scipy.fftpack.fftfreq(N, dt) - # re-order frequencies from most negative to most positive (centre on 0) - idx = np.array([], dtype='int') - idx = np.append(idx, np.where(f < 0.0)) - idx = np.append(idx, np.where(f >= 0.0)) - return 2*np.pi*f[idx], 2*dt*np.real(F[idx]) - - -# ----------------------------------------------------------------------------- -# PRIVATE SOLVER METHODS -# ----------------------------------------------------------------------------- - -# master 2t correlation solver - -def _correlation_2t(H, state0, tlist, taulist, c_ops, a_op, b_op, c_op, - solver="me", args={}, options=SolverOptions()): - """ - Internal function for calling solvers in order to calculate the - three-operator two-time correlation function: - - """ - - # Note: the current form of the correlator is sufficient for all possible - # two-time correlations (incuding those with 2ops vs 3). Ex: to compute a - # correlation of the form : a_op = identity, b_op = A, - # and c_op = B. - - if debug: - print(inspect.stack()[0][3]) - - if min(tlist) != 0: - raise TypeError("tlist must be positive and contain the element 0.") - if min(taulist) != 0: - raise TypeError("taulist must be positive and contain the element 0.") - - if solver == "me": - return _correlation_me_2t(H, state0, tlist, taulist, - c_ops, a_op, b_op, c_op, - args=args, options=options) - elif solver == "mc": - return _correlation_mc_2t(H, state0, tlist, taulist, - c_ops, a_op, b_op, c_op, - args=args, options=options) - elif solver == "es": - return _correlation_es_2t(H, state0, tlist, taulist, - c_ops, a_op, b_op, c_op) - raise ValueError("Unrecognized choice of solver" + - "%s (use me, mc, or es)." % solver) - - -# master equation solvers - -def _correlation_me_2t(H, state0, tlist, taulist, c_ops, a_op, b_op, c_op, - args={}, options=SolverOptions()): - """ - Internal function for calculating the three-operator two-time - correlation function: - - using a master equation solver. - """ - - # the solvers only work for positive time differences and the correlators - # require positive tau - if state0 is None: - rho0 = steadystate(H, c_ops) - tlist = [0] - elif state0.isket: - rho0 = state0.proj() - else: - rho0 = state0 - - if debug: - print(inspect.stack()[0][3]) - - rho_t = mesolve(H, rho0, tlist, c_ops, args=args, options=options).states - corr_mat = np.zeros([np.size(tlist), np.size(taulist)], dtype=complex) - H = QobjEvo(H, args=args, tlist=tlist, copy=False) - c_ops = [QobjEvo(op, args=args, tlist=tlist, copy=False) for op in c_ops] - - for t_idx, rho in enumerate(rho_t): - H_shifted = H._insert_time_shift(tlist[t_idx]) - c_ops_shifted = [op._insert_time_shift(tlist[t_idx]) for op in c_ops] - - corr_mat[t_idx, :] = mesolve( - H_shifted, c_op * rho * a_op, taulist, c_ops_shifted, - [b_op], args=args, options=options - ).expect[0] - - return corr_mat - - -# exponential series solvers - -def _correlation_es_2t(H, state0, tlist, taulist, c_ops, a_op, b_op, c_op): - """ - Internal function for calculating the three-operator two-time - correlation function: - - using an exponential series solver. - """ - - # the solvers only work for positive time differences and the correlators - # require positive tau - if state0 is None: - rho0 = steadystate(H, c_ops) - tlist = [0] - elif state0.isket: - rho0 = state0.proj() - else: - rho0 = state0 - - if debug: - print(inspect.stack()[0][3]) - - # contruct the Liouvillian - L = liouvillian(H, c_ops) - corr_mat = np.zeros([np.size(tlist), np.size(taulist)], dtype=complex) - solES_t = QobjEvo([[rho, _exponential_term(w)] - for rho, w in zip(*_diagonal_evolution(L, rho0))]) - rho_t = c_op * solES_t * a_op - # evaluate the correlation function - for i, t in enumerate(tlist): - states, rates = _diagonal_evolution(L, rho_t(t)) - expects = np.array([expect(b_op, state) for state in states]) - for k, tau in enumerate(taulist): - corr_mat[i, k] = expects @ np.exp(rates * tau) - return corr_mat - - -def _spectrum_es(H, wlist, c_ops, a_op, b_op): - r""" - Internal function for calculating the spectrum of the correlation function - :math:`\left`. - """ - if debug: - print(inspect.stack()[0][3]) - - # construct the Liouvillian - L = liouvillian(H, c_ops) - # find the steady state density matrix and a_op and b_op expecation values - rho0 = steadystate(L) - a_op_ss = expect(a_op, rho0) - b_op_ss = expect(b_op, rho0) - # eseries solution for (b * rho0)(t) - states, rates = _diagonal_evolution(L, b_op * rho0) - # correlation - ampls = expect(a_op, states) - # make covariance - ampls = np.concatenate([ampls, [-a_op_ss*b_op_ss]]) - rates = np.concatenate([rates, [0]]) - # Tidy up similar rates. - uniques = {} - for r, a in zip(rates, ampls): - for r_ in uniques: - if np.abs(r - r_) < 1e-10: - uniques[r_] += a - break - else: - uniques[r] = a - ampls, rates = [], [] - for r, a in uniques.items(): - if np.abs(a) > 1e-10: - ampls.append(a) - rates.append(r) - ampls, rates = np.array(ampls), np.array(rates) - return np.array([2*np.dot(ampls, 1/(1j * w - rates)).real for w in wlist]) - - -# Monte Carlo solvers - -def _correlation_mc_2t(H, state0, tlist, taulist, c_ops, a_op, b_op, c_op, - args={}, options=SolverOptions()): - """ - Internal function for calculating the three-operator two-time - correlation function: - - using a Monte Carlo solver. - """ - if not c_ops: - raise TypeError("If no collapse operators are required, use the `me`" + - "or `es` solvers") - # the solvers only work for positive time differences and the correlators - # require positive tau - if state0 is None: - raise NotImplementedError("steady state not implemented for " + - "mc solver, please use `es` or `me`") - if not state0.isket: - raise TypeError("state0 must be a state vector.") - psi0 = state0 - - if debug: - print(inspect.stack()[0][3]) - - psi_t_mat = mcsolve( - H, psi0, tlist, c_ops, [], - args=args, ntraj=options['ntraj'][0], options=options, - progress_bar=None, - ).states - - corr_mat = np.zeros([np.size(tlist), np.size(taulist)], dtype=complex) - H = QobjEvo(H, args=args, tlist=tlist, copy=False) - c_ops = [QobjEvo(op, args=args, tlist=tlist, copy=False) for op in c_ops] - - # calculation of from only knowledge of psi0 requires - # averaging over both t and tau - for t_idx in range(np.size(tlist)): - H_shifted = H._insert_time_shift(tlist[t_idx]) - c_ops_shifted = [op._insert_time_shift(tlist[t_idx]) for op in c_ops] - - for trial_idx in range(options['ntraj'][0]): - if isinstance(a_op, Qobj) and isinstance(c_op, Qobj): - if a_op.dag() == c_op: - # A shortcut here, requires only 1/4 the trials - chi_0 = (options.mcsolve['mc_corr_eps'] + c_op) * \ - psi_t_mat[trial_idx, t_idx] - - # evolve these states and calculate expectation value of B - c_tau = chi_0.norm()**2 * mcsolve( - H_shifted, chi_0/chi_0.norm(), taulist, c_ops_shifted, - [b_op], - args=args, ntraj=options['ntraj'][1], options=options, - progress_bar=None - ).expect[0] - - # final correlation vector computed by combining the - # averages - corr_mat[t_idx, :] += c_tau/options['ntraj'][1] - else: - # otherwise, need four trial wavefunctions - # (Ad+C)*psi_t, (Ad+iC)*psi_t, (Ad-C)*psi_t, (Ad-iC)*psi_t - if isinstance(a_op, Qobj): - a_op_dag = a_op.dag() - else: - # assume this is a number, ex. i.e. a_op = 1 - # if this is not correct, the over-loaded addition - # operation will raise errors - a_op_dag = a_op - chi_0 = [(options.mcsolve['mc_corr_eps'] + a_op_dag + - np.exp(1j*x*np.pi/2)*c_op) * - psi_t_mat[trial_idx, t_idx] - for x in range(4)] - - # evolve these states and calculate expectation value of B - c_tau = [ - chi.norm()**2 * mcsolve( - H_shifted, chi/chi.norm(), taulist, c_ops_shifted, - [b_op], - args=args, ntraj=options['ntraj'][1], options=options, - progress_bar=None - ).expect[0] - for chi in chi_0 - ] - - # final correlation vector computed by combining the averages - corr_mat_add = np.asarray( - 1.0 / (4*options['ntraj'][0]) * - (c_tau[0] - c_tau[2] - 1j*c_tau[1] + 1j*c_tau[3]), - dtype=corr_mat.dtype - ) - corr_mat[t_idx, :] += corr_mat_add - - return corr_mat - - -# pseudo-inverse solvers -def _spectrum_pi(H, wlist, c_ops, a_op, b_op, use_pinv=False): - r""" - Internal function for calculating the spectrum of the correlation function - :math:`\left`. - """ - L = H if H.issuper else liouvillian(H, c_ops) - tr_mat = tensor([qeye(n) for n in L.dims[0][0]]) - N = np.prod(L.dims[0][0]) - A = L.full() - b = spre(b_op).full() - a = spre(a_op).full() - - tr_vec = np.transpose(stack_columns(tr_mat.full())) - - rho_ss = steadystate(L) - rho = np.transpose(stack_columns(rho_ss.full())) - - I = np.identity(N * N) - P = np.kron(np.transpose(rho), tr_vec) - Q = I - P - - spectrum = np.zeros(len(wlist)) - for idx, w in enumerate(wlist): - if use_pinv: - MMR = np.linalg.pinv(-1.0j * w * I + A) - else: - MMR = np.dot(Q, np.linalg.solve(-1.0j * w * I + A, Q)) - - s = np.dot(tr_vec, - np.dot(a, np.dot(MMR, np.dot(b, np.transpose(rho))))) - spectrum[idx] = -2 * np.real(s[0, 0]) - return spectrum - - -class _exponential_term: - def __init__(self, rate, amplitude=1): - self.amplitude = amplitude - self.rate = rate - - def __call__(self, t, args=None): - return self.amplitude * np.exp(self.rate * t) - - -def _diagonal_evolution(L, rho0): - """ - Diagonalise the evolution of density matrix rho0 under a constant - Liouvillian L. Returns a list of `states` and an array of the eigenvalues - such that the time evolution of rho0 is represented by - sum_k states[k] * exp(evals[k] * t) - This is effectively the same as the legacy QuTiP function ode2es, but does - not use the removed eseries class. It exists here because ode2es and - essolve were removed. - """ - rho0_full = rho0.full() - if np.abs(rho0_full).sum() < 1e-10 + 1e-24: - return qzero(rho0.dims[0]), np.array([0]) - evals, evecs = L.eigenstates() - evecs = np.vstack([ket.full()[:, 0] for ket in evecs]).T - # evals[i] = eigenvalue i - # evecs[:, i] = eigenvector i - size = rho0.shape[0] * rho0.shape[1] - r0 = stack_columns(rho0_full)[:, 0] - v0 = scipy.linalg.solve(evecs, r0) - vv = evecs * v0[None, :] # product equivalent to `evecs @ np.diag(v0)` - states = [Qobj(unstack_columns(vv[:, i]), dims=rho0.dims, type='oper') - for i in range(size)] - # We don't use QobjEvo because it isn't designed to be efficient when - # calculating - return states, evals diff --git a/qutip/solve/countstat.py b/qutip/solve/countstat.py deleted file mode 100644 index e92bead857..0000000000 --- a/qutip/solve/countstat.py +++ /dev/null @@ -1,253 +0,0 @@ -""" -This module contains functions for calculating current and current noise using -the counting statistics formalism. -""" -__all__ = ['countstat_current', 'countstat_current_noise'] - -import numpy as np -import scipy.sparse as sp - -from ..core import ( - sprepost, spre, operator_to_vector, identity, tensor, -) -from ..core import data as _data -from .steadystate import pseudo_inverse, steadystate -from ..settings import settings - -# Load MKL spsolve if avaiable -if settings.has_mkl: - from qutip._mkl.spsolve import mkl_spsolve - - -def countstat_current(L, c_ops=None, rhoss=None, J_ops=None): - """ - Calculate the current corresponding a system Liouvillian `L` and a list of - current collapse operators `c_ops` or current superoperators `J_ops` - (either must be specified). Optionally the steadystate density matrix - `rhoss` and a list of current superoperators `J_ops` can be specified. If - either of these are omitted they are computed internally. - - Parameters - ---------- - - L : :class:`qutip.Qobj` - Qobj representing the system Liouvillian. - - c_ops : array / list (optional) - List of current collapse operators. - - rhoss : :class:`qutip.Qobj` (optional) - The steadystate density matrix corresponding the system Liouvillian - `L`. - - J_ops : array / list (optional) - List of current superoperators. - - Returns - -------- - I : array - The currents `I` corresponding to each current collapse operator - `c_ops` (or, equivalently, each current superopeator `J_ops`). - """ - - if J_ops is None: - if c_ops is None: - raise ValueError("c_ops must be given if J_ops is not") - J_ops = [sprepost(c, c.dag()) for c in c_ops] - - if rhoss is None: - if c_ops is None: - raise ValueError("c_ops must be given if rhoss is not") - rhoss = steadystate(L, c_ops) - - rhoss_vec = _data.column_stack(rhoss.data.copy()) - - N = len(J_ops) - I = np.zeros(N) - - for i, Ji in enumerate(J_ops): - I[i] = _data.expect_super(Ji.data, rhoss_vec).real - - return I - - -def countstat_current_noise(L, c_ops, wlist=None, rhoss=None, J_ops=None, - sparse=True, method='direct'): - """ - Compute the cross-current noise spectrum for a list of collapse operators - `c_ops` corresponding to monitored currents, given the system - Liouvillian `L`. The current collapse operators `c_ops` should be part - of the dissipative processes in `L`, but the `c_ops` given here does not - necessarily need to be all collapse operators contributing to dissipation - in the Liouvillian. Optionally, the steadystate density matrix `rhoss` - and the current operators `J_ops` correpsonding to the current collapse - operators `c_ops` can also be specified. If either of - `rhoss` and `J_ops` are omitted, they will be computed internally. - 'wlist' is an optional list of frequencies at which to evaluate the noise - spectrum. - - Note: - The default method is a direct solution using dense matrices, as sparse - matrix methods fail for some examples of small systems. - For larger systems it is reccomended to use the sparse solver - with the direct method, as it avoids explicit calculation of the - pseudo-inverse, as 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 - - Parameters - ---------- - - L : :class:`qutip.Qobj` - Qobj representing the system Liouvillian. - - c_ops : array / list - List of current collapse operators. - - rhoss : :class:`qutip.Qobj` (optional) - The steadystate density matrix corresponding the system Liouvillian - `L`. - - wlist : array / list (optional) - List of frequencies at which to evaluate (if none are given, evaluates - at zero frequency) - - J_ops : array / list (optional) - List of current superoperators. - - sparse : bool - Flag that indicates whether to use sparse or dense matrix methods when - computing the pseudo inverse. Default is false, as sparse solvers - can fail for small systems. For larger systems the sparse solvers - are reccomended. - - - Returns - -------- - I, S : tuple of arrays - The currents `I` corresponding to each current collapse operator - `c_ops` (or, equivalently, each current superopeator `J_ops`) and the - zero-frequency cross-current correlation `S`. - """ - - if rhoss is None: - rhoss = steadystate(L, c_ops) - - if J_ops is None: - J_ops = [sprepost(c, c.dag()) for c in c_ops] - - - - N = len(J_ops) - I = np.zeros(N) - - if wlist is None: - wlist = [0.] - S = np.zeros((N, N, len(wlist))) - - if sparse == False: - rhoss_vec = _data.column_stack(rhoss.data.copy()) - for k, w in enumerate(wlist): - R = pseudo_inverse(L, rhoss=rhoss, w=w, - sparse=sparse, method=method).data - for i, Ji in enumerate(J_ops): - Ji = Ji.data - for j, Jj in enumerate(J_ops): - Jj = Jj.data - if i == j: - I[i] = _data.expect_super(Ji, rhoss_vec).real - S[i, j, k] = I[i] - op = _data.add( - _data.matmul(_data.matmul(Ji, R), Jj), - _data.matmul(_data.matmul(Jj, R), Ji), - ) - S[i, j, k] -= _data.expect_super(op, rhoss_vec).real - else: - if method == "direct": - N = np.prod(L.dims[0][0]) - - rhoss_vec = operator_to_vector(rhoss) - - tr_op = tensor([identity(n) for n in L.dims[0][0]]) - tr_op_vec = operator_to_vector(tr_op) - - Pop = _data.kron(rhoss_vec.data, tr_op_vec.data.transpose()) - Iop = _data.identity(N*N) - Q = _data.sub(Iop, Pop) - - for k, w in enumerate(wlist): - - if w != 0.0: - L_temp = 1.0j*w*spre(tr_op) + L - else: - # At zero frequency some solvers fail for small systems. - # Adding a small finite frequency of order 1e-15 - # helps prevent the solvers from throwing an exception. - L_temp = 1e-15j*spre(tr_op) + L - - if not settings.has_mkl: - A = _data.to(_data.CSR, L_temp.data).as_scipy().tocsc() - else: - A = _data.to(_data.CSR, L_temp.data).as_scipy() - A.sort_indices() - - rhoss_vec = _data.column_stack(rhoss.data.copy()) - - for j, Jj in enumerate(J_ops): - Jj = Jj.data - Qj = _data.matmul(Q, _data.matmul(Jj, rhoss_vec), - dtype=_data.Dense).as_ndarray() - try: - if settings.has_mkl: - X_rho_vec_j = mkl_spsolve(A, Qj) - else: - X_rho_vec_j =\ - sp.linalg.splu(A, - permc_spec='COLAMD').solve(Qj) - except: - X_rho_vec_j = sp.linalg.lsqr(A, Qj)[0] - X_rho_vec_j = _data.dense.fast_from_numpy(X_rho_vec_j) - for i, Ji in enumerate(J_ops): - Ji = Ji.data - Qi = _data.matmul(Q, _data.matmul(Ji, rhoss_vec), - dtype=_data.Dense).as_ndarray() - try: - if settings.has_mkl: - X_rho_vec_i = mkl_spsolve(A, Qi) - else: - X_rho_vec_i = ( - sp.linalg.splu(A, permc_spec='COLAMD') - .solve(Qi) - ) - except: - X_rho_vec_i = sp.linalg.lsqr(A, Qi)[0] - if i == j: - I[i] = _data.expect_super(Ji, rhoss_vec).real - S[j, i, k] = I[i] - - X_rho_vec_i = _data.dense.fast_from_numpy(X_rho_vec_i) - S[j, i, k] -= ( - _data.expect_super(_data.matmul(Jj, Q), X_rho_vec_i) - + _data.expect_super(_data.matmul(Ji, Q), X_rho_vec_j) - ).real - - else: - rhoss_vec = _data.column_stack(rhoss.data.copy()) - for k, w in enumerate(wlist): - - R = pseudo_inverse(L, rhoss=rhoss, w=w, sparse=sparse, - method=method) - - for i, Ji in enumerate(J_ops): - Ji = Ji.data - for j, Jj in enumerate(J_ops): - Jj = Jj.data - if i == j: - I[i] = _data.expect_super(Ji, rhoss_vec).real - S[i, j, k] = I[i] - op = _data.add( - _data.matmul(_data.matmul(Ji, R), Jj), - _data.matmul(_data.matmul(Jj, R), Ji), - ) - S[i, j, k] -= _data.expect_super(op, rhoss_vec).real - return I, S diff --git a/qutip/solve/floquet.py b/qutip/solve/floquet.py deleted file mode 100644 index a533ab51b6..0000000000 --- a/qutip/solve/floquet.py +++ /dev/null @@ -1,1057 +0,0 @@ -__all__ = ['floquet_modes', 'floquet_modes_t', 'floquet_modes_table', - 'floquet_modes_t_lookup', 'floquet_states', 'floquet_states_t', - 'floquet_wavefunction', 'floquet_wavefunction_t', - 'floquet_state_decomposition', 'fsesolve', - 'floquet_master_equation_rates', 'floquet_collapse_operators', - 'floquet_master_equation_tensor', - 'floquet_master_equation_steadystate', 'floquet_basis_transform', - 'floquet_markov_mesolve', 'fmmesolve'] - -import numpy as np -import scipy.linalg as la -import scipy -import warnings -from copy import copy -from numpy import angle, pi, exp, sqrt -from types import FunctionType -from .. import ( - Qobj, unstacked_index, stack_columns, unstack_columns, projection, expect, -) -from ..core import data as _data -from .sesolve import sesolve -from .steadystate import steadystate -from .solver import SolverOptions -from ..solver.propagator import propagator -from .solver import Result, _solver_safety_check -from ..utilities import n_thermal - - - -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. - - Returns a list of :class:`qutip.qobj` instances representing the Floquet - modes and a list of corresponding quasienergies, sorted by increasing - quasienergy in the interval [-pi/T, pi/T]. The optional parameter `sort` - decides if the output is to be sorted in increasing quasienergies or not. - - Parameters - ---------- - - H : :class:`qutip.qobj` - system Hamiltonian, time-dependent with period `T` - - args : dictionary - dictionary with variables required to evaluate H - - T : float - The period of the time-dependence of the hamiltonian. The default value - 'None' indicates that the 'tlist' spans a single period of the driving. - - U : :class:`qutip.qobj` - The propagator for the time-dependent Hamiltonian with period `T`. - If U is `None` (default), it will be calculated from the Hamiltonian - `H` using :func:`qutip.propagator.propagator`. - - options : :class:`qutip.solver` - options for the ODE solver. For the propagator U. - - Returns - ------- - - output : list of kets, list of quasi energies - - Two lists: the Floquet modes as kets and the quasi energies. - - """ - - if U is None: - # get the unitary propagator - U = propagator(H, T, [], args=args, options=copy(options)) - - # find the eigenstates for the propagator - evals, evecs = la.eig(U.full()) - - eargs = angle(evals) - - # make sure that the phase is in the interval [-pi, pi], so that - # the quasi energy is in the interval [-pi/T, pi/T] where T is the - # period of the driving. eargs += (eargs <= -2*pi) * (2*pi) + - # (eargs > 0) * (-2*pi) - eargs += (eargs <= -pi) * (2 * pi) + (eargs > pi) * (-2 * pi) - e_quasi = -eargs / T - - # sort by the quasi energy - if sort: - order = np.argsort(-e_quasi) - else: - order = list(range(len(evals))) - - # prepare a list of kets for the floquet states - new_dims = [U.dims[0], [1] * len(U.dims[0])] - new_shape = [U.shape[0], 1] - kets_order = [Qobj(np.array(evecs[:, o]).T, - dims=new_dims) for o in order] - - return kets_order, e_quasi[order] - - -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) - - Parameters - ---------- - - f_modes_0 : list of :class:`qutip.qobj` (kets) - Floquet modes at :math:`t` - - f_energies : list - Floquet energies. - - t : float - The time at which to evaluate the floquet modes. - - H : :class:`qutip.qobj` - system Hamiltonian, time-dependent with period `T` - - args : dictionary - dictionary with variables required to evaluate H - - T : float - The period of the time-dependence of the hamiltonian. - - options : :class:`qutip.solver` - options for the ODE solver. For the propagator. - - Returns - ------- - - output : list of kets - - The Floquet modes as kets at time :math:`t` - - """ - # find t in [0,T] such that t_orig = t + n * T for integer n - t = t - int(t / T) * T - f_modes_t = [] - - # get the unitary propagator from 0 to t - if t > 0.0: - U = propagator(H, t, [], args, options=copy(options)) - - for n in np.arange(len(f_modes_0)): - f_modes_t.append(U * f_modes_0[n] * exp(1j * f_energies[n] * t)) - else: - f_modes_t = f_modes_0 - - return f_modes_t - - -def floquet_modes_table(f_modes_0, f_energies, tlist, H, T, args=None, - options=None): - """ - Pre-calculate the Floquet modes for a range of times spanning the floquet - period. Can later be used as a table to look up the floquet modes for - any time. - - Parameters - ---------- - - f_modes_0 : list of :class:`qutip.qobj` (kets) - Floquet modes at :math:`t` - - f_energies : list - Floquet energies. - - tlist : array - The list of times at which to evaluate the floquet modes. - - H : :class:`qutip.qobj` - system Hamiltonian, time-dependent with period `T` - - T : float - The period of the time-dependence of the hamiltonian. - - args : dictionary - dictionary with variables required to evaluate H - - options : :class:`qutip.solver` - options for the ODE solver. - - Returns - ------- - - output : nested list - - A nested list of Floquet modes as kets for each time in `tlist` - - """ - options = copy(options) or SolverOptions() - # truncate tlist to the driving period - tlist_period = tlist[np.where(tlist <= T)] - - f_modes_table_t = [[] for t in tlist_period] - - opt = SolverOptions() - - for n, f_mode in enumerate(f_modes_0): - output = sesolve(H, f_mode, tlist_period, [], args, opt) - for t_idx, f_state_t in enumerate(output.states): - f_modes_table_t[t_idx].append( - f_state_t * exp(1j * f_energies[n] * tlist_period[t_idx])) - - return f_modes_table_t - - -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. - - Parameters - ---------- - - f_modes_table_t : nested list of :class:`qutip.qobj` (kets) - A lookup-table of Floquet modes at times precalculated by - :func:`qutip.floquet.floquet_modes_table`. - - t : float - The time for which to evaluate the Floquet modes. - - T : float - The period of the time-dependence of the hamiltonian. - - Returns - ------- - - output : nested list - - A list of Floquet modes as kets for the time that most closely matching - the time `t` in the supplied table of Floquet modes. - """ - - # find t_wrap in [0,T] such that t = t_wrap + n * T for integer n - t_wrap = t - int(t / T) * T - - # find the index in the table that corresponds to t_wrap (= tlist[t_idx]) - t_idx = int(t_wrap / T * len(f_modes_table_t)) - - # XXX: might want to give a warning if the cast of t_idx to int discard - # a significant fraction in t_idx, which would happen if the list of time - # values isn't perfect matching the driving period - # if debug: print "t = %f -> t_wrap = %f @ %d of %d" % (t, t_wrap, t_idx, - # N) - - return f_modes_table_t[t_idx] - - -def floquet_states(f_modes_t, f_energies, t): - """ - Evaluate the floquet states at time t given the Floquet modes at that time. - - Parameters - ---------- - - f_modes_t : list of :class:`qutip.qobj` (kets) - A list of Floquet modes for time :math:`t`. - - f_energies : array - The Floquet energies. - - t : float - The time for which to evaluate the Floquet states. - - Returns - ------- - - output : list - - A list of Floquet states for the time :math:`t`. - - """ - - return [(f_modes_t[i] * exp(-1j * f_energies[i] * t)) - for i in np.arange(len(f_energies))] - - -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. - - Parameters - ---------- - - f_modes_t : list of :class:`qutip.qobj` (kets) - A list of initial Floquet modes (for time :math:`t=0`). - - f_energies : array - The Floquet energies. - - t : float - The time for which to evaluate the Floquet states. - - H : :class:`qutip.qobj` - System Hamiltonian, time-dependent with period `T`. - - T : float - The period of the time-dependence of the hamiltonian. - - args : dictionary - Dictionary with variables required to evaluate H. - - options : :class:`qutip.solver` - options for the ODE solver. - - Returns - ------- - - output : list - - A list of Floquet states for the time :math:`t`. - - """ - - f_modes_t = floquet_modes_t(f_modes_0, f_energies, t, H, T, args, - options=options) - return [(f_modes_t[i] * exp(-1j * f_energies[i] * t)) - for i in np.arange(len(f_energies))] - - -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`. - - Parameters - ---------- - - f_modes_t : list of :class:`qutip.qobj` (kets) - A list of initial Floquet modes (for time :math:`t=0`). - - f_energies : array - The Floquet energies. - - f_coeff : array - The coefficients for Floquet decomposition of the initial wavefunction. - - t : float - The time for which to evaluate the Floquet states. - - Returns - ------- - - output : :class:`qutip.qobj` - - The wavefunction for the time :math:`t`. - - """ - return sum( - [f_modes_t[i] * exp(-1j * f_energies[i] * t) * f_coeff[i] - for i in np.arange(1, len(f_energies))], - start=f_modes_t[0] * exp(-1j * f_energies[0] * t) * f_coeff[0] - ) - - -def floquet_wavefunction_t(f_modes_0, f_energies, f_coeff, t, H, T, args=None, - options=None): - """ - Evaluate the wavefunction for a time t using the Floquet state - decompositon, given the initial Floquet modes. - - Parameters - ---------- - - f_modes_t : list of :class:`qutip.qobj` (kets) - A list of initial Floquet modes (for time :math:`t=0`). - - f_energies : array - The Floquet energies. - - f_coeff : array - The coefficients for Floquet decomposition of the initial wavefunction. - - t : float - The time for which to evaluate the Floquet states. - - H : :class:`qutip.qobj` - System Hamiltonian, time-dependent with period `T`. - - T : float - The period of the time-dependence of the hamiltonian. - - args : dictionary - Dictionary with variables required to evaluate H. - - Returns - ------- - - output : :class:`qutip.qobj` - - The wavefunction for the time :math:`t`. - - """ - - f_states_t = floquet_states_t(f_modes_0, f_energies, t, H, T, args, - options=options) - return sum( - [f_states_t[i] * f_coeff[i] for i in np.arange(1, len(f_energies))], - start = f_states_t[0] * f_coeff[0] - ) - - -def floquet_state_decomposition(f_states, f_energies, psi): - r""" - Decompose the wavefunction `psi` (typically an initial state) in terms of - the Floquet states, :math:`\psi = \sum_\alpha c_\alpha \psi_\alpha(0)`. - - Parameters - ---------- - - f_states : list of :class:`qutip.qobj` (kets) - A list of Floquet modes. - - f_energies : array - The Floquet energies. - - psi : :class:`qutip.qobj` - The wavefunction to decompose in the Floquet state basis. - - Returns - ------- - - output : array - - The coefficients :math:`c_\alpha` in the Floquet state decomposition. - - """ - return [state.dag() * psi for state in f_states] - - - -def fsesolve(H, psi0, tlist, e_ops=[], T=None, args={}, Tsteps=100, - options_modes=None): - """ - Solve the Schrodinger equation using the Floquet formalism. - - Parameters - ---------- - - H : :class:`qutip.qobj.Qobj` - System Hamiltonian, time-dependent with period `T`. - - psi0 : :class:`qutip.qobj` - Initial state vector (ket). - - tlist : *list* / *array* - list of times for :math:`t`. - - e_ops : list of :class:`qutip.qobj` / callback function - 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. - - T : float - The period of the time-dependence of the hamiltonian. - - args : dictionary - Dictionary with variables required to evaluate H. - - Tsteps : integer - The number of time steps in one driving period for which to - precalculate the Floquet modes. `Tsteps` should be an even number. - - options_modes : :class:`qutip.solver` - options for the ODE solver. - - Returns - ------- - - output : :class:`qutip.solver.Result` - - An instance of the class :class:`qutip.solver.Result`, which - contains either an *array* of expectation values or an array of - state vectors, for the times specified by `tlist`. - """ - - if not T: - # assume that tlist span exactly one period of the driving - T = tlist[-1] - - if options_modes is None: - options_modes_table = SolverOptions() - else: - options_modes_table = options_modes - - # find the floquet modes for the time-dependent hamiltonian - f_modes_0, f_energies = floquet_modes(H, T, args, - options=options_modes) - - # calculate the wavefunctions using the from the floquet modes - f_modes_table_t = floquet_modes_table(f_modes_0, f_energies, - np.linspace(0, T, Tsteps + 1), - H, T, args, - options=options_modes_table) - - # setup Result for storing the results - output = Result() - output.times = tlist - output.solver = "fsesolve" - - if isinstance(e_ops, FunctionType): - output.num_expect = 0 - expt_callback = True - - elif isinstance(e_ops, list): - - output.num_expect = len(e_ops) - expt_callback = False - - if output.num_expect == 0: - output.states = [] - else: - output.expect = [] - for op in e_ops: - if op.isherm: - output.expect.append(np.zeros(len(tlist))) - else: - output.expect.append(np.zeros(len(tlist), dtype=complex)) - - else: - raise TypeError("e_ops must be a list Qobj or a callback function") - - psi0_fb = psi0.transform(f_modes_0) - for t_idx, t in enumerate(tlist): - f_modes_t = floquet_modes_t_lookup(f_modes_table_t, t, T) - f_states_t = floquet_states(f_modes_t, f_energies, t) - psi_t = psi0_fb.transform(f_states_t, True) - - if expt_callback: - # use callback method - e_ops(t, psi_t) - else: - # calculate all the expectation values, or output psi if - # no expectation value operators where defined - if output.num_expect == 0: - output.states.append(Qobj(psi_t)) - else: - for e_idx, e in enumerate(e_ops): - output.expect[e_idx][t_idx] = expect(e, psi_t) - - return output - - -def floquet_master_equation_rates(f_modes_0, f_energies, c_op, H, T, - args, J_cb, w_th, kmax=5, - f_modes_table_t=None): - """ - Calculate the rates and matrix elements for the Floquet-Markov master - equation. - - .. note : - The number of integration steps (for calculating X) within one period - is set to 20 * kmax. - - Parameters - ---------- - - f_modes_0 : list of :class:`qutip.qobj` (kets) - A list of initial Floquet modes. - - f_energies : array - The Floquet energies. - - c_op : :class:`qutip.qobj` - The collapse operators describing the dissipation. - - H : :class:`qutip.qobj` - System Hamiltonian, time-dependent with period `T`. - - T : float - The period of the time-dependence of the hamiltonian. - - args : dictionary - Dictionary with variables required to evaluate H. - - J_cb : callback functions - A callback function that computes the noise power spectrum, as - a function of frequency, associated with the collapse operator `c_op`. - - w_th : float - The temperature in units of frequency. - - kmax : int - The truncation of the number of sidebands (default 5). - - f_modes_table_t : nested list of :class:`qutip.qobj` (kets) - A lookup-table of Floquet modes at times precalculated by - :func:`qutip.floquet.floquet_modes_table` (optional). - - options : :class:`qutip.solver` - options for the ODE solver. - - Returns - ------- - - output : list - - A list (Delta, X, Gamma, A) containing the matrices Delta, X, Gamma - and A used in the construction of the Floquet-Markov master equation. - - """ - - N = len(f_energies) - M = 2 * kmax + 1 - - omega = (2 * pi) / T - - Delta = np.zeros((N, N, M)) - X = np.zeros((N, N, M), dtype=complex) - Gamma = np.zeros((N, N, M)) - A = np.zeros((N, N)) - - # time steps for integration of coupling operator - nT = int(np.max([20 * kmax, 100])) - dT = T / nT - tlist = np.arange(dT, T + dT / 2, dT) - - if f_modes_table_t is None: - f_modes_table_t = floquet_modes_table(f_modes_0, f_energies, - np.linspace(0, T, nT + 1), H, T, - args) - - for t in tlist: - # TODO: repeated invocations of floquet_modes_t is - # inefficient... make a and b outer loops and use the mesolve - # instead of the propagator. - - f_modes_t = np.hstack([f.full() for f in floquet_modes_t_lookup( - f_modes_table_t, t, T)]) - FF = f_modes_t.T.conj() @ c_op.full() @ f_modes_t - phi = exp(-1j * np.arange(-kmax, kmax+1) * omega * t) - X += (dT / T) * np.einsum("ij,k->ijk", FF, phi) - - Heaviside = lambda x: ((np.sign(x) + 1) / 2.0) - for a in range(N): - for b in range(N): - k_idx = 0 - for k in range(-kmax, kmax + 1, 1): - Delta[a, b, k_idx] = f_energies[a] - f_energies[b] + k * omega - Gamma[a, b, k_idx] = 2 * pi * Heaviside(Delta[a, b, k_idx]) * \ - J_cb(Delta[a, b, k_idx]) * abs(X[a, b, k_idx]) ** 2 - k_idx += 1 - - for a in range(N): - for b in range(N): - for k in range(-kmax, kmax + 1, 1): - k1_idx = k + kmax - k2_idx = -k + kmax - A[a, b] += Gamma[a, b, k1_idx] + \ - n_thermal(abs(Delta[a, b, k1_idx]), w_th) * \ - (Gamma[a, b, k1_idx] + Gamma[b, a, k2_idx]) - - return Delta, X, Gamma, A - - -def floquet_collapse_operators(A): - """ - Construct collapse operators corresponding to the Floquet-Markov - master-equation rate matrix `A`. - - .. note:: - - Experimental. - - """ - c_ops = [] - - N, M = np.shape(A) - - # - # Here we really need a master equation on Bloch-Redfield form, or perhaps - # we can use the Lindblad form master equation with some rotating frame - # approximations? ... - # - for a in range(N): - for b in range(N): - if a != b and abs(A[a, b]) > 0.0: - # only relaxation terms included... - c_ops.append(sqrt(A[a, b]) * projection(N, a, b)) - - return c_ops - - -def floquet_master_equation_tensor(Alist, f_energies): - """ - Construct a tensor that represents the master equation in the floquet - basis (with constant Hamiltonian and collapse operators). - - Simplest RWA approximation [Grifoni et al, Phys.Rep. 304 229 (1998)] - - Parameters - ---------- - - Alist : list - A list of Floquet-Markov master equation rate matrices. - - f_energies : array - The Floquet energies. - - Returns - ------- - - output : array - - The Floquet-Markov master equation tensor `R`. - - """ - - if isinstance(Alist, list): - # Alist can be a list of rate matrices corresponding - # to different operators that couple to the environment - N, M = np.shape(Alist[0]) - else: - # or a simple rate matrix, in which case we put it in a list - Alist = [Alist] - N, M = np.shape(Alist[0]) - - Rdata_lil = scipy.sparse.lil_matrix((N * N, N * N), dtype=complex) - - AsumList = [np.sum(A, axis=1) for A in Alist] - - for k in range(len(Alist)): - for i in range(N): - Rdata_lil[i+N*i, i+N*i] -= -Alist[k][i, i] + AsumList[k][i] - for j in range(i+1, N): - Rdata_lil[i+N*i, j+N*j] += Alist[k][j, i] - Rdata_lil[j+N*j, i+N*i] += Alist[k][i, j] - a_term = -(1/2)*(AsumList[k][i] + AsumList[k][j]) - Rdata_lil[i+N*j, i+N*j] += a_term - Rdata_lil[j+N*i, j+N*i] += a_term - - return Qobj(Rdata_lil, dims=[[N, N], [N, N]]) - - -def floquet_master_equation_steadystate(H, A): - """ - Returns the steadystate density matrix (in the floquet basis!) for the - Floquet-Markov master equation. - """ - c_ops = floquet_collapse_operators(A) - rho_ss = steadystate(H, c_ops) - return rho_ss - - -def floquet_basis_transform(f_modes, f_energies, rho0): - """ - Make a basis transform that takes rho0 from the floquet basis to the - computational basis. - """ - return rho0.transform(f_modes, True) - - -# ----------------------------------------------------------------------------- -# Floquet-Markov master equation -# -# - - -def floquet_markov_mesolve( - R, rho0, tlist, e_ops, options=None, floquet_basis=True, - f_modes_0=None, f_modes_table_t=None, f_energies=None, T=None, -): - """ - Solve the dynamics for the system using the Floquet-Markov master equation. - - .. note:: - - It is important to understand in which frame and basis the results - are returned here. - - Parameters - ---------- - - R : array - The Floquet-Markov master equation tensor `R`. - - rho0 : :class:`qutip.qobj` - Initial density matrix. If ``f_modes_0`` is not passed, this density - matrix is assumed to be in the Floquet picture. - - tlist : *list* / *array* - list of times for :math:`t`. - - e_ops : list of :class:`qutip.qobj` / callback function - list of operators for which to evaluate expectation values. - - options : :class:`qutip.solver` - options for the ODE solver. - - floquet_basis: bool, True - If ``True``, states and expectation values will be returned in the - Floquet basis. If ``False``, a transformation will be made to the - computational basis; this will be in the lab frame if - ``f_modes_table``, ``T` and ``f_energies`` are all supplied, or the - interaction picture (defined purely be f_modes_0) if they are not. - - f_modes_0 : list of :class:`qutip.qobj` (kets), optional - A list of initial Floquet modes, used to transform the given starting - density matrix into the Floquet basis. If this is not passed, it is - assumed that ``rho`` is already in the Floquet basis. - - f_modes_table_t : nested list of :class:`qutip.qobj` (kets), optional - A lookup-table of Floquet modes at times precalculated by - :func:`qutip.floquet.floquet_modes_table`. Necessary if - ``floquet_basis`` is ``False`` and the transformation should be made - back to the lab frame. - - f_energies : array_like of float, optional - The precalculated Floquet quasienergies. Necessary if - ``floquet_basis`` is ``False`` and the transformation should be made - back to the lab frame. - - T : float, optional - The time period of driving. Necessary if ``floquet_basis`` is - ``False`` and the transformation should be made back to the lab frame. - - Returns - ------- - - output : :class:`qutip.solver.Result` - An instance of the class :class:`qutip.solver.Result`, which - contains either an *array* of expectation values or an array of - state vectors, for the times specified by `tlist`. - """ - opt = options or SolverOptions() - rho0 = rho0.proj() if rho0.isket else rho0 - - # Prepare output object. - dt = tlist[1] - tlist[0] - output = Result() - output.solver = "fmmesolve" - output.times = tlist - if isinstance(e_ops, FunctionType): - expt_callback = True - store_states = opt['store_states'] or False - else: - expt_callback = False - try: - e_ops = list(e_ops) - except TypeError: - raise TypeError("`e_ops` must be iterable or a function") from None - n_expt_op = len(e_ops) - if n_expt_op == 0: - store_states = True - else: - output.expect = [] - output.num_expect = n_expt_op - for op in e_ops: - dtype = np.float64 if op.isherm else np.complex128 - output.expect.append(np.zeros(len(tlist), dtype=dtype)) - store_states = opt['store_states'] or (n_expt_op == 0) - if store_states: - output.states = [] - - # Choose which frame transformations should be done on the initial and - # evolved states. - lab_lookup = [f_modes_table_t, f_energies, T] - if ( - any(x is None for x in lab_lookup) - and not all(x is None for x in lab_lookup) - ): - warnings.warn( - "if transformation back to the computational basis in the lab" - "frame is desired, all of `f_modes_t`, `f_energies` and `T` must" - "be supplied." - ) - f_modes_table_t = f_energies = T = None - - # Initial state. - if f_modes_0 is not None: - rho0 = rho0.transform(f_modes_0) - - # Evolved states. - if floquet_basis: - def transform(rho, t): - return rho - elif f_modes_table_t is not None: - # Lab frame, computational basis. - def transform(rho, t): - f_modes_t = floquet_modes_t_lookup(f_modes_table_t, t, T) - f_states_t = floquet_states(f_modes_t, f_energies, t) - return rho.transform(f_states_t, True) - elif f_modes_0 is not None: - # Interaction picture, computational basis. - def transform(rho, t): - return rho.transform(f_modes_0, False) - else: - raise ValueError( - "cannot transform out of the Floquet basis without some knowledge " - "of the Floquet modes. Pass `f_modes_0`, or all of `f_modes_t`, " - "`f_energies` and `T`." - ) - - # - # setup integrator - # - initial_vector = stack_columns(rho0.full()) - r = scipy.integrate.ode(_wrap_matmul) - r.set_f_params(R.data) - r.set_integrator('zvode', method=opt['method'], order=opt['order'], - atol=opt['atol'], rtol=opt['rtol'], max_step=opt['max_step']) - r.set_initial_value(initial_vector, tlist[0]) - - # Main evolution loop. - for t_idx, t in enumerate(tlist): - if not r.successful(): - break - - rho = transform(Qobj(unstack_columns(r.y), rho0.dims), t) - - if expt_callback: - e_ops(t, rho) - else: - for m, e_op in enumerate(e_ops): - output.expect[m][t_idx] = expect(e_op, rho) - if store_states: - output.states.append(rho) - r.integrate(r.t + dt) - return output - - -def _wrap_matmul(t, state, operator): - return _data.matmul(operator, _data.dense.fast_from_numpy(state), - dtype=_data.Dense).as_ndarray() - -# ----------------------------------------------------------------------------- -# Solve the Floquet-Markov master equation -# -# - - -def fmmesolve(H, rho0, tlist, c_ops=[], e_ops=[], spectra_cb=[], T=None, - args={}, options=SolverOptions(), floquet_basis=True, kmax=5, - _safe_mode=True, options_modes=None): - """ - Solve the dynamics for the system using the Floquet-Markov master equation. - - Parameters - ---------- - - H : :class:`qutip.qobj` - system Hamiltonian. - - rho0 / psi0 : :class:`qutip.qobj` - initial density matrix or state vector (ket). - - tlist : *list* / *array* - list of times for :math:`t`. - - c_ops : list of :class:`qutip.qobj` - list of collapse operators. - - e_ops : list of :class:`qutip.qobj` / callback function - list of operators for which to evaluate expectation values. - - 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`. - - T : float - The period of the time-dependence of the hamiltonian. The default value - 'None' indicates that the 'tlist' spans a single period of the driving. - - args : *dictionary* - dictionary of parameters for time-dependent Hamiltonians and - collapse operators. - - This dictionary should also contain an entry 'w_th', which is - the temperature of the environment (if finite) in the - energy/frequency units of the Hamiltonian. For example, if - the Hamiltonian written in units of 2pi GHz, and the - temperature is given in K, use the following conversion - - >>> temperature = 25e-3 # unit K # doctest: +SKIP - >>> h = 6.626e-34 # doctest: +SKIP - >>> kB = 1.38e-23 # doctest: +SKIP - >>> args['w_th'] = temperature * (kB / h) * 2 * pi * 1e-9 \ - #doctest: +SKIP - - options : :class:`qutip.solver` - options for the ODE solver. For solving the master equation. - - floquet_basis : bool - Will return results in Floquet basis or computational basis - (optional). - - k_max : int - The truncation of the number of sidebands (default 5). - - options_modes : :class:`qutip.solver` - options for the ODE solver. For computing Floquet modes. - - Returns - ------- - - output : :class:`qutip.solver` - - An instance of the class :class:`qutip.solver`, which contains either - an *array* of expectation values for the times specified by `tlist`. - """ - - if _safe_mode: - _solver_safety_check(H, rho0, c_ops, e_ops, args) - - if options_modes is None: - options_modes_table = SolverOptions() - else: - options_modes_table = options_modes - - if T is None: - T = max(tlist) - - if len(spectra_cb) == 0: - # add white noise callbacks if absent - spectra_cb = [lambda w: 1.0] * len(c_ops) - - if len(spectra_cb) != len(c_ops): - raise ValueError("Length of c_ops and spectra_cb don't match.") - - f_modes_0, f_energies = floquet_modes(H, T, args, - options=options_modes) - - f_modes_table_t = floquet_modes_table(f_modes_0, f_energies, - np.linspace(0, T, 500 + 1), - H, T, args, - options=options_modes_table) - - # get w_th from args if it exists - if 'w_th' in args: - w_th = args['w_th'] - else: - w_th = 0 - - # floquet-markov master equation tensor - R = 0 - # loop over input c_ops and spectra_cb, calculate one R for each set - for c_op, spectrum in zip(c_ops, spectra_cb): - # calculate the rate-matrices for the floquet-markov master equation - Delta, X, Gamma, Amat = floquet_master_equation_rates( - f_modes_0, f_energies, c_op, H, T, args, spectrum, - w_th, kmax, f_modes_table_t) - - # calculate temporary floquet-markov master equation tensor - R += floquet_master_equation_tensor(Amat, f_energies) - - return floquet_markov_mesolve(R, rho0, tlist, e_ops, - options=options, - floquet_basis=floquet_basis, - f_modes_0=f_modes_0, - f_modes_table_t=f_modes_table_t, - T=T, - f_energies=f_energies) diff --git a/qutip/solve/krylovsolve.py b/qutip/solve/krylovsolve.py deleted file mode 100644 index 8ae51b6441..0000000000 --- a/qutip/solve/krylovsolve.py +++ /dev/null @@ -1,721 +0,0 @@ -__all__ = ["krylovsolve"] -""" -This module provides approximations of the time evolution operator -using small dimensional Krylov subspaces. -""" - -from scipy.optimize import root_scalar -from math import ceil -import numpy as np -import warnings - -from qutip import expect, Qobj -from .solver import Result, SolverOptions -from qutip.ui.progressbar import BaseProgressBar, TextProgressBar -from qutip.core.data.eigen import eigs -from qutip.core import data as _data - - -def krylovsolve( - H: Qobj, - psi0: Qobj, - tlist: np.array, - krylov_dim: int, - e_ops=None, - options=None, - progress_bar: bool = None, - sparse: bool = False, -): - """ - Time evolution of state vectors for time independent Hamiltonians. - Evolve the state vector ("psi0") finding an approximation for the time - evolution operator of Hamiltonian ("H") by obtaining the projection of - the time evolution operator on a set of small dimensional Krylov - subspaces (m << dim(H)). - - The output is either the state vector or the expectation values of - supplied operators ("e_ops") at arbitrary points at ("tlist"). - - **Additional options** - - Additional options to krylovsolve can be set with the following: - - * "store_states": stores states even though expectation values are - requested via the "e_ops" argument. - - * "store_final_state": store final state even though expectation values are - requested via the "e_ops" argument. - - Parameters - ---------- - H : :class:`qutip.Qobj` - System Hamiltonian. - psi0 : :class: `qutip.Qobj` - Initial state vector (ket). - tlist : None / *list* / *array* - List of times on which to evolve the initial state. If None, nothing - happens but the code won't break. - krylov_dim: int - Dimension of Krylov approximation subspaces used for the time - evolution approximation. - e_ops : None / list of :class:`qutip.Qobj` - Single operator or list of operators for which to evaluate - expectation values. - options : SolverOptions - Instance of ODE solver options, as well as krylov parameters. - atol: controls (approximately) the error desired for the final - solution. (Defaults to 1e-8) - nsteps: maximum number of krylov's internal number of Lanczos - iterations. (Defaults to 10000) - progress_bar : None / BaseProgressBar - Optional instance of BaseProgressBar, or a subclass thereof, for - showing the progress of the simulation. - sparse : bool (default False) - Use np.array to represent system Hamiltonians. If True, scipy sparse - arrays are used instead. - - Returns - ------- - result: :class:`qutip.Result` - An instance of the class :class:`qutip.Result`, which contains - either an *array* `result.expect` of expectation values for the times - `tlist`, or an *array* `result.states` of state vectors corresponding - to the times `tlist` [if `e_ops` is an empty list]. - """ - # check the physics - _check_inputs(H, psi0, krylov_dim) - - # check extra inputs - e_ops, e_ops_dict = _check_e_ops(e_ops) - pbar = _check_progress_bar(progress_bar) - - # transform inputs type from Qobj to np.ndarray/csr_matrix - if sparse: - _H = _data.to('csr', H.data).as_scipy() # (fast_) csr_matrix - else: - _H = H.full().copy() # np.ndarray - _psi = psi0.full().copy() - _psi = _psi / np.linalg.norm(_psi) - - # create internal variable and output containers - if options is None: - options = SolverOptions(nsteps=10000) - krylov_results = Result() - krylov_results.solver = "krylovsolve" - - # handle particular cases of an empty tlist or single element - n_tlist_steps = len(tlist) - if n_tlist_steps < 1: - return krylov_results - - if n_tlist_steps == 1: # if tlist has only one element, return it - krylov_results = particular_tlist_or_happy_breakdown( - tlist, n_tlist_steps, options, psi0, e_ops, krylov_results, pbar - ) # this will also raise a warning - return krylov_results - - tf = tlist[-1] - t0 = tlist[0] - - # optimization step using Lanczos, then reuse it for the first partition - dim_m = krylov_dim - krylov_basis, T_m = lanczos_algorithm( - _H, _psi, krylov_dim=dim_m, sparse=sparse - ) - - # check if a happy breakdown occurred - if T_m.shape[0] < krylov_dim + 1: - if T_m.shape[0] == 1: - # this means that the state does not evolve in time, it lies in a - # symmetry of H subspace. Thus, theres no work to be done. - krylov_results = particular_tlist_or_happy_breakdown( - tlist, - n_tlist_steps, - options, - psi0, - e_ops, - krylov_results, - pbar, - happy_breakdown=True, - ) - return krylov_results - else: - # no optimization is required, convergence is guaranteed. - delta_t = tf - t0 - n_timesteps = 1 - else: - - # calculate optimal number of internal timesteps. - delta_t = _optimize_lanczos_timestep_size( - T_m, krylov_basis=krylov_basis, tlist=tlist, options=options - ) - n_timesteps = int(ceil((tf - t0) / delta_t)) - - if n_timesteps >= options['nsteps']: - raise Exception( - f"Optimization requires a number {n_timesteps} of lanczos iterations, " - f"which exceeds the defined allowed number {options['nsteps']}. This can " - "be increased via the 'SolverOptions.nsteps' property." - ) - - partitions = _make_partitions(tlist=tlist, n_timesteps=n_timesteps) - - if progress_bar: - pbar.start(len(partitions)) - - # update parameters regarding e_ops - krylov_results, expt_callback, options, n_expt_op = _e_ops_outputs( - krylov_results, e_ops, n_tlist_steps, options - ) - - # parameters for the lazy iteration evolve tlist - psi_norm = np.linalg.norm(_psi) - last_t = t0 - - for idx, partition in enumerate(partitions): - - evolved_states = _evolve_krylov_tlist( - H=_H, - psi0=_psi, - krylov_dim=dim_m, - tlist=partition, - t0=last_t, - psi_norm=psi_norm, - krylov_basis=krylov_basis, - T_m=T_m, - sparse=sparse, - ) - - if idx == 0: - krylov_basis = None - T_m = None - t_idx = 0 - - _psi = evolved_states[-1] - psi_norm = np.linalg.norm(_psi) - last_t = partition[-1] - - # apply qobj to each evolved state, remove repeated tail elements - qobj_evolved_states = [ - Qobj(state, dims=psi0.dims) for state in evolved_states[1:-1] - ] - - krylov_results = _expectation_values( - e_ops, - n_expt_op, - expt_callback, - krylov_results, - qobj_evolved_states, - partitions, - idx, - t_idx, - options, - ) - - t_idx += len(partition[1:-1]) - - pbar.update(idx) - - pbar.finished() - - if e_ops_dict: - krylov_results.expect = { - e: krylov_results.expect[n] - for n, e in enumerate(e_ops_dict.keys()) - } - - return krylov_results - - -def _expectation_values( - e_ops, - n_expt_op, - expt_callback, - res, - evolved_states, - partitions, - idx, - t_idx, - options, -): - - if options['store_states']: - res.states += evolved_states - - for t, state in zip( - range(t_idx, t_idx + len(partitions[idx][1:-1])), evolved_states - ): - - if expt_callback: - # use callback method - res.expect.append(e_ops(t, state)) - - for m in range(n_expt_op): - op = e_ops[m] - if not isinstance(op, Qobj) and callable(op): - res.expect[m][t] = op(t, state) - continue - - res.expect[m][t] = expect(op, state) - - if ( - idx == len(partitions) - 1 - and options['store_final_state'] - and not options['store_states'] - ): - res.states = [evolved_states[-1]] - - return res - - -def lanczos_algorithm( - H, - psi: np.ndarray, - krylov_dim: int, - sparse: bool = False, -): - """ - Computes a basis of the Krylov subspace for Hamiltonian 'H', a system - state 'psi' and Krylov dimension 'krylov_dim'. The space is spanned - by {psi, H psi, H^2 psi, ..., H^(krylov_dim) psi}. - Parameters - ------------ - H : np.ndarray or csr_matrix - System Hamiltonian. If the Hamiltonian is dense, a np.ndarray is - preferred, whereas if it is sparse, a scipy csr_matrix is optimal. - psi: np.ndarray - State used to calculate Krylov subspace. - krylov_dim: int - Dimension (krylov_dim + 1) of the spanned Krylov subspace. - sparse: bool (optional, default False) - Wether to perform scipy sparse matrix multiplication operations or - numpy dense matrix multiplications. - Returns - --------- - v: np.ndarray - Lanczos eigenvector. - T: np.ndarray - Tridiagonal decomposition. - """ - - v = np.zeros((krylov_dim + 1, psi.shape[0]), dtype=complex) - T_m = np.zeros((krylov_dim + 1, krylov_dim + 1), dtype=complex) - - v[0, :] = psi.squeeze() - - w_prime = H.dot(v[0, :]) - - alpha = np.vdot(w_prime, v[0, :]) - - w = w_prime - alpha * v[0, :] - - T_m[0, 0] = alpha - - for j in range(1, krylov_dim + 1): - - beta = np.linalg.norm(w) - - if beta < 1e-7: - - # Happy breakdown - v_happy = v[0:j, :] - T_m_happy = T_m[0:j, 0:j] - - return v_happy, T_m_happy - - v[j, :] = w / beta - w_prime = H.dot(v[j, :]) - alpha = np.vdot(w_prime, v[j, :]) - - w = w_prime - alpha * v[j, :] - beta * v[j - 1, :] - - T_m[j, j] = alpha - T_m[j, j - 1] = beta - T_m[j - 1, j] = beta - - return v, T_m - - -def _evolve(t0: float, krylov_basis: np.ndarray, T_m: np.ndarray): - """ - Computes the time evolution operator 'U(t - t0) psi0_k', where 'psi0_k' - is the first basis element of the Krylov subspace, as a function of time. - Parameters - ------------ - t0: float - Initial time for the time evolution. - krylov_basis: np.ndarray - Krylov basis projector operator. - T_m: np.ndarray - Tridiagonal matrix decomposition of the system given by lanczos - algorithm. - Returns - --------- - time_evolution: function - Time evolution given by the Krylov subspace approximation. - """ - - eigenvalues, eigenvectors = eigs(_data.Dense(T_m), True) - eigenvectors = eigenvectors.to_array() - U = np.matmul(krylov_basis.T, eigenvectors) - e0 = eigenvectors.conj().T[:, 0] - - def time_evolution(t): - delta_t = t - t0 - aux = np.multiply(np.exp(-1j * delta_t * eigenvalues), e0) - return np.matmul(U, aux) - - return time_evolution - - -def _evolve_krylov_tlist( - H, - psi0: np.ndarray, - krylov_dim: int, - tlist: list, - t0: float, - psi_norm: float = None, - krylov_basis: np.array = None, - T_m: np.array = None, - sparse: bool = False, -): - """ - Computes the Krylov approximation time evolution of dimension 'krylov_dim' - for Hamiltonian 'H' and initial state 'psi0' for each time in 'tlist'. - Parameters - ------------ - H: np.ndarray or csr_matrix - System Hamiltonian. - psi0: np.ndarray - Initial state vector. - krylov_basis: np.ndarray - Krylov basis projector operator. - tlist: list - List of timesteps for the time evolution. - t0: float - Initial time for the time evolution. - psi_norm: float (optional, default False) - Norm-2 of psi0. - krylov_basis: np.ndarray (optional, default None) - Krylov basis projector operator. If 'krylov_basis' is None, perform - a lanczos iteration. - T_m: np.ndarray (optional, default None) - Tridiagonal matrix decomposition of the system given by lanczos - algorithm. If 'T_m' is None, perform a lanczos iteration. - Returns - --------- - psi_list: List[np.ndarray] - List of evolved states at times t in 'tlist'. - """ - - if psi_norm is None: - psi_norm = np.linalg.norm(psi0) - - if psi_norm != 1: - psi = psi0 / psi_norm - else: - psi = psi0 - - if (krylov_basis is None) or (T_m is None): - krylov_basis, T_m = lanczos_algorithm( - H=H, psi=psi, krylov_dim=krylov_dim, sparse=sparse - ) - - evolve = _evolve(t0, krylov_basis, T_m) - psi_list = list(map(evolve, tlist)) - - return psi_list - - -# ---------------------------------------------------------------------- -# Auxiliar functions - - -def _check_inputs(H, psi0, krylov_dim): - """Check that the inputs 'H' and 'psi0' have the correct structures.""" - if not isinstance(H, Qobj): - raise TypeError( - "krylovsolve currently supports Hamiltonian Qobj operators only" - ) - - if not H.isherm: - raise TypeError("Hamiltonian 'H' must be hermician.") - - if not isinstance(psi0, Qobj): - raise TypeError("'psi0' must be a Qobj.") - - if not psi0.isket: - raise TypeError("Initial state must be a ket Qobj.") - - if not ((len(H.shape) == 2) and (H.shape[0] == H.shape[1])): - raise ValueError("the Hamiltonian must be 2-dimensional square Qobj.") - - if not (psi0.dims[0] == H.dims[0]): - raise ValueError( - "'psi0' and the Hamiltonian must share the same dimension." - ) - - if not (H.shape[0] >= krylov_dim): - raise ValueError( - "the Hamiltonian dimension must be greater or equal to the maximum" - " allowed krylov dimension 'krylov_dim'." - ) - - -def _check_e_ops(e_ops): - """ - Check instances of e_ops and return the formatted version of e_ops - and e_ops_dict. - """ - if e_ops is None: - e_ops = [] - if isinstance(e_ops, Qobj): - e_ops = [e_ops] - if isinstance(e_ops, dict): - e_ops_dict = e_ops - e_ops = [e for e in e_ops.values()] - else: - e_ops_dict = None - return e_ops, e_ops_dict - - -def _check_progress_bar(progress_bar): - """ - Check instance of progress_bar and return the object. - """ - if progress_bar is None: - pbar = BaseProgressBar() - if progress_bar is True: - pbar = TextProgressBar() - return pbar - - -def particular_tlist_or_happy_breakdown( - tlist, - n_tlist_steps, - options, - psi0, - e_ops, - res, - progress_bar, - happy_breakdown=False, -): - """Deals with the problem when 'tlist' contains a single element, where - that same ket is returned and evaluated at 'e_ops', if provided. - """ - - if len(tlist) == 0: - warnings.warn( - "Input 'tlist' contains a single element. If 'e_ops' were provided" - ", return its corresponding expectation values at 'psi0', else " - "return 'psi0'." - ) - - progress_bar.start(1) - - res, expt_callback, options, n_expt_op = _e_ops_outputs( - res, e_ops, n_tlist_steps, options - ) - - if options['store_states']: - res.states = [psi0] - - e_0 = None - if expt_callback: - # use callback method - e_0 = e_ops(0, psi0) - res.expect.append(e_0) - - e_m_0 = [] - for m in range(n_expt_op): - op = e_ops[m] - - if not isinstance(op, Qobj) and callable(op): - e_m_0.append(op(0, psi0)) - res.expect[m][0] = e_m_0[m] - continue - - e_m_0.append(expect(op, psi0)) - res.expect[m][0] = e_m_0[m] - - if happy_breakdown: - res = _happy_breakdown( - tlist, - options, - res, - psi0, - expt_callback, - e_0, - n_expt_op, - e_ops, - e_m_0, - ) - - if (options['store_final_state']) and (not options['store_states']): - res.states = [psi0] - - progress_bar.update(1) - progress_bar.finished() - return res - - -def _happy_breakdown( - tlist, options, res, psi0, expt_callback, e_0, n_expt_op, e_ops, e_m_0 -): - """ - Dummy evolves the system if a happy breakdown of an eigenstate occurs. - """ - for i in range(1, len(tlist)): - if options['store_states']: - res.states.append(psi0) - if expt_callback: - res.expect.append(e_0) - - for m in range(n_expt_op): - op = e_ops[m] - res.expect[m][i] = e_m_0[m] - return res - - -def _optimize_lanczos_timestep_size(T, krylov_basis, tlist, options): - """ - Solves the equation defined to optimize the number of Lanczos - iterations to be performed inside Krylov's algorithm. - """ - - f = _lanczos_error_equation_to_optimize_delta_t( - T, - krylov_basis=krylov_basis, - t0=tlist[0], - tf=tlist[-1], - target_tolerance=options['atol'], - ) - - # To avoid the singularity at t0, we add a small epsilon value - t_min = (tlist[-1] - tlist[0]) / options['nsteps'] + tlist[0] - bracket = [t_min, tlist[-1]] - - if (np.sign(f(bracket[0])) == -1) and (np.sign(f(bracket[-1])) == -1): - delta_t = tlist[-1] - tlist[0] - return delta_t - - elif (np.sign(f(bracket[0])) == 1) and (np.sign(f(bracket[-1])) == 1): - raise ValueError( - "No solution exists with the given combination of parameters 'krylov_dim', " - "tolerance = 'options['atol']', maximum number allowed of krylov internal " - "partitions = 'options['nsteps']' and 'tlist'. Try reducing the tolerance, or " - "increasing 'krylov_dim'. If nothing works, then a deeper analysis of the " - "problem is recommended." - ) - - else: - sol = root_scalar(f=f, bracket=bracket, method="brentq", xtol=options['atol']) - if sol.converged: - delta_t = sol.root - return delta_t - else: - raise Exception( - "Method did not converge, try increasing 'krylov_dim', " - "taking a lesser final time 'tlist[-1]' or decreasing the " - "tolerance via SolverOptions().atol. " - "If nothing works, this problem might not be suitable for " - "Krylov or a deeper analysis might be required." - ) - - -def _lanczos_error_equation_to_optimize_delta_t( - T, krylov_basis, t0, tf, target_tolerance -): - """ - Function to optimize in order to obtain the optimal number of - Lanczos algorithm iterations, governed by the optimal timestep size between - Lanczos iteractions. - """ - eigenvalues1, eigenvectors = eigs(_data.Dense(T[0:, 0:]), True) - eigenvectors1 = eigenvectors.to_array() - U1 = np.matmul(krylov_basis[0:, 0:].T, eigenvectors1) - e01 = eigenvectors1.conj().T[:, 0] - - eigenvalues2, eigenvectors = eigs(_data.Dense(T[0:-1, 0: T.shape[1] - 1]), True) - eigenvectors2 = eigenvectors.to_array() - U2 = np.matmul(krylov_basis[0:-1, :].T, eigenvectors2) - e02 = eigenvectors2.conj().T[:, 0] - - def f(t): - delta_t = -1j * (t - t0) - - aux1 = np.multiply(np.exp(delta_t * eigenvalues1), e01) - psi1 = np.matmul(U1, aux1) - - aux2 = np.multiply(np.exp(delta_t * eigenvalues2), e02) - psi2 = np.matmul(U2, aux2) - - error = np.linalg.norm(psi1 - psi2) - - steps = max(1, (tf - t0) // (t - t0)) - return np.log10(error) + np.log10(steps) - np.log10(target_tolerance) - - return f - - -def _make_partitions(tlist, n_timesteps): - """Generates an internal 'partitions' list of np.arrays to iterate Lanczos - algorithms on each of them, based on 'tlist' and the optimized number of - iterations 'n_timesteps'. - """ - - _tlist = np.copy(tlist) - - if n_timesteps == 1: - _tlist = np.insert(_tlist, 0, tlist[0]) - _tlist = np.append(_tlist, tlist[-1]) - partitions = [_tlist] - return partitions - - n_timesteps += 1 - krylov_tlist = np.linspace(tlist[0], tlist[-1], n_timesteps) - krylov_partitions = [ - np.array(krylov_tlist[i: i + 2]) for i in range(n_timesteps - 1) - ] - partitions = [] - for krylov_partition in krylov_partitions: - start = krylov_partition[0] - end = krylov_partition[-1] - condition = _tlist <= end - partitions.append([start] + _tlist[condition].tolist() + [end]) - _tlist = _tlist[~condition] - - return partitions - - -def _e_ops_outputs(krylov_results, e_ops, n_tlist_steps, opt): - krylov_results.expect = [] - if callable(e_ops): - n_expt_op = 0 - expt_callback = True - krylov_results.num_expect = 1 - elif isinstance(e_ops, list): - n_expt_op = len(e_ops) - expt_callback = False - krylov_results.num_expect = n_expt_op - if n_expt_op == 0: - # fall back on storing states - opt['store_states'] = True - else: - for op in e_ops: - if not isinstance(op, Qobj) and callable(op): - krylov_results.expect.append( - np.zeros(n_tlist_steps, dtype=complex) - ) - continue - if op.isherm: - krylov_results.expect.append(np.zeros(n_tlist_steps)) - else: - krylov_results.expect.append( - np.zeros(n_tlist_steps, dtype=complex) - ) - - else: - raise TypeError("Expectation parameter must be a list or a function") - - return krylov_results, expt_callback, opt, n_expt_op diff --git a/qutip/solve/mcsolve.py b/qutip/solve/mcsolve.py deleted file mode 100644 index 01fa52ba12..0000000000 --- a/qutip/solve/mcsolve.py +++ /dev/null @@ -1,704 +0,0 @@ -__all__ = ['mcsolve'] - -import warnings - -import numpy as np -from numpy.random import RandomState, randint -from scipy.integrate import ode -from scipy.integrate._ode import zvode - -from ..core import Qobj, QobjEvo -from ..core import data as _data -from ..solver.parallel import parallel_map, serial_map -from ._mcsolve import CyMcOde, CyMcOdeDiag -from .sesolve import sesolve -from .solver import SolverOptions, Result, ExpectOps, solver_safe, SolverSystem -from ..ui.progressbar import TextProgressBar, BaseProgressBar - - -# -# Internal, global variables for storing references to dynamically loaded -# cython functions -class qutip_zvode(zvode): - def step(self, *args): - itask = self.call_args[2] - self.rwork[0] = args[4] - self.call_args[2] = 5 - r = self.run(*args) - self.call_args[2] = itask - return r - - -def mcsolve(H, psi0, tlist, c_ops=None, e_ops=None, ntraj=0, - args=None, options=None, progress_bar=None, - map_func=parallel_map, map_kwargs=None, _safe_mode=True): - r"""Monte Carlo evolution of a state vector :math:`|\psi \rangle` for a - given Hamiltonian and sets of collapse operators, and possibly, operators - for calculating expectation values. Options for the underlying ODE solver - are given by the Options class. - - mcsolve supports time-dependent Hamiltonians and collapse operators using - either Python functions of strings to represent time-dependent - coefficients. Note that, the system Hamiltonian MUST have at least one - constant term. - - As an example of a time-dependent problem, consider a Hamiltonian with two - terms ``H0`` and ``H1``, where ``H1`` is time-dependent with coefficient - ``sin(w*t)``, and collapse operators ``C0`` and ``C1``, where ``C1`` is - time-dependent with coeffcient ``exp(-a*t)``. Here, w and a are constant - arguments with values ``W`` and ``A``. - - Using the Python function time-dependent format requires two Python - functions, one for each collapse coefficient. Therefore, this problem could - be expressed as:: - - def H1_coeff(t,args): - return sin(args['w']*t) - - def C1_coeff(t,args): - return exp(-args['a']*t) - - H = [H0, [H1, H1_coeff]] - - c_ops = [C0, [C1, C1_coeff]] - - args={'a': A, 'w': W} - - or in String (Cython) format we could write:: - - H = [H0, [H1, 'sin(w*t)']] - - c_ops = [C0, [C1, 'exp(-a*t)']] - - args={'a': A, 'w': W} - - Constant terms are preferably placed first in the Hamiltonian and collapse - operator lists. - - Parameters - ---------- - H : :class:`qutip.Qobj`, ``list`` - System Hamiltonian. - - psi0 : :class:`qutip.Qobj` - Initial state vector - - tlist : array_like - Times at which results are recorded. - - ntraj : int - Number of trajectories to run. - - c_ops : :class:`qutip.Qobj`, ``list`` - single collapse operator or a ``list`` of collapse operators. - - e_ops : :class:`qutip.Qobj`, ``list`` - single operator as Qobj or ``list`` or equivalent of Qobj operators - for calculating expectation values. - - args : dict - Arguments for time-dependent Hamiltonian and collapse operator terms. - - options : SolverOptions - Instance of ODE solver options. - - progress_bar: BaseProgressBar - Optional instance of BaseProgressBar, or a subclass thereof, for - showing the progress of the simulation. Set to None to disable the - progress bar. - - map_func: function - A map function for managing the calls to the single-trajactory solver. - - map_kwargs: dictionary - Optional keyword arguments to the map_func function. - - Returns - ------- - results : :class:`qutip.solver.Result` - Object storing all results from the simulation. - - .. note:: - - It is possible to reuse the random number seeds from a previous run - of the mcsolver by passing the output Result object seeds via the - Options class, i.e. SolverOptions(seeds=prev_result.seeds). - """ - args = args or {} - map_kwargs = map_kwargs or {} - c_ops = [c_ops] if isinstance(c_ops, (Qobj, QobjEvo)) else (c_ops or []) - e_ops = [e_ops] if isinstance(e_ops, (Qobj, QobjEvo)) else (e_ops or []) - options = options if options is not None else SolverOptions() - if False and not isinstance(H, SolverSystem): - # TODO: deprecate when going to class based solver. - H = solver_safe.get("mcsolve", H) - ntraj = ntraj or options['ntraj'] - try: - num_traj = int(ntraj) - except TypeError: - num_traj = max(ntraj) - - # set the physics - if not psi0.isket: - raise ValueError("Initial state must be a state vector.") - - if len(c_ops) == 0: - warnings.warn("No c_ops, using sesolve") - return sesolve(H, psi0, tlist, e_ops=e_ops, args=args, - options=options, progress_bar=progress_bar, - _safe_mode=_safe_mode) - - # load monte carlo class - mc = _MC(options) - - if isinstance(H, SolverSystem): - mc.ss = H - else: - mc.make_system(H, c_ops, tlist, args, options) - - mc.reset(tlist[0], psi0) - - mc.set_e_ops(e_ops) - - if _safe_mode: - mc.run_test() - - # Run the simulation - mc.run(num_traj=num_traj, tlist=tlist, - progress_bar=progress_bar, - map_func=map_func, map_kwargs=map_kwargs) - - return mc.get_result(ntraj) - - -# ----------------------------------------------------------------------------- -# MONTE CARLO CLASS -# ----------------------------------------------------------------------------- -class _MC(): - """ - Private class for solving Monte Carlo evolution from mcsolve - """ - def __init__(self, options=None): - if options is None: - options = SolverOptions() - self.options = options - self.ss = None - self.tlist = None - self.e_ops = None - self.ran = False - self.psi0 = None - self.seeds = [] - self.t = 0. - self.num_traj = 0 - self.args_col = None - - self._psi_out = [] - self._expect_out = [] - self._collapse = [] - self._ss_out = [] - - def reset(self, t=0., psi0=None): - if psi0 is not None: - self.psi0 = psi0 - if self.psi0 is not None: - self.initial_vector = self.psi0.full().ravel("F") - if self.ss is not None and self.ss.type == "Diagonal": - self.initial_vector = np.dot(self.ss.Ud, self.initial_vector) - - self.t = t - self.ran = False - self._psi_out = [] - self._expect_out = [] - self._collapse = [] - self._ss_out = [] - - def seed(self, ntraj, seeds=[]): - # setup seeds array - np.random.seed() - try: - seed = int(seeds) - np.random.seed(seed) - seeds = [] - except TypeError: - pass - - if len(seeds) < ntraj: - self.seeds = seeds + list(randint(0, 2**32, - size=ntraj-len(seeds), - dtype=np.uint32)) - else: - self.seeds = seeds[:ntraj] - - def make_system(self, H, c_ops, tlist=None, args={}, options=None): - if options is None: - options = self.options - else: - self.options = options - var = _collapse_args(args) - - ss = SolverSystem() - ss.td_c_ops = [] - ss.td_n_ops = [] - ss.args = args - ss.col_args = var - for c in c_ops: - cevo = QobjEvo(c, args, tlist=tlist) - cdc = cevo.dag() @ cevo - ss.td_c_ops.append(cevo) - ss.td_n_ops.append(cdc) - - H_td = QobjEvo(H, args, tlist=tlist) - H_td *= -1j - for c in ss.td_n_ops: - H_td += -0.5 * c - ss.H_td = H_td - ss.makefunc = _qobjevo_set - ss.type = "QobjEvo" - - solver_safe["mcsolve"] = ss - self.ss = ss - self.reset() - - def set_e_ops(self, e_ops=[]): - if e_ops: - self.e_ops = ExpectOps(e_ops) - else: - self.e_ops = ExpectOps([]) - - self.e_ops.check_dims(self.ss.td_c_ops[0].dims) - - ss = self.ss - if ss is not None and ss.type == "Diagonal" and not self.e_ops.isfunc: - e_ops = [ - Qobj(ss.Ud @ e.full() @ ss.U, dims=e.dims) - for e in self.e_ops.e_ops - ] - self.e_ops = ExpectOps(e_ops) - - if not self.e_ops: - self.options['store_states'] = True - - def run_test(self): - try: - for c_op in self.ss.td_c_ops: - c_op.matmul(0, self.psi0) - except: - raise Exception("c_ops are not consistant with psi0") - - if self.ss.type == "QobjEvo": - try: - self.ss.H_td.matmul(0., self.psi0) - except: - raise Exception("Error calculating H") - else: - try: - rhs, ode_args = self.ss.makefunc(self.ss) - rhs(0, self.psi0.full().ravel(), ode_args) - except Exception as e: - raise Exception("Error calculating H") from e - - def run(self, num_traj=0, psi0=None, tlist=None, - args={}, e_ops=None, options=None, - progress_bar=True, - map_func=parallel_map, map_kwargs={}): - # %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - # 4 situation for run: - # - first run - # - change parameters - # - add trajectories - # (self.add_traj) Not Implemented - # - continue from the last time and states - # (self.continue_runs) Not Implemented - # %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - options = options if options is not None else self.options - - if self.ran and tlist[0] == self.t: - # psi0 is ignored since we restart from a - # different states for each trajectories - self.continue_runs(num_traj, tlist, args, e_ops, options, - progress_bar, map_func, map_kwargs) - return - - if e_ops and e_ops != self.e_ops: - self.set_e_ops(e_ops) - self.reset() - - if psi0 is not None and psi0 != self.psi0: - self.psi0 = psi0 - self.reset() - - tlist = np.array(tlist) - if tlist is not None and np.all(tlist != self.tlist): - self.tlist = tlist - self.reset() - - if self.ran: - if options['store_states'] and self._psi_out[0].shape[0] == 1: - self.reset() - else: - # if not reset here, add trajectories - self.add_traj(num_traj, progress_bar, map_func, map_kwargs) - return - - if not num_traj: - num_traj = options['ntraj'] - - if num_traj == 1: - map_func = serial_map - - if len(self.seeds) != num_traj: - self.seed(num_traj, self.seeds) - - if progress_bar is None: - progress_bar = "" - elif progress_bar is True: - progress_bar = "text" - - # set arguments for input to monte carlo - map_kwargs_ = {'progress_bar': progress_bar} - # 'num_cpus': options.num_cpus} - map_kwargs_.update(map_kwargs) - map_kwargs = map_kwargs_ - - if self.e_ops is None: - self.set_e_ops() - - if self.ss.type == "Diagonal": - results = map_func(self._single_traj_diag, list(range(num_traj)), - **map_kwargs) - else: - results = map_func(self._single_traj, list(range(num_traj)), - **map_kwargs) - - self.t = self.tlist[-1] - self.num_traj = num_traj - self.ran = True - - for result in results: - state_out, ss_out, expect, collapse = result - self._psi_out.append(state_out) - self._ss_out.append(ss_out) - self._expect_out.append(expect) - self._collapse.append(collapse) - self._psi_out = np.stack(self._psi_out) - self._ss_out = np.stack(self._ss_out) - - def add_traj(self, num_traj, - progress_bar=True, - map_func=parallel_map, map_kwargs={}): - raise NotImplementedError - - def continue_runs(self, num_traj, tlist, args={}, e_ops=[], options=None, - progress_bar=True, - map_func=parallel_map, map_kwargs={}): - raise NotImplementedError - - # -------------------------------------------------------------------------- - # results functions - # -------------------------------------------------------------------------- - @property - def states(self): - dims = self.psi0.dims[0] - len_ = self._psi_out.shape[2] - if self._psi_out.shape[1] == 1: - dm_t = np.zeros((len_, len_), dtype=complex) - for i in range(self.num_traj): - vec = self._psi_out[i, 0] - dm_t += np.outer(vec, vec.conj()) - return Qobj(dm_t/self.num_traj, dims=[dims, dims]) - else: - states = np.empty((len(self.tlist)), dtype=object) - for j in range(len(self.tlist)): - dm_t = np.zeros((len_, len_), dtype=complex) - for i in range(self.num_traj): - vec = self._psi_out[i, j] - dm_t += np.outer(vec, vec.conj()) - states[j] = Qobj(dm_t/self.num_traj, dims=[dims, dims], - type='oper') - return states - - @property - def final_state(self): - dims = self.psi0.dims[0] - len_ = self._psi_out.shape[2] - dm_t = np.zeros((len_, len_), dtype=complex) - for i in range(self.num_traj): - vec = self._psi_out[i, -1] - dm_t += np.outer(vec, vec.conj()) - return Qobj(dm_t/self.num_traj, dims=[dims, dims], type='oper') - - @property - def runs_final_states(self): - dims = self.psi0.dims[0] - psis = np.empty((self.num_traj), dtype=object) - for i in range(self.num_traj): - psis[i] = Qobj(self._psi_out[i, -1, :], dims=dims) - return psis - - @property - def expect(self): - return self.expect_traj_avg() - - @property - def runs_expect(self): - return [expt.finish() for expt in self._expect_out] - - def expect_traj_avg(self, ntraj=0): - if not ntraj: - ntraj = len(self._expect_out) - expect = np.stack([expt.raw_out for expt in self._expect_out[:ntraj]]) - expect = np.mean(expect, axis=0) - - result = [] - for ii in range(self.e_ops.e_num): - if self.e_ops.e_ops_isherm[ii]: - result.append(np.real(expect[ii, :])) - else: - result.append(expect[ii, :]) - - if self.e_ops.e_ops_dict: - result = {e: result[n] - for n, e in enumerate(self.e_ops.e_ops_dict.keys())} - return result - - @property - def steady_state(self): - if self._ss_out is not None: - dims = self.psi0.dims[0] - len_ = self.psi0.shape[0] - return Qobj(np.mean(self._ss_out, axis=0), - dims=[dims, dims], shape=(len_, len_)) - # TO-DO rebuild steady_state from _psi_out if needed - # elif self._psi_out is not None: - # return sum(self.state_average) / self.num_traj - else: - return None - - @property - def runs_states(self): - dims = self.psi0.dims - psis = np.empty((self.num_traj, len(self.tlist)), dtype=object) - for i in range(self.num_traj): - for j in range(len(self.tlist)): - psis[i, j] = Qobj(self._psi_out[i, j], dims=dims, type='ket') - return psis - - @property - def collapse(self): - return self._collapse - - @property - def collapse_times(self): - out = [] - for col_ in self._collapse: - col = list(zip(*col_)) - col = ([] if len(col) == 0 else col[0]) - out.append(np.array(col)) - return out - - @property - def collapse_which(self): - out = [] - for col_ in self._collapse: - col = list(zip(*col_)) - col = ([] if len(col) == 0 else col[1]) - out.append(np.array(col)) - return out - - def get_result(self, ntraj=None): - # Store results in the Result object - if not ntraj: - ntraj = [self.num_traj] - elif not isinstance(ntraj, list): - ntraj = [ntraj] - - output = Result() - output.solver = 'mcsolve' - output.seeds = self.seeds - - options = self.options - output.options = options - - if options['steady_state_average']: - output.states = self.steady_state - elif options['average_states'] and options['store_states']: - output.states = self.states - elif options['store_states']: - output.states = self.runs_states - - if options['store_final_state']: - if options['average_states']: - output.final_state = self.final_state - else: - output.final_state = self.runs_final_states - - if options['average_expect']: - output.expect = [self.expect_traj_avg(n) for n in ntraj] - if len(output.expect) == 1: - output.expect = output.expect[0] - else: - output.expect = self.runs_expect - - # simulation parameters - output.times = self.tlist - output.num_expect = self.e_ops.e_num - output.num_collapse = len(self.ss.td_c_ops) - output.ntraj = self.num_traj - output.col_times = self.collapse_times - output.col_which = self.collapse_which - - return output - - # -------------------------------------------------------------------------- - # single-trajectory for monte carlo - # -------------------------------------------------------------------------- - def _single_traj(self, nt): - """ - Monte Carlo algorithm returning state-vector or expectation values - at times tlist for a single trajectory. - """ - # SEED AND RNG AND GENERATE - prng = RandomState(self.seeds[nt]) - opt = self.options - - # set initial conditions - ss = self.ss - tlist = self.tlist - e_ops = self.e_ops.copy() - opt = self.options - rhs, ode_args = self.ss.makefunc(ss) - ODE = self._build_integration_func(rhs, ode_args, opt) - ODE.set_initial_value(self.initial_vector, tlist[0]) - e_ops.init(tlist) - - cymc = CyMcOde(ss, opt) - states_out, ss_out, collapses = cymc.run_ode(ODE, tlist, e_ops, prng) - - # Run at end of mc_alg function - # ----------------------------- - if opt['steady_state_average']: - ss_out /= float(len(tlist)) - - return (states_out, ss_out, e_ops, collapses) - - def _build_integration_func(self, rhs, ode_args, opt): - """ - Create the integration function while fixing the parameters - """ - ODE = ode(rhs) - if ode_args: - ODE.set_f_params(ode_args) - # initialize ODE solver for RHS - ODE.set_integrator('zvode', method="adams") - ODE._integrator = qutip_zvode( - method=opt['method'], order=opt['order'], atol=opt['atol'], - rtol=opt['rtol'], nsteps=opt['nsteps'], - first_step=opt['first_step'], - min_step=opt['min_step'], max_step=opt['max_step']) - return ODE - - # -------------------------------------------------------------------------- - # In development diagonalize the Hamiltonian before solving - # Same seeds give same evolution - # 3~5 time faster - # constant system only. - # -------------------------------------------------------------------------- - def make_diag_system(self, H, c_ops): - ss = SolverSystem() - ss.td_c_ops = [] - ss.td_n_ops = [] - - H_ = H.copy() - H_ *= -1j - for c in c_ops: - H_ += -0.5 * c.dag() @ c - - w, v = np.linalg.eig(H_.full()) - arg = np.argsort(np.abs(w)) - eig = w[arg] - U = v.T[arg].T - Ud = U.T.conj() - - for c in c_ops: - c_diag = Qobj(Ud @ c.full() @ U, dims=c.dims) - cevo = QobjEvo(c_diag) - cdc = cevo @ cevo.dag() - ss.td_c_ops.append(cevo) - ss.td_n_ops.append(cdc) - - ss.H_diag = eig - ss.Ud = Ud - ss.U = U - ss.args = {} - ss.type = "Diagonal" - solver_safe["mcsolve"] = ss - - if self.e_ops and not self.e_ops.isfunc: - e_ops = [ - Qobj(Ud @ e.full() @ U, dims=e.dims) - for e in self.e_ops.e_ops - ] - self.e_ops = ExpectOps(e_ops) - self.ss = ss - self.reset() - - def _single_traj_diag(self, nt): - """ - Monte Carlo algorithm returning state-vector or expectation values - at times tlist for a single trajectory. - """ - # SEED AND RNG AND GENERATE - prng = RandomState(self.seeds[nt]) - opt = self.options - - ss = self.ss - tlist = self.tlist - e_ops = self.e_ops.copy() - opt = self.options - e_ops.init(tlist) - - cymc = CyMcOdeDiag(ss, opt) - states_out, ss_out, collapses =\ - cymc.run_ode(self.initial_vector, tlist, e_ops, prng) - - if opt['steady_state_average']: - ss_out = ss.U @ ss_out @ ss.Ud - states_out = np.inner(ss.U, states_out).T - if opt['steady_state_average']: - ss_out /= float(len(tlist)) - return (states_out, ss_out, e_ops, collapses) - - -# ----------------------------------------------------------------------------- -# CODES FOR PYTHON FUNCTION BASED TIME-DEPENDENT RHS -# ----------------------------------------------------------------------------- -def _wrap_matmul(t, state, extras): - # Unlike mesolve and sesolve, the arguments here are generally passed in - # through a parallel mapping function, which does not unwrap additional - # tuple arguments. We have to do that here. - cqobj, = extras - return cqobj.matmul_data(t, _data.dense.fast_from_numpy(state)).as_ndarray() - - -def _qobjevo_set(ss, psi0=None, args={}, opt=None): - if args: - self.set_args(args) - return _wrap_matmul, (ss.H_td,) - - -def _mc_dm_avg(psi_list): - """ - Private function that averages density matrices in parallel - over all trajectories for a single time using parfor. - """ - ln = len(psi_list) - return sum(psi_list) / ln - - -def _collapse_args(args): - for key in args: - if key == "collapse": - if not isinstance(args[key], list): - args[key] = [] - return key - return "" diff --git a/qutip/solve/mesolve.py b/qutip/solve/mesolve.py deleted file mode 100644 index cc95e0816a..0000000000 --- a/qutip/solve/mesolve.py +++ /dev/null @@ -1,549 +0,0 @@ -""" -This module provides solvers for the Lindblad master equation and von Neumann -equation. -""" - -__all__ = ['mesolve'] - -import numpy as np -import scipy.integrate -from .. import ( - Qobj, QobjEvo, isket, issuper, spre, spost, liouvillian, - lindblad_dissipator, ket2dm, -) -from ..core import data as _data -from .solver import SolverOptions, Result, solver_safe, SolverSystem -from .sesolve import sesolve -from ..ui.progressbar import BaseProgressBar, TextProgressBar - - -# ----------------------------------------------------------------------------- -# pass on to wavefunction solver or master equation solver depending on whether -# any collapse operators were given. -# -def mesolve(H, rho0, tlist, c_ops=None, e_ops=None, args=None, options=None, - progress_bar=None, _safe_mode=True): - """ - 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 - 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 - does not use any return values. - - 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 specified in a - nested-list format where each element in the list is a list of length 2, - containing an operator (:class:`qutip.qobj`) at the first element and where - the second element is either a string (*list string format*), a callback - function (*list callback format*) that evaluates to the time-dependent - coefficient for the corresponding operator, or a NumPy array (*list - array format*) which specifies the value of the coefficient to the - corresponding operator for each value of t in `tlist`. - - Alternatively, `H` (but not `c_ops`) can be a callback function with the - signature `f(t, args) -> Qobj` (*callback format*), which can return the - Hamiltonian or Liouvillian superoperator at any point in time. If the - equation cannot be put in standard Lindblad form, then this time-dependence - format must be used. - - *Examples* - - H = [[H0, 'sin(w*t)'], [H1, 'sin(2*w*t)']] - - H = [[H0, f0_t], [H1, f1_t]] - - where f0_t and f1_t are python functions with signature f_t(t, args). - - H = [[H0, np.sin(w*tlist)], [H1, np.sin(2*w*tlist)]] - - In the *list string format* and *list callback format*, the string - expression and the callback function must evaluate to a real or complex - number (coefficient for the corresponding operator). - - In all cases of time-dependent operators, `args` is a dictionary of - parameters that is used when evaluating operators. It is passed to the - callback functions as their second argument. - - **Additional options** - - Additional options to mesolve can be set via the `options` argument, which - should be an instance of :class:`qutip.solver.SolverOptions`. 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:: - - If an element in the list-specification of the Hamiltonian or - the list of collapse operators are in superoperator form it will be - added to the total Liouvillian of the problem without further - transformation. This allows for using mesolve for solving master - equations that are not in standard Lindblad form. - - .. note:: - - On using callback functions: mesolve transforms all :class:`qutip.Qobj` - objects to sparse matrices before handing the problem to the integrator - function. In order for your callback function to work correctly, pass - all :class:`qutip.Qobj` objects that are used in constructing the - Hamiltonian via `args`. mesolve will check for :class:`qutip.Qobj` in - `args` and handle the conversion to sparse matrices. All other - :class:`qutip.Qobj` objects that are not passed via `args` will be - passed on to the integrator in scipy which will raise a NotImplemented - exception. - - Parameters - ---------- - - H : :class:`qutip.Qobj` - System Hamiltonian, or a callback function for time-dependent - Hamiltonians, or alternatively a system Liouvillian. - - rho0 : :class:`qutip.Qobj` - initial density matrix or state vector (ket). - - tlist : *list* / *array* - list of times for :math:`t`. - - c_ops : None / list of :class:`qutip.Qobj` - single collapse operator, or list of collapse operators, or a list - of Liouvillian superoperators. - - e_ops : None / list / callback function, optional - A list of operators as `Qobj` and/or callable functions (can be mixed) - or a single callable function. For operators, the result's expect will - be computed by :func:`qutip.expect`. For callable functions, they are - called as ``f(t, state)`` and return the expectation value. - A single callback's expectation value can be any type, but a callback - part of a list must return a number as the expectation value. - - args : None / *dictionary* - dictionary of parameters for time-dependent Hamiltonians and - collapse operators. - - options : None / :class:`qutip.SolverOptions` - with options for the solver. - - progress_bar : None / BaseProgressBar - Optional instance of BaseProgressBar, or a subclass thereof, for - showing the progress of the simulation. - - Returns - ------- - result: :class:`qutip.Result` - - An instance of the class :class:`qutip.Result`, which contains - either an *array* `result.expect` of expectation values for the times - specified by `tlist`, or an *array* `result.states` of state vectors or - density matrices corresponding to the times in `tlist` [if `e_ops` is - an empty list], or nothing if a callback function was given in place of - operators for which to calculate the expectation values. - - """ - if c_ops is None: - c_ops = [] - if isinstance(c_ops, (Qobj, QobjEvo)): - c_ops = [c_ops] - - if e_ops is None: - e_ops = [] - if isinstance(e_ops, Qobj): - e_ops = [e_ops] - - if isinstance(e_ops, dict): - e_ops_dict = e_ops - e_ops = [e for e in e_ops.values()] - else: - e_ops_dict = None - - if progress_bar is None: - progress_bar = BaseProgressBar() - if progress_bar is True: - progress_bar = TextProgressBar() - - # check if rho0 is a superoperator, in which case e_ops argument should - # be empty, i.e., e_ops = [] - # TODO: e_ops for superoperator - if rho0.issuper and not e_ops == []: - raise TypeError("Must have e_ops = [] when initial condition rho0 is" - " a superoperator.") - - if options is None: - options = SolverOptions() - if False and not isinstance(H, SolverSystem): - # TODO: deprecate when going to class based solver. - if "mesolve" in solver_safe: - # print(" ") - H = solver_safe["mesolve"] - else: - pass - # raise Exception("Could not find the Hamiltonian to reuse.") - - if args is None: - args = {} - - use_mesolve = ( - (c_ops and len(c_ops) > 0) - or (not rho0.isket) - or (isinstance(H, Qobj) and H.issuper) - or (isinstance(H, QobjEvo) and H.issuper) - or (isinstance(H, list) and isinstance(H[0], Qobj) and H[0].issuper) - or (not isinstance(H, (Qobj, QobjEvo)) - and callable(H) - and H(0., args).issuper) - or (not isinstance(H, (Qobj, QobjEvo)) - and callable(H)) - ) - - if not use_mesolve: - return sesolve(H, rho0, tlist, e_ops=e_ops, args=args, options=options, - progress_bar=progress_bar, _safe_mode=_safe_mode) - - if isket(rho0): - rho0 = ket2dm(rho0) - if (not (rho0.isoper or rho0.issuper)) or (rho0.dims[0] != rho0.dims[1]): - raise ValueError( - "input state must be a pure state vector, square density matrix, " - "or superoperator" - ) - - if isinstance(H, SolverSystem): - ss = H - else: - H = QobjEvo(H, args=args, tlist=tlist) - ss = _mesolve_QobjEvo(H, c_ops, tlist, args, options) - """ - elif isinstance(H, (list, Qobj, QobjEvo)): - ss = _mesolve_QobjEvo(H, c_ops, tlist, args, options) - elif callable(H): - ss = _mesolve_func_td(H, c_ops, rho0, tlist, args, options) - else: - raise Exception("Invalid H type") - """ - - func, ode_args = ss.makefunc(ss, rho0, args, e_ops, options) - - if _safe_mode: - # This is to test safety of the function before starting the loop. - v = rho0.full().ravel('F') - func(0., v, *ode_args)[:, 0] + v - - res = _generic_ode_solve(func, ode_args, rho0, tlist, e_ops, options, - progress_bar, dims=rho0.dims) - res.num_collapse = len(c_ops) - - if e_ops_dict: - res.expect = {e: res.expect[n] - for n, e in enumerate(e_ops_dict.keys())} - - return res - - -# ----------------------------------------------------------------------------- -# A time-dependent unitary wavefunction equation on the list-function format -# _mesolve_QobjEvo(H, c_ops, tlist, args, options) -def _mesolve_QobjEvo(H, c_ops, tlist, args, opt): - """ - Prepare the system for the solver, H can be an QobjEvo. - """ - H_td = QobjEvo(H, args, tlist=tlist) - if not H_td.issuper: - L_td = liouvillian(H_td) - else: - L_td = H_td - for op in c_ops: - op_td = QobjEvo(op, args, tlist=tlist) - if not op_td.issuper: - op_td = lindblad_dissipator(op_td) - L_td += op_td - - # if opt.rhs_with_state: - # L_td._check_old_with_state() - - ss = SolverSystem() - ss.H = L_td - ss.makefunc = _qobjevo_set - solver_safe["mesolve"] = ss - return ss - - -def _test_liouvillian_dimensions(L_dims, rho_dims): - """ - Raise ValueError if the dimensions of the Liouvillian and the density - matrix or superoperator state are incompatible with the master equation. - """ - if L_dims[0] != L_dims[1]: - raise ValueError("Liouvillian had nonsquare dims: " + str(L_dims)) - if not ((L_dims[1] == rho_dims) or (L_dims[1] == rho_dims[0])): - raise ValueError("".join([ - "incompatible Liouvillian and state dimensions: ", - str(L_dims), " and ", str(rho_dims), - ])) - - -def _wrap_matmul(t, state, cqobj, unstack): - data = _data.dense.fast_from_numpy(state) - if unstack: - data = _data.column_unstack_dense(data, cqobj.shape[1], inplace=True) - out = cqobj.matmul_data(t, data) - if unstack: - out = _data.column_stack_dense(out, inplace=True) - return out.as_ndarray() - - -def _qobjevo_set(HS, rho0, args, e_ops, opt): - """ - From the system, get the ode function and args - """ - H_td = HS.H - H_td.arguments(args) - if not (rho0.issuper or rho0.isoper or rho0.isket): - raise TypeError("The unitary solver requires rho0 to be" - " a ket or dm as initial state" - " or a super operator as initial state.") - _test_liouvillian_dimensions(H_td.dims, rho0.dims) - return _wrap_matmul, (H_td, rho0.issuper) - - -# ----------------------------------------------------------------------------- -# Master equation solver for python-function time-dependence. -# -class _LiouvillianFromFunc: - def __init__(self, func, c_ops, rho_dims): - self.f = func - self.c_ops = c_ops - self.rho_dims = rho_dims - - def H2L(self, t, rho, args): - Ht = self.f(t, args) - Lt = -1.0j * (spre(Ht) - spost(Ht)) - _test_liouvillian_dimensions(Lt.dims, self.rho_dims) - for op in self.c_ops: - Lt += op(t) - return Lt.data - - def H2L_with_state(self, t, rho, args): - Ht = self.f(t, rho, args) - Lt = -1.0j * (spre(Ht) - spost(Ht)) - _test_liouvillian_dimensions(Lt.dims, self.rho_dims) - for op in self.c_ops: - Lt += op(t) - return Lt.data - - def L(self, t, rho, args): - Lt = self.f(t, args) - _test_liouvillian_dimensions(Lt.dims, self.rho_dims) - for op in self.c_ops: - Lt += op(t) - return Lt.data - - def L_with_state(self, t, rho, args): - Lt = self.f(t, rho, args) - _test_liouvillian_dimensions(Lt.dims, self.rho_dims) - for op in self.c_ops: - Lt += op(t) - return Lt.data - - -def _mesolve_func_td(L_func, c_op_list, rho0, tlist, args, opt): - """ - Evolve the density matrix using an ODE solver with time dependent - Hamiltonian. - """ - c_ops = [] - for op in c_op_list: - op_td = QobjEvo(op, args, tlist=tlist, copy=False) - if not op_td.issuper: - c_ops += [lindblad_dissipator(op_td)] - else: - c_ops += [op_td] - if c_op_list: - c_ops_ = [sum(c_ops)] - else: - c_ops_ = [] - - if False: # TODO check: old version was `opt.rhs_with_state` - state0 = rho0.full().ravel("F") - obj = L_func(0., state0, args) - if not issuper(obj): - L_func = _LiouvillianFromFunc(L_func, c_ops_).H2L_with_state - else: - L_func = _LiouvillianFromFunc(L_func, c_ops_).L_with_state - else: - obj = L_func(0., args) - L_func = L_api.L if issuper(obj) else L_api.H2L - ss = SolverSystem() - ss.L = L_func - ss.makefunc = _Lfunc_set - solver_safe["mesolve"] = ss - return ss - - -def _Lfunc_set(HS, rho0, args, e_ops, opt): - """ - From the system, get the ode function and args - """ - L_func = HS.L - if issuper(rho0): - func = _ode_super_func_td - else: - func = _ode_rho_func_td - - return func, (L_func, args) - - -def _ode_rho_func_td(t, y, L_func, args): - L = L_func(t, y, args) - data = _data.dense.fast_from_numpy(y) - return _data.matmul(L, data, dtype=_data.Dense).as_ndarray() - - -def _ode_super_func_td(t, y, L_func, args): - L = L_func(t, y, args) - data = _data.column_unstack_dense(_data.dense.fast_from_numpy(y), - L.shape[1], - inplace=True) - matmul = _data.matmul(L, data, dtype=_data.Dense) - return _data.column_stack_dense(matmul, inplace=True).as_ndarray() - -# ----------------------------------------------------------------------------- -# Generic ODE solver: shared code among the various ODE solver -# ----------------------------------------------------------------------------- - -def _generic_ode_solve(func, ode_args, rho0, tlist, e_ops, opt, - progress_bar, dims=None): - """ - Internal function for solving ME. - Calculate the required expectation values or invoke - callback function at each time step. - """ - # %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - # This function is made similar to sesolve's one for futur merging in a - # solver class - # %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - - # prepare output array - n_tsteps = len(tlist) - output = Result() - output.solver = "mesolve" - output.times = tlist - size = rho0.shape[0] - - initial_vector = rho0.full().ravel('F') - - r = scipy.integrate.ode(func) - r.set_integrator('zvode', method=opt['method'], order=opt['order'], - atol=opt['atol'], rtol=opt['rtol'], nsteps=opt['nsteps'], - first_step=opt['first_step'], min_step=opt['min_step'], - max_step=opt['max_step']) - if ode_args: - r.set_f_params(*ode_args) - r.set_initial_value(initial_vector, tlist[0]) - - e_ops_data = [] - output.expect = [] - if callable(e_ops): - n_expt_op = 0 - expt_callback = True - output.num_expect = 1 - elif isinstance(e_ops, list): - n_expt_op = len(e_ops) - expt_callback = False - output.num_expect = n_expt_op - if n_expt_op == 0: - # fall back on storing states - opt['store_states'] = True - else: - for op in e_ops: - if not isinstance(op, Qobj) and callable(op): - output.expect.append(np.zeros(n_tsteps, dtype=complex)) - continue - if op.dims != rho0.dims: - raise TypeError(f"e_ops dims ({op.dims}) are not " - f"compatible with the state's " - f"({rho0.dims})") - e_ops_data.append(spre(op).data) - dtype = (np.float64 if op.isherm and rho0.isherm - else np.complex128) - output.expect.append(np.zeros(n_tsteps, dtype=dtype)) - else: - raise TypeError("Expectation parameter must be a list or a function") - - if opt['store_states']: - output.states = [] - - def get_curr_state_data(r): - return _data.dense.fast_from_numpy(r.y) - - # - # start evolution - # - dt = np.diff(tlist) - cdata = None - progress_bar.start(n_tsteps) - for t_idx, t in enumerate(tlist): - progress_bar.update(t_idx) - - if not r.successful(): - raise Exception("ODE integration error: Try to increase " - "the allowed number of substeps by increasing " - "the nsteps parameter in the Options class.") - - if opt['store_states'] or expt_callback or n_expt_op: - cdata = get_curr_state_data(r) - - if opt['store_states']: - # Unstacking with a copying operation keeps us safe if a later call - # unstacks the columns again. - if cdata.shape[0] != size: - cdata = _data.column_unstack_dense(cdata, size, inplace=False) - output.states.append(Qobj(cdata, - dims=dims, type=rho0.type, copy=False)) - - if expt_callback: - # use callback method - if cdata.shape[0] != size: - cdata = _data.column_unstack_dense(cdata, size, inplace=False) - output.expect.append(e_ops(t, Qobj(cdata, - dims=dims, type=rho0.type, - copy=False))) - - for m in range(n_expt_op): - if cdata.shape[1] == size: - cdata = _data.column_stack_dense(cdata, inplace=False) - if not isinstance(e_ops[m], Qobj) and callable(e_ops[m]): - output.expect[m][t_idx] = e_ops[m](t, Qobj(cdata, dims=dims)) - else: - val = _data.expect_super(e_ops_data[m], cdata) - if e_ops[m].isherm and rho0.isherm: - val = val.real - output.expect[m][t_idx] = val - - if t_idx < n_tsteps - 1: - r.integrate(r.t + dt[t_idx]) - - progress_bar.finished() - - if opt['store_final_state']: - cdata = get_curr_state_data(r) - matrix = _data.column_unstack_dense(cdata, size) - output.final_state = Qobj(matrix, - dims=dims, type=rho0.type, - isherm=rho0.isherm or None, copy=False) - - return output diff --git a/qutip/solve/nonmarkov/memorycascade.py b/qutip/solve/nonmarkov/memorycascade.py index b0b24ad714..35996d21cd 100644 --- a/qutip/solve/nonmarkov/memorycascade.py +++ b/qutip/solve/nonmarkov/memorycascade.py @@ -53,7 +53,7 @@ class MemoryCascade: Run integrator in parallel if True. Only implemented for 'propagator' as the integrator method. - options : :class:`qutip.solver.SolverOptions` + options : dict Generic solver options. """ @@ -61,7 +61,7 @@ def __init__(self, H_S, L1, L2, S_matrix=None, c_ops_markov=None, integrator='propagator', parallel=False, options=None): if options is None: - self.options = qt.SolverOptions() + self.options = {} else: self.options = options @@ -383,10 +383,11 @@ def _generator(k, H, L1, L2, S=None, c_ops_markov=None): def _integrate(L, E0, ti, tf, integrator='propagator', parallel=False, - opt=qt.SolverOptions()): + opt=None): """ Basic ode integrator """ + opt = opt or {} if tf > ti: if integrator == 'mesolve': if parallel: diff --git a/qutip/solve/nonmarkov/transfertensor.py b/qutip/solve/nonmarkov/transfertensor.py index 55ba123b18..561b7554e1 100644 --- a/qutip/solve/nonmarkov/transfertensor.py +++ b/qutip/solve/nonmarkov/transfertensor.py @@ -45,7 +45,6 @@ class TTMSolverOptions: def __init__(self, dynmaps=None, times=[], learningtimes=[], thres=0.0, options=None): - if options is None: options = SolverOptions() @@ -89,7 +88,7 @@ def ttmsolve(dynmaps, rho0, times, e_ops=[], learningtimes=None, tensors=None, kwargs : dictionary Optional keyword arguments. See - :class:`qutip.nonmarkov.ttm.TTMSolverOptions`. + :class:`qutip.nonmarkov.transfertensor.TTMSolverOptions`. Returns ------- @@ -147,10 +146,13 @@ def ttmsolve(dynmaps, rho0, times, e_ops=[], learningtimes=None, tensors=None, K = len(tensors) states = [rho0vec] for n in range(1, len(times)): - states.append(None) - for k in range(n): - if n-k < K: - states[-1] += tensors[n-k]*states[k] + # Set current state + state = None + for j in range(1, min(K, n + 1)): + tmp = tensors[j] * states[n - j] + state = tmp if state is None else tmp + state + # Append state to all states + states.append(state) for i, r in enumerate(states): if opt.store_states or expt_callback: if r.type == 'operator-ket': @@ -199,7 +201,7 @@ def _generatetensors(dynmaps, learningtimes=None, **kwargs): kwargs : dictionary Optional keyword arguments. See - :class:`qutip.nonmarkov.ttm.TTMSolverOptions`. + :class:`qutip.nonmarkov.transfertensor.TTMSolverOptions`. Returns ------- @@ -213,14 +215,18 @@ def _generatetensors(dynmaps, learningtimes=None, **kwargs): raise TypeError("Argument 'learnintimes' required when 'dynmaps'" + "is a callback function.") - def dynmapfunc(n): return dynmaps(learningtimes[n]) + def dynmapfunc(n): + return dynmaps(learningtimes[n]) + Kmax = len(learningtimes) else: try: tmp = dynmaps[:] del tmp - def dynmapfunc(n): return dynmaps[n] + def dynmapfunc(n): + return dynmaps[n] + Kmax = len(dynmaps) except TypeError: raise TypeError("Argument 'dynmaps' should be a callable or" + @@ -237,12 +243,12 @@ def dynmapfunc(n): return dynmaps[n] for n in range(Kmax): T = dynmapfunc(n) for m in range(1, n): - T -= Tlist[n-m]*dynmapfunc(m) + T -= Tlist[n - m] * dynmapfunc(m) Tlist.append(T) if n > 1: - diff.append((Tlist[-1]-Tlist[-2]).norm()) + diff.append((Tlist[-1] - Tlist[-2]).norm()) if diff[-1] < opt.thres: # Below threshold for truncation - print('breaking', (Tlist[-1]-Tlist[-2]).norm(), n) + print('breaking', (Tlist[-1] - Tlist[-2]).norm(), n) break return Tlist, diff diff --git a/qutip/parallel.py b/qutip/solve/parallel.py similarity index 98% rename from qutip/parallel.py rename to qutip/solve/parallel.py index eafeadf3a9..36e35b8531 100644 --- a/qutip/parallel.py +++ b/qutip/solve/parallel.py @@ -2,7 +2,7 @@ This function provides functions for parallel execution of loops and function mappings, using the builtin Python module multiprocessing. """ -__all__ = ['parfor', 'parallel_map', 'serial_map'] +__all__ = ['parallel_map', 'serial_map'] from scipy import array import multiprocessing @@ -44,7 +44,7 @@ def parfor(func, *args, **kwargs): .. note:: - From QuTiP 3.1, we recommend to use :func:`qutip.parallel_map` + From QuTiP 3.1, we recommend to use :func:`qutip.parallel.parallel_map` instead of this function. Parameters @@ -119,7 +119,8 @@ def serial_map(task, values, task_args=tuple(), task_kwargs={}, **kwargs): 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:`qutip.parallel.parallel_map`. Parameters ---------- diff --git a/qutip/solve/pdpsolve.py b/qutip/solve/pdpsolve.py index 4e05649b10..1a1d2964d2 100644 --- a/qutip/solve/pdpsolve.py +++ b/qutip/solve/pdpsolve.py @@ -8,7 +8,7 @@ ) from ..core import data as _data from .solver import Result, SolverOptions -from ..parallel import serial_map +from .parallel import serial_map from ..ui.progressbar import TextProgressBar from ..settings import settings debug = settings.debug diff --git a/qutip/solve/rcsolve.py b/qutip/solve/rcsolve.py index c7b6db4282..b357bc6e02 100644 --- a/qutip/solve/rcsolve.py +++ b/qutip/solve/rcsolve.py @@ -15,10 +15,8 @@ from numpy import linalg from .. import ( spre, spost, sprepost, thermal_dm, tensor, identity, destroy, sigmax, - sigmaz, basis, qeye, + sigmaz, basis, qeye, mesolve ) -from .mesolve import mesolve -from .solver import SolverOptions def rcsolve(Hsys, psi0, tlist, e_ops, Q, wc, alpha, N, w_th, sparse=False, @@ -50,8 +48,8 @@ def rcsolve(Hsys, psi0, tlist, e_ops, Q, wc, alpha, N, w_th, sparse=False, Temperature. sparse: Boolean Optional argument to call the sparse eigenstates solver if needed. - options : :class:`qutip.SolverOptions` - With options for the solver. + options : dict + Options for the solver. Returns ------- @@ -59,7 +57,7 @@ def rcsolve(Hsys, psi0, tlist, e_ops, Q, wc, alpha, N, w_th, sparse=False, System evolution. """ if options is None: - options = SolverOptions() + options = {} dot_energy, dot_state = Hsys.eigenstates(sparse=sparse) deltaE = dot_energy[1] - dot_energy[0] diff --git a/qutip/solve/sesolve.py b/qutip/solve/sesolve.py deleted file mode 100644 index f4479234d8..0000000000 --- a/qutip/solve/sesolve.py +++ /dev/null @@ -1,373 +0,0 @@ -""" -This module provides solvers for the unitary Schrodinger equation. -""" - -__all__ = ['sesolve'] - -import numpy as np -import scipy.integrate -from scipy.linalg import norm as la_norm -from .. import Qobj, QobjEvo -from ..core import data as _data -from .solver import Result, SolverOptions, solver_safe, SolverSystem -from ..ui.progressbar import BaseProgressBar, TextProgressBar - - -def sesolve(H, psi0, tlist, e_ops=None, args=None, options=None, - progress_bar=None, _safe_mode=True): - """ - Schrödinger 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 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 - 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 - - Parameters - ---------- - - H : :class:`~Qobj`, :class:`~QobjEvo`, list, or callable - System Hamiltonian as a :obj:`~Qobj , list of :obj:`Qobj` and - coefficient, :obj:`~QObjEvo`, or a callback function for time-dependent - Hamiltonians. List format and options can be found in QobjEvo's - description. - - psi0 : :class:`~Qobj` - Initial state vector (ket) or initial unitary operator ``psi0 = U``. - - tlist : array_like of float - List of times for :math:`t`. - - e_ops : None / list / callback function, optional - A list of operators as `Qobj` and/or callable functions (can be mixed) - or a single callable function. For callable functions, they are called - as ``f(t, state)`` and return the expectation value. A single - callback's expectation value can be any type, but a callback part of a - list must return a number as the expectation value. For operators, the - result's expect will be computed by :func:`qutip.expect` when the state - is a ``ket``. For operator evolution, the overlap is computed by: :: - - (e_ops[i].dag() * op(t)).tr() - - args : dict, optional - Dictionary of scope parameters for time-dependent Hamiltonians. - - options : None / :class:`qutip.SolverOptions`, optional - Options for the ODE solver. - - progress_bar : :obj:`~BaseProgressBar`, optional - Optional instance of :obj:`~BaseProgressBar`, or a subclass thereof, - for showing the progress of the simulation. - - Returns - ------- - - output: :class:`~solver.Result` - An instance of the class :class:`~solver.Result`, which contains either - an array of expectation values for the times specified by ``tlist``, or - an array or state vectors corresponding to the times in ``tlist`` (if - ``e_ops`` is an empty list), or nothing if a callback function was - given inplace of operators for which to calculate the expectation - values. - """ - if e_ops is None: - e_ops = [] - if isinstance(e_ops, Qobj): - e_ops = [e_ops] - elif isinstance(e_ops, dict): - e_ops_dict = e_ops - e_ops = [e for e in e_ops.values()] - else: - e_ops_dict = None - - if progress_bar is None: - progress_bar = BaseProgressBar() - if progress_bar is True: - progress_bar = TextProgressBar() - - if not (psi0.isket or psi0.isunitary): - raise TypeError("The unitary solver requires psi0 to be" - " a ket as initial state" - " or a unitary as initial operator.") - - if options is None: - options = SolverOptions() - """ - if options.rhs_reuse and not isinstance(H, SolverSystem): - # TODO: deprecate when going to class based solver. - if "sesolve" in solver_safe: - H = solver_safe["sesolve"] - else: - pass - # raise Exception("Could not find the Hamiltonian to reuse.") - """ - - if args is None: - args = {} - - if isinstance(H, SolverSystem): - ss = H - elif isinstance(H, (list, Qobj, QobjEvo)): - ss = _sesolve_QobjEvo(H, tlist, args, options) - elif callable(H): - ss = _sesolve_func_td(H, args, options) - else: - raise Exception("Invalid H type") - - func, ode_args = ss.makefunc(ss, psi0, args, e_ops, options) - - if _safe_mode: - v = psi0.full().ravel('F') - func(0., v, *ode_args)[:, 0] + v - - res = _generic_ode_solve(func, ode_args, psi0, tlist, e_ops, options, - progress_bar, dims=psi0.dims) - if e_ops_dict: - res.expect = {e: res.expect[n] - for n, e in enumerate(e_ops_dict.keys())} - return res - - -# ----------------------------------------------------------------------------- -# A time-dependent unitary wavefunction equation on the list-function format -# -def _sesolve_QobjEvo(H, tlist, args, opt): - """ - Prepare the system for the solver, H can be an QobjEvo. - """ - H_td = -1.0j * QobjEvo(H, args, tlist=tlist) - - ss = SolverSystem() - ss.H = H_td - ss.makefunc = _qobjevo_set - solver_safe["sesolve"] = ss - return ss - - -def _wrap_matmul(t, state, cqobj, oper): - state = _data.dense.fast_from_numpy(state) - if oper: - state = _data.column_unstack_dense(state, cqobj.shape[1], inplace=True) - out = cqobj.matmul_data(t, state) - if oper: - out = _data.column_stack_dense(out, inplace=True) - return out.as_ndarray() - - -def _qobjevo_set(HS, psi, args, e_ops, opt): - """ - From the system, get the ode function and args - """ - H_td = HS.H - H_td.arguments(args) - if psi.isket or psi.isunitary: - return _wrap_matmul, (H_td, psi.isunitary) - raise TypeError("The unitary solver requires psi0 to be" - " a ket as initial state" - " or a unitary as initial operator.") - - -# ----------------------------------------------------------------------------- -# Wave function evolution using a ODE solver (unitary quantum evolution), for -# time dependent hamiltonians. -# -def _sesolve_func_td(H_func, args, opt): - """ - Prepare the system for the solver, H is a function. - """ - ss = SolverSystem() - ss.H = H_func - ss.makefunc = _Hfunc_set - solver_safe["sesolve"] = ss - return ss - - -def _Hfunc_set(HS, psi, args, e_ops, opt): - """ - From the system, get the ode function and args - """ - return _sesolve_rhs_func, (HS.H, args, psi.isunitary, False) - - -# ----------------------------------------------------------------------------- -# evaluate dU(t)/dt according to the schrodinger equation -# -def _sesolve_rhs_func(t, y, H_func, args, oper, with_state): - H = H_func(t, y, args) if with_state else H_func(t, args) - if isinstance(H, Qobj): - H = H.data - y = _data.dense.fast_from_numpy(y) - if oper: - ym = _data.column_unstack_dense(y, H.shape[1], inplace=True) - out = _data.matmul(H, ym, scale=-1j) - return _data.column_stack_dense(out, inplace=True).as_ndarray() - return _data.matmul(H, y, scale=-1j, dtype=_data.Dense).as_ndarray() - - -# ----------------------------------------------------------------------------- -# Solve an ODE for func. -# Calculate the required expectation values or invoke callback -# function at each time step. -def _generic_ode_solve(func, ode_args, psi0, tlist, e_ops, opt, - progress_bar, dims=None): - """ - Internal function for solving ODEs. - """ - # %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - # This function is made similar to mesolve's one for futur merging in a - # solver class - # %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - - # prepare output array - n_tsteps = len(tlist) - output = Result() - output.solver = "sesolve" - output.times = tlist - - if psi0.isunitary: - initial_vector = psi0.full().ravel('F') - oper_evo = True - elif psi0.isket: - initial_vector = psi0.full().ravel() - oper_evo = False - - r = scipy.integrate.ode(func) - r.set_integrator('zvode', method=opt['method'], order=opt['order'], - atol=opt['atol'], rtol=opt['rtol'], nsteps=opt['nsteps'], - first_step=opt['first_step'], min_step=opt['min_step'], - max_step=opt['max_step']) - if ode_args: - r.set_f_params(*ode_args) - r.set_initial_value(initial_vector, tlist[0]) - - e_ops_data = [] - output.expect = [] - if callable(e_ops): - n_expt_op = 0 - expt_callback = True - output.num_expect = 1 - elif isinstance(e_ops, list): - n_expt_op = len(e_ops) - expt_callback = False - output.num_expect = n_expt_op - if n_expt_op == 0: - # fallback on storing states - opt['store_states'] = True - else: - for op in e_ops: - if not isinstance(op, Qobj) and callable(op): - output.expect.append(np.zeros(n_tsteps, dtype=complex)) - continue - if op.isherm: - output.expect.append(np.zeros(n_tsteps)) - else: - output.expect.append(np.zeros(n_tsteps, dtype=complex)) - if oper_evo: - for e in e_ops: - if not isinstance(e, Qobj): - e_ops_data.append(e) - elif e.dims[1] != psi0.dims[0]: - raise TypeError(f"e_ops dims ({e.dims}) are not compatible" - f" with the state's ({psi0.dims})") - else: - e_ops_data.append(e.dag().data) - else: - for e in e_ops: - if not isinstance(e, Qobj): - e_ops_data.append(e) - elif e.dims[1] != psi0.dims[0]: - raise TypeError(f"e_ops dims ({e.dims}) are not compatible" - f" with the state's ({psi0.dims})") - else: - e_ops_data.append(e.data) - else: - raise TypeError("Expectation parameter must be a list or a function") - - if opt['store_states']: - output.states = [] - - if oper_evo: - def get_curr_state_data(r): - return _data.column_unstack_dense(_data.dense.fast_from_numpy(r.y), - psi0.shape[0], inplace=True) - else: - def get_curr_state_data(r): - return _data.dense.fast_from_numpy(r.y) - - # - # start evolution - # - dt = np.diff(tlist) - cdata = None - progress_bar.start(n_tsteps) - for t_idx, t in enumerate(tlist): - progress_bar.update(t_idx) - if not r.successful(): - raise Exception("ODE integration error: Try to increase " - "the allowed number of substeps by increasing " - "the nsteps parameter in the Options class.") - # get the current state / oper data if needed - if ( - opt['store_states'] - or opt['normalize_output'] - or n_expt_op > 0 - or expt_callback - ): - cdata = get_curr_state_data(r) - - if opt['normalize_output']: - # normalize per column - if oper_evo: - cdata_nd = cdata.as_ndarray() - cdata_nd /= la_norm(cdata_nd, axis=0) - # Don't do this in place, because we use it later. - initial = _data.column_stack_dense(cdata, inplace=False) - r.set_initial_value(initial.as_ndarray(), r.t) - else: - norm = _data.norm.l2(cdata) - if abs(norm - 1) > 1e-12: - # only reset the solver if state changed - cdata = _data.mul(cdata, 1/norm) - r.set_initial_value(cdata.as_ndarray(), r.t) - else: - r._y = cdata.as_ndarray() - - if opt['store_states']: - output.states.append(Qobj(cdata, - dims=dims, type=psi0.type)) - - if expt_callback: - # use callback method - output.expect.append(e_ops(t, Qobj(cdata, - dims=dims, type=psi0.type))) - - for m in range(n_expt_op): - if not isinstance(e_ops[m], Qobj) and callable(e_ops[m]): - output.expect[m][t_idx] = e_ops[m](t, Qobj(cdata, dims=dims)) - else: - val = _data.expect(e_ops_data[m], cdata) - if e_ops[m].isherm: - val = val.real - output.expect[m][t_idx] = val - - if t_idx < n_tsteps - 1: - r.integrate(r.t + dt[t_idx]) - - progress_bar.finished() - - if opt['store_final_state']: - cdata = get_curr_state_data(r) - if opt['normalize_output']: - cdata = cdata.as_ndarray() - cdata /= la_norm(cdata, axis=0) - output.final_state = Qobj(cdata, dims=dims) - - return output diff --git a/qutip/solve/steadystate.py b/qutip/solve/steadystate.py deleted file mode 100644 index a6266a090a..0000000000 --- a/qutip/solve/steadystate.py +++ /dev/null @@ -1,1373 +0,0 @@ -""" -Module contains functions for solving for the steady state density matrix of -open quantum systems defined by a Liouvillian or Hamiltonian and a list of -collapse operators. -""" - -__all__ = ['steadystate', 'steadystate_floquet', - 'build_preconditioner', 'pseudo_inverse'] - -import warnings -import time - -import numpy as np -import scipy -import scipy.sparse -import scipy.sparse.csgraph -import scipy.linalg -from scipy.sparse.linalg import ( - use_solver, splu, spilu, eigs, LinearOperator, gmres, lgmres, bicgstab, -) - -from .. import ( - Qobj, liouvillian, unstack_columns, stack_columns, spre, tensor, identity, - operator_to_vector, -) -from ..core import data as _data -from ..settings import settings -from . import _steadystate - -import qutip.logging_utils -logger = qutip.logging_utils.get_logger('qutip.solve.steadystate') -logger.setLevel('DEBUG') - -# Load MKL spsolve if avaiable -if settings.has_mkl: - from qutip._mkl.spsolve import (mkl_splu, mkl_spsolve) - - -def _permute(matrix, rows, cols): - """ - Permute the rows and columns of the scipy CSR or CSC matrix such that row - `i` becomes row `rows[i]` and so on. Returns the same type of CSR or CSC - matrix. - """ - # If the three data buffers of a CSC matrix are read as a CSR matrix (which - # the corresponding shape change), it appears to be the transpose of the - # input. To handle CSC matrices here, we always use the data-layer type - # CSR, but switch the rows and cols in the permutation if we actually want - # a CSC, and take care to output the transpose of the transpose at the end. - if not np.any(rows): - rows = np.arange(matrix.shape[0]) - if not np.any(cols): - cols = np.arange(matrix.shape[1]) - - rows = np.argsort(rows) - cols = np.argsort(cols) - - shape = matrix.shape - if isinstance(matrix, scipy.sparse.csc_matrix): - rows, cols = cols, rows - shape = (shape[1], shape[0]) - temp = _data.CSR((matrix.data, matrix.indices, matrix.indptr), - shape=shape) - temp = _data.permute.indices_csr(temp, rows, cols).as_scipy() - if isinstance(matrix, scipy.sparse.csr_matrix): - return temp - return scipy.sparse.csc_matrix((temp.data, temp.indices, temp.indptr), - shape=matrix.shape) - - - -def _profile(graph): - profile = 0 - for row in range(graph.shape[0]): - row_min = row_max = 0 - for ptr in range(graph.indptr[row], graph.indptr[row + 1]): - if graph.data[ptr] == 0: - continue - dist = graph.indices[ptr] - row - row_max = dist if dist > row_max else row_max - row_min = dist if dist < row_min else row_min - profile += row_max - row_min - return profile - - -def _bandwidth(graph): - upper = lower = 0 - for row in range(graph.shape[0]): - for ptr in range(graph.indptr[row], graph.indptr[row + 1]): - if graph.data[ptr] == 0: - continue - dist = graph.indices[ptr] - row - lower = dist if dist < lower else lower - upper = dist if dist > upper else upper - return upper - lower + 1 - - -def _weighted_bipartite_matching(A, perm_type='row'): - """ - Returns an array of row permutations that attempts to maximize the product - of the ABS values of the diagonal elements in a nonsingular square CSC - sparse matrix. Such a permutation is always possible provided that the - matrix is nonsingular. - - This function looks at both the structure and ABS values of the underlying - matrix. - - Parameters - ---------- - A : csc_matrix - Input matrix - - perm_type : str {'row', 'column'} - Type of permutation to generate. - - Returns - ------- - perm : array - Array of row or column permutations. - - Notes - ----- - This function uses a weighted maximum cardinality bipartite matching - algorithm based on breadth-first search (BFS). The columns are weighted - according to the element of max ABS value in the associated rows and are - traversed in descending order by weight. When performing the BFS - traversal, the row associated to a given column is the one with maximum - weight. Unlike other techniques[1]_, this algorithm does not guarantee the - product of the diagonal is maximized. However, this limitation is offset - by the substantially faster runtime of this method. - - References - ---------- - I. S. Duff and J. Koster, "The design and use of algorithms for permuting - large entries to the diagonal of sparse matrices", SIAM J. Matrix Anal. - and Applics. 20, no. 4, 889 (1997). - """ - nrows = A.shape[0] - if A.shape[0] != A.shape[1]: - raise ValueError('weighted_bfs_matching requires a square matrix.') - if scipy.sparse.isspmatrix_csr(A) or scipy.sparse.isspmatrix_coo(A): - A = A.tocsc() - elif not scipy.sparse.isspmatrix_csc(A): - raise TypeError("matrix must be in CSC, CSR, or COO format.") - - if perm_type == 'column': - A = A.transpose().tocsc() - perm = _steadystate.weighted_bipartite_matching( - np.asarray(np.abs(A.data), dtype=float), - A.indices, A.indptr, nrows) - if np.any(perm == -1): - raise Exception('Possibly singular input matrix.') - return perm - - -def _empty_info_dict(): - def_info = {'perm': [], 'solution_time': None, - 'residual_norm': None, - 'solver': None, 'method': None} - - return def_info - - -def _default_steadystate_args(): - def_args = {'sparse': True, 'use_rcm': False, - 'use_wbm': False, 'use_precond': False, - 'all_states': False, 'M': None, 'x0': None, 'drop_tol': 1e-4, - 'fill_factor': 100, 'diag_pivot_thresh': 0.1, 'maxiter': 1000, - 'permc_spec': 'COLAMD', 'ILU_MILU': 'smilu_2', - 'restart': 20, - 'max_iter_refine': 10, - 'scaling_vectors': True, - 'weighted_matching': True, - 'return_info': False, 'info': _empty_info_dict(), - 'verbose': False, 'solver': 'scipy', 'weight': None, - 'tol': 1e-12, 'matol': 1e-15, 'mtol': None} - return def_args - - -def steadystate(A, c_op_list=[], method='direct', solver=None, **kwargs): - """ - Calculates the steady state for quantum evolution subject to the supplied - Hamiltonian or Liouvillian operator and (if given a Hamiltonian) a list of - collapse operators. - - If the user passes a Hamiltonian then it, along with the list of collapse - operators, will be converted into a Liouvillian operator in Lindblad form. - - Parameters - ---------- - A : :obj:`~Qobj` - A Hamiltonian or Liouvillian operator. - - c_op_list : list - A list of collapse operators. - - solver : {'scipy', 'mkl'}, optional - Selects the sparse solver to use. Default is to auto-select based on - the availability of the MKL library. - - method : str, default 'direct' - The allowed methods are - - 'direct' - - 'eigen' - - 'iterative-gmres' - - 'iterative-lgmres' - - 'iterative-bicgstab' - - 'svd' - - 'power' - - 'power-gmres' - - 'power-lgmres' - - 'power-bicgstab' - - Method for solving the underlying linear equation. Direct LU solver - 'direct' (default), sparse eigenvalue problem 'eigen', iterative GMRES - method 'iterative-gmres', iterative LGMRES method 'iterative-lgmres', - iterative BICGSTAB method 'iterative-bicgstab', SVD 'svd' (dense), or - inverse-power method 'power'. The iterative power methods - 'power-gmres', 'power-lgmres', 'power-bicgstab' use the same solvers as - their direct counterparts. - - return_info : bool, default False - Return a dictionary of solver-specific infomation about the solution - and how it was obtained. - - sparse : bool, default True - Solve for the steady state using sparse algorithms. If set to False, - the underlying Liouvillian operator will be converted into a dense - matrix. Use only for 'smaller' systems. - - use_rcm : bool, default False - Use reverse Cuthill-Mckee reordering to minimize fill-in in the LU - factorization of the Liouvillian. - - use_wbm : bool, default False - Use Weighted Bipartite Matching reordering to make the Liouvillian - diagonally dominant. This is useful for iterative preconditioners - only, and is set to ``True`` by default when finding a preconditioner. - - weight : float, optional - Sets the size of the elements used for adding the unity trace condition - to the linear solvers. This is set to the average abs value of the - Liouvillian elements if not specified by the user. - - max_iter_refine : int, default 10 - MKL ONLY. Max. number of iterative refinements to perform. - - scaling_vectors : bool - MKL ONLY. Scale matrix to unit norm columns and rows. - - weighted_matching : bool - MKL ONLY. Use weighted matching to better condition diagonal. - - x0 : ndarray, optional - ITERATIVE ONLY. Initial guess for solution vector. - - maxiter : int, optional, default=1000 - ITERATIVE ONLY. Maximum number of iterations to perform. - - tol : float, default 1e-12 - ITERATIVE ONLY. Tolerance used for terminating solver. - - mtol : float, optional - ITERATIVE 'power' methods ONLY. Tolerance for lu solve method. If None - given then ``max(0.1*tol, 1e-15)`` is used. - - matol : float, default 1e-15 - ITERATIVE ONLY. Absolute tolerance for lu solve method. - - permc_spec : str, optional - ITERATIVE ONLY. Column ordering used internally by superLU for the - 'direct' LU decomposition method. Options include 'COLAMD' and - 'NATURAL'. If using RCM then this is set to 'NATURAL' automatically - unless explicitly specified. - - use_precond : bool optional, default = False - ITERATIVE ONLY. Use an incomplete sparse LU decomposition as a - preconditioner for the 'iterative' GMRES and BICG solvers. - Speeds up convergence time by orders of magnitude in many cases. - - M : {sparse matrix, dense matrix, LinearOperator}, optional - ITERATIVE ONLY. Preconditioner for A. The preconditioner should - approximate the inverse of A. Effective preconditioning can - dramatically improve the rate of convergence for iterative methods. - If no preconditioner is given and ``use_precond = True``, then one - is generated automatically. - - fill_factor : float, default 100 - ITERATIVE ONLY. Specifies the fill ratio upper bound (>=1) of the iLU - preconditioner. Lower values save memory at the cost of longer - execution times and a possible singular factorization. - - drop_tol : float, default 1e-4 - ITERATIVE ONLY. Sets the threshold for the magnitude of preconditioner - elements that should be dropped. Can be reduced for a courser - factorization at the cost of an increased number of iterations, and a - possible singular factorization. - - diag_pivot_thresh : float, optional - ITERATIVE ONLY. Sets the threshold between [0,1] for which diagonal - elements are considered acceptable pivot points when using a - preconditioner. A value of zero forces the pivot to be the diagonal - element. - - ILU_MILU : str, default 'smilu_2' - ITERATIVE ONLY. Selects the incomplete LU decomposition method algoithm - used in creating the preconditoner. Should only be used by advanced - users. - - Returns - ------- - dm : qobj - Steady state density matrix. - info : dict, optional - Dictionary containing solver-specific information about the solution. - - Notes - ----- - The SVD method works only for dense operators (i.e. small systems). - """ - if solver is None: - solver = 'scipy' - if settings.has_mkl: - if method in ['direct', 'power']: - solver = 'mkl' - elif solver == 'mkl' and \ - (method not in ['direct', 'power']): - raise ValueError('MKL solver only for direct or power methods.') - - elif solver not in ['scipy', 'mkl']: - raise ValueError('Invalid solver kwarg.') - - ss_args = _default_steadystate_args() - ss_args['method'] = method - if solver is not None: - ss_args['solver'] = solver - ss_args['info']['solver'] = ss_args['solver'] - ss_args['info']['method'] = ss_args['method'] - - for key, value in kwargs.items(): - if key in ss_args: - ss_args[key] = value - else: - raise TypeError( - "Invalid keyword argument '"+key+"' passed to steadystate.") - - # Set column perm to NATURAL if using RCM and not specified by user - if ss_args['use_rcm'] and ('permc_spec' not in kwargs): - ss_args['permc_spec'] = 'NATURAL' - - # Create & check Liouvillian - A = _steadystate_setup(A, c_op_list) - - # Set weight parameter to max abs val in L if not set explicitly - if 'weight' not in kwargs.keys(): - # set the weight to the mean of the non-zero absoluate values in A: - A_np = np.abs(A.full()) - ss_args['weight'] = np.mean(A_np[A_np > 0]) - ss_args['info']['weight'] = ss_args['weight'] - - if ss_args['method'] == 'direct': - if (ss_args['solver'] == 'scipy' and ss_args['sparse']) \ - or ss_args['solver'] == 'mkl': - return _steadystate_direct_sparse(A, ss_args) - else: - return _steadystate_direct_dense(A, ss_args) - - elif ss_args['method'] == 'eigen': - return _steadystate_eigen(A, ss_args) - - elif ss_args['method'] in ['iterative-gmres', - 'iterative-lgmres', 'iterative-bicgstab']: - return _steadystate_iterative(A, ss_args) - - elif ss_args['method'] == 'svd': - return _steadystate_svd_dense(A, ss_args) - - elif ss_args['method'] in ['power', 'power-gmres', - 'power-lgmres', 'power-bicgstab']: - return _steadystate_power(A, ss_args) - - else: - raise ValueError('Invalid method argument for steadystate.') - - -def _steadystate_setup(A, c_op_list): - """Build Liouvillian (if necessary) and check input.""" - if A.isoper: - if len(c_op_list) > 0: - return liouvillian(A, c_op_list) - raise TypeError('Cannot calculate the steady state for a ' + - 'non-dissipative system ' + - '(no collapse operators given)') - if A.issuper: - return A - raise TypeError('Solving for steady states requires ' + - 'Liouvillian (super) operators') - - -def _steadystate_LU_liouvillian(L, ss_args, has_mkl=0): - """Creates modified Liouvillian for LU based SS methods. - """ - perm = None - perm2 = None - rev_perm = None - n = int(np.sqrt(L.shape[0])) - L = _data.to(_data.CSR, L.data).as_scipy() - if has_mkl: - constructor = scipy.sparse.csr_matrix - else: - L = L.tocsc() - constructor = scipy.sparse.csc_matrix - L = L + constructor((ss_args['weight'] * np.ones(n), - (np.zeros(n), [nn * (n+1) for nn in range(n)])), - shape=(n*n, n*n)) - - if settings.debug: - old_band = _bandwidth(L) - old_pro = _profile(L) - logger.debug('Orig. NNZ: %i', L.nnz) - if ss_args['use_rcm']: - logger.debug('Original bandwidth: %i', old_band) - - if ss_args['use_wbm']: - if settings.debug: - logger.debug('Calculating Weighted Bipartite Matching ordering...') - _wbm_start = time.time() - perm = _weighted_bipartite_matching(L) - _wbm_end = time.time() - L = _permute(L, perm, None) - ss_args['info']['perm'].append('wbm') - ss_args['info']['wbm_time'] = _wbm_end-_wbm_start - if settings.debug: - wbm_band = _bandwidth(L) - logger.debug('WBM bandwidth: %i' % wbm_band) - - if ss_args['use_rcm']: - if settings.debug: - logger.debug('Calculating Reverse Cuthill-Mckee ordering...') - _rcm_start = time.time() - perm2 = scipy.sparse.csgraph.reverse_cuthill_mckee(L) - _rcm_end = time.time() - rev_perm = np.argsort(perm2) - L = _permute(L, perm2, perm2) - ss_args['info']['perm'].append('rcm') - ss_args['info']['rcm_time'] = _rcm_end-_rcm_start - if settings.debug: - rcm_band = _bandwidth(L) - rcm_pro = _profile(L) - logger.debug('RCM bandwidth: %i' % rcm_band) - logger.debug('Bandwidth reduction factor: %f' % - (old_band/rcm_band)) - logger.debug('Profile reduction factor: %f' % - (old_pro/rcm_pro)) - L.sort_indices() - return L, perm, perm2, rev_perm, ss_args - - -def _steadystate_direct_sparse(L, ss_args): - """ - Direct solver that uses scipy sparse matrices - """ - if settings.debug: - logger.debug('Starting direct LU solver.') - - dims = L.dims[0] - n = int(np.sqrt(L.shape[0])) - b = np.zeros(n ** 2, dtype=complex) - b[0] = ss_args['weight'] - - if ss_args['solver'] == 'mkl': - has_mkl = 1 - else: - has_mkl = 0 - - ss_lu_liouv_list = _steadystate_LU_liouvillian(L, ss_args, has_mkl) - L, perm, perm2, rev_perm, ss_args = ss_lu_liouv_list - if np.any(perm): - b = b[np.ix_(perm,)] - if np.any(perm2): - b = b[np.ix_(perm2,)] - - if ss_args['solver'] == 'scipy': - ss_args['info']['permc_spec'] = ss_args['permc_spec'] - ss_args['info']['drop_tol'] = ss_args['drop_tol'] - ss_args['info']['diag_pivot_thresh'] = ss_args['diag_pivot_thresh'] - ss_args['info']['fill_factor'] = ss_args['fill_factor'] - ss_args['info']['ILU_MILU'] = ss_args['ILU_MILU'] - - if not ss_args['solver'] == 'mkl': - # Use superLU solver - orig_nnz = L.nnz - _direct_start = time.time() - lu = splu(L, permc_spec=ss_args['permc_spec'], - diag_pivot_thresh=ss_args['diag_pivot_thresh'], - options=dict(ILU_MILU=ss_args['ILU_MILU'])) - v = lu.solve(b) - _direct_end = time.time() - ss_args['info']['solution_time'] = _direct_end - _direct_start - if (settings.debug or ss_args['return_info']): - L_nnz = lu.L.nnz - U_nnz = lu.U.nnz - ss_args['info']['l_nnz'] = L_nnz - ss_args['info']['u_nnz'] = U_nnz - ss_args['info']['lu_fill_factor'] = (L_nnz + U_nnz)/L.nnz - if settings.debug: - logger.debug('L NNZ: %i ; U NNZ: %i' % (L_nnz, U_nnz)) - logger.debug('Fill factor: %f' % ((L_nnz + U_nnz)/orig_nnz)) - - else: # Use MKL solver - if len(ss_args['info']['perm']) != 0: - in_perm = np.arange(n**2, dtype=np.int32) - else: - in_perm = None - _direct_start = time.time() - v = mkl_spsolve(L, b, perm=in_perm, verbose=ss_args['verbose'], - max_iter_refine=ss_args['max_iter_refine'], - scaling_vectors=ss_args['scaling_vectors'], - weighted_matching=ss_args['weighted_matching']) - _direct_end = time.time() - ss_args['info']['solution_time'] = _direct_end-_direct_start - - if ss_args['return_info']: - ss_args['info']['residual_norm'] = scipy.linalg.norm(b - L*v, np.inf) - ss_args['info']['max_iter_refine'] = ss_args['max_iter_refine'] - ss_args['info']['scaling_vectors'] = ss_args['scaling_vectors'] - ss_args['info']['weighted_matching'] = ss_args['weighted_matching'] - - if ss_args['use_rcm']: - v = v[np.ix_(rev_perm,)] - - data = unstack_columns(_data.create(v), (n, n)) - data = _data.mul(_data.add(data, _data.adjoint(data)), 0.5) - if ss_args['return_info']: - return Qobj(data, dims=dims, isherm=True), ss_args['info'] - else: - return Qobj(data, dims=dims, isherm=True) - - -def _steadystate_direct_dense(L, ss_args): - """ - Direct solver that uses numpy arrays. Suitable for small systems with few - states. - """ - if settings.debug: - logger.debug('Starting direct dense solver.') - - dims = L.dims[0] - n = int(np.sqrt(L.shape[0])) - b = np.zeros(n ** 2) - b[0] = ss_args['weight'] - - L = L.full() - L[0, :] = np.diag(ss_args['weight'] * np.ones(n)).reshape(n ** 2) - _dense_start = time.time() - v = np.linalg.solve(L, b) - _dense_end = time.time() - ss_args['info']['solution_time'] = _dense_end-_dense_start - if ss_args['return_info']: - ss_args['info']['residual_norm'] = scipy.linalg.norm(b - L @ v, np.inf) - data = unstack_columns(v) - data = 0.5 * (data + data.conj().T) - - return Qobj(data, dims=dims, isherm=True) - - -def _steadystate_eigen(L, ss_args): - """ - Internal function for solving the steady state problem by - finding the eigenvector corresponding to the zero eigenvalue - of the Liouvillian using ARPACK. - """ - ss_args['info'].pop('weight', None) - if settings.debug: - logger.debug('Starting Eigen solver.') - - dims = L.dims[0] - L = _data.to(_data.CSR, L.data).as_scipy().tocsc() - - if ss_args['use_rcm']: - ss_args['info']['perm'].append('rcm') - if settings.debug: - old_band = _bandwidth(L) - logger.debug('Original bandwidth: %i', old_band) - perm = scipy.sparse.csgraph.reverse_cuthill_mckee(L) - rev_perm = np.argsort(perm) - L = _permute(L, perm, perm) - if settings.debug: - rcm_band = _bandwidth(L) - logger.debug('RCM bandwidth: %i', rcm_band) - logger.debug('Bandwidth reduction factor: %f', old_band/rcm_band) - - _eigen_start = time.time() - eigval, eigvec = eigs(L, k=1, sigma=1e-15, tol=ss_args['tol'], - which='LM', maxiter=ss_args['maxiter']) - ss_args['info']['solution_time'] = time.time() - _eigen_start - if ss_args['return_info']: - ss_args['info']['residual_norm'] = scipy.linalg.norm(L*eigvec, np.inf) - if ss_args['use_rcm']: - eigvec = eigvec[np.ix_(rev_perm,)] - size = int(np.sqrt(eigvec.size)) - data = _data.column_unstack(_data.dense.fast_from_numpy(eigvec), size) - data = _data.mul(_data.add(data, _data.adjoint(data)), 0.5) - out = Qobj(data, dims=dims, type='oper', isherm=True, copy=False) - if ss_args['return_info']: - return out/out.tr(), ss_args['info'] - else: - return out/out.tr() - - -def _iterative_precondition(A, n, ss_args): - """ - Internal function for preconditioning the steadystate problem for use - with iterative solvers. - """ - if settings.debug: - logger.debug('Starting preconditioner.') - _precond_start = time.time() - try: - P = spilu(A, permc_spec=ss_args['permc_spec'], - drop_tol=ss_args['drop_tol'], - diag_pivot_thresh=ss_args['diag_pivot_thresh'], - fill_factor=ss_args['fill_factor'], - options=dict(ILU_MILU=ss_args['ILU_MILU'])) - - M = LinearOperator((n ** 2, n ** 2), matvec=P.solve) - _precond_end = time.time() - ss_args['info']['permc_spec'] = ss_args['permc_spec'] - ss_args['info']['drop_tol'] = ss_args['drop_tol'] - ss_args['info']['diag_pivot_thresh'] = ss_args['diag_pivot_thresh'] - ss_args['info']['fill_factor'] = ss_args['fill_factor'] - ss_args['info']['ILU_MILU'] = ss_args['ILU_MILU'] - ss_args['info']['precond_time'] = _precond_end-_precond_start - - if settings.debug or ss_args['return_info']: - if settings.debug: - logger.debug('Preconditioning succeeded.') - logger.debug('Precond. time: %f' % - (_precond_end - _precond_start)) - L_nnz = P.L.nnz - U_nnz = P.U.nnz - ss_args['info']['l_nnz'] = L_nnz - ss_args['info']['u_nnz'] = U_nnz - ss_args['info']['ilu_fill_factor'] = (L_nnz+U_nnz)/A.nnz - e = np.ones(n ** 2, dtype=int) - condest = scipy.linalg.norm(M*e, np.inf) - ss_args['info']['ilu_condest'] = condest - if settings.debug: - logger.debug('L NNZ: %i ; U NNZ: %i' % (L_nnz, U_nnz)) - logger.debug('Fill factor: %f' % ((L_nnz+U_nnz)/A.nnz)) - logger.debug('iLU condest: %f' % condest) - - except Exception: - raise Exception("Failed to build preconditioner. Try increasing " + - "fill_factor and/or drop_tol.") - - return M, ss_args - - -def _steadystate_iterative(L, ss_args): - """ - Iterative steady state solver using the GMRES, LGMRES, or BICGSTAB - algorithm and a sparse incomplete LU preconditioner. - """ - ss_iters = {'iter': 0} - - def _iter_count(r): - ss_iters['iter'] += 1 - return - - if settings.debug: - logger.debug('Starting %s solver.' % ss_args['method']) - - n = int(np.sqrt(L.shape[0])) - dims = L.dims[0] - b = np.zeros(n ** 2) - b[0] = ss_args['weight'] - - L, perm, perm2, rev_perm, ss_args = _steadystate_LU_liouvillian(L, ss_args) - if np.any(perm): - b = b[np.ix_(perm,)] - if np.any(perm2): - b = b[np.ix_(perm2,)] - - use_solver(assumeSortedIndices=True) - - if ss_args['M'] is None and ss_args['use_precond']: - ss_args['M'], ss_args = _iterative_precondition(L, n, ss_args) - if ss_args['M'] is None: - warnings.warn("Preconditioning failed. Continuing without.", - UserWarning) - - # Select iterative solver type - _iter_start = time.time() - if ss_args['method'] == 'iterative-gmres': - v, check = gmres(L, b, tol=ss_args['tol'], atol=ss_args['matol'], - M=ss_args['M'], x0=ss_args['x0'], - restart=ss_args['restart'], - maxiter=ss_args['maxiter'], - callback=_iter_count, callback_type='legacy') - elif ss_args['method'] == 'iterative-lgmres': - v, check = lgmres(L, b, tol=ss_args['tol'], atol=ss_args['matol'], - M=ss_args['M'], x0=ss_args['x0'], - maxiter=ss_args['maxiter'], - callback=_iter_count) - elif ss_args['method'] == 'iterative-bicgstab': - v, check = bicgstab(L, b, tol=ss_args['tol'], - atol=ss_args['matol'], - M=ss_args['M'], x0=ss_args['x0'], - maxiter=ss_args['maxiter'], - callback=_iter_count) - else: - raise Exception("Invalid iterative solver method.") - _iter_end = time.time() - - ss_args['info']['iter_time'] = _iter_end - _iter_start - if 'precond_time' in ss_args['info'].keys(): - ss_args['info']['solution_time'] = (ss_args['info']['iter_time'] + - ss_args['info']['precond_time']) - else: - ss_args['info']['solution_time'] = ss_args['info']['iter_time'] - ss_args['info']['iterations'] = ss_iters['iter'] - if ss_args['return_info']: - ss_args['info']['residual_norm'] = scipy.linalg.norm(b - L*v, np.inf) - - if settings.debug: - logger.debug('Number of Iterations: %i', ss_iters['iter']) - logger.debug('Iteration. time: %f', (_iter_end - _iter_start)) - - if check > 0: - raise Exception("Steadystate error: Did not reach tolerance after " + - str(ss_args['maxiter']) + " steps." + - "\nResidual norm: " + - str(ss_args['info']['residual_norm'])) - - elif check < 0: - raise Exception( - "Steadystate error: Failed with fatal error: " + str(check) + ".") - - if ss_args['use_rcm']: - v = v[np.ix_(rev_perm,)] - - size = int(np.sqrt(v.size)) - data = _data.column_unstack(_data.dense.fast_from_numpy(v), size) - data = _data.mul(_data.add(data, _data.adjoint(data)), 0.5) - if ss_args['return_info']: - return Qobj(data, dims=dims, isherm=True), ss_args['info'] - else: - return Qobj(data, dims=dims, isherm=True) - - -def _steadystate_svd_dense(L, ss_args): - """ - Find the steady state(s) of an open quantum system by solving for the - nullspace of the Liouvillian. - """ - ss_args['info'].pop('weight', None) - atol = 1e-12 - rtol = 1e-12 - if settings.debug: - logger.debug('Starting SVD solver.') - _svd_start = time.time() - u, s, vh = np.linalg.svd(L.full(), full_matrices=False) - tol = max(atol, rtol * s[0]) - nnz = (s >= tol).sum() - ns = vh[nnz:].conj().T - _svd_end = time.time() - ss_args['info']['solution_time'] = _svd_end-_svd_start - if ss_args['all_states']: - rhoss_list = [] - for n in range(ns.shape[1]): - rhoss = Qobj(unstack_columns(ns[:, n]), dims=L.dims[0]) - rhoss_list.append(rhoss / rhoss.tr()) - if ss_args['return_info']: - return rhoss_list, ss_args['info'] - else: - if ss_args['return_info']: - return rhoss_list, ss_args['info'] - else: - return rhoss_list - else: - rhoss = Qobj(unstack_columns(ns[:, 0]), dims=L.dims[0]) - return rhoss / rhoss.tr() - - -def _steadystate_power_liouvillian(L, ss_args, has_mkl=0): - """Creates modified Liouvillian for power based SS methods. - """ - perm = None - perm2 = None - rev_perm = None - n = L.shape[0] - L = _data.sub(L.data, _data.identity(n, 1e-15)) - if ss_args['solver'] == 'mkl': - kind = 'csr' - else: - kind = 'csc' - if settings.debug: - old_band = _bandwidth(L.as_scipy()) - old_pro = _profile(L.as_scipy()) - logger.debug('Original bandwidth: %i', old_band) - logger.debug('Original profile: %i', old_pro) - - if ss_args['use_wbm']: - if settings.debug: - logger.debug('Calculating Weighted Bipartite Matching ordering...') - _wbm_start = time.time() - perm2 = _weighted_bipartite_matching(L.as_scipy()) - _wbm_end = time.time() - L = _data.permute.indices_csr(L, perm, np.arange(n, dtype=np.int32)) - ss_args['info']['perm'].append('wbm') - ss_args['info']['wbm_time'] = _wbm_end-_wbm_start - if settings.debug: - wbm_band = _bandwidth(L.as_scipy()) - wbm_pro = _profile(L.as_scipy()) - logger.debug('WBM bandwidth: %i', wbm_band) - logger.debug('WBM profile: %i', wbm_pro) - - if ss_args['use_rcm']: - if settings.debug: - logger.debug('Calculating Reverse Cuthill-Mckee ordering...') - ss_args['info']['perm'].append('rcm') - _rcm_start = time.time() - perm2 = scipy.sparse.csgraph.reverse_cuthill_mckee(L.as_scipy()) - _rcm_end = time.time() - ss_args['info']['rcm_time'] = _rcm_end-_rcm_start - rev_perm = np.argsort(perm2) - L = _data.CSR(_permute(L.as_scipy(), perm2, perm2)) - if settings.debug: - new_band = _bandwidth(L.as_scipy()) - new_pro = _profile(L.as_scipy()) - logger.debug('RCM bandwidth: %i', new_band) - logger.debug('Bandwidth reduction factor: %f', old_band/new_band) - logger.debug('RCM profile: %i', new_pro) - logger.debug('Profile reduction factor: %f', old_pro/new_pro) - L.sort_indices() - L = L.as_scipy() - if kind == 'csc': - L = L.tocsc() - return L, perm, perm2, rev_perm, ss_args - - -def _steadystate_power(L, ss_args): - """ - Inverse power method for steady state solving. - """ - ss_args['info'].pop('weight', None) - if settings.debug: - logger.debug('Starting iterative inverse-power method solver.') - tol = ss_args['tol'] - mtol = ss_args['mtol'] - if mtol is None: - mtol = max(0.1*tol, 1e-15) - maxiter = ss_args['maxiter'] - - use_solver(assumeSortedIndices=True) - sflag = L.type == 'super' - if sflag: - rhoss_dims = L.dims[0] - else: - rhoss_dims = [L.dims[0], 1] - n = L.shape[0] - # Build Liouvillian - if ss_args['solver'] == 'mkl' and ss_args['method'] == 'power': - has_mkl = 1 - else: - has_mkl = 0 - L, perm, perm2, rev_perm, ss_args = _steadystate_power_liouvillian(L, - ss_args, - has_mkl) - orig_nnz = L.nnz - # start with all ones as RHS - v = np.ones(n, dtype=complex) - if ss_args['use_rcm']: - v = v[np.ix_(perm2,)] - - # Do preconditioning - if ss_args['solver'] == 'scipy': - if ss_args['M'] is None and ss_args['use_precond'] and \ - ss_args['method'] in ['power-gmres', - 'power-lgmres', - 'power-bicgstab']: - ss_args['M'], ss_args = _iterative_precondition(L, int(np.sqrt(n)), - ss_args) - if ss_args['M'] is None: - warnings.warn("Preconditioning failed. Continuing without.", - UserWarning) - - ss_iters = {'iter': 0} - - def _iter_count(*args): - ss_iters['iter'] += 1 - - _power_start = time.time() - # Get LU factors - if ss_args['method'] == 'power': - if ss_args['solver'] == 'mkl': - lu = mkl_splu(L, - max_iter_refine=ss_args['max_iter_refine'], - scaling_vectors=ss_args['scaling_vectors'], - weighted_matching=ss_args['weighted_matching']) - else: - lu = splu(L, - permc_spec=ss_args['permc_spec'], - diag_pivot_thresh=ss_args['diag_pivot_thresh'], - options=dict(ILU_MILU=ss_args['ILU_MILU'])) - - if settings.debug: - L_nnz = lu.L.nnz - U_nnz = lu.U.nnz - logger.debug('L NNZ: %i ; U NNZ: %i', L_nnz, U_nnz) - logger.debug('Fill factor: %f', (L_nnz+U_nnz)/orig_nnz) - - it = 0 - while (scipy.linalg.norm(L * v, np.inf) > tol) and (it < maxiter): - check = 0 - if ss_args['method'] == 'power': - v = lu.solve(v) - elif ss_args['method'] == 'power-gmres': - v, check = gmres(L, v, tol=mtol, atol=ss_args['matol'], - M=ss_args['M'], x0=ss_args['x0'], - restart=ss_args['restart'], - maxiter=ss_args['maxiter'], - callback=_iter_count, - callback_type='legacy') - elif ss_args['method'] == 'power-lgmres': - v, check = lgmres(L, v, tol=mtol, atol=ss_args['matol'], - M=ss_args['M'], x0=ss_args['x0'], - maxiter=ss_args['maxiter'], - callback=_iter_count) - elif ss_args['method'] == 'power-bicgstab': - v, check = bicgstab(L, v, tol=mtol, atol=ss_args['matol'], - M=ss_args['M'], x0=ss_args['x0'], - maxiter=ss_args['maxiter'], - callback=_iter_count) - else: - raise Exception("Invalid iterative solver method.") - if check > 0: - raise Exception("{} failed to find solution in " - "{} iterations.".format(ss_args['method'], - check)) - if check < 0: - raise Exception("Breakdown in {}".format(ss_args['method'])) - v = v / scipy.linalg.norm(v, np.inf) - it += 1 - if ss_args['method'] == 'power' and ss_args['solver'] == 'mkl': - lu.delete() - if ss_args['return_info']: - ss_args['info']['max_iter_refine'] = ss_args['max_iter_refine'] - ss_args['info']['scaling_vectors'] = ss_args['scaling_vectors'] - ss_args['info']['weighted_matching'] = ss_args['weighted_matching'] - - if it >= maxiter: - raise Exception('Failed to find steady state after ' + - str(maxiter) + ' iterations') - _power_end = time.time() - ss_args['info']['solution_time'] = _power_end-_power_start - ss_args['info']['iterations'] = it - if ss_args['return_info']: - ss_args['info']['residual_norm'] = scipy.linalg.norm(L*v, np.inf) - if settings.debug: - logger.debug('Number of iterations: %i', it) - - if ss_args['use_rcm']: - v = v[np.ix_(rev_perm,)] - - # normalise according to type of problem - if sflag: - trow = v[::np.prod(rhoss_dims[0])+1] - data = v / np.sum(trow) - else: - data = data / scipy.linalg.norm(v) - - data = unstack_columns(_data.create(data)) - data = _data.mul(_data.add(data, _data.adjoint(data)), 0.5) - rhoss = Qobj(data, - dims=rhoss_dims, - isherm=True, - copy=False) - if ss_args['return_info']: - return rhoss, ss_args['info'] - else: - return rhoss - - -def steadystate_floquet(H_0, c_ops, Op_t, w_d=1.0, n_it=3, sparse=False): - """ - Calculates the effective steady state for a driven - system with a time-dependent cosinusoidal term: - .. math:: - \\mathcal{\\hat{H}}(t) = \\hat{H}_0 + - \\mathcal{\\hat{O}} \\cos(\\omega_d t) - Parameters - ---------- - H_0 : :obj:`~Qobj` - A Hamiltonian or Liouvillian operator. - c_ops : list - A list of collapse operators. - Op_t : :obj:`~Qobj` - The the interaction operator which is multiplied by the cosine - w_d : float, default 1.0 - The frequency of the drive - n_it : int, default 3 - The number of iterations for the solver - sparse : bool, default False - Solve for the steady state using sparse algorithms. - Actually, dense seems to be faster. - Returns - ------- - dm : qobj - Steady state density matrix. - .. note:: - See: Sze Meng Tan, - https://copilot.caltech.edu/documents/16743/qousersguide.pdf, - Section (10.16) - """ - if False: - # TODO: rewrite using `core.Data` - N = H_0.shape[0] - - L_0 = liouvillian(H_0, c_ops).data.tocsc() - L_t = liouvillian(Op_t) - L_p = (0.5 * L_t).data.tocsc() - # L_p and L_m correspond to the positive and negative - # frequency terms respectively. - # They are independent in the model, so we keep both names. - L_m = L_p - L_p_array = L_p.todense() - L_m_array = L_p_array - - Id = scipy.sparse.eye(N ** 2, format="csc", dtype=np.complex128) - S = T = scipy.sparse.csc_matrix((N ** 2, N ** 2), dtype=np.complex128) - - for n_i in np.arange(n_it, 0, -1): - L = scipy.sparse.csc_matrix(L_0 - 1j * n_i * w_d * Id + L_m.dot(S)) - L.sort_indices() - LU = splu(L) - S = - LU.solve(L_p_array) - - L = scipy.sparse.csc_matrix(L_0 + 1j * n_i * w_d * Id + L_p.dot(T)) - L.sort_indices() - LU = splu(L) - T = - LU.solve(L_m_array) - - M_subs = L_0 + L_m.dot(S) + L_p.dot(T) - else: - N = H_0.shape[0] - - L_0 = liouvillian(H_0, c_ops).full() - L_t = liouvillian(Op_t) - L_p = (0.5 * L_t).full() - L_m = L_p - - Id = np.eye(N ** 2) - S, T = np.zeros((N ** 2, N ** 2)), np.zeros((N ** 2, N ** 2)) - - for n_i in np.arange(n_it, 0, -1): - L = L_0 - 1j * n_i * w_d * Id + np.matmul(L_m, S) - lu, piv = scipy.linalg.lu_factor(L) - S = - scipy.linalg.lu_solve((lu, piv), L_p) - - L = L_0 + 1j * n_i * w_d * Id + np.matmul(L_p, T) - lu, piv = scipy.linalg.lu_factor(L) - T = - scipy.linalg.lu_solve((lu, piv), L_m) - - M_subs = L_0 + np.matmul(L_m, S) + np.matmul(L_p, T) - - return steadystate(Qobj(M_subs, type="super", dims=L_t.dims)) - - -def build_preconditioner(A, c_op_list=[], **kwargs): - """Constructs a iLU preconditioner necessary for solving for - the steady state density matrix using the iterative linear solvers - in the 'steadystate' function. - - Parameters - ---------- - A : qobj - A Hamiltonian or Liouvillian operator. - - c_op_list : list - A list of collapse operators. - - return_info : bool, optional, default = False - Return a dictionary of solver-specific infomation about the - solution and how it was obtained. - - use_rcm : bool, optional, default = False - Use reverse Cuthill-Mckee reordering to minimize fill-in in the - LU factorization of the Liouvillian. - - use_wbm : bool, optional, default = False - Use Weighted Bipartite Matching reordering to make the Liouvillian - diagonally dominant. This is useful for iterative preconditioners - only, and is set to ``True`` by default when finding a preconditioner. - - weight : float, optional - Sets the size of the elements used for adding the unity trace condition - to the linear solvers. This is set to the average abs value of the - Liouvillian elements if not specified by the user. - - method : str, default = 'iterative' - Tells the preconditioner what type of Liouvillian to build for - iLU factorization. For direct iterative methods use 'iterative'. - For power iterative methods use 'power'. - - permc_spec : str, optional, default='COLAMD' - Column ordering used internally by superLU for the - 'direct' LU decomposition method. Options include 'COLAMD' and - 'NATURAL'. If using RCM then this is set to 'NATURAL' automatically - unless explicitly specified. - - fill_factor : float, optional, default = 100 - Specifies the fill ratio upper bound (>=1) of the iLU - preconditioner. Lower values save memory at the cost of longer - execution times and a possible singular factorization. - - drop_tol : float, optional, default = 1e-4 - Sets the threshold for the magnitude of preconditioner - elements that should be dropped. Can be reduced for a courser - factorization at the cost of an increased number of iterations, and a - possible singular factorization. - - diag_pivot_thresh : float, optional, default = None - Sets the threshold between [0,1] for which diagonal - elements are considered acceptable pivot points when using a - preconditioner. A value of zero forces the pivot to be the diagonal - element. - - ILU_MILU : str, optional, default = 'smilu_2' - Selects the incomplete LU decomposition method algoithm used in - creating the preconditoner. Should only be used by advanced users. - - Returns - ------- - lu : object - Returns a SuperLU object representing iLU preconditioner. - - info : dict, optional - Dictionary containing solver-specific information. - """ - ss_args = _default_steadystate_args() - ss_args['method'] = 'iterative' - for key, value in kwargs.items(): - if key in ss_args: - ss_args[key] = value - else: - raise TypeError("Invalid keyword argument '" + key + - "' passed to steadystate.") - - # Set column perm to NATURAL if using RCM and not specified by user - if ss_args['use_rcm'] and ('permc_spec' not in kwargs): - ss_args['permc_spec'] = 'NATURAL' - - L = _steadystate_setup(A, c_op_list) - # Set weight parameter to max abs val in L if not set explicitly - if 'weight' not in kwargs.keys(): - ss_args['weight'] = np.abs(_data.norm.max(L.data)) - ss_args['info']['weight'] = ss_args['weight'] - - n = int(np.sqrt(L.shape[0])) - if ss_args['method'] == 'iterative': - ss_list = _steadystate_LU_liouvillian(L, ss_args) - L, _, _, _, ss_args = ss_list - elif ss_args['method'] == 'power': - ss_list = _steadystate_power_liouvillian(L, ss_args) - L, _, _, _, ss_args = ss_list - else: - raise ValueError("Invalid preconditioning method.") - - M, ss_args = _iterative_precondition(L, n, ss_args) - - if ss_args['return_info']: - return M, ss_args['info'] - else: - return M - - -def _pseudo_inverse_dense(L, rhoss, w=None, **pseudo_args): - """ - Internal function for computing the pseudo inverse of an Liouvillian using - dense matrix methods. See pseudo_inverse for details. - """ - rho_vec = np.transpose(stack_columns(rhoss.full())) - - tr_mat = tensor([identity(n) for n in L.dims[0][0]]) - tr_vec = np.transpose(stack_columns(tr_mat.full())) - N = np.prod(L.dims[0][0]) - I = np.identity(N * N) - P = np.kron(np.transpose(rho_vec), tr_vec) - Q = I - P - - if w is None: - L = L - else: - L = 1.0j*w*spre(tr_mat)+L - - if pseudo_args['method'] == 'direct': - try: - LIQ = np.linalg.solve(L.full(), Q) - except Exception: - LIQ = np.linalg.lstsq(L.full(), Q)[0] - - R = np.dot(Q, LIQ) - - return Qobj(R, dims=L.dims) - - elif pseudo_args['method'] == 'numpy': - return Qobj(np.dot(Q, np.dot(np.linalg.pinv(L.full()), Q)), - dims=L.dims) - - elif pseudo_args['method'] == 'scipy': - # return Qobj(la.pinv(L.full()), dims=L.dims) - return Qobj(np.dot(Q, np.dot(scipy.linalg.pinv(L.full()), Q)), - dims=L.dims) - - elif pseudo_args['method'] == 'scipy2': - # return Qobj(la.pinv2(L.full()), dims=L.dims) - return Qobj(np.dot(Q, np.dot(scipy.linalg.pinv2(L.full()), Q)), - dims=L.dims) - - else: - raise ValueError( - "Unsupported method '%s'. Use 'direct', 'numpy', 'scipy' or" - " 'scipy2'" % pseudo_args['method']) - - -def _pseudo_inverse_sparse(L, rhoss, w=None, **pseudo_args): - """ - Internal function for computing the pseudo inverse of an Liouvillian using - sparse matrix methods. See pseudo_inverse for details. - """ - - L = L.to(_data.CSR) - N = np.prod(L.dims[0][0]) - - rhoss_vec = operator_to_vector(rhoss) - - tr_op = tensor([identity(n) for n in L.dims[0][0]]) - tr_op_vec = operator_to_vector(tr_op) - - P = _data.kron(rhoss_vec.data, tr_op_vec.data.transpose(), dtype=_data.CSR) - I = _data.csr.identity(N * N) - Q = _data.sub(I, P) - - if w is None: - L = 1e-15j*spre(tr_op) + L - else: - if w != 0.0: - L = 1.0j*w*spre(tr_op) + L - else: - L = 1e-15j*spre(tr_op) + L - - if pseudo_args['use_rcm']: - perm = scipy.sparse.csgraph.reverse_cuthill_mckee(L.data.as_scipy()) - A = _data.permute.indices(L.data, perm, perm, dtype=_data.CSR) - Q = _data.permute.indices(Q, perm, perm, dtype=_data.CSR) - else: - if pseudo_args['solver'] == 'scipy': - A = L.data.as_scipy().tocsc() - A.sort_indices() - - if pseudo_args['method'] == 'splu': - if settings.has_mkl: - L.data.sort_indices() - A = L.data.as_scipy() - LIQ = mkl_spsolve(A, Q.to_array()) - else: - pspec = pseudo_args['permc_spec'] - diag_p_thresh = pseudo_args['diag_pivot_thresh'] - lu = splu(A, - permc_spec=pspec, - diag_pivot_thresh=diag_p_thresh, - options={'ILU_MILU': pseudo_args['ILU_MILU']}) - LIQ = lu.solve(Q.to_array()) - - elif pseudo_args['method'] == 'spilu': - lu = spilu(A, - permc_spec=pseudo_args['permc_spec'], - fill_factor=pseudo_args['fill_factor'], - drop_tol=pseudo_args['drop_tol']) - LIQ = lu.solve(Q.to_array()) - - else: - raise ValueError("unsupported method '%s'" % pseudo_args['method']) - - R = _data.matmul(Q, _data.create(LIQ)) - - if pseudo_args['use_rcm']: - rev_perm = np.argsort(perm) - R = _data.permute.indices(R, rev_perm, rev_perm) - - return Qobj(R, dims=L.dims) - - -def pseudo_inverse(L, rhoss=None, w=None, sparse=True, - method='splu', **kwargs): - """ - Compute the pseudo inverse for a Liouvillian superoperator, optionally - given its steady state density matrix (which will be computed if not - given). - - Returns - ------- - L : Qobj - A Liouvillian superoperator for which to compute the pseudo inverse. - - - rhoss : Qobj - A steadystate density matrix as Qobj instance, for the Liouvillian - superoperator L. - - w : double - 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 - Flag that indicate whether to use sparse or dense matrix methods when - computing the pseudo inverse. - - method : string - Name of method to use. For sparse=True, allowed values are 'spsolve', - 'splu' and 'spilu'. For sparse=False, allowed values are 'direct' and - 'numpy'. - - kwargs : dictionary - Additional keyword arguments for setting parameters for solver methods. - - Returns - ------- - 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. - - """ - pseudo_args = _default_steadystate_args() - for key, value in kwargs.items(): - if key in pseudo_args: - pseudo_args[key] = value - else: - raise TypeError( - "Invalid keyword argument '"+key+"' passed to pseudo_inverse.") - pseudo_args['method'] = method - - # Set column perm to NATURAL if using RCM and not specified by user - if pseudo_args['use_rcm'] and ('permc_spec' not in kwargs): - pseudo_args['permc_spec'] = 'NATURAL' - - if rhoss is None: - rhoss = steadystate(L, **pseudo_args) - - if sparse: - return _pseudo_inverse_sparse(L, rhoss, w=w, **pseudo_args) - else: - if pseudo_args['method'] != 'splu': - pseudo_args['method'] = pseudo_args['method'] - else: - pseudo_args['method'] = 'direct' - return _pseudo_inverse_dense(L, rhoss, w=w, **pseudo_args) diff --git a/qutip/solve/stochastic.py b/qutip/solve/stochastic.py index 7ee186e147..99829ae8ff 100644 --- a/qutip/solve/stochastic.py +++ b/qutip/solve/stochastic.py @@ -12,7 +12,7 @@ GenericSSolver, Solvers ) from .solver import Result, SolverOptions, _solver_safety_check -from ..parallel import serial_map +from .parallel import serial_map from ..ui.progressbar import TextProgressBar from qutip.core import data as _data @@ -178,19 +178,19 @@ class StochasticSolverOptions: Within the attribute list, a ``time_dependent_object`` is either - - :class:`~Qobj`: a constant term + - :class:`~qutip.Qobj`: a constant term - 2-element list of ``[Qobj, time_dependence]``: a time-dependent term where the ``Qobj`` will be multiplied by the time-dependent scalar. For more details on all allowed time-dependent objects, see the - documentation for :class:`~QobjEvo`. + documentation for :class:`~qutip.QobjEvo`. Attributes ---------- H : time_dependent_object or list of time_dependent_object System Hamiltonian in standard time-dependent list format. This is the - same as the argument that (e.g.) :func:`~mesolve` takes. If this is a - list of elements, they are summed. + same as the argument that (e.g.) :func:`~qutip.mesolve` takes. + If this is a list of elements, they are summed. state0 : :class:`qutip.Qobj` Initial state vector (ket) or density matrix. diff --git a/qutip/solver/__init__.py b/qutip/solver/__init__.py index efc4aa55aa..99d84ccd88 100644 --- a/qutip/solver/__init__.py +++ b/qutip/solver/__init__.py @@ -1,6 +1,18 @@ from .result import * from .options import * -from .integrator import * -from .ode import * +import qutip.solver.integrator +from .integrator import IntegratorException +from .sesolve import * +from .mesolve import * +from .mcsolve import * from .propagator import * from .scattering import * +from .correlation import * +from .spectrum import * +from .floquet import * +from .floquet_bwcomp import * +from .steadystate import * +from .countstat import * +from .brmesolve import * +from .krylovsolve import * +from .parallel import * diff --git a/qutip/solver/brmesolve.py b/qutip/solver/brmesolve.py index f686e0b529..7bfc8b27ad 100644 --- a/qutip/solver/brmesolve.py +++ b/qutip/solver/brmesolve.py @@ -66,7 +66,7 @@ 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 :class:`Coefficient`: ``spline = qutip.coefficient(array, tlist=times)`` Whether the ``a_ops`` is time dependent is decided by the type of @@ -245,15 +245,7 @@ def __init__(self, H, a_ops, c_ops=None, sec_cutoff=0.1, *, options=None): self.rhs = None self.sec_cutoff = sec_cutoff - self._options = _SolverOptions( - self.solver_options, - self._apply_options, - self.name, - self.__class__.options.__doc__ - ) - self._options.ode = {} - if options is not None: - self.options = options + self.options = options if not isinstance(H, (Qobj, QobjEvo)): raise TypeError("The Hamiltonian must be a Qobj or QobjEvo") @@ -366,5 +358,5 @@ def _apply_options(self, keys): self._integrator = self._get_integrator() self._integrator.set_state(*state) else: - self._integrator.options = new_options + self._integrator.options = self._options self._integrator.reset(hard=True) diff --git a/qutip/solver/correlation.py b/qutip/solver/correlation.py new file mode 100644 index 0000000000..b05514c519 --- /dev/null +++ b/qutip/solver/correlation.py @@ -0,0 +1,517 @@ +__all__ = [ + 'correlation_3op', + 'correlation_2op_1t', 'correlation_2op_2t', 'correlation_3op_1t', + 'correlation_3op_2t', 'coherence_function_g1', 'coherence_function_g2' +] + +import numpy as np +import scipy.fftpack + +from ..core import ( + qeye, Qobj, QobjEvo, liouvillian, spre, unstack_columns, stack_columns, + tensor, qzero, expect +) +from .mesolve import MESolver +from .mcsolve import MCSolver +from .brmesolve import BRSolver +from .heom.bofin_solvers import HEOMSolver + +from .steadystate import steadystate +from ..ui.progressbar import progess_bars + +# ----------------------------------------------------------------------------- +# PUBLIC API +# ----------------------------------------------------------------------------- + +# low level correlation + + +def correlation_2op_1t(H, state0, taulist, c_ops, a_op, b_op, + solver="me", reverse=False, args={}, + options={}): + r""" + Calculate the two-operator one-time correlation function: + :math:`\left` + along one time axis using the quantum regression theorem and the evolution + solver indicated by the `solver` parameter. + + Parameters + ---------- + + H : :class:`Qobj`, :class:`QobjEvo` + System Hamiltonian, may be time-dependent for solver choice of `me`. + state0 : :class:`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 + if ``c_ops`` are provided and the Hamiltonian is constant. + 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`} + List of collapse operators + a_op : :class:`Qobj`, :class:`QobjEvo` + Operator A. + b_op : :class:`Qobj`, :class:`QobjEvo` + Operator B. + reverse : bool {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"}``. + options : dict, optional + Options for the solver. Only used with `me` solver. + + Returns + ------- + corr_vec : ndarray + An array of correlation values for the times specified by `taulist`. + + See Also + -------- + :func:`correlation_3op` : + Similar function supporting various solver types. + + References + ---------- + See, Gardiner, Quantum Noise, Section 5.2. + + """ + solver = _make_solver(H, c_ops, args, options, solver) + + if reverse: + A_op, B_op, C_op = a_op, b_op, 1 + else: + A_op, B_op, C_op = 1, a_op, b_op + if state0 is None: + state0 = steadystate(H, c_ops) + + return correlation_3op(solver, state0, [0], taulist, A_op, B_op, C_op)[0] + + +def correlation_2op_2t(H, state0, tlist, taulist, c_ops, a_op, b_op, + solver="me", reverse=False, args={}, + options={}): + r""" + Calculate the two-operator two-time correlation function: + :math:`\left` + along two time axes using the quantum regression theorem and the + evolution solver indicated by the ``solver`` parameter. + + Parameters + ---------- + H : :class:`Qobj`, :class:`QobjEvo` + System Hamiltonian, may be time-dependent for solver choice of `me`. + state0 : :class:`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 + if ``c_ops`` are provided and the Hamiltonian is constant. + tlist : array_like + List of times for :math:`t`. tlist must be positive and contain the + element `0`. When taking steady-steady correlations only one ``tlist`` + value is necessary, i.e. when :math:`t \rightarrow \infty`. + If ``tlist`` is ``None``, ``tlist=[0]`` is assumed. + 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`} + List of collapse operators + a_op : :class:`Qobj`, :class:`QobjEvo` + Operator A. + b_op : :class:`Qobj`, :class:`QobjEvo` + Operator B. + reverse : bool {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"}``. + options : dict, optional + Options for the solver. Only used with `me` 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). + + See Also + -------- + :func:`correlation_3op` : + Similar function supporting various solver types. + + References + ---------- + See, Gardiner, Quantum Noise, Section 5.2. + + """ + solver = _make_solver(H, c_ops, args, options, solver) + if tlist is None: + tlist = [0] + if state0 is None: + state0 = steadystate(H, c_ops) + + if reverse: + A_op, B_op, C_op = a_op, b_op, 1 + else: + A_op, B_op, C_op = 1, a_op, b_op + + return correlation_3op(solver, state0, tlist, taulist, A_op, B_op, C_op) + + +def correlation_3op_1t(H, state0, taulist, c_ops, a_op, b_op, c_op, + solver="me", args={}, + options={}): + r""" + Calculate the three-operator two-time correlation function: + :math:`\left` along one time axis using the + quantum regression theorem and the evolution solver indicated by the + `solver` parameter. + + Note: it is not possibly to calculate a physically meaningful correlation + of this form where :math:`\tau<0`. + + Parameters + ---------- + H : :class:`Qobj`, :class:`QobjEvo` + System Hamiltonian, may be time-dependent for solver choice of `me`. + state0 : :class:`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 + if ``c_ops`` are provided and the Hamiltonian is constant. + 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`} + List of collapse operators + a_op : :class:`Qobj`, :class:`QobjEvo` + Operator A. + b_op : :class:`Qobj`, :class:`QobjEvo` + Operator B. + c_op : :class:`Qobj`, :class:`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"}``. + options : dict, optional + Options for the solver. Only used with `me` solver. + + Returns + ------- + corr_vec : array + An array of correlation values for the times specified by `taulist`. + + See Also + -------- + :func:`correlation_3op` : + Similar function supporting various solver types. + + References + ---------- + See, Gardiner, Quantum Noise, Section 5.2. + + """ + solver = _make_solver(H, c_ops, args, options, solver) + if state0 is None: + state0 = steadystate(H, c_ops) + return correlation_3op(solver, state0, [0], taulist, a_op, b_op, c_op)[0] + + +def correlation_3op_2t(H, state0, tlist, taulist, c_ops, a_op, b_op, c_op, + solver="me", args={}, + options={}): + r""" + Calculate the three-operator two-time correlation function: + :math:`\left` along two time axes using the + quantum regression theorem and the evolution solver indicated by the + `solver` parameter. + + Note: it is not possibly to calculate a physically meaningful correlation + of this form where :math:`\tau<0`. + + Parameters + ---------- + H : :class:`Qobj`, :class:`QobjEvo` + System Hamiltonian, may be time-dependent for solver choice of `me`. + state0 : :class:`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 + if ``c_ops`` are provided and the Hamiltonian is constant. + tlist : array_like + List of times for :math:`t`. tlist must be positive and contain the + element `0`. When taking steady-steady correlations only one tlist + value is necessary, i.e. when :math:`t \rightarrow \infty`. + If ``tlist`` is ``None``, ``tlist=[0]`` is assumed. + 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`} + List of collapse operators + a_op : :class:`Qobj`, :class:`QobjEvo` + Operator A. + b_op : :class:`Qobj`, :class:`QobjEvo` + Operator B. + c_op : :class:`Qobj`, :class:`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"}``. + options : dict, optional + 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). + + See Also + -------- + :func:`correlation_3op` : + Similar function supporting various solver types. + + References + ---------- + + See, Gardiner, Quantum Noise, Section 5.2. + + """ + solver = _make_solver(H, c_ops, args, options, solver) + + if tlist is None: + tlist = [0] + if state0 is None: + state0 = steadystate(H, c_ops) + + return correlation_3op(solver, state0, tlist, taulist, a_op, b_op, c_op) + + +# high level correlation + +def coherence_function_g1( + H, state0, taulist, c_ops, a_op, solver="me", args={}, options={} +): + r""" + Calculate the normalized first-order quantum coherence function: + + .. math:: + + g^{(1)}(\tau) = + \frac{\langle A^\dagger(\tau)A(0)\rangle} + {\sqrt{\langle A^\dagger(\tau)A(\tau)\rangle + \langle A^\dagger(0)A(0)\rangle}} + + using the quantum regression theorem and the evolution solver indicated by + the `solver` parameter. + + Parameters + ---------- + H : :class:`Qobj`, :class:`QobjEvo` + System Hamiltonian, may be time-dependent for solver choice of `me`. + state0 : :class:`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 + if ``c_ops`` are provided and the Hamiltonian is constant. + 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`} + List of collapse operators + a_op : :class:`Qobj`, :class:`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"}``. + options : dict, optional + Options for the solver. Only used with `me` solver. + + Returns + ------- + g1, G1 : tuple + The normalized and unnormalized second-order coherence function. + + """ + solver = _make_solver(H, c_ops, args, options, solver) + + # first calculate the photon number + if state0 is None: + state0 = steadystate(H, c_ops) + n = np.array([expect(state0, a_op.dag() * a_op)]) + else: + n = solver.run(state0, taulist, e_ops=[a_op.dag() * a_op]).expect[0] + + # calculate the correlation function G1 and normalize with n to obtain g1 + G1 = correlation_3op(solver, state0, [0], taulist, None, a_op.dag(), a_op)[0] + + g1 = G1 / np.sqrt(n[0] * np.array(n))[0] + return g1, G1 + + +def coherence_function_g2(H, state0, taulist, c_ops, a_op, solver="me", + args={}, options={}): + r""" + Calculate the normalized second-order quantum coherence function: + + .. math:: + + g^{(2)}(\tau) = + \frac{\langle A^\dagger(0)A^\dagger(\tau)A(\tau)A(0)\rangle} + {\langle A^\dagger(\tau)A(\tau)\rangle + \langle A^\dagger(0)A(0)\rangle} + + using the quantum regression theorem and the evolution solver indicated by + the `solver` parameter. + + Parameters + ---------- + H : :class:`Qobj`, :class:`QobjEvo` + System Hamiltonian, may be time-dependent for solver choice of `me`. + state0 : :class:`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 + if ``c_ops`` are provided and the Hamiltonian is constant. + taulist : array_like + List of times for :math:`\tau`. taulist must be positive and contain + the element `0`. + c_ops : list + List of collapse operators, may be time-dependent for solver choice of + `me`. + a_op : :class:`Qobj` + Operator A. + args : dict + 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"}``. + options : dict, optional + Options for the solver. Only used with `me` solver. + + Returns + ------- + g2, G2 : tuple + The normalized and unnormalized second-order coherence function. + + """ + solver = _make_solver(H, c_ops, args, options, solver) + + # first calculate the photon number + if state0 is None: + state0 = steadystate(H, c_ops) + n = np.array([expect(state0, a_op.dag() * a_op)]) + else: + n = solver.run(state0, taulist, e_ops=[a_op.dag() * a_op]).expect[0] + + # calculate the correlation function G2 and normalize with n to obtain g2 + G2 = correlation_3op(solver, state0, [0], taulist, + a_op.dag(), a_op.dag() * a_op, a_op)[0] + + g2 = G2 / (n[0] * np.array(n)) + return g2, G2 + + +def _make_solver(H, c_ops, args, options, solver): + H = QobjEvo(H, args=args) + c_ops = [QobjEvo(c_op, args=args) for c_op in c_ops] + if solver == "me": + solver_instance = MESolver(H, c_ops, options=options) + elif solver == "es": + options = {"method": "diag"} + solver_instance = MESolver(H, c_ops, options=options) + elif solver == "mc": + raise ValueError("MC solver for correlation has been removed") + return solver_instance + + +def correlation_3op(solver, state0, tlist, taulist, A=None, B=None, C=None): + r""" + Calculate the three-operator two-time correlation function: + + :math:`\left`. + + 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` + Qutip solver for an open system. + state0 : :class:`Qobj` + Initial state density matrix :math:`\rho(t_0)` or state vector + :math:`\psi(t_0)`. + tlist : array_like + List of times for :math:`t`. tlist must be positive and contain the + element `0`. + 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 + 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. + + Returns + ------- + 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 + is returned instead. + """ + taulist = np.asarray(taulist) + + dims = state0.dims[0] + A = QobjEvo(qeye(dims) if A in [None, 1] else A) + B = QobjEvo(qeye(dims) if B in [None, 1] else B) + C = QobjEvo(qeye(dims) if C in [None, 1] else C) + + if isinstance(solver, (MESolver, BRSolver)): + out = _correlation_3op_dm(solver, state0, tlist, taulist, A, B, C) + elif isinstance(solver, MCSolver): + raise TypeError("Monte Carlo support for correlation was removed. " + "Please, tell us on GitHub issues if you need it!") + elif isinstance(solver, HEOMSolver): + raise TypeError("HEOM is not supported by correlation. " + "Please, tell us on GitHub issues if you need it!") + else: + raise TypeError("Only solvers able to evolve density matrices" + " are supported.") + + return out + + +def _correlation_3op_dm(solver, state0, tlist, taulist, A, B, C): + old_opt = solver.options.copy() + try: + # We don't want to modify the solver + # TODO: Solver could have a ``with`` or ``copy``. + solver.options["normalize_output"] = False + solver.options["progress_bar"] = False + + progress_bar = progess_bars[old_opt['progress_bar']]() + progress_bar.start(len(taulist) + 1, **old_opt['progress_kwargs']) + rho_t = solver.run(state0, tlist).states + corr_mat = np.zeros([np.size(tlist), np.size(taulist)], dtype=complex) + progress_bar.update() + + for t_idx, rho in enumerate(rho_t): + t = tlist[t_idx] + corr_mat[t_idx, :] = solver.run( + C(t) @ rho @ A(t), + taulist + t, + e_ops=B + ).expect[0] + progress_bar.update() + progress_bar.finished() + + finally: + solver.options = old_opt + + return corr_mat diff --git a/qutip/solver/countstat.py b/qutip/solver/countstat.py index 9536f75ccf..58c1d135cf 100644 --- a/qutip/solver/countstat.py +++ b/qutip/solver/countstat.py @@ -13,7 +13,7 @@ operator_to_vector, vector_to_operator ) from ..core import data as _data -from qutip import pseudo_inverse, steadystate +from .steadystate import pseudo_inverse, steadystate from ..settings import settings # Load MKL spsolve if avaiable diff --git a/qutip/solver/floquet.py b/qutip/solver/floquet.py new file mode 100644 index 0000000000..7ce6cc4f11 --- /dev/null +++ b/qutip/solver/floquet.py @@ -0,0 +1,931 @@ +__all__ = [ + "FloquetBasis", + "floquet_tensor", + "fsesolve", + "fmmesolve", + "FMESolver", +] + +import numpy as np +from qutip.core import data as _data +from qutip import Qobj, QobjEvo +from .propagator import Propagator +from .mesolve import MESolver +from .solver_base import Solver +from .integrator import Integrator +from .result import Result +from time import time +from ..ui.progressbar import progess_bars + + +class FloquetBasis: + """ + Utility to compute floquet modes and states. + + Attributes + ---------- + U : :class:`Propagator` + The propagator of the Hamiltonian over one period. + + evecs : :class:`qutip.data.Data` + Matrix where each column is an initial Floquet mode. + + e_quasi : np.ndarray[float] + The quasi energies of the Hamiltonian. + """ + + def __init__( + self, + H, + T, + args=None, + options=None, + sparse=False, + sort=True, + precompute=None, + ): + """ + Parameters + ---------- + H : :class:`Qobj`, :class:`QobjEvo`, QobjEvo compatible format. + System Hamiltonian, with period `T`. + + T : float + Period of the Hamiltonian. + + args : None / *dictionary* + dictionary of parameters for time-dependent Hamiltonians and + collapse operators. + + options : dict [None] + Options used by sesolve to compute the floquet modes. + + sparse : bool [False] + Whether to use the sparse eigen solver when computing the + quasi-energies. + + sort : bool [True] + Whether to sort the quasi-energies. + + precompute : list [None] + If provided, a list of time at which to store the propagators + for later use when computing modes and states. Default is + ``linspace(0, T, 101)`` corresponding to the default integration + steps used for the floquet tensor computation. + """ + if not T > 0: + raise ValueError("The period need to be a positive number.") + self.T = T + if precompute is not None: + tlist = np.unique(np.atleast_1d(precompute) % self.T) + memoize = len(tlist) + if tlist[0] != 0: + memoize += 1 + if tlist[-1] != T: + memoize += 1 + else: + # Default computation + tlist = np.linspace(0, T, 101) + memoize = 101 + self.U = Propagator(H, args=args, options=options, memoize=memoize) + for t in tlist: + # Do the evolution by steps to save the intermediate results. + self.U(t) + U_T = self.U(self.T) + if not sparse and isinstance(U_T.data, _data.CSR): + U_T = U_T.to("Dense") + evals, evecs = _data.eigs(U_T.data) + e_quasi = -np.angle(evals) / T + if sort: + perm = np.argsort(e_quasi) + self.evecs = _data.permute.indices(evecs, col_perm=np.argsort(perm)) + self.e_quasi = e_quasi[perm] + else: + self.evecs = evecs + self.e_quasi = e_quasi + + def _as_ketlist(self, kets_mat): + """ + Split the Data array in a list of kets. + """ + dims = [self.U(0).dims[0], [1]] + return [ + Qobj(ket, dims=dims, type="ket") + for ket in _data.split_columns(kets_mat) + ] + + def mode(self, t, data=False): + """ + Calculate the Floquet modes at time ``t``. + + Parameters + ---------- + t : float + The time for which to evaluate the Floquet mode. + + data : bool [False] + Whether to return the states as a single data matrix or a list of + ket states. + + Returns + ------- + output : list[:class:`Qobj`], :class:`qutip.data.Data` + A list of Floquet states for the time ``t`` or the states as column + in a single matrix. + """ + t = t % self.T + if t == 0.0: + kets_mat = self.evecs + else: + U = self.U(t).data + phases = _data.diag(np.exp(1j * t * self.e_quasi)) + kets_mat = U @ self.evecs @ phases + if data: + return kets_mat + else: + return self._as_ketlist(kets_mat) + + def state(self, t, data=False): + """ + Evaluate the floquet states at time t. + + Parameters + ---------- + t : float + The time for which to evaluate the Floquet states. + + data : bool [False] + Whether to return the states as a single data matrix or a list of + ket states. + + Returns + ------- + output : list[:class:`Qobj`], :class:`qutip.data.Data` + A list of Floquet states for the time ``t`` or the states as column + in a single matrix. + """ + if t: + phases = _data.diag(np.exp(-1j * t * self.e_quasi)) + states_mat = self.mode(t, True) @ phases + else: + states_mat = self.evecs + if data: + return states_mat + else: + return self._as_ketlist(states_mat) + + def from_floquet_basis(self, floquet_basis, t=0): + """ + Transform a ket or density matrix from the Floquet basis at time ``t`` + to the lab basis. + + Parameters + ---------- + floquet_basis : :class:`Qobj`, :class:`qutip.data.Data` + Initial state in the Floquet basis at time ``t``. May be either a + ket or density matrix. + + t : float [0] + The time at which to evaluate the Floquet states. + + Returns + ------- + output : :class:`Qobj`, :class:`qutip.data.Data` + The state in the lab basis. The return type is the same as the type + of the input state. + """ + is_Qobj = isinstance(floquet_basis, Qobj) + if is_Qobj: + dims = floquet_basis.dims + floquet_basis = floquet_basis.data + if dims[0] != self.U(0).dims[1]: + raise ValueError( + "Dimensions of the state do not match the Hamiltonian" + ) + + state_mat = self.state(t, True) + lab_basis = state_mat @ floquet_basis + if floquet_basis.shape[1] != 1: + lab_basis = lab_basis @ state_mat.adjoint() + + if is_Qobj: + return Qobj(lab_basis, dims=dims) + return lab_basis + + def to_floquet_basis(self, lab_basis, t=0): + """ + Transform a ket or density matrix in the lab basis + to the Floquet basis at time ``t``. + + Parameters + ---------- + lab_basis : :class:`Qobj`, :class:`qutip.data.Data` + Initial state in the lab basis. + + t : float [0] + The time at which to evaluate the Floquet states. + + Returns + ------- + output : :class:`Qobj`, :class:`qutip.data.Data` + The state in the Floquet basis. The return type is the same as the + type of the input state. + """ + is_Qobj = isinstance(lab_basis, Qobj) + if is_Qobj: + dims = lab_basis.dims + lab_basis = lab_basis.data + if dims[0] != self.U(0).dims[1]: + raise ValueError( + "Dimensions of the state do not match the Hamiltonian" + ) + + state_mat = self.state(t, True) + floquet_basis = state_mat.adjoint() @ lab_basis + if lab_basis.shape[1] != 1: + floquet_basis = floquet_basis @ state_mat + + if is_Qobj: + return Qobj(floquet_basis, dims=dims) + return floquet_basis + + +def _floquet_delta_tensor(f_energies, kmax, T): + """ + Floquet-Markov master equation X matrices. + + Parameters + ---------- + f_energies : np.ndarray + The Floquet energies. + + kmax : int + The truncation of the number of sidebands (default 5). + + T : float + The period of the time-dependence of the Hamiltonian. + + Returns + ------- + delta : np.ndarray + Floquet delta tensor. + """ + delta = np.subtract.outer(f_energies, f_energies) + return np.add.outer(delta, np.arange(-kmax, kmax + 1) * (2 * np.pi / T)) + + +def _floquet_X_matrices(floquet_basis, c_ops, kmax, ntimes=100): + """ + Floquet-Markov master equation X matrices. + + Parameters + ---------- + floquet_basis : :class:`FloquetBasis` + The system Hamiltonian wrapped in a FloquetBasis object. + + c_ops : list of :class:`Qobj` + The collapse operators describing the dissipation. + + kmax : int + The truncation of the number of sidebands (default 5). + + ntimes : int [100] + The number of integration steps (for calculating X) within one period. + + Returns + ------- + X : list of dict of :class:`qutip.data.Data` + A dict of the sidebands ``k`` for the X matrices of each c_ops + """ + T = floquet_basis.T + N = floquet_basis.U(0).shape[0] + omega = (2 * np.pi) / T + tlist = np.linspace(T / ntimes, T, ntimes) + ks = np.arange(-kmax, kmax + 1) + out = {k: [_data.csr.zeros(N, N)] * len(c_ops) for k in ks} + + for t in tlist: + mode = floquet_basis.mode(t, data=True) + FFs = [mode.adjoint() @ c_op.data @ mode for c_op in c_ops] + for k, phi in zip(ks, np.exp(-1j * ks * omega * t) / ntimes): + out[k] = [ + _data.add(prev, new, phi) for prev, new in zip(out[k], FFs) + ] + + return [{k: out[k][i] for k in ks} for i in range(len(c_ops))] + + +def _floquet_gamma_matrices(X, delta, J_cb): + """ + Floquet-Markov master equation gamma matrices. + + Parameters + ---------- + X : list of dict of :class:`qutip.data.Data` + Floquet X matrices created by :func:`_floquet_X_matrices`. + + delta : np.ndarray + Floquet delta tensor created by :func:`_floquet_delta_tensor`. + + J_cb : list of callables + A list callback functions that compute the noise power spectrum as + a function of frequency. The list should contain one callable for each + collapse operator `c_op`, in the same order as the elements of `X`. + Each callable should accept a numpy array of frequencies and return a + numpy array of corresponding noise power. + + Returns + ------- + gammas : dict of :class:`qutip.data.Data` + A dict mapping the sidebands ``k`` to their gamma matrices. + """ + N = delta.shape[0] + kmax = (delta.shape[2] - 1) // 2 + gamma = {k: _data.csr.zeros(N, N) for k in range(-kmax, kmax + 1, 1)} + + for X_c_op, sp in zip(X, J_cb): + response = sp(delta) * ((2 + 0j) * np.pi) + response = [ + _data.Dense(response[:, :, k], copy=False) + for k in range(2 * kmax + 1) + ] + for k in range(-kmax, kmax + 1, 1): + gamma[k] = _data.add( + gamma[k], + _data.multiply( + _data.multiply(X_c_op[k].conj(), X_c_op[k]), + response[k + kmax], + ), + ) + return gamma + + +def _floquet_A_matrix(delta, gamma, w_th): + """ + Floquet-Markov master equation rate matrix. + + Parameters + ---------- + delta : np.ndarray + Floquet delta tensor created by :func:`_floquet_delta_tensor`. + + gamma : dict of :class:`qutip.data.Data` + Floquet gamma matrices created by :func:`_floquet_gamma_matrices`. + + w_th : float + The temperature in units of frequency. + """ + kmax = (delta.shape[2] - 1) // 2 + + if w_th > 0.0: + deltap = np.copy(delta) + deltap[deltap == 0.0] = np.inf + thermal = 1.0 / (np.exp(np.abs(deltap) / w_th) - 1.0) + thermal = [_data.Dense(thermal[:, :, k]) for k in range(2 * kmax + 1)] + + gamma_kk = _data.add(gamma[0], gamma[0].transpose()) + A = _data.add(gamma[0], _data.multiply(thermal[kmax], gamma_kk)) + + for k in range(1, kmax + 1): + g_kk = _data.add(gamma[k], gamma[-k].transpose()) + thermal_kk = _data.multiply(thermal[kmax + k], g_kk) + A = _data.add(A, _data.add(gamma[k], thermal_kk)) + thermal_kk = _data.multiply(thermal[kmax - k], g_kk.transpose()) + A = _data.add(A, _data.add(gamma[-k], thermal_kk)) + else: + # w_th is 0, thermal = 0s + A = gamma[0] + for k in range(1, kmax + 1): + A = _data.add(gamma[k], A) + A = _data.add(gamma[-k], A) + + return A + + +def _floquet_master_equation_tensor(A): + """ + Construct a tensor that represents the master equation in the floquet + basis (with constant Hamiltonian and collapse operators?). + + Simplest RWA approximation [Grifoni et al, Phys.Rep. 304 229 (1998)] + + Parameters + ---------- + A : :class:`qutip.data.Data` + Floquet-Markov master equation rate matrix. + + Returns + ------- + output : array + The Floquet-Markov master equation tensor `R`. + """ + N = A.shape[0] + + # R[i+N*i, j+N*j] = A[j, i] + cols = np.arange(N, dtype=np.int32) + rows = np.linspace(1 - 1 / (N + 1), N, N**2 + 1, dtype=np.int32) + data = np.ones(N, dtype=complex) + expand = _data.csr.CSR((data, cols, rows), shape=(N**2, N)) + + R = expand @ A.transpose() @ expand.transpose() + + # S[i+N*j, j+N*i] = -1/2 * sum_k(A[i, k] + A[j, k]) + ket_1 = _data.Dense(np.ones(N, dtype=complex)) + Asum = A @ ket_1 + to_super = _data.add(_data.kron(Asum, ket_1), _data.kron(ket_1, Asum)) + S = _data.diag(to_super.to_array().flatten() * -0.5, 0) + + return _data.add(R, S) + + +def floquet_tensor(H, c_ops, spectra_cb, T=0, w_th=0.0, kmax=5, nT=100): + """ + Construct a tensor that represents the master equation in the floquet + basis. + + Simplest RWA approximation [Grifoni et al, Phys.Rep. 304 229 (1998)] + + Parameters + ---------- + H : :class:`QobjEvo` + Periodic Hamiltonian + + T : float + The period of the time-dependence of the hamiltonian. + + c_ops : list of :class:`qutip.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 + The temperature in units of frequency. + + kmax : int + The truncation of the number of sidebands (default 5). + + Returns + ------- + output : array + The Floquet-Markov master equation tensor `R`. + """ + if isinstance(H, FloquetBasis): + floquet_basis = H + T = H.T + else: + floquet_basis = FloquetBasis(H, T) + energy = floquet_basis.e_quasi + delta = _floquet_delta_tensor(energy, kmax, T) + x = _floquet_X_matrices(floquet_basis, c_ops, kmax, nT) + gamma = _floquet_gamma_matrices(x, delta, spectra_cb) + 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 + ) + + +def fsesolve(H, psi0, tlist, e_ops=None, T=0.0, args=None, options=None): + """ + Solve the Schrodinger equation using the Floquet formalism. + + 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. + + psi0 : :class:`qutip.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 + 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. + + T : float, default=tlist[-1] + The period of the time-dependence of the hamiltonian. + + args : dictionary, optional + Dictionary with variables required to evaluate H. + + 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. + + Returns + ------- + output : :class:`qutip.solver.Result` + An instance of the class :class:`qutip.solver.Result`, which + contains either an *array* of expectation values or an array of + state vectors, for the times specified by `tlist`. + """ + if isinstance(H, FloquetBasis): + floquet_basis = H + else: + T = T or tlist[-1] + # `fsesolve` is a fallback from `fmmesolve`, for the later, options + # are for the open system evolution. + floquet_basis = FloquetBasis(H, T, args, precompute=tlist) + + f_coeff = floquet_basis.to_floquet_basis(psi0) + result_options = { + "store_final_state": False, + "store_states": None, + "normalize_output": True, + } + result_options.update(options or {}) + result = Result(e_ops, result_options, solver="fsesolve") + for t in tlist: + state_t = floquet_basis.from_floquet_basis(f_coeff, t) + result.add(t, state_t) + + return result + + +def fmmesolve( + H, + rho0, + tlist, + c_ops=None, + e_ops=None, + spectra_cb=None, + T=0, + w_th=0.0, + args=None, + options=None, +): + """ + Solve the dynamics for the system using the Floquet-Markov master equation. + + 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. + + rho0 / psi0 : :class:`qutip.Qobj` + Initial density matrix or state vector (ket). + + tlist : *list* / *array* + List of times for :math:`t`. + + c_ops : list of :class:`qutip.Qobj` + List of collapse operators. Time dependent collapse operators are not + supported. + + e_ops : list of :class:`qutip.Qobj` / callback function + 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 + List of callback functions that compute the noise power spectrum as + a function of frequency for the collapse operators in `c_ops`. + + T : float + The period of the time-dependence of the hamiltonian. The default value + 'None' indicates that the 'tlist' spans a single period of the driving. + + w_th : float + 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: + + temperature = 25e-3 # unit K + h = 6.626e-34 + kB = 1.38e-23 + args['w_th'] = temperature * (kB / h) * 2 * pi * 1e-9 + + args : *dictionary* + Dictionary of parameters for time-dependent Hamiltonian + + options : None / dict + 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. + + Other options could be supported depending on the integration method, + see `Integrator <./classes.html#classes-ode>`_. + + Returns + ------- + result: :class:`qutip.Result` + + An instance of the class :class:`qutip.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: + return fsesolve( + H, + rho0, + tlist, + e_ops=e_ops, + T=T, + w_th=w_th, + args=args, + options=options, + ) + + if isinstance(H, FloquetBasis): + floquet_basis = H + else: + T = T or tlist[-1] + t_precompute = np.concatenate([tlist, np.linspace(0, T, 101)]) + # `fsesolve` is a fallback from `fmmesolve`, for the later, options + # are for the open system evolution. + floquet_basis = FloquetBasis(H, T, args, precompute=t_precompute) + + if not w_th and args: + w_th = args.get("w_th", 0.0) + + if isinstance(c_ops, Qobj): + c_ops = [c_ops] + + if spectra_cb is None: + spectra_cb = [lambda w: (w > 0)] + elif callable(spectra_cb): + spectra_cb = [spectra_cb] + if len(spectra_cb) == 1: + spectra_cb = spectra_cb * len(c_ops) + + a_ops = list(zip(c_ops, spectra_cb)) + + solver = FMESolver(floquet_basis, a_ops, w_th=w_th, options=options) + return solver.run(rho0, tlist, e_ops=e_ops) + + +class FloquetResult(Result): + def _post_init(self, floquet_basis): + self.floquet_basis = floquet_basis + if self.options["store_floquet_states"]: + self.floquet_states = [] + else: + self.floquet_states = None + super()._post_init() + + def add(self, t, state): + if self.options["store_floquet_states"]: + self.floquet_states.append(state) + + state = self.floquet_basis.from_floquet_basis(state, t) + super().add(t, state) + + +class FMESolver(MESolver): + """ + Solver for the Floquet-Markov master equation. + + .. note :: + Operators (``c_ops`` and ``e_ops``) are in the laboratory basis. + + Parameters + ---------- + floquet_basis : :class:`qutip.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) + List of collapse operators and the corresponding function for the noise + power spectrum. The collapse operator must be a :class:`Qobj` and + cannot be time dependent. The spectrum function must take and return + an numpy array. + + w_th : float + The temperature of the environment in units of Hamiltonian frequency. + + kmax : int [5] + The truncation of the number of sidebands.. + + nT : int [20*kmax] + The number of integration steps (for calculating X) within one period. + + options : dict, optional + Options for the solver, see :obj:`FMESolver.options` and + `Integrator <./classes.html#classes-ode>`_ for a list of all options. + """ + + name = "fmmesolve" + _avail_integrators = {} + resultclass = FloquetResult + solver_options = { + "progress_bar": "text", + "progress_kwargs": {"chunk_size": 10}, + "store_final_state": False, + "store_states": None, + "normalize_output": True, + "method": "adams", + "store_floquet_states": False, + } + + def __init__( + self, floquet_basis, a_ops, w_th=0.0, *, kmax=5, nT=None, options=None + ): + self.options = options + if isinstance(floquet_basis, FloquetBasis): + self.floquet_basis = floquet_basis + else: + raise TypeError("The ``floquet_basis`` must be a FloquetBasis") + + nT = nT or max(100, 20 * kmax) + self._num_collapse = len(a_ops) + c_ops, spectra_cb = zip(*a_ops) + if not all( + isinstance(c_op, Qobj) and callable(spectrum) + for c_op, spectrum in a_ops + ): + raise TypeError("a_ops must be tuple of (Qobj, callable)") + self.rhs = QobjEvo( + floquet_tensor( + self.floquet_basis, + c_ops, + spectra_cb, + w_th=w_th, + kmax=kmax, + nT=nT, + ) + ) + + self._integrator = self._get_integrator() + self._state_metadata = {} + self.stats = self._initialize_stats() + + def _initialize_stats(self): + stats = Solver._initialize_stats(self) + stats.update( + { + "solver": "Floquet-Markov master equation", + "num_collapse": self._num_collapse, + } + ) + return stats + + def _argument(self, args): + if args: + raise ValueError("FMESolver cannot update arguments") + + def start(self, state0, t0, *, floquet=False): + """ + Set the initial state and time for a step evolution. + ``options`` for the evolutions are read at this step. + + Parameters + ---------- + state0 : :class:`Qobj` + Initial state of the evolution. + + t0 : double + Initial time of the evolution. + + floquet : bool, optional {False} + Whether the initial state is in the floquet basis or laboratory + basis. + """ + if not floquet: + state0 = self.floquet_basis.to_floquet_basis(state0, t0) + super().start(state0, t0) + + def step(self, t, *, args=None, copy=True, floquet=False): + """ + Evolve the state to ``t`` and return the state as a :class:`Qobj`. + + Parameters + ---------- + t : double + Time to evolve to, must be higher than the last call. + + copy : bool, optional {True} + Whether to return a copy of the data or the data in the ODE solver. + + floquet : bool, optional {False} + Whether to return the state in the floquet basis or laboratory + basis. + + 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. + """ + if args: + raise ValueError("FMESolver cannot update arguments") + state = super().step(t) + if not floquet: + state = self.floquet_basis.from_floquet_basis(state, t) + elif copy: + state = state.copy() + return state + + def run(self, state0, tlist, *, floquet=False, args=None, e_ops=None): + """ + Calculate the evolution of the quantum system. + + 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 + stored results are determined by ``options``. + + Parameters + ---------- + state0 : :class:`Qobj` + Initial state of the evolution. + + tlist : list of double + Time for which to save the results (state and/or expect) of the + evolution. The first element of the list is the initial time of the + evolution. Each times of the list must be increasing, but does not + need to be uniformy distributed. + + floquet : bool, optional {False} + Whether the initial state in the floquet basis or laboratory basis. + + args : dict, optional {None} + Not supported + + e_ops : list {None} + List of Qobj, QobjEvo or callable to compute the expectation + values. Function[s] must have the signature + f(t : float, state : Qobj) -> expect. + + Returns + ------- + results : :class:`qutip.solver.FloquetResult` + Results of the evolution. States and/or expect will be saved. You + can control the saved data in the options. + """ + if args: + raise ValueError("FMESolver cannot update arguments") + if not floquet: + state0 = self.floquet_basis.to_floquet_basis(state0, tlist[0]) + _time_start = time() + _data0 = self._prepare_state(state0) + self._integrator.set_state(tlist[0], _data0) + stats = self._initialize_stats() + results = self.resultclass( + e_ops, + self.options, + solver=self.name, + stats=stats, + floquet_basis=self.floquet_basis, + ) + results.add(tlist[0], self._restore_state(_data0, copy=False)) + stats["preparation time"] += time() - _time_start + + progress_bar = progess_bars[self.options["progress_bar"]]() + progress_bar.start(len(tlist) - 1, **self.options["progress_kwargs"]) + for t, state in self._integrator.run(tlist): + progress_bar.update() + results.add(t, self._restore_state(state, copy=False)) + progress_bar.finished() + + stats["run time"] = progress_bar.total_time() + # TODO: It would be nice if integrator could give evolution statistics + # stats.update(_integrator.stats) + return results diff --git a/qutip/solver/floquet_bwcomp.py b/qutip/solver/floquet_bwcomp.py new file mode 100644 index 0000000000..8ee683ffd8 --- /dev/null +++ b/qutip/solver/floquet_bwcomp.py @@ -0,0 +1,244 @@ +""" Floquet solver compatibility functions that behave like the corresponding +functions from QuTiP 4.7. + +These functions are indented to be used when porting code from QuTiP 4.7 to +QuTiP 5. They are deprecated and will be removed in QuTiP 5.1. +""" + +__all__ = [ + "floquet_modes", + "floquet_modes_t", + "floquet_modes_table", + "floquet_modes_t_lookup", + "floquet_states", + "floquet_states_t", + "floquet_wavefunction", + "floquet_wavefunction_t", + "floquet_state_decomposition", + "floquet_master_equation_rates", +] + +from .floquet import * +import numpy as np +import warnings + + +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: + + fbasis = FloquetBasis(H, T, args=args, options=options, sort=sort) + f_mode_0 = fbasis.mode(0) + f_energies = fbasis.e_quasi + """ + warnings.warn(FutureWarning( + "`floquet_modes` is deprecated. Use `FloquetBasis.mode` instead." + )) + fbasis = FloquetBasis(H, T, args=args, options=options, sort=sort) + f_mode_0 = fbasis.mode(0) + f_energies = fbasis.e_quasi + return f_mode_0, f_energies + + +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: + + fbasis = FloquetBasis(H, T, args=args, options=options) + f_mode_t = fbasis.mode(t) + """ + warnings.warn(FutureWarning( + "`floquet_modes_t` is deprecated. Use `FloquetBasis.mode` instead." + )) + fbasis = FloquetBasis(H, T, args=args, options=options) + return fbasis.mode(t) + + +def floquet_modes_table( + f_modes_0, f_energies, tlist, H, T, args=None, options=None +): + """ + Pre-calculate the Floquet modes for a range of times spanning the floquet + 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: + + fbasis = FloquetBasis(H, T, args=args, options=options, precompute=tlist) + """ + raise NotImplementedError("`floquet_modes_table` is deprecated.") + + +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: + + f_modes_table_t = fbasis = FloquetBasis(...) + f_mode_t = f_modes_table_t.mode(t) + """ + raise NotImplementedError( + "`floquet_modes_t_lookup` is no longer provided. " + "Use `FloquetBasis` instead." + ) + + +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: + + fbasis = FloquetBasis(H, T, args=args, options=options) + f_state_t = fbasis.state(t) + """ + warnings.warn(FutureWarning( + "`floquet_states` is deprecated. " + "Use `FloquetBasis.state` instead." + )) + return [ + (f_modes_t[i] * np.exp(-1j * f_energies[i] * t)) + for i in np.arange(len(f_energies)) + ] + + +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: + + fbasis = FloquetBasis(H, T, args=args, options=options) + f_state_t = fbasis.state(t) + """ + warnings.warn(FutureWarning( + "`floquet_states_t` is deprecated. " + "Use `FloquetBasis.state` instead." + )) + fbasis = FloquetBasis(H, T, args=args, options=options) + return fbasis.state(t) + + +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: + + fbasis = FloquetBasis(H, T, args=args, options=options) + psi_t = fbasis.from_floquet_basis(f_coeff, t) + """ + raise NotImplementedError( + "`floquet_wavefunction` is not longer provided. " + "Use `FloquetBasis.from_floquet_basis` instead." + ) + + +def floquet_wavefunction_t( + f_modes_0, f_energies, f_coeff, t, H, T, args=None, options=None +): + """ + 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: + + fbasis = FloquetBasis(H, T, args=args, options=options) + psi_t = fbasis.from_floquet_basis(f_coeff, t) + """ + warnings.warn(FutureWarning( + "`floquet_wavefunction_t` is deprecated. " + "Use `FloquetBasis.from_floquet_basis` instead." + )) + fbasis = FloquetBasis(H, T, args=args, options=options) + return fbasis.from_floquet_basis(f_coeff, t) + + +def floquet_state_decomposition(f_states, f_energies, psi): + r""" + 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: + + fbasis = FloquetBasis(H, T, args=args, options=options) + f_coeff = fbasis.to_floquet_basis(psi) + """ + raise NotImplementedError( + "`floquet_state_decomposition` is deprecated. " + "Use `FloquetBasis.to_floquet_basis` instead." + ) + + +def floquet_master_equation_rates( + f_modes_0, + f_energies, + c_op, + H, + T, + args, + J_cb, + w_th, + kmax=5, + f_modes_table_t=None, +): + """ + Calculate the rates and matrix elements for the Floquet-Markov master + equation. + + .. note :: + + Deprecated. For the Floquet-Markov master equation's tensor, use + :func:`floquet_tensor`. For the rates matrices, use + :func:`floquet_delta_tensor`, :func:`floquet_X_matrices`, + :func:`floquet_gamma_matrices` and/or s:func:`floquet_A_matrix`. + + Parameters + ---------- + f_modes_0 : Any + No longer used. + f_energies : Any + No longer used. + c_op : :class:`qutip.qobj` + The collapse operators describing the dissipation. + H : :class:`qutip.qobj` + System Hamiltonian, time-dependent with period `T`. + T : float + The period of the time-dependence of the hamiltonian. + args : dictionary + Dictionary with variables required to evaluate H. + J_cb : callback functions + A callback function that computes the noise power spectrum, as + a function of frequency, associated with the collapse operator `c_op`. + w_th : float + The temperature in units of frequency. + kmax : int, default=5 + The truncation of the number of sidebands. + f_modes_table_t : Any + No longer used. + + Returns + ------- + output : list + A list (Delta, X, Gamma, A) containing the matrices Delta, X, Gamma + and A used in the construction of the Floquet-Markov master equation. + """ + warnings.warn( + FutureWarning("`floquet_master_equation_rates` is deprecated.") + ) + floquet_basis = FloquetBasis(H, T, args=args) + energy = floquet_basis.e_quasi + delta = floquet_delta_tensor(energy, kmax, T) + x = floquet_X_matrices(floquet_basis, [c_op], kmax, nT) + gamma = floquet_gamma_matrices(x, delta, [J_cb]) + a = floquet_A_matrix(delta, gamma, w_th) + return delta, x[0], gamma, a diff --git a/qutip/solver/heom/bofin_solvers.py b/qutip/solver/heom/bofin_solvers.py index 171718a346..b029dea9aa 100644 --- a/qutip/solver/heom/bofin_solvers.py +++ b/qutip/solver/heom/bofin_solvers.py @@ -16,7 +16,7 @@ import scipy.sparse as sp from scipy.sparse.linalg import spsolve -from qutip import settings +from qutip.settings import settings from qutip import state_number_enumerate from qutip.core import data as _data from qutip.core.data import csr as _csr @@ -318,7 +318,7 @@ class HierarchyADOsState: Parameters ---------- - rho : :class:`Qobj` + rho : :class:`~qutip.Qobj` The current state of the system (i.e. the 0th component of the hierarchy). ados : :class:`HierarchyADOs` @@ -362,7 +362,7 @@ def extract(self, idx_or_label): Returns ------- Qobj - A :obj:`Qobj` representing the state of the specified ADO. + A :obj:`~qutip.Qobj` representing the state of the specified ADO. """ if isinstance(idx_or_label, int): idx = idx_or_label @@ -655,6 +655,16 @@ def __init__(self, H, bath, max_depth, *, options=None): super().__init__(rhs, options=options) + @property + def sys_dims(self): + """ + Dimensions of the space that the system use, excluding any environment: + + ``qutip.basis(sovler.dims)`` will create a state with proper dimensions + for this solver. + """ + return self._sys_dims + def _initialize_stats(self): stats = super()._initialize_stats() stats.update({ diff --git a/qutip/solver/ode/__init__.py b/qutip/solver/integrator/__init__.py similarity index 57% rename from qutip/solver/ode/__init__.py rename to qutip/solver/integrator/__init__.py index b22087ab66..987cfcd545 100644 --- a/qutip/solver/ode/__init__.py +++ b/qutip/solver/integrator/__init__.py @@ -1,2 +1,4 @@ +from .integrator import * from .scipy_integrator import * from .qutip_integrator import * +from .krylov import * diff --git a/qutip/solver/ode/explicit_rk.pxd b/qutip/solver/integrator/explicit_rk.pxd similarity index 100% rename from qutip/solver/ode/explicit_rk.pxd rename to qutip/solver/integrator/explicit_rk.pxd diff --git a/qutip/solver/ode/explicit_rk.pyx b/qutip/solver/integrator/explicit_rk.pyx similarity index 100% rename from qutip/solver/ode/explicit_rk.pyx rename to qutip/solver/integrator/explicit_rk.pyx diff --git a/qutip/solver/integrator.py b/qutip/solver/integrator/integrator.py similarity index 99% rename from qutip/solver/integrator.py rename to qutip/solver/integrator/integrator.py index 71e0dd4137..44df5546f1 100644 --- a/qutip/solver/integrator.py +++ b/qutip/solver/integrator/integrator.py @@ -167,7 +167,6 @@ def mcstep(self, t, copy=True): ) return self.integrate(t, copy) - def get_state(self, copy=True): """ Obtain the state of the solver as a pair (t, state). diff --git a/qutip/solver/integrator/krylov.py b/qutip/solver/integrator/krylov.py new file mode 100644 index 0000000000..d9cf5757ed --- /dev/null +++ b/qutip/solver/integrator/krylov.py @@ -0,0 +1,238 @@ +from ..integrator import IntegratorException, Integrator +import numpy as np +from qutip.core import data as _data +from scipy.optimize import root_scalar +from ..sesolve import SESolver + + +__all__ = ["IntegratorKrylov"] + + +class IntegratorKrylov(Integrator): + """ + Evolve the state vector ("psi0") finding an approximation for the time + evolution operator of Hamiltonian ("H") by obtaining the projection of + the time evolution operator on a set of small dimensional Krylov + subspaces (m << dim(H)). + """ + integrator_options = { + 'atol': 1e-7, + 'nsteps': 100, + 'min_step': 1e-5, + 'max_step': 1e5, + 'krylov_dim': 0, + 'sub_system_tol': 1e-7, + 'always_compute_step': False, + } + support_time_dependant = False + supports_blackbox = False + method = 'krylov' + + def _prepare(self): + if not self.system.isconstant: + raise ValueError("krylov method only support constant system.") + self._max_step = -np.inf + krylov_dim = self.options["krylov_dim"] + if krylov_dim < 0 or krylov_dim > self.system.shape[0]: + raise ValueError("The options 'krylov_dim', must be a positive " + "integer smaller that the system size") + + if krylov_dim == 0: + # TODO: krylov_dim, max_step and error (atol) are related by + # err ~= exp(-krylov_dim / dt**(1~2)) + # We could ask for 2 and determine the third one. + N = self.system.shape[0] + krylov_dim = min(int((N + 100)**0.5), N-1) + self.options["krylov_dim"] = krylov_dim + + if not self.options["always_compute_step"]: + from qutip import rand_ket + N = self.system.shape[0] + krylov_tridiag, krylov_basis = \ + self._lanczos_algorithm(rand_ket(N).data) + if ( + krylov_tridiag.shape[0] < self.options["krylov_dim"] + or krylov_tridiag.shape[0] == N + ): + self._max_step = np.inf + else: + self._max_step = self._compute_max_step(krylov_tridiag, + krylov_basis) + + def _lanczos_algorithm(self, psi): + """ + Computes a basis of the Krylov subspace for Hamiltonian 'H', a system + state 'psi' and Krylov dimension 'krylov_dim'. The space is spanned + by {psi, H psi, H^2 psi, ..., H^(krylov_dim) psi}. + + Parameters + ------------ + psi: np.ndarray + State used to calculate Krylov subspace. + """ + krylov_dim = self.options['krylov_dim'] + H = (1j * self.system(0)).data + + v = [] + T_diag = np.zeros(krylov_dim + 1, dtype=complex) + T_subdiag = np.zeros(krylov_dim + 1, dtype=complex) + + w_prime = _data.matmul(H, psi) + T_diag[0] = _data.inner(w_prime, psi) + v.append(psi) + w = _data.add(w_prime, v[-1], -T_diag[0]) + T_subdiag[0] = _data.norm.l2(w) + j = 0 + + while j < krylov_dim and T_subdiag[j] > self.options['sub_system_tol']: + j += 1 + v.append(_data.mul(w, 1 / T_subdiag[j-1])) + w_prime = _data.matmul(H, v[-1]) + T_diag[j] = _data.inner(w_prime, v[-1]) + w = _data.add(w_prime, v[-1], -T_diag[j]) + w = _data.add(w, v[-2], -T_subdiag[j-1]) + T_subdiag[j] = _data.norm.l2(w) + + krylov_tridiag = _data.diag["dense"]( + [T_subdiag[:j], T_diag[:j+1], T_subdiag[:j]], + [-1, 0, 1] + ) + krylov_basis = _data.Dense(np.hstack([psi.to_array() for psi in v])) + + return krylov_tridiag, krylov_basis + + def _compute_krylov_set(self, krylov_tridiag, krylov_basis): + """ + Compute the eigen energies, basis transformation operator (U) and e0. + """ + eigenvalues, eigenvectors = _data.eigs(krylov_tridiag, True) + N = eigenvalues.shape[0] + U = _data.matmul(krylov_basis, eigenvectors) + e0 = eigenvectors.adjoint() @ _data.one_element_dense((N, 1), (0, 0), 1.0) + return eigenvalues, U, e0 + + def _compute_psi(self, dt, eigenvalues, U, e0): + """ + compute the state at time ``t``. + """ + phases = _data.Dense(np.exp(-1j * dt * eigenvalues)) + aux = _data.multiply(phases, e0) + return _data.matmul(U, aux) + + def _compute_max_step(self, krylov_tridiag, krylov_basis, krylov_state=None): + """ + Compute the maximum step length to stay under the desired tolerance. + """ + if not krylov_state: + krylov_state = self._compute_krylov_set(krylov_tridiag, krylov_basis) + + small_tridiag = _data.Dense(krylov_tridiag.as_ndarray()[:-1, :-1]) + small_basis = _data.Dense(krylov_basis.as_ndarray()[:, :-1]) + reduced_state = self._compute_krylov_set(small_tridiag, small_basis) + + def krylov_error(t): + # we divide by atol and take the log so that the error returned is 0 + # at atol, which is convenient for calling root_scalar with. + return np.log(_data.norm.l2( + self._compute_psi(t, *krylov_state) - + self._compute_psi(t, *reduced_state) + ) / self.options["atol"]) + + dt = self.options["min_step"] + err = krylov_error(dt) + if err > 0: + ValueError( + f"With the krylov dim of {self.options['krylov_dim']}, the " + f"error with the minimum step {dt} is {err}, higher than the " + f"desired tolerance of {self.options['atol']}." + ) + + while krylov_error(dt * 10) < 0 and dt < self.options["max_step"]: + dt *= 10 + + if dt > self.options["max_step"]: + return self.options["max_step"] + + sol = root_scalar(f=krylov_error, bracket=[dt, dt * 10], + method="brentq", xtol=self.options['atol']) + if sol.converged: + return sol.root + else: + return dt + + def set_state(self, t, state0): + self._t_0 = t + krylov_tridiag, krylov_basis = self._lanczos_algorithm(state0) + self._krylov_state = self._compute_krylov_set(krylov_tridiag, krylov_basis) + + if ( + krylov_tridiag.shape[0] <= self.options['krylov_dim'] + or krylov_tridiag.shape == self.system.shape + ): + # happy_breakdown + self._max_step = np.inf + return + + if ( + not np.isfinite(self._max_step) + or self.options["always_compute_step"] + ): + self._max_step = self._compute_max_step( + krylov_tridiag, krylov_basis, self._krylov_state, + ) + + def get_state(self, copy=True): + return self._t_0, self._compute_psi(0, *self._krylov_state) + + def integrate(self, t, copy=True): + step = 0 + while t > self._t_0 + self._max_step: + # The approximation in only valid in the range t_0, t_0 + max step + # If outside, advance the range + step += 1 + if step >= self.options["nsteps"]: + raise IntegratorException(f"Maximum number of integration steps ({self.options['nsteps']}) exceeded") + new_psi = self._compute_psi(self._max_step, *self._krylov_state) + self.set_state(self._t_0 + self._max_step, new_psi) + + delta_t = t - self._t_0 + out = self._compute_psi(delta_t, *self._krylov_state) + return t, out + + @property + def options(self): + """ + Supported options by krylov method: + + atol : float, default=1e-7 + Absolute tolerance. + + nsteps : int, default=100 + Max. number of internal steps/call. + + min_step, max_step : float, default=(1e-5, 1e5) + Minimum and maximum step size. + + 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 + 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 + 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. + """ + return self._options + + @options.setter + def options(self, new_options): + Integrator.options.fset(self, new_options) + + +SESolver.add_integrator(IntegratorKrylov, 'krylov') diff --git a/qutip/solver/ode/qutip_integrator.py b/qutip/solver/integrator/qutip_integrator.py similarity index 91% rename from qutip/solver/ode/qutip_integrator.py rename to qutip/solver/integrator/qutip_integrator.py index c5f651fc8d..03749a57bd 100644 --- a/qutip/solver/ode/qutip_integrator.py +++ b/qutip/solver/integrator/qutip_integrator.py @@ -136,7 +136,7 @@ class IntegratorDiag(Integrator): Usable with ``method="diag"`` """ - integrator_options = {} + integrator_options = {"eigensolver_dtype": "dense"} support_time_dependant = False supports_blackbox = False method = 'diag' @@ -145,13 +145,13 @@ def __init__(self, system, options): if not system.isconstant: raise ValueError("Hamiltonian system must be constant to use " "diagonalized method") - self.system = system - self._dt = 0. - self._expH = None - self._prepare() + super().__init__(system, options) def _prepare(self): - self.diag, self.U = _data.eigs(self.system(0).data, False) + self._dt = 0. + self._expH = None + H0 = self.system(0).to(self.options["eigensolver_dtype"]) + self.diag, self.U = _data.eigs(H0.data, False) self.diag = self.diag.reshape((-1, 1)) self.Uinv = _data.inv(self.U) self.name = "qutip diagonalized" @@ -181,7 +181,12 @@ def set_state(self, t, state0): @property def options(self): """ - Diagonalization method do not use any options. + Supported options by "diag" method: + + 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. """ return self._options diff --git a/qutip/solver/ode/scipy_integrator.py b/qutip/solver/integrator/scipy_integrator.py similarity index 100% rename from qutip/solver/ode/scipy_integrator.py rename to qutip/solver/integrator/scipy_integrator.py diff --git a/qutip/solver/ode/verner7efficient.py b/qutip/solver/integrator/verner7efficient.py similarity index 100% rename from qutip/solver/ode/verner7efficient.py rename to qutip/solver/integrator/verner7efficient.py diff --git a/qutip/solver/ode/verner9efficient.py b/qutip/solver/integrator/verner9efficient.py similarity index 100% rename from qutip/solver/ode/verner9efficient.py rename to qutip/solver/integrator/verner9efficient.py diff --git a/qutip/solver/krylovsolve.py b/qutip/solver/krylovsolve.py new file mode 100644 index 0000000000..99fa78b229 --- /dev/null +++ b/qutip/solver/krylovsolve.py @@ -0,0 +1,102 @@ +__all__ = ['krylovsolve'] + +from .. import QobjEvo +from .sesolve import SESolver + + +def krylovsolve( + H, psi0, tlist, krylov_dim, e_ops=None, args=None, options=None +): + """ + Schrodinger equation evolution of a state vector for time independent + Hamiltonians using Krylov method. + + Evolve the state vector ("psi0") finding an approximation for the time + evolution operator of Hamiltonian ("H") by obtaining the projection of + the time evolution operator on a set of small dimensional Krylov + subspaces (m << dim(H)). + + 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 + 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 + + Parameters + ---------- + H : :class:`Qobj`, :class:`QobjEvo`, :class:`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. + + psi0 : :class:`qutip.qobj` + initial state vector (ket) + or initial unitary operator `psi0 = U` + + tlist : *list* / *array* + list of times for :math:`t`. + + krylov_dim: int + Dimension of Krylov approximation subspaces used for the time + evolution approximation. + + e_ops : :class:`qutip.qobj`, callable, or list. + 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* + dictionary of parameters for time-dependent Hamiltonians + + options : None / dict + 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``. + + Returns + ------- + result: :class:`qutip.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]. + """ + H = QobjEvo(H, args=args, tlist=tlist) + options = options or {} + options["method"] = "krylov" + options["krylov_dim"] = krylov_dim + solver = SESolver(H, options=options) + return solver.run(psi0, tlist, e_ops=e_ops) diff --git a/qutip/solver/mcsolve.py b/qutip/solver/mcsolve.py index fb38f93cb9..3a326ce405 100644 --- a/qutip/solver/mcsolve.py +++ b/qutip/solver/mcsolve.py @@ -1,4 +1,4 @@ -__all__ = ['mcsolve', "McSolver"] +__all__ = ['mcsolve', "MCSolver"] import warnings @@ -8,7 +8,7 @@ from .multitraj import MultiTrajSolver from .solver_base import Solver from .result import McResult, Result -from .mesolve import mesolve, MeSolver +from .mesolve import mesolve, MESolver import qutip.core.data as _data from time import time @@ -103,6 +103,7 @@ def mcsolve(H, state, tlist, c_ops=(), e_ops=None, ntraj=500, *, 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 @@ -135,21 +136,23 @@ def mcsolve(H, state, tlist, c_ops=(), e_ops=None, ntraj=500, *, c_ops = [QobjEvo(c_op, args=args, tlist=tlist) for c_op in c_ops] if len(c_ops) == 0: + if options is None: + options = {} options = { key: options[key] for key in options - if key in MeSolver.solver_options + if key in MESolver.solver_options } return mesolve(H, state, tlist, e_ops=e_ops, args=args, options=options) - if isinstance(ntraj, list): + if isinstance(ntraj, (list, tuple)): raise TypeError( - "List ntraj is no longer supported, use `result.expect_traj_avg`" - "with the options `keep_runs_results=True`." + "ntraj must be an integer. " + "A list of numbers is not longer supported." ) - mc = McSolver(H, c_ops, options=options) + 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) return result @@ -317,7 +320,7 @@ def integrator_options(self): # ----------------------------------------------------------------------------- # MONTE CARLO CLASS # ----------------------------------------------------------------------------- -class McSolver(MultiTrajSolver): +class MCSolver(MultiTrajSolver): r""" Monte Carlo Solver of a state vector :math:`|\psi \rangle` for a given Hamiltonian and sets of collapse operators. Options for the @@ -441,7 +444,7 @@ def _run_one_traj(self, seed, state, tlist, e_ops): def _get_integrator(self): _time_start = time() - method = self._options["method"] + method = self.options["method"] if method in self.avail_integrators(): integrator = self.avail_integrators()[method] elif issubclass(method, Integrator): @@ -458,7 +461,7 @@ def _get_integrator(self): @property def options(self): """ - Options for bloch redfield solver: + Options for monte carlo solver: store_final_state: bool, default=False Whether or not to store the final state of the evolution in the diff --git a/qutip/solver/mesolve.py b/qutip/solver/mesolve.py index ed1cd23e42..ed865dcabd 100644 --- a/qutip/solver/mesolve.py +++ b/qutip/solver/mesolve.py @@ -3,7 +3,7 @@ equation. """ -__all__ = ['mesolve', 'MeSolver'] +__all__ = ['mesolve', 'MESolver'] import numpy as np from time import time @@ -11,7 +11,7 @@ from ..core import stack_columns, unstack_columns from ..core.data import to from .solver_base import Solver -from .sesolve import sesolve, SeSolver +from .sesolve import sesolve, SESolver def mesolve(H, rho0, tlist, c_ops=None, e_ops=None, args=None, options=None): @@ -138,12 +138,12 @@ def mesolve(H, rho0, tlist, c_ops=None, e_ops=None, args=None, options=None): return sesolve(H, rho0, tlist, e_ops=e_ops, args=args, options=options) - solver = MeSolver(H, c_ops, options=options) + solver = MESolver(H, c_ops, options=options) return solver.run(rho0, tlist, e_ops=e_ops) -class MeSolver(SeSolver): +class MESolver(SESolver): """ Master equation evolution of a density matrix for a given Hamiltonian and set of collapse operators, or a Liouvillian. @@ -170,7 +170,7 @@ class MeSolver(SeSolver): of Liouvillian superoperators. None is equivalent to an empty list. options : dict, optional - Options for the solver, see :obj:`SeSolver.options` and + Options for the solver, see :obj:`SESolver.options` and `Integrator <./classes.html#classes-ode>`_ for a list of all options. attributes diff --git a/qutip/solver/multitraj.py b/qutip/solver/multitraj.py index 73f84de66b..204178d116 100644 --- a/qutip/solver/multitraj.py +++ b/qutip/solver/multitraj.py @@ -49,8 +49,7 @@ class MultiTrajSolver(Solver): def __init__(self, rhs, *, options=None): self.rhs = rhs - self._options = {} - self.options = {} if options is None else options + self.options = options self.seed_sequence = np.random.SeedSequence() self._integrator = self._get_integrator() self._state_metadata = {} @@ -175,7 +174,7 @@ def run(self, state, tlist, ntraj=1, *, ) result.add_end_condition(ntraj, target_tol) - map_func = _get_map[self._options['map']] + map_func = _get_map[self.options['map']] map_kw = { 'timeout': timeout, 'job_timeout': self.options['job_timeout'], diff --git a/qutip/solver/parallel.py b/qutip/solver/parallel.py index 74129ef29e..4ceeb0fc44 100644 --- a/qutip/solver/parallel.py +++ b/qutip/solver/parallel.py @@ -81,7 +81,7 @@ def serial_map(task, values, task_args=None, task_kwargs=None, - 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 @@ -161,7 +161,7 @@ def parallel_map(task, values, task_args=None, task_kwargs=None, - 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 diff --git a/qutip/solver/propagator.py b/qutip/solver/propagator.py index d04c893286..179c0dcef0 100644 --- a/qutip/solver/propagator.py +++ b/qutip/solver/propagator.py @@ -5,8 +5,11 @@ from .. import Qobj, qeye, unstack_columns, QobjEvo from ..core import data as _data -from .mesolve import mesolve, MeSolver -from .sesolve import sesolve, SeSolver +from .mesolve import mesolve, MESolver +from .sesolve import sesolve, SESolver +from .heom.bofin_solvers import HEOMSolver +from .solver_base import Solver +from .multitraj import MultiTrajSolver def propagator(H, t, c_ops=(), args=None, options=None, **kwargs): @@ -110,10 +113,15 @@ class Propagator: Parameters ---------- - H : :class:`Qobj`, :class:`QobjEvo`, :class:`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. + system : :class:`Qobj`, :class:`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. + + Solvers that run non-deterministacilly, such as :class:`MCSolver`, are + not supported. c_ops : list, optional List of Qobj or QobjEvo collapse operators. @@ -138,22 +146,33 @@ class Propagator: into a :class:`QobjEvo` with :: U = QobjEvo(Propagator(H)) """ - def __init__(self, H, c_ops=(), args=None, options=None, + def __init__(self, system, *, c_ops=(), args=None, options=None, memoize=10, tol=1e-14): - Hevo = QobjEvo(H, args=args) - c_ops = [QobjEvo(op, args=args) for op in c_ops] + if isinstance(system, MultiTrajSolver): + raise TypeError("Non-deterministic solvers cannot be used " + "as a propagator system") + elif isinstance(system, HEOMSolver): + raise NotImplementedError( + "HEOM is not supported by Propagator. " + "Please, tell us on GitHub issues if you need it!" + ) + elif isinstance(system, Solver): + self.solver = system + else: + Hevo = QobjEvo(system, args=args) + c_ops = [QobjEvo(op, args=args) for op in c_ops] + if Hevo.issuper or c_ops: + self.solver = MESolver(Hevo, c_ops=c_ops, options=options) + else: + self.solver = SESolver(Hevo, options=options) + self.times = [0] self.invs = [None] - if Hevo.issuper or c_ops: - self.props = [qeye(Hevo.dims)] - self.solver = MeSolver(Hevo, c_ops=c_ops, options=options) - else: - self.props = [qeye(Hevo.dims[0])] - self.solver = SeSolver(Hevo, options=options) + self.props = [qeye(self.solver.sys_dims)] + self.solver.start(self.props[0], self.times[0]) self.cte = self.solver.rhs.isconstant - self.unitary = (not self.solver.rhs.issuper - and isinstance(H, Qobj) - and H.isherm) + H_0 = self.solver.rhs(0) + self.unitary = not H_0.issuper and H_0.isherm self.args = args self.memoize = max(3, int(memoize)) self.tol = tol @@ -191,8 +210,10 @@ def __call__(self, t, t_start=0, **args): # We could improve it when the system is constant using U(2t) = U(t)**2 if not self.cte and args and args != self.args: self.args = args + self.solver._argument(args) self.times = [0] self.props = [qeye(self.props[0].dims[0])] + self.solver.start(self.props[0], self.times[0]) if t_start: if t == t_start: @@ -227,13 +248,16 @@ def _compute(self, t, idx): Compute the propagator at ``t``, ``idx`` point to a pair of (time, propagator) close to the desired time. """ - if idx > 0: + t_last = self.solver._integrator.get_state(copy=False)[0] + if self.times[idx-1] <= t_last <= t: + U = self.solver.step(t) + elif idx > 0: self.solver.start(self.props[idx-1], self.times[idx-1]) - U = self.solver.step(t, args=self.args) + U = self.solver.step(t) else: # Evolving backward in time is not supported by all integrator. self.solver.start(qeye(self.props[0].dims[0]), t) - Uinv = self.solver.step(self.times[idx], args=self.args) + Uinv = self.solver.step(self.times[idx]) U = self._inv(Uinv) return U diff --git a/qutip/solver/result.py b/qutip/solver/result.py index 3db0972cc8..a87ad9206c 100644 --- a/qutip/solver/result.py +++ b/qutip/solver/result.py @@ -126,9 +126,10 @@ class Result(_BaseResult): operator given the state at ``t``. If the element is a function, ``f``, the value recorded is ``f(t, state)``. - The values are recorded in the ``.expect`` attribute of this result - object. ``.expect`` is a list, where each item contains the values - of the corresponding ``e_op``. + The values are recorded in the ``e_data`` and ``expect`` attributes of + this result object. ``e_data`` is a dictionary and ``expect`` is a + list, where each item contains the values of the corresponding + ``e_op``. options : dict The options for this result class. @@ -153,10 +154,10 @@ class Result(_BaseResult): 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 lists of expectation values + expect : list of arrays of expectation values A list containing the values of each ``e_op``. The list is in the same order in which the ``e_ops`` were supplied and empty if no ``e_ops`` were given. @@ -197,7 +198,6 @@ def __init__(self, e_ops, options, *, 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} - self.expect = list(self.e_data.values()) self.e_ops = {} for k, op in raw_ops.items(): f = self._e_op_func(op) @@ -325,6 +325,10 @@ def __repr__(self): lines.append(">") return "\n".join(lines) + @property + def expect(self): + return [np.array(e_op) for e_op in self.e_data.values()] + class MultiTrajResult(_BaseResult): """ @@ -345,7 +349,7 @@ class MultiTrajResult(_BaseResult): Function ``e_ops`` must return a number so the average can be computed. - options : :obj:`~SolverResultsOptions` + options : dict The options for this result class. solver : str or None @@ -376,7 +380,7 @@ class MultiTrajResult(_BaseResult): The final state (if the recording of the final state was requested) averaged over all trajectories as a density matrix. - runs_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). @@ -462,13 +466,9 @@ def __init__(self, e_ops, options, *, solver=None, stats=None, **kw): self._target_tols = None self.average_e_data = {} - self.average_expect = [] self.e_data = {} - self.expect = [] self.std_e_data = {} - self.std_expect = [] self.runs_e_data = {} - self.runs_expect = [] self._post_init(**kw) @@ -492,17 +492,24 @@ def _add_first_traj(self, trajectory): state = trajectory.final_state self._sum_final_states = qzero(state.dims[0]) - self._sum_expect = np.array(trajectory.expect) * 0. - self._sum2_expect = np.array(trajectory.expect) * 0. + self._sum_expect = [ + np.zeros_like(expect) for expect in trajectory.expect + ] + self._sum2_expect = [ + np.zeros_like(expect) for expect in trajectory.expect + ] self.e_ops = trajectory.e_ops self.average_e_data = { - k: avg_expect if np.iscomplexobj(expect) else avg_expect.real - for k, avg_expect, expect - in zip(self._raw_ops, self._sum_expect, trajectory.expect) + k: list(avg_expect) + for k, avg_expect + in zip(self._raw_ops, self._sum_expect) } - self.average_expect = list(self.average_e_data.values()) + self.e_data = self.average_e_data + 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) @@ -522,38 +529,23 @@ def _reduce_expect(self, trajectory): Compute the average of the expectation values and store it in it's multiple formats. """ - self._sum_expect += np.array(trajectory.expect) - self._sum2_expect += np.abs(np.array(trajectory.expect))**2 + for i, k in enumerate(self._raw_ops): + expect_traj = trajectory.expect[i] - avg = self._sum_expect / self.num_trajectories - avg2 = self._sum2_expect / self.num_trajectories + self._sum_expect[i] += expect_traj + self._sum2_expect[i] += expect_traj**2 - self.average_e_data = { - k: avg_expect if np.iscomplexobj(expect) else avg_expect.real - for k, avg_expect, expect - in zip(self._raw_ops, avg, self.average_expect) - } - self.average_expect = list(self.average_e_data.values()) + avg = self._sum_expect[i] / self.num_trajectories + avg2 = self._sum2_expect[i] / self.num_trajectories - self.expect = self.average_expect - self.e_data = self.average_e_data + self.average_e_data[k] = list(avg) - # mean(expect**2) - mean(expect)**2 can something be very small - # negative (-1e-15) which raise an error for float sqrt. - self.std_e_data = { - k: np.sqrt(np.abs(avg_expect2 - np.abs(avg_expect**2))) - for k, avg_expect, avg_expect2 in zip(self._raw_ops, avg, avg2) - } - self.std_expect = list(self.std_e_data.values()) + # mean(expect**2) - mean(expect)**2 can something be very small + # negative (-1e-15) which raise an error for float sqrt. + self.std_e_data[k] = list(np.sqrt(np.abs(avg2 - np.abs(avg**2)))) - if self.trajectories: - self.runs_e_data = { - k: np.stack([traj.e_data[k] for traj in self.trajectories]) - for k in self._raw_ops - } - self.runs_expect = list(self.runs_e_data.values()) - self.expect = self.runs_expect - self.e_data = self.runs_e_data + if self.runs_e_data: + self.runs_e_data[k].append(trajectory.e_data[k]) def _increment_traj(self, trajectory): if self.num_trajectories == 0: @@ -584,8 +576,8 @@ def _target_tolerance_end(self): """ if self.num_trajectories <= 1: return np.inf - avg = self._sum_expect / self.num_trajectories - avg2 = self._sum2_expect / self.num_trajectories + avg = np.array(self._sum_expect) / self.num_trajectories + avg2 = np.array(self._sum2_expect) / self.num_trajectories target = np.array([ atol + rtol * mean for mean, (atol, rtol) @@ -706,7 +698,7 @@ def runs_states(self): """ States of every runs as ``states[run][t]``. """ - if self.trajectories: + if self.trajectories and self.trajectories[0].states: return [traj.states for traj in self.trajectories] else: return None @@ -732,7 +724,7 @@ def runs_final_states(self): """ Last states of each trajectories. """ - if self.trajectories: + if self.trajectories and self.trajectories[0].final_state: return [traj.final_state for traj in self.trajectories] else: return None @@ -753,6 +745,22 @@ def final_state(self): """ return self.runs_final_states or self.average_final_state + @property + def average_expect(self): + return [np.array(val) for val in self.average_e_data.values()] + + @property + def std_expect(self): + return [np.array(val) for val in self.std_e_data.values()] + + @property + def runs_expect(self): + return [np.array(val) for val in self.runs_e_data.values()] + + @property + def expect(self): + return [np.array(val) for val in self.e_data.values()] + def steady_state(self, N=0): """ Average the states of the last ``N`` times of every runs as a density @@ -772,82 +780,35 @@ def steady_state(self, N=0): else: return None - def e_data_traj_avg(self, ntraj=-1): - """ - Average of the expectation values for the ``ntraj`` first runs as a - dict. Trajectories must be saved. - - Parameters - ---------- - ntraj : int, [optional] - Number of trajectories's expect to average. - Default: all trajectories. - """ - if not self.trajectories: - return None - return { - k: np.mean(np.stack([ - traj.e_data[k] for traj in self.trajectories[:ntraj] - ]), axis=0) - for k in self._raw_ops - } - - def expect_traj_avg(self, ntraj=-1): - """ - Average of the expectation values for the ``ntraj`` first runs as a - list. Trajectories must be saved. - - Parameters - ---------- - ntraj : int, [optional] - Number of trajectories's expect to average. - Default: all trajectories. - """ - if not self.trajectories: - return None - return list(self.e_data_traj_avg(ntraj).values()) - - def e_data_traj_std(self, ntraj=-1): - """ - Standard derivation of the expectation values for the ``ntraj`` - first runs as a dict. Trajectories must be saved. - - Parameters - ---------- - ntraj : int, [optional] - Number of trajectories's expect to compute de standard derivation. - Default: all trajectories. - """ - if not self.trajectories: - return None - return { - k: np.std(np.stack([ - traj.e_data[k] for traj in self.trajectories[:ntraj] - ]), axis=0) - for k in self._raw_ops - } - - def expect_traj_std(self, ntraj=-1): - """ - Standard derivation of the expectation values for the ``ntraj`` - first runs as a dict. Trajectories must be saved. - - Parameters - ---------- - ntraj : int, [optional] - Number of trajectories's expect to compute de standard derivation. - Default: all trajectories. - """ - if not self.trajectories: - return None - return list(self.e_data_traj_std(ntraj).values()) - def __repr__(self): - repr = super().__repr__() - lines = repr.split("\n") - lines.insert(-1, f" Number of trajectories: {self.num_trajectories}") + lines = [ + f"<{self.__class__.__name__}", + f" Solver: {self.solver}", + ] + if self.stats: + lines.append(" Solver stats:") + 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)}") + if self.states: + lines.append(" States saved.") + elif self.final_state is not None: + lines.append(" Final state saved.") + else: + lines.append(" State not saved.") + lines.append(f" Number of trajectories: {self.num_trajectories}") if self.trajectories: - lines.insert(-1, " Trajectories saved.") + lines.append(" Trajectories saved.") + else: + lines.append(" Trajectories not saved.") + lines.append(">") return "\n".join(lines) def __add__(self, other): @@ -877,35 +838,26 @@ def __add__(self, other): ) new._target_tols = None - if self._raw_ops: - new._sum_expect = self._sum_expect + other._sum_expect - new._sum2_expect = self._sum2_expect + other._sum2_expect + new._sum_expect = [] + new._sum2_expect = [] + new.average_e_data = {} + new.std_e_data = {} - avg = new._sum_expect / new.num_trajectories - avg2 = new._sum2_expect / new.num_trajectories + 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.average_e_data = { - k: avg_expect - for k, avg_expect in zip(self._raw_ops, avg) - } - new.average_expect = list(new.average_e_data.values()) + avg = new._sum_expect[i] / new.num_trajectories + avg2 = new._sum2_expect[i] / new.num_trajectories - new.expect = new.average_expect + new.average_e_data[k] = list(avg) new.e_data = new.average_e_data - new.std_e_data = { - k: np.sqrt(avg_expect2 - abs(avg_expect**2)) - for k, avg_expect, avg_expect2 in zip(self._raw_ops, avg, avg2) - } - new.std_expect = list(new.std_e_data.values()) + new.std_e_data[k] = np.sqrt(np.abs(avg2 - np.abs(avg**2))) if new.trajectories: - new.runs_e_data = { - k: np.stack([traj.e_data[k] for traj in new.trajectories]) - for k in new._raw_ops - } - new.runs_expect = list(new.runs_e_data.values()) - new.expect = new.runs_expect + 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"] diff --git a/qutip/solver/sesolve.py b/qutip/solver/sesolve.py index 2450fc652f..0914624232 100644 --- a/qutip/solver/sesolve.py +++ b/qutip/solver/sesolve.py @@ -2,7 +2,7 @@ This module provides solvers for the unitary Schrodinger equation. """ -__all__ = ['sesolve', 'SeSolver'] +__all__ = ['sesolve', 'SESolver'] import numpy as np from time import time @@ -99,11 +99,11 @@ def sesolve(H, psi0, tlist, e_ops=None, args=None, options=None): is an empty list of `store_states=True` in options]. """ H = QobjEvo(H, args=args, tlist=tlist) - solver = SeSolver(H, options=options) + solver = SESolver(H, options=options) return solver.run(psi0, tlist, e_ops=e_ops) -class SeSolver(Solver): +class SESolver(Solver): """ Schrodinger equation evolution of a state vector or unitary matrix for a given Hamiltonian. @@ -116,7 +116,7 @@ class SeSolver(Solver): that can be made into :class:`QobjEvo` are also accepted. options : dict, optional - Options for the solver, see :obj:`SeSolver.options` and + Options for the solver, see :obj:`SESolver.options` and `Integrator <./classes.html#classes-ode>`_ for a list of all options. attributes diff --git a/qutip/solver/solver_base.py b/qutip/solver/solver_base.py index 241122c2da..953bfab490 100644 --- a/qutip/solver/solver_base.py +++ b/qutip/solver/solver_base.py @@ -46,8 +46,7 @@ def __init__(self, rhs, *, options=None): self.rhs = QobjEvo(rhs) else: TypeError("The rhs must be a QobjEvo") - self._options = {} - self.options = {} if options is None else options + self.options = options self._integrator = self._get_integrator() self._state_metadata = {} self.stats = self._initialize_stats() @@ -224,6 +223,16 @@ def _get_integrator(self): self._init_integrator_time = time() - _time_start return integrator_instance + @property + def sys_dims(self): + """ + Dimensions of the space that the system use: + + ``qutip.basis(sovler.dims)`` will create a state with proper dimensions + for this solver. + """ + return self.rhs.dims[0] + @property def options(self): """ @@ -266,6 +275,10 @@ def _parse_options(self, new_options, default, old_options): @options.setter def options(self, new_options): + if not hasattr(self, "_options"): + self._options = {} + if new_options is None: + new_options = {} if not isinstance(new_options, dict): raise TypeError("options most to be a dictionary.") new_solver_options, new_ode_options = self._parse_options( diff --git a/qutip/solver/spectrum.py b/qutip/solver/spectrum.py new file mode 100644 index 0000000000..cb92e1f0e6 --- /dev/null +++ b/qutip/solver/spectrum.py @@ -0,0 +1,188 @@ +__all__ = ['spectrum', 'spectrum_correlation_fft'] + +import numpy as np +import scipy.fftpack + +from .steadystate import steadystate +from ..core import liouvillian, spre, expect +from ..core import data as _data +from qutip.settings import settings + +def spectrum(H, wlist, c_ops, a_op, b_op, solver="es"): + r""" + Calculate the spectrum of the correlation function + :math:`\lim_{t \to \infty} \left`, + i.e., the Fourier transform of the correlation function: + + .. math:: + + S(\omega) = \int_{-\infty}^{\infty} + \lim_{t \to \infty} \left + e^{-i\omega\tau} d\tau. + + 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` + system Hamiltonian. + wlist : array_like + List of frequencies for :math:`\omega`. + c_ops : list + List of collapse operators. + a_op : Qobj + Operator A. + b_op : Qobj + Operator B. + solver : str + Choice of solver (`es` for exponential series and + `pi` for psuedo-inverse, `solve` for generic solver). + + Returns + ------- + spectrum : array + An array with spectrum :math:`S(\omega)` for the frequencies + specified in `wlist`. + + """ + if not H.issuper: + L = liouvillian(H, c_ops) + else: + L = H + sum([lindblad_dissipator(c) for c in c_ops]) + if solver == "es": + return _spectrum_es(L, wlist, a_op, b_op) + elif solver in ["pi", "solve"]: + return _spectrum_pi(L, wlist, a_op, b_op, use_pinv=solver=="pi") + raise ValueError( + f"Unrecognized choice of solver {solver} (use 'es', 'pi' or 'solve')." + ) + + +def spectrum_correlation_fft(tlist, y, inverse=False): + """ + Calculate the power spectrum corresponding to a two-time correlation + function using FFT. + + Parameters + ---------- + tlist : array_like + 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 + boolean parameter for using a positive exponent in the Fourier + Transform instead. Default is False. + + Returns + ------- + w, S : tuple + Returns an array of angular frequencies 'w' and the corresponding + two-sided power spectrum 'S(w)'. + + """ + tlist = np.asarray(tlist) + N = tlist.shape[0] + dt = tlist[1] - tlist[0] + if not np.allclose(np.diff(tlist), dt * np.ones(N - 1, dtype=float)): + raise ValueError('tlist must be equally spaced for FFT.') + F = (N * scipy.fftpack.ifft(y)) if inverse else scipy.fftpack.fft(y) + # calculate the frequencies for the components in F + f = scipy.fftpack.fftfreq(N, dt) + # re-order frequencies from most negative to most positive (centre on 0) + idx = np.array([], dtype='int') + idx = np.append(idx, np.where(f < 0.0)) + idx = np.append(idx, np.where(f >= 0.0)) + return 2 * np.pi * f[idx], 2 * dt * np.real(F[idx]) + + +def _spectrum_es(L, wlist, a_op, b_op): + r""" + Internal function for calculating the spectrum of the correlation function + :math:`\left`. + """ + # find the steady state density matrix and a_op and b_op expecation values + rho0 = steadystate(L) + a_op_ss = expect(a_op, rho0) + b_op_ss = expect(b_op, rho0) + # eseries solution for (b * rho0)(t) + states, rates = _diagonal_evolution(L, b_op * rho0) + # correlation + ampls = [_data.expect(a_op.data, state) for state in states] + # make covariance + ampls += [-a_op_ss * b_op_ss] + rates += [0] + # Tidy up similar rates. + order = np.argsort(rates) + clean_rates = [] + clean_ampls = [] + prev_rate = np.nan + for idx in order: + if np.abs(rates[idx] - prev_rate) < settings.core["atol"]: + clean_ampls[-1] += ampls[idx] + else: + clean_rates.append(rates[idx]) + clean_ampls.append(ampls[idx]) + prev_rate = rates[idx] + # Remove 0 amplitude + rates, ampls = zip(*[ + (rate, ampl) + for rate, ampl in zip(clean_rates, clean_ampls) + if np.abs(ampl) > settings.core["atol"] + ]) + ampls, rates = np.array(ampls), np.array(rates) + LW = np.subtract.outer(1j * np.array(wlist), rates).T + return (ampls @ (2 / LW)).real + + +# +# pseudo-inverse solvers +def _spectrum_pi(L, wlist, a_op, b_op, use_pinv=False): + r""" + Internal function for calculating the spectrum of the correlation function + :math:`\left`. + """ + dtype = type(L.data) + rho_ss = steadystate(L) + tr_mat = _data.identity[dtype](rho_ss.shape[0]) + tr_vec = _data.column_stack(tr_mat).transpose() + rho = _data.column_stack(rho_ss.data) + + A = L.data + ket = spre(b_op).data @ rho + bra = tr_vec @ spre(a_op).data + + I = _data.identity[dtype](L.shape[0]) + P = _data.kron(rho, tr_vec) + Q = I - P + + spectrum = np.zeros(len(wlist)) + for idx, w in enumerate(wlist): + if use_pinv and np.abs(w) > settings.core["atol"]: + # At w == 0., "L - iw" is singular + MMR = _data.inv(-1.0j * w * I + A) + else: + MMR = Q @ _data.solve(-1.0j * w * I + A, Q) + + spectrum[idx] = -2 * _data.inner_op(bra, MMR, ket).real + return spectrum + + +def _diagonal_evolution(L, rho0, sparse=False): + if rho0.norm() < settings.core["atol"]: + return [_data.zeros["CSR"](*rho0.shape)], [0] + if isinstance(L.data, _data.CSR) and not sparse: + L = L.to(_data.Dense) + evals, evecs = _data.eigs(L.data) + size = rho0.shape[0] * rho0.shape[1] + r0 = _data.column_stack(rho0.data) + v0 = _data.solve(evecs, r0) + vv = evecs @ _data.diag(v0.to_array().flatten(), [0]) + states = [] + rates = [] + for ket, rate in zip(_data.split_columns(vv), evals): + if _data.norm.l2(ket) < settings.core["atol"]: + continue + states.append(_data.column_unstack(ket, rho0.shape[0])) + rates.append(rate) + return states, rates diff --git a/qutip/solver/steadystate.py b/qutip/solver/steadystate.py new file mode 100644 index 0000000000..f576c9f2bb --- /dev/null +++ b/qutip/solver/steadystate.py @@ -0,0 +1,537 @@ +from qutip import liouvillian, lindblad_dissipator, Qobj, qeye, qzero +from qutip import vector_to_operator, operator_to_vector +from qutip import settings +import qutip.core.data as _data +import numpy as np +import scipy.sparse.csgraph +import scipy.sparse.linalg +from warnings import warn + + +__all__ = ["steadystate", "steadystate_floquet", "pseudo_inverse"] + + +def _permute_wbm(L, b): + perm = scipy.sparse.csgraph.maximum_bipartite_matching(L.as_scipy()) + L = _data.permute.indices(L, perm, None) + b = _data.permute.indices(b, perm, None) + return L, b + + +def _permute_rcm(L, b): + perm = scipy.sparse.csgraph.reverse_cuthill_mckee(L.as_scipy()) + L = _data.permute.indices(L, perm, perm) + b = _data.permute.indices(b, perm, None) + return L, b, perm + + +def _reverse_rcm(rho, perm): + rev_perm = np.argsort(perm) + rho = _data.permute.indices(rho, rev_perm, None) + return rho + + +def steadystate(A, c_ops=[], *, method='direct', solver=None, **kwargs): + """ + Calculates the steady state for quantum evolution subject to the supplied + Hamiltonian or Liouvillian operator and (if given a Hamiltonian) a list of + collapse operators. + + If the user passes a Hamiltonian then it, along with the list of collapse + operators, will be converted into a Liouvillian operator in Lindblad form. + + Parameters + ---------- + A : :obj:`~Qobj` + A Hamiltonian or Liouvillian operator. + + c_op_list : list + A list of collapse operators. + + method : str, 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 + 'direct' and 'power' methods only. + Solver to use when solving the ``L(rho_ss) = 0`` equation. + Default supported solver are: + + - "solve", "lstsq" + dense solver from numpy.linalg + - "spsolve", "gmres", "lgmres", "bicgstab" + sparse solver from scipy.sparse.linalg + - "mkl_spsolve" + sparse solver by mkl. + + Extension to qutip, such as qutip-tensorflow, can use come with their + own solver. When ``A`` and ``c_ops`` use these data backends, see the + corresponding libraries ``linalg`` for available solver. + + Extra options for these solver can be passed in ``**kw``. + + 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 Weighted Bipartite Matching reordering to make the Liouvillian + diagonally dominant. This is useful for iterative preconditioners + only. Used with 'direct' or 'power' method. + + weight : float, optional + Sets the size of the elements used for adding the unity trace condition + to the linear solvers. This is set to the average abs value of the + Liouvillian elements if not specified by the user. + Used with 'direct' method. + + power_tol : float, default 1e-12 + Tolerance for the solution when using the 'power' method. + + 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 + Small weight used in the "power" method. + + sparse: bool + 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 + used as default solver. + + **kwargs : + Extra options to pass to the linear system solver. See the + documentation of the used solver in ``numpy.linalg`` or + ``scipy.sparse.linalg`` to see what extra arguments are supported. + + Returns + ------- + dm : qobj + Steady state density matrix. + info : dict, optional + Dictionary containing solver-specific information about the solution. + + .. note:: + + 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) + + if "-" in method: + # to support v4's "power-gmres" method + method, solver = method.split("-") + + if solver == "mkl": + solver = "mkl_spsolve" + + # Keys supported in v4, but removed in v5 + if kwargs.pop("return_info", False): + warn("Steadystate no longer supports return_info", DeprecationWarning) + if "mtol" in kwargs and "power_tol" not in kwargs: + kwargs["power_tol"] = kwargs["mtol"] + kwargs.pop("mtol", None) + + if method == "eigen": + return _steadystate_eigen(A, **kwargs) + if method == "svd": + return _steadystate_svd(A, **kwargs) + + # We want to be able to use this without having to know what data type the + # liouvillian uses. For extra data types (tensorflow) we can expect + # the users to know they are using them and choose an appropriate solver + sparse_solvers = ["spsolve", "mkl_spsolve", "gmres", "lgmres", "bicgstab"] + if not isinstance(A.data, (_data.CSR, _data.Dense)): + # Tensorflow, jax, etc. data type + pass + elif isinstance(A.data, _data.CSR) and solver in ["solve", "lstsq"]: + A = A.to("dense") + elif isinstance(A.data, _data.Dense) and solver in sparse_solvers: + A = A.to("csr") + elif solver is None and kwargs.get("sparse", False): + A = A.to("csr") + solver = "mkl_spsolve" if settings.has_mkl else "spsolve" + elif solver is None and (kwargs.get("sparse", None) is False): + # sparse is explicitly set to false, v4 tag to use `numpy.linalg.solve` + A = A.to("dense") + solver = "solve" + + if method in ["direct", "iterative"]: + # Remove unused kwargs, so only used and pass-through ones are included + kwargs.pop("power_tol", 0) + kwargs.pop("power_maxiter", 0) + kwargs.pop("power_eps", 0) + kwargs.pop("sparse", 0) + return _steadystate_direct(A, kwargs.pop("weight", 0), + method=solver, **kwargs) + + elif method == "power": + # Remove unused kwargs, so only used and pass-through ones are included + kwargs.pop("weight", 0) + kwargs.pop("sparse", 0) + return _steadystate_power(A, method=solver, **kwargs) + else: + raise ValueError(f"method {method} not supported.") + + +def _steadystate_direct(A, weight, **kw): + # Find the weight, no good dispatched function available... + if weight: + pass + elif isinstance(A.data, _data.CSR): + weight = np.mean(np.abs(A.data.as_scipy().data)) + else: + A_np = np.abs(A.full()) + weight = np.mean(A_np[A_np > 0]) + + # Add weight to the Liouvillian + # A[:, 0] = vectorized(eye * weight) + # We don't have a function to overwrite part of an array, so + N = A.shape[0] + n = int(N**0.5) + dtype = type(A.data) + 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) + ) + L = _data.add(weight_mat, A.data) + b = _data.one_element[dtype]((N, 1), (0, 0), weight) + + # Permutation are part of scipy.sparse, thus only supported for CSR. + if kw.pop("use_wbm", False): + if isinstance(L, _data.CSR): + L, b = _permute_wbm(L, b) + else: + warn("Only sparse matrice 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 sparse matrice can be permuted.", RuntimeWarning) + if kw.pop("use_precond", False): + if isinstance(L, _data.CSR): + kw["M"] = _compute_precond(L, kw) + else: + warn("Only sparse solver use preconditioners.", RuntimeWarning) + + + method = kw.pop("method", None) + steadystate = _data.solve(L, b, method, options=kw) + + if use_rcm: + steadystate = _reverse_rcm(steadystate, perm) + + rho_ss = _data.column_unstack(steadystate, n) + rho_ss = _data.add(rho_ss, rho_ss.adjoint()) * 0.5 + + return Qobj(rho_ss, dims=A.dims[0], isherm=True) + + +def _steadystate_eigen(L, **kw): + val, vec = (L.dag() @ L).eigenstates( + eigvals=1, + sort="low", + # v4's implementation only uses sparse eigen solver + sparse=kw.pop("sparse", True) + ) + rho = vector_to_operator(vec[0]) + return rho / rho.tr() + + +def _steadystate_svd(L, **kw): + u, s, vh = _data.svd(L.data, True) + vec = Qobj(_data.split_columns(vh.adjoint())[-1], dims=[L.dims[0],[1]]) + rho = vector_to_operator(vec) + return rho / rho.tr() + + +def _steadystate_power(A, **kw): + A += kw.pop("power_eps", 1e-15) + L = A.data + N = L.shape[1] + y = _data.Dense([1]*N) + + # Permutation are part of scipy.sparse, thus only supported for CSR. + if kw.pop("use_wbm", False): + if isinstance(L, _data.CSR): + L, y = _permute_wbm(L, y) + else: + warn("Only sparse matrice 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 sparse matrice can be permuted.", RuntimeWarning) + if kw.pop("use_precond", False): + if isinstance(L, _data.CSR): + kw["M"] = _compute_precond(L, kw) + else: + warn("Only sparse solver use preconditioners.", RuntimeWarning) + + it = 0 + maxiter = kw.pop("power_maxiter", 10) + tol = kw.pop("power_tol", 1e-12) + method = kw.pop("method", None) + while it < maxiter and _data.norm.max(L @ y) > tol: + y = _data.solve(L, y, method, options=kw) + y = y / _data.norm.max(y) + it += 1 + + if it >= maxiter: + raise Exception('Failed to find steady state after ' + + str(maxiter) + ' iterations') + + if use_rcm: + y = _reverse_rcm(y, perm) + + rho_ss = Qobj(_data.column_unstack(y, N**0.5), dims=A.dims[0]) + rho_ss = rho_ss + rho_ss.dag() + rho_ss = rho_ss / rho_ss.tr() + rho_ss.isherm = True + return rho_ss + + +def steadystate_floquet(H_0, c_ops, Op_t, w_d=1.0, n_it=3, sparse=False, + solver=None, **kwargs): + """ + Calculates the effective steady state for a driven + system with a time-dependent cosinusoidal term: + + .. math:: + + \\mathcal{\\hat{H}}(t) = \\hat{H}_0 + + \\mathcal{\\hat{O}} \\cos(\\omega_d t) + + Parameters + ---------- + H_0 : :obj:`~Qobj` + A Hamiltonian or Liouvillian operator. + + c_ops : list + A list of collapse operators. + + Op_t : :obj:`~Qobj` + The the interaction operator which is multiplied by the cosine + + w_d : float, default 1.0 + The frequency of the drive + + n_it : int, default 3 + The number of iterations for the solver + + sparse : bool, default False + Solve for the steady state using sparse algorithms. + + solver : str, default=None + Solver to use when solving the linear system. + Default supported solver are: + + - "solve", "lstsq" + dense solver from numpy.linalg + - "spsolve", "gmres", "lgmres", "bicgstab" + sparse solver from scipy.sparse.linalg + - "mkl_spsolve" + sparse solver by mkl. + + Extensions to qutip, such as qutip-tensorflow, may provide their own solvers. + When ``H_0`` and ``c_ops`` use these data backends, see their documentation + for the names and details of additional solvers they may provide. + + **kwargs: + Extra options to pass to the linear system solver. See the + documentation of the used solver in ``numpy.linalg`` or + ``scipy.sparse.linalg`` to see what extra arguments are supported. + + Returns + ------- + dm : qobj + Steady state density matrix. + + .. note:: + + See: Sze Meng Tan, + https://copilot.caltech.edu/documents/16743/qousersguide.pdf, + Section (10.16) + + """ + + L_0 = liouvillian(H_0, c_ops) + L_m = L_p = 0.5 * liouvillian(Op_t) + # L_p and L_m correspond to the positive and negative + # frequency terms respectively. + # They are independent in the model, so we keep both names. + Id = qeye(L_0.dims[0], dtype=type(L_0.data)) + S = T = qzero(L_0.dims[0], dtype=type(L_0.data)) + + if isinstance(H_0.data, _data.CSR) and not sparse: + L_0 = L_0.to("Dense") + L_m = L_m.to("Dense") + L_p = L_p.to("Dense") + Id = Id.to("Dense") + + for n_i in np.arange(n_it, 0, -1): + L = L_0 - 1j * n_i * w_d * Id + L_m @ S + S.data = - _data.solve(L.data, L_p.data, solver, kwargs) + L = L_0 - 1j * n_i * w_d * Id + L_p @ T + T.data = - _data.solve(L.data, L_m.data, solver, kwargs) + + M_subs = L_0 + L_m @ S + L_p @ T + return steadystate(M_subs, solver=solver, **kwargs) + + +def pseudo_inverse(L, rhoss=None, w=None, method='splu', *, use_rcm=False, + **kwargs): + """ + Compute the pseudo inverse for a Liouvillian superoperator, optionally + given its steady state density matrix (which will be computed if not + given). + + Parameters + ---------- + L : Qobj + A Liouvillian superoperator for which to compute the pseudo inverse. + + rhoss : Qobj + A steadystate density matrix as Qobj instance, for the Liouvillian + superoperator L. + + w : double + 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 + Flag that indicate whether to use sparse or dense matrix methods when + computing the pseudo inverse. + + method : string + Method used to compte matrix inverse. + Choice are 'pinv' to use scipy's function of the same name, or a linear + system solver. + Default supported solver are: + + - "solve", "lstsq" + dense solver from numpy.linalg + - "spsolve", "gmres", "lgmres", "bicgstab", "splu" + sparse solver from scipy.sparse.linalg + - "mkl_spsolve", + sparse solver by mkl. + + Extension to qutip, such as qutip-tensorflow, can use come with their + own solver. When ``L`` use these data backends, see the corresponding + libraries ``linalg`` for available solver. + + kwargs : dictionary + Additional keyword arguments for setting parameters for solver methods. + + Returns + ------- + 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. + + """ + if rhoss is None: + rhoss = steadystate(L) + + sparse = kwargs.pop("sparse", False) + if method == "direct": + method = "splu" if sparse else "pinv" + sparse_solvers = ["splu", "mkl_spsolve", "spilu"] + dense_solvers = ["solve", "lstsq", "pinv"] + if isinstance(L.data, _data.CSR) and method in dense_solvers: + L = L.to("dense") + elif isinstance(L.data, _data.Dense) and method in sparse_solvers: + L = L.to("csr") + + N = np.prod(L.dims[0][0]) + dtype = type(L.data) + rhoss_vec = operator_to_vector(rhoss) + + tr_op = qeye(L.dims[0][0]) + tr_op_vec = operator_to_vector(tr_op) + + P = _data.kron(rhoss_vec.data, tr_op_vec.data.transpose(), dtype=dtype) + I = _data.csr.identity(N * N) + Q = _data.sub(I, P) + + if w in [None, 0.0]: + L += 1e-15j + else: + L += 1.0j*w + + use_rcm = use_rcm and isinstance(L.data, _data.CSR) + + if use_rcm: + perm = scipy.sparse.csgraph.reverse_cuthill_mckee(L.data.as_scipy()) + A = _data.permute.indices(L.data, perm, perm) + Q = _data.permute.indices(Q, perm, perm, dtype=_data.CSR) + else: + A = L.data + + if method in ["pinv", "numpy", "scipy", "scipy2"]: + # from scipy 1.7.0, they all use the same algorithm. + LI = _data.Dense(scipy.linalg.pinv(A.to_array()), copy=False) + LIQ = _data.matmul(LI, Q) + elif method == "spilu": + if not isinstance(A, _data.CSR): + raise TypeError("'spilu' method can only be used with sparse data") + ILU = scipy.sparse.linalg.spilu(A.as_scipy().tocsc(), **kwargs) + LIQ = _data.Dense(ILU.solve(Q.to_array())) + else: + LIQ = _data.solve(A, Q, method, options=kwargs) + + R = _data.matmul(Q, LIQ) + + if use_rcm: + rev_perm = np.argsort(perm) + R = _data.permute.indices(R, rev_perm, rev_perm) + + return Qobj(R, dims=L.dims) + + +def _compute_precond(L, args): + spilu_keys = { + 'permc_spec', + 'drop_tol', + 'diag_pivot_thresh', + 'fill_factor', + 'options', + } + ss_args = { + key: args.pop(key) + for key in spilu_keys + if key in args + } + P = scipy.sparse.linalg.spilu(L.as_scipy().tocsc(), **ss_args) + return scipy.sparse.linalg.LinearOperator(L.shape, matvec=P.solve) diff --git a/qutip/tests/core/data/test_linalg.py b/qutip/tests/core/data/test_linalg.py new file mode 100644 index 0000000000..01681d4e23 --- /dev/null +++ b/qutip/tests/core/data/test_linalg.py @@ -0,0 +1,133 @@ +import qutip.settings as settings +import numpy as np +import scipy +import pytest +import qutip + +from qutip.core import data as _data +from qutip.core.data import Data, Dense, CSR + + +class TestSolve(): + def op_numpy(self, A, b): + return np.linalg.solve(A, b) + + def _gen_op(self, N, dtype): + return qutip.rand_unitary(N, dtype=dtype).data + + def _gen_ket(self, N, dtype): + return qutip.rand_ket(N, dtype=dtype).data + + @pytest.mark.parametrize(['method', "opt"], [ + ("spsolve", {}), + ("splu", {"csc": True}), + ("gmres", {"atol": 1e-8}), + ("lsqr", {}), + pytest.param( + "mkl_spsolve", {}, + marks=pytest.mark.skipif(not settings.has_mkl, reason="mkl not available") + ), + ], + ids=["spsolve", "splu", "gmres", "lsqr", "mkl_spsolve"] + ) + def test_mathematically_correct_CSR(self, method, opt): + """ + Test that the binary operation is mathematically correct for all the + known type specialisations. + """ + A = self._gen_op(10, CSR) + b = self._gen_ket(10, Dense) + expected = self.op_numpy(A.to_array(), b.to_array()) + test = _data.solve_csr_dense(A, b, method, opt) + test1 = _data.solve(A, b, method, opt) + + assert test.shape == expected.shape + np.testing.assert_allclose(test.to_array(), expected, + atol=1e-7, rtol=1e-7) + np.testing.assert_allclose(test1.to_array(), expected, + atol=1e-7, rtol=1e-7) + + @pytest.mark.parametrize(['method', "opt"], [ + ("solve", {}), + ("lstsq", {}), + ]) + def test_mathematically_correct_Dense(self, method, opt): + A = self._gen_op(10, Dense) + b = self._gen_ket(10, Dense) + expected = self.op_numpy(A.to_array(), b.to_array()) + test = _data.solve_dense(A, b, method, opt) + test1 = _data.solve(A, b, method, opt) + + assert test.shape == expected.shape + np.testing.assert_allclose(test.to_array(), expected, + atol=1e-7, rtol=1e-7) + np.testing.assert_allclose(test1.to_array(), expected, + atol=1e-7, rtol=1e-7) + + + def test_incorrect_shape_non_square(self): + A = qutip.Qobj(np.random.rand(5, 10)).data + b = qutip.Qobj(np.random.rand(10, 1)).data + with pytest.raises(ValueError): + test1 = _data.solve(A, b) + + + def test_incorrect_shape_mismatch(self): + A = qutip.Qobj(np.random.rand(10, 10)).data + b = qutip.Qobj(np.random.rand(9, 1)).data + with pytest.raises(ValueError): + test1 = _data.solve(A, b) + + +class TestSVD(): + def op_numpy(self, A): + return np.linalg.svd(A) + + def _gen_dm(self, N, rank, dtype): + return qutip.rand_dm(N, rank=rank, dtype=dtype).data + + def _gen_non_square(self, N): + mat = np.random.randn(N, N//2) + for i in range(N//2): + # Ensure no zeros singular values + mat[i,i] += 5 + return _data.Dense(mat) + + @pytest.mark.parametrize("shape", ["square", "non-square"]) + def test_mathematically_correct_svd(self, shape): + if shape == "square": + matrix = self._gen_dm(10, 6, Dense) + else: + matrix = self._gen_non_square(12) + u, s, v = self.op_numpy(matrix.to_array()) + test_U, test_S, test_V = _data.svd(matrix, True) + only_S = _data.svd(matrix, False) + + assert sum(test_S > 1e-10) == 6 + np.testing.assert_allclose(test_U.to_array(), u, atol=1e-7, rtol=1e-7) + np.testing.assert_allclose(test_V.to_array(), v, atol=1e-7, rtol=1e-7) + np.testing.assert_allclose(test_S, s, atol=1e-7, rtol=1e-7) + np.testing.assert_allclose(only_S, s, atol=1e-7, rtol=1e-7) + + s_as_matrix = _data.diag(test_S, 0, (test_U.shape[1], test_V.shape[0])) + + np.testing.assert_allclose( + matrix.to_array(), + (test_U @ s_as_matrix @ test_V).to_array(), + atol=1e-7, rtol=1e-7 + ) + + def test_mathematically_correct_svd_csr(self): + rank = 5 + matrix = self._gen_dm(100, rank, CSR) + test_U, test_S1, test_V = _data.svd_csr(matrix, True, k=rank) + test_S2 = _data.svd_csr(matrix, False, k=rank) + + assert len(test_S1) == rank + assert len(test_S2) == rank + + np.testing.assert_allclose( + matrix.to_array(), + (test_U @ _data.diag(test_S1, 0) @ test_V).to_array(), + atol=1e-7, rtol=1e-7 + ) diff --git a/qutip/tests/core/data/test_mathematics.py b/qutip/tests/core/data/test_mathematics.py index 311688c774..ceeecda9a7 100644 --- a/qutip/tests/core/data/test_mathematics.py +++ b/qutip/tests/core/data/test_mathematics.py @@ -614,6 +614,7 @@ def op_numpy(self, left, right, scalar_is_ket=False): specialisations = [ pytest.param(data.inner_csr, CSR, CSR, complex), + pytest.param(data.inner_dense, Dense, Dense, complex), ] def generate_scalar_is_ket(self, metafunc): @@ -683,6 +684,7 @@ def op_numpy(self, left, mid, right, scalar_is_ket=False): specialisations = [ pytest.param(data.inner_op_csr, CSR, CSR, CSR, complex), + pytest.param(data.inner_op_dense, Dense, Dense, Dense, complex), ] def generate_scalar_is_ket(self, metafunc): @@ -810,6 +812,7 @@ def op_numpy(self, matrix, n): bad_shapes = shapes_not_square() specialisations = [ pytest.param(data.pow_csr, CSR, CSR), + pytest.param(data.pow_dense, Dense, Dense), ] @pytest.mark.parametrize("n", [0, 1, 10], ids=["n_0", "n_1", "n_10"]) diff --git a/qutip/tests/core/data/test_properties.py b/qutip/tests/core/data/test_properties.py index e2a0c84c70..de844baec7 100644 --- a/qutip/tests/core/data/test_properties.py +++ b/qutip/tests/core/data/test_properties.py @@ -154,3 +154,22 @@ def test_structure_detection(self, datatype): [1,0,1]]) base = _data.to(datatype, _data.create(base)) assert not _data.isherm(base, tol=self.tol) + + +class Test_isdiag: + @pytest.mark.parametrize("shape", + [(10, 1), (2, 5), (5, 2), (5, 5)] + ) + def test_isdiag(self, shape, datatype): + mat = np.zeros(shape) + data = _data.to(datatype, _data.Dense(mat)) + # empty matrices are diagonal + assert _data.isdiag(data) + + mat[0, 0] = 1 + data = _data.to(datatype, _data.Dense(mat)) + assert _data.isdiag(data) + + mat[1, 0] = 1 + data = _data.to(datatype, _data.Dense(mat)) + assert not _data.isdiag(data) diff --git a/qutip/tests/core/test_coefficient.py b/qutip/tests/core/test_coefficient.py index 01847bf8fd..02287c5f58 100644 --- a/qutip/tests/core/test_coefficient.py +++ b/qutip/tests/core/test_coefficient.py @@ -4,7 +4,7 @@ import numpy as np import scipy.interpolate as interp from functools import partial -from qutip.core.coefficient import (coefficient, norm, conj, shift, +from qutip.core.coefficient import (coefficient, norm, conj, CompilationOptions, Coefficient, clean_compiled_coefficient ) @@ -175,21 +175,6 @@ def test_CoeffUnitaryTransform(style, transform, expected): _assert_eq_over_interval(transform(coeff), lambda t: expected(coeff(t))) -@pytest.mark.parametrize(['style'], [ - pytest.param("func", id="func"), - pytest.param("array", id="array"), - pytest.param("arraylog", id="logarray"), - pytest.param("string", id="string"), - pytest.param("steparray", id="steparray"), - pytest.param("steparraylog", id="steparraylog") -]) -def test_CoeffShift(style): - coeff = coeff_generator(style, "f") - dt = np.e / 30 - _assert_eq_over_interval(shift(coeff, dt), - lambda t: coeff(t + dt)) - - @pytest.mark.parametrize(['style_left'], [ pytest.param("func", id="func"), pytest.param("array", id="array"), @@ -310,10 +295,6 @@ def _mul(coeff): return coeff * coeff -def _shift(coeff): - return shift(coeff, 0.05) - - @pytest.mark.parametrize(['style'], [ pytest.param("func", id="func"), pytest.param("array", id="array"), @@ -328,7 +309,6 @@ def _shift(coeff): pytest.param(_mul, id="prod"), pytest.param(norm, id="norm"), pytest.param(conj, id="conj"), - pytest.param(_shift, id="shift"), ]) def test_Coeffpickle(style, transform): coeff = coeff_generator(style, "f") @@ -351,7 +331,6 @@ def test_Coeffpickle(style, transform): pytest.param(_mul, id="prod"), pytest.param(norm, id="norm"), pytest.param(conj, id="conj"), - pytest.param(_shift, id="shift"), ]) def test_Coeffcopy(style, transform): coeff = coeff_generator(style, "f") diff --git a/qutip/tests/core/test_expect.py b/qutip/tests/core/test_expect.py index 437aac18a1..906880ceca 100644 --- a/qutip/tests/core/test_expect.py +++ b/qutip/tests/core/test_expect.py @@ -140,7 +140,7 @@ def test_compatibility_with_solver(solve): h = qutip.sigmax() state = qutip.basis(2, 0) times = np.linspace(0, 10, 101) - options = qutip.SolverOptions(store_states=True) + options = {"store_states":True} result = solve(h, state, times, e_ops=e_ops, options=options) direct, states = result.expect, result.states indirect = qutip.expect(e_ops[:-1], states) @@ -151,16 +151,15 @@ def test_compatibility_with_solver(solve): assert isinstance(direct_, np.ndarray) assert isinstance(indirect_, np.ndarray) assert direct_.dtype == indirect_.dtype - np.testing.assert_allclose(direct_, indirect_, atol=1e-12) + np.testing.assert_allclose(np.array(direct_), indirect_, atol=1e-12) # test measurement operators based on lambda functions direct_ = direct[-1] - # by design, lambda measurements are of complex type - indirect_ = np.sin(times, dtype=complex) + indirect_ = np.sin(times) assert len(direct_) == len(indirect_) assert isinstance(direct_, np.ndarray) assert isinstance(indirect_, np.ndarray) assert direct_.dtype == indirect_.dtype - np.testing.assert_allclose(direct_, indirect_, atol=1e-12) + np.testing.assert_allclose(np.array(direct_), indirect_, atol=1e-12) def test_no_real_attribute(monkeypatch): diff --git a/qutip/tests/core/test_operators.py b/qutip/tests/core/test_operators.py index 664eb2fc0f..2b883b2871 100644 --- a/qutip/tests/core/test_operators.py +++ b/qutip/tests/core/test_operators.py @@ -146,6 +146,7 @@ def test_position(): expected = (np.diag((np.arange(1, N) / 2)**0.5, k=-1) + np.diag((np.arange(1, N) / 2)**0.5, k=1)) np.testing.assert_allclose(operator.full(), expected) + assert operator._isherm == True def test_momentum(): @@ -153,6 +154,7 @@ def test_momentum(): expected = (np.diag((np.arange(1, N) / 2)**0.5, k=-1) - np.diag((np.arange(1, N) / 2)**0.5, k=1)) * 1j np.testing.assert_allclose(operator.full(), expected) + assert operator._isherm == True def test_squeeze(): diff --git a/qutip/tests/core/test_qobj.py b/qutip/tests/core/test_qobj.py index 9cb9e77206..85e091b608 100644 --- a/qutip/tests/core/test_qobj.py +++ b/qutip/tests/core/test_qobj.py @@ -10,6 +10,11 @@ from qutip.core import data as _data +@pytest.fixture(params=[_data.CSR, _data.Dense], ids=["CSR", "Dense"]) +def datatype(request): + return request.param + + def _random_not_singular(N): """ return a N*N complex array with determinant not 0. @@ -352,6 +357,58 @@ def test_QobjMulNotValidScalar(not_scalar): q1 * not_scalar +# Allowed division operations (scalar) +@pytest.mark.parametrize("scalar", + [2+2j, np.array(2+2j), np.array([2+2j])], + ids=[ + "python_number", + "scalar_like_array_shape_0", + "scalar_like_array_shape_1", + ]) +def test_QobjDivisionValidScalar(scalar): + "Tests multiplication of Qobj times scalar." + data = np.array([[1, 2], [3, 4]]) + q = qutip.Qobj(data) + expect = data / (2+2j) + + # Check __truediv__ + result = q / scalar + assert np.all(result.full() == expect) + + +# Similar as in test_QobjMulNotValidScalar, we do not allow multiplication by +# non scalar numpy values. This should also be removed in case of implementing +# broadcasting rules for Qobj. +@pytest.mark.parametrize("not_scalar", + [np.array([2j, 1]), np.array([[1, 2], [3, 4]])], + ids=[ + "not_scalar_like_vector", + "not_scalar_like_matrix", + ]) +def test_QobjDivisionNotValidScalar(not_scalar): + q1 = qutip.Qobj(np.array([[1, 2], [3, 4]])) + + with pytest.raises(TypeError): + q1 / not_scalar + + +def test_QobjNotImplemented(): + class MockerScalar(): + def __mul__(self, other): + return "object not accepted by _data.mul, mul" + + def __rmul__(self, other): + return "object not accepted by _data.mul, rmul" + + def __rtruediv__(self, other): + return "object not accepted by _data.mul, rtruediv" + + qobj = qutip.Qobj(np.array([[1, 2], [3, 4]])) + scalar = MockerScalar() + assert (qobj*scalar) == "object not accepted by _data.mul, rmul" + assert (scalar*qobj) == "object not accepted by _data.mul, mul" + assert (qobj/scalar) == "object not accepted by _data.mul, rtruediv" + def test_QobjDivision(): "qutip.Qobj division" data = _random_not_singular(5) @@ -641,11 +698,11 @@ def test_QobjPurity(): np.testing.assert_allclose(rho_mixed.purity(), 0.5) -def test_QobjPermute(): +def test_QobjPermute(datatype): "qutip.Qobj permute" - A = qutip.basis(3, 0) - B = qutip.basis(5, 4) - C = qutip.basis(4, 2) + A = qutip.basis(3, 0, dtype=datatype) + B = qutip.basis(5, 4, dtype=datatype) + C = qutip.basis(4, 2, dtype=datatype) psi = qutip.tensor(A, B, C) psi2 = psi.permute([2, 0, 1]) assert psi2 == qutip.tensor(C, A, B) @@ -654,17 +711,17 @@ def test_QobjPermute(): psi2_bra = psi_bra.permute([2, 0, 1]) assert psi2_bra == qutip.tensor(C, A, B).dag() - A = qutip.fock_dm(3, 0) - B = qutip.fock_dm(5, 4) - C = qutip.fock_dm(4, 2) + A = qutip.fock_dm(3, 0, dtype=datatype) + B = qutip.fock_dm(5, 4, dtype=datatype) + C = qutip.fock_dm(4, 2, dtype=datatype) rho = qutip.tensor(A, B, C) rho2 = rho.permute([2, 0, 1]) assert rho2 == qutip.tensor(C, A, B) for _ in range(3): - A = qutip.rand_ket(3) - B = qutip.rand_ket(4) - C = qutip.rand_ket(5) + A = qutip.rand_ket(3, dtype=datatype) + B = qutip.rand_ket(4, dtype=datatype) + C = qutip.rand_ket(5, dtype=datatype) psi = qutip.tensor(A, B, C) psi2 = psi.permute([1, 0, 2]) assert psi2 == qutip.tensor(B, A, C) @@ -674,9 +731,9 @@ def test_QobjPermute(): assert psi2_bra == qutip.tensor(B, A, C).dag() for _ in range(3): - A = qutip.rand_dm(3) - B = qutip.rand_dm(4) - C = qutip.rand_dm(5) + A = qutip.rand_dm(3, dtype=datatype) + B = qutip.rand_dm(4, dtype=datatype) + C = qutip.rand_dm(5, dtype=datatype) rho = qutip.tensor(A, B, C) rho2 = rho.permute([1, 0, 2]) assert rho2 == qutip.tensor(B, A, C) @@ -692,7 +749,7 @@ def test_QobjPermute(): for _ in range(3): super_dims = [3, 5, 4] - U = qutip.rand_unitary(super_dims) + U = qutip.rand_unitary(super_dims, dtype=datatype) Unew = U.permute([2, 1, 0]) S_tens = qutip.to_super(U) S_tens_new = qutip.to_super(Unew) diff --git a/qutip/tests/core/test_qobjevo.py b/qutip/tests/core/test_qobjevo.py index cb0fe34996..2e9d4a07c3 100644 --- a/qutip/tests/core/test_qobjevo.py +++ b/qutip/tests/core/test_qobjevo.py @@ -1,7 +1,8 @@ import operator import pytest -from qutip import * +from qutip import (Qobj, QobjEvo, coefficient, qeye, sigmax, sigmaz, + rand_stochastic, rand_herm, rand_ket, liouvillian) import numpy as np from numpy.testing import assert_allclose @@ -59,18 +60,18 @@ def _cplx(t, args): real_qevo = Pseudo_qevo( - rand_stochastic(N).to(data.CSR), - rand_stochastic(N).to(data.CSR), + rand_stochastic(N).to(_data.CSR), + rand_stochastic(N).to(_data.CSR), _real, "sin(t*w1)", args) herm_qevo = Pseudo_qevo( - rand_herm(N).to(data.Dense), - rand_herm(N).to(data.Dense), + rand_herm(N).to(_data.Dense), + rand_herm(N).to(_data.Dense), _real, "sin(t*w1)", args) cplx_qevo = Pseudo_qevo( - rand_stochastic(N).to(data.Dense), - rand_stochastic(N).to(data.CSR) + rand_stochastic(N).to(data.CSR) * 1j, + rand_stochastic(N).to(_data.Dense), + rand_stochastic(N).to(_data.CSR) + rand_stochastic(N).to(_data.CSR) * 1j, _cplx, "exp(1j*t*w2)", args) @@ -300,12 +301,6 @@ def test_QobjEvo_pickle(all_qevo): recreated = pickle.loads(pickled) _assert_qobjevo_equivalent(recreated, obj) -def test_shift(all_qevo): - dt = 0.2 - obj = all_qevo - shited = obj._insert_time_shift(dt) - for t in TESTTIMES: - _assert_qobj_almost_eq(obj(t + dt), shited(t)) def test_mul_vec(all_qevo): "QobjEvo matmul ket" @@ -315,6 +310,7 @@ def test_mul_vec(all_qevo): assert_allclose((op(t) @ vec).full(), op.matmul(t, vec).full(), atol=1e-14) + def test_matmul(all_qevo): "QobjEvo matmul oper" mat = np.random.rand(N, N) + 1 + 1j * np.random.rand(N, N) @@ -355,7 +351,7 @@ def test_expect_rho(all_qevo): @pytest.mark.parametrize('dtype', [pytest.param(dtype, id=dtype.__name__) - for dtype in core.data.to.dtypes]) + for dtype in _data.to.dtypes]) def test_convert(all_qevo, dtype): "QobjEvo expect rho" op = all_qevo.to(dtype) @@ -380,14 +376,14 @@ def test_compress(): @pytest.mark.parametrize(['qobjdtype'], [pytest.param(dtype, id=dtype.__name__) - for dtype in core.data.to.dtypes]) + for dtype in _data.to.dtypes]) @pytest.mark.parametrize(['statedtype'], [pytest.param(dtype, id=dtype.__name__) - for dtype in core.data.to.dtypes]) + for dtype in _data.to.dtypes]) def test_layer_support(qobjdtype, statedtype): N = 10 qevo = QobjEvo(rand_herm(N).to(qobjdtype)) - state_dense = rand_ket(N).to(core.data.Dense) + state_dense = rand_ket(N).to(_data.Dense) state = state_dense.to(statedtype).data state_dense = state_dense.data exp_any = qevo.expect_data(0, state) @@ -431,3 +427,15 @@ def test_QobjEvo_step_coeff(): assert qobjevo(6.7)[0,1] == coeff2[4] assert qobjevo(7.9999)[0,1] == coeff2[4] assert qobjevo(3.9999)[0,1] == coeff2[1] + + +def test_QobjEvo_isherm_flag_knowcase(): + assert QobjEvo(sigmax())(0)._isherm is True + non_hermitian = sigmax() + 1j + non_hermitian.isherm # set flag + assert QobjEvo(non_hermitian)(0)._isherm is False + assert QobjEvo([sigmax(), sigmaz()])(0)._isherm is True + assert QobjEvo([sigmax(), "t"])(0)._isherm is True + assert QobjEvo([sigmax(), "1j"])(0)._isherm is None + assert QobjEvo([[sigmax(), "t"], [sigmaz(), "1"]])(0)._isherm is True + assert QobjEvo([[sigmax(), "t"], [sigmaz(), "1j"]])(0)._isherm is None diff --git a/qutip/tests/solve/test_floquet.py b/qutip/tests/solve/test_floquet.py deleted file mode 100644 index 1bbd9b6f95..0000000000 --- a/qutip/tests/solve/test_floquet.py +++ /dev/null @@ -1,411 +0,0 @@ -import numpy as np -from qutip import fsesolve, sigmax, sigmaz, rand_ket, num, mesolve, sigmay -from qutip import sigmap, sigmam, floquet_master_equation_rates, expect, Qobj -from qutip import floquet_modes, floquet_modes_table, fmmesolve -from qutip import floquet_modes_t_lookup -import pytest - - -class TestFloquet: - """ - A test class for the QuTiP functions for Floquet formalism. - """ - - def testFloquetUnitary(self): - """ - Floquet: test unitary evolution of time-dependent two-level system - """ - - delta = 1.0 * 2 * np.pi - eps0 = 1.0 * 2 * np.pi - A = 0.5 * 2 * np.pi - omega = np.sqrt(delta ** 2 + eps0 ** 2) - T = (2 * np.pi) / omega - tlist = np.linspace(0.0, 2 * T, 101) - psi0 = rand_ket(2) - H0 = - eps0 / 2.0 * sigmaz() - delta / 2.0 * sigmax() - H1 = A / 2.0 * sigmax() - args = {'w': omega} - H = [H0, [H1, lambda t, args: np.sin(args['w'] * t)]] - e_ops = [num(2)] - - # Solve schrodinger equation with floquet solver - sol = fsesolve(H, psi0, tlist, e_ops, T, args) - - # Compare with results from standard schrodinger equation - sol_ref = mesolve(H, psi0, tlist, [], e_ops, args) - - np.testing.assert_allclose(sol.expect[0], sol_ref.expect[0], atol=1e-4) - - def testFloquetMasterEquation1(self): - """ - Test Floquet-Markov Master Equation for a driven two-level system - without dissipation. - """ - - delta = 1.0 * 2 * np.pi - eps0 = 1.0 * 2 * np.pi - A = 0.5 * 2 * np.pi - omega = np.sqrt(delta ** 2 + eps0 ** 2) - T = (2 * np.pi) / omega - tlist = np.linspace(0.0, 2 * T, 101) - psi0 = rand_ket(2) - H0 = - eps0 / 2.0 * sigmaz() - delta / 2.0 * sigmax() - H1 = A / 2.0 * sigmax() - args = {'w': omega} - H = [H0, [H1, lambda t, args: np.sin(args['w'] * t)]] - e_ops = [num(2)] - gamma1 = 0 - - # Collapse operator for Floquet-Markov Master Equation - c_op_fmmesolve = sigmax() - - # Collapse operators for Lindblad Master Equation - def noise_spectrum(omega): - if omega > 0: - return 0.5 * gamma1 * omega/(2*np.pi) - else: - return 0 - - ep, vp = H0.eigenstates() - op0 = vp[0]*vp[0].dag() - op1 = vp[1]*vp[1].dag() - - c_op_mesolve = [] - for i in range(2): - for j in range(2): - if i != j: - # caclculate the rate - gamma = 2 * np.pi * c_op_fmmesolve.matrix_element( - vp[j], vp[i]) * c_op_fmmesolve.matrix_element( - vp[i], vp[j]) * noise_spectrum(ep[j] - ep[i]) - - # add c_op for mesolve - c_op_mesolve.append( - np.sqrt(gamma) * (vp[i] * vp[j].dag()) - ) - - # Find the floquet modes - f_modes_0, f_energies = floquet_modes(H, T, args) - - # Precalculate mode table - f_modes_table_t = floquet_modes_table(f_modes_0, f_energies, - np.linspace(0, T, 500 + 1), - H, T, args) - - # Solve the floquet-markov master equation - output1 = fmmesolve(H, psi0, tlist, [c_op_fmmesolve], [], - [noise_spectrum], T, args, floquet_basis=True) - - # Calculate expectation values in the computational basis - p_ex = np.zeros(np.shape(tlist), dtype=complex) - for idx, t in enumerate(tlist): - f_modes_t = floquet_modes_t_lookup(f_modes_table_t, t, T) - f_states_t = [np.exp(-1j*t*f_energies[0])*f_modes_t[0], - np.exp(-1j*t*f_energies[1])*f_modes_t[1]] - p_ex[idx] = expect(num(2), output1.states[idx].transform( - f_states_t, True)) - - # Compare with mesolve - output2 = mesolve(H, psi0, tlist, c_op_mesolve, [], args) - p_ex_ref = expect(num(2), output2.states) - - np.testing.assert_allclose(np.real(p_ex), np.real(p_ex_ref), atol=1e-4) - - def testFloquetMasterEquation2(self): - """ - Test Floquet-Markov Master Equation for a two-level system - subject to dissipation. - """ - - delta = 1.0 * 2 * np.pi - eps0 = 1.0 * 2 * np.pi - A = 0.5 * 2 * np.pi - omega = np.sqrt(delta ** 2 + eps0 ** 2) - T = (2 * np.pi) / omega - tlist = np.linspace(0.0, 2 * T, 101) - psi0 = rand_ket(2) - H0 = - eps0 / 2.0 * sigmaz() - delta / 2.0 * sigmax() - H1 = A / 2.0 * sigmax() - args = {'w': omega} - H = [H0, [H1, lambda t, args: np.sin(args['w'] * t)]] - e_ops = [num(2)] - gamma1 = 1 - - A = 0. * 2 * np.pi - psi0 = rand_ket(2) - H1 = A / 2.0 * sigmax() - args = {'w': omega} - H = [H0, [H1, lambda t, args: np.sin(args['w'] * t)]] - - # Collapse operator for Floquet-Markov Master Equation - c_op_fmmesolve = sigmax() - - # Collapse operator for Lindblad Master Equation - def noise_spectrum(omega): - if omega > 0: - return 0.5 * gamma1 * omega/(2*np.pi) - else: - return 0 - - ep, vp = H0.eigenstates() - op0 = vp[0]*vp[0].dag() - op1 = vp[1]*vp[1].dag() - - c_op_mesolve = [] - for i in range(2): - for j in range(2): - if i != j: - # caclculate the rate - gamma = 2 * np.pi * c_op_fmmesolve.matrix_element( - vp[j], vp[i]) * c_op_fmmesolve.matrix_element( - vp[i], vp[j]) * noise_spectrum(ep[j] - ep[i]) - - # add c_op for mesolve - c_op_mesolve.append( - np.sqrt(gamma) * (vp[i] * vp[j].dag()) - ) - - # Find the floquet modes - f_modes_0, f_energies = floquet_modes(H, T, args) - - # Precalculate mode table - f_modes_table_t = floquet_modes_table(f_modes_0, f_energies, - np.linspace(0, T, 500 + 1), - H, T, args) - - # Solve the floquet-markov master equation - output1 = fmmesolve( - H, psi0, tlist, [c_op_fmmesolve], [], - [noise_spectrum], T, args, floquet_basis=True) - # Calculate expectation values in the computational basis - p_ex = np.zeros(np.shape(tlist), dtype=complex) - for idx, t in enumerate(tlist): - f_modes_t = floquet_modes_t_lookup(f_modes_table_t, t, T) - f_states_t = [np.exp(-1j*t*f_energies[0])*f_modes_t[0], - np.exp(-1j*t*f_energies[1])*f_modes_t[1]] - p_ex[idx] = expect(num(2), output1.states[idx].transform( - f_states_t, - True)) - - # Compare with mesolve - output2 = mesolve(H, psi0, tlist, c_op_mesolve, [], args) - p_ex_ref = expect(num(2), output2.states) - - np.testing.assert_allclose(np.real(p_ex), np.real(p_ex_ref), atol=1e-4) - - @pytest.mark.parametrize("kmax", [5, 100, 300]) - def testFloquetMasterEquation3(self, kmax): - """ - Test Floquet-Markov Master Equation for a two-level system - subject to dissipation with internal transform of fmmesolve - """ - - delta = 1.0 * 2 * np.pi - eps0 = 1.0 * 2 * np.pi - A = 0.5 * 2 * np.pi - omega = np.sqrt(delta ** 2 + eps0 ** 2) - T = (2 * np.pi) / omega - tlist = np.linspace(0.0, 2 * T, 101) - psi0 = rand_ket(2) - H0 = - eps0 / 2.0 * sigmaz() - delta / 2.0 * sigmax() - H1 = A / 2.0 * sigmax() - args = {'w': omega} - H = [H0, [H1, lambda t, args: np.sin(args['w'] * t)]] - e_ops = [num(2)] - gamma1 = 1 - - A = 0. * 2 * np.pi - psi0 = rand_ket(2) - H1 = A / 2.0 * sigmax() - args = {'w': omega} - H = [H0, [H1, lambda t, args: np.sin(args['w'] * t)]] - - # Collapse operator for Floquet-Markov Master Equation - c_op_fmmesolve = sigmax() - - # Collapse operator for Lindblad Master Equation - def noise_spectrum(omega): - if omega > 0: - return 0.5 * gamma1 * omega/(2*np.pi) - else: - return 0 - - ep, vp = H0.eigenstates() - op0 = vp[0]*vp[0].dag() - op1 = vp[1]*vp[1].dag() - - c_op_mesolve = [] - for i in range(2): - for j in range(2): - if i != j: - # caclculate the rate - gamma = 2*np.pi*c_op_fmmesolve.matrix_element( - vp[j], vp[i])*c_op_fmmesolve.matrix_element( - vp[i], vp[j])*noise_spectrum(ep[j]-ep[i]) - - # add c_op for mesolve - c_op_mesolve.append( - np.sqrt(gamma) * (vp[i] * vp[j].dag()) - ) - - # Solve the floquet-markov master equation - output1 = fmmesolve( - H, psi0, tlist, [c_op_fmmesolve], [num(2)], - [noise_spectrum], T, args, floquet_basis=False, - kmax=kmax) - p_ex = output1.expect[0] - # Compare with mesolve - output2 = mesolve(H, psi0, tlist, c_op_mesolve, [num(2)], args) - p_ex_ref = output2.expect[0] - - np.testing.assert_allclose(np.real(p_ex), np.real(p_ex_ref), - atol=5 * 1e-4) - - def testFloquetMasterEquation_multiple_coupling(self): - """ - Test Floquet-Markov Master Equation for a two-level system - subject to dissipation with multiple coupling operators - """ - - delta = 1.0 * 2 * np.pi - eps0 = 1.0 * 2 * np.pi - A = 0.5 * 2 * np.pi - omega = np.sqrt(delta ** 2 + eps0 ** 2) - T = (2 * np.pi) / omega - tlist = np.linspace(0.0, 2 * T, 101) - psi0 = rand_ket(2) - H0 = - eps0 / 2.0 * sigmaz() - delta / 2.0 * sigmax() - H1 = A / 2.0 * sigmax() - args = {'w': omega} - H = [H0, [H1, lambda t, args: np.sin(args['w'] * t)]] - e_ops = [num(2)] - gamma1 = 1 - - A = 0. * 2 * np.pi - psi0 = rand_ket(2) - H1 = A / 2.0 * sigmax() - args = {'w': omega} - H = [H0, [H1, lambda t, args: np.sin(args['w'] * t)]] - - # Collapse operator for Floquet-Markov Master Equation - c_ops_fmmesolve = [sigmax(), sigmay()] - - # Collapse operator for Lindblad Master Equation - def noise_spectrum1(omega): - if omega > 0: - return 0.5 * gamma1 * omega/(2*np.pi) - else: - return 0 - def noise_spectrum2(omega): - if omega > 0: - return 0.5 * gamma1 / (2 * np.pi) - else: - return 0 - - noise_spectra = [noise_spectrum1, noise_spectrum2] - - ep, vp = H0.eigenstates() - op0 = vp[0]*vp[0].dag() - op1 = vp[1]*vp[1].dag() - - c_op_mesolve = [] - - # Convert the c_ops for fmmesolve to c_ops for mesolve - for c_op_fmmesolve, noise_spectrum in zip(c_ops_fmmesolve, - noise_spectra): - for i in range(2): - for j in range(2): - if i != j: - # caclculate the rate - gamma = 2*np.pi*c_op_fmmesolve.matrix_element( - vp[j], vp[i])*c_op_fmmesolve.matrix_element( - vp[i], vp[j])*noise_spectrum(ep[j]-ep[i]) - # add c_op for mesolve - c_op_mesolve.append( - np.sqrt(gamma) * (vp[i] * vp[j].dag()) - ) - - # Solve the floquet-markov master equation - output1 = fmmesolve( - H, psi0, tlist, c_ops_fmmesolve, [num(2)], - noise_spectra, T, args, floquet_basis=False) - p_ex = output1.expect[0] - # Compare with mesolve - output2 = mesolve(H, psi0, tlist, c_op_mesolve, [num(2)], args) - p_ex_ref = output2.expect[0] - - np.testing.assert_allclose(np.real(p_ex), np.real(p_ex_ref), atol=1e-4) - - def testFloquetRates(self): - """ - Compare rate transition and frequency transitions to analytical - results for a driven two-level system, for different drive amplitudes. - """ - - # Parameters - wq = 5.4 * 2 * np.pi - wd = 6.7 * 2 * np.pi - delta = wq - wd - T = 2 * np.pi / wd - tlist = np.linspace(0.0, 2 * T, 101) - array_A = np.linspace(0.001, 3 * 2 * np.pi, 10, endpoint=False) - H0 = Qobj(wq/2 * sigmaz()) - arg = {'wd': wd} - c_ops = sigmax() - gamma1 = 1 - - delta_ana_deltas = [] - array_ana_E0 = [-np.sqrt((delta/2)**2 + a**2) for a in array_A] - array_ana_E1 = [np.sqrt((delta/2)**2 + a**2) for a in array_A] - array_ana_delta = [2*np.sqrt((delta/2)**2 + a**2) for a in array_A] - - def noise_spectrum(omega): - if omega > 0: - return 0.5 * gamma1 * omega/(2*np.pi) - else: - return 0 - - idx = 0 - for a in array_A: - # Hamiltonian - H1_p = Qobj(a * sigmap()) - H1_m = Qobj(a * sigmam()) - H = [H0, [H1_p, lambda t, args: np.exp(-1j * arg['wd'] * t)], - [H1_m, lambda t, args: np.exp(1j * arg['wd'] * t)]] - - # Floquet modes - fmodes0, fenergies = floquet_modes(H, T, args={}, sort=True) - f_modes_table_t = floquet_modes_table(fmodes0, fenergies, - tlist, H, T, - args={}) - # Get X delta - DeltaMatrix, X, frates, Amat = floquet_master_equation_rates( - fmodes0, fenergies, - c_ops, H, T, {}, - noise_spectrum, 0, 5) - # Check energies - deltas = np.ndarray.flatten(DeltaMatrix) - - # deltas and array_ana_delta have at least 1 value in common. - assert (min(abs(deltas-array_ana_delta[idx])) < 1e-4) - - # Check matrix elements - Xs = np.ndarray.flatten(X) - - normPlus = np.sqrt(a**2 + (array_ana_E1[idx] - delta/2)**2) - normMinus = np.sqrt(a**2 + (array_ana_E0[idx] - delta/2)**2) - - Xpp_p1 = (a/normPlus**2)*(array_ana_E1[idx]-delta/2) - assert (min(abs(Xs-Xpp_p1)) < 1e-4) - Xpp_m1 = (a/normPlus**2)*(array_ana_E1[idx]-delta/2) - assert (min(abs(Xs-Xpp_m1)) < 1e-4) - Xmm_p1 = (a/normMinus**2)*(array_ana_E0[idx]-delta/2) - assert (min(abs(Xs-Xmm_p1)) < 1e-4) - Xmm_m1 = (a/normMinus**2)*(array_ana_E0[idx]-delta/2) - assert (min(abs(Xs-Xmm_m1)) < 1e-4) - Xpm_p1 = (a/(normMinus*normPlus))*(array_ana_E0[idx]-delta/2) - assert (min(abs(Xs-Xmm_p1)) < 1e-4) - Xpm_m1 = (a/(normMinus*normPlus))*(array_ana_E1[idx]-delta/2) - assert (min(abs(Xs-Xpm_m1)) < 1e-4) - idx += 1 diff --git a/qutip/tests/solve/test_krylovsolve.py b/qutip/tests/solve/test_krylovsolve.py deleted file mode 100644 index d6fc75a950..0000000000 --- a/qutip/tests/solve/test_krylovsolve.py +++ /dev/null @@ -1,515 +0,0 @@ -from qutip.core.data.eigen import eigs -from qutip import krylovsolve, sesolve -from qutip.solve.solver import SolverOptions, Result -from qutip import tensor, Qobj, basis, expect, ket -from qutip import sigmax, sigmay, sigmaz, qeye, jmat -from qutip import rand_herm, rand_ket, rand_unitary, num, destroy, create -import os -import math -import types -import pytest -from scipy.linalg import expm -import numpy as np - -np.random.seed(0) - - -@pytest.fixture(params=[ - pytest.param(50, id="small dim"), - pytest.param(150, id="intermediate dim"), - pytest.param(300, id="large dim"), -]) -def dimensions(request): - return request.param - - -@pytest.fixture(params=[ - pytest.param(np.linspace(0, 5, 200), id="normal tlist"), - pytest.param([2], id="single element tlist"), - pytest.param([], id="empty tlist"), -]) -def tlists(request): - return request.param - - -@pytest.fixture(params=[ - pytest.param((ket([1, 0, 0, 0]), 0.5, 0, 1), id="eigenstate"), - pytest.param((ket([0, 0, 1, 0]), 1.0, 1, 0), - id="magnetization subspace state XXZ model"), -]) -def happy_breakdown_parameters(request): - return request.param - - -def h_sho(dim): - U = rand_unitary(dim) - H = U * (num(dim) + 0.5) * U.dag() - H = H / np.linalg.norm(H.full(), ord=2) - return H - - -def h_ising_transverse( - N: int, hx: float, hz: float, Jx: float, Jy: float, Jz: float -): - id = qeye(2) - sx, sy, sz = sigmax(), sigmay(), sigmaz() - sx_list = [tensor([id] * n + [sx] + [id] * (N-n-1)) for n in range(N)] - sy_list = [tensor([id] * n + [sy] + [id] * (N-n-1)) for n in range(N)] - sz_list = [tensor([id] * n + [sz] + [id] * (N-n-1)) for n in range(N)] - - # construct the hamiltonian - H = 0 - - # energy splitting terms - for n in range(N): - H += hz * sz_list[n] - H += hx * sx_list[n] - - if n < N-1: - # interaction terms - H += -Jx * sx_list[n] * sx_list[n + 1] - H += -Jy * sy_list[n] * sy_list[n + 1] - H += -Jz * sz_list[n] * sz_list[n + 1] - - return H - - -def create_test_e_ops(e_ops_type, dim): - """Creates e_ops used for testing give H and dim.""" - if e_ops_type == "[c]": - e_ops = [lambda t, psi: expect(num(dim), psi)] - if e_ops_type == "[q]": - e_ops = [jmat((dim - 1)/2, "x")] - if e_ops_type == "[c, c]": - e_ops = [lambda t, psi: expect(num(dim), psi), - lambda t, psi: expect(num(dim)/2, psi)] - if e_ops_type == "[c, q]": - e_ops = [lambda t, psi: expect(num(dim), psi), - jmat((dim - 1) / 2.0, "x")] - if e_ops_type == "[q, q]": - e_ops = [jmat((dim - 1) / 2.0, "x"), - jmat((dim - 1) / 2.0, "y"), - jmat((dim - 1) / 2.0, "z")] - return e_ops - - -def err_psi(psi_a, psi_b): - """Error between to kets.""" - err = 1 - np.abs(psi_a.overlap(psi_b)) ** 2 - return err - - -def expect_value(e_ops, res_1, tlist): - """Calculates expectation values of results object.""" - if isinstance(e_ops, types.FunctionType): - expect_values = [ - e_ops(t, state) for (t, state) in zip(tlist, res_1.states) - ] - else: - expect_values = [expect(e_ops, state) for state in res_1.states] - return expect_values - - -def assert_err_states_less_than_tol(res_1, res_2, tol): - """Asserts error of states of two results less than tol.""" - - err_states = [err_psi(psi_1, psi_2) for (psi_1, psi_2) - in zip(res_1.states, res_2.states)] - for err in err_states: - assert err <= tol,\ - f"err in states {err} is > than tolerance {tol}." - - -def assert_err_expect_less_than_tol(exp_1, exp_2, tol): - """Asserts error of expect values of two results less than tol.""" - err_expect = [np.abs(ex_1 - ex_2) for (ex_1, ex_2) - in zip(exp_1, exp_2)] - - for err in err_expect: - assert err <= tol, \ - f"err in expect values {err} is > than tol {tol}." - - -def exactsolve(H, psi0, tlist): - """Calculates exact solution by direct diagonalization.""" - - dims = psi0.dims - eigenvalues, eigenvectors = eigs(H.data, True) - eigenvectors = eigenvectors.to_array() - - psi0 = psi0.full() - - psi_base_diag = np.matmul(np.linalg.inv(eigenvectors), psi0) - U = np.exp(np.outer(-1j * eigenvalues, tlist)) - psi_list = np.matmul( - eigenvectors, np.multiply(U, psi_base_diag.reshape([-1, 1])) - ) - exact_results = Result() - exact_results.states = [Qobj(state, dims=dims) for state in psi_list.T] - - return exact_results - - -class TestKrylovSolve: - """ - A test class for the QuTiP Krylov Approximation method Solver. - """ - - def check_sparse_vs_dense(self, output_sparse, output_dense, tol=1e-5): - "simple check of errors between two outputs states" - - assert_err_states_less_than_tol(res_1=output_sparse, - res_2=output_dense, - tol=tol) - - @pytest.mark.parametrize("density, dim", [(0.1, 128), (0.9, 300)], - ids=["sparse H check", "dense H check"]) - def test_01_check_sparse_vs_non_sparse_with_density_H( - self, density, dim, krylov_dim=20 - ): - "krylovsolve: comparing sparse vs non sparse." - psi0 = rand_ket(dim) - H_sparse = rand_herm(dim, density=0.1) - tlist = np.linspace(0, 10, 200) - - output_sparse = krylovsolve( - H_sparse, psi0, tlist, krylov_dim, sparse=True - ) - output_dense = krylovsolve( - H_sparse, psi0, tlist, krylov_dim, sparse=False - ) - - self.check_sparse_vs_dense(output_sparse, output_dense) - - def simple_check_states_e_ops( - self, - H, - psi0, - tlist, - tol=1e-5, - tol2=1e-4, - krylov_dim=25, - square_hamiltonian=True, - ): - """ - Compare integrated evolution with sesolve and exactsolve result. - """ - - options = SolverOptions(store_states=True) - - e_ops = [ - jmat((H.shape[0] - 1) / 2.0, "x"), - jmat((H.shape[0] - 1) / 2.0, "y"), - jmat((H.shape[0] - 1) / 2.0, "z"), - ] - if not square_hamiltonian: - _e_ops = [] - for op in e_ops: - op2 = op.copy() - op2.dims = H.dims - _e_ops.append(op2) - e_ops = _e_ops - - output = krylovsolve( - H, psi0, tlist, krylov_dim, e_ops=e_ops, options=options - ) - output_ss = sesolve(H, psi0, tlist, e_ops=e_ops, options=options) - output_exact = exactsolve(H, psi0, tlist) - - assert_err_states_less_than_tol(res_1=output, - res_2=output_ss, tol=tol) - - assert_err_states_less_than_tol(res_1=output, - res_2=output_exact, tol=tol) - - # for the operators, test against exactsolve for accuracy - for i in range(len(e_ops)): - - output_exact.expect = expect_value(e_ops[i], output_exact, tlist) - - assert_err_expect_less_than_tol(exp_1=output.expect[i], - exp_2=output_exact.expect, - tol=tol) - - def test_02_simple_check_states_e_ops_H_random(self): - "krylovsolve: states with const H random" - dim = 128 - psi0 = rand_ket(dim) - H = rand_herm(dim) - tlist = np.linspace(0, 10, 200) - - self.simple_check_states_e_ops(H, psi0, tlist) - - def test_03_simple_check_states_e_ops_H_ising_transverse(self): - "krylovsolve: states with const H Ising Transverse Field" - N = 6 - dim = 2**N - H = h_ising_transverse(N, hx=0.1, hz=0.5, Jx=1, Jy=0, Jz=1) - _dims = H.dims - _dims2 = [1] * N - psi0 = rand_ket(_dims[0]) - tlist = np.linspace(0, 20, 200) - - self.simple_check_states_e_ops( - H, psi0, tlist, square_hamiltonian=False) - - def test_04_simple_check_states_e_ops_H_sho(self): - "krylovsolve: states with const H SHO" - dim = 100 - psi0 = rand_ket(dim) - H = h_sho(dim) - tlist = np.linspace(0, 20, 200) - - self.simple_check_states_e_ops(H, psi0, tlist) - - def check_e_ops_none( - self, H, psi0, tlist, dim, krylov_dim=30, tol=1e-5, tol2=1e-4 - ): - "Check input possibilities when e_ops=None" - - krylov_outputs = krylovsolve(H, psi0, tlist, krylov_dim, e_ops=None) - - try: - sesolve_outputs = sesolve(H, psi0, tlist, e_ops=None) - except IndexError: # if tlist=[], sesolve breaks but krylov doesn't - pass - - if len(tlist) > 1: - assert_err_states_less_than_tol( - krylov_outputs, sesolve_outputs, tol) - elif len(tlist) == 1: - assert krylov_outputs.states == sesolve_outputs.states - else: - assert krylov_outputs.states == [] - - def test_05_check_e_ops_none(self, dimensions, tlists): - "krylovsolve: check e_ops=None inputs with random H different tlists." - psi0 = rand_ket(dimensions) - H = rand_herm(dimensions, density=0.5) - self.check_e_ops_none(H, psi0, tlists, dimensions) - - def check_e_ops_callable( - self, - H, - psi0, - tlist, - dim, - krylov_dim=35, - tol=1e-5, - square_hamiltonian=True, - ): - "Check input possibilities when e_ops=callable" - - def e_ops(t, psi): return expect(num(dim), psi) - - if not square_hamiltonian: - H.dims = [[H.shape[0]], [H.shape[0]]] - psi0.dims = [[H.shape[0]], [1]] - - krylov_outputs = krylovsolve(H, psi0, tlist, krylov_dim, e_ops=e_ops) - exact_output = exactsolve(H, psi0, tlist) - exact_output.expect = expect_value(e_ops=e_ops, - res_1=exact_output, - tlist=tlist) - - try: - sesolve_outputs = sesolve(H, psi0, tlist, e_ops=e_ops) - except IndexError: # if tlist=[], sesolve breaks but krylov doesn't - pass - - if len(tlist) > 1: - - assert len(krylov_outputs.expect) == len( - sesolve_outputs.expect - ), "shape of outputs between krylov and sesolve differs" - - assert_err_expect_less_than_tol(exp_1=krylov_outputs.expect, - exp_2=exact_output.expect, - tol=tol) - elif len(tlist) == 1: - assert ( - np.abs(krylov_outputs.expect[0] - sesolve_outputs.expect[0]) - <= 1e-7 - ), "krylov and sesolve outputs differ for len(tlist)=1" - else: - assert krylov_outputs.states == [] - - def test_06_check_e_ops_callable(self, dimensions, tlists): - "krylovsolve: check e_ops=call inputs with random H different tlists." - psi0 = rand_ket(dimensions) - H = rand_herm(dimensions, density=0.5) - self.check_e_ops_callable(H, psi0, tlists, dimensions) - - def check_e_ops_list_single_operator( - self, - e_ops, - H, - psi0, - tlist, - dim, - krylov_dim=35, - tol=1e-5, - square_hamiltonian=True, - ): - "Check input possibilities when e_ops=[callable | qobj]" - - if not square_hamiltonian: - H.dims = [[H.shape[0]], [H.shape[0]]] - psi0.dims = [[H.shape[0]], [1]] - - krylov_outputs = krylovsolve(H, psi0, tlist, krylov_dim, e_ops=e_ops) - exact_output = exactsolve(H, psi0, tlist) - exact_output.expect = expect_value(e_ops[0], exact_output, tlist) - - try: - sesolve_outputs = sesolve(H, psi0, tlist, e_ops=e_ops) - except IndexError: # if tlist=[], sesolve breaks but krylov doesn't - pass - - if len(tlist) > 1: - - assert len(krylov_outputs.expect) == len( - sesolve_outputs.expect - ), "shape of outputs between krylov and sesolve differs" - - assert_err_expect_less_than_tol(exp_1=krylov_outputs.expect[0], - exp_2=exact_output.expect, - tol=tol) - - elif len(tlist) == 1: - assert ( - np.abs(krylov_outputs.expect[0] - sesolve_outputs.expect[0]) - <= tol - ), "expect outputs from krylovsolve and sesolve are not equal" - else: - assert krylov_outputs.states == [] - - @pytest.mark.parametrize("e_ops_type", [("[c]"), ("[q]")], ids=["[c]", "[q]"]) - def test_07_check_e_ops_list_single_callable(self, e_ops_type, dimensions, tlists): - "krylovsolve: check e_ops=[call | qobj] random H different tlists." - psi0 = rand_ket(dimensions) - H = rand_herm(dimensions, density=0.5) - e_ops = create_test_e_ops(e_ops_type, dimensions) - self.check_e_ops_list_single_operator( - e_ops, H, psi0, tlists, dimensions) - - def check_e_ops_mixed_list( - self, e_ops, H, psi0, tlist, dim, krylov_dim=35, tol=1e-5, - ): - "Check input possibilities when e_ops=[call | qobj] and len(e_ops) > 1" - - krylov_outputs = krylovsolve(H, psi0, tlist, krylov_dim, e_ops=e_ops) - exact_output = exactsolve(H, psi0, tlist) - - try: - sesolve_outputs = sesolve(H, psi0, tlist, e_ops=e_ops) - except IndexError: # if tlist=[], sesolve breaks but krylov doesn't - pass - - if len(tlist) > 1: - - assert len(krylov_outputs.expect) == len( - sesolve_outputs.expect - ), "shape of outputs between krylov and sesolve differs" - - for idx, k_expect in enumerate(krylov_outputs.expect): - exact_output.expect = expect_value( - e_ops[idx], exact_output, tlist) - assert_err_expect_less_than_tol(exp_1=k_expect, - exp_2=exact_output.expect, - tol=tol) - elif len(tlist) == 1: - assert len(krylov_outputs.expect) == len( - sesolve_outputs.expect - ), "shape of outputs between krylov and sesolve differs" - for k_expect, ss_expect in zip( - krylov_outputs.expect, sesolve_outputs.expect - ): - assert ( - np.abs(k_expect - ss_expect) <= tol - ), "expect outputs from krylovsolve and sesolve are not equal" - else: - assert krylov_outputs.states == [] - - @pytest.mark.parametrize("e_ops_type", - [("[c, c]"), ("[c, q]"), ("[q, q]")], - ids=["[c, c]", "[c, q]", "[q, q]"]) - def test_08_check_e_ops_mixed_list(self, e_ops_type, dimensions, tlists): - "krylovsolve: check e_ops=[call | qobj] with len(e_ops)>1 for" - "random H different tlists." - psi0 = rand_ket(dimensions) - H = rand_herm(dimensions, density=0.5) - e_ops = create_test_e_ops(e_ops_type, dimensions) - self.check_e_ops_mixed_list(e_ops, H, psi0, tlists, dimensions) - - def test_9_happy_breakdown_simple(self, happy_breakdown_parameters): - "krylovsolve: check simple at happy breakdowns" - psi0, hz, Jx, Jz = happy_breakdown_parameters - krylov_dim = 12 - N = 4 - dim = 2**N - H = h_ising_transverse(N, hx=0, hz=hz, Jx=Jx, Jy=0, Jz=Jz) - tlist = np.linspace(0, 20, 200) - self.simple_check_states_e_ops( - H, psi0, tlist, krylov_dim=krylov_dim, square_hamiltonian=False - ) - - def test_10_happy_breakdown_e_ops_none(self, happy_breakdown_parameters): - "krylovsolve: check e_ops=None at happy breakdowns" - psi0, hz, Jx, Jz = happy_breakdown_parameters - krylov_dim = 12 - N = 4 - dim = 2**N - H = h_ising_transverse(N, hx=0, hz=hz, Jx=Jx, Jy=0, Jz=Jz) - tlist = np.linspace(0, 20, 200) - self.check_e_ops_none( - H, psi0, tlist, dim, krylov_dim=krylov_dim - ) - - def test_11_happy_breakdown_e_ops_callable(self, happy_breakdown_parameters): - "krylovsolve: check e_ops=callable at happy breakdowns" - psi0, hz, Jx, Jz = happy_breakdown_parameters - krylov_dim = 12 - N = 4 - dim = 2**N - H = h_ising_transverse(N, hx=0, hz=hz, Jx=Jx, Jy=0, Jz=Jz) - def e_ops(t, psi): return expect(num(dim), psi) - tlist = np.linspace(0, 20, 200) - - self.check_e_ops_callable( - H, - psi0, - tlist, - dim, - krylov_dim=krylov_dim, - square_hamiltonian=False, - ) - - def test_12_happy_breakdown_e_ops_list_single_callable(self, happy_breakdown_parameters): - "krylovsolve: check e_ops=[callable] at happy breakdowns" - psi0, hz, Jx, Jz = happy_breakdown_parameters - krylov_dim = 12 - N = 4 - dim = 2**N - H = h_ising_transverse(N, hx=0, hz=hz, Jx=Jx, Jy=0, Jz=Jz) - e_ops = [lambda t, psi: expect(num(dim), psi)] - - tlist = np.linspace(0, 20, 200) - self.check_e_ops_list_single_operator( - e_ops, - H, - psi0, - tlist, - dim, - krylov_dim=krylov_dim, - square_hamiltonian=False, - ) - - -def test_13_krylovsolve_bad_krylov_dim(dim=15, krylov_dim=20): - """Check errors from bad krylov dimension inputs.""" - H = rand_herm(dim) - psi0 = basis(dim, 0) - tlist = np.linspace(0, 1, 100) - with pytest.raises(ValueError) as exc: - krylovsolve(H, psi0, tlist, krylov_dim) diff --git a/qutip/tests/solve/test_mcsolve.py b/qutip/tests/solve/test_mcsolve.py deleted file mode 100644 index 2106bf5cc9..0000000000 --- a/qutip/tests/solve/test_mcsolve.py +++ /dev/null @@ -1,299 +0,0 @@ -import pytest -import numpy as np -import qutip - - -def _return_constant(t, args): - return args['constant'] - - -def _return_decay(t, args): - return args['constant'] * np.exp(-args['rate'] * t) - - -@pytest.mark.usefixtures("in_temporary_directory") -class StatesAndExpectOutputCase: - """ - Mixin class to test the states and expectation values from ``mcsolve``. - """ - size = 10 - h = qutip.num(size) - state = qutip.basis(size, size-1) - times = np.linspace(0, 1, 101) - e_ops = [qutip.num(size)] - ntraj = 750 - - def _assert_states(self, result, expected, tol): - assert hasattr(result, 'states') - assert len(result.states) == len(self.times) - for test_operator, expected_part in zip(self.e_ops, expected): - test = qutip.expect(test_operator, result.states) - np.testing.assert_allclose(test, expected_part, rtol=tol) - - def _assert_expect(self, result, expected, tol): - assert hasattr(result, 'expect') - assert len(result.expect) == len(self.e_ops) - for test, expected_part in zip(result.expect, expected): - np.testing.assert_allclose(test, expected_part, rtol=tol) - - def test_states_and_expect(self, hamiltonian, args, c_ops, expected, tol): - options = qutip.SolverOptions(average_states=True, store_states=True) - result = qutip.mcsolve(hamiltonian, self.state, self.times, args=args, - c_ops=c_ops, e_ops=self.e_ops, ntraj=self.ntraj, - options=options) - self._assert_expect(result, expected, tol) - self._assert_states(result, expected, tol) - - -class TestNoCollapse(StatesAndExpectOutputCase): - """ - Test that `mcsolve` correctly solves the system when there is a constant - Hamiltonian and no collapses. - """ - def pytest_generate_tests(self, metafunc): - tol = 1e-8 - expect = (qutip.expect(self.e_ops[0], self.state) - * np.ones_like(self.times)) - hamiltonian_types = [ - (self.h, {}, "Qobj", False), - ([self.h], {}, "list", False), - ([self.h, [self.h, '0']], {}, "string", True), - ([self.h, [self.h, _return_constant]], {'constant': 0}, - "function", False), - ] - cases = [] - for hamiltonian, args, id, slow in hamiltonian_types: - if slow and 'only' in metafunc.function.__name__: - # Skip the single-output test if it's a slow case. - continue - marks = [pytest.mark.slow] if slow else [] - cases.append(pytest.param(hamiltonian, args, [], [expect], tol, - id=id, marks=marks)) - metafunc.parametrize(['hamiltonian', 'args', 'c_ops', 'expected', - 'tol'], - cases) - - @pytest.mark.filterwarnings("ignore::UserWarning") - def test_states_and_expect(self, hamiltonian, args, c_ops, expected, tol): - super().test_states_and_expect(hamiltonian, args, c_ops, expected, tol) - - -class TestConstantCollapse(StatesAndExpectOutputCase): - """ - Test that `mcsolve` correctly solves the system when there is a constant - collapse operator. - """ - def pytest_generate_tests(self, metafunc): - tol = 0.05 - coupling = 0.2 - expect = (qutip.expect(self.e_ops[0], self.state) - * np.exp(-coupling * self.times)) - collapse_op = qutip.destroy(self.size) - c_op_types = [ - (np.sqrt(coupling)*collapse_op, {}, "constant"), - ([collapse_op, 'sqrt({})'.format(coupling)], {}, "string"), - ([collapse_op, _return_constant], {'constant': np.sqrt(coupling)}, - "function"), - ] - cases = [] - for c_op, args, id in c_op_types: - cases.append(pytest.param(self.h, args, [c_op], [expect], tol, - id=id, marks=[pytest.mark.slow])) - metafunc.parametrize(['hamiltonian', 'args', 'c_ops', 'expected', - 'tol'], - cases) - - # Previously the "states_only" and "expect_only" tests were mixed in to - # every other test case. We move them out into the simplest set so that - # their behaviour remains tested, but isn't repeated as often to keep test - # runtimes shorter. The known-good cases are still tested in the other - # test cases, this is just testing the single-output behaviour. - - def test_states_only(self, hamiltonian, args, c_ops, expected, tol): - options = qutip.SolverOptions(average_states=True, store_states=True) - result = qutip.mcsolve(hamiltonian, self.state, self.times, args=args, - c_ops=c_ops, e_ops=[], ntraj=self.ntraj, - options=options) - self._assert_states(result, expected, tol) - - def test_expect_only(self, hamiltonian, args, c_ops, expected, tol): - result = qutip.mcsolve(hamiltonian, self.state, self.times, args=args, - c_ops=c_ops, e_ops=self.e_ops, ntraj=self.ntraj) - self._assert_expect(result, expected, tol) - - -class TestTimeDependentCollapse(StatesAndExpectOutputCase): - """ - Test that `mcsolve` correctly solves the system when the collapse operators - are time-dependent. - """ - def pytest_generate_tests(self, metafunc): - tol = 0.1 - coupling = 0.2 - expect = (qutip.expect(self.e_ops[0], self.state) - * np.exp(-coupling * (1 - np.exp(-self.times)))) - collapse_op = qutip.destroy(self.size) - collapse_args = {'constant': np.sqrt(coupling), 'rate': 0.5} - collapse_string = 'sqrt({} * exp(-t))'.format(coupling) - c_op_types = [ - ([collapse_op, _return_decay], collapse_args, "function"), - ([collapse_op, collapse_string], {}, "string"), - ] - cases = [] - for c_op, args, id in c_op_types: - cases.append(pytest.param(self.h, args, [c_op], [expect], tol, - id=id, marks=[pytest.mark.slow])) - metafunc.parametrize(['hamiltonian', 'args', 'c_ops', 'expected', - 'tol'], - cases) - - -def test_stored_collapse_operators_and_times(): - """ - Test that the output contains information on which collapses happened and - at what times, and make sure that this information makes sense. - """ - size = 10 - a = qutip.destroy(size) - H = qutip.num(size) - state = qutip.basis(size, size-1) - times = np.linspace(0, 10, 100) - c_ops = [a, a] - result = qutip.mcsolve(H, state, times, c_ops, ntraj=1) - assert len(result.col_times[0]) > 0 - assert len(result.col_which) == len(result.col_times) - assert all(col in [0, 1] for col in result.col_which[0]) - - -@pytest.mark.parametrize('options', [ - pytest.param(qutip.SolverOptions(average_expect=True), - id="average_expect=True"), - pytest.param(qutip.SolverOptions(average_states=False), - id="average_states=False"), -]) -def test_expectation_dtype(options): - # We're just testing the output value, so it's important whether certain - # things are complex or real, but not what the magnitudes of constants are. - focks = 5 - a = qutip.tensor(qutip.destroy(focks), qutip.qeye(2)) - sm = qutip.tensor(qutip.qeye(focks), qutip.sigmam()) - H = 1j*a.dag()*sm + a - H = H + H.dag() - state = qutip.basis([focks, 2], [0, 1]) - times = np.linspace(0, 10, 5) - c_ops = [a, sm] - e_ops = [a.dag()*a, sm.dag()*sm, a] - data = qutip.mcsolve(H, state, times, c_ops, e_ops, ntraj=5, - options=options) - assert isinstance(data.expect[0][1], float) - assert isinstance(data.expect[1][1], float) - assert isinstance(data.expect[2][1], complex) - - -class TestSeeds: - sizes = [6, 6, 6] - dampings = [0.1, 0.4, 0.1] - ntraj = 25 # Big enough to ensure there are differences without being slow - a = [qutip.destroy(size) for size in sizes] - H = 1j * (qutip.tensor(a[0], a[1].dag(), a[2].dag()) - - qutip.tensor(a[0].dag(), a[1], a[2])) - state = qutip.tensor(qutip.coherent(sizes[0], np.sqrt(2)), - qutip.basis(sizes[1:], [0, 0])) - times = np.linspace(0, 10, 2) - c_ops = [ - np.sqrt(2*dampings[0]) * qutip.tensor(a[0], qutip.qeye(sizes[1:])), - (np.sqrt(2*dampings[1]) - * qutip.tensor(qutip.qeye(sizes[0]), a[1], qutip.qeye(sizes[2]))), - np.sqrt(2*dampings[2]) * qutip.tensor(qutip.qeye(sizes[:2]), a[2]), - ] - - @pytest.mark.xfail(reason="current limitation of SolverOptions") - def test_seeds_can_be_reused(self): - args = (self.H, self.state, self.times) - kwargs = {'c_ops': self.c_ops, 'ntraj': self.ntraj} - first = qutip.mcsolve(*args, **kwargs) - options = qutip.SolverOptions(seeds=first.seeds) - second = qutip.mcsolve(*args, options=options, **kwargs) - for first_t, second_t in zip(first.col_times, second.col_times): - np.testing.assert_equal(first_t, second_t) - for first_w, second_w in zip(first.col_which, second.col_which): - np.testing.assert_equal(first_w, second_w) - - def test_seeds_are_not_reused_by_default(self): - args = (self.H, self.state, self.times) - kwargs = {'c_ops': self.c_ops, 'ntraj': self.ntraj} - first = qutip.mcsolve(*args, **kwargs) - second = qutip.mcsolve(*args, **kwargs) - assert not all(np.array_equal(first_t, second_t) - for first_t, second_t in zip(first.col_times, - second.col_times)) - assert not all(np.array_equal(first_w, second_w) - for first_w, second_w in zip(first.col_which, - second.col_which)) - - -def test_list_ntraj(): - """Test that `ntraj` can be a list.""" - size = 5 - a = qutip.destroy(size) - H = qutip.num(size) - state = qutip.basis(size, 1) - times = np.linspace(0, 0.8, 100) - # Arbitrary coupling and bath temperature. - coupling = 1 / 0.129 - n_th = 0.063 - c_ops = [np.sqrt(coupling * (n_th + 1)) * a, - np.sqrt(coupling * n_th) * a.dag()] - e_ops = [qutip.num(size)] - ntraj = [1, 5, 15, 100] - mc = qutip.mcsolve(H, state, times, c_ops, e_ops, ntraj=ntraj) - assert len(ntraj) == len(mc.expect) - - -# Defined in module-scope so it's pickleable. -def _dynamic(t, args): - return 0 if args["collapse"] else 1 - - -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 = qutip.mcsolve(H, state, times, c_ops, ntraj=25, args={"collapse": []}) - assert all(len(collapses) <= 1 for collapses in mc.col_which) - - -def _regression_490_f1(t, args): - return t-1 - - -def _regression_490_f2(t, args): - return -t - - -@pytest.mark.filterwarnings("ignore:No c_ops, using sesolve:UserWarning") -def test_regression_490(): - """Test for regression of gh-490.""" - h = [qutip.sigmax(), - [qutip.sigmay(), _regression_490_f1], - [qutip.sigmaz(), _regression_490_f2]] - state = (qutip.basis(2, 0) + qutip.basis(2, 1)).unit() - times = np.linspace(0, 3, 10) - result_me = qutip.mesolve(h, state, times) - result_mc = qutip.mcsolve(h, state, times, ntraj=1) - for state_me, state_mc in zip(result_me.states, result_mc.states): - np.testing.assert_allclose(state_me.full(), state_mc.full(), atol=1e-8) - - -def test_mcsolve_bad_e_ops(): - H = qutip.sigmaz() - c_ops = [qutip.sigmax()] - psi0 = qutip.basis(2, 0) - tlist = np.linspace(0, 20, 200) - with pytest.raises(TypeError) as exc: - qutip.mcsolve(H, psi0, tlist=tlist, c_ops=c_ops, e_ops=[qutip.qeye(3)]) diff --git a/qutip/tests/solve/test_mesolve.py b/qutip/tests/solve/test_mesolve.py deleted file mode 100644 index 80dff7be62..0000000000 --- a/qutip/tests/solve/test_mesolve.py +++ /dev/null @@ -1,947 +0,0 @@ -from functools import partial - -import numpy as np -from numpy.testing import assert_, run_module_suite, assert_allclose -import pytest - -# disable the MC progress bar -import os - -from qutip import * -from qutip.random_objects import rand_ket - -os.environ['QUTIP_GRAPHICS'] = "NO" - - -class TestJCModelEvolution: - """ - A test class for the QuTiP functions for the evolution of JC model - """ - - def qubit_integrate(self, tlist, psi0, epsilon, delta, g1, g2): - - H = epsilon / 2.0 * sigmaz() + delta / 2.0 * sigmax() - - c_op_list = [] - - rate = g1 - if rate > 0.0: - c_op_list.append(np.sqrt(rate) * sigmam()) - - rate = g2 - if rate > 0.0: - c_op_list.append(np.sqrt(rate) * sigmaz()) - - output = mesolve( - H, psi0, tlist, c_op_list, [sigmax(), sigmay(), sigmaz()]) - expt_list = output.expect[0], output.expect[1], output.expect[2] - - return expt_list[0], expt_list[1], expt_list[2] - - def jc_steadystate(self, N, wc, wa, g, kappa, gamma, - pump, psi0, use_rwa, tlist): - - # Hamiltonian - a = tensor(destroy(N), identity(2)) - sm = tensor(identity(N), destroy(2)) - - if use_rwa: - # use the rotating wave approxiation - H = wc * a.dag( - ) * a + wa * sm.dag() * sm + g * (a.dag() * sm + a * sm.dag()) - else: - H = wc * a.dag() * a + wa * sm.dag() * sm + g * ( - a.dag() + a) * (sm + sm.dag()) - - # collapse operators - c_op_list = [] - - n_th_a = 0.0 # zero temperature - - rate = kappa * (1 + n_th_a) - c_op_list.append(np.sqrt(rate) * a) - - rate = kappa * n_th_a - if rate > 0.0: - c_op_list.append(np.sqrt(rate) * a.dag()) - - rate = gamma - if rate > 0.0: - c_op_list.append(np.sqrt(rate) * sm) - - rate = pump - if rate > 0.0: - c_op_list.append(np.sqrt(rate) * sm.dag()) - - # find the steady state - rho_ss = steadystate(H, c_op_list) - - return expect(a.dag() * a, rho_ss), expect(sm.dag() * sm, rho_ss) - - def jc_integrate(self, N, wc, wa, g, kappa, gamma, - pump, psi0, use_rwa, tlist): - - # Hamiltonian - a = tensor(destroy(N), identity(2)) - sm = tensor(identity(N), destroy(2)) - - if use_rwa: - # use the rotating wave approxiation - H = wc * a.dag() * a + wa * sm.dag() * sm + g * ( - a.dag() * sm + a * sm.dag()) - else: - H = wc * a.dag() * a + wa * sm.dag() * sm + g * ( - a.dag() + a) * (sm + sm.dag()) - - # collapse operators - c_op_list = [] - - n_th_a = 0.0 # zero temperature - - rate = kappa * (1 + n_th_a) - c_op_list.append(np.sqrt(rate) * a) - - rate = kappa * n_th_a - if rate > 0.0: - c_op_list.append(np.sqrt(rate) * a.dag()) - - rate = gamma - if rate > 0.0: - c_op_list.append(np.sqrt(rate) * sm) - - rate = pump - if rate > 0.0: - c_op_list.append(np.sqrt(rate) * sm.dag()) - - # evolve and calculate expectation values - output = mesolve( - H, psi0, tlist, c_op_list, [a.dag() * a, sm.dag() * sm]) - expt_list = output.expect[0], output.expect[1] - return expt_list[0], expt_list[1] - - def testQubitDynamics1(self): - "mesolve: qubit with dissipation" - - epsilon = 0.0 * 2 * np.pi # cavity frequency - delta = 1.0 * 2 * np.pi # atom frequency - g2 = 0.1 - g1 = 0.0 - psi0 = basis(2, 0) # initial state - tlist = np.linspace(0, 5, 200) - - sx, sy, sz = self.qubit_integrate(tlist, psi0, epsilon, delta, g1, g2) - - sx_analytic = np.zeros(np.shape(tlist)) - sy_analytic = -np.sin(2 * np.pi * tlist) * np.exp(-tlist * g2) - sz_analytic = np.cos(2 * np.pi * tlist) * np.exp(-tlist * g2) - - assert np.max(np.abs(sx - sx_analytic)) < 0.05 - assert np.max(np.abs(sy - sy_analytic)) < 0.05 - assert np.max(np.abs(sz - sz_analytic)) < 0.05 - - def testQubitDynamics2(self): - "mesolve: qubit without dissipation" - - epsilon = 0.0 * 2 * np.pi # cavity frequency - delta = 1.0 * 2 * np.pi # atom frequency - g2 = 0.0 - g1 = 0.0 - psi0 = basis(2, 0) # initial state - tlist = np.linspace(0, 5, 200) - - sx, sy, sz = self.qubit_integrate(tlist, psi0, epsilon, delta, g1, g2) - - sx_analytic = np.zeros(np.shape(tlist)) - sy_analytic = -np.sin(2 * np.pi * tlist) * np.exp(-tlist * g2) - sz_analytic = np.cos(2 * np.pi * tlist) * np.exp(-tlist * g2) - - assert np.max(np.abs(sx - sx_analytic)) < 0.05 - assert np.max(np.abs(sy - sy_analytic)) < 0.05 - assert np.max(np.abs(sz - sz_analytic)) < 0.05 - - def testCase1(self): - "mesolve: cavity-qubit interaction, no dissipation" - - use_rwa = True - N = 4 # number of cavity fock states - wc = 2 * np.pi * 1.0 # cavity frequency - wa = 2 * np.pi * 1.0 # atom frequency - g = 2 * np.pi * 0.01 # coupling strength - kappa = 0.0 # cavity dissipation rate - gamma = 0.0 # atom dissipation rate - pump = 0.0 # atom pump rate - - # start with an excited atom and maximum number of photons - n = N - 2 - psi0 = tensor(basis(N, n), basis(2, 1)) - tlist = np.linspace(0, 1000, 2000) - - nc, na = self.jc_integrate( - N, wc, wa, g, kappa, gamma, pump, psi0, use_rwa, tlist) - - nc_ex = (n + 0.5 * (1 - np.cos(2 * g * np.sqrt(n + 1) * tlist))) - na_ex = 0.5 * (1 + np.cos(2 * g * np.sqrt(n + 1) * tlist)) - - assert np.max(np.abs(nc - nc_ex)) < 0.005 - assert np.max(np.abs(na - na_ex)) < 0.005 - - def testCase2(self): - "mesolve: cavity-qubit without interaction, decay" - - use_rwa = True - N = 4 # number of cavity fock states - wc = 2 * np.pi * 1.0 # cavity frequency - wa = 2 * np.pi * 1.0 # atom frequency - g = 2 * np.pi * 0.0 # coupling strength - kappa = 0.005 # cavity dissipation rate - gamma = 0.01 # atom dissipation rate - pump = 0.0 # atom pump rate - - # start with an excited atom and maximum number of photons - n = N - 2 - psi0 = tensor(basis(N, n), basis(2, 1)) - tlist = np.linspace(0, 1000, 2000) - - nc, na = self.jc_integrate( - N, wc, wa, g, kappa, gamma, pump, psi0, use_rwa, tlist) - - nc_ex = (n + 0.5 * (1 - np.cos(2 * g * np.sqrt(n + 1) * tlist))) * \ - np.exp(-kappa * tlist) - na_ex = 0.5 * (1 + np.cos(2 * g * np.sqrt(n + 1) * tlist)) * \ - np.exp(-gamma * tlist) - - assert np.max(np.abs(nc - nc_ex)) < 0.005 - assert np.max(np.abs(na - na_ex)) < 0.005 - - def testCase3(self): - "mesolve: cavity-qubit with interaction, decay" - - use_rwa = True - N = 4 # number of cavity fock states - wc = 2 * np.pi * 1.0 # cavity frequency - wa = 2 * np.pi * 1.0 # atom frequency - g = 2 * np.pi * 0.1 # coupling strength - kappa = 0.05 # cavity dissipation rate - gamma = 0.001 # atom dissipation rate - pump = 0.25 # atom pump rate - - # start with an excited atom and maximum number of photons - n = N - 2 - psi0 = tensor(basis(N, n), basis(2, 1)) - tlist = np.linspace(0, 200, 500) - - nc, na = self.jc_integrate( - N, wc, wa, g, kappa, gamma, pump, psi0, use_rwa, tlist) - - # we don't have any analytics for this parameters, so - # compare with the steady state - nc_ss, na_ss = self.jc_steadystate( - N, wc, wa, g, kappa, gamma, pump, psi0, use_rwa, tlist) - - nc_ss = nc_ss * np.ones(np.shape(nc)) - na_ss = na_ss * np.ones(np.shape(na)) - - assert_(abs(nc[-1] - nc_ss[-1]) < 0.005, True) - assert_(abs(na[-1] - na_ss[-1]) < 0.005, True) - -# percent error for failure -me_error = 1e-8 - - -class TestMESolverConstDecay: - """ - A test class for the time-dependent ode check function. - """ - - def testMEDecay(self): - "mesolve: simple constant decay" - - 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 - c_op_list = [np.sqrt(kappa) * a] - tlist = np.linspace(0, 10, 100) - medata = mesolve(H, psi0, tlist, c_op_list, [a.dag() * a]) - expt = medata.expect[0] - actual_answer = 9.0 * np.exp(-kappa * tlist) - avg_diff = np.mean(abs(actual_answer - expt) / actual_answer) - assert_(avg_diff < me_error) - - def testMEDecaySingleCollapse(self): - "mesolve: simple constant decay" - - 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 - c_op = np.sqrt(kappa) * a - tlist = np.linspace(0, 10, 100) - medata = mesolve(H, psi0, tlist, [c_op], [a.dag() * a]) - expt = medata.expect[0] - actual_answer = 9.0 * np.exp(-kappa * tlist) - avg_diff = np.mean(abs(actual_answer - expt) / actual_answer) - assert_(avg_diff < me_error) - - - def testMEDecayAsFuncList(self): - "mesolve: constant decay as function list" - - 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 - - def sqrt_kappa(t, args): - return np.sqrt(kappa) - - c_op_list = [[a, sqrt_kappa]] - tlist = np.linspace(0, 10, 100) - medata = mesolve(H, psi0, tlist, c_op_list, [a.dag() * a]) - expt = medata.expect[0] - actual_answer = 9.0 * np.exp(-kappa * tlist) - avg_diff = np.mean(abs(actual_answer - expt) / actual_answer) - assert_(avg_diff < me_error) - - def testMEDecayAsStrList(self): - "mesolve: constant decay as string list" - - 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 - c_op_list = [[a, 'sqrt(k)']] - args = {'k': kappa} - tlist = np.linspace(0, 10, 100) - medata = mesolve(H, psi0, tlist, c_op_list, [a.dag() * a], args=args) - expt = medata.expect[0] - actual_answer = 9.0 * np.exp(-kappa * tlist) - avg_diff = np.mean(abs(actual_answer - expt) / actual_answer) - assert_(avg_diff < me_error) - - -# average error for failure -me_error = 1e-6 - - -class TestMESolveTDDecay: - """ - A test class for the time-dependent odes. Comparing to analytic answer - - N(t)=9 * exp[ -kappa*( 1-exp(-t) ) ] - - """ - - def testMETDDecayAsFuncList(self): - "mesolve: time-dependence as function list" - - 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 - - def sqrt_kappa(t, args): - return np.sqrt(kappa * np.exp(-t)) - c_op_list = [[a, sqrt_kappa]] - tlist = np.linspace(0, 10, 100) - medata = mesolve(H, psi0, tlist, c_op_list, [a.dag() * a]) - expt = medata.expect[0] - actual_answer = 9.0 * np.exp(-kappa * (1.0 - np.exp(-tlist))) - avg_diff = np.mean(abs(actual_answer - expt) / actual_answer) - assert_(avg_diff < me_error) - - def testMETDDecayAsPartFuncList(self): - "mesolve: time-dependence as partial function list" - - me_error = 1e-5 - N = 10 - a = destroy(N) - H = num(N) - psi0 = basis(N, 9) - tlist = np.linspace(0, 10, 100) - c_ops = [[[a, partial(lambda t, k: - np.sqrt(k * np.exp(-t)), k=kappa)]] - for kappa in [0.05, 0.1, 0.2]] - - for idx, kappa in enumerate([0.05, 0.1, 0.2]): - medata = mesolve(H, psi0, tlist, c_ops[idx], [H]) - ref = 9.0 * np.exp(-kappa * (1.0 - np.exp(-tlist))) - avg_diff = np.mean(abs(ref - medata.expect[0]) / ref) - assert_(avg_diff < me_error) - - def testMETDDecayAsStrList(self): - "mesolve: time-dependence as string list" - - 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 - c_op_list = [[a, 'sqrt(k*exp(-t))']] - args = {'k': kappa} - tlist = np.linspace(0, 10, 100) - medata = mesolve(H, psi0, tlist, c_op_list, [a.dag() * a], args=args) - expt = medata.expect[0] - actual_answer = 9.0 * np.exp(-kappa * (1.0 - np.exp(-tlist))) - avg_diff = np.mean(abs(actual_answer - expt) / actual_answer) - assert_(avg_diff < me_error) - - def testMETDDecayAsArray(self): - "mesolve: time-dependence as array" - - N = 10 - a = destroy(N) - H = a.dag() * a - psi0 = basis(N, 9) - kappa = 0.2 - tlist = np.linspace(0, 10, 1000) - c_op_list = [[a, np.sqrt(kappa * np.exp(-tlist))]] - medata = mesolve(H, psi0, tlist, c_op_list, [a.dag() * a]) - expt = medata.expect[0] - actual_answer = 9.0 * np.exp(-kappa * (1.0 - np.exp(-tlist))) - avg_diff = np.mean(abs(actual_answer - expt) / actual_answer) - assert_(avg_diff < 100 * me_error) - - def testMETDDecayAsFunc(self): - "mesolve: time-dependent Liouvillian as single function" - - N = 10 # number of basis states to consider - a = destroy(N) - H = a.dag() * a - rho0 = ket2dm(basis(N, 9)) # initial state - kappa = 0.2 # coupling to oscillator - - def Liouvillian_func(t, args): - c = np.sqrt(kappa * np.exp(-t))*a - return liouvillian(H, [c]) - - tlist = np.linspace(0, 10, 100) - args = {'kappa': kappa} - out1 = mesolve(Liouvillian_func, rho0, tlist, [], [], args=args) - expt = expect(a.dag()*a, out1.states) - actual_answer = 9.0 * np.exp(-kappa * (1.0 - np.exp(-tlist))) - avg_diff = np.mean(np.abs(actual_answer - expt) / actual_answer) - assert avg_diff < me_error - - -# average error for failure -# me_error = 1e-6 - -class TestMESolveSuperInit: - """ - A test class comparing mesolve run with an identity super-operator and - a density matrix as initial conditions, respectively. - """ - - def fidelitycheck(self, out1, out2, rho0vec): - fid = np.zeros(len(out1.states)) - for i, E in enumerate(out2.states): - rhot = vector_to_operator(E*rho0vec) - fid[i] = fidelity(out1.states[i], rhot) - return fid - - def jc_integrate(self, N, wc, wa, g, kappa, gamma, - pump, psi0, use_rwa, tlist): - # Hamiltonian - a = tensor(destroy(N), identity(2)) - sm = tensor(identity(N), destroy(2)) - - # Identity super-operator - E0 = sprepost(tensor(qeye(N), qeye(2)), tensor(qeye(N), qeye(2))) - - if use_rwa: - # use the rotating wave approxiation - H = wc * a.dag() * a + wa * sm.dag() * sm + g * ( - a.dag() * sm + a * sm.dag()) - else: - H = wc * a.dag() * a + wa * sm.dag() * sm + g * ( - a.dag() + a) * (sm + sm.dag()) - - # collapse operators - c_op_list = [] - - n_th_a = 0.0 # zero temperature - - rate = kappa * (1 + n_th_a) - c_op_list.append(np.sqrt(rate) * a) - - rate = kappa * n_th_a - if rate > 0.0: - c_op_list.append(np.sqrt(rate) * a.dag()) - - rate = gamma - if rate > 0.0: - c_op_list.append(np.sqrt(rate) * sm) - - rate = pump - if rate > 0.0: - c_op_list.append(np.sqrt(rate) * sm.dag()) - - # evolve and calculate expectation values - output1 = mesolve(H, psi0, tlist, c_op_list, []) - output2 = mesolve(H, E0, tlist, c_op_list, []) - return output1, output2 - - def testSuperJC(self): - "mesolve: super vs. density matrix as initial condition" - me_error = 1e-6 - - use_rwa = True - N = 4 # number of cavity fock states - wc = 2 * np.pi * 1.0 # cavity frequency - wa = 2 * np.pi * 1.0 # atom frequency - g = 2 * np.pi * 0.1 # coupling strength - kappa = 0.05 # cavity dissipation rate - gamma = 0.001 # atom dissipation rate - pump = 0.25 # atom pump rate - - # start with an excited atom and maximum number of photons - n = N - 2 - psi0 = basis([N, 2], [n, 1]) - rho0vec = operator_to_vector(psi0.proj()) - tlist = np.linspace(0, 100, 50) - - out1, out2 = self.jc_integrate( - N, wc, wa, g, kappa, gamma, pump, psi0, use_rwa, tlist) - - fid = self.fidelitycheck(out1, out2, rho0vec) - assert np.max(np.abs(1.0 - fid)) < me_error - - def testMETDDecayAsFuncList(self): - "mesolve: time-dependence as function list with super as init cond" - me_error = 1e-6 - - N = 10 # number of basis states to consider - a = destroy(N) - H = a.dag() * a - psi0 = basis(N, 9) # initial state - rho0vec = operator_to_vector(psi0*psi0.dag()) - E0 = sprepost(qeye(N), qeye(N)) - kappa = 0.2 # coupling to oscillator - - def sqrt_kappa(t, args): - return np.sqrt(kappa * np.exp(-t)) - c_op_list = [[a, sqrt_kappa]] - tlist = np.linspace(0, 10, 100) - out1 = mesolve(H, psi0, tlist, c_op_list, []) - out2 = mesolve(H, E0, tlist, c_op_list, []) - - fid = self.fidelitycheck(out1, out2, rho0vec) - assert_(max(abs(1.0-fid)) < me_error, True) - - def testMETDDecayAsPartFuncList(self): - "mesolve: time-dep. as partial function list with super as init cond" - me_error = 1e-5 - - N = 10 - a = destroy(N) - H = num(N) - psi0 = basis(N, 9) - rho0vec = operator_to_vector(psi0*psi0.dag()) - E0 = sprepost(qeye(N), qeye(N)) - tlist = np.linspace(0, 10, 100) - c_ops = [[[a, partial(lambda t, k: - np.sqrt(k * np.exp(-t)), k=kappa)]] - for kappa in [0.05, 0.1, 0.2]] - - for idx, kappa in enumerate([0.05, 0.1, 0.2]): - out1 = mesolve(H, psi0, tlist, c_ops[idx], []) - out2 = mesolve(H, E0, tlist, c_ops[idx], []) - fid = self.fidelitycheck(out1, out2, rho0vec) - assert_(max(abs(1.0-fid)) < me_error, True) - - def testMETDDecayAsStrList(self): - "mesolve: time-dependence as string list with super as init cond" - me_error = 1e-6 - - N = 10 # number of basis states to consider - a = destroy(N) - H = a.dag() * a - psi0 = basis(N, 9) # initial state - rho0vec = operator_to_vector(psi0*psi0.dag()) - E0 = sprepost(qeye(N), qeye(N)) - kappa = 0.2 # coupling to oscillator - c_op_list = [[a, 'sqrt(k*exp(-t))']] - args = {'k': kappa} - tlist = np.linspace(0, 10, 100) - out1 = mesolve(H, psi0, tlist, c_op_list, [], args=args) - out2 = mesolve(H, E0, tlist, c_op_list, [], args=args) - fid = self.fidelitycheck(out1, out2, rho0vec) - assert_(max(abs(1.0-fid)) < me_error, True) - - def testMETDDecayAsArray(self): - "mesolve: time-dependence as array with super as init cond" - me_error = 1e-5 - - N = 10 - a = destroy(N) - H = a.dag() * a - psi0 = basis(N, 9) - rho0vec = operator_to_vector(psi0*psi0.dag()) - E0 = sprepost(qeye(N), qeye(N)) - kappa = 0.2 - tlist = np.linspace(0, 10, 1000) - c_op_list = [[a, np.sqrt(kappa * np.exp(-tlist))]] - out1 = mesolve(H, psi0, tlist, c_op_list, []) - out2 = mesolve(H, E0, tlist, c_op_list, []) - fid = self.fidelitycheck(out1, out2, rho0vec) - assert_(max(abs(1.0-fid)) < me_error, True) - - def testMETDDecayAsFunc(self): - "mesolve: time-dependence as function with super as init cond" - - N = 10 # number of basis states to consider - a = destroy(N) - H = a.dag() * a - rho0 = ket2dm(basis(N, 9)) # initial state - rho0vec = operator_to_vector(rho0) - E0 = sprepost(qeye(N), qeye(N)) - kappa = 0.2 # coupling to oscillator - - def Liouvillian_func(t, args): - c = np.sqrt(kappa * np.exp(-t))*a - data = liouvillian(H, [c]) - return data - - tlist = np.linspace(0, 10, 100) - args = {'kappa': kappa} - out1 = mesolve(Liouvillian_func, rho0, tlist, [], [], args=args) - out2 = mesolve(Liouvillian_func, E0, tlist, [], [], args=args) - - fid = self.fidelitycheck(out1, out2, rho0vec) - assert_(max(abs(1.0-fid)) < me_error, True) - - def test_me_interp1(self): - "mesolve: interp time-dependent collapse operator #1" - - N = 10 # number of basis states to consider - kappa = 0.2 # coupling to oscillator - tlist = np.linspace(0, 10, 100) - a = destroy(N) - H = a.dag() * a - psi0 = basis(N, 9) # initial state - S = core.coefficient(np.sqrt(kappa*np.exp(-tlist)), tlist=tlist) - c_op_list = [[a, S]] - medata = mesolve(H, psi0, tlist, c_op_list, [a.dag() * a]) - expt = medata.expect[0] - actual_answer = 9.0 * np.exp(-kappa * (1.0 - np.exp(-tlist))) - avg_diff = np.mean(abs(actual_answer - expt) / actual_answer) - assert_(avg_diff < 1e-5) - - def test_me_interp2(self): - "mesolve: interp time-dependent collapse operator #2" - - N = 10 # number of basis states to consider - kappa = 0.2 # coupling to oscillator - tlist = np.linspace(0, 10, 100) - C = core.coefficient(np.ones_like(tlist), tlist=tlist) - S = core.coefficient(np.sqrt(kappa*np.exp(-tlist)), tlist=tlist) - a = destroy(N) - H = [[a.dag() * a, C]] - psi0 = basis(N, 9) # initial state - c_op_list = [[a, S]] - medata = mesolve(H, psi0, tlist, c_op_list, [a.dag() * a]) - expt = medata.expect[0] - actual_answer = 9.0 * np.exp(-kappa * (1.0 - np.exp(-tlist))) - avg_diff = np.mean(abs(actual_answer - expt) / actual_answer) - assert_(avg_diff < 1e-5) - - def test_me_interp3(self): - "mesolve: interp time-dependent collapse operator #3" - - N = 10 # number of basis states to consider - kappa = 0.2 # coupling to oscillator - tlist = np.linspace(0, 10, 100) - C = core.coefficient(np.ones_like(tlist), tlist=tlist) - S = core.coefficient(np.sqrt(kappa*np.exp(-tlist)), tlist=tlist) - a = destroy(N) - H = [a.dag() * a, [a.dag() * a, C]] - psi0 = basis(N, 9) # initial state - c_op_list = [[a, S]] - medata = mesolve(H, psi0, tlist, c_op_list, [a.dag() * a]) - expt = medata.expect[0] - actual_answer = 9.0 * np.exp(-kappa * (1.0 - np.exp(-tlist))) - avg_diff = np.mean(abs(actual_answer - expt) / actual_answer) - assert_(avg_diff < 1e-5) - - def test_me_interp4(self): - "mesolve: interp time-dependent collapse operator #4" - - N = 10 # number of basis states to consider - kappa = 0.2 # coupling to oscillator - tlist = np.linspace(0, 10, 100) - C = core.coefficient(np.ones_like(tlist), tlist=tlist) - S = core.coefficient(np.sqrt(kappa*np.exp(-tlist)), tlist=tlist) - a = destroy(N) - H = [a.dag() * a, [a.dag() * a, C]] - psi0 = basis(N, 9) # initial state - c_op_list = [[a, S],[a, S]] - medata = mesolve(H, psi0, tlist, c_op_list, [a.dag() * a]) - expt = medata.expect[0] - actual_answer = 9.0 * np.exp(-2*kappa * (1.0 - np.exp(-tlist))) - avg_diff = np.mean(abs(actual_answer - expt) / actual_answer) - assert_(avg_diff < 1e-5) - - -class TestMESolverMisc: - """ - A test class for the misc mesolve features. - """ - def testMEFinalState(self): - "mesolve: final_state has correct dims" - - N = 5 - psi0 = tensor(basis(N+1,0), basis(N+1,0), basis(N+1,N)) - a = tensor(destroy(N+1), qeye(N+1), qeye(N+1)) - b = tensor(qeye(N+1), destroy(N+1), qeye(N+1)) - c = tensor(qeye(N+1), qeye(N+1), destroy(N+1)) - H = a*b*c.dag() * c.dag() - a.dag()*b.dag()*c * c - - times = np.linspace(0.0, 2.0, 100) - opts = SolverOptions(store_states=False, store_final_state=True) - rho0 = ket2dm(psi0) - result = mesolve(H, rho0, times, [], [a.dag()*a, b.dag()*b, c.dag()*c], - options=opts) - assert_(rho0.dims == result.final_state.dims) - - - def testSEFinalState(self): - "sesolve: final_state has correct dims" - - N = 5 - psi0 = tensor(basis(N+1,0), basis(N+1,0), basis(N+1,N)) - a = tensor(destroy(N+1), qeye(N+1), qeye(N+1)) - b = tensor(qeye(N+1), destroy(N+1), qeye(N+1)) - c = tensor(qeye(N+1), qeye(N+1), destroy(N+1)) - H = a*b*c.dag() * c.dag() - a.dag()*b.dag()*c * c - - times = np.linspace(0.0, 2.0, 100) - opts = SolverOptions(store_states=False, store_final_state=True) - result = mesolve(H, psi0, times, [], [a.dag()*a, b.dag()*b, c.dag()*c], - options=opts) - assert_(psi0.dims == result.final_state.dims) - - def test_num_collapse_set(self): - H = sigmaz() - psi = (basis(2, 0) + basis(2, 1)).unit() - ts = [0, 1] - for c_ops in ( - sigmax(), - [sigmax()], - [sigmay(), sigmax()], - ): - res = mesolve(H, psi, ts, c_ops=c_ops) - if not isinstance(c_ops, list): - c_ops = [c_ops] - assert res.num_collapse == len(c_ops) - - # All Hamiltonians should have dimensions [3, 2], so "bad" states can have - # [2, 3] instead - the aim is to test mismatch in most cases. - @pytest.mark.parametrize(['state'], [ - pytest.param(basis([2, 3], [0, 0]), id='ket_bad_tensor'), - pytest.param(basis([2, 3], [0, 0]).proj(), id='dm_bad_tensor'), - pytest.param(basis([2, 3], [0, 0]).dag(), id='bra_bad_tensor'), - pytest.param(to_super(basis([2, 3], [0, 0]).proj()), - id='super_bad_tensor'), - pytest.param(basis([3, 2], [0, 0]).dag(), id='bra_good_tensor'), - pytest.param(operator_to_vector(basis([3, 2], [0, 0]).proj()), - id='operket_good_tensor'), - pytest.param(operator_to_vector(basis([3, 2], [0, 0]).proj()).dag(), - id='operbra_good_tensor'), - pytest.param(tensor(basis(2, 0), qeye(3)), id='nonsquare_operator'), - ]) - @pytest.mark.parametrize(['operator'], [ - pytest.param(qeye([3, 2]), id='constant_hamiltonian'), - pytest.param(liouvillian(qeye([3, 2]), []), id='constant_liouvillian'), - pytest.param([[qeye([3, 2]), lambda t, args: 1]], id='py_scalar'), - pytest.param(lambda t, args: qeye([3, 2]), id='py_hamiltonian'), - pytest.param(lambda t, args: liouvillian(qeye([3, 2]), []), - id='py_liouvillian'), - ]) - def test_incorrect_state_caught(self, state, operator): - """ - Test that mesolve will correctly catch an input state that is not a - correctly shaped state, relative to the Hamiltonian or Liouvillian. - - Regression test for gh-1456. - """ - times = [0, 1e-5] - c_op = qeye([3, 2]) - with pytest.raises(ValueError): - mesolve(operator, state, times, c_ops=[c_op]) - - -class TestMESolveStepFuncCoeff: - """ - A Test class for using time-dependent array coefficients - as step functions instead of doing interpolation - """ - def python_coeff(self, t, args): - if t < np.pi/2: - return 1. - else: - return 0. - - def test_py_coeff(self): - """ - Test for Python function as coefficient as step function coeff - """ - rho0 = rand_ket(2) - tlist = np.array([0, np.pi/2]) - qu = QobjEvo([[sigmax(), self.python_coeff]], tlist=tlist, order=0) - result = mesolve(qu, rho0=rho0, tlist=tlist) - assert_allclose( - fidelity(result.states[-1], sigmax()*rho0), 1, rtol=1.e-7) - - def test_array_cte_coeff(self): - """ - Test for Array coefficient with uniform tlist as step function coeff - """ - rho0 = rand_ket(2) - tlist = np.array([0., np.pi/2, np.pi], dtype=float) - npcoeff = np.array([0.25, 0.75, 0.75]) - qu = QobjEvo([[sigmax(), npcoeff]], tlist=tlist, order=0) - result = mesolve(qu, rho0=rho0, tlist=tlist) - assert_allclose( - fidelity(result.states[-1], sigmax()*rho0), 1, rtol=1.e-7) - - def test_array_t_coeff(self): - """ - Test for Array with non-uniform tlist as step function coeff - """ - rho0 = rand_ket(2) - tlist = np.array([0., np.pi/2, np.pi*3/2], dtype=float) - npcoeff = np.array([0.5, 0.25, 0.25]) - qu = QobjEvo([[sigmax(), npcoeff]], tlist=tlist, order=0) - result = mesolve(qu, rho0=rho0, tlist=tlist) - assert_allclose( - fidelity(result.states[-1], sigmax()*rho0), 1, rtol=1.e-7) - - def test_array_str_coeff(self): - """ - Test for Array and string as step function coeff. - qobjevo_codegen is used and uniform tlist - """ - rho0 = rand_ket(2) - tlist = np.array([0., np.pi/2, np.pi], dtype=float) - npcoeff1 = np.array([0.25, 0.75, 0.75], dtype=complex) - npcoeff2 = np.array([0.5, 1.5, 1.5], dtype=float) - strcoeff = "1." - qu = QobjEvo( - [[sigmax(), npcoeff1], [sigmax(), strcoeff], [sigmax(), npcoeff2]], - tlist=tlist, order=0) - result = mesolve(qu, rho0=rho0, tlist=tlist) - assert_allclose( - fidelity(result.states[-1], sigmax()*rho0), 1, rtol=1.e-7) - - def test_array_str_py_coeff(self): - """ - Test for Array, string and Python function as step function coeff. - qobjevo_codegen is used and non non-uniform tlist - """ - rho0 = rand_ket(2) - tlist = np.array([0., np.pi/4, np.pi/2, np.pi], dtype=float) - npcoeff1 = np.array([0.4, 1.6, 1.0, 1.0], dtype=complex) - npcoeff2 = np.array([0.4, 1.6, 1.0, 1.0], dtype=float) - strcoeff = "1." - qu = QobjEvo( - [[sigmax(), npcoeff1], [sigmax(), npcoeff2], - [sigmax(), self.python_coeff], [sigmax(), strcoeff]], - tlist=tlist, order=0) - result = mesolve(qu, rho0=rho0, tlist=tlist) - assert_allclose( - fidelity(result.states[-1], sigmax()*rho0), 1, rtol=3.e-7) - -def test_non_hermitian_dm(): - """Test that mesolve works correctly for density matrices that are - not Hermitian. - See Issue #1460 - """ - N = 2 - a = destroy(N) - x = (a + a.dag())/np.sqrt(2) - H = a.dag() * a - - # Create non-Hermitian initial state. - rho0 = x*fock_dm(N, 0) - - tlist = np.linspace(0, 0.1, 2) - - options = SolverOptions() - options['store_final_state'] = True - options['store_states'] = True - - result = mesolve(H, rho0, tlist, e_ops=[x], options=options) - - msg = ('Mesolve is not working properly with a non Hermitian density' + - ' matrix as input. Check computation of ' - ) - - imag_part = np.abs(np.imag(result.expect[0][-1])) - # Since we used an initial state that is not Hermitian, the expectation of - # x must be imaginary for t>0. - assert_(imag_part > 0, - msg + "expectation values. They should be imaginary") - - # Check that the output state is not hermitian since the input was not - # Hermitian either. - assert_(not result.final_state.isherm, - msg + " final density matrix. It should not be hermitian") - assert_(not result.states[-1].isherm, - msg + " states. They should not be hermitian.") - - # Check that when suing a callable we get imaginary expectation values. - def callable_x(t, rho): - "Dummy callable_x expectation operator." - return expect(rho, x) - result = mesolve(H, rho0, tlist, e_ops=callable_x) - - imag_part = np.abs(np.imag(result.expect[-1])) - assert_(imag_part > 0, - msg + "expectation values when using callable operator." + - "They should be imaginary.") - - -def test_tlist_h_with_constant_c_ops(): - """ - Test that it's possible to mix a time-dependent Hamiltonian given as a - QobjEvo with interpolated coefficients with time-independent collapse - operators, if the solver times are not equal to the interpolation times of - the Hamiltonian. - - See gh-1560. - """ - state = basis(2, 0) - all_times = np.linspace(0, 1, 11) - few_times = np.linspace(0, 1, 3) - dependence = np.cos(2*np.pi * all_times) - hamiltonian = QobjEvo([[sigmax(), dependence]], tlist=all_times) - collapse = qeye(2) - result = mesolve(hamiltonian, state, few_times, c_ops=[collapse]) - assert result.num_collapse == 1 - assert len(result.states) == len(few_times) - - -def test_tlist_h_with_other_tlist_c_ops_raises(): - state = basis(2, 0) - all_times = np.linspace(0, 1, 11) - few_times = np.linspace(0, 1, 3) - dependence = np.cos(2*np.pi * all_times) - hamiltonian = QobjEvo([[sigmax(), dependence]], tlist=all_times) - collapse = [qeye(2), np.cos(2*np.pi * few_times)] - with pytest.raises(ValueError) as exc: - mesolve(hamiltonian, state, all_times, c_ops=[collapse]) - - -def test_mesolve_bad_e_ops(): - H = sigmaz() - c_ops = [sigmax()] - psi0 = basis(2, 0) - tlist = np.linspace(0, 20, 200) - with pytest.raises(TypeError) as exc: - mesolve(H, psi0, tlist=tlist, c_ops=c_ops, e_ops=[qeye(3)]) diff --git a/qutip/tests/solve/test_sesolve.py b/qutip/tests/solve/test_sesolve.py deleted file mode 100644 index e61f4dcb58..0000000000 --- a/qutip/tests/solve/test_sesolve.py +++ /dev/null @@ -1,331 +0,0 @@ -import numpy as np -from numpy.testing import assert_, run_module_suite - -# disable the progress bar -import os - -from qutip import ( - sigmax, sigmay, sigmaz, qeye, basis, expect, num, destroy, create, - sesolve, coefficient -) -from qutip.solve import SolverOptions -import pytest - -os.environ['QUTIP_GRAPHICS'] = "NO" - - -class TestSESolve: - """ - A test class for the QuTiP Schrodinger Eq. solver - """ - - def check_evolution(self, H, delta, psi0, tlist, analytic_func, - U0=None, td_args={}, tol=5e-3): - """ - Compare integrated evolution with analytical result - If U0 is not None then operator evo is checked - Otherwise state evo - """ - - if U0 is None: - output = sesolve(H, psi0, tlist, [sigmax(), sigmay(), sigmaz()], - args=td_args) - sx, sy, sz = output.expect[0], output.expect[1], output.expect[2] - else: - output = sesolve(H, U0, tlist, args=td_args) - sx = [expect(sigmax(), U*psi0) for U in output.states] - sy = [expect(sigmay(), U*psi0) for U in output.states] - sz = [expect(sigmaz(), U*psi0) for U in output.states] - - sx_analytic = np.zeros(np.shape(tlist)) - sy_analytic = np.array([-np.sin(delta*analytic_func(t, td_args)) - for t in tlist]) - sz_analytic = np.array([np.cos(delta*analytic_func(t, td_args)) - for t in tlist]) - - np.testing.assert_allclose(sx, sx_analytic, atol=tol) - np.testing.assert_allclose(sy, sy_analytic, atol=tol) - np.testing.assert_allclose(sz, sz_analytic, atol=tol) - - def test_01_1_state_with_const_H(self): - "sesolve: state with const H" - delta = 1.0 * 2*np.pi # atom frequency - psi0 = basis(2, 0) # initial state - H1 = 0.5*delta*sigmax() # Hamiltonian operator - tlist = np.linspace(0, 20, 200) - - analytic_func = lambda t, args: t - - self.check_evolution(H1, delta, psi0, tlist, analytic_func) - - def test_01_1_unitary_with_const_H(self): - "sesolve: unitary operator with const H" - delta = 1.0 * 2*np.pi # atom frequency - psi0 = basis(2, 0) # initial state - U0 = qeye(2) # initital operator - H1 = 0.5*delta*sigmax() # Hamiltonian operator - tlist = np.linspace(0, 20, 200) - - analytic_func = lambda t, args: t - - self.check_evolution(H1, delta, psi0, tlist, analytic_func, U0) - - def test_02_1_state_with_func_H(self): - "sesolve: state with td func H" - delta = 1.0 * 2*np.pi # atom frequency - psi0 = basis(2, 0) # initial state - H1 = 0.5*delta*sigmax() # Hamiltonian operator - tlist = np.linspace(0, 20, 200) - - alpha = 0.1 - td_args = {'alpha':alpha} - h1_func = lambda t, args: H1*np.exp(-args['alpha']*t) - analytic_func = lambda t, args: ((1 - np.exp(-args['alpha']*t)) - /args['alpha']) - - self.check_evolution(h1_func, delta, psi0, tlist, analytic_func, - td_args=td_args) - - def test_02_2_unitary_with_func_H(self): - "sesolve: unitary operator with td func H" - delta = 1.0 * 2*np.pi # atom frequency - psi0 = basis(2, 0) # initial state - U0 = qeye(2) # initital operator - H1 = 0.5*delta*sigmax() # Hamiltonian operator - tlist = np.linspace(0, 20, 200) - - alpha = 0.1 - td_args = {'alpha':alpha} - h1_func = lambda t, args: H1*np.exp(-args['alpha']*t) - analytic_func = lambda t, args: ((1 - np.exp(-args['alpha']*t)) - /args['alpha']) - - self.check_evolution(h1_func, delta, psi0, tlist, analytic_func, U0, - td_args=td_args) - - def test_03_1_state_with_list_func_H(self): - "sesolve: state with td list func H" - delta = 1.0 * 2*np.pi # atom frequency - psi0 = basis(2, 0) # initial state - H1 = 0.5*delta*sigmax() # Hamiltonian operator - tlist = np.linspace(0, 20, 200) - - alpha = 0.1 - td_args = {'alpha':alpha} - h1_coeff = lambda t, args: np.exp(-args['alpha']*t) - H = [[H1, h1_coeff]] - analytic_func = lambda t, args: ((1 - np.exp(-args['alpha']*t)) - /args['alpha']) - - self.check_evolution(H, delta, psi0, tlist, analytic_func, - td_args=td_args) - - def test_03_2_unitary_with_list_func_H(self): - "sesolve: unitary operator with td list func H" - delta = 1.0 * 2*np.pi # atom frequency - psi0 = basis(2, 0) # initial state - U0 = qeye(2) # initital operator - H1 = 0.5*delta*sigmax() # Hamiltonian operator - tlist = np.linspace(0, 20, 200) - - alpha = 0.1 - td_args = {'alpha':alpha} - h1_coeff = lambda t, args: np.exp(-args['alpha']*t) - H = [[H1, h1_coeff]] - analytic_func = lambda t, args: ((1 - np.exp(-args['alpha']*t)) - /args['alpha']) - - self.check_evolution(H, delta, psi0, tlist, analytic_func, U0, - td_args=td_args) - - def test_04_1_state_with_list_str_H(self): - "sesolve: state with td list str H" - delta = 1.0 * 2*np.pi # atom frequency - psi0 = basis(2, 0) # initial state - H1 = 0.5*delta*sigmax() # Hamiltonian operator - tlist = np.linspace(0, 20, 200) - - alpha = 0.1 - td_args = {'alpha':alpha} - H = [[H1, 'exp(-alpha*t)']] - analytic_func = lambda t, args: ((1 - np.exp(-args['alpha']*t)) - /args['alpha']) - - self.check_evolution(H, delta, psi0, tlist, analytic_func, - td_args=td_args) - - def test_04_2_unitary_with_list_func_H(self): - "sesolve: unitary operator with td list str H" - delta = 1.0 * 2*np.pi # atom frequency - psi0 = basis(2, 0) # initial state - U0 = qeye(2) # initital operator - H1 = 0.5*delta*sigmax() # Hamiltonian operator - tlist = np.linspace(0, 20, 200) - - alpha = 0.1 - td_args = {'alpha':alpha} - H = [[H1, 'exp(-alpha*t)']] - analytic_func = lambda t, args: ((1 - np.exp(-args['alpha']*t)) - /args['alpha']) - - self.check_evolution(H, delta, psi0, tlist, analytic_func, U0, - td_args=td_args) - - - def test_05_1_state_with_interp_H(self): - "sesolve: state with td interp H" - delta = 1.0 * 2*np.pi # atom frequency - psi0 = basis(2, 0) # initial state - H1 = 0.5*delta*sigmax() # Hamiltonian operator - tlist = np.linspace(0, 20, 200) - - alpha = 0.1 - td_args = {'alpha':alpha} - tcub = np.linspace(0, 20, 50) - S = coefficient(np.exp(-alpha*tcub), tlist=tcub) - H = [[H1, S]] - analytic_func = lambda t, args: ((1 - np.exp(-args['alpha']*t)) - /args['alpha']) - - self.check_evolution(H, delta, psi0, tlist, analytic_func, - td_args=td_args) - - def test_05_2_unitary_with_interp_H(self): - "sesolve: unitary operator with td interp H" - delta = 1.0 * 2*np.pi # atom frequency - psi0 = basis(2, 0) # initial state - U0 = qeye(2) # initital operator - H1 = 0.5*delta*sigmax() # Hamiltonian operator - tlist = np.linspace(0, 20, 200) - - alpha = 0.1 - td_args = {'alpha':alpha} - tcub = np.linspace(0, 20, 50) - S = coefficient(np.exp(-alpha*tcub), tlist=tcub) - H = [[H1, S]] - analytic_func = lambda t, args: ((1 - np.exp(-args['alpha']*t)) - /args['alpha']) - - self.check_evolution(H, delta, psi0, tlist, analytic_func, U0, - td_args=td_args) - - def compare_evolution(self, H, psi0, tlist, - normalize=False, td_args={}, tol=5e-5): - """ - Compare integrated evolution of unitary operator with state evo - """ - U0 = qeye(2) - options = SolverOptions(store_states=True, normalize_output=normalize) - out_s = sesolve(H, psi0, tlist, [sigmax(), sigmay(), sigmaz()], - options=options,args=td_args) - xs, ys, zs = out_s.expect[0], out_s.expect[1], out_s.expect[2] - - out_u = sesolve(H, U0, tlist, options=options, args=td_args) - xu = [expect(sigmax(), U*psi0) for U in out_u.states] - yu = [expect(sigmay(), U*psi0) for U in out_u.states] - zu = [expect(sigmaz(), U*psi0) for U in out_u.states] - - if normalize: - msg_ext = ". (Normalized)" - else: - msg_ext = ". (Not normalized)" - assert_(max(abs(xs - xu)) < tol, - msg="expect X not matching" + msg_ext) - assert_(max(abs(ys - yu)) < tol, - msg="expect Y not matching" + msg_ext) - assert_(max(abs(zs - zu)) < tol, - msg="expect Z not matching" + msg_ext) - - def test_06_1_compare_state_and_unitary_const(self): - "sesolve: compare state and unitary operator evo - const H" - eps = 0.2 * 2*np.pi - delta = 1.0 * 2*np.pi # atom frequency - w0 = 0.5*eps - w1 = 0.5*delta - H0 = w0*sigmaz() - H1 = w1*sigmax() - H = H0 + H1 - - psi0 = basis(2, 0) # initial state - tlist = np.linspace(0, 20, 200) - - self.compare_evolution(H, psi0, tlist, - normalize=False, tol=5e-5) - self.compare_evolution(H, psi0, tlist, - normalize=True, tol=5e-5) - - def test_06_2_compare_state_and_unitary_func(self): - "sesolve: compare state and unitary operator evo - func td" - eps = 0.2 * 2*np.pi - delta = 1.0 * 2*np.pi # atom frequency - w0 = 0.5*eps - w1 = 0.5*delta - H0 = w0*sigmaz() - H1 = w1*sigmax() - a = 0.1 - alpha = 0.1 - td_args = {'a':a, 'alpha':alpha} - H_func = lambda t, args: a*t*H0 + H1*np.exp(-alpha*t) - H = H_func - - psi0 = basis(2, 0) # initial state - tlist = np.linspace(0, 20, 200) - - self.compare_evolution(H, psi0, tlist, - normalize=False, td_args=td_args, tol=5e-5) - self.compare_evolution(H, psi0, tlist, - normalize=True, td_args=td_args, tol=5e-5) - - def test_06_3_compare_state_and_unitary_list_func(self): - "sesolve: compare state and unitary operator evo - list func td" - eps = 0.2 * 2*np.pi - delta = 1.0 * 2*np.pi # atom frequency - w0 = 0.5*eps - w1 = 0.5*delta - H0 = w0*sigmaz() - H1 = w1*sigmax() - a = 0.1 - w_a = w0 - td_args = {'a':a, 'w_a':w_a} - h0_func = lambda t, args: a*t - h1_func = lambda t, args: np.cos(w_a*t) - H = [[H0, h0_func], [H1, h1_func]] - - psi0 = basis(2, 0) # initial state - tlist = np.linspace(0, 20, 200) - - self.compare_evolution(H, psi0, tlist, - normalize=False, td_args=td_args, tol=5e-5) - self.compare_evolution(H, psi0, tlist, - normalize=True, td_args=td_args, tol=5e-5) - - def test_06_4_compare_state_and_unitary_list_str(self): - "sesolve: compare state and unitary operator evo - list str td" - eps = 0.2 * 2*np.pi - delta = 1.0 * 2*np.pi # atom frequency - w0 = 0.5*eps - w1 = 0.5*delta - H0 = w0*sigmaz() - H1 = w1*sigmax() - w_a = w0 - - td_args = {'w_a':w_a} - H = [H0, [H1, 'cos(w_a*t)']] - - psi0 = basis(2, 0) # initial state - tlist = np.linspace(0, 20, 200) - - self.compare_evolution(H, psi0, tlist, - normalize=False, td_args=td_args, tol=5e-5) - self.compare_evolution(H, psi0, tlist, - normalize=True, td_args=td_args, tol=5e-5) - - - - -def test_sesolve_bad_e_ops(): - H = sigmaz() - psi0 = basis(2, 0) - tlist = np.linspace(0, 20, 200) - with pytest.raises(TypeError) as exc: - sesolve(H, psi0, tlist=tlist, e_ops=[qeye(3)]) diff --git a/qutip/tests/solve/test_stochastic_me.py b/qutip/tests/solve/test_stochastic_me.py index 3c9ce1e2a3..27ac7c821a 100644 --- a/qutip/tests/solve/test_stochastic_me.py +++ b/qutip/tests/solve/test_stochastic_me.py @@ -4,8 +4,9 @@ from qutip import ( smesolve, mesolve, photocurrent_mesolve, liouvillian, QobjEvo, spre, spost, - destroy, coherent, parallel_map, qeye, fock_dm, general_stochastic, num, + destroy, coherent, qeye, fock_dm, general_stochastic, num, ) +from qutip.solve.parallel import parallel_map from qutip.core import data as _data def f(t, args): return args["a"] * t diff --git a/qutip/tests/solve/test_stochastic_se.py b/qutip/tests/solve/test_stochastic_se.py index c00400fbcd..8a40f2d756 100644 --- a/qutip/tests/solve/test_stochastic_se.py +++ b/qutip/tests/solve/test_stochastic_se.py @@ -3,10 +3,10 @@ from numpy.testing import assert_ from qutip import ( - ssesolve, destroy, coherent, mesolve, fock, qeye, parallel_map, + ssesolve, destroy, coherent, mesolve, fock, qeye, photocurrent_sesolve, num, ) - +from qutip.solve.parallel import parallel_map def f(t, args): return args["a"] * t diff --git a/qutip/tests/solver/heom/test_bofin_solvers.py b/qutip/tests/solver/heom/test_bofin_solvers.py index 764334ee96..27db82ac31 100644 --- a/qutip/tests/solver/heom/test_bofin_solvers.py +++ b/qutip/tests/solver/heom/test_bofin_solvers.py @@ -930,7 +930,7 @@ def test_solving_with_step(self): ck_real, vk_real, ck_imag, vk_imag = dlm.bath_coefficients() bath = BosonicBath(dlm.Q, ck_real, vk_real, ck_imag, vk_imag) - options = {"nsteps": 15_000} + options = {"nsteps": 15_000, "store_ados": True} hsolver = HEOMSolver(dlm.H, bath, 14, options=options) tlist = np.linspace(0, 10, 21) diff --git a/qutip/tests/solve/test_correlation.py b/qutip/tests/solver/test_correlation.py similarity index 83% rename from qutip/tests/solve/test_correlation.py rename to qutip/tests/solver/test_correlation.py index 58d444526c..bf96b045da 100644 --- a/qutip/tests/solve/test_correlation.py +++ b/qutip/tests/solver/test_correlation.py @@ -15,7 +15,6 @@ @pytest.mark.parametrize(["solver", "start"], [ pytest.param("es", _equivalence_coherent, id="es"), pytest.param("es", None, id="es-steady state"), - pytest.param("mc", _equivalence_fock, id="mc", marks=pytest.mark.slow), ]) def test_correlation_solver_equivalence(solver, start): """ @@ -65,6 +64,7 @@ def _spectrum_fft(H, c_ops, a, b): pytest.param(_spectrum_fft, id="fft"), pytest.param(_spectrum_wrapper("es"), id="es"), pytest.param(_spectrum_wrapper("pi"), id="pi"), + pytest.param(_spectrum_wrapper("solve"), id="solve"), ]) def test_spectrum_solver_equivalence_to_es(spectrum): """Test equivalence of the spectrum solvers to the base "es" method.""" @@ -127,6 +127,7 @@ def _2ls_g2_0(H, c_ops): sp = qutip.sigmap() start = qutip.basis(2, 0) times = _2ls_times + H = qutip.QobjEvo(H, args=_2ls_args, tlist=times) correlation = qutip.correlation_3op_2t(H, start, times, times, [sp], sp.dag(), sp.dag()*sp, sp, args=_2ls_args) @@ -227,8 +228,6 @@ def test_hamiltonian_order_unimportant(): pytest.param('es', _equivalence_fock, id="es-ket"), pytest.param('es', _equivalence_coherent, id="es-dm"), pytest.param('es', None, id="es-steady"), - pytest.param('mc', _equivalence_fock, id="mc-ket", - marks=[pytest.mark.slow]), ]) @pytest.mark.parametrize("is_e_op_hermitian", [True, False], ids=["hermitian", "nonhermitian"]) @@ -269,3 +268,62 @@ def test_correlation_2op_1t_known_cases(solver, cmp = qutip.correlation_2op_1t(H, state, times, c_ops, a_op, b_op, solver=solver) np.testing.assert_allclose(base, cmp, atol=0.25 if solver == 'mc' else 2e-5) + + +def test_correlation_timedependant_op(): + num = qutip.num(2) + a = qutip.destroy(2) + sx = qutip.sigmax() + sz = qutip.sigmaz() + times = np.arange(4) + # switch between sx and sz at t=1.5 + A_op = qutip.QobjEvo([[sx, lambda t: t<=1.5], [sz, lambda t: t>1.5]]) + + cmp_sx = qutip.correlation_2op_1t(num, None, times, [a], sx, sx) + cmp_sz = qutip.correlation_2op_1t(num, None, times, [a], sz, sx) + cmp_switch = qutip.correlation_2op_1t(num, None, times, [a], A_op, sx) + np.testing.assert_allclose(cmp_sx[:2], cmp_switch[:2]) + np.testing.assert_allclose(cmp_sz[-2:], cmp_switch[-2:]) + + +def test_alternative_solver(): + from qutip.solver.mesolve import MESolver + from qutip.solver.brmesolve import BRSolver + + H = qutip.num(5) + a = qutip.destroy(5) + a_ops = [(a+a.dag(), qutip.coefficient(lambda _, w: w>0, args={"w":0}))] + + br = BRSolver(H, a_ops) + me = MESolver(H, [a]) + times = np.arange(4) + + br_corr = qutip.correlation_3op(br, qutip.basis(5), [0], times, a, a.dag()) + me_corr = qutip.correlation_3op(me, qutip.basis(5), [0], times, a, a.dag()) + + np.testing.assert_allclose(br_corr, me_corr) + + +def test_G1(): + H = qutip.Qobj([[0,1], [1,0]]) + psi0 = qutip.basis(2) + taus = np.linspace(0, 1, 11) + scale = 2 + a_op = qutip.sigmaz() * scale + g1, G1 = qutip.coherence_function_g1(H, psi0, taus, [], a_op) + expected = np.array([np.cos(t)**2 - np.sin(t)**2 for t in taus]) + np.testing.assert_allclose(g1, expected, rtol=2e-5) + np.testing.assert_allclose(G1, expected * scale**2, rtol=2e-5) + + +def test_G2(): + N = 10 + H = qutip.rand_dm(N) + psi0 = qutip.rand_ket(N) + taus = np.linspace(0, 1, 11) + scale = 2 + a_op = qutip.rand_unitary(N) * scale + g1, G1 = qutip.coherence_function_g2(H, psi0, taus, [], a_op) + expected = np.ones(11) + np.testing.assert_allclose(g1, expected, rtol=2e-5) + np.testing.assert_allclose(G1, expected * scale**4, rtol=2e-5) diff --git a/qutip/tests/solver/test_countstat.py b/qutip/tests/solver/test_countstat.py index 142115302e..6d389b20d1 100644 --- a/qutip/tests/solver/test_countstat.py +++ b/qutip/tests/solver/test_countstat.py @@ -37,6 +37,7 @@ def test_dqd_current(): for n, eps in enumerate(eps_vec): H = eps / 2 * sz + tc * sx + L0 = qutip.liouvillian(H) L = qutip.liouvillian(H, c_ops) rhoss = qutip.steadystate(L) current_1, noise_1 = qutip.countstat_current_noise( @@ -48,10 +49,10 @@ def test_dqd_current(): current_2 = qutip.countstat_current(L, rhoss=rhoss, J_ops=J_ops) assert abs(current_1[0] - current_2[0]) < 1e-8 - current_2 = qutip.countstat_current(L, c_ops, J_ops=J_ops) + current_2 = qutip.countstat_current(L0, c_ops, J_ops=J_ops) assert abs(current_1[0] - current_2[0]) < 1e-8 - current_2 = qutip.countstat_current(L, c_ops) + current_2 = qutip.countstat_current(H, c_ops) assert abs(current_1[0] - current_2[0]) < 1e-8 current_3, noise_3 = qutip.countstat_current_noise( diff --git a/qutip/tests/solver/test_floquet.py b/qutip/tests/solver/test_floquet.py new file mode 100644 index 0000000000..be90e6fab2 --- /dev/null +++ b/qutip/tests/solver/test_floquet.py @@ -0,0 +1,333 @@ +import numpy as np +from qutip import ( + sigmax, sigmay, sigmaz, sigmap, sigmam, + rand_ket, num, destroy, + mesolve, expect, sesolve, + Qobj, QobjEvo, coefficient + ) + +from qutip.solver.floquet import ( + FloquetBasis, floquet_tensor, fmmesolve, FMESolver, + _floquet_delta_tensor, _floquet_X_matrices, fsesolve +) +import pytest + + +def _convert_c_ops(c_op_fmmesolve, noise_spectrum, vp, ep): + """ + Convert e_ops for fmmesolve to mesolve + """ + c_op_mesolve = [] + N = len(vp) + for i in range(N): + for j in range(N): + if i != j: + # caclculate the rate + gamma = 2 * np.pi * c_op_fmmesolve.matrix_element( + vp[j], vp[i]) * c_op_fmmesolve.matrix_element( + vp[i], vp[j]) * noise_spectrum(ep[j] - ep[i]) + + # add c_op for mesolve + c_op_mesolve.append( + np.sqrt(gamma) * (vp[i] * vp[j].dag()) + ) + return c_op_mesolve + + +class TestFloquet: + """ + A test class for the QuTiP functions for Floquet formalism. + """ + + def testFloquetBasis(self): + N = 10 + a = destroy(N) + H = num(N) + (a+a.dag()) * coefficient(lambda t: np.cos(t)) + T = 2 * np.pi + floquet_basis = FloquetBasis(H, T) + psi0 = rand_ket(N) + tlist = np.linspace(0, 10, 11) + floquet_psi0 = floquet_basis.to_floquet_basis(psi0) + 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) + + def testFloquetUnitary(self): + N = 10 + a = destroy(N) + H = num(N) + (a+a.dag()) * coefficient(lambda t: np.cos(t)) + T = 2 * np.pi + psi0 = rand_ket(N) + tlist = np.linspace(0, 10, 11) + states_se = sesolve(H, psi0, tlist).states + states_fse = fsesolve(H, psi0, tlist, T=T).states + for state_se, state_fse in zip(states_se, states_fse): + assert state_se.overlap(state_fse) == pytest.approx(1., abs=5e-5) + + + def testFloquetMasterEquation1(self): + """ + Test Floquet-Markov Master Equation for a driven two-level system + without dissipation. + """ + delta = 1.0 * 2 * np.pi + eps0 = 1.0 * 2 * np.pi + A = 0.5 * 2 * np.pi + omega = np.sqrt(delta ** 2 + eps0 ** 2) + T = (2 * np.pi) / omega + tlist = np.linspace(0.0, 2 * T, 101) + psi0 = rand_ket(2) + H0 = - eps0 / 2.0 * sigmaz() - delta / 2.0 * sigmax() + H1 = A / 2.0 * sigmax() + args = {'w': omega} + H = [H0, [H1, lambda t, args: np.sin(args['w'] * t)]] + e_ops = [num(2)] + gamma1 = 0 + + # Collapse operator for Floquet-Markov Master Equation + c_op_fmmesolve = sigmax() + + # Collapse operators for Lindblad Master Equation + def spectrum(omega): + return (omega > 0) * omega * 0.5 * gamma1 / (2 * np.pi) + + ep, vp = H0.eigenstates() + op0 = vp[0]*vp[0].dag() + op1 = vp[1]*vp[1].dag() + + c_op_mesolve = _convert_c_ops(c_op_fmmesolve, spectrum, vp, ep) + + # Solve the floquet-markov master equation + p_ex = fmmesolve(H, psi0, tlist, [c_op_fmmesolve], [num(2)], + [spectrum], T, args=args).expect[0] + + # Compare with mesolve + p_ex_ref = mesolve( + H, psi0, tlist, c_op_mesolve, [num(2)], args + ).expect[0] + + np.testing.assert_allclose(np.real(p_ex), np.real(p_ex_ref), atol=1e-4) + + def testFloquetMasterEquation2(self): + """ + Test Floquet-Markov Master Equation for a two-level system + subject to dissipation. + """ + delta = 1.0 * 2 * np.pi + eps0 = 1.0 * 2 * np.pi + A = 0.0 * 2 * np.pi + omega = np.sqrt(delta ** 2 + eps0 ** 2) + T = (2 * np.pi) / omega + tlist = np.linspace(0.0, 2 * T, 101) + psi0 = rand_ket(2) + H0 = - eps0 / 2.0 * sigmaz() - delta / 2.0 * sigmax() + H1 = A / 2.0 * sigmax() + args = {'w': omega} + H = QobjEvo([H0, [H1, lambda t, w: np.sin(w * t)]], + args=args) + e_ops = [num(2)] + gamma1 = 1 + + # Collapse operator for Floquet-Markov Master Equation + c_op_fmmesolve = sigmax() + + # Collapse operator for Lindblad Master Equation + def spectrum(omega): + return (omega > 0) * omega * 0.5 * gamma1 / (2 * np.pi) + + ep, vp = H0.eigenstates() + op0 = vp[0]*vp[0].dag() + op1 = vp[1]*vp[1].dag() + + c_op_mesolve = _convert_c_ops(c_op_fmmesolve, spectrum, vp, ep) + + # Solve the floquet-markov master equation + p_ex = fmmesolve( + H, psi0, tlist, [c_op_fmmesolve], [num(2)], + [spectrum], T, args=args + ).expect[0] + + # Compare with mesolve + p_ex_ref = mesolve( + H, psi0, tlist, c_op_mesolve, [num(2)], args + ).expect[0] + + np.testing.assert_allclose(np.real(p_ex), np.real(p_ex_ref), atol=1e-4) + + @pytest.mark.parametrize("kmax", [5, 25, 100]) + def testFloquetMasterEquation3(self, kmax): + """ + Test Floquet-Markov Master Equation for a two-level system + subject to dissipation with internal transform of fmmesolve + """ + delta = 1.0 * 2 * np.pi + eps0 = 1.0 * 2 * np.pi + A = 0.0 * 2 * np.pi + omega = np.sqrt(delta ** 2 + eps0 ** 2) + T = (2 * np.pi) / omega + tlist = np.linspace(0.0, 2 * T, 101) + psi0 = rand_ket(2) + H0 = - eps0 / 2.0 * sigmaz() - delta / 2.0 * sigmax() + H1 = A / 2.0 * sigmax() + args = {'w': omega} + H = QobjEvo([H0, [H1, lambda t, w: np.sin(w * t)]], + args=args) + e_ops = [num(2)] + gamma1 = 1 + + # Collapse operator for Floquet-Markov Master Equation + c_op_fmmesolve = sigmax() + + # Collapse operator for Lindblad Master Equation + def spectrum(omega): + return (omega > 0) * 0.5 * gamma1 * omega / (2 * np.pi) + + ep, vp = H0.eigenstates() + op0 = vp[0]*vp[0].dag() + op1 = vp[1]*vp[1].dag() + + c_op_mesolve = _convert_c_ops(c_op_fmmesolve, spectrum, vp, ep) + + # Solve the floquet-markov master equation + floquet_basis = FloquetBasis(H, T) + solver = FMESolver( + floquet_basis, [(c_op_fmmesolve, spectrum)], kmax=kmax + ) + solver.start(psi0, tlist[0]) + p_ex = [expect(num(2), solver.step(t)) for t in tlist] + + # Compare with mesolve + output2 = mesolve(H, psi0, tlist, c_op_mesolve, [num(2)], args) + p_ex_ref = output2.expect[0] + + np.testing.assert_allclose(np.real(p_ex), np.real(p_ex_ref), + atol=5 * 1e-4) + + def testFloquetMasterEquation_multiple_coupling(self): + """ + Test Floquet-Markov Master Equation for a two-level system + subject to dissipation with multiple coupling operators + """ + delta = 1.0 * 2 * np.pi + eps0 = 1.0 * 2 * np.pi + A = 0.0 * 2 * np.pi + omega = np.sqrt(delta ** 2 + eps0 ** 2) + T = (2 * np.pi) / omega + tlist = np.linspace(0.0, 2 * T, 101) + psi0 = rand_ket(2) + H0 = - eps0 / 2.0 * sigmaz() - delta / 2.0 * sigmax() + H1 = A / 2.0 * sigmax() + args = {'w': omega} + H = QobjEvo([H0, [H1, lambda t, w: np.sin(w * t)]], + args=args) + e_ops = [num(2)] + gamma1 = 1 + + # Collapse operator for Floquet-Markov Master Equation + c_ops_fmmesolve = [sigmax(), sigmay()] + + # Collapse operator for Lindblad Master Equation + def noise_spectrum1(omega): + return (omega > 0) * 0.5 * gamma1 * omega/(2*np.pi) + + def noise_spectrum2(omega): + return (omega > 0) * 0.5 * gamma1 / (2 * np.pi) + + noise_spectra = [noise_spectrum1, noise_spectrum2] + + ep, vp = H0.eigenstates() + op0 = vp[0]*vp[0].dag() + op1 = vp[1]*vp[1].dag() + + c_op_mesolve = [] + + # Convert the c_ops for fmmesolve to c_ops for mesolve + for c_op_fmmesolve, noise_spectrum in zip(c_ops_fmmesolve, + noise_spectra): + c_op_mesolve += _convert_c_ops( + c_op_fmmesolve, noise_spectrum, vp, ep + ) + + # Solve the floquet-markov master equation + output1 = fmmesolve( + H, psi0, tlist, c_ops_fmmesolve, e_ops, noise_spectra, T + ) + p_ex = output1.expect[0] + # Compare with mesolve + output2 = mesolve(H, psi0, tlist, c_op_mesolve, e_ops, args) + p_ex_ref = output2.expect[0] + + np.testing.assert_allclose(np.real(p_ex), np.real(p_ex_ref), atol=1e-4) + + def testFloquetRates(self): + """ + Compare rate transition and frequency transitions to analytical + results for a driven two-level system, for different drive amplitudes. + """ + # Parameters + wq = 5.4 * 2 * np.pi + wd = 6.7 * 2 * np.pi + delta = wq - wd + T = 2 * np.pi / wd + tlist = np.linspace(0.0, 2 * T, 101) + array_A = np.linspace(0.001, 3 * 2 * np.pi, 10, endpoint=False) + H0 = Qobj(wq / 2 * sigmaz()) + args = {'wd': wd} + c_ops = sigmax() + gamma1 = 1 + kmax = 5 + + array_ana_E0 = [-np.sqrt((delta / 2)**2 + a**2) for a in array_A] + array_ana_E1 = [np.sqrt((delta / 2)**2 + a**2) for a in array_A] + array_ana_delta = [ + 2 * np.sqrt((delta / 2)**2 + a**2) + for a in array_A + ] + + def noise_spectrum(omega): + return (omega > 0) * 0.5 * gamma1 * omega/(2*np.pi) + + idx = 0 + for a in array_A: + # Hamiltonian + H1_p = a * sigmap() + H1_m = a * sigmam() + H = QobjEvo( + [H0, [H1_p, lambda t, wd: np.exp(-1j * wd * t)], + [H1_m, lambda t, wd: np.exp(1j * wd * t)]], args=args + ) + + floquet_basis = FloquetBasis(H, T) + DeltaMatrix = _floquet_delta_tensor(floquet_basis.e_quasi, kmax, T) + X = _floquet_X_matrices(floquet_basis, [c_ops], kmax) + + # Check energies + deltas = np.ndarray.flatten(DeltaMatrix) + + # deltas and array_ana_delta have at least 1 value in common. + assert (min(abs(deltas - array_ana_delta[idx])) < 1e-4) + + # Check matrix elements + Xs = np.concatenate( + [X[0][k].to_array() for k in range(-kmax, kmax+1)] + ).flatten() + + normPlus = np.sqrt(a**2 + (array_ana_E1[idx] - delta / 2)**2) + normMinus = np.sqrt(a**2 + (array_ana_E0[idx] - delta / 2)**2) + + Xpp_p1 = (a / normPlus**2) * (array_ana_E1[idx] - delta / 2) + assert (min(abs(Xs - Xpp_p1)) < 1e-4) + Xpp_m1 = (a / normPlus**2) * (array_ana_E1[idx] - delta / 2) + assert (min(abs(Xs - Xpp_m1)) < 1e-4) + Xmm_p1 = (a / normMinus**2) * (array_ana_E0[idx] - delta / 2) + assert (min(abs(Xs - Xmm_p1)) < 1e-4) + Xmm_m1 = (a / normMinus**2) * (array_ana_E0[idx] - delta / 2) + assert (min(abs(Xs - Xmm_m1)) < 1e-4) + Xpm_p1 = (a / (normMinus * normPlus) + * (array_ana_E0[idx]- delta / 2)) + assert (min(abs(Xs - Xpm_p1)) < 1e-4) + Xpm_m1 = (a / (normMinus * normPlus) + * (array_ana_E1[idx] - delta / 2)) + assert (min(abs(Xs - Xpm_m1)) < 1e-4) + idx += 1 diff --git a/qutip/tests/solver/test_integrator.py b/qutip/tests/solver/test_integrator.py index 3ba60add1c..8779ea76a2 100644 --- a/qutip/tests/solver/test_integrator.py +++ b/qutip/tests/solver/test_integrator.py @@ -1,7 +1,8 @@ -from qutip.solver.sesolve import SeSolver -from qutip.solver.mesolve import MeSolver +from qutip.solver.sesolve import SESolver +from qutip.solver.mesolve import MESolver +from qutip.solver.mcsolve import MCSolver from qutip.solver.solver_base import Solver -from qutip.solver.ode.scipy_integrator import * +from qutip.solver.integrator import * import qutip import numpy as np from numpy.testing import assert_allclose @@ -15,30 +16,30 @@ class TestIntegratorCte(): me_system = qutip.liouvillian(qutip.QobjEvo(qutip.qeye(2)), c_ops=[qutip.destroy(2)]) - @pytest.fixture(params=list(SeSolver.avail_integrators().keys())) + @pytest.fixture(params=list(SESolver.avail_integrators().keys())) def se_method(self, request): return request.param - @pytest.fixture(params=list(MeSolver.avail_integrators().keys())) + @pytest.fixture(params=list(MESolver.avail_integrators().keys())) def me_method(self, request): return request.param - # TODO: Change when the McSolver is added - @pytest.fixture(params=list(Solver.avail_integrators().keys())) + # TODO: Change when the MCSolver is added + @pytest.fixture(params=list(MCSolver.avail_integrators().keys())) def mc_method(self, request): return request.param def test_se_integration(self, se_method): - evol = SeSolver.avail_integrators()[se_method](self.se_system, {}) - state0 = qutip.core.unstack_columns(qutip.basis(6,0).data, (2, 3)) + evol = SESolver.avail_integrators()[se_method](self.se_system, {}) + state0 = qutip.basis(2, 0).data evol.set_state(0, state0) for t, state in evol.run(np.linspace(0, 2, 21)): assert_allclose(self._analytical_se(t), state.to_array()[0, 0], atol=2e-5) - assert state.shape == (2, 3) + assert state.shape == (2, 1) def test_me_integration(self, me_method): - evol = MeSolver.avail_integrators()[me_method](self.me_system, {}) + evol = MESolver.avail_integrators()[me_method](self.me_system, {}) state0 = qutip.operator_to_vector(qutip.fock_dm(2,1)).data evol.set_state(0, state0) for t in np.linspace(0, 2, 21): @@ -48,7 +49,7 @@ def test_me_integration(self, me_method): state.to_array()[0, 0], atol=2e-5) def test_mc_integration(self, mc_method): - evol = Solver.avail_integrators()[mc_method](self.se_system, {}) + evol = MCSolver.avail_integrators()[mc_method](self.se_system, {}) state = qutip.basis(2,0).data evol.set_state(0, state) t = 0 @@ -99,27 +100,48 @@ class TestIntegrator(TestIntegratorCte): ) @pytest.fixture( - params=[key for key, integrator in SeSolver.avail_integrators().items() + params=[key for key, integrator in SESolver.avail_integrators().items() if integrator.support_time_dependant] ) def se_method(self, request): return request.param @pytest.fixture( - params=[key for key, integrator in MeSolver.avail_integrators().items() + params=[key for key, integrator in MESolver.avail_integrators().items() if integrator.support_time_dependant] ) def me_method(self, request): return request.param @pytest.fixture( - params=[key for key, integrator in Solver.avail_integrators().items() + params=[key for key, integrator in MCSolver.avail_integrators().items() if integrator.support_time_dependant] ) def mc_method(self, request): return request.param +@pytest.mark.parametrize('sizes', [(1, 100), (10, 10), (100, 0)], + ids=["large", "multiple subspaces", "diagonal"]) +def test_krylov(sizes): + # Krylov solve act differently for large systems composed tensored + # sub systems. + N, M = sizes + H = qutip.qeye(N) + if M: + H = H & (qutip.num(M) + qutip.create(M) + qutip.destroy(M)) + H = qutip.QobjEvo(-1j * H) + integrator = IntegratorKrylov(H, {}) + ref_integrator = IntegratorDiag(H, {}) + psi = qutip.basis(100, 95).data + integrator.set_state(0, psi) + ref_integrator.set_state(0, psi) + for t in np.linspace(0.25, 1, 4): + out = integrator.integrate(t)[1] + ref = ref_integrator.integrate(t)[1] + assert qutip.data.norm.l2(out - ref) == pytest.approx(0, abs=1e-6) + + @pytest.mark.parametrize('integrator', [IntegratorScipyAdams, IntegratorScipyBDF, IntegratorScipylsoda], ids=["adams", 'bdf', "lsoda"] diff --git a/qutip/tests/solver/test_mcsolve.py b/qutip/tests/solver/test_mcsolve.py index ec2be2e8b8..be5b5b3bad 100644 --- a/qutip/tests/solver/test_mcsolve.py +++ b/qutip/tests/solver/test_mcsolve.py @@ -2,7 +2,7 @@ import numpy as np import qutip from copy import copy -from qutip.solver.mcsolve import mcsolve, McSolver +from qutip.solver.mcsolve import mcsolve, MCSolver from qutip.solver.solver_base import Solver def _return_constant(t, args): @@ -253,8 +253,6 @@ def test_expectation_outputs(keep_runs_results): assert isinstance(data.runs_expect[0][0][1], float) assert isinstance(data.runs_expect[1][0][1], float) assert isinstance(data.runs_expect[2][0][1], complex) - assert not np.allclose(data.std_expect[0][1], - data.expect_traj_std(3)[0][1]) assert isinstance(data.photocurrent[0][0], float) assert isinstance(data.photocurrent[1][0], float) assert (np.array(data.runs_photocurrent).shape @@ -341,7 +339,7 @@ def test_stepping(self): size = 10 a = qutip.QobjEvo([qutip.destroy(size), 'alpha'], args={'alpha': 0}) H = qutip.num(size) - mcsolver = McSolver(H, a, options={'map': 'serial'}) + mcsolver = MCSolver(H, a, options={'map': 'serial'}) mcsolver.start(qutip.basis(size, size-1), 0, seed=5) state_1 = mcsolver.step(1, args={'alpha':1}) @@ -385,11 +383,11 @@ def test_super_H(): np.testing.assert_allclose(mc_expected.expect[0], mc.expect[0], atol=0.5) -def test_McSolver_run(): +def test_MCSolver_run(): size = 10 a = qutip.QobjEvo([qutip.destroy(size), 'coupling'], args={'coupling':0}) H = qutip.num(size) - solver = McSolver(H, a) + solver = MCSolver(H, a) solver.options = {'store_final_state': True} res = solver.run(qutip.basis(size, size-1), np.linspace(0, 5.0, 11), e_ops=[qutip.qeye(size)], args={'coupling': 1}) @@ -405,11 +403,11 @@ def test_McSolver_run(): assert res.num_trajectories == 1001 -def test_McSolver_stepping(): +def test_MCSolver_stepping(): size = 10 a = qutip.QobjEvo([qutip.destroy(size), 'coupling'], args={'coupling':0}) H = qutip.num(size) - solver = McSolver(H, a) + solver = MCSolver(H, a) solver.start(qutip.basis(size, size-1), 0, seed=0) solver.options = {'method': 'lsoda'} state = solver.step(1) diff --git a/qutip/tests/solver/test_mesolve.py b/qutip/tests/solver/test_mesolve.py index 90bfe615d7..6f6bbab6c7 100644 --- a/qutip/tests/solver/test_mesolve.py +++ b/qutip/tests/solver/test_mesolve.py @@ -2,13 +2,13 @@ from types import FunctionType import qutip -from qutip.solver.mesolve import mesolve, MeSolver +from qutip.solver.mesolve import mesolve, MESolver from qutip.solver.solver_base import Solver import pickle import pytest all_ode_method = [ - method for method, integrator in MeSolver.avail_integrators().items() + method for method, integrator in MESolver.avail_integrators().items() if integrator.support_time_dependant ] @@ -222,7 +222,7 @@ def test_mesolve_normalization(self, state_type): def test_mesolver_pickling(self): options = {"progress_bar": None} - solver_obj = MeSolver(self.ada, c_ops=[self.a], options=options) + solver_obj = MESolver(self.ada, c_ops=[self.a], options=options) solver_copy = pickle.loads(pickle.dumps(solver_obj)) e1 = solver_obj.run(qutip.basis(self.N, 9), [0, 1, 2, 3], e_ops=[self.ada]).expect[0] @@ -234,7 +234,7 @@ def test_mesolver_pickling(self): all_ode_method, ids=all_ode_method) def test_mesolver_stepping(self, method): options = {"method": method, "progress_bar": None} - solver_obj = MeSolver( + solver_obj = MESolver( self.ada, c_ops=qutip.QobjEvo( [self.a, lambda t, kappa: np.sqrt(kappa * np.exp(-t))], @@ -661,17 +661,17 @@ def test_num_collapse_set(): def test_mesolve_bad_H(): with pytest.raises(TypeError): - MeSolver([qutip.qeye(3), 't'], []) + MESolver([qutip.qeye(3), 't'], []) with pytest.raises(TypeError): - MeSolver(qutip.qeye(3), [[qutip.qeye(3), 't']]) + MESolver(qutip.qeye(3), [[qutip.qeye(3), 't']]) def test_mesolve_bad_state(): - solver = MeSolver(qutip.qeye(4), []) + solver = MESolver(qutip.qeye(4), []) with pytest.raises(TypeError): solver.start(qutip.basis(2,1) & qutip.basis(2,0), 0) def test_mesolve_bad_options(): with pytest.raises(TypeError): - MeSolver(qutip.qeye(4), [], options=False) + MESolver(qutip.qeye(4), [], options=False) diff --git a/qutip/tests/solver/test_options.py b/qutip/tests/solver/test_options.py index 79d16e1d0b..5f75405b1c 100644 --- a/qutip/tests/solver/test_options.py +++ b/qutip/tests/solver/test_options.py @@ -89,10 +89,10 @@ def test_print(): def test_in_solver(): opt = {"method": "adams", "store_states": True, "atol": 1} - solver = qutip.solver.sesolve.SeSolver(qutip.qeye(1), options=opt) - adams = qutip.solver.ode.scipy_integrator.IntegratorScipyAdams - lsoda = qutip.solver.ode.scipy_integrator.IntegratorScipylsoda - bdf = qutip.solver.ode.scipy_integrator.IntegratorScipyBDF + solver = qutip.SESolver(qutip.qeye(1), options=opt) + adams = qutip.integrator.IntegratorScipyAdams + lsoda = qutip.integrator.IntegratorScipylsoda + bdf = qutip.integrator.IntegratorScipyBDF assert solver.options["store_states"] is True assert solver.options["method"] == "adams" assert solver.options["atol"] == 1 @@ -119,7 +119,7 @@ def test_in_solver(): def test_options_update_solver(): opt = {"method": "adams", "normalize_output": False} - solver = qutip.solver.sesolve.SeSolver(1j * qutip.qeye(1), options=opt) + solver = qutip.SESolver(1j * qutip.qeye(1), options=opt) solver.start(qutip.basis(1), 0) err_atol_def = (solver.step(1) - np.exp(1)).norm() diff --git a/qutip/tests/solver/test_propagator.py b/qutip/tests/solver/test_propagator.py index 09605a5e74..2d36d9f977 100644 --- a/qutip/tests/solver/test_propagator.py +++ b/qutip/tests/solver/test_propagator.py @@ -1,6 +1,12 @@ import numpy as np from qutip import (destroy, propagator, Propagator, propagator_steadystate, steadystate, tensor, qeye, basis, QobjEvo, sesolve) +import qutip +import pytest +from qutip.solver.brmesolve import BRSolver +from qutip.solver.mesolve import MESolver +from qutip.solver.sesolve import SESolver +from qutip.solver.mcsolve import MCSolver def testPropHOB(): @@ -15,7 +21,7 @@ def testPropObj(): opt = {"method": "dop853"} a = destroy(5) H = a.dag()*a - U = Propagator(H, [a], options=opt, memoize=5, tol=1e-5) + U = Propagator(H, c_ops=[a], options=opt, memoize=5, tol=1e-5) # Few call to fill the stored propagators. U(0.5), U(0.25), U(0.75), U(1), U(-1), U(-.5) assert len(U.times) == 5 @@ -44,7 +50,7 @@ def testPropHOTd(): def testPropObjTd(): a = destroy(5) H = a.dag()*a - U = Propagator([H, [H, "w*t"]], [a], args={'w': 1}) + U = Propagator([H, [H, "w*t"]], c_ops=[a], args={'w': 1}) assert ( U(1) - propagator([H, [H, "w*t"]], 1, [a], args={'w': 1}) ).norm('max') < 1e-4 @@ -92,3 +98,43 @@ def testPropEvo(): ).states for t, psi_t in zip(tlist, psi_expected): assert abs(psi(t).overlap(psi_t)) > 1-1e-6 + + +def _make_se(H, a): + return SESolver(H) + + +def _make_me(H, a): + return MESolver(H, [a]) + + +def _make_br(H, a): + spectra = qutip.coefficient(lambda t, w: w >= 0, args={"w": 0}) + return BRSolver(H, [(a+a.dag(), spectra)]) + + +@pytest.mark.parametrize('solver', [ + pytest.param(_make_se, id='SESolver'), + pytest.param(_make_me, id='MESolver'), + pytest.param(_make_br, id='BRSolver'), +]) +def testPropSolver(solver): + a = destroy(5) + H = a.dag()*a + U = Propagator(solver(H, a)) + c_ops = [] + if solver is not _make_se: + c_ops = [a] + + assert (U(1) - propagator(H, 1, c_ops)).norm('max') < 1e-4 + assert (U(0.5) - propagator(H, 0.5, c_ops)).norm('max') < 1e-4 + assert (U(1.5, 0.5) - propagator(H, 1, c_ops)).norm('max') < 1e-4 + + +def testPropMCSolver(): + a = destroy(5) + H = a.dag()*a + solver = MCSolver(H, [a]) + with pytest.raises(TypeError) as err: + Propagator(solver) + assert str(err.value).startswith("Non-deterministic") diff --git a/qutip/tests/solver/test_results.py b/qutip/tests/solver/test_results.py index ea5de3f801..3aa46982c9 100644 --- a/qutip/tests/solver/test_results.py +++ b/qutip/tests/solver/test_results.py @@ -99,7 +99,7 @@ def test_expect_and_e_ops(self, N, e_ops, results): res.e_ops[k](i, qutip.basis(N, i)) for i in range(N) ] if isinstance(res.expect[i][0], qutip.Qobj): - assert res.expect[i] == results[k] + assert (res.expect[i] == results[k]).all() assert res.e_data[k] == results[k] assert e_op_call_values == results[k] else: @@ -195,17 +195,9 @@ def _expect_check_types(self, multiresult): if multiresult.trajectories: assert isinstance(multiresult.runs_expect, list) assert isinstance(multiresult.runs_e_data, dict) - assert isinstance(multiresult.expect_traj_avg(), list) - assert isinstance(multiresult.expect_traj_std(), list) - assert isinstance(multiresult.e_data_traj_avg(), dict) - assert isinstance(multiresult.e_data_traj_std(), dict) else: assert multiresult.runs_expect == [] assert multiresult.runs_e_data == {} - assert multiresult.expect_traj_avg() is None - assert multiresult.expect_traj_std() is None - assert multiresult.e_data_traj_avg() is None - assert multiresult.e_data_traj_std() is None @pytest.mark.parametrize('keep_runs_results', [True, False]) @pytest.mark.parametrize('dm', [True, False]) @@ -266,10 +258,6 @@ def test_multitraj_expect(self, keep_runs_results, e_ops, results): np.testing.assert_allclose(expect, expected, atol=1e-14, rtol=0.1) - for expect, expected in zip(m_res.expect_traj_avg(25), results): - np.testing.assert_allclose(expect, expected, - atol=1e-14, rtol=0.04) - self._expect_check_types(m_res) assert m_res.stats['end_condition'] == "unknown" diff --git a/qutip/tests/solver/test_sesolve.py b/qutip/tests/solver/test_sesolve.py index 135be222bf..5ca744f973 100644 --- a/qutip/tests/solver/test_sesolve.py +++ b/qutip/tests/solver/test_sesolve.py @@ -2,11 +2,12 @@ import pickle import qutip import numpy as np -from qutip.solver.sesolve import sesolve, SeSolver +from qutip.solver.sesolve import sesolve, SESolver +from qutip.solver.krylovsolve import krylovsolve from qutip.solver.solver_base import Solver all_ode_method = [ - method for method, integrator in SeSolver.avail_integrators().items() + method for method, integrator in SESolver.avail_integrators().items() if integrator.support_time_dependant ] @@ -200,7 +201,7 @@ def test_compare_evolution(self, H, normalize, args, tol=5e-5): def test_sesolver_args(self): options = {"progress_bar": None} - solver_obj = SeSolver(qutip.QobjEvo([self.H0, [self.H1,'a']], + solver_obj = SESolver(qutip.QobjEvo([self.H0, [self.H1,'a']], args={'a': 1}), options=options) res = solver_obj.run(qutip.basis(2,1), [0, 1, 2, 3], @@ -210,7 +211,7 @@ def test_sesolver_args(self): def test_sesolver_pickling(self): e_ops = [qutip.sigmax(), qutip.sigmay(), qutip.sigmaz()] options = {"progress_bar": None} - solver_obj = SeSolver(self.H0 + self.H1, + solver_obj = SESolver(self.H0 + self.H1, options=options) solver_copy = pickle.loads(pickle.dumps(solver_obj)) sx, sy, sz = solver_obj.run(qutip.basis(2,1), [0, 1, 2, 3], @@ -229,7 +230,7 @@ def test_sesolver_stepping(self, method): "rtol": 1e-8, "progress_bar": None } - solver_obj = SeSolver( + solver_obj = SESolver( qutip.QobjEvo([self.H1, lambda t, a: a], args={"a":0.25}), options=options ) @@ -268,13 +269,13 @@ def test_sesolver_stepping(self, method): def test_sesolve_bad_H(): with pytest.raises(TypeError): - SeSolver(np.eye(3)) + SESolver(np.eye(3)) with pytest.raises(ValueError): - SeSolver(qutip.basis(3,1)) + SESolver(qutip.basis(3,1)) def test_sesolve_bad_state(): - solver = SeSolver(qutip.qeye(4)) + solver = SESolver(qutip.qeye(4)) with pytest.raises(TypeError): solver.start(qutip.basis(4,1).dag(), 0) with pytest.raises(TypeError): @@ -282,6 +283,19 @@ def test_sesolve_bad_state(): def test_sesolve_step_no_start(): - solver = SeSolver(qutip.qeye(4)) + solver = SESolver(qutip.qeye(4)) with pytest.raises(RuntimeError): solver.step(1) + + +@pytest.mark.parametrize("always_compute_step", [True, False]) +def test_krylovsolve(always_compute_step): + H = qutip.tensor([qutip.rand_herm(2) for _ in range(8)]) + psi0 = qutip.basis([2]*8, [1]*8) + e_op = qutip.num(256) + e_op.dims = H.dims + tlist = np.linspace(0, 1, 11) + ref = sesolve(H, psi0, tlist, e_ops=[e_op]).expect[0] + 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) diff --git a/qutip/tests/solve/test_steadystate.py b/qutip/tests/solver/test_steadystate.py similarity index 70% rename from qutip/tests/solve/test_steadystate.py rename to qutip/tests/solver/test_steadystate.py index f760cf2eba..6ae02a6489 100644 --- a/qutip/tests/solve/test_steadystate.py +++ b/qutip/tests/solver/test_steadystate.py @@ -9,25 +9,24 @@ pytest.param('direct', {'solver':'mkl'}, id="direct_mkl", marks=pytest.mark.skipif(not qutip.settings.has_mkl, reason='MKL extensions not found.')), - pytest.param('direct', {'return_info':True}, id="direct_info"), pytest.param('direct', {'sparse':False}, id="direct_dense"), pytest.param('direct', {'use_rcm':True}, id="direct_rcm"), pytest.param('direct', {'use_wbm':True}, id="direct_wbm"), pytest.param('eigen', {}, id="eigen"), pytest.param('eigen', {'use_rcm':True}, id="eigen_rcm"), pytest.param('svd', {}, id="svd"), - pytest.param('power', {'mtol':1e-5}, id="power"), - pytest.param('power', {'mtol':1e-5, 'solver':'mkl'}, id="power_mkl", + pytest.param('power', {'power_tol':1e-5}, id="power"), + pytest.param('power', {'power_tol':1e-5, 'solver':'mkl'}, id="power_mkl", marks=pytest.mark.skipif(not qutip.settings.has_mkl, reason='MKL extensions not found.')), - pytest.param('power-gmres', {'mtol':1e-1}, id="power-gmres"), - pytest.param('power-gmres', {'mtol':1e-1, 'use_rcm':True, 'use_wbm':True}, + pytest.param('power-gmres', {'tol':1e-1, 'atol': 1e-12}, id="power-gmres"), + pytest.param('power-gmres', {'tol':1e-1, 'use_rcm':True, 'use_wbm':True, 'atol': 1e-12}, id="power-gmres_perm"), - pytest.param('power-bicgstab', {'use_precond':1}, id="power-bicgstab"), - pytest.param('iterative-gmres', {}, id="iterative-gmres"), - pytest.param('iterative-gmres', {'use_rcm':True, 'use_wbm':True}, + pytest.param('power-bicgstab', {"atol": 1e-10, "tol": 1e-10, 'use_precond':1}, id="power-bicgstab"), + pytest.param('iterative-gmres', {'tol': 1e-12, 'atol': 1e-12}, id="iterative-gmres"), + pytest.param('iterative-gmres', {'use_rcm':True, 'use_wbm':True, 'tol': 1e-12, 'atol': 1e-12}, id="iterative-gmres_perm"), - pytest.param('iterative-bicgstab', {'return_info':True}, + pytest.param('iterative-bicgstab', {'atol': 1e-12, "tol": 1e-10}, id="iterative-bicgstab"), ]) def test_qubit(method, kwargs): @@ -41,23 +40,15 @@ def test_qubit(method, kwargs): wth_vec = np.linspace(0.1, 3, 20) p_ss = np.zeros(np.shape(wth_vec)) - with warnings.catch_warnings(): - if 'use_wbm' in kwargs: - # The deprecation has been fixed in dev.major - warnings.simplefilter("ignore", category=DeprecationWarning) - - for idx, wth in enumerate(wth_vec): - n_th = 1.0 / (np.exp(1.0 / wth) - 1) # bath temperature - c_op_list = [] - rate = gamma1 * (1 + n_th) - c_op_list.append(np.sqrt(rate) * sm) - rate = gamma1 * n_th - c_op_list.append(np.sqrt(rate) * sm.dag()) - rho_ss = qutip.steadystate(H, c_op_list, method=method, **kwargs) - if 'return_info' in kwargs: - rho_ss, info = rho_ss - assert isinstance(info, dict) - p_ss[idx] = qutip.expect(sm.dag() * sm, rho_ss) + for idx, wth in enumerate(wth_vec): + n_th = 1.0 / (np.exp(1.0 / wth) - 1) # bath temperature + c_op_list = [] + rate = gamma1 * (1 + n_th) + c_op_list.append(np.sqrt(rate) * sm) + rate = gamma1 * n_th + c_op_list.append(np.sqrt(rate) * sm.dag()) + rho_ss = qutip.steadystate(H, c_op_list, method=method, **kwargs) + p_ss[idx] = qutip.expect(sm.dag() * sm, rho_ss) p_ss_analytic = np.exp(-1.0 / wth_vec) / (1 + np.exp(-1.0 / wth_vec)) np.testing.assert_allclose(p_ss_analytic, p_ss, atol=1e-5) @@ -92,22 +83,20 @@ def test_exact_solution_for_simple_methods(method, kwargs): @pytest.mark.parametrize(['method', 'kwargs'], [ pytest.param('direct', {}, id="direct"), pytest.param('direct', {'sparse':False}, id="direct_dense"), - pytest.param('eigen', {}, id="eigen"), - pytest.param('power', {'mtol':1e-5}, id="power"), - pytest.param('power-gmres', {'mtol':1e-1, 'use_precond':1}, id="power-gmres"), - pytest.param('power-bicgstab', {'use_precond':1}, id="power-bicgstab"), - pytest.param('iterative-lgmres', {'use_precond':1}, id="iterative-lgmres"), - pytest.param('iterative-gmres', {}, id="iterative-gmres"), - pytest.param('iterative-bicgstab', {}, id="iterative-bicgstab"), + pytest.param('eigen', {'sparse':False}, id="eigen"), + pytest.param('power', {'power_tol':1e-5}, id="power"), + pytest.param('iterative-lgmres', {'tol': 1e-7, 'atol': 1e-7}, id="iterative-lgmres"), + pytest.param('iterative-gmres', {'tol': 1e-7, 'atol': 1e-7}, id="iterative-gmres"), + pytest.param('iterative-bicgstab', {'tol': 1e-7, 'atol': 1e-7}, id="iterative-bicgstab"), ]) def test_ho(method, kwargs): # thermal steadystate of an oscillator: compare numerics with analytical # formula - a = qutip.destroy(35) + a = qutip.destroy(30) H = 0.5 * 2 * np.pi * a.dag() * a gamma1 = 0.05 - wth_vec = np.linspace(0.1, 3, 20) + wth_vec = np.linspace(0.1, 3, 11) p_ss = np.zeros(np.shape(wth_vec)) for idx, wth in enumerate(wth_vec): @@ -122,7 +111,7 @@ def test_ho(method, kwargs): p_ss[idx] = np.real(qutip.expect(a.dag() * a, rho_ss)) p_ss_analytic = 1.0 / (np.exp(1.0 / wth_vec) - 1) - np.testing.assert_allclose(p_ss_analytic, p_ss, atol=1e-3) + np.testing.assert_allclose(p_ss_analytic, p_ss, rtol=1e-3, atol=1e-3) @pytest.mark.parametrize(['method', 'kwargs'], [ @@ -130,13 +119,13 @@ def test_ho(method, kwargs): pytest.param('direct', {'sparse':False}, id="direct_dense"), pytest.param('eigen', {}, id="eigen"), pytest.param('svd', {}, id="svd"), - pytest.param('power', {'mtol':1e-5}, id="power"), - pytest.param('power-gmres', {'mtol':1e-1, 'use_precond':1, 'M':'iterative'}, + pytest.param('power', {}, id="power"), + pytest.param('power-gmres', {"atol": 1e-5, 'tol':1e-1, 'use_precond':1}, id="power-gmres"), - pytest.param('power-bicgstab', {'use_precond':1, 'M':'power'}, + pytest.param('power', {"solver": "bicgstab", "atol": 1e-6, "tol": 1e-6, 'use_precond':1}, id="power-bicgstab"), - pytest.param('iterative-gmres', {}, id="iterative-gmres"), - pytest.param('iterative-bicgstab', {}, id="iterative-bicgstab"), + pytest.param('iterative-gmres', {"atol": 1e-10, "tol": 1e-10}, id="iterative-gmres"), + pytest.param('iterative-bicgstab', {"atol": 1e-10, "tol": 1e-10}, id="iterative-bicgstab"), ]) def test_driven_cavity(method, kwargs): N = 30 @@ -146,22 +135,18 @@ def test_driven_cavity(method, kwargs): a = qutip.destroy(N) H = Omega * (a.dag() + a) c_ops = [np.sqrt(Gamma) * a] - if 'use_precond' in kwargs: - kwargs['M'] = qutip.build_preconditioner(H, c_ops, method=kwargs['M']) rho_ss = qutip.steadystate(H, c_ops, method=method, **kwargs).full() rho_ss_analytic = qutip.coherent_dm(N, -1.0j * (Omega)/(Gamma/2)).full() np.testing.assert_allclose( rho_ss, rho_ss_analytic, atol=1e-4 ) - assert rho_ss.trace() == pytest.approx(1, abs=1e-11) + assert rho_ss.trace() == pytest.approx(1, abs=1e-10) @pytest.mark.parametrize(['method', 'kwargs'], [ - pytest.param('splu', {'sparse':False}, id="dense_direct"), - pytest.param('numpy', {'sparse':False}, id="dense_numpy"), - pytest.param('scipy', {'sparse':False}, id="dense_scipy"), - pytest.param('splu', {}, id="splu"), + pytest.param('solve', {}, id="dense_direct"), + pytest.param('numpy', {}, id="dense_numpy"), pytest.param('spilu', {}, id="spilu"), ]) def test_pseudo_inverse(method, kwargs): @@ -246,17 +231,6 @@ def test_bad_options_pseudo_inverse(): qutip.pseudo_inverse(L, method='not a method') -def test_bad_options_build_preconditioner(): - N = 4 - a = qutip.destroy(N) - H = (a.dag() + a) - c_ops = [a] - with pytest.raises(TypeError): - qutip.build_preconditioner(H, c_ops, method='power', bad_opt=True) - with pytest.raises(ValueError): - qutip.build_preconditioner(H, c_ops, method='not a method') - - def test_bad_system(): N = 4 a = qutip.destroy(N) diff --git a/qutip/tests/test_fileio.py b/qutip/tests/test_fileio.py index 3d5044fd66..7433760684 100644 --- a/qutip/tests/test_fileio.py +++ b/qutip/tests/test_fileio.py @@ -13,6 +13,7 @@ _dimension = 10 + def _random_file_name(): return "_" + str(uuid.uuid4()) @@ -57,3 +58,5 @@ def test_qsave_qload(use_path, suffix): qutip.qsave(ops_in, filename) ops_out = qutip.qload(filename) assert ops_in == ops_out + # check that the file was saved with the correct name: + assert Path(str(filename) + ".qu").exists() diff --git a/qutip/tests/test_parallel.py b/qutip/tests/test_parallel.py deleted file mode 100644 index 015f1af45c..0000000000 --- a/qutip/tests/test_parallel.py +++ /dev/null @@ -1,62 +0,0 @@ -import numpy as np -import time -from numpy.testing import assert_, run_module_suite - -from qutip.parallel import parfor, parallel_map, serial_map - - -def _func1(x): - time.sleep(np.random.rand() * 0.25) # random delay - return x**2 - - -def _func2(x, a, b, c, d=0, e=0, f=0): - time.sleep(np.random.rand() * 0.25) # random delay - return x**2 - - -def test_parfor1(): - "parfor" - - x = np.arange(10) - y1 = list(map(_func1, x)) - y2 = parfor(_func1, x) - - assert_((np.array(y1) == np.array(y2)).all()) - - -def test_parallel_map(): - "parallel_map" - - args = (1, 2, 3) - kwargs = {'d': 4, 'e': 5, 'f': 6} - - x = np.arange(10) - y1 = list(map(_func1, x)) - y1 = [_func2(xx, *args, **kwargs)for xx in x] - - y2 = parallel_map(_func2, x, args, kwargs, num_cpus=1) - assert_((np.array(y1) == np.array(y2)).all()) - - y2 = parallel_map(_func2, x, args, kwargs, num_cpus=2) - assert_((np.array(y1) == np.array(y2)).all()) - - -def test_serial_map(): - "serial_map" - - args = (1, 2, 3) - kwargs = {'d': 4, 'e': 5, 'f': 6} - - x = np.arange(10) - y1 = list(map(_func1, x)) - y1 = [_func2(xx, *args, **kwargs)for xx in x] - - y2 = serial_map(_func2, x, args, kwargs, num_cpus=1) - assert_((np.array(y1) == np.array(y2)).all()) - - y2 = serial_map(_func2, x, args, kwargs, num_cpus=2) - assert_((np.array(y1) == np.array(y2)).all()) - -if __name__ == "__main__": - run_module_suite() diff --git a/qutip/tests/test_simdiag.py b/qutip/tests/test_simdiag.py index d6bb0e8e64..a45076fd60 100644 --- a/qutip/tests/test_simdiag.py +++ b/qutip/tests/test_simdiag.py @@ -3,6 +3,7 @@ import qutip +@pytest.mark.flaky(reruns=2) @pytest.mark.parametrize('num_mat', [1, 2, 3, 5]) def test_simdiag(num_mat): N = 10 @@ -17,6 +18,7 @@ def test_simdiag(num_mat): assert matrix * evec == evec * eval +@pytest.mark.flaky(reruns=2) @pytest.mark.parametrize('num_mat', [1, 2, 3, 5]) def test_simdiag_no_evals(num_mat): N = 10 @@ -33,6 +35,7 @@ def test_simdiag_no_evals(num_mat): assert matrix * evec == evec * eval +@pytest.mark.flaky(reruns=2) def test_simdiag_degen(): N = 10 U = qutip.rand_unitary(N) @@ -48,11 +51,11 @@ def test_simdiag_degen(): np.testing.assert_allclose( (matrix * evec).full(), (evec * eval).full(), - atol=3e-15 + atol=3e-14 ) @pytest.mark.flaky(reruns=2) -@pytest.mark.repeat(5) +@pytest.mark.repeat(2) def test_simdiag_degen_large(): N = 20 U = qutip.rand_unitary(N) @@ -67,7 +70,7 @@ def test_simdiag_degen_large(): np.testing.assert_allclose( (matrix * evec).full(), (evec * eval).full(), - atol=1e-14 + atol=1e-13 ) diff --git a/qutip/tests/test_transfertensor.py b/qutip/tests/test_transfertensor.py new file mode 100644 index 0000000000..c736ef04ac --- /dev/null +++ b/qutip/tests/test_transfertensor.py @@ -0,0 +1,45 @@ +import pytest +import qutip as qt +import numpy as np +from qutip.solve.nonmarkov.transfertensor import ttmsolve + + +def test_ttmsolve_jc_model(): + """ + Checks the output of ttmsolve using an example from Jaynes-Cumming model, + which can also be found in the qutip-notebooks repository. + """ + # Define Hamiltonian and states + N, kappa, g = 3, 1.0, 10 + a = qt.tensor(qt.qeye(2), qt.destroy(N)) + sm = qt.tensor(qt.sigmam(), qt.qeye(N)) + sz = qt.tensor(qt.sigmaz(), qt.qeye(N)) + H = g * (a.dag() * sm + a * sm.dag()) + c_ops = [np.sqrt(kappa) * a] + # identity superoperator + Id = qt.tensor(qt.qeye(2), qt.qeye(N)) + E0 = qt.sprepost(Id, Id) + # partial trace superoperator + ptracesuper = qt.tensor_contract(E0, (1, N)) + # initial states + rho0a = qt.ket2dm(qt.basis(2, 0)) + psi0c = qt.basis(N, 0) + rho0c = qt.ket2dm(psi0c) + rho0 = qt.tensor(rho0a, rho0c) + superrho0cav = qt.sprepost(qt.tensor(qt.qeye(2), psi0c), + qt.tensor(qt.qeye(2), psi0c.dag())) + + # calculate exact solution using mesolve + times = np.arange(0, 5.0, 0.1) + exactsol = qt.mesolve(H, rho0, times, c_ops, []) + exact_z = qt.expect(sz, exactsol.states) + + # solve using transfer method + learning_times = np.arange(0, 2.0, 0.1) + Et_list = qt.mesolve(H, E0, learning_times, c_ops, []).states + learning_maps = [ptracesuper * (Et * superrho0cav) for Et in Et_list] + ttmsol = ttmsolve(learning_maps, rho0a, times) + ttm_z = qt.expect(qt.sigmaz(), ttmsol.states) + + # check that ttm result and exact solution are close in the learning times + assert np.allclose(ttm_z, exact_z, atol=1e-5) diff --git a/qutip/three_level_atom.py b/qutip/three_level_atom.py index 60d7cc458d..eee506ef56 100644 --- a/qutip/three_level_atom.py +++ b/qutip/three_level_atom.py @@ -56,7 +56,7 @@ def three_level_ops(): Operators for a three level system (qutrit) Returns - -------- + ------- ops : np.array :obj:`numpy.ndarray` of three level operators. """ diff --git a/qutip/visualization.py b/qutip/visualization.py index 979c94dea3..27fe62c60a 100644 --- a/qutip/visualization.py +++ b/qutip/visualization.py @@ -61,7 +61,7 @@ def plot_wigner_sphere(fig, ax, wigner, reflections): If the reflections of the sphere should be plotted as well. Notes - ------ + ----- Special thanks to Russell P Rundle for writing this function. """ ax.set_xlabel("x") @@ -164,8 +164,8 @@ def _cb_labels(left_dims): return [ map(fmt.format, basis_labels) for fmt in ( + r"$\langle{}|$", r"$|{}\rangle$", - r"$\langle{}|$" ) ] @@ -301,7 +301,7 @@ def hinton(rho, xlabels=None, ylabels=None, title=None, ax=None, cmap=None, height, width = W.shape - w_max = 1.25 * max(abs(np.diag(np.array(W)))) + w_max = 1.25 * max(abs(np.array(W)).flatten()) if w_max <= 0.0: w_max = 1.0 @@ -329,8 +329,8 @@ def color_fn(w): _x = x + 1 _y = y + 1 _blob( - _x - 0.5, height - _y + 0.5, W[x, y], w_max, - min(1, abs(W[x, y]) / w_max), color_fn=color_fn, ax=ax) + _x - 0.5, height - _y + 0.5, W[y, x], w_max, + min(1, abs(W[y, x]) / w_max), color_fn=color_fn, ax=ax) # color axis vmax = np.pi if color_style == "phase" else abs(W).max() @@ -969,7 +969,7 @@ def plot_fock_distribution(rho, offset=0, fig=None, ax=None, Parameters ---------- - rho : :class:`qutip.qobj.Qobj` + rho : :class:`qutip.Qobj` The density matrix (or ket) of the state to visualize. fig : a matplotlib Figure instance @@ -1032,7 +1032,7 @@ def plot_wigner(rho, fig=None, ax=None, figsize=(6, 6), Parameters ---------- - rho : :class:`qutip.qobj.Qobj` + rho : :class:`qutip.Qobj` The density matrix (or ket) of the state to visualize. fig : a matplotlib Figure instance @@ -1125,7 +1125,7 @@ def plot_wigner_fock_distribution(rho, fig=None, axes=None, figsize=(8, 4), Parameters ---------- - rho : :class:`qutip.qobj.Qobj` + rho : :class:`qutip.Qobj` The density matrix (or ket) of the state to visualize. fig : a matplotlib Figure instance diff --git a/qutip/wigner.py b/qutip/wigner.py index de725f582f..48abd66385 100644 --- a/qutip/wigner.py +++ b/qutip/wigner.py @@ -16,7 +16,7 @@ import qutip from qutip import Qobj, ket2dm, jmat -from .parallel import parfor +from .solver.parallel import parallel_map from .utilities import clebsch from .core import data as _data from .core.data.eigen import eigh @@ -322,7 +322,7 @@ def _wigner_laguerre(rho, xvec, yvec, g, parallel): if parallel: iterator = ( (m, rho, A, B) for m in range(len(rho.data.indptr) - 1)) - W1_out = parfor(_par_wig_eval, iterator) + W1_out = parallel_map(_par_wig_eval, iterator) W += sum(W1_out) else: for m in range(len(rho.data.indptr) - 1): @@ -806,7 +806,7 @@ def qfunc( that is used for single kets, set ``precompute_memory=None``. Returns - -------- + ------- ndarray Values representing the Husimi-Q function calculated over the specified range ``[xvec, yvec]``. diff --git a/setup.py b/setup.py index a88cfa0e2a..80e80c4157 100755 --- a/setup.py +++ b/setup.py @@ -3,7 +3,6 @@ import collections import os import pathlib -import re import subprocess import sys import sysconfig @@ -80,6 +79,13 @@ def _determine_user_arguments(options): """ Add the 'openmp' option to the collection, based on the passed command-line arguments or environment variables. + + If using PEP517 builds, one can pass these options on the command-line + using, for example: + + python -m build \ + --wheel \ + --config-setting="--global-option=--with-openmp" """ # options = _parse_bool_user_argument(options, 'openmp') options['openmp'] = False # Not supported yet @@ -141,9 +147,7 @@ def _determine_version(options): version_filename = os.path.join(options['rootdir'], 'VERSION') with open(version_filename, "r") as version_file: version_string = version_file.read().strip() - version = packaging.version.parse(version_string) - if isinstance(version, packaging.version.LegacyVersion): - raise ValueError("invalid version: " + version_string) + version = packaging.version.Version(version_string) options['short_version'] = str(version.public) options['release'] = not version.is_devrelease if not options['release']: