diff --git a/.buildkite/CUDA_Ext.yml b/.buildkite/CUDA_Ext.yml new file mode 100644 index 00000000..b5035f9f --- /dev/null +++ b/.buildkite/CUDA_Ext.yml @@ -0,0 +1,33 @@ +steps: + - label: "CUDA Julia {{matrix.version}}" + matrix: + setup: + version: + - "1.10" # oldest + #- "1" # latest + plugins: + - JuliaCI/julia#v1: + version: "{{matrix.version}}" + - JuliaCI/julia-test#v1: + test_args: "--quickfail" + - JuliaCI/julia-coverage#v1: + codecov: true + dirs: + - src + - ext + agents: + queue: "juliagpu" + cuda: "*" + env: + GROUP: "CUDA_Ext" + SECRET_CODECOV_TOKEN: "ZfhQu/IcRLqNyZ//ZNs5sjBPaV76IHfU5gui52Qn+Rp8tOurukqgScuyDt+3HQ4R0hJYBw1/Nqg6jmBsvWSc9NEUx8kGsUJFHfN3no0+b+PFxA8oJkWc9EpyIsjht5ZIjlsFWR3f0DpPqMEle/QyWOPcal63CChXR8oAoR+Fz1Bh8GkokLlnC8F9Ugp9xBlu401GCbyZhvLTZnNIgK5yy9q8HBJnBg1cPOhI81J6JvYpEmcIofEzFV/qkfpTUPclu43WNoFX2DZPzbxilf3fsAd5/+nRkRfkNML8KiN4mnmjHxPPbuY8F5zC/PS5ybXtDpfvaMQc01WApXCkZk0ZAQ==;U2FsdGVkX1+eDT7dqCME5+Ox5i8GvWRTQbwiP/VYjapThDbxXFDeSSIC6Opmon+M8go22Bun3bat6Fzie65ang==" + timeout_in_minutes: 60 + if: | + // Don't run Buildkite if the commit message includes the text [skip ci], [ci skip], or [no ci] + // Don't run Buildkite for PR draft + // Only run Buildkite when new commits and PR are made to main branch + build.message !~ /\[skip ci\]/ && + build.message !~ /\[ci skip\]/ && + build.message !~ /\[no ci\]/ && + !build.pull_request.draft && + (build.branch =~ /main/ || build.pull_request.base_branch =~ /main/) diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index 4c4a23cd..83595dff 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -1,25 +1,16 @@ +# see: https://github.com/staticfloat/forerunner-buildkite-plugin steps: - - label: "QuantumToolboxCUDAExt" + - label: ":runner: Dynamically launch pipelines" plugins: - - JuliaCI/julia#v1: - version: "1" - - JuliaCI/julia-test#v1: - test_args: "--quickfail" - - JuliaCI/julia-coverage#v1: - dirs: - - src - - ext + - staticfloat/forerunner: # CUDA.jl tests + watch: + - ".buildkite/pipeline.yml" + - ".buildkite/CUDA_Ext.yml" + - "src/**" + - "ext/QuantumToolboxCUDAExt.jl" + - "test/runtests.jl" + - "test/ext-test/cuda_ext.jl" + - "Project.toml" + target: ".buildkite/CUDA_Ext.yml" agents: queue: "juliagpu" - cuda: "*" - env: - GROUP: "CUDA_Ext" - SECRET_CODECOV_TOKEN: # "encrypted codecov token" - timeout_in_minutes: 60 - if: | - // Don't run Buildkite if the commit message includes the text [skip tests] - // Don't run Buildkite for PR draft - // Only run Buildkite when new commits and PR are made to main branch - build.message !~ /\[skip tests\]/ && - !build.pull_request.draft && - (build.branch =~ /main/ || build.pull_request.base_branch =~ /main/) \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 5c60318e..ef904506 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -37,7 +37,7 @@ body: attributes: label: Your Environment description: Please use `QuantumToolbox.about()` or `QuantumToolbox.versioninfo()` to get the information about your environment and paste it here (automatically formatted) - placeholder: "QuantumToolbox Ver. ***\nLinearSolve Ver. ***\nOrdinaryDiffEq Ver. ***\nJulia Version: ***\nOS : ***\nWORD_SIZE: ***\nLIBM : ***\nLLVM : ***\nBLAS : ***" + placeholder: "Julia Ver. ***\nQuantumToolbox Ver. ***\nLinearSolve Ver. ***\nOrdinaryDiffEqCore Ver. ***\nOS : ***\nWORD_SIZE: ***\nLIBM : ***\nLLVM : ***\nBLAS : ***" render: shell validations: required: true diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index f486924a..dbb5b259 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -3,9 +3,9 @@ Thank you for contributing to `QuantumToolbox.jl`! Please make sure you have fin - [ ] Please read [Contributor Covenant Code of Conduct](https://github.com/qutip/QuantumToolbox.jl/blob/main/CODE_OF_CONDUCT.md) - [ ] Any code changes were done in a way that does not break public API -- [ ] Appropriate tests were added. -- [ ] Any code changes should be formatted by running: `julia -e 'using JuliaFormatter; format(".")'` -- [ ] All documentation (in `docs/` folder) related to code changes were updated. +- [ ] Appropriate tests were added and tested locally by running: `make test`. +- [ ] Any code changes should be `julia` formatted by running: `make format`. +- [ ] All documents (in `docs/` folder) related to code changes were updated and able to build locally by running `make docs`. Request for a review 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. @@ -13,6 +13,6 @@ Request for a review after you have completed all the tasks. If you have not fin Describe the proposed change here. ## Related issues or PRs -Please mention the related issues or PRs here. If the PR fixes an issue, use the keyword close/closes/closed/fix/fixes/fixed/resolve/resolves/resolved followed by the issue id, e.g. fix #1234 +Please mention the related issues or PRs here. If the PR fixes an issue, use the keyword close/closes/closed/fix/fixes/fixed/resolve/resolves/resolved followed by the issue id, e.g. fix #[id] -## Additional context \ No newline at end of file +## Additional context diff --git a/.github/workflows/CI-Julia-nightly.yml b/.github/workflows/CI-Julia-nightly.yml new file mode 100644 index 00000000..389e2d3f --- /dev/null +++ b/.github/workflows/CI-Julia-nightly.yml @@ -0,0 +1,61 @@ +name: Runtests (Julia nightly) + +on: + push: + branches: + - 'main' + paths: + - '.github/workflows/CI-Julia-nightly.yml' + - 'src/**' + - 'ext/**' + - 'test/runtests.jl' + - 'test/core-test/**' + - 'Project.toml' + pull_request: + branches: + - 'main' + paths: + - '.github/workflows/CI-Julia-nightly.yml' + - 'src/**' + - 'ext/**' + - 'test/runtests.jl' + - 'test/core-test/**' + - 'Project.toml' + types: + - opened + - reopened + - synchronize + - ready_for_review + +jobs: + test: + name: ${{ matrix.os }} - ${{ matrix.arch }} ( ${{ matrix.group }} ) + runs-on: ${{ matrix.os }} + permissions: # needed to allow julia-actions/cache to delete old caches that it has created + actions: write + contents: read + if: ${{ !github.event.pull_request.draft }} + strategy: + fail-fast: false + matrix: + version: + - 'nightly' + os: + - 'ubuntu-latest' + arch: + - 'x64' + group: + - 'Core' + - 'Code-Quality' + + steps: + - uses: actions/checkout@v4 + - uses: julia-actions/setup-julia@v2 + with: + version: ${{ matrix.version }} + arch: ${{ matrix.arch }} + - uses: julia-actions/cache@v2 + - uses: julia-actions/julia-buildpkg@v1 + - uses: julia-actions/julia-runtest@v1 + env: + GROUP: ${{ matrix.group }} diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 6f139128..f1ebe096 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -8,7 +8,8 @@ on: - '.github/workflows/CI.yml' - 'src/**' - 'ext/**' - - 'test/**' + - 'test/runtests.jl' + - 'test/core-test/**' - 'Project.toml' pull_request: branches: @@ -17,7 +18,8 @@ on: - '.github/workflows/CI.yml' - 'src/**' - 'ext/**' - - 'test/**' + - 'test/runtests.jl' + - 'test/core-test/**' - 'Project.toml' types: - opened @@ -27,8 +29,8 @@ on: jobs: test: - name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} ( ${{ matrix.group }} ) - runs-on: ${{ matrix.os }} + name: Julia ${{ matrix.version }} - ${{ matrix.node.os }} - ${{ matrix.node.arch }} ( ${{ matrix.group }} ) + runs-on: ${{ matrix.node.os }} permissions: # needed to allow julia-actions/cache to delete old caches that it has created actions: write contents: read @@ -38,49 +40,39 @@ jobs: matrix: # for core tests (latest and oldest supported versions) version: - - '1.7' # oldest - - '1' # latest - os: - - ubuntu-latest - - windows-latest - arch: - - x64 - group: - - Core - - include: - # for core tests on macOS - - version: '1.7' # oldest (julia v1.7 does not support M-series chip) - os: 'macOS-13' # Intel chip - arch: 'x64' - group: 'Core' - - version: '1' # latest - os: 'macOS-latest' # M-series chip - arch: 'arm64' - group: 'Core' - - # for core tests (intermediate versions) - - version: '1.8' - os: 'ubuntu-latest' + - '1.10' # oldest + # - '1' # latest + node: + - os: 'ubuntu-latest' arch: 'x64' - group: 'Core' - - version: '1.9' - os: 'ubuntu-latest' + - os: 'windows-latest' arch: 'x64' - group: 'Core' + - os: 'macOS-latest' + arch: 'arm64' + group: + - 'Core' + include: # for code quality tests - version: '1' - os: 'ubuntu-latest' - arch: 'x64' + node: + os: 'ubuntu-latest' + arch: 'x64' group: 'Code-Quality' + # for core tests (intermediate versions) + # - version: '1.x' + # node: + # os: 'ubuntu-latest' + # arch: 'x64' + # group: 'Core' + steps: - uses: actions/checkout@v4 - uses: julia-actions/setup-julia@v2 with: version: ${{ matrix.version }} - arch: ${{ matrix.arch }} + arch: ${{ matrix.node.arch }} - uses: julia-actions/cache@v2 - uses: julia-actions/julia-buildpkg@v1 - uses: julia-actions/julia-runtest@v1 diff --git a/.github/workflows/FormatCheck.yml b/.github/workflows/FormatCheck.yml index 2b46352d..dda62fa1 100644 --- a/.github/workflows/FormatCheck.yml +++ b/.github/workflows/FormatCheck.yml @@ -45,6 +45,6 @@ jobs: write(stdout, output) write(stdout, "-----\n") write(stdout, "Please format them by running the following command:\n") - write(stdout, "julia -e \"using JuliaFormatter; format(\\\".\\\")\"") + write(stdout, "make format") exit(1) end' \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..1da02514 --- /dev/null +++ b/Makefile @@ -0,0 +1,24 @@ +JULIA:=julia + +default: help + +docs: + ${JULIA} --project=docs -e 'using Pkg; Pkg.develop(PackageSpec(path=pwd())); Pkg.instantiate()' + ${JULIA} --project=docs docs/make.jl + +format: + ${JULIA} -e 'using JuliaFormatter; format(".")' + +test: + ${JULIA} --project -e 'using Pkg; Pkg.resolve(); Pkg.test()' + +all: format test docs + +help: + @echo "The following make commands are available:" + @echo " - make docs: instantiate and build the documentation" + @echo " - make format: format codes with JuliaFormatter" + @echo " - make test: run the tests" + @echo " - make all: run every commands above" + +.PHONY: default docs format test all help \ No newline at end of file diff --git a/Project.toml b/Project.toml index 7b33dab5..21de7a24 100644 --- a/Project.toml +++ b/Project.toml @@ -1,23 +1,29 @@ name = "QuantumToolbox" uuid = "6c2fb7c5-b903-41d2-bc5e-5a7c320b9fab" authors = ["Alberto Mercurio", "Luca Gravina", "Yi-Te Huang"] -version = "0.12.0" +version = "0.14.1" [deps] ArrayInterface = "4fba245c-0d91-5ea0-9b3e-6abc04ee57a9" +DiffEqBase = "2b5f629d-d688-5b77-993f-72d75c75574e" DiffEqCallbacks = "459566f4-90b8-5000-8ac3-15dfb0a30def" +DiffEqNoiseProcess = "77a26b50-5914-5dd7-bc55-306e6241c503" FFTW = "7a1cc6ca-52ef-59f5-83cd-3a7055c09341" Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6" IncompleteLU = "40713840-3770-5561-ab4c-a76e7d0d7895" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" -LinearMaps = "7a12625a-238d-50fd-b39a-03d52299707e" LinearSolve = "7ed4a6bd-45f5-4d41-b270-4a48e9bafcae" -OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed" +OrdinaryDiffEqCore = "bbf590c4-e513-4bbe-9b18-05decba2e5d8" +OrdinaryDiffEqTsit5 = "b1df2697-797e-41e3-8120-5422d3b24e4a" Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" Reexport = "189a3867-3050-52da-a836-e630ba90ab69" +SciMLBase = "0bca4576-84f4-4d90-8ffe-ffa030f20462" +SciMLOperators = "c0aeaf25-5076-4817-a8d5-81caf7dfa961" SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" SpecialFunctions = "276daf66-3868-5448-9aa4-cd146d93841b" +StaticArraysCore = "1e83bf80-4336-4d27-bf5d-d5a4f845583c" +StochasticDiffEq = "789caeaf-c7a9-5a7d-9973-96adeb23e2a0" [weakdeps] CUDA = "052768ef-5323-5732-b1bb-66c8b64840ba" @@ -28,21 +34,27 @@ QuantumToolboxCUDAExt = "CUDA" [compat] ArrayInterface = "6, 7" CUDA = "5" +DiffEqBase = "6" DiffEqCallbacks = "2 - 3.1, 3.8" +DiffEqNoiseProcess = "5" FFTW = "1.5" Graphs = "1.7" IncompleteLU = "0.2" LinearAlgebra = "<0.0.1, 1" -LinearMaps = "3" LinearSolve = "2" -OrdinaryDiffEq = "6" +OrdinaryDiffEqCore = "1" +OrdinaryDiffEqTsit5 = "1" Pkg = "<0.0.1, 1" Random = "<0.0.1, 1" Reexport = "1" +SciMLBase = "2" +SciMLOperators = "0.3" SparseArrays = "<0.0.1, 1" SpecialFunctions = "2" +StochasticDiffEq = "6" +StaticArraysCore = "1" Test = "<0.0.1, 1" -julia = "1.7" +julia = "1.10" [extras] CUDA = "052768ef-5323-5732-b1bb-66c8b64840ba" diff --git a/README.md b/README.md index 14ad5d7d..cd4219aa 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ and [Y.-T. Huang](https://github.com/ytdHuang). | **Runtests** | [![Runtests][runtests-img]][runtests-url] [![Coverage][codecov-img]][codecov-url] [![Aqua QA][aqua-img]][aqua-url] [![JET][jet-img]][jet-url] | | **Documentation** | [![Doc-Stable][docs-stable-img]][docs-stable-url] [![Doc-Dev][docs-develop-img]][docs-develop-url] | | **Benchmark** | [![Benchmarks][benchmark-img]][benchmark-url] | +| **Support** | [![Unitary Fund](https://img.shields.io/badge/Supported%20By-UNITARY%20FUND-brightgreen.svg?style=for-the-badge)](https://unitary.fund) | [release-img]: https://img.shields.io/github/release/qutip/QuantumToolbox.jl.svg [release-url]: https://github.com/qutip/QuantumToolbox.jl/releases @@ -67,7 +68,7 @@ QuantumToolbox.jl is equipped with a robust set of features: ## Installation -> **_NOTE:_** `QuantumToolbox.jl` requires `Julia 1.7+`. +> **_NOTE:_** `QuantumToolbox.jl` requires `Julia 1.10+`. To install `QuantumToolbox.jl`, run the following commands inside Julia's interactive session (also known as REPL): ```julia @@ -76,7 +77,7 @@ Pkg.add("QuantumToolbox") ``` Alternatively, this can also be done in Julia's [Pkg REPL](https://julialang.github.io/Pkg.jl/v1/getting-started/) by pressing the key `]` in the REPL to use the package mode, and then type the following command: ```julia-repl -(1.7) pkg> add QuantumToolbox +(1.10) pkg> add QuantumToolbox ``` More information about `Julia`'s package manager can be found at [`Pkg.jl`](https://julialang.github.io/Pkg.jl/v1/). @@ -143,11 +144,10 @@ We can extract the expectation value of the number operator $\hat{a}^\dagger \ha We can easily pass the computation to the GPU, by simply passing all the `Qobj`s to the GPU: -> **_NOTE:_** The described feature requires `Julia 1.9+`. - ```julia using QuantumToolbox using CUDA +CUDA.allowscalar(false) # Avoid unexpected scalar indexing a_gpu = cu(destroy(N)) # The only difference in the code is the cu() function diff --git a/benchmarks/Project.toml b/benchmarks/Project.toml index 32e8b0b0..1aaefa0a 100644 --- a/benchmarks/Project.toml +++ b/benchmarks/Project.toml @@ -1,3 +1,5 @@ [deps] BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" +LinearSolve = "7ed4a6bd-45f5-4d41-b270-4a48e9bafcae" +OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed" QuantumToolbox = "6c2fb7c5-b903-41d2-bc5e-5a7c320b9fab" diff --git a/benchmarks/runbenchmarks.jl b/benchmarks/runbenchmarks.jl index d232a369..e921c81a 100644 --- a/benchmarks/runbenchmarks.jl +++ b/benchmarks/runbenchmarks.jl @@ -1,5 +1,7 @@ using BenchmarkTools using QuantumToolbox +using OrdinaryDiffEq +using LinearSolve BLAS.set_num_threads(1) diff --git a/docs/README.md b/docs/README.md index e7c090ca..9fe1e761 100644 --- a/docs/README.md +++ b/docs/README.md @@ -3,16 +3,25 @@ ## Working Directory All the commands should be run under the root folder of the package: `/path/to/QuantumToolbox.jl/` -## Build Pkg +The document pages will be generated in the directory: `/path/to/QuantumToolbox.jl/docs/build/` (which is ignored by git). + +## Method 1: Run with `make` command +Run the following command: +```shell +make docs +``` + +## Method 2: Run `julia` command manually + +### Build Pkg Run the following command: ```shell julia --project=docs -e 'using Pkg; Pkg.develop(PackageSpec(path=pwd())); Pkg.instantiate()' ``` > **_NOTE:_** `Pkg.develop(PackageSpec(path=pwd()))` adds the local version of `QuantumToolbox` as dev-dependency instead of pulling from the registered version. -## Build Documentation +### Build Documentation Run the following command: ```shell julia --project=docs docs/make.jl ``` -The document pages will be generated in the directory: `/path/to/QuantumToolbox.jl/docs/build/` (which is ignored by git). \ No newline at end of file diff --git a/docs/make.jl b/docs/make.jl index 01d5a01d..9255bca5 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -21,6 +21,7 @@ const PAGES = [ "Getting Started" => [ "Introduction" => "index.md", "Key differences from QuTiP" => "qutip_differences.md", + "The Importance of Type-Stability" => "type_stability.md", # "Cite QuantumToolbox.jl" => "cite.md", ], "Users Guide" => [ diff --git a/docs/src/api.md b/docs/src/api.md index fb66c49a..a48a7f08 100644 --- a/docs/src/api.md +++ b/docs/src/api.md @@ -179,11 +179,14 @@ sesolveProblem mesolveProblem lr_mesolveProblem mcsolveProblem +ssesolveProblem mcsolveEnsembleProblem +ssesolveEnsembleProblem sesolve mesolve lr_mesolve mcsolve +ssesolve dfd_mesolve dsf_mesolve dsf_mcsolve @@ -219,6 +222,12 @@ wigner negativity ``` +## [Linear Maps](@id doc-API:Linear-Maps) + +```@docs +AbstractLinearMap +``` + ## [Utility functions](@id doc-API:Utility-functions) ```@docs diff --git a/docs/src/index.md b/docs/src/index.md index bc71957f..4d1642c6 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -21,7 +21,7 @@ QuantumToolbox.jl is equipped with a robust set of features: ## [Installation](@id doc:Installation) !!! note "Requirements" - `QuantumToolbox.jl` requires `Julia 1.7+`. + `QuantumToolbox.jl` requires `Julia 1.10+`. To install `QuantumToolbox.jl`, run the following commands inside Julia's interactive session (also known as REPL): ```julia @@ -30,7 +30,7 @@ Pkg.add("QuantumToolbox") ``` Alternatively, this can also be done in Julia's [Pkg REPL](https://julialang.github.io/Pkg.jl/v1/getting-started/) by pressing the key `]` in the REPL to use the package mode, and then type the following command: ```julia-REPL -(1.7) pkg> add QuantumToolbox +(1.10) pkg> add QuantumToolbox ``` More information about `Julia`'s package manager can be found at [`Pkg.jl`](https://julialang.github.io/Pkg.jl/v1/). @@ -97,12 +97,10 @@ We can extract the expectation value of the number operator ``\hat{a}^\dagger \h We can easily pass the computation to the GPU, by simply passing all the `Qobj`s to the GPU: -!!! compat "Compat" - The described feature requires `Julia 1.9+`. See [CUDA extension](@ref doc:CUDA) for more details. - ```julia using QuantumToolbox using CUDA +CUDA.allowscalar(false) # Avoid unexpected scalar indexing a_gpu = cu(destroy(N)) # The only difference in the code is the cu() function diff --git a/docs/src/tutorials/lowrank.md b/docs/src/tutorials/lowrank.md index a04bf3e8..42a100bd 100644 --- a/docs/src/tutorials/lowrank.md +++ b/docs/src/tutorials/lowrank.md @@ -130,7 +130,7 @@ Define the options for the low-rank evolution ```@example lowrank opt = - LRMesolveOptions(alg = Tsit5(), err_max = 1e-3, p0 = 0.0, atol_inv = 1e-6, adj_condition = "variational", Δt = 0.0); + LRMesolveOptions(err_max = 1e-3, p0 = 0.0, atol_inv = 1e-6, adj_condition = "variational", Δt = 0.0); @time lrsol = lr_mesolve(H, z, B, tl, c_ops; e_ops = e_ops, f_ops = (f_purity, f_entropy, f_trace), opt = opt); ``` diff --git a/docs/src/type_stability.md b/docs/src/type_stability.md new file mode 100644 index 00000000..c3cd0e12 --- /dev/null +++ b/docs/src/type_stability.md @@ -0,0 +1,3 @@ +# [The Importance of Type-Stability](@id doc:Type-Stability) + +This page is still under construction. diff --git a/docs/src/users_guide/QuantumObject/QuantumObject.md b/docs/src/users_guide/QuantumObject/QuantumObject.md index 2086fa4d..01a86914 100644 --- a/docs/src/users_guide/QuantumObject/QuantumObject.md +++ b/docs/src/users_guide/QuantumObject/QuantumObject.md @@ -1,3 +1,295 @@ # [Quantum Objects (Qobj)](@id doc:Qobj) -This page is still under construction, please visit [API](@ref doc-API) first. \ No newline at end of file +## Introduction +The key difference between classical and quantum mechanics is the use of operators instead of numbers as variables. Moreover, we need to specify state vectors and their properties. Therefore, in computing the dynamics of quantum systems, we need a data structure that encapsulates the properties of a quantum operator and ket/bra vectors. The quantum object structure, [`QuantumObject`](@ref), accomplishes this using array representation. + +[`QuantumObject`](@ref) supports general `Julia` arrays (`Base.AbstractArray`). For example, it can be: +- `Base.Vector` (dense vector) +- `Base.Matrix` (dense matrix) +- `SparseArrays.SparseVector` (sparse vector) +- `SparseArrays.SparseMatrixCSC` (sparse matrix) +- `CUDA.CuArray` (dense GPU vector / matrix) +- `CUDA.CUSPARSE.CuSparseVector` (sparse GPU vector) +- `CUDA.CUSPARSE.CuSparseMatrixCSC` (sparse GPU matrix) +- `CUDA.CUSPARSE.CuSparseMatrixCSR` (sparse GPU matrix) +- and even more ... + +!!! note "Support for GPU arrays" + See [CUDA extension](@ref doc:CUDA) for more details. + +We can create a [`QuantumObject`](@ref) with a user defined data set by passing an array of data into the [`QuantumObject`](@ref): + +```@setup Qobj +using QuantumToolbox +``` + +```@example Qobj +QuantumObject([1, 2, 3, 4, 5]) +``` + +We can also use the same function [`Qobj`](@ref) in [`QuTiP` (`Python`)](https://github.com/qutip/qutip): + +```@example Qobj +Qobj([1, 2, 3, 4, 5]) +``` + +```@example Qobj +Qobj([1 2 3 4 5]) +``` + +```@example Qobj +Qobj(rand(4, 4)) +``` + +```@example Qobj +M = rand(ComplexF64, 4, 4) +Qobj(M, dims = [2, 2]) # dims as Vector +Qobj(M, dims = (2, 2)) # dims as Tuple (recommended) +Qobj(M, dims = SVector(2, 2)) # dims as StaticArrays.SVector (recommended) +``` + +!!! warning "Beware of type-stability!" + Please note that here we put the `dims` as a tuple `(2, 2)`. Although it supports also `Vector` type (`dims = [2, 2]`), it is recommended to use `Tuple` or `SVector` from [`StaticArrays.jl`](https://github.com/JuliaArrays/StaticArrays.jl) to improve performance. For a brief explanation on the impact of the type of `dims`, see the Section [The Importance of Type-Stability](@ref doc:Type-Stability). + +```@example Qobj +Qobj(rand(4, 4), type = SuperOperator) +``` + +!!! note "Difference between `dims` and `size`" + Notice that `type`, `dims`, and `size` will change according to the input `data`. Although `dims` and `size` appear to be the same, `dims` keep tracking the dimension of individual Hilbert spaces of a multipartite system, while `size` does not. We refer the reader to the section [Tensor Products and Partial Traces](@ref doc:Tensor-products-and-Partial-Traces) for more information. + +## 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. Therefore, `QuantumToolbox` includes predefined functions to construct variety of states and operators (you can click the function links and see the corresponding docstring): + +### States +- [`zero_ket`](@ref): zero ket vector +- [`fock`](@ref) or [`basis`](@ref): fock state ket vector +- [`fock_dm`](@ref): density matrix of a fock state +- [`coherent`](@ref): coherent state ket vector +- [`rand_ket`](@ref): random ket vector +- [`coherent_dm`](@ref): density matrix of a coherent state +- [`thermal_dm`](@ref): density matrix of a thermal state +- [`maximally_mixed_dm`](@ref): density matrix of a maximally mixed state +- [`rand_dm`](@ref): random density matrix +- [`spin_state`](@ref): spin state +- [`spin_coherent`](@ref): coherent spin state +- [`bell_state`](@ref): Bell state +- [`singlet_state`](@ref): two particle singlet state +- [`triplet_states`](@ref): list of the two particle triplet states +- [`w_state`](@ref): `n`-qubit W-state +- [`ghz_state`](@ref): `n`-qudit GHZ state + +### Operators +- [`eye`](@ref) or [`qeye`](@ref): identity operator +- [`destroy`](@ref): lowering (destruction) operator +- [`create`](@ref): raising (creation) operator +- [`projection`](@ref): projection operator +- [`displace`](@ref): displacement operator +- [`squeeze`](@ref): single-mode squeeze operator +- [`num`](@ref): bosonic number operator +- [`phase`](@ref): single-mode Pegg-Barnett phase operator +- [`QuantumToolbox.position`](@ref): position operator +- [`QuantumToolbox.momentum`](@ref): momentum operator +- [`rand_unitary`](@ref): random unitary operator +- [`sigmax`](@ref): Pauli-``X`` operator +- [`sigmay`](@ref): Pauli-``Y`` operator +- [`sigmaz`](@ref): Pauli-``Z`` operator +- [`sigmap`](@ref): Pauli ladder (``\sigma_+``) operator +- [`sigmam`](@ref): Pauli ladder (``\sigma_-``) operator +- [`jmat`](@ref): high-order Spin-`j` operator +- [`spin_Jx`](@ref): ``S_x`` operator for Spin-`j` +- [`spin_Jy`](@ref): ``S_y`` operator for Spin-`j` +- [`spin_Jz`](@ref): ``S_z`` operator for Spin-`j` +- [`spin_Jm`](@ref): ``S_-`` operator for Spin-`j` +- [`spin_Jp`](@ref): ``S_+`` operator for Spin-`j` +- [`spin_J_set`](@ref): a set of Spin-`j` operators ``(S_x, S_y, S_z)`` +- [`fdestroy`](@ref): fermion destruction operator +- [`fcreate`](@ref): fermion creation operator +- [`commutator`](@ref): commutator or anti-commutator +- [`tunneling`](@ref): tunneling operator +- [`qft`](@ref): discrete quantum Fourier transform matrix + +As an example, we give the output for a few of these functions: + +```@example Qobj +basis(5, 3) +``` + +```@example Qobj +coherent(5, 0.5 - 0.5im) +``` + +```@example Qobj +destroy(4) +``` + +```@example Qobj +sigmaz() +``` + +## Qobj fields (attributes) + +We have seen that a structure [`QuantumObject`](@ref) has several fields (attributes), such as `data`, `type` and `dims`. These can be accessed in the following way: + +```@example Qobj +a = destroy(4) +a.data +``` + +```@example Qobj +a[2, 3] # the indexing in Julia starts from `1` +``` + +```@example Qobj +a.type +``` + +```@example Qobj +a.dims +``` + +In general, the properties of a [`QuantumObject`](@ref) can be retrieved using several functions with inputting [`QuantumObject`](@ref): + +```@example Qobj +size(a) +``` + +```@example Qobj +shape(a) # synonym of size(a) +``` + +```@example Qobj +length(a) +``` + +```@example Qobj +eltype(a) # element type +``` + +```@example Qobj +println(isket(a)) # ket +println(isbra(a)) # bra +println(isoper(a)) # operator +println(isoperket(a)) # operator-ket +println(isoperbra(a)) # operator-bra +println(issuper(a)) # super operator +println(ishermitian(a)) # Hermitian +println(isherm(a)) # synonym of ishermitian(a) +println(issymmetric(a)) # symmetric +println(isposdef(a)) # positive definite (and Hermitian) +println(isunitary(a)) # unitary +``` + +### `data` conversions + +As we mentioned above, `QuantumObject.data` supports general `Julia` arrays. The conversion between different type of `QuantumObject.data` is done using the standard type-conversion for arrays in `Julia`: + +```@example Qobj +v_d = basis(2, 0) +``` + +```@example Qobj +Vector{Int64}(v_d) +``` + +```@example Qobj +v_s = SparseVector(v_d) +``` + +```@example Qobj +SparseVector{Float64}(v_s) +``` + +```@example Qobj +x_s = sigmax() +``` + +```@example Qobj +SparseMatrixCSC{Int64}(x_s) +``` + +```@example Qobj +Matrix{Float64}(x_s) +``` + +To convert between dense and sparse arrays, one can also use [`dense_to_sparse`](@ref) and [`sparse_to_dense`](@ref): + +```@example Qobj +x_d = sparse_to_dense(x_s) +``` + +```@example Qobj +dense_to_sparse(x_d) +``` + +!!! note "Convert to GPU arrays" + See [CUDA extension](@ref doc:CUDA) for more details. + +`QuantumToolbox` will do conversion when needed to keep everything working in any format. However these conversions could slow down computation and it is recommended to keep to one format family where possible. + +## Qobj math + +The rules for mathematical operations on [`QuantumObject`](@ref) are similar to the standard scalar, vector, and matrix arithmetic: + +```@example Qobj +a = destroy(4) +``` + +```@example Qobj +a' # synonym of adjoint(a) +``` + +```@example Qobj +a + 5 +``` + +```@example Qobj +a' * a +``` + +```@example Qobj +a ^ 3 +``` + +```@example Qobj +x = sigmax() +``` + +```@example Qobj +x / sqrt(2) +``` + +```@example Qobj +x ^ 3 == x +``` + +```@example Qobj +# type \approx + to get symbol ≈ +x ^ 3 ≈ x +``` + +Of course, like matrices, multiplying two objects of incompatible `dims` or `size` throws an error: +```@repl Qobj +a * x +``` + +Note that there is a special case for multiplying [`Ket`](@ref) and [`Bra`](@ref), which results in outer product ``|u\rangle \otimes \langle v|``: + +```@example Qobj +u = Qobj([1, 2, 3]) +``` + +```@example Qobj +v = Qobj([4, 5, 6]) +v' +``` + +```@example Qobj +u * v' +``` + +Of course, if you switch the order of multiplication, it becomes inner product ``\langle v | u \rangle``: +```@example Qobj +v' * u +``` diff --git a/docs/src/users_guide/QuantumObject/QuantumObject_functions.md b/docs/src/users_guide/QuantumObject/QuantumObject_functions.md index 00d16300..698017dc 100644 --- a/docs/src/users_guide/QuantumObject/QuantumObject_functions.md +++ b/docs/src/users_guide/QuantumObject/QuantumObject_functions.md @@ -1,3 +1,107 @@ # [Functions operating on Qobj](@id doc:Functions-operating-on-Qobj) -This page is still under construction, please visit [API](@ref doc-API) first. \ No newline at end of file +`QuantumToolbox` also provide functions (methods) that operates on [`QuantumObject`](@ref). + +You can click the function links and see the corresponding docstring for more information. + +## Linear algebra and attributes + +Here is a table that summarizes all the supported linear algebra functions and attribute functions operating on a given [`QuantumObject`](@ref) `Q`: + +| **Description** | **Function call** | **Synonyms** | +|:----------------|:------------------|:-------------| +| conjugate | [`conj(Q)`](@ref conj) | - | +| transpose | [`transpose(Q)`](@ref transpose) | [`trans(Q)`](@ref trans) | +| conjugate transposition | [`adjoint(Q)`](@ref adjoint) | [`Q'`](@ref adjoint), [`dag(Q)`](@ref dag) | +| partial transpose | [`partial_transpose(Q, mask)`](@ref partial_transpose) | - | +| dot product | [`dot(Q1, Q2)`](@ref dot) | - | +| generalized dot product | [`dot(Q1, Q2, Q3)`](@ref dot) | [`matrix_element(Q1, Q2, Q3)`](@ref matrix_element) | +| trace | [`tr(Q)`](@ref tr) | - | +| partial trace | [`ptrace(Q, sel)`](@ref ptrace) | - | +| singular values | [`svdvals(Q)`](@ref svdvals) | - | +| standard vector `p`-norm or [Schatten](https://en.wikipedia.org/wiki/Schatten_norm) `p`-norm | [`norm(Q, p)`](@ref norm) | - | +| normalization | [`normalize(Q, p)`](@ref normalize) | [`unit(Q, p)`](@ref unit) | +| normalization (in-place) | [`normalize!(Q, p)`](@ref normalize!) | - | +| matrix inverse | [`inv(Q)`](@ref inv) | - | +| matrix square root | [`sqrt(Q)`](@ref sqrt) | [`√(Q)`](@ref sqrt), [`sqrtm(Q)`](@ref sqrtm) | +| matrix logarithm | [`log(Q)`](@ref log) | [`logm(Q)`](@ref logm) | +| matrix exponential | [`exp(Q)`](@ref exp) | [`expm(Q)`](@ref expm) | +| matrix sine | [`sin(Q)`](@ref sin) | [`sinm(Q)`](@ref sinm) | +| matrix cosine | [`cos(Q)`](@ref cos) | [`cosm(Q)`](@ref cosm) | +| diagonal elements | [`diag(Q)`](@ref diag) | - | +| projector | [`proj(Q)`](@ref proj) | - | +| purity | [`purity(Q)`](@ref purity) | - | +| permute | [`permute(Q, order)`](@ref permute) | - | +| remove small elements | [`tidyup(Q, tol)`](@ref tidyup) | - | +| remove small elements (in-place) | [`tidyup!(Q, tol)`](@ref tidyup!) | - | +| get data | [`get_data(Q)`](@ref get_data) | - | +| get coherence | [`get_coherence(Q)`](@ref get_coherence) | - | + +## Eigenvalue decomposition + +- [`eigenenergies`](@ref): return eigenenergies (eigenvalues) +- [`eigenstates`](@ref): return [`EigsolveResult`](@ref) (contains eigenvalues and eigenvectors) +- [`eigvals`](@ref): return eigenvalues +- [`eigen`](@ref): using dense eigen solver and return [`EigsolveResult`](@ref) (contains eigenvalues and eigenvectors) +- [`eigsolve`](@ref): using sparse eigen solver and return [`EigsolveResult`](@ref) (contains eigenvalues and eigenvectors) +- [`eigsolve_al`](@ref): using the Arnoldi-Lindblad eigen solver and return [`EigsolveResult`](@ref) (contains eigenvalues and eigenvectors) + +## Examples + +```@setup Qobj_Function +using QuantumToolbox +``` + +```@example Qobj_Function +ψ = normalize(basis(4, 1) + basis(4, 2)) +``` + +```@example Qobj_Function +ψ' +``` + +```@example Qobj_Function +ρ = coherent_dm(5, 1) +``` + +```@example Qobj_Function +diag(ρ) +``` + +```@example Qobj_Function +get_data(ρ) +``` + +```@example Qobj_Function +norm(ρ) +``` + +```@example Qobj_Function +sqrtm(ρ) +``` + +```@example Qobj_Function +tr(ρ) +``` + +```@example Qobj_Function +eigenenergies(ρ) +``` + +```@example Qobj_Function +result = eigenstates(ρ) +``` + +```@example Qobj_Function +λ, ψ = result +λ # eigenvalues +``` + +```@example Qobj_Function +ψ # eigenvectors +``` + +```@example Qobj_Function +λ, ψ, T = result +T # transformation matrix +``` \ No newline at end of file diff --git a/docs/src/users_guide/extensions/cuda.md b/docs/src/users_guide/extensions/cuda.md index dea34c45..11290c6f 100644 --- a/docs/src/users_guide/extensions/cuda.md +++ b/docs/src/users_guide/extensions/cuda.md @@ -1,3 +1,163 @@ -# [CUDA.jl](@id doc:CUDA) +# [Extension for CUDA.jl](@id doc:CUDA) -This page is still under construction, please visit [API](@ref doc-API) first. \ No newline at end of file +## Introduction + +This is an extension to support `QuantumObject.data` conversion from standard dense and sparse CPU arrays to GPU ([`CUDA.jl`](https://github.com/JuliaGPU/CUDA.jl)) arrays. + +This extension will be automatically loaded if user imports both `QuantumToolbox` and [`CUDA.jl`](https://github.com/JuliaGPU/CUDA.jl): + +```julia +using QuantumToolbox +using CUDA +using CUDA.CUSPARSE +CUDA.allowscalar(false) # Avoid unexpected scalar indexing +``` + +We wrapped several functions in `CUDA` and `CUDA.CUSPARSE` in order to not only converting `QuantumObject.data` into GPU arrays, but also changing the element type and word size (`32` and `64`) since some of the GPUs perform better in `32`-bit. The functions are listed as follows (where input `A` is a [`QuantumObject`](@ref)): + +- `cu(A; word_size=64)`: return a new [`QuantumObject`](@ref) with `CUDA` arrays and specified `word_size`. +- `CuArray(A)`: If `A.data` is a dense array, return a new [`QuantumObject`](@ref) with `CUDA.CuArray`. +- `CuArray{T}(A)`: If `A.data` is a dense array, return a new [`QuantumObject`](@ref) with `CUDA.CuArray` under element type `T`. +- `CuSparseVector(A)`: If `A.data` is a sparse vector, return a new [`QuantumObject`](@ref) with `CUDA.CUSPARSE.CuSparseVector`. +- `CuSparseVector{T}(A)`: If `A.data` is a sparse vector, return a new [`QuantumObject`](@ref) with `CUDA.CUSPARSE.CuSparseVector` under element type `T`. +- `CuSparseMatrixCSC(A)`: If `A.data` is a sparse matrix, return a new [`QuantumObject`](@ref) with `CUDA.CUSPARSE.CuSparseMatrixCSC`. +- `CuSparseMatrixCSC{T}(A)`: If `A.data` is a sparse matrix, return a new [`QuantumObject`](@ref) with `CUDA.CUSPARSE.CuSparseMatrixCSC` under element type `T`. +- `CuSparseMatrixCSR(A)`: If `A.data` is a sparse matrix, return a new [`QuantumObject`](@ref) with `CUDA.CUSPARSE.CuSparseMatrixCSR`. +- `CuSparseMatrixCSR{T}(A)`: If `A.data` is a sparse matrix, return a new [`QuantumObject`](@ref) with `CUDA.CUSPARSE.CuSparseMatrixCSR` under element type `T`. + +We suggest to convert the arrays from CPU to GPU memory by using the function `cu` because it allows different `data`-types of input [`QuantumObject`](@ref). + +Here are some examples: + +## Converting dense arrays + +```julia +V = fock(2, 0) # CPU dense vector +``` + +``` +Quantum Object: type=Ket dims=[2] size=(2,) +2-element Vector{ComplexF64}: + 1.0 + 0.0im + 0.0 + 0.0im +``` + +```julia +cu(V) +``` + +``` +Quantum Object: type=Ket dims=[2] size=(2,) +2-element CuArray{ComplexF64, 1, CUDA.DeviceMemory}: + 1.0 + 0.0im + 0.0 + 0.0im +``` + +```julia +cu(V; word_size = 32) +``` + +``` +Quantum Object: type=Ket dims=[2] size=(2,) +2-element CuArray{ComplexF32, 1, CUDA.DeviceMemory}: + 1.0 + 0.0im + 0.0 + 0.0im +``` + +```julia +M = Qobj([1 2; 3 4]) # CPU dense matrix +``` + +``` +Quantum Object: type=Operator dims=[2] size=(2, 2) ishermitian=false +2×2 Matrix{Int64}: + 1 2 + 3 4 +``` + +```julia +cu(M) +``` + +``` +Quantum Object: type=Operator dims=[2] size=(2, 2) ishermitian=false +2×2 CuArray{Int64, 2, CUDA.DeviceMemory}: + 1 2 + 3 4 +``` + +```julia +cu(M; word_size = 32) +``` + +``` +Quantum Object: type=Operator dims=[2] size=(2, 2) ishermitian=false +2×2 CuArray{Int32, 2, CUDA.DeviceMemory}: + 1 2 + 3 4 +``` + +## Converting sparse arrays + +```julia +V = fock(2, 0; sparse=true) # CPU sparse vector +``` + +``` +Quantum Object: type=Ket dims=[2] size=(2,) +2-element SparseVector{ComplexF64, Int64} with 1 stored entry: + [1] = 1.0+0.0im +``` + +```julia +cu(V) +``` + +``` +Quantum Object: type=Ket dims=[2] size=(2,) +2-element CuSparseVector{ComplexF64, Int32} with 1 stored entry: + [1] = 1.0+0.0im +``` + +```julia +cu(V; word_size = 32) +``` + +``` +Quantum Object: type=Ket dims=[2] size=(2,) +2-element CuSparseVector{ComplexF32, Int32} with 1 stored entry: + [1] = 1.0+0.0im +``` + +```julia +M = sigmax() # CPU sparse matrix +``` + +``` +Quantum Object: type=Operator dims=[2] size=(2, 2) ishermitian=true +2×2 SparseMatrixCSC{ComplexF64, Int64} with 2 stored entries: + ⋅ 1.0+0.0im + 1.0+0.0im ⋅ +``` + +```julia +cu(M) +``` + +``` +Quantum Object: type=Operator dims=[2] size=(2, 2) ishermitian=true +2×2 CuSparseMatrixCSC{ComplexF64, Int32} with 2 stored entries: + ⋅ 1.0+0.0im + 1.0+0.0im ⋅ +``` + +```julia +cu(M; word_size = 32) +``` + +``` +Quantum Object: type=Operator dims=[2] size=(2, 2) ishermitian=true +2×2 CuSparseMatrixCSC{ComplexF32, Int32} with 2 stored entries: + ⋅ 1.0+0.0im + 1.0+0.0im ⋅ +``` diff --git a/docs/src/users_guide/states_and_operators.md b/docs/src/users_guide/states_and_operators.md index 565aece2..b836c348 100644 --- a/docs/src/users_guide/states_and_operators.md +++ b/docs/src/users_guide/states_and_operators.md @@ -1,17 +1,407 @@ -# [States and Operators](@id doc:States-and-Operators) - -This page is still under construction, please visit [API](@ref doc-API) first. +# [Manipulating States and Operators](@id doc:Manipulating-States-and-Operators) ## Introduction +In the previous guide section [Basic Operations on Quantum Objects](@ref doc:Qobj), we saw how to create states and operators, using the functions built into `QuantumToolbox`. In this portion of the guide, we will look at performing basic operations with states and operators. For more detailed demonstrations on how to use and manipulate these objects, see the examples given in the tutorial section. + +```@setup states_and_operators +using QuantumToolbox +``` + +## [State Vectors (kets or bras)](@id doc:State-vectors) +Here we begin by creating a Fock [`basis`](@ref) (or [`fock`](@ref)) vacuum state vector ``|0\rangle`` with in a Hilbert space with `5` number states, from `0` to `4`: + +```@example states_and_operators +vac = basis(5, 0) +``` + +and then create a lowering operator ``\hat{a}`` corresponding to `5` number states using the [`destroy`](@ref) function: + +```@example states_and_operators +a = destroy(5) +``` + +Now lets apply the lowering operator `\hat{a}` to our vacuum state `vac`: + +```@example states_and_operators +a * vac +``` + +We see that, as expected, the vacuum is transformed to the zero vector. A more interesting example comes from using the `adjoint` of the lowering operator ``\hat{a}``, the raising operator ``\hat{a}^\dagger``: + +```@example states_and_operators +a' * vac +``` + +The raising operator has in indeed raised the state `vac` from the vacuum to the ``|1\rangle`` state. Instead of using the `adjoint` method to raise the state, we could have also used the built-in [`create`](@ref) function to make a raising operator: + +```@example states_and_operators +ad = create(5) +ad * vac +``` + +which does the same thing. We can raise the vacuum state more than once by successively apply the raising operator: + +```@example states_and_operators +ad * ad * vac +``` + +or just taking the square of the raising operator ``\left(\hat{a}^\dagger\right)^2``: + +```@example states_and_operators +ad^2 * vac +``` + +Applying the raising operator twice gives the expected ``\sqrt{n+1}`` dependence. We can use the product of ``\hat{a}^\dagger \hat{a}`` to also apply the number operator to the state vector `vac`: + +```@example states_and_operators +ad * a * vac +``` + +or on the ``|1\rangle`` state: + +```@example states_and_operators +ad * a * (ad * vac) +``` + +or on the ``|2\rangle`` state: + +```@example states_and_operators +ad * a * (ad^2 * vac) +``` + +Notice how in this last example, application of the number operator does not give the expected value ``n=2``, but rather ``2\sqrt{2}``. This is because this last state is not normalized to unity as ``\hat{a}^\dagger|n\rangle=\sqrt{n+1}|n+1\rangle``. Therefore, we should [`normalize`](@ref) (or use [`unit`](@ref)) our vector first: + +```@example states_and_operators +ad * a * normalize(ad^2 * vac) +``` + +Since we are giving a demonstration of using states and operators, we have done a lot more work than we should have. For example, we do not need to operate on the vacuum state to generate a higher number Fock state. Instead we can use the [`basis`](@ref) (or [`fock`](@ref)) function to directly obtain the required state: + +```@example states_and_operators +ket = basis(5, 2) +``` + +Notice how it is automatically normalized. We can also use the built in number operator [`num`](@ref): + +```@example states_and_operators +n = num(5) +``` + +Therefore, instead of `ad * a * normalize(ad^2 * vac)`, we have: + +```@example states_and_operators +n * ket +``` + +We can also create superpositions of states: + +```@example states_and_operators +ket = normalize(basis(5, 0) + basis(5, 1)) +``` + +where we have used the `normalize` function again to normalize the state. Apply the number opeartor again: + +```@example states_and_operators +n * ket +``` + +We can also create coherent states and squeezed states by applying the [`displace`](@ref) and [`squeeze`](@ref) functions to the vacuum state: + +```@example states_and_operators +vac = basis(5, 0) + +d = displace(5, 1im) + +s = squeeze(5, 0.25 + 0.25im) + +d * vac +``` + +```@example states_and_operators +d * s * vac +``` + +Of course, displacing the vacuum gives a coherent state, which can also be generated using the built in [`coherent`](@ref) function. + +## [Density matrices](@id doc:Density-matrices) + +One of the main purpose of `QuantumToolbox` is to explore the dynamics of open quantum systems, where the most general state of a system is no longer a state vector, but rather a density matrix. Since operations on density matrices operate identically to those of vectors, we will just briefly highlight creating and using these structures. + +The simplest density matrix is created by forming the outer-product ``|\psi\rangle\langle\psi|`` of a ket vector: + +```@example states_and_operators +ket = basis(5, 2) +ket * ket' +``` + +A similar task can also be accomplished via the [`fock_dm`](@ref) or [`ket2dm`](@ref) functions: + +```@example states_and_operators +fock_dm(5, 2) +``` + +```@example states_and_operators +ket2dm(ket) +``` + +If we want to create a density matrix with equal classical probability of being found in the ``|2\rangle`` or ``|4\rangle`` number states, we can do the following: + +```@example states_and_operators +0.5 * fock_dm(5, 2) + 0.5 * fock_dm(5, 4) # with fock_dm +0.5 * ket2dm(basis(5, 2)) + 0.5 * ket2dm(basis(5, 4)) # with ket2dm +``` + +There are also several other built-in functions for creating predefined density matrices, for example [`coherent_dm`](@ref) and [`thermal_dm`](@ref) which create coherent state and thermal state density matrices, respectively. + +```@example states_and_operators +coherent_dm(5, 1.25) +``` + +```@example states_and_operators +thermal_dm(5, 1.25) +``` + +`QuantumToolbox` also provides a set of distance metrics for determining how close two density matrix distributions are to each other. Included are the [`fidelity`](@ref), and trace distance ([`tracedist`](@ref)). + +```@example states_and_operators +x = coherent_dm(5, 1.25) + +y = coherent_dm(5, 1.25im) + +z = thermal_dm(5, 0.125) + +fidelity(x, y) +``` +Note that the definition of [`fidelity`](@ref) here is from **Nielsen & Chuang, "Quantum Computation and Quantum Information"**. It is the square root of the fidelity defined in **R. Jozsa, Journal of Modern Optics, 41:12, 2315 (1994)**. We also know that for two pure states, the trace distance (``T``) and the fidelity (``F``) are related by ``T = \sqrt{1-F^2}``: + +```@example states_and_operators +tracedist(x, y) ≈ sqrt(1 - (fidelity(x, y))^2) +``` + +For a pure state and a mixed state, ``1 - F \leq T`` which can also be verified: + +```@example states_and_operators +1 - fidelity(x, z) < tracedist(x, z) +``` + +## [Two-level systems (Qubits)](@id doc:Two-level-systems) + +Having spent a fair amount of time on basis states that represent harmonic oscillator states, we now move on to qubit, or two-level quantum systems (for example a spin-``1/2``). To create a state vector corresponding to a qubit system, we use the same basis, or fock, function with only two levels: + +```@example states_and_operators +spin = basis(2, 0) +``` + +Now at this point one may ask how this state is different than that of a harmonic oscillator in the vacuum state truncated to two energy levels? + +```@example states_and_operators +vac = basis(2, 0) +``` + +At this stage, there is no difference. This should not be surprising as we called the exact same function twice. The difference between the two comes from the action of the spin operators [`sigmax`](@ref), [`sigmay`](@ref), [`sigmaz`](@ref), [`sigmap`](@ref), and [`sigmam`](@ref) on these two-level states. For example, if `vac` corresponds to the vacuum state of a harmonic oscillator, then, as we have already seen, we can use the raising operator ([`create`](@ref)) to get the ``|1\rangle`` state: + +```@example states_and_operators +create(2) * vac +``` + +For a spin system, the operator analogous to the raising operator is the ``\hat{\sigma}_+`` operator [`sigmap`](@ref). Applying on the spin state gives: + +```@example states_and_operators +sigmap() * spin +``` + +Now we see the difference! The [`sigmap`](@ref) operator acting on the spin state returns the zero vector. Why is this? To see what happened, let us use the ``\hat{\sigma}_z`` ([`sigmaz`](@ref)) operator: + +```@example states_and_operators +sigmaz() +``` + +```@example states_and_operators +sigmaz() * spin +``` + +```@example states_and_operators +spin2 = basis(2, 1) +``` + +```@example states_and_operators +sigmaz() * spin2 +``` + +The answer is now apparent. Since the `QuantumToolbox` [`sigmaz`](@ref) function uses the standard ``Z``-basis representation of the ``\hat{\sigma}_z`` spin operator, the `spin` state corresponds to the ``|\uparrow\rangle`` state of a two-level spin system while `spin2` gives the ``|\downarrow\rangle`` state. Therefore, in our previous example `sigmap() * spin`, we raised the qubit state out of the truncated two-level Hilbert space resulting in the zero state. + +While at first glance this convention might seem somewhat odd, it is in fact quite handy. For one, the spin operators remain in the conventional form. Second, this corresponds nicely with the quantum information definitions of qubit states, where the excited ``|\uparrow\rangle`` state is label as ``|0\rangle``, and the ``|\downarrow\rangle`` state by ``|1\rangle``. + +If one wants to create spin operators for higher spin systems, then the [`jmat`](@ref) function comes in handy. + +## [Expectation values](@id doc:Expectation-values) + +Some of the most important information about quantum systems comes from calculating the expectation value of operators, both Hermitian and non-Hermitian, as the state or density matrix of the system varies in time. Therefore, in this section we demonstrate the use of the [`expect`](@ref) function. To begin: + +```@example states_and_operators +vac = basis(5, 0) + +one = basis(5, 1) + +c = create(5) + +N = num(5) + +coh = coherent_dm(5, 1.0im) + +cat = normalize(basis(5, 4) + 1.0im * basis(5, 3)) + +println(expect(N, vac) ≈ 0) +println(expect(N, one) ≈ 1) +println(expect(N, coh) ≈ 0.9970555745806597) +println(expect(c, cat) ≈ 1im) +``` + +The [`expect`](@ref) function also accepts lists or arrays of state vectors or density matrices for the second input: + +```@example states_and_operators +states = [normalize(c^k * vac) for k in 0:4] + +expect(N, states) +``` + +```@example states_and_operators +cat_list = [normalize(basis(5, 4) + x * basis(5, 3)) for x in [0, 1.0im, -1.0, -1.0im]] + +expect(c, cat_list) +``` + +Notice how in this last example, all of the return values are complex numbers. This is because the expect function looks to see whether the operator is Hermitian or not. If the operator is Hermitian, then the output will always be real. In the case of non-Hermitian operators, the return values may be complex. Therefore, the expect function will return an array of complex values for non-Hermitian operators when the input is a list/array of states or density matrices. + +Of course, the expect function works for spin states and operators: + +```@example states_and_operators +up = basis(2, 0) + +dn = basis(2, 1) + +println(expect(sigmaz(), up) ≈ 1) +println(expect(sigmaz(), dn) ≈ -1) +``` + +as well as the composite objects discussed in the next section [Tensor Products and Partial Traces](@ref doc:Tensor-products-and-Partial-Traces): + +```@example states_and_operators +spin1 = basis(2, 0) + +spin2 = basis(2, 1) + +two_spins = tensor(spin1, spin2) + +sz1 = tensor(sigmaz(), qeye(2)) + +sz2 = tensor(qeye(2), sigmaz()) + +println(expect(sz1, two_spins) ≈ 1) +println(expect(sz2, two_spins) ≈ -1) +``` + +## [Superoperators and Vectorized Operators](@id doc:Superoperators-and-Vectorized-Operators) + +In addition to state vectors and density operators, `QuantumToolbox` allows for representing maps that act linearly on density operators using the Liouville supermatrix formalisms. + +This support is based on the correspondence between linear operators acting on a Hilbert space, and vectors in two copies of that Hilbert space (which is also called the Fock-Liouville space), +```math +\textrm{vec} : \mathcal{L}(\mathcal{H}) \rightarrow \mathcal{H}\otimes\mathcal{H}. +``` +Therefore, a given density matrix ``\hat{\rho}`` can then be vectorized, denoted as +```math +|\hat{\rho}\rangle\rangle = \textrm{vec}(\hat{\rho}). +``` + +`QuantumToolbox` uses the column-stacking convention for the isomorphism between ``\mathcal{L}(\mathcal{H})`` and ``\mathcal{H}\otimes\mathcal{H}``. This isomorphism is implemented by the functions [`mat2vec`](@ref) and [`vec2mat`](@ref): + +```@example states_and_operators +rho = Qobj([1 2; 3 4]) +``` + +```@example states_and_operators +vec_rho = mat2vec(rho) +``` + +```@example states_and_operators +rho2 = vec2mat(vec_rho) +``` + +The `QuantumObject.type` attribute indicates whether a quantum object is a vector corresponding to an [`OperatorKet`](@ref), or its Hermitian conjugate [`OperatorBra`](@ref). One can also use [`isoper`](@ref), [`isoperket`](@ref), and [`isoperbra`](@ref) to check the type: + +```@example states_and_operators +println(isoper(vec_rho)) +println(isoperket(vec_rho)) +println(isoperbra(vec_rho)) +println(isoper(vec_rho')) +println(isoperket(vec_rho')) +println(isoperbra(vec_rho')) +``` + +Because `Julia` is a column-oriented languages (like `Fortran` and `MATLAB`), in `QuantumToolbox`, we define the [`spre`](@ref) (left), [`spost`](@ref) (right), and [`sprepost`](@ref) (left-and-right) multiplication superoperators as follows: + +```math +\begin{align} +\hat{A}\hat{\rho}~~~ &\rightarrow \textrm{spre}(\hat{A}) * \textrm{vec}(\hat{\rho}) = \hat{\mathbb{1}}\otimes \hat{A} ~ |\hat{\rho}\rangle\rangle,\notag\\ +\hat{\rho} \hat{B} &\rightarrow \textrm{spost}(\hat{B}) * \textrm{vec}(\hat{\rho}) = \hat{B}^T\otimes \hat{\mathbb{1}} ~ |\hat{\rho}\rangle\rangle,\notag\\ +\hat{A} \hat{\rho} \hat{B} &\rightarrow \textrm{sprepost}(\hat{A},\hat{B}) * \textrm{vec}(\hat{\rho}) = \hat{B}^T\otimes \hat{A} ~ |\hat{\rho}\rangle\rangle,\notag +\end{align} +``` +where ``\hat{\mathbb{1}}`` represents the identity operator with Hilbert space dimension equal to ``\hat{\rho}``. + +```@example states_and_operators +A = Qobj([1 2; 3 4]) +S_A = spre(A) +``` + +```@example states_and_operators +B = Qobj([5 6; 7 8]) +S_B = spost(B) +``` + +```@example states_and_operators +S_AB = sprepost(A, B) +``` + +```@example states_and_operators +S_AB ≈ S_A * S_B ≈ S_B * S_A +``` + +One can also use [`issuper`](@ref) to check the type: + +```@example states_and_operators +println(isoper(S_AB)) +println(issuper(S_AB)) +``` + +With the above definitions, the following equalities hold in `Julia`: + +```math +\textrm{vec}(\hat{A} \hat{\rho} \hat{B}) = \textrm{spre}(\hat{A}) * \textrm{spre}(\hat{B}) * \textrm{vec}(\hat{\rho}) = \textrm{sprepost}(\hat{A},\hat{B}) * \textrm{vec}(\hat{\rho}) ~~\forall~~\hat{A}, \hat{B}, \hat{\rho} +``` + +```@example states_and_operators +N = 10 +A = Qobj(rand(ComplexF64, N, N)) +B = Qobj(rand(ComplexF64, N, N)) +ρ = rand_dm(N) # random density matrix +mat2vec(A * ρ * B) ≈ spre(A) * spost(B) * mat2vec(ρ) ≈ sprepost(A, B) * mat2vec(ρ) +``` -## [State Vectors (kets or bras)](@id doc: State vectors) +In addition, dynamical generators on this extended space, often called Liouvillian superoperators, can be created using the [`liouvillian`](@ref) function. Each of these takes a Hamiltonian along with a list of collapse operators, and returns a [`type=SuperOperator`](@ref SuperOperator) object that can be exponentiated to find the superoperator for that evolution. -## [Density matrices](@id doc: Density matrices) +```@example states_and_operators +H = 10 * sigmaz() -## [Two-level systems (qubits)](@id doc: Two-level systems) +c = destroy(2) -## [Expectation values](@id doc: Expectation values) +L = liouvillian(H, [c]) +``` -## [Superoperators and Vectorized Operators](@id doc: Superoperators and Vectorized Operators) +```@example states_and_operators +t = 0.8 +exp(L * t) +``` -## [Generating Random States and Operators](@id doc: Generating Random States and Operators) +See the section [Time Evolution and Quantum System Dynamics](@ref doc:Time-Evolution-and-Quantum-System-Dynamics) for more details. diff --git a/docs/src/users_guide/tensor.md b/docs/src/users_guide/tensor.md index 23d89e0a..61e12535 100644 --- a/docs/src/users_guide/tensor.md +++ b/docs/src/users_guide/tensor.md @@ -1,3 +1,152 @@ -# [Tensor products](@id doc:Tensor-products) +# [Tensor Products and Partial Traces](@id doc:Tensor-products-and-Partial-Traces) -This page is still under construction, please visit [API](@ref doc-API) first. \ No newline at end of file +```@setup tensor_products +using QuantumToolbox +``` + +## [Tensor products](@id doc: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 `QuantumToolbox`, the function [`tensor`](@ref) (or [`kron`](@ref)) is used to accomplish this task. This function takes a collection of [`Ket`](@ref) or [`Operator`](@ref) as argument and returns a composite [`QuantumObject`](@ref) for the combined Hilbert space. The function accepts an arbitrary number of [`QuantumObject`](@ref) as argument. The `type` of returned [`QuantumObject`](@ref) is the same as that of the input(s). + +A collection of [`QuantumObject`](@ref): +```@example tensor_products +tensor(sigmax(), sigmax(), sigmax()) +``` + +or a `Vector{QuantumObject}`: + +```@example tensor_products +op_list = fill(sigmax(), 3) +tensor(op_list) +``` + +!!! warning "Beware of type-stability!" + Please note that `tensor(op_list)` or `kron(op_list)` with `op_list` is a `Vector` is type-instable and can hurt performance. It is recommended to use `tensor(op_list...)` or `kron(op_list...)` instead. See the Section [The Importance of Type-Stability](@ref doc:Type-Stability) for more details. + +For example, the state vector describing two qubits in their ground states is formed by taking the tensor product of the two single-qubit ground state vectors: + +```@example tensor_products +tensor(basis(2, 0), basis(2, 0)) +``` + +One can generalize to more qubits by adding more component state vectors in the argument list to the [`tensor`](@ref) (or [`kron`](@ref)) function, as illustrated in the following example: + +```@example tensor_products +states = QuantumObject[ + normalize(basis(2, 0) + basis(2, 1)), + normalize(basis(2, 0) + basis(2, 1)), + basis(2, 0) +] +tensor(states...) +``` +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 [`tensor`](@ref) (or [`kron`](@ref)) function. For example, to form the operator that represents the simultaneous action of the ``\hat{\sigma}_x`` operator on two qubits: + +```@example tensor_products +tensor(sigmax(), sigmax()) +``` + +To create operators in a combined Hilbert space that only act on a single component, we take the tensor product of the operator acting on the subspace of interest, with the identity operators corresponding to the components that are to be unchanged. For example, the operator that represents ``\hat{\sigma}_z`` on the first qubit in a two-qubit system, while leaving the second qubit unaffected: + +```@example tensor_products +tensor(sigmaz(), qeye(2)) +``` + +## Example: Constructing composite Hamiltonians + +The [`tensor`](@ref) (or [`kron`](@ref)) function is extensively used when constructing Hamiltonians for composite systems. Here we’ll look at some simple examples. + +### Two coupled qubits + +First, let’s consider a system of two coupled qubits. Assume that both the qubits have equal energy splitting, and that the qubits are coupled through a ``\hat{\sigma}_x \otimes \hat{\sigma}_x`` interaction with strength ``g = 0.05`` (in units where the bare qubit energy splitting is unity). The Hamiltonian describing this system is: + +```@example tensor_products +H = tensor(sigmaz(), qeye(2)) + + tensor(qeye(2), sigmaz()) + + 0.05 * tensor(sigmax(), sigmax()) +``` + +### Three coupled qubits + +The two-qubit example is easily generalized to three coupled qubits: + +```@example tensor_products +H = tensor(sigmaz(), qeye(2), qeye(2)) + + tensor(qeye(2), sigmaz(), qeye(2)) + + tensor(qeye(2), qeye(2), sigmaz()) + + 0.5 * tensor(sigmax(), sigmax(), qeye(2)) + + 0.25 * tensor(qeye(2), sigmax(), sigmax()) +``` + +### A two-level system coupled to a cavity: The Jaynes-Cummings model + +The simplest possible quantum mechanical description for light-matter interaction is encapsulated in the Jaynes-Cummings model, which describes the coupling between a two-level atom and a single-mode electromagnetic field (a cavity mode). Denoting the energy splitting of the atom and cavity ``\omega_a`` and ``\omega_c``, respectively, and the atom-cavity interaction strength ``g``, the Jaynes-Cummings Hamiltonian can be constructed as: + +```math +H = \frac{\omega_a}{2}\hat{\sigma}_z + \omega_c \hat{a}^\dagger \hat{a} + g (\hat{a}^\dagger \hat{\sigma}_- + \hat{a} \hat{\sigma}_+) +``` + +```@example tensor_products +N = 6 # cavity fock space truncation +ωc = 1.25 # frequency of cavity +ωa = 1.0 # frequency of two-level atom +g = 0.75 # interaction strength + +a = tensor(qeye(2), destroy(N)) # cavity annihilation operator + +# two-level atom operators +σm = tensor(destroy(2), qeye(N)) +σz = tensor(sigmaz(), qeye(N)) + +H = 0.5 * ωa * σz + ωc * a' * a + g * (a' * σm + a * σm') +``` + +## [Partial trace](@id doc:Partial-trace) + +The partial trace is an operation that reduces the dimension of a Hilbert space by eliminating some degrees of freedom by averaging (tracing). In this sense it is therefore the converse of the tensor product. It is useful when one is interested in only a part of a coupled quantum system. For open quantum systems, this typically involves tracing over the environment leaving only the system of interest. In `QuantumToolbox` the function [`ptrace`](@ref) is used to take partial traces. [`ptrace`](@ref) takes one [`QuantumObject`](@ref) as an input, and also one argument `sel`, which marks the component systems that should be kept, and all the other components are traced out. + +Remember that the index of `Julia` starts from `1`, and all the elements in `sel` should be positive `Integer`. Therefore, the type of `sel` can be either `Integer`, `Tuple`, `SVector`, or `Vector`. + +!!! warning "Beware of type-stability!" + Although it supports also `Vector` type, it is recommended to use `Tuple` or `SVector` from [`StaticArrays.jl`](https://github.com/JuliaArrays/StaticArrays.jl) to improve performance. For a brief explanation on the impact of the type of `sel`, see the section [The Importance of Type-Stability](@ref doc:Type-Stability). + +For example, the density matrix describing a single qubit obtained from a coupled two-qubit system is obtained via: + +```@example tensor_products +ψ = tensor( + basis(2, 0), + basis(2, 1), + normalize(basis(2, 0) + basis(2, 1)) +) +``` + +```@example tensor_products +ptrace(ψ, 1) # trace out 2nd and 3rd systems +``` + +```@example tensor_products +ptrace(ψ, (1, 3)) # trace out 2nd system +``` + +Note that the partial trace always results in a [`Operator`](@ref) (density matrix), regardless of whether the composite system is a pure state (described by a [`Ket`](@ref)) or a mixed state (described by a [`Operator`](@ref)): + +```@example tensor_products +ψ1 = normalize(basis(2, 0) + basis(2, 1)) +ψ2 = basis(2, 0) +ψT = tensor(ψ1, ψ2) +``` + +```@example tensor_products +ptrace(ψT, 1) +``` + +```@example tensor_products +ρT = tensor(ket2dm(ψ1), ket2dm(ψ1)) +``` + +```@example tensor_products +ptrace(ρT, 1) +``` diff --git a/ext/QuantumToolboxCUDAExt.jl b/ext/QuantumToolboxCUDAExt.jl index e1b3418b..7d379c3a 100644 --- a/ext/QuantumToolboxCUDAExt.jl +++ b/ext/QuantumToolboxCUDAExt.jl @@ -89,4 +89,10 @@ _change_eltype(::Type{T}, ::Val{32}) where {T<:AbstractFloat} = Float32 _change_eltype(::Type{Complex{T}}, ::Val{64}) where {T<:Union{Int,AbstractFloat}} = ComplexF64 _change_eltype(::Type{Complex{T}}, ::Val{32}) where {T<:Union{Int,AbstractFloat}} = ComplexF32 +sparse_to_dense(::Type{T}, A::CuArray{T}) where {T<:Number} = A +sparse_to_dense(::Type{T1}, A::CuArray{T2}) where {T1<:Number,T2<:Number} = CuArray{T1}(A) +sparse_to_dense(::Type{T}, A::CuSparseVector) where {T<:Number} = CuArray{T}(A) +sparse_to_dense(::Type{T}, A::CuSparseMatrixCSC) where {T<:Number} = CuArray{T}(A) +sparse_to_dense(::Type{T}, A::CuSparseMatrixCSR) where {T<:Number} = CuArray{T}(A) + end diff --git a/src/QuantumToolbox.jl b/src/QuantumToolbox.jl index bb99a645..9b177c5b 100644 --- a/src/QuantumToolbox.jl +++ b/src/QuantumToolbox.jl @@ -1,37 +1,54 @@ module QuantumToolbox # Re-export: -# 1. basic functions in LinearAlgebra and SparseArrays -# 2. the solvers in ODE and LinearSolve +# 1. StaticArraysCore.SVector for the type of dims +# 2. basic functions in LinearAlgebra and SparseArrays import Reexport: @reexport +@reexport import StaticArraysCore: SVector @reexport using LinearAlgebra @reexport using SparseArrays @reexport using OrdinaryDiffEq +@reexport using StochasticDiffEq @reexport using LinearSolve # other functions in LinearAlgebra import LinearAlgebra: BlasReal, BlasInt, BlasFloat, BlasComplex, checksquare import LinearAlgebra.BLAS: @blasfunc -if VERSION < v"1.10" - import LinearAlgebra: chkstride1 - import LinearAlgebra.BLAS: libblastrampoline - import LinearAlgebra.LAPACK: chklapackerror - import Base: require_one_based_indexing -else - import LinearAlgebra.LAPACK: hseqr! -end +import LinearAlgebra.LAPACK: hseqr! + +# SciML packages (for OrdinaryDiffEq and LinearSolve) +import SciMLBase: + solve, + solve!, + init, + reinit!, + remake, + u_modified!, + ODEProblem, + EnsembleProblem, + EnsembleThreads, + FullSpecialize, + CallbackSet, + ContinuousCallback, + DiscreteCallback +import SciMLOperators: MatrixOperator +import LinearSolve: LinearProblem, SciMLLinearSolveAlgorithm, KrylovJL_MINRES, KrylovJL_GMRES +import DiffEqBase: get_tstops +import DiffEqCallbacks: PeriodicCallback, PresetTimeCallback, TerminateSteadyState +import OrdinaryDiffEqCore: OrdinaryDiffEqAlgorithm +import OrdinaryDiffEqTsit5: Tsit5 # other dependencies (in alphabetical order) import ArrayInterface: allowed_getindex, allowed_setindex! import DiffEqCallbacks: DiscreteCallback, PeriodicCallback, PresetTimeCallback, TerminateSteadyState +import DiffEqNoiseProcess: RealWienerProcess, RealWienerProcess! import FFTW: fft, fftshift import Graphs: connected_components, DiGraph import IncompleteLU: ilu -import LinearMaps: LinearMap -import OrdinaryDiffEq: OrdinaryDiffEqAlgorithm import Pkg import Random import SpecialFunctions: loggamma +import StaticArraysCore: MVector # Setting the number of threads to 1 allows # to achieve better performances for more massive parallelizations @@ -41,6 +58,7 @@ BLAS.set_num_threads(1) include("utilities.jl") include("versioninfo.jl") include("progress_bar.jl") +include("linear_maps.jl") # Quantum Object include("qobj/quantum_object.jl") @@ -60,6 +78,7 @@ include("time_evolution/mesolve.jl") include("time_evolution/lr_mesolve.jl") include("time_evolution/sesolve.jl") include("time_evolution/mcsolve.jl") +include("time_evolution/ssesolve.jl") include("time_evolution/time_evolution_dynamical.jl") # Others diff --git a/src/correlations.jl b/src/correlations.jl index 196188fd..5f3c6d03 100644 --- a/src/correlations.jl +++ b/src/correlations.jl @@ -20,7 +20,7 @@ ExponentialSeries(; tol = 1e-14, calc_steadystate = false) = ExponentialSeries(t A::QuantumObject, B::QuantumObject, C::QuantumObject, - c_ops::AbstractVector=[]; + c_ops::Union{Nothing,AbstractVector}=nothing; kwargs...) Returns the two-times correlation function of three operators ``\hat{A}``, ``\hat{B}`` and ``\hat{C}``: ``\expval{\hat{A}(t) \hat{B}(t + \tau) \hat{C}(t)}`` @@ -35,7 +35,7 @@ function correlation_3op_2t( A::QuantumObject{<:AbstractArray{T3},OperatorQuantumObject}, B::QuantumObject{<:AbstractArray{T4},OperatorQuantumObject}, C::QuantumObject{<:AbstractArray{T5},OperatorQuantumObject}, - c_ops::AbstractVector = []; + c_ops::Union{Nothing,AbstractVector} = nothing; kwargs..., ) where { T1, @@ -65,7 +65,7 @@ end τ_l::AbstractVector, A::QuantumObject, B::QuantumObject, - c_ops::AbstractVector=[]; + c_ops::Union{Nothing,AbstractVector}=nothing; reverse::Bool=false, kwargs...) @@ -81,7 +81,7 @@ function correlation_2op_2t( τ_l::AbstractVector, A::QuantumObject{<:AbstractArray{T3},OperatorQuantumObject}, B::QuantumObject{<:AbstractArray{T4},OperatorQuantumObject}, - c_ops::AbstractVector = []; + c_ops::Union{Nothing,AbstractVector} = nothing; reverse::Bool = false, kwargs..., ) where { @@ -108,7 +108,7 @@ end τ_l::AbstractVector, A::QuantumObject, B::QuantumObject, - c_ops::AbstractVector=[]; + c_ops::Union{Nothing,AbstractVector}=nothing; reverse::Bool=false, kwargs...) @@ -122,7 +122,7 @@ function correlation_2op_1t( τ_l::AbstractVector, A::QuantumObject{<:AbstractArray{T3},OperatorQuantumObject}, B::QuantumObject{<:AbstractArray{T4},OperatorQuantumObject}, - c_ops::AbstractVector = []; + c_ops::Union{Nothing,AbstractVector} = nothing; reverse::Bool = false, kwargs..., ) where { @@ -143,7 +143,7 @@ end ω_list::AbstractVector, A::QuantumObject{<:AbstractArray{T2},OperatorQuantumObject}, B::QuantumObject{<:AbstractArray{T3},OperatorQuantumObject}, - c_ops::AbstractVector=[]; + c_ops::Union{Nothing,AbstractVector}=nothing; solver::MySolver=ExponentialSeries(), kwargs...) @@ -158,16 +158,14 @@ function spectrum( ω_list::AbstractVector, A::QuantumObject{<:AbstractArray{T2},OperatorQuantumObject}, B::QuantumObject{<:AbstractArray{T3},OperatorQuantumObject}, - c_ops::Vector{QuantumObject{MT2,COpType}} = Vector{QuantumObject{MT1,HOpType}}([]); + c_ops::Union{Nothing,AbstractVector} = nothing; solver::MySolver = ExponentialSeries(), kwargs..., ) where { MT1<:AbstractMatrix, - MT2<:AbstractMatrix, T2, T3, HOpType<:Union{OperatorQuantumObject,SuperOperatorQuantumObject}, - COpType<:Union{OperatorQuantumObject,SuperOperatorQuantumObject}, MySolver<:SpectrumSolver, } return _spectrum(H, ω_list, A, B, c_ops, solver; kwargs...) diff --git a/src/linear_maps.jl b/src/linear_maps.jl new file mode 100644 index 00000000..8afd88e7 --- /dev/null +++ b/src/linear_maps.jl @@ -0,0 +1,54 @@ +export AbstractLinearMap + +@doc raw""" + AbstractLinearMap{T, TS} + +Represents a general linear map with element type `T` and size `TS`. + +## Overview + +A **linear map** is a transformation `L` that satisfies: + +- **Additivity**: + ```math + L(u + v) = L(u) + L(v) + ``` +- **Homogeneity**: + ```math + L(cu) = cL(u) + ``` + +It is typically represented as a matrix with dimensions given by `size`, and this abtract type helps to define this map when the matrix is not explicitly available. + +## Methods + +- `Base.eltype(A)`: Returns the element type `T`. +- `Base.size(A)`: Returns the size `A.size`. +- `Base.size(A, i)`: Returns the `i`-th dimension. + +## Example + +As an example, we now define the linear map used in the [`eigsolve_al`](@ref) function for Arnoldi-Lindblad eigenvalue solver: + +```julia-repl +struct ArnoldiLindbladIntegratorMap{T,TS,TI} <: AbstractLinearMap{T,TS} + elty::Type{T} + size::TS + integrator::TI +end + +function LinearAlgebra.mul!(y::AbstractVector, A::ArnoldiLindbladIntegratorMap, x::AbstractVector) + reinit!(A.integrator, x) + solve!(A.integrator) + return copyto!(y, A.integrator.u) +end +``` + +where `integrator` is the ODE integrator for the time-evolution. In this way, we can diagonalize this linear map using the [`eigsolve`](@ref) function. +""" +abstract type AbstractLinearMap{T,TS} end + +Base.eltype(A::AbstractLinearMap{T}) where {T} = T + +Base.size(A::AbstractLinearMap) = A.size +Base.size(A::AbstractLinearMap, i::Int) = A.size[i] diff --git a/src/metrics.jl b/src/metrics.jl index 0a026ffb..d8fd4f5e 100644 --- a/src/metrics.jl +++ b/src/metrics.jl @@ -53,36 +53,36 @@ function entropy_vn( base::Int = 0, tol::Real = 1e-15, ) where {T} - vals = eigvals(ρ) - indexes = abs.(vals) .> tol - 1 ∉ indexes && return 0 + vals = eigenenergies(ρ) + indexes = findall(x -> abs(x) > tol, vals) + length(indexes) == 0 && return zero(real(T)) nzvals = vals[indexes] logvals = base != 0 ? log.(base, Complex.(nzvals)) : log.(Complex.(nzvals)) - return -real(sum(nzvals .* logvals)) + return -real(mapreduce(*, +, nzvals, logvals)) end """ - entanglement(QO::QuantumObject, sel::Vector) + entanglement(QO::QuantumObject, sel::Union{Int,AbstractVector{Int},Tuple}) Calculates the entanglement by doing the partial trace of `QO`, selecting only the dimensions with the indices contained in the `sel` vector, and then using the Von Neumann entropy [`entropy_vn`](@ref). """ function entanglement( QO::QuantumObject{<:AbstractArray{T},OpType}, - sel::Vector{Int}, + sel::Union{AbstractVector{Int},Tuple}, ) where {T,OpType<:Union{BraQuantumObject,KetQuantumObject,OperatorQuantumObject}} ψ = normalize(QO) ρ_tr = ptrace(ψ, sel) entropy = entropy_vn(ρ_tr) return (entropy > 0) * entropy end -entanglement(QO::QuantumObject, sel::Int) = entanglement(QO, [sel]) +entanglement(QO::QuantumObject, sel::Int) = entanglement(QO, (sel,)) @doc raw""" tracedist(ρ::QuantumObject, σ::QuantumObject) Calculates the [trace distance](https://en.wikipedia.org/wiki/Trace_distance) between two [`QuantumObject`](@ref): -``T(\rho, \sigma) = \frac{1}{2} \lVert \rho - \sigma \rVert_1`` +``T(\hat{\rho}, \hat{\sigma}) = \frac{1}{2} \lVert \hat{\rho} - \hat{\sigma} \rVert_1`` Note that `ρ` and `σ` must be either [`Ket`](@ref) or [`Operator`](@ref). """ @@ -100,7 +100,7 @@ tracedist( fidelity(ρ::QuantumObject, σ::QuantumObject) Calculate the fidelity of two [`QuantumObject`](@ref): -``F(\rho, \sigma) = \textrm{Tr} \sqrt{\sqrt{\rho} \sigma \sqrt{\rho}}`` +``F(\hat{\rho}, \hat{\sigma}) = \textrm{Tr} \sqrt{\sqrt{\hat{\rho}} \hat{\sigma} \sqrt{\hat{\rho}}}`` Here, the definition is from Nielsen & Chuang, "Quantum Computation and Quantum Information". It is the square root of the fidelity defined in R. Jozsa, Journal of Modern Optics, 41:12, 2315 (1994). diff --git a/src/negativity.jl b/src/negativity.jl index e3669e38..35d91c75 100644 --- a/src/negativity.jl +++ b/src/negativity.jl @@ -3,9 +3,9 @@ export negativity, partial_transpose @doc raw""" negativity(ρ::QuantumObject, subsys::Int; logarithmic::Bool=false) -Compute the [negativity](https://en.wikipedia.org/wiki/Negativity_(quantum_mechanics)) ``N(\rho) = \frac{\Vert \rho^{\Gamma}\Vert_1 - 1}{2}`` -where ``\rho^{\Gamma}`` is the partial transpose of ``\rho`` with respect to the subsystem, -and ``\Vert X \Vert_1=\textrm{Tr}\sqrt{X^\dagger X}`` is the trace norm. +Compute the [negativity](https://en.wikipedia.org/wiki/Negativity_(quantum_mechanics)) ``N(\hat{\rho}) = \frac{\Vert \hat{\rho}^{\Gamma}\Vert_1 - 1}{2}`` +where ``\hat{\rho}^{\Gamma}`` is the partial transpose of ``\hat{\rho}`` with respect to the subsystem, +and ``\Vert \hat{X} \Vert_1=\textrm{Tr}\sqrt{\hat{X}^\dagger \hat{X}}`` is the trace norm. # Arguments - `ρ::QuantumObject`: The density matrix (`ρ.type` must be [`OperatorQuantumObject`](@ref)). diff --git a/src/progress_bar.jl b/src/progress_bar.jl index e15b5a1c..40c81d45 100644 --- a/src/progress_bar.jl +++ b/src/progress_bar.jl @@ -1,18 +1,29 @@ export ProgressBar, next! -mutable struct ProgressBar{CT,T1<:Integer,T2<:Real,T3<:Real} +struct ProgressBar{CT,T1<:Integer,T2<:Real,T3,T4<:Real,LT} counter::CT max_counts::T1 enable::Bool bar_width::T1 start_time::T2 - previous_time::T2 - interval::T3 + previous_time::T3 + interval::T4 + lock::LT end function ProgressBar(max_counts::Int; enable::Bool = true, bar_width::Int = 30, interval = 1.0) start_time = time() - return ProgressBar(Threads.Atomic{Int}(0), max_counts, enable, bar_width, start_time, start_time, interval) + lock = ReentrantLock() + return ProgressBar( + Threads.Atomic{Int}(0), + max_counts, + enable, + bar_width, + start_time, + Threads.Atomic{Float64}(start_time), + interval, + lock, + ) end function next!(p::ProgressBar, io::IO = stdout) @@ -26,19 +37,21 @@ function next!(p::ProgressBar, io::IO = stdout) max_counts = p.max_counts bar_width = p.bar_width start_time = p.start_time - previous_time = p.previous_time + previous_time = p.previous_time[] interval = p.interval - ((time() - previous_time) < interval && counter != p.max_counts) && return + current_time = time() + + ((current_time - previous_time) < interval && counter != p.max_counts) && return - p.previous_time = time() + Threads.atomic_xchg!(p.previous_time, current_time) percentage = counter / max_counts percentage_100 = lpad(round(100 * percentage, digits = 1), 5, " ") progress = floor(Int, bar_width * percentage) # Calculate the elapsed time in seconds - elapsed_time = floor(Int, time() - start_time) + elapsed_time = floor(Int, current_time - start_time) # Convert the elapsed time into a string in hours, minutes and seconds elapsed_time_str = string( elapsed_time ÷ 3600, @@ -57,11 +70,16 @@ function next!(p::ProgressBar, io::IO = stdout) # Construct the progress bar string bar = "[" * repeat("=", progress) * repeat(" ", bar_width - progress) * "]" - print(io, "\rProgress: $bar $percentage_100% --- Elapsed Time: $elapsed_time_str (ETA: $eta_str)") + lock(p.lock) + try + print(io, "\rProgress: $bar $percentage_100% --- Elapsed Time: $elapsed_time_str (ETA: $eta_str)") - counter == p.max_counts && print(io, "\n") + counter == p.max_counts && print(io, "\n") - flush(io) + flush(io) + finally + unlock(p.lock) + end return nothing end diff --git a/src/qobj/arithmetic_and_attributes.jl b/src/qobj/arithmetic_and_attributes.jl index ae765c70..29b872de 100644 --- a/src/qobj/arithmetic_and_attributes.jl +++ b/src/qobj/arithmetic_and_attributes.jl @@ -135,7 +135,7 @@ end @doc raw""" dot(i::QuantumObject, A::QuantumObject j::QuantumObject) -Compute the generalized dot product `dot(i, A*j)` between three [`QuantumObject`](@ref): ``\langle i | A | j \rangle`` +Compute the generalized dot product `dot(i, A*j)` between three [`QuantumObject`](@ref): ``\langle i | \hat{A} | j \rangle`` Supports the following inputs: - `A` is in the type of [`Operator`](@ref), with `i` and `j` are both [`Ket`](@ref). @@ -239,8 +239,10 @@ julia> tr(a' * a) """ LinearAlgebra.tr( A::QuantumObject{<:AbstractArray{T},OpType}, -) where {T,OpType<:Union{OperatorQuantumObject,SuperOperatorQuantumObject}} = - ishermitian(A) ? real(tr(A.data)) : tr(A.data) +) where {T,OpType<:Union{OperatorQuantumObject,SuperOperatorQuantumObject}} = tr(A.data) +LinearAlgebra.tr( + A::QuantumObject{<:Union{<:Hermitian{TF},Symmetric{TR}},OpType}, +) where {TF<:BlasFloat,TR<:Real,OpType<:OperatorQuantumObject} = real(tr(A.data)) @doc raw""" svdvals(A::QuantumObject) @@ -471,10 +473,11 @@ proj(ψ::QuantumObject{<:AbstractArray{T},KetQuantumObject}) where {T} = ψ * ψ proj(ψ::QuantumObject{<:AbstractArray{T},BraQuantumObject}) where {T} = ψ' * ψ @doc raw""" - ptrace(QO::QuantumObject, sel::Vector{Int}) + ptrace(QO::QuantumObject, sel) + +[Partial trace](https://en.wikipedia.org/wiki/Partial_trace) of a quantum state `QO` leaving only the dimensions with the indices present in the `sel` vector. -[Partial trace](https://en.wikipedia.org/wiki/Partial_trace) of a quantum state `QO` leaving only the dimensions -with the indices present in the `sel` vector. +Note that this function will always return [`Operator`](@ref). No matter the input [`QuantumObject`](@ref) is a [`Ket`](@ref), [`Bra`](@ref), or [`Operator`](@ref). # Examples Two qubits in the state ``\ket{\psi} = \ket{e,g}``: @@ -487,7 +490,7 @@ Quantum Object: type=Ket dims=[2, 2] size=(4,) 0.0 + 0.0im 0.0 + 0.0im -julia> ptrace(ψ, [2]) +julia> ptrace(ψ, 2) Quantum Object: type=Operator dims=[2] size=(2, 2) ishermitian=true 2×2 Matrix{ComplexF64}: 0.0+0.0im 0.0+0.0im @@ -504,70 +507,117 @@ Quantum Object: type=Ket dims=[2, 2] size=(4,) 0.0 + 0.0im 0.7071067811865475 + 0.0im -julia> ptrace(ψ, [1]) +julia> ptrace(ψ, 1) Quantum Object: type=Operator dims=[2] size=(2, 2) ishermitian=true 2×2 Matrix{ComplexF64}: 0.5+0.0im 0.0+0.0im 0.0+0.0im 0.5+0.0im ``` """ -function ptrace(QO::QuantumObject{<:AbstractArray{T1},KetQuantumObject}, sel::Vector{T2}) where {T1,T2<:Integer} - length(QO.dims) == 1 && return QO +function ptrace(QO::QuantumObject{<:AbstractArray,KetQuantumObject}, sel::Union{AbstractVector{Int},Tuple}) + _non_static_array_warning("sel", sel) + + ns = length(sel) + if ns == 0 # return full trace for empty sel + return tr(ket2dm(QO)) + else + nd = length(QO.dims) + + (any(>(nd), sel) || any(<(1), sel)) && throw( + ArgumentError("Invalid indices in `sel`: $(sel), the given QuantumObject only have $(nd) sub-systems"), + ) + (ns != length(unique(sel))) && throw(ArgumentError("Duplicate selection indices in `sel`: $(sel)")) + (nd == 1) && return ket2dm(QO) # ptrace should always return Operator + end - ρtr, dkeep = _ptrace_ket(QO.data, QO.dims, sel) - return QuantumObject(ρtr, dims = dkeep) + _sort_sel = sort(SVector{length(sel),Int}(sel)) + ρtr, dkeep = _ptrace_ket(QO.data, QO.dims, _sort_sel) + return QuantumObject(ρtr, type = Operator, dims = dkeep) end -ptrace(QO::QuantumObject{<:AbstractArray{T1},BraQuantumObject}, sel::Vector{T2}) where {T1,T2<:Integer} = - ptrace(QO', sel) +ptrace(QO::QuantumObject{<:AbstractArray,BraQuantumObject}, sel::Union{AbstractVector{Int},Tuple}) = ptrace(QO', sel) + +function ptrace(QO::QuantumObject{<:AbstractArray,OperatorQuantumObject}, sel::Union{AbstractVector{Int},Tuple}) + _non_static_array_warning("sel", sel) -function ptrace(QO::QuantumObject{<:AbstractArray{T1},OperatorQuantumObject}, sel::Vector{T2}) where {T1,T2<:Integer} - length(QO.dims) == 1 && return QO + ns = length(sel) + if ns == 0 # return full trace for empty sel + return tr(QO) + else + nd = length(QO.dims) - ρtr, dkeep = _ptrace_oper(QO.data, QO.dims, sel) - return QuantumObject(ρtr, dims = dkeep) + (any(>(nd), sel) || any(<(1), sel)) && throw( + ArgumentError("Invalid indices in `sel`: $(sel), the given QuantumObject only have $(nd) sub-systems"), + ) + (ns != length(unique(sel))) && throw(ArgumentError("Duplicate selection indices in `sel`: $(sel)")) + (nd == 1) && return QO + end + + _sort_sel = sort(SVector{length(sel),Int}(sel)) + ρtr, dkeep = _ptrace_oper(QO.data, QO.dims, _sort_sel) + return QuantumObject(ρtr, type = Operator, dims = dkeep) end -ptrace(QO::QuantumObject, sel::Int) = ptrace(QO, [sel]) +ptrace(QO::QuantumObject, sel::Int) = ptrace(QO, SVector(sel)) + +function _ptrace_ket(QO::AbstractArray, dims::Union{SVector,MVector}, sel) + nd = length(dims) -function _ptrace_ket(QO::AbstractArray{T1}, dims::Vector{<:Integer}, sel::Vector{T2}) where {T1,T2<:Integer} - rd = dims - nd = length(rd) + nd == 1 && return QO, dims - nd == 1 && return QO, rd + qtrace = filter(i -> i ∉ sel, 1:nd) + dkeep = dims[sel] + dtrace = dims[qtrace] + nt = length(dtrace) - dkeep = rd[sel] - qtrace = filter!(e -> e ∉ sel, Vector(1:nd)) - dtrace = @view(rd[qtrace]) + # Concatenate qtrace and sel without losing the length information + # Tuple(qtrace..., sel...) + qtrace_sel = ntuple(Val(nd)) do i + if i <= nt + @inbounds qtrace[i] + else + @inbounds sel[i-nt] + end + end - vmat = reshape(QO, reverse(rd)...) - topermute = nd + 1 .- vcat(sel, qtrace) - vmat = PermutedDimsArray(vmat, topermute) + vmat = reshape(QO, reverse(dims)...) + topermute = reverse(nd + 1 .- qtrace_sel) + vmat = permutedims(vmat, topermute) # TODO: use PermutedDimsArray when Julia v1.11.0 is released vmat = reshape(vmat, prod(dkeep), prod(dtrace)) return vmat * vmat', dkeep end -function _ptrace_oper(QO::AbstractArray{T1}, dims::Vector{<:Integer}, sel::Vector{T2}) where {T1,T2<:Integer} - rd = dims - nd = length(rd) - - nd == 1 && return QO, rd - - dkeep = rd[sel] - qtrace = filter!(e -> e ∉ sel, Vector(1:nd)) - dtrace = @view(rd[qtrace]) - - ρmat = reshape(QO, reverse!(repeat(rd, 2))...) - topermute = 2 * nd + 1 .- vcat(qtrace, qtrace .+ nd, sel, sel .+ nd) - reverse!(topermute) - ρmat = PermutedDimsArray(ρmat, topermute) - - ## TODO: Check if it works always +function _ptrace_oper(QO::AbstractArray, dims::Union{SVector,MVector}, sel) + nd = length(dims) + + nd == 1 && return QO, dims + + qtrace = filter(i -> i ∉ sel, 1:nd) + dkeep = dims[sel] + dtrace = dims[qtrace] + nk = length(dkeep) + nt = length(dtrace) + _2_nt = 2 * nt + + # Concatenate qtrace and sel without losing the length information + # Tuple(qtrace..., sel...) + qtrace_sel = ntuple(Val(2 * nd)) do i + if i <= nt + @inbounds qtrace[i] + elseif i <= _2_nt + @inbounds qtrace[i-nt] + nd + elseif i <= _2_nt + nk + @inbounds sel[i-_2_nt] + else + @inbounds sel[i-_2_nt-nk] + nd + end + end - # ρmat = row_major_reshape(ρmat, prod(dtrace), prod(dtrace), prod(dkeep), prod(dkeep)) - # res = dropdims(mapslices(tr, ρmat, dims=(1,2)), dims=(1,2)) + ρmat = reshape(QO, reverse(vcat(dims, dims))...) + topermute = reverse(2 * nd + 1 .- qtrace_sel) + ρmat = permutedims(ρmat, topermute) # TODO: use PermutedDimsArray when Julia v1.11.0 is released ρmat = reshape(ρmat, prod(dkeep), prod(dkeep), prod(dtrace), prod(dtrace)) - res = dropdims(mapslices(tr, ρmat, dims = (3, 4)), dims = (3, 4)) + res = map(tr, eachslice(ρmat, dims = (1, 2))) return res, dkeep end @@ -620,7 +670,7 @@ get_data(A::QuantumObject) = A.data Get the coherence value ``\alpha`` by measuring the expectation value of the destruction operator ``\hat{a}`` on a state ``\ket{\psi}`` or a density matrix ``\hat{\rho}``. -It returns both ``\alpha`` and the corresponding state with the coherence removed: ``\ket{\delta_\alpha} = \exp ( \bar{\alpha} \hat{a} - \alpha \hat{a}^\dagger ) \ket{\psi}`` for a pure state, and ``\hat{\rho_\alpha} = \exp ( \bar{\alpha} \hat{a} - \alpha \hat{a}^\dagger ) \hat{\rho} \exp ( -\bar{\alpha} \hat{a} + \alpha \hat{a}^\dagger )`` for a density matrix. These states correspond to the quantum fluctuations around the coherent state ``\ket{\alpha}`` or ``\dyad{\alpha}``. +It returns both ``\alpha`` and the corresponding state with the coherence removed: ``\ket{\delta_\alpha} = \exp ( \alpha^* \hat{a} - \alpha \hat{a}^\dagger ) \ket{\psi}`` for a pure state, and ``\hat{\rho_\alpha} = \exp ( \alpha^* \hat{a} - \alpha \hat{a}^\dagger ) \hat{\rho} \exp ( -\bar{\alpha} \hat{a} + \alpha \hat{a}^\dagger )`` for a density matrix. These states correspond to the quantum fluctuations around the coherent state ``\ket{\alpha}`` or ``|\alpha\rangle\langle\alpha|``. """ function get_coherence(ψ::QuantumObject{<:AbstractArray,KetQuantumObject}) a = destroy(prod(ψ.dims)) @@ -639,7 +689,7 @@ function get_coherence(ρ::QuantumObject{<:AbstractArray,OperatorQuantumObject}) end @doc raw""" - permute(A::QuantumObject, order::Vector{Int}) + permute(A::QuantumObject, order::Union{AbstractVector{Int},Tuple}) Permute the tensor structure of a [`QuantumObject`](@ref) `A` according to the specified `order` list @@ -647,7 +697,7 @@ Note that this method currently works for [`Ket`](@ref), [`Bra`](@ref), and [`Op # Examples -If `order = [2, 1, 3]`, the Hilbert space structure will be re-arranged: H₁ ⊗ H₂ ⊗ H₃ → H₂ ⊗ H₁ ⊗ H₃. +If `order = [2, 1, 3]`, the Hilbert space structure will be re-arranged: ``\mathcal{H}_1 \otimes \mathcal{H}_2 \otimes \mathcal{H}_3 \rightarrow \mathcal{H}_2 \otimes \mathcal{H}_1 \otimes \mathcal{H}_3``. ``` julia> ψ1 = fock(2, 0) @@ -657,31 +707,42 @@ julia> ψ_123 = tensor(ψ1, ψ2, ψ3) julia> permute(ψ_123, [2, 1, 3]) ≈ tensor(ψ2, ψ1, ψ3) true ``` + +!!! warning "Beware of type-stability!" + It is highly recommended to use `permute(A, order)` with `order` as `Tuple` or `SVector` to keep type stability. See the [related Section](@ref doc:Type-Stability) about type stability for more details. """ function permute( A::QuantumObject{<:AbstractArray{T},ObjType}, - order::AbstractVector{Int}, + order::Union{AbstractVector{Int},Tuple}, ) where {T,ObjType<:Union{KetQuantumObject,BraQuantumObject,OperatorQuantumObject}} (length(order) != length(A.dims)) && throw(ArgumentError("The order list must have the same length as the number of subsystems (A.dims)")) !isperm(order) && throw(ArgumentError("$(order) is not a valid permutation of the subsystems (A.dims)")) + _non_static_array_warning("order", order) + + order_svector = SVector{length(order),Int}(order) # convert it to SVector for performance + # obtain the arguments: dims for reshape; perm for PermutedDimsArray - dims, perm = _dims_and_perm(A.type, A.dims, order, length(order)) + dims, perm = _dims_and_perm(A.type, A.dims, order_svector, length(order_svector)) - return QuantumObject(reshape(PermutedDimsArray(reshape(A.data, dims...), perm), size(A)), A.type, A.dims[order]) + return QuantumObject( + reshape(permutedims(reshape(A.data, dims...), Tuple(perm)), size(A)), + A.type, + A.dims[order_svector], + ) end function _dims_and_perm( ::ObjType, - dims::AbstractVector{Int}, + dims::SVector{N,Int}, order::AbstractVector{Int}, L::Int, -) where {ObjType<:Union{KetQuantumObject,BraQuantumObject}} - return reverse(dims), reverse!((L + 1) .- order) +) where {ObjType<:Union{KetQuantumObject,BraQuantumObject},N} + return reverse(dims), reverse((L + 1) .- order) end -function _dims_and_perm(::OperatorQuantumObject, dims::AbstractVector{Int}, order::AbstractVector{Int}, L::Int) - return reverse!([dims; dims]), reverse!((2 * L + 1) .- [order; order .+ L]) +function _dims_and_perm(::OperatorQuantumObject, dims::SVector{N,Int}, order::AbstractVector{Int}, L::Int) where {N} + return reverse(vcat(dims, dims)), reverse((2 * L + 1) .- vcat(order, order .+ L)) end diff --git a/src/qobj/eigsolve.jl b/src/qobj/eigsolve.jl index b22f37a5..dee88be2 100644 --- a/src/qobj/eigsolve.jl +++ b/src/qobj/eigsolve.jl @@ -6,11 +6,11 @@ export EigsolveResult export eigenenergies, eigenstates, eigsolve, eigsolve_al @doc raw""" - struct EigsolveResult{T1<:Vector{<:Number}, T2<:AbstractMatrix{<:Number}, ObjType<:Union{Nothing,OperatorQuantumObject,SuperOperatorQuantumObject}} + struct EigsolveResult{T1<:Vector{<:Number}, T2<:AbstractMatrix{<:Number}, ObjType<:Union{Nothing,OperatorQuantumObject,SuperOperatorQuantumObject},N} values::T1 vectors::T2 type::ObjType - dims::Vector{Int} + dims::SVector{N,Int} iter::Int numops::Int converged::Bool @@ -22,7 +22,7 @@ A struct containing the eigenvalues, the eigenvectors, and some information from - `values::AbstractVector`: the eigenvalues - `vectors::AbstractMatrix`: the transformation matrix (eigenvectors) - `type::Union{Nothing,QuantumObjectType}`: the type of [`QuantumObject`](@ref), or `nothing` means solving eigen equation for general matrix -- `dims::Vector{Int}`: the `dims` of [`QuantumObject`](@ref) +- `dims::SVector`: the `dims` of [`QuantumObject`](@ref) - `iter::Int`: the number of iteration during the solving process - `numops::Int` : number of times the linear map was applied in krylov methods - `converged::Bool`: Whether the result is converged @@ -54,11 +54,12 @@ struct EigsolveResult{ T1<:Vector{<:Number}, T2<:AbstractMatrix{<:Number}, ObjType<:Union{Nothing,OperatorQuantumObject,SuperOperatorQuantumObject}, + N, } values::T1 vectors::T2 type::ObjType - dims::Vector{Int} + dims::SVector{N,Int} iter::Int numops::Int converged::Bool @@ -83,156 +84,27 @@ function Base.show(io::IO, res::EigsolveResult) return show(io, MIME("text/plain"), res.vectors) end -if VERSION < v"1.10" - for (hseqr, elty) in ((:zhseqr_, :ComplexF64), (:chseqr_, :ComplexF32)) - @eval begin - # * .. Scalar Arguments .. - # CHARACTER JOB, COMPZ - # INTEGER N, ILO, IHI, LWORK, LDH, LDZ, INFO - # * .. - # * .. Array Arguments .. - # COMPLEX*16 H( LDH, * ), Z( LDZ, * ), WORK( * ) - function hseqr!( - job::AbstractChar, - compz::AbstractChar, - ilo::Int, - ihi::Int, - H::AbstractMatrix{$elty}, - Z::AbstractMatrix{$elty}, - ) - require_one_based_indexing(H, Z) - chkstride1(H) - n = checksquare(H) - checksquare(Z) == n || throw(DimensionMismatch()) - ldh = max(1, stride(H, 2)) - ldz = max(1, stride(Z, 2)) - w = similar(H, $elty, n) - work = Vector{$elty}(undef, 1) - lwork = BlasInt(-1) - info = Ref{BlasInt}() - for i in 1:2 # first call returns lwork as work[1] - ccall( - (@blasfunc($hseqr), libblastrampoline), - Cvoid, - ( - Ref{UInt8}, - Ref{UInt8}, - Ref{BlasInt}, - Ref{BlasInt}, - Ref{BlasInt}, - Ptr{$elty}, - Ref{BlasInt}, - Ptr{$elty}, - Ptr{$elty}, - Ref{BlasInt}, - Ptr{$elty}, - Ref{BlasInt}, - Ptr{BlasInt}, - ), - job, - compz, - n, - ilo, - ihi, - H, - ldh, - w, - Z, - ldz, - work, - lwork, - info, - ) - chklapackerror(info[]) - if i == 1 - lwork = BlasInt(real(work[1])) - resize!(work, lwork) - end - end - return H, Z, w - end - end - end +struct ArnoldiLindbladIntegratorMap{T,TS,TI} <: AbstractLinearMap{T,TS} + elty::Type{T} + size::TS + integrator::TI +end - for (hseqr, elty) in ((:dhseqr_, :Float64), (:shseqr_, :Float32)) - @eval begin - # * .. Scalar Arguments .. - # CHARACTER JOB, COMPZ - # INTEGER N, ILO, IHI, LWORK, LDH, LDZ, INFO - # * .. - # * .. Array Arguments .. - # COMPLEX*16 H( LDH, * ), Z( LDZ, * ), WORK( * ) - function hseqr!( - job::AbstractChar, - compz::AbstractChar, - ilo::Int, - ihi::Int, - H::AbstractMatrix{$elty}, - Z::AbstractMatrix{$elty}, - ) - require_one_based_indexing(H, Z) - chkstride1(H) - n = checksquare(H) - checksquare(Z) == n || throw(DimensionMismatch()) - ldh = max(1, stride(H, 2)) - ldz = max(1, stride(Z, 2)) - wr = similar(H, $elty, n) - wi = similar(H, $elty, n) - work = Vector{$elty}(undef, 1) - lwork = BlasInt(-1) - info = Ref{BlasInt}() - for i in 1:2 # first call returns lwork as work[1] - ccall( - (@blasfunc($hseqr), libblastrampoline), - Cvoid, - ( - Ref{UInt8}, - Ref{UInt8}, - Ref{BlasInt}, - Ref{BlasInt}, - Ref{BlasInt}, - Ptr{$elty}, - Ref{BlasInt}, - Ptr{$elty}, - Ptr{$elty}, - Ptr{$elty}, - Ref{BlasInt}, - Ptr{$elty}, - Ref{BlasInt}, - Ptr{BlasInt}, - ), - job, - compz, - n, - ilo, - ihi, - H, - ldh, - wr, - wi, - Z, - ldz, - work, - lwork, - info, - ) - chklapackerror(info[]) - if i == 1 - lwork = BlasInt(real(work[1])) - resize!(work, lwork) - end - end - return H, Z, complex.(wr, wi) - end - end - end - hseqr!(H::StridedMatrix{T}, Z::StridedMatrix{T}) where {T<:BlasFloat} = hseqr!('S', 'V', 1, size(H, 1), H, Z) - hseqr!(H::StridedMatrix{T}) where {T<:BlasFloat} = hseqr!('S', 'I', 1, size(H, 1), H, similar(H)) +function LinearAlgebra.mul!(y::AbstractVector, A::ArnoldiLindbladIntegratorMap, x::AbstractVector) + reinit!(A.integrator, x) + solve!(A.integrator) + return copyto!(y, A.integrator.u) end -function _map_ldiv(linsolve, y, x) - linsolve.b .= x - return y .= LinearSolve.solve!(linsolve).u +struct EigsolveInverseMap{T,TS,TI} <: AbstractLinearMap{T,TS} + elty::Type{T} + size::TS + linsolve::TI +end + +function LinearAlgebra.mul!(y::AbstractVector, A::EigsolveInverseMap, x::AbstractVector) + A.linsolve.b .= x + return copyto!(y, solve!(A.linsolve).u) end function _permuteschur!( @@ -271,7 +143,7 @@ function _eigsolve( A, b::AbstractVector{T}, type::ObjType, - dims::Vector{Int}, + dims::SVector, k::Int = 1, m::Int = max(20, 2 * k + 1); tol::Real = 1e-8, @@ -360,13 +232,23 @@ function _eigsolve( end @doc raw""" - function eigsolve(A::QuantumObject; v0::Union{Nothing,AbstractVector}=nothing, - sigma::Union{Nothing, Real}=nothing, k::Int = 1, - krylovdim::Int = max(20, 2*k+1), tol::Real = 1e-8, maxiter::Int = 200, - solver::Union{Nothing, LinearSolve.SciMLLinearSolveAlgorithm} = nothing, kwargs...) + eigsolve(A::QuantumObject; + v0::Union{Nothing,AbstractVector}=nothing, + sigma::Union{Nothing, Real}=nothing, + k::Int = 1, + krylovdim::Int = max(20, 2*k+1), + tol::Real = 1e-8, + maxiter::Int = 200, + solver::Union{Nothing, SciMLLinearSolveAlgorithm} = nothing, + kwargs...) Solve for the eigenvalues and eigenvectors of a matrix `A` using the Arnoldi method. -The keyword arguments are passed to the linear solver. + +# Notes +- For more details about `solver` and extra `kwargs`, please refer to [`LinearSolve.jl`](https://docs.sciml.ai/LinearSolve/stable/) + +# Returns +- `EigsolveResult`: A struct containing the eigenvalues, the eigenvectors, and some information about the eigsolver """ function eigsolve( A::QuantumObject{<:AbstractMatrix}; @@ -376,7 +258,7 @@ function eigsolve( krylovdim::Int = max(20, 2 * k + 1), tol::Real = 1e-8, maxiter::Int = 200, - solver::Union{Nothing,LinearSolve.SciMLLinearSolveAlgorithm} = nothing, + solver::Union{Nothing,SciMLLinearSolveAlgorithm} = nothing, kwargs..., ) return eigsolve( @@ -398,19 +280,21 @@ function eigsolve( A; v0::Union{Nothing,AbstractVector} = nothing, type::Union{Nothing,OperatorQuantumObject,SuperOperatorQuantumObject} = nothing, - dims::Vector{Int} = Int[], + dims = SVector{0,Int}(), sigma::Union{Nothing,Real} = nothing, k::Int = 1, krylovdim::Int = max(20, 2 * k + 1), tol::Real = 1e-8, maxiter::Int = 200, - solver::Union{Nothing,LinearSolve.SciMLLinearSolveAlgorithm} = nothing, + solver::Union{Nothing,SciMLLinearSolveAlgorithm} = nothing, kwargs..., ) T = eltype(A) isH = ishermitian(A) v0 === nothing && (v0 = normalize!(rand(T, size(A, 1)))) + dims = SVector(dims) + if sigma === nothing res = _eigsolve(A, v0, type, dims, k, krylovdim, tol = tol, maxiter = maxiter) vals = res.values @@ -427,7 +311,8 @@ function eigsolve( prob = LinearProblem(Aₛ, v0) linsolve = init(prob, solver; kwargs2...) - Amap = LinearMap{T}((y, x) -> _map_ldiv(linsolve, y, x), length(v0)) + + Amap = EigsolveInverseMap(T, size(A), linsolve) res = _eigsolve(Amap, v0, type, dims, k, krylovdim, tol = tol, maxiter = maxiter) vals = @. (1 + sigma * res.values) / res.values @@ -438,7 +323,7 @@ end @doc raw""" eigsolve_al(H::QuantumObject, - T::Real, c_ops::AbstractVector=[]; + T::Real, c_ops::Union{Nothing,AbstractVector}=nothing; alg::OrdinaryDiffEqAlgorithm=Tsit5(), H_t::Union{Nothing,Function}=nothing, params::NamedTuple=NamedTuple(), @@ -454,7 +339,7 @@ Solve the eigenvalue problem for a Liouvillian superoperator `L` using the Arnol # Arguments - `H`: The Hamiltonian (or directly the Liouvillian) of the system. - `T`: The time at which to evaluate the time evolution -- `c_ops`: A vector of collapse operators +- `c_ops`: A vector of collapse operators. Default is `nothing` meaning the system is closed. - `alg`: The differential equation solver algorithm - `H_t`: A function `H_t(t)` that returns the additional term at time `t` - `params`: A dictionary of additional parameters @@ -465,18 +350,20 @@ Solve the eigenvalue problem for a Liouvillian superoperator `L` using the Arnol - `eigstol`: The tolerance for the eigsolver - `kwargs`: Additional keyword arguments passed to the differential equation solver +# Notes +- For more details about `alg` please refer to [`DifferentialEquations.jl` (ODE Solvers)](https://docs.sciml.ai/DiffEqDocs/stable/solvers/ode_solve/) +- For more details about `kwargs` please refer to [`DifferentialEquations.jl` (Keyword Arguments)](https://docs.sciml.ai/DiffEqDocs/stable/basics/common_solver_opts/) + # Returns - `EigsolveResult`: A struct containing the eigenvalues, the eigenvectors, and some information about the eigsolver # References -- [1] Minganti, F., & Huybrechts, D. (2022). Arnoldi-Lindblad time evolution: -Faster-than-the-clock algorithm for the spectrum of time-independent -and Floquet open quantum systems. Quantum, 6, 649. +- [1] Minganti, F., & Huybrechts, D. (2022). Arnoldi-Lindblad time evolution: Faster-than-the-clock algorithm for the spectrum of time-independent and Floquet open quantum systems. Quantum, 6, 649. """ function eigsolve_al( H::QuantumObject{MT1,HOpType}, T::Real, - c_ops::Vector{QuantumObject{MT2,COpType}} = Vector{QuantumObject{MT1,HOpType}}([]); + c_ops::Union{Nothing,AbstractVector} = nothing; alg::OrdinaryDiffEqAlgorithm = Tsit5(), H_t::Union{Nothing,Function} = nothing, params::NamedTuple = NamedTuple(), @@ -486,12 +373,7 @@ function eigsolve_al( maxiter::Int = 200, eigstol::Real = 1e-6, kwargs..., -) where { - MT1<:AbstractMatrix, - MT2<:AbstractMatrix, - HOpType<:Union{OperatorQuantumObject,SuperOperatorQuantumObject}, - COpType<:Union{OperatorQuantumObject,SuperOperatorQuantumObject}, -} +) where {MT1<:AbstractMatrix,HOpType<:Union{OperatorQuantumObject,SuperOperatorQuantumObject}} L = liouvillian(H, c_ops) prob = mesolveProblem( L, @@ -507,13 +389,7 @@ function eigsolve_al( # prog = ProgressUnknown(desc="Applications:", showspeed = true, enabled=progress) - function arnoldi_lindblad_solve(ρ) - reinit!(integrator, ρ) - solve!(integrator) - return integrator.u - end - - Lmap = LinearMap{eltype(MT1)}(arnoldi_lindblad_solve, size(L, 1), ismutating = false) + Lmap = ArnoldiLindbladIntegratorMap(eltype(MT1), size(L), integrator) res = _eigsolve(Lmap, mat2vec(ρ0), L.type, L.dims, k, krylovdim, maxiter = maxiter, tol = eigstol) # finish!(prog) diff --git a/src/qobj/functions.jl b/src/qobj/functions.jl index 6dde41d6..de729d08 100644 --- a/src/qobj/functions.jl +++ b/src/qobj/functions.jl @@ -17,7 +17,7 @@ ket2dm(ψ::QuantumObject{<:AbstractArray{T},KetQuantumObject}) where {T} = ψ * ket2dm(ρ::QuantumObject{<:AbstractArray{T},OperatorQuantumObject}) where {T} = ρ @doc raw""" - expect(O::QuantumObject, ψ::QuantumObject) + expect(O::QuantumObject, ψ::Union{QuantumObject,Vector{QuantumObject}}) Expectation value of the [`Operator`](@ref) `O` with the state `ψ`. The state can be a [`Ket`](@ref), [`Bra`](@ref) or [`Operator`](@ref). @@ -25,7 +25,9 @@ If `ψ` is a [`Ket`](@ref) or [`Bra`](@ref), the function calculates ``\langle\p If `ψ` is a density matrix ([`Operator`](@ref)), the function calculates ``\textrm{Tr} \left[ \hat{O} \hat{\psi} \right]`` -The function returns a real number if `O` is hermitian, and returns a complex number otherwise. +The function returns a real number if `O` is of `Hermitian` type or `Symmetric` type, and returns a complex number otherwise. You can make an operator `O` hermitian by using `Hermitian(O)`. + +Note that `ψ` can also be given as a list of [`QuantumObject`](@ref), it returns a list of expectation values. # Examples @@ -34,46 +36,71 @@ julia> ψ = 1 / √2 * (fock(10,2) + fock(10,4)); julia> a = destroy(10); -julia> expect(a' * a, ψ) ≈ 3 -true +julia> expect(a' * a, ψ) |> round +3.0 + 0.0im + +julia> expect(Hermitian(a' * a), ψ) |> round +3.0 ``` """ function expect( O::QuantumObject{<:AbstractArray{T1},OperatorQuantumObject}, ψ::QuantumObject{<:AbstractArray{T2},KetQuantumObject}, ) where {T1,T2} - ψd = ψ.data - Od = O.data - return ishermitian(O) ? real(dot(ψd, Od, ψd)) : dot(ψd, Od, ψd) + return dot(ψ.data, O.data, ψ.data) end function expect( O::QuantumObject{<:AbstractArray{T1},OperatorQuantumObject}, ψ::QuantumObject{<:AbstractArray{T2},BraQuantumObject}, ) where {T1,T2} - ψd = ψ.data' - Od = O.data - return ishermitian(O) ? real(dot(ψd, Od, ψd)) : dot(ψd, Od, ψd) + return expect(O, ψ') end function expect( O::QuantumObject{<:AbstractArray{T1},OperatorQuantumObject}, ρ::QuantumObject{<:AbstractArray{T2},OperatorQuantumObject}, ) where {T1,T2} - return ishermitian(O) ? real(tr(O * ρ)) : tr(O * ρ) + return tr(O * ρ) +end +function expect( + O::QuantumObject{<:Union{<:Hermitian{TF},<:Symmetric{TR}},OperatorQuantumObject}, + ψ::QuantumObject{<:AbstractArray{T2},KetQuantumObject}, +) where {TF<:Number,TR<:Real,T2} + return real(dot(ψ.data, O.data, ψ.data)) +end +function expect( + O::QuantumObject{<:Union{<:Hermitian{TF},<:Symmetric{TR}},OperatorQuantumObject}, + ψ::QuantumObject{<:AbstractArray{T2},BraQuantumObject}, +) where {TF<:Number,TR<:Real,T2} + return real(expect(O, ψ')) +end +function expect( + O::QuantumObject{<:Union{<:Hermitian{TF},<:Symmetric{TR}},OperatorQuantumObject}, + ρ::QuantumObject{<:AbstractArray{T2},OperatorQuantumObject}, +) where {TF<:Number,TR<:Real,T2} + return real(tr(O * ρ)) +end +function expect(O::QuantumObject{<:AbstractArray{T1},OperatorQuantumObject}, ρ::Vector{<:QuantumObject}) where {T1} + _expect = _ρ -> expect(O, _ρ) + return _expect.(ρ) end @doc raw""" - variance(O::QuantumObject, ψ::QuantumObject) + variance(O::QuantumObject, ψ::Union{QuantumObject,Vector{QuantumObject}}) Variance of the [`Operator`](@ref) `O`: ``\langle\hat{O}^2\rangle - \langle\hat{O}\rangle^2``, where ``\langle\hat{O}\rangle`` is the expectation value of `O` with the state `ψ` (see also [`expect`](@ref)), and the state `ψ` can be a [`Ket`](@ref), [`Bra`](@ref) or [`Operator`](@ref). The function returns a real number if `O` is hermitian, and returns a complex number otherwise. + +Note that `ψ` can also be given as a list of [`QuantumObject`](@ref), it returns a list of expectation values. """ variance( O::QuantumObject{<:AbstractArray{T1},OperatorQuantumObject}, ψ::QuantumObject{<:AbstractArray{T2}}, ) where {T1,T2} = expect(O^2, ψ) - expect(O, ψ)^2 +variance(O::QuantumObject{<:AbstractArray{T1},OperatorQuantumObject}, ψ::Vector{<:QuantumObject}) where {T1} = + expect(O^2, ψ) .- expect(O, ψ) .^ 2 @doc raw""" sparse_to_dense(A::QuantumObject) @@ -81,12 +108,16 @@ variance( Converts a sparse QuantumObject to a dense QuantumObject. """ sparse_to_dense(A::QuantumObject{<:AbstractVecOrMat}) = QuantumObject(sparse_to_dense(A.data), A.type, A.dims) -sparse_to_dense(A::MT) where {MT<:AbstractSparseMatrix} = Array(A) +sparse_to_dense(A::MT) where {MT<:AbstractSparseArray} = Array(A) for op in (:Transpose, :Adjoint) @eval sparse_to_dense(A::$op{T,<:AbstractSparseMatrix}) where {T<:BlasFloat} = Array(A) end sparse_to_dense(A::MT) where {MT<:AbstractArray} = A +sparse_to_dense(::Type{T}, A::AbstractSparseArray) where {T<:Number} = Array{T}(A) +sparse_to_dense(::Type{T1}, A::AbstractArray{T2}) where {T1<:Number,T2<:Number} = Array{T1}(A) +sparse_to_dense(::Type{T}, A::AbstractArray{T}) where {T<:Number} = A + function sparse_to_dense(::Type{M}) where {M<:SparseMatrixCSC} T = M par = T.parameters @@ -160,11 +191,15 @@ Quantum Object: type=Operator dims=[20, 20] size=(400, 400) ishermitian= ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⠦ ``` """ -function LinearAlgebra.kron( +LinearAlgebra.kron( A::QuantumObject{<:AbstractArray{T1},OpType}, B::QuantumObject{<:AbstractArray{T2},OpType}, -) where {T1,T2,OpType<:Union{KetQuantumObject,BraQuantumObject,OperatorQuantumObject}} - return QuantumObject(kron(A.data, B.data), A.type, vcat(A.dims, B.dims)) +) where {T1,T2,OpType<:Union{KetQuantumObject,BraQuantumObject,OperatorQuantumObject}} = + QuantumObject(kron(A.data, B.data), A.type, vcat(A.dims, B.dims)) +LinearAlgebra.kron(A::QuantumObject) = A +function LinearAlgebra.kron(A::Vector{<:QuantumObject}) + @warn "`tensor(A)` or `kron(A)` with `A` is a `Vector` can hurt performance. Try to use `tensor(A...)` or `kron(A...)` instead." + return kron(A...) end @doc raw""" diff --git a/src/qobj/operator_sum.jl b/src/qobj/operator_sum.jl index 6f3642f5..909e27db 100644 --- a/src/qobj/operator_sum.jl +++ b/src/qobj/operator_sum.jl @@ -3,7 +3,7 @@ export OperatorSum @doc raw""" struct OperatorSum -A structure to represent a sum of operators ``\sum_i c_i \hat{O}_i`` with a list of coefficients ``c_i`` and a list of operators ``\hat{O}_i``. +A constructor to represent a sum of operators ``\sum_i c_i \hat{O}_i`` with a list of coefficients ``c_i`` and a list of operators ``\hat{O}_i``. This is very useful when we have to update only the coefficients, without allocating memory by performing the sum of the operators. """ @@ -18,7 +18,7 @@ struct OperatorSum{CT<:Vector{<:Number},OT<:AbstractVector} <: AbstractQuantumOb mapreduce(x -> size(x) == size_1, &, operators) || throw(DimensionMismatch("All the operators must have the same dimensions.")) T = promote_type( - mapreduce(x -> eltype(x.data), promote_type, operators), + mapreduce(x -> eltype(x), promote_type, operators), mapreduce(eltype, promote_type, coefficients), ) coefficients2 = T.(coefficients) @@ -47,3 +47,7 @@ end end return y end + +function liouvillian(A::OperatorSum, Id_cache = I(prod(A.operators[1].dims))) + return OperatorSum(A.coefficients, liouvillian.(A.operators, Ref(Id_cache))) +end diff --git a/src/qobj/operators.jl b/src/qobj/operators.jl index 255014e7..feb15780 100644 --- a/src/qobj/operators.jl +++ b/src/qobj/operators.jl @@ -13,24 +13,29 @@ export tunneling export qft @doc raw""" - rand_unitary(dimensions, distribution=:haar) + rand_unitary(dimensions, distribution=Val(:haar)) Returns a random unitary [`QuantumObject`](@ref). The `dimensions` can be either the following types: - `dimensions::Int`: Number of basis states in the Hilbert space. -- `dimensions::Vector{Int}`: list of dimensions representing the each number of basis in the subsystems. +- `dimensions::Union{AbstractVector{Int},Tuple}`: list of dimensions representing the each number of basis in the subsystems. The `distribution` specifies which of the method used to obtain the unitary matrix: - `:haar`: Haar random unitary matrix using the algorithm from reference 1 -- `:exp`: Uses ``\exp(-iH)``, where ``H`` is a randomly generated Hermitian operator. +- `:exp`: Uses ``\exp(-i\hat{H})``, where ``\hat{H}`` is a randomly generated Hermitian operator. # References 1. [F. Mezzadri, How to generate random matrices from the classical compact groups, arXiv:math-ph/0609050 (2007)](https://arxiv.org/abs/math-ph/0609050) + +!!! warning "Beware of type-stability!" + If you want to keep type stability, it is recommended to use `rand_unitary(dimensions, Val(distribution))` instead of `rand_unitary(dimensions, distribution)`. Also, put `dimensions` as `Tuple` or `SVector`. See [this link](https://docs.julialang.org/en/v1/manual/performance-tips/#man-performance-value-type) and the [related Section](@ref doc:Type-Stability) about type stability for more details. """ -rand_unitary(dimensions::Int, distribution::Symbol = :haar) = rand_unitary([dimensions], Val(distribution)) -rand_unitary(dimensions::Vector{Int}, distribution::Symbol = :haar) = rand_unitary(dimensions, Val(distribution)) -function rand_unitary(dimensions::Vector{Int}, ::Val{:haar}) +rand_unitary(dimensions::Int, distribution::Union{Symbol,Val} = Val(:haar)) = + rand_unitary(SVector(dimensions), makeVal(distribution)) +rand_unitary(dimensions::Union{AbstractVector{Int},Tuple}, distribution::Union{Symbol,Val} = Val(:haar)) = + rand_unitary(dimensions, makeVal(distribution)) +function rand_unitary(dimensions::Union{AbstractVector{Int},Tuple}, ::Val{:haar}) N = prod(dimensions) # generate N x N matrix Z of complex standard normal random variates @@ -43,9 +48,9 @@ function rand_unitary(dimensions::Vector{Int}, ::Val{:haar}) # Because inv(Λ) ⋅ R has real and strictly positive elements, Q · Λ is therefore Haar distributed. Λ = diag(R) # take the diagonal elements of R Λ ./= abs.(Λ) # rescaling the elements - return QuantumObject(dense_to_sparse(Q * Diagonal(Λ)); type = Operator, dims = dimensions) + return QuantumObject(sparse_to_dense(Q * Diagonal(Λ)); type = Operator, dims = dimensions) end -function rand_unitary(dimensions::Vector{Int}, ::Val{:exp}) +function rand_unitary(dimensions::Union{AbstractVector{Int},Tuple}, ::Val{:exp}) N = prod(dimensions) # generate N x N matrix Z of complex standard normal random variates @@ -54,16 +59,17 @@ function rand_unitary(dimensions::Vector{Int}, ::Val{:exp}) # generate Hermitian matrix H = QuantumObject((Z + Z') / 2; type = Operator, dims = dimensions) - return exp(-1.0im * H) + return sparse_to_dense(exp(-1.0im * H)) end -rand_unitary(dimensions::Vector{Int}, ::Val{T}) where {T} = throw(ArgumentError("Invalid distribution: $(T)")) +rand_unitary(dimensions::Union{AbstractVector{Int},Tuple}, ::Val{T}) where {T} = + throw(ArgumentError("Invalid distribution: $(T)")) @doc raw""" commutator(A::QuantumObject, B::QuantumObject; anti::Bool=false) Return the commutator (or `anti`-commutator) of the two [`QuantumObject`](@ref): -- commutator (`anti=false`): ``AB-BA`` -- anticommutator (`anti=true`): ``AB+BA`` +- commutator (`anti=false`): ``\hat{A}\hat{B}-\hat{B}\hat{A}`` +- anticommutator (`anti=true`): ``\hat{A}\hat{B}+\hat{B}\hat{A}`` Note that `A` and `B` must be [`Operator`](@ref) """ @@ -86,17 +92,17 @@ This operator acts on a fock state as ``\hat{a} \ket{n} = \sqrt{n} \ket{n-1}``. julia> a = destroy(20) Quantum Object: type=Operator dims=[20] size=(20, 20) ishermitian=false 20×20 SparseMatrixCSC{ComplexF64, Int64} with 19 stored entries: -⠈⠢⡀⠀⠀⠀⠀⠀⠀⠀ -⠀⠀⠈⠢⡀⠀⠀⠀⠀⠀ -⠀⠀⠀⠀⠈⠢⡀⠀⠀⠀ -⠀⠀⠀⠀⠀⠀⠈⠢⡀⠀ -⠀⠀⠀⠀⠀⠀⠀⠀⠈⠢ +⎡⠈⠢⡀⠀⠀⠀⠀⠀⠀⠀⎤ +⎢⠀⠀⠈⠢⡀⠀⠀⠀⠀⠀⎥ +⎢⠀⠀⠀⠀⠈⠢⡀⠀⠀⠀⎥ +⎢⠀⠀⠀⠀⠀⠀⠈⠢⡀⠀⎥ +⎣⠀⠀⠀⠀⠀⠀⠀⠀⠈⠢⎦ julia> fock(20, 3)' * a * fock(20, 4) 2.0 + 0.0im ``` """ -destroy(N::Int) = QuantumObject(spdiagm(1 => Array{ComplexF64}(sqrt.(1:N-1))), Operator, [N]) +destroy(N::Int) = QuantumObject(spdiagm(1 => Array{ComplexF64}(sqrt.(1:N-1))), Operator, N) @doc raw""" create(N::Int) @@ -111,17 +117,17 @@ This operator acts on a fock state as ``\hat{a}^\dagger \ket{n} = \sqrt{n+1} \ke julia> a_d = create(20) Quantum Object: type=Operator dims=[20] size=(20, 20) ishermitian=false 20×20 SparseMatrixCSC{ComplexF64, Int64} with 19 stored entries: -⠢⡀⠀⠀⠀⠀⠀⠀⠀⠀ -⠀⠈⠢⡀⠀⠀⠀⠀⠀⠀ -⠀⠀⠀⠈⠢⡀⠀⠀⠀⠀ -⠀⠀⠀⠀⠀⠈⠢⡀⠀⠀ -⠀⠀⠀⠀⠀⠀⠀⠈⠢⡀ +⎡⠢⡀⠀⠀⠀⠀⠀⠀⠀⠀⎤ +⎢⠀⠈⠢⡀⠀⠀⠀⠀⠀⠀⎥ +⎢⠀⠀⠀⠈⠢⡀⠀⠀⠀⠀⎥ +⎢⠀⠀⠀⠀⠀⠈⠢⡀⠀⠀⎥ +⎣⠀⠀⠀⠀⠀⠀⠀⠈⠢⡀⎦ julia> fock(20, 4)' * a_d * fock(20, 3) 2.0 + 0.0im ``` """ -create(N::Int) = QuantumObject(spdiagm(-1 => Array{ComplexF64}(sqrt.(1:N-1))), Operator, [N]) +create(N::Int) = QuantumObject(spdiagm(-1 => Array{ComplexF64}(sqrt.(1:N-1))), Operator, N) @doc raw""" displace(N::Int, α::Number) @@ -162,7 +168,7 @@ Bosonic number operator with Hilbert space cutoff `N`. This operator is defined as ``\hat{N}=\hat{a}^\dagger \hat{a}``, where ``\hat{a}`` is the bosonic annihilation operator. """ -num(N::Int) = QuantumObject(spdiagm(0 => Array{ComplexF64}(0:N-1)), Operator, [N]) +num(N::Int) = QuantumObject(spdiagm(0 => Array{ComplexF64}(0:N-1)), Operator, N) @doc raw""" position(N::Int) @@ -218,22 +224,22 @@ function phase(N::Int, ϕ0::Real = 0) N_list = collect(0:(N-1)) ϕ = ϕ0 .+ (2 * π / N) .* N_list states = [exp.((1.0im * ϕ[m]) .* N_list) ./ sqrt(N) for m in 1:N] - return QuantumObject(sum([ϕ[m] * states[m] * states[m]' for m in 1:N]); type = Operator, dims = [N]) + return QuantumObject(sum([ϕ[m] * states[m] * states[m]' for m in 1:N]); type = Operator, dims = N) end @doc raw""" - jmat(j::Real, which::Symbol) + jmat(j::Real, which::Union{Symbol,Val}) Generate higher-order Spin-`j` operators, where `j` is the spin quantum number and can be a non-negative integer or half-integer The parameter `which` specifies which of the following operator to return. -- `:x`: ``S_x`` -- `:y`: ``S_y`` -- `:z`: ``S_z`` -- `:+`: ``S_+`` -- `:-`: ``S_-`` +- `:x`: ``\hat{S}_x`` +- `:y`: ``\hat{S}_y`` +- `:z`: ``\hat{S}_z`` +- `:+`: ``\hat{S}_+`` +- `:-`: ``\hat{S}_-`` -Note that if the parameter `which` is not specified, returns a set of Spin-`j` operators: ``(S_x, S_y, S_z)`` +Note that if the parameter `which` is not specified, returns a set of Spin-`j` operators: ``(\hat{S}_x, \hat{S}_y, \hat{S}_z)`` # Examples ``` @@ -248,7 +254,18 @@ Quantum Object: type=Operator dims=[2] size=(2, 2) ishermitian=false 2×2 SparseMatrixCSC{ComplexF64, Int64} with 1 stored entry: ⋅ ⋅ 1.0+0.0im ⋅ + +julia> jmat(1.5, Val(:z)) +Quantum Object: type=Operator dims=[4] size=(4, 4) ishermitian=true +4×4 SparseMatrixCSC{ComplexF64, Int64} with 4 stored entries: + 1.5+0.0im ⋅ ⋅ ⋅ + ⋅ 0.5+0.0im ⋅ ⋅ + ⋅ ⋅ -0.5+0.0im ⋅ + ⋅ ⋅ ⋅ -1.5+0.0im ``` + +!!! warning "Beware of type-stability!" + If you want to keep type stability, it is recommended to use `jmat(j, Val(which))` instead of `jmat(j, which)`. See [this link](https://docs.julialang.org/en/v1/manual/performance-tips/#man-performance-value-type) and the [related Section](@ref doc:Type-Stability) about type stability for more details. """ jmat(j::Real, which::Symbol) = jmat(j, Val(which)) jmat(j::Real) = (jmat(j, Val(:x)), jmat(j, Val(:y)), jmat(j, Val(:z))) @@ -258,7 +275,7 @@ function jmat(j::Real, ::Val{:x}) throw(ArgumentError("The spin quantum number (j) must be a non-negative integer or half-integer.")) σ = _jm(j) - return QuantumObject((σ' + σ) / 2, Operator, [Int(J)]) + return QuantumObject((σ' + σ) / 2, Operator, Int(J)) end function jmat(j::Real, ::Val{:y}) J = 2 * j + 1 @@ -266,28 +283,28 @@ function jmat(j::Real, ::Val{:y}) throw(ArgumentError("The spin quantum number (j) must be a non-negative integer or half-integer.")) σ = _jm(j) - return QuantumObject((σ' - σ) / 2im, Operator, [Int(J)]) + return QuantumObject((σ' - σ) / 2im, Operator, Int(J)) end function jmat(j::Real, ::Val{:z}) J = 2 * j + 1 ((floor(J) != J) || (j < 0)) && throw(ArgumentError("The spin quantum number (j) must be a non-negative integer or half-integer.")) - return QuantumObject(_jz(j), Operator, [Int(J)]) + return QuantumObject(_jz(j), Operator, Int(J)) end function jmat(j::Real, ::Val{:+}) J = 2 * j + 1 ((floor(J) != J) || (j < 0)) && throw(ArgumentError("The spin quantum number (j) must be a non-negative integer or half-integer.")) - return QuantumObject(adjoint(_jm(j)), Operator, [Int(J)]) + return QuantumObject(adjoint(_jm(j)), Operator, Int(J)) end function jmat(j::Real, ::Val{:-}) J = 2 * j + 1 ((floor(J) != J) || (j < 0)) && throw(ArgumentError("The spin quantum number (j) must be a non-negative integer or half-integer.")) - return QuantumObject(_jm(j), Operator, [Int(J)]) + return QuantumObject(_jm(j), Operator, Int(J)) end jmat(j::Real, ::Val{T}) where {T} = throw(ArgumentError("Invalid spin operator: $(T)")) @@ -301,7 +318,7 @@ _jz(j::Real) = spdiagm(0 => Array{ComplexF64}(j .- (0:Int(2 * j)))) @doc raw""" spin_Jx(j::Real) -``S_x`` operator for Spin-`j`, where `j` is the spin quantum number and can be a non-negative integer or half-integer +``\hat{S}_x`` operator for Spin-`j`, where `j` is the spin quantum number and can be a non-negative integer or half-integer. See also [`jmat`](@ref). """ @@ -310,7 +327,7 @@ spin_Jx(j::Real) = jmat(j, Val(:x)) @doc raw""" spin_Jy(j::Real) -``S_y`` operator for Spin-`j`, where `j` is the spin quantum number and can be a non-negative integer or half-integer +``\hat{S}_y`` operator for Spin-`j`, where `j` is the spin quantum number and can be a non-negative integer or half-integer. See also [`jmat`](@ref). """ @@ -319,7 +336,7 @@ spin_Jy(j::Real) = jmat(j, Val(:y)) @doc raw""" spin_Jz(j::Real) -``S_z`` operator for Spin-`j`, where `j` is the spin quantum number and can be a non-negative integer or half-integer +``\hat{S}_z`` operator for Spin-`j`, where `j` is the spin quantum number and can be a non-negative integer or half-integer. See also [`jmat`](@ref). """ @@ -328,7 +345,7 @@ spin_Jz(j::Real) = jmat(j, Val(:z)) @doc raw""" spin_Jm(j::Real) -``S_-`` operator for Spin-`j`, where `j` is the spin quantum number and can be a non-negative integer or half-integer +``\hat{S}_-`` operator for Spin-`j`, where `j` is the spin quantum number and can be a non-negative integer or half-integer. See also [`jmat`](@ref). """ @@ -337,7 +354,7 @@ spin_Jm(j::Real) = jmat(j, Val(:-)) @doc raw""" spin_Jp(j::Real) -``S_+`` operator for Spin-`j`, where `j` is the spin quantum number and can be a non-negative integer or half-integer +``\hat{S}_+`` operator for Spin-`j`, where `j` is the spin quantum number and can be a non-negative integer or half-integer. See also [`jmat`](@ref). """ @@ -346,7 +363,7 @@ spin_Jp(j::Real) = jmat(j, Val(:+)) @doc raw""" spin_J_set(j::Real) -A set of Spin-`j` operators ``(S_x, S_y, S_z)``, where `j` is the spin quantum number and can be a non-negative integer or half-integer +A set of Spin-`j` operators ``(\hat{S}_x, \hat{S}_y, \hat{S}_z)``, where `j` is the spin quantum number and can be a non-negative integer or half-integer. Note that this functions is same as `jmat(j)`. See also [`jmat`](@ref). """ @@ -355,7 +372,7 @@ spin_J_set(j::Real) = jmat(j) @doc raw""" sigmap() -Pauli ladder operator ``\hat{\sigma}_+ = \hat{\sigma}_x + i \hat{\sigma}_y``. +Pauli ladder operator ``\hat{\sigma}_+ = (\hat{\sigma}_x + i \hat{\sigma}_y) / 2``. See also [`jmat`](@ref). """ @@ -364,7 +381,7 @@ sigmap() = jmat(0.5, Val(:+)) @doc raw""" sigmam() -Pauli ladder operator ``\hat{\sigma}_- = \hat{\sigma}_x - i \hat{\sigma}_y``. +Pauli ladder operator ``\hat{\sigma}_- = (\hat{\sigma}_x - i \hat{\sigma}_y) / 2``. See also [`jmat`](@ref). """ @@ -414,44 +431,57 @@ eye( QuantumObject(Diagonal(ones(ComplexF64, N)); type = type, dims = dims) @doc raw""" - fdestroy(N::Int, j::Int) + fdestroy(N::Union{Int,Val}, j::Int) Construct a fermionic destruction operator acting on the `j`-th site, where the fock space has totally `N`-sites: Here, we use the [Jordan-Wigner transformation](https://en.wikipedia.org/wiki/Jordan%E2%80%93Wigner_transformation), namely ```math -d_j = \sigma_z^{\otimes j} \otimes \sigma_{-} \otimes I^{\otimes N-j-1} +\hat{d}_j = \hat{\sigma}_z^{\otimes j-1} \otimes \hat{\sigma}_{+} \otimes \hat{\mathbb{1}}^{\otimes N-j} ``` -Note that the site index `j` should satisfy: `0 ≤ j ≤ N - 1` +The site index `j` should satisfy: `1 ≤ j ≤ N`. + +Note that we put ``\hat{\sigma}_{+} = \begin{pmatrix} 0 & 1 \\ 0 & 0 \end{pmatrix}`` here because we consider ``|0\rangle = \begin{pmatrix} 1 \\ 0 \end{pmatrix}`` to be ground (vacant) state, and ``|1\rangle = \begin{pmatrix} 0 \\ 1 \end{pmatrix}`` to be excited (occupied) state. + +!!! warning "Beware of type-stability!" + If you want to keep type stability, it is recommended to use `fdestroy(Val(N), j)` instead of `fdestroy(N, j)`. See [this link](https://docs.julialang.org/en/v1/manual/performance-tips/#man-performance-value-type) and the [related Section](@ref doc:Type-Stability) about type stability for more details. """ -fdestroy(N::Int, j::Int) = _Jordan_Wigner(N, j, sigmam()) +fdestroy(N::Union{Int,Val}, j::Int) = _Jordan_Wigner(N, j, sigmap()) @doc raw""" - fcreate(N::Int, j::Int) + fcreate(N::Union{Int,Val}, j::Int) Construct a fermionic creation operator acting on the `j`-th site, where the fock space has totally `N`-sites: Here, we use the [Jordan-Wigner transformation](https://en.wikipedia.org/wiki/Jordan%E2%80%93Wigner_transformation), namely ```math -d_j^\dagger = \sigma_z^{\otimes j} \otimes \sigma_{+} \otimes I^{\otimes N-j-1} +\hat{d}^\dagger_j = \hat{\sigma}_z^{\otimes j-1} \otimes \hat{\sigma}_{-} \otimes \hat{\mathbb{1}}^{\otimes N-j} ``` -Note that the site index `j` should satisfy: `0 ≤ j ≤ N - 1` +The site index `j` should satisfy: `1 ≤ j ≤ N`. + +Note that we put ``\hat{\sigma}_{-} = \begin{pmatrix} 0 & 0 \\ 1 & 0 \end{pmatrix}`` here because we consider ``|0\rangle = \begin{pmatrix} 1 \\ 0 \end{pmatrix}`` to be ground (vacant) state, and ``|1\rangle = \begin{pmatrix} 0 \\ 1 \end{pmatrix}`` to be excited (occupied) state. + +!!! warning "Beware of type-stability!" + If you want to keep type stability, it is recommended to use `fcreate(Val(N), j)` instead of `fcreate(N, j)`. See [this link](https://docs.julialang.org/en/v1/manual/performance-tips/#man-performance-value-type) and the [related Section](@ref doc:Type-Stability) about type stability for more details. """ -fcreate(N::Int, j::Int) = _Jordan_Wigner(N, j, sigmap()) +fcreate(N::Union{Int,Val}, j::Int) = _Jordan_Wigner(N, j, sigmam()) + +_Jordan_Wigner(N::Int, j::Int, op::QuantumObject{<:AbstractArray{T},OperatorQuantumObject}) where {T} = + _Jordan_Wigner(Val(N), j, op) -function _Jordan_Wigner(N::Int, j::Int, op::QuantumObject{<:AbstractArray{T},OperatorQuantumObject}) where {T} +function _Jordan_Wigner(::Val{N}, j::Int, op::QuantumObject{<:AbstractArray{T},OperatorQuantumObject}) where {N,T} (N < 1) && throw(ArgumentError("The total number of sites (N) cannot be less than 1")) - ((j >= N) || (j < 0)) && throw(ArgumentError("The site index (j) should satisfy: 0 ≤ j ≤ N - 1")) + ((j > N) || (j < 1)) && throw(ArgumentError("The site index (j) should satisfy: 1 ≤ j ≤ N")) σz = sigmaz().data - Z_tensor = kron(1, 1, fill(σz, j)...) + Z_tensor = kron(1, 1, fill(σz, j - 1)...) - S = 2^(N - j - 1) + S = 2^(N - j) I_tensor = sparse((1.0 + 0.0im) * LinearAlgebra.I, S, S) - return QuantumObject(kron(Z_tensor, op.data, I_tensor); type = Operator, dims = fill(2, N)) + return QuantumObject(kron(Z_tensor, op.data, I_tensor); type = Operator, dims = ntuple(i -> 2, Val(N))) end @doc raw""" @@ -459,10 +489,10 @@ end Generates the projection operator ``\hat{O} = \dyad{i}{j}`` with Hilbert space dimension `N`. """ -projection(N::Int, i::Int, j::Int) = QuantumObject(sparse([i + 1], [j + 1], [1.0 + 0.0im], N, N)) +projection(N::Int, i::Int, j::Int) = QuantumObject(sparse([i + 1], [j + 1], [1.0 + 0.0im], N, N), type = Operator) @doc raw""" - tunneling(N::Int, m::Int=1; sparse::Bool=false) + tunneling(N::Int, m::Int=1; sparse::Union{Bool,Val{<:Bool}}=Val(false)) Generate a tunneling operator defined as: @@ -471,15 +501,20 @@ Generate a tunneling operator defined as: ``` where ``N`` is the number of basis states in the Hilbert space, and ``m`` is the number of excitations in tunneling event. + +If `sparse=true`, the operator is returned as a sparse matrix, otherwise a dense matrix is returned. + +!!! warning "Beware of type-stability!" + If you want to keep type stability, it is recommended to use `tunneling(N, m, Val(sparse))` instead of `tunneling(N, m, sparse)`. See [this link](https://docs.julialang.org/en/v1/manual/performance-tips/#man-performance-value-type) and the [related Section](@ref doc:Type-Stability) about type stability for more details. """ -function tunneling(N::Int, m::Int = 1; sparse::Bool = false) +function tunneling(N::Int, m::Int = 1; sparse::Union{Bool,Val} = Val(false)) (m < 1) && throw(ArgumentError("The number of excitations (m) cannot be less than 1")) data = ones(ComplexF64, N - m) - if sparse - return QuantumObject(spdiagm(m => data, -m => data); type = Operator, dims = [N]) + if getVal(makeVal(sparse)) + return QuantumObject(spdiagm(m => data, -m => data); type = Operator, dims = N) else - return QuantumObject(diagm(m => data, -m => data); type = Operator, dims = [N]) + return QuantumObject(diagm(m => data, -m => data); type = Operator, dims = N) end end @@ -490,7 +525,7 @@ Generates a discrete Fourier transform matrix ``\hat{F}_N`` for [Quantum Fourier The `dimensions` can be either the following types: - `dimensions::Int`: Number of basis states in the Hilbert space. -- `dimensions::Vector{Int}`: list of dimensions representing the each number of basis in the subsystems. +- `dimensions::Union{AbstractVector{Int},Tuple}`: list of dimensions representing the each number of basis in the subsystems. ``N`` represents the total dimension, and therefore the matrix is defined as @@ -506,9 +541,13 @@ The `dimensions` can be either the following types: ``` where ``\omega = \exp(\frac{2 \pi i}{N})``. + +!!! warning "Beware of type-stability!" + It is highly recommended to use `qft(dimensions)` with `dimensions` as `Tuple` or `SVector` to keep type stability. See the [related Section](@ref doc:Type-Stability) about type stability for more details. """ -qft(dimensions::Int) = QuantumObject(_qft_op(dimensions), Operator, [dimensions]) -qft(dimensions::Vector{Int}) = QuantumObject(_qft_op(prod(dimensions)), Operator, dimensions) +qft(dimensions::Int) = QuantumObject(_qft_op(dimensions), Operator, dimensions) +qft(dimensions::Union{AbstractVector{T},Tuple}) where {T} = + QuantumObject(_qft_op(prod(dimensions)), Operator, dimensions) function _qft_op(N::Int) ω = exp(2.0im * π / N) arr = 0:(N-1) diff --git a/src/qobj/quantum_object.jl b/src/qobj/quantum_object.jl index 7abf2e66..f55244b2 100644 --- a/src/qobj/quantum_object.jl +++ b/src/qobj/quantum_object.jl @@ -111,10 +111,10 @@ A constant representing the type of [`OperatorKetQuantumObject`](@ref): a ket st const OperatorKet = OperatorKetQuantumObject() @doc raw""" - struct QuantumObject{MT<:AbstractArray,ObjType<:QuantumObjectType} + struct QuantumObject{MT<:AbstractArray,ObjType<:QuantumObjectType,N} data::MT type::ObjType - dims::Vector{Int} + dims::SVector{N, Int} end Julia struct representing any quantum objects. @@ -125,20 +125,35 @@ Julia struct representing any quantum objects. julia> a = destroy(20) Quantum Object: type=Operator dims=[20] size=(20, 20) ishermitian=false 20×20 SparseMatrixCSC{ComplexF64, Int64} with 19 stored entries: -⠈⠢⡀⠀⠀⠀⠀⠀⠀⠀ -⠀⠀⠈⠢⡀⠀⠀⠀⠀⠀ -⠀⠀⠀⠀⠈⠢⡀⠀⠀⠀ -⠀⠀⠀⠀⠀⠀⠈⠢⡀⠀ -⠀⠀⠀⠀⠀⠀⠀⠀⠈⠢ +⎡⠈⠢⡀⠀⠀⠀⠀⠀⠀⠀⎤ +⎢⠀⠀⠈⠢⡀⠀⠀⠀⠀⠀⎥ +⎢⠀⠀⠀⠀⠈⠢⡀⠀⠀⠀⎥ +⎢⠀⠀⠀⠀⠀⠀⠈⠢⡀⠀⎥ +⎣⠀⠀⠀⠀⠀⠀⠀⠀⠈⠢⎦ julia> a isa QuantumObject true ``` """ -struct QuantumObject{MT<:AbstractArray,ObjType<:QuantumObjectType} <: AbstractQuantumObject +struct QuantumObject{MT<:AbstractArray,ObjType<:QuantumObjectType,N} <: AbstractQuantumObject data::MT type::ObjType - dims::Vector{Int} + dims::SVector{N,Int} + + function QuantumObject(data::MT, type::ObjType, dims) where {MT<:AbstractArray,ObjType<:QuantumObjectType} + _check_dims(dims) + + _size = _get_size(data) + _check_QuantumObject(type, dims, _size[1], _size[2]) + + N = length(dims) + + return new{MT,ObjType,N}(data, type, SVector{N,Int}(dims)) + end +end + +function QuantumObject(A::AbstractArray, type::ObjType, dims::Integer) where {ObjType<:QuantumObjectType} + return QuantumObject(A, type, SVector{1,Int}(dims)) end function QuantumObject( @@ -146,7 +161,7 @@ function QuantumObject( type::ObjType = nothing, dims = nothing, ) where {T,ObjType<:Union{Nothing,QuantumObjectType}} - _size = size(A) + _size = _get_size(A) if type isa Nothing type = (_size[1] == 1 && _size[2] > 1) ? Bra : Operator # default type @@ -160,15 +175,12 @@ function QuantumObject( if dims isa Nothing if type isa OperatorQuantumObject || type isa BraQuantumObject - dims = [_size[2]] + dims = SVector{1,Int}(_size[2]) elseif type isa SuperOperatorQuantumObject || type isa OperatorBraQuantumObject - dims = [isqrt(_size[2])] + dims = SVector{1,Int}(isqrt(_size[2])) end - else - _check_dims(dims) end - _check_QuantumObject(type, dims, _size[1], _size[2]) return QuantumObject(A, type, dims) end @@ -183,19 +195,15 @@ function QuantumObject( throw(ArgumentError("The argument type must be Ket or OperatorKet if the input array is a vector.")) end - _size = (length(A), 1) - if dims isa Nothing + _size = _get_size(A) if type isa KetQuantumObject - dims = [_size[1]] + dims = SVector{1,Int}(_size[1]) elseif type isa OperatorKetQuantumObject - dims = [isqrt(_size[1])] + dims = SVector{1,Int}(isqrt(_size[1])) end - else - _check_dims(dims) end - _check_QuantumObject(type, dims, _size[1], _size[2]) return QuantumObject(A, type, dims) end @@ -207,44 +215,51 @@ function QuantumObject( throw(DomainError(size(A), "The size of the array is not compatible with vector or matrix.")) end -_check_dims(dims::Vector{Int}) = - all(>(0), dims) || throw(DomainError(dims, "The argument dims must be a vector with positive integers.")) -_check_dims(dims::Any) = throw(ArgumentError("The argument dims must be a vector with positive integers.")) +function _check_dims(dims::Union{AbstractVector{T},NTuple{N,T}}) where {T<:Integer,N} + _non_static_array_warning("dims", dims) + return (all(>(0), dims) && length(dims) > 0) || + throw(DomainError(dims, "The argument dims must be of non-zero length and contain only positive integers.")) +end +_check_dims(dims::Any) = throw( + ArgumentError( + "The argument dims must be a Tuple or a StaticVector of non-zero length and contain only positive integers.", + ), +) -function _check_QuantumObject(type::KetQuantumObject, dims::Vector{Int}, m::Int, n::Int) +function _check_QuantumObject(type::KetQuantumObject, dims, m::Int, n::Int) (n != 1) && throw(DomainError((m, n), "The size of the array is not compatible with Ket")) (prod(dims) != m) && throw(DimensionMismatch("Ket with dims = $(dims) does not fit the array size = $((m, n)).")) return nothing end -function _check_QuantumObject(type::BraQuantumObject, dims::Vector{Int}, m::Int, n::Int) +function _check_QuantumObject(type::BraQuantumObject, dims, m::Int, n::Int) (m != 1) && throw(DomainError((m, n), "The size of the array is not compatible with Bra")) (prod(dims) != n) && throw(DimensionMismatch("Bra with dims = $(dims) does not fit the array size = $((m, n)).")) return nothing end -function _check_QuantumObject(type::OperatorQuantumObject, dims::Vector{Int}, m::Int, n::Int) +function _check_QuantumObject(type::OperatorQuantumObject, dims, m::Int, n::Int) (m != n) && throw(DomainError((m, n), "The size of the array is not compatible with Operator")) (prod(dims) != m) && throw(DimensionMismatch("Operator with dims = $(dims) does not fit the array size = $((m, n)).")) return nothing end -function _check_QuantumObject(type::SuperOperatorQuantumObject, dims::Vector{Int}, m::Int, n::Int) +function _check_QuantumObject(type::SuperOperatorQuantumObject, dims, m::Int, n::Int) (m != n) && throw(DomainError((m, n), "The size of the array is not compatible with SuperOperator")) (prod(dims) != sqrt(m)) && throw(DimensionMismatch("SuperOperator with dims = $(dims) does not fit the array size = $((m, n)).")) return nothing end -function _check_QuantumObject(type::OperatorKetQuantumObject, dims::Vector{Int}, m::Int, n::Int) +function _check_QuantumObject(type::OperatorKetQuantumObject, dims, m::Int, n::Int) (n != 1) && throw(DomainError((m, n), "The size of the array is not compatible with OperatorKet")) (prod(dims) != sqrt(m)) && throw(DimensionMismatch("OperatorKet with dims = $(dims) does not fit the array size = $((m, n)).")) return nothing end -function _check_QuantumObject(type::OperatorBraQuantumObject, dims::Vector{Int}, m::Int, n::Int) +function _check_QuantumObject(type::OperatorBraQuantumObject, dims, m::Int, n::Int) (m != 1) && throw(DomainError((m, n), "The size of the array is not compatible with OperatorBra")) (prod(dims) != sqrt(n)) && throw(DimensionMismatch("OperatorBra with dims = $(dims) does not fit the array size = $((m, n)).")) @@ -353,3 +368,7 @@ SparseArrays.SparseMatrixCSC(A::QuantumObject{<:AbstractMatrix}) = QuantumObject(SparseMatrixCSC(A.data), A.type, A.dims) SparseArrays.SparseMatrixCSC{T}(A::QuantumObject{<:SparseMatrixCSC}) where {T<:Number} = QuantumObject(SparseMatrixCSC{T}(A.data), A.type, A.dims) + +# functions for getting Float or Complex element type +_FType(::QuantumObject{<:AbstractArray{T}}) where {T<:Number} = _FType(T) +_CType(::QuantumObject{<:AbstractArray{T}}) where {T<:Number} = _CType(T) diff --git a/src/qobj/states.jl b/src/qobj/states.jl index 0dd76534..df01f81c 100644 --- a/src/qobj/states.jl +++ b/src/qobj/states.jl @@ -14,36 +14,46 @@ Returns a zero [`Ket`](@ref) vector with given argument `dimensions`. The `dimensions` can be either the following types: - `dimensions::Int`: Number of basis states in the Hilbert space. -- `dimensions::Vector{Int}`: list of dimensions representing the each number of basis in the subsystems. +- `dimensions::Union{AbstractVector{Int}, Tuple}`: list of dimensions representing the each number of basis in the subsystems. + +!!! warning "Beware of type-stability!" + It is highly recommended to use `zero_ket(dimensions)` with `dimensions` as `Tuple` or `SVector` to keep type stability. See the [related Section](@ref doc:Type-Stability) about type stability for more details. """ -zero_ket(dimensions::Int) = QuantumObject(zeros(ComplexF64, dimensions), Ket, [dimensions]) -zero_ket(dimensions::Vector{Int}) = QuantumObject(zeros(ComplexF64, prod(dimensions)), Ket, dimensions) +zero_ket(dimensions::Int) = QuantumObject(zeros(ComplexF64, dimensions), Ket, dimensions) +zero_ket(dimensions::Union{AbstractVector{Int},Tuple}) = + QuantumObject(zeros(ComplexF64, prod(dimensions)), Ket, dimensions) @doc raw""" - fock(N::Int, pos::Int=0; dims::Vector{Int}=[N], sparse::Bool=false) + fock(N::Int, j::Int=0; dims::Union{Int,AbstractVector{Int},Tuple}=N, sparse::Union{Bool,Val}=Val(false)) Generates a fock state ``\ket{\psi}`` of dimension `N`. It is also possible to specify the list of dimensions `dims` if different subsystems are present. + +!!! warning "Beware of type-stability!" + If you want to keep type stability, it is recommended to use `fock(N, j, dims=dims, sparse=Val(sparse))` instead of `fock(N, j, dims=dims, sparse=sparse)`. Consider also to use `dims` as a `Tuple` or `SVector` instead of `Vector`. See [this link](https://docs.julialang.org/en/v1/manual/performance-tips/#man-performance-value-type) and the [related Section](@ref doc:Type-Stability) about type stability for more details. """ -function fock(N::Int, pos::Int = 0; dims::Vector{Int} = [N], sparse::Bool = false) - if sparse - array = sparsevec([pos + 1], [1.0 + 0im], N) +function fock(N::Int, j::Int = 0; dims::Union{Int,AbstractVector{Int},Tuple} = N, sparse::Union{Bool,Val} = Val(false)) + if getVal(makeVal(sparse)) + array = sparsevec([j + 1], [1.0 + 0im], N) else array = zeros(ComplexF64, N) - array[pos+1] = 1 + array[j+1] = 1 end return QuantumObject(array; type = Ket, dims = dims) end @doc raw""" - basis(N::Int, pos::Int = 0; dims::Vector{Int}=[N]) + basis(N::Int, j::Int = 0; dims::Union{Int,AbstractVector{Int},Tuple}=N) Generates a fock state like [`fock`](@ref). It is also possible to specify the list of dimensions `dims` if different subsystems are present. + +!!! warning "Beware of type-stability!" + If you want to keep type stability, it is recommended to use `basis(N, j, dims=dims)` with `dims` as a `Tuple` or `SVector` instead of `Vector`. See [this link](https://docs.julialang.org/en/v1/manual/performance-tips/#man-performance-value-type) and the [related Section](@ref doc:Type-Stability) about type stability for more details. """ -basis(N::Int, pos::Int = 0; dims::Vector{Int} = [N]) = fock(N, pos, dims = dims) +basis(N::Int, j::Int = 0; dims::Union{Int,AbstractVector{Int},Tuple} = N) = fock(N, j, dims = dims) @doc raw""" coherent(N::Int, α::Number) @@ -61,25 +71,36 @@ Generate a random normalized [`Ket`](@ref) vector with given argument `dimension The `dimensions` can be either the following types: - `dimensions::Int`: Number of basis states in the Hilbert space. -- `dimensions::Vector{Int}`: list of dimensions representing the each number of basis in the subsystems. +- `dimensions::Union{AbstractVector{Int},Tuple}`: list of dimensions representing the each number of basis in the subsystems. + +!!! warning "Beware of type-stability!" + If you want to keep type stability, it is recommended to use `rand_ket(dimensions)` with `dimensions` as `Tuple` or `SVector` to keep type stability. See the [related Section](@ref doc:Type-Stability) about type stability for more details. """ -rand_ket(dimensions::Int) = rand_ket([dimensions]) -function rand_ket(dimensions::Vector{Int}) +rand_ket(dimensions::Int) = rand_ket(SVector(dimensions)) +function rand_ket(dimensions::Union{AbstractVector{Int},Tuple}) N = prod(dimensions) ψ = rand(ComplexF64, N) .- (0.5 + 0.5im) return QuantumObject(normalize!(ψ); type = Ket, dims = dimensions) end @doc raw""" - fock_dm(N::Int, pos::Int=0; dims::Vector{Int}=[N], sparse::Bool=false) + fock_dm(N::Int, j::Int=0; dims::Union{Int,AbstractVector{Int},Tuple}=N, sparse::Union{Bool,Val}=Val(false)) Density matrix representation of a Fock state. Constructed via outer product of [`fock`](@ref). + +!!! warning "Beware of type-stability!" + If you want to keep type stability, it is recommended to use `fock_dm(N, j, dims=dims, sparse=Val(sparse))` instead of `fock_dm(N, j, dims=dims, sparse=sparse)`. Consider also to use `dims` as a `Tuple` or `SVector` instead of `Vector`. See [this link](https://docs.julialang.org/en/v1/manual/performance-tips/#man-performance-value-type) and the [related Section](@ref doc:Type-Stability) about type stability for more details. """ -function fock_dm(N::Int, pos::Int = 0; dims::Vector{Int} = [N], sparse::Bool = false) - ψ = fock(N, pos; dims = dims, sparse = sparse) - return ψ * ψ' +function fock_dm( + N::Int, + j::Int = 0; + dims::Union{Int,AbstractVector{Int},Tuple} = N, + sparse::Union{Bool,Val} = Val(false), +) + ψ = fock(N, j; dims = dims, sparse = sparse) + return ket2dm(ψ) end @doc raw""" @@ -91,24 +112,28 @@ Constructed via outer product of [`coherent`](@ref). """ function coherent_dm(N::Int, α::T) where {T<:Number} ψ = coherent(N, α) - return ψ * ψ' + return ket2dm(ψ) end @doc raw""" - thermal_dm(N::Int, n::Real; sparse::Bool=false) + thermal_dm(N::Int, n::Real; sparse::Union{Bool,Val}=Val(false)) Density matrix for a thermal state (generating thermal state probabilities) with the following arguments: - `N::Int`: Number of basis states in the Hilbert space - `n::Real`: Expectation value for number of particles in the thermal state. +- `sparse::Union{Bool,Val}`: If `true`, return a sparse matrix representation. + +!!! warning "Beware of type-stability!" + If you want to keep type stability, it is recommended to use `thermal_dm(N, n, sparse=Val(sparse))` instead of `thermal_dm(N, n, sparse=sparse)`. See [this link](https://docs.julialang.org/en/v1/manual/performance-tips/#man-performance-value-type) and the [related Section](@ref doc:Type-Stability) about type stability for more details. """ -function thermal_dm(N::Int, n::Real; sparse::Bool = false) +function thermal_dm(N::Int, n::Real; sparse::Union{Bool,Val} = Val(false)) β = log(1.0 / n + 1.0) N_list = Array{Float64}(0:N-1) data = exp.(-β .* N_list) - if sparse - return QuantumObject(spdiagm(0 => data ./ sum(data)), Operator, [N]) + if getVal(makeVal(sparse)) + return QuantumObject(spdiagm(0 => data ./ sum(data)), Operator, N) else - return QuantumObject(diagm(0 => data ./ sum(data)), Operator, [N]) + return QuantumObject(diagm(0 => data ./ sum(data)), Operator, N) end end @@ -119,10 +144,13 @@ Returns the maximally mixed density matrix with given argument `dimensions`. The `dimensions` can be either the following types: - `dimensions::Int`: Number of basis states in the Hilbert space. -- `dimensions::Vector{Int}`: list of dimensions representing the each number of basis in the subsystems. +- `dimensions::Union{AbstractVector{Int},Tuple}`: list of dimensions representing the each number of basis in the subsystems. + +!!! warning "Beware of type-stability!" + If you want to keep type stability, it is recommended to use `maximally_mixed_dm(dimensions)` with `dimensions` as `Tuple` or `SVector` to keep type stability. See the [related Section](@ref doc:Type-Stability) about type stability for more details. """ -maximally_mixed_dm(dimensions::Int) = QuantumObject(I(dimensions) / complex(dimensions), Operator, [dimensions]) -function maximally_mixed_dm(dimensions::Vector{Int}) +maximally_mixed_dm(dimensions::Int) = QuantumObject(I(dimensions) / complex(dimensions), Operator, SVector(dimensions)) +function maximally_mixed_dm(dimensions::Union{AbstractVector{Int},Tuple}) N = prod(dimensions) return QuantumObject(I(N) / complex(N), Operator, dimensions) end @@ -134,16 +162,19 @@ Generate a random density matrix from Ginibre ensemble with given argument `dime The `dimensions` can be either the following types: - `dimensions::Int`: Number of basis states in the Hilbert space. -- `dimensions::Vector{Int}`: list of dimensions representing the each number of basis in the subsystems. +- `dimensions::Union{AbstractVector{Int},Tuple}`: list of dimensions representing the each number of basis in the subsystems. The default keyword argument `rank = prod(dimensions)` (full rank). +!!! warning "Beware of type-stability!" + If you want to keep type stability, it is recommended to use `rand_dm(dimensions; rank=rank)` with `dimensions` as `Tuple` or `SVector` instead of `Vector`. See [this link](https://docs.julialang.org/en/v1/manual/performance-tips/#man-performance-value-type) and the [related Section](@ref doc:Type-Stability) about type stability for more details. + # References - [J. Ginibre, Statistical ensembles of complex, quaternion, and real matrices, Journal of Mathematical Physics 6.3 (1965): 440-449](https://doi.org/10.1063/1.1704292) - [K. Życzkowski, et al., Generating random density matrices, Journal of Mathematical Physics 52, 062201 (2011)](http://dx.doi.org/10.1063/1.3595693) """ -rand_dm(dimensions::Int; rank::Int = prod(dimensions)) = rand_dm([dimensions], rank = rank) -function rand_dm(dimensions::Vector{Int}; rank::Int = prod(dimensions)) +rand_dm(dimensions::Int; rank::Int = prod(dimensions)) = rand_dm(SVector(dimensions), rank = rank) +function rand_dm(dimensions::Union{AbstractVector{Int},Tuple}; rank::Int = prod(dimensions)) N = prod(dimensions) (rank < 1) && throw(DomainError(rank, "The argument rank must be larger than 1.")) (rank > N) && throw(DomainError(rank, "The argument rank cannot exceed dimensions.")) @@ -159,7 +190,7 @@ end Generate the spin state: ``|j, m\rangle`` -The eigenstate of the Spin-`j` ``S_z`` operator with eigenvalue `m`, where where `j` is the spin quantum number and can be a non-negative integer or half-integer +The eigenstate of the Spin-`j` ``\hat{S}_z`` operator with eigenvalue `m`, where where `j` is the spin quantum number and can be a non-negative integer or half-integer See also [`jmat`](@ref). """ @@ -182,15 +213,17 @@ end Generate the coherent spin state (rotation of the ``|j, j\rangle`` state), namely ```math -|\theta, \phi \rangle = R(\theta, \phi) |j, j\rangle +|\theta, \phi \rangle = \hat{R}(\theta, \phi) |j, j\rangle ``` where the rotation operator is defined as ```math -R(\theta, \phi) = \exp \left( \frac{\theta}{2} (S_- e^{i\phi} - S_+ e^{-i\phi}) \right) +\hat{R}(\theta, \phi) = \exp \left( \frac{\theta}{2} (\hat{S}_- e^{i\phi} - \hat{S}_+ e^{-i\phi}) \right) ``` +and ``\hat{S}_\pm`` are plus and minus Spin-`j` operators, respectively. + # Arguments - `j::Real`: The spin quantum number and can be a non-negative integer or half-integer - `θ::Real`: rotation angle from z-axis @@ -207,7 +240,7 @@ function spin_coherent(j::Real, θ::Real, ϕ::Real) end @doc raw""" - bell_state(x::Int, z::Int) + bell_state(x::Union{Int}, z::Union{Int}) Return the [Bell state](https://en.wikipedia.org/wiki/Bell_state) depending on the arguments `(x, z)`: - `(0, 0)`: ``| \Phi^+ \rangle = ( |00\rangle + |11\rangle ) / \sqrt{2}`` @@ -227,13 +260,24 @@ Quantum Object: type=Ket dims=[2, 2] size=(4,) 0.0 + 0.0im 0.0 + 0.0im 0.7071067811865475 + 0.0im + +julia> bell_state(Val(1), Val(0)) +Quantum Object: type=Ket dims=[2, 2] size=(4,) +4-element Vector{ComplexF64}: + 0.0 + 0.0im + 0.7071067811865475 + 0.0im + 0.7071067811865475 + 0.0im + 0.0 + 0.0im ``` + +!!! warning "Beware of type-stability!" + If you want to keep type stability, it is recommended to use `bell_state(Val(x), Val(z))` instead of `bell_state(x, z)`. See [this link](https://docs.julialang.org/en/v1/manual/performance-tips/#man-performance-value-type) and the [related Section](@ref doc:Type-Stability) for more details. """ bell_state(x::Int, z::Int) = bell_state(Val(x), Val(z)) -bell_state(::Val{0}, ::Val{0}) = QuantumObject(ComplexF64[1, 0, 0, 1] / sqrt(2), Ket, [2, 2]) -bell_state(::Val{0}, ::Val{1}) = QuantumObject(ComplexF64[1, 0, 0, -1] / sqrt(2), Ket, [2, 2]) -bell_state(::Val{1}, ::Val{0}) = QuantumObject(ComplexF64[0, 1, 1, 0] / sqrt(2), Ket, [2, 2]) -bell_state(::Val{1}, ::Val{1}) = QuantumObject(ComplexF64[0, 1, -1, 0] / sqrt(2), Ket, [2, 2]) +bell_state(::Val{0}, ::Val{0}) = QuantumObject(ComplexF64[1, 0, 0, 1] / sqrt(2), Ket, (2, 2)) +bell_state(::Val{0}, ::Val{1}) = QuantumObject(ComplexF64[1, 0, 0, -1] / sqrt(2), Ket, (2, 2)) +bell_state(::Val{1}, ::Val{0}) = QuantumObject(ComplexF64[0, 1, 1, 0] / sqrt(2), Ket, (2, 2)) +bell_state(::Val{1}, ::Val{1}) = QuantumObject(ComplexF64[0, 1, -1, 0] / sqrt(2), Ket, (2, 2)) bell_state(::Val{T1}, ::Val{T2}) where {T1,T2} = throw(ArgumentError("Invalid Bell state: $(T1), $(T2)")) @doc raw""" @@ -241,7 +285,7 @@ bell_state(::Val{T1}, ::Val{T2}) where {T1,T2} = throw(ArgumentError("Invalid Be Return the two particle singlet state: ``\frac{1}{\sqrt{2}} ( |01\rangle - |10\rangle )`` """ -singlet_state() = QuantumObject(ComplexF64[0, 1, -1, 0] / sqrt(2), Ket, [2, 2]) +singlet_state() = QuantumObject(ComplexF64[0, 1, -1, 0] / sqrt(2), Ket, (2, 2)) @doc raw""" triplet_states() @@ -254,29 +298,33 @@ Return a list of the two particle triplet states: """ function triplet_states() return QuantumObject[ - QuantumObject(ComplexF64[0, 0, 0, 1], Ket, [2, 2]), - QuantumObject(ComplexF64[0, 1, 1, 0] / sqrt(2), Ket, [2, 2]), - QuantumObject(ComplexF64[1, 0, 0, 0], Ket, [2, 2]), + QuantumObject(ComplexF64[0, 0, 0, 1], Ket, (2, 2)), + QuantumObject(ComplexF64[0, 1, 1, 0] / sqrt(2), Ket, (2, 2)), + QuantumObject(ComplexF64[1, 0, 0, 0], Ket, (2, 2)), ] end @doc raw""" - w_state(n::Int) + w_state(n::Union{Int,Val}) Returns the `n`-qubit [W-state](https://en.wikipedia.org/wiki/W_state): ```math \frac{1}{\sqrt{n}} \left( |100...0\rangle + |010...0\rangle + \cdots + |00...01\rangle \right) ``` + +!!! warning "Beware of type-stability!" + If you want to keep type stability, it is recommended to use `w_state(Val(n))` instead of `w_state(n)`. See [this link](https://docs.julialang.org/en/v1/manual/performance-tips/#man-performance-value-type) and the [related Section](@ref doc:Type-Stability) for more details. """ -function w_state(n::Int) +function w_state(::Val{n}) where {n} nzind = 2 .^ (0:(n-1)) .+ 1 nzval = fill(ComplexF64(1 / sqrt(n)), n) - return QuantumObject(SparseVector(2^n, nzind, nzval), Ket, fill(2, n)) + return QuantumObject(SparseVector(2^n, nzind, nzval), Ket, ntuple(x -> 2, Val(n))) end +w_state(n::Int) = w_state(Val(n)) @doc raw""" - ghz_state(n::Int; d::Int=2) + ghz_state(n::Union{Int,Val}; d::Int=2) Returns the generalized `n`-qudit [Greenberger–Horne–Zeilinger (GHZ) state](https://en.wikipedia.org/wiki/Greenberger%E2%80%93Horne%E2%80%93Zeilinger_state): @@ -285,9 +333,13 @@ Returns the generalized `n`-qudit [Greenberger–Horne–Zeilinger (GHZ) state]( ``` Here, `d` specifies the dimension of each qudit. Default to `d=2` (qubit). + +!!! warning "Beware of type-stability!" + If you want to keep type stability, it is recommended to use `ghz_state(Val(n))` instead of `ghz_state(n)`. See [this link](https://docs.julialang.org/en/v1/manual/performance-tips/#man-performance-value-type) and the [related Section](@ref doc:Type-Stability) for more details. """ -function ghz_state(n::Int; d::Int = 2) +function ghz_state(::Val{n}; d::Int = 2) where {n} nzind = collect((0:(d-1)) .* Int((d^n - 1) / (d - 1)) .+ 1) nzval = ones(ComplexF64, d) / sqrt(d) - return QuantumObject(SparseVector(d^n, nzind, nzval), Ket, fill(d, n)) + return QuantumObject(SparseVector(d^n, nzind, nzval), Ket, ntuple(x -> d, Val(n))) end +ghz_state(n::Int; d::Int = 2) = ghz_state(Val(n), d = d) diff --git a/src/qobj/superoperators.jl b/src/qobj/superoperators.jl index 752befb2..b95c2ee2 100644 --- a/src/qobj/superoperators.jl +++ b/src/qobj/superoperators.jl @@ -5,23 +5,15 @@ Functions for generating (common) quantum super-operators. export spre, spost, sprepost, lindblad_dissipator # intrinsic functions for super-operators +# (keep these because they take AbstractMatrix as input) _spre(A::AbstractMatrix, Id::AbstractMatrix) = kron(Id, sparse(A)) _spre(A::AbstractSparseMatrix, Id::AbstractMatrix) = kron(Id, A) -if VERSION < v"1.10" - _spost(B::AbstractMatrix, Id::AbstractMatrix) = kron(sparse(transpose(sparse(B))), Id) - _spost(B::AbstractSparseMatrix, Id::AbstractMatrix) = kron(sparse(transpose(B)), Id) - _sprepost(A::AbstractMatrix, B::AbstractMatrix) = kron(sparse(transpose(sparse(B))), sparse(A)) - _sprepost(A::AbstractMatrix, B::AbstractSparseMatrix) = kron(sparse(transpose(B)), sparse(A)) - _sprepost(A::AbstractSparseMatrix, B::AbstractMatrix) = kron(sparse(transpose(sparse(B))), A) - _sprepost(A::AbstractSparseMatrix, B::AbstractSparseMatrix) = kron(sparse(transpose(B)), A) -else - _spost(B::AbstractMatrix, Id::AbstractMatrix) = kron(transpose(sparse(B)), Id) - _spost(B::AbstractSparseMatrix, Id::AbstractMatrix) = kron(transpose(B), Id) - _sprepost(A::AbstractMatrix, B::AbstractMatrix) = kron(transpose(sparse(B)), sparse(A)) - _sprepost(A::AbstractMatrix, B::AbstractSparseMatrix) = kron(transpose(B), sparse(A)) - _sprepost(A::AbstractSparseMatrix, B::AbstractMatrix) = kron(transpose(sparse(B)), A) - _sprepost(A::AbstractSparseMatrix, B::AbstractSparseMatrix) = kron(transpose(B), A) -end +_spost(B::AbstractMatrix, Id::AbstractMatrix) = kron(transpose(sparse(B)), Id) +_spost(B::AbstractSparseMatrix, Id::AbstractMatrix) = kron(transpose(B), Id) +_sprepost(A::AbstractMatrix, B::AbstractMatrix) = kron(transpose(sparse(B)), sparse(A)) +_sprepost(A::AbstractMatrix, B::AbstractSparseMatrix) = kron(transpose(B), sparse(A)) +_sprepost(A::AbstractSparseMatrix, B::AbstractMatrix) = kron(transpose(sparse(B)), A) +_sprepost(A::AbstractSparseMatrix, B::AbstractSparseMatrix) = kron(transpose(B), A) @doc raw""" spre(A::QuantumObject, Id_cache=I(size(A,1))) @@ -33,6 +25,7 @@ Since the density matrix is vectorized in [`OperatorKet`](@ref) form: ``|\hat{\r ```math \mathcal{O} \left(\hat{A}\right) \left[ \hat{\rho} \right] = \hat{\mathbb{1}} \otimes \hat{A} ~ |\hat{\rho}\rangle\rangle ``` +(see the section in documentation: [Superoperators and Vectorized Operators](@ref doc:Superoperators-and-Vectorized-Operators) for more details) The optional argument `Id_cache` can be used to pass a precomputed identity matrix. This can be useful when the same function is applied multiple times with a known Hilbert space dimension. @@ -50,6 +43,7 @@ Since the density matrix is vectorized in [`OperatorKet`](@ref) form: ``|\hat{\r ```math \mathcal{O} \left(\hat{B}\right) \left[ \hat{\rho} \right] = \hat{B}^T \otimes \hat{\mathbb{1}} ~ |\hat{\rho}\rangle\rangle ``` +(see the section in documentation: [Superoperators and Vectorized Operators](@ref doc:Superoperators-and-Vectorized-Operators) for more details) The optional argument `Id_cache` can be used to pass a precomputed identity matrix. This can be useful when the same function is applied multiple times with a known Hilbert space dimension. @@ -65,8 +59,9 @@ Returns the [`SuperOperator`](@ref) form of `A` and `B` acting on the left and r Since the density matrix is vectorized in [`OperatorKet`](@ref) form: ``|\hat{\rho}\rangle\rangle``, this [`SuperOperator`](@ref) is always a matrix ``\hat{B}^T \otimes \hat{A}``, namely ```math -\mathcal{O} \left(\hat{A}, \hat{B}\right) \left[ \hat{\rho} \right] = \hat{B}^T \otimes \hat{A} ~ |\hat{\rho}\rangle\rangle = \textrm{spre}(A) * \textrm{spost}(B) ~ |\hat{\rho}\rangle\rangle +\mathcal{O} \left(\hat{A}, \hat{B}\right) \left[ \hat{\rho} \right] = \hat{B}^T \otimes \hat{A} ~ |\hat{\rho}\rangle\rangle = \textrm{spre}(\hat{A}) * \textrm{spost}(\hat{B}) ~ |\hat{\rho}\rangle\rangle ``` +(see the section in documentation: [Superoperators and Vectorized Operators](@ref doc:Superoperators-and-Vectorized-Operators) for more details) See also [`spre`](@ref) and [`spost`](@ref). """ diff --git a/src/qobj/synonyms.jl b/src/qobj/synonyms.jl index 5b991d5c..17f59e99 100644 --- a/src/qobj/synonyms.jl +++ b/src/qobj/synonyms.jl @@ -58,7 +58,7 @@ dag(A::QuantumObject{<:AbstractArray{T}}) where {T} = adjoint(A) @doc raw""" matrix_element(i::QuantumObject, A::QuantumObject j::QuantumObject) -Compute the generalized dot product `dot(i, A*j)` between three [`QuantumObject`](@ref): ``\langle i | A | j \rangle`` +Compute the generalized dot product `dot(i, A*j)` between three [`QuantumObject`](@ref): ``\langle i | \hat{A} | j \rangle`` Note that this function is same as `dot(i, A, j)` @@ -184,7 +184,7 @@ Quantum Object: type=Operator dims=[2, 2, 2] size=(8, 8) ishermitian=tru 1.0+0.0im ⋅ ⋅ ⋅ ⋅ ⋅ ``` """ -tensor(A::QuantumObject...) = kron(A...) +tensor(A...) = kron(A...) @doc raw""" ⊗(A::QuantumObject, B::QuantumObject) diff --git a/src/spin_lattice.jl b/src/spin_lattice.jl index cf3ba11d..26aa48ae 100644 --- a/src/spin_lattice.jl +++ b/src/spin_lattice.jl @@ -18,7 +18,7 @@ end #Definition of many-body operators function mb(s::QuantumObject{<:AbstractArray{T1},OperatorQuantumObject}, i::Integer, N::Integer) where {T1} T = s.dims[1] - return QuantumObject(kron(eye(T^(i - 1)), s, eye(T^(N - i))); dims = fill(2, N)) + return QuantumObject(kron(eye(T^(i - 1)), s, eye(T^(N - i))); dims = ntuple(j -> 2, Val(N))) end mb(s::QuantumObject{<:AbstractArray{T1},OperatorQuantumObject}, i::Integer, latt::Lattice) where {T1} = mb(s, i, latt.N) mb(s::QuantumObject{<:AbstractArray{T1},OperatorQuantumObject}, row::Integer, col::Integer, latt::Lattice) where {T1} = @@ -42,8 +42,7 @@ function TFIM(Jx::Real, Jy::Real, Jz::Real, hx::Real, γ::Real, latt::Lattice; b S = [mb(sm, i, latt) for i in 1:latt.N] c_ops = sqrt(γ) .* S - op_sum(S::Vector{QuantumObject{SparseMatrixCSC{ComplexF64,Int64},OperatorQuantumObject}}, i::CartesianIndex) = - S[latt.lin_idx[i]] * sum(S[latt.lin_idx[nn(i, latt, bc; order = order)]]) + op_sum(S, i::CartesianIndex) = S[latt.lin_idx[i]] * sum(S[latt.lin_idx[nn(i, latt, bc; order = order)]]) H = 0 if (Jx != 0 || hx != 0) diff --git a/src/steadystate.jl b/src/steadystate.jl index 29bcea9f..080f1728 100644 --- a/src/steadystate.jl +++ b/src/steadystate.jl @@ -18,7 +18,7 @@ A structure representing an ordinary differential equation (ODE) solver for solv It includes a field (attribute) `SteadyStateODESolver.alg` that specifies the solving algorithm. Default to `Tsit5()`. -For more details about the solvers, please refer to [`DifferentialEquations.jl`](https://diffeq.sciml.ai/stable/) +For more details about the solvers, please refer to [`OrdinaryDiffEq.jl`](https://docs.sciml.ai/OrdinaryDiffEq/stable/) """ Base.@kwdef struct SteadyStateODESolver{MT<:OrdinaryDiffEqAlgorithm} <: SteadyStateSolver alg::MT = Tsit5() @@ -31,11 +31,14 @@ end struct SteadyStateDirectSolver <: SteadyStateSolver end struct SteadyStateEigenSolver <: SteadyStateSolver end -Base.@kwdef struct SteadyStateLinearSolver{MT<:Union{LinearSolve.SciMLLinearSolveAlgorithm,Nothing}} <: - SteadyStateSolver - alg::MT = nothing - Pl::Union{Function,Nothing} = nothing - Pr::Union{Function,Nothing} = nothing +Base.@kwdef struct SteadyStateLinearSolver{ + MT<:Union{SciMLLinearSolveAlgorithm,Nothing}, + PlT<:Union{Function,Nothing}, + PrT<:Union{Function,Nothing}, +} <: SteadyStateSolver + alg::MT = KrylovJL_GMRES() + Pl::PlT = nothing + Pr::PrT = nothing end @doc raw""" @@ -43,7 +46,7 @@ end H::QuantumObject{MT1,HOpType}, ψ0::QuantumObject{<:AbstractArray{T2},StateOpType}, tspan::Real = Inf, - c_ops::Vector{QuantumObject{Tc,COpType}} = QuantumObject{MT1,HOpType}[]; + c_ops::Union{Nothing,AbstractVector} = nothing; solver::SteadyStateODESolver = SteadyStateODESolver(), reltol::Real = 1.0e-8, abstol::Real = 1.0e-10, @@ -55,20 +58,20 @@ Solve the stationary state based on time evolution (ordinary differential equati The termination condition of the stationary state ``|\rho\rangle\rangle`` is that either the following condition is `true`: ```math -\lVert\frac{\partial |\rho\rangle\rangle}{\partial t}\rVert \leq \textrm{reltol} \times\lVert\frac{\partial |\rho\rangle\rangle}{\partial t}+|\rho\rangle\rangle\rVert +\lVert\frac{\partial |\hat{\rho}\rangle\rangle}{\partial t}\rVert \leq \textrm{reltol} \times\lVert\frac{\partial |\hat{\rho}\rangle\rangle}{\partial t}+|\hat{\rho}\rangle\rangle\rVert ``` or ```math -\lVert\frac{\partial |\rho\rangle\rangle}{\partial t}\rVert \leq \textrm{abstol} +\lVert\frac{\partial |\hat{\rho}\rangle\rangle}{\partial t}\rVert \leq \textrm{abstol} ``` # Parameters - `H::QuantumObject`: The Hamiltonian or the Liouvillian of the system. - `ψ0::QuantumObject`: The initial state of the system. - `tspan::Real=Inf`: The final time step for the steady state problem. -- `c_ops::AbstractVector=[]`: The list of the collapse operators. +- `c_ops::Union{Nothing,AbstractVector}=nothing`: The list of the collapse operators. - `solver::SteadyStateODESolver=SteadyStateODESolver()`: see [`SteadyStateODESolver`](@ref) for more details. - `reltol::Real=1.0e-8`: Relative tolerance in steady state terminate condition and solver adaptive timestepping. - `abstol::Real=1.0e-10`: Absolute tolerance in steady state terminate condition and solver adaptive timestepping. @@ -78,7 +81,7 @@ function steadystate( H::QuantumObject{MT1,HOpType}, ψ0::QuantumObject{<:AbstractArray{T2},StateOpType}, tspan::Real = Inf, - c_ops::Vector{QuantumObject{Tc,COpType}} = QuantumObject{MT1,HOpType}[]; + c_ops::Union{Nothing,AbstractVector} = nothing; solver::SteadyStateODESolver = SteadyStateODESolver(), reltol::Real = 1.0e-8, abstol::Real = 1.0e-10, @@ -86,18 +89,18 @@ function steadystate( ) where { MT1<:AbstractMatrix, T2, - Tc<:AbstractMatrix, HOpType<:Union{OperatorQuantumObject,SuperOperatorQuantumObject}, StateOpType<:Union{KetQuantumObject,OperatorQuantumObject}, - COpType<:Union{OperatorQuantumObject,SuperOperatorQuantumObject}, } (H.dims != ψ0.dims) && throw(DimensionMismatch("The two quantum objects are not of the same Hilbert dimension.")) N = prod(H.dims) - u0 = mat2vec(ket2dm(ψ0).data) + u0 = sparse_to_dense(_CType(ψ0), mat2vec(ket2dm(ψ0).data)) + L = MatrixOperator(liouvillian(H, c_ops).data) - prob = ODEProblem{true}(L, u0, (0.0, tspan)) + ftype = _FType(ψ0) + prob = ODEProblem{true}(L, u0, (ftype(0), ftype(tspan))) # Convert tspan to support GPUs and avoid type instabilities for OrdinaryDiffEq.jl sol = solve( prob, solver.alg; @@ -108,7 +111,6 @@ function steadystate( ) ρss = reshape(sol.u[end], N, N) - ρss = (ρss + ρss') / 2 # Hermitianize return QuantumObject(ρss, Operator, H.dims) end @@ -125,22 +127,14 @@ function _steadystate_ode_condition(integrator, abstol, reltol, min_t) end function steadystate( - L::QuantumObject{<:AbstractArray{T},SuperOperatorQuantumObject}; - solver::SteadyStateSolver = SteadyStateDirectSolver(), - kwargs..., -) where {T} - return _steadystate(L, solver; kwargs...) -end - -function steadystate( - H::QuantumObject{<:AbstractArray{T},OpType}, - c_ops::AbstractVector; + H::QuantumObject{<:AbstractArray,OpType}, + c_ops::Union{Nothing,AbstractVector} = nothing; solver::SteadyStateSolver = SteadyStateDirectSolver(), kwargs..., -) where {T,OpType<:Union{OperatorQuantumObject,SuperOperatorQuantumObject}} +) where {OpType<:Union{OperatorQuantumObject,SuperOperatorQuantumObject}} L = liouvillian(H, c_ops) - return steadystate(L; solver = solver, kwargs...) + return _steadystate(L, solver; kwargs...) end function _steadystate( @@ -232,7 +226,7 @@ end H_p::QuantumObject{<:AbstractArray,OpType2}, H_m::QuantumObject{<:AbstractArray,OpType3}, ωd::Number, - c_ops::AbstractVector = QuantumObject{MT,OpType1}[]; + c_ops::Union{Nothing,AbstractVector} = nothing; n_max::Integer = 2, tol::R = 1e-8, solver::FSolver = SSFloquetLinearSystem, @@ -310,7 +304,7 @@ function steadystate_floquet( H_p::QuantumObject{<:AbstractArray,OpType2}, H_m::QuantumObject{<:AbstractArray,OpType3}, ωd::Number, - c_ops::AbstractVector = QuantumObject{MT,OpType1}[]; + c_ops::Union{Nothing,AbstractVector} = nothing; n_max::Integer = 2, tol::R = 1e-8, solver::FSolver = SSFloquetLinearSystem(), @@ -374,13 +368,13 @@ function _steadystate_floquet( ρ0 = reshape(ρtot[offset1+1:offset2], Ns, Ns) ρ0_tr = tr(ρ0) ρ0 = ρ0 / ρ0_tr - ρ0 = QuantumObject((ρ0 + ρ0') / 2, dims = L_0.dims) + ρ0 = QuantumObject((ρ0 + ρ0') / 2, type = Operator, dims = L_0.dims) ρtot = ρtot / ρ0_tr ρ_list = [ρ0] for i in 0:n_max-1 ρi_m = reshape(ρtot[offset1-(i+1)*N+1:offset1-i*N], Ns, Ns) - ρi_m = QuantumObject(ρi_m, dims = L_0.dims) + ρi_m = QuantumObject(ρi_m, type = Operator, dims = L_0.dims) push!(ρ_list, ρi_m) end diff --git a/src/time_evolution/lr_mesolve.jl b/src/time_evolution/lr_mesolve.jl index 856f665d..eeda3efa 100644 --- a/src/time_evolution/lr_mesolve.jl +++ b/src/time_evolution/lr_mesolve.jl @@ -13,7 +13,7 @@ struct LRTimeEvolutionSol{TT<:Vector{<:Real},TS<:AbstractVector,TE<:Matrix{Compl M::TM end -struct LRMesolveOptions{AlgType<:OrdinaryDiffEq.OrdinaryDiffEqAlgorithm} +struct LRMesolveOptions{AlgType<:OrdinaryDiffEqAlgorithm} alg::AlgType progress::Bool err_max::Real @@ -27,7 +27,7 @@ struct LRMesolveOptions{AlgType<:OrdinaryDiffEq.OrdinaryDiffEqAlgorithm} end function LRMesolveOptions(; - alg::OrdinaryDiffEq.OrdinaryDiffEqAlgorithm = Tsit5(), + alg::OrdinaryDiffEqAlgorithm = Tsit5(), progress::Bool = true, err_max::Real = 0.0, p0::Real = 0.0, @@ -399,7 +399,7 @@ function lr_mesolveProblem( f_ops::Tuple = (), opt::LRMesolveOptions{AlgType} = LRMesolveOptions(), kwargs..., -) where {T1,T2,AlgType<:OrdinaryDiffEq.OrdinaryDiffEqAlgorithm} +) where {T1,T2,AlgType<:OrdinaryDiffEqAlgorithm} # Formulation of problem H -= 0.5im * sum([Γ' * Γ for Γ in c_ops]) @@ -495,7 +495,7 @@ function lr_mesolve( f_ops::Tuple = (), opt::LRMesolveOptions{AlgType} = LRMesolveOptions(), kwargs..., -) where {T1,T2,AlgType<:OrdinaryDiffEq.OrdinaryDiffEqAlgorithm} +) where {T1,T2,AlgType<:OrdinaryDiffEqAlgorithm} prob = lr_mesolveProblem(H, z, B, t_l, c_ops; e_ops = e_ops, f_ops = f_ops, opt = opt, kwargs...) return lr_mesolve(prob; kwargs...) end diff --git a/src/time_evolution/mcsolve.jl b/src/time_evolution/mcsolve.jl index 853588ad..c2054a8c 100644 --- a/src/time_evolution/mcsolve.jl +++ b/src/time_evolution/mcsolve.jl @@ -86,10 +86,10 @@ function _mcsolve_output_func(sol, i) return (sol, false) end -function _mcsolve_generate_statistics(sol, i, times, states, expvals_all, jump_times, jump_which) +function _mcsolve_generate_statistics!(sol, i, times, states, expvals_all, jump_times, jump_which) sol_i = sol[:, i] !isempty(sol_i.prob.kwargs[:saveat]) ? - states[i] = [QuantumObject(sol_i.u[i], dims = sol_i.prob.p.Hdims) for i in 1:length(sol_i.u)] : nothing + states[i] = [QuantumObject(normalize!(sol_i.u[i]), dims = sol_i.prob.p.Hdims) for i in 1:length(sol_i.u)] : nothing copyto!(view(expvals_all, i, :, :), sol_i.prob.p.expvals) times[i] = sol_i.t @@ -101,8 +101,8 @@ end mcsolveProblem(H::QuantumObject{<:AbstractArray{T1},OperatorQuantumObject}, ψ0::QuantumObject{<:AbstractArray{T2},KetQuantumObject}, tlist::AbstractVector, - c_ops::Vector{QuantumObject{Tc, OperatorQuantumObject}}=QuantumObject{Matrix, OperatorQuantumObject}[]; - alg::OrdinaryDiffEq.OrdinaryDiffEqAlgorithm=Tsit5(), + c_ops::Union{Nothing,AbstractVector}=nothing; + alg::OrdinaryDiffEqAlgorithm=Tsit5(), e_ops::Union{Nothing,AbstractVector}=nothing, H_t::Union{Nothing,Function,TimeDependentOperatorSum}=nothing, params::NamedTuple=NamedTuple(), @@ -148,8 +148,8 @@ If the environmental measurements register a quantum jump, the wave function und - `H::QuantumObject`: Hamiltonian of the system ``\hat{H}``. - `ψ0::QuantumObject`: Initial state of the system ``|\psi(0)\rangle``. - `tlist::AbstractVector`: List of times at which to save the state of the system. -- `c_ops::Vector`: List of collapse operators ``\{\hat{C}_n\}_n``. -- `alg::OrdinaryDiffEq.OrdinaryDiffEqAlgorithm`: Algorithm to use for the time evolution. +- `c_ops::Union{Nothing,AbstractVector}`: List of collapse operators ``\{\hat{C}_n\}_n``. +- `alg::OrdinaryDiffEqAlgorithm`: Algorithm to use for the time evolution. - `e_ops::Union{Nothing,AbstractVector}`: List of operators for which to calculate expectation values. - `H_t::Union{Nothing,Function,TimeDependentOperatorSum}`: Time-dependent part of the Hamiltonian. - `params::NamedTuple`: Dictionary of parameters to pass to the solver. @@ -162,7 +162,8 @@ If the environmental measurements register a quantum jump, the wave function und - The states will be saved depend on the keyword argument `saveat` in `kwargs`. - If `e_ops` is specified, the default value of `saveat=[tlist[end]]` (only save the final state), otherwise, `saveat=tlist` (saving the states corresponding to `tlist`). You can also specify `e_ops` and `saveat` separately. - The default tolerances in `kwargs` are given as `reltol=1e-6` and `abstol=1e-8`. -- For more details about `alg` and extra `kwargs`, please refer to [`DifferentialEquations.jl`](https://diffeq.sciml.ai/stable/) +- For more details about `alg` please refer to [`DifferentialEquations.jl` (ODE Solvers)](https://docs.sciml.ai/DiffEqDocs/stable/solvers/ode_solve/) +- For more details about `kwargs` please refer to [`DifferentialEquations.jl` (Keyword Arguments)](https://docs.sciml.ai/DiffEqDocs/stable/basics/common_solver_opts/) # Returns @@ -170,25 +171,28 @@ If the environmental measurements register a quantum jump, the wave function und """ function mcsolveProblem( H::QuantumObject{MT1,OperatorQuantumObject}, - ψ0::QuantumObject{<:AbstractArray{T2},KetQuantumObject}, + ψ0::QuantumObject{<:AbstractArray,KetQuantumObject}, tlist::AbstractVector, - c_ops::Vector{QuantumObject{Tc,OperatorQuantumObject}} = QuantumObject{MT1,OperatorQuantumObject}[]; - alg::OrdinaryDiffEq.OrdinaryDiffEqAlgorithm = Tsit5(), + c_ops::Union{Nothing,AbstractVector} = nothing; + alg::OrdinaryDiffEqAlgorithm = Tsit5(), e_ops::Union{Nothing,AbstractVector} = nothing, H_t::Union{Nothing,Function,TimeDependentOperatorSum} = nothing, params::NamedTuple = NamedTuple(), seeds::Union{Nothing,Vector{Int}} = nothing, jump_callback::TJC = ContinuousLindbladJumpCallback(), kwargs..., -) where {MT1<:AbstractMatrix,T2,Tc<:AbstractMatrix,TJC<:LindbladJumpCallbackType} +) where {MT1<:AbstractMatrix,TJC<:LindbladJumpCallbackType} H.dims != ψ0.dims && throw(DimensionMismatch("The two quantum objects are not of the same Hilbert dimension.")) haskey(kwargs, :save_idxs) && throw(ArgumentError("The keyword argument \"save_idxs\" is not supported in QuantumToolbox.")) - t_l = convert(Vector{Float64}, tlist) # Convert it into Float64 to avoid type instabilities for OrdinaryDiffEq.jl + c_ops isa Nothing && + throw(ArgumentError("The list of collapse operators must be provided. Use sesolveProblem instead.")) + + t_l = convert(Vector{_FType(ψ0)}, tlist) # Convert it to support GPUs and avoid type instabilities for OrdinaryDiffEq.jl - H_eff = H - T2(0.5im) * mapreduce(op -> op' * op, +, c_ops) + H_eff = H - 1im * mapreduce(op -> op' * op, +, c_ops) / 2 if e_ops isa Nothing expvals = Array{ComplexF64}(undef, 0, length(t_l)) @@ -237,7 +241,7 @@ function mcsolveProblem( H_eff::QuantumObject{<:AbstractArray{T1},OperatorQuantumObject}, ψ0::QuantumObject{<:AbstractArray{T2},KetQuantumObject}, t_l::AbstractVector, - alg::OrdinaryDiffEq.OrdinaryDiffEqAlgorithm, + alg::OrdinaryDiffEqAlgorithm, H_t::Union{Nothing,Function,TimeDependentOperatorSum}, params::NamedTuple, jump_callback::DiscreteLindbladJumpCallback; @@ -254,15 +258,15 @@ function mcsolveProblem( end function mcsolveProblem( - H_eff::QuantumObject{<:AbstractArray{T1},OperatorQuantumObject}, - ψ0::QuantumObject{<:AbstractArray{T2},KetQuantumObject}, + H_eff::QuantumObject{<:AbstractArray,OperatorQuantumObject}, + ψ0::QuantumObject{<:AbstractArray,KetQuantumObject}, t_l::AbstractVector, - alg::OrdinaryDiffEq.OrdinaryDiffEqAlgorithm, + alg::OrdinaryDiffEqAlgorithm, H_t::Union{Nothing,Function,TimeDependentOperatorSum}, params::NamedTuple, jump_callback::ContinuousLindbladJumpCallback; kwargs..., -) where {T1,T2} +) cb1 = ContinuousCallback( LindbladJumpContinuousCondition, LindbladJumpAffect!, @@ -283,9 +287,9 @@ end mcsolveEnsembleProblem(H::QuantumObject{<:AbstractArray{T1},OperatorQuantumObject}, ψ0::QuantumObject{<:AbstractArray{T2},KetQuantumObject}, tlist::AbstractVector, - c_ops::Vector{QuantumObject{Tc, OperatorQuantumObject}}=QuantumObject{Matrix, OperatorQuantumObject}[]; - alg::OrdinaryDiffEq.OrdinaryDiffEqAlgorithm=Tsit5(), - e_ops::Vector{QuantumObject{Te, OperatorQuantumObject}}=QuantumObject{Matrix, OperatorQuantumObject}[], + c_ops::Union{Nothing,AbstractVector}=nothing; + alg::OrdinaryDiffEqAlgorithm=Tsit5(), + e_ops::Union{Nothing,AbstractVector}=nothing, H_t::Union{Nothing,Function,TimeDependentOperatorSum}=nothing, params::NamedTuple=NamedTuple(), jump_callback::TJC=ContinuousLindbladJumpCallback(), @@ -332,9 +336,9 @@ If the environmental measurements register a quantum jump, the wave function und - `H::QuantumObject`: Hamiltonian of the system ``\hat{H}``. - `ψ0::QuantumObject`: Initial state of the system ``|\psi(0)\rangle``. - `tlist::AbstractVector`: List of times at which to save the state of the system. -- `c_ops::Vector`: List of collapse operators ``\{\hat{C}_n\}_n``. -- `alg::OrdinaryDiffEq.OrdinaryDiffEqAlgorithm`: Algorithm to use for the time evolution. -- `e_ops::Vector`: List of operators for which to calculate expectation values. +- `c_ops::Union{Nothing,AbstractVector}`: List of collapse operators ``\{\hat{C}_n\}_n``. +- `alg::OrdinaryDiffEqAlgorithm`: Algorithm to use for the time evolution. +- `e_ops::Union{Nothing,AbstractVector}`: List of operators for which to calculate expectation values. - `H_t::Union{Nothing,Function,TimeDependentOperatorSum}`: Time-dependent part of the Hamiltonian. - `params::NamedTuple`: Dictionary of parameters to pass to the solver. - `seeds::Union{Nothing, Vector{Int}}`: List of seeds for the random number generator. Length must be equal to the number of trajectories provided. @@ -348,7 +352,8 @@ If the environmental measurements register a quantum jump, the wave function und - The states will be saved depend on the keyword argument `saveat` in `kwargs`. - If `e_ops` is specified, the default value of `saveat=[tlist[end]]` (only save the final state), otherwise, `saveat=tlist` (saving the states corresponding to `tlist`). You can also specify `e_ops` and `saveat` separately. - The default tolerances in `kwargs` are given as `reltol=1e-6` and `abstol=1e-8`. -- For more details about `alg` and extra `kwargs`, please refer to [`DifferentialEquations.jl`](https://diffeq.sciml.ai/stable/) +- For more details about `alg` please refer to [`DifferentialEquations.jl` (ODE Solvers)](https://docs.sciml.ai/DiffEqDocs/stable/solvers/ode_solve/) +- For more details about `kwargs` please refer to [`DifferentialEquations.jl` (Keyword Arguments)](https://docs.sciml.ai/DiffEqDocs/stable/basics/common_solver_opts/) # Returns @@ -358,9 +363,9 @@ function mcsolveEnsembleProblem( H::QuantumObject{MT1,OperatorQuantumObject}, ψ0::QuantumObject{<:AbstractArray{T2},KetQuantumObject}, tlist::AbstractVector, - c_ops::Vector{QuantumObject{Tc,OperatorQuantumObject}} = QuantumObject{MT1,OperatorQuantumObject}[]; - alg::OrdinaryDiffEq.OrdinaryDiffEqAlgorithm = Tsit5(), - e_ops::Vector{QuantumObject{Te,OperatorQuantumObject}} = QuantumObject{MT1,OperatorQuantumObject}[], + c_ops::Union{Nothing,AbstractVector} = nothing; + alg::OrdinaryDiffEqAlgorithm = Tsit5(), + e_ops::Union{Nothing,AbstractVector} = nothing, H_t::Union{Nothing,Function,TimeDependentOperatorSum} = nothing, params::NamedTuple = NamedTuple(), jump_callback::TJC = ContinuousLindbladJumpCallback(), @@ -368,7 +373,7 @@ function mcsolveEnsembleProblem( prob_func::Function = _mcsolve_prob_func, output_func::Function = _mcsolve_output_func, kwargs..., -) where {MT1<:AbstractMatrix,T2,Tc<:AbstractMatrix,Te<:AbstractMatrix,TJC<:LindbladJumpCallbackType} +) where {MT1<:AbstractMatrix,T2,TJC<:LindbladJumpCallbackType} prob_mc = mcsolveProblem( H, ψ0, @@ -392,9 +397,9 @@ end mcsolve(H::QuantumObject{<:AbstractArray{T1},OperatorQuantumObject}, ψ0::QuantumObject{<:AbstractArray{T2},KetQuantumObject}, tlist::AbstractVector, - c_ops::Vector{QuantumObject{Tc, OperatorQuantumObject}}=QuantumObject{Matrix, OperatorQuantumObject}[]; - alg::OrdinaryDiffEq.OrdinaryDiffEqAlgorithm=Tsit5(), - e_ops::Vector{QuantumObject{Te, OperatorQuantumObject}}=QuantumObject{Matrix, OperatorQuantumObject}[], + c_ops::Union{Nothing,AbstractVector}=nothing; + alg::OrdinaryDiffEqAlgorithm=Tsit5(), + e_ops::Union{Nothing,AbstractVector}=nothing, H_t::Union{Nothing,Function,TimeDependentOperatorSum}=nothing, params::NamedTuple=NamedTuple(), n_traj::Int=1, @@ -441,9 +446,9 @@ If the environmental measurements register a quantum jump, the wave function und - `H::QuantumObject`: Hamiltonian of the system ``\hat{H}``. - `ψ0::QuantumObject`: Initial state of the system ``|\psi(0)\rangle``. - `tlist::AbstractVector`: List of times at which to save the state of the system. -- `c_ops::Vector`: List of collapse operators ``\{\hat{C}_n\}_n``. -- `alg::OrdinaryDiffEq.OrdinaryDiffEqAlgorithm`: Algorithm to use for the time evolution. -- `e_ops::Vector`: List of operators for which to calculate expectation values. +- `c_ops::Union{Nothing,AbstractVector}`: List of collapse operators ``\{\hat{C}_n\}_n``. +- `alg::OrdinaryDiffEqAlgorithm`: Algorithm to use for the time evolution. +- `e_ops::Union{Nothing,AbstractVector}`: List of operators for which to calculate expectation values. - `H_t::Union{Nothing,Function,TimeDependentOperatorSum}`: Time-dependent part of the Hamiltonian. - `params::NamedTuple`: Dictionary of parameters to pass to the solver. - `seeds::Union{Nothing, Vector{Int}}`: List of seeds for the random number generator. Length must be equal to the number of trajectories provided. @@ -460,7 +465,8 @@ If the environmental measurements register a quantum jump, the wave function und - The states will be saved depend on the keyword argument `saveat` in `kwargs`. - If `e_ops` is specified, the default value of `saveat=[tlist[end]]` (only save the final state), otherwise, `saveat=tlist` (saving the states corresponding to `tlist`). You can also specify `e_ops` and `saveat` separately. - The default tolerances in `kwargs` are given as `reltol=1e-6` and `abstol=1e-8`. -- For more details about `alg` and extra `kwargs`, please refer to [`DifferentialEquations.jl`](https://diffeq.sciml.ai/stable/) +- For more details about `alg` please refer to [`DifferentialEquations.jl` (ODE Solvers)](https://docs.sciml.ai/DiffEqDocs/stable/solvers/ode_solve/) +- For more details about `kwargs` please refer to [`DifferentialEquations.jl` (Keyword Arguments)](https://docs.sciml.ai/DiffEqDocs/stable/basics/common_solver_opts/) # Returns @@ -470,9 +476,9 @@ function mcsolve( H::QuantumObject{MT1,OperatorQuantumObject}, ψ0::QuantumObject{<:AbstractArray{T2},KetQuantumObject}, tlist::AbstractVector, - c_ops::Vector{QuantumObject{Tc,OperatorQuantumObject}} = QuantumObject{MT1,OperatorQuantumObject}[]; - alg::OrdinaryDiffEq.OrdinaryDiffEqAlgorithm = Tsit5(), - e_ops::Vector{QuantumObject{Te,OperatorQuantumObject}} = QuantumObject{MT1,OperatorQuantumObject}[], + c_ops::Union{Nothing,AbstractVector} = nothing; + alg::OrdinaryDiffEqAlgorithm = Tsit5(), + e_ops::Union{Nothing,AbstractVector} = nothing, H_t::Union{Nothing,Function,TimeDependentOperatorSum} = nothing, params::NamedTuple = NamedTuple(), seeds::Union{Nothing,Vector{Int}} = nothing, @@ -482,7 +488,7 @@ function mcsolve( prob_func::Function = _mcsolve_prob_func, output_func::Function = _mcsolve_output_func, kwargs..., -) where {MT1<:AbstractMatrix,T2,Tc<:AbstractMatrix,Te<:AbstractMatrix,TJC<:LindbladJumpCallbackType} +) where {MT1<:AbstractMatrix,T2,TJC<:LindbladJumpCallbackType} if !isnothing(seeds) && length(seeds) != n_traj throw(ArgumentError("Length of seeds must match n_traj ($n_traj), but got $(length(seeds))")) end @@ -508,7 +514,7 @@ end function mcsolve( ens_prob_mc::EnsembleProblem; - alg::OrdinaryDiffEq.OrdinaryDiffEqAlgorithm = Tsit5(), + alg::OrdinaryDiffEqAlgorithm = Tsit5(), n_traj::Int = 1, ensemble_method = EnsembleThreads(), ) @@ -524,7 +530,7 @@ function mcsolve( jump_which = Vector{Vector{Int16}}(undef, length(sol)) foreach( - i -> _mcsolve_generate_statistics(sol, i, times, states, expvals_all, jump_times, jump_which), + i -> _mcsolve_generate_statistics!(sol, i, times, states, expvals_all, jump_times, jump_which), eachindex(sol), ) expvals = dropdims(sum(expvals_all, dims = 1), dims = 1) ./ length(sol) diff --git a/src/time_evolution/mesolve.jl b/src/time_evolution/mesolve.jl index f8a7c39e..64cc2bdc 100644 --- a/src/time_evolution/mesolve.jl +++ b/src/time_evolution/mesolve.jl @@ -52,8 +52,8 @@ end mesolveProblem(H::QuantumObject, ψ0::QuantumObject, tlist::AbstractVector, - c_ops::AbstractVector=[]; - alg::OrdinaryDiffEq.OrdinaryDiffEqAlgorithm=Tsit5(), + c_ops::Union{Nothing,AbstractVector}=nothing; + alg::OrdinaryDiffEqAlgorithm=Tsit5(), e_ops::Union{Nothing,AbstractVector}=nothing, H_t::Union{Nothing,Function,TimeDependentOperatorSum}=nothing, params::NamedTuple=NamedTuple(), @@ -63,13 +63,13 @@ end Generates the ODEProblem for the master equation time evolution of an open quantum system: ```math -\frac{\partial \rho(t)}{\partial t} = -i[\hat{H}, \rho(t)] + \sum_n \mathcal{D}(\hat{C}_n) [\rho(t)] +\frac{\partial \hat{\rho}(t)}{\partial t} = -i[\hat{H}, \hat{\rho}(t)] + \sum_n \mathcal{D}(\hat{C}_n) [\hat{\rho}(t)] ``` where ```math -\mathcal{D}(\hat{C}_n) [\rho(t)] = \hat{C}_n \rho(t) \hat{C}_n^\dagger - \frac{1}{2} \hat{C}_n^\dagger \hat{C}_n \rho(t) - \frac{1}{2} \rho(t) \hat{C}_n^\dagger \hat{C}_n +\mathcal{D}(\hat{C}_n) [\hat{\rho}(t)] = \hat{C}_n \hat{\rho}(t) \hat{C}_n^\dagger - \frac{1}{2} \hat{C}_n^\dagger \hat{C}_n \hat{\rho}(t) - \frac{1}{2} \hat{\rho}(t) \hat{C}_n^\dagger \hat{C}_n ``` # Arguments @@ -77,8 +77,8 @@ where - `H::QuantumObject`: The Hamiltonian ``\hat{H}`` or the Liouvillian of the system. - `ψ0::QuantumObject`: The initial state of the system. - `tlist::AbstractVector`: The time list of the evolution. -- `c_ops::AbstractVector=[]`: The list of the collapse operators ``\{\hat{C}_n\}_n``. -- `alg::OrdinaryDiffEq.OrdinaryDiffEqAlgorithm=Tsit5()`: The algorithm used for the time evolution. +- `c_ops::Union{Nothing,AbstractVector}=nothing`: The list of the collapse operators ``\{\hat{C}_n\}_n``. +- `alg::OrdinaryDiffEqAlgorithm=Tsit5()`: The algorithm used for the time evolution. - `e_ops::Union{Nothing,AbstractVector}=nothing`: The list of the operators for which the expectation values are calculated. - `H_t::Union{Nothing,Function,TimeDependentOperatorSum}=nothing`: The time-dependent Hamiltonian or Liouvillian. - `params::NamedTuple=NamedTuple()`: The parameters of the time evolution. @@ -90,7 +90,8 @@ where - The states will be saved depend on the keyword argument `saveat` in `kwargs`. - If `e_ops` is specified, the default value of `saveat=[tlist[end]]` (only save the final state), otherwise, `saveat=tlist` (saving the states corresponding to `tlist`). You can also specify `e_ops` and `saveat` separately. - The default tolerances in `kwargs` are given as `reltol=1e-6` and `abstol=1e-8`. -- For more details about `alg` and extra `kwargs`, please refer to [`DifferentialEquations.jl`](https://diffeq.sciml.ai/stable/) +- For more details about `alg` please refer to [`DifferentialEquations.jl` (ODE Solvers)](https://docs.sciml.ai/DiffEqDocs/stable/solvers/ode_solve/) +- For more details about `kwargs` please refer to [`DifferentialEquations.jl` (Keyword Arguments)](https://docs.sciml.ai/DiffEqDocs/stable/basics/common_solver_opts/) # Returns @@ -100,8 +101,8 @@ function mesolveProblem( H::QuantumObject{MT1,HOpType}, ψ0::QuantumObject{<:AbstractArray{T2},StateOpType}, tlist, - c_ops::Vector{QuantumObject{Tc,COpType}} = QuantumObject{MT1,HOpType}[]; - alg::OrdinaryDiffEq.OrdinaryDiffEqAlgorithm = Tsit5(), + c_ops::Union{Nothing,AbstractVector} = nothing; + alg::OrdinaryDiffEqAlgorithm = Tsit5(), e_ops::Union{Nothing,AbstractVector} = nothing, H_t::Union{Nothing,Function,TimeDependentOperatorSum} = nothing, params::NamedTuple = NamedTuple(), @@ -110,10 +111,8 @@ function mesolveProblem( ) where { MT1<:AbstractMatrix, T2, - Tc<:AbstractMatrix, HOpType<:Union{OperatorQuantumObject,SuperOperatorQuantumObject}, StateOpType<:Union{KetQuantumObject,OperatorQuantumObject}, - COpType<:Union{OperatorQuantumObject,SuperOperatorQuantumObject}, } H.dims != ψ0.dims && throw(DimensionMismatch("The two quantum objects are not of the same Hilbert dimension.")) @@ -123,9 +122,9 @@ function mesolveProblem( is_time_dependent = !(H_t isa Nothing) progress_bar_val = makeVal(progress_bar) - t_l = convert(Vector{Float64}, tlist) # Convert it into Float64 to avoid type instabilities for OrdinaryDiffEq.jl + ρ0 = sparse_to_dense(_CType(ψ0), mat2vec(ket2dm(ψ0).data)) # Convert it to dense vector with complex element type - ρ0 = mat2vec(ket2dm(ψ0).data) + t_l = convert(Vector{_FType(ψ0)}, tlist) # Convert it to support GPUs and avoid type instabilities for OrdinaryDiffEq.jl L = liouvillian(H, c_ops).data progr = ProgressBar(length(t_l), enable = getVal(progress_bar_val)) @@ -140,6 +139,10 @@ function mesolveProblem( is_empty_e_ops = isempty(e_ops) end + if H_t isa TimeDependentOperatorSum + H_t = liouvillian(H_t) + end + p = ( L = L, progr = progr, @@ -159,14 +162,14 @@ function mesolveProblem( dudt! = is_time_dependent ? mesolve_td_dudt! : mesolve_ti_dudt! tspan = (t_l[1], t_l[end]) - return ODEProblem{true,SciMLBase.FullSpecialize}(dudt!, ρ0, tspan, p; kwargs3...) + return ODEProblem{true,FullSpecialize}(dudt!, ρ0, tspan, p; kwargs3...) end @doc raw""" mesolve(H::QuantumObject, ψ0::QuantumObject, tlist::AbstractVector, - c_ops::AbstractVector=[]; + c_ops::Union{Nothing,AbstractVector}=nothing; alg::OrdinaryDiffEqAlgorithm=Tsit5(), e_ops::Union{Nothing,AbstractVector}=nothing, H_t::Union{Nothing,Function,TimeDependentOperatorSum}=nothing, @@ -177,13 +180,13 @@ end Time evolution of an open quantum system using Lindblad master equation: ```math -\frac{\partial \rho(t)}{\partial t} = -i[\hat{H}, \rho(t)] + \sum_n \mathcal{D}(\hat{C}_n) [\rho(t)] +\frac{\partial \hat{\rho}(t)}{\partial t} = -i[\hat{H}, \hat{\rho}(t)] + \sum_n \mathcal{D}(\hat{C}_n) [\hat{\rho}(t)] ``` where ```math -\mathcal{D}(\hat{C}_n) [\rho(t)] = \hat{C}_n \rho(t) \hat{C}_n^\dagger - \frac{1}{2} \hat{C}_n^\dagger \hat{C}_n \rho(t) - \frac{1}{2} \rho(t) \hat{C}_n^\dagger \hat{C}_n +\mathcal{D}(\hat{C}_n) [\hat{\rho}(t)] = \hat{C}_n \hat{\rho}(t) \hat{C}_n^\dagger - \frac{1}{2} \hat{C}_n^\dagger \hat{C}_n \hat{\rho}(t) - \frac{1}{2} \hat{\rho}(t) \hat{C}_n^\dagger \hat{C}_n ``` # Arguments @@ -191,7 +194,7 @@ where - `H::QuantumObject`: The Hamiltonian ``\hat{H}`` or the Liouvillian of the system. - `ψ0::QuantumObject`: The initial state of the system. - `tlist::AbstractVector`: The time list of the evolution. -- `c_ops::AbstractVector=[]`: The list of the collapse operators ``\{\hat{C}_n\}_n``. +- `c_ops::Union{Nothing,AbstractVector}=nothing`: The list of the collapse operators ``\{\hat{C}_n\}_n``. - `alg::OrdinaryDiffEqAlgorithm`: Algorithm to use for the time evolution. - `e_ops::Union{Nothing,AbstractVector}`: List of operators for which to calculate expectation values. - `H_t::Union{Nothing,Function,TimeDependentOperatorSum}`: Time-dependent part of the Hamiltonian. @@ -204,7 +207,8 @@ where - The states will be saved depend on the keyword argument `saveat` in `kwargs`. - If `e_ops` is specified, the default value of `saveat=[tlist[end]]` (only save the final state), otherwise, `saveat=tlist` (saving the states corresponding to `tlist`). You can also specify `e_ops` and `saveat` separately. - The default tolerances in `kwargs` are given as `reltol=1e-6` and `abstol=1e-8`. -- For more details about `alg` and extra `kwargs`, please refer to [`DifferentialEquations.jl`](https://diffeq.sciml.ai/stable/) +- For more details about `alg` please refer to [`DifferentialEquations.jl` (ODE Solvers)](https://docs.sciml.ai/DiffEqDocs/stable/solvers/ode_solve/) +- For more details about `kwargs` please refer to [`DifferentialEquations.jl` (Keyword Arguments)](https://docs.sciml.ai/DiffEqDocs/stable/basics/common_solver_opts/) # Returns @@ -214,7 +218,7 @@ function mesolve( H::QuantumObject{MT1,HOpType}, ψ0::QuantumObject{<:AbstractArray{T2},StateOpType}, tlist::AbstractVector, - c_ops::Vector{QuantumObject{Tc,COpType}} = QuantumObject{MT1,HOpType}[]; + c_ops::Union{Nothing,AbstractVector} = nothing; alg::OrdinaryDiffEqAlgorithm = Tsit5(), e_ops::Union{Nothing,AbstractVector} = nothing, H_t::Union{Nothing,Function,TimeDependentOperatorSum} = nothing, @@ -224,10 +228,8 @@ function mesolve( ) where { MT1<:AbstractMatrix, T2, - Tc<:AbstractMatrix, HOpType<:Union{OperatorQuantumObject,SuperOperatorQuantumObject}, StateOpType<:Union{KetQuantumObject,OperatorQuantumObject}, - COpType<:Union{OperatorQuantumObject,SuperOperatorQuantumObject}, } prob = mesolveProblem( H, diff --git a/src/time_evolution/sesolve.jl b/src/time_evolution/sesolve.jl index f6e9d5bd..264a6527 100644 --- a/src/time_evolution/sesolve.jl +++ b/src/time_evolution/sesolve.jl @@ -47,7 +47,7 @@ end sesolveProblem(H::QuantumObject, ψ0::QuantumObject, tlist::AbstractVector; - alg::OrdinaryDiffEq.OrdinaryDiffEqAlgorithm=Tsit5() + alg::OrdinaryDiffEqAlgorithm=Tsit5() e_ops::Union{Nothing,AbstractVector} = nothing, H_t::Union{Nothing,Function,TimeDependentOperatorSum}=nothing, params::NamedTuple=NamedTuple(), @@ -65,7 +65,7 @@ Generates the ODEProblem for the Schrödinger time evolution of a quantum system - `H::QuantumObject`: The Hamiltonian of the system ``\hat{H}``. - `ψ0::QuantumObject`: The initial state of the system ``|\psi(0)\rangle``. - `tlist::AbstractVector`: The time list of the evolution. -- `alg::OrdinaryDiffEq.OrdinaryDiffEqAlgorithm`: The algorithm used for the time evolution. +- `alg::OrdinaryDiffEqAlgorithm`: The algorithm used for the time evolution. - `e_ops::Union{Nothing,AbstractVector}`: The list of operators to be evaluated during the evolution. - `H_t::Union{Nothing,Function,TimeDependentOperatorSum}`: The time-dependent Hamiltonian of the system. If `nothing`, the Hamiltonian is time-independent. - `params::NamedTuple`: The parameters of the system. @@ -77,7 +77,8 @@ Generates the ODEProblem for the Schrödinger time evolution of a quantum system - The states will be saved depend on the keyword argument `saveat` in `kwargs`. - If `e_ops` is specified, the default value of `saveat=[tlist[end]]` (only save the final state), otherwise, `saveat=tlist` (saving the states corresponding to `tlist`). You can also specify `e_ops` and `saveat` separately. - The default tolerances in `kwargs` are given as `reltol=1e-6` and `abstol=1e-8`. -- For more details about `alg` and extra `kwargs`, please refer to [`DifferentialEquations.jl`](https://diffeq.sciml.ai/stable/) +- For more details about `alg` please refer to [`DifferentialEquations.jl` (ODE Solvers)](https://docs.sciml.ai/DiffEqDocs/stable/solvers/ode_solve/) +- For more details about `kwargs` please refer to [`DifferentialEquations.jl` (Keyword Arguments)](https://docs.sciml.ai/DiffEqDocs/stable/basics/common_solver_opts/) # Returns @@ -87,7 +88,7 @@ function sesolveProblem( H::QuantumObject{MT1,OperatorQuantumObject}, ψ0::QuantumObject{<:AbstractVector{T2},KetQuantumObject}, tlist::AbstractVector; - alg::OrdinaryDiffEq.OrdinaryDiffEqAlgorithm = Tsit5(), + alg::OrdinaryDiffEqAlgorithm = Tsit5(), e_ops::Union{Nothing,AbstractVector} = nothing, H_t::Union{Nothing,Function,TimeDependentOperatorSum} = nothing, params::NamedTuple = NamedTuple(), @@ -102,9 +103,9 @@ function sesolveProblem( is_time_dependent = !(H_t isa Nothing) progress_bar_val = makeVal(progress_bar) - t_l = convert(Vector{Float64}, tlist) # Convert it into Float64 to avoid type instabilities for OrdinaryDiffEq.jl + ϕ0 = sparse_to_dense(_CType(ψ0), get_data(ψ0)) # Convert it to dense vector with complex element type - ϕ0 = get_data(ψ0) + t_l = convert(Vector{_FType(ψ0)}, tlist) # Convert it to support GPUs and avoid type instabilities for OrdinaryDiffEq.jl U = -1im * get_data(H) progr = ProgressBar(length(t_l), enable = getVal(progress_bar_val)) @@ -138,14 +139,14 @@ function sesolveProblem( dudt! = is_time_dependent ? sesolve_td_dudt! : sesolve_ti_dudt! tspan = (t_l[1], t_l[end]) - return ODEProblem{true,SciMLBase.FullSpecialize}(dudt!, ϕ0, tspan, p; kwargs3...) + return ODEProblem{true,FullSpecialize}(dudt!, ϕ0, tspan, p; kwargs3...) end @doc raw""" sesolve(H::QuantumObject, ψ0::QuantumObject, tlist::AbstractVector; - alg::OrdinaryDiffEq.OrdinaryDiffEqAlgorithm=Tsit5(), + alg::OrdinaryDiffEqAlgorithm=Tsit5(), e_ops::Union{Nothing,AbstractVector} = nothing, H_t::Union{Nothing,Function,TimeDependentOperatorSum}=nothing, params::NamedTuple=NamedTuple(), @@ -163,7 +164,7 @@ Time evolution of a closed quantum system using the Schrödinger equation: - `H::QuantumObject`: The Hamiltonian of the system ``\hat{H}``. - `ψ0::QuantumObject`: The initial state of the system ``|\psi(0)\rangle``. - `tlist::AbstractVector`: List of times at which to save the state of the system. -- `alg::OrdinaryDiffEq.OrdinaryDiffEqAlgorithm`: Algorithm to use for the time evolution. +- `alg::OrdinaryDiffEqAlgorithm`: Algorithm to use for the time evolution. - `e_ops::Union{Nothing,AbstractVector}`: List of operators for which to calculate expectation values. - `H_t::Union{Nothing,Function,TimeDependentOperatorSum}`: Time-dependent part of the Hamiltonian. - `params::NamedTuple`: Dictionary of parameters to pass to the solver. @@ -175,7 +176,8 @@ Time evolution of a closed quantum system using the Schrödinger equation: - The states will be saved depend on the keyword argument `saveat` in `kwargs`. - If `e_ops` is specified, the default value of `saveat=[tlist[end]]` (only save the final state), otherwise, `saveat=tlist` (saving the states corresponding to `tlist`). You can also specify `e_ops` and `saveat` separately. - The default tolerances in `kwargs` are given as `reltol=1e-6` and `abstol=1e-8`. -- For more details about `alg` and extra `kwargs`, please refer to [`DifferentialEquations.jl`](https://diffeq.sciml.ai/stable/) +- For more details about `alg` please refer to [`DifferentialEquations.jl` (ODE Solvers)](https://docs.sciml.ai/DiffEqDocs/stable/solvers/ode_solve/) +- For more details about `kwargs` please refer to [`DifferentialEquations.jl` (Keyword Arguments)](https://docs.sciml.ai/DiffEqDocs/stable/basics/common_solver_opts/) # Returns @@ -185,7 +187,7 @@ function sesolve( H::QuantumObject{MT1,OperatorQuantumObject}, ψ0::QuantumObject{<:AbstractVector{T2},KetQuantumObject}, tlist::AbstractVector; - alg::OrdinaryDiffEq.OrdinaryDiffEqAlgorithm = Tsit5(), + alg::OrdinaryDiffEqAlgorithm = Tsit5(), e_ops::Union{Nothing,AbstractVector} = nothing, H_t::Union{Nothing,Function,TimeDependentOperatorSum} = nothing, params::NamedTuple = NamedTuple(), @@ -207,7 +209,7 @@ function sesolve( return sesolve(prob, alg) end -function sesolve(prob::ODEProblem, alg::OrdinaryDiffEq.OrdinaryDiffEqAlgorithm = Tsit5()) +function sesolve(prob::ODEProblem, alg::OrdinaryDiffEqAlgorithm = Tsit5()) sol = solve(prob, alg) ψt = map(ϕ -> QuantumObject(ϕ, type = Ket, dims = sol.prob.p.Hdims), sol.u) diff --git a/src/time_evolution/ssesolve.jl b/src/time_evolution/ssesolve.jl new file mode 100644 index 00000000..378a6501 --- /dev/null +++ b/src/time_evolution/ssesolve.jl @@ -0,0 +1,406 @@ +export ssesolveProblem, ssesolveEnsembleProblem, ssesolve + +#TODO: Check if works in GPU +function _ssesolve_update_coefficients!(ψ, coefficients, sc_ops) + _get_en = op -> real(dot(ψ, op, ψ)) #this is en/2: /2 = Re + @. coefficients[2:end-1] = _get_en(sc_ops) #coefficients of the OperatorSum: Σ Sn * en/2 + coefficients[end] = -sum(x -> x^2, coefficients[2:end-1]) / 2 #this last coefficient is -Σen^2/8 + return nothing +end + +function ssesolve_drift!(du, u, p, t) + _ssesolve_update_coefficients!(u, p.K.coefficients, p.sc_ops) + + mul!(du, p.K, u) + + return nothing +end + +function ssesolve_diffusion!(du, u, p, t) + @inbounds en = @view(p.K.coefficients[2:end-1]) + + # du:(H,W). du_reshaped:(H*W,). + # H:Hilbert space dimension, W: number of sc_ops + du_reshaped = reshape(du, :) + mul!(du_reshaped, p.D, u) #du[:,i] = D[i] * u + + du .-= u .* reshape(en, 1, :) #du[:,i] -= en[i] * u + + return nothing +end + +function _ssesolve_prob_func(prob, i, repeat) + internal_params = prob.p + + noise = RealWienerProcess!( + prob.tspan[1], + zeros(length(internal_params.sc_ops)), + zeros(length(internal_params.sc_ops)), + save_everystep = false, + ) + + prm = merge( + internal_params, + ( + expvals = similar(internal_params.expvals), + progr = ProgressBar(size(internal_params.expvals, 2), enable = false), + ), + ) + + return remake(prob, p = prm, noise = noise) +end + +_ssesolve_output_func(sol, i) = (sol, false) + +function _ssesolve_generate_statistics!(sol, i, states, expvals_all) + sol_i = sol[:, i] + !isempty(sol_i.prob.kwargs[:saveat]) ? + states[i] = [QuantumObject(sol_i.u[i], dims = sol_i.prob.p.Hdims) for i in 1:length(sol_i.u)] : nothing + + copyto!(view(expvals_all, i, :, :), sol_i.prob.p.expvals) + return nothing +end + +@doc raw""" + ssesolveProblem(H::QuantumObject, + ψ0::QuantumObject, + tlist::AbstractVector; + sc_ops::Vector{QuantumObject{Tc, OperatorQuantumObject}}=QuantumObject{Matrix, OperatorQuantumObject}[]; + alg::StochasticDiffEq.StochasticDiffEqAlgorithm=SRA1() + e_ops::Union{Nothing,AbstractVector} = nothing, + H_t::Union{Nothing,Function,TimeDependentOperatorSum}=nothing, + params::NamedTuple=NamedTuple(), + progress_bar::Union{Val,Bool}=Val(true), + kwargs...) + +Generates the SDEProblem for the Stochastic Schrödinger time evolution of a quantum system. This is defined by the following stochastic differential equation: + + ```math + d|\psi(t)\rangle = -i K |\psi(t)\rangle dt + \sum_n M_n |\psi(t)\rangle dW_n(t) + ``` + +where + +```math + K = \hat{H} + i \sum_n \left(\frac{e_j} C_n - \frac{1}{2} \sum_{j} C_n^\dagger C_n - \frac{e_j^2}{8}\right), + ``` + ```math + M_n = C_n - \frac{e_n}{2}, + ``` + and + ```math + e_n = \langle C_n + C_n^\dagger \rangle. + ``` + +Above, `C_n` is the `n`-th collapse operator and `dW_j(t)` is the real Wiener increment associated to `C_n`. + +# Arguments + +- `H::QuantumObject`: The Hamiltonian of the system ``\hat{H}``. +- `ψ0::QuantumObject`: The initial state of the system ``|\psi(0)\rangle``. +- `tlist::AbstractVector`: The time list of the evolution. +- `sc_ops::Vector`: List of stochastic collapse operators ``\{\hat{C}_n\}_n``. +- `alg::StochasticDiffEq.StochasticDiffEqAlgorithm`: The algorithm used for the time evolution. +- `e_ops::Union{Nothing,AbstractVector}`: The list of operators to be evaluated during the evolution. +- `H_t::Union{Nothing,Function,TimeDependentOperatorSum}`: The time-dependent Hamiltonian of the system. If `nothing`, the Hamiltonian is time-independent. +- `params::NamedTuple`: The parameters of the system. +- `progress_bar::Union{Val,Bool}`: Whether to show the progress bar. Using non-`Val` types might lead to type instabilities. +- `kwargs...`: The keyword arguments passed to the `ODEProblem` constructor. + +# Notes + +- The states will be saved depend on the keyword argument `saveat` in `kwargs`. +- If `e_ops` is specified, the default value of `saveat=[tlist[end]]` (only save the final state), otherwise, `saveat=tlist` (saving the states corresponding to `tlist`). You can also specify `e_ops` and `saveat` separately. +- For more details about `alg` and extra `kwargs`, please refer to [`DifferentialEquations.jl`](https://diffeq.sciml.ai/stable/) + +# Returns + +- `prob`: The `SDEProblem` for the Stochastic Schrödinger time evolution of the system. +""" +function ssesolveProblem( + H::QuantumObject{MT1,OperatorQuantumObject}, + ψ0::QuantumObject{<:AbstractArray{T2},KetQuantumObject}, + tlist::AbstractVector, + sc_ops::Vector{QuantumObject{Tc,OperatorQuantumObject}} = QuantumObject{MT1,OperatorQuantumObject}[]; + alg::StochasticDiffEq.StochasticDiffEqAlgorithm = SRA1(), + e_ops::Union{Nothing,AbstractVector} = nothing, + H_t::Union{Nothing,Function,TimeDependentOperatorSum} = nothing, + params::NamedTuple = NamedTuple(), + progress_bar::Union{Val,Bool} = Val(true), + kwargs..., +) where {MT1<:AbstractMatrix,T2,Tc<:AbstractMatrix} + H.dims != ψ0.dims && throw(DimensionMismatch("The two quantum objects are not of the same Hilbert dimension.")) + + haskey(kwargs, :save_idxs) && + throw(ArgumentError("The keyword argument \"save_idxs\" is not supported in QuantumToolbox.")) + + !(H_t isa Nothing) && throw(ArgumentError("Time-dependent Hamiltonians are not currently supported in ssesolve.")) + + progress_bar_val = makeVal(progress_bar) + + t_l = convert(Vector{Float64}, tlist) # Convert it into Float64 to avoid type instabilities for StochasticDiffEq.jl + + ϕ0 = get_data(ψ0) + + H_eff = get_data(H - T2(0.5im) * mapreduce(op -> op' * op, +, sc_ops)) + sc_ops2 = get_data.(sc_ops) + + coefficients = [1.0, fill(0.0, length(sc_ops) + 1)...] + operators = [-1im * H_eff, sc_ops2..., MT1(I(prod(H.dims)))] + K = OperatorSum(coefficients, operators) + _ssesolve_update_coefficients!(ϕ0, K.coefficients, sc_ops2) + + D = reduce(vcat, sc_ops2) + + progr = ProgressBar(length(t_l), enable = getVal(progress_bar_val)) + + if e_ops isa Nothing + expvals = Array{ComplexF64}(undef, 0, length(t_l)) + e_ops2 = MT1[] + is_empty_e_ops = true + else + expvals = Array{ComplexF64}(undef, length(e_ops), length(t_l)) + e_ops2 = get_data.(e_ops) + is_empty_e_ops = isempty(e_ops) + end + + p = ( + K = K, + D = D, + e_ops = e_ops2, + sc_ops = sc_ops2, + expvals = expvals, + progr = progr, + Hdims = H.dims, + H_t = H_t, + is_empty_e_ops = is_empty_e_ops, + params..., + ) + + saveat = e_ops isa Nothing ? t_l : [t_l[end]] + default_values = (DEFAULT_SDE_SOLVER_OPTIONS..., saveat = saveat) + kwargs2 = merge(default_values, kwargs) + kwargs3 = _generate_sesolve_kwargs(e_ops, progress_bar_val, t_l, kwargs2) + + tspan = (t_l[1], t_l[end]) + noise = RealWienerProcess!(t_l[1], zeros(length(sc_ops)), zeros(length(sc_ops)), save_everystep = false) + noise_rate_prototype = similar(ϕ0, length(ϕ0), length(sc_ops)) + return SDEProblem{true}( + ssesolve_drift!, + ssesolve_diffusion!, + ϕ0, + tspan, + p; + noise_rate_prototype = noise_rate_prototype, + noise = noise, + kwargs3..., + ) +end + +@doc raw""" + ssesolveEnsembleProblem(H::QuantumObject, + ψ0::QuantumObject, + tlist::AbstractVector; + sc_ops::Vector{QuantumObject{Tc, OperatorQuantumObject}}=QuantumObject{Matrix, OperatorQuantumObject}[]; + alg::StochasticDiffEq.StochasticDiffEqAlgorithm=SRA1() + e_ops::Union{Nothing,AbstractVector} = nothing, + H_t::Union{Nothing,Function,TimeDependentOperatorSum}=nothing, + params::NamedTuple=NamedTuple(), + prob_func::Function=_mcsolve_prob_func, + output_func::Function=_mcsolve_output_func, + kwargs...) + +Generates the SDE EnsembleProblem for the Stochastic Schrödinger time evolution of a quantum system. This is defined by the following stochastic differential equation: + + ```math + d|\psi(t)\rangle = -i K |\psi(t)\rangle dt + \sum_n M_n |\psi(t)\rangle dW_n(t) + ``` + +where + +```math + K = \hat{H} + i \sum_n \left(\frac{e_j} C_n - \frac{1}{2} \sum_{j} C_n^\dagger C_n - \frac{e_j^2}{8}\right), + ``` + ```math + M_n = C_n - \frac{e_n}{2}, + ``` + and + ```math + e_n = \langle C_n + C_n^\dagger \rangle. + ``` + +Above, `C_n` is the `n`-th collapse operator and `dW_j(t)` is the real Wiener increment associated to `C_n`. + +# Arguments + +- `H::QuantumObject`: The Hamiltonian of the system ``\hat{H}``. +- `ψ0::QuantumObject`: The initial state of the system ``|\psi(0)\rangle``. +- `tlist::AbstractVector`: The time list of the evolution. +- `sc_ops::Vector`: List of stochastic collapse operators ``\{\hat{C}_n\}_n``. +- `alg::StochasticDiffEq.StochasticDiffEqAlgorithm`: The algorithm used for the time evolution. +- `e_ops::Union{Nothing,AbstractVector}`: The list of operators to be evaluated during the evolution. +- `H_t::Union{Nothing,Function,TimeDependentOperatorSum}`: The time-dependent Hamiltonian of the system. If `nothing`, the Hamiltonian is time-independent. +- `params::NamedTuple`: The parameters of the system. +- `prob_func::Function`: Function to use for generating the SDEProblem. +- `output_func::Function`: Function to use for generating the output of a single trajectory. +- `kwargs...`: The keyword arguments passed to the `ODEProblem` constructor. + +# Notes + +- The states will be saved depend on the keyword argument `saveat` in `kwargs`. +- If `e_ops` is specified, the default value of `saveat=[tlist[end]]` (only save the final state), otherwise, `saveat=tlist` (saving the states corresponding to `tlist`). You can also specify `e_ops` and `saveat` separately. +- For more details about `alg` and extra `kwargs`, please refer to [`DifferentialEquations.jl`](https://diffeq.sciml.ai/stable/) + +# Returns + +- `prob::EnsembleProblem with SDEProblem`: The Ensemble SDEProblem for the Stochastic Shrödinger time evolution. +""" +function ssesolveEnsembleProblem( + H::QuantumObject{MT1,OperatorQuantumObject}, + ψ0::QuantumObject{<:AbstractArray{T2},KetQuantumObject}, + tlist::AbstractVector, + sc_ops::Vector{QuantumObject{Tc,OperatorQuantumObject}} = QuantumObject{MT1,OperatorQuantumObject}[]; + alg::StochasticDiffEq.StochasticDiffEqAlgorithm = SRA1(), + e_ops::Union{Nothing,AbstractVector} = nothing, + H_t::Union{Nothing,Function,TimeDependentOperatorSum} = nothing, + params::NamedTuple = NamedTuple(), + prob_func::Function = _ssesolve_prob_func, + output_func::Function = _ssesolve_output_func, + kwargs..., +) where {MT1<:AbstractMatrix,T2,Tc<:AbstractMatrix} + prob_sse = ssesolveProblem(H, ψ0, tlist, sc_ops; alg = alg, e_ops = e_ops, H_t = H_t, params = params, kwargs...) + + ensemble_prob = EnsembleProblem(prob_sse, prob_func = prob_func, output_func = output_func) + + return ensemble_prob +end + +@doc raw""" + ssesolve(H::QuantumObject, + ψ0::QuantumObject{<:AbstractArray{T2},KetQuantumObject}, + tlist::AbstractVector, + sc_ops::Vector{QuantumObject{Tc, OperatorQuantumObject}}=QuantumObject{Matrix, OperatorQuantumObject}[]; + alg::StochasticDiffEq.StochasticDiffEqAlgorithm=SRA1(), + e_ops::Vector{QuantumObject{Te, OperatorQuantumObject}}=QuantumObject{Matrix, OperatorQuantumObject}[], + H_t::Union{Nothing,Function,TimeDependentOperatorSum}=nothing, + params::NamedTuple=NamedTuple(), + n_traj::Int=1, + ensemble_method=EnsembleThreads(), + prob_func::Function=_mcsolve_prob_func, + output_func::Function=_mcsolve_output_func, + kwargs...) + + +Stochastic Schrödinger equation evolution of a quantum system given the system Hamiltonian ``\hat{H}`` and a list of stochadtic collapse (jump) operators ``\{\hat{C}_n\}_n``. +The stochastic evolution of the state ``|\psi(t)\rangle`` is defined by: + + ```math + d|\psi(t)\rangle = -i K |\psi(t)\rangle dt + \sum_n M_n |\psi(t)\rangle dW_n(t) + ``` + +where + +```math + K = \hat{H} + i \sum_n \left(\frac{e_j} C_n - \frac{1}{2} \sum_{j} C_n^\dagger C_n - \frac{e_j^2}{8}\right), + ``` + ```math + M_n = C_n - \frac{e_n}{2}, + ``` + and + ```math + e_n = \langle C_n + C_n^\dagger \rangle. + ``` + +Above, `C_n` is the `n`-th collapse operator and `dW_j(t)` is the real Wiener increment associated to `C_n`. + + +# Arguments + +- `H::QuantumObject`: Hamiltonian of the system ``\hat{H}``. +- `ψ0::QuantumObject`: Initial state of the system ``|\psi(0)\rangle``. +- `tlist::AbstractVector`: List of times at which to save the state of the system. +- `sc_ops::Vector`: List of stochastic collapse operators ``\{\hat{C}_n\}_n``. +- `alg::StochasticDiffEq.StochasticDiffEqAlgorithm`: Algorithm to use for the time evolution. +- `e_ops::Vector`: List of operators for which to calculate expectation values. +- `H_t::Union{Nothing,Function,TimeDependentOperatorSum}`: Time-dependent part of the Hamiltonian. +- `params::NamedTuple`: Dictionary of parameters to pass to the solver. +- `seeds::Union{Nothing, Vector{Int}}`: List of seeds for the random number generator. Length must be equal to the number of trajectories provided. +- `n_traj::Int`: Number of trajectories to use. +- `ensemble_method`: Ensemble method to use. +- `prob_func::Function`: Function to use for generating the SDEProblem. +- `output_func::Function`: Function to use for generating the output of a single trajectory. +- `kwargs...`: Additional keyword arguments to pass to the solver. + +# Notes + +- `ensemble_method` can be one of `EnsembleThreads()`, `EnsembleSerial()`, `EnsembleDistributed()` +- The states will be saved depend on the keyword argument `saveat` in `kwargs`. +- If `e_ops` is specified, the default value of `saveat=[tlist[end]]` (only save the final state), otherwise, `saveat=tlist` (saving the states corresponding to `tlist`). You can also specify `e_ops` and `saveat` separately. +- The default tolerances in `kwargs` are given as `reltol=1e-6` and `abstol=1e-8`. +- For more details about `alg` and extra `kwargs`, please refer to [`DifferentialEquations.jl`](https://diffeq.sciml.ai/stable/) + +# Returns + +- `sol::TimeEvolutionSSESol`: The solution of the time evolution. See also [`TimeEvolutionSSESol`](@ref) +""" +function ssesolve( + H::QuantumObject{MT1,OperatorQuantumObject}, + ψ0::QuantumObject{<:AbstractArray{T2},KetQuantumObject}, + tlist::AbstractVector, + sc_ops::Vector{QuantumObject{Tc,OperatorQuantumObject}} = QuantumObject{MT1,OperatorQuantumObject}[]; + alg::StochasticDiffEq.StochasticDiffEqAlgorithm = SRA1(), + e_ops::Union{Nothing,AbstractVector} = nothing, + H_t::Union{Nothing,Function,TimeDependentOperatorSum} = nothing, + params::NamedTuple = NamedTuple(), + n_traj::Int = 1, + ensemble_method = EnsembleThreads(), + prob_func::Function = _ssesolve_prob_func, + output_func::Function = _ssesolve_output_func, + kwargs..., +) where {MT1<:AbstractMatrix,T2,Tc<:AbstractMatrix} + ens_prob = ssesolveEnsembleProblem( + H, + ψ0, + tlist, + sc_ops; + alg = alg, + e_ops = e_ops, + H_t = H_t, + params = params, + prob_func = prob_func, + output_func = output_func, + kwargs..., + ) + + return ssesolve(ens_prob; alg = alg, n_traj = n_traj, ensemble_method = ensemble_method) +end + +function ssesolve( + ens_prob::EnsembleProblem; + alg::StochasticDiffEq.StochasticDiffEqAlgorithm = SRA1(), + n_traj::Int = 1, + ensemble_method = EnsembleThreads(), +) + sol = solve(ens_prob, alg, ensemble_method, trajectories = n_traj) + _sol_1 = sol[:, 1] + + expvals_all = Array{ComplexF64}(undef, length(sol), size(_sol_1.prob.p.expvals)...) + states = + isempty(_sol_1.prob.kwargs[:saveat]) ? fill(QuantumObject[], length(sol)) : + Vector{Vector{QuantumObject}}(undef, length(sol)) + + foreach(i -> _ssesolve_generate_statistics!(sol, i, states, expvals_all), eachindex(sol)) + expvals = dropdims(sum(expvals_all, dims = 1), dims = 1) ./ length(sol) + + return TimeEvolutionSSESol( + n_traj, + _sol_1.t, + states, + expvals, + expvals_all, + sol.converged, + _sol_1.alg, + _sol_1.prob.kwargs[:abstol], + _sol_1.prob.kwargs[:reltol], + ) +end diff --git a/src/time_evolution/time_evolution.jl b/src/time_evolution/time_evolution.jl index 7c2fd89a..9828622e 100644 --- a/src/time_evolution/time_evolution.jl +++ b/src/time_evolution/time_evolution.jl @@ -1,9 +1,10 @@ export TimeDependentOperatorSum -export TimeEvolutionSol, TimeEvolutionMCSol +export TimeEvolutionSol, TimeEvolutionMCSol, TimeEvolutionSSESol export liouvillian, liouvillian_floquet, liouvillian_generalized const DEFAULT_ODE_SOLVER_OPTIONS = (abstol = 1e-8, reltol = 1e-6, save_everystep = false, save_end = true) +const DEFAULT_SDE_SOLVER_OPTIONS = (abstol = 1e-2, reltol = 1e-2, save_everystep = false, save_end = true) @doc raw""" struct TimeEvolutionSol @@ -25,7 +26,7 @@ struct TimeEvolutionSol{TT<:Vector{<:Real},TS<:AbstractVector,TE<:Matrix{Complex states::TS expect::TE retcode::Enum - alg::OrdinaryDiffEq.OrdinaryDiffEqAlgorithm + alg::OrdinaryDiffEqAlgorithm abstol::Real reltol::Real end @@ -77,7 +78,7 @@ struct TimeEvolutionMCSol{ jump_times::TJT jump_which::TJW converged::Bool - alg::OrdinaryDiffEq.OrdinaryDiffEqAlgorithm + alg::OrdinaryDiffEqAlgorithm abstol::Real reltol::Real end @@ -95,6 +96,55 @@ function Base.show(io::IO, sol::TimeEvolutionMCSol) return nothing end +@doc raw""" + struct TimeEvolutionSSESol + +A structure storing the results and some information from solving trajectories of the Stochastic Shrodinger equation time evolution. + +# Fields (Attributes) + +- `n_traj::Int`: Number of trajectories +- `times::AbstractVector`: The time list of the evolution in each trajectory. +- `states::Vector{Vector{QuantumObject}}`: The list of result states in each trajectory. +- `expect::Matrix`: The expectation values (averaging all trajectories) corresponding to each time point in `times`. +- `expect_all::Array`: The expectation values corresponding to each trajectory and each time point in `times` +- `converged::Bool`: Whether the solution is converged or not. +- `alg`: The algorithm which is used during the solving process. +- `abstol::Real`: The absolute tolerance which is used during the solving process. +- `reltol::Real`: The relative tolerance which is used during the solving process. +""" +struct TimeEvolutionSSESol{ + TT<:Vector{<:Real}, + TS<:AbstractVector, + TE<:Matrix{ComplexF64}, + TEA<:Array{ComplexF64,3}, + T1<:Real, + T2<:Real, +} + n_traj::Int + times::TT + states::TS + expect::TE + expect_all::TEA + converged::Bool + alg::StochasticDiffEq.StochasticDiffEqAlgorithm + abstol::T1 + reltol::T2 +end + +function Base.show(io::IO, sol::TimeEvolutionSSESol) + print(io, "Solution of quantum trajectories\n") + print(io, "(converged: $(sol.converged))\n") + print(io, "--------------------------------\n") + print(io, "num_trajectories = $(sol.n_traj)\n") + print(io, "num_states = $(length(sol.states[1]))\n") + print(io, "num_expect = $(size(sol.expect, 1))\n") + print(io, "SDE alg.: $(sol.alg)\n") + print(io, "abstol = $(sol.abstol)\n") + print(io, "reltol = $(sol.reltol)\n") + return nothing +end + abstract type LindbladJumpCallbackType end struct ContinuousLindbladJumpCallback <: LindbladJumpCallbackType @@ -140,11 +190,15 @@ end return mul!(y, A.operator_sum, x, α, β) end +function liouvillian(A::TimeDependentOperatorSum, Id_cache = I(prod(A.operator_sum.operators[1].dims))) + return TimeDependentOperatorSum(A.coefficient_functions, liouvillian(A.operator_sum, Id_cache)) +end + ####################################### ### LIOUVILLIAN ### @doc raw""" - liouvillian(H::QuantumObject, c_ops::AbstractVector, Id_cache=I(prod(H.dims))) + liouvillian(H::QuantumObject, c_ops::Union{AbstractVector,Nothing}=nothing, Id_cache=I(prod(H.dims))) Construct the Liouvillian [`SuperOperator`](@ref) for a system Hamiltonian ``\hat{H}`` and a set of collapse operators ``\{\hat{C}_n\}_n``: @@ -164,27 +218,22 @@ See also [`spre`](@ref), [`spost`](@ref), and [`lindblad_dissipator`](@ref). """ function liouvillian( H::QuantumObject{MT1,OpType1}, - c_ops::Vector{QuantumObject{MT2,OpType2}} = Vector{QuantumObject{MT1,OpType1}}([]), + c_ops::Union{AbstractVector,Nothing} = nothing, Id_cache = I(prod(H.dims)), -) where { - MT1<:AbstractMatrix, - MT2<:AbstractMatrix, - OpType1<:Union{OperatorQuantumObject,SuperOperatorQuantumObject}, - OpType2<:Union{OperatorQuantumObject,SuperOperatorQuantumObject}, -} +) where {MT1<:AbstractMatrix,OpType1<:Union{OperatorQuantumObject,SuperOperatorQuantumObject}} L = liouvillian(H, Id_cache) - for c_op in c_ops - L += lindblad_dissipator(c_op, Id_cache) + if !(c_ops isa Nothing) + for c_op in c_ops + L += lindblad_dissipator(c_op, Id_cache) + end end return L end -liouvillian( - H::QuantumObject{MT1,OperatorQuantumObject}, - Id_cache::Diagonal = I(prod(H.dims)), -) where {MT1<:AbstractMatrix} = -1im * (spre(H, Id_cache) - spost(H, Id_cache)) +liouvillian(H::QuantumObject{<:AbstractMatrix,OperatorQuantumObject}, Id_cache::Diagonal = I(prod(H.dims))) = + -1im * (spre(H, Id_cache) - spost(H, Id_cache)) -liouvillian(H::QuantumObject{MT1,SuperOperatorQuantumObject}, Id_cache::Diagonal) where {MT1<:AbstractMatrix} = H +liouvillian(H::QuantumObject{<:AbstractMatrix,SuperOperatorQuantumObject}, Id_cache::Diagonal) = H function liouvillian_floquet( L₀::QuantumObject{<:AbstractArray{T1},SuperOperatorQuantumObject}, @@ -202,10 +251,10 @@ end function liouvillian_floquet( H::QuantumObject{<:AbstractArray{T1},OpType1}, - c_ops::AbstractVector, Hₚ::QuantumObject{<:AbstractArray{T2},OpType2}, Hₘ::QuantumObject{<:AbstractArray{T3},OpType3}, - ω::Real; + ω::Real, + c_ops::Union{AbstractVector,Nothing} = nothing; n_max::Int = 3, tol::Real = 1e-15, ) where { @@ -231,30 +280,31 @@ function liouvillian_generalized( H::QuantumObject{MT,OperatorQuantumObject}, fields::Vector, T_list::Vector{<:Real}; - N_trunc::Int = size(H, 1), + N_trunc::Union{Int,Nothing} = nothing, tol::Real = 1e-12, σ_filter::Union{Nothing,Real} = nothing, ) where {MT<:AbstractMatrix} (length(fields) == length(T_list)) || throw(DimensionMismatch("The number of fields, ωs and Ts must be the same.")) - dims = N_trunc == size(H, 1) ? H.dims : [N_trunc] + dims = (N_trunc isa Nothing) ? H.dims : SVector(N_trunc) + final_size = prod(dims) result = eigen(H) - E = real.(result.values[1:N_trunc]) + E = real.(result.values[1:final_size]) U = QuantumObject(result.vectors, result.type, result.dims) - H_d = QuantumObject(Diagonal(complex(E)), dims = dims) + H_d = QuantumObject(Diagonal(complex(E)), type = Operator, dims = dims) Ω = E' .- E Ωp = triu(dense_to_sparse(Ω, tol), 1) # Filter in the Hilbert space σ = isnothing(σ_filter) ? 500 * maximum([norm(field) / length(field) for field in fields]) : σ_filter - F1 = QuantumObject(gaussian.(Ω, 0, σ), dims = dims) + F1 = QuantumObject(gaussian.(Ω, 0, σ), type = Operator, dims = dims) F1 = dense_to_sparse(F1, tol) # Filter in the Liouville space - # M1 = ones(N_trunc, N_trunc) - M1 = similar(E, N_trunc, N_trunc) + # M1 = ones(final_size, final_size) + M1 = similar(E, final_size, final_size) M1 .= 1 Ω1 = kron(Ω, M1) Ω2 = kron(M1, Ω) @@ -266,16 +316,16 @@ function liouvillian_generalized( for i in eachindex(fields) # The operator that couples the system to the bath in the eigenbasis - X_op = dense_to_sparse((U'*fields[i]*U).data[1:N_trunc, 1:N_trunc], tol) + X_op = dense_to_sparse((U'*fields[i]*U).data[1:final_size, 1:final_size], tol) if ishermitian(fields[i]) X_op = (X_op + X_op') / 2 # Make sure it's hermitian end # Ohmic reservoir N_th = n_th.(Ωp, T_list[i]) - Sp₀ = QuantumObject(triu(X_op, 1), dims = dims) - Sp₁ = QuantumObject(droptol!((@. Ωp * N_th * Sp₀.data), tol), dims = dims) - Sp₂ = QuantumObject(droptol!((@. Ωp * (1 + N_th) * Sp₀.data), tol), dims = dims) + Sp₀ = QuantumObject(triu(X_op, 1), type = Operator, dims = dims) + Sp₁ = QuantumObject(droptol!((@. Ωp * N_th * Sp₀.data), tol), type = Operator, dims = dims) + Sp₂ = QuantumObject(droptol!((@. Ωp * (1 + N_th) * Sp₀.data), tol), type = Operator, dims = dims) # S0 = QuantumObject( spdiagm(diag(X_op)), dims=dims ) L += diff --git a/src/time_evolution/time_evolution_dynamical.jl b/src/time_evolution/time_evolution_dynamical.jl index c5738170..9ee68fac 100644 --- a/src/time_evolution/time_evolution_dynamical.jl +++ b/src/time_evolution/time_evolution_dynamical.jl @@ -4,24 +4,23 @@ export dfd_mesolve, dsf_mesolve, dsf_mcsolve function _reduce_dims( QO::AbstractArray{T}, - dims::Vector{<:Integer}, - sel::AbstractVector, - reduce::AbstractVector, -) where {T} - rd = dims - nd = length(rd) - rd_new = zero(rd) - rd_new[sel] .= reduce - @. rd_new = rd - rd_new + dims::Union{SVector{N,DT},MVector{N,DT}}, + sel, + reduce, +) where {T,N,DT<:Integer} + nd = length(dims) + dims_new = zero(dims) + dims_new[sel] .= reduce + @. dims_new = dims - dims_new if nd == 1 - ρmat = similar(QO, rd_new[1], rd_new[1]) - copyto!(ρmat, view(QO, 1:rd_new[1], 1:rd_new[1])) + ρmat = similar(QO, dims_new[1], dims_new[1]) + copyto!(ρmat, view(QO, 1:dims_new[1], 1:dims_new[1])) else - ρmat = reshape(QO, reverse!(repeat(rd, 2))...) - ρmat2 = similar(QO, reverse!(repeat(rd_new, 2))...) - copyto!(ρmat2, view(ρmat, reverse!(repeat([1:n for n in rd_new], 2))...)) - ρmat = reshape(ρmat2, prod(rd_new), prod(rd_new)) + ρmat = reshape(QO, reverse(vcat(dims, dims))...) + ρmat2 = similar(QO, reverse(vcat(dims_new, dims_new))...) + copyto!(ρmat2, view(ρmat, reverse!(repeat([1:n for n in dims_new], 2))...)) + ρmat = reshape(ρmat2, prod(dims_new), prod(dims_new)) end return ρmat @@ -29,30 +28,29 @@ end function _increase_dims( QO::AbstractArray{T}, - dims::Vector{<:Integer}, - sel::AbstractVector, - increase::AbstractVector, -) where {T} - rd = dims - nd = length(rd) - rd_new = zero(rd) - rd_new[sel] .= increase - @. rd_new = rd + rd_new + dims::Union{SVector{N,DT},MVector{N,DT}}, + sel, + increase, +) where {T,N,DT<:Integer} + nd = length(dims) + dims_new = MVector(zero(dims)) # Mutable SVector + dims_new[sel] .= increase + @. dims_new = dims + dims_new if nd == 1 - ρmat = similar(QO, rd_new[1], rd_new[1]) - fill!(selectdim(ρmat, 1, rd[1]+1:rd_new[1]), 0) - fill!(selectdim(ρmat, 2, rd[1]+1:rd_new[1]), 0) - copyto!(view(ρmat, 1:rd[1], 1:rd[1]), QO) + ρmat = similar(QO, dims_new[1], dims_new[1]) + fill!(selectdim(ρmat, 1, dims[1]+1:dims_new[1]), 0) + fill!(selectdim(ρmat, 2, dims[1]+1:dims_new[1]), 0) + copyto!(view(ρmat, 1:dims[1], 1:dims[1]), QO) else - ρmat2 = similar(QO, reverse!(repeat(rd_new, 2))...) - ρmat = reshape(QO, reverse!(repeat(rd, 2))...) + ρmat2 = similar(QO, reverse(vcat(dims_new, dims_new))...) + ρmat = reshape(QO, reverse(vcat(dims, dims))...) for i in eachindex(sel) - fill!(selectdim(ρmat2, nd - sel[i] + 1, rd[sel[i]]+1:rd_new[sel[i]]), 0) - fill!(selectdim(ρmat2, 2 * nd - sel[i] + 1, rd[sel[i]]+1:rd_new[sel[i]]), 0) + fill!(selectdim(ρmat2, nd - sel[i] + 1, dims[sel[i]]+1:dims_new[sel[i]]), 0) + fill!(selectdim(ρmat2, 2 * nd - sel[i] + 1, dims[sel[i]]+1:dims_new[sel[i]]), 0) end - copyto!(view(ρmat2, reverse!(repeat([1:n for n in rd], 2))...), ρmat) - ρmat = reshape(ρmat2, prod(rd_new), prod(rd_new)) + copyto!(view(ρmat2, reverse!(repeat([1:n for n in dims], 2))...), ρmat) + ρmat = reshape(ρmat2, prod(dims_new), prod(dims_new)) end return ρmat @@ -78,7 +76,7 @@ function _DFDIncreaseReduceCondition(u, t, integrator) dim_i = dim_list[i] pillow_i = pillow_list[i] if dim_i < maxdim_i && dim_i > 2 && maxdim_i != 0 - ρi = _ptrace_oper(vec2mat(dfd_ρt_cache), dim_list, [i])[1] + ρi = _ptrace_oper(vec2mat(dfd_ρt_cache), dim_list, SVector(i))[1] @views res = norm(ρi[diagind(ρi)[end-pillow_i:end]], 1) * sqrt(dim_i) / pillow_i if res > tol_list[i] increase_list[i] = true @@ -106,9 +104,9 @@ function _DFDIncreaseReduceAffect!(integrator) ρt = vec2mat(dfd_ρt_cache) - @views pillow_increase = pillow_list[increase_list] - @views pillow_reduce = pillow_list[reduce_list] - dim_increase = findall(increase_list) + pillow_increase = pillow_list[increase_list] # TODO: This returns a Vector. Find a way to return an SVector or NTuple + pillow_reduce = pillow_list[reduce_list] + dim_increase = findall(increase_list) # TODO: This returns a Vector. Find a way to return an SVector or NTuple dim_reduce = findall(reduce_list) if length(dim_increase) > 0 @@ -131,7 +129,9 @@ function _DFDIncreaseReduceAffect!(integrator) resize!(integrator, size(L, 1)) copyto!(integrator.u, mat2vec(ρt)) - return integrator.p = merge(internal_params, (L = L, e_ops = e_ops2, dfd_ρt_cache = similar(integrator.u))) + integrator.p = merge(internal_params, (L = L, e_ops = e_ops2, dfd_ρt_cache = similar(integrator.u))) + + return nothing end function dfd_mesolveProblem( @@ -141,7 +141,7 @@ function dfd_mesolveProblem( c_ops::Function, maxdims::Vector{T2}, dfd_params::NamedTuple = NamedTuple(); - alg::OrdinaryDiffEq.OrdinaryDiffEqAlgorithm = Tsit5(), + alg::OrdinaryDiffEqAlgorithm = Tsit5(), e_ops::Function = (dim_list) -> Vector{Vector{T1}}([]), H_t::Union{Nothing,Function,TimeDependentOperatorSum} = nothing, params::NamedTuple = NamedTuple(), @@ -151,15 +151,15 @@ function dfd_mesolveProblem( length(ψ0.dims) != length(maxdims) && throw(DimensionMismatch("'dim_list' and 'maxdims' do not have the same dimension.")) - dim_list = deepcopy(ψ0.dims) + dim_list = MVector(ψ0.dims) H₀ = H(dim_list, dfd_params) c_ops₀ = c_ops(dim_list, dfd_params) e_ops₀ = e_ops(dim_list, dfd_params) dim_list_evo_times = [0.0] dim_list_evo = [dim_list] - reduce_list = zeros(Bool, length(dim_list)) - increase_list = zeros(Bool, length(dim_list)) + reduce_list = MVector(ntuple(i -> false, length(dim_list))) + increase_list = MVector(ntuple(i -> false, length(dim_list))) pillow_list = _dfd_set_pillow.(dim_list) params2 = merge( @@ -194,7 +194,7 @@ end dfd_mesolve(H::Function, ψ0::QuantumObject, t_l::AbstractVector, c_ops::Function, maxdims::AbstractVector, dfd_params::NamedTuple=NamedTuple(); - alg::OrdinaryDiffEq.OrdinaryDiffEqAlgorithm=Tsit5(), + alg::OrdinaryDiffEqAlgorithm=Tsit5(), e_ops::Function=(dim_list) -> Vector{Vector{T1}}([]), H_t::Union{Nothing,Function,TimeDependentOperatorSum}=nothing, params::NamedTuple=NamedTuple(), @@ -202,6 +202,10 @@ end kwargs...) Time evolution of an open quantum system using master equation, dynamically changing the dimension of the Hilbert subspaces. + +# Notes +- For more details about `alg` please refer to [`DifferentialEquations.jl` (ODE Solvers)](https://docs.sciml.ai/DiffEqDocs/stable/solvers/ode_solve/) +- For more details about `kwargs` please refer to [`DifferentialEquations.jl` (Keyword Arguments)](https://docs.sciml.ai/DiffEqDocs/stable/basics/common_solver_opts/) """ function dfd_mesolve( H::Function, @@ -210,7 +214,7 @@ function dfd_mesolve( c_ops::Function, maxdims::Vector{T2}, dfd_params::NamedTuple = NamedTuple(); - alg::OrdinaryDiffEq.OrdinaryDiffEqAlgorithm = Tsit5(), + alg::OrdinaryDiffEqAlgorithm = Tsit5(), e_ops::Function = (dim_list) -> Vector{Vector{T1}}([]), H_t::Union{Nothing,Function,TimeDependentOperatorSum} = nothing, params::NamedTuple = NamedTuple(), @@ -237,6 +241,7 @@ function dfd_mesolve( ρt = map( i -> QuantumObject( vec2mat(sol.u[i]), + type = Operator, dims = sol.prob.p.dim_list_evo[searchsortedlast(sol.prob.p.dim_list_evo_times, sol.t[i])], ), eachindex(sol.t), @@ -341,7 +346,7 @@ function dsf_mesolveProblem( op_list::Vector{TOl}, α0_l::Vector{<:Number} = zeros(length(op_list)), dsf_params::NamedTuple = NamedTuple(); - alg::OrdinaryDiffEq.OrdinaryDiffEqAlgorithm = Tsit5(), + alg::OrdinaryDiffEqAlgorithm = Tsit5(), e_ops::Function = (op_list, p) -> Vector{TOl}([]), H_t::Union{Nothing,Function,TimeDependentOperatorSum} = nothing, params::NamedTuple = NamedTuple(), @@ -408,7 +413,7 @@ end op_list::Vector{TOl}, α0_l::Vector{<:Number}=zeros(length(op_list)), dsf_params::NamedTuple=NamedTuple(); - alg::OrdinaryDiffEq.OrdinaryDiffEqAlgorithm=Tsit5(), + alg::OrdinaryDiffEqAlgorithm=Tsit5(), e_ops::Function=(op_list,p) -> Vector{TOl}([]), H_t::Union{Nothing,Function,TimeDependentOperatorSum}=nothing, params::NamedTuple=NamedTuple(), @@ -417,6 +422,10 @@ end kwargs...) Time evolution of an open quantum system using master equation and the Dynamical Shifted Fock algorithm. + +# Notes +- For more details about `alg` please refer to [`DifferentialEquations.jl` (ODE Solvers)](https://docs.sciml.ai/DiffEqDocs/stable/solvers/ode_solve/) +- For more details about `kwargs` please refer to [`DifferentialEquations.jl` (Keyword Arguments)](https://docs.sciml.ai/DiffEqDocs/stable/basics/common_solver_opts/) """ function dsf_mesolve( H::Function, @@ -426,7 +435,7 @@ function dsf_mesolve( op_list::Vector{TOl}, α0_l::Vector{<:Number} = zeros(length(op_list)), dsf_params::NamedTuple = NamedTuple(); - alg::OrdinaryDiffEq.OrdinaryDiffEqAlgorithm = Tsit5(), + alg::OrdinaryDiffEqAlgorithm = Tsit5(), e_ops::Function = (op_list, p) -> Vector{TOl}([]), H_t::Union{Nothing,Function,TimeDependentOperatorSum} = nothing, params::NamedTuple = NamedTuple(), @@ -461,7 +470,7 @@ function dsf_mesolve( op_list::Vector{TOl}, α0_l::Vector{<:Number} = zeros(length(op_list)), dsf_params::NamedTuple = NamedTuple(); - alg::OrdinaryDiffEq.OrdinaryDiffEqAlgorithm = Tsit5(), + alg::OrdinaryDiffEqAlgorithm = Tsit5(), e_ops::Function = (op_list, p) -> Vector{TOl}([]), H_t::Union{Nothing,Function,TimeDependentOperatorSum} = nothing, params::NamedTuple = NamedTuple(), @@ -607,7 +616,7 @@ function dsf_mcsolveEnsembleProblem( op_list::Vector{TOl}, α0_l::Vector{<:Number} = zeros(length(op_list)), dsf_params::NamedTuple = NamedTuple(); - alg::OrdinaryDiffEq.OrdinaryDiffEqAlgorithm = Tsit5(), + alg::OrdinaryDiffEqAlgorithm = Tsit5(), e_ops::Function = (op_list, p) -> Vector{TOl}([]), H_t::Union{Nothing,Function,TimeDependentOperatorSum} = nothing, params::NamedTuple = NamedTuple(), @@ -673,7 +682,7 @@ end op_list::Vector{TOl}, α0_l::Vector{<:Number}=zeros(length(op_list)), dsf_params::NamedTuple=NamedTuple(); - alg::OrdinaryDiffEq.OrdinaryDiffEqAlgorithm=Tsit5(), + alg::OrdinaryDiffEqAlgorithm=Tsit5(), e_ops::Function=(op_list,p) -> Vector{TOl}([]), H_t::Union{Nothing,Function,TimeDependentOperatorSum}=nothing, params::NamedTuple=NamedTuple(), @@ -685,6 +694,10 @@ end kwargs...) Time evolution of a quantum system using the Monte Carlo wave function method and the Dynamical Shifted Fock algorithm. + +# Notes +- For more details about `alg` please refer to [`DifferentialEquations.jl` (ODE Solvers)](https://docs.sciml.ai/DiffEqDocs/stable/solvers/ode_solve/) +- For more details about `kwargs` please refer to [`DifferentialEquations.jl` (Keyword Arguments)](https://docs.sciml.ai/DiffEqDocs/stable/basics/common_solver_opts/) """ function dsf_mcsolve( H::Function, @@ -694,7 +707,7 @@ function dsf_mcsolve( op_list::Vector{TOl}, α0_l::Vector{<:Number} = zeros(length(op_list)), dsf_params::NamedTuple = NamedTuple(); - alg::OrdinaryDiffEq.OrdinaryDiffEqAlgorithm = Tsit5(), + alg::OrdinaryDiffEqAlgorithm = Tsit5(), e_ops::Function = (op_list, p) -> Vector{TOl}([]), H_t::Union{Nothing,Function,TimeDependentOperatorSum} = nothing, params::NamedTuple = NamedTuple(), diff --git a/src/utilities.jl b/src/utilities.jl index 63d5a6d4..a2888b70 100644 --- a/src/utilities.jl +++ b/src/utilities.jl @@ -54,3 +54,30 @@ makeVal(x::Val{T}) where {T} = x makeVal(x) = Val(x) getVal(x::Val{T}) where {T} = T + +_get_size(A::AbstractMatrix) = size(A) +_get_size(A::AbstractVector) = (length(A), 1) + +_non_static_array_warning(argname, arg::Tuple{}) = + throw(ArgumentError("The argument $argname must be a Tuple or a StaticVector of non-zero length.")) +_non_static_array_warning(argname, arg::Union{SVector{N,T},MVector{N,T},NTuple{N,T}}) where {N,T} = nothing +_non_static_array_warning(argname, arg::AbstractVector{T}) where {T} = + @warn "The argument $argname should be a Tuple or a StaticVector for better performance. Try to use `$argname = $(Tuple(arg))` or `$argname = SVector(" * + join(arg, ", ") * + ")` instead of `$argname = $arg`." maxlog = 1 + +# functions for getting Float or Complex element type +_FType(::AbstractArray{T}) where {T<:Number} = _FType(T) +_FType(::Type{Int32}) = Float32 +_FType(::Type{Int64}) = Float64 +_FType(::Type{Float32}) = Float32 +_FType(::Type{Float64}) = Float64 +_FType(::Type{ComplexF32}) = Float32 +_FType(::Type{ComplexF64}) = Float64 +_CType(::AbstractArray{T}) where {T<:Number} = _CType(T) +_CType(::Type{Int32}) = ComplexF32 +_CType(::Type{Int64}) = ComplexF64 +_CType(::Type{Float32}) = ComplexF32 +_CType(::Type{Float64}) = ComplexF64 +_CType(::Type{ComplexF32}) = ComplexF32 +_CType(::Type{ComplexF64}) = ComplexF64 diff --git a/src/versioninfo.jl b/src/versioninfo.jl index b5087840..4b67e311 100644 --- a/src/versioninfo.jl +++ b/src/versioninfo.jl @@ -28,13 +28,15 @@ function versioninfo(io::IO = stdout) io, "Package information:\n", "====================================\n", - "QuantumToolbox Ver. $(_get_pkg_version("QuantumToolbox"))\n", - "LinearSolve Ver. $(_get_pkg_version("LinearSolve"))\n", - "OrdinaryDiffEq Ver. $(_get_pkg_version("OrdinaryDiffEq"))\n", + "Julia Ver. $(VERSION)\n", + "QuantumToolbox Ver. $(_get_pkg_version("QuantumToolbox"))\n", + "LinearSolve Ver. $(_get_pkg_version("LinearSolve"))\n", + "OrdinaryDiffEqCore Ver. $(_get_pkg_version("OrdinaryDiffEqCore"))\n", ) # print System informations - println(io, "System information:\n", "====================================\n", "Julia Version: $(VERSION)") + println(io, "System information:") + println(io, "====================================") println(io, """OS : $(OS_name) ($(Sys.MACHINE))""") println(io, """CPU : $(length(cpu)) × $(cpu[1].model)""") println(io, """Memory : $(round(Sys.total_memory() / 2 ^ 30, digits=3)) GB""") diff --git a/test/code_quality.jl b/test/core-test/code_quality.jl similarity index 75% rename from test/code_quality.jl rename to test/core-test/code_quality.jl index 591561a4..724639a3 100644 --- a/test/code_quality.jl +++ b/test/core-test/code_quality.jl @@ -2,7 +2,7 @@ using Aqua, JET @testset "Code quality" verbose = true begin @testset "Aqua.jl" begin - Aqua.test_all(QuantumToolbox; ambiguities = false) + Aqua.test_all(QuantumToolbox; ambiguities = false, unbound_args = false) end @testset "JET.jl" begin diff --git a/test/correlations_and_spectrum.jl b/test/core-test/correlations_and_spectrum.jl similarity index 81% rename from test/correlations_and_spectrum.jl rename to test/core-test/correlations_and_spectrum.jl index 6e56c2c4..5c507909 100644 --- a/test/correlations_and_spectrum.jl +++ b/test/core-test/correlations_and_spectrum.jl @@ -17,9 +17,7 @@ @test sum(abs2.(spec2[idxs2] .- test_func2[idxs2])) / sum(abs2.(test_func2[idxs2])) < 0.01 @testset "Type Inference spectrum" begin - if VERSION >= v"1.10" - @inferred spectrum(H, ω_l, a', a, c_ops, solver = FFTCorrelation(), progress_bar = Val(false)) - @inferred spectrum(H, ω_l, a', a, c_ops) - end + @inferred spectrum(H, ω_l, a', a, c_ops, solver = FFTCorrelation(), progress_bar = Val(false)) + @inferred spectrum(H, ω_l, a', a, c_ops) end end diff --git a/test/dynamical-shifted-fock.jl b/test/core-test/dynamical-shifted-fock.jl similarity index 100% rename from test/dynamical-shifted-fock.jl rename to test/core-test/dynamical-shifted-fock.jl diff --git a/test/dynamical_fock_dimension_mesolve.jl b/test/core-test/dynamical_fock_dimension_mesolve.jl similarity index 100% rename from test/dynamical_fock_dimension_mesolve.jl rename to test/core-test/dynamical_fock_dimension_mesolve.jl diff --git a/test/eigenvalues_and_operators.jl b/test/core-test/eigenvalues_and_operators.jl similarity index 83% rename from test/eigenvalues_and_operators.jl rename to test/core-test/eigenvalues_and_operators.jl index 6f281208..9e8ad76a 100644 --- a/test/eigenvalues_and_operators.jl +++ b/test/core-test/eigenvalues_and_operators.jl @@ -59,7 +59,7 @@ # eigen solve for general matrices vals, _, vecs = eigsolve(L.data, sigma = 0.01, k = 10, krylovdim = 50) vals2, vecs2 = eigen(sparse_to_dense(L.data)) - vals3, state3, vecs3 = eigsolve_al(liouvillian(H, c_ops), 1 \ (40 * κ), k = 10, krylovdim = 50) + vals3, state3, vecs3 = eigsolve_al(L, 1 \ (40 * κ), k = 10, krylovdim = 50) idxs = sortperm(vals2, by = abs) vals2 = vals2[idxs][1:10] vecs2 = vecs2[:, idxs][:, 1:10] @@ -91,4 +91,27 @@ @test isapprox(abs2(vals2[1]), abs2(vals3[1]), atol = 1e-7) @test isapprox(vec2mat(vecs[1]).data * exp(-1im * angle(vecs[1][1])), vec2mat(vecs2[1]).data, atol = 1e-7) @test isapprox(vec2mat(vecs[1]).data * exp(-1im * angle(vecs[1][1])), vec2mat(state3[1]).data, atol = 1e-5) + + @testset "Type Inference (eigen)" begin + N = 5 + a = kron(destroy(N), qeye(N)) + a_d = a' + b = kron(qeye(N), destroy(N)) + b_d = b' + + ωc = 1 + ωb = 1 + g = 0.01 + κ = 0.1 + n_thermal = 0.01 + + H = ωc * a_d * a + ωb * b_d * b + g * (a + a_d) * (b + b_d) + c_ops = [√((1 + n_thermal) * κ) * a, √κ * b, √(n_thermal * κ) * a_d] + L = liouvillian(H, c_ops) + + @inferred eigenstates(H, sparse = false) + @inferred eigenstates(H, sparse = true) + @inferred eigenstates(L, sparse = true) + @inferred eigsolve_al(L, 1 \ (40 * κ), k = 10) + end end diff --git a/test/entanglement.jl b/test/core-test/entanglement.jl similarity index 62% rename from test/entanglement.jl rename to test/core-test/entanglement.jl index 860e168c..c9df3e20 100644 --- a/test/entanglement.jl +++ b/test/core-test/entanglement.jl @@ -5,4 +5,9 @@ rho = state * state' @test entanglement(state, 1) / log(2) ≈ 1 @test entanglement(rho, 1) / log(2) ≈ 1 + + @testset "Type Stability (entanglement)" begin + @inferred entanglement(state, 1) + @inferred entanglement(rho, 1) + end end diff --git a/test/generalized_master_equation.jl b/test/core-test/generalized_master_equation.jl similarity index 71% rename from test/generalized_master_equation.jl rename to test/core-test/generalized_master_equation.jl index 965206f9..080ab17e 100644 --- a/test/generalized_master_equation.jl +++ b/test/core-test/generalized_master_equation.jl @@ -26,7 +26,7 @@ c_ops = [sqrt(0.01) * a2, sqrt(0.01) * sm2] L2 = liouvillian(H_d, c_ops) - @test (expect(Xp' * Xp, steadystate(L1)) < 1e-10 && expect(Xp' * Xp, steadystate(L2)) > 1e-3) + @test (expect(Hermitian(Xp' * Xp), steadystate(L1)) < 1e-10 && expect(Hermitian(Xp' * Xp), steadystate(L2)) > 1e-3) H = 1 * a' * a + 1 * sz / 2 + 1e-5 * (a * sp + a' * sm) @@ -41,4 +41,23 @@ sm2 = Qobj(dense_to_sparse((U'*sm*U).data[1:N_trunc, 1:N_trunc], tol)) @test abs(expect(Xp' * Xp, steadystate(L1)) - n_th(1, Tlist[1])) / n_th(1, Tlist[1]) < 1e-4 + + @testset "Type Inference (liouvillian_generalized)" begin + N_c = 30 + N_trunc = 10 + tol = 1e-14 + + a = kron(destroy(N_c), qeye(2)) + sm = kron(qeye(N_c), sigmam()) + sp = sm' + sx = sm + sp + sz = sp * sm - sm * sp + + H = 1 * a' * a + 1 * sz / 2 + 0.5 * (a + a') * sx + + fields = [sqrt(0.01) * (a + a'), sqrt(0.01) * sx] + Tlist = [0, 0.01] + + @inferred liouvillian_generalized(H, fields, Tlist, N_trunc = N_trunc, tol = tol) + end end diff --git a/test/low_rank_dynamics.jl b/test/core-test/low_rank_dynamics.jl similarity index 95% rename from test/low_rank_dynamics.jl rename to test/core-test/low_rank_dynamics.jl index a83a84de..99dd3305 100644 --- a/test/low_rank_dynamics.jl +++ b/test/core-test/low_rank_dynamics.jl @@ -8,7 +8,7 @@ M = Nx * Ny + 1 # Define initial state - ϕ = Vector{QuantumObject{Vector{ComplexF64},KetQuantumObject}}(undef, M) + ϕ = Vector{QuantumObject{Vector{ComplexF64},KetQuantumObject,M - 1}}(undef, M) ϕ[1] = tensor(repeat([basis(2, 0)], N_modes)...) i = 1 for j in 1:N_modes @@ -29,7 +29,7 @@ B = Matrix(Diagonal([1 + 0im; zeros(M - 1)])) S = z' * z B = B / tr(S * B) - ρ = Qobj(z * B * z', dims = ones(Int, N_modes) * N_cut) + ρ = Qobj(z * B * z', dims = ntuple(i -> 1, Val(N_modes)) .* N_cut) # Define Hamiltonian and collapse operators Jx = 0.9 @@ -61,7 +61,6 @@ end opt = LRMesolveOptions( - alg = Tsit5(), err_max = 1e-3, p0 = 0.0, atol_inv = 1e-6, diff --git a/test/negativity_and_partial_transpose.jl b/test/core-test/negativity_and_partial_transpose.jl similarity index 68% rename from test/negativity_and_partial_transpose.jl rename to test/core-test/negativity_and_partial_transpose.jl index a72178ce..ba10db89 100644 --- a/test/negativity_and_partial_transpose.jl +++ b/test/core-test/negativity_and_partial_transpose.jl @@ -7,18 +7,23 @@ 1 -3 5 1 15 1 1 15 ]; - dims = [2, 2], + dims = (2, 2), ) Neg = negativity(rho, 1) @test Neg ≈ 0.25 @test negativity(rho, 2) ≈ Neg @test negativity(rho, 1; logarithmic = true) ≈ log2(2 * Neg + 1) @test_throws ArgumentError negativity(rho, 3) + + @testset "Type Inference (negativity)" begin + @inferred negativity(rho, 1) + @inferred negativity(rho, 1; logarithmic = true) + end end @testset "partial_transpose" begin # A (24 * 24)-matrix which contains number 1 ~ 576 - A_dense = Qobj(reshape(1:(24^2), (24, 24)), dims = [2, 3, 4]) + A_dense = Qobj(reshape(1:(24^2), (24, 24)), dims = (2, 3, 4)) A_sparse = dense_to_sparse(A_dense) PT = (true, false) for s1 in PT @@ -30,5 +35,10 @@ end end @test_throws ArgumentError partial_transpose(A_dense, [true]) + + @testset "Type Inference (partial_transpose)" begin + @inferred partial_transpose(A_dense, [true, false, true]) + @inferred partial_transpose(A_sparse, [true, false, true]) + end end end diff --git a/test/permutation.jl b/test/core-test/permutation.jl similarity index 82% rename from test/permutation.jl rename to test/core-test/permutation.jl index 3b28d9bf..6f089254 100644 --- a/test/permutation.jl +++ b/test/core-test/permutation.jl @@ -7,14 +7,13 @@ θ = atan(tg) U = sin(θ) κ2 = cos(θ) - κ1 = 0.0 κϕ = 1e-3 nth = 0.0 a = destroy(N) ad = create(N) H = -Δ * ad * a + G / 2 * (ad^2 + a^2) + U / 2 * (ad^2 * a^2) - c_ops = [√(κ2) * a^2, √(κ1 * (nth + 1)) * a, √(κ1 * nth) * ad, √(κϕ) * ad * a] + c_ops = [√(κ2) * a^2, √(κϕ) * ad * a] L = liouvillian(H, c_ops) P, L_bd, block_sizes = bdf(L) @@ -24,4 +23,8 @@ @test length(blocks_list) == 4 @test length(block_indices) == 4 @test sum(block_sizes .== 100) == 4 + + @testset "Type Inference (bdf)" begin + @inferred bdf(L) + end end diff --git a/test/progress_bar.jl b/test/core-test/progress_bar.jl similarity index 100% rename from test/progress_bar.jl rename to test/core-test/progress_bar.jl diff --git a/test/quantum_objects.jl b/test/core-test/quantum_objects.jl similarity index 64% rename from test/quantum_objects.jl rename to test/core-test/quantum_objects.jl index b1bd0d93..8ef46b78 100644 --- a/test/quantum_objects.jl +++ b/test/core-test/quantum_objects.jl @@ -1,28 +1,4 @@ @testset "Quantum Objects" verbose = true begin - @testset "Type Inference" begin - for T in [ComplexF32, ComplexF64] - N = 4 - a = rand(T, N) - @inferred QuantumObject{typeof(a),KetQuantumObject} Qobj(a) - for type in [Ket, OperatorKet] - @inferred Qobj(a, type = type) - end - - UnionType = Union{QuantumObject{Matrix{T},BraQuantumObject},QuantumObject{Matrix{T},OperatorQuantumObject}} - a = rand(T, 1, N) - @inferred UnionType Qobj(a) - for type in [Bra, OperatorBra] - @inferred Qobj(a, type = type) - end - - a = rand(T, N, N) - @inferred UnionType Qobj(a) - for type in [Operator, SuperOperator] - @inferred Qobj(a, type = type) - end - end - end - # ArgumentError: type is incompatible with vector or matrix @testset "ArgumentError" begin a = rand(ComplexF64, 2) @@ -51,19 +27,22 @@ # unsupported type of dims @testset "unsupported dims" begin - @test_throws ArgumentError Qobj(rand(2, 2), dims = 2) - @test_throws ArgumentError Qobj(rand(2, 2), dims = [2.0]) - @test_throws ArgumentError Qobj(rand(2, 2), dims = [2.0 + 0.0im]) - @test_throws DomainError Qobj(rand(2, 2), dims = [0]) - @test_throws DomainError Qobj(rand(2, 2), dims = [2, -2]) + @test_throws ArgumentError Qobj(rand(2, 2), dims = 2.0) + @test_throws ArgumentError Qobj(rand(2, 2), dims = 2.0 + 0.0im) + @test_throws DomainError Qobj(rand(2, 2), dims = 0) + @test_throws DomainError Qobj(rand(2, 2), dims = (2, -2)) + @test_logs ( + :warn, + "The argument dims should be a Tuple or a StaticVector for better performance. Try to use `dims = (2, 2)` or `dims = SVector(2, 2)` instead of `dims = [2, 2]`.", + ) Qobj(rand(4, 4), dims = [2, 2]) end @testset "Ket and Bra" begin N = 10 a = rand(ComplexF64, 10) # @test_logs (:warn, "The norm of the input data is not one.") QuantumObject(a) - @test_throws DimensionMismatch Qobj(a, dims = [2]) - @test_throws DimensionMismatch Qobj(a', dims = [2]) + @test_throws DimensionMismatch Qobj(a, dims = 2) + @test_throws DimensionMismatch Qobj(a', dims = 2) a2 = Qobj(a') a3 = Qobj(a) @test dag(a3) == a2 # Here we are also testing the dag function @@ -103,7 +82,7 @@ @test isoperket(a3) == false @test isoperbra(a3) == false @test isunitary(a3) == false - @test_throws DimensionMismatch Qobj(a, dims = [2]) + @test_throws DimensionMismatch Qobj(a, dims = 2) end @testset "OperatorKet and OperatorBra" begin @@ -137,8 +116,8 @@ @test L * ρ_ket ≈ -1im * (+(spre(H) * ρ_ket) - spost(H) * ρ_ket) @test (ρ_bra * L')' == L * ρ_ket @test sum((conj(ρ) .* ρ).data) ≈ dot(ρ_ket, ρ_ket) ≈ ρ_bra * ρ_ket - @test_throws DimensionMismatch Qobj(ρ_ket.data, type = OperatorKet, dims = [4]) - @test_throws DimensionMismatch Qobj(ρ_bra.data, type = OperatorBra, dims = [4]) + @test_throws DimensionMismatch Qobj(ρ_ket.data, type = OperatorKet, dims = 4) + @test_throws DimensionMismatch Qobj(ρ_bra.data, type = OperatorBra, dims = 4) end @testset "arithmetic" begin @@ -291,40 +270,115 @@ @test typeof(SparseMatrixCSC{ComplexF64}(Ms).data) == SparseMatrixCSC{ComplexF64,Int64} end + @testset "Type Inference (QuantumObject)" begin + for T in [ComplexF32, ComplexF64] + N = 4 + a = rand(T, N) + @inferred QuantumObject{typeof(a),KetQuantumObject} Qobj(a) + for type in [Ket, OperatorKet] + @inferred Qobj(a, type = type) + end + + UnionType = + Union{QuantumObject{Matrix{T},BraQuantumObject,1},QuantumObject{Matrix{T},OperatorQuantumObject,1}} + a = rand(T, 1, N) + @inferred UnionType Qobj(a) + for type in [Bra, OperatorBra] + @inferred Qobj(a, type = type) + end + + a = rand(T, N, N) + @inferred UnionType Qobj(a) + for type in [Operator, SuperOperator] + @inferred Qobj(a, type = type) + end + end + + @testset "Math Operation" begin + a = destroy(20) + σx = sigmax() + @inferred a + a + @inferred a + a' + @inferred a + 2 + @inferred 2 * a + @inferred a / 2 + @inferred a^2 + @inferred a .+ 2 + @inferred a .* 2 + @inferred a ./ 2 + @inferred a .^ 2 + @inferred a * a + @inferred a * a' + @inferred kron(a) + @inferred kron(a, σx) + @inferred kron(a, eye(2)) + end + end + + @testset "tensor" begin + σx = sigmax() + X3 = kron(σx, σx, σx) + @test tensor(σx) == kron(σx) + @test tensor(fill(σx, 3)...) == X3 + X_warn = @test_logs ( + :warn, + "`tensor(A)` or `kron(A)` with `A` is a `Vector` can hurt performance. Try to use `tensor(A...)` or `kron(A...)` instead.", + ) tensor(fill(σx, 3)) + @test X_warn == X3 + end + @testset "projection" begin N = 10 - a = fock(N, 3) - @test proj(a) ≈ proj(a') ≈ sparse(ket2dm(a)) ≈ projection(N, 3, 3) - @test isket(a') == false - @test isbra(a') == true - @test shape(a) == (N,) - @test shape(a') == (1, N) - @test norm(a) ≈ 1 - @test norm(a') ≈ 1 + ψ = fock(N, 3) + @test proj(ψ) ≈ proj(ψ') ≈ sparse(ket2dm(ψ)) ≈ projection(N, 3, 3) + @test isket(ψ') == false + @test isbra(ψ') == true + @test shape(ψ) == (N,) + @test shape(ψ') == (1, N) + @test norm(ψ) ≈ 1 + @test norm(ψ') ≈ 1 + + @testset "Type Inference (proj)" begin + @inferred proj(ψ) + @inferred proj(ψ') + end end @testset "dot product" begin ψ = rand_ket(10) @test dot(ψ, ψ) ≈ ψ' * ψ ≈ norm(ψ) ≈ 1.0 + + @testset "Type Inference (dot)" begin + @inferred dot(ψ, ψ) + @inferred ψ' * ψ + @inferred norm(ψ) + end end @testset "normalization" begin # normalize, normalize!, unit N = 10 - a = Qobj(rand(ComplexF64, N)) - M = a * a' - @test (norm(a) ≈ 1) == false + ψ = Qobj(rand(ComplexF64, N)) + M = ψ * ψ' + @test (norm(ψ) ≈ 1) == false @test (norm(M) ≈ 1) == false - @test (norm(unit(a)) ≈ 1) == true + @test (norm(unit(ψ)) ≈ 1) == true @test (norm(unit(M)) ≈ 1) == true - @test (norm(a) ≈ 1) == false # Again, to be sure that it is still non-normalized + @test (norm(ψ) ≈ 1) == false # Again, to be sure that it is still non-normalized @test (norm(M) ≈ 1) == false # Again, to be sure that it is still non-normalized - normalize!(a) + normalize!(ψ) normalize!(M) - @test (norm(a) ≈ 1) == true + @test (norm(ψ) ≈ 1) == true @test (norm(M) ≈ 1) == true - @test M ≈ a * a' + @test M ≈ ψ * ψ' @test (unit(qeye(N)) ≈ (qeye(N) / N)) == true + + @testset "Type Inference (normalize)" begin + ψ = Qobj(rand(ComplexF64, N)) + M = ket2dm(ψ) + @inferred normalize(ψ) + @inferred normalize(M) + end end @testset "expectation value" begin @@ -338,6 +392,26 @@ ψ = fock(N, 3) @test norm(ψ' * a) ≈ 2 @test expect(a' * a, ψ' * a) ≈ 16 + + ρ = rand_dm(N) + @test expect(a, ρ) ≈ tr(a * ρ) + @test variance(a, ρ) ≈ tr(a^2 * ρ) - tr(a * ρ)^2 + + # when input is a vector of states + xlist = [1.0, 1.0im, -1.0, -1.0im] + ψlist = [normalize!(basis(N, 4) + x * basis(N, 3)) for x in xlist] + @test all(expect(a', ψlist) .≈ xlist) + + @testset "Type Inference (expect)" begin + @inferred expect(a, ψ) + @inferred expect(a, ψ') + @inferred variance(a, ψ) + @inferred variance(a, ψ') + @inferred expect(a, ρ) + @inferred variance(a, ρ) + @inferred expect(a, ψlist) + @inferred variance(a, ψlist) + end end @testset "get coherence" begin @@ -347,6 +421,11 @@ ρ = ket2dm(ψ) α, δρ = get_coherence(ρ) @test isapprox(abs(α), 3, atol = 1e-5) && abs2(δρ[1, 1]) > 0.999 + + @testset "Type Inference (get_coherence)" begin + @inferred get_coherence(ψ) + @inferred get_coherence(ρ) + end end @testset "SVD and Schatten p-norm" begin @@ -358,6 +437,13 @@ @test svdvals(vs)[1] ≈ √(vs' * vs) @test norm(Md, 1) ≈ sum(sqrt, abs.(eigenenergies(Md' * Md))) atol = 1e-6 @test norm(Ms, 1) ≈ sum(sqrt, abs.(eigenenergies(Ms' * Ms))) atol = 1e-6 + + @testset "Type Inference (SVD and Schatten p-norm)" begin + @inferred svdvals(vd) + @inferred svdvals(vs) + @inferred norm(Md, 1) + @inferred norm(Ms, 1) + end end @testset "purity" begin @@ -371,6 +457,13 @@ @test purity(ψd) ≈ norm(ψd)^2 ≈ 1.0 @test purity(ρ1) ≈ 1.0 @test (1.0 / N) <= purity(ρ2) <= 1.0 + + @testset "Type Inference (purity)" begin + @inferred purity(ψ) + @inferred purity(ψd) + @inferred purity(ρ1) + @inferred purity(ρ2) + end end @testset "trace distance" begin @@ -383,6 +476,13 @@ @test tracedist(ρz0, ψz1) ≈ 1.0 @test tracedist(ψz1, ρz0) ≈ 1.0 @test tracedist(ρz0, ρz1) ≈ 1.0 + + @testset "Type Inference (trace distance)" begin + @inferred tracedist(ψz0, ψx0) + @inferred tracedist(ρz0, ψz1) + @inferred tracedist(ψz1, ρz0) + @inferred tracedist(ρz0, ρz1) + end end @testset "sqrt and fidelity" begin @@ -394,6 +494,13 @@ @test sqrtm(M0) ≈ sqrtm(sparse_to_dense(M0)) @test isapprox(fidelity(M0, M1), fidelity(ψ1, M0); atol = 1e-6) @test isapprox(fidelity(ψ1, ψ2), fidelity(ket2dm(ψ1), ket2dm(ψ2)); atol = 1e-6) + + @testset "Type Inference (sqrt and fidelity)" begin + @inferred sqrtm(M0) + @inferred fidelity(M0, M1) + @inferred fidelity(ψ1, M0) + @inferred fidelity(ψ1, ψ2) + end end @testset "log, exp, sinm, cosm" begin @@ -445,6 +552,110 @@ ρ2 = dense_to_sparse(ρ1) @test tidyup(ρ2, tol) != ρ2 @test dense_to_sparse(tidyup(ρ1, tol)) == tidyup(ρ2, tol) + + @testset "Type Inference (tidyup)" begin + @inferred tidyup(ψ1, tol) + @inferred tidyup(ρ1, tol) + @inferred tidyup(ψ2, tol) + @inferred tidyup(ρ2, tol) + end + end + + @testset "ptrace" begin + g = fock(2, 1) + e = fock(2, 0) + α = sqrt(0.7) + β = sqrt(0.3) * 1im + ψ = α * kron(g, e) + β * kron(e, g) + + ρ1 = ptrace(ψ, 1) + ρ2 = ptrace(ψ, 2) + @test ρ1.data ≈ [0.3 0.0; 0.0 0.7] atol = 1e-10 + @test ρ2.data ≈ [0.7 0.0; 0.0 0.3] atol = 1e-10 + + ψ_d = ψ' + + ρ1 = ptrace(ψ_d, 1) + ρ2 = ptrace(ψ_d, 2) + @test ρ1.data ≈ [0.3 0.0; 0.0 0.7] atol = 1e-10 + @test ρ2.data ≈ [0.7 0.0; 0.0 0.3] atol = 1e-10 + + ρ = ket2dm(ψ) + ρ1 = ptrace(ρ, 1) + ρ2 = ptrace(ρ, 2) + @test ρ1.data ≈ [0.3 0.0; 0.0 0.7] atol = 1e-10 + @test ρ2.data ≈ [0.7 0.0; 0.0 0.3] atol = 1e-10 + + ψ1 = normalize(g + 1im * e) + ψ2 = normalize(g + e) + ρ1 = ket2dm(ψ1) + ρ2 = ket2dm(ψ2) + ρ = kron(ρ1, ρ2) + ρ1_ptr = ptrace(ρ, 1) + ρ2_ptr = ptrace(ρ, 2) + @test ρ1.data ≈ ρ1_ptr.data atol = 1e-10 + @test ρ2.data ≈ ρ2_ptr.data atol = 1e-10 + + ψlist = [rand_ket(2), rand_ket(3), rand_ket(4), rand_ket(5)] + ρlist = [rand_dm(2), rand_dm(3), rand_dm(4), rand_dm(5)] + ψtotal = tensor(ψlist...) + ρtotal = tensor(ρlist...) + sel_tests = [ + SVector{0,Int}(), + 1, + 2, + 3, + 4, + (1, 2), + (1, 3), + (1, 4), + (2, 3), + (2, 4), + (3, 4), + (1, 2, 3), + (1, 2, 4), + (1, 3, 4), + (2, 3, 4), + (1, 2, 3, 4), + ] + for sel in sel_tests + if length(sel) == 0 + @test ptrace(ψtotal, sel) ≈ 1.0 + @test ptrace(ρtotal, sel) ≈ 1.0 + else + @test ptrace(ψtotal, sel) ≈ tensor([ket2dm(ψlist[i]) for i in sel]...) + @test ptrace(ρtotal, sel) ≈ tensor([ρlist[i] for i in sel]...) + end + end + @test ptrace(ψtotal, (1, 3, 4)) ≈ ptrace(ψtotal, (4, 3, 1)) # check sort of sel + @test ptrace(ρtotal, (1, 3, 4)) ≈ ptrace(ρtotal, (3, 1, 4)) # check sort of sel + @test_logs ( + :warn, + "The argument sel should be a Tuple or a StaticVector for better performance. Try to use `sel = (1, 2)` or `sel = SVector(1, 2)` instead of `sel = [1, 2]`.", + ) ptrace(ψtotal, [1, 2]) + @test_logs ( + :warn, + "The argument sel should be a Tuple or a StaticVector for better performance. Try to use `sel = (1, 2)` or `sel = SVector(1, 2)` instead of `sel = [1, 2]`.", + ) ptrace(ρtotal, [1, 2]) + @test_throws ArgumentError ptrace(ψtotal, 0) + @test_throws ArgumentError ptrace(ψtotal, 5) + @test_throws ArgumentError ptrace(ψtotal, (0, 2)) + @test_throws ArgumentError ptrace(ψtotal, (2, 5)) + @test_throws ArgumentError ptrace(ψtotal, (2, 2, 3)) + @test_throws ArgumentError ptrace(ρtotal, 0) + @test_throws ArgumentError ptrace(ρtotal, 5) + @test_throws ArgumentError ptrace(ρtotal, (0, 2)) + @test_throws ArgumentError ptrace(ρtotal, (2, 5)) + @test_throws ArgumentError ptrace(ρtotal, (2, 2, 3)) + + @testset "Type Inference (ptrace)" begin + @inferred ptrace(ρ, 1) + @inferred ptrace(ρ, 2) + @inferred ptrace(ψ_d, 1) + @inferred ptrace(ψ_d, 2) + @inferred ptrace(ψ, 1) + @inferred ptrace(ψ, 2) + end end @testset "permute" begin @@ -452,17 +663,17 @@ ket_b = Qobj(rand(ComplexF64, 3)) ket_c = Qobj(rand(ComplexF64, 4)) ket_d = Qobj(rand(ComplexF64, 5)) - ket_bdca = permute(tensor(ket_a, ket_b, ket_c, ket_d), [2, 4, 3, 1]) + ket_bdca = permute(tensor(ket_a, ket_b, ket_c, ket_d), (2, 4, 3, 1)) bra_a = ket_a' bra_b = ket_b' bra_c = ket_c' bra_d = ket_d' - bra_bdca = permute(tensor(bra_a, bra_b, bra_c, bra_d), [2, 4, 3, 1]) + bra_bdca = permute(tensor(bra_a, bra_b, bra_c, bra_d), (2, 4, 3, 1)) op_a = Qobj(rand(ComplexF64, 2, 2)) op_b = Qobj(rand(ComplexF64, 3, 3)) op_c = Qobj(rand(ComplexF64, 4, 4)) op_d = Qobj(rand(ComplexF64, 5, 5)) - op_bdca = permute(tensor(op_a, op_b, op_c, op_d), [2, 4, 3, 1]) + op_bdca = permute(tensor(op_a, op_b, op_c, op_d), (2, 4, 3, 1)) correct_dims = [3, 5, 4, 2] wrong_order1 = [1] wrong_order2 = [2, 3, 4, 5] @@ -481,5 +692,11 @@ @test_throws ArgumentError permute(bra_bdca, wrong_order2) @test_throws ArgumentError permute(op_bdca, wrong_order1) @test_throws ArgumentError permute(op_bdca, wrong_order2) + + @testset "Type Inference (permute)" begin + @inferred permute(ket_bdca, (2, 4, 3, 1)) + @inferred permute(bra_bdca, (2, 4, 3, 1)) + @inferred permute(op_bdca, (2, 4, 3, 1)) + end end end diff --git a/test/states_and_operators.jl b/test/core-test/states_and_operators.jl similarity index 76% rename from test/states_and_operators.jl rename to test/core-test/states_and_operators.jl index e57a35f3..573dedd0 100644 --- a/test/states_and_operators.jl +++ b/test/core-test/states_and_operators.jl @@ -1,7 +1,7 @@ @testset "States and Operators" verbose = true begin @testset "zero state" begin v1 = zero_ket(4) - v2 = zero_ket([2, 2]) + v2 = zero_ket((2, 2)) @test size(v1) == (4,) @test size(v2) == (4,) @test v1.data == v2.data @@ -14,8 +14,8 @@ @testset "fock state" begin # fock, basis, and fock_dm - @test fock_dm(4; dims = [2, 2], sparse = true) ≈ ket2dm(basis(4; dims = [2, 2])) - @test_throws DimensionMismatch fock(4; dims = [2]) + @test fock_dm(4; dims = (2, 2), sparse = true) ≈ ket2dm(basis(4; dims = (2, 2))) + @test_throws DimensionMismatch fock(4; dims = 2) end @testset "coherent state" begin @@ -51,7 +51,7 @@ @testset "maximally mixed state" begin ρ1 = maximally_mixed_dm(4) - ρ2 = maximally_mixed_dm([2, 2]) + ρ2 = maximally_mixed_dm((2, 2)) @test size(ρ1) == (4, 4) @test size(ρ2) == (4, 4) @test tr(ρ1) ≈ 1.0 @@ -67,7 +67,7 @@ @testset "random state" begin # rand_ket and rand_dm ψ = rand_ket(10) - ρ_AB = rand_dm([2, 2]) + ρ_AB = rand_dm((2, 2)) ρ_A = ptrace(ρ_AB, 1) ρ_B = ptrace(ρ_AB, 2) rank = 5 @@ -173,7 +173,7 @@ @testset "quantum Fourier transform" begin N = 9 - dims = [3, 3] + dims = (3, 3) ω = exp(2.0im * π / N) x = Qobj(rand(ComplexF64, N)) ψx = basis(N, 0; dims = dims) @@ -192,8 +192,8 @@ @testset "random unitary" begin U1 = rand_unitary(20) U2 = rand_unitary(20, :exp) - U3 = rand_unitary([5, 5]) - U4 = rand_unitary([5, 5], :exp) + U3 = rand_unitary((5, 5)) + U4 = rand_unitary((5, 5), :exp) @test isunitary(U1) @test isunitary(U2) @test isunitary(U3) @@ -202,10 +202,6 @@ @test U3.dims == U4.dims == [5, 5] @test_throws ArgumentError rand_unitary(20, :wrong) - @testset "Type Inference" begin - @inferred rand_unitary(10, :haar) - @inferred rand_unitary(10, :exp) - end end @testset "Spin-j operators" begin @@ -268,7 +264,7 @@ # also verify the equation in: # Robert Jones, Spin Coherent States and Statistical Physics, page 3 - s = 3.5 + s = 1.5 θ1 = π * rand() θ2 = π * rand() ϕ1 = 2 * π * rand() @@ -286,14 +282,14 @@ # test commutation relations for fermionic creation and annihilation operators sites = 4 SIZE = 2^sites - dims = fill(2, sites) + dims = ntuple(i -> 2, Val(sites)) Q_iden = Qobj(sparse((1.0 + 0.0im) * LinearAlgebra.I, SIZE, SIZE); dims = dims) Q_zero = Qobj(spzeros(ComplexF64, SIZE, SIZE); dims = dims) - for i in 0:(sites-1) + for i in 1:sites d_i = fdestroy(sites, i) @test d_i' ≈ fcreate(sites, i) - for j in 0:(sites-1) + for j in 1:sites d_j = fdestroy(sites, j) if i == j @@ -305,16 +301,27 @@ @test commutator(d_i, d_j; anti = true) ≈ Q_zero end end + zero = zero_ket((2, 2)) + vac = tensor(basis(2, 0), basis(2, 0)) + d1 = sigmap() ⊗ qeye(2) + d2 = sigmaz() ⊗ sigmap() + @test d1 * vac == zero + @test d2 * vac == zero + @test d1' * vac == tensor(basis(2, 1), basis(2, 0)) + @test d2' * vac == tensor(basis(2, 0), basis(2, 1)) + @test d1' * d2' * vac == tensor(basis(2, 1), basis(2, 1)) + @test d1' * d1' * d2' * vac == zero + @test d2' * d1' * d2' * vac == zero @test_throws ArgumentError fdestroy(0, 0) - @test_throws ArgumentError fdestroy(sites, -1) - @test_throws ArgumentError fdestroy(sites, sites) + @test_throws ArgumentError fdestroy(sites, 0) + @test_throws ArgumentError fdestroy(sites, sites + 1) end @testset "identity operator" begin I_op1 = qeye(4) - I_op2 = qeye(4, dims = [2, 2]) + I_op2 = qeye(4, dims = (2, 2)) I_su1 = qeye(4, type = SuperOperator) - I_su2 = qeye(4, type = SuperOperator, dims = [2]) + I_su2 = qeye(4, type = SuperOperator, dims = 2) @test isunitary(I_op1) == true @test isunitary(I_op2) == true @test isunitary(I_su1) == false @@ -326,17 +333,17 @@ @test (I_op2 == I_su1) == false @test (I_op2 == I_su2) == false @test (I_su1 == I_su2) == true - @test_throws DimensionMismatch qeye(4, dims = [2]) + @test_throws DimensionMismatch qeye(4, dims = 2) @test_throws DimensionMismatch qeye(2, type = SuperOperator) - @test_throws DimensionMismatch qeye(4, type = SuperOperator, dims = [2, 2]) + @test_throws DimensionMismatch qeye(4, type = SuperOperator, dims = (2, 2)) end @testset "superoperators" begin # spre, spost, and sprepost Xs = sigmax() Xd = sparse_to_dense(Xs) - A_wrong1 = Qobj(rand(4, 4), dims = [4]) - A_wrong2 = Qobj(rand(4, 4), dims = [2, 2]) + A_wrong1 = Qobj(rand(4, 4), dims = 4) + A_wrong2 = Qobj(rand(4, 4), dims = (2, 2)) A_wrong3 = Qobj(rand(3, 3)) @test (typeof(spre(Xd).data) <: SparseMatrixCSC) == true @test (typeof(spre(Xs).data) <: SparseMatrixCSC) == true @@ -349,4 +356,90 @@ @test_throws DimensionMismatch sprepost(A_wrong1, A_wrong2) @test_throws DimensionMismatch sprepost(A_wrong1, A_wrong3) end + + @testset "Type Inference (States)" begin + @inferred zero_ket(4) + @inferred zero_ket((2, 2)) + + @inferred fock(4, 1, dims = (2, 2), sparse = Val(true)) + @inferred fock(4, 1, dims = (2, 2), sparse = Val(false)) + @inferred basis(4, 1, dims = (2, 2)) + + @inferred coherent(5, 0.25im) + + @inferred rand_ket(10) + @inferred rand_ket((2, 5)) + + @inferred fock_dm(4; dims = (2, 2), sparse = Val(true)) + @inferred fock_dm(4; dims = (2, 2), sparse = Val(false)) + + @inferred coherent_dm(5, 0.25im) + + @inferred thermal_dm(10, 0.123) + @inferred thermal_dm(10, 0.123; sparse = Val(true)) + + @inferred maximally_mixed_dm(4) + @inferred maximally_mixed_dm((2, 2)) + + @inferred rand_dm((2, 2)) + @inferred rand_dm(10, rank = 5) + + @inferred spin_state(3.5, 1.5) + @inferred spin_coherent(0.5, π, 2π) + + @inferred bell_state(Val(0), Val(0)) + @inferred bell_state(Val(0), Val(1)) + @inferred bell_state(Val(1), Val(0)) + @inferred bell_state(Val(1), Val(1)) + @inferred singlet_state() + @inferred triplet_states() + @inferred w_state(Val(2)) + + @inferred ghz_state(Val(2)) + @inferred ghz_state(Val(3)) + @inferred ghz_state(Val(3); d = 3) + end + + @testset "Type Inference (Operators)" begin + @inferred rand_unitary(10, Val(:haar)) + @inferred rand_unitary(10, Val(:exp)) + + a = destroy(20) + a_d = create(20) + @inferred commutator(a, a_d) + + @inferred destroy(20) + @inferred create(20) + @inferred num(20) + @inferred displace(20, 0.5 + 0.5im) + @inferred squeeze(20, 0.5 + 0.5im) + @inferred position(20) + @inferred momentum(20) + + @inferred phase(20, 0.5) + + @inferred jmat(2.5) + @inferred jmat(2.5, Val(:x)) + @inferred jmat(2.5, Val(:y)) + @inferred jmat(2.5, Val(:z)) + + @inferred sigmap() + @inferred sigmam() + @inferred sigmax() + @inferred sigmay() + @inferred sigmaz() + + @inferred eye(20) + + @inferred fdestroy(Val(10), 4) + @inferred fcreate(Val(10), 4) + + @inferred projection(20, 5, 3) + + @inferred tunneling(20, 1, sparse = Val(false)) + @inferred tunneling(20, 1, sparse = Val(true)) + + @inferred qft(20) + @inferred qft((2, 10)) + end end diff --git a/test/steady_state.jl b/test/core-test/steady_state.jl similarity index 51% rename from test/steady_state.jl rename to test/core-test/steady_state.jl index 3ee36efd..8bc4ee0c 100644 --- a/test/steady_state.jl +++ b/test/core-test/steady_state.jl @@ -22,7 +22,7 @@ ρ_ss = steadystate(H, c_ops, solver = solver) @test tracedist(rho_me, ρ_ss) < 1e-4 - solver = SteadyStateLinearSolver(alg = KrylovJL_GMRES()) + solver = SteadyStateLinearSolver() ρ_ss = steadystate(H, c_ops, solver = solver) @test tracedist(rho_me, ρ_ss) < 1e-4 @@ -30,18 +30,57 @@ ρ_ss = steadystate(H, c_ops, solver = solver) @test tracedist(rho_me, ρ_ss) < 1e-4 + @testset "Type Inference (steadystate)" begin + L = liouvillian(H, c_ops) + + solver = SteadyStateODESolver() + @inferred steadystate(H, psi0, t_l[end], c_ops, solver = solver) + @inferred steadystate(L, psi0, t_l[end], solver = solver) + + solver = SteadyStateDirectSolver() + @inferred steadystate(H, c_ops, solver = solver) + @inferred steadystate(L, solver = solver) + + solver = SteadyStateLinearSolver() + @inferred steadystate(H, c_ops, solver = solver) + @inferred steadystate(L, solver = solver) + + solver = SteadyStateLinearSolver() + @inferred steadystate(H, c_ops, solver = solver) + @inferred steadystate(L, solver = solver) + + solver = SteadyStateEigenSolver() + @inferred steadystate(H, c_ops, solver = solver) + @inferred steadystate(L, solver = solver) + + @inferred steadystate(H, c_ops) + @inferred steadystate(L) + end + H = a_d * a H_t = 0.1 * (a + a_d) c_ops = [sqrt(0.1) * a] e_ops = [a_d * a] psi0 = fock(N, 3) t_l = LinRange(0, 100 * 2π, 1000) - H_t_f = TimeDependentOperatorSum([(t, p) -> sin(t)], [liouvillian(H_t)]) - sol_me = mesolve(H, psi0, t_l, c_ops, e_ops = e_ops, H_t = H_t_f, alg = Vern7(), progress_bar = Val(false)) + H_t_f = TimeDependentOperatorSum((((t, p) -> sin(t)),), [H_t]) # It will be converted to liouvillian internally + sol_me = mesolve(H, psi0, t_l, c_ops, e_ops = e_ops, H_t = H_t_f, progress_bar = Val(false)) ρ_ss1 = steadystate_floquet(H, -1im * 0.5 * H_t, 1im * 0.5 * H_t, 1, c_ops, solver = SSFloquetLinearSystem())[1] ρ_ss2 = steadystate_floquet(H, -1im * 0.5 * H_t, 1im * 0.5 * H_t, 1, c_ops, solver = SSFloquetEffectiveLiouvillian()) @test abs(sum(sol_me.expect[1, end-100:end]) / 101 - expect(e_ops[1], ρ_ss1)) < 1e-3 @test abs(sum(sol_me.expect[1, end-100:end]) / 101 - expect(e_ops[1], ρ_ss2)) < 1e-3 + + @testset "Type Inference (steadystate_floquet)" begin + @inferred steadystate_floquet(H, -1im * 0.5 * H_t, 1im * 0.5 * H_t, 1, c_ops, solver = SSFloquetLinearSystem()) + @inferred steadystate_floquet( + H, + -1im * 0.5 * H_t, + 1im * 0.5 * H_t, + 1, + c_ops, + solver = SSFloquetEffectiveLiouvillian(), + ) + end end diff --git a/test/time_evolution_and_partial_trace.jl b/test/core-test/time_evolution.jl similarity index 64% rename from test/time_evolution_and_partial_trace.jl rename to test/core-test/time_evolution.jl index 214126ee..25d43b29 100644 --- a/test/time_evolution_and_partial_trace.jl +++ b/test/core-test/time_evolution.jl @@ -16,8 +16,6 @@ sol3 = sesolve(H, psi0, t_l, e_ops = e_ops, saveat = t_l, progress_bar = Val(false)) sol_string = sprint((t, s) -> show(t, "text/plain", s), sol) @test sum(abs.(sol.expect[1, :] .- sin.(η * t_l) .^ 2)) / length(t_l) < 0.1 - @test ptrace(sol.states[end], 1) ≈ ptrace(ket2dm(sol.states[end]), 1) - @test ptrace(sol.states[end]', 1) ≈ ptrace(sol.states[end], 1) @test length(sol.states) == 1 @test size(sol.expect) == (length(e_ops), length(t_l)) @test length(sol2.states) == length(t_l) @@ -35,16 +33,16 @@ "reltol = $(sol.reltol)\n" @testset "Type Inference sesolve" begin - if VERSION >= v"1.10" - @inferred sesolveProblem(H, psi0, t_l) - @inferred sesolve(H, psi0, t_l, e_ops = e_ops, progress_bar = Val(false)) - @inferred sesolve(H, psi0, t_l, progress_bar = Val(false)) - @inferred sesolve(H, psi0, t_l, e_ops = e_ops, saveat = t_l, progress_bar = Val(false)) - end + @inferred sesolveProblem(H, psi0, t_l, progress_bar = Val(false)) + @inferred sesolveProblem(H, psi0, [0, 10], progress_bar = Val(false)) + @inferred sesolveProblem(H, Qobj(zeros(Int64, N * 2); dims = (N, 2)), t_l, progress_bar = Val(false)) + @inferred sesolve(H, psi0, t_l, e_ops = e_ops, progress_bar = Val(false)) + @inferred sesolve(H, psi0, t_l, progress_bar = Val(false)) + @inferred sesolve(H, psi0, t_l, e_ops = e_ops, saveat = t_l, progress_bar = Val(false)) end end - @testset "mesolve and mcsolve" begin + @testset "mesolve, mcsolve, and ssesolve" begin N = 10 a = destroy(N) a_d = a' @@ -57,9 +55,19 @@ sol_me2 = mesolve(H, psi0, t_l, c_ops, progress_bar = Val(false)) sol_me3 = mesolve(H, psi0, t_l, c_ops, e_ops = e_ops, saveat = t_l, progress_bar = Val(false)) sol_mc = mcsolve(H, psi0, t_l, c_ops, n_traj = 500, e_ops = e_ops, progress_bar = Val(false)) + sol_sse = ssesolve(H, psi0, t_l, c_ops, n_traj = 500, e_ops = e_ops, progress_bar = Val(false)) + sol_mc_states = mcsolve(H, psi0, t_l, c_ops, n_traj = 500, saveat = t_l, progress_bar = Val(false)) + + ρt_mc = [ket2dm.(normalize.(states)) for states in sol_mc_states.states] + expect_mc_states = mapreduce(states -> expect.(Ref(e_ops[1]), states), hcat, ρt_mc) + expect_mc_states_mean = sum(expect_mc_states, dims = 2) / size(expect_mc_states, 2) + sol_me_string = sprint((t, s) -> show(t, "text/plain", s), sol_me) sol_mc_string = sprint((t, s) -> show(t, "text/plain", s), sol_mc) + sol_sse_string = sprint((t, s) -> show(t, "text/plain", s), sol_sse) @test sum(abs.(sol_mc.expect .- sol_me.expect)) / length(t_l) < 0.1 + @test sum(abs.(sol_sse.expect .- sol_me.expect)) / length(t_l) < 0.1 + @test sum(abs.(vec(expect_mc_states_mean) .- vec(sol_me.expect))) / length(t_l) < 0.1 @test length(sol_me.states) == 1 @test size(sol_me.expect) == (length(e_ops), length(t_l)) @test length(sol_me2.states) == length(t_l) @@ -85,19 +93,45 @@ "ODE alg.: $(sol_mc.alg)\n" * "abstol = $(sol_mc.abstol)\n" * "reltol = $(sol_mc.reltol)\n" + @test sol_sse_string == + "Solution of quantum trajectories\n" * + "(converged: $(sol_sse.converged))\n" * + "--------------------------------\n" * + "num_trajectories = $(sol_sse.n_traj)\n" * + "num_states = $(length(sol_sse.states[1]))\n" * + "num_expect = $(size(sol_sse.expect, 1))\n" * + "SDE alg.: $(sol_sse.alg)\n" * + "abstol = $(sol_sse.abstol)\n" * + "reltol = $(sol_sse.reltol)\n" @testset "Type Inference mesolve" begin - if VERSION >= v"1.10" - @inferred mesolveProblem(H, psi0, t_l, c_ops, e_ops = e_ops, progress_bar = Val(false)) - @inferred mesolve(H, psi0, t_l, c_ops, e_ops = e_ops, progress_bar = Val(false)) - @inferred mesolve(H, psi0, t_l, c_ops, progress_bar = Val(false)) - @inferred mesolve(H, psi0, t_l, c_ops, e_ops = e_ops, saveat = t_l, progress_bar = Val(false)) - end + @inferred mesolveProblem(H, psi0, t_l, c_ops, e_ops = e_ops, progress_bar = Val(false)) + @inferred mesolveProblem(H, psi0, [0, 10], c_ops, e_ops = e_ops, progress_bar = Val(false)) + @inferred mesolveProblem(H, Qobj(zeros(Int64, N)), t_l, c_ops, e_ops = e_ops, progress_bar = Val(false)) + @inferred mesolve(H, psi0, t_l, c_ops, e_ops = e_ops, progress_bar = Val(false)) + @inferred mesolve(H, psi0, t_l, c_ops, progress_bar = Val(false)) + @inferred mesolve(H, psi0, t_l, c_ops, e_ops = e_ops, saveat = t_l, progress_bar = Val(false)) end @testset "Type Inference mcsolve" begin + @inferred mcsolveEnsembleProblem( + H, + psi0, + t_l, + c_ops, + n_traj = 500, + e_ops = e_ops, + progress_bar = Val(false), + ) + @inferred mcsolve(H, psi0, t_l, c_ops, n_traj = 500, e_ops = e_ops, progress_bar = Val(false)) + @inferred mcsolve(H, psi0, t_l, c_ops, n_traj = 500, progress_bar = Val(true)) + @inferred mcsolve(H, psi0, [0, 10], c_ops, n_traj = 500, progress_bar = Val(false)) + @inferred mcsolve(H, Qobj(zeros(Int64, N)), t_l, c_ops, n_traj = 500, progress_bar = Val(false)) + end + + @testset "Type Inference ssesolve" begin if VERSION >= v"1.10" - @inferred mcsolveEnsembleProblem( + @inferred ssesolveEnsembleProblem( H, psi0, t_l, @@ -106,8 +140,8 @@ e_ops = e_ops, progress_bar = Val(false), ) - @inferred mcsolve(H, psi0, t_l, c_ops, n_traj = 500, e_ops = e_ops, progress_bar = Val(false)) - @inferred mcsolve(H, psi0, t_l, c_ops, n_traj = 500, progress_bar = Val(true)) + @inferred ssesolve(H, psi0, t_l, c_ops, n_traj = 500, e_ops = e_ops, progress_bar = Val(false)) + @inferred ssesolve(H, psi0, t_l, c_ops, n_traj = 500, progress_bar = Val(true)) end end end diff --git a/test/wigner.jl b/test/core-test/wigner.jl similarity index 71% rename from test/wigner.jl rename to test/core-test/wigner.jl index 3a4d3ee2..ae30b188 100644 --- a/test/wigner.jl +++ b/test/core-test/wigner.jl @@ -20,4 +20,11 @@ wig2 = maximum(wig) * reshape(kron(wig_tmp1, wig_tmp2), 300, 300) @test sqrt(sum(abs.(wig2 .- wig)) / length(wig)) < 0.1 + + @testset "Type Inference (wigner)" begin + @inferred wigner(ψ, xvec, yvec, solver = WignerLaguerre(tol = 1e-6)) + @inferred wigner(ρ, xvec, yvec, solver = WignerLaguerre(parallel = false)) + @inferred wigner(ρ, xvec, yvec, solver = WignerLaguerre(parallel = true)) + @inferred wigner(ψ, xvec, yvec, solver = WignerClenshaw()) + end end diff --git a/test/cuda_ext.jl b/test/ext-test/cuda_ext.jl similarity index 98% rename from test/cuda_ext.jl rename to test/ext-test/cuda_ext.jl index 1653af59..f8c7f1f8 100644 --- a/test/cuda_ext.jl +++ b/test/ext-test/cuda_ext.jl @@ -1,5 +1,6 @@ using CUDA using CUDA.CUSPARSE +CUDA.allowscalar(false) # Avoid unexpected scalar indexing QuantumToolbox.about() CUDA.versioninfo() diff --git a/test/runtests.jl b/test/runtests.jl index 4854e8f3..70c361cd 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -22,24 +22,24 @@ core_tests = [ "quantum_objects.jl", "states_and_operators.jl", "steady_state.jl", - "time_evolution_and_partial_trace.jl", + "time_evolution.jl", "wigner.jl", ] -if ((GROUP == "All") || (GROUP == "Code-Quality")) && (VERSION >= v"1.9") +if (GROUP == "All") || (GROUP == "Code-Quality") Pkg.add(["Aqua", "JET"]) - include(joinpath(testdir, "code_quality.jl")) + include(joinpath(testdir, "core-test", "code_quality.jl")) end if (GROUP == "All") || (GROUP == "Core") QuantumToolbox.about() for test in core_tests - include(joinpath(testdir, test)) + include(joinpath(testdir, "core-test", test)) end end if (GROUP == "CUDA_Ext")# || (GROUP == "All") Pkg.add("CUDA") - include(joinpath(testdir, "cuda_ext.jl")) + include(joinpath(testdir, "ext-test", "cuda_ext.jl")) end